diff --git a/.gitignore b/.gitignore
index 5f32962..0af181c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
# Godot 4+ specific ignores
.godot/
/android/
-/output
diff --git a/addons/godot_ai/LICENSE b/addons/godot_ai/LICENSE
deleted file mode 100644
index 7806d22..0000000
--- a/addons/godot_ai/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2025 Godot AI contributors
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/addons/godot_ai/README.md b/addons/godot_ai/README.md
deleted file mode 100644
index 88b524f..0000000
--- a/addons/godot_ai/README.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# Godot AI
-
-Connect AI assistants to a live Godot editor via the [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP).
-
-Godot AI bridges Claude Code, Codex, Antigravity, DeepSeek Harness, and other MCP clients with your editor — inspect scenes, create nodes, modify properties, run tests, search project files, and more, all from a prompt.
-
-## Quick Start
-
-1. For a fresh install, verify and extract the signed v4 exact-tree archive;
- for v3 migration, follow `docs/v4-migration.md` in the repository
-2. Enable the plugin: **Project > Project Settings > Plugins > Godot AI**
-3. Pick your MCP client in the **Godot AI** dock and press **Configure**
-
-The plugin auto-starts the MCP server and connects over WebSocket. No manual configuration required.
-
-## Requirements
-
-- Godot 4.7+ within the 4.x line
-- [uv](https://docs.astral.sh/uv/) (used to install the Python server)
-
- Install uv
-
- **macOS / Linux:**
- ```bash
- curl -LsSf https://astral.sh/uv/install.sh | sh
- ```
-
- **Windows (PowerShell):**
- ```powershell
- powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
- ```
-
- **Homebrew (macOS / Linux):**
- ```bash
- brew install uv
- ```
-
- **pipx:**
- ```bash
- pipx install uv
- ```
-
- See the [uv install docs](https://docs.astral.sh/uv/getting-started/installation/) for more options.
-
-
-- An MCP client ([Claude Code](https://docs.anthropic.com/en/docs/claude-code) | [Codex](https://openai.com/index/codex/) | [Antigravity](https://www.antigravity.dev/))
-
-## Documentation
-
-Full documentation, contributing guide, and source code: [github.com/hi-godot/godot-ai](https://github.com/hi-godot/godot-ai)
-
-## License
-
-[MIT](LICENSE)
diff --git a/addons/godot_ai/client_configurator.gd b/addons/godot_ai/client_configurator.gd
deleted file mode 100644
index bd3980f..0000000
--- a/addons/godot_ai/client_configurator.gd
+++ /dev/null
@@ -1,2067 +0,0 @@
-@tool
-class_name McpClientConfigurator
-extends RefCounted
-
-## Public facade for the MCP client configuration system.
-##
-## Per-client logic lives in clients/*.gd (one descriptor per client) and is
-## dispatched through clients/_registry.gd. This file:
-## - owns server-side identifiers (SERVER_NAME, HTTP/WS port helpers)
-## - registers the EditorSettings port overrides and resolves the live
-## port/URL via `http_port()` / `ws_port()` / `http_url()`
-## - keeps server-launch discovery (.venv → uvx → system godot-ai)
-## - exposes string-id wrappers around configure / check_status / remove /
-## manual_command so callers don't need to touch the registry directly
-##
-## To add a new client: drop a file in clients/, then preload it in
-## clients/_registry.gd. No edits required here.
-
-const Client := preload("res://addons/godot_ai/clients/_base.gd")
-const ClientRegistry := preload("res://addons/godot_ai/clients/_registry.gd")
-const JsonStrategy := preload("res://addons/godot_ai/clients/_json_strategy.gd")
-const TomlStrategy := preload("res://addons/godot_ai/clients/_toml_strategy.gd")
-const YamlStrategy := preload("res://addons/godot_ai/clients/_yaml_strategy.gd")
-const DshStrategy := preload("res://addons/godot_ai/clients/_dsh_strategy.gd")
-const CliStrategy := preload("res://addons/godot_ai/clients/_cli_strategy.gd")
-const ManualCommand := preload("res://addons/godot_ai/clients/_manual_command.gd")
-const CliFinder := preload("res://addons/godot_ai/clients/_cli_finder.gd")
-const MutationLock := preload("res://addons/godot_ai/utils/client_mutation_lock.gd")
-const WindowsPortReservation := preload("res://addons/godot_ai/utils/windows_port_reservation.gd")
-const PortResolver := preload("res://addons/godot_ai/utils/port_resolver.gd")
-const UvResolution := preload("res://addons/godot_ai/utils/uv_resolution_policy.gd")
-
-const SERVER_NAME := "godot-ai"
-
-## Fallback ports. Live port selection goes through `http_port()` / `ws_port()`,
-## which read overrides from EditorSettings first. Users on Windows whose 8000
-## is grabbed by Hyper-V / WSL2 / Docker can pick a different port in
-## Editor Settings > Plugins > godot_ai without touching code. See #146 for
-## the Windows-reservation diagnostics this is the escape hatch for.
-const DEFAULT_HTTP_PORT := 8000
-const DEFAULT_WS_PORT := 9500
-const STARTUP_TRACE_ENV := "GODOT_AI_STARTUP_TRACE"
-const MIN_PORT := 1024
-const MAX_PORT := 65535
-## Cap on `can_bind_local_port` probes per `suggest_free_port` call so a
-## pathological run of occupied ports can't stall the (cold-path) caller.
-## 64 localhost binds are sub-millisecond; finding a free port realistically
-## takes one or two probes, so this only bounds the worst case.
-const SUGGEST_PORT_MAX_PROBES := 64
-const SETTING_WS_PORT := "godot_ai/ws_port"
-const SETTING_V4_ENDPOINT_PORTS := "godot_ai/v4_endpoint_ports"
-const SETTING_STARTUP_TRACE := "godot_ai/log_startup_timing"
-const SETTING_KEEP_SERVER_ON_EXIT := "godot_ai/keep_server_on_exit"
-## External cwd the user can set when an MCP client (Pi, code-server, …) reads
-## project-tier config files from a directory this editor can't see. The
-## merge-tier strategies include it as an extra `_project_candidate_paths` root
-## so project overrides there are detected instead of silently shadowed by a
-## global-tier write. Codex-review finding F1.
-const SETTING_EXTERNAL_CLIENT_CWD := "godot_ai/external_client_cwd"
-const _DISCOVERY_TIMEOUT_MS := 3000
-## Codex launches Windows console-subsystem MCP commands in a visible terminal.
-## A GUI-subsystem Python keeps the bridge attached to Codex's redirected MCP
-## pipes without allocating a console, then starts console launchers such as
-## uvx with CREATE_NO_WINDOW. Keep stdin/stdout/stderr explicit: pythonw can use
-## its own inherited pipes, but subprocess defaults do not reliably forward
-## them to a child when no console exists.
-## This string is a wire format written verbatim into user config `args`.
-## Whitespace or formatting changes make every existing Windows entry report
-## CONFIGURED_MISMATCH, so changing it is a deliberate migration decision, not
-## a refactor. The inline `-c` script is required because the uvx and system
-## tiers resolve a system interpreter where `godot_ai` is not importable.
-const _WINDOWS_STDIO_BOOTSTRAP := (
- "import subprocess,sys; "
- + "raise SystemExit(subprocess.call(sys.argv[1:], stdin=sys.stdin, stdout=sys.stdout, "
- + "stderr=sys.stderr, creationflags=0x08000000))"
-)
-
-
-## Active HTTP port: user override (if in range) or `DEFAULT_HTTP_PORT`.
-static func http_port() -> int:
- var override := v4_endpoint_ports_status()
- if bool(override.present):
- return int(override.get("http_port", 0))
- return _read_port_setting(McpSettings.SETTING_HTTP_PORT, DEFAULT_HTTP_PORT)
-
-
-## Active WebSocket port: user override (if in range) or `DEFAULT_WS_PORT`.
-static func ws_port() -> int:
- var override := v4_endpoint_ports_status()
- if bool(override.present):
- return int(override.get("ws_port", 0))
- return _read_port_setting(SETTING_WS_PORT, DEFAULT_WS_PORT)
-
-
-## An absent override preserves historical settings. A malformed present
-## override grants no endpoint; callers can surface its error before startup.
-static func v4_endpoint_ports_status() -> Dictionary:
- var es := EditorInterface.get_editor_settings()
- if es == null or not es.has_setting(SETTING_V4_ENDPOINT_PORTS):
- return {"ok": true, "present": false}
- var pair: Variant = es.get_setting(SETTING_V4_ENDPOINT_PORTS)
- var invalid := {"ok": false, "present": true,
- "error": "Invalid %s: set distinct integer http_port and ws_port values between %d and %d, or remove the override." % [SETTING_V4_ENDPOINT_PORTS, MIN_PORT, MAX_PORT]}
- if not (pair is Dictionary) or pair.size() != 2:
- return invalid
- for key in ["http_port", "ws_port"]:
- var value: Variant = pair.get(key)
- if not (value is int or value is float) or not is_finite(float(value)):
- return invalid
- if float(value) < MIN_PORT or float(value) > MAX_PORT or float(value) != float(int(value)):
- return invalid
- if int(pair.http_port) == int(pair.ws_port):
- return invalid
- return {"ok": true, "present": true, "http_port": int(pair.http_port), "ws_port": int(pair.ws_port)}
-
-
-## Call only after a verified successful upgrade and the activation barrier.
-## This selects independent ports, not authority over the legacy backend.
-static func prepare_major_upgrade_endpoints(from_version: String, to_version: String) -> Dictionary:
- var previous := McpServerVersionCheck.version_tuple(from_version)
- var installed := McpServerVersionCheck.version_tuple(to_version)
- if previous.is_empty() or installed.is_empty() or int(previous[0]) >= 4 or int(installed[0]) < 4:
- return {"ok": true, "changed": false}
- if OS.get_thread_caller_id() != OS.get_main_thread_id():
- return {"ok": false, "error": "upgrade endpoint selection requires the main thread"}
- var existing := v4_endpoint_ports_status()
- if not bool(existing.ok) or bool(existing.present):
- existing["changed"] = false
- return existing
- var es := EditorInterface.get_editor_settings()
- if es == null:
- return {"ok": false, "error": "EditorSettings is unavailable"}
- var legacy_http := _read_port_setting(McpSettings.SETTING_HTTP_PORT, DEFAULT_HTTP_PORT)
- var legacy_ws := _read_port_setting(SETTING_WS_PORT, DEFAULT_WS_PORT)
- var selected: Array[int] = []
- var candidate := legacy_http + 1 if legacy_http < MAX_PORT else MIN_PORT
- candidate = WindowsPortReservation.suggest_non_excluded_port(candidate, MAX_PORT - candidate + 1, MAX_PORT)
- var reserved_output := str(WindowsPortReservation._get_cached_excluded_output().get("text", ""))
- for _probe in range(SUGGEST_PORT_MAX_PROBES):
- candidate = WindowsPortReservation.suggest_non_excluded_port_from_output(reserved_output, candidate, MAX_PORT - candidate + 1, MAX_PORT)
- if candidate < MIN_PORT or candidate > MAX_PORT:
- break
- if candidate not in [legacy_http, legacy_ws] and not selected.has(candidate) and PortResolver.can_bind_local_port(candidate) and not PortResolver.is_port_in_use(candidate):
- selected.append(candidate)
- if selected.size() == 2:
- break
- candidate += 1
- if candidate > MAX_PORT:
- candidate = MIN_PORT
- if selected.size() != 2:
- return {"ok": false, "error": "No independent HTTP/WebSocket port pair was available for this major upgrade."}
- for port in selected:
- if WindowsPortReservation.parse_excluded(reserved_output, port) or not PortResolver.can_bind_local_port(port) or PortResolver.is_port_in_use(port):
- return {"ok": false, "error": "The selected upgrade port %d became unavailable; retry endpoint selection." % port}
- var pair := {"http_port": selected[0], "ws_port": selected[1]}
- es.set_setting(SETTING_V4_ENDPOINT_PORTS, pair)
- return {"ok": true, "changed": true, "http_port": selected[0], "ws_port": selected[1]}
-
-
-static func http_url() -> String:
- return "http://127.0.0.1:%d/mcp" % http_port()
-
-
-## Read a URL already captured on the main thread without evaluating the
-## EditorSettings-backed fallback unless the snapshot is genuinely incomplete.
-static func server_url_from(launch_context: Dictionary) -> String:
- if launch_context.has("server_url"):
- return str(launch_context["server_url"])
- return http_url()
-
-
-static func _read_port_setting(key: String, default_port: int) -> int:
- var es := EditorInterface.get_editor_settings()
- if es == null or not es.has_setting(key):
- return default_port
- var value: int = int(es.get_setting(key))
- if value < MIN_PORT or value > MAX_PORT:
- return default_port
- return value
-
-
-## Register the port overrides in EditorSettings so they show up in the
-## editor's Settings > Plugins section with a range hint. Called once from
-## `plugin.gd._enter_tree` before `_start_server` so spawn args see the
-## configured values. Safe to call repeatedly — `add_property_info` is
-## idempotent and `set_initial_value` only seeds the default.
-static func ensure_settings_registered() -> void:
- var es := EditorInterface.get_editor_settings()
- if es == null:
- return
- _register_port_setting(es, McpSettings.SETTING_HTTP_PORT, DEFAULT_HTTP_PORT)
- _register_port_setting(es, SETTING_WS_PORT, DEFAULT_WS_PORT)
- _register_bool_setting(es, SETTING_STARTUP_TRACE, false)
- _register_bool_setting(es, SETTING_KEEP_SERVER_ON_EXIT, false)
- _register_string_setting(es, McpSettings.SETTING_EXCLUDED_DOMAINS, "")
- _register_bool_setting(es, McpSettings.SETTING_TELEMETRY_ENABLED, true)
- _register_string_setting(es, McpSettings.SETTING_ALLOW_HOSTS, "")
- _register_client_scope_setting(es)
- _register_string_setting(es, SETTING_EXTERNAL_CLIENT_CWD, "")
-
-
-static func _register_port_setting(es: EditorSettings, key: String, default_port: int) -> void:
- if not es.has_setting(key):
- es.set_setting(key, default_port)
- es.set_initial_value(key, default_port, false)
- es.add_property_info({
- "name": key,
- "type": TYPE_INT,
- "hint": PROPERTY_HINT_RANGE,
- "hint_string": "%d,%d,1" % [MIN_PORT, MAX_PORT],
- })
-
-
-## Surface the CLI registration scope as an enum in Settings > Plugins so it is
-## discoverable without hand-editing editor_settings-4.tres. Kept at `user` by
-## default; `project` writes the entry into /.mcp.json instead.
-static func _register_client_scope_setting(es: EditorSettings) -> void:
- var key := McpSettings.SETTING_CLIENT_SCOPE
- if not es.has_setting(key):
- es.set_setting(key, McpSettings.DEFAULT_CLIENT_SCOPE)
- es.set_initial_value(key, McpSettings.DEFAULT_CLIENT_SCOPE, false)
- es.add_property_info({
- "name": key,
- "type": TYPE_STRING,
- "hint": PROPERTY_HINT_ENUM,
- "hint_string": ",".join(PackedStringArray(McpSettings.CLIENT_SCOPES)),
- })
-
-
-static func _register_bool_setting(es: EditorSettings, key: String, default_value: bool) -> void:
- if not es.has_setting(key):
- es.set_setting(key, default_value)
- es.set_initial_value(key, default_value, false)
- es.add_property_info({
- "name": key,
- "type": TYPE_BOOL,
- })
-
-
-static func _register_string_setting(es: EditorSettings, key: String, default_value: String) -> void:
- ## Same idempotent set-then-hint dance as the bool helper; `PROPERTY_HINT_NONE`
- ## leaves the editor's plain text-input widget in place (the cwd string is
- ## freeform). The hint-string slot is required by the API even when empty.
- if not es.has_setting(key):
- es.set_setting(key, default_value)
- es.set_initial_value(key, default_value, false)
- es.add_property_info({
- "name": key,
- "type": TYPE_STRING,
- "hint": PROPERTY_HINT_NONE,
- "hint_string": "",
- })
-
-
-static func startup_trace_enabled() -> bool:
- ## env_lookup + _editor_setting_lookup for the same worker-thread
- ## reason as mode_override (#691).
- var raw := McpPathTemplate.env_lookup(STARTUP_TRACE_ENV).strip_edges().to_lower()
- if raw == "1" or raw == "true" or raw == "yes" or raw == "on":
- return true
- var setting: Variant = _editor_setting_lookup(SETTING_STARTUP_TRACE)
- if setting != null:
- return bool(setting)
- return false
-
-
-## keep_server_on_exit (#800): when enabled, editor teardown detaches from
-## the managed server instead of killing it, and the spawn env opts the
-## server out of both self-reap paths (owner-PID watchdog, session-idle
-## backstop) — so MCP clients connected over HTTP stay served across editor
-## sessions, and the next editor start adopts the survivor. Off by default:
-## "server dies with the editor" stays the shipped behavior. Read via
-## _editor_setting_lookup for the same worker-thread reason as
-## startup_trace_enabled (#691).
-static func keep_server_on_exit() -> bool:
- var setting: Variant = _editor_setting_lookup(SETTING_KEEP_SERVER_ON_EXIT)
- if setting != null:
- return bool(setting)
- return false
-
-
-## #691: EditorSettings counterpart of McpPathTemplate.env_lookup. The #678
-## startup walk's discovery worker reaches mode_override() (via
-## get_server_command) and EditorInterface / EditorSettings are not
-## thread-safe objects. Main thread: live read + mutex-guarded snapshot
-## refresh. Worker thread: snapshot only — a never-warmed key reads as
-## null (unset), never a live EditorInterface call. Warmed alongside the
-## env snapshot in warm_env_snapshot(), which runs on the main thread
-## before any worker dispatch.
-static var _setting_snapshot := {}
-static var _setting_snapshot_mutex := Mutex.new()
-## The aggregate MCP status command runs on a worker thread. Keep the same
-## main-thread-only LaunchContext contract used by dock workers by publishing a
-## deep snapshot whenever capture_launch_context() runs on the main thread.
-static var _launch_context_snapshot := {}
-static var _endpoint_policy_snapshot := {}
-static var _launch_context_snapshot_mutex := Mutex.new()
-
-
-static func _editor_setting_lookup(key: String) -> Variant:
- if OS.get_thread_caller_id() == OS.get_main_thread_id():
- var live: Variant = null
- if Engine.is_editor_hint():
- var es := EditorInterface.get_editor_settings()
- if es != null and es.has_setting(key):
- live = es.get_setting(key)
- _setting_snapshot_mutex.lock()
- _setting_snapshot[key] = live
- _setting_snapshot_mutex.unlock()
- return live
- _setting_snapshot_mutex.lock()
- var cached: Variant = _setting_snapshot.get(key, null)
- _setting_snapshot_mutex.unlock()
- return cached
-
-
-## Read the `godot_ai/excluded_domains` EditorSetting as a canonicalized
-## comma-separated list (sorted, deduplicated, whitespace-stripped). Returns
-## "" when the setting is missing or resolves to an empty set — callers can
-## skip appending the flag in that case so older servers that don't know
-## `--exclude-domains` don't see an empty argument.
-##
-## Unknown domain names (e.g. a domain removed since the setting was last
-## written) are dropped here, at the single chokepoint both the startup
-## flag builder (plugin.gd) and the dock display read — the server's
-## `parse_exclude_list` hard-fails on unknown names, so a stale setting
-## would otherwise block server startup.
-static func excluded_domains() -> String:
- var es := EditorInterface.get_editor_settings()
- if es == null or not es.has_setting(McpSettings.SETTING_EXCLUDED_DOMAINS):
- return ""
- return _canonicalize_excluded_domains(str(es.get_setting(McpSettings.SETTING_EXCLUDED_DOMAINS)))
-
-
-## Pure canonicalizer shared by the main-thread LaunchContext capture and
-## tests. Unknown domains are dropped for the same startup-safety reason as
-## `excluded_domains()` above.
-static func _canonicalize_excluded_domains(raw: String) -> String:
- var parts := PackedStringArray()
- for p in raw.split(","):
- var t := p.strip_edges()
- if t.is_empty() or parts.find(t) != -1:
- continue
- if not McpToolCatalog.is_excludable_domain(t):
- continue
- parts.append(t)
- parts.sort()
- return ",".join(parts)
-
-
-## One activation-scoped desired endpoint policy. The composition root resolves
-## the WS port, publishes this value once, and derives both managed-server and
-## attach plans from it. Returned dictionaries never alias the retained copy.
-static func capture_endpoint_policy(resolved_ws_port: int = -1) -> Dictionary:
- var selected_ws_port := ws_port() if resolved_ws_port <= 0 else resolved_ws_port
- return {
- "http_port": http_port(),
- "ws_port": selected_ws_port,
- "excluded_domains": excluded_domains(),
- "allow_hosts": allow_hosts(),
- "keep_alive": keep_server_on_exit(),
- "telemetry_enabled": McpSettings.telemetry_enabled(),
- }
-
-
-## Snapshot every value needed to render or verify an attach launch command.
-## Endpoint fields come only from the root-published activation policy; later
-## main-thread refreshes update discovery/project values without re-reading a
-## divergent desired endpoint. Workers receive the deep snapshot only (#691).
-static func capture_launch_context(endpoint_policy: Dictionary = {}) -> Dictionary:
- if OS.get_thread_caller_id() != OS.get_main_thread_id():
- _launch_context_snapshot_mutex.lock()
- var cached := _launch_context_snapshot.duplicate(true)
- _launch_context_snapshot_mutex.unlock()
- return cached
- var policy := endpoint_policy.duplicate(true)
- if policy.is_empty():
- _launch_context_snapshot_mutex.lock()
- policy = _endpoint_policy_snapshot.duplicate(true)
- _launch_context_snapshot_mutex.unlock()
- if policy.is_empty():
- ## Standalone/test callers without a composition root still get a
- ## complete context. A real plugin activation always publishes first.
- policy = capture_endpoint_policy()
- var captured_http_port := int(policy.get("http_port", DEFAULT_HTTP_PORT))
- var context := {
- "http_port": captured_http_port,
- "ws_port": int(policy.get("ws_port", DEFAULT_WS_PORT)),
- "excluded_domains": str(policy.get("excluded_domains", "")),
- "plugin_version": get_plugin_version(),
- "allow_dev_venv": mode_override() != "user",
- "platform": OS.get_name(),
- "server_url": "http://127.0.0.1:%d/mcp" % captured_http_port,
- "project_roots": capture_project_roots(),
- ## The opt-out must ride the attach argv: the client spawns the bridge
- ## (and the bridge its backend) with no editor in the loop, so the
- ## env-injection path in server_lifecycle.gd never runs for them.
- "telemetry_enabled": bool(policy.get("telemetry_enabled", true)),
- }
- _launch_context_snapshot_mutex.lock()
- _endpoint_policy_snapshot = policy.duplicate(true)
- _launch_context_snapshot = context.duplicate(true)
- _launch_context_snapshot_mutex.unlock()
- return context
-
-
-## Validate and persist a Dock settings intent at the existing configurator
-## boundary. Validation completes before the first write, so a bad mixed delta
-## cannot partially change desired launch policy.
-static func apply_endpoint_settings(changes: Dictionary) -> Dictionary:
- if OS.get_thread_caller_id() != OS.get_main_thread_id():
- return {"ok": false, "error": "settings writes require the main thread"}
- var normalized := {}
- for key in changes:
- var value: Variant = changes[key]
- match str(key):
- "http_port":
- if not (value is int or value is float):
- return {"ok": false, "error": "HTTP port must be an integer"}
- var port := int(value)
- if float(value) != float(port) or port < MIN_PORT or port > MAX_PORT:
- return {"ok": false, "error": "HTTP port is outside the supported range"}
- normalized[McpSettings.SETTING_HTTP_PORT] = port
- "ws_port":
- if not (value is int or value is float):
- return {"ok": false, "error": "WebSocket port must be an integer"}
- var ws_port_value := int(value)
- if (
- float(value) != float(ws_port_value)
- or ws_port_value < MIN_PORT
- or ws_port_value > MAX_PORT
- ):
- return {"ok": false, "error": "WebSocket port is outside the supported range"}
- normalized[SETTING_WS_PORT] = ws_port_value
- "excluded_domains":
- if not (value is String):
- return {"ok": false, "error": "excluded domains must be text"}
- normalized[McpSettings.SETTING_EXCLUDED_DOMAINS] = _canonicalize_excluded_domains(value)
- "telemetry_enabled":
- if not (value is bool):
- return {"ok": false, "error": "telemetry preference must be boolean"}
- normalized[McpSettings.SETTING_TELEMETRY_ENABLED] = value
- "allow_hosts":
- if not (value is String):
- return {"ok": false, "error": "allow-host policy must be text"}
- var allowed := McpAllowHosts.normalize(value)
- if not McpAllowHosts.invalid_tokens(allowed).is_empty():
- return {"ok": false, "error": "allow-host policy is invalid"}
- normalized[McpSettings.SETTING_ALLOW_HOSTS] = allowed
- _:
- return {"ok": false, "error": "unknown endpoint setting: %s" % key}
- var next_http := int(normalized.get(McpSettings.SETTING_HTTP_PORT, http_port()))
- var next_ws := int(normalized.get(SETTING_WS_PORT, ws_port()))
- var override := v4_endpoint_ports_status()
- var ports_changed := normalized.has(McpSettings.SETTING_HTTP_PORT) or normalized.has(SETTING_WS_PORT)
- if bool(override.present) and ports_changed:
- if next_http < MIN_PORT or next_http > MAX_PORT or next_ws < MIN_PORT or next_ws > MAX_PORT:
- return {"ok": false, "error": "A complete valid HTTP/WebSocket pair is required to repair the v4 override."}
- normalized.erase(McpSettings.SETTING_HTTP_PORT)
- normalized.erase(SETTING_WS_PORT)
- normalized[SETTING_V4_ENDPOINT_PORTS] = {"http_port": next_http, "ws_port": next_ws}
- if next_http == next_ws and (ports_changed or not bool(override.present)):
- return {"ok": false, "error": "HTTP and WebSocket ports must differ"}
- var es := EditorInterface.get_editor_settings()
- if es == null:
- return {"ok": false, "error": "EditorSettings is unavailable"}
- for key in normalized:
- es.set_setting(key, normalized[key])
- return {"ok": true, "changes": normalized.duplicate(true)}
-
-
-## Resolve roots used by cwd-relative client project config tiers while the
-## engine singletons are safe to access. Workers receive this immutable snapshot.
-static func capture_project_roots() -> PackedStringArray:
- var current_access := DirAccess.open(".")
- var current_root := "" if current_access == null else current_access.get_current_dir().simplify_path()
- var project_root := ProjectSettings.globalize_path("res://").simplify_path()
- # Codex F1: when the user sets `godot_ai/external_client_cwd` we probe that
- # directory in addition to the two guessed roots. The setter / setter-snapshot
- # pathway is the same as the existing EditorSettings (warm_env_snapshot
- # pre-warms this key so worker-thread callers see the snapshot, not the live
- # EditorInterface). Empty string is filtered out by `_canonicalize_roots_for_test`.
- var external_cwd := str(_editor_setting_lookup(SETTING_EXTERNAL_CLIENT_CWD))
- return _canonicalize_roots_for_test(PackedStringArray([current_root, project_root, external_cwd]))
-
-
-## Canonicalize a list of root paths to one filesystem representation per
-## directory and deduplicate. `ProjectSettings.globalize_path` maps any
-## `res://` / `user://` form to the absolute path and is idempotent on an
-## already-absolute path, so a `res://`-form candidate and its absolute twin
-## collapse to the same entry. Without this pass, `_project_candidate_paths`
-## would probe `/.pi/mcp.json` against both representations of the same
-## directory and `manual_target_details` would mis-report a single project
-## override as multiple tiers (codex-review finding F4). Public-ish seam
-## (named `_..._for_test`) so the clients suite can pin the behaviour without
-## having to mock `DirAccess.open(".")`.
-static func _canonicalize_roots_for_test(raws: PackedStringArray) -> PackedStringArray:
- var seen := {}
- var roots := PackedStringArray()
- for raw in raws:
- var s := str(raw).strip_edges()
- if s.is_empty():
- continue
- var canon := ProjectSettings.globalize_path(s).simplify_path()
- if canon.is_empty() or seen.has(canon):
- continue
- seen[canon] = true
- roots.append(canon)
- return roots
-
-
-static func _project_roots_from_context(context: Dictionary) -> PackedStringArray:
- var roots = context.get("project_roots", PackedStringArray())
- return roots if roots is PackedStringArray else PackedStringArray()
-
-
-## Read the `godot_ai/allow_remote_hosts` EditorSetting as a canonicalized
-## comma-separated list of CIDRs / bare IPs (#507). Returns "" when the
-## setting is missing or empty — callers skip appending `--allow-host` in
-## that case so spawns stay byte-for-byte identical to the loopback-only
-## default (and compatible with pre-#421 servers). Mirrors
-## `excluded_domains()` above.
-static func allow_hosts() -> String:
- var es := EditorInterface.get_editor_settings()
- if es == null or not es.has_setting(McpSettings.SETTING_ALLOW_HOSTS):
- return ""
- return McpAllowHosts.normalize(str(es.get_setting(McpSettings.SETTING_ALLOW_HOSTS)))
-
-
-## Suggest a port the caller can actually switch to. Walks
-## `candidate`..`candidate+span-1` and returns the first port that is both
-## (a) NOT inside a Windows winnat reservation range (Hyper-V / WSL2 / Docker
-## grab these; bind fails with WinError 10013 and netstat shows nothing) and
-## (b) actually bindable right now on 127.0.0.1. The bind probe is what makes
-## "free" honest on macOS/Linux, where the reservation table is empty but the
-## next port up may still be occupied — the same suggestion feeds the dock
-## crash body, the port-picker spinbox, and the non-recoverable INCOMPATIBLE
-## log line. Falls back to the clamped candidate if nothing in the window
-## clears both checks (caller surfaces it as a best-effort hint; the user can
-## retry or pick another). Best-effort by nature: a TOCTOU window remains
-## between the probe and the caller actually binding the port. The bind probe
-## is bounded to `SUGGEST_PORT_MAX_PROBES` attempts so this cold path can't
-## stall on a pathological run of occupied ports.
-static func suggest_free_port(start: int, span: int = 2048) -> int:
- var candidate := clampi(start, MIN_PORT, MAX_PORT - span + 1)
- var limit := mini(candidate + span - 1, MAX_PORT)
- var p := candidate
- var probes := 0
- while p <= limit and probes < SUGGEST_PORT_MAX_PROBES:
- ## Jump past a whole Windows-reserved range in one step (no-op on
- ## POSIX: returns `p` unchanged), so we don't probe port-by-port
- ## through the large adjacent ranges those services reserve. The
- ## jump itself runs no bind probes, so it doesn't count against the cap.
- var not_reserved := WindowsPortReservation.suggest_non_excluded_port(p, limit - p + 1, MAX_PORT)
- if not_reserved < p or not_reserved > limit:
- break
- p = not_reserved
- probes += 1
- if PortResolver.can_bind_local_port(p):
- return p
- p += 1
- return candidate
-
-
-# --- Client operations (string id) ---------------------------------------
-
-static func client_ids() -> PackedStringArray:
- return ClientRegistry.ids()
-
-
-static func has_client(id: String) -> bool:
- return ClientRegistry.has_id(id)
-
-
-static func client_display_name(id: String) -> String:
- var c := ClientRegistry.get_by_id(id)
- return c.display_name if c != null else id
-
-
-## Pass an explicit `url` when calling from a worker thread: `http_url()`
-## reads `EditorInterface.get_editor_settings()`, which is main-thread-only.
-## Empty defaults to the live server URL — appropriate for MCP-tool callers
-## that always run on main.
-static func configure(id: String, url: String = "", launch_context: Dictionary = {}) -> Dictionary:
- if ClientRegistry.stale_session_detected():
- return {"status": "error", "message": ClientRegistry.RESTART_TO_FINISH_UPDATE}
- var client := ClientRegistry.get_by_id(id)
- if client == null:
- return {"status": "error", "message": "Unknown client: %s" % id}
- if not client.automatic_config_edits:
- return _manual_edit_result(client, "configure")
- var path_error := _config_path_resolution_error(client)
- if not path_error.is_empty():
- return {"status": "error", "message": path_error}
- ## Capture `url` once so a port flip in EditorSettings between write and
- ## verify can't trigger a spurious CONFIGURED_MISMATCH against an entry
- ## that just landed correctly.
- if url.is_empty():
- url = http_url()
- var context := launch_context
- if client.command_shape != Client.CommandShape.NONE and context.is_empty():
- if OS.get_thread_caller_id() != OS.get_main_thread_id():
- return {
- "status": "error",
- "message": "Cannot configure %s without a main-thread launch snapshot; retry from the dock." % client.display_name,
- }
- context = capture_launch_context()
- var launch := (
- resolve_attach_launch(context)
- if client.command_shape != Client.CommandShape.NONE
- else {}
- )
- var claim := MutationLock.acquire(client.id, "configure")
- if not bool(claim.get("ok", false)):
- return {
- "status": "error",
- "message": str(claim.get("error", MutationLock.recovery_message())),
- }
- var result := _dispatch_configure(client, url, launch, context)
- ## Trust-but-verify: a strategy may report ok and have actually written the
- ## file, yet the entry is missing/stale on the read-back path — most often
- ## because the user's installed client is reading a different file than
- ## `path_template` resolves to (issue #201). Re-read the live state and
- ## surface a clear error before the dock reports a bogus green dot.
- var verified := _verify_post_state(
- client, result, Client.Status.CONFIGURED, url, "configure", launch, context
- )
- return _finish_client_mutation(claim, verified)
-
-
-static func check_status(id: String) -> Client.Status:
- if ClientRegistry.stale_session_detected():
- return Client.Status.ERROR
- var client := ClientRegistry.get_by_id(id)
- if client == null:
- return Client.Status.NOT_CONFIGURED
- var context := capture_launch_context() if client.command_shape != Client.CommandShape.NONE else {}
- return _dispatch_check_status(client, http_url(), context)
-
-
-static func check_status_for_url_with_cli_path(
- id: String, url: String, cli_path: String, launch_context: Dictionary = {}
-) -> Client.Status:
- return check_status_details_for_url_with_cli_path(id, url, cli_path, launch_context).get("status", Client.Status.NOT_CONFIGURED)
-
-
-## True when 's stored entry verifies EXACTLY against the launch this
-## editor would have rendered at `from_version` — same ports, exclusions,
-## telemetry flag, command shape; nothing differs but the version pin. This
-## is the post-update auto-repin gate: only such entries are provably "what
-## Configure wrote before the update", so rewriting them to the current
-## version restores the user's own prior intent. Anything else — entries
-## pointing at another editor's ports (the smoke-fixture blast radius that
-## motivated this gate), hand-edits, changed settings — stays untouched for
-## the drift banner's human click. An empty `from_version` (marker written
-## by a pre-gate runner) fails closed.
-##
-## The version-substituted context is safe to resolve and cache:
-## `plugin_version` is part of both the attach-launch resolution
-## (`_resolve_attach_launch_uncached`) and its cache key.
-static func entry_drift_is_version_pin_only(
- id: String, from_version: String, launch_context: Dictionary = {}
-) -> bool:
- var pinned_from := from_version.strip_edges()
- if pinned_from.is_empty():
- return false
- ## Never shell out from this gate — it runs on the MAIN thread from the
- ## dock's sweep-completion callback (#890 review P2). The cli-descriptor
- ## probe paths run a subprocess with a multi-second timeout (`claude mcp
- ## get` is 6s), which would freeze the editor; those clients fail closed
- ## to the drift banner's click flow, whose fan-out already runs on
- ## worker threads. Every path left after these guards is a bounded
- ## config-file read plus cached launch discovery (worst case one CLI
- ## lookup on a cold CliFinder cache — the same bounded one-shot the
- ## dock's `_reprobe_uv_if_negative` accepts on this thread).
- var client := ClientRegistry.get_by_id(id)
- if client == null:
- return false
- if client.config_type == "cli":
- if client.command_shape == Client.CommandShape.NONE:
- return false
- if not client.has_json_fallback():
- return false
- if _scope_diverges_from_json_fallback(client):
- return false
- var context := launch_context if not launch_context.is_empty() else capture_launch_context()
- var old_context := context.duplicate(true)
- old_context["plugin_version"] = pinned_from
- ## Same URL under both versions: the URL carries the port, not the
- ## version, so URL-mode entries can never be version-pin-only drift and
- ## correctly fail this check.
- var url := str(context.get("server_url", http_url()))
- var status := check_status_for_url_with_cli_path(id, url, "", old_context)
- return status == Client.Status.CONFIGURED
-
-
-## Detailed variant used by the dock refresh worker. Returns
-## `{"status": Status, "error_msg": String}` so the worker can surface
-## "probe timed out" on the row instead of silently flipping it to
-## NOT_CONFIGURED. Callers that only need the status can use the simpler
-## helper above.
-static func check_status_details_for_url_with_cli_path(
- id: String,
- url: String,
- cli_path: String,
- launch_context: Dictionary = {},
- resolved_launch: Dictionary = {},
-) -> Dictionary:
- ## One comprehensible line per row beats a wall of per-field type errors —
- ## the dock keeps painting, every row names the same repair (restart).
- if ClientRegistry.stale_session_detected():
- return {"status": Client.Status.ERROR, "error_msg": ClientRegistry.RESTART_TO_FINISH_UPDATE}
- var client := ClientRegistry.get_by_id(id)
- if client == null:
- return {"status": Client.Status.NOT_CONFIGURED, "error_msg": ""}
- # A cli client with no resolved binary normally reads as NOT_CONFIGURED.
- # Skip that shortcut when the client has a JSON fallback (#463): the
- # dispatch below reads its config file directly so the status dot reflects
- # a fallback-configured entry instead of always showing red.
- if client.config_type == "cli" and cli_path.is_empty() and not client.has_json_fallback():
- return {"status": Client.Status.NOT_CONFIGURED, "error_msg": ""}
- var path_error := _config_path_resolution_error(client)
- if not path_error.is_empty():
- return {"status": Client.Status.ERROR, "error_msg": path_error}
- if client.command_shape != Client.CommandShape.NONE and launch_context.is_empty():
- return {
- "status": Client.Status.ERROR,
- "error_msg": "Missing launch-context snapshot; retry the status refresh.",
- }
- return _dispatch_check_status_with_cli_path_details(
- client, url, cli_path, launch_context, resolved_launch
- )
-
-
-## #691: main-thread pre-warm of McpPathTemplate's env snapshot, covering
-## the base vars plus every descriptor-declared config-file/config-home env
-## (OPENCODE_CONFIG, CLAUDE_CONFIG_DIR, CODEX_HOME, …), so worker-thread config-path
-## resolution never calls OS.get_environment concurrently with the spawn
-## window's setenv/unsetenv. Also warms the EditorSettings snapshot for
-## the mode/trace overrides so worker-thread mode_override() /
-## startup_trace_enabled() never touch EditorInterface. Idempotent;
-## called from plugin _enter_tree and before each dock worker dispatch.
-static func warm_env_snapshot(endpoint_policy: Dictionary = {}) -> void:
- var extras := PackedStringArray()
- for id in client_ids():
- var client := ClientRegistry.get_by_id(String(id))
- if client == null:
- continue
- ## Reflected get(): after an in-session self-update these fields can
- ## read as Nil on stale instances, and String(Nil) is a hard error (#850). Skipping just degrades env-override
- ## resolution until the restart the registry is already asking for.
- for env_name in [client.get("config_file_env"), client.get("config_home_env")]:
- if env_name is String and not env_name.is_empty() and not extras.has(env_name):
- extras.append(env_name)
- McpPathTemplate.warm_env_snapshot(extras)
- UvResolution.warm_environment()
- _editor_setting_lookup(MODE_OVERRIDE_SETTING)
- _editor_setting_lookup(SETTING_STARTUP_TRACE)
- _editor_setting_lookup(SETTING_KEEP_SERVER_ON_EXIT)
- _editor_setting_lookup(SETTING_EXTERNAL_CLIENT_CWD)
- McpSettings.warm_client_scope()
- # Publish the complete launch context while EditorInterface access is safe;
- # worker callers of capture_launch_context() read this snapshot only.
- capture_launch_context(endpoint_policy)
-
-
-static func client_status_probe_snapshot(id: String) -> Dictionary:
- var client := ClientRegistry.get_by_id(id)
- if client == null:
- return {}
- var cli_path := ""
- var installed := false
- if client.config_type == "cli":
- cli_path = CliStrategy.resolve_cli_path(client)
- # #463: a JSON-fallback cli client (Claude Code as a VS Code extension)
- # is "installed" when its fallback config exists, even with no binary.
- installed = not cli_path.is_empty() or client.is_installed()
- else:
- installed = client.is_installed()
- return {
- "id": id,
- "cli_path": cli_path,
- "installed": installed,
- "automatic_config_edits": client.automatic_config_edits,
- }
-
-
-## Pure response projection shared by the one client-work owner and tests.
-## Missing rows are explicit errors, never silently reported as unconfigured.
-static func client_status_response(results: Dictionary) -> Dictionary:
- var clients := []
- for client_id in client_ids():
- var details: Dictionary = results.get(client_id, {
- "status": Client.Status.ERROR,
- "installed": false,
- "error_msg": "Client status probe returned no result.",
- })
- clients.append(_client_status_sweep_entry(
- client_id, details, bool(details.get("installed", false))
- ))
- return {"data": {"clients": clients}}
-
-
-static func _client_status_sweep_entry(
- client_id: String, details: Dictionary, installed: bool
-) -> Dictionary:
- var status = details.get("status", Client.Status.NOT_CONFIGURED)
- var entry := {
- "id": client_id,
- "display_name": client_display_name(client_id),
- "status": Client.status_label(status),
- "installed": installed,
- }
- var error_msg := str(details.get("error_msg", ""))
- if not error_msg.is_empty():
- entry["error"] = error_msg
- return entry
-
-
-## Pass an explicit `url` when calling from a worker thread — see
-## `configure()` above for why. The url is only used to format the
-## verify-after-write diagnostic message; the remove itself doesn't need it.
-static func remove(id: String, url: String = "", launch_context: Dictionary = {}) -> Dictionary:
- if ClientRegistry.stale_session_detected():
- return {"status": "error", "message": ClientRegistry.RESTART_TO_FINISH_UPDATE}
- var client := ClientRegistry.get_by_id(id)
- if client == null:
- return {"status": "error", "message": "Unknown client: %s" % id}
- if not client.automatic_config_edits:
- return _manual_edit_result(client, "remove")
- var path_error := _config_path_resolution_error(client)
- if not path_error.is_empty():
- return {"status": "error", "message": path_error}
- if url.is_empty():
- url = http_url()
- var context := launch_context
- if client.command_shape != Client.CommandShape.NONE and context.is_empty():
- if OS.get_thread_caller_id() != OS.get_main_thread_id():
- return {
- "status": "error",
- "message": "Cannot remove %s without a main-thread launch snapshot; retry from the dock." % client.display_name,
- }
- context = capture_launch_context()
- var launch := (
- resolve_attach_launch(context)
- if client.command_shape != Client.CommandShape.NONE
- else {}
- )
- var claim := MutationLock.acquire(client.id, "remove")
- if not bool(claim.get("ok", false)):
- return {
- "status": "error",
- "message": str(claim.get("error", MutationLock.recovery_message())),
- }
- var result := _dispatch_remove(client, context)
- var verified := _verify_post_state(
- client, result, Client.Status.NOT_CONFIGURED, url, "remove", launch, context
- )
- return _finish_client_mutation(claim, verified)
-
-
-## Resolve config-backed path errors before attach-launch discovery. This both
-## gives ambiguity precedence over unrelated launcher failures and avoids
-## spending the status worker's command budget on a Configure action that must
-## fail closed regardless. CLI clients keep their existing CLI/fallback dispatch.
-static func _config_path_resolution_error(client: Client) -> String:
- if client.config_type == "cli":
- return ""
- return str(client.resolved_config_path_details().get("error", ""))
-
-
-# --- Strategy dispatch + verify (testable seam) --------------------------
-
-static func _manual_edit_result(client: Client, action: String) -> Dictionary:
- return {
- "status": "error",
- "message": (
- "%s settings require a manual edit; godot-ai will not %s this file automatically. "
- + "Use the configuration shown below."
- ) % [client.display_name, action],
- }
-
-
-## One global claim covers the mutation and its read-back proof for every
-## automatic strategy. Ambiguous CLI termination deliberately strands it;
-## every other outcome releases the exact token or fails closed.
-static func _finish_client_mutation(claim: Dictionary, result: Dictionary) -> Dictionary:
- if bool(result.get("termination_failed", false)):
- return result
- if MutationLock.release(claim):
- return result
- return {
- "status": "error",
- "message": MutationLock.recovery_message(),
- "mutation_lock_release_failed": true,
- }
-
-static func _dispatch_configure(
- client: Client, url: String, launch: Dictionary = {}, launch_context: Dictionary = {}
-) -> Dictionary:
- launch = launch_for_client(client, launch)
- match client.config_type:
- "json":
- return JsonStrategy.configure(client, SERVER_NAME, url, launch, _project_roots_from_context(launch_context))
- "toml":
- return TomlStrategy.configure(client, SERVER_NAME, url, launch)
- "yaml":
- return YamlStrategy.configure(client, SERVER_NAME, url, launch)
- "dsh":
- return DshStrategy.configure(client, SERVER_NAME, url, launch)
- "cli":
- # #463: fall back to writing the config file directly when the CLI
- # binary isn't on PATH (Claude Code as a VS Code/Cursor extension).
- if client.has_json_fallback() and CliStrategy.resolve_cli_path(client).is_empty():
- return _note_unhonoured_scope(
- client, JsonStrategy.configure(client, SERVER_NAME, url, launch, _project_roots_from_context(launch_context))
- )
- return CliStrategy.configure(client, SERVER_NAME, url, launch)
- return {"status": "error", "message": "Unknown config_type for %s: %s" % [client.id, client.config_type]}
-
-
-static func _dispatch_remove(client: Client, launch_context: Dictionary = {}) -> Dictionary:
- match client.config_type:
- "json":
- return JsonStrategy.remove(client, SERVER_NAME, _project_roots_from_context(launch_context))
- "toml":
- return TomlStrategy.remove(client, SERVER_NAME)
- "yaml":
- return YamlStrategy.remove(client, SERVER_NAME)
- "dsh":
- return DshStrategy.remove(client, SERVER_NAME)
- "cli":
- # #463: mirror the configure fallback so Remove also works without
- # the CLI binary — otherwise a fallback-written entry is unremovable.
- if client.has_json_fallback() and CliStrategy.resolve_cli_path(client).is_empty():
- return JsonStrategy.remove(client, SERVER_NAME, _project_roots_from_context(launch_context))
- return CliStrategy.remove(client, SERVER_NAME)
- return {"status": "error", "message": "Unknown config_type for %s: %s" % [client.id, client.config_type]}
-
-
-static func _dispatch_check_status(
- client: Client, url: String, launch_context: Dictionary = {}
-) -> Client.Status:
- return _dispatch_check_status_with_cli_path(client, url, "", launch_context)
-
-
-static func _dispatch_check_status_with_cli_path(
- client: Client, url: String, cli_path: String, launch_context: Dictionary = {}
-) -> Client.Status:
- return _dispatch_check_status_with_cli_path_details(client, url, cli_path, launch_context).get("status", Client.Status.NOT_CONFIGURED)
-
-
-static func _dispatch_check_status_with_cli_path_details(
- client: Client,
- url: String,
- cli_path: String,
- launch_context: Dictionary = {},
- resolved_launch: Dictionary = {},
-) -> Dictionary:
- match client.config_type:
- "json":
- var launch := {}
- if client.command_shape != Client.CommandShape.NONE:
- launch = _resolved_or_discovered_launch(client, resolved_launch, launch_context)
- return JsonStrategy.check_status_details(client, SERVER_NAME, url, launch, _project_roots_from_context(launch_context))
- "toml":
- var launch := {}
- if client.command_shape != Client.CommandShape.NONE:
- launch = _resolved_or_discovered_launch(client, resolved_launch, launch_context)
- return TomlStrategy.check_status_details(client, SERVER_NAME, url, launch)
- "yaml":
- var yaml_launch := {}
- if client.command_shape != Client.CommandShape.NONE:
- yaml_launch = _resolved_or_discovered_launch(client, resolved_launch, launch_context)
- return YamlStrategy.check_status_details(client, SERVER_NAME, url, yaml_launch)
- "dsh":
- var dsh_launch := {}
- if client.command_shape != Client.CommandShape.NONE:
- dsh_launch = _resolved_or_discovered_launch(client, resolved_launch, launch_context)
- return DshStrategy.check_status_details(client, SERVER_NAME, url, dsh_launch)
- "cli":
- # Command-shape CLI clients register through their CLI, but the entry
- # lands in the same file the JSON fallback reads (`claude mcp add
- # --scope user` writes mcpServers in ~/.claude.json). Reading that
- # file gives exact launch-drift detection — a changed port, version
- # pin, or exclusion list — which scanning `mcp list` stdout cannot,
- # so it is preferred even when the CLI binary resolves.
- # #872: ...but only while the selected scope is still the one
- # `path_template` points at — see _scope_diverges_from_json_fallback.
- if (
- client.command_shape != Client.CommandShape.NONE
- and client.has_json_fallback()
- and not _scope_diverges_from_json_fallback(client)
- ):
- var command_launch := _resolved_or_discovered_launch(client, resolved_launch, launch_context)
- return JsonStrategy.check_status_details(client, SERVER_NAME, url, command_launch, _project_roots_from_context(launch_context))
- var resolved_cli := cli_path if not cli_path.is_empty() else CliStrategy.resolve_cli_path(client)
- # #463: with no CLI binary, read the JSON fallback config so a
- # fallback-configured entry reports CONFIGURED instead of red.
- if resolved_cli.is_empty() and client.has_json_fallback():
- var fallback_launch := {}
- if client.command_shape != Client.CommandShape.NONE:
- fallback_launch = _resolved_or_discovered_launch(client, resolved_launch, launch_context)
- return JsonStrategy.check_status_details(client, SERVER_NAME, url, fallback_launch, _project_roots_from_context(launch_context))
- var cli_launch := {}
- if client.command_shape != Client.CommandShape.NONE:
- cli_launch = _resolved_or_discovered_launch(client, resolved_launch, launch_context)
- # #872: at a scope `path_template` can't see, a plain listing probe
- # cannot tell our entry from a leftover one in another scope. When
- # the descriptor offers a scope-aware probe, use it — same single
- # subprocess, but it reports which scope actually resolved.
- if (
- not client.cli_scope_status_template.is_empty()
- and CliStrategy.uses_scope_token(client)
- ):
- return CliStrategy.check_scope_status_details(
- client, SERVER_NAME, url, resolved_cli, cli_launch, McpSettings.client_scope()
- )
- return CliStrategy.check_status_details(client, SERVER_NAME, url, resolved_cli, cli_launch)
- return {"status": Client.Status.NOT_CONFIGURED, "error_msg": ""}
-
-
-## #872: the #463 JSON fallback writes the user-scope file whatever
-## `godot_ai/mcp_client_scope` says — that file IS `path_template`, and
-## `clients/_path_template.gd` has no project-relative token to aim it
-## elsewhere. Returning a bare "configured" would hide that the requested
-## scope was not honoured, so say so in the message.
-##
-## Deliberately a message and not a status. `_verify_post_state` compares the
-## post-write status against CONFIGURED and replaces the ok with an error on
-## any mismatch (pinned by test_verify_post_state_treats_drift_as_failure_
-## after_configure), so reporting this write as NOT_CONFIGURED or MISMATCH
-## would turn a *successful* configure into a user-facing failure — the same
-## false-error class #872's blocker was about, just moved onto the fallback
-## path. The entry is genuinely written and genuinely works; only its scope
-## differs from the request, and that is a caveat, not a failure.
-static func _note_unhonoured_scope(client: Client, result: Dictionary) -> Dictionary:
- if result.get("status") != "ok":
- return result
- if not CliStrategy.uses_scope_token(client):
- return result
- var scope := McpSettings.client_scope()
- if scope == McpSettings.DEFAULT_CLIENT_SCOPE:
- return result
- var noted := result.duplicate(true)
- noted["message"] = (
- "%s — wrote %s scope, not %s: the %s CLI wasn't found, and the file fallback can only write %s"
- % [
- str(result.get("message", "")),
- McpSettings.DEFAULT_CLIENT_SCOPE,
- scope,
- client.display_name,
- client.resolved_config_path(),
- ]
- )
- return noted
-
-
-## True when this client's CLI writes its entry somewhere `path_template`
-## cannot see, so reading that file would describe the wrong thing (#872).
-##
-## `claude mcp add --scope project` writes /.mcp.json and `--scope local`
-## writes a per-project block inside ~/.claude.json — neither is the top-level
-## `mcpServers` map `path_template` + `server_key_path` resolve to. Reading it
-## anyway makes `_verify_post_state` turn every successful project-scope
-## Configure into "configure ok but verification still reads Not configured",
-## and pins the dock row red (or green, describing a stale user-scope entry).
-##
-## Diverging sends status down the CLI probe instead: `mcp list` is
-## scope-agnostic and inherits the same cwd the register ran in, so read-back
-## and write agree by construction. The cost is losing exact launch-drift
-## detection — a stdout scan sees the command, not the full argv — which is the
-## documented trade for a status that is merely coarse instead of wrong.
-##
-## Deliberately says nothing about whether the CLI resolves (#879). An earlier
-## version walked PATH here and returned false for an unresolvable binary, on
-## the reasoning that configure would then have taken the #463 JSON fallback —
-## which writes user scope whatever the setting says — so the file was the
-## right thing to read again. That extra clause could not change any outcome:
-## this is only consulted from the one call site below, already guarded on
-## `has_json_fallback()`, and when the CLI does not resolve the very next
-## branch (`resolved_cli.is_empty() and client.has_json_fallback()`) takes the
-## fallback read anyway. All it bought was a second full `McpCliFinder.find`
-## sweep per status check, on top of the one that immediately follows.
-static func _scope_diverges_from_json_fallback(client: Client) -> bool:
- if not CliStrategy.uses_scope_token(client):
- return false
- return McpSettings.client_scope() != McpSettings.DEFAULT_CLIENT_SCOPE
-
-
-static func _resolved_or_discovered_launch(
- client: Client, resolved_launch: Dictionary, launch_context: Dictionary
-) -> Dictionary:
- var launch := (
- resolved_launch
- if not resolved_launch.is_empty()
- else resolve_attach_launch(launch_context)
- )
- return launch_for_client(client, launch)
-
-
-## After a configure/remove returns ok, re-read the live status. If it doesn't
-## match `expected`, replace the result with an error that names the actual
-## status and the resolved config path so the user can self-diagnose. The
-## strategy's own error path is left untouched — already actionable.
-static func _verify_post_state(
- client: Client,
- result: Dictionary,
- expected: Client.Status,
- url: String,
- action: String,
- resolved_launch: Dictionary = {},
- launch_context: Dictionary = {},
-) -> Dictionary:
- if result.get("status") != "ok":
- return result
- var details := _dispatch_check_status_with_cli_path_details(
- client, url, "", launch_context, resolved_launch
- )
- var actual := details.get("status", Client.Status.NOT_CONFIGURED)
- if actual == expected:
- return result
- var path_hint := _post_state_path_hint(client, str(details.get("resolved_scope", "")))
- return {
- "status": "error",
- "message": "%s reported %s ok but verification still reads %s (expected %s).%s" % [
- client.display_name, action,
- Client.status_label(actual), Client.status_label(expected),
- path_hint,
- ],
- }
-
-
-## Where to send the user when verification finds an entry that should not be
-## there. `resolved_config_path()` is `path_template` — right only for the
-## default user scope. Naming ~/.claude.json for a project-scope survivor sends
-## them to a file with nothing in it, which is worse than naming no file at all
-## (#879). The scope probe supplies `resolved_scope`; `path_template` has no
-## project-relative token (see `_note_unhonoured_scope`), so the project case is
-## described rather than resolved to a path.
-static func _post_state_path_hint(client: Client, resolved_scope: String) -> String:
- match resolved_scope:
- "project":
- return (
- " The surviving entry is project-scoped: inspect the .mcp.json in the"
- + " directory the editor was launched from and remove the godot-ai entry"
- + " by hand if needed."
- )
- "local":
- var local_path := client.resolved_config_path()
- if local_path.is_empty():
- return ""
- return (
- " The surviving entry is local-scoped: inspect the block for the"
- + " directory the editor was launched from in %s and remove the"
- + " godot-ai entry by hand if needed."
- ) % local_path
- _:
- var path := client.resolved_config_path()
- if path.is_empty():
- return ""
- return " Inspect %s and remove the godot-ai entry by hand if needed." % path
-
-
-## #877: Configure's first act is the all-scope pre-cleanup — it deletes the
-## `godot-ai` entry from every scope the descriptor can write to, including a
-## `.mcp.json` resolved against the CLI's working directory, which need not be
-## this project's folder. The manual-command panel that spells those removes
-## out is only shown when Configure FAILS (`mcp_dock.gd`), so on the success
-## path — the one where the sweep actually ran — this note is its only
-## disclosure. Empty for descriptors without a scope token: their single
-## implicit pass removes exactly the entry the register is about to rewrite,
-## which needs no warning (the same rule `_sweep_caveat` applies).
-##
-## "attempted", not "cleared": `CliStrategy.configure` discards every
-## pre-cleanup result, and `mcp remove` exits non-zero for an absent entry as
-## well as for a real failure, so a timed-out or failed remove leaves the scope
-## untouched while the register still returns ok. The note names what ran,
-## not what it can prove.
-static func configure_sweep_note(id: String) -> String:
- var client := ClientRegistry.get_by_id(id)
- if client == null or client.config_type != "cli":
- return ""
- if client.cli_unregister_template.is_empty() or not CliStrategy.uses_scope_token(client):
- return ""
- return "attempted to clear %s from %s" % [
- SERVER_NAME, ", ".join(CliStrategy.cleanup_scopes(client)),
- ]
-
-
-static func manual_command(id: String) -> String:
- var client := ClientRegistry.get_by_id(id)
- if client == null:
- return ""
- var path_resolution := client.resolved_config_path_details()
- var path_error := str(path_resolution.get("error", ""))
- if not path_error.is_empty():
- return "Config path unavailable: %s" % path_error
- var context := capture_launch_context() if client.command_shape != Client.CommandShape.NONE else {}
- var launch := (
- launch_for_client(client, resolve_attach_launch(context))
- if client.command_shape != Client.CommandShape.NONE
- else {}
- )
- var cmd := ManualCommand.build(
- client,
- SERVER_NAME,
- server_url_from(context),
- str(path_resolution.get("path", "")),
- launch,
- _project_roots_from_context(context),
- )
- if cmd.is_empty():
- return cmd
- ## #507: when the allow-host opt-in names a non-loopback range, also
- ## surface the LAN URL so the user can copy-paste the right address into
- ## a remote agent. Informational only — configure/remove still WRITE the
- ## loopback URL above; nothing about the config-file contract changes.
- var note := McpAllowHosts.lan_url_note(allow_hosts(), IP.get_local_addresses(), http_port())
- if not note.is_empty():
- cmd += "\n\n" + note
- return cmd
-
-
-static func config_path(id: String) -> String:
- var client := ClientRegistry.get_by_id(id)
- return client.resolved_config_path() if client != null else ""
-
-
-## Resolve the highest-precedence config tier that actually contains the server
-## entry. For Pi-style merge clients, returns the tier path the entry lives in
-## (e.g. `~/.pi/agent/.mcp.json`) instead of the lowest-tier `path_template`
-## `config_path()` returns. Falls back to `path_template` when no entry is
-## found anywhere. Empty `launch_context` is allowed; command-shape clients
-## synthesize one via `capture_launch_context()` so dock row construction can
-## call this without first capturing launch state.
-static func effective_config_path(id: String, launch_context: Dictionary = {}) -> String:
- var client := ClientRegistry.get_by_id(id)
- if client == null:
- return ""
- var resolution := client.resolved_config_path_details()
- var fallback := str(resolution.get("path", ""))
- var context := launch_context
- if context.is_empty() and client.command_shape != Client.CommandShape.NONE:
- context = capture_launch_context()
- var roots := _project_roots_from_context(context)
- var target := JsonStrategy.manual_target_details(client, SERVER_NAME, fallback, roots)
- if bool(target.get("ok", false)):
- return str(target.get("path", fallback))
- return fallback
-
-
-## Path that `_check_status_merged` would consider authoritative for the
-## server entry, or `path_template` when no entry is found anywhere.
-## For Pi-style merge clients this is the latest project tier when one
-## exists (matching the F2 last-wins status logic), falling back to the
-## latest global tier, then to `path_template`.
-##
-## The dock uses this for Open/Reveal so the file the user inspects is
-## the same file that drives the status check — `effective_config_path`
-## fails closed (returns `path_template`) when there are multiple project
-## tiers, which historically sent users to the wrong file (codex round 3,
-## F-3-4). Splitting this from `effective_config_path` keeps the manual
-## instructions fail-closed while letting the dock follow the
-## authoritative tier.
-static func effective_authoritative_path(id: String, launch_context: Dictionary = {}) -> String:
- var client := ClientRegistry.get_by_id(id)
- if client == null:
- return ""
- var resolution := client.resolved_config_path_details()
- var fallback := str(resolution.get("path", ""))
- var context := launch_context
- if context.is_empty() and client.command_shape != Client.CommandShape.NONE:
- context = capture_launch_context()
- var roots := _project_roots_from_context(context)
- var authoritative: String = JsonStrategy.authoritative_tier_path(client, SERVER_NAME, roots)
- if not authoritative.is_empty():
- return authoritative
- return fallback
-
-
-static func is_installed(id: String) -> bool:
- var client := ClientRegistry.get_by_id(id)
- return client != null and client.is_installed()
-
-
-# --- Server command discovery --------------------------------------------
-#
-# Three-tier resolution:
-# 1. .venv python — dev checkout, source code
-# 2. uvx — user install, published package from PyPI
-# 3. godot-ai CLI — system-wide pip/pipx/uv install
-
-static func get_plugin_version() -> String:
- var cfg := ConfigFile.new()
- if cfg.load("res://addons/godot_ai/plugin.cfg") == OK:
- return cfg.get_value("plugin", "version", "0.0.1")
- return "0.0.1"
-
-
-## Strip PEP 440 local build metadata for PyPI pins: `3.0.2+local.1` → `3.0.2`.
-## Pre-release segments (`3.1.0-rc1`) are preserved — only `+…` is removed.
-static func _pypi_pin_version(version: String) -> String:
- var v := version.strip_edges()
- var plus := v.find("+")
- if plus >= 0:
- v = v.substr(0, plus)
- return v
-
-
-## Resolve the client-owned `godot-ai attach` command from a main-thread
-## LaunchContext. Discovery itself is worker-safe: path/environment lookup is
-## snapshot-backed and subprocess probes are wall-clock bounded.
-##
-## `discovery_override` is a data-only test seam. Supplying a key (including
-## an empty value) bypasses that tier's live lookup; `system_version_result`
-## bypasses the real `godot-ai --version` subprocess.
-static func resolve_attach_launch(
- launch_context: Dictionary, discovery_override: Dictionary = {}
-) -> Dictionary:
- ## Test overrides always bypass the session cache so fixture-controlled
- ## discovery remains deterministic. Production results are keyed by every
- ## setting that affects the rendered command; a port/domain/version change
- ## therefore cannot reuse stale arguments.
- if not discovery_override.is_empty():
- return _resolve_attach_launch_uncached(launch_context, discovery_override)
- var cache_key := _attach_launch_cache_key(launch_context)
- _attach_launch_cache_mutex.lock()
- if _attach_launch_cache.has(cache_key):
- var cached: Dictionary = _attach_launch_cache[cache_key].duplicate(true)
- _attach_launch_cache_mutex.unlock()
- return cached
- ## Keep the cache lock through the bounded discovery probes. This cold path
- ## runs at most once per distinct context and prevents simultaneous status
- ## workers from repeating the same subprocess probes. Invalidation waits for
- ## the in-flight result, then clears it, so stale work cannot repopulate a
- ## freshly invalidated cache.
- var resolved := _resolve_attach_launch_uncached(launch_context)
- _attach_launch_cache[cache_key] = resolved.duplicate(true)
- _attach_launch_cache_mutex.unlock()
- return resolved
-
-
-static func _resolve_attach_launch_uncached(
- launch_context: Dictionary, discovery_override: Dictionary = {}
-) -> Dictionary:
- for key in ["http_port", "ws_port", "excluded_domains", "plugin_version", "allow_dev_venv", "platform"]:
- if not launch_context.has(key):
- return _attach_discovery_error("Launch context is missing `%s`; retry Configure." % key)
-
- var plugin_version := str(launch_context.get("plugin_version", "")).strip_edges()
- if plugin_version.is_empty():
- return _attach_discovery_error("The bundled godot-ai version is unavailable; reinstall the plugin and retry Configure.")
-
- var common_args: Array[String] = [
- "attach",
- "--port", str(int(launch_context.get("http_port", DEFAULT_HTTP_PORT))),
- "--ws-port", str(int(launch_context.get("ws_port", DEFAULT_WS_PORT))),
- ]
- var exclusions := str(launch_context.get("excluded_domains", "")).strip_edges()
- if not exclusions.is_empty():
- common_args.append_array(["--exclude-domains", exclusions])
- ## Default true when the key is absent (hand-built contexts in tests, stale
- ## pre-upgrade snapshots) — matching the server's send-by-default posture.
- ## Toggling the setting changes the rendered argv, so existing entries read
- ## CONFIGURED_MISMATCH and the dock offers Reconfigure, like any other
- ## launch-affecting value.
- if not bool(launch_context.get("telemetry_enabled", true)):
- common_args.append("--disable-telemetry")
-
- var venv_python := ""
- if discovery_override.has("venv_python"):
- venv_python = str(discovery_override["venv_python"])
- elif bool(launch_context.get("allow_dev_venv", true)):
- venv_python = _cached_venv_python()
- if bool(launch_context.get("allow_dev_venv", true)) and not venv_python.is_empty():
- var venv_args: Array[String] = ["-m", "godot_ai"]
- venv_args.append_array(common_args)
- return _finalize_attach_launch(
- "dev_venv", venv_python, venv_args, launch_context, discovery_override
- )
-
- var uvx := ""
- if discovery_override.has("uvx_path"):
- uvx = str(discovery_override["uvx_path"])
- else:
- ## Strict lookup for attach entries: never write a bare `uvx` command
- ## that a GUI-launched client may be unable to resolve from its PATH.
- uvx = find_uvx()
- if not uvx.is_empty():
- var uvx_args := UvResolution.args()
- uvx_args.append_array([
- "--link-mode", "copy",
- "--from", "godot-ai==%s" % _pypi_pin_version(plugin_version),
- "godot-ai",
- ])
- uvx_args.append_array(common_args)
- return _finalize_attach_launch(
- "uvx", uvx, uvx_args, launch_context, discovery_override
- )
-
- var system_cmd := ""
- if discovery_override.has("system_path"):
- system_cmd = str(discovery_override["system_path"])
- else:
- system_cmd = _find_system_install()
- if not system_cmd.is_empty():
- var probe: Dictionary
- if discovery_override.has("system_version_result"):
- probe = discovery_override["system_version_result"] as Dictionary
- else:
- probe = McpCliExec.run(system_cmd, ["--version"], _DISCOVERY_TIMEOUT_MS, false)
- var version_check := _system_version_from_probe(probe)
- if bool(version_check.get("ok", false)):
- var found_version := str(version_check.get("version", ""))
- if found_version == plugin_version:
- return _finalize_attach_launch(
- "system", system_cmd, common_args, launch_context, discovery_override
- )
- return _attach_discovery_error(
- "System godot-ai is version %s, but this plugin requires %s. Install uv or update the system package, then retry Configure."
- % [found_version, plugin_version]
- )
- if bool(probe.get("timed_out", false)):
- return _attach_discovery_error(
- "Timed out checking the system godot-ai version. Install uv or repair the system command, then retry Configure."
- )
- return _attach_discovery_error(
- "Could not verify the system godot-ai version. Install uv or repair the system command, then retry Configure."
- )
-
- return _attach_discovery_error(
- "No compatible godot-ai launcher was found. Install uv (provides uvx), then retry Configure."
- )
-
-
-## Return a launch shape that cannot allocate a visible console on Windows.
-## The development tier can execute its sibling pythonw directly. uvx and the
-## system entry point still need their own environments, so pythonw acts only
-## as a stdio-preserving, CREATE_NO_WINDOW process bootstrap for those tiers.
-static func _finalize_attach_launch(
- tier: String,
- command: String,
- args: Array[String],
- launch_context: Dictionary,
- discovery_override: Dictionary,
-) -> Dictionary:
- if str(launch_context.get("platform", "")) != "Windows":
- return {"ok": true, "tier": tier, "command": command, "args": args}
-
- var pythonw := _resolve_consoleless_python(command, tier, discovery_override)
- if pythonw.is_empty():
- return _attach_discovery_error(
- "Windows requires pythonw.exe to launch the MCP bridge without opening a terminal window. Repair this Python or uv installation, then retry Configure."
- )
-
- ## `console_command`/`console_args` carry the unwrapped console-subsystem
- ## launch for clients that opt out of pythonw via
- ## `needs_consoleless_launcher = false` (#863). Strategies only consume
- ## `command`/`args`/`ok`; `launch_for_client` swaps the shapes per client.
- if tier == "dev_venv":
- return {
- "ok": true, "tier": tier, "command": pythonw, "args": args,
- "console_command": command, "console_args": args,
- }
-
- var wrapped_args: Array[String] = ["-c", _WINDOWS_STDIO_BOOTSTRAP, command]
- wrapped_args.append_array(args)
- return {
- "ok": true, "tier": tier, "command": pythonw, "args": wrapped_args,
- "console_command": command, "console_args": args,
- }
-
-
-## Select the launch shape a specific client should see. Clients with
-## `needs_consoleless_launcher = false` (Antigravity, #863) get the plain
-## console command captured by `_finalize_attach_launch`; everyone else keeps
-## the pythonw shape unchanged. Idempotent: the returned dict carries no
-## console keys, so a second application is a no-op.
-static func launch_for_client(client: Client, launch: Dictionary) -> Dictionary:
- if client == null or client.needs_consoleless_launcher:
- return launch
- if not launch.has("console_command"):
- return launch
- var selected := launch.duplicate(true)
- selected["command"] = selected["console_command"]
- selected["args"] = selected["console_args"]
- selected.erase("console_command")
- selected.erase("console_args")
- return selected
-
-
-static func _resolve_consoleless_python(
- command: String, tier: String, discovery_override: Dictionary
-) -> String:
- ## Data-only override keeps resolver tests independent of the host's Python.
- if discovery_override.has("consoleless_python"):
- return str(discovery_override["consoleless_python"])
-
- ## Venv/system console-script launchers normally keep pythonw beside their
- ## python.exe. The dev tier must use that exact interpreter so godot_ai is
- ## imported from the selected checkout rather than some unrelated install.
- var sibling := command.get_base_dir().path_join("pythonw.exe")
- if FileAccess.file_exists(sibling):
- return sibling
- if tier == "dev_venv":
- return ""
-
- ## uvx may be installed without a PATH-visible CPython. Ask its sibling uv
- ## for the already-managed system interpreter; Godot AI's existing uvx
- ## server launch ensures one normally exists before client configuration.
- if tier == "uvx":
- var uv := command.get_base_dir().path_join("uv.exe")
- if not FileAccess.file_exists(uv):
- uv = CliFinder.find(["uv.exe"])
- if not uv.is_empty():
- var probe := McpCliExec.run(
- uv, ["python", "find", "--system"], _DISCOVERY_TIMEOUT_MS, false
- )
- if int(probe.get("exit_code", -1)) == 0:
- var python := str(probe.get("stdout", "")).strip_edges()
- if not python.is_empty():
- var managed_pythonw := _consoleless_python_for_interpreter(python)
- if not managed_pythonw.is_empty():
- return managed_pythonw
-
- ## A system Python GUI launcher is sufficient for the non-dev bootstrap;
- ## it does not import godot_ai itself.
- return CliFinder.find(["pythonw.exe"])
-
-
-## uv can return a launcher in ~/.local/bin whose GUI interpreter lives in
-## the managed Python installation. Ask that interpreter for its base path.
-## The optional result keeps tests independent of installed executables.
-static func _consoleless_python_for_interpreter(
- python: String, probe_result: Dictionary = {}
-) -> String:
- if not python.is_absolute_path() or not FileAccess.file_exists(python):
- return ""
- var sibling := python.get_base_dir().path_join("pythonw.exe")
- if FileAccess.file_exists(sibling):
- return sibling
- var probe := probe_result
- if probe.is_empty():
- probe = McpCliExec.run(
- python, ["-I", "-c", "import sys; print(getattr(sys, '_base_executable', sys.executable))"],
- _DISCOVERY_TIMEOUT_MS, false
- )
- if int(probe.get("exit_code", -1)) != 0 or not probe.get("stdout") is String:
- return ""
- var base_python := str(probe["stdout"]).strip_edges()
- if base_python.contains("\n") or base_python.contains("\r"):
- return ""
- if not base_python.is_absolute_path() or not FileAccess.file_exists(base_python):
- return ""
- var base_pythonw := base_python.get_base_dir().path_join("pythonw.exe")
- return base_pythonw if FileAccess.file_exists(base_pythonw) else ""
-
-
-static func _system_version_from_probe(probe: Dictionary) -> Dictionary:
- if int(probe.get("exit_code", -1)) != 0:
- return {"ok": false}
- var output := str(probe.get("stdout", "")).strip_edges()
- var pattern := RegEx.new()
- if pattern.compile("^godot-ai\\s+([^\\s]+)(?:\\s|$)") != OK:
- return {"ok": false}
- var matched := pattern.search(output)
- if matched == null:
- return {"ok": false}
- return {"ok": true, "version": matched.get_string(1)}
-
-
-static func _attach_discovery_error(message: String) -> Dictionary:
- return {"ok": false, "error": message}
-
-
-## Override for the dev-vs-user heuristic. Accepted values:
-## "dev" — force dev-checkout mode (skip update check + self-install)
-## "user" — force user-install mode (run update check, allow self-install)
-## as long as the data-safety guard (addons_dir_is_symlink) passes
-## other / unset — "auto": fall back to the .venv-proximity heuristic
-##
-## Use `user` to test the AssetLib self-update flow from inside a dev
-## checkout (there's a .venv nearby but `addons/godot_ai` is a plain copy —
-## e.g. after unpacking a release zip into `test_project/`).
-##
-## Two ways to set it, resolved in priority order:
-## 1. EditorSettings → `godot_ai/mode_override` — set manually via
-## Editor Settings (no dock UI writes it today); persists
-## per-editor-install and wins over the env var so an editor-side
-## choice always takes effect without relaunching.
-## 2. Env var `GODOT_AI_MODE` — useful for CLI launches and CI.
-const MODE_OVERRIDE_ENV := "GODOT_AI_MODE"
-const MODE_OVERRIDE_SETTING := "godot_ai/mode_override"
-
-
-static func mode_override() -> String:
- # 1. EditorSetting wins — the user explicitly set it via Editor Settings.
- # _editor_setting_lookup handles the `Engine.is_editor_hint()` gate
- # (no-op in the game subprocess; see CLAUDE.md "Game-side code") and
- # serves worker threads from a main-thread-warmed snapshot — this
- # runs on the #678 startup walk's discovery worker, and
- # EditorInterface/EditorSettings are not thread-safe (#691).
- var setting: Variant = _editor_setting_lookup(MODE_OVERRIDE_SETTING)
- if setting != null:
- var setting_val := str(setting).strip_edges().to_lower()
- if setting_val == "dev" or setting_val == "user":
- return setting_val
- # 2. Env var fallback. env_lookup, not OS.get_environment: same
- # worker-thread reason (#691).
- var raw := McpPathTemplate.env_lookup(MODE_OVERRIDE_ENV).strip_edges().to_lower()
- if raw == "dev" or raw == "user":
- return raw
- return ""
-
-
-static func is_dev_checkout() -> bool:
- match mode_override():
- "dev":
- return true
- "user":
- return false
- return not _find_venv_python().is_empty()
-
-
-## Data-safety check for self-install: is `res://addons/godot_ai` a symbolic
-## link? In a dev checkout this points at the canonical `plugin/` source
-## tree, and writing files into it would clobber tracked source. This check
-## is independent of `is_dev_checkout()` so a forced-user mode override
-## still cannot extract a release zip over the symlink.
-static func addons_dir_is_symlink() -> bool:
- return _is_symlink(ProjectSettings.globalize_path("res://addons/godot_ai"))
-
-
-## Mirrors the idiom used in `mcp_dock.gd::_resolve_plugin_symlink_target` —
-## open the parent dir and ask Godot via `DirAccess.is_link()`, which
-## handles symlinks on POSIX and reparse points on Windows natively.
-static func _is_symlink(path: String) -> bool:
- if path.is_empty():
- return false
- var dir := DirAccess.open(path.get_base_dir())
- if dir == null:
- ## This is a data-safety guard (a symlinked addons dir is a dev
- ## checkout self-update must never write through). When the path
- ## exists but its parent can't be opened, we can't PROVE it isn't
- ## a link — fail closed and treat it as one (#711).
- return DirAccess.dir_exists_absolute(path) or FileAccess.file_exists(path)
- return dir.is_link(path)
-
-
-static func get_server_command() -> Array[String]:
- ## `mode_override() == "user"` skips the dev_venv tier even when a nearby
- ## .venv exists — the override then becomes an actual workaround for
- ## the "user venv misidentified as dev checkout" bug, not just a
- ## cosmetic relabel.
- if mode_override() != "user":
- var venv_python := _cached_venv_python()
- if not venv_python.is_empty():
- print("MCP | using dev venv: %s" % venv_python)
- return [venv_python, "-m", "godot_ai"]
-
- var uvx := find_uvx()
- if not uvx.is_empty():
- var version := get_plugin_version()
- ## PEP 440 local build tags (e.g. 3.0.2+local.1) are not on PyPI.
- ## Pin uvx to the public base version so the server still boots;
- ## checkout-local extras need the dev_venv tier above
- ## (symlink/junction → repo .venv).
- var pypi_version := _pypi_pin_version(version)
- ## Pin to the EXACT plugin version rather than `~=`. Under the
- ## tilde form, uvx was happy to reuse a cached tool env that matched
- ## the minor constraint — so an install that first spawned 1.2.0 kept
- ## using 1.2.0 even after 1.2.1/1.2.2 landed. Exact pinning makes the
- ## cache key version-specific: if the cached env matches, fast hit;
- ## otherwise uvx installs the exact version fresh. Keeps plugin and
- ## server version in lockstep without needing `--refresh-package` on
- ## every spawn. See issue #133.
- if pypi_version != version:
- print(
- "MCP | using uvx (godot-ai==%s; local plugin %s not on PyPI)"
- % [pypi_version, version]
- )
- else:
- print("MCP | using uvx (godot-ai==%s)" % pypi_version)
- var cmd: Array[String] = [uvx]
- cmd.append_array(UvResolution.args())
- cmd.append_array(["--from", "godot-ai==%s" % pypi_version, "godot-ai"])
- return cmd
-
- var system_cmd := _find_system_install()
- if not system_cmd.is_empty():
- print("MCP | using system install: %s" % system_cmd)
- return [system_cmd]
-
- push_warning("MCP | no server found — install uv or run: pip install godot-ai")
- return []
-
-
-## Which tier `get_server_command` would resolve to, without side-effects.
-## Returned as a stable string so handshakes and session_list can expose it
-## to MCP callers. Values track the `Literal` on the Python side.
-static func get_server_launch_mode() -> String:
- if mode_override() != "user" and not _cached_venv_python().is_empty():
- return "dev_venv"
- if not find_uvx().is_empty():
- return "uvx"
- if not _find_system_install().is_empty():
- return "system"
- return "unknown"
-
-
-## Wall-clock budget for the Configure-time pre-warm. Far above
-## `McpCliExec.DEFAULT_TIMEOUT_MS` (8s) on purpose: this call is *expected* to
-## take tens of seconds on a cold cache — downloading and unpacking the tool
-## environment is the entire point — so the usual CLI-registry budget would
-## kill it right when it is doing useful work. Still bounded so a wedged uv
-## can't hold the worker thread forever.
-const PREWARM_TIMEOUT_MS := 180000
-
-
-static func find_uvx() -> String:
- return CliFinder.find(_uvx_cli_names())
-
-
-## Pure argv builder for the pre-warm spawn, split out so tests can pin the
-## exact command without spawning a process. Empty array when there is no
-## version to pin, or when the version carries characters outside the PEP
-## 440 alphabet — the value can originate from a GitHub release tag, and
-## while OS.create_process argv can't be shell-injected, constraining the
-## pin keeps a hostile tag from smuggling anything into the requirement
-## spec uv parses (#890 review).
-static func prewarm_server_package_argv(version: String) -> Array[String]:
- var pinned := version.strip_edges()
- if pinned.is_empty():
- return []
- ## No `*`: PEP 440 reserves it for prefix-match SPECIFIERS (`==3.2.*`),
- ## not version identifiers — letting it through would turn the exact pin
- ## into a prefix match (#890 CodeRabbit).
- var re := RegEx.new()
- if re.compile("^[A-Za-z0-9.+!-]+$") != OK or re.search(pinned) == null:
- return []
- var args := UvResolution.args()
- args.append_array(["--from", "godot-ai==%s" % pinned, "godot-ai", "--version"])
- return args
-
-
-## Bounded worker-thread prewarm for Configure and the post-update client
-## migration barrier (#851, and the reconnect timeouts reported alongside it).
-##
-## The Configure case that bites is when the plugin ADOPTS an already-running
-## server (no uvx
-## spawn, so nothing warms the env), the user clicks Configure, and the first
-## client launch is the one that pays for building the whole tool environment.
-## That build is ~67 packages; a warm launch is ~0.1s. Two symptoms fall out
-## of the cold path: a visible terminal window on Windows (#851), and a spawn
-## that overruns the MCP client's default 30s connect timeout, which the user
-## sees as the Godot AI tools silently disappearing.
-##
-## The work is deliberately owned and joined rather than detached. Configure is
-## an explicit click, so the cost is attributable and the Dock can show it as
-## progress rather than as an unexplained pause. Runs on the dock's client
-## action worker thread — never the main thread — and `McpCliExec.run` bounds
-## it, so a wedged uv cannot trap the worker.
-##
-## Best-effort by contract. The config file is already written and correct
-## when this runs; a failure here only means the next client launch pays the
-## cold cost it always used to. Callers must NOT downgrade a successful
-## Configure because of this result.
-##
-## Returns the `McpCliExec.run` dict, plus `skipped` (true when there is no
-## uvx tier to warm or no usable version pin).
-static func prewarm_server_package_blocking(
- version: String,
- timeout_ms: int = PREWARM_TIMEOUT_MS,
- cancel_check: Callable = Callable(),
-) -> Dictionary:
- var args := prewarm_server_package_argv(version)
- if args.is_empty():
- return {"skipped": true, "reason": "no version pin"}
- var uvx := find_uvx()
- if uvx.is_empty():
- ## dev-venv and system tiers have no per-version cache to warm.
- return {"skipped": true, "reason": "no uvx"}
- var command: Array[String] = [uvx]
- command.append_array(args)
- var result := McpCliExec.run(
- uvx,
- args,
- timeout_ms,
- true,
- cancel_check,
- UvResolution.is_production_command(command),
- )
- result["skipped"] = false
- return result
-
-
-## Decide whether the freshly-written entry has an environment worth warming.
-##
-## Pure and side-effect free, split from the spawn below so tests can pin the
-## decision without building a real uv environment — the same split
-## `prewarm_server_package_argv` uses for the argv (#890).
-##
-## Warms only the `uvx` tier: dev-venv and system launches run an
-## already-installed package and have no per-version environment to build.
-## Resolving the tier here rather than in the dock keeps launcher knowledge in
-## the configurator; the dock stays free of tier branching.
-##
-## Returns `{warm: bool, version: String, reason: String}`.
-static func prewarm_attach_plan(
- launch_context: Dictionary, discovery_override: Dictionary = {}
-) -> Dictionary:
- var launch := resolve_attach_launch(launch_context, discovery_override)
- if not bool(launch.get("ok", false)):
- return {"warm": false, "version": "", "reason": "launch discovery failed"}
- var tier := str(launch.get("tier", ""))
- if tier != "uvx":
- return {"warm": false, "version": "", "reason": "tier %s has no env to warm" % tier}
- var version := _pypi_pin_version(str(launch_context.get("plugin_version", "")))
- if version.is_empty():
- return {"warm": false, "version": "", "reason": "no version pin"}
- return {"warm": true, "version": version, "reason": ""}
-
-
-## Warm the environment the freshly-written entry will actually launch.
-## Thin wrapper over `prewarm_attach_plan` + `prewarm_server_package_blocking`.
-static func prewarm_attach_launch(
- launch_context: Dictionary,
- timeout_ms: int = PREWARM_TIMEOUT_MS,
- discovery_override: Dictionary = {},
- cancel_check: Callable = Callable(),
-) -> Dictionary:
- var plan := prewarm_attach_plan(launch_context, discovery_override)
- if not bool(plan.get("warm", false)):
- return {"skipped": true, "reason": str(plan.get("reason", ""))}
- return prewarm_server_package_blocking(
- str(plan.get("version", "")), timeout_ms, cancel_check
- )
-
-
-static func _uvx_cli_names() -> Array[String]:
- var names: Array[String] = []
- names.append("uvx.exe" if OS.get_name() == "Windows" else "uvx")
- return names
-
-
-## Drop the `CliFinder` cache for the platform-specific uvx binary
-## name. Pairs with `invalidate_uv_version_cache()` so the dock's
-## `_on_install_uv` can refresh both caches with one call each. The
-## OS-specific name matters: Windows caches under `uvx.exe`, every
-## other platform under `uvx`; hard-coding `"uvx"` here would leave
-## the CLI-path cache stale on Windows after a fresh install and the
-## dock would keep showing "uv: not found" for the rest of the session.
-static func invalidate_uvx_cli_cache() -> void:
- for name in _uvx_cli_names():
- CliFinder.invalidate(name)
-
-
-## Drop the entire `CliFinder` cache. Called from any explicit-user-action
-## refresh path (`force=true` in `_request_client_status_refresh` — manual
-## Refresh button, popup-open, compat wrapper, future external API) so a
-## freshly-installed CLI (claude, codex, gemini, …) gets detected without
-## an editor restart. Per-CLI invalidation (`invalidate_uvx_cli_cache`) is
-## preferred when the dock knows which binary changed; this catch-all
-## handles the "any CLI may have been installed since the last sweep" case.
-##
-## Thread safety: `CliFinder.invalidate()` guards `_cache` / `_searched`
-## with a mutex so it can race safely against worker threads calling
-## `find()` from `_run_client_action_worker`. The mutex is held only
-## across the dictionary clear, never across the bounded subprocess lookup,
-## so this call can never block the main thread on a subprocess.
-static func invalidate_cli_cache() -> void:
- CliFinder.invalidate()
-
-
-static var _uv_version_cache: String = ""
-static var _uv_version_searched: bool = false
-
-
-## Cached for the editor session. The dock's `_refresh_setup_status`
-## (called via `call_deferred` from `_build_ui`) calls this on the
-## main thread in user mode, so the cold `uvx --version` probe is
-## wall-clock bounded and cached. Subsequent calls (focus-in refresh,
-## manual Refresh clicks) reuse the cached string.
-##
-## Invalidate via `invalidate_uv_version_cache()` when the user
-## installs / reinstalls uv via the dock so the next refresh reflects
-## the new install. The dock's `_on_install_uv` calls this alongside
-## `CliFinder.invalidate("uvx")` to clear both the path cache and
-## the version cache in one place.
-static func check_uv_version() -> String:
- if _uv_version_searched:
- return _uv_version_cache
- var uvx := find_uvx()
- if uvx.is_empty():
- _uv_version_searched = true
- _uv_version_cache = ""
- return ""
- var result := McpCliExec.run(uvx, ["--version"], _DISCOVERY_TIMEOUT_MS, false)
- if int(result.get("exit_code", -1)) == 0:
- var lines := PackedStringArray(str(result.get("stdout", "")).split("\n"))
- _uv_version_cache = lines[0].strip_edges() if lines.size() > 0 else ""
- else:
- _uv_version_cache = ""
- _uv_version_searched = true
- return _uv_version_cache
-
-
-static func invalidate_uv_version_cache() -> void:
- _uv_version_searched = false
- _uv_version_cache = ""
-
-
-## True when a probe has run this session and came back empty — i.e. the
-## dock is currently rendering "uv: not found". Lets callers decide when
-## a re-probe is worth paying for (server-connect transition, manual
-## Refresh) without ever re-probing once uv has been found.
-static func uv_probe_negative() -> bool:
- return _uv_version_searched and _uv_version_cache.is_empty()
-
-
-## Drop both uv caches — the resolved uvx path AND the cached
-## `uvx --version` output — so the next check_uv_version() re-runs the
-## full detection. #739: a probe that fails once at editor startup
-## (contended spawn, cold Defender scan, stale PATH under a
-## Steam-launched editor) used to pin "uv: not found" for the whole
-## session; the Install-uv click was the only invalidation path. Callers
-## invoke this on events that suggest the failure was transient.
-static func invalidate_uv_detection() -> void:
- invalidate_uvx_cli_cache()
- invalidate_uv_version_cache()
- _attach_launch_cache_mutex.lock()
- _attach_launch_cache.clear()
- _attach_launch_cache_mutex.unlock()
-
-
-static var _venv_python_cache: String = ""
-static var _venv_python_searched: bool = false
-## #678 worker threads write this cache while main-thread callers read
-## it; same lock discipline as McpCliFinder (clients/_cli_finder.gd).
-static var _venv_mutex: Mutex = Mutex.new()
-static var _attach_launch_cache := {}
-static var _attach_launch_cache_mutex := Mutex.new()
-
-
-static func _attach_launch_cache_key(launch_context: Dictionary) -> String:
- return JSON.stringify([
- launch_context.get("http_port", null),
- launch_context.get("ws_port", null),
- launch_context.get("excluded_domains", null),
- launch_context.get("plugin_version", null),
- launch_context.get("allow_dev_venv", null),
- launch_context.get("platform", null),
- launch_context.get("telemetry_enabled", null),
- ])
-
-
-static func _cached_venv_python() -> String:
- _venv_mutex.lock()
- if not _venv_python_searched:
- _venv_python_cache = _find_venv_python()
- _venv_python_searched = true
- var cached := _venv_python_cache
- _venv_mutex.unlock()
- return cached
-
-
-## Absolute path to `res://addons/godot_ai`, resolving Windows junctions /
-## POSIX symlinks via `DirAccess.read_link`. Unresolved globalize_path only
-## walks the *logical* project path (e.g. MyGame/addons/godot_ai → MyGame)
-## and never reaches a fork checkout's `.venv` (…/godot-ai/.venv).
-static func resolve_addons_realpath() -> String:
- var addons_path := ProjectSettings.globalize_path("res://addons/godot_ai").rstrip("/").rstrip("\\")
- if addons_path.is_empty():
- return ""
- var parent := addons_path.get_base_dir()
- var dir := DirAccess.open(parent)
- if dir != null and dir.is_link(addons_path):
- var target := dir.read_link(addons_path)
- if not target.is_empty():
- if target.is_relative_path():
- target = parent.path_join(target).simplify_path()
- return target.rstrip("/").rstrip("\\")
- return addons_path
-
-
-static func _find_venv_python() -> String:
- ## Optional hard override (junction edge cases / CI).
- var env_py := McpPathTemplate.env_lookup("GODOT_AI_VENV_PYTHON").strip_edges()
- if not env_py.is_empty():
- if FileAccess.file_exists(env_py):
- return env_py
- ## An explicit override pointing nowhere is a misconfiguration the
- ## user needs to see — falling through silently would make the dev
- ## venv appear randomly ignored.
- push_warning(
- "godot-ai: GODOT_AI_VENV_PYTHON is set but no file exists at '%s'; ignoring override."
- % env_py
- )
- ## 1) Walk up from the open project (classic monorepo / test_project layout).
- var from_project := _find_venv_python_in(
- ProjectSettings.globalize_path("res://").rstrip("/").rstrip("\\")
- )
- if not from_project.is_empty():
- return from_project
- ## 2) Junctioned plugin: resolve reparse target, then walk up to fork root.
- var addons_real := resolve_addons_realpath()
- if not addons_real.is_empty():
- var from_addons := _find_venv_python_in(addons_real)
- if not from_addons.is_empty():
- return from_addons
- return ""
-
-
-## Pure path-based lookup so tests can drive it with a scratch dir instead of
-## monkey-patching `res://`. Only treats a `.venv/bin/python` as a godot-ai dev
-## venv if a sibling `src/godot_ai/` exists in the same parent dir — otherwise
-## an unrelated user venv (e.g. `~/.venv` from a data-science side project)
- ## gets picked up and `python -m godot_ai` fails with ModuleNotFoundError.
- ## The detection therefore has to be right before launch.
-static func _find_venv_python_in(start_dir: String) -> String:
- var dir := start_dir.rstrip("/").rstrip("\\")
- var python_name := "python" if OS.get_name() != "Windows" else "python.exe"
- var venv_dir := ".venv/bin/" if OS.get_name() != "Windows" else ".venv/Scripts/"
- ## 8 hops: game project roots are shallow; junctioned plugins sit at
- ## /plugin/addons/godot_ai (4) and nested worktrees may be deeper.
- for i in 8:
- var venv_path := dir.path_join(venv_dir + python_name)
- if FileAccess.file_exists(venv_path) and DirAccess.dir_exists_absolute(dir.path_join("src/godot_ai")):
- return venv_path
- var parent := dir.get_base_dir()
- if parent == dir or parent.is_empty():
- break
- dir = parent
- return ""
-
-
-## Walk up from `start_dir` looking for a sibling `src/godot_ai/` — returns
-## the absolute path of the enclosing `src/` dir, or "". Used by the dev
-## server launcher to prepend the caller's own source to PYTHONPATH so a
-## worktree-launched editor serves the worktree's Python, not the root
-## repo's editable install. See #84.
-static func find_worktree_src_dir(start_dir: String) -> String:
- var dir := start_dir.rstrip("/")
- for i in 5:
- var candidate := dir.path_join("src/godot_ai")
- if DirAccess.dir_exists_absolute(candidate):
- return dir.path_join("src")
- var parent := dir.get_base_dir()
- if parent == dir:
- break
- dir = parent
- return ""
-
-
-## Delegates to McpCliFinder rather than shelling out to which/where
-## directly: the finder adds the well-known-install-dirs and login-shell
-## PATH tiers plus its per-exe cache, and this drops the private
-## `_pick_best_path` cross-class reach (#711).
-static func _find_system_install() -> String:
- ## Built with append, not a ternary of untyped literals — assigning a
- ## ternary's Array to Array[String] is a runtime error on newer Godot
- ## builds (same idiom as _uvx_cli_names above).
- var names: Array[String] = ["godot-ai"]
- if OS.get_name() == "Windows":
- names.push_front("godot-ai.exe")
- return CliFinder.find(names)
diff --git a/addons/godot_ai/client_configurator.gd.uid b/addons/godot_ai/client_configurator.gd.uid
deleted file mode 100644
index 9182096..0000000
--- a/addons/godot_ai/client_configurator.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://1kiy8hqyymyj
diff --git a/addons/godot_ai/clients/_atomic_write.gd b/addons/godot_ai/clients/_atomic_write.gd
deleted file mode 100644
index 3dbf72c..0000000
--- a/addons/godot_ai/clients/_atomic_write.gd
+++ /dev/null
@@ -1,251 +0,0 @@
-@tool
-class_name McpAtomicWrite
-extends RefCounted
-
-## Write text to a file via temp + rename so a crash mid-write never leaves
-## the user's MCP config truncated. Creates the parent dir if needed and
-## keeps a one-shot `.backup` of the prior file.
-##
-## On filesystems where rename-over-existing fails (Windows under AV / lock
-## pressure, some SMB shares), falls back to overwrite-copy plus a
-## backup-restore on failure. The original file is never removed before the
-## new bytes are verified on disk — if both the rename and the copy fail,
-## the user's prior config is restored from the `.backup` snapshot. See
-## issue #297 finding #10 for the data-loss scenario this guards against.
-
-
-static func write(path: String, content: String) -> bool:
- # Last line of defence for the path-resolution layer above. A relative (or
- # empty) destination resolves against the EDITOR's working directory, not the
- # caller's. The empty-path case is especially deceptive: editor safe-save
- # runs mkstemp on "-XXXXXX", writes the payload into the project root, then
- # fails only when renaming back to "" and leaves the random file behind.
- # Callers gate this already (`McpClient.resolved_config_path_details`), but a
- # writer that cannot tell where it is writing must refuse rather than guess.
- if not path.is_absolute_path():
- return false
- # If the target is a symlink (stow/chezmoi-managed dotfiles), rename-over
- # would replace the LINK with a regular file, silently detaching the
- # config from the user's dotfile repo (#534). Resolve the link chain and
- # write to the real target so the symlink survives.
- path = _resolve_symlink_target(path)
- var dir_path := path.get_base_dir()
- if not DirAccess.dir_exists_absolute(dir_path):
- if DirAccess.make_dir_recursive_absolute(dir_path) != OK:
- return false
-
- # Decide the permission mode the final file (and its backup) must carry
- # BEFORE we replace anything. A rewrite must preserve the prior file's
- # mode: the Claude CLI creates ~/.claude.json as 0600 (it holds OAuth
- # creds + history), and a naive FileAccess write + DirAccess copy would
- # silently relax that to the umask default (0644) and leak it on shared
- # machines. A brand-new config defaults to owner-only 0600 since these
- # files routinely carry tokens. On platforms without POSIX permissions
- # (Windows) the get/set calls no-op and this logic is inert. See #297
- # finding TC-1.
- var had_original := FileAccess.file_exists(path)
- var target_mode := _resolve_target_mode(path, had_original)
-
- # Suffix the temp name with this process's PID so two editors writing the
- # same config concurrently (both clicking Configure) can't interleave
- # bytes on a shared fixed ".tmp" path (#534). Each process stages its own
- # temp file; the final rename remains the atomic commit point.
- var tmp_path := "%s.tmp.%d" % [path, OS.get_process_id()]
- var file := _open_restricted_temp(tmp_path, target_mode)
- if file == null:
- return false
- file.store_string(content)
- # Push Godot's internal buffer out to the OS before the rename. Godot
- # exposes no fsync, so the bytes aren't guaranteed durable on the physical
- # disk until the OS flushes its own cache — a power loss in that window can
- # still lose the data. But flush() ensures the rename can't be ordered ahead
- # of the write at the application layer, which is the failure this guards.
- file.flush()
- file.close()
- # Re-assert the mode on the closed inode before rename. The temp was already
- # restricted before this handle opened, so this is defence in depth rather
- # than the first point at which secrets become owner-only.
- if not _apply_mode(tmp_path, target_mode):
- DirAccess.remove_absolute(tmp_path)
- return false
-
- # Verify the staged temp landed intact before committing it anywhere. The
- # copy-fallback path below already guards this (`_written_size_matches` at
- # the rename-fallback check); the rename path was the one gap — under
- # disk-full/quota the temp can be silently truncated, and an unverified
- # rename would swap a truncated file over the live target while the
- # caller is told the write succeeded (#687).
- if not _written_size_matches(tmp_path, content):
- DirAccess.remove_absolute(tmp_path)
- return false
-
- # Best-effort: snapshot the prior file before we touch the target so we
- # can restore on a failed swap. The backup is also kept on success as a
- # one-shot rollback aid for the user — give it the same (preserved) mode
- # so a 0600 config's backup isn't itself a world-readable copy.
- #
- # copy_absolute creates the backup at the umask default and we can only
- # chmod it afterward, so there's a sub-millisecond window where the backup
- # carries default perms. Accepted: it duplicates bytes already sitting at
- # `path` (which the caller created 0600) inside the user's own config dir,
- # and Godot exposes no API to create the copy pre-chmod'd. Not worth
- # reimplementing copy by hand to shave that window.
- var backup_path := path + ".backup"
- var backup_made := false
- if had_original:
- DirAccess.remove_absolute(backup_path)
- if DirAccess.copy_absolute(path, backup_path) == OK:
- if not _apply_mode(backup_path, target_mode):
- ## The backup contains the same secrets as the live config. Do
- ## not leave that copy behind — or touch the destination — when
- ## it cannot be restricted to the intended mode.
- DirAccess.remove_absolute(backup_path)
- DirAccess.remove_absolute(tmp_path)
- return false
- backup_made = true
- else:
- ## copy_absolute may leave a partial destination on failure. It
- ## contains config bytes under the process umask, so remove it.
- DirAccess.remove_absolute(backup_path)
-
- if DirAccess.rename_absolute(tmp_path, path) == OK:
- return true
-
- # Rename-over-existing rejected (Windows + AV / lock timing, some SMB
- # shares). Use overwrite-copy as the recovery path: copy_absolute never
- # removes the original before writing the new bytes, so a failure here
- # leaves the user's prior config in place rather than nuking it.
- if DirAccess.copy_absolute(tmp_path, path) == OK and _written_size_matches(path, content):
- # copy_absolute creates the destination with the default mode, so
- # re-apply the preserved/owner-only mode after the copy lands. A
- # permission failure is a failed write: fall through to the same
- # restore/remove path as a partial copy instead of reporting success
- # with a potentially exposed token-bearing config.
- if _apply_mode(path, target_mode):
- DirAccess.remove_absolute(tmp_path)
- return true
-
- # Copy didn't land cleanly. Restore the destination to its pre-call state.
- if backup_made:
- # Restore the snapshot we took before the swap. `copy_absolute`
- # overwrites the destination, so we don't pre-remove `path` — the
- # pre-remove created a window where `path` was gone if the
- # subsequent copy itself failed. If the restore copy fails now the
- # user's prior bytes are still in `.backup` for manual recovery
- # and the false return value tells the caller the swap didn't
- # complete.
- var restored := (
- DirAccess.copy_absolute(backup_path, path) == OK
- and _apply_mode(path, target_mode)
- )
- if not restored:
- ## The backup's mode was already verified before the swap. Remove
- ## any partial/default-mode destination, then move that restricted
- ## inode back into place. If even the same-directory rename fails,
- ## the complete prior contents remain in `.backup` for recovery.
- DirAccess.remove_absolute(path)
- DirAccess.rename_absolute(backup_path, path)
- elif not had_original and FileAccess.file_exists(path):
- # No prior file existed but copy_absolute landed partial bytes at
- # `path`. Remove them so the failure leaves nothing on disk rather
- # than a truncated/invalid new file. The `file_exists` guard keeps
- # us off non-file destinations (a path that points at a directory
- # yields `had_original=false` too, but we must not try to delete
- # the directory). Issue #297 PR review.
- DirAccess.remove_absolute(path)
- # (If `had_original` is true but the snapshot couldn't be taken, the
- # original on disk is whatever copy_absolute managed to write before
- # failing. This is a best-effort path — the false return value tells the
- # caller the swap didn't complete; recovery beyond that requires a
- # backup we couldn't take.)
- DirAccess.remove_absolute(tmp_path)
- return false
-
-
-## Follow a symlink chain at `path` and return the final real target, so the
-## temp+rename lands on the linked-to file instead of replacing the link.
-##
-## Best-effort by design: DirAccess.is_link()/read_link() are only implemented
-## on platforms with POSIX symlinks (Linux/macOS; on Windows and other
-## platforms is_link() returns false), and opening the parent dir can fail for
-## exotic paths. In every "can't tell" case we return `path` unchanged, which
-## is exactly the pre-#534 behavior — never worse, symlink-preserving where
-## the engine lets us detect one.
-static func _resolve_symlink_target(path: String) -> String:
- var resolved := path
- # Bounded hops so a symlink cycle can't loop us forever.
- for _hop in 8:
- var base_dir := resolved.get_base_dir()
- var da := DirAccess.open(base_dir)
- if da == null or not da.is_link(resolved):
- return resolved
- var target := da.read_link(resolved)
- if target.is_empty():
- return resolved
- if target.is_relative_path():
- target = base_dir.path_join(target)
- resolved = target.simplify_path()
- return resolved
-
-
-static func _resolve_target_mode(path: String, had_original: bool) -> int:
- # Preserve the prior file's POSIX mode on a rewrite; default a brand-new
- # config (or any case we can't read a mode for) to owner read+write (0600).
- #
- # get_unix_permissions returns 0 both on Windows (no POSIX perms) and for a
- # genuine 0000 file. Treating 0 as "use the 0600 floor" is deliberate, not a
- # missed case: these are config files the plugin must read and write, 0000 is
- # unusable, and re-applying 0000 would lock the owner out next run. 0600 is
- # still owner-only so this never widens access. (A genuinely-0000 file can't
- # reach a rewrite through the config strategies anyway — their read-first
- # guard fails to open it and refuses the write before we get here.)
- if had_original:
- var existing := FileAccess.get_unix_permissions(path)
- if existing > 0:
- return existing
- return FileAccess.UNIX_READ_OWNER | FileAccess.UNIX_WRITE_OWNER
-
-
-## Create/truncate an EMPTY temp file, close it, restrict its mode, then reopen
-## it without truncation for the payload write. Godot 4.6 on macOS returns
-## FAILED when set_unix_permissions targets a path whose FileAccess is still
-## open; the old create -> chmod-while-open -> write sequence therefore emitted
-## one warning per configured client and briefly wrote secrets under the umask
-## mode. Closing the empty inode first makes the security ordering real and
-## removes that platform-specific warning without suppressing genuine errors.
-static func _open_restricted_temp(path: String, mode: int) -> FileAccess:
- var created := FileAccess.open(path, FileAccess.WRITE)
- if created == null:
- return null
- created.close()
- if not _apply_mode(path, mode):
- DirAccess.remove_absolute(path)
- return null
- return FileAccess.open(path, FileAccess.READ_WRITE)
-
-
-static func _apply_mode(path: String, mode: int) -> bool:
- # Best-effort. set_unix_permissions returns ERR_UNAVAILABLE on platforms
- # without POSIX permissions (Windows); that's expected and ignored so the
- # write still works there. mode <= 0 should never happen (resolve always
- # returns >0) but is guarded so a future caller can't chmod a file to nothing.
- if mode <= 0:
- return true
- var err := FileAccess.set_unix_permissions(path, mode)
- if err == OK or err == ERR_UNAVAILABLE:
- return true
- # Surface a real chmod failure (not the Windows no-op) so permission
- # hardening on a sensitive config doesn't fail completely silently.
- push_warning("MCP | could not set permissions on %s: %s" % [path, error_string(err)])
- return false
-
-
-static func _written_size_matches(path: String, content: String) -> bool:
- # `store_string` writes UTF-8 bytes with no BOM and no newline translation,
- # so the byte length on disk must match `to_utf8_buffer().size()` exactly.
- var f := FileAccess.open(path, FileAccess.READ)
- if f == null:
- return false
- var on_disk := f.get_length()
- f.close()
- return on_disk == content.to_utf8_buffer().size()
diff --git a/addons/godot_ai/clients/_atomic_write.gd.uid b/addons/godot_ai/clients/_atomic_write.gd.uid
deleted file mode 100644
index add9f7f..0000000
--- a/addons/godot_ai/clients/_atomic_write.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://6fkb5uau0r4h
diff --git a/addons/godot_ai/clients/_base.gd b/addons/godot_ai/clients/_base.gd
deleted file mode 100644
index 0483496..0000000
--- a/addons/godot_ai/clients/_base.gd
+++ /dev/null
@@ -1,578 +0,0 @@
-@tool
-class_name McpClient
-extends RefCounted
-
-## Descriptor for one MCP client (Cursor, Claude Desktop, Codex, ...).
-##
-## Subclasses set fields in `_init()` and MUST NOT carry Callables — strategies
-## (json/toml/cli) interpret the data. Enforced by
-## `test_clients.gd::test_descriptors_are_data_only`.
-##
-## Why no Callables: per-client `.gd` files get hot-reloaded on disk-mtime
-## change. A worker thread mid-call into a descriptor lambda races the
-## bytecode swap and SEGVs (issue #229). Bonus: also obsoletes the stale-
-## Callable workaround from #192.
-
-## CONFIGURED_MISMATCH = an entry with our `SERVER_NAME` exists in the user's
-## client config, but its URL or launch command doesn't match the current
-## ports/version/exclusions — typical after a setting change or update.
-## Distinguishing this from `NOT_CONFIGURED` lets the dock surface a "your
-## saved client configuration is stale" banner instead of conflating it with
-## "you never configured this client".
-enum Status { NOT_CONFIGURED, CONFIGURED, CONFIGURED_MISMATCH, ERROR }
-
-
-## Lowercase string label for a `Status` value. Single source of truth so the
-## MCP `client_status` tool, the dock, and the verify-after-write diagnostic
-## in `McpClientConfigurator` all emit the same names — agents pattern-match
-## against this set, so a fifth value being silently introduced would break
-## them.
-static func status_label(status: McpClient.Status) -> String:
- match status:
- Status.CONFIGURED:
- return "configured"
- Status.NOT_CONFIGURED:
- return "not_configured"
- Status.CONFIGURED_MISMATCH:
- return "configured_mismatch"
- return "error"
-
-
-## One-line configure success message, shared by every strategy so the dock
-## and the `client_manage` tool describe the transport that was actually
-## written. Command-shape clients register the stdio `godot-ai attach`
-## bridge — the URL-era "(HTTP: )" suffix would name a transport the
-## write never touched (found live in the #838 Windows smoke).
-static func configured_message(client: McpClient, server_url: String) -> String:
- if client.command_shape != CommandShape.NONE:
- return "%s configured (stdio attach)" % client.display_name
- return "%s configured (HTTP: %s)" % [client.display_name, server_url]
-
-var id: String = "" ## stable key, e.g. "cursor"
-var display_name: String = "" ## "Cursor"
-var config_type: String = "" ## "json" | "toml" | "yaml" | "cli" | "dsh"
-## False when the client's settings format cannot be round-tripped safely by
-## the matching strategy. Status checks remain read-only; Configure and Remove
-## return the existing manual instructions without touching the file.
-var automatic_config_edits: bool = true
-
-## True when the config file is JSONC (Zed ships a `//` header). Honored by the
-## JSON strategy's reads only — comments are stripped from a throwaway parse
-## copy, so the row shows a real status instead of a permanent parse error
-## (#914). Requires `automatic_config_edits = false`; the strategy ignores it
-## otherwise rather than let a re-serializing Configure drop the comments.
-var config_allows_comments: bool = false
-
-# JSON / TOML clients ------------------------------------------------------
-## {"darwin": "~/...", "windows": "$APPDATA/...", "linux": "$XDG_CONFIG_HOME/..."}
-## Keys may also use "unix" as a shorthand for darwin+linux.
-var path_template: Dictionary = {}
-
-## Optional ordered path candidates by platform. Each value is an Array of
-## templates; one `*` may appear in a directory segment so packaged-app roots
-## can be discovered without hardcoding publisher hashes.
-##
-## Resolution contract:
-## 1. Existing files win in descriptor order, except that a unique wildcard
-## match is authoritative even before its config leaf exists. A matching
-## package root therefore creates inside that package rather than writing
-## a fallback path that may become invisible after copy-on-write. When
-## that private leaf is new, Configure seeds it from the first later
-## existing candidate so read-through content is not shadowed.
-## 2. If no file or wildcard package match exists, the first non-wildcard
-## template is the deterministic create target.
-## 3. Multiple matches within any wildcard group are ambiguous and fail
-## closed instead of choosing an arbitrary package.
-##
-## Exact-file and config-home environment overrides still have higher
-## priority. When this map has no entry for the current platform,
-## `path_template` remains the fallback.
-var config_path_candidates: Dictionary = {}
-
-## Optional global JSON config files merged by the client from lowest to highest
-## precedence. Unlike `config_path_candidates`, every existing file participates;
-## Configure updates the effective last definition, status verifies it, and
-## Remove clears every global definition transactionally.
-var config_merge_path_templates: Dictionary = {}
-## Project-relative tiers that may override the global files. Their root is the
-## external client's working directory, which the Godot process cannot know.
-## The strategy checks plausible roots and fails closed instead of mutating them.
-var config_merge_project_paths: PackedStringArray = PackedStringArray()
-
-## De-duplicate persistent path-ambiguity warnings across recurring status
-## refreshes. The actionable message still returns on every resolution; only
-## the editor-console echo is single-shot until the ambiguity clears/changes.
-var _last_config_path_warning := ""
-var _config_path_warning_mutex := Mutex.new()
-
-## Path inside the config object where the per-server map lives.
-## Cursor / Claude Desktop / most others: ["mcpServers"]
-## VS Code: ["servers"]
-## OpenCode: ["mcp"]
-var server_key_path: PackedStringArray = PackedStringArray()
-## Alternative server-map paths accepted by the client, in precedence order
-## after `server_key_path`. JSON strategy operations select the first non-null
-## existing path so Configure, status, and Remove agree with the client parser.
-var server_key_path_aliases: Array[PackedStringArray] = []
-
-## Field inside the entry dict that holds our server URL.
-## "url" by default; some clients use "serverUrl" or "httpUrl".
-var entry_url_field: String = "url"
-
-## Required entry fields — written on every Configure AND verified by the
-## default verifier. Use this for transport pins (e.g. `type:
-## "streamable-http"`) where a missing/wrong value breaks negotiation: a
-## legacy entry without the pin fails verification and surfaces as drift.
-##
-## DO NOT put user-mutable state here (auto-approval lists, `disabled`
-## flags, opt-in toggles). Verifying those treats every user customisation
-## as drift, and Configure-All-Mismatched then silently overwrites them
-## back to defaults — see the `entry_initial_fields` doc below.
-var entry_extra_fields: Dictionary = {}
-
-## Default fields written ONLY when the entry doesn't yet exist. Reconfigure
-## preserves whatever the user (or the client itself) has set; the verifier
-## ignores these keys entirely. Use for opt-in flags and user-state arrays —
-## e.g. Roo / Cline / Kilo `alwaysAllow` / `autoApprove` lists, `disabled:
-## false`, `isActive: true`. The pre-#229 behaviour was equivalent: per-
-## client `entry_builder` lambdas seeded these as defaults but the
-## per-client `verify_entry` lambdas only checked transport pins, so a
-## user-customised array was `CONFIGURED`, not drift. Splitting the field
-## restores that contract under the data-only descriptor model.
-var entry_initial_fields: Dictionary = {}
-
-## Client-owned stdio launch shape. Each strategy renders the shape in its
-## config language:
-##
-## - FLAT — `command` string + `args` array as sibling keys. JSON and YAML
-## strategies. A client whose docs require a type discriminator next to the
-## flat keys (VS Code's `type: "stdio"`, Claude Code's fallback file) stays
-## FLAT and declares it via `command_transport_key` / `command_transport_value`,
-## so TYPED_FLAT remains reserved vocabulary.
-## - COMMAND_ARRAY — the launch argv carried as one array. In the JSON
-## strategy the entry's `command` field IS that array (OpenCode's
-## `"command": ["uvx", …]`). In the TOML strategy the launcher renders as a
-## `command = "…"` line plus an `args = […]` array (Codex, Grok) — the name
-## refers to the argv-as-TOML-array body it emits.
-## - NESTED_COMMAND — command/args nested inside a sub-object. Reserved; no
-## current client needs it and strategies reject it with an actionable error.
-##
-## CLI-registered clients (`config_type == "cli"`) express the launch through
-## `cli_register_template` tokens instead; their `command_shape` governs the
-## JSON-fallback file rendering (Claude Code, #463).
-##
-## Values are data-only shared vocabulary; keeping them data-only avoids
-## reintroducing the descriptor Callable race from #229.
-enum CommandShape { NONE, FLAT, TYPED_FLAT, COMMAND_ARRAY, NESTED_COMMAND }
-var command_shape: CommandShape = CommandShape.NONE
-
-## Optional discriminator required by a client's command transport shape
-## (for example `type = "stdio"`). Empty means command+args are sufficient.
-var command_transport_key: String = ""
-var command_transport_value: Variant = null
-
-## Whether this client's Windows stdio entry must launch through the
-## GUI-subsystem pythonw bootstrap (#827). The bootstrap exists for clients
-## that run console-subsystem MCP commands in a visible terminal (Codex);
-## Electron-family spawners hide child consoles themselves, and at least one
-## (Antigravity) hangs tool calls when handed a GUI-subsystem executable
-## (#863). Set false to write the plain console launcher on Windows.
-var needs_consoleless_launcher: bool = true
-
-## Keys from the legacy transport that Configure must delete. Codex removes
-## `url`, because Codex rejects a server entry containing both URL and stdio
-## launch fields.
-var command_legacy_keys: PackedStringArray = PackedStringArray()
-
-## Keys inside a preserved JSON `env` object that belonged to a legacy launch
-## shape and must be removed during migration. Other environment values remain
-## user-owned and survive Configure. Currently consumed by the JSON strategy.
-var command_env_legacy_keys: PackedStringArray = PackedStringArray()
-
-## Defaults seeded only for a new entry. Reconfigure preserves user values.
-## Codex uses this for enabled/startup/tool timeout defaults.
-var command_initial_fields: Dictionary = {}
-
-## Declarative documentation of fields owned by the user and timeout fields
-## supported by this client. Strategies preserve these values and tests pin
-## the descriptor contract; no control flow lives on the descriptor.
-var command_user_fields: PackedStringArray = PackedStringArray()
-var command_timeout_fields: PackedStringArray = PackedStringArray()
-
-## Paths whose existence implies the user has this client installed.
-## Used purely for the dock's "installed" badge. `is_installed()` additionally
-## checks `resolved_config_path()`, so a config relocated via an environment
-## override is detected without listing it here.
-var detect_paths: PackedStringArray = PackedStringArray()
-
-# Config-path env overrides --------------------------------------------------
-## Some clients name the exact config file in an environment variable
-## (OpenCode: `$OPENCODE_CONFIG`). When the variable is set and non-empty, it
-## wins over directory-valued `config_home_env` and `path_template`. Relative
-## values fail closed because the editor and client may have different working
-## directories; auto-configuration cannot safely assume they resolve alike.
-var config_file_env: String = ""
-
-## Some clients honor an env var that relocates their entire config home
-## (Codex: `$CODEX_HOME/config.toml`; Claude Code: `$CLAUDE_CONFIG_DIR/.claude.json`).
-## When `config_home_env` names an env var that is set and non-empty,
-## `resolved_config_path()` returns `/`
-## instead of resolving `path_template`. Both fields must be non-empty for the
-## override to apply. Only declare a mapping when the client's docs guarantee
-## the env var relocates the exact file we write — a wrong mapping writes the
-## MCP entry somewhere the client never reads and Configure false-succeeds.
-## Relative values fail closed for the same reason `config_file_env`'s do.
-var config_home_env: String = ""
-## Path of the config file relative to the env var's directory, e.g.
-## "config.toml". Joined verbatim — no per-OS variants needed because the env
-## value itself is already an absolute (or ~-prefixed) directory.
-var config_home_env_subpath: String = ""
-
-# CLI clients --------------------------------------------------------------
-var cli_names: PackedStringArray = PackedStringArray()
-## Argument templates with `{name}` and `{url}` tokens; the strategy
-## substitutes them at call time. Tokens are matched verbatim — no escaping
-## semantics, no shell expansion. Command-shape templates additionally use the
-## whole-element tokens `{command}` / `{args...}` (see `McpCliStrategy.format_args`).
-## Populated by CLI descriptors (currently `claude_code`; `kimi_code` moved to
-## mcp.json in #813).
-var cli_register_template: PackedStringArray = PackedStringArray()
-var cli_unregister_template: PackedStringArray = PackedStringArray()
-## Args run to read current state; stdout is scanned for the server name and
-## URL. Presence of `name` AND `url` → CONFIGURED, name only → MISMATCH,
-## neither → NOT_CONFIGURED.
-var cli_status_args: PackedStringArray = PackedStringArray()
-## Optional scope-aware status template with a `{name}` token, for descriptors
-## that also take `{scope}` in their register template. `cli_status_args` lists
-## whatever the CLI resolves for the current directory and cannot say WHICH
-## scope an entry came from, so a leftover user-scope entry reads as CONFIGURED
-## while the selected project scope is empty — the dock then shows green over
-## exactly the "loaded in every workspace" state the scope setting exists to
-## end (#872). This template must print a `Scope: …` line
-## (`claude mcp get ` does) and exit non-zero when the server is absent.
-## Only consulted when the selected scope isn't the one `path_template` reads.
-var cli_scope_status_template: PackedStringArray = PackedStringArray()
-
-# Codex / TOML clients -----------------------------------------------------
-## Dotted TOML path under which our entry lives, e.g. ["mcp_servers", "godot-ai"].
-## Strategies build the [section."name"] header from this.
-var toml_section_path: PackedStringArray = PackedStringArray()
-var toml_legacy_section_aliases: PackedStringArray = PackedStringArray()
-## Lines (without the [header]) emitted under the section, with `{url}`
-## tokens. Substituted at call time.
-var toml_body_template: PackedStringArray = PackedStringArray()
-
-
-## Resolved absolute config path for this client on the current OS. Exact-file
-## overrides win first, followed by directory-valued `config_home_env`, then
-## ordered candidates / `path_template`. Ignoring either override can write a
-## file the client never reads and false-succeed.
-func resolved_config_path() -> String:
- return str(resolved_config_path_details().get("path", ""))
-
-
-## Detailed sibling used by status/configure/remove so safe resolution
-## failures reach the dock instead of collapsing into NOT_CONFIGURED. `error`
-## is empty for ordinary unsupported/missing path mappings to preserve the
-## long-standing status behavior for clients not installed on this platform.
-func resolved_config_path_details() -> Dictionary:
- var details := _resolve_config_path_details()
- var path := str(details.get("path", ""))
- if path.is_empty() or path.is_absolute_path():
- return details
- ## Last gate before every strategy's read/write. `McpPathTemplate.expand`
- ## leaves a token it cannot resolve in place, so an unset `$USERPROFILE` (or
- ## a `~` with no HOME) reaches here as a RELATIVE path rather than as the
- ## root-relative `/godot` that sails through `is_absolute_path()`. Acting on
- ## it would resolve against the editor's own working directory and create a
- ## literal `$USERPROFILE` folder in the Godot project. No descriptor
- ## template is intentionally relative — every one is `~`- or `$VAR`-rooted —
- ## so a non-absolute survivor is always a resolution failure. Report it with
- ## the unexpanded path, which still shows what could not be resolved.
- return {"path": "", "error": unresolved_config_path_error(display_name, path)}
-
-
-## Shared wording for a path template `McpPathTemplate.expand` could not fully
-## resolve. Used by the guard above and by the merge-tier loader in
-## `_json_strategy.gd`, which resolves its own templates and never passes
-## through `resolved_config_path_details`.
-static func unresolved_config_path_error(client_name: String, path: String) -> String:
- return (
- "Could not resolve %s's config path: %s did not expand to an absolute "
- + "path. Set HOME/USERPROFILE (or the variable the path names) in the "
- + "environment the editor was launched from."
- ) % [client_name, path]
-
-
-func _resolve_config_path_details() -> Dictionary:
- ## Reflected reads: after an in-session self-update, an instance created
- ## before the update can answer Nil for vars the update added, and the
- ## typed calls below would each hard-error (Nil -> Dictionary, #850's
- ## per-row error wall). Fail with the
- ## one repair message instead; the registry's coherence probe drives the
- ## same text on the status path.
- var candidates: Variant = get("config_path_candidates")
- var template: Variant = get("path_template")
- var file_env: Variant = get("config_file_env")
- if not (candidates is Dictionary) or not (template is Dictionary) or not (file_env is String):
- return {"path": "", "error": McpClientRegistry.RESTART_TO_FINISH_UPDATE}
- var file_override := config_file_override_details()
- if not str(file_override.get("path", "")).is_empty() or not str(file_override.get("error", "")).is_empty():
- _clear_config_path_warning()
- return file_override
- var home_override := config_home_override_details()
- if not str(home_override.get("path", "")).is_empty() or not str(home_override.get("error", "")).is_empty():
- _clear_config_path_warning()
- return home_override
- var candidate_key := McpPathTemplate.platform_key(candidates)
- if not candidate_key.is_empty():
- return _resolve_ordered_config_path_candidates(candidates[candidate_key])
- _clear_config_path_warning()
- return {"path": McpPathTemplate.resolve(template), "error": ""}
-
-
-## The exact-file env override plus any fail-closed diagnostic. Empty path and
-## error means no override applies (no mapping, unset, or blank env var).
-func config_file_override_details() -> Dictionary:
- if config_file_env.is_empty():
- return {"path": "", "error": ""}
- ## env_lookup, not OS.get_environment: this can run on dock workers (#691).
- var raw_path := McpPathTemplate.env_lookup(config_file_env).strip_edges()
- if raw_path.is_empty():
- return {"path": "", "error": ""}
- var expanded := McpPathTemplate.expand(raw_path)
- if not expanded.is_absolute_path():
- return {
- "path": "",
- "error": "%s's $%s override must be an absolute config-file path; got %s" % [
- display_name, config_file_env, raw_path,
- ],
- }
- if DirAccess.dir_exists_absolute(expanded):
- return {
- "path": "",
- "error": "%s's $%s override must point to a config file, not a directory: %s" % [
- display_name, config_file_env, expanded,
- ],
- }
- return {"path": expanded, "error": ""}
-
-
-func _resolve_ordered_config_path_candidates(templates: Variant) -> Dictionary:
- if not (templates is Array or templates is PackedStringArray):
- _clear_config_path_warning()
- return {"path": "", "error": ""}
- var ordered_templates: Array = []
- for template_variant in templates:
- ordered_templates.append(str(template_variant))
- var fallback_create_path := ""
- for index in range(ordered_templates.size()):
- var template := str(ordered_templates[index])
- var expanded_template := McpPathTemplate.expand(template)
- # An empty wildcard group ordinarily means "this package is not
- # installed", but an unresolved root token produces the same empty
- # group. Do not silently treat "could not inspect" as "not present" and
- # fall through to a later writable candidate.
- if not expanded_template.is_absolute_path():
- _clear_config_path_warning()
- return {
- "path": "",
- "error": unresolved_config_path_error(display_name, expanded_template),
- }
- var group := McpPathTemplate.expand_path_candidates(template)
- if group.size() > 1:
- var message := (
- "%s has multiple matching config package paths for %s: %s. "
- + "Remove the stale package installation or edit the intended config manually."
- ) % [display_name, template, ", ".join(group)]
- _warn_config_path_once(message)
- return {"path": "", "error": message}
- if group.is_empty():
- continue
- var path := String(group[0])
- if FileAccess.file_exists(path):
- _clear_config_path_warning()
- return {"path": path, "error": ""}
- # A wildcard only resolves when its package directory exists. Treat that
- # installation evidence as authoritative and create its private config
- # directly instead of relying on copy-on-write read-through. Preserve
- # anything currently visible through read-through by naming the first
- # later existing candidate as a one-time seed source.
- if template.contains("*"):
- var seed := _first_existing_later_candidate(ordered_templates, index + 1)
- if not str(seed.get("error", "")).is_empty():
- _clear_config_path_warning()
- return {"path": "", "error": seed["error"]}
- _clear_config_path_warning()
- return {"path": path, "error": "", "seed_path": seed.get("path", "")}
- if fallback_create_path.is_empty():
- fallback_create_path = path
- _clear_config_path_warning()
- return {"path": fallback_create_path, "error": ""}
-
-
-## Find a readable seed for a newly-created authoritative wildcard target.
-## An unresolved later root is an error, not an absent seed: writing without
-## inspecting it could silently discard configuration visible through the
-## package's read-through fallback.
-func _first_existing_later_candidate(templates: Array, start_index: int) -> Dictionary:
- for index in range(start_index, templates.size()):
- var template := str(templates[index])
- var expanded_template := McpPathTemplate.expand(template)
- if not expanded_template.is_absolute_path():
- return {
- "path": "",
- "error": unresolved_config_path_error(display_name, expanded_template),
- }
- var group := McpPathTemplate.expand_path_candidates(template)
- # A seed is optional. Never choose among an ambiguous later wildcard;
- # the authoritative target was already resolved by the earlier group.
- if group.size() != 1:
- continue
- var path := String(group[0])
- if FileAccess.file_exists(path):
- return {"path": path, "error": ""}
- return {"path": "", "error": ""}
-
-
-func _warn_config_path_once(message: String) -> void:
- _config_path_warning_mutex.lock()
- var should_warn := message != _last_config_path_warning
- _last_config_path_warning = message
- _config_path_warning_mutex.unlock()
- if should_warn:
- push_warning(message)
-
-
-func _clear_config_path_warning() -> void:
- _config_path_warning_mutex.lock()
- _last_config_path_warning = ""
- _config_path_warning_mutex.unlock()
-
-
-## The config-home env override plus any fail-closed diagnostic. Empty path and
-## error means no override applies (no mapping declared, env var unset, or env
-## var empty/whitespace).
-func config_home_override_details() -> Dictionary:
- if config_home_env.is_empty() or config_home_env_subpath.is_empty():
- return {"path": "", "error": ""}
- ## env_lookup, not OS.get_environment: this runs on dock worker threads,
- ## which must not race the spawn window's setenv/unsetenv (#691).
- var home := McpPathTemplate.env_lookup(config_home_env).strip_edges()
- if home.is_empty():
- return {"path": "", "error": ""}
- # Expand a leading ~ so `CODEX_HOME=~/codex-alt` behaves like the shell.
- var expanded := McpPathTemplate.expand(home)
- ## Same fail-closed rule as `config_file_override_details`: a relative value
- ## resolves against the EDITOR's working directory, not the client's, so
- ## honouring it silently aims the write at the Godot project directory
- ## instead of the file the client reads. The write layer can only name the
- ## mangled path it was handed; only here do we still know which env var
- ## produced it, so this is where the user-facing explanation belongs.
- if not expanded.is_absolute_path():
- return {
- "path": "",
- "error": "%s's $%s override must be an absolute config-home directory path; got %s" % [
- display_name, config_home_env, home,
- ],
- }
- return {"path": expanded.path_join(config_home_env_subpath), "error": ""}
-
-
-## The env-var-relocated config path, or "" when no override applies (no
-## mapping declared, env var unset, env var empty/whitespace, or a value that
-## failed closed — `config_home_override_details` carries that diagnostic).
-func config_home_override() -> String:
- return str(config_home_override_details().get("path", ""))
-
-
-## True when a CLI client also declares where its config file lives, so it can
-## fall back to writing that file directly when the CLI binary isn't on PATH.
-## #463: Claude Code installed only as a VS Code / Cursor extension exposes no
-## `claude` binary, but `claude mcp add --scope user` just writes `mcpServers`
-## into ~/.claude.json — so we can produce the same entry ourselves.
-func has_json_fallback() -> bool:
- return config_type == "cli" and not path_template.is_empty() and not server_key_path.is_empty()
-
-
-## True if the user appears to have this client installed locally.
-func is_installed() -> bool:
- if config_type == "cli":
- if not McpCliFinder.find(_array_from_packed(cli_names)).is_empty():
- return true
- # CLI not on PATH. A cli client with a JSON fallback (Claude Code as a
- # VS Code/Cursor extension, #463) still counts as installed if its
- # fallback config file already exists.
- if has_json_fallback():
- var cfg := resolved_config_path()
- return not cfg.is_empty() and FileAccess.file_exists(cfg)
- return false
- for p in detect_paths:
- for resolved in McpPathTemplate.expand_path_candidates(p):
- if FileAccess.file_exists(resolved) or DirAccess.dir_exists_absolute(resolved):
- return true
- # Fall back to "config file already exists" — usually means installed at some point.
- var cfg := resolved_config_path()
- return not cfg.is_empty() and FileAccess.file_exists(cfg)
-
-
-static func _array_from_packed(packed: PackedStringArray) -> Array[String]:
- var out: Array[String] = []
- for s in packed:
- out.append(s)
- return out
-
-
-## Slice a PackedStringArray into a new PackedStringArray over [from, to).
-## Used by `_toml_strategy` and `_manual_command` to peel the section path
-## apart for `[a.b."c"]` header rendering.
-static func _packed_slice(packed: PackedStringArray, from: int, to: int) -> PackedStringArray:
- var out := PackedStringArray()
- for i in range(from, to):
- out.append(packed[i])
- return out
-
-
-## Whether an existing entry's launch text launches Godot AI: an executable
-## named `godot-ai`, a `godot-ai==` package pin, the bare `godot-ai`
-## console-script argument, or the `godot_ai` module. Tokens are matched
-## exactly, so a URL or path that merely contains the name (a docs link, a
-## project directory) does not count. The post-update major migration
-## rewrites only such entries; anything else is the user's own server.
-static func launch_mentions_godot_ai(text: String) -> bool:
- ## Free text (a CLI probe's output) is split on spaces; structured launch
- ## values should go through `launch_values_mention_godot_ai` unsplit.
- return launch_values_mention_godot_ai(PackedStringArray(text.split(" ", false)))
-
-
-## Each value is one command, argument or URL. A URI never names an
-## executable, and Windows separators are normalized before the basename
-## check so `C:\Program Files\Godot AI\godot-ai.exe` is recognized whole.
-static func launch_values_mention_godot_ai(values: PackedStringArray) -> bool:
- for raw_value in values:
- var value := raw_value.strip_edges().lstrip("\"'[{(").rstrip("\"'])},")
- if value.is_empty() or value.contains("://"):
- continue
- if value == "godot-ai" or value == "godot_ai" or value.begins_with("godot-ai=="):
- return true
- var base := value.replace("\\", "/").get_file()
- if base == "godot-ai" or base == "godot-ai.exe":
- return true
- return false
-
-
-## The launch-bearing fields of an entry, one value each, for the check
-## above. The entry sits under our server name, so the name itself must not
-## count.
-static func entry_launch_values(entry: Dictionary) -> PackedStringArray:
- var values := PackedStringArray()
- for key in ["command", "args", "url"]:
- if not entry.has(key):
- continue
- var value: Variant = entry[key]
- if value is Array:
- for item in value:
- values.append(str(item))
- else:
- values.append(str(value))
- return values
diff --git a/addons/godot_ai/clients/_base.gd.uid b/addons/godot_ai/clients/_base.gd.uid
deleted file mode 100644
index 5ea77d3..0000000
--- a/addons/godot_ai/clients/_base.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cyowqr1x12ilg
diff --git a/addons/godot_ai/clients/_cli_exec.gd b/addons/godot_ai/clients/_cli_exec.gd
deleted file mode 100644
index c645730..0000000
--- a/addons/godot_ai/clients/_cli_exec.gd
+++ /dev/null
@@ -1,219 +0,0 @@
-@tool
-class_name McpCliExec
-extends RefCounted
-
-## Wall-clock-bounded CLI invocation. Every dock shell-out to a per-client
-## CLI (`claude mcp list`, `claude mcp add ...`, etc.) goes through here so
-## a hung subprocess can't trap the calling thread forever.
-##
-## Without the timeout, a contended `claude mcp list` has been observed to
-## hang for 6+ minutes (issues #238, #239) — wedging the dock's status
-## refresh worker, and on the Configure / Remove paths the editor main
-## thread itself.
-##
-## Why poll/kill instead of `OS.execute(..., true)`: GDScript can't
-## interrupt a blocking `OS.execute`, so a hung CLI takes its caller's
-## thread with it. `OS.execute_with_pipe` returns immediately with a PID;
-## we drive the wait ourselves and terminate the exact captured process if
-## budget expires. CLI registry commands have bounded output (a few hundred
-## bytes), so we don't bother draining the pipe during the poll loop —
-## the kernel buffer absorbs it.
-##
-## Returns a Dictionary with:
-## exit_code: process exit code (0 = success). -1 on timeout / spawn failure.
-## stdout: captured stdout text. May be partial on timeout.
-## stderr: captured stderr text. May be partial on timeout. Empty when
-## `capture_stderr` is false.
-## output: stdout + (newline + stderr if non-empty). Convenience for
-## the common case of "show whatever the CLI said when it
-## failed" — `claude mcp add` writes its real diagnostics to
-## stderr, so callers that only read `stdout` would surface
-## a generic "exit code 1" instead.
-## timed_out: true if the wall-clock budget expired.
-## cancelled: true if the caller requested a cooperative early stop.
-## spawn_failed: true if `OS.execute_with_pipe` didn't return a usable PID.
-## termination_failed: true if an expired/cancelled process or any of its
-## possible descendants could not be proven stopped. POSIX
-## only gives this helper exact-PID authority, so timeout and
-## cancellation are necessarily unproven there.
-
-const DEFAULT_TIMEOUT_MS := 8000
-const _POLL_INTERVAL_MS := 50
-const _QUICK_EXIT_WINDOW_MS := 250
-const _KILL_GRACE_MS := 500
-const PortResolver := preload("res://addons/godot_ai/utils/port_resolver.gd")
-const UvResolution := preload("res://addons/godot_ai/utils/uv_resolution_policy.gd")
-
-
-static func run(
- exe: String,
- args: Array,
- timeout_ms: int = DEFAULT_TIMEOUT_MS,
- capture_stderr: bool = true,
- cancel_check: Callable = Callable(),
- isolate_uv_resolution: bool = false,
-) -> Dictionary:
- if exe.is_empty():
- return _spawn_failed_result()
- return _run_piped(
- exe, args, timeout_ms, capture_stderr, cancel_check, isolate_uv_resolution
- )
-
-
-static func _run_piped(
- exe: String,
- args: Array,
- timeout_ms: int,
- capture_stderr: bool,
- cancel_check: Callable,
- isolate_uv_resolution: bool,
-) -> Dictionary:
-
- var spawn_exe := exe
- var spawn_args := args
- if OS.get_name() == "Windows":
- var lower := exe.to_lower()
- if lower.ends_with(".cmd") or lower.ends_with(".bat"):
- ## CreateProcessW can't launch `.cmd` / `.bat` scripts on its
- ## own — they're cmd.exe input, not PE binaries. Without this
- ## wrap, the moment `McpCliFinder` resolves a Node-style shim
- ## (npm's `claude.cmd`, pnpm's wrappers, …) the next
- ## `OS.execute_with_pipe` surfaces "Could not create child
- ## process: ..." in Godot's output log (#251). Passing
- ## `exe` as a separate argv element keeps spaces in the path
- ## quoted by Godot's standard quoter — no manual escaping.
- spawn_exe = "cmd.exe"
- spawn_args = ["/c", exe]
- spawn_args.append_array(args)
-
- PortResolver.lock_process_spawn()
- var saved_uv_environment := (
- UvResolution.isolate_environment() if isolate_uv_resolution else {}
- )
- var info := OS.execute_with_pipe(spawn_exe, spawn_args)
- UvResolution.restore_environment(saved_uv_environment)
- PortResolver.unlock_process_spawn()
- if info.is_empty():
- return _spawn_failed_result()
-
- var pid: int = int(info.get("pid", -1))
- var stdio: Variant = info.get("stdio", null)
- var stderr_pipe: Variant = info.get("stderr", null)
- if pid <= 0:
- _close_pipes(stdio, stderr_pipe)
- return _spawn_failed_result()
- var deadline := Time.get_ticks_msec() + maxi(timeout_ms, _POLL_INTERVAL_MS)
- var kill_grant: Dictionary = {}
- ## Most discovery commands finish within a few polls. Do not spend Windows
- ## identity-query time capturing kill authority for an already-exited child.
- ## This short window shares the command deadline and observes cancellation;
- ## a child still running afterward needs the same exact grant as before.
- var quick_deadline := mini(deadline, Time.get_ticks_msec() + _QUICK_EXIT_WINDOW_MS)
- var cancellation_requested := false
- while OS.is_process_running(pid) and Time.get_ticks_msec() < quick_deadline:
- if cancel_check.is_valid() and bool(cancel_check.call()):
- cancellation_requested = true
- break
- OS.delay_msec(mini(_POLL_INTERVAL_MS, maxi(0, quick_deadline - Time.get_ticks_msec())))
- if OS.is_process_running(pid):
- kill_grant = PortResolver.capture_process_kill_grant(pid)
-
- while OS.is_process_running(pid):
- var cancelled := cancellation_requested or (cancel_check.is_valid() and bool(cancel_check.call()))
- if cancelled or Time.get_ticks_msec() >= deadline:
- ## Kill before draining: a pipe read can block while the launcher or
- ## one of its descendants still owns the write end. Windows taskkill
- ## /T gives us tree authority; POSIX OS.kill only targets this exact
- ## PID, so even a dead launcher cannot prove its descendants stopped.
- var killed: Array[int] = []
- if not kill_grant.is_empty():
- killed = PortResolver.kill_exact_processes([kill_grant], false, true)
- var kill_deadline := Time.get_ticks_msec() + _KILL_GRACE_MS
- while PortResolver.pid_alive(pid) and Time.get_ticks_msec() < kill_deadline:
- OS.delay_msec(_POLL_INTERVAL_MS)
-
- var termination_failed := (
- PortResolver.pid_alive(pid)
- or OS.get_name() != "Windows"
- or not killed.has(pid)
- )
- var partial_stdout := ""
- var partial_stderr := ""
- if not termination_failed:
- partial_stdout = _drain_pipe(stdio)
- partial_stderr = _drain_pipe(stderr_pipe) if capture_stderr else ""
- _close_pipes(stdio, stderr_pipe)
- return {
- "exit_code": -1,
- "stdout": partial_stdout,
- "stderr": partial_stderr,
- "output": _join_streams(partial_stdout, partial_stderr),
- "timed_out": not cancelled,
- "cancelled": cancelled,
- "spawn_failed": false,
- "termination_failed": termination_failed,
- }
- OS.delay_msec(_POLL_INTERVAL_MS)
-
- var stdout := _drain_pipe(stdio)
- var stderr_text := _drain_pipe(stderr_pipe) if capture_stderr else ""
- _close_pipes(stdio, stderr_pipe)
-
- return {
- "exit_code": OS.get_process_exit_code(pid),
- "stdout": stdout,
- "stderr": stderr_text,
- "output": _join_streams(stdout, stderr_text),
- "timed_out": false,
- "cancelled": false,
- "spawn_failed": false,
- "termination_failed": false,
- }
-
-
-static func _spawn_failed_result() -> Dictionary:
- return {
- "exit_code": -1,
- "stdout": "",
- "stderr": "",
- "output": "",
- "timed_out": false,
- "cancelled": false,
- "spawn_failed": true,
- "termination_failed": false,
- }
-
-
-static func _drain_pipe(pipe: Variant) -> String:
- if not (pipe is FileAccess):
- return ""
- var f := pipe as FileAccess
- var bytes := PackedByteArray()
- var max_bytes := 1 << 20 # 1 MiB, far above expected client CLI output.
- while bytes.size() < max_bytes:
- var chunk := f.get_buffer(mini(4096, max_bytes - bytes.size()))
- if chunk.is_empty():
- break
- bytes.append_array(chunk)
- if f.eof_reached():
- break
- return bytes.get_string_from_utf8()
-
-
-static func _join_streams(stdout: String, stderr_text: String) -> String:
- ## Most CLIs write their actionable diagnostics to one stream or the
- ## other, never both — so concatenation gives "the message" without
- ## the caller having to guess which key to read. Newline-separate so
- ## callers that grep don't see two lines run together.
- if stderr_text.is_empty():
- return stdout
- if stdout.is_empty():
- return stderr_text
- return "%s\n%s" % [stdout, stderr_text]
-
-
-static func _close_pipes(stdio: Variant, stderr_pipe: Variant) -> void:
- if stdio is FileAccess:
- (stdio as FileAccess).close()
- if stderr_pipe is FileAccess:
- (stderr_pipe as FileAccess).close()
diff --git a/addons/godot_ai/clients/_cli_exec.gd.uid b/addons/godot_ai/clients/_cli_exec.gd.uid
deleted file mode 100644
index 4a97a2c..0000000
--- a/addons/godot_ai/clients/_cli_exec.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dhoe3ypkhm12v
diff --git a/addons/godot_ai/clients/_cli_finder.gd b/addons/godot_ai/clients/_cli_finder.gd
deleted file mode 100644
index 394e76c..0000000
--- a/addons/godot_ai/clients/_cli_finder.gd
+++ /dev/null
@@ -1,181 +0,0 @@
-@tool
-class_name McpCliFinder
-extends RefCounted
-
-## Generic three-tier CLI resolution for clients whose binary lives somewhere
-## a GUI-launched Godot's minimal PATH won't see:
-## 1. Well-known install locations (~/.local/bin, /opt/homebrew/bin, ...)
-## 2. Login shell lookup (`bash -lc 'command -v '`) — picks up .zshrc / .bashrc
-## 3. Plain `which` / `where` against the inherited PATH
-## Caches per-exe so repeated dock refreshes don't fork a shell every frame.
-##
-## Thread safety: `find()` runs on action-worker threads
-## (`_run_client_action_worker` in `mcp_dock.gd`), and `invalidate()` runs on
-## the main thread (manual Refresh path). Godot `Dictionary` is not safe for
-## concurrent mutation, so `_cache` / `_searched` access is guarded by
-## `_mutex`. The mutex is held only across dictionary read/write — the slow
-## `_resolve()` path (FileAccess + bounded subprocess lookup) runs unlocked, so a
-## main-thread `invalidate()` can never block on a worker's subprocess.
-## Two workers racing the same exe both call `_resolve()` and both write
-## back the same answer; that's wasted work, not corruption.
-
-
-static var _mutex: Mutex = Mutex.new()
-static var _cache: Dictionary = {} # exe_name -> resolved path (or "")
-static var _searched: Dictionary = {}
-
-const _LOOKUP_TIMEOUT_MS := 3000
-
-
-## Find any of the supplied exe names; returns the first hit.
-## On Windows pass the .exe variant in `exe_names` if relevant.
-static func find(exe_names: Array[String]) -> String:
- for name in exe_names:
- var hit := _find_one(name)
- if not hit.is_empty():
- return hit
- return ""
-
-
-## Drop cache for one exe (call after the user installs / reinstalls).
-static func invalidate(exe_name: String = "") -> void:
- _mutex.lock()
- if exe_name.is_empty():
- _cache.clear()
- _searched.clear()
- else:
- _cache.erase(exe_name)
- _searched.erase(exe_name)
- _mutex.unlock()
-
-
-static func _find_one(exe_name: String) -> String:
- _mutex.lock()
- var already_searched: bool = _searched.get(exe_name, false)
- var cached: String = _cache.get(exe_name, "")
- _mutex.unlock()
- if already_searched:
- return cached
- # `_resolve()` does FileAccess + bounded subprocess lookup (forks
- # `bash -lc` / `which`), which can take 100ms-1s. Holding the mutex across that
- # would let a concurrent `invalidate()` on the main thread freeze the
- # editor for the duration of the subprocess — which defeats the whole
- # point of running CLI lookup off the main thread.
- var hit := _resolve(exe_name)
- _mutex.lock()
- _cache[exe_name] = hit
- _searched[exe_name] = true
- _mutex.unlock()
- return hit
-
-
-static func _resolve(exe_name: String) -> String:
- var is_windows := OS.get_name() == "Windows"
-
- # 1. Well-known locations
- for dir in _well_known_dirs():
- var full := dir.path_join(exe_name)
- if FileAccess.file_exists(full):
- return full
-
- # 2. Login shell lookup (Unix only)
- if not is_windows:
- ## env_lookup, not OS.get_environment: CLI resolution runs on dock
- ## worker threads (configure/remove actions) and must not race the
- ## spawn window's setenv/unsetenv (#691).
- var shell := McpPathTemplate.env_lookup("SHELL")
- if shell.is_empty():
- shell = "/bin/bash"
- var stripped := exe_name.trim_suffix(".exe")
- var login_result := McpCliExec.run(shell, ["-lc", "command -v %s" % stripped], _LOOKUP_TIMEOUT_MS, false)
- if int(login_result.get("exit_code", -1)) == 0:
- var login_found: String = str(login_result.get("stdout", "")).strip_edges()
- if not login_found.is_empty() and FileAccess.file_exists(login_found):
- return login_found
-
- # 3. which / where with inherited PATH
- var lookup := "where" if is_windows else "which"
- var result := McpCliExec.run(lookup, [exe_name], _LOOKUP_TIMEOUT_MS, false)
- if int(result.get("exit_code", -1)) == 0:
- var output := str(result.get("stdout", ""))
- var lines := PackedStringArray(output.split("\n"))
- var found := _pick_best_path(lines) if is_windows else lines[0].strip_edges()
- if not found.is_empty():
- return found
- return ""
-
-
-## Executable extensions Windows' CreateProcessW can launch from a path
-## (after the cmd.exe wrap in `_cli_exec.gd`). Order is preference: `.exe`
-## is a native PE binary; `.cmd` / `.bat` go through the shell; `.com` is
-## the legacy COM-format executable that some shims still ship.
-const _WINDOWS_EXEC_EXTS := [".exe", ".cmd", ".bat", ".com"]
-
-
-## Pick the best path from `where` output on Windows.
-##
-## npm-installed Node CLIs ship as BOTH `/` (a POSIX bash shim
-## for WSL / Git Bash users) AND `/.cmd` (the actual Windows
-## wrapper). `where ` lists both. CreateProcessW — the underlying
-## syscall behind `OS.execute_with_pipe` — refuses to launch the
-## extensionless POSIX shim, surfacing as
-## `ERROR: Could not create child process: "...\claude" mcp list`
-## in Godot's output log (#251). Picking a path with a real executable
-## extension dodges that entirely.
-##
-## Extension scan is the OUTER loop so the order in `_WINDOWS_EXEC_EXTS`
-## drives preference — `.exe` wins over `.cmd` even when the `.cmd` shows
-## up first in `where` output (one fewer process per shell-out). Falls
-## back to the first non-empty line when no entry has a recognised
-## extension, so we never come up empty when `where` returned *something*.
-static func _pick_best_path(lines: PackedStringArray) -> String:
- var stripped := PackedStringArray()
- for raw in lines:
- var line := raw.strip_edges()
- if not line.is_empty():
- stripped.append(line)
- if stripped.is_empty():
- return ""
- for ext in _WINDOWS_EXEC_EXTS:
- for candidate in stripped:
- if candidate.to_lower().ends_with(ext):
- return candidate
- return stripped[0]
-
-
-static func _well_known_dirs() -> Array[String]:
- ## env_lookup, not OS.get_environment — see _resolve()'s worker-thread
- ## note (#691).
- var home := McpPathTemplate.env_lookup("HOME")
- if home.is_empty():
- home = McpPathTemplate.env_lookup("USERPROFILE")
- match OS.get_name():
- "macOS":
- return [
- home.path_join(".local/bin"),
- home.path_join(".claude/local"),
- home.path_join(".cargo/bin"),
- "/opt/homebrew/bin",
- "/usr/local/bin",
- ]
- "Windows":
- var local := McpPathTemplate.env_lookup("LOCALAPPDATA")
- var prog := McpPathTemplate.env_lookup("ProgramFiles")
- var paths: Array[String] = []
- if not home.is_empty():
- paths.append(home.path_join(".claude/local"))
- paths.append(home.path_join(".local/bin"))
- paths.append(home.path_join(".cargo/bin"))
- paths.append(home.path_join("AppData/Local/Programs/uv"))
- if not local.is_empty():
- paths.append(local.path_join("Programs/uv"))
- if not prog.is_empty():
- paths.append(prog.path_join("uv"))
- return paths
- _:
- return [
- home.path_join(".local/bin"),
- home.path_join(".claude/local"),
- home.path_join(".cargo/bin"),
- "/usr/local/bin",
- ]
diff --git a/addons/godot_ai/clients/_cli_finder.gd.uid b/addons/godot_ai/clients/_cli_finder.gd.uid
deleted file mode 100644
index 9985270..0000000
--- a/addons/godot_ai/clients/_cli_finder.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cnp5b6fcwou2y
diff --git a/addons/godot_ai/clients/_cli_strategy.gd b/addons/godot_ai/clients/_cli_strategy.gd
deleted file mode 100644
index 2d2d013..0000000
--- a/addons/godot_ai/clients/_cli_strategy.gd
+++ /dev/null
@@ -1,466 +0,0 @@
-@tool
-class_name McpCliStrategy
-extends RefCounted
-
-## Strategy for MCP clients that own their own state via a CLI (e.g.
-## `claude mcp add`). Reads `cli_register_template` / `cli_unregister_template`
-## / `cli_status_args` from the descriptor and substitutes `{name}` / `{url}`
-## tokens. Command-shape descriptors additionally use the whole-element launch
-## tokens `{command}` / `{args...}` (see `format_args`). No descriptor-supplied
-## Callables — see `_base.gd` for why.
-##
-## Every shell-out goes through `McpCliExec.run`, which wraps the call in a
-## wall-clock timeout. A hung CLI (e.g. `claude mcp list` under
-## inter-Claude-Code contention) gets killed at the budget instead of
-## locking up the caller forever — see issues #238 / #239.
-
-## The descriptor token resolved from `godot_ai/mcp_client_scope` (#872).
-## Only Claude Code takes it today; any `config_type = "cli"` descriptor can.
-const SCOPE_TOKEN := "{scope}"
-
-const _CONFIGURE_TIMEOUT_MS := 10000
-const _REMOVE_TIMEOUT_MS := 10000
-const _STATUS_TIMEOUT_MS := 6000
-const MutationLock := preload("res://addons/godot_ai/utils/client_mutation_lock.gd")
-
-
-static func configure(
- client: McpClient,
- server_name: String,
- server_url: String,
- launch: Dictionary = {},
-) -> Dictionary:
- ## Fail closed before any subprocess runs: a command-shape client without a
- ## verified attach launcher must not register anything (see
- ## docs/client-configuration.md — an ERROR beats an entry known to be broken).
- var launch_error := command_launch_error(client, launch)
- if not launch_error.is_empty():
- return {"status": "error", "message": launch_error}
- var cli := _resolve_cli(client)
- if cli.is_empty():
- return {"status": "error", "message": "%s not found" % client.display_name}
- return _configure_claimed(client, server_name, server_url, launch, cli)
-
-
-static func _configure_claimed(
- client: McpClient,
- server_name: String,
- server_url: String,
- launch: Dictionary,
- cli: String,
-) -> Dictionary:
-
- # Best-effort prior cleanup so re-configure is idempotent. Ordinary absent-
- # entry/non-zero results remain ignorable; an unproven timeout is not,
- # because its descendant could race the configure that follows.
- if not client.cli_unregister_template.is_empty():
- ## #872: a `{scope}` descriptor sweeps EVERY scope, not just the
- ## selected one. Flipping the setting user -> project and pressing
- ## Configure would otherwise leave the old user-scope entry alive —
- ## exactly the "loaded in every unrelated workspace" problem the
- ## setting exists to fix. `mcp remove` on an absent entry is a no-op,
- ## so ordinary results are discarded; ambiguous termination aborts the
- ## sequence. Each extra scope costs one bounded subprocess.
- for pre_scope in _cleanup_scopes(client):
- var pre_args := _format_args(
- client.cli_unregister_template, server_name, server_url, {}, pre_scope
- )
- var cleaned := McpCliExec.run(cli, pre_args, _REMOVE_TIMEOUT_MS)
- var cleanup_failure := _unproven_mutation_failure(
- client, "Prior cleanup", cleaned
- )
- if not cleanup_failure.is_empty():
- return cleanup_failure
-
- if client.cli_register_template.is_empty():
- return {"status": "error", "message": "%s descriptor missing cli_register_template" % client.display_name}
- var args := _format_args(client.cli_register_template, server_name, server_url, launch)
- var result := McpCliExec.run(cli, args, _CONFIGURE_TIMEOUT_MS)
- var termination_failure := _unproven_mutation_failure(client, "Configure", result)
- if not termination_failure.is_empty():
- return termination_failure
- if result.get("timed_out", false):
- return {
- "status": "error",
- "message": "Configure %s timed out after %ds — see 'Run this manually' below to retry by hand" % [
- client.display_name, _CONFIGURE_TIMEOUT_MS / 1000,
- ],
- }
- if result.get("spawn_failed", false):
- return {"status": "error", "message": "Failed to spawn %s" % client.display_name}
- if int(result.get("exit_code", -1)) == 0:
- return {"status": "ok", "message": McpClient.configured_message(client, server_url)}
- ## `claude mcp add` writes its real failure diagnostics to stderr, so
- ## prefer `output` (stdout + stderr) over `stdout` alone — otherwise
- ## the user sees "exit code 1" instead of the actual error.
- var combined := str(result.get("output", "")).strip_edges()
- var err := combined if not combined.is_empty() else "exit code %d" % int(result.get("exit_code", -1))
- return {"status": "error", "message": "Failed to configure %s: %s" % [client.display_name, err]}
-
-
-## Run the descriptor's `cli_status_args`, scan stdout for `server_name` and
-## the expected target. The matching rule is the only sensible one for "list
-## MCP entries" output across CLI clients we currently support: name AND
-## target present → CONFIGURED; name only → MISMATCH; neither →
-## NOT_CONFIGURED. For URL descriptors the target is `server_url`; for
-## command-shape descriptors it is the resolved attach launcher path (the
-## listing prints the registered command line, not a URL). Command-shape CLI
-## clients with a JSON fallback file get exact drift detection via the JSON
-## strategy instead — the configurator prefers that path and only lands here
-## for CLI clients whose state isn't file-readable.
-static func check_status(
- client: McpClient, server_name: String, server_url: String, launch: Dictionary = {}
-) -> McpClient.Status:
- return check_status_with_cli_path(client, server_name, server_url, _resolve_cli(client), launch)
-
-
-static func check_status_with_cli_path(
- client: McpClient, server_name: String, server_url: String, cli: String, launch: Dictionary = {}
-) -> McpClient.Status:
- return check_status_details(client, server_name, server_url, cli, launch).get("status", McpClient.Status.NOT_CONFIGURED)
-
-
-## Detailed variant used by the dock's refresh worker so it can surface a
-## "probe timed out" badge on the affected row instead of silently
-## conflating the timeout with NOT_CONFIGURED. Returns
-## `{"status": Status, "error_msg": String}`. The caller plumbs
-## `error_msg` straight into `_apply_row_status`.
-static func check_status_details(
- client: McpClient, server_name: String, server_url: String, cli: String, launch: Dictionary = {}
-) -> Dictionary:
- if cli.is_empty():
- return _status_details(McpClient.Status.NOT_CONFIGURED)
- if client.cli_status_args.is_empty():
- return _status_details(McpClient.Status.NOT_CONFIGURED)
- var expected_target := server_url
- if client.command_shape != McpClient.CommandShape.NONE:
- ## Same fail-closed contract as configure: without a verified launcher
- ## there is no target to compare against, and guessing would report a
- ## broken entry as green.
- var launch_error := command_launch_error(client, launch)
- if not launch_error.is_empty():
- return _status_details(McpClient.Status.ERROR, launch_error)
- expected_target = str(launch.get("command", ""))
- var result := McpCliExec.run(
- cli,
- McpClient._array_from_packed(client.cli_status_args),
- _STATUS_TIMEOUT_MS,
- false
- )
- if result.get("timed_out", false):
- return _status_details(McpClient.Status.ERROR, "probe timed out")
- if result.get("spawn_failed", false):
- return _status_details(McpClient.Status.NOT_CONFIGURED)
- if int(result.get("exit_code", -1)) != 0:
- return _status_details(McpClient.Status.NOT_CONFIGURED)
- var text := str(result.get("stdout", ""))
- if text.find(server_name) < 0:
- return _status_details(McpClient.Status.NOT_CONFIGURED)
- ## Server registered, but pointing somewhere else — drift after a
- ## port change. Surface as mismatch so the dock offers Reconfigure.
- if text.find(expected_target) < 0:
- return _status_details(
- McpClient.Status.CONFIGURED_MISMATCH, "", _probe_output_launches_godot_ai(text)
- )
- return _status_details(McpClient.Status.CONFIGURED)
-
-
-## Empty string when this client's launch requirements are satisfied. A
-## command-shape descriptor requires a successfully resolved attach launch;
-## URL descriptors (`CommandShape.NONE`) never require one. Mirrors
-## `McpJsonStrategy.command_launch_error` / the TOML equivalent.
-static func command_launch_error(client: McpClient, launch: Dictionary) -> String:
- if client.command_shape == McpClient.CommandShape.NONE:
- return ""
- if not bool(launch.get("ok", false)):
- return str(launch.get("error", "No compatible attach launcher was found."))
- return ""
-
-
-static func _status_details(
- status: McpClient.Status, error_msg: String = "", owned: bool = false
-) -> Dictionary:
- return {"status": status, "error_msg": error_msg, "owned": owned}
-
-
-static func remove(client: McpClient, server_name: String) -> Dictionary:
- var cli := _resolve_cli(client)
- if cli.is_empty():
- return {"status": "error", "message": "%s not found" % client.display_name}
- return _remove_claimed(client, server_name, cli)
-
-
-static func _remove_claimed(client: McpClient, server_name: String, cli: String) -> Dictionary:
- if client.cli_unregister_template.is_empty():
- return {"status": "error", "message": "%s descriptor missing cli_unregister_template" % client.display_name}
- var args := _format_args(client.cli_unregister_template, server_name, "")
- var result := McpCliExec.run(cli, args, _REMOVE_TIMEOUT_MS)
- var termination_failure := _unproven_mutation_failure(client, "Remove", result)
- if not termination_failure.is_empty():
- return termination_failure
- if result.get("timed_out", false):
- return {
- "status": "error",
- "message": "Remove %s timed out after %ds — see 'Run this manually' below to retry by hand" % [
- client.display_name, _REMOVE_TIMEOUT_MS / 1000,
- ],
- }
- if result.get("spawn_failed", false):
- return {"status": "error", "message": "Failed to spawn %s" % client.display_name}
- if int(result.get("exit_code", -1)) == 0:
- return {"status": "ok", "message": "%s configuration removed" % client.display_name}
- ## `claude mcp add` writes its real failure diagnostics to stderr, so
- ## prefer `output` (stdout + stderr) over `stdout` alone — otherwise
- ## the user sees "exit code 1" instead of the actual error.
- var combined := str(result.get("output", "")).strip_edges()
- var err := combined if not combined.is_empty() else "exit code %d" % int(result.get("exit_code", -1))
- return {"status": "error", "message": "Failed to remove %s: %s" % [client.display_name, err]}
-
-
-## A POSIX exact-PID kill cannot prove that a CLI's descendants stopped. Never
-## issue the next write after that ambiguous boundary: the old process could
-## otherwise land a stale mutation after the new one. The durable global claim
-## remains until the user explicitly clears it after stopping those processes.
-static func _unproven_mutation_failure(
- client: McpClient, operation: String, result: Dictionary
-) -> Dictionary:
- if not bool(result.get("termination_failed", false)):
- return {}
- return {
- "status": "error",
- "message": (
- "%s for %s could not be proven stopped. %s"
- % [operation, client.display_name, MutationLock.recovery_message()]
- ),
- "termination_failed": true,
- }
-
-
-## Substitute `{name}` and `{url}` tokens in every template entry.
-## Tokens match verbatim — `{name_suffix}` is NOT touched, so callers don't
-## have to worry about partial-token collisions in their argv.
-##
-## Launch tokens are whole-element only: an element that is exactly
-## `{command}` becomes the resolved attach launcher path, and an element that
-## is exactly `{args...}` is spliced into the argv as one element per launch
-## arg. Whole-element matching keeps a literal brace inside a path or flag
-## from ever triggering an expansion.
-##
-## `scope_override` forces a specific `{scope}` value instead of the live
-## setting; the configure pre-cleanup uses it to sweep the scopes the user is
-## NOT currently on. Empty means "resolve from the setting".
-static func format_args(
- template: PackedStringArray,
- server_name: String,
- server_url: String,
- launch: Dictionary = {},
- scope_override: String = "",
-) -> Array[String]:
- return _format_args(template, server_name, server_url, launch, scope_override)
-
-
-static func _format_args(
- template: PackedStringArray,
- server_name: String,
- server_url: String,
- launch: Dictionary = {},
- scope_override: String = "",
-) -> Array[String]:
- var scope := scope_override if not scope_override.is_empty() else McpSettings.client_scope()
- var out: Array[String] = []
- for arg in template:
- var s := String(arg)
- if s == "{command}":
- out.append(str(launch.get("command", "")))
- continue
- if s == "{args...}":
- for launch_arg in launch.get("args", []):
- out.append(str(launch_arg))
- continue
- s = s.replace("{name}", server_name)
- s = s.replace("{url}", server_url)
- ## Resolved per-call rather than baked into the descriptor so the
- ## setting can change without an editor restart, and so the manual
- ## command shown in the dock always matches what Configure would run.
- s = s.replace(SCOPE_TOKEN, scope)
- out.append(s)
- return out
-
-
-## Scope-aware status for descriptors that declare `cli_scope_status_template`.
-## Returns the same `{"status", "error_msg"}` shape as check_status_details.
-##
-## Split from the subprocess call so the verdict is a pure function of what the
-## CLI printed — `_scope_probe_verdict` is unit-tested against real recorded
-## `claude mcp get` output rather than needing a `claude` binary on the runner.
-static func check_scope_status_details(
- client: McpClient,
- server_name: String,
- server_url: String,
- cli: String,
- launch: Dictionary,
- expected_scope: String,
-) -> Dictionary:
- if cli.is_empty() or client.cli_scope_status_template.is_empty():
- return _status_details(McpClient.Status.NOT_CONFIGURED)
- var expected_target := server_url
- var expected_args: Array[String] = []
- var expected_type := ""
- if client.command_shape != McpClient.CommandShape.NONE:
- ## Same fail-closed contract as configure and the plain status probe.
- var launch_error := command_launch_error(client, launch)
- if not launch_error.is_empty():
- return _status_details(McpClient.Status.ERROR, launch_error)
- expected_target = str(launch.get("command", ""))
- for launch_arg in launch.get("args", []):
- expected_args.append(str(launch_arg))
- expected_type = str(client.command_transport_value)
- var args := _format_args(client.cli_scope_status_template, server_name, server_url)
- var result := McpCliExec.run(cli, args, _STATUS_TIMEOUT_MS, false)
- if result.get("timed_out", false):
- return _status_details(McpClient.Status.ERROR, "probe timed out")
- if result.get("spawn_failed", false):
- return _status_details(McpClient.Status.NOT_CONFIGURED)
- return _scope_probe_verdict(
- int(result.get("exit_code", -1)),
- str(result.get("stdout", "")),
- expected_scope,
- expected_target,
- expected_args,
- expected_type,
- )
-
-
-## The `Scope:` label the probe printed, normalised to a CLIENT_SCOPES value,
-## or "" when no line was recognisable. Matched on the first word after the
-## label so the parenthetical blurb ("(shared via .mcp.json)") can change
-## wording between CLI releases without breaking detection.
-static func _scope_from_probe_output(text: String) -> String:
- for raw_line in text.split("\n"):
- var line := String(raw_line).strip_edges()
- var marker := line.find("Scope:")
- if marker < 0:
- continue
- var rest := line.substr(marker + 6).strip_edges().to_lower()
- for scope in McpSettings.CLIENT_SCOPES:
- var name := String(scope)
- if rest.begins_with(name):
- return name
- return ""
-
-
-## The probe output names our server; only its launch fields decide whether
-## the existing entry is ours to rewrite after an update.
-static func _probe_output_launches_godot_ai(text: String) -> bool:
- return McpClient.launch_mentions_godot_ai(
- _field_from_probe_output(text, "Command:")
- + " "
- + _field_from_probe_output(text, "Args:")
- + " "
- + _field_from_probe_output(text, "URL:")
- )
-
-
-static func _field_from_probe_output(text: String, label: String) -> String:
- for raw_line in text.split("\n"):
- var line := String(raw_line).strip_edges()
- if line.begins_with(label):
- return line.substr(label.length()).strip_edges()
- return ""
-
-
-## Pure verdict for a scope probe. A non-zero exit is the CLI's "no such
-## server" signal. An entry resolved from a scope other than the selected one
-## is MISMATCH, not CONFIGURED: the dock offers Reconfigure, and Configure's
-## all-scope pre-cleanup is what actually moves it.
-##
-## A missing or unrecognised `Scope:` line degrades to the target check alone,
-## deliberately rather than failing closed. `_verify_post_state` turns any
-## non-CONFIGURED status after a successful write into an error, so treating an
-## unparsed label as MISMATCH would mean a future CLI release that reworded or
-## dropped that line breaks Configure outright — a false error plus a
-## permanently amber row whose Reconfigure button cannot clear it. The entry
-## has already been matched by name and exact launcher path at that point; the
-## only thing in doubt is which scope it came from, and a slightly optimistic
-## dot is a much cheaper failure than a configure flow that reports success as
-## failure. Today's claude (2.1.241) always prints the line, so this is a
-## forward-compatibility hedge, not a live code path.
-static func _scope_probe_verdict(
- exit_code: int,
- text: String,
- expected_scope: String,
- expected_target: String,
- expected_args: Array[String] = [],
- expected_type: String = "",
-) -> Dictionary:
- if exit_code != 0:
- return _status_details(McpClient.Status.NOT_CONFIGURED)
- ## `resolved_scope` rides along as structured data so callers can act on it
- ## — `_verify_post_state`'s path hint needs to know WHICH scope survived to
- ## name the right file, and parsing it back out of the human-facing message
- ## would couple that hint to this wording (#879). Parsed BEFORE the target
- ## check so both mismatch paths carry it: an entry whose launcher drifted
- ## sends the user to the wrong file just as readily as one whose scope did.
- var resolved := _scope_from_probe_output(text)
- var command_matches := (
- expected_target.is_empty()
- or _field_from_probe_output(text, "Command:") == expected_target
- )
- var launch_details_match := true
- if not expected_type.is_empty():
- launch_details_match = (
- _field_from_probe_output(text, "Type:") == expected_type
- and _field_from_probe_output(text, "Args:") == " ".join(expected_args)
- )
- if not command_matches or not launch_details_match:
- var drifted := _status_details(
- McpClient.Status.CONFIGURED_MISMATCH, "", _probe_output_launches_godot_ai(text)
- )
- if not resolved.is_empty():
- drifted["resolved_scope"] = resolved
- return drifted
- if resolved.is_empty():
- return _status_details(McpClient.Status.CONFIGURED)
- if resolved != expected_scope:
- var details := _status_details(
- McpClient.Status.CONFIGURED_MISMATCH,
- "registered at %s scope, not %s" % [resolved, expected_scope],
- _probe_output_launches_godot_ai(text),
- )
- details["resolved_scope"] = resolved
- return details
- return _status_details(McpClient.Status.CONFIGURED)
-
-
-## True when the descriptor's register template takes `{scope}`, i.e. where
-## its entry lands is decided by the `godot_ai/mcp_client_scope` setting rather
-## than fixed by the descriptor. The configurator uses this to decide whether
-## the JSON-fallback file is still a valid place to read status back from.
-static func uses_scope_token(client: McpClient) -> bool:
- return client.cli_register_template.has(SCOPE_TOKEN)
-
-
-## Public view of the pre-cleanup sweep, for the manual-command text: what the
-## dock tells the user to run has to match what Configure actually runs (#877).
-static func cleanup_scopes(client: McpClient) -> Array[String]:
- return _cleanup_scopes(client)
-
-
-## Scopes the configure pre-cleanup removes from. A descriptor without the
-## `{scope}` token has exactly one place its entry can live, so it keeps the
-## single pass it always had — "" means "resolve the scope normally".
-static func _cleanup_scopes(client: McpClient) -> Array[String]:
- if not uses_scope_token(client):
- return [""]
- var scopes: Array[String] = []
- for scope in McpSettings.CLIENT_SCOPES:
- scopes.append(String(scope))
- return scopes
-
-
-static func _resolve_cli(client: McpClient) -> String:
- return McpCliFinder.find(McpClient._array_from_packed(client.cli_names))
-
-
-static func resolve_cli_path(client: McpClient) -> String:
- return _resolve_cli(client)
diff --git a/addons/godot_ai/clients/_cli_strategy.gd.uid b/addons/godot_ai/clients/_cli_strategy.gd.uid
deleted file mode 100644
index f84a92b..0000000
--- a/addons/godot_ai/clients/_cli_strategy.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bvib7d8eabbcm
diff --git a/addons/godot_ai/clients/_dsh_strategy.gd b/addons/godot_ai/clients/_dsh_strategy.gd
deleted file mode 100644
index 4d86c72..0000000
--- a/addons/godot_ai/clients/_dsh_strategy.gd
+++ /dev/null
@@ -1,763 +0,0 @@
-@tool
-class_name McpDshStrategy
-extends RefCounted
-
-## Configure / status / remove for DeepSeek Harness (dsh) MCP servers.
-##
-## DeepSeek Harness has no `mcp` CLI verb (verified against dsh
-## 0.1.0-rc.6 — `dsh --help` exposes only `web` and `plugin`). MCP servers
-## register as `@deepseek-ai/dsh-mcp-client` plugin entries in the HOME patch
-## layer `$DSH_HOME/cordis.patch.yml` — the machine-local layer dsh applies
-## over EVERY profile's own patch layer (the web GUI included), so one entry
-## covers every profile the user boots.
-##
-## The patch file is a top-level YAML array of loader patch entries
-## (`@deepseek-ai/cordis-plugin-include`'s `applyEntryPatches`). A non-insert
-## row only OVERRIDES an existing bundle row (a new id is skipped with a
-## warning), so a brand-new server must be added with an `insert` row:
-##
-## - insert:
-## - id: mcp-godot-ai
-## name: '@deepseek-ai/dsh-mcp-client'
-## config:
-## serverName: godot-ai
-## transport: stdio
-## command: "..."
-## args: [...]
-##
-## Both shapes verified live against the dsh loader
-## (`dsh --profile web --dump-config` composes the inserted entry; a plain
-## row for a new id warns "patch: entry not found" and is skipped). We write
-## one dedicated insert row per server, preserve every other row byte-for-byte
-## (including other users' insert rows, overrides, comments, and `!!js`
-## expressions), and status-verify the entry we wrote — the same file-read
-## drift contract the JSON/TOML/YAML strategies use.
-##
-## The entry's `config` nests the launch under `serverName`/`transport`/
-## `command`/`args`; `serverName` is the model-facing tool namespace
-## (`mcp__godot-ai__`), so it carries the server name like the
-## `server_key_path` map key does for the other strategies.
-
-## Loader entry id prefix inside the patch list. The full id is
-## `` — "mcp-godot-ai" for the godot-ai server.
-const ENTRY_ID_PREFIX := "mcp-"
-## The loader plugin that bridges MCP servers into the harness. Must be
-## installed in the profile's node_modules (dsh ships it as a dependency of
-## the CLI package). Quoted in the file: `@` is a reserved YAML indicator and
-## cannot start a plain scalar.
-const PLUGIN_NAME := "@deepseek-ai/dsh-mcp-client"
-## Indent of a nested entry header under `- insert:` (children step 2 spaces
-## deeper), matching the shipped profile patch files.
-const ENTRY_INDENT := 4
-
-
-static func entry_id(server_name: String) -> String:
- return ENTRY_ID_PREFIX + server_name
-
-
-static func configure(
- client: McpClient,
- server_name: String,
- server_url: String,
- launch: Dictionary = {},
-) -> Dictionary:
- var resolution := client.resolved_config_path_details()
- var path := str(resolution.get("path", ""))
- var path_error := str(resolution.get("error", ""))
- if not path_error.is_empty():
- return {"status": "error", "message": path_error}
- if path.is_empty():
- return {"status": "error", "message": "Could not resolve config path for %s on this OS" % client.display_name}
- ## Fail closed before touching the file — same contract as JSON/TOML/YAML.
- var launch_error := command_launch_error(client, launch)
- if not launch_error.is_empty():
- return {"status": "error", "message": launch_error}
-
- var read := _read(path)
- if not read["ok"]:
- return {"status": "error", "message": "Refusing to overwrite %s: %s. Fix or move the file, then re-run Configure." % [path, read["error"]]}
- ## Fail closed on content that is not a loader patch list: dsh's patch
- ## parser rejects a non-array file at boot, so appending our row to a
- ## top-level mapping would hand the user a broken file (#867 review).
- ## `[]` is a valid empty sequence meaning "no rows"; the literal must not
- ## survive an append (block rows after `[]` are malformed YAML), so only
- ## that line is dropped — comments around it belong to the user and stay.
- var text := String(read["data"])
- text = _strip_empty_flow_sequence(text)
- if not _is_patch_sequence(text):
- return {
- "status": "error",
- "message": "Refusing to overwrite %s: it is not a top-level YAML list of loader entries. Fix or remove the file, then re-run Configure." % path,
- }
-
- var existing_config: Variant = null
- var lines := _split_lines(text)
- var found := _find_entry_block(lines, entry_id(server_name), true)
- if found.is_empty():
- var plain := _find_entry_block(lines, entry_id(server_name), false)
- if not plain.is_empty():
- ## A plain `- id: mcp-godot-ai` row is inert for a new id — the
- ## loader skips it with a warning and never registers the server.
- ## Replacing the whole row with the insert form migrates it.
- found = plain
- if not found.is_empty():
- ## Pass the parsed config through so user-mutable fields (env,
- ## toolCallTimeoutMs, reconnect) survive a reconfigure.
- existing_config = _extract_config(lines, found)
- var new_entry := build_entry(client, server_name, server_url, existing_config, launch)
- ## Nested replacement must keep the sequence's existing header indent —
- ## items of one block sequence share indentation, so a hand-written
- ## 2-space file re-emitted at 4 spaces would no longer parse (#867
- ## review). Fresh rows and plain-row migrations keep the 4-space style.
- var base_indent := ENTRY_INDENT
- if not found.is_empty():
- var header_indent := int(found.get("indent", 0))
- if header_indent > 0:
- base_indent = header_indent
- var rendered := render_nested_entry(entry_id(server_name), new_entry, base_indent)
- var out := ""
- if found.is_empty():
- out = _append_row(lines, rendered)
- else:
- out = _replace_lines(lines, found, rendered)
- if not McpAtomicWrite.write(path, out):
- return {"status": "error", "message": "Cannot write to %s" % path}
- return {"status": "ok", "message": McpClient.configured_message(client, server_url)}
-
-
-static func check_status(
- client: McpClient,
- server_name: String,
- server_url: String,
- launch: Dictionary = {},
-) -> McpClient.Status:
- return check_status_details(client, server_name, server_url, launch)["status"]
-
-
-## Same contract as the other strategies (#711): {status, error_msg}. An
-## existing-but-unreadable config is ERROR with the diagnostic — not
-## NOT_CONFIGURED — so the dock row can tell "no config" from "config the
-## editor can't read" instead of offering a Configure that would fail.
-static func check_status_details(
- client: McpClient, server_name: String, server_url: String, launch: Dictionary = {}
-) -> Dictionary:
- var resolution := client.resolved_config_path_details()
- var path := str(resolution.get("path", ""))
- var path_error := str(resolution.get("error", ""))
- if not path_error.is_empty():
- return {"status": McpClient.Status.ERROR, "error_msg": path_error}
- if path.is_empty() or not FileAccess.file_exists(path):
- return {"status": McpClient.Status.NOT_CONFIGURED, "error_msg": ""}
- var read := _read(path)
- if not read["ok"]:
- return {
- "status": McpClient.Status.ERROR,
- "error_msg": "Cannot read %s: %s" % [path, read["error"]],
- }
- var text := String(read["data"])
- if not _is_patch_sequence(text):
- ## A top-level mapping/scalar is not a dsh patch list: nothing in it
- ## can be "configured", and a nested `- id:` inside a mapping must
- ## not read as ours (#867 review).
- return {
- "status": McpClient.Status.ERROR,
- "error_msg": "%s is not a top-level YAML list of loader entries; fix or remove the file." % path,
- }
- var lines := _split_lines(text)
- var found := _find_entry_block(lines, entry_id(server_name), true)
- if found.is_empty():
- ## A plain row for our id is inert (the loader skips unknown ids), so
- ## the server is NOT registered no matter what the file says. It only
- ## becomes registered once Configure rewrites it as an insert row.
- return {"status": McpClient.Status.NOT_CONFIGURED, "error_msg": ""}
- var config := _extract_config(lines, found)
- if config.is_empty():
- return {"status": McpClient.Status.NOT_CONFIGURED, "error_msg": ""}
- ## An entry exists but no verified launcher does — mirror the other
- ## strategies: this is an environment ERROR, not entry drift.
- var launch_error := command_launch_error(client, launch)
- if not launch_error.is_empty():
- return {"status": McpClient.Status.ERROR, "error_msg": launch_error}
- if verify_entry(client, config, server_name, server_url, launch):
- return {"status": McpClient.Status.CONFIGURED, "error_msg": ""}
- return {
- "status": McpClient.Status.CONFIGURED_MISMATCH,
- "error_msg": "",
- "owned": McpClient.launch_values_mention_godot_ai(McpClient.entry_launch_values(config)),
- }
-
-
-static func remove(client: McpClient, server_name: String) -> Dictionary:
- var resolution := client.resolved_config_path_details()
- var path := str(resolution.get("path", ""))
- var path_error := str(resolution.get("error", ""))
- if not path_error.is_empty():
- return {"status": "error", "message": path_error}
- if path.is_empty() or not FileAccess.file_exists(path):
- return {"status": "ok", "message": "Not configured"}
- var read := _read(path)
- if not read["ok"]:
- return {"status": "error", "message": "Refusing to rewrite %s: %s." % [path, read["error"]]}
- var text := String(read["data"])
- if not _is_patch_sequence(text):
- ## Same fail-closed contract as configure (#867 review): never modify
- ## a file that dsh cannot load as a patch list.
- return {
- "status": "error",
- "message": "Refusing to rewrite %s: it is not a top-level YAML list of loader entries. Fix or remove the file, then re-run Remove." % path,
- }
- var lines := _split_lines(text)
- var found := _find_entry_block(lines, entry_id(server_name), true)
- if found.is_empty():
- found = _find_entry_block(lines, entry_id(server_name), false)
- if found.is_empty():
- return {"status": "ok", "message": "%s configuration removed" % client.display_name}
- var out := _remove_lines(lines, found)
- if _is_blank_or_comment_only(out):
- ## dsh's patch parser requires a top-level YAML array: an empty or
- ## comment-only patch file FAILS BOOT (`loadOptionalPatches` throws on
- ## a non-array file), while an absent file means "no layer". After our
- ## row is gone, an all-blank file must therefore be deleted, not
- ## written back.
- if FileAccess.file_exists(path):
- var remove_err := DirAccess.remove_absolute(path)
- if remove_err != OK:
- return {"status": "error", "message": "Cannot remove %s (error %d)" % [path, remove_err]}
- return {"status": "ok", "message": "%s configuration removed" % client.display_name}
- if not McpAtomicWrite.write(path, out):
- return {"status": "error", "message": "Cannot write to %s" % path}
- return {"status": "ok", "message": "%s configuration removed" % client.display_name}
-
-
-## Build the `config` dict written inside the loader entry. Command mode
-## (FLAT, #838) nests `serverName`/`transport`/`command`/`args`; URL mode nests
-## `serverName`/`transport`/`url`. Existing user state (env, timeouts,
-## reconnect) survives a reconfigure via the deep copy.
-static func build_entry(
- client: McpClient,
- server_name: String,
- server_url: String,
- existing: Variant = null,
- launch: Dictionary = {},
-) -> Dictionary:
- var entry: Dictionary = (existing as Dictionary).duplicate(true) if existing is Dictionary else {}
- entry["serverName"] = server_name
- if client.command_shape == McpClient.CommandShape.FLAT:
- entry["command"] = str(launch.get("command", ""))
- entry["args"] = _array_copy(launch.get("args", []))
- if not client.command_transport_key.is_empty():
- entry[client.command_transport_key] = client.command_transport_value
- for key in client.command_initial_fields:
- if not entry.has(key):
- entry[key] = client.command_initial_fields[key]
- for key in client.command_legacy_keys:
- entry.erase(String(key))
- return entry
- if client.command_shape == McpClient.CommandShape.NONE:
- ## Single URL-mode source of truth (#867 review): build_entry's URL
- ## branch delegates to build_url_entry so the two can never drift.
- return build_url_entry(client, server_name, server_url, existing)
- return {}
-
-
-## URL-mode sibling for the manual-instruction fallback text and the
-## build_entry URL branch. DeepSeek Harness' mcp-client bridge accepts
-## `transport: streamable-http` with a `url`, so the entry keeps the same
-## `serverName` namespace with an HTTP transport instead of the stdio attach
-## bridge. Existing user state survives via the deep copy.
-static func build_url_entry(
- client: McpClient, server_name: String, server_url: String, existing: Variant = null
-) -> Dictionary:
- var entry: Dictionary = (existing as Dictionary).duplicate(true) if existing is Dictionary else {}
- entry["serverName"] = server_name
- entry["transport"] = "streamable-http"
- if not client.entry_url_field.is_empty():
- entry[client.entry_url_field] = server_url
- return entry
-
-
-## Verify a stored `config` dict matches the current launch. Command mode
-## checks every launch-affecting value exactly and requires legacy URL keys
-## gone; serverName must match the pinned namespace. URL mode checks the url
-## and the streamable-http pin.
-static func verify_entry(
- client: McpClient,
- config: Dictionary,
- server_name: String,
- server_url: String,
- launch: Dictionary = {},
-) -> bool:
- if config.get("serverName") != server_name:
- return false
- if client.command_shape != McpClient.CommandShape.NONE:
- if client.command_shape != McpClient.CommandShape.FLAT or not bool(launch.get("ok", false)):
- return false
- for key in client.command_legacy_keys:
- if config.has(String(key)):
- return false
- if config.get("command") != launch.get("command"):
- return false
- if not _arrays_equal(config.get("args", null), launch.get("args", null)):
- return false
- if not client.command_transport_key.is_empty():
- if config.get(client.command_transport_key, null) != client.command_transport_value:
- return false
- return true
- if config.get(client.entry_url_field, "") != server_url:
- return false
- return config.get("transport", "") == "streamable-http"
-
-
-## Empty string when this client's launch requirements are satisfied.
-## Mirrors `McpYamlStrategy.command_launch_error`; the patch-list dialect
-## supports FLAT (and the URL fallback) only.
-static func command_launch_error(client: McpClient, launch: Dictionary) -> String:
- if client.command_shape == McpClient.CommandShape.NONE:
- return ""
- if client.command_shape != McpClient.CommandShape.FLAT:
- return "%s uses a command shape not supported by DeepSeek Harness config yet" % client.display_name
- if not bool(launch.get("ok", false)):
- return str(launch.get("error", "No compatible attach launcher was found."))
- return ""
-
-
-# --- Rendering --------------------------------------------------------------
-
-## Render a full `- insert:` row containing one nested loader entry. Public
-## seam for the dock's manual-instruction text so the pasted YAML matches
-## what Configure would write byte-for-byte.
-static func render_insert_row(entry_id_value: String, config: Dictionary) -> PackedStringArray:
- var lines: PackedStringArray = ["- insert:"]
- lines.append_array(render_nested_entry(entry_id_value, config))
- return lines
-
-
-## Render one nested loader entry (the `- id:` block inside an `insert`
-## list). `base_indent` defaults to the 4-space style used by the shipped
-## patch files; a nested replacement reuses the indent the existing entry was
-## found at so the block sequence keeps a single indentation (#867 review).
-static func render_nested_entry(
- entry_id_value: String, config: Dictionary, base_indent: int = ENTRY_INDENT
-) -> PackedStringArray:
- var lines: PackedStringArray = []
- lines.append("%s- id: %s" % [_indent_str(base_indent), entry_id_value])
- lines.append("%sname: '%s'" % [_indent_str(base_indent + 2), PLUGIN_NAME])
- lines.append("%sconfig:" % _indent_str(base_indent + 2))
- for key in config:
- lines.append_array(_emit_field(String(key), config[key], base_indent + 4))
- return lines
-
-
-## Emit one `key: value` line (or a nested block for Dictionary values) at
-## the given indent. Arrays render in flow style; scalars through the shared
-## YAML emitter so quoting rules stay identical across the two YAML dialects.
-static func _emit_field(key: String, value: Variant, indent: int) -> PackedStringArray:
- var lines: PackedStringArray = []
- var prefix := _indent_str(indent)
- if value is Dictionary:
- lines.append("%s%s:" % [prefix, key])
- for sub_key in value:
- lines.append_array(_emit_field(String(sub_key), value[sub_key], indent + 2))
- elif value is Array or value is PackedStringArray:
- lines.append("%s%s: %s" % [prefix, key, McpYamlStrategy.emit_flow_array(_array_copy(value))])
- else:
- lines.append("%s%s: %s" % [prefix, key, McpYamlStrategy.emit_scalar(value)])
- return lines
-
-
-# --- Line-level parsing (no YAML parser in Godot) ---------------------------
-
-## Split file text into lines, dropping only a single trailing newline so the
-## remainder round-trips byte-for-byte. `text` is never split on the empty
-## string, so an empty file yields an empty array.
-static func _split_lines(text: String) -> PackedStringArray:
- if text.is_empty():
- return PackedStringArray()
- var lines := text.split("\n")
- if lines.size() > 0 and String(lines[lines.size() - 1]).is_empty():
- lines.remove_at(lines.size() - 1)
- return lines
-
-
-## Locate the block owned by our loader entry:
-## require_nested=true — the `- id: ` line inside an `insert`
-## list (indent > 0), plus its indented children.
-## require_nested=false — a plain top-level `- id: ` row (indent
-## 0), plus its indented children.
-## Returns {"start": int, "end": int, "indent": int} (end exclusive) or {}
-## when absent. The header indent is detected dynamically so a hand-written
-## file using non-2-space indentation is still found; children are lines
-## deeper than the header. `end` stops right AFTER the last content line:
-## trailing blank and comment lines between our entry and the next sibling
-## belong to the file, not to our block, so `_replace_lines` / `_remove_lines`
-## (which splice [start, end)) never delete them (#867 review).
-static func _find_entry_block(lines: PackedStringArray, entry_id_value: String, require_nested: bool) -> Dictionary:
- for i in range(lines.size()):
- var indent := _indent_of(lines[i])
- if require_nested and indent <= 0:
- continue
- if not require_nested and indent != 0:
- continue
- var parsed := _parse_entry_header(lines[i])
- if parsed.is_empty() or parsed.get("id") != entry_id_value:
- continue
- ## A nested match must actually live inside an `- insert:` row: a
- ## `- id:` list under some other top-level row (an override row's own
- ## sequence, say) is not a loader entry and is not ours to touch.
- if require_nested and not _under_insert_row(lines, i):
- continue
- var j := i + 1
- var last_content := i
- while j < lines.size():
- var l := lines[j]
- if _is_blank_or_comment(l):
- j += 1
- continue
- if _indent_of(l) <= indent:
- break
- last_content = j
- j += 1
- return {"start": i, "end": last_content + 1, "indent": indent}
- return {}
-
-
-## True when the nearest preceding 0-indent content line above `index` is an
-## `- insert:` row header. Blank and comment lines are skipped; hitting the
-## top of the file without a header means the nested line is orphaned and
-## cannot be a loader entry.
-static func _under_insert_row(lines: PackedStringArray, index: int) -> bool:
- for i in range(index - 1, -1, -1):
- if _is_blank_or_comment(lines[i]):
- continue
- if _indent_of(lines[i]) == 0:
- return lines[i].strip_edges().begins_with("- insert:")
- return false
-
-
-## Parse a `- id: ` header line into {"id": String}. Returns {} for
-## any other line. The value may be plain or single/double-quoted.
-static func _parse_entry_header(line: String) -> Dictionary:
- var stripped := line.strip_edges()
- if not stripped.begins_with("- "):
- return {}
- var body := stripped.substr(2).strip_edges()
- if not body.begins_with("id:"):
- return {}
- var value := body.substr(3).strip_edges()
- if value.begins_with("\"") and value.ends_with("\"") and value.length() >= 2:
- var parsed: Variant = JSON.parse_string(value)
- if parsed is String:
- value = parsed
- elif value.begins_with("'") and value.ends_with("'") and value.length() >= 2:
- value = value.substr(1, value.length() - 2)
- return {"id": value}
-
-
-## Extract the `config:` sub-dict from a found entry block. Fields may nest
-## (env, reconnect); deeper values are parsed with the same scalar coercion
-## the YAML strategy uses. Returns {} when the block has no config block.
-static func _extract_config(lines: PackedStringArray, block: Dictionary) -> Dictionary:
- var start := int(block.get("start", -1))
- var end := int(block.get("end", -1))
- if start < 0:
- return {}
- var config_indent := -1
- var config_start := -1
- for i in range(start, end):
- var stripped := lines[i].strip_edges()
- if _is_blank_or_comment(lines[i]):
- continue
- if stripped == "config:":
- config_indent = _indent_of(lines[i])
- config_start = i
- break
- if config_start < 0:
- return {}
- return _parse_block_lines(lines, config_start + 1, end, config_indent)
-
-
-## Parse sibling `key: value` lines at a fixed minimum indent into a
-## Dictionary. Nested blocks (a key with no inline value) recurse and their
-## child lines are consumed by the recursion — the parent index advances past
-## them so they are never re-parsed as top-level config fields (#867 review).
-## Flow arrays parse back into Arrays via JSON. Quoted and plain scalars
-## strip via the YAML strategy's shared `coerce_scalar`.
-static func _parse_block_lines(lines: PackedStringArray, start: int, end: int, min_indent: int) -> Dictionary:
- var out: Dictionary = {}
- var i := start
- while i < end:
- var l := lines[i]
- if _is_blank_or_comment(l):
- i += 1
- continue
- var indent := _indent_of(l)
- if indent <= min_indent:
- break
- var stripped := l.strip_edges()
- var colon := stripped.find(":")
- if colon < 0:
- i += 1
- continue
- var key := stripped.substr(0, colon).strip_edges()
- var value := stripped.substr(colon + 1).strip_edges()
- if value.is_empty():
- ## Nested block: find where its children end (a line at or above
- ## the block key's indent), parse only that span, and jump past it.
- var child_end := i + 1
- while child_end < end:
- if not _is_blank_or_comment(lines[child_end]) and _indent_of(lines[child_end]) <= indent:
- break
- child_end += 1
- out[key] = _parse_block_lines(lines, i + 1, child_end, indent)
- i = child_end
- else:
- ## Shared with the YAML strategy so the two dialects can never
- ## drift on quoting/typing rules (#867 review).
- out[key] = McpYamlStrategy.coerce_scalar(value)
- i += 1
- return out
-
-
-# --- Rewrites ---------------------------------------------------------------
-
-## Append a new `- insert:` row at the end of the file. A blank separator
-## line keeps rows visually distinct; the file always ends with a newline.
-static func _append_row(lines: PackedStringArray, nested_lines: PackedStringArray) -> String:
- var parts: Array[String] = []
- for l in lines:
- parts.append(l)
- ## Collapse trailing blank lines so the appended row is not orphaned
- ## below stray empties; one separator blank keeps rows readable.
- while parts.size() > 0 and String(parts[parts.size() - 1]).strip_edges().is_empty():
- parts.remove_at(parts.size() - 1)
- if parts.size() > 0:
- parts.append("")
- parts.append("- insert:")
- for l in nested_lines:
- parts.append(l)
- return _join_lines(parts)
-
-
-## Replace the lines owned by our entry. When the found block is a plain
-## top-level row (indent 0 header), the whole row — including its children —
-## is replaced by the insert form. When it is a nested block inside an
-## insert row, only that block's lines are replaced, leaving sibling entries
-## and comments in the same row byte-for-byte intact.
-static func _replace_lines(lines: PackedStringArray, block: Dictionary, nested_lines: PackedStringArray) -> String:
- var start := int(block.get("start", -1))
- var end := int(block.get("end", -1))
- var parts: Array[String] = []
- for i in range(lines.size()):
- if i == start:
- ## A nested block keeps the surrounding `- insert:` row, so only
- ## the nested entry lines (indent > 0) are spliced in.
- if _indent_of(lines[start]) > 0:
- for l in nested_lines:
- parts.append(l)
- else:
- parts.append("- insert:")
- for l in nested_lines:
- parts.append(l)
- if i >= start and i < end:
- continue
- parts.append(lines[i])
- return _join_lines(parts)
-
-
-## Remove our entry. A nested block is spliced out; if its enclosing insert
-## row is left without any nested entries (only the bare `- insert:` header,
-## blanks, and comments), the row header goes too so the file stays a valid
-## patch list. A plain row for our id is removed wholesale.
-static func _remove_lines(lines: PackedStringArray, block: Dictionary) -> String:
- var start := int(block.get("start", -1))
- var end := int(block.get("end", -1))
- var nested := _indent_of(lines[start]) > 0
- var parts: Array[String] = []
- if nested:
- ## Find the enclosing `- insert:` row header: the nearest preceding
- ## 0-indent `- ` line.
- var row_start := -1
- for i in range(start - 1, -1, -1):
- if _indent_of(lines[i]) == 0 and lines[i].strip_edges().begins_with("- "):
- row_start = i
- break
- ## Row end: first 0-indent non-blank/comment line after the block.
- var row_end := lines.size()
- for i in range(end, lines.size()):
- if _indent_of(lines[i]) == 0 and not _is_blank_or_comment(lines[i]):
- row_end = i
- break
- var has_other_entries := false
- for i in range(row_start + 1, row_end):
- if i >= start and i < end:
- continue
- if _is_blank_or_comment(lines[i]):
- continue
- if _indent_of(lines[i]) > 0 and lines[i].strip_edges().begins_with("- "):
- has_other_entries = true
- break
- if has_other_entries or row_start < 0:
- ## Only our block goes; sibling entries and any trailing
- ## comments in the same row survive.
- for i in range(lines.size()):
- if i >= start and i < end:
- continue
- parts.append(lines[i])
- else:
- ## The row held only our entry: drop the `- insert:` header and
- ## our block, but keep trailing blank/comment lines — a comment
- ## the user wrote above the next row must survive (#867 review).
- for i in range(lines.size()):
- if i >= row_start and i < end:
- continue
- parts.append(lines[i])
- parts = _trim_trailing_blanks(parts)
- else:
- ## Plain top-level row for our id: drop the whole block.
- for i in range(lines.size()):
- if i >= start and i < end:
- continue
- parts.append(lines[i])
- parts = _trim_trailing_blanks(parts)
- return _join_lines(parts)
-
-
-## Remove trailing blank lines left behind by a removal.
-static func _trim_trailing_blanks(parts: Array[String]) -> Array[String]:
- while parts.size() > 0 and String(parts[parts.size() - 1]).strip_edges().is_empty():
- parts.remove_at(parts.size() - 1)
- return parts
-
-
-## True when the text holds no patch rows at all — blank lines and comments
-## only. Such a file cannot be written back (see `remove`), because dsh's
-## patch parser rejects a non-array file at boot.
-static func _is_blank_or_comment_only(text: String) -> bool:
- for line in text.split("\n"):
- var stripped := line.strip_edges()
- if not stripped.is_empty() and not stripped.begins_with("#"):
- return false
- return true
-
-
-## When the document's only content line is the empty flow sequence `[]`,
-## return the text with that one line removed — blanks and comments stay,
-## because they belong to the user. Any other document comes back unchanged;
-## a `[]` mixed with real rows is left for `_is_patch_sequence` to refuse.
-static func _strip_empty_flow_sequence(text: String) -> String:
- var lines := _split_lines(text)
- var bracket_line := -1
- for i in range(lines.size()):
- if _is_blank_or_comment(lines[i]):
- continue
- if bracket_line < 0 and lines[i].strip_edges() == "[]":
- bracket_line = i
- continue
- return text
- if bracket_line < 0:
- return text
- var parts: Array[String] = []
- for i in range(lines.size()):
- if i != bracket_line:
- parts.append(lines[i])
- if parts.is_empty():
- return ""
- return _join_lines(parts)
-
-
-## True when the text is a valid loader patch list (or empty/comment-only /
-## the empty flow sequence `[]`, which all mean "no layer"). Anything else —
-## a top-level mapping or scalar — would make dsh fail boot, so Configure
-## refuses to write into it instead of producing a broken file (#867 review).
-## Only 0-indent content lines define the top-level structure: they must all
-## be `- ` rows. Deeper lines are row children and are not inspected. `[]` is
-## valid only as the COMPLETE document — appending rows after the empty flow
-## sequence would produce malformed YAML, so a mixed `[]` + rows file fails
-## the check too.
-static func _is_patch_sequence(text: String) -> bool:
- if text.strip_edges().is_empty():
- return true
- var saw_brackets := false
- var content_count := 0
- for line in text.split("\n"):
- var stripped := line.strip_edges()
- if stripped.is_empty() or stripped.begins_with("#"):
- continue
- content_count += 1
- if stripped == "[]":
- saw_brackets = true
- continue
- if _indent_of(line) == 0 and not stripped.begins_with("- "):
- return false
- if saw_brackets and content_count > 1:
- return false
- return true
-
-
-static func _join_lines(parts: Array) -> String:
- if parts.is_empty():
- return ""
- var lines: Array[String] = []
- for p in parts:
- lines.append(String(p))
- return "\n".join(lines) + "\n"
-
-
-# --- Small helpers ----------------------------------------------------------
-
-static func _indent_str(n: int) -> String:
- var out := ""
- for i in range(n):
- out += " "
- return out
-
-
-static func _indent_of(line: String) -> int:
- var n := 0
- while n < line.length() and (line[n] == " " or line[n] == "\t"):
- n += 1
- return n
-
-
-static func _is_blank_or_comment(line: String) -> bool:
- var stripped := line.strip_edges()
- return stripped.is_empty() or stripped.begins_with("#")
-
-
-static func _array_copy(value: Variant) -> Array:
- if value is Array:
- return (value as Array).duplicate(true)
- if value is PackedStringArray:
- return McpClient._array_from_packed(value)
- return []
-
-
-static func _arrays_equal(left: Variant, right: Variant) -> bool:
- if not (left is Array or left is PackedStringArray):
- return false
- if not (right is Array or right is PackedStringArray):
- return false
- var left_array := _array_copy(left)
- var right_array := _array_copy(right)
- if left_array.size() != right_array.size():
- return false
- for i in range(left_array.size()):
- if left_array[i] != right_array[i]:
- return false
- return true
-
-
-## Returns {"ok": true, "data": String} when the file is absent or readable,
-## and {"ok": false, "error": String} when unreadable. Callers must NOT fall
-## back to an empty string on the error path — doing so blows away the user's
-## other patch rows on the next write.
-static func _read(path: String) -> Dictionary:
- if not FileAccess.file_exists(path):
- return {"ok": true, "data": ""}
- var f := FileAccess.open(path, FileAccess.READ)
- if f == null:
- var err := FileAccess.get_open_error()
- return {"ok": false, "error": "could not open for reading (%s)" % error_string(err)}
- var t := f.get_as_text()
- f.close()
- return {"ok": true, "data": t}
diff --git a/addons/godot_ai/clients/_dsh_strategy.gd.uid b/addons/godot_ai/clients/_dsh_strategy.gd.uid
deleted file mode 100644
index 36703f4..0000000
--- a/addons/godot_ai/clients/_dsh_strategy.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ch0brn7swb6qx
diff --git a/addons/godot_ai/clients/_json_strategy.gd b/addons/godot_ai/clients/_json_strategy.gd
deleted file mode 100644
index e7f9a50..0000000
--- a/addons/godot_ai/clients/_json_strategy.gd
+++ /dev/null
@@ -1,1114 +0,0 @@
-@tool
-class_name McpJsonStrategy
-extends RefCounted
-
-## Read–merge–write strategy for JSON-backed MCP clients.
-## All knobs come from the McpClient descriptor as plain data — no Callables.
-## See `_base.gd` for why descriptors are data-only.
-
-
-static func configure(
- client: McpClient,
- server_name: String,
- server_url: String,
- launch: Dictionary = {},
- project_roots: PackedStringArray = PackedStringArray(),
-) -> Dictionary:
- if _uses_merge_tiers(client):
- return _configure_merged(client, server_name, server_url, launch, project_roots)
- var resolution := client.resolved_config_path_details()
- var path := str(resolution.get("path", ""))
- var path_error := str(resolution.get("error", ""))
- if not path_error.is_empty():
- return {"status": "error", "message": path_error}
- if path.is_empty():
- return {"status": "error", "message": "Could not resolve config path for %s on this OS" % client.display_name}
-
- var seed_path := str(resolution.get("seed_path", ""))
- var read_path := seed_path if not FileAccess.file_exists(path) and not seed_path.is_empty() else path
- var read := _read_or_init(read_path)
- if not read["ok"]:
- return {"status": "error", "message": "Refusing to overwrite %s: %s. Fix or move the file, then re-run Configure." % [read_path, read["error"]]}
- var launch_error := command_launch_error(client, launch)
- if not launch_error.is_empty():
- return {"status": "error", "message": launch_error}
- var config: Dictionary = read["data"]
- var holder := _ensure_path(config, select_server_key_path(config, client))
- ## Pass the existing entry through so `build_entry` can preserve user-mutable
- ## state (auto-approval lists, `disabled` toggles) instead of resetting it
- ## to descriptor defaults on every Configure click. See `entry_initial_fields`
- ## docs in `_base.gd`.
- var existing: Variant = holder.get(server_name, null)
- holder[server_name] = build_entry(client, server_url, existing, launch)
-
- # F5: refuse to re-serialize a file whose parsed integers above 2^53 would
- # lose precision. Editing by hand keeps those values intact.
- if _has_lossy_numbers(config):
- return {"status": "error", "message": "Refusing to rewrite %s: contains integers above 2^53 that Godot's JSON parser would re-emit imprecise. Edit the file by hand." % path}
- if not McpAtomicWrite.write(path, JSON.stringify(_narrow_integral_numbers(config), "\t", false)):
- return {"status": "error", "message": "Cannot write to %s" % path}
- return {"status": "ok", "message": McpClient.configured_message(client, server_url)}
-
-## Pi-style clients merge several global config files. Update the effective
-## highest-precedence definition; fail closed when a project override exists
-## because Pi's external working directory cannot be inferred safely here.
-static func _configure_merged(
- client: McpClient,
- server_name: String,
- server_url: String,
- launch: Dictionary,
- project_roots: PackedStringArray,
-) -> Dictionary:
- var launch_error := command_launch_error(client, launch)
- if not launch_error.is_empty():
- return {"status": "error", "message": launch_error}
- var project := _load_project_definitions(client, server_name, project_roots)
- if not project.get("ok", false):
- return {"status": "error", "message": str(project.get("error", "Cannot inspect project config tiers"))}
- var project_tiers: Array = project.get("tiers", [])
- if not project_tiers.is_empty():
- return {"status": "error", "message": _project_override_message(project_tiers, "update or remove", client.display_name, server_name)}
- var loaded := _load_merge_tiers(client)
- if not loaded.get("ok", false):
- return {"status": "error", "message": str(loaded.get("error", "Cannot read merged config tiers"))}
- var tiers: Array = loaded.get("tiers", [])
- if tiers.is_empty():
- return {"status": "error", "message": "Could not resolve config path for %s on this OS" % client.display_name}
- var target_index := 0
- for index in range(tiers.size()):
- var config: Dictionary = tiers[index]["data"]
- var holder := _walk_path(config, select_server_key_path(config, client))
- if holder is Dictionary and holder.has(server_name):
- target_index = index
- var tier: Dictionary = tiers[target_index]
- var config: Dictionary = tier["data"]
- var holder := _ensure_path(config, select_server_key_path(config, client))
- var existing: Variant = holder.get(server_name, null)
- holder[server_name] = build_entry(client, server_url, existing, launch)
- var path := str(tier["path"])
- # F5: refuse to re-serialize a tier whose parsed integers above 2^53 would
- # lose precision. The same check guards the simple `configure` path.
- if _has_lossy_numbers(config):
- return {"status": "error", "message": "Refusing to rewrite %s: contains integers above 2^53 that Godot's JSON parser would re-emit imprecise. Edit the file by hand." % path}
- if not McpAtomicWrite.write(path, JSON.stringify(_narrow_integral_numbers(config), "\t", false)):
- return {"status": "error", "message": "Cannot write to %s" % path}
- # Codex F1 caveat: when we wrote a global tier but couldn't find any project
- # override in the roots we probed AND the user hasn't told us where Pi (or any
- # external cwd client) actually runs from, we can't prove this write is the
- # effective config. Surface the actionable hint instead of silently passing.
- # The early `not project_tiers.is_empty()` return above already covers the
- # "project override exists in probed roots" case (we fail closed there), so
- # reaching this point with `project_tiers.is_empty()` means: no probed
- # override was found.
- var message := McpClient.configured_message(client, server_url)
- var external_cwd := str(McpClientConfigurator._editor_setting_lookup(McpClientConfigurator.SETTING_EXTERNAL_CLIENT_CWD))
- if external_cwd.is_empty() and project_tiers.is_empty():
- message += " If %s is launched from a cwd this editor cannot see, the effective entry may still be in a project-tier file — set godot_ai/external_client_cwd to that cwd to verify." % client.display_name
- return {"status": "ok", "message": message}
-
-
-static func check_status(
- client: McpClient,
- server_name: String,
- server_url: String,
- launch: Dictionary = {},
- project_roots: PackedStringArray = PackedStringArray(),
-) -> McpClient.Status:
- return check_status_details(client, server_name, server_url, launch, project_roots).get("status", McpClient.Status.NOT_CONFIGURED)
-
-
-## Detailed variant feeding the dock's error_msg plumbing (#711): a config
-## file that EXISTS but can't be read or parsed is Status.ERROR carrying the
-## read/parse error, not NOT_CONFIGURED — the write path refuses to touch
-## such a file (see `_read_or_init`), so the status dot must say "broken
-## file", not "click Configure".
-static func check_status_details(
- client: McpClient,
- server_name: String,
- server_url: String,
- launch: Dictionary = {},
- project_roots: PackedStringArray = PackedStringArray(),
-) -> Dictionary:
- if _uses_merge_tiers(client):
- return _check_status_merged(client, server_name, server_url, launch, project_roots)
- var resolution := client.resolved_config_path_details()
- var path := str(resolution.get("path", ""))
- var path_error := str(resolution.get("error", ""))
- if not path_error.is_empty():
- return {"status": McpClient.Status.ERROR, "error_msg": path_error}
- if path.is_empty() or not FileAccess.file_exists(path):
- return {"status": McpClient.Status.NOT_CONFIGURED, "error_msg": ""}
- var read := _read_or_init(path, _status_allows_comments(client))
- if not read["ok"]:
- return {"status": McpClient.Status.ERROR, "error_msg": String(read["error"])}
- var config: Dictionary = read["data"]
- var holder := _walk_path(config, select_server_key_path(config, client))
- if not (holder is Dictionary) or not holder.has(server_name):
- return {"status": McpClient.Status.NOT_CONFIGURED, "error_msg": ""}
- return _entry_status_details(client, holder[server_name], server_url, launch)
-
-
-## Verify the effective last definition after applying the client's merge order.
-static func _check_status_merged(
- client: McpClient,
- server_name: String,
- server_url: String,
- launch: Dictionary,
- project_roots: PackedStringArray,
-) -> Dictionary:
- var allow_comments := _status_allows_comments(client)
- var loaded := _load_merge_tiers(client, allow_comments)
- if not loaded.get("ok", false):
- return {"status": McpClient.Status.ERROR, "error_msg": str(loaded.get("error", "Cannot read merged config tiers"))}
- var effective: Variant = null
- for tier in loaded.get("tiers", []):
- var config: Dictionary = tier["data"]
- var holder := _walk_path(config, select_server_key_path(config, client))
- if holder is Dictionary and holder.has(server_name):
- effective = holder[server_name]
- var project := _load_project_definitions(client, server_name, project_roots, allow_comments)
- if not project.get("ok", false):
- return {"status": McpClient.Status.ERROR, "error_msg": str(project.get("error", "Cannot inspect project config tiers"))}
- var project_tiers: Array = project.get("tiers", [])
- if not project_tiers.is_empty():
- # Last-definition-wins mirrors the global-tier fold above and how
- # pi-codemode-mcp merges project tiers on disk. Earlier tiers are dead
- # once a later one defines the same server, so an early-stale entry
- # doesn't make Pi's effective config drift (codex-review finding F2).
- # Pass `[latest]` to `_project_override_message` so the error names only
- # the file the user actually has to edit.
- var latest: Dictionary = project_tiers[project_tiers.size() - 1]
- var details := _entry_status_details(client, latest["entry"], server_url, launch)
- if details.get("status") != McpClient.Status.CONFIGURED:
- ## Keep `owned` from the effective entry: the post-update migration
- ## decides from it whether this mismatch is ours to repin.
- var mismatch := details.duplicate()
- mismatch["status"] = McpClient.Status.CONFIGURED_MISMATCH
- mismatch["error_msg"] = _project_override_message([latest], "update or remove", client.display_name, server_name)
- return mismatch
- return {"status": McpClient.Status.CONFIGURED, "error_msg": ""}
- if effective == null:
- return {"status": McpClient.Status.NOT_CONFIGURED, "error_msg": ""}
- return _entry_status_details(client, effective, server_url, launch)
-
-
-static func _entry_status_details(
- client: McpClient,
- entry: Variant,
- server_url: String,
- launch: Dictionary,
-) -> Dictionary:
- if not (entry is Dictionary):
- return {"status": McpClient.Status.NOT_CONFIGURED, "error_msg": ""}
- var launch_error := command_launch_error(client, launch)
- if not launch_error.is_empty():
- return {"status": McpClient.Status.ERROR, "error_msg": launch_error}
- if verify_entry(client, entry, server_url, launch):
- return {"status": McpClient.Status.CONFIGURED, "error_msg": ""}
- return {
- "status": McpClient.Status.CONFIGURED_MISMATCH,
- "error_msg": "",
- "owned": McpClient.launch_values_mention_godot_ai(McpClient.entry_launch_values(entry)),
- }
-
-
-static func remove(
- client: McpClient,
- server_name: String,
- project_roots: PackedStringArray = PackedStringArray(),
-) -> Dictionary:
- if _uses_merge_tiers(client):
- return _remove_merged(client, server_name, project_roots)
- var resolution := client.resolved_config_path_details()
- var path := str(resolution.get("path", ""))
- var path_error := str(resolution.get("error", ""))
- if not path_error.is_empty():
- return {"status": "error", "message": path_error}
- if path.is_empty() or not FileAccess.file_exists(path):
- return {"status": "ok", "message": "Not configured"}
- # Token-preserving removal (F5): pull the original file text via
- # `_read_file_text` and edit only the entry's bytes — keeps every other
- # byte, including integers above 2^53, byte-for-byte identical to what
- # the user wrote. The parsed-dict round-trip path is gone.
- var file_read := _read_file_text(path)
- if not file_read.get("ok", false):
- return {"status": "error", "message": "Refusing to rewrite %s: %s." % [path, file_read.get("error", "")]}
- var config: Dictionary = file_read.get("data", {})
- var key_path := select_server_key_path(config, client)
- var original_text: String = str(file_read.get("original_text", ""))
- var updated: String = _text_remove_server_entry(original_text, key_path, server_name)
- if updated == original_text:
- return {"status": "ok", "message": "Not configured"}
- if not McpAtomicWrite.write(path, updated):
- return {"status": "error", "message": "Cannot write to %s" % path}
- return {"status": "ok", "message": "%s configuration removed" % client.display_name}
-
-
-static func _remove_merged(
- client: McpClient, server_name: String, project_roots: PackedStringArray
-) -> Dictionary:
- var project := _load_project_definitions(client, server_name, project_roots)
- if not project.get("ok", false):
- return {"status": "error", "message": str(project.get("error", "Cannot inspect project config tiers"))}
- var project_tiers: Array = project.get("tiers", [])
- if not project_tiers.is_empty():
- return {"status": "error", "message": _project_override_message(project_tiers, "remove", client.display_name, server_name)}
- var loaded := _load_merge_tiers(client)
- if not loaded.get("ok", false):
- return {"status": "error", "message": str(loaded.get("error", "Cannot read merged config tiers"))}
- var writes: Array[Dictionary] = []
- for tier in loaded.get("tiers", []):
- if not tier.get("exists", false):
- continue
- var key_path := select_server_key_path(tier["data"], client)
- var original: String = str(tier.get("original_text", ""))
- # Token-preserving removal (F5): edit `original` textually instead of
- # re-serialising the parsed config. This preserves every byte outside
- # the entry block, so unrelated integers (including those above the
- # IEEE-754 exact-int range that `_narrow_integral_numbers` cannot
- # round-trip) keep their original literals.
- var updated: String = _text_remove_server_entry(original, key_path, server_name)
- if updated == original:
- continue # entry not in this tier; skip
- writes.append({
- "path": tier["path"],
- "text": updated,
- "original_text": original,
- })
- if writes.is_empty():
- return {"status": "ok", "message": "Not configured"}
- var written := _write_transaction(writes)
- if not written.get("ok", false):
- return {"status": "error", "message": written.get("error", "Cannot remove merged configuration")}
- return {"status": "ok", "message": "%s configuration removed" % client.display_name}
-
-
-## Synthesize the entry dict the strategy writes under
-## `server_key_path[server_name]`. Both URL and command entries deep-copy the
-## existing dict before overwriting strategy-owned fields, preserving unknown
-## client additions as well as descriptor-documented user fields.
-static func build_entry(
- client: McpClient,
- server_url: String,
- existing: Variant = null,
- launch: Dictionary = {},
-) -> Dictionary:
- if _is_supported_command_shape(client.command_shape):
- var command_entry: Dictionary = (existing as Dictionary).duplicate(true) if existing is Dictionary else {}
- if client.command_shape == McpClient.CommandShape.COMMAND_ARRAY:
- ## OpenCode-style: the entry's `command` field IS the argv array.
- ## A stale sibling `args` from a FLAT-style hand edit would be
- ## ambiguous next to it, so it is strategy-owned and removed.
- command_entry["command"] = _launch_argv(launch)
- command_entry.erase("args")
- else:
- command_entry["command"] = str(launch.get("command", ""))
- command_entry["args"] = _array_copy(launch.get("args", []))
- if not client.command_transport_key.is_empty():
- command_entry[client.command_transport_key] = client.command_transport_value
- for key in client.command_initial_fields:
- if not command_entry.has(key):
- command_entry[key] = client.command_initial_fields[key]
- for key in client.command_legacy_keys:
- command_entry.erase(String(key))
- _remove_legacy_env_keys(command_entry, client.command_env_legacy_keys)
- return command_entry
- if client.command_shape != McpClient.CommandShape.NONE:
- return {}
- return build_url_entry(client, server_url, existing)
-
-
-static func build_url_entry(client: McpClient, server_url: String, existing: Variant = null) -> Dictionary:
- var entry: Dictionary = (existing as Dictionary).duplicate(true) if existing is Dictionary else {}
- entry[client.entry_url_field] = server_url
- for k in client.entry_extra_fields:
- entry[k] = client.entry_extra_fields[k]
- for k in client.entry_initial_fields:
- if not entry.has(k):
- entry[k] = client.entry_initial_fields[k]
- return entry
-
-
-## Default verifier for a stored entry. Command entries must match every
-## launch-affecting value exactly; legacy URL or env keys are migration drift.
-## For URL clients, assert `entry[entry_url_field] == url` AND every
-## key in `entry_extra_fields` matches verbatim. Type-pinning for Cline /
-## Roo / Kilo (`type: "streamable-http"` etc.) falls out of this — pre-fix
-## entries that lack the type field fail verification and surface as drift.
-static func verify_entry(
- client: McpClient,
- entry: Dictionary,
- server_url: String,
- launch: Dictionary = {},
-) -> bool:
- if client.command_shape != McpClient.CommandShape.NONE:
- if not _is_supported_command_shape(client.command_shape) or not bool(launch.get("ok", false)):
- return false
- for key in client.command_legacy_keys:
- if entry.has(String(key)):
- return false
- var env = entry.get("env", null)
- if env is Dictionary:
- for key in client.command_env_legacy_keys:
- if env.has(String(key)):
- return false
- if client.command_shape == McpClient.CommandShape.COMMAND_ARRAY:
- if not _arrays_equal(entry.get("command", null), _launch_argv(launch)):
- return false
- if entry.has("args"):
- return false
- else:
- if entry.get("command") != launch.get("command"):
- return false
- if not _arrays_equal(entry.get("args", null), launch.get("args", null)):
- return false
- if not client.command_transport_key.is_empty():
- if not entry.has(client.command_transport_key):
- return false
- if entry.get(client.command_transport_key) != client.command_transport_value:
- return false
- return true
- if entry.get(client.entry_url_field, "") != server_url:
- return false
- for k in client.entry_extra_fields:
- if entry.get(k) != client.entry_extra_fields[k]:
- return false
- return true
-
-
-static func command_launch_error(client: McpClient, launch: Dictionary) -> String:
- if client.command_shape == McpClient.CommandShape.NONE:
- return ""
- if not _is_supported_command_shape(client.command_shape):
- return "%s uses a command shape not supported by JSON yet" % client.display_name
- if not bool(launch.get("ok", false)):
- return str(launch.get("error", "No compatible attach launcher was found."))
- return ""
-
-
-static func _is_supported_command_shape(shape: McpClient.CommandShape) -> bool:
- return shape == McpClient.CommandShape.FLAT or shape == McpClient.CommandShape.COMMAND_ARRAY
-
-
-## The full launch argv as one array: launcher path followed by every arg.
-static func _launch_argv(launch: Dictionary) -> Array:
- var argv: Array = [str(launch.get("command", ""))]
- argv.append_array(_array_copy(launch.get("args", [])))
- return argv
-
-
-static func _remove_legacy_env_keys(entry: Dictionary, legacy_keys: PackedStringArray) -> void:
- if legacy_keys.is_empty():
- return
- var existing_env = entry.get("env", null)
- if not (existing_env is Dictionary):
- return
- var env: Dictionary = (existing_env as Dictionary).duplicate(true)
- for key in legacy_keys:
- env.erase(String(key))
- if env.is_empty():
- entry.erase("env")
- else:
- entry["env"] = env
-
-
-static func _array_copy(value: Variant) -> Array:
- if value is Array:
- return (value as Array).duplicate(true)
- if value is PackedStringArray:
- return McpClient._array_from_packed(value)
- return []
-
-
-static func _arrays_equal(left: Variant, right: Variant) -> bool:
- if not (left is Array or left is PackedStringArray):
- return false
- if not (right is Array or right is PackedStringArray):
- return false
- var left_array := _array_copy(left)
- var right_array := _array_copy(right)
- if left_array.size() != right_array.size():
- return false
- for i in range(left_array.size()):
- if left_array[i] != right_array[i]:
- return false
- return true
-
-
-## Read a config file once and parse the captured text. Returns
-## {"exists": bool, "ok": bool, "data": Dictionary, "original_text": String}
-## when the file is absent or parses cleanly, and
-## {"exists": true, "ok": false, "error": String, "original_text": String}
-## when the file exists with non-empty content we cannot safely round-trip.
-## Callers must NOT treat the error path as an empty config — doing so blows
-## away the user's other MCP entries on the next write. The `original_text`
-## is the exact captured source so transactional rollback can restore
-## byte-for-byte; the UTF-8 BOM is stripped only from the parsing copy.
-## `allow_comments` also strips JSONC comments from the parsing copy; only
-## read-only callers pass it (see `_status_allows_comments`).
-static func _read_file_text(path: String, allow_comments: bool = false) -> Dictionary:
- if not FileAccess.file_exists(path):
- return {"exists": false, "ok": true, "data": {}, "original_text": ""}
- var file := FileAccess.open(path, FileAccess.READ)
- if file == null:
- var open_err := FileAccess.get_open_error()
- return {"exists": true, "ok": false, "error": "could not open for reading (error %d)" % open_err, "original_text": ""}
- # Read bytes, not `get_as_text()`. Godot's UTF-8 decoder (both `get_as_text`
- # and `get_string_from_utf8`) silently consumes a leading EF BB BF, so a
- # text-only read dropped the BOM from `original_text` and the next Remove
- # wrote it back out of the user's file — a byte mutation outside the entry
- # we were asked to touch. Detect the marker on the raw buffer and restore
- # U+FEFF when the decoder ate it.
- var buf := file.get_buffer(file.get_length())
- file.close()
- var had_bom := buf.size() >= 3 and buf[0] == 0xEF and buf[1] == 0xBB and buf[2] == 0xBF
- var content := buf.get_string_from_utf8()
- if had_bom and not content.begins_with(""):
- content = "" + content
- # Everything downstream of the BOM parses and measures the body: JSON.parse
- # rejects a leading U+FEFF outright, and a BOM-only file is empty, not
- # broken. `original_text` keeps the marker so writes round-trip it.
- var body := content.substr(1) if content.begins_with("") else content
- if body.strip_edges().is_empty():
- return {"exists": true, "ok": true, "data": {}, "original_text": content}
- var parse_copy := body
- if allow_comments:
- # Parse copy only — `original_text` stays byte-for-byte, and an
- # unterminated block comment fails closed rather than letting the
- # leftover source parse as valid JSON.
- var stripped := _strip_jsonc(parse_copy)
- if not stripped.get("ok", false):
- var strip_msg := str(stripped.get("error", "invalid JSONC"))
- push_warning("MCP | %s in %s" % [strip_msg, path])
- return {"exists": true, "ok": false, "error": strip_msg, "original_text": content}
- parse_copy = str(stripped.get("text", ""))
- var json := JSON.new()
- if json.parse(parse_copy) != OK:
- var msg := "JSON parse error on line %d: %s" % [json.get_error_line(), json.get_error_message()]
- push_warning("MCP | %s in %s" % [msg, path])
- return {"exists": true, "ok": false, "error": msg, "original_text": content}
- if not (json.data is Dictionary):
- return {"exists": true, "ok": false, "error": "top-level value is %s, expected object" % type_string(typeof(json.data)), "original_text": content}
- return {"exists": true, "ok": true, "data": json.data, "original_text": content}
-
-
-## Read-only by construction: requiring both flags means no Configure/Remove
-## can parse a file it would then re-serialize without the discarded comments.
-## Re-enabling automatic edits restores the parse error instead of a lossy write.
-static func _status_allows_comments(client: McpClient) -> bool:
- return client.config_allows_comments and not client.automatic_config_edits
-
-
-## Strip `//` and `/* */` comments, leaving sequences inside JSON strings
-## intact. Parse copies only.
-## Returns `{"ok": true, "text": String}` or `{"ok": false, "error": String}`.
-static func _strip_jsonc(text: String) -> Dictionary:
- var parts := PackedStringArray()
- var i := 0
- var n := text.length()
- var in_string := false
- var escape := false
- var run_start := 0
- while i < n:
- var c := text[i]
- if in_string:
- if escape:
- escape = false
- elif c == "\\":
- escape = true
- elif c == '"':
- in_string = false
- i += 1
- continue
- if c == '"':
- in_string = true
- i += 1
- continue
- var skipped := _skip_jsonc_comment_at(text, i)
- if skipped < 0:
- return {"ok": false, "error": "unterminated block comment"}
- if skipped != i:
- if i > run_start:
- parts.append(text.substr(run_start, i - run_start))
- # Re-emit newlines so JSON.parse error lines still match the source.
- var emitted_nl := false
- for j in range(i, skipped):
- if text[j] == "\n":
- parts.append("\n")
- emitted_nl = true
- # A comment must not glue its neighbours (`tru/* x */e` is not
- # `true`). A newline already splits them; otherwise use a space.
- if not emitted_nl:
- parts.append(" ")
- i = skipped
- run_start = i
- continue
- i += 1
- if in_string:
- return {"ok": false, "error": "unterminated string"}
- if n > run_start:
- parts.append(text.substr(run_start, n - run_start))
- return {"ok": true, "text": "".join(parts)}
-
-
-## Skip a JSONC comment starting at `i`. Returns `i` unchanged when `i` is not
-## a comment opener, the index after a consumed comment, or -1 when a block
-## comment runs to EOF without `*/`.
-static func _skip_jsonc_comment_at(text: String, i: int) -> int:
- var n := text.length()
- if i + 1 >= n or text[i] != "/":
- return i
- if text[i + 1] == "/":
- i += 2
- # `get_string_from_utf8()` keeps CR, so a CR-only file never presents a `\n` and
- # a `\n`-only terminator would swallow the rest of the file.
- while i < n and text[i] != "\n" and text[i] != "\r":
- i += 1
- return i
- if text[i + 1] == "*":
- i += 2
- while i + 1 < n:
- if text[i] == "*" and text[i + 1] == "/":
- return i + 2
- i += 1
- return -1
- return i
-
-
-## Returns {"ok": true, "data": Dictionary} when the file is absent or parses
-## cleanly, and {"ok": false, "error": String} when the file exists with
-## non-empty content we cannot safely round-trip. Callers must NOT fall back
-## to an empty dict on the error path — doing so blows away the user's other
-## MCP entries on the next write.
-static func _read_or_init(path: String, allow_comments: bool = false) -> Dictionary:
- var read := _read_file_text(path, allow_comments)
- var result: Dictionary = {"ok": read.get("ok", false), "data": read.get("data", {})}
- if not result.get("ok", false):
- result["error"] = read.get("error", "")
- return result
-
-
-## Walk a key path, creating intermediate Dicts as needed. Returns the leaf Dict.
-static func _ensure_path(root: Dictionary, key_path: PackedStringArray) -> Dictionary:
- var cur := root
- for key in key_path:
- var next = cur.get(key)
- if not (next is Dictionary):
- next = {}
- cur[key] = next
- cur = next
- return cur
-
-
-## Walk a key path, returning the leaf Dict if all hops exist; else null.
-static func _walk_path(root: Dictionary, key_path: PackedStringArray) -> Variant:
- var cur: Variant = root
- for key in key_path:
- if not (cur is Dictionary) or not cur.has(key):
- return null
- cur = cur[key]
- return cur
-
-
-## Match clients that accept multiple server-map keys without creating a
-## higher-precedence canonical map that shadows an existing legacy map. A
-## non-null scalar still wins, matching nullish-coalescing parsers; Configure's
-## `_ensure_path` then repairs it using the strategy's existing behavior.
-static func select_server_key_path(root: Dictionary, client: McpClient) -> PackedStringArray:
- var candidates: Array[PackedStringArray] = [client.server_key_path]
- # Dynamic access keeps mixed-snapshot self-updates parse-safe when a new
- # strategy is briefly loaded with an older McpClient base (#398/#736).
- var aliases = client.get("server_key_path_aliases")
- if aliases is Array:
- for alias in aliases:
- if alias is PackedStringArray:
- candidates.append(alias)
- for key_path in candidates:
- if _walk_path(root, key_path) != null:
- return key_path
- return client.server_key_path
-
-
-static func _uses_merge_tiers(client: McpClient) -> bool:
- var templates = client.get("config_merge_path_templates")
- return templates is Dictionary and not templates.is_empty()
-
-
-## Resolve the global config tiers in the client's documented merge order.
-static func _merge_paths(client: McpClient) -> PackedStringArray:
- var result := PackedStringArray()
- var templates = client.get("config_merge_path_templates")
- if templates is Dictionary:
- var platform_key := McpPathTemplate.platform_key(templates)
- if not platform_key.is_empty():
- var raw_paths: Variant = templates.get(platform_key, [])
- if raw_paths is Array or raw_paths is PackedStringArray:
- for template in raw_paths:
- var path := McpPathTemplate.expand(str(template)).simplify_path()
- if not path.is_empty() and not result.has(path):
- result.append(path)
- return result
-
-
-## Pi's project tiers are relative to Pi's process cwd, not Godot's. Inspect
-## plausible roots only to detect overrides; callers fail closed rather than
-## mutating both guesses.
-static func _project_candidate_paths(
- client: McpClient, roots: PackedStringArray
-) -> PackedStringArray:
- var result := PackedStringArray()
- var project_paths = client.get("config_merge_project_paths")
- if not (project_paths is PackedStringArray):
- return result
- for project_path in project_paths:
- var absolute_path := String(project_path).simplify_path()
- if absolute_path.is_absolute_path() and FileAccess.file_exists(absolute_path) and not result.has(absolute_path):
- result.append(absolute_path)
- for root in roots:
- if String(root).is_empty():
- continue
- for relative_path in project_paths:
- if String(relative_path).is_absolute_path():
- continue
- var path := String(root).path_join(String(relative_path)).simplify_path()
- if FileAccess.file_exists(path) and not result.has(path):
- result.append(path)
- return result
-
-
-static func _load_project_definitions(
- client: McpClient,
- server_name: String,
- project_roots: PackedStringArray,
- allow_comments: bool = false,
-) -> Dictionary:
- var tiers: Array[Dictionary] = []
- for path in _project_candidate_paths(client, project_roots):
- var read := _read_or_init(path, allow_comments)
- if not read.get("ok", false):
- return {"ok": false, "error": "Cannot inspect project config %s: %s" % [path, read.get("error", "invalid JSON")]}
- var config: Dictionary = read["data"]
- var key_path := select_server_key_path(config, client)
- var holder := _walk_path(config, key_path)
- if holder is Dictionary and holder.has(server_name):
- tiers.append({
- "path": path,
- "data": config,
- "key_path": key_path,
- "entry": holder[server_name],
- })
- return {"ok": true, "tiers": tiers}
-
-
-static func _project_override_message(
- tiers: Array, action: String, client_name: String, server_name: String
-) -> String:
- var paths := PackedStringArray()
- for tier in tiers:
- paths.append(str(tier["path"]))
- return "%s project config overrides %s at %s. %s resolves project files from its own working directory, so the dock cannot safely choose one; %s the entry manually." % [client_name, server_name, ", ".join(paths), client_name, action]
-
-
-static func _load_merge_tiers(client: McpClient, allow_comments: bool = false) -> Dictionary:
- var tiers: Array[Dictionary] = []
- for path in _merge_paths(client):
- ## These templates never pass through `resolved_config_path_details`,
- ## so they need its fail-closed gate here: `McpPathTemplate.expand`
- ## leaves a token it cannot resolve in place, and the relative survivor
- ## would be read — and on Configure, written — against the editor's own
- ## working directory. Fail the whole fold rather than dropping the tier:
- ## a dropped tier reads as "the user has no config there", which is
- ## exactly the wrong thing to conclude when we could not look.
- if not path.is_absolute_path():
- return {
- "ok": false,
- "error": McpClient.unresolved_config_path_error(client.display_name, path),
- }
- var read := _read_file_text(path, allow_comments)
- if not read.get("ok", false):
- return {
- "ok": false,
- "error": "Refusing to rewrite %s: %s. Fix or move the file, then re-run Configure." % [path, read.get("error", "invalid JSON")],
- }
- tiers.append({
- "path": path,
- "exists": read.get("exists", false),
- "original_text": read.get("original_text", ""),
- "data": read.get("data", {}),
- })
- return {"ok": true, "tiers": tiers}
-
-
-## Commit a multi-file removal with best-effort rollback. Each individual write
-## is atomic; this layer restores earlier tiers if a later commit fails.
-static func _write_transaction(writes: Array[Dictionary]) -> Dictionary:
- var completed: Array[Dictionary] = []
- for write in writes:
- var path := str(write["path"])
- if McpAtomicWrite.write(path, str(write["text"])):
- completed.append(write)
- continue
- var rollback_failed := PackedStringArray()
- completed.reverse()
- for previous in completed:
- if not McpAtomicWrite.write(str(previous["path"]), str(previous["original_text"])):
- rollback_failed.append(str(previous["path"]))
- var suffix := ""
- if not rollback_failed.is_empty():
- suffix = " Rollback also failed for: %s" % ", ".join(rollback_failed)
- return {"ok": false, "error": "Cannot write to %s; earlier tier changes were rolled back.%s" % [path, suffix]}
- return {"ok": true}
-## Path that `_check_status_merged`'s last-wins logic would consider
-## authoritative for the server entry, or "" when no entry is found in
-## any tier. Mirrors the same project-tier / global-tier resolution the
-## status check uses so callers (notably the dock Open/Reveal buttons)
-## land on the file that actually drives Pi's effective config.
-##
-## Resolution order (codex round 3, F-3-4):
-## 1. Latest project tier containing the entry — F2 last-wins means the
-## latest of `.pi/mcp.json` and `.mcp.json` wins (matches `_check_status_merged`).
-## 2. Latest global tier containing the entry — already iterated in
-## merge order, last-iterated-wins (same loop pattern as
-## `manual_target_details` lines 660-665).
-## 3. "" when no tier has the entry — caller decides what fallback
-## (`path_template`) to use.
-##
-## Returns "" (not `path_template`) on the "nothing anywhere" branch so
-## the dock can distinguish "no entry yet" from "entry is at the
-## template path" if it wants to.
-static func authoritative_tier_path(
- client: McpClient,
- server_name: String,
- project_roots: PackedStringArray,
-) -> String:
- if not _uses_merge_tiers(client):
- return ""
- var allow_comments := _status_allows_comments(client)
- var project := _load_project_definitions(client, server_name, project_roots, allow_comments)
- if not bool(project.get("ok", false)):
- return ""
- var project_tiers: Array = project.get("tiers", [])
- if not project_tiers.is_empty():
- # F2 last-wins: latest project tier is authoritative. When there are
- # multiple project tiers, the user-visible status is driven by the
- # latest, so the Open/Reveal buttons should send them there too.
- var latest: Dictionary = project_tiers[project_tiers.size() - 1]
- return str(latest.get("path", ""))
- var loaded := _load_merge_tiers(client, allow_comments)
- if not bool(loaded.get("ok", false)):
- return ""
- var tiers: Array = loaded.get("tiers", [])
- # Iterate in priority order, overwrite `selected_path` each time we
- # find a tier containing the entry. With Pi's merge path order
- # [mcp.json, .mcp.json] this leaves the higher-precedence `.mcp.json`.
- var selected_path: String = ""
- for tier in tiers:
- var data: Dictionary = tier.get("data", {})
- var holder: Variant = _walk_path(data, select_server_key_path(data, client))
- if holder is Dictionary and holder.has(server_name):
- selected_path = str(tier.get("path", ""))
- return selected_path
-
-
-## Pick the file and top-level map shown by the manual JSON instructions using
-## the same merge and alias precedence as automatic Configure.
-static func manual_target_details(
- client: McpClient,
- server_name: String,
- fallback_path: String,
- project_roots: PackedStringArray = PackedStringArray(),
-) -> Dictionary:
- # A read: the instructions a JSONC client is sent to must not carry
- # "Target inspection failed" for the file its own client ships.
- var allow_comments := _status_allows_comments(client)
- if _uses_merge_tiers(client):
- var project := _load_project_definitions(client, server_name, project_roots, allow_comments)
- if not project.get("ok", false):
- return {"ok": false, "error": project.get("error", "Cannot inspect project config tiers")}
- var project_tiers: Array = project.get("tiers", [])
- if project_tiers.size() > 1:
- return {"ok": false, "error": _project_override_message(project_tiers, "update or remove", client.display_name, server_name)}
- if project_tiers.size() == 1:
- var selected_project: Dictionary = project_tiers[0]
- return {
- "ok": true,
- "path": selected_project["path"],
- "key_path": selected_project["key_path"],
- }
- var loaded := _load_merge_tiers(client, allow_comments)
- if not loaded.get("ok", false):
- return {"ok": false, "error": loaded.get("error", "Cannot read merged config tiers")}
- var tiers: Array = loaded.get("tiers", [])
- var selected: Variant = tiers[0] if not tiers.is_empty() else null
- for tier in tiers:
- var config: Dictionary = tier["data"]
- var holder := _walk_path(config, select_server_key_path(config, client))
- if holder is Dictionary and holder.has(server_name):
- selected = tier
- if selected != null:
- var config: Dictionary = selected["data"]
- return {
- "ok": true,
- "path": selected["path"],
- "key_path": select_server_key_path(config, client),
- }
- var read := _read_or_init(fallback_path, allow_comments)
- if not read.get("ok", false):
- return {"ok": false, "error": "Cannot inspect %s: %s" % [fallback_path, read.get("error", "invalid JSON")]}
- return {
- "ok": true,
- "path": fallback_path,
- "key_path": select_server_key_path(read["data"], client),
- }
-
-
-## Godot's JSON.parse turns every JSON number into a float, so a later
-## JSON.stringify re-emits the user's integer fields as "8080.0" — which strict
-## consumers (Go's encoding/json into an int field, etc.) reject, and which
-## needlessly rewrites every number across the user's *other* entries. Re-narrow
-## exactly-representable integral floats back to int so they serialize without
-## the ".0". Walks dicts/arrays in place and returns the (same) value.
-##
-## Integers above 2^53 already lost precision when Godot parsed them to double,
-## so they're left as the float Godot produced rather than faking exactness —
-## byte-perfect preservation would require not parsing the file at all, and such
-## magnitudes don't occur in MCP client configs.
-static func _narrow_integral_numbers(value: Variant) -> Variant:
- match typeof(value):
- TYPE_FLOAT:
- if is_finite(value) and value == floor(value) and absf(value) <= 9007199254740992.0:
- return int(value)
- TYPE_DICTIONARY:
- for k in value:
- value[k] = _narrow_integral_numbers(value[k])
- TYPE_ARRAY:
- for i in value.size():
- value[i] = _narrow_integral_numbers(value[i])
- return value
-
-
-## True when `value` contains any float outside the IEEE-754 exact-int range
-## (`abs(f) > 2^53`) that would lose precision when JSON.stringify re-emits it.
-## The configure path refuses such files instead of silently mutating unrelated
-## integers; the remove path never needs this check because it does
-## token-preserving surgery (F5).
-static func _has_lossy_numbers(value: Variant) -> bool:
- match typeof(value):
- TYPE_FLOAT:
- # `>=` not `>`: `2^53 + 1` rounds down to `2^53.0` when Godot parses
- # the integer to double, so even a "clean" `2^53.0` after parse may
- # have lost a least-significant bit we cannot recover. Refusing at
- # the boundary is conservative but correct — and 2^53+ magnitudes
- # don't appear in real MCP client configs anyway.
- if is_finite(value) and absf(value) >= 9007199254740992.0:
- return true
- TYPE_DICTIONARY:
- for k in value:
- if _has_lossy_numbers(value[k]):
- return true
- TYPE_ARRAY:
- for i in value.size():
- if _has_lossy_numbers(value[i]):
- return true
- return false
-
-
-## Token-preserving entry removal. Walks the JSON text with a brace-balanced
-## scanner to locate the entry at `key_path[0]/.../[server_name]` and returns
-## `text` with that entry's bytes deleted (along with the surrounding
-## whitespace + trailing comma). Everything else — including integers above
-## the IEEE-754 exact-int range that `_narrow_integral_numbers` would silently
-## mutate — is preserved byte-for-byte. Returns `text` unchanged when the
-## entry isn't found in `text` (callers treat this as a no-op write).
-static func _text_remove_server_entry(text: String, key_path: PackedStringArray, server_name: String) -> String:
- # Descend through `key_path`. Each step finds `"":` directly inside the
- # current container and moves the cursor past the value's opening `{` or `[`.
- # After the loop, `cursor` is positioned just past the opening bracket of the
- # container that holds our entry, and `container_depth` tracks how deeply
- # nested that container is from the root (0 = root object/array).
- var cursor := 0
- # Skip past the root's opening `{` or `[` (and any leading whitespace or
- # UTF-8 BOM) so `_find_key_at_container_depth` starts scanning from
- # inside the root container with `inner_depth == 0`. Without this step,
- # the first character encountered is the root's `{`, which bumps
- # `inner_depth` to 1 and makes the top-level key check fail. The BOM
- # skip mirrors `_read_file_text`'s parse_copy BOM-strip above so the
- # scanner sees the same view of the file the parser does; the BOM
- # itself stays in `text` so the byte-survival F5 contract still holds
- # (codex round 3, F-3-6 — without it, files saved with a Windows BOM
- # left the entry in place after Remove).
- # The BOM can only ever be byte 0, so it is skipped BEFORE the whitespace
- # walk: a file saved as BOM + newline + `{` (common from Windows editors)
- # otherwise leaves the cursor on the newline, the root `{` is never
- # consumed, and the entry silently survives Remove.
- if cursor < text.length() and text[cursor] == "":
- cursor += 1
- while cursor < text.length() and _is_json_ws(text[cursor]):
- cursor += 1
- if cursor < text.length() and (text[cursor] == "{" or text[cursor] == "["):
- cursor += 1
- var container_depth := 0
- for key in key_path:
- var key_bytes := JSON.stringify(String(key))
- var match := _find_key_at_container_depth(text, cursor, key_bytes)
- if match.is_empty():
- return text
- var open_char := text[match["value_start"]]
- if open_char != "{" and open_char != "[":
- return text # leaf isn't an object/array — path can't reach our entry
- container_depth += 1
- cursor = match["value_start"] + 1
- # Now find `"":` at the top level of the container we descended
- # into (the helper gates on `inner_depth == 0`).
- var entry_bytes := JSON.stringify(server_name)
- var entry_match := _find_key_at_container_depth(text, cursor, entry_bytes)
- if entry_match.is_empty():
- return text
- var key_start: int = entry_match["key_start"]
- var value_end: int = entry_match["value_end"]
- # Trim the surrounding comma in a mutually exclusive way: the trailing comma
- # (after the entry's value) carries the burden when the entry is NOT the last
- # in its container, and the leading comma (before the entry's key) carries it
- # only when the entry IS the last. Trimming BOTH unconditionally corrupts a
- # middle-position entry into `"x"}"y"` with no separator — codex-review
- # finding F5 (regression after the initial round).
- var remove_end := value_end
- # Skip JSON whitespace before checking for the trailing comma — valid
- # (if unusual) formats like `"godot-ai":{} ,"other":{}` put whitespace
- # between the value and its separator. codex round 4 finding.
- var trailing_scan := remove_end
- while trailing_scan < text.length() and _is_json_ws(text[trailing_scan]):
- trailing_scan += 1
- var had_trailing_comma := trailing_scan < text.length() and text[trailing_scan] == ","
- if had_trailing_comma:
- # Jump past the comma (and any whitespace before it we just skipped).
- remove_end = trailing_scan + 1
- while remove_end < text.length() and _is_json_ws(text[remove_end]):
- remove_end += 1
- var remove_start := key_start
- if not had_trailing_comma:
- while remove_start > 0 and _is_json_ws(text[remove_start - 1]):
- remove_start -= 1
- if remove_start > 0 and text[remove_start - 1] == ",":
- remove_start -= 1
- while remove_start > 0 and _is_json_ws(text[remove_start - 1]):
- remove_start -= 1
- return text.substr(0, remove_start) + text.substr(remove_end)
-
-
-## Look for `key_bytes` (a JSON-encoded string literal including the surrounding
-## quotes) at the TOP LEVEL of the container we're inside, scanning `text` from
-## `start` onwards. The caller is expected to advance `start` just past the
-## container's opening `{` or `[`, so we begin with `inner_depth == 0` and a
-## key matches when we hit it while `inner_depth` is still 0. The scan exits
-## immediately if `inner_depth` drops below 0 (the target container closed)
-## to prevent same-named keys in later sibling containers from being picked
-## up. Returns `{key_start, value_start, value_end}` on success or `{}` if
-## the key isn't present in that container.
-static func _find_key_at_container_depth(text: String, start: int, key_bytes: String) -> Dictionary:
- var i := start
- var in_string := false
- var escape := false
- var inner_depth := 0 # depth *inside* the current container
- while i < text.length():
- var c := text[i]
- if in_string:
- if escape:
- escape = false
- elif c == "\\":
- escape = true
- elif c == '"':
- in_string = false
- i += 1
- continue
- if c == '"':
- if text.substr(i, key_bytes.length()) == key_bytes:
- var after_key := i + key_bytes.length()
- while after_key < text.length() and _is_json_ws(text[after_key]):
- after_key += 1
- if after_key < text.length() and text[after_key] == ":":
- var value_start := after_key + 1
- while value_start < text.length() and _is_json_ws(text[value_start]):
- value_start += 1
- var value_end := _json_value_span_end(text, value_start)
- if value_end >= 0 and inner_depth == 0:
- return {"key_start": i, "value_start": value_start, "value_end": value_end}
- in_string = true
- i += 1
- continue
- if c == "{" or c == "[":
- inner_depth += 1
- elif c == "}" or c == "]":
- inner_depth -= 1
- # Bail out the moment we leave the container we're scanning. Without
- # this guard, a later sibling container at the same level (e.g. a
- # top-level `extensions` block after `mcpServers` closes) re-balances
- # `inner_depth` back to 0 and a same-named key in that sibling would
- # match — silently deleting data we never intended to touch
- # (codex-review finding F5, regression after the initial round).
- if inner_depth < 0:
- return {}
- i += 1
- return {}
-
-
-## Given `value_start` pointing at the first char of a JSON value, return the
-## position AFTER the value's last char. Handles objects, arrays, strings,
-## and bare literals (numbers, true/false/null). Returns -1 if the value is
-## unterminated.
-static func _json_value_span_end(text: String, value_start: int) -> int:
- if value_start >= text.length():
- return -1
- var c := text[value_start]
- if c == "{" or c == "[":
- var open_char := c
- var close_char := "}" if c == "{" else "]"
- var depth := 1
- var i := value_start + 1
- var in_string := false
- var escape := false
- while i < text.length() and depth > 0:
- var ch := text[i]
- if in_string:
- if escape:
- escape = false
- elif ch == "\\":
- escape = true
- elif ch == '"':
- in_string = false
- i += 1
- continue
- if ch == '"':
- in_string = true
- elif ch == open_char:
- depth += 1
- elif ch == close_char:
- depth -= 1
- i += 1
- return i if depth == 0 else -1
- if c == '"':
- var i := value_start + 1
- var escape := false
- while i < text.length():
- var ch := text[i]
- if escape:
- escape = false
- elif ch == "\\":
- escape = true
- elif ch == '"':
- return i + 1
- i += 1
- return -1
- # Bare literal: number / true / false / null. Walk until a JSON-significant
- # delimiter (`,`, `}`, `]`, or whitespace).
- var i := value_start
- while i < text.length():
- var ch := text[i]
- if ch == "," or ch == "}" or ch == "]" or _is_json_ws(ch):
- break
- i += 1
- return i
-
-
-static func _is_json_ws(c: String) -> bool:
- return c == " " or c == "\t" or c == "\n" or c == "\r"
diff --git a/addons/godot_ai/clients/_json_strategy.gd.uid b/addons/godot_ai/clients/_json_strategy.gd.uid
deleted file mode 100644
index 5e41fbb..0000000
--- a/addons/godot_ai/clients/_json_strategy.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://g8a4iijpk22w
diff --git a/addons/godot_ai/clients/_manual_command.gd b/addons/godot_ai/clients/_manual_command.gd
deleted file mode 100644
index fef2ffc..0000000
--- a/addons/godot_ai/clients/_manual_command.gd
+++ /dev/null
@@ -1,353 +0,0 @@
-@tool
-class_name McpManualCommand
-extends RefCounted
-
-const SHELL_POSIX := "posix"
-const SHELL_POWERSHELL := "powershell"
-## Keep this intersection deliberately small. PowerShell treats a leading `@`
-## as splatting syntax and commas as list separators, while POSIX shells accept
-## both literally; quoting either is safer than trying to infer token position.
-const _SHELL_BARE_SAFE_CHARS := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_+=:./-"
-
-## Synthesize the "Run this manually" string the dock surfaces when
-## auto-configure can't find a CLI / write a file. Generated from the
-## descriptor's declarative fields — there is no per-client builder
-## Callable. See `_base.gd` for why descriptors are data-only.
-
-
-static func build(
- client: McpClient,
- server_name: String,
- server_url: String,
- resolved_path: String,
- launch: Dictionary = {},
- project_roots: PackedStringArray = PackedStringArray(),
-) -> String:
- if client.command_shape == McpClient.CommandShape.NONE:
- return "%s has no authenticated v4 configuration path; do not persist the backend URL." % client.display_name
- match client.config_type:
- "cli":
- return _build_cli(client, server_name, server_url, resolved_path, launch, project_roots)
- "json":
- return _build_json(client, server_name, server_url, resolved_path, launch, project_roots)
- "toml":
- return _build_toml(client, server_name, server_url, resolved_path, launch)
- "yaml":
- return _build_yaml(client, server_name, server_url, resolved_path, launch)
- "dsh":
- return _build_dsh(client, server_name, server_url, resolved_path, launch)
- return ""
-
-
-## CLI clients: format the register template against the *short* CLI name so
-## the user can paste it into a terminal regardless of where their binary
-## lives. (The auto-configure path resolves to an absolute uvx-style path;
-## that's noise for a paste-into-terminal hint. The attach launcher path
-## inside a command-shape line stays absolute — status verification compares
-## the registered command against the resolved launcher verbatim.)
-static func _build_cli(
- client: McpClient,
- server_name: String,
- server_url: String,
- resolved_path: String = "",
- launch: Dictionary = {},
- project_roots: PackedStringArray = PackedStringArray(),
-) -> String:
- if client.cli_register_template.is_empty() or client.cli_names.is_empty():
- return ""
- var shell_kind := _shell_kind_for_platform()
- var short_name: String = String(client.cli_names[0])
- # Prefer the non-.exe form for a cross-platform-looking command line.
- for n in client.cli_names:
- if not String(n).ends_with(".exe"):
- short_name = String(n)
- break
- var cmd := ""
- if client.command_shape != McpClient.CommandShape.NONE:
- var launch_error := McpCliStrategy.command_launch_error(client, launch)
- if not launch_error.is_empty():
- cmd = "Attach launch command unavailable: %s" % launch_error
- else:
- var args := McpCliStrategy.format_args(client.cli_register_template, server_name, server_url, launch)
- var parts: Array[String] = [short_name]
- for arg in args:
- parts.append(String(arg))
- var lines := _sweep_lines(client, server_name, server_url, short_name, shell_kind)
- lines.append(_render_command_line(parts, shell_kind))
- cmd = _format_shell_block(lines, shell_kind) + _sweep_caveat(client)
- else:
- var args := McpCliStrategy.format_args(client.cli_register_template, server_name, server_url)
- var parts: Array[String] = [short_name]
- for arg in args:
- parts.append(String(arg))
- var lines := _sweep_lines(client, server_name, server_url, short_name, shell_kind)
- lines.append(_render_command_line(parts, shell_kind))
- cmd = _format_shell_block(lines, shell_kind) + _sweep_caveat(client)
- # #463: a CLI client with a JSON fallback (Claude Code) may have no `claude`
- # binary at all — e.g. installed only as a VS Code/Cursor extension. The CLI
- # line above is useless to that user, so also show the config-file edit that
- # auto-configure falls back to writing.
- if client.has_json_fallback() and not resolved_path.is_empty():
- return "%s\n\nNo `%s` CLI (e.g. installed as a VS Code/Cursor extension)? %s" % [
- cmd, short_name, _build_json(client, server_name, server_url, resolved_path, launch, project_roots),
- ]
- return cmd
-
-
-## #877: Configure's first act is the pre-cleanup — it runs the unregister
-## template once per scope the descriptor can write to (`_cli_strategy.gd`
-## `configure`) before registering. Rendering only the register line made this
-## hint disagree with what the button does, which is the worst property a
-## "run this manually" string can have. The removes go in the SAME shell block
-## as the register, in the order Configure runs them, so the whole thing stays
-## one copy-paste rather than a labelled command plus loose prose.
-static func _sweep_lines(
- client: McpClient,
- server_name: String,
- server_url: String,
- short_name: String,
- shell_kind: String,
-) -> Array[String]:
- var lines: Array[String] = []
- if client.cli_unregister_template.is_empty():
- return lines
- for pre_scope in McpCliStrategy.cleanup_scopes(client):
- var args := McpCliStrategy.format_args(
- client.cli_unregister_template, server_name, server_url, {}, pre_scope
- )
- var parts: Array[String] = [short_name]
- for arg in args:
- parts.append(String(arg))
- lines.append(_render_command_line(parts, shell_kind))
- return lines
-
-
-## The one thing the commands themselves cannot show: that the project pass
-## resolves `.mcp.json` against the CLI's cwd, so it can delete an entry from a
-## repository that has nothing to do with this project. Only for `{scope}`
-## descriptors — a single implicit-scope pass removes exactly the entry the
-## register is about to rewrite, which needs no warning. Kept outside the shell
-## block so pasting the block never picks up prose.
-static func _sweep_caveat(client: McpClient) -> String:
- if client.cli_unregister_template.is_empty() or not McpCliStrategy.uses_scope_token(client):
- return ""
- return (
- "\n\nThe project-scope remove rewrites the .mcp.json in the directory the"
- + " editor was launched from — not necessarily this project — so it drops a"
- + " hand-maintained godot-ai entry there. Other servers in that file are left"
- + " alone."
- )
-
-
-static func _shell_kind_for_platform() -> String:
- return SHELL_POWERSHELL if OS.get_name() == "Windows" else SHELL_POSIX
-
-
-## Render a command for one explicitly named shell. The label is load-bearing:
-## POSIX and PowerShell use different escaping for embedded single quotes, so
-## presenting the command without its target shell invites a bad copy/paste.
-static func _format_shell_command(parts: Array[String], shell_kind: String) -> String:
- return _format_shell_block([_render_command_line(parts, shell_kind)], shell_kind)
-
-
-## One label over N command lines. The label is per-block, not per-line:
-## repeating "Run in PowerShell:" four times would bury the single line that
-## actually registers.
-static func _format_shell_block(lines: Array[String], shell_kind: String) -> String:
- var label := "Run in PowerShell:" if shell_kind == SHELL_POWERSHELL else "Run in a POSIX shell:"
- return "%s\n%s" % [label, "\n".join(lines)]
-
-
-static func _render_command_line(parts: Array[String], shell_kind: String) -> String:
- var rendered: Array[String] = []
- for part in parts:
- rendered.append(_shell_display_arg(part, shell_kind))
- return " ".join(rendered)
-
-
-## Quote one argv element for the paste-into-terminal hint. Single-quoted
-## strings are literal in both supported shells, but embedded single quotes
-## have shell-specific spellings. Backslashes, double quotes, dollar signs,
-## and PowerShell backticks remain byte-for-byte unchanged inside the quotes.
-static func _shell_display_arg(arg: String, shell_kind: String) -> String:
- if arg.is_empty():
- return "''"
- var stays_bare := true
- for index in range(arg.length()):
- if _SHELL_BARE_SAFE_CHARS.find(arg.substr(index, 1)) < 0:
- stays_bare = false
- break
- if stays_bare:
- return arg
- if shell_kind == SHELL_POWERSHELL:
- return "'%s'" % arg.replace("'", "''")
- return "'%s'" % arg.replace("'", "'\"'\"'")
-
-
-static func _build_json(
- client: McpClient,
- server_name: String,
- server_url: String,
- resolved_path: String,
- launch: Dictionary = {},
- project_roots: PackedStringArray = PackedStringArray(),
-) -> String:
- var target := McpJsonStrategy.manual_target_details(client, server_name, resolved_path, project_roots)
- var target_note := ""
- if not target.get("ok", false):
- target_note = "Target inspection failed: %s" % str(target.get("error", "cannot inspect config"))
- target = {"path": resolved_path, "key_path": client.server_key_path}
- var target_path := str(target.get("path", resolved_path))
- var key_path: PackedStringArray = target.get("key_path", client.server_key_path)
- var key := key_path[0] if key_path.size() > 0 else "mcpServers"
- if client.command_shape != McpClient.CommandShape.NONE:
- var lines: Array[String] = []
- var launch_error := McpJsonStrategy.command_launch_error(client, launch)
- if launch_error.is_empty():
- var command_entry := McpJsonStrategy.build_entry(client, server_url, null, launch)
- lines.append("Edit %s and add under \"%s\":" % [target_path, key])
- lines.append(" \"%s\": %s" % [server_name, _format_entry_inline(command_entry)])
- else:
- lines.append("Attach launch command unavailable: %s" % launch_error)
- if not target_note.is_empty():
- lines.append("")
- lines.append(target_note)
- return "\n".join(lines)
- var entry := McpJsonStrategy.build_entry(client, server_url)
- var instructions := "Edit %s and add under \"%s\":\n \"%s\": %s" % [target_path, key, server_name, _format_entry_inline(entry)]
- if not target_note.is_empty():
- instructions += "\n\n" + target_note
- return instructions
-
-
-static func _build_toml(
- client: McpClient,
- _server_name: String,
- server_url: String,
- resolved_path: String,
- launch: Dictionary = {},
-) -> String:
- var header := _toml_header(client)
- if client.command_shape != McpClient.CommandShape.NONE:
- var lines: Array[String] = []
- var rendered := McpTomlStrategy.render_body(client, server_url, launch)
- if bool(rendered.get("ok", false)):
- lines.append("Edit %s and add:" % resolved_path)
- lines.append(" %s" % header)
- for body_line in rendered.get("lines", []):
- lines.append(" %s" % str(body_line))
- else:
- lines.append("Attach launch command unavailable: %s" % str(rendered.get("error", "no compatible launcher found")))
- return "\n".join(lines)
- var body := McpTomlStrategy.format_body(client.toml_body_template, server_url)
- var lines: Array[String] = ["Edit %s and add:" % resolved_path, " %s" % header]
- for b in body:
- lines.append(" %s" % String(b))
- return "\n".join(lines)
-
-
-static func _build_yaml(
- client: McpClient,
- server_name: String,
- server_url: String,
- resolved_path: String,
- launch: Dictionary = {},
-) -> String:
- var key := client.server_key_path[0] if client.server_key_path.size() > 0 else "mcp_servers"
- if client.command_shape != McpClient.CommandShape.NONE:
- var lines: Array[String] = []
- var launch_error := McpYamlStrategy.command_launch_error(client, launch)
- if launch_error.is_empty():
- var command_entry := McpYamlStrategy.build_entry(client, server_url, null, launch)
- lines.append("Edit %s and add under '%s':" % [resolved_path, key])
- for entry_line in McpYamlStrategy.render_entry_lines(server_name, command_entry):
- lines.append(String(entry_line))
- else:
- lines.append("Attach launch command unavailable: %s" % launch_error)
- return "\n".join(lines)
- var entry := McpYamlStrategy.build_entry(client, server_url)
- var lines: Array[String] = [
- "Edit %s and add under '%s':" % [resolved_path, key],
- " %s:" % server_name,
- ]
- for k in entry:
- lines.append(" %s: %s" % [k, str(entry[k])])
- return "\n".join(lines)
-
-
-## DeepSeek Harness: the user pastes a loader `insert` row into the home
-## patch layer (`$DSH_HOME/cordis.patch.yml`). Auto-configure writes exactly
-## these lines, so the manual text matches the file Configure would produce
-## byte-for-byte.
-static func _build_dsh(
- client: McpClient,
- server_name: String,
- server_url: String,
- resolved_path: String,
- launch: Dictionary = {},
-) -> String:
- var lines: Array[String] = ["Edit %s and add this loader entry (an `insert` row; a plain `- id:` row only overrides an existing bundle id and would be skipped):" % resolved_path]
- var entry_id_value := McpDshStrategy.entry_id(server_name)
- if client.command_shape != McpClient.CommandShape.NONE:
- var launch_error := McpDshStrategy.command_launch_error(client, launch)
- if launch_error.is_empty():
- var command_entry := McpDshStrategy.build_entry(client, server_name, server_url, null, launch)
- for row_line in McpDshStrategy.render_insert_row(entry_id_value, command_entry):
- lines.append(String(row_line))
- else:
- lines.append("Attach launch command unavailable: %s" % launch_error)
- return "\n".join(lines)
- var url_entry := McpDshStrategy.build_url_entry(client, server_name, server_url)
- for row_line in McpDshStrategy.render_insert_row(entry_id_value, url_entry):
- lines.append(String(row_line))
- return "\n".join(lines)
-
-
-## Mirrors the [section."name"] header `_toml_strategy._primary_header`
-## emits, kept here so the manual-command text matches the file we'd write.
-static func _toml_header(client: McpClient) -> String:
- var parts := client.toml_section_path
- if parts.size() < 2:
- return "[%s]" % ".".join(parts)
- var section := ".".join(McpClient._array_from_packed(McpClient._packed_slice(parts, 0, parts.size() - 1)))
- var name := parts[parts.size() - 1]
- return "[%s.\"%s\"]" % [section, name]
-
-
-## Format an entry dict as a single inline JSON-ish string, matching the
-## pre-refactor manual-command style: `{ "k": v, "k": v }` with spaces.
-## Pre-existing manual-command tests assert the exact substring shape; this
-## keeps them stable.
-##
-## Uses `JSON.stringify` for every leaf String (key OR value) so paths
-## containing backslashes / quotes / newlines render as syntactically valid
-## JSON. A Windows uvx path like `C:\Users\foo\uvx.exe` would otherwise be
-## emitted as `"C:\Users\foo\uvx.exe"` — invalid JSON, unsafe to paste.
-static func _format_entry_inline(entry: Dictionary) -> String:
- var parts: Array[String] = []
- for k in entry:
- parts.append("%s: %s" % [JSON.stringify(String(k)), _format_value(entry[k])])
- if parts.is_empty():
- return "{}"
- return "{ %s }" % ", ".join(parts)
-
-
-static func _format_value(value: Variant) -> String:
- # Strings, bools, numbers, null all round-trip correctly through JSON.stringify
- # without spurious quoting of non-string scalars (true → `true`, 5 → `5`).
- # Arrays and Dictionaries are formatted manually so the inline ` { k: v } `
- # spacing matches the pre-refactor manual-command output shape that tests
- # pin with assert_contains.
- if value is Array:
- var arr_parts: Array[String] = []
- for v in value:
- arr_parts.append(_format_value(v))
- return "[%s]" % ", ".join(arr_parts)
- if value is Dictionary:
- var d_parts: Array[String] = []
- for k in value:
- d_parts.append("%s: %s" % [JSON.stringify(String(k)), _format_value(value[k])])
- if d_parts.is_empty():
- return "{}"
- return "{ %s }" % ", ".join(d_parts)
- return JSON.stringify(value)
diff --git a/addons/godot_ai/clients/_manual_command.gd.uid b/addons/godot_ai/clients/_manual_command.gd.uid
deleted file mode 100644
index 07a96f4..0000000
--- a/addons/godot_ai/clients/_manual_command.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ct1wmgfk408x0
diff --git a/addons/godot_ai/clients/_path_template.gd b/addons/godot_ai/clients/_path_template.gd
deleted file mode 100644
index 8fc9753..0000000
--- a/addons/godot_ai/clients/_path_template.gd
+++ /dev/null
@@ -1,242 +0,0 @@
-@tool
-class_name McpPathTemplate
-extends RefCounted
-
-## Expands ~ / $HOME / $APPDATA / $XDG_CONFIG_HOME / $LOCALAPPDATA / $USERPROFILE
-## inside path templates so per-client descriptors can declare paths declaratively
-## without hand-rolling per-OS lookups.
-
-## #691: dock worker threads (client-status refresh, configure/remove
-## actions) and the #678 startup walk's discovery worker expand these
-## templates off the main thread, while the spawn step mutates the
-## process-global environment around `OS.create_process`
-## (`GODOT_AI_OWNER_PID`, `GODOT_AI_PLUGIN_SPAWNED`, `PYTHONPATH`,
-## `GODOT_AI_DISABLE_TELEMETRY`). A glibc `getenv` racing a concurrent
-## `setenv` can return a freed pointer — rare but process-fatal. All env
-## reads in this layer therefore go through `env_lookup`: on the MAIN
-## thread it reads live and refreshes a mutex-guarded snapshot; off the
-## main thread it serves from the snapshot, so no `OS.get_environment`
-## runs concurrently with the spawn window's mutations. Callers pre-warm
-## every var their workers can touch via `warm_env_snapshot` (plugin
-## `_enter_tree` and the dock's phase-1 refresh prep, both main-thread,
-## both before any worker starts).
-static var _env_snapshot := {}
-static var _env_snapshot_mutex := Mutex.new()
-
-## Every var this layer and its sibling consumers (`_base.gd`
-## `config_file_override_details`, `config_home_override_details`, `_cli_finder.gd` lookups,
-## `client_configurator.gd` mode/trace reads) can touch off-main.
-## Descriptor-declared config-file/config-home env names are passed as extras
-## by the warm callers.
-const _BASE_ENV_VARS: Array[String] = [
- "HOME",
- "USERPROFILE",
- "XDG_CONFIG_HOME",
- "APPDATA",
- "LOCALAPPDATA",
- "SHELL",
- "ProgramFiles",
- "GODOT_AI_MODE",
- "GODOT_AI_STARTUP_TRACE",
- ## #804 (#752 adoption): _find_venv_python reads this via env_lookup on
- ## the dock's worker path; without pre-warming, a set override reads as
- ## empty there and is silently ignored — the exact misconfiguration the
- ## push_warning in client_configurator.gd exists to surface.
- "GODOT_AI_VENV_PYTHON",
-]
-
-
-## Thread-safe env read (#691). Main thread: live read + snapshot refresh.
-## Worker thread: snapshot only, so it can never race a main-thread
-## setenv/unsetenv. A worker read of a never-warmed var returns "" — the
-## same value an unset var reads as — never a live OS.get_environment,
-## which would reintroduce the race for exactly the vars nobody thought
-## to warm. Missing warm-up degrades resolution; it must not touch the
-## process-global environment off-main.
-static func env_lookup(name: String) -> String:
- if OS.get_thread_caller_id() == OS.get_main_thread_id():
- var live := OS.get_environment(name)
- _env_snapshot_mutex.lock()
- _env_snapshot[name] = live
- _env_snapshot_mutex.unlock()
- return live
- _env_snapshot_mutex.lock()
- var cached: Variant = _env_snapshot.get(name, null)
- _env_snapshot_mutex.unlock()
- if cached != null:
- return str(cached)
- return ""
-
-
-## Main-thread pre-warm so subsequent worker reads never touch the real
-## environment. Idempotent; safe to call before every worker dispatch.
-static func warm_env_snapshot(extra_vars: PackedStringArray = PackedStringArray()) -> void:
- for var_name in _BASE_ENV_VARS:
- env_lookup(var_name)
- for var_name in extra_vars:
- if not String(var_name).is_empty():
- env_lookup(String(var_name))
-
-
-## Pick the right entry from a {"darwin": ..., "windows": ..., "linux": ...} map.
-static func resolve(template_map: Dictionary) -> String:
- var key := platform_key(template_map)
- if key.is_empty():
- return ""
- var template: String = template_map[key]
- return expand(template)
-
-
-## Return the platform-specific key present in a descriptor map. `unix` is a
-## shorthand for macOS and Linux. Public so descriptors can use the same
-## platform selection for ordered path-candidate arrays as for one path.
-static func platform_key(template_map: Dictionary) -> String:
- var key := _os_key()
- if template_map.has(key):
- return key
- if (key == "darwin" or key == "linux") and template_map.has("unix"):
- return "unix"
- return ""
-
-
-## Expand one path template into zero or more concrete paths. A single `*` is
-## allowed inside one DIRECTORY segment (for example `Packages/Claude_*`). The
-## wildcard is resolved by enumerating that segment's parent; the remaining
-## suffix may name a file that does not exist yet, which lets callers derive a
-## deterministic create target for a fresh packaged-app install.
-##
-## Multiple wildcards fail closed and return no candidates. A wildcard final
-## segment may identify an installation directory for `detect_paths`. Returned
-## paths are sorted for deterministic tests and diagnostics; callers still
-## reject ambiguous config groups rather than picking one.
-static func expand_path_candidates(template: String) -> PackedStringArray:
- var expanded := expand(template)
- if expanded.is_empty():
- return PackedStringArray()
- var star := expanded.find("*")
- if star < 0:
- return PackedStringArray([expanded])
- if expanded.find("*", star + 1) >= 0:
- return PackedStringArray()
-
- var slash_before := maxi(expanded.rfind("/", star), expanded.rfind("\\", star))
- var forward_after := expanded.find("/", star)
- var backward_after := expanded.find("\\", star)
- var slash_after := forward_after
- if slash_after < 0 or (backward_after >= 0 and backward_after < slash_after):
- slash_after = backward_after
- if slash_before < 0:
- return PackedStringArray()
-
- var parent := expanded.substr(0, slash_before)
- var pattern := (
- expanded.substr(slash_before + 1)
- if slash_after < 0
- else expanded.substr(slash_before + 1, slash_after - slash_before - 1)
- )
- var suffix := "" if slash_after < 0 else expanded.substr(slash_after + 1)
- var pattern_star := pattern.find("*")
- if pattern_star < 0:
- return PackedStringArray()
- var prefix := pattern.substr(0, pattern_star)
- var ending := pattern.substr(pattern_star + 1)
- var dir := DirAccess.open(parent)
- if dir == null:
- return PackedStringArray()
-
- var matches := PackedStringArray()
- for child in dir.get_directories():
- if _wildcard_segment_matches(String(child), prefix, ending):
- var matched_path := parent.path_join(String(child))
- matches.append(matched_path if suffix.is_empty() else matched_path.path_join(suffix))
- matches.sort()
- return matches
-
-
-## Substitute env vars and ~ in a single template string.
-##
-## A token that cannot be resolved — the var is unset and no home-derived
-## fallback applies, or a dock worker's env snapshot was never warmed with it —
-## is LEFT IN PLACE rather than replaced with "". Substituting "" is what turns
-## `$USERPROFILE/godot` into `/godot`: a root-relative path that
-## `is_absolute_path()` accepts, so every fail-closed guard downstream waves it
-## through and the caller reads or writes a directory the user never named.
-## `$USERPROFILE` is the easy repro (unset off Windows, and the only one of
-## these vars with no fallback), but `$HOME`/`$XDG_CONFIG_HOME`/`$APPDATA`/
-## `$LOCALAPPDATA` reach the same state whenever `_home()` is empty, which the
-## missing-warm-up degradation above makes reachable on a worker thread.
-##
-## The surviving token keeps the result non-absolute, so those guards catch it,
-## and the unexpanded path they report still shows what failed to resolve.
-## `~` is left alone for the same reason: collapsing `~/godot` to the relative
-## `godot` aims it at the editor's own working directory.
-static func expand(template: String) -> String:
- if template.is_empty():
- return ""
- var out := template
- if out.begins_with("~/") or out == "~":
- var home := _home()
- if not home.is_empty():
- out = home if out == "~" else home.path_join(out.substr(2))
- # $HOME, $APPDATA, $LOCALAPPDATA, $USERPROFILE, $XDG_CONFIG_HOME
- for var_name in ["XDG_CONFIG_HOME", "LOCALAPPDATA", "USERPROFILE", "APPDATA", "HOME"]:
- var token := "$%s" % var_name
- if out.find(token) < 0:
- continue
- var value := env_lookup(var_name)
- if value.is_empty():
- value = _home_fallback(var_name)
- if value.is_empty():
- continue
- out = out.replace(token, value)
- return out
-
-
-## Home-derived default for a var the environment does not define. Returns ""
-## when home itself is unresolvable so `expand` leaves the token alone: rooting
-## these at an empty home yields a RELATIVE path, not an absent one —
-## `"".path_join(".config")` is `.config`, which reads against the editor's
-## working directory rather than the user's config dir. `USERPROFILE` has no
-## fallback (it is the thing `_home()` itself falls back to).
-static func _home_fallback(var_name: String) -> String:
- var home := _home()
- if home.is_empty():
- return ""
- match var_name:
- "XDG_CONFIG_HOME":
- return home.path_join(".config")
- "APPDATA":
- return home.path_join("AppData/Roaming")
- "LOCALAPPDATA":
- return home.path_join("AppData/Local")
- "HOME":
- return home
- return ""
-
-
-static func _os_key() -> String:
- match OS.get_name():
- "macOS":
- return "darwin"
- "Windows":
- return "windows"
- _:
- return "linux"
-
-
-static func _wildcard_segment_matches(value: String, prefix: String, ending: String) -> bool:
- # Prefix/suffix tests alone allow the two fixed portions to overlap inside a
- # too-short value. Glob semantics require room for both portions even when
- # `*` matches an empty string.
- if value.length() < prefix.length() + ending.length():
- return false
- if OS.get_name() == "Windows":
- return value.to_lower().begins_with(prefix.to_lower()) and value.to_lower().ends_with(ending.to_lower())
- return value.begins_with(prefix) and value.ends_with(ending)
-
-
-static func _home() -> String:
- var h := env_lookup("HOME")
- if h.is_empty():
- h = env_lookup("USERPROFILE")
- return h
diff --git a/addons/godot_ai/clients/_path_template.gd.uid b/addons/godot_ai/clients/_path_template.gd.uid
deleted file mode 100644
index f2403d7..0000000
--- a/addons/godot_ai/clients/_path_template.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://5pd418va35ms
diff --git a/addons/godot_ai/clients/_registry.gd b/addons/godot_ai/clients/_registry.gd
deleted file mode 100644
index 000317b..0000000
--- a/addons/godot_ai/clients/_registry.gd
+++ /dev/null
@@ -1,156 +0,0 @@
-@tool
-class_name McpClientRegistry
-extends RefCounted
-
-## Central enumeration of every supported MCP client. Adding a new client
-## means: drop a file in clients/, then append one path below.
-##
-## Paths, not preloads (#736): a preload array pulled all client descriptor
-## scripts into the boot-time compile closure of everything that preloads
-## this registry (plugin.gd via client_configurator.gd and mcp_dock.gd),
-## stalling "Initializing plugins" on every editor boot. Descriptors are
-## only needed when the dock refreshes client statuses or a client_*
-## command runs, so they load lazily on first registry access.
-
-const _CLIENT_SCRIPT_PATHS := [
- "res://addons/godot_ai/clients/claude_code.gd",
- "res://addons/godot_ai/clients/claude_desktop.gd",
- "res://addons/godot_ai/clients/codex.gd",
- "res://addons/godot_ai/clients/codebuddy.gd",
- "res://addons/godot_ai/clients/grok.gd",
- "res://addons/godot_ai/clients/antigravity.gd",
- "res://addons/godot_ai/clients/cursor.gd",
- "res://addons/godot_ai/clients/windsurf.gd",
- "res://addons/godot_ai/clients/vscode.gd",
- "res://addons/godot_ai/clients/vscode_insiders.gd",
- "res://addons/godot_ai/clients/zed.gd",
- "res://addons/godot_ai/clients/gemini_cli.gd",
- "res://addons/godot_ai/clients/cline.gd",
- "res://addons/godot_ai/clients/kilo_code.gd",
- "res://addons/godot_ai/clients/roo_code.gd",
- "res://addons/godot_ai/clients/zoo_code.gd",
- "res://addons/godot_ai/clients/kiro.gd",
- "res://addons/godot_ai/clients/trae.gd",
- "res://addons/godot_ai/clients/opencode.gd",
- "res://addons/godot_ai/clients/qwen_code.gd",
- "res://addons/godot_ai/clients/kimi_code.gd",
- "res://addons/godot_ai/clients/hermes.gd",
- "res://addons/godot_ai/clients/pi.gd",
- "res://addons/godot_ai/clients/deepseek_harness.gd",
-]
-
-static var _instances: Array[McpClient] = []
-static var _by_id: Dictionary = {}
-## First registry access can come from the dock's client-status refresh
-## worker thread while the main thread hits it via a client_* command —
-## serialize the one-time load so a racing thread can never observe a
-## half-built registry. load() itself is thread-safe via ResourceLoader.
-static var _load_mutex := Mutex.new()
-## True when even a fresh rebuild yields instances missing base-schema
-## fields — the deep stale-script state after an in-session self-update
-## (#850; docs/releasing.md release-shape rules). Only an editor restart
-## heals it; callers surface RESTART_TO_FINISH_UPDATE instead of erroring
-## per client. Never reset within a session: rebuilding again cannot help,
-## it would only repeat the load work and warning on every dock sweep.
-static var _stale_session := false
-
-const RESTART_TO_FINISH_UPDATE := (
- "Godot AI was updated in this editor session. Restart the editor to finish the update."
-)
-
-
-static func all() -> Array[McpClient]:
- _ensure_loaded()
- return _instances
-
-
-static func get_by_id(id: String) -> McpClient:
- _ensure_loaded()
- return _by_id.get(id, null)
-
-
-static func ids() -> PackedStringArray:
- var out := PackedStringArray()
- for c in all():
- out.append(c.id)
- return out
-
-
-static func has_id(id: String) -> bool:
- _ensure_loaded()
- return _by_id.has(id)
-
-
-## True when this editor session is running a self-update whose script
-## reloads left descriptor state unusable. Client operations short-circuit
-## with RESTART_TO_FINISH_UPDATE rather than spamming per-field errors.
-static func stale_session_detected() -> bool:
- _ensure_loaded()
- return _stale_session
-
-
-## An instance is coherent when fields added to the CURRENT McpClient schema
-## read back with their declared types. After an in-session self-update,
-## hot-patched or pre-update instances answer Nil for vars the update added
-## (#850: `config_path_candidates` and
-## `config_file_env` read as Nil, crashing platform_key / String()). The
-## reflected `get()` avoids typed-access errors on such instances.
-static func _instance_is_coherent(inst: Object) -> bool:
- if inst == null:
- return false
- return (
- inst.get("config_path_candidates") is Dictionary
- and inst.get("config_file_env") is String
- and inst.get("path_template") is Dictionary
- and inst.get("server_key_path_aliases") is Array
- and inst.get("config_merge_path_templates") is Dictionary
- and inst.get("config_merge_project_paths") is PackedStringArray
- )
-
-
-static func _cache_is_coherent() -> bool:
- return not _instances.is_empty() and _instance_is_coherent(_instances[0])
-
-
-static func _ensure_loaded() -> void:
- if _stale_session:
- return
- if _cache_is_coherent():
- return
- _load_mutex.lock()
- ## Re-check under the lock: another thread may have rebuilt (or concluded
- ## staleness) while this one waited.
- if not _stale_session and not _cache_is_coherent():
- ## Covers both the first load and the post-self-update rebuild: this
- ## registry file can survive an update unchanged, so its statics keep
- ## serving pre-update instances to freshly reloaded callers. A rebuild
- ## instantiates from the reloaded descriptor scripts, which repairs
- ## every case except a stale base Script object itself.
- _load()
- if not _instances.is_empty() and not _cache_is_coherent():
- _stale_session = true
- push_warning("MCP | %s" % RESTART_TO_FINISH_UPDATE)
- _load_mutex.unlock()
-
-
-static func _load() -> void:
- ## Build into locals and publish whole containers last, so the lock-free
- ## fast path in _ensure_loaded can never see a partially-filled registry.
- var instances: Array[McpClient] = []
- var by_id: Dictionary = {}
- for path in _CLIENT_SCRIPT_PATHS:
- var script := load(path) as GDScript
- if script == null:
- push_warning("MCP | failed to load client descriptor %s" % path)
- continue
- var inst: McpClient = script.new()
- if inst.id.is_empty():
- push_warning("MCP | client descriptor %s has empty id" % path)
- continue
- if by_id.has(inst.id):
- push_warning("MCP | duplicate client id: %s" % inst.id)
- continue
- instances.append(inst)
- by_id[inst.id] = inst
- _by_id = by_id
- _instances = instances
diff --git a/addons/godot_ai/clients/_registry.gd.uid b/addons/godot_ai/clients/_registry.gd.uid
deleted file mode 100644
index 57edcf7..0000000
--- a/addons/godot_ai/clients/_registry.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bxougoq8xwg1
diff --git a/addons/godot_ai/clients/_toml_strategy.gd b/addons/godot_ai/clients/_toml_strategy.gd
deleted file mode 100644
index 063f387..0000000
--- a/addons/godot_ai/clients/_toml_strategy.gd
+++ /dev/null
@@ -1,754 +0,0 @@
-@tool
-class_name McpTomlStrategy
-extends RefCounted
-
-## TOML upsert for URL entries and client-owned command entries.
-##
-## This remains deliberately smaller than a general TOML parser, but the
-## parts that affect migration are semantic: assignments retain their whole
-## value span (including multiline arrays/strings), and command/args status
-## verification decodes TOML strings and arrays rather than comparing text.
-
-
-static func configure(
- client: McpClient,
- _server_name: String,
- server_url: String,
- launch: Dictionary = {},
-) -> Dictionary:
- var resolution := client.resolved_config_path_details()
- var path := str(resolution.get("path", ""))
- var path_error := str(resolution.get("error", ""))
- if not path_error.is_empty():
- return {"status": "error", "message": path_error}
- if path.is_empty():
- return {"status": "error", "message": "Could not resolve config path for %s" % client.display_name}
-
- var seed_path := str(resolution.get("seed_path", ""))
- var read_path := seed_path if not FileAccess.file_exists(path) and not seed_path.is_empty() else path
- var read := _read_or_init(read_path)
- if not read["ok"]:
- return {"status": "error", "message": "Refusing to overwrite %s: %s. Fix or move the file, then re-run Configure." % [read_path, read["error"]]}
-
- var rendered := render_body(client, server_url, launch)
- if not bool(rendered.get("ok", false)):
- return {"status": "error", "message": str(rendered.get("error", "Could not build the TOML entry."))}
-
- var lines: Array[String] = _split_lines(String(read["data"]))
- var body: Array[String] = rendered["lines"]
- var pinned_keys: Dictionary = rendered["pinned_keys"]
- var initial_keys: Dictionary = rendered["initial_keys"]
- var removed_keys: Dictionary = rendered["removed_keys"]
-
- var section := _find_section(lines, _all_headers(client))
- var header := _primary_header(client)
- var new_lines: Array[String] = [header]
-
- if section.is_empty():
- new_lines.append_array(body)
- var output_fresh: Array[String] = []
- output_fresh.append_array(lines)
- if not output_fresh.is_empty() and not output_fresh[-1].strip_edges().is_empty():
- output_fresh.append("")
- output_fresh.append_array(new_lines)
- if not McpAtomicWrite.write(path, _join_lines(output_fresh)):
- return {"status": "error", "message": "Cannot write to %s" % path}
- return {"status": "ok", "message": McpClient.configured_message(client, server_url)}
-
- var old_items := _value_items(lines, int(section["start"]) + 1, int(section["end"]))
- var old_by_key := {}
- for item in old_items:
- var old_key := str(item.get("key", ""))
- if not old_key.is_empty() and not old_by_key.has(old_key):
- old_by_key[old_key] = item
-
- var body_items := _value_items(body, 0, body.size())
- var emitted_keys := {}
- for item in body_items:
- var key := str(item.get("key", ""))
- if key.is_empty():
- new_lines.append_array(item["lines"])
- continue
- emitted_keys[key] = true
- if initial_keys.has(key) and old_by_key.has(key):
- new_lines.append_array(old_by_key[key]["lines"])
- else:
- ## Pinned keys always use the freshly rendered span. A generated key
- ## that is neither pinned nor initial is also rendered deterministically.
- new_lines.append_array(item["lines"])
-
- ## Carry unknown/user-owned assignments and standalone comments verbatim.
- ## Whole item spans prevent multiline arrays/strings from being truncated.
- for item in old_items:
- var key := str(item.get("key", ""))
- if not key.is_empty():
- if emitted_keys.has(key) or pinned_keys.has(key) or removed_keys.has(key):
- continue
- new_lines.append_array(item["lines"])
- continue
- var item_lines: Array = item.get("lines", [])
- for line in item_lines:
- if not str(line).strip_edges().is_empty():
- new_lines.append(str(line))
-
- var output: Array[String] = []
- output.append_array(_slice(lines, 0, int(section["start"])))
- output.append_array(new_lines)
- output.append_array(_slice(lines, int(section["end"]), lines.size()))
- output = _rewrite_legacy_descendant_headers(output, client)
-
- if not McpAtomicWrite.write(path, _join_lines(output)):
- return {"status": "error", "message": "Cannot write to %s" % path}
- return {"status": "ok", "message": McpClient.configured_message(client, server_url)}
-
-
-static func check_status(
- client: McpClient,
- server_name: String,
- server_url: String,
- launch: Dictionary = {},
-) -> McpClient.Status:
- return check_status_details(client, server_name, server_url, launch).get("status", McpClient.Status.NOT_CONFIGURED)
-
-
-static func check_status_details(
- client: McpClient,
- _server_name: String,
- server_url: String,
- launch: Dictionary = {},
-) -> Dictionary:
- var resolution := client.resolved_config_path_details()
- var path := str(resolution.get("path", ""))
- var path_error := str(resolution.get("error", ""))
- if not path_error.is_empty():
- return {"status": McpClient.Status.ERROR, "error_msg": path_error}
- if path.is_empty() or not FileAccess.file_exists(path):
- return {"status": McpClient.Status.NOT_CONFIGURED, "error_msg": ""}
- var read := _read_or_init(path)
- if not read["ok"]:
- return {"status": McpClient.Status.ERROR, "error_msg": String(read["error"])}
- var lines: Array[String] = _split_lines(String(read["data"]))
- var section := _find_section(lines, _all_headers(client))
- if section.is_empty():
- return {"status": McpClient.Status.NOT_CONFIGURED, "error_msg": ""}
-
- var items := _value_items(lines, int(section["start"]) + 1, int(section["end"]))
- var by_key := {}
- for item in items:
- var key := str(item.get("key", ""))
- if not key.is_empty() and not by_key.has(key):
- by_key[key] = item
- ## Whether the existing entry launches Godot AI at all: a major migration
- ## may rewrite such an entry, never a foreign one (client_job_owner.gd).
- ## Only the launch values count; the section header carries our name.
- var launch_tokens := PackedStringArray()
- for launch_key in ["command", "url"]:
- if by_key.has(launch_key):
- var decoded := _decode_toml_string(_item_value(by_key[launch_key]))
- if bool(decoded.get("ok", false)):
- launch_tokens.append(str(decoded.get("value", "")))
- if by_key.has("args"):
- var decoded_args := _decode_toml_string_array(_item_value(by_key["args"]))
- if bool(decoded_args.get("ok", false)):
- for argument in decoded_args.get("value", []):
- launch_tokens.append(str(argument))
- var mismatch := {
- "status": McpClient.Status.CONFIGURED_MISMATCH,
- "error_msg": "",
- "owned": McpClient.launch_values_mention_godot_ai(launch_tokens),
- }
-
- if client.command_shape != McpClient.CommandShape.NONE:
- if not bool(launch.get("ok", false)):
- return {
- "status": McpClient.Status.ERROR,
- "error_msg": str(launch.get("error", "No compatible attach launcher was found.")),
- }
- for legacy_key in client.command_legacy_keys:
- if by_key.has(String(legacy_key)):
- return mismatch
- if not by_key.has("command") or not by_key.has("args"):
- return mismatch
- var command_value := _decode_toml_string(_item_value(by_key["command"]))
- var args_value := _decode_toml_string_array(_item_value(by_key["args"]))
- if not bool(command_value.get("ok", false)) or not bool(args_value.get("ok", false)):
- return mismatch
- if str(command_value.get("value", "")) != str(launch.get("command", "")):
- return mismatch
- if not _string_arrays_equal(args_value.get("value", []), launch.get("args", [])):
- return mismatch
- if not client.command_transport_key.is_empty():
- var transport_key := client.command_transport_key
- if not by_key.has(transport_key):
- return mismatch
- var decoded_transport := _decode_toml_scalar(_item_value(by_key[transport_key]))
- if not bool(decoded_transport.get("ok", false)) or decoded_transport.get("value") != client.command_transport_value:
- return mismatch
- return {"status": McpClient.Status.CONFIGURED, "error_msg": ""}
-
- if not by_key.has("url"):
- return mismatch
- var url_value := _decode_toml_string(_item_value(by_key["url"]))
- if not bool(url_value.get("ok", false)) or str(url_value.get("value", "")) != server_url:
- return mismatch
- return {"status": McpClient.Status.CONFIGURED, "error_msg": ""}
-
-
-static func remove(client: McpClient, _server_name: String) -> Dictionary:
- var resolution := client.resolved_config_path_details()
- var path := str(resolution.get("path", ""))
- var path_error := str(resolution.get("error", ""))
- if not path_error.is_empty():
- return {"status": "error", "message": path_error}
- if path.is_empty() or not FileAccess.file_exists(path):
- return {"status": "ok", "message": "Not configured"}
- var read := _read_or_init(path)
- if not read["ok"]:
- return {"status": "error", "message": "Refusing to rewrite %s: %s." % [path, read["error"]]}
- var lines: Array[String] = _split_lines(String(read["data"]))
- var headers := _all_headers(client)
- var subtable_prefixes := _subtable_prefixes(headers)
-
- var output: Array[String] = []
- var i := 0
- while i < lines.size():
- if _matches_any_header(lines[i], headers) or _matches_subtable_prefix(lines[i], subtable_prefixes):
- i += 1
- while i < lines.size() and not _is_any_section_header(lines[i]):
- i += 1
- continue
- output.append(lines[i])
- i += 1
-
- if not McpAtomicWrite.write(path, _join_lines(output)):
- return {"status": "error", "message": "Cannot write to %s" % path}
- return {"status": "ok", "message": "%s configuration removed" % client.display_name}
-
-
-## Substitute `{url}` in every legacy URL body-template line.
-static func format_body(template: PackedStringArray, server_url: String) -> PackedStringArray:
- var out := PackedStringArray()
- for line in template:
- out.append(String(line).replace("{url}", server_url))
- return out
-
-
-## Encode a TOML basic string. This is intentionally public for the
-## cross-language fixture test that parses the rendered sample with tomllib.
-static func encode_basic_string(value: String) -> String:
- return '"%s"' % value.replace("\\", "\\\\").replace('"', '\\"').replace("\b", "\\b").replace("\t", "\\t").replace("\n", "\\n").replace("\f", "\\f").replace("\r", "\\r")
-
-
-## Multi-line string-array encoding used by command-shape entries.
-static func encode_string_array(values: Variant) -> Array[String]:
- var out: Array[String] = ["["]
- for value in values:
- out.append(" %s," % encode_basic_string(str(value)))
- out.append("]")
- return out
-
-
-static func render_body(client: McpClient, server_url: String, launch: Dictionary) -> Dictionary:
- if client.command_shape == McpClient.CommandShape.NONE:
- if client.toml_body_template.is_empty():
- return {"ok": false, "error": "%s descriptor missing toml_body_template" % client.display_name}
- var legacy_body := format_body(client.toml_body_template, server_url)
- var legacy_lines: Array[String] = []
- var pinned := {}
- var initial := {}
- for idx in range(legacy_body.size()):
- legacy_lines.append(String(legacy_body[idx]))
- var key := _line_key(String(client.toml_body_template[idx]))
- if key.is_empty():
- continue
- if String(client.toml_body_template[idx]).contains("{url}"):
- pinned[key] = true
- else:
- initial[key] = true
- return {
- "ok": true,
- "lines": legacy_lines,
- "pinned_keys": pinned,
- "initial_keys": initial,
- "removed_keys": {},
- }
-
- if client.command_shape != McpClient.CommandShape.COMMAND_ARRAY:
- return {"ok": false, "error": "%s uses a command shape not supported by TOML yet" % client.display_name}
- if not bool(launch.get("ok", false)):
- return {"ok": false, "error": str(launch.get("error", "No compatible attach launcher was found."))}
-
- var command_lines: Array[String] = [
- "command = %s" % encode_basic_string(str(launch.get("command", ""))),
- ]
- command_lines.append_array(_encode_assignment("args", launch.get("args", [])))
-
- var pinned_keys := {"command": true, "args": true}
- if not client.command_transport_key.is_empty():
- var encoded_transport := _encode_scalar(client.command_transport_value)
- if encoded_transport.is_empty():
- return {
- "ok": false,
- "error": "Unsupported TOML transport `%s` for %s" % [
- client.command_transport_key, client.display_name,
- ],
- }
- command_lines.append("%s = %s" % [client.command_transport_key, encoded_transport])
- pinned_keys[client.command_transport_key] = true
-
- var initial_keys := {}
- for key in client.command_initial_fields:
- var encoded := _encode_assignment(str(key), client.command_initial_fields[key])
- if encoded.is_empty():
- return {"ok": false, "error": "Unsupported TOML default `%s` for %s" % [key, client.display_name]}
- command_lines.append_array(encoded)
- initial_keys[str(key)] = true
-
- var removed_keys := {}
- for key in client.command_legacy_keys:
- removed_keys[String(key)] = true
- return {
- "ok": true,
- "lines": command_lines,
- "pinned_keys": pinned_keys,
- "initial_keys": initial_keys,
- "removed_keys": removed_keys,
- }
-
-
-static func _encode_assignment(key: String, value: Variant) -> Array[String]:
- if value is Array or value is PackedStringArray:
- var lines := encode_string_array(value)
- lines[0] = "%s = %s" % [key, lines[0]]
- return lines
- var encoded := _encode_scalar(value)
- var lines: Array[String] = []
- if not encoded.is_empty():
- lines.append("%s = %s" % [key, encoded])
- return lines
-
-
-static func _encode_scalar(value: Variant) -> String:
- if value is String:
- return encode_basic_string(value)
- if value is bool:
- return "true" if value else "false"
- if value is int or value is float:
- return str(value)
- return ""
-
-
-# --- span-aware merge helpers -------------------------------------------
-
-static func _value_items(lines: Array[String], from: int, to: int) -> Array[Dictionary]:
- var out: Array[Dictionary] = []
- var i := from
- while i < to:
- var key := _line_key(lines[i])
- if key.is_empty():
- out.append({"key": "", "lines": [lines[i]]})
- i += 1
- continue
- var end := _value_span_end(lines, i, to)
- out.append({"key": key, "lines": _slice(lines, i, end)})
- i = end
- return out
-
-
-static func _value_span_end(lines: Array[String], start: int, limit: int) -> int:
- var state := {"quote": "", "square": 0, "curly": 0, "escaped": false}
- for i in range(start, limit):
- var begin := 0
- if i == start:
- var eq := _assignment_equal(lines[i])
- begin = eq + 1 if eq >= 0 else 0
- _scan_toml_value_line(lines[i], begin, state)
- if str(state["quote"]).is_empty() and int(state["square"]) == 0 and int(state["curly"]) == 0:
- return i + 1
- return limit
-
-
-static func _scan_toml_value_line(line: String, begin: int, state: Dictionary) -> void:
- var i := begin
- while i < line.length():
- var quote := str(state["quote"])
- if quote == '"""' or quote == "'''":
- if line.substr(i).begins_with(quote):
- state["quote"] = ""
- i += 3
- continue
- if quote == '"""' and line.unicode_at(i) == 92 and not bool(state["escaped"]):
- state["escaped"] = true
- i += 1
- continue
- state["escaped"] = false
- i += 1
- continue
- if quote == '"' or quote == "'":
- var c := line.unicode_at(i)
- if quote == '"' and c == 92 and not bool(state["escaped"]):
- state["escaped"] = true
- i += 1
- continue
- if c == quote.unicode_at(0) and not bool(state["escaped"]):
- state["quote"] = ""
- state["escaped"] = false
- i += 1
- continue
-
- if line.substr(i).begins_with('"""'):
- state["quote"] = '"""'
- i += 3
- continue
- if line.substr(i).begins_with("'''"):
- state["quote"] = "'''"
- i += 3
- continue
- var c := line.unicode_at(i)
- if c == 34:
- state["quote"] = '"'
- elif c == 39:
- state["quote"] = "'"
- elif c == 35:
- break
- elif c == 91:
- state["square"] = int(state["square"]) + 1
- elif c == 93:
- state["square"] = maxi(0, int(state["square"]) - 1)
- elif c == 123:
- state["curly"] = int(state["curly"]) + 1
- elif c == 125:
- state["curly"] = maxi(0, int(state["curly"]) - 1)
- i += 1
-
-
-static func _line_key(line: String) -> String:
- var eq := _assignment_equal(line)
- if eq <= 0:
- return ""
- return line.substr(0, eq).strip_edges()
-
-
-static func _assignment_equal(line: String) -> int:
- var quote := 0
- var escaped := false
- for i in range(line.length()):
- var c := line.unicode_at(i)
- if quote != 0:
- if quote == 34 and c == 92 and not escaped:
- escaped = true
- continue
- if c == quote and not escaped:
- quote = 0
- escaped = false
- continue
- if c == 34 or c == 39:
- quote = c
- elif c == 35:
- return -1
- elif c == 61:
- return i
- return -1
-
-
-# --- semantic value decoding --------------------------------------------
-
-static func _item_value(item: Dictionary) -> String:
- var item_lines: Array = item.get("lines", [])
- if item_lines.is_empty():
- return ""
- var first := str(item_lines[0])
- var eq := _assignment_equal(first)
- if eq < 0:
- return ""
- var parts: Array[String] = [first.substr(eq + 1)]
- for i in range(1, item_lines.size()):
- parts.append(str(item_lines[i]))
- return "\n".join(parts)
-
-
-static func _decode_toml_scalar(raw: String) -> Dictionary:
- var cleaned := _without_comments(raw).strip_edges()
- if cleaned == "true":
- return {"ok": true, "value": true}
- if cleaned == "false":
- return {"ok": true, "value": false}
- var string_value := _decode_toml_string(cleaned)
- if bool(string_value.get("ok", false)):
- return string_value
- if cleaned.is_valid_int():
- return {"ok": true, "value": cleaned.to_int()}
- if cleaned.is_valid_float():
- return {"ok": true, "value": cleaned.to_float()}
- return {"ok": false}
-
-
-static func _decode_toml_string(raw: String) -> Dictionary:
- var cleaned := _without_comments(raw).strip_edges()
- if cleaned.length() >= 2 and cleaned.begins_with("'") and cleaned.ends_with("'"):
- return {"ok": true, "value": cleaned.substr(1, cleaned.length() - 2)}
- if cleaned.length() < 2 or not cleaned.begins_with('"') or not cleaned.ends_with('"'):
- return {"ok": false}
- var parsed: Variant = JSON.parse_string(cleaned)
- if parsed is String:
- return {"ok": true, "value": parsed}
- return {"ok": false}
-
-
-static func _decode_toml_string_array(raw: String) -> Dictionary:
- var cleaned := _without_comments(raw).strip_edges()
- if not cleaned.begins_with("["):
- return {"ok": false}
- var i := 1
- var values: Array[String] = []
- while true:
- i = _skip_space(cleaned, i)
- if i >= cleaned.length():
- return {"ok": false}
- if cleaned.unicode_at(i) == 93:
- i = _skip_space(cleaned, i + 1)
- return {"ok": i == cleaned.length(), "value": values}
- var parsed := _parse_string_at(cleaned, i)
- if not bool(parsed.get("ok", false)):
- return {"ok": false}
- values.append(str(parsed.get("value", "")))
- i = _skip_space(cleaned, int(parsed.get("next", i)))
- if i >= cleaned.length():
- return {"ok": false}
- var c := cleaned.unicode_at(i)
- if c == 44:
- i += 1
- continue
- if c == 93:
- continue
- return {"ok": false}
- return {"ok": false} # Unreachable; keeps GDScript's return analysis explicit.
-
-
-static func _parse_string_at(text: String, start: int) -> Dictionary:
- if start >= text.length():
- return {"ok": false}
- var quote := text.unicode_at(start)
- if quote != 34 and quote != 39:
- return {"ok": false}
- var i := start + 1
- var escaped := false
- while i < text.length():
- var c := text.unicode_at(i)
- if quote == 34 and c == 92 and not escaped:
- escaped = true
- i += 1
- continue
- if c == quote and not escaped:
- var raw := text.substr(start, i - start + 1)
- if quote == 39:
- return {"ok": true, "value": raw.substr(1, raw.length() - 2), "next": i + 1}
- var parsed: Variant = JSON.parse_string(raw)
- if parsed is String:
- return {"ok": true, "value": parsed, "next": i + 1}
- return {"ok": false}
- escaped = false
- i += 1
- return {"ok": false}
-
-
-static func _without_comments(raw: String) -> String:
- var out: Array[String] = []
- for line in raw.split("\n"):
- var quote := 0
- var escaped := false
- var kept := ""
- for i in range(line.length()):
- var c := line.unicode_at(i)
- if quote != 0:
- kept += line.substr(i, 1)
- if quote == 34 and c == 92 and not escaped:
- escaped = true
- continue
- if c == quote and not escaped:
- quote = 0
- escaped = false
- continue
- if c == 34 or c == 39:
- quote = c
- kept += line.substr(i, 1)
- elif c == 35:
- break
- else:
- kept += line.substr(i, 1)
- out.append(kept)
- return "\n".join(out)
-
-
-static func _skip_space(text: String, start: int) -> int:
- var i := start
- while i < text.length() and text.substr(i, 1) in [" ", "\t", "\r", "\n"]:
- i += 1
- return i
-
-
-static func _string_arrays_equal(left: Variant, right: Variant) -> bool:
- if not (left is Array or left is PackedStringArray):
- return false
- if not (right is Array or right is PackedStringArray):
- return false
- if left.size() != right.size():
- return false
- for i in range(left.size()):
- if str(left[i]) != str(right[i]):
- return false
- return true
-
-
-# --- file / section helpers ---------------------------------------------
-
-static func _read_or_init(path: String) -> Dictionary:
- if not FileAccess.file_exists(path):
- return {"ok": true, "data": ""}
- var f := FileAccess.open(path, FileAccess.READ)
- if f == null:
- var err := FileAccess.get_open_error()
- return {"ok": false, "error": "could not open for reading (error %d)" % err}
- var text := f.get_as_text()
- f.close()
- return {"ok": true, "data": text}
-
-
-static func _join_lines(lines: Array[String]) -> String:
- var joined := "\n".join(lines)
- return joined if joined.is_empty() or joined.ends_with("\n") else joined + "\n"
-
-
-static func _split_lines(content: String) -> Array[String]:
- var out: Array[String] = []
- for line in content.split("\n"):
- out.append(line)
- return out
-
-
-static func _slice(lines: Array[String], from: int, to: int) -> Array[String]:
- var out: Array[String] = []
- for i in range(from, to):
- out.append(lines[i])
- return out
-
-
-static func _primary_header(client: McpClient) -> String:
- var parts := client.toml_section_path
- if parts.size() < 2:
- return "[%s]" % ".".join(parts)
- var section := ".".join(McpClient._packed_slice(parts, 0, parts.size() - 1))
- var name := parts[parts.size() - 1]
- return "[%s.\"%s\"]" % [section, name]
-
-
-static func _all_headers(client: McpClient) -> Array[String]:
- var primary := _primary_header(client)
- var out: Array[String] = [primary]
- var bare := _bare_key_header(client)
- if not bare.is_empty() and bare != primary:
- out.append(bare)
- for legacy in client.toml_legacy_section_aliases:
- out.append("[%s]" % legacy)
- return out
-
-
-static func _bare_key_header(client: McpClient) -> String:
- var parts := client.toml_section_path
- if parts.is_empty():
- return ""
- for part in parts:
- if not _is_bare_key(String(part)):
- return ""
- return "[%s]" % ".".join(parts)
-
-
-static func _is_bare_key(value: String) -> bool:
- if value.is_empty():
- return false
- for i in range(value.length()):
- var c := value.unicode_at(i)
- var alpha := (c >= 65 and c <= 90) or (c >= 97 and c <= 122)
- var digit := c >= 48 and c <= 57
- if not (alpha or digit or c == 45 or c == 95):
- return false
- return true
-
-
-static func _subtable_prefixes(headers: Array[String]) -> Array[String]:
- var out: Array[String] = []
- for header in headers:
- if header.length() > 2 and header.ends_with("]"):
- out.append(header.substr(0, header.length() - 1) + ".")
- return out
-
-
-static func _matches_subtable_prefix(line: String, prefixes: Array[String]) -> bool:
- var trimmed := line.strip_edges()
- for prefix in prefixes:
- if not trimmed.begins_with(prefix):
- continue
- var rest := trimmed.substr(prefix.length())
- var bracket := rest.find("]")
- if bracket < 0:
- continue
- var remainder := rest.substr(bracket + 1).strip_edges()
- if remainder.is_empty() or remainder.begins_with("#"):
- return true
- return false
-
-
-static func _matches_any_header(line: String, headers: Array[String]) -> bool:
- var trimmed := line.strip_edges()
- for header in headers:
- if not trimmed.begins_with(header):
- continue
- var remainder := trimmed.substr(header.length()).strip_edges()
- if remainder.is_empty() or remainder.begins_with("#"):
- return true
- return false
-
-
-static func _find_section(lines: Array[String], headers: Array[String]) -> Dictionary:
- for i in range(lines.size()):
- if _matches_any_header(lines[i], headers):
- var end := lines.size()
- for j in range(i + 1, lines.size()):
- if _is_any_section_header(lines[j]):
- end = j
- break
- return {"start": i, "end": end}
- return {}
-
-
-static func _is_any_section_header(line: String) -> bool:
- var trimmed := line.strip_edges()
- if not trimmed.begins_with("["):
- return false
- var bracket := trimmed.find("]")
- if bracket < 0:
- return false
- var remainder := trimmed.substr(bracket + 1).strip_edges()
- return remainder.is_empty() or remainder.begins_with("#")
-
-
-static func _rewrite_legacy_descendant_headers(
- lines: Array[String], client: McpClient
-) -> Array[String]:
- if client.toml_legacy_section_aliases.is_empty():
- return lines
- var primary := _primary_header(client)
- var primary_prefix := primary.substr(0, primary.length() - 1) + "."
- var out: Array[String] = []
- for line in lines:
- var rewritten := line
- var trimmed := line.strip_edges()
- var indent_length := line.find("[")
- var indent := line.substr(0, indent_length) if indent_length >= 0 else ""
- for alias in client.toml_legacy_section_aliases:
- var legacy_prefix := "[%s." % String(alias)
- if trimmed.begins_with(legacy_prefix):
- rewritten = indent + primary_prefix + trimmed.substr(legacy_prefix.length())
- break
- out.append(rewritten)
- return out
diff --git a/addons/godot_ai/clients/_toml_strategy.gd.uid b/addons/godot_ai/clients/_toml_strategy.gd.uid
deleted file mode 100644
index 723cb79..0000000
--- a/addons/godot_ai/clients/_toml_strategy.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cwdvxgn0aurqv
diff --git a/addons/godot_ai/clients/_yaml_strategy.gd b/addons/godot_ai/clients/_yaml_strategy.gd
deleted file mode 100644
index becf4ac..0000000
--- a/addons/godot_ai/clients/_yaml_strategy.gd
+++ /dev/null
@@ -1,553 +0,0 @@
-@tool
-class_name McpYamlStrategy
-extends RefCounted
-
-## Minimal YAML upsert for Hermes Agent MCP config.
-##
-## Hermes reads MCP servers from ~/.hermes/config.yaml under the
-## `mcp_servers` key (snake_case, YAML). HTTP entries are transport-inferred:
-## just `url` (plus optional `headers`), no `type` field. We only parse the
-## `mcp_servers` block and re-emit it; other top-level keys in the user's
-## config.yaml are preserved verbatim by round-tripping the raw lines around
-## that block. No general YAML parser — Godot has none in stdlib, and Hermes
-## only needs this one shape. See issue #640.
-
-const INDENT := " " # YAML forbids tab indentation; match the 2-space style of ~/.hermes/config.yaml
-
-
-static func configure(
- client: McpClient,
- server_name: String,
- server_url: String,
- launch: Dictionary = {},
-) -> Dictionary:
- var resolution := client.resolved_config_path_details()
- var path := str(resolution.get("path", ""))
- var path_error := str(resolution.get("error", ""))
- if not path_error.is_empty():
- return {"status": "error", "message": path_error}
- if path.is_empty():
- return {"status": "error", "message": "Could not resolve config path for %s on this OS" % client.display_name}
- ## Fail closed before touching the file — same contract as JSON/TOML.
- var launch_error := command_launch_error(client, launch)
- if not launch_error.is_empty():
- return {"status": "error", "message": launch_error}
-
- var seed_path := str(resolution.get("seed_path", ""))
- var read_path := seed_path if not FileAccess.file_exists(path) and not seed_path.is_empty() else path
- var read := _read(read_path)
- if not read["ok"]:
- return {"status": "error", "message": "Refusing to overwrite %s: %s. Fix or move the file, then re-run Configure." % [read_path, read["error"]]}
-
- var text: String = read["data"]
- var block := _extract_block(text)
- var entries: Dictionary = block["entries"]
-
- # Preserve existing entry's user-mutable keys; force the transport keys.
- var existing: Dictionary = entries.get(server_name, {})
- var new_entry := build_entry(client, server_url, existing, launch)
- entries[server_name] = new_entry
-
- var out := _assemble(text, block["prefix_lines"], entries, block["suffix_lines"])
- if not McpAtomicWrite.write(path, out):
- return {"status": "error", "message": "Cannot write to %s" % path}
- return {"status": "ok", "message": McpClient.configured_message(client, server_url)}
-
-
-static func check_status(
- client: McpClient, server_name: String, server_url: String, launch: Dictionary = {}
-) -> McpClient.Status:
- return check_status_details(client, server_name, server_url, launch)["status"]
-
-
-## Same contract as the JSON/TOML strategies (#711): {status, error_msg}.
-## An existing-but-unreadable config is ERROR with the diagnostic — not
-## NOT_CONFIGURED — so the dock row can tell "no config" from "config the
-## editor can't read" instead of offering a Configure that would fail.
-static func check_status_details(
- client: McpClient, server_name: String, server_url: String, launch: Dictionary = {}
-) -> Dictionary:
- var resolution := client.resolved_config_path_details()
- var path := str(resolution.get("path", ""))
- var path_error := str(resolution.get("error", ""))
- if not path_error.is_empty():
- return {"status": McpClient.Status.ERROR, "error_msg": path_error}
- if path.is_empty() or not FileAccess.file_exists(path):
- return {"status": McpClient.Status.NOT_CONFIGURED, "error_msg": ""}
- var read := _read(path)
- if not read["ok"]:
- return {
- "status": McpClient.Status.ERROR,
- "error_msg": "Cannot read %s: %s" % [path, read["error"]],
- }
- var block := _extract_block(String(read["data"]))
- var entries: Dictionary = block["entries"]
- if not entries.has(server_name):
- return {"status": McpClient.Status.NOT_CONFIGURED, "error_msg": ""}
- var entry: Variant = entries[server_name]
- if not (entry is Dictionary):
- return {"status": McpClient.Status.NOT_CONFIGURED, "error_msg": ""}
- ## An entry exists but no verified launcher does — mirror JSON/TOML: this
- ## is an environment ERROR, not entry drift.
- var launch_error := command_launch_error(client, launch)
- if not launch_error.is_empty():
- return {"status": McpClient.Status.ERROR, "error_msg": launch_error}
- if verify_entry(client, entry, server_url, launch):
- return {"status": McpClient.Status.CONFIGURED, "error_msg": ""}
- return {
- "status": McpClient.Status.CONFIGURED_MISMATCH,
- "error_msg": "",
- "owned": McpClient.launch_values_mention_godot_ai(McpClient.entry_launch_values(entry)),
- }
-
-
-static func remove(client: McpClient, server_name: String) -> Dictionary:
- var resolution := client.resolved_config_path_details()
- var path := str(resolution.get("path", ""))
- var path_error := str(resolution.get("error", ""))
- if not path_error.is_empty():
- return {"status": "error", "message": path_error}
- if path.is_empty() or not FileAccess.file_exists(path):
- return {"status": "ok", "message": "Not configured"}
- var read := _read(path)
- if not read["ok"]:
- return {"status": "error", "message": "Refusing to rewrite %s: %s." % [path, read["error"]]}
- var text: String = read["data"]
- var block := _extract_block(text)
- var entries: Dictionary = block["entries"]
- if not entries.has(server_name):
- return {"status": "ok", "message": "%s configuration removed" % client.display_name}
- entries.erase(server_name)
- var out := _assemble(text, block["prefix_lines"], entries, block["suffix_lines"])
- if not McpAtomicWrite.write(path, out):
- return {"status": "error", "message": "Cannot write to %s" % path}
- return {"status": "ok", "message": "%s configuration removed" % client.display_name}
-
-
-## Build the entry dict written under mcp_servers[server_name].
-##
-## URL mode (`CommandShape.NONE`): Hermes HTTP entries are transport-inferred
-## — { url: } plus whatever user-mutable keys (headers, enabled, tools,
-## ...) the existing entry carries. Stdio-bridge keys are the one exception
-## (see _STDIO_BRIDGE_KEYS). No `type` field.
-##
-## Command mode (`CommandShape.FLAT`, #838): flat `command` + `args` keys,
-## transport inferred the same way — which is exactly why the descriptor's
-## `command_legacy_keys` (url, headers) must be scrubbed: a Hermes entry with
-## both a url and a command picks the wrong transport. User keys survive.
-static func build_entry(
- client: McpClient,
- server_url: String,
- existing: Variant = null,
- launch: Dictionary = {},
-) -> Dictionary:
- if client.command_shape == McpClient.CommandShape.FLAT:
- var command_entry: Dictionary = (existing as Dictionary).duplicate(true) if existing is Dictionary else {}
- command_entry["command"] = str(launch.get("command", ""))
- command_entry["args"] = _array_copy(launch.get("args", []))
- if not client.command_transport_key.is_empty():
- command_entry[client.command_transport_key] = client.command_transport_value
- for key in client.command_initial_fields:
- if not command_entry.has(key):
- command_entry[key] = client.command_initial_fields[key]
- for key in client.command_legacy_keys:
- command_entry.erase(String(key))
- return command_entry
- if client.command_shape != McpClient.CommandShape.NONE:
- ## Every production caller checks command_launch_error first. Keep this
- ## builder defensive too so a future unsupported shape cannot silently
- ## degrade into a flat YAML entry.
- return {}
- var entry: Dictionary = {}
- if existing is Dictionary:
- ## User-mutable keys (headers, enabled, tools, ...) survive a
- ## reconfigure — the same preservation contract the JSON strategy's
- ## entry_initial_fields split implements. Only the stdio-bridge keys
- ## are scrubbed (see _STDIO_BRIDGE_KEYS), then the url is repointed.
- entry = (existing as Dictionary).duplicate(true)
- for stale_key in _STDIO_BRIDGE_KEYS:
- entry.erase(stale_key)
- entry[client.entry_url_field] = server_url
- return entry
-
-
-## Keys a prior stdio-bridge entry (e.g. `command: uvx mcp-proxy`) may carry.
-## These must NOT survive a URL reconfigure: a Hermes entry with both a url
-## and a command picks the wrong transport.
-const _STDIO_BRIDGE_KEYS := ["command", "args", "env"]
-
-
-## Empty string when this client's launch requirements are satisfied.
-## Mirrors `McpJsonStrategy.command_launch_error`; YAML supports FLAT only.
-static func command_launch_error(client: McpClient, launch: Dictionary) -> String:
- if client.command_shape == McpClient.CommandShape.NONE:
- return ""
- if client.command_shape != McpClient.CommandShape.FLAT:
- return "%s uses a command shape not supported by YAML yet" % client.display_name
- if not bool(launch.get("ok", false)):
- return str(launch.get("error", "No compatible attach launcher was found."))
- return ""
-
-
-## Verify a stored entry matches.
-##
-## URL mode: Hermes entries have no transport type pin, so verification is:
-## url matches. Extra keys (headers, enabled, tools) are user-mutable and
-## intentionally NOT checked (mirrors json entry_initial_fields).
-##
-## Command mode: every launch-affecting value must match exactly and legacy
-## URL-transport keys must be gone — their presence is migration drift.
-static func verify_entry(
- client: McpClient,
- entry: Dictionary,
- server_url: String,
- launch: Dictionary = {},
-) -> bool:
- if client.command_shape != McpClient.CommandShape.NONE:
- if client.command_shape != McpClient.CommandShape.FLAT or not bool(launch.get("ok", false)):
- return false
- for key in client.command_legacy_keys:
- if entry.has(String(key)):
- return false
- if entry.get("command") != launch.get("command"):
- return false
- if not _arrays_equal(entry.get("args", null), launch.get("args", null)):
- return false
- if not client.command_transport_key.is_empty():
- if entry.get(client.command_transport_key, null) != client.command_transport_value:
- return false
- return true
- return entry.get(client.entry_url_field, "") == server_url
-
-
-static func _array_copy(value: Variant) -> Array:
- if value is Array:
- return (value as Array).duplicate(true)
- if value is PackedStringArray:
- return McpClient._array_from_packed(value)
- return []
-
-
-static func _arrays_equal(left: Variant, right: Variant) -> bool:
- if not (left is Array or left is PackedStringArray):
- return false
- if not (right is Array or right is PackedStringArray):
- return false
- var left_array := _array_copy(left)
- var right_array := _array_copy(right)
- if left_array.size() != right_array.size():
- return false
- for i in range(left_array.size()):
- if left_array[i] != right_array[i]:
- return false
- return true
-
-
-# --- YAML block handling (scoped to mcp_servers) -------------------------
-
-## Parse the file into three regions:
-## prefix_lines — everything before `mcp_servers:` (may be empty)
-## entries — the map of server_name -> {url, ...} under mcp_servers
-## suffix_lines — everything after the mcp_servers block (may be empty)
-## This lets us rewrite only the mcp_servers block and keep the rest of the
-## user's config.yaml byte-for-byte intact.
-static func _extract_block(text: String) -> Dictionary:
- ## allow_empty must stay true: dropping empty splits would silently strip
- ## the user's blank lines from the preserved prefix/suffix regions on
- ## every rewrite. The parse loops below already skip blank lines.
- var lines := text.split("\n")
- var prefix: PackedStringArray = []
- var entries: Dictionary = {}
- var suffix: PackedStringArray = []
- var header_idx := -1
- for i in range(lines.size()):
- if lines[i].strip_edges().begins_with("mcp_servers:"):
- header_idx = i
- break
- if header_idx < 0:
- # No mcp_servers yet — whole file is prefix; block will be appended.
- prefix = lines.duplicate()
- return {"prefix_lines": prefix, "entries": entries, "suffix_lines": [], "header_idx": -1}
-
- for i in range(0, header_idx):
- prefix.append(lines[i])
-
- # Determine the indent of the first entry so we can tell sibling
- # entries (same indent) apart from nested keys (deeper indent) and
- # parent-level keys (less indent). All server entries under
- # `mcp_servers:` share one indent level; breaking on 0-indent alone
- # mis-nests 2-space-indented siblings under the first entry.
- var entry_indent := -1
- var probe := header_idx + 1
- while probe < lines.size() and _is_blank_or_comment(lines[probe]):
- probe += 1
- if probe < lines.size():
- entry_indent = _indent_of(lines[probe])
-
- # Empty block guard: the first nonblank line after the header must sit
- # DEEPER than the header itself to be an entry. At or above the header's
- # indent it is a sibling/parent key — parsing it as an entry would
- # swallow the user's next top-level key and re-emit it nested under
- # mcp_servers, corrupting the file.
- if probe < lines.size() and entry_indent <= _indent_of(lines[header_idx]):
- for j in range(header_idx + 1, lines.size()):
- suffix.append(lines[j])
- return {"prefix_lines": prefix, "entries": entries, "suffix_lines": suffix, "header_idx": header_idx}
-
- var i := header_idx + 1
- while i < lines.size():
- var raw := lines[i]
- ## Comment-only lines inside the block are skipped like blanks —
- ## treating one as an entry header would re-emit it as a bogus
- ## `# comment:` server on rewrite. (Comments INSIDE the rewritten
- ## block are consequently dropped; comments outside the block live
- ## in prefix/suffix and survive verbatim.)
- if _is_blank_or_comment(raw):
- i += 1
- continue
- # Stop at any line indented less than a sibling entry (parent key
- # or a new top-level section), or at the header's own level.
- if _indent_of(raw) < entry_indent:
- break
- var entry := _parse_entry(raw, lines, i, entry_indent)
- if not entry["name"].is_empty():
- entries[entry["name"]] = entry["data"]
- i = entry["next_idx"]
-
- for j in range(i, lines.size()):
- suffix.append(lines[j])
-
- return {"prefix_lines": prefix, "entries": entries, "suffix_lines": suffix, "header_idx": header_idx}
-
-
-## Parse one ` name:` entry starting at `lines[start]`. Consumes all deeper-
-## indented sublines (url, headers, etc.) and returns the next sibling index.
-static func _parse_entry(raw: String, lines: PackedStringArray, start: int, entry_indent: int) -> Dictionary:
- var name := raw.strip_edges().trim_suffix(":").strip_edges()
- var data: Dictionary = {}
- var i := start + 1
- while i < lines.size():
- var l := lines[i]
- ## Comments inside an entry (e.g. ` # auth for CI`) would parse
- ## as a `# auth for CI` key — skip them like blanks.
- if _is_blank_or_comment(l):
- i += 1
- continue
- # A line at or above the entry's indent is a sibling/parent key.
- if _indent_of(l) <= entry_indent:
- break
- var stripped := l.strip_edges()
- var colon := stripped.find(":")
- if colon < 0:
- i += 1
- continue
- var key := stripped.substr(0, colon).strip_edges()
- var val := stripped.substr(colon + 1).strip_edges()
- if val.is_empty():
- # Nested block (e.g. headers:). Parse as raw sub-dict lines for
- # preservation; we don't introspect deeper than url at the top.
- var sub := _parse_subblock(lines, i + 1, entry_indent)
- data[key] = sub["value"]
- i = sub["next_idx"]
- else:
- data[key] = coerce_scalar(val)
- i += 1
- return {"name": name, "data": data, "next_idx": i}
-
-
-## Parse a nested block (e.g. headers:) as a preserved sub-dictionary of
-## scalar key/values. Deeper nesting is flattened into scalar strings — fine
-## for Hermes' known shape (headers are flat key: value).
-static func _parse_subblock(lines: PackedStringArray, start: int, entry_indent: int) -> Dictionary:
- var sub: Dictionary = {}
- var i := start
- while i < lines.size():
- var l := lines[i]
- if _is_blank_or_comment(l):
- i += 1
- continue
- # A line at or above the parent entry's indent ends the nested block.
- if _indent_of(l) <= entry_indent:
- break
- var stripped := l.strip_edges()
- var colon := stripped.find(":")
- if colon < 0:
- i += 1
- continue
- var key := stripped.substr(0, colon).strip_edges()
- var val := stripped.substr(colon + 1).strip_edges()
- if val.is_empty():
- i += 1
- continue
- sub[key] = coerce_scalar(val)
- i += 1
- return {"value": sub, "next_idx": i}
-
-
-## Reassemble the full file text from prefix + a freshly built mcp_servers
-## block + suffix. If the block didn't exist before, it is appended.
-static func _assemble(_text: String, prefix: PackedStringArray, entries: Dictionary, suffix: PackedStringArray) -> String:
- var out: PackedStringArray = []
- for l in prefix:
- out.append(l)
- # Trim trailing blank lines from prefix so we don't stack double blanks.
- while out.size() > 0 and out[out.size() - 1].strip_edges().is_empty():
- out.remove_at(out.size() - 1)
-
- if not _text.contains("mcp_servers:"):
- # File existed but had no mcp_servers block — append it.
- if out.size() > 0:
- out.append("")
- out.append("mcp_servers:")
- for name in entries:
- out.append_array(_emit_entry(name, entries[name]))
- else:
- out.append("mcp_servers:")
- for name in entries:
- out.append_array(_emit_entry(name, entries[name]))
-
- # Suffix: keep as-is.
- for l in suffix:
- out.append(l)
- return "\n".join(out)
-
-
-## Public rendering seam for the dock's manual-instruction text, so the
-## pasted YAML matches what Configure would write byte-for-byte.
-static func render_entry_lines(name: String, data: Dictionary) -> PackedStringArray:
- return _emit_entry(name, data)
-
-
-## Emit one ` name:` entry with its scalar keys (top level only; headers
-## sub-dict is re-emitted as nested scalars; arrays — the command entry's
-## `args` — are emitted in flow style on one line).
-static func _emit_entry(name: String, data: Dictionary) -> PackedStringArray:
- var lines: PackedStringArray = []
- lines.append(INDENT + "%s:" % name)
- for key in data:
- var val = data[key]
- if val is Dictionary:
- lines.append(INDENT + INDENT + "%s:" % key)
- for sk in val:
- lines.append(INDENT + INDENT + INDENT + "%s: %s" % [sk, emit_scalar(val[sk])])
- elif val is Array or val is PackedStringArray:
- lines.append(INDENT + INDENT + "%s: %s" % [key, emit_flow_array(_array_copy(val))])
- else:
- lines.append(INDENT + INDENT + "%s: %s" % [key, emit_scalar(val)])
- return lines
-
-
-## Flow-style sequence with every item double-quoted. JSON string quoting is
-## valid YAML double-quote style (shared escape set), so the same encoding
-## both writes the file and — via JSON.parse_string in `coerce_scalar` —
-## reads it back for verification. Shared with `McpDshStrategy` for the
-## DeepSeek Harness patch-list `args` lines.
-static func emit_flow_array(values: Array) -> String:
- var parts: Array[String] = []
- for v in values:
- parts.append(JSON.stringify(str(v)))
- return "[%s]" % ", ".join(parts)
-
-
-static func emit_scalar(v: Variant) -> String:
- match typeof(v):
- TYPE_BOOL:
- return "true" if bool(v) else "false"
- TYPE_INT:
- return str(int(v))
- TYPE_FLOAT:
- return str(float(v))
- _:
- return emit_string_scalar(str(v))
-
-
-## Plain YAML scalars cannot safely carry ": ", " #", quotes, flow
-## indicators, or leading indicator characters — a Windows launcher path with
-## spaces would silently corrupt the entry. Quote exactly when needed so
-## existing plain values (urls, bools-as-strings) keep their current
-## byte-shape on rewrite. Backslashes are quoted too: a Windows path parses
-## either way, but quoting makes the written shape unambiguous for any
-## consumer. Shared with `McpDshStrategy` for DeepSeek Harness patch entries.
-static func emit_string_scalar(s: String) -> String:
- if s.is_empty():
- return "\"\""
- var needs_quote := s.begins_with(" ") or s.ends_with(" ")
- if not needs_quote:
- for needle in [": ", " #", "\"", "'", "\n", "\t", "\\", "{", "}", "[", "]", ","]:
- if s.contains(needle):
- needs_quote = true
- break
- if not needs_quote:
- for prefix in ["#", "-", "?", "&", "*", "!", "|", ">", "%", "@", "`"]:
- if s.begins_with(prefix):
- needs_quote = true
- break
- return JSON.stringify(s) if needs_quote else s
-
-
-## Blank and comment-only lines carry no structure — every scan loop skips
-## them the same way so a `# comment` can never be mistaken for an entry
-## header or a key/value line.
-static func _is_blank_or_comment(line: String) -> bool:
- var stripped := line.strip_edges()
- return stripped.is_empty() or stripped.begins_with("#")
-
-
-## Returns the leading-whitespace indent width of a line (spaces + tabs
-## counted as 1 each). Used to distinguish sibling entries (same indent)
-## from nested keys (deeper indent) and parent-level keys (less indent).
-static func _indent_of(line: String) -> int:
- var n := 0
- while n < line.length() and (line[n] == " " or line[n] == "\t"):
- n += 1
- return n
-
-
-## Minimal scalar coercion for parsed YAML values. Quotes are stripped;
-## bare true/false/numbers are typed; double-quoted flow sequences (the
-## command entry's `args`) parse back into an Array. Good enough for Hermes'
-## url/headers/command/args. A hand-edited args in block style or with
-## unquoted items doesn't JSON-parse — it stays a raw string, compares
-## unequal, and surfaces as CONFIGURED_MISMATCH, which Reconfigure
-## normalizes back to the flow form. Public: the dsh strategy parses its
-## patch-list scalars with the same rules, so both YAML dialects share one
-## coercion (#867 review).
-static func coerce_scalar(s: String) -> Variant:
- var t := s.strip_edges()
- if t.begins_with("[") and t.ends_with("]"):
- var parsed_array: Variant = JSON.parse_string(t)
- if parsed_array is Array:
- return parsed_array
- return t
- if t.begins_with("\"") and t.ends_with("\""):
- var parsed_string: Variant = JSON.parse_string(t)
- if parsed_string is String:
- return parsed_string
- return t.substr(1, t.length() - 2)
- if t.begins_with("'") and t.ends_with("'"):
- return t.substr(1, t.length() - 2)
- if t == "true":
- return true
- if t == "false":
- return false
- if t.is_valid_int():
- return t.to_int()
- if t.is_valid_float():
- return t.to_float()
- return t
-
-
-## Returns {"ok": true, "data": String} when the file is absent or readable,
-## and {"ok": false, "error": String} when unreadable. Callers must NOT fall
-## back to an empty string on the error path — doing so blows away the user's
-## other config.yaml entries on the next write.
-static func _read(path: String) -> Dictionary:
- if not FileAccess.file_exists(path):
- return {"ok": true, "data": ""}
- var f := FileAccess.open(path, FileAccess.READ)
- if f == null:
- var err := FileAccess.get_open_error()
- return {"ok": false, "error": "could not open for reading (%s)" % error_string(err)}
- var t := f.get_as_text()
- f.close()
- return {"ok": true, "data": t}
diff --git a/addons/godot_ai/clients/_yaml_strategy.gd.uid b/addons/godot_ai/clients/_yaml_strategy.gd.uid
deleted file mode 100644
index d8adce8..0000000
--- a/addons/godot_ai/clients/_yaml_strategy.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cfnw4oe71ra1l
diff --git a/addons/godot_ai/clients/antigravity.gd b/addons/godot_ai/clients/antigravity.gd
deleted file mode 100644
index ce82681..0000000
--- a/addons/godot_ai/clients/antigravity.gd
+++ /dev/null
@@ -1,38 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "antigravity"
- display_name = "Antigravity"
- config_type = "json"
- ## Antigravity moved its shared MCP config from `~/.gemini/antigravity/`
- ## to `~/.gemini/config/` (IDE + CLI now read the same file there); the
- ## old path is left in `detect_paths` below so an existing install is
- ## still recognized, but new/updated entries write to the current path.
- path_template = {
- "unix": "~/.gemini/config/mcp_config.json",
- "windows": "$USERPROFILE/.gemini/config/mcp_config.json",
- }
- server_key_path = PackedStringArray(["mcpServers"])
- entry_url_field = "serverUrl"
- ## `disabled` is user-state (they may have flipped the entry off in the
- ## UI); seeded on first Configure but preserved across reconfigure.
- entry_initial_fields = {"disabled": false}
- ## Attach migration (#838). Antigravity stdio entries are flat
- ## command/args/env with no type discriminator — transport is inferred
- ## from `command` vs `serverUrl` presence (antigravity.google/docs/mcp),
- ## so the legacy `serverUrl` must not survive next to a command.
- command_shape = McpClient.CommandShape.FLAT
- command_legacy_keys = PackedStringArray(["serverUrl"])
- command_initial_fields = {"disabled": false}
- command_user_fields = PackedStringArray(["disabled", "disabledTools", "authProviderType", "env"])
- ## Antigravity's spawner hangs stdio tool calls when the entry launches a
- ## GUI-subsystem pythonw.exe (#863), and it hides child console windows
- ## itself, so the visible-terminal problem the bootstrap solves (#827)
- ## never applies. Write the plain console launcher on Windows.
- needs_consoleless_launcher = false
- detect_paths = PackedStringArray(path_template.values() + [
- "~/.gemini/antigravity/mcp_config.json",
- "$USERPROFILE/.gemini/antigravity/mcp_config.json",
- ])
diff --git a/addons/godot_ai/clients/antigravity.gd.uid b/addons/godot_ai/clients/antigravity.gd.uid
deleted file mode 100644
index 0721a2f..0000000
--- a/addons/godot_ai/clients/antigravity.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b4l1g0apa2hch
diff --git a/addons/godot_ai/clients/claude_code.gd b/addons/godot_ai/clients/claude_code.gd
deleted file mode 100644
index de4a5db..0000000
--- a/addons/godot_ai/clients/claude_code.gd
+++ /dev/null
@@ -1,81 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "claude_code"
- display_name = "Claude Code"
- config_type = "cli"
- cli_names = PackedStringArray(["claude", "claude.exe"] if OS.get_name() == "Windows" else ["claude"])
- ## Stdio registration through the client-owned `godot-ai attach` bridge
- ## (#838). `--` stops claude's own flag parsing so the attach argv passes
- ## through verbatim; stdio is the CLI's default transport. Scope comes from
- ## the `godot_ai/mcp_client_scope` EditorSetting via the `{scope}` token; it
- ## defaults to `user` — the same ~/.claude.json the pre-attach HTTP entry
- ## lived in — and can be set to `project` to write /.mcp.json so the
- ## server is not loaded in every unrelated workspace.
- cli_register_template = PackedStringArray(
- ["mcp", "add", "--scope", "{scope}", "{name}", "--", "{command}", "{args...}"]
- )
- ## Explicit scope: an unscoped `mcp remove` deletes from whichever scope
- ## matches first, so the flag keeps each removal aimed at one known place.
- ## Note what that does and does not protect (#877): the dock's **Remove**
- ## button runs this once, at the selected scope only. **Configure** runs it
- ## once per scope in `CLIENT_SCOPES` before registering, so a Configure at
- ## any setting does delete a `godot-ai` entry from /.mcp.json if one is
- ## there — see the cwd caveat below for which directory that actually is.
- cli_unregister_template = PackedStringArray(["mcp", "remove", "--scope", "{scope}", "{name}"])
- ## Scope caveat: `--scope project` resolves `.mcp.json` against the
- ## *spawned CLI's* working directory, which is whatever the editor process
- ## inherited at launch — not necessarily `res://`. Godot has no API to set
- ## a child's cwd (`OS.execute_with_pipe` takes none), so this is an
- ## accepted limitation rather than something the descriptor can pin. It is
- ## self-consistent: `cli_status_args` runs from the same cwd, so the
- ## read-back finds whatever the register wrote (see
- ## `_scope_diverges_from_json_fallback` in client_configurator.gd).
- cli_status_args = PackedStringArray(["mcp", "list"])
- ## `mcp list` prints the resolved entry for the cwd without saying which
- ## scope won, so it cannot tell "registered at project scope" from "an old
- ## user-scope entry is shadowing an empty project scope". `mcp get` prints
- ## both the command and a `Scope: …` line, in the same single subprocess,
- ## so scope-token clients probe with it instead (#872). Verified against
- ## claude 2.1.241, which prints one of:
- ## Scope: User config (available in all your projects)
- ## Scope: Project config (shared via .mcp.json)
- ## Scope: Local config (private to you in this project)
- ## and exits 1 with "No MCP server named …" when the entry is absent.
- cli_scope_status_template = PackedStringArray(["mcp", "get", "{name}"])
- ## #463: JSON fallback for when the `claude` binary isn't on PATH — e.g.
- ## Claude Code installed only as a VS Code / Cursor extension. The CLI is
- ## still preferred for Configure whenever it resolves; this is what gets
- ## written otherwise. `claude mcp add --scope user -- `
- ## produces exactly this shape under `mcpServers` in ~/.claude.json
- ## (verified live against claude CLI in an isolated CLAUDE_CONFIG_DIR):
- ## "godot-ai": { "type": "stdio", "command": "", "args": [...], "env": {} }
- ## The fallback writer omits the empty `env`; the verifier accepts both.
- ## Status reads this file only while the selected scope is still `user` —
- ## it is the CLI's own store for that scope, and file reads give exact
- ## launch-drift detection that `mcp list` stdout scanning cannot. At
- ## `project`/`local` the entry is somewhere `path_template` cannot see, so
- ## `_scope_diverges_from_json_fallback` (client_configurator.gd) routes
- ## status to `cli_scope_status_template` instead and this file is not read.
- path_template = {"unix": "~/.claude.json", "windows": "~/.claude.json"}
- server_key_path = PackedStringArray(["mcpServers"])
- ## Legacy HTTP entries used this shape; command rendering repins it to stdio.
- entry_extra_fields = {"type": "http"}
- command_shape = McpClient.CommandShape.FLAT
- command_transport_key = "type"
- command_transport_value = "stdio"
- ## Legacy HTTP entries carried a `url`; Claude Code rejects an entry mixing
- ## url with command fields, and the stale `type: "http"` is repinned to
- ## "stdio" by the transport key above.
- command_legacy_keys = PackedStringArray(["url"])
- command_user_fields = PackedStringArray(["env"])
- ## Documented: $CLAUDE_CONFIG_DIR relocates Claude Code's config home,
- ## including .claude.json ($CLAUDE_CONFIG_DIR/.claude.json). The preferred
- ## CLI path needs no help — the spawned `claude` binary inherits the
- ## editor's environment and resolves the dir itself — but the JSON
- ## fallback above would otherwise write ~/.claude.json that a relocated
- ## install never reads (#617).
- config_home_env = "CLAUDE_CONFIG_DIR"
- config_home_env_subpath = ".claude.json"
diff --git a/addons/godot_ai/clients/claude_code.gd.uid b/addons/godot_ai/clients/claude_code.gd.uid
deleted file mode 100644
index 3d3335f..0000000
--- a/addons/godot_ai/clients/claude_code.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cp1u1hdpa6f8d
diff --git a/addons/godot_ai/clients/claude_desktop.gd b/addons/godot_ai/clients/claude_desktop.gd
deleted file mode 100644
index c1aa9bb..0000000
--- a/addons/godot_ai/clients/claude_desktop.gd
+++ /dev/null
@@ -1,38 +0,0 @@
-@tool
-extends McpClient
-
-## Claude Desktop's mcpServers entries launch a local stdio process. The
-## client-owned `godot-ai attach` bridge keeps that stdio session stable while
-## adopting or starting the shared HTTP backend as Godot editors come and go.
-
-
-func _init() -> void:
- id = "claude_desktop"
- display_name = "Claude Desktop"
- config_type = "json"
- path_template = {
- "darwin": "~/Library/Application Support/Claude/claude_desktop_config.json",
- "windows": "$APPDATA/Claude/claude_desktop_config.json",
- "linux": "$XDG_CONFIG_HOME/Claude/claude_desktop_config.json",
- }
- ## Store-installed Claude runs inside MSIX AppData virtualization. Godot is
- ## outside that container, so `%APPDATA%` names a different physical file
- ## once Claude has created its private copy. A unique Store package root is
- ## authoritative even before the config leaf exists: create the private file
- ## directly so a later copy-on-write cannot hide an entry written to roaming.
- ## With no Store package, use the conventional roaming path. The wildcard
- ## avoids coupling to the publisher-hash suffix.
- config_path_candidates = {
- "windows": [
- "$LOCALAPPDATA/Packages/Claude_*/LocalCache/Roaming/Claude/claude_desktop_config.json",
- "$APPDATA/Claude/claude_desktop_config.json",
- ],
- }
- detect_paths = PackedStringArray([
- "$LOCALAPPDATA/Packages/Claude_*",
- ])
- server_key_path = PackedStringArray(["mcpServers"])
- command_shape = McpClient.CommandShape.FLAT
- command_legacy_keys = PackedStringArray(["url"])
- command_env_legacy_keys = PackedStringArray(["UV_LINK_MODE"])
- command_user_fields = PackedStringArray(["env", "disabled"])
diff --git a/addons/godot_ai/clients/claude_desktop.gd.uid b/addons/godot_ai/clients/claude_desktop.gd.uid
deleted file mode 100644
index 9759a1a..0000000
--- a/addons/godot_ai/clients/claude_desktop.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bilntn5n8oqe3
diff --git a/addons/godot_ai/clients/cline.gd b/addons/godot_ai/clients/cline.gd
deleted file mode 100644
index 9e827b6..0000000
--- a/addons/godot_ai/clients/cline.gd
+++ /dev/null
@@ -1,42 +0,0 @@
-@tool
-extends McpClient
-
-## Cline is a VS Code extension. Its MCP settings live in VS Code's
-## globalStorage under the extension id `saoudrizwan.claude-dev`.
-
-
-func _init() -> void:
- id = "cline"
- display_name = "Cline"
- config_type = "json"
- path_template = {
- "darwin": "~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
- "windows": "$APPDATA/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
- "linux": "$XDG_CONFIG_HOME/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
- }
- server_key_path = PackedStringArray(["mcpServers"])
- ## Cline (like Roo) defaults a typeless entry to SSE transport, which
- ## returns HTTP 400 against our streamable-http endpoint on `/mcp`. Pin
- ## the type explicitly. Cline's schema uses "streamableHttp" (camelCase,
- ## see src/services/mcp/schemas.ts in the cline repo) — distinct from
- ## Roo's "streamable-http" string. Parallel to the Roo fix in #190.
- entry_extra_fields = {"type": "streamableHttp"}
- ## `disabled` and `autoApprove` are user-state (they may have flipped the
- ## entry off, or auto-approved specific tools). Seed on first Configure
- ## but preserve across reconfigure — see `entry_initial_fields` in `_base.gd`.
- entry_initial_fields = {"disabled": false, "autoApprove": []}
- ## Attach migration (#838). Cline stdio entries are flat command/args/env;
- ## its schema accepts `type: "stdio"` and normalizes typeless command
- ## entries to it (apps/vscode/src/services/mcp/schemas.ts), so pin the
- ## type — that also repins the legacy "streamableHttp" value instead of
- ## letting it survive the deep-copy and misroute the transport.
- command_shape = McpClient.CommandShape.FLAT
- command_transport_key = "type"
- command_transport_value = "stdio"
- command_legacy_keys = PackedStringArray(["url", "headers"])
- command_initial_fields = {"disabled": false, "autoApprove": []}
- command_user_fields = PackedStringArray([
- "disabled", "autoApprove", "timeout", "oauth", "metadata",
- "remoteConfigured", "env", "cwd",
- ])
- command_timeout_fields = PackedStringArray(["timeout"])
diff --git a/addons/godot_ai/clients/cline.gd.uid b/addons/godot_ai/clients/cline.gd.uid
deleted file mode 100644
index 95e20f1..0000000
--- a/addons/godot_ai/clients/cline.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d36nywn2nkgts
diff --git a/addons/godot_ai/clients/codebuddy.gd b/addons/godot_ai/clients/codebuddy.gd
deleted file mode 100644
index 3d043f0..0000000
--- a/addons/godot_ai/clients/codebuddy.gd
+++ /dev/null
@@ -1,25 +0,0 @@
-@tool
-extends McpClient
-
-## CodeBuddy IDE: https://www.codebuddy.ai/docs/zh/ide/User-guide/MCP
-## Official docs specify mcpServers + type:stdio + command/args/env.
-## Global ~/.codebuddy/mcp.json was verified by the reporter in #941.
-## This descriptor uses the global user scope,
-## matching the registry's absolute user-path contract; project configuration
-## remains available manually in CodeBuddy's MCP settings.
-
-
-func _init() -> void:
- id = "codebuddy"
- display_name = "CodeBuddy"
- config_type = "json"
- path_template = {
- "unix": "~/.codebuddy/mcp.json",
- "windows": "$USERPROFILE/.codebuddy/mcp.json",
- }
- server_key_path = PackedStringArray(["mcpServers"])
- command_shape = McpClient.CommandShape.FLAT
- command_transport_key = "type"
- command_transport_value = "stdio"
- command_legacy_keys = PackedStringArray(["url", "headers"])
- command_user_fields = PackedStringArray(["env", "description"])
diff --git a/addons/godot_ai/clients/codebuddy.gd.uid b/addons/godot_ai/clients/codebuddy.gd.uid
deleted file mode 100644
index 88506e6..0000000
--- a/addons/godot_ai/clients/codebuddy.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cutvvrwaef0rk
diff --git a/addons/godot_ai/clients/codex.gd b/addons/godot_ai/clients/codex.gd
deleted file mode 100644
index c80f7f3..0000000
--- a/addons/godot_ai/clients/codex.gd
+++ /dev/null
@@ -1,44 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "codex"
- display_name = "Codex"
- config_type = "toml"
- path_template = {"unix": "~/.codex/config.toml", "windows": "$USERPROFILE/.codex/config.toml"}
- ## Documented: when $CODEX_HOME is set, Codex reads config.toml directly
- ## from it instead of ~/.codex (#617).
- config_home_env = "CODEX_HOME"
- config_home_env_subpath = "config.toml"
- toml_section_path = PackedStringArray(["mcp_servers", "godot-ai"])
- # Older Codex builds used the unquoted form with underscore-substituted ids.
- toml_legacy_section_aliases = PackedStringArray(["mcp_servers.godot_ai"])
- command_shape = McpClient.CommandShape.COMMAND_ARRAY
- command_legacy_keys = PackedStringArray(["url"])
- ## Initial-only: users may disable the entry or tune either timeout and
- ## Configure preserves that choice. Codex currently defaults to 10s for
- ## startup and 60s per tool; test_run legitimately has a 300s server
- ## budget, so the generated config leaves transport margin at the client.
- command_initial_fields = {
- "enabled": true,
- "startup_timeout_sec": 60,
- "tool_timeout_sec": 360,
- }
- command_timeout_fields = PackedStringArray([
- "startup_timeout_sec",
- "tool_timeout_sec",
- ])
- command_user_fields = PackedStringArray([
- "enabled",
- "required",
- "startup_timeout_sec",
- "tool_timeout_sec",
- "enabled_tools",
- "disabled_tools",
- "default_tools_approval_mode",
- "env",
- "env_vars",
- "cwd",
- ])
- detect_paths = PackedStringArray(path_template.values())
diff --git a/addons/godot_ai/clients/codex.gd.uid b/addons/godot_ai/clients/codex.gd.uid
deleted file mode 100644
index 1e1c3ae..0000000
--- a/addons/godot_ai/clients/codex.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://hdlwcfdr8mdk
diff --git a/addons/godot_ai/clients/cursor.gd b/addons/godot_ai/clients/cursor.gd
deleted file mode 100644
index 9dd4ea0..0000000
--- a/addons/godot_ai/clients/cursor.gd
+++ /dev/null
@@ -1,20 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "cursor"
- display_name = "Cursor"
- config_type = "json"
- path_template = {"unix": "~/.cursor/mcp.json", "windows": "$USERPROFILE/.cursor/mcp.json"}
- server_key_path = PackedStringArray(["mcpServers"])
- ## Attach migration (#838). Cursor's stdio entries are flat command/args/env
- ## (cursor.com/docs/context/mcp). The docs' reference table documents
- ## `type: "stdio"`; pinning it also repins any hand-added `type: "http"`
- ## left on the legacy URL entry, which would otherwise survive the
- ## deep-copy migration and misroute the transport.
- command_shape = McpClient.CommandShape.FLAT
- command_transport_key = "type"
- command_transport_value = "stdio"
- command_legacy_keys = PackedStringArray(["url"])
- command_user_fields = PackedStringArray(["env", "envFile"])
diff --git a/addons/godot_ai/clients/cursor.gd.uid b/addons/godot_ai/clients/cursor.gd.uid
deleted file mode 100644
index e0c7ddf..0000000
--- a/addons/godot_ai/clients/cursor.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bvpbssfanukef
diff --git a/addons/godot_ai/clients/deepseek_harness.gd b/addons/godot_ai/clients/deepseek_harness.gd
deleted file mode 100644
index db11ffc..0000000
--- a/addons/godot_ai/clients/deepseek_harness.gd
+++ /dev/null
@@ -1,44 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "deepseek_harness"
- display_name = "DeepSeek Harness"
- ## DeepSeek Harness (dsh) has no `mcp` CLI verb — MCP servers register as
- ## `@deepseek-ai/dsh-mcp-client` plugin entries in the HOME patch layer
- ## `$DSH_HOME/cordis.patch.yml`, which dsh applies over every profile's
- ## own patch layer (the web GUI included). config_type "dsh" routes to
- ## `McpDshStrategy`. Verified live against dsh 0.1.0-rc.6: new entries
- ## must be `insert` rows; plain rows only override existing bundle ids.
- config_type = "dsh"
- ## Home patch layer, not a per-profile file: one entry covers the web
- ## GUI, headless, tui, and any custom profile.
- path_template = {
- "unix": "~/.dsh/cordis.patch.yml",
- "windows": "~/.dsh/cordis.patch.yml",
- }
- ## Documented: `$DSH_HOME` relocates the whole harness home, and the
- ## loader reads `$DSH_HOME/cordis.patch.yml` (homePatchPath) — same
- ## false-success-write class as CODEX_HOME (#617), so the override must
- ## be declared.
- config_home_env = "DSH_HOME"
- config_home_env_subpath = "cordis.patch.yml"
- ## Installed detection: the harness home directory itself (dsh GUI and
- ## CLI both live there), or the config file once one exists.
- detect_paths = PackedStringArray(["~/.dsh"])
- ## Attach migration (#838). The loader entry nests the launch under
- ## `config`; the mcp-client plugin requires `transport` next to
- ## command/args, and a `url` must never coexist with command fields.
- command_shape = McpClient.CommandShape.FLAT
- command_transport_key = "transport"
- command_transport_value = "stdio"
- command_legacy_keys = PackedStringArray(["url"])
- ## Fields the mcp-client plugin documents as user-owned; preserved on
- ## reconfigure and never drift-checked.
- command_user_fields = PackedStringArray([
- "env", "cwd", "toolCallTimeoutMs", "failOnStartupError", "reconnect",
- ])
- command_timeout_fields = PackedStringArray(["toolCallTimeoutMs"])
- ## The bridge accepts `transport: streamable-http` with a `url`, so the
- ## manual instructions may offer the HTTP alternative.
diff --git a/addons/godot_ai/clients/deepseek_harness.gd.uid b/addons/godot_ai/clients/deepseek_harness.gd.uid
deleted file mode 100644
index 8cc2ed3..0000000
--- a/addons/godot_ai/clients/deepseek_harness.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cw3pghlevtql8
diff --git a/addons/godot_ai/clients/gemini_cli.gd b/addons/godot_ai/clients/gemini_cli.gd
deleted file mode 100644
index 34257b6..0000000
--- a/addons/godot_ai/clients/gemini_cli.gd
+++ /dev/null
@@ -1,24 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "gemini_cli"
- display_name = "Gemini CLI"
- config_type = "json"
- path_template = {
- "unix": "~/.gemini/settings.json",
- "windows": "$USERPROFILE/.gemini/settings.json",
- }
- server_key_path = PackedStringArray(["mcpServers"])
- entry_url_field = "httpUrl"
- ## Attach migration (#838). Gemini CLI stdio entries are flat
- ## command/args/env(+cwd); the config is one-of `command` | `url` (SSE) |
- ## `httpUrl` (docs/tools/mcp-server.md), so BOTH URL keys are legacy next
- ## to a command. `trust` bypasses tool confirmations — never seed it.
- command_shape = McpClient.CommandShape.FLAT
- command_legacy_keys = PackedStringArray(["httpUrl", "url"])
- command_user_fields = PackedStringArray([
- "timeout", "trust", "includeTools", "excludeTools", "env", "cwd",
- ])
- command_timeout_fields = PackedStringArray(["timeout"])
diff --git a/addons/godot_ai/clients/gemini_cli.gd.uid b/addons/godot_ai/clients/gemini_cli.gd.uid
deleted file mode 100644
index 2d4e85a..0000000
--- a/addons/godot_ai/clients/gemini_cli.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b8288pxninajy
diff --git a/addons/godot_ai/clients/grok.gd b/addons/godot_ai/clients/grok.gd
deleted file mode 100644
index 4c54216..0000000
--- a/addons/godot_ai/clients/grok.gd
+++ /dev/null
@@ -1,34 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "grok"
- display_name = "Grok Build"
- config_type = "toml"
- # Grok Build reads MCP servers from ~/.grok/config.toml
- # (https://x.ai / Grok user guide: MCP servers section).
- path_template = {
- "unix": "~/.grok/config.toml",
- "windows": "$USERPROFILE/.grok/config.toml",
- }
- toml_section_path = PackedStringArray(["mcp_servers", "godot-ai"])
- # Some docs / older notes used an underscore form.
- toml_legacy_section_aliases = PackedStringArray(["mcp_servers.godot_ai"])
- ## Attach migration (#838). Grok's stdio sections are flat command/args/env
- ## with no type discriminator (docs.x.ai/build/features/mcp-servers);
- ## url/headers are the HTTP form and must not survive next to a command.
- ## Docs default startup_timeout_sec to 30 — a cold `uvx` install of the
- ## pinned package can exceed that, so new entries seed 60 (preserved once
- ## the user tunes it). tool_timeout_sec's documented 6000s default already
- ## clears test_run's 300s budget, so it is left alone. The old body
- ## template's `enabled = true` line was never documented for Grok — it is
- ## no longer seeded, and an existing value survives as a user key.
- command_shape = McpClient.CommandShape.COMMAND_ARRAY
- command_legacy_keys = PackedStringArray(["url", "headers"])
- command_initial_fields = {"startup_timeout_sec": 60}
- command_user_fields = PackedStringArray([
- "env", "enabled", "startup_timeout_sec", "tool_timeout_sec",
- ])
- command_timeout_fields = PackedStringArray(["startup_timeout_sec", "tool_timeout_sec"])
- detect_paths = PackedStringArray(path_template.values())
diff --git a/addons/godot_ai/clients/grok.gd.uid b/addons/godot_ai/clients/grok.gd.uid
deleted file mode 100644
index f2d12c2..0000000
--- a/addons/godot_ai/clients/grok.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ckchsj5s3q1b0
diff --git a/addons/godot_ai/clients/hermes.gd b/addons/godot_ai/clients/hermes.gd
deleted file mode 100644
index 2e9f7bc..0000000
--- a/addons/godot_ai/clients/hermes.gd
+++ /dev/null
@@ -1,45 +0,0 @@
-@tool
-extends McpClient
-
-func _init() -> void:
- id = "hermes"
- display_name = "Hermes Agent"
- config_type = "yaml"
-
- # Hermes reads MCP config from ~/.hermes/config.yaml (YAML), NOT mcp.json.
- # Verified against the official docs:
- # https://hermes-agent.nousresearch.com/docs/user-guide/features/mcp
- # Windows: Hermes stores config under $LOCALAPPDATA/hermes (NOT $APPDATA,
- # which is Roaming) — confirmed by where the running Hermes process reads.
- # NOTE: _path_template.expand() only substitutes $VAR tokens, not %VAR%.
- path_template = {
- "unix": "~/.hermes/config.yaml",
- "windows": "$LOCALAPPDATA/hermes/config.yaml"
- }
-
- # Hermes uses the snake_case `mcp_servers` key (not `mcpServers`).
- # PackedStringArray explicitly, matching every other descriptor — an
- # untyped Array literal relies on implicit conversion that newer Godot
- # builds enforce more strictly (the #722 CI lesson for Array[String]).
- server_key_path = PackedStringArray(["mcp_servers"])
-
- # HTTP entries use `url` (+ optional `headers`); transport is inferred —
- # there is no `type` field in Hermes MCP config.
- entry_url_field = "url"
-
- # No transport pin: Hermes infers streamable-http from the URL.
- entry_extra_fields = {}
- entry_initial_fields = {}
-
- ## Attach migration (#838). Hermes stdio entries are flat command/args/env
- ## (hermes-agent.nousresearch.com/docs/user-guide/features/mcp), transport
- ## inferred exactly like the URL form — which is why `url` and the
- ## HTTP-only `headers` must not survive next to a command: an entry with
- ## both picks the wrong transport. `enabled`/`tools`/`env` stay user-owned.
- command_shape = McpClient.CommandShape.FLAT
- command_legacy_keys = PackedStringArray(["url", "headers"])
- command_user_fields = PackedStringArray(["enabled", "tools", "env"])
-
- # Hermes is "installed" wherever the config.yaml lives; presence of the
- # file is sufficient for the dock's installed badge.
- detect_paths = PackedStringArray()
diff --git a/addons/godot_ai/clients/hermes.gd.uid b/addons/godot_ai/clients/hermes.gd.uid
deleted file mode 100644
index 14cabd8..0000000
--- a/addons/godot_ai/clients/hermes.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ewmadhrvs5d7
diff --git a/addons/godot_ai/clients/kilo_code.gd b/addons/godot_ai/clients/kilo_code.gd
deleted file mode 100644
index b904837..0000000
--- a/addons/godot_ai/clients/kilo_code.gd
+++ /dev/null
@@ -1,37 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "kilo_code"
- display_name = "Kilo Code"
- config_type = "json"
- path_template = {
- "darwin": "~/Library/Application Support/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json",
- "windows": "$APPDATA/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json",
- "linux": "$XDG_CONFIG_HOME/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json",
- }
- server_key_path = PackedStringArray(["mcpServers"])
- ## Kilo Code (like Roo) defaults a typeless entry to SSE transport, which
- ## returns HTTP 400 against our streamable-http endpoint on `/mcp`. Pin
- ## the type explicitly. Parallel to the Roo fix in #190.
- entry_extra_fields = {"type": "streamable-http"}
- ## `disabled` and `alwaysAllow` are user-state (they may have flipped the
- ## entry off, or auto-approved specific tools). Seed on first Configure
- ## but preserve across reconfigure — see `entry_initial_fields` in `_base.gd`.
- entry_initial_fields = {"disabled": false, "alwaysAllow": []}
- ## Attach migration (#838). UNLIKE its Roo siblings the stdio entry must be
- ## TYPELESS: Kilo's v7 platform treats this file as a migration source and
- ## routes on `type` — any http type value sends the entry down the remote
- ## branch where it is dropped for lack of a url, and only a bare
- ## command/args/env entry is verified to work in BOTH the legacy extension
- ## and the v7 migrator (packages/opencode/src/kilocode/mcp-migrator.ts).
- ## `type` therefore joins the legacy keys instead of being repinned.
- command_shape = McpClient.CommandShape.FLAT
- command_legacy_keys = PackedStringArray(["url", "type", "headers"])
- command_initial_fields = {"disabled": false, "alwaysAllow": []}
- command_user_fields = PackedStringArray([
- "disabled", "alwaysAllow", "timeout", "cwd", "watchPaths",
- "disabledTools", "env",
- ])
- command_timeout_fields = PackedStringArray(["timeout"])
diff --git a/addons/godot_ai/clients/kilo_code.gd.uid b/addons/godot_ai/clients/kilo_code.gd.uid
deleted file mode 100644
index 3ee5152..0000000
--- a/addons/godot_ai/clients/kilo_code.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dc1x77i1cmb6w
diff --git a/addons/godot_ai/clients/kimi_code.gd b/addons/godot_ai/clients/kimi_code.gd
deleted file mode 100644
index 04354a4..0000000
--- a/addons/godot_ai/clients/kimi_code.gd
+++ /dev/null
@@ -1,33 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "kimi_code"
- display_name = "Kimi Code"
- ## Kimi Code has no `mcp` CLI subcommand (verified against v0.28.1 —
- ## `kimi mcp` falls through to the root --help, and the docs at
- ## moonshotai.github.io/kimi-code/en/customization/mcp confirm servers are
- ## managed via ~/.kimi-code/mcp.json, not a CLI verb). JSON is therefore
- ## the only working config method, not a fallback.
- config_type = "json"
- path_template = {"unix": "~/.kimi-code/mcp.json", "windows": "~/.kimi-code/mcp.json"}
- server_key_path = PackedStringArray(["mcpServers"])
- entry_extra_fields = {"transport": "http"}
- ## Documented: `$KIMI_CODE_HOME/mcp.json` relocates the config
- ## (moonshotai.github.io/kimi-code/en/customization/mcp) — same
- ## false-success-write class as CODEX_HOME (#617).
- config_home_env = "KIMI_CODE_HOME"
- config_home_env_subpath = "mcp.json"
- ## Attach migration (#838). Kimi Code stdio entries are flat
- ## command/args/env(+cwd): "Entries with a `command` field are stdio
- ## servers". `transport` is only defined for SSE-with-url, so the legacy
- ## `transport: "http"` must be removed alongside `url`. Timeout fields are
- ## camelCase, unlike Codex's snake_case.
- command_shape = McpClient.CommandShape.FLAT
- command_legacy_keys = PackedStringArray(["url", "transport"])
- command_user_fields = PackedStringArray([
- "enabled", "startupTimeoutMs", "toolTimeoutMs", "enabledTools",
- "disabledTools", "env", "cwd",
- ])
- command_timeout_fields = PackedStringArray(["startupTimeoutMs", "toolTimeoutMs"])
diff --git a/addons/godot_ai/clients/kimi_code.gd.uid b/addons/godot_ai/clients/kimi_code.gd.uid
deleted file mode 100644
index 5a05a0e..0000000
--- a/addons/godot_ai/clients/kimi_code.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d2whd6a5fofhg
diff --git a/addons/godot_ai/clients/kiro.gd b/addons/godot_ai/clients/kiro.gd
deleted file mode 100644
index d7d6b72..0000000
--- a/addons/godot_ai/clients/kiro.gd
+++ /dev/null
@@ -1,22 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "kiro"
- display_name = "Kiro"
- config_type = "json"
- path_template = {
- "unix": "~/.kiro/settings/mcp.json",
- "windows": "$USERPROFILE/.kiro/settings/mcp.json",
- }
- server_key_path = PackedStringArray(["mcpServers"])
- ## `disabled` is user-state — preserved across reconfigure.
- entry_initial_fields = {"disabled": false}
- ## Attach migration (#838). Kiro stdio entries are flat command/args/env
- ## with no type discriminator (kiro.dev/docs/mcp/configuration).
- ## `autoApprove` is user-state, same contract as the URL entry's fields.
- command_shape = McpClient.CommandShape.FLAT
- command_legacy_keys = PackedStringArray(["url"])
- command_initial_fields = {"disabled": false}
- command_user_fields = PackedStringArray(["disabled", "autoApprove", "disabledTools", "env"])
diff --git a/addons/godot_ai/clients/kiro.gd.uid b/addons/godot_ai/clients/kiro.gd.uid
deleted file mode 100644
index dde3e87..0000000
--- a/addons/godot_ai/clients/kiro.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dqdmd2jw5qen7
diff --git a/addons/godot_ai/clients/opencode.gd b/addons/godot_ai/clients/opencode.gd
deleted file mode 100644
index eab6518..0000000
--- a/addons/godot_ai/clients/opencode.gd
+++ /dev/null
@@ -1,54 +0,0 @@
-@tool
-extends McpClient
-
-## OpenCode stores MCP servers under `mcp.` (not the typical mcpServers
-## map) and uses `type: "remote"` for HTTP servers.
-
-
-func _init() -> void:
- id = "opencode"
- display_name = "OpenCode"
- config_type = "json"
- ## `$HOME` on Windows is deliberate: OpenCode reads ~/.config/... on ALL
- ## platforms (verified via `opencode debug paths`), and
- ## McpPathTemplate._home() falls back to USERPROFILE when HOME is unset —
- ## pinned by test_opencode_client_uses_home_config_on_windows. The documented
- ## `OPENCODE_CONFIG` override names an exact file and must win over this
- ## default for configure, status, remove, and manual instructions.
- path_template = {
- "unix": "~/.config/opencode/opencode.json",
- "windows": "$HOME/.config/opencode/opencode.json",
- }
- config_file_env = "OPENCODE_CONFIG"
- ## OpenCode loads BOTH opencode.json and opencode.jsonc from its config
- ## directory and merges them, later file winning per key
- ## (anomalyco/opencode#32447). Writing only opencode.json therefore
- ## leaves a stale `godot-ai` entry in an existing opencode.jsonc in
- ## charge (#1011). Declaring the merge order makes Configure update the
- ## effective last definition, status verify it, and Remove clear both.
- config_merge_path_templates = {
- "unix": PackedStringArray([
- "~/.config/opencode/opencode.json",
- "~/.config/opencode/opencode.jsonc",
- ]),
- "windows": PackedStringArray([
- "$HOME/.config/opencode/opencode.json",
- "$HOME/.config/opencode/opencode.jsonc",
- ]),
- }
- server_key_path = PackedStringArray(["mcp"])
- entry_extra_fields = {"type": "remote"}
- ## `enabled` is user-state (they may have toggled the server off).
- entry_initial_fields = {"enabled": true}
- ## Attach migration (#838). OpenCode local entries carry the launch as ONE
- ## argv array — `"command": ["uvx", …]` with no separate args key — plus a
- ## schema-REQUIRED `type: "local"` (McpLocalConfig in opencode.ai/config.json;
- ## env lives under `environment`, not `env`). The pin rewrites the legacy
- ## `type: "remote"` in place; url/headers are remote-only and must go.
- command_shape = McpClient.CommandShape.COMMAND_ARRAY
- command_transport_key = "type"
- command_transport_value = "local"
- command_legacy_keys = PackedStringArray(["url", "headers"])
- command_initial_fields = {"enabled": true}
- command_user_fields = PackedStringArray(["enabled", "timeout", "environment", "cwd"])
- command_timeout_fields = PackedStringArray(["timeout"])
diff --git a/addons/godot_ai/clients/opencode.gd.uid b/addons/godot_ai/clients/opencode.gd.uid
deleted file mode 100644
index dc2ad00..0000000
--- a/addons/godot_ai/clients/opencode.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://s8n0vfirf2pj
diff --git a/addons/godot_ai/clients/pi.gd b/addons/godot_ai/clients/pi.gd
deleted file mode 100644
index 0862cc6..0000000
--- a/addons/godot_ai/clients/pi.gd
+++ /dev/null
@@ -1,61 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "pi"
- display_name = "Pi Agent"
- config_type = "json"
- # pi-codemode-mcp reads MCP server definitions from ~/.pi/agent/mcp.json
- # (first merge tier; ~/.pi/agent/.mcp.json, .pi/mcp.json, and
- # .mcp.json merge after, project-scope last). Documented at
- # github.com/mitsuhiko/pi-codemode-mcp README "Configuration files"
- # section. Windows path mirrors antigravity's $USERPROFILE choice so
- # the descriptor round-trips on every supported platform.
- path_template = {
- "unix": "~/.pi/agent/mcp.json",
- "windows": "$USERPROFILE/.pi/agent/mcp.json",
- }
- # Pi merges these files in order; later definitions of the same server win.
- # Keep this separate from config_path_candidates, whose entries are alternatives
- # rather than merge tiers.
- config_merge_path_templates = {
- "unix": PackedStringArray([
- "~/.pi/agent/mcp.json",
- "~/.pi/agent/.mcp.json",
- ]),
- "windows": PackedStringArray([
- "$USERPROFILE/.pi/agent/mcp.json",
- "$USERPROFILE/.pi/agent/.mcp.json",
- ]),
- }
- config_merge_project_paths = PackedStringArray([".pi/mcp.json", ".mcp.json"])
- server_key_path = PackedStringArray(["mcpServers"])
- # pi-codemode-mcp resolves these with nullish precedence:
- # mcpServers ?? mcp-servers ?? servers. Reuse whichever map already exists
- # so Configure cannot shadow and deactivate the user's existing servers.
- var aliases: Array[PackedStringArray] = [
- PackedStringArray(["mcp-servers"]),
- PackedStringArray(["servers"]),
- ]
- server_key_path_aliases = aliases
- entry_url_field = "url"
- # pi-codemode-mcp selects stdio from `command` and remote transport from
- # `url`; it ignores `type`. Keep generated stdio entries minimal and typeless.
- entry_extra_fields = {}
- entry_initial_fields = {}
- # Attach migration (#838). Pi stdio entries are flat command/args/env.
- # When Configure replaces a legacy URL entry, remove `url`, `headers`, and
- # any redundant `type` field so the generated stdio entry stays canonical.
- command_shape = McpClient.CommandShape.FLAT
- command_legacy_keys = PackedStringArray(["url", "headers", "type"])
- # `env` is the only user-mutable field pi's docs surface. Future
- # fields still survive — the strategy deep-copies unknown keys.
- command_user_fields = PackedStringArray(["env"])
- # Pi's loader also accepts pure-URL entries (the example mcp.json in
- # the pi-codemode-mcp repo ships stdio + URL entries side-by-side).
- # ~/.pi/agent/mcp.json is created by the first pi launch, so its
- # presence is the strongest install signal. Mirror antigravity's
- # `detect_paths = path_template.values()` pattern so the dock shows
- # the installed badge before Configure has run.
- detect_paths = PackedStringArray(path_template.values())
diff --git a/addons/godot_ai/clients/pi.gd.uid b/addons/godot_ai/clients/pi.gd.uid
deleted file mode 100644
index 53aeb7c..0000000
--- a/addons/godot_ai/clients/pi.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://it00ohwkm2nd
diff --git a/addons/godot_ai/clients/qwen_code.gd b/addons/godot_ai/clients/qwen_code.gd
deleted file mode 100644
index bed4492..0000000
--- a/addons/godot_ai/clients/qwen_code.gd
+++ /dev/null
@@ -1,25 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "qwen_code"
- display_name = "Qwen Code"
- config_type = "json"
- path_template = {
- "unix": "~/.qwen/settings.json",
- "windows": "$USERPROFILE/.qwen/settings.json",
- }
- server_key_path = PackedStringArray(["mcpServers"])
- entry_url_field = "httpUrl"
- ## Attach migration (#838). Qwen Code is a gemini-cli fork with the same
- ## flat stdio shape and one-of `command` | `url` | `httpUrl` rule
- ## (docs/users/features/mcp.md). Qwen adds `discoveryTimeoutMs` (stdio
- ## discovery handshake cap, default 30s).
- command_shape = McpClient.CommandShape.FLAT
- command_legacy_keys = PackedStringArray(["httpUrl", "url"])
- command_user_fields = PackedStringArray([
- "timeout", "trust", "includeTools", "excludeTools", "env", "cwd",
- "discoveryTimeoutMs",
- ])
- command_timeout_fields = PackedStringArray(["timeout", "discoveryTimeoutMs"])
diff --git a/addons/godot_ai/clients/qwen_code.gd.uid b/addons/godot_ai/clients/qwen_code.gd.uid
deleted file mode 100644
index 5f2eb1a..0000000
--- a/addons/godot_ai/clients/qwen_code.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://qwb5udkf423q
diff --git a/addons/godot_ai/clients/roo_code.gd b/addons/godot_ai/clients/roo_code.gd
deleted file mode 100644
index 5a1e697..0000000
--- a/addons/godot_ai/clients/roo_code.gd
+++ /dev/null
@@ -1,41 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "roo_code"
- display_name = "Roo Code"
- config_type = "json"
- path_template = {
- "darwin": "~/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json",
- "windows": "$APPDATA/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json",
- "linux": "$XDG_CONFIG_HOME/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json",
- }
- server_key_path = PackedStringArray(["mcpServers"])
- ## Roo defaults an entry with no "type" to SSE transport — which returns
- ## HTTP 400 against our streamable-http endpoint on `/mcp`. Pin the type
- ## explicitly so Roo negotiates streamable-http (the current MCP spec's
- ## recommended remote transport). See issue #189. The default verifier
- ## requires every entry_extra_fields key to match, so a pre-#189 typeless
- ## entry surfaces as drift instead of silently passing as configured.
- entry_extra_fields = {"type": "streamable-http"}
- ## `disabled` and `alwaysAllow` are user-state (they may have flipped the
- ## entry off, or auto-approved specific tools like `session_manage`).
- ## Seed on first Configure but preserve across reconfigure — without this
- ## split, the Configure-All-Mismatched sweep silently wipes the user's
- ## auto-approval list every time the type pin or URL drifts.
- entry_initial_fields = {"disabled": false, "alwaysAllow": []}
- ## Attach migration (#838). Roo stdio entries are flat command/args/env
- ## (+cwd); docs state `type` defaults to "stdio" for command configs and
- ## the stdio schema forbids url/headers. Pin type=stdio — it is documented
- ## and repins the legacy "streamable-http" value in place.
- command_shape = McpClient.CommandShape.FLAT
- command_transport_key = "type"
- command_transport_value = "stdio"
- command_legacy_keys = PackedStringArray(["url", "headers"])
- command_initial_fields = {"disabled": false, "alwaysAllow": []}
- command_user_fields = PackedStringArray([
- "disabled", "alwaysAllow", "timeout", "disabledTools", "watchPaths",
- "env", "cwd",
- ])
- command_timeout_fields = PackedStringArray(["timeout"])
diff --git a/addons/godot_ai/clients/roo_code.gd.uid b/addons/godot_ai/clients/roo_code.gd.uid
deleted file mode 100644
index 1dcae84..0000000
--- a/addons/godot_ai/clients/roo_code.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://denjdf50qrf66
diff --git a/addons/godot_ai/clients/trae.gd b/addons/godot_ai/clients/trae.gd
deleted file mode 100644
index 68f66de..0000000
--- a/addons/godot_ai/clients/trae.gd
+++ /dev/null
@@ -1,22 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "trae"
- display_name = "Trae"
- config_type = "json"
- path_template = {
- "darwin": "~/Library/Application Support/Trae/User/mcp.json",
- "windows": "$APPDATA/Trae/User/mcp.json",
- "linux": "$XDG_CONFIG_HOME/Trae/User/mcp.json",
- }
- server_key_path = PackedStringArray(["mcpServers"])
- ## Attach migration (#838). Trae stdio entries are flat command/args/env
- ## with no type discriminator (docs.trae.cn/ide/add-mcp-servers); transport
- ## is inferred from `command` vs `url`, so url/headers are legacy next to a
- ## command. Entry stays minimal — Trae manages enable/disable in its UI,
- ## and its startup/run timeouts ride user-owned env vars, which survive.
- command_shape = McpClient.CommandShape.FLAT
- command_legacy_keys = PackedStringArray(["url", "headers"])
- command_user_fields = PackedStringArray(["env"])
diff --git a/addons/godot_ai/clients/trae.gd.uid b/addons/godot_ai/clients/trae.gd.uid
deleted file mode 100644
index f10046e..0000000
--- a/addons/godot_ai/clients/trae.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cwpu48772vfj1
diff --git a/addons/godot_ai/clients/vscode.gd b/addons/godot_ai/clients/vscode.gd
deleted file mode 100644
index 9b97c53..0000000
--- a/addons/godot_ai/clients/vscode.gd
+++ /dev/null
@@ -1,29 +0,0 @@
-@tool
-extends McpClient
-
-## VS Code (stable) reads MCP servers from per-user mcp.json under
-## `servers.` with `{ "type": "http", "url": ... }`.
-
-
-func _init() -> void:
- id = "vscode"
- display_name = "VS Code"
- config_type = "json"
- path_template = {
- "darwin": "~/Library/Application Support/Code/User/mcp.json",
- "windows": "$APPDATA/Code/User/mcp.json",
- "linux": "$XDG_CONFIG_HOME/Code/User/mcp.json",
- }
- server_key_path = PackedStringArray(["servers"])
- entry_extra_fields = {"type": "http"}
- ## Attach migration (#838). VS Code stdio entries are flat command/args/env
- ## under `servers` with a documented `type: "stdio"` discriminator
- ## (code.visualstudio.com/docs/agents/reference/mcp-configuration). The
- ## stdio schema is `additionalProperties: false` (mcpConfiguration.ts), so
- ## removing the legacy url/headers is load-bearing — leftovers invalidate
- ## the whole entry, and the pin flips the legacy `type: "http"` in place.
- command_shape = McpClient.CommandShape.FLAT
- command_transport_key = "type"
- command_transport_value = "stdio"
- command_legacy_keys = PackedStringArray(["url", "headers"])
- command_user_fields = PackedStringArray(["env", "envFile", "cwd", "sandboxEnabled", "dev"])
diff --git a/addons/godot_ai/clients/vscode.gd.uid b/addons/godot_ai/clients/vscode.gd.uid
deleted file mode 100644
index 1c79881..0000000
--- a/addons/godot_ai/clients/vscode.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dl6cm044pihub
diff --git a/addons/godot_ai/clients/vscode_insiders.gd b/addons/godot_ai/clients/vscode_insiders.gd
deleted file mode 100644
index 0fc9427..0000000
--- a/addons/godot_ai/clients/vscode_insiders.gd
+++ /dev/null
@@ -1,22 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "vscode_insiders"
- display_name = "VS Code Insiders"
- config_type = "json"
- path_template = {
- "darwin": "~/Library/Application Support/Code - Insiders/User/mcp.json",
- "windows": "$APPDATA/Code - Insiders/User/mcp.json",
- "linux": "$XDG_CONFIG_HOME/Code - Insiders/User/mcp.json",
- }
- server_key_path = PackedStringArray(["servers"])
- entry_extra_fields = {"type": "http"}
- ## Attach migration (#838). Identical format to vscode.gd — Insiders ships
- ## the same mcpConfiguration.ts schema; see that descriptor for citations.
- command_shape = McpClient.CommandShape.FLAT
- command_transport_key = "type"
- command_transport_value = "stdio"
- command_legacy_keys = PackedStringArray(["url", "headers"])
- command_user_fields = PackedStringArray(["env", "envFile", "cwd", "sandboxEnabled", "dev"])
diff --git a/addons/godot_ai/clients/vscode_insiders.gd.uid b/addons/godot_ai/clients/vscode_insiders.gd.uid
deleted file mode 100644
index c763703..0000000
--- a/addons/godot_ai/clients/vscode_insiders.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cad5w4ofyg8a2
diff --git a/addons/godot_ai/clients/windsurf.gd b/addons/godot_ai/clients/windsurf.gd
deleted file mode 100644
index e069557..0000000
--- a/addons/godot_ai/clients/windsurf.gd
+++ /dev/null
@@ -1,28 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- # #623: Windsurf was rebranded to Devin Desktop by Cognition (June 2026).
- # The id stays "windsurf" — it is the stable registry key used for
- # configured-status lookups. The MCP config path is unchanged by the
- # rebrand: per the official docs (docs.devin.ai/desktop/cascade/mcp) the
- # global config still lives under the platform's `.codeium/windsurf/`
- # directory (~/.codeium/windsurf/ on unix, $USERPROFILE/.codeium/windsurf/
- # on Windows), and migrated installs carry their settings over in place.
- id = "windsurf"
- display_name = "Devin Desktop (Windsurf)"
- config_type = "json"
- path_template = {
- "unix": "~/.codeium/windsurf/mcp_config.json",
- "windows": "$USERPROFILE/.codeium/windsurf/mcp_config.json",
- }
- server_key_path = PackedStringArray(["mcpServers"])
- entry_url_field = "serverUrl"
- ## Attach migration (#838). Stdio entries are flat command/args/env
- ## (docs.devin.ai/desktop/cascade/mcp); transport is inferred from
- ## `command` vs `serverUrl` presence and no type field is documented —
- ## do not write one.
- command_shape = McpClient.CommandShape.FLAT
- command_legacy_keys = PackedStringArray(["serverUrl"])
- command_user_fields = PackedStringArray(["env"])
diff --git a/addons/godot_ai/clients/windsurf.gd.uid b/addons/godot_ai/clients/windsurf.gd.uid
deleted file mode 100644
index af34b60..0000000
--- a/addons/godot_ai/clients/windsurf.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b6pqiok2mlsmg
diff --git a/addons/godot_ai/clients/zed.gd b/addons/godot_ai/clients/zed.gd
deleted file mode 100644
index e189064..0000000
--- a/addons/godot_ai/clients/zed.gd
+++ /dev/null
@@ -1,35 +0,0 @@
-@tool
-extends McpClient
-
-## Zed registers MCP servers under `context_servers.` and supports both
-## stdio and streamable http transports.
-
-
-func _init() -> void:
- id = "zed"
- display_name = "Zed"
- config_type = "json"
- ## Zed settings are JSONC. Owning a second JSON tokenizer solely to preserve
- ## comments would make the shared JSON strategy much harder to trust. Keep
- ## status checks read-only and give users the exact manual entry instead.
- automatic_config_edits = false
- ## Reads still have to cope with the `//` header Zed ships, or a stock
- ## install shows a parse error in the row instead of a status.
- config_allows_comments = true
- path_template = {
- "darwin": "~/.config/zed/settings.json",
- "linux": "$XDG_CONFIG_HOME/zed/settings.json",
- "windows": "$APPDATA/Zed/settings.json",
- }
- server_key_path = PackedStringArray(["context_servers"])
- ## Attach migration (#838). Current Zed's context_servers entries are an
- ## untagged serde enum discriminated by shape: `command` (string) + args/env
- ## → stdio, `url` → HTTP (zed.dev/docs/ai/mcp; settings_content/project.rs).
- ## BECAUSE the enum is untagged, HTTP-only keys left next to `command`
- ## (`url`, `headers`, `oauth`) make the entry match no variant and break it —
- ## removing them on migration is load-bearing, not cosmetic. `enabled`,
- ## `remote`, and `timeout` are user-state on the stdio variant.
- command_shape = McpClient.CommandShape.FLAT
- command_legacy_keys = PackedStringArray(["url", "headers", "oauth"])
- command_user_fields = PackedStringArray(["enabled", "remote", "timeout", "env"])
- command_timeout_fields = PackedStringArray(["timeout"])
diff --git a/addons/godot_ai/clients/zed.gd.uid b/addons/godot_ai/clients/zed.gd.uid
deleted file mode 100644
index b9b313a..0000000
--- a/addons/godot_ai/clients/zed.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d152l0u0r6fsc
diff --git a/addons/godot_ai/clients/zoo_code.gd b/addons/godot_ai/clients/zoo_code.gd
deleted file mode 100644
index 85529bf..0000000
--- a/addons/godot_ai/clients/zoo_code.gd
+++ /dev/null
@@ -1,35 +0,0 @@
-@tool
-extends McpClient
-
-
-func _init() -> void:
- id = "zoo_code"
- display_name = "Zoo Code"
- config_type = "json"
- path_template = {
- "darwin": "~/Library/Application Support/Code/User/globalStorage/zoocodeorganization.zoo-code/settings/mcp_settings.json",
- "windows": "$APPDATA/Code/User/globalStorage/zoocodeorganization.zoo-code/settings/mcp_settings.json",
- "linux": "$XDG_CONFIG_HOME/Code/User/globalStorage/zoocodeorganization.zoo-code/settings/mcp_settings.json",
- }
- server_key_path = PackedStringArray(["mcpServers"])
- ## Local validation against the installed extension shows Zoo stores MCP
- ## entries in `settings/mcp_settings.json` under `mcpServers`, matching Roo's
- ## shape. Its changelog also references Streamable HTTP support, so pin the
- ## transport explicitly to avoid any typeless entry falling back to SSE.
- entry_extra_fields = {"type": "streamable-http"}
- ## Preserve user-controlled state across reconfigure, parallel to Roo/Kilo.
- entry_initial_fields = {"disabled": false, "alwaysAllow": []}
- ## Attach migration (#838). Zoo's stdio zod schema is Roo's: flat
- ## command/args/env(+cwd), `type: z.enum(["stdio"]).optional()`, and
- ## url/headers explicitly forbidden on stdio entries (McpHub.ts) — so both
- ## are legacy keys and the documented type pin repins "streamable-http".
- command_shape = McpClient.CommandShape.FLAT
- command_transport_key = "type"
- command_transport_value = "stdio"
- command_legacy_keys = PackedStringArray(["url", "headers"])
- command_initial_fields = {"disabled": false, "alwaysAllow": []}
- command_user_fields = PackedStringArray([
- "disabled", "alwaysAllow", "timeout", "watchPaths", "disabledTools",
- "env", "cwd",
- ])
- command_timeout_fields = PackedStringArray(["timeout"])
diff --git a/addons/godot_ai/clients/zoo_code.gd.uid b/addons/godot_ai/clients/zoo_code.gd.uid
deleted file mode 100644
index dd3713e..0000000
--- a/addons/godot_ai/clients/zoo_code.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://fmp0nlzcm3ukl
diff --git a/addons/godot_ai/connection.gd b/addons/godot_ai/connection.gd
deleted file mode 100644
index 5877356..0000000
--- a/addons/godot_ai/connection.gd
+++ /dev/null
@@ -1,1321 +0,0 @@
-@tool
-class_name McpConnection
-extends Node
-
-## WebSocket transport to the Godot AI Python server.
-## Only handles connect, reconnect, send, and receive.
-## Command dispatch is owned by McpDispatcher.
-
-const RECONNECT_DELAYS: Array[float] = [1.0, 2.0, 4.0, 8.0, 16.0, 30.0, 60.0]
-const RECONNECT_VERBOSE_ATTEMPTS := 5
-const RECONNECT_LOG_HEARTBEAT_MSEC := 60_000
-## Backpressure policy: do not queue responses once the WebSocket's current
-## outbound buffer plus the next payload would exceed this cap. Command
-## responses get a compact structured error when that can still be sent;
-## state events report failure so their callers can retry on a later tick.
-const OUTBOUND_BUFFER_LIMIT_BYTES := 4 * 1024 * 1024
-## Cap the inbound packet drain per `_process` tick. A flooding peer or a
-## fast batch could otherwise saturate `_handle_message` in one frame and
-## blow the documented 4ms budget. Packets beyond this cap spill to the
-## next frame; the cumulative spill counter is logged so flood patterns
-## are observable in `logs_read`. See audit-v2 finding #12 (issue #356).
-const PACKET_DRAIN_CAP_PER_TICK := 32
-const MAX_QUEUED_PACKETS := 64
-const WS_PROTOCOL_VERSION := 2
-const HANDSHAKE_TIMEOUT_MSEC := 5000
-const MAX_HANDSHAKE_FRAME_BYTES := 8 * 1024
-const CLOSE_CODE_DUPLICATE_SESSION := 4001
-const CLOSE_CODE_PROTOCOL_MISMATCH := 4002
-const CLOSE_CODE_AUTH_FAILED := 4003
-const _SERVER_PROOF_DOMAIN := "godot-ai-ws-v2/server-proof"
-const _CLIENT_PROOF_DOMAIN := "godot-ai-ws-v2/client-proof"
-const ClientConfigurator := preload("res://addons/godot_ai/client_configurator.gd")
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Emitted whenever the authenticated v4 editor channel becomes usable/unusable.
-## Subscribers (e.g. the plugin-side telemetry helper) use this to drain
-## events that were enqueued before authentication completed.
-signal connection_state_changed(is_open: bool)
-
-var _peer := WebSocketPeer.new()
-## Seeded by plugin.gd from the configured EditorSettings port before the
-## first dial, then republished with the fully resolved port once the
-## deferred startup walk (#678) finishes resolving/spawning. Each connect
-## attempt recomputes the URL from the latest value, so reconnects keep
-## dialing the port the Python server was asked to bind.
-var ws_port := ClientConfigurator.DEFAULT_WS_PORT
-## Per-launch editor capability. Root supplies the copied value only after
-## lifecycle proof has read the child-published private capability record.
-## It never crosses the wire or enters EditorSettings; empty/malformed values
-## fail closed and require a fresh lifecycle episode.
-var auth_token := ""
-var _url := ""
-var _connected := false
-var _reconnect_attempt := 0
-var _reconnect_timer := 0.0
-## Pull-based reconnect observability. The peer owns CONNECTING/CLOSING
-## timing; tracking entry time here lets logs and the dock distinguish those
-## phases from the plugin-owned CLOSED-state backoff without changing policy.
-var _observed_peer_state := WebSocketPeer.STATE_CLOSED
-var _peer_state_entered_msec := 0
-var _last_reconnect_transition_log_msec := -1
-var _transient_diagnostic: Dictionary = {}
-## One pre-OPEN failure diagnostic per WebSocketPeer. Without this guard the
-## CLOSED state is polled every frame and would flood the editor log.
-var _preopen_failure_logged_for_peer := false
-var _session_id := ""
-var _handshake_started_msec := 0
-var _client_nonce := ""
-var _server_nonce := ""
-var _challenged_server_version := ""
-var _server_verified := false
-var _auth_response_sent := false
-var _handshake_complete := false
-## Godot-AI Python package version reported by the server in its `handshake_ack`
-## reply. Empty until the authenticated v4 handshake completes.
-var server_version := ""
-
-var dispatcher
-var log_buffer
-var surfaced_error_tracker
-## Set by plugin.gd. Lets the per-frame play-state poll end game-run
-## bookkeeping when the game exits on its own (self-quit, crash) — the
-## debugger session's stopped signal is not reliably connected, and no MCP
-## stop op runs in that path (#642).
-var debugger_plugin
-## Set by plugin.gd when the HTTP port is occupied by an incompatible or
-## unverified server. Keeping the Connection node alive lets handlers and the
-## dock share one object, but no WebSocket is opened to the wrong server.
-var connect_blocked := false
-var connect_block_reason := ""
-var _blocked_notice_logged := false
-## Compatibility property used by existing handlers. Setting true increments
-## the pause depth; setting false decrements it. Processing stays paused until
-## every nested pause has resumed.
-var pause_processing: bool:
- get: return _pause_depth > 0
- set(value):
- if value:
- pause()
- else:
- resume()
-var _pause_depth := 0
-## Cumulative count of inbound packets that didn't fit in their tick's drain
-## budget and got deferred to a subsequent tick. Reset on disconnect so each
-## connection starts with a clean spillover history. Logged whenever new
-## spillover occurs so flood patterns surface in `logs_read`.
-var _packet_spillover_total := 0
-
-
-func _ready() -> void:
- _session_id = _make_session_id(ProjectSettings.globalize_path("res://"))
- ## Increase outbound buffer for large messages (e.g. screenshot base64).
- ## Default is 64 KB; screenshots can be several MB.
- _peer.outbound_buffer_size = OUTBOUND_BUFFER_LIMIT_BYTES
- ## Stay under the pre-auth byte ceiling until the server proof validates;
- ## only then may the peer allocate the normal 4 MiB command buffer.
- _peer.inbound_buffer_size = MAX_HANDSHAKE_FRAME_BYTES
- _peer.max_queued_packets = MAX_QUEUED_PACKETS
- if connect_blocked:
- _log_blocked_notice_once()
- set_process(false)
- return
- _connect_to_server()
- _hook_editor_signals()
-
-
-func _process(delta: float) -> void:
- if pause_processing:
- return
- _peer.poll()
- ## Run-stop bookkeeping must not wait behind the socket-state machine:
- ## if the game stops while disconnected, the first command drained on
- ## reconnect would still observe stale "live" state (PR #642 review).
- _check_game_run_play_state(EditorInterface.is_playing_scene())
-
- var peer_state := _peer.get_ready_state()
- var transition := _observe_peer_state(peer_state, Time.get_ticks_msec())
- match peer_state:
- WebSocketPeer.STATE_OPEN:
- if not _connected:
- _connected = true
- if log_buffer:
- log_buffer.log("connected to server; authenticating")
- _send_auth_hello()
-
- _drain_inbound_packets(_peer)
- if not _handshake_complete:
- if (
- _handshake_started_msec > 0
- and Time.get_ticks_msec() - _handshake_started_msec >= HANDSHAKE_TIMEOUT_MSEC
- ):
- _fail_handshake(CLOSE_CODE_AUTH_FAILED, "v4 editor handshake timed out")
- return
-
- _check_state_changes()
-
- if dispatcher:
- for response in dispatcher.tick():
- _send_json(response)
-
- WebSocketPeer.STATE_CLOSED:
- if _connected:
- _connected = false
- var was_authenticated := _handshake_complete
- var server_was_verified := _server_verified
- ## This peer reached OPEN, so its one close diagnostic is the
- ## post-OPEN line below. Mark the peer consumed; otherwise a
- ## stale reconnect delay leaves it in CLOSED for another frame
- ## and the pre-OPEN branch emits a mislabeled duplicate.
- _preopen_failure_logged_for_peer = true
- _clear_on_disconnect()
- var code := _peer.get_close_code()
- var reason := _peer.get_close_reason()
- if _should_regenerate_session_id(code, server_was_verified, was_authenticated):
- _session_id = _make_session_id(ProjectSettings.globalize_path("res://"))
- var open_elapsed_sec := float(transition.get("previous_elapsed_sec", 0.0))
- var close_diagnostic := _handshake_close_diagnostic(code, was_authenticated)
- if close_diagnostic.is_empty():
- close_diagnostic = {
- "reason_code": "connection_lost",
- "reason": _close_reason_text(code, reason),
- }
- _transient_diagnostic = close_diagnostic
- _log_reconnect_transition(
- _postopen_close_diagnostic(
- open_elapsed_sec,
- code,
- reason,
- _url,
- ),
- maxi(1, _reconnect_attempt),
- true,
- )
- if was_authenticated:
- connection_state_changed.emit(false)
- elif not _preopen_failure_logged_for_peer:
- _preopen_failure_logged_for_peer = true
- ## A failed attempt never reached OPEN, so any post-OPEN reason
- ## belongs to the previous peer and must not describe this one.
- _transient_diagnostic.clear()
- ## Initial failure is attempt 1 for diagnostics. Later transition
- ## summaries are time-throttled so a missing listener stays
- ## observable without tying log volume to attempt duration.
- var failed_attempt := maxi(1, _reconnect_attempt)
- var connecting_elapsed_sec := float(
- transition.get("previous_elapsed_sec", 0.0)
- )
- _log_reconnect_transition(
- _preopen_failure_diagnostic(
- failed_attempt,
- connecting_elapsed_sec,
- _reconnect_timer,
- _peer.get_close_code(),
- _peer.get_close_reason(),
- _url
- ),
- failed_attempt,
- )
- _reconnect_timer -= delta
- if _reconnect_timer <= 0.0:
- _attempt_reconnect()
-
- WebSocketPeer.STATE_CLOSING:
- pass
- WebSocketPeer.STATE_CONNECTING:
- pass
-
-func get_session_id() -> String:
- return _session_id
-
-## Drain up to PACKET_DRAIN_CAP_PER_TICK inbound packets and dispatch each
-## via `_handle_message`. Anything past the cap stays in the peer's queue
-## and gets picked up next tick. The cumulative spillover count is logged
-## (via `log_buffer`) only when the cap was actually hit AND packets remain
-## — sustained flood thus emits one log line per tick with the running
-## total, while a normal-traffic frame stays silent.
-##
-## `peer` is untyped (Variant) so tests can inject a duck-typed fake with
-## `get_available_packet_count()` + `get_packet()`. Production passes the
-## real `_peer: WebSocketPeer`.
-func _drain_inbound_packets(peer) -> Dictionary:
- var drained := 0
- while peer.get_available_packet_count() > 0 and drained < PACKET_DRAIN_CAP_PER_TICK:
- var raw: String = peer.get_packet().get_string_from_utf8()
- _handle_message(raw)
- drained += 1
-
- var spilled := 0
- if drained >= PACKET_DRAIN_CAP_PER_TICK and peer.get_available_packet_count() > 0:
- spilled = peer.get_available_packet_count()
- _packet_spillover_total += spilled
- if log_buffer:
- log_buffer.log(
- (
- "[backpressure] inbound drain capped at %d/tick;"
- + " %d packets spilled to next frame (cumulative %d)"
- )
- % [PACKET_DRAIN_CAP_PER_TICK, spilled, _packet_spillover_total]
- )
-
- return {"drained": drained, "spilled": spilled}
-
-
-var is_connected: bool:
- get: return _connected and _handshake_complete
-
-
-func disconnect_from_server(reason := "Plugin unloading") -> void:
- var was_authenticated := _handshake_complete
- if _peer.get_ready_state() != WebSocketPeer.STATE_CLOSED:
- _peer.close(1000, reason)
- ## Drop the peer reference immediately instead of depending on a future
- ## `_process()` poll to finish a closing handshake. Authority revocation
- ## disables processing, so retaining a CLOSING peer here would keep the
- ## underlying channel alive beyond the revocation boundary.
- _peer = WebSocketPeer.new()
- _observed_peer_state = WebSocketPeer.STATE_CLOSED
- _peer_state_entered_msec = Time.get_ticks_msec()
- _connected = false
- ## A deliberate close owns all cleanup synchronously. Waiting for a later
- ## CLOSED poll would leave authenticated commands dispatchable after root
- ## revoked the transport authority.
- _preopen_failure_logged_for_peer = true
- _clear_on_disconnect()
- if was_authenticated:
- connection_state_changed.emit(false)
-
-
-## Revoke a copied lifecycle authority immediately. This is stronger than
-## `connect_blocked`: it also terminates an existing channel and erases every
-## queued/deferred command before returning to the caller.
-func revoke_transport(reason: String) -> void:
- auth_token = ""
- connect_blocked = true
- connect_block_reason = reason
- set_process(false)
- disconnect_from_server("Transport authority revoked")
-
-
-## Apply one proven lifecycle grant and dial immediately. Keeping the copied
-## authority transition here makes revoke/authorize symmetric and avoids
-## depending on a later CLOSED-peer frame after a plugin reload.
-func authorize_transport(p_ws_port: int, p_auth_token: String) -> void:
- ws_port = p_ws_port
- auth_token = p_auth_token
- connect_blocked = false
- connect_block_reason = ""
- server_version = ""
- _reconnect_timer = 0.0
- set_process(true)
- if is_inside_tree():
- _attempt_reconnect()
-
-
-## Reset per-connection state that was filled in by the previous server
-## and must NOT bleed into the next one. `force_restart_server` swaps
-## servers without reloading the plugin, so without this reset the dock
-## would keep showing the killed server's version until the next ack.
-## Also fires on plain reconnect-loop drops — correct either way.
-func _clear_on_disconnect() -> void:
- server_version = ""
- _handshake_started_msec = 0
- _client_nonce = ""
- _server_nonce = ""
- _challenged_server_version = ""
- _server_verified = false
- _auth_response_sent = false
- _handshake_complete = false
- ## Reset the spillover counter so a flood pattern from the previous
- ## connection doesn't pollute the next one's `logs_read` baseline.
- _packet_spillover_total = 0
- if dispatcher:
- dispatcher.clear_deferred_responses()
- ## Queued-but-unexecuted commands from the dead connection must not
- ## run under the next one (#712): their requester's futures were
- ## already failed server-side, so executing them after reconnect is
- ## an uncorrelatable surprise write.
- dispatcher.clear_command_queue()
-
-
-## Full pre-free cleanup for plugin unload: stop _process, close the
-## socket, and drop dispatcher/log_buffer refs so their Callable-held
-## RefCounted handlers decref before plugin.gd clears _handlers.
-## See issue #46 and plugin.gd::_exit_tree.
-func teardown() -> void:
- set_process(false)
- disconnect_from_server()
- dispatcher = null
- log_buffer = null
-
-
-func _connect_to_server() -> void:
- _url = "ws://127.0.0.1:%d" % ws_port
- var err := _peer.connect_to_url(_url)
- if err != OK:
- log_buffer.log("failed to initiate connection (error %d)" % err)
- _observed_peer_state = _peer.get_ready_state()
- _peer_state_entered_msec = Time.get_ticks_msec()
-
-
-func _attempt_reconnect() -> void:
- if connect_blocked:
- _log_blocked_notice_once()
- set_process(false)
- return
- var delay := _reconnect_delay_for_attempt(_reconnect_attempt)
- _reconnect_attempt += 1
- _reconnect_timer = delay
- _log_reconnect_transition(
- "connecting to server (attempt %d)" % _reconnect_attempt,
- _reconnect_attempt,
- )
- ## Always create a fresh WebSocketPeer before reconnecting. A peer that has
- ## reached STATE_CLOSED is terminal; reusing it can leave the editor stuck in
- ## a quiet reconnect loop after the Python server restarts.
- _peer = WebSocketPeer.new()
- _preopen_failure_logged_for_peer = false
- _peer.outbound_buffer_size = OUTBOUND_BUFFER_LIMIT_BYTES
- _peer.inbound_buffer_size = MAX_HANDSHAKE_FRAME_BYTES
- _peer.max_queued_packets = MAX_QUEUED_PACKETS
- _connect_to_server()
-
-
-func pause() -> void:
- _pause_depth += 1
-
-
-func resume() -> void:
- _pause_depth = maxi(0, _pause_depth - 1)
-
-
-func pause_depth() -> int:
- return _pause_depth
-
-
-static func _reconnect_delay_for_attempt(attempt_index: int) -> float:
- var delay_idx := mini(attempt_index, RECONNECT_DELAYS.size() - 1)
- return RECONNECT_DELAYS[delay_idx]
-
-
-static func _should_log_reconnect_transition(
- attempt_number: int,
- now_msec: int,
- last_log_msec: int
-) -> bool:
- ## Keep the first few transitions visible, then emit at most one summary per
- ## minute. Attempt-number throttling goes quiet for minutes when the engine
- ## spends a long time in CONNECTING, which is the state this log explains.
- return (
- attempt_number <= RECONNECT_VERBOSE_ATTEMPTS
- or last_log_msec < 0
- or now_msec - last_log_msec >= RECONNECT_LOG_HEARTBEAT_MSEC
- )
-
-
-func _log_reconnect_transition(message: String, attempt_number: int, force := false) -> void:
- if not log_buffer:
- return
- var now_msec := Time.get_ticks_msec()
- if not force and not _should_log_reconnect_transition(
- attempt_number,
- now_msec,
- _last_reconnect_transition_log_msec,
- ):
- return
- _last_reconnect_transition_log_msec = now_msec
- log_buffer.log(message)
-
-
-static func _preopen_failure_diagnostic(
- attempt_number: int,
- connecting_elapsed_sec: float,
- retry_in_sec: float,
- code: int,
- reason: String,
- url: String
-) -> String:
- var retry_text := "retrying now"
- if retry_in_sec > 0.0:
- retry_text = "retrying in %.0fs" % retry_in_sec
- return (
- "connection attempt %d failed before OPEN after %.1fs; %s"
- + " (code %d, reason %s, url %s)"
- ) % [
- attempt_number,
- maxf(0.0, connecting_elapsed_sec),
- retry_text,
- code,
- _sanitized_close_reason(reason),
- url,
- ]
-
-
-static func _postopen_close_diagnostic(
- open_elapsed_sec: float,
- code: int,
- reason: String,
- url: String
-) -> String:
- return (
- "connection lost after being open for %.1fs (code %d, reason %s, url %s); reconnecting"
- % [maxf(0.0, open_elapsed_sec), code, _sanitized_close_reason(reason), url]
- )
-
-
-static func _sanitized_close_reason(reason: String) -> String:
- var reason_label := reason.strip_edges()
- if reason_label.is_empty():
- return ""
- return reason_label.replace("\r", "\\r").replace("\n", "\\n")
-
-
-static func _close_reason_text(code: int, reason: String) -> String:
- return "Close code %d: %s" % [code, _sanitized_close_reason(reason)]
-
-
-func _handshake_close_diagnostic(code: int, authenticated := false) -> Dictionary:
- if code == CLOSE_CODE_PROTOCOL_MISMATCH:
- return {
- "reason_code": "ws_protocol_mismatch",
- "reason": "The editor and server use different Godot AI major protocols; update/restart both sides.",
- }
- if code == CLOSE_CODE_AUTH_FAILED:
- return {
- "reason_code": "ws_auth_failed",
- "reason": "The v4 editor capability was rejected; restart the managed server from this Godot editor.",
- }
- if not authenticated:
- return {
- "reason_code": "ws_handshake_failed",
- "reason": "The v4 handshake closed before authentication; update/restart both editor and server.",
- }
- return {}
-
-
-## Record one peer-state transition and return the duration of the state that
-## just ended. Kept separate from `_transport_status_snapshot` so tests can
-## exercise the status contract with injected values and no live socket.
-func _observe_peer_state(state: int, now_msec: int) -> Dictionary:
- if _peer_state_entered_msec <= 0:
- _observed_peer_state = state
- _peer_state_entered_msec = now_msec
- return {"changed": false, "previous_elapsed_sec": 0.0}
- if state == _observed_peer_state:
- return {"changed": false, "previous_elapsed_sec": 0.0}
- var previous_elapsed_sec := maxf(
- 0.0,
- (now_msec - _peer_state_entered_msec) / 1000.0,
- )
- var previous_state := _observed_peer_state
- _observed_peer_state = state
- _peer_state_entered_msec = now_msec
- return {
- "changed": true,
- "previous_state": previous_state,
- "previous_elapsed_sec": previous_elapsed_sec,
- }
-
-
-## Pure transport-status contract shared by the connection log and dock. It
-## intentionally knows nothing about lifecycle diagnoses; the thin public
-## wrapper below applies generic `blocked`, while server_lifecycle.gd remains
-## authoritative for exact terminal states such as incompatible/foreign port.
-static func _transport_status_snapshot(
- state: int,
- state_elapsed_sec: float,
- attempt: int,
- retry_timer: float
-) -> Dictionary:
- var phase := "closing"
- match state:
- WebSocketPeer.STATE_OPEN:
- phase = "connected"
- WebSocketPeer.STATE_CONNECTING:
- phase = "connecting"
- WebSocketPeer.STATE_CLOSED:
- phase = "retrying"
- WebSocketPeer.STATE_CLOSING:
- phase = "closing"
- var snapshot := {
- "phase": phase,
- "attempt": maxi(0, attempt),
- "state_elapsed_sec": maxf(0.0, state_elapsed_sec),
- }
- ## `retry_in_sec` is deliberately unrepresentable outside CLOSED-state
- ## backoff. CONNECTING is an in-flight attempt, never "retrying in 0s".
- if phase == "retrying":
- snapshot["retry_in_sec"] = maxf(0.0, retry_timer)
- return snapshot
-
-
-func get_transport_status() -> Dictionary:
- var now_msec := Time.get_ticks_msec()
- var elapsed_sec := 0.0
- if _peer_state_entered_msec > 0:
- elapsed_sec = maxf(0.0, (now_msec - _peer_state_entered_msec) / 1000.0)
- var snapshot := _transport_status_snapshot(
- _peer.get_ready_state(),
- elapsed_sec,
- _reconnect_attempt,
- _reconnect_timer,
- )
- if connect_blocked:
- snapshot["phase"] = "blocked"
- snapshot.erase("retry_in_sec")
- snapshot["reason_code"] = "connection_blocked"
- if not connect_block_reason.is_empty():
- snapshot["reason"] = connect_block_reason
- elif _connected and not _handshake_complete:
- snapshot["phase"] = "authenticating"
- snapshot.erase("retry_in_sec")
- elif not _transient_diagnostic.is_empty():
- for key in _transient_diagnostic:
- snapshot[key] = _transient_diagnostic[key]
- return snapshot
-
-
-func _log_blocked_notice_once() -> void:
- if _blocked_notice_logged:
- return
- _blocked_notice_logged = true
- if log_buffer and not connect_block_reason.is_empty():
- log_buffer.log(connect_block_reason)
-
-
-func _send_auth_hello() -> void:
- _handshake_started_msec = maxi(1, Time.get_ticks_msec())
- if not _is_lower_hex_64(auth_token):
- _fail_handshake(
- CLOSE_CODE_AUTH_FAILED,
- "missing v4 editor capability; restart the managed server from Godot",
- )
- return
- var nonce_bytes := Crypto.new().generate_random_bytes(32)
- if nonce_bytes.size() != 32:
- _fail_handshake(CLOSE_CODE_AUTH_FAILED, "could not generate v4 client nonce")
- return
- _client_nonce = nonce_bytes.hex_encode()
- if not _send_json(_build_auth_hello(_client_nonce), true):
- _fail_handshake(CLOSE_CODE_AUTH_FAILED, "could not send v4 auth hello")
-
-
-static func _build_auth_hello(client_nonce: String) -> Dictionary:
- return {
- "type": "auth_hello",
- "protocol_version": WS_PROTOCOL_VERSION,
- "client_nonce": client_nonce,
- }
-
-
-func _handle_auth_challenge(parsed: Dictionary) -> void:
- if _server_verified or parsed.get("protocol_version") != WS_PROTOCOL_VERSION:
- _fail_handshake(
- CLOSE_CODE_PROTOCOL_MISMATCH,
- "Godot AI v4 WebSocket protocol %d required" % WS_PROTOCOL_VERSION,
- )
- return
- var expected_keys: Array[String] = [
- "type",
- "protocol_version",
- "client_nonce",
- "server_nonce",
- "server_version",
- "server_proof",
- ]
- if (
- not _has_exact_keys(parsed, expected_keys)
- or parsed.get("type") != "auth_challenge"
- or parsed.get("client_nonce") != _client_nonce
- or not _is_lower_hex_64(parsed.get("server_nonce"))
- or not _is_lower_hex_64(parsed.get("server_proof"))
- or not _is_bounded_text(parsed.get("server_version"), 64)
- ):
- _fail_handshake(CLOSE_CODE_AUTH_FAILED, "invalid v4 server challenge")
- return
- var expected_proof := _server_proof(
- auth_token,
- _client_nonce,
- str(parsed["server_nonce"]),
- str(parsed["server_version"]),
- )
- if not _constant_time_equal(str(parsed["server_proof"]), expected_proof):
- _fail_handshake(CLOSE_CODE_AUTH_FAILED, "v4 server proof failed")
- return
-
- ## Only this proven transition unlocks project/session metadata.
- _server_nonce = str(parsed["server_nonce"])
- _challenged_server_version = str(parsed["server_version"])
- _server_verified = true
- _peer.inbound_buffer_size = OUTBOUND_BUFFER_LIMIT_BYTES
- _last_readiness = get_readiness()
- var response := _build_auth_response()
- if response.is_empty() or not _send_json(response, true):
- _fail_handshake(CLOSE_CODE_AUTH_FAILED, "could not send v4 editor proof")
- return
- _auth_response_sent = true
-
-
-func _build_auth_response() -> Dictionary:
- var godot_version := str(Engine.get_version_info().get("string", "unknown"))
- var project_path := ProjectSettings.globalize_path("res://")
- var plugin_version := ClientConfigurator.get_plugin_version()
- var launch_mode := ClientConfigurator.get_server_launch_mode()
- var editor_pid := OS.get_process_id()
- if (
- not _is_bounded_text(_session_id, 128)
- or not _is_bounded_text(godot_version, 64)
- or not _is_bounded_text(project_path, 4096)
- or not _is_bounded_text(plugin_version, 64)
- or not _is_bounded_text(launch_mode, 32)
- or _last_readiness not in ["ready", "importing", "playing", "no_scene"]
- or editor_pid < 0
- ):
- return {}
- var client_proof := _client_proof(
- auth_token,
- _client_nonce,
- _server_nonce,
- _challenged_server_version,
- _session_id,
- godot_version,
- project_path,
- plugin_version,
- _last_readiness,
- editor_pid,
- launch_mode,
- )
- if not _is_lower_hex_64(client_proof):
- return {}
- return {
- "type": "auth_response",
- "protocol_version": WS_PROTOCOL_VERSION,
- "client_nonce": _client_nonce,
- "server_nonce": _server_nonce,
- "client_proof": client_proof,
- "session_id": _session_id,
- "godot_version": godot_version,
- "project_path": project_path,
- "plugin_version": plugin_version,
- "readiness": _last_readiness,
- "editor_pid": editor_pid,
- "server_launch_mode": launch_mode,
- }
-
-
-## Classify one raw inbound frame. Shared by the normal dispatch path
-## (`_handle_message`, which enqueues commands) and the exclusive-run
-## service path (`_service_handle_message`, which rejects them) — one
-## parser, two sinks, so the paths can't drift. `kind` is one of:
-## "challenge", "ack", "command", "malformed_command", "ignore".
-func _classify_message(raw: String) -> Dictionary:
- var parsed = JSON.parse_string(raw)
- if parsed == null:
- push_warning("MCP: failed to parse websocket JSON")
- return {"kind": "ignore", "parsed": null}
- if not (parsed is Dictionary):
- return {"kind": "ignore", "parsed": null}
- if parsed.get("type", "") == "auth_challenge":
- return {"kind": "challenge", "parsed": parsed}
- if parsed.get("type", "") == "handshake_ack":
- return {"kind": "ack", "parsed": parsed}
- if parsed.has("request_id") and parsed.has("command"):
- if (
- parsed.get("request_id") is String
- and parsed.get("command") is String
- and (not parsed.has("params") or parsed.get("params") is Dictionary)
- ):
- return {"kind": "command", "parsed": parsed}
- return {"kind": "malformed_command", "parsed": parsed}
- return {"kind": "ignore", "parsed": parsed}
-
-
-func _handle_message(raw: String) -> void:
- if not _handshake_complete and raw.to_utf8_buffer().size() > MAX_HANDSHAKE_FRAME_BYTES:
- _fail_handshake(CLOSE_CODE_PROTOCOL_MISMATCH, "v4 handshake frame too large")
- return
- var classified := _classify_message(raw)
- if not _handshake_complete:
- match classified["kind"]:
- "challenge":
- _handle_auth_challenge(classified["parsed"])
- "ack":
- _handle_handshake_ack(classified["parsed"])
- _:
- _fail_handshake(
- CLOSE_CODE_PROTOCOL_MISMATCH,
- "Godot AI v4 expected auth_challenge or handshake_ack",
- )
- return
- match classified["kind"]:
- "challenge", "ack":
- _fail_handshake(CLOSE_CODE_PROTOCOL_MISMATCH, "unexpected v4 handshake frame")
- "command":
- if dispatcher:
- dispatcher.enqueue(classified["parsed"])
- "malformed_command":
- _reply_malformed_command(classified["parsed"])
-
-
-func _handle_handshake_ack(parsed: Dictionary) -> void:
- var expected_keys: Array[String] = ["type", "protocol_version", "server_version"]
- if (
- _handshake_complete
- or not _server_verified
- or not _auth_response_sent
- or not _has_exact_keys(parsed, expected_keys)
- or parsed.get("type") != "handshake_ack"
- or parsed.get("protocol_version") != WS_PROTOCOL_VERSION
- or parsed.get("server_version") != _challenged_server_version
- ):
- _fail_handshake(CLOSE_CODE_AUTH_FAILED, "invalid v4 handshake ACK")
- return
- server_version = _challenged_server_version
- _handshake_complete = true
- _reconnect_attempt = 0
- ## Re-emit non-default scene/play state only after the server has published
- ## this authenticated peer and normal frames are legal.
- _last_scene_path = ""
- _last_play_state = false
- _transient_diagnostic.clear()
- if log_buffer:
- log_buffer.log("authenticated with server")
- connection_state_changed.emit(true)
-
-
-func _fail_handshake(code: int, reason: String) -> void:
- if log_buffer:
- log_buffer.log("v4 websocket handshake failed: %s" % reason)
- _peer.close(code, reason)
-
-
-static func _has_exact_keys(value: Dictionary, expected: Array[String]) -> bool:
- if value.size() != expected.size():
- return false
- for key in expected:
- if not value.has(key):
- return false
- return true
-
-
-static func _is_bounded_text(value: Variant, max_length: int) -> bool:
- return value is String and not String(value).is_empty() and String(value).length() <= max_length
-
-
-static func _is_lower_hex_64(value: Variant) -> bool:
- if not (value is String) or String(value).length() != 64:
- return false
- for character in String(value):
- if not (character >= "0" and character <= "9") and not (
- character >= "a" and character <= "f"
- ):
- return false
- return true
-
-
-static func _proof_message(domain: String, values: Array) -> PackedByteArray:
- var message := domain.to_utf8_buffer()
- for value in values:
- var encoded := str(value).to_utf8_buffer()
- message.append_array(("\n%d:" % encoded.size()).to_utf8_buffer())
- message.append_array(encoded)
- return message
-
-
-static func _hmac_proof(capability: String, domain: String, values: Array) -> String:
- if not _is_lower_hex_64(capability):
- return ""
- var context := HMACContext.new()
- if context.start(HashingContext.HASH_SHA256, capability.to_utf8_buffer()) != OK:
- return ""
- if context.update(_proof_message(domain, values)) != OK:
- return ""
- return context.finish().hex_encode()
-
-
-static func _server_proof(
- capability: String,
- client_nonce: String,
- server_nonce: String,
- server_version_value: String,
-) -> String:
- return _hmac_proof(
- capability,
- _SERVER_PROOF_DOMAIN,
- [WS_PROTOCOL_VERSION, client_nonce, server_nonce, server_version_value],
- )
-
-
-static func _client_proof(
- capability: String,
- client_nonce: String,
- server_nonce: String,
- server_version_value: String,
- session_id: String,
- godot_version: String,
- project_path: String,
- plugin_version: String,
- readiness: String,
- editor_pid: int,
- server_launch_mode: String,
-) -> String:
- return _hmac_proof(
- capability,
- _CLIENT_PROOF_DOMAIN,
- [
- WS_PROTOCOL_VERSION,
- client_nonce,
- server_nonce,
- server_version_value,
- session_id,
- godot_version,
- project_path,
- plugin_version,
- readiness,
- editor_pid,
- server_launch_mode,
- ],
- )
-
-
-static func _constant_time_equal(left: String, right: String) -> bool:
- var difference := left.length() ^ right.length()
- var length := maxi(left.length(), right.length())
- for index in length:
- var left_code := left.unicode_at(index) if index < left.length() else 0
- var right_code := right.unicode_at(index) if index < right.length() else 0
- difference |= left_code ^ right_code
- return difference == 0
-
-
-## Never enqueue a malformed command frame: the dispatcher's typed casts
-## would error on the queue head every tick, wedging every later command
-## behind it. Reply with an error when the request_id is usable so the
-## server's pending future resolves instead of waiting out the full
-## command timeout.
-func _reply_malformed_command(parsed: Dictionary) -> void:
- push_warning("MCP: dropping malformed command frame (request_id/command must be String, params a Dictionary)")
- var rid: Variant = parsed.get("request_id")
- if rid is String and not String(rid).is_empty():
- var response := ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Malformed command frame: request_id/command must be strings and params a dict"
- )
- response["request_id"] = rid
- response["readiness"] = get_readiness()
- _stamp_error_watermark(response)
- _send_json(response)
-
-
-## Send a state event to the server (not a command response).
-func send_event(event_name: String, data: Dictionary = {}) -> bool:
- return _send_json({"type": "event", "event": event_name, "data": data})
-
-
-## Push a command response for a request_id whose handler deferred its reply
-## (see McpDispatcher.DEFERRED_RESPONSE). `payload` must carry either a `data`
-## or `error` field in the same shape handlers normally return.
-func send_deferred_response(request_id: String, payload: Dictionary) -> void:
- if dispatcher != null and not dispatcher.has_pending_deferred_response(request_id):
- if log_buffer:
- log_buffer.log("[defer] dropped late response for expired request %s" % request_id)
- return
- var response := payload.duplicate()
- response["request_id"] = request_id
- if not response.has("status"):
- response["status"] = "ok" if payload.has("data") else "error"
- ## Symmetric with McpDispatcher::_dispatch — stamp live readiness on the
- ## deferred reply so the server's session cache self-heals from any
- ## response, not just the synchronous ones. Lets `project_stop` (the
- ## main deferred-response producer) stay correct even if its bespoke
- ## `readiness_after` payload field were ever dropped.
- if not response.has("readiness"):
- response["readiness"] = get_readiness()
- if not response.has("error_watermark"):
- _stamp_error_watermark(response)
- if _send_json(response) and dispatcher != null:
- dispatcher.complete_deferred_response(request_id)
-
-
-## Result of one cooperative transport-servicing pass during an exclusive
-## synchronous run (currently only the test runner). PAUSED is an
-## invariant violation for callers, not a healthy state: a pause held
-## across servicing checkpoints would silently starve the heartbeat.
-enum ServiceStatus { SERVICED, DISCONNECTED, PAUSED, BLOCKED }
-
-## Cumulative cap on application packets processed across ONE exclusive
-## run. Counts every drained packet — valid command, malformed frame, or
-## ack-like — so no frame kind evades it. Past the cap the connection is
-## closed (1013): bounded rejects, never unbounded stale buffering. 2048
-## leaves headroom under Godot's default max_queued_packets (4096) and
-## sits above stormtest's ~1000-call default workload; tune with
-## telemetry/benchmarks if rejection traffic ever extends a checkpoint.
-const EXCLUSIVE_RUN_PACKET_CAP := 2048
-const CLOSE_CODE_EXCLUSIVE_RUN_FLOOD := 1013
-## Reject-log throttle: first few rejects verbatim, then periodic totals.
-const _SERVICE_REJECT_LOG_FIRST := 5
-const _SERVICE_REJECT_LOG_EVERY := 100
-
-## Service the WebSocket transport from inside a long synchronous handler
-## (an "exclusive run" — the test runner). The editor main thread is
-## blocked, so `_process` cannot poll; without this the server keepalive
-## (`DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS` / `_PING_TIMEOUT_SECONDS` in
-## `transport/websocket.py` — 20s interval, 60s pong deadline since #958)
-## closes the session mid-run. See docs/test-run-transport-starvation-plan.md.
-##
-## Contract — do NOT extend this method to dispatch:
-## - `WebSocketPeer.poll()` has no heartbeat-only mode; it also buffers
-## application frames. Buffering them past this call would replay them
-## STALE after their server-side futures expire (the #712 hazard), so
-## every drained command frame is REJECTED immediately with a retryable
-## EDITOR_NOT_READY / EDITOR_TEST_RUNNING error instead.
-## - Drains to quiescence: poll → drain everything available → poll again,
-## until no packets remain. A full packet queue could hide a ping deeper
-## in the TCP stream, so nothing may spill to a later checkpoint.
-## - `run_state` is caller-owned mutable state carrying the cumulative
-## packet counter under "packets_serviced" — no connection-global
-## lifecycle that could leak if the run dies.
-func service_transport_during_exclusive_run(run_state: Dictionary) -> ServiceStatus:
- if connect_blocked:
- return ServiceStatus.BLOCKED
- if pause_processing:
- return ServiceStatus.PAUSED
- while true:
- _peer.poll()
- if _peer.get_ready_state() != WebSocketPeer.STATE_OPEN:
- return ServiceStatus.DISCONNECTED
- if _peer.get_available_packet_count() == 0:
- return ServiceStatus.SERVICED
- while _peer.get_available_packet_count() > 0:
- var raw: String = _peer.get_packet().get_string_from_utf8()
- if _service_note_packet(run_state):
- if log_buffer:
- log_buffer.log(
- "[busy] packet flood during test run (%d > cap %d) — closing connection"
- % [int(run_state.get("packets_serviced", 0)), EXCLUSIVE_RUN_PACKET_CAP]
- )
- _peer.close(CLOSE_CODE_EXCLUSIVE_RUN_FLOOD, "command flood during test run")
- return ServiceStatus.DISCONNECTED
- _service_handle_message(raw, int(run_state.get("packets_serviced", 0)))
- ## Unreachable: every exit above returns. Keeps the typed signature happy.
- return ServiceStatus.SERVICED
-
-
-## Shared between-phase checkpoint for exclusive runs: deadline first
-## (cheap), then transport servicing via `service_cb`. Returns "" to
-## continue, or a terminal outcome: "timeout" | "transport_lost" |
-## "paused". Static and stateless so the runner's between-test checkpoints
-## and the handler's discovery checkpoints share ONE outcome mapping —
-## PAUSED is abort-worthy (a held pause would silently skip every later
-## poll and starve the heartbeat), and DISCONNECTED/BLOCKED both mean "no
-## live transport".
-static func exclusive_run_checkpoint(
- service_cb: Callable, deadline_ticks_ms: int, run_state: Dictionary
-) -> String:
- if deadline_ticks_ms > 0 and Time.get_ticks_msec() >= deadline_ticks_ms:
- return "timeout"
- if not service_cb.is_valid():
- return ""
- var status: int = service_cb.call(run_state)
- if status == ServiceStatus.SERVICED:
- return ""
- if status == ServiceStatus.PAUSED:
- return "paused"
- return "transport_lost"
-
-
-## Count one application packet against the exclusive-run cap. Counts EVERY
-## drained packet regardless of kind (valid command, malformed, ack-like)
-## so no frame kind can evade the flood limit. Returns true once the cap is
-## exceeded — the caller closes the connection.
-static func _service_note_packet(run_state: Dictionary) -> bool:
- var count: int = int(run_state.get("packets_serviced", 0)) + 1
- run_state["packets_serviced"] = count
- return count > EXCLUSIVE_RUN_PACKET_CAP
-
-
-## Exclusive-run sink for `_classify_message`: acks are still processed,
-## malformed frames keep their normal reply, and valid commands are
-## rejected without touching the dispatcher.
-func _service_handle_message(raw: String, packets_serviced: int) -> void:
- var classified := _classify_message(raw)
- match classified["kind"]:
- "challenge", "ack":
- _fail_handshake(CLOSE_CODE_PROTOCOL_MISMATCH, "unexpected v4 handshake frame")
- "command":
- _service_reject_command(classified["parsed"], packets_serviced)
- "malformed_command":
- _reply_malformed_command(classified["parsed"])
-
-
-func _service_reject_command(parsed: Dictionary, packets_serviced: int) -> void:
- _send_json(_build_service_reject(parsed))
- if log_buffer and (
- packets_serviced <= _SERVICE_REJECT_LOG_FIRST
- or packets_serviced % _SERVICE_REJECT_LOG_EVERY == 0
- ):
- ## Ring-buffer only (echo=false): a flood must not bury the console.
- log_buffer.log(
- "[busy] rejected '%s' during test run (packet %d)"
- % [parsed.get("command", ""), packets_serviced],
- false,
- )
-
-
-## Build the busy-reject response for a valid command frame that arrived
-## mid-run. Split from the send so tests can assert the exact wire shape.
-func _build_service_reject(parsed: Dictionary) -> Dictionary:
- var command: String = parsed.get("command", "")
- var response := ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_TEST_RUNNING,
- (
- "A test run is in progress on this editor — '%s' was not executed. "
- + "Retry when the run completes, or fetch results afterward with "
- + "test_manage(op=\"results_get\")."
- ) % command,
- true,
- )
- response["request_id"] = parsed.get("request_id", "")
- response["readiness"] = get_readiness()
- _stamp_error_watermark(response)
- return response
-
-
-func _hook_editor_signals() -> void:
- # Scene change: poll in _process since there's no direct signal for scene switch
- # Play state: EditorInterface signals
- EditorInterface.get_editor_settings() # ensure interface is ready
- _last_scene_path = _get_current_scene_path()
- _last_play_state = EditorInterface.is_playing_scene()
- _last_play_state_for_run = _last_play_state
-
-
-var _last_scene_path := ""
-var _last_play_state := false
-## Separate edge tracker for game-run bookkeeping: _last_play_state only
-## advances when the play_state_changed event sends successfully, but ending
-## run tracking must not depend on the websocket being up.
-var _last_play_state_for_run := false
-var _last_readiness := ""
-
-
-## Compute current editor readiness from live Godot state.
-static func get_readiness() -> String:
- if EditorInterface.get_resource_filesystem().is_scanning():
- return "importing"
- if EditorInterface.is_playing_scene():
- return "playing"
- if EditorInterface.get_edited_scene_root() == null:
- return "no_scene"
- return "ready"
-
-
-## Check for scene/play state changes each frame (lightweight polling).
-func _check_state_changes() -> void:
- var scene_path := _get_current_scene_path()
- if scene_path != _last_scene_path:
- if send_event("scene_changed", {"current_scene": scene_path}):
- _last_scene_path = scene_path
- if log_buffer:
- log_buffer.log("[event] scene_changed -> %s" % scene_path)
-
- var playing := EditorInterface.is_playing_scene()
- if playing != _last_play_state:
- var state := "playing" if playing else "stopped"
- if send_event("play_state_changed", {"play_state": state}):
- _last_play_state = playing
- if log_buffer:
- log_buffer.log("[event] play_state_changed -> %s" % state)
-
- var readiness := get_readiness()
- if readiness != _last_readiness:
- if send_event("readiness_changed", {"readiness": readiness}):
- _last_readiness = readiness
- if log_buffer:
- ## echo=false: readiness flips on every filesystem scan
- ## (each import cycles importing -> ready), so echoing to
- ## console spams every install during normal editing (#626).
- ## The line stays in the ring for the dock's log panel.
- log_buffer.log("[event] readiness -> %s" % readiness, false)
-
-
-## Stopped↔playing edge handler for game-run bookkeeping. Handles both the
-## stopped→playing edge (adopting editor-started runs) and the playing→stopped
-## edge (ending runs). Runs every process tick (any socket state) so a
-## self-quit game's run ends even while the transport is down or reconnecting.
-func _check_game_run_play_state(playing: bool) -> void:
- if playing == _last_play_state_for_run:
- return
- if debugger_plugin != null:
- if playing:
- ## #891: adopt a play the user started from the editor. The
- ## debugger session's own adoption in `_setup_session` only fires
- ## when `is_playing_scene()` is already true when the session
- ## attaches, which an F5/F6 launch routinely misses; this edge is
- ## the other half of that race. Polled every tick regardless of
- ## socket state, same as the stop edge below.
- debugger_plugin.note_editor_play_started()
- else:
- debugger_plugin.note_editor_play_stopped()
- _last_play_state_for_run = playing
-
-
-func _get_current_scene_path() -> String:
- var scene_root := EditorInterface.get_edited_scene_root()
- return scene_root.scene_file_path if scene_root else ""
-
-
-func _send_json(data: Dictionary, allow_pre_auth := false) -> bool:
- if not _connected or (not _handshake_complete and not allow_pre_auth):
- return false
- var text := JSON.stringify(data)
- if allow_pre_auth and text.to_utf8_buffer().size() > MAX_HANDSHAKE_FRAME_BYTES:
- return false
- var buffered_bytes := _peer.get_current_outbound_buffered_amount()
- ## `send_text` encodes the string to UTF-8 internally, so an exact
- ## `to_utf8_buffer().size()` here would encode every payload twice. Almost
- ## all payloads sit far below the limit, so gate on a cheap upper bound
- ## (<= 4 UTF-8 bytes per code point) and only pay for the exact count when
- ## the estimate lands near the backpressure ceiling.
- if _might_exceed_outbound_backpressure(buffered_bytes, text.length()):
- var message_bytes := text.to_utf8_buffer().size()
- if _would_exceed_outbound_backpressure(buffered_bytes, message_bytes):
- return _handle_outbound_backpressure(data, buffered_bytes, message_bytes)
- var err := _peer.send_text(text)
- if err != OK:
- if log_buffer:
- log_buffer.log("[send] websocket send_text failed: %s" % error_string(err))
- return false
- return true
-
-
-static func _would_exceed_outbound_backpressure(buffered_bytes: int, message_bytes: int) -> bool:
- return buffered_bytes + message_bytes > OUTBOUND_BUFFER_LIMIT_BYTES
-
-
-## Cheap pre-check on the code-point count: UTF-8 uses at most 4 bytes per code
-## point, so `char_count * 4` upper-bounds the encoded size. When even that
-## upper bound fits under the ceiling the payload is definitely safe and we can
-## skip the exact encode; only a positive here warrants `to_utf8_buffer()`.
-static func _might_exceed_outbound_backpressure(buffered_bytes: int, char_count: int) -> bool:
- return buffered_bytes + char_count * 4 > OUTBOUND_BUFFER_LIMIT_BYTES
-
-
-func _handle_outbound_backpressure(
- data: Dictionary,
- buffered_bytes: int,
- message_bytes: int,
-) -> bool:
- var request_id: String = data.get("request_id", "")
- if request_id.is_empty():
- if log_buffer:
- log_buffer.log(
- "[send] requestless payload blocked by websocket backpressure "
- + "(buffered=%d, message=%d, limit=%d)"
- % [buffered_bytes, message_bytes, OUTBOUND_BUFFER_LIMIT_BYTES]
- )
- return false
-
- var err_response := _make_backpressure_error(request_id, buffered_bytes, message_bytes)
- _stamp_error_watermark(err_response)
- var err_text := JSON.stringify(err_response)
- var err_bytes := err_text.to_utf8_buffer().size()
- if _would_exceed_outbound_backpressure(buffered_bytes, err_bytes):
- if log_buffer:
- log_buffer.log(
- "[send] dropped response for request %s due to websocket backpressure "
- + "(buffered=%d, message=%d, limit=%d)"
- % [request_id, buffered_bytes, message_bytes, OUTBOUND_BUFFER_LIMIT_BYTES]
- )
- return false
-
- var send_err := _peer.send_text(err_text)
- if send_err != OK:
- if log_buffer:
- log_buffer.log("[send] websocket backpressure error send failed: %s" % error_string(send_err))
- return false
- if log_buffer:
- log_buffer.log(
- "[send] %s -> error: outbound websocket backpressure"
- % data.get("command", "response")
- )
- return true
-
-
-static func _make_backpressure_error(
- request_id: String,
- buffered_bytes: int,
- message_bytes: int,
-) -> Dictionary:
- return {
- "request_id": request_id,
- "status": "error",
- "data": {},
- ## Stamp readiness on the backpressure error too — the server's
- ## per-response self-heal applies to every response shape the
- ## plugin emits, and the next legitimate reply may already be
- ## queued behind this one.
- "readiness": get_readiness(),
- "error": {
- "code": ErrorCodes.INTERNAL_ERROR,
- "message": (
- "Outbound WebSocket buffer is full; dropped response before queueing "
- + "more data. Retry with a smaller payload (for screenshots, lower "
- + "max_resolution or set include_image=false)."
- ),
- "data": {
- "buffered_bytes": buffered_bytes,
- "message_bytes": message_bytes,
- "limit_bytes": OUTBOUND_BUFFER_LIMIT_BYTES,
- },
- },
- }
-
-
-func _stamp_error_watermark(response: Dictionary) -> void:
- McpSurfacedErrorTracker.stamp_watermark(response, surfaced_error_tracker)
-
-
-## Build a human-readable session ID of form "@<16hex>" from the project path.
-## The slug is derived from the project directory name so agents can recognize
-## which editor they're targeting; the hex suffix disambiguates same-project twins.
-static func _make_session_id(project_path: String) -> String:
- var base := project_path.rstrip("/\\").get_file()
- if base == "":
- base = "project"
- var slug := _slugify(base)
- if slug == "":
- slug = "project"
- var random_bytes := Crypto.new().generate_random_bytes(8)
- if random_bytes.size() != 8:
- return ""
- var suffix := random_bytes.hex_encode()
- return "%s@%s" % [slug, suffix]
-
-
-static func _slugify(s: String) -> String:
- var out := ""
- var prev_dash := false
- for c in s.to_lower():
- if (c >= "a" and c <= "z") or (c >= "0" and c <= "9"):
- out += c
- prev_dash = false
- elif not prev_dash and out != "":
- out += "-"
- prev_dash = true
- return out.trim_suffix("-")
-
-
-static func _should_regenerate_session_id(
- close_code: int,
- server_verified: bool,
- handshake_complete: bool,
-) -> bool:
- ## A duplicate rejection arrives after the server proved possession of the
- ## capability but before its final ACK. Ignore unauthenticated close codes so
- ## an unrelated listener cannot churn the editor identity.
- return (
- close_code == CLOSE_CODE_DUPLICATE_SESSION
- and server_verified
- and not handshake_complete
- )
diff --git a/addons/godot_ai/connection.gd.uid b/addons/godot_ai/connection.gd.uid
deleted file mode 100644
index ff78405..0000000
--- a/addons/godot_ai/connection.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bmnk8rsotiks2
diff --git a/addons/godot_ai/custom_tools/custom_tool_wrapper.gd b/addons/godot_ai/custom_tools/custom_tool_wrapper.gd
deleted file mode 100644
index b4e13c7..0000000
--- a/addons/godot_ai/custom_tools/custom_tool_wrapper.gd
+++ /dev/null
@@ -1,80 +0,0 @@
-@tool
-extends RefCounted
-
-## bridge between dispatcher and addon handler
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-var _spec: McpCustomToolSpec
-var _locator: McpServiceLocator
-var _handler_instance = null # lazily loaded from _spec.script_path
-
-func _init(spec: McpCustomToolSpec, locator: McpServiceLocator) -> void:
- _spec = spec
- _locator = locator
-
-
-func quiesce_for_script_swap() -> Dictionary:
- if _handler_instance == null:
- return {"ok": true}
- if not _handler_instance.has_method("quiesce_for_script_swap"):
- return {
- "ok": false,
- "error": "Custom tool '%s' must provide quiesce_for_script_swap() before hot updates." % _spec.name,
- }
- var result: Variant = _handler_instance.call("quiesce_for_script_swap")
- if not result is Dictionary or result.get("ok") != true:
- return {"ok": false, "error": "Custom tool '%s' still owns active work." % _spec.name}
- return {"ok": true}
-
-## Invoked by dispatcher._call_handler as .call(params) — SINGLE ARG.
-## Internally splits into (clean_params, ctx) for the addon handler.
-func invoke(params: Dictionary) -> Dictionary:
- ## Dock enable/disable gate: a disabled tool is dropped from the
- ## catalog push, but a client holding a stale list (or batch_execute)
- ## can still name it — reject at dispatch too.
- var registry := McpToolRegistry.get_instance()
- if registry != null and not registry.is_tool_enabled(_spec.name):
- return ErrorCodes.make(ErrorCodes.CUSTOM_TOOL_DISABLED,
- "Custom tool '%s' is disabled in the Godot AI dock" % _spec.name)
- ## Readiness gate (https://github.com/hi-godot/godot-ai/issues/781#issuecomment-5036376599 #1): block writes during play/import.
- ## Checked BEFORE the lazy load so a gated tool's handler script (and any
- ## _init side effects) never runs while the editor is busy.
- var _readiness := McpConnection.get_readiness()
- if _spec.requires_writable and (_readiness == "importing" or _readiness == "playing"):
- return ErrorCodes.make(ErrorCodes.EDITOR_NOT_READY, "Editor is '%s' — write blocked for custom tool '%s'" % [_readiness, _spec.name])
- if _handler_instance == null:
- var script := load(_spec.script_path) as GDScript
- if script == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Cannot load %s" % _spec.script_path)
- _handler_instance = script.new() # no-arg; per-call context arrives via ctx
- if not _handler_instance.has_method(_spec.method):
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "%s has no method '%s' for custom tool '%s'" % [_spec.script_path, _spec.method, _spec.name])
- ## Extract _request_id (dispatcher injected it) and strip from params
- ## so the addon sees clean params matching its declared schema (https://github.com/hi-godot/godot-ai/issues/781#issuecomment-5036376599 #2).
- var request_id: String= params.get("_request_id", "")
- var clean_params := params.duplicate()
- clean_params.erase("_request_id")
- ## Construct ctx with transport metadata + live-object locator.
- var ctx := McpCallContext.new()
- ctx.request_id = request_id
- ctx.session_id = _locator.get_connection().get_session_id()
- ctx.spec = _spec
- ctx.attach_locator(_locator)
- ctx.deadline_msec = Time.get_ticks_msec() + _spec.timeout_ms
- var result: Dictionary = _handler_instance.call(_spec.method, clean_params, ctx)
- if result.get("_deferred", false):
- ## Enforce the declared contract: batch_execute and the server's
- ## timeout budget both trust spec.deferred, so a non-deferred spec
- ## whose handler defers anyway would report success while its real
- ## reply arrives uncorrelated later.
- if not _spec.deferred:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Custom tool '%s' returned a deferred response but its spec declares deferred=false" % _spec.name)
- if _spec.timeout_ms > 0:
- ## Handlers return the SHARED McpDispatcher.DEFERRED_RESPONSE
- ## const, which Godot makes read-only — stamping the per-spec
- ## budget on it directly is a script error that aborts the call.
- result = result.duplicate()
- result["_deferred_timeout_ms"] = _spec.timeout_ms
- return result
diff --git a/addons/godot_ai/custom_tools/custom_tool_wrapper.gd.uid b/addons/godot_ai/custom_tools/custom_tool_wrapper.gd.uid
deleted file mode 100644
index ff02cb9..0000000
--- a/addons/godot_ai/custom_tools/custom_tool_wrapper.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b68uel6lsb7jr
diff --git a/addons/godot_ai/custom_tools/mcp_call_context.gd b/addons/godot_ai/custom_tools/mcp_call_context.gd
deleted file mode 100644
index 6e40d86..0000000
--- a/addons/godot_ai/custom_tools/mcp_call_context.gd
+++ /dev/null
@@ -1,44 +0,0 @@
-@tool
-class_name McpCallContext
-extends RefCounted
-
-## Per-call context handed to addon handlers.
-##
-## STABLE addon-facing surface (see AGENTS.md — published class_names are
-## permanent compat surface): `request_id`, `session_id`, `deadline_msec`,
-## `spec` (read-only), `is_expired()`, `send_deferred()`. Anything
-## underscore-prefixed is internal wiring owned by the wrapper and may
-## change between releases — addons must not reach into it. Keeping this
-## surface narrow now is deliberate: it is much cheaper than breaking
-## published addons later (#875 review).
-
-var request_id: String = ""
-var session_id: String = ""
-var deadline_msec: int = 0
-var spec: McpCustomToolSpec = null
-
-## Internal — injected by custom_tool_wrapper via attach_locator(). Do not
-## use from addon code; the capability methods below are the contract.
-var _locator: McpServiceLocator = null
-
-
-## Internal — called by the wrapper during ctx construction.
-func attach_locator(locator: McpServiceLocator) -> void:
- _locator = locator
-
-
-func is_expired() -> bool:
- if deadline_msec == 0:
- return false
- return Time.get_ticks_msec() > deadline_msec
-
-
-func send_deferred(payload: Dictionary) -> void:
- if _locator == null:
- push_error("McpCallContext: cannot send deferred response, locator is null")
- return
- var conn := _locator.get_connection()
- if conn == null:
- push_error("McpCallContext: cannot send deferred response, connection is null")
- return
- conn.send_deferred_response(request_id, payload)
diff --git a/addons/godot_ai/custom_tools/mcp_call_context.gd.uid b/addons/godot_ai/custom_tools/mcp_call_context.gd.uid
deleted file mode 100644
index e37765b..0000000
--- a/addons/godot_ai/custom_tools/mcp_call_context.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bdgqe4gyian2o
diff --git a/addons/godot_ai/custom_tools/mcp_custom_tool_spec.gd b/addons/godot_ai/custom_tools/mcp_custom_tool_spec.gd
deleted file mode 100644
index 8c5a551..0000000
--- a/addons/godot_ai/custom_tools/mcp_custom_tool_spec.gd
+++ /dev/null
@@ -1,66 +0,0 @@
-@tool
-class_name McpCustomToolSpec
-extends RefCounted
-
-## Descriptor for one custom tool registered by an addon. Mirrors the
-## clients/_base.gd data-only descriptor pattern: fields are set by the
-## caller, NO Callables (handler instance is materialized lazily by the
-## dispatcher from script_path + method, so hot-reload can't SEGV a
-## worker mid-call — same rationale as McpClient, issue #229).
-##
-## Construction: instantiate and set fields, then pass to
-## McpToolRegistry.register(spec). Do NOT subclass — custom tools are
-## dynamic (one addon may register several), not file-system-scanned
-## like clients.
-
-# --- identity ---
-var name: String = "" ## tool name, e.g. "gdunit_run". Must not shadow a built-in.
-var description: String = "" ## shown to the agent by the MCP server
-var params_schema: Dictionary = {} ## JSON Schema; advertised to the agent for shaping calls. Params are forwarded UNVALIDATED — the handler must validate its own input.
-
-# --- handler resolution (lazy materialization by dispatcher) ---
-var script_path: String = "" ## "res://addons/.../handler.gd"; load()ed on first call
-var method: StringName = &"" ## method on the handler; signature: (params: Dictionary, ctx: McpCallContext) -> Dictionary
-
-# --- source identity (https://github.com/hi-godot/godot-ai/issues/781#issuecomment-5036376599 #8) ---
-var source_path: String = "" ## "plugin.cfg" path: "res://addons/gdunit4_mcp/plugin.cfg". Same path = same addon (replace allowed); different path colliding = reject + dock warning. NOTE: self-declared — a collision/ownership policy for cooperating addons, NOT a security boundary (any in-editor code can claim any path).
-var source: String = "" ## display: "gdunit4_mcp". If empty, registry reads [plugin] name from source_path.
-
-# --- exposure ---
-var promoted: bool = false ## opt-in: ask the server to ALSO register this tool as a first-class MCP tool ("custom_") with params_schema attached, so agents get native schemas/validation instead of the custom_manage indirection. The server caps promoted count; overflow stays reachable via custom_manage.
-
-# --- execution contract ---
-var timeout_ms: int = 4500 ## deferred timeout → DEFERRED_TIMEOUT_MS_BY_COMMAND["custom:"]
-var deferred: bool = false ## true → handler may return DEFERRED_RESPONSE, push later via ctx.send_deferred(payload)
-var requires_writable: bool = false ## https://github.com/hi-godot/godot-ai/issues/781#issuecomment-5036376599 #1: readiness gate. false → reads run any time; true → blocks during play/import
-var undoable: bool = false ## https://github.com/hi-godot/godot-ai/issues/781#issuecomment-5036376599 #6: must be true to participate in undo=true batch_execute
-
-# --- budgets (https://github.com/hi-godot/godot-ai/issues/781#issuecomment-5036376599 #5, enforced at register time) ---
-const MAX_DESCRIPTION_CHARS := 600
-const MAX_SCHEMA_BYTES := 8192
-const MAX_TIMEOUT_MS := 120000
-const MIN_TIMEOUT_MS := 500
-
-
-## Returns an array of human-readable validation errors (empty = valid).
-## Called by McpToolRegistry.register() and usable standalone for early
-## feedback in addon _enter_tree before the registry is even live.
-func validate() -> Array[String]:
- var errors: Array[String] = []
- if name.is_empty():
- errors.append("name is empty\n")
- if not name.is_valid_ascii_identifier():
- errors.append("name '%s' is not a valid identifier\n" % name)
- if description.length() > MAX_DESCRIPTION_CHARS:
- errors.append("description exceeds %d chars\n" % MAX_DESCRIPTION_CHARS)
- if JSON.stringify(params_schema).to_utf8_buffer().size() > MAX_SCHEMA_BYTES:
- errors.append("params_schema exceeds %d bytes\n" % MAX_SCHEMA_BYTES)
- if script_path.is_empty() or not ResourceLoader.exists(script_path):
- errors.append("script_path '%s' does not exist" % script_path)
- if method.is_empty():
- errors.append("method is empty")
- if timeout_ms < MIN_TIMEOUT_MS or timeout_ms > MAX_TIMEOUT_MS:
- errors.append("timeout_ms %d out of range [%d, %d]" % [timeout_ms, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS])
- if source_path.is_empty() or not source_path.ends_with("plugin.cfg") or not FileAccess.file_exists(source_path):
- errors.append("source_path '%s' must be an existing plugin.cfg path" % source_path)
- return errors
diff --git a/addons/godot_ai/custom_tools/mcp_custom_tool_spec.gd.uid b/addons/godot_ai/custom_tools/mcp_custom_tool_spec.gd.uid
deleted file mode 100644
index 42fe408..0000000
--- a/addons/godot_ai/custom_tools/mcp_custom_tool_spec.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://h7k8j5tpscwy
diff --git a/addons/godot_ai/custom_tools/mcp_service_locator.gd b/addons/godot_ai/custom_tools/mcp_service_locator.gd
deleted file mode 100644
index 4e50ac5..0000000
--- a/addons/godot_ai/custom_tools/mcp_service_locator.gd
+++ /dev/null
@@ -1,16 +0,0 @@
-@tool
-class_name McpServiceLocator
-extends RefCounted
-
-var _connection: McpConnection = null
-var _log_buffer: McpLogBuffer = null
-
-func setup(connection: McpConnection, log_buffer: McpLogBuffer) -> void:
- _connection = connection
- _log_buffer = log_buffer
-
-func get_connection() -> McpConnection:
- return _connection
-
-func get_log_buffer() -> McpLogBuffer:
- return _log_buffer
diff --git a/addons/godot_ai/custom_tools/mcp_service_locator.gd.uid b/addons/godot_ai/custom_tools/mcp_service_locator.gd.uid
deleted file mode 100644
index 1815007..0000000
--- a/addons/godot_ai/custom_tools/mcp_service_locator.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dfvphmvo7en3i
diff --git a/addons/godot_ai/custom_tools/mcp_tool_registry.gd b/addons/godot_ai/custom_tools/mcp_tool_registry.gd
deleted file mode 100644
index 407d405..0000000
--- a/addons/godot_ai/custom_tools/mcp_tool_registry.gd
+++ /dev/null
@@ -1,277 +0,0 @@
-@tool
-class_name McpToolRegistry
-extends RefCounted
-
-## Central registry for custom tools. INSTANCE SINGLETON (not static) —
-## unlike McpClientRegistry which is read-only/loaded-once/query-only (static
-## is fine there), this registry is MUTABLE, has LIFECYCLE (clear on teardown),
-## must EMIT SIGNALS to multiple consumers, and needs DEPENDENCY INJECTION
-## (dispatcher, locator). Static funcs can't emit instance signals in GDScript,
-## and static vars create #46 freed-instance regressions across reloads.
-##
-## plugin.gd owns the instance (new + setup + clear). Addons access it via
-## get_instance() so they don't hold a reference. Source identity is
-## spec.source_path (explicit plugin.cfg path, https://github.com/hi-godot/godot-ai/issues/781#issuecomment-5036376599 #8) — same source_path
-## = same addon (hot-reload replace); different source_path colliding on
-## a name = reject + dock warning.
-
-## singleton instance
-static var _instance: McpToolRegistry = null
-
-## Singleton accessor for addons. Returns null if Godot AI isn't loaded.
-static func get_instance() -> McpToolRegistry:
- return _instance
-
-
-## Instance fields (NOT static — lifecycle tied to plugin.gd)
-var _specs: Dictionary = {} # name -> McpCustomToolSpec
-var _by_source_path: Dictionary = {} # source_path -> Array[McpCustomToolSpec] (batch unregister index)
-var _dispatcher: McpDispatcher = null # injected by plugin.gd via setup()
-var _locator: McpServiceLocator = null # injected by plugin.gd via setup()
-var _ready: bool = false
-var _promoted_cnt = 0
-
-## Instance signals (LEGAL — instance funcs can emit these). Multiple consumers
-## (plugin.gd, dock, addons) can connect.
-signal tools_changed # triggers custom_tools_changed push to server
-signal registry_ready # https://github.com/hi-godot/godot-ai/issues/781#issuecomment-5036376599 #8: emit after Godot AI reload so addons can re-register
-
-
-## Called by plugin.gd in _enter_tree. Injects live dependencies and registers
-## the singleton. Addons can now reach the registry via get_instance().
-func setup(dispatcher: McpDispatcher, locator: McpServiceLocator) -> void:
- _dispatcher = dispatcher
- _locator = locator
- _load_disabled_tools()
- _instance = self
-
-
-func register(spec: McpCustomToolSpec) -> bool:
- if not _validate_one(spec):
- return false
- _commit_one(spec)
- tools_changed.emit()
- return true
-
-
-## Atomic batch registration: validate ALL specs first (phase 1, zero
-## mutation), then commit ALL (phase 2, cannot fail), then emit ONCE.
-## A phase-1 failure leaves the registry AND dispatcher untouched — a
-## per-spec register-then-bail loop would leave earlier specs committed
-## and dispatchable while the server's tool list never hears about them.
-func batch_register(specs: Array[McpCustomToolSpec]) -> bool:
- if specs.is_empty():
- return true
- ## Phase 1 — validate only. Also reject duplicate names WITHIN the
- ## batch: _validate_one checks against committed state (nothing from
- ## this batch is committed yet), and silently letting the last
- ## duplicate win would hide an addon packaging bug.
- var batch_names := {}
- for spec in specs:
- if not _validate_one(spec):
- return false
- if batch_names.has(spec.name):
- push_warning("McpToolRegistry: duplicate name '%s' within batch — rejected" % spec.name)
- return false
- batch_names[spec.name] = true
- ## Phase 2 — commit everything, emit once.
- for spec in specs:
- _commit_one(spec)
- tools_changed.emit()
- return true
-
-
-## Phase 1 helper: validation + collision policy. NO registry/dispatcher
-## mutation — the only side effect is filling the display-source fallback
-## on the spec itself, which is harmless on reject.
-func _validate_one(spec: McpCustomToolSpec) -> bool:
- var errors := spec.validate()
- if not errors.is_empty():
- push_error("McpToolRegistry: rejecting spec '%s': %s" % [spec.name, ", ".join(errors)])
- return false
- ## Fill display source from plugin.cfg if addon left it empty.
- if spec.source.is_empty():
- spec.source = _read_plugin_name(spec.source_path)
- ## reject-on-collision: same source_path (hot-reload) replaces, different rejects
- var existing: McpCustomToolSpec= _specs.get(spec.name)
- if existing != null and existing.source_path != spec.source_path:
- push_warning("McpToolRegistry: name '%s' collision between %s and %s — rejected" % [spec.name, existing.source_path, spec.source_path])
- return false
- return true
-
-
-## Phase 2 helper: commit one ALREADY-VALIDATED spec. Handles the
-## hot-reload replace (same source_path — guaranteed by _validate_one).
-## Never fails, never emits — callers own the tools_changed emit.
-func _commit_one(spec: McpCustomToolSpec) -> void:
- var existing: McpCustomToolSpec = _specs.get(spec.name)
- if existing != null:
- ## Replace: purge the OLD registration from all four dispatcher
- ## dicts. Command name is "custom_tool:" — passing the bare
- ## name would leave a materialized Callable in _handlers and the
- ## hot-reloaded spec would silently never take effect.
- _dispatcher.unregister("custom_tool:" + existing.name, "custom:" + existing.name)
- _erase_from_source_path_index_locked(existing)
- _specs[spec.name] = spec
- _by_source_path.get_or_add(spec.source_path, []).append(spec)
- ## Bridge to dispatcher lazy registration: register_lazy_handler + register_lazy.
- ## handler_key = "custom:"; wrapper is CustomToolWrapper (https://github.com/hi-godot/godot-ai/issues/781#issuecomment-5036376599 #2).
-
- var handler_key := "custom:" + spec.name
- var wrapper_path := "res://addons/godot_ai/custom_tools/custom_tool_wrapper.gd"
- _dispatcher.register_lazy_handler(handler_key, wrapper_path, [spec, _locator])
- _dispatcher.register_lazy("custom_tool:" + spec.name, handler_key, &"invoke")
-
-
-func unregister(name: String) -> bool:
- var spec: McpCustomToolSpec = _specs.get(name)
- if spec == null:
- return false
- if _dispatcher != null:
- _dispatcher.unregister("custom_tool:" + name, "custom:" + name)
- _specs.erase(name)
- _erase_from_source_path_index_locked(spec)
- tools_changed.emit()
- return true
-
-
-## https://github.com/hi-godot/godot-ai/issues/781#issuecomment-5036376599 #8: batch-unregister all tools from one addon. Pass the source_path
-## (plugin.cfg path) — the registry removes every spec whose source_path matches.
-func unregister_source(source_path: String) -> int:
- var specs: Array = _by_source_path.get(source_path, [])
- var count := specs.size()
- for spec in specs.duplicate():
- if _dispatcher != null:
- _dispatcher.unregister("custom_tool:" + spec.name, "custom:" + spec.name)
- _specs.erase(spec.name)
- _by_source_path.erase(source_path)
- if count > 0:
- tools_changed.emit()
- return count
-
-
-func all() -> Array[McpCustomToolSpec]:
- var out: Array[McpCustomToolSpec] = []
- for spec in _specs.values():
- out.append(spec)
- return out
-
-
-## Enabled specs only — the catalog-push source. Disabled tools stay
-## registered (the dock still lists them for re-enabling) but are never
-## advertised to the server and are rejected at dispatch.
-func enabled() -> Array[McpCustomToolSpec]:
- var out: Array[McpCustomToolSpec] = []
- for spec in _specs.values():
- if is_tool_enabled(spec.name):
- out.append(spec)
- return out
-
-
-# --- per-tool enable state (dock UI) ---
-## Persisted per-project via EditorSettings project metadata (the same
-## store the editor uses for per-project UI state — no project.godot
-## churn, no cross-project bleed). Applies LIVE: toggling re-emits
-## tools_changed, which re-pushes the filtered catalog to the server.
-
-const _META_SECTION := "godot_ai"
-const _META_KEY_DISABLED := "disabled_custom_tools"
-
-var _disabled_tools: Dictionary = {} # name -> true
-
-
-func is_tool_enabled(name: String) -> bool:
- return not _disabled_tools.has(name)
-
-
-func set_tool_enabled(name: String, tool_enabled: bool) -> void:
- if tool_enabled:
- if not _disabled_tools.has(name):
- return
- _disabled_tools.erase(name)
- else:
- if _disabled_tools.has(name):
- return
- _disabled_tools[name] = true
- _save_disabled_tools()
- tools_changed.emit()
-
-
-func _load_disabled_tools() -> void:
- _disabled_tools.clear()
- var es := _editor_settings()
- if es == null:
- return
- var stored: Variant = es.get_project_metadata(_META_SECTION, _META_KEY_DISABLED, PackedStringArray())
- for name in PackedStringArray(stored):
- _disabled_tools[String(name)] = true
-
-
-func _save_disabled_tools() -> void:
- var es := _editor_settings()
- if es == null:
- return
- var names := PackedStringArray()
- for name in _disabled_tools.keys():
- names.append(String(name))
- es.set_project_metadata(_META_SECTION, _META_KEY_DISABLED, names)
-
-
-static func _editor_settings() -> EditorSettings:
- if not Engine.is_editor_hint():
- return null
- return EditorInterface.get_editor_settings()
-
-
-func get_spec(name: String) -> McpCustomToolSpec:
- var out: McpCustomToolSpec = _specs.get(name)
- return out
-
-
-func is_ready() -> bool:
- return _ready
-
-
-## Called from plugin.gd teardown — wipes everything so stale specs/Callables
-## don't survive a Godot AI reload (#46 regression class). Signals die with
-## the instance (consumers disconnect via plugin.gd's own teardown).
-func clear() -> void:
- _specs.clear()
- _by_source_path.clear()
- _disabled_tools.clear() # in-memory only; persistence reloads on next setup()
- _ready = false
- _dispatcher = null
- _locator = null
- _instance = null
-
-
-## Called from plugin.gd _enter_tree after setup(). Addons that missed
-## _enter_tree during a Godot AI reload listen for registry_ready (via
-## get_instance().registry_ready.connect(...)) and re-register deterministically.
-func mark_ready() -> void:
- _ready = true
- registry_ready.emit()
-
-
-# --- internal helpers ---
-
-func _erase_from_source_path_index_locked(spec: McpCustomToolSpec) -> void:
- var specs: Array = _by_source_path.get(spec.source_path, [])
- specs.erase(spec)
- if specs.is_empty():
- _by_source_path.erase(spec.source_path)
-
-
-## Read [plugin] name from a plugin.cfg for display purposes.
-## Returns "" on any error — display source is best-effort.
-static func _read_plugin_name(plugin_cfg_path: String) -> String:
- var cfg := ConfigFile.new()
- if cfg.load(plugin_cfg_path) != OK:
- return ""
- return cfg.get_value("plugin", "name", "")
-
-static func _get_command_name(spec: McpCustomToolSpec) -> String:
- return "custom_tool:" + spec.name
-
-static func _get_handler_name(spec: McpCustomToolSpec) -> String:
- return "custom:" + spec.name
diff --git a/addons/godot_ai/custom_tools/mcp_tool_registry.gd.uid b/addons/godot_ai/custom_tools/mcp_tool_registry.gd.uid
deleted file mode 100644
index 23b9822..0000000
--- a/addons/godot_ai/custom_tools/mcp_tool_registry.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bq2btxtm8flhb
diff --git a/addons/godot_ai/debugger/mcp_debugger_plugin.gd b/addons/godot_ai/debugger/mcp_debugger_plugin.gd
deleted file mode 100644
index 0bf9320..0000000
--- a/addons/godot_ai/debugger/mcp_debugger_plugin.gd
+++ /dev/null
@@ -1,2088 +0,0 @@
-@tool
-class_name McpDebuggerPlugin
-extends EditorDebuggerPlugin
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-const ScriptWork := preload("res://addons/godot_ai/utils/script_work.gd")
-
-## Editor-side half of the game-process capture bridge.
-##
-## The game-side counterpart (`plugin/addons/godot_ai/runtime/game_helper.gd`,
-## registered as autoload `_mcp_game_helper`) listens on EngineDebugger's
-## message channel. This plugin sends "mcp:take_screenshot" requests and
-## routes the replies back through the WebSocket McpConnection using the
-## request_id the MCP dispatcher threaded through params.
-##
-## Why this exists: the game always runs as a separate OS process. Even
-## "Embed Game Mode" on Windows/Linux (and macOS 4.5+) just reparents the
-## game's window into the editor — the game's framebuffer is never reachable
-## from the editor's Viewport. The debugger channel is the engine's own
-## supported IPC and works identically regardless of embed mode.
-
-const CAPTURE_PREFIX := "mcp"
-## CI runners under xvfb can be slow to spin up the game subprocess and
-## register the autoload's capture. 8s keeps the message responsive for
-## interactive users while still covering slow-CI startup.
-const DEFAULT_TIMEOUT_SEC := 8.0
-## How long to wait for the game-side autoload to beacon mcp:hello
-## before sending the screenshot request. Godot's debugger drops
-## messages whose prefix has no registered capture, so sending
-## take_screenshot before the game registers its "mcp" capture is a
-## silent black hole. On CI the game subprocess has been observed
-## taking ~15s to boot + register.
-const GAME_READY_WAIT_SEC := 20.0
-## #500: how long to wait for the game-side autoload to beacon mcp:hello before
-## issuing a game_eval. This is deliberately MUCH shorter than the 20s
-## screenshot wait above: the eval path's total editor-side budget is this wait
-## plus the 250ms liveness probe and 10s eval backstop
-## (request_game_eval's timeout_sec), and that total MUST stay below the 15s
-## game_eval timeout enforced at two layers: the Python server's send_command
-## budget (src/godot_ai/handlers/editor.py::game_eval) and this plugin's own
-## deferred budget (dispatcher.gd's 15000ms game_eval entry, editor/plugin-side
-## — not server-side). Either firing produces the opaque tail.
-## With the 20s screenshot wait, a not-yet-ready game made the editor poll past
-## the 15s deadline, so the server gave up first with an opaque
-## ~15s TimeoutError instead of the actionable "Is the game actually running?"
-## error below ever reaching the client (#500's residual TimeoutError bucket).
-## 3s wait + 0.25s probe + 10s backstop = 13.25s, comfortably under the 15s
-## server timeout, so the actionable error always wins. A game launched moments
-## before the eval still has the 3s grace to register; if it needs longer, the
-## user gets a fast, clear "is it running?" rather than a 15s hang.
-const EVAL_READY_WAIT_SEC := 3.0
-## #859: once the helper has registered, confirm its game loop is still
-## advancing before dispatching an eval. The helper replies synchronously from
-## the debugger capture, so 250ms is ample while keeping not-ready failures
-## below the telemetry target of 0.5s.
-const EVAL_LIVENESS_WAIT_SEC := 0.25
-## #490: how long to wait for the game's mcp:eval_compiled beacon before
-## concluding the eval source failed to compile. A parse error aborts the
-## game-side handler before it can reply, so without this we'd wait the
-## full eval timeout for a syntax mistake. reload() of valid source is
-## sub-millisecond, so 3s is comfortably clear of false positives.
-const EVAL_COMPILE_GRACE_SEC := 3.0
-## #490: once an eval compiles, the editor polls the game every this many
-## seconds with mcp:eval_check. A backgrounded play-in-editor game has a
-## frozen idle loop (no _process / SceneTreeTimer ticks) so it can't
-## self-report a runtime error that aborted the eval — but its debugger
-## capture callback still answers a probe. The editor's own loop keeps
-## ticking, so it drives the poll. 0.35s keeps detection well under a second
-## without flooding the channel; most evals reply before the first probe.
-const EVAL_PROBE_INTERVAL_SEC := 0.35
-const GAME_DEBUG_CONTROL_TIMEOUT_SEC := 5.0
-const EMBED_SUSPEND_TOGGLE := 0
-const EMBED_NEXT_FRAME := 1
-
-const VisionRoutingScript := preload("res://addons/godot_ai/vision_routing.gd")
-
-var _log_buffer: McpLogBuffer
-var _game_log_buffer: McpGameLogBuffer
-var _editor_log_buffer: McpEditorLogBuffer
-var _surfaced_error_tracker
-var vision_routing: VisionRoutingScript = null
-
-## Pending request_id -> phase-specific data. Every eval phase carries kind
-## and connection; timer-backed phases also retain their bound timeout lambda
-## so `_clear_pending` can disconnect it on success/error. Otherwise the
-## SceneTreeTimer pins the captured request_id until its timeout elapses.
-var _pending: Dictionary = {}
-
-## Testing seam for the readiness wait below. Production leaves this invalid
-## and awaits SceneTree.process_frame; editor tests inject a synchronous
-## callable because their runner does not pump frames while a test is running.
-var _eval_ready_frame_waiter: Callable = Callable()
-
-## Flipped true when the game-side autoload sends its "mcp:hello" boot
-## beacon for the current project_run. Reset as soon as a new run is
-## requested, before Godot has attached the fresh debugger session, so
-## editor_state cannot leak readiness from the previous game process.
-var _game_ready := false
-var _game_run_token := 0
-var _ready_run_token := -1
-var _game_session_id := -1
-var _game_run_active := false
-## Debugger session id of an `mcp:hello` that arrived before the run was
-## adopted, or -1 when none is held (#891). A manually started play can beat
-## its own adoption: the helper's boot beacon is a one-shot, so discarding it
-## left the runtime bridge dead for the whole run with no retry. Buffer it and
-## replay on adoption instead. Cleared whenever a run ends, so a beacon can
-## never carry across into a later run.
-var _pending_hello_session_id := -1
-## True when holding a beacon already rotated the game-log run id, so the
-## adoption that consumes it must not rotate again (#891 review).
-var _pre_adoption_log_rotated := false
-var _manual_run_armed := false
-var _game_run_started_msec := 0
-var _game_run_started_editor_cursor := 0
-var _game_run_started_debugger_cursor := 0
-var _game_helper_expected := true
-
-## #645: a GDScript parse error hit while an editor-launched game boots calls
-## GDScriptLanguage::debug_break_parse — the game parks in a remote-debugger
-## break BEFORE the helper's mcp:hello and before any record reaches the
-## Errors tab, the editor Logger, or the game log. The only editor-side traces
-## are the debugger break signals; the stack frames land in the Stack Trace
-## panel a few frames later. Track the break here so game_status can report
-## status="break" and a synthesized error record can name the failure.
-var _break_active := false
-var _break_can_debug := false
-var _break_reason := ""
-var _break_pre_live := false
-var _break_run_token := -1
-var _break_record_synthesized := false
-
-## #645: how long after the break signal to scrape the Stack Trace panel for
-## frames. The editor requests the stack dump from the game separately, so the
-## panel is empty at signal time; ~0.5s has it populated. The late tick
-## synthesizes with whatever is available so a scrape failure still yields a
-## record carrying the break reason.
-const BREAK_FRAME_SCRAPE_DELAYS_SEC: Array[float] = [0.5, 2.0]
-
-
-
-func _init(log_buffer: McpLogBuffer = null, game_log_buffer: McpGameLogBuffer = null, editor_log_buffer: McpEditorLogBuffer = null, surfaced_error_tracker = null, vision_routing: VisionRoutingScript = null) -> void:
- _log_buffer = log_buffer
- _game_log_buffer = game_log_buffer
- _editor_log_buffer = editor_log_buffer
- _surfaced_error_tracker = surfaced_error_tracker
- self.vision_routing = vision_routing
-
-
-func quiesce_for_script_swap() -> Dictionary:
- if not _pending.is_empty():
- return {"ok": false, "error": "Wait for pending game debugger requests before updating."}
- return ScriptWork.quiescence()
-
-
-func _has_capture(prefix: String) -> bool:
- return prefix == CAPTURE_PREFIX
-
-
-## Fires when a debugger session attaches — once for the editor's own
-## self-session at startup, and again each time the user hits Play and a
-## new game subprocess connects. Reset _game_ready so the next capture
-## request waits for the (new) game's mcp:hello beacon before sending,
-## avoiding stale-flag timeouts across Play→Stop→Play cycles.
-##
-## Do NOT log here: add_debugger_plugin() triggers this virtual before
-## plugin.gd's _enter_tree logs "plugin loaded", and ci-reload-test
-## asserts "plugin loaded" is the first line after a plugin reload.
-func _setup_session(session_id: int) -> void:
- _connect_session_stopped(session_id)
- _connect_session_break_signals(session_id)
- if EditorInterface.is_playing_scene() and not _game_run_active:
- _begin_game_run_tracking(_editor_log_cursor(), true, true, true, true, true)
- else:
- _game_ready = false
- _ready_run_token = -1
- _game_session_id = session_id
-
-
-func begin_game_run(editor_log_cursor: int = 0, helper_expected: bool = true) -> void:
- _begin_game_run_tracking(editor_log_cursor, helper_expected, true, true)
-
-
-func _begin_game_run_tracking(
- editor_log_cursor: int = 0,
- helper_expected: bool = true,
- rotate_game_log: bool = true,
- sticky_debugger_scan: bool = true,
- quiet: bool = false,
- manual_armed: bool = false,
- keep_session_id: int = -1,
-) -> void:
- _game_run_token += 1
- _game_run_active = true
- _manual_run_armed = manual_armed
- _game_ready = false
- _ready_run_token = -1
- ## Adopting a run whose game is ALREADY attached keeps that session id
- ## (#891 review): clearing it would leave every staleness guard comparing
- ## against -1, so a foreign session's `stopped` could end this live run —
- ## `_on_debugger_session_stopped` lets a manual-armed run end on any
- ## session while the id is unknown. Spawn-first callers pass -1 and keep
- ## the historical clear, since their game has not attached yet.
- _game_session_id = keep_session_id
- clear_debug_break()
- _game_run_started_msec = Time.get_ticks_msec()
- _game_run_started_editor_cursor = maxi(0, editor_log_cursor)
- if _surfaced_error_tracker != null:
- _surfaced_error_tracker.note_game_run_started(sticky_debugger_scan)
- _game_run_started_debugger_cursor = _surfaced_error_tracker.debugger_promoted_total()
- else:
- _game_run_started_debugger_cursor = 0
- _game_helper_expected = helper_expected
- var run_id := ""
- if _game_log_buffer and rotate_game_log:
- if _pre_adoption_log_rotated:
- ## Already rotated when this game's beacon was held, so its boot
- ## output is tagged with the run id this adoption is about to
- ## announce. Rotating again here would orphan those lines under a
- ## superseded id and `logs_read(source="game")` — which returns the
- ## CURRENT run only — would report an empty log for a game whose
- ## only output happens in `_ready()` (#891 review).
- run_id = _game_log_buffer.run_id()
- else:
- run_id = _game_log_buffer.clear_for_new_run()
- _pre_adoption_log_rotated = false
- if _log_buffer and not quiet:
- var log_text := "[debug] game capture pending run token %d" % _game_run_token
- if not run_id.is_empty():
- log_text += " (run %s)" % run_id
- _log_buffer.log(log_text)
- ## #891: a beacon that beat this adoption is held rather than dropped —
- ## consume it now so the bridge comes up without waiting for a second
- ## hello the game will never send.
- _replay_pending_hello()
-
-
-func _editor_log_cursor() -> int:
- return _editor_log_buffer.appended_total() if _editor_log_buffer != null else 0
-
-
-func end_game_run() -> void:
- _fail_pending_evals_not_ready(
- "The game run ended before game_eval completed — the game stopped, crashed, or is restarting. Confirm it is running and retry."
- )
- _fail_pending_game_debug_controls_not_ready(
- "The game run ended before runtime control completed — confirm it is running and retry."
- )
- ## Never carry a held beacon across a run boundary (#891).
- _pending_hello_session_id = -1
- _pre_adoption_log_rotated = false
- _game_run_active = false
- _manual_run_armed = false
- _game_ready = false
- _ready_run_token = -1
- _game_session_id = -1
- clear_debug_break()
- if _surfaced_error_tracker != null:
- _surfaced_error_tracker.note_game_run_stopped()
-
-
-## Editor play-state edge, stopped -> playing (#891). `_setup_session` adopts a
-## manually started play only when `EditorInterface.is_playing_scene()` is
-## ALREADY true at the instant Godot attaches the debugger session — an F5/F6
-## launch routinely loses that race, and the run was then never adopted at all,
-## leaving game_eval, game logs and runtime inspection silently dead for its
-## whole lifetime. This closes the race from the other side: whichever of the
-## two edges lands second performs the adoption. No-op for MCP-started runs,
-## which `project_run` already adopted via `begin_game_run`.
-func note_editor_play_started() -> void:
- if _game_run_active:
- return
- ## State belonging to the game that is already talking must survive the
- ## adoption that is about to reset run bookkeeping (#891 review).
- var attached_session := _game_session_id
- var had_break := _break_active
- var break_can_debug := _break_can_debug
- var break_reason := _break_reason
- _begin_game_run_tracking(
- _editor_log_cursor(), true, true, true, true, true, attached_session
- )
- if had_break:
- ## A boot parse error can break the game before the play-state edge
- ## lands. `_begin_game_run_tracking` clears break state, which would
- ## drop the actionable #645 diagnosis and leave game_status reporting
- ## "not_live" instead of "break". Re-record it against the new run so
- ## `_break_pre_live` and the synthetic-error scan are computed for THIS
- ## run — the beacon will never arrive for a game parked at a break.
- note_debug_break(break_can_debug, break_reason)
-
-
-## Authoritative fallback for runs whose debugger `stopped` signal never
-## fired or was never connected: the editor's play state falling to stopped
-## means the game process is gone. A game that exits on its own
-## (get_tree().quit(), crash) has no MCP stop op to run the bookkeeping, and
-## without this game_status stayed "live" until the next run (#642 smoke).
-## Called on the playing→stopped edge only, so the pre-play launch window
-## (run tracking begun, is_playing_scene() not yet true) is never clipped.
-func note_editor_play_stopped() -> void:
- if not _game_run_active:
- ## A beacon held for an adoption that never happened must not survive
- ## into the next run (#891).
- _pending_hello_session_id = -1
- _pre_adoption_log_rotated = false
- return
- end_game_run()
-
-
-## Apply the game helper's boot beacon: the game has registered its "mcp"
-## capture and is safe to send take_screenshot / eval requests to — before
-## this, Godot's debugger drops our messages silently. Shared by the live
-## `mcp:hello` branch in `_capture` and by the buffered replay in
-## `_replay_pending_hello`, so a held beacon produces exactly the same state
-## and side effects as one that arrived after adoption (#891).
-func _accept_game_hello(session_id: int) -> void:
- _pending_hello_session_id = -1
- ## Bind the run to the game that actually announced itself. Adoption
- ## clears `_game_session_id`, so without this a replayed beacon would
- ## leave the run bound to nothing and the staleness checks toothless
- ## until Godot's next `_setup_session` (#891 review).
- if session_id != -1:
- _game_session_id = session_id
- _game_ready = true
- _ready_run_token = _game_run_token
- ## #641: boot-time parse errors race the hello beacon — both ride
- ## the same debugger channel, and the editor inserts Errors-tab
- ## rows with a per-frame throttle, so rows can land moments after
- ## the run is declared live. Arm forced scans so those rows get
- ## promoted into the watermark even if no tool call follows.
- if _surfaced_error_tracker != null:
- _surfaced_error_tracker.schedule_deferred_scans()
- if _log_buffer:
- if _game_log_buffer:
- _log_buffer.log("[debug] <- mcp:hello from game_helper (run %s)" % _game_log_buffer.run_id())
- else:
- _log_buffer.log("[debug] <- mcp:hello from game_helper")
-
-
-## Consume a beacon that arrived before this run was adopted. Called at the end
-## of run-tracking setup; no-op when nothing is held (#891).
-func _replay_pending_hello() -> void:
- if _pending_hello_session_id == -1:
- return
- var held := _pending_hello_session_id
- if _game_session_id != -1 and held != _game_session_id:
- ## Belongs to a different debugger session than the one this run is
- ## bound to — drop it rather than declare the wrong game live.
- _pending_hello_session_id = -1
- if _log_buffer:
- _log_buffer.log("[debug] dropped held mcp:hello from debugger session %d (current %d)" % [held, _game_session_id])
- return
- if _log_buffer:
- _log_buffer.log("[debug] replaying held mcp:hello from debugger session %d" % held)
- _accept_game_hello(held)
-
-
-func _connect_session_stopped(session_id: int) -> void:
- var session = get_session(session_id)
- if session == null:
- return
- var stopped := Callable(self, "_on_debugger_session_stopped").bind(session_id)
- if not session.stopped.is_connected(stopped):
- session.stopped.connect(stopped)
-
-
-func _on_debugger_session_stopped(session_id: int) -> void:
- if _game_session_id != -1 and session_id != _game_session_id:
- return
- ## MCP-started runs normally end via project_manage(op="stop"), but a game
- ## that exits on its own (get_tree().quit(), crash) emits only this signal.
- ## Without ending the run here, game_status stays "live" until the next
- ## run's bookkeeping rewrites it (#642 live smoke). Before the game session
- ## attaches (_game_session_id == -1) only manual runs may end on this
- ## signal — a foreign session's stop must not cancel a launching MCP run.
- if not _manual_run_armed and _game_session_id == -1:
- return
- end_game_run()
-
-
-## #859: a closed debugger session is authoritative. Do not leave evals on
-## their 10s backstop after the game process has already gone away. Other
-## pending request kinds keep their existing ownership and timeout behavior.
-func _fail_pending_evals_not_ready(message: String) -> void:
- for request_id in _pending.keys():
- var pending: Dictionary = _pending.get(request_id, {})
- if str(pending.get("kind", "")) not in [
- "eval_ready_wait", "eval_liveness", "eval"
- ]:
- continue
- var connection: McpConnection = pending.get("connection")
- _clear_pending(str(request_id))
- if connection != null and is_instance_valid(connection):
- _send_error(connection, str(request_id), ErrorCodes.EVAL_GAME_NOT_READY, message)
-
-
-func _is_current_game_run(run_token: int) -> bool:
- return _game_run_active and _game_run_token == run_token
-
-
-func _fail_eval_not_ready(request_id: String, message: String) -> void:
- var pending: Dictionary = _pending.get(request_id, {})
- var connection: McpConnection = pending.get("connection")
- _clear_pending(request_id)
- if connection != null and is_instance_valid(connection):
- _send_error(connection, request_id, ErrorCodes.EVAL_GAME_NOT_READY, message)
-
-
-## --- #645: boot-time debugger breaks ---------------------------------------
-
-func _connect_session_break_signals(session_id: int) -> void:
- var session = get_session(session_id)
- if session != null:
- var breaked_cb := Callable(self, "_on_debugger_session_breaked").bind(session_id)
- if not session.breaked.is_connected(breaked_cb):
- session.breaked.connect(breaked_cb)
- var continued_cb := Callable(self, "_on_debugger_session_continued").bind(session_id)
- if not session.continued.is_connected(continued_cb):
- session.continued.connect(continued_cb)
- _connect_script_debugger_breaked()
-
-
-## The session-level `breaked` signal carries only can_debug; the underlying
-## ScriptEditorDebugger's own `breaked` also carries the human-readable break
-## reason ("Parser Error: ..."), which is otherwise visible only in the
-## Debugger UI. EditorDebuggerSession does not expose its debugger node, so
-## locate ScriptEditorDebugger instances by walking the editor UI — the same
-## approach as the tracker's Errors-tab scrape.
-func _connect_script_debugger_breaked() -> void:
- var base := EditorInterface.get_base_control()
- if base == null:
- return
- var debuggers: Array[Node] = []
- _collect_nodes_of_class(base, "ScriptEditorDebugger", debuggers)
- for dbg in debuggers:
- if not dbg.has_signal("breaked"):
- continue
- var cb := Callable(self, "_on_script_debugger_breaked")
- if not dbg.is_connected("breaked", cb):
- dbg.connect("breaked", cb)
-
-
-static func _collect_nodes_of_class(node: Node, klass: String, out: Array[Node]) -> void:
- if node.get_class() == klass:
- out.append(node)
- for child in node.get_children():
- _collect_nodes_of_class(child, klass, out)
-
-
-func _on_debugger_session_breaked(can_debug: bool, session_id: int) -> void:
- if _game_session_id != -1 and session_id != _game_session_id:
- return
- note_debug_break(can_debug, "")
-
-
-func _on_debugger_session_continued(session_id: int) -> void:
- if _game_session_id != -1 and session_id != _game_session_id:
- return
- clear_debug_break()
-
-
-## reallydid=false is Godot's "left the break" notification (debug_exit).
-func _on_script_debugger_breaked(reallydid: bool, can_debug: bool, reason: String, _has_stackdump: bool) -> void:
- if not reallydid:
- clear_debug_break()
- return
- note_debug_break(can_debug, reason)
-
-
-## Record that the game process is parked in a remote-debugger break. Fires
-## once per break from the session signal (no reason text) and again moments
-## later from the ScriptEditorDebugger signal (with reason) — the notices
-## merge into one break. Public so tests can drive break state directly.
-func note_debug_break(can_debug: bool, reason: String) -> void:
- var first_notice := not _break_active
- _break_active = true
- _break_can_debug = can_debug
- if not reason.is_empty():
- _break_reason = reason
- if not first_notice:
- return
- _break_run_token = _game_run_token
- _break_pre_live = _game_run_active and not is_game_capture_ready()
- _break_record_synthesized = false
- if _log_buffer:
- _log_buffer.log("[debug] debugger break (pre_live=%s can_debug=%s)" % [str(_break_pre_live), str(can_debug)])
- if _break_pre_live:
- _schedule_break_record_synthesis()
-
-
-func clear_debug_break() -> void:
- _break_active = false
- _break_can_debug = false
- _break_reason = ""
- _break_pre_live = false
- _break_run_token = -1
- _break_record_synthesized = false
-
-
-## Stack frames land in the Stack Trace panel a few frames after the break
-## signal (the editor requests the stack dump separately), so the record is
-## synthesized on short timers rather than at signal time.
-func _schedule_break_record_synthesis() -> void:
- var tree := Engine.get_main_loop() as SceneTree
- if tree == null:
- return
- var token := _break_run_token
- for i in BREAK_FRAME_SCRAPE_DELAYS_SEC.size():
- var final := i == BREAK_FRAME_SCRAPE_DELAYS_SEC.size() - 1
- var timer := tree.create_timer(BREAK_FRAME_SCRAPE_DELAYS_SEC[i])
- var work := ScriptWork.begin("debugger_break_scrape")
- timer.timeout.connect(func() -> void:
- _on_break_scrape_tick(token, final)
- ScriptWork.finish(work))
-
-
-func _on_break_scrape_tick(run_token: int, final: bool) -> void:
- if not _break_active or _break_record_synthesized or run_token != _break_run_token:
- return
- var frames := _scrape_break_stack_frames()
- if frames.is_empty() and not final:
- return
- synthesize_break_error_record(frames)
-
-
-## Read the debugger's Stack Trace panel rows. Row metadata is a Dictionary
-## {frame, file, function, line} regardless of editor locale, so stack trees
-## are identified by metadata shape rather than the translated column title.
-func _scrape_break_stack_frames() -> Array[Dictionary]:
- var base := EditorInterface.get_base_control()
- if base == null:
- return []
- var debuggers: Array[Node] = []
- _collect_nodes_of_class(base, "ScriptEditorDebugger", debuggers)
- for dbg in debuggers:
- var trees: Array[Node] = []
- _collect_nodes_of_class(dbg, "Tree", trees)
- for t in trees:
- var frames := _frames_from_stack_tree(t as Tree)
- if not frames.is_empty():
- return frames
- return []
-
-
-static func _frames_from_stack_tree(tree: Tree) -> Array[Dictionary]:
- var frames: Array[Dictionary] = []
- var root := tree.get_root()
- if root == null:
- return frames
- var item := root.get_first_child()
- while item != null:
- var meta = item.get_metadata(0)
- if not (meta is Dictionary and meta.has("file") and meta.has("frame")):
- return []
- frames.append({
- "path": str(meta.get("file", "")),
- "line": int(meta.get("line", 0)),
- "function": str(meta.get("function", "")),
- })
- item = item.get_next()
- return frames
-
-
-## Build the Errors-tab-shaped record for a boot-time break and promote it via
-## the tracker so recent_editor_errors_since / logs_read / the watermark all
-## surface it — the break itself produces no record anywhere else (#645).
-## Public so tests can synthesize without waiting on scrape timers.
-func synthesize_break_error_record(frames: Array[Dictionary]) -> void:
- if _break_record_synthesized:
- return
- _break_record_synthesized = true
- if _surfaced_error_tracker == null:
- return
- var reason := _break_reason
- if reason.is_empty():
- reason = "Game process broke into the debugger during startup (script parse/load error; reason not captured)"
- var top: Dictionary = frames[0] if not frames.is_empty() else {}
- var location := {
- "path": str(top.get("path", "")),
- "line": int(top.get("line", 0)),
- "function": str(top.get("function", "")),
- }
- var entry := {
- "source": "editor",
- "level": "error",
- "text": reason,
- "path": location["path"],
- "line": location["line"],
- "function": location["function"],
- "details": {
- "debugger_tab": "Stack Trace",
- "message": reason,
- "error_type_name": "debugger_break",
- "source": location.duplicate(true),
- "resolved": location.duplicate(true),
- "frames": frames.duplicate(true),
- },
- }
- _surfaced_error_tracker.record_synthetic_error(entry)
- if _log_buffer:
- _log_buffer.log("[debug] synthesized boot-break error record: %s" % McpSurfacedErrorTracker.format_editor_error_summary(entry))
-
-
-## --- end #645 ---------------------------------------------------------------
-
-
-func is_game_capture_ready() -> bool:
- return _game_run_active and _game_ready and _ready_run_token == _game_run_token
-
-
-static func with_liveness_flags(status: Dictionary) -> Dictionary:
- var enriched := status.duplicate(true)
- var state := str(enriched.get("status", "stopped"))
- enriched["helper_live"] = state == "live"
- enriched["session_active"] = not state in ["not_live", "stopped"]
- return enriched
-
-
-func get_game_status(now_msec: int = -1, ready_wait_sec: float = GAME_READY_WAIT_SEC) -> Dictionary:
- var resolved_now := Time.get_ticks_msec() if now_msec < 0 else now_msec
- var ready_wait_msec := maxi(0, int(ready_wait_sec * 1000.0))
- var elapsed_msec := maxi(0, resolved_now - _game_run_started_msec) if _game_run_active else 0
- ## "stopped" also covers idle/never-ran; no game run is currently active.
- var status := "stopped"
- if _game_run_active:
- ## #645: a parked process takes precedence over "live" — a game frozen
- ## in a remote-debugger break cannot service game-side tools even when
- ## its helper registered before the break.
- if _break_active:
- status = "break"
- elif is_game_capture_ready():
- status = "live"
- elif not _game_helper_expected:
- status = "no_helper"
- elif elapsed_msec >= ready_wait_msec:
- status = "not_live"
- else:
- status = "launching"
- var out := {
- "status": status,
- "run_token": _game_run_token,
- "active": _game_run_active,
- "ready": is_game_capture_ready(),
- "helper_expected": _game_helper_expected,
- "run_started_msec": _game_run_started_msec,
- "elapsed_msec": elapsed_msec,
- "ready_wait_msec": ready_wait_msec,
- "editor_log_cursor": _game_run_started_editor_cursor,
- }
- if status == "break":
- out["break"] = {
- "reason": _break_reason,
- "can_debug": _break_can_debug,
- "pre_live": _break_pre_live,
- }
- return with_liveness_flags(out)
-
-
-func _explain_not_live(status: Dictionary, code: String = ErrorCodes.INTERNAL_ERROR) -> Dictionary:
- var state := str(status.get("status", "stopped"))
- var errors_info := recent_editor_errors_since(int(status.get("editor_log_cursor", 0)))
- var recent_errors: Array = errors_info.get("errors", [])
- var recent_errors_scope := str(errors_info.get("scope", "none"))
- var truncated := bool(errors_info.get("truncated", false))
- var data := {
- "game_status": status.duplicate(true),
- "recent_errors": recent_errors,
- "recent_errors_scope": recent_errors_scope,
- "recent_errors_may_predate_run": recent_errors_scope == "retained_recent",
- "recent_errors_truncated": truncated,
- }
- data.merge(split_errors_by_scope(recent_errors, recent_errors_scope), true)
- var message := ""
- match state:
- "not_live":
- if not recent_errors.is_empty() and recent_errors_scope == "run":
- message = "The game failed to load or crashed before the Godot AI game helper registered: %s. Check logs_read(source='editor', include_details=true)." % _format_editor_error_summary(recent_errors[0])
- if truncated:
- message += " Editor logs since this run may be truncated; showing retained errors."
- elif not recent_errors.is_empty():
- message = "The game is not responding and reported no load errors during this run. A recent editor error may be related, but may predate this run: %s. Check logs_read(source='editor', include_details=true)." % _format_editor_error_summary(recent_errors[0])
- else:
- message = "The game is not responding and reported no load errors before the helper-ready window elapsed. It may still be booting or may have failed silently; check logs_read(source='editor', include_details=true) and retry."
- "break":
- var break_info: Dictionary = status.get("break", {})
- var break_reason := str(break_info.get("reason", ""))
- var reason_suffix := (": %s" % break_reason) if not break_reason.is_empty() else ""
- if bool(break_info.get("pre_live", true)):
- message = "The game hit a script error during startup and is frozen at a debugger break%s. It cannot become live; call project_manage(op='stop') to end the run, fix the error, and relaunch. Check logs_read(source='editor', include_details=true)." % reason_suffix
- else:
- message = "The game is paused at a debugger break%s. Resume it from the editor's Debugger panel or call project_manage(op='stop')." % reason_suffix
- "no_helper":
- message = "The running game has no _mcp_game_helper autoload, so game-side tools cannot connect. If this is a headless or custom-main-loop project, use editor_screenshot(source='viewport') where applicable. Otherwise, re-enable the plugin and relaunch the game."
- "launching":
- message = "The game is still starting (%.1fs elapsed); the Godot AI game helper has not registered yet. Retry shortly." % (float(status.get("elapsed_msec", 0)) / 1000.0)
- "stopped":
- message = "The game is not running. Start the project and retry the game-side tool."
- _:
- message = "The game-side tool could not confirm the game is live (status=%s). Check logs_read(source='editor', include_details=true) and retry." % state
- var err := ErrorCodes.make(code, message)
- var inner: Dictionary = err.get("error", {})
- inner["data"] = data
- err["error"] = inner
- return err
-
-
-static func split_errors_by_scope(recent_errors: Array, scope: String) -> Dictionary:
- var current_run_errors: Array = []
- var retained_errors: Array = []
- if scope == "run":
- current_run_errors = recent_errors
- elif scope == "retained_recent":
- retained_errors = recent_errors
- return {
- "current_run_errors": current_run_errors,
- "retained_errors": retained_errors,
- }
-
-
-## `force_debugger_scan` bypasses the tracker's scan gate for one read. Keep it
-## false on per-frame polling paths (the run-liveness loop) — a forced scan
-## walks the Debugger dock UI — and pass true only for one-shot reads that must
-## see rows which landed after the last gated scan (#641).
-func recent_editor_errors_since(cursor: int, force_debugger_scan: bool = false) -> Dictionary:
- return _recent_editor_errors_since(cursor, force_debugger_scan)
-
-
-func _recent_editor_errors_since(cursor: int, force_debugger_scan: bool = false) -> Dictionary:
- var out: Array[Dictionary] = []
- var truncated := false
- if _surfaced_error_tracker != null:
- var captured_by_tracker: Dictionary = _surfaced_error_tracker.editor_entries_since(
- maxi(0, cursor),
- _game_run_started_debugger_cursor,
- force_debugger_scan,
- )
- truncated = bool(captured_by_tracker.get("truncated", false))
- for raw_entry in captured_by_tracker.get("entries", []):
- var compact := _compact_editor_error(raw_entry)
- if compact.is_empty():
- continue
- out.append(compact)
- if out.size() >= 5:
- break
- if not out.is_empty():
- return {"errors": out, "truncated": truncated, "scope": "run"}
- for raw_entry in _surfaced_error_tracker.retained_recent_editor_entries():
- var compact := _compact_editor_error(raw_entry, true)
- if compact.is_empty():
- continue
- out.append(compact)
- if out.size() >= 5:
- break
- if not out.is_empty():
- return {"errors": out, "truncated": false, "scope": "retained_recent"}
- return {"errors": out, "truncated": false, "scope": "none"}
- if _editor_log_buffer == null:
- return {"errors": out, "truncated": false, "scope": "none"}
- var captured: Dictionary = _editor_log_buffer.get_since(maxi(0, cursor), -1)
- truncated = bool(captured.get("truncated", false))
- for raw_entry in captured.get("entries", []):
- var compact := _compact_editor_error(raw_entry)
- if compact.is_empty():
- continue
- out.append(compact)
- if out.size() >= 5:
- break
- if not out.is_empty():
- return {"errors": out, "truncated": truncated, "scope": "run"}
-
- for raw_entry in _reversed_entries(_editor_log_buffer.get_recent(McpEditorLogBuffer.MAX_LINES)):
- var compact := _compact_editor_error(raw_entry, true)
- if compact.is_empty():
- continue
- out.append(compact)
- if out.size() >= 5:
- break
- if not out.is_empty():
- return {"errors": out, "truncated": false, "scope": "retained_recent"}
- return {"errors": out, "truncated": false, "scope": "none"}
-
-
-func _compact_editor_error(raw_entry: Variant, fallback_recent: bool = false) -> Dictionary:
- if not raw_entry is Dictionary:
- return {}
- var entry := raw_entry as Dictionary
- if str(entry.get("level", "info")) != "error":
- return {}
- var path := str(entry.get("path", ""))
- if fallback_recent and _is_diagnostic_noise_path(path):
- return {}
- var compact := {
- "source": "editor",
- "level": "error",
- "text": str(entry.get("text", "")),
- "path": path,
- "line": int(entry.get("line", 0)),
- "function": str(entry.get("function", "")),
- }
- if entry.has("details"):
- compact["details"] = entry["details"].duplicate(true)
- return compact
-
-
-func _is_diagnostic_noise_path(path: String) -> bool:
- return path.begins_with("res://addons/godot_ai/") or path.begins_with("res://tests/")
-
-
-func _reversed_entries(entries: Array[Dictionary]) -> Array[Dictionary]:
- var out: Array[Dictionary] = []
- for i in range(entries.size() - 1, -1, -1):
- out.append(entries[i])
- return out
-
-
-func _format_editor_error_summary(entry: Dictionary) -> String:
- return McpSurfacedErrorTracker.format_editor_error_summary(entry)
-
-
-func _capture(message: String, data: Array, session_id: int) -> bool:
- ## Godot passes the full "prefix:tail" string as `message`.
- match message:
- "mcp:screenshot_response":
- _on_screenshot_response(data)
- return true
- "mcp:screenshot_error":
- _on_screenshot_error(data)
- return true
- "mcp:log_batch":
- _on_log_batch(data)
- return true
- "mcp:hello":
- ## Staleness is checked FIRST, before the buffering branch below:
- ## `_begin_game_run_tracking` clears `_game_session_id`, so a beacon
- ## validated only at replay time could never be rejected (#891
- ## review). A beacon from a session other than the attached one is
- ## some other game's — never hold it, never apply it.
- if _game_session_id != -1 and session_id != _game_session_id:
- if _log_buffer:
- _log_buffer.log("[debug] ignored stale mcp:hello from debugger session %d (current %d)" % [session_id, _game_session_id])
- return true
- if not _game_run_active:
- ## #891: the run is not adopted YET — a manually started play
- ## whose debugger session attached before the editor flipped
- ## `is_playing_scene()` lands here. The beacon is a one-shot,
- ## so hold it for the imminent adoption instead of dropping it
- ## (which left game_eval waiting forever for a hello that had
- ## already been thrown away).
- _pending_hello_session_id = session_id
- ## The beacon is this game's first word, and its boot output
- ## follows immediately (the helper flushes on its first
- ## `_process`). Rotate the run id NOW so those lines are tagged
- ## with the identity adoption will announce, instead of landing
- ## under the previous run and vanishing from
- ## `logs_read(source="game")` (#891 review).
- if _game_log_buffer and not _pre_adoption_log_rotated:
- _game_log_buffer.clear_for_new_run()
- _pre_adoption_log_rotated = true
- if _log_buffer:
- _log_buffer.log("[debug] holding mcp:hello from debugger session %d until the run is adopted" % session_id)
- return true
- _accept_game_hello(session_id)
- return true
- "mcp:eval_liveness_response":
- _on_eval_liveness_response(data)
- return true
- "mcp:debug_status_response":
- _on_debug_status_response(data)
- return true
- "mcp:eval_response":
- _on_eval_response(data)
- return true
- "mcp:eval_error":
- _on_eval_error(data)
- return true
- "mcp:eval_ack":
- _on_eval_ack(data)
- return true
- "mcp:eval_compiled":
- _on_eval_compiled(data)
- return true
- "mcp:eval_runtime_error":
- _on_eval_runtime_error(data)
- return true
- "mcp:game_command_response":
- _on_game_command_response(data)
- return true
- "mcp:game_command_error":
- _on_game_command_error(data)
- return true
- return false
-
-
-func _on_log_batch(data: Array) -> void:
- if _game_log_buffer == null:
- return
- ## data layout: [[[level, text, details?], ...]]
- if data.is_empty() or not (data[0] is Array):
- return
- var entries: Array = data[0]
- for entry in entries:
- if entry is Dictionary:
- var dict_details: Dictionary = {}
- var raw_dict_details = entry.get("details", {})
- if raw_dict_details is Dictionary:
- dict_details = raw_dict_details
- _game_log_buffer.append(str(entry.get("level", "info")), str(entry.get("text", "")), dict_details)
- continue
- if not (entry is Array) or entry.size() < 2:
- continue
- var details: Dictionary = {}
- if entry.size() > 2 and entry[2] is Dictionary:
- details = entry[2]
- _game_log_buffer.append(str(entry[0]), str(entry[1]), details)
-
-
-## Request a game-process framebuffer capture over the debugger channel.
-## Reply is pushed back through `connection` out-of-band because the MCP
-## dispatcher has already returned a deferred-response marker for this
-## request_id. Synchronous from the caller's perspective — if the
-## game-side autoload hasn't beaconed yet, the wait + send run as a
-## fire-and-forget coroutine kicked off from here. Structured this way
-## so the call site in EditorHandler stays a plain non-await invocation.
-func request_game_screenshot(
- request_id: String,
- max_resolution: int,
- connection: McpConnection,
- timeout_sec: float = DEFAULT_TIMEOUT_SEC,
-) -> void:
- if request_id.is_empty():
- push_warning("MCP debugger: screenshot request missing request_id")
- return
-
- var tree := Engine.get_main_loop() as SceneTree
- if tree == null:
- _send_error(connection, request_id, ErrorCodes.INTERNAL_ERROR,
- "Editor main loop is not a SceneTree — cannot schedule capture")
- return
-
- if is_game_capture_ready():
- _send_take_screenshot(tree, request_id, max_resolution, connection, timeout_sec)
- return
-
- ## Not ready yet — run the wait-then-send flow as a detached
- ## coroutine. It keeps itself alive via the signal subscription on
- ## tree.process_frame; the caller doesn't need to (and shouldn't)
- ## await this entrypoint.
- if _log_buffer:
- _log_buffer.log("[debug] waiting for game_helper hello (%s)" % request_id)
- _wait_then_send(tree, request_id, max_resolution, connection, timeout_sec)
-
-
-## Coroutine: poll each editor frame until the mcp:hello beacon arrives
-## (flipping _game_ready true) or the deadline elapses. Once resolved,
-## either dispatch the capture or return an actionable timeout error.
-func _wait_then_send(
- tree: SceneTree,
- request_id: String,
- max_resolution: int,
- connection: McpConnection,
- timeout_sec: float,
-) -> void:
- var work := ScriptWork.begin("game_screenshot_ready")
- await _settle_capture_ready(tree, request_id, max_resolution, connection, timeout_sec)
- ScriptWork.finish(work)
-
-
-func _settle_capture_ready(
- tree: SceneTree, request_id: String, max_resolution: int,
- connection: McpConnection, timeout_sec: float,
-) -> void:
- var deadline := Time.get_ticks_msec() + int(GAME_READY_WAIT_SEC * 1000.0)
- ## #645: always yield at least one frame — the dispatcher registers the
- ## deferred request only after the handler returns DEFERRED_RESPONSE, so a
- ## same-frame error reply would be dropped as an expired request. The break
- ## check then bails out with the actionable break error instead of waiting
- ## out the full window (a game parked in a debugger break never beacons).
- await tree.process_frame
- while not is_game_capture_ready() and not _break_active and Time.get_ticks_msec() < deadline:
- await tree.process_frame
- if not is_game_capture_ready():
- _send_error_response(connection, request_id,
- _explain_not_live(get_game_status(-1, GAME_READY_WAIT_SEC), ErrorCodes.INTERNAL_ERROR))
- return
- _send_take_screenshot(tree, request_id, max_resolution, connection, timeout_sec)
-
-
-## Send the mcp:take_screenshot message and arm the reply timeout.
-## Assumes _game_ready is true.
-func _send_take_screenshot(
- tree: SceneTree,
- request_id: String,
- max_resolution: int,
- connection: McpConnection,
- timeout_sec: float,
-) -> void:
- var session: EditorDebuggerSession = _first_active_session()
- if session == null:
- _send_error(connection, request_id, ErrorCodes.INTERNAL_ERROR,
- "No active debugger session — is the game actually running and started from this editor?")
- return
-
- var timer: SceneTreeTimer = tree.create_timer(timeout_sec)
- var timeout_callable := func() -> void: _on_timeout(request_id)
- timer.timeout.connect(timeout_callable)
- _pending[request_id] = {
- "connection": connection,
- "timer": timer,
- "timeout_callable": timeout_callable,
- }
-
- session.send_message("mcp:take_screenshot", [request_id, max_resolution])
- if _log_buffer:
- _log_buffer.log("[debug] -> mcp:take_screenshot (%s)" % request_id)
-
-
-func _first_active_session() -> EditorDebuggerSession:
- for s in get_sessions():
- if s is EditorDebuggerSession and s.is_active():
- return s
- return null
-
-
-func _on_screenshot_response(data: Array) -> void:
- if data.size() < 6:
- push_warning("MCP debugger: malformed screenshot response (expected 6 fields, got %d)" % data.size())
- return
- var request_id: String = data[0]
- var pending = _pending.get(request_id)
- if pending == null:
- ## Timed out or unknown — silently drop.
- return
- _clear_pending(request_id)
-
- var connection: McpConnection = pending.connection
- if connection == null or not is_instance_valid(connection):
- return
-
- var payload := {
- "source": "game",
- "width": int(data[2]),
- "height": int(data[3]),
- "original_width": int(data[4]),
- "original_height": int(data[5]),
- "format": "png",
- "image_base64": data[1],
- }
- ## #777: game helpers append frames_drawn + a stale flag so a capture
- ## taken while the game's main loop is frozen (backgrounded window) is
- ## honestly labeled instead of timing out. Older helpers send six fields
- ## — leave the keys absent rather than guessing.
- if data.size() >= 8:
- payload["frames_drawn"] = int(data[6])
- payload["stale_frame"] = bool(data[7])
- if bool(data[7]):
- payload["note"] = ("The game window appears backgrounded or its main loop is "
- + "stalled; returning the last rendered frame. Focus the game window and "
- + "retry for a current frame.")
- ## Vision Routing: when enabled, describe the frame through the configured
- ## vision provider on a
- ## worker thread and reply with the text description instead of the raw
- ## image (see vision_routing.gd). The router replies for us in that case.
- if vision_routing != null and vision_routing.is_routing_enabled():
- if vision_routing.route_game_payload(connection, request_id, {"data": payload}):
- return
- connection.send_deferred_response(request_id, {"data": payload})
- if _log_buffer:
- _log_buffer.log("[debug] <- mcp:screenshot_response (%s)" % request_id)
-
-
-func _on_screenshot_error(data: Array) -> void:
- if data.size() < 2:
- return
- var request_id: String = data[0]
- var message: String = data[1]
- var pending = _pending.get(request_id)
- if pending == null:
- return
- _clear_pending(request_id)
- var connection: McpConnection = pending.connection
- if connection == null or not is_instance_valid(connection):
- return
- _send_error(connection, request_id, ErrorCodes.INTERNAL_ERROR, message)
-
-
-## #777: the 8s reply timer fired — the screenshot request reached (or should
-## have reached) the game helper and no reply came back. Mirror the eval
-## timeout split (#518): a not-live game gets the attributed
-## _explain_not_live payload; a live game gets GAME_HELPER_TIMEOUT instead of
-## the former opaque INTERNAL_ERROR. With the game side's stalled-loop
-## stale-frame fallback, a live game only lands here when it has nothing
-## rendered to fall back on, its debugger servicing is itself wedged, or the
-## helper died mid-run.
-func _on_timeout(request_id: String) -> void:
- var pending = _pending.get(request_id)
- if pending == null:
- return
- _pending.erase(request_id)
- var connection: McpConnection = pending.connection
- if connection == null or not is_instance_valid(connection):
- return
- var status := get_game_status(-1, GAME_READY_WAIT_SEC)
- var err: Dictionary
- if status.get("status", "") != "live":
- err = _explain_not_live(status, ErrorCodes.INTERNAL_ERROR)
- else:
- err = ErrorCodes.make(ErrorCodes.GAME_HELPER_TIMEOUT,
- "The game process did not return a frame. The game window may be backgrounded or its main loop blocked — focus the game window and retry, or use game_command to confirm liveness.")
- _send_error_response(connection, request_id, err)
- if _log_buffer:
- _log_buffer.log("[debug] !! screenshot timeout (%s)" % request_id)
-
-
-func _send_error(connection: McpConnection, request_id: String, code: String, message: String) -> void:
- _send_error_response(connection, request_id, ErrorCodes.make(code, message))
-
-
-func _send_error_response(connection: McpConnection, request_id: String, err: Dictionary) -> void:
- if connection == null or not is_instance_valid(connection):
- return
- connection.send_deferred_response(request_id, err)
-
-
-func _clear_pending(request_id: String) -> void:
- var pending: Dictionary = _pending.get(request_id, {})
- var timer: SceneTreeTimer = pending.get("timer")
- var cb: Callable = pending.get("timeout_callable", Callable())
- if timer != null and timer.timeout.is_connected(cb):
- timer.timeout.disconnect(cb)
- ## #490: eval requests also carry a compile-grace timer and a runtime probe.
- var grace: SceneTreeTimer = pending.get("grace_timer")
- var gcb: Callable = pending.get("grace_callable", Callable())
- if grace != null and grace.timeout.is_connected(gcb):
- grace.timeout.disconnect(gcb)
- var probe: SceneTreeTimer = pending.get("probe_timer")
- var pcb: Callable = pending.get("probe_callable", Callable())
- if probe != null and probe.timeout.is_connected(pcb):
- probe.timeout.disconnect(pcb)
- _pending.erase(request_id)
-
-
-## --- game_eval: execute arbitrary GDScript in the running game ---
-
-## Editor-side fallback timer for game_eval. MUST stay above the game-side
-## EVAL_TIMEOUT_SEC (8.0) in runtime/game_helper.gd and below the dispatcher's
-## game_eval budget (15000 ms) in dispatcher.gd — i.e. game 8s < editor 10s <
-## dispatcher 15s. This timer only fires when the game never replies at all;
-## _on_eval_timeout then attributes the failure (game not live vs never-acked
-## vs started-and-hung, #518). Drop timeout_sec at/below 8s and it pre-empts
-## the game's more specific "Eval exceeded 8s" message — see the TIMEOUT
-## ORDERING note on EVAL_TIMEOUT_SEC.
-##
-## #500/#859: the *not-ready* path adds EVAL_READY_WAIT_SEC (3s) and the
-## EVAL_LIVENESS_WAIT_SEC probe (0.25s) on top of this 10s backstop. That sum
-## (13.25s) must also stay below the dispatcher/server 15s budget, or a
-## not-yet-ready game makes the server time out opaquely before the editor's
-## actionable error returns — exactly the residual ~15s TimeoutError bucket
-## #500 tracked down. The cross-file contract is enforced by
-## tests/unit/test_game_eval_timeout_ordering.py.
-func request_game_eval(
- code: String,
- request_id: String,
- connection: McpConnection,
- timeout_sec: float = 10.0,
-) -> void:
- if request_id.is_empty():
- push_warning("MCP debugger: eval request missing request_id")
- return
-
- var tree := Engine.get_main_loop() as SceneTree
- if tree == null:
- _send_error(connection, request_id, ErrorCodes.INTERNAL_ERROR,
- "Editor main loop is not a SceneTree — cannot schedule eval")
- return
-
- var run_token := _game_run_token
- if is_game_capture_ready():
- _probe_then_eval(tree, code, request_id, connection, timeout_sec, run_token)
- return
-
- if _log_buffer:
- _log_buffer.log("[debug] waiting for game_helper hello before eval (%s)" % request_id)
- _wait_then_eval(tree, code, request_id, connection, timeout_sec, run_token)
-
-
-func _wait_then_eval(
- tree: SceneTree,
- code: String,
- request_id: String,
- connection: McpConnection,
- timeout_sec: float,
- run_token: int,
-) -> void:
- var work := ScriptWork.begin("game_eval_ready")
- await _settle_eval_ready(tree, code, request_id, connection, timeout_sec, run_token)
- ScriptWork.finish(work)
-
-
-func _settle_eval_ready(
- tree: SceneTree, code: String, request_id: String,
- connection: McpConnection, timeout_sec: float, run_token: int,
-) -> void:
- ## #500: eval uses EVAL_READY_WAIT_SEC (not the 20s GAME_READY_WAIT_SEC) so
- ## the not-ready path returns its actionable error before the 15s server-side
- ## command timeout fires an opaque TimeoutError. See EVAL_READY_WAIT_SEC.
- _pending[request_id] = {
- "kind": "eval_ready_wait",
- "connection": connection,
- "run_token": run_token,
- }
- var deadline := Time.get_ticks_msec() + int(EVAL_READY_WAIT_SEC * 1000.0)
- ## #645: the leading yield guarantees the dispatcher has registered the
- ## deferred request before any reply (a same-frame reply is dropped as
- ## expired); the break check bails out early because a parked game never
- ## registers its capture.
- if _eval_ready_frame_waiter.is_valid():
- await _eval_ready_frame_waiter.call()
- else:
- await tree.process_frame
- while (
- _pending.has(request_id)
- and _is_current_game_run(run_token)
- and not is_game_capture_ready()
- and not _break_active
- and Time.get_ticks_msec() < deadline
- ):
- if _eval_ready_frame_waiter.is_valid():
- await _eval_ready_frame_waiter.call()
- else:
- await tree.process_frame
- ## end_game_run() owns the reply once it has cleared this tracked wait.
- if not _pending.has(request_id):
- return
- if not _is_current_game_run(run_token):
- _fail_eval_not_ready(request_id,
- "The game run changed before game_eval could start — the game stopped or restarted. Retry against the current run.")
- return
- if not is_game_capture_ready():
- ## #518: EVAL_GAME_NOT_READY (not INTERNAL_ERROR) — the play session is up
- ## but the game-side capture didn't register within the short wait. Fast
- ## and caller-actionable; classifying it apart from the opaque 10s hang
- ## keeps the INTERNAL_ERROR telemetry bucket meaning "the eval truly hung".
- _clear_pending(request_id)
- _send_error_response(connection, request_id,
- _explain_not_live(get_game_status(-1, EVAL_READY_WAIT_SEC), ErrorCodes.EVAL_GAME_NOT_READY))
- return
- _clear_pending(request_id)
- _probe_then_eval(tree, code, request_id, connection, timeout_sec, run_token)
-
-
-## #859: registration is run-scoped but not a liveness guarantee. Ping the
-## helper's debugger capture and let it report whether its _process beacon is
-## still advancing before arming the much longer eval backstop.
-func _probe_then_eval(
- tree: SceneTree,
- code: String,
- request_id: String,
- connection: McpConnection,
- timeout_sec: float,
- run_token: int,
-) -> void:
- if not _is_current_game_run(run_token) or not is_game_capture_ready():
- _send_error(connection, request_id, ErrorCodes.EVAL_GAME_NOT_READY,
- "The game run changed before game_eval could be checked — the game stopped or restarted. Retry against the current run.")
- return
- var session: EditorDebuggerSession = _first_active_session()
- if session == null:
- _send_error(connection, request_id, ErrorCodes.EVAL_GAME_NOT_READY,
- "Game-side capture registered but its debugger session is no longer active — the game likely just stopped or is restarting. Confirm it is running and retry.")
- return
-
- var timer := tree.create_timer(EVAL_LIVENESS_WAIT_SEC)
- var timeout_callable := func() -> void: _on_eval_liveness_timeout(request_id)
- timer.timeout.connect(timeout_callable)
- _pending[request_id] = {
- "kind": "eval_liveness",
- "connection": connection,
- "timer": timer,
- "timeout_callable": timeout_callable,
- "code": code,
- "eval_timeout_sec": timeout_sec,
- "run_token": run_token,
- }
- session.send_message("mcp:eval_liveness", [request_id])
- if _log_buffer:
- _log_buffer.log("[debug] -> mcp:eval_liveness (%s)" % request_id)
-
-
-func _on_eval_liveness_timeout(request_id: String) -> void:
- var pending_entry = _pending.get(request_id)
- if pending_entry == null or str(pending_entry.get("kind", "")) != "eval_liveness":
- return
- var connection: McpConnection = pending_entry.get("connection")
- _clear_pending(request_id)
- if connection == null or not is_instance_valid(connection):
- return
- _send_error(connection, request_id, ErrorCodes.EVAL_GAME_NOT_READY,
- ("The game helper did not answer the liveness probe within %.0fms — the game may be backgrounded, frozen, stopped, or restarting. Focus the game window (or relaunch it) and retry."
- % (EVAL_LIVENESS_WAIT_SEC * 1000.0)))
-
-
-func _on_eval_liveness_response(data: Array) -> void:
- if data.size() < 2:
- push_warning("MCP debugger: malformed eval liveness response")
- return
- var request_id := str(data[0])
- var pending_entry = _pending.get(request_id)
- if pending_entry == null or str(pending_entry.get("kind", "")) != "eval_liveness":
- return
- var connection: McpConnection = pending_entry.get("connection")
- var code := str(pending_entry.get("code", ""))
- var timeout_sec := float(pending_entry.get("eval_timeout_sec", 10.0))
- var run_token := int(pending_entry.get("run_token", -1))
- var loop_live := bool(data[1])
- _clear_pending(request_id)
- if connection == null or not is_instance_valid(connection):
- return
- if not _is_current_game_run(run_token) or not is_game_capture_ready():
- _send_error(connection, request_id, ErrorCodes.EVAL_GAME_NOT_READY,
- "The game run changed while game_eval liveness was being checked — the game stopped or restarted. Retry against the current run.")
- return
- if not loop_live:
- _send_error(connection, request_id, ErrorCodes.EVAL_GAME_NOT_READY,
- "The game helper is registered but its main loop is not advancing — the game window may be backgrounded or the game may be frozen. Focus the game window (or relaunch it) and retry.")
- return
- var current_tree := Engine.get_main_loop() as SceneTree
- if current_tree == null:
- _send_error(connection, request_id, ErrorCodes.INTERNAL_ERROR,
- "Editor main loop is not a SceneTree — cannot schedule eval")
- return
- _send_eval(current_tree, code, request_id, connection, timeout_sec)
-
-
-func _send_eval(
- tree: SceneTree,
- code: String,
- request_id: String,
- connection: McpConnection,
- timeout_sec: float,
-) -> void:
- var session: EditorDebuggerSession = _first_active_session()
- if session == null:
- ## #518: capture reported ready but the debugger session is no longer live
- ## (the game just stopped / is restarting) — a not-ready race, so the same
- ## caller-actionable EVAL_GAME_NOT_READY rather than the opaque hang bucket.
- _send_error(connection, request_id, ErrorCodes.EVAL_GAME_NOT_READY,
- "Game-side capture registered but its debugger session is no longer active — the game likely just stopped or is restarting. Confirm it's running and retry.")
- return
-
- var timer: SceneTreeTimer = tree.create_timer(timeout_sec)
- var timeout_callable := func() -> void: _on_eval_timeout(request_id, timeout_sec)
- timer.timeout.connect(timeout_callable)
-
- ## #490: arm the compile-grace timer. _on_eval_grace concludes a parse error
- ## only when the game acked the eval (it received the message and started
- ## reload()) but never sent mcp:eval_compiled — see there for why a missing
- ## ack must NOT be read as a compile error.
- var grace: SceneTreeTimer = tree.create_timer(EVAL_COMPILE_GRACE_SEC)
- var grace_callable := func() -> void: _on_eval_grace(request_id)
- grace.timeout.connect(grace_callable)
-
- _pending[request_id] = {
- "kind": "eval",
- "connection": connection,
- "run_token": _game_run_token,
- "timer": timer,
- "timeout_callable": timeout_callable,
- "grace_timer": grace,
- "grace_callable": grace_callable,
- "acked": false,
- "compiled": false,
- }
-
- session.send_message("mcp:eval", [request_id, code])
- if _log_buffer:
- _log_buffer.log("[debug] -> mcp:eval (%s)" % request_id)
-
-
-## #518: the 10s editor-side backstop fired — the game never replied at all.
-## Attribute the failure instead of emitting a one-size-fits-all INTERNAL_ERROR:
-##
-## - game not live anymore (parked in a debugger break, stopped, crashed):
-## the eval couldn't run/finish for a *game-state* reason. Reply with the
-## same caller-actionable EVAL_GAME_NOT_READY + `_explain_not_live` payload
-## the pre-hello break path already uses — a break freezes the game's idle
-## loop, so any awaiting eval parks here even though sync evals still work.
-## - game live but never acked the eval: its main thread never serviced the
-## debugger message (long frame/load, CPU-bound prior eval, or a
-## backgrounded window whose idle loop is frozen).
-## - game live, acked, compiled: the eval genuinely started and never
-## finished, and the game couldn't even self-report via its own 8s guard
-## (which needs a ticking idle loop) — hung await, CPU-bound loop, or a
-## reply the debugger channel dropped.
-##
-## The live branches reply EVAL_HUNG: the eval code never finished. That code
-## plus the game-side 8s guard (also EVAL_HUNG, via mcp:eval_error's code
-## element) empties the former INTERNAL_ERROR bucket on this path.
-func _on_eval_timeout(request_id: String, timeout_sec: float) -> void:
- var pending_entry = _pending.get(request_id)
- if pending_entry == null:
- return
- var run_token := int(pending_entry.get("run_token", _game_run_token))
- _clear_pending(request_id)
- var conn: McpConnection = pending_entry.connection
- if conn == null or not is_instance_valid(conn):
- return
- if run_token != _game_run_token:
- _send_error(conn, request_id, ErrorCodes.EVAL_GAME_NOT_READY,
- "The game run changed before game_eval completed — the original game stopped or restarted. Retry against the current run.")
- if _log_buffer:
- _log_buffer.log("[debug] !! eval timeout from prior run (%s, run=%d, current=%d)"
- % [request_id, run_token, _game_run_token])
- return
- var status := get_game_status(-1, EVAL_READY_WAIT_SEC)
- if str(status.get("status", "")) != "live":
- _send_error_response(conn, request_id,
- _explain_not_live(status, ErrorCodes.EVAL_GAME_NOT_READY))
- if _log_buffer:
- _log_buffer.log("[debug] !! eval timeout, game not live (%s, status=%s)"
- % [request_id, str(status.get("status", ""))])
- return
- var message: String
- if not bool(pending_entry.get("acked", false)):
- message = ("Game eval was sent but the game never picked it up within %.0fs — "
- + "its main thread is busy or frozen (a long frame/load, a CPU-bound "
- + "prior eval, or a backgrounded game window whose loop is throttled). "
- + "Check logs_read(source='game') and retry.") % timeout_sec
- else:
- message = ("Game eval compiled and started running but never returned within "
- + "%.0fs — the code is likely stuck in an infinite loop or awaiting a "
- + "signal/timer that never fires (a backgrounded game window also freezes "
- + "awaits). Check logs_read(source='game').") % timeout_sec
- _send_error(conn, request_id, ErrorCodes.EVAL_HUNG, message)
- if _log_buffer:
- _log_buffer.log("[debug] !! eval timeout (%s)" % request_id)
-
-
-func _on_eval_response(data: Array) -> void:
- if data.size() < 2:
- push_warning("MCP debugger: malformed eval response (expected 2 fields, got %d)" % data.size())
- return
- var request_id: String = data[0]
- var pending_entry = _pending.get(request_id)
- if pending_entry == null:
- return
- _clear_pending(request_id)
-
- var connection: McpConnection = pending_entry.connection
- if connection == null or not is_instance_valid(connection):
- return
-
- var result_json: String = data[1] if data.size() > 1 else "null"
- var json := JSON.new()
- var parse_err := json.parse(result_json)
- connection.send_deferred_response(request_id, {
- "data": {
- "result": json.data if parse_err == OK else result_json,
- "source": "game",
- }
- })
- if _log_buffer:
- _log_buffer.log("[debug] <- mcp:eval_response (%s)" % request_id)
-
-
-## #518: codes the game side may attach as mcp:eval_error's optional third
-## payload element. Allowlisted so a game process can't mint arbitrary
-## top-level error codes over the debugger channel; anything else (including
-## the legacy two-element payload from an older game helper mid-update)
-## falls back to INTERNAL_ERROR exactly as before.
-const _GAME_EVAL_ERROR_CODES: Array[String] = [
- ErrorCodes.EVAL_HUNG,
- ErrorCodes.EVAL_RESULT_TOO_LARGE,
-]
-
-
-func _on_eval_error(data: Array) -> void:
- if data.size() < 2:
- return
- var request_id: String = data[0]
- var message: String = data[1]
- var pending_entry = _pending.get(request_id)
- if pending_entry == null:
- return
- _clear_pending(request_id)
- var connection: McpConnection = pending_entry.connection
- if connection == null or not is_instance_valid(connection):
- return
- var code := ErrorCodes.INTERNAL_ERROR
- if data.size() > 2 and str(data[2]) in _GAME_EVAL_ERROR_CODES:
- code = str(data[2])
- _send_error(connection, request_id, code, message)
- if _log_buffer:
- _log_buffer.log("[debug] <- mcp:eval_error (%s): %s" % [request_id, message])
-
-
-## #490: the game sends this at the top of _handle_eval, BEFORE reload() (so it
-## survives a parse-error abort). It positively signals "the game received this
-## eval and started compiling it" — letting _on_eval_grace tell a real parse
-## error (acked, never compiled) apart from a message the game hasn't serviced
-## yet (never acked — main thread blocked by a long frame/load or a CPU-bound
-## prior eval).
-func _on_eval_ack(data: Array) -> void:
- if data.is_empty():
- return
- var request_id: String = data[0]
- var pending_entry = _pending.get(request_id)
- if pending_entry == null:
- return
- pending_entry["acked"] = true
- if _log_buffer:
- _log_buffer.log("[debug] <- mcp:eval_ack (%s)" % request_id)
-
-
-## #490: compile-grace timer fired. Conclude a parse error ONLY when the game
-## acked the eval (started reload()) but never sent mcp:eval_compiled. If it
-## never acked, the game simply hasn't serviced the message yet — NOT a parse
-## error — so leave _pending intact and let the normal eval timeout handle it
-## rather than false-failing a valid eval and dropping its eventual real reply.
-func _on_eval_grace(request_id: String) -> void:
- var pending_entry = _pending.get(request_id)
- if pending_entry == null or pending_entry.get("compiled", false):
- return
- if not pending_entry.get("acked", false):
- if _log_buffer:
- _log_buffer.log("[debug] eval grace: no ack yet, deferring to timeout (%s)" % request_id)
- return
- _clear_pending(request_id)
- var conn: McpConnection = pending_entry.connection
- if conn == null or not is_instance_valid(conn):
- return
- _send_error(conn, request_id, ErrorCodes.EVAL_COMPILE_ERROR,
- "Game eval failed to compile — likely a GDScript syntax/parse error. The parse error text is in the editor's Output/Debugger panel; it is not capturable from the running game. Check your eval code's syntax.")
- if _log_buffer:
- _log_buffer.log("[debug] !! eval compile error (%s)" % request_id)
-
-
-## #490: the game sends this the instant reload() of the eval source
-## succeeds. Flips the pending entry's `compiled` flag so the compile-grace
-## timer won't fire a false EVAL_COMPILE_ERROR.
-func _on_eval_compiled(data: Array) -> void:
- if data.is_empty():
- return
- var request_id: String = data[0]
- var pending_entry = _pending.get(request_id)
- if pending_entry == null:
- return
- pending_entry["compiled"] = true
- if _log_buffer:
- _log_buffer.log("[debug] <- mcp:eval_compiled (%s)" % request_id)
- ## #490: compiled OK — start polling for a runtime error that may have
- ## aborted execute(). A backgrounded game can't self-report it, so the
- ## editor probes via mcp:eval_check until the eval resolves.
- _arm_eval_probe(request_id)
-
-
-## #490: the game reported a runtime error that aborted the eval — either
-## from its _process fast path (focused game) or in answer to an editor
-## eval_check probe (backgrounded game). Reply fast with the real error text
-## instead of waiting for the hang timeout.
-func _on_eval_runtime_error(data: Array) -> void:
- if data.size() < 2:
- return
- var request_id: String = data[0]
- var message: String = data[1]
- var pending_entry = _pending.get(request_id)
- if pending_entry == null:
- return
- _clear_pending(request_id)
- var connection: McpConnection = pending_entry.connection
- if connection == null or not is_instance_valid(connection):
- return
- var msg := "Game eval raised a runtime error: %s" % message if not message.is_empty() else "Game eval raised a runtime error (no message captured). Check logs_read(source='game')."
- _send_error(connection, request_id, ErrorCodes.EVAL_RUNTIME_ERROR, msg)
- if _log_buffer:
- _log_buffer.log("[debug] <- mcp:eval_runtime_error (%s): %s" % [request_id, message])
-
-
-## #490: arm one probe tick for an in-flight eval. Re-arms itself each tick
-## until the request resolves — eval_response / eval_runtime_error /
-## eval_compile_error / hang-timeout all call _clear_pending, which erases the
-## entry and stops the chain. Uses the editor's own SceneTreeTimer because the
-## editor loop keeps ticking even while a backgrounded game's loop is frozen.
-func _arm_eval_probe(request_id: String) -> void:
- var pending_entry = _pending.get(request_id)
- if pending_entry == null:
- return
- var tree := Engine.get_main_loop() as SceneTree
- if tree == null:
- return
- var probe_timer: SceneTreeTimer = tree.create_timer(EVAL_PROBE_INTERVAL_SEC)
- var probe_callable := func() -> void: _on_eval_probe_tick(request_id)
- pending_entry["probe_timer"] = probe_timer
- pending_entry["probe_callable"] = probe_callable
- probe_timer.timeout.connect(probe_callable)
-
-
-## #490: poke the game for a runtime-error verdict, then re-arm. The game's
-## _handle_eval_check answers with mcp:eval_runtime_error if a script error
-## aborted this eval, else stays silent and we poll again next interval.
-func _on_eval_probe_tick(request_id: String) -> void:
- if not _pending.has(request_id):
- return ## resolved — stop probing
- var session: EditorDebuggerSession = _first_active_session()
- if session != null and session.is_active():
- session.send_message("mcp:eval_check", [request_id])
- _arm_eval_probe(request_id)
-
-
-## --- native embedded Game View runtime control (#939) ---
-
-static func game_debug_verification(
- action: String, state_before: Dictionary, state_after: Dictionary
-) -> Dictionary:
- var before_ticks := int(state_before.get("process_ticks", -1))
- var after_ticks := int(state_after.get("process_ticks", -1))
- var ticks_advanced := after_ticks - before_ticks if before_ticks >= 0 and after_ticks >= 0 else -1
- match action:
- "suspend":
- return {"status": "verified" if bool(state_after.get("suspended", false)) else "pending", "ticks_advanced": ticks_advanced}
- "resume":
- return {"status": "verified" if not bool(state_after.get("suspended", true)) else "pending", "ticks_advanced": ticks_advanced}
- "next_frame":
- if before_ticks < 0 or after_ticks < 0:
- return {"status": "failed", "reason": "missing_process_ticks", "ticks_advanced": ticks_advanced}
- if ticks_advanced > 1:
- return {"status": "failed", "reason": "multiple_process_ticks", "ticks_advanced": ticks_advanced}
- if ticks_advanced == 1 and bool(state_after.get("suspended", false)):
- return {"status": "verified", "ticks_advanced": 1}
- return {"status": "pending", "ticks_advanced": ticks_advanced}
- return {"status": "failed", "reason": "unsupported_action", "ticks_advanced": ticks_advanced}
-
-
-func _active_game_debug_mutation() -> Dictionary:
- for raw_request_id in _pending.keys():
- var pending_entry: Dictionary = _pending.get(raw_request_id, {})
- if str(pending_entry.get("kind", "")) != "game_debug_control":
- continue
- var active_action := str(pending_entry.get("action", ""))
- if active_action in ["suspend", "resume", "next_frame"]:
- return {"request_id": str(raw_request_id), "action": active_action}
- return {}
-
-
-func request_game_debug_control(
- action: String,
- request_id: String,
- connection: McpConnection,
- timeout_sec: float = GAME_DEBUG_CONTROL_TIMEOUT_SEC,
-) -> void:
- if request_id.is_empty():
- push_warning("MCP debugger: game debug control missing request_id")
- return
- ## The dispatcher registers the deferred request only after the handler returns.
- ## Start the entire guard/control flow deferred so even fast failures cannot race
- ## that registration and get dropped as an expired response.
- _begin_game_debug_control.call_deferred(action, request_id, connection, timeout_sec)
-
-
-func _begin_game_debug_control(
- action: String,
- request_id: String,
- connection: McpConnection,
- timeout_sec: float,
-) -> void:
- if action not in ["suspend", "resume", "next_frame", "debug_status"]:
- _send_error(connection, request_id, ErrorCodes.VALUE_OUT_OF_RANGE,
- "Unsupported game debug action: %s" % action)
- return
- var tree := Engine.get_main_loop() as SceneTree
- if tree == null:
- _send_error(connection, request_id, ErrorCodes.INTERNAL_ERROR,
- "Editor main loop is not a SceneTree — cannot schedule game debug control")
- return
- if not is_game_capture_ready():
- _send_error_response(connection, request_id,
- _explain_not_live(get_game_status(-1, GAME_READY_WAIT_SEC), ErrorCodes.INTERNAL_ERROR))
- return
- if action != "debug_status":
- var active_mutation := _active_game_debug_mutation()
- if not active_mutation.is_empty():
- var busy := ErrorCodes.make(
- ErrorCodes.EDITOR_NOT_READY,
- "Another game runtime-control mutation is already in flight — retry after it completes."
- )
- busy["error"]["data"] = {
- "action": action,
- "active_action": str(active_mutation.get("action", "")),
- "retryable": true,
- }
- _send_error_response(connection, request_id, busy)
- return
- var session := _first_active_session()
- if session == null:
- _send_error(connection, request_id, ErrorCodes.INTERNAL_ERROR,
- "No active debugger session — is the game actually running?")
- return
- var timer := tree.create_timer(timeout_sec)
- var timeout_callable := func() -> void:
- var pending_entry: Dictionary = _pending.get(request_id, {})
- if pending_entry.is_empty():
- return
- _pending.erase(request_id)
- var conn: McpConnection = pending_entry.get("connection")
- if conn != null and is_instance_valid(conn):
- var err := ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Game debug action '%s' timed out after %.0fs" % [action, timeout_sec])
- err["error"]["data"] = {
- "action": action,
- "state_before": pending_entry.get("state_before", {}),
- "state_after": pending_entry.get("state_after", {}),
- "focus_handoff": pending_entry.get("focus_handoff", {}),
- "path": pending_entry.get("path", ""),
- "game_view_ui_synced": pending_entry.get("game_view_ui_synced", false),
- }
- _send_error_response(conn, request_id, err)
- timer.timeout.connect(timeout_callable)
- _pending[request_id] = {
- "kind": "game_debug_control",
- "phase": "before",
- "action": action,
- "connection": connection,
- "timer": timer,
- "timeout_callable": timeout_callable,
- "run_token": _game_run_token,
- }
- session.send_message("mcp:debug_status", [request_id])
-
-
-func _on_debug_status_response(data: Array) -> void:
- if data.size() < 2 or not (data[1] is Dictionary):
- push_warning("MCP debugger: malformed debug_status response")
- return
- var request_id := str(data[0])
- var pending_entry: Dictionary = _pending.get(request_id, {})
- if str(pending_entry.get("kind", "")) != "game_debug_control":
- return
- var connection: McpConnection = pending_entry.get("connection")
- if not _is_current_game_run(int(pending_entry.get("run_token", -1))):
- _clear_pending(request_id)
- if connection != null and is_instance_valid(connection):
- _send_error(connection, request_id, ErrorCodes.EVAL_GAME_NOT_READY,
- "The game run changed while runtime control was in flight — retry against the current run.")
- return
- var state: Dictionary = (data[1] as Dictionary).duplicate(true)
- pending_entry["state_after"] = state
- match str(pending_entry.get("phase", "before")):
- "before":
- _handle_game_debug_before(request_id, pending_entry, state)
- "verify":
- _handle_game_debug_verify(request_id, pending_entry, state)
- _:
- _clear_pending(request_id)
- if connection != null and is_instance_valid(connection):
- _send_error(connection, request_id, ErrorCodes.INTERNAL_ERROR,
- "Invalid game debug control phase")
-
-
-func _default_focus_handoff() -> Dictionary:
- return {"attempted": false, "main_screen": "", "embedded_process_found": false, "focused": false}
-
-
-func _handle_game_debug_before(
- request_id: String, pending_entry: Dictionary, state: Dictionary
-) -> void:
- var action := str(pending_entry.get("action", ""))
- var connection: McpConnection = pending_entry.get("connection")
- var focus_handoff := _default_focus_handoff()
- if action == "debug_status":
- _clear_pending(request_id)
- if connection != null and is_instance_valid(connection):
- connection.send_deferred_response(request_id, {"data": {
- "action": action, "state": state, "focus_handoff": focus_handoff,
- }})
- return
- var suspended := bool(state.get("suspended", false))
- if action in ["suspend", "resume"]:
- var desired := action == "suspend"
- if suspended == desired:
- _clear_pending(request_id)
- if connection != null and is_instance_valid(connection):
- connection.send_deferred_response(request_id, {"data": {
- "action": action, "changed": false, "verified": true,
- "state_before": state, "state_after": state,
- "focus_handoff": focus_handoff,
- }})
- return
- elif action == "next_frame" and not suspended:
- _clear_pending(request_id)
- if connection != null and is_instance_valid(connection):
- _send_error(connection, request_id, ErrorCodes.INVALID_PARAMS,
- "next_frame requires a suspended game — call suspend first")
- return
- if action == "next_frame":
- focus_handoff = _focus_embedded_game_view()
- var desired_suspended := action == "suspend"
- var control := _emit_game_debug_runtime_action(action, desired_suspended)
- if not bool(control.get("ok", false)):
- var err := ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "No active debugger path accepted the native game runtime action")
- err["error"]["data"] = {
- "action": action,
- "reason": control.get("reason", "runtime_control_unavailable"),
- "focus_handoff": focus_handoff,
- }
- _clear_pending(request_id)
- if connection != null and is_instance_valid(connection):
- _send_error_response(connection, request_id, err)
- return
- pending_entry["phase"] = "verify"
- pending_entry["state_before"] = state
- pending_entry["focus_handoff"] = focus_handoff
- pending_entry["path"] = str(control.get("path", ""))
- pending_entry["game_view_ui_synced"] = bool(control.get("game_view_ui_synced", false))
- _request_game_debug_verify.call_deferred(request_id)
-
-
-func _request_game_debug_verify(request_id: String) -> void:
- var tree := Engine.get_main_loop() as SceneTree
- if tree == null:
- return
- await tree.process_frame
- var pending_entry: Dictionary = _pending.get(request_id, {})
- if str(pending_entry.get("phase", "")) != "verify":
- return
- var session := _first_active_session()
- if session == null:
- var connection: McpConnection = pending_entry.get("connection")
- _clear_pending(request_id)
- if connection != null and is_instance_valid(connection):
- _send_error(connection, request_id, ErrorCodes.EVAL_GAME_NOT_READY,
- "The debugger session ended before runtime control could be verified")
- return
- session.send_message("mcp:debug_status", [request_id])
-
-
-func _handle_game_debug_verify(
- request_id: String, pending_entry: Dictionary, state_after: Dictionary
-) -> void:
- var action := str(pending_entry.get("action", ""))
- var connection: McpConnection = pending_entry.get("connection")
- var state_before: Dictionary = pending_entry.get("state_before", {})
- var verdict := game_debug_verification(action, state_before, state_after)
- match str(verdict.get("status", "failed")):
- "pending":
- _request_game_debug_verify.call_deferred(request_id)
- return
- "verified":
- pass
- _:
- var err := ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Game debug action '%s' violated its verification contract" % action)
- err["error"]["data"] = {
- "action": action,
- "reason": verdict.get("reason", "verification_failed"),
- "ticks_advanced": verdict.get("ticks_advanced", -1),
- "state_before": state_before,
- "state_after": state_after,
- "focus_handoff": pending_entry.get("focus_handoff", {}),
- "path": pending_entry.get("path", ""),
- "game_view_ui_synced": pending_entry.get("game_view_ui_synced", false),
- }
- _clear_pending(request_id)
- if connection != null and is_instance_valid(connection):
- _send_error_response(connection, request_id, err)
- return
- var payload := {
- "action": action,
- "changed": true,
- "verified": true,
- "state_before": state_before,
- "state_after": state_after,
- "focus_handoff": pending_entry.get("focus_handoff", {}),
- "path": pending_entry.get("path", ""),
- "game_view_ui_synced": pending_entry.get("game_view_ui_synced", false),
- }
- if action == "next_frame":
- payload["ticks_advanced"] = int(verdict.get("ticks_advanced", -1))
- _clear_pending(request_id)
- if connection != null and is_instance_valid(connection):
- connection.send_deferred_response(request_id, {"data": payload})
-
-
-func _focus_embedded_game_view() -> Dictionary:
- var result := {
- "attempted": true,
- "main_screen": "Game",
- "embedded_process_found": false,
- "focused": false,
- }
- EditorInterface.set_main_screen_editor("Game")
- var base := EditorInterface.get_base_control()
- if base == null:
- return result
- var embedded_nodes: Array[Node] = []
- _collect_nodes_of_class(base, "EmbeddedProcess", embedded_nodes)
- for node in embedded_nodes:
- if node is Control:
- var embedded := node as Control
- result["embedded_process_found"] = true
- embedded.grab_focus()
- result["focused"] = embedded.has_focus()
- break
- return result
-
-
-static func direct_game_debug_message(action: String, desired_suspended: bool) -> Dictionary:
- match action:
- "suspend", "resume":
- return {"message": "scene:suspend_changed", "data": [desired_suspended]}
- "next_frame":
- return {"message": "scene:next_frame", "data": []}
- return {}
-
-
-func _emit_game_debug_runtime_action(action: String, desired_suspended: bool) -> Dictionary:
- var embed_action := EMBED_NEXT_FRAME if action == "next_frame" else EMBED_SUSPEND_TOGGLE
- var base := EditorInterface.get_base_control()
- if base != null:
- var debuggers: Array[Node] = []
- _collect_nodes_of_class(base, "ScriptEditorDebugger", debuggers)
- for debugger in debuggers:
- if not debugger.has_signal("embed_shortcut_requested"):
- continue
- if debugger.get_signal_connection_list("embed_shortcut_requested").is_empty():
- continue
- debugger.emit_signal("embed_shortcut_requested", embed_action)
- return {"ok": true, "path": "embed_signal", "game_view_ui_synced": true}
-
- var session := _first_active_session()
- if session == null or not session.is_active():
- return {"ok": false, "reason": "no_active_debugger_session"}
- var direct := direct_game_debug_message(action, desired_suspended)
- if direct.is_empty():
- return {"ok": false, "reason": "unsupported_action"}
- session.send_message(str(direct.message), direct.data as Array)
- ## The direct debugger-session path works without embedding, but bypasses
- ## Game View's suspend button, so its visual pressed state is not synchronized.
- return {"ok": true, "path": "direct_session", "game_view_ui_synced": false}
-
-
-func _fail_pending_game_debug_controls_not_ready(message: String) -> void:
- for request_id in _pending.keys():
- var pending_entry: Dictionary = _pending.get(request_id, {})
- if str(pending_entry.get("kind", "")) != "game_debug_control":
- continue
- var connection: McpConnection = pending_entry.get("connection")
- _clear_pending(str(request_id))
- if connection != null and is_instance_valid(connection):
- _send_error(connection, str(request_id), ErrorCodes.EVAL_GAME_NOT_READY, message)
-
-
-## --- game_command: curated runtime game operations ---
-
-func request_game_command(
- op: String,
- params: Dictionary,
- request_id: String,
- connection: McpConnection,
- timeout_sec: float = 10.0,
-) -> void:
- if request_id.is_empty():
- push_warning("MCP debugger: game command request missing request_id")
- return
-
- var tree := Engine.get_main_loop() as SceneTree
- if tree == null:
- _send_error(connection, request_id, ErrorCodes.INTERNAL_ERROR,
- "Editor main loop is not a SceneTree — cannot schedule game command")
- return
-
- if is_game_capture_ready():
- _send_game_command(tree, op, params, request_id, connection, timeout_sec)
- return
-
- if _log_buffer:
- _log_buffer.log("[debug] waiting for game_helper hello before game_command (%s)" % request_id)
- _wait_then_game_command(tree, op, params, request_id, connection, timeout_sec)
-
-
-func _wait_then_game_command(
- tree: SceneTree,
- op: String,
- params: Dictionary,
- request_id: String,
- connection: McpConnection,
- timeout_sec: float,
-) -> void:
- var work := ScriptWork.begin("game_command_ready")
- await _settle_game_command_ready(tree, op, params, request_id, connection, timeout_sec)
- ScriptWork.finish(work)
-
-
-func _settle_game_command_ready(
- tree: SceneTree, op: String, params: Dictionary, request_id: String,
- connection: McpConnection, timeout_sec: float,
-) -> void:
- var deadline := Time.get_ticks_msec() + int(GAME_READY_WAIT_SEC * 1000.0)
- ## #645: the leading yield guarantees the dispatcher has registered the
- ## deferred request before any reply (a same-frame reply is dropped as
- ## expired); the break check bails out early because a parked game never
- ## registers its capture.
- await tree.process_frame
- while not is_game_capture_ready() and not _break_active and Time.get_ticks_msec() < deadline:
- await tree.process_frame
- if not is_game_capture_ready():
- _send_error_response(connection, request_id,
- _explain_not_live(get_game_status(-1, GAME_READY_WAIT_SEC), ErrorCodes.INTERNAL_ERROR))
- return
- _send_game_command(tree, op, params, request_id, connection, timeout_sec)
-
-
-func _send_game_command(
- tree: SceneTree,
- op: String,
- params: Dictionary,
- request_id: String,
- connection: McpConnection,
- timeout_sec: float,
-) -> void:
- var session: EditorDebuggerSession = _first_active_session()
- if session == null:
- _send_error(connection, request_id, ErrorCodes.INTERNAL_ERROR,
- "No active debugger session — is the game actually running?")
- return
-
- var timer: SceneTreeTimer = tree.create_timer(timeout_sec)
- var timeout_callable := func() -> void:
- var pending_entry = _pending.get(request_id)
- if pending_entry == null:
- return
- _pending.erase(request_id)
- var conn: McpConnection = pending_entry.connection
- if conn == null or not is_instance_valid(conn):
- return
- _send_error(conn, request_id, ErrorCodes.INTERNAL_ERROR,
- "Game command '%s' timed out after %.0fs" % [op, timeout_sec])
- if _log_buffer:
- _log_buffer.log("[debug] !! game_command timeout (%s)" % request_id)
- timer.timeout.connect(timeout_callable)
- _pending[request_id] = {
- "connection": connection,
- "timer": timer,
- "timeout_callable": timeout_callable,
- }
-
- session.send_message("mcp:game_command", [request_id, op, JSON.stringify(params)])
- if _log_buffer:
- _log_buffer.log("[debug] -> mcp:game_command %s (%s)" % [op, request_id])
-
-
-func _on_game_command_response(data: Array) -> void:
- if data.size() < 2:
- push_warning("MCP debugger: malformed game_command response (expected 2 fields, got %d)" % data.size())
- return
- var request_id: String = data[0]
- var pending_entry = _pending.get(request_id)
- if pending_entry == null:
- return
- _clear_pending(request_id)
-
- var connection: McpConnection = pending_entry.connection
- if connection == null or not is_instance_valid(connection):
- return
-
- var result_json: String = data[1] if data.size() > 1 else "{}"
- var json := JSON.new()
- var parse_err := json.parse(result_json)
- connection.send_deferred_response(request_id, {
- "data": json.data if parse_err == OK else {"source": "game", "result": result_json}
- })
- if _log_buffer:
- _log_buffer.log("[debug] <- mcp:game_command_response (%s)" % request_id)
-
-
-func _on_game_command_error(data: Array) -> void:
- if data.size() < 2:
- return
- var request_id: String = data[0]
- var message: String = data[1]
- var pending_entry = _pending.get(request_id)
- if pending_entry == null:
- return
- _clear_pending(request_id)
- var connection: McpConnection = pending_entry.connection
- if connection == null or not is_instance_valid(connection):
- return
- _send_error(connection, request_id, ErrorCodes.INTERNAL_ERROR, message)
- if _log_buffer:
- _log_buffer.log("[debug] <- mcp:game_command_error (%s): %s" % [request_id, message])
diff --git a/addons/godot_ai/debugger/mcp_debugger_plugin.gd.uid b/addons/godot_ai/debugger/mcp_debugger_plugin.gd.uid
deleted file mode 100644
index 1d5c148..0000000
--- a/addons/godot_ai/debugger/mcp_debugger_plugin.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bd1k63iye1bsl
diff --git a/addons/godot_ai/dispatcher.gd b/addons/godot_ai/dispatcher.gd
deleted file mode 100644
index 252fcb6..0000000
--- a/addons/godot_ai/dispatcher.gd
+++ /dev/null
@@ -1,529 +0,0 @@
-@tool
-class_name McpDispatcher
-extends RefCounted
-
-## Routes incoming commands to handlers and manages the command queue
-## with a per-frame time budget.
-
-var _command_queue: Array[Dictionary] = []
-var _handlers: Dictionary = {} # command_name -> Callable
-## Lazy handler registration (#736): plugin.gd registers command names
-## against a handler key plus a per-handler script path and constructor
-## args, and the handler script is load()ed and instantiated at the FIRST
-## dispatch of one of its commands. This keeps the ~30 handler scripts
-## (and everything they preload) out of plugin.gd's eager compile
-## closure, which stalled "Initializing plugins" on every editor boot.
-## Materialized commands are promoted into `_handlers`, so the lazy dicts
-## are only consulted on the first call per command.
-var _lazy_handler_specs: Dictionary = {} # handler_key -> {path: String, args: Array}
-var _lazy_handler_cache: Dictionary = {} # handler_key -> handler instance
-var _lazy_commands: Dictionary = {} # command_name -> {handler: String, method: StringName}
-var _pending_deferred: Dictionary = {} # request_id -> {command, started_ms, timeout_ms}
-var _tick_active := false
-var _log_buffer
-var _surfaced_error_tracker
-## The McpConnection whose pause_processing handlers flip around unsafe
-## editor operations (#288 guard). Set by plugin.gd; untyped to honor the
-## self-update field-storage policy. When set, _call_handler restores the
-## pause depth a crashed handler left unbalanced (#712) — without this a
-## single handler crash inside a pause window freezes the transport
-## forever (pause has no watchdog or disconnect reset by design).
-var pause_target
-var mcp_logging := true
-var deferred_timeout_overrides_ms: Dictionary = {}
-
-const DEFAULT_DEFERRED_TIMEOUT_MS := 4500
-const DEFERRED_TIMEOUT_MS_BY_COMMAND := {
- "create_script": 4500,
- ## Fresh-`.gd` writes defer through the same import-settle window as
- ## create_script (#714) — same headroom over IMPORT_SETTLE_MAX_MSEC.
- "write_file": 4500,
- "stop_project": 4500,
- "run_project": 6000,
- "take_screenshot": 30000,
- "check_client_status": 30000,
- "game_eval": 15000,
- "game_command": 15000,
- ## The editor-side runtime-control timer owns 5s; keep the dispatcher
- ## outside it so the actionable control result wins before DEFERRED_TIMEOUT.
- "game_debug_control": 6500,
- "scan_filesystem": 30000,
-}
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-const FuzzySuggestions := preload("res://addons/godot_ai/utils/fuzzy_suggestions.gd")
-const PluginReload := preload("res://addons/godot_ai/utils/plugin_reload.gd")
-
-
-func _init(log_buffer: McpLogBuffer, surfaced_error_tracker = null) -> void:
- _log_buffer = log_buffer
- _surfaced_error_tracker = surfaced_error_tracker
-
-
-## Register a command handler. The callable receives (params: Dictionary) -> Dictionary.
-func register(command_name: String, handler: Callable) -> void:
- _handlers[command_name] = handler
-
-
-## Declare a lazily-constructed handler (#736). `script_path` is load()ed
-## and instantiated with `ctor_args` at the first dispatch of any command
-## registered against `handler_key` via register_lazy. `ctor_args` may hold
-## plugin-lifetime objects (connection, buffers, the dispatcher itself for
-## batch); clear() drops them so teardown ordering matches the old eager
-## registration (#46).
-func register_lazy_handler(handler_key: String, script_path: String, ctor_args: Array) -> void:
- _lazy_handler_specs[handler_key] = {"path": script_path, "args": ctor_args}
-
-
-## Register a command that resolves to `method` on the lazily-constructed
-## handler declared under `handler_key`. Same dispatch semantics as
-## register(); only construction timing differs.
-func register_lazy(command_name: String, handler_key: String, method: StringName) -> void:
- _lazy_commands[command_name] = {"handler": handler_key, "method": method}
-
-func unregister(command_name: String, handler_key: String) -> void:
- _handlers.erase(command_name)
- _lazy_commands.erase(command_name)
- ## Drop the lazy spec/cache only when no remaining command routes to
- ## this handler key. Custom tools use 1:1 keys ("custom:") so this
- ## always erases for them, but built-in keys serve many commands —
- ## erasing the shared spec would strand the siblings on
- ## "No lazy handler declared" at dispatch time.
- for entry in _lazy_commands.values():
- if entry.get("handler", "") == handler_key:
- return
- _lazy_handler_cache.erase(handler_key)
- _lazy_handler_specs.erase(handler_key)
-
-## Synchronously realize handler-owned work before any add-on script can be
-## replaced. A handler that cannot prove quiescence keeps the dispatcher live
-## and makes the caller fail closed.
-func quiesce_for_script_swap() -> Dictionary:
- # Deferred timeouts are not proof that the underlying work has returned;
- # the independent ledger also covers an old composition after a reload.
- var work: Dictionary = preload("res://addons/godot_ai/utils/script_work.gd").quiescence()
- if not bool(work.get("ok", false)):
- return work
- if not _pending_deferred.is_empty():
- return {"ok": false, "error": "Wait for pending tool responses before updating."}
- for handler_key in _lazy_handler_cache:
- var instance: Variant = _lazy_handler_cache[handler_key]
- if not is_instance_valid(instance) or not instance.has_method("quiesce_for_script_swap"):
- return {
- "ok": false,
- "error": "Command handler '%s' cannot prove quiescence for script replacement." % handler_key,
- }
- var result: Variant = instance.call("quiesce_for_script_swap")
- if not result is Dictionary or not bool(result.get("ok", false)):
- return {
- "ok": false,
- "error": "Command handler '%s' could not quiesce for script replacement: %s" % [
- handler_key, result,
- ],
- }
- return {"ok": true}
-
-
-## Drop registered handlers, queued commands, and the log buffer ref so
-## plugin.gd can release RefCounted handlers before Godot reloads their
-## class_name scripts (issue #46). After a successful clear(), the dispatcher
-## is inert. A failed quiesce leaves every reference intact.
-func clear() -> Dictionary:
- var quiesced := quiesce_for_script_swap()
- if not bool(quiesced.get("ok", false)):
- return quiesced
- release_after_teardown()
- return {"ok": true}
-
-
-## Ordinary plugin teardown is not permission to replace scripts. The root
-## first stops transport and joins its client/vision workers, then drops this
-## graph even when a handler cannot certify hot script replacement. Requiring
-## that stronger certificate here leaks the dispatcher <-> handler cycles at
-## every editor exit. Hot-update callers must still use clear(), which refuses
-## to release anything until every materialized handler proves quiescence.
-func release_after_teardown() -> void:
- _handlers.clear()
- ## Release lazily-constructed handler instances (and the ctor args that
- ## reference plugin-lifetime objects) at the same teardown point where
- ## eager handler Callables used to be dropped — their destructors must
- ## run while their scripts are still loaded (#46). This also breaks the
- ## dispatcher -> batch handler -> dispatcher ref cycle.
- _lazy_handler_specs.clear()
- _lazy_handler_cache.clear()
- _lazy_commands.clear()
- _command_queue.clear()
- _pending_deferred.clear()
- _log_buffer = null
- _surfaced_error_tracker = null
- pause_target = null
-
-
-## Drop queued-but-unexecuted commands. Called by the connection on
-## disconnect (#712): commands queued by the previous connection must not
-## execute under the next one — the requester is gone, its in-flight
-## futures were already failed server-side, and a mutation landing after
-## reconnect is a surprise write nobody can correlate. Deferred bookkeeping
-## has its own reset (clear_deferred_responses).
-func clear_command_queue() -> void:
- _command_queue.clear()
-
-
-## Invoke a registered handler directly by name. Returns the handler's raw
-## response dict (no request_id or status wrapping). Returns an UNKNOWN_COMMAND
-## error dict if the command is not registered. Used by batch_execute.
-func dispatch_direct(command: String, params: Dictionary) -> Dictionary:
- if not has_command(command):
- return ErrorCodes.make(ErrorCodes.UNKNOWN_COMMAND, "Unknown command: %s" % command)
- ## Strip the reserved deferred-reply key: only _dispatch may thread it.
- ## A caller-supplied _request_id (e.g. inside a batch_execute
- ## sub-command's params) would flip a deferred-capable handler into
- ## deferred mode against a request id the dispatcher never registered —
- ## the direct caller would get the DEFERRED sentinel instead of a result
- ## and the out-of-band reply would be dropped as expired.
- if params.has("_request_id"):
- params = params.duplicate()
- params.erase("_request_id")
- return _call_handler(command, params)
-
-
-## Whether a command is registered (eagerly or lazily).
-func has_command(command: String) -> bool:
- return _handlers.has(command) or _lazy_commands.has(command)
-
-
-## Rank registered commands by similarity to `cmd_name` and return the top `limit`
-## matches. Uses Godot's built-in String.similarity() (0.0–1.0). Returns an empty
-## array if no candidates clear the threshold. Used by batch_execute to surface
-## "did you mean" suggestions when an unknown command is passed.
-func suggest_similar(cmd_name: String, limit: int = 3, threshold: float = 0.5) -> Array[String]:
- return FuzzySuggestions.rank(cmd_name, _registered_command_names(), limit, threshold, 0.0, 0.0)
-
-
-## Union of eagerly-registered and lazily-registered command names.
-## Materialized lazy commands live in both dicts, so dedupe via keys.
-func _registered_command_names() -> Array:
- var names: Dictionary = {}
- for command in _handlers:
- names[command] = true
- for command in _lazy_commands:
- names[command] = true
- return names.keys()
-
-
-## Enqueue a raw command dict received from the WebSocket.
-func enqueue(cmd: Dictionary) -> void:
- _command_queue.append(cmd)
-
-
-func pending_deferred_count() -> int:
- return _pending_deferred.size()
-
-
-func clear_deferred_responses() -> void:
- _pending_deferred.clear()
-
-
-func has_pending_deferred_response(request_id: String) -> bool:
- return request_id.is_empty() or _pending_deferred.has(request_id)
-
-
-func complete_deferred_response(request_id: String) -> bool:
- if request_id.is_empty():
- return true
- if not _pending_deferred.has(request_id):
- return false
- _pending_deferred.erase(request_id)
- return true
-
-
-## Handlers whose response flows out-of-band (e.g. debugger-channel capture)
-## return this marker so tick() skips auto-sending a response. The handler is
-## responsible for pushing the final response via McpConnection._send_json when
-## the async operation completes. The dispatcher tracks the request_id and emits
-## DEFERRED_TIMEOUT if the out-of-band response never arrives. The request_id is
-## threaded through params under the "_request_id" key so the handler can
-## correlate the response.
-const DEFERRED_RESPONSE := {"_deferred": true}
-
-
-## Process queued commands within a frame budget (milliseconds).
-## Returns an array of response dictionaries to send back.
-func tick(budget_ms: float = 4.0) -> Array[Dictionary]:
- ## Editor filesystem operations can pump another frame before the handler
- ## returns. That frame must not dispatch the still-queued command again.
- if _tick_active or PluginReload.is_reload_pending():
- return []
- _tick_active = true
- var responses: Array[Dictionary] = _collect_deferred_timeouts()
- var start := Time.get_ticks_msec()
- var idx := 0
-
- while idx < _command_queue.size() and (Time.get_ticks_msec() - start) < budget_ms:
- var cmd: Dictionary = _command_queue[idx]
- var response := _dispatch(cmd)
- if not response.get("_deferred", false):
- responses.append(response)
- idx += 1
- if PluginReload.is_reload_pending():
- break
-
- if idx > 0:
- _command_queue = _command_queue.slice(idx)
-
- _tick_active = false
- return responses
-
-
-func _dispatch(cmd: Dictionary) -> Dictionary:
- var request_id: String = cmd.get("request_id", "")
- var command: String = cmd.get("command", "")
- var raw_params: Dictionary = cmd.get("params", {})
- ## Duplicate so the internal _request_id key we thread through doesn't
- ## mutate the queued command's params (which is the same dict we're
- ## about to JSON-log below, and which later readers like batch_execute
- ## shouldn't see dispatcher-internal metadata from).
- var params: Dictionary = raw_params.duplicate()
- params["_request_id"] = request_id
-
- if mcp_logging:
- _log_buffer.log("[recv] %s(%s)" % [command, JSON.stringify(raw_params)])
-
- var result: Dictionary
-
- if has_command(command):
- result = _call_handler(command, params)
- else:
- result = ErrorCodes.make(ErrorCodes.UNKNOWN_COMMAND, "Unknown command: %s" % command)
-
- if result.get("_deferred", false):
- ## A handler may attach `_deferred_timeout_ms` to its deferred sentinel
- ## to claim a per-request budget larger than its command's shared entry
- ## (e.g. game_command's `input_sequence`, which steps frames well past
- ## the 15s that suits one-shot game ops). 0/absent falls back to the
- ## per-command table.
- _register_deferred(request_id, command, int(result.get("_deferred_timeout_ms", 0)))
- if mcp_logging:
- _log_buffer.log("[defer] %s (request %s)" % [command, request_id])
- return result
-
- result["request_id"] = request_id
- if not result.has("status"):
- result["status"] = "ok"
- ## Stamp live editor readiness onto every command-response envelope so
- ## the server's `Session.readiness` cache self-heals on the very next
- ## tool call. Without this, a single dropped `readiness_changed` event
- ## (or a one-frame race around `pause_processing`) leaves the cache
- ## stuck at "playing" / "importing" long after the editor has settled,
- ## and write tools fail with EDITOR_NOT_READY against a writable editor.
- ## See connection.gd::send_deferred_response for the deferred-response
- ## counterpart, which stamps the same field.
- result["readiness"] = McpConnection.get_readiness()
- _stamp_error_watermark(result)
-
- if mcp_logging:
- var status: String = result.get("status", "ok")
- if status == "ok":
- _log_buffer.log("[send] %s -> ok" % command)
- else:
- var err_msg: String = result.get("error", {}).get("message", "unknown")
- _log_buffer.log("[send] %s -> error: %s" % [command, err_msg])
-
- return result
-
-
-## Truncate JSON-stringified args at this many chars when stuffing them into
-## a malformed-result error message — large dicts shouldn't bloat the
-## response, but a few hundred chars usually pinpoints which param was the
-## wrong shape.
-const _MALFORMED_ARGS_MAX := 400
-
-
-func _call_handler(command: String, params: Dictionary) -> Dictionary:
- if not _handlers.has(command):
- var materialize_error := _materialize_lazy_command(command)
- if not materialize_error.is_empty():
- return materialize_error
- ## #712: a handler that crashes between pause_processing = true and its
- ## matching false leaves the pause depth unbalanced — GDScript swallows
- ## the error, the dispatcher reports "malformed result", and the
- ## transport stays paused FOREVER (no watchdog, no disconnect reset).
- ## Restore balance at this boundary: the depth a handler leaves behind
- ## must equal the depth it started with.
- var pause_depth_before: int = pause_target.pause_depth() if pause_target != null else 0
- var result: Dictionary = _handlers[command].call(params)
- if pause_target != null and pause_target.pause_depth() > pause_depth_before:
- var leaked: int = pause_target.pause_depth() - pause_depth_before
- while pause_target.pause_depth() > pause_depth_before:
- pause_target.resume()
- if mcp_logging and _log_buffer != null:
- _log_buffer.log(
- "[error] %s leaked %d pause_processing level(s) — restored (handler crash?)"
- % [command, leaked]
- )
- ## Handlers must return {"data": ...} on success or {"error": ...} on failure.
- ## Anything else (null, empty, missing keys) means the handler crashed
- ## mid-call — GDScript swallows the error and returns an empty dict.
- if result == null or not (result.has("data") or result.has("error") or result.has("_deferred")):
- var safe_params := params.duplicate()
- safe_params.erase("_request_id")
- var args_json := JSON.stringify(safe_params)
- if args_json.length() > _MALFORMED_ARGS_MAX:
- args_json = args_json.substr(0, _MALFORMED_ARGS_MAX) + "..."
- var backtrace := _capture_compact_backtrace()
- var msg := (
- "Handler '%s' returned malformed result — likely a runtime error in the handler "
- + "(e.g. param type mismatch). Args received: %s"
- ) % [command, args_json]
- if not backtrace.is_empty():
- msg += "\nBacktrace:\n%s" % backtrace
- if mcp_logging and _log_buffer != null:
- var compact_backtrace := backtrace.replace("\n", " | ")
- _log_buffer.log(
- "[error] %s -> malformed result; args=%s; backtrace=%s"
- % [command, args_json, compact_backtrace]
- )
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, msg)
- return result
-
-
-## Resolve a lazily-registered command into a live Callable in `_handlers`.
-## Loads + constructs the owning handler on first use (cached per handler
-## key, so one load() covers every command the handler serves). Returns an
-## empty dict on success or a protocol error dict on failure — a missing
-## script or method is a plugin packaging bug and must surface loudly, not
-## as a silent no-op.
-func _materialize_lazy_command(command: String) -> Dictionary:
- var command_spec: Dictionary = _lazy_commands.get(command, {})
- if command_spec.is_empty():
- return ErrorCodes.make(ErrorCodes.UNKNOWN_COMMAND, "Unknown command: %s" % command)
- var handler_key: String = command_spec["handler"]
- var instance = _lazy_handler_cache.get(handler_key)
- if instance == null:
- var handler_spec: Dictionary = _lazy_handler_specs.get(handler_key, {})
- if handler_spec.is_empty():
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "No lazy handler '%s' declared for command '%s'" % [handler_key, command]
- )
- ## Existence-check first so a missing script surfaces as one clean
- ## protocol error instead of also spraying engine load errors.
- if not ResourceLoader.exists(handler_spec["path"]):
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Missing handler script '%s' for command '%s'" % [handler_spec["path"], command]
- )
- var script := load(handler_spec["path"]) as GDScript
- if script == null:
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Failed to load handler script '%s' for command '%s'" % [handler_spec["path"], command]
- )
- instance = script.callv("new", handler_spec["args"])
- if instance == null:
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Failed to construct handler '%s' for command '%s'" % [handler_key, command]
- )
- _lazy_handler_cache[handler_key] = instance
- var method: StringName = command_spec["method"]
- if not instance.has_method(method):
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Handler '%s' has no method '%s' for command '%s'" % [handler_key, method, command]
- )
- _handlers[command] = Callable(instance, method)
- return {}
-
-
-func _register_deferred(request_id: String, command: String, timeout_override_ms: int = 0) -> void:
- if request_id.is_empty():
- return
- ## A positive per-request override wins over the per-command table so a
- ## single deferred call can claim more headroom without globally widening
- ## the command's budget (see _dispatch: input_sequence needs ~30s, but the
- ## other game_command ops must keep their tight 15s).
- var timeout_ms: int = (
- timeout_override_ms if timeout_override_ms > 0
- else _deferred_timeout_ms_for_command(command)
- )
- _pending_deferred[request_id] = {
- "command": command,
- "started_ms": Time.get_ticks_msec(),
- "timeout_ms": timeout_ms,
- }
-
-
-func _deferred_timeout_ms_for_command(command: String) -> int:
- if deferred_timeout_overrides_ms.has(command):
- return int(deferred_timeout_overrides_ms[command])
- return int(DEFERRED_TIMEOUT_MS_BY_COMMAND.get(command, DEFAULT_DEFERRED_TIMEOUT_MS))
-
-
-func _collect_deferred_timeouts() -> Array[Dictionary]:
- var responses: Array[Dictionary] = []
- if _pending_deferred.is_empty():
- return responses
- var now := Time.get_ticks_msec()
- for request_id in _pending_deferred.keys():
- var entry: Dictionary = _pending_deferred[request_id]
- var timeout_ms: int = entry.get("timeout_ms", DEFAULT_DEFERRED_TIMEOUT_MS)
- var elapsed_ms := now - int(entry.get("started_ms", now))
- if elapsed_ms < timeout_ms:
- continue
- _pending_deferred.erase(request_id)
- var command: String = entry.get("command", "")
- var response := ErrorCodes.make(
- ErrorCodes.DEFERRED_TIMEOUT,
- "Deferred response for '%s' timed out after %dms" % [command, timeout_ms]
- )
- response["request_id"] = request_id
- response["error"]["data"] = {
- "command": command,
- "elapsed_ms": elapsed_ms,
- "timeout_ms": timeout_ms,
- }
- ## Same envelope-level readiness stamp as `_dispatch` — keep the
- ## self-heal channel symmetric across every reply shape the
- ## dispatcher emits so the server cache can't drift just because
- ## the editor happened to time out a deferred command.
- response["readiness"] = McpConnection.get_readiness()
- _stamp_error_watermark(response)
- responses.append(response)
- if mcp_logging and _log_buffer != null:
- _log_buffer.log("[defer] %s (request %s) -> timeout" % [command, request_id])
- return responses
-
-
-func _stamp_error_watermark(response: Dictionary) -> void:
- McpSurfacedErrorTracker.stamp_watermark(response, _surfaced_error_tracker)
-
-
-static func _capture_compact_backtrace(max_frames: int = 8) -> String:
- var traces: Array = Engine.capture_script_backtraces(false)
- for bt in traces:
- if bt != null and not bt.is_empty():
- return _trim_backtrace_string(bt.format(0, 2), max_frames)
- return _format_stack_frames(get_stack(), max_frames)
-
-
-static func _trim_backtrace_string(text: String, max_frames: int) -> String:
- var lines := text.strip_edges().split("\n")
- var kept: Array[String] = []
- for i in range(min(lines.size(), max_frames)):
- kept.append(lines[i].strip_edges())
- return "\n".join(kept)
-
-
-static func _format_stack_frames(frames: Array, max_frames: int) -> String:
- var lines: Array[String] = []
- for i in range(min(frames.size(), max_frames)):
- var frame: Dictionary = frames[i]
- lines.append(
- "%s:%s in %s"
- % [
- frame.get("source", "?"),
- frame.get("line", 0),
- frame.get("function", "?"),
- ]
- )
- return "\n".join(lines)
diff --git a/addons/godot_ai/dispatcher.gd.uid b/addons/godot_ai/dispatcher.gd.uid
deleted file mode 100644
index 25a05bd..0000000
--- a/addons/godot_ai/dispatcher.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ctldk7ivsoo3i
diff --git a/addons/godot_ai/dock_panels/log_viewer.gd b/addons/godot_ai/dock_panels/log_viewer.gd
deleted file mode 100644
index 150912d..0000000
--- a/addons/godot_ai/dock_panels/log_viewer.gd
+++ /dev/null
@@ -1,86 +0,0 @@
-@tool
-extends VBoxContainer
-
-## Dock subpanel — renders the MCP request/response log buffer. Owns its own
-## UI subtree, the line-count cursor, and the display-visibility toggle. It
-## receives copied `{sequence, reset, lines}` values; neither this view nor its
-## parent Dock retains the root-owned log buffer.
-##
-## Extracted from mcp_dock.gd as part of audit-v2 #360 — see the comment at
-## the top of mcp_dock.gd for the broader extraction story.
-
-signal logging_enabled_changed(enabled: bool)
-
-const Dock := preload("res://addons/godot_ai/mcp_dock.gd")
-const Settings := preload("res://addons/godot_ai/utils/settings.gd")
-
-var _log_display: RichTextLabel
-var _log_toggle: CheckButton
-## Last `McpLogBuffer.total_logged()` value painted into the display. Tracking
-## the buffer's monotonic sequence (rather than its bounded `total_count()`)
-## keeps the viewer painting once the ring fills — a size-based cursor would
-## freeze at MAX_LINES on every subsequent append. See PR #392 for the bug.
-var _last_log_seq := 0
-
-
-## Build the UI synchronously here so callers (and detached-tree tests that
-## instantiate the dock with `McpDockScript.new()` and never enter the tree)
-## can interact with the panel's controls right after `setup()`. Mirrors the
-## pre-extraction inline-build behavior that test_dock.gd relies on.
-##
-## Idempotent: `_log_display == null` covers an unlikely double-`setup()` call
-## without rebuilding (which would orphan the prior controls).
-func setup() -> void:
- if _log_display == null:
- _build_ui()
-
-
-func _build_ui() -> void:
- add_child(HSeparator.new())
-
- var log_header_row := HBoxContainer.new()
- var log_header := Dock._make_header("MCP Log")
- log_header.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- log_header_row.add_child(log_header)
-
- _log_toggle = CheckButton.new()
- _log_toggle.text = "Log"
- ## Restore the persisted choice — a hardcoded `true` here meant the
- ## toggle reset to noisy on every editor restart (#626).
- _log_toggle.button_pressed = Settings.mcp_logging_enabled()
- _log_toggle.toggled.connect(_on_log_toggled)
- log_header_row.add_child(_log_toggle)
-
- add_child(log_header_row)
-
- _log_display = RichTextLabel.new()
- _log_display.custom_minimum_size = Vector2(0, 80)
- _log_display.scroll_following = true
- _log_display.bbcode_enabled = false
- _log_display.selection_enabled = true
- _log_display.visible = _log_toggle.button_pressed
- add_child(_log_display)
-
-
-func sequence() -> int:
- return _last_log_seq
-
-
-func present_snapshot(snapshot: Dictionary) -> void:
- if _log_display == null:
- return
- var seq := int(snapshot.get("sequence", _last_log_seq))
- if bool(snapshot.get("reset", false)) or seq < _last_log_seq:
- _log_display.clear()
- _last_log_seq = 0
- var lines: Array[String] = []
- lines.assign(snapshot.get("lines", []))
- for line in lines:
- _log_display.add_text(line + "\n")
- _last_log_seq = seq
-
-
-func _on_log_toggled(enabled: bool) -> void:
- Settings.set_mcp_logging_enabled(enabled)
- _log_display.visible = enabled
- logging_enabled_changed.emit(enabled)
diff --git a/addons/godot_ai/dock_panels/log_viewer.gd.uid b/addons/godot_ai/dock_panels/log_viewer.gd.uid
deleted file mode 100644
index c261627..0000000
--- a/addons/godot_ai/dock_panels/log_viewer.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cr5nbnd6vj3b8
diff --git a/addons/godot_ai/dock_panels/port_picker_panel.gd b/addons/godot_ai/dock_panels/port_picker_panel.gd
deleted file mode 100644
index ea935b4..0000000
--- a/addons/godot_ai/dock_panels/port_picker_panel.gd
+++ /dev/null
@@ -1,106 +0,0 @@
-@tool
-extends VBoxContainer
-
-## Dock subpanel — port-change escape hatch surfaced inside the spawn-failure
-## crash panel when a port is contested or an incompatible server cannot be reclaimed.
-##
-## It moves BOTH ports. A godot-ai server binds its HTTP and WebSocket ports
-## together, so moving only the HTTP port onto a free number lands the next
-## launch on a WebSocket port the previous server still holds, and the server
-## dies at preflight ("WebSocket port 19630 is already in use"). Emits
-## `port_apply_requested(new_http_port, new_ws_port)` after range-validation;
-## the dock writes the EditorSettings and reloads the plugin.
-##
-## Extracted from mcp_dock.gd as part of audit-v2 #360 — see the comment at
-## the top of mcp_dock.gd for the broader extraction story.
-
-const ClientConfigurator := preload("res://addons/godot_ai/client_configurator.gd")
-const PortResolver := preload("res://addons/godot_ai/utils/port_resolver.gd")
-
-signal port_apply_requested(new_http_port: int, new_ws_port: int)
-
-var _spinbox: SpinBox
-var _ws_spinbox: SpinBox
-## Bind probe used when seeding; tests substitute a deterministic one.
-var port_in_use_probe: Callable = Callable(PortResolver, "is_port_in_use")
-
-
-## Build the UI synchronously here so callers (and detached-tree tests that
-## instantiate the dock with `McpDockScript.new()` and never enter the tree)
-## can interact with the panel's controls right after `setup()`. Mirrors the
-## pre-extraction inline-build behavior that test_dock.gd relies on.
-##
-## Idempotent: `_spinbox == null` covers an unlikely double-`setup()` call
-## without rebuilding (which would orphan the prior controls).
-func setup() -> void:
- if _spinbox == null:
- _build_ui()
-
-
-func _build_ui() -> void:
- add_theme_constant_override("separation", 4)
- visible = false
-
- var picker_row := HBoxContainer.new()
- picker_row.add_theme_constant_override("separation", 6)
-
- _spinbox = _port_spinbox(ClientConfigurator.http_port(), "Effective HTTP port")
- picker_row.add_child(_spinbox)
- _ws_spinbox = _port_spinbox(ClientConfigurator.ws_port(), "Effective WebSocket port")
- picker_row.add_child(_ws_spinbox)
-
- var apply_btn := Button.new()
- apply_btn.text = "Apply + Reload"
- apply_btn.tooltip_text = (
- "Saves the effective HTTP and WebSocket ports to Editor Settings and reloads"
- + " the plugin so the server spawns on the new ports. Reconfigure your AI"
- + " clients afterwards so their bridges use the new ports."
- )
- apply_btn.pressed.connect(_on_apply_pressed)
- picker_row.add_child(apply_btn)
-
- add_child(picker_row)
-
-
-static func _port_spinbox(value: int, tooltip: String) -> SpinBox:
- var spinbox := SpinBox.new()
- spinbox.min_value = ClientConfigurator.MIN_PORT
- spinbox.max_value = ClientConfigurator.MAX_PORT
- spinbox.step = 1
- spinbox.value = value
- spinbox.tooltip_text = tooltip
- spinbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- return spinbox
-
-
-## Seed both boxes every time the panel surfaces, so a stale value from a
-## previous spawn-failure round cannot carry over. The diagnosed
-## `conflict_port` and any port that is simply in use get a fresh free
-## suggestion; a port that is free keeps its value, so clients whose entries
-## carry it keep working. Note that this OVERWRITES unsaved user input — fine
-## in practice because the dock only calls this on `server_status`
-## transitions (`if server_status == _last_server_status: return`).
-func seed_suggested_ports(conflict_port := 0) -> void:
- if _spinbox == null:
- return
- var http := ClientConfigurator.http_port()
- var ws := ClientConfigurator.ws_port()
- if conflict_port == http or bool(port_in_use_probe.call(http)):
- http = ClientConfigurator.suggest_free_port(http + 1)
- if conflict_port == ws or ws == http or bool(port_in_use_probe.call(ws)):
- ws = ClientConfigurator.suggest_free_port(ws + 1)
- if ws == http:
- ws = ClientConfigurator.suggest_free_port(ws + 1)
- _spinbox.value = http
- _ws_spinbox.value = ws
-
-
-func _on_apply_pressed() -> void:
- var new_http: int = int(_spinbox.value)
- var new_ws: int = int(_ws_spinbox.value)
- for port in [new_http, new_ws]:
- if port < ClientConfigurator.MIN_PORT or port > ClientConfigurator.MAX_PORT:
- return
- if new_http == new_ws:
- return
- port_apply_requested.emit(new_http, new_ws)
diff --git a/addons/godot_ai/dock_panels/port_picker_panel.gd.uid b/addons/godot_ai/dock_panels/port_picker_panel.gd.uid
deleted file mode 100644
index 38b6320..0000000
--- a/addons/godot_ai/dock_panels/port_picker_panel.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://hlggbo1q65eq
diff --git a/addons/godot_ai/export/mcp_export_plugin.gd b/addons/godot_ai/export/mcp_export_plugin.gd
deleted file mode 100644
index 5a8b885..0000000
--- a/addons/godot_ai/export/mcp_export_plugin.gd
+++ /dev/null
@@ -1,69 +0,0 @@
-@tool
-extends EditorExportPlugin
-
-## Strips the MCP game-helper autoload from exported builds (#740).
-##
-## plugin.gd writes `autoload/_mcp_game_helper` into project.godot so the
-## editor-spawned game process loads the helper. Exports bake project
-## settings into the pack's project.binary, so without this plugin every
-## export ships the autoload — and users who exclude addons/godot_ai/**
-## in their export preset get three "Failed to instantiate an autoload"
-## errors at game start. Even when the files ARE shipped, the helper is
-## editor-tooling: no exported build should carry it.
-##
-## Strip mechanics: clear the in-memory ProjectSettings entry in
-## _export_begin, restore it in _export_end. The export pipeline reads
-## the live ProjectSettings when it bakes project.binary, which happens
-## after _export_begin — verified end-to-end by
-## script/ci-export-strip-smoke, which exports a real pack and asserts
-## the autoload is absent inside it. We never call ProjectSettings.save()
-## while stripped, so project.godot on disk keeps the autoload
-## throughout; only the export snapshot loses it.
-##
-## Failure containment: if an export aborts so hard that _export_end
-## never fires, the damage is bounded to the editor's in-memory settings
-## — the running game reads project.godot from disk, and plugin.gd's
-## _ensure_game_helper_autoload() re-asserts the entry on the next
-## plugin enable / editor launch.
-
-## Must equal "autoload/" + plugin.gd's GAME_HELPER_AUTOLOAD_NAME.
-## Duplicated (not preloaded from plugin.gd) to avoid a cyclic preload —
-## plugin.gd preloads this script. The pairing is locked by
-## test_export_strip.gd's constants-contract test.
-const AUTOLOAD_KEY := "autoload/_mcp_game_helper"
-
-var _saved_value: Variant = null
-var _stripped := false
-
-
-func _get_name() -> String:
- return "GodotAIStripAutoload"
-
-
-func _export_begin(_features: PackedStringArray, _is_debug: bool, _path: String, _flags: int) -> void:
- ## `_stripped` guard: if a previous export died before _export_end,
- ## don't overwrite the genuinely-saved value with the already-cleared
- ## state — restore semantics stay anchored to the original value.
- if _stripped:
- return
- if not ProjectSettings.has_setting(AUTOLOAD_KEY):
- return
- _saved_value = ProjectSettings.get_setting(AUTOLOAD_KEY)
- ## Setting a project setting to null erases it.
- ProjectSettings.set_setting(AUTOLOAD_KEY, null)
- _stripped = true
- print("MCP | export: stripping %s from the exported pack (restored in the editor after export)" % AUTOLOAD_KEY)
-
-
-func _export_end() -> void:
- if not _stripped:
- return
- ProjectSettings.set_setting(AUTOLOAD_KEY, _saved_value)
- ## Mirror _ensure_game_helper_autoload()'s registration shape so the
- ## restored entry is indistinguishable from the original: initial
- ## value "" keeps project.godot diff-clean, basic keeps it visible in
- ## the non-advanced settings view.
- ProjectSettings.set_initial_value(AUTOLOAD_KEY, "")
- ProjectSettings.set_as_basic(AUTOLOAD_KEY, true)
- _saved_value = null
- _stripped = false
diff --git a/addons/godot_ai/export/mcp_export_plugin.gd.uid b/addons/godot_ai/export/mcp_export_plugin.gd.uid
deleted file mode 100644
index f2a3634..0000000
--- a/addons/godot_ai/export/mcp_export_plugin.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://np0cp6fwpim7
diff --git a/addons/godot_ai/handlers/_node_validator.gd b/addons/godot_ai/handlers/_node_validator.gd
deleted file mode 100644
index 61bbe10..0000000
--- a/addons/godot_ai/handlers/_node_validator.gd
+++ /dev/null
@@ -1,71 +0,0 @@
-@tool
-class_name McpNodeValidator
-extends RefCounted
-
-## Shared resolve-or-error helper that subsumes the 38+ sites where
-## handlers each rolled their own "is the editor ready, does the path
-## resolve, otherwise return EDITOR_NOT_READY / NODE_NOT_FOUND" guard.
-##
-## audit-v2 #20 (issue #364). Uses the audit-v2 #21 (issue #365) error
-## vocabulary.
-
-## Local const names alias the preloaded scripts. The naming choice is
-## stylistic, not an upgrade-safety boundary: bare `McpErrorCodes.MEMBER`
-## and `ErrorCodes.MEMBER` both depend on the Script object Godot has for
-## `error_codes.gd`. The transient #398 parse errors were caused by the
-## old runner scanning a mixed old/new plugin snapshot and seeing stale
-## Script-object content; the runner now writes one v(N+1) snapshot before
-## its scan.
-const ScenePath := preload("res://addons/godot_ai/utils/scene_path.gd")
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-
-## Resolve a scene-relative path to the live Node, or return a structured
-## error dict.
-##
-## Success shape: `{"node": Node, "scene_root": Node, "path": String}`.
-## Error shape: matches `ErrorCodes.make(...)` so callers can
-## `return resolved` to propagate.
-##
-## Errors (in order checked):
-## - `MISSING_REQUIRED_PARAM`: `node_path` is empty
-## - `EDITOR_NOT_READY`: no scene open
-## - `EDITED_SCENE_MISMATCH`: caller pinned `scene_file` and the open
-## scene's path doesn't match
-## - `NODE_NOT_FOUND`: `node_path` doesn't resolve under the scene root
-##
-## `param_name` is the agent-facing name reported in the
-## `MISSING_REQUIRED_PARAM` message — handlers pass "node_path",
-## "player_path", "target_path", etc. so the error reads like the
-## hand-written messages it replaces.
-static func resolve_or_error(
- node_path: String,
- param_name: String = "path",
- scene_file: String = "",
-) -> Dictionary:
- if node_path.is_empty():
- return ErrorCodes.make(
- ErrorCodes.MISSING_REQUIRED_PARAM,
- "Missing required param: %s" % param_name,
- )
- var scene_check := ScenePath.require_edited_scene(scene_file)
- if scene_check.has("error"):
- return scene_check
- var scene_root: Node = scene_check.node
- var node := ScenePath.resolve(node_path, scene_root)
- if node == null:
- return ErrorCodes.make(
- ErrorCodes.NODE_NOT_FOUND,
- ScenePath.format_node_error(node_path, scene_root),
- )
- return {"node": node, "scene_root": scene_root, "path": node_path}
-
-
-## When the caller needs the scene root but no specific node yet — e.g.
-## handlers that walk children or filter by group. Returns either
-## `{"scene_root": Node}` or an `ErrorCodes.make(...)` error dict.
-static func require_scene_or_error(scene_file: String = "") -> Dictionary:
- var scene_check := ScenePath.require_edited_scene(scene_file)
- if scene_check.has("error"):
- return scene_check
- return {"scene_root": scene_check.node}
diff --git a/addons/godot_ai/handlers/_node_validator.gd.uid b/addons/godot_ai/handlers/_node_validator.gd.uid
deleted file mode 100644
index b3153a6..0000000
--- a/addons/godot_ai/handlers/_node_validator.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dn75jifad0ghx
diff --git a/addons/godot_ai/handlers/_param_validators.gd b/addons/godot_ai/handlers/_param_validators.gd
deleted file mode 100644
index a316b32..0000000
--- a/addons/godot_ai/handlers/_param_validators.gd
+++ /dev/null
@@ -1,30 +0,0 @@
-@tool
-class_name McpParamValidators
-extends RefCounted
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Type-check a JSON-decoded param Variant before assigning it into a typed
-## GDScript local. The dispatcher only catches handler crashes as an opaque
-## "malformed result" (issue #210), so a typed assignment like
-## var group: String = params.get("group", "")
-## will runtime-error and bubble up without telling the caller which param
-## was the wrong shape. Only string params are guarded — int/bool params
-## can't be: Godot's JSON parser decodes every number as float (a wire `5`
-## arrives as `5.0`), so a strict int check would reject every legitimate
-## integer a client sends, and GDScript's typed assignment already converts
-## numeric Variants safely. Bool params arrive as real bools and a wrong
-## type surfaces through the dispatcher's malformed-result path.
-
-
-## Returns null iff `value` is a String or StringName. On any other type
-## returns an INVALID_PARAMS error dict whose message names both `name` and
-## the actual Variant type (via Godot's built-in `type_string`).
-static func require_string(name: String, value: Variant) -> Variant:
- var t := typeof(value)
- if t == TYPE_STRING or t == TYPE_STRING_NAME:
- return null
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Param '%s' must be a String, got %s" % [name, type_string(t)],
- )
diff --git a/addons/godot_ai/handlers/_param_validators.gd.uid b/addons/godot_ai/handlers/_param_validators.gd.uid
deleted file mode 100644
index cd2061e..0000000
--- a/addons/godot_ai/handlers/_param_validators.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://difa877m8dsla
diff --git a/addons/godot_ai/handlers/_property_errors.gd b/addons/godot_ai/handlers/_property_errors.gd
deleted file mode 100644
index 316b941..0000000
--- a/addons/godot_ai/handlers/_property_errors.gd
+++ /dev/null
@@ -1,82 +0,0 @@
-@tool
-class_name McpPropertyErrors
-extends RefCounted
-
-## Shared helper for building "Property not found" error messages that include
-## "did you mean" suggestions and a tail of available property names. All
-## handlers that validate user-supplied property names against a target Object
-## (Node, Resource, …) should route through build_message() so agents get
-## consistent, actionable errors on typos.
-##
-## Ranking combines Godot's built-in String.similarity() with a substring
-## bonus so both "radus" → "radius" (edit distance) and "top" → "top_radius"
-## (substring) surface naturally.
-
-const _SIMILARITY_THRESHOLD: float = 0.4
-const _SUBSTRING_BONUS: float = 0.5
-const _MAX_SUGGESTIONS: int = 5
-const _MAX_TAIL: int = 10
-
-
-static func build_message(target: Object, bad_name: String) -> String:
- if target == null:
- return "Property '%s' not found" % bad_name
- var class_label := _class_label(target)
- var available := _available_property_names(target)
- if available.is_empty():
- return "Property '%s' not found on %s" % [bad_name, class_label]
-
- var msg := "Property '%s' not found on %s" % [bad_name, class_label]
- var suggestions := _rank_suggestions(bad_name, available)
- if not suggestions.is_empty():
- msg += ". Did you mean: %s?" % ", ".join(suggestions)
-
- var tail_names := available.slice(0, min(_MAX_TAIL, available.size()))
- msg += " (available: %s" % ", ".join(tail_names)
- if available.size() > tail_names.size():
- msg += ", ..."
- msg += ")"
- return msg
-
-
-## Prefer a scripted class_name if the target has one, else the engine class.
-static func _class_label(target: Object) -> String:
- var scr := target.get_script()
- if scr != null and scr.has_method("get_global_name"):
- var gcn: String = scr.get_global_name()
- if not gcn.is_empty():
- return gcn
- return target.get_class()
-
-
-## Editor-visible properties, alphabetised, with internal/category entries dropped.
-static func _available_property_names(target: Object) -> Array:
- var names: Array = []
- for p in target.get_property_list():
- var usage: int = int(p.get("usage", 0))
- if (usage & PROPERTY_USAGE_EDITOR) == 0:
- continue
- var name: String = p.get("name", "")
- if name.is_empty() or name.begins_with("_"):
- continue
- names.append(name)
- names.sort()
- return names
-
-
-static func _rank_suggestions(bad: String, available: Array) -> Array:
- if bad.is_empty():
- return []
- var bad_lower := bad.to_lower()
- var scored: Array = []
- for n in available:
- var score: float = bad.similarity(n)
- if n.to_lower().find(bad_lower) != -1 or bad_lower.find(n.to_lower()) != -1:
- score += _SUBSTRING_BONUS
- if score >= _SIMILARITY_THRESHOLD:
- scored.append([score, n])
- scored.sort_custom(func(a, b): return a[0] > b[0])
- var result: Array = []
- for i in range(min(_MAX_SUGGESTIONS, scored.size())):
- result.append(scored[i][1])
- return result
diff --git a/addons/godot_ai/handlers/_property_errors.gd.uid b/addons/godot_ai/handlers/_property_errors.gd.uid
deleted file mode 100644
index c29d21c..0000000
--- a/addons/godot_ai/handlers/_property_errors.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c74d560g4l86b
diff --git a/addons/godot_ai/handlers/animation_handler.gd b/addons/godot_ai/handlers/animation_handler.gd
deleted file mode 100644
index 13e6777..0000000
--- a/addons/godot_ai/handlers/animation_handler.gd
+++ /dev/null
@@ -1,825 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Handles AnimationPlayer authoring: creating players, animations, tracks,
-## keyframes, autoplay, and dev-ergonomics playback.
-##
-## Animations live inside an AnimationLibrary attached to an AnimationPlayer
-## node in the scene. They save with the .tscn — no separate resource file
-## needed. Undo callables hold direct Animation references (not paths).
-##
-## Split (issue #342, audit finding #13):
-## - animation_presets.gd → preset_fade / slide / shake / pulse + helpers
-## - animation_values.gd → animation_list / get / validate + shared
-## value coercion / serialization
-## Both submodules hold a WeakRef back to this handler. The handler's
-## preset_* / list / get / validate methods are thin proxies so existing
-## dispatcher registrations and test fixtures don't change.
-
-const AnimationPresets := preload("res://addons/godot_ai/handlers/animation_presets.gd")
-const AnimationValues := preload("res://addons/godot_ai/handlers/animation_values.gd")
-
-var _undo_redo: EditorUndoRedoManager
-var _presets
-var _values
-
-const _LOOP_MODES := {
- "none": Animation.LOOP_NONE,
- "linear": Animation.LOOP_LINEAR,
- "pingpong": Animation.LOOP_PINGPONG,
-}
-
-const _INTERP_MODES := {
- "nearest": Animation.INTERPOLATION_NEAREST,
- "linear": Animation.INTERPOLATION_LINEAR,
- "cubic": Animation.INTERPOLATION_CUBIC,
-}
-
-
-func _init(undo_redo: EditorUndoRedoManager) -> void:
- _undo_redo = undo_redo
- _presets = AnimationPresets.new(self)
- _values = AnimationValues.new(self)
-
-
-# ============================================================================
-# animation_player_create
-# ============================================================================
-
-func create_player(params: Dictionary) -> Dictionary:
- var parent_path: String = params.get("parent_path", "")
- var node_name: String = params.get("name", "AnimationPlayer")
-
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- var parent: Node = scene_root
- if not parent_path.is_empty():
- parent = McpScenePath.resolve(parent_path, scene_root)
- if parent == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, McpScenePath.format_parent_error(parent_path, scene_root))
-
- var player := AnimationPlayer.new()
- if not node_name.is_empty():
- player.name = node_name
-
- # Attach the default library before adding to tree — it persists on redo.
- var library := AnimationLibrary.new()
- player.add_animation_library("", library)
-
- _undo_redo.create_action("MCP: Create AnimationPlayer %s" % player.name)
- _undo_redo.add_do_method(parent, "add_child", player, true)
- _undo_redo.add_do_method(player, "set_owner", scene_root)
- _undo_redo.add_do_reference(player)
- _undo_redo.add_do_reference(library)
- _undo_redo.add_undo_method(parent, "remove_child", player)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": McpScenePath.from_node(player, scene_root),
- "parent_path": McpScenePath.from_node(parent, scene_root),
- "name": String(player.name),
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# animation_create
-# ============================================================================
-
-func create_animation(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var anim_name: String = params.get("name", "")
- var length: float = float(params.get("length", 1.0))
- var loop_mode_str: String = params.get("loop_mode", "none")
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
- if anim_name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: name")
- if length <= 0.0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "length must be > 0 (got %s)" % length)
-
- if not _LOOP_MODES.has(loop_mode_str):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid loop_mode '%s'. Valid: %s" % [loop_mode_str, ", ".join(_LOOP_MODES.keys())])
-
- var resolved := _resolve_player(player_path, true)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
- var library: AnimationLibrary = resolved.library
- var created_player: bool = resolved.get("player_created", false)
- var player_parent: Node = resolved.get("player_parent", null)
- var created_library := false
- if library == null:
- library = AnimationLibrary.new()
- created_library = true
-
- var overwrite: bool = params.get("overwrite", false)
- var old_anim: Animation = null
- if library.has_animation(anim_name):
- if not overwrite:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "Animation '%s' already exists. Pass overwrite=true or delete it first." % anim_name)
- old_anim = library.get_animation(anim_name)
-
- var anim := Animation.new()
- anim.length = length
- anim.loop_mode = _LOOP_MODES[loop_mode_str]
-
- _commit_animation_add("MCP: Create animation %s" % anim_name,
- player, library, created_library, anim_name, anim, old_anim,
- created_player, player_parent)
-
- return {
- "data": {
- "player_path": player_path,
- "name": anim_name,
- "length": length,
- "loop_mode": loop_mode_str,
- "library_created": created_library or created_player,
- "animation_player_created": created_player,
- "overwritten": old_anim != null,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# animation_delete
-# ============================================================================
-
-func delete_animation(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var anim_name: String = params.get("animation_name", "")
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
- if anim_name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: animation_name")
-
- var resolved := _resolve_player(player_path)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
-
- # Use _resolve_animation so we can delete from ANY library, not just the
- # default. Mirrors the read-side symmetry with animation_get / animation_play
- # which already search all libraries via _resolve_animation.
- var anim_resolved := _resolve_animation(player, anim_name)
- if anim_resolved.has("error"):
- return anim_resolved
- var old_anim: Animation = anim_resolved.animation
- var library: AnimationLibrary = anim_resolved.library
- # Clip key within the owning library — strips the "libname/" prefix if the
- # caller passed a qualified name.
- var clip_key: String = anim_name
- var slash := anim_name.find("/")
- if slash >= 0:
- clip_key = anim_name.substr(slash + 1)
-
- _undo_redo.create_action("MCP: Delete animation %s" % anim_name)
- _undo_redo.add_do_method(library, "remove_animation", clip_key)
- _undo_redo.add_undo_method(library, "add_animation", clip_key, old_anim)
- _undo_redo.add_do_reference(old_anim) # prevent GC so undo→redo works
- _undo_redo.commit_action()
-
- return {
- "data": {
- "player_path": player_path,
- "animation_name": anim_name,
- "library_key": anim_resolved.get("library_key", ""),
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# animation_add_property_track
-# ============================================================================
-
-func add_property_track(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var anim_name: String = params.get("animation_name", "")
- var track_path: String = params.get("track_path", "")
- var keyframes = params.get("keyframes", [])
- var interp_str: String = params.get("interpolation", "linear")
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
- if anim_name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: animation_name")
- if track_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM,
- "Missing required param: track_path (format: 'NodeName:property', e.g. 'Panel:modulate')")
- if not track_path.contains(":"):
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "track_path must include ':property' suffix (e.g. 'Panel:modulate', '.:position')")
- if not _INTERP_MODES.has(interp_str):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid interpolation '%s'. Valid: %s" % [interp_str, ", ".join(_INTERP_MODES.keys())])
- if typeof(keyframes) != TYPE_ARRAY or keyframes.is_empty():
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "keyframes must be a non-empty array")
-
- var resolved := _resolve_player(player_path)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
-
- var anim_resolved := _resolve_animation(player, anim_name)
- if anim_resolved.has("error"):
- return anim_resolved
- var anim: Animation = anim_resolved.animation
-
- # Validate + pre-coerce keyframes before mutating. Coercion errors
- # surface as INVALID_PARAMS rather than silently inserting garbage keys.
- # Resolve the target property's type ONCE — dense clips used to re-walk
- # get_property_list() per keyframe.
- var ctx := AnimationValues.resolve_track_prop_context(track_path, player)
- if ctx.has("error"):
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, ctx.error)
- var coerced_keyframes: Array = []
- for kf in keyframes:
- if typeof(kf) != TYPE_DICTIONARY:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Each keyframe must be a dictionary")
- if not "time" in kf:
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Each keyframe must have a 'time' field")
- if not "value" in kf:
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Each keyframe must have a 'value' field")
- var coerce_result := AnimationValues.coerce_with_context(kf.get("value"), ctx)
- if coerce_result.has("error"):
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, coerce_result.error)
- coerced_keyframes.append({
- "time": kf.get("time"),
- "value": coerce_result.ok,
- "transition": kf.get("transition", "linear"),
- })
-
- _create_scene_pinned_action("MCP: Add property track %s to %s" % [track_path, anim_name])
- _undo_redo.add_do_method(self, "_do_add_property_track", anim, track_path, interp_str, coerced_keyframes)
- # Undo locates the track by (path, type) at undo time rather than caching
- # an index captured at do time. Cached indices go stale if any other track
- # mutation lands between do and undo (Godot editor, another MCP call, etc.)
- _undo_redo.add_undo_method(self, "_undo_remove_track_by_path", anim, track_path, Animation.TYPE_VALUE)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "player_path": player_path,
- "animation_name": anim_name,
- "track_path": track_path,
- "interpolation": interp_str,
- "keyframe_count": keyframes.size(),
- "undoable": true,
- }
- }
-
-
-## Insert a pre-coerced track into the animation. Callers must coerce
-## values against the target property before calling this (see
-## AnimationValues.coerce_value_for_track) — this method runs inside the
-## undo do-method path where error propagation isn't possible.
-func _do_add_property_track(
- anim: Animation,
- track_path: String,
- interp_str: String,
- keyframes: Array,
-) -> void:
- var idx := anim.add_track(Animation.TYPE_VALUE)
- anim.track_set_path(idx, NodePath(track_path))
- anim.track_set_interpolation_type(idx, _INTERP_MODES.get(interp_str, Animation.INTERPOLATION_LINEAR))
- for kf in keyframes:
- var t: float = float(kf.get("time", 0.0))
- var trans: float = AnimationValues.parse_transition(kf.get("transition", "linear"))
- anim.track_insert_key(idx, t, kf.get("value"), trans)
-
-
-# ============================================================================
-# animation_add_method_track
-# ============================================================================
-
-func add_method_track(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var anim_name: String = params.get("animation_name", "")
- var target_path: String = params.get("target_node_path", "")
- var keyframes = params.get("keyframes", [])
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
- if anim_name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: animation_name")
- if target_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: target_node_path")
- if target_path.contains(":"):
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "target_node_path is a bare NodePath without ':property' (got '%s'). " % target_path +
- "Method name goes in each keyframe's 'method' field, not the path.")
- if typeof(keyframes) != TYPE_ARRAY or keyframes.is_empty():
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "keyframes must be a non-empty array")
-
- for kf in keyframes:
- if typeof(kf) != TYPE_DICTIONARY:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Each keyframe must be a dictionary")
- if not "time" in kf:
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Each keyframe must have a 'time' field")
- if not "method" in kf:
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Each keyframe must have a 'method' field")
- var method_field = kf.get("method")
- if typeof(method_field) != TYPE_STRING or (method_field as String).is_empty():
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "'method' must be a non-empty string")
- if kf.has("args") and typeof(kf.get("args")) != TYPE_ARRAY:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "'args' must be an array if provided (got %s)" % type_string(typeof(kf.get("args"))))
-
- var resolved := _resolve_player(player_path)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
-
- var anim_resolved := _resolve_animation(player, anim_name)
- if anim_resolved.has("error"):
- return anim_resolved
- var anim: Animation = anim_resolved.animation
-
- _create_scene_pinned_action("MCP: Add method track %s to %s" % [target_path, anim_name])
- _undo_redo.add_do_method(self, "_do_add_method_track", anim, target_path, keyframes)
- # Undo locates the track by (path, type) at undo time — see add_property_track.
- _undo_redo.add_undo_method(self, "_undo_remove_track_by_path", anim, target_path, Animation.TYPE_METHOD)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "player_path": player_path,
- "animation_name": anim_name,
- "target_node_path": target_path,
- "keyframe_count": keyframes.size(),
- "undoable": true,
- }
- }
-
-
-## Remove a track identified by (path, type) at undo time. Robust to
-## history interleaving: if another track was added since the do, the
-## find_track call still resolves to the correct index. Returns silently
-## if the track is no longer present (e.g. a prior undo already removed it).
-func _undo_remove_track_by_path(anim: Animation, track_path: String, track_type: int) -> void:
- var idx := anim.find_track(NodePath(track_path), track_type)
- if idx >= 0:
- anim.remove_track(idx)
-
-
-func _do_add_method_track(anim: Animation, target_path: String, keyframes: Array) -> void:
- var idx := anim.add_track(Animation.TYPE_METHOD)
- anim.track_set_path(idx, NodePath(target_path))
- for kf in keyframes:
- var t: float = float(kf.get("time", 0.0))
- var method_name: String = str(kf.get("method", ""))
- var args: Array = kf.get("args", [])
- anim.track_insert_key(idx, t, {"method": method_name, "args": args})
-
-
-# ============================================================================
-# animation_set_autoplay
-# ============================================================================
-
-func set_autoplay(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var anim_name: String = params.get("animation_name", "")
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
-
- var resolved := _resolve_player(player_path)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
-
- # Allow empty string to clear autoplay; otherwise validate the name exists.
- if not anim_name.is_empty() and not player.has_animation(anim_name):
- return ErrorCodes.make(ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Animation '%s' not found on player at %s" % [anim_name, player_path])
-
- var old_autoplay: String = player.autoplay
-
- _undo_redo.create_action("MCP: Set autoplay %s on %s" % [anim_name, player_path])
- _undo_redo.add_do_property(player, "autoplay", anim_name)
- _undo_redo.add_undo_property(player, "autoplay", old_autoplay)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "player_path": player_path,
- "animation_name": anim_name,
- "previous_autoplay": old_autoplay,
- "cleared": anim_name.is_empty(),
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# animation_play (dev ergonomics — not saved with scene)
-# ============================================================================
-
-func play(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var anim_name: String = params.get("animation_name", "")
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
-
- var resolved := _resolve_player(player_path)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
-
- if not anim_name.is_empty() and not player.has_animation(anim_name):
- return ErrorCodes.make(ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Animation '%s' not found on player at %s" % [anim_name, player_path])
-
- player.play(anim_name)
-
- return {
- "data": {
- "player_path": player_path,
- "animation_name": anim_name,
- "undoable": false,
- "reason": "Runtime playback state — not saved with scene",
- }
- }
-
-
-# ============================================================================
-# animation_stop (dev ergonomics — not saved with scene)
-# ============================================================================
-
-func stop(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
-
- var resolved := _resolve_player(player_path)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
-
- player.stop()
-
- return {
- "data": {
- "player_path": player_path,
- "undoable": false,
- "reason": "Runtime playback state — not saved with scene",
- }
- }
-
-
-# ============================================================================
-# animation_create_simple (composer)
-# ============================================================================
-
-func create_simple(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var anim_name: String = params.get("name", "")
- var tweens = params.get("tweens", [])
- var loop_mode_str: String = params.get("loop_mode", "none")
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
- if anim_name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: name")
- if typeof(tweens) != TYPE_ARRAY or tweens.is_empty():
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "tweens must be a non-empty array")
- if not _LOOP_MODES.has(loop_mode_str):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid loop_mode '%s'. Valid: %s" % [loop_mode_str, ", ".join(_LOOP_MODES.keys())])
-
- # Validate all tween specs before touching the scene.
- var seen_paths := {}
- for spec in tweens:
- if typeof(spec) != TYPE_DICTIONARY:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Each tween spec must be a dictionary")
- for field in ["target", "property", "from", "to", "duration"]:
- if not field in spec:
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM,
- "Each tween spec must have '%s'" % field)
- if float(spec.get("duration", 0.0)) <= 0.0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "tween 'duration' must be > 0")
- var dup_key: String = str(spec.target) + ":" + str(spec.property)
- if seen_paths.has(dup_key):
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "Duplicate tween target '%s' — merge keyframes into a single track " % dup_key +
- "via animation_add_property_track instead of two separate tweens.")
- seen_paths[dup_key] = true
-
- # Compute/validate length before resolving the player — a fresh auto-created
- # AnimationPlayer is a detached Node that leaks if we return after creation.
- var has_length: bool = params.has("length") and params.get("length") != null
- var computed_length: float = 0.0
- if has_length:
- computed_length = float(params.get("length"))
- if computed_length <= 0.0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "'length' must be > 0 when provided (got %s)" % str(params.get("length")))
- else:
- for spec in tweens:
- var end_time: float = float(spec.get("delay", 0.0)) + float(spec.get("duration", 0.0))
- if end_time > computed_length:
- computed_length = end_time
- if computed_length <= 0.0:
- computed_length = 1.0
-
- var resolved := _resolve_player(player_path, true)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
- var library: AnimationLibrary = resolved.library
- var created_player: bool = resolved.get("player_created", false)
- var player_parent: Node = resolved.get("player_parent", null)
- var created_library := false
- if library == null:
- library = AnimationLibrary.new()
- created_library = true
-
- var overwrite: bool = params.get("overwrite", false)
- var old_anim: Animation = null
- if library.has_animation(anim_name):
- if not overwrite:
- if created_player:
- player.queue_free()
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "Animation '%s' already exists. Pass overwrite=true or delete it first." % anim_name)
- old_anim = library.get_animation(anim_name)
-
- # Pre-coerce all tween values before touching the anim — coercion errors
- # surface as INVALID_PARAMS, not silent garbage keyframes.
- # When the player was auto-created, it isn't in the tree yet — pass its
- # future parent so the coercer can still resolve target property types.
- var coerce_root: Node = player_parent if created_player else null
- var per_track_keyframes: Array = []
- for spec in tweens:
- var target: String = str(spec.get("target", ""))
- var property: String = str(spec.get("property", ""))
- var track_path: String = target + ":" + property
- var duration: float = float(spec.get("duration", 1.0))
- var delay: float = float(spec.get("delay", 0.0))
- var trans_str = spec.get("transition", "linear")
- var from_result := AnimationValues.coerce_value_for_track(spec.get("from"), track_path, player, coerce_root)
- if from_result.has("error"):
- if created_player:
- player.queue_free()
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "tween '%s': %s" % [track_path, from_result.error])
- var to_result := AnimationValues.coerce_value_for_track(spec.get("to"), track_path, player, coerce_root)
- if to_result.has("error"):
- if created_player:
- player.queue_free()
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "tween '%s': %s" % [track_path, to_result.error])
- per_track_keyframes.append({
- "track_path": track_path,
- "keyframes": [
- {"time": delay, "value": from_result.ok, "transition": trans_str},
- {"time": delay + duration, "value": to_result.ok, "transition": trans_str},
- ],
- })
-
- # Build the animation fully in memory before touching the undo stack.
- var anim := Animation.new()
- anim.length = computed_length
- anim.loop_mode = _LOOP_MODES[loop_mode_str]
-
- for entry in per_track_keyframes:
- _do_add_property_track(anim, entry.track_path, "linear", entry.keyframes)
-
- # One atomic undo action — bundles player creation (if any), library
- # creation (if any), and the animation add. A single Ctrl-Z rolls back all.
- _commit_animation_add("MCP: Create animation %s (%d tracks)" % [anim_name, anim.get_track_count()],
- player, library, created_library, anim_name, anim, old_anim,
- created_player, player_parent)
-
- return {
- "data": {
- "player_path": player_path,
- "name": anim_name,
- "length": computed_length,
- "loop_mode": loop_mode_str,
- "track_count": anim.get_track_count(),
- "library_created": created_library or created_player,
- "animation_player_created": created_player,
- "overwritten": old_anim != null,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# Proxies — preset_* and read methods live in the submodules. Kept here so
-# the dispatcher registrations and `_handler.method(...)` test fixtures stay
-# unchanged across the split.
-# ============================================================================
-
-func preset_fade(params: Dictionary) -> Dictionary:
- return _presets.preset_fade(params)
-
-
-func preset_slide(params: Dictionary) -> Dictionary:
- return _presets.preset_slide(params)
-
-
-func preset_shake(params: Dictionary) -> Dictionary:
- return _presets.preset_shake(params)
-
-
-func preset_pulse(params: Dictionary) -> Dictionary:
- return _presets.preset_pulse(params)
-
-
-func list_animations(params: Dictionary) -> Dictionary:
- return _values.list_animations(params)
-
-
-func get_animation(params: Dictionary) -> Dictionary:
- return _values.get_animation(params)
-
-
-func validate_animation(params: Dictionary) -> Dictionary:
- return _values.validate_animation(params)
-
-
-# ============================================================================
-# Helpers — undo
-# ============================================================================
-
-## Shared undo setup for create_animation and create_simple. Handles fresh-
-## create, overwrite, library auto-create, and player auto-create in a single
-## atomic action. When `created_player` is true, the player already has the
-## library attached (eagerly, from `_instantiate_player`) and the library
-## doesn't need its own undo bookkeeping — it rides along with the add_child.
-func _commit_animation_add(
- action_label: String,
- player: AnimationPlayer,
- library: AnimationLibrary,
- created_library: bool,
- anim_name: String,
- anim: Animation,
- old_anim: Animation, ## null when not overwriting
- created_player: bool = false,
- player_parent: Node = null,
-) -> void:
- _undo_redo.create_action(action_label)
- if created_player:
- var scene_root := EditorInterface.get_edited_scene_root()
- _undo_redo.add_do_method(player_parent, "add_child", player, true)
- _undo_redo.add_do_method(player, "set_owner", scene_root)
- _undo_redo.add_do_reference(player)
- _undo_redo.add_do_reference(library)
- _undo_redo.add_undo_method(player_parent, "remove_child", player)
- elif created_library:
- _undo_redo.add_do_method(player, "add_animation_library", "", library)
- _undo_redo.add_undo_method(player, "remove_animation_library", "")
- _undo_redo.add_do_reference(library)
- if old_anim != null:
- _undo_redo.add_do_method(library, "remove_animation", anim_name)
- _undo_redo.add_do_method(library, "add_animation", anim_name, anim)
- if old_anim != null:
- _undo_redo.add_undo_method(library, "remove_animation", anim_name)
- _undo_redo.add_undo_method(library, "add_animation", anim_name, old_anim)
- _undo_redo.add_do_reference(old_anim)
- else:
- _undo_redo.add_undo_method(library, "remove_animation", anim_name)
- _undo_redo.add_do_reference(anim)
- _undo_redo.commit_action()
-
-
-## Open a `create_action` pinned to the edited scene's history.
-##
-## Without an explicit context, `add_do_method(self, ...)` against a
-## RefCounted handler lands in GLOBAL_HISTORY while sibling actions whose
-## first do-target is a Resource (e.g. AnimationLibrary) land in the scene's
-## history. Mismatched histories make the test-side `editor_undo` helper
-## (walks scene first) undo the wrong action, and break batch_handler's
-## rollback. Mirrors `camera_handler.gd`'s identical pinning rationale.
-func _create_scene_pinned_action(action_label: String) -> void:
- _undo_redo.create_action(
- action_label, UndoRedo.MERGE_DISABLE, EditorInterface.get_edited_scene_root(),
- )
-
-
-# ============================================================================
-# Helpers — resolution
-# ============================================================================
-
-## Resolve an AnimationPlayer and its default library for write operations.
-## Returns {player, library, player_created, player_parent} on success, or an
-## error dict. library is null if the player exists but has no default library
-## yet — callers bundle an `add_animation_library` step into their undo action.
-##
-## When `create_if_missing` is true and `player_path` resolves to nothing, a
-## fresh AnimationPlayer is instantiated (with an empty default library attached
-## eagerly) but is NOT added to the scene tree — callers must bundle the
-## add_child step into their undo action via `_commit_animation_add`.
-## If the resolved node exists but isn't an AnimationPlayer, that's still an
-## error — we don't clobber an existing node of a different type.
-func _resolve_player(player_path: String, create_if_missing: bool = false) -> Dictionary:
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
- var node := McpScenePath.resolve(player_path, scene_root)
- if node == null:
- if not create_if_missing:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(player_path, scene_root))
- return _instantiate_player(player_path, scene_root)
- if not node is AnimationPlayer:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
- "Node at %s is not an AnimationPlayer (got %s)" % [player_path, node.get_class()])
- var player := node as AnimationPlayer
- var lib: AnimationLibrary = null
- if player.has_animation_library(""):
- lib = player.get_animation_library("")
- return {"player": player, "library": lib, "player_created": false, "player_parent": null}
-
-
-## Build a new AnimationPlayer (with empty default library) for insertion under
-## the parent implied by `player_path`. Returns an error dict if the parent
-## can't be resolved or the path has no usable leaf name.
-func _instantiate_player(player_path: String, scene_root: Node) -> Dictionary:
- var slash := player_path.rfind("/")
- var parent_path: String
- var player_name: String
- if slash < 0:
- parent_path = ""
- player_name = player_path
- else:
- parent_path = player_path.substr(0, slash)
- player_name = player_path.substr(slash + 1)
- if player_name.is_empty():
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "Cannot auto-create AnimationPlayer: player_path '%s' has no leaf name" % player_path)
- var parent: Node
- if parent_path.is_empty():
- parent = scene_root
- else:
- parent = McpScenePath.resolve(parent_path, scene_root)
- if parent == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND,
- "Cannot auto-create AnimationPlayer at %s: %s" % [
- player_path, McpScenePath.format_parent_error(parent_path, scene_root)])
- var new_player := AnimationPlayer.new()
- new_player.name = player_name
- var lib := AnimationLibrary.new()
- new_player.add_animation_library("", lib)
- return {
- "player": new_player,
- "library": lib,
- "player_created": true,
- "player_parent": parent,
- }
-
-
-## Resolve for read operations (no library requirement).
-func _resolve_player_read(player_path: String) -> Dictionary:
- var resolved := McpNodeValidator.resolve_or_error(player_path, "player_path")
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- if not node is AnimationPlayer:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
- "Node at %s is not an AnimationPlayer (got %s)" % [player_path, node.get_class()])
- return {"player": node as AnimationPlayer}
-
-
-## Resolve an animation by name, searching all libraries.
-## Accepts bare clip names ("idle") and library-qualified names ("moves/idle")
-## as returned by `list_animations` for non-default libraries.
-func _resolve_animation(player: AnimationPlayer, anim_name: String) -> Dictionary:
- if not player.has_animation(anim_name):
- return ErrorCodes.make(ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Animation '%s' not found on player. Available: %s" % [
- anim_name,
- ", ".join(Array(player.get_animation_list()))
- ])
- # If the caller passed "library/clip", look up in that specific library.
- var slash := anim_name.find("/")
- if slash >= 0:
- var lib_key := anim_name.substr(0, slash)
- var clip_key := anim_name.substr(slash + 1)
- if player.has_animation_library(lib_key):
- var lib: AnimationLibrary = player.get_animation_library(lib_key)
- if lib.has_animation(clip_key):
- return {"animation": lib.get_animation(clip_key), "library": lib, "library_key": lib_key}
- # Otherwise scan libraries for a bare clip name.
- for lib_name in player.get_animation_library_list():
- var lib2: AnimationLibrary = player.get_animation_library(lib_name)
- if lib2.has_animation(anim_name):
- return {"animation": lib2.get_animation(anim_name), "library": lib2, "library_key": lib_name}
- # Fallback — shouldn't happen if has_animation returned true.
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Animation found by player but not in any library")
diff --git a/addons/godot_ai/handlers/animation_handler.gd.uid b/addons/godot_ai/handlers/animation_handler.gd.uid
deleted file mode 100644
index 934f665..0000000
--- a/addons/godot_ai/handlers/animation_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c0jrius46xsd4
diff --git a/addons/godot_ai/handlers/animation_presets.gd b/addons/godot_ai/handlers/animation_presets.gd
deleted file mode 100644
index 40f1ddc..0000000
--- a/addons/godot_ai/handlers/animation_presets.gd
+++ /dev/null
@@ -1,536 +0,0 @@
-@tool
-extends RefCounted
-
-## Curated motion presets for the AnimationPlayer surface.
-##
-## Each preset_* method:
-## 1. Validates params + resolves the player (auto-creating its default lib).
-## 2. Resolves the target node + classifies it as control / 2d / 3d.
-## 3. Builds a single-track Animation with shape-appropriate keyframes.
-## 4. Commits the add through the handler's shared `_commit_animation_add`
-## so a single Ctrl-Z rolls back any auto-created library + the animation.
-##
-## Holds a WeakRef back to the AnimationHandler instance so the handler can
-## continue to own this module strongly via `_presets` without forming a
-## RefCounted cycle. Resolution / undo helpers live on the handler — keeping
-## the `_undo_redo` member single-source there avoids drift.
-
-
-const AnimationValues := preload("res://addons/godot_ai/handlers/animation_values.gd")
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-const ScenePath := preload("res://addons/godot_ai/utils/scene_path.gd")
-
-
-var _handler_weak: WeakRef
-
-
-func _init(handler) -> void:
- _handler_weak = weakref(handler)
-
-
-func _h():
- return _handler_weak.get_ref()
-
-
-# ============================================================================
-# animation_preset_fade
-# ============================================================================
-
-func preset_fade(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var target_path: String = params.get("target_path", "")
- var mode: String = params.get("mode", "in")
- var duration: float = float(params.get("duration", 0.5))
- var anim_name: String = params.get("animation_name", "")
- var overwrite: bool = params.get("overwrite", false)
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
- if target_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: target_path")
- if mode != "in" and mode != "out":
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid mode '%s'. Valid: 'in', 'out'" % mode)
- if duration <= 0.0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "'duration' must be > 0")
-
- var handler = _h()
- if handler == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_UNAVAILABLE,
- "AnimationHandler not available", false)
- var resolved: Dictionary = handler._resolve_player(player_path)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
- var library: AnimationLibrary = resolved.library
- var created_library := false
- if library == null:
- library = AnimationLibrary.new()
- created_library = true
-
- var target_resolved := _resolve_preset_target(player, target_path)
- if target_resolved.has("error"):
- return target_resolved
- var target: Node = target_resolved.node
- var track_target: String = target_resolved.track_path_root
-
- # Fade requires a `modulate` property (CanvasItem/Control/Node2D/Sprite3D/etc).
- var has_modulate := false
- for p in target.get_property_list():
- if p.name == "modulate":
- has_modulate = true
- break
- if not has_modulate:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
- "Target '%s' (class %s) has no 'modulate' property — fade requires a CanvasItem, Control, Node2D, or Sprite3D"
- % [target_path, target.get_class()])
-
- if anim_name.is_empty():
- anim_name = "fade_%s" % mode
-
- var old_anim: Animation = null
- if library.has_animation(anim_name):
- if not overwrite:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "Animation '%s' already exists. Pass overwrite=true or delete it first." % anim_name)
- old_anim = library.get_animation(anim_name)
-
- var start_a: float = 0.0 if mode == "in" else 1.0
- var end_a: float = 1.0 if mode == "in" else 0.0
-
- var anim := Animation.new()
- anim.length = duration
- anim.loop_mode = Animation.LOOP_NONE
-
- var track_path := "%s:modulate:a" % track_target
- handler._do_add_property_track(anim, track_path, "linear", [
- {"time": 0.0, "value": start_a, "transition": "linear"},
- {"time": duration, "value": end_a, "transition": "linear"},
- ])
-
- handler._commit_animation_add(
- "MCP: Create animation %s" % anim_name,
- player, library, created_library, anim_name, anim, old_anim,
- )
-
- return {
- "data": {
- "player_path": player_path,
- "animation_name": anim_name,
- "mode": mode,
- "length": duration,
- "track_count": anim.get_track_count(),
- "library_created": created_library,
- "overwritten": old_anim != null,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# animation_preset_slide
-# ============================================================================
-
-func preset_slide(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var target_path: String = params.get("target_path", "")
- var direction: String = params.get("direction", "left")
- var mode: String = params.get("mode", "in")
- var duration: float = float(params.get("duration", 0.4))
- var anim_name: String = params.get("animation_name", "")
- var overwrite: bool = params.get("overwrite", false)
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
- if target_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: target_path")
- if not ["left", "right", "up", "down"].has(direction):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid direction '%s'. Valid: 'left', 'right', 'up', 'down'" % direction)
- if mode != "in" and mode != "out":
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid mode '%s'. Valid: 'in', 'out'" % mode)
- if duration <= 0.0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "'duration' must be > 0")
-
- var handler = _h()
- if handler == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_UNAVAILABLE,
- "AnimationHandler not available", false)
- var resolved: Dictionary = handler._resolve_player(player_path)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
- var library: AnimationLibrary = resolved.library
- var created_library := false
- if library == null:
- library = AnimationLibrary.new()
- created_library = true
-
- var target_resolved := _resolve_preset_target(player, target_path)
- if target_resolved.has("error"):
- return target_resolved
- var target = target_resolved.node
- var kind: String = target_resolved.kind
- var track_target: String = target_resolved.track_path_root
-
- # Default distance picks 3D units vs screen pixels based on target kind.
- var default_distance: float = 1.0 if kind == "3d" else 100.0
- var distance: float = float(params.get("distance", default_distance))
- if distance == 0.0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "'distance' must be non-zero")
-
- var offset: Variant = _direction_offset(kind, direction, distance)
- var current_pos: Variant = target.position
- var start_pos: Variant
- var end_pos: Variant
- if mode == "in":
- start_pos = current_pos + offset
- end_pos = current_pos
- else:
- start_pos = current_pos
- end_pos = current_pos + offset
-
- if anim_name.is_empty():
- anim_name = "slide_%s_%s" % [mode, direction]
-
- var old_anim: Animation = null
- if library.has_animation(anim_name):
- if not overwrite:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "Animation '%s' already exists. Pass overwrite=true or delete it first." % anim_name)
- old_anim = library.get_animation(anim_name)
-
- var anim := Animation.new()
- anim.length = duration
- anim.loop_mode = Animation.LOOP_NONE
-
- var track_path := "%s:position" % track_target
- handler._do_add_property_track(anim, track_path, "linear", [
- {"time": 0.0, "value": start_pos, "transition": "linear"},
- {"time": duration, "value": end_pos, "transition": "linear"},
- ])
-
- handler._commit_animation_add(
- "MCP: Create animation %s" % anim_name,
- player, library, created_library, anim_name, anim, old_anim,
- )
-
- return {
- "data": {
- "player_path": player_path,
- "animation_name": anim_name,
- "direction": direction,
- "mode": mode,
- "distance": distance,
- "length": duration,
- "track_count": anim.get_track_count(),
- "library_created": created_library,
- "overwritten": old_anim != null,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# animation_preset_shake
-# ============================================================================
-
-func preset_shake(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var target_path: String = params.get("target_path", "")
- var duration: float = float(params.get("duration", 0.3))
- var frequency: float = float(params.get("frequency", 30.0))
- var rng_seed: int = int(params.get("seed", 0))
- var anim_name: String = params.get("animation_name", "")
- var overwrite: bool = params.get("overwrite", false)
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
- if target_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: target_path")
- if duration <= 0.0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "'duration' must be > 0")
- if frequency <= 0.0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "'frequency' must be > 0")
-
- var handler = _h()
- if handler == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_UNAVAILABLE,
- "AnimationHandler not available", false)
- var resolved: Dictionary = handler._resolve_player(player_path)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
- var library: AnimationLibrary = resolved.library
- var created_library := false
- if library == null:
- library = AnimationLibrary.new()
- created_library = true
-
- var target_resolved := _resolve_preset_target(player, target_path)
- if target_resolved.has("error"):
- return target_resolved
- var target = target_resolved.node
- var kind: String = target_resolved.kind
- var track_target: String = target_resolved.track_path_root
-
- var default_intensity: float = 0.1 if kind == "3d" else 10.0
- var intensity: float = float(params.get("intensity", default_intensity))
- if intensity <= 0.0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "'intensity' must be > 0")
-
- if anim_name.is_empty():
- anim_name = "shake"
-
- var old_anim: Animation = null
- if library.has_animation(anim_name):
- if not overwrite:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "Animation '%s' already exists. Pass overwrite=true or delete it first." % anim_name)
- old_anim = library.get_animation(anim_name)
-
- var rng := RandomNumberGenerator.new()
- if rng_seed != 0:
- rng.seed = rng_seed
- else:
- rng.randomize()
-
- # Samples between t=0 and t=duration (exclusive); bookended by at-rest keys.
- var sample_count: int = int(ceil(frequency * duration))
- if sample_count < 2:
- sample_count = 2
-
- var current_pos: Variant = target.position
- var kfs: Array = []
- kfs.append({"time": 0.0, "value": current_pos, "transition": "linear"})
- for i in range(1, sample_count):
- var t: float = (float(i) / float(sample_count)) * duration
- var jx: float = rng.randf_range(-intensity, intensity)
- var jy: float = rng.randf_range(-intensity, intensity)
- var jittered: Variant
- if kind == "3d":
- var jz: float = rng.randf_range(-intensity, intensity)
- jittered = current_pos + Vector3(jx, jy, jz)
- else:
- jittered = current_pos + Vector2(jx, jy)
- kfs.append({"time": t, "value": jittered, "transition": "linear"})
- kfs.append({"time": duration, "value": current_pos, "transition": "linear"})
-
- var anim := Animation.new()
- anim.length = duration
- anim.loop_mode = Animation.LOOP_NONE
-
- var track_path := "%s:position" % track_target
- handler._do_add_property_track(anim, track_path, "linear", kfs)
-
- handler._commit_animation_add(
- "MCP: Create animation %s" % anim_name,
- player, library, created_library, anim_name, anim, old_anim,
- )
-
- return {
- "data": {
- "player_path": player_path,
- "animation_name": anim_name,
- "length": duration,
- "frequency": frequency,
- "intensity": intensity,
- "keyframe_count": kfs.size(),
- "track_count": anim.get_track_count(),
- "library_created": created_library,
- "overwritten": old_anim != null,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# animation_preset_pulse
-# ============================================================================
-
-func preset_pulse(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var target_path: String = params.get("target_path", "")
- var from_scale: float = float(params.get("from_scale", 1.0))
- var to_scale: float = float(params.get("to_scale", 1.1))
- var duration: float = float(params.get("duration", 0.4))
- var anim_name: String = params.get("animation_name", "")
- var overwrite: bool = params.get("overwrite", false)
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
- if target_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: target_path")
- if duration <= 0.0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "'duration' must be > 0")
- if from_scale <= 0.0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "'from_scale' must be > 0")
- if to_scale <= 0.0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "'to_scale' must be > 0")
-
- var handler = _h()
- if handler == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_UNAVAILABLE,
- "AnimationHandler not available", false)
- var resolved: Dictionary = handler._resolve_player(player_path)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
- var library: AnimationLibrary = resolved.library
- var created_library := false
- if library == null:
- library = AnimationLibrary.new()
- created_library = true
-
- var target_resolved := _resolve_preset_target(player, target_path)
- if target_resolved.has("error"):
- return target_resolved
- var kind: String = target_resolved.kind
- var track_target: String = target_resolved.track_path_root
-
- if anim_name.is_empty():
- anim_name = "pulse"
-
- var old_anim: Animation = null
- if library.has_animation(anim_name):
- if not overwrite:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "Animation '%s' already exists. Pass overwrite=true or delete it first." % anim_name)
- old_anim = library.get_animation(anim_name)
-
- var from_vec: Variant
- var to_vec: Variant
- if kind == "3d":
- from_vec = Vector3(from_scale, from_scale, from_scale)
- to_vec = Vector3(to_scale, to_scale, to_scale)
- else:
- from_vec = Vector2(from_scale, from_scale)
- to_vec = Vector2(to_scale, to_scale)
-
- var anim := Animation.new()
- anim.length = duration
- anim.loop_mode = Animation.LOOP_NONE
-
- var track_path := "%s:scale" % track_target
- handler._do_add_property_track(anim, track_path, "linear", [
- {"time": 0.0, "value": from_vec, "transition": "linear"},
- {"time": duration * 0.5, "value": to_vec, "transition": "linear"},
- {"time": duration, "value": from_vec, "transition": "linear"},
- ])
-
- handler._commit_animation_add(
- "MCP: Create animation %s" % anim_name,
- player, library, created_library, anim_name, anim, old_anim,
- )
-
- return {
- "data": {
- "player_path": player_path,
- "animation_name": anim_name,
- "from_scale": from_scale,
- "to_scale": to_scale,
- "length": duration,
- "track_count": anim.get_track_count(),
- "library_created": created_library,
- "overwritten": old_anim != null,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# Helpers — preset resolution
-# ============================================================================
-
-## Resolve a preset target node and classify its transform kind.
-##
-## Accepts two `target_path` shapes:
-## * Scene-absolute (starts with "/") — resolved through `ScenePath.resolve`,
-## matching the convention used by every other scene-mutating tool. Targets
-## outside the player's `root_node` subtree are converted to `..`-prefixed
-## paths via `root_node.get_path_to(target)`, mirroring what the relative
-## form accepts and how Godot stores track paths.
-## * Relative — used as-is against the player's `root_node`, matching how
-## animation tracks themselves are stored.
-##
-## Returns `{node, kind, track_path_root}` where `track_path_root` is the path
-## (relative to `root_node`) that callers should embed in the track path. For
-## scene-absolute inputs this is the converted relative path; for relative
-## inputs it equals the input. `kind` ∈ {"control", "2d", "3d"}.
-##
-## Mirrors the same root-node fallback that
-## `AnimationValues.resolve_track_prop_context` uses so tool inputs match how
-## the track path will resolve at playback.
-func _resolve_preset_target(player: AnimationPlayer, target_path: String) -> Dictionary:
- var root_node := AnimationValues.player_root_node(player)
- if root_node == null:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "AnimationPlayer at %s has no resolvable root_node (is the scene open?)" % str(player.get_path()))
-
- var target: Node = null
- var track_path_root: String = target_path
- if target_path.begins_with("/"):
- var scene_root := EditorInterface.get_edited_scene_root()
- if scene_root == null:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "Cannot resolve scene-absolute target_path '%s': no scene open" % target_path)
- target = ScenePath.resolve(target_path, scene_root)
- if target == null:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- ScenePath.format_node_error(target_path, scene_root))
- # Convert to a root_node-relative path. For targets outside the
- # subtree this yields a `..`-prefixed path, matching what the
- # relative form already accepts (root_node.get_node_or_null
- # resolves `..` segments) and what Godot's animation engine
- # stores natively.
- track_path_root = str(root_node.get_path_to(target))
- else:
- target = root_node.get_node_or_null(target_path)
- if target == null:
- # root_node.get_path() leaks the editor's SubViewport-wrapped
- # path; use the clean scene-relative form so the hint is
- # actionable.
- var scene_root := EditorInterface.get_edited_scene_root()
- var root_hint := ScenePath.from_node(root_node, scene_root) if scene_root != null else str(root_node.name)
- var abs_example := "/%s/path/to/target" % scene_root.name if scene_root != null else "/SceneRoot/path/to/target"
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- ("Target node not found at '%s' (resolved relative to AnimationPlayer's root_node '%s'). "
- + "Pass a path relative to root_node (e.g. \"path/to/target\") or a scene-absolute path (e.g. \"%s\").")
- % [target_path, root_hint, abs_example])
-
- var kind: String
- if target is Control:
- kind = "control"
- elif target is Node2D:
- kind = "2d"
- elif target is Node3D:
- kind = "3d"
- else:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
- "Target '%s' must be a Control, Node2D, or Node3D (got %s)" % [target_path, target.get_class()])
- return {"node": target, "kind": kind, "track_path_root": track_path_root}
-
-
-## Build a directional offset for slide presets.
-## Axis conventions:
-## Control + Node2D (screen-space, y-down): left/right = ∓x, up = -y, down = +y
-## Node3D (world-up): left/right = ∓x, up = +y, down = -y
-static func _direction_offset(kind: String, direction: String, distance: float) -> Variant:
- if kind == "3d":
- match direction:
- "left": return Vector3(-distance, 0.0, 0.0)
- "right": return Vector3(distance, 0.0, 0.0)
- "up": return Vector3(0.0, distance, 0.0)
- "down": return Vector3(0.0, -distance, 0.0)
- else:
- match direction:
- "left": return Vector2(-distance, 0.0)
- "right": return Vector2(distance, 0.0)
- "up": return Vector2(0.0, -distance)
- "down": return Vector2(0.0, distance)
- return null
diff --git a/addons/godot_ai/handlers/animation_presets.gd.uid b/addons/godot_ai/handlers/animation_presets.gd.uid
deleted file mode 100644
index f463501..0000000
--- a/addons/godot_ai/handlers/animation_presets.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c4s3h78bwvr6w
diff --git a/addons/godot_ai/handlers/animation_values.gd b/addons/godot_ai/handlers/animation_values.gd
deleted file mode 100644
index ab6a1e9..0000000
--- a/addons/godot_ai/handlers/animation_values.gd
+++ /dev/null
@@ -1,446 +0,0 @@
-@tool
-extends RefCounted
-
-const VariantSerializer := preload("res://addons/godot_ai/utils/variant_serializer.gd")
-
-## Read-only animation introspection + shared value-coercion / serialization.
-##
-## Holds:
-## - Static helpers used by both the write handler (track building, simple
-## composer) and the preset module (target/property resolution).
-## - Instance methods that back the read MCP ops: animation_list,
-## animation_get, animation_validate.
-##
-## The instance methods need the handler to resolve players / animations.
-## To keep that without introducing a RefCounted cycle (the handler holds a
-## strong ref to this module via `_values`), the back-pointer is a WeakRef.
-## When the handler is freed during plugin teardown, _h() returns null and
-## the (no-longer-routable) calls short-circuit to a generic editor-not-ready
-## error — matches the dispatcher already being torn down at that point.
-
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-const PropertyErrors := preload("res://addons/godot_ai/handlers/_property_errors.gd")
-
-
-const _NAMED_TRANSITIONS := {
- "linear": 1.0,
- "ease_in": 2.0,
- "ease_out": 0.5,
- "ease_in_out": -2.0,
-}
-
-## Component letters accepted on each aggregate base type, paired with the
-## scalar Variant type the component resolves to. A subpath like `position:y`
-## on a Vector3 maps to TYPE_FLOAT; on a Vector3i it maps to TYPE_INT.
-const _SUBPATH_COMPONENTS := {
- TYPE_VECTOR2: ["xy", TYPE_FLOAT],
- TYPE_VECTOR3: ["xyz", TYPE_FLOAT],
- TYPE_VECTOR4: ["xyzw", TYPE_FLOAT],
- TYPE_QUATERNION: ["xyzw", TYPE_FLOAT],
- TYPE_COLOR: ["rgba", TYPE_FLOAT],
- TYPE_VECTOR2I: ["xy", TYPE_INT],
- TYPE_VECTOR3I: ["xyz", TYPE_INT],
- TYPE_VECTOR4I: ["xyzw", TYPE_INT],
-}
-
-
-var _handler_weak: WeakRef
-
-
-func _init(handler) -> void:
- _handler_weak = weakref(handler)
-
-
-func _h():
- return _handler_weak.get_ref()
-
-
-# ============================================================================
-# animation_list (read)
-# ============================================================================
-
-func list_animations(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
-
- var handler = _h()
- if handler == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_UNAVAILABLE,
- "AnimationHandler not available", false)
- var resolved: Dictionary = handler._resolve_player_read(player_path)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
-
- var animations: Array[Dictionary] = []
- for lib_name in player.get_animation_library_list():
- var lib: AnimationLibrary = player.get_animation_library(lib_name)
- for anim_name in lib.get_animation_list():
- var anim: Animation = lib.get_animation(anim_name)
- var display_name: String = anim_name if lib_name == "" else "%s/%s" % [lib_name, anim_name]
- animations.append({
- "name": display_name,
- "length": anim.length,
- "loop_mode": loop_mode_to_string(anim.loop_mode),
- "track_count": anim.get_track_count(),
- })
-
- return {
- "data": {
- "player_path": player_path,
- "animations": animations,
- "count": animations.size(),
- }
- }
-
-
-# ============================================================================
-# animation_get (read)
-# ============================================================================
-
-func get_animation(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var anim_name: String = params.get("animation_name", "")
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
- if anim_name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: animation_name")
-
- var handler = _h()
- if handler == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_UNAVAILABLE,
- "AnimationHandler not available", false)
- var resolved: Dictionary = handler._resolve_player_read(player_path)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
-
- var anim_resolved: Dictionary = handler._resolve_animation(player, anim_name)
- if anim_resolved.has("error"):
- return anim_resolved
- var anim: Animation = anim_resolved.animation
-
- var tracks: Array[Dictionary] = []
- for i in anim.get_track_count():
- var track_type := anim.track_get_type(i)
- var type_name := track_type_to_string(track_type)
- var keys: Array[Dictionary] = []
- for k in anim.track_get_key_count(i):
- var key_val = anim.track_get_key_value(i, k)
- keys.append({
- "time": anim.track_get_key_time(i, k),
- "value": serialize_value(key_val),
- "transition": anim.track_get_key_transition(i, k),
- })
- tracks.append({
- "index": i,
- "type": type_name,
- "path": str(anim.track_get_path(i)),
- "interpolation": interp_to_string(anim.track_get_interpolation_type(i)),
- "key_count": keys.size(),
- "keys": keys,
- })
-
- return {
- "data": {
- "player_path": player_path,
- "name": anim_name,
- "length": anim.length,
- "loop_mode": loop_mode_to_string(anim.loop_mode),
- "track_count": anim.get_track_count(),
- "tracks": tracks,
- }
- }
-
-
-# ============================================================================
-# animation_validate (read-only)
-# ============================================================================
-
-func validate_animation(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var anim_name: String = params.get("animation_name", "")
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
- if anim_name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: animation_name")
-
- var handler = _h()
- if handler == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_UNAVAILABLE,
- "AnimationHandler not available", false)
- var resolved: Dictionary = handler._resolve_player_read(player_path)
- if resolved.has("error"):
- return resolved
- var player: AnimationPlayer = resolved.player
-
- if not player.has_animation(anim_name):
- return ErrorCodes.make(ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Animation '%s' not found on player at %s" % [anim_name, player_path])
-
- var anim: Animation = player.get_animation(anim_name)
-
- var root_node := player_root_node(player)
-
- var broken_tracks: Array[Dictionary] = []
- var valid_count := 0
-
- for i in anim.get_track_count():
- var track_path_str := str(anim.track_get_path(i))
- # Split on the FIRST colon (node↔property boundary), not the last.
- # Godot's get_node_or_null strips the ":property" tail natively, so
- # the valid/broken classification is the same either way — but for
- # BROKEN tracks the broken_tracks[].node_path field is what callers
- # read to diagnose the missing node, and rfind would surface
- # "MissingTarget:modulate" instead of "MissingTarget" for subpath
- # tracks like the "Target:modulate:a" shape preset_fade emits.
- var colon := track_path_str.find(":")
- var node_part: String
- if colon >= 0:
- node_part = track_path_str.substr(0, colon)
- else:
- node_part = track_path_str
-
- var target_node: Node = null
- if root_node != null:
- target_node = root_node.get_node_or_null(node_part)
-
- if target_node == null:
- broken_tracks.append({
- "index": i,
- "path": track_path_str,
- "type": track_type_to_string(anim.track_get_type(i)),
- "issue": "node_not_found",
- "node_path": node_part,
- })
- else:
- valid_count += 1
-
- return {
- "data": {
- "player_path": player_path,
- "animation_name": anim_name,
- "track_count": anim.get_track_count(),
- "valid_count": valid_count,
- "broken_count": broken_tracks.size(),
- "broken_tracks": broken_tracks,
- "valid": broken_tracks.is_empty(),
- }
- }
-
-
-# ============================================================================
-# Static helpers — shared with handler + presets
-# ============================================================================
-
-## Resolve the effective root node an AnimationPlayer animates against.
-## Falls back to the player's parent when the explicit root_node NodePath is
-## empty or unresolvable. Returns null when the player isn't in the tree.
-##
-## Mirrors the resolution Godot does at playback time so the validator,
-## preset target resolver, and track-property coercer all see the same root.
-static func player_root_node(player: AnimationPlayer) -> Node:
- if not player.is_inside_tree():
- return null
- var rn := player.root_node
- if rn != NodePath():
- var n := player.get_node_or_null(rn)
- if n != null:
- return n
- return player.get_parent()
-
-
-## Coerce a JSON value to match the expected Godot type for the given
-## track_path. Returns {"ok": value} or {"error": msg}.
-## Passes the raw value through when the target node isn't in the scene
-## yet (authoring-time path). Errors when the target exists but the
-## property doesn't, or when parsing a typed value (Color/Vector2/Vector3)
-## clearly fails — better to reject than silently store garbage.
-## `override_root_node` lets callers supply the root to resolve target paths
-## against when the player isn't in the tree yet (auto-create flow) — the
-## player's future parent stands in for the root the AnimationPlayer will
-## eventually use.
-static func coerce_value_for_track(value: Variant, track_path: String, player: AnimationPlayer, override_root_node: Node = null) -> Dictionary:
- var ctx := resolve_track_prop_context(track_path, player, override_root_node)
- if ctx.has("error"):
- return {"error": ctx.error}
- return coerce_with_context(value, ctx)
-
-
-## Resolve a track_path's target property type once, so callers coercing many
-## keyframes avoid walking `get_property_list()` on every one. Returns:
-## {pass_through: true} — no resolution / authoring-time
-## {pass_through: false, prop_type, prop_name} — coerce against this type
-## {error: msg} — property not found on target
-##
-## Supports Godot's native NodePath subpath form `property:sub` (e.g.
-## `position:y`, `modulate:a`) — splits on the FIRST colon (node↔property
-## boundary), resolves the base property on the target, and for known
-## scalar subpaths (x/y/z/w on vectors, r/g/b/a on Color) narrows the
-## coerce target to TYPE_FLOAT so JSON numbers land as floats, not dicts.
-static func resolve_track_prop_context(track_path: String, player: AnimationPlayer, override_root_node: Node = null) -> Dictionary:
- var colon := track_path.find(":")
- if colon < 0:
- return {"pass_through": true}
-
- var node_part := track_path.substr(0, colon)
- var prop_full := track_path.substr(colon + 1)
-
- # Property may include a subpath: "position:y", "modulate:a", etc.
- var sub_colon := prop_full.find(":")
- var prop_base := prop_full if sub_colon < 0 else prop_full.substr(0, sub_colon)
- var prop_sub := "" if sub_colon < 0 else prop_full.substr(sub_colon + 1)
-
- var root_node: Node = override_root_node
- if root_node == null:
- root_node = player_root_node(player)
- if root_node == null:
- return {"pass_through": true}
-
- var target: Node = root_node.get_node_or_null(node_part)
- if target == null:
- # Target node isn't in the scene yet — authoring-time path. Pass through.
- return {"pass_through": true}
-
- for p in target.get_property_list():
- if p.name == prop_base:
- var base_type: int = p.get("type", TYPE_NIL)
- var coerce_type := base_type
- if not prop_sub.is_empty():
- var sub_type := subpath_component_type(base_type, prop_sub)
- if sub_type == TYPE_NIL:
- # Unknown subpath component — pass through so Godot's own
- # NodePath resolution raises at playback if it's truly bogus,
- # rather than fabricating a coerce error for a valid-but-
- # uncommon form (e.g. Transform3D subpaths).
- return {"pass_through": true}
- coerce_type = sub_type
- return {
- "pass_through": false,
- "prop_type": coerce_type,
- "prop_name": prop_full,
- }
-
- # Target exists but the property doesn't. Reject loudly — silently storing
- # the raw value here produces garbage keyframes at playback time.
- return {"error":
- "%s (target path: '%s')" %
- [PropertyErrors.build_message(target, prop_base), node_part]}
-
-
-## Map a `property:sub` subpath to its scalar component type. Returns
-## TYPE_NIL when the base type / subkey pair isn't one we recognise —
-## callers pass-through in that case rather than mis-coerce.
-static func subpath_component_type(base_type: int, sub: String) -> int:
- var entry = _SUBPATH_COMPONENTS.get(base_type)
- if entry == null or sub.length() != 1:
- return TYPE_NIL
- return entry[1] if (entry[0] as String).contains(sub) else TYPE_NIL
-
-
-static func coerce_with_context(value: Variant, ctx: Dictionary) -> Dictionary:
- if ctx.get("pass_through", false):
- return {"ok": value}
- return coerce_for_type(value, ctx.prop_type, ctx.prop_name)
-
-
-## Coerce a single value to the given Godot variant type. Returns
-## {"ok": coerced} or {"error": msg}. Unknown types pass through.
-static func coerce_for_type(value: Variant, prop_type: int, prop_name: String) -> Dictionary:
- match prop_type:
- TYPE_COLOR:
- ## Canonical strict parser (#714): same shapes as every other
- ## color-accepting handler, including [r,g,b(,a)] arrays.
- var col = McpJsonValues.parse_color(value)
- if col != null:
- return {"ok": col}
- return {"error": "Cannot coerce value to Color for property '%s' (expected \"#rrggbb(aa)\"/named string, {r,g,b[,a]}, [r,g,b(,a)], or Color)" % prop_name}
- TYPE_VECTOR2:
- var v2 = McpJsonValues.parse_vector2(value)
- if v2 != null:
- return {"ok": v2}
- return {"error": "Cannot coerce value to Vector2 for property '%s' (expected {x,y}, [x,y], or Vector2)" % prop_name}
- TYPE_VECTOR3:
- var v3 = McpJsonValues.parse_vector3(value)
- if v3 != null:
- return {"ok": v3}
- return {"error": "Cannot coerce value to Vector3 for property '%s' (expected {x,y,z}, [x,y,z], or Vector3)" % prop_name}
- TYPE_FLOAT:
- ## Canonical scalar parser (#964): int/float passthrough plus
- ## strictly-numeric strings. Null falls through to the generic
- ## passthrough below, matching the old behavior for garbage.
- var parsed: Variant = McpJsonValues.parse_float(value)
- if parsed != null:
- return {"ok": parsed}
- TYPE_INT:
- if value is float or value is int:
- return {"ok": int(value)}
- TYPE_BOOL:
- if value is int or value is float or value is bool:
- return {"ok": bool(value)}
- return {"ok": value}
-
-
-# ============================================================================
-# Static helpers — parsing + serializing
-# ============================================================================
-
-## Parse a transition value: named string or raw float.
-## Named values live in `_NAMED_TRANSITIONS` so the mapping has a single source.
-static func parse_transition(v: Variant) -> float:
- if v is float or v is int:
- return float(v)
- if v is String:
- var key: String = (v as String).to_lower()
- if _NAMED_TRANSITIONS.has(key):
- return float(_NAMED_TRANSITIONS[key])
- return 1.0
-
-
-## Map an Animation.TrackType enum to a stable string. Unknown types report
-## as "unknown" rather than being silently coerced to "method" — callers that
-## only produce value/method tracks can ignore the others; clients that want
-## to round-trip bezier/audio/etc. get an honest label to key off.
-static func track_type_to_string(track_type: int) -> String:
- match track_type:
- Animation.TYPE_VALUE: return "value"
- Animation.TYPE_METHOD: return "method"
- Animation.TYPE_POSITION_3D: return "position_3d"
- Animation.TYPE_ROTATION_3D: return "rotation_3d"
- Animation.TYPE_SCALE_3D: return "scale_3d"
- Animation.TYPE_BLEND_SHAPE: return "blend_shape"
- Animation.TYPE_BEZIER: return "bezier"
- Animation.TYPE_AUDIO: return "audio"
- Animation.TYPE_ANIMATION: return "animation"
- _: return "unknown"
-
-
-static func loop_mode_to_string(mode: int) -> String:
- match mode:
- Animation.LOOP_LINEAR: return "linear"
- Animation.LOOP_PINGPONG: return "pingpong"
- _: return "none"
-
-
-static func interp_to_string(mode: int) -> String:
- match mode:
- Animation.INTERPOLATION_NEAREST: return "nearest"
- Animation.INTERPOLATION_CUBIC: return "cubic"
- _: return "linear"
-
-
-## Convert a Godot Variant to a JSON-safe value.
-static func serialize_value(value: Variant) -> Variant:
- ## Delegates to the shared serializer (#714) — the drifted private copy
- ## stringified rotation_3d keyframe Quaternions into opaque text where
- ## McpVariantSerializer emits the {x,y,z,w} dict callers can round-trip
- ## (it also NaN/Inf-guards floats, matching the wire contract).
- return VariantSerializer.serialize(value)
diff --git a/addons/godot_ai/handlers/animation_values.gd.uid b/addons/godot_ai/handlers/animation_values.gd.uid
deleted file mode 100644
index 5d2a8b7..0000000
--- a/addons/godot_ai/handlers/animation_values.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bguta2eb8blgf
diff --git a/addons/godot_ai/handlers/api_handler.gd b/addons/godot_ai/handlers/api_handler.gd
deleted file mode 100644
index 35fcd14..0000000
--- a/addons/godot_ai/handlers/api_handler.gd
+++ /dev/null
@@ -1,89 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-## Read-only access to version-correct Godot class metadata.
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-const ClassIntrospection := preload("res://addons/godot_ai/utils/class_introspection.gd")
-const FuzzySuggestions := preload("res://addons/godot_ai/utils/fuzzy_suggestions.gd")
-
-func get_class_info(params: Dictionary) -> Dictionary:
- var requested_class: String = params.get("class_name", "")
- if requested_class.is_empty():
- return ErrorCodes.make(
- ErrorCodes.MISSING_REQUIRED_PARAM,
- "Missing required param: class_name"
- )
- if not ClassDB.class_exists(requested_class):
- var script_class := _global_script_class(requested_class)
- if not script_class.is_empty():
- return _script_class_error(requested_class, script_class)
- return _unknown_class_error(requested_class)
- if params.has("limit") and int(params.get("limit")) < 0:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "limit must be >= 0; use limit=0 only when an unlimited section is needed"
- )
- var section_check := ClassIntrospection.validate_sections(
- params.get("sections", ClassIntrospection.DEFAULT_SECTIONS)
- )
- if not section_check.invalid.is_empty():
- return _invalid_sections_error(section_check.invalid)
- return {"data": ClassIntrospection.build(requested_class, params)}
-
-
-static func _unknown_class_error(requested_class: String) -> Dictionary:
- var suggestions := _suggest_classes(requested_class)
- var message := "Unknown Godot class: %s" % requested_class
- if not suggestions.is_empty():
- message += ". Did you mean: %s?" % ", ".join(suggestions)
- var result := ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, message)
- result["error"]["data"] = {"suggestions": suggestions}
- return result
-
-
-static func _suggest_classes(requested_class: String) -> Array[String]:
- return FuzzySuggestions.rank(requested_class, ClassDB.get_class_list())
-
-
-static func _global_script_class(requested_class: String) -> Dictionary:
- for raw_info in ProjectSettings.get_global_class_list():
- var info: Dictionary = raw_info
- if info.get("class", "") == requested_class:
- return info
- return {}
-
-
-static func _script_class_error(requested_class: String, script_class: Dictionary) -> Dictionary:
- var path := str(script_class.get("path", ""))
- var base := str(script_class.get("base", ""))
- var message := (
- "%s is a project script class, not a ClassDB class. "
- + "Use script_manage(op=\"find_symbols\", params={\"path\": \"%s\"}) for script symbols."
- ) % [requested_class, path]
- var result := ErrorCodes.make(ErrorCodes.WRONG_TYPE, message)
- result["error"]["data"] = {
- "script_class": true,
- "class_name": requested_class,
- "base_class": base,
- "path": path,
- }
- return result
-
-
-static func _invalid_sections_error(invalid_sections: Array[String]) -> Dictionary:
- var suggestions := {}
- for section in invalid_sections:
- suggestions[section] = FuzzySuggestions.rank(
- section,
- ClassIntrospection.SUGGESTABLE_SECTION_TOKENS,
- 3,
- 0.3
- )
- var message := "Unknown class-info section(s): %s. Valid sections: %s (or \"all\" for all documentation sections; \"inheritors\" must be requested by name)" % [
- ", ".join(invalid_sections),
- ", ".join(ClassIntrospection.KNOWN_SECTIONS),
- ]
- var result := ErrorCodes.make(ErrorCodes.INVALID_PARAMS, message)
- result["error"]["data"] = {"suggestions": suggestions}
- return result
diff --git a/addons/godot_ai/handlers/api_handler.gd.uid b/addons/godot_ai/handlers/api_handler.gd.uid
deleted file mode 100644
index f519c48..0000000
--- a/addons/godot_ai/handlers/api_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://v3rkd7ueunii
diff --git a/addons/godot_ai/handlers/audio_handler.gd b/addons/godot_ai/handlers/audio_handler.gd
deleted file mode 100644
index ba7bd59..0000000
--- a/addons/godot_ai/handlers/audio_handler.gd
+++ /dev/null
@@ -1,363 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Handles AudioStreamPlayer / 2D / 3D authoring — node creation, stream
-## assignment, playback-property edits, and real editor preview playback.
-##
-## Stream assignment loads a Godot-imported AudioStream resource from
-## res:// (the editor's import step converts .ogg / .wav / .mp3 into a
-## streamable AudioStream subclass before we ever see it).
-##
-## play() / stop() call the live node method directly — no undo, no
-## persistence; they match what the inspector's play button does.
-
-
-const _VALID_TYPES := {
- "1d": "AudioStreamPlayer",
- "2d": "AudioStreamPlayer2D",
- "3d": "AudioStreamPlayer3D",
-}
-
-## Whitelist of playback properties settable via audio_player_set_playback.
-## Each value is the expected Variant type of the param dict value.
-const _PLAYBACK_KEYS := {
- "volume_db": TYPE_FLOAT,
- "pitch_scale": TYPE_FLOAT,
- "autoplay": TYPE_BOOL,
- "bus": TYPE_STRING,
-}
-
-
-var _undo_redo: EditorUndoRedoManager
-
-
-func _init(undo_redo: EditorUndoRedoManager) -> void:
- _undo_redo = undo_redo
-
-
-# ============================================================================
-# audio_player_create
-# ============================================================================
-
-func create_player(params: Dictionary) -> Dictionary:
- var parent_path: String = params.get("parent_path", "")
- var node_name: String = params.get("name", "AudioStreamPlayer")
- var type_str: String = params.get("type", "1d")
-
- if not _VALID_TYPES.has(type_str):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid audio player type '%s'. Valid: %s" % [type_str, ", ".join(_VALID_TYPES.keys())]
- )
-
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- var parent: Node = scene_root
- if not parent_path.is_empty():
- parent = McpScenePath.resolve(parent_path, scene_root)
- if parent == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, McpScenePath.format_parent_error(parent_path, scene_root))
-
- var node := _instantiate_player(type_str)
- if node == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate audio player")
- if not node_name.is_empty():
- node.name = node_name
-
- _undo_redo.create_action("MCP: Create %s '%s'" % [_VALID_TYPES[type_str], node.name])
- _undo_redo.add_do_method(parent, "add_child", node, true)
- _undo_redo.add_do_method(node, "set_owner", scene_root)
- _undo_redo.add_do_reference(node)
- _undo_redo.add_undo_method(parent, "remove_child", node)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": McpScenePath.from_node(node, scene_root),
- "parent_path": McpScenePath.from_node(parent, scene_root),
- "name": String(node.name),
- "type": type_str,
- "class": _VALID_TYPES[type_str],
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# audio_player_set_stream
-# ============================================================================
-
-func set_stream(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var stream_path: String = params.get("stream_path", "")
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
- if stream_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: stream_path")
-
- var stream_path_err = McpPathValidator.loadable_error(stream_path, "stream_path")
- if stream_path_err != null:
- return stream_path_err
-
- var resolved := _resolve_player(player_path)
- if resolved.has("error"):
- return resolved
- var player: Node = resolved.player
-
- if not ResourceLoader.exists(stream_path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "AudioStream not found: %s" % stream_path)
- var loaded := ResourceLoader.load(stream_path)
- if loaded == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to load AudioStream: %s" % stream_path)
- if not (loaded is AudioStream):
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Resource at %s is not an AudioStream (got %s)" % [stream_path, loaded.get_class()]
- )
-
- var old_stream: AudioStream = player.stream
-
- _undo_redo.create_action("MCP: Set audio stream on %s" % player.name)
- _undo_redo.add_do_property(player, "stream", loaded)
- _undo_redo.add_undo_property(player, "stream", old_stream)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "player_path": player_path,
- "stream_path": stream_path,
- "stream_class": loaded.get_class(),
- "duration_seconds": float(loaded.get_length()),
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# audio_player_set_playback
-# ============================================================================
-
-func set_playback(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
-
- var resolved := _resolve_player(player_path)
- if resolved.has("error"):
- return resolved
- var player: Node = resolved.player
-
- var updates: Dictionary = {}
- for key in _PLAYBACK_KEYS:
- if params.has(key):
- var expected_type: int = _PLAYBACK_KEYS[key]
- var value = params.get(key)
- var coerced = _coerce_playback_value(value, expected_type)
- if coerced == null:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Invalid value for %s: expected %s, got %s" % [
- key, type_string(expected_type), type_string(typeof(value))
- ]
- )
- updates[key] = coerced
-
- if updates.is_empty():
- return ErrorCodes.make(
- ErrorCodes.MISSING_REQUIRED_PARAM,
- "At least one of %s is required" % ", ".join(_PLAYBACK_KEYS.keys())
- )
-
- var old_values: Dictionary = {}
- for key in updates:
- old_values[key] = player.get(key)
-
- _undo_redo.create_action("MCP: Update playback on %s" % player.name)
- for key in updates:
- _undo_redo.add_do_property(player, key, updates[key])
- _undo_redo.add_undo_property(player, key, old_values[key])
- _undo_redo.commit_action()
-
- return {
- "data": {
- "player_path": player_path,
- "applied": updates.keys(),
- "values": updates,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# audio_play (runtime preview — not saved with scene)
-# ============================================================================
-
-func play(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- var from_position: float = float(params.get("from_position", 0.0))
-
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
-
- var resolved := _resolve_player(player_path)
- if resolved.has("error"):
- return resolved
- var player: Node = resolved.player
-
- if player.stream == null:
- return ErrorCodes.make(
- ErrorCodes.MISSING_REQUIRED_PARAM,
- "Player has no stream assigned — call audio_player_set_stream first"
- )
-
- player.play(from_position)
-
- return {
- "data": {
- "player_path": player_path,
- "from_position": from_position,
- "playing": bool(player.playing),
- "undoable": false,
- "reason": "Runtime playback state — not saved with scene",
- }
- }
-
-
-# ============================================================================
-# audio_stop (runtime preview — not saved with scene)
-# ============================================================================
-
-func stop(params: Dictionary) -> Dictionary:
- var player_path: String = params.get("player_path", "")
- if player_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: player_path")
-
- var resolved := _resolve_player(player_path)
- if resolved.has("error"):
- return resolved
- var player: Node = resolved.player
-
- player.stop()
-
- return {
- "data": {
- "player_path": player_path,
- "playing": bool(player.playing),
- "undoable": false,
- "reason": "Runtime playback state — not saved with scene",
- }
- }
-
-
-# ============================================================================
-# audio_list (read — scan project for AudioStream resources)
-# ============================================================================
-
-func list_streams(params: Dictionary) -> Dictionary:
- var root: String = params.get("root", "res://")
- var include_duration: bool = bool(params.get("include_duration", true))
-
- var root_err = McpPathValidator.path_error(root, "root")
- if root_err != null:
- return root_err
-
- var efs := EditorInterface.get_resource_filesystem()
- if efs == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_UNAVAILABLE,
- "EditorFileSystem not available", false)
-
- var results: Array[Dictionary] = []
- var start_dir := efs.get_filesystem_path(root)
- if start_dir == null:
- start_dir = efs.get_filesystem()
- _scan_audio(start_dir, root, include_duration, results)
- return {
- "data": {
- "root": root,
- "streams": results,
- "count": results.size(),
- }
- }
-
-
-func _scan_audio(dir: EditorFileSystemDirectory, root: String, include_duration: bool, out: Array[Dictionary]) -> void:
- if dir == null:
- return
- for i in dir.get_file_count():
- var file_path := dir.get_file_path(i)
- if not file_path.begins_with(root):
- continue
- var file_type := dir.get_file_type(i)
- var is_audio := file_type == "AudioStream" or ClassDB.is_parent_class(file_type, "AudioStream")
- if not is_audio:
- continue
- var entry: Dictionary = {
- "path": file_path,
- "class": file_type,
- }
- if include_duration:
- var res := ResourceLoader.load(file_path)
- if res is AudioStream:
- entry["duration_seconds"] = float((res as AudioStream).get_length())
- else:
- entry["duration_seconds"] = 0.0
- out.append(entry)
- for i in dir.get_subdir_count():
- _scan_audio(dir.get_subdir(i), root, include_duration, out)
-
-
-# ============================================================================
-# Helpers
-# ============================================================================
-
-static func _instantiate_player(type_str: String) -> Node:
- match type_str:
- "1d":
- return AudioStreamPlayer.new()
- "2d":
- return AudioStreamPlayer2D.new()
- "3d":
- return AudioStreamPlayer3D.new()
- return null
-
-
-func _resolve_player(player_path: String) -> Dictionary:
- var resolved := McpNodeValidator.resolve_or_error(player_path, "player_path")
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var is_player := node is AudioStreamPlayer \
- or node is AudioStreamPlayer2D \
- or node is AudioStreamPlayer3D
- if not is_player:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Node at %s is not an AudioStreamPlayer/2D/3D (got %s)" % [player_path, node.get_class()]
- )
- return {"player": node}
-
-
-## Coerce a playback param value to the expected type. int→float and
-## strictly-numeric strings are allowed so JSON integers and stringified
-## floats (#964) pass through; everything else requires the exact type.
-## Returns the coerced value, or null on type mismatch.
-static func _coerce_playback_value(value: Variant, expected_type: int) -> Variant:
- match expected_type:
- TYPE_FLOAT:
- var parsed: Variant = McpJsonValues.parse_float(value)
- if parsed != null:
- return parsed
- TYPE_BOOL:
- if value is bool:
- return value
- TYPE_STRING:
- if value is String:
- return value
- return null
diff --git a/addons/godot_ai/handlers/audio_handler.gd.uid b/addons/godot_ai/handlers/audio_handler.gd.uid
deleted file mode 100644
index 2510ee7..0000000
--- a/addons/godot_ai/handlers/audio_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cjtvod52xxocs
diff --git a/addons/godot_ai/handlers/autoload_handler.gd b/addons/godot_ai/handlers/autoload_handler.gd
deleted file mode 100644
index 790521b..0000000
--- a/addons/godot_ai/handlers/autoload_handler.gd
+++ /dev/null
@@ -1,91 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Handles autoload listing, adding, and removing via ProjectSettings.
-
-
-func list_autoloads(_params: Dictionary) -> Dictionary:
- var autoloads: Array[Dictionary] = []
- for prop in ProjectSettings.get_property_list():
- var key: String = prop.get("name", "")
- if not key.begins_with("autoload/"):
- continue
- var name := key.substr("autoload/".length())
- var raw_value: String = ProjectSettings.get_setting(key, "")
- var is_singleton := raw_value.begins_with("*")
- var path := raw_value.substr(1) if is_singleton else raw_value
- autoloads.append({
- "name": name,
- "path": path,
- "singleton": is_singleton,
- })
- return {"data": {"autoloads": autoloads, "count": autoloads.size()}}
-
-
-func add_autoload(params: Dictionary) -> Dictionary:
- var name: String = params.get("name", "")
- var path: String = params.get("path", "")
- var singleton: bool = params.get("singleton", true)
-
- if name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: name")
- if path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
- var path_err = McpPathValidator.path_error(path, "path")
- if path_err != null:
- return path_err
- if not FileAccess.file_exists(path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "File not found: %s" % path)
-
- var key := "autoload/%s" % name
- if ProjectSettings.has_setting(key):
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "Autoload '%s' already exists" % name)
-
- var value := ("*" if singleton else "") + path
- ProjectSettings.set_setting(key, value)
- ProjectSettings.set_initial_value(key, "")
- ProjectSettings.set_as_basic(key, true)
- var err := ProjectSettings.save()
- if err != OK:
- ProjectSettings.clear(key)
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Failed to save project settings while adding autoload '%s': %s (error %d)" % [name, error_string(err), err])
-
- return {
- "data": {
- "name": name,
- "path": path,
- "singleton": singleton,
- "undoable": false,
- "reason": "Autoload changes are saved to project.godot",
- }
- }
-
-
-func remove_autoload(params: Dictionary) -> Dictionary:
- var name: String = params.get("name", "")
- if name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: name")
-
- var key := "autoload/%s" % name
- if not ProjectSettings.has_setting(key):
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, "Autoload '%s' not found" % name)
-
- var old_value: String = ProjectSettings.get_setting(key, "")
- ProjectSettings.clear(key)
- var err := ProjectSettings.save()
- if err != OK:
- ProjectSettings.set_setting(key, old_value)
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Failed to save project settings while removing autoload '%s': %s (error %d)" % [name, error_string(err), err])
-
- return {
- "data": {
- "name": name,
- "removed": true,
- "undoable": false,
- "reason": "Autoload changes are saved to project.godot",
- }
- }
diff --git a/addons/godot_ai/handlers/autoload_handler.gd.uid b/addons/godot_ai/handlers/autoload_handler.gd.uid
deleted file mode 100644
index 921ed4e..0000000
--- a/addons/godot_ai/handlers/autoload_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bb0inov044jn6
diff --git a/addons/godot_ai/handlers/batch_handler.gd b/addons/godot_ai/handlers/batch_handler.gd
deleted file mode 100644
index 3db1392..0000000
--- a/addons/godot_ai/handlers/batch_handler.gd
+++ /dev/null
@@ -1,237 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Executes a list of sub-commands through the dispatcher with stop-on-first-error
-## semantics. When undo=true (default), any successful sub-commands are rolled
-## back via the scene's UndoRedo history if a later sub-command fails.
-
-## Commands that cannot run as batch sub-commands, each with the reason a batch
-## can't host it.
-## - batch_execute: would recurse.
-## - reload_plugin: schedules teardown; later batch operations could pump
-## its callback while the batch handler is still executing.
-## - run_tests: a batch executes synchronously inside one dispatcher tick with
-## NO transport servicing, so a full suite starves the WebSocket heartbeat
-## (the exact disconnect the serviced test_run path exists to prevent) and
-## the Python batch handler only allows 30s anyway — call the test_run tool
-## directly.
-## - game_command: it is deferred — its reply flows out-of-band correlated by a
-## _request_id that dispatch_direct deliberately strips (see
-## McpDispatcher.dispatch_direct), so a game op nested in a batch would have
-## no completion channel and hang or lose its reply. input_sequence made this
-## concrete (#814); the whole game_command surface shares the deferred path.
-const FORBIDDEN_SUBCOMMANDS := {
- "reload_plugin": "reload_plugin must be called directly so no batch continues during reload",
- "batch_execute": "batch_execute cannot be nested inside another batch",
- "run_tests":
- "run_tests is not allowed as a sub-command — a batch runs synchronously "
- + "with no transport servicing; call the test_run tool directly",
- "game_command":
- "game_command ops are deferred (their reply arrives out-of-band) and "
- + "have no completion channel inside a batch — run them as their own tool call",
- "configure_client":
- "configure_client is deferred and has no completion channel inside a batch — "
- + "run it as its own tool call",
- "remove_client":
- "remove_client is deferred and has no completion channel inside a batch — "
- + "run it as its own tool call",
-}
-
-## The whole batch executes synchronously inside one dispatcher tick,
-## outside the 4ms frame budget — an unbounded array freezes the editor
-## for the batch's full duration. 500 is far above any legitimate scene
-## edit while keeping worst-case stalls in check.
-const MAX_BATCH_COMMANDS := 500
-
-var _dispatcher: McpDispatcher
-var _undo_redo: EditorUndoRedoManager
-
-
-func _init(dispatcher: McpDispatcher, undo_redo: EditorUndoRedoManager) -> void:
- _dispatcher = dispatcher
- _undo_redo = undo_redo
-
-
-func batch_execute(params: Dictionary) -> Dictionary:
- var commands = params.get("commands", null)
- if typeof(commands) != TYPE_ARRAY:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "commands must be a list")
- if commands.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "commands must not be empty")
- if commands.size() > MAX_BATCH_COMMANDS:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "commands exceeds the %d-command batch cap (got %d) — split into multiple batches" % [MAX_BATCH_COMMANDS, commands.size()]
- )
-
- var undo: bool = params.get("undo", true)
-
- for idx in range(commands.size()):
- var item = commands[idx]
- if typeof(item) != TYPE_DICTIONARY:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "commands[%d] must be a dict" % idx)
- var cmd_name: String = item.get("command", "")
- if cmd_name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "commands[%d] missing 'command' field" % idx)
- if FORBIDDEN_SUBCOMMANDS.has(cmd_name):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "commands[%d]: %s" % [idx, FORBIDDEN_SUBCOMMANDS[cmd_name]])
- if not _dispatcher.has_command(cmd_name):
- return _unknown_command_error(idx, cmd_name)
- ## Pre-validate params type: the execution loop's typed Dictionary
- ## local would hard-error on a non-dict mid-batch, aborting AFTER
- ## earlier mutations committed. Catching it here keeps the
- ## all-or-nothing contract for malformed input.
- if typeof(item.get("params", {})) != TYPE_DICTIONARY:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "commands[%d].params must be a dict" % idx)
- if cmd_name.begins_with("custom_tool:"):
- var _custom_tools := McpToolRegistry.get_instance()
- if _custom_tools:
- var spec := _custom_tools.get_spec(cmd_name.trim_prefix("custom_tool:"))
- ## A deferred tool's real reply arrives via ctx.send_deferred
- ## AFTER the batch has already returned its synchronous
- ## results — the sub-command would report an opaque
- ## "_deferred" placeholder and the actual payload would be
- ## orphaned. Reject up-front, matching the FORBIDDEN_SUBCOMMANDS
- ## contract for run_tests.
- if spec != null and spec.deferred:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "commands[%d] custom_tool:%s is deferred — its reply outlives the batch response; call it directly instead" % [idx, cmd_name.trim_prefix("custom_tool:")]
- )
- if undo and spec != null and not spec.undoable:
- return ErrorCodes.make(
- ErrorCodes.CUSTOM_TOOL_NOT_UNDOABLE,
- "commands[%d] custom_tool:%s is not undoable" % [idx, cmd_name.trim_prefix("custom_tool:")]
- )
-
- var results: Array = []
- var succeeded := 0
- var stopped_at = null
- var all_undoable := true
- ## One entry per action a sub-command actually committed, in commit order,
- ## naming the UndoRedo that received it. Rollback replays this in reverse.
- ##
- ## Measured rather than inferred. Counting *successes* over-undoes, because
- ## a sub-command can succeed without committing anything (`create_script`
- ## and `write_text_file` write straight to disk) — the extra undos then eat
- ## the user's own pre-batch edits. Trusting the response's `undoable` flag
- ## instead under-undoes, because `custom_tool:` sub-commands never carry one
- ## (`custom_tool_wrapper.gd` returns the addon handler's dict verbatim) even
- ## when their spec declares `undoable = true` and they did commit. Comparing
- ## `UndoRedo.get_version()` across the call is the only reading that can't
- ## disagree with what the editor actually recorded.
- var committed: Array = []
-
- for idx in range(commands.size()):
- var item: Dictionary = commands[idx]
- var cmd_name: String = item["command"]
- var sub_params: Dictionary = item.get("params", {})
-
- var tracked := _tracked_histories()
- var before: Array = []
- for ur in tracked:
- before.append(ur.get_version())
-
- var raw_result: Dictionary = _dispatcher.dispatch_direct(cmd_name, sub_params)
- ## Record before reading status. A handler can commit_action() and still
- ## return an error; those actions must be in `committed` so a later
- ## (or this) failure rolls them back.
- _record_committed(tracked, before, committed)
- var status: String = raw_result.get("status", "ok")
-
- var result_entry: Dictionary = {"command": cmd_name, "status": status}
- if status == "error":
- result_entry["error"] = raw_result.get("error", {})
- results.append(result_entry)
- stopped_at = idx
- break
- else:
- var data: Dictionary = raw_result.get("data", raw_result)
- result_entry["data"] = data
- if typeof(data) == TYPE_DICTIONARY and data.get("undoable", false) != true:
- all_undoable = false
- results.append(result_entry)
- succeeded += 1
-
- var rolled_back := false
- if stopped_at != null and undo and not committed.is_empty():
- rolled_back = _rollback(committed)
-
- var response_data: Dictionary = {
- "succeeded": succeeded,
- "stopped_at": stopped_at,
- "results": results,
- "undo": undo,
- "rolled_back": rolled_back,
- "undoable": stopped_at == null and all_undoable and not rolled_back,
- }
- if stopped_at != null:
- response_data["error"] = results[-1]["error"]
- return {"data": response_data}
-
-
-## The histories a batch sub-command can commit into.
-##
-## Most write handlers pin their action to the edited scene, but not all:
-## handlers that `create_action(label)` without a context object bind it to the
-## handler `RefCounted` instead, and it lands in `GLOBAL_HISTORY` (see
-## `animation_handler.gd`'s note on mismatched histories, and
-## `testing/test_suite.gd`, which watches both for the same reason). Watching
-## only the scene history would leave those actions un-rolled-back.
-func _tracked_histories() -> Array:
- var out: Array = []
- var scene_root := EditorInterface.get_edited_scene_root()
- if scene_root != null:
- var scene_ur := _undo_redo.get_history_undo_redo(
- _undo_redo.get_object_history_id(scene_root)
- )
- if scene_ur != null:
- out.append(scene_ur)
- var global_ur := _undo_redo.get_history_undo_redo(EditorUndoRedoManager.GLOBAL_HISTORY)
- if global_ur != null and not global_ur in out:
- out.append(global_ur)
- return out
-
-
-## Append one entry to `committed` per action the just-finished sub-command
-## pushed, by diffing each watched history's version against `before`.
-## `UndoRedo.get_version()` advances once per `commit_action`, so the delta is
-## the action count — a handler that commits twice is recorded twice, and one
-## that commits nothing is recorded not at all.
-func _record_committed(tracked: Array, before: Array, committed: Array) -> void:
- for i in range(tracked.size()):
- var delta: int = tracked[i].get_version() - int(before[i])
- for _n in range(delta):
- committed.append(tracked[i])
-
-
-## Build the unknown-command error for a sub-command. Clarifies that
-## batch_execute expects plugin command names (not MCP tool names) and
-## surfaces fuzzy suggestions in both the message and structured data.
-func _unknown_command_error(idx: int, cmd_name: String) -> Dictionary:
- var suggestions := _dispatcher.suggest_similar(cmd_name)
- var msg := "commands[%d]: unknown plugin command '%s'. batch_execute expects plugin command names (e.g. 'create_node'), not MCP tool names (e.g. 'node_create')." % [idx, cmd_name]
- if not suggestions.is_empty():
- msg += " Did you mean: %s?" % ", ".join(suggestions)
- var err := ErrorCodes.make(ErrorCodes.UNKNOWN_COMMAND, msg)
- err["error"]["data"] = {"suggestions": suggestions}
- return err
-
-
-## Undo every action the batch committed, newest first, against the history
-## that actually received it. Returns true iff every undo() reported success.
-##
-## Strict LIFO across histories matters: undoing a scene action before a global
-## one committed after it would replay them out of order. Walking `committed`
-## in reverse preserves commit order regardless of which history each landed in.
-func _rollback(committed: Array) -> bool:
- if committed.is_empty():
- return false
- var ok := true
- for i in range(committed.size() - 1, -1, -1):
- if not committed[i].undo():
- ok = false
- return ok
diff --git a/addons/godot_ai/handlers/batch_handler.gd.uid b/addons/godot_ai/handlers/batch_handler.gd.uid
deleted file mode 100644
index 630a596..0000000
--- a/addons/godot_ai/handlers/batch_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dt7um75oofdrh
diff --git a/addons/godot_ai/handlers/camera_handler.gd b/addons/godot_ai/handlers/camera_handler.gd
deleted file mode 100644
index 5675037..0000000
--- a/addons/godot_ai/handlers/camera_handler.gd
+++ /dev/null
@@ -1,1131 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Handles Camera2D / Camera3D authoring — create, configure, bounds, damping,
-## node-parent-based follow, presets.
-##
-## All writes are bundled into a single EditorUndoRedoManager action.
-## Setting current=true auto-unmarks previously-current cameras of the same
-## class in the same action so one Ctrl-Z reverts the switch.
-
-const CameraValues := preload("res://addons/godot_ai/handlers/camera_values.gd")
-const CameraPresets := preload("res://addons/godot_ai/handlers/camera_presets.gd")
-
-const _VALID_TYPES := {
- "2d": "Camera2D",
- "3d": "Camera3D",
-}
-
-const _KEYS_2D := [
- "zoom",
- "offset",
- "anchor_mode",
- "ignore_rotation",
- "enabled",
- "current",
- "process_callback",
- "position_smoothing_enabled",
- "position_smoothing_speed",
- "rotation_smoothing_enabled",
- "rotation_smoothing_speed",
- "drag_horizontal_enabled",
- "drag_vertical_enabled",
- "drag_horizontal_offset",
- "drag_vertical_offset",
- "drag_left_margin",
- "drag_top_margin",
- "drag_right_margin",
- "drag_bottom_margin",
- "limit_left",
- "limit_right",
- "limit_top",
- "limit_bottom",
- "limit_smoothed",
-]
-
-const _KEYS_3D := [
- "fov",
- "near",
- "far",
- "size",
- "projection",
- "keep_aspect",
- "cull_mask",
- "doppler_tracking",
- "h_offset",
- "v_offset",
- "current",
-]
-
-# Transform-shaped keys live on Node2D / Node3D, not in the camera-specific
-# schema — rejecting them without a hint sends agents searching for the wrong
-# tool.
-const _NODE_TRANSFORM_KEYS := [
- "position", "rotation", "scale", "transform",
- "global_position", "global_rotation", "global_scale", "global_transform",
-]
-
-const _DAMPING_MARGIN_KEYS := ["left", "top", "right", "bottom"]
-const _CURRENT_SETTLE_ATTEMPTS := 8
-const _CURRENT_SETTLE_DELAY_MSEC := 10
-
-
-var _undo_redo: EditorUndoRedoManager
-
-# Per-scene logical-current bookkeeping. Keys are scene-root InstanceIDs;
-# values are { "2d": NodePath-as-String, "3d": NodePath-as-String } with
-# missing keys meaning "no logical current for that class."
-#
-# Stored on the handler instance (NOT as Node metadata on the scene root)
-# because set_meta() persists into the .tscn on save, contaminating user
-# scene files with MCP-internal sidecar state that lingers across reloads
-# and travels in commits.
-var _logical_current: Dictionary = {}
-
-
-func _init(undo_redo: EditorUndoRedoManager) -> void:
- _undo_redo = undo_redo
-
-
-# Camera2D doesn't expose `current` as a settable property in Godot 4 —
-# only is_current() / make_current() / clear_current(). Camera3D exposes
-# both, but using methods uniformly avoids per-class branching.
-static func _is_current(cam: Node) -> bool:
- if cam == null:
- return false
- return bool(cam.is_current())
-
-
-static func _viewport_current_camera(scene_root: Node) -> Node:
- if scene_root == null:
- return null
- var viewport := scene_root.get_viewport()
- if viewport == null:
- return null
- var current_2d := viewport.get_camera_2d()
- if current_2d != null and scene_root.is_ancestor_of(current_2d):
- return current_2d
- var current_3d := viewport.get_camera_3d()
- if current_3d != null and scene_root.is_ancestor_of(current_3d):
- return current_3d
- return null
-
-
-static func _is_effective_current(cam: Node) -> bool:
- if _is_current(cam):
- return true
- if cam is Camera2D:
- var viewport_2d := cam.get_viewport()
- return viewport_2d != null and viewport_2d.get_camera_2d() == cam
- if cam is Camera3D:
- var viewport_3d := cam.get_viewport()
- return viewport_3d != null and viewport_3d.get_camera_3d() == cam
- return false
-
-
-# Logical-current bookkeeping. Updated from inside _apply_make_current /
-# _apply_clear_current so DO and UNDO callables stamp the same logical
-# slot they touch in the viewport. Reads consult the logical slot first
-# and treat it as authoritative when set — the viewport read is the
-# fallback for "MCP never touched this scene's cameras."
-
-func _set_logical_current(cam: Node) -> void:
- if cam == null or not is_instance_valid(cam) or not cam.is_inside_tree():
- return
- var type_str := _camera_type_str(cam)
- if type_str.is_empty():
- return
- var scene_root := EditorInterface.get_edited_scene_root()
- if scene_root == null or not scene_root.is_ancestor_of(cam):
- return
- var slot: Dictionary = _logical_current.get(scene_root.get_instance_id(), {})
- slot[type_str] = McpScenePath.from_node(cam, scene_root)
- _logical_current[scene_root.get_instance_id()] = slot
-
-
-func _clear_logical_current(cam: Node) -> void:
- if cam == null:
- return
- var type_str := _camera_type_str(cam)
- if type_str.is_empty():
- return
- var scene_root := EditorInterface.get_edited_scene_root()
- if scene_root == null:
- return
- var key := scene_root.get_instance_id()
- if not _logical_current.has(key):
- return
- var slot: Dictionary = _logical_current[key]
- if not slot.has(type_str):
- return
- # Only clear if the logical slot still points at this camera; otherwise
- # a later make_current already took the slot and we'd stomp it.
- var current_path := ""
- if is_instance_valid(cam) and cam.is_inside_tree() and scene_root.is_ancestor_of(cam):
- current_path = McpScenePath.from_node(cam, scene_root)
- if String(slot[type_str]) == current_path:
- slot.erase(type_str)
- if slot.is_empty():
- _logical_current.erase(key)
- else:
- _logical_current[key] = slot
-
-
-func _logical_current_camera(scene_root: Node, type_str: String = "") -> Node:
- if scene_root == null:
- return null
- var key := scene_root.get_instance_id()
- if not _logical_current.has(key):
- return null
- var slot: Dictionary = _logical_current[key]
- var types: Array[String] = []
- if type_str == "2d" or type_str == "3d":
- types = [type_str]
- else:
- types = ["2d", "3d"]
- for t in types:
- if not slot.has(t):
- continue
- var path := String(slot[t])
- if path.is_empty():
- slot.erase(t)
- continue
- var node := McpScenePath.resolve(path, scene_root)
- if node == null or not _is_camera(node) or _camera_type_str(node) != t:
- slot.erase(t)
- continue
- return node
- if slot.is_empty():
- _logical_current.erase(key)
- else:
- _logical_current[key] = slot
- return null
-
-
-# Public introspection for tests that need to distinguish "handler has a
-# logical marker" from "handler is falling back to engine state". `get_camera`
-# / `list_cameras` both use `_resolve_current` which falls through to
-# `_is_effective_current` when no marker is set — that's correct for callers
-# but masks the marker presence from anyone trying to gate on
-# "did the handler actually record this state?". Returns the logical-current
-# Camera2D / Camera3D for the given type ("2d" / "3d" / "" for either), or
-# null when no marker is set. See #316 PR #372 review feedback.
-func peek_logical_current(scene_root: Node, type_str: String = "") -> Node:
- return _logical_current_camera(scene_root, type_str)
-
-
-# Authoritative answer for "is `cam` the current camera of its class?"
-#
-# When a logical marker exists for the camera's class, it is the single
-# source of truth — only the marker's referenced camera reports current,
-# every other camera of that class reports false even if the viewport
-# slot still points at one of them (the headless-CI lag in #140 / #278 /
-# #301). Without a logical marker, fall through to the viewport read so
-# scenes MCP never touched still answer correctly.
-func _resolve_current(scene_root: Node, cam: Node) -> bool:
- if scene_root == null or cam == null:
- return false
- var logical := _logical_current_camera(scene_root, _camera_type_str(cam))
- if logical != null:
- return logical == cam
- return _is_effective_current(cam)
-
-
-# list_cameras pre-fetches the per-class logical pointers once; this
-# variant takes those pointers to avoid an O(n²) walk over the meta
-# bookkeeping for each camera in the scene.
-func _resolve_current_with_logicals(cam: Node, logical_2d: Node, logical_3d: Node) -> bool:
- if cam == null:
- return false
- if cam is Camera2D:
- if logical_2d != null:
- return logical_2d == cam
- elif cam is Camera3D:
- if logical_3d != null:
- return logical_3d == cam
- return _is_effective_current(cam)
-
-
-# Register a current=true switch on `node` in the open undo action,
-# unmarking previously-current siblings of the same class so a single
-# Ctrl-Z reverts the whole switch.
-#
-# Both DO and UNDO route through `_apply_make_current` / `_apply_clear_current`
-# on the handler itself rather than calling Camera.make_current() directly.
-# The helpers do the make_current (or clear_current) call and then verify the
-# viewport slot synchronously. CI used to report `is_current() == false`
-# immediately after a committed make_current (#140 / #278 / #301 / #316); that
-# was never engine lag — Camera2D's current state is a synchronous viewport
-# read — but `Camera2D.make_current()` being silently dropped by the engine's
-# group-call skip set. See `_broadcast_make_current_2d` for the mechanism and
-# the deterministic repair.
-#
-# Because those callables bind to `self` (a RefCounted handler, not a scene
-# node), every action that calls this helper must pin its history via
-# `create_action(name, MERGE_DISABLE, scene_root)` — otherwise the
-# handler-bound ops land in GLOBAL_HISTORY while the scene-node ops land in
-# the scene's history, and a single editor_undo reverts only half the action.
-#
-# Both DO and UNDO use a single make_current() call — never a
-# clear_current() + make_current() pair. make_current() takes over the
-# viewport slot atomically (Godot enforces one current camera per class
-# per viewport), so the displaced camera naturally returns
-# is_current() == false without an explicit clear. The two-step approach
-# leaves the viewport temporarily with no current camera between the
-# clear and the make, which races with editor cleanup on macOS headless
-# (observed flaking CI runs 24674252085, 24675424785).
-func _add_make_current_to_action(node: Node, type_str: String, scene_root: Node) -> void:
- var prev_current: Node = null
- for cam in _list_cameras_in_scene(scene_root, type_str):
- if cam == node:
- continue
- if _resolve_current(scene_root, cam):
- prev_current = cam
- break
- _undo_redo.add_do_method(self, "_apply_make_current", node)
- if prev_current != null:
- _undo_redo.add_undo_method(self, "_apply_make_current", prev_current)
- else:
- _undo_redo.add_undo_method(self, "_apply_clear_current", node)
-
-
-# Apply make_current on `cam` and verify the viewport slot. Registered as the
-# do/undo callable by `_add_make_current_to_action`. Safe against a freed
-# camera node — short-circuits if the node is gone or not in the tree.
-#
-# Everything here is synchronous: `Camera2D.is_current()` is literally
-# `viewport->get_camera_2d() == this` and `Camera3D.is_current()` reads the
-# node's own flag while it is part of the edited scene, so there is no engine
-# state that a delay or a retry could wait for. The one way `make_current()`
-# fails is the group-call skip described on `_broadcast_make_current_2d`, and
-# that is repaired in place.
-func _apply_make_current(cam: Node) -> void:
- if cam == null or not is_instance_valid(cam) or not cam.is_inside_tree():
- return
- _set_logical_current(cam)
- var scene_root := EditorInterface.get_edited_scene_root()
- var type_str := _camera_type_str(cam)
- cam.make_current()
- if cam is Camera2D and not _is_current_settled(cam):
- _broadcast_make_current_2d(cam as Camera2D, scene_root)
- _force_camera_refresh(cam)
- # In the editor a displaced Camera3D keeps its own `current` flag (the
- # viewport's LOST_CURRENT notification does not clear it), so it still
- # answers is_current() == true. Sweep same-class siblings and clear them.
- _force_clear_other_currents(cam, type_str, scene_root)
-
-
-# `Camera2D.make_current()` does not touch the viewport itself: it calls
-# `SceneTree.call_group("__cameras_", "_make_current", self)` and
-# every camera in the group sets or releases `Viewport.camera_2d` in
-# `_make_current(which)`. `call_group` skips any node whose pointer is in
-# `SceneTree::nodes_removed_on_group_call` — a raw-pointer set of every node
-# removed from the tree since the current `_process()` pass (or the outermost
-# group call) began, cleared only when that pass unwinds. A camera allocated
-# into the address of a node removed and freed earlier in the same frame — a
-# `memdelete`d undo reference, a `free()`d node, an editor Control rebuild —
-# is therefore silently skipped: `make_current()` returns, the slot never
-# changes, and `is_current()` stays false while the handler's own bookkeeping
-# says current. Reproduced deterministically on 4.7.2 (49/50 iterations with
-# a same-frame `free()`); this is the "engine-state lag" behind #140 / #278 /
-# #301 / #316. Camera3D is not affected: it writes the viewport directly.
-#
-# Repair by replicating the broadcast with the same bound `_make_current`
-# method the engine's group call invokes: every other Camera2D in the scene
-# releases the slot, then the target claims it. `_make_current` is engine
-# API, not documented — guard on `has_method` so a future engine that drops
-# it degrades to the plain `make_current()` result instead of erroring.
-func _broadcast_make_current_2d(cam: Camera2D, scene_root: Node) -> void:
- if not cam.has_method("_make_current"):
- return
- if scene_root != null:
- for other in _list_cameras_in_scene(scene_root, "2d"):
- if other == cam or not is_instance_valid(other) or not other.is_inside_tree():
- continue
- if other.has_method("_make_current"):
- other.call("_make_current", cam)
- cam.call("_make_current", cam)
-
-
-# Walk same-class siblings and force-clear any that still report is_current().
-# Best-effort: clear_current errors when called on a non-current camera, so
-# guard. Camera2D's clear_current path also flushes the viewport slot, which
-# is the one we actually care about settling for #301.
-func _force_clear_other_currents(target: Node, type_str: String, scene_root: Node) -> void:
- if scene_root == null or type_str.is_empty():
- return
- for sibling in _list_cameras_in_scene(scene_root, type_str):
- if sibling == target:
- continue
- if not is_instance_valid(sibling) or not sibling.is_inside_tree():
- continue
- if not _is_current(sibling):
- # Even if is_current() reports false, the viewport slot can
- # still point at this sibling on macOS — re-make target to
- # take it back. Cheap (idempotent) when the slot is fine.
- if sibling is Camera2D:
- var vp_other: Viewport = (sibling as Camera2D).get_viewport()
- if vp_other != null and vp_other.get_camera_2d() == sibling:
- target.make_current()
- _force_camera_refresh(target)
- continue
- sibling.clear_current()
- if sibling is Camera2D:
- (sibling as Camera2D).force_update_scroll()
-
-
-# Call after commit_action() whenever the action registered a make_current DO.
-# The undo path cannot use a post-undo hook, so it relies on `_apply_make_current`
-# directly; create/configure/apply_preset get this extra post-commit verifier.
-func _verify_current_after_commit(node: Node) -> void:
- _apply_make_current(node)
-
-
-func _force_camera_refresh(cam: Node) -> void:
- if cam is Camera2D:
- (cam as Camera2D).force_update_scroll()
-
-
-func _is_current_settled(cam: Node) -> bool:
- if not _is_current(cam):
- return false
- if cam is Camera2D:
- var viewport := cam.get_viewport()
- if viewport != null and viewport.get_camera_2d() != cam:
- return false
- return true
-
-
-# Symmetric counterpart to `_apply_make_current` for the "no previous
-# current camera" branch (create_camera with make_current=true and no
-# sibling was current). clear_current errors in Godot if called on a
-# non-current camera, so guard on is_current first.
-func _apply_clear_current(cam: Node) -> void:
- if cam == null or not is_instance_valid(cam) or not cam.is_inside_tree():
- return
- _clear_logical_current(cam)
- for attempt in range(_CURRENT_SETTLE_ATTEMPTS):
- if _is_clear_settled(cam):
- return
- if _is_current(cam):
- cam.clear_current()
- _force_camera_refresh(cam)
- # Camera2D-only: is_current() may answer false while the viewport
- # slot still points at cam. Toggle enabled to force the viewport
- # to release, then restore.
- if cam is Camera2D:
- var vp := cam.get_viewport()
- if vp != null and vp.get_camera_2d() == cam:
- var was_enabled := (cam as Camera2D).enabled
- if was_enabled:
- (cam as Camera2D).enabled = false
- _force_camera_refresh(cam)
- if was_enabled:
- (cam as Camera2D).enabled = true
- if _is_clear_settled(cam):
- return
- if attempt < _CURRENT_SETTLE_ATTEMPTS - 1:
- OS.delay_msec(_CURRENT_SETTLE_DELAY_MSEC)
-
-
-func _is_clear_settled(cam: Node) -> bool:
- if cam == null:
- return true
- if _is_current(cam):
- return false
- if cam is Camera2D:
- var vp := cam.get_viewport()
- if vp != null and vp.get_camera_2d() == cam:
- return false
- return true
-
-
-# ============================================================================
-# camera_create
-# ============================================================================
-
-func create_camera(params: Dictionary) -> Dictionary:
- var parent_path: String = params.get("parent_path", "")
- var node_name: String = params.get("name", "Camera")
- var type_str: String = params.get("type", "2d")
- var make_current: bool = bool(params.get("make_current", false))
-
- if not _VALID_TYPES.has(type_str):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid camera type '%s'. Valid: %s" % [type_str, ", ".join(_VALID_TYPES.keys())]
- )
-
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- var parent: Node = scene_root
- if not parent_path.is_empty():
- parent = McpScenePath.resolve(parent_path, scene_root)
- if parent == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, McpScenePath.format_parent_error(parent_path, scene_root))
-
- var node := _instantiate_camera(type_str)
- if node == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate camera")
- if not node_name.is_empty():
- node.name = node_name
-
- _undo_redo.create_action(
- "MCP: Create %s '%s'" % [_VALID_TYPES[type_str], node.name],
- UndoRedo.MERGE_DISABLE, scene_root
- )
- _undo_redo.add_do_method(parent, "add_child", node, true)
- _undo_redo.add_do_method(node, "set_owner", scene_root)
- _undo_redo.add_do_reference(node)
- if make_current:
- # Must land AFTER add_child: making current before the node is in the
- # tree is a silent no-op on the viewport.
- _add_make_current_to_action(node, type_str, scene_root)
- _undo_redo.add_undo_method(parent, "remove_child", node)
- _undo_redo.commit_action()
- if make_current:
- _verify_current_after_commit(node)
-
- return {
- "data": {
- "path": McpScenePath.from_node(node, scene_root),
- "parent_path": McpScenePath.from_node(parent, scene_root),
- "name": String(node.name),
- "type": type_str,
- "class": _VALID_TYPES[type_str],
- "current": bool(make_current),
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# camera_configure
-# ============================================================================
-
-func configure(params: Dictionary) -> Dictionary:
- var resolved := _resolve_camera(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- var type_str: String = resolved.type
- var scene_root: Node = resolved.scene_root
-
- var properties: Dictionary = params.get("properties", {})
- if properties.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "properties dict is empty")
-
- var valid_keys: Array = _KEYS_2D if type_str == "2d" else _KEYS_3D
- var prop_types := _property_type_map(node)
- var coerced: Dictionary = {}
- var old_values: Dictionary = {}
- # `current` is special-cased via methods (Camera2D doesn't expose it as a property).
- var current_request: Variant = null
-
- for property in properties:
- var prop_name: String = String(property)
- if not (prop_name in valid_keys):
- var msg := "Property '%s' not valid for %s. Valid: %s" % [
- prop_name, _VALID_TYPES[type_str], ", ".join(valid_keys)
- ]
- if prop_name in _NODE_TRANSFORM_KEYS:
- msg += (
- ". Transforms live on the Node, not on the camera config — "
- + "use node_set_property(path=%s, property=\"%s\", value=...)" % [node_path, prop_name]
- )
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, msg)
- if prop_name == "current":
- current_request = bool(properties[prop_name])
- continue
- var prop_type: int = prop_types.get(prop_name, TYPE_NIL)
- if prop_type == TYPE_NIL:
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Property '%s' not present on %s" % [prop_name, node.get_class()]
- )
- var coerce_result := CameraValues.coerce(prop_name, properties[prop_name], prop_type)
- if not coerce_result.ok:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerce_result.error))
- coerced[prop_name] = coerce_result.value
- old_values[prop_name] = node.get(prop_name)
-
- _undo_redo.create_action(
- "MCP: Configure camera %s" % node.name,
- UndoRedo.MERGE_DISABLE, scene_root
- )
- for prop_name in coerced:
- _undo_redo.add_do_property(node, prop_name, coerced[prop_name])
- _undo_redo.add_undo_property(node, prop_name, old_values[prop_name])
- var verify_current_after := false
- if current_request != null:
- var want_on: bool = bool(current_request)
- var was_on: bool = _resolve_current(scene_root, node)
- if want_on and not was_on:
- _add_make_current_to_action(node, type_str, scene_root)
- verify_current_after = true
- elif not want_on and was_on:
- _undo_redo.add_do_method(self, "_apply_clear_current", node)
- _undo_redo.add_undo_method(self, "_apply_make_current", node)
- _undo_redo.commit_action()
- if verify_current_after:
- _verify_current_after_commit(node)
-
- var applied: Array[String] = []
- var serialized: Dictionary = {}
- for prop_name in coerced:
- applied.append(prop_name)
- serialized[prop_name] = CameraValues.serialize(coerced[prop_name])
- if current_request != null:
- applied.append("current")
- serialized["current"] = bool(current_request)
-
- return {
- "data": {
- "path": node_path,
- "type": type_str,
- "class": node.get_class(),
- "applied": applied,
- "values": serialized,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# camera_set_limits_2d
-# ============================================================================
-
-func set_limits_2d(params: Dictionary) -> Dictionary:
- var resolved := _resolve_camera(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- var type_str: String = resolved.type
-
- if type_str != "2d":
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "camera_set_limits_2d requires a Camera2D (got %s)" % node.get_class()
- )
-
- var applied: Dictionary = {}
- var old_values: Dictionary = {}
- var edges := {
- "left": "limit_left",
- "right": "limit_right",
- "top": "limit_top",
- "bottom": "limit_bottom",
- }
- for edge in edges:
- var v = params.get(edge)
- if v != null:
- var prop_name: String = edges[edge]
- applied[prop_name] = int(v)
- old_values[prop_name] = node.get(prop_name)
-
- var smoothed = params.get("smoothed")
- if smoothed != null:
- applied["limit_smoothed"] = bool(smoothed)
- old_values["limit_smoothed"] = node.get("limit_smoothed")
-
- if applied.is_empty():
- return ErrorCodes.make(
- ErrorCodes.MISSING_REQUIRED_PARAM,
- "No limits specified; provide at least one of left, right, top, bottom, smoothed"
- )
-
- _undo_redo.create_action("MCP: Set camera limits on %s" % node.name)
- for prop_name in applied:
- _undo_redo.add_do_property(node, prop_name, applied[prop_name])
- _undo_redo.add_undo_property(node, prop_name, old_values[prop_name])
- _undo_redo.commit_action()
-
- var values: Dictionary = {}
- for prop_name in applied:
- values[prop_name] = applied[prop_name]
-
- return {
- "data": {
- "path": node_path,
- "applied": applied.keys(),
- "values": values,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# camera_set_damping_2d
-# ============================================================================
-
-func set_damping_2d(params: Dictionary) -> Dictionary:
- var resolved := _resolve_camera(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- var type_str: String = resolved.type
-
- if type_str != "2d":
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "camera_set_damping_2d requires a Camera2D (got %s)" % node.get_class()
- )
-
- var applied: Dictionary = {}
- var old_values: Dictionary = {}
-
- # position_speed: set position_smoothing_speed AND toggle position_smoothing_enabled.
- var pos_v = params.get("position_speed")
- if pos_v != null:
- var pos_speed := float(pos_v)
- var pos_enable := pos_speed > 0.0
- applied["position_smoothing_enabled"] = pos_enable
- old_values["position_smoothing_enabled"] = node.get("position_smoothing_enabled")
- if pos_enable:
- applied["position_smoothing_speed"] = pos_speed
- old_values["position_smoothing_speed"] = node.get("position_smoothing_speed")
-
- # rotation_speed: same pattern for rotation_smoothing_*.
- var rot_v = params.get("rotation_speed")
- if rot_v != null:
- var rot_speed := float(rot_v)
- var rot_enable := rot_speed > 0.0
- applied["rotation_smoothing_enabled"] = rot_enable
- old_values["rotation_smoothing_enabled"] = node.get("rotation_smoothing_enabled")
- if rot_enable:
- applied["rotation_smoothing_speed"] = rot_speed
- old_values["rotation_smoothing_speed"] = node.get("rotation_smoothing_speed")
-
- for flag in ["drag_horizontal_enabled", "drag_vertical_enabled"]:
- var flag_v = params.get(flag)
- if flag_v != null:
- applied[flag] = bool(flag_v)
- old_values[flag] = node.get(flag)
-
- # drag_margins: dict {left, top, right, bottom} floats in [0,1]; null/missing keys untouched.
- var margins_v = params.get("drag_margins")
- if margins_v != null:
- if not (margins_v is Dictionary):
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "drag_margins must be a dict with optional keys left/top/right/bottom"
- )
- var margins: Dictionary = margins_v
- for edge in _DAMPING_MARGIN_KEYS:
- var margin_v = margins.get(edge)
- if margin_v == null:
- continue
- var v := float(margin_v)
- if v < 0.0 or v > 1.0:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "drag_margins.%s must be in [0, 1] (got %s)" % [edge, v]
- )
- var prop_name: String = "drag_%s_margin" % edge
- applied[prop_name] = v
- old_values[prop_name] = node.get(prop_name)
-
- if applied.is_empty():
- return ErrorCodes.make(
- ErrorCodes.MISSING_REQUIRED_PARAM,
- "No damping params specified; provide at least one of position_speed, rotation_speed, drag_margins, drag_horizontal_enabled, drag_vertical_enabled"
- )
-
- _undo_redo.create_action("MCP: Set camera damping on %s" % node.name)
- for prop_name in applied:
- _undo_redo.add_do_property(node, prop_name, applied[prop_name])
- _undo_redo.add_undo_property(node, prop_name, old_values[prop_name])
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "applied": applied.keys(),
- "values": applied,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# camera_follow_2d
-# ============================================================================
-
-func follow_2d(params: Dictionary) -> Dictionary:
- var resolved := _resolve_camera(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- var type_str: String = resolved.type
- var scene_root: Node = resolved.scene_root
-
- if type_str != "2d":
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "camera_follow_2d requires a Camera2D (got %s)" % node.get_class()
- )
-
- var target_path: String = params.get("target_path", "")
- if target_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: target_path")
- var target := McpScenePath.resolve(target_path, scene_root)
- if target == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND,
- "target_path: %s" % McpScenePath.format_node_error(target_path, scene_root))
- if not (target is Node2D) and target != scene_root:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Follow target must be a Node2D (got %s)" % target.get_class()
- )
- if target == node:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "Camera cannot follow itself")
- if target.is_ancestor_of(node) and node.get_parent() != target:
- # A non-parent ancestor — still valid to reparent under (direct parent).
- pass
- if node.is_ancestor_of(target):
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Cannot follow a descendant of the camera"
- )
-
- var smoothing_speed := float(params.get("smoothing_speed", 5.0))
- var zero_transform: bool = bool(params.get("zero_transform", true))
-
- var old_parent := node.get_parent()
- var old_idx: int = node.get_index() if old_parent != null else 0
- var old_position = node.get("position")
- var old_rotation = node.get("rotation")
- var old_smoothing_enabled: bool = bool(node.get("position_smoothing_enabled"))
- var old_smoothing_speed: float = float(node.get("position_smoothing_speed"))
-
- var already_child: bool = old_parent == target
- var reparented: bool = not already_child
-
- _undo_redo.create_action("MCP: Camera follow %s" % target.name)
- if reparented:
- _undo_redo.add_do_method(old_parent, "remove_child", node)
- _undo_redo.add_do_method(target, "add_child", node, true)
- _undo_redo.add_do_method(node, "set_owner", scene_root)
- _undo_redo.add_do_reference(node)
- if zero_transform:
- if target is Node2D:
- _undo_redo.add_do_property(node, "position", Vector2.ZERO)
- _undo_redo.add_undo_property(node, "position", old_position)
- _undo_redo.add_do_property(node, "rotation", 0.0)
- _undo_redo.add_undo_property(node, "rotation", old_rotation)
- _undo_redo.add_do_property(node, "position_smoothing_enabled", true)
- _undo_redo.add_undo_property(node, "position_smoothing_enabled", old_smoothing_enabled)
- if smoothing_speed > 0.0:
- _undo_redo.add_do_property(node, "position_smoothing_speed", smoothing_speed)
- _undo_redo.add_undo_property(node, "position_smoothing_speed", old_smoothing_speed)
- if reparented:
- _undo_redo.add_undo_method(target, "remove_child", node)
- _undo_redo.add_undo_method(old_parent, "add_child", node, true)
- _undo_redo.add_undo_method(old_parent, "move_child", node, old_idx)
- _undo_redo.add_undo_method(node, "set_owner", scene_root)
- _undo_redo.add_undo_reference(node)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": McpScenePath.from_node(node, scene_root),
- "target_path": McpScenePath.from_node(target, scene_root),
- "reparented": reparented,
- "smoothing_speed": smoothing_speed,
- "zero_transform": zero_transform and (target is Node2D),
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# camera_get
-# ============================================================================
-
-func get_camera(params: Dictionary) -> Dictionary:
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- var camera_path: String = params.get("camera_path", "")
- var node: Node = null
- var resolved_via: String = ""
- if camera_path.is_empty():
- # Empty: prefer the viewport's active camera. In headless editor CI,
- # Camera2D.is_current() can lag make_current() briefly even after the
- # viewport slot has switched; falling through to "first" during that
- # window makes camera_get("") nondeterministic.
- var all_cams := _list_cameras_in_scene(scene_root, "")
- var logical_current := _logical_current_camera(scene_root)
- if logical_current != null and all_cams.has(logical_current):
- node = logical_current
- resolved_via = "current"
- var viewport_current := _viewport_current_camera(scene_root)
- if node == null and viewport_current != null and all_cams.has(viewport_current):
- node = viewport_current
- resolved_via = "current"
- for cam in all_cams:
- if node != null:
- break
- if _is_current(cam):
- node = cam
- resolved_via = "current"
- break
- if node == null and not all_cams.is_empty():
- node = all_cams[0]
- resolved_via = "first"
- else:
- node = McpScenePath.resolve(camera_path, scene_root)
- if node == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(camera_path, scene_root))
- if not _is_camera(node):
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Node %s is not a camera (got %s)" % [camera_path, node.get_class()]
- )
- resolved_via = "path"
-
- if node == null:
- return {
- "data": {
- "path": "",
- "type": "",
- "class": "",
- "current": false,
- "properties": {},
- "resolved_via": "not_found",
- }
- }
-
- var type_str := _camera_type_str(node)
- var keys: Array = _KEYS_2D if type_str == "2d" else _KEYS_3D
- var prop_types := _property_type_map(node)
- var props: Dictionary = {}
- var is_current_effective := _resolve_current(scene_root, node)
- for key in keys:
- if key == "current":
- props[key] = is_current_effective
- continue
- if prop_types.has(key):
- props[key] = CameraValues.serialize(node.get(key))
-
- return {
- "data": {
- "path": McpScenePath.from_node(node, scene_root),
- "type": type_str,
- "class": node.get_class(),
- "current": is_current_effective,
- "properties": props,
- "resolved_via": resolved_via,
- }
- }
-
-
-# ============================================================================
-# camera_list
-# ============================================================================
-
-func list_cameras(_params: Dictionary) -> Dictionary:
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- var cams := _list_cameras_in_scene(scene_root, "")
- var out: Array[Dictionary] = []
- var logical_2d := _logical_current_camera(scene_root, "2d")
- var logical_3d := _logical_current_camera(scene_root, "3d")
- for cam in cams:
- out.append({
- "path": McpScenePath.from_node(cam, scene_root),
- "class": cam.get_class(),
- "type": _camera_type_str(cam),
- "current": _resolve_current_with_logicals(cam, logical_2d, logical_3d),
- })
- return {"data": {"cameras": out}}
-
-
-# ============================================================================
-# camera_apply_preset
-# ============================================================================
-
-func apply_preset(params: Dictionary) -> Dictionary:
- var preset_name: String = params.get("preset", "")
- if preset_name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: preset")
-
- var overrides: Dictionary = params.get("overrides", {})
- var blueprint = CameraPresets.build(preset_name, overrides)
- if blueprint == null:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Unknown preset '%s'. Valid: %s" % [preset_name, ", ".join(CameraPresets.list_presets())]
- )
-
- var parent_path: String = params.get("parent_path", "")
- var node_name: String = params.get("name", "")
- var type_str: String = params.get("type", String(blueprint.get("default_type", "2d")))
- var make_current: bool = bool(params.get("make_current", true))
- if node_name.is_empty():
- node_name = preset_name.capitalize()
- if not _VALID_TYPES.has(type_str):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid camera type '%s'. Valid: %s" % [type_str, ", ".join(_VALID_TYPES.keys())]
- )
-
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- var parent: Node = scene_root
- if not parent_path.is_empty():
- parent = McpScenePath.resolve(parent_path, scene_root)
- if parent == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, McpScenePath.format_parent_error(parent_path, scene_root))
-
- var node := _instantiate_camera(type_str)
- node.name = node_name
-
- var preset_props: Dictionary = blueprint.get("properties", {})
- var valid_keys: Array = _KEYS_2D if type_str == "2d" else _KEYS_3D
- var prop_types := _property_type_map(node)
- var applied: Array[String] = []
- for prop in preset_props:
- var prop_name := String(prop)
- if not (prop_name in valid_keys):
- continue # Silently skip preset keys that don't apply to this camera class.
- # `current` lives on methods, not as a writable property on Camera2D —
- # always handled via the make_current path below.
- if prop_name == "current":
- continue
- var prop_type: int = prop_types.get(prop_name, TYPE_NIL)
- if prop_type == TYPE_NIL:
- continue
- var coerce_result := CameraValues.coerce(prop_name, preset_props[prop_name], prop_type)
- if not coerce_result.ok:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerce_result.error))
- node.set(prop_name, coerce_result.value)
- applied.append(prop_name)
-
- _undo_redo.create_action(
- "MCP: Apply camera preset %s" % preset_name,
- UndoRedo.MERGE_DISABLE, scene_root
- )
- _undo_redo.add_do_method(parent, "add_child", node, true)
- _undo_redo.add_do_method(node, "set_owner", scene_root)
- _undo_redo.add_do_reference(node)
- if make_current:
- _add_make_current_to_action(node, type_str, scene_root)
- _undo_redo.add_undo_method(parent, "remove_child", node)
- _undo_redo.commit_action()
- if make_current:
- _verify_current_after_commit(node)
-
- return {
- "data": {
- "path": McpScenePath.from_node(node, scene_root),
- "parent_path": McpScenePath.from_node(parent, scene_root),
- "name": node_name,
- "preset": preset_name,
- "type": type_str,
- "class": _VALID_TYPES[type_str],
- "applied": applied,
- "current": bool(make_current),
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# Helpers
-# ============================================================================
-
-static func _instantiate_camera(type_str: String) -> Node:
- match type_str:
- "2d":
- return Camera2D.new()
- "3d":
- return Camera3D.new()
- return null
-
-
-static func _is_camera(node: Node) -> bool:
- return node is Camera2D or node is Camera3D
-
-
-static func _camera_type_str(node: Node) -> String:
- if node is Camera2D:
- return "2d"
- if node is Camera3D:
- return "3d"
- return ""
-
-
-func _resolve_camera(params: Dictionary) -> Dictionary:
- var resolved := McpNodeValidator.resolve_or_error(
- params.get("camera_path", ""), "camera_path",
- )
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- var scene_root: Node = resolved.scene_root
- if not _is_camera(node):
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Node %s is not a camera (got %s)" % [node_path, node.get_class()]
- )
- return {
- "node": node,
- "path": node_path,
- "type": _camera_type_str(node),
- "scene_root": scene_root,
- }
-
-
-## Walk the edited scene for cameras. class_filter: "2d", "3d", or "" for all.
-static func _list_cameras_in_scene(scene_root: Node, class_filter: String) -> Array:
- var result: Array = []
- if scene_root == null:
- return result
- _collect_cameras(scene_root, class_filter, result)
- return result
-
-
-static func _collect_cameras(node: Node, class_filter: String, out: Array) -> void:
- var matches := false
- match class_filter:
- "2d":
- matches = node is Camera2D
- "3d":
- matches = node is Camera3D
- _:
- matches = node is Camera2D or node is Camera3D
- if matches:
- out.append(node)
- for child in node.get_children():
- _collect_cameras(child, class_filter, out)
-
-
-## Build a name -> property-type dict from the object's property list.
-## Single walk of get_property_list() amortizes lookups across a batch of
-## properties in configure / apply_preset.
-static func _property_type_map(obj: Object) -> Dictionary:
- var out: Dictionary = {}
- if obj == null:
- return out
- for prop in obj.get_property_list():
- out[prop.name] = int(prop.get("type", TYPE_NIL))
- return out
diff --git a/addons/godot_ai/handlers/camera_handler.gd.uid b/addons/godot_ai/handlers/camera_handler.gd.uid
deleted file mode 100644
index 86d2aae..0000000
--- a/addons/godot_ai/handlers/camera_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c0lcviccrlrl8
diff --git a/addons/godot_ai/handlers/camera_presets.gd b/addons/godot_ai/handlers/camera_presets.gd
deleted file mode 100644
index a1922c2..0000000
--- a/addons/godot_ai/handlers/camera_presets.gd
+++ /dev/null
@@ -1,81 +0,0 @@
-@tool
-extends RefCounted
-
-## Opinionated Camera2D / Camera3D presets.
-##
-## build(preset_name, overrides) -> {default_type, properties} | null
-## properties are merged with caller overrides (overrides win).
-
-
-const _PRESETS := {
- # Top-down roguelite / arena — damped follow feel, drag deadzone.
- "topdown_2d": {
- "default_type": "2d",
- "properties": {
- "zoom": {"x": 2.0, "y": 2.0},
- "anchor_mode": "drag_center",
- "position_smoothing_enabled": true,
- "position_smoothing_speed": 5.0,
- "rotation_smoothing_enabled": false,
- "drag_horizontal_enabled": true,
- "drag_vertical_enabled": true,
- "drag_left_margin": 0.2,
- "drag_right_margin": 0.2,
- "drag_top_margin": 0.2,
- "drag_bottom_margin": 0.2,
- },
- },
- # Platformer — tight horizontal follow, vertical snap with smoothing on.
- "platformer_2d": {
- "default_type": "2d",
- "properties": {
- "zoom": {"x": 1.5, "y": 1.5},
- "anchor_mode": "drag_center",
- "position_smoothing_enabled": true,
- "position_smoothing_speed": 8.0,
- "drag_horizontal_enabled": true,
- "drag_vertical_enabled": false,
- "drag_left_margin": 0.15,
- "drag_right_margin": 0.15,
- },
- },
- # Cinematic 3D — narrow FOV, long range. Good for dramatic wide shots.
- "cinematic_3d": {
- "default_type": "3d",
- "properties": {
- "fov": 40.0,
- "near": 0.1,
- "far": 500.0,
- "projection": "perspective",
- },
- },
- # Action 3D — wider FOV for first/third-person action gameplay.
- "action_3d": {
- "default_type": "3d",
- "properties": {
- "fov": 70.0,
- "near": 0.1,
- "far": 200.0,
- "projection": "perspective",
- },
- },
-}
-
-
-static func list_presets() -> Array:
- return _PRESETS.keys()
-
-
-## Build a preset blueprint. Returns null if preset_name is unknown.
-## overrides is merged on top of preset defaults (caller values win).
-static func build(preset_name: String, overrides: Dictionary) -> Variant:
- if not _PRESETS.has(preset_name):
- return null
- var preset: Dictionary = _PRESETS[preset_name]
- var properties: Dictionary = (preset.get("properties", {}) as Dictionary).duplicate(true)
- for key in overrides:
- properties[key] = overrides[key]
- return {
- "default_type": preset.get("default_type", "2d"),
- "properties": properties,
- }
diff --git a/addons/godot_ai/handlers/camera_presets.gd.uid b/addons/godot_ai/handlers/camera_presets.gd.uid
deleted file mode 100644
index 9f9f839..0000000
--- a/addons/godot_ai/handlers/camera_presets.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bl3rfy72o3wy5
diff --git a/addons/godot_ai/handlers/camera_values.gd b/addons/godot_ai/handlers/camera_values.gd
deleted file mode 100644
index 2ee4865..0000000
--- a/addons/godot_ai/handlers/camera_values.gd
+++ /dev/null
@@ -1,132 +0,0 @@
-@tool
-extends RefCounted
-
-## Value coercion helpers for camera authoring.
-##
-## Handles:
-## - enum-by-name (keep_aspect="keep_height" -> Camera3D.KEEP_HEIGHT)
-## - {x, y} dict -> Vector2 (zoom, offset, drag_*_offset)
-## - serialization back to JSON-friendly shapes
-
-
-const _ENUM_TABLES := {
- "projection": {
- "perspective": Camera3D.PROJECTION_PERSPECTIVE,
- "orthogonal": Camera3D.PROJECTION_ORTHOGONAL,
- "frustum": Camera3D.PROJECTION_FRUSTUM,
- },
- "keep_aspect": {
- "keep_width": Camera3D.KEEP_WIDTH,
- "keep_height": Camera3D.KEEP_HEIGHT,
- },
- "anchor_mode": {
- "fixed_top_left": Camera2D.ANCHOR_MODE_FIXED_TOP_LEFT,
- "drag_center": Camera2D.ANCHOR_MODE_DRAG_CENTER,
- },
- "doppler_tracking": {
- "disabled": Camera3D.DOPPLER_TRACKING_DISABLED,
- "idle_step": Camera3D.DOPPLER_TRACKING_IDLE_STEP,
- "physics_step": Camera3D.DOPPLER_TRACKING_PHYSICS_STEP,
- },
- "process_callback": {
- "physics": Camera2D.CAMERA2D_PROCESS_PHYSICS,
- "idle": Camera2D.CAMERA2D_PROCESS_IDLE,
- },
-}
-
-
-## Return the enum int for (property, string_name), or null if not a known enum string.
-static func resolve_enum(property: String, value: Variant) -> Variant:
- if not (value is String):
- return null
- if not _ENUM_TABLES.has(property):
- return null
- var table: Dictionary = _ENUM_TABLES[property]
- var key: String = String(value).to_lower()
- if table.has(key):
- return table[key]
- return null
-
-
-## Valid enum names for a property, for error messages.
-static func enum_keys(property: String) -> Array:
- if not _ENUM_TABLES.has(property):
- return []
- return (_ENUM_TABLES[property] as Dictionary).keys()
-
-
-static func parse_vector2(value: Variant) -> Variant:
- ## Camera-specific sugar kept from the pre-#714 copy: a bare number is
- ## a uniform zoom, splatted to both axes. Everything else goes through
- ## the canonical strict parser.
- if value is int or value is float:
- return Vector2(float(value), float(value))
- return McpJsonValues.parse_vector2(value)
-
-
-static func parse_vector3(value: Variant) -> Variant:
- return McpJsonValues.parse_vector3(value)
-
-
-## Coerce a JSON-shaped value for a camera property against the declared type.
-## Returns {ok: true, value: ...} or {ok: false, error: "..."}.
-static func coerce(property: String, value: Variant, target_type: int) -> Dictionary:
- # Enum-by-name: must match before generic TYPE_INT coercion.
- if _ENUM_TABLES.has(property):
- if value is String:
- var enum_val = resolve_enum(property, value)
- if enum_val == null:
- return {
- "ok": false,
- "error": "Invalid %s value: '%s'. Valid: %s" % [
- property, value, ", ".join(enum_keys(property))
- ],
- }
- return {"ok": true, "value": int(enum_val)}
- if value is int or value is float:
- return {"ok": true, "value": int(value)}
-
- match target_type:
- TYPE_VECTOR2:
- var v2 = parse_vector2(value)
- if v2 == null:
- return {"ok": false, "error": "Invalid vector2 for %s: %s" % [property, value]}
- return {"ok": true, "value": v2}
- TYPE_VECTOR3:
- var v3 = parse_vector3(value)
- if v3 == null:
- return {"ok": false, "error": "Invalid vector3 for %s: %s" % [property, value]}
- return {"ok": true, "value": v3}
- TYPE_BOOL:
- if value is bool:
- return {"ok": true, "value": value}
- if value is int or value is float:
- return {"ok": true, "value": bool(value)}
- return {"ok": false, "error": "Expected bool for %s" % property}
- TYPE_INT:
- if value is int:
- return {"ok": true, "value": value}
- if value is float:
- return {"ok": true, "value": int(value)}
- return {"ok": false, "error": "Expected int for %s" % property}
- TYPE_FLOAT:
- ## Canonical scalar parser (#964): numeric strings coerce too.
- var parsed: Variant = McpJsonValues.parse_float(value)
- if parsed != null:
- return {"ok": true, "value": parsed}
- return {"ok": false, "error": "Expected number for %s" % property}
- TYPE_STRING:
- return {"ok": true, "value": String(value)}
-
- return {"ok": true, "value": value}
-
-
-## Serialize a Variant into a JSON-friendly shape for responses.
-static func serialize(value: Variant) -> Variant:
- if value == null:
- return null
- if value is Vector2:
- return {"x": value.x, "y": value.y}
- if value is Vector3:
- return {"x": value.x, "y": value.y, "z": value.z}
- return value
diff --git a/addons/godot_ai/handlers/camera_values.gd.uid b/addons/godot_ai/handlers/camera_values.gd.uid
deleted file mode 100644
index 45e3c09..0000000
--- a/addons/godot_ai/handlers/camera_values.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bgjnubgnv6ses
diff --git a/addons/godot_ai/handlers/client_handler.gd b/addons/godot_ai/handlers/client_handler.gd
deleted file mode 100644
index f6df09c..0000000
--- a/addons/godot_ai/handlers/client_handler.gd
+++ /dev/null
@@ -1,58 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Handles MCP client configuration commands.
-
-var _client_jobs
-
-
-func _init(client_jobs = null) -> void:
- _client_jobs = client_jobs
-
-
-func configure_client(params: Dictionary) -> Dictionary:
- return _request_client_action(params, "configure")
-
-
-func remove_client(params: Dictionary) -> Dictionary:
- return _request_client_action(params, "remove")
-
-
-func _request_client_action(params: Dictionary, action: String) -> Dictionary:
- var client_id: String = params.get("client", "")
- if not McpClientConfigurator.has_client(client_id):
- var valid := ", ".join(McpClientConfigurator.client_ids())
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Unknown client: %s. Use one of: %s" % [client_id, valid])
- var request_id: String = params.get("_request_id", "")
- if _client_jobs == null or request_id.is_empty():
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Client mutations must run as top-level deferred commands.",
- )
- var started: Dictionary = _client_jobs.request_mcp_action(request_id, client_id, action)
- if not bool(started.get("ok", false)):
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- str(started.get("error", "Client worker is unavailable.")),
- )
- return {
- "_deferred": true,
- "_deferred_timeout_ms": int(started.get("deferred_timeout_ms", 0)),
- }
-
-
-func check_client_status(params: Dictionary) -> Dictionary:
- var request_id: String = params.get("_request_id", "")
- if _client_jobs == null or request_id.is_empty():
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Client status requires a deferred request context.",
- )
- if not _client_jobs.request_mcp_status(request_id):
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Client status worker is unavailable.",
- )
- return McpDispatcher.DEFERRED_RESPONSE
diff --git a/addons/godot_ai/handlers/client_handler.gd.uid b/addons/godot_ai/handlers/client_handler.gd.uid
deleted file mode 100644
index 25687f5..0000000
--- a/addons/godot_ai/handlers/client_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bmo4foc5fq75c
diff --git a/addons/godot_ai/handlers/command_handler.gd b/addons/godot_ai/handlers/command_handler.gd
deleted file mode 100644
index f7096d4..0000000
--- a/addons/godot_ai/handlers/command_handler.gd
+++ /dev/null
@@ -1,13 +0,0 @@
-@tool
-extends RefCounted
-
-## Built-in handlers execute synchronously, or register every detached
-## coroutine with ScriptWork. Client/vision OS workers belong to the root's
-## worker owners and are joined separately before any scripts are replaced.
-## New async entry points MUST retain their ledger entry until they return,
-## including early returns; a request timeout/disconnect must not clear it.
-const ScriptWork := preload("res://addons/godot_ai/utils/script_work.gd")
-
-
-func quiesce_for_script_swap() -> Dictionary:
- return ScriptWork.quiescence()
diff --git a/addons/godot_ai/handlers/command_handler.gd.uid b/addons/godot_ai/handlers/command_handler.gd.uid
deleted file mode 100644
index 0e99878..0000000
--- a/addons/godot_ai/handlers/command_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://xx6r8yaedry5
diff --git a/addons/godot_ai/handlers/control_draw_recipe_handler.gd b/addons/godot_ai/handlers/control_draw_recipe_handler.gd
deleted file mode 100644
index 2076d67..0000000
--- a/addons/godot_ai/handlers/control_draw_recipe_handler.gd
+++ /dev/null
@@ -1,318 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Handles the control_draw_recipe MCP command. Attaches a shared DrawRecipe
-## script to a Control and stores the caller's ordered draw ops in node
-## metadata under "_ops". The DrawRecipe script dispatches each op to a
-## CanvasItem draw_* call in _draw(). One Ctrl+Z reverts script + meta as a
-## single undo step.
-
-const DRAW_RECIPE_SCRIPT := preload("res://addons/godot_ai/runtime/draw_recipe.gd")
-const UiHandler := preload("res://addons/godot_ai/handlers/ui_handler.gd")
-
-var _undo_redo: EditorUndoRedoManager
-
-
-func _init(undo_redo: EditorUndoRedoManager) -> void:
- _undo_redo = undo_redo
-
-
-func control_draw_recipe(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
- var ops_raw: Variant = params.get("ops", null)
- var clear_existing: bool = bool(params.get("clear_existing", true))
-
- if path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
- if typeof(ops_raw) != TYPE_ARRAY:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "ops must be an Array")
-
- var _resolved := McpNodeValidator.resolve_or_error(path, "path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var scene_root: Node = _resolved.scene_root
- if not node is Control:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "control_draw_recipe requires a Control node, got %s" % node.get_class()
- )
-
- var coerced := _coerce_ops(ops_raw)
- if coerced.has("error"):
- return coerced
- var coerced_ops: Array = coerced.ops
-
- var old_script: Variant = node.get_script()
- if old_script != null and old_script != DRAW_RECIPE_SCRIPT:
- if not clear_existing:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- (
- "Node %s already has a script. Pass clear_existing=true to replace."
- % path
- )
- )
-
- var had_meta := node.has_meta("_ops")
- var old_ops: Variant = node.get_meta("_ops") if had_meta else null
-
- _undo_redo.create_action("MCP: Draw recipe on %s" % node.name)
- _undo_redo.add_do_method(node, "set_script", DRAW_RECIPE_SCRIPT)
- _undo_redo.add_do_method(node, "set_meta", "_ops", coerced_ops)
- _undo_redo.add_do_method(node, "queue_redraw")
- _undo_redo.add_undo_method(node, "set_script", old_script)
- if had_meta:
- _undo_redo.add_undo_method(node, "set_meta", "_ops", old_ops)
- else:
- _undo_redo.add_undo_method(node, "remove_meta", "_ops")
- _undo_redo.add_undo_method(node, "queue_redraw")
- _undo_redo.commit_action()
-
- return {
- "data":
- {
- "path": McpScenePath.from_node(node, scene_root),
- "ops_count": coerced_ops.size(),
- "script_attached": old_script == null,
- "script_replaced": old_script != null and old_script != DRAW_RECIPE_SCRIPT,
- "undoable": true,
- }
- }
-
-
-
-## Validate and coerce every op dict. Returns {"ops": Array} or an error dict.
-func _coerce_ops(ops: Array) -> Dictionary:
- var result: Array = []
- for i in ops.size():
- var op: Variant = ops[i]
- if typeof(op) != TYPE_DICTIONARY:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE, "ops[%d] must be a dictionary" % i
- )
- var coerced := _coerce_single_op(op, i)
- if coerced.has("error"):
- return coerced
- result.append(coerced.op)
- return {"ops": result}
-
-
-func _coerce_single_op(op: Dictionary, idx: int) -> Dictionary:
- var draw_type: String = op.get("draw", "")
- if draw_type.is_empty():
- return ErrorCodes.make(
- ErrorCodes.MISSING_REQUIRED_PARAM, "ops[%d]: missing 'draw' field" % idx
- )
- match draw_type:
- "line":
- return _coerce_line(op, idx)
- "rect":
- return _coerce_rect(op, idx)
- "arc":
- return _coerce_arc(op, idx)
- "circle":
- return _coerce_circle(op, idx)
- "polyline":
- return _coerce_polyline_or_polygon(op, idx, "polyline")
- "polygon":
- return _coerce_polyline_or_polygon(op, idx, "polygon")
- "string":
- return _coerce_string(op, idx)
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "ops[%d]: unknown draw type '%s'" % [idx, draw_type]
- )
-
-
-func _require_fields(op: Dictionary, idx: int, kind: String, fields: Array) -> Dictionary:
- for f in fields:
- if not op.has(f):
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "ops[%d] (%s): missing '%s'" % [idx, kind, f]
- )
- return {}
-
-
-func _coerce_typed(value: Variant, prop_type: int, idx: int, kind: String, field: String) -> Dictionary:
- var r := UiHandler._coerce_for_type(value, prop_type)
- if r.ok:
- return {"ok": true, "value": r.value}
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE, "ops[%d] (%s): invalid '%s'" % [idx, kind, field]
- )
-
-
-func _coerce_line(op: Dictionary, idx: int) -> Dictionary:
- var missing := _require_fields(op, idx, "line", ["from", "to", "color"])
- if missing.has("error"):
- return missing
- var frm := _coerce_typed(op.from, TYPE_VECTOR2, idx, "line", "from")
- if frm.has("error"):
- return frm
- var to_ := _coerce_typed(op.to, TYPE_VECTOR2, idx, "line", "to")
- if to_.has("error"):
- return to_
- var c := _coerce_typed(op.color, TYPE_COLOR, idx, "line", "color")
- if c.has("error"):
- return c
- var out := {"draw": "line", "from": frm.value, "to": to_.value, "color": c.value}
- if op.has("width"):
- out["width"] = float(op.width)
- if op.has("antialiased"):
- out["antialiased"] = bool(op.antialiased)
- return {"op": out}
-
-
-func _coerce_rect(op: Dictionary, idx: int) -> Dictionary:
- var missing := _require_fields(op, idx, "rect", ["rect", "color"])
- if missing.has("error"):
- return missing
- var r := _coerce_typed(op.rect, TYPE_RECT2, idx, "rect", "rect")
- if r.has("error"):
- return r
- var c := _coerce_typed(op.color, TYPE_COLOR, idx, "rect", "color")
- if c.has("error"):
- return c
- var out := {"draw": "rect", "rect": r.value, "color": c.value}
- if op.has("filled"):
- out["filled"] = bool(op.filled)
- if op.has("width"):
- out["width"] = float(op.width)
- return {"op": out}
-
-
-func _coerce_arc(op: Dictionary, idx: int) -> Dictionary:
- var missing := _require_fields(
- op, idx, "arc", ["center", "radius", "start_angle", "end_angle", "color"]
- )
- if missing.has("error"):
- return missing
- var center := _coerce_typed(op.center, TYPE_VECTOR2, idx, "arc", "center")
- if center.has("error"):
- return center
- var c := _coerce_typed(op.color, TYPE_COLOR, idx, "arc", "color")
- if c.has("error"):
- return c
- var out := {
- "draw": "arc",
- "center": center.value,
- "radius": float(op.radius),
- "start_angle": float(op.start_angle),
- "end_angle": float(op.end_angle),
- "color": c.value,
- }
- if op.has("point_count"):
- out["point_count"] = int(op.point_count)
- if op.has("width"):
- out["width"] = float(op.width)
- if op.has("antialiased"):
- out["antialiased"] = bool(op.antialiased)
- return {"op": out}
-
-
-func _coerce_circle(op: Dictionary, idx: int) -> Dictionary:
- var missing := _require_fields(op, idx, "circle", ["center", "radius", "color"])
- if missing.has("error"):
- return missing
- var center := _coerce_typed(op.center, TYPE_VECTOR2, idx, "circle", "center")
- if center.has("error"):
- return center
- var c := _coerce_typed(op.color, TYPE_COLOR, idx, "circle", "color")
- if c.has("error"):
- return c
- return {
- "op":
- {
- "draw": "circle",
- "center": center.value,
- "radius": float(op.radius),
- "color": c.value,
- }
- }
-
-
-func _coerce_polyline_or_polygon(op: Dictionary, idx: int, kind: String) -> Dictionary:
- if not op.has("points"):
- return ErrorCodes.make(
- ErrorCodes.MISSING_REQUIRED_PARAM, "ops[%d] (%s): missing 'points'" % [idx, kind]
- )
- if typeof(op.points) != TYPE_ARRAY:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "ops[%d] (%s): 'points' must be an Array" % [idx, kind]
- )
- var points := PackedVector2Array()
- for j in op.points.size():
- var p := UiHandler._coerce_for_type(op.points[j], TYPE_VECTOR2)
- if not p.ok:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "ops[%d] (%s): points[%d] invalid" % [idx, kind, j]
- )
- points.append(p.value)
-
- var out := {"draw": kind, "points": points}
-
- if op.has("colors"):
- if typeof(op.colors) != TYPE_ARRAY:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "ops[%d] (%s): 'colors' must be an Array" % [idx, kind]
- )
- var colors := PackedColorArray()
- for k in op.colors.size():
- var ck := UiHandler._coerce_for_type(op.colors[k], TYPE_COLOR)
- if not ck.ok:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "ops[%d] (%s): colors[%d] invalid" % [idx, kind, k]
- )
- colors.append(ck.value)
- out["colors"] = colors
- elif op.has("color"):
- var c := UiHandler._coerce_for_type(op.color, TYPE_COLOR)
- if not c.ok:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE, "ops[%d] (%s): invalid 'color'" % [idx, kind]
- )
- out["color"] = c.value
- else:
- return ErrorCodes.make(
- ErrorCodes.MISSING_REQUIRED_PARAM,
- "ops[%d] (%s): missing 'color' or 'colors'" % [idx, kind]
- )
-
- if op.has("width"):
- out["width"] = float(op.width)
- if op.has("antialiased"):
- out["antialiased"] = bool(op.antialiased)
- return {"op": out}
-
-
-func _coerce_string(op: Dictionary, idx: int) -> Dictionary:
- var missing := _require_fields(op, idx, "string", ["position", "text", "color"])
- if missing.has("error"):
- return missing
- var pos := _coerce_typed(op.position, TYPE_VECTOR2, idx, "string", "position")
- if pos.has("error"):
- return pos
- var c := _coerce_typed(op.color, TYPE_COLOR, idx, "string", "color")
- if c.has("error"):
- return c
- var out := {
- "draw": "string",
- "position": pos.value,
- "text": str(op.text),
- "color": c.value,
- }
- if op.has("font_size"):
- out["font_size"] = int(op.font_size)
- if op.has("align"):
- out["align"] = int(op.align)
- if op.has("max_width"):
- out["max_width"] = float(op.max_width)
- return {"op": out}
diff --git a/addons/godot_ai/handlers/control_draw_recipe_handler.gd.uid b/addons/godot_ai/handlers/control_draw_recipe_handler.gd.uid
deleted file mode 100644
index da0aaf9..0000000
--- a/addons/godot_ai/handlers/control_draw_recipe_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://buat1mt0fjlqb
diff --git a/addons/godot_ai/handlers/csg_handler.gd b/addons/godot_ai/handlers/csg_handler.gd
deleted file mode 100644
index b85143d..0000000
--- a/addons/godot_ai/handlers/csg_handler.gd
+++ /dev/null
@@ -1,118 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-## CSG authoring — create CSG shapes (box, sphere, cylinder, torus, prism)
-## and set their boolean operation (union / intersection / subtraction) so
-## agents can carve geometry (holes, caves, tunnels) directly in the editor.
-##
-## All ops target nodes in the currently edited scene by scene-relative
-## path. All write ops are undoable via EditorUndoRedoManager. Sibling CSG
-## shapes under the same parent combine automatically; use a CSGCombiner3D
-## parent when you need explicit grouping.
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-const SHAPES := {
- "box": "CSGBox3D",
- "sphere": "CSGSphere3D",
- "cylinder": "CSGCylinder3D",
- "torus": "CSGTorus3D",
- "polygon": "CSGPolygon3D",
-}
-
-const OPERATIONS := {
- "union": CSGShape3D.OPERATION_UNION,
- "intersection": CSGShape3D.OPERATION_INTERSECTION,
- "subtraction": CSGShape3D.OPERATION_SUBTRACTION,
-}
-
-var _undo_redo: EditorUndoRedoManager
-
-
-func _init(undo_redo: EditorUndoRedoManager) -> void:
- _undo_redo = undo_redo
-
-
-## Create a CSG shape under a Node3D parent.
-## params: {parent_path, name="", shape="box", operation="union"}
-## Returns: {path, name, shape, operation, undoable}
-func create(params: Dictionary) -> Dictionary:
- var parent_path: String = params.get("parent_path", "")
- var shape: String = params.get("shape", "box")
- var operation: String = params.get("operation", "union")
- var scene_file: String = params.get("scene_file", "")
-
- var shape_class: String = SHAPES.get(shape, "")
- if shape_class.is_empty():
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "Unknown shape: %s. Valid shapes: %s" % [shape, ", ".join(SHAPES.keys())])
- if not OPERATIONS.has(operation):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "Unknown operation: %s. Valid operations: %s" % [operation, ", ".join(OPERATIONS.keys())])
-
- var scene_check := McpScenePath.require_edited_scene(scene_file)
- if scene_check.has("error"):
- return scene_check
- var scene_root: Node = scene_check.node
-
- var parent: Node = scene_root
- if not parent_path.is_empty():
- parent = McpScenePath.resolve(parent_path, scene_root)
- if parent == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND,
- McpScenePath.format_parent_error(parent_path, scene_root))
- if not parent is Node3D:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
- "CSG parent must be a Node3D (got %s)" % parent.get_class())
-
- var node: CSGShape3D = ClassDB.instantiate(shape_class)
- if node == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate %s" % shape_class)
- var node_name: String = params.get("name", "")
- if node_name.is_empty():
- node_name = shape_class
- node.name = node_name
- node.operation = OPERATIONS[operation]
-
- _undo_redo.create_action("MCP: Create %s" % node.name)
- _undo_redo.add_do_method(parent, "add_child", node, true)
- _undo_redo.add_do_method(node, "set_owner", scene_root)
- _undo_redo.add_do_reference(node)
- _undo_redo.add_undo_method(parent, "remove_child", node)
- _undo_redo.commit_action()
-
- return {"data": {
- "path": McpScenePath.from_node(node, scene_root),
- "name": node.name,
- "shape": shape,
- "operation": operation,
- "undoable": true,
- }}
-
-
-## Set the boolean operation of a CSG shape.
-## params: {path, operation}
-## Returns: {operation, undoable}
-func set_operation(params: Dictionary) -> Dictionary:
- var operation: String = params.get("operation", "")
- if not OPERATIONS.has(operation):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "Unknown operation: %s. Valid operations: %s" % [operation, ", ".join(OPERATIONS.keys())])
- var resolved := McpNodeValidator.resolve_or_error(
- params.get("path", ""), "path", params.get("scene_file", ""))
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- if not node is CSGShape3D:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
- "Node is not a CSGShape3D: %s" % params.get("path", ""))
- var shape: CSGShape3D = node
- var prev: int = shape.operation
- var next: int = OPERATIONS[operation]
- ## Target the node in both callbacks so the action lands in the scene
- ## undo history (first-target routing); `set` exists on every Object.
- _undo_redo.create_action("MCP: CSG set_operation")
- _undo_redo.add_do_method(shape, "set", "operation", next)
- _undo_redo.add_undo_method(shape, "set", "operation", prev)
- _undo_redo.commit_action()
- return {"data": {"operation": operation, "undoable": true}}
diff --git a/addons/godot_ai/handlers/csg_handler.gd.uid b/addons/godot_ai/handlers/csg_handler.gd.uid
deleted file mode 100644
index 0f7b976..0000000
--- a/addons/godot_ai/handlers/csg_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c4mb0tptu52x2
diff --git a/addons/godot_ai/handlers/curve_handler.gd b/addons/godot_ai/handlers/curve_handler.gd
deleted file mode 100644
index fb7c593..0000000
--- a/addons/godot_ai/handlers/curve_handler.gd
+++ /dev/null
@@ -1,243 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Replaces all points on a Curve / Curve2D / Curve3D resource. The point
-## list shape depends on resource type (see `set_points` for the schemas).
-##
-## Dedicated tool rather than a property set because Curve2D/Curve3D.add_point
-## is a method call, not a property — resource_create's `properties` dict can't
-## reach it.
-
-const NodeHandler := preload("res://addons/godot_ai/handlers/node_handler.gd")
-
-var _undo_redo: EditorUndoRedoManager
-var _connection: McpConnection
-
-
-func _init(undo_redo: EditorUndoRedoManager, connection: McpConnection = null) -> void:
- _undo_redo = undo_redo
- _connection = connection
-
-
-func set_points(params: Dictionary) -> Dictionary:
- var node_path: String = params.get("path", "")
- var property: String = params.get("property", "")
- var resource_path: String = params.get("resource_path", "")
- var new_points: Array = params.get("points", [])
-
- var home_err := McpResourceIO.validate_home(params)
- if home_err != null:
- return home_err
- var has_file_target := not resource_path.is_empty()
- if not (new_points is Array):
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "points must be an array")
-
- var curve: Resource
- var node: Node = null
- var curve_created := false
- if has_file_target:
- var rpath_err = McpPathValidator.loadable_error(resource_path, "resource_path")
- if rpath_err != null:
- return rpath_err
- if not ResourceLoader.exists(resource_path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s" % resource_path)
- # ResourceLoader.load() returns Godot's cached Resource. Duplicate
- # before mutating so: (a) open scenes holding a reference to this
- # .tres don't silently see the new points outside any undo action,
- # and (b) if ResourceSaver.save() fails we haven't corrupted the
- # in-memory cache (cache/disk divergence). Also guard against
- # ResourceLoader.exists() succeeding but load() returning null
- # (corrupt .tres, unregistered class) — otherwise curve.get_class()
- # on the response line below would crash the plugin.
- var loaded_curve: Resource = ResourceLoader.load(resource_path)
- if loaded_curve == null:
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Failed to load curve from %s (file exists but load returned null — may be corrupt)" % resource_path
- )
- curve = loaded_curve.duplicate()
- else:
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
- node = McpScenePath.resolve(node_path, scene_root)
- if node == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(node_path, scene_root))
- if not (property in node):
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- McpPropertyErrors.build_message(node, property)
- )
- curve = node.get(property)
- # Auto-create a fresh Curve subclass if the slot is empty. Infer the
- # concrete class from the property's hint_string (e.g. Path3D.curve's
- # hint is "Curve3D"). Creation is bundled into the same undo action
- # as the point-set below, so Ctrl-Z rolls back both.
- if curve == null:
- var inferred := _infer_curve_class(node, property)
- if inferred.is_empty():
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Curve slot on %s.%s is null and the Curve class can't be inferred from the property hint — create one first with resource_create (type=Curve3D/Curve2D/Curve)" % [node.get_class(), property]
- )
- curve = ClassDB.instantiate(inferred)
- if curve == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate %s" % inferred)
- curve_created = true
-
- if not (curve is Curve or curve is Curve2D or curve is Curve3D):
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Resource is %s — must be Curve, Curve2D, or Curve3D" % curve.get_class()
- )
-
- var coerced := _coerce_points(curve, new_points)
- if coerced.has("error"):
- return coerced.error
-
- var new_snapshot: Array = coerced.snapshot
-
- if has_file_target:
- _apply_snapshot_to_curve(curve, new_snapshot)
- # curve_set_points EDITS an existing .tres, so override the default
- # "delete to revert" message via extra_fields.
- return McpResourceIO.save_to_disk(curve, resource_path, true, "Curve", {
- "curve_class": curve.get_class(),
- "point_count": new_snapshot.size(),
- "reason": "File save is persistent; edit the .tres file manually to revert",
- }, _connection)
-
- # Inline (node-attached) path: swap the curve property so the action lands
- # cleanly in scene history, mirroring the resource-swap pattern used by
- # material_handler::assign_material. When curve_created is true the
- # "old" value is null — undo clears the slot back to empty.
- var new_curve: Resource = curve if curve_created else curve.duplicate()
- _apply_snapshot_to_curve(new_curve, new_snapshot)
- var old_curve: Resource = null if curve_created else curve
-
- _undo_redo.create_action("MCP: Set %d points on %s.%s" % [new_snapshot.size(), node.name, property])
- _undo_redo.add_do_property(node, property, new_curve)
- _undo_redo.add_undo_property(node, property, old_curve)
- _undo_redo.add_do_reference(new_curve)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "property": property,
- "curve_class": new_curve.get_class(),
- "point_count": new_snapshot.size(),
- "curve_created": curve_created,
- "undoable": true,
- }
- }
-
-
-## Infer the concrete Curve class to instantiate for a null property slot.
-## Reads the property's hint_string (set by Godot on resource-typed exports)
-## to get the exact accepted class name (e.g. "Curve3D" for Path3D.curve).
-## Returns empty string if no viable curve class can be determined.
-static func _infer_curve_class(node: Node, property: String) -> String:
- for prop in node.get_property_list():
- if prop.name != property:
- continue
- var hint_string: String = prop.get("hint_string", "")
- if hint_string.is_empty():
- return ""
- if not ClassDB.class_exists(hint_string):
- return ""
- if hint_string == "Curve" or hint_string == "Curve2D" or hint_string == "Curve3D":
- return hint_string
- # Some custom properties may list a parent class; require an exact
- # match against our three supported types to avoid surprises.
- return ""
- return ""
-
-
-## Convert input `points` into a normalized snapshot of typed values for
-## the given curve type. Returns {snapshot: Array} on success or
-## {error: ...} on failure.
-static func _coerce_points(curve: Resource, points: Array) -> Dictionary:
- var snapshot: Array = []
- if curve is Curve:
- for i in range(points.size()):
- var p = points[i]
- if not (p is Dictionary) or not p.has("offset") or not p.has("value"):
- return {"error": ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Curve points[%d] must be {offset, value, [left_tangent, right_tangent]}" % i
- )}
- snapshot.append({
- "offset": float(p["offset"]),
- "value": float(p["value"]),
- "left_tangent": float(p.get("left_tangent", 0.0)),
- "right_tangent": float(p.get("right_tangent", 0.0)),
- })
- elif curve is Curve2D:
- var zero2 := {"x": 0, "y": 0}
- for i in range(points.size()):
- var p2 = points[i]
- if not (p2 is Dictionary) or not p2.has("position"):
- return {"error": ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Curve2D points[%d] must have 'position' (and optional 'in', 'out')" % i
- )}
- var axes2 := {
- "position": p2["position"],
- "in": p2.get("in", zero2),
- "out": p2.get("out", zero2),
- }
- var coerced2 := {}
- for field in ["position", "in", "out"]:
- var v = NodeHandler._coerce_value(axes2[field], TYPE_VECTOR2)
- var err := NodeHandler._check_coerced(v, TYPE_VECTOR2, "Curve2D points[%d].%s" % [i, field])
- if err != null:
- return {"error": err}
- coerced2[field] = v
- snapshot.append(coerced2)
- else: # Curve3D
- var zero3 := {"x": 0, "y": 0, "z": 0}
- for i in range(points.size()):
- var p3 = points[i]
- if not (p3 is Dictionary) or not p3.has("position"):
- return {"error": ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Curve3D points[%d] must have 'position' (and optional 'in', 'out', 'tilt')" % i
- )}
- var axes3 := {
- "position": p3["position"],
- "in": p3.get("in", zero3),
- "out": p3.get("out", zero3),
- }
- var coerced3 := {}
- for field in ["position", "in", "out"]:
- var v = NodeHandler._coerce_value(axes3[field], TYPE_VECTOR3)
- var err := NodeHandler._check_coerced(v, TYPE_VECTOR3, "Curve3D points[%d].%s" % [i, field])
- if err != null:
- return {"error": err}
- coerced3[field] = v
- coerced3["tilt"] = float(p3.get("tilt", 0.0))
- snapshot.append(coerced3)
- return {"snapshot": snapshot}
-
-
-func _apply_snapshot_to_curve(curve: Resource, snapshot: Array) -> void:
- curve.clear_points()
- if curve is Curve:
- for p: Dictionary in snapshot:
- curve.add_point(
- Vector2(p.offset, p.value),
- p.left_tangent,
- p.right_tangent
- )
- elif curve is Curve2D:
- for p: Dictionary in snapshot:
- curve.add_point(p.position, p["in"], p.out)
- elif curve is Curve3D:
- for i in range(snapshot.size()):
- var p: Dictionary = snapshot[i]
- curve.add_point(p.position, p["in"], p.out)
- curve.set_point_tilt(i, p.tilt)
diff --git a/addons/godot_ai/handlers/curve_handler.gd.uid b/addons/godot_ai/handlers/curve_handler.gd.uid
deleted file mode 100644
index 8eb5b25..0000000
--- a/addons/godot_ai/handlers/curve_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dboqr06a1fvqx
diff --git a/addons/godot_ai/handlers/editor_handler.gd b/addons/godot_ai/handlers/editor_handler.gd
deleted file mode 100644
index ec889c1..0000000
--- a/addons/godot_ai/handlers/editor_handler.gd
+++ /dev/null
@@ -1,1069 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-const Telemetry := preload("res://addons/godot_ai/telemetry.gd")
-const PluginReload := preload("res://addons/godot_ai/utils/plugin_reload.gd")
-const VisionRoutingScript := preload("res://addons/godot_ai/vision_routing.gd")
-
-## Handles editor state, selection, log, screenshot, and performance commands.
-
-var _log_buffer: McpLogBuffer
-var _connection: McpConnection
-var _debugger_plugin: McpDebuggerPlugin
-var _game_log_buffer: McpGameLogBuffer
-var _editor_log_buffer: McpEditorLogBuffer
-var _debugger_errors_root: Node
-var _surfaced_error_tracker
-var _vision_routing: VisionRoutingScript = null
-
-
-func _init(log_buffer: McpLogBuffer, connection: McpConnection = null, debugger_plugin: McpDebuggerPlugin = null, game_log_buffer: McpGameLogBuffer = null, editor_log_buffer: McpEditorLogBuffer = null, debugger_errors_root: Node = null, surfaced_error_tracker = null, vision_routing: VisionRoutingScript = null) -> void:
- _log_buffer = log_buffer
- _connection = connection
- _debugger_plugin = debugger_plugin
- _game_log_buffer = game_log_buffer
- _editor_log_buffer = editor_log_buffer
- _debugger_errors_root = debugger_errors_root
- _surfaced_error_tracker = surfaced_error_tracker
- _vision_routing = vision_routing
- if _surfaced_error_tracker == null:
- _surfaced_error_tracker = McpSurfacedErrorTracker.new(_editor_log_buffer, _game_log_buffer, _debugger_errors_root)
-
-
-func get_editor_state(_params: Dictionary) -> Dictionary:
- var scene_root := EditorInterface.get_edited_scene_root()
- var game_status := _current_game_status()
- var data := {
- "godot_version": Engine.get_version_info().get("string", "unknown"),
- "project_name": ProjectSettings.get_setting("application/config/name", ""),
- "current_scene": scene_root.scene_file_path if scene_root else "",
- "is_playing": EditorInterface.is_playing_scene(),
- "readiness": McpConnection.get_readiness(),
- ## True once the game subprocess autoload has beaconed mcp:hello;
- ## false between Play→Stop cycles. Lets capture-source=game callers
- ## poll for a real ready signal instead of guessing with sleep().
- "game_capture_ready": _debugger_plugin != null and _debugger_plugin.is_game_capture_ready(),
- "game_status": game_status,
- "helper_live": bool(game_status.get("helper_live", false)),
- "session_active": bool(game_status.get("session_active", false)),
- }
- return {"data": data}
-
-
-func get_selection(_params: Dictionary) -> Dictionary:
- var scene_root := EditorInterface.get_edited_scene_root()
- var selected := EditorInterface.get_selection().get_selected_nodes()
- var paths: Array[String] = []
- for node in selected:
- paths.append(McpScenePath.from_node(node, scene_root))
- return {"data": {"selected_paths": paths, "count": paths.size()}}
-
-
-const VALID_LOG_SOURCES := ["plugin", "game", "editor", "all"]
-
-## Deferred budget for the `input_sequence` game op. Unlike the one-shot game
-## ops (covered by game_command's 15s entry), it drives the game forward one
-## frame per step, so the reply legitimately takes seconds. The game side caps
-## the sequence length (GameHelper.MAX_SEQUENCE_FRAMES) well inside this; the
-## budget is the backstop for a frozen game loop, mirroring take_screenshot.
-const INPUT_SEQUENCE_TIMEOUT_SEC := 30.0
-
-
-func get_logs(params: Dictionary) -> Dictionary:
- ## Coerce defensively — MCP clients can send JSON numbers as floats or
- ## stray `null` values that would otherwise fail the typed locals
- ## before we ever reach the INVALID_PARAMS return below.
- var count: int = maxi(0, int(params.get("count", 50)))
- var offset: int = maxi(0, int(params.get("offset", 0)))
- var source: String = str(params.get("source", "plugin"))
- var include_details: bool = bool(params.get("include_details", false))
- var has_since_cursor := params.has("since_cursor") and params.get("since_cursor") != null
- var since_cursor: int = maxi(0, int(params.get("since_cursor", 0)))
- var since_run_id := "" if params.get("since_run_id", null) == null else str(params.get("since_run_id", ""))
- if not source in VALID_LOG_SOURCES:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid source '%s' — use 'plugin', 'game', 'editor', or 'all'" % source,
- )
-
- match source:
- "plugin":
- return _get_plugin_logs(count, offset)
- "game":
- return _get_game_logs(count, offset, include_details, since_run_id)
- "editor":
- return _get_editor_logs(count, offset, include_details, has_since_cursor, since_cursor)
- "all":
- return _get_all_logs(count, offset, include_details)
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Unreachable")
-
-
-func _current_game_status() -> Dictionary:
- if _debugger_plugin == null:
- return McpDebuggerPlugin.with_liveness_flags({
- "status": "stopped",
- "active": false,
- "ready": false,
- "helper_expected": true,
- })
- return _debugger_plugin.get_game_status()
-
-
-func _get_plugin_logs(count: int, offset: int) -> Dictionary:
- var all_lines := _log_buffer.get_recent(_log_buffer.total_count())
- var page: Array[Dictionary] = []
- var stop := mini(all_lines.size(), offset + count)
- for i in range(mini(offset, all_lines.size()), stop):
- page.append({"source": "plugin", "level": "info", "text": all_lines[i]})
- return {
- "data": {
- "source": "plugin",
- "lines": page,
- "total_count": all_lines.size(),
- "returned_count": page.size(),
- "offset": offset,
- }
- }
-
-
-func _get_game_logs(count: int, offset: int, include_details: bool, since_run_id: String = "") -> Dictionary:
- var game_status := _current_game_status()
- var helper_live := bool(game_status.get("helper_live", false))
- var session_active := bool(game_status.get("session_active", false))
- if _game_log_buffer == null:
- return {
- "data": {
- "source": "game",
- "lines": [],
- "total_count": 0,
- "returned_count": 0,
- "offset": offset,
- "run_id": "",
- "current_run_id": "",
- "is_running": session_active,
- "helper_live": helper_live,
- "session_active": session_active,
- "game_status": game_status,
- "dropped_count": 0,
- "stale_run_id": false,
- }
- }
- var current_run_id := _game_log_buffer.run_id()
- var target_run_id := since_run_id if not since_run_id.is_empty() else current_run_id
- var stale_run_id := not since_run_id.is_empty() and since_run_id != current_run_id
- var run_page := _game_log_buffer.get_run_page(target_run_id, offset, count)
- var page := _entries_for_response(run_page.get("entries", []), include_details)
- var data := {
- "source": "game",
- "lines": page,
- "total_count": int(run_page.get("total_count", 0)),
- "returned_count": page.size(),
- "offset": offset,
- "run_id": target_run_id,
- "current_run_id": current_run_id,
- "is_running": session_active,
- "helper_live": helper_live,
- "session_active": session_active,
- "game_status": game_status,
- "dropped_count": _game_log_buffer.dropped_count(),
- "stale_run_id": stale_run_id,
- }
- _merge_editor_errors_hint(data, game_status)
- return {"data": data}
-
-
-## #641: boot-time parse errors happen while autoload scripts compile — before
-## the game helper's logger attaches via OS.add_logger — so they can NEVER
-## appear in the game buffer. They surface only through the editor scope
-## (Errors-tab rows + editor logger). Cross-reference them here so an
-## empty/clean game log is not mistaken for a clean launch.
-func _merge_editor_errors_hint(data: Dictionary, game_status: Dictionary) -> void:
- if _debugger_plugin == null:
- return
- ## A since_run_id read of a prior run must not carry the CURRENT run's
- ## editor errors — the hint interprets the run being read.
- if bool(data.get("stale_run_id", false)):
- return
- ## run_token == 0 means no tracked run ever started this session; the
- ## run-start cursor would be 0 and every retained editor error would be
- ## misattributed to "this run".
- if int(game_status.get("run_token", 0)) <= 0:
- return
- ## One-shot read — force the scan so rows that landed after the last
- ## gated scan (and before the deferred timers fire) make the FIRST
- ## logs_read(source='game') response, not just a later one.
- var errors_info: Dictionary = _debugger_plugin.recent_editor_errors_since(
- int(game_status.get("editor_log_cursor", 0)), true)
- if str(errors_info.get("scope", "none")) != "run":
- return
- var errors: Array = errors_info.get("errors", [])
- if errors.is_empty():
- return
- data["editor_errors_count"] = errors.size()
- data["editor_errors_hint"] = (
- "%d editor-side error%s from this run (first: %s) missing from the game log — boot-time parse/load errors occur before the game helper's logger attaches. Read logs_read(source='editor', include_details=true)."
- % [errors.size(), "s" if errors.size() != 1 else "", _format_editor_error_summary(errors[0])]
- )
-
-
-func _format_editor_error_summary(entry: Dictionary) -> String:
- return McpSurfacedErrorTracker.format_editor_error_summary(entry)
-
-
-func _get_editor_logs(count: int, offset: int, include_details: bool, has_since_cursor: bool = false, since_cursor: int = 0) -> Dictionary:
- ## Editor-process script errors (parse errors, @tool runtime errors,
- ## EditorPlugin errors, push_error/push_warning). Captured by
- ## editor_logger.gd via OS.add_logger on every v4-supported engine. During
- ## partial initialization the buffer can still be null. Godot also sends GDScript reload
- ## warnings/errors straight to the Debugger dock's Errors tab; those do
- ## not flow through OS.add_logger, so merge the visible tree rows here.
- if has_since_cursor:
- return _get_editor_logs_since(count, since_cursor, include_details)
- var all_entries := _collect_editor_log_entries()
- var page := _entries_for_response(_slice_entries(all_entries, offset, count), include_details)
- var appended_total := _editor_log_buffer.appended_total() if _editor_log_buffer != null else 0
- return {
- "data": {
- "source": "editor",
- "lines": page,
- "total_count": all_entries.size(),
- "returned_count": page.size(),
- "offset": offset,
- "dropped_count": _editor_log_buffer.dropped_count() if _editor_log_buffer != null else 0,
- "next_cursor": appended_total,
- "appended_total": appended_total,
- }
- }
-
-
-func _get_editor_logs_since(count: int, since_cursor: int, include_details: bool) -> Dictionary:
- ## Cursor reads are defined over the monotonic editor logger ring only.
- ## Visible Debugger Errors-tab rows are live UI state, not ring entries,
- ## so regular offset reads still merge them while since_cursor polling
- ## reports only Logger-backed entries.
- var captured := {
- "cursor": since_cursor,
- "oldest_cursor": 0,
- "next_cursor": 0,
- "appended_total": 0,
- "truncated": false,
- "has_more": false,
- "entries": [],
- }
- var dropped := 0
- if _editor_log_buffer != null:
- captured = _editor_log_buffer.get_since(since_cursor, count)
- dropped = _editor_log_buffer.dropped_count()
- var page := _entries_for_response(captured.get("entries", []), include_details)
- return {
- "data": {
- "source": "editor",
- "lines": page,
- "total_count": int(captured.get("appended_total", 0)),
- "returned_count": page.size(),
- "offset": 0,
- "dropped_count": dropped,
- "cursor": int(captured.get("cursor", since_cursor)),
- "oldest_cursor": int(captured.get("oldest_cursor", 0)),
- "next_cursor": int(captured.get("next_cursor", 0)),
- "appended_total": int(captured.get("appended_total", 0)),
- "truncated": bool(captured.get("truncated", false)),
- "has_more": bool(captured.get("has_more", false)),
- }
- }
-
-
-func _get_all_logs(count: int, offset: int, include_details: bool) -> Dictionary:
- ## Plugin lines have no timestamp, so we can't merge chronologically.
- ## Concatenate plugin → editor → game and apply the offset/count window
- ## over the combined list. The per-line `source` field tells callers
- ## where each entry came from. Editor goes between plugin and game so
- ## script errors stay grouped near the plugin recv/send traffic that
- ## triggered them, with game runtime logs at the end.
- var combined: Array[Dictionary] = []
- for line in _log_buffer.get_recent(_log_buffer.total_count()):
- combined.append({"source": "plugin", "level": "info", "text": line})
- for entry in _collect_editor_log_entries():
- combined.append(entry)
- var run_id := ""
- var current_run_id := ""
- var dropped := 0
- if _game_log_buffer != null:
- run_id = _game_log_buffer.run_id()
- current_run_id = run_id
- dropped = _game_log_buffer.dropped_count()
- var run_page := _game_log_buffer.get_run_page(run_id, 0, McpGameLogBuffer.MAX_LINES)
- for entry in run_page.get("entries", []):
- combined.append(entry)
- var stop := mini(combined.size(), offset + count)
- var page: Array[Dictionary] = []
- for i in range(mini(offset, combined.size()), stop):
- page.append(combined[i])
- page = _entries_for_response(page, include_details)
- if _editor_log_buffer != null:
- dropped += _editor_log_buffer.dropped_count()
- var game_status := _current_game_status()
- return {
- "data": {
- "source": "all",
- "lines": page,
- "total_count": combined.size(),
- "returned_count": page.size(),
- "offset": offset,
- "run_id": run_id,
- "current_run_id": current_run_id,
- "is_running": bool(game_status.get("session_active", false)),
- "helper_live": bool(game_status.get("helper_live", false)),
- "session_active": bool(game_status.get("session_active", false)),
- "game_status": game_status,
- "dropped_count": dropped,
- }
- }
-
-
-func _entries_for_response(entries: Array[Dictionary], include_details: bool) -> Array[Dictionary]:
- ## Compact responses only drop the top-level "details" key, so a shallow
- ## copy is enough; the deep copy is reserved for the opt-in details path
- ## where nested dicts leave the buffer.
- var out: Array[Dictionary] = []
- for entry in entries:
- if include_details:
- out.append(entry.duplicate(true))
- else:
- var copy: Dictionary = entry.duplicate(false)
- copy.erase("details")
- out.append(copy)
- return out
-
-
-func _collect_editor_log_entries() -> Array[Dictionary]:
- return _surfaced_error_tracker.collect_editor_log_entries()
-
-
-static func _slice_entries(entries: Array[Dictionary], offset: int, count: int) -> Array[Dictionary]:
- var page: Array[Dictionary] = []
- var stop := mini(entries.size(), offset + count)
- for i in range(mini(offset, entries.size()), stop):
- page.append(entries[i])
- return page
-
-
-## Map of human-readable monitor names to Performance.Monitor enum values.
-const MONITORS := {
- "time/fps": Performance.TIME_FPS,
- "time/process": Performance.TIME_PROCESS,
- "time/physics_process": Performance.TIME_PHYSICS_PROCESS,
- "time/navigation_process": Performance.TIME_NAVIGATION_PROCESS,
- "memory/static": Performance.MEMORY_STATIC,
- "memory/static_max": Performance.MEMORY_STATIC_MAX,
- "memory/message_buffer_max": Performance.MEMORY_MESSAGE_BUFFER_MAX,
- "object/count": Performance.OBJECT_COUNT,
- "object/resource_count": Performance.OBJECT_RESOURCE_COUNT,
- "object/node_count": Performance.OBJECT_NODE_COUNT,
- "object/orphan_node_count": Performance.OBJECT_ORPHAN_NODE_COUNT,
- "render/total_objects_in_frame": Performance.RENDER_TOTAL_OBJECTS_IN_FRAME,
- "render/total_primitives_in_frame": Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME,
- "render/total_draw_calls_in_frame": Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME,
- "render/video_mem_used": Performance.RENDER_VIDEO_MEM_USED,
- "physics_2d/active_objects": Performance.PHYSICS_2D_ACTIVE_OBJECTS,
- "physics_2d/collision_pairs": Performance.PHYSICS_2D_COLLISION_PAIRS,
- "physics_2d/island_count": Performance.PHYSICS_2D_ISLAND_COUNT,
- "physics_3d/active_objects": Performance.PHYSICS_3D_ACTIVE_OBJECTS,
- "physics_3d/collision_pairs": Performance.PHYSICS_3D_COLLISION_PAIRS,
- "physics_3d/island_count": Performance.PHYSICS_3D_ISLAND_COUNT,
- "navigation/active_maps": Performance.NAVIGATION_ACTIVE_MAPS,
- "navigation/region_count": Performance.NAVIGATION_REGION_COUNT,
- "navigation/agent_count": Performance.NAVIGATION_AGENT_COUNT,
- "navigation/link_count": Performance.NAVIGATION_LINK_COUNT,
- "navigation/polygon_count": Performance.NAVIGATION_POLYGON_COUNT,
- "navigation/edge_count": Performance.NAVIGATION_EDGE_COUNT,
- "navigation/edge_merge_count": Performance.NAVIGATION_EDGE_MERGE_COUNT,
- "navigation/edge_connection_count": Performance.NAVIGATION_EDGE_CONNECTION_COUNT,
- "navigation/edge_free_count": Performance.NAVIGATION_EDGE_FREE_COUNT,
-}
-
-
-## Compute coverage angles from the target's AABB geometry.
-## Returns an establishing perspective shot (faces the longest ground axis)
-## and an orthographic top-down for spatial layout. The AI iterates from
-## there with explicit elevation/azimuth/fov for closeups and detail shots.
-func _compute_coverage_angles(aabb: AABB) -> Array[Dictionary]:
- var size := aabb.size
- var ground_x := maxf(size.x, 0.01)
- var ground_z := maxf(size.z, 0.01)
-
- ## Face the longest ground axis — establishing shot shows maximum extent
- var estab_azimuth: float
- if ground_x >= ground_z:
- estab_azimuth = 0.0 # face along Z, showing X width
- else:
- estab_azimuth = 90.0 # face along X, showing Z width
-
- ## FOV: wider for spread-out subjects, narrower for compact ones
- var ground_ratio := maxf(ground_x, ground_z) / minf(ground_x, ground_z)
- var estab_fov := clampf(40.0 + ground_ratio * 5.0, 45.0, 65.0)
-
- return [
- {"label": "establishing", "elevation": 25.0, "azimuth": estab_azimuth + 20.0,
- "fov": estab_fov, "ortho": false, "padding": 1.8},
- {"label": "top", "elevation": 90.0, "azimuth": 0.0,
- "fov": 0.0, "ortho": true},
- ]
-
-
-func take_screenshot(params: Dictionary) -> Dictionary:
- ## Vision Routing hook: when enabled, the capture is described by the
- ## configured vision provider on a worker thread and the text description
- ## is returned instead of the raw image (see vision_routing.gd). Off, no
- ## key, or non-image results keep the original behavior. The single source
- ## of truth for the `match source:` dispatch lives in _take_screenshot_impl
- ## (pinned by tests/unit/test_docs_screenshot_sources.py).
- if _vision_routing != null and _vision_routing.is_routing_enabled():
- return _vision_routing.route_editor_screenshot(params, Callable(self, "_take_screenshot_impl"), _connection)
- return _take_screenshot_impl(params)
-
-
-func _take_screenshot_impl(params: Dictionary) -> Dictionary:
- var source: String = params.get("source", "viewport")
- var max_resolution: int = params.get("max_resolution", 0)
- var view_target: String = params.get("view_target", "")
- var coverage: bool = params.get("coverage", false)
- var custom_elevation = params.get("elevation", null)
- var custom_azimuth = params.get("azimuth", null)
- var custom_fov = params.get("fov", null)
-
- var viewport: Viewport
- match source:
- "viewport":
- viewport = EditorInterface.get_editor_viewport_3d()
- if viewport == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_VIEWPORT_UNAVAILABLE,
- "No 3D viewport available", false)
- ## The 3D viewport's texture is empty when the edited scene
- ## has no Node3D content (2D-only scene, or no scene open),
- ## and the empty-image guard further down used to surface
- ## that as INTERNAL_ERROR — leaving callers with no signal
- ## that the failure was caller-side. Reject up front with a
- ## structured hint so the LLM can pick a sensible next step
- ## (open a 3D scene, switch to source="cinematic", etc.).
- var precheck := viewport_screenshot_precheck(EditorInterface.get_edited_scene_root())
- if precheck.has("error"):
- return precheck
- "game":
- if not EditorInterface.is_playing_scene():
- ## Same editor state as game_eval/game_command's gate below —
- ## same EDITOR_NOT_READY shape, not INVALID_PARAMS (the params
- ## were fine; the editor just isn't in the required state).
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_GAME_NOT_RUNNING,
- "Game is not running — start the project first", false,
- "Use source='viewport' for the editor viewport, or start the game with project_run and retry.")
- ## The game is always a separate OS process (embedded mode just
- ## reparents its window into the editor). Reach the framebuffer
- ## via the debugger channel: the `_mcp_game_helper` autoload
- ## inside the game process replies with a PNG, and
- ## McpDebuggerPlugin pushes the response back through our
- ## WebSocket with the same request_id via McpConnection.send_deferred_response.
- if _debugger_plugin == null or _connection == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Debugger bridge unavailable — plugin may not be fully initialised")
- var request_id: String = params.get("_request_id", "")
- if request_id.is_empty():
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Missing request_id — cannot correlate deferred response")
- _debugger_plugin.request_game_screenshot(request_id, max_resolution, _connection)
- return McpDispatcher.DEFERRED_RESPONSE
- "cinematic":
- return _take_cinematic_screenshot(max_resolution)
- "viewport_2d":
- viewport = EditorInterface.get_editor_viewport_2d()
- if viewport == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_VIEWPORT_UNAVAILABLE,
- "No 2D viewport available", false)
- var scene_root_2d := EditorInterface.get_edited_scene_root()
- if scene_root_2d == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_NO_SCENE,
- "No scene open — open a scene first", false,
- "Call scene_open with a scene path (e.g. \"res://main.tscn\") first.")
- if not view_target.is_empty() or coverage or custom_elevation != null or custom_azimuth != null or custom_fov != null:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "view_target, coverage, elevation, azimuth, and fov are not supported with source='viewport_2d'"
- )
- ## Capture the 2D editor viewport directly; no view_target/coverage for 2D.
- RenderingServer.force_draw(false)
- var image_2d: Image = viewport.get_texture().get_image()
- if image_2d == null or image_2d.is_empty():
- return _empty_image_error(
- "viewport_2d",
- "Captured an empty image from the 2D viewport. The 2D viewport produced no output — typically headless mode or the 2D viewport has not drawn a frame yet."
- )
- return _finalize_image(image_2d, "viewport_2d", max_resolution)
- _:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid source '%s' — use 'viewport', 'viewport_2d', 'cinematic', or 'game'" % source)
-
- ## Handle view_target: temporarily reposition the editor's own camera to
- ## frame one or more target nodes, force a render, capture, then restore.
- if not view_target.is_empty() and source == "viewport":
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- ## Parse comma-separated paths, deduplicate
- var raw_paths := view_target.split(",")
- var seen := {}
- var unique_paths: Array[String] = []
- for rp in raw_paths:
- var p := rp.strip_edges()
- if not p.is_empty() and not seen.has(p):
- seen[p] = true
- unique_paths.append(p)
-
- ## Resolve each path, collect valid Node3D targets
- var targets: Array[Node3D] = []
- var not_found: Array[String] = []
- for p in unique_paths:
- var node := McpScenePath.resolve(p, scene_root)
- if node == null:
- not_found.append(p)
- elif not node is Node3D:
- not_found.append(p)
- else:
- targets.append(node as Node3D)
-
- if targets.is_empty():
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, "No valid Node3D targets found: %s" % ", ".join(not_found))
-
- var cam := viewport.get_camera_3d()
- if cam == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_VIEWPORT_UNAVAILABLE,
- "No camera in 3D viewport", false)
-
- ## Merge AABBs from all targets
- var combined_aabb := _get_visual_aabb(targets[0])
- for i in range(1, targets.size()):
- combined_aabb = combined_aabb.merge(_get_visual_aabb(targets[i]))
-
- var cam_rid := cam.get_camera_rid()
- var saved_xform := cam.global_transform
- var saved_fov := cam.fov
- var saved_near := cam.near
- var saved_far := cam.far
-
- ## --- Coverage path: multi-angle sweep ---
- if coverage:
- var images: Array[Dictionary] = []
- for preset in _compute_coverage_angles(combined_aabb):
- if preset.get("ortho", false):
- ## Orthographic top-down view
- var ortho_size := combined_aabb.size.length() * 1.8
- var cam_height := maxf(combined_aabb.size.length() * 3.0, 10.0)
- var center := combined_aabb.get_center()
- var xform := Transform3D(Basis.IDENTITY, center + Vector3.UP * cam_height)
- xform = xform.looking_at(center, Vector3.FORWARD)
- RenderingServer.camera_set_orthogonal(cam_rid, ortho_size, saved_near, maxf(saved_far, cam_height * 2.0))
- RenderingServer.camera_set_transform(cam_rid, xform)
- else:
- ## Perspective view — padding per preset (wide for establishing, tight for detail)
- var pad: float = preset.get("padding", 2.5)
- var xform := _frame_transform_for_aabb(combined_aabb, preset.fov, preset.elevation, preset.azimuth, pad)
- RenderingServer.camera_set_perspective(cam_rid, preset.fov, saved_near, saved_far)
- RenderingServer.camera_set_transform(cam_rid, xform)
- RenderingServer.force_draw(false)
- var img: Image = viewport.get_texture().get_image()
- if img != null and not img.is_empty():
- var entry := _finalize_image(img, "viewport", max_resolution)
- entry.data["label"] = preset.label
- entry.data["elevation"] = preset.elevation
- entry.data["azimuth"] = preset.azimuth
- entry.data["fov"] = preset.fov
- entry.data["ortho"] = preset.get("ortho", false)
- images.append(entry.data)
-
- ## Restore camera state (back to perspective + original transform)
- RenderingServer.camera_set_perspective(cam_rid, saved_fov, saved_near, saved_far)
- RenderingServer.camera_set_transform(cam_rid, saved_xform)
-
- ## Consistent with single-shot path: error if no frames rendered
- ## (e.g. headless mode where force_draw produces no output).
- if images.is_empty():
- return _empty_image_error(
- "viewport",
- "Coverage sweep rendered no images. The 3D viewport produced no output across any of the preset angles — typically because the editor is in headless mode (force_draw has no rendered output) or the 3D viewport has not drawn a frame yet."
- )
-
- var aabb_center := combined_aabb.get_center()
- var aabb_size := combined_aabb.size
- var result_data := {
- "source": "viewport",
- "view_target": view_target,
- "view_target_count": targets.size(),
- "coverage": true,
- "images": images,
- "aabb_center": [aabb_center.x, aabb_center.y, aabb_center.z],
- "aabb_size": [aabb_size.x, aabb_size.y, aabb_size.z],
- "aabb_longest_ground_axis": "x" if aabb_size.x >= aabb_size.z else "z",
- }
- if not not_found.is_empty():
- result_data["view_target_not_found"] = not_found
- return {"data": result_data}
-
- ## --- Custom angle / FOV path ---
- var use_elev: float = 25.0 if custom_elevation == null else float(custom_elevation)
- var use_azim: float = 30.0 if custom_azimuth == null else float(custom_azimuth)
- var use_fov: float = saved_fov if custom_fov == null else float(custom_fov)
-
- var cam_xform := _frame_transform_for_aabb(combined_aabb, use_fov, use_elev, use_azim)
-
- if custom_fov != null:
- RenderingServer.camera_set_perspective(cam_rid, use_fov, saved_near, saved_far)
- RenderingServer.camera_set_transform(cam_rid, cam_xform)
- RenderingServer.force_draw(false)
-
- var image: Image = viewport.get_texture().get_image()
-
- ## Restore camera state
- if custom_fov != null:
- RenderingServer.camera_set_perspective(cam_rid, saved_fov, saved_near, saved_far)
- RenderingServer.camera_set_transform(cam_rid, saved_xform)
-
- if image == null or image.is_empty():
- return _empty_image_error(
- "viewport",
- "Framed viewport rendered an empty image after repositioning the camera onto the view_target. The 3D viewport produced no output — typically headless mode or the 3D viewport has not drawn a frame yet."
- )
-
- var result := _finalize_image(image, "viewport", max_resolution)
- result.data["view_target"] = view_target
- result.data["view_target_count"] = targets.size()
- var aabb_c := combined_aabb.get_center()
- var aabb_s := combined_aabb.size
- result.data["aabb_center"] = [aabb_c.x, aabb_c.y, aabb_c.z]
- result.data["aabb_size"] = [aabb_s.x, aabb_s.y, aabb_s.z]
- result.data["aabb_longest_ground_axis"] = "x" if aabb_s.x >= aabb_s.z else "z"
- if custom_elevation != null or custom_azimuth != null:
- result.data["elevation"] = use_elev
- result.data["azimuth"] = use_azim
- if custom_fov != null:
- result.data["fov"] = use_fov
- if not not_found.is_empty():
- result.data["view_target_not_found"] = not_found
- return result
-
- var image: Image = viewport.get_texture().get_image()
-
- if image == null or image.is_empty():
- return _empty_image_error(
- source,
- "Captured an empty image from %s. The 3D viewport produced no output — typically headless mode or the 3D viewport has not drawn a frame yet." % source
- )
-
- return _finalize_image(image, source, max_resolution)
-
-
-## Render the edited scene through its active Camera3D without running the
-## game. Mirrors Godot's "Cinematic Preview" display mode but via a
-## throwaway SubViewport, so the output has no editor gizmos, selection
-## outlines, or grid lines.
-func _take_cinematic_screenshot(max_resolution: int) -> Dictionary:
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- var scene_camera := _find_current_camera_3d(scene_root)
- if scene_camera == null:
- return ErrorCodes.make(
- ErrorCodes.NODE_NOT_FOUND,
- "No current Camera3D in scene — mark a Camera3D as `current` or add one to the scene",
- )
-
- ## Default to a 16:9 HD capture; size is overridden by _finalize_image's
- ## `max_resolution` downscale step when requested.
- var render_size := Vector2i(1920, 1080)
- var edit_vp := EditorInterface.get_editor_viewport_3d()
- if edit_vp != null:
- var vs := edit_vp.get_visible_rect().size
- if vs.x >= 1.0 and vs.y >= 1.0:
- render_size = Vector2i(int(vs.x), int(vs.y))
-
- var sub_vp := SubViewport.new()
- sub_vp.size = render_size
- sub_vp.own_world_3d = false
- sub_vp.transparent_bg = false
- sub_vp.render_target_update_mode = SubViewport.UPDATE_ONCE
-
- var cam := Camera3D.new()
- cam.fov = scene_camera.fov
- cam.near = scene_camera.near
- cam.far = scene_camera.far
- cam.projection = scene_camera.projection
- cam.size = scene_camera.size
- cam.keep_aspect = scene_camera.keep_aspect
- cam.cull_mask = scene_camera.cull_mask
- cam.environment = scene_camera.environment
- cam.attributes = scene_camera.attributes
- cam.current = true
-
- sub_vp.add_child(cam)
- scene_root.add_child(sub_vp)
- ## global_transform is resolved against the ancestor Node3D chain, so it
- ## must be set after parenting — otherwise the camera ends up at origin.
- cam.global_transform = scene_camera.global_transform
- ## NOTIFICATION_TRANSFORM_CHANGED is delivered deferred (next frame's
- ## flush_transform_notifications), but force_draw renders immediately —
- ## without this flush the RenderingServer still has the identity
- ## transform pushed at ENTER_WORLD and the capture shows only sky
- ## instead of the camera's actual view (issue #650).
- cam.force_update_transform()
-
- RenderingServer.force_draw(false)
- var image: Image = sub_vp.get_texture().get_image()
-
- scene_root.remove_child(sub_vp)
- sub_vp.queue_free()
-
- if image == null or image.is_empty():
- return _empty_image_error(
- "cinematic",
- "Cinematic render produced an empty image. The SubViewport returned no texture — typically headless mode (force_draw has no rendered output) or the scene's Camera3D is positioned so nothing visible is in frame."
- )
-
- var result := _finalize_image(image, "cinematic", max_resolution)
- result.data["camera_path"] = McpScenePath.from_node(scene_camera, scene_root)
- return result
-
-
-## Reject a `source="viewport"` screenshot before we ever pull the
-## texture if the edited scene has no Node3D content. The 3D viewport
-## returns an empty (or stale) image in that case; surfacing it as
-## INTERNAL_ERROR ("Failed to capture image from viewport") gave LLM
-## callers no signal that the right move is to switch source or open a
-## 3D scene. 152 hits / 63 uuids in 24h across plugin versions 2.5.0 ->
-## 2.5.6 traced back to this. Returns `{}` on success.
-##
-## Caller passes `EditorInterface.get_edited_scene_root()`; the static
-## form lets tests exercise the branches with a synthetic scene root
-## without driving the editor.
-static func viewport_screenshot_precheck(scene_root: Node) -> Dictionary:
- if scene_root == null:
- var no_scene_err := _make_viewport_not_3d_error(
- "",
- "The editor 3D viewport is empty because no scene is open. Open a scene with `scene_open` first."
- )
- ## The honest state here is "no scene", not "scene lacks 3D content"
- ## — relabel the sub-code so telemetry doesn't conflate the two.
- ## `editor_state` stays "viewport_not_3d" for pre-#651 consumers.
- no_scene_err["error"]["data"]["sub_code"] = ErrorCodes.SUB_EDITOR_NO_SCENE
- return no_scene_err
- ## A scene with any Node3D content — root or descendant — has
- ## something the 3D viewport can render. Walking the tree (rather
- ## than only checking the root type) avoids a false reject on the
- ## common `Node` / `Node2D` root + Node3D descendant pattern.
- if _scene_has_node3d_content(scene_root):
- return {}
- var root_type := scene_root.get_class()
- var hint: String
- var is_2d_scene := scene_root is CanvasItem
- if is_2d_scene:
- hint = (
- "The 3D viewport is empty because the current scene is 2D (%s root) with no Node3D descendants. "
- + "Options: (a) open a 3D scene, "
- + "(b) use source=\"cinematic\" if a Camera3D exists in the scene, "
- + "(c) use source=\"viewport_2d\" to capture the 2D editor viewport directly, "
- + "(d) call scene_get_hierarchy first to inspect what's available."
- ) % root_type
- else:
- hint = (
- "The 3D viewport is empty because the current scene (%s root) has no Node3D content anywhere in the tree. "
- + "Options: (a) open or add a Node3D, "
- + "(b) use source=\"cinematic\" if a Camera3D exists in the scene, "
- + "(c) call scene_get_hierarchy first to inspect what's available."
- ) % root_type
- var err := _make_viewport_not_3d_error(root_type, hint)
- if is_2d_scene:
- err["error"]["data"]["suggestion"] = "use source='viewport_2d' for 2D scenes"
- return err
-
-
-## True if scene_root is itself a Node3D or owns any Node3D descendant.
-## DFS short-circuits on the first hit so empty 2D scenes stay cheap.
-static func _scene_has_node3d_content(scene_root: Node) -> bool:
- if scene_root is Node3D:
- return true
- var stack: Array[Node] = [scene_root]
- while not stack.is_empty():
- var node: Node = stack.pop_back()
- for child in node.get_children():
- if child is Node3D:
- return true
- stack.append(child)
- return false
-
-
-static func _make_viewport_not_3d_error(scene_root_type: String, hint: String) -> Dictionary:
- ## `hint` becomes `error.message`; not duplicated into `data` because
- ## `GodotCommandError`'s string form already appends every `data` key
- ## as a suffix on the agent-visible error.
- var err := ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_VIEWPORT_NOT_3D, hint, false)
- err["error"]["data"]["editor_state"] = "viewport_not_3d"
- err["error"]["data"]["scene_root_type"] = scene_root_type
- return err
-
-
-## Reached only when the precheck passed but the texture still came
-## back empty — headless rendering, a freshly opened editor whose 3D
-## viewport hasn't drawn a frame, or a SubViewport that lost its target.
-static func _empty_image_error(source: String, hint: String) -> Dictionary:
- ## retryable=false: an empty capture is usually headless mode, where a
- ## retry loops forever — the not-yet-drawn-frame case is transient but
- ## indistinguishable from here, so don't invite a retry loop.
- var err := ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_VIEWPORT_EMPTY, hint, false)
- err["error"]["data"]["editor_state"] = "viewport_empty"
- err["error"]["data"]["source"] = source
- return err
-
-
-## Return the Camera3D that would be active if the scene were running.
-## Preference: a descendant with `current=true`, else the first Camera3D
-## found in a depth-first walk.
-func _find_current_camera_3d(root: Node) -> Camera3D:
- var first: Camera3D = null
- var stack: Array[Node] = [root]
- while not stack.is_empty():
- var node: Node = stack.pop_back()
- if node is Camera3D:
- if node.current:
- return node
- if first == null:
- first = node
- for child in node.get_children():
- stack.append(child)
- return first
-
-
-func _finalize_image(image: Image, source: String, max_resolution: int) -> Dictionary:
- ## Shared with the game-process copy in runtime/game_helper.gd (#716).
- var encoded := McpScreenshotEncode.downscale_and_encode(image, max_resolution)
- return {
- "data": {
- "source": source,
- "width": encoded.width,
- "height": encoded.height,
- "original_width": encoded.original_width,
- "original_height": encoded.original_height,
- "format": "png",
- "image_base64": encoded.base64,
- }
- }
-
-
-## Recursively compute the visual bounding box of a Node3D and its children.
-func _get_visual_aabb(node: Node3D) -> AABB:
- var aabb := AABB()
- var found := false
- if node is VisualInstance3D:
- aabb = node.global_transform * node.get_aabb()
- found = true
- for child in node.get_children():
- if child is Node3D:
- var child_aabb := _get_visual_aabb(child)
- if child_aabb.size != Vector3.ZERO:
- if found:
- aabb = aabb.merge(child_aabb)
- else:
- aabb = child_aabb
- found = true
- if not found:
- aabb = AABB(node.global_position - Vector3(0.5, 0.5, 0.5), Vector3(1, 1, 1))
- return aabb
-
-
-## Calculate a camera Transform3D that frames the given AABB nicely.
-## elevation_deg: camera elevation (0 = level, 90 = directly above). Default 25.
-## azimuth_deg: camera azimuth (0 = front, 90 = right side). Default 30.
-## padding: distance multiplier for breathing room (1.2 = tight, 2.5 = context). Default 1.8.
-func _frame_transform_for_aabb(aabb: AABB, fov_degrees: float = 75.0, elevation_deg: float = 25.0, azimuth_deg: float = 30.0, padding: float = 1.8) -> Transform3D:
- var center := aabb.get_center()
- var radius := aabb.size.length() * 0.5
- var fov_rad := deg_to_rad(fov_degrees)
- var distance := radius / tan(fov_rad * 0.5) * padding
- ## Floor with an absolute offset so unit-scale AABBs don't place the camera
- ## inside or against the target. `radius * 2.0` alone scales to zero as the
- ## AABB shrinks; the +1.0 guarantees a minimum of ~1 world-unit of standoff.
- distance = maxf(distance, radius * 2.0 + 1.0)
- var elev := deg_to_rad(elevation_deg)
- var azim := deg_to_rad(azimuth_deg)
- var cam_pos := center + Vector3(
- distance * cos(elev) * sin(azim),
- distance * sin(elev),
- distance * cos(elev) * cos(azim),
- )
- var xform := Transform3D(Basis.IDENTITY, cam_pos)
- ## At ~90° elevation the view direction is parallel to Vector3.UP — use
- ## FORWARD as the up hint so looking_at doesn't degenerate.
- var up := Vector3.FORWARD if elevation_deg > 85.0 else Vector3.UP
- return xform.looking_at(center, up)
-
-
-func get_performance_monitors(params: Dictionary) -> Dictionary:
- var filter: Array = params.get("monitors", [])
- var result := {}
-
- if filter.is_empty():
- for key in MONITORS:
- result[key] = Performance.get_monitor(MONITORS[key])
- else:
- for key in filter:
- if MONITORS.has(key):
- result[key] = Performance.get_monitor(MONITORS[key])
-
- return {
- "data": {
- "monitors": result,
- "monitor_count": result.size(),
- }
- }
-
-
-func clear_logs(params: Dictionary) -> Dictionary:
- var count := _log_buffer.total_count()
- _log_buffer.clear()
- var data := {"cleared_count": count}
- ## The Debugger Errors panel is user-visible editor UI, not an MCP-owned
- ## buffer — wiping it stays behind an explicit opt-in.
- if bool(params.get("clear_debugger_errors", false)):
- data["debugger_errors_cleared"] = _clear_debugger_error_trees()
- return {"data": data}
-
-
-func _clear_debugger_error_trees() -> int:
- return _surfaced_error_tracker.clear_debugger_error_trees()
-
-
-func reload_plugin(_params: Dictionary) -> Dictionary:
- var work := PluginReload.reserve_reload()
- if work == 0:
- return ErrorCodes.make(ErrorCodes.EDITOR_NOT_READY, "A plugin reload is already pending.")
- _log_buffer.log("reload_plugin requested, reloading next frame")
- ## Persist a pending plugin_reload telemetry event *before* the
- ## disable kills the live WebSocket. The re-enabled plugin's
- ## _enter_tree flushes via `_telemetry.flush_pending_plugin_reload()`.
- Telemetry.record_pending_plugin_reload("mcp_tool")
- _do_reload_plugin.call_deferred(work)
- return {"data": {"status": "reloading", "message": "Plugin reload initiated"}}
-
-
-## Force a filesystem rescan before toggling the plugin, so Godot's
-## class-name registry picks up any .gd files added since the last scan
-## (e.g. via git pull or an agent-driven sync). Without this, re-enable can
-## fail with "Could not find type X" when new class_name scripts are on disk
-## but not yet registered, leaving the plugin disabled with no recovery path
-## short of killing the editor. See issue #83.
-# The deferred entry captures no handler. The helper owns the bounded native
-# signal handoff: is_scanning() can become false before resource reloads and
-# filesystem_changed run on the main thread. No suspended GDScript frame may
-# span a scan which can recompile that very frame's script.
-static func _do_reload_plugin(work: int = 0) -> void:
- PluginReload.reload_after_scan(work)
-
-
-func quit_editor(_params: Dictionary) -> Dictionary:
- _log_buffer.log("quit_editor requested, quitting next frame")
- ## Defer the quit so the response is sent back before the editor exits.
- EditorInterface.get_base_control().get_tree().call_deferred("quit")
- return {"data": {"status": "quitting", "message": "Editor quit initiated"}}
-
-
-func game_eval(params: Dictionary) -> Dictionary:
- var code: String = params.get("code", "")
- if code.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "code is required")
-
- if _debugger_plugin == null or _connection == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Debugger bridge unavailable — plugin may not be fully initialised")
-
- if not EditorInterface.is_playing_scene():
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_GAME_NOT_RUNNING,
- "Game is not running — start the project first", false,
- "Start the game with project_run (or wait for the user to run it), then retry.")
-
- var request_id: String = params.get("_request_id", "")
- if request_id.is_empty():
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Missing request_id — cannot correlate deferred response")
-
- _debugger_plugin.request_game_eval(code, request_id, _connection)
- return McpDispatcher.DEFERRED_RESPONSE
-
-
-func game_debug_control(params: Dictionary) -> Dictionary:
- var action := str(params.get("action", ""))
- if action not in ["suspend", "resume", "next_frame", "debug_status"]:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid game debug action '%s' — use suspend, resume, next_frame, or debug_status" % action,
- )
- if _debugger_plugin == null or _connection == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Debugger bridge unavailable — plugin may not be fully initialised")
- if not EditorInterface.is_playing_scene():
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_GAME_NOT_RUNNING,
- "Game is not running — start the project first", false,
- "Start the game with project_run (or wait for the user to run it), then retry.")
- var request_id := str(params.get("_request_id", ""))
- if request_id.is_empty():
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Missing internal _request_id — cannot correlate deferred response")
- _debugger_plugin.request_game_debug_control(action, request_id, _connection)
- return McpDispatcher.DEFERRED_RESPONSE
-
-
-func game_command(params: Dictionary) -> Dictionary:
- var op: String = str(params.get("op", ""))
- if op.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "op is required")
-
- if _debugger_plugin == null or _connection == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Debugger bridge unavailable — plugin may not be fully initialised")
-
- if not EditorInterface.is_playing_scene():
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_GAME_NOT_RUNNING,
- "Game is not running — start the project first", false,
- "Start the game with project_run (or wait for the user to run it), then retry.")
-
- var request_id: String = params.get("_request_id", "")
- if request_id.is_empty():
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Missing request_id — cannot correlate deferred response")
-
- var command_params: Dictionary = params.get("params", {})
-
- ## input_sequence steps the game forward frame-by-frame in one call, so it
- ## needs a far larger budget than the one-shot game ops that share the
- ## `game_command` deferred entry (15s). Widen both timers only for it: the
- ## debugger-side pending timer (below) and the dispatcher-side deferred
- ## budget (via the sentinel's `_deferred_timeout_ms`). Every other op keeps
- ## request_game_command's tight default.
- if op == "input_sequence":
- _debugger_plugin.request_game_command(
- op, command_params, request_id, _connection, INPUT_SEQUENCE_TIMEOUT_SEC
- )
- return {
- "_deferred": true,
- "_deferred_timeout_ms": int(INPUT_SEQUENCE_TIMEOUT_SEC * 1000.0),
- }
-
- _debugger_plugin.request_game_command(op, command_params, request_id, _connection)
- return McpDispatcher.DEFERRED_RESPONSE
diff --git a/addons/godot_ai/handlers/editor_handler.gd.uid b/addons/godot_ai/handlers/editor_handler.gd.uid
deleted file mode 100644
index 16d785f..0000000
--- a/addons/godot_ai/handlers/editor_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dcro7yc8bor6v
diff --git a/addons/godot_ai/handlers/environment_handler.gd b/addons/godot_ai/handlers/environment_handler.gd
deleted file mode 100644
index 3722c3e..0000000
--- a/addons/godot_ai/handlers/environment_handler.gd
+++ /dev/null
@@ -1,181 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Creates an Environment (+ optional Sky + ProceduralSkyMaterial) chain and
-## either assigns it to a WorldEnvironment node or saves it to a .tres file.
-## Bundles sub-resource creation + assignment in a single undo action.
-
-const ResourceHandler := preload("res://addons/godot_ai/handlers/resource_handler.gd")
-
-var _undo_redo: EditorUndoRedoManager
-var _connection: McpConnection
-
-
-func _init(undo_redo: EditorUndoRedoManager, connection: McpConnection = null) -> void:
- _undo_redo = undo_redo
- _connection = connection
-
-
-const _PRESETS := {
- "default": {"sky": true, "fog": false},
- "clear": {"sky": true, "fog": false},
- "sunset": {"sky": true, "fog": false},
- "night": {"sky": true, "fog": false},
- "fog": {"sky": true, "fog": true},
-}
-
-
-func create_environment(params: Dictionary) -> Dictionary:
- var node_path: String = params.get("path", "")
- var resource_path: String = params.get("resource_path", "")
- var overwrite: bool = params.get("overwrite", false)
- var preset: String = params.get("preset", "default")
- var properties: Dictionary = params.get("properties", {})
- var sky_param = params.get("sky", null) # nullable — falls back to preset default
-
- # environment_create targets the whole WorldEnvironment node (no separate
- # `property` param) — pass require_property=false.
- var home_err := McpResourceIO.validate_home(params, false)
- if home_err != null:
- return home_err
-
- if not _PRESETS.has(preset):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid preset '%s'. Valid: %s" % [preset, ", ".join(_PRESETS.keys())]
- )
-
- var preset_config: Dictionary = _PRESETS[preset]
- var want_sky: bool = preset_config.sky
- var sky_properties: Dictionary = {}
- if sky_param != null:
- if sky_param is bool:
- want_sky = sky_param
- elif sky_param is Dictionary:
- var sky_config: Dictionary = (sky_param as Dictionary).duplicate()
- var material_type: String = String(sky_config.get("sky_material", "procedural")).to_lower()
- if material_type != "procedural":
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "sky.sky_material must be 'procedural' when sky is a dictionary"
- )
- sky_config.erase("sky_material")
- sky_properties = sky_config
- want_sky = true
- else:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "sky must be a bool, null, or dictionary of ProceduralSkyMaterial properties"
- )
-
- var env := Environment.new()
- var sky: Sky = null
- var sky_material: ProceduralSkyMaterial = null
- if want_sky:
- sky_material = ProceduralSkyMaterial.new()
- sky = Sky.new()
- sky.sky_material = sky_material
- env.background_mode = Environment.BG_SKY
- env.sky = sky
- else:
- env.background_mode = Environment.BG_CLEAR_COLOR
-
- _apply_preset(env, sky_material, preset)
- if not sky_properties.is_empty():
- var sky_apply_err := ResourceHandler._apply_resource_properties(sky_material, sky_properties)
- if sky_apply_err != null:
- return sky_apply_err
- if preset_config.fog:
- env.volumetric_fog_enabled = true
- env.volumetric_fog_density = 0.03
-
- if not properties.is_empty():
- var apply_err := ResourceHandler._apply_resource_properties(env, properties)
- if apply_err != null:
- return apply_err
-
- if not resource_path.is_empty():
- return _save_environment(env, sky, sky_material, resource_path, overwrite, preset)
- return _assign_environment(env, sky, sky_material, node_path, preset)
-
-
-static func _apply_preset(env: Environment, sky_material: ProceduralSkyMaterial, preset: String) -> void:
- match preset:
- "default", "clear":
- if sky_material != null:
- sky_material.sky_top_color = Color(0.38, 0.45, 0.55)
- sky_material.sky_horizon_color = Color(0.65, 0.67, 0.7)
- sky_material.ground_horizon_color = Color(0.65, 0.67, 0.7)
- sky_material.ground_bottom_color = Color(0.2, 0.17, 0.13)
- sky_material.sun_angle_max = 30.0
- env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
- env.ambient_light_energy = 1.0
- "sunset":
- if sky_material != null:
- sky_material.sky_top_color = Color(0.25, 0.3, 0.55)
- sky_material.sky_horizon_color = Color(1.0, 0.55, 0.3)
- sky_material.ground_horizon_color = Color(0.85, 0.4, 0.25)
- sky_material.ground_bottom_color = Color(0.2, 0.12, 0.1)
- env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
- env.ambient_light_color = Color(1.0, 0.75, 0.55)
- env.ambient_light_energy = 0.8
- "night":
- if sky_material != null:
- sky_material.sky_top_color = Color(0.02, 0.02, 0.07)
- sky_material.sky_horizon_color = Color(0.05, 0.07, 0.15)
- sky_material.ground_horizon_color = Color(0.04, 0.05, 0.1)
- sky_material.ground_bottom_color = Color(0.0, 0.0, 0.02)
- env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
- env.ambient_light_color = Color(0.2, 0.22, 0.35)
- env.ambient_light_energy = 0.4
- "fog":
- if sky_material != null:
- sky_material.sky_top_color = Color(0.65, 0.65, 0.7)
- sky_material.sky_horizon_color = Color(0.8, 0.8, 0.82)
- sky_material.ground_horizon_color = Color(0.7, 0.7, 0.72)
- sky_material.ground_bottom_color = Color(0.3, 0.3, 0.32)
- env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
- env.ambient_light_energy = 0.7
-
-
-func _assign_environment(env: Environment, sky: Sky, sky_material: ProceduralSkyMaterial, node_path: String, preset: String) -> Dictionary:
- var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var _scene_root: Node = _resolved.scene_root
- if not (node is WorldEnvironment):
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Node at %s is %s — must be WorldEnvironment" % [node_path, node.get_class()]
- )
-
- var old_env = (node as WorldEnvironment).environment
-
- _undo_redo.create_action("MCP: Create Environment (%s) for %s" % [preset, node.name])
- _undo_redo.add_do_property(node, "environment", env)
- _undo_redo.add_undo_property(node, "environment", old_env)
- _undo_redo.add_do_reference(env)
- if sky != null:
- _undo_redo.add_do_reference(sky)
- if sky_material != null:
- _undo_redo.add_do_reference(sky_material)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "preset": preset,
- "sky_created": sky != null,
- "sky_material_class": sky_material.get_class() if sky_material != null else "",
- "undoable": true,
- }
- }
-
-
-func _save_environment(env: Environment, _sky: Sky, _sky_material: ProceduralSkyMaterial, resource_path: String, overwrite: bool, preset: String) -> Dictionary:
- return McpResourceIO.save_to_disk(env, resource_path, overwrite, "Environment", {
- "preset": preset,
- }, _connection)
diff --git a/addons/godot_ai/handlers/environment_handler.gd.uid b/addons/godot_ai/handlers/environment_handler.gd.uid
deleted file mode 100644
index f495f8f..0000000
--- a/addons/godot_ai/handlers/environment_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b1k7jldwjp5jt
diff --git a/addons/godot_ai/handlers/filesystem_handler.gd b/addons/godot_ai/handlers/filesystem_handler.gd
deleted file mode 100644
index 16c46f5..0000000
--- a/addons/godot_ai/handlers/filesystem_handler.gd
+++ /dev/null
@@ -1,320 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-const ScriptHandler := preload("res://addons/godot_ai/handlers/script_handler.gd")
-
-## Handles file read/write operations and reimport within the Godot project.
-
-## Bounds for the deferred scan wait. `write_file`/`reimport` register single
-## files with `update_file()` (cheap, no global-class rebuild); `scan_filesystem`
-## is the heavier, explicit "rebuild the class registry" path agents call after
-## adding `class_name` scripts headlessly (no window focus to trigger it).
-## Kept under the dispatcher's "scan_filesystem" deferred timeout (30s) so we
-## always send a real reply before a DEFERRED_TIMEOUT is synthesised.
-const _SCAN_START_GRACE_MSEC := 750
-const _SCAN_SETTLE_MAX_MSEC := 28000
-
-## Sidecar the editor writes next to every imported resource. `reimport` reads
-## it to tell imported assets from files that merely have a filesystem entry
-## (see `_is_imported_resource`).
-const IMPORT_SIDECAR_SUFFIX := ".import"
-
-## Shared single-flight latch for scan_filesystem. `is_scanning()` alone can't
-## enforce single-flight: `EditorFileSystem.scan()` doesn't flip `is_scanning()`
-## for a frame or two (hence _SCAN_START_GRACE_MSEC), so a second request landing
-## in that window would observe `false` and stack another scan() — the exact
-## stacked-worker SIGABRT this op exists to avoid (dsarno/godot#6). The latch is
-## set before the first scan() and cleared when its settle coroutine finishes;
-## concurrent requests coalesce onto the running scan instead of starting one.
-## `static` so it's shared across handler instances; it resets on plugin reload
-## (script re-parse), which self-heals any latch orphaned by a mid-await teardown.
-static var _scan_in_flight := false
-
-var _connection: McpConnection
-
-
-func _init(connection: McpConnection = null) -> void:
- _connection = connection
-
-
-func read_file(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
-
- var path_err = McpPathValidator.path_error(path, "path")
- if path_err != null:
- return path_err
-
- if not FileAccess.file_exists(path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "File not found: %s" % path)
-
- var file := FileAccess.open(path, FileAccess.READ)
- if file == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to open file: %s" % path)
-
- var content := file.get_as_text()
- file.close()
-
- return {
- "data": {
- "path": path,
- "content": content,
- "size": content.length(),
- "line_count": content.count("\n") + (1 if not content.is_empty() else 0),
- }
- }
-
-
-func write_file(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
- var content: String = params.get("content", "")
-
- var path_err = McpPathValidator.path_error(path, "path", true)
- if path_err != null:
- return path_err
-
- var existed_before := FileAccess.file_exists(path)
-
- # Shared write path (#714): parent mkdir + write/flush + explicit error
- # check live on McpResourceIO so this can't drift from create_script.
- var write_failure: Variant = McpResourceIO.write_text_to_disk(path, content)
- if write_failure != null:
- return write_failure
-
- # Single-file register, not a full scan() — a scan() per write stacks
- # filesystem WorkerThreadPool tasks under concurrent writes and can SIGABRT
- # in the global-class update (see dsarno/godot#6 and create_script in
- # script_handler.gd). update_file() is what reimport()/material/theme use.
- var efs := EditorInterface.get_resource_filesystem()
- if efs != null:
- efs.update_file(path)
-
- var data := {
- "path": path,
- "size": content.length(),
- "undoable": false,
- "reason": "File system operations cannot be undone via editor undo",
- }
- var is_gdscript := path.ends_with(".gd")
- ## A .gd written through the filesystem tool used to skip the parse
- ## diagnostics create_script attaches (#714) — the agent's broken
- ## script reported plain success and the parse error surfaced only in
- ## later editor logs. Same shared check, same response fields. A bare
- ## ScriptHandler works here: the diagnostics path touches no instance
- ## state (it stays an instance method only for test stubbing).
- if is_gdscript:
- ScriptHandler.new(null)._attach_gdscript_diagnostics(data, path, content)
- data["committed"] = true
- data["import_settled"] = existed_before
- data["import_settle"] = "already_known" if existed_before else "not_waited"
- McpResourceIO.attach_cleanup_hint(data, existed_before, [path])
-
- ## Fresh `.gd` writes take create_script's import-settle deferral (#714,
- ## #261): reply only once ResourceLoader can see the new resource (or the
- ## bounded window elapses), so write_file -> script_attach back-to-back
- ## can't 404 on the not-yet-imported script. This CHANGES write_file's
- ## response timing for that case — the reply lands up to
- ## McpResourceIO.IMPORT_SETTLE_MAX_MSEC later instead of immediately.
- ## Scoped to .gd: ResourceLoader never learns plain text files, so an
- ## unconditional wait would burn the full window on every fresh .txt.
- ## Overwrites, batch_execute (no request_id) and unit-test contexts (no
- ## connection) keep the synchronous reply.
- var request_id: String = params.get("_request_id", "")
- if is_gdscript and not existed_before and _connection != null and not request_id.is_empty():
- McpResourceIO.finish_text_write_deferred(_connection, request_id, path, data)
- return McpDispatcher.DEFERRED_RESPONSE
-
- return {"data": data}
-
-
-func reimport(params: Dictionary) -> Dictionary:
- var paths: Array = params.get("paths", [])
-
- if paths.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: paths (non-empty array)")
-
- var efs := EditorInterface.get_resource_filesystem()
- if efs == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_UNAVAILABLE,
- "EditorFileSystem not available", false)
-
- var reimported: Array[String] = []
- var skipped_non_imported: Array[String] = []
- var not_found: Array[String] = []
-
- for path_variant in paths:
- var path: String = str(path_variant)
- var path_err := McpPathValidator.validate_resource_path(path)
- if not path_err.is_empty():
- not_found.append("%s (%s)" % [path, path_err])
- continue
- if not FileAccess.file_exists(path):
- not_found.append("%s (file does not exist)" % path)
- continue
- efs.update_file(path)
- if _is_imported_resource(path):
- reimported.append(path)
- else:
- skipped_non_imported.append(path)
-
- var data := {
- "reimported": reimported,
- "skipped_non_imported": skipped_non_imported,
- "not_found": not_found,
- "reimported_count": reimported.size(),
- "skipped_non_imported_count": skipped_non_imported.size(),
- "not_found_count": not_found.size(),
- "undoable": false,
- "reason": "Reimport is a file system operation",
- }
- ## Only when it applies: a hint on every call would cost tokens on the
- ## all-assets path this op is actually for.
- if not skipped_non_imported.is_empty():
- data["skipped_non_imported_hint"] = (
- "%d path(s) are not imported resources. Their editor filesystem entry was "
- + "refreshed, but no import ran — a success here is not evidence that a "
- + "script parsed or that diagnostics were produced. Use script_patch/"
- + "script_create for GDScript diagnostics, or filesystem_manage(op=\"scan\") "
- + "for an asset the editor has not imported yet."
- ) % skipped_non_imported.size()
- return {"data": data}
-
-
-## #778: `update_file()` registers a path with the resource pipeline; it only
-## runs an *import* for files that have one. Scripts, scenes and hand-written
-## `.tres` are not imported resources, so listing them under `reimported` reads
-## as proof that a parse or import ran when nothing did.
-##
-## The `.import` sidecar is the editor's own record that a path goes through
-## the import pipeline, so it decides the split. An extension allow-list was
-## rejected: importers come and go with plugins, so the list would drift out of
-## agreement with the editor it claims to describe.
-##
-## Known edge: an asset the editor has never imported (just written, no scan
-## yet) has no sidecar and reports as non-imported. That is accurate at the
-## moment of the call — `update_file()` did not import it either — and the
-## hint names `scan` as the way through.
-##
-## Behaviour is unchanged for every path: `update_file()` still runs on all of
-## them, because refreshing an externally-edited `.tscn`/`.tres` is a real use
-## of this op. This splits the report, not the work.
-static func _is_imported_resource(path: String) -> bool:
- if path.ends_with(IMPORT_SIDECAR_SUFFIX):
- return false ## The sidecar itself is not an imported resource.
- return FileAccess.file_exists(path + IMPORT_SIDECAR_SUFFIX)
-
-
-## Force a full EditorFileSystem scan and wait for it to settle. This is the
-## headless equivalent of the editor regaining window focus: `update_file()`
-## (used by write_file/reimport/script_create) registers a single file with the
-## resource pipeline but does NOT rebuild the global `class_name` table, so a
-## freshly-created `class_name MyThing extends Resource` stays invisible to
-## `ClassDB`/`ProjectSettings.get_global_class_list()` until a scan runs. Agents
-## driving the editor without focus call this once after a batch of script
-## creates to make new types instantiable/referenceable. See issue #83.
-func scan_filesystem(params: Dictionary) -> Dictionary:
- var efs := EditorInterface.get_resource_filesystem()
- if efs == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_UNAVAILABLE,
- "EditorFileSystem not available", false)
-
- var request_id: String = params.get("_request_id", "")
- # Async path: a scan can't be awaited on the calling frame without freezing
- # the editor, so hand control back to the dispatcher (DEFERRED_RESPONSE) and
- # push the real reply from a static coroutine once the scan settles — by
- # which point new class_names are registered.
- if _connection != null and not request_id.is_empty():
- _finish_scan_deferred(_connection, request_id, efs)
- return McpDispatcher.DEFERRED_RESPONSE
-
- # Synchronous fallback: batch_execute (no request_id) and unit-test contexts
- # (no connection) can't await, so kick a single-flight scan and return
- # immediately without the settle confirmation. Respect the latch so we don't
- # stack onto a deferred scan; don't set it (there's no coroutine here to
- # clear it — the brief is_scanning() window covers the rest).
- var already := _scan_in_flight or efs.is_scanning()
- if not already:
- efs.scan()
- return {
- "data": {
- "scan_completed": false,
- "scan_settle": "not_waited",
- "was_already_scanning": already,
- "global_class_count": ProjectSettings.get_global_class_list().size(),
- # Present in both paths for a consistent response shape; the sync
- # path doesn't await, so it can't measure a delta.
- "global_classes_registered_delta": 0,
- "undoable": false,
- "reason": "Filesystem scan is an editor operation",
- }
- }
-
-
-## `static` is load-bearing for the same reason as ScriptHandler's deferred
-## finish: the coroutine must outlive the handler RefCounted, which can be freed
-## mid-await (e.g. an editor_reload_plugin fired during the scan). Parameterise
-## everything; reference no instance state.
-static func _finish_scan_deferred(
- connection: McpConnection,
- request_id: String,
- efs: EditorFileSystem,
-) -> void:
- var work := ScriptWork.begin("scan_filesystem")
- await _settle_scan(connection, request_id, efs)
- ScriptWork.finish(work)
-
-
-static func _settle_scan(
- connection: McpConnection, request_id: String, efs: EditorFileSystem,
-) -> void:
- if not is_instance_valid(connection):
- return
- var tree := connection.get_tree()
- if tree == null:
- return
- var classes_before := ProjectSettings.get_global_class_list().size()
- # Single-flight via the shared `_scan_in_flight` latch (NOT is_scanning(),
- # which lags scan() by a frame or two — see the latch declaration). Only the
- # request that sets the latch calls scan(); concurrent requests coalesce and
- # just await the running scan. This is what actually prevents the stacked
- # scan() SIGABRT (dsarno/godot#6), even within the start-grace window.
- var was_already_scanning := _scan_in_flight or efs.is_scanning()
- var we_started := not was_already_scanning
- if we_started:
- _scan_in_flight = true
- efs.scan()
- # Hand back a frame so _dispatch() registers this request as deferred before
- # the coroutine can push a reply (mirrors McpResourceIO.finish_text_write_deferred).
- await tree.process_frame
- var deadline_ms := Time.get_ticks_msec() + _SCAN_SETTLE_MAX_MSEC
- var start_grace_ms := Time.get_ticks_msec() + _SCAN_START_GRACE_MSEC
- var saw_scanning := efs.is_scanning()
- while Time.get_ticks_msec() < deadline_ms:
- if efs.is_scanning():
- saw_scanning = true
- elif saw_scanning or Time.get_ticks_msec() > start_grace_ms:
- # Either the scan ran and finished, or it never flipped is_scanning()
- # within the grace window (a no-op scan because nothing changed).
- break
- await tree.process_frame
- # Clear the latch in all paths (no try/finally in GDScript): do it before the
- # is_instance_valid early-return so a freed connection can't orphan it.
- if we_started:
- _scan_in_flight = false
- if not is_instance_valid(connection):
- return
- var completed := not efs.is_scanning()
- var classes_after := ProjectSettings.get_global_class_list().size()
- connection.send_deferred_response(request_id, {
- "data": {
- "scan_completed": completed,
- "scan_settle": "settled" if completed else "timeout",
- "was_already_scanning": was_already_scanning,
- "global_class_count": classes_after,
- "global_classes_registered_delta": classes_after - classes_before,
- "undoable": false,
- "reason": "Filesystem scan is an editor operation",
- }
- })
diff --git a/addons/godot_ai/handlers/filesystem_handler.gd.uid b/addons/godot_ai/handlers/filesystem_handler.gd.uid
deleted file mode 100644
index 94135a4..0000000
--- a/addons/godot_ai/handlers/filesystem_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c7ovtpdiumtju
diff --git a/addons/godot_ai/handlers/gridmap_handler.gd b/addons/godot_ai/handlers/gridmap_handler.gd
deleted file mode 100644
index 8d8544e..0000000
--- a/addons/godot_ai/handlers/gridmap_handler.gd
+++ /dev/null
@@ -1,192 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-## GridMap authoring — set, fill, clear, and read 3D cells plus mesh-library
-## items directly in the editor scene with full undo/redo support.
-##
-## All ops target GridMap nodes in the currently edited scene by
-## scene-relative path (e.g. "/Main/Terrain"). All write ops are undoable
-## via EditorUndoRedoManager.
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-const MAX_FILL_CELLS := 4096
-
-var _undo_redo: EditorUndoRedoManager
-
-
-func _init(undo_redo: EditorUndoRedoManager) -> void:
- _undo_redo = undo_redo
-
-
-## Set a single cell item. item = -1 erases the cell.
-## params: {path, item, map_x, map_y, map_z, orientation=0}
-## Returns: {map_x, map_y, map_z, item, orientation, undoable}
-func set_item(params: Dictionary) -> Dictionary:
- var gm := _resolve_gridmap(params)
- if gm.has("error"): return gm
- var node: GridMap = gm.node
- var pos := Vector3i(int(params.get("map_x", 0)), int(params.get("map_y", 0)), int(params.get("map_z", 0)))
- var item := int(params.get("item", 0))
- var orientation := int(params.get("orientation", 0))
- if orientation < 0 or orientation > 24:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "orientation must be in 0..24 (GridMap baked rotations), got %d" % orientation)
- var prev := _capture_cell_state(node, pos)
- _undo_redo.create_action("MCP: GridMap set_item")
- _undo_redo.add_do_method(node, "set_cell_item", pos, item, orientation)
- _undo_redo.add_undo_method(self, "_restore_cell_state", node, pos, prev)
- _undo_redo.commit_action()
- return {"data": {"map_x": pos.x, "map_y": pos.y, "map_z": pos.z,
- "item": item, "orientation": orientation, "undoable": true}}
-
-
-## Fill a box region with one item in a single undo action.
-## params: {path, item, rect_x, rect_y, rect_z, rect_w, rect_h, rect_d, orientation=0}
-## Returns: {cells_filled, rect: {x, y, z, w, h, d}}
-func fill(params: Dictionary) -> Dictionary:
- var gm := _resolve_gridmap(params)
- if gm.has("error"): return gm
- var node: GridMap = gm.node
- var item := int(params.get("item", 0))
- var orientation := int(params.get("orientation", 0))
- if orientation < 0 or orientation > 24:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "orientation must be in 0..24 (GridMap baked rotations), got %d" % orientation)
- var rx := int(params.get("rect_x", 0)); var ry := int(params.get("rect_y", 0)); var rz := int(params.get("rect_z", 0))
- var rw := int(params.get("rect_w", 1)); var rh := int(params.get("rect_h", 1)); var rd := int(params.get("rect_d", 1))
- if rw <= 0 or rh <= 0 or rd <= 0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "rect_w, rect_h and rect_d must be > 0 (got %d x %d x %d)" % [rw, rh, rd])
- var cell_count := rw * rh * rd
- if cell_count > MAX_FILL_CELLS:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "Region too large: %d cells exceeds max %d" % [cell_count, MAX_FILL_CELLS])
- var snapshot: Array[Dictionary] = []
- for x in range(rx, rx + rw):
- for y in range(ry, ry + rh):
- for z in range(rz, rz + rd):
- var pos := Vector3i(x, y, z)
- snapshot.append({"pos": pos, "state": _capture_cell_state(node, pos)})
- _undo_redo.create_action("MCP: GridMap fill %dx%dx%d" % [rw, rh, rd])
- ## First callback targets the node so the action lands in the scene undo
- ## history (first-target routing); _apply_fill batches the rest of the
- ## region into a single history entry instead of one per cell.
- _undo_redo.add_do_method(node, "set_cell_item", snapshot[0].pos, item, orientation)
- _undo_redo.add_do_method(self, "_apply_fill", node, snapshot, item, orientation)
- _undo_redo.add_undo_method(self, "_restore_rect_snapshot", node, snapshot)
- _undo_redo.commit_action()
- return {"data": {"cells_filled": snapshot.size(),
- "rect": {"x": rx, "y": ry, "z": rz, "w": rw, "h": rh, "d": rd}, "undoable": true}}
-
-
-## Remove all cells from the GridMap.
-## params: {path}
-## Returns: {cleared: true}
-func clear_layer(params: Dictionary) -> Dictionary:
- var gm := _resolve_gridmap(params)
- if gm.has("error"): return gm
- var node: GridMap = gm.node
- var used := node.get_used_cells()
- if used.size() > MAX_FILL_CELLS:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "GridMap has %d cells, exceeds max %d for undoable clear"
- % [used.size(), MAX_FILL_CELLS])
- var snapshot := _capture_used_cells_snapshot(node)
- _undo_redo.create_action("MCP: GridMap clear")
- _undo_redo.add_do_method(node, "clear")
- _undo_redo.add_undo_method(self, "_restore_cells_snapshot", node, snapshot)
- _undo_redo.commit_action()
- return {"data": {"cleared": true, "undoable": true}}
-
-
-## Return all used cell coordinates.
-## params: {path}
-## Returns: {cells: [{x, y, z}, ...], count: int}
-func get_used_cells(params: Dictionary) -> Dictionary:
- var gm := _resolve_gridmap(params)
- if gm.has("error"): return gm
- var node: GridMap = gm.node
- var result: Array = []
- for c in node.get_used_cells():
- result.append({"x": c.x, "y": c.y, "z": c.z})
- return {"data": {"cells": result, "count": result.size()}}
-
-
-## List the items available in the GridMap's MeshLibrary, so agents can
-## discover item ids and names before placing cells (the 3D analogue of
-## tileset atlas inspection).
-## params: {path}
-## Returns: {library: res:// path or "", items: [{item, name, mesh}...], count}
-func list_library_items(params: Dictionary) -> Dictionary:
- var gm := _resolve_gridmap(params)
- if gm.has("error"): return gm
- var node: GridMap = gm.node
- var library: MeshLibrary = node.mesh_library
- if library == null:
- return {"data": {"library": "", "items": [], "count": 0}}
- var items: Array = []
- var ids := library.get_item_list()
- ids.sort()
- for item in ids:
- var mesh: Mesh = library.get_item_mesh(item)
- var mesh_path := mesh.resource_path if mesh != null else ""
- items.append({
- "item": item,
- "name": library.get_item_name(item),
- "mesh": mesh_path,
- })
- return {"data": {"library": library.resource_path, "items": items, "count": items.size()}}
-
-
-## Resolve a GridMap node from params["path"] in the currently edited
-## scene. Returns {"node": GridMap} on success, or an error dict.
-func _resolve_gridmap(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
- var scene_file: String = params.get("scene_file", "")
- var resolved := McpNodeValidator.resolve_or_error(path, "path", scene_file)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- if not node is GridMap:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
- "Node is not a GridMap: %s" % path)
- return {"node": node}
-
-
-func _capture_cell_state(node: GridMap, pos: Vector3i) -> Dictionary:
- var item := node.get_cell_item(pos)
- if item == -1:
- return {"has_item": false}
- return {"has_item": true, "item": item, "orientation": node.get_cell_item_orientation(pos)}
-
-
-func _capture_used_cells_snapshot(node: GridMap) -> Array[Dictionary]:
- var snapshot: Array[Dictionary] = []
- for pos in node.get_used_cells():
- snapshot.append({"pos": pos, "state": _capture_cell_state(node, pos)})
- return snapshot
-
-
-func _restore_cells_snapshot(node: GridMap, snapshot: Array[Dictionary]) -> void:
- node.clear()
- for entry in snapshot:
- _restore_cell_state(node, entry.pos, entry.state)
-
-
-## Batched do-method for fill: one undo-history entry applies the whole
-## region instead of one entry per cell.
-func _apply_fill(node: GridMap, snapshot: Array[Dictionary], item: int, orientation: int) -> void:
- for entry in snapshot:
- node.set_cell_item(entry.pos, item, orientation)
-
-
-func _restore_rect_snapshot(node: GridMap, snapshot: Array[Dictionary]) -> void:
- for entry in snapshot:
- _restore_cell_state(node, entry.pos, entry.state)
-
-
-func _restore_cell_state(node: GridMap, pos: Vector3i, state: Dictionary) -> void:
- if not state.get("has_item", false):
- node.set_cell_item(pos, -1)
- return
- node.set_cell_item(pos, int(state.get("item", 0)), int(state.get("orientation", 0)))
diff --git a/addons/godot_ai/handlers/gridmap_handler.gd.uid b/addons/godot_ai/handlers/gridmap_handler.gd.uid
deleted file mode 100644
index 35bb1d0..0000000
--- a/addons/godot_ai/handlers/gridmap_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cdv1yrjeyo5es
diff --git a/addons/godot_ai/handlers/input_handler.gd b/addons/godot_ai/handlers/input_handler.gd
deleted file mode 100644
index d423ee7..0000000
--- a/addons/godot_ai/handlers/input_handler.gd
+++ /dev/null
@@ -1,462 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Handles input action listing, creation, removal, and event binding.
-## Actions are persisted via ProjectSettings so they survive editor restarts.
-
-
-func list_actions(params: Dictionary) -> Dictionary:
- var include_builtin: bool = params.get("include_builtin", false)
- ## Authoritative source for user-authored actions is the ``[input]``
- ## section of ``project.godot``. ``ProjectSettings.has_setting`` is not
- ## reliable here because Godot registers ``ui_*`` defaults via
- ## ``GLOBAL_DEF_BASIC``, which makes ``has_setting`` return true for
- ## them. Reading the file via ``ConfigFile`` distinguishes the user's
- ## entries from engine-registered defaults regardless of namespace.
- ## See #213.
- var user_authored := _read_user_authored_actions()
- var actions: Array[Dictionary] = []
- var seen := {}
- for action_name in InputMap.get_actions():
- var name_str := str(action_name)
- var is_user_action := user_authored.has(name_str)
- if not include_builtin and not is_user_action:
- continue
- seen[name_str] = true
- var events: Array[Dictionary] = []
- for event in InputMap.action_get_events(action_name):
- events.append(_serialize_event(event))
- actions.append({
- "name": name_str,
- "events": events,
- "event_count": events.size(),
- "is_builtin": not is_user_action,
- "loaded_in_input_map": true,
- })
- for action_name in user_authored.keys():
- var name_str := str(action_name)
- if seen.has(name_str):
- continue
- var setting: Dictionary = user_authored.get(name_str, {})
- var events: Array[Dictionary] = []
- for event in setting.get("events", []):
- if event is InputEvent:
- events.append(_serialize_event(event))
- else:
- events.append({"type": type_string(typeof(event)), "string": str(event)})
- actions.append({
- "name": name_str,
- "events": events,
- "event_count": events.size(),
- "is_builtin": false,
- "loaded_in_input_map": false,
- })
- return {"data": {"actions": actions, "count": actions.size()}}
-
-
-func _read_user_authored_actions() -> Dictionary:
- var cfg := ConfigFile.new()
- if cfg.load("res://project.godot") != OK:
- return {}
- if not cfg.has_section("input"):
- return {}
- var result: Dictionary = {}
- for key in cfg.get_section_keys("input"):
- var value = cfg.get_value("input", key, {})
- result[key] = value if value is Dictionary else {}
- return result
-
-
-func add_action(params: Dictionary) -> Dictionary:
- var action: String = params.get("action", "")
- var deadzone: float = params.get("deadzone", 0.5)
-
- if action.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: action")
-
- var deadzone_error := _validate_deadzone(deadzone)
- if deadzone_error.has("error"):
- return deadzone_error
-
- if InputMap.has_action(action):
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "Action '%s' already exists" % action)
-
- InputMap.add_action(action, deadzone)
-
- var key := "input/%s" % action
- ProjectSettings.set_setting(key, {
- "deadzone": deadzone,
- "events": [],
- })
- var err := ProjectSettings.save()
- if err != OK:
- InputMap.erase_action(action)
- ProjectSettings.clear(key)
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Failed to save project settings while adding action '%s': %s (error %d)" % [action, error_string(err), err])
-
- return {
- "data": {
- "action": action,
- "deadzone": deadzone,
- "undoable": false,
- "reason": "Input actions are saved to project.godot",
- }
- }
-
-
-func ensure_action(params: Dictionary) -> Dictionary:
- var action: String = params.get("action", "")
- var deadzone: float = params.get("deadzone", 0.5)
-
- if action.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: action")
-
- var deadzone_error := _validate_deadzone(deadzone)
- if deadzone_error.has("error"):
- return deadzone_error
-
- var result := _ensure_action_state(action, deadzone)
- if result.has("error"):
- return result
- return {"data": result}
-
-
-func remove_action(params: Dictionary) -> Dictionary:
- var action: String = params.get("action", "")
- if action.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: action")
-
- var key := "input/%s" % action
- var was_loaded := InputMap.has_action(action)
- var old_setting = ProjectSettings.get_setting(key) if ProjectSettings.has_setting(key) else null
-
- ## An action can live in the editor process's InputMap, in project.godot,
- ## or both. Actions persisted by a previous editor session exist only on
- ## disk (`loaded_in_input_map: false` in list_actions) — those must still
- ## be removable. #632
- if not was_loaded and old_setting == null:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Action '%s' not found" % action)
-
- if was_loaded:
- InputMap.erase_action(action)
-
- if old_setting != null:
- ProjectSettings.clear(key)
- var err := ProjectSettings.save()
- if err != OK:
- if was_loaded:
- var dz: float = old_setting.get("deadzone", 0.5) if old_setting is Dictionary else 0.5
- InputMap.add_action(action, dz)
- if old_setting is Dictionary:
- for ev in old_setting.get("events", []):
- if ev is InputEvent:
- InputMap.action_add_event(action, ev)
- ProjectSettings.set_setting(key, old_setting)
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Failed to save project settings while removing action '%s': %s (error %d)" % [action, error_string(err), err])
-
- return {
- "data": {
- "action": action,
- "removed": true,
- "was_loaded": was_loaded,
- "undoable": false,
- "reason": "Input actions are saved to project.godot",
- }
- }
-
-
-func bind_event(params: Dictionary) -> Dictionary:
- var action: String = params.get("action", "")
- var event_type: String = params.get("event_type", "")
-
- if action.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: action")
- if event_type.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: event_type")
-
- if not InputMap.has_action(action):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "Action '%s' not found. Call input_map_manage(op='add_action', params={action: '%s'}) first." % [action, action])
-
- var event_or_error = _create_event(event_type, params)
- if event_or_error is Dictionary:
- return event_or_error
- var event: InputEvent = event_or_error
-
- InputMap.action_add_event(action, event)
-
- var err := _save_action_events(action)
- if err != OK:
- InputMap.action_erase_event(action, event)
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Failed to save project settings while binding event to action '%s': %s (error %d)" % [action, error_string(err), err])
-
- return {
- "data": {
- "action": action,
- "event": _serialize_event(event),
- "undoable": false,
- "reason": "Input bindings are saved to project.godot",
- }
- }
-
-
-func ensure_binding(params: Dictionary) -> Dictionary:
- var action: String = params.get("action", "")
- var event_type: String = params.get("event_type", "")
- var deadzone: float = params.get("deadzone", 0.5)
-
- if action.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: action")
- if event_type.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: event_type")
-
- var deadzone_error := _validate_deadzone(deadzone)
- if deadzone_error.has("error"):
- return deadzone_error
-
- var event_or_error = _create_event(event_type, params)
- if event_or_error is Dictionary:
- return event_or_error
- var event: InputEvent = event_or_error
-
- var ensured := _ensure_action_state(action, deadzone)
- if ensured.has("error"):
- return ensured
-
- for existing in InputMap.action_get_events(action):
- if _events_match(existing, event):
- return {
- "data": {
- "action": action,
- "event": _serialize_event(existing),
- "already_bound": true,
- "action_created": ensured.get("created", false),
- "undoable": false,
- "reason": "Input binding already exists",
- }
- }
-
- InputMap.action_add_event(action, event)
- var err := _save_action_events(action)
- if err != OK:
- InputMap.action_erase_event(action, event)
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Failed to save project settings while binding event to action '%s': %s (error %d)" % [action, error_string(err), err])
-
- return {
- "data": {
- "action": action,
- "event": _serialize_event(event),
- "already_bound": false,
- "action_created": ensured.get("created", false),
- "undoable": false,
- "reason": "Input bindings are saved to project.godot",
- }
- }
-
-
-func _validate_deadzone(deadzone: float) -> Dictionary:
- if deadzone < 0.0 or deadzone > 1.0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "deadzone must be in [0.0, 1.0] (got %s). Typical values are 0.2-0.5; default is 0.5." % deadzone)
- return {}
-
-
-func _ensure_action_state(action: String, deadzone: float) -> Dictionary:
- var key := "input/%s" % action
- var user_authored := _read_user_authored_actions()
- var existed_in_input_map := InputMap.has_action(action)
- var existed_in_project := user_authored.has(action) or ProjectSettings.has_setting(key)
- var old_setting = user_authored.get(action, null) if user_authored.has(action) else null
- if old_setting == null and ProjectSettings.has_setting(key):
- old_setting = ProjectSettings.get_setting(key)
-
- if not existed_in_input_map:
- var dz := deadzone
- if old_setting is Dictionary:
- dz = float(old_setting.get("deadzone", deadzone))
- InputMap.add_action(action, dz)
- if old_setting is Dictionary:
- for ev in old_setting.get("events", []):
- if ev is InputEvent:
- InputMap.action_add_event(action, ev)
-
- if not existed_in_project:
- var err := _save_action_events(action)
- if err != OK:
- if not existed_in_input_map:
- InputMap.erase_action(action)
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
- "Failed to save project settings while ensuring action '%s': %s (error %d)" % [action, error_string(err), err])
-
- var stored_deadzone := deadzone
- if ProjectSettings.has_setting(key):
- var stored = ProjectSettings.get_setting(key)
- if stored is Dictionary:
- stored_deadzone = float(stored.get("deadzone", deadzone))
- return {
- "action": action,
- "deadzone": stored_deadzone,
- "created": not existed_in_input_map and not existed_in_project,
- "already_exists": existed_in_input_map or existed_in_project,
- "loaded_in_input_map": true,
- "persisted": true,
- "undoable": false,
- "reason": "Input actions are saved to project.godot",
- }
-
-
-## Returns an InputEvent on success, or a Dictionary error on failure.
-## Caller must check ``result is Dictionary`` before treating it as an event.
-func _create_event(event_type: String, params: Dictionary):
- match event_type:
- "key":
- var ev := InputEventKey.new()
- var keycode_str: String = params.get("keycode", "")
- if keycode_str.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM,
- "event_type='key' requires keycode (e.g. 'Space', 'A', 'Enter', 'Escape', 'F1').")
- ev.keycode = OS.find_keycode_from_string(keycode_str)
- if ev.keycode == KEY_NONE:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid keycode '%s'. Use Godot keycode names like 'A', 'Space', 'Enter', 'Escape', 'F1', 'Left', 'Right'." % keycode_str)
- ev.ctrl_pressed = params.get("ctrl", false)
- ev.alt_pressed = params.get("alt", false)
- ev.shift_pressed = params.get("shift", false)
- ev.meta_pressed = params.get("meta", false)
- ev.device = -1
- return ev
- "mouse_button":
- if not params.has("button"):
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM,
- "event_type='mouse_button' requires button (1=left, 2=right, 3=middle, 4=wheel up, 5=wheel down).")
- var button: int = int(params.get("button", 0))
- if button <= 0:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "mouse_button button must be > 0 (got %d). Use 1=left, 2=right, 3=middle, 4=wheel up, 5=wheel down." % button)
- var ev := InputEventMouseButton.new()
- ev.button_index = button
- ev.device = -1
- return ev
- "joy_button":
- if not params.has("button"):
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM,
- "event_type='joy_button' requires button (JoyButton index, e.g. 0=A/Cross, 1=B/Circle).")
- var ev := InputEventJoypadButton.new()
- ev.button_index = int(params.get("button", 0))
- return ev
- "joy_axis":
- var axis_param = params.get("axis", null)
- if axis_param == null:
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM,
- "event_type='joy_axis' requires axis (JoyAxis index, e.g. 0=left stick X, 1=left stick Y).")
- var axis: int
- match typeof(axis_param):
- TYPE_INT:
- axis = axis_param
- TYPE_FLOAT:
- if axis_param != floor(axis_param):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "joy_axis axis must be an integer JoyAxis index (got %s)." % str(axis_param))
- axis = int(axis_param)
- TYPE_STRING:
- var axis_text := str(axis_param)
- if not axis_text.is_valid_int():
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
- "joy_axis axis must be an integer JoyAxis index (got '%s')." % axis_text)
- axis = int(axis_text)
- _:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
- "joy_axis axis must be an integer JoyAxis index (got %s)." % type_string(typeof(axis_param)))
- var ev := InputEventJoypadMotion.new()
- ev.axis = axis
- ev.axis_value = float(params.get("axis_value", 1.0))
- return ev
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "Unsupported event_type: '%s'. Use 'key', 'mouse_button', 'joy_button', or 'joy_axis'." % event_type)
-
-
-func _serialize_event(event: InputEvent) -> Dictionary:
- if event is InputEventKey:
- return {
- "type": "key",
- "keycode": OS.get_keycode_string(event.keycode),
- "physical_keycode": OS.get_keycode_string(event.physical_keycode),
- "ctrl": event.ctrl_pressed,
- "alt": event.alt_pressed,
- "shift": event.shift_pressed,
- "meta": event.meta_pressed,
- }
- if event is InputEventMouseButton:
- return {
- "type": "mouse_button",
- "button": event.button_index,
- }
- if event is InputEventJoypadButton:
- return {
- "type": "joy_button",
- "button": event.button_index,
- }
- if event is InputEventJoypadMotion:
- return {
- "type": "joy_axis",
- "axis": event.axis,
- "axis_value": event.axis_value,
- }
- return {"type": event.get_class(), "string": str(event)}
-
-
-func _events_match(a: InputEvent, b: InputEvent) -> bool:
- if a is InputEventKey and b is InputEventKey:
- return _key_events_match(a as InputEventKey, b as InputEventKey)
- return _serialize_event(a) == _serialize_event(b)
-
-
-func _key_events_match(a: InputEventKey, b: InputEventKey) -> bool:
- if a.ctrl_pressed != b.ctrl_pressed:
- return false
- if a.alt_pressed != b.alt_pressed:
- return false
- if a.shift_pressed != b.shift_pressed:
- return false
- if a.meta_pressed != b.meta_pressed:
- return false
- var a_codes := [a.keycode, a.physical_keycode]
- var b_codes := [b.keycode, b.physical_keycode]
- for a_code in a_codes:
- if int(a_code) == KEY_NONE:
- continue
- for b_code in b_codes:
- if int(b_code) != KEY_NONE and int(a_code) == int(b_code):
- return true
- return false
-
-
-func _save_action_events(action: String) -> int:
- var events: Array = []
- for event in InputMap.action_get_events(action):
- events.append(event)
- var key := "input/%s" % action
- var had_setting := ProjectSettings.has_setting(key)
- var old_setting = ProjectSettings.get_setting(key) if had_setting else null
- var deadzone: float = 0.5
- if old_setting is Dictionary:
- deadzone = old_setting.get("deadzone", 0.5)
- elif InputMap.has_action(action):
- deadzone = InputMap.action_get_deadzone(action)
- ProjectSettings.set_setting(key, {
- "deadzone": deadzone,
- "events": events,
- })
- var err := ProjectSettings.save()
- if err != OK:
- if had_setting:
- ProjectSettings.set_setting(key, old_setting)
- else:
- ProjectSettings.clear(key)
- return err
diff --git a/addons/godot_ai/handlers/input_handler.gd.uid b/addons/godot_ai/handlers/input_handler.gd.uid
deleted file mode 100644
index 7d509f4..0000000
--- a/addons/godot_ai/handlers/input_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://buk68rbwssqwp
diff --git a/addons/godot_ai/handlers/material_handler.gd b/addons/godot_ai/handlers/material_handler.gd
deleted file mode 100644
index bd617ef..0000000
--- a/addons/godot_ai/handlers/material_handler.gd
+++ /dev/null
@@ -1,809 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Handles Material authoring: creating .tres files, setting BaseMaterial3D
-## properties / shader uniforms, assigning to nodes, high-level presets.
-##
-## File-resource lifecycle mirrors ThemeHandler (create/load/mutate/save).
-## Undo pattern mirrors AnimationHandler (single create_action bundles
-## every dependency spawn).
-
-const MaterialValues := preload("res://addons/godot_ai/handlers/material_values.gd")
-const MaterialPresets := preload("res://addons/godot_ai/handlers/material_presets.gd")
-
-const _TYPE_TO_CLASS := {
- "standard": "StandardMaterial3D",
- "orm": "ORMMaterial3D",
- "canvas_item": "CanvasItemMaterial",
- "shader": "ShaderMaterial",
-}
-
-const _SUPPORTED_SUFFIXES := [".tres", ".material", ".res"]
-
-
-var _undo_redo: EditorUndoRedoManager
-var _connection: McpConnection
-
-
-func _init(undo_redo: EditorUndoRedoManager, connection: McpConnection = null) -> void:
- _undo_redo = undo_redo
- _connection = connection
-
-
-# ============================================================================
-# material_create
-# ============================================================================
-
-func create_material(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
- var type_str: String = params.get("type", "standard")
- var shader_path: String = params.get("shader_path", "")
- var overwrite: bool = params.get("overwrite", false)
-
- var err := _validate_material_path(path, "path", true)
- if err != null:
- return err
-
- if not _TYPE_TO_CLASS.has(type_str):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid material type '%s'. Valid: %s" % [type_str, ", ".join(_TYPE_TO_CLASS.keys())]
- )
-
- var existed_before := FileAccess.file_exists(path)
- if existed_before and not overwrite:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Material already exists at %s (pass overwrite=true to replace)" % path
- )
-
- var mat := _instantiate_material(type_str)
- if mat == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate material")
-
- if type_str == "shader":
- if shader_path.is_empty():
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "ShaderMaterial requires shader_path (res:// / uid:// / user:// path to a .gdshader)"
- )
- var shader_path_err = McpPathValidator.loadable_error(shader_path, "shader_path")
- if shader_path_err != null:
- return shader_path_err
- if not ResourceLoader.exists(shader_path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Shader not found: %s" % shader_path)
- var shader_res := ResourceLoader.load(shader_path)
- if not (shader_res is Shader):
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a Shader" % shader_path)
- (mat as ShaderMaterial).shader = shader_res
-
- var dir_path := path.get_base_dir()
- var mkdir_err := DirAccess.make_dir_recursive_absolute(dir_path)
- if mkdir_err != OK and mkdir_err != ERR_ALREADY_EXISTS:
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Failed to create directory: %s (error %d)" % [dir_path, mkdir_err]
- )
-
- var save_err := McpResourceIO.guarded_save(mat, path, _connection)
- if save_err != OK:
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Failed to save material to %s (error %d)" % [path, save_err]
- )
-
- var efs := EditorInterface.get_resource_filesystem()
- if efs != null:
- efs.update_file(path)
-
- return {
- "data": {
- "path": path,
- "type": type_str,
- "class": mat.get_class(),
- "shader_path": shader_path,
- "overwritten": existed_before,
- "undoable": false,
- "reason": "File creation is persistent; delete the file manually to revert",
- }
- }
-
-
-# ============================================================================
-# material_set_param
-# ============================================================================
-
-func set_param(params: Dictionary) -> Dictionary:
- var load_result := _load_material_from_path(params.get("path", ""), true)
- if load_result.has("error"):
- return load_result
- var mat: Material = load_result.material
- var mat_path: String = load_result.path
-
- var property: String = params.get("param", "")
- if property.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: param")
-
- if not ("value" in params):
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: value")
-
- var raw_value = params.get("value")
-
- # Probe the property. We allow any property present in get_property_list,
- # plus `shader` on ShaderMaterial.
- var prop_type: int = TYPE_NIL
- var property_exists := false
- for prop in mat.get_property_list():
- if prop.name == property:
- property_exists = true
- prop_type = prop.get("type", TYPE_NIL)
- break
- if not property_exists:
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- McpPropertyErrors.build_message(mat, property)
- )
-
- var coerced := MaterialValues.coerce_material_value(property, raw_value, prop_type)
- if not coerced.ok:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerced.error))
- var new_value = coerced.value
-
- var old_value = mat.get(property)
-
- _undo_redo.create_action("MCP: Set material %s.%s" % [mat_path.get_file(), property])
- _undo_redo.add_do_method(self, "_apply_param", mat_path, property, new_value, false)
- _undo_redo.add_undo_method(self, "_apply_param", mat_path, property, old_value, false)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": mat_path,
- "property": property,
- "value": MaterialValues.serialize_value(new_value),
- "previous_value": MaterialValues.serialize_value(old_value),
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# material_set_shader_param
-# ============================================================================
-
-func set_shader_param(params: Dictionary) -> Dictionary:
- var load_result := _load_material_from_path(params.get("path", ""), true)
- if load_result.has("error"):
- return load_result
- var mat: Material = load_result.material
- var mat_path: String = load_result.path
-
- if not (mat is ShaderMaterial):
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Material at %s is %s, not ShaderMaterial" % [mat_path, mat.get_class()]
- )
- var shader_mat := mat as ShaderMaterial
- if shader_mat.shader == null:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "ShaderMaterial at %s has no shader assigned" % mat_path
- )
-
- var param_name: String = params.get("param", "")
- if param_name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: param")
-
- if not ("value" in params):
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: value")
-
- # Verify the uniform exists in the shader.
- var uniform_type := _shader_uniform_type(shader_mat.shader, param_name)
- if uniform_type == TYPE_NIL:
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Shader uniform '%s' not declared on shader at %s" % [param_name, shader_mat.shader.resource_path]
- )
-
- var raw_value = params.get("value")
- var coerced := MaterialValues.coerce_material_value(param_name, raw_value, uniform_type)
- if not coerced.ok:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerced.error))
- var new_value = coerced.value
-
- var old_value = shader_mat.get_shader_parameter(param_name)
-
- _undo_redo.create_action("MCP: Set shader param %s.%s" % [mat_path.get_file(), param_name])
- _undo_redo.add_do_method(self, "_apply_shader_param", mat_path, param_name, new_value)
- _undo_redo.add_undo_method(self, "_apply_shader_param", mat_path, param_name, old_value)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": mat_path,
- "param": param_name,
- "value": MaterialValues.serialize_value(new_value),
- "previous_value": MaterialValues.serialize_value(old_value),
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# material_get
-# ============================================================================
-
-func get_material(params: Dictionary) -> Dictionary:
- var load_result := _load_material_from_path(params.get("path", ""))
- if load_result.has("error"):
- return load_result
- var mat: Material = load_result.material
- var mat_path: String = load_result.path
-
- var properties: Array[Dictionary] = []
- for prop in mat.get_property_list():
- var usage: int = prop.get("usage", 0)
- if not (usage & PROPERTY_USAGE_EDITOR):
- continue
- var name: String = prop.name
- if name.begins_with("shader_parameter/"):
- continue # handled below
- var value = mat.get(name)
- if value == null and prop.type != TYPE_NIL:
- continue
- properties.append({
- "name": name,
- "type": type_string(prop.type),
- "value": MaterialValues.serialize_value(value),
- })
-
- var shader_params: Array[Dictionary] = []
- if mat is ShaderMaterial:
- var shader_mat := mat as ShaderMaterial
- if shader_mat.shader != null:
- for u in shader_mat.shader.get_shader_uniform_list():
- var u_name: String = u.get("name", "")
- if u_name.is_empty():
- continue
- shader_params.append({
- "name": u_name,
- "type": type_string(u.get("type", TYPE_NIL)),
- "value": MaterialValues.serialize_value(shader_mat.get_shader_parameter(u_name)),
- })
-
- var reverse_type_map := _reverse_type_map()
-
- var shader_path_str := ""
- if mat is ShaderMaterial:
- var sm := mat as ShaderMaterial
- if sm.shader != null:
- shader_path_str = sm.shader.resource_path
-
- return {
- "data": {
- "path": mat_path,
- "class": mat.get_class(),
- "type": reverse_type_map.get(mat.get_class(), ""),
- "properties": properties,
- "property_count": properties.size(),
- "shader_parameters": shader_params,
- "shader_path": shader_path_str,
- }
- }
-
-
-# ============================================================================
-# material_list
-# ============================================================================
-
-func list_materials(params: Dictionary) -> Dictionary:
- var root: String = params.get("root", "res://")
- var type_filter: String = params.get("type", "")
-
- var root_err = McpPathValidator.path_error(root, "root")
- if root_err != null:
- return root_err
-
- var efs := EditorInterface.get_resource_filesystem()
- if efs == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_UNAVAILABLE,
- "EditorFileSystem not available", false)
-
- var results: Array[Dictionary] = []
- var start_dir := efs.get_filesystem_path(root)
- if start_dir == null:
- start_dir = efs.get_filesystem()
- _scan_materials(start_dir, type_filter, root, results)
-
- return {"data": {"materials": results, "count": results.size()}}
-
-
-func _scan_materials(dir: EditorFileSystemDirectory, type_filter: String, root: String, out: Array[Dictionary]) -> void:
- if dir == null:
- return
- for i in dir.get_file_count():
- var file_path := dir.get_file_path(i)
- if not file_path.begins_with(root):
- continue
- var file_type := dir.get_file_type(i)
- var is_material := file_type == "Material" or ClassDB.is_parent_class(file_type, "Material")
- if not is_material:
- # Some material variants serialize as specific classes.
- if not (file_type in _TYPE_TO_CLASS.values()):
- continue
-
- if not type_filter.is_empty():
- if file_type != type_filter and not ClassDB.is_parent_class(file_type, type_filter):
- continue
-
- out.append({"path": file_path, "class": file_type})
-
- for i in dir.get_subdir_count():
- _scan_materials(dir.get_subdir(i), type_filter, root, out)
-
-
-# ============================================================================
-# material_assign
-# ============================================================================
-
-func assign_material(params: Dictionary) -> Dictionary:
- var node_path: String = params.get("node_path", "")
- if node_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: node_path")
-
- var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var _scene_root: Node = _resolved.scene_root
-
- var slot: String = params.get("slot", "override")
- var resource_path: String = params.get("resource_path", "")
- var create_if_missing: bool = params.get("create_if_missing", false)
- var type_str: String = params.get("type", "standard")
-
- var slot_result := _resolve_slot_property(node, slot)
- if slot_result.has("error"):
- return slot_result
- var property: String = slot_result.property
-
- # Load or create the material.
- var mat: Material = null
- var material_created := false
- if not resource_path.is_empty():
- var rpath_err = McpPathValidator.loadable_error(resource_path, "resource_path")
- if rpath_err != null:
- return rpath_err
- if not ResourceLoader.exists(resource_path):
- if create_if_missing:
- # We'd need to create a new file here — refuse; callers should
- # use material_create first or omit resource_path to get an
- # inline material.
- return ErrorCodes.make(
- ErrorCodes.RESOURCE_NOT_FOUND,
- "Resource not found: %s. Create it first with material_create or omit resource_path for an inline material." % resource_path
- )
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s" % resource_path)
- var loaded := ResourceLoader.load(resource_path)
- if not (loaded is Material):
- var loaded_class := "null"
- if loaded != null:
- loaded_class = loaded.get_class()
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Resource at %s is not a Material (got %s)" % [resource_path, loaded_class]
- )
- mat = loaded
- else:
- if not create_if_missing:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Missing resource_path (pass create_if_missing=true to create a new inline material)"
- )
- if not _TYPE_TO_CLASS.has(type_str):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid material type '%s'" % type_str
- )
- mat = _instantiate_material(type_str)
- material_created = true
-
- var old_value = node.get(property)
-
- _undo_redo.create_action("MCP: Assign material to %s.%s" % [node.name, property])
- _undo_redo.add_do_property(node, property, mat)
- _undo_redo.add_undo_property(node, property, old_value)
- if material_created:
- _undo_redo.add_do_reference(mat)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "node_path": node_path,
- "property": property,
- "slot": slot,
- "resource_path": resource_path,
- "material_class": mat.get_class(),
- "material_created": material_created,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# material_apply_to_node
-# ============================================================================
-
-func apply_to_node(params: Dictionary) -> Dictionary:
- var node_path: String = params.get("node_path", "")
- if node_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: node_path")
-
- var type_str: String = params.get("type", "standard")
- if not _TYPE_TO_CLASS.has(type_str):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid material type '%s'. Valid: %s" % [type_str, ", ".join(_TYPE_TO_CLASS.keys())]
- )
-
- var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var _scene_root: Node = _resolved.scene_root
-
- var slot: String = params.get("slot", "override")
- var slot_result := _resolve_slot_property(node, slot)
- if slot_result.has("error"):
- return slot_result
- var property: String = slot_result.property
-
- var mat := _instantiate_material(type_str)
-
- var props_to_set: Dictionary = params.get("params", {})
- var applied: Array[String] = []
- for prop_name in props_to_set:
- var apply_err := _apply_one_param_on_instance(mat, String(prop_name), props_to_set[prop_name])
- if apply_err != null:
- return apply_err
- applied.append(String(prop_name))
-
- var save_to: String = params.get("save_to", "")
- var saved := false
- var overwritten := false
- if not save_to.is_empty():
- var save_err_validation := _validate_material_path(save_to, "save_to", true)
- if save_err_validation != null:
- return save_err_validation
- # Same clobber guard as create_material/apply_preset: agents reuse
- # names like res://materials/metal.tres, and a silent save here
- # destroys a hand-authored file that undo can't restore (undo only
- # reverts the node's slot assignment, not file contents). See #685.
- var existed_before := FileAccess.file_exists(save_to)
- if existed_before and not params.get("overwrite", false):
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Material already exists at %s (pass overwrite=true to replace)" % save_to
- )
- overwritten = existed_before
- var dir_path := save_to.get_base_dir()
- var mkdir_err := DirAccess.make_dir_recursive_absolute(dir_path)
- if mkdir_err != OK and mkdir_err != ERR_ALREADY_EXISTS:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to create directory: %s" % dir_path)
- var save_err := McpResourceIO.guarded_save(mat, save_to, _connection)
- if save_err != OK:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to save material to %s (error %d)" % [save_to, save_err])
- var efs := EditorInterface.get_resource_filesystem()
- if efs != null:
- efs.update_file(save_to)
- # Prefer the on-disk reference (keeps the scene ref small), but fall
- # back to the in-memory material if the reload fails — otherwise a null
- # would clear the slot and crash mat.get_class() below.
- var reloaded := ResourceLoader.load(save_to)
- if reloaded != null:
- mat = reloaded
- saved = true
-
- var old_value = node.get(property)
-
- _undo_redo.create_action("MCP: Apply %s material to %s" % [type_str, node.name])
- _undo_redo.add_do_property(node, property, mat)
- _undo_redo.add_undo_property(node, property, old_value)
- _undo_redo.add_do_reference(mat)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "node_path": node_path,
- "property": property,
- "slot": slot,
- "type": type_str,
- "class": mat.get_class(),
- "applied_params": applied,
- "material_created": true,
- "saved_to": save_to if saved else "",
- "overwritten": overwritten,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# material_apply_preset
-# ============================================================================
-
-func apply_preset(params: Dictionary) -> Dictionary:
- var preset_name: String = params.get("preset", "")
- if preset_name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: preset")
-
- var overrides: Dictionary = params.get("overrides", {})
- var blueprint = MaterialPresets.build(preset_name, overrides)
- if blueprint == null:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Unknown preset '%s'. Valid: %s" % [preset_name, ", ".join(MaterialPresets.list())]
- )
-
- var type_str: String = blueprint.get("type", "standard")
- var preset_params: Dictionary = blueprint.get("params", {})
-
- var path: String = params.get("path", "")
- var node_path: String = params.get("node_path", "")
-
- if path.is_empty() and node_path.is_empty():
- return ErrorCodes.make(
- ErrorCodes.MISSING_REQUIRED_PARAM,
- "Pass at least one of: path (save to disk), node_path (assign to node)"
- )
-
- # If both path and node_path, save to disk, then assign the saved resource.
- # If only path, save to disk.
- # If only node_path, inline material via apply_to_node.
-
- if not node_path.is_empty() and path.is_empty():
- # Inline
- var inline_result := apply_to_node({
- "node_path": node_path,
- "type": type_str,
- "params": preset_params,
- "slot": params.get("slot", "override"),
- })
- if inline_result.has("data"):
- inline_result.data["preset"] = preset_name
- inline_result.data["assigned"] = true
- inline_result.data["path"] = ""
- inline_result.data["saved_to_disk"] = false
- inline_result.data["reason"] = "Inline material assigned to node"
- return inline_result
-
- # Save-to-disk path. Validate the path BEFORE the exists/overwrite
- # check, matching create_material's order — an invalid path should
- # always be reported as invalid, not as an overwrite conflict.
- var path_err := _validate_material_path(path, "path", true)
- if path_err != null:
- return path_err
-
- var existed_before := FileAccess.file_exists(path)
- if existed_before and not params.get("overwrite", false):
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Material already exists at %s (pass overwrite=true to replace)" % path
- )
-
- var mat := _instantiate_material(type_str)
- for prop_name in preset_params:
- var apply_err := _apply_one_param_on_instance(mat, String(prop_name), preset_params[prop_name])
- if apply_err != null:
- return apply_err
-
- var dir_path := path.get_base_dir()
- var mkdir_err := DirAccess.make_dir_recursive_absolute(dir_path)
- if mkdir_err != OK and mkdir_err != ERR_ALREADY_EXISTS:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to create directory: %s" % dir_path)
-
- var save_err := McpResourceIO.guarded_save(mat, path, _connection)
- if save_err != OK:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to save material: %s" % path)
-
- var efs := EditorInterface.get_resource_filesystem()
- if efs != null:
- efs.update_file(path)
-
- var assigned := false
- if not node_path.is_empty():
- var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var _scene_root: Node = _resolved.scene_root
- var slot_result := _resolve_slot_property(node, params.get("slot", "override"))
- if slot_result.has("error"):
- return slot_result
- var property: String = slot_result.property
- var saved_mat := ResourceLoader.load(path)
- var old_value = node.get(property)
- _undo_redo.create_action("MCP: Apply preset %s to %s" % [preset_name, node.name])
- _undo_redo.add_do_property(node, property, saved_mat)
- _undo_redo.add_undo_property(node, property, old_value)
- _undo_redo.commit_action()
- assigned = true
-
- return {
- "data": {
- "preset": preset_name,
- "type": type_str,
- "path": path,
- "node_path": node_path,
- "material_created": true,
- "assigned": assigned,
- "saved_to_disk": true,
- "undoable": assigned, # assign is undoable; save is not
- "reason": "" if assigned else "File save is not undoable",
- }
- }
-
-
-# ============================================================================
-# Undo-callable: applies a param on the loaded resource and saves.
-# ============================================================================
-
-func _apply_param(mat_path: String, property: String, value: Variant, _is_shader: bool) -> void:
- var mat: Material = ResourceLoader.load(mat_path)
- if mat == null:
- push_warning("MCP: Failed to load material for undo/redo: %s" % mat_path)
- return
- mat.set(property, value)
- McpResourceIO.guarded_save(mat, mat_path, _connection)
-
-
-func _apply_shader_param(mat_path: String, param_name: String, value: Variant) -> void:
- var mat: Material = ResourceLoader.load(mat_path)
- if mat == null or not (mat is ShaderMaterial):
- push_warning("MCP: Failed to load shader material for undo/redo: %s" % mat_path)
- return
- (mat as ShaderMaterial).set_shader_parameter(param_name, value)
- McpResourceIO.guarded_save(mat, mat_path, _connection)
-
-
-# ============================================================================
-# Helpers
-# ============================================================================
-
-static func _instantiate_material(type_str: String) -> Material:
- match type_str:
- "standard":
- return StandardMaterial3D.new()
- "orm":
- return ORMMaterial3D.new()
- "canvas_item":
- return CanvasItemMaterial.new()
- "shader":
- return ShaderMaterial.new()
- return null
-
-
-static func _reverse_type_map() -> Dictionary:
- var out := {}
- for k in _TYPE_TO_CLASS:
- out[_TYPE_TO_CLASS[k]] = k
- return out
-
-
-static func _validate_material_path(path: String, param_name: String, for_write: bool = false) -> Variant:
- if path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: %s" % param_name)
- var path_err := McpPathValidator.validate_resource_path(path, for_write)
- if not path_err.is_empty():
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "%s: %s" % [param_name, path_err])
- var has_suffix := false
- for s in _SUPPORTED_SUFFIXES:
- if path.ends_with(s):
- has_suffix = true
- break
- if not has_suffix:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "%s must end with one of %s (got %s)" % [param_name, ", ".join(_SUPPORTED_SUFFIXES), path]
- )
- return null
-
-
-func _load_material_from_path(path: String, for_write: bool = false) -> Dictionary:
- var err := _validate_material_path(path, "path", for_write)
- if err != null:
- return err
- if not ResourceLoader.exists(path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Material not found: %s" % path)
- var res := ResourceLoader.load(path)
- if res == null or not (res is Material):
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a Material" % path)
- return {"material": res, "path": path}
-
-
-## Map a slot name to a Godot property name on the given node.
-## Returns {property: "..."} or an error dict.
-func _resolve_slot_property(node: Node, slot: String) -> Dictionary:
- if slot == "override":
- if node is MeshInstance3D or node is CSGShape3D:
- return {"property": "material_override"}
- if node is CanvasItem:
- return {"property": "material"}
- if node is GPUParticles3D or node is GPUParticles2D or node is CPUParticles3D or node is CPUParticles2D:
- return {"property": "material_override"} if node is GeometryInstance3D else {"property": "material"}
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Slot 'override' not supported on %s" % node.get_class()
- )
- if slot == "canvas":
- if node is CanvasItem:
- return {"property": "material"}
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Slot 'canvas' requires a CanvasItem (got %s)" % node.get_class()
- )
- if slot == "process":
- if node is GPUParticles3D or node is GPUParticles2D:
- return {"property": "process_material"}
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Slot 'process' requires a GPUParticles2D/3D (got %s)" % node.get_class()
- )
- if slot.begins_with("surface_"):
- if not (node is MeshInstance3D):
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Slot '%s' requires a MeshInstance3D (got %s)" % [slot, node.get_class()]
- )
- var idx_str := slot.substr(len("surface_"))
- if not idx_str.is_valid_int():
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid surface slot: %s" % slot)
- var idx := int(idx_str)
- var mi := node as MeshInstance3D
- var surf_count := mi.mesh.get_surface_count() if mi.mesh != null else 0
- if idx < 0 or idx >= surf_count:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Surface index %d out of range (mesh has %d surfaces)" % [idx, surf_count]
- )
- return {"property": "surface_material_override/%d" % idx}
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Unknown slot '%s'. Valid: override, canvas, process, surface_N" % slot
- )
-
-
-## Apply one property to an in-memory material instance; returns null on
-## success or an error dict on failure.
-func _apply_one_param_on_instance(mat: Material, property: String, raw_value: Variant) -> Variant:
- var prop_type: int = TYPE_NIL
- var property_exists := false
- for prop in mat.get_property_list():
- if prop.name == property:
- property_exists = true
- prop_type = prop.get("type", TYPE_NIL)
- break
- if not property_exists:
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- McpPropertyErrors.build_message(mat, property)
- )
- var coerced := MaterialValues.coerce_material_value(property, raw_value, prop_type)
- if not coerced.ok:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerced.error))
- mat.set(property, coerced.value)
- return null
-
-
-## Inspect a shader to get the Variant type of a uniform. Returns TYPE_NIL if
-## the uniform is not declared.
-static func _shader_uniform_type(shader: Shader, name: String) -> int:
- if shader == null:
- return TYPE_NIL
- for u in shader.get_shader_uniform_list():
- if u.get("name", "") == name:
- return int(u.get("type", TYPE_NIL))
- return TYPE_NIL
diff --git a/addons/godot_ai/handlers/material_handler.gd.uid b/addons/godot_ai/handlers/material_handler.gd.uid
deleted file mode 100644
index fdf789a..0000000
--- a/addons/godot_ai/handlers/material_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://blh4norn3rjga
diff --git a/addons/godot_ai/handlers/material_presets.gd b/addons/godot_ai/handlers/material_presets.gd
deleted file mode 100644
index db48036..0000000
--- a/addons/godot_ai/handlers/material_presets.gd
+++ /dev/null
@@ -1,92 +0,0 @@
-@tool
-extends RefCounted
-
-## Curated material preset blueprints.
-##
-## Each preset returns {type, params}. Handler applies them through the
-## normal material build path so they get undo + validation for free.
-
-
-const _PRESETS := {
- "metal": {
- "type": "orm",
- "params": {
- "metallic": 1.0,
- "roughness": 0.25,
- "albedo_color": {"r": 0.85, "g": 0.85, "b": 0.88, "a": 1.0},
- },
- },
- "glass": {
- "type": "standard",
- "params": {
- "transparency": "alpha",
- "albedo_color": {"r": 0.9, "g": 0.95, "b": 1.0, "a": 0.3},
- "metallic": 0.0,
- "metallic_specular": 0.5,
- "roughness": 0.05,
- "refraction_enabled": true,
- "refraction_scale": 0.05,
- },
- },
- "emissive": {
- "type": "standard",
- "params": {
- "emission_enabled": true,
- "emission_energy_multiplier": 3.0,
- "emission": {"r": 1.0, "g": 1.0, "b": 1.0, "a": 1.0},
- "albedo_color": {"r": 1.0, "g": 1.0, "b": 1.0, "a": 1.0},
- },
- },
- "unlit": {
- "type": "standard",
- "params": {
- "shading_mode": "unshaded",
- "albedo_color": {"r": 1.0, "g": 1.0, "b": 1.0, "a": 1.0},
- },
- },
- "matte": {
- "type": "standard",
- "params": {
- "roughness": 1.0,
- "metallic": 0.0,
- "albedo_color": {"r": 0.7, "g": 0.7, "b": 0.7, "a": 1.0},
- },
- },
- "ceramic": {
- "type": "standard",
- "params": {
- "roughness": 0.4,
- "metallic": 0.0,
- "clearcoat_enabled": true,
- "clearcoat": 0.7,
- "clearcoat_roughness": 0.15,
- "albedo_color": {"r": 0.95, "g": 0.95, "b": 0.95, "a": 1.0},
- },
- },
-}
-
-
-static func list() -> Array:
- return _PRESETS.keys()
-
-
-static func has(preset_name: String) -> bool:
- return _PRESETS.has(preset_name)
-
-
-## Returns a deep-copied {type, params} blueprint for the named preset, or
-## null if the preset is unknown. Overrides are merged into params.
-static func build(preset_name: String, overrides: Dictionary) -> Variant:
- if not _PRESETS.has(preset_name):
- return null
- var entry: Dictionary = _PRESETS[preset_name].duplicate(true)
- var params: Dictionary = entry.get("params", {})
- # Allow overrides to change type, too.
- if overrides.has("type"):
- entry["type"] = overrides["type"]
- for key in overrides:
- if key == "type":
- continue
- params[key] = overrides[key]
- entry["params"] = params
- return entry
diff --git a/addons/godot_ai/handlers/material_presets.gd.uid b/addons/godot_ai/handlers/material_presets.gd.uid
deleted file mode 100644
index 2f46051..0000000
--- a/addons/godot_ai/handlers/material_presets.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bnuwye1r8ow7g
diff --git a/addons/godot_ai/handlers/material_values.gd b/addons/godot_ai/handlers/material_values.gd
deleted file mode 100644
index 466c8bb..0000000
--- a/addons/godot_ai/handlers/material_values.gd
+++ /dev/null
@@ -1,194 +0,0 @@
-@tool
-extends RefCounted
-
-## Value coercion helpers for material authoring.
-##
-## Extends node_handler._coerce_value with material-specific cases:
-## - enum-by-name (transparency="alpha" → TRANSPARENCY_ALPHA)
-## - texture path → Texture2D
-## - {r,g,b,a} dict → Color (also handled by node coerce, but we want it inline)
-
-
-const _ENUM_TABLES := {
- "transparency": {
- "disabled": BaseMaterial3D.TRANSPARENCY_DISABLED,
- "alpha": BaseMaterial3D.TRANSPARENCY_ALPHA,
- "alpha_scissor": BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR,
- "alpha_hash": BaseMaterial3D.TRANSPARENCY_ALPHA_HASH,
- "alpha_depth_pre_pass": BaseMaterial3D.TRANSPARENCY_ALPHA_DEPTH_PRE_PASS,
- },
- "shading_mode": {
- "unshaded": BaseMaterial3D.SHADING_MODE_UNSHADED,
- "per_pixel": BaseMaterial3D.SHADING_MODE_PER_PIXEL,
- "per_vertex": BaseMaterial3D.SHADING_MODE_PER_VERTEX,
- },
- "blend_mode": {
- "mix": BaseMaterial3D.BLEND_MODE_MIX,
- "add": BaseMaterial3D.BLEND_MODE_ADD,
- "sub": BaseMaterial3D.BLEND_MODE_SUB,
- "mul": BaseMaterial3D.BLEND_MODE_MUL,
- },
- "cull_mode": {
- "back": BaseMaterial3D.CULL_BACK,
- "front": BaseMaterial3D.CULL_FRONT,
- "disabled": BaseMaterial3D.CULL_DISABLED,
- },
- "depth_draw_mode": {
- "opaque_only": BaseMaterial3D.DEPTH_DRAW_OPAQUE_ONLY,
- "always": BaseMaterial3D.DEPTH_DRAW_ALWAYS,
- "disabled": BaseMaterial3D.DEPTH_DRAW_DISABLED,
- },
- "diffuse_mode": {
- "burley": BaseMaterial3D.DIFFUSE_BURLEY,
- "lambert": BaseMaterial3D.DIFFUSE_LAMBERT,
- "lambert_wrap": BaseMaterial3D.DIFFUSE_LAMBERT_WRAP,
- "toon": BaseMaterial3D.DIFFUSE_TOON,
- },
- "specular_mode": {
- "schlick_ggx": BaseMaterial3D.SPECULAR_SCHLICK_GGX,
- "toon": BaseMaterial3D.SPECULAR_TOON,
- "disabled": BaseMaterial3D.SPECULAR_DISABLED,
- },
- "billboard_mode": {
- "disabled": BaseMaterial3D.BILLBOARD_DISABLED,
- "enabled": BaseMaterial3D.BILLBOARD_ENABLED,
- "fixed_y": BaseMaterial3D.BILLBOARD_FIXED_Y,
- "particles": BaseMaterial3D.BILLBOARD_PARTICLES,
- },
- "texture_filter": {
- "nearest": BaseMaterial3D.TEXTURE_FILTER_NEAREST,
- "linear": BaseMaterial3D.TEXTURE_FILTER_LINEAR,
- "nearest_mipmap": BaseMaterial3D.TEXTURE_FILTER_NEAREST_WITH_MIPMAPS,
- "linear_mipmap": BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS,
- },
-}
-
-
-## Return the enum int for (property, string_name), or null if not a known enum string.
-static func resolve_enum(property: String, value: Variant) -> Variant:
- if not (value is String):
- return null
- if not _ENUM_TABLES.has(property):
- return null
- var table: Dictionary = _ENUM_TABLES[property]
- var key: String = String(value).to_lower()
- if table.has(key):
- return table[key]
- return null
-
-
-## Parse a color from Color, "#rrggbb(aa)", named string, {r,g,b[,a]} dict,
-## or [r,g,b(,a)] array. Delegates to the canonical parser (#714); returns
-## null if the input cannot be parsed.
-static func parse_color(value: Variant) -> Variant:
- return McpJsonValues.parse_color(value)
-
-
-static func parse_vector3(value: Variant) -> Variant:
- return McpJsonValues.parse_vector3(value)
-
-
-static func parse_vector2(value: Variant) -> Variant:
- return McpJsonValues.parse_vector2(value)
-
-
-## Load a Texture2D from a res:// / uid:// / user:// path (validate_loadable_path).
-## Returns null on failure (including a path that fails confinement / traversal).
-static func load_texture(path: String) -> Texture2D:
- if not McpPathValidator.validate_loadable_path(path).is_empty():
- return null
- if not ResourceLoader.exists(path):
- return null
- var res := ResourceLoader.load(path)
- if res is Texture2D:
- return res
- return null
-
-
-## Coerce a JSON-shaped value for a material property.
-## Returns a dict {ok: true, value: ...} on success, or {ok: false, error: "..."} on failure.
-## For properties the coercer doesn't have special logic for, falls back to target_type.
-static func coerce_material_value(property: String, value: Variant, target_type: int) -> Dictionary:
- # Enum-by-name: must match before generic TYPE_INT coercion.
- if _ENUM_TABLES.has(property):
- if value is String:
- var enum_val = resolve_enum(property, value)
- if enum_val == null:
- return {
- "ok": false,
- "error": "Invalid %s value: '%s'. Valid: %s" % [
- property, value, ", ".join(_ENUM_TABLES[property].keys())
- ],
- }
- return {"ok": true, "value": int(enum_val)}
- if value is int or value is float:
- return {"ok": true, "value": int(value)}
-
- match target_type:
- TYPE_COLOR:
- var c = parse_color(value)
- if c == null:
- return {"ok": false, "error": "Invalid color for %s: %s" % [property, value]}
- return {"ok": true, "value": c}
- TYPE_VECTOR3:
- var v3 = parse_vector3(value)
- if v3 == null:
- return {"ok": false, "error": "Invalid vector3 for %s: %s" % [property, value]}
- return {"ok": true, "value": v3}
- TYPE_VECTOR2:
- var v2 = parse_vector2(value)
- if v2 == null:
- return {"ok": false, "error": "Invalid vector2 for %s: %s" % [property, value]}
- return {"ok": true, "value": v2}
- TYPE_BOOL:
- if value is bool:
- return {"ok": true, "value": value}
- if value is int or value is float:
- return {"ok": true, "value": bool(value)}
- return {"ok": false, "error": "Expected bool for %s" % property}
- TYPE_INT:
- if value is int:
- return {"ok": true, "value": value}
- if value is float:
- return {"ok": true, "value": int(value)}
- return {"ok": false, "error": "Expected int for %s" % property}
- TYPE_FLOAT:
- ## Canonical scalar parser (#964): numeric strings coerce too.
- var parsed: Variant = McpJsonValues.parse_float(value)
- if parsed != null:
- return {"ok": true, "value": parsed}
- return {"ok": false, "error": "Expected number for %s" % property}
- TYPE_OBJECT:
- if value == null:
- return {"ok": true, "value": null}
- if value is Object:
- return {"ok": true, "value": value}
- if value is String:
- var tex := load_texture(value)
- if tex == null:
- return {"ok": false, "error": "Resource not found or wrong type: %s" % value}
- return {"ok": true, "value": tex}
- return {"ok": false, "error": "Expected resource path (string) for %s" % property}
- TYPE_STRING:
- return {"ok": true, "value": String(value)}
-
- # Unknown target type — pass through.
- return {"ok": true, "value": value}
-
-
-## Serialize a Variant into JSON-friendly shape for responses.
-static func serialize_value(value: Variant) -> Variant:
- if value == null:
- return null
- if value is Color:
- return {"r": value.r, "g": value.g, "b": value.b, "a": value.a}
- if value is Vector3:
- return {"x": value.x, "y": value.y, "z": value.z}
- if value is Vector2:
- return {"x": value.x, "y": value.y}
- if value is Resource:
- var path := (value as Resource).resource_path
- if path.is_empty():
- return {"type": value.get_class(), "path": ""}
- return {"type": value.get_class(), "path": path}
- return value
diff --git a/addons/godot_ai/handlers/material_values.gd.uid b/addons/godot_ai/handlers/material_values.gd.uid
deleted file mode 100644
index 7f8bbe9..0000000
--- a/addons/godot_ai/handlers/material_values.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://daqgjkflia8nk
diff --git a/addons/godot_ai/handlers/node_handler.gd b/addons/godot_ai/handlers/node_handler.gd
deleted file mode 100644
index 9a969e6..0000000
--- a/addons/godot_ai/handlers/node_handler.gd
+++ /dev/null
@@ -1,1418 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-const VariantSerializer := preload("res://addons/godot_ai/utils/variant_serializer.gd")
-
-## Handles node creation and manipulation with undo/redo support.
-
-const ResourceHandler := preload("res://addons/godot_ai/handlers/resource_handler.gd")
-
-var _undo_redo: EditorUndoRedoManager
-
-
-func _init(undo_redo: EditorUndoRedoManager) -> void:
- _undo_redo = undo_redo
-
-
-func create_node(params: Dictionary) -> Dictionary:
- var node_type: String = params.get("type", "")
- var node_name: String = params.get("name", "")
- var parent_path: String = params.get("parent_path", "")
- var scene_path: String = params.get("scene_path", "")
-
- var scene_check := McpScenePath.require_edited_scene(params.get("scene_file", ""))
- if scene_check.has("error"):
- return scene_check
- var scene_root: Node = scene_check.node
-
- var parent: Node = scene_root
- if not parent_path.is_empty():
- parent = McpScenePath.resolve(parent_path, scene_root)
- if parent == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, McpScenePath.format_parent_error(parent_path, scene_root))
-
- var new_node: Node
-
- if not scene_path.is_empty():
- # Scene instancing path — load and instantiate a PackedScene.
- # GEN_EDIT_STATE_INSTANCE makes the editor treat the result as a real
- # scene instance (foldout icon, the .tscn stores a reference instead of
- # an exploded subtree). Descendants remain owned by their sub-scene;
- # setting their owner to our scene_root would break the instance link.
- var scene_path_err = McpPathValidator.loadable_error(scene_path, "scene_path")
- if scene_path_err != null:
- return scene_path_err
- if not ResourceLoader.exists(scene_path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Scene not found: %s" % scene_path)
- var packed_scene = ResourceLoader.load(scene_path)
- if packed_scene == null or not packed_scene is PackedScene:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a PackedScene" % scene_path)
- new_node = packed_scene.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)
- if new_node == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate scene: %s" % scene_path)
- else:
- # ClassDB path — create by type.
- if node_type.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: type (or provide scene_path)")
- if not ClassDB.class_exists(node_type):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Unknown node type: %s" % node_type)
- if not ClassDB.is_parent_class(node_type, "Node"):
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "%s is not a Node type" % node_type)
- new_node = ClassDB.instantiate(node_type)
- if new_node == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate %s" % node_type)
-
- if not node_name.is_empty():
- new_node.name = node_name
-
- _undo_redo.create_action("MCP: Create %s" % new_node.name)
- _undo_redo.add_do_method(parent, "add_child", new_node, true)
- _undo_redo.add_do_method(new_node, "set_owner", scene_root)
- _undo_redo.add_do_reference(new_node)
- _undo_redo.add_undo_method(parent, "remove_child", new_node)
- _undo_redo.commit_action()
-
- var response := {
- "name": new_node.name,
- "type": new_node.get_class(),
- "path": McpScenePath.from_node(new_node, scene_root),
- "parent_path": McpScenePath.from_node(parent, scene_root),
- "undoable": true,
- }
- if not scene_path.is_empty():
- response["scene_path"] = scene_path
- return {"data": response}
-
-
-func delete_node(params: Dictionary) -> Dictionary:
- var resolved := _resolve_node(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- var scene_root: Node = resolved.scene_root
-
- var root_err := _reject_if_scene_root(node, scene_root, "delete")
- if root_err != null:
- return root_err
-
- var parent := node.get_parent()
- var idx := node.get_index()
-
- _undo_redo.create_action("MCP: Delete %s" % node.name)
- _undo_redo.add_do_method(parent, "remove_child", node)
- _undo_redo.add_undo_method(parent, "add_child", node, true)
- _undo_redo.add_undo_method(parent, "move_child", node, idx)
- _undo_redo.add_undo_method(node, "set_owner", scene_root)
- _undo_redo.add_undo_reference(node)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "undoable": true,
- }
- }
-
-
-func reparent_node(params: Dictionary) -> Dictionary:
- var resolved := _resolve_node(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- var scene_root: Node = resolved.scene_root
-
- var new_parent_path: String = params.get("new_parent", "")
- if new_parent_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: new_parent")
-
- var new_parent := McpScenePath.resolve(new_parent_path, scene_root)
- if new_parent == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, McpScenePath.format_parent_error(new_parent_path, scene_root))
-
- var root_err := _reject_if_scene_root(node, scene_root, "reparent")
- if root_err != null:
- return root_err
-
- # Prevent reparenting a node to itself or to one of its own descendants.
- # Godot's `A.is_ancestor_of(B)` returns true iff B is a descendant of A, so
- # the direction here matters: we want `node.is_ancestor_of(new_parent)` to
- # catch "new_parent is below node in the tree" and thus would create a
- # cycle. The previous direction (`new_parent.is_ancestor_of(node)`) asked
- # the opposite question — whether we were trying to move a node to one of
- # its own ancestors — which is a perfectly valid operation. See issue #121.
- if node == new_parent or node.is_ancestor_of(new_parent):
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "Cannot reparent a node to itself or its descendant")
-
- var old_parent := node.get_parent()
- var old_idx := node.get_index()
- ## Ported from upstream PR #927 at
- ## 1a95bcca51d81d29de925c2f636814eaa037c1c2 (issue #904). Snapshot
- ## descendants before commit: remove_child clears owner on any child whose
- ## owner sits outside the pruned subtree, so both do and undo must restore
- ## those owners as part of the recorded action.
- var descendants := _collect_descendants(node)
-
- _undo_redo.create_action("MCP: Reparent %s" % node.name)
- _undo_redo.add_do_method(old_parent, "remove_child", node)
- _undo_redo.add_do_method(new_parent, "add_child", node, true)
- _undo_redo.add_do_method(node, "set_owner", scene_root)
- for child in descendants:
- _undo_redo.add_do_method(child, "set_owner", scene_root)
- _undo_redo.add_do_reference(node)
- _undo_redo.add_undo_method(new_parent, "remove_child", node)
- _undo_redo.add_undo_method(old_parent, "add_child", node, true)
- _undo_redo.add_undo_method(old_parent, "move_child", node, old_idx)
- _undo_redo.add_undo_method(node, "set_owner", scene_root)
- for child in descendants:
- ## Keep a null owner as null. Substituting scene_root would make an
- ## intentionally unowned descendant scene-owned on undo (#904).
- var prior_owner: Node = child.owner
- _undo_redo.add_undo_method(child, "set_owner", prior_owner)
- _undo_redo.add_undo_reference(node)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": McpScenePath.from_node(node, scene_root),
- "old_parent": McpScenePath.from_node(old_parent, scene_root),
- "new_parent": McpScenePath.from_node(new_parent, scene_root),
- "undoable": true,
- }
- }
-
-
-func set_property(params: Dictionary) -> Dictionary:
- var resolved := _resolve_node(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- var scene_root: Node = resolved.scene_root
-
- var property: String = params.get("property", "")
- if property.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: property")
-
- if not "value" in params:
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: value")
-
- var value = params.get("value")
-
- var found := false
- var prop_type: int = TYPE_NIL
- for prop in node.get_property_list():
- if prop.name == property:
- found = true
- prop_type = prop.get("type", TYPE_NIL)
- break
- if not found:
- return ErrorCodes.make(ErrorCodes.PROPERTY_NOT_ON_CLASS, McpPropertyErrors.build_message(node, property))
-
- var old_value = node.get(property)
- # Prefer declared property type; fall back to runtime type for dynamic props
- # (scripted @export vars can report TYPE_NIL in the property list).
- var target_type: int = prop_type if prop_type != TYPE_NIL else typeof(old_value)
-
- var instantiated_resource := false
-
- # Some MCP clients (Cline) stringify the documented {"__class__": "BoxMesh", ...}
- # value before sending. Promote that string back to a Dictionary here so the
- # `__class__` branch below handles it, instead of the next branch treating
- # the JSON blob as a res:// path and emitting "Resource not found: {...}".
- # See #206.
- if target_type == TYPE_OBJECT and value is String and value.begins_with("{"):
- var json := JSON.new()
- if json.parse(value) == OK and json.data is Dictionary and (json.data as Dictionary).has("__class__"):
- value = json.data
-
- var nil_resource_string: bool = target_type == TYPE_NIL and (value == "" or (value is String and value.begins_with("res://")))
- var resource_string_value: bool = value is String and (target_type == TYPE_OBJECT or nil_resource_string)
- if resource_string_value:
- if value == "":
- value = null
- else:
- var value_path_err = McpPathValidator.loadable_error(value, "value")
- if value_path_err != null:
- return value_path_err
- if not ResourceLoader.exists(value):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s" % value)
- var loaded := ResourceLoader.load(value)
- if loaded == null:
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s" % value)
- value = loaded
- elif target_type == TYPE_OBJECT and value is Dictionary and value.has("__class__"):
- # Shortcut: {"__class__": "BoxMesh", "size": {...}} instantiates a
- # fresh Resource subclass and applies the remaining keys as
- # properties. Mirrors resource_create's inline-assign path but
- # avoids a separate tool call for the common case.
- var type_str: String = value.get("__class__", "")
- var made := ResourceHandler._instantiate_resource(type_str)
- if made is Dictionary:
- return made
- var res: Resource = made
- var remaining: Dictionary = (value as Dictionary).duplicate()
- remaining.erase("__class__")
- if not remaining.is_empty():
- var apply_err := ResourceHandler._apply_resource_properties(res, remaining)
- if apply_err != null:
- return apply_err
- value = res
- instantiated_resource = true
- elif target_type == TYPE_ARRAY and old_value is Array and (old_value as Array).is_typed():
- ## Typed Array[T] slot (#612): the generic TYPE_ARRAY passthrough
- ## hands an untyped Array to Godot's typed setter, which rejects it
- ## wholesale and leaves the slot at its default — with success still
- ## reported. Route through the element-aware coercer instead; errors
- ## name the offending element index.
- var typed_out: Variant = _coerce_typed_array(value, old_value)
- if typed_out is Dictionary:
- return typed_out
- value = typed_out
- elif (
- target_type == TYPE_DICTIONARY
- and old_value is Dictionary
- and (old_value as Dictionary).is_typed()
- ):
- ## Typed Dictionary[K, V] slot (#612 stage 3) — same silent-drop
- ## family as typed arrays. A successful result is always a TYPED
- ## Dictionary (a cleared duplicate of the slot), while the error
- ## envelope is an untyped {"error": ...} — that's the discriminator
- ## (a legit payload could contain an "error" key; typedness can't lie).
- var typed_dict_out: Dictionary = _coerce_typed_dictionary(value, old_value)
- if not typed_dict_out.is_typed():
- return typed_dict_out
- value = typed_dict_out
- else:
- value = _coerce_value(value, target_type)
- ## Refuse any value that didn't land as the target compound Variant
- ## — wrong-shape dict (#123) or non-dict input like list / JSON string
- ## that used to silently default-construct Vector3.ZERO (#191).
- var coerce_err := _check_coerced(value, target_type)
- if coerce_err != null:
- return coerce_err
-
- _undo_redo.create_action("MCP: Set %s.%s" % [node.name, property])
- _undo_redo.add_do_property(node, property, value)
- _undo_redo.add_undo_property(node, property, old_value)
- if instantiated_resource:
- _undo_redo.add_do_reference(value)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "property": property,
- "value": _serialize_value(node.get(property)),
- "old_value": _serialize_value(old_value),
- "undoable": true,
- }
- }
-
-
-func rename_node(params: Dictionary) -> Dictionary:
- var resolved := _resolve_node(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- var scene_root: Node = resolved.scene_root
-
- var new_name: String = params.get("new_name", "")
- if new_name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: new_name")
-
- ## The scene root's name is baked into the .tscn serialization and is
- ## referenced by every NodePath that starts with `/` (AnimationPlayer
- ## tracks, RemoteTransform3D targets, exported NodePath @vars, etc.).
- ## Renaming it silently breaks those references. The MCP tool's docstring
- ## has always promised "Cannot rename the scene root" — enforce it. #122
- if node == scene_root:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "Cannot rename the scene root")
-
- if new_name.validate_node_name() != new_name:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid characters in name: %s" % new_name)
-
- var old_name := String(node.name)
- if old_name == new_name:
- return {
- "data": {
- "path": node_path,
- "name": new_name,
- "old_name": old_name,
- "unchanged": true,
- "undoable": false,
- "reason": "Name unchanged",
- }
- }
-
- var parent := node.get_parent()
- for sibling in parent.get_children():
- if sibling != node and String(sibling.name) == new_name:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "A sibling already has the name '%s'" % new_name)
-
- _undo_redo.create_action("MCP: Rename %s to %s" % [old_name, new_name])
- _undo_redo.add_do_property(node, "name", new_name)
- _undo_redo.add_undo_property(node, "name", old_name)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": McpScenePath.from_node(node, scene_root),
- "old_path": node_path,
- "name": String(node.name),
- "old_name": old_name,
- "undoable": true,
- }
- }
-
-
-func duplicate_node(params: Dictionary) -> Dictionary:
- var resolved := _resolve_node(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- var scene_root: Node = resolved.scene_root
-
- var root_err := _reject_if_scene_root(node, scene_root, "duplicate")
- if root_err != null:
- return root_err
-
- var parent := node.get_parent()
- var dup: Node = node.duplicate()
- if dup == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to duplicate node")
-
- # Apply optional name
- var new_name: String = params.get("name", "")
- if not new_name.is_empty():
- dup.name = new_name
-
- ## Ported from upstream PR #927 (issue #904). Record descendant owners
- ## inside the action so redo restores them. Undo is just remove_child of
- ## the copy; descendants live on `dup` via add_do_reference and do not need
- ## their own undo set_owner.
- var descendants := _collect_descendants(dup)
-
- _undo_redo.create_action("MCP: Duplicate %s" % node.name)
- _undo_redo.add_do_method(parent, "add_child", dup, true)
- _undo_redo.add_do_method(dup, "set_owner", scene_root)
- for child in descendants:
- _undo_redo.add_do_method(child, "set_owner", scene_root)
- _undo_redo.add_do_reference(dup)
- _undo_redo.add_undo_method(parent, "remove_child", dup)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": McpScenePath.from_node(dup, scene_root),
- "original_path": node_path,
- "name": dup.name,
- "type": dup.get_class(),
- "undoable": true,
- }
- }
-
-
-func move_node(params: Dictionary) -> Dictionary:
- var resolved := _resolve_node(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- var scene_root: Node = resolved.scene_root
-
- var root_err := _reject_if_scene_root(node, scene_root, "reorder")
- if root_err != null:
- return root_err
-
- if not "index" in params:
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: index")
-
- var new_index: int = params.get("index", 0)
- var parent := node.get_parent()
- var old_index := node.get_index()
- var sibling_count := parent.get_child_count()
-
- if new_index < 0 or new_index >= sibling_count:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Index %d out of range (0..%d)" % [new_index, sibling_count - 1])
-
- _undo_redo.create_action("MCP: Move %s to index %d" % [node.name, new_index])
- _undo_redo.add_do_method(parent, "move_child", node, new_index)
- _undo_redo.add_undo_method(parent, "move_child", node, old_index)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "old_index": old_index,
- "new_index": new_index,
- "undoable": true,
- }
- }
-
-
-func add_to_group(params: Dictionary) -> Dictionary:
- var resolved := _resolve_node(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
-
- var group_value: Variant = params.get("group", "")
- var type_err := McpParamValidators.require_string("group", group_value)
- if type_err != null:
- return type_err
- var group := String(group_value)
- if group.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: group")
-
- if node.is_in_group(group):
- return {"data": {"path": node_path, "group": group, "already_member": true, "undoable": false, "reason": "No change made"}}
-
- _undo_redo.create_action("MCP: Add %s to group %s" % [node.name, group])
- _undo_redo.add_do_method(node, "add_to_group", group, true)
- _undo_redo.add_undo_method(node, "remove_from_group", group)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "group": group,
- "undoable": true,
- }
- }
-
-
-func remove_from_group(params: Dictionary) -> Dictionary:
- var resolved := _resolve_node(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
-
- var group_value: Variant = params.get("group", "")
- var type_err := McpParamValidators.require_string("group", group_value)
- if type_err != null:
- return type_err
- var group := String(group_value)
- if group.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: group")
-
- if not node.is_in_group(group):
- return {"data": {"path": node_path, "group": group, "not_member": true, "undoable": false, "reason": "Node not in group"}}
-
- _undo_redo.create_action("MCP: Remove %s from group %s" % [node.name, group])
- _undo_redo.add_do_method(node, "remove_from_group", group)
- _undo_redo.add_undo_method(node, "add_to_group", group, true)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "group": group,
- "undoable": true,
- }
- }
-
-
-func set_selection(params: Dictionary) -> Dictionary:
- var paths: Array = params.get("paths", [])
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- var selection := EditorInterface.get_selection()
- selection.clear()
-
- var selected: Array[String] = []
- var not_found: Array[String] = []
- for path_variant in paths:
- var path: String = str(path_variant)
- var node := McpScenePath.resolve(path, scene_root)
- if node:
- selection.add_node(node)
- selected.append(path)
- else:
- not_found.append(path)
-
- return {
- "data": {
- "selected": selected,
- "not_found": not_found,
- "count": selected.size(),
- "undoable": false,
- "reason": "Selection changes are not tracked in undo history",
- }
- }
-
-
-## All descendants of `node` (not including `node` itself), depth-first.
-## Used to record per-child set_owner inside an undo action without targeting
-## the handler as an UndoRedo receiver (upstream PR #927 / issue #904).
-static func _collect_descendants(node: Node) -> Array[Node]:
- var out: Array[Node] = []
- for child in node.get_children():
- out.append(child)
- out.append_array(_collect_descendants(child))
- return out
-
-
-## Canonical dict-key sets for dict→Variant coercion. Alpha on `COLOR_KEYS`
-## is optional — the coercer defaults it to 1.0 when absent.
-const VECTOR2_KEYS: Array[String] = ["x", "y"]
-const VECTOR3_KEYS: Array[String] = ["x", "y", "z"]
-const VECTOR4_KEYS: Array[String] = ["x", "y", "z", "w"]
-const COLOR_KEYS: Array[String] = ["r", "g", "b"]
-
-
-## End-to-end coerce check for compound JSON-shaped targets
-## (Vector2/Vector3/Color). Returns a full `make(...)`-shaped error dict
-## if `value` didn't land as the target Variant after `_coerce_value`,
-## else null. Wrong-shape dicts get the `_check_dict_coerce_failed`
-## message (expected-vs-got keys); non-dict inputs (Array, String,
-## primitive) name the received type and a JSON shape hint. No-op for
-## non-compound targets — Godot's setter handles those.
-##
-## Used by set_property, resource_handler, and validation handlers
-## (curve, texture). Issue #191 — passing a list, JSON string, or
-## anything else to a Vector3 property used to silently store
-## Vector3.ZERO; this gates that path.
-static func _check_coerced(value: Variant, target_type: int, prefix: String = "") -> Variant:
- var ok := false
- match target_type:
- TYPE_VECTOR2:
- ok = value is Vector2
- TYPE_VECTOR3:
- ok = value is Vector3
- TYPE_COLOR:
- ok = value is Color
- TYPE_PACKED_VECTOR2_ARRAY:
- ok = value is PackedVector2Array
- TYPE_PACKED_VECTOR3_ARRAY:
- ok = value is PackedVector3Array
- TYPE_PACKED_VECTOR4_ARRAY:
- ok = value is PackedVector4Array
- TYPE_PACKED_COLOR_ARRAY:
- ok = value is PackedColorArray
- TYPE_PACKED_INT32_ARRAY:
- ok = value is PackedInt32Array
- TYPE_PACKED_INT64_ARRAY:
- ok = value is PackedInt64Array
- TYPE_PACKED_FLOAT32_ARRAY:
- ok = value is PackedFloat32Array
- TYPE_PACKED_FLOAT64_ARRAY:
- ok = value is PackedFloat64Array
- TYPE_PACKED_STRING_ARRAY:
- ok = value is PackedStringArray
- TYPE_VECTOR2I: ok = value is Vector2i
- TYPE_VECTOR3I: ok = value is Vector3i
- TYPE_VECTOR4: ok = value is Vector4
- TYPE_VECTOR4I: ok = value is Vector4i
- TYPE_QUATERNION: ok = value is Quaternion
- TYPE_RECT2: ok = value is Rect2
- TYPE_RECT2I: ok = value is Rect2i
- TYPE_AABB: ok = value is AABB
- TYPE_PLANE: ok = value is Plane
- TYPE_BASIS: ok = value is Basis
- TYPE_TRANSFORM2D: ok = value is Transform2D
- TYPE_TRANSFORM3D: ok = value is Transform3D
- TYPE_PROJECTION: ok = value is Projection
- _:
- # null / untyped-TYPE_NIL / already-correct-type are handled by
- # Godot's setter; anything else would silently no-op, so error.
- if value == null or target_type == TYPE_NIL or typeof(value) == target_type:
- return null
- var unsupported := ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Cannot write %s to a %s property; godot-ai has no coercion for that type" % [
- type_string(typeof(value)), type_string(target_type),
- ],
- )
- return ErrorCodes.prefix_message(unsupported, prefix)
- if ok:
- return null
- var dict_err := _check_dict_coerce_failed(value, target_type)
- if dict_err != null:
- return ErrorCodes.prefix_message(dict_err, prefix)
- ## Wording stays neutral on shape — `_shape_hint` already produces a
- ## dict-shaped string for Vector2/3/Color and a list-shaped one for
- ## the Packed*Array slots. The old "expected a dict like [...]" phrasing
- ## read self-contradictory for packed targets (PR #424 review).
- var err := ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Cannot coerce %s to %s; expected %s" % [
- type_string(typeof(value)), type_string(target_type), _shape_hint(target_type),
- ],
- )
- return ErrorCodes.prefix_message(err, prefix)
-
-
-## Build a "{\"x\":1,...}" hint string from the canonical key constants
-## so adding a key (e.g. Vector4) only touches VECTORN_KEYS. Packed*Array
-## targets short-circuit to a literal list-shaped hint.
-static func _shape_hint(target_type: int) -> String:
- match target_type:
- TYPE_PACKED_VECTOR2_ARRAY:
- return "[{\"x\":0,\"y\":0}, ...]"
- TYPE_PACKED_VECTOR3_ARRAY:
- return "[{\"x\":0,\"y\":0,\"z\":0}, ...]"
- TYPE_PACKED_VECTOR4_ARRAY:
- return "[{\"x\":0,\"y\":0,\"z\":0,\"w\":0}, ...]"
- TYPE_PACKED_COLOR_ARRAY:
- return "[{\"r\":0,\"g\":0,\"b\":0,\"a\":1}, ...]"
- TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY:
- return "[int, ...]"
- TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY:
- return "[float, ...]"
- TYPE_PACKED_STRING_ARRAY:
- return "[\"...\", ...]"
- TYPE_VECTOR2I:
- return "{\"x\":0,\"y\":0}"
- TYPE_VECTOR3I:
- return "{\"x\":0,\"y\":0,\"z\":0}"
- TYPE_VECTOR4, TYPE_VECTOR4I, TYPE_QUATERNION:
- return "{\"x\":0,\"y\":0,\"z\":0,\"w\":0}"
- TYPE_RECT2, TYPE_RECT2I, TYPE_AABB:
- return "{\"position\":{...},\"size\":{...}}"
- TYPE_PLANE:
- return "{\"normal\":{...},\"d\":0}"
- TYPE_BASIS:
- return "{\"x\":{...},\"y\":{...},\"z\":{...}}"
- TYPE_TRANSFORM2D:
- return "{\"x\":{...},\"y\":{...},\"origin\":{...}}"
- TYPE_TRANSFORM3D:
- return "{\"basis\":{...},\"origin\":{...}}"
- TYPE_PROJECTION:
- return "{\"x\":{...},\"y\":{...},\"z\":{...},\"w\":{...}}"
- var keys: Array[String] = []
- match target_type:
- TYPE_VECTOR2: keys = VECTOR2_KEYS
- TYPE_VECTOR3: keys = VECTOR3_KEYS
- TYPE_COLOR: keys = COLOR_KEYS
- var pairs: Array[String] = []
- for k in keys:
- pairs.append("\"%s\":0" % k)
- return "{" + ",".join(pairs) + "}"
-
-
-## Detect a failed dict→typed-Variant coercion. Returns an INVALID_PARAMS
-## error dict if `value` is still a Dictionary after a coercion attempt
-## targeting a Vector2/Vector3/Color slot, else null. Message names the
-## expected keys and the keys actually received so agents self-correct
-## on the next retry.
-static func _check_dict_coerce_failed(value: Variant, target_type: int) -> Variant:
- if not (value is Dictionary):
- return null
- var expected: Array[String] = []
- var type_name := ""
- match target_type:
- TYPE_VECTOR2:
- expected = VECTOR2_KEYS
- type_name = "Vector2"
- TYPE_VECTOR3:
- expected = VECTOR3_KEYS
- type_name = "Vector3"
- TYPE_COLOR:
- expected = COLOR_KEYS
- type_name = "Color"
- _:
- return null
- var got_keys: Array = (value as Dictionary).keys()
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Cannot coerce dict to %s: expected keys %s; got %s" % [type_name, str(expected), str(got_keys)]
- )
-
-
-## Coerce JSON-shaped values into Godot Variants when the target property
-## type is known. Returns the coerced value on success, or the input
-## unchanged on failure — callers detect the type mismatch via an
-## `is ` check (curve_handler, texture_handler) or via the
-## `_check_dict_coerce_failed` helper (set_property, resource_handler).
-##
-## Dictionary→Vector2/Vector3/Color cases REQUIRE all canonical keys;
-## wrong-shape dicts flow through unchanged. See issue #123 — previous
-## `dict.get(key, 0)` defaults silently zero-filled missing axes.
-static func _coerce_value(value: Variant, target_type: int) -> Variant:
- match target_type:
- ## Vector2/Vector3/Color route through the canonical strict parser
- ## (#714): same dict/array/string shapes as every other handler, and
- ## non-numeric components fall through (returning the original
- ## value) so _check_coerced flags them instead of crashing a typed
- ## constructor or silently writing black/zeros.
- TYPE_VECTOR2:
- var v2 = McpJsonValues.parse_vector2(value)
- if v2 != null:
- return v2
- TYPE_VECTOR3:
- var v3 = McpJsonValues.parse_vector3(value)
- if v3 != null:
- return v3
- TYPE_COLOR:
- var col = McpJsonValues.parse_color(value)
- if col != null:
- return col
- TYPE_BOOL:
- if value is float or value is int:
- return bool(value)
- TYPE_INT:
- if value is float:
- return int(value)
- TYPE_FLOAT:
- if value is int:
- return float(value)
- if value is String:
- ## #964: some MCP clients stringify float arguments ("4.0").
- ## Accept strictly-numeric strings; unparseable ones flow
- ## through unchanged so _check_coerced raises the typed
- ## WRONG_TYPE error instead of a silent zero/null write.
- var parsed: Variant = McpJsonValues.parse_float(value)
- if parsed != null:
- return parsed
- TYPE_STRING_NAME:
- if value is String:
- return StringName(value)
- TYPE_NODE_PATH:
- if value is String:
- return NodePath(value)
- if value == null:
- return NodePath()
- TYPE_OBJECT:
- # Resource loading is handled in set_property so we can return a
- # typed error; here we only pass through cleared values.
- if value == null:
- return null
- TYPE_ARRAY:
- if value is Array:
- return value
- TYPE_DICTIONARY:
- if value is Dictionary:
- return value
- TYPE_PACKED_VECTOR2_ARRAY:
- if value is Array:
- var out := PackedVector2Array()
- for item in value:
- if item is Vector2:
- out.append(item)
- elif item is Dictionary and item.has_all(VECTOR2_KEYS):
- out.append(Vector2(item["x"], item["y"]))
- else:
- return value # leave for _check_coerced to flag
- return out
- TYPE_PACKED_VECTOR3_ARRAY:
- if value is Array:
- var out := PackedVector3Array()
- for item in value:
- if item is Vector3:
- out.append(item)
- elif item is Dictionary and item.has_all(VECTOR3_KEYS):
- out.append(Vector3(item["x"], item["y"], item["z"]))
- else:
- return value
- return out
- TYPE_PACKED_VECTOR4_ARRAY:
- if value is Array:
- var out := PackedVector4Array()
- for item in value:
- if item is Vector4:
- out.append(item)
- elif item is Dictionary and item.has_all(VECTOR4_KEYS):
- out.append(Vector4(item["x"], item["y"], item["z"], item["w"]))
- else:
- return value
- return out
- TYPE_PACKED_COLOR_ARRAY:
- if value is Array:
- var out := PackedColorArray()
- for item in value:
- if item is Color:
- out.append(item)
- elif item is Dictionary and item.has_all(COLOR_KEYS):
- out.append(Color(item["r"], item["g"], item["b"], item.get("a", 1.0)))
- elif item is String:
- out.append(Color(item))
- else:
- return value
- return out
- TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY:
- if value is Array:
- var out: Variant = PackedInt32Array() if target_type == TYPE_PACKED_INT32_ARRAY else PackedInt64Array()
- for item in value:
- if item is int or item is float:
- out.append(int(item))
- else:
- return value
- return out
- TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY:
- if value is Array:
- var out: Variant = PackedFloat32Array() if target_type == TYPE_PACKED_FLOAT32_ARRAY else PackedFloat64Array()
- for item in value:
- if item is float or item is int:
- out.append(float(item))
- else:
- return value
- return out
- TYPE_PACKED_STRING_ARRAY:
- if value is Array:
- var out := PackedStringArray()
- for item in value:
- if item is String:
- out.append(item)
- else:
- return value
- return out
- TYPE_VECTOR2I:
- if value is Dictionary and value.has_all(VECTOR2_KEYS):
- return Vector2i(int(value["x"]), int(value["y"]))
- TYPE_VECTOR3I:
- if value is Dictionary and value.has_all(VECTOR3_KEYS):
- return Vector3i(int(value["x"]), int(value["y"]), int(value["z"]))
- TYPE_VECTOR4:
- if value is Dictionary and value.has_all(VECTOR4_KEYS):
- return Vector4(value["x"], value["y"], value["z"], value["w"])
- TYPE_VECTOR4I:
- if value is Dictionary and value.has_all(VECTOR4_KEYS):
- return Vector4i(int(value["x"]), int(value["y"]), int(value["z"]), int(value["w"]))
- TYPE_QUATERNION:
- if value is Dictionary and value.has_all(VECTOR4_KEYS):
- return Quaternion(value["x"], value["y"], value["z"], value["w"])
- TYPE_RECT2:
- if value is Dictionary and value.has("position") and value.has("size"):
- var p: Variant = _coerce_value(value["position"], TYPE_VECTOR2)
- var s: Variant = _coerce_value(value["size"], TYPE_VECTOR2)
- if p is Vector2 and s is Vector2:
- return Rect2(p, s)
- TYPE_RECT2I:
- if value is Dictionary and value.has("position") and value.has("size"):
- var p: Variant = _coerce_value(value["position"], TYPE_VECTOR2I)
- var s: Variant = _coerce_value(value["size"], TYPE_VECTOR2I)
- if p is Vector2i and s is Vector2i:
- return Rect2i(p, s)
- TYPE_AABB:
- if value is Dictionary and value.has("position") and value.has("size"):
- var p: Variant = _coerce_value(value["position"], TYPE_VECTOR3)
- var s: Variant = _coerce_value(value["size"], TYPE_VECTOR3)
- if p is Vector3 and s is Vector3:
- return AABB(p, s)
- TYPE_PLANE:
- if value is Dictionary and value.has("normal") and value.has("d"):
- var n: Variant = _coerce_value(value["normal"], TYPE_VECTOR3)
- if n is Vector3:
- return Plane(n, float(value["d"]))
- TYPE_BASIS:
- if value is Dictionary and value.has_all(["x", "y", "z"]):
- var bx: Variant = _coerce_value(value["x"], TYPE_VECTOR3)
- var by: Variant = _coerce_value(value["y"], TYPE_VECTOR3)
- var bz: Variant = _coerce_value(value["z"], TYPE_VECTOR3)
- if bx is Vector3 and by is Vector3 and bz is Vector3:
- return Basis(bx, by, bz)
- TYPE_TRANSFORM2D:
- if value is Dictionary and value.has_all(["x", "y", "origin"]):
- var tx: Variant = _coerce_value(value["x"], TYPE_VECTOR2)
- var ty: Variant = _coerce_value(value["y"], TYPE_VECTOR2)
- var to_: Variant = _coerce_value(value["origin"], TYPE_VECTOR2)
- if tx is Vector2 and ty is Vector2 and to_ is Vector2:
- return Transform2D(tx, ty, to_)
- TYPE_TRANSFORM3D:
- if value is Dictionary and value.has("basis") and value.has("origin"):
- var b: Variant = _coerce_value(value["basis"], TYPE_BASIS)
- var o: Variant = _coerce_value(value["origin"], TYPE_VECTOR3)
- if b is Basis and o is Vector3:
- return Transform3D(b, o)
- TYPE_PROJECTION:
- if value is Dictionary and value.has_all(VECTOR4_KEYS):
- var px: Variant = _coerce_value(value["x"], TYPE_VECTOR4)
- var py: Variant = _coerce_value(value["y"], TYPE_VECTOR4)
- var pz: Variant = _coerce_value(value["z"], TYPE_VECTOR4)
- var pw: Variant = _coerce_value(value["w"], TYPE_VECTOR4)
- if px is Vector4 and py is Vector4 and pz is Vector4 and pw is Vector4:
- return Projection(px, py, pz, pw)
- # PackedByteArray intentionally unhandled — needs design decision
- # (base64 string vs. raw int list); JSON has no native byte type.
- return value
-
-
-## Fill a typed `Array[T]` slot from a JSON list (#612 stage 1: value-element
-## types). `slot_value` is the property's current typed Array — Godot's getter
-## returns the (possibly empty) typed container, which carries the element
-## type, so no PROPERTY_HINT_TYPE_STRING parsing is needed. Elements coerce
-## one at a time through the existing `_coerce_value` / `_check_coerced`
-## pair, then bulk-move via `Array.assign()` with a post-assign size check,
-## so a wrong element can never silently drop the write: it errors naming
-## the element index. Returns the filled typed Array on success, or a
-## `make(...)`-shaped error Dictionary (callers discriminate on
-## `result is Dictionary` — a successful result is always an Array).
-##
-## Object elements (Array[Texture2D], Array[MyResource], ...) coerce per
-## element through `_coerce_object_element` (#612 stage 2), mirroring the
-## single-slot TYPE_OBJECT paths: res:// strings load, {"__class__": ...}
-## instantiates, and each landed element is conformance-checked against the
-## slot's element class/script so a wrong-class Resource errors naming the
-## index instead of being rejected wholesale by `assign`.
-static func _coerce_typed_array(value: Variant, slot_value: Array, prefix: String = "") -> Variant:
- var elem_label := _typed_array_element_label(slot_value)
- if not (value is Array):
- var err := ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Cannot write %s to a typed Array[%s] property; expected a list" % [
- type_string(typeof(value)), elem_label,
- ],
- )
- return ErrorCodes.prefix_message(err, prefix)
- var elem_type := slot_value.get_typed_builtin()
- var staging: Array = []
- var in_list: Array = value
- for i in in_list.size():
- var elem_prefix := ("element %d" % i) if prefix.is_empty() else "%s element %d" % [prefix, i]
- var coerced: Variant
- if elem_type == TYPE_OBJECT:
- coerced = _coerce_object_element(in_list[i], elem_prefix)
- if coerced is Dictionary:
- ## Object elements are never legit Dictionaries (a dict input
- ## is either {"__class__"} — consumed above — or an error), so
- ## a Dictionary return is unambiguously the error envelope.
- return coerced
- if coerced != null and not _object_element_conforms(coerced, slot_value):
- var conform_err := ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "element is %s, which is not a %s" % [
- (coerced as Object).get_class(), elem_label,
- ],
- )
- return ErrorCodes.prefix_message(conform_err, elem_prefix)
- else:
- coerced = _coerce_value(in_list[i], elem_type)
- var elem_err := _check_coerced(coerced, elem_type, elem_prefix)
- if elem_err != null:
- return elem_err
- if coerced == null:
- ## Prefix with `elem_prefix` (which already folds in `prefix`),
- ## not `prefix` again — the latter double-stamped the property
- ## context (PR #682 review). Object arrays allow null entries
- ## (Godot typed object arrays store null); value-type arrays
- ## don't.
- var null_err := ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "cannot store null in Array[%s]" % elem_label,
- )
- return ErrorCodes.prefix_message(null_err, elem_prefix)
- staging.append(coerced)
- var out := slot_value.duplicate()
- out.clear()
- out.assign(staging)
- if out.size() != staging.size():
- ## Backstop for element shapes `_check_coerced` waves through but the
- ## typed container still rejects — `assign` loud-rejects and leaves a
- ## short array, which without this check would be a partial write.
- var assign_err := ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Array[%s] element conversion failed during assign (%d of %d elements landed)" % [
- elem_label, out.size(), staging.size(),
- ],
- )
- return ErrorCodes.prefix_message(assign_err, prefix)
- return out
-
-
-## "int" / "Vector3" / "Texture2D" / "MyItemData" — element-type name of a
-## typed Array slot, for error messages.
-static func _typed_array_element_label(slot_value: Array) -> String:
- if slot_value.get_typed_builtin() == TYPE_OBJECT:
- return _object_type_label(slot_value.get_typed_class_name(), slot_value.get_typed_script())
- return type_string(slot_value.get_typed_builtin())
-
-
-## Coerce one element of an object-typed Array (#612 stage 2). Mirrors the
-## single-slot TYPE_OBJECT paths in set_property: a res:// path string loads
-## the Resource; {"__class__": "X", ...} (including the #206 stringified
-## form) instantiates via ResourceHandler and applies the remaining keys;
-## "" / null store a null entry (typed object arrays allow them). Returns
-## the Object (or null), or a make(...)-shaped error Dictionary with
-## `elem_prefix` already folded in.
-static func _coerce_object_element(elem: Variant, elem_prefix: String) -> Variant:
- if elem == null:
- return null
- if elem is Object:
- return elem
- if elem is String and (elem as String).begins_with("{"):
- var json := JSON.new()
- if json.parse(elem) == OK and json.data is Dictionary and (json.data as Dictionary).has("__class__"):
- elem = json.data
- if elem is String:
- if String(elem).is_empty():
- return null
- var path_err = McpPathValidator.loadable_error(elem, "value")
- if path_err != null:
- return ErrorCodes.prefix_message(path_err, elem_prefix)
- if not ResourceLoader.exists(elem):
- return ErrorCodes.prefix_message(
- ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s" % elem),
- elem_prefix,
- )
- var loaded := ResourceLoader.load(elem)
- if loaded == null:
- return ErrorCodes.prefix_message(
- ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s" % elem),
- elem_prefix,
- )
- return loaded
- if elem is Dictionary and (elem as Dictionary).has("__class__"):
- var type_str: String = (elem as Dictionary).get("__class__", "")
- var made := ResourceHandler._instantiate_resource(type_str)
- if made is Dictionary:
- return ErrorCodes.prefix_message(made, elem_prefix)
- var res: Resource = made
- var remaining: Dictionary = (elem as Dictionary).duplicate()
- remaining.erase("__class__")
- if not remaining.is_empty():
- var apply_err: Variant = ResourceHandler._apply_resource_properties(res, remaining)
- if apply_err != null:
- return ErrorCodes.prefix_message(apply_err, elem_prefix)
- return res
- return ErrorCodes.prefix_message(
- ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- 'cannot convert %s to an Object element; pass a res:// path or {"__class__": ...}'
- % type_string(typeof(elem)),
- ),
- elem_prefix,
- )
-
-
-## True when `elem` satisfies the object-typed slot's element constraint —
-## script type when the slot is Array[MyScriptClass], native class
-## otherwise. Checked per element so a wrong-class Resource errors naming
-## the index instead of being rejected wholesale by `Array.assign()`.
-static func _object_element_conforms(elem: Object, slot_value: Array) -> bool:
- return _object_conforms(elem, slot_value.get_typed_class_name(), slot_value.get_typed_script())
-
-
-## Shared class/script conformance predicate for typed Array elements and
-## typed Dictionary values (#612 stages 2–3).
-static func _object_conforms(elem: Object, cls_name: StringName, script: Variant) -> bool:
- if script is Script:
- return is_instance_of(elem, script)
- var cls := String(cls_name)
- return cls.is_empty() or elem.is_class(cls)
-
-
-## Fill a typed `Dictionary[K, V]` slot from a JSON object (#612 stage 3).
-## `slot_value` is the property's current typed Dictionary — the getter
-## returns the (possibly empty) typed container carrying both constraint
-## sides. Keys coerce via `_coerce_typed_dict_key` (JSON object keys are
-## always Strings, so int/float/StringName key slots parse the string and
-## fail closed on anything inexact); values mirror the typed-array element
-## rules — object values through `_coerce_object_element` + conformance,
-## everything else through `_coerce_value`/`_check_coerced`. Never partial:
-## any bad key or value errors naming the key and nothing is written.
-##
-## Returns the filled TYPED Dictionary on success or an UNTYPED
-## `make(...)`-shaped error Dictionary — callers discriminate on
-## `is_typed()`, since a success result is always a duplicate of the typed
-## slot and error envelopes are plain dicts.
-static func _coerce_typed_dictionary(
- value: Variant, slot_value: Dictionary, prefix: String = ""
-) -> Dictionary:
- var label := _typed_dictionary_label(slot_value)
- if not (value is Dictionary):
- var err := ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Cannot write %s to a typed %s property; expected an object" % [
- type_string(typeof(value)), label,
- ],
- )
- return ErrorCodes.prefix_message(err, prefix)
- var key_type := slot_value.get_typed_key_builtin() if slot_value.is_typed_key() else TYPE_NIL
- var value_type := (
- slot_value.get_typed_value_builtin() if slot_value.is_typed_value() else TYPE_NIL
- )
- var out := slot_value.duplicate()
- out.clear()
- var in_dict: Dictionary = value
- for raw_key in in_dict.keys():
- var key_prefix := (
- 'key "%s"' % str(raw_key) if prefix.is_empty()
- else '%s key "%s"' % [prefix, str(raw_key)]
- )
- var key: Variant = _coerce_typed_dict_key(raw_key, key_type, label, key_prefix)
- if key is Dictionary:
- ## Scalar-only key coercion never returns a legit Dictionary key,
- ## so a Dictionary here is unambiguously the error envelope.
- return key
- var raw_value: Variant = in_dict[raw_key]
- var coerced: Variant
- if value_type == TYPE_NIL:
- ## Untyped value side (e.g. Dictionary[String, Variant]).
- coerced = raw_value
- elif value_type == TYPE_OBJECT:
- coerced = _coerce_object_element(raw_value, key_prefix)
- if coerced is Dictionary:
- return coerced
- if coerced != null and not _object_conforms(
- coerced,
- slot_value.get_typed_value_class_name(),
- slot_value.get_typed_value_script(),
- ):
- var conform_err := ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "value is %s, which is not a %s" % [
- (coerced as Object).get_class(),
- _object_type_label(
- slot_value.get_typed_value_class_name(),
- slot_value.get_typed_value_script(),
- ),
- ],
- )
- return ErrorCodes.prefix_message(conform_err, key_prefix)
- else:
- coerced = _coerce_value(raw_value, value_type)
- var value_err := _check_coerced(coerced, value_type, key_prefix)
- if value_err != null:
- return value_err
- if coerced == null:
- var null_err := ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "cannot store null as a %s value in %s" % [type_string(value_type), label],
- )
- return ErrorCodes.prefix_message(null_err, key_prefix)
- out[key] = coerced
- if out.size() != in_dict.size():
- ## Two input keys collapsing onto one coerced key ("1" and "01" both
- ## parse to int 1) would silently lose an entry — refuse instead.
- var collide_err := ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "%s keys collide after coercion (%d of %d entries landed)" % [
- label, out.size(), in_dict.size(),
- ],
- )
- return ErrorCodes.prefix_message(collide_err, prefix)
- return out
-
-
-## Coerce one JSON-object key onto a typed Dictionary's key slot. JSON keys
-## are always Strings, so int/float/StringName key types accept exactly the
-## strings that parse cleanly; everything else fails closed naming the key.
-## Object/compound key types are unreachable from JSON and refuse loudly.
-static func _coerce_typed_dict_key(
- raw_key: Variant, key_type: int, label: String, key_prefix: String
-) -> Variant:
- if key_type == TYPE_NIL or typeof(raw_key) == key_type:
- return raw_key
- if raw_key is String:
- var key_str := raw_key as String
- match key_type:
- TYPE_STRING_NAME:
- return StringName(key_str)
- TYPE_INT:
- if key_str.is_valid_int():
- return int(key_str)
- TYPE_FLOAT:
- if key_str.is_valid_float():
- return float(key_str)
- ## String key that didn't parse: JSON object keys are ALWAYS strings,
- ## so blaming the String-ness would imply the caller could somehow
- ## send a non-string key — name the expected key type instead.
- var parse_err := ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "key does not parse as %s (the %s key type)" % [type_string(key_type), label],
- )
- return ErrorCodes.prefix_message(parse_err, key_prefix)
- if raw_key is float and key_type == TYPE_INT and is_equal_approx(raw_key, roundf(raw_key)):
- ## Whole JSON numbers arrive as floats through some non-JSON callers.
- return int(raw_key)
- var err := ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "cannot use %s as a %s key" % [type_string(typeof(raw_key)), label],
- )
- return ErrorCodes.prefix_message(err, key_prefix)
-
-
-## "Dictionary[String, int]" / "Dictionary[int, Texture2D]" — for error
-## messages. Untyped sides read as Variant.
-static func _typed_dictionary_label(slot_value: Dictionary) -> String:
- var key_label := "Variant"
- if slot_value.is_typed_key():
- key_label = (
- _object_type_label(
- slot_value.get_typed_key_class_name(), slot_value.get_typed_key_script()
- )
- if slot_value.get_typed_key_builtin() == TYPE_OBJECT
- else type_string(slot_value.get_typed_key_builtin())
- )
- var value_label := "Variant"
- if slot_value.is_typed_value():
- value_label = (
- _object_type_label(
- slot_value.get_typed_value_class_name(), slot_value.get_typed_value_script()
- )
- if slot_value.get_typed_value_builtin() == TYPE_OBJECT
- else type_string(slot_value.get_typed_value_builtin())
- )
- return "Dictionary[%s, %s]" % [key_label, value_label]
-
-
-## Class/script display name for an object-typed constraint side.
-static func _object_type_label(cls_name: StringName, script: Variant) -> String:
- var cls := String(cls_name)
- if script is Script and not String((script as Script).get_global_name()).is_empty():
- cls = String((script as Script).get_global_name())
- return cls if not cls.is_empty() else "Object"
-
-
-func get_node_properties(params: Dictionary) -> Dictionary:
- var resolved := _resolve_node(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- var scene_root: Node = resolved.scene_root
-
- # Optional token-reducing filter: `fields` restricts the response to a
- # named subset. Defaults off (empty), so existing callers see the full
- # dump unchanged. The MCP tool types this as a list, but batch_execute and
- # raw callers bypass that, so validate the shape here before iterating.
- var fields_param: Variant = params.get("fields", [])
- if fields_param == null:
- fields_param = []
- if not (fields_param is Array):
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "'fields' must be an array of property names, got %s (%s)" % [
- type_string(typeof(fields_param)), str(fields_param),
- ],
- )
- var field_filter := {}
- for f in fields_param:
- ## Property names are strings on the wire; anything else is a
- ## malformed filter (e.g. [123] or [["fov"]]) — reject rather than
- ## silently stringify into a filter that matches nothing (#123/#126:
- ## strict within the accepted shape). StringName is allowed for
- ## editor-side callers.
- if not (f is String or f is StringName):
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "'fields' elements must be property-name strings, got %s in %s" % [
- type_string(typeof(f)), str(fields_param),
- ],
- )
- field_filter[str(f)] = true
- var use_field_filter := not field_filter.is_empty()
-
- var properties: Array[Dictionary] = []
- var editor_property_count := 0
- var matched_fields := {}
- for prop in node.get_property_list():
- var usage: int = prop.get("usage", 0)
- if not (usage & PROPERTY_USAGE_EDITOR):
- continue
- editor_property_count += 1
- if use_field_filter:
- if not field_filter.has(prop.name):
- continue
- matched_fields[prop.name] = true
- # Null reads are values, not omissions: `script` on an unscripted node
- # and unset Resource slots (mesh, material, …) read back null and must
- # appear as "value": null with their declared type, so callers can tell
- # "Object-typed, currently unset" from "doesn't exist" (#771).
- properties.append({
- "name": prop.name,
- "type": type_string(prop.type),
- "value": _serialize_value(node.get(prop.name)),
- })
- # Requested names that matched no editor-visible property — distinguishes
- # "you asked for something that doesn't exist" from "exists and is null".
- var unknown_fields: Array[String] = []
- for f in field_filter:
- if not matched_fields.has(f):
- unknown_fields.append(f)
- return {
- "data": {
- "path": node_path,
- "node_type": node.get_class(),
- "properties": properties,
- "count": properties.size(),
- # Total editor-visible properties before field filtering, so a
- # caller that passed `fields` knows how many were withheld.
- # Invariant: an unfiltered call returns every editor-visible
- # property, so count == total_count; only the `fields` filter
- # can make count < total_count.
- "total_count": editor_property_count,
- "unknown_fields": unknown_fields,
- }
- }
-
-
-func get_children(params: Dictionary) -> Dictionary:
- var resolved := _resolve_node(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- var scene_root: Node = resolved.scene_root
-
- var children: Array[Dictionary] = []
- for child in node.get_children():
- children.append({
- "name": child.name,
- "type": child.get_class(),
- "path": McpScenePath.from_node(child, scene_root),
- "children_count": child.get_child_count(),
- })
- return {
- "data": {
- "parent_path": node_path,
- "children": children,
- "count": children.size(),
- }
- }
-
-
-func get_groups(params: Dictionary) -> Dictionary:
- var resolved := _resolve_node(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
-
- var groups: Array[String] = []
- for group in node.get_groups():
- # Skip internal groups (start with underscore)
- if not str(group).begins_with("_"):
- groups.append(str(group))
- return {
- "data": {
- "path": node_path,
- "groups": groups,
- "count": groups.size(),
- }
- }
-
-
-## Validate path param, resolve to node. Returns dict with node/path/scene_root
-## on success, or an error dict (has "error" key) on failure. Thin wrapper
-## around the shared `McpNodeValidator.resolve_or_error` helper (audit-v2 #20).
-func _resolve_node(params: Dictionary) -> Dictionary:
- return McpNodeValidator.resolve_or_error(
- params.get("path", ""), "path", params.get("scene_file", ""),
- )
-
-
-## Reject operations targeting the scene root. Returns an INVALID_PARAMS error
-## dict with "Cannot the scene root", or null if `node` is not the root.
-static func _reject_if_scene_root(node: Node, scene_root: Node, op: String) -> Variant:
- if node == scene_root:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "Cannot %s the scene root" % op)
- return null
-
-
-## Convert a Godot Variant to a JSON-safe value. Compound geometry types
-## (AABB, Rect2, Transforms, …) and packed arrays serialize as structured
-## dicts/arrays so agents can inspect fields instead of parsing Godot's
-## debug repr — see issue #214.
-static func _serialize_value(value: Variant) -> Variant:
- return VariantSerializer.serialize(value)
diff --git a/addons/godot_ai/handlers/node_handler.gd.uid b/addons/godot_ai/handlers/node_handler.gd.uid
deleted file mode 100644
index 86149b4..0000000
--- a/addons/godot_ai/handlers/node_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://qhhd5mm5awym
diff --git a/addons/godot_ai/handlers/particle_handler.gd b/addons/godot_ai/handlers/particle_handler.gd
deleted file mode 100644
index d994bad..0000000
--- a/addons/godot_ai/handlers/particle_handler.gd
+++ /dev/null
@@ -1,860 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Handles particle emitter authoring (GPU + CPU, 2D + 3D).
-##
-## All write operations bundle node creation and sub-resource spawns
-## (ParticleProcessMaterial, default QuadMesh) in a single create_action
-## so Ctrl-Z rolls back the whole effect atomically.
-
-const ParticleValues := preload("res://addons/godot_ai/handlers/particle_values.gd")
-const ParticlePresets := preload("res://addons/godot_ai/handlers/particle_presets.gd")
-
-const _VALID_TYPES := {
- "gpu_3d": "GPUParticles3D",
- "gpu_2d": "GPUParticles2D",
- "cpu_3d": "CPUParticles3D",
- "cpu_2d": "CPUParticles2D",
-}
-
-const _MAIN_KEYS := [
- "amount",
- "lifetime",
- "one_shot",
- "explosiveness",
- "preprocess",
- "speed_scale",
- "randomness",
- "fixed_fps",
- "emitting",
- "local_coords",
- "interp_to_end",
-]
-
-
-var _undo_redo: EditorUndoRedoManager
-
-
-func _init(undo_redo: EditorUndoRedoManager) -> void:
- _undo_redo = undo_redo
-
-
-# ============================================================================
-# particle_create
-# ============================================================================
-
-func create_particle(params: Dictionary) -> Dictionary:
- var parent_path: String = params.get("parent_path", "")
- var node_name: String = params.get("name", "Particles")
- var type_str: String = params.get("type", "gpu_3d")
-
- if not _VALID_TYPES.has(type_str):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid particle type '%s'. Valid: %s" % [type_str, ", ".join(_VALID_TYPES.keys())]
- )
-
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- var parent: Node = scene_root
- if not parent_path.is_empty():
- parent = McpScenePath.resolve(parent_path, scene_root)
- if parent == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, McpScenePath.format_parent_error(parent_path, scene_root))
-
- var node := _instantiate_particle(type_str)
- if node == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate particle node")
- if not node_name.is_empty():
- node.name = node_name
-
- var process_mat: ParticleProcessMaterial = null
- var process_material_created := false
- var draw_mesh: Mesh = null
- var draw_material: StandardMaterial3D = null
- var draw_pass_mesh_created := false
- var draw_material_created := false
-
- if type_str == "gpu_3d" or type_str == "gpu_2d":
- process_mat = ParticleProcessMaterial.new()
- process_material_created = true
- if type_str == "gpu_3d":
- draw_mesh = QuadMesh.new()
- (draw_mesh as QuadMesh).size = Vector2(0.25, 0.25)
- # Without a material, the mesh renders flat white — ignoring
- # ParticleProcessMaterial.color_ramp entirely. Give it the standard
- # billboard + vertex-color-as-albedo setup so color_ramp works.
- draw_material = ParticleValues.build_draw_material({}).material
- (draw_mesh as QuadMesh).material = draw_material
- draw_pass_mesh_created = true
- draw_material_created = true
-
- _undo_redo.create_action("MCP: Create %s '%s'" % [_VALID_TYPES[type_str], node.name])
- _undo_redo.add_do_method(parent, "add_child", node, true)
- _undo_redo.add_do_method(node, "set_owner", scene_root)
- if process_mat != null:
- _undo_redo.add_do_property(node, "process_material", process_mat)
- _undo_redo.add_do_reference(process_mat)
- if draw_mesh != null:
- _undo_redo.add_do_property(node, "draw_pass_1", draw_mesh)
- _undo_redo.add_do_reference(draw_mesh)
- if draw_material != null:
- _undo_redo.add_do_reference(draw_material)
- _undo_redo.add_do_reference(node)
- _undo_redo.add_undo_method(parent, "remove_child", node)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": McpScenePath.from_node(node, scene_root),
- "parent_path": McpScenePath.from_node(parent, scene_root),
- "name": String(node.name),
- "type": type_str,
- "class": _VALID_TYPES[type_str],
- "process_material_created": process_material_created,
- "draw_pass_mesh_created": draw_pass_mesh_created,
- "draw_material_created": draw_material_created,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# particle_set_main
-# ============================================================================
-
-func set_main(params: Dictionary) -> Dictionary:
- var resolved := _resolve_particle(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
-
- var properties: Dictionary = params.get("properties", {})
- if properties.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "properties dict is empty")
-
- var coerced: Dictionary = {}
- var old_values: Dictionary = {}
- for property in properties:
- var prop_name: String = String(property)
- if not (prop_name in _MAIN_KEYS):
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Unknown main property '%s'. Valid: %s" % [prop_name, ", ".join(_MAIN_KEYS)]
- )
- var prop_type := _node_property_type(node, prop_name)
- if prop_type == TYPE_NIL:
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Property '%s' not present on %s" % [prop_name, node.get_class()]
- )
- var coerce_result := ParticleValues.coerce(prop_name, properties[prop_name], prop_type)
- if not coerce_result.ok:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerce_result.error))
- coerced[prop_name] = coerce_result.value
- old_values[prop_name] = node.get(prop_name)
-
- _undo_redo.create_action("MCP: Set particle main on %s" % node.name)
- for prop_name in coerced:
- _undo_redo.add_do_property(node, prop_name, coerced[prop_name])
- _undo_redo.add_undo_property(node, prop_name, old_values[prop_name])
- _undo_redo.commit_action()
-
- var applied: Array[String] = []
- var serialized_values: Dictionary = {}
- for prop_name in coerced:
- applied.append(prop_name)
- serialized_values[prop_name] = ParticleValues.serialize(coerced[prop_name])
-
- return {
- "data": {
- "path": node_path,
- "applied": applied,
- "values": serialized_values,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# particle_set_process
-# ============================================================================
-
-func set_process(params: Dictionary) -> Dictionary:
- var resolved := _resolve_particle(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
-
- var properties: Dictionary = params.get("properties", {})
- if properties.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "properties dict is empty")
-
- # GPU: work through process_material; CPU: properties live on node directly.
- if node is GPUParticles3D or node is GPUParticles2D:
- return _set_process_gpu(node, node_path, properties)
- return _set_process_cpu(node, node_path, properties)
-
-
-func _set_process_gpu(node: Node, node_path: String, properties: Dictionary) -> Dictionary:
- var existing_mat: ParticleProcessMaterial = node.process_material as ParticleProcessMaterial
- var process_material_created := false
- var mat: ParticleProcessMaterial = existing_mat
- if mat == null:
- mat = ParticleProcessMaterial.new()
- process_material_created = true
-
- var coerced: Dictionary = {}
- for property in properties:
- var prop_name: String = String(property)
- var prop_type := _object_property_type(mat, prop_name)
- if prop_type == TYPE_NIL:
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Property '%s' not present on ParticleProcessMaterial" % prop_name
- )
- var coerce_result := ParticleValues.coerce(prop_name, properties[prop_name], prop_type)
- if not coerce_result.ok:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerce_result.error))
- coerced[prop_name] = coerce_result.value
-
- _undo_redo.create_action("MCP: Set particle process on %s" % node.name)
- if process_material_created:
- _undo_redo.add_do_property(node, "process_material", mat)
- _undo_redo.add_undo_property(node, "process_material", null)
- _undo_redo.add_do_reference(mat)
- # Apply new values directly on the (newly created) material. No old values to restore.
- for prop_name in coerced:
- mat.set(prop_name, coerced[prop_name])
- else:
- # Use the reusable apply/restore pattern for existing material.
- var old_values: Dictionary = {}
- for prop_name in coerced:
- old_values[prop_name] = mat.get(prop_name)
- for prop_name in coerced:
- _undo_redo.add_do_property(mat, prop_name, coerced[prop_name])
- _undo_redo.add_undo_property(mat, prop_name, old_values[prop_name])
- _undo_redo.commit_action()
-
- var applied: Array[String] = []
- var serialized: Dictionary = {}
- for prop_name in coerced:
- applied.append(prop_name)
- serialized[prop_name] = ParticleValues.serialize(mat.get(prop_name))
-
- return {
- "data": {
- "path": node_path,
- "applied": applied,
- "values": serialized,
- "process_material_created": process_material_created,
- "undoable": true,
- }
- }
-
-
-func _set_process_cpu(node: Node, node_path: String, properties: Dictionary) -> Dictionary:
- # CPU particles expose the same property vocabulary directly on the node,
- # so property names pass through unchanged.
- var coerced: Dictionary = {}
- var old_values: Dictionary = {}
-
- for property in properties:
- var prop_name: String = String(property)
- var prop_type := _node_property_type(node, prop_name)
- if prop_type == TYPE_NIL:
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Property '%s' not present on %s" % [prop_name, node.get_class()]
- )
- var coerce_result := ParticleValues.coerce(prop_name, properties[property], prop_type)
- if not coerce_result.ok:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerce_result.error))
- coerced[prop_name] = coerce_result.value
- old_values[prop_name] = node.get(prop_name)
-
- _undo_redo.create_action("MCP: Set particle process on %s" % node.name)
- for prop_name in coerced:
- _undo_redo.add_do_property(node, prop_name, coerced[prop_name])
- _undo_redo.add_undo_property(node, prop_name, old_values[prop_name])
- _undo_redo.commit_action()
-
- var applied: Array[String] = []
- var serialized: Dictionary = {}
- for prop_name in coerced:
- applied.append(prop_name)
- serialized[prop_name] = ParticleValues.serialize(coerced[prop_name])
-
- return {
- "data": {
- "path": node_path,
- "applied": applied,
- "values": serialized,
- "process_material_created": false,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# particle_set_draw_pass
-# ============================================================================
-
-func set_draw_pass(params: Dictionary) -> Dictionary:
- var resolved := _resolve_particle(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
-
- var pass_idx: int = int(params.get("pass", 1))
- var mesh_path: String = params.get("mesh", "")
- var texture_path: String = params.get("texture", "")
- var material_path: String = params.get("material", "")
-
- if node is GPUParticles3D:
- return _set_draw_pass_gpu_3d(node, node_path, pass_idx, mesh_path, material_path)
- if node is CPUParticles3D:
- return _set_draw_pass_cpu_3d(node, node_path, mesh_path, material_path)
- if node is GPUParticles2D or node is CPUParticles2D:
- return _set_draw_pass_2d(node, node_path, texture_path)
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Node %s is not a particle node" % node.get_class())
-
-
-func _set_draw_pass_gpu_3d(node: GPUParticles3D, node_path: String, pass_idx: int, mesh_path: String, material_path: String) -> Dictionary:
- if pass_idx < 1 or pass_idx > 4:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "pass must be 1..4 (got %d)" % pass_idx)
-
- var mesh: Mesh = null
- var mesh_created := false
- var property_name := "draw_pass_%d" % pass_idx
- # draw_pass_N is only a live property when draw_passes >= N. Probe via
- # get_property_list so we don't read a ghost value.
- var existing_mesh: Mesh = null
- if int(node.draw_passes) >= pass_idx:
- existing_mesh = node.get(property_name) as Mesh
- if not mesh_path.is_empty():
- var mesh_path_err = McpPathValidator.loadable_error(mesh_path, "mesh_path")
- if mesh_path_err != null:
- return mesh_path_err
- if not ResourceLoader.exists(mesh_path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Mesh not found: %s" % mesh_path)
- var loaded := ResourceLoader.load(mesh_path)
- if not (loaded is Mesh):
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a Mesh" % mesh_path)
- mesh = loaded
- else:
- if existing_mesh == null:
- mesh = QuadMesh.new()
- (mesh as QuadMesh).size = Vector2(0.25, 0.25)
- mesh_created = true
- else:
- mesh = existing_mesh
-
- var material: Material = null
- if not material_path.is_empty():
- var material_path_err = McpPathValidator.loadable_error(material_path, "material_path")
- if material_path_err != null:
- return material_path_err
- if not ResourceLoader.exists(material_path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Material not found: %s" % material_path)
- var loaded_mat := ResourceLoader.load(material_path)
- if not (loaded_mat is Material):
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a Material" % material_path)
- material = loaded_mat
-
- var old_draw_passes: int = int(node.draw_passes)
- var new_draw_passes: int = max(old_draw_passes, pass_idx)
- var old_value = existing_mesh # Null if draw_passes < pass_idx
- var old_material: Material = null
- if material != null:
- old_material = node.material_override
-
- _undo_redo.create_action("MCP: Set %s.draw_pass_%d" % [node.name, pass_idx])
- # Grow draw_passes first so draw_pass_N property exists before we set it.
- if new_draw_passes != old_draw_passes:
- _undo_redo.add_do_property(node, "draw_passes", new_draw_passes)
- _undo_redo.add_undo_property(node, "draw_passes", old_draw_passes)
- if not mesh_path.is_empty() or mesh_created:
- _undo_redo.add_do_property(node, property_name, mesh)
- _undo_redo.add_undo_property(node, property_name, old_value)
- if mesh_created:
- _undo_redo.add_do_reference(mesh)
- if material != null:
- _undo_redo.add_do_property(node, "material_override", material)
- _undo_redo.add_undo_property(node, "material_override", old_material)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "pass": pass_idx,
- "mesh_path": mesh_path,
- "mesh_class": mesh.get_class() if mesh else "",
- "material_path": material_path,
- "draw_pass_mesh_created": mesh_created,
- "draw_passes_grown": new_draw_passes != old_draw_passes,
- "undoable": true,
- }
- }
-
-
-func _set_draw_pass_cpu_3d(node: CPUParticles3D, node_path: String, mesh_path: String, material_path: String) -> Dictionary:
- if mesh_path.is_empty() and material_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "CPUParticles3D requires mesh or material param")
-
- var mesh: Mesh = node.mesh
- var old_mesh: Mesh = mesh
- if not mesh_path.is_empty():
- var mesh_path_err = McpPathValidator.loadable_error(mesh_path, "mesh_path")
- if mesh_path_err != null:
- return mesh_path_err
- if not ResourceLoader.exists(mesh_path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Mesh not found: %s" % mesh_path)
- var loaded := ResourceLoader.load(mesh_path)
- if not (loaded is Mesh):
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a Mesh" % mesh_path)
- mesh = loaded
-
- var material: Material = null
- var old_material: Material = node.material_override
- if not material_path.is_empty():
- var material_path_err = McpPathValidator.loadable_error(material_path, "material_path")
- if material_path_err != null:
- return material_path_err
- if not ResourceLoader.exists(material_path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Material not found: %s" % material_path)
- var loaded_mat := ResourceLoader.load(material_path)
- if not (loaded_mat is Material):
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a Material" % material_path)
- material = loaded_mat
-
- _undo_redo.create_action("MCP: Set CPU particle draw on %s" % node.name)
- if not mesh_path.is_empty():
- _undo_redo.add_do_property(node, "mesh", mesh)
- _undo_redo.add_undo_property(node, "mesh", old_mesh)
- if material != null:
- _undo_redo.add_do_property(node, "material_override", material)
- _undo_redo.add_undo_property(node, "material_override", old_material)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "mesh_path": mesh_path,
- "material_path": material_path,
- "draw_pass_mesh_created": false,
- "undoable": true,
- }
- }
-
-
-func _set_draw_pass_2d(node: Node, node_path: String, texture_path: String) -> Dictionary:
- if texture_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "2D particles require texture param")
- var texture_path_err = McpPathValidator.loadable_error(texture_path, "texture_path")
- if texture_path_err != null:
- return texture_path_err
- if not ResourceLoader.exists(texture_path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Texture not found: %s" % texture_path)
- var tex := ResourceLoader.load(texture_path)
- if not (tex is Texture2D):
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a Texture2D" % texture_path)
-
- var old_texture: Texture2D = node.get("texture")
-
- _undo_redo.create_action("MCP: Set 2D particle texture on %s" % node.name)
- _undo_redo.add_do_property(node, "texture", tex)
- _undo_redo.add_undo_property(node, "texture", old_texture)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "texture_path": texture_path,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# particle_restart
-# ============================================================================
-
-func restart_particle(params: Dictionary) -> Dictionary:
- var resolved := _resolve_particle(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- if node.has_method("restart"):
- node.restart()
- return {
- "data": {
- "path": node_path,
- "undoable": false,
- "reason": "Restart is a runtime operation, not tracked in undo history",
- }
- }
-
-
-# ============================================================================
-# particle_get
-# ============================================================================
-
-func get_particle(params: Dictionary) -> Dictionary:
- var resolved := _resolve_particle(params)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
-
- var type_str := ""
- for key in _VALID_TYPES:
- if node.get_class() == _VALID_TYPES[key]:
- type_str = key
- break
-
- var main_values: Dictionary = {}
- var node_prop_names := _property_names(node)
- for key in _MAIN_KEYS:
- if node_prop_names.has(key):
- main_values[key] = ParticleValues.serialize(node.get(key))
-
- var process_data: Dictionary = {}
- if node is GPUParticles3D or node is GPUParticles2D:
- var mat: ParticleProcessMaterial = node.process_material as ParticleProcessMaterial
- if mat != null:
- var process_props: Dictionary = {}
- for prop in mat.get_property_list():
- var usage: int = prop.get("usage", 0)
- if not (usage & PROPERTY_USAGE_EDITOR):
- continue
- var v = mat.get(prop.name)
- if v == null:
- continue
- process_props[prop.name] = ParticleValues.serialize(v)
- process_data = {
- "class": "ParticleProcessMaterial",
- "properties": process_props,
- }
-
- var draw_passes: Array[Dictionary] = []
- if node is GPUParticles3D:
- var active_draw_pass_count: int = min(int(node.draw_passes), 4)
- for i in range(1, active_draw_pass_count + 1):
- var prop_name := "draw_pass_%d" % i
- var m: Mesh = node.get(prop_name) as Mesh
- draw_passes.append({
- "pass": i,
- "mesh_class": m.get_class() if m != null else "",
- })
-
- var texture_path := ""
- if node is GPUParticles2D or node is CPUParticles2D:
- var t: Texture2D = node.get("texture")
- if t != null:
- texture_path = t.resource_path
-
- return {
- "data": {
- "path": node_path,
- "type": type_str,
- "class": node.get_class(),
- "main": main_values,
- "process": process_data,
- "draw_passes": draw_passes,
- "texture_path": texture_path,
- }
- }
-
-
-# ============================================================================
-# particle_apply_preset
-# ============================================================================
-
-func apply_preset(params: Dictionary) -> Dictionary:
- var preset_name: String = params.get("preset", "")
- if preset_name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: preset")
-
- var overrides: Dictionary = params.get("overrides", {})
- var blueprint = ParticlePresets.build(preset_name, overrides)
- if blueprint == null:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Unknown preset '%s'. Valid: %s" % [preset_name, ", ".join(ParticlePresets.list())]
- )
-
- var parent_path: String = params.get("parent_path", "")
- var node_name: String = params.get("name", "")
- var type_str: String = params.get("type", "gpu_3d")
- if node_name.is_empty():
- node_name = preset_name.capitalize()
- if not _VALID_TYPES.has(type_str):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid particle type '%s'. Valid: %s" % [type_str, ", ".join(_VALID_TYPES.keys())]
- )
-
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- var parent: Node = scene_root
- if not parent_path.is_empty():
- parent = McpScenePath.resolve(parent_path, scene_root)
- if parent == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, McpScenePath.format_parent_error(parent_path, scene_root))
-
- var node := _instantiate_particle(type_str)
- node.name = node_name
-
- var is_gpu := type_str == "gpu_3d" or type_str == "gpu_2d"
- var is_3d := type_str == "gpu_3d" or type_str == "cpu_3d"
-
- var process_mat: ParticleProcessMaterial = null
- var process_material_created := false
- if is_gpu:
- process_mat = ParticleProcessMaterial.new()
- process_material_created = true
-
- # User-supplied override keys per group. Preset-blueprint keys may skip
- # silently on types they don't apply to (presets are cross-type by
- # design); user-requested overrides must apply or error (#770).
- var user_keys: Dictionary = blueprint.get("user_keys", {})
- var user_main: Dictionary = user_keys.get("main", {})
- var user_process: Dictionary = user_keys.get("process", {})
- var user_draw: Dictionary = user_keys.get("draw", {})
-
- var draw_mesh: Mesh = null
- var draw_material: StandardMaterial3D = null
- var draw_pass_mesh_created := false
- var draw_material_created := false
- var applied_draw: Array[String] = []
- var draw_config: Dictionary = blueprint.get("draw", {})
- if type_str == "gpu_3d":
- var draw_result := ParticleValues.build_draw_material(draw_config)
- if not draw_result.ok:
- node.free()
- var msg := String(draw_result.error)
- if draw_result.get("unknown_key", false):
- msg = _draw_key_unsupported_message(String(draw_result.key), type_str)
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, msg)
- draw_mesh = QuadMesh.new()
- (draw_mesh as QuadMesh).size = Vector2(0.25, 0.25)
- draw_material = draw_result.material
- (draw_mesh as QuadMesh).material = draw_material
- draw_pass_mesh_created = true
- draw_material_created = true
- for applied_key in draw_result.applied:
- applied_draw.append(String(applied_key))
- elif type_str == "gpu_2d":
- # GPUParticles2D has no draw-pass material; the one draw override it
- # supports is draw.texture → the node's texture property.
- for key in user_draw:
- if String(key) != "texture":
- node.free()
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- _draw_key_unsupported_message(String(key), type_str)
- )
- if user_draw.has("texture"):
- var tex_result := _load_draw_texture(draw_config.get("texture"))
- if tex_result.has("error"):
- node.free()
- return tex_result
- node.set("texture", tex_result.texture)
- applied_draw.append("texture")
- else:
- # cpu_3d / cpu_2d: no draw support — reject user draw overrides
- # instead of dropping them.
- for key in user_draw:
- node.free()
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- _draw_key_unsupported_message(String(key), type_str)
- )
-
- # Pre-apply preset values to in-memory targets (no undo needed; nodes not in tree yet).
- var main_values: Dictionary = blueprint.get("main", {})
- var process_values: Dictionary = blueprint.get("process", {})
- var applied_main: Array[String] = []
- var applied_process: Array[String] = []
-
- for prop in main_values:
- var prop_name := String(prop)
- var prop_type := _object_property_type(node, prop_name)
- if prop_type == TYPE_NIL:
- if user_main.has(prop_name):
- var node_class := node.get_class()
- node.free()
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Override main.%s does not apply to type '%s' (no such property on %s)" % [
- prop_name, type_str, node_class
- ]
- )
- continue # Blueprint key: not all main keys apply to all types.
- var coerce_result := ParticleValues.coerce(prop_name, main_values[prop_name], prop_type)
- if not coerce_result.ok:
- node.free()
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerce_result.error))
- node.set(prop_name, coerce_result.value)
- applied_main.append(prop_name)
-
- # Apply process: GPU targets the ParticleProcessMaterial; CPU targets the node.
- var process_target: Object = process_mat if is_gpu else node
- for prop in process_values:
- var prop_name := String(prop)
- var prop_type := _object_property_type(process_target, prop_name)
- if prop_type == TYPE_NIL:
- if user_process.has(prop_name):
- var target_class := process_target.get_class()
- node.free()
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Override process.%s does not apply to type '%s' (no such property on %s)" % [
- prop_name, type_str, target_class
- ]
- )
- continue # Blueprint key: preset property doesn't apply to this variant.
- var coerce_result := ParticleValues.coerce(prop_name, process_values[prop_name], prop_type)
- if not coerce_result.ok:
- node.free()
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerce_result.error))
- process_target.set(prop_name, coerce_result.value)
- applied_process.append(prop_name)
-
- _undo_redo.create_action("MCP: Apply preset %s" % preset_name)
- _undo_redo.add_do_method(parent, "add_child", node, true)
- _undo_redo.add_do_method(node, "set_owner", scene_root)
- _undo_redo.add_do_reference(node)
- if process_mat != null:
- _undo_redo.add_do_property(node, "process_material", process_mat)
- _undo_redo.add_do_reference(process_mat)
- if draw_mesh != null:
- _undo_redo.add_do_property(node, "draw_pass_1", draw_mesh)
- _undo_redo.add_do_reference(draw_mesh)
- if draw_material != null:
- _undo_redo.add_do_reference(draw_material)
- _undo_redo.add_undo_method(parent, "remove_child", node)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": McpScenePath.from_node(node, scene_root),
- "parent_path": McpScenePath.from_node(parent, scene_root),
- "name": node_name,
- "preset": preset_name,
- "type": type_str,
- "class": _VALID_TYPES[type_str],
- "applied_main": applied_main,
- "applied_process": applied_process,
- "applied_draw": applied_draw,
- "process_material_created": process_material_created,
- "draw_pass_mesh_created": draw_pass_mesh_created,
- "draw_material_created": draw_material_created,
- "is_3d": is_3d,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# Helpers
-# ============================================================================
-
-## Actionable rejection for a user draw override that can't apply to the
-## selected particle type: name the key and where it IS supported.
-static func _draw_key_unsupported_message(key: String, type_str: String) -> String:
- if key == "texture":
- return "draw.texture is only supported for gpu_2d (got type '%s')" % type_str
- var probe := StandardMaterial3D.new()
- if _object_property_type(probe, key) != TYPE_NIL:
- return "draw.%s is only supported for gpu_3d (got type '%s')" % [key, type_str]
- return (
- "Unknown draw key '%s' (draw overrides configure the gpu_3d "
- + "draw-pass StandardMaterial3D; gpu_2d supports only draw.texture)"
- ) % key
-
-
-## Load a Texture2D for the gpu_2d draw.texture override. Returns
-## {texture: Texture2D} or an error dict (same validation as set_draw_pass).
-static func _load_draw_texture(value: Variant) -> Dictionary:
- if not (value is String) or String(value).is_empty():
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "draw.texture must be a non-empty res:// path string (got %s)" % type_string(typeof(value))
- )
- var texture_path := String(value)
- var texture_path_err = McpPathValidator.loadable_error(texture_path, "draw.texture")
- if texture_path_err != null:
- return texture_path_err
- if not ResourceLoader.exists(texture_path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Texture not found: %s" % texture_path)
- var tex := ResourceLoader.load(texture_path)
- if not (tex is Texture2D):
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a Texture2D" % texture_path)
- return {"texture": tex}
-
-
-static func _instantiate_particle(type_str: String) -> Node:
- match type_str:
- "gpu_3d":
- return GPUParticles3D.new()
- "gpu_2d":
- return GPUParticles2D.new()
- "cpu_3d":
- return CPUParticles3D.new()
- "cpu_2d":
- return CPUParticles2D.new()
- return null
-
-
-func _resolve_particle(params: Dictionary) -> Dictionary:
- var resolved := McpNodeValidator.resolve_or_error(
- params.get("node_path", ""), "node_path",
- )
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- var node_path: String = resolved.path
- var is_particle := node is GPUParticles3D or node is GPUParticles2D \
- or node is CPUParticles3D or node is CPUParticles2D
- if not is_particle:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Node %s is not a particle node (got %s)" % [node_path, node.get_class()]
- )
- return {"node": node, "path": node_path}
-
-
-static func _node_property_type(node: Object, name: String) -> int:
- return _object_property_type(node, name)
-
-
-static func _object_property_type(obj: Object, name: String) -> int:
- if obj == null:
- return TYPE_NIL
- for prop in obj.get_property_list():
- if prop.name == name:
- return int(prop.get("type", TYPE_NIL))
- return TYPE_NIL
-
-
-static func _property_names(obj: Object) -> Dictionary:
- var out: Dictionary = {}
- if obj == null:
- return out
- for prop in obj.get_property_list():
- out[prop.name] = true
- return out
diff --git a/addons/godot_ai/handlers/particle_handler.gd.uid b/addons/godot_ai/handlers/particle_handler.gd.uid
deleted file mode 100644
index ead3722..0000000
--- a/addons/godot_ai/handlers/particle_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://byfc0pnyb5qww
diff --git a/addons/godot_ai/handlers/particle_presets.gd b/addons/godot_ai/handlers/particle_presets.gd
deleted file mode 100644
index 3bbf1f4..0000000
--- a/addons/godot_ai/handlers/particle_presets.gd
+++ /dev/null
@@ -1,293 +0,0 @@
-@tool
-extends RefCounted
-
-## Curated particle effect blueprints.
-##
-## Each preset returns {main, process, draw}. The handler applies them
-## through the normal write path (one undo action wraps all spawns).
-
-
-## Each preset has {main, process, draw}. `draw` configures the StandardMaterial3D
-## attached to the auto-created QuadMesh in draw_pass_1 (GPU 3D only); if
-## omitted, the handler falls back to a sensible billboard-particles default.
-## `blend_mode: "add"` is what makes fire/magic/explosion glow — without
-## additive blending, additively-layered particles just stack to gray.
-const _PRESETS := {
- "fire": {
- "main": {
- "amount": 80,
- "lifetime": 1.2,
- "one_shot": false,
- "explosiveness": 0.0,
- "preprocess": 0.5,
- "local_coords": false,
- },
- "process": {
- "emission_shape": "sphere",
- "emission_sphere_radius": 0.3,
- "direction": {"x": 0.0, "y": 1.0, "z": 0.0},
- "spread": 15.0,
- "initial_velocity_min": 2.0,
- "initial_velocity_max": 4.0,
- "gravity": {"x": 0.0, "y": 1.0, "z": 0.0}, # buoyancy
- "scale_min": 0.4,
- "scale_max": 0.8,
- "color_ramp": {
- "stops": [
- {"time": 0.0, "color": [1.0, 1.0, 0.9, 1.0]},
- {"time": 0.3, "color": [1.0, 0.6, 0.1, 1.0]},
- {"time": 0.7, "color": [0.8, 0.1, 0.05, 0.7]},
- {"time": 1.0, "color": [0.2, 0.05, 0.05, 0.0]},
- ]
- },
- },
- "draw": {"blend_mode": "add"},
- },
- "smoke": {
- "main": {
- "amount": 40,
- "lifetime": 3.0,
- "one_shot": false,
- "explosiveness": 0.0,
- "local_coords": false,
- },
- "process": {
- "emission_shape": "sphere",
- "emission_sphere_radius": 0.4,
- "direction": {"x": 0.0, "y": 1.0, "z": 0.0},
- "spread": 20.0,
- "initial_velocity_min": 0.5,
- "initial_velocity_max": 1.5,
- "gravity": {"x": 0.0, "y": 0.2, "z": 0.0},
- "scale_min": 0.6,
- "scale_max": 1.4,
- "color_ramp": {
- "stops": [
- {"time": 0.0, "color": [0.3, 0.3, 0.3, 0.0]},
- {"time": 0.25, "color": [0.35, 0.35, 0.35, 0.7]},
- {"time": 0.75, "color": [0.2, 0.2, 0.2, 0.5]},
- {"time": 1.0, "color": [0.1, 0.1, 0.1, 0.0]},
- ]
- },
- },
- # Smoke uses regular alpha blending so it darkens the background.
- "draw": {"blend_mode": "mix"},
- },
- "spark_burst": {
- "main": {
- "amount": 60,
- "lifetime": 0.8,
- "one_shot": true,
- "explosiveness": 1.0,
- "local_coords": false,
- },
- "process": {
- "emission_shape": "point",
- "direction": {"x": 0.0, "y": 1.0, "z": 0.0},
- "spread": 180.0,
- "initial_velocity_min": 5.0,
- "initial_velocity_max": 12.0,
- "gravity": {"x": 0.0, "y": -9.8, "z": 0.0},
- "scale_min": 0.05,
- "scale_max": 0.12,
- "color": {"r": 1.0, "g": 0.9, "b": 0.2, "a": 1.0},
- },
- "draw": {
- "blend_mode": "add",
- "emission_enabled": true,
- "emission": {"r": 1.0, "g": 0.8, "b": 0.2, "a": 1.0},
- "emission_energy_multiplier": 2.0,
- },
- },
- "magic_swirl": {
- "main": {
- "amount": 120,
- "lifetime": 2.0,
- "one_shot": false,
- "explosiveness": 0.0,
- "local_coords": false,
- },
- "process": {
- "emission_shape": "ring",
- "emission_ring_radius": 0.8,
- "emission_ring_inner_radius": 0.6,
- "emission_ring_height": 0.0,
- "direction": {"x": 0.0, "y": 1.0, "z": 0.0},
- "spread": 30.0,
- "initial_velocity_min": 1.0,
- "initial_velocity_max": 2.0,
- "gravity": {"x": 0.0, "y": 0.0, "z": 0.0},
- "angular_velocity_min": 90.0,
- "angular_velocity_max": 180.0,
- "scale_min": 0.1,
- "scale_max": 0.2,
- "color_ramp": {
- "stops": [
- {"time": 0.0, "color": [0.4, 0.9, 1.0, 0.0]},
- {"time": 0.3, "color": [0.5, 0.7, 1.0, 1.0]},
- {"time": 0.7, "color": [1.0, 0.4, 0.9, 1.0]},
- {"time": 1.0, "color": [0.8, 0.2, 0.7, 0.0]},
- ]
- },
- },
- "draw": {"blend_mode": "add"},
- },
- "rain": {
- "main": {
- "amount": 500,
- "lifetime": 1.5,
- "one_shot": false,
- "explosiveness": 0.0,
- "local_coords": false,
- },
- "process": {
- "emission_shape": "box",
- "emission_box_extents": {"x": 10.0, "y": 0.1, "z": 10.0},
- "direction": {"x": 0.0, "y": -1.0, "z": 0.0},
- "spread": 2.0,
- "initial_velocity_min": 15.0,
- "initial_velocity_max": 18.0,
- "gravity": {"x": 0.0, "y": -2.0, "z": 0.0},
- "scale_min": 0.02,
- "scale_max": 0.04,
- "color": {"r": 0.7, "g": 0.85, "b": 1.0, "a": 0.5},
- },
- # Rain drops render as streaks; fixed_y aligns them vertically.
- "draw": {"billboard_mode": "fixed_y", "blend_mode": "mix"},
- },
- "explosion": {
- "main": {
- "amount": 200,
- "lifetime": 1.5,
- "one_shot": true,
- "explosiveness": 1.0,
- "local_coords": false,
- },
- "process": {
- "emission_shape": "sphere",
- "emission_sphere_radius": 0.1,
- "direction": {"x": 0.0, "y": 1.0, "z": 0.0},
- "spread": 180.0,
- "initial_velocity_min": 6.0,
- "initial_velocity_max": 10.0,
- "gravity": {"x": 0.0, "y": -4.0, "z": 0.0},
- "scale_min": 0.3,
- "scale_max": 0.7,
- "color_ramp": {
- "stops": [
- {"time": 0.0, "color": [1.0, 0.95, 0.5, 1.0]},
- {"time": 0.2, "color": [1.0, 0.4, 0.1, 1.0]},
- {"time": 0.7, "color": [0.3, 0.15, 0.1, 0.7]},
- {"time": 1.0, "color": [0.1, 0.1, 0.1, 0.0]},
- ]
- },
- },
- "draw": {
- "blend_mode": "add",
- "emission_enabled": true,
- "emission": {"r": 1.0, "g": 0.5, "b": 0.1, "a": 1.0},
- "emission_energy_multiplier": 1.5,
- },
- },
- "lightning": {
- # Short, bright, electric-blue spark burst. One-shot — call
- # particle_restart to re-trigger. Pairs well with a scene-wide flash.
- "main": {
- "amount": 40,
- "lifetime": 0.35,
- "one_shot": true,
- "explosiveness": 1.0,
- "local_coords": false,
- },
- "process": {
- "emission_shape": "box",
- "emission_box_extents": {"x": 0.1, "y": 1.5, "z": 0.1},
- "direction": {"x": 0.0, "y": -1.0, "z": 0.0},
- "spread": 8.0,
- "initial_velocity_min": 18.0,
- "initial_velocity_max": 28.0,
- "gravity": {"x": 0.0, "y": 0.0, "z": 0.0},
- "scale_min": 0.08,
- "scale_max": 0.18,
- "color_ramp": {
- "stops": [
- {"time": 0.0, "color": [1.0, 1.0, 1.0, 1.0]},
- {"time": 0.2, "color": [0.6, 0.85, 1.0, 1.0]},
- {"time": 0.6, "color": [0.3, 0.5, 1.0, 0.9]},
- {"time": 1.0, "color": [0.1, 0.2, 0.7, 0.0]},
- ]
- },
- },
- "draw": {
- "blend_mode": "add",
- "emission_enabled": true,
- "emission": {"r": 0.5, "g": 0.8, "b": 1.0, "a": 1.0},
- "emission_energy_multiplier": 4.0,
- },
- },
-}
-
-
-static func list() -> Array:
- return _PRESETS.keys()
-
-
-static func has(preset_name: String) -> bool:
- return _PRESETS.has(preset_name)
-
-
-## Return deep-copied {main, process, draw} blueprint with overrides merged in,
-## plus "user_keys" ({main/process/draw: {key: true}}) recording which keys the
-## caller supplied. The handler needs that distinction: preset-blueprint keys
-## may skip silently on types they don't apply to (presets are cross-type by
-## design), but user-requested overrides must apply or error (#770).
-## Overrides may include top-level "main" / "process" / "draw" dicts, or bare
-## keys routed to main (_MAIN_KEYS) or process — draw keys must be nested.
-static func build(preset_name: String, overrides: Dictionary) -> Variant:
- if not _PRESETS.has(preset_name):
- return null
- var entry: Dictionary = _PRESETS[preset_name].duplicate(true)
- var main: Dictionary = entry.get("main", {})
- var process: Dictionary = entry.get("process", {})
- var draw: Dictionary = entry.get("draw", {})
- var user_keys := {"main": {}, "process": {}, "draw": {}}
- for key in overrides:
- var val = overrides[key]
- if key == "main" and val is Dictionary:
- for k in val:
- main[k] = val[k]
- user_keys.main[String(k)] = true
- elif key == "process" and val is Dictionary:
- for k in val:
- process[k] = val[k]
- user_keys.process[String(k)] = true
- elif key == "draw" and val is Dictionary:
- for k in val:
- draw[k] = val[k]
- user_keys.draw[String(k)] = true
- elif _MAIN_KEYS.has(key):
- main[key] = val
- user_keys.main[String(key)] = true
- else:
- process[key] = val
- user_keys.process[String(key)] = true
- entry["main"] = main
- entry["process"] = process
- entry["draw"] = draw
- entry["user_keys"] = user_keys
- return entry
-
-
-const _MAIN_KEYS := {
- "amount": true,
- "lifetime": true,
- "one_shot": true,
- "explosiveness": true,
- "preprocess": true,
- "speed_scale": true,
- "randomness": true,
- "fixed_fps": true,
- "emitting": true,
- "local_coords": true,
- "interp_to_end": true,
-}
diff --git a/addons/godot_ai/handlers/particle_presets.gd.uid b/addons/godot_ai/handlers/particle_presets.gd.uid
deleted file mode 100644
index 7c1aac9..0000000
--- a/addons/godot_ai/handlers/particle_presets.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bss2ccpmsxo4p
diff --git a/addons/godot_ai/handlers/particle_values.gd b/addons/godot_ai/handlers/particle_values.gd
deleted file mode 100644
index 66782f6..0000000
--- a/addons/godot_ai/handlers/particle_values.gd
+++ /dev/null
@@ -1,246 +0,0 @@
-@tool
-extends RefCounted
-
-## Value coercion + gradient/curve builders for particle properties.
-
-const MaterialValues := preload("res://addons/godot_ai/handlers/material_values.gd")
-
-const _EMISSION_SHAPES := {
- "point": ParticleProcessMaterial.EMISSION_SHAPE_POINT,
- "sphere": ParticleProcessMaterial.EMISSION_SHAPE_SPHERE,
- "sphere_surface": ParticleProcessMaterial.EMISSION_SHAPE_SPHERE_SURFACE,
- "box": ParticleProcessMaterial.EMISSION_SHAPE_BOX,
- "points": ParticleProcessMaterial.EMISSION_SHAPE_POINTS,
- "directed_points": ParticleProcessMaterial.EMISSION_SHAPE_DIRECTED_POINTS,
- "ring": ParticleProcessMaterial.EMISSION_SHAPE_RING,
-}
-
-
-## Resolve a shape name to the int enum, or return null.
-static func resolve_emission_shape(value: Variant) -> Variant:
- if value is int:
- return value
- if value is float:
- return int(value)
- if value is String:
- var key := String(value).to_lower()
- if _EMISSION_SHAPES.has(key):
- return _EMISSION_SHAPES[key]
- return null
-
-
-static func emission_shape_names() -> Array:
- return _EMISSION_SHAPES.keys()
-
-
-## Build a Gradient from {stops: [{time, color}]} dict.
-static func build_gradient(value: Variant) -> Variant:
- if value is Gradient:
- return value
- if value is GradientTexture1D:
- return (value as GradientTexture1D).gradient
- if not (value is Dictionary):
- return null
- var d: Dictionary = value
- if not d.has("stops"):
- return null
- var stops_array = d.get("stops")
- if not (stops_array is Array):
- return null
- var offsets := PackedFloat32Array()
- var colors := PackedColorArray()
- for stop in stops_array:
- if not (stop is Dictionary):
- return null
- offsets.append(float(stop.get("time", 0.0)))
- var c = MaterialValues.parse_color(stop.get("color"))
- if c == null:
- return null
- colors.append(c)
- var grad := Gradient.new()
- grad.offsets = offsets
- grad.colors = colors
- return grad
-
-
-## Build a GradientTexture1D wrapping a Gradient (what ParticleProcessMaterial.color_ramp wants).
-static func build_gradient_texture(value: Variant) -> Variant:
- if value is GradientTexture1D:
- return value
- var grad = build_gradient(value)
- if grad == null:
- return null
- var tex := GradientTexture1D.new()
- tex.gradient = grad
- return tex
-
-
-## Build a Curve from [{time, value}] or {points: [...]} (float-over-time).
-static func build_curve(value: Variant) -> Variant:
- if value is Curve:
- return value
- if value is CurveTexture:
- return (value as CurveTexture).curve
- var points_array: Variant = null
- if value is Array:
- points_array = value
- elif value is Dictionary and value.has("points"):
- points_array = value["points"]
- if not (points_array is Array):
- return null
- var curve := Curve.new()
- for pt in points_array:
- if not (pt is Dictionary):
- return null
- var t := float(pt.get("time", 0.0))
- var v := float(pt.get("value", 0.0))
- curve.add_point(Vector2(t, v))
- return curve
-
-
-static func build_curve_texture(value: Variant) -> Variant:
- if value is CurveTexture:
- return value
- var curve = build_curve(value)
- if curve == null:
- return null
- var tex := CurveTexture.new()
- tex.curve = curve
- return tex
-
-
-## Coerce a particle property value to the appropriate type.
-## Handles: Vector3/gravity/direction, Color, float, int, bool, enum strings.
-## For color_ramp returns a GradientTexture1D; for *_curve returns CurveTexture.
-static func coerce(property: String, value: Variant, target_type: int) -> Dictionary:
- # Special-cased properties.
- if property == "emission_shape":
- var shape = resolve_emission_shape(value)
- if shape == null:
- return {
- "ok": false,
- "error": "Invalid emission_shape '%s'. Valid: %s" % [
- value, ", ".join(emission_shape_names())
- ],
- }
- return {"ok": true, "value": int(shape)}
-
- if property == "color_ramp" or property == "color_initial_ramp":
- var tex = build_gradient_texture(value)
- if tex == null:
- return {"ok": false, "error": "Invalid gradient for %s (expected {stops: [{time, color}]})" % property}
- return {"ok": true, "value": tex}
-
- if property == "color" and value is Dictionary and not (value as Dictionary).has("stops"):
- # color is a single Color, not a ramp.
- var c = MaterialValues.parse_color(value)
- if c == null:
- return {"ok": false, "error": "Invalid color"}
- return {"ok": true, "value": c}
-
- if property.ends_with("_curve"):
- var tex = build_curve_texture(value)
- if tex == null:
- return {"ok": false, "error": "Invalid curve for %s (expected [{time, value}])" % property}
- return {"ok": true, "value": tex}
-
- # Fall through to the material coercer (handles Color/Vec3/Vec2/float/int/bool/enum).
- return MaterialValues.coerce_material_value(property, value, target_type)
-
-
-## Build a StandardMaterial3D suitable for GPUParticles3D draw-pass rendering.
-##
-## Godot's default Mesh has no material, which means ParticleProcessMaterial's
-## color_ramp (which drives the COLOR varying) gets ignored and particles
-## render as flat white squares that don't face the camera. A correct default
-## must have vertex_color_use_as_albedo=true, billboard=particles, unshaded,
-## and alpha transparency so the gradient actually modulates the pixels.
-##
-## Config is an optional dict that overrides individual properties. Supported
-## keys match BaseMaterial3D properties (plus enum-by-name via MaterialValues):
-## blend_mode: "mix" | "add" | "sub" | "mul"
-## transparency: "disabled" | "alpha" | "alpha_scissor" | "alpha_hash" | "alpha_depth_pre_pass"
-## shading_mode: "unshaded" | "per_pixel" | "per_vertex"
-## billboard_mode: "disabled" | "enabled" | "fixed_y" | "particles"
-## vertex_color_use_as_albedo: bool
-## emission_enabled: bool
-## emission: Color
-## emission_energy_multiplier: float
-## albedo_color: Color
-## albedo_texture: res:// path
-## (anything else accepted by BaseMaterial3D.set())
-##
-## Returns {ok: true, material: StandardMaterial3D, applied: Array[String]},
-## or {ok: false, key, error, unknown_key} when a config key is not a
-## StandardMaterial3D property or its value fails coercion — draw config must
-## not disappear silently (#770).
-static func build_draw_material(config: Dictionary) -> Dictionary:
- var mat := StandardMaterial3D.new()
- # Sensible defaults for particle draw-pass rendering.
- mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
- mat.vertex_color_use_as_albedo = true
- mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
- mat.billboard_mode = BaseMaterial3D.BILLBOARD_PARTICLES
- mat.billboard_keep_scale = true
- # Configure from dict overrides.
- var applied: Array[String] = []
- for key in config:
- var prop_name := String(key)
- var prop_type := _object_property_type(mat, prop_name)
- if prop_type == TYPE_NIL:
- return {
- "ok": false,
- "key": prop_name,
- "unknown_key": true,
- "error": "Unknown draw key '%s' (not a StandardMaterial3D property)" % prop_name,
- }
- var coerce_result := MaterialValues.coerce_material_value(
- prop_name, config[prop_name], prop_type
- )
- if not coerce_result.ok:
- return {
- "ok": false,
- "key": prop_name,
- "unknown_key": false,
- "error": "draw.%s: %s" % [prop_name, coerce_result.error],
- }
- mat.set(prop_name, coerce_result.value)
- applied.append(prop_name)
- return {"ok": true, "material": mat, "applied": applied}
-
-
-static func _object_property_type(obj: Object, name: String) -> int:
- if obj == null:
- return TYPE_NIL
- for prop in obj.get_property_list():
- if prop.name == name:
- return int(prop.get("type", TYPE_NIL))
- return TYPE_NIL
-
-
-## Serialize for response.
-static func serialize(value: Variant) -> Variant:
- if value == null:
- return null
- if value is GradientTexture1D:
- var grad := (value as GradientTexture1D).gradient
- if grad == null:
- return {"type": "GradientTexture1D", "stops": []}
- var stops: Array = []
- for i in grad.offsets.size():
- var c: Color = grad.colors[i]
- stops.append({
- "time": grad.offsets[i],
- "color": {"r": c.r, "g": c.g, "b": c.b, "a": c.a},
- })
- return {"type": "GradientTexture1D", "stops": stops}
- if value is CurveTexture:
- var curve := (value as CurveTexture).curve
- if curve == null:
- return {"type": "CurveTexture", "points": []}
- var points: Array = []
- for i in curve.get_point_count():
- var p := curve.get_point_position(i)
- points.append({"time": p.x, "value": p.y})
- return {"type": "CurveTexture", "points": points}
- return MaterialValues.serialize_value(value)
diff --git a/addons/godot_ai/handlers/particle_values.gd.uid b/addons/godot_ai/handlers/particle_values.gd.uid
deleted file mode 100644
index 71f6adc..0000000
--- a/addons/godot_ai/handlers/particle_values.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bnnnjq06dmclc
diff --git a/addons/godot_ai/handlers/physics_shape_handler.gd b/addons/godot_ai/handlers/physics_shape_handler.gd
deleted file mode 100644
index b958abb..0000000
--- a/addons/godot_ai/handlers/physics_shape_handler.gd
+++ /dev/null
@@ -1,772 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Sizes a CollisionShape2D/CollisionShape3D to match a visual sibling's
-## bounds. Auto-creates the concrete Shape subclass when the slot is empty
-## or the requested type differs — bundling creation and sizing in a single
-## undo action.
-##
-## Shape type defaults: Box for 3D, Rectangle for 2D.
-
-var _undo_redo: EditorUndoRedoManager
-var _connection
-
-
-func _init(undo_redo: EditorUndoRedoManager, connection = null) -> void:
- _undo_redo = undo_redo
- _connection = connection
-
-
-const _SHAPE_3D_CLASSES := {
- "box": "BoxShape3D",
- "sphere": "SphereShape3D",
- "capsule": "CapsuleShape3D",
- "cylinder": "CylinderShape3D",
-}
-
-const _SHAPE_2D_CLASSES := {
- "rectangle": "RectangleShape2D",
- "circle": "CircleShape2D",
- "capsule": "CapsuleShape2D",
-}
-
-const _GENERATED_BODY_CLASSES := {
- "static": "StaticBody3D",
- "area": "Area3D",
-}
-const _GENERATE_DIRECT_MAX_PATHS := 16
-const _GENERATE_MAX_PATHS := 1024
-## One deferred request may spend this long across editor frames. The Python
-## handler's timeout is this plus its transport margin (a source-shape test
-## keeps the two together).
-const _GENERATE_DEFERRED_TIMEOUT_MS := 30000
-## Wall-clock budget one editor frame gives the job, the same order as the
-## dispatcher's own tick budget: a 1024-path request neither stalls the editor
-## nor waits out hundreds of near-empty frames.
-const _GENERATE_FRAME_BUDGET_USEC := 4000
-const _GENERATE_COLLIDER_SUFFIX := "Collider"
-const _GENERATE_SCALE_EPSILON := 0.0001
-
-
-## Accept either the short form ("box") or the matching Godot class name
-## ("BoxShape3D") — every other tool in the server takes class names, and
-## resource_get_info(type="Shape3D") surfaces concrete_subclasses by class.
-## Returns "" when neither form matches.
-static func _normalize_shape_type(type_map: Dictionary, requested: String) -> String:
- if type_map.has(requested):
- return requested
- for short_form in type_map:
- if type_map[short_form] == requested:
- return short_form
- return ""
-
-
-## Generates sibling physics bodies and collision shapes for MeshInstance3D
-## nodes. Every path and option is validated before the first mutation, and a
-## deferred request re-validates each mesh again when it is applied, so a
-## scene edited during the window can only fail the whole request, never
-## leave a partially generated batch.
-func generate(params: Dictionary) -> Dictionary:
- var validated := _validate_generate_request(params)
- if validated.has("error"):
- return validated
-
- var request_id: String = params.get("_request_id", "")
- if _connection != null and not request_id.is_empty():
- var job := _generate_job(validated, _undo_redo, _connection, request_id)
- _drive_generate_job(job, _connection)
- return {
- "_deferred": true,
- "_deferred_timeout_ms": _GENERATE_DEFERRED_TIMEOUT_MS,
- }
-
- ## `dispatch_direct()` strips the request id, so batch_execute and unit-test
- ## callers cannot yield across frames. Keep that compatibility path bounded;
- ## larger requests must use the normal MCP command and its deferred reply.
- var path_count: int = validated.paths.size()
- if path_count > _GENERATE_DIRECT_MAX_PATHS:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- (
- "physics_shape_generate received %d paths; inside batch_execute at most %d are "
- + "supported — call resource_manage(op='physics_shape_generate') directly for larger batches"
- ) % [path_count, _GENERATE_DIRECT_MAX_PATHS],
- )
- var job := _generate_job(validated, _undo_redo, null, "")
- while not _generate_step(job, -1):
- pass
- return job.result
-
-
-## Validate the request shape and options first (no scene needed), then the
-## edited scene. Nothing here touches a node.
-static func _validate_generate_request(params: Dictionary) -> Dictionary:
- var requested_shape := String(params.get("shape_type", "box"))
- var shape_type := _normalize_shape_type(_SHAPE_3D_CLASSES, requested_shape)
- if shape_type.is_empty():
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid shape_type '%s'. Valid: %s" % [requested_shape, ", ".join(_SHAPE_3D_CLASSES.keys())]
- )
- var body_type := String(params.get("body_type", "static"))
- if not _GENERATED_BODY_CLASSES.has(body_type):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid body_type '%s'. Valid: %s" % [body_type, ", ".join(_GENERATED_BODY_CLASSES.keys())]
- )
-
- var raw_paths: Variant = params.get("paths", [])
- if not raw_paths is Array:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "paths must be an array of MeshInstance3D scene paths, got %s" % type_string(typeof(raw_paths)),
- )
- if raw_paths.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: paths")
- if raw_paths.size() > _GENERATE_MAX_PATHS:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "paths supports at most %d entries per request, got %d" % [_GENERATE_MAX_PATHS, raw_paths.size()],
- )
- var paths: Array[String] = []
- var seen := {}
- for index in raw_paths.size():
- var raw_path: Variant = raw_paths[index]
- if not raw_path is String:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "paths[%d] must be a MeshInstance3D scene path string, got %s"
- % [index, type_string(typeof(raw_path))],
- )
- if seen.has(raw_path):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "paths lists %s twice (entries %d and %d); each mesh gets one collider"
- % [raw_path, int(seen[raw_path]), index],
- )
- seen[raw_path] = index
- paths.append(raw_path)
-
- var scene_check := McpScenePath.require_edited_scene(params.get("scene_file", ""))
- if scene_check.has("error"):
- return scene_check
- return {
- "data": true,
- "scene_root": scene_check.node,
- "scene_file": params.get("scene_file", ""),
- "shape_type": shape_type,
- "body_type": body_type,
- "paths": paths,
- }
-
-
-## Resolve one mesh and capture everything the apply step must find unchanged.
-static func _plan_generate_mesh(
- mesh_path: String, scene_file: String, scene_root: Node, shape_type: String
-) -> Dictionary:
- var resolved := McpNodeValidator.resolve_or_error(mesh_path, "paths", scene_file)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- if node == scene_root:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "%s is the scene root — a sibling body needs a parent inside the scene" % mesh_path
- )
- if not node is MeshInstance3D:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Node at %s is %s — must be MeshInstance3D" % [mesh_path, node.get_class()]
- )
- var mesh := node as MeshInstance3D
- var parent := mesh.get_parent()
- if parent == null:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "MeshInstance3D at %s has no parent — cannot create a sibling body" % mesh_path
- )
- if mesh.mesh == null:
- return ErrorCodes.make(
- ErrorCodes.RESOURCE_NOT_FOUND,
- "MeshInstance3D at %s has no mesh resource — there are no bounds to fit" % mesh_path
- )
- var collider_name := mesh.name + _GENERATE_COLLIDER_SUFFIX
- var existing := parent.get_node_or_null(NodePath(collider_name))
- if existing != null:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "MeshInstance3D at %s already has a collider sibling at %s — remove or rename it first"
- % [mesh_path, McpScenePath.from_node(existing, scene_root)]
- )
- ## A sibling in parent space inherits the parent chain's scale exactly like
- ## the mesh does, so its box matches the visual; but Godot cannot scale a
- ## sphere, capsule or cylinder non-uniformly (the warning it prints is a
- ## deformed collider). A top-level mesh ignores its parent's transform.
- if not mesh.top_level and shape_type != "box" and parent is Node3D:
- var parent_scale: Vector3 = (parent as Node3D).global_transform.basis.get_scale()
- var spread := absf(parent_scale.x - parent_scale.y)
- spread = maxf(spread, absf(parent_scale.y - parent_scale.z))
- if spread > _GENERATE_SCALE_EPSILON * maxf(1.0, parent_scale.length()):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- (
- "MeshInstance3D at %s sits under a parent chain scaled %s; a %s collider "
- + "cannot be scaled non-uniformly — use shape_type 'box' or reparent the mesh"
- ) % [mesh_path, parent_scale, shape_type]
- )
- var source_transform := mesh.global_transform if mesh.top_level else mesh.transform
- var body_transform := Transform3D(
- Basis(source_transform.basis.get_rotation_quaternion()),
- source_transform.origin,
- )
- var mesh_to_body := body_transform.affine_inverse() * source_transform
- var bounds: AABB = mesh_to_body * mesh.get_aabb()
- if bounds.size.x <= 0.0 or bounds.size.y <= 0.0 or bounds.size.z <= 0.0:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "MeshInstance3D at %s has empty bounds %s — nothing to fit" % [mesh_path, bounds]
- )
- return {"plan": {
- "mesh": mesh,
- "mesh_path": mesh_path,
- "parent": parent,
- "collider_name": collider_name,
- "top_level": mesh.top_level,
- "source_transform": source_transform,
- "body_transform": body_transform,
- "bounds": bounds,
- }}
-
-
-## Why a plan captured earlier no longer describes the scene, or "" when it
-## still does. Checked again right before each body is added, so a deferred
-## request can never apply plan-time state to a mesh that moved, was
-## reparented, lost its mesh, gained a collider, or went away meanwhile.
-static func _plan_stale_reason(plan: Dictionary) -> String:
- var mesh: MeshInstance3D = plan.mesh
- ## A node taken out of the tree and queued to free is still a valid
- ## instance for the rest of the frame; not being in the tree is what
- ## "removed" means here.
- if not is_instance_valid(mesh) or not mesh.is_inside_tree():
- return "was removed"
- var parent: Node = plan.parent
- if not is_instance_valid(parent) or mesh.get_parent() != parent:
- return "was reparented"
- if mesh.top_level != bool(plan.top_level):
- return "changed its top_level setting"
- var source_transform := mesh.global_transform if mesh.top_level else mesh.transform
- if not source_transform.is_equal_approx(plan.source_transform):
- return "moved"
- if mesh.mesh == null:
- return "lost its mesh resource"
- if parent.get_node_or_null(NodePath(str(plan.collider_name))) != null:
- return "gained a collider sibling"
- return ""
-
-
-## Build one detached body/collision pair from a prevalidated mesh plan.
-static func _create_generated_entry(
- plan: Dictionary, shape_type: String, body_type: String
-) -> Dictionary:
- var mesh: MeshInstance3D = plan.mesh
- var body: CollisionObject3D = ClassDB.instantiate(_GENERATED_BODY_CLASSES[body_type])
- body.name = str(plan.collider_name)
- body.top_level = bool(plan.top_level)
- body.transform = plan.body_transform
- var collision := CollisionShape3D.new()
- collision.name = "CollisionShape3D"
- var bounds: AABB = plan.bounds
- collision.position = bounds.get_center()
- var shape: Shape3D = ClassDB.instantiate(_SHAPE_3D_CLASSES[shape_type])
- _apply_shape_size(shape, shape_type, {"aabb": bounds}, true)
- collision.shape = shape
- body.add_child(collision)
- return {
- "mesh": mesh,
- "parent": plan.parent,
- "body": body,
- "collision": collision,
- }
-
-
-## Record all generated nodes as one undo action, optionally executing it.
-static func _commit_generated_action(
- created_nodes: Array[Dictionary],
- scene_root: Node,
- undo_redo: EditorUndoRedoManager,
- execute: bool,
-) -> void:
- undo_redo.create_action("MCP: Generate physics shapes for %d mesh(es)" % created_nodes.size())
- for entry in created_nodes:
- var parent: Node = entry.parent
- var body: CollisionObject3D = entry.body
- var collision: CollisionShape3D = entry.collision
- undo_redo.add_do_method(parent, "add_child", body, true)
- undo_redo.add_do_method(body, "set_owner", scene_root)
- undo_redo.add_do_method(collision, "set_owner", scene_root)
- undo_redo.add_do_reference(body)
- undo_redo.add_undo_method(parent, "remove_child", body)
- undo_redo.commit_action(execute)
-
-
-## Build the public response after every body is present in the edited scene.
-static func _generated_response(
- created_nodes: Array[Dictionary], scene_root: Node, shape_type: String, body_type: String
-) -> Dictionary:
- var created: Array[Dictionary] = []
- for entry in created_nodes:
- created.append({
- "mesh_path": McpScenePath.from_node(entry.mesh, scene_root),
- "body_path": McpScenePath.from_node(entry.body, scene_root),
- "shape_path": McpScenePath.from_node(entry.collision, scene_root),
- "shape_type": shape_type,
- "body_type": body_type,
- })
- return {"data": {"created": created, "undoable": true}}
-
-
-## The whole request as a value the frame loop (or a test) advances with
-## `_generate_step`. Phase "plan" resolves and measures every path; phase
-## "apply" re-validates each plan and adds its body immediately; the last
-## step records one undo action and fills `result`.
-static func _generate_job(
- validated: Dictionary, undo_redo: EditorUndoRedoManager, connection, request_id: String
-) -> Dictionary:
- return {
- "validated": validated,
- "undo_redo": undo_redo,
- "connection": connection,
- "request_id": request_id,
- "phase": "plan",
- "index": 0,
- "plans": [],
- "created": [],
- "result": {},
- "started_ms": Time.get_ticks_msec(),
- }
-
-
-## Advance the job for at most `budget_usec` of wall-clock time (0 does one
-## item, a negative budget runs to completion). Returns true once `job.result`
-## holds the reply, or once the request was abandoned (empty result).
-static func _generate_step(job: Dictionary, budget_usec: int) -> bool:
- if str(job.phase) == "done":
- return true
- var validated: Dictionary = job.validated
- var connection = job.connection
- if connection != null:
- if not _deferred_request_pending(connection, str(job.request_id)):
- ## The dispatcher gave up on this request (timeout, client gone):
- ## nothing may be left behind and nothing can be answered.
- _generate_rollback(job)
- job.phase = "done"
- return true
- if Time.get_ticks_msec() - int(job.started_ms) > _GENERATE_DEFERRED_TIMEOUT_MS:
- return _generate_fail(job, ErrorCodes.make(
- ErrorCodes.DEFERRED_TIMEOUT,
- "physics_shape_generate exceeded its %d ms budget" % _GENERATE_DEFERRED_TIMEOUT_MS,
- ))
- var scene_root: Node = validated.scene_root
- if not is_instance_valid(scene_root) or EditorInterface.get_edited_scene_root() != scene_root:
- return _generate_fail(job, ErrorCodes.make(
- ErrorCodes.EDITED_SCENE_MISMATCH,
- "The edited scene changed while physics shapes were generated",
- ))
- ## The budget is checked before each item after the first, so every step
- ## makes progress and a phase that just finished its last item moves on.
- var frame_start := Time.get_ticks_usec()
- var work := 0
- var paths: Array = validated.paths
- while str(job.phase) == "plan":
- if int(job.index) >= paths.size():
- job.phase = "apply"
- job.index = 0
- break
- if work > 0 and budget_usec >= 0 and Time.get_ticks_usec() - frame_start >= budget_usec:
- return false
- var planned := _plan_generate_mesh(
- str(paths[int(job.index)]), str(validated.scene_file), scene_root, str(validated.shape_type)
- )
- if planned.has("error"):
- return _generate_fail(job, planned)
- job.plans.append(planned.plan)
- job.index = int(job.index) + 1
- work += 1
- var plans: Array = job.plans
- while str(job.phase) == "apply" and int(job.index) < plans.size():
- if work > 0 and budget_usec >= 0 and Time.get_ticks_usec() - frame_start >= budget_usec:
- return false
- var plan: Dictionary = plans[int(job.index)]
- var stale := _plan_stale_reason(plan)
- if not stale.is_empty():
- var code: String = ErrorCodes.NODE_NOT_FOUND if stale == "was removed" else ErrorCodes.EDITED_SCENE_MISMATCH
- return _generate_fail(job, ErrorCodes.make(
- code,
- "MeshInstance3D at %s %s while physics shapes were generated; nothing was changed"
- % [str(plan.mesh_path), stale],
- ))
- var entry := _create_generated_entry(plan, str(validated.shape_type), str(validated.body_type))
- var parent: Node = plan.parent
- parent.add_child(entry.body, true)
- entry.body.set_owner(scene_root)
- entry.collision.set_owner(scene_root)
- job.created.append(entry)
- job.index = int(job.index) + 1
- work += 1
- ## The nodes are already in the scene, so commit with execute=false: one
- ## atomic undo/redo action without replaying every mutation now.
- var created: Array[Dictionary] = []
- created.assign(job.created)
- _commit_generated_action(created, scene_root, job.undo_redo, false)
- job.result = _generated_response(
- created, scene_root, str(validated.shape_type), str(validated.body_type)
- )
- job.phase = "done"
- return true
-
-
-static func _generate_fail(job: Dictionary, error: Dictionary) -> bool:
- _generate_rollback(job)
- job.result = error
- job.phase = "done"
- return true
-
-
-## Remove and free every body this job added; none is in an undo action yet.
-static func _generate_rollback(job: Dictionary) -> void:
- for entry in job.created:
- var body: Node = entry.body
- if not is_instance_valid(body):
- continue
- var parent := body.get_parent()
- if parent != null:
- parent.remove_child(body)
- body.free()
- job.created.clear()
-
-
-## Check that the connection and deferred dispatcher entry are both live.
-static func _deferred_request_pending(connection, request_id: String) -> bool:
- if not is_instance_valid(connection):
- return false
- var dispatcher = connection.dispatcher
- return dispatcher == null or dispatcher.has_pending_deferred_response(request_id)
-
-
-## Drive a deferred job one editor frame at a time and reply when it ends.
-## `send_deferred_response` itself drops a reply whose request is gone.
-static func _drive_generate_job(job: Dictionary, connection) -> void:
- if not is_instance_valid(connection):
- return
- var tree: SceneTree = connection.get_tree()
- if tree == null:
- return
- ## The first yield lets the dispatcher register the deferred request before
- ## any validation error or successful result can be sent.
- await tree.process_frame
- while is_instance_valid(connection) and not _generate_step(job, _GENERATE_FRAME_BUDGET_USEC):
- await tree.process_frame
- if is_instance_valid(connection) and not job.result.is_empty():
- connection.send_deferred_response(str(job.request_id), job.result)
-
-
-func autofit(params: Dictionary) -> Dictionary:
- var node_path: String = params.get("path", "")
- if node_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
-
- var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var scene_root: Node = _resolved.scene_root
-
- var is_3d := node is CollisionShape3D
- var is_2d := node is CollisionShape2D
- if not (is_3d or is_2d):
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Node at %s is %s — must be CollisionShape3D or CollisionShape2D" % [node_path, node.get_class()]
- )
-
- var source_path: String = params.get("source_path", "")
- var source: Node = null
- if source_path.is_empty():
- var search := _find_bounds_visual(node, is_3d, scene_root)
- if search.has("error"):
- return search.error
- source = search.source
- else:
- source = McpScenePath.resolve(source_path, scene_root)
- if source == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND,
- "source_path: %s" % McpScenePath.format_node_error(source_path, scene_root))
-
- var requested_shape: String = params.get("shape_type", "box" if is_3d else "rectangle")
- var type_map := _SHAPE_3D_CLASSES if is_3d else _SHAPE_2D_CLASSES
- var shape_type := _normalize_shape_type(type_map, requested_shape)
- if shape_type.is_empty():
- var valid_pairs: Array[String] = []
- for short_form in type_map:
- valid_pairs.append("%s (%s)" % [short_form, type_map[short_form]])
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid shape_type '%s' for %s. Valid: %s" % [requested_shape, node.get_class(), ", ".join(valid_pairs)]
- )
- var shape_class: String = type_map[shape_type]
-
- # Measure the visual.
- var bounds := _measure_bounds(source, is_3d)
- if bounds.has("error"):
- return bounds.error
-
- # Reuse the existing shape if it already matches the requested class;
- # otherwise create a fresh one of the right type in the same undo action.
- var existing_shape: Shape3D = null
- var existing_shape_2d: Shape2D = null
- if is_3d:
- existing_shape = node.shape
- else:
- existing_shape_2d = node.shape
-
- var needs_new_shape := false
- if is_3d:
- needs_new_shape = existing_shape == null or existing_shape.get_class() != shape_class
- else:
- needs_new_shape = existing_shape_2d == null or existing_shape_2d.get_class() != shape_class
-
- var target_shape: Resource
- if needs_new_shape:
- var instance := ClassDB.instantiate(shape_class)
- if instance == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate %s" % shape_class)
- target_shape = instance
- else:
- target_shape = existing_shape if is_3d else existing_shape_2d
-
- # Compute and apply size.
- var size_info := _apply_shape_size(target_shape, shape_type, bounds, is_3d)
- var old_shape = existing_shape if is_3d else existing_shape_2d
-
- _undo_redo.create_action("MCP: Autofit %s on %s" % [shape_class, node.name])
- if needs_new_shape:
- _undo_redo.add_do_property(node, "shape", target_shape)
- _undo_redo.add_undo_property(node, "shape", old_shape)
- _undo_redo.add_do_reference(target_shape)
- else:
- # Existing shape stays, but its size changes — snapshot size for undo.
- for key in size_info.applied:
- var new_val = target_shape.get(key)
- var old_val = size_info.previous.get(key)
- _undo_redo.add_do_property(target_shape, key, new_val)
- _undo_redo.add_undo_property(target_shape, key, old_val)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "source_path": McpScenePath.from_node(source, scene_root) if source_path.is_empty() else source_path,
- "shape_type": shape_type,
- "shape_class": shape_class,
- "shape_created": needs_new_shape,
- "size": size_info.size_response,
- "undoable": true,
- }
- }
-
-
-## Returns `{source: Node}` on success, `{error: }` on failure.
-## Ambiguous tier-2 matches put candidate scene paths in
-## `error.data.candidates` so callers can pick one explicitly.
-static func _find_bounds_visual(collision_node: Node, is_3d: bool, scene_root: Node) -> Dictionary:
- var parent := collision_node.get_parent()
- if parent == null:
- return {"error": _no_visual_error(is_3d)}
-
- # Tier 1: direct siblings of the collision shape. Uses the broad
- # VisualInstance3D filter for backwards compatibility — callers who put
- # the visual directly next to the collision picked it on purpose.
- var siblings := _measurable_visuals(parent.get_children(), collision_node, is_3d, false)
- if not siblings.is_empty():
- return {"source": siblings[0]}
-
- # Tier 2: parent siblings (uncles). Tighten the filter to
- # GeometryInstance3D so we don't auto-pick a Light3D / DirectionalLight3D
- # as a collision source. Auto-pick only when unambiguous; surface
- # multiple candidates so the agent chooses.
- var grandparent := parent.get_parent()
- if grandparent == null:
- return {"error": _no_visual_error(is_3d)}
- var uncles := _measurable_visuals(grandparent.get_children(), parent, is_3d, true)
- if uncles.size() == 1:
- return {"source": uncles[0]}
- if uncles.size() > 1:
- var paths: Array[String] = []
- for n in uncles:
- paths.append(McpScenePath.from_node(n, scene_root))
- var msg := "Multiple visual candidates near %s — pass source_path explicitly. Candidates: %s" % [
- McpScenePath.from_node(collision_node, scene_root),
- ", ".join(paths),
- ]
- var err := ErrorCodes.make(ErrorCodes.INVALID_PARAMS, msg)
- err["error"]["data"] = {"candidates": paths}
- return {"error": err}
- return {"error": _no_visual_error(is_3d)}
-
-
-## Filter `nodes` for ones we can measure as a collision source. When
-## `strict` is true (tier 2 / uncles) only GeometryInstance3D counts in 3D —
-## avoids picking up lights as accidental sources. 2D filter is already
-## narrow enough that strictness doesn't change behavior.
-static func _measurable_visuals(nodes: Array, exclude: Node, is_3d: bool, strict: bool) -> Array[Node]:
- var out: Array[Node] = []
- for n in nodes:
- if n == exclude:
- continue
- if is_3d:
- if strict:
- if n is GeometryInstance3D:
- out.append(n)
- elif n is VisualInstance3D:
- out.append(n)
- elif n is Sprite2D or n is TextureRect:
- out.append(n)
- return out
-
-
-static func _no_visual_error(is_3d: bool) -> Dictionary:
- var hint := "MeshInstance3D" if is_3d else "Sprite2D"
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "No visual found near collision shape — searched siblings and parent-siblings. Pass source_path explicitly (e.g. a %s)" % hint,
- )
-
-
-## Measure the visual bounds of `source`. Returns {aabb: AABB} for 3D or
-## {rect: Rect2} for 2D on success, or {error: ...} on failure.
-## Bounds are returned in world-ish size (local extents scaled by the source
-## node's own transform scale) so a MeshInstance3D at scale=(2,2,2) gives an
-## 8× volume collider, not a unit collider.
-static func _measure_bounds(source: Node, is_3d: bool) -> Dictionary:
- if is_3d:
- if source is VisualInstance3D:
- var aabb: AABB = (source as VisualInstance3D).get_aabb()
- # get_aabb() is local-space; apply the source's scale so the
- # collider tracks what you actually see in the viewport. Going
- # through the transform keeps a mirrored (negative) scale a
- # positive size, which BoxShape3D would otherwise reject.
- var scale_3d: Vector3 = (source as Node3D).transform.basis.get_scale()
- return {"aabb": Transform3D(Basis.from_scale(scale_3d), Vector3.ZERO) * aabb}
- return {"error": ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Source %s has no measurable 3D bounds (must be VisualInstance3D subclass)" % source.get_class()
- )}
- # 2D
- if source is Sprite2D:
- var s: Sprite2D = source
- var srect: Rect2 = s.get_rect()
- # get_rect() reports the local texture rect and ignores scale.
- srect.position = srect.position * s.scale
- srect.size = srect.size * s.scale
- return {"rect": srect}
- if source is TextureRect:
- var tr: TextureRect = source
- # tr.size is the Control's laid-out size, which is Vector2.ZERO
- # before the first layout pass (e.g. just after the node was created
- # via MCP). Fall back to the texture's own size when that happens,
- # so autofit doesn't silently produce a zero-sized shape.
- var tr_size: Vector2 = tr.size
- if tr_size.is_zero_approx():
- if tr.texture != null:
- tr_size = tr.texture.get_size() * tr.scale
- else:
- return {"error": ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "TextureRect at %s has zero layout size and no texture to fall back to — autofit would produce a zero-sized shape" % source.name
- )}
- return {"rect": Rect2(Vector2.ZERO, tr_size)}
- return {"error": ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Source %s has no measurable 2D bounds (must be Sprite2D or TextureRect)" % source.get_class()
- )}
-
-
-## Apply size to `shape` based on `bounds` and the requested shape_type.
-## Returns {applied: [property_names], previous: {name: old_value}, size_response: dict}.
-static func _apply_shape_size(shape: Resource, shape_type: String, bounds: Dictionary, is_3d: bool) -> Dictionary:
- var applied: Array[String] = []
- var previous := {}
- var size_response := {}
-
- if is_3d:
- var aabb: AABB = bounds.aabb
- var size_v: Vector3 = aabb.size
- match shape_type:
- "box":
- previous["size"] = shape.get("size")
- (shape as BoxShape3D).size = size_v
- applied.append("size")
- size_response = {"x": size_v.x, "y": size_v.y, "z": size_v.z}
- "sphere":
- var r := maxf(maxf(size_v.x, size_v.y), size_v.z) * 0.5
- previous["radius"] = shape.get("radius")
- (shape as SphereShape3D).radius = r
- applied.append("radius")
- size_response = {"radius": r}
- "capsule":
- var cap := shape as CapsuleShape3D
- var r2 := maxf(size_v.x, size_v.z) * 0.5
- var h := size_v.y
- previous["radius"] = cap.radius
- previous["height"] = cap.height
- # CapsuleShape3D enforces height >= 2*radius and silently
- # clamps setters that would violate it. Read back the
- # stored values so the response reflects reality.
- cap.radius = r2
- cap.height = h
- applied.append("radius")
- applied.append("height")
- size_response = {"radius": cap.radius, "height": cap.height}
- "cylinder":
- var cyl := shape as CylinderShape3D
- var r3 := maxf(size_v.x, size_v.z) * 0.5
- var ch := size_v.y
- previous["radius"] = cyl.radius
- previous["height"] = cyl.height
- cyl.radius = r3
- cyl.height = ch
- applied.append("radius")
- applied.append("height")
- size_response = {"radius": cyl.radius, "height": cyl.height}
- else:
- var rect: Rect2 = bounds.rect
- var sz: Vector2 = rect.size
- match shape_type:
- "rectangle":
- previous["size"] = shape.get("size")
- (shape as RectangleShape2D).size = sz
- applied.append("size")
- size_response = {"x": sz.x, "y": sz.y}
- "circle":
- var cr := maxf(sz.x, sz.y) * 0.5
- previous["radius"] = shape.get("radius")
- (shape as CircleShape2D).radius = cr
- applied.append("radius")
- size_response = {"radius": cr}
- "capsule":
- var cap2 := shape as CapsuleShape2D
- var cr2 := sz.x * 0.5
- var ch2 := sz.y
- previous["radius"] = cap2.radius
- previous["height"] = cap2.height
- # CapsuleShape2D has the same height >= 2*radius invariant
- # as its 3D counterpart; read back what Godot actually kept.
- cap2.radius = cr2
- cap2.height = ch2
- applied.append("radius")
- applied.append("height")
- size_response = {"radius": cap2.radius, "height": cap2.height}
-
- return {"applied": applied, "previous": previous, "size_response": size_response}
diff --git a/addons/godot_ai/handlers/physics_shape_handler.gd.uid b/addons/godot_ai/handlers/physics_shape_handler.gd.uid
deleted file mode 100644
index 09acb4a..0000000
--- a/addons/godot_ai/handlers/physics_shape_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cdg8kthqla1cj
diff --git a/addons/godot_ai/handlers/project_handler.gd b/addons/godot_ai/handlers/project_handler.gd
deleted file mode 100644
index 45ed197..0000000
--- a/addons/godot_ai/handlers/project_handler.gd
+++ /dev/null
@@ -1,669 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Handles project settings and filesystem search commands.
-
-const NodeHandler := preload("res://addons/godot_ai/handlers/node_handler.gd")
-const RUN_READY_WAIT_SEC := 3.0
-
-## ProjectSettings keys that decide what the engine EXECUTES at project
-## startup. `McpPathValidator._reject_sensitive_write` already refuses direct
-## writes to `res://project.godot`, `res://override.cfg` and `res://.godot/`
-## for exactly this reason — but `set_project_setting` writes that same
-## manifest through the settings API, so without this list the guard is
-## trivially side-steppable:
-##
-## settings_set key="autoload/Boot" value="*res://../../evil.gd"
-##
-## would land arbitrary code on the next project open, and a `project.godot`
-## diff is easy to miss in review. Blocked keys that DO have a legitimate agent
-## workflow get a narrow validated op of their own instead of an exception
-## carved into this list — `autoload_handler.add_autoload` for `autoload/`,
-## `set_main_scene` below for the main scene — so the denylist stays a flat set
-## of keys with no escape hatches to reason about. The rest are refused
-## outright.
-##
-## Deliberately NARROW. A denylist that over-blocks turns settings_set into a
-## tool agents can't use, so only keys that actually carry code (or a command
-## line) are listed — not their whole section. `application/run/` is an exact
-## entry for `main_scene` precisely because its siblings (`max_fps`,
-## `low_processor_mode`, …) are inert and must stay writable; likewise
-## `application/boot_splash/image` is data, not code, and is NOT blocked.
-##
-## Prefix entries match the key and anything beneath it, and are used only
-## where every key under the prefix is code-bearing. Exact entries match only
-## themselves. Comparison is case-folded — ProjectSettings keys are
-## case-sensitive, but a case variant that Godot would reject is still a
-## clearer error coming from here than from a half-applied save.
-const STARTUP_EXECUTION_KEY_PREFIXES: Array[String] = [
- "autoload/", ## every key under it is a script path
- "editor_plugins/", ## enabled-plugin list; each entry is loaded as code
-]
-const STARTUP_EXECUTION_KEYS_EXACT: Array[String] = [
- "application/run/main_scene", ## the scene the game boots into
- "editor/script/templates_search_path", ## where the editor loads templates from
- "editor/run/main_run_args", ## command line for the run
-]
-
-## ProjectSettings key that names the scene the game boots into.
-const MAIN_SCENE_KEY := "application/run/main_scene"
-
-## Validated routes for blocked keys, appended to the refusal. `settings_get`
-## on a blocked key works, so an agent can read the value it needs to change
-## and then be refused the write — without a pointer it reads that asymmetry as
-## "this workflow is closed" and hands a non-bootable project back to the user
-## (#915). Keyed by the lowercased exact entry above.
-const STARTUP_EXECUTION_KEY_ALTERNATIVES := {
- "application/run/main_scene":
- "Use project_manage(op='set_main_scene', params={'path': 'res://.tscn'}), "
- + "which accepts only an existing PackedScene inside the project.",
-}
-
-
-## Returns "" when `key` may be written via set_project_setting, or a
-## human-readable refusal reason otherwise. Static so it is unit-testable
-## without instancing the handler.
-static func startup_execution_key_refusal(key: String) -> String:
- var lowered := key.strip_edges().to_lower()
- for prefix in STARTUP_EXECUTION_KEY_PREFIXES:
- if lowered.begins_with(prefix):
- if prefix == "autoload/":
- return (
- "Refusing to set '%s' — autoloads run code at project startup. " % key
- + "Use autoload_manage(op='add'), which validates the script path."
- )
- return (
- "Refusing to set '%s' — keys under '%s' are loaded as code " % [key, prefix]
- + "at project startup."
- )
- for exact in STARTUP_EXECUTION_KEYS_EXACT:
- if lowered == exact:
- var message := (
- "Refusing to set '%s' — this key controls what the engine loads " % key
- + "or executes at project startup."
- )
- var alternative := str(STARTUP_EXECUTION_KEY_ALTERNATIVES.get(exact, ""))
- if not alternative.is_empty():
- message += " " + alternative
- return message
- return ""
-
-var _connection: McpConnection
-var _debugger_plugin
-var _editor_log_buffer
-
-
-func _init(connection: McpConnection = null, debugger_plugin = null, editor_log_buffer = null) -> void:
- _connection = connection
- _debugger_plugin = debugger_plugin
- _editor_log_buffer = editor_log_buffer
-
-
-func get_project_setting(params: Dictionary) -> Dictionary:
- var key: String = params.get("key", "")
- if key.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: key")
-
- if not ProjectSettings.has_setting(key):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Setting not found: %s" % key)
-
- var value = ProjectSettings.get_setting(key)
- return {
- "data": {
- "key": key,
- "value": NodeHandler._serialize_value(value),
- "type": type_string(typeof(value)),
- }
- }
-
-
-func set_project_setting(params: Dictionary) -> Dictionary:
- var key: String = params.get("key", "")
- if key.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: key")
-
- if not params.has("value"):
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: value")
-
- ## Refuse the startup-execution surface before touching ProjectSettings —
- ## see STARTUP_EXECUTION_KEY_PREFIXES for why this guard exists here and
- ## not only in McpPathValidator.
- var refusal := startup_execution_key_refusal(key)
- if not refusal.is_empty():
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, refusal)
-
- var value = params.get("value")
- var had_setting := ProjectSettings.has_setting(key)
- var old_value = ProjectSettings.get_setting(key) if had_setting else null
- # JSON has no distinct int type: Godot parses `1920` as float. If the
- # existing setting is TYPE_INT, coerce whole-number floats back to int so
- # we don't silently flip typed-int settings (viewport_width, etc.) to
- # floats on disk. See issue #31.
- if had_setting and typeof(old_value) == TYPE_INT and typeof(value) == TYPE_FLOAT and float(int(value)) == value:
- value = int(value)
- ProjectSettings.set_setting(key, value)
- var err := ProjectSettings.save()
- if err != OK:
- if had_setting:
- ProjectSettings.set_setting(key, old_value)
- else:
- ProjectSettings.clear(key)
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to save project settings (error %d)" % err)
-
- return {
- "data": {
- "key": key,
- "value": NodeHandler._serialize_value(value),
- "old_value": NodeHandler._serialize_value(old_value),
- "type": type_string(typeof(value)),
- "undoable": false,
- "reason": "ProjectSettings changes are saved to disk",
- }
- }
-
-
-## The validated route to `application/run/main_scene`, which
-## `set_project_setting` refuses (STARTUP_EXECUTION_KEYS_EXACT) because a
-## generic setter has no way to tell a scene path from an arbitrary payload.
-## Without it an agent can create every scene, script, node and resource for a
-## new project and still not make it bootable: `project_run(mode="main")` has
-## nothing to launch, and the last step — pure mechanical configuration — has
-## to be finished by hand in the editor (#915).
-##
-## The blocklist's intent survives because the value can only ever name a scene
-## the project already contains: the path is confined under `res://` by
-## `McpPathValidator`, must resolve through `ResourceLoader`, and must load as
-## a `PackedScene`. No new startup-execution surface is opened.
-func set_main_scene(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
- var path_err = McpPathValidator.path_error(path, "path")
- if path_err != null:
- return path_err
-
- if not ResourceLoader.exists(path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Scene not found: %s" % path)
- ## Load rather than trusting `ResourceLoader.exists(path, "PackedScene")`:
- ## the type hint there comes from the loader's guess for the extension, so a
- ## `.tscn` that fails to parse would still pass it. A main scene that cannot
- ## load is a project that cannot boot, so pay for the real load here instead
- ## of surfacing the failure as a mystified `project_run`.
- var resource := ResourceLoader.load(path)
- if resource == null:
- return ErrorCodes.make(
- ErrorCodes.RESOURCE_NOT_FOUND,
- "Failed to load scene (it may be corrupt or have a broken dependency): %s" % path
- )
- if not (resource is PackedScene):
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "path must be a PackedScene, got %s: %s" % [resource.get_class(), path]
- )
-
- var had_setting := ProjectSettings.has_setting(MAIN_SCENE_KEY)
- var old_value = ProjectSettings.get_setting(MAIN_SCENE_KEY) if had_setting else null
- ProjectSettings.set_setting(MAIN_SCENE_KEY, path)
- var err := ProjectSettings.save()
- if err != OK:
- if had_setting:
- ProjectSettings.set_setting(MAIN_SCENE_KEY, old_value)
- else:
- ProjectSettings.clear(MAIN_SCENE_KEY)
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Failed to save project settings while setting the main scene: %s (error %d)"
- % [error_string(err), err]
- )
-
- return {
- "data": {
- "key": MAIN_SCENE_KEY,
- "path": path,
- "old_value": NodeHandler._serialize_value(old_value),
- "undoable": false,
- "reason": "ProjectSettings changes are saved to disk",
- }
- }
-
-
-func run_project(params: Dictionary) -> Dictionary:
- var mode: String = params.get("mode", "main")
- var autosave: bool = params.get("autosave", true)
- # Idempotent: a project that's already running satisfies the caller's intent.
- # Returning INVALID_PARAMS here punished agents that legitimately called run
- # to ensure the project is playing (87+ installs/day hit the matching
- # stop-not-running case in telemetry). Surface state via was_already_running
- # so a caller wanting a *different* scene can detect and stop+restart.
- if EditorInterface.is_playing_scene():
- return _run_project_current_liveness_response(
- _run_project_base_data(
- mode,
- str(params.get("scene", "")),
- autosave,
- true,
- "Project was already running; no action taken"
- )
- )
-
- var validation_error: Variant = null
- if mode == "custom":
- var custom_scene: String = params.get("scene", "")
- if custom_scene.is_empty():
- validation_error = ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: scene (required when mode='custom')")
- else:
- ## play_custom_scene() was the last path-taking op in the plugin
- ## with no containment check; every sibling that accepts a scene
- ## path validates it (scene_handler.open_scene,
- ## node_handler.create_node's scene_path).
- validation_error = McpPathValidator.loadable_error(custom_scene, "scene")
- elif mode != "main" and mode != "current":
- validation_error = ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid mode '%s' — use 'main', 'current', or 'custom'" % mode)
- if validation_error != null:
- return validation_error
-
- # play_*_scene internally triggers try_autosave() → _save_scene_with_preview()
- # which renders a preview thumbnail and calls frame processing. If our
- # WebSocket connection's _process() re-enters during that render, the
- # engine crashes (SIGABRT in _save_scene_with_preview). Pause processing
- # around the play call — same pattern as SceneHandler.save_scene.
- if _connection:
- _connection.pause_processing = true
-
- # try_autosave() reads run/auto_save/save_before_running every call, so
- # toggling it off around the play call suppresses the save without
- # touching the user's persisted preference. Issue #81.
- var autosave_key := "run/auto_save/save_before_running"
- var editor_settings: EditorSettings = null
- if not autosave:
- editor_settings = EditorInterface.get_editor_settings()
- var prior_autosave: bool = true
- var restore_setting := false
- if editor_settings != null and editor_settings.has_setting(autosave_key):
- prior_autosave = bool(editor_settings.get_setting(autosave_key))
- editor_settings.set_setting(autosave_key, false)
- restore_setting = true
-
- if _debugger_plugin != null:
- _debugger_plugin.begin_game_run(_editor_log_cursor(), _game_helper_autoload_expected())
-
- match mode:
- "main":
- EditorInterface.play_main_scene()
- "current":
- EditorInterface.play_current_scene()
- "custom":
- var scene_path: String = params.get("scene", "")
- EditorInterface.play_custom_scene(scene_path)
-
- if restore_setting:
- editor_settings.set_setting(autosave_key, prior_autosave)
-
- if _connection:
- _connection.pause_processing = false
-
- var base_data := _run_project_base_data(
- mode,
- str(params.get("scene", "")),
- autosave,
- false,
- "Play/stop is a runtime action"
- )
- var request_id: String = params.get("_request_id", "")
- if _connection != null and _debugger_plugin != null and not request_id.is_empty():
- _finish_run_project_deferred(request_id, base_data, _connection, _debugger_plugin)
- return McpDispatcher.DEFERRED_RESPONSE
-
- return _run_project_current_liveness_response(base_data)
-
-
-func _editor_log_cursor() -> int:
- return _editor_log_buffer.appended_total() if _editor_log_buffer != null else 0
-
-
-func _game_helper_autoload_expected() -> bool:
- return ProjectSettings.has_setting("autoload/_mcp_game_helper")
-
-
-static func _run_project_base_data(
- mode: String,
- scene: String,
- autosave: bool,
- was_already_running: bool,
- reason: String
-) -> Dictionary:
- return {
- "mode": mode,
- "scene": scene,
- "autosave": autosave,
- "was_already_running": was_already_running,
- "undoable": false,
- "reason": reason,
- }
-
-
-func _run_project_current_liveness_response(base_data: Dictionary) -> Dictionary:
- if _debugger_plugin == null:
- return {"data": base_data}
- var status: Dictionary = _debugger_plugin.get_game_status(-1, RUN_READY_WAIT_SEC)
- ## One-shot read — force a Debugger-tab scan so boot errors that landed
- ## after the last gated scan are in this response (#641).
- var errors_info: Dictionary = _debugger_plugin.recent_editor_errors_since(int(status.get("editor_log_cursor", 0)), true)
- return _run_project_response(base_data, _run_project_liveness_decision(status, errors_info))
-
-
-## `static` is load-bearing (#712, same rationale as the script/filesystem
-## handlers and editor_handler._do_reload_plugin): this coroutine awaits
-## across frames, and a plugin reload frees this RefCounted handler
-## mid-await — resuming an instance coroutine on a freed object errors.
-## `connection` and `debugger_plugin` are parameterized explicitly and
-## re-validated after every await; the response is dropped silently when
-## either died (the server's command timeout surfaces the failure).
-static func _finish_run_project_deferred(
- request_id: String, base_data: Dictionary, connection, debugger_plugin
-) -> void:
- var work := ScriptWork.begin("run_project")
- await _settle_run_project(request_id, base_data, connection, debugger_plugin)
- ScriptWork.finish(work)
-
-
-static func _settle_run_project(
- request_id: String, base_data: Dictionary, connection, debugger_plugin
-) -> void:
- var tree: SceneTree = connection.get_tree()
- while true:
- await tree.process_frame
- if not is_instance_valid(connection) or not is_instance_valid(debugger_plugin):
- return
- var pre_status: Dictionary = debugger_plugin.get_game_status(-1, RUN_READY_WAIT_SEC)
- if (
- not EditorInterface.is_playing_scene()
- and int(pre_status.get("elapsed_msec", 0)) > 100
- and str(pre_status.get("status", "stopped")) == "launching"
- ):
- debugger_plugin.end_game_run()
- var status: Dictionary = debugger_plugin.get_game_status(-1, RUN_READY_WAIT_SEC)
- var errors_info: Dictionary = debugger_plugin.recent_editor_errors_since(int(status.get("editor_log_cursor", 0)))
- var decision := _run_project_liveness_decision(status, errors_info)
- if not bool(decision.get("resolve", false)):
- continue
- ## #641: the loop above polls with gated (cheap) scans; boot parse
- ## errors can land in the Errors tab in the same frames the run goes
- ## live. Re-gather once with a forced scan before replying so the
- ## response reports them instead of leaving them to a later
- ## logs_read. Rebuilding the decision with strictly-more errors can
- ## only keep it resolved (errors never un-resolve a decision).
- errors_info = debugger_plugin.recent_editor_errors_since(int(status.get("editor_log_cursor", 0)), true)
- decision = _run_project_liveness_decision(status, errors_info)
- connection.send_deferred_response(request_id, _run_project_response(base_data, decision))
- return
-
-
-static func _run_project_response(base_data: Dictionary, decision: Dictionary) -> Dictionary:
- var data := base_data.duplicate(true)
- var game_status: Dictionary = decision.get("game_status", {})
- data["game_status"] = game_status
- data["helper_live"] = bool(game_status.get("helper_live", false))
- data["session_active"] = bool(game_status.get("session_active", false))
- if bool(data.get("was_already_running", false)):
- data["reason"] = _run_project_already_running_message(decision)
- else:
- data["reason"] = decision.get("message", data.get("reason", "Play/stop is a runtime action"))
- data["recent_errors"] = decision.get("recent_errors", [])
- data["recent_errors_scope"] = decision.get("recent_errors_scope", "none")
- data["recent_errors_may_predate_run"] = decision.get("recent_errors_may_predate_run", false)
- data["recent_errors_truncated"] = decision.get("recent_errors_truncated", false)
- data.merge(McpDebuggerPlugin.split_errors_by_scope(data["recent_errors"], data["recent_errors_scope"]), true)
- return {"data": data}
-
-
-static func _run_project_already_running_message(decision: Dictionary) -> String:
- var state := str(decision.get("liveness_status", "unknown"))
- match state:
- "live":
- var live_errors: Array = decision.get("recent_errors", [])
- if not live_errors.is_empty() and str(decision.get("recent_errors_scope", "none")) == "run":
- return (
- "Project was already running; the Godot AI game helper is live, but %d editor error%s surfaced during this run (first: %s). Check logs_read(source='editor', include_details=true)."
- % [live_errors.size(), "s" if live_errors.size() != 1 else "", _format_editor_error_summary(live_errors[0])]
- )
- return "Project was already running; the Godot AI game helper is live."
- "not_live":
- var errors: Array = decision.get("recent_errors", [])
- var scope := str(decision.get("recent_errors_scope", "none"))
- if not errors.is_empty() and scope == "run":
- return "Project was already running but failed to load before the Godot AI game helper registered: %s. Check logs_read(source='editor', include_details=true)." % _format_editor_error_summary(errors[0])
- if not errors.is_empty():
- return "Project was already running but is not responding. A recent editor error may be related, but may predate this run: %s. Check logs_read(source='editor', include_details=true)." % _format_editor_error_summary(errors[0])
- return "Project was already running but did not become live before the helper-ready window elapsed. Check logs_read(source='editor', include_details=true) and poll editor_state."
- "break":
- var break_errors: Array = decision.get("recent_errors", [])
- if not break_errors.is_empty() and str(decision.get("recent_errors_scope", "none")) == "run":
- return "Project was already running but the game is parked at a debugger break: %s. Call project_manage(op='stop') to end the run, fix the error, and relaunch." % _format_editor_error_summary(break_errors[0])
- if not break_errors.is_empty():
- return "Project was already running but the game is parked at a debugger break. A recent editor error may be related, but may predate this run: %s. Call project_manage(op='stop') to end the run." % _format_editor_error_summary(break_errors[0])
- return "Project was already running but the game is parked at a debugger break. Call project_manage(op='stop') to end the run; the break reason is in the editor's Debugger panel."
- "no_helper":
- return "Project was already running, but no _mcp_game_helper autoload is expected. Headless or custom-main-loop projects cannot confirm helper liveness."
- "launching":
- return "Project was already running and is still waiting for the Godot AI game helper to register. Poll editor_state shortly."
- "stopped":
- return "Project was already marked playing by the editor, but no active game liveness run exists."
- _:
- return "Project was already running; current liveness status is %s." % state
-
-
-## Static (with the rest of the deferred-finisher chain) so the #712
-## load-bearing-static coroutines above can call it after their owner
-## handler was freed. Uses no instance state.
-static func _run_project_liveness_decision(status: Dictionary, errors_info: Dictionary = {}) -> Dictionary:
- var enriched_status := McpDebuggerPlugin.with_liveness_flags(status)
- var state := str(status.get("status", "stopped"))
- var recent_errors: Array = errors_info.get("errors", [])
- var errors_scope := str(errors_info.get("scope", "none"))
- var truncated := bool(errors_info.get("truncated", false))
- ## Clear errors that predate this run's window — they don't belong in
- ## a successful launch response and only add noise. They remain
- ## reachable via logs_read(source='editor') and the retained buffer so
- ## failed-run debugging is unaffected.
- ## #635 tradeoff: a genuine in-run error whose Errors-tab row carries
- ## an empty or byte-identical time text can be misclassified as
- ## retained_recent and will be dropped here. Still reachable via
- ## logs_read.
- if state == "live" and errors_scope == "retained_recent":
- recent_errors = []
- errors_scope = "none"
- var correlated_error := not recent_errors.is_empty() and errors_scope == "run"
- var elapsed_msec := int(status.get("elapsed_msec", 0))
- var ready_wait_msec := int(status.get("ready_wait_msec", int(RUN_READY_WAIT_SEC * 1000.0)))
- var decision := {
- "resolve": false,
- "game_status": enriched_status,
- "liveness_status": state,
- "recent_errors": recent_errors,
- "recent_errors_scope": errors_scope,
- "recent_errors_may_predate_run": errors_scope == "retained_recent",
- "recent_errors_truncated": truncated,
- "message": "",
- }
- if state == "live":
- decision["resolve"] = true
- if correlated_error:
- ## #641: "live" only means the helper autoload registered — scripts
- ## can still have failed to parse or load during boot (a broken
- ## node script does not stop the game from running). Surface those
- ## errors in the success message so agents don't read a clean
- ## launch into a run that silently lost scripts.
- decision["message"] = (
- "Game launched and the Godot AI game helper is live, but %d editor error%s surfaced during startup (first: %s) — likely a script that failed to parse or load. Check logs_read(source='editor', include_details=true)."
- % [recent_errors.size(), "s" if recent_errors.size() != 1 else "", _format_editor_error_summary(recent_errors[0])]
- )
- if truncated:
- decision["message"] += " Editor logs since this run may be truncated; showing retained errors."
- else:
- decision["message"] = "Game launched and the Godot AI game helper is live."
- elif state == "break":
- ## #645: the game process is parked in a remote-debugger break. A
- ## boot-time parse error (GDScriptLanguage::debug_break_parse) produces
- ## no Errors-tab row, no Logger entry, and no game-log line — the
- ## synthesized break record is the only evidence, and it lands a
- ## moment after the break signal (stack frames arrive async). Wait for
- ## it (correlated_error) before resolving; the ready window is the
- ## fallback if synthesis never lands.
- var break_info: Dictionary = status.get("break", {})
- var break_reason := str(break_info.get("reason", ""))
- if bool(break_info.get("pre_live", true)):
- decision["resolve"] = correlated_error or elapsed_msec >= ready_wait_msec
- var summary := break_reason
- if correlated_error:
- summary = _format_editor_error_summary(recent_errors[0])
- if summary.is_empty():
- summary = "script parse/load error (reason not captured)"
- decision["message"] = "Game hit a script error during startup and is frozen at a debugger break before the Godot AI game helper registered: %s. The run cannot continue; call project_manage(op='stop'), fix the error, and relaunch. Check logs_read(source='editor', include_details=true)." % summary
- else:
- var reason_suffix := (": %s" % break_reason) if not break_reason.is_empty() else ""
- decision["resolve"] = true
- decision["message"] = "Game is paused at a debugger break%s. Resume it from the editor's Debugger panel or call project_manage(op='stop')." % reason_suffix
- elif correlated_error:
- decision["resolve"] = true
- decision["liveness_status"] = "not_live"
- decision["message"] = "Game launched but failed to load before the Godot AI game helper registered: %s. Check logs_read(source='editor', include_details=true)." % _format_editor_error_summary(recent_errors[0])
- if truncated:
- decision["message"] += " Editor logs since this run may be truncated; showing retained errors."
- elif state == "not_live":
- decision["resolve"] = true
- if not recent_errors.is_empty():
- decision["message"] = "Game launched but is not responding. A recent editor error may be related, but may predate this run: %s. Check logs_read(source='editor', include_details=true)." % _format_editor_error_summary(recent_errors[0])
- else:
- decision["message"] = "Game launched but did not become live before the helper-ready window elapsed. It may still be booting or may have failed silently; check logs_read(source='editor', include_details=true) and poll editor_state."
- elif state == "no_helper":
- decision["resolve"] = true
- decision["message"] = "Game launched, but no _mcp_game_helper autoload is expected. Headless or custom-main-loop projects cannot confirm helper liveness; use editor_state and viewport/editor tools where applicable."
- elif state == "stopped":
- decision["resolve"] = true
- decision["message"] = "The play session stopped, or no active game liveness run exists, before the Godot AI game helper became live."
- elif state == "launching" and elapsed_msec >= ready_wait_msec:
- decision["resolve"] = true
- decision["message"] = "Game launched but is not yet live after %.1fs; it may still be booting. Poll editor_state and check logs_read(source='editor', include_details=true)." % (float(elapsed_msec) / 1000.0)
- return decision
-
-
-static func _format_editor_error_summary(entry: Dictionary) -> String:
- return McpSurfacedErrorTracker.format_editor_error_summary(entry)
-
-
-func stop_project(params: Dictionary) -> Dictionary:
- # Idempotent: a project that's already stopped satisfies the caller's intent.
- # Returning INVALID_PARAMS here was the largest single source of fleet-wide
- # project_manage failures (87 installs/24h). was_running=false lets callers
- # distinguish a no-op stop from one that actually halted a running session.
- if not EditorInterface.is_playing_scene():
- return {
- "data": {
- "stopped": true,
- "was_running": false,
- "undoable": false,
- "reason": "Project was not running; no action taken",
- }
- }
-
- if _debugger_plugin != null:
- _debugger_plugin.end_game_run()
- EditorInterface.stop_playing_scene()
-
- # stop_playing_scene() is async — is_playing_scene() only flips to false on
- # the next frame, and readiness_changed follows in _process. Defer the
- # response so we can reply with authoritative readiness instead of letting
- # the server poll for the event. Issue #29.
- var request_id: String = params.get("_request_id", "")
- if _connection != null and not request_id.is_empty():
- _finish_stop_project_deferred(request_id, _connection)
- return McpDispatcher.DEFERRED_RESPONSE
-
- # Fallback for contexts without a connection (e.g. batch_execute via
- # dispatch_direct, or unit tests that instantiate the handler with null).
- return {
- "data": {
- "stopped": true,
- "was_running": true,
- "undoable": false,
- "reason": "Play/stop is a runtime action",
- }
- }
-
-
-# Wait two frames so Godot can tick the stop-play state change. After this
-# is_playing_scene() reflects truth and get_readiness() is authoritative.
-# If the plugin tears down (_exit_tree frees _connection) during the await,
-# is_instance_valid() goes false and we drop the response silently — the
-# server's 5s request timeout will surface the failure to the caller.
-# `static` is load-bearing (#712): see _finish_run_project_deferred.
-static func _finish_stop_project_deferred(request_id: String, connection) -> void:
- var work := ScriptWork.begin("stop_project")
- await _settle_stop_project(request_id, connection)
- ScriptWork.finish(work)
-
-
-static func _settle_stop_project(request_id: String, connection) -> void:
- var tree: SceneTree = connection.get_tree()
- await tree.process_frame
- await tree.process_frame
- if not is_instance_valid(connection):
- return
- connection.send_deferred_response(request_id, {
- "data": {
- "stopped": true,
- "was_running": true,
- "undoable": false,
- "reason": "Play/stop is a runtime action",
- "readiness_after": McpConnection.get_readiness(),
- }
- })
-
-
-func search_filesystem(params: Dictionary) -> Dictionary:
- var name_filter: String = params.get("name", "")
- var type_filter: String = params.get("type", "")
- var path_filter: String = params.get("path", "")
-
- if name_filter.is_empty() and type_filter.is_empty() and path_filter.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "At least one filter (name, type, path) is required")
-
- var efs := EditorInterface.get_resource_filesystem()
- if efs == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_UNAVAILABLE,
- "EditorFileSystem not available", false)
-
- var results: Array[Dictionary] = []
- _scan_directory(efs.get_filesystem(), name_filter, type_filter, path_filter, results)
- return {"data": {"files": results, "count": results.size()}}
-
-
-func _scan_directory(dir: EditorFileSystemDirectory, name_filter: String, type_filter: String, path_filter: String, out: Array[Dictionary]) -> void:
- for i in dir.get_file_count():
- var file_path := dir.get_file_path(i)
- var file_type := dir.get_file_type(i)
-
- var matches := true
-
- if not name_filter.is_empty():
- if file_path.get_file().to_lower().find(name_filter.to_lower()) == -1:
- matches = false
-
- if matches and not type_filter.is_empty():
- if file_type != type_filter:
- matches = false
-
- if matches and not path_filter.is_empty():
- if file_path.to_lower().find(path_filter.to_lower()) == -1:
- matches = false
-
- if matches:
- out.append({
- "path": file_path,
- "type": file_type,
- })
-
- for i in dir.get_subdir_count():
- _scan_directory(dir.get_subdir(i), name_filter, type_filter, path_filter, out)
diff --git a/addons/godot_ai/handlers/project_handler.gd.uid b/addons/godot_ai/handlers/project_handler.gd.uid
deleted file mode 100644
index ec2b5d7..0000000
--- a/addons/godot_ai/handlers/project_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://brf8u32hvha68
diff --git a/addons/godot_ai/handlers/resource_handler.gd b/addons/godot_ai/handlers/resource_handler.gd
deleted file mode 100644
index c43a6d3..0000000
--- a/addons/godot_ai/handlers/resource_handler.gd
+++ /dev/null
@@ -1,591 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-const ClassIntrospection := preload("res://addons/godot_ai/utils/class_introspection.gd")
-
-## Handles resource search, inspection, and assignment to nodes.
-
-const NodeHandler := preload("res://addons/godot_ai/handlers/node_handler.gd")
-
-var _undo_redo: EditorUndoRedoManager
-var _connection: McpConnection
-
-
-func _init(undo_redo: EditorUndoRedoManager, connection: McpConnection = null) -> void:
- _undo_redo = undo_redo
- _connection = connection
-
-
-func search_resources(params: Dictionary) -> Dictionary:
- var type_filter: String = params.get("type", "")
- var path_filter: String = params.get("path", "")
-
- if type_filter.is_empty() and path_filter.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "At least one filter (type, path) is required")
-
- var efs := EditorInterface.get_resource_filesystem()
- if efs == null:
- return ErrorCodes.make_not_ready(
- ErrorCodes.SUB_EDITOR_UNAVAILABLE,
- "EditorFileSystem not available", false)
-
- var results: Array[Dictionary] = []
- _scan_resources(efs.get_filesystem(), type_filter, path_filter, results)
- return {"data": {"resources": results, "count": results.size()}}
-
-
-func _scan_resources(dir: EditorFileSystemDirectory, type_filter: String, path_filter: String, out: Array[Dictionary]) -> void:
- for i in dir.get_file_count():
- var file_path := dir.get_file_path(i)
- var file_type := dir.get_file_type(i)
-
- var matches := true
-
- if not type_filter.is_empty():
- # Check if the file type matches or is a subclass of the requested type
- if file_type != type_filter and not ClassDB.is_parent_class(file_type, type_filter):
- matches = false
-
- if matches and not path_filter.is_empty():
- if file_path.to_lower().find(path_filter.to_lower()) == -1:
- matches = false
-
- if matches:
- out.append({
- "path": file_path,
- "type": file_type,
- })
-
- for i in dir.get_subdir_count():
- _scan_resources(dir.get_subdir(i), type_filter, path_filter, out)
-
-
-func load_resource(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
-
- if path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
-
- var path_err = McpPathValidator.loadable_error(path, "path")
- if path_err != null:
- return path_err
-
- if not ResourceLoader.exists(path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s" % path)
-
- var res: Resource = load(path)
- if res == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to load resource: %s" % path)
-
- var properties: Array[Dictionary] = []
- for prop in res.get_property_list():
- var usage: int = prop.get("usage", 0)
- if not (usage & PROPERTY_USAGE_EDITOR):
- continue
- var value = res.get(prop.name)
- if value == null and prop.type != TYPE_NIL:
- continue
- properties.append({
- "name": prop.name,
- "type": type_string(prop.type),
- "value": NodeHandler._serialize_value(value),
- })
-
- return {
- "data": {
- "path": path,
- "type": res.get_class(),
- "properties": properties,
- "property_count": properties.size(),
- }
- }
-
-
-func assign_resource(params: Dictionary) -> Dictionary:
- var node_path: String = params.get("path", "")
- var property: String = params.get("property", "")
- var resource_path: String = params.get("resource_path", "")
-
- if node_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
-
- if property.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: property")
-
- if resource_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: resource_path")
-
- var rpath_err = McpPathValidator.loadable_error(resource_path, "resource_path")
- if rpath_err != null:
- return rpath_err
-
- var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var _scene_root: Node = _resolved.scene_root
-
- # Verify property exists
- var found := false
- for prop in node.get_property_list():
- if prop.name == property:
- found = true
- break
- if not found:
- return ErrorCodes.make(ErrorCodes.PROPERTY_NOT_ON_CLASS, McpPropertyErrors.build_message(node, property))
-
- if not ResourceLoader.exists(resource_path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s" % resource_path)
-
- var res: Resource = load(resource_path)
- if res == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to load resource: %s" % resource_path)
-
- var old_value = node.get(property)
-
- _undo_redo.create_action("MCP: Assign %s to %s.%s" % [resource_path.get_file(), node.name, property])
- _undo_redo.add_do_property(node, property, res)
- _undo_redo.add_undo_property(node, property, old_value)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "property": property,
- "resource_path": resource_path,
- "resource_type": res.get_class(),
- "undoable": true,
- }
- }
-
-
-## Instantiate a built-in Resource subclass, optionally apply `properties`,
-## and either assign it to a node slot (undoable) or save it to a .tres file
-## (not undoable — mirrors material_create). Exactly one home is required;
-## a resource with no home would be GC'd after the handler returns.
-func create_resource(params: Dictionary) -> Dictionary:
- var type_str: String = params.get("type", "")
- if type_str.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: type")
-
- var properties: Dictionary = params.get("properties", {})
- var node_path: String = params.get("path", "")
- var property: String = params.get("property", "")
- var resource_path: String = params.get("resource_path", "")
- var overwrite: bool = params.get("overwrite", false)
-
- var home_err := McpResourceIO.validate_home(params)
- if home_err != null:
- return home_err
- var has_file_target := not resource_path.is_empty()
-
- var made := _instantiate_resource(type_str)
- if made is Dictionary:
- return made
- var res: Resource = made
-
- if not properties.is_empty():
- var apply_err := _apply_resource_properties(res, properties)
- if apply_err != null:
- return apply_err
-
- if has_file_target:
- return _save_created_resource(res, type_str, resource_path, overwrite, properties.size())
- return _assign_created_resource(res, type_str, node_path, property, properties.size())
-
-
-## Validate that `type_str` names a concrete Resource subclass that we can
-## instantiate. Returns an error dict on failure, or null on success.
-static func _validate_resource_class(type_str: String) -> Variant:
- if not ClassDB.class_exists(type_str):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Unknown resource type: %s" % type_str)
- if ClassDB.is_parent_class(type_str, "Node"):
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "%s is a Node type, not a Resource — use node_create instead" % type_str
- )
- if not ClassDB.is_parent_class(type_str, "Resource"):
- var parent := ClassDB.get_parent_class(type_str)
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "%s is not a Resource type (extends %s)" % [type_str, parent]
- )
- if not ClassDB.can_instantiate(type_str):
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "%s is abstract and cannot be instantiated — use a concrete subclass (e.g. BoxMesh, BoxShape3D, StyleBoxFlat)" % type_str
- )
- return null
-
-
-## Build the "Unknown resource type" error with a steer toward op="scan". A type
-## that reaches here is neither an engine built-in (ClassDB) nor a registered
-## project class (the global script-class registry). In an agent-driven workflow
-## the most common cause is a `class_name` script just made via script_create
-## that isn't registered yet — the global class table only rebuilds on a
-## filesystem scan (normally an editor-focus event). Point the caller at the one
-## cheap call that fixes that, so it doesn't fall back to a full plugin reload.
-## See #614 for the headless scan op.
-static func _unknown_resource_type_error(type_str: String) -> Dictionary:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- (
- "Unknown resource type: %s — not an engine built-in or a registered project class. "
- + "If you just created it with script_create, the global class table is stale until a "
- + "scan: call filesystem_manage(op=\"scan\"), then retry. Otherwise check the spelling."
- ) % type_str
- )
-
-
-## Resolve a resource type name to a fresh instance. Handles engine built-ins
-## (ClassDB) and project `class_name` Resources (the global script-class
-## registry). Returns a Resource on success, or an error dict on failure.
-static func _instantiate_resource(type_str: String) -> Variant:
- if ClassDB.class_exists(type_str):
- var class_err: Variant = _validate_resource_class(type_str)
- if class_err != null:
- return class_err
- var built_in := ClassDB.instantiate(type_str)
- if built_in == null or not (built_in is Resource):
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate %s as a Resource" % type_str)
- return built_in
- for entry in ProjectSettings.get_global_class_list():
- if entry.get("class", "") == type_str:
- var script_path: String = entry.get("path", "")
- var scr: Variant = load(script_path)
- # Reject non-Resource script classes BEFORE constructing them:
- # scr.new() runs _init(), and an @tool class_name extending a
- # non-RefCounted type (e.g. Node) would otherwise build — and leak —
- # an orphan instance this path never frees. get_instance_base_type()
- # resolves to the native base, so multi-level custom Resource
- # hierarchies (B extends A extends Resource) still pass.
- var base_or_err: Variant = _script_base_type_or_error(scr, type_str, script_path)
- if base_or_err is Dictionary:
- return base_or_err
- var base_type: StringName = base_or_err
- if not ClassDB.is_parent_class(base_type, "Resource"):
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "%s is not a Resource type (extends %s)" % [type_str, base_type])
- if not scr.can_instantiate():
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "%s cannot be instantiated in the editor (abstract, or a non-@tool script — add @tool to instantiate it here)" % type_str)
- # Reject scripts whose _init() requires arguments BEFORE scr.new():
- # scr.new() passes no args, so a required-arg _init raises and aborts
- # this handler mid-call, null-cascading into a generic "malformed
- # result" error instead of a clean rejection. get_script_method_list()
- # reports the effective (incl. inherited) _init; required args =
- # args - default_args. Statically detectable only — a _init that runs
- # but throws still falls through to scr.new() and the dispatcher catch.
- for method in scr.get_script_method_list():
- if method.get("name", "") == "_init":
- var required_args: int = (method.get("args", []) as Array).size() - (method.get("default_args", []) as Array).size()
- if required_args > 0:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "%s cannot be instantiated: its _init() requires arguments" % type_str)
- break
- var made: Variant = scr.new()
- if made == null or not (made is Resource):
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate %s as a Resource" % type_str)
- return made
- return _unknown_resource_type_error(type_str)
-
-
-## Maximum nesting depth for the {"__class__": ...} sub-resource shortcut.
-## Caller-supplied dicts recurse through _apply_resource_properties; without a
-## cap a deeply nested payload overflows the GDScript call stack and crashes
-## the editor (#536). 32 is far beyond any legitimate sub-resource chain.
-const MAX_NESTED_RESOURCE_DEPTH := 32
-
-
-## Apply a dict of property values to a freshly-instantiated Resource,
-## reusing NodeHandler's coercion so Vector3/Color/etc. dicts land typed.
-## Returns null on success or an error dict on failure.
-## `depth` is internal recursion bookkeeping for the nested {"__class__": ...}
-## shortcut — external callers use the default of 0.
-static func _apply_resource_properties(res: Resource, properties: Dictionary, depth: int = 0) -> Variant:
- if depth > MAX_NESTED_RESOURCE_DEPTH:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Nested resource properties exceed the maximum depth of %d — flatten the {\"__class__\": ...} nesting or create the deep sub-resources in separate calls" % MAX_NESTED_RESOURCE_DEPTH
- )
- var prop_types := {}
- for prop in res.get_property_list():
- prop_types[prop.name] = prop.get("type", TYPE_NIL)
- for key in properties.keys():
- if not prop_types.has(key):
- var valid: Array[String] = []
- for prop in res.get_property_list():
- if prop.get("usage", 0) & PROPERTY_USAGE_EDITOR:
- valid.append(prop.name)
- valid.sort()
- # Name the script's class_name (e.g. MyTestResource) rather than the
- # native base (Resource) so the hint names the type the agent created,
- # and point at the real MCP verb — resource_manage(op="get_info") now
- # answers for project class_name Resources too.
- var type_label := res.get_class()
- var res_script: Variant = res.get_script()
- if res_script is Script and not String(res_script.get_global_name()).is_empty():
- type_label = String(res_script.get_global_name())
- var err := ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Property '%s' not found on %s. Call resource_manage(op=\"get_info\", params={\"type\": \"%s\"}) to list available properties." % [key, type_label, type_label]
- )
- err["error"]["data"] = {"valid_properties": valid}
- return err
- var target_type: int = prop_types[key]
- if target_type == TYPE_NIL:
- target_type = typeof(res.get(key))
- var v = properties[key]
- if target_type == TYPE_OBJECT and v is String:
- if v == "":
- v = null
- else:
- var vpath_err = McpPathValidator.loadable_error(v, "property '%s'" % key)
- if vpath_err != null:
- return vpath_err
- var loaded := ResourceLoader.load(v)
- if loaded == null:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Resource not found at path '%s' for property '%s'" % [v, key]
- )
- v = loaded
- elif target_type == TYPE_OBJECT and v is Dictionary and v.has("__class__"):
- # Nested shortcut: the same {"__class__": "X", ...} form that
- # node_handler.set_property accepts, now also supported here so
- # resource_create/environment_create callers can populate
- # sub-resource slots (ShaderMaterial.shader, etc.) in one shot.
- var sub_type: String = v.get("__class__", "")
- # Resolve via the shared helper so the nested shortcut accepts both
- # engine built-ins (ClassDB) and project `class_name` Resources,
- # exactly like the top-level resource_create path.
- var sub_made := _instantiate_resource(sub_type)
- if sub_made is Dictionary:
- # Preserve the property-slot context the inline path used to add.
- sub_made["error"]["message"] = "%s (for property '%s')" % [sub_made["error"]["message"], key]
- return sub_made
- var sub_res: Resource = sub_made
- var remaining: Dictionary = (v as Dictionary).duplicate()
- remaining.erase("__class__")
- if not remaining.is_empty():
- var nested_err := _apply_resource_properties(sub_res, remaining, depth + 1)
- if nested_err != null:
- return nested_err
- v = sub_res
- else:
- var slot_value: Variant = res.get(key)
- if target_type == TYPE_ARRAY and slot_value is Array and (slot_value as Array).is_typed():
- ## Typed Array[T] slot (#612): mirror set_property's dispatch —
- ## the generic passthrough would hand an untyped Array to the
- ## typed setter, which drops it silently while we report success.
- var typed_out: Variant = NodeHandler._coerce_typed_array(
- v, slot_value, "Property '%s'" % key
- )
- if typed_out is Dictionary:
- return typed_out
- v = typed_out
- elif (
- target_type == TYPE_DICTIONARY
- and slot_value is Dictionary
- and (slot_value as Dictionary).is_typed()
- ):
- ## Typed Dictionary[K, V] slot (#612 stage 3): success is a
- ## typed duplicate of the slot; the error envelope is untyped.
- var typed_dict_out: Dictionary = NodeHandler._coerce_typed_dictionary(
- v, slot_value, "Property '%s'" % key
- )
- if not typed_dict_out.is_typed():
- return typed_dict_out
- v = typed_dict_out
- else:
- v = NodeHandler._coerce_value(v, target_type)
- ## Mirror set_property's coerce check: wrong-shape dicts (#123) and
- ## non-dict inputs that don't land as the target compound Variant
- ## (#191) both error here instead of writing zero-filled Variants.
- var coerce_err := NodeHandler._check_coerced(v, target_type, "Property '%s'" % key)
- if coerce_err != null:
- return coerce_err
- res.set(key, v)
- return null
-
-
-func _assign_created_resource(res: Resource, type_str: String, node_path: String, property: String, applied_count: int) -> Dictionary:
- var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var _scene_root: Node = _resolved.scene_root
-
- var found := false
- var prop_type: int = TYPE_NIL
- for prop in node.get_property_list():
- if prop.name == property:
- found = true
- prop_type = prop.get("type", TYPE_NIL)
- break
- if not found:
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- McpPropertyErrors.build_message(node, property)
- )
- if prop_type != TYPE_NIL and prop_type != TYPE_OBJECT:
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Property '%s' on %s is not an Object slot (type %s)" % [property, node.get_class(), type_string(prop_type)]
- )
-
- var old_value = node.get(property)
-
- _undo_redo.create_action("MCP: Create %s for %s.%s" % [type_str, node.name, property])
- _undo_redo.add_do_property(node, property, res)
- _undo_redo.add_undo_property(node, property, old_value)
- _undo_redo.add_do_reference(res)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "property": property,
- "type": type_str,
- "resource_class": res.get_class(),
- "properties_applied": applied_count,
- "undoable": true,
- }
- }
-
-
-func _save_created_resource(res: Resource, type_str: String, resource_path: String, overwrite: bool, applied_count: int) -> Dictionary:
- return McpResourceIO.save_to_disk(res, resource_path, overwrite, "Resource", {
- "type": type_str,
- "resource_class": res.get_class(),
- "properties_applied": applied_count,
- }, _connection)
-
-
-## Introspect a Resource class — return its editor-visible properties, parent,
-## whether it's abstract, and (for abstract bases) the list of concrete
-## subclasses that resource_create can instantiate. Read-only.
-func get_resource_info(params: Dictionary) -> Dictionary:
- var type_str: String = params.get("type", "")
- if type_str.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: type")
-
- if not ClassDB.class_exists(type_str):
- # Project class_name Resources aren't in ClassDB; resolve them through the
- # global script-class registry so get_info answers for the same custom
- # types resource_create can make. Read-only — never instantiates.
- var custom_info: Variant = _custom_resource_info(type_str)
- if custom_info != null:
- return custom_info
- return _unknown_resource_type_error(type_str)
- if ClassDB.is_parent_class(type_str, "Node"):
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "%s is a Node type, not a Resource — use node_* tools for node introspection" % type_str
- )
- if not ClassDB.is_parent_class(type_str, "Resource") and type_str != "Resource":
- var parent := ClassDB.get_parent_class(type_str)
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "%s is not a Resource type (extends %s)" % [type_str, parent]
- )
-
- var can_instantiate: bool = ClassDB.can_instantiate(type_str)
- var class_info := ClassIntrospection.build(type_str, {
- "sections": ["properties"],
- "include_inherited": true,
- "include_inheritors": not can_instantiate,
- "limit": 0,
- })
- var data: Dictionary = {
- "type": type_str,
- "parent_class": class_info.parent_class,
- "can_instantiate": can_instantiate,
- "is_abstract": not can_instantiate,
- "properties": class_info.properties,
- "property_count": class_info.property_count,
- }
-
- # For abstract bases (Shape3D, Material, Texture, StyleBox, ...) surface
- # the concrete Resource subclasses an agent could try next.
- if not can_instantiate:
- data["concrete_subclasses"] = class_info.concrete_inheritors
-
- return {"data": data}
-
-
-## Resolve a loaded global-class script to its native base type, or an error if
-## the script failed to load (not a Script) or to compile (empty base type).
-## Shared by the create and get_info custom-Resource paths so both report a
-## compile failure rather than a misleading "is not a Resource type (extends )".
-static func _script_base_type_or_error(scr: Variant, type_str: String, script_path: String) -> Variant:
- if not (scr is Script):
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to load script class %s from %s" % [type_str, script_path])
- var base_type: StringName = scr.get_instance_base_type()
- if String(base_type).is_empty():
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "%s failed to compile or parse (script %s)" % [type_str, script_path])
- return base_type
-
-
-## get_info for a project `class_name` Resource (not in ClassDB). Returns an info
-## dict, an error dict (for a class_name whose native base is not a Resource), or
-## null if `type_str` is not a registered global class. Read-only: resolves
-## properties from the script + its native base WITHOUT instantiating (no _init()).
-static func _custom_resource_info(type_str: String) -> Variant:
- for entry in ProjectSettings.get_global_class_list():
- if entry.get("class", "") != type_str:
- continue
- var script_path: String = entry.get("path", "")
- var scr: Variant = load(script_path)
- var base_or_err: Variant = _script_base_type_or_error(scr, type_str, script_path)
- if base_or_err is Dictionary:
- return base_or_err
- var base_type: StringName = base_or_err
- if not ClassDB.is_parent_class(base_type, "Resource"):
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "%s is not a Resource type (extends %s)" % [type_str, base_type])
- var can_instantiate: bool = scr.can_instantiate()
- # Inherited (native) properties come from the engine base via ClassDB...
- var class_info := ClassIntrospection.build(String(base_type), {
- "sections": ["properties"],
- "include_inherited": true,
- "limit": 0,
- })
- var props: Array = []
- for native_prop in class_info.properties:
- props.append(native_prop)
- # ...and the script's own (and inherited script) exported properties come
- # from the Script itself, so we never construct the resource. A real
- # default isn't available without instantiating, so script props carry an
- # explicit null — keeping one uniform key set across the array (native
- # props carry their real default).
- for raw_prop in scr.get_script_property_list():
- var prop: Dictionary = raw_prop
- var usage := int(prop.get("usage", 0))
- if not (usage & PROPERTY_USAGE_EDITOR):
- continue
- props.append({
- "name": str(prop.get("name", "")),
- "type": type_string(int(prop.get("type", TYPE_NIL))),
- "class_name": str(prop.get("class_name", "")),
- "hint": int(prop.get("hint", PROPERTY_HINT_NONE)),
- "hint_string": str(prop.get("hint_string", "")),
- "usage": usage,
- "default": null,
- })
- props.sort_custom(func(a, b): return a.name < b.name)
- # parent_class is the immediate script parent when there is one (so a
- # multi-level chain B -> A -> Resource reports A), else the native base.
- var parent_name := String(base_type)
- var base_script: Variant = scr.get_base_script()
- if base_script is Script and not String(base_script.get_global_name()).is_empty():
- parent_name = String(base_script.get_global_name())
- return {"data": {
- "type": type_str,
- "parent_class": parent_name,
- "can_instantiate": can_instantiate,
- # is_abstract reflects real abstractness (the @abstract annotation),
- # NOT editor-instantiability — a non-@tool concrete Resource has
- # can_instantiate()==false in-editor but is not abstract.
- "is_abstract": scr.is_abstract(),
- "properties": props,
- "property_count": props.size(),
- }}
- return null
diff --git a/addons/godot_ai/handlers/resource_handler.gd.uid b/addons/godot_ai/handlers/resource_handler.gd.uid
deleted file mode 100644
index d79d3cf..0000000
--- a/addons/godot_ai/handlers/resource_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dwwd0n3c56ir
diff --git a/addons/godot_ai/handlers/scene_handler.gd b/addons/godot_ai/handlers/scene_handler.gd
deleted file mode 100644
index f34f5fc..0000000
--- a/addons/godot_ai/handlers/scene_handler.gd
+++ /dev/null
@@ -1,429 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Handles scene tree reading and node search.
-
-var _connection: McpConnection
-var _save_scene_callable: Callable = Callable()
-var _save_scene_as_callable: Callable = Callable()
-
-
-func _init(connection: McpConnection = null) -> void:
- _connection = connection
-
-
-func get_scene_tree(params: Dictionary) -> Dictionary:
- var max_depth: int = params.get("depth", 10)
- var offset: int = maxi(0, int(params.get("offset", 0)))
- # limit <= 0 means "no limit" (the hierarchy resource reads the whole tree);
- # the scene_get_hierarchy tool passes an explicit positive limit. Paginating
- # here — rather than walking + serializing the full tree and slicing on the
- # Python side — means only the requested window builds node dicts and clean
- # scene paths, and only the window crosses the WebSocket.
- var limit: int = int(params.get("limit", 0))
- var scene_root := EditorInterface.get_edited_scene_root()
- if scene_root == null:
- return {"data": {
- "nodes": [],
- "total_count": 0,
- "offset": offset,
- "limit": limit,
- "has_more": false,
- "message": "No scene open",
- }}
-
- var nodes: Array[Dictionary] = []
- # index_ref[0] is the running DFS index shared across the recursion (Arrays
- # pass by reference in GDScript). The walk still visits every node to get an
- # accurate total_count, but only materializes those inside the window.
- var index_ref: Array[int] = [0]
- # _walk_tree self-seeds the root's path for full reads; pass "" explicitly.
- _walk_tree(scene_root, nodes, 0, max_depth, scene_root, offset, limit, index_ref, "")
- var total: int = index_ref[0]
- return {"data": {
- "nodes": nodes,
- "total_count": total,
- "offset": offset,
- "limit": limit,
- "has_more": limit > 0 and offset + limit < total,
- }}
-
-
-func get_open_scenes(_params: Dictionary) -> Dictionary:
- var scene_paths := EditorInterface.get_open_scenes()
- var scene_root := EditorInterface.get_edited_scene_root()
- var current := scene_root.scene_file_path if scene_root else ""
- return {
- "data": {
- "scenes": scene_paths,
- "current_scene": current,
- "count": scene_paths.size(),
- }
- }
-
-
-func find_nodes(params: Dictionary) -> Dictionary:
- var name_filter: String = params.get("name", "")
- var type_filter: String = params.get("type", "")
- var group_filter: String = params.get("group", "")
-
- if name_filter.is_empty() and type_filter.is_empty() and group_filter.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "At least one filter (name, type, group) is required")
-
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- var results: Array[Dictionary] = []
- _find_recursive(scene_root, scene_root, name_filter, type_filter, group_filter, results)
- return {"data": {"nodes": results, "count": results.size()}}
-
-
-func _find_recursive(node: Node, scene_root: Node, name_filter: String, type_filter: String, group_filter: String, out: Array[Dictionary]) -> void:
- var matches := true
-
- if not name_filter.is_empty():
- if node.name.to_lower().find(name_filter.to_lower()) == -1:
- matches = false
-
- if matches and not type_filter.is_empty():
- if node.get_class() != type_filter:
- matches = false
-
- if matches and not group_filter.is_empty():
- if not node.is_in_group(group_filter):
- matches = false
-
- if matches:
- out.append({
- "name": node.name,
- "type": node.get_class(),
- "path": McpScenePath.from_node(node, scene_root),
- })
-
- for child in node.get_children():
- _find_recursive(child, scene_root, name_filter, type_filter, group_filter, out)
-
-
-## Create a new scene with the given root node type, save to disk, and open it.
-func create_scene(params: Dictionary) -> Dictionary:
- var root_type: String = params.get("root_type", "Node3D")
- var path: String = params.get("path", "")
-
- if path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
-
- var path_err = McpPathValidator.path_error(path, "path", true)
- if path_err != null:
- return path_err
-
- if not path.ends_with(".tscn") and not path.ends_with(".scn"):
- path += ".tscn"
-
- if not ClassDB.class_exists(root_type):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Unknown node type: %s" % root_type)
- if not ClassDB.is_parent_class(root_type, "Node"):
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "%s is not a Node type" % root_type)
-
- # Ensure parent directory exists
- var dir_path := path.get_base_dir()
- if not DirAccess.dir_exists_absolute(dir_path):
- var err := DirAccess.make_dir_recursive_absolute(dir_path)
- if err != OK:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to create directory: %s" % dir_path)
-
- var root: Node = ClassDB.instantiate(root_type)
- if root == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate %s" % root_type)
-
- var root_name: String = params.get("root_name", "")
- if root_name.is_empty():
- root_name = path.get_file().get_basename()
- root.name = root_name
-
- if _connection:
- _connection.pause_processing = true
- var err := _pack_and_save_with_uid(root, path)
- if err == OK:
- EditorInterface.open_scene_from_path(path)
- if _connection:
- _connection.pause_processing = false
-
- if err != OK:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to save scene: %s" % error_string(err))
-
- return {
- "data": {
- "path": path,
- "root_type": root_type,
- "root_name": root_name,
- "undoable": false,
- "reason": "Scene creation involves file system operations",
- }
- }
-
-
-## Pack `root` and save it to `path`, embedding a fresh uid or preserving the
-## one `path` already had — the exact save sequence `create_scene` runs,
-## minus the `pause_processing` guard (the caller owns that, since it also
-## needs to bracket `open_scene_from_path`) and minus opening the scene
-## (switching the editor's active scene isn't safe inside the shared test
-## runner, so tests call this directly instead of going through
-## `create_scene` end-to-end). Frees `root`. Returns `OK`, or the first
-## `Error` encountered.
-func _pack_and_save_with_uid(root: Node, path: String) -> Error:
- var packed := PackedScene.new()
- packed.pack(root)
- root.free()
-
- # Captured BEFORE the save below overwrites the file — see
- # McpResourceIO.ensure_uid's doc comment.
- var prior_uid := ResourceLoader.get_resource_uid(path) if FileAccess.file_exists(path) else ResourceUID.INVALID_ID
-
- var err := ResourceSaver.save(packed, path)
- if err == OK:
- err = McpResourceIO.ensure_uid(path, prior_uid)
- return err
-
-
-## How long open_scene waits for the editor to actually switch to the
-## requested scene before replying switched=false. Tab switches normally land
-## within a few frames; keep this under the dispatcher's 4500 ms deferred
-## default so the coroutine always answers before DEFERRED_TIMEOUT fires.
-const _OPEN_SETTLE_MAX_MSEC := 3000
-
-
-## Open an existing scene by file path.
-func open_scene(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
- var force_reload: bool = params.get("force_reload", false)
- if path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
-
- var path_err = McpPathValidator.loadable_error(path, "path")
- if path_err != null:
- return path_err
-
- if not ResourceLoader.exists(path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Scene not found: %s" % path)
-
- var scene_root := EditorInterface.get_edited_scene_root()
- var current_path := scene_root.scene_file_path if scene_root else ""
- ## Instance id of the root at call time. A completed open OR reload always
- ## replaces the edited-scene root with a NEW instance, so this is the
- ## reliable completion signal — unlike scene_file_path, which is unchanged
- ## across a force_reload of the already-open scene (#633 review).
- var prev_root_id := scene_root.get_instance_id() if scene_root else 0
- var payload := {
- "path": path,
- "force_reload": force_reload,
- "reloaded_from_disk": false,
- "previous_scene_path": current_path,
- "undoable": false,
- "reason": "Scene navigation cannot be undone via editor undo",
- }
-
- if current_path == path and not force_reload:
- ## Already the edited scene — nothing switches, reply immediately.
- payload["switched"] = true
- payload["settle"] = "already_current"
- return {"data": payload}
-
- if force_reload and current_path == path:
- EditorInterface.reload_scene_from_path(path)
- payload["reloaded_from_disk"] = true
- else:
- EditorInterface.open_scene_from_path(path)
-
- ## The tab switch completes asynchronously; replying now lets an immediate
- ## follow-up write land on the PREVIOUS scene (#633 — a scene_save issued
- ## right after open_scene saved the old scene). Defer the reply until the
- ## edited scene actually is `path` AND its root is a fresh instance, so
- ## success means "the editor is now editing the (re)loaded scene".
- var request_id: String = params.get("_request_id", "")
- if _connection != null and not request_id.is_empty():
- _finish_open_scene_deferred(_connection, request_id, path, prev_root_id, payload)
- return McpDispatcher.DEFERRED_RESPONSE
-
- ## Synchronous fallback (batch_execute and unit-test contexts can't await):
- ## preserve the old reply-immediately behavior, flagged as not waited on.
- payload["switched"] = false
- payload["settle"] = "not_waited"
- return {"data": payload}
-
-
-## `static` is load-bearing (same reason as FilesystemHandler's deferred scan
-## finish): the coroutine must outlive this RefCounted handler, which can be
-## freed mid-await by an editor_reload_plugin. Parameterise everything;
-## reference no instance state.
-static func _finish_open_scene_deferred(
- connection: McpConnection,
- request_id: String,
- path: String,
- prev_root_id: int,
- payload: Dictionary,
-) -> void:
- var work := ScriptWork.begin("open_scene")
- await _settle_open_scene(connection, request_id, path, prev_root_id, payload)
- ScriptWork.finish(work)
-
-
-static func _settle_open_scene(
- connection: McpConnection, request_id: String, path: String,
- prev_root_id: int, payload: Dictionary,
-) -> void:
- if not is_instance_valid(connection):
- return
- var tree := connection.get_tree()
- if tree == null:
- return
- # Hand back a frame so _dispatch() registers this request as deferred
- # before the coroutine can push a reply.
- await tree.process_frame
- var deadline_ms := Time.get_ticks_msec() + _OPEN_SETTLE_MAX_MSEC
- while Time.get_ticks_msec() < deadline_ms:
- var root := EditorInterface.get_edited_scene_root()
- # Require BOTH the target path AND a fresh root instance: a
- # force_reload keeps scene_file_path == path across the reload, so the
- # instance swap is what proves the (re)load actually completed rather
- # than the coroutine settling on the stale pre-reload root.
- if root != null and root.scene_file_path == path and root.get_instance_id() != prev_root_id:
- if not is_instance_valid(connection):
- return
- payload["switched"] = true
- payload["settle"] = "settled"
- connection.send_deferred_response(request_id, {"data": payload})
- return
- await tree.process_frame
- if not is_instance_valid(connection):
- return
- payload["switched"] = false
- payload["settle"] = "timeout"
- connection.send_deferred_response(request_id, {"data": payload})
-
-
-## Save the currently edited scene.
-## Pauses WebSocket processing during save to prevent re-entrant _process()
-## calls during EditorNode::_save_scene_with_preview's thumbnail render.
-func save_scene(_params: Dictionary) -> Dictionary:
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- var path := scene_root.scene_file_path
- if path.is_empty():
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Current scene has never been saved; call scene_manage(op='save_as') with a res://... path ending in .tscn or .scn."
- )
-
- if _connection:
- _connection.pause_processing = true
- var err := _save_current_scene()
- if _connection:
- _connection.pause_processing = false
-
- if err != OK:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to save scene: %s" % error_string(err))
-
- return {
- "data": {
- "path": path,
- "undoable": false,
- "reason": "File save cannot be undone via editor undo",
- }
- }
-
-
-## Save the currently edited scene to a new file path.
-func save_scene_as(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
- if path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
-
- var path_err = McpPathValidator.path_error(path, "path", true)
- if path_err != null:
- return path_err
-
- if not path.ends_with(".tscn") and not path.ends_with(".scn"):
- path += ".tscn"
-
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- # Ensure parent directory exists
- var dir_path := path.get_base_dir()
- if not DirAccess.dir_exists_absolute(dir_path):
- var err := DirAccess.make_dir_recursive_absolute(dir_path)
- if err != OK:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to create directory: %s" % dir_path)
-
- if _connection:
- _connection.pause_processing = true
- _save_current_scene_as(path)
- if _connection:
- _connection.pause_processing = false
-
- return {
- "data": {
- "path": path,
- "undoable": false,
- "reason": "File save cannot be undone via editor undo",
- }
- }
-
-
-func _save_current_scene() -> int:
- if _save_scene_callable.is_valid():
- return int(_save_scene_callable.call())
- return EditorInterface.save_scene()
-
-
-func _save_current_scene_as(path: String) -> void:
- if _save_scene_as_callable.is_valid():
- _save_scene_as_callable.call(path)
- return
- EditorInterface.save_scene_as(path)
-
-
-func _walk_tree(node: Node, out: Array[Dictionary], depth: int, max_depth: int, scene_root: Node, offset: int, limit: int, index_ref: Array[int], node_path: String) -> void:
- if depth > max_depth:
- return
- var idx: int = index_ref[0]
- index_ref[0] = idx + 1
- # Materialize only nodes inside the [offset, offset+limit) window. Outside
- # it we still recurse (to count total_count) but skip the per-node dict.
- #
- # Path build strategy depends on the read shape (identical output either way):
- # * A whole-tree read (offset == 0 and limit <= 0 — the resource-style read
- # backing godot://scene/hierarchy) threads the parent's clean path down the
- # DFS: each node's path is one O(1) concat reusing the descent, instead of
- # McpScenePath.from_node's two native walks back up (is_ancestor_of +
- # get_path_to). Benchmarked ~1.8x faster on a ~1.5k-node tree, up to ~5x on
- # deep chains.
- # * Any windowed read (limit > 0, or an offset > 0 skip) keeps from_node for
- # just the emitted nodes: threading would concatenate a path for every node
- # visited for total_count, which benchmarks ~20% slower for a small window.
- #
- # `node_path` is self-seeded at the scene root below, so a caller cannot leave
- # a full read unseeded (it has no default — pass "" for windowed reads).
- var incremental := limit <= 0 and offset == 0
- if incremental and node == scene_root:
- node_path = "/" + String(scene_root.name)
- var in_window := idx >= offset and (limit <= 0 or idx < offset + limit)
- if in_window:
- out.append({
- "name": node.name,
- "type": node.get_class(),
- "path": node_path if incremental else McpScenePath.from_node(node, scene_root),
- "children_count": node.get_child_count(),
- })
- for child in node.get_children():
- var child_path := (node_path + "/" + String(child.name)) if incremental else ""
- _walk_tree(child, out, depth + 1, max_depth, scene_root, offset, limit, index_ref, child_path)
diff --git a/addons/godot_ai/handlers/scene_handler.gd.uid b/addons/godot_ai/handlers/scene_handler.gd.uid
deleted file mode 100644
index af3711f..0000000
--- a/addons/godot_ai/handlers/scene_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://7ms40gm6t2r4
diff --git a/addons/godot_ai/handlers/script_handler.gd b/addons/godot_ai/handlers/script_handler.gd
deleted file mode 100644
index 050b243..0000000
--- a/addons/godot_ai/handlers/script_handler.gd
+++ /dev/null
@@ -1,572 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-const DiagnosticsCapture := preload("res://addons/godot_ai/utils/diagnostics_capture.gd")
-const ValidationLogger := preload("res://addons/godot_ai/runtime/validation_logger.gd")
-
-## Handles script creation, reading, attaching, detaching, and symbol inspection.
-
-var _undo_redo: EditorUndoRedoManager
-var _connection: McpConnection
-
-# The bounded import-settle window and the deferred completion coroutine
-# live on McpResourceIO since #714 — write_file's fresh-`.gd` path shares
-# them, so create_script and write_file can't drift apart again (#261).
-
-func _init(undo_redo: EditorUndoRedoManager, connection: McpConnection = null) -> void:
- _undo_redo = undo_redo
- _connection = connection
-
-
-func create_script(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
- var content: String = params.get("content", "")
-
- var path_err = McpPathValidator.path_error(path, "path", true)
- if path_err != null:
- return path_err
-
- if not path.ends_with(".gd"):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Path must end with .gd")
-
- var existed_before := FileAccess.file_exists(path)
-
- # Shared write path (#714): parent mkdir + write/flush + explicit error
- # check live on McpResourceIO so write_file can't drift from this again.
- var write_failure: Variant = McpResourceIO.write_text_to_disk(path, content)
- if write_failure != null:
- return write_failure
-
- var data := {
- "path": path,
- "size": content.length(),
- "committed": true,
- "import_settled": existed_before,
- "import_settle": "already_known" if existed_before else "not_waited",
- "undoable": false,
- "reason": "File system operations cannot be undone via editor undo",
- }
- _attach_gdscript_diagnostics(data, path, content)
-
- # A freshly-declared `class_name` is NOT in the global class table until a
- # filesystem scan runs — update_file() below registers the file with the
- # resource pipeline but not the class registry (see the scan() comment).
- # Surface that precisely (only when the class isn't already registered) so a
- # headless caller knows to follow up with filesystem_manage(op="scan")
- # instead of hitting a confusing "Unknown type" / "Unknown resource type" on
- # the very next call. We don't scan here — a scan() per create is the exact
- # SIGABRT race documented below; the explicit op is single-flight.
- # Skip the hint when the script failed to parse: a scan won't register a
- # class from a broken script, so pointing at op="scan" would steer the caller
- # away from the real fix (the parse error already attached above).
- var declared_class := _extract_class_name(content)
- if (
- not declared_class.is_empty()
- and not _script_has_error_diagnostics(data)
- and not _class_name_registered(declared_class)
- ):
- data["class_name"] = declared_class
- data["class_registration"] = "scan_required"
- data["class_registration_hint"] = (
- "New class_name '%s' isn't in the global class table yet. " % declared_class
- + "Call filesystem_manage(op=\"scan\") if it won't resolve on the next "
- + "call (e.g. resource_manage op=\"create\", or used as a type in another "
- + "script). The editor also registers it on its next filesystem scan or "
- + "when its window regains focus."
- )
-
- # An overwrite can target a script that is already loaded (attached to a
- # node, preloaded, open in the script editor). Registering the bytes with
- # the editor leaves that live GDScript on the old source (#937), so the very
- # next call would run stale code after a "successful" write. Refresh it
- # BEFORE registering the file: a GUI editor loads the script inside
- # update_file() (see _refresh_loaded_gdscript for the ordering contract).
- if existed_before:
- _refresh_loaded_gdscript(data, path, content)
-
- _register_written_file(path)
-
- # `.gd.uid` is the sidecar Godot generates on scan; list both so the caller
- # can rm the full set in one go.
- McpResourceIO.attach_cleanup_hint(data, existed_before, [path, path + ".uid"])
-
- # scan() is async — ResourceLoader.exists(path) returns false until Godot's
- # filesystem pipeline finishes. If we reply now, an immediate attach_script
- # races and 404s (#261). Defer the response until the resource is visible
- # (or a bounded timeout elapses). For freshly-created files we wait; on
- # overwrite the resource was already known to ResourceLoader, so reply now.
- var request_id: String = params.get("_request_id", "")
- if not existed_before and _connection != null and not request_id.is_empty():
- McpResourceIO.finish_text_write_deferred(_connection, request_id, path, data)
- return McpDispatcher.DEFERRED_RESPONSE
-
- # Synchronous fallback: batch_execute (no request_id) and unit-test contexts
- # (no connection) get the immediate reply that the previous behaviour gave.
- return {"data": data}
-
-
-## Extract the `class_name` a script declares, or "" if none. A cheap line scan
-## (no full parse) for create_script's "scan_required" hint. Stops at the first
-## space/tab or comma so all three valid forms yield just the name:
-## `class_name Foo`, `class_name Foo extends Bar`, and the icon form
-## `class_name Foo, "res://icon.svg"`.
-static func _extract_class_name(content: String) -> String:
- for raw_line in content.split("\n"):
- var line := raw_line.strip_edges()
- if line.begins_with("class_name "):
- var rest := line.substr(11).strip_edges()
- var cut := rest.length()
- for i in rest.length():
- var ch := rest[i]
- if ch == " " or ch == "\t" or ch == ",":
- cut = i
- break
- return rest.substr(0, cut)
- return ""
-
-
-## True if create_script's diagnostics captured a parse error for this script.
-## Used to suppress the "scan_required" hint when the class can't register
-## anyway — see create_script.
-static func _script_has_error_diagnostics(data: Dictionary) -> bool:
- for diag in data.get("diagnostics", []):
- if diag is Dictionary and diag.get("level", "") == "error":
- return true
- return false
-
-
-## True if `cn` is already usable as a type — an engine built-in (ClassDB) or an
-## already-registered project global class. A brand-new class_name returns false
-## until a filesystem scan registers it.
-static func _class_name_registered(cn: String) -> bool:
- if ClassDB.class_exists(cn):
- return true
- for entry in ProjectSettings.get_global_class_list():
- if entry.get("class", "") == cn:
- return true
- return false
-
-
-func read_script(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
-
- var path_err = McpPathValidator.path_error(path, "path")
- if path_err != null:
- return path_err
-
- if not FileAccess.file_exists(path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "File not found: %s" % path)
-
- var file := FileAccess.open(path, FileAccess.READ)
- if file == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to open file: %s" % path)
-
- var content := file.get_as_text()
- file.close()
-
- return {
- "data": {
- "path": path,
- "content": content,
- "size": content.length(),
- "line_count": content.count("\n") + (1 if not content.is_empty() else 0),
- }
- }
-
-
-## Instance (not static) despite using no instance state: tests stub
-## `_capture_gdscript_load_diagnostics` via subclass override, and static
-## calls bind lexically — see test_script.gd. filesystem_handler shares
-## this by instantiating a bare ScriptHandler (#714).
-func _attach_gdscript_diagnostics(data: Dictionary, path: String, content: String) -> void:
- var validation := _validate_gdscript_source(content)
- var diagnostics: Array = []
- var diagnostics_detail := "none"
- var diagnostics_status := "checked"
-
- if not validation.get("ok", true):
- var capture := _capture_gdscript_load_diagnostics(path)
- diagnostics = capture.get("diagnostics", [])
- diagnostics_detail = capture.get("diagnostics_detail", "none")
- diagnostics_status = capture.get("diagnostics_status", "checked")
- if not validation.get("ok", true) and diagnostics.is_empty():
- diagnostics.append(_fallback_gdscript_diagnostic(path, validation.get("error_code", FAILED), content))
- diagnostics_detail = "fallback"
- data["diagnostics"] = diagnostics
- data["diagnostics_detail"] = diagnostics_detail
- data["diagnostics_scope"] = "this_file"
- data["diagnostics_status"] = diagnostics_status
-
-
-## Register just this file with the editor instead of a full recursive
-## scan(). A scan() per write stacks `update_scripts_classes` /
-## `update_script_paths_documentation` WorkerThreadPool tasks under concurrent
-## script creation ("Task ... already exists" / "!tasks.has(p_task)"), which
-## races the global-class registry and can SIGABRT in
-## ScriptServer::remove_global_class_by_path (see dsarno/godot#6).
-## update_file() is the single-file path the rest of the plugin already uses.
-##
-## Call it only after `_refresh_loaded_gdscript` has taken its decision (see
-## the ordering contract there). Instance (not static) so a test can stand in
-## for what a GUI editor does inside update_file() — load or reload the
-## script — and lock that ordering from a headless run.
-func _register_written_file(path: String) -> void:
- var efs := EditorInterface.get_resource_filesystem()
- if efs != null:
- efs.update_file(path)
-
-
-## Bring an already-loaded GDScript back in step with the bytes just written.
-##
-## ResourceLoader caches GDScript by path, and registering the write with
-## EditorFileSystem does not refresh that cache in a headless editor — so after
-## a successful write, a script that a node, a preload(), or the script editor
-## already holds keeps executing the previous source (#937). When the path is
-## cached and the new source parsed, push the source into the live object and
-## reload it in place (keep_state so existing instances survive). Reports
-## `reloaded` plus a `reload_reason` when it did not, so a caller can tell
-## "the file changed" from "the code changed".
-##
-## Ordering contract: this runs BEFORE `_register_written_file`. A GUI editor
-## (not a headless one — EditorNode's cmdline mode skips the step) runs its
-## script-documentation pass synchronously inside update_file(), which
-## ResourceLoader.load()s the script, caching a never-loaded one, and
-## reload_from_file()s a cached one that is not open in the script editor.
-## Deciding after that call reports a false `already_current` for a script
-## nobody held (instead of `not_loaded`) and for one this helper should have
-## refreshed itself. The question is what was loaded before the write, so it
-## is answered first; the editor's own pass then meets the same bytes.
-##
-## Skipped when validation failed: the diagnostics capture above has already
-## reloaded the shared GDScriptCache entry with the broken source, so there is
-## no good code to push; `reload_reason: parse_error` tells the caller the
-## loaded code did NOT change to something runnable.
-static func _refresh_loaded_gdscript(data: Dictionary, path: String, content: String) -> void:
- data["reloaded"] = false
- if _script_has_error_diagnostics(data):
- data["reload_reason"] = "parse_error"
- return
- if not ResourceLoader.has_cached(path):
- data["reload_reason"] = "not_loaded"
- return
- var loaded := ResourceLoader.load(path)
- if not (loaded is GDScript):
- data["reload_reason"] = "not_gdscript"
- return
- var script := loaded as GDScript
- if script.source_code == content:
- data["reloaded"] = true
- data["reload_reason"] = "already_current"
- return
- script.source_code = content
- var err := script.reload(true)
- if err != OK:
- data["reload_reason"] = "reload_failed"
- data["reload_error"] = err
- return
- data["reloaded"] = true
-
-
-static func _validate_gdscript_source(content: String) -> Dictionary:
- var script := GDScript.new()
- script.source_code = content
- ## Keep validation off the live cached resource: assigning resource_path to
- ## this ephemeral Script can collide with loaded instances. reload() still
- ## performs normal GDScript analysis, including static initializer work, so
- ## this check is intentionally scoped to `.gd` writes where the editor would
- ## compile the file on scan anyway.
- var err := script.reload()
- return {
- "ok": err == OK,
- "error_code": err,
- }
-
-
-func _capture_gdscript_load_diagnostics(path: String) -> Dictionary:
- var buffer := McpEditorLogBuffer.new()
- var logger := ValidationLogger.new(buffer)
- var capture := DiagnosticsCapture.capture_this_file(buffer, path, func() -> Dictionary:
- OS.add_logger(logger)
- # ResourceLoader.load() reports parse failure instead of throwing, and
- # a failed GDScript parse does not execute user code; remove immediately
- # after the synchronous load to keep the private capture window tiny.
- ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
- OS.remove_logger(logger)
- return {}
- )
- return capture
-
-
-static func _fallback_gdscript_diagnostic(path: String, error_code: int, content: String) -> Dictionary:
- var line := _fallback_gdscript_error_line(content)
- return {
- "source": "editor",
- "level": "error",
- "text": "GDScript reload failed with error code %d." % error_code,
- "path": path,
- "line": line,
- "function": "GDScript::reload",
- "details": {
- "code": "gdscript_reload_failed",
- "error_code": error_code,
- "fallback_line": true,
- "source": {
- "path": path,
- "line": line,
- },
- },
- }
-
-
-static func _fallback_gdscript_error_line(content: String) -> int:
- var lines := content.split("\n")
- for i in range(lines.size() - 1, -1, -1):
- if not str(lines[i]).strip_edges().is_empty():
- return i + 1
- return 1
-
-
-func patch_script(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
- var old_text: String = params.get("old_text", "")
- var new_text: String = params.get("new_text", "")
- var replace_all: bool = params.get("replace_all", false)
-
- var path_err = McpPathValidator.path_error(path, "path", true)
- if path_err != null:
- return path_err
- if not "old_text" in params:
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: old_text")
- if not "new_text" in params:
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: new_text")
- if not path.ends_with(".gd"):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Path must end with .gd (use filesystem_write_text for other text files)")
- if old_text.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "old_text must not be empty")
-
- var read := FileAccess.open(path, FileAccess.READ)
- if read == null:
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "File not found or unreadable: %s" % path)
- var content := read.get_as_text()
- read.close()
-
- var match_count := content.count(old_text)
- if match_count == 0:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "old_text not found in %s" % path)
- if match_count > 1 and not replace_all:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "old_text matches %d times; pass replace_all=true or provide a more specific snippet" % match_count,
- )
-
- var new_content: String
- var replacements: int
- if replace_all:
- new_content = content.replace(old_text, new_text)
- replacements = match_count
- else:
- var idx := content.find(old_text)
- new_content = content.substr(0, idx) + new_text + content.substr(idx + old_text.length())
- replacements = 1
-
- # Shared write path (#714). No import-settle deferral here: the file
- # already exists, so ResourceLoader knows it and there is no scan to wait
- # for — same rationale as create_script's overwrite arm.
- var write_failure: Variant = McpResourceIO.write_text_to_disk(path, new_content)
- if write_failure != null:
- return write_failure
-
- var data := {
- "path": path,
- "replacements": replacements,
- "size": new_content.length(),
- "old_size": content.length(),
- "undoable": false,
- "reason": "File system operations cannot be undone via editor undo",
- }
- _attach_gdscript_diagnostics(data, path, new_content)
-
- # The file is fresh but any already-loaded GDScript for it is not (#937);
- # make "patch succeeded" mean the loaded code changed, not just the bytes.
- # Decide and refresh before registering the file with the editor — a GUI
- # editor loads the script inside update_file() (see _refresh_loaded_gdscript).
- _refresh_loaded_gdscript(data, path, new_content)
- _register_written_file(path)
-
- return {"data": data}
-
-
-func attach_script(params: Dictionary) -> Dictionary:
- var node_path: String = params.get("path", "")
- var script_path: String = params.get("script_path", "")
-
- if node_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
-
- if script_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: script_path")
-
- var spath_err = McpPathValidator.loadable_error(script_path, "script_path")
- if spath_err != null:
- return spath_err
-
- var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var _scene_root: Node = _resolved.scene_root
-
- if not ResourceLoader.exists(script_path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Script not found: %s" % script_path)
-
- var script: Script = load(script_path)
- if script == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to load script: %s" % script_path)
-
- var old_script: Script = node.get_script()
-
- _undo_redo.create_action("MCP: Attach script to %s" % node.name)
- _undo_redo.add_do_method(node, "set_script", script)
- _undo_redo.add_undo_method(node, "set_script", old_script)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "script_path": script_path,
- "had_previous_script": old_script != null,
- "undoable": true,
- }
- }
-
-
-func detach_script(params: Dictionary) -> Dictionary:
- var node_path: String = params.get("path", "")
-
- if node_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
-
- var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var _scene_root: Node = _resolved.scene_root
-
- var old_script: Script = node.get_script()
- if old_script == null:
- return {"data": {"path": node_path, "had_script": false, "undoable": false, "reason": "No script attached"}}
-
- _undo_redo.create_action("MCP: Detach script from %s" % node.name)
- _undo_redo.add_do_method(node, "set_script", null)
- _undo_redo.add_undo_method(node, "set_script", old_script)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "removed_script": old_script.resource_path if old_script.resource_path else "(inline)",
- "undoable": true,
- }
- }
-
-
-func find_symbols(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
-
- var path_err = McpPathValidator.path_error(path, "path")
- if path_err != null:
- return path_err
-
- if not FileAccess.file_exists(path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "File not found: %s" % path)
-
- var file := FileAccess.open(path, FileAccess.READ)
- if file == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to open file: %s" % path)
-
- var content := file.get_as_text()
- file.close()
-
- var functions: Array[Dictionary] = []
- var signals_list: Array[String] = []
- var exports: Array[Dictionary] = []
- var class_name_str := ""
- var extends_str := ""
-
- var lines := content.split("\n")
- for i in lines.size():
- var line := lines[i].strip_edges()
-
- # class_name — same cut logic as _extract_class_name so the
- # `extends Bar` / icon-form tails don't leak into the symbol name.
- if line.begins_with("class_name "):
- var cn_rest := line.substr(11).strip_edges()
- var cn_cut := cn_rest.length()
- for ci in cn_rest.length():
- var cn_ch := cn_rest[ci]
- if cn_ch == " " or cn_ch == "\t" or cn_ch == ",":
- cn_cut = ci
- break
- class_name_str = cn_rest.substr(0, cn_cut)
-
- # extends
- if line.begins_with("extends "):
- extends_str = line.substr(8).strip_edges()
-
- # signal
- if line.begins_with("signal "):
- var sig_text := line.substr(7).strip_edges()
- # Strip any parameters for the name
- var paren_idx := sig_text.find("(")
- if paren_idx >= 0:
- signals_list.append(sig_text.substr(0, paren_idx).strip_edges())
- else:
- signals_list.append(sig_text)
-
- # func (including `static func` — strip the leading `static ` first)
- var func_line := line.substr(7).strip_edges() if line.begins_with("static func ") else line
- if func_line.begins_with("func "):
- var func_text := func_line.substr(5).strip_edges()
- var paren_idx := func_text.find("(")
- if paren_idx >= 0:
- functions.append({
- "name": func_text.substr(0, paren_idx).strip_edges(),
- "line": i + 1,
- })
-
- # @export
- if line.begins_with("@export"):
- # Next non-empty line should have the var declaration
- # But often export and var are on the same logical flow
- # Try to find "var" on the same line or the next line
- var var_line := line
- if var_line.find("var ") == -1 and i + 1 < lines.size():
- var_line = lines[i + 1].strip_edges()
- var var_idx := var_line.find("var ")
- if var_idx >= 0:
- var rest := var_line.substr(var_idx + 4).strip_edges()
- # Extract variable name (up to : or = or end)
- var end_idx := rest.length()
- for ch_idx in rest.length():
- if rest[ch_idx] == ":" or rest[ch_idx] == "=" or rest[ch_idx] == " ":
- end_idx = ch_idx
- break
- exports.append({
- "name": rest.substr(0, end_idx),
- "line": i + 1,
- })
-
- return {
- "data": {
- "path": path,
- "class_name": class_name_str,
- "extends": extends_str,
- "functions": functions,
- "signals": signals_list,
- "exports": exports,
- "function_count": functions.size(),
- "signal_count": signals_list.size(),
- "export_count": exports.size(),
- }
- }
diff --git a/addons/godot_ai/handlers/script_handler.gd.uid b/addons/godot_ai/handlers/script_handler.gd.uid
deleted file mode 100644
index 6be1ac4..0000000
--- a/addons/godot_ai/handlers/script_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dhub87454jxb3
diff --git a/addons/godot_ai/handlers/signal_handler.gd b/addons/godot_ai/handlers/signal_handler.gd
deleted file mode 100644
index 76656f0..0000000
--- a/addons/godot_ai/handlers/signal_handler.gd
+++ /dev/null
@@ -1,274 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Handles signal listing, connecting, and disconnecting on scene nodes.
-
-var _undo_redo: EditorUndoRedoManager
-
-
-func _init(undo_redo: EditorUndoRedoManager) -> void:
- _undo_redo = undo_redo
-
-
-func list_signals(params: Dictionary) -> Dictionary:
- var path_value: Variant = params.get("path", "")
- var path_type_err = McpParamValidators.require_string("path", path_value)
- if path_type_err != null:
- return path_type_err
- ## String(...) conversion: require_string accepts StringName too, and
- ## a bare typed assignment from StringName would defeat the guard.
- var path: String = String(path_value)
- if path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
-
- var _resolved := McpNodeValidator.resolve_or_error(path, "path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var scene_root: Node = _resolved.scene_root
-
- ## Default: hide editor-internal connections (SceneTreeEditor observers
- ## live on every scene node and would otherwise dominate the response).
- ## Pass include_editor=true to see them. See #213.
- var include_editor: bool = params.get("include_editor", false)
-
- var signals: Array[Dictionary] = []
- for sig in node.get_signal_list():
- var args: Array[Dictionary] = []
- for arg in sig.get("args", []):
- args.append({"name": arg.get("name", ""), "type": type_string(arg.get("type", 0))})
- signals.append({
- "name": sig.get("name", ""),
- "args": args,
- })
-
- var connections: Array[Dictionary] = []
- var editor_connection_count := 0
- for sig in signals:
- for conn in node.get_signal_connection_list(sig.name):
- var callable: Callable = conn.get("callable", Callable())
- var target := callable.get_object()
- if target == null:
- continue # skip connections to freed objects
- if not include_editor and _is_editor_internal_target(target, scene_root):
- editor_connection_count += 1
- continue
- connections.append({
- "signal": sig.name,
- "target": _format_target_path(target, scene_root),
- "method": callable.get_method(),
- })
-
- return {
- "data": {
- "path": McpScenePath.from_node(node, scene_root),
- "signals": signals,
- "signal_count": signals.size(),
- "connections": connections,
- "connection_count": connections.size(),
- "editor_connection_count": editor_connection_count,
- }
- }
-
-
-## A target is "editor-internal" when it's a Node sitting outside the edited
-## scene tree AND not anywhere under a declared autoload — typical case is
-## the SceneTreeEditor dock listening for visibility/script/state changes on
-## every scene node. Connections to autoloads (declared under ``autoload/*``
-## in ProjectSettings) are user-authored even though they live under
-## ``/root/`` rather than under the edited scene root, so the autoload
-## root *and* any descendant of it stay visible. Non-Node targets
-## (anonymous Callables, RefCounted listeners etc.) also stay visible — we
-## can't reliably classify them.
-func _is_editor_internal_target(target: Object, scene_root: Node) -> bool:
- if not (target is Node):
- return false
- var node_target: Node = target
- if node_target == scene_root:
- return false
- if scene_root.is_ancestor_of(node_target):
- return false
- if _is_under_autoload(node_target):
- return false
- return true
-
-
-## True if `node` is a declared autoload root or sits anywhere under one.
-## When the node is in the SceneTree we read its absolute path
-## (``/root//...``) and check the first segment after ``/root/``;
-## this covers connections to deep descendants of editor-instanced
-## autoloads (e.g. ``/root/MyAutoload/Foo/Bar``). When the node isn't in
-## the tree (test fixtures often construct nodes in isolation), we walk
-## the parent chain and match each ancestor's ``name`` against the
-## autoload key as a best-effort fallback.
-static func _is_under_autoload(node: Node) -> bool:
- if node.is_inside_tree():
- var path := str(node.get_path())
- if not path.begins_with("/root/"):
- return false
- var first_segment := path.substr(6).split("/", true, 1)[0]
- return ProjectSettings.has_setting("autoload/" + first_segment)
- var cursor: Node = node
- while cursor != null:
- if ProjectSettings.has_setting("autoload/" + str(cursor.name)):
- return true
- cursor = cursor.get_parent()
- return false
-
-
-## Serialize a connection's target path. Descendants of (or equal to) the
-## edited scene root render as the usual scene-relative form
-## (``/Main/Camera3D``). Non-descendants — autoload subtrees in particular
-## — render as their canonical absolute SceneTree path
-## (``/root/MyAutoload/Child``) instead of a scene-relative path full of
-## ``..`` segments, which agents can't navigate back to. Non-Node targets
-## (anonymous Callables, etc.) fall back to their string representation.
-static func _format_target_path(target: Object, scene_root: Node) -> String:
- if not (target is Node):
- return str(target)
- var node_target: Node = target
- if node_target == scene_root or scene_root.is_ancestor_of(node_target):
- return McpScenePath.from_node(node_target, scene_root)
- if node_target.is_inside_tree():
- return str(node_target.get_path())
- return McpScenePath.from_node(node_target, scene_root)
-
-
-func connect_signal(params: Dictionary) -> Dictionary:
- var resolved := _resolve_signal_params(params)
- if resolved.has("error"):
- return resolved
-
- var source: Node = resolved.source
- var target: Node = resolved.target
- var signal_name: String = resolved.signal_name
- var method: String = resolved.method
- var scene_root: Node = resolved.scene_root
-
- if not source.has_signal(signal_name):
- return ErrorCodes.make(ErrorCodes.PROPERTY_NOT_ON_CLASS, "Signal '%s' not found on %s" % [signal_name, params.path])
-
- if not target.has_method(method):
- return ErrorCodes.make(ErrorCodes.PROPERTY_NOT_ON_CLASS, "Method '%s' not found on %s" % [method, params.target])
-
- var callable := Callable(target, method)
- if source.is_connected(signal_name, callable):
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "Signal '%s' already connected to %s.%s" % [signal_name, params.target, method])
-
- _undo_redo.create_action("MCP: Connect signal %s" % signal_name)
- _undo_redo.add_do_method(source, "connect", signal_name, callable, Object.CONNECT_PERSIST)
- _undo_redo.add_undo_method(source, "disconnect", signal_name, callable)
- _undo_redo.commit_action()
-
- return {"data": _signal_response(source, signal_name, target, method, scene_root)}
-
-
-func disconnect_signal(params: Dictionary) -> Dictionary:
- var resolved := _resolve_signal_params(params)
- if resolved.has("error"):
- return resolved
-
- var source: Node = resolved.source
- var target: Node = resolved.target
- var signal_name: String = resolved.signal_name
- var method: String = resolved.method
- var scene_root: Node = resolved.scene_root
-
- var callable := Callable(target, method)
- if not source.is_connected(signal_name, callable):
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "Signal '%s' is not connected to %s.%s" % [signal_name, params.target, method])
-
- # Capture the connection's current flags so undo restores it exactly as it
- # was, not unconditionally as CONNECT_PERSIST. Hardcoding PERSIST here would
- # silently promote a runtime-only connection into one that serializes on the
- # next save. (The connection still exists at this point — checked above.)
- var reconnect_flags := 0
- for conn in source.get_signal_connection_list(signal_name):
- if conn.get("callable", Callable()) == callable:
- reconnect_flags = int(conn.get("flags", 0))
- break
-
- _undo_redo.create_action("MCP: Disconnect signal %s" % signal_name)
- _undo_redo.add_do_method(source, "disconnect", signal_name, callable)
- _undo_redo.add_undo_method(source, "connect", signal_name, callable, reconnect_flags)
- _undo_redo.commit_action()
-
- return {"data": _signal_response(source, signal_name, target, method, scene_root)}
-
-
-func _resolve_signal_params(params: Dictionary) -> Dictionary:
- for key in ["path", "signal", "target", "method"]:
- ## Type-check before calling .is_empty(): a non-string value (e.g. an
- ## int or dict) has no is_empty() and would crash the handler, which
- ## the dispatcher only reports as an opaque "malformed result" (#210).
- var value = params.get(key, "")
- var type_err = McpParamValidators.require_string(key, value)
- if type_err != null:
- return type_err
- if value.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: %s" % key)
-
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- var source_result := _resolve_node_or_autoload(params.path, scene_root, "Source")
- if source_result.has("error"):
- return source_result
- var source: Node = source_result.node
-
- var target_result := _resolve_node_or_autoload(params.target, scene_root, "Target")
- if target_result.has("error"):
- return target_result
- var target: Node = target_result.node
-
- return {
- "source": source,
- "target": target,
- "signal_name": params.signal,
- "method": params.method,
- "scene_root": scene_root,
- }
-
-
-## Resolve a path to a Node, with three distinct outcomes:
-## 1. Found in the edited scene tree → returns {node}
-## 2. Declared as an autoload AND instantiated at edit time → returns {node}
-## 3. Declared as an autoload but NOT instantiated at edit time → returns
-## INVALID_PARAMS with guidance. Most autoloads are runtime-only, so a
-## silent "not found" hides the real reason the connection can't be made.
-## 4. Not in scene and not a declared autoload → returns INVALID_PARAMS.
-func _resolve_node_or_autoload(path: String, scene_root: Node, role: String) -> Dictionary:
- var node := McpScenePath.resolve(path, scene_root)
- if node != null:
- return {"node": node}
-
- var name := path.trim_prefix("/")
- if ProjectSettings.has_setting("autoload/" + name):
- # Autoload is declared — see if the editor has it instanced.
- var tree := Engine.get_main_loop()
- if tree is SceneTree:
- var live := (tree as SceneTree).root.get_node_or_null(name)
- if live != null:
- return {"node": live}
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "%s '%s' is a declared autoload but isn't instantiated in the editor. " % [role, name] +
- "Most autoloads are runtime-only; edit-time signal connection isn't supported for them. " +
- "Connect it from a script attached to the scene using @onready + connect(), " +
- "or enable editor-instancing for this autoload in Project Settings > Autoload.")
-
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND,
- "%s node not found: %s (not in scene tree or autoloads)" % [role, path])
-
-
-func _signal_response(source: Node, signal_name: String, target: Node, method: String, scene_root: Node) -> Dictionary:
- return {
- "source": McpScenePath.from_node(source, scene_root),
- "signal": signal_name,
- "target": McpScenePath.from_node(target, scene_root),
- "method": method,
- "undoable": true,
- }
diff --git a/addons/godot_ai/handlers/signal_handler.gd.uid b/addons/godot_ai/handlers/signal_handler.gd.uid
deleted file mode 100644
index d95e1d3..0000000
--- a/addons/godot_ai/handlers/signal_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b4n8byjeqeddm
diff --git a/addons/godot_ai/handlers/test_handler.gd b/addons/godot_ai/handlers/test_handler.gd
deleted file mode 100644
index d04f47c..0000000
--- a/addons/godot_ai/handlers/test_handler.gd
+++ /dev/null
@@ -1,373 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-## Discovers and runs McpTestSuite scripts from res://tests/.
-## Exposes run_tests and get_test_results as MCP commands.
-##
-## Live MCP runs service the WebSocket transport between tests
-## (McpConnection.service_transport_during_exclusive_run) so a long suite
-## can no longer starve the server's keepalive, and abort at a per-call
-## ceiling derived from the server-provided time budget. Direct callers,
-## unit-test fixtures, and batch contexts (no request id / no connection)
-## keep the legacy fully-synchronous behavior with no ceiling.
-## See docs/test-run-transport-starvation-plan.md.
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-const CACHE_WARNING := (
- "Preloaded GDScript dependencies may be stale after source edits. "
- + "Restart the editor before treating this run as validation of dependency changes. "
- + "ResourceLoader cache modes do not invalidate GDScript's preload cache."
-)
-
-## Clamp bounds for the server-provided ``timeout_budget_sec`` param. The
-## floor is purely defensive (a malformed or buggy server value must not
-## abort every run instantly); the param is not user-facing.
-const BUDGET_MIN_SEC := 30.0
-const BUDGET_MAX_SEC := 3600.0
-## Conservative default when the server sent no (or an invalid) budget: an
-## old server's own test_run timeout is 120s, and the plugin must abort
-## and reply before that future expires.
-const BUDGET_DEFAULT_SEC := 110.0
-## Abort this long before the server would time the call out, so the
-## partial-results reply beats the server-side timeout.
-const CEILING_MARGIN_SEC := 10.0
-
-var _runner: McpTestRunner
-var _undo_redo: EditorUndoRedoManager
-var _log_buffer: McpLogBuffer
-## Live plugin dispatcher, exposed to suites via ctx so tests can prove the
-## lazy handler registrations (#736) materialize with their real ctor args.
-## Optional third arg keeps old two-arg fixtures working; untyped because
-## the dispatcher constructs this handler (avoids a load-time type cycle).
-var _dispatcher
-## Live connection for exclusive-run transport servicing. Null in unit-test
-## fixtures and batch contexts, which keep the legacy synchronous path.
-var _connection: McpConnection
-
-
-func _init(
- undo_redo: EditorUndoRedoManager,
- log_buffer: McpLogBuffer,
- dispatcher = null,
- connection: McpConnection = null,
-) -> void:
- _runner = McpTestRunner.new()
- _undo_redo = undo_redo
- _log_buffer = log_buffer
- _dispatcher = dispatcher
- _connection = connection
-
-
-func run_tests(params: Dictionary) -> Dictionary:
- var suite_filter: String = params.get("suite", "")
- var test_filter: String = params.get("test_name", "")
- var exclude_test_filter: String = params.get("exclude_test_name", "")
- var verbose: bool = params.get("verbose", false)
-
- var request_id: String = params.get("_request_id", "")
- var live := _connection != null and not request_id.is_empty()
- var service_cb := Callable()
- var deadline_ticks_ms := 0
- var budget_sec := 0.0
- var started_ms := Time.get_ticks_msec()
- var run_state := {}
- if live:
- budget_sec = _validated_budget_sec(params)
- service_cb = Callable(_connection, "service_transport_during_exclusive_run")
- deadline_ticks_ms = started_ms + int((budget_sec - CEILING_MARGIN_SEC) * 1000.0)
-
- ## Clear the previous run's results BEFORE discovery so an abort at any
- ## later point can never expose a stale prior run via get_test_results.
- _runner.clear()
-
- var discovery := _discover_suites(service_cb, deadline_ticks_ms, run_state)
- var discovery_outcome: String = discovery.get("outcome", "")
- if not discovery_outcome.is_empty():
- ## Aborted during discovery: no suite has begun, so there is no
- ## suite teardown to run. Same outcome mapping as the run itself.
- var empty_results: Dictionary = _runner.get_results(verbose)
- if not discovery.errors.is_empty():
- empty_results["load_errors"] = discovery.errors
- return _map_outcome(
- discovery_outcome, "discovery", empty_results, 0, started_ms, budget_sec
- )
-
- var suites: Array = discovery.suites
- ## #882: an explicit `suite` filter that matches nothing must say so, not
- ## return {"total": 0} — a caller cannot otherwise distinguish "suite has
- ## no tests" from "suite was never registered" (truncated discovery).
- ## Checked BEFORE the no-suites branch so an empty discovery still names
- ## the unmatched filter (with suites_available: []).
- var unknown := unknown_suite_error(suite_filter, suites)
- if not unknown.is_empty():
- if not discovery.errors.is_empty():
- unknown["error"]["data"]["load_errors"] = discovery.errors
- return unknown
-
- if suites.is_empty():
- var msg := "No test suites found in res://tests/"
- if not discovery.errors.is_empty():
- msg += " (%d script(s) failed to load: %s)" % [
- discovery.errors.size(),
- ", ".join(discovery.errors),
- ]
- var no_suites := {"error": msg, "total": 0, "load_errors": discovery.errors, "cache_warning": CACHE_WARNING}
- ## Keep the edited_scene annotation on the no-suites error payload too,
- ## so the response contract is consistent across every return path.
- _annotate_edited_scene(no_suites)
- return {"data": no_suites}
-
- var ctx := {
- "undo_redo": _undo_redo,
- "log_buffer": _log_buffer,
- "dispatcher": _dispatcher,
- }
-
- var run: Dictionary = _runner.run_suites_serviced(
- suites, suite_filter, test_filter, ctx, verbose, exclude_test_filter,
- service_cb, deadline_ticks_ms, run_state
- )
- var results: Dictionary = run["results"]
- if not discovery.errors.is_empty():
- results["load_errors"] = discovery.errors
- return _map_outcome(
- run["outcome"], "run", results, run["tests_not_run"], started_ms, budget_sec
- )
-
-
-## Empty when `suite_filter` is empty or names a discovered suite; otherwise
-## the INVALID_PARAMS protocol error run_tests returns (same ErrorCodes.make
-## shape as the paused/timeout aborts — never an error inside a success
-## envelope). Static + pure so the suite can pin it without calling
-## run_tests() end-to-end (which would recursively re-run the whole corpus
-## from inside itself — see test_serviced_test_run.gd).
-static func unknown_suite_error(suite_filter: String, suites: Array) -> Dictionary:
- if suite_filter.is_empty():
- return {}
- var names := PackedStringArray()
- for suite: McpTestSuite in suites:
- names.append(suite.suite_name())
- if names.has(suite_filter):
- return {}
- var err := ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- (
- "No suite named '%s' is registered (%d discovered)."
- + " If it should exist, discovery may be truncated —"
- + " re-import and restart the editor (#882)."
- ) % [suite_filter, names.size()],
- )
- err["error"]["data"] = {
- "suite": suite_filter,
- "suites_available": Array(names),
- "cache_warning": CACHE_WARNING,
- }
- return err
-
-
-## Map a runner/discovery outcome onto the response envelope. Ownership is
-## deliberately here, not in the runner: the runner reports WHAT happened,
-## the handler decides how it goes over the wire (plan D2).
-func _map_outcome(
- outcome: String,
- phase: String,
- results: Dictionary,
- tests_not_run: int,
- started_ms: int,
- budget_sec: float,
-) -> Dictionary:
- results["cache_warning"] = CACHE_WARNING
- var elapsed_ms := Time.get_ticks_msec() - started_ms
- match outcome:
- "completed":
- _annotate_edited_scene(results)
- return {"data": results}
- "transport_lost":
- ## The peer is gone (or flood-closed); the send will fail against
- ## the dead socket regardless, but a sync handler must return an
- ## envelope. Partials stay retrievable via get_test_results after
- ## the plugin reconnects.
- results["aborted"] = "transport_lost"
- results["tests_not_run"] = tests_not_run
- _annotate_edited_scene(results)
- return {"data": results}
- "paused":
- var depth := _connection.pause_depth() if _connection != null else 0
- if _log_buffer != null:
- _log_buffer.log(
- "[error] test run aborted in %s: transport paused at checkpoint (depth %d)"
- % [phase, depth]
- )
- var paused_err := ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- (
- "Test run aborted: the MCP transport was paused at a between-test "
- + "checkpoint (pause depth %d) — a paused transport cannot service "
- + "the WebSocket heartbeat, so continuing would starve the session. "
- + "Partial results: test_manage(op=\"results_get\")."
- ) % depth
- )
- paused_err["error"]["data"] = _abort_data(
- phase, results, tests_not_run, elapsed_ms, budget_sec, {"pause_depth": depth}
- )
- return paused_err
- "timeout":
- var timeout_err := ErrorCodes.make(
- ErrorCodes.TEST_RUN_TIMEOUT,
- (
- "Test run hit its abort ceiling after %.1fs (budget %.0fs, ceiling = "
- + "budget - %.0fs): %d passed, %d failed, %d of the selected tests "
- + "never ran. Narrow the run with suite=/test_name= filters, or fetch "
- + "the partial results with test_manage(op=\"results_get\")."
- ) % [
- elapsed_ms / 1000.0, budget_sec, CEILING_MARGIN_SEC,
- int(results.get("passed", 0)), int(results.get("failed", 0)),
- tests_not_run,
- ]
- )
- timeout_err["error"]["data"] = _abort_data(
- phase, results, tests_not_run, elapsed_ms, budget_sec, {}
- )
- return timeout_err
- ## Unknown outcome is a runner bug — surface it loudly.
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR, "Unknown test run outcome '%s'" % outcome
- )
-
-
-func _abort_data(
- phase: String,
- results: Dictionary,
- tests_not_run: int,
- elapsed_ms: int,
- budget_sec: float,
- extra: Dictionary,
-) -> Dictionary:
- var data := {
- "phase": phase,
- "cache_warning": CACHE_WARNING,
- "elapsed_ms": elapsed_ms,
- "budget_sec": budget_sec,
- "passed": int(results.get("passed", 0)),
- "failed": int(results.get("failed", 0)),
- "skipped": int(results.get("skipped", 0)),
- "total": int(results.get("total", 0)),
- "tests_not_run": tests_not_run,
- }
- data.merge(extra)
- return data
-
-
-## Strict validation of the server-provided per-call budget: numeric,
-## finite, positive, then clamped to [BUDGET_MIN_SEC, BUDGET_MAX_SEC].
-## Everything else (missing, wrong type, NaN/inf, non-positive) falls back
-## to BUDGET_DEFAULT_SEC. typeof() so bool never sneaks through as int.
-func _validated_budget_sec(params: Dictionary) -> float:
- var raw: Variant = params.get("timeout_budget_sec", null)
- var t := typeof(raw)
- if t == TYPE_FLOAT or t == TYPE_INT:
- var v := float(raw)
- if is_finite(v) and v > 0.0:
- return clampf(v, BUDGET_MIN_SEC, BUDGET_MAX_SEC)
- return BUDGET_DEFAULT_SEC
-
-
-## Many suites assume the project's main scene is the edited scene (they read
-## /Main/... nodes directly). Running with another scene open produces a flood
-## of phantom failures that look like real regressions. Surface the edited
-## scene and a warning when it differs from run/main_scene so the failures are
-## attributable at a glance instead of costing a debugging round (#635).
-func _annotate_edited_scene(results: Dictionary) -> void:
- var scene_root := EditorInterface.get_edited_scene_root()
- var edited := scene_root.scene_file_path if scene_root else ""
- results["edited_scene"] = edited
- var main_scene := str(ProjectSettings.get_setting("application/run/main_scene", ""))
- if main_scene.is_empty() or edited == main_scene:
- return
- if int(results.get("failed", 0)) <= 0:
- return
- results["scene_warning"] = (
- "Edited scene is '%s' but the project main scene is '%s'. Many suites "
- % [edited if not edited.is_empty() else "", main_scene]
- + "assume the main scene is open and will report phantom failures "
- + "otherwise. If these failures are unexpected, scene_open('%s') and re-run." % main_scene
- )
-
-
-func get_test_results(params: Dictionary) -> Dictionary:
- var verbose: bool = params.get("verbose", false)
- var results := _runner.get_results(verbose)
- results["cache_warning"] = CACHE_WARNING
- return {"data": results}
-
-
-## Returns {"suites": Array, "errors": Array[String], "outcome": String}.
-## Resilient: a broken script doesn't kill discovery of the rest. A
-## non-empty outcome ("timeout" / "transport_lost" / "paused") means a
-## between-load checkpoint aborted discovery — script loading is itself an
-## atomic phase, and a directory of heavy scripts must neither starve the
-## heartbeat nor escape the run budget.
-func _discover_suites(
- service_cb: Callable = Callable(),
- deadline_ticks_ms: int = 0,
- run_state: Dictionary = {},
-) -> Dictionary:
- var suites := []
- var errors: Array[String] = []
- var dir := DirAccess.open("res://tests")
- if dir == null:
- return {
- "suites": suites,
- "errors": ["DirAccess.open('res://tests') returned null — directory may not exist"],
- "outcome": "",
- }
-
- dir.list_dir_begin()
- var file_name := dir.get_next()
- while not file_name.is_empty():
- if file_name.begins_with("test_") and file_name.ends_with(".gd"):
- var stop := _discovery_checkpoint(service_cb, deadline_ticks_ms, run_state)
- if not stop.is_empty():
- return {"suites": suites, "errors": errors, "outcome": stop}
- var path := "res://tests/" + file_name
- var script = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
- if script == null:
- errors.append("%s (load failed — check for parse errors or duplicate methods)" % file_name)
- elif script.can_instantiate():
- var instance = script.new()
- if instance is McpTestSuite:
- suites.append(instance)
- else:
- errors.append("%s (not a McpTestSuite subclass)" % file_name)
- else:
- ## Name the cause: a fresh reload prints the parse or compile error
- ## the cached load swallowed and returns its code, so a CI log says
- ## more than "abstract or broken".
- var reload_error: Error = script.reload(true)
- var base: Variant = script.get_base_script()
- errors.append(
- "%s (cannot instantiate — abstract or broken; reload=%s, base=%s, valid=%s)"
- % [
- file_name,
- error_string(reload_error),
- str(base.resource_path) if base != null else "none",
- str(script.can_instantiate()),
- ]
- )
- file_name = dir.get_next()
-
- ## Sort by suite name for deterministic order.
- suites.sort_custom(func(a, b) -> bool:
- return a.suite_name() < b.suite_name()
- )
- return {"suites": suites, "errors": errors, "outcome": ""}
-
-
-## Discovery-phase twin of McpTestRunner._checkpoint. Both delegate to the
-## shared McpConnection.exclusive_run_checkpoint so the outcome mapping
-## cannot drift between the discovery and between-test paths.
-func _discovery_checkpoint(
- service_cb: Callable, deadline_ticks_ms: int, run_state: Dictionary
-) -> String:
- return McpConnection.exclusive_run_checkpoint(service_cb, deadline_ticks_ms, run_state)
diff --git a/addons/godot_ai/handlers/test_handler.gd.uid b/addons/godot_ai/handlers/test_handler.gd.uid
deleted file mode 100644
index e56fce1..0000000
--- a/addons/godot_ai/handlers/test_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bfg3c6iinhwmx
diff --git a/addons/godot_ai/handlers/texture_handler.gd b/addons/godot_ai/handlers/texture_handler.gd
deleted file mode 100644
index 5668890..0000000
--- a/addons/godot_ai/handlers/texture_handler.gd
+++ /dev/null
@@ -1,198 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Creates procedural textures — GradientTexture2D (wrapping a Gradient)
-## and NoiseTexture2D (wrapping a FastNoiseLite). Assigns to a node slot
-## (undoable, bundles sub-resources) or saves to a .tres file.
-
-const NodeHandler := preload("res://addons/godot_ai/handlers/node_handler.gd")
-
-var _undo_redo: EditorUndoRedoManager
-var _connection: McpConnection
-
-
-func _init(undo_redo: EditorUndoRedoManager, connection: McpConnection = null) -> void:
- _undo_redo = undo_redo
- _connection = connection
-
-
-const _FILL_MODES := {
- "linear": GradientTexture2D.FILL_LINEAR,
- "radial": GradientTexture2D.FILL_RADIAL,
- "square": GradientTexture2D.FILL_SQUARE,
-}
-
-const _NOISE_TYPES := {
- "simplex": FastNoiseLite.TYPE_SIMPLEX,
- "simplex_smooth": FastNoiseLite.TYPE_SIMPLEX_SMOOTH,
- "perlin": FastNoiseLite.TYPE_PERLIN,
- "cellular": FastNoiseLite.TYPE_CELLULAR,
- "value": FastNoiseLite.TYPE_VALUE,
- "value_cubic": FastNoiseLite.TYPE_VALUE_CUBIC,
-}
-
-
-# ============================================================================
-# gradient_texture_create
-# ============================================================================
-
-func create_gradient_texture(params: Dictionary) -> Dictionary:
- var stops: Array = params.get("stops", [])
- var width: int = params.get("width", 256)
- var height: int = params.get("height", 1)
- var fill: String = params.get("fill", "linear")
-
- if stops.size() < 2:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "gradient_texture_create requires at least 2 stops, got %d" % stops.size()
- )
- if not _FILL_MODES.has(fill):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid fill '%s'. Valid: %s" % [fill, ", ".join(_FILL_MODES.keys())]
- )
-
- var home_err := McpResourceIO.validate_home(params)
- if home_err != null:
- return home_err
-
- var gradient := Gradient.new()
- var offsets := PackedFloat32Array()
- var colors := PackedColorArray()
- for i in range(stops.size()):
- var stop = stops[i]
- if not stop is Dictionary:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "stops[%d] must be a dict with 'offset' and 'color' keys" % i
- )
- if not stop.has("offset") or not stop.has("color"):
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "stops[%d] missing 'offset' or 'color' key" % i
- )
- offsets.append(float(stop["offset"]))
- var color_value = NodeHandler._coerce_value(stop["color"], TYPE_COLOR)
- var color_err := NodeHandler._check_coerced(color_value, TYPE_COLOR, "stops[%d].color" % i)
- if color_err != null:
- return color_err
- colors.append(color_value)
- gradient.offsets = offsets
- gradient.colors = colors
-
- var tex := GradientTexture2D.new()
- tex.gradient = gradient
- tex.width = width
- tex.height = height
- tex.fill = _FILL_MODES[fill]
-
- return _finalize(tex, [gradient], params, "Gradient texture", {
- "texture_class": "GradientTexture2D",
- "gradient_class": "Gradient",
- "stop_count": stops.size(),
- "fill": fill,
- })
-
-
-# ============================================================================
-# noise_texture_create
-# ============================================================================
-
-func create_noise_texture(params: Dictionary) -> Dictionary:
- var noise_type: String = params.get("noise_type", "simplex_smooth")
- var width: int = params.get("width", 512)
- var height: int = params.get("height", 512)
- var frequency: float = params.get("frequency", 0.01)
- var seed_value: int = params.get("seed", 0)
- var fractal_octaves: int = params.get("fractal_octaves", 0) # 0 = leave default
-
- if not _NOISE_TYPES.has(noise_type):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid noise_type '%s'. Valid: %s" % [noise_type, ", ".join(_NOISE_TYPES.keys())]
- )
-
- var home_err := McpResourceIO.validate_home(params)
- if home_err != null:
- return home_err
-
- var noise := FastNoiseLite.new()
- noise.noise_type = _NOISE_TYPES[noise_type]
- noise.frequency = frequency
- noise.seed = seed_value
- if fractal_octaves > 0:
- noise.fractal_octaves = fractal_octaves
-
- var tex := NoiseTexture2D.new()
- tex.noise = noise
- tex.width = width
- tex.height = height
-
- return _finalize(tex, [noise], params, "Noise texture", {
- "texture_class": "NoiseTexture2D",
- "noise_class": "FastNoiseLite",
- "noise_type": noise_type,
- })
-
-
-# ============================================================================
-# shared helpers
-# ============================================================================
-
-func _finalize(tex: Resource, sub_resources: Array, params: Dictionary, label: String, extra: Dictionary) -> Dictionary:
- var node_path: String = params.get("path", "")
- var property: String = params.get("property", "")
- var resource_path: String = params.get("resource_path", "")
- var overwrite: bool = params.get("overwrite", false)
-
- if not resource_path.is_empty():
- return McpResourceIO.save_to_disk(tex, resource_path, overwrite, label, extra, _connection)
- return _assign_texture(tex, sub_resources, node_path, property, label, extra)
-
-
-func _assign_texture(tex: Resource, sub_resources: Array, node_path: String, property: String, label: String, extra: Dictionary) -> Dictionary:
- var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var _scene_root: Node = _resolved.scene_root
-
- var found := false
- var prop_type: int = TYPE_NIL
- for prop in node.get_property_list():
- if prop.name == property:
- found = true
- prop_type = prop.get("type", TYPE_NIL)
- break
- if not found:
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- McpPropertyErrors.build_message(node, property)
- )
- if prop_type != TYPE_NIL and prop_type != TYPE_OBJECT:
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Property '%s' on %s is not an Object slot" % [property, node.get_class()]
- )
-
- var old_value = node.get(property)
-
- _undo_redo.create_action("MCP: Create %s for %s.%s" % [label, node.name, property])
- _undo_redo.add_do_property(node, property, tex)
- _undo_redo.add_undo_property(node, property, old_value)
- _undo_redo.add_do_reference(tex)
- for sub in sub_resources:
- _undo_redo.add_do_reference(sub)
- _undo_redo.commit_action()
-
- var data := {
- "path": node_path,
- "property": property,
- "undoable": true,
- }
- data.merge(extra)
- return {"data": data}
-
diff --git a/addons/godot_ai/handlers/texture_handler.gd.uid b/addons/godot_ai/handlers/texture_handler.gd.uid
deleted file mode 100644
index a0a0f73..0000000
--- a/addons/godot_ai/handlers/texture_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cmloikhre8lhe
diff --git a/addons/godot_ai/handlers/theme_handler.gd b/addons/godot_ai/handlers/theme_handler.gd
deleted file mode 100644
index 519989c..0000000
--- a/addons/godot_ai/handlers/theme_handler.gd
+++ /dev/null
@@ -1,476 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Handles Theme resource authoring: creating, modifying color/constant/font-size/
-## stylebox slots, and applying a theme to a Control subtree.
-##
-## Themes are Godot's equivalent of USS: a Theme holds (class, name) -> value
-## entries (colors, constants, fonts, font_sizes, styleboxes, icons) which
-## cascade down a Control subtree when the theme is assigned at any ancestor.
-## One well-authored theme replaces hundreds of per-node property sets.
-
-const _COLOR_HINT := "expected hex #rrggbb, named color, or {r,g,b,a} dict"
-
-var _undo_redo: EditorUndoRedoManager
-var _connection: McpConnection
-
-
-func _init(undo_redo: EditorUndoRedoManager, connection: McpConnection = null) -> void:
- _undo_redo = undo_redo
- _connection = connection
-
-
-# ============================================================================
-# theme_create
-# ============================================================================
-
-func create_theme(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
- var overwrite: bool = params.get("overwrite", false)
-
- var err := _validate_res_path(path, ".tres", "path", true)
- if err != null:
- return err
-
- # Capture whether the file was already there BEFORE the save so we can
- # report `overwritten` accurately (after save the file always exists).
- var existed_before := FileAccess.file_exists(path)
- if existed_before and not overwrite:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Theme already exists at %s (pass overwrite=true to replace)" % path
- )
-
- # Ensure parent directory exists. make_dir_recursive is idempotent —
- # no need to check dir_exists first (avoids TOCTOU race).
- var dir_path := path.get_base_dir()
- var mkdir_err := DirAccess.make_dir_recursive_absolute(dir_path)
- if mkdir_err != OK and mkdir_err != ERR_ALREADY_EXISTS:
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Failed to create directory: %s (error %d)" % [dir_path, mkdir_err]
- )
-
- var theme := Theme.new()
- var save_err := McpResourceIO.guarded_save(theme, path, _connection)
- if save_err != OK:
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Failed to save theme to %s: %s (error %d)" % [path, error_string(save_err), save_err]
- )
-
- # Make sure the editor's filesystem picks up the new file.
- var efs := EditorInterface.get_resource_filesystem()
- if efs != null:
- efs.update_file(path)
-
- return {
- "data": {
- "path": path,
- "overwritten": existed_before,
- "undoable": false,
- "reason": "File creation is persistent; delete the file manually to revert",
- }
- }
-
-
-# ============================================================================
-# theme_set_color / theme_set_constant / theme_set_font_size
-# ============================================================================
-
-func set_color(params: Dictionary) -> Dictionary:
- return _set_scalar(params, "color", func(theme, name, cls): return theme.get_color(name, cls),
- func(theme, name, cls, val): theme.set_color(name, cls, val),
- func(theme, name, cls): theme.clear_color(name, cls),
- func(theme, name, cls): return theme.has_color(name, cls),
- func(v): return _parse_color(v))
-
-
-# constant / font_size parsers validate before coercing: int("abc")/int({})/int([])
-# all return 0 in GDScript (never null), so a bare `int(v)` would silently store
-# garbage as 0 and report success. Returning null for non-numeric input lets
-# _set_scalar's null guard surface a VALUE_OUT_OF_RANGE error, matching the
-# color path's contract.
-func set_constant(params: Dictionary) -> Dictionary:
- return _set_scalar(params, "constant", func(theme, name, cls): return theme.get_constant(name, cls),
- func(theme, name, cls, val): theme.set_constant(name, cls, int(val)),
- func(theme, name, cls): theme.clear_constant(name, cls),
- func(theme, name, cls): return theme.has_constant(name, cls),
- func(v): return int(v) if (v is int or v is float or (v is String and v.is_valid_int())) else null)
-
-
-func set_font_size(params: Dictionary) -> Dictionary:
- return _set_scalar(params, "font_size", func(theme, name, cls): return theme.get_font_size(name, cls),
- func(theme, name, cls, val): theme.set_font_size(name, cls, int(val)),
- func(theme, name, cls): theme.clear_font_size(name, cls),
- func(theme, name, cls): return theme.has_font_size(name, cls),
- func(v): return int(v) if (v is int or v is float or (v is String and v.is_valid_int())) else null)
-
-
-# Shared implementation for scalar Theme slots (color, constant, font_size).
-# Captures old value, applies new value, saves to disk, registers undo that
-# restores the old value and saves again.
-func _set_scalar(
- params: Dictionary,
- kind: String,
- getter: Callable,
- setter: Callable,
- clearer: Callable,
- has_fn: Callable,
- parser: Callable,
-) -> Dictionary:
- var load_result := _load_theme_from_params(params)
- if load_result.has("error"):
- return load_result
- var theme: Theme = load_result.theme
- var theme_path: String = load_result.path
-
- var class_name_param: String = params.get("class_name", "")
- if class_name_param.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: class_name")
-
- var name: String = params.get("name", "")
- if name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: name")
-
- if not "value" in params:
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: value")
-
- var raw_value = params.get("value")
- if raw_value == null:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid %s value: null (pass a concrete value; use the appropriate clear command to remove a slot)" % kind
- )
- var parsed = parser.call(raw_value)
- if parsed == null:
- ## color slots want a color hint; constant/font_size are integer slots.
- var hint := _COLOR_HINT if kind == "color" else "expected an integer"
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
- "Invalid %s value: %s (%s)" % [kind, raw_value, hint])
-
- var had_before: bool = has_fn.call(theme, name, class_name_param)
- var before_value = getter.call(theme, name, class_name_param) if had_before else null
-
- _undo_redo.create_action("MCP: Theme set %s %s/%s" % [kind, class_name_param, name])
- _undo_redo.add_do_method(self, "_apply_scalar", theme_path, setter, name, class_name_param, parsed)
- if had_before:
- _undo_redo.add_undo_method(self, "_apply_scalar", theme_path, setter, name, class_name_param, before_value)
- else:
- _undo_redo.add_undo_method(self, "_clear_scalar", theme_path, clearer, name, class_name_param)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": theme_path,
- "kind": kind,
- "class_name": class_name_param,
- "name": name,
- "value": _serialize_value(parsed),
- "previous_value": _serialize_value(before_value) if had_before else null,
- "undoable": true,
- }
- }
-
-
-func _apply_scalar(theme_path: String, setter: Callable, name: String, class_name_param: String, value: Variant) -> void:
- var theme: Theme = ResourceLoader.load(theme_path)
- if theme == null:
- push_warning("MCP: Failed to load theme for undo/redo: %s" % theme_path)
- return
- setter.call(theme, name, class_name_param, value)
- McpResourceIO.guarded_save(theme, theme_path, _connection)
-
-
-func _clear_scalar(theme_path: String, clearer: Callable, name: String, class_name_param: String) -> void:
- var theme: Theme = ResourceLoader.load(theme_path)
- if theme == null:
- push_warning("MCP: Failed to load theme for undo/redo: %s" % theme_path)
- return
- clearer.call(theme, name, class_name_param)
- McpResourceIO.guarded_save(theme, theme_path, _connection)
-
-
-# ============================================================================
-# theme_set_stylebox_flat
-# ============================================================================
-
-## Compose a StyleBoxFlat and assign it to a theme slot.
-##
-## Parameters (beyond theme_path / class_name / name):
-## bg_color (Color, "#rrggbb", "#rrggbbaa", or {r,g,b,a})
-## border_color (Color)
-## border {all|top|bottom|left|right: int} — side keys override `all`
-## corners {all|top_left|top_right|bottom_left|bottom_right: int}
-## margins {all|top|bottom|left|right: float}
-## shadow {color, size: int, offset_x: float, offset_y: float}
-## anti_aliasing (bool)
-##
-## Unknown keys inside any nested dict are rejected with INVALID_PARAMS so
-## typos fail loudly instead of silently being ignored.
-func set_stylebox_flat(params: Dictionary) -> Dictionary:
- var load_result := _load_theme_from_params(params)
- if load_result.has("error"):
- return load_result
- var theme: Theme = load_result.theme
- var theme_path: String = load_result.path
-
- var class_name_param: String = params.get("class_name", "")
- if class_name_param.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: class_name")
-
- var name: String = params.get("name", "")
- if name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: name")
-
- var sb := StyleBoxFlat.new()
- if params.has("bg_color"):
- var bg := _parse_color(params.bg_color)
- if bg == null:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid bg_color: %s (%s)" % [str(params.bg_color), _COLOR_HINT])
- sb.bg_color = bg
- if params.has("border_color"):
- var bc := _parse_color(params.border_color)
- if bc == null:
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid border_color: %s (%s)" % [str(params.border_color), _COLOR_HINT])
- sb.border_color = bc
-
- # border: {all, top, bottom, left, right} — int widths
- if params.has("border"):
- var err := _apply_sides(sb, params.border, "border",
- ["top", "bottom", "left", "right"],
- "border_width_",
- TYPE_INT)
- if err != "":
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, err)
-
- # corners: {all, top_left, top_right, bottom_left, bottom_right} — int radii
- if params.has("corners"):
- var err2 := _apply_sides(sb, params.corners, "corners",
- ["top_left", "top_right", "bottom_left", "bottom_right"],
- "corner_radius_",
- TYPE_INT)
- if err2 != "":
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, err2)
-
- # margins: {all, top, bottom, left, right} — float padding
- if params.has("margins"):
- var err3 := _apply_sides(sb, params.margins, "margins",
- ["top", "bottom", "left", "right"],
- "content_margin_",
- TYPE_FLOAT)
- if err3 != "":
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, err3)
-
- # shadow: {color, size, offset_x, offset_y}
- if params.has("shadow"):
- if typeof(params.shadow) != TYPE_DICTIONARY:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "'shadow' must be a dict with color/size/offset_x/offset_y")
- var shadow: Dictionary = params.shadow
- var allowed_shadow_keys := {"color": true, "size": true, "offset_x": true, "offset_y": true}
- for k in shadow.keys():
- if not allowed_shadow_keys.has(k):
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "Unknown key in 'shadow': %s (valid: color, size, offset_x, offset_y)" % k)
- if shadow.has("color"):
- var sc := _parse_color(shadow.color)
- if sc == null:
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
- "Invalid shadow.color: %s (%s)" % [str(shadow.color), _COLOR_HINT])
- sb.shadow_color = sc
- if shadow.has("size"):
- sb.shadow_size = int(shadow.size)
- if shadow.has("offset_x") or shadow.has("offset_y"):
- sb.shadow_offset = Vector2(
- float(shadow.get("offset_x", 0)),
- float(shadow.get("offset_y", 0)),
- )
-
- if params.has("anti_aliasing"):
- sb.anti_aliasing = bool(params.anti_aliasing)
-
- var had_before := theme.has_stylebox(name, class_name_param)
- var before_sb: StyleBox = theme.get_stylebox(name, class_name_param) if had_before else null
-
- _undo_redo.create_action("MCP: Theme set stylebox %s/%s" % [class_name_param, name])
- _undo_redo.add_do_method(self, "_apply_stylebox", theme_path, name, class_name_param, sb)
- if had_before:
- _undo_redo.add_undo_method(self, "_apply_stylebox", theme_path, name, class_name_param, before_sb)
- else:
- _undo_redo.add_undo_method(self, "_clear_stylebox", theme_path, name, class_name_param)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": theme_path,
- "class_name": class_name_param,
- "name": name,
- "stylebox_class": "StyleBoxFlat",
- "bg_color": _serialize_value(sb.bg_color),
- "border": {
- "top": sb.border_width_top,
- "bottom": sb.border_width_bottom,
- "left": sb.border_width_left,
- "right": sb.border_width_right,
- },
- "corners": {
- "top_left": sb.corner_radius_top_left,
- "top_right": sb.corner_radius_top_right,
- "bottom_left": sb.corner_radius_bottom_left,
- "bottom_right": sb.corner_radius_bottom_right,
- },
- "margins": {
- "top": sb.content_margin_top,
- "bottom": sb.content_margin_bottom,
- "left": sb.content_margin_left,
- "right": sb.content_margin_right,
- },
- "undoable": true,
- }
- }
-
-
-## Parse a {all, , , ...} dict and apply it to StyleBoxFlat via
-## its set_ properties. Returns "" on success, an error
-## message on failure. Validates that only known keys are present.
-func _apply_sides(sb: StyleBoxFlat, sides_dict: Variant, dict_name: String,
- side_names: Array, prop_prefix: String, value_type: int) -> String:
- if typeof(sides_dict) != TYPE_DICTIONARY:
- return "'%s' must be a dict with 'all' and/or side-specific keys" % dict_name
- var valid_keys := {"all": true}
- for s in side_names:
- valid_keys[s] = true
- for k in sides_dict.keys():
- if not valid_keys.has(k):
- return "Unknown key in '%s': %s (valid: all, %s)" % [
- dict_name, k, ", ".join(side_names)
- ]
- # Apply `all` first, then override with side-specific keys.
- if sides_dict.has("all"):
- var all_val: Variant = sides_dict.all
- for s in side_names:
- var v: Variant = int(all_val) if value_type == TYPE_INT else float(all_val)
- sb.set(prop_prefix + s, v)
- for s in side_names:
- if sides_dict.has(s):
- var v2: Variant = int(sides_dict[s]) if value_type == TYPE_INT else float(sides_dict[s])
- sb.set(prop_prefix + s, v2)
- return ""
-
-
-func _apply_stylebox(theme_path: String, name: String, class_name_param: String, sb: StyleBox) -> void:
- var theme: Theme = ResourceLoader.load(theme_path)
- if theme == null:
- push_warning("MCP: Failed to load theme for undo/redo: %s" % theme_path)
- return
- theme.set_stylebox(name, class_name_param, sb)
- McpResourceIO.guarded_save(theme, theme_path, _connection)
-
-
-func _clear_stylebox(theme_path: String, name: String, class_name_param: String) -> void:
- var theme: Theme = ResourceLoader.load(theme_path)
- if theme == null:
- push_warning("MCP: Failed to load theme for undo/redo: %s" % theme_path)
- return
- theme.clear_stylebox(name, class_name_param)
- McpResourceIO.guarded_save(theme, theme_path, _connection)
-
-
-# ============================================================================
-# theme_apply — assign a theme to a Control
-# ============================================================================
-
-func apply_theme(params: Dictionary) -> Dictionary:
- var node_path: String = params.get("node_path", "")
- if node_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: node_path")
-
- var theme_path: String = params.get("theme_path", "")
- var theme: Theme = null
- if not theme_path.is_empty():
- var path_err := _validate_res_path(theme_path, ".tres")
- if path_err != null:
- return path_err
- if not ResourceLoader.exists(theme_path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Theme not found: %s" % theme_path)
- theme = ResourceLoader.load(theme_path)
- if theme == null or not theme is Theme:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a Theme" % theme_path)
-
- var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var _scene_root: Node = _resolved.scene_root
- if not node is Control and not node is Window:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Node %s is not a Control or Window (got %s)" % [node_path, node.get_class()]
- )
-
- var before_theme: Theme = node.theme
- _undo_redo.create_action("MCP: Apply theme to %s" % node.name)
- _undo_redo.add_do_property(node, "theme", theme)
- _undo_redo.add_undo_property(node, "theme", before_theme)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "node_path": node_path,
- "theme_path": theme_path if theme != null else "",
- "cleared": theme == null,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# Helpers
-# ============================================================================
-
-func _load_theme_from_params(params: Dictionary) -> Dictionary:
- var theme_path: String = params.get("theme_path", "")
- var err := _validate_res_path(theme_path, ".tres", "theme_path", true)
- if err != null:
- return err
- if not ResourceLoader.exists(theme_path):
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Theme not found: %s" % theme_path)
- var theme: Theme = ResourceLoader.load(theme_path)
- if theme == null or not theme is Theme:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a Theme" % theme_path)
- return {"theme": theme, "path": theme_path}
-
-
-static func _validate_res_path(path: String, required_suffix: String, param_name: String = "theme_path", for_write: bool = false) -> Variant:
- if path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: %s" % param_name)
- var path_err := McpPathValidator.validate_resource_path(path, for_write)
- if not path_err.is_empty():
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "%s: %s" % [param_name, path_err])
- if not path.ends_with(required_suffix):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "%s must end with %s (got %s)" % [param_name, required_suffix, path]
- )
- return null
-
-
-## Parse a color from Color, "#rrggbb", "#rrggbbaa", named (red/blue/...) or dict.
-## Returns null if the input cannot be parsed.
-## Delegates to the canonical parser (#714) — gains [r,g,b(,a)] array
-## support and strict key/component checking, same shapes as every other
-## color-accepting handler.
-static func _parse_color(value: Variant) -> Variant:
- return McpJsonValues.parse_color(value)
-
-
-static func _serialize_value(value: Variant) -> Variant:
- if value == null:
- return null
- if value is Color:
- return {"r": value.r, "g": value.g, "b": value.b, "a": value.a}
- if value is Vector2:
- return {"x": value.x, "y": value.y}
- return value
diff --git a/addons/godot_ai/handlers/theme_handler.gd.uid b/addons/godot_ai/handlers/theme_handler.gd.uid
deleted file mode 100644
index b77af13..0000000
--- a/addons/godot_ai/handlers/theme_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://gjyldaddj7mu
diff --git a/addons/godot_ai/handlers/tilemap_handler.gd b/addons/godot_ai/handlers/tilemap_handler.gd
deleted file mode 100644
index cc80da4..0000000
--- a/addons/godot_ai/handlers/tilemap_handler.gd
+++ /dev/null
@@ -1,163 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-## TileMap / TileMapLayer authoring — set, fill, clear, and read tile cells
-## directly in the editor scene with full undo/redo support.
-##
-## All ops target TileMapLayer nodes in the currently edited scene by
-## scene-relative path (e.g. "/LavaLake20x20/Ground").
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-const MAX_RECT_FILL_CELLS := 4096
-
-var _undo_redo: EditorUndoRedoManager
-
-
-func _init(undo_redo: EditorUndoRedoManager) -> void:
- _undo_redo = undo_redo
-
-
-## Set a single tile cell.
-## params: {path, source_id, atlas_col, atlas_row, map_x, map_y}
-## Returns: {map_x, map_y, source_id, atlas_col, atlas_row}
-func set_cell(params: Dictionary) -> Dictionary:
- var layer := _resolve_layer(params)
- if layer.has("error"): return layer
- var node: TileMapLayer = layer.node
- var pos := Vector2i(params.get("map_x", 0), params.get("map_y", 0))
- var src := int(params.get("source_id", 0))
- var atlas := Vector2i(params.get("atlas_col", 0), params.get("atlas_row", 0))
- var prev := _capture_cell_state(node, pos)
- _undo_redo.create_action("MCP: TileMap set_cell")
- _undo_redo.add_do_method(node, "set_cell", pos, src, atlas)
- _undo_redo.add_undo_method(self, "_restore_cell_state", node, pos, prev)
- _undo_redo.commit_action()
- return {"data": {"map_x": pos.x, "map_y": pos.y, "source_id": src,
- "atlas_col": atlas.x, "atlas_row": atlas.y, "undoable": true}}
-
-
-## Fill a rectangular region with one tile type in a single undo action.
-## params: {path, source_id, atlas_col, atlas_row, rect_x, rect_y, rect_w, rect_h}
-## Returns: {cells_filled, rect: {x, y, w, h}}
-func set_cells_rect(params: Dictionary) -> Dictionary:
- var layer := _resolve_layer(params)
- if layer.has("error"): return layer
- var node: TileMapLayer = layer.node
- var src := int(params.get("source_id", 0))
- var atlas := Vector2i(params.get("atlas_col", 0), params.get("atlas_row", 0))
- var rx := int(params.get("rect_x", 0)); var ry := int(params.get("rect_y", 0))
- var rw := int(params.get("rect_w", 1)); var rh := int(params.get("rect_h", 1))
- if rw <= 0 or rh <= 0:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "rect_w and rect_h must be > 0 (got %d x %d)" % [rw, rh]
- )
- var cell_count := rw * rh
- if cell_count > MAX_RECT_FILL_CELLS:
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Rect too large: %d cells exceeds max %d" % [cell_count, MAX_RECT_FILL_CELLS]
- )
- var cells: Array[Vector2i] = []
- var snapshot: Array[Dictionary] = []
- for x in range(rx, rx + rw):
- for y in range(ry, ry + rh):
- var pos := Vector2i(x, y)
- cells.append(pos)
- snapshot.append({"pos": pos, "state": _capture_cell_state(node, pos)})
- _undo_redo.create_action("MCP: TileMap set_cells_rect %dx%d" % [rw, rh])
- for pos in cells:
- _undo_redo.add_do_method(node, "set_cell", pos, src, atlas)
- _undo_redo.add_undo_method(self, "_restore_rect_snapshot", node, snapshot)
- _undo_redo.commit_action()
- return {"data": {"cells_filled": cells.size(),
- "rect": {"x": rx, "y": ry, "w": rw, "h": rh}, "undoable": true}}
-
-
-## Remove all tiles from a TileMapLayer.
-## params: {path}
-## Returns: {cleared: true}
-func clear_layer(params: Dictionary) -> Dictionary:
- var layer := _resolve_layer(params)
- if layer.has("error"): return layer
- var node: TileMapLayer = layer.node
- var snapshot := _capture_used_cells_snapshot(node)
- _undo_redo.create_action("MCP: TileMap clear")
- _undo_redo.add_do_method(node, "clear")
- _undo_redo.add_undo_method(self, "_restore_cells_snapshot", node, snapshot)
- _undo_redo.commit_action()
- return {"data": {"cleared": true, "undoable": true}}
-
-
-## Return all used cell coordinates.
-## params: {path}
-## Returns: {cells: [{x, y}, ...], count: int}
-func get_used_cells(params: Dictionary) -> Dictionary:
- var layer := _resolve_layer(params)
- if layer.has("error"): return layer
- var node: TileMapLayer = layer.node
- var cells := node.get_used_cells()
- var result: Array = []
- for c in cells:
- result.append({"x": c.x, "y": c.y})
- return {"data": {"cells": result, "count": result.size()}}
-
-
-## Resolve a TileMapLayer node from params["path"] in the currently edited
-## scene. Returns {"node": TileMapLayer} on success, or an error dict.
-func _resolve_layer(params: Dictionary) -> Dictionary:
- var path: String = params.get("path", "")
- var scene_file: String = params.get("scene_file", "")
- var resolved := McpNodeValidator.resolve_or_error(path, "path", scene_file)
- if resolved.has("error"):
- return resolved
- var node: Node = resolved.node
- if not node is TileMapLayer:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
- "Node is not a TileMapLayer: %s" % path)
- return {"node": node}
-
-
-func _capture_cell_state(node: TileMapLayer, pos: Vector2i) -> Dictionary:
- var source_id := node.get_cell_source_id(pos)
- if source_id == -1:
- return {"has_tile": false}
- var atlas: Vector2i = node.get_cell_atlas_coords(pos)
- var alternative := node.get_cell_alternative_tile(pos)
- return {
- "has_tile": true,
- "source_id": source_id,
- "atlas_col": atlas.x,
- "atlas_row": atlas.y,
- "alternative": alternative,
- }
-
-
-func _capture_used_cells_snapshot(node: TileMapLayer) -> Array[Dictionary]:
- var snapshot: Array[Dictionary] = []
- for pos in node.get_used_cells():
- snapshot.append({"pos": pos, "state": _capture_cell_state(node, pos)})
- return snapshot
-
-
-func _restore_cells_snapshot(node: TileMapLayer, snapshot: Array[Dictionary]) -> void:
- node.clear()
- for entry in snapshot:
- _restore_cell_state(node, entry.pos, entry.state)
-
-
-func _restore_rect_snapshot(node: TileMapLayer, snapshot: Array[Dictionary]) -> void:
- for entry in snapshot:
- _restore_cell_state(node, entry.pos, entry.state)
-
-
-func _restore_cell_state(node: TileMapLayer, pos: Vector2i, state: Dictionary) -> void:
- if not state.get("has_tile", false):
- node.erase_cell(pos)
- return
- node.set_cell(
- pos,
- int(state.get("source_id", -1)),
- Vector2i(int(state.get("atlas_col", -1)), int(state.get("atlas_row", -1))),
- int(state.get("alternative", 0))
- )
diff --git a/addons/godot_ai/handlers/tilemap_handler.gd.uid b/addons/godot_ai/handlers/tilemap_handler.gd.uid
deleted file mode 100644
index 8cbed53..0000000
--- a/addons/godot_ai/handlers/tilemap_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cm8s7a0ey2q6k
diff --git a/addons/godot_ai/handlers/tileset_handler.gd b/addons/godot_ai/handlers/tileset_handler.gd
deleted file mode 100644
index f3c5756..0000000
--- a/addons/godot_ai/handlers/tileset_handler.gd
+++ /dev/null
@@ -1,178 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-## TileSet management — atlas inspection helpers.
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-
-func _init() -> void:
- pass
-
-
-## Query all occupied atlas tile positions for a single source.
-##
-## params:
-## tileset_path — res:// path to the TileSet resource (required, non-empty)
-## source_id — raw TileSet source id (required)
-##
-## Returns:
-## {"data": {"tiles": [{"col": int, "row": int}, ...], "count": int}}
-## on success (including empty sources, where tiles=[] and count=0)
-## ErrorCodes.make(code, message) on any validation or load failure
-##
-## Error codes:
-## MISSING_REQUIRED_PARAM — tileset_path absent/empty, or source_id absent
-## RESOURCE_NOT_FOUND — ResourceLoader.exists(tileset_path) is false
-## WRONG_TYPE — loaded resource is not a TileSet, or source is
-## not a TileSetAtlasSource
-## VALUE_OUT_OF_RANGE — source_id not present in TileSet
-##
-## This method is read-only: it never calls ResourceSaver or modifies any resource.
-func get_atlas_tiles(params: Dictionary) -> Dictionary:
- var resolved := _resolve_atlas_source(params)
- if resolved.has("error"):
- return resolved
- var src: TileSetAtlasSource = resolved.src
-
- var tiles: Array = []
- for i in range(src.get_tiles_count()):
- var v: Vector2i = src.get_tile_id(i)
- tiles.append({"col": v.x, "row": v.y})
-
- return {"data": {"tiles": tiles, "count": tiles.size()}}
-
-
-## Return the atlas texture of a TileSetAtlasSource as a Base64-encoded PNG.
-##
-## params:
-## tileset_path — res:// path to the TileSet resource (required, non-empty)
-## source_id — raw TileSet source id (required)
-## max_size — optional int; if > 0, the image is scaled so its longest
-## edge is at most max_size pixels (default 0 = full res)
-##
-## Returns:
-## {"data": {"image_base64": String, "width": int, "height": int,
-## "original_width": int, "original_height": int, "format": "png"}}
-## on success
-## ErrorCodes.make(code, message) on any validation or load failure
-##
-## Error codes:
-## MISSING_REQUIRED_PARAM — tileset_path absent/empty, or source_id absent
-## RESOURCE_NOT_FOUND — ResourceLoader.exists(tileset_path) is false
-## WRONG_TYPE — loaded resource is not a TileSet, or source is
-## not a TileSetAtlasSource, or texture is null
-## VALUE_OUT_OF_RANGE — source_id not present in TileSet
-##
-## This method is read-only: it never calls ResourceSaver or modifies anything.
-func get_atlas_image(params: Dictionary) -> Dictionary:
- var resolved := _resolve_atlas_source(params)
- if resolved.has("error"):
- return resolved
- var source_id: int = resolved.source_id
- var src: TileSetAtlasSource = resolved.src
-
- var tex: Texture2D = src.texture
- if tex == null:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Source %d has no texture assigned" % source_id
- )
-
- var img: Image = tex.get_image()
- if img == null:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Could not retrieve image data from texture of source %d" % source_id
- )
- if img.is_compressed():
- var decompress_err := img.decompress()
- if decompress_err != OK:
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Could not decompress texture of source %d: %s" % [source_id, error_string(decompress_err)]
- )
-
- var original_width: int = img.get_width()
- var original_height: int = img.get_height()
-
- var max_size: int = params.get("max_size", 0)
- if max_size > 0:
- var longest_edge: int = max(original_width, original_height)
- if longest_edge > max_size:
- var scale: float = float(max_size) / float(longest_edge)
- var new_w: int = max(1, int(original_width * scale))
- var new_h: int = max(1, int(original_height * scale))
- img.resize(new_w, new_h, Image.INTERPOLATE_LANCZOS)
-
- var png_bytes: PackedByteArray = img.save_png_to_buffer()
- if png_bytes.is_empty():
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "PNG encoding produced empty output for source %d" % source_id
- )
- var b64: String = Marshalls.raw_to_base64(png_bytes)
-
- return {
- "data": {
- "image_base64": b64,
- "width": img.get_width(),
- "height": img.get_height(),
- "original_width": original_width,
- "original_height": original_height,
- "format": "png",
- }
- }
-
-
-func _resolve_atlas_source(params: Dictionary) -> Dictionary:
- var tileset_path: String = params.get("tileset_path", "")
- if tileset_path.is_empty():
- return ErrorCodes.make(
- ErrorCodes.MISSING_REQUIRED_PARAM,
- "'tileset_path' parameter is required and must not be empty"
- )
-
- if not params.has("source_id"):
- return ErrorCodes.make(
- ErrorCodes.MISSING_REQUIRED_PARAM,
- "'source_id' parameter is required"
- )
-
- var tileset_path_err = McpPathValidator.loadable_error(tileset_path, "tileset_path")
- if tileset_path_err != null:
- return tileset_path_err
-
- if not ResourceLoader.exists(tileset_path):
- return ErrorCodes.make(
- ErrorCodes.RESOURCE_NOT_FOUND,
- "TileSet resource not found: %s" % tileset_path
- )
-
- var ts = load(tileset_path)
- if not ts is TileSet:
- var loaded_type := "null" if ts == null else ts.get_class()
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Resource at '%s' is not a TileSet (got %s)" % [tileset_path, loaded_type]
- )
-
- var source_id: int = int(params.get("source_id", -999))
- if source_id < 0 or not ts.has_source(source_id):
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "source_id %d does not exist in TileSet" % source_id
- )
-
- var src = ts.get_source(source_id)
- if not src is TileSetAtlasSource:
- var source_type: String = "null" if src == null else src.get_class()
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Source %d is not a TileSetAtlasSource (got %s)" % [source_id, source_type]
- )
-
- return {
- "source_id": source_id,
- "src": src,
- }
diff --git a/addons/godot_ai/handlers/tileset_handler.gd.uid b/addons/godot_ai/handlers/tileset_handler.gd.uid
deleted file mode 100644
index 643cfd3..0000000
--- a/addons/godot_ai/handlers/tileset_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://de3v4m1pnk7tr
diff --git a/addons/godot_ai/handlers/ui_handler.gd b/addons/godot_ai/handlers/ui_handler.gd
deleted file mode 100644
index f183751..0000000
--- a/addons/godot_ai/handlers/ui_handler.gd
+++ /dev/null
@@ -1,525 +0,0 @@
-@tool
-extends "res://addons/godot_ai/handlers/command_handler.gd"
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Handles UI-specific (Control) layout helpers: anchor presets, etc.
-##
-## Anchors/offsets are the worst part of Control layout to set one-property-at-a-time.
-## This handler wraps Godot's built-in presets (FULL_RECT, CENTER, TOP_LEFT, ...) so
-## callers can set a whole layout with one command, with proper undo.
-
-var _undo_redo: EditorUndoRedoManager
-
-
-const _PRESETS := {
- "top_left": Control.PRESET_TOP_LEFT,
- "top_right": Control.PRESET_TOP_RIGHT,
- "bottom_left": Control.PRESET_BOTTOM_LEFT,
- "bottom_right": Control.PRESET_BOTTOM_RIGHT,
- "center_left": Control.PRESET_CENTER_LEFT,
- "center_top": Control.PRESET_CENTER_TOP,
- "center_right": Control.PRESET_CENTER_RIGHT,
- "center_bottom": Control.PRESET_CENTER_BOTTOM,
- "center": Control.PRESET_CENTER,
- "left_wide": Control.PRESET_LEFT_WIDE,
- "top_wide": Control.PRESET_TOP_WIDE,
- "right_wide": Control.PRESET_RIGHT_WIDE,
- "bottom_wide": Control.PRESET_BOTTOM_WIDE,
- "vcenter_wide": Control.PRESET_VCENTER_WIDE,
- "hcenter_wide": Control.PRESET_HCENTER_WIDE,
- "full_rect": Control.PRESET_FULL_RECT,
-}
-
-const _RESIZE_MODES := {
- "minsize": Control.PRESET_MODE_MINSIZE,
- "keep_width": Control.PRESET_MODE_KEEP_WIDTH,
- "keep_height": Control.PRESET_MODE_KEEP_HEIGHT,
- "keep_size": Control.PRESET_MODE_KEEP_SIZE,
-}
-
-const _ANCHOR_OFFSET_PROPS := [
- "anchor_left", "anchor_top", "anchor_right", "anchor_bottom",
- "offset_left", "offset_top", "offset_right", "offset_bottom",
-]
-
-
-func _init(undo_redo: EditorUndoRedoManager) -> void:
- _undo_redo = undo_redo
-
-
-## Apply a Control layout preset (anchors + offsets) to a UI node.
-##
-## Params:
-## path - scene path to a Control node (required)
-## preset - preset name: full_rect, center, top_left, ... (required)
-## resize_mode - minsize | keep_width | keep_height | keep_size (default: minsize)
-## margin - integer margin in pixels from the anchor edges (default: 0)
-func set_anchor_preset(params: Dictionary) -> Dictionary:
- var node_path: String = params.get("path", "")
- if node_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
-
- var preset_name: String = str(params.get("preset", "")).to_lower()
- if preset_name.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: preset")
- if not _PRESETS.has(preset_name):
- var names := _PRESETS.keys()
- names.sort()
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Unknown preset '%s'. Valid: %s" % [preset_name, ", ".join(names)]
- )
-
- var resize_mode_name: String = str(params.get("resize_mode", "minsize")).to_lower()
- if not _RESIZE_MODES.has(resize_mode_name):
- var names := _RESIZE_MODES.keys()
- names.sort()
- return ErrorCodes.make(
- ErrorCodes.VALUE_OUT_OF_RANGE,
- "Unknown resize_mode '%s'. Valid: %s" % [resize_mode_name, ", ".join(names)]
- )
-
- var margin: int = int(params.get("margin", 0))
-
- var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var scene_root: Node = _resolved.scene_root
- if not node is Control:
- var got_class: String = node.get_class()
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Node %s is not a Control (got %s)%s" % [
- node_path, got_class, _canvas_layer_overlay_hint(got_class)
- ]
- )
-
- var control := node as Control
- var preset_value: int = _PRESETS[preset_name]
- var resize_mode_value: int = _RESIZE_MODES[resize_mode_name]
-
- # Snapshot before so we can undo every property the preset may have touched.
- var before: Dictionary = {}
- for prop in _ANCHOR_OFFSET_PROPS:
- before[prop] = control.get(prop)
-
- _undo_redo.create_action("MCP: Set %s anchor preset %s" % [control.name, preset_name])
- _undo_redo.add_do_method(
- control, "set_anchors_and_offsets_preset", preset_value, resize_mode_value, margin
- )
- for prop in _ANCHOR_OFFSET_PROPS:
- _undo_redo.add_undo_property(control, prop, before[prop])
- _undo_redo.commit_action()
-
- var after: Dictionary = {}
- for prop in _ANCHOR_OFFSET_PROPS:
- after[prop] = control.get(prop)
-
- return {
- "data": {
- "path": node_path,
- "preset": preset_name,
- "resize_mode": resize_mode_name,
- "margin": margin,
- "anchors": {
- "left": after.anchor_left,
- "top": after.anchor_top,
- "right": after.anchor_right,
- "bottom": after.anchor_bottom,
- },
- "offsets": {
- "left": after.offset_left,
- "top": after.offset_top,
- "right": after.offset_right,
- "bottom": after.offset_bottom,
- },
- "undoable": true,
- }
- }
-
-
-## Set the visible `text` property on a UI Control (Label, Button + subclasses,
-## LineEdit, TextEdit, RichTextLabel, LinkButton). Undoable.
-func set_text(params: Dictionary) -> Dictionary:
- var node_path: String = params.get("path", "")
- if node_path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
-
- if not params.has("text"):
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: text")
- var text_value: Variant = params["text"]
- if typeof(text_value) != TYPE_STRING:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "text must be a string")
-
- var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
- if _resolved.has("error"):
- return _resolved
- var node: Node = _resolved.node
- var scene_root: Node = _resolved.scene_root
- var node_type := node.get_class()
- if not node is Control:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Node %s is not a Control (got %s)" % [node_path, node_type]
- )
- # Scan get_property_list() (matches set_property / _apply_property in this
- # repo) so we can both confirm `text` exists and that it's actually a String
- # — guards against a custom Control whose `text` happens to be some other
- # type, where set()-ing a String would silently mis-coerce.
- var text_prop_type := TYPE_NIL
- var has_text := false
- for prop in node.get_property_list():
- if prop.get("name", "") == "text":
- has_text = true
- text_prop_type = prop.get("type", TYPE_NIL)
- break
- if not has_text:
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Control %s has no 'text' property (got %s)" % [node_path, node_type]
- )
- if text_prop_type != TYPE_STRING:
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- "Control %s has a non-string 'text' property (got %s)" % [node_path, node_type]
- )
-
- var old_value: String = node.get("text")
-
- _undo_redo.create_action("MCP: Set %s text" % node.name)
- _undo_redo.add_do_property(node, "text", text_value)
- _undo_redo.add_undo_property(node, "text", old_value)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "path": node_path,
- "text": text_value,
- "old_text": old_value,
- "node_type": node_type,
- "undoable": true,
- }
- }
-
-
-# ============================================================================
-# build_layout — declarative nested-dict → Control tree in one undo action
-# ============================================================================
-
-## Build a tree of Control nodes atomically.
-##
-## Params:
-## tree - Dictionary describing the root node. Required fields: "type".
-## Optional: "name", "properties" (dict), "anchor_preset",
-## "anchor_margin", "theme" (res://, uid:// or user:// path), "children" (array).
-## parent_path - Parent scene path. Empty or "/" = scene root.
-##
-## Validation is done before any scene mutation: class names, property
-## existence, and res:// paths are all checked up-front. If anything is
-## invalid, no node is created.
-func build_layout(params: Dictionary) -> Dictionary:
- var tree = params.get("tree")
- if not params.has("tree"):
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: tree")
- if typeof(tree) != TYPE_DICTIONARY:
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "tree must be a dictionary")
-
- var _scene_check := McpNodeValidator.require_scene_or_error()
- if _scene_check.has("error"):
- return _scene_check
- var scene_root: Node = _scene_check.scene_root
-
- var parent_path: String = params.get("parent_path", "")
- var parent: Node = scene_root
- if not parent_path.is_empty() and parent_path != "/":
- parent = McpScenePath.resolve(parent_path, scene_root)
- if parent == null:
- return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, McpScenePath.format_parent_error(parent_path, scene_root))
-
- # Validate + build in memory first; if anything fails, free and bail.
- var built := _build_subtree(tree)
- if built.has("error"):
- return built
- var root_node: Node = built.node
- var created: Array[Node] = built.created
-
- _undo_redo.create_action("MCP: Build UI layout (%d nodes)" % created.size())
- _undo_redo.add_do_method(parent, "add_child", root_node, true)
- _undo_redo.add_do_method(root_node, "set_owner", scene_root)
- for n in created:
- _undo_redo.add_do_method(n, "set_owner", scene_root)
- _undo_redo.add_do_reference(n)
- _undo_redo.add_undo_method(parent, "remove_child", root_node)
- _undo_redo.commit_action()
-
- return {
- "data": {
- "root_path": McpScenePath.from_node(root_node, scene_root),
- "node_count": created.size(),
- "undoable": true,
- }
- }
-
-
-## Recursively instantiate + configure a node and its children in memory.
-## Returns {"node": root, "created": [all descendants incl. root]} or {"error": ...}.
-func _build_subtree(spec: Dictionary) -> Dictionary:
- var node_type: String = spec.get("type", "")
- if node_type.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Every layout node requires a 'type'")
- if not ClassDB.class_exists(node_type):
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Unknown type: %s" % node_type)
- if not ClassDB.is_parent_class(node_type, "Node"):
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "%s is not a Node type" % node_type)
-
- var node: Node = ClassDB.instantiate(node_type)
- if node == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate %s" % node_type)
-
- var node_name: String = spec.get("name", "")
- if not node_name.is_empty():
- node.name = node_name
-
- # Properties.
- if spec.has("properties"):
- var props = spec.get("properties")
- if typeof(props) != TYPE_DICTIONARY:
- node.free()
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "properties must be a dictionary")
- for key in props:
- var value = props[key]
- var apply_err := _apply_property(node, str(key), value)
- if apply_err != null:
- node.free()
- return apply_err
-
- # Theme (res:// / uid:// / user:// path -> Resource).
- if spec.has("theme"):
- var theme_path: String = str(spec.get("theme", ""))
- if not theme_path.is_empty():
- var theme_path_err = McpPathValidator.loadable_error(theme_path, "theme")
- if theme_path_err != null:
- node.free()
- return theme_path_err
- if not ResourceLoader.exists(theme_path):
- node.free()
- return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Theme not found: %s" % theme_path)
- var theme_res: Resource = ResourceLoader.load(theme_path)
- if theme_res == null or not theme_res is Theme:
- node.free()
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "theme path must point to a Theme resource: %s" % theme_path)
- if not node is Control and not node is Window:
- node.free()
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "theme can only be set on Control / Window (got %s)%s" % [
- node_type, _canvas_layer_overlay_hint(node_type)
- ]
- )
- node.theme = theme_res as Theme
-
- # Anchor preset — applied before children so children inherit sensible anchors.
- if spec.has("anchor_preset"):
- var preset_name: String = str(spec.get("anchor_preset", "")).to_lower()
- if not _PRESETS.has(preset_name):
- node.free()
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Unknown anchor_preset: %s" % preset_name)
- if not node is Control:
- node.free()
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "anchor_preset requires a Control (got %s)%s" % [
- node_type, _canvas_layer_overlay_hint(node_type)
- ]
- )
- var preset_value: int = _PRESETS[preset_name]
- var margin: int = int(spec.get("anchor_margin", 0))
- (node as Control).set_anchors_and_offsets_preset(preset_value, Control.PRESET_MODE_MINSIZE, margin)
-
- var created: Array[Node] = [node]
- if spec.has("children"):
- var children = spec.get("children")
- if typeof(children) != TYPE_ARRAY:
- node.free()
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "children must be an array")
- for child_spec in children:
- if typeof(child_spec) != TYPE_DICTIONARY:
- node.free()
- return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "each child must be a dictionary")
- var child_result := _build_subtree(child_spec)
- if child_result.has("error"):
- node.free()
- return child_result
- var child_node: Node = child_result.node
- node.add_child(child_node)
- for n in child_result.created:
- created.append(n)
- return {"node": node, "created": created}
-
-
-## Mapping from theme_override_* property prefixes to their add/remove methods.
-const _THEME_OVERRIDE_MAP := {
- "theme_override_colors/": {
- "add": "add_theme_color_override",
- "remove": "remove_theme_color_override",
- "coerce_type": TYPE_COLOR,
- },
- "theme_override_constants/": {
- "add": "add_theme_constant_override",
- "remove": "remove_theme_constant_override",
- "coerce_type": TYPE_INT,
- },
- "theme_override_font_sizes/": {
- "add": "add_theme_font_size_override",
- "remove": "remove_theme_font_size_override",
- "coerce_type": TYPE_INT,
- },
- "theme_override_styles/": {
- "add": "add_theme_stylebox_override",
- "remove": "remove_theme_stylebox_override",
- "coerce_type": TYPE_OBJECT,
- },
-}
-
-
-## Apply a property to a newly-instantiated node. Handles Color/Vector2/NodePath
-## coercion from JSON-friendly forms. Returns null on success, error dict on failure.
-func _apply_property(node: Node, prop: String, value: Variant) -> Variant:
- # Handle theme_override_* pseudo-properties before the regular property scan.
- for prefix in _THEME_OVERRIDE_MAP:
- if prop.begins_with(prefix):
- if not node is Control:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "theme_override_* requires a Control node (got %s)" % node.get_class()
- )
- var override_name := prop.substr(prefix.length())
- var info: Dictionary = _THEME_OVERRIDE_MAP[prefix]
- var coerce_type: int = info.coerce_type
-
- # For stylebox overrides, load from a res:// / uid:// / user:// path.
- if coerce_type == TYPE_OBJECT:
- if value is String and (value.begins_with("res://") or value.begins_with("uid://") or value.begins_with("user://")):
- var style_path_err = McpPathValidator.loadable_error(value, "stylebox")
- if style_path_err != null:
- return style_path_err
- var res := ResourceLoader.load(value)
- if res == null or not res is StyleBox:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "Style resource not found or not a StyleBox: %s" % value
- )
- node.call(info.add, override_name, res)
- else:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "theme_override_styles/ expects a res:// / uid:// / user:// path to a StyleBox"
- )
- else:
- var coercion := _coerce_for_type(value, coerce_type)
- if not coercion.ok:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Cannot coerce '%s' for %s" % [value, prop]
- )
- node.call(info.add, override_name, coercion.value)
- return null
-
- var found := false
- var prop_type := TYPE_NIL
- for p in node.get_property_list():
- if p.name == prop:
- found = true
- prop_type = p.get("type", TYPE_NIL)
- break
- if not found:
- return ErrorCodes.make(
- ErrorCodes.PROPERTY_NOT_ON_CLASS,
- McpPropertyErrors.build_message(node, prop)
- )
-
- var coercion := _coerce_for_type(value, prop_type)
- if not coercion.ok:
- return ErrorCodes.make(
- ErrorCodes.WRONG_TYPE,
- "Property '%s' on %s expects type %s (cannot coerce %s)" % [
- prop, node.get_class(), type_string(prop_type), value
- ]
- )
- node.set(prop, coercion.value)
- return null
-
-
-## Coerce a JSON-friendly value to the target Godot type. Returns
-## {"ok": true, "value": coerced} on success, {"ok": false} on failure.
-## For types we don't explicitly coerce, the value is returned as-is
-## (Godot will typecheck at set() time and fail loudly if it disagrees).
-static func _coerce_for_type(value: Variant, prop_type: int) -> Dictionary:
- match prop_type:
- TYPE_COLOR:
- ## Canonical parser (#714): adds [r,g,b(,a)] array support and
- ## strict key/component checking, same shapes everywhere.
- var parsed_color = McpJsonValues.parse_color(value)
- if parsed_color != null:
- return {"ok": true, "value": parsed_color}
- return {"ok": false}
- TYPE_VECTOR2:
- ## Same canonical parser as TYPE_COLOR (CodeRabbit review):
- ## keeping the inline copy here would re-introduce exactly the
- ## permissive-vs-strict drift this PR removes elsewhere.
- var parsed_v2 = McpJsonValues.parse_vector2(value)
- if parsed_v2 != null:
- return {"ok": true, "value": parsed_v2}
- return {"ok": false}
- TYPE_VECTOR2I:
- if value is Vector2i:
- return {"ok": true, "value": value}
- if value is Dictionary and value.has("x") and value.has("y"):
- return {"ok": true, "value": Vector2i(int(value.x), int(value.y))}
- if value is Array and value.size() == 2:
- return {"ok": true, "value": Vector2i(int(value[0]), int(value[1]))}
- return {"ok": false}
- TYPE_RECT2:
- if value is Rect2:
- return {"ok": true, "value": value}
- if value is Array and value.size() == 4:
- return {
- "ok": true,
- "value":
- Rect2(float(value[0]), float(value[1]), float(value[2]), float(value[3])),
- }
- if value is Dictionary:
- if value.has("x") and value.has("y") and value.has("w") and value.has("h"):
- return {
- "ok": true,
- "value":
- Rect2(float(value.x), float(value.y), float(value.w), float(value.h)),
- }
- if value.has("position") and value.has("size"):
- var pos := _coerce_for_type(value.position, TYPE_VECTOR2)
- var sz := _coerce_for_type(value.size, TYPE_VECTOR2)
- if pos.ok and sz.ok:
- return {"ok": true, "value": Rect2(pos.value, sz.value)}
- return {"ok": false}
- TYPE_NODE_PATH:
- if value is NodePath:
- return {"ok": true, "value": value}
- if value is String:
- return {"ok": true, "value": NodePath(value)}
- return {"ok": false}
- return {"ok": true, "value": value}
-
-
-# CanvasLayer is the canonical HUD parent but isn't a Control, so applying
-# Control-only properties (theme, anchor_preset) to it is a common mistake.
-# The recovery shape is always the same: nest a Control child under the layer.
-static func _canvas_layer_overlay_hint(node_class: String) -> String:
- if node_class != "CanvasLayer":
- return ""
- return (
- ". CanvasLayer is not a Control — add a Control (e.g. Panel or Control "
- + "with anchor_preset=full_rect) as its child and apply theme / "
- + "anchor_preset to that overlay."
- )
diff --git a/addons/godot_ai/handlers/ui_handler.gd.uid b/addons/godot_ai/handlers/ui_handler.gd.uid
deleted file mode 100644
index f48f841..0000000
--- a/addons/godot_ai/handlers/ui_handler.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ckm6f1objpgvw
diff --git a/addons/godot_ai/mcp_dock.gd b/addons/godot_ai/mcp_dock.gd
deleted file mode 100644
index 1d90189..0000000
--- a/addons/godot_ai/mcp_dock.gd
+++ /dev/null
@@ -1,2849 +0,0 @@
-@tool
-class_name McpDock
-extends VBoxContainer
-
-## Editor dock panel showing MCP connection status, client config, and command log.
-##
-## Audit-v2 #360 partial extraction. Two cohesive subpanels live in
-## res://addons/godot_ai/dock_panels/:
-## - log_viewer.gd: MCP request/response log (dev-mode only).
-## - port_picker_panel.gd: spawn-failure escape hatch nested in the crash panel.
-##
-## The audit also called for ServerStatusPanel and ClientRowController
-## extractions; those were *deliberately deferred*. Their UI scatters across
-## the dock layout (status icon at top, crash panel mid, setup section lower;
-## client rows + drift banner + scroll grid spread similarly), so a clean
-## extract-by-panel needs either visible UI reorganization or a coordinator-
-## Node pattern with property-accessor façades on McpDock that re-tangle the
-## very state they claim to move.
-##
-## Client workers and update orchestration have plugin lifetime and are owned
-## by plugin.gd. This replaceable view emits intents and paints copied
-## snapshots/outcomes; detaching it cannot destroy domain work.
-
-const ServerStateScript := preload("res://addons/godot_ai/utils/mcp_server_state.gd")
-const ClientRefreshStateScript := preload("res://addons/godot_ai/utils/mcp_client_refresh_state.gd")
-const Client := preload("res://addons/godot_ai/clients/_base.gd")
-const ClientConfigurator := preload("res://addons/godot_ai/client_configurator.gd")
-const ClientRegistry := preload("res://addons/godot_ai/clients/_registry.gd")
-const ToolCatalog := preload("res://addons/godot_ai/tool_catalog.gd")
-const LogViewerScript := preload("res://addons/godot_ai/dock_panels/log_viewer.gd")
-const PortPickerPanelScript := preload("res://addons/godot_ai/dock_panels/port_picker_panel.gd")
-const VisionRoutingScript := preload("res://addons/godot_ai/vision_routing.gd")
-
-const DEV_MODE_SETTING := "godot_ai/dev_mode"
-## "Change the port + reconfigure your clients" guide. Surfaced from the crash
-## panel when a foreign process holds the HTTP port — the one piece of recovery
-## (per-client config rewrite) that doesn't fit in the inline crash body.
-## Resolved against the installed plugin version at click time (see
-## `_port_conflict_docs_url`) so a shipped build opens the guide as it shipped,
-## not tip-of-main, which may have drifted from that build's UI.
-const PORT_CONFLICT_DOCS_PATH := "docs/port-conflicts.md"
-const REPO_BLOB_BASE := "https://github.com/hi-godot/godot-ai/blob"
-const RELEASES_PAGE := "https://github.com/hi-godot/godot-ai/releases/latest"
-## Opened by the "How to install uv" button. See _on_install_uv for why the
-## dock links here instead of running an installer itself.
-const UV_INSTALL_DOCS_URL := "https://docs.astral.sh/uv/getting-started/installation/"
-static var COLOR_MUTED := Color(0.7, 0.7, 0.7)
-static var COLOR_HEADER := Color(0.95, 0.95, 0.95)
-## Used for "in-progress" / "stale, action needed" UI: the startup-grace
-## status icon, the spawn-failure suggested-port hint, the drift banner,
-## and the per-row mismatch dot. One constant so a future palette tweak
-## doesn't have to find every literal.
-static var COLOR_AMBER := Color(1.0, 0.75, 0.25)
-
-signal update_requested
-signal client_action_requested(client_id: String, action: String)
-signal client_status_refresh_requested(client_ids: Array[String], force: bool)
-signal status_snapshot_requested
-signal live_server_probe_requested(port: int)
-signal lifecycle_action_requested(action: int)
-signal dev_server_action_requested(action: int)
-signal mcp_logging_changed(enabled: bool)
-signal log_snapshot_requested(after_sequence: int)
-signal plugin_reload_requested(reason: String)
-signal settings_apply_requested(changes: Dictionary, reload: bool)
-signal post_update_action_requested(action: String)
-
-enum LifecycleAction { RECOVER_INCOMPATIBLE, RESTART_SERVER }
-enum DevServerAction { START_OR_RESTART, STOP }
-
-## Copied root-owned values only. The Dock never retains Connection, lifecycle,
-## plugin, log-buffer, or process owners.
-var _transport_snapshot: Dictionary = {
- "connected": false,
- "server_version": "",
- "status": {},
-}
-var _lifecycle_snapshot: Dictionary = {
- "state": ServerStateScript.UNINITIALIZED,
- "server_pid": -1,
- "resolved_ws_port": 0,
- "can_restart_managed": false,
- "can_recover_incompatible": false,
- "normal_start_released": false,
-}
-var _live_server_probe_result: Dictionary = {}
-
-# Always visible
-var _redock_btn: Button
-var _status_icon: ColorRect
-var _status_label: Label
-var _body_scroll: ScrollContainer
-var _body: VBoxContainer
-var _client_grid: VBoxContainer
-var _client_configure_all_btn: Button
-var _client_empty_cta_btn: Button
-var _clients_summary_label: Label
-var _clients_window: Window
-var _dev_mode_toggle: CheckButton
-var _install_label: Label
-
-# Tools tab (secondary window, Tab 2) — domain-exclusion UI for clients
-# that cap total tool count (Antigravity: 100). Pending set is mutated by
-# checkbox clicks; saved set reflects what the spawned server actually
-# sees. `Apply and Restart Server` writes pending → setting and triggers a
-# plugin reload so the new server comes up with the trimmed list.
-var _tools_pending_excluded: PackedStringArray = PackedStringArray()
-var _tools_saved_excluded: PackedStringArray = PackedStringArray()
-## Custom (addon-registered) tools list — rebuilt live on registry
-## tools_changed; per-tool checkboxes apply immediately (no restart).
-var _custom_tools_list: VBoxContainer
-var _custom_tools_count_label: Label
-var _tools_domain_checkboxes: Dictionary = {}
-var _tools_count_label: Label
-var _tools_apply_btn: Button
-var _tools_reset_btn: Button
-var _tools_dirty_warning: Label
-var _tools_close_confirm: ConfirmationDialog
-## Update saves the project and relaunches the editor; the user says so first.
-var _update_confirm: ConfirmationDialog
-var _update_candidate_version := ""
-var _telemetry_toggle: CheckButton
-var _telemetry_pending_enabled: bool = true
-var _telemetry_saved_enabled: bool = true
-
-# Settings tab (secondary window, Tab 3) — Vision Routing section plus the
-# LAN opt-in (#507): "Allow remote hosts (CIDR)" behind a collapsed
-# "Remote access (advanced)" disclosure (auto-expands when a non-empty
-# allowlist is configured). The value feeds `--allow-host` at server spawn
-# (see plugin.gd::_build_server_flags). The LineEdit's live text is the
-# pending state; `_allow_hosts_saved` mirrors the persisted EditorSetting,
-# same pending/saved shape as the Tools tab above.
-var _allow_hosts_section: VBoxContainer
-var _allow_hosts_fold: FoldableContainer
-var _allow_hosts_edit: LineEdit
-var _allow_hosts_hint: Label
-var _allow_hosts_apply_btn: Button
-var _allow_hosts_saved: String = ""
-
-## Per-client UI handles, keyed by client id. Each entry holds the row's
-## status dot, configure/remove buttons, config-file buttons, and manual panel.
-var _client_rows: Dictionary = {}
-
-# Drift banner — surfaced near the Clients section when one or more clients
-# have a stored entry whose URL no longer matches `http_url()` (typical after
-# the user changes `godot_ai/http_port`). Refreshes are stale-while-refreshing:
-# cached row dots/banner remain visible while a background worker performs the
-# potentially blocking config/CLI probes, then the main thread applies results.
-# Automatic focus-in refreshes use a short cooldown to avoid repeated sweeps
-# during tab-away/tab-back churn. See #166 and #226.
-var _drift_banner: VBoxContainer
-var _drift_label: Label
-## Set when the user clicks "How to install uv"; consumed by the next
-## application focus-in so the uv row is re-probed after the user has had a
-## chance to install, not immediately. See _on_install_uv and _notification.
-## (Deliberately spelled without the focus-in constant name: the guard in
-## tests/unit/test_editor_focus_refocus.py locates the notification handler
-## by first occurrence of that token.)
-var _uv_recheck_pending := false
-## Handles for the Setup section's "Server" row. `_update_status_label` keeps
-## the label text/color in sync with `McpConnection.server_version` so the
-## dock reports the TRUE running server version, not the plugin's
-## expected version. See #174 follow-up — a plugin upgrade via self-
-## update can leave the plugin connected to an older adopted server
-## (foreign-port branch never sets `_server_pid`, so `_stop_server`
-## can't kill it); the line has to show the mismatch honestly.
-var _setup_server_label: Label
-## Last rendered server-version string. `_update_status_label` runs every
-## frame; early-outs text repaint when nothing changed. Empty means
-## "no line rendered yet" (dev-checkout branch doesn't render a
-## user-mode Server line).
-var _last_rendered_server_text: String = ""
-## Restart-server button shown next to the Setup container when
-## `McpConnection.server_version` drifts from the plugin version. Hidden
-## in the match case so the UI stays calm.
-var _version_restart_btn: Button
-var _server_restart_in_progress := false
-## Sorted snapshot of the most recent mismatched-client set. Powers two things:
-## (a) the Reconfigure button reuses this list instead of re-running
-## `check_status` per row (saves ~18 filesystem reads per click), and
-## (b) `_refresh_drift_banner` early-returns when the set is unchanged so
-## repeated explicit refreshes don't repaint identical text. Mirrors the
-## `_last_server_status` pattern used by the crash panel.
-var _last_mismatched_ids: Array[String] = []
-## Copied plugin-lifetime client-job state. This contains no Thread, mutex,
-## cancellation map, generation, or mutable owner record.
-var _client_work_snapshot: Dictionary = {
- "accepting_work": false,
- "refresh_state": ClientRefreshStateScript.IDLE,
- "refresh_completed": false,
- "busy_actions": [],
- "action_names": {},
- "action_phases": {},
-}
-var _update_install_in_flight := false
-
-# Dev-mode only
-var _dev_section: VBoxContainer
-var _server_label: Label
-var _reload_btn: Button
-var _setup_section: VBoxContainer
-var _setup_container: VBoxContainer
-## Developer controls are a view of the ordinary lifecycle owner. They never
-## start a second process topology or kill an externally launched server.
-var _dev_primary_btn: Button
-## Small "✕" affordance next to the primary — enabled only for the exact
-## plugin-owned process grant.
-var _dev_stop_btn: Button
-var _log_viewer: LogViewerScript
-## Vision Routing (optional) - set by plugin.gd; builds the "Vision Routing"
-## tab in Clients & Tools and the quick toggle under Developer mode.
-var vision_routing: VisionRoutingScript = null
-
-var _last_connected := false
-var _last_status_text := ""
-var _last_status_tooltip := ""
-var _startup_grace_until_msec: int = 0
-
-# Spawn-failure panel — rendered when `get_server_status` reports a
-# non-OK `state`. One panel, one body paragraph per state, no cascading
-# booleans. See `_crash_body_for_state`.
-var _crash_panel: VBoxContainer
-var _crash_output: RichTextLabel
-var _crash_restart_btn: Button
-var _crash_reload_btn: Button
-## Help link — visible only for the genuinely-foreign-occupant INCOMPATIBLE
-## case (no `can_recover_incompatible` proof). The inline body names a free
-## port; this button carries the per-client reconfigure steps that don't fit
-## inline. See `PORT_CONFLICT_DOCS` and `_update_crash_panel`.
-var _crash_docs_btn: Button
-## Port-picker escape hatch — visible inside the crash panel when the root
-## cause is port contention (PORT_EXCLUDED or FOREIGN_PORT). The dock writes
-## the EditorSetting and reloads the plugin in response to the panel's
-## `port_apply_requested` signal.
-var _port_picker_panel: PortPickerPanelScript
-## Last status Dict rendered into the panel — used to skip re-population
-## when nothing changed, which would otherwise reset the user's scroll
-## position on every frame. GDScript Dicts compare by value with `==`.
-var _last_server_status: Dictionary = {}
-
-# First-run grace: uvx installs 60+ Python packages on first run (can take
-# 10-30s on a slow connection). Don't scare users with "Disconnected" during
-# that window — show "Starting server…" instead. After this expires, fall
-# back to the normal disconnect UI.
-const STARTUP_GRACE_MSEC := 60 * 1000
-
-# Update banner — visible UI only. Release polling, ZIP download, and
-# installation remain root-owned and arrive here as copied presentation state.
-var _update_banner: VBoxContainer
-var _update_label: Label
-var _update_status_label: Label
-var _update_btn: Button
-## The button is an action, never a status line: progress and failure text
-## goes to `_update_status_label`, and the button only enables or disables.
-const _UPDATE_ACTION_TEXT := "Update"
-const _UPDATE_LABEL_COLOR := Color(1.0, 0.85, 0.3)
-var _post_update_action := ""
-## True from the moment an update starts its client migration until the
-## server it then starts is connected. The transport reads "blocked" for
-## that whole window (the migration barrier, then the launch), which is
-## not a fault: name the phase instead of alarming the user (#999).
-var _post_update_server_pending := false
-
-func _ready() -> void:
- _startup_grace_until_msec = Time.get_ticks_msec() + STARTUP_GRACE_MSEC
- _build_ui()
-
-
-func _process(_delta: float) -> void:
- _update_status()
- if _log_viewer != null and _log_viewer.visible:
- log_snapshot_requested.emit(_log_viewer.sequence())
-
-
-func present_transport_snapshot(snapshot: Dictionary) -> void:
- _transport_snapshot = snapshot.duplicate(true)
-
-
-func present_lifecycle_snapshot(snapshot: Dictionary) -> void:
- _lifecycle_snapshot = snapshot.duplicate(true)
-
-
-func present_live_server_probe_result(result: Dictionary) -> void:
- _live_server_probe_result = result.duplicate(true)
-
-
-func present_log_snapshot(snapshot: Dictionary) -> void:
- if _log_viewer != null:
- _log_viewer.present_snapshot(snapshot)
-
-
-func _notification(what: int) -> void:
- # Detect dock/undock by watching for reparenting events.
- if what == NOTIFICATION_PARENTED or what == NOTIFICATION_UNPARENTED:
- _update_redock_visibility.call_deferred()
- elif what == NOTIFICATION_APPLICATION_FOCUS_IN:
- if _should_refresh_client_statuses_on_focus_in():
- _request_client_status_refresh(false)
- ## Re-probe uv only when the user actually went off to install it
- ## (see _on_install_uv). `check_uv_version()` is cached, so an
- ## ungated refresh here would usually be free — but after the
- ## button invalidated that cache it costs one blocking
- ## `uvx --version`, and this notification must not grow a probe on
- ## the common focus-in path. One-shot: clear before refreshing.
- if _uv_recheck_pending:
- _uv_recheck_pending = false
- _refresh_setup_status.call_deferred()
-
-
-## Godot can leave its shared progress dialog under one of our modal windows.
-## Return it before removing the dock: freeing it leaves the editor's pointer dangling.
-func release_editor_progress_dialog() -> void:
- if not is_inside_tree():
- return
- for dialog in find_children("*", "ProgressDialog", true, false):
- dialog.reparent(get_tree().root)
-
-
-func _should_refresh_client_statuses_on_focus_in() -> bool:
- ## Focus-in is part of Godot/editor window activation. Keep automatic refresh,
- ## but only through the async/cooldown-protected path; never run a blocking
- ## client-status sweep directly from this notification.
- return true
-
-
-func _is_floating() -> bool:
- var p := get_parent()
- while p != null:
- if p is Window:
- return p != get_tree().root
- p = p.get_parent()
- return false
-
-
-func _update_redock_visibility() -> void:
- if _redock_btn == null:
- return
- var floating := _is_floating()
- if _redock_btn.visible != floating:
- _redock_btn.visible = floating
-
-
-func _on_redock() -> void:
- # When floating, our Window is NOT the editor root. Closing it triggers
- # Godot's internal dock-return logic (same as clicking the window's X).
- var win := get_window()
- if win != null and win != get_tree().root:
- win.close_requested.emit()
-
-
-func _build_margin_container(margin: int = 12) -> MarginContainer:
- var margin_container := MarginContainer.new()
- margin_container.add_theme_constant_override("margin_left", margin)
- margin_container.add_theme_constant_override("margin_right", margin)
- margin_container.add_theme_constant_override("margin_top", margin)
- margin_container.add_theme_constant_override("margin_bottom", margin)
- return margin_container
-
-
-func _build_ui() -> void:
- add_theme_constant_override("separation", 8)
-
- # --- Top row: status indicator + redock button (when floating) ---
- var status_row := HBoxContainer.new()
- status_row.add_theme_constant_override("separation", 8)
-
- _status_icon = ColorRect.new()
- _status_icon.custom_minimum_size = Vector2(14, 14)
- # Amber on first paint — matches the "Starting server…" label text and
- # distinguishes from a real disconnect (red).
- _status_icon.color = COLOR_AMBER
- var icon_center := CenterContainer.new()
- icon_center.add_child(_status_icon)
- status_row.add_child(icon_center)
-
- _status_label = Label.new()
- # Start in grace state — _update_status_label will take over on the next frame
- # once the connection is available. Never show bare "Disconnected" on
- # first paint because that's misleading while the server is still
- # spinning up.
- _status_label.text = "Starting server…"
- _status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- status_row.add_child(_status_label)
-
- _redock_btn = Button.new()
- _redock_btn.text = "Dock"
- _redock_btn.tooltip_text = "Return this panel to the editor dock"
- _redock_btn.visible = false
- _redock_btn.pressed.connect(_on_redock)
- status_row.add_child(_redock_btn)
-
- add_child(status_row)
-
- # Install-mode line — so a git-clone user doesn't press the yellow Update
- # banner below and silently downgrade from main to the last release tag.
- # See #144.
- _install_label = Label.new()
- _install_label.add_theme_color_override("font_color", COLOR_MUTED)
- _install_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- _install_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _install_label.text = _install_mode_text()
- _install_label.tooltip_text = _install_mode_tooltip()
- _install_label.mouse_filter = Control.MOUSE_FILTER_STOP
- add_child(_install_label)
-
- _body_scroll = ScrollContainer.new()
- _body_scroll.name = "DockBodyScroll"
- _body_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _body_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
- _body_scroll.custom_minimum_size = Vector2(0, 48)
- _body_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
- add_child(_body_scroll)
-
- _body = VBoxContainer.new()
- _body.name = "DockBody"
- _body.add_theme_constant_override("separation", 8)
- _body.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _body_scroll.add_child(_body)
-
- # --- Spawn-failure panel (shown when `_start_server` reports a non-OK
- # state via `get_server_status`). One body paragraph + the matching
- # action; the top status label already carries the state headline.
- _crash_panel = VBoxContainer.new()
- _crash_panel.add_theme_constant_override("separation", 6)
- _crash_panel.visible = false
-
- _crash_output = RichTextLabel.new()
- _crash_output.custom_minimum_size = Vector2(0, 60)
- _crash_output.bbcode_enabled = false
- _crash_output.selection_enabled = true
- _crash_output.scroll_following = false
- _crash_output.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- _crash_output.fit_content = true
- _crash_panel.add_child(_crash_output)
-
- _port_picker_panel = PortPickerPanelScript.new()
- _port_picker_panel.setup()
- _port_picker_panel.port_apply_requested.connect(_on_port_apply_requested)
- _crash_panel.add_child(_port_picker_panel)
-
- _crash_restart_btn = Button.new()
- _crash_restart_btn.text = "Restart Server"
- _crash_restart_btn.tooltip_text = "Stop the old server on this port and start the bundled godot-ai server"
- _crash_restart_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _crash_restart_btn.add_theme_color_override("font_color", Color.WHITE)
- _crash_restart_btn.add_theme_color_override("font_hover_color", Color.WHITE)
- _crash_restart_btn.add_theme_color_override("font_pressed_color", Color.WHITE)
- _crash_restart_btn.pressed.connect(_on_restart_stale_server)
- _crash_restart_btn.visible = false
- _crash_panel.add_child(_crash_restart_btn)
-
- _crash_reload_btn = Button.new()
- _crash_reload_btn.text = "Reload Plugin"
- _crash_reload_btn.tooltip_text = "Re-run the spawn after fixing the underlying issue"
- _crash_reload_btn.pressed.connect(_on_reload_plugin)
- _crash_panel.add_child(_crash_reload_btn)
-
- _crash_docs_btn = Button.new()
- _crash_docs_btn.text = "How to change the port"
- _crash_docs_btn.tooltip_text = "Open the guide: change godot_ai/http_port and reconfigure your MCP clients"
- _crash_docs_btn.visible = false
- _crash_docs_btn.pressed.connect(func(): OS.shell_open(_port_conflict_docs_url()))
- _crash_panel.add_child(_crash_docs_btn)
-
- _crash_panel.add_child(HSeparator.new())
- _body.add_child(_crash_panel)
-
- # --- Update banner (top of dock, hidden until check finds a newer version) ---
- _update_banner = VBoxContainer.new()
- _update_banner.add_theme_constant_override("separation", 4)
- _update_banner.visible = false
-
- _update_label = Label.new()
- _update_label.add_theme_font_size_override("font_size", 15)
- _update_label.add_theme_color_override("font_color", _UPDATE_LABEL_COLOR)
- ## Wrap long banner text (e.g. the < 4.5 support-floor guidance) instead
- ## of letting a single line stretch the whole dock wide. The dock is a
- ## fixed-width side panel, so constrain horizontally and wrap.
- _update_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- _update_label.size_flags_horizontal = Control.SIZE_FILL
- _update_label.custom_minimum_size = Vector2(0, 0)
- _update_banner.add_child(_update_label)
-
- _update_status_label = Label.new()
- _update_status_label.add_theme_font_size_override("font_size", 13)
- _update_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- _update_status_label.size_flags_horizontal = Control.SIZE_FILL
- _update_status_label.custom_minimum_size = Vector2(0, 0)
- _update_status_label.visible = false
- _update_banner.add_child(_update_status_label)
-
- var update_btn_row := HBoxContainer.new()
- update_btn_row.add_theme_constant_override("separation", 6)
-
- _update_btn = Button.new()
- _update_btn.text = _UPDATE_ACTION_TEXT
- _update_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _update_btn.pressed.connect(_on_update_pressed)
- update_btn_row.add_child(_update_btn)
-
- var release_link := Button.new()
- release_link.text = "Release notes"
- release_link.pressed.connect(func(): OS.shell_open(RELEASES_PAGE))
- update_btn_row.add_child(release_link)
-
- _update_banner.add_child(update_btn_row)
- _update_banner.add_child(HSeparator.new())
-
- _body.add_child(_update_banner)
-
- # --- Dev-only connection extras (server label + reload button) ---
- _dev_section = VBoxContainer.new()
- _dev_section.add_theme_constant_override("separation", 6)
- _body.add_child(_dev_section)
-
- _server_label = Label.new()
- _server_label.add_theme_color_override("font_color", COLOR_MUTED)
- _dev_section.add_child(_server_label)
- _refresh_server_label()
-
- var btn_row := HBoxContainer.new()
- btn_row.add_theme_constant_override("separation", 6)
-
- _reload_btn = Button.new()
- _reload_btn.text = "Dev: Reload Plugin"
- _reload_btn.tooltip_text = "Developer utility: reload the GDScript plugin. This does not restart or replace the server."
- _reload_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _reload_btn.pressed.connect(_on_reload_plugin)
- btn_row.add_child(_reload_btn)
-
- _dev_section.add_child(btn_row)
-
- # --- Setup section (dev-only or when uv missing) ---
- _setup_section = VBoxContainer.new()
- _setup_section.add_theme_constant_override("separation", 6)
- _body.add_child(_setup_section)
-
- _setup_section.add_child(HSeparator.new())
- _setup_section.add_child(_make_header("Setup"))
- _setup_container = VBoxContainer.new()
- _setup_container.add_theme_constant_override("separation", 6)
- _setup_section.add_child(_setup_container)
-
- _body.add_child(HSeparator.new())
-
- # --- Clients ---
- var clients_header_row := HBoxContainer.new()
- clients_header_row.add_theme_constant_override("separation", 8)
-
- var clients_header := _make_header("Clients")
- clients_header_row.add_child(clients_header)
-
- _clients_summary_label = Label.new()
- _clients_summary_label.add_theme_color_override("font_color", COLOR_MUTED)
- _clients_summary_label.clip_text = true
- _clients_summary_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
- _clients_summary_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- clients_header_row.add_child(_clients_summary_label)
-
- var clients_actions := HFlowContainer.new()
- clients_actions.add_theme_constant_override("h_separation", 8)
- clients_actions.add_theme_constant_override("v_separation", 4)
-
- var clients_refresh_btn := Button.new()
- clients_refresh_btn.text = "Refresh"
- clients_refresh_btn.tooltip_text = "Refresh client status in the background. Cached status stays visible while checks run."
- clients_refresh_btn.pressed.connect(_on_refresh_clients_pressed)
- clients_actions.add_child(clients_refresh_btn)
-
- var clients_open_btn := Button.new()
- clients_open_btn.text = "Clients & Tools"
- clients_open_btn.tooltip_text = "Open the Clients & Tools window — configure AI clients, choose telemetry preferences, or disable tool domains to fit under a client's hard tool-count cap (e.g. Antigravity's 100)."
- clients_open_btn.pressed.connect(_on_open_clients_window)
- clients_actions.add_child(clients_open_btn)
-
- _body.add_child(clients_header_row)
- _body.add_child(clients_actions)
-
- _client_empty_cta_btn = Button.new()
- _client_empty_cta_btn.text = "Configure an AI client ->"
- _client_empty_cta_btn.tooltip_text = "Open the Clients tab to configure an AI coding client for this Godot AI server."
- _client_empty_cta_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _client_empty_cta_btn.visible = false
- _client_empty_cta_btn.pressed.connect(_on_open_clients_window)
- _body.add_child(_client_empty_cta_btn)
-
- # Drift banner — hidden until a sweep finds at least one mismatched client.
- _drift_banner = VBoxContainer.new()
- _drift_banner.add_theme_constant_override("separation", 4)
- _drift_banner.visible = false
- _drift_label = Label.new()
- _drift_label.add_theme_color_override("font_color", COLOR_AMBER)
- _drift_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- _drift_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _drift_banner.add_child(_drift_label)
- var drift_btn := Button.new()
- drift_btn.text = "Reconfigure mismatched"
- drift_btn.tooltip_text = "Re-run Configure on every client whose stored URL doesn't match the current server URL."
- drift_btn.pressed.connect(_on_reconfigure_mismatched)
- _drift_banner.add_child(drift_btn)
- _body.add_child(_drift_banner)
-
- _clients_window = Window.new()
- _clients_window.title = "Godot AI Settings"
- ## `Vector2i * float` yields Vector2; wrap the result back to Vector2i.
- _clients_window.min_size = Vector2i(Vector2(560, 460) * EditorInterface.get_editor_scale())
- _clients_window.visible = false
- _clients_window.close_requested.connect(_on_clients_window_close_requested)
- add_child(_clients_window)
-
- ## Tabbed secondary window: Clients (per-client rows), Tools (domain-
- ## exclusion checkboxes for clients that cap total tool count, like
- ## Antigravity at 100), and Settings (allow-host LAN opt-in, #507).
- ## Adding another tab is one more _build_*_tab call — no surgery on the
- ## rest of the window.
- var tabs := TabContainer.new()
- tabs.anchor_right = 1.0
- tabs.anchor_bottom = 1.0
- _clients_window.add_child(tabs)
-
- var clients_tab := VBoxContainer.new()
- clients_tab.add_theme_constant_override("separation", 8)
- var clients_margin := _build_margin_container()
- clients_margin.name = "Clients"
- clients_margin.add_child(clients_tab)
- tabs.add_child(clients_margin)
-
- _client_configure_all_btn = Button.new()
- _client_configure_all_btn.text = "Configure all"
- _client_configure_all_btn.tooltip_text = "Configure every client that isn't already pointing at this server"
- _client_configure_all_btn.size_flags_horizontal = Control.SIZE_SHRINK_END
- _client_configure_all_btn.pressed.connect(_on_configure_all_clients)
- clients_tab.add_child(_client_configure_all_btn)
-
- var clients_scroll := ScrollContainer.new()
- clients_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- clients_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
- clients_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
- clients_tab.add_child(clients_scroll)
-
- _client_grid = VBoxContainer.new()
- _client_grid.add_theme_constant_override("separation", 4)
- _client_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- clients_scroll.add_child(_client_grid)
-
- for client_id in ClientConfigurator.client_ids():
- _build_client_row(client_id)
-
- _build_tools_tab(tabs)
- _build_settings_tab(tabs)
-
- _body.add_child(HSeparator.new())
-
- # --- Dev mode toggle (always visible) ---
- var dev_toggle_row := HBoxContainer.new()
- var dev_toggle_label := Label.new()
- dev_toggle_label.text = "Developer mode"
- dev_toggle_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- dev_toggle_row.add_child(dev_toggle_label)
-
- _dev_mode_toggle = CheckButton.new()
- _dev_mode_toggle.button_pressed = _load_dev_mode()
- _dev_mode_toggle.toggled.connect(_on_dev_mode_toggled)
- dev_toggle_row.add_child(_dev_mode_toggle)
- _body.add_child(dev_toggle_row)
-
- # --- Log section (dev-only) ---
- _log_viewer = LogViewerScript.new()
- _log_viewer.setup()
- _log_viewer.logging_enabled_changed.connect(_on_log_logging_enabled_changed)
- _body.add_child(_log_viewer)
-
- # Apply initial dev-mode visibility
- _apply_dev_mode_visibility()
- _refresh_setup_status.call_deferred()
- _perform_initial_client_status_refresh()
-
-
-## Static so `dock_panels/*.gd` subpanels can call it via `McpDock._make_header(...)`
-## without re-declaring identical helpers + COLOR_HEADER constants.
-static func _make_header(text: String) -> Label:
- var label := Label.new()
- label.text = text
- label.add_theme_font_size_override("font_size", 18)
- label.add_theme_color_override("font_color", COLOR_HEADER)
- return label
-
-
-func _build_client_row(client_id: String) -> void:
- var row := HBoxContainer.new()
- row.add_theme_constant_override("separation", 6)
- row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
-
- var dot := ColorRect.new()
- dot.custom_minimum_size = Vector2(10, 10)
- dot.color = COLOR_MUTED
- var dot_center := CenterContainer.new()
- dot_center.add_child(dot)
- row.add_child(dot_center)
-
- var name_label := Label.new()
- name_label.text = ClientConfigurator.client_display_name(client_id)
- name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- ## Every advertised client uses the authenticated local attach bridge.
- var transport_tag := Label.new()
- transport_tag.text = _client_transport_tag(client_id)
- transport_tag.add_theme_color_override("font_color", COLOR_MUTED)
- transport_tag.tooltip_text = "Configure writes a local `godot-ai attach` launch command for this client."
- ## Long error messages from `_verify_post_state` (e.g. "reported remove ok
- ## but verification still reads configured…") used to push the Retry /
- ## Configure button off-screen — the row's Label wanted its full text
- ## width as minimum size, so the buttons got squeezed out. Wrap onto
- ## multiple lines instead so the row keeps its right edge stable and
- ## the buttons remain visible; the user can also read the whole message
- ## without resizing the window.
- name_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
- row.add_child(name_label)
- row.add_child(transport_tag)
-
- var configure_btn := Button.new()
- configure_btn.text = "Configure"
- configure_btn.pressed.connect(_on_configure_client.bind(client_id))
- row.add_child(configure_btn)
-
- var remove_btn := Button.new()
- remove_btn.text = "Remove"
- remove_btn.visible = false
- remove_btn.pressed.connect(_on_remove_client.bind(client_id))
- row.add_child(remove_btn)
-
- # F-3-4: use the authoritative facade so Open/Reveal land on the same
- # file `_check_status_merged` drives (last-wins across project tiers,
- # matching the F2 status fix).
- var config_path := ClientConfigurator.effective_authoritative_path(client_id)
- var open_config_btn := Button.new()
- _apply_editor_icon(open_config_btn, "ExternalLink", "Open")
- open_config_btn.custom_minimum_size = Vector2(28, 28)
- open_config_btn.visible = not config_path.is_empty()
- open_config_btn.pressed.connect(_on_open_config_file.bind(client_id))
- row.add_child(open_config_btn)
-
- var reveal_btn := Button.new()
- _apply_editor_icon(reveal_btn, "Folder", "Reveal")
- reveal_btn.custom_minimum_size = Vector2(28, 28)
- reveal_btn.visible = not config_path.is_empty()
- reveal_btn.pressed.connect(_on_reveal_config_folder.bind(client_id))
- row.add_child(reveal_btn)
-
- _client_grid.add_child(row)
-
- var manual_panel := VBoxContainer.new()
- manual_panel.add_theme_constant_override("separation", 4)
- manual_panel.visible = false
-
- var manual_hint := Label.new()
- manual_hint.text = "Run this manually:"
- manual_hint.add_theme_color_override("font_color", COLOR_MUTED)
- manual_panel.add_child(manual_hint)
-
- var manual_text := TextEdit.new()
- manual_text.editable = false
- manual_text.custom_minimum_size = Vector2(0, 60)
- manual_text.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
- manual_panel.add_child(manual_text)
-
- var copy_btn := Button.new()
- copy_btn.text = "Copy"
- copy_btn.pressed.connect(_on_copy_manual_command.bind(client_id))
- manual_panel.add_child(copy_btn)
-
- _client_grid.add_child(manual_panel)
-
- _client_rows[client_id] = {
- "dot": dot,
- "status": Client.Status.NOT_CONFIGURED,
- "name_label": name_label,
- "configure_btn": configure_btn,
- "remove_btn": remove_btn,
- "open_config_btn": open_config_btn,
- "reveal_btn": reveal_btn,
- "config_path": config_path,
- "manual_panel": manual_panel,
- "manual_text": manual_text,
- }
- _refresh_client_config_file_buttons(client_id)
-
-
-func _apply_editor_icon(button: Button, icon_name: String, fallback_text: String) -> void:
- if has_theme_icon(icon_name, "EditorIcons"):
- button.icon = get_theme_icon(icon_name, "EditorIcons")
- else:
- button.text = fallback_text
-
-
-# --- Status updates ---
-
-func _update_status() -> void:
- ## Signal delivery is synchronous: the composition root copies current
- ## transport/lifecycle values into the presentation methods below before
- ## this frame renders. If no root is attached, the last snapshot remains.
- status_snapshot_requested.emit()
- var connected := bool(_transport_snapshot.get("connected", false))
- var transport_status: Dictionary = _transport_snapshot.get("status", {})
- var server_status := _lifecycle_snapshot
- var state: int = int(server_status.get("state", ServerStateScript.UNINITIALIZED))
- if ServerStateScript.blocks_client_health(state):
- connected = false
- if connected:
- _post_update_server_pending = false
-
- ## One `match`/`elif` chain, one source of truth. Adding a new
- ## spawn outcome = one `ServerStateScript` constant + one arm here +
- ## one body string in `_crash_body_for_state`.
- ## Default covers both a missing/old Connection instance and an unknown
- ## future transport phase. Every recognized state below overrides it, so
- ## startup grace and settled disconnect have one rendering path.
- var inside_startup_grace := Time.get_ticks_msec() < _startup_grace_until_msec
- var status_text := "Starting server…" if inside_startup_grace else "Disconnected"
- var status_color := COLOR_AMBER if inside_startup_grace else Color.RED
- if _server_restart_in_progress:
- status_text = "Restarting server..."
- status_color = COLOR_AMBER
- elif connected:
- status_text = _connected_status_text()
- status_color = Color.GREEN
- elif bool(server_status.get("handoff_retry_pending", false)):
- status_text = "Recovering after update…"
- status_color = COLOR_AMBER
- elif state == ServerStateScript.CRASHED:
- var exit_ms: int = server_status.get("exit_ms", 0)
- status_text = "Server exited after %.1fs" % (exit_ms / 1000.0)
- status_color = Color.RED
- elif state == ServerStateScript.PORT_EXCLUDED:
- status_text = "Port %d reserved by Windows" % ClientConfigurator.http_port()
- status_color = Color.RED
- elif state == ServerStateScript.INCOMPATIBLE:
- status_text = "Incompatible server on port %d" % ClientConfigurator.http_port()
- status_color = Color.RED
- elif state == ServerStateScript.FOREIGN_PORT:
- ## #647: the post-crash probe names the actual conflicting port
- ## (HTTP or WS) — don't blame port 8000 when 9500 is the occupant.
- var conflict_port: int = int(server_status.get("conflict_port", 0))
- if conflict_port <= 0:
- conflict_port = ClientConfigurator.http_port()
- status_text = "Port %d held by another process" % conflict_port
- status_color = Color.RED
- elif state == ServerStateScript.NO_COMMAND:
- status_text = "No server command found"
- status_color = Color.RED
- elif _post_update_server_pending:
- ## Every terminal spawn failure matched above; what is left is the
- ## post-update window where the server is being brought back.
- status_text = "Finishing update — starting server…"
- status_color = COLOR_AMBER
- elif state == ServerStateScript.SPAWNING:
- status_text = "Starting server…"
- status_color = COLOR_AMBER
- elif not transport_status.is_empty():
- var transport_phase := str(transport_status.get("phase", ""))
- if transport_phase == "connecting":
- status_text = _transport_status_text(transport_status)
- status_color = COLOR_AMBER
- elif transport_phase == "retrying":
- status_text = _transport_status_text(transport_status)
- status_color = COLOR_AMBER
- elif transport_phase == "closing":
- status_text = _transport_status_text(transport_status)
- status_color = COLOR_AMBER
- elif transport_phase == "blocked":
- ## Exact terminal labels come from lifecycle state above. This is a
- ## generic fallback for a blocked connection without a diagnosis.
- status_text = _transport_status_text(transport_status)
- status_color = Color.RED
-
- ## keep_server_on_exit (#800): the reaper env opt-outs are staged at
- ## spawn, so a mid-session toggle only lands on the next server start —
- ## say so while the running server still carries the old behavior.
- if connected and ClientConfigurator.keep_server_on_exit() != bool(server_status.get("keep_alive", false)):
- status_text += " — keep-server-on-exit applies after Restart"
-
- _update_crash_panel(server_status)
- _refresh_server_version_label(server_status)
- _refresh_server_label(server_status)
-
- ## A transient disconnect reason remains in the transport snapshot until
- ## handshake_ack. Once the dock renders the connection as OPEN, do not pair
- ## its green label with the previous peer's recovery diagnostic.
- var status_tooltip := "" if connected else str(transport_status.get("reason", ""))
- var changed: bool = (
- connected != _last_connected
- or status_text != _last_status_text
- or status_tooltip != _last_status_tooltip
- )
- if not changed:
- return
- var just_connected: bool = connected and not _last_connected
- _last_connected = connected
- _last_status_text = status_text
- _last_status_tooltip = status_tooltip
- _status_icon.color = status_color
- _status_label.text = status_text
- _status_label.tooltip_text = status_tooltip
- if just_connected:
- ## #739: the server just came up. If the startup uv probe failed
- ## (the reporter's screenshot: green "Server connected" beside a
- ## red "uv: not found" row), the failure was transient — re-probe
- ## instead of pinning the red row for the whole session. Runs
- ## AFTER the label writes above and via the deferred queue, so the
- ## status-machine state is committed before the probe can block.
- _schedule_uv_reprobe()
-
- ## Status transitions are exactly when "is the launch still settling?"
- ## can change (Starting server… -> connected / Disconnected / terminal
- ## diagnosis), so re-evaluate the Setup section's visibility here (#744).
- ## Cheap: runs only on `changed`, and the uv probe result is cached.
- _apply_dev_mode_visibility()
-
- _update_dev_section_buttons()
-
-
-## Render the diagnostic panel body for a given spawn state. The top
-## status label already names the problem; this answers "what do I do?".
-## Panel shows for any non-OK state; picker shows only when moving the HTTP
-## port alone is a valid recovery. Incompatible godot-ai servers commonly
-## hold both HTTP and WS ports, so their message points to Editor Settings
-## instead of offering the HTTP-only quick picker.
-func _update_crash_panel(server_status: Dictionary) -> void:
- var state: int = int(server_status.get("state", ServerStateScript.UNINITIALIZED))
- if not ServerStateScript.is_terminal_diagnosis(state):
- if _crash_panel.visible:
- _crash_panel.visible = false
- _last_server_status = {}
- return
- if server_status == _last_server_status:
- return
- _last_server_status = server_status.duplicate()
- _crash_panel.visible = true
- _crash_output.clear()
- _crash_output.add_text(_crash_body_for_state(state, server_status))
- var show_recovery_restart := (
- state == ServerStateScript.INCOMPATIBLE
- and bool(server_status.get("can_recover_incompatible", false))
- )
- if _crash_restart_btn != null:
- _crash_restart_btn.visible = show_recovery_restart
- _crash_restart_btn.disabled = _server_restart_in_progress
- _crash_restart_btn.text = "Restarting..." if _server_restart_in_progress else "Restart Server"
- if _crash_reload_btn != null:
- _crash_reload_btn.visible = (
- not show_recovery_restart
- and state != ServerStateScript.INCOMPATIBLE
- )
- ## Docs link only for the genuinely-foreign occupant: a recoverable
- ## (older godot-ai) server gets Restart Server instead, and the inline
- ## body already names a free port — the link carries the per-client
- ## reconfigure steps that don't fit inline.
- if _crash_docs_btn != null:
- _crash_docs_btn.visible = (
- state == ServerStateScript.INCOMPATIBLE
- and not bool(server_status.get("can_recover_incompatible", false))
- )
-
- ## The picker moves both ports (#647 hid it for a WebSocket-side
- ## conflict when it could only move the HTTP port), seeded with the
- ## diagnosed conflict so only the contested port changes.
- var conflict_port := int(server_status.get("conflict_port", 0))
- var port_picker_visible := (
- state == ServerStateScript.PORT_EXCLUDED or state == ServerStateScript.FOREIGN_PORT
- or (state == ServerStateScript.INCOMPATIBLE
- and not bool(server_status.get("can_recover_incompatible", false)))
- )
- _port_picker_panel.visible = port_picker_visible
- if port_picker_visible:
- _port_picker_panel.seed_suggested_ports(conflict_port)
-
-
-static func _crash_body_for_state(state: int, server_status: Dictionary = {}) -> String:
- ## Single sentence per state. The top status label already names the
- ## problem; don't repeat it here. This copy answers "what do I do?".
- var port := ClientConfigurator.http_port()
- match state:
- ServerStateScript.PORT_EXCLUDED:
- return "Windows (Hyper-V / WSL2 / Docker) reserved port %d. Pick a free port or try `net stop winnat; net start winnat` in an admin shell." % port
- ServerStateScript.INCOMPATIBLE:
- var message := str(server_status.get("message", ""))
- if bool(server_status.get("can_recover_incompatible", false)):
- var expected := str(server_status.get("expected_version", ""))
- if expected.is_empty():
- expected = ClientConfigurator.get_plugin_version()
- if not message.is_empty():
- return "%s Click Restart Server below to replace it with godot-ai v%s." % [message, expected]
- return "Port %d is occupied by an older godot-ai server. Click Restart Server below to replace it with godot-ai v%s." % [port, expected]
- ## Genuinely foreign occupant (no recovery proof). Name a concrete
- ## free port so the user doesn't have to hunt for one, and let the
- ## crash panel's "How to change the port" link carry the per-client
- ## reconfigure steps. `suggest_free_port` already routes through the
- ## Windows reservation table, so the named port won't itself fail
- ## with WinError 10013.
- var hint := _free_port_hint(port)
- if not message.is_empty():
- return "%s %s" % [message, hint]
- return "Port %d is occupied by an incompatible server. %s" % [port, hint]
- ServerStateScript.FOREIGN_PORT:
- ## #647: prefer the lifecycle's diagnosis (it names the right
- ## port — HTTP vs WS — and the Editor Setting to change) over
- ## the generic HTTP-port fallback.
- var foreign_message := str(server_status.get("message", ""))
- if not foreign_message.is_empty():
- return foreign_message
- return "Another process is already bound to port %d. Pick a free port or stop the other process." % port
- ServerStateScript.CRASHED:
- ## #805: a specific crash diagnosis from the lifecycle (e.g. the
- ## flapping-occupant latch) beats the generic launch-mode copy.
- ## Generic crash paths clear the message, so stale text from an
- ## earlier state can't leak in here.
- var crash_message := str(server_status.get("message", ""))
- if not crash_message.is_empty():
- return crash_message
- ## Both spawn attempts failed on the uvx tier — stock releases:
- ## PyPI lag. Local builds (version with +metadata): almost always the
- ## dev venv was not found (unresolved junction/symlink) so uvx tried
- ## a pin that may lack checkout-local extras.
- if ClientConfigurator.get_server_launch_mode() == "uvx":
- var version := ClientConfigurator.get_plugin_version()
- var pin := ClientConfigurator._pypi_pin_version(version)
- if pin != version:
- ## `%` binds tighter than `+` in GDScript — format the fully
- ## concatenated string, never the last fragment alone.
- return (
- "The server exited before the WebSocket handshake. "
- + "Local plugin version is %s (PEP 440 local build metadata) — uvx pins PyPI godot-ai==%s. "
- + "If you need checkout-local server code, ensure addons/godot_ai resolves to your "
- + "dev tree (symlink/junction) with a `.venv`, or set GODOT_AI_VENV_PYTHON to that "
- + "venv's python binary, then Reload Plugin. Log should show 'MCP | using dev venv: ...'."
- ) % [version, pin]
- return (
- "The server exited before the WebSocket handshake, even after a `uvx --refresh` retry. "
- + "If this is a brand-new release, PyPI's index may still be propagating (~10 min). "
- + "Wait a moment and click Reload Plugin to retry, or check Godot's output log for Python's traceback. "
- + "Target: godot-ai==%s."
- ) % pin
- return "The server exited before the WebSocket handshake. Check Godot's output log (bottom panel) for Python's traceback."
- ServerStateScript.NO_COMMAND:
- return "No godot-ai server found. Install `uv` via the Setup panel above, or run `pip install godot-ai`."
- _:
- return ""
-
-
-## One sentence naming concrete free ports for the user to switch to. Names
-## BOTH http and ws: this branch also fires for an incompatible godot-ai
-## server we can't prove we own, which commonly holds both ports — moving only
-## http would then leave the new server unable to bind ws. Both suggestions are
-## routed through `suggest_free_port` so they clear Windows' winnat reservation
-## table (no point suggesting a port that 10013s on bind). Both ports reach
-## the client's attach command, so clients must be reconfigured afterwards.
-## The per-client reconfigure steps live behind the crash panel's docs link.
-static func _free_port_hint(port: int) -> String:
- var free_http := ClientConfigurator.suggest_free_port(port + 1)
- var free_ws := ClientConfigurator.suggest_free_port(ClientConfigurator.ws_port() + 1)
- return "Suggested ports: %d (HTTP) and %d (WS). Choose both ports below, click Apply + Reload, then Configure your AI clients to use the new pair." % [free_http, free_ws]
-
-
-## URL for the port-conflict guide, pinned to the release tag that matches the
-## installed plugin version (releases are tagged `v`). The crash-panel
-## button only exists in builds that ship `docs/port-conflicts.md`, so the
-## versioned ref always resolves — and a shipped build never points users at a
-## tip-of-main guide that has drifted from its own UI.
-static func _port_conflict_docs_url() -> String:
- var version := ClientConfigurator.get_plugin_version()
- var git_ref := ("v%s" % version) if not version.is_empty() else "main"
- return "%s/%s/%s" % [REPO_BLOB_BASE, git_ref, PORT_CONFLICT_DOCS_PATH]
-
-
-## Signal handler for the extracted LogViewer — the panel owns its own
-## display visibility, the dock owns logging routing. Routes to BOTH the
-## dispatcher (gates [recv]/[send] recording) and the log buffer's console
-## echo — the connection logs [event]/[defer] lines directly to the buffer,
-## bypassing the dispatcher, so gating only `mcp_logging` left the console
-## spamming with the toggle off (#626). Ring recording is unaffected, so
-## the dock's log panel keeps working while the console stays quiet.
-func _on_log_logging_enabled_changed(enabled: bool) -> void:
- mcp_logging_changed.emit(enabled)
-
-
-## Signal handler for the extracted PortPickerPanel. The replaceable Dock emits
-## a copied value intent; the composition root owns persistence and reload.
-func _on_port_apply_requested(new_http_port: int, new_ws_port: int) -> void:
- settings_apply_requested.emit({"http_port": new_http_port, "ws_port": new_ws_port}, true)
-
-
-func _refresh_server_label(server_status: Dictionary = {}) -> void:
- if _server_label == null:
- return
- var ws_port := int(_lifecycle_snapshot.get("resolved_ws_port", 0))
- if ws_port <= 0:
- ws_port = ClientConfigurator.ws_port()
- var text := "WS: %d HTTP: %d" % [ws_port, ClientConfigurator.http_port()]
- if server_status.is_empty():
- server_status = _lifecycle_snapshot
- var ownership := _server_ownership_tag(
- int(server_status.get("state", ServerStateScript.UNINITIALIZED)),
- int(server_status.get("server_pid", -1)),
- )
- if not ownership.is_empty():
- text += " · %s" % ownership
- _server_label.text = text
-
-
-## #838/#816 step 11: name which backend flavor the editor is riding.
-## Diagnostic display only — never kill proof (external adoption clears PID
-## authority, see server_lifecycle.gd::adopt_compatible_server / #669).
-static func _server_ownership_tag(state: int, server_pid: int) -> String:
- if state != ServerStateScript.READY:
- return ""
- return "plugin-managed backend" if server_pid > 0 else "externally adopted backend"
-
-
-static func _client_transport_tag(client_id: String) -> String:
- var client := ClientRegistry.get_by_id(client_id)
- if client == null:
- return ""
- return "attach"
-
-
-# --- Telemetry setting persistence ---
-
-
-## Returns true if GODOT_AI_DISABLE_TELEMETRY or DISABLE_TELEMETRY is set
-## to a truthy value, false if either is set and non-truthy, null if neither
-## env var is present at all.
-func _is_telemetry_disabled_via_env() -> Variant:
- if not (OS.has_environment("GODOT_AI_DISABLE_TELEMETRY") or OS.has_environment("DISABLE_TELEMETRY")):
- return null
- return McpSettings.env_truthy("GODOT_AI_DISABLE_TELEMETRY") or McpSettings.env_truthy("DISABLE_TELEMETRY")
-
-
-## Reads the telemetry preference, applying env-var override when present.
-## Initialises _telemetry_pending_enabled / _telemetry_saved_enabled and
-## sets the checkbox state + locked tooltip. Call after _telemetry_toggle
-## has been created.
-func _load_telemetry_setting() -> void:
- var env_disabled = _is_telemetry_disabled_via_env()
-
- var enabled: bool
- if env_disabled != null:
- ## Environment policy controls this activation but does not rewrite the
- ## user's persisted preference.
- enabled = not bool(env_disabled)
- else:
- ## Defaults are registered by the composition root before Dock creation.
- var es := EditorInterface.get_editor_settings()
- if es != null and es.has_setting(McpSettings.SETTING_TELEMETRY_ENABLED):
- enabled = bool(es.get_setting(McpSettings.SETTING_TELEMETRY_ENABLED))
- else:
- enabled = true
-
- _telemetry_pending_enabled = enabled
- _telemetry_saved_enabled = enabled
-
- if _telemetry_toggle == null:
- return
- _telemetry_toggle.set_pressed_no_signal(enabled)
- if env_disabled != null:
- _telemetry_toggle.disabled = true
- _telemetry_toggle.tooltip_text = (
- "Telemetry is controlled by an environment variable "
- + "(GODOT_AI_DISABLE_TELEMETRY / DISABLE_TELEMETRY)."
- )
- else:
- _telemetry_toggle.disabled = false
- _telemetry_toggle.tooltip_text = _live_telemetry_tooltip(enabled)
-
-
-func _on_telemetry_toggled(pressed: bool) -> void:
- _telemetry_pending_enabled = pressed
- if _telemetry_toggle != null:
- _telemetry_toggle.tooltip_text = _live_telemetry_tooltip(pressed)
- _refresh_tools_ui_state()
-
-
-## Report the running server's telemetry state, not just this editor's
-## checkbox, so the two can never silently disagree (#913). Applying an
-## opt-out reaches the live server over the authenticated WebSocket and
-## latches there, so both directions of a disagreement are real states worth
-## explaining — not just the unreachable one.
-func _live_telemetry_tooltip(local_enabled: bool) -> String:
- _live_server_probe_result = {}
- live_server_probe_requested.emit(ClientConfigurator.http_port())
- var live := _live_server_probe_result
- if not (live.get("telemetry_enabled") is bool):
- ## Absent means "too old to publish it", not false. Say nothing.
- return ""
- var server_enabled: bool = live.get("telemetry_enabled")
- if server_enabled == local_enabled:
- return "Running server telemetry is %s." % ("on" if server_enabled else "off")
- if local_enabled:
- return (
- "The running server has telemetry off and will stay off until it is "
- + "replaced. An opt-out — this editor's earlier one, another editor "
- + "sharing this server, or GODOT_AI_DISABLE_TELEMETRY in its "
- + "environment — latched it. Restart the server to apply this."
- )
- return (
- "The running server still has telemetry on. Applying sends it an "
- + "opt-out over the authenticated connection, which it honors for the "
- + "rest of its life."
- )
-
-
-# --- Dev mode persistence ---
-
-
-func _load_dev_mode() -> bool:
- # Default OFF for every install (including dev checkouts). Contributors
- # who want the extra diagnostic UI (Reload Plugin, MCP log
- # panel, Start/Stop Dev Server) can flip the toggle once — editor
- # settings persist across sessions.
- var es := EditorInterface.get_editor_settings()
- if es == null:
- return false
- if not es.has_setting(DEV_MODE_SETTING):
- es.set_setting(DEV_MODE_SETTING, false)
- return false
- return bool(es.get_setting(DEV_MODE_SETTING))
-
-
-func _on_dev_mode_toggled(enabled: bool) -> void:
- var es := EditorInterface.get_editor_settings()
- if es != null:
- es.set_setting(DEV_MODE_SETTING, enabled)
- _apply_dev_mode_visibility()
- _refresh_setup_status()
-
-
-func _apply_dev_mode_visibility() -> void:
- if _dev_mode_toggle == null:
- return ## dock UI not built yet (unit tests, teardown window)
- var dev := _dev_mode_toggle.button_pressed
- _dev_section.visible = dev
- if _log_viewer != null:
- _log_viewer.visible = dev
- # Setup section: visible in dev mode, OR in user mode when uv is missing
- # (so users can install uv from the dock) — but not while the server
- # launch is still settling (#744): mid-launch a red "uv: not found" row
- # is usually a transient probe failure (#739) or irrelevant because the
- # launch is succeeding via the .venv or system tiers. `_update_status_label`
- # re-applies visibility on every status transition, so the section
- # appears the moment the launch outcome makes it relevant.
- var is_dev := ClientConfigurator.is_dev_checkout()
- var uv_missing := not is_dev and ClientConfigurator.check_uv_version().is_empty()
- _setup_section.visible = _setup_section_should_show(dev, uv_missing, _server_launch_pending())
-
-
-## Pure visibility decision for the Setup section (#744). Split out so the
-## truth table is unit-testable without faking the uv probe or a dev
-## checkout: dev toggle always shows the section; a missing uv only shows
-## it once the server launch has settled.
-static func _setup_section_should_show(
- dev_toggle: bool, uv_missing: bool, launch_pending: bool
-) -> bool:
- return dev_toggle or (uv_missing and not launch_pending)
-
-
-## True while the server launch outcome is still unknown: not connected,
-## no terminal diagnosis yet, and the startup grace window ("Starting
-## server…" in the status row) is still running. Mirrors the status-label
-## logic in `_update_status_label` so the Setup section and the amber status
-## text agree on what "still launching" means.
-func _server_launch_pending() -> bool:
- if _last_connected:
- return false
- var state := int(_lifecycle_snapshot.get("state", ServerStateScript.UNINITIALIZED))
- if ServerStateScript.is_terminal_diagnosis(state):
- return false
- return Time.get_ticks_msec() < _startup_grace_until_msec
-
-
-# --- Button handlers ---
-
-
-func _on_reload_plugin() -> void:
- plugin_reload_requested.emit("dock_button")
-
-
-## Setup-section "Server" row: always report the TRUE running server
-## version (from the handshake_ack) rather than the plugin's expected
-## version, and highlight the mismatch so self-update drift is visible
-## at a glance instead of silently masked by a green label.
-##
-## Render states, keyed off live version metadata:
-## - empty (pre-ack): show the expected version only as an unverified target
-## - matches plugin: show it green, no Restart button
-## - dev mismatch: show amber with an explicit dev marker
-## - release mismatch: show actual vs expected; only surface Restart when the
-## plugin has ownership proof for the process
-func _refresh_server_version_label(server_status: Dictionary = {}) -> void:
- if _setup_server_label == null:
- return
- var plugin_ver := ClientConfigurator.get_plugin_version()
- if server_status.is_empty():
- server_status = _lifecycle_snapshot
- var server_ver := str(_transport_snapshot.get("server_version", ""))
- if server_ver.is_empty():
- server_ver = str(server_status.get("actual_version", ""))
- var expected_ver := str(server_status.get("expected_version", ""))
- if expected_ver.is_empty():
- expected_ver = plugin_ver
- var state: int = int(server_status.get("state", ServerStateScript.UNINITIALIZED))
- if _server_restart_in_progress and (
- server_ver == expected_ver
- or (
- ServerStateScript.is_terminal_diagnosis(state)
- and state != ServerStateScript.INCOMPATIBLE
- )
- ):
- _server_restart_in_progress = false
- var text: String
- var color: Color
- var show_restart := false
- if _server_restart_in_progress:
- text = "restarting server..."
- color = COLOR_AMBER
- show_restart = true
- elif server_ver.is_empty():
- text = "checking live version (expected godot-ai == %s)" % expected_ver
- color = COLOR_MUTED
- elif server_ver == expected_ver:
- text = "godot-ai == %s" % server_ver
- color = Color.GREEN
- else:
- text = "godot-ai == %s (expected %s)" % [server_ver, expected_ver]
- var is_incompatible: bool = state == ServerStateScript.INCOMPATIBLE
- color = Color.RED if is_incompatible else COLOR_AMBER
- var has_managed_proof := bool(server_status.get("can_restart_managed", false))
- var can_recover: bool = bool(server_status.get("can_recover_incompatible", false))
- show_restart = (
- (not is_incompatible and has_managed_proof)
- ## Recoverable incompatible servers get the primary action in
- ## the top error panel. Duplicating it in Setup made the UI
- ## look like it had multiple restart paths.
- or (is_incompatible and can_recover and _crash_restart_btn == null)
- )
- if text == _last_rendered_server_text:
- _setup_server_label.add_theme_color_override("font_color", color)
- _update_restart_button(show_restart)
- return
- _last_rendered_server_text = text
- _setup_server_label.text = text
- _setup_server_label.add_theme_color_override("font_color", color)
- _update_restart_button(show_restart)
-
-
-func _update_restart_button(visible: bool) -> void:
- if _version_restart_btn != null:
- _version_restart_btn.visible = visible
- _version_restart_btn.disabled = _server_restart_in_progress
- _version_restart_btn.text = "Restarting..." if _server_restart_in_progress else "Restart"
- if _crash_restart_btn != null:
- _crash_restart_btn.disabled = _server_restart_in_progress
- _crash_restart_btn.text = "Restarting..." if _server_restart_in_progress else "Restart Server"
-
-
-func _on_restart_stale_server() -> void:
- if _server_restart_in_progress:
- return
- _server_restart_in_progress = true
- _last_rendered_server_text = ""
- _refresh_server_version_label()
- if not is_inside_tree():
- _dispatch_stale_server_restart()
- _server_restart_in_progress = false
- _last_rendered_server_text = ""
- _refresh_server_version_label()
- return
- call_deferred("_restart_stale_server_after_feedback")
-
-
-func _restart_stale_server_after_feedback() -> void:
- await get_tree().create_timer(0.15).timeout
- if not _dispatch_stale_server_restart():
- _server_restart_in_progress = false
- _last_rendered_server_text = ""
- _refresh_server_version_label()
-
-
-func _dispatch_stale_server_restart() -> bool:
- if int(_lifecycle_snapshot.get("state", ServerStateScript.UNINITIALIZED)) == ServerStateScript.INCOMPATIBLE:
- lifecycle_action_requested.emit(LifecycleAction.RECOVER_INCOMPATIBLE)
- return true
- if not bool(_lifecycle_snapshot.get("can_restart_managed", false)):
- return false
- lifecycle_action_requested.emit(LifecycleAction.RESTART_SERVER)
- return true
-
-
-## Root acknowledgement for an asynchronous lifecycle intent. Successful
-## restarts stay busy until the next lifecycle/transport snapshot settles;
-## rejection restores the controls immediately.
-func present_lifecycle_action_result(accepted: bool) -> void:
- if accepted:
- return
- _server_restart_in_progress = false
- _last_rendered_server_text = ""
- _refresh_server_version_label()
-
-
-# --- Setup section ---
-
-## #739: a `uvx --version` probe that failed once at editor startup used
-## to pin "uv: not found" for the whole session — the Install-uv click
-## was the only invalidation path, so the fix users discovered was
-## re-clicking Install on every launch. Re-probe on events that suggest
-## the failure was transient (server-connect transition, manual Refresh).
-## No-op once uv has been found, so this costs nothing in the healthy
-## steady state; when uv is genuinely absent, the re-probe is a fast
-## negative (CliFinder's well-known-dir walk plus one bounded `where`).
-## Runs on the main thread like the initial probe — same wall-clock
-## bound, and the triggering events are rare (once per connect / click).
-##
-## Callers go through _schedule_uv_reprobe() rather than calling this
-## inline: the cache-miss probe shells out (bounded at 3s) on the
-## calling thread, and both call sites sit mid-flow in UI handlers —
-## the connect transition wants its status-label writes committed
-## first, and the Refresh click wants the client sweep dispatched
-## without waiting on the probe. Same deferred convention as
-## _on_install_uv. (The deferred queue still flushes on the main
-## thread, so a worst-case 3s probe delays that frame — acceptable for
-## a rare, bounded event; a worker thread would be the heavier cure.)
-func _schedule_uv_reprobe() -> void:
- _reprobe_uv_if_negative.call_deferred()
-
-
-func _reprobe_uv_if_negative() -> void:
- if not ClientConfigurator.uv_probe_negative():
- return
- ClientConfigurator.invalidate_uv_detection()
- _refresh_setup_status()
- _apply_dev_mode_visibility()
-
-
-func _refresh_setup_status() -> void:
- if _setup_container == null:
- return
- for child in _setup_container.get_children():
- child.queue_free()
- _dev_primary_btn = null
- _dev_stop_btn = null
-
- var is_dev := ClientConfigurator.is_dev_checkout()
- if is_dev:
- _setup_container.add_child(_make_status_row("Mode", "Dev (venv)", Color.CYAN))
-
- var btn_row := HBoxContainer.new()
- btn_row.add_theme_constant_override("separation", 4)
- btn_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
-
- _dev_primary_btn = Button.new()
- _dev_primary_btn.text = "Restart Managed Server"
- _dev_primary_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _dev_primary_btn.pressed.connect(_on_dev_primary_pressed)
- btn_row.add_child(_dev_primary_btn)
-
- _dev_stop_btn = Button.new()
- _dev_stop_btn.text = "✕"
- _dev_stop_btn.tooltip_text = "Stop the exact plugin-owned server."
- _dev_stop_btn.pressed.connect(_on_dev_stop_pressed)
- btn_row.add_child(_dev_stop_btn)
-
- _setup_container.add_child(btn_row)
- _update_dev_section_buttons()
- return
-
- # User mode — check for uv
- var uv_version := ClientConfigurator.check_uv_version()
- if not uv_version.is_empty():
- var compact_uv_version := _compact_uv_version_text(uv_version)
- var uv_tooltip := uv_version if compact_uv_version != uv_version else ""
- _setup_container.add_child(_make_status_row("uv", compact_uv_version, Color.GREEN, uv_tooltip))
- ## Build the Server row with a placeholder label we can update every
- ## frame. `_refresh_server_version_label` replaces the text + color
- ## once `McpConnection.server_version` lands via `handshake_ack`, and
- ## flips to amber + "(plugin X)" on drift. Pre-ack we show the
- ## plugin's expected version so the row isn't blank.
- var server_row := HBoxContainer.new()
- server_row.add_theme_constant_override("separation", 8)
- var key_label := Label.new()
- key_label.text = "Server"
- key_label.add_theme_color_override("font_color", COLOR_MUTED)
- key_label.custom_minimum_size = Vector2(60, 0)
- server_row.add_child(key_label)
- _setup_server_label = Label.new()
- _setup_server_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- server_row.add_child(_setup_server_label)
- _version_restart_btn = Button.new()
- _version_restart_btn.text = "Restart"
- _version_restart_btn.tooltip_text = "Kill the server on port %d and respawn with the plugin's bundled version" % ClientConfigurator.http_port()
- _version_restart_btn.pressed.connect(_on_restart_stale_server)
- _version_restart_btn.visible = false
- server_row.add_child(_version_restart_btn)
- _setup_container.add_child(server_row)
- _last_rendered_server_text = ""
- _refresh_server_version_label()
- else:
- _setup_container.add_child(_make_status_row("uv", "not found", Color.RED))
- var install_btn := Button.new()
- install_btn.text = "How to install uv"
- install_btn.tooltip_text = (
- "Opens the official uv installation docs. Godot AI deliberately does "
- + "not run the installer for you — see _on_install_uv."
- )
- install_btn.pressed.connect(_on_install_uv)
- _setup_container.add_child(install_btn)
-
-
-func _install_mode_text() -> String:
- if ClientConfigurator.is_dev_checkout():
- return "Install: dev checkout — update via git pull"
- return "Install: v%s" % ClientConfigurator.get_plugin_version()
-
-
-func _install_mode_tooltip() -> String:
- if not ClientConfigurator.is_dev_checkout():
- return "Plugin installed from a release ZIP, Asset Library, or source copy. Update button in this dock downloads the latest GitHub release."
- var target := _resolve_plugin_symlink_target()
- if target.is_empty():
- return "Plugin source tree resolved via local .venv — press Reload Plugin after editing."
- return "Plugin source: %s\nPress Reload Plugin after editing." % target
-
-
-func _resolve_plugin_symlink_target() -> String:
- var logical := ProjectSettings.globalize_path("res://addons/godot_ai").rstrip("/").rstrip("\\")
- var resolved := ClientConfigurator.resolve_addons_realpath()
- if resolved.is_empty() or resolved == logical:
- return ""
- return resolved
-
-
-static func _compact_uv_version_text(uv_version: String) -> String:
- var text := uv_version.strip_edges()
- if text.ends_with(")"):
- var metadata_start := text.rfind(" (")
- if metadata_start >= 0:
- return text.substr(0, metadata_start).strip_edges()
- return text
-
-
-func _make_status_row(
- label_text: String,
- value_text: String,
- value_color: Color,
- tooltip_text: String = ""
-) -> HBoxContainer:
- var row := HBoxContainer.new()
- row.add_theme_constant_override("separation", 6)
- if not tooltip_text.is_empty():
- row.tooltip_text = tooltip_text
-
- var label := Label.new()
- label.text = label_text
- label.add_theme_color_override("font_color", COLOR_MUTED)
- label.custom_minimum_size.x = 60
- if not tooltip_text.is_empty():
- label.tooltip_text = tooltip_text
- row.add_child(label)
-
- var value := Label.new()
- value.text = value_text
- value.add_theme_color_override("font_color", value_color)
- if not tooltip_text.is_empty():
- value.tooltip_text = tooltip_text
- row.add_child(value)
-
- return row
-
-
-## Pure view of the lifecycle's process authority. "External" means transport
-## may be adopted, but this plugin has no right to stop its launcher.
-static func _dev_primary_btn_state(managed: bool, external: bool) -> Dictionary:
- var port := ClientConfigurator.http_port()
- if managed:
- return {
- "text": "Restart Managed Server",
- "enabled": true,
- "tooltip": "Restart the exact plugin-owned server on port %d." % port,
- }
- if external:
- return {
- "text": "External Server Running",
- "enabled": false,
- "tooltip": "Stop the server on port %d from the process that launched it." % port,
- }
- return {
- "text": "Start Managed Server",
- "enabled": true,
- "tooltip": "Start a plugin-owned server on port %d from the current checkout." % port,
- }
-
-
-static func _dev_stop_btn_state(managed: bool) -> Dictionary:
- if managed:
- return {"enabled": true, "tooltip": "Stop the exact plugin-owned server."}
- return {"enabled": false, "tooltip": "No plugin-owned server to stop."}
-
-
-func _on_dev_primary_pressed() -> void:
- if _server_restart_in_progress:
- return
- _server_restart_in_progress = true
- _update_dev_section_buttons()
- if not is_inside_tree():
- ## Test path — no scene tree means no timer; run synchronously
- ## so suite assertions see the dispatch without `await`.
- dev_server_action_requested.emit(DevServerAction.START_OR_RESTART)
- _server_restart_in_progress = false
- return
- call_deferred("_perform_dev_restart_after_feedback")
-
-
-func _on_dev_stop_pressed() -> void:
- dev_server_action_requested.emit(DevServerAction.STOP)
- _update_dev_section_buttons.call_deferred()
-
-
-func _perform_dev_restart_after_feedback() -> void:
- ## Brief paint cycle so the user sees "Restarting..." before dispatch.
- await get_tree().create_timer(0.15).timeout
- dev_server_action_requested.emit(DevServerAction.START_OR_RESTART)
- ## Lifecycle snapshots normally repaint first; this is a bounded UI fallback.
- await get_tree().create_timer(2.0).timeout
- _server_restart_in_progress = false
- _update_dev_section_buttons()
-
-
-## Derive both buttons from the already-copied lifecycle value. No second port
-## scan or dev-only ownership state exists.
-func _update_dev_section_buttons() -> void:
- var managed := int(_lifecycle_snapshot.get("server_pid", -1)) > 1
- var normal_start_released := bool(
- _lifecycle_snapshot.get("normal_start_released", false)
- )
- var state := int(_lifecycle_snapshot.get("state", ServerStateScript.UNINITIALIZED))
- var external := not managed and (
- str(_lifecycle_snapshot.get("ready_kind", "")) == "adopted"
- or state in [ServerStateScript.INCOMPATIBLE, ServerStateScript.FOREIGN_PORT]
- )
- if _dev_primary_btn != null:
- if _server_restart_in_progress:
- _dev_primary_btn.disabled = true
- _dev_primary_btn.text = "Restarting..."
- _dev_primary_btn.tooltip_text = "Restarting the plugin-owned server..."
- elif not normal_start_released:
- _dev_primary_btn.disabled = true
- _dev_primary_btn.text = "Server Start Blocked"
- _dev_primary_btn.tooltip_text = (
- "Complete post-update client migration before starting the server."
- )
- else:
- var primary_state := _dev_primary_btn_state(managed, external)
- _dev_primary_btn.disabled = not bool(primary_state["enabled"])
- _dev_primary_btn.text = primary_state["text"]
- _dev_primary_btn.tooltip_text = primary_state["tooltip"]
- if _dev_stop_btn != null:
- var stop_state := _dev_stop_btn_state(managed)
- _dev_stop_btn.disabled = (not stop_state["enabled"]) or _server_restart_in_progress
- _dev_stop_btn.tooltip_text = stop_state["tooltip"]
-
-
-func _client_status_refresh_has_completed() -> bool:
- return bool(_client_work_snapshot.get("refresh_completed", false))
-
-
-func _client_refresh_state() -> int:
- return int(_client_work_snapshot.get("refresh_state", ClientRefreshStateScript.IDLE))
-
-
-func _connected_status_text() -> String:
- return "Server connected"
-
-
-static func _transport_status_text(snapshot: Dictionary) -> String:
- ## Total over the transport enum for isolated consumers/tests. The dock's
- ## connected fast path renders `_connected_status_text()` before calling it.
- var phase := str(snapshot.get("phase", ""))
- var attempt := maxi(1, int(snapshot.get("attempt", 0)))
- match phase:
- "connected":
- return "Server connected"
- "connecting":
- return "Connecting — attempt %d" % attempt
- "retrying":
- var retry_in_sec := ceili(maxf(0.0, float(snapshot.get("retry_in_sec", 0.0))))
- return "Retrying in %ds — attempt %d" % [retry_in_sec, attempt]
- "closing":
- return "Disconnecting…"
- "blocked":
- return "Connection blocked"
- return "Disconnected"
-
-
-## Open uv's official install documentation rather than executing an
-## installer on the user's behalf.
-##
-## This used to shell out to `curl -LsSf https://astral.sh/uv/install.sh | sh`
-## (and the PowerShell `irm … | iex` equivalent). That is arbitrary remote
-## code execution as the editor user, one dock click deep, with no version
-## pin, no checksum, and no signature — while this same plugin verifies its
-## OWN updates with an RSA-4096 signature over a SHA-256 sidecar, pinned to a
-## GitHub host and this repo's release-asset path. Holding a third-party
-## installer to a weaker standard than our own payload is the wrong trade,
-## and pinning a digest here would only cover the bootstrap script, not the
-## uv binary it goes on to fetch.
-##
-## Opening the docs keeps the discovery value of the button (the user still
-## learns uv is missing and how to get it) while leaving the decision to
-## install — and the choice of install method — with the user. Mirrors the
-## dock's existing "Run this manually" fallback for client CLIs.
-func _on_install_uv() -> void:
- OS.shell_open(UV_INSTALL_DOCS_URL)
- ## Drop the cached uvx path AND the cached `uvx --version` so that once
- ## the user has installed uv (in a terminal, from the docs we just
- ## opened), the dock finds the new binary instead of replaying the
- ## cached "not found" result for the rest of the session.
- ## Routing through the configurator matters on Windows, where the
- ## CLI-finder cache key is `uvx.exe` — invalidating just `"uvx"`
- ## would leave the cache stale and the dock would keep showing
- ## "uv: not found" for the rest of the session.
- ClientConfigurator.invalidate_uv_detection()
- ## Deliberately do NOT refresh here. `OS.shell_open` returns as soon as
- ## the browser is handed the URL, so an immediate refresh would run long
- ## before the user could install anything and would simply re-cache
- ## "not found" — undoing the invalidation above. (The old shell-out was
- ## a blocking `OS.execute`, so refreshing straight after it was correct
- ## then; it stopped being correct when the installer call went away.)
- ## Re-probe when the editor regains focus instead — see _notification.
- _uv_recheck_pending = true
-
-
-# --- Client section ---
-
-func _on_configure_client(client_id: String) -> void:
- ## Configure writes an explicit url + live plugin version; it does not
- ## need a healthy occupant. INCOMPATIBLE only suppresses status
- ## interpretation (#916).
- _dispatch_client_action(client_id, "configure")
-
-
-func _on_remove_client(client_id: String) -> void:
- _dispatch_client_action(client_id, "remove")
-
-
-## Emit a value intent; plugin.gd routes it to the plugin-lifetime job owner.
-## The Dock updates only view-local button state and never holds a Thread.
-func _dispatch_client_action(client_id: String, action: String) -> void:
- if _is_self_update_in_progress():
- return
- if _busy_client_actions().has(client_id):
- return
- var row: Dictionary = _client_rows.get(client_id, {})
- if row.is_empty():
- return
- _set_row_action_in_flight(client_id, action)
- client_action_requested.emit(client_id, action)
-
-
-func present_client_action_result(
- client_id: String,
- action: String,
- result: Dictionary,
- prewarm: Dictionary = {},
-) -> void:
- _report_prewarm_outcome(client_id, prewarm)
- _finalize_action_buttons(client_id)
- var success_status := Client.Status.NOT_CONFIGURED if action == "remove" else Client.Status.CONFIGURED
- if result.get("status") == "ok":
- ## #877: Remove targets only the selected scope, so a configure is the
- ## only action with an all-scope sweep to disclose. The manual panel
- ## that lists those removes is shown on the failure path below, which
- ## left the success path — where the sweep actually ran — silent.
- var sweep_note := (
- ClientConfigurator.configure_sweep_note(client_id) if action == "configure" else ""
- )
- _apply_row_status(client_id, success_status, sweep_note)
- var row: Dictionary = _client_rows.get(client_id, {})
- if not row.is_empty():
- (row["manual_panel"] as VBoxContainer).visible = false
- else:
- _apply_row_status(client_id, Client.Status.ERROR, str(result.get("message", "failed")))
- if action == "configure":
- _show_manual_command_for(client_id)
- _refresh_clients_summary()
-
-
-func present_client_action_timeout(client_id: String, _action: String, detail: String) -> void:
- _apply_row_status(client_id, Client.Status.ERROR, detail)
- _refresh_clients_summary()
-
-
-func present_client_work_snapshot(snapshot: Dictionary) -> void:
- _client_work_snapshot = snapshot.duplicate(true)
- var busy := _busy_client_actions()
- var names: Dictionary = _client_work_snapshot.get("action_names", {})
- var phases: Dictionary = _client_work_snapshot.get("action_phases", {})
- for client_id in _client_rows:
- var id := String(client_id)
- if busy.has(id):
- _set_row_action_in_flight(id, String(names.get(id, "configure")))
- if String(phases.get(id, "")) == "prewarm":
- (_client_rows[id]["configure_btn"] as Button).text = "Installing…"
- else:
- _finalize_action_buttons(id)
- _refresh_clients_summary()
-
-
-func _busy_client_actions() -> Array[String]:
- var result: Array[String] = []
- result.assign(_client_work_snapshot.get("busy_actions", []))
- return result
-
-
-## One line per Configure so a cold build is attributable after the fact —
-## the dock label is transient, and #851's symptom (a terminal window on
-## Windows) is easiest to correlate against a timestamped log entry. Silent
-## on the skip paths: a dev-venv/system tier having no env to warm is the
-## normal case, not news.
-func _report_prewarm_outcome(client_id: String, prewarm: Variant) -> void:
- if not (prewarm is Dictionary):
- return
- var data := prewarm as Dictionary
- if data.is_empty() or bool(data.get("skipped", false)):
- return
- if bool(data.get("timed_out", false)):
- print(
- "MCP | %s: package pre-warm timed out; the first client launch will build the environment"
- % client_id
- )
- return
- if int(data.get("exit_code", -1)) != 0:
- print(
- "MCP | %s: package pre-warm failed (exit %d); the first client launch will build the environment"
- % [client_id, int(data.get("exit_code", -1))]
- )
- return
- print("MCP | %s: package environment pre-warmed" % client_id)
-
-
-## In-flight visual: rewrite the verb onto the button the user just
-## clicked ("Configuring…" / "Removing…") so the feedback lands where
-## their attention already is. Don't pollute the row label — that'd
-## clobber any drift hint ("URL out of date") still relevant to the row.
-## The dot turns amber so the row reads as "busy" at a glance, not as
-## green (premature success) or red (premature failure). Both buttons
-## go disabled so a double-click or second action can't queue stale
-## work behind the in-flight worker.
-func _set_row_action_in_flight(client_id: String, action: String) -> void:
- var row: Dictionary = _client_rows.get(client_id, {})
- if row.is_empty():
- return
- var configure_btn: Button = row["configure_btn"]
- var remove_btn: Button = row["remove_btn"]
- configure_btn.disabled = true
- remove_btn.disabled = true
- if action == "remove":
- remove_btn.text = "Removing…"
- else:
- configure_btn.text = "Configuring…"
- (row["dot"] as ColorRect).color = COLOR_AMBER
-
-
-## Re-enable both buttons and reset their text back to canonical labels.
-## Restore labels from cached row status as well as enabling the controls.
-## This matters when a timed-out orphan finishes: the owner publishes a new
-## snapshot but has no action result to repaint the row for us.
-func _finalize_action_buttons(client_id: String) -> void:
- var row: Dictionary = _client_rows.get(client_id, {})
- if row.is_empty():
- return
- var configure_btn := row["configure_btn"] as Button
- configure_btn.disabled = false
- match row.get("status", Client.Status.NOT_CONFIGURED):
- Client.Status.CONFIGURED, Client.Status.CONFIGURED_MISMATCH:
- configure_btn.text = "Reconfigure"
- Client.Status.NOT_CONFIGURED:
- configure_btn.text = "Configure"
- _:
- configure_btn.text = "Retry"
- var remove_btn: Button = row["remove_btn"]
- remove_btn.disabled = false
- remove_btn.text = "Remove"
-
-
-func _on_refresh_clients_pressed() -> void:
- ## Explicit user action — also give a failed uv probe another chance
- ## (#739), mirroring how the same click already re-sweeps client CLIs.
- _schedule_uv_reprobe()
- _request_client_status_refresh(true)
-
-
-func _on_configure_all_clients() -> void:
- ## Per-row Configure already bypasses the RUNNING gate. INCOMPATIBLE
- ## skips health interpretation (#916), so Configure all must do the same
- ## even if a status sweep is still in flight — `_set_incompatible_server`
- ## does not reset `_refresh_state`.
- if (
- ClientRefreshStateScript.should_disable_client_actions(_client_refresh_state())
- and not _server_blocks_client_health()
- ):
- return
- for client_id in _client_rows:
- var status: Client.Status = _client_rows[client_id].get("status", Client.Status.NOT_CONFIGURED)
- if status == Client.Status.CONFIGURED:
- continue
- _on_configure_client(String(client_id))
- _refresh_clients_summary()
-
-
-func _on_open_clients_window() -> void:
- if _clients_window == null:
- return
- ## Re-sweep before the user has time to act on stale dot colors. The request
- ## is async/stale-while-refreshing so the popup paints immediately with
- ## last-known state; the fresh colors land when the background worker returns.
- ## This is an explicit user action, so it bypasses the focus-in cooldown.
- _request_client_status_refresh(true)
- ## Also re-sync the Tools tab from the persisted setting — another
- ## editor instance (or a hand-edit of editor_settings-4.tres) may have
- ## changed the excluded list while the window was closed.
- _reset_tools_pending_from_setting()
- _refresh_tools_ui_state()
- if vision_routing != null:
- vision_routing.refresh_ui()
- # popup_centered() with a minsize forces the window to that size and
- # centers on the parent viewport. Setting .size on a hidden Window
- # doesn't always take effect, so we force it at popup time here.
- _clients_window.popup_centered(Vector2i(640, 600))
-
-
-func _settings_are_dirty() -> bool:
- return (
- _tools_pending_excluded != _tools_saved_excluded
- or _telemetry_pending_enabled != _telemetry_saved_enabled
- or _allow_hosts_is_dirty()
- )
-
-
-func _on_clients_window_close_requested() -> void:
- if _clients_window == null:
- return
- ## If the user has unapplied settings, a close would silently throw the
- ## pending state away. Prompt before discarding current options and if
- ## they confirm, reset pending → saved so the window shows the persisted
- ## state the next time they open it.
- if _settings_are_dirty():
- _show_tools_close_confirm()
- return
- _clients_window.hide()
-
-
-# --- Tools tab (domain exclusion) ---
-
-func _build_tools_tab(tabs: TabContainer) -> void:
- ## Tab 2 — domain-exclusion checkboxes. Rendered once, on dock construction.
- ## `_reset_tools_pending_from_setting()` re-syncs checkbox state from the
- ## saved setting each time the window opens.
- var tools_tab := VBoxContainer.new()
- tools_tab.add_theme_constant_override("separation", 8)
- var tools_margin := _build_margin_container()
- tools_margin.name = "Tools"
- tools_margin.add_child(tools_tab)
- tabs.add_child(tools_margin)
-
- var intro := Label.new()
- intro.text = (
- "Some MCP clients cap tools per connection (Antigravity: 100). "
- + "Uncheck a domain to drop its non-core tools from this server. "
- + "Core tools stay on. Changes require a server restart."
- )
- intro.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- intro.add_theme_color_override("font_color", COLOR_MUTED)
- intro.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- tools_tab.add_child(intro)
-
- var count_row := HBoxContainer.new()
- count_row.add_theme_constant_override("separation", 8)
- var count_header := Label.new()
- count_header.text = "Tools Enabled:"
- count_header.add_theme_color_override("font_color", COLOR_MUTED)
- count_row.add_child(count_header)
- _tools_count_label = Label.new()
- _tools_count_label.add_theme_font_size_override("font_size", 15)
- count_row.add_child(_tools_count_label)
- _tools_dirty_warning = Label.new()
- _tools_dirty_warning.add_theme_color_override("font_color", COLOR_AMBER)
- _tools_dirty_warning.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _tools_dirty_warning.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
- _tools_dirty_warning.visible = false
- _tools_dirty_warning.text = "Unapplied changes"
- count_row.add_child(_tools_dirty_warning)
- tools_tab.add_child(count_row)
-
- tools_tab.add_child(HSeparator.new())
-
- var scroll := ScrollContainer.new()
- scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
- scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
- tools_tab.add_child(scroll)
-
- var grid := VBoxContainer.new()
- grid.add_theme_constant_override("separation", 4)
- grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- scroll.add_child(grid)
-
- ## Core pseudo-row — disabled checkbox, always checked. Shows the 5
- ## always-loaded tools as a single line item so the user can see where
- ## their baseline tool budget goes without listing individual core names
- ## inline (tooltip has them).
- var core_row := HBoxContainer.new()
- core_row.add_theme_constant_override("separation", 8)
- var core_chk := CheckBox.new()
- core_chk.button_pressed = true
- core_chk.disabled = true
- core_chk.focus_mode = Control.FOCUS_NONE
- core_row.add_child(core_chk)
- var core_label := Label.new()
- core_label.text = "Core (always on)"
- core_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- core_row.add_child(core_label)
- var core_count := Label.new()
- core_count.text = "%d tools" % (ToolCatalog.CORE_TOOLS.size() + ToolCatalog.ALWAYS_ON_TOOLS.size())
- core_count.add_theme_color_override("font_color", COLOR_MUTED)
- core_row.add_child(core_count)
- core_row.tooltip_text = "%s · always on: %s" % [
- ", ".join(ToolCatalog.CORE_TOOLS),
- ", ".join(ToolCatalog.ALWAYS_ON_TOOLS),
- ]
- grid.add_child(core_row)
-
- grid.add_child(HSeparator.new())
-
- _tools_domain_checkboxes.clear()
- for entry in ToolCatalog.DOMAINS:
- _build_tools_domain_row(grid, entry)
-
- ## Custom (addon-registered) tools — unlike the domain rows above,
- ## toggles apply LIVE: the registry re-pushes the filtered catalog to
- ## the server on every change, no restart needed.
- grid.add_child(HSeparator.new())
- var custom_header_row := HBoxContainer.new()
- custom_header_row.add_theme_constant_override("separation", 8)
- var custom_header := Label.new()
- custom_header.text = "Custom tools (addon-registered)"
- custom_header.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- custom_header_row.add_child(custom_header)
- _custom_tools_count_label = Label.new()
- _custom_tools_count_label.add_theme_color_override("font_color", COLOR_MUTED)
- custom_header_row.add_child(_custom_tools_count_label)
- grid.add_child(custom_header_row)
- var custom_hint := Label.new()
- custom_hint.text = "Applies immediately — disabled tools are hidden from agents and rejected if called."
- custom_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- custom_hint.add_theme_color_override("font_color", COLOR_MUTED)
- custom_hint.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- grid.add_child(custom_hint)
- _custom_tools_list = VBoxContainer.new()
- _custom_tools_list.add_theme_constant_override("separation", 4)
- _custom_tools_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- grid.add_child(_custom_tools_list)
- _connect_custom_tool_registry()
- _refresh_custom_tools_rows()
-
- tools_tab.add_child(HSeparator.new())
-
- var telemetry_row := HBoxContainer.new()
- telemetry_row.add_theme_constant_override("separation", 8)
- var telemetry_label := Label.new()
- telemetry_label.text = "Telemetry"
- telemetry_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- telemetry_row.add_child(telemetry_label)
- _telemetry_toggle = CheckButton.new()
- _telemetry_toggle.toggled.connect(_on_telemetry_toggled)
- telemetry_row.add_child(_telemetry_toggle)
- tools_tab.add_child(telemetry_row)
-
- tools_tab.add_child(HSeparator.new())
-
- var footer := HBoxContainer.new()
- footer.add_theme_constant_override("separation", 8)
-
- _tools_apply_btn = Button.new()
- _tools_apply_btn.text = "Apply and Restart Server"
- _tools_apply_btn.tooltip_text = "Save the excluded list to Editor Settings and reload the plugin so the server respawns with --exclude-domains."
- _tools_apply_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _tools_apply_btn.pressed.connect(_on_tools_apply)
- footer.add_child(_tools_apply_btn)
-
- _tools_reset_btn = Button.new()
- _tools_reset_btn.text = "Reset to defaults"
- _tools_reset_btn.tooltip_text = "Re-enable every domain (no --exclude-domains flag). Still needs Apply."
- _tools_reset_btn.pressed.connect(_on_tools_reset)
- footer.add_child(_tools_reset_btn)
-
- tools_tab.add_child(footer)
-
- _tools_close_confirm = ConfirmationDialog.new()
- _tools_close_confirm.title = "Discard unapplied changes?"
- ## Generic wording: _settings_are_dirty() covers domain toggles, the
- ## telemetry switch, AND the Settings tab's allow-host field (#507) —
- ## the old "checked/unchecked domains" text misled non-domain edits.
- _tools_close_confirm.dialog_text = (
- "You have unapplied changes in this window.\n"
- + "Close it and discard those changes?"
- )
- _tools_close_confirm.ok_button_text = "Discard"
- _tools_close_confirm.confirmed.connect(_on_tools_discard_confirmed)
- add_child(_tools_close_confirm)
-
- _update_confirm = ConfirmationDialog.new()
- _update_confirm.title = "Update Godot AI?"
- _update_confirm.ok_button_text = "Update plugin"
- _update_confirm.cancel_button_text = "Later"
- _update_confirm.confirmed.connect(_on_update_confirmed)
- add_child(_update_confirm)
-
- _reset_tools_pending_from_setting()
- _refresh_tools_ui_state()
-
-
-## --- Custom (addon-registered) tools section ---
-
-func _connect_custom_tool_registry() -> void:
- var registry := McpToolRegistry.get_instance()
- if registry == null:
- return
- if not registry.tools_changed.is_connected(_on_custom_tool_registry_changed):
- registry.tools_changed.connect(_on_custom_tool_registry_changed)
-
-
-func _on_custom_tool_registry_changed() -> void:
- ## Deferred: tools_changed can fire from inside a checkbox toggle in
- ## this very list — rebuilding synchronously would free the emitting
- ## control mid-signal.
- _refresh_custom_tools_rows.call_deferred()
-
-
-func _refresh_custom_tools_rows() -> void:
- if _custom_tools_list == null or not is_instance_valid(_custom_tools_list):
- return
- for child in _custom_tools_list.get_children():
- child.queue_free()
- var registry := McpToolRegistry.get_instance()
- var specs: Array = [] if registry == null else registry.all()
- specs.sort_custom(func(a, b): return String(a.name) < String(b.name))
- var enabled_count := 0
- for spec in specs:
- if registry.is_tool_enabled(spec.name):
- enabled_count += 1
- _custom_tools_list.add_child(_build_custom_tool_row(registry, spec))
- if _custom_tools_count_label != null and is_instance_valid(_custom_tools_count_label):
- _custom_tools_count_label.text = "%d/%d enabled" % [enabled_count, specs.size()]
- if specs.is_empty():
- var empty := Label.new()
- empty.text = "None registered. Addons add tools via McpToolRegistry — see docs/plugin-architecture.md."
- empty.add_theme_color_override("font_color", COLOR_MUTED)
- empty.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- empty.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _custom_tools_list.add_child(empty)
-
-
-func _build_custom_tool_row(registry: McpToolRegistry, spec: McpCustomToolSpec) -> HBoxContainer:
- var row := HBoxContainer.new()
- row.add_theme_constant_override("separation", 8)
- var chk := CheckBox.new()
- chk.button_pressed = registry.is_tool_enabled(spec.name)
- chk.toggled.connect(func(pressed: bool): registry.set_tool_enabled(spec.name, pressed))
- row.add_child(chk)
- var name_label := Label.new()
- name_label.text = spec.name + (" · promoted" if spec.promoted else "")
- name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- row.add_child(name_label)
- var source_label := Label.new()
- source_label.text = spec.source if not spec.source.is_empty() else spec.source_path.get_base_dir().get_file()
- source_label.add_theme_color_override("font_color", COLOR_MUTED)
- row.add_child(source_label)
- ## Tooltip = the agent-facing description plus provenance, so the user
- ## can judge what they're enabling without leaving the dock.
- row.tooltip_text = "%s\n\nsource: %s%s" % [
- spec.description,
- spec.source_path,
- "\npromoted: registers as first-class MCP tool custom_%s" % spec.name if spec.promoted else "",
- ]
- name_label.tooltip_text = row.tooltip_text
- return row
-
-
-func _build_tools_domain_row(parent: VBoxContainer, entry: Dictionary) -> void:
- var row := HBoxContainer.new()
- row.add_theme_constant_override("separation", 8)
-
- var chk := CheckBox.new()
- chk.button_pressed = true # default; `_reset_tools_pending_from_setting` corrects
- chk.toggled.connect(_on_tools_domain_toggled.bind(String(entry["id"])))
- row.add_child(chk)
-
- var name_label := Label.new()
- name_label.text = String(entry["label"])
- name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- row.add_child(name_label)
-
- var count_label := Label.new()
- count_label.text = "%d tools" % int(entry["count"])
- count_label.add_theme_color_override("font_color", COLOR_MUTED)
- row.add_child(count_label)
-
- ## Hover tooltip = flat list of tool names in this domain. Lets the
- ## user decide without leaving the dock (e.g. "I just want to drop
- ## `animation_preset_*` — do I lose anything else?").
- var tools_list: Array = entry.get("tools", [])
- row.tooltip_text = ", ".join(tools_list)
- name_label.tooltip_text = row.tooltip_text
- count_label.tooltip_text = row.tooltip_text
-
- parent.add_child(row)
- _tools_domain_checkboxes[String(entry["id"])] = chk
-
-
-func _reset_tools_pending_from_setting() -> void:
- ## Read the saved setting → pending/saved arrays, then sync checkbox state.
- ## Unknown domain names in the setting (e.g. from an older plugin
- ## version) are dropped from the display here (only ids with a checkbox
- ## survive). The startup path is protected separately:
- ## `ClientConfigurator.excluded_domains()` filters unknown names before
- ## they reach `--exclude-domains`, whose `parse_exclude_list` hard-fails
- ## on them.
- var saved_raw := ClientConfigurator.excluded_domains()
- var saved := PackedStringArray()
- if not saved_raw.is_empty():
- for part in saved_raw.split(","):
- var t := part.strip_edges()
- if t.is_empty():
- continue
- if _tools_domain_checkboxes.has(t) and saved.find(t) == -1:
- saved.append(t)
- saved.sort()
- _tools_saved_excluded = saved
- _tools_pending_excluded = saved.duplicate()
- for id in _tools_domain_checkboxes:
- var chk: CheckBox = _tools_domain_checkboxes[id]
- ## `set_pressed_no_signal` — mutating programmatically should not
- ## fire the toggled handler, which would mutate pending back.
- chk.set_pressed_no_signal(_tools_pending_excluded.find(id) == -1)
- ## Also reset telemetry pending state from the persisted setting.
- if _telemetry_toggle != null:
- _load_telemetry_setting()
- ## And the Settings tab's allow-host field (#507) — same window-open /
- ## discard-confirm re-sync contract as the tools checkboxes.
- _reset_allow_hosts_from_setting()
-
-
-func _on_tools_domain_toggled(pressed: bool, domain_id: String) -> void:
- var idx := _tools_pending_excluded.find(domain_id)
- if pressed and idx != -1:
- _tools_pending_excluded.remove_at(idx)
- elif not pressed and idx == -1:
- _tools_pending_excluded.append(domain_id)
- _tools_pending_excluded.sort()
- _refresh_tools_ui_state()
-
-
-func _refresh_tools_ui_state() -> void:
- if _tools_count_label == null:
- return
- var enabled := ToolCatalog.enabled_tool_count(_tools_pending_excluded)
- var total := ToolCatalog.total_tool_count()
- _tools_count_label.text = "%d / %d" % [enabled, total]
- var dirty := _settings_are_dirty()
- _tools_dirty_warning.visible = dirty
- _tools_apply_btn.disabled = not dirty
- ## Color the count when the user is over Antigravity's cap — a soft
- ## signal that their selection still won't fit. 100 is the Antigravity
- ## limit; other clients may cap higher, so this is advisory only.
- if enabled > 100:
- _tools_count_label.add_theme_color_override("font_color", COLOR_AMBER)
- else:
- _tools_count_label.remove_theme_color_override("font_color")
-
-
-func _on_tools_apply() -> void:
- var canonical_excluded := ToolCatalog.canonical(_tools_pending_excluded)
- _tools_saved_excluded = _tools_pending_excluded.duplicate()
- _telemetry_saved_enabled = _telemetry_pending_enabled
- _refresh_tools_ui_state()
- settings_apply_requested.emit({
- "excluded_domains": canonical_excluded,
- "telemetry_enabled": _telemetry_pending_enabled,
- }.duplicate(true), true)
-
-
-func _on_tools_reset() -> void:
- ## Resets only the tool-domain exclusions, not the telemetry toggle.
- ## Telemetry is a privacy preference users typically want to set once
- ## and have honored — flipping it back to "on" via a generic Reset
- ## button would be a surprising privacy regression. The button label
- ## is scoped to tools accordingly.
- _tools_pending_excluded = PackedStringArray()
- for id in _tools_domain_checkboxes:
- var chk: CheckBox = _tools_domain_checkboxes[id]
- chk.set_pressed_no_signal(true)
- _refresh_tools_ui_state()
-
-
-func _show_tools_close_confirm() -> void:
- if _tools_close_confirm == null:
- return
- _tools_close_confirm.popup_centered()
-
-
-func _on_tools_discard_confirmed() -> void:
- _reset_tools_pending_from_setting()
- _refresh_tools_ui_state()
- if _clients_window != null:
- _clients_window.hide()
-
-
-# --- Settings tab (allow-host LAN opt-in, #507) ---
-
-func _build_settings_tab(tabs: TabContainer) -> void:
- ## Tab 3 — settings-style controls that don't fit Clients or Tools: the
- ## Vision Routing section plus the `--allow-host` LAN opt-in behind a
- ## collapsed "Remote access (advanced)" disclosure, so its security
- ## warning renders exactly at the point of configuration. Rendered once
- ## on dock construction, mirroring `_build_tools_tab`;
- ## `_reset_allow_hosts_from_setting()` and `vision_routing.refresh_ui()`
- ## re-sync each time the window opens (via
- ## `_reset_tools_pending_from_setting` / `_on_open_clients_window`).
- var settings_tab := VBoxContainer.new()
- settings_tab.add_theme_constant_override("separation", 8)
- var settings_margin := _build_margin_container()
- settings_margin.name = "Settings"
- settings_margin.add_child(settings_tab)
- tabs.add_child(settings_margin)
-
- ## Vision Routing is configuration, not status — it lives here rather
- ## than in the dock. Not dev-gated: it is the Settings tab's primary
- ## content and must work for every user.
- if vision_routing != null:
- vision_routing.build_section(settings_tab)
-
- ## Remote access (advanced): collapsed by default; auto-expands when a
- ## non-empty CIDR allowlist is already configured so an active
- ## off-loopback bind is never hidden behind a collapsed header. The
- ## disclosure replaces the former developer-mode gate for this block.
- _allow_hosts_fold = FoldableContainer.new()
- _allow_hosts_fold.title = "Remote access (advanced)"
- _allow_hosts_fold.folded = true
- settings_tab.add_child(_allow_hosts_fold)
-
- _allow_hosts_section = VBoxContainer.new()
- _allow_hosts_section.add_theme_constant_override("separation", 6)
- _allow_hosts_fold.add_child(_allow_hosts_section)
-
- _allow_hosts_section.add_child(_make_header("Allow remote hosts (CIDR)"))
-
- var intro := Label.new()
- intro.text = (
- "Comma-separated CIDRs or bare IPs (e.g. 192.168.1.0/24, 10.0.0.5). "
- + "When non-empty, the server binds off loopback and accepts MCP "
- + "connections from these ranges (--allow-host)."
- )
- intro.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- intro.add_theme_color_override("font_color", COLOR_MUTED)
- intro.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _allow_hosts_section.add_child(intro)
-
- ## Warning banner — the DNS-rebinding guard is widened to every machine
- ## in the named ranges, so make the user name a network they trust
- ## instead of offering a blanket "expose everything" toggle (#507).
- var warning := Label.new()
- warning.text = (
- "Warning: every machine in these ranges can drive this Godot editor, "
- + "and the DNS-rebinding guard's Host allowlist is widened to match. "
- + "Only name networks you trust. On untrusted or shared networks, "
- + "prefer an SSH tunnel or Tailscale instead of exposing the port."
- )
- warning.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- warning.add_theme_color_override("font_color", COLOR_AMBER)
- warning.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _allow_hosts_section.add_child(warning)
-
- _allow_hosts_edit = LineEdit.new()
- _allow_hosts_edit.placeholder_text = "e.g. 192.168.1.0/24, 10.0.0.5 — empty = loopback only"
- _allow_hosts_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _allow_hosts_edit.text_changed.connect(_on_allow_hosts_text_changed)
- _allow_hosts_section.add_child(_allow_hosts_edit)
-
- _allow_hosts_hint = Label.new()
- _allow_hosts_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- _allow_hosts_hint.add_theme_color_override("font_color", Color.RED)
- _allow_hosts_hint.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _allow_hosts_hint.visible = false
- _allow_hosts_section.add_child(_allow_hosts_hint)
-
- _allow_hosts_apply_btn = Button.new()
- _allow_hosts_apply_btn.text = "Apply and Restart Server"
- _allow_hosts_apply_btn.tooltip_text = (
- "Save the allowlist to Editor Settings and reload the plugin so the "
- + "server respawns with --allow-host. Clear the field and Apply to "
- + "return to loopback-only."
- )
- _allow_hosts_apply_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _allow_hosts_apply_btn.pressed.connect(_on_allow_hosts_apply)
- _allow_hosts_section.add_child(_allow_hosts_apply_btn)
-
- _reset_allow_hosts_from_setting()
-
-
-func _reset_allow_hosts_from_setting() -> void:
- _allow_hosts_saved = ClientConfigurator.allow_hosts()
- _refresh_allow_hosts_fold_state()
- if _allow_hosts_edit == null:
- return
- _allow_hosts_edit.text = _allow_hosts_saved
- _refresh_allow_hosts_ui_state()
-
-
-## Auto-expands the "Remote access (advanced)" disclosure whenever a
-## non-empty allowlist is configured, so an active off-loopback bind is
-## never hidden behind a collapsed header.
-func _refresh_allow_hosts_fold_state() -> void:
- if _allow_hosts_fold == null:
- return
- if ClientConfigurator.allow_hosts().is_empty():
- _allow_hosts_fold.fold()
- else:
- _allow_hosts_fold.expand()
-
-
-func _allow_hosts_is_dirty() -> bool:
- if _allow_hosts_edit == null:
- return false
- return McpAllowHosts.normalize(_allow_hosts_edit.text) != _allow_hosts_saved
-
-
-func _on_allow_hosts_text_changed(_new_text: String) -> void:
- _refresh_allow_hosts_ui_state()
-
-
-func _refresh_allow_hosts_ui_state() -> void:
- if _allow_hosts_edit == null or _allow_hosts_apply_btn == null:
- return
- var invalid := McpAllowHosts.invalid_tokens(_allow_hosts_edit.text)
- if invalid.is_empty():
- _allow_hosts_hint.visible = false
- else:
- ## Name the accepted syntax in the hint — matches the server's
- ## `parse_allow_hosts` (CIDR / bare IP, comma-separated).
- _allow_hosts_hint.text = (
- "Invalid entries (must be a CIDR like 192.168.1.0/24 or a bare IP, comma-separated): %s"
- % ", ".join(invalid)
- )
- _allow_hosts_hint.visible = true
- _allow_hosts_apply_btn.disabled = not _allow_hosts_is_dirty() or not invalid.is_empty()
-
-
-func _on_allow_hosts_apply() -> void:
- if _allow_hosts_edit == null:
- return
- var normalized := McpAllowHosts.normalize(_allow_hosts_edit.text)
- if not McpAllowHosts.invalid_tokens(normalized).is_empty():
- return
- _allow_hosts_saved = normalized
- _allow_hosts_edit.text = normalized
- _refresh_allow_hosts_ui_state()
- settings_apply_requested.emit({"allow_hosts": normalized}, true)
-
-
-func _refresh_clients_summary() -> void:
- # Count from cached row status values — `_apply_row_status` is the single
- # source of truth, and reading cached status avoids re-running
- # filesystem/CLI-hitting checks on every refresh. The same cache re-derives
- # the drift banner so per-row mutations (Configure/Reconfigure/Remove on a
- # row in the Clients & Tools window) keep the dock-level banner in sync
- # without an extra sweep. See #166 and #226.
- if _clients_summary_label == null:
- return
- var configured := 0
- var mismatched_ids: Array[String] = []
- for client_id in _client_rows:
- var status: Client.Status = _client_rows[client_id].get("status", Client.Status.NOT_CONFIGURED)
- if status == Client.Status.CONFIGURED:
- configured += 1
- elif status == Client.Status.CONFIGURED_MISMATCH:
- mismatched_ids.append(client_id)
- var text := "%d / %d configured" % [configured, _client_rows.size()]
- if mismatched_ids.size() > 0:
- text += " (%d stale)" % mismatched_ids.size()
- var refresh_state := _client_refresh_state()
- if ClientRefreshStateScript.should_show_checking_badge(refresh_state):
- text += (
- " (checking...)"
- if refresh_state != ClientRefreshStateScript.RUNNING_TIMED_OUT
- else " (client probe still running)"
- )
- _clients_summary_label.text = text
- if _client_configure_all_btn != null:
- _client_configure_all_btn.disabled = (
- ClientRefreshStateScript.should_disable_client_actions(refresh_state)
- and not _server_blocks_client_health()
- )
- if _client_empty_cta_btn != null:
- _client_empty_cta_btn.visible = configured == 0 and _client_status_refresh_has_completed()
- _refresh_drift_banner(mismatched_ids)
- _update_status()
-
-
-func _show_manual_command_for(client_id: String) -> void:
- var row: Dictionary = _client_rows.get(client_id, {})
- if row.is_empty():
- return
- var cmd := ClientConfigurator.manual_command(client_id)
- if cmd.is_empty():
- row["manual_panel"].visible = false
- return
- row["manual_text"].text = cmd
- row["manual_panel"].visible = true
- ## #680: for rows low in the list the panel materializes below the
- ## visible scroll area and the Configure click looks like a no-op.
- ## Deferred so the just-shown panel has a settled rect to scroll to.
- _scroll_manual_panel_into_view.call_deferred(row["manual_panel"])
-
-
-func _scroll_manual_panel_into_view(panel: Control) -> void:
- if panel == null or not panel.is_inside_tree():
- return
- var ancestor := panel.get_parent()
- while ancestor != null and not (ancestor is ScrollContainer):
- ancestor = ancestor.get_parent()
- if ancestor != null:
- (ancestor as ScrollContainer).ensure_control_visible(panel)
-
-
-func _on_copy_manual_command(client_id: String) -> void:
- var row: Dictionary = _client_rows.get(client_id, {})
- if row.is_empty():
- return
- DisplayServer.clipboard_set(row["manual_text"].text)
-
-
-func _on_open_config_file(client_id: String) -> void:
- var path := _client_config_path_for_row(client_id)
- if path.is_empty():
- return
- if FileAccess.file_exists(path):
- OS.shell_open(path)
- return
- _reveal_config_folder(path)
-
-
-func _on_reveal_config_folder(client_id: String) -> void:
- var path := _client_config_path_for_row(client_id)
- if path.is_empty():
- return
- _reveal_config_folder(path)
-
-
-func _client_config_path_for_row(client_id: String) -> String:
- var row: Dictionary = _client_rows.get(client_id, {})
- if row.is_empty():
- return ""
- return String(row.get("config_path", ""))
-
-
-func _reveal_config_folder(path: String) -> void:
- var dir := path.get_base_dir()
- if dir.is_empty():
- return
- OS.shell_open(dir)
-
-
-func _refresh_all_client_statuses() -> void:
- _request_client_status_refresh(true)
-
-
-func _perform_initial_client_status_refresh() -> void:
- if not is_inside_tree():
- return
- _request_client_status_refresh(false)
-
-
-func _request_client_status_refresh(force: bool = false) -> bool:
- if _server_blocks_client_health():
- return false
- if _is_self_update_in_progress() or _client_rows.is_empty():
- return false
- var ids: Array[String] = []
- for client_id in _client_rows:
- ids.append(String(client_id))
- client_status_refresh_requested.emit(ids, force)
- return true
-
-
-func present_client_status_refresh_results(results: Dictionary) -> void:
- if _server_blocks_client_health():
- return
- var busy := _busy_client_actions()
- for client_id in results:
- if busy.has(String(client_id)):
- continue
- var result: Dictionary = results[client_id]
- _apply_row_status(
- String(client_id),
- result.get("status", Client.Status.NOT_CONFIGURED),
- str(result.get("error_msg", "")),
- result.get("installed", false)
- )
- _refresh_clients_summary()
-
-
-func _server_blocks_client_health() -> bool:
- return ServerStateScript.blocks_client_health(
- int(_lifecycle_snapshot.get("state", ServerStateScript.UNINITIALIZED))
- )
-
-
-func _server_blocked_client_message() -> String:
- var message := str(_lifecycle_snapshot.get("message", ""))
- return message if not message.is_empty() else "server incompatible"
-
-
-func _refresh_drift_banner(mismatched_ids: Array[String]) -> void:
- if _drift_banner == null:
- return
- ## Sort so set-equality is order-independent — `_client_rows` iteration
- ## order is dict-insertion order, but a future change to the iteration
- ## site shouldn't make us repaint identical content.
- mismatched_ids = mismatched_ids.duplicate()
- mismatched_ids.sort()
- if mismatched_ids == _last_mismatched_ids:
- return
- _last_mismatched_ids = mismatched_ids
- if mismatched_ids.is_empty():
- _drift_banner.visible = false
- return
- var names: Array[String] = []
- for id in mismatched_ids:
- names.append(ClientConfigurator.client_display_name(id))
- ## Active server URL is already shown on the WS:/HTTP: line above the
- ## Clients section, so it doesn't need to repeat here. Lead with the
- ## client names — that's the only thing the user can act on.
- var verb := "needs" if mismatched_ids.size() == 1 else "need"
- _drift_label.text = "%s %s to be reconfigured." % [", ".join(names), verb]
- _drift_banner.visible = true
-
-
-func _on_reconfigure_mismatched() -> void:
- ## Re-Configure every client whose URL is currently stale. Iterates the
- ## cached list from the most recent sweep instead of re-running
- ## `check_status` per row (saves ~18 filesystem reads per click). The
- ## trailing `_refresh_all_client_statuses()` re-sweeps anyway, so any
- ## entries the user manually fixed between sweep and click get re-counted
- ## as CONFIGURED there.
- for client_id in _last_mismatched_ids:
- if _client_rows.has(client_id):
- _on_configure_client(client_id)
- _refresh_all_client_statuses()
-
-
-func _apply_row_status(
- client_id: String,
- status: Client.Status,
- error_msg: String = "",
- installed_override: Variant = null,
-) -> void:
- var row: Dictionary = _client_rows.get(client_id, {})
- if row.is_empty():
- return
- row["status"] = status
- var dot: ColorRect = row["dot"]
- var configure_btn: Button = row["configure_btn"]
- var remove_btn: Button = row["remove_btn"]
- var name_label: Label = row["name_label"]
- var base_name := ClientConfigurator.client_display_name(client_id)
- _refresh_client_config_file_buttons(client_id)
- match status:
- Client.Status.CONFIGURED:
- dot.color = Color.GREEN
- configure_btn.text = "Reconfigure"
- remove_btn.visible = true
- ## `error_msg` doubles as a detail slot on the green path: a
- ## successful configure passes the sweep note (#877). Transient by
- ## design — the next status refresh re-applies CONFIGURED with no
- ## detail, so the row settles back to its plain name.
- name_label.text = (
- "%s (%s)" % [base_name, error_msg] if not error_msg.is_empty() else base_name
- )
- Client.Status.NOT_CONFIGURED:
- dot.color = COLOR_MUTED
- configure_btn.text = "Configure"
- remove_btn.visible = false
- var installed: bool = installed_override if installed_override != null else ClientConfigurator.is_installed(client_id)
- name_label.text = base_name if installed else "%s (not detected)" % base_name
- Client.Status.CONFIGURED_MISMATCH:
- ## Amber matches the dock-level drift banner so a glance at the
- ## row + the banner read as the same condition.
- dot.color = COLOR_AMBER
- configure_btn.text = "Reconfigure"
- remove_btn.visible = true
- ## Drift is usually a stale URL, but a `{scope}` client can also be
- ## registered in a scope the user did not select (#872), and
- ## "URL out of date" would be a wrong description of that. Prefer
- ## the probe's own words whenever it supplied any.
- name_label.text = (
- "%s (%s)" % [base_name, error_msg]
- if not error_msg.is_empty()
- else "%s (URL out of date)" % base_name
- )
- _:
- dot.color = Color.RED
- configure_btn.text = "Retry"
- remove_btn.visible = false
- name_label.text = "%s — %s" % [base_name, error_msg] if not error_msg.is_empty() else base_name
-
-
-func _refresh_client_config_file_buttons(client_id: String) -> void:
- var row: Dictionary = _client_rows.get(client_id, {})
- if row.is_empty():
- return
- # Re-resolve on every refresh so the Open/Reveal buttons follow the entry
- # when it lands in (or moves to) a higher-precedence merge tier (codex F3).
- # F-3-4: switched from `effective_config_path` to the authoritative facade
- # so multi-project-tier cases resolve to the LATEST tier (matching F2
- # status semantics) instead of failing closed to `path_template`.
- row["config_path"] = ClientConfigurator.effective_authoritative_path(client_id)
- var config_path := String(row["config_path"])
- var has_path := not config_path.is_empty()
- var open_config_btn: Button = row["open_config_btn"]
- var reveal_btn: Button = row["reveal_btn"]
- open_config_btn.visible = has_path
- reveal_btn.visible = has_path
- open_config_btn.disabled = not has_path
- reveal_btn.disabled = not has_path
- if has_path:
- open_config_btn.tooltip_text = "Open config file:\n%s" % config_path
- reveal_btn.tooltip_text = "Reveal in folder:\n%s" % config_path.get_base_dir()
- else:
- open_config_btn.tooltip_text = ""
- reveal_btn.tooltip_text = ""
-
-
-# --- Update check & self-update ---
-
-## Tolerates a null manager so test fixtures that build the dock without
-## `_build_ui()` don't false-positive on the worker-spawn gate.
-func _is_self_update_in_progress() -> bool:
- return _update_install_in_flight
-
-
-func _on_update_pressed() -> void:
- if not _post_update_action.is_empty():
- post_update_action_requested.emit(_post_update_action)
- return
- ## Updating briefly disconnects AI tools while the add-on is replaced.
- ## A dock outside the scene tree has no dialog and proceeds directly.
- if _update_confirm != null and is_inside_tree():
- _update_confirm.dialog_text = update_confirm_text(
- _update_candidate_version, ClientConfigurator.get_plugin_version()
- )
- _update_confirm.popup_centered()
- return
- update_requested.emit()
-
-
-func _on_update_confirmed() -> void:
- update_requested.emit()
-
-
-static func update_confirm_text(version: String, current_version: String) -> String:
- var target := "Godot AI v%s" % version if not version.is_empty() else "the new Godot AI"
- var text := "Update to %s? Unsaved changes are kept." % target
- if McpServerVersionCheck.attached_bridges_follow(current_version, version):
- text += "\n\nRestart AI clients older than v4.0.4."
- else:
- text += "\n\nRestart your AI client after updating."
- return text
-
-
-func present_update_check(result: Dictionary) -> void:
- _update_candidate_version = String(result.get("version", ""))
- _update_label.text = String(result.get("label_text", ""))
- _update_label.add_theme_color_override("font_color", _UPDATE_LABEL_COLOR)
- _update_banner.visible = true
- ## A fresh candidate re-arms the action. The restarted editor after an
- ## update shows "Godot AI installed" with the button disabled; a newer release
- ## found later in that same session must still be installable. A running
- ## install or a pending post-update action keeps ownership of the button.
- if _update_install_in_flight or not _post_update_action.is_empty():
- return
- _set_update_status("")
- if _update_btn != null:
- _update_btn.text = _UPDATE_ACTION_TEXT
- _update_btn.disabled = false
-
-
-## Apply only the keys present so the manager can ship partial updates
-## (e.g. status-only during the download phase) without clobbering banner
-## state. `button_text` names an action ("Retry client migration");
-## progress and failure messages arrive as `status_text` and never replace
-## the button label.
-func present_update_state(state: Dictionary) -> void:
- if state.has("post_update_action"):
- _post_update_action = String(state["post_update_action"])
- if _post_update_action.is_empty() and _update_btn != null:
- _update_btn.text = _UPDATE_ACTION_TEXT
- if state.has("install_in_flight"):
- _update_install_in_flight = bool(state["install_in_flight"])
- if String(state.get("post_update_action", "")) == "retry":
- ## The migration barrier refused: the connection really is blocked.
- _post_update_server_pending = false
- elif bool(state.get("install_in_flight", false)) or String(state.get("outcome", "")) == "success":
- _post_update_server_pending = true
- if _status_label != null:
- _update_status()
- elif state.has("install_in_flight"):
- ## The install ended without a swap (`_fail_update`): the previous
- ## version is live and the transport status is the truth again.
- _post_update_server_pending = false
- if _status_label != null:
- _update_status()
- if state.has("button_text") and _update_btn != null:
- _update_btn.text = String(state["button_text"])
- if state.has("button_disabled") and _update_btn != null:
- _update_btn.disabled = bool(state["button_disabled"])
- if state.has("status_text"):
- _set_update_status(String(state["status_text"]))
- if state.has("label_text") and _update_label != null:
- _update_label.text = String(state["label_text"])
- if state.has("banner_visible") and _update_banner != null:
- _update_banner.visible = bool(state["banner_visible"])
- if state.has("label_text") and _update_label != null:
- ## Installation is distinct from server/client readiness. Instructions
- ## can still name failed migrations or clients that must reconnect.
- _update_label.add_theme_color_override("font_color", _UPDATE_LABEL_COLOR)
-
-
-func _set_update_status(text: String) -> void:
- if _update_status_label == null:
- return
- _update_status_label.text = text
- _update_status_label.visible = not text.is_empty()
diff --git a/addons/godot_ai/mcp_dock.gd.uid b/addons/godot_ai/mcp_dock.gd.uid
deleted file mode 100644
index 5868719..0000000
--- a/addons/godot_ai/mcp_dock.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b8yknttdjanm5
diff --git a/addons/godot_ai/migration_bridge.gd b/addons/godot_ai/migration_bridge.gd
deleted file mode 100644
index 7783bc2..0000000
--- a/addons/godot_ai/migration_bridge.gd
+++ /dev/null
@@ -1,8 +0,0 @@
-@tool
-extends Node
-
-## Retained capsule path: Godot can finish an old parser's dependency list
-## while loading the new plugin. Migration is complete in the canonical tree;
-## the verified capsule carries the active implementation at this same path.
-func start() -> void:
- queue_free()
diff --git a/addons/godot_ai/migration_bridge.gd.uid b/addons/godot_ai/migration_bridge.gd.uid
deleted file mode 100644
index d53f987..0000000
--- a/addons/godot_ai/migration_bridge.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://e3r4fem8egus
diff --git a/addons/godot_ai/migration_coordinator.gd b/addons/godot_ai/migration_coordinator.gd
deleted file mode 100644
index afb8775..0000000
--- a/addons/godot_ai/migration_coordinator.gd
+++ /dev/null
@@ -1,20 +0,0 @@
-@tool
-extends Node
-
-## Compatibility path for cached capsule dependencies after a signed update.
-## The canonical plugin has no remaining capsule installation to coordinate.
-const STATUS_SETTING := "godot_ai/v4_migration_bridge_status"
-
-
-func start(_package: Dictionary) -> void:
- queue_free()
-
-
-static func record_status(error: String) -> void:
- var settings := EditorInterface.get_editor_settings()
- if settings != null:
- settings.set_setting(status_setting(), JSON.stringify({"error": error}))
-
-
-static func status_setting() -> String:
- return STATUS_SETTING + "_" + ProjectSettings.globalize_path("res://").sha256_text()
diff --git a/addons/godot_ai/migration_coordinator.gd.uid b/addons/godot_ai/migration_coordinator.gd.uid
deleted file mode 100644
index 7b3ce25..0000000
--- a/addons/godot_ai/migration_coordinator.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://qqsn55jmog6v
diff --git a/addons/godot_ai/migration_fallback.gd b/addons/godot_ai/migration_fallback.gd
deleted file mode 100644
index eb59fcd..0000000
--- a/addons/godot_ai/migration_fallback.gd
+++ /dev/null
@@ -1,11 +0,0 @@
-@tool
-extends Node
-
-## Retained capsule dependency path. The canonical v4 tree has no embedded
-## v3 fallback; keeping this path must never initiate a downgrade.
-static func available() -> bool:
- return false
-
-
-func start() -> void:
- queue_free()
diff --git a/addons/godot_ai/migration_fallback.gd.uid b/addons/godot_ai/migration_fallback.gd.uid
deleted file mode 100644
index 0851f5c..0000000
--- a/addons/godot_ai/migration_fallback.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bym2nenea1nvd
diff --git a/addons/godot_ai/plugin.cfg b/addons/godot_ai/plugin.cfg
deleted file mode 100644
index 5657942..0000000
--- a/addons/godot_ai/plugin.cfg
+++ /dev/null
@@ -1,7 +0,0 @@
-[plugin]
-
-name="Godot AI"
-description="MCP server and AI tools for Godot"
-author="Godot AI"
-version="4.1.0"
-script="plugin.gd"
diff --git a/addons/godot_ai/plugin.gd b/addons/godot_ai/plugin.gd
deleted file mode 100644
index 77f617c..0000000
--- a/addons/godot_ai/plugin.gd
+++ /dev/null
@@ -1,1807 +0,0 @@
-@tool
-extends EditorPlugin
-
-const GAME_HELPER_AUTOLOAD_NAME := "_mcp_game_helper"
-const GAME_HELPER_AUTOLOAD_PATH := "res://addons/godot_ai/runtime/game_helper.gd"
-
-## Editor-process Logger subclass — captures parse errors, @tool runtime
-## errors, and push_error/push_warning so the LLM can read them via
-## `logs_read(source="editor")`.
-const EditorLogger := preload("res://addons/godot_ai/runtime/editor_logger.gd")
-
-const PluginReload := preload("res://addons/godot_ai/utils/plugin_reload.gd")
-const ReleaseVerifier := preload("res://addons/godot_ai/utils/release_verifier.gd")
-const UpdateInstaller := preload("res://addons/godot_ai/utils/update_installer.gd")
-const LIVE_ADDON_ROOT := "res://addons/godot_ai"
-const PLUGIN_CFG := "res://addons/godot_ai/plugin.cfg"
-const MIN_GODOT_MAJOR := 4
-const MIN_GODOT_MINOR := 7
-const UNSUPPORTED_GODOT_MESSAGE := \
- "Godot AI v4 requires Godot 4.7 or newer in the 4.x line; plugin remains inactive."
-
-## The lifecycle manager owns the serialized server episode and process
-## authority. This root only captures its immutable launch plan and routes
-## copied snapshots/results to the connection and Dock.
-const ServerLifecycleManager := preload("res://addons/godot_ai/utils/server_lifecycle.gd")
-const PortResolver := preload("res://addons/godot_ai/utils/port_resolver.gd")
-const ServerStateScript := preload("res://addons/godot_ai/utils/mcp_server_state.gd")
-const TransportCapability := preload("res://addons/godot_ai/utils/transport_capability.gd")
-
-## Plugin-class scripts used by this file. The script-local preload aliases
-## are ordinary dependency shorthand and keep construction sites compact.
-## They are not the self-update safety boundary. Whole-tree namespace swaps
-## happen inside the editor; the live tree is renamed, never overlaid, and the
-## only disable/scan/enable; no script mutates the tree that contains itself.
-const Connection := preload("res://addons/godot_ai/connection.gd")
-const Dispatcher := preload("res://addons/godot_ai/dispatcher.gd")
-const Telemetry := preload("res://addons/godot_ai/telemetry.gd")
-const LogBuffer := preload("res://addons/godot_ai/utils/log_buffer.gd")
-const GameLogBuffer := preload("res://addons/godot_ai/utils/game_log_buffer.gd")
-const EditorLogBuffer := preload("res://addons/godot_ai/utils/editor_log_buffer.gd")
-const SurfacedErrorTracker := preload("res://addons/godot_ai/utils/surfaced_error_tracker.gd")
-const Dock := preload("res://addons/godot_ai/mcp_dock.gd")
-const DebuggerPlugin := preload("res://addons/godot_ai/debugger/mcp_debugger_plugin.gd")
-const VisionRoutingScript := preload("res://addons/godot_ai/vision_routing.gd")
-const ExportPlugin := preload("res://addons/godot_ai/export/mcp_export_plugin.gd")
-const ClientConfigurator := preload("res://addons/godot_ai/client_configurator.gd")
-const CliExec := preload("res://addons/godot_ai/clients/_cli_exec.gd")
-const ClientJobOwner := preload("res://addons/godot_ai/utils/client_job_owner.gd")
-const UpdateManager := preload("res://addons/godot_ai/utils/update_manager.gd")
-const WindowsPortReservation := preload("res://addons/godot_ai/utils/windows_port_reservation.gd")
-const McpToolRegistry := preload("res://addons/godot_ai/custom_tools/mcp_tool_registry.gd")
-const McpServiceLocator := preload("res://addons/godot_ai/custom_tools/mcp_service_locator.gd")
-
-## Handlers are intentionally NOT preloaded here (#736). The old
-## `const X := preload("res://addons/godot_ai/handlers/...")` block pulled
-## every handler — and everything handlers preload — into plugin.gd's
-## compile closure, so Godot parsed/compiled ~119 addon scripts before the
-## first instruction of _enter_tree ran. GDScript has no cross-restart
-## compile cache, so that stalled "Initializing plugins" on every editor
-## boot and every plugin re-enable. Handlers are now registered by script
-## path via McpDispatcher.register_lazy_handler / register_lazy and are
-## load()ed at the first dispatch of one of their commands.
-##
-## Handlers remain preload-style scripts with no `class_name` so they don't
-## pollute the project-wide global scope (#253): a user project that happens
-## to define its own `InputHandler`, `SceneHandler`, etc. would otherwise
-## hard-error on plugin enable.
-const HANDLERS_DIR := "res://addons/godot_ai/handlers/"
-
-const STARTUP_TRACE_COUNTER_NAMES := [
- "powershell",
- "netstat",
- "netsh",
- "lsof",
- "http_status_probe",
- "server_command_discovery",
-]
-
-## Untyped on purpose — see policy below. Type fences move to handler `_init`
-## sites that take typed parameters.
-##
-## Self-update field and load-surface policy: plugin entry-load fields that
-## survive reload stay untyped. Typed fields against plugin-defined classes
-## were the #242 / #244 crash class: Godot can reparse a long-lived script
-## while its old field storage and the new type shape disagree. Static-var
-## initializers are the most dangerous form because they execute at
-## script-load; a top-level typed Dictionary/Array storage change can fail
-## before `_enter_tree` runs.
-##
-## The mitigation is two-part:
-## (1) Field declarations are untyped (this block).
-## (2) Construction and static access use local names declared at the top
-## of the file (e.g. `Connection`, `Dispatcher`, `LogBuffer`,
-## `ClientConfigurator`, `WindowsPortReservation`, ...), which keeps
-## this entry script's load surface explicit and reviewable.
-##
-## Constructors, constants, and static methods on `Mcp*` classes are not the
-## self-update safety metric. The installer swaps one exact, re-hashed tree
-## before the resource scan, so the root never observes an extracted-over-live
-## mixture. In short: preload aliases are not the self-update safety metric.
-##
-var _connection
-var _dispatcher
-var _telemetry
-var _log_buffer
-var _game_log_buffer
-var _editor_log_buffer
-var _surfaced_error_tracker
-var _editor_logger: Logger
-var _dock
-var _debugger_plugin
-var _vision_routing
-var _export_plugin
-var _custom_tool_registry
-var _custom_tool_service_locator
-## Plugin-lifetime owners. The Dock only emits intents and receives copied
-## snapshots/outcomes, so replacing the view cannot abandon a live Thread or
-## a self-update.
-var _client_jobs
-var _update_manager
-## Process identity continuity and the immutable terminal outcome cross only
-## as values. The coordinator retains neither this plugin nor its objects.
-var _post_update_outcome: Dictionary = {}
-## The version an update just replaced. An AI client attached through the
-## update may spawn a backend of that version into the restart window; the
-## restarted editor replaces it once instead of asking the user to.
-var _post_update_replaced_version := ""
-## Clients the post-update migration left unchanged (`{id, reason}`), named
-## in the dock's completion banner so the user knows to click Configure.
-## Untyped on purpose: a typed collection field is the hot-reload crash
-## class (#245) the self-update smoke injects to prove the swap survives it.
-var _post_update_deferred := []
-var _last_logged_block := ""
-## Bounded re-probes while that backend is still binding its port: a port
-## that is bound but not yet answering status reads as merely occupied.
-const POST_UPDATE_REPROBE_LIMIT := 10
-var _post_update_reprobes_left := POST_UPDATE_REPROBE_LIMIT
-var _post_update_retry_episode := 0
-## A pre-v4 server left on the port by a still-running v3 attach bridge
-## outlives the fast budget above: its lease lasts 30 s after that client
-## quits and its idle backstop another 120 s. Poll slowly across that
-## window so the editor comes up green once the user has relaunched the
-## client, without any replacement authority over a server we cannot
-## authenticate.
-const POST_UPDATE_STALE_REPROBE_SECONDS := 10.0
-const POST_UPDATE_STALE_REPROBE_LIMIT := 21
-var _post_update_stale_reprobes_left := POST_UPDATE_STALE_REPROBE_LIMIT
-## An old bridge spawns again as soon as the port frees, so one replacement
-## may not be the last; a few are allowed before the dock takes over.
-const POST_UPDATE_REPLACEMENT_LIMIT := 3
-var _post_update_replacements_left := POST_UPDATE_REPLACEMENT_LIMIT
-## Set once the live tree has been renamed; the lock then belongs to the restart.
-var _update_swapped := false
-var _post_update_action := ""
-var _normal_start_released := false
-var _update_barrier_blocked := false
-## Serialized lifecycle episode owner. Construction is inert; `_enter_tree`
-## supplies a copied plan and starts it only after composition completes.
-var _lifecycle
-static var _resolved_ws_port := ClientConfigurator.DEFAULT_WS_PORT
-var _endpoint_policy: Dictionary = {}
-var _headless_disabled := false
-var _startup_trace_enabled := false
-var _startup_trace_start_ms := 0
-var _startup_trace_last_ms := 0
-var _startup_trace_counters: Dictionary = {}
-## Startup-path probes can now run on a worker thread (#678); the trace
-## counters they bump are shared with the main thread, so serialize.
-var _startup_trace_mutex := Mutex.new()
-var _startup_trace_netsh_start_count := 0
-var _unsupported_engine := false
-var _loaded_plugin_version := ""
-
-
-func _init() -> void:
- _unsupported_engine = not _supports_godot_version(Engine.get_version_info())
- if _unsupported_engine:
- return
- _lifecycle = ServerLifecycleManager.new()
-
-
-func _enter_tree() -> void:
- if _unsupported_engine:
- push_error(UNSUPPORTED_GODOT_MESSAGE + _pending_swap_hint())
- return
- _startup_trace_begin()
-
- ## Keep main-thread result polling off until the vision worker owner exists.
- ## Unsupported/headless/barrier-blocked startup therefore has no process
- ## callback and constructs no worker-capable object.
- set_process(false)
-
- _loaded_plugin_version = get_plugin_version()
- ## An installed tree that was just swapped in by the updater (or by the
- ## v3 migration capsule) proves itself before anything else is built: the
- ## live tree must hash to exactly what the signed manifest promised. A
- ## mismatch restores the retained backup; a missing backup keeps the plugin
- ## inactive with the exact paths. Dev checkouts never self-update.
- if not ClientConfigurator.is_dev_checkout():
- var was_swapped := str(UpdateInstaller.read_pending().get("status", "")) == "swapped"
- var outcome: Dictionary = UpdateInstaller.verify_after_restart(LIVE_ADDON_ROOT)
- if was_swapped and not outcome.is_empty():
- _warn_if_verified_elsewhere(outcome)
- if not outcome.is_empty():
- _post_update_outcome = outcome.duplicate(true)
- _post_update_outcome["outcome"] = str(outcome.get("status", ""))
- var status := str(outcome.get("status", ""))
- if status == "repair_required":
- _block_update_startup(str(outcome.get("error", "")))
- return
- if status == "rolled_back" and was_swapped:
- ## The backup was just restored on disk while this process still
- ## runs the rejected tree. Restart into the restored tree; the next
- ## start presents the failure from the marker.
- push_error("MCP | update to %s failed and the previous version was restored: %s" % [
- str(outcome.get("to_version", "")), str(outcome.get("error", ""))
- ])
- UpdateInstaller.persist_next_start_enabled(PLUGIN_CFG)
- UpdateInstaller.request_restart.call_deferred()
- return
-
- _continue_enter_tree_after_update_barrier()
-
-
-## An update swapped in under one Godot and restarted into another (seen on
-## macOS, where the relaunch goes through LaunchServices by bundle). Say which
-## editor is waiting to verify it instead of refusing blindly.
-static func _pending_swap_hint() -> String:
- var pending := UpdateInstaller.read_pending()
- if str(pending.get("status", "")) != "swapped":
- return ""
- return (
- " An update to %s was swapped in under Godot %s and is waiting to be verified;"
- + " reopen the project in that Godot."
- ) % [str(pending.get("to_version", "")), str(pending.get("godot_version", "unknown"))]
-
-
-static func _warn_if_verified_elsewhere(outcome: Dictionary) -> void:
- var swapped_in := str(outcome.get("godot_version", ""))
- var running := str(Engine.get_version_info().get("string", ""))
- if swapped_in.is_empty() or swapped_in == running:
- return
- push_warning(
- "MCP | update to %s was swapped in under Godot %s; this editor is Godot %s"
- % [str(outcome.get("to_version", "")), swapped_in, running]
- )
-
-
-func _continue_enter_tree_after_update_barrier() -> void:
-
- ## Register only after the update barrier, but before the headless guard:
- ## `godot --headless --export-*` still needs to strip the game-helper
- ## autoload from exported packs. The export plugin is otherwise inert.
- _export_plugin = ExportPlugin.new()
- add_export_plugin(_export_plugin)
-
- if _mcp_disabled_for_headless_launch():
- _headless_disabled = true
- print("MCP | plugin disabled in headless mode")
- return
-
- ## Register port overrides before spawn so `http_port()` / `ws_port()`
- ## return the user's configured values (if any) when `_start_server`
- ## builds the CLI args.
- ClientConfigurator.ensure_settings_registered()
- _startup_trace_phase("settings_registered")
-
- _log_buffer = LogBuffer.new()
- ## Apply the persisted dock "Log" toggle before anything logs through the
- ## buffer. Without this the choice only took effect after a manual toggle
- ## and reset to noisy on every editor restart (#626).
- _log_buffer.enabled = McpSettings.mcp_logging_enabled()
-
- ## Capture desired endpoint settings without probing the OS. WS reservation
- ## is an activation effect on Windows (`netsh`), so it runs only after every
- ## owner and the Dock have been constructed and wired below.
- _endpoint_policy = ClientConfigurator.capture_endpoint_policy()
- _resolved_ws_port = int(_endpoint_policy.ws_port)
-
- ## Construct plugin-lifetime work owners before attaching the replaceable
- ## Dock. Constructors are inert; activate() is the single normal-start seam
- ## that Phase 6's durable activation barrier will guard.
- _client_jobs = ClientJobOwner.new()
- _client_jobs.snapshot_changed.connect(_on_client_work_snapshot_changed)
- _client_jobs.status_refresh_completed.connect(_on_client_status_refresh_completed)
- _client_jobs.mcp_status_completed.connect(_on_mcp_client_status_completed)
- _client_jobs.mcp_action_completed.connect(_on_mcp_client_action_completed)
- _client_jobs.post_update_repin_completed.connect(_on_post_update_repin_completed)
- _client_jobs.action_completed.connect(_on_client_action_completed)
- _client_jobs.action_timed_out.connect(_on_client_action_timed_out)
- add_child(_client_jobs)
- _update_manager = UpdateManager.new()
- _update_manager.update_check_completed.connect(_on_update_check_completed)
- _update_manager.install_state_changed.connect(_on_update_install_state_changed)
- _update_manager.activation_requested.connect(_on_update_activation_requested)
- add_child(_update_manager)
-
- _lifecycle.snapshot_changed.connect(_on_lifecycle_snapshot_changed)
- _lifecycle.transport_ready.connect(_on_lifecycle_transport_ready)
- _lifecycle.transport_cleared.connect(_on_lifecycle_transport_cleared)
- _lifecycle.startup_finished.connect(_startup_trace_finish)
-
- _game_log_buffer = GameLogBuffer.new()
- _editor_log_buffer = EditorLogBuffer.new()
- _surfaced_error_tracker = SurfacedErrorTracker.new(_editor_log_buffer, _game_log_buffer)
- _attach_editor_logger()
- _dispatcher = Dispatcher.new(_log_buffer, _surfaced_error_tracker)
- _dispatcher.mcp_logging = _log_buffer.enabled
- _startup_trace_phase("core_objects")
-
- _connection = Connection.new()
- _connection.log_buffer = _log_buffer
- _connection.surfaced_error_tracker = _surfaced_error_tracker
- _connection.ws_port = _resolved_ws_port
- _connection.auth_token = ""
- ## Pause-depth restore boundary (#712): the dispatcher rebalances any
- ## pause_processing level a crashed handler leaked.
- _dispatcher.pause_target = _connection
- _connection.connect_blocked = _lifecycle.is_connection_blocked()
- _connection.connect_block_reason = _lifecycle.get_status_dict().get("message", "")
- ## Replay the custom-tool catalog on (re)connect so tools registered
- ## before the initial connection or during a disconnect window reach
- ## the server — send_event silently drops while _connected is false.
- _connection.connection_state_changed.connect(_on_connection_state_changed)
-
- _telemetry = Telemetry.new(_connection)
-
- _debugger_plugin = DebuggerPlugin.new(_log_buffer, _game_log_buffer, _editor_log_buffer, _surfaced_error_tracker)
- _vision_routing = VisionRoutingScript.new()
- _vision_routing.log_buffer = _log_buffer
- set_process(true)
- _debugger_plugin.vision_routing = _vision_routing
- add_debugger_plugin(_debugger_plugin)
- _connection.debugger_plugin = _debugger_plugin
- _ensure_game_helper_autoload()
-
- ## Lazy handler registration (#736): declare each handler's script path
- ## and constructor args, then map every command to (handler_key, method).
- ## The dispatcher load()s + constructs a handler at the first dispatch of
- ## one of its commands and caches the instance, so this block is the
- ## authoritative command list without pulling any handler script into the
- ## boot-time compile closure. Constructor args are captured now (they are
- ## all plugin-lifetime objects) and released by _dispatcher.clear() in
- ## _exit_tree.
- var undo := get_undo_redo()
- _dispatcher.register_lazy_handler("editor", HANDLERS_DIR + "editor_handler.gd", [_log_buffer, _connection, _debugger_plugin, _game_log_buffer, _editor_log_buffer, null, _surfaced_error_tracker, _vision_routing])
- _dispatcher.register_lazy_handler("scene", HANDLERS_DIR + "scene_handler.gd", [_connection])
- _dispatcher.register_lazy_handler("node", HANDLERS_DIR + "node_handler.gd", [undo])
- _dispatcher.register_lazy_handler("project", HANDLERS_DIR + "project_handler.gd", [_connection, _debugger_plugin, _editor_log_buffer])
- _dispatcher.register_lazy_handler(
- "client",
- HANDLERS_DIR + "client_handler.gd",
- [_client_jobs],
- )
- _dispatcher.register_lazy_handler("script", HANDLERS_DIR + "script_handler.gd", [undo, _connection])
- _dispatcher.register_lazy_handler("resource", HANDLERS_DIR + "resource_handler.gd", [undo, _connection])
- _dispatcher.register_lazy_handler("api", HANDLERS_DIR + "api_handler.gd", [])
- _dispatcher.register_lazy_handler("filesystem", HANDLERS_DIR + "filesystem_handler.gd", [_connection])
- _dispatcher.register_lazy_handler("signal", HANDLERS_DIR + "signal_handler.gd", [undo])
- _dispatcher.register_lazy_handler("autoload", HANDLERS_DIR + "autoload_handler.gd", [])
- _dispatcher.register_lazy_handler("input", HANDLERS_DIR + "input_handler.gd", [])
- _dispatcher.register_lazy_handler("test", HANDLERS_DIR + "test_handler.gd", [undo, _log_buffer, _dispatcher, _connection])
- _dispatcher.register_lazy_handler("batch", HANDLERS_DIR + "batch_handler.gd", [_dispatcher, undo])
- _dispatcher.register_lazy_handler("ui", HANDLERS_DIR + "ui_handler.gd", [undo])
- _dispatcher.register_lazy_handler("theme", HANDLERS_DIR + "theme_handler.gd", [undo, _connection])
- _dispatcher.register_lazy_handler("animation", HANDLERS_DIR + "animation_handler.gd", [undo])
- _dispatcher.register_lazy_handler("material", HANDLERS_DIR + "material_handler.gd", [undo, _connection])
- _dispatcher.register_lazy_handler("particle", HANDLERS_DIR + "particle_handler.gd", [undo])
- _dispatcher.register_lazy_handler("camera", HANDLERS_DIR + "camera_handler.gd", [undo])
- _dispatcher.register_lazy_handler("audio", HANDLERS_DIR + "audio_handler.gd", [undo])
- _dispatcher.register_lazy_handler(
- "physics_shape", HANDLERS_DIR + "physics_shape_handler.gd", [undo, _connection]
- )
- _dispatcher.register_lazy_handler("environment", HANDLERS_DIR + "environment_handler.gd", [undo, _connection])
- _dispatcher.register_lazy_handler("texture", HANDLERS_DIR + "texture_handler.gd", [undo, _connection])
- _dispatcher.register_lazy_handler("curve", HANDLERS_DIR + "curve_handler.gd", [undo, _connection])
- _dispatcher.register_lazy_handler("control_draw_recipe", HANDLERS_DIR + "control_draw_recipe_handler.gd", [undo])
- _dispatcher.register_lazy_handler("tilemap", HANDLERS_DIR + "tilemap_handler.gd", [undo])
- _dispatcher.register_lazy_handler("tileset", HANDLERS_DIR + "tileset_handler.gd", [])
- _dispatcher.register_lazy_handler("gridmap", HANDLERS_DIR + "gridmap_handler.gd", [undo])
- _dispatcher.register_lazy_handler("csg", HANDLERS_DIR + "csg_handler.gd", [undo])
-
- _dispatcher.register_lazy("get_editor_state", "editor", &"get_editor_state")
- _dispatcher.register_lazy("get_scene_tree", "scene", &"get_scene_tree")
- _dispatcher.register_lazy("get_open_scenes", "scene", &"get_open_scenes")
- _dispatcher.register_lazy("find_nodes", "scene", &"find_nodes")
- _dispatcher.register_lazy("create_scene", "scene", &"create_scene")
- _dispatcher.register_lazy("open_scene", "scene", &"open_scene")
- _dispatcher.register_lazy("save_scene", "scene", &"save_scene")
- _dispatcher.register_lazy("save_scene_as", "scene", &"save_scene_as")
- _dispatcher.register_lazy("get_selection", "editor", &"get_selection")
- _dispatcher.register_lazy("create_node", "node", &"create_node")
- _dispatcher.register_lazy("delete_node", "node", &"delete_node")
- _dispatcher.register_lazy("reparent_node", "node", &"reparent_node")
- _dispatcher.register_lazy("set_property", "node", &"set_property")
- _dispatcher.register_lazy("rename_node", "node", &"rename_node")
- _dispatcher.register_lazy("duplicate_node", "node", &"duplicate_node")
- _dispatcher.register_lazy("move_node", "node", &"move_node")
- _dispatcher.register_lazy("add_to_group", "node", &"add_to_group")
- _dispatcher.register_lazy("remove_from_group", "node", &"remove_from_group")
- _dispatcher.register_lazy("set_selection", "node", &"set_selection")
- _dispatcher.register_lazy("get_node_properties", "node", &"get_node_properties")
- _dispatcher.register_lazy("get_children", "node", &"get_children")
- _dispatcher.register_lazy("get_groups", "node", &"get_groups")
- _dispatcher.register_lazy("get_logs", "editor", &"get_logs")
- _dispatcher.register_lazy("clear_logs", "editor", &"clear_logs")
- _dispatcher.register_lazy("take_screenshot", "editor", &"take_screenshot")
- _dispatcher.register_lazy("get_performance_monitors", "editor", &"get_performance_monitors")
- _dispatcher.register_lazy("reload_plugin", "editor", &"reload_plugin")
- _dispatcher.register_lazy("quit_editor", "editor", &"quit_editor")
- _dispatcher.register_lazy("game_eval", "editor", &"game_eval")
- _dispatcher.register_lazy("game_command", "editor", &"game_command")
- _dispatcher.register_lazy("game_debug_control", "editor", &"game_debug_control")
- _dispatcher.register_lazy("get_project_setting", "project", &"get_project_setting")
- _dispatcher.register_lazy("set_project_setting", "project", &"set_project_setting")
- _dispatcher.register_lazy("set_main_scene", "project", &"set_main_scene")
- _dispatcher.register_lazy("run_project", "project", &"run_project")
- _dispatcher.register_lazy("stop_project", "project", &"stop_project")
- _dispatcher.register_lazy("search_filesystem", "project", &"search_filesystem")
- _dispatcher.register_lazy("configure_client", "client", &"configure_client")
- _dispatcher.register_lazy("remove_client", "client", &"remove_client")
- _dispatcher.register_lazy("check_client_status", "client", &"check_client_status")
- _dispatcher.register_lazy("create_script", "script", &"create_script")
- _dispatcher.register_lazy("patch_script", "script", &"patch_script")
- _dispatcher.register_lazy("read_script", "script", &"read_script")
- _dispatcher.register_lazy("attach_script", "script", &"attach_script")
- _dispatcher.register_lazy("detach_script", "script", &"detach_script")
- _dispatcher.register_lazy("find_symbols", "script", &"find_symbols")
- _dispatcher.register_lazy("search_resources", "resource", &"search_resources")
- _dispatcher.register_lazy("load_resource", "resource", &"load_resource")
- _dispatcher.register_lazy("assign_resource", "resource", &"assign_resource")
- _dispatcher.register_lazy("create_resource", "resource", &"create_resource")
- _dispatcher.register_lazy("get_resource_info", "resource", &"get_resource_info")
- _dispatcher.register_lazy("get_class_info", "api", &"get_class_info")
- _dispatcher.register_lazy("read_file", "filesystem", &"read_file")
- _dispatcher.register_lazy("write_file", "filesystem", &"write_file")
- _dispatcher.register_lazy("reimport", "filesystem", &"reimport")
- _dispatcher.register_lazy("scan_filesystem", "filesystem", &"scan_filesystem")
- _dispatcher.register_lazy("list_signals", "signal", &"list_signals")
- _dispatcher.register_lazy("connect_signal", "signal", &"connect_signal")
- _dispatcher.register_lazy("disconnect_signal", "signal", &"disconnect_signal")
- _dispatcher.register_lazy("list_autoloads", "autoload", &"list_autoloads")
- _dispatcher.register_lazy("add_autoload", "autoload", &"add_autoload")
- _dispatcher.register_lazy("remove_autoload", "autoload", &"remove_autoload")
- _dispatcher.register_lazy("list_actions", "input", &"list_actions")
- _dispatcher.register_lazy("add_action", "input", &"add_action")
- _dispatcher.register_lazy("ensure_action", "input", &"ensure_action")
- _dispatcher.register_lazy("remove_action", "input", &"remove_action")
- _dispatcher.register_lazy("bind_event", "input", &"bind_event")
- _dispatcher.register_lazy("ensure_binding", "input", &"ensure_binding")
- _dispatcher.register_lazy("run_tests", "test", &"run_tests")
- _dispatcher.register_lazy("get_test_results", "test", &"get_test_results")
- _dispatcher.register_lazy("batch_execute", "batch", &"batch_execute")
- _dispatcher.register_lazy("set_anchor_preset", "ui", &"set_anchor_preset")
- _dispatcher.register_lazy("set_text", "ui", &"set_text")
- _dispatcher.register_lazy("build_layout", "ui", &"build_layout")
- _dispatcher.register_lazy("create_theme", "theme", &"create_theme")
- _dispatcher.register_lazy("theme_set_color", "theme", &"set_color")
- _dispatcher.register_lazy("theme_set_constant", "theme", &"set_constant")
- _dispatcher.register_lazy("theme_set_font_size", "theme", &"set_font_size")
- _dispatcher.register_lazy("theme_set_stylebox_flat", "theme", &"set_stylebox_flat")
- _dispatcher.register_lazy("apply_theme", "theme", &"apply_theme")
- _dispatcher.register_lazy("animation_player_create", "animation", &"create_player")
- _dispatcher.register_lazy("animation_create", "animation", &"create_animation")
- _dispatcher.register_lazy("animation_add_property_track", "animation", &"add_property_track")
- _dispatcher.register_lazy("animation_add_method_track", "animation", &"add_method_track")
- _dispatcher.register_lazy("animation_set_autoplay", "animation", &"set_autoplay")
- _dispatcher.register_lazy("animation_play", "animation", &"play")
- _dispatcher.register_lazy("animation_stop", "animation", &"stop")
- _dispatcher.register_lazy("animation_list", "animation", &"list_animations")
- _dispatcher.register_lazy("animation_get", "animation", &"get_animation")
- _dispatcher.register_lazy("animation_create_simple", "animation", &"create_simple")
- _dispatcher.register_lazy("animation_delete", "animation", &"delete_animation")
- _dispatcher.register_lazy("animation_validate", "animation", &"validate_animation")
- _dispatcher.register_lazy("animation_preset_fade", "animation", &"preset_fade")
- _dispatcher.register_lazy("animation_preset_slide", "animation", &"preset_slide")
- _dispatcher.register_lazy("animation_preset_shake", "animation", &"preset_shake")
- _dispatcher.register_lazy("animation_preset_pulse", "animation", &"preset_pulse")
- _dispatcher.register_lazy("material_create", "material", &"create_material")
- _dispatcher.register_lazy("material_set_param", "material", &"set_param")
- _dispatcher.register_lazy("material_set_shader_param", "material", &"set_shader_param")
- _dispatcher.register_lazy("material_get", "material", &"get_material")
- _dispatcher.register_lazy("material_list", "material", &"list_materials")
- _dispatcher.register_lazy("material_assign", "material", &"assign_material")
- _dispatcher.register_lazy("material_apply_to_node", "material", &"apply_to_node")
- _dispatcher.register_lazy("material_apply_preset", "material", &"apply_preset")
- _dispatcher.register_lazy("particle_create", "particle", &"create_particle")
- _dispatcher.register_lazy("particle_set_main", "particle", &"set_main")
- _dispatcher.register_lazy("particle_set_process", "particle", &"set_process")
- _dispatcher.register_lazy("particle_set_draw_pass", "particle", &"set_draw_pass")
- _dispatcher.register_lazy("particle_restart", "particle", &"restart_particle")
- _dispatcher.register_lazy("particle_get", "particle", &"get_particle")
- _dispatcher.register_lazy("particle_apply_preset", "particle", &"apply_preset")
- _dispatcher.register_lazy("camera_create", "camera", &"create_camera")
- _dispatcher.register_lazy("camera_configure", "camera", &"configure")
- _dispatcher.register_lazy("camera_set_limits_2d", "camera", &"set_limits_2d")
- _dispatcher.register_lazy("camera_set_damping_2d", "camera", &"set_damping_2d")
- _dispatcher.register_lazy("camera_follow_2d", "camera", &"follow_2d")
- _dispatcher.register_lazy("camera_get", "camera", &"get_camera")
- _dispatcher.register_lazy("camera_list", "camera", &"list_cameras")
- _dispatcher.register_lazy("camera_apply_preset", "camera", &"apply_preset")
- _dispatcher.register_lazy("audio_player_create", "audio", &"create_player")
- _dispatcher.register_lazy("audio_player_set_stream", "audio", &"set_stream")
- _dispatcher.register_lazy("audio_player_set_playback", "audio", &"set_playback")
- _dispatcher.register_lazy("audio_play", "audio", &"play")
- _dispatcher.register_lazy("audio_stop", "audio", &"stop")
- _dispatcher.register_lazy("audio_list", "audio", &"list_streams")
- _dispatcher.register_lazy("physics_shape_autofit", "physics_shape", &"autofit")
- _dispatcher.register_lazy("physics_shape_generate", "physics_shape", &"generate")
- _dispatcher.register_lazy("environment_create", "environment", &"create_environment")
- _dispatcher.register_lazy("gradient_texture_create", "texture", &"create_gradient_texture")
- _dispatcher.register_lazy("noise_texture_create", "texture", &"create_noise_texture")
- _dispatcher.register_lazy("curve_set_points", "curve", &"set_points")
- _dispatcher.register_lazy("control_draw_recipe", "control_draw_recipe", &"control_draw_recipe")
- _dispatcher.register_lazy("tilemap_set_cell", "tilemap", &"set_cell")
- _dispatcher.register_lazy("tilemap_set_cells_rect", "tilemap", &"set_cells_rect")
- _dispatcher.register_lazy("tilemap_clear", "tilemap", &"clear_layer")
- _dispatcher.register_lazy("tilemap_get_cells", "tilemap", &"get_used_cells")
- _dispatcher.register_lazy("tileset_get_atlas_tiles", "tileset", &"get_atlas_tiles")
- _dispatcher.register_lazy("tileset_get_atlas_image", "tileset", &"get_atlas_image")
- _dispatcher.register_lazy("gridmap_set_item", "gridmap", &"set_item")
- _dispatcher.register_lazy("gridmap_fill", "gridmap", &"fill")
- _dispatcher.register_lazy("gridmap_clear", "gridmap", &"clear_layer")
- _dispatcher.register_lazy("gridmap_get_used_cells", "gridmap", &"get_used_cells")
- _dispatcher.register_lazy("gridmap_list_library_items", "gridmap", &"list_library_items")
- _dispatcher.register_lazy("csg_create", "csg", &"create")
- _dispatcher.register_lazy("csg_set_operation", "csg", &"set_operation")
-
- _connection.dispatcher = _dispatcher
- add_child(_connection)
- _startup_trace_phase("handlers_registered")
-
- # Custom tool registry
- _custom_tool_service_locator = McpServiceLocator.new()
- _custom_tool_service_locator.setup(_connection, _log_buffer)
- _custom_tool_registry = McpToolRegistry.new()
- _custom_tool_registry.setup(_dispatcher, _custom_tool_service_locator)
- _custom_tool_registry.tools_changed.connect(_on_custom_tools_changed)
- _custom_tool_registry.mark_ready()
- _startup_trace_phase("custom_tools_ready")
-
- # Dock panel
- _dock = Dock.new()
- _dock.vision_routing = _vision_routing
- _dock.name = "Godot AI"
- _dock.update_requested.connect(_on_dock_update_requested)
- _dock.client_action_requested.connect(_on_dock_client_action_requested)
- _dock.client_status_refresh_requested.connect(_on_dock_client_status_refresh_requested)
- _dock.status_snapshot_requested.connect(_on_dock_status_snapshot_requested)
- _dock.live_server_probe_requested.connect(_on_dock_live_server_probe_requested)
- _dock.lifecycle_action_requested.connect(_on_dock_lifecycle_action_requested)
- _dock.dev_server_action_requested.connect(_on_dock_dev_server_action_requested)
- _dock.mcp_logging_changed.connect(_on_dock_mcp_logging_changed)
- _dock.log_snapshot_requested.connect(_on_dock_log_snapshot_requested)
- _dock.plugin_reload_requested.connect(_on_dock_plugin_reload_requested)
- _dock.settings_apply_requested.connect(_on_dock_settings_apply_requested)
- _dock.post_update_action_requested.connect(_on_dock_post_update_action_requested)
- _client_jobs.set_client_health_blocked(_client_health_is_blocked())
- _publish_dock_status_snapshots()
- add_control_to_dock(DOCK_SLOT_RIGHT_BL, _dock)
- _dock.present_client_work_snapshot(_client_jobs.snapshot())
- _startup_trace_phase("dock_attached")
- ## Activation barrier: no process, socket, or probe effect begins until all
- ## owners and the replaceable Dock have been constructed and wired.
- _activate_startup_endpoints()
-
-
-## Endpoint selection is retryable before the immutable launch policy or any
-## client migration worker exists. An old bridge keeps its own ports.
-func _activate_startup_endpoints() -> void:
- if str(_post_update_outcome.get("outcome", "")) == "success":
- var prepared := ClientConfigurator.prepare_major_upgrade_endpoints(
- str(_post_update_outcome.get("from_version", "")),
- str(_post_update_outcome.get("to_version", "")),
- )
- if not bool(prepared.get("ok", false)):
- _present_endpoint_setup_failure(str(prepared.get("error", "Endpoint selection failed.")))
- return
- var override := ClientConfigurator.v4_endpoint_ports_status()
- if not bool(override.get("ok", false)):
- _present_endpoint_setup_failure(str(override.get("error", "Invalid endpoint override.")))
- return
- var resolved_policy := ClientConfigurator.capture_endpoint_policy()
- var http_port := int(resolved_policy.http_port)
- var configured_ws := int(resolved_policy.ws_port)
- if (
- http_port < ClientConfigurator.MIN_PORT or http_port > ClientConfigurator.MAX_PORT
- or configured_ws < ClientConfigurator.MIN_PORT or configured_ws > ClientConfigurator.MAX_PORT
- or http_port == configured_ws
- ):
- _present_endpoint_setup_failure("Choose distinct HTTP and WebSocket ports between %d and %d in Godot AI settings." % [ClientConfigurator.MIN_PORT, ClientConfigurator.MAX_PORT])
- return
- var resolved_ws := _resolve_ws_port(configured_ws)
- if (
- resolved_ws < ClientConfigurator.MIN_PORT or resolved_ws > ClientConfigurator.MAX_PORT
- or resolved_ws == http_port
- or (bool(override.present) and resolved_ws != configured_ws)
- ):
- _present_endpoint_setup_failure("The configured WebSocket port is unavailable. Choose another endpoint pair in Godot AI settings, then retry.")
- return
- resolved_policy["ws_port"] = resolved_ws
- resolved_policy["capability_path"] = TransportCapability.path_for_http_port(http_port)
- _set_endpoint_policy(resolved_policy)
- if _connection != null:
- _connection.ws_port = resolved_ws
- if _post_update_action == "retry_endpoints":
- _post_update_action = ""
- if _dock != null:
- _dock.present_update_state({"post_update_action": "", "status_text": "", "label_text": "", "banner_visible": false})
- ## #691: publish every environment/setting value before the first worker.
- ClientConfigurator.warm_env_snapshot(_endpoint_policy)
- _lifecycle.configure(_capture_lifecycle_plan())
- _begin_startup_release()
-
-
-func _present_endpoint_setup_failure(error: String) -> void:
- _post_update_action = "retry_endpoints"
- _lifecycle._block_without_effect("endpoint_setup_failed", error)
- if _dock != null:
- _dock.present_update_state({
- "install_in_flight": false,
- "button_text": "Retry endpoint setup",
- "status_text": "Client endpoint setup failed",
- "button_disabled": false,
- "label_text": error,
- "banner_visible": true,
- "post_update_action": "retry_endpoints",
- })
-
-
-func _client_health_is_blocked() -> bool:
- return ServerStateScript.blocks_client_health(
- int(_lifecycle.get_status_dict().get("state", ServerStateScript.UNINITIALIZED))
- )
-
-
-func _on_dock_client_action_requested(client_id: String, action: String) -> void:
- if _client_jobs == null:
- return
- if not _client_jobs.request_action(client_id, action) and _dock != null:
- _dock.present_client_work_snapshot(_client_jobs.snapshot())
-
-
-func _on_dock_client_status_refresh_requested(client_ids: Array[String], force: bool) -> void:
- if _client_jobs == null:
- return
- _client_jobs.set_client_health_blocked(_client_health_is_blocked())
- _client_jobs.request_status_refresh(client_ids, force)
-
-
-func _on_client_work_snapshot_changed(snapshot: Dictionary) -> void:
- if _dock != null:
- _dock.present_client_work_snapshot(snapshot)
-
-
-func _on_client_status_refresh_completed(results: Dictionary) -> void:
- if _dock != null:
- _dock.present_client_status_refresh_results(results)
-
-
-func _on_mcp_client_status_completed(
- request_ids: Array[String], payload: Dictionary
-) -> void:
- if _connection == null:
- return
- for request_id in request_ids:
- _connection.send_deferred_response(request_id, payload)
-
-
-func _on_mcp_client_action_completed(request_id: String, payload: Dictionary) -> void:
- if _connection != null:
- _connection.send_deferred_response(request_id, payload)
-
-
-func _on_client_action_completed(
- client_id: String, action: String, result: Dictionary, prewarm: Dictionary
-) -> void:
- if _dock != null:
- _dock.present_client_action_result(client_id, action, result, prewarm)
-
-
-func _on_client_action_timed_out(client_id: String, action: String, detail: String) -> void:
- if _dock != null:
- _dock.present_client_action_timeout(client_id, action, detail)
-
-
-func _transport_snapshot_for_dock() -> Dictionary:
- if _connection == null:
- return {"connected": false, "server_version": "", "status": {}}
- return {
- "connected": _connection.is_connected,
- "server_version": _connection.server_version,
- "status": _connection.get_transport_status(),
- }
-
-
-func _lifecycle_snapshot_for_dock() -> Dictionary:
- if _lifecycle == null:
- return {
- "state": ServerStateScript.UNINITIALIZED,
- "server_pid": -1,
- "resolved_ws_port": _resolved_ws_port,
- "can_restart_managed": false,
- "can_recover_incompatible": false,
- "normal_start_released": _normal_start_released,
- }
- var snapshot: Dictionary = _lifecycle.get_status_dict().duplicate(true)
- snapshot["server_pid"] = _lifecycle.get_server_pid()
- snapshot["resolved_ws_port"] = _resolved_ws_port
- snapshot["can_restart_managed"] = (
- _normal_start_released and _lifecycle.can_restart_managed_server()
- )
- snapshot["can_recover_incompatible"] = (
- _normal_start_released and bool(snapshot.get("can_recover_incompatible", false))
- )
- snapshot["normal_start_released"] = _normal_start_released
- if _post_update_retry_episode > 0 and int(snapshot.get("episode_id", 0)) == _post_update_retry_episode:
- var episode: Dictionary = _lifecycle.episode_snapshot()
- if str(episode.get("state", "")) == "BLOCKED" and str(episode.get("reason", "")) == "launch_gone" and str(episode.get("proof_pending_reason", "")) == "capability_pair":
- ## Only the presentation changes; transport remains blocked until proof.
- snapshot["state"] = ServerStateScript.SPAWNING
- snapshot["handoff_retry_pending"] = true
- return snapshot
-
-
-func _publish_dock_status_snapshots() -> void:
- if _dock == null:
- return
- _dock.present_transport_snapshot(_transport_snapshot_for_dock())
- _dock.present_lifecycle_snapshot(_lifecycle_snapshot_for_dock())
-
-
-func _on_dock_status_snapshot_requested() -> void:
- _publish_dock_status_snapshots()
-
-
-func _on_dock_live_server_probe_requested(port: int) -> void:
- if _dock != null:
- _dock.present_live_server_probe_result(
- ServerLifecycleManager.probe_live_server_status(
- port, ServerLifecycleManager.DEFAULT_PROBE_TIMEOUT_MS,
- str(_endpoint_policy.get("capability_path", ""))
- )
- )
-
-
-func _on_dock_lifecycle_action_requested(action: int) -> void:
- if not _normal_start_released:
- if _dock != null:
- _dock.present_lifecycle_action_result(false)
- _publish_dock_status_snapshots()
- return
- var accepted := false
- match action:
- Dock.LifecycleAction.RECOVER_INCOMPATIBLE:
- accepted = bool(await recover_incompatible_server())
- Dock.LifecycleAction.RESTART_SERVER:
- accepted = force_restart_server()
- if _dock != null:
- _dock.present_lifecycle_action_result(accepted)
- _publish_dock_status_snapshots()
-
-
-func _on_dock_dev_server_action_requested(action: int) -> void:
- if action != Dock.DevServerAction.STOP and not _normal_start_released:
- _publish_dock_status_snapshots()
- return
- match action:
- Dock.DevServerAction.START_OR_RESTART:
- if _telemetry != null:
- _telemetry.record_dev_server_toggle("start")
- restart_or_start_managed_server()
- Dock.DevServerAction.STOP:
- if _telemetry != null:
- _telemetry.record_dev_server_toggle("stop")
- stop_managed_server()
- _publish_dock_status_snapshots()
-
-
-func _on_dock_mcp_logging_changed(enabled: bool) -> void:
- if _dispatcher != null:
- _dispatcher.mcp_logging = enabled
- if _log_buffer != null:
- _log_buffer.enabled = enabled
-
-
-func _on_dock_log_snapshot_requested(after_sequence: int) -> void:
- if _dock == null or _log_buffer == null:
- return
- var sequence: int = _log_buffer.total_logged()
- var reset: bool = sequence < after_sequence
- var count: int = sequence if reset else maxi(0, sequence - after_sequence)
- _dock.present_log_snapshot({
- "sequence": sequence,
- "reset": reset,
- "lines": _log_buffer.get_recent(count),
- })
-
-
-func _on_dock_plugin_reload_requested(reason: String) -> void:
- Telemetry.record_pending_plugin_reload(reason)
- ## The reload frees this plugin, so it must not run on one of this
- ## instance's own frames: defer the static call itself, never a method of
- ## this instance. A deferred method that reloads synchronously returns into
- ## a freed script and takes the editor down (SIGBUS/SIGABRT after
- ## "Bad address index").
- PluginReload.reload_enabled_plugin.call_deferred()
-
-
-func _on_dock_settings_apply_requested(changes: Dictionary, reload: bool) -> void:
- var applied := ClientConfigurator.apply_endpoint_settings(changes.duplicate(true))
- if not bool(applied.get("ok", false)):
- push_error("MCP | refused settings change: %s" % str(applied.get("error", "invalid settings")))
- return
- ## #913: the setting is persisted, so push any opt-out to the live server —
- ## the reload below replaces this plugin, not a server it merely adopted.
- ## Best-effort: the reload may cut the socket first, and the reconnect
- ## re-asserts it from `Telemetry._on_connection_state_changed` anyway.
- if _telemetry != null:
- _telemetry.assert_opt_out()
- if reload:
- _on_dock_plugin_reload_requested("endpoint_settings")
-
-
-func _on_dock_update_requested() -> void:
- if _update_manager != null and _update_manager.has_install_candidate():
- _update_manager.start_install(_preflight_update())
-
-
-func _on_update_check_completed(result: Dictionary) -> void:
- if _dock != null:
- _dock.present_update_check(result)
-
-
-func _on_update_install_state_changed(state: Dictionary) -> void:
- if not bool(state.get("install_in_flight", true)) and _client_jobs != null:
- _client_jobs.resume_after_quiesce()
- if not bool(state.get("install_in_flight", true)) and not _update_swapped:
- UpdateInstaller.release_lock()
- if _dock != null:
- _dock.present_update_state(state)
-
-
-func _on_update_activation_requested(package: Dictionary) -> void:
- install_downloaded_update(package)
-
-
-## Hold successful activation behind the client-migration barrier. Rollback
-## resumes normally after recording its terminal outcome.
-func _begin_startup_release() -> void:
- if str(_post_update_outcome.get("outcome", "")) != "success":
- if not _post_update_outcome.is_empty():
- _present_post_update_failure()
- UpdateInstaller.clear_pending()
- _fan_post_update_outcome()
- _release_normal_startup()
- return
- var started: Dictionary = _client_jobs.begin_post_update_repin(
- str(_post_update_outcome.get("from_version", "")),
- str(_post_update_outcome.get("to_version", "")),
- bool(_post_update_outcome.get(
- "replace_owned_mismatches",
- _post_update_outcome.get("manual_migration", false),
- )),
- )
- if not bool(started.get("ok", false)):
- _present_post_update_barrier_failure(str(started.get("error", "Client migration could not start.")))
- return
- _post_update_action = ""
- if _dock != null:
- _dock.present_update_state({
- "install_in_flight": true,
- "status_text": "Migrating client configuration…",
- "button_disabled": true,
- "label_text": "The server remains stopped until configured clients are repinned.",
- "banner_visible": true,
- "post_update_action": "",
- })
-
-
-func _on_post_update_repin_completed(result: Dictionary) -> void:
- if str(_post_update_outcome.get("outcome", "")) != "success":
- return
- if not bool(result.get("ok", false)):
- _present_post_update_barrier_failure(str(result.get("error", "Client migration failed.")))
- return
- for client_id in result.get("foreign_ids", []):
- push_warning(
- "MCP | the %s entry named godot-ai launches something else; it was left unchanged. Use Configure in the dock to replace it."
- % str(client_id)
- )
- _post_update_deferred = []
- for entry in result.get("deferred", []):
- if entry is Dictionary:
- _post_update_deferred.append((entry as Dictionary).duplicate(true))
- push_warning(
- "MCP | %s was not migrated automatically because %s; it was left unchanged. Use Configure in the dock."
- % [str(entry.get("id", "")), str(entry.get("reason", ""))]
- )
- ## A click cannot prove that an external client restarted. The enforceable
- ## boundary is the one we own: repin its configuration, mark the update
- ## complete, then start and authenticate that server. A major upgrade can
- ## select independent ports; old clients must reload the migrated config,
- ## but cannot hold the new editor endpoint behind their existing leases.
- _finish_post_update()
-
-
-func _present_post_update_barrier_failure(error: String) -> void:
- _post_update_action = "retry"
- push_error("MCP | post-update client migration blocked startup: %s" % error)
- if _dock != null:
- _dock.present_update_state({
- "install_in_flight": false,
- "button_text": "Retry client migration",
- "status_text": "Installed — client migration failed",
- "button_disabled": false,
- "label_text": "Server startup is blocked: %s" % error,
- "banner_visible": true,
- "post_update_action": "retry",
- })
-
-
-func _on_dock_post_update_action_requested(action: String) -> void:
- if action != _post_update_action:
- return
- if action == "retry_endpoints":
- _activate_startup_endpoints()
- elif action == "retry":
- _begin_startup_release()
-
-
-func _finish_post_update() -> void:
- print("MCP | client migration completed")
- ## The success marker stays as the durable record of the last update; the
- ## next swap overwrites it and preflight only refuses swapped/repair states.
- ## Recording the migration keeps later starts from repeating it.
- var recorded := UpdateInstaller.record_clients_migrated()
- if recorded != OK:
- push_warning("MCP | could not record client migration in the update marker: %s" % error_string(recorded))
- ## In-editor updates retain old script graphs for undo. Keep their backing
- ## files until a fresh editor process can safely prune older generations.
- if not get_tree().root.has_meta("godot_ai_retained_update_scripts"):
- UpdateInstaller.prune_backups(str(_post_update_outcome.get("from_version", "")))
- _post_update_replaced_version = str(_post_update_outcome.get("from_version", ""))
- _post_update_reprobes_left = POST_UPDATE_REPROBE_LIMIT
- _post_update_stale_reprobes_left = POST_UPDATE_STALE_REPROBE_LIMIT
- _post_update_replacements_left = POST_UPDATE_REPLACEMENT_LIMIT
- var to_version := str(_post_update_outcome.get("to_version", ""))
- if McpServerVersionCheck.attached_bridges_follow(_post_update_replaced_version, to_version):
- print("MCP | AI clients using v%s can reconnect to v%s; relaunch clients still using older versions" % [_post_update_replaced_version, to_version])
- else:
- print(
- "MCP | Refresh the Godot AI MCP connection and reload its configuration once to use v%s; relaunch the AI app if it cannot reload the configuration"
- % to_version
- )
- _present_post_update_complete()
- _fan_post_update_outcome()
- _release_normal_startup()
-
-
-func _present_post_update_failure() -> void:
- var error := str(_post_update_outcome.get("error", ""))
- var to_version := str(_post_update_outcome.get("to_version", ""))
- push_error("MCP | update to %s failed; the previous version is live: %s" % [to_version, error])
- if _dock != null:
- _dock.present_update_state({
- "install_in_flight": false,
- "status_text": "Update failed — previous version restored",
- "button_disabled": false,
- "label_text": error,
- "banner_visible": true,
- "post_update_action": "",
- })
-
-
-func _present_post_update_complete() -> void:
- _post_update_action = ""
- if _dock != null:
- _dock.present_update_state({
- "install_in_flight": false,
- "status_text": "Godot AI installed" if _post_update_deferred.is_empty() else "Installed — client setup needed",
- "button_disabled": true,
- "label_text": _post_update_complete_label(),
- "banner_visible": true,
- "post_update_action": "",
- "outcome": "success",
- })
-
-
-func _post_update_complete_label() -> String:
- var to_version := str(_post_update_outcome.get("to_version", ""))
- var from_version := str(_post_update_outcome.get("from_version", ""))
- var text := (
- "AI clients already using v%s can reconnect to v%s without restarting. Refresh older MCP connections and reload their configuration; relaunch the AI app if needed." % [from_version, to_version]
- if McpServerVersionCheck.attached_bridges_follow(from_version, to_version)
- else "Refresh the Godot AI MCP connection and reload its configuration once to use v%s. If the AI app cannot reload its configuration, quit and relaunch it."
- % to_version
- )
- if _post_update_deferred.is_empty():
- return text
- var named: Array[String] = []
- for entry in _post_update_deferred:
- var client_id := str(entry.get("id", ""))
- var client := McpClientRegistry.get_by_id(client_id)
- var name: String = client.display_name if client != null else client_id
- named.append("%s (%s)" % [name, str(entry.get("reason", "not migrated"))])
- return text + " Not migrated: %s. Use Configure to replace them." % ", ".join(named)
-
-
-## Sole release point for ordinary work and the server lifecycle. Keeping
-## these effects together makes the post-update legal ordering reviewable.
-func _release_normal_startup() -> void:
- if _normal_start_released:
- return
- _normal_start_released = true
- _client_jobs.set_client_health_blocked(_client_health_is_blocked())
- _client_jobs.activate()
- _update_manager.check_for_updates.call_deferred()
- _start_server()
- _startup_trace_phase("server_start")
- _log_buffer.log("plugin loaded")
- if _telemetry != null:
- _telemetry.record_dock_startup()
- _telemetry.flush_pending_plugin_reload()
-
-
-## Fan one immutable terminal update outcome. It is held only in this
-## instance's memory, never EditorSettings, and is cleared exactly once.
-func _fan_post_update_outcome() -> void:
- if _post_update_outcome.is_empty():
- return
- var outcome := str(_post_update_outcome.get("outcome", ""))
- var status := "unknown"
- match outcome:
- "success":
- status = "success"
- "rolled_back":
- status = "failed_clean"
- "repair_required", "quarantined":
- status = "failed_mixed"
- var error := "" if outcome == "success" else str(_post_update_outcome.get("error", outcome.replace("_", " ")))
- var from_version := str(_post_update_outcome.get("from_version", ""))
- var to_version := str(_post_update_outcome.get("to_version", ""))
- if _telemetry != null and not bool(_post_update_outcome.get("manual_migration", false)):
- _telemetry.record_self_update(status, from_version, to_version, error)
- _post_update_outcome.clear()
-
-
-func _block_update_startup(reason: String) -> void:
- _update_barrier_blocked = true
- push_error("MCP | update recovery blocked plugin startup: %s" % reason)
- _disable_after_update_barrier.call_deferred()
-
-
-func _disable_after_update_barrier() -> void:
- ## A live tree that neither matches its manifest nor has a backup to
- ## restore must not run. Disable the shell so nothing is built from it;
- ## the marker keeps the exact paths for manual recovery.
- if _update_barrier_blocked:
- print("MCP | disabling plugin after update barrier refusal")
- EditorInterface.set_plugin_enabled("res://addons/godot_ai/plugin.cfg", false)
-
-
-func _exit_tree() -> void:
- if _unsupported_engine:
- return
- set_process(false)
- ## Registered before the headless guard in _enter_tree, so it must be
- ## removed before the headless early-return here too.
- if _export_plugin != null:
- remove_export_plugin(_export_plugin)
- _export_plugin = null
-
- if _headless_disabled:
- _headless_disabled = false
- ## Ported from upstream PR #936 at
- ## 537a490c865837bedb96042d10ee0fc74673cd99: `_lifecycle` is built in
- ## _init(), before the headless guard in _enter_tree() runs, so it exists
- ## even on this path — null it here too (full teardown below is skipped).
- _lifecycle = null
- return
-
- if _update_barrier_blocked:
- _update_barrier_blocked = false
- _lifecycle = null
- return
-
- ## Client work has plugin lifetime, independent of the Dock. Realize every
- ## thread before either owner script can be reloaded or freed.
- if _client_jobs != null:
- _client_jobs.quiesce()
-
- if _custom_tool_registry != null:
- _custom_tool_registry.clear()
- _custom_tool_registry = null
-
- _custom_tool_service_locator = null
-
- ## Outer-to-inner teardown. Dispatcher Callables hold RefCounted handlers
- ## alive past the point where Godot reloads their class_name scripts — the
- ## first post-reload call into a typed-array-holding handler (e.g.
- ## McpGameLogBuffer._storage) then SIGSEGVs against a stale class descriptor.
- ## See issue #46.
-
- # Stop inbound work first so _process can't enqueue new commands or
- # null-deref log_buffer on the next tick mid-teardown.
- if _connection:
- _connection.teardown()
-
- if _vision_routing:
- _vision_routing.shutdown()
- _vision_routing = null
- # Transport is stopped and both worker owners have joined. Release the
- # ordinary plugin-lifetime graph; this is not the stronger hot script-swap
- # authorization (prepare_for_update_reload still requires clear()).
- if _dispatcher:
- _dispatcher.release_after_teardown()
-
- if _dock:
- _dock.release_editor_progress_dialog()
- remove_control_from_docks(_dock)
- _dock.queue_free()
- _dock = null
- if _update_manager:
- _update_manager.cancel_install()
- _update_manager.queue_free()
- _update_manager = null
- if _client_jobs:
- _client_jobs.queue_free()
- _client_jobs = null
- if _connection:
- _connection.queue_free()
- _connection = null
- if _debugger_plugin:
- remove_debugger_plugin(_debugger_plugin)
- _debugger_plugin = null
-
- ## Detach the editor logger BEFORE nulling the buffer. After remove_logger
- ## returns, Godot guarantees no further virtual calls — so the logger's
- ## next access to `_buffer` (if any in flight) lands on a still-live
- ## ref-counted buffer, not a freed one.
- _detach_editor_logger()
-
- _dispatcher = null
- _log_buffer = null
- _game_log_buffer = null
- _editor_log_buffer = null
- _surfaced_error_tracker = null
-
- ## Teardown follows the immutable launch plan: keep-on-exit or an active
- ## lease detaches; otherwise the exact owned-process grant is stopped.
- _lifecycle.teardown_for_editor_exit()
- _lifecycle = null
- print("MCP | plugin unloaded")
-
-
-func _process(_delta: float) -> void:
- if _vision_routing != null:
- _vision_routing.poll_completed()
-
-
-## Attach editor_logger.gd as a Godot logger so editor-process script
-## errors (parse errors, @tool runtime errors, EditorPlugin errors,
-## push_error/push_warning) flow into _editor_log_buffer for
-## logs_read(source="editor").
-##
-## Limitation called out in the issue: parse errors fired *before* the
-## plugin's _enter_tree (e.g. during the editor's initial filesystem
-## scan, or for scripts that fail on first project open) happen before
-## add_logger is called and are not captured. There's no public API to
-## drain the editor's already-emitted error history; rescanning the
-## file would re-emit them but at the cost of disrupting the user's
-## editing state, so we accept the gap.
-func _attach_editor_logger() -> void:
- _editor_logger = EditorLogger.new(_editor_log_buffer)
- OS.add_logger(_editor_logger)
-
-
-func _detach_editor_logger() -> void:
- if _editor_logger != null:
- OS.remove_logger(_editor_logger)
- _editor_logger = null
-
-
-## Register the game-side autoload on plugin enable. Runs the helper inside
-## the game process so the editor-side debugger plugin can request
-## framebuffer captures over EngineDebugger messages. Removed on
-## _disable_plugin so disabling the plugin leaves project.godot clean.
-func _enable_plugin() -> void:
- if _mcp_disabled_for_headless_launch():
- return
- _ensure_game_helper_autoload()
-
-
-static func _mcp_disabled_for_headless_launch() -> bool:
- return _mcp_disabled_for_headless(
- OS.get_cmdline_args(),
- DisplayServer.get_name(),
- OS.get_environment("GODOT_AI_ALLOW_HEADLESS")
- )
-
-
-static func _mcp_disabled_for_headless(args: PackedStringArray, display_name: String, allow_value: String) -> bool:
- if McpSettings.truthy(allow_value):
- return false
- return _args_request_headless(args) or display_name.to_lower() == "headless"
-
-
-static func _args_request_headless(args: PackedStringArray) -> bool:
- for i in range(args.size()):
- var arg := args[i]
- if arg == "--headless":
- return true
- if arg == "--display-driver" and i + 1 < args.size() and args[i + 1] == "headless":
- return true
- if arg.begins_with("--display-driver=") and arg.get_slice("=", 1) == "headless":
- return true
- return false
-
-
-
-
-func _disable_plugin() -> void:
- var key := "autoload/" + GAME_HELPER_AUTOLOAD_NAME
- if not ProjectSettings.has_setting(key):
- return
- ProjectSettings.clear(key)
- ProjectSettings.save()
-
-
-func _ensure_game_helper_autoload() -> void:
- ## Write the autoload directly to ProjectSettings and save immediately.
- ## EditorPlugin.add_autoload_singleton only mutates in-memory settings —
- ## the on-disk project.godot is only persisted when the editor saves
- ## (e.g. on quit). CI spawns the game subprocess before any save fires,
- ## so the child process never sees the autoload and the capture times
- ## out. Mirror AutoloadHandler's pattern: set_setting + save().
- var key := "autoload/" + GAME_HELPER_AUTOLOAD_NAME
- var value := "*" + GAME_HELPER_AUTOLOAD_PATH # "*" prefix = singleton
- if ProjectSettings.get_setting(key, "") == value:
- return ## already registered with the right target
- ProjectSettings.set_setting(key, value)
- ProjectSettings.set_initial_value(key, "")
- ProjectSettings.set_as_basic(key, true)
- var err := ProjectSettings.save()
- if err != OK:
- push_warning("MCP: failed to save project.godot after registering %s autoload (error %d)"
- % [GAME_HELPER_AUTOLOAD_NAME, err])
-
-
-func _startup_trace_begin() -> void:
- _startup_trace_enabled = ClientConfigurator.startup_trace_enabled()
- if not _startup_trace_enabled:
- return
- _startup_trace_start_ms = Time.get_ticks_msec()
- _startup_trace_last_ms = _startup_trace_start_ms
- _startup_trace_netsh_start_count = WindowsPortReservation.netsh_query_count()
- _startup_trace_counters.clear()
- for counter in STARTUP_TRACE_COUNTER_NAMES:
- _startup_trace_counters[counter] = 0
- print(
- "MCP startup trace | begin platform=%s http_port=%d ws_port=%d"
- % [
- OS.get_name(),
- ClientConfigurator.http_port(),
- ClientConfigurator.ws_port(),
- ]
- )
-
-
-func _startup_trace_count(counter: String, amount: int = 1) -> void:
- if not _startup_trace_enabled:
- return
- _startup_trace_mutex.lock()
- _startup_trace_counters[counter] = int(_startup_trace_counters.get(counter, 0)) + amount
- _startup_trace_mutex.unlock()
-
-
-func _startup_trace_phase(name: String) -> void:
- if not _startup_trace_enabled:
- return
- var now := Time.get_ticks_msec()
- print(
- "MCP startup trace | phase=%s delta_ms=%d total_ms=%d"
- % [name, now - _startup_trace_last_ms, now - _startup_trace_start_ms]
- )
- _startup_trace_last_ms = now
-
-
-func _startup_trace_finish(path: String) -> void:
- if not _startup_trace_enabled:
- return
- var now := Time.get_ticks_msec()
- ## Same lock as _startup_trace_count — a worker probe may still be
- ## bumping counters while this reads/writes the shared dictionary.
- _startup_trace_mutex.lock()
- _startup_trace_counters["netsh"] = (
- WindowsPortReservation.netsh_query_count() - _startup_trace_netsh_start_count
- )
- var counters_snapshot: Dictionary = _startup_trace_counters.duplicate()
- _startup_trace_mutex.unlock()
- print(
- "MCP startup trace | done path=%s total_ms=%d counters=%s"
- % [path, now - _startup_trace_start_ms, str(counters_snapshot)]
- )
-
-
-func _start_server() -> void:
- if not _normal_start_released:
- push_error("MCP | server start refused before normal startup release")
- return
- _lifecycle.start_server()
-
-
-func _capture_lifecycle_plan() -> Dictionary:
- var policy := _endpoint_policy.duplicate(true)
- var http_port := int(policy.get("http_port", ClientConfigurator.DEFAULT_HTTP_PORT))
- var worktree_src := ""
- if ClientConfigurator.is_dev_checkout():
- worktree_src = ClientConfigurator.find_worktree_src_dir(
- ProjectSettings.globalize_path("res://")
- )
- return {
- "http_port": http_port,
- "capability_path": str(policy.get("capability_path", "")),
- "ws_port": int(policy.get("ws_port", ClientConfigurator.DEFAULT_WS_PORT)),
- "expected_version": ClientConfigurator.get_plugin_version(),
- "server_command": ClientConfigurator.get_server_command(),
- "pid_file": ProjectSettings.globalize_path(PortResolver.SERVER_PID_FILE),
- "startup_report": ProjectSettings.globalize_path(PortResolver.SERVER_STARTUP_REPORT),
- "http_port_reserved": WindowsPortReservation.is_port_excluded(http_port),
- "excluded_domains": str(policy.get("excluded_domains", "")),
- "allow_hosts": str(policy.get("allow_hosts", "")),
- "keep_alive": bool(policy.get("keep_alive", false)),
- "worktree_src": worktree_src,
- "ambient_pythonpath": OS.get_environment("PYTHONPATH"),
- "disable_telemetry": not bool(policy.get("telemetry_enabled", true)),
- "automatic_effects": true,
- "defer_effects": true,
- }
-
-
-static func _supports_godot_version(version_info: Dictionary) -> bool:
- var major := int(version_info.get("major", 0))
- var minor := int(version_info.get("minor", 0))
- return major == MIN_GODOT_MAJOR and minor >= MIN_GODOT_MINOR
-
-
-func _on_lifecycle_snapshot_changed(snapshot: Dictionary) -> void:
- if int(snapshot.get("episode_id", 0)) != _post_update_retry_episode or str(snapshot.get("episode_state", "")) != "BLOCKED":
- _post_update_retry_episode = 0
- if _connection != null and bool(snapshot.get("connection_blocked", true)):
- _connection.connect_blocked = true
- _connection.connect_block_reason = str(snapshot.get("message", ""))
- _replace_server_left_by_update(snapshot)
- _log_lifecycle_block(snapshot)
- if _client_jobs != null:
- _client_jobs.set_client_health_blocked(
- ServerStateScript.blocks_client_health(
- int(snapshot.get("state", ServerStateScript.UNINITIALIZED))
- )
- )
- _publish_dock_status_snapshots()
-
-
-## The dock shows why a server start blocked; the editor log should too, once
-## per distinct reason, so a report from the field carries it.
-func _log_lifecycle_block(snapshot: Dictionary) -> void:
- if str(snapshot.get("episode_state", "")) != "BLOCKED":
- _last_logged_block = ""
- return
- var message := str(snapshot.get("message", ""))
- if message.is_empty() or message == _last_logged_block:
- return
- _last_logged_block = message
- var retry_pending := bool(_lifecycle_snapshot_for_dock().get("handoff_retry_pending", false))
- print("MCP | %s: %s" % ["server handoff retry pending" if retry_pending else "server start blocked", message])
-
-
-## Once, right after an update: a godot-ai server at the version we just
-## replaced is on our port (typically spawned by an attach bridge that lost
-## server A during the restart). Replace it instead of asking the user to.
-## Any other conflict keeps the dock's explicit Restart Server authority.
-func _replace_server_left_by_update(snapshot: Dictionary) -> void:
- if _post_update_replaced_version.is_empty():
- return
- if not bool(snapshot.get("connection_blocked", true)):
- _post_update_replaced_version = ""
- return
- if not bool(snapshot.get("can_recover_incompatible", false)):
- ## Any other block right after an update is the bridge's backend in
- ## flight: bound but not answering yet, or bound between our probe and
- ## our launch so our server exited on bind. Probe again shortly, a
- ## bounded number of times; the re-probe finds that backend answering
- ## and takes the replacement path. Then the dock's Restart Server is
- ## the remaining path.
- if str(snapshot.get("episode_state", "")) != "BLOCKED":
- return
- var episode_id := int(snapshot.get("episode_id", 0))
- if episode_id <= 0 or episode_id == _post_update_retry_episode:
- return
- if str(snapshot.get("blocked_hint", "")) == ServerLifecycleManager.STALE_PRE_V4_HINT:
- if _post_update_stale_reprobes_left <= 0:
- return
- if _post_update_stale_reprobes_left == POST_UPDATE_STALE_REPROBE_LIMIT:
- print(
- "MCP | a pre-v4 godot-ai server holds port %d; waiting for it to exit once its AI client is relaunched"
- % int(snapshot.get("conflict_port", 0))
- )
- _post_update_stale_reprobes_left -= 1
- _post_update_retry_episode = episode_id
- get_tree().create_timer(POST_UPDATE_STALE_REPROBE_SECONDS).timeout.connect(
- _reprobe_after_update.bind(episode_id), CONNECT_ONE_SHOT
- )
- return
- if _post_update_reprobes_left > 0:
- _post_update_reprobes_left -= 1
- _post_update_retry_episode = episode_id
- get_tree().create_timer(1.0).timeout.connect(_reprobe_after_update.bind(episode_id), CONNECT_ONE_SHOT)
- return
- var version := str(snapshot.get("conflict_version", ""))
- if not _update_may_replace(version):
- return
- if _post_update_replacements_left <= 0:
- return
- _post_update_replacements_left -= 1
- print(
- "MCP | replacing the v%s server left on port %d by the update"
- % [version, int(snapshot.get("conflict_port", 0))]
- )
- if not _lifecycle.request_replacement():
- push_warning(
- "MCP | could not replace the v%s server automatically; use Restart Server in the dock"
- % version
- )
-
-
-## The server the update superseded, or any older server of our major
-## version an attach bridge left on the port (a client pinned further back).
-## Never a newer one: that is another editor's server, and adoption or the
-## dock's explicit Restart Server decides there.
-func _update_may_replace(conflict_version: String) -> bool:
- if conflict_version.is_empty():
- return false
- if conflict_version == _post_update_replaced_version:
- return true
- return McpServerVersionCheck.is_older_same_major(
- conflict_version, ClientConfigurator.get_plugin_version()
- )
-
-
-func _reprobe_after_update(episode_id: int) -> void:
- if episode_id != _post_update_retry_episode:
- return
- _post_update_retry_episode = 0
- if _post_update_replaced_version.is_empty() or _lifecycle == null or not _normal_start_released:
- _publish_dock_status_snapshots()
- return
- var snapshot: Dictionary = _lifecycle.get_status_dict()
- if int(snapshot.get("episode_id", 0)) != episode_id or str(snapshot.get("episode_state", "")) != "BLOCKED":
- _publish_dock_status_snapshots()
- return
- _lifecycle.start_server()
-
-
-func _on_lifecycle_transport_ready(ws_port: int, ws_capability: String) -> void:
- _set_resolved_ws_port(ws_port)
- if _client_jobs != null:
- _client_jobs.set_client_health_blocked(false)
- _client_jobs.request_status_refresh(ClientConfigurator.client_ids(), true)
- if _connection != null:
- _connection.authorize_transport(ws_port, ws_capability)
- _publish_dock_status_snapshots()
-
-
-func _on_lifecycle_transport_cleared(reason: String) -> void:
- if _connection != null:
- _connection.revoke_transport(reason)
- _publish_dock_status_snapshots()
-
-
-## Snapshot of the server-spawn outcome for the dock.
-##
-## `state` is one of the `McpServerState.*` int constants; the dock owns
-## the UI copy per state via its own `_crash_body_for_state`. `exit_ms`
-## is only meaningful for `CRASHED`.
-func get_server_status() -> Dictionary:
- return _lifecycle.get_status_dict()
-
-
-## Diagnostic accessor for the Dock. Positive means this lifecycle owns an
-## exact process grant; adoption always reports -1 and cannot authorize stop.
-func get_server_pid() -> int:
- return _lifecycle.get_server_pid()
-
-
-func get_resolved_ws_port() -> int:
- return _resolved_ws_port
-
-
-func _set_resolved_ws_port(port: int) -> void:
- var policy := _endpoint_policy.duplicate(true)
- if policy.is_empty():
- policy = ClientConfigurator.capture_endpoint_policy(port)
- policy["ws_port"] = port
- _set_endpoint_policy(policy)
- if _connection != null:
- _connection.ws_port = port
-
-
-func _set_endpoint_policy(policy: Dictionary) -> void:
- _endpoint_policy = policy.duplicate(true)
- _resolved_ws_port = int(_endpoint_policy.get(
- "ws_port", ClientConfigurator.DEFAULT_WS_PORT
- ))
- ClientConfigurator.capture_launch_context(_endpoint_policy)
-
-
-func _resolve_ws_port(configured_port: int) -> int:
- return PortResolver.resolve_ws_port(
- configured_port,
- ClientConfigurator.MAX_PORT,
- _log_buffer,
- )
-
-
-func prepare_for_update_reload() -> Dictionary:
- ## Refuse while frame-yielding work is still live, before stopping any
- ## owner. These probes are non-mutating; retry after the work completes.
- if _dispatcher != null:
- var handlers_ready: Dictionary = _dispatcher.quiesce_for_script_swap()
- if not bool(handlers_ready.get("ok", false)):
- return handlers_ready
- if _debugger_plugin != null:
- var debugger_ready: Dictionary = _debugger_plugin.quiesce_for_script_swap()
- if not bool(debugger_ready.get("ok", false)):
- return debugger_ready
- ## Stop the exact managed process only after the non-mutating probes.
- var lifecycle_quiesced: Dictionary = _lifecycle.prepare_for_update_reload()
- if not bool(lifecycle_quiesced.get("ok", false)):
- return lifecycle_quiesced
- if _vision_routing != null:
- _vision_routing.shutdown()
- if _dispatcher != null:
- var quiesced: Dictionary = _dispatcher.quiesce_for_script_swap()
- if not bool(quiesced.get("ok", false)):
- quiesced["reload_required"] = true
- return quiesced
- var cleared: Dictionary = _dispatcher.clear()
- if not bool(cleared.get("ok", false)):
- cleared["reload_required"] = true
- return cleared
- return {"ok": true}
-
-
-## Refuse to start a download while an earlier update is unresolved or another
-## live editor holds this project's update lock. The lock is taken here, before
-## the download, and released by every failure path before the swap.
-func _preflight_update() -> Dictionary:
- var pending: Dictionary = UpdateInstaller.read_pending()
- var status := str(pending.get("status", ""))
- if status == "swapped" or status == "repair_required":
- return {"ok": false, "error": "an earlier update is unresolved (%s)" % status, "download_root": ""}
- var pid := OS.get_process_id()
- var lock: Dictionary = UpdateInstaller.acquire_lock(pid, McpPortResolver.process_fingerprint(pid))
- if not bool(lock.get("ok", false)):
- return {"ok": false, "error": str(lock.get("error", "")), "download_root": ""}
- var download_root := ProjectSettings.globalize_path("user://godot_ai_update/download").trim_suffix("/")
- if DirAccess.dir_exists_absolute(download_root):
- _remove_tree(download_root)
- if DirAccess.make_dir_recursive_absolute(download_root) != OK:
- UpdateInstaller.release_lock()
- return {"ok": false, "error": "could not create the download directory", "download_root": ""}
- return {"ok": true, "error": "", "download_root": download_root}
-
-
-static func _remove_tree(path: String) -> void:
- var dir := DirAccess.open(path)
- if dir == null:
- return
- dir.include_hidden = true
- for name in dir.get_directories():
- _remove_tree(path.path_join(name))
- for name in dir.get_files():
- DirAccess.remove_absolute(path.path_join(name))
- DirAccess.remove_absolute(path)
-
-
-## Verify, stage, quiesce, then hand activation to an independent runner. Every check runs in this editor
-## against the downloaded bytes; nothing outside the editor is executed. The
-## live tree is touched only by the two renames inside `swap`, and only after
-## the staged tree has been re-hashed against the signed manifest.
-## Activation runs on the main thread: verification hashes the archive,
-## staging extracts it, and the worker quiescence waits for threads. Each
-## phase names itself in the dock and yields one frame first so the label
-## repaints; without that the dock sat on "Downloading…" for seconds after
-## the download had finished, looking frozen.
-func install_downloaded_update(package: Dictionary) -> void:
- await _present_install_phase("Verifying signed update…")
- var manifest_bytes := FileAccess.get_file_as_bytes(str(package.get("manifest", "")))
- var signature := FileAccess.get_file_as_bytes(str(package.get("signature", "")))
- var verified: Dictionary = ReleaseVerifier.verify_manifest(
- manifest_bytes,
- signature,
- {
- "repository": str(package.get("repository", "")),
- "channel": str(package.get("channel", "")),
- "version": str(package.get("version", "")),
- "current_version": ClientConfigurator.get_plugin_version(),
- },
- UpdateManager.RELEASE_SIGNING_PUBLIC_KEY_PEM,
- )
- var manifest: Dictionary = verified.get("manifest", {})
- var checked: Dictionary = verified
- if bool(verified.get("ok", false)):
- checked = ReleaseVerifier.verify_archive(str(package.get("archive", "")), manifest)
- if not bool(checked.get("ok", false)):
- _fail_update("Update verification failed", "signed update refused: %s" % str(checked.get("error", "")))
- return
- await _present_install_phase("Staging the verified tree…")
- var staged: Dictionary = UpdateInstaller.stage(str(package.get("archive", "")), manifest)
- if not bool(staged.get("ok", false)):
- _fail_update("Update staging failed", "update staging refused: %s" % str(staged.get("error", "")))
- return
- if _update_manager != null:
- _update_manager.discard_downloads()
- await _present_install_phase("Waiting for client workers…")
- if _client_jobs != null:
- var jobs_quiesced: Dictionary = _client_jobs.quiesce(
- Time.get_ticks_msec() + ClientConfigurator.PREWARM_TIMEOUT_MS
- )
- if not bool(jobs_quiesced.get("ok", false)):
- UpdateInstaller.discard_stage()
- _fail_update("Update cancelled safely", "client workers refused update quiescence")
- return
- var script_quiesced := prepare_for_update_reload()
- if not bool(script_quiesced.get("ok", false)):
- UpdateInstaller.discard_stage()
- _fail_update("Update cancelled safely", "command workers refused update quiescence")
- if bool(script_quiesced.get("reload_required", false)):
- _reload_plugin_after_failed_update()
- return
- _on_update_install_state_changed({
- "install_in_flight": true,
- "status_text": "Activating verified update…",
- "button_disabled": true,
- })
- var to_version := str(manifest.get("version", ""))
- var record := {
- "from_version": ClientConfigurator.get_plugin_version(),
- "to_version": to_version,
- "manifest_sha256": ReleaseVerifier.sha256_bytes(manifest_bytes),
- "expected_tree_sha256": str(staged.get("tree_sha256", "")),
- "editor_nonce": Crypto.new().generate_random_bytes(16).hex_encode(),
- "replace_owned_mismatches": false,
- }
- ## Compile an independent script with no resource path. Loading this as a
- ## normal Script would let the filesystem scan replace our live runner.
- var runner_script := GDScript.new()
- runner_script.source_code = FileAccess.get_file_as_string(
- "res://addons/godot_ai/utils/update_activation_runner.gd"
- )
- if runner_script.source_code.is_empty() or runner_script.reload() != OK:
- UpdateInstaller.discard_stage()
- _fail_update("Update cancelled safely", "could not compile the independent activation runner")
- _reload_plugin_after_failed_update()
- return
- var runner = runner_script.new()
- get_tree().root.add_child(runner)
- if not runner.start({"stage_root": str(staged.get("stage_root", "")), "record": record}):
- var refusal_reason := str(runner.refusal_reason)
- runner.queue_free()
- UpdateInstaller.discard_stage()
- _fail_update("Update cancelled safely", refusal_reason)
- _reload_plugin_after_failed_update()
- return
- ## The runner now owns the lock. Teardown's cancellation signal must not
- ## release it before the deferred disable/drain/swap sequence completes.
- _update_swapped = true
- ## Return: no frame of this plugin may be suspended across source replacement.
-
-
-
-## Name the activation phase in the dock and let it repaint before the
-## phase's main-thread work begins.
-func _present_install_phase(status_text: String) -> void:
- _on_update_install_state_changed({
- "install_in_flight": true,
- "status_text": status_text,
- "button_disabled": true,
- })
- var tree := get_tree()
- if tree != null:
- await tree.process_frame
-
-
-func _fail_update(status_text: String, error: String) -> void:
- if _update_manager != null:
- _update_manager.discard_downloads()
- push_error("MCP | %s" % error)
- _on_update_install_state_changed({
- "install_in_flight": false,
- "status_text": status_text,
- "button_disabled": false,
- })
-
-
-func _reload_plugin_after_failed_update() -> void:
- ## The signed prepared tree was aborted before mutation. Reconstruct the
- ## unchanged old composition if any later quiescence step had already
- ## stopped vision or released handler references. The reload frees this
- ## plugin, so it must not run on one of this plugin's own frames: defer
- ## the static call itself, not a method of this instance.
- PluginReload.reload_enabled_plugin.call_deferred()
-
-
-func can_recover_incompatible_server() -> bool:
- return _normal_start_released and _lifecycle.can_recover_incompatible_server()
-
-
-func recover_incompatible_server(_user_initiated: bool = true, _stale_version: String = "") -> bool:
- ## The Dock click is the sole source of replacement authority. The manager
- ## binds, spends, and discards one authorization for this exact target.
- if not _normal_start_released:
- return false
- return _lifecycle.request_replacement()
-
-
-## Managed restart uses the exact owned grant. An unowned incompatible server
-## can only reach replacement through the separate explicit Dock intent.
-func force_restart_server() -> bool:
- if not _normal_start_released:
- return false
- return _lifecycle.force_restart_server()
-
-
-## Developer controls use the same lifecycle owner as ordinary startup. The
-## plugin may restart only its exact process grant; a compatible external
-## server is adopted for transport but remains the launcher's responsibility.
-func restart_or_start_managed_server() -> bool:
- if not _normal_start_released:
- return false
- if has_managed_server():
- _lifecycle.force_restart_server()
- return true
- var port := ClientConfigurator.http_port()
- if PortResolver.is_port_in_use(port):
- push_warning(
- "MCP | refusing to restart the unowned server on port %d; stop it from its launcher"
- % port
- )
- return false
- _lifecycle.start_server()
- return true
-
-
-func stop_managed_server() -> void:
- if has_managed_server():
- _lifecycle.stop_server()
-
-
-func has_managed_server() -> bool:
- ## Returns true if the plugin is currently managing a server process it spawned.
- return _lifecycle.has_managed_server()
-
-
-func can_restart_managed_server() -> bool:
- ## Restart needs an owned grant or a currently replaceable blocked target.
- return _normal_start_released and _lifecycle.can_restart_managed_server()
-
-
-func _on_custom_tools_changed() -> void:
- if _connection == null:
- push_warning("MCP | connection isn't established")
- return
- var tool_list: Array[Dictionary] = []
- ## Send all definitions plus their state: the server hides disabled tools
- ## from fresh tools/list responses but retains a callable tombstone so a
- ## client using a cached promoted name receives CUSTOM_TOOL_DISABLED.
- for spec in _custom_tool_registry.all():
- tool_list.append({
- "name": spec.name,
- "description": spec.description,
- "params_schema": spec.params_schema,
- "source": spec.source,
- "deferred": spec.deferred,
- "timeout_ms": spec.timeout_ms,
- "requires_writable": spec.requires_writable,
- "undoable": spec.undoable,
- "promoted": spec.promoted,
- "enabled": _custom_tool_registry.is_tool_enabled(spec.name)
- })
- _connection.send_event("custom_tools_changed", {"tools": tool_list})
-
-
-## On (re)connect, replay the current custom-tool catalog so tools
-## registered before the initial connection or during a disconnect
-## window reach the server. send_event silently drops while
-## _connected is false (connection.gd::_send_json), so without this
-## replay those tools only surface on the next registry mutation.
-func _on_connection_state_changed(is_open: bool) -> void:
- if is_open:
- _lifecycle.transport_authenticated(_connection.server_version)
- if _custom_tool_registry != null:
- _on_custom_tools_changed()
- else:
- _lifecycle.transport_lost()
diff --git a/addons/godot_ai/plugin.gd.uid b/addons/godot_ai/plugin.gd.uid
deleted file mode 100644
index 4c550dd..0000000
--- a/addons/godot_ai/plugin.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d3ui3yx6vdigl
diff --git a/addons/godot_ai/runtime/draw_recipe.gd b/addons/godot_ai/runtime/draw_recipe.gd
deleted file mode 100644
index 8204bd7..0000000
--- a/addons/godot_ai/runtime/draw_recipe.gd
+++ /dev/null
@@ -1,86 +0,0 @@
-@tool
-extends Control
-
-## Runtime helper attached by control_draw_recipe.
-## Reads an array of op dicts from node metadata under key "_ops" and dispatches
-## each to a CanvasItem draw call in _draw(). The ops list is set by the handler
-## via set_meta; this script is deterministic — re-setting meta + queue_redraw
-## is enough to update the visuals.
-
-const META_KEY := "_ops"
-
-
-func _ready() -> void:
- queue_redraw()
-
-
-func _draw() -> void:
- if not has_meta(META_KEY):
- return
- var ops: Variant = get_meta(META_KEY)
- if typeof(ops) != TYPE_ARRAY:
- return
- for op in ops:
- if typeof(op) != TYPE_DICTIONARY:
- continue
- match op.get("draw", ""):
- "line":
- draw_line(
- op.from,
- op.to,
- op.color,
- float(op.get("width", 1.0)),
- bool(op.get("antialiased", false))
- )
- "rect":
- # Godot warns if `width` is passed when `filled` is true —
- # width has no effect on filled rects. Split the call so we
- # only pass width when stroking an outline.
- var filled := bool(op.get("filled", true))
- if filled:
- draw_rect(op.rect, op.color, true)
- else:
- draw_rect(
- op.rect,
- op.color,
- false,
- float(op.get("width", 1.0))
- )
- "arc":
- draw_arc(
- op.center,
- float(op.radius),
- float(op.start_angle),
- float(op.end_angle),
- int(op.get("point_count", 32)),
- op.color,
- float(op.get("width", 1.0)),
- bool(op.get("antialiased", false))
- )
- "circle":
- draw_circle(op.center, float(op.radius), op.color)
- "polyline":
- draw_polyline(
- op.points,
- op.color,
- float(op.get("width", 1.0)),
- bool(op.get("antialiased", false))
- )
- "polygon":
- var colors: PackedColorArray = (
- op.colors if op.has("colors") else PackedColorArray([op.color])
- )
- draw_polygon(op.points, colors)
- "string":
- var font: Font = get_theme_default_font()
- if font == null:
- continue
- draw_string(
- font,
- op.position,
- str(op.text),
- int(op.get("align", HORIZONTAL_ALIGNMENT_LEFT)),
- float(op.get("max_width", -1.0)),
- int(op.get("font_size", 16)),
- op.color
- )
diff --git a/addons/godot_ai/runtime/draw_recipe.gd.uid b/addons/godot_ai/runtime/draw_recipe.gd.uid
deleted file mode 100644
index 5de2df2..0000000
--- a/addons/godot_ai/runtime/draw_recipe.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://da3fqfqv6gtgm
diff --git a/addons/godot_ai/runtime/editor_logger.gd b/addons/godot_ai/runtime/editor_logger.gd
deleted file mode 100644
index 2d19daa..0000000
--- a/addons/godot_ai/runtime/editor_logger.gd
+++ /dev/null
@@ -1,139 +0,0 @@
-@tool
-extends Logger
-
-## Editor-process Logger subclass.
-##
-## NOTE: deliberately no `class_name`. Registered from plugin.gd::_enter_tree
-## so we can intercept editor-process script errors — parse errors, @tool
-## runtime errors, EditorPlugin errors, push_error/push_warning — and surface
-## them via `logs_read(source="editor")`. Without this, the LLM sees nothing
-## in `logs_read` while the same errors show in red lines in Godot's Output
-## panel.
-##
-## Why only `_log_error` and not `_log_message`:
-## `_log_message(msg, error)` covers print() and printerr(), which is the
-## firehose path — running editors print thousands of internal info lines
-## a session. The issue (#231) explicitly asks to filter so the buffer
-## isn't drowned. Errors and warnings flow through `_log_error` (parse
-## errors, push_error/push_warning, runtime errors), which is what
-## debugging callers actually need. If we discover @tool printerr() is a
-## valuable source later, _log_message can be added behind the same filter.
-##
-## Logger virtuals can be called from any thread (e.g. async script
-## loaders push parse errors off the main thread). McpEditorLogBuffer is
-## mutex-protected so we can append directly without an intermediate queue.
-
-const ADDON_PATH_MARKER := "/addons/godot_ai/"
-
-## Resolve McpLogBacktrace by path, not by the `McpLogBacktrace` class_name.
-## A bare class_name reference depends on the global class-name table being populated
-## at compile time, which isn't guaranteed on a cold editor enable mid-scan.
-## `const preload` resolves at compile time independent of the registry —
-## matches game_logger.gd's deliberate choice for the same reason.
-const _LogBacktrace := preload("res://addons/godot_ai/utils/log_backtrace.gd")
-
-## Constructor-injected so the hot path doesn't need a per-call null check.
-var _buffer
-
-
-func _init(buffer = null) -> void:
- _buffer = buffer
-
-
-func _log_error(
- function: String,
- file: String,
- line: int,
- code: String,
- rationale: String,
- _editor_notify: bool,
- error_type: int,
- script_backtraces: Array,
-) -> void:
- if _buffer == null:
- return
- ## Cheap reject for the firehose: when `file` is already non-user (the
- ## bulk of editor-internal C++ chatter), there's no backtrace to remap
- ## from, and the message doesn't name a project resource, the resolved
- ## path can only stay non-user — drop without paying for resolve_error's
- ## call frame + dict allocation.
- var message := rationale if not rationale.is_empty() else code
- var message_res_path := _extract_user_res_path(message)
- if not _is_user_script(file) and script_backtraces.is_empty() and message_res_path.is_empty():
- return
- var resolved := _LogBacktrace.resolve_error(
- function, file, line, code, rationale, error_type, script_backtraces,
- )
- if not _is_user_script(resolved.path):
- if message_res_path.is_empty():
- return
- resolved.path = message_res_path
- resolved.line = 0
- resolved.function = function
- _update_resolved_details(resolved)
- if _is_in_godot_ai_addon(resolved.path):
- return
- if not message_res_path.is_empty() and _is_in_godot_ai_addon(message_res_path):
- return
- var details: Dictionary = resolved.get("details", {})
- _buffer.append(resolved.level, resolved.message, resolved.path, resolved.line, resolved.function, details)
-
-
-static func _update_resolved_details(resolved: Dictionary) -> void:
- var details: Dictionary = resolved.get("details", {})
- if details.is_empty():
- return
- details["resolved"] = {
- "path": resolved.get("path", ""),
- "line": resolved.get("line", 0),
- "function": resolved.get("function", ""),
- }
- resolved["details"] = details
-
-
-## Predicate broken out so tests can drive the path-filter logic without
-## constructing real Logger calls.
-static func _is_user_script(path: String) -> bool:
- if path.is_empty():
- return false
- ## Match .gd / .cs (case-insensitively to handle .GD on case-insensitive
- ## filesystems). C# scripts compile elsewhere but the parser path can
- ## still surface .cs files for assembly load failures.
- var lower := path.to_lower()
- return lower.ends_with(".gd") or lower.ends_with(".cs")
-
-
-## Path-substring check works for both `res://addons/godot_ai/foo.gd` and
-## globalized absolute paths (`/Users/.../addons/godot_ai/foo.gd`) that
-## Godot can also report depending on where the error originated.
-static func _is_in_godot_ai_addon(path: String) -> bool:
- if path.begins_with("res://addons/godot_ai/"):
- return true
- return path.find(ADDON_PATH_MARKER) >= 0
-
-
-## Some engine-origin errors have no ScriptBacktrace even though they are
-## project-relevant, notably ResourceLoader failures:
-## `Failed loading resource: res://does/not/exist.tres.`. Capture these by
-## extracting a named `res://` path from the message while keeping editor
-## internals and this addon's own resources filtered.
-static func _extract_user_res_path(message: String) -> String:
- var start := message.find("res://")
- if start < 0:
- return ""
- var end := message.length()
- var quote_end := message.find("'", start)
- if quote_end >= 0:
- end = mini(end, quote_end)
- quote_end = message.find("\"", start)
- if quote_end >= 0:
- end = mini(end, quote_end)
- quote_end = message.find("`", start)
- if quote_end >= 0:
- end = mini(end, quote_end)
- var path := message.substr(start, end - start).strip_edges()
- while not path.is_empty() and path.substr(path.length() - 1, 1) in [".", ",", ";", ":", ")"]:
- path = path.substr(0, path.length() - 1)
- if path.is_empty() or _is_in_godot_ai_addon(path):
- return ""
- return path
diff --git a/addons/godot_ai/runtime/editor_logger.gd.uid b/addons/godot_ai/runtime/editor_logger.gd.uid
deleted file mode 100644
index 9db6035..0000000
--- a/addons/godot_ai/runtime/editor_logger.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cxfregddqj5b8
diff --git a/addons/godot_ai/runtime/game_helper.gd b/addons/godot_ai/runtime/game_helper.gd
deleted file mode 100644
index 85d9c23..0000000
--- a/addons/godot_ai/runtime/game_helper.gd
+++ /dev/null
@@ -1,1352 +0,0 @@
-extends Node
-
-## Godot AI MCP — game-process helper.
-##
-## Registered as an autoload by plugin.gd when the Godot AI plugin is enabled.
-## Runs in the running game process (separate from the editor) so the plugin
-## can request the game's framebuffer over the editor-debugger channel.
-##
-## The editor never has direct access to the game's pixels: even when "Embed
-## Game Mode" is on, the game is still a separate OS child process whose
-## window is reparented into the editor via Win32 SetParent / X11
-## XReparentWindow / macOS remote layer (Godot PR godotengine/godot#99010).
-## So viewport-texture capture on the editor side never contains game pixels.
-## This autoload solves that by replying to "mcp:take_screenshot" debug
-## messages with a PNG of Viewport.get_texture() from inside the game.
-##
-## No-ops in the editor (Engine.is_editor_hint) and silently sits idle
-## when the debugger channel is inactive (e.g. exported release builds)
-## — register_message_capture is safe to call either way, it's
-## send_message that requires an active channel.
-
-const CAPTURE_PREFIX := "mcp"
-## Cap per-frame flush so a runaway print loop can't blow the debugger's
-## packet budget in a single send. Surplus stays queued for the next frame.
-const FLUSH_BATCH_LIMIT := 200
-## How long take_screenshot waits for the game's first real presentation
-## before reading the viewport texture back. The "mcp" capture registers in
-## this autoload's _ready(), which runs BEFORE the main scene enters the tree
-## and before the renderer has presented anything — so a request arriving
-## right after mcp:hello would otherwise read back the clear-color
-## framebuffer (observed as a uniform RGB(77,77,77) PNG on GitHub's
-## GPU-less paravirtualized macOS runners, where the first present lags
-## seconds behind boot). MUST stay below the editor-side reply timer
-## (DEFAULT_TIMEOUT_SEC = 8.0 in debugger/mcp_debugger_plugin.gd) so a
-## game that genuinely can't render falls through to the existing
-## texture/image error replies before the editor gives up with its
-## generic timeout.
-const FIRST_FRAME_WAIT_SEC := 6.0
-## #777: how long the main loop can go without ticking _process before
-## _handle_take_screenshot treats it as frozen and commits a synchronous
-## stale-frame capture instead of awaiting frames that will never come.
-## A backgrounded/minimized play-in-editor game stops iterating its main
-## loop entirely, so any real threshold works; 1s keeps a merely-slow game
-## (heavy frame, low FPS) on the fresh-frame await path.
-const MAIN_LOOP_STALL_MSEC := 1000
-## How long frames_drawn can stay flat before _handle_take_screenshot treats
-## rendering as suppressed and commits the synchronous stale-frame capture.
-## On Windows, minimizing the game window freezes frame presentation but NOT
-## the main loop — _process keeps ticking, so the MAIN_LOOP_STALL_MSEC beacon
-## never trips and every capture used to burn the full FIRST_FRAME_WAIT_SEC
-## await before replying stale (issue #794 smoke, item 1b). Larger than the
-## loop threshold so a heavy-but-rendering game (~1 FPS frame gaps) stays on
-## the fresh-frame await path; a sub-0.7 FPS game that trips this still gets
-## an honestly stale-flagged image immediately instead of a 6s wait.
-const RENDER_STALL_MSEC := 1500
-
-const GameLogger := preload("res://addons/godot_ai/runtime/game_logger.gd")
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-## Shared with the editor-side copy in editor_handler.gd (#716). Preload by
-## path, not class_name: this autoload runs in the game process and must not
-## depend on the editor's global-class cache being warm.
-const ScreenshotEncode := preload("res://addons/godot_ai/utils/screenshot_encode.gd")
-
-var _registered := false
-## Captures game-process print, warning, and error output for the editor.
-var _logger: Logger
-var _logger_attached := false
-## Entries drained from the logger but not yet sent over the debugger
-## channel. Holds the tail of one drain() so we can bleed it out across
-## frames at FLUSH_BATCH_LIMIT per frame rather than blasting the whole
-## queue in a single _process tick.
-var _pending_outbound: Array = []
-## #490: in-flight evals, keyed by request_id (multiple deferred game_evals
-## can run at once). Each entry: {node:Node, token:String, baseline:int}.
-## `token` names this eval's unique wrapper function so a runtime error is
-## attributed only to the eval that actually raised it — not an unrelated
-## background game error, and not a sibling overlapping eval. `baseline` is the
-## logger's script-error seq just before this eval ran. The editor's eval_check
-## probe (and #488's in-flight poll loop, when the game is focused) consult
-## these to report a runtime error that aborted execute() before the reply.
-var _inflight_evals: Dictionary = {}
-var _eval_token_counter: int = 0
-## #777: last time _process ran, in ticks msec. The debugger message capture
-## stays live while a backgrounded game's main loop is frozen, so this is how
-## _handle_take_screenshot (running inside that capture) detects the freeze
-## synchronously. -1 until the first tick.
-var _last_loop_tick_msec: int = -1
-var _mcp_runtime_process_ticks: int = 0
-var _last_debug_status_reply: Dictionary = {}
-## Rendering-freeze beacon for the Windows-minimize state (#794 smoke, 1b):
-## the frames_drawn value last observed in _process, and when it last
-## advanced. -1 until the first observed advance, so a booting or
-## render-less game (frames_drawn stuck at 0) can never read as
-## render-stalled and keeps the fresh-frame await path's error replies.
-var _last_frames_drawn_seen: int = -1
-var _last_frames_advance_msec: int = -1
-## #859: test/diagnostic seam for the synchronous eval liveness reply. The
-## debugger capture remains callable while the game loop is parked, so the
-## reply reports whether the _process beacon is actually advancing rather
-## than merely proving that the capture was registered once.
-var _last_eval_liveness_reply: Dictionary = {}
-
-
-func _ready() -> void:
- ## Only run in the game process, not in the editor. Use is_editor_hint
- ## — NOT OS.has_feature("editor"), which is a BUILD-config check
- ## (TOOLS_ENABLED) and returns true in the game subprocess too because
- ## the game is spawned with the same editor binary. is_editor_hint is
- ## the runtime-context check: true only inside the editor GUI, false
- ## in play-from-editor. The earlier has_feature check was causing us
- ## to skip registration in the game and time out every capture.
- if Engine.is_editor_hint():
- return
- ## Keep ticking while the tree is paused: _process both ferries game logs
- ## and timestamps main-loop liveness for the stalled-loop screenshot
- ## fallback (#777). A paused game still iterates its loop and renders, and
- ## must not be misread as frozen.
- process_mode = Node.PROCESS_MODE_ALWAYS
- ## register_message_capture is safe to call before the debugger
- ## handshake completes; the capture sits until a message arrives.
- EngineDebugger.register_message_capture(CAPTURE_PREFIX, _on_debug_message)
- _registered = true
- ## Capture print() / printerr() / push_error() / push_warning() and
- ## ferry them to the editor in mcp:log_batch messages flushed from
- ## _process.
- _logger = GameLogger.new()
- OS.add_logger(_logger)
- _logger_attached = true
- ## Routed to the editor's Output panel via Godot's remote-stdout
- ## forwarder — handy when diagnosing why capture timed out.
- print("[godot_ai game_helper] registered mcp capture (debugger active=%s, logger=%s)"
- % [EngineDebugger.is_active(), _logger_attached])
- ## Boot beacon so the editor side can confirm the autoload ran even
- ## if no screenshot was ever requested.
- if EngineDebugger.is_active():
- EngineDebugger.send_message("mcp:hello", [])
-
-
-func _process(_delta: float) -> void:
- _mcp_runtime_process_ticks += 1
- ## #777: liveness beacon for _handle_take_screenshot's stalled-loop check.
- ## Recorded before the early returns below so the signal stays truthful
- ## even when the logger or debugger channel is unavailable.
- _last_loop_tick_msec = Time.get_ticks_msec()
- ## Rendering beacon: on Windows a minimized game keeps ticking _process
- ## while presentation stops, so frames_drawn stagnation — not loop
- ## silence — is the observable freeze signal there (#794 smoke, 1b).
- var frames_now := Engine.get_frames_drawn()
- if frames_now != _last_frames_drawn_seen:
- _last_frames_drawn_seen = frames_now
- _last_frames_advance_msec = _last_loop_tick_msec
- ## Drain the logger queue on the main thread (Logger virtuals can fire
- ## from any thread; EngineDebugger.send_message is only safe from main).
- ## Send at most one FLUSH_BATCH_LIMIT-sized batch per frame so a runaway
- ## print loop can't stall the game by shoving thousands of entries
- ## through the debugger packet path in a single tick. Surplus stays in
- ## `_pending_outbound` and bleeds out across subsequent frames.
- if not _logger_attached or _logger == null:
- return
- if not EngineDebugger.is_active():
- return
- if _pending_outbound.is_empty():
- if not _logger.has_pending():
- return
- _pending_outbound = _logger.drain()
- var batch := _pending_outbound.slice(0, FLUSH_BATCH_LIMIT)
- _pending_outbound = _pending_outbound.slice(FLUSH_BATCH_LIMIT)
- EngineDebugger.send_message("mcp:log_batch", [batch])
-
-
-func _exit_tree() -> void:
- if _registered:
- EngineDebugger.unregister_message_capture(CAPTURE_PREFIX)
- _registered = false
- if _logger_attached and _logger != null:
- OS.remove_logger(_logger)
- _logger_attached = false
- _logger = null
-
-
-## Dispatched for messages prefixed "mcp:" on the debugger channel.
-## Godot passes the full message ("mcp:take_screenshot") to the capture
-## callable; trim defensively so tests can still call the helper with either
-## form.
-func _on_debug_message(message: String, data: Array) -> bool:
- var action := message.trim_prefix("mcp:")
- match action:
- "take_screenshot":
- _handle_take_screenshot(data)
- return true
- "eval_liveness":
- _reply_eval_liveness(data)
- return true
- "debug_status":
- _reply_debug_status(data)
- return true
- "eval":
- _handle_eval(data)
- return true
- "eval_check":
- _handle_eval_check(data)
- return true
- "game_command":
- _handle_game_command(data)
- return true
- return false
-
-
-## #859: answer from the debugger capture without awaiting a game frame. The
-## capture can still run while a backgrounded/frozen game's main loop cannot,
-## so the _process beacon is the liveness signal that matters for game_eval.
-func _reply_eval_liveness(data: Array) -> void:
- var request_id: String = data[0] if data.size() > 0 else ""
- var loop_live := not _main_loop_appears_stalled()
- _last_eval_liveness_reply = {"request_id": request_id, "loop_live": loop_live}
- if EngineDebugger.is_active():
- EngineDebugger.send_message("mcp:eval_liveness_response", [request_id, loop_live])
-
-
-func _debug_status_snapshot() -> Dictionary:
- var inside_tree := is_inside_tree()
- var tree := get_tree() if inside_tree else null
- return {
- "probe_version": 1,
- "helper_found": true,
- ## GameHelper is PROCESS_MODE_ALWAYS, so pause does not stop it; native
- ## SceneTree suspension does. This exposes Godot's debugger-owned suspend
- ## bit through Node::can_process() without conflating it with pause/time scale.
- "suspended": inside_tree and not can_process(),
- "loop_live": not _main_loop_appears_stalled(),
- "loop_tick_msec": _last_loop_tick_msec,
- "process_ticks": _mcp_runtime_process_ticks,
- "tree_paused": tree.paused if tree != null else false,
- "frames_drawn": Engine.get_frames_drawn(),
- "process_frames": Engine.get_process_frames(),
- "physics_frames": Engine.get_physics_frames(),
- "time_scale": Engine.time_scale,
- }
-
-
-func _reply_debug_status(data: Array) -> void:
- var request_id: String = data[0] if data.size() > 0 else ""
- var state := _debug_status_snapshot()
- _last_debug_status_reply = {"request_id": request_id, "state": state.duplicate(true)}
- if EngineDebugger.is_active():
- EngineDebugger.send_message("mcp:debug_status_response", [request_id, state])
-
-
-func _handle_take_screenshot(data: Array) -> void:
- var request_id: String = data[0] if data.size() > 0 else ""
- var max_resolution: int = int(data[1]) if data.size() > 1 else 0
-
- var tree := get_tree()
- var viewport := tree.root if tree != null else null
- if viewport == null:
- _reply_error(request_id, "No game root viewport available")
- return
-
- ## #777: this function runs inside the debugger message capture, which
- ## stays live even when a backgrounded/minimized play-in-editor game has
- ## frozen its main loop. In that state awaiting `process_frame` parks
- ## this coroutine forever — no reply is ever sent, and the game side
- ## cannot self-timeout because timers need the same frozen loop. Commit a
- ## synchronous capture of the last rendered frame instead: stale, but a
- ## real image, flagged as such in the reply. Only fall through to the
- ## fresh-frame awaits when the loop is demonstrably alive.
- if _should_capture_stale_sync(
- _main_loop_appears_stalled(),
- _rendering_appears_stalled(),
- tree.current_scene != null,
- Engine.get_frames_drawn()
- ):
- _capture_and_reply(request_id, viewport, max_resolution, Engine.get_frames_drawn())
- return
-
- ## Wait (bounded — see FIRST_FRAME_WAIT_SEC) until the main scene is in
- ## the tree and at least one frame has been drawn after this request, so
- ## the readback never precedes the first real present. Past the deadline,
- ## fall through anyway: current_scene stays null under a custom main
- ## loop, and frames_drawn never advances in a render-less game — both
- ## are handled by the texture/image error replies below.
- var deadline := Time.get_ticks_msec() + int(FIRST_FRAME_WAIT_SEC * 1000.0)
- while tree.current_scene == null and Time.get_ticks_msec() < deadline:
- await tree.process_frame
- var frames_at_request := Engine.get_frames_drawn()
- while Engine.get_frames_drawn() <= frames_at_request and Time.get_ticks_msec() < deadline:
- await tree.process_frame
-
- _capture_and_reply(request_id, viewport, max_resolution, frames_at_request)
-
-
-## #777: pure decision for the synchronous stale-frame path. Sync capture is
-## only worth committing when awaiting can't produce a fresh frame — the main
-## loop is frozen (macOS/suspend), or the loop still ticks but presentation
-## is suppressed (Windows minimize, #794 smoke 1b) — AND the viewport
-## plausibly holds a real frame: the main scene is in the tree and at least
-## one frame was presented. Without those, the stale readback would be the
-## boot clear-color framebuffer — worse than the honest timeout.
-static func _should_capture_stale_sync(
- loop_stalled: bool, render_stalled: bool, has_current_scene: bool, frames_drawn: int
-) -> bool:
- return (loop_stalled or render_stalled) and has_current_scene and frames_drawn > 0
-
-
-## #777: true when _process hasn't ticked within MAIN_LOOP_STALL_MSEC —
-## i.e. the main loop is frozen (backgrounded window) or has never run.
-func _main_loop_appears_stalled() -> bool:
- ## The hello→probe round trip normally follows at least one _process tick.
- ## Treat the never-ticked sentinel as stalled defensively rather than
- ## dispatching an eval into a game whose main loop has not started.
- if _last_loop_tick_msec < 0:
- return true
- return Time.get_ticks_msec() - _last_loop_tick_msec > MAIN_LOOP_STALL_MSEC
-
-
-## True when frames_drawn has sat flat past RENDER_STALL_MSEC while _process
-## kept ticking — Windows minimize suppresses presentation without freezing
-## the loop, so the loop beacon alone misses it (#794 smoke, 1b). False until
-## the first observed frame advance: a game that has never presented has no
-## trustworthy frame to return, and must fall through to the await path's
-## texture/image error replies instead.
-func _rendering_appears_stalled() -> bool:
- if _last_frames_advance_msec < 0:
- return false
- return Time.get_ticks_msec() - _last_frames_advance_msec > RENDER_STALL_MSEC
-
-
-## Read back the viewport texture and reply — fully synchronous, so it is
-## safe to call from the debugger capture while the main loop is frozen.
-## `frames_at_request` is Engine.get_frames_drawn() at request receipt: if no
-## further frame was drawn by capture time, the image predates the request
-## and the reply is flagged stale.
-func _capture_and_reply(
- request_id: String, viewport: Viewport, max_resolution: int, frames_at_request: int
-) -> void:
- var texture := viewport.get_texture()
- if texture == null:
- _reply_error(request_id, "Root viewport has no texture (headless?)")
- return
-
- var image := texture.get_image()
- if image == null or image.is_empty():
- _reply_error(request_id, "Captured an empty image from game viewport")
- return
-
- var encoded: Dictionary = ScreenshotEncode.downscale_and_encode(image, max_resolution)
- var frames_drawn := Engine.get_frames_drawn()
- var stale := frames_drawn <= frames_at_request
-
- _last_screenshot_reply = {
- "kind": "response",
- "request_id": request_id,
- "frames_drawn": frames_drawn,
- "stale": stale,
- "width": encoded.width,
- "height": encoded.height,
- }
- if EngineDebugger.is_active():
- ## Fields 7+8 are new in #777; older editors read the first six and
- ## ignore the rest.
- EngineDebugger.send_message("mcp:screenshot_response", [
- request_id,
- encoded.base64,
- encoded.width,
- encoded.height,
- encoded.original_width,
- encoded.original_height,
- frames_drawn,
- stale,
- ])
-
-
-## Testing seam: the last screenshot reply (response or error), recorded
-## before hitting the EngineDebugger channel (inactive in the editor-side
-## test harness). Mirrors _last_eval_reply.
-var _last_screenshot_reply: Dictionary = {}
-
-
-func _reply_error(request_id: String, message: String) -> void:
- _last_screenshot_reply = {"kind": "error", "request_id": request_id, "message": message}
- if EngineDebugger.is_active():
- EngineDebugger.send_message("mcp:screenshot_error", [request_id, message])
-
-
-## --- game_command: curated runtime inspection and input ---
-
-func _handle_game_command(data: Array) -> void:
- var request_id: String = data[0] if data.size() > 0 else ""
- var op: String = data[1] if data.size() > 1 else ""
- var params_json: String = data[2] if data.size() > 2 else "{}"
-
- if request_id.is_empty():
- return
- if op.is_empty():
- _reply_game_command_error(request_id, "No op provided")
- return
-
- var json := JSON.new()
- var parse_err := json.parse(params_json)
- if parse_err != OK or not (json.data is Dictionary):
- _reply_game_command_error(request_id, "Invalid params JSON")
- return
-
- var result: Dictionary
- match op:
- "get_scene_tree":
- result = _game_get_scene_tree(json.data)
- "get_node_info":
- result = _game_get_node_info(json.data)
- "get_ui_elements":
- result = _game_get_ui_elements(json.data)
- "input_key":
- result = _game_input_key(json.data)
- "input_mouse":
- result = _game_input_mouse(json.data)
- "input_gamepad":
- result = _game_input_gamepad(json.data)
- "input_action":
- result = _game_input_action(json.data)
- "input_state":
- result = _game_input_state(json.data)
- "input_sequence":
- ## Async: steps frames and replies itself (deferred), so bail out
- ## before the synchronous send below — same shape as the eval and
- ## screenshot capture paths.
- _run_input_sequence(request_id, json.data)
- return
- _:
- _reply_game_command_error(request_id, "Unknown game op: %s" % op)
- return
-
- result["source"] = "game"
- result["op"] = op
- EngineDebugger.send_message("mcp:game_command_response",
- [request_id, JSON.stringify(_variant_to_json(result))])
-
-
-func _reply_game_command_error(request_id: String, message: String) -> void:
- EngineDebugger.send_message("mcp:game_command_error", [request_id, message])
-
-
-func _game_get_scene_tree(params: Dictionary) -> Dictionary:
- var depth := maxi(0, int(params.get("depth", 10)))
- var root := _resolve_runtime_node(str(params.get("root_path", "")))
- if root == null:
- return {"root": "", "nodes": [], "total_count": 0, "not_found": params.get("root_path", "")}
-
- var nodes: Array[Dictionary] = []
- _collect_runtime_nodes(root, 0, depth, nodes)
- return {
- "root": _runtime_path(root),
- "nodes": nodes,
- "total_count": nodes.size(),
- }
-
-
-func _collect_runtime_nodes(node: Node, current_depth: int, max_depth: int, out: Array[Dictionary]) -> void:
- out.append({
- "name": node.name,
- "type": node.get_class(),
- "path": _runtime_path(node),
- "children_count": node.get_child_count(),
- })
- if current_depth >= max_depth:
- return
- for child in node.get_children():
- if child is Node:
- _collect_runtime_nodes(child, current_depth + 1, max_depth, out)
-
-
-func _game_get_node_info(params: Dictionary) -> Dictionary:
- var path := str(params.get("path", ""))
- var node := _resolve_runtime_node(path)
- if node == null:
- return {"path": path, "found": false}
-
- var info := {
- "path": _runtime_path(node),
- "name": node.name,
- "type": node.get_class(),
- "children_count": node.get_child_count(),
- "groups": node.get_groups(),
- "found": true,
- }
- if bool(params.get("include_properties", true)):
- info["properties"] = _runtime_node_properties(node)
- return info
-
-
-func _game_get_ui_elements(params: Dictionary) -> Dictionary:
- var max_depth := maxi(0, int(params.get("max_depth", 10)))
- var include_hidden := bool(params.get("include_hidden", false))
- var include_disabled := bool(params.get("include_disabled", true))
- var root_path := str(params.get("root_path", ""))
- var root := _resolve_runtime_node(root_path)
- if root == null:
- return {"root": "", "elements": [], "total_count": 0, "not_found": root_path}
-
- var elements: Array[Dictionary] = []
- _collect_ui_elements(root, 0, max_depth, include_hidden, include_disabled, elements)
- return {
- "root": _runtime_path(root),
- "elements": elements,
- "total_count": elements.size(),
- }
-
-
-func _collect_ui_elements(
- node: Node,
- current_depth: int,
- max_depth: int,
- include_hidden: bool,
- include_disabled: bool,
- out: Array[Dictionary]
-) -> void:
- if node is Control:
- var control := node as Control
- var visible := _control_visible_in_tree(control)
- var disabled := _control_disabled(control)
- if (include_hidden or visible) and (include_disabled or not disabled):
- out.append(_ui_element_info(control, visible, disabled))
-
- if current_depth >= max_depth:
- return
- for child in node.get_children():
- if child is Node:
- _collect_ui_elements(
- child,
- current_depth + 1,
- max_depth,
- include_hidden,
- include_disabled,
- out
- )
-
-
-func _ui_element_info(control: Control, visible: bool, disabled: bool) -> Dictionary:
- var info := {
- "path": _runtime_path(control),
- "name": control.name,
- "type": control.get_class(),
- "visible": visible,
- "disabled": disabled,
- "rect": _variant_to_json(control.get_rect()),
- "global_rect": _variant_to_json(control.get_global_rect()),
- }
- if _object_has_property(control, "text"):
- info["text"] = str(control.get("text"))
- return info
-
-
-func _control_disabled(control: Control) -> bool:
- if _object_has_property(control, "disabled"):
- return bool(control.get("disabled"))
- return false
-
-
-func _control_visible_in_tree(control: Control) -> bool:
- if not control.visible:
- return false
- var parent := control.get_parent()
- while parent != null:
- if parent is CanvasItem and not (parent as CanvasItem).visible:
- return false
- parent = parent.get_parent()
- if Engine.is_editor_hint():
- return true
- return control.is_visible_in_tree()
-
-
-static var _property_name_cache: Dictionary = {}
-
-
-func _object_has_property(obj: Object, property_name: String) -> bool:
- var key := _property_cache_key(obj)
- if not _property_name_cache.has(key):
- var names := {}
- for prop in obj.get_property_list():
- names[str(prop.get("name", ""))] = true
- _property_name_cache[key] = names
- return (_property_name_cache[key] as Dictionary).has(property_name)
-
-
-func _property_cache_key(obj: Object) -> String:
- var script = obj.get_script()
- if script == null:
- return obj.get_class()
- var script_id := str(script.get_instance_id())
- if not script.resource_path.is_empty():
- script_id = script.resource_path
- return "%s:%s" % [obj.get_class(), script_id]
-
-
-func _runtime_node_properties(node: Node) -> Dictionary:
- var props := {}
- for p in node.get_property_list():
- var name := str(p.get("name", ""))
- var usage := int(p.get("usage", 0))
- if name.is_empty() or (usage & PROPERTY_USAGE_EDITOR) == 0:
- continue
- props[name] = _variant_to_json(node.get(name))
- return props
-
-
-func _resolve_runtime_node(path: String) -> Node:
- var scene_root := _current_scene_root()
- if scene_root == null:
- return null
- if path.is_empty() or path == "/":
- return scene_root
-
- if path.begins_with("/root/"):
- return get_tree().root.get_node_or_null(path.trim_prefix("/root/"))
-
- var scene_path := path.trim_prefix("/")
- if scene_path == str(scene_root.name):
- return scene_root
- var prefix := str(scene_root.name) + "/"
- if scene_path.begins_with(prefix):
- scene_path = scene_path.substr(prefix.length())
- return scene_root.get_node_or_null(scene_path)
-
-
-func _runtime_path(node: Node) -> String:
- var scene_root := _current_scene_root()
- if scene_root == null:
- return str(node.get_path())
- if node == scene_root:
- return "/" + str(scene_root.name)
- return "/" + str(scene_root.name) + "/" + str(scene_root.get_path_to(node))
-
-
-func _current_scene_root() -> Node:
- var tree := get_tree()
- if tree == null:
- return null
- var scene_root := tree.current_scene
- if scene_root == null and Engine.is_editor_hint():
- # Look the editor singleton up by name rather than referencing the bare
- # `EditorInterface` identifier: that identifier is compiled out of export
- # templates, so the GDScript parser rejects it ("Identifier
- # "EditorInterface" not declared in the current scope") in an exported
- # build even though `Engine.is_editor_hint()` would never run it there.
- # That parse failure stops this autoload from loading in every export.
- var editor := Engine.get_singleton(&"EditorInterface")
- if editor:
- scene_root = editor.get_edited_scene_root()
- return scene_root
-
-
-func _game_input_key(params: Dictionary) -> Dictionary:
- var key_name := str(params.get("key", ""))
- var keycode := OS.find_keycode_from_string(key_name)
- if keycode == KEY_NONE:
- return {"sent": false, "error": "Unknown key: %s" % key_name}
- var ev := InputEventKey.new()
- ev.keycode = keycode
- ev.physical_keycode = keycode
- ev.pressed = bool(params.get("pressed", true))
- ev.echo = bool(params.get("echo", false))
- Input.parse_input_event(ev)
- return {"sent": true, "key": key_name, "pressed": ev.pressed}
-
-
-func _game_input_mouse(params: Dictionary) -> Dictionary:
- var event := str(params.get("event", "button"))
- var pos_result := _resolve_mouse_position(params.get("position"))
- if pos_result.has("error"):
- return {"sent": false, "event": event, "error": pos_result.error}
- var pos: Vector2 = pos_result.position
- match event:
- "motion":
- var motion := InputEventMouseMotion.new()
- motion.position = pos
- motion.global_position = pos
- Input.parse_input_event(motion)
- return {"sent": true, "event": "motion", "position": _variant_to_json(pos)}
- "button":
- var button_event := InputEventMouseButton.new()
- button_event.position = pos
- button_event.global_position = pos
- button_event.button_index = _mouse_button_index(str(params.get("button", "left")))
- button_event.pressed = bool(params.get("pressed", true))
- Input.parse_input_event(button_event)
- return {
- "sent": true,
- "event": "button",
- "button": params.get("button", "left"),
- "pressed": button_event.pressed,
- "position": _variant_to_json(pos),
- }
- return {"sent": false, "error": "Invalid mouse event: %s" % event}
-
-
-func _game_input_gamepad(params: Dictionary) -> Dictionary:
- var device := int(params.get("device", 0))
- var control := str(params.get("control", "button"))
- match control:
- "button":
- var button := InputEventJoypadButton.new()
- button.device = device
- button.button_index = int(params.get("index", 0))
- button.pressed = bool(params.get("pressed", true))
- Input.parse_input_event(button)
- return {"sent": true, "control": "button", "device": device, "index": button.button_index, "pressed": button.pressed}
- "axis":
- var axis := InputEventJoypadMotion.new()
- axis.device = device
- axis.axis = int(params.get("index", 0))
- axis.axis_value = float(params.get("value", 0.0))
- Input.parse_input_event(axis)
- return {"sent": true, "control": "axis", "device": device, "index": axis.axis, "value": axis.axis_value}
- return {"sent": false, "error": "Invalid gamepad control: %s" % control}
-
-
-func _game_input_action(params: Dictionary) -> Dictionary:
- var action := str(params.get("action", ""))
- if action.is_empty():
- return {"sent": false, "error": "Missing action"}
- if not InputMap.has_action(action):
- return {"sent": false, "action": action, "error": "Unknown action: %s" % action}
- var pressed := bool(params.get("pressed", true))
- var strength := clampf(float(params.get("strength", 1.0)), 0.0, 1.0)
- if pressed:
- Input.action_press(action, strength)
- else:
- Input.action_release(action)
- return {
- "sent": true,
- "action": action,
- "pressed": pressed,
- "strength": strength,
- "delivery": "action_state",
- }
-
-
-func _game_input_state(params: Dictionary) -> Dictionary:
- var actions: Array = params.get("actions", [])
- if actions.is_empty():
- actions = InputMap.get_actions()
- var states := {}
- for action in actions:
- var name := str(action)
- states[name] = Input.is_action_pressed(name)
- return {"actions": states}
-
-
-## --- input_sequence: frame-timed action timeline (deferred) ---
-##
-## Per-step round-trips can't hit a target frame — network jitter lands each
-## input on whatever frame its reply happens to arrive on, so a jump arc or a
-## timed combo is unreproducible (#814). input_sequence takes the whole
-## timeline in one call and drives it game-side, applying each step's action on
-## its scheduled frame, then replies once (deferred). Frame count (not ms) is
-## the timing basis: it's what reproduces identically across runs.
-
-## Hard caps mirrored by the server-side schema (see game handlers). The game
-## side re-checks them so a malformed direct message can't park the coroutine
-## on an unbounded await; the server rejects the same cases up front with a
-## clearer error.
-##
-## The frame cap bounds the sequence in *frames*, which is a wall-clock time
-## only at a given FPS: 600 frames is ~10s at 60fps but longer under load or on
-## a throttled runner. It is not sized to the ~30s deferred budget
-## (editor_handler.INPUT_SEQUENCE_TIMEOUT_SEC) — the two are independent
-## safeguards. If a genuinely slow run exceeds the budget, the dispatcher
-## returns a clean DEFERRED_TIMEOUT rather than hanging, so the cap can stay a
-## simple frame count.
-const MAX_SEQUENCE_STEPS := 256
-const MAX_SEQUENCE_FRAMES := 600
-
-## Testing seam: the last input_sequence reply (response or error), recorded
-## before the EngineDebugger channel (inactive in the editor-side test
-## harness). Mirrors _last_screenshot_reply / _last_eval_reply.
-var _last_game_command_reply: Dictionary = {}
-
-## Testing seam: overrides the per-frame wait in _run_input_sequence. Left
-## invalid in production (real `process_frame` awaits). A test sets it to a
-## synchronously-returning Callable so the multi-frame loop runs to completion
-## in one call — the editor test runner invokes tests synchronously and never
-## pumps `process_frame`, so a real frame-await would suspend and record zero
-## assertions. Timing itself (one frame per step) is engine-guaranteed; this
-## seam covers the scheduling/application/reply logic layered on top.
-var _frame_waiter: Callable = Callable()
-
-
-## Validate + normalize an input_sequence request. Pure (no engine state), so
-## the ordering/cap/shape rules are unit-testable without a running game.
-## Returns {"error": String} or {"steps": Array, "end_frame": int}.
-func _plan_input_sequence(params: Dictionary) -> Dictionary:
- var raw_steps: Variant = params.get("steps", null)
- if not (raw_steps is Array):
- return {"error": "steps must be an array"}
- var steps_arr: Array = raw_steps
- if steps_arr.is_empty():
- return {"error": "steps must not be empty"}
- if steps_arr.size() > MAX_SEQUENCE_STEPS:
- return {"error": "steps exceeds cap of %d (got %d)" % [MAX_SEQUENCE_STEPS, steps_arr.size()]}
-
- ## Validate field *kinds* rather than coercing them: the server already
- ## rejects bad shapes, but this planner is also the backstop for a
- ## malformed direct debugger message, so it must not silently turn
- ## pressed="false" into true or at_frame="oops" into 0. _is_number accepts
- ## int or float (JSON round-trips whole numbers as either) but not bool or
- ## string, so this stays consistent with the server without tripping on
- ## JSON's number typing.
- var settle_raw: Variant = params.get("settle_frames", 0)
- if not _is_number(settle_raw):
- return {"error": "settle_frames must be a number"}
- var settle_frames := int(settle_raw)
- if settle_frames < 0:
- return {"error": "settle_frames must be >= 0"}
-
- var normalized: Array = []
- var prev_frame := -1
- for i in steps_arr.size():
- var raw: Variant = steps_arr[i]
- if not (raw is Dictionary):
- return {"error": "steps[%d] must be an object" % i}
- var step: Dictionary = raw
- if not (step.get("action", "") is String) or str(step.get("action", "")).is_empty():
- return {"error": "steps[%d].action is required" % i}
- var action: String = step["action"]
- var at_frame_raw: Variant = step.get("at_frame", 0)
- if not _is_number(at_frame_raw):
- return {"error": "steps[%d].at_frame must be a number" % i}
- var at_frame := int(at_frame_raw)
- if at_frame < 0:
- return {"error": "steps[%d].at_frame must be >= 0" % i}
- if at_frame < prev_frame:
- return {"error": "steps must be ordered by at_frame (steps[%d]=%d < previous %d)" % [i, at_frame, prev_frame]}
- prev_frame = at_frame
- var pressed_raw: Variant = step.get("pressed", true)
- if not (pressed_raw is bool):
- return {"error": "steps[%d].pressed must be a boolean" % i}
- var strength_raw: Variant = step.get("strength", 1.0)
- if not _is_number(strength_raw):
- return {"error": "steps[%d].strength must be a number" % i}
- normalized.append({
- "at_frame": at_frame,
- "action": action,
- "pressed": pressed_raw,
- "strength": clampf(float(strength_raw), 0.0, 1.0),
- })
-
- var end_frame: int = int(normalized[-1]["at_frame"]) + settle_frames
- if end_frame > MAX_SEQUENCE_FRAMES:
- return {"error": "sequence spans %d frames, exceeds cap of %d" % [end_frame, MAX_SEQUENCE_FRAMES]}
- return {"steps": normalized, "end_frame": end_frame}
-
-
-## Async: apply each step's action on its scheduled frame, awaiting one
-## process_frame per frame, then reply (deferred). Bails out before applying
-## anything if the plan is invalid or any action is unknown to the running
-## game's InputMap — a half-applied timeline leaves inputs in an undefined
-## state, so it's all-or-nothing on the pre-checks.
-func _run_input_sequence(request_id: String, params: Dictionary) -> void:
- var plan := _plan_input_sequence(params)
- if plan.has("error"):
- _reply_input_sequence_error(request_id, plan["error"])
- return
-
- var steps: Array = plan["steps"]
- var end_frame: int = plan["end_frame"]
-
- ## Resolve action names against the *game's* InputMap up front — the server
- ## can't see it, so this is the first place unknown actions surface.
- for step in steps:
- if not InputMap.has_action(step["action"]):
- _reply_input_sequence_error(request_id, "Unknown action: %s" % step["action"])
- return
-
- var tree := get_tree()
- if tree == null:
- _reply_input_sequence_error(request_id, "No SceneTree available for input sequence")
- return
-
- var applied: Array = []
- var step_i := 0
- for f in range(0, end_frame + 1):
- while step_i < steps.size() and int(steps[step_i]["at_frame"]) == f:
- var step: Dictionary = steps[step_i]
- _game_input_action(step)
- applied.append({"at_frame": f, "action": step["action"], "pressed": step["pressed"]})
- step_i += 1
- if f < end_frame:
- if _frame_waiter.is_valid():
- await _frame_waiter.call()
- else:
- await tree.process_frame
-
- _reply_input_sequence_ok(request_id, {
- "completed": true,
- "steps_applied": applied.size(),
- "frames_elapsed": end_frame,
- "applied": applied,
- "actions_pressed_at_end": _actions_pressed_at_end(steps),
- })
-
-
-## Distinct actions the sequence touched that are still held at the end, so the
-## caller knows what it must release (a press with no matching release leaves
-## the action stuck on across the next frames).
-func _actions_pressed_at_end(steps: Array) -> Array:
- var seen := {}
- var pressed: Array = []
- for step in steps:
- var action: String = step["action"]
- if seen.has(action):
- continue
- seen[action] = true
- if Input.is_action_pressed(action):
- pressed.append(action)
- return pressed
-
-
-func _reply_input_sequence_ok(request_id: String, result: Dictionary) -> void:
- result["source"] = "game"
- result["op"] = "input_sequence"
- _last_game_command_reply = {"kind": "response", "op": "input_sequence", "result": result}
- if EngineDebugger.is_active():
- EngineDebugger.send_message("mcp:game_command_response",
- [request_id, JSON.stringify(_variant_to_json(result))])
-
-
-func _reply_input_sequence_error(request_id: String, message: String) -> void:
- _last_game_command_reply = {"kind": "error", "op": "input_sequence", "message": message}
- if EngineDebugger.is_active():
- EngineDebugger.send_message("mcp:game_command_error", [request_id, message])
-
-
-## Resolve a mouse-position param. Absent (null, or an empty {}) falls back to
-## the live cursor position — a deliberate default. A present but wrong-shaped
-## value is rejected instead of silently substituting the cursor, which
-## previously hid caller bugs (#635). Accepts a {x, y} dict or an [x, y] array;
-## returns {position: Vector2} or {error: String}.
-func _resolve_mouse_position(value: Variant) -> Dictionary:
- var viewport := get_viewport()
- var fallback := viewport.get_mouse_position() if viewport != null else Vector2.ZERO
- if value == null:
- return {"position": fallback}
- if value is Dictionary:
- var dict: Dictionary = value
- if dict.is_empty():
- return {"position": fallback}
- # A non-empty dict that carries neither coordinate is a caller mistake,
- # not "use the default" — reject rather than silently substitute.
- if not dict.has("x") and not dict.has("y"):
- return {"error": "position object must have an 'x' and/or 'y' key (got keys %s)" % str(dict.keys())}
- var x_val: Variant = dict.get("x", fallback.x)
- var y_val: Variant = dict.get("y", fallback.y)
- if not _is_number(x_val) or not _is_number(y_val):
- return {"error": "position x/y must be numbers (got x=%s, y=%s)" % [type_string(typeof(x_val)), type_string(typeof(y_val))]}
- return {"position": Vector2(float(x_val), float(y_val))}
- if value is Array:
- var arr: Array = value
- if arr.size() != 2:
- return {"error": "position array must be [x, y] (got %d elements)" % arr.size()}
- if not _is_number(arr[0]) or not _is_number(arr[1]):
- return {"error": "position array elements must be numbers (got [%s, %s])" % [type_string(typeof(arr[0])), type_string(typeof(arr[1]))]}
- return {"position": Vector2(float(arr[0]), float(arr[1]))}
- return {"error": "position must be a {x, y} object or [x, y] array (got %s)" % type_string(typeof(value))}
-
-
-func _is_number(v: Variant) -> bool:
- return typeof(v) == TYPE_INT or typeof(v) == TYPE_FLOAT
-
-
-func _mouse_button_index(name: String) -> int:
- match name:
- "right":
- return MOUSE_BUTTON_RIGHT
- "middle":
- return MOUSE_BUTTON_MIDDLE
- "wheel_up":
- return MOUSE_BUTTON_WHEEL_UP
- "wheel_down":
- return MOUSE_BUTTON_WHEEL_DOWN
- return MOUSE_BUTTON_LEFT
-
-
-## --- game_eval: execute arbitrary GDScript in the running game ---
-
-## Wall-clock ceiling for a single game_eval. Evaluated code that awaits
-## something which never completes (a signal that never fires, a timer on a
-## paused tree) would otherwise pin the request open until the dispatcher's
-## 15s deferred budget / the server's 15s command timeout fires it as an
-## opaque INTERNAL_ERROR — with the temp eval Node leaked into the tree.
-## Bounding it here lets us free the node and reply with an actionable
-## message instead. See hi-godot/godot-ai#487.
-##
-## TIMEOUT ORDERING — load-bearing across three files: this value MUST stay
-## below the editor-side fallback timer in
-## `debugger/mcp_debugger_plugin.gd::request_game_eval` (`timeout_sec`,
-## default 10.0), which in turn stays below the dispatcher's `game_eval`
-## budget in `dispatcher.gd` (15000 ms). So: game 8s < editor 10s <
-## dispatcher 15s. Only this game-side guard emits the specific
-## "Eval exceeded 8s" message (both it and the editor backstop now carry the
-## EVAL_HUNG code, #518, but the editor's message can't name the cause).
-## Raise this at/above the editor timer (or drop that timer below this) and
-## the less specific editor message wins the race, silently losing the
-## diagnostic this fix exists to provide. The cross-file contract is enforced
-## by tests/unit/test_game_eval_timeout_ordering.py.
-##
-## NOTE: this catches a hung `await`, not a CPU-bound loop with no `await` —
-## a tight `while true:` with no yield blocks the main thread, so nothing
-## (including this poll) runs until it yields. That case is out of scope.
-const EVAL_TIMEOUT_SEC := 8.0
-
-
-func _handle_eval(data: Array) -> void:
- var request_id: String = data[0] if data.size() > 0 else ""
- var code: String = data[1] if data.size() > 1 else ""
-
- if code.is_empty():
- _reply_eval_error(request_id, "No code provided")
- return
-
- ## Wrap user code in an execute() coroutine (so it can `await` internally)
- ## whose inner function is uniquely named per eval. A runtime error's
- ## backtrace then carries `_mcp_run_`, letting us attribute it to
- ## THIS eval — not an unrelated background game error, and not a sibling
- ## overlapping eval. (#490)
- _eval_token_counter += 1
- var token := str(_eval_token_counter)
- var run_fn := "_mcp_run_%s" % token
- var script_source := (
- "extends Node\n"
- + "func execute():\n"
- + "\treturn await %s()\n\n" % run_fn
- + "func %s():\n" % run_fn
- + _indent_eval_code(code)
- )
-
- ## Snapshot the logger's script-error seq BEFORE running so we only attribute
- ## errors raised by this eval. In a debug build a parse error aborts reload()
- ## and a runtime error aborts execute() — either way this function may never
- ## reach its reply: the editor infers a compile error from the missing
- ## mcp:eval_compiled beacon, and a runtime error is reported (via the
- ## eval_check probe / the in-flight poll loop) once a logged error past this
- ## baseline carries this eval's token.
- var baseline: int = _logger.script_error_seq() if _logger != null else 0
-
- var script: GDScript = GDScript.new()
- script.source_code = script_source
- ## #490: ack BEFORE reload(). A parse error aborts this function at reload()
- ## without a return code in a debug build, so this is our only chance to tell
- ## the editor "received + about to compile." The editor uses that to tell a
- ## real parse error (acked, never compiled) apart from a message it simply
- ## hasn't serviced yet (never acked); see mcp_debugger_plugin._on_eval_grace.
- EngineDebugger.send_message("mcp:eval_ack", [request_id])
- ## reload() ABORTS this function on a parse error in a debug build (it does
- ## not return a non-OK code there), so the lines below only run when the
- ## source compiled. Keep reload() INLINE — moving it behind a timer/await
- ## poisons subsequent evals (#490). The err branch still matters for the
- ## editor process (handler unit tests), where reload() does return.
- var err: int = script.reload()
- if err != OK:
- _reply_eval_error(request_id,
- "Failed to compile GDScript (error %d). Check syntax." % err)
- return
-
- ## Compiled OK — tell the editor so its grace timer doesn't flag a compile
- ## error and so it begins probing for a runtime error.
- EngineDebugger.send_message("mcp:eval_compiled", [request_id])
-
- var temp_node := Node.new()
- temp_node.set_script(script)
- temp_node.process_mode = Node.PROCESS_MODE_ALWAYS
- add_child(temp_node)
-
- if not temp_node.has_method("execute"):
- temp_node.queue_free()
- _reply_eval_error(request_id, "Internal error: eval wrapper is missing execute().")
- return
-
- ## Register in-flight BEFORE running: a runtime error aborts execute() (and
- ## may unwind this function) before we could record it afterward, and the
- ## editor probe / poll loop need the entry to attribute and report the error.
- _inflight_evals[request_id] = {"node": temp_node, "token": token, "baseline": baseline}
-
- ## Drive execute() as a fire-and-forget coroutine that records its outcome
- ## into `holder`, then poll frames until it finishes or the deadline passes
- ## (#488's hung-await guard). A plain `await temp_node.execute()` has no
- ## escape hatch: if user code never returns, we never reach the reply/cleanup
- ## below and the request hangs with the node leaked.
- var holder := {"done": false, "value": null, "abandoned": false}
- _drive_eval(temp_node, holder)
-
- var tree := get_tree()
- var deadline_ms := int(EVAL_TIMEOUT_SEC * 1000.0)
- var start_ms := Time.get_ticks_msec()
- while not holder["done"] and (Time.get_ticks_msec() - start_ms) < deadline_ms:
- ## #490 focused fast path: a runtime error aborts _drive_eval (holder
- ## never completes), so check each frame whether THIS eval's token now
- ## appears in a logged error and report it immediately. (Backgrounded,
- ## this loop is frozen and the editor probe does the same job.)
- if _try_report_eval_runtime_error(request_id):
- holder["abandoned"] = true
- return
- await tree.process_frame
-
- if not holder["done"]:
- ## Past the 8s deadline. Disambiguate a runtime error (its token is in a
- ## logged error) from a genuine hung await before the generic timeout.
- holder["abandoned"] = true
- if _try_report_eval_runtime_error(request_id):
- return
- _inflight_evals.erase(request_id)
- if is_instance_valid(temp_node):
- remove_child(temp_node)
- _reply_eval_error(request_id,
- ("Eval exceeded %ds and was aborted — the code likely awaits "
- + "something that never completes (a signal that never fires, a timer on "
- + "a paused tree) or loops forever. Check logs_read(source='game').")
- % int(EVAL_TIMEOUT_SEC),
- ErrorCodes.EVAL_HUNG)
- return
-
- ## Clean finish.
- _inflight_evals.erase(request_id)
- temp_node.queue_free()
- _reply_eval_response(request_id, holder["value"])
-
-
-## Run the compiled eval node's execute() and stash the result. Kept
-## separate from _handle_eval so the latter can race it against a deadline
-## via frame polling. If the eval was abandoned (timed out) before this
-## resumes, drop the result and free the now-detached node — _handle_eval
-## has already replied.
-##
-## RESIDUAL LEAK (accepted): if the awaited thing *never* fires, this
-## coroutine never resumes, so the `node` it holds is detached (via
-## _handle_eval's remove_child) but never freed — one orphaned Node per such
-## timeout, for the game-process lifetime. GDScript has no way to cancel a
-## suspended coroutine, so this is the best achievable in-process. It is still
-## strictly better than the pre-#487 behavior, where the node leaked *into*
-## the live tree and the request hung to the 15s ceiling.
-func _drive_eval(node: Node, holder: Dictionary) -> void:
- var value = await node.execute()
- if holder.get("abandoned", false):
- if is_instance_valid(node):
- node.queue_free()
- return
- holder["value"] = value
- holder["done"] = true
-
-
-## #518: cap on the serialized eval result. Godot's remote-debugger TCP peer
-## silently discards any single message over ~8 MiB, so a bigger reply never
-## reaches the editor and the request rides to the 10s backstop as a phantom
-## "hang". (Results over the editor↔server WebSocket buffer cap of 4 MiB fail
-## there with their own explicit error; this game-side cap only needs to stay
-## under the debugger peer's drop threshold to keep the failure visible.)
-const EVAL_RESULT_MAX_BYTES := 6 * 1024 * 1024
-
-## Testing seam: the last eval reply, recorded before hitting the
-## EngineDebugger channel (inactive in the editor-side test harness).
-var _last_eval_reply: Dictionary = {}
-
-
-## `code` (optional) rides as a third payload element so the editor can map
-## the reply to a specific error code instead of the generic INTERNAL_ERROR;
-## the editor allowlists the value (see mcp_debugger_plugin._on_eval_error).
-func _reply_eval_error(request_id: String, message: String, code: String = "") -> void:
- _last_eval_reply = {"kind": "error", "request_id": request_id,
- "message": message, "code": code}
- var payload := [request_id, message]
- if not code.is_empty():
- payload.append(code)
- if EngineDebugger.is_active():
- EngineDebugger.send_message("mcp:eval_error", payload)
-
-
-func _reply_eval_response(request_id: String, value: Variant) -> void:
- var serialized := JSON.stringify(_variant_to_json(value))
- var serialized_bytes := serialized.to_utf8_buffer().size()
- if serialized_bytes > EVAL_RESULT_MAX_BYTES:
- _reply_eval_error(request_id,
- ("Eval result too large to return (%d bytes serialized, limit %d). "
- + "Return a smaller slice instead — e.g. counts, node paths, or a "
- + "truncated substring.") % [serialized_bytes, EVAL_RESULT_MAX_BYTES],
- ErrorCodes.EVAL_RESULT_TOO_LARGE)
- return
- _last_eval_reply = {"kind": "response", "request_id": request_id}
- if EngineDebugger.is_active():
- EngineDebugger.send_message("mcp:eval_response", [request_id, serialized])
-
-
-## #490: if a logged script error past THIS eval's baseline carries its unique
-## wrapper-function token, a runtime error aborted it before it could reply —
-## report it with the real text + line. Returns true if it reported. Called
-## from the editor's eval_check probe (the reliable path when a backgrounded
-## game's idle loop is frozen — the debugger capture callback still runs) and
-## from _handle_eval's poll loop (the focused fast path). Token + baseline
-## matching means an unrelated background error, or a sibling overlapping
-## eval's error, can never fail this request.
-func _try_report_eval_runtime_error(request_id: String) -> bool:
- if _logger == null:
- return false
- var entry = _inflight_evals.get(request_id)
- if entry == null:
- return false
- var text: String = _logger.find_script_error_since(
- int(entry["baseline"]), "_mcp_run_%s" % str(entry["token"]))
- if text.is_empty():
- return false
- _inflight_evals.erase(request_id)
- var node: Node = entry["node"]
- if node != null and is_instance_valid(node):
- node.queue_free()
- if EngineDebugger.is_active():
- EngineDebugger.send_message("mcp:eval_runtime_error", [request_id, text])
- return true
-
-
-## #490: answer an editor eval_check probe. The editor polls this once the
-## eval has compiled but not yet replied. This runs in the debugger capture
-## callback, which stays live even when the backgrounded game's _process is
-## frozen — so it's the reliable channel for reporting a runtime error that
-## aborted the eval. Report if one is detected for this request, else stay
-## silent (the editor keeps polling until the real reply or the hang timeout).
-func _handle_eval_check(data: Array) -> void:
- var request_id: String = data[0] if data.size() > 0 else ""
- if request_id.is_empty():
- return
- _try_report_eval_runtime_error(request_id)
-
-
-func _indent_eval_code(code: String) -> String:
- var lines: PackedStringArray = code.split("\n")
- var out := ""
- for line in lines:
- out += "\t" + line + "\n"
- return out
-
-
-## Serialize any Godot Variant to a JSON-safe dictionary/array/primitive.
-## Ported from godot-mcp's mcp_interaction_server.gd.
-func _variant_to_json(value: Variant) -> Variant:
- if value == null:
- return null
- if value is bool or value is int or value is float or value is String:
- return value
- if value is Vector2:
- return {"x": value.x, "y": value.y}
- if value is Vector3:
- return {"x": value.x, "y": value.y, "z": value.z}
- if value is Vector4:
- return {"x": value.x, "y": value.y, "z": value.z, "w": value.w}
- if value is Vector2i:
- return {"x": value.x, "y": value.y}
- if value is Vector3i:
- return {"x": value.x, "y": value.y, "z": value.z}
- if value is Vector4i:
- return {"x": value.x, "y": value.y, "z": value.z, "w": value.w}
- if value is Color:
- return {"r": value.r, "g": value.g, "b": value.b, "a": value.a}
- if value is Quaternion:
- return {"x": value.x, "y": value.y, "z": value.z, "w": value.w}
- if value is Basis:
- return {
- "x": _variant_to_json(value.x),
- "y": _variant_to_json(value.y),
- "z": _variant_to_json(value.z),
- }
- if value is Transform3D:
- return {
- "basis": _variant_to_json(value.basis),
- "origin": _variant_to_json(value.origin),
- }
- if value is Transform2D:
- return {
- "x": _variant_to_json(value.x),
- "y": _variant_to_json(value.y),
- "origin": _variant_to_json(value.origin),
- }
- if value is Rect2:
- return {
- "position": _variant_to_json(value.position),
- "size": _variant_to_json(value.size),
- }
- if value is Rect2i:
- return {
- "position": _variant_to_json(value.position),
- "size": _variant_to_json(value.size),
- }
- if value is AABB:
- return {
- "position": _variant_to_json(value.position),
- "size": _variant_to_json(value.size),
- }
- if value is NodePath or value is StringName:
- return str(value)
- if value is Plane:
- return {
- "normal": _variant_to_json(value.normal),
- "d": value.d,
- }
- if value is Projection:
- return {
- "x": _variant_to_json(value.x),
- "y": _variant_to_json(value.y),
- "z": _variant_to_json(value.z),
- "w": _variant_to_json(value.w),
- }
- ## Packed arrays
- if value is PackedByteArray:
- var arr: Array = []
- for item in value: arr.append(item)
- return arr
- if value is PackedInt32Array or value is PackedInt64Array:
- var arr: Array = []
- for item in value: arr.append(item)
- return arr
- if value is PackedFloat32Array or value is PackedFloat64Array:
- var arr: Array = []
- for item in value: arr.append(item)
- return arr
- if value is PackedStringArray:
- var arr: Array = []
- for item in value: arr.append(item)
- return arr
- if value is PackedVector2Array:
- var arr: Array = []
- for item in value: arr.append({"x": item.x, "y": item.y})
- return arr
- if value is PackedVector3Array:
- var arr: Array = []
- for item in value: arr.append({"x": item.x, "y": item.y, "z": item.z})
- return arr
- if value is PackedVector4Array:
- var arr: Array = []
- for item in value: arr.append({"x": item.x, "y": item.y, "z": item.z, "w": item.w})
- return arr
- if value is PackedColorArray:
- var arr: Array = []
- for item in value: arr.append({"r": item.r, "g": item.g, "b": item.b, "a": item.a})
- return arr
- ## Generic arrays and dictionaries — recurse
- if value is Array:
- var arr: Array = []
- for item in value:
- arr.append(_variant_to_json(item))
- return arr
- if value is Dictionary:
- var dict: Dictionary = {}
- for key in value.keys():
- dict[str(key)] = _variant_to_json(value[key])
- return dict
- ## Fallback: string representation
- return str(value)
diff --git a/addons/godot_ai/runtime/game_helper.gd.uid b/addons/godot_ai/runtime/game_helper.gd.uid
deleted file mode 100644
index 7224a50..0000000
--- a/addons/godot_ai/runtime/game_helper.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://gfybkdtsclti
diff --git a/addons/godot_ai/runtime/game_logger.gd b/addons/godot_ai/runtime/game_logger.gd
deleted file mode 100644
index 6384960..0000000
--- a/addons/godot_ai/runtime/game_logger.gd
+++ /dev/null
@@ -1,142 +0,0 @@
-@tool
-extends Logger
-
-## Game-process Logger subclass.
-##
-## NOTE: deliberately no `class_name`. Registered from inside the running
-## game so we can intercept print(), printerr(), push_error(), and
-## push_warning() and ferry them back to the editor over the EngineDebugger
-## channel — the same bridge PR #76 uses for screenshots.
-##
-## Logger virtuals can be called from any thread (e.g. async loaders push
-## errors off the main thread). We accumulate into _pending under a Mutex
-## and the host (game_helper.gd) flushes once per frame from the main
-## thread, where EngineDebugger.send_message is safe to call.
-
-## `McpLogBacktrace` is published as a `class_name` on log_backtrace.gd, but a
-## freshly-launched game subprocess (no prior editor scan; e.g. CI launching
-## `--headless --path`) hits this autoload before the global class_name table
-## is populated, and parsing this script fails with
-## "Identifier 'McpLogBacktrace' not declared in the current scope". Using
-## `const preload` resolves the path at parse time and is independent of the
-## class_name registry — matches the project convention in CLAUDE.md
-## ("Internals … skip class_name entirely and load via const preload").
-const _LogBacktrace := preload("res://addons/godot_ai/utils/log_backtrace.gd")
-
-var _pending: Array = []
-var _mutex := Mutex.new()
-## #490: a monotonic sequence + a small ring of recent GDScript runtime
-## (script-type) errors, each with its text AND the function names in its
-## backtrace. game_helper uses this to attribute a runtime error to the
-## *specific* eval that raised it: each eval's wrapper has a uniquely named
-## inner function, and game_helper asks find_script_error_since() whether any
-## error past its pre-eval baseline carries that function in its stack. This
-## avoids failing an eval on an unrelated background game error that merely
-## advanced a global counter, and keeps overlapping evals from cross-
-## attributing. Gated on ERROR_TYPE_SCRIPT (2) so push_error()/push_warning()
-## (types 0/1) never count. Mutex-guarded: _log_error can fire from any thread.
-const _ERROR_TYPE_SCRIPT := 2
-const _MAX_RECENT_SCRIPT_ERRORS := 64
-var _script_error_seq: int = 0
-var _recent_script_errors: Array = []
-
-
-func _log_message(message: String, error: bool) -> void:
- ## `error` is true for printerr(), false for print().
- var level := "error" if error else "info"
- _append(level, message)
-
-
-func _log_error(
- function: String,
- file: String,
- line: int,
- code: String,
- rationale: String,
- _editor_notify: bool,
- error_type: int,
- script_backtraces: Array,
-) -> void:
- ## EngineDebugger's payload shape is `[level, text]` — the source
- ## location has nowhere structured to land for the game side, so we
- ## inline it into `text`. editor_logger keeps the resolved fields
- ## as structured columns instead.
- var resolved := _LogBacktrace.resolve_error(
- function, file, line, code, rationale, error_type, script_backtraces,
- )
- var loc := ""
- if not resolved.path.is_empty():
- loc = "%s:%d @ %s" % [resolved.path, resolved.line, resolved.function] if not resolved.function.is_empty() else "%s:%d" % [resolved.path, resolved.line]
- var text: String = "%s (%s)" % [resolved.message, loc] if not loc.is_empty() else resolved.message
- var details: Dictionary = resolved.get("details", {})
- _append(resolved.level, text, details)
- if error_type == _ERROR_TYPE_SCRIPT:
- ## Collect every function name in the first non-empty backtrace so
- ## game_helper can match its eval's uniquely named wrapper function.
- var funcs := PackedStringArray()
- for bt: RefCounted in script_backtraces:
- if bt != null and bt.get_frame_count() > 0:
- for i: int in bt.get_frame_count():
- funcs.append(bt.get_frame_function(i))
- break
- _mutex.lock()
- _script_error_seq += 1
- _recent_script_errors.append({"seq": _script_error_seq, "text": text, "funcs": funcs})
- if _recent_script_errors.size() > _MAX_RECENT_SCRIPT_ERRORS:
- _recent_script_errors.remove_at(0)
- _mutex.unlock()
-
-
-func _append(level: String, text: String, details: Dictionary = {}) -> void:
- _mutex.lock()
- if details.is_empty():
- _pending.append([level, text])
- else:
- _pending.append([level, text, details.duplicate(true)])
- _mutex.unlock()
-
-
-## Drain the pending queue and return entries as [[level, text], ...].
-## Called from the main thread by game_helper each frame.
-func drain() -> Array:
- _mutex.lock()
- var out := _pending
- _pending = []
- _mutex.unlock()
- return out
-
-
-func has_pending() -> bool:
- _mutex.lock()
- var any := not _pending.is_empty()
- _mutex.unlock()
- return any
-
-
-## #490: monotonic count of script-type runtime errors seen this run.
-## game_helper snapshots this before an eval to use as the `since_seq`
-## baseline for find_script_error_since(). Mutex-guarded.
-func script_error_seq() -> int:
- _mutex.lock()
- var v := _script_error_seq
- _mutex.unlock()
- return v
-
-
-## #490: text of the most recent script error with seq > since_seq whose
-## backtrace includes `function_name`, or "" if none. Lets game_helper
-## attribute a runtime error to the exact eval whose uniquely named wrapper
-## function appears in the stack — ignoring unrelated game errors and errors
-## from before the eval started. Mutex-guarded.
-func find_script_error_since(since_seq: int, function_name: String) -> String:
- _mutex.lock()
- var found := ""
- for i in range(_recent_script_errors.size() - 1, -1, -1):
- var rec: Dictionary = _recent_script_errors[i]
- if int(rec["seq"]) <= since_seq:
- break
- if (rec["funcs"] as PackedStringArray).has(function_name):
- found = rec["text"]
- break
- _mutex.unlock()
- return found
diff --git a/addons/godot_ai/runtime/game_logger.gd.uid b/addons/godot_ai/runtime/game_logger.gd.uid
deleted file mode 100644
index d4e263e..0000000
--- a/addons/godot_ai/runtime/game_logger.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dwcs6d1y7vhqi
diff --git a/addons/godot_ai/runtime/validation_logger.gd b/addons/godot_ai/runtime/validation_logger.gd
deleted file mode 100644
index 672b11f..0000000
--- a/addons/godot_ai/runtime/validation_logger.gd
+++ /dev/null
@@ -1,43 +0,0 @@
-@tool
-extends Logger
-
-## Short-lived Logger used only for per-write validation loads.
-##
-## Unlike editor_logger.gd this deliberately has no addon feedback-loop filter:
-## the caller attaches it around one ResourceLoader.load() call, reads its
-## private buffer, and immediately removes it. The shared editor logger should
-## still drop these validation-load errors so logs_read(source="editor") stays
-## clean.
-
-const _LogBacktrace := preload("res://addons/godot_ai/utils/log_backtrace.gd")
-
-var _buffer
-
-
-func _init(buffer = null) -> void:
- _buffer = buffer
-
-
-func _log_error(
- function: String,
- file: String,
- line: int,
- code: String,
- rationale: String,
- _editor_notify: bool,
- error_type: int,
- script_backtraces: Array,
-) -> void:
- if _buffer == null:
- return
- var resolved := _LogBacktrace.resolve_error(
- function,
- file,
- line,
- code,
- rationale,
- error_type,
- script_backtraces,
- )
- var details: Dictionary = resolved.get("details", {})
- _buffer.append(resolved.level, resolved.message, resolved.path, resolved.line, resolved.function, details)
diff --git a/addons/godot_ai/runtime/validation_logger.gd.uid b/addons/godot_ai/runtime/validation_logger.gd.uid
deleted file mode 100644
index 4e693b4..0000000
--- a/addons/godot_ai/runtime/validation_logger.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b2ff3aot6t2l4
diff --git a/addons/godot_ai/telemetry.gd b/addons/godot_ai/telemetry.gd
deleted file mode 100644
index 0dfe414..0000000
--- a/addons/godot_ai/telemetry.gd
+++ /dev/null
@@ -1,224 +0,0 @@
-## Plugin-side telemetry helper.
-##
-## Relays plugin-only events (dock startup, self-update outcome, plugin
-## reload, dev-server toggle) to the Python MCP server via the existing
-## `send_event("plugin_event", {...})` channel. The server's
-## `transport/websocket.py` allowlists event names and forwards into the
-## central telemetry pipeline — meaning opt-out, endpoint, customer_uuid
-## and the bounded-queue worker stay in one place (Python), not
-## duplicated in GDScript.
-##
-## Opt-out options priority:
-## 1. `GODOT_AI_DISABLE_TELEMETRY` / `DISABLE_TELEMETRY` env vars —
-## checked first so CI / operators can force-disable without touching
-## EditorSettings.
-## 2. The `godot_ai/telemetry_enabled` EditorSetting — set through the
-## MCP dock and persisted between sessions.
-##
-## When telemetry is disabled, events are never buffered or sent. Only a
-## *truthy* env var force-disables; a falsey or absent env var falls through
-## to the EditorSetting (which defaults to enabled). See McpSettings.telemetry_enabled.
-##
-## Buffering: events recorded before the WebSocket is connected go into
-## a small bounded buffer and flush on the next `record_event` call once
-## connected. The buffer is intentionally small (`_MAX_BUFFER`); plugin
-## events are sparse, and a flood means something is misconfigured.
-
-extends RefCounted
-
-## Allowlist mirrored on the Python side in
-## `src/godot_ai/transport/websocket.py::_PLUGIN_EVENT_NAMES`. Update
-## both together.
-const _ALLOWED_EVENTS := [
- "dock_startup",
- "plugin_reload",
- "self_update",
- "dev_server_toggle",
-]
-
-## Runtime opt-out sent over the authenticated WebSocket (#913). Twinned
-## with `transport/websocket.py::TELEMETRY_OPT_OUT_EVENT`. Outside
-## `_ALLOWED_EVENTS` on purpose: that list gates events the server records,
-## and this one tells it to stop.
-const OPT_OUT_EVENT := "telemetry_opt_out"
-
-const _MAX_BUFFER := 32
-
-## EditorSetting key used to defer a ``plugin_reload`` event across the
-## disable -> enable boundary. Callers that trigger plugin reload (the
-## dock reload button, ``editor_reload_plugin`` MCP-tool path) write
-## here *before* the disable kills the live WebSocket; the new
-## plugin's ``_enter_tree`` flushes via ``flush_pending_plugin_reload``.
-const PENDING_PLUGIN_RELOAD_KEY := "godot_ai/pending_plugin_reload_event"
-
-
-## Persist a ``plugin_reload`` event so the re-enabled plugin instance
-## can emit it once its new WebSocket is up. Static so callers without
-## a telemetry instance handle (e.g. ``editor_handler.reload_plugin``)
-## can use it via the preloaded const alias.
-static func record_pending_plugin_reload(source: String) -> void:
- var settings := EditorInterface.get_editor_settings()
- if settings == null:
- return
- settings.set_setting(
- PENDING_PLUGIN_RELOAD_KEY,
- JSON.stringify({"source": source, "success": true}),
- )
-
-
-## Read + clear an EditorSetting JSON-encoded event payload. Returns
-## the parsed dict, or ``null`` if the key is absent / empty /
-## malformed. Used by ``flush_pending_plugin_reload`` (below) and by
-## ``plugin.gd::_flush_pending_self_update_telemetry``. Centralising
-## the read-and-clear dance keeps both flush sites symmetric with the
-## ``record_pending_*`` writers and prevents the "key gets stuck"
-## class of bug if a future flush helper forgets the clear step.
-static func _drain_editor_setting_dict(key: String):
- var settings := EditorInterface.get_editor_settings()
- if settings == null:
- return null
- if not settings.has_setting(key):
- return null
- var raw := str(settings.get_setting(key))
- settings.set_setting(key, "")
- if raw == "":
- return null
- var parsed = JSON.parse_string(raw)
- if typeof(parsed) != TYPE_DICTIONARY:
- return null
- return parsed
-
-var _connection
-var _disabled: bool = false
-var _pending: Array = [] # of {name: String, data: Dictionary}
-
-func _init(connection) -> void:
- _connection = connection
- _disabled = not McpSettings.telemetry_enabled()
- ## Subscribe to ``connection_state_changed`` so events buffered before
- ## the WebSocket handshake (e.g. ``record_dock_startup`` from
- ## ``plugin._enter_tree``) actually leave the editor. Without this,
- ## the buffer only drained on the next ``record_event`` call — when
- ## that call never came (the common single-session case), the very
- ## events we cared about most sat in the queue forever.
- if _connection != null and _connection.has_signal("connection_state_changed"):
- _connection.connection_state_changed.connect(_on_connection_state_changed)
-
-
-func record_event(name: String, data: Dictionary = {}) -> void:
- if _disabled:
- return
- if not _ALLOWED_EVENTS.has(name):
- ## Drop silently — matches the server's behavior for unknown
- ## names, and avoids editor yellow-bar noise from third-party
- ## callers or stale event names mid-rollout.
- return
- if _connection != null and _connection.is_connected:
- _flush()
- _send_one(name, data)
- return
- ## Pre-handshake: stash in a small bounded buffer. Drained on the
- ## first ``connection_state_changed(true)`` after this point (see
- ## ``_on_connection_state_changed``). Falling back to "drain on the
- ## next record_event" is a footgun: the most useful plugin events
- ## (``dock_startup``, pending ``self_update``) fire from
- ## ``plugin._enter_tree`` before the handshake, and a single-session
- ## editor may never emit a second event — so without the signal-
- ## driven flush they sat buffered forever.
- if _pending.size() >= _MAX_BUFFER:
- _pending.pop_front()
- _pending.append({"name": name, "data": data})
-
-
-func _on_connection_state_changed(is_open: bool) -> void:
- if is_open:
- ## Before the flush: a server we adopted must hear our preference
- ## before it records anything else on this session.
- assert_opt_out()
- _flush()
-
-
-## Tell the connected server to stop sending telemetry, if this editor is
-## opted out (#913). Spawn-time env injection only reaches a server the
-## plugin started; this reaches one it adopted, over the same authenticated
-## WebSocket every command uses. The server latches it — off until that
-## process is replaced, never back on.
-##
-## Reads the setting live rather than the `_disabled` snapshot so the dock
-## can call this the moment the user applies a change, before the plugin
-## reload swaps this instance out. Returns whether anything was sent.
-func assert_opt_out() -> bool:
- if McpSettings.telemetry_enabled():
- return false
- if _connection == null or not _connection.is_connected:
- return false
- return bool(_connection.send_event(OPT_OUT_EVENT, {}))
-
-func _flush() -> void:
- if _pending.is_empty():
- return
- var to_send := _pending.duplicate()
- _pending.clear()
- for entry in to_send:
- _send_one(entry["name"], entry["data"])
-
-func _send_one(name: String, data: Dictionary) -> void:
- if _connection == null:
- return
- _connection.send_event("plugin_event", {"name": name, "data": data})
-
-# --- convenience emitters --------------------------------------------------
-
-func record_dock_startup(extra: Dictionary = {}) -> void:
- record_event("dock_startup", extra)
-
-func record_self_update(
- status: String,
- from_version: String = "",
- to_version: String = "",
- error: String = "",
-) -> void:
- var data := {"status": status}
- if from_version != "":
- data["from_version"] = from_version
- if to_version != "":
- data["to_version"] = to_version
- if error != "":
- data["error"] = error.substr(0, 200)
- record_event("self_update", data)
-
-func record_dev_server_toggle(action: String) -> void:
- record_event("dev_server_toggle", {"action": action})
-
-
-## Drain a pending ``plugin_reload`` event written by the previous
-## instance before it disabled itself. Pending events are currently
-## always success=true — ``record_pending_plugin_reload`` above is the
-## only writer and hardcodes it (a reload that fails never reaches the
-## flush anyway; there is no new instance to drain the key). The
-## error/success parsing below stays tolerant for forward compat with
-## a writer that records failures.
-func flush_pending_plugin_reload() -> void:
- var parsed = _drain_editor_setting_dict(PENDING_PLUGIN_RELOAD_KEY)
- if parsed == null:
- return
- var data := {
- "success": bool(parsed.get("success", true)),
- "source": str(parsed.get("source", "unknown")),
- }
- var error := str(parsed.get("error", ""))
- if error != "":
- data["error"] = error.substr(0, 200)
- record_event("plugin_reload", data)
-
-# --- test seam -------------------------------------------------------------
-
-## Inject a fake connection or force the disabled flag for unit tests
-## that don't have a live WebSocket. Production code does not call this.
-func _test_set_state(connection, disabled: bool) -> void:
- _connection = connection
- _disabled = disabled
- _pending.clear()
-
-func _test_pending_count() -> int:
- return _pending.size()
diff --git a/addons/godot_ai/telemetry.gd.uid b/addons/godot_ai/telemetry.gd.uid
deleted file mode 100644
index 6f46be6..0000000
--- a/addons/godot_ai/telemetry.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dlul2gculiy1p
diff --git a/addons/godot_ai/testing/script_error_capture.gd b/addons/godot_ai/testing/script_error_capture.gd
deleted file mode 100644
index 92c53ed..0000000
--- a/addons/godot_ai/testing/script_error_capture.gd
+++ /dev/null
@@ -1,49 +0,0 @@
-@tool
-extends Logger
-
-## Captures GDScript runtime errors emitted while a test is running.
-##
-## Deliberately no class_name: this is an internal test helper.
-##
-## Only ERROR_TYPE_SCRIPT is captured. push_error(), push_warning(), and
-## engine-internal ERR_FAIL_* checks are often valid negative-path assertions and
-## should not abort the test.
-
-var _mutex := Mutex.new()
-var _capturing := false
-var _errors := PackedStringArray()
-
-
-func begin_capture() -> void:
- _mutex.lock()
- _capturing = true
- _errors.clear()
- _mutex.unlock()
-
-
-func end_capture() -> PackedStringArray:
- _mutex.lock()
- var captured := _errors.duplicate()
- _capturing = false
- _errors.clear()
- _mutex.unlock()
- return captured
-
-
-func _log_error(
- function: String,
- file: String,
- line: int,
- code: String,
- rationale: String,
- _editor_notify: bool,
- error_type: int,
- _script_backtraces: Array,
-) -> void:
- if error_type != ERROR_TYPE_SCRIPT:
- return
- _mutex.lock()
- if _capturing:
- var text := rationale if not rationale.is_empty() else code
- _errors.append("%s (%s:%d in %s)" % [text, file, line, function])
- _mutex.unlock()
diff --git a/addons/godot_ai/testing/script_error_capture.gd.uid b/addons/godot_ai/testing/script_error_capture.gd.uid
deleted file mode 100644
index 6b2d712..0000000
--- a/addons/godot_ai/testing/script_error_capture.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://crk8w2nei087v
diff --git a/addons/godot_ai/testing/test_runner.gd b/addons/godot_ai/testing/test_runner.gd
deleted file mode 100644
index 1aa0041..0000000
--- a/addons/godot_ai/testing/test_runner.gd
+++ /dev/null
@@ -1,441 +0,0 @@
-@tool
-class_name McpTestRunner
-extends RefCounted
-
-## Lightweight test runner for MCP plugin tests. Discovers test_* methods
-## on McpTestSuite instances, runs them, and collects structured results.
-
-const ScriptErrorCapture := preload("res://addons/godot_ai/testing/script_error_capture.gd")
-
-var _results: Array[Dictionary] = []
-var _last_run_ms: int = 0
-var _script_error_capture: ScriptErrorCapture = null
-var _capture_registered := false
-
-
-func _notification(what: int) -> void:
- if what == NOTIFICATION_PREDELETE and _capture_registered and _script_error_capture != null:
- OS.remove_logger(_script_error_capture)
- _capture_registered = false
-
-
-func run_suite(suite: McpTestSuite, test_filter: String = "", exclude_test_filter: String = "") -> void:
- var owns_capture := not _capture_registered
- if owns_capture:
- _register_capture()
-
- _run_suite_tests(suite, test_filter, exclude_test_filter, Callable(), 0, {})
-
- if owns_capture:
- _unregister_capture()
-
-
-## Shared per-test loop for both the legacy synchronous path and the
-## serviced driver. Returns "" when the loop completed, or a terminal
-## outcome ("timeout" / "transport_lost" / "paused") when a checkpoint
-## aborted it. Checkpoints run BETWEEN tests — an atomic test body is
-## never preempted (see docs/test-run-transport-starvation-plan.md).
-func _run_suite_tests(
- suite: McpTestSuite,
- test_filter: String,
- exclude_test_filter: String,
- service_cb: Callable,
- deadline_ticks_ms: int,
- run_state: Dictionary,
-) -> String:
- var name := suite.suite_name()
- var methods := _get_test_methods(suite)
- var exclusions := _parse_exclusions(exclude_test_filter)
-
- for method_name in methods:
- if not test_filter.is_empty() and method_name.find(test_filter) == -1:
- continue
- _selected_processed += 1
- if _matches_any_exclusion(method_name, exclusions):
- _results.append({
- "suite": name,
- "test": method_name,
- "passed": true,
- "skipped": true,
- "message": "Excluded by exclude_test_name filter",
- "assertion_count": 0,
- "duration_ms": 0,
- })
- continue
-
- var test_start := Time.get_ticks_msec()
- var entry := _run_one_test(suite, name, method_name)
- entry["duration_ms"] = Time.get_ticks_msec() - test_start
- _results.append(entry)
-
- var stop := _checkpoint(service_cb, deadline_ticks_ms, run_state)
- if not stop.is_empty():
- return stop
- return ""
-
-
-## Execute one test method and return its result entry (not yet appended;
-## the caller stamps `duration_ms`). Extracted from run_suite so the legacy
-## and serviced drivers share one execution core — behavior must stay
-## byte-identical to the pre-refactor loop body.
-func _run_one_test(suite: McpTestSuite, name: String, method_name: String) -> Dictionary:
- suite._reset()
- _begin_script_error_capture()
- suite.setup()
- suite.call(method_name)
- suite.teardown()
- var script_errors := suite._unexpected_script_errors(_end_script_error_capture())
- suite._free_tracked()
-
- ## Issue #19 defence: free any `_McpTest*` nodes the test created, even
- ## nested ones. If the scene gets auto-saved mid-test while one of these
- ## exists, the reference bakes into main.tscn and breaks the next open
- ## with a "missing dependency" error. Runs after every test, not just at
- ## suite boundaries, so a test that fails mid-flow can't leave a trap
- ## for the next test or for scene autosave.
- var scene_root_for_cleanup := _edited_scene_root()
- if scene_root_for_cleanup != null and scene_root_for_cleanup.is_inside_tree():
- _free_mcp_test_nodes_recursive(scene_root_for_cleanup)
-
- if not script_errors.is_empty():
- var abort_message := "Aborted by SCRIPT ERROR: %s" % "; ".join(script_errors)
- if suite._failed:
- abort_message += " (after assertion failure: %s)" % suite._message
- return {
- "suite": name,
- "test": method_name,
- "passed": false,
- "message": abort_message,
- "assertion_count": suite._assertion_count,
- }
-
- ## A failed assertion always wins over a later skip(): a test that
- ## fails and then hits a skip-guard must report the failure, not
- ## park itself as green-skipped.
- if suite._skipped and not suite._failed:
- return {
- "suite": name,
- "test": method_name,
- "passed": true,
- "skipped": true,
- "message": suite._skip_reason,
- "assertion_count": 0,
- }
-
- var passed := not suite._failed
- var msg := suite._message
-
- ## Warn about zero-assertion tests (likely silently skipped logic).
- if passed and suite._assertion_count == 0:
- passed = false
- msg = "Test completed with 0 assertions (likely skipped its logic)"
-
- return {
- "suite": name,
- "test": method_name,
- "passed": passed,
- "message": msg,
- "assertion_count": suite._assertion_count,
- }
-
-
-func run_suites(suites: Array, suite_filter: String = "", test_filter: String = "", ctx: Dictionary = {}, verbose: bool = false, exclude_test_filter: String = "") -> Dictionary:
- ## Legacy synchronous API — unchanged signature and semantics for direct
- ## callers, unit-test fixtures, and any batch context. No servicing, no
- ## ceiling: with an invalid Callable and deadline 0 every checkpoint is a
- ## no-op and the outcome is always "completed".
- var run := run_suites_serviced(suites, suite_filter, test_filter, ctx, verbose, exclude_test_filter)
- return run["results"]
-
-
-## Serviced driver for live MCP runs. Between tests and at suite
-## boundaries it (a) aborts once `deadline_ticks_ms` passes and (b) calls
-## `service_cb` (McpConnection.service_transport_during_exclusive_run) so
-## the WebSocket heartbeat stays alive while the suite monopolizes the
-## main thread. Returns:
-## {
-## "outcome": "completed" | "timeout" | "transport_lost" | "paused",
-## "results": ,
-## "tests_not_run": ,
-## }
-## Every outcome path — including aborts — runs the current suite's
-## teardown + leak cleanup, restores console echo, and unregisters the
-## capture logger. `run_state` is caller-owned and threaded through to the
-## service callback (cumulative packet counter lives there).
-func run_suites_serviced(
- suites: Array,
- suite_filter: String = "",
- test_filter: String = "",
- ctx: Dictionary = {},
- verbose: bool = false,
- exclude_test_filter: String = "",
- service_cb: Callable = Callable(),
- deadline_ticks_ms: int = 0,
- run_state: Dictionary = {},
-) -> Dictionary:
- _results.clear()
- _selected_processed = 0
- var selected_total := _count_selected_tests(suites, suite_filter, test_filter)
- var start := Time.get_ticks_msec()
- var outcome := ""
-
- ## Silence the plugin's ring-buffer console echo while tests run. Negative-
- ## path suites deliberately fill the ring with 500 lines and log malformed-
- ## result errors; echoing all of that buries an all-green run in scary
- ## console output. The ring contents tests assert on are untouched, and
- ## the flag is restored after the run so live logging resumes.
- var _prev_console_echo := McpLogBuffer.console_echo
- McpLogBuffer.console_echo = false
-
- ## If a prior run was interrupted after registering the logger but before
- ## normal teardown, remove that stale registration before starting fresh.
- _unregister_capture()
- _register_capture()
-
- for suite: McpTestSuite in suites:
- if not suite_filter.is_empty() and suite.suite_name() != suite_filter:
- continue
-
- ## Snapshot scene children before the suite so we can clean up leaks.
- var scene_root := _edited_scene_root()
- var before_children: Array[Node] = []
- if scene_root != null:
- before_children = _get_children_snapshot(scene_root)
-
- suite._reset_suite_state()
- suite.suite_setup(ctx.duplicate(true))
-
- ## fail_setup() / skip_suite() gives suites a clean way to bail out of
- ## suite_setup without leaving N tests to fail with "0 assertions". We
- ## emit ONE suite-level result and skip individual tests entirely.
- if suite._suite_failed:
- _results.append({
- "suite": suite.suite_name(),
- "test": "",
- "passed": false,
- "message": "suite_setup() failed: %s (subsequent tests not run)" % suite._suite_failed_message,
- "assertion_count": 0,
- })
- ## The bailed suite's tests are accounted for, not "not run".
- _selected_processed += _count_selected_tests([suite], "", test_filter)
- elif suite._suite_skipped:
- _results.append({
- "suite": suite.suite_name(),
- "test": "",
- "passed": true,
- "skipped": true,
- "message": "suite_setup() skipped: %s" % suite._suite_skipped_reason,
- "assertion_count": 0,
- })
- _selected_processed += _count_selected_tests([suite], "", test_filter)
- else:
- outcome = _checkpoint(service_cb, deadline_ticks_ms, run_state)
- if outcome.is_empty():
- outcome = _run_suite_tests(
- suite, test_filter, exclude_test_filter,
- service_cb, deadline_ticks_ms, run_state
- )
- ## Suite epilogue runs on EVERY path, including aborts: the suite has
- ## begun, so its teardown and leak cleanup must not be skipped.
- suite.suite_teardown()
- suite._free_tracked()
-
- ## Remove any nodes the suite left behind (failed undo, missing cleanup).
- if scene_root != null and scene_root.is_inside_tree():
- _cleanup_leaked_nodes(scene_root, before_children)
-
- if not outcome.is_empty():
- break
-
- outcome = _checkpoint(service_cb, deadline_ticks_ms, run_state)
- if not outcome.is_empty():
- break
-
- _last_run_ms = Time.get_ticks_msec() - start
- McpLogBuffer.console_echo = _prev_console_echo
- _unregister_capture()
- if outcome.is_empty():
- outcome = "completed"
- return {
- "outcome": outcome,
- "results": get_results(verbose),
- "tests_not_run": maxi(0, selected_total - _selected_processed),
- }
-
-
-## Count of selected (filter-surviving) tests already looped over this run,
-## including exclusion-skips and bailed-suite accounting. Drives the
-## `tests_not_run` estimate on aborted runs.
-var _selected_processed := 0
-
-
-func _count_selected_tests(suites: Array, suite_filter: String, test_filter: String) -> int:
- var count := 0
- for suite: McpTestSuite in suites:
- if not suite_filter.is_empty() and suite.suite_name() != suite_filter:
- continue
- for method_name in _get_test_methods(suite):
- if not test_filter.is_empty() and method_name.find(test_filter) == -1:
- continue
- count += 1
- return count
-
-
-## Between-phase checkpoint: "" to continue, or a terminal outcome.
-## Delegates to the shared mapping so the discovery checkpoints in
-## test_handler.gd can never drift from the between-test ones (plan §9 Q2).
-func _checkpoint(service_cb: Callable, deadline_ticks_ms: int, run_state: Dictionary) -> String:
- return McpConnection.exclusive_run_checkpoint(service_cb, deadline_ticks_ms, run_state)
-
-
-func _register_capture() -> void:
- if _capture_registered:
- return
- if _script_error_capture == null:
- _script_error_capture = ScriptErrorCapture.new()
- if _script_error_capture == null:
- return
- OS.add_logger(_script_error_capture)
- _capture_registered = true
-
-
-func _unregister_capture() -> void:
- if not _capture_registered:
- return
- if _script_error_capture == null:
- _capture_registered = false
- return
- OS.remove_logger(_script_error_capture)
- _capture_registered = false
-
-
-func _begin_script_error_capture() -> void:
- if _script_error_capture != null and _capture_registered:
- _script_error_capture.begin_capture()
-
-
-func _end_script_error_capture() -> PackedStringArray:
- if _script_error_capture == null or not _capture_registered:
- return PackedStringArray()
- return _script_error_capture.end_capture()
-
-
-static func _edited_scene_root() -> Node:
- if not Engine.is_editor_hint():
- return null
- return EditorInterface.get_edited_scene_root()
-
-
-func get_results(verbose: bool = false) -> Dictionary:
- var passed := 0
- var failed := 0
- var skipped := 0
- var failures: Array[Dictionary] = []
- var suites_seen := {}
- for r in _results:
- suites_seen[r.suite] = true
- if r.get("skipped", false):
- skipped += 1
- elif r.passed:
- passed += 1
- else:
- failed += 1
- failures.append(r)
-
- var result := {
- "passed": passed,
- "failed": failed,
- "skipped": skipped,
- "total": _results.size(),
- "duration_ms": _last_run_ms,
- "suites_run": suites_seen.keys(),
- "suite_count": suites_seen.size(),
- }
-
- if not failures.is_empty():
- result["failures"] = failures
-
- if verbose:
- result["results"] = _results
-
- return result
-
-
-func clear() -> void:
- _results.clear()
- _last_run_ms = 0
-
-
-func _get_test_methods(obj: Object) -> Array[String]:
- var methods: Array[String] = []
- for m in obj.get_method_list():
- var name: String = m.get("name", "")
- if name.begins_with("test_"):
- methods.append(name)
- methods.sort()
- return methods
-
-
-func _get_children_snapshot(node: Node) -> Array[Node]:
- var children: Array[Node] = []
- for child in node.get_children():
- children.append(child)
- return children
-
-
-## Remove any nodes in scene_root that weren't present before the suite ran,
-## plus any _McpTest* named nodes anywhere in the tree (catches nested leaks).
-## NOTE: this bypasses EditorUndoRedoManager by design — the test runner
-## owns these leaks and needs to clear them unconditionally. Don't Ctrl-Z in
-## the editor immediately after a test run that triggered cleanup; the undo
-## stack may reference freed nodes.
-func _cleanup_leaked_nodes(scene_root: Node, before: Array[Node]) -> void:
- var before_set := {}
- for n in before:
- before_set[n] = true
- for child in scene_root.get_children():
- if not before_set.has(child):
- scene_root.remove_child(child)
- child.queue_free()
-
-
-## Recursively free every node whose name starts with `_McpTest`, anywhere in
-## the scene. Intentionally bypasses undo — these are test leaks, not user
-## work. Walk breadth-first so we can collect victims before mutating the tree.
-func _free_mcp_test_nodes_recursive(root: Node) -> void:
- var victims: Array[Node] = []
- var queue: Array[Node] = [root]
- while not queue.is_empty():
- var node: Node = queue.pop_back()
- for child in node.get_children():
- if str(child.name).begins_with("_McpTest"):
- victims.append(child)
- else:
- queue.append(child)
- for v in victims:
- if v.get_parent() != null:
- v.get_parent().remove_child(v)
- v.queue_free()
-
-
-## Split the `exclude_test_name` filter into individual substring matchers.
-## Comma-separated so the CI smoke harness can list multiple flaky tests
-## without shipping a richer schema (single names still work — same string,
-## no comma, same one-element list). Whitespace around each name is stripped
-## so `"a, b"` and `"a,b"` behave identically.
-static func _parse_exclusions(filter: String) -> Array[String]:
- var out: Array[String] = []
- if filter.is_empty():
- return out
- for part in filter.split(","):
- var trimmed := part.strip_edges()
- if not trimmed.is_empty():
- out.append(trimmed)
- return out
-
-
-static func _matches_any_exclusion(method_name: String, exclusions: Array[String]) -> bool:
- for ex in exclusions:
- if method_name.find(ex) != -1:
- return true
- return false
diff --git a/addons/godot_ai/testing/test_runner.gd.uid b/addons/godot_ai/testing/test_runner.gd.uid
deleted file mode 100644
index c0befb0..0000000
--- a/addons/godot_ai/testing/test_runner.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://367b77qh5grt
diff --git a/addons/godot_ai/testing/test_suite.gd b/addons/godot_ai/testing/test_suite.gd
deleted file mode 100644
index e5e7adb..0000000
--- a/addons/godot_ai/testing/test_suite.gd
+++ /dev/null
@@ -1,302 +0,0 @@
-@tool
-class_name McpTestSuite
-extends RefCounted
-
-## Base class for MCP test suites. Provides assertion methods and
-## lifecycle hooks. Subclass this, add test_* methods, and drop the
-## script in res://tests/.
-
-## Override to return a short name for this suite (e.g. "scene", "node").
-func suite_name() -> String:
- return "unnamed"
-
-
-## Called once before the suite runs. Override to create handlers.
-func suite_setup(_ctx: Dictionary) -> void:
- pass
-
-
-## Called before each test method.
-func setup() -> void:
- pass
-
-
-## Called after each test method.
-func teardown() -> void:
- pass
-
-
-## Called once after the suite finishes.
-func suite_teardown() -> void:
- pass
-
-
-# ----- tracked allocations (freed by the runner after each test) -----
-
-var _tracked_objects: Array[Object] = []
-
-
-## Register a manually-managed Object (plain Object or out-of-tree Node) so
-## the runner frees it after the current test. RefCounted instances are
-## accepted but ignored because they manage their own lifetime.
-func track(obj: Object) -> Object:
- if obj != null and not obj is RefCounted:
- _tracked_objects.append(obj)
- return obj
-
-
-## Free everything registered via track(). Called by the runner after each
-## test's teardown() and again after suite_teardown().
-func _free_tracked() -> void:
- for obj in _tracked_objects:
- if not is_instance_valid(obj) or (obj is Node and obj.is_queued_for_deletion()):
- continue
- if obj is Node:
- var parent := (obj as Node).get_parent()
- if parent != null:
- parent.remove_child(obj)
- obj.free()
- _tracked_objects.clear()
-
-
-# ----- assertion state (managed by McpTestRunner) -----
-
-var _failed: bool = false
-var _message: String = ""
-var _assertion_count: int = 0
-var _skipped: bool = false
-var _skip_reason: String = ""
-var _expected_script_error_substrings: Array[String] = []
-
-# ----- suite-level state (managed by McpTestRunner) -----
-
-var _suite_failed: bool = false
-var _suite_failed_message: String = ""
-var _suite_skipped: bool = false
-var _suite_skipped_reason: String = ""
-
-
-func _reset() -> void:
- _failed = false
- _message = ""
- _assertion_count = 0
- _skipped = false
- _skip_reason = ""
- _expected_script_error_substrings.clear()
-
-
-func _reset_suite_state() -> void:
- _suite_failed = false
- _suite_failed_message = ""
- _suite_skipped = false
- _suite_skipped_reason = ""
-
-
-## Mark the current test as skipped. Use when a precondition isn't met
-## (e.g. no scene open, no Node3D in scene) and the test can't run.
-## Skipped tests count separately from passed/failed.
-func skip(reason: String = "") -> void:
- _skipped = true
- _skip_reason = reason
-
-
-## Bail out of suite_setup() with a failure. Subsequent tests in this suite
-## are not run; the runner reports a single suite-level failure with the
-## given reason instead of N zero-assertion noise lines per test.
-##
-## Example:
-## func suite_setup(ctx):
-## var arena = preload("res://game/arena.gd").new()
-## if arena == null:
-## fail_setup("arena.gd failed to instantiate in @tool scope")
-## return
-func fail_setup(reason: String) -> void:
- _suite_failed = true
- _suite_failed_message = reason
-
-
-## Bail out of suite_setup() because a precondition isn't met (no scene open,
-## no game running, etc.). Subsequent tests are not run and the runner emits
-## a single suite-level skip rather than per-test skip noise.
-func skip_suite(reason: String) -> void:
- _suite_skipped = true
- _suite_skipped_reason = reason
-
-
-## Allow one captured SCRIPT ERROR whose text contains `substring`.
-## Use only for negative-path tests that intentionally compile or execute
-## invalid GDScript and assert on the resulting diagnostics.
-func expect_script_error_containing(substring: String) -> void:
- _expected_script_error_substrings.append(substring)
-
-
-func _unexpected_script_errors(captured: PackedStringArray) -> PackedStringArray:
- var unexpected := PackedStringArray()
- var remaining := _expected_script_error_substrings.duplicate()
- for error in captured:
- var matched_index := -1
- for i in range(remaining.size()):
- if error.find(remaining[i]) != -1:
- matched_index = i
- break
- if matched_index == -1:
- unexpected.append(error)
- else:
- remaining.remove_at(matched_index)
- return unexpected
-
-
-## Trigger an undo against whichever history (scene or global) holds the most
-## recent action. `EditorUndoRedoManager` in Godot 4.x doesn't expose `.undo()`
-## directly — you resolve the history's underlying UndoRedo and call it there.
-## Actions registered via `add_do_method(self, …)` with a non-scene target land
-## in GLOBAL_HISTORY, while actions on scene nodes land in the scene's history,
-## so we try both (matches the pattern in batch_handler.gd).
-func editor_undo(undo_redo: EditorUndoRedoManager) -> bool:
- for ur in _collect_histories(undo_redo):
- if ur.undo():
- return true
- return false
-
-
-## Mirror of `editor_undo` for redo.
-func editor_redo(undo_redo: EditorUndoRedoManager) -> bool:
- for ur in _collect_histories(undo_redo):
- if ur.redo():
- return true
- return false
-
-
-func _collect_histories(undo_redo: EditorUndoRedoManager) -> Array:
- var out: Array = []
- if undo_redo == null:
- return out
- var scene_root := EditorInterface.get_edited_scene_root()
- if scene_root != null:
- var scene_id := undo_redo.get_object_history_id(scene_root)
- var scene_ur := undo_redo.get_history_undo_redo(scene_id)
- if scene_ur != null:
- out.append(scene_ur)
- var global_ur := undo_redo.get_history_undo_redo(EditorUndoRedoManager.GLOBAL_HISTORY)
- if global_ur != null and not global_ur in out:
- out.append(global_ur)
- return out
-
-
-# ----- assertions -----
-
-func assert_true(condition: bool, msg: String = "") -> void:
- _assertion_count += 1
- if _failed:
- return
- if not condition:
- _failed = true
- _message = msg if msg else "Expected true"
-
-
-func assert_false(condition: bool, msg: String = "") -> void:
- _assertion_count += 1
- if _failed:
- return
- if condition:
- _failed = true
- _message = msg if msg else "Expected false"
-
-
-func assert_eq(actual: Variant, expected: Variant, msg: String = "") -> void:
- _assertion_count += 1
- if _failed:
- return
- if actual != expected:
- _failed = true
- _message = msg if msg else "Expected %s, got %s" % [str(expected), str(actual)]
-
-
-func assert_ne(actual: Variant, not_expected: Variant, msg: String = "") -> void:
- _assertion_count += 1
- if _failed:
- return
- if actual == not_expected:
- _failed = true
- _message = msg if msg else "Expected value != %s" % str(not_expected)
-
-
-func assert_gt(actual: Variant, threshold: Variant, msg: String = "") -> void:
- _assertion_count += 1
- if _failed:
- return
- if not (actual > threshold):
- _failed = true
- _message = msg if msg else "Expected %s > %s" % [str(actual), str(threshold)]
-
-
-func assert_has_key(dict: Variant, key: String, msg: String = "") -> void:
- _assertion_count += 1
- if _failed:
- return
- if not dict is Dictionary:
- _failed = true
- _message = msg if msg else "Expected Dictionary, got %s" % type_string(typeof(dict))
- return
- if not dict.has(key):
- _failed = true
- _message = msg if msg else "Missing key: %s (keys: %s)" % [key, str(dict.keys())]
-
-
-func assert_contains(haystack: Variant, needle: Variant, msg: String = "") -> void:
- _assertion_count += 1
- if _failed:
- return
- if haystack is String:
- if haystack.find(str(needle)) == -1:
- _failed = true
- _message = msg if msg else "'%s' not found in '%s'" % [str(needle), haystack]
- elif haystack is Array:
- if not haystack.has(needle):
- _failed = true
- _message = msg if msg else "%s not found in array" % str(needle)
- else:
- _failed = true
- _message = msg if msg else "assert_contains requires String or Array"
-
-
-func assert_is_error(result: Dictionary, expected_code: String = "", msg: String = "") -> void:
- _assertion_count += 1
- if _failed:
- return
- if not result.has("error"):
- _failed = true
- _message = msg if msg else "Expected error response, got: %s" % str(result.keys())
- return
- if expected_code and result.error.get("code", "") != expected_code:
- _failed = true
- _message = msg if msg else "Expected error code %s, got %s" % [expected_code, result.error.get("code", "")]
-
-
-# ----- scene helpers (shared across suites that create/remove Controls) -----
-
-## Add a Control under the scene root. Creates a Panel if ctl is null.
-## Returns the scene path, or "" when no scene is open — in which case a
-## caller-supplied ctl is freed to prevent leaks.
-func _add_control(ctl_name: String, ctl: Control = null) -> String:
- var scene_root := EditorInterface.get_edited_scene_root()
- if scene_root == null:
- if ctl != null:
- ctl.queue_free()
- return ""
- if ctl == null:
- ctl = Panel.new()
- ctl.name = ctl_name
- scene_root.add_child(ctl)
- ctl.owner = scene_root
- return "/" + scene_root.name + "/" + ctl_name
-
-
-func _remove_control(path: String) -> void:
- var scene_root := EditorInterface.get_edited_scene_root()
- if scene_root == null:
- return
- var node := McpScenePath.resolve(path, scene_root)
- if node != null:
- node.get_parent().remove_child(node)
- node.queue_free()
diff --git a/addons/godot_ai/testing/test_suite.gd.uid b/addons/godot_ai/testing/test_suite.gd.uid
deleted file mode 100644
index b75e726..0000000
--- a/addons/godot_ai/testing/test_suite.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dlrq2s7jhp71s
diff --git a/addons/godot_ai/tool_catalog.gd b/addons/godot_ai/tool_catalog.gd
deleted file mode 100644
index d1e8b4a..0000000
--- a/addons/godot_ai/tool_catalog.gd
+++ /dev/null
@@ -1,110 +0,0 @@
-@tool
-class_name McpToolCatalog
-extends RefCounted
-
-## Mirror of src/godot_ai/tools/domains.py — drives the dock's Tools tab
-## so the UI can render checkboxes, tool counts, and tooltips without
-## round-tripping to a running server.
-##
-## DO NOT EDIT by hand. tests/unit/test_tool_domains.py verifies this file
-## against actual tool registration and fails CI when they drift; the
-## failure message prints the up-to-date catalog body for paste-over.
-##
-## The four core tools are always registered and cannot be excluded — they
-## render as a single grayed-out "Core" row in the UI. Each non-core domain
-## now exposes one or two named verbs plus a single rolled-up
-## `_manage` tool.
-
-const CORE_TOOLS := [
- "editor_state",
- "node_get_properties",
- "scene_get_hierarchy",
- "session_activate",
-]
-
-## Non-core tools that live in a NON-excludable domain (only `session`
-## today), so they appear in no DOMAINS row yet are always registered.
-## Counted alongside CORE_TOOLS so the dock's totals match the real
-## server surface.
-const ALWAYS_ON_TOOLS := [
- "session_manage",
-]
-
-## Ordered list of user-toggleable domains. Each entry:
-## id: matches the name passed to `--exclude-domains`
-## label: human-friendly display (same as id for now, kept separate so
-## a future renaming doesn't break the setting)
-## count: number of NON-CORE tools in this domain
-## tools: flat list of tool names registered by this domain (non-core only)
-const DOMAINS := [
- {"id": "animation", "label": "animation", "count": 2, "tools": ["animation_create", "animation_manage"]},
- {"id": "api", "label": "api", "count": 1, "tools": ["api_manage"]},
- {"id": "audio", "label": "audio", "count": 1, "tools": ["audio_manage"]},
- {"id": "autoload", "label": "autoload", "count": 1, "tools": ["autoload_manage"]},
- {"id": "batch", "label": "batch", "count": 1, "tools": ["batch_execute"]},
- {"id": "camera", "label": "camera", "count": 1, "tools": ["camera_manage"]},
- {"id": "client", "label": "client", "count": 1, "tools": ["client_manage"]},
- {"id": "custom", "label": "custom", "count": 1, "tools": ["custom_manage"]},
- {"id": "editor", "label": "editor", "count": 4, "tools": ["editor_manage", "editor_reload_plugin", "editor_screenshot", "logs_read"]},
- {"id": "filesystem", "label": "filesystem", "count": 1, "tools": ["filesystem_manage"]},
- {"id": "game", "label": "game", "count": 1, "tools": ["game_manage"]},
- {"id": "input_map", "label": "input_map", "count": 1, "tools": ["input_map_manage"]},
- {"id": "material", "label": "material", "count": 1, "tools": ["material_manage"]},
- {"id": "node", "label": "node", "count": 4, "tools": ["node_create", "node_find", "node_manage", "node_set_property"]},
- {"id": "particle", "label": "particle", "count": 1, "tools": ["particle_manage"]},
- {"id": "project", "label": "project", "count": 2, "tools": ["project_manage", "project_run"]},
- {"id": "resource", "label": "resource", "count": 1, "tools": ["resource_manage"]},
- {"id": "scene", "label": "scene", "count": 3, "tools": ["scene_manage", "scene_open", "scene_save"]},
- {"id": "script", "label": "script", "count": 4, "tools": ["script_attach", "script_create", "script_manage", "script_patch"]},
- {"id": "signal", "label": "signal", "count": 1, "tools": ["signal_manage"]},
- {"id": "testing", "label": "testing", "count": 2, "tools": ["test_manage", "test_run"]},
- {"id": "theme", "label": "theme", "count": 1, "tools": ["theme_manage"]},
- {"id": "tilemap", "label": "tilemap", "count": 1, "tools": ["tilemap_manage"]},
- {"id": "tileset", "label": "tileset", "count": 1, "tools": ["tileset_manage"]},
- {"id": "gridmap", "label": "gridmap", "count": 1, "tools": ["gridmap_manage"]},
- {"id": "csg", "label": "csg", "count": 1, "tools": ["csg_manage"]},
- {"id": "ui", "label": "ui", "count": 1, "tools": ["ui_manage"]},
-]
-
-
-## Whether `id` is a real, excludable domain in this plugin version. Used to
-## drop stale names (e.g. a domain removed since the setting was written) so
-## they never reach the server's `--exclude-domains`, whose `parse_exclude_list`
-## hard-fails on unknown names.
-static func is_excludable_domain(id: String) -> bool:
- for d in DOMAINS:
- if d["id"] == id:
- return true
- return false
-
-
-## Total tool count when no domains are excluded. Used for the "Enabled: N / M"
-## readout in the Tools tab without looping the catalog on every repaint.
-static func total_tool_count() -> int:
- var n := CORE_TOOLS.size() + ALWAYS_ON_TOOLS.size()
- for d in DOMAINS:
- n += int(d["count"])
- return n
-
-
-## Tool count remaining after excluding the given set of domain ids.
-static func enabled_tool_count(excluded: PackedStringArray) -> int:
- var n := CORE_TOOLS.size() + ALWAYS_ON_TOOLS.size()
- for d in DOMAINS:
- if excluded.find(d["id"]) == -1:
- n += int(d["count"])
- return n
-
-
-## Canonical comma-separated string for a set of domain ids — sorted and
-## deduplicated so two equivalent settings (entered in different orders)
-## hash to the same EditorSetting value. Matches `excluded_domains()` in
-## client_configurator.gd.
-static func canonical(excluded: PackedStringArray) -> String:
- var seen := PackedStringArray()
- for e in excluded:
- var t := e.strip_edges()
- if not t.is_empty() and seen.find(t) == -1:
- seen.append(t)
- seen.sort()
- return ",".join(seen)
diff --git a/addons/godot_ai/tool_catalog.gd.uid b/addons/godot_ai/tool_catalog.gd.uid
deleted file mode 100644
index c0c179b..0000000
--- a/addons/godot_ai/tool_catalog.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d1vqyt4uyo378
diff --git a/addons/godot_ai/utils/allow_hosts.gd b/addons/godot_ai/utils/allow_hosts.gd
deleted file mode 100644
index a34c964..0000000
--- a/addons/godot_ai/utils/allow_hosts.gd
+++ /dev/null
@@ -1,170 +0,0 @@
-@tool
-class_name McpAllowHosts
-extends RefCounted
-
-## Client-side helpers for the `--allow-host` LAN opt-in (#507, server core
-## in #421). Pure static functions only — no EditorSettings, no sockets —
-## so the settings-value → launch-args plumbing and the manual-command LAN
-## URL builder are deterministically testable without a live editor.
-##
-## Accepted syntax mirrors the server's `parse_allow_hosts`
-## (src/godot_ai/transport/origin_guard.py): each token is a bare IP
-## (IPv4 or IPv6) or a CIDR, comma-separated. Host bits set on a CIDR are
-## tolerated server-side (`strict=False`), so we only validate the IP part
-## and the prefix length here — anything else fails loudly at server
-## startup, which the dock-side validation exists to pre-empt.
-
-
-## Canonicalize a comma-separated allow-host value: whitespace-stripped,
-## deduplicated, sorted. Returns "" for a value with no usable tokens so
-## callers can skip appending `--allow-host` entirely (keeps spawns
-## compatible with pre-#421 servers — same contract as
-## `ClientConfigurator.excluded_domains()`).
-static func normalize(raw: String) -> String:
- var parts := PackedStringArray()
- for p in raw.split(","):
- var t := p.strip_edges()
- if not t.is_empty() and parts.find(t) == -1:
- parts.append(t)
- parts.sort()
- return ",".join(parts)
-
-
-## Whether a single token is a bare IP or a CIDR the server will accept.
-static func token_is_valid(token: String) -> bool:
- var t := token.strip_edges()
- if t.is_empty():
- return false
- var ip := t
- var prefix := 0
- ## Track slash presence separately from the prefix value: reusing -1 as
- ## the "no slash" sentinel let an explicit negative prefix like
- ## "10.0.0.0/-1" validate (is_valid_int accepts "-1"). CodeRabbit review.
- var has_prefix := false
- var slash := t.find("/")
- if slash != -1:
- ip = t.substr(0, slash)
- var prefix_text := t.substr(slash + 1)
- ## Explicit signs are rejected by the server's parse_allow_hosts
- ## (ipaddress refuses "10.0.0.0/+8"), but is_valid_int accepts
- ## them — keep the mirror honest.
- if prefix_text.is_empty() or prefix_text.begins_with("+") or prefix_text.begins_with("-"):
- return false
- if not prefix_text.is_valid_int():
- return false
- prefix = int(prefix_text)
- has_prefix = true
- if not ip.is_valid_ip_address():
- return false
- var max_prefix := 128 if ip.contains(":") else 32
- return not has_prefix or (prefix >= 0 and prefix <= max_prefix)
-
-
-## Every token in `raw` that fails `token_is_valid` — the dock surfaces
-## these inline so a typo is caught before it aborts the server spawn.
-static func invalid_tokens(raw: String) -> PackedStringArray:
- var bad := PackedStringArray()
- for p in raw.split(","):
- var t := p.strip_edges()
- if t.is_empty():
- continue
- if not token_is_valid(t) and bad.find(t) == -1:
- bad.append(t)
- return bad
-
-
-## True when the allowlist names at least one non-loopback range — i.e.
-## the server is actually reachable off this machine and the manual
-## command should surface a LAN URL.
-static func is_lan_allowlist_active(value: String) -> bool:
- for p in value.split(","):
- var t := p.strip_edges()
- if t.is_empty():
- continue
- var ip := t.substr(0, t.find("/")) if t.contains("/") else t
- if not _is_loopback(ip):
- return true
- return false
-
-
-## Choose the LAN address to show in the manual command from the
-## machine's local addresses (caller passes `IP.get_local_addresses()`
-## so this stays pure). Loopback and link-local addresses are dropped;
-## the first private-range IPv4 wins, then any remaining IPv4, then
-## anything left (IPv6). Returns `{"address": String, "ambiguous": bool}`
-## — `ambiguous` flags multiple viable candidates so the note can tell
-## the user to pick the interface on their trusted network.
-static func pick_lan_address(addresses: PackedStringArray) -> Dictionary:
- var candidates := PackedStringArray()
- for a in addresses:
- var addr := String(a).strip_edges()
- if addr.is_empty() or _is_loopback(addr) or _is_link_local(addr):
- continue
- candidates.append(addr)
- if candidates.is_empty():
- return {"address": "", "ambiguous": false}
- var chosen := ""
- for addr in candidates:
- if _is_private_ipv4(addr):
- chosen = addr
- break
- if chosen.is_empty():
- for addr in candidates:
- if addr.contains("."):
- chosen = addr
- break
- if chosen.is_empty():
- chosen = candidates[0]
- return {"address": chosen, "ambiguous": candidates.size() > 1}
-
-
-## Informational LAN-URL note appended to the manual command when the
-## allowlist is active (#507). Never changes what gets WRITTEN to client
-## configs — loopback stays the write target; this is copy-paste help for
-## pointing a remote agent at the right address.
-static func lan_url_note(allow_hosts_value: String, addresses: PackedStringArray, http_port: int) -> String:
- if not is_lan_allowlist_active(allow_hosts_value):
- return ""
- var pick := pick_lan_address(addresses)
- var addr := String(pick.get("address", ""))
- if addr.is_empty():
- return (
- "LAN access is enabled (--allow-host %s), but no LAN address was detected on this machine."
- % allow_hosts_value
- )
- var host := "[%s]" % addr if addr.contains(":") else addr
- var note := (
- "LAN access is enabled (--allow-host %s). Remote agents on the allowed network can use: http://%s:%d/mcp"
- % [allow_hosts_value, host, http_port]
- )
- if bool(pick.get("ambiguous", false)):
- note += "\n(multiple network interfaces detected — pick the address on the network you allowed)"
- return note
-
-
-static func _is_loopback(addr: String) -> bool:
- var a := addr.to_lower()
- return a.begins_with("127.") or a == "::1" or a == "localhost"
-
-
-static func _is_link_local(addr: String) -> bool:
- var a := addr.to_lower()
- if a.begins_with("169.254."):
- return true
- ## IPv6 link-local is fe80::/10 — the whole fe80-febf first hextet, not
- ## just literal "fe80" (Copilot review on #507's PR: fea0::... etc. must
- ## also be excluded from LAN-URL candidates).
- if a.length() >= 4 and a.begins_with("fe") and a[2] in "89ab":
- return true
- return false
-
-
-static func _is_private_ipv4(addr: String) -> bool:
- if not addr.contains("."):
- return false
- if addr.begins_with("10.") or addr.begins_with("192.168."):
- return true
- if addr.begins_with("172."):
- var second := int(addr.get_slice(".", 1))
- return second >= 16 and second <= 31
- return false
diff --git a/addons/godot_ai/utils/allow_hosts.gd.uid b/addons/godot_ai/utils/allow_hosts.gd.uid
deleted file mode 100644
index 02fecd1..0000000
--- a/addons/godot_ai/utils/allow_hosts.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b7qk3vw2nxr4d
diff --git a/addons/godot_ai/utils/class_introspection.gd b/addons/godot_ai/utils/class_introspection.gd
deleted file mode 100644
index 3076740..0000000
--- a/addons/godot_ai/utils/class_introspection.gd
+++ /dev/null
@@ -1,259 +0,0 @@
-@tool
-extends RefCounted
-
-## Builds stable, JSON-safe metadata for any class registered in ClassDB.
-
-const VariantSerializer := preload("res://addons/godot_ai/utils/variant_serializer.gd")
-
-## Sections returned when the caller does not name any. Deliberately narrow:
-## a bare `get_class` is almost always "what properties does X have", and the
-## full five-section dump for a large class (e.g. Node, Control) costs an agent
-## thousands of tokens it rarely wanted. Callers opt into the rest by name, or
-## request the lot with the "all" keyword (see `_sections`).
-const DEFAULT_SECTIONS: Array[String] = ["properties"]
-## The full documentation-shaped section set (excludes the heavier, separately
-## gated "inheritors"). Expanded from the "all" keyword.
-const ALL_SECTIONS: Array[String] = ["properties", "methods", "signals", "enums", "constants"]
-const KNOWN_SECTIONS: Array[String] = ["properties", "methods", "signals", "enums", "constants", "inheritors"]
-## Tokens a caller may legitimately pass in `sections` — the known sections plus
-## the "all" meta-keyword. Used for error suggestions so a typo like "al" can
-## resolve to "all"; "all" is NOT a section (it expands in `_sections`), so it
-## stays out of KNOWN_SECTIONS which gates validity.
-const SUGGESTABLE_SECTION_TOKENS: Array[String] = [
- "properties", "methods", "signals", "enums", "constants", "inheritors", "all"
-]
-const MAX_DEFAULT_ITEMS := 100
-
-
-static func build(type_name: String, options: Dictionary = {}) -> Dictionary:
- var sections := _sections(options.get("sections", DEFAULT_SECTIONS))
- var include_inherited := bool(options.get("include_inherited", false))
- var include_inheritors := bool(options.get("include_inheritors", false))
- var offset := max(0, int(options.get("offset", 0)))
- var limit := int(options.get("limit", MAX_DEFAULT_ITEMS))
- if limit < 0:
- limit = MAX_DEFAULT_ITEMS
- var can_instantiate := ClassDB.can_instantiate(type_name)
-
- var data := {
- "class_name": type_name,
- "engine_version": Engine.get_version_info().get("string", ""),
- "parent_class": str(ClassDB.get_parent_class(type_name)),
- "inheritance_chain": _inheritance_chain(type_name),
- "can_instantiate": can_instantiate,
- "is_singleton": Engine.has_singleton(type_name),
- "include_inherited": include_inherited,
- "offset": offset,
- "limit": limit,
- }
- if include_inheritors or sections.has("inheritors"):
- _add_paged(data, "inheritor", "inheritors", _inheritors(type_name, false), offset, limit)
- _add_paged(
- data,
- "concrete_inheritor",
- "concrete_inheritors",
- _inheritors(type_name, true),
- offset,
- limit
- )
- if sections.has("properties"):
- _add_paged(data, "property", "properties", _properties(type_name, include_inherited), offset, limit)
- if sections.has("methods"):
- _add_paged(data, "method", "methods", _methods(type_name, include_inherited), offset, limit)
- if sections.has("signals"):
- _add_paged(data, "signal", "signals", _signals(type_name, include_inherited), offset, limit)
- if sections.has("enums"):
- _add_paged(data, "enum", "enums", _enums(type_name, include_inherited), offset, limit)
- if sections.has("constants"):
- _add_paged(
- data,
- "constant",
- "constants",
- _unscoped_constants(type_name, include_inherited),
- offset,
- limit
- )
- return data
-
-
-static func validate_sections(raw_sections: Variant) -> Dictionary:
- var sections := _sections(raw_sections)
- var invalid: Array[String] = []
- for section in sections:
- if not KNOWN_SECTIONS.has(section):
- invalid.append(section)
- return {"sections": sections, "invalid": invalid}
-
-
-static func _inheritance_chain(type_name: String) -> Array[String]:
- var chain: Array[String] = []
- var current := type_name
- while not current.is_empty():
- chain.append(current)
- current = str(ClassDB.get_parent_class(current))
- return chain
-
-
-static func _sections(raw_sections: Variant) -> Array[String]:
- var result: Array[String] = []
- var values: Array = []
- if raw_sections is String:
- values = raw_sections.split(",", false)
- elif raw_sections is Array:
- values = raw_sections
- else:
- values = DEFAULT_SECTIONS
- for raw_section in values:
- var section := str(raw_section).strip_edges().to_lower()
- if section == "all":
- for expanded in ALL_SECTIONS:
- if not result.has(expanded):
- result.append(expanded)
- continue
- if not section.is_empty() and not result.has(section):
- result.append(section)
- if result.is_empty():
- result.assign(DEFAULT_SECTIONS)
- return result
-
-
-static func _add_paged(
- data: Dictionary,
- singular: String,
- key: String,
- items: Array,
- offset: int,
- limit: int
-) -> void:
- var end := items.size() if limit == 0 else min(items.size(), offset + limit)
- var page: Array = []
- if offset < items.size():
- page = items.slice(offset, end)
- data[key] = page
- data["%s_count" % singular] = items.size()
- data["%s_returned_count" % singular] = page.size()
-
-
-static func _inheritors(type_name: String, concrete_only: bool) -> Array[String]:
- var result: Array[String] = []
- for inheritor in ClassDB.get_inheriters_from_class(type_name):
- var inheritor_name := str(inheritor)
- if concrete_only and not ClassDB.can_instantiate(inheritor_name):
- continue
- result.append(inheritor_name)
- result.sort()
- return result
-
-
-static func _properties(type_name: String, include_inherited: bool) -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- for raw_prop in ClassDB.class_get_property_list(type_name, not include_inherited):
- var prop: Dictionary = raw_prop
- var usage := int(prop.get("usage", 0))
- if not (usage & PROPERTY_USAGE_EDITOR):
- continue
- var prop_name := str(prop.get("name", ""))
- result.append({
- "name": prop_name,
- "type": type_string(int(prop.get("type", TYPE_NIL))),
- "class_name": str(prop.get("class_name", "")),
- "hint": int(prop.get("hint", PROPERTY_HINT_NONE)),
- "hint_string": str(prop.get("hint_string", "")),
- "usage": usage,
- "default": VariantSerializer.serialize(
- ClassDB.class_get_property_default_value(type_name, prop_name)
- ),
- })
- result.sort_custom(func(a, b): return a.name < b.name)
- return result
-
-
-static func _methods(type_name: String, include_inherited: bool) -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- for raw_method in ClassDB.class_get_method_list(type_name, not include_inherited):
- var method: Dictionary = raw_method
- var args: Array[Dictionary] = []
- for raw_arg in method.get("args", []):
- args.append(_argument_info(raw_arg))
- var defaults: Array = []
- for value in method.get("default_args", []):
- defaults.append(VariantSerializer.serialize(value))
- result.append({
- "name": str(method.get("name", "")),
- "arguments": args,
- "default_arguments": defaults,
- "return": _argument_info(method.get("return", {})),
- "flags": int(method.get("flags", 0)),
- })
- result.sort_custom(func(a, b): return a.name < b.name)
- return result
-
-
-static func _signals(type_name: String, include_inherited: bool) -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- for raw_signal in ClassDB.class_get_signal_list(type_name, not include_inherited):
- var signal_info: Dictionary = raw_signal
- var args: Array[Dictionary] = []
- for raw_arg in signal_info.get("args", []):
- args.append(_argument_info(raw_arg))
- var defaults: Array = []
- for value in signal_info.get("default_args", []):
- defaults.append(VariantSerializer.serialize(value))
- result.append({
- "name": str(signal_info.get("name", "")),
- "arguments": args,
- "default_arguments": defaults,
- "flags": int(signal_info.get("flags", 0)),
- })
- result.sort_custom(func(a, b): return a.name < b.name)
- return result
-
-
-static func _argument_info(raw_info: Variant) -> Dictionary:
- var info: Dictionary = raw_info if raw_info is Dictionary else {}
- return {
- "name": str(info.get("name", "")),
- "type": type_string(int(info.get("type", TYPE_NIL))),
- "class_name": str(info.get("class_name", "")),
- "hint": int(info.get("hint", PROPERTY_HINT_NONE)),
- "hint_string": str(info.get("hint_string", "")),
- "usage": int(info.get("usage", 0)),
- }
-
-
-static func _enums(type_name: String, include_inherited: bool) -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- var enum_names: Array[String] = []
- for enum_name in ClassDB.class_get_enum_list(type_name, not include_inherited):
- enum_names.append(str(enum_name))
- enum_names.sort()
- for enum_name in enum_names:
- var values: Array[Dictionary] = []
- for constant_name in ClassDB.class_get_enum_constants(type_name, enum_name, not include_inherited):
- values.append({
- "name": str(constant_name),
- "value": ClassDB.class_get_integer_constant(type_name, constant_name),
- })
- values.sort_custom(func(a, b): return a.name < b.name)
- result.append({
- "name": enum_name,
- "is_bitfield": ClassDB.is_class_enum_bitfield(type_name, enum_name, not include_inherited),
- "values": values,
- })
- return result
-
-
-static func _unscoped_constants(type_name: String, include_inherited: bool) -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- for constant_name in ClassDB.class_get_integer_constant_list(type_name, not include_inherited):
- var enum_name := str(
- ClassDB.class_get_integer_constant_enum(type_name, constant_name, not include_inherited)
- )
- if not enum_name.is_empty():
- continue
- result.append({
- "name": str(constant_name),
- "value": ClassDB.class_get_integer_constant(type_name, constant_name),
- })
- result.sort_custom(func(a, b): return a.name < b.name)
- return result
diff --git a/addons/godot_ai/utils/class_introspection.gd.uid b/addons/godot_ai/utils/class_introspection.gd.uid
deleted file mode 100644
index 3eda5ca..0000000
--- a/addons/godot_ai/utils/class_introspection.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://caedbsmsl6fk4
diff --git a/addons/godot_ai/utils/client_job_owner.gd b/addons/godot_ai/utils/client_job_owner.gd
deleted file mode 100644
index 58ef803..0000000
--- a/addons/godot_ai/utils/client_job_owner.gd
+++ /dev/null
@@ -1,966 +0,0 @@
-@tool
-extends Node
-
-## Plugin-lifetime owner for every background client-configuration job.
-##
-## The Dock is replaceable UI. Threads cannot share that lifetime: a timed-out
-## probe or package pre-warm can outlive the panel that requested it, and
-## destroying a live Godot Thread corrupts the GDScript VM. This node therefore
-## owns both worker pools, their cancellation, phases, and one action slot per
-## client until its Thread has been joined. It publishes copied value snapshots
-## and outcomes; it never retains the Dock, plugin, lifecycle, or update manager.
-
-const Client := preload("res://addons/godot_ai/clients/_base.gd")
-const ClientConfigurator := preload("res://addons/godot_ai/client_configurator.gd")
-const ClientRegistry := preload("res://addons/godot_ai/clients/_registry.gd")
-const JsonStrategy := preload("res://addons/godot_ai/clients/_json_strategy.gd")
-const TomlStrategy := preload("res://addons/godot_ai/clients/_toml_strategy.gd")
-const DshStrategy := preload("res://addons/godot_ai/clients/_dsh_strategy.gd")
-const CliStrategy := preload("res://addons/godot_ai/clients/_cli_strategy.gd")
-const RefreshState := preload("res://addons/godot_ai/utils/mcp_client_refresh_state.gd")
-const MutationLock := preload("res://addons/godot_ai/utils/client_mutation_lock.gd")
-
-const STATUS_COOLDOWN_MSEC := 15 * 1000
-const STATUS_TIMEOUT_MSEC := 30 * 1000
-## One scoped CLI configure may run three bounded cleanup commands, one write,
-## discovery, and read-back verification before it reaches the optional package
-## prewarm. Keep the owner watchdog above that real mutation budget.
-const ACTION_TIMEOUT_MSEC := 75 * 1000
-const MCP_ACTION_DEFERRED_GRACE_MSEC := 5 * 1000
-const MAX_MCP_STATUS_WAITERS := 64
-const PHASE_PREWARM := "prewarm"
-const MUTATION_UNPROVEN_META := &"_godot_ai_client_mutation_termination_unproven"
-const POST_UPDATE_UNPROVEN_META := &"_godot_ai_post_update_termination_unproven"
-
-signal snapshot_changed(snapshot: Dictionary)
-signal status_refresh_completed(results: Dictionary)
-signal mcp_status_completed(request_ids: Array[String], payload: Dictionary)
-signal mcp_action_completed(request_id: String, payload: Dictionary)
-signal post_update_repin_completed(result: Dictionary)
-signal action_completed(client_id: String, action: String, result: Dictionary, prewarm: Dictionary)
-signal action_timed_out(client_id: String, action: String, detail: String)
-
-var _accepting_work := false
-var _client_health_blocked := false
-var _client_ids: Array[String] = []
-
-var _refresh_thread: Thread
-var _refresh_state := RefreshState.IDLE
-var _refresh_pending := false
-var _refresh_pending_force := false
-var _refresh_completed_msec := 0
-var _refresh_started_msec := 0
-var _refresh_generation := 0
-## request_id -> first refresh generation allowed to answer it. An MCP request
-## arriving behind a partial Dock refresh waits for the coalesced all-client
-## retry instead of accepting the partial result.
-var _mcp_status_waiters: Dictionary = {}
-
-var _action_threads: Dictionary = {}
-var _action_started_msec: Dictionary = {}
-var _action_names: Dictionary = {}
-var _action_request_ids: Dictionary = {}
-var _action_timeout_reported: Dictionary = {}
-var _action_phase_mutex := Mutex.new()
-var _action_phases: Dictionary = {}
-var _action_cancel_mutex := Mutex.new()
-var _action_cancelled: Dictionary = {}
-## client_id -> action whose CLI termination could not be proven. Mirrored into
-## process-lifetime Engine metadata for immediate reload safety; the CLI
-## strategy's global lock directory is the durable cross-restart authority.
-var _mutation_termination_unproven: Dictionary = {}
-## A successful update enters one pre-activation migration job. It is owned and
-## joined here like every other client worker, while ordinary Dock/MCP work
-## remains closed and the server lifecycle stays dormant.
-var _post_update_thread: Thread
-var _post_update_generation := 0
-var _post_update_cancel_mutex := Mutex.new()
-var _post_update_cancelled := false
-var _post_update_termination_unproven := false
-var _last_snapshot: Dictionary = {}
-
-
-func _init() -> void:
- var mutations: Variant = Engine.get_meta(MUTATION_UNPROVEN_META, {})
- if mutations is Dictionary:
- _mutation_termination_unproven = (mutations as Dictionary).duplicate(true)
- _post_update_termination_unproven = bool(
- Engine.get_meta(POST_UPDATE_UNPROVEN_META, false)
- )
-
-
-func _ready() -> void:
- ## Work can start — and the owner can be activated — before this node is
- ## ready. The composition root adds this child, wires it, and calls
- ## `activate()` inside the plugin's `_enter_tree`; Godot defers a child's
- ## `_ready` until that returns, so this callback runs LAST, after both the
- ## post-update migration thread and the ordinary activation seam.
- ##
- ## Scripts that define `_process` are processing by default, so an inert
- ## owner still has to turn it off here. What it must never do is turn it
- ## off again once work has been admitted: `_process` is the only thing that
- ## joins finished workers, so disabling it after `activate()` strands every
- ## refresh and client action for the rest of the session — the sweep never
- ## leaves RUNNING, Configure never leaves its last published phase.
- set_process(_accepting_work or _has_work_in_flight())
-
-
-func _has_work_in_flight() -> bool:
- return _post_update_thread != null or _refresh_thread != null or not _action_threads.is_empty()
-
-
-## Construction is inert. The composition root calls activate only after its
-## startup barrier has allowed normal client work.
-func activate() -> void:
- if _refresh_state == RefreshState.SHUTTING_DOWN or _post_update_thread != null:
- return
- _accepting_work = true
- set_process(true)
- _publish_snapshot()
- _retry_deferred_refresh()
-
-
-func set_client_health_blocked(blocked: bool) -> void:
- _client_health_blocked = blocked
- if not blocked:
- _retry_deferred_refresh()
-
-
-func snapshot() -> Dictionary:
- var busy: Array[String] = []
- for client_id in _action_threads:
- busy.append(String(client_id))
- busy.sort()
- return {
- "accepting_work": _accepting_work,
- "refresh_state": _refresh_state,
- "refresh_completed": _refresh_completed_msec > 0,
- "post_update_repin_running": _post_update_thread != null,
- "busy_actions": busy,
- "action_names": _action_names.duplicate(true),
- "action_phases": _read_action_phases(),
- }
-
-
-func _publish_snapshot() -> void:
- var value := snapshot()
- if value == _last_snapshot:
- return
- _last_snapshot = value.duplicate(true)
- snapshot_changed.emit(value)
-
-
-func _process(_delta: float) -> void:
- _poll_post_update_repin()
- _poll_refresh()
- _poll_actions()
- _check_refresh_timeout()
- _check_action_timeouts()
- _retry_deferred_refresh()
-
-
-## Stop accepting work, cooperatively cancel action subprocesses, then realize
-## every Thread before scripts can be overwritten. Godot exposes no timed join;
-## deadline_msec is reported honestly rather than pretending the join can be
-## interrupted without risking VM corruption.
-func quiesce(deadline_msec: int = 0) -> Dictionary:
- _accepting_work = false
- _refresh_state = RefreshState.SHUTTING_DOWN
- _publish_snapshot()
- for client_id in _action_threads:
- _set_action_cancelled(String(client_id), true)
- _set_post_update_cancelled(true)
- _refresh_generation += 1
- if _refresh_thread != null:
- _refresh_thread.wait_to_finish()
- _refresh_thread = null
- if _post_update_thread != null:
- _record_unproven_post_update_result(_post_update_thread.wait_to_finish())
- _post_update_thread = null
- for thread in _action_threads.values():
- if thread != null:
- _record_unproven_action_payload((thread as Thread).wait_to_finish())
- _action_threads.clear()
- _action_started_msec.clear()
- _action_names.clear()
- _action_request_ids.clear()
- _action_timeout_reported.clear()
- _action_phase_mutex.lock()
- _action_phases.clear()
- _action_phase_mutex.unlock()
- _action_cancel_mutex.lock()
- _action_cancelled.clear()
- _action_cancel_mutex.unlock()
- _refresh_pending = false
- _refresh_pending_force = false
- _mcp_status_waiters.clear()
- set_process(false)
- _publish_snapshot()
- var termination_unproven := (
- _post_update_termination_unproven
- or not _mutation_termination_unproven.is_empty()
- or MutationLock.is_locked()
- )
- return {
- "ok": not termination_unproven,
- "deadline_exceeded": deadline_msec > 0 and Time.get_ticks_msec() > deadline_msec,
- "termination_unproven": termination_unproven,
- }
-
-
-## Re-open the same owner after a pre-activation update failure. Successful
-## activation tears down the plugin instead, so this is only a bail-out path.
-func resume_after_quiesce() -> void:
- if _refresh_state != RefreshState.SHUTTING_DOWN:
- return
- _refresh_state = RefreshState.IDLE
- _set_post_update_cancelled(false)
- activate()
-
-
-## Start the hard post-update client barrier. The signed transaction outcome
-## supplies both versions; a mismatch with the code actually loaded fails
-## closed before touching client config or starting the server.
-func begin_post_update_repin(
- from_version: String, to_version: String, replace_owned_mismatches: bool = false
-) -> Dictionary:
- ## `from_version` is empty when the closed-editor installer put v4 into a
- ## project that had no add-on: nothing was replaced, and owned client
- ## entries still repin to the installed version on this first start.
- var replaced := from_version.strip_edges()
- var installed := to_version.strip_edges()
- if installed.is_empty():
- return {"ok": false, "error": "Post-update target version is missing."}
- if installed != ClientConfigurator.get_plugin_version():
- return {"ok": false, "error": "Post-update target does not match the loaded plugin."}
- if MutationLock.is_locked():
- return {"ok": false, "error": MutationLock.recovery_message()}
- if _post_update_termination_unproven or not _mutation_termination_unproven.is_empty():
- return {"ok": false, "error": MutationLock.recovery_message()}
- if (
- _accepting_work
- or _post_update_thread != null
- or _refresh_thread != null
- or not _action_threads.is_empty()
- or _refresh_state == RefreshState.SHUTTING_DOWN
- ):
- return {"ok": false, "error": "Client work is not idle for post-update migration."}
- ClientConfigurator.warm_env_snapshot()
- _warm_worker_bytecode()
- var probes: Array[Dictionary] = []
- for client_id in ClientConfigurator.client_ids():
- var probe := ClientConfigurator.client_status_probe_snapshot(client_id)
- ## Manual-only descriptors remain visible in ordinary status UI, but M6
- ## cannot safely parse or rewrite them. In particular Zed owns JSONC and
- ## may contain comments that the strict shared JSON parser must reject.
- ## Keep manual remediation outside the hard startup barrier.
- if not probe.is_empty() and bool(probe.get("automatic_config_edits", true)):
- probes.append(probe)
- var context := ClientConfigurator.capture_launch_context()
- if context.is_empty():
- return {"ok": false, "error": "Post-update launch context is unavailable."}
- _set_post_update_cancelled(false)
- _post_update_generation += 1
- _post_update_thread = Thread.new()
- var start_error := _post_update_thread.start(
- Callable(self, "_run_post_update_repin").bind(
- probes,
- context,
- replaced,
- installed,
- replace_owned_mismatches,
- _post_update_generation,
- )
- )
- if start_error != OK:
- _post_update_thread = null
- return {"ok": false, "error": "Could not start post-update client migration."}
- set_process(true)
- _publish_snapshot()
- return {"ok": true}
-
-
-func _run_post_update_repin(
- probes: Array[Dictionary],
- context: Dictionary,
- from_version: String,
- to_version: String,
- replace_owned_mismatches: bool,
- generation: int,
-) -> Dictionary:
- var prewarm := ClientConfigurator.prewarm_server_package_blocking(
- to_version,
- ClientConfigurator.PREWARM_TIMEOUT_MS,
- Callable(self, "_is_post_update_cancelled"),
- )
- var configured_ids: Array[String] = []
- var repinned_ids: Array[String] = []
- var foreign_ids: Array[String] = []
- var deferred: Array[Dictionary] = []
- if bool(prewarm.get("termination_failed", false)):
- return _post_update_result(
- false,
- generation,
- configured_ids,
- repinned_ids,
- "Post-update package pre-warm could not be proven stopped; stop relevant uvx processes or reboot before retrying.",
- prewarm,
- true,
- )
- var server_url := ClientConfigurator.server_url_from(context)
- var resolved_launch := ClientConfigurator.resolve_attach_launch(context)
- for probe in probes:
- if _is_post_update_cancelled():
- return _post_update_result(false, generation, configured_ids, repinned_ids,
- "Post-update client migration was cancelled.", prewarm)
- var client_id := String(probe.get("id", ""))
- var details := ClientConfigurator.check_status_details_for_url_with_cli_path(
- client_id,
- server_url,
- String(probe.get("cli_path", "")),
- context,
- resolved_launch,
- )
- var status := int(details.get("status", Client.Status.ERROR))
- if status == Client.Status.NOT_CONFIGURED:
- continue
- if status == Client.Status.ERROR:
- ## One client whose configuration cannot even be read must not
- ## hold every other client and the server itself hostage: leave
- ## it for the dock's Configure and say so.
- deferred.append({
- "id": client_id,
- "reason": "its configuration could not be read: %s"
- % str(details.get("error_msg", "unknown error")),
- })
- continue
- if status == Client.Status.CONFIGURED:
- configured_ids.append(client_id)
- continue
- if replace_owned_mismatches:
- ## A major migration rewrites only entries that launch Godot AI.
- ## An entry under our name that starts something else is the
- ## user's own server: leave it, name it, let Configure replace it.
- if not bool(details.get("owned", false)):
- foreign_ids.append(client_id)
- continue
- elif not ClientConfigurator.entry_drift_is_version_pin_only(
- client_id, from_version, context
- ):
- ## #890's write restriction stands: an entry that is not provably
- ## what Configure wrote before the update is never rewritten here.
- ## But refusing the write is not a reason to refuse the server
- ## (#999): the entry is left untouched, named, and deferred to
- ## the dock's Configure, and startup continues.
- deferred.append({
- "id": client_id,
- "reason": "its godot-ai entry differs from what Configure wrote before the update",
- })
- continue
- var configured := ClientConfigurator.configure(client_id, server_url, context)
- if str(configured.get("status", "error")) != "ok":
- return _post_update_result(false, generation, configured_ids, repinned_ids,
- "%s repin failed: %s" % [client_id, str(configured.get("message", "unknown error"))],
- prewarm,
- bool(configured.get("termination_failed", false)),
- client_id)
- var verified := ClientConfigurator.check_status_details_for_url_with_cli_path(
- client_id,
- server_url,
- String(probe.get("cli_path", "")),
- context,
- resolved_launch,
- )
- if int(verified.get("status", Client.Status.ERROR)) != Client.Status.CONFIGURED:
- return _post_update_result(false, generation, configured_ids, repinned_ids,
- "%s repin could not be verified." % client_id, prewarm)
- configured_ids.append(client_id)
- repinned_ids.append(client_id)
- configured_ids.sort()
- repinned_ids.sort()
- foreign_ids.sort()
- deferred.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return str(a.id) < str(b.id))
- return _post_update_result(
- true, generation, configured_ids, repinned_ids, "", prewarm, false, "", foreign_ids, deferred
- )
-
-
-static func _post_update_result(
- ok: bool,
- generation: int,
- configured_ids: Array[String],
- repinned_ids: Array[String],
- error: String,
- prewarm: Dictionary,
- termination_failed: bool = false,
- unsafe_client_id: String = "",
- foreign_ids: Array[String] = [],
- deferred: Array[Dictionary] = [],
-) -> Dictionary:
- return {
- "ok": ok,
- "generation": generation,
- "configured_ids": configured_ids.duplicate(),
- "repinned_ids": repinned_ids.duplicate(),
- "error": error,
- "prewarm": prewarm.duplicate(true),
- "termination_failed": termination_failed,
- "unsafe_client_id": unsafe_client_id,
- ## Entries under our name that launch something else; left unchanged.
- "foreign_ids": foreign_ids.duplicate(),
- ## `{id, reason}` for entries the migration left unchanged because
- ## they were unreadable or drifted beyond the version pin (#999).
- ## They are the dock's Configure job, not a startup barrier.
- "deferred": deferred.duplicate(true),
- }
-
-
-func _poll_post_update_repin() -> void:
- if _post_update_thread == null or _post_update_thread.is_alive():
- return
- var payload: Variant = _post_update_thread.wait_to_finish()
- _post_update_thread = null
- if _refresh_state == RefreshState.SHUTTING_DOWN:
- return
- var result: Dictionary = (
- (payload as Dictionary).duplicate(true)
- if payload is Dictionary
- else {"ok": false, "error": "Post-update client migration returned no result."}
- )
- _record_unproven_post_update_result(result)
- if int(result.get("generation", -1)) != _post_update_generation:
- return
- post_update_repin_completed.emit(result)
- _publish_snapshot()
-
-
-func request_action(client_id: String, action: String) -> bool:
- var started := _start_action(client_id, action, "")
- if not bool(started.get("ok", false)) and bool(started.get("termination_failed", false)):
- action_completed.emit(
- client_id,
- action,
- {
- "status": "error",
- "message": str(started.get("error", "Client action is unavailable.")),
- "termination_failed": true,
- },
- {},
- )
- return bool(started.get("ok", false))
-
-
-## MCP mutations use the same one-slot owner as Dock actions. They preserve the
-## tool's previous mutation-only behavior: package prewarm remains a Dock-only
-## convenience. The returned timeout is the bounded out-of-band reply budget.
-func request_mcp_action(request_id: String, client_id: String, action: String) -> Dictionary:
- if request_id.is_empty():
- return {"ok": false, "error": "Client mutation requires a deferred request context."}
- return _start_action(client_id, action, request_id)
-
-
-func _start_action(client_id: String, action: String, request_id: String) -> Dictionary:
- if not _accepting_work:
- return {"ok": false, "error": "Client worker is unavailable."}
- if action not in ["configure", "remove"]:
- return {"ok": false, "error": "Unknown client action: %s" % action}
- if _mutation_termination_unproven.has(client_id):
- return {
- "ok": false,
- "error": MutationLock.recovery_message(),
- "termination_failed": true,
- }
- if _action_threads.has(client_id):
- return {"ok": false, "error": "A client action is already running for %s." % client_id}
- if not ClientConfigurator.has_client(client_id):
- return {"ok": false, "error": "Unknown client: %s" % client_id}
- ClientConfigurator.warm_env_snapshot()
- _set_action_cancelled(client_id, false)
- _clear_action_phase(client_id)
- var context := ClientConfigurator.capture_launch_context()
- _action_names[client_id] = action
- _action_request_ids[client_id] = request_id
- _action_timeout_reported.erase(client_id)
- _action_started_msec[client_id] = Time.get_ticks_msec()
- var thread := Thread.new()
- _action_threads[client_id] = thread
- var error := thread.start(
- Callable(self, "_run_action").bind(
- client_id,
- action,
- ClientConfigurator.server_url_from(context),
- context,
- request_id.is_empty(),
- )
- )
- if error != OK:
- _action_threads.erase(client_id)
- _action_started_msec.erase(client_id)
- _action_names.erase(client_id)
- _action_request_ids.erase(client_id)
- return {"ok": false, "error": "Could not start client worker thread."}
- ## Same invariant `begin_post_update_repin` states: a live Thread is only
- ## ever joined from `_process`, so spawning one must guarantee the poll.
- set_process(true)
- _publish_snapshot()
- return {
- "ok": true,
- "deferred_timeout_ms": _mcp_action_deferred_timeout_msec(action),
- }
-
-
-func _run_action(
- client_id: String,
- action: String,
- server_url: String,
- context: Dictionary,
- prewarm_after_configure: bool,
-) -> Dictionary:
- var result: Dictionary
- var prewarm: Dictionary = {}
- if action == "remove":
- result = ClientConfigurator.remove(client_id, server_url, context)
- else:
- result = ClientConfigurator.configure(client_id, server_url, context)
- if result.get("status") == "ok" and prewarm_after_configure:
- _set_action_phase(client_id, PHASE_PREWARM)
- prewarm = ClientConfigurator.prewarm_attach_launch(
- context,
- ClientConfigurator.PREWARM_TIMEOUT_MS,
- {},
- Callable(self, "_is_action_cancelled").bind(client_id),
- )
- return {
- "client_id": client_id,
- "action": action,
- "result": result,
- "prewarm": prewarm,
- }
-
-
-func _poll_actions() -> void:
- var phase_changed := false
- for client_id in _action_threads.keys():
- var thread: Thread = _action_threads[client_id]
- if thread == null:
- continue
- if thread.is_alive():
- phase_changed = phase_changed or not _read_action_phase(String(client_id)).is_empty()
- continue
- var payload: Variant = thread.wait_to_finish()
- _action_threads[client_id] = null
- var action := String(_action_names.get(client_id, "configure"))
- var result: Dictionary = {"status": "error", "message": "worker returned no result"}
- var prewarm: Dictionary = {}
- if payload is Dictionary:
- var data := payload as Dictionary
- action = String(data.get("action", action))
- result = (data.get("result", {}) as Dictionary).duplicate(true)
- prewarm = (data.get("prewarm", {}) as Dictionary).duplicate(true)
- _finalize_action(String(client_id), action, result, prewarm)
- if phase_changed:
- _publish_snapshot()
-
-
-func _finalize_action(
- client_id: String,
- action: String,
- result: Dictionary,
- prewarm: Dictionary,
-) -> void:
- ## Safety evidence is consumed before the sole slot is released. A completed
- ## timed-out worker therefore cannot reopen a race window between is_alive()
- ## flipping false and its termination_failed payload being persisted.
- _record_unproven_action_result(client_id, action, result, prewarm)
- var request_id := String(_action_request_ids.get(client_id, ""))
- _action_threads.erase(client_id)
- _action_started_msec.erase(client_id)
- _action_names.erase(client_id)
- _action_request_ids.erase(client_id)
- _action_timeout_reported.erase(client_id)
- _set_action_cancelled(client_id, false)
- _clear_action_phase(client_id)
- if _refresh_state != RefreshState.SHUTTING_DOWN:
- action_completed.emit(client_id, action, result.duplicate(true), prewarm.duplicate(true))
- if not request_id.is_empty():
- mcp_action_completed.emit(
- request_id,
- _mcp_action_payload(client_id, result),
- )
- _publish_snapshot()
-
-
-static func _mcp_action_payload(client_id: String, result: Dictionary) -> Dictionary:
- if str(result.get("status", "error")) == "error":
- return McpErrorCodes.make(
- McpErrorCodes.INTERNAL_ERROR,
- str(result.get("message", "Configuration failed for '%s'" % client_id)),
- )
- return {"data": result.duplicate(true)}
-
-
-static func _mcp_action_deferred_timeout_msec(_action: String) -> int:
- return ACTION_TIMEOUT_MSEC + MCP_ACTION_DEFERRED_GRACE_MSEC
-
-
-func _record_unproven_action_payload(payload: Variant) -> void:
- if not payload is Dictionary:
- return
- var data := payload as Dictionary
- var result: Dictionary = {}
- var prewarm: Dictionary = {}
- if data.get("result", null) is Dictionary:
- result = data.get("result", {}) as Dictionary
- if data.get("prewarm", null) is Dictionary:
- prewarm = data.get("prewarm", {}) as Dictionary
- _record_unproven_action_result(
- str(data.get("client_id", "")),
- str(data.get("action", "configure")),
- result,
- prewarm,
- )
-
-
-func _record_unproven_action_result(
- client_id: String,
- action: String,
- result: Dictionary,
- prewarm: Dictionary,
-) -> void:
- if client_id.is_empty():
- return
- if (
- bool(result.get("termination_failed", false))
- or bool(prewarm.get("termination_failed", false))
- ):
- _mark_mutation_termination_unproven(client_id, action)
-
-
-func _record_unproven_post_update_result(payload: Variant) -> void:
- if not payload is Dictionary:
- return
- var result := payload as Dictionary
- if not bool(result.get("termination_failed", false)):
- return
- var client_id := str(result.get("unsafe_client_id", ""))
- if client_id.is_empty():
- _post_update_termination_unproven = true
- Engine.set_meta(POST_UPDATE_UNPROVEN_META, true)
- else:
- _mark_mutation_termination_unproven(client_id, "post_update_repin")
-
-
-func _mark_mutation_termination_unproven(client_id: String, action: String) -> void:
- _mutation_termination_unproven[client_id] = action
- Engine.set_meta(
- MUTATION_UNPROVEN_META,
- _mutation_termination_unproven.duplicate(true),
- )
-
-
-func _check_action_timeouts(now_msec: int = -1) -> void:
- var now := Time.get_ticks_msec() if now_msec < 0 else now_msec
- for client_id in _action_threads.keys():
- if bool(_action_timeout_reported.get(client_id, false)):
- continue
- var started := int(_action_started_msec.get(client_id, 0))
- if started > 0 and now - started >= _action_budget(String(client_id)):
- _report_action_timeout(String(client_id), now - started)
-
-
-func _action_budget(client_id: String) -> int:
- if _read_action_phase(client_id) == PHASE_PREWARM:
- return ACTION_TIMEOUT_MSEC + ClientConfigurator.PREWARM_TIMEOUT_MS
- return ACTION_TIMEOUT_MSEC
-
-
-func _report_action_timeout(client_id: String, elapsed_msec: int) -> void:
- if not _action_threads.has(client_id):
- return
- var thread: Thread = _action_threads[client_id]
- var alive := thread != null and thread.is_alive()
- if alive:
- _set_action_cancelled(client_id, true)
- _action_timeout_reported[client_id] = true
- var action := String(_action_names.get(client_id, "configure"))
- var label := "Remove" if action == "remove" else "Configure"
- var detail := (
- "%s is taking longer than expected and is being stopped; this row remains locked until its worker is joined." % label
- if alive
- else "%s did not report completion in time; waiting to inspect its result." % label
- )
- print(
- "MCP | client action timed out: client=%s action=%s elapsed_ms=%d worker_alive=%s"
- % [client_id, action, elapsed_msec, str(alive)]
- )
- action_timed_out.emit(client_id, action, detail)
- _publish_snapshot()
-
-
-func request_status_refresh(ids: Array[String], force := false) -> bool:
- _client_ids = ids.duplicate()
- if not _accepting_work or _client_health_blocked:
- return false
- if RefreshState.has_worker_alive(_refresh_state):
- ## One refresh worker total. A timed-out probe remains visible and owns
- ## the slot until its bounded subprocess calls return; repeated force
- ## requests collapse into one retry instead of growing live Threads.
- _refresh_pending = true
- _refresh_pending_force = _refresh_pending_force or force
- _publish_snapshot()
- return false
- if RefreshState.is_blocked_for_spawn(_refresh_state):
- return false
- if not force and _refresh_completed_msec > 0:
- if Time.get_ticks_msec() - _refresh_completed_msec < STATUS_COOLDOWN_MSEC:
- return false
- if ids.is_empty():
- return false
- if _filesystem_busy():
- _refresh_state = RefreshState.DEFERRED_FOR_FILESYSTEM
- _refresh_pending_force = _refresh_pending_force or force
- _publish_snapshot()
- return false
- if force:
- ClientConfigurator.invalidate_cli_cache()
- _warm_worker_bytecode()
- var probes: Array[Dictionary] = []
- for client_id in ids:
- var probe := ClientConfigurator.client_status_probe_snapshot(client_id)
- if not probe.is_empty():
- probes.append(probe)
- if probes.is_empty():
- _finish_refresh({})
- return false
- var context := ClientConfigurator.capture_launch_context()
- _refresh_state = RefreshState.RUNNING
- _refresh_pending = false
- _refresh_pending_force = false
- _refresh_started_msec = Time.get_ticks_msec()
- _refresh_generation += 1
- _refresh_thread = Thread.new()
- var error := _refresh_thread.start(
- Callable(self, "_run_status_refresh").bind(
- probes,
- ClientConfigurator.server_url_from(context),
- context,
- _refresh_generation,
- )
- )
- if error != OK:
- _refresh_thread = null
- _refresh_state = RefreshState.IDLE
- _publish_snapshot()
- return false
- ## See `_start_action`: the sweep leaves RUNNING only through `_poll_refresh`.
- set_process(true)
- _publish_snapshot()
- return true
-
-
-## Queue one deferred MCP response onto the same aggregate refresh worker the
-## Dock uses. The dispatcher bounds its pending table; this local cap also
-## protects direct/future callers. No connection or handler is retained.
-func request_mcp_status(request_id: String) -> bool:
- if (
- request_id.is_empty()
- or not _accepting_work
- or _client_health_blocked
- or _refresh_state == RefreshState.SHUTTING_DOWN
- or _mcp_status_waiters.has(request_id)
- or _mcp_status_waiters.size() >= MAX_MCP_STATUS_WAITERS
- ):
- return false
- var target_generation := _refresh_generation + 1
- _mcp_status_waiters[request_id] = target_generation
- request_status_refresh(ClientConfigurator.client_ids(), true)
- if (
- _refresh_generation >= target_generation
- or _refresh_pending
- or _refresh_state == RefreshState.DEFERRED_FOR_FILESYSTEM
- ):
- return true
- _mcp_status_waiters.erase(request_id)
- return false
-
-
-func _run_status_refresh(
- probes: Array[Dictionary],
- server_url: String,
- context: Dictionary,
- generation: int,
-) -> Dictionary:
- var results: Dictionary = {}
- var resolved_launch := ClientConfigurator.resolve_attach_launch(context)
- for probe in probes:
- var client_id := String(probe.get("id", ""))
- if client_id.is_empty():
- continue
- var details := ClientConfigurator.check_status_details_for_url_with_cli_path(
- client_id,
- server_url,
- String(probe.get("cli_path", "")),
- context,
- resolved_launch,
- )
- results[client_id] = {
- "status": details.get("status", Client.Status.NOT_CONFIGURED),
- "installed": bool(probe.get("installed", false)),
- "error_msg": details.get("error_msg", ""),
- }
- return {"results": results, "generation": generation}
-
-
-func _poll_refresh() -> void:
- if _refresh_thread == null or _refresh_thread.is_alive():
- return
- var payload: Variant = _refresh_thread.wait_to_finish()
- _refresh_thread = null
- if not (payload is Dictionary):
- _finish_refresh({}, _refresh_generation, "Client status worker returned an invalid response.")
- return
- var data := payload as Dictionary
- if int(data.get("generation", -1)) != _refresh_generation:
- return
- var results := (data.get("results", {}) as Dictionary).duplicate(true)
- var timed_out := _refresh_state == RefreshState.RUNNING_TIMED_OUT
- _finish_refresh(
- {} if timed_out else results,
- int(data.get("generation", _refresh_generation)),
- "Client status probe timed out." if timed_out else "",
- )
-
-
-func _finish_refresh(
- results: Dictionary, completed_generation: int = -1, mcp_error: String = ""
-) -> void:
- _refresh_completed_msec = Time.get_ticks_msec()
- if _refresh_state != RefreshState.SHUTTING_DOWN:
- _refresh_state = RefreshState.IDLE
- if not _client_health_blocked and _refresh_state != RefreshState.SHUTTING_DOWN:
- status_refresh_completed.emit(results.duplicate(true))
- _complete_mcp_status_waiters(results, completed_generation, mcp_error)
- _publish_snapshot()
- if _refresh_pending:
- var force := _refresh_pending_force
- _refresh_pending = false
- _refresh_pending_force = false
- request_status_refresh(_client_ids, force)
-
-
-func _complete_mcp_status_waiters(
- results: Dictionary, completed_generation: int, error: String
-) -> void:
- if completed_generation < 0 or _mcp_status_waiters.is_empty():
- return
- var completed: Array[String] = []
- for request_id in _mcp_status_waiters:
- if int(_mcp_status_waiters[request_id]) <= completed_generation:
- completed.append(String(request_id))
- for request_id in completed:
- _mcp_status_waiters.erase(request_id)
- if completed.is_empty():
- return
- var payload := (
- McpErrorCodes.make(McpErrorCodes.INTERNAL_ERROR, error)
- if not error.is_empty()
- else ClientConfigurator.client_status_response(results)
- )
- mcp_status_completed.emit(completed, payload.duplicate(true))
-
-func _refresh_timed_out() -> bool:
- return (
- RefreshState.has_worker_alive(_refresh_state)
- and _refresh_started_msec > 0
- and Time.get_ticks_msec() - _refresh_started_msec >= STATUS_TIMEOUT_MSEC
- )
-
-
-func _check_refresh_timeout() -> void:
- if _refresh_state == RefreshState.RUNNING and _refresh_timed_out():
- _refresh_state = RefreshState.RUNNING_TIMED_OUT
- _publish_snapshot()
-
-
-func _retry_deferred_refresh() -> void:
- if _refresh_state != RefreshState.DEFERRED_FOR_FILESYSTEM:
- return
- if not _accepting_work or _client_health_blocked or _filesystem_busy():
- return
- var force := _refresh_pending_force
- _refresh_state = RefreshState.IDLE
- _refresh_pending_force = false
- request_status_refresh(_client_ids, force)
-
-
-func _filesystem_busy() -> bool:
- var filesystem := EditorInterface.get_resource_filesystem()
- return filesystem != null and filesystem.is_scanning()
-
-
-func _warm_worker_bytecode() -> void:
- var ids := ClientConfigurator.client_ids()
- if ids.is_empty():
- return
- var client := ClientRegistry.get_by_id(String(ids[0]))
- if client != null:
- JsonStrategy.verify_entry(client, {}, "")
- DshStrategy.command_launch_error(client, {})
- TomlStrategy.format_body(PackedStringArray(), "")
- CliStrategy.format_args(PackedStringArray(), "", "")
- ClientConfigurator.warm_env_snapshot()
-
-
-func _set_action_cancelled(client_id: String, cancelled: bool) -> void:
- _action_cancel_mutex.lock()
- if cancelled:
- _action_cancelled[client_id] = true
- else:
- _action_cancelled.erase(client_id)
- _action_cancel_mutex.unlock()
-
-
-func _is_action_cancelled(client_id: String) -> bool:
- _action_cancel_mutex.lock()
- var cancelled := bool(_action_cancelled.get(client_id, false))
- _action_cancel_mutex.unlock()
- return cancelled
-
-
-func _set_post_update_cancelled(cancelled: bool) -> void:
- _post_update_cancel_mutex.lock()
- _post_update_cancelled = cancelled
- _post_update_cancel_mutex.unlock()
-
-
-func _is_post_update_cancelled() -> bool:
- _post_update_cancel_mutex.lock()
- var cancelled := _post_update_cancelled
- _post_update_cancel_mutex.unlock()
- return cancelled
-
-
-func _set_action_phase(client_id: String, phase: String) -> void:
- _action_phase_mutex.lock()
- _action_phases[client_id] = phase
- _action_phase_mutex.unlock()
-
-
-func _read_action_phase(client_id: String) -> String:
- _action_phase_mutex.lock()
- var phase := String(_action_phases.get(client_id, ""))
- _action_phase_mutex.unlock()
- return phase
-
-
-func _read_action_phases() -> Dictionary:
- _action_phase_mutex.lock()
- var phases := _action_phases.duplicate(true)
- _action_phase_mutex.unlock()
- return phases
-
-
-func _clear_action_phase(client_id: String) -> void:
- _action_phase_mutex.lock()
- _action_phases.erase(client_id)
- _action_phase_mutex.unlock()
diff --git a/addons/godot_ai/utils/client_job_owner.gd.uid b/addons/godot_ai/utils/client_job_owner.gd.uid
deleted file mode 100644
index 43da106..0000000
--- a/addons/godot_ai/utils/client_job_owner.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://jo18d41wbdpx
diff --git a/addons/godot_ai/utils/client_mutation_lock.gd b/addons/godot_ai/utils/client_mutation_lock.gd
deleted file mode 100644
index 864f8cf..0000000
--- a/addons/godot_ai/utils/client_mutation_lock.gd
+++ /dev/null
@@ -1,131 +0,0 @@
-@tool
-class_name McpClientMutationLock
-extends RefCounted
-
-## Durable, atomic authority for all automatic client mutations.
-##
-## A POSIX exact-PID kill cannot prove that a timed-out CLI left no descendant
-## behind. The lock directory therefore survives plugin reload, editor restart,
-## and crashes. Only a sequence whose every direct child terminated without an
-## ambiguous kill removes it. An existing or malformed lock always fails closed.
-
-const ROOT_DIR_NAME := "godot-ai"
-const LOCK_DIR_NAME := "client_mutation.lock"
-const RECORD_NAME := "owner.json"
-const SCHEMA := 1
-const _POSIX_PERMISSION_MASK := 0x1ff ## 0777
-const _OWNER_DIRECTORY_MODE := (
- FileAccess.UNIX_READ_OWNER
- | FileAccess.UNIX_WRITE_OWNER
- | FileAccess.UNIX_EXECUTE_OWNER
-)
-
-
-static func lock_path() -> String:
- return _root_path().path_join(LOCK_DIR_NAME)
-
-
-static func recovery_message() -> String:
- return (
- "Automatic MCP client mutations are safety-locked. Stop relevant client processes "
- + "or reboot, then explicitly remove '%s' before retrying; restarting Godot alone "
- + "does not prove an old descendant stopped."
- ) % lock_path()
-
-
-static func is_locked() -> bool:
- var path := lock_path()
- return DirAccess.dir_exists_absolute(path) or FileAccess.file_exists(path)
-
-
-## Atomically claim the one global mutation slot. Creation wins ownership;
-## failure for any reason is indistinguishable from an existing unsafe owner.
-static func acquire(client_id: String, operation: String) -> Dictionary:
- var path := lock_path()
- if not _prepare_private_root():
- return {"ok": false, "error": recovery_message(), "path": path}
- if DirAccess.make_dir_absolute(path) != OK:
- return {"ok": false, "error": recovery_message(), "path": path}
- if not _has_private_mode(path):
- ## The directory itself remains as the durable deny marker. Never
- ## write ownership data into a location whose privacy is unproven.
- return {"ok": false, "error": recovery_message(), "path": path}
- var token := Crypto.new().generate_random_bytes(16).hex_encode()
- var record := {
- "schema": SCHEMA,
- "token": token,
- "pid": OS.get_process_id(),
- "client_id": client_id,
- "operation": operation,
- "started_unix": int(Time.get_unix_time_from_system()),
- }
- var record_path := path.path_join(RECORD_NAME)
- var file := FileAccess.open(record_path, FileAccess.WRITE)
- if file == null:
- return {"ok": false, "error": recovery_message(), "path": path}
- file.store_string(JSON.stringify(record))
- file.flush()
- var write_error := file.get_error()
- file.close()
- if write_error != OK:
- return {"ok": false, "error": recovery_message(), "path": path}
- return {"ok": true, "path": path, "record_path": record_path, "token": token}
-
-
-## Release only the exact claim this process wrote. Every failure leaves either
-## the record or its parent directory in place, which keeps later mutations
-## denied rather than guessing that cleanup succeeded.
-static func release(claim: Dictionary) -> bool:
- var path := str(claim.get("path", ""))
- var record_path := str(claim.get("record_path", ""))
- var token := str(claim.get("token", ""))
- if (
- path != lock_path()
- or record_path != path.path_join(RECORD_NAME)
- or token.is_empty()
- ):
- return false
- var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(record_path))
- if (
- not parsed is Dictionary
- or int(parsed.get("schema", 0)) != SCHEMA
- or str(parsed.get("token", "")) != token
- or int(parsed.get("pid", 0)) != OS.get_process_id()
- ):
- return false
- if DirAccess.remove_absolute(record_path) != OK:
- return false
- return DirAccess.remove_absolute(path) == OK
-
-
-static func _root_path() -> String:
- ## Unlike user://, the OS config directory is shared by every Godot
- ## project owned by this account. User-scope client config is shared too,
- ## so a project-specific lock would leave the real cross-editor race open.
- return OS.get_config_dir().path_join(ROOT_DIR_NAME)
-
-
-static func _prepare_private_root() -> bool:
- var root := _root_path()
- if FileAccess.file_exists(root) or _is_link(root):
- return false
- if not DirAccess.dir_exists_absolute(root):
- var created := DirAccess.make_dir_absolute(root)
- ## Another editor may have won the benign root-creation race. Accept
- ## that only after independently proving the resulting directory.
- if created != OK and not DirAccess.dir_exists_absolute(root):
- return false
- return not _is_link(root) and _has_private_mode(root)
-
-
-static func _has_private_mode(path: String) -> bool:
- if OS.get_name() == "Windows":
- return true
- var permission_error := FileAccess.set_unix_permissions(path, _OWNER_DIRECTORY_MODE)
- var actual_mode := FileAccess.get_unix_permissions(path) & _POSIX_PERMISSION_MASK
- return permission_error == OK and actual_mode == _OWNER_DIRECTORY_MODE
-
-
-static func _is_link(path: String) -> bool:
- var parent := DirAccess.open(path.get_base_dir())
- return parent == null or parent.is_link(path)
diff --git a/addons/godot_ai/utils/client_mutation_lock.gd.uid b/addons/godot_ai/utils/client_mutation_lock.gd.uid
deleted file mode 100644
index c6ef2a2..0000000
--- a/addons/godot_ai/utils/client_mutation_lock.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://climutlockv4x1
diff --git a/addons/godot_ai/utils/diagnostics_capture.gd b/addons/godot_ai/utils/diagnostics_capture.gd
deleted file mode 100644
index 22441ad..0000000
--- a/addons/godot_ai/utils/diagnostics_capture.gd
+++ /dev/null
@@ -1,66 +0,0 @@
-@tool
-class_name McpDiagnosticsCapture
-extends RefCounted
-
-## Small helper for scoped validation-log capture windows. Callers snapshot a
-## private log cursor, perform a deliberate validation action, then only report
-## new diagnostics whose original source location is the target file.
-
-
-static func capture_this_file(log_buffer: McpEditorLogBuffer, target_path: String, action: Callable) -> Dictionary:
- var cursor := 0
- if log_buffer != null:
- cursor = log_buffer.appended_total()
-
- var action_result = action.call()
- var diagnostics: Array[Dictionary] = []
- var truncated := false
-
- if log_buffer != null:
- var captured: Dictionary = log_buffer.get_since(cursor)
- truncated = captured.get("truncated", false)
- diagnostics = _diagnostics_for_target(captured.get("entries", []), target_path)
-
- return {
- "action": action_result if action_result is Dictionary else {},
- "diagnostics": diagnostics,
- "diagnostics_detail": "log_capture" if not diagnostics.is_empty() else "none",
- "diagnostics_scope": "this_file",
- "diagnostics_status": "partial" if truncated else "checked",
- }
-
-
-static func _diagnostics_for_target(entries: Array, target_path: String) -> Array[Dictionary]:
- var out: Array[Dictionary] = []
- for raw_entry in entries:
- if not raw_entry is Dictionary:
- continue
- var entry: Dictionary = raw_entry
- if not _entry_matches_target(entry, target_path):
- continue
- out.append(_normalize_entry(entry, target_path))
- return out
-
-
-static func _entry_matches_target(entry: Dictionary, target_path: String) -> bool:
- var source := _source_location(entry)
- return str(source.get("path", "")) == target_path
-
-
-static func _normalize_entry(entry: Dictionary, target_path: String) -> Dictionary:
- var normalized := entry.duplicate(true)
- var source := _source_location(entry)
- normalized["path"] = str(source.get("path", target_path))
- normalized["line"] = int(source.get("line", normalized.get("line", 0)))
- normalized["function"] = str(source.get("function", normalized.get("function", "")))
- if normalized.has("details") and normalized.details is Dictionary:
- normalized["details"] = normalized.details.duplicate(true)
- return normalized
-
-
-static func _source_location(entry: Dictionary) -> Dictionary:
- if entry.get("details") is Dictionary:
- var details: Dictionary = entry.details
- if details.get("source") is Dictionary:
- return details.source
- return {}
diff --git a/addons/godot_ai/utils/diagnostics_capture.gd.uid b/addons/godot_ai/utils/diagnostics_capture.gd.uid
deleted file mode 100644
index 95973c9..0000000
--- a/addons/godot_ai/utils/diagnostics_capture.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b3npxxpuobbc2
diff --git a/addons/godot_ai/utils/editor_log_buffer.gd b/addons/godot_ai/utils/editor_log_buffer.gd
deleted file mode 100644
index 0ea44e7..0000000
--- a/addons/godot_ai/utils/editor_log_buffer.gd
+++ /dev/null
@@ -1,127 +0,0 @@
-@tool
-class_name McpEditorLogBuffer
-extends McpStructuredLogRing
-
-## Ring buffer for editor-process script errors and warnings (parse errors,
-## @tool runtime errors, EditorPlugin errors, push_error/push_warning) captured
-## by editor_logger.gd's Logger subclass.
-##
-## Smaller cap than McpGameLogBuffer (500 vs 2000) — the editor only emits errors,
-## not the full println firehose a game can produce. No run_id rotation: editor
-## errors persist across project_run cycles (they're about *editing* state, not
-## about the playing game).
-##
-## Mutex-protected because Logger virtuals can fire from any thread (e.g.
-## async script-loader threads emitting parse errors), and the buffer is
-## read on the main thread by EditorHandler.get_logs. Each public method
-## wraps the base ring's lockless helpers in `_mutex.lock()/unlock()` —
-## the base stays lockless so McpGameLogBuffer's hot path doesn't pay an
-## unused mutex cost.
-##
-## Entry shape: {source: "editor", level: "info"|"warn"|"error",
-## text, path, line, function} — `path/line/function` may be empty/zero
-## when the source location wasn't recoverable (e.g. printerr from a
-## thread without a script context).
-
-const MAX_LINES := 500
-
-var _mutex := Mutex.new()
-var _error_appended_total := 0
-var _warn_appended_total := 0
-
-
-func _init() -> void:
- super._init(MAX_LINES)
-
-
-func append(level: String, text: String, path: String = "", line: int = 0, function: String = "", details: Dictionary = {}) -> void:
- var coerced_level := _coerce_level(level)
- var entry := {
- "source": "editor",
- "level": coerced_level,
- "text": text,
- "path": path,
- "line": line,
- "function": function,
- }
- if not details.is_empty():
- entry["details"] = details.duplicate(true)
- _mutex.lock()
- _append_entry(entry)
- if coerced_level == "error":
- _error_appended_total += 1
- elif coerced_level == "warn":
- _warn_appended_total += 1
- _mutex.unlock()
-
-
-func get_range(offset: int, count: int) -> Array[Dictionary]:
- _mutex.lock()
- var out := _get_range_unlocked(offset, count)
- _mutex.unlock()
- return out
-
-
-func get_recent(count: int) -> Array[Dictionary]:
- ## Single-lock so the size we compute `start` from can't race against
- ## a concurrent append between the size read and the slice copy.
- _mutex.lock()
- var size := _total_count_unlocked()
- var start := maxi(0, size - count)
- var out := _get_range_unlocked(start, size - start)
- _mutex.unlock()
- return out
-
-
-func get_since(since_seq: int, limit: int = -1) -> Dictionary:
- ## Single-lock so the cursor snapshot and slice copy can't race against a
- ## Logger-thread append.
- _mutex.lock()
- var out := _get_since_unlocked(since_seq, limit)
- _mutex.unlock()
- return out
-
-
-func total_count() -> int:
- _mutex.lock()
- var n := _total_count_unlocked()
- _mutex.unlock()
- return n
-
-
-func dropped_count() -> int:
- _mutex.lock()
- var n := _dropped_count_unlocked()
- _mutex.unlock()
- return n
-
-
-func appended_total() -> int:
- _mutex.lock()
- var n := _appended_total_unlocked()
- _mutex.unlock()
- return n
-
-
-func error_appended_total() -> int:
- _mutex.lock()
- var n := _error_appended_total
- _mutex.unlock()
- return n
-
-
-func warn_appended_total() -> int:
- _mutex.lock()
- var n := _warn_appended_total
- _mutex.unlock()
- return n
-
-
-func clear() -> int:
- _mutex.lock()
- var n := _total_count_unlocked()
- _clear_storage()
- _error_appended_total = 0
- _warn_appended_total = 0
- _mutex.unlock()
- return n
diff --git a/addons/godot_ai/utils/editor_log_buffer.gd.uid b/addons/godot_ai/utils/editor_log_buffer.gd.uid
deleted file mode 100644
index 8c8d823..0000000
--- a/addons/godot_ai/utils/editor_log_buffer.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b6ynms0856hhq
diff --git a/addons/godot_ai/utils/error_codes.gd b/addons/godot_ai/utils/error_codes.gd
deleted file mode 100644
index 9136144..0000000
--- a/addons/godot_ai/utils/error_codes.gd
+++ /dev/null
@@ -1,164 +0,0 @@
-@tool
-class_name McpErrorCodes
-extends RefCounted
-
-## Error code constants shared across handlers. Mirrors protocol/errors.py.
-##
-## This `class_name` shipped in v2.3.2 and earlier and must stay reachable
-## through self-update. v2.4.1 dropped it and triggered a "Could not resolve
-## script" cascade for every user upgrading from any earlier version; v2.4.2
-## restored it as a hot-fix. The cascade fires because Godot keeps stale
-## registry entries during the disable -> extract -> enable window when a
-## previously-registered class_name disappears, and that failure mode is
-## independent of the runner's install ordering. See CLAUDE.md's
-## never-delete-published-class_name policy for the shape-aware shim path
-## that retirement (if ever needed) must follow.
-##
-## All consumers use the preload-alias pattern
-## (`const ErrorCodes := preload(...)`) introduced in #412. The alias is
-## stylistic; both `McpErrorCodes.X` and `ErrorCodes.X` resolve through the
-## same Script object cache, so the alias is not a parse-safety boundary
-## under the single-phase runner.
-
-const INVALID_PARAMS := "INVALID_PARAMS"
-const EDITED_SCENE_MISMATCH := "EDITED_SCENE_MISMATCH"
-const EDITOR_NOT_READY := "EDITOR_NOT_READY"
-const UNKNOWN_COMMAND := "UNKNOWN_COMMAND"
-const INTERNAL_ERROR := "INTERNAL_ERROR"
-const DEFERRED_TIMEOUT := "DEFERRED_TIMEOUT"
-## Python-originated transport/attach bridge codes. GDScript has no emit path,
-## but the public registry intentionally mirrors protocol/errors.py.
-const TRANSPORT_OUTCOME_UNKNOWN := "TRANSPORT_OUTCOME_UNKNOWN"
-const TRANSPORT_OVERLOADED := "TRANSPORT_OVERLOADED"
-const NEW_CLIENT_SESSION_REQUIRED := "NEW_CLIENT_SESSION_REQUIRED"
-const ATTACH_LOCK_TIMEOUT := "ATTACH_LOCK_TIMEOUT"
-const ATTACH_LOCK_ERROR := "ATTACH_LOCK_ERROR"
-const ATTACH_RUNTIME_DIR_ERROR := "ATTACH_RUNTIME_DIR_ERROR"
-const PORT_OCCUPIED := "PORT_OCCUPIED"
-const BACKEND_START_FAILED := "BACKEND_START_FAILED"
-const BACKEND_START_TIMEOUT := "BACKEND_START_TIMEOUT"
-# game_eval failure codes (#490) — keep in sync with protocol/errors.py
-const EVAL_COMPILE_ERROR := "EVAL_COMPILE_ERROR"
-const EVAL_RUNTIME_ERROR := "EVAL_RUNTIME_ERROR"
-## #518/#859: the play session is up (EditorInterface.is_playing_scene() is true,
-## so editor_handler's EDITOR_NOT_READY "game is not running" gate already
-## passed) but the game cannot service evals: the helper did not register within
-## EVAL_READY_WAIT_SEC, its main-loop beacon is stale, or its debugger session
-## closed mid-eval. Carved out of INTERNAL_ERROR so these fast,
-## caller-actionable failures do not burn the opaque 10s eval backstop.
-const EVAL_GAME_NOT_READY := "EVAL_GAME_NOT_READY"
-## #518: the eval genuinely never finished inside the timeout ladder — the
-## game-side 8s deadline aborted a hung await, or the editor-side 10s backstop
-## fired because the game never replied at all (CPU-bound loop, frozen /
-## backgrounded idle loop). Carved out of INTERNAL_ERROR — the last big
-## still-unlabeled bucket from #487/#488 — so "your eval code never finished"
-## stops reading as an internal fault in telemetry and agent-facing errors.
-const EVAL_HUNG := "EVAL_HUNG"
-## #518: the eval completed but its serialized result is too large for the
-## debugger + WebSocket pipeline. Without this the reply is dropped silently
-## (the debugger TCP peer discards messages over ~8 MiB) and the request rides
-## to the 10s backstop as a phantom "hang". Failing fast game-side with the
-## real byte count makes the failure actionable (return a smaller slice).
-const EVAL_RESULT_TOO_LARGE := "EVAL_RESULT_TOO_LARGE"
-## #777: a game-side request (currently editor_screenshot source="game")
-## reached a live, registered game helper but no reply came back before the
-## editor-side timer fired. Every editor gate already passed
-## (is_playing_scene, helper hello) so this is a TOP-LEVEL code, not an
-## EDITOR_NOT_READY sub-code: the game process itself failed to respond —
-## backgrounded with a frozen main loop and nothing rendered to fall back
-## on, main thread blocked, or the helper died mid-run. Carved out of
-## INTERNAL_ERROR (the largest opaque timeout bucket fleet-wide) so the
-## residual timeout is attributable and actionable.
-const GAME_HELPER_TIMEOUT := "GAME_HELPER_TIMEOUT"
-## audit-v2 #21 (issue #365): finer-grained codes carved out of the 471
-## INVALID_PARAMS sites so agents can distinguish recoverable input
-## errors from structural ones. INVALID_PARAMS stays for genuinely
-## catch-all input errors that don't fit any of the buckets below.
-##
-## - NODE_NOT_FOUND: scene-tree/autoload node lookup failed (path didn't
-## resolve to a Node).
-## - RESOURCE_NOT_FOUND: a `res://` path lookup failed (file/.tres/
-## .gdshader/.tscn etc. doesn't exist or couldn't load). Distinct from
-## NODE_NOT_FOUND because the recovery path differs — agents need to
-## know whether to fix a node path vs. create/import a resource.
-## - PROPERTY_NOT_ON_CLASS: property/signal/method/uniform/slot lookup
-## failed on a known instance (path resolved, but the requested
-## member doesn't exist on that class).
-## - VALUE_OUT_OF_RANGE: numeric/index bound violation OR enum value
-## not in the allowed set.
-## - WRONG_TYPE: input was a value (or a loaded resource) of the wrong
-## type — the param was provided, but `typeof` or `is X` failed.
-## - MISSING_REQUIRED_PARAM: required input field was absent or empty.
-const NODE_NOT_FOUND := "NODE_NOT_FOUND"
-const RESOURCE_NOT_FOUND := "RESOURCE_NOT_FOUND"
-const PROPERTY_NOT_ON_CLASS := "PROPERTY_NOT_ON_CLASS"
-const VALUE_OUT_OF_RANGE := "VALUE_OUT_OF_RANGE"
-const WRONG_TYPE := "WRONG_TYPE"
-const MISSING_REQUIRED_PARAM := "MISSING_REQUIRED_PARAM"
-const CUSTOM_TOOL_NOT_UNDOABLE := "CUSTOM_TOOL_NOT_UNDOABLE"
-const CUSTOM_TOOL_DISABLED := "CUSTOM_TOOL_DISABLED"
-
-## #651 stage 1: EDITOR_NOT_READY sub-codes. These travel in
-## `error.data.sub_code`, NEVER as the top-level `error.code` — existing
-## callers and dashboards key on EDITOR_NOT_READY, so the top-level code is
-## frozen. Each sub-code names the concrete editor state at rejection time,
-## limited to states EditorInterface/EditorFileSystem can report
-## deterministically. States we cannot observe (script compilation,
-## resource reload, modal dialogs) intentionally get NO sub-code: a bare
-## EDITOR_NOT_READY stays the honest fallback rather than a guessed label.
-## Keep in sync with protocol/errors.py::EditorNotReadySubCode — enforced
-## by tests/unit/test_editor_not_ready_hint_contract.py.
-const SUB_EDITOR_IMPORTING := "EDITOR_IMPORTING"
-const SUB_EDITOR_PLAYING := "EDITOR_PLAYING"
-const SUB_EDITOR_NO_SCENE := "EDITOR_NO_SCENE"
-const SUB_EDITOR_GAME_NOT_RUNNING := "EDITOR_GAME_NOT_RUNNING"
-const SUB_EDITOR_VIEWPORT_UNAVAILABLE := "EDITOR_VIEWPORT_UNAVAILABLE"
-const SUB_EDITOR_VIEWPORT_NOT_3D := "EDITOR_VIEWPORT_NOT_3D"
-const SUB_EDITOR_VIEWPORT_EMPTY := "EDITOR_VIEWPORT_EMPTY"
-const SUB_EDITOR_UNAVAILABLE := "EDITOR_UNAVAILABLE"
-## Emitted only by the exclusive-run transport servicing path: a command
-## arrived while a synchronous test run holds the main thread, and was
-## rejected (not buffered) so it can't replay stale after its server-side
-## future expires. See connection.gd::service_transport_during_exclusive_run.
-const SUB_EDITOR_TEST_RUNNING := "EDITOR_TEST_RUNNING"
-
-## Terminal code for a test run that hit its between-test abort ceiling
-## before finishing. error.data carries the partial summary; full partial
-## results stay retrievable via get_test_results.
-const TEST_RUN_TIMEOUT := "TEST_RUN_TIMEOUT"
-
-
-## Build a standard error response dictionary.
-static func make(code: String, message: String) -> Dictionary:
- return {"status": "error", "error": {"code": code, "message": message}}
-
-
-## Build an EDITOR_NOT_READY error carrying the #651 stage-1 attribution
-## payload: `data.sub_code` + `retryable` + `hint`. Mirrors the shape
-## scene_path.gd::require_edited_scene established (editor_state/retryable/
-## hint). `hint` may be empty when `message` already IS the recovery hint —
-## the server's GodotCommandError string-appends every data key, so
-## duplicating the message into data would double the agent-visible text.
-static func make_not_ready(
- sub_code: String, message: String, retryable: bool, hint: String = ""
-) -> Dictionary:
- var err := make(EDITOR_NOT_READY, message)
- var data := {"sub_code": sub_code, "retryable": retryable}
- if not hint.is_empty():
- data["hint"] = hint
- err["error"]["data"] = data
- return err
-
-
-## Return a NEW error dict with the original code and a prefixed message.
-## Prefer this over mutating `err["error"]["message"]` in place — callers
-## that want to add context ("Property '%s': …") shouldn't need to know
-## the internal shape of the dict returned by `make`. Empty `prefix`
-## returns `err` unchanged so callers don't need their own guard.
-static func prefix_message(err: Dictionary, prefix: String) -> Dictionary:
- if prefix.is_empty():
- return err
- var inner: Dictionary = err.get("error", {})
- var code: String = inner.get("code", INTERNAL_ERROR)
- var message: String = inner.get("message", "")
- return make(code, "%s: %s" % [prefix, message])
diff --git a/addons/godot_ai/utils/error_codes.gd.uid b/addons/godot_ai/utils/error_codes.gd.uid
deleted file mode 100644
index fcc1e82..0000000
--- a/addons/godot_ai/utils/error_codes.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d2klnglf5p861
diff --git a/addons/godot_ai/utils/fuzzy_suggestions.gd b/addons/godot_ai/utils/fuzzy_suggestions.gd
deleted file mode 100644
index de7cc19..0000000
--- a/addons/godot_ai/utils/fuzzy_suggestions.gd
+++ /dev/null
@@ -1,39 +0,0 @@
-@tool
-extends RefCounted
-
-## Shared fuzzy ranking for typo suggestions.
-
-
-static func rank(
- needle: String,
- candidates: Array,
- limit: int = 5,
- threshold: float = 0.4,
- substring_bonus: float = 0.5,
- prefix_bonus: float = 1.0
-) -> Array[String]:
- if needle.is_empty() or candidates.is_empty():
- return []
- var needle_lower := needle.to_lower()
- var scored: Array = []
- for raw_candidate in candidates:
- var candidate := str(raw_candidate)
- var candidate_lower := candidate.to_lower()
- var score := needle.similarity(candidate)
- if prefix_bonus != 0.0 and candidate_lower.begins_with(needle_lower):
- score += prefix_bonus
- elif substring_bonus != 0.0 and (
- candidate_lower.contains(needle_lower) or needle_lower.contains(candidate_lower)
- ):
- score += substring_bonus
- if score >= threshold:
- scored.append([score, candidate])
- scored.sort_custom(func(a, b):
- if a[0] == b[0]:
- return a[1] < b[1]
- return a[0] > b[0]
- )
- var result: Array[String] = []
- for index in range(min(limit, scored.size())):
- result.append(scored[index][1])
- return result
diff --git a/addons/godot_ai/utils/fuzzy_suggestions.gd.uid b/addons/godot_ai/utils/fuzzy_suggestions.gd.uid
deleted file mode 100644
index c663d01..0000000
--- a/addons/godot_ai/utils/fuzzy_suggestions.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bxwaws6w0xw60
diff --git a/addons/godot_ai/utils/game_log_buffer.gd b/addons/godot_ai/utils/game_log_buffer.gd
deleted file mode 100644
index 2f1b3b0..0000000
--- a/addons/godot_ai/utils/game_log_buffer.gd
+++ /dev/null
@@ -1,105 +0,0 @@
-@tool
-class_name McpGameLogBuffer
-extends McpStructuredLogRing
-
-## Ring buffer for game-process log lines (print, push_warning, push_error)
-## ferried back from the playing game over the EngineDebugger channel.
-##
-## Larger cap than McpEditorLogBuffer because games can be noisy. `run_id`
-## rotates at play-start, giving agents a stable cursor for "lines from
-## this run" even when the game never reaches the mcp:hello boot beacon.
-##
-## Single-threaded — game_helper.gd drains its logger from `_process` and
-## calls `append` from the main thread, so this subclass can use the base
-## ring's lockless reads/writes directly.
-
-const MAX_LINES := 2000
-
-var _run_id := ""
-var _run_seq := 0
-var _error_warn_total := 0
-var _error_total := 0
-
-
-func _init() -> void:
- super._init(MAX_LINES)
-
-
-func append(level: String, text: String, details: Dictionary = {}) -> void:
- var coerced_level := _coerce_level(level)
- var entry := {
- "source": "game",
- "level": coerced_level,
- "text": text,
- "run_id": _run_id,
- }
- if not details.is_empty():
- entry["details"] = details.duplicate(true)
- _append_entry(entry)
- if coerced_level in ["warn", "error"]:
- _error_warn_total += 1
- if coerced_level == "error":
- _error_total += 1
-
-
-## Rotate the run identifier without dropping buffered entries. Called at
-## play-start so even no-hello parse failures get a fresh current-run identity.
-## Historical lines stay tagged with their original run_id and can still be
-## queried explicitly.
-func clear_for_new_run() -> String:
- _run_id = _generate_run_id()
- _error_warn_total = 0
- _error_total = 0
- return _run_id
-
-
-func run_id() -> String:
- return _run_id
-
-
-func error_warn_total() -> int:
- return _error_warn_total
-
-
-func error_total() -> int:
- return _error_total
-
-
-## Warn-level lines for the current run: the combined error+warn tally minus
-## the error-only tally. Feeds the `game_warn` watermark component so a run
-## that only emitted push_warning is no longer reported as clean.
-func warn_total() -> int:
- return _error_warn_total - _error_total
-
-
-func get_run_range(run_id: String, offset: int, count: int) -> Array[Dictionary]:
- return get_run_page(run_id, offset, count).entries
-
-
-func get_run_page(run_id: String, offset: int, count: int) -> Dictionary:
- var entries := _entries_for_run(run_id)
- var start := mini(maxi(0, offset), entries.size())
- var stop := mini(entries.size(), start + maxi(0, count))
- var out: Array[Dictionary] = []
- for i in range(start, stop):
- out.append(entries[i])
- return {
- "entries": out,
- "total_count": entries.size(),
- }
-
-
-func _entries_for_run(run_id: String) -> Array[Dictionary]:
- var out: Array[Dictionary] = []
- for entry in get_range(0, total_count()):
- if str(entry.get("run_id", "")) == run_id:
- out.append(entry)
- return out
-
-
-func _generate_run_id() -> String:
- ## Opaque to agents — they only check equality. Time-based is plenty
- ## unique within a single editor session; the local sequence protects
- ## fast back-to-back test runs within the same millisecond.
- _run_seq += 1
- return "r%d-%d" % [Time.get_ticks_msec(), _run_seq]
diff --git a/addons/godot_ai/utils/game_log_buffer.gd.uid b/addons/godot_ai/utils/game_log_buffer.gd.uid
deleted file mode 100644
index 6c7ce7d..0000000
--- a/addons/godot_ai/utils/game_log_buffer.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://biojw0xl64haw
diff --git a/addons/godot_ai/utils/json_values.gd b/addons/godot_ai/utils/json_values.gd
deleted file mode 100644
index 6100836..0000000
--- a/addons/godot_ai/utils/json_values.gd
+++ /dev/null
@@ -1,108 +0,0 @@
-@tool
-class_name McpJsonValues
-extends RefCounted
-
-## Canonical JSON→Variant parsers for the wire shapes agents send.
-##
-## One parser family instead of five drifted per-handler copies (#714) —
-## the canonical color set is the maintainer decision recorded on that
-## issue. parse_color accepts: Color passthrough; "#rrggbb"/"#rrggbbaa"
-## hex or named-color strings (two-sentinel Color.from_string
-## validation); {r,g,b[,a]} dicts; [r,g,b[,a]] arrays. parse_vector2/3
-## accept the Vector passthrough, {x,y[,z]} dicts, and [x,y[,z]] arrays.
-##
-## Strict WITHIN each shape (the #123/#126 contract): wrong dict keys,
-## wrong array lengths, or non-numeric components return null instead of
-## guessing zeros — callers turn null into their own typed error.
-
-const COLOR_KEYS: Array[String] = ["r", "g", "b"]
-const VECTOR2_KEYS: Array[String] = ["x", "y"]
-const VECTOR3_KEYS: Array[String] = ["x", "y", "z"]
-
-
-static func parse_color(value: Variant) -> Variant:
- if value is Color:
- return value
- if value is String:
- ## Color.from_string returns the fallback on parse failure — call
- ## twice with distinct sentinels; agreement means a real parse.
- var a := Color.from_string(value, Color(0, 0, 0, 0))
- var b := Color.from_string(value, Color(1, 1, 1, 1))
- if a != b:
- return null
- return a
- if value is Dictionary:
- var d: Dictionary = value
- if not d.has_all(COLOR_KEYS):
- return null
- var alpha: Variant = d.get("a", 1.0)
- if not (_is_number(d.r) and _is_number(d.g) and _is_number(d.b) and _is_number(alpha)):
- return null
- return Color(float(d.r), float(d.g), float(d.b), float(alpha))
- if value is Array:
- var arr: Array = value
- if arr.size() != 3 and arr.size() != 4:
- return null
- for item in arr:
- if not _is_number(item):
- return null
- var a4 := float(arr[3]) if arr.size() == 4 else 1.0
- return Color(float(arr[0]), float(arr[1]), float(arr[2]), a4)
- return null
-
-
-static func parse_vector2(value: Variant) -> Variant:
- if value is Vector2:
- return value
- if value is Dictionary:
- var d: Dictionary = value
- if not d.has_all(VECTOR2_KEYS) or not (_is_number(d.x) and _is_number(d.y)):
- return null
- return Vector2(float(d.x), float(d.y))
- if value is Array:
- var arr: Array = value
- if arr.size() != 2 or not (_is_number(arr[0]) and _is_number(arr[1])):
- return null
- return Vector2(float(arr[0]), float(arr[1]))
- return null
-
-
-static func parse_vector3(value: Variant) -> Variant:
- if value is Vector3:
- return value
- if value is Dictionary:
- var d: Dictionary = value
- if not d.has_all(VECTOR3_KEYS):
- return null
- if not (_is_number(d.x) and _is_number(d.y) and _is_number(d.z)):
- return null
- return Vector3(float(d.x), float(d.y), float(d.z))
- if value is Array:
- var arr: Array = value
- if arr.size() != 3:
- return null
- for item in arr:
- if not _is_number(item):
- return null
- return Vector3(float(arr[0]), float(arr[1]), float(arr[2]))
- return null
-
-
-## Parse a scalar float from the wire shapes agents send: float passthrough,
-## int (JSON integers land as numbers, not strings), and strictly-numeric
-## strings — some MCP clients stringify float arguments ("4.0"; #964).
-## Anything else returns null so callers turn it into their own typed
-## error; null input stays null (callers gate on the input's type first
-## when null is a meaningful "clear" value).
-static func parse_float(value: Variant) -> Variant:
- if _is_number(value):
- return float(value)
- if value is String:
- var text := value as String
- if text.is_valid_float():
- return text.to_float()
- return null
-
-
-static func _is_number(v: Variant) -> bool:
- return v is int or v is float
diff --git a/addons/godot_ai/utils/json_values.gd.uid b/addons/godot_ai/utils/json_values.gd.uid
deleted file mode 100644
index b3eeba9..0000000
--- a/addons/godot_ai/utils/json_values.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://4wbf83hwckms
diff --git a/addons/godot_ai/utils/log_backtrace.gd b/addons/godot_ai/utils/log_backtrace.gd
deleted file mode 100644
index 3fdf838..0000000
--- a/addons/godot_ai/utils/log_backtrace.gd
+++ /dev/null
@@ -1,113 +0,0 @@
-@tool
-class_name McpLogBacktrace
-extends RefCounted
-
-## Helpers for interpreting Godot's `_log_error` virtual arguments.
-## (Named `McpLogBacktrace`, not `ScriptBacktrace`: Godot ships a built-in
-## `ScriptBacktrace` class — the type of `script_backtraces[i]` entries
-## — so class_name'ing ours the same would collide. Verified against
-## the engine's `--doctool` output in 4.6.)
-##
-## Both `editor_logger.gd` and `game_logger.gd` need to:
-## - Map `error_type` (0=ERROR, 1=WARNING, 2=SCRIPT, 3=SHADER) to a
-## two-bucket "error" / "warn" string so callers can filter without
-## consulting the enum.
-## - Fall back to `code` when `rationale` is empty — single-arg
-## `push_error("msg")` leaves rationale empty and stuffs the user's
-## string into `code`; without the fallback the user message is
-## silently lost. The two-arg form `push_error(code, rationale)`
-## populates both and rationale wins.
-## - Remap the source location to the first frame of `script_backtraces[0]`
-## when present. `push_error` / `push_warning` always report
-## `file=core/variant/variant_utility.cpp`; the actual user GDScript
-## caller is in the backtrace.
-##
-## Centralising the rules keeps the next push_error semantics shift
-## (already happened once between 4.5 and 4.6, see PR #78) a one-place
-## fix instead of a two-place hunt.
-
-
-## Coalesce the per-virtual-arg shape Godot hands `_log_error` into a
-## flat record. Always walks `script_backtraces` for the first non-empty
-## frame; loggers that need to filter by source path call this first and
-## then check the resolved `path` field.
-##
-## Returns: `{level, message, path, line, function, details}`
-## - `level`: "error" or "warn" (warn iff `error_type == 1`).
-## - `message`: `rationale` when non-empty, else `code`.
-## - `path` / `line` / `function`: first backtrace frame when one is
-## available; otherwise the original `file` / `line` / `function`.
-## - `details`: original `_log_error` fields plus the first non-empty
-## backtrace as frames, mirroring the debugger Errors tab context.
-const ERROR_TYPE_NAMES := {
- 0: "error",
- 1: "warning",
- 2: "script",
- 3: "shader",
-}
-
-
-static func resolve_error(
- function: String,
- file: String,
- line: int,
- code: String,
- rationale: String,
- error_type: int,
- script_backtraces: Array,
-) -> Dictionary:
- var src_file := file
- var src_line := line
- var src_function := function
- var frames: Array[Dictionary] = []
- ## First non-empty frame wins, not just `script_backtraces[0]` —
- ## chained errors can leave the leading entry empty with the actual
- ## user frame in `script_backtraces[1]`.
- for bt in script_backtraces:
- if bt != null and bt.get_frame_count() > 0:
- frames = _frames_from_backtrace(bt)
- src_file = str(frames[0].get("path", ""))
- src_line = int(frames[0].get("line", 0))
- src_function = str(frames[0].get("function", ""))
- break
- var message := rationale if not rationale.is_empty() else code
- return {
- "level": "warn" if error_type == 1 else "error",
- "message": message,
- "path": src_file,
- "line": src_line,
- "function": src_function,
- "details": {
- "message": message,
- "code": code,
- "rationale": rationale,
- "error_type": error_type,
- "error_type_name": _error_type_name(error_type),
- "source": {
- "path": file,
- "line": line,
- "function": function,
- },
- "resolved": {
- "path": src_file,
- "line": src_line,
- "function": src_function,
- },
- "frames": frames,
- },
- }
-
-
-static func _frames_from_backtrace(bt) -> Array[Dictionary]:
- var frames: Array[Dictionary] = []
- for i in bt.get_frame_count():
- frames.append({
- "path": bt.get_frame_file(i),
- "line": bt.get_frame_line(i),
- "function": bt.get_frame_function(i),
- })
- return frames
-
-
-static func _error_type_name(error_type: int) -> String:
- return str(ERROR_TYPE_NAMES.get(error_type, "unknown"))
diff --git a/addons/godot_ai/utils/log_backtrace.gd.uid b/addons/godot_ai/utils/log_backtrace.gd.uid
deleted file mode 100644
index 4845ad8..0000000
--- a/addons/godot_ai/utils/log_backtrace.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b8t9kznr2pqxa
diff --git a/addons/godot_ai/utils/log_buffer.gd b/addons/godot_ai/utils/log_buffer.gd
deleted file mode 100644
index 34bd2b5..0000000
--- a/addons/godot_ai/utils/log_buffer.gd
+++ /dev/null
@@ -1,67 +0,0 @@
-@tool
-class_name McpLogBuffer
-extends RefCounted
-
-## Ring buffer for MCP log lines. Also prints to Godot console.
-
-const MAX_LINES := 500
-
-## When false, `log()` still records into the ring buffer but does not echo the
-## line to the Godot console. The test runner flips this off for the duration
-## of a run so negative-path suites (which intentionally drive a 500-line ring
-## fill and malformed-result error logging) don't bury an all-green run in
-## console noise. Ring *contents* — what tests assert on via `get_recent()` /
-## `total_logged()` — are unaffected. Engine-level C++ errors raised by
-## negative-path tests are not routed through here and still surface.
-static var console_echo := true
-
-var _lines: Array[String] = []
-## Monotonic count of every line ever passed to `log()` since the last
-## `clear()`. Distinct from `_lines.size()`, which is bounded at MAX_LINES.
-## Consumers that need to detect "new lines arrived" (e.g. `LogViewer.tick`)
-## must track this rather than the bounded size — once the ring fills, the
-## size stays at MAX_LINES on every subsequent append, so a size-based
-## cursor would freeze and the consumer would stop seeing new entries.
-var _total_logged: int = 0
-var enabled := true
-
-
-## `echo=false` records the line into the ring (so the dock's log panel shows
-## it) without printing to the Godot console. Used for high-frequency
-## machine-driven lines like readiness flips, which spammed the console of
-## every install (#626) — filesystem scans toggle readiness on each import.
-func log(msg: String, echo: bool = true) -> void:
- var line := "MCP | %s" % msg
- if enabled and console_echo and echo:
- print(line)
- _lines.append(line)
- if _lines.size() > MAX_LINES:
- _lines = _lines.slice(-MAX_LINES)
- _total_logged += 1
-
-
-func get_recent(count: int = 50) -> Array[String]:
- var start := maxi(0, _lines.size() - count)
- var result: Array[String] = []
- result.assign(_lines.slice(start))
- return result
-
-
-func clear() -> void:
- _lines.clear()
- ## Reset the monotonic counter so a viewer's `seq < _last_seq` shrink
- ## detection still recognizes the clear. Callers that want a cumulative
- ## ever-produced count across clears can wrap their own counter.
- _total_logged = 0
-
-
-func total_count() -> int:
- return _lines.size()
-
-
-## Monotonic sequence — number of lines ever appended via `log()` since
-## the last `clear()`. Strictly increases per append, even once the ring
-## has filled and `total_count()` is pinned at MAX_LINES. See `_total_logged`
-## for rationale.
-func total_logged() -> int:
- return _total_logged
diff --git a/addons/godot_ai/utils/log_buffer.gd.uid b/addons/godot_ai/utils/log_buffer.gd.uid
deleted file mode 100644
index 58341ca..0000000
--- a/addons/godot_ai/utils/log_buffer.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ddkslse7511e6
diff --git a/addons/godot_ai/utils/mcp_adoption_label.gd b/addons/godot_ai/utils/mcp_adoption_label.gd
deleted file mode 100644
index d3537c4..0000000
--- a/addons/godot_ai/utils/mcp_adoption_label.gd
+++ /dev/null
@@ -1,8 +0,0 @@
-@tool
-class_name McpAdoptionLabel
-extends RefCounted
-
-## Retired v4 lifecycle vocabulary kept only because this class_name and its
-## constants were published. Runtime ownership now lives in the tagged episode.
-const MANAGED := "managed"
-const EXTERNAL := "external"
diff --git a/addons/godot_ai/utils/mcp_adoption_label.gd.uid b/addons/godot_ai/utils/mcp_adoption_label.gd.uid
deleted file mode 100644
index aee58fe..0000000
--- a/addons/godot_ai/utils/mcp_adoption_label.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://klhsu1cuhcue
diff --git a/addons/godot_ai/utils/mcp_client_refresh_state.gd b/addons/godot_ai/utils/mcp_client_refresh_state.gd
deleted file mode 100644
index 7b88c34..0000000
--- a/addons/godot_ai/utils/mcp_client_refresh_state.gd
+++ /dev/null
@@ -1,101 +0,0 @@
-@tool
-class_name McpClientRefreshState
-extends RefCounted
-
-## State machine for the plugin-lifetime client's status refresh sweep. Single
-## source of truth — supersedes the seven booleans + deadline previously
-## scattered across `mcp_dock.gd` (`_client_status_refresh_in_flight`,
-## `_client_status_refresh_pending`, `_client_status_refresh_pending_force`,
-## `_client_status_refresh_timed_out`, `_client_status_refresh_started_msec`,
-## `_client_status_refresh_deferred_until_filesystem_ready`,
-## `_client_status_refresh_deferred_force`,
-## `_client_status_refresh_deferred_initial`,
-## `_client_status_refresh_shutdown_requested`).
-##
-## The ints are stable for tests; reordering is a breaking change.
-
-## No worker running, no pending request. Default state.
-const IDLE := 0
-## A refresh request landed but the editor filesystem is busy
-## (`EditorInterface.get_resource_filesystem().is_scanning()` is true);
-## the job owner parks the request and retries on the next `_process` after
-## the scan settles. A separate force bit preserves an explicit retry request.
-const DEFERRED_FOR_FILESYSTEM := 1
-## Worker thread is alive and probing client status off-main. The
-## Dock snapshots paint "(checking...)" in the clients summary and the owner accepts
-## additional requests as `pending`.
-const RUNNING := 2
-## Worker has been alive past CLIENT_STATUS_REFRESH_TIMEOUT_MSEC. The
-## dock paints "(client probe still running)"; further requests coalesce into
-## one retry after this sole worker exits.
-const RUNNING_TIMED_OUT := 3
-## Plugin teardown / update activation is draining workers. New refresh
-## requests are rejected outright until an explicitly failed handoff resumes
-## the same owner.
-const SHUTTING_DOWN := 4
-
-const _NAMES := {
- IDLE: "idle",
- DEFERRED_FOR_FILESYSTEM: "deferred_for_filesystem",
- RUNNING: "running",
- RUNNING_TIMED_OUT: "running_timed_out",
- SHUTTING_DOWN: "shutting_down",
-}
-
-
-static func name_of(state: int) -> String:
- return _NAMES.get(state, "unknown(%d)" % state)
-
-
-## True when a worker thread should be alive in this state. Combined
-## state — RUNNING or RUNNING_TIMED_OUT both have the sole worker running.
-static func has_worker_alive(state: int) -> bool:
- return state == RUNNING or state == RUNNING_TIMED_OUT
-
-
-## True while the status worker is still within its healthy budget. Once a
-## refresh has timed out, the dock keeps the warning badge but must let users
-## retry Configure / Configure all instead of stranding the controls behind an
-## orphaned, uninterruptible worker.
-static func should_disable_client_actions(state: int) -> bool:
- return state == RUNNING
-
-
-## True when the dock should reject new refresh spawns. Used by the
-## job owner's deferred entrypoint and status-refresh scheduler.
-static func is_blocked_for_spawn(state: int) -> bool:
- return state == SHUTTING_DOWN
-
-
-## True when the summary label should show the in-flight badge.
-static func should_show_checking_badge(state: int) -> bool:
- return state == RUNNING or state == RUNNING_TIMED_OUT
-
-
-## Transition table. Same shape as McpServerState — illegal transitions
-## return false; callers `push_warning` and no-op.
-static func can_transition(from: int, to: int) -> bool:
- if from == to:
- return true
- ## Shutdown is sticky.
- if from == SHUTTING_DOWN:
- return false
- ## Anything → SHUTTING_DOWN is legal (drain on _exit_tree / install).
- if to == SHUTTING_DOWN:
- return true
- match from:
- IDLE:
- return to == RUNNING or to == DEFERRED_FOR_FILESYSTEM
- DEFERRED_FOR_FILESYSTEM:
- ## When the filesystem scan settles we either spawn a worker
- ## (RUNNING) or roll back to IDLE if no rows need probing.
- return to == RUNNING or to == IDLE
- RUNNING:
- ## Worker finishes -> IDLE. Worker outlives budget ->
- ## RUNNING_TIMED_OUT.
- return to == IDLE or to == RUNNING_TIMED_OUT
- RUNNING_TIMED_OUT:
- ## The sole late-arriving worker drops back to IDLE. A coalesced
- ## retry then starts through the ordinary IDLE -> RUNNING edge.
- return to == IDLE
- return false
diff --git a/addons/godot_ai/utils/mcp_client_refresh_state.gd.uid b/addons/godot_ai/utils/mcp_client_refresh_state.gd.uid
deleted file mode 100644
index 9e4129c..0000000
--- a/addons/godot_ai/utils/mcp_client_refresh_state.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dv4tukg6eioww
diff --git a/addons/godot_ai/utils/mcp_server_state.gd b/addons/godot_ai/utils/mcp_server_state.gd
deleted file mode 100644
index f127015..0000000
--- a/addons/godot_ai/utils/mcp_server_state.gd
+++ /dev/null
@@ -1,57 +0,0 @@
-@tool
-class_name McpServerState
-extends RefCounted
-## Stable presentation values consumed by the Dock and editor-status API.
-## McpServerLifecycleManager's tagged episode is the only transition owner.
-const UNINITIALIZED := 0
-const SPAWNING := 1
-# Slot 2 stays unused so existing status consumers keep stable values.
-const READY := 3
-const INCOMPATIBLE := 4
-const CRASHED := 5
-const NO_COMMAND := 6
-const PORT_EXCLUDED := 7
-const FOREIGN_PORT := 8
-const STOPPING := 10
-const STOPPED := 11
-
-const _NAMES := {
- UNINITIALIZED: "uninitialized",
- SPAWNING: "spawning",
- READY: "ready",
- INCOMPATIBLE: "incompatible",
- CRASHED: "crashed",
- NO_COMMAND: "no_command",
- PORT_EXCLUDED: "port_excluded",
- FOREIGN_PORT: "foreign_port",
- STOPPING: "stopping",
- STOPPED: "stopped",
-}
-
-
-## Human-readable label. Used in startup-trace logs and transition
-## warnings. Falls back to `unknown()` for unrecognised values so
-## a future enum addition won't crash the formatter.
-static func name_of(state: int) -> String:
- return _NAMES.get(state, "unknown(%d)" % state)
-
-
-## True for any state the dock should render as a non-OK diagnostic
-## panel. Used as the "should we hide the spawn-failure panel?" gate.
-static func is_terminal_diagnosis(state: int) -> bool:
- return (
- state == CRASHED
- or state == NO_COMMAND
- or state == PORT_EXCLUDED
- or state == INCOMPATIBLE
- or state == FOREIGN_PORT
- )
-
-
-## True when the dock should skip interpreting client health (incompatible
-## tool surface). This must NOT block Configure writes — those take an
-## explicit url and the live plugin version (#916). Currently just
-## INCOMPATIBLE — FOREIGN_PORT is transitional and may resolve to READY
-## if the foreign occupant turns out to speak our handshake.
-static func blocks_client_health(state: int) -> bool:
- return state == INCOMPATIBLE
diff --git a/addons/godot_ai/utils/mcp_server_state.gd.uid b/addons/godot_ai/utils/mcp_server_state.gd.uid
deleted file mode 100644
index dc5bef9..0000000
--- a/addons/godot_ai/utils/mcp_server_state.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d3ial4erjonlq
diff --git a/addons/godot_ai/utils/mcp_startup_path.gd b/addons/godot_ai/utils/mcp_startup_path.gd
deleted file mode 100644
index dc0ae16..0000000
--- a/addons/godot_ai/utils/mcp_startup_path.gd
+++ /dev/null
@@ -1,15 +0,0 @@
-@tool
-class_name McpStartupPath
-extends RefCounted
-
-## Retired v4 lifecycle vocabulary kept only because this class_name and its
-## constants were published. Runtime startup state lives in one tagged episode.
-const UNSET := ""
-const GUARDED := "guarded"
-const ADOPTED := "adopted"
-const SPAWNED := "spawned"
-const CRASHED := "crashed"
-const RESERVED := "reserved"
-const NO_COMMAND := "no_command"
-const INCOMPATIBLE := "incompatible"
-const FREE := "free"
diff --git a/addons/godot_ai/utils/mcp_startup_path.gd.uid b/addons/godot_ai/utils/mcp_startup_path.gd.uid
deleted file mode 100644
index fd01066..0000000
--- a/addons/godot_ai/utils/mcp_startup_path.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cikdvq2x4vs4x
diff --git a/addons/godot_ai/utils/path_validator.gd b/addons/godot_ai/utils/path_validator.gd
deleted file mode 100644
index 8cdc77c..0000000
--- a/addons/godot_ai/utils/path_validator.gd
+++ /dev/null
@@ -1,216 +0,0 @@
-@tool
-class_name McpPathValidator
-extends RefCounted
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Validates `res://`-rooted paths against directory-traversal escape.
-##
-## Issue #347 (audit-v2 #3): handlers were accepting `res://../etc/passwd.gd`
-## because the only check was `path.begins_with("res://")`. LLM-driven path
-## generation (prompt injection, agent typos, untrusted issue/PR text in
-## context) can produce traversal payloads for the write tools that produce
-## arbitrary disk content (`script_create`, `filesystem_write_text`,
-## `patch_script`) and for the matching reads (info disclosure surface).
-##
-## Two entry points:
-## * `validate_resource_path` — for paths that name a `res://` disk file the
-## plugin will read or (with `for_write`) write. This is the strict one.
-## * `validate_loadable_path` — for paths handed to `ResourceLoader`, which
-## also accepts `uid://` (an opaque resource-DB id that cannot express
-## traversal) and `user://` (the per-project user data sandbox). Load
-## handlers must use this so `uid://` references copied out of `.tscn`
-## ExtResource / `.uid` sidecars and `user://` runtime assets keep loading.
-##
-## Error wrapping: callers should use `path_error` / `loadable_error`, which
-## return a ready `ErrorCodes.make(VALUE_OUT_OF_RANGE, …)` dict (or null). A
-## bad path is a value-domain error, and funneling every site through one
-## wrapper keeps the error code consistent across all handlers.
-##
-## Known limitation: containment is lexical (`globalize_path` + `simplify_path`
-## prefix match). It does NOT resolve symlinks — GDScript exposes no realpath.
-## A symlink *inside* the project that points outside it can therefore defeat
-## the under-root check. This matches the engine's own `res://` resolution and
-## is accepted; the loopback trust boundary is the primary control.
-
-
-# Cached project / user roots. `globalize_path` is stable across the editor's
-# lifetime — caching avoids redundant resolution on every call. Matters most
-# for `reimport`, which loops the validator over each path in a batch.
-# Lazy-init on first call so static-load timing can't see a half-initialised
-# ProjectSettings.
-static var _cached_res_root: String = ""
-static var _cached_user_root: String = ""
-
-
-static func _res_root() -> String:
- if _cached_res_root.is_empty():
- _cached_res_root = ProjectSettings.globalize_path("res://").simplify_path()
- return _cached_res_root
-
-
-static func _user_root() -> String:
- if _cached_user_root.is_empty():
- _cached_user_root = ProjectSettings.globalize_path("user://").simplify_path()
- return _cached_user_root
-
-
-## True when `path` carries a codepoint that must never reach a filesystem API.
-##
-## Issue #889: the previous guard built its sentinel with `String.chr(0)`, and
-## Godot emits `Unicode parsing error … Unexpected NUL character` while
-## *constructing* that string. Every validated call — including reads that then
-## failed with "File not found" — printed an alarming engine error that had
-## nothing to do with the caller's path. Reading codepoints instead never
-## materialises a NUL String, so the noise is gone.
-##
-## Both codepoints are rejected, for different reasons:
-## * `0x0000` — a real embedded NUL can truncate a C string, so the path that
-## is validated is not the path that gets opened. Current Godot cannot
-## retain one in a String, making this a no-op today; it stays as
-## defense-in-depth for any build or input path that can.
-## * `0xFFFD` — what current Godot actually substitutes for a NUL or any other
-## undecodable input. Once the engine has replaced the byte there is no way
-## left to tell an attempted NUL from malformed UTF-8 from a literal
-## replacement character, so the only safe reading at a security boundary is
-## to refuse all three. A legitimate filename containing U+FFFD is rejected
-## too; that false positive is deliberate and vanishingly rare.
-static func _contains_invalid_path_codepoint(path: String) -> bool:
- for i in path.length():
- var codepoint := path.unicode_at(i)
- if codepoint == 0x0000 or codepoint == 0xFFFD:
- return true
- return false
-
-
-## Returns "" when the path is a safe `res://`-rooted reference inside the
-## project root. Returns a human-readable error message otherwise.
-## Prefer `path_error` over calling this directly — it wraps the message in the
-## canonical error code.
-##
-## Pass `for_write = true` for any handler that creates/overwrites the file
-## (write_file, create_script, patch_script, ResourceSaver-backed saves,
-## scene saves). Write callers additionally refuse the project manifest and
-## startup override, plus the `.godot/` metadata dir. Reads default to
-## `for_write = false`, which permits inspecting those files.
-static func validate_resource_path(path: String, for_write: bool = false) -> String:
- if path.is_empty():
- return "Missing required param: path"
- if _contains_invalid_path_codepoint(path):
- return "Path must not contain null bytes or invalid Unicode"
- if not path.begins_with("res://"):
- return "Path must start with res://"
- var confine_err := _confine_under(path, _res_root(), "res://")
- if not confine_err.is_empty():
- return confine_err
- if for_write:
- return _reject_sensitive_write(path)
- return ""
-
-
-## Returns "" when `path` is safe to hand to `ResourceLoader.load` / `.exists`.
-## Accepts, in addition to confined `res://` paths:
-## * `uid://` — an opaque 64-bit resource id; it cannot express a path
-## and the engine only ever resolves it to a resource already in the
-## project, so there is nothing to confine.
-## * `user://…` — the per-project user data dir, confined under its root the
-## same way `res://` is (so `user://../…` can't escape the sandbox).
-static func validate_loadable_path(path: String) -> String:
- if path.is_empty():
- return "Missing required param: path"
- if _contains_invalid_path_codepoint(path):
- return "Path must not contain null bytes or invalid Unicode"
- if path.begins_with("uid://"):
- return ""
- if path.begins_with("user://"):
- return _confine_under(path, _user_root(), "user://")
- if path.begins_with("res://"):
- return _confine_under(path, _res_root(), "res://")
- return "Path must start with res://, uid://, or user://"
-
-
-## Shared traversal + under-root containment. `root` must already be simplified.
-static func _confine_under(path: String, root: String, label: String) -> String:
- if ".." in path:
- return "Path must not contain '..' (path traversal not allowed)"
- var globalized := ProjectSettings.globalize_path(path).simplify_path()
- # Append a separator so `/proj_evil/...` can't pretend to be inside `/proj`
- # via prefix match. `globalized == root` covers the bare `res://` / `user://`.
- if globalized != root and not globalized.begins_with(root + "/"):
- return "Path must resolve under %s root" % label
- return ""
-
-
-## Refuse writes that would clobber project-critical files. The path is already
-## confirmed `res://`-rooted and traversal-free by the caller.
-##
-## Comparisons are case-folded: macOS (APFS) and Windows (NTFS) are
-## case-insensitive by default, so `res://Project.godot` resolves to the real
-## `project.godot` and must be refused too.
-##
-## `.import` sidecars are deliberately NOT blocked — editing an asset's import
-## options then re-importing is a legitimate, recoverable workflow (the file is
-## source-controlled). The blocked set is the startup-execution surface only:
-## the manifest, its `override.cfg` shadow, and the `.godot/` cache dir.
-static func _reject_sensitive_write(path: String) -> String:
- # Normalize once, then reuse for filename and segment checks. The writer
- # (`FileAccess::fix_path` / `FileAccessWindows`) folds `\`, `//`, and `.`
- # the same way, so `res://project.godot/.` and `res://override.cfg/.` must
- # not slip past `get_file()` as a trailing `.` while still opening the
- # protected file.
- #
- # - `res://.godot\uid_cache.bin` used to collapse to one fused segment
- # and match neither this check nor the loaded-plugin one below, while the
- # write itself still landed in `.godot/`.
- # - a `.` segment shifts the *anchored* plugin-tree check below, so
- # `res://./addons/godot_ai/plugin.gd` slipped past it even once the
- # separators were normalized.
- #
- # `..` never reaches here -- `_confine_under` refuses it first -- so its
- # folding is inert. Because the engine normalizes backslashes on Linux too,
- # a POSIX file literally named `addons\godot_ai\x.gd` is not addressable
- # through `res://` at all, so normalizing costs no legitimate write.
- var normalized := path.replace("\\", "/").simplify_path()
- var file_lower := normalized.get_file().to_lower()
- if file_lower == "project.godot":
- return "Refusing to write res://project.godot (project manifest)"
- if file_lower == "override.cfg":
- return "Refusing to write res://override.cfg (startup config override)"
- # Reject the `.godot/` editor-metadata dir at any depth. Split drops empty
- # segments so a trailing slash can't hide a segment from the check.
- var segments := normalized.trim_prefix("res://").split("/", false)
- for segment in segments:
- if segment.to_lower() == ".godot":
- return "Refusing to write under res://.godot/ (editor metadata)"
- # Reject the currently-loaded plugin's own script tree. Overwriting a
- # loaded .gd here (plus the immediate reimport triggered by update_file())
- # can SIGABRT the editor mid-call, or silently corrupt the installed
- # plugin so the next enable fails to resolve scripts.
- if segments.size() >= 2 and segments[0].to_lower() == "addons" and segments[1].to_lower() == "godot_ai":
- return "Refusing to write under res://addons/godot_ai/ (overwriting a loaded plugin script can crash the editor)"
- return ""
-
-
-## Validate a write/read `res://` path and return a ready error dict, or null
-## when the path is fine. The single wrapper every handler should use so the
-## error code (VALUE_OUT_OF_RANGE — a bad path is a value-domain error) stays
-## consistent. `param_name` is prefixed onto the message for context.
-static func path_error(path: String, param_name: String = "path", for_write: bool = false) -> Variant:
- if path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: %s" % param_name)
- var err := validate_resource_path(path, for_write)
- if err.is_empty():
- return null
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "%s: %s" % [param_name, err])
-
-
-## Same as `path_error` but for paths handed to `ResourceLoader` (allows
-## `uid://` / `user://`). Returns a ready error dict or null. An empty path is
-## reported as MISSING_REQUIRED_PARAM rather than a value error.
-static func loadable_error(path: String, param_name: String = "path") -> Variant:
- if path.is_empty():
- return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: %s" % param_name)
- var err := validate_loadable_path(path)
- if err.is_empty():
- return null
- return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "%s: %s" % [param_name, err])
diff --git a/addons/godot_ai/utils/path_validator.gd.uid b/addons/godot_ai/utils/path_validator.gd.uid
deleted file mode 100644
index 8ed4acf..0000000
--- a/addons/godot_ai/utils/path_validator.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://blxntmd65ljyu
diff --git a/addons/godot_ai/utils/plugin_reload.gd b/addons/godot_ai/utils/plugin_reload.gd
deleted file mode 100644
index 5577f51..0000000
--- a/addons/godot_ai/utils/plugin_reload.gd
+++ /dev/null
@@ -1,125 +0,0 @@
-@tool
-extends RefCounted
-
-## Ordinary in-editor reload only. Transaction activation keeps its own
-## quiescence, lock and readiness protocol; this is not a hot-update grant.
-const PLUGIN_CFG := "res://addons/godot_ai/plugin.cfg"
-const ScriptWork := preload("res://addons/godot_ai/utils/script_work.gd")
-## A reload waits for the filesystem scan it asks for. An editor that is
-## still importing at startup can take well over 5 s to answer on a slow
-## host, and a reload abandoned then leaves the plugin unchanged while the
-## caller keeps waiting; stay under the server's 90 s reconnect budget.
-const SCAN_TIMEOUT_SECONDS := 60.0
-## The native timer's budget; tests shorten it to prove the timer itself
-## (not its length) settles an unfinished scan under a paused time scale.
-static var _scan_timeout_seconds: float = SCAN_TIMEOUT_SECONDS
-
-## One command-scoped native-signal handoff, not a suspended GDScript frame.
-## Named callbacks survive the source reload caused by the scan itself.
-static var _pending_scan: Dictionary = {}
-
-
-static func is_reload_pending() -> bool:
- return not _pending_scan.is_empty()
-
-
-## Reserve before deferring the scan so the current dispatcher tick cannot
-## start another handler which pumps the already-scheduled reload callback.
-static func reserve_reload() -> int:
- if is_reload_pending():
- return 0
- var work := ScriptWork.begin("reload_plugin")
- _pending_scan = {"work": work}
- return work
-
-
-static func reload_after_scan(work: int = 0) -> void:
- if work == 0:
- work = reserve_reload()
- if work == 0:
- return
- elif _pending_scan.get("work", 0) != work or bool(_pending_scan.get("toggling", false)):
- return # A direct reload or cancellation consumed this deferred request.
- _start_scan(EditorInterface.get_resource_filesystem(), null, work)
-
-
-static func _start_scan(filesystem: Object, timer: Object, work: int) -> void:
- if not _pending_scan.is_empty() and (
- _pending_scan.get("work", 0) != work or _pending_scan.has("filesystem")
- or bool(_pending_scan.get("toggling", false))
- ):
- if _pending_scan.get("work", 0) != work:
- ScriptWork.finish(work)
- push_error("MCP | a plugin reload is already waiting for its filesystem scan")
- return
- if timer == null:
- # Editor deadlines must not inherit a project's pause or time scale.
- timer = Engine.get_main_loop().create_timer(_scan_timeout_seconds, true, false, true)
- var complete := _finish_scan.bind(work, false)
- var timeout := _finish_scan.bind(work, true)
- _pending_scan = {"work": work, "filesystem": filesystem, "timer": timer,
- "complete": complete, "timeout": timeout}
- filesystem.filesystem_changed.connect(complete, CONNECT_ONE_SHOT | CONNECT_DEFERRED)
- timer.timeout.connect(timeout, CONNECT_ONE_SHOT)
- filesystem.scan()
-
-
-static func _take_scan() -> Dictionary:
- var pending := _pending_scan
- _pending_scan = {}
- if pending.has("filesystem"):
- if pending.filesystem.filesystem_changed.is_connected(pending.complete):
- pending.filesystem.filesystem_changed.disconnect(pending.complete)
- if pending.timer.timeout.is_connected(pending.timeout):
- pending.timer.timeout.disconnect(pending.timeout)
- return pending
-
-
-static func _finish_scan(work: int, timed_out: bool) -> void:
- # A queued callback from an earlier scan can never consume a later request.
- if _pending_scan.get("work", 0) != work or not _pending_scan.has("filesystem"):
- return
- if timed_out:
- var pending := _take_scan()
- ScriptWork.finish(pending.work)
- push_error(
- "MCP | filesystem scan did not finish within %d s; plugin left unchanged, retry reload"
- % int(_scan_timeout_seconds)
- )
- else:
- reload_enabled_plugin()
-
-
-static func reload_enabled_plugin() -> Error:
- # Keep the existing reservation through teardown: toggling the plugin can
- # itself pump frames. Direct callers also gate dispatch during the toggle.
- if not is_reload_pending():
- reserve_reload()
- elif bool(_pending_scan.get("toggling", false)):
- return ERR_BUSY
- # Disconnect scan signals before a toggle pumps frames, but retain the
- # work reservation so already-queued callbacks cannot start another toggle.
- var pending := _take_scan()
- _pending_scan = {"work": pending.work, "toggling": true}
- var result := _toggle_enabled_plugin()
- _take_scan()
- ScriptWork.finish(pending.work)
- return result
-
-
-static func _toggle_enabled_plugin() -> Error:
- if not EditorInterface.is_plugin_enabled(PLUGIN_CFG):
- push_error("MCP | cannot reload a disabled plugin")
- return ERR_UNAVAILABLE
- EditorInterface.set_plugin_enabled(PLUGIN_CFG, false)
- EditorInterface.set_plugin_enabled(PLUGIN_CFG, true)
- if not EditorInterface.is_plugin_enabled(PLUGIN_CFG):
- push_error("MCP | plugin could not be re-enabled after reload")
- return FAILED
- ## Plugin callbacks save autoload changes while the temporary disabled state
- ## is still visible. Persist only after set_plugin_enabled has completed, or
- ## the next editor process can reopen with this working plugin disabled.
- var error := ProjectSettings.save()
- if error != OK:
- push_error("MCP | reloaded plugin enablement could not be saved: %s" % error_string(error))
- return error
diff --git a/addons/godot_ai/utils/plugin_reload.gd.uid b/addons/godot_ai/utils/plugin_reload.gd.uid
deleted file mode 100644
index 711b7a4..0000000
--- a/addons/godot_ai/utils/plugin_reload.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c287g7fv5n3en
diff --git a/addons/godot_ai/utils/port_resolver.gd b/addons/godot_ai/utils/port_resolver.gd
deleted file mode 100644
index 9ef32e9..0000000
--- a/addons/godot_ai/utils/port_resolver.gd
+++ /dev/null
@@ -1,768 +0,0 @@
-@tool
-class_name McpPortResolver
-extends RefCounted
-
-## Pure-static port discovery / OS-specific scrapers. No instance state,
-## no editor dependencies. plugin.gd has thin instance shims that wrap
-## these and increment the cold-start trace counters.
-
-## Canonical pid-file path. plugin.gd::SERVER_PID_FILE re-exports this so
-## external readers and tests can use either name.
-const SERVER_PID_FILE := "user://godot_ai_server.pid"
-## Where a plugin-spawned server reports a failure that happens before it
-## publishes its capability record (`--startup-report`). Removed before every
-## spawn, so a report that exists afterwards belongs to that launch.
-const SERVER_STARTUP_REPORT := "user://godot_ai_server_startup.json"
-const WindowsPortReservation := preload("res://addons/godot_ai/utils/windows_port_reservation.gd")
-static var _process_spawn_mutex := Mutex.new()
-
-
-## Serialize plugin-owned subprocess creation across the brief process-global
-## environment override used for server capabilities. Callers must keep this
-## window to environment mutation + one spawn and release it immediately.
-static func lock_process_spawn() -> void:
- _process_spawn_mutex.lock()
-
-
-static func unlock_process_spawn() -> void:
- _process_spawn_mutex.unlock()
-
-
-## A managed Linux server needs a listener PID tool to prove ownership.
-## Query each launch so installing the missing tool makes Retry work.
-static func listener_tools_problem() -> String:
- if OS.get_name() != "Linux":
- return ""
- var output: Array = []
- var available := OS.execute("/bin/sh", ["-c",
- "command -v lsof >/dev/null 2>&1 || command -v ss >/dev/null 2>&1"
- ], output, true)
- if available == 0:
- return ""
- return "Cannot verify Linux listener ownership: neither lsof nor ss is available on the editor's PATH. Install lsof or iproute2 (which provides ss), then retry starting the server."
-
-
-static func can_bind_local_port(port: int) -> bool:
- var server := TCPServer.new()
- var err := server.listen(port, "127.0.0.1")
- if err == OK:
- server.stop()
- return true
- return false
-
-
-## True when `port` is bound on 127.0.0.1. Probes via TCPServer first,
-## falls back to OS scraping. Callers that want per-scraper trace
-## counters should call `is_port_in_use_via_scrape` with a trace hook
-## after their own `can_bind_local_port` probe.
-static func is_port_in_use(port: int) -> bool:
- if can_bind_local_port(port):
- ## On POSIX, an IPv6 wildcard listener can coexist with a
- ## successful 127.0.0.1 bind probe. Confirm with lsof so startup
- ## sees the same listener set that shutdown/recovery would see.
- if OS.get_name() != "Windows":
- return is_port_in_use_via_scrape(port)
- return false
- return is_port_in_use_via_scrape(port)
-
-
-## `trace` mirrors `find_all_pids_on_port`'s hook: one call per OS
-## invocation with the counter name of the scraper that actually ran, so
-## a wrapping caller's startup trace sees a genuine PowerShell fallback
-## as `powershell`, not as a silent extra second under `netstat`.
-static func is_port_in_use_via_scrape(port: int, trace: Callable = Callable()) -> bool:
- var output: Array = []
- if OS.get_name() == "Windows":
- _trace(trace, "netstat")
- var exit_code := OS.execute("netstat", ["-ano"], output, true)
- if exit_code == 0 and output.size() > 0:
- var stdout := str(output[0])
- if parse_windows_netstat_listening(stdout, port):
- return true
- ## A healthy dump with no listener row IS the answer — don't
- ## pay the ~1.2s powershell.exe spawn to confirm "not in use"
- ## (see find_all_pids_on_port for the cost rationale).
- if windows_netstat_dump_parseable(stdout):
- return false
- ## Fallback: netstat can be absent or unparseable on
- ## stripped/locale-odd Windows installs.
- _trace(trace, "powershell")
- return not find_listener_pids_windows(port).is_empty()
- _trace(trace, "lsof")
- var exit_code := OS.execute("lsof", ["-ti:%d" % port, "-sTCP:LISTEN"], output, true)
- if exit_code == 0 and output.size() > 0 and not output[0].strip_edges().is_empty():
- return true
- if OS.get_name() != "Linux":
- return false
- _trace(trace, "ss")
- output.clear()
- return (
- OS.execute("ss", ["-H", "-ltnp"], output, true) == 0
- and not parse_linux_ss_pids(str(output[0]) if not output.is_empty() else "", port).is_empty()
- )
-
-
-## Return the PID currently listening on the given TCP port, or 0 if
-## the port is free. Thin convenience wrapper around `find_all_pids_on_port`
-## — the per-OS scraping logic lives in one place.
-static func find_pid_on_port(port: int, trace: Callable = Callable()) -> int:
- var pids := find_all_pids_on_port(port, trace)
- return pids[0] if not pids.is_empty() else 0
-
-
-## Returns every PID bound LISTEN on `port`. Used by the kill paths so
-## both the uvicorn reloader parent AND its worker child are caught when
-## both bind the same port.
-##
-## `trace` is an optional Callable that fires once per OS invocation with
-## a counter name (`"netstat"` / `"powershell"` / `"lsof"` / `"ss"`) so the plugin
-## can keep its cold-start trace accurate. The Windows path may fall
-## through netstat → PowerShell, and a wrapping caller can't see which
-## scraper actually ran without the hook.
-static func find_all_pids_on_port(port: int, trace: Callable = Callable()) -> Array[int]:
- if OS.get_name() == "Windows":
- var output: Array = []
- _trace(trace, "netstat")
- var exit_code := OS.execute("netstat", ["-ano"], output, true)
- if exit_code == 0 and not output.is_empty():
- var stdout := str(output[0])
- var netstat_pids := parse_windows_netstat_pids(stdout, port)
- if not netstat_pids.is_empty():
- return netstat_pids
- ## An empty per-port parse from a healthy dump IS the answer
- ## ("no listener"). Confirming it through the PowerShell probe
- ## costs a powershell.exe spawn (~1.2s measured) against ~30ms
- ## for the netstat scrape — two such confirmations dominated a
- ## ~7s Windows startup walk. Only fall through when the dump
- ## itself is unusable (netstat absent, or so format-odd that
- ## zero TCP rows parse).
- if windows_netstat_dump_parseable(stdout):
- var no_listeners: Array[int] = []
- return no_listeners
- _trace(trace, "powershell")
- return find_listener_pids_windows(port)
- var output: Array = []
- _trace(trace, "lsof")
- var exit_code := OS.execute("lsof", ["-ti:%d" % port, "-sTCP:LISTEN"], output, true)
- if exit_code == 0 and not output.is_empty():
- var lsof_pids := parse_lsof_pids(str(output[0]))
- if not lsof_pids.is_empty() or OS.get_name() != "Linux":
- return lsof_pids
- elif OS.get_name() != "Linux":
- var empty: Array[int] = []
- return empty
- _trace(trace, "ss")
- output.clear()
- if OS.execute("ss", ["-H", "-ltnp"], output, true) != 0 or output.is_empty():
- var empty: Array[int] = []
- return empty
- return parse_linux_ss_pids(str(output[0]), port)
-
-
-static func _trace(trace: Callable, counter: String) -> void:
- if trace.is_valid():
- trace.call(counter)
-
-
-static func find_listener_pids_windows(port: int) -> Array[int]:
- var script := (
- "Get-NetTCPConnection -LocalPort %d -State Listen "
- + "-ErrorAction SilentlyContinue | "
- + "Select-Object -ExpandProperty OwningProcess"
- ) % port
- var output: Array = []
- var exit_code := execute_windows_powershell(script, output)
- return windows_listener_pids_from_execute_result(exit_code, output)
-
-
-static func execute_windows_powershell(script: String, output: Array) -> int:
- var args := ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script]
- for exe in windows_powershell_candidates():
- output.clear()
- var exit_code := OS.execute(exe, args, output, true)
- if exit_code == 0:
- return exit_code
- return -1
-
-
-static func windows_powershell_candidates() -> Array[String]:
- var candidates: Array[String] = []
- ## Prefer an already-installed PowerShell 7 at its fixed machine path.
- ## Missing or failing installations retain every existing fallback below.
- for variable in ["ProgramW6432", "ProgramFiles"]:
- var program_files := OS.get_environment(variable).replace("\\", "/").trim_suffix("/")
- if not program_files.is_empty():
- var pwsh := program_files.path_join("PowerShell/7/pwsh.exe")
- if FileAccess.file_exists(pwsh) and not candidates.has(pwsh):
- candidates.append(pwsh)
- var system_root := OS.get_environment("SystemRoot")
- if system_root.is_empty():
- system_root = "C:/Windows"
- system_root = system_root.replace("\\", "/").trim_suffix("/")
- candidates.append(system_root + "/System32/WindowsPowerShell/v1.0/powershell.exe")
- candidates.append("powershell.exe")
- candidates.append("pwsh.exe")
- return candidates
-
-
-static func windows_listener_pids_from_execute_result(exit_code: int, output: Array) -> Array[int]:
- var empty: Array[int] = []
- if exit_code == 0 and not output.is_empty():
- return parse_pid_lines(str(output[0]))
- return empty
-
-
-static func windows_listener_execute_result_in_use(exit_code: int, output: Array) -> bool:
- return not windows_listener_pids_from_execute_result(exit_code, output).is_empty()
-
-
-## Pure parser for `lsof -ti` output — newline-separated decimal PIDs.
-## Empty lines, non-positive values, non-numeric tokens, and duplicate
-## IPv4/IPv6 rows for the same listener are dropped.
-static func parse_lsof_pids(raw: String) -> Array[int]:
- return parse_pid_lines(raw)
-
-
-## Parse Linux `ss -H -ltnp` without trusting process names or localized
-## state text. The local endpoint is the fourth whitespace-delimited field;
-## every `pid=` value on an exact-port row is retained for later brand,
-## lineage, and fingerprint checks.
-static func parse_linux_ss_pids(raw: String, port: int) -> Array[int]:
- var result: Array[int] = []
- if port < 1 or port > 65535:
- return result
- var expression := RegEx.new()
- if expression.compile("(?:^|,)pid=([0-9]+)(?:,|\\))") != OK:
- return result
- for line in raw.split("\n", false):
- var fields := line.split(" ", false)
- if fields.size() < 4 or not str(fields[3]).ends_with(":%d" % port):
- continue
- for matched in expression.search_all(line):
- var pid := int(matched.get_string(1))
- if pid > 0 and not result.has(pid):
- result.append(pid)
- return result
-
-
-static func parse_pid_lines(raw: String) -> Array[int]:
- var pids: Array[int] = []
- for line in raw.strip_edges().split("\n", false):
- var stripped := line.strip_edges()
- if stripped.is_valid_int():
- var pid := int(stripped)
- if pid > 0 and not pids.has(pid):
- pids.append(pid)
- return pids
-
-
-## Parse a Windows `netstat -ano` dump and return PIDs of rows whose
-## local address ends with `:port` AND state is `LISTENING`. Substring
-## matching the whole dump is wrong: a remote address containing
-## `:port` would false-positive against an unrelated ESTABLISHED row.
-static func parse_windows_netstat_pid(stdout: String, port: int) -> int:
- var pids := parse_windows_netstat_pids(stdout, port)
- return pids[0] if not pids.is_empty() else 0
-
-
-static func parse_windows_netstat_pids(stdout: String, port: int) -> Array[int]:
- var pids: Array[int] = []
- var port_suffix := ":%d" % port
- for line in stdout.split("\n"):
- var s := line.strip_edges()
- if s.is_empty():
- continue
- var fields := split_on_whitespace(s)
- if fields.size() < 5: # proto, local, remote, state, pid
- continue
- ## Locale-independent listener signal (mirrors script/_dev_env.py):
- ## the state column is localized ("LISTENING"/"ABHÖREN"/"ÉCOUTE"...),
- ## but a listener's FOREIGN address is always the wildcard ":0".
- if not fields[2].ends_with(":0"):
- continue
- if not fields[1].ends_with(port_suffix):
- continue
- var pid_str := fields[fields.size() - 1]
- if pid_str.is_valid_int():
- var pid := int(pid_str)
- if pid > 0 and not pids.has(pid):
- pids.append(pid)
- return pids
-
-
-static func parse_windows_netstat_listening(stdout: String, port: int) -> bool:
- return parse_windows_netstat_pid(stdout, port) > 0
-
-
-## True when `stdout` looks like a healthy `netstat -ano` dump: at least
-## one row parses as a TCP connection (proto column literally "TCP", an
-## address containing ":", an integer PID in the last column). Locale-
-## independent — protocol names are never localized, unlike the state
-## column. Gates whether an empty per-port parse can be trusted as "no
-## listener": a live Windows host always carries TCP rows (svchost/RPC
-## listen on 135 at minimum), so a dump with zero parseable rows means
-## netstat itself is absent/broken and the PowerShell fallback must run.
-static func windows_netstat_dump_parseable(stdout: String) -> bool:
- for line in stdout.split("\n"):
- var fields := split_on_whitespace(line.strip_edges())
- if fields.size() < 5:
- continue
- if fields[0].to_upper() != "TCP":
- continue
- if fields[1].find(":") < 0:
- continue
- if fields[fields.size() - 1].is_valid_int():
- return true
- return false
-
-
-## `String.split(" ", false)` only splits on single spaces; netstat
-## columns are separated by runs of spaces / tabs. Collapse manually.
-static func split_on_whitespace(s: String) -> PackedStringArray:
- var out: PackedStringArray = []
- var cur := ""
- for i in s.length():
- var c := s.substr(i, 1)
- if c == " " or c == "\t":
- if not cur.is_empty():
- out.append(cur)
- cur = ""
- else:
- cur += c
- if not cur.is_empty():
- out.append(cur)
- return out
-
-
-static func read_pid_file(path := SERVER_PID_FILE) -> int:
- if not FileAccess.file_exists(path):
- return 0
- var f := FileAccess.open(path, FileAccess.READ)
- if f == null:
- return 0
- var content := f.get_as_text().strip_edges()
- f.close()
- if content.is_empty() or not content.is_valid_int():
- return 0
- var pid := int(content)
- return pid if pid > 0 else 0
-
-
-static func clear_pid_file() -> void:
- if FileAccess.file_exists(SERVER_PID_FILE):
- DirAccess.remove_absolute(ProjectSettings.globalize_path(SERVER_PID_FILE))
-
-
-## `kill -0` returns 0 for both running and zombie processes; Godot
-## never `waitpid`s on `OS.create_process` children, so a fast-failing
-## uvx launcher lingers as a zombie forever and `kill -0` would block
-## the spawn-failure branch in check_server_health from firing. Use
-## `ps -o stat=` instead. State codes: R/S/D/I/T (live), Z (zombie). #172.
-static func pid_alive(pid: int, snapshot: Variant = null) -> bool:
- if pid <= 0:
- return false
- if snapshot != null:
- return not _process_snapshot_row(snapshot, pid).is_empty()
- if OS.get_name() == "Windows":
- var output: Array = []
- var exit_code := OS.execute("tasklist", ["/FI", "PID eq %d" % pid, "/NH", "/FO", "CSV"], output, true)
- if exit_code != 0 or output.is_empty():
- return false
- for line in output:
- if str(line).find("\"%d\"" % pid) >= 0:
- return true
- return false
- var output: Array = []
- var exit_code := OS.execute("ps", ["-p", str(pid), "-o", "stat="], output, true)
- if exit_code != 0 or output.is_empty():
- return false
- var stat := str(output[0]).strip_edges()
- return not stat.is_empty() and not stat.begins_with("Z")
-
-
-## Read-only process identity helpers. Lifecycle authority is granted only
-## after capturing a fingerprint and every later stop re-reads the fingerprint
-## before acting, so PID reuse fails closed.
-## One Windows query captures the target and at most fifteen ancestors. The
-## dictionary belongs to one proof boundary, never a cache: callers must take
-## a fresh snapshot when closing the capture window or authorizing a kill.
-## Non-Windows callers receive null and keep the existing live-query path.
-## The process enumeration stays inside PowerShell; only the bounded target
-## ancestry crosses back into Godot, without logging command-line metadata.
-static func capture_process_snapshot(pid: int) -> Variant:
- if OS.get_name() != "Windows":
- return null
- if pid <= 1:
- return {"capture_error": true}
- var script := _windows_process_snapshot_script(pid)
- var output: Array = []
- if execute_windows_powershell(script, output) != 0 or output.is_empty():
- return {"capture_error": true}
- return parse_process_snapshot(str(output[0]), pid)
-
-
-static func _windows_process_snapshot_script(pid: int) -> String:
- return (
- "$rows = @(); $current = %d; $seen = @{}; $capture_failed = $false; "
- + "$processes = @{}; try { Get-CimInstance Win32_Process -ErrorAction Stop | "
- + "ForEach-Object { $processes[[int]$_.ProcessId] = $_ } } catch {}; "
- + "for ($depth = 0; $depth -lt 16 -and $current -gt 1; $depth++) { "
- + "if ($seen.ContainsKey($current)) { break }; $seen[$current] = $true; "
- + "$p = $processes[$current]; "
- + "if ($null -eq $p) { "
- + "try { $identity = [string](Get-Process -Id $current -ErrorAction Stop).StartTime.ToFileTimeUtc(); "
- + "$rows += @{pid=$current;parent_pid=0;identity=$identity;commandline=''} } catch { if ($_.CategoryInfo.Category -ne 'ObjectNotFound') { $capture_failed = $true } }; break }; "
- + "$rows += @{pid=[int]$p.ProcessId;parent_pid=[int]$p.ParentProcessId; "
- + "identity=([string]$p.CreationDate + '|' + [string]$p.CommandLine);commandline=[string]$p.CommandLine}; "
- + "$current = [int]$p.ParentProcessId }; "
- + "if ($capture_failed) { 'null' } else { ConvertTo-Json -InputObject @($rows) -Compress -Depth 3 }"
- ) % pid
-
-
-## Independent CIM maps in separate scriptblock scopes are
-## captured within one shell invocation. No snapshot crosses a grant call.
-static func _capture_windows_process_snapshot_pair(pid: int) -> Array:
- var query := _windows_process_snapshot_script(pid)
- var script := (
- "$first = & { %s }; $final = & { %s }; "
- + "ConvertTo-Json -InputObject @([string]$first,[string]$final) -Compress"
- ) % [query, query]
- var output: Array = []
- if execute_windows_powershell(script, output) != 0 or output.is_empty():
- return [{"capture_error": true}, {"capture_error": true}]
- return _parse_process_snapshot_pair(str(output[0]), pid)
-
-
-static func _parse_process_snapshot_pair(raw: String, pid: int) -> Array:
- if raw.length() > 4 * 1024 * 1024 + 16:
- return [{"capture_error": true}, {"capture_error": true}]
- var json := JSON.new()
- if json.parse(raw) != OK:
- return [{"capture_error": true}, {"capture_error": true}]
- var pair: Variant = json.data
- if not (pair is Array) or pair.size() != 2 or not (pair[0] is String and pair[1] is String):
- return [{"capture_error": true}, {"capture_error": true}]
- return [parse_process_snapshot(pair[0], pid), parse_process_snapshot(pair[1], pid)]
-
-
-static func parse_process_snapshot(raw: String, expected_pid: int) -> Dictionary:
- if raw.length() > 1024 * 1024:
- return {"capture_error": true}
- var json := JSON.new()
- if json.parse(raw) != OK:
- return {"capture_error": true}
- var rows: Variant = json.data
- if not (rows is Array) or rows.size() > 16:
- return {"capture_error": true}
- var snapshot := {}
- var next_pid := expected_pid
- for row in rows:
- if not (row is Dictionary):
- return {"capture_error": true}
- var value: Variant = row.get("pid")
- if (
- not (value is int or value is float) or not is_finite(float(value))
- or float(value) <= 1.0 or float(value) > 4294967295.0
- or float(value) != float(int(value))
- ):
- return {"capture_error": true}
- var pid := int(value)
- if pid <= 1 or pid != next_pid or snapshot.has(pid):
- return {"capture_error": true}
- snapshot[pid] = row
- if _process_snapshot_row(snapshot, pid).is_empty():
- return {"capture_error": true}
- next_pid = int(row.parent_pid)
- if snapshot.has(next_pid):
- return {"capture_error": true}
- return snapshot
-
-
-## An unavailable capture is not evidence that its PID has exited.
-static func capture_failed(snapshot: Variant) -> bool:
- return snapshot is Dictionary and bool(snapshot.get("capture_error", false))
-
-
-static func _process_snapshot_row(snapshot: Variant, pid: int) -> Dictionary:
- if not (snapshot is Dictionary) or capture_failed(snapshot) or pid <= 1:
- return {}
- var row: Variant = snapshot.get(pid)
- if not (row is Dictionary) or row.size() != 4:
- return {}
- var parent: Variant = row.get("parent_pid")
- if (
- row.get("pid") != pid
- or not (parent is int or parent is float)
- or not is_finite(float(parent)) or float(parent) < 0.0 or float(parent) > 4294967295.0
- or float(parent) != float(int(parent))
- or not (row.get("identity") is String) or str(row.identity).strip_edges().is_empty()
- or not (row.get("commandline") is String)
- ):
- return {}
- var identity := str(row.identity)
- var commandline := str(row.commandline)
- var separator := identity.find("|")
- if separator >= 0:
- if separator == 0 or identity.substr(separator + 1) != commandline:
- return {}
- elif not identity.is_valid_int() or not commandline.is_empty() or int(parent) != 0:
- return {}
- return row
-
-
-static func process_commandline(pid: int, snapshot: Variant = null) -> String:
- if pid <= 1:
- return ""
- if snapshot != null:
- return str(_process_snapshot_row(snapshot, pid).get("commandline", ""))
- if OS.get_name() == "Windows":
- var output: Array = []
- var script := (
- "Get-CimInstance Win32_Process -Filter 'ProcessId = %d' | "
- + "Select-Object -ExpandProperty CommandLine"
- ) % pid
- if execute_windows_powershell(script, output) != 0 or output.is_empty():
- return ""
- return str(output[0]).strip_edges()
- var proc_path := "/proc/%d/cmdline" % pid
- if FileAccess.file_exists(proc_path):
- var file := FileAccess.open(proc_path, FileAccess.READ)
- if file != null:
- var bytes := PackedByteArray()
- while bytes.size() < (1 << 20):
- var chunk := file.get_buffer(4096)
- if chunk.is_empty():
- break
- bytes.append_array(chunk)
- if file.eof_reached():
- break
- file.close()
- for index in range(bytes.size()):
- if bytes[index] == 0:
- bytes[index] = 0x20
- return bytes.get_string_from_utf8().strip_edges()
- var output: Array = []
- if OS.execute("ps", ["-ww", "-p", str(pid), "-o", "args="], output, true) != 0:
- return ""
- return str(output[0]).strip_edges() if not output.is_empty() else ""
-
-
-static func process_parent(pid: int, snapshot: Variant = null) -> int:
- if pid <= 1:
- return 0
- if snapshot != null:
- return int(_process_snapshot_row(snapshot, pid).get("parent_pid", 0))
- var output: Array = []
- if OS.get_name() == "Windows":
- var script := (
- "Get-CimInstance Win32_Process -Filter 'ProcessId = %d' | "
- + "Select-Object -ExpandProperty ParentProcessId"
- ) % pid
- if execute_windows_powershell(script, output) != 0 or output.is_empty():
- return 0
- else:
- if OS.execute("ps", ["-o", "ppid=", "-p", str(pid)], output, true) != 0 or output.is_empty():
- return 0
- var raw := str(output[0]).strip_edges()
- return int(raw) if raw.is_valid_int() else 0
-
-
-static func process_descends_from(pid: int, ancestor_pid: int, snapshot: Variant = null) -> bool:
- if pid <= 1 or ancestor_pid <= 1:
- return false
- if snapshot != null and _process_snapshot_row(snapshot, pid).is_empty():
- return false
- var current := pid
- for _depth in range(16):
- if snapshot != null and _process_snapshot_row(snapshot, current).is_empty():
- return false
- if current == ancestor_pid:
- return true
- var parent := process_parent(current, snapshot)
- if parent <= 1 or parent == current:
- return false
- current = parent
- return false
-
-
-static func commandline_is_godot_ai_server(commandline: String) -> bool:
- if commandline.is_empty():
- return false
- var lower := commandline.to_lower()
- var expression := RegEx.new()
- var brand_search := lower
- ## Paths we choose ourselves must not satisfy the brand: strip the pid-file
- ## and startup-report values (they live under user:// and carry our name)
- ## before searching for it.
- if expression.compile("--(pid-file|startup-report)(?:=|\\s+)\\S+") == OK:
- brand_search = expression.sub(lower, "--$1 ", true)
- var branded := brand_search.contains("godot-ai") or brand_search.contains("godot_ai")
- return branded and (lower.contains("--pid-file") or lower.contains("--transport"))
-
-
-static func pid_cmdline_is_godot_ai(pid: int, snapshot: Variant = null) -> bool:
- var current := pid
- for _depth in range(5):
- if current <= 1:
- return false
- if commandline_is_godot_ai_server(process_commandline(current, snapshot)):
- return true
- current = process_parent(current, snapshot)
- return false
-
-
-static func process_fingerprint(pid: int, snapshot: Variant = null) -> String:
- if not pid_alive(pid, snapshot):
- return ""
- var output: Array = []
- var identity := ""
- if snapshot != null:
- identity = str(_process_snapshot_row(snapshot, pid).get("identity", "")).strip_edges()
- elif OS.get_name() == "Windows":
- var script := (
- "Get-CimInstance Win32_Process -Filter 'ProcessId = %d' | "
- + "ForEach-Object { [string]$_.CreationDate + '|' + [string]$_.CommandLine }"
- ) % pid
- if execute_windows_powershell(script, output) == 0 and not output.is_empty():
- identity = str(output[0]).strip_edges()
- if identity.is_empty():
- ## Some restricted Windows hosts expose process start time through
- ## Get-Process while CIM returns no Win32_Process projection. PID +
- ## creation time is the reuse-resistant identity; command-line brand
- ## is checked independently before destructive authority is minted.
- output.clear()
- script = (
- "(Get-Process -Id %d -ErrorAction Stop).StartTime.ToFileTimeUtc()"
- ) % pid
- if execute_windows_powershell(script, output) == 0 and not output.is_empty():
- identity = str(output[0]).strip_edges()
- else:
- if OS.execute(
- "ps", ["-ww", "-p", str(pid), "-o", "lstart=", "-o", "args="], output, true
- ) == 0 and not output.is_empty():
- identity = str(output[0]).strip_edges()
- if identity.is_empty():
- return ""
- var context := HashingContext.new()
- context.start(HashingContext.HASH_SHA256)
- context.update((str(pid) + "|" + identity).to_utf8_buffer())
- return context.finish().hex_encode()
-
-
-## Capture the process identity that a later destructive call must present.
-## A PID or command-line brand alone is never kill authority: either can refer
-## to a different process by the time a worker reaches the effect boundary.
-## `diagnostics`, when given, receives one word naming the check that
-## refused the grant. It costs no extra probe and never changes the result.
-static func capture_process_kill_grant(
- pid: int, require_brand := false, diagnostics: Array = []
-) -> Dictionary:
- if pid <= 1 or pid == OS.get_process_id():
- diagnostics.append("invalid_pid")
- return {}
- var pair: Array = _capture_windows_process_snapshot_pair(pid) if OS.get_name() == "Windows" else []
- var first: Variant = pair[0] if not pair.is_empty() else capture_process_snapshot(pid)
- if capture_failed(first):
- diagnostics.append("identity_unavailable")
- return {}
- if not pid_alive(pid, first):
- diagnostics.append("not_alive")
- return {}
- if require_brand and not pid_cmdline_is_godot_ai(pid, first):
- diagnostics.append("unbranded")
- return {}
- var fingerprint := process_fingerprint(pid, first)
- if fingerprint.is_empty():
- diagnostics.append("fingerprint_unavailable")
- return {}
- ## Close the capture window: both identity and optional lineage/brand must
- ## still describe the same process in the independently collected final snapshot.
- var final: Variant = pair[1] if not pair.is_empty() else capture_process_snapshot(pid)
- if capture_failed(final):
- diagnostics.append("identity_unavailable")
- return {}
- if process_fingerprint(pid, final) != fingerprint:
- diagnostics.append("fingerprint_changed")
- return {}
- if require_brand and not pid_cmdline_is_godot_ai(pid, final):
- diagnostics.append("brand_changed")
- return {}
- return {"pid": pid, "fingerprint": fingerprint}
-
-
-## Kill only exact process grants and re-read identity immediately before the
-## OS call. On Windows, taskkill /T already handles real descendants; do not
-## append command-line lookalikes such as spoofed multiprocessing children.
-static func kill_exact_processes(
- grants: Array[Dictionary], require_brand := false, require_tree_proof := false
-) -> Array[int]:
- var killed: Array[int] = []
- var seen: Array[int] = []
- for grant in grants:
- if grant.keys().size() != 2 or not grant.has("pid") or not grant.has("fingerprint"):
- continue
- var pid := int(grant.get("pid", 0))
- var fingerprint := str(grant.get("fingerprint", ""))
- if pid <= 1 or pid == OS.get_process_id() or seen.has(pid) or fingerprint.is_empty():
- continue
- var snapshot: Variant = capture_process_snapshot(pid)
- if (
- process_fingerprint(pid, snapshot) != fingerprint
- or (require_brand and not pid_cmdline_is_godot_ai(pid, snapshot))
- ):
- continue
- seen.append(pid)
- if OS.get_name() == "Windows":
- var output: Array = []
- var code := OS.execute("taskkill", ["/PID", str(pid), "/T", "/F"], output, true)
- ## A vanished direct PID is enough for ordinary exact-process callers.
- ## Timeout safety needs stronger evidence: only taskkill /T success
- ## proves that Windows accepted termination of the whole child tree.
- if code == 0 or (not require_tree_proof and not pid_alive(pid)):
- killed.append(pid)
- elif OS.kill(pid) == OK or not pid_alive(pid):
- killed.append(pid)
- return killed
-
-
-## Poll until the given port is no longer bound, or the timeout elapses.
-## Used after `OS.kill` so we don't race the port-in-use check on rebind.
-static func wait_for_port_free(port: int, timeout_s: float) -> void:
- var deadline := Time.get_ticks_msec() + int(timeout_s * 1000.0)
- while is_port_in_use(port):
- if Time.get_ticks_msec() >= deadline:
- push_warning("MCP | port %d still in use after %.1fs — proceeding anyway" % [port, timeout_s])
- return
- OS.delay_msec(100)
-
-
-## Choose a non-Windows-reserved WS port. Returns `configured` when free;
-## otherwise the first non-excluded port within `span` of it. Optional
-## `log_buffer` is a duck-typed sink (`log(String)`) that gets the
-## remap notice so users see why the port shifted.
-static func resolve_ws_port(configured: int, max_port: int, log_buffer = null) -> int:
- var resolved := WindowsPortReservation.suggest_non_excluded_port(
- configured,
- 2048,
- max_port
- )
- if resolved != configured:
- var message := "WebSocket port %d is reserved by Windows; using %d" % [configured, resolved]
- print("MCP | %s" % message)
- if log_buffer != null:
- log_buffer.log(message)
- return resolved
-
-
-static func resolve_ws_port_from_output(
- configured_port: int,
- netsh_output: String,
- max_port: int,
- span: int = 2048
-) -> int:
- return WindowsPortReservation.suggest_non_excluded_port_from_output(
- netsh_output,
- configured_port,
- span,
- max_port
- )
diff --git a/addons/godot_ai/utils/port_resolver.gd.uid b/addons/godot_ai/utils/port_resolver.gd.uid
deleted file mode 100644
index 54a3d73..0000000
--- a/addons/godot_ai/utils/port_resolver.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://pk0212qfh61x
diff --git a/addons/godot_ai/utils/release_verifier.gd b/addons/godot_ai/utils/release_verifier.gd
deleted file mode 100644
index 77ce158..0000000
--- a/addons/godot_ai/utils/release_verifier.gd
+++ /dev/null
@@ -1,876 +0,0 @@
-@tool
-class_name McpReleaseVerifier
-extends RefCounted
-
-## Pure, static verification of a signed v4 release: the canonical manifest,
-## its RSA signature, the archive, and the tree hash the installer compares
-## after extraction and again after restart.
-##
-## Every rule here mirrors `src/godot_ai/release_verify.py`. The two must stay
-## byte-for-byte compatible: the release pipeline produces the manifest with
-## Python's `_canonical()` and the installed-tree hash with Python's
-## `hash_tree()`, and this file re-derives both from raw bytes. Every function
-## fails closed with a specific error string and never raises.
-
-const REPOSITORY := "hi-godot/godot-ai"
-const CHANNEL := "stable"
-const ASSET_NAME := "godot-ai-v4-plugin.zip"
-const PLUGIN_PREFIX := "addons/godot_ai/"
-const PLUGIN_CONFIG := "addons/godot_ai/plugin.cfg"
-const SCHEMA_VERSION := 1
-const MAX_FILES := 4096
-const MAX_FILE_SIZE := 64 * 1024 * 1024
-const MAX_ARCHIVE_SIZE := 64 * 1024 * 1024
-const MAX_TREE_SIZE := MAX_ARCHIVE_SIZE
-const MAX_MANIFEST_SIZE := 1024 * 1024
-## Signature length for the production RSA-4096 key. The check itself is
-## derived from the supplied key's modulus so a smaller injected test key
-## still verifies its own (shorter) signatures.
-const SIGNATURE_SIZE := 512
-const MIN_RSA_MODULUS_BYTES := 256
-## Largest integer JSON can round-trip through Godot's double-based parser.
-const MAX_SAFE_INTEGER := 9007199254740992
-const READ_CHUNK := 1024 * 1024
-
-const ROOT_KEYS := [
- "asset", "channel", "inventory", "repository", "schema_version", "source_commit", "tag", "version",
-]
-const ASSET_KEYS := ["name", "sha256", "size"]
-const ROW_KEYS := ["path", "sha256", "size"]
-const RESERVED_NAMES := [
- "CON", "PRN", "AUX", "NUL", "CONIN$", "CONOUT$",
- "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
- "COM¹", "COM²", "COM³",
- "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
- "LPT¹", "LPT²", "LPT³",
-]
-const _UNSAFE_PATH_CHARS := "<>:\"|?*"
-## DER-encoded AlgorithmIdentifier for rsaEncryption with NULL parameters.
-const _RSA_ALGORITHM := [0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00]
-
-
-# ----- public API -------------------------------------------------------
-
-
-## Authenticate raw manifest bytes against a detached signature and the
-## caller's explicit expectations. `expected` carries `repository` and
-## `channel`, optionally `version` (must equal the manifest's), and optionally
-## `current_version` (the manifest must be strictly newer). Returns
-## `{"ok": bool, "error": String, "manifest": Dictionary}`; the returned
-## manifest has integer sizes normalised to `int`.
-static func verify_manifest(
- manifest_bytes: PackedByteArray,
- signature: PackedByteArray,
- expected: Dictionary,
- public_key_pem: String,
-) -> Dictionary:
- var result := {"ok": false, "error": "", "manifest": {}}
- if manifest_bytes.is_empty() or manifest_bytes.size() > MAX_MANIFEST_SIZE:
- result.error = "manifest: expected 1..%d bytes, got %d" % [MAX_MANIFEST_SIZE, manifest_bytes.size()]
- return result
- if not is_valid_utf8(manifest_bytes):
- result.error = "manifest: not valid UTF-8"
- return result
- var json := JSON.new()
- var parse_error := json.parse(manifest_bytes.get_string_from_utf8())
- if parse_error != OK:
- result.error = "manifest: invalid JSON: %s (line %d)" % [json.get_error_message(), json.get_error_line()]
- return result
- var canonical := canonical_json(json.data)
- if not bool(canonical.ok):
- result.error = "manifest: encoding is not canonical JSON: %s" % str(canonical.error)
- return result
- if canonical.bytes != manifest_bytes:
- result.error = "manifest: encoding is not canonical JSON"
- return result
- if not json.data is Dictionary:
- result.error = "manifest: expected a JSON object"
- return result
- var validated := validate_manifest_dict(json.data)
- if not bool(validated.ok):
- result.error = str(validated.error)
- return result
- var manifest: Dictionary = validated.manifest
- var identity_error := _check_identity(manifest, expected)
- if not identity_error.is_empty():
- result.error = identity_error
- return result
- var signature_error := verify_signature(manifest_bytes, signature, public_key_pem)
- if not signature_error.is_empty():
- result.error = signature_error
- return result
- result.ok = true
- result.manifest = manifest
- return result
-
-
-## Check a zip on disk against an already-verified manifest: whole-file size
-## and SHA-256 against `manifest.asset`, then the entry list against the
-## inventory (exact names, exact order, no directory entries), then every
-## entry's size and SHA-256 against its row. Returns `{"ok", "error"}`.
-static func verify_archive(zip_path: String, manifest: Dictionary) -> Dictionary:
- var result := {"ok": false, "error": ""}
- var validated := validate_manifest_dict(manifest)
- if not bool(validated.ok):
- result.error = str(validated.error)
- return result
- var normalised: Dictionary = validated.manifest
- var asset: Dictionary = normalised["asset"]
- var hashed := sha256_file(zip_path)
- if not bool(hashed.ok):
- result.error = "archive: %s" % str(hashed.error)
- return result
- if int(hashed["size"]) != int(asset["size"]):
- result.error = "archive: size %d differs from signed manifest size %d" % [int(hashed["size"]), int(asset["size"])]
- return result
- if str(hashed.sha256) != str(asset["sha256"]):
- result.error = "archive: SHA-256 differs from signed manifest"
- return result
- var reader := ZIPReader.new()
- var open_error := reader.open(_absolute(zip_path))
- if open_error != OK:
- result.error = "archive: cannot open zip: %s" % error_string(open_error)
- return result
- var entries := reader.get_files()
- var rows: Array = normalised["inventory"]
- var expected_paths: Array[String] = []
- var expected_rows := {}
- for row in rows:
- expected_paths.append(str(row["path"]))
- expected_rows[str(row["path"])] = row
- var listing_error := _check_entry_names(entries, expected_paths, expected_rows)
- if not listing_error.is_empty():
- reader.close()
- result.error = listing_error
- return result
- var total := 0
- var config := PackedByteArray()
- for entry in entries:
- var row: Dictionary = expected_rows[entry]
- var data := reader.read_file(entry)
- if data.size() != int(row["size"]):
- reader.close()
- result.error = "archive entry %s: size %d differs from inventory size %d" % [entry, data.size(), int(row["size"])]
- return result
- if sha256_bytes(data) != str(row["sha256"]):
- reader.close()
- result.error = "archive entry %s: content differs from inventory" % entry
- return result
- total += data.size()
- if total > MAX_TREE_SIZE:
- reader.close()
- result.error = "archive: expanded tree exceeds %d-byte bound" % MAX_TREE_SIZE
- return result
- if entry == PLUGIN_CONFIG:
- config = data
- reader.close()
- var config_version := plugin_cfg_version(config)
- if config_version != str(normalised["version"]):
- result.error = "archive: plugin.cfg version %s differs from signed manifest version %s" % [
- config_version if not config_version.is_empty() else "(unreadable)", str(normalised["version"]),
- ]
- return result
- result.ok = true
- return result
-
-
-## Hash every regular file under `root` recursively. Returns
-## `{"ok", "error", "files": {relative_path: {"size", "sha256"}}, "tree_sha256"}`.
-##
-## Tree-hash algorithm (shared verbatim with Python `release_verify.hash_tree`):
-## relative POSIX paths sorted by UTF-8 byte order; for each file one line
-## `sha256 + " " + str(size) + " " + path + "\n"`; `tree_sha256` is the hex
-## SHA-256 of those lines concatenated. Symlinks anywhere in the tree fail.
-static func hash_tree(root: String) -> Dictionary:
- var result := {"ok": false, "error": "", "files": {}, "tree_sha256": ""}
- var root_abs := _absolute(root)
- if not DirAccess.dir_exists_absolute(root_abs):
- result.error = "tree: directory does not exist: %s" % root_abs
- return result
- var files := {}
- var pending: Array[String] = [""]
- var total := 0
- while not pending.is_empty():
- var relative: String = pending.pop_back()
- var directory_abs := root_abs if relative.is_empty() else root_abs.path_join(relative)
- var directory := DirAccess.open(directory_abs)
- if directory == null:
- result.error = "tree: cannot open %s: %s" % [directory_abs, error_string(DirAccess.get_open_error())]
- return result
- directory.include_hidden = true
- directory.include_navigational = false
- for name in directory.get_directories():
- var child_abs := directory_abs.path_join(name)
- if directory.is_link(child_abs):
- result.error = "tree: link is not allowed: %s" % child_abs
- return result
- pending.append(name if relative.is_empty() else relative + "/" + name)
- for name in directory.get_files():
- var child_abs := directory_abs.path_join(name)
- if directory.is_link(child_abs):
- result.error = "tree: link is not allowed: %s" % child_abs
- return result
- var hashed := sha256_file(child_abs)
- if not bool(hashed.ok):
- result.error = "tree: %s" % str(hashed.error)
- return result
- total += int(hashed["size"])
- if files.size() >= MAX_FILES or total > MAX_TREE_SIZE:
- result.error = "tree: exceeds %d files or %d bytes" % [MAX_FILES, MAX_TREE_SIZE]
- return result
- var relative_path := name if relative.is_empty() else relative + "/" + name
- files[relative_path] = {"size": int(hashed["size"]), "sha256": str(hashed.sha256)}
- var sorted_paths: Array[String] = []
- for path in files:
- sorted_paths.append(str(path))
- sorted_paths.sort()
- var ordered := {}
- for path in sorted_paths:
- ordered[path] = files[path]
- result.files = ordered
- result.tree_sha256 = tree_hash_from_files(ordered)
- result.ok = true
- return result
-
-
-## The tree hash a correct extraction of `manifest.inventory` must produce:
-## the same line algorithm as `hash_tree` over the inventory rows with the
-## `addons/godot_ai/` prefix stripped. Only `inventory` is read, and row
-## order does not matter. Returns "" for a malformed inventory or a row
-## outside `addons/godot_ai/`, which can never equal a real hash.
-static func inventory_tree_hash(manifest: Dictionary) -> String:
- if not manifest.has("inventory") or not manifest["inventory"] is Array:
- return ""
- var files := {}
- for value in manifest["inventory"]:
- if not value is Dictionary:
- return ""
- var row: Dictionary = value
- if not _check_keys(row, ROW_KEYS, "row").is_empty():
- return ""
- var size := _integer(row["size"], "row.size", MAX_FILE_SIZE)
- if not row["path"] is String or not bool(size.ok) or not row["sha256"] is String:
- return ""
- var path := str(row["path"])
- var digest := str(row["sha256"])
- if not path.begins_with(PLUGIN_PREFIX) or path.length() == PLUGIN_PREFIX.length() or not _is_hex(digest, 64):
- return ""
- var relative := path.substr(PLUGIN_PREFIX.length())
- if files.has(relative):
- return ""
- files[relative] = {"size": int(size.value), "sha256": digest}
- if files.is_empty():
- return ""
- return tree_hash_from_files(files)
-
-
-## Apply the tree-hash line algorithm to a `{relative_path: {size, sha256}}`
-## dictionary (the `files` shape `hash_tree` returns).
-static func tree_hash_from_files(files: Dictionary) -> String:
- var paths: Array[String] = []
- for path in files:
- paths.append(str(path))
- paths.sort()
- var context := HashingContext.new()
- context.start(HashingContext.HASH_SHA256)
- for path in paths:
- var entry: Dictionary = files[path]
- var line := "%s %d %s\n" % [str(entry["sha256"]), int(entry["size"]), path]
- context.update(line.to_utf8_buffer())
- return context.finish().hex_encode()
-
-
-## Hex SHA-256 of a byte buffer.
-static func sha256_bytes(bytes: PackedByteArray) -> String:
- var context := HashingContext.new()
- context.start(HashingContext.HASH_SHA256)
- if not bytes.is_empty():
- context.update(bytes)
- return context.finish().hex_encode()
-
-
-## Chunked SHA-256 of a file. Returns `{"ok", "error", "size", "sha256"}`;
-## files beyond MAX_FILE_SIZE fail without being read to the end.
-static func sha256_file(path: String) -> Dictionary:
- var result := {"ok": false, "error": "", "size": 0, "sha256": ""}
- var absolute := _absolute(path)
- var file := FileAccess.open(absolute, FileAccess.READ)
- if file == null:
- result.error = "cannot read %s: %s" % [absolute, error_string(FileAccess.get_open_error())]
- return result
- var context := HashingContext.new()
- context.start(HashingContext.HASH_SHA256)
- var size := 0
- while true:
- var chunk := file.get_buffer(READ_CHUNK)
- if chunk.is_empty():
- break
- size += chunk.size()
- if size > MAX_FILE_SIZE:
- file.close()
- result.error = "%s exceeds %d-byte bound" % [absolute, MAX_FILE_SIZE]
- return result
- context.update(chunk)
- var read_error := file.get_error()
- file.close()
- if read_error != OK and read_error != ERR_FILE_EOF:
- result.error = "cannot read %s: %s" % [absolute, error_string(read_error)]
- return result
- result.size = size
- result.sha256 = context.finish().hex_encode()
- result.ok = true
- return result
-
-
-## Serialise `value` exactly as Python's
-## `json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"`
-## encodes it: sorted keys, no whitespace, integers (including integral
-## floats, which is what Godot's JSON parser yields for them) as integers,
-## non-ASCII unescaped. Returns `{"ok", "error", "bytes"}`. Non-integral
-## numbers fail: the manifest schema has none and Godot's float formatting
-## is not guaranteed to match Python's.
-static func canonical_json(value: Variant) -> Dictionary:
- var result := {"ok": false, "error": "", "bytes": PackedByteArray()}
- var text := _canonical_value(value, result)
- if not str(result.error).is_empty():
- return result
- result.bytes = (text + "\n").to_utf8_buffer()
- result.ok = true
- return result
-
-
-## Strict UTF-8 validation: rejects overlong forms, surrogates, and code
-## points beyond U+10FFFF, the same input Python's `bytes.decode` rejects.
-static func is_valid_utf8(bytes: PackedByteArray) -> bool:
- var index := 0
- var size := bytes.size()
- while index < size:
- var lead := bytes[index]
- if lead < 0x80:
- index += 1
- continue
- var need := 0
- var code := 0
- if lead >= 0xC2 and lead <= 0xDF:
- need = 1
- code = lead & 0x1F
- elif lead >= 0xE0 and lead <= 0xEF:
- need = 2
- code = lead & 0x0F
- elif lead >= 0xF0 and lead <= 0xF4:
- need = 3
- code = lead & 0x07
- else:
- return false
- if index + need >= size:
- return false
- for offset in range(1, need + 1):
- var continuation := bytes[index + offset]
- if (continuation & 0xC0) != 0x80:
- return false
- code = (code << 6) | (continuation & 0x3F)
- if need == 2 and code < 0x800:
- return false
- if need == 3 and (code < 0x10000 or code > 0x10FFFF):
- return false
- if code >= 0xD800 and code <= 0xDFFF:
- return false
- index += need + 1
- return true
-
-
-## Validate a parsed manifest's schema, bounds, and inventory path safety
-## without checking identity or the signature. Returns
-## `{"ok", "error", "manifest"}` where `manifest` is a normalised copy with
-## integer fields as `int`.
-static func validate_manifest_dict(root: Dictionary) -> Dictionary:
- var result := {"ok": false, "error": "", "manifest": {}}
- var root_error := _check_keys(root, ROOT_KEYS, "manifest")
- if not root_error.is_empty():
- result.error = root_error
- return result
- var schema := _integer(root["schema_version"], "manifest.schema_version", SCHEMA_VERSION)
- if not bool(schema.ok):
- result.error = str(schema.error)
- return result
- if int(schema.value) != SCHEMA_VERSION:
- result.error = "manifest.schema_version: expected %d" % SCHEMA_VERSION
- return result
- var manifest := {"schema_version": SCHEMA_VERSION}
- for key in ["repository", "channel", "tag", "version", "source_commit"]:
- var text := _string(root[key], "manifest.%s" % key)
- if not bool(text.ok):
- result.error = str(text.error)
- return result
- manifest[key] = str(text.value)
- if not root["asset"] is Dictionary:
- result.error = "manifest.asset: expected object"
- return result
- var asset: Dictionary = root["asset"]
- var asset_error := _check_keys(asset, ASSET_KEYS, "manifest.asset")
- if not asset_error.is_empty():
- result.error = asset_error
- return result
- if not asset["name"] is String or str(asset["name"]) != ASSET_NAME:
- result.error = "manifest.asset.name: expected %s" % ASSET_NAME
- return result
- var asset_size := _integer(asset["size"], "manifest.asset.size", MAX_ARCHIVE_SIZE)
- if not bool(asset_size.ok):
- result.error = str(asset_size.error)
- return result
- if not asset["sha256"] is String or not _is_hex(str(asset["sha256"]), 64):
- result.error = "manifest.asset.sha256: expected 64 lowercase hex characters"
- return result
- manifest["asset"] = {"name": ASSET_NAME, "size": int(asset_size.value), "sha256": str(asset["sha256"])}
- if not root["inventory"] is Array:
- result.error = "manifest.inventory: expected array"
- return result
- var rows: Array = root["inventory"]
- var inventory: Array = []
- var paths: Array[String] = []
- var total := 0
- for index in rows.size():
- var context := "manifest.inventory[%d]" % index
- if not rows[index] is Dictionary:
- result.error = "%s: expected object" % context
- return result
- var row: Dictionary = rows[index]
- var row_error := _check_keys(row, ROW_KEYS, context)
- if not row_error.is_empty():
- result.error = row_error
- return result
- var path := _string(row["path"], context + ".path")
- if not bool(path.ok):
- result.error = str(path.error)
- return result
- var size := _integer(row["size"], context + ".size", MAX_FILE_SIZE)
- if not bool(size.ok):
- result.error = str(size.error)
- return result
- if not row["sha256"] is String or not _is_hex(str(row["sha256"]), 64):
- result.error = "%s.sha256: expected 64 lowercase hex characters" % context
- return result
- total += int(size.value)
- paths.append(str(path.value))
- inventory.append({"path": str(path.value), "size": int(size.value), "sha256": str(row["sha256"])})
- var paths_error := _validate_paths(paths, "manifest.inventory")
- if not paths_error.is_empty():
- result.error = paths_error
- return result
- if total > MAX_TREE_SIZE:
- result.error = "manifest.inventory: expanded tree exceeds %d-byte bound" % MAX_TREE_SIZE
- return result
- if not paths.has(PLUGIN_CONFIG):
- result.error = "manifest.inventory: missing %s" % PLUGIN_CONFIG
- return result
- manifest["inventory"] = inventory
- result.manifest = manifest
- result.ok = true
- return result
-
-
-## Verify a PKCS#1 v1.5 SHA-256 signature over `message` with a PEM
-## SubjectPublicKeyInfo. Returns "" on success, otherwise the reason.
-static func verify_signature(message: PackedByteArray, signature: PackedByteArray, public_key_pem: String) -> String:
- var modulus_size := rsa_modulus_size(public_key_pem)
- if modulus_size < MIN_RSA_MODULUS_BYTES:
- return "public key: expected a PEM RSA SubjectPublicKeyInfo of at least %d bits" % (MIN_RSA_MODULUS_BYTES * 8)
- if signature.size() != modulus_size:
- return "manifest signature: expected exactly %d bytes, got %d" % [modulus_size, signature.size()]
- var key := CryptoKey.new()
- if key.load_from_string(public_key_pem, true) != OK:
- return "public key: could not be loaded"
- var crypto := Crypto.new()
- var digest := PackedByteArray()
- var context := HashingContext.new()
- context.start(HashingContext.HASH_SHA256)
- context.update(message)
- digest = context.finish()
- if not crypto.verify(HashingContext.HASH_SHA256, digest, signature, key):
- return "manifest signature: RSA PKCS#1 v1.5 SHA-256 verification failed"
- return ""
-
-
-## Byte length of the RSA modulus inside a PEM SubjectPublicKeyInfo, or 0
-## when the PEM is not a well-formed rsaEncryption public key.
-static func rsa_modulus_size(public_key_pem: String) -> int:
- var lines := public_key_pem.replace("\r\n", "\n").strip_edges().split("\n")
- if (
- lines.size() < 3
- or lines[0].strip_edges() != "-----BEGIN PUBLIC KEY-----"
- or lines[lines.size() - 1].strip_edges() != "-----END PUBLIC KEY-----"
- ):
- return 0
- var encoded := ""
- for index in range(1, lines.size() - 1):
- encoded += lines[index].strip_edges()
- if encoded.is_empty() or not _is_base64(encoded):
- return 0
- var der := Marshalls.base64_to_raw(encoded)
- if der.is_empty():
- return 0
- var outer := _der_item(der, 0, 0x30)
- if not bool(outer.ok) or int(outer.end) != der.size():
- return 0
- var body: PackedByteArray = outer.value
- var algorithm := _der_item(body, 0, 0x30)
- if not bool(algorithm.ok) or algorithm.value != PackedByteArray(_RSA_ALGORITHM):
- return 0
- var bits := _der_item(body, int(algorithm.end), 0x03)
- if not bool(bits.ok) or int(bits.end) != body.size():
- return 0
- var bit_string: PackedByteArray = bits.value
- if bit_string.is_empty() or bit_string[0] != 0:
- return 0
- var key := _der_item(bit_string, 1, 0x30)
- if not bool(key.ok) or int(key.end) != bit_string.size():
- return 0
- var key_body: PackedByteArray = key.value
- var modulus := _der_item(key_body, 0, 0x02)
- if not bool(modulus.ok):
- return 0
- var raw: PackedByteArray = modulus.value
- if raw.is_empty() or raw[0] & 0x80:
- return 0
- if raw.size() > 1 and raw[0] == 0:
- if not (raw[1] & 0x80):
- return 0
- raw = raw.slice(1)
- var exponent := _der_item(key_body, int(modulus.end), 0x02)
- if not bool(exponent.ok) or int(exponent.end) != key_body.size() or exponent.value.is_empty():
- return 0
- return raw.size()
-
-
-## The single quoted `version="..."` line of a plugin.cfg, or "" when the
-## file does not carry exactly one.
-static func plugin_cfg_version(data: PackedByteArray) -> String:
- if not is_valid_utf8(data):
- return ""
- var expression := RegEx.new()
- if expression.compile("(?m)^\\s*version\\s*=\\s*\"([^\"]+)\"\\s*$") != OK:
- return ""
- var matches := expression.search_all(data.get_string_from_utf8())
- if matches.size() != 1:
- return ""
- return matches[0].get_string(1)
-
-
-## Parse a dotted `major.minor.patch` version into three ints, or [] when
-## it is not exactly three unsigned decimal components.
-static func version_parts(version: String) -> Array[int]:
- var parts := version.split(".")
- if parts.size() != 3:
- return []
- var numbers: Array[int] = []
- for part in parts:
- if not _all_digits(part):
- return []
- numbers.append(int(part))
- return numbers
-
-
-## True when `candidate` is strictly newer than `current` by tuple order.
-static func is_newer_version(candidate: String, current: String) -> bool:
- var left := version_parts(candidate)
- var right := version_parts(current)
- if left.is_empty() or right.is_empty():
- return false
- for index in 3:
- if left[index] != right[index]:
- return left[index] > right[index]
- return false
-
-
-# ----- identity ---------------------------------------------------------
-
-
-static func _check_identity(manifest: Dictionary, expected: Dictionary) -> String:
- var repository := str(manifest["repository"])
- if repository != REPOSITORY or repository != str(expected.get("repository", REPOSITORY)):
- return "manifest.repository: expected %s" % REPOSITORY
- var channel := str(manifest["channel"])
- if channel != CHANNEL or channel != str(expected.get("channel", CHANNEL)):
- return "manifest.channel: v4 releases are stable-only"
- var version := str(manifest["version"])
- var parts := version_parts(version)
- if parts.is_empty() or parts[0] != 4:
- return "manifest.version: expected a 4.x.y version"
- if str(manifest["tag"]) != "v" + version:
- return "manifest.tag: expected v%s" % version
- if not _is_hex(str(manifest["source_commit"]), 40):
- return "manifest.source_commit: expected 40 lowercase hex characters"
- var expected_version := str(expected.get("version", ""))
- if not expected_version.is_empty() and expected_version != version:
- return "manifest.version: %s does not match the expected release %s" % [version, expected_version]
- var expected_tag := str(expected.get("tag", ""))
- if not expected_tag.is_empty() and expected_tag != str(manifest["tag"]):
- return "manifest.tag: %s does not match the expected tag %s" % [str(manifest["tag"]), expected_tag]
- var expected_commit := str(expected.get("source_commit", ""))
- if not expected_commit.is_empty() and expected_commit != str(manifest["source_commit"]):
- return "manifest.source_commit: does not match the expected commit"
- var current_version := str(expected.get("current_version", ""))
- if not current_version.is_empty():
- if version_parts(current_version).is_empty():
- return "current_version: %s is not a dotted major.minor.patch version" % current_version
- if not is_newer_version(version, current_version):
- return "manifest.version: %s is not newer than the installed %s" % [version, current_version]
- return ""
-
-
-# ----- archive listing --------------------------------------------------
-
-
-static func _check_entry_names(
- entries: PackedStringArray, expected_paths: Array[String], expected_rows: Dictionary
-) -> String:
- var seen := {}
- for entry in entries:
- if entry.ends_with("/"):
- return "archive: directory entry is not allowed: %s" % entry
- if not expected_rows.has(entry):
- return "archive: entry is not in the signed inventory: %s" % entry
- if seen.has(entry):
- return "archive: duplicate entry: %s" % entry
- seen[entry] = true
- for path in expected_paths:
- if not seen.has(path):
- return "archive: inventory file is missing from the zip: %s" % path
- if Array(entries) != Array(expected_paths):
- return "archive: entries are not in the sorted inventory order"
- return ""
-
-
-# ----- path safety ------------------------------------------------------
-
-
-static func _validate_paths(paths: Array[String], context: String) -> String:
- if paths.is_empty() or paths.size() > MAX_FILES:
- return "%s: expected 1..%d paths" % [context, MAX_FILES]
- var sorted := paths.duplicate()
- sorted.sort()
- var exact := {}
- var identities := {}
- for index in paths.size():
- var path := paths[index]
- var path_error := _check_path(path, "%s[%d]" % [context, index])
- if not path_error.is_empty():
- return path_error
- if exact.has(path):
- return "%s: duplicate path %s" % [context, path]
- exact[path] = true
- var identity := path.to_lower()
- if identities.has(identity):
- return "%s: case-colliding paths %s and %s" % [context, str(identities[identity]), path]
- identities[identity] = path
- if paths != sorted:
- return "%s: paths are not in bytewise order" % context
- for path in paths:
- var parent := path.get_base_dir()
- while parent.contains("/"):
- if exact.has(parent):
- return "%s: file/ancestor collision at %s" % [context, parent]
- parent = parent.get_base_dir()
- return ""
-
-
-static func _check_path(path: String, context: String) -> String:
- if not path.begins_with(PLUGIN_PREFIX) or path.ends_with("/") or path.contains("\\"):
- return "%s: path must be a file beneath %s: %s" % [context, PLUGIN_PREFIX, path]
- for part in path.split("/"):
- if part.is_empty() or part == "." or part == ".." or part.ends_with(" ") or part.ends_with("."):
- return "%s: unsafe path component in %s" % [context, path]
- for index in part.length():
- var code := part.unicode_at(index)
- if code < 32 or _UNSAFE_PATH_CHARS.contains(char(code)):
- return "%s: unsafe path character in %s" % [context, path]
- var stem := part.split(".", true, 1)[0]
- if RESERVED_NAMES.has(stem.to_upper()):
- return "%s: reserved path component in %s" % [context, path]
- return ""
-
-
-# ----- schema helpers ---------------------------------------------------
-
-
-static func _check_keys(value: Dictionary, expected_keys: Array, context: String) -> String:
- var keys := value.keys()
- for key in keys:
- if not key is String:
- return "%s: expected object with exactly keys %s" % [context, str(expected_keys)]
- keys.sort()
- if keys != expected_keys:
- return "%s: expected object with exactly keys %s" % [context, str(expected_keys)]
- return ""
-
-
-static func _string(value: Variant, context: String) -> Dictionary:
- if not value is String or str(value).is_empty():
- return {"ok": false, "error": "%s: expected non-empty string" % context, "value": ""}
- return {"ok": true, "error": "", "value": str(value)}
-
-
-## Accept `int`, or the integral `float` Godot's JSON parser yields, within
-## `[0, maximum]`. Booleans are rejected.
-static func _integer(value: Variant, context: String, maximum: int) -> Dictionary:
- var failure := {"ok": false, "error": "%s: expected integer in [0, %d]" % [context, maximum], "value": 0}
- var number := 0
- if value is int:
- number = value
- elif value is float:
- var real: float = value
- if real != floor(real) or absf(real) > MAX_SAFE_INTEGER:
- return failure
- number = int(real)
- else:
- return failure
- if number < 0 or number > maximum:
- return failure
- return {"ok": true, "error": "", "value": number}
-
-
-static func _is_hex(text: String, length: int) -> bool:
- if text.length() != length:
- return false
- for index in text.length():
- var code := text.unicode_at(index)
- if not ((code >= 48 and code <= 57) or (code >= 97 and code <= 102)):
- return false
- return true
-
-
-static func _all_digits(text: String) -> bool:
- if text.is_empty():
- return false
- for index in text.length():
- var code := text.unicode_at(index)
- if code < 48 or code > 57:
- return false
- return true
-
-
-static func _is_base64(text: String) -> bool:
- for index in text.length():
- var code := text.unicode_at(index)
- var ok := (
- (code >= 65 and code <= 90)
- or (code >= 97 and code <= 122)
- or (code >= 48 and code <= 57)
- or code == 43 or code == 47 or code == 61
- )
- if not ok:
- return false
- return true
-
-
-# ----- canonical JSON ---------------------------------------------------
-
-
-static func _canonical_value(value: Variant, result: Dictionary) -> String:
- match typeof(value):
- TYPE_NIL:
- return "null"
- TYPE_BOOL:
- return "true" if value else "false"
- TYPE_INT:
- return String.num_int64(value)
- TYPE_FLOAT:
- var real: float = value
- if real != floor(real) or absf(real) > MAX_SAFE_INTEGER:
- result.error = "non-integer number %s" % str(real)
- return ""
- return String.num_int64(int(real))
- TYPE_STRING, TYPE_STRING_NAME:
- return _escape_json_string(str(value))
- TYPE_ARRAY:
- var items: Array[String] = []
- for item in value:
- items.append(_canonical_value(item, result))
- if not str(result.error).is_empty():
- return ""
- return "[" + ",".join(items) + "]"
- TYPE_DICTIONARY:
- var dictionary: Dictionary = value
- var keys: Array = dictionary.keys()
- for key in keys:
- if not key is String:
- result.error = "non-string object key"
- return ""
- keys.sort()
- var members: Array[String] = []
- for key in keys:
- var member := _canonical_value(dictionary[key], result)
- if not str(result.error).is_empty():
- return ""
- members.append(_escape_json_string(str(key)) + ":" + member)
- return "{" + ",".join(members) + "}"
- _:
- result.error = "unsupported JSON value type %s" % type_string(typeof(value))
- return ""
-
-
-static func _escape_json_string(text: String) -> String:
- var out := "\""
- for index in text.length():
- var code := text.unicode_at(index)
- match code:
- 0x22:
- out += "\\\""
- 0x5C:
- out += "\\\\"
- 0x0A:
- out += "\\n"
- 0x0D:
- out += "\\r"
- 0x09:
- out += "\\t"
- 0x08:
- out += "\\b"
- 0x0C:
- out += "\\f"
- _:
- if code < 0x20:
- out += "\\u%04x" % code
- else:
- out += char(code)
- return out + "\""
-
-
-# ----- DER --------------------------------------------------------------
-
-
-static func _der_item(data: PackedByteArray, offset: int, tag: int) -> Dictionary:
- var failure := {"ok": false, "value": PackedByteArray(), "end": 0}
- var size := data.size()
- if offset >= size or data[offset] != tag:
- return failure
- offset += 1
- if offset >= size:
- return failure
- var first := data[offset]
- offset += 1
- var length := 0
- if first < 0x80:
- length = first
- else:
- var count := first & 0x7F
- if count < 1 or count > 4 or offset + count > size or data[offset] == 0:
- return failure
- for index in count:
- length = (length << 8) | data[offset + index]
- offset += count
- if length < 0x80:
- return failure
- var end := offset + length
- if end > size:
- return failure
- return {"ok": true, "value": data.slice(offset, end), "end": end}
-
-
-# ----- paths ------------------------------------------------------------
-
-
-static func _absolute(path: String) -> String:
- if path.begins_with("res://") or path.begins_with("user://"):
- return ProjectSettings.globalize_path(path)
- return path
diff --git a/addons/godot_ai/utils/release_verifier.gd.uid b/addons/godot_ai/utils/release_verifier.gd.uid
deleted file mode 100644
index 34abbfe..0000000
--- a/addons/godot_ai/utils/release_verifier.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cvjv1388502ne
diff --git a/addons/godot_ai/utils/resource_io.gd b/addons/godot_ai/utils/resource_io.gd
deleted file mode 100644
index bde3a51..0000000
--- a/addons/godot_ai/utils/resource_io.gd
+++ /dev/null
@@ -1,286 +0,0 @@
-@tool
-class_name McpResourceIO
-extends RefCounted
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Shared helpers for "save a Resource to .tres" and the mutually-exclusive
-## path-vs-resource_path param validation that every resource-authoring
-## handler needs. Extracted to remove 4-way duplication across
-## resource_handler, environment_handler, texture_handler, and curve_handler.
-## Also home to the shared write-a-text-file path + deferred import-settle
-## completion used by both script_handler.create_script and
-## filesystem_handler.write_file (#714).
-
-# Bounded settle window for `ResourceLoader.exists(path)` after a fresh text
-# write registers with the filesystem, so an agent calling
-# create_script/write_file -> attach_script back-to-back doesn't race the
-# editor's import pipeline (#261, extended to write_file by #714). Polled once
-# per frame, with an elapsed-time cap below the dispatcher's deferred timeouts
-# for both commands. If import is still not visible at the cap, we still
-# return committed data instead of letting the already-written file surface
-# as DEFERRED_TIMEOUT.
-const IMPORT_SETTLE_MAX_FRAMES := 300
-const IMPORT_SETTLE_MAX_MSEC := 3500
-
-
-## Validate that exactly one of {path, resource_path} is provided.
-##
-## When `require_property` is true (default), also requires a non-empty
-## `property` param when `path` is given — this matches the semantics of
-## "assign a resource to node.property" (resource_create, texture tools,
-## curve_set_points). Pass false for tools where the path itself IS the
-## target (environment_create assigning to WorldEnvironment.environment).
-##
-## Returns null on success or an error dict on failure.
-static func validate_home(params: Dictionary, require_property: bool = true) -> Variant:
- var node_path: String = params.get("path", "")
- var property: String = params.get("property", "")
- var resource_path: String = params.get("resource_path", "")
- var has_node_target := not node_path.is_empty()
- var has_file_target := not resource_path.is_empty()
-
- if has_node_target and has_file_target:
- var both_msg := "Provide either path+property or resource_path, not both" if require_property else "Provide either path or resource_path, not both"
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, both_msg)
- if not has_node_target and not has_file_target:
- var none_msg := "Must provide either path+property (assign inline) or resource_path (save .tres)" if require_property else "Must provide either path or resource_path"
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, none_msg)
- if require_property and has_node_target and property.is_empty():
- return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "Missing required param: property (required when path is given)")
- return null
-
-
-## Save `res` to `resource_path` as a .tres/.res file.
-##
-## Handles: res:// prefix validation, overwrite check, parent-directory
-## creation, ResourceSaver.save error reporting, and the post-save
-## EditorFileSystem.update_file() so the dock picks up the change.
-##
-## `label` is the human-readable resource-kind for error messages (e.g.
-## "Environment", "Gradient texture", "Curve"). `extra_fields` is merged
-## into the success response alongside the standard fields
-## (`resource_path`, `overwritten`, `undoable: false`, `reason`). Passing
-## a `reason` key in `extra_fields` overrides the default — useful for
-## tools that edit existing files rather than creating fresh ones.
-##
-## `pause_target` should be the handler's `McpConnection`. When supplied,
-## `pause_processing` is flipped on around `ResourceSaver.save()` so the
-## dispatcher's WebSocket pump can't re-enter while Godot pumps
-## `Main::iteration()` for the resource-save's progress UI / script-class
-## update task. Without this guard a queued command landing during the
-## save can trigger another `save_to_disk` that tries to add the same
-## `update_scripts_classes` editor task — "Task already exists" → null
-## deref → SIGSEGV. Same family of bug as godotengine/godot#118545 and
-## the same mitigation as `SceneHandler`'s `save_scene*` wraps. See
-## issue #288.
-##
-## Returns either an error dict or a {"data": {...}} success dict — ready
-## for the handler to return directly.
-static func save_to_disk(
- res: Resource,
- resource_path: String,
- overwrite: bool,
- label: String,
- extra_fields: Dictionary = {},
- pause_target: McpConnection = null,
-) -> Dictionary:
- var path_err = McpPathValidator.path_error(resource_path, "resource_path", true)
- if path_err != null:
- return path_err
-
- var existed_before := FileAccess.file_exists(resource_path)
- if existed_before and not overwrite:
- return ErrorCodes.make(
- ErrorCodes.INVALID_PARAMS,
- "%s already exists at %s (pass overwrite=true to replace)" % [label, resource_path]
- )
- # Captured BEFORE the overwrite below so a resave of an already-uid'd file
- # (overwrite=true) can restore its own uid instead of losing it — see
- # ensure_uid's doc comment.
- var prior_uid := ResourceLoader.get_resource_uid(resource_path) if existed_before else ResourceUID.INVALID_ID
-
- var dir_path := resource_path.get_base_dir()
- var mkdir_err := DirAccess.make_dir_recursive_absolute(dir_path)
- if mkdir_err != OK and mkdir_err != ERR_ALREADY_EXISTS:
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Failed to create directory %s: %s" % [dir_path, error_string(mkdir_err)]
- )
-
- if pause_target != null:
- pause_target.pause_processing = true
- var save_err := ResourceSaver.save(res, resource_path)
- if pause_target != null:
- pause_target.pause_processing = false
- if save_err != OK:
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Failed to save %s to %s: %s" % [label, resource_path, error_string(save_err)]
- )
- var uid_err := ensure_uid(resource_path, prior_uid)
- if uid_err != OK:
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "%s saved to %s but failed to write its uid: %s" % [label, resource_path, error_string(uid_err)]
- )
-
- var efs := EditorInterface.get_resource_filesystem()
- if efs != null:
- efs.update_file(resource_path)
-
- var data := {
- "resource_path": resource_path,
- "overwritten": existed_before,
- "undoable": false,
- "reason": "File creation is persistent; delete the file manually to revert",
- }
- attach_cleanup_hint(data, existed_before, [resource_path])
- # merge with overwrite=true so callers (e.g. curve_set_points editing an
- # existing .tres) can supply a domain-specific `reason`.
- data.merge(extra_fields, true)
- return {"data": data}
-
-
-## Save `res` to `resource_path` with the same `pause_processing` re-entrancy
-## guard as `save_to_disk` (see its doc for the #288 SIGSEGV background), for
-## call sites that need to pick their own error handling / overwrite policy
-## instead of `save_to_disk`'s full validate+mkdir+overwrite-guard bundle
-## (undo/redo callables reloading-mutating-resaving an existing resource,
-## `apply_to_node`'s inline-then-save branch). Returns the raw
-## `ResourceSaver.save` error code.
-static func guarded_save(res: Resource, resource_path: String, pause_target: McpConnection) -> int:
- var prior_uid := ResourceLoader.get_resource_uid(resource_path) if FileAccess.file_exists(resource_path) else ResourceUID.INVALID_ID
- if pause_target != null:
- pause_target.pause_processing = true
- var save_err := ResourceSaver.save(res, resource_path)
- if pause_target != null:
- pause_target.pause_processing = false
- if save_err != OK:
- return save_err
- return ensure_uid(resource_path, prior_uid)
-
-
-## Make `resource_path` carry a stable uid after a successful
-## `ResourceSaver.save()`, matching what Godot's own "New Scene"/"New
-## Resource" editor flows always embed. A bare `ResourceSaver.save()` call
-## does neither on its own: a brand-new file gets no `uid=` at all, and
-## resaving a file that already had one silently drops it (#737). Call this
-## immediately after every successful save.
-##
-## `prior_uid` is whatever `ResourceLoader.get_resource_uid(resource_path)`
-## returned BEFORE this save overwrote the file (pass `ResourceUID.INVALID_ID`
-## for a brand-new path). Reusing the prior id — instead of always minting a
-## fresh one — keeps any `uid://...` references elsewhere in the project
-## resolving to the same file.
-##
-## Returns the `Error` from `ResourceSaver.set_uid()` so callers can surface a
-## uid-write failure instead of silently reporting success on a file that
-## didn't end up with the uid it was supposed to get.
-static func ensure_uid(resource_path: String, prior_uid: int) -> Error:
- var id := prior_uid
- if id == ResourceUID.INVALID_ID:
- id = ResourceUID.create_id()
- return ResourceSaver.set_uid(resource_path, id)
-
-
-## Attach a `cleanup.rm` hint listing `paths` to `data` — only when the call
-## just created a new file (`existed_before == false`). On overwrite the field
-## is omitted because the caller already had the file on disk, and handing
-## them a cleanup list would invite dropping user content instead of just
-## scratch artifacts. Used by write-and-return handlers (create_script,
-## filesystem_write_text, resource_create/save_to_disk) so callers running
-## transient smoke tests can rm artifacts without tracking paths. See #82.
-static func attach_cleanup_hint(data: Dictionary, existed_before: bool, paths: Array) -> void:
- if existed_before:
- return
- data["cleanup"] = {"rm": paths}
-
-
-## Shared write-a-text-file path (#714): parent-directory mkdir, write +
-## flush with an explicit error check so a truncated write (disk full,
-## permission flip mid-write) surfaces as an error instead of plain success.
-## Deliberately does NOT call `EditorFileSystem.update_file()` — callers
-## register the file themselves after assembling their response fields, so
-## the registration comment (the dsarno/godot#6 scan-stacking rationale)
-## stays next to the call. Returns null on success or an error dict ready
-## to return from the handler.
-static func write_text_to_disk(path: String, content: String) -> Variant:
- var dir_path := path.get_base_dir()
- if not DirAccess.dir_exists_absolute(dir_path):
- var err := DirAccess.make_dir_recursive_absolute(dir_path)
- if err != OK:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to create directory: %s" % dir_path)
-
- var file := FileAccess.open(path, FileAccess.WRITE)
- if file == null:
- return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to open file for writing: %s" % path)
-
- file.store_string(content)
- file.flush()
- var write_err := file.get_error()
- file.close()
- if write_err != OK:
- return ErrorCodes.make(
- ErrorCodes.INTERNAL_ERROR,
- "Write failed for %s (%s); file may be truncated" % [path, error_string(write_err)]
- )
- return null
-
-
-# `static` is load-bearing: the deferred completion captures no `self`, so the
-# coroutine survives even if the calling handler RefCounted is freed mid-await.
-# Under concurrent create storms with editor_reload_plugin fired during the
-# burst, an instance-method coroutine is otherwise GC'd between `await` and
-# resume, producing "Resumed function ... after await, but class instance is
-# gone" errors and dropping the response. Keep this function static and
-# parameterise everything it needs explicitly — do not reference instance
-# state. Shared by create_script and write_file's fresh-`.gd` path (#714).
-static func finish_text_write_deferred(
- connection: McpConnection,
- request_id: String,
- path: String,
- data: Dictionary,
-) -> void:
- var ledger := preload("res://addons/godot_ai/utils/script_work.gd")
- var work: int = ledger.begin("write_file")
- await _settle_text_write(connection, request_id, path, data)
- ledger.finish(work)
-
-
-static func _settle_text_write(
- connection: McpConnection, request_id: String, path: String, data: Dictionary,
-) -> void:
- if not is_instance_valid(connection):
- return
- var tree := connection.get_tree()
- if tree == null:
- return
- var deadline_ms := Time.get_ticks_msec() + IMPORT_SETTLE_MAX_MSEC
- # Let _dispatch() return DEFERRED_RESPONSE and register the request before
- # this coroutine can send a committed result. ResourceLoader.exists(path)
- # may already be true on fast imports; without this handoff the connection
- # treats the response as late/unregistered and drops it, then the dispatcher
- # times out a file that was already written (#324). The deadline starts
- # before this await so a slow handoff frame is counted against the bounded
- # settle window.
- await tree.process_frame
- var frames := 0
- while (
- frames < IMPORT_SETTLE_MAX_FRAMES
- and Time.get_ticks_msec() < deadline_ms
- and not ResourceLoader.exists(path)
- ):
- await tree.process_frame
- frames += 1
- # If the plugin tears down (_exit_tree frees the connection) during the
- # await, is_instance_valid() goes false and we drop the response silently —
- # the server's request timeout will surface the failure to the caller.
- if not is_instance_valid(connection):
- return
- var payload := data.duplicate()
- var settled := ResourceLoader.exists(path)
- payload["import_settled"] = settled
- payload["import_settle"] = "settled" if settled else "timeout"
- payload["import_pending"] = not settled
- connection.send_deferred_response(request_id, {"data": payload})
diff --git a/addons/godot_ai/utils/resource_io.gd.uid b/addons/godot_ai/utils/resource_io.gd.uid
deleted file mode 100644
index 94da4c9..0000000
--- a/addons/godot_ai/utils/resource_io.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://de2rwdoa4wabf
diff --git a/addons/godot_ai/utils/scene_path.gd b/addons/godot_ai/utils/scene_path.gd
deleted file mode 100644
index 782aea1..0000000
--- a/addons/godot_ai/utils/scene_path.gd
+++ /dev/null
@@ -1,155 +0,0 @@
-@tool
-class_name McpScenePath
-extends RefCounted
-
-const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
-
-## Utility for converting between Godot internal node paths and clean
-## scene-relative paths like /Main/Camera3D.
-
-
-## Return a clean path relative to the scene root (e.g. /Main/Camera3D).
-## Returns "" when `node` is not the scene root or a descendant of it —
-## without the ancestry guard, get_path_to() returns an empty NodePath that
-## concatenates into a plausible-looking but invalid "/Main/".
-static func from_node(node: Node, scene_root: Node) -> String:
- if scene_root == null or node == null:
- return ""
- if node == scene_root:
- return "/" + scene_root.name
- if not scene_root.is_ancestor_of(node):
- return ""
- var relative := scene_root.get_path_to(node)
- return "/" + scene_root.name + "/" + str(relative)
-
-
-## Resolve a clean scene path like "/Main/Camera3D" to the actual node.
-##
-## Accepts forms relative to the edited scene root:
-## "/Main" — explicit root prefix (canonical)
-## "/Main/Camera3D" — descendant path
-## "Camera3D" — bare relative to scene_root
-## "World/Ground" — nested bare relative to scene_root
-##
-## Also accepts SceneTree-style "/root/[/...]" as an alias for
-## the edited scene root. Agents reach for /root/Foo right after creating a
-## scene because that's where scenes live at runtime; we honor it so the call
-## doesn't fail with a confusing "not found" error. The alias only kicks in
-## when the segment after /root matches the scene root's name — paths like
-## "/root/@EditorNode@.../Main/..." (returned by Node.get_path() in the editor)
-## fall through to the absolute-path fallback unchanged.
-static func resolve(scene_path: String, scene_root: Node) -> Node:
- if scene_root == null:
- return null
-
- ## Bare "/" alias: the most natural first guess for "the scene root".
- ## There is exactly one edited-scene root, so the alias is unambiguous;
- ## without it, "/" falls through to get_node_or_null("/") → null and
- ## every call costs the agent a NODE_NOT_FOUND round trip (issue #624).
- if scene_path == "/":
- return scene_root
-
- ## /root/[/...] alias: strip the /root prefix and recurse.
- ## Match the scene root by name explicitly so we don't capture editor-
- ## internal paths that legitimately live under /root.
- var alias_prefix := "/root/" + scene_root.name
- if scene_path == alias_prefix or scene_path.begins_with(alias_prefix + "/"):
- return resolve(scene_path.substr(5), scene_root) # keep leading slash
-
- var root_prefix := "/" + scene_root.name
- if scene_path == root_prefix:
- return scene_root
- if scene_path.begins_with(root_prefix + "/"):
- var relative := scene_path.substr(root_prefix.length() + 1)
- return scene_root.get_node_or_null(relative)
-
- # Try as-is (relative path, or absolute SceneTree path).
- return scene_root.get_node_or_null(scene_path)
-
-
-## Return the edited scene root, or an error dict if the editor has no open
-## scene or the open scene doesn't match `expected_scene_file`.
-##
-## `expected_scene_file` is the caller's `scene_file` parameter — an empty
-## string means "target whatever is currently edited" (current behaviour,
-## no guard). A non-empty value must match `scene_file_path` on the current
-## edited scene root exactly, or we return EDITED_SCENE_MISMATCH so the
-## caller can re-open the right scene.
-##
-## Shape on success: {"node": }. Shape on error matches
-## `ErrorCodes.make()` so callers can propagate the result directly.
-static func require_edited_scene(expected_scene_file: String) -> Dictionary:
- var root := EditorInterface.get_edited_scene_root()
- if root == null:
- # Mirrors the structured payload that the Python-side require_writable
- # gate attaches for `playing` / `importing`. Together these cover the
- # three recoverable editor *states* (playing / importing / no_scene)
- # — the EDITOR_NOT_READY paths an AI caller can act on. Other
- # EDITOR_NOT_READY callsites describing internal-state failures
- # ("EditorFileSystem not available" etc.) carry sub_code + retryable
- # via ErrorCodes.make_not_ready (#651 stage 1) but intentionally
- # omit the hint — there's no useful caller action to name.
- var err := ErrorCodes.make(ErrorCodes.EDITOR_NOT_READY, "No scene open")
- err["error"]["data"] = {
- "sub_code": ErrorCodes.SUB_EDITOR_NO_SCENE,
- "editor_state": "no_scene",
- "retryable": false,
- "hint": (
- "No scene is open. Call scene_open with a scene path "
- + "(e.g. \"res://main.tscn\") before issuing scene-mutating tools."
- ),
- }
- return err
- if not expected_scene_file.is_empty() and root.scene_file_path != expected_scene_file:
- var actual := root.scene_file_path if not root.scene_file_path.is_empty() else ""
- return ErrorCodes.make(
- ErrorCodes.EDITED_SCENE_MISMATCH,
- (
- "Expected edited scene \"%s\" but \"%s\" is active. "
- + "Call scene_open(\"%s\") first, or omit scene_file to target the active scene."
- ) % [expected_scene_file, actual, expected_scene_file],
- )
- return {"node": root}
-
-
-## Format a "parent not found" error that names the path convention.
-## Agents routinely try /root/Foo or absolute SceneTree paths; the bare
-## "Parent not found: X" gave them no hint that paths are scene-relative.
-## Wording is generic ("Paths are relative...") so the helper works for any
-## param name (parent_path, new_parent, …).
-static func format_parent_error(path: String, scene_root: Node) -> String:
- if scene_root == null:
- return "Parent not found: %s. No edited scene is open." % path
- var root_name := str(scene_root.name)
- return "Parent not found: %s. Paths are relative to the edited scene root (e.g. \"/%s\" or \"\"), not the SceneTree. Scene root is \"/%s\"." % [path, root_name, root_name]
-
-
-## Format a "node not found" error that names the path convention and, when
-## possible, suggests a corrected path. Agents routinely pass /root/Foo
-## (runtime SceneTree) or unprefixed names; the bare "Node not found: X"
-## gives no hint that paths are edited-scene-relative.
-##
-## Suggestion logic (highest-confidence first):
-## 1. /root/[/...] where is not the scene root → suggest //[/...]
-## 2. path doesn't start with "/" → suggest "//"
-## 3. otherwise no concrete "did you mean", just the convention reminder.
-static func format_node_error(path: String, scene_root: Node) -> String:
- if scene_root == null:
- return "Node not found: %s. No edited scene is open." % path
- var root_name := str(scene_root.name)
- var suggestion := ""
-
- if path.begins_with("/root/"):
- var after_root := path.substr(6) # "/root/" is 6 chars
- # Only suggest if the segment after /root/ isn't already the scene root
- # (resolve() handles /root//... as an alias, so a failure
- # with that prefix means a deeper segment is wrong — no clean rewrite).
- var first_seg := after_root.split("/")[0]
- if first_seg != root_name and not first_seg.is_empty():
- suggestion = "/" + root_name + "/" + after_root
- elif not path.begins_with("/") and not path.is_empty():
- suggestion = "/" + root_name + "/" + path
-
- if suggestion.is_empty():
- return "Node not found: %s. Paths are relative to the edited scene root (e.g. \"/%s/Child\"), not runtime /root/... paths. Scene root is \"/%s\"." % [path, root_name, root_name]
- return "Node not found: %s. Did you mean \"%s\"? Paths are relative to the edited scene root, not runtime /root/... paths. Scene root is \"/%s\"." % [path, suggestion, root_name]
diff --git a/addons/godot_ai/utils/scene_path.gd.uid b/addons/godot_ai/utils/scene_path.gd.uid
deleted file mode 100644
index 85795a9..0000000
--- a/addons/godot_ai/utils/scene_path.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c1irdrss0amex
diff --git a/addons/godot_ai/utils/screenshot_encode.gd b/addons/godot_ai/utils/screenshot_encode.gd
deleted file mode 100644
index e3b0210..0000000
--- a/addons/godot_ai/utils/screenshot_encode.gd
+++ /dev/null
@@ -1,38 +0,0 @@
-@tool
-class_name McpScreenshotEncode
-extends RefCounted
-
-## Shared downscale + PNG + base64 block for the screenshot paths (#716).
-##
-## Two call sites straddle the editor/game process boundary — the editor's
-## take_screenshot (editor_handler) and the game-process autoload
-## (runtime/game_helper) — and were maintained as manually synchronized
-## copies. Pure static, no editor APIs, so it loads safely in the game
-## process too.
-
-
-## Downscale `image` in place so its longest edge is at most
-## `max_resolution` (0 = no cap), then PNG-encode. Returns
-## {base64, width, height, original_width, original_height}.
-static func downscale_and_encode(image: Image, max_resolution: int) -> Dictionary:
- var original_width := image.get_width()
- var original_height := image.get_height()
-
- if max_resolution > 0:
- var longest := maxi(original_width, original_height)
- if longest > max_resolution:
- var scale := float(max_resolution) / float(longest)
- ## Clamp to 1px min: extreme aspect ratios at very small
- ## max_resolution could otherwise compute a zero dimension and
- ## crash image.resize().
- var new_w := maxi(1, int(original_width * scale))
- var new_h := maxi(1, int(original_height * scale))
- image.resize(new_w, new_h, Image.INTERPOLATE_LANCZOS)
-
- return {
- "base64": Marshalls.raw_to_base64(image.save_png_to_buffer()),
- "width": image.get_width(),
- "height": image.get_height(),
- "original_width": original_width,
- "original_height": original_height,
- }
diff --git a/addons/godot_ai/utils/screenshot_encode.gd.uid b/addons/godot_ai/utils/screenshot_encode.gd.uid
deleted file mode 100644
index 0bcdac2..0000000
--- a/addons/godot_ai/utils/screenshot_encode.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cl0xhoxwsbmow
diff --git a/addons/godot_ai/utils/script_work.gd b/addons/godot_ai/utils/script_work.gd
deleted file mode 100644
index bb919a4..0000000
--- a/addons/godot_ai/utils/script_work.gd
+++ /dev/null
@@ -1,29 +0,0 @@
-@tool
-extends RefCounted
-
-## A response timeout is not proof that its coroutine has returned. Track
-## actual script execution across frame yields, independently of transport
-## bookkeeping. This is process-wide deliberately: an older composition may
-## still own a coroutine in the same scripts after an ordinary plugin reload.
-## Only the main thread may use this ledger; OS workers have separate joins.
-static var _next_id := 0
-static var _active: Dictionary = {}
-
-
-static func begin(label: String) -> int:
- _next_id += 1
- _active[_next_id] = label
- return _next_id
-
-
-static func finish(id: int) -> void:
- _active.erase(id)
-
-
-static func quiescence() -> Dictionary:
- if not _active.is_empty():
- return {
- "ok": false,
- "error": "Wait for active editor work before updating: %s" % str(_active.values()),
- }
- return {"ok": true}
diff --git a/addons/godot_ai/utils/script_work.gd.uid b/addons/godot_ai/utils/script_work.gd.uid
deleted file mode 100644
index a97dd01..0000000
--- a/addons/godot_ai/utils/script_work.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ghcyiy3v3nsk
diff --git a/addons/godot_ai/utils/server_authority.gd b/addons/godot_ai/utils/server_authority.gd
deleted file mode 100644
index eec177f..0000000
--- a/addons/godot_ai/utils/server_authority.gd
+++ /dev/null
@@ -1,171 +0,0 @@
-@tool
-extends RefCounted
-
-const TransportCapability := preload("res://addons/godot_ai/utils/transport_capability.gd")
-
-## Capability and process-control values used by the lifecycle owner. None of
-## these objects performs work or retains another owner. Keeping the three
-## grants distinct makes it impossible to turn "I can talk to this server"
-## into "I may kill or replace it" by accident.
-
-
-class TransportAuthority:
- extends RefCounted
-
- var _http_port: int
- var _ws_port: int
- var _server_instance_id: String
- var _http_capability: String
- var _ws_capability: String
-
-
- func _init(
- p_http_port: int,
- p_ws_port: int,
- p_server_instance_id: String,
- p_http_capability: String,
- p_ws_capability: String,
- ) -> void:
- _http_port = p_http_port
- _ws_port = p_ws_port
- _server_instance_id = p_server_instance_id
- _http_capability = p_http_capability
- _ws_capability = p_ws_capability
-
-
- func is_valid() -> bool:
- return (
- _http_port > 0
- and _http_port <= 65535
- and _ws_port > 0
- and _ws_port <= 65535
- and TransportCapability.is_lower_hex(_server_instance_id, 32)
- and TransportCapability.is_http_capability(_http_capability)
- and TransportCapability.is_lower_hex(_ws_capability, 64)
- and _http_capability != _ws_capability
- )
-
-
- func http_capability() -> String:
- return _http_capability
-
-
- func ws_capability() -> String:
- return _ws_capability
-
-
- func http_port() -> int:
- return _http_port
-
-
- func ws_port() -> int:
- return _ws_port
-
-
- func server_instance_id() -> String:
- return _server_instance_id
-
-
- ## Safe for UI/status fanout: secrets never leave the lifecycle/connection
- ## boundary through a generic Dictionary snapshot.
- func public_snapshot() -> Dictionary:
- return {
- "http_port": _http_port,
- "ws_port": _ws_port,
- "server_instance_id": _server_instance_id,
- }
-
-
-class OwnedProcessGrant:
- extends RefCounted
-
- var _pid: int
- var _process_fingerprint: String
- var _issued_at_msec: int
-
-
- func _init(
- p_pid: int,
- p_process_fingerprint: String,
- p_issued_at_msec: int,
- ) -> void:
- _pid = p_pid
- _process_fingerprint = p_process_fingerprint
- _issued_at_msec = p_issued_at_msec
-
-
- func is_valid() -> bool:
- return (
- _pid > 1
- and _pid != OS.get_process_id()
- and not _process_fingerprint.is_empty()
- and _issued_at_msec > 0
- )
-
-
- func process_id() -> int:
- return _pid
-
-
- func matches(p_pid: int, p_fingerprint: String) -> bool:
- return (
- is_valid()
- and _pid == p_pid
- and _process_fingerprint == p_fingerprint
- )
-
-
-class ReplacementAuthorization:
- extends RefCounted
-
- var _target_instance_id: String
- var _target_version: String
- var _target_port: int
- var _expires_at_msec: int
- var _spent := false
-
-
- func _init(
- p_target_instance_id: String,
- p_target_version: String,
- p_target_port: int,
- p_expires_at_msec: int,
- ) -> void:
- _target_instance_id = p_target_instance_id
- _target_version = p_target_version
- _target_port = p_target_port
- _expires_at_msec = p_expires_at_msec
-
-
- func is_available(now_msec: int) -> bool:
- return (
- not _spent
- and now_msec <= _expires_at_msec
- and _target_port > 0
- and not _target_instance_id.is_empty()
- and not _target_version.is_empty()
- )
-
-
- ## Spend is intentionally binding and destructive. A caller cannot probe
- ## one target and then reuse the same click-authority against another.
- func spend(
- now_msec: int,
- p_target_instance_id: String,
- p_target_version: String,
- p_target_port: int,
- ) -> bool:
- if not is_available(now_msec):
- return false
- if (
- _target_instance_id != p_target_instance_id
- or _target_version != p_target_version
- or _target_port != p_target_port
- ):
- return false
- _spent = true
- return true
-
-
- func is_spent() -> bool:
- return _spent
diff --git a/addons/godot_ai/utils/server_authority.gd.uid b/addons/godot_ai/utils/server_authority.gd.uid
deleted file mode 100644
index 7c17adf..0000000
--- a/addons/godot_ai/utils/server_authority.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://2gx74pk3lpq2
diff --git a/addons/godot_ai/utils/server_lifecycle.gd b/addons/godot_ai/utils/server_lifecycle.gd
deleted file mode 100644
index b4f1795..0000000
--- a/addons/godot_ai/utils/server_lifecycle.gd
+++ /dev/null
@@ -1,1721 +0,0 @@
-@tool
-class_name McpServerLifecycleManager
-extends RefCounted
-
-## One serialized lifecycle episode. Late effect results carry its ID.
-signal snapshot_changed(snapshot: Dictionary)
-signal transport_ready(ws_port: int, ws_capability: String)
-signal transport_cleared(reason: String)
-signal effect_requested(episode_id: int, effect: String, payload: Dictionary)
-signal startup_finished(path: String)
-
-const Authority := preload("res://addons/godot_ai/utils/server_authority.gd")
-const ClientConfigurator := preload("res://addons/godot_ai/client_configurator.gd")
-const PortResolver := preload("res://addons/godot_ai/utils/port_resolver.gd")
-const ServerState := preload("res://addons/godot_ai/utils/mcp_server_state.gd")
-const ServerVersionCheck := preload("res://addons/godot_ai/utils/server_version_check.gd")
-const TransportCapability := preload("res://addons/godot_ai/utils/transport_capability.gd")
-const UvResolution := preload("res://addons/godot_ai/utils/uv_resolution_policy.gd")
-
-const DORMANT := "DORMANT"
-const STARTING := "STARTING"
-const READY := "READY"
-const BLOCKED := "BLOCKED"
-const RECOVERING := "RECOVERING"
-## How long a server launched to replace an occupant may wait for the port.
-## A Windows update took 25.262 s from the child's port-wait phase until the
-## incumbent released the port. Keep time for those exact identity checks
-## and termination before the child gives up. Python caps this at the same
-## 60 s; the later capability proof has its own 180 s deadline.
-const REPLACEMENT_WAIT_FOR_PORT_MS := 60_000
-## How long the replacement gives its launched server to reach that port
-## wait before it kills the occupant. The launch may be uvx installing the
-## version the update just brought in; until the server reports it is at its
-## bind loop, the occupant keeps serving and the port never looks free to an
-## attach bridge that would spawn a backend of its own into the gap.
-const REPLACEMENT_LAUNCH_READY_TIMEOUT_MS := 120_000
-const STARTUP_PHASE_WAITING_FOR_PORT := "waiting_for_port"
-const STOPPING := "STOPPING"
-
-const PROBE := "PROBE"
-const LAUNCH := "LAUNCH"
-const PROVE := "PROVE"
-const REPLACE := "REPLACE"
-const STOP := "STOP"
-
-const STATUS_PATH := "/godot-ai/status"
-## Normal starts and recovery need the same settling window as post-update
-## adoption. A healthy authenticated status response can take over a second.
-const DEFAULT_PROBE_TIMEOUT_MS := 3000
-const STALE_PRE_V4_HINT := "stale_pre_v4_server"
-const DEFAULT_PROVE_TIMEOUT_MS := 180_000
-const LAUNCH_FINGERPRINT_TIMEOUT_MS := 15_000
-const REPLACEMENT_TTL_MS := 15_000
-## An authenticated endpoint that drops (a late keepalive, a server restart)
-## is re-probed automatically with this backoff, each attempt re-proving the
-## server exactly as a start does; after the last one the block stays until
-## the dock's Restart (#962). A server that stays ready this long earns a
-## fresh budget, so a flapping one cannot loop forever.
-const ENDPOINT_RECOVERY_DELAYS_SECONDS: Array[float] = [1.0, 2.0, 4.0, 8.0, 16.0]
-const ENDPOINT_RECOVERY_STABLE_MS := 60_000
-
-var _endpoint_recovery_attempts := 0
-## When the current READY began; a loss after a stable minute earns a fresh budget.
-var _ready_since_msec := 0
-## The episode whose re-probe is scheduled; 0 when none is pending.
-var _endpoint_recovery_pending_episode := 0
-const MAX_STATUS_BODY_BYTES := 8 * 1024
-
-var _episode: Dictionary = _dormant_episode(0)
-var _next_episode_id := 0
-var _plan: Dictionary = {}
-var _transport
-var _process_grant
-var _replacement_authorization
-var _effect: Dictionary = {}
-
-func configure(plan: Dictionary) -> void:
- ## Capture caller-owned values once. Configuration is intentionally inert.
- if not _plan.is_empty():
- return
- _plan = plan.duplicate(true)
- _plan["http_port"] = int(_plan.get("http_port", ClientConfigurator.DEFAULT_HTTP_PORT))
- _plan["ws_port"] = int(_plan.get("ws_port", ClientConfigurator.DEFAULT_WS_PORT))
- _plan["expected_version"] = str(_plan.get("expected_version", ""))
- _plan["capability_path"] = str(_plan.get("capability_path", ""))
- _plan["probe_timeout_ms"] = int(_plan.get("probe_timeout_ms", DEFAULT_PROBE_TIMEOUT_MS))
- _plan["prove_timeout_ms"] = int(_plan.get("prove_timeout_ms", DEFAULT_PROVE_TIMEOUT_MS))
- _plan["keep_alive"] = bool(_plan.get("keep_alive", false))
- _plan["automatic_effects"] = bool(_plan.get("automatic_effects", true))
- _plan["defer_effects"] = bool(_plan.get("defer_effects", true))
-
-func start_server() -> void:
- if _plan.is_empty():
- _block_without_effect("not_configured", "Server lifecycle is not configured.")
- return
- if str(_episode.get("state")) in [STARTING, RECOVERING, STOPPING]:
- return
- if _process_grant != null:
- force_restart_server()
- return
- _begin_start_episode()
-
-
-func _begin_start_episode(existing_id := 0, probe := true) -> void:
- _cancel_effect()
- _replacement_authorization = null
- _transport = null
- if existing_id <= 0:
- _next_episode_id += 1
- existing_id = _next_episode_id
- _episode = {
- "id": existing_id,
- "state": STARTING,
- "phase": PROBE if probe else LAUNCH,
- "reason": "",
- "message": "Probing the configured server endpoint." if probe else "Launching the managed server.",
- "ready_kind": "",
- "effect": "",
- "expected_version": str(_plan.get("expected_version", "")),
- "actual_version": "",
- "blocked_target": {},
- "launch": {},
- "after_stop": "",
- }
- _publish()
- if not probe:
- return
- _request_effect(PROBE, {
- "http_port": int(_plan.http_port),
- "expected_version": str(_plan.expected_version),
- "expected_ws_port": int(_plan.ws_port),
- "timeout_ms": int(_plan.probe_timeout_ms),
- "grant": _process_grant,
- })
-
-
-func stop_server(force_inline := false) -> void:
- ## Ordinary duplicate Stop clicks coalesce. Update/exit teardown passes
- ## force_inline: it must join even an existing STOP worker before scripts can
- ## be replaced, then re-check the same exact grant synchronously.
- if str(_episode.get("state")) == STOPPING and not force_inline:
- return
- _adopt_cancelled_launch(_cancel_effect())
- _next_episode_id += 1
- var launch: Dictionary = _episode.get("launch", {}).duplicate(true)
- _episode = _stopping_episode(_next_episode_id, launch, "")
- _transport = null
- _replacement_authorization = null
- transport_cleared.emit("server stopping")
- _publish()
- if _process_grant == null:
- _finish_stop()
- return
- _request_effect(STOP, _stop_payload(launch), force_inline)
-
-
-func force_restart_server() -> bool:
- if not has_managed_server():
- return false
- _cancel_effect()
- _next_episode_id += 1
- var launch: Dictionary = _episode.get("launch", {}).duplicate(true)
- _episode = _stopping_episode(_next_episode_id, launch, "start")
- _episode["message"] = "Restarting the managed server."
- _transport = null
- transport_cleared.emit("server restarting")
- _publish()
- _request_effect(STOP, _stop_payload(launch))
- return true
-
-
-func authorize_replacement(now_msec := -1, ttl_msec := REPLACEMENT_TTL_MS) -> bool:
- if str(_episode.get("state")) != BLOCKED:
- return false
- var target: Dictionary = _episode.get("blocked_target", {})
- if not bool(target.get("replaceable", false)):
- return false
- if now_msec < 0:
- now_msec = Time.get_ticks_msec()
- _replacement_authorization = Authority.ReplacementAuthorization.new(
- str(target.get("instance_id", "")),
- str(target.get("version", "")),
- int(target.get("port", 0)),
- now_msec + maxi(0, ttl_msec),
- )
- return _replacement_authorization.is_available(now_msec)
-
-
-func replace_authorized(now_msec := -1) -> bool:
- if str(_episode.get("state")) != BLOCKED or _replacement_authorization == null:
- return false
- if now_msec < 0:
- now_msec = Time.get_ticks_msec()
- var target: Dictionary = _episode.get("blocked_target", {}).duplicate(true)
- if not _replacement_authorization.spend(
- now_msec,
- str(target.get("instance_id", "")),
- str(target.get("version", "")),
- int(target.get("port", 0)),
- ):
- _replacement_authorization = null
- return false
- _replacement_authorization = null
- _episode["state"] = RECOVERING
- _episode["phase"] = ""
- _episode["message"] = "Replacing the explicitly authorized server."
- _episode["effect"] = ""
- _publish()
- ## The replacement launches our server first and kills the target only
- ## then, so the port never looks free to a bridge polling to spawn its
- ## own backend; the launched server waits for the port to free.
- var launch_plan := _plan.duplicate(true)
- launch_plan["wait_for_port_ms"] = REPLACEMENT_WAIT_FOR_PORT_MS
- _request_effect(REPLACE, {
- "target": target,
- "timeout_ms": int(_plan.get("probe_timeout_ms", DEFAULT_PROBE_TIMEOUT_MS)),
- "launch": launch_plan,
- })
- return true
-
-
-func request_replacement() -> bool:
- var now := Time.get_ticks_msec()
- return authorize_replacement(now) and replace_authorized(now)
-
-
-func can_recover_incompatible_server() -> bool:
- return (
- str(_episode.get("state")) == BLOCKED
- and bool(_episode.get("blocked_target", {}).get("replaceable", false))
- )
-
-
-func prepare_for_update_reload() -> Dictionary:
- stop_server(true)
- var state := str(_episode.get("state", ""))
- if state == DORMANT and _process_grant == null:
- return {"ok": true}
- return {
- "ok": false,
- "reason": str(_episode.get("reason", "stop_incomplete")),
- "message": str(_episode.get(
- "message", "The managed server did not stop before update activation."
- )),
- }
-
-
-func teardown_for_editor_exit(live_snapshot: Dictionary = {}) -> void:
- ## A launch effect can create the process immediately before teardown joins
- ## it. Recover that exact PID+fingerprint result before deciding whether
- ## there is an owned child to stop or intentionally detach.
- _adopt_cancelled_launch(_cancel_effect())
- if _process_grant == null:
- _detach_to_dormant("adopted server left running")
- return
- if bool(_plan.get("keep_alive", false)):
- _detach_to_dormant("keep_server_on_exit")
- return
- var live := live_snapshot.duplicate(true) if not live_snapshot.is_empty() else _probe_current_transport()
- if active_lease_count(live) > 0:
- _detach_to_dormant("active attach lease")
- return
- stop_server(true)
-
-
-func detach_server(reason := "server left running") -> void:
- _detach_to_dormant(reason)
-
-
-func _detach_to_dormant(reason: String) -> void:
- _cancel_effect()
- _process_grant = null
- _transport = null
- _replacement_authorization = null
- _next_episode_id += 1
- _episode = _dormant_episode(_next_episode_id)
- _episode["message"] = reason
- transport_cleared.emit(reason)
- _publish()
-
-
-func transport_authenticated(version: String) -> void:
- if str(_episode.get("state")) != READY:
- return
- var expected := str(_episode.get("expected_version", ""))
- if _server_version_compatibility(version, expected).get("compatible", false):
- _episode["actual_version"] = version
- _publish()
- return
- var public_transport: Dictionary = _transport.public_snapshot() if _transport != null else {}
- _block(
- "incompatible",
- "Server v%s does not match plugin v%s. Choose Replace to continue."
- % [version, expected],
- {
- "instance_id": str(public_transport.get("server_instance_id", "")),
- "version": version,
- "port": int(public_transport.get("http_port", 0)),
- "replaceable": not str(public_transport.get("server_instance_id", "")).is_empty(),
- },
- )
-
-
-func transport_lost(reason := "Authenticated server endpoint was lost.") -> void:
- if str(_episode.get("state")) != READY:
- return
- _transport = null
- ## Stability is measured from the moment the server became READY, not
- ## from when a recovery started, so a slow recovery cannot refresh its
- ## own budget and a server that held for a minute does.
- if _ready_since_msec != 0 and Time.get_ticks_msec() - _ready_since_msec >= ENDPOINT_RECOVERY_STABLE_MS:
- _endpoint_recovery_attempts = 0
- _ready_since_msec = 0
- var limit := ENDPOINT_RECOVERY_DELAYS_SECONDS.size()
- if _endpoint_recovery_attempts >= limit:
- _block(
- "endpoint_lost",
- "%s Automatic re-probing gave up after %d attempts; click Restart." % [reason, limit],
- )
- return
- _endpoint_recovery_attempts += 1
- var delay := float(ENDPOINT_RECOVERY_DELAYS_SECONDS[_endpoint_recovery_attempts - 1])
- _block(
- "endpoint_lost",
- "%s Re-probing in %ds (attempt %d of %d)." % [
- reason, int(delay), _endpoint_recovery_attempts, limit,
- ],
- )
- print(
- "MCP | server endpoint lost (%s); re-probing in %ds (attempt %d of %d)"
- % [reason, int(delay), _endpoint_recovery_attempts, limit]
- )
- _endpoint_recovery_pending_episode = int(_episode.get("id", 0))
- if bool(_plan.get("automatic_effects", true)):
- _recover_lost_endpoint_after(delay, _endpoint_recovery_pending_episode)
-
-
-## The scheduled re-probe. Only the episode that lost its endpoint, and only
-## while its attempt is still pending, may start it: a dock Restart or a
-## newer loss in between supersedes the timer.
-func recover_lost_endpoint(episode_id: int) -> bool:
- if episode_id <= 0 or episode_id != _endpoint_recovery_pending_episode:
- return false
- if episode_id != int(_episode.get("id", -1)):
- _endpoint_recovery_pending_episode = 0
- return false
- if str(_episode.get("state")) != BLOCKED or str(_episode.get("reason")) != "endpoint_lost":
- _endpoint_recovery_pending_episode = 0
- return false
- _endpoint_recovery_pending_episode = 0
- ## Losing a socket does not prove that the shared backend needs restarting.
- ## The probe retains ownership only after checking the exact listener again.
- _begin_start_episode()
- return str(_episode.get("state")) != BLOCKED
-
-
-func _recover_lost_endpoint_after(delay_seconds: float, episode_id: int) -> void:
- var tree := Engine.get_main_loop()
- if tree is SceneTree:
- await (tree as SceneTree).create_timer(delay_seconds).timeout
- if not is_instance_valid(self):
- return
- recover_lost_endpoint(episode_id)
-
-
-func complete_effect(episode_id: int, effect: String, result: Dictionary) -> bool:
- if episode_id != int(_episode.get("id", -1)):
- return false
- if effect != str(_episode.get("effect", "")):
- return false
- _episode["effect"] = ""
- match effect:
- PROBE:
- _complete_probe(result)
- LAUNCH:
- _complete_launch(result)
- PROVE:
- _complete_prove(result)
- REPLACE:
- _complete_replace(result)
- STOP:
- _complete_stop(result)
- _:
- return false
- return true
-
-
-func _complete_probe(result: Dictionary) -> void:
- match str(result.get("outcome", "blocked")):
- "compatible":
- var transport = result.get("transport")
- if transport == null or not transport.is_valid():
- _block("invalid_transport", "The server returned invalid transport authority.")
- return
- var disposition := str(result.get("owned_disposition", ""))
- if _process_grant != null and disposition == "owned":
- _ready("owned", transport, str(result.get("version", "")))
- elif _process_grant == null or disposition in ["gone", "replaced"]:
- _process_grant = null
- _ready("adopted", transport, str(result.get("version", "")))
- else:
- _block("process_authority_mismatch", "Managed process identity could not be proven during recovery. Click Restart to retry.")
- "free":
- if _process_grant != null:
- if str(result.get("owned_disposition", "")) not in ["gone", "replaced"]:
- _block("process_authority_mismatch", "The managed process is still running without a proven endpoint. Click Restart to retry.")
- return
- _process_grant = null
- _episode["phase"] = LAUNCH
- _episode["message"] = "Launching the managed server."
- _publish()
- var launch_plan := _plan.duplicate(true)
- launch_plan["baseline_instance_id"] = str(result.get("baseline_instance_id", ""))
- _request_effect(LAUNCH, launch_plan)
- _:
- _block(
- str(result.get("reason", "occupied")),
- str(result.get("message", "The configured endpoint is unavailable.")),
- result.get("target", {}),
- )
-
-
-func _complete_launch(result: Dictionary) -> void:
- if not bool(result.get("ok", false)):
- ## The process usually died before its identity could be captured
- ## because it refused to start; it says why in its startup report.
- _block(
- str(result.get("reason", "launch_failed")),
- str(result.get("message", "Server launch failed."))
- + startup_report_summary(str(_plan.get("startup_report", ""))),
- )
- return
- var pid := int(result.get("pid", 0))
- var fingerprint := str(result.get("fingerprint", ""))
- var launch := {
- "pid": pid,
- "fingerprint": fingerprint,
- "http_capability": str(result.get("http_capability", "")),
- "ws_capability": str(result.get("ws_capability", "")),
- "baseline_instance_id": str(result.get("baseline_instance_id", "")),
- }
- _process_grant = _owned_process_grant(pid, fingerprint)
- if not _process_grant.is_valid():
- _process_grant = null
- _block("launch_unproven", "The launched process identity could not be proven.")
- return
- _episode["launch"] = launch
- _episode["phase"] = PROVE
- _episode["prove_deadline_msec"] = (
- Time.get_ticks_msec() + int(_plan.get("prove_timeout_ms", DEFAULT_PROVE_TIMEOUT_MS))
- )
- _episode["message"] = "Waiting for the managed server capability record."
- _publish()
- _request_effect(PROVE, _prove_payload())
-
-
-func _complete_prove(result: Dictionary) -> void:
- if bool(result.get("pending", false)):
- _episode["proof_pending_reason"] = str(result.get("reason", "unknown"))
- if Time.get_ticks_msec() >= int(_episode.get("prove_deadline_msec", 0)):
- _block(
- "proof_timeout",
- "The managed server proof timed out at %s."
- % str(_episode.get("proof_pending_reason", "unknown"))
- + startup_report_summary(str(_plan.get("startup_report", ""))),
- )
- return
- _retry_effect_after(PROVE, _prove_payload(), 0.15)
- return
- if not bool(result.get("ok", false)):
- var message := str(result.get("message", "Managed server proof failed."))
- var pending_reason := str(_episode.get("proof_pending_reason", ""))
- if not pending_reason.is_empty():
- message += " Last pending proof: %s." % pending_reason
- _block(str(result.get("reason", "proof_failed")), message)
- return
- var launch: Dictionary = _episode.get("launch", {})
- var pid := int(result.get("pid", 0))
- var fingerprint := str(result.get("fingerprint", ""))
- var exact_grant = _owned_process_grant(pid, fingerprint)
- if not exact_grant.is_valid():
- _block("process_proof_failed", "The server process identity changed before proof completed.")
- return
- _process_grant = exact_grant
- launch["pid"] = pid
- launch["fingerprint"] = fingerprint
- _episode["launch"] = launch
- _ready("owned", result.get("transport"), str(result.get("version", "")))
-
-
-func _complete_replace(result: Dictionary) -> void:
- if not bool(result.get("ok", false)):
- _block(str(result.get("reason", "replacement_failed")), str(result.get("message", "Authorized replacement failed.")))
- return
- _process_grant = null
- _transport = null
- var launch: Dictionary = result.get("launch", {})
- if launch.is_empty():
- _begin_start_episode(int(_episode.get("id", 0)))
- return
- ## Our server was launched before the target was killed and is taking
- ## the port now; continue exactly as a fresh launch would, at its proof.
- _begin_start_episode(int(_episode.get("id", 0)), false)
- _complete_launch(launch)
-
-
-func _complete_stop(result: Dictionary) -> void:
- if not bool(result.get("ok", false)):
- _block(str(result.get("reason", "stop_refused")), str(result.get("message", "Managed stop authority no longer matches the process.")))
- return
- _finish_stop()
-
-
-func _finish_stop() -> void:
- var continue_with_start := str(_episode.get("after_stop", "")) == "start"
- var stopped_pid := get_server_pid()
- _process_grant = null
- _transport = null
- _replacement_authorization = null
- var pid_file := str(_plan.get("pid_file", ""))
- if not pid_file.is_empty() and FileAccess.file_exists(pid_file):
- DirAccess.remove_absolute(pid_file)
- var completed_id := int(_episode.get("id", 0))
- _episode = _dormant_episode(completed_id)
- _publish()
- if stopped_pid > 1:
- print("MCP | stopped server (PID %d)" % stopped_pid)
- if continue_with_start:
- _begin_start_episode()
-
-
-func _ready(kind: String, transport, version: String) -> void:
- if transport == null or not transport.is_valid():
- _block("invalid_transport", "The server returned invalid transport authority.")
- return
- _transport = transport
- _ready_since_msec = maxi(1, Time.get_ticks_msec())
- _episode["state"] = READY
- _episode["phase"] = ""
- _episode["ready_kind"] = kind
- _episode["actual_version"] = version
- _episode["reason"] = ""
- _episode["message"] = "Server ready (%s)." % kind
- _episode["blocked_target"] = {}
- _episode["launch"] = {}
- _publish()
- if kind == "owned":
- print("MCP | started server (PID %d)" % get_server_pid())
- elif kind == "adopted":
- print("MCP | adopted external server")
- transport_ready.emit(transport.ws_port(), transport.ws_capability())
- startup_finished.emit(kind)
-
-
-func _block(reason: String, message: String, target: Dictionary = {}) -> void:
- _transport = null
- _replacement_authorization = null
- _episode["state"] = BLOCKED
- _episode["phase"] = ""
- _episode["reason"] = reason
- _episode["message"] = message
- _episode["blocked_target"] = target.duplicate(true)
- _episode["effect"] = ""
- _publish()
- transport_cleared.emit(message)
- startup_finished.emit("blocked:%s" % reason)
-
-
-func _block_without_effect(reason: String, message: String) -> void:
- _next_episode_id += 1
- _episode = _dormant_episode(_next_episode_id)
- _episode["state"] = BLOCKED
- _episode["reason"] = reason
- _episode["message"] = message
- _publish()
-
-
-func _request_effect(kind: String, payload: Dictionary, force_inline := false) -> void:
- if not _effect.is_empty():
- _block("effect_overlap", "Lifecycle attempted overlapping effects.")
- return
- _episode["effect"] = kind
- _publish()
- var episode_id := int(_episode.id)
- var copied := payload.duplicate(true)
- if not bool(_plan.get("automatic_effects", true)):
- effect_requested.emit(episode_id, kind, copied)
- return
- if force_inline or not bool(_plan.get("defer_effects", true)) or not (Engine.get_main_loop() is SceneTree):
- complete_effect(episode_id, kind, _execute_effect(kind, copied))
- return
- var thread := Thread.new()
- if thread.start(_execute_effect.bind(kind, copied)) != OK:
- complete_effect(episode_id, kind, {"ok": false, "reason": "worker_start_failed"})
- return
- _effect = {"thread": thread, "episode_id": episode_id, "kind": kind}
- _await_effect(thread, episode_id, kind)
-
-
-func _await_effect(thread: Thread, episode_id: int, kind: String) -> void:
- var tree := Engine.get_main_loop()
- while thread.is_alive() and tree is SceneTree:
- await (tree as SceneTree).process_frame
- if _effect.get("thread") != thread:
- return
- var result: Variant = thread.wait_to_finish()
- _effect.clear()
- if result is Dictionary:
- complete_effect(episode_id, kind, result)
- else:
- complete_effect(episode_id, kind, {"ok": false, "reason": "invalid_effect_result"})
-
-
-func _cancel_effect() -> Dictionary:
- if _effect.is_empty():
- return {}
- var kind := str(_effect.get("kind", ""))
- var thread: Thread = _effect.get("thread")
- _effect.clear()
- if thread == null:
- return {"kind": kind, "result": {"ok": false, "reason": "missing_effect_thread"}}
- var result: Variant = thread.wait_to_finish()
- return {
- "kind": kind,
- "result": (
- result
- if result is Dictionary
- else {"ok": false, "reason": "invalid_effect_result"}
- ),
- }
-
-
-## Preserve process authority when cancellation joins a launch that already
-## spawned. The caller may then stop by exact fingerprint; it never kills by a
-## bare PID and never treats a failed/unproven launch as owned.
-func _adopt_cancelled_launch(cancelled: Dictionary) -> bool:
- if str(cancelled.get("kind", "")) != LAUNCH:
- return false
- var result: Dictionary = cancelled.get("result", {})
- if not bool(result.get("ok", false)):
- return false
- var grant = _owned_process_grant(
- int(result.get("pid", 0)), str(result.get("fingerprint", ""))
- )
- if not grant.is_valid():
- return false
- _process_grant = grant
- _episode["launch"] = {
- "pid": int(result.get("pid", 0)),
- "fingerprint": str(result.get("fingerprint", "")),
- "http_capability": str(result.get("http_capability", "")),
- "ws_capability": str(result.get("ws_capability", "")),
- "baseline_instance_id": str(result.get("baseline_instance_id", "")),
- }
- return true
-
-
-static func _owned_process_grant(pid: int, fingerprint: String):
- return Authority.OwnedProcessGrant.new(
- pid, fingerprint, maxi(1, Time.get_ticks_msec())
- )
-
-
-func _retry_effect_after(kind: String, payload: Dictionary, delay_seconds: float) -> void:
- var episode_id := int(_episode.get("id", 0))
- var tree := Engine.get_main_loop()
- if tree is SceneTree:
- await (tree as SceneTree).create_timer(delay_seconds).timeout
- if episode_id == int(_episode.get("id", -1)) and str(_episode.get("state")) == STARTING:
- _request_effect(kind, payload)
-
-
-func _execute_effect(kind: String, payload: Dictionary) -> Dictionary:
- match kind:
- PROBE:
- return _effect_probe(payload)
- LAUNCH:
- return _effect_launch(payload)
- PROVE:
- return _effect_prove(payload)
- REPLACE:
- return _effect_replace(payload)
- STOP:
- return _effect_stop(payload)
- return {"ok": false, "reason": "unknown_effect"}
-
-
-func _effect_probe(payload: Dictionary) -> Dictionary:
- var port := int(payload.http_port)
- var expected_version := str(payload.expected_version)
- var expected_ws_port := int(payload.expected_ws_port)
- var grant = payload.get("grant")
- var disposition := "gone"
- if grant != null:
- var pid := int(grant.process_id())
- var alive := PortResolver.pid_alive(pid)
- disposition = _owned_process_disposition(
- grant, alive, PortResolver.process_fingerprint(pid) if alive else "",
- )
- if disposition == "unproven":
- return _blocked_probe_result("process_authority_mismatch", port, {}, false,
- "managed process identity could not be proven; click Restart to retry")
- var capability := _read_capability(port)
- var live := _probe_with_capability(port, capability, int(payload.timeout_ms))
- if _authenticated_status_matches_record(live, capability):
- var version := str(live.get("version", ""))
- var ws_port := int(live.get("ws_port", 0))
- if _server_status_compatibility(version, expected_version, ws_port, expected_ws_port).get("compatible", false):
- if disposition == "owned" and (
- not PortResolver.find_all_pids_on_port(port).has(int(grant.process_id()))
- or not grant.matches(int(grant.process_id()), PortResolver.process_fingerprint(int(grant.process_id())))
- ):
- return _blocked_probe_result("process_authority_mismatch", port, live, false,
- "the authenticated endpoint does not match the managed process; click Restart to retry")
- return {
- "outcome": "compatible",
- "owned_disposition": disposition,
- "version": version,
- "transport": _transport_from(port, ws_port, live, capability),
- }
- return _blocked_probe_result("incompatible", port, live, true)
- if PortResolver.is_port_in_use(port):
- var detail := _record_probe_failure_detail(capability, live)
- var blocked := _blocked_probe_result("occupied", port, live, false, detail)
- var pre_v4 := _untrusted_pre_v4_occupant_version(port, int(payload.timeout_ms))
- if not pre_v4.is_empty():
- blocked["message"] = stale_pre_v4_message(port, pre_v4)
- blocked["target"]["hint"] = STALE_PRE_V4_HINT
- return blocked
- ## The server binds both ports before it publishes anything. A held
- ## WebSocket port (a server moved off the HTTP port, or another editor)
- ## would kill the launch at preflight; say so now and name the setting.
- if expected_ws_port > 0 and PortResolver.is_port_in_use(expected_ws_port):
- return ws_port_blocked_result(expected_ws_port)
- return {
- "outcome": "free",
- "owned_disposition": disposition,
- "baseline_instance_id": str(capability.get("instance_nonce", "")),
- }
-
-
-## Why an existing capability record did not authenticate the occupant, so a
-## "held by another process" report can be acted on. Empty when there was no
-## record to try, or when the probe succeeded and the mismatch is elsewhere.
-static func _record_probe_failure_detail(capability: Dictionary, live: Dictionary) -> String:
- if str(capability.get("http", "")).is_empty():
- return ""
- var error := str(live.get("error", "")).strip_edges()
- if error.is_empty():
- if str(live.get("name", "")) != "godot-ai":
- return "a godot-ai record for this port exists, but the listener did not answer as godot-ai"
- return "a godot-ai record for this port exists, but it belongs to a different server instance"
- return "a godot-ai record for this port exists, but its status probe failed: %s" % error
-
-
-static func ws_port_blocked_result(ws_port: int) -> Dictionary:
- return {
- "outcome": "blocked",
- "reason": "ws_occupied",
- "message": (
- "WebSocket port %d is already in use by another process. "
- + "Set `godot_ai/ws_port` in Editor Settings to a free port "
- + "(the dock's port picker moves both ports), then reconfigure your AI clients."
- ) % ws_port,
- "target": {"instance_id": "", "version": "", "port": ws_port, "replaceable": false},
- }
-
-
-func _effect_launch(payload: Dictionary) -> Dictionary:
- var port := int(payload.http_port)
- if bool(payload.get("http_port_reserved", false)):
- return {"ok": false, "reason": "port_reserved", "message": "Port %d is reserved by Windows." % port}
- var server_command: Array = payload.get("server_command", [])
- if server_command.is_empty():
- return {"ok": false, "reason": "no_command", "message": "No godot-ai server command was found."}
- var listener_problem := PortResolver.listener_tools_problem()
- if not listener_problem.is_empty():
- return {"ok": false, "reason": "listener_tools_missing", "message": listener_problem}
- ## Do not spawn into a directory the server cannot publish from (#988):
- ## the failure would only surface as a proof timeout three minutes later.
- var directory_problem := TransportCapability.directory_write_problem(port)
- if not directory_problem.is_empty():
- return {"ok": false, "reason": "capability_dir_unwritable", "message": directory_problem}
- var args: Array[String] = []
- args.assign(server_command.slice(1))
- args.append_array(_server_flags(payload))
- var environment := {
- "GODOT_AI_PLUGIN_SPAWNED": "1",
- }
- if int(payload.get("wait_for_port_ms", 0)) > 0:
- environment["GODOT_AI_WAIT_FOR_PORT_MS"] = str(int(payload.get("wait_for_port_ms", 0)))
- if OS.get_name() != "Windows" and not bool(payload.get("keep_alive", false)):
- environment["GODOT_AI_OWNER_PID"] = str(OS.get_process_id())
- if bool(payload.get("keep_alive", false)):
- environment["GODOT_AI_NO_IDLE_EXIT"] = "1"
- if bool(payload.get("disable_telemetry", false)):
- environment["GODOT_AI_DISABLE_TELEMETRY"] = "true"
- var worktree_src := str(payload.get("worktree_src", ""))
- if not worktree_src.is_empty():
- var old_pythonpath := str(payload.get("ambient_pythonpath", ""))
- var separator := ";" if OS.get_name() == "Windows" else ":"
- environment["PYTHONPATH"] = (
- worktree_src if old_pythonpath.is_empty() else worktree_src + separator + old_pythonpath
- )
- var pid_file := str(payload.get("pid_file", ""))
- if not pid_file.is_empty() and FileAccess.file_exists(pid_file):
- DirAccess.remove_absolute(pid_file)
- var startup_report := str(payload.get("startup_report", ""))
- if not startup_report.is_empty() and FileAccess.file_exists(startup_report):
- DirAccess.remove_absolute(startup_report)
- ## A report this launch could not clear would be read as this
- ## launch's: a stale "waiting_for_port" phase would let a replacement
- ## kill its occupant before the new server holds the port.
- if FileAccess.file_exists(startup_report):
- return {
- "ok": false,
- "reason": "stale_startup_report",
- "message": "A previous server's startup report could not be removed: %s" % startup_report,
- }
- ## Names this launch in what the server reports, so a phase read from the
- ## report is never another launch's.
- var launch_id := "%d-%d-%d" % [OS.get_process_id(), Time.get_ticks_usec(), randi()]
- environment["GODOT_AI_LAUNCH_ID"] = launch_id
- var spawned := spawn_capability_process(str(server_command[0]), args, environment)
- var pid := int(spawned.pid)
- if pid <= 1:
- return {"ok": false, "reason": "launch_failed", "message": "The server process could not be launched."}
- ## OS.create_process can return before POSIX exec replaces the child image.
- ## Capture only after the command is branded and its exact fingerprint is
- ## stable across two reads; otherwise a legitimate exec looks like PID reuse.
- var capture_started := Time.get_ticks_msec()
- var attempts: Array = []
- var exact_grant := PortResolver.capture_process_kill_grant(pid, true, attempts)
- var fingerprint_deadline := capture_started + LAUNCH_FINGERPRINT_TIMEOUT_MS
- while exact_grant.is_empty() and Time.get_ticks_msec() < fingerprint_deadline:
- OS.delay_msec(100)
- exact_grant = PortResolver.capture_process_kill_grant(pid, true, attempts)
- var fingerprint := str(exact_grant.get("fingerprint", ""))
- return {
- "ok": not fingerprint.is_empty(),
- "reason": "launch_unproven" if fingerprint.is_empty() else "",
- "message": (
- _launch_unproven_message(pid, attempts, Time.get_ticks_msec() - capture_started)
- if fingerprint.is_empty()
- else ""
- ),
- "pid": pid,
- "fingerprint": fingerprint,
- "http_capability": spawned.http,
- "ws_capability": spawned.websocket,
- "baseline_instance_id": str(payload.get("baseline_instance_id", "")),
- "launch_id": launch_id,
- }
-
-
-## Name which identity check failed so a Windows report can be acted on
-## (#988, #1012 both surfaced as this bare sentence). Diagnostic only: the
-## values are read once more after the capture budget and never grant
-## authority.
-func _launch_unproven_message(pid: int, attempts: Array, elapsed_ms: int) -> String:
- var snapshot: Variant = _capture_process_snapshot(pid)
- var alive := PortResolver.pid_alive(pid, snapshot)
- var commandline := PortResolver.process_commandline(pid, snapshot) if alive else ""
- if commandline.length() > 200:
- commandline = commandline.substr(0, 199) + "…"
- ## Summarise the per-attempt refusals as "reason×count" in first-seen order.
- var counts := {}
- var order: Array[String] = []
- for entry in attempts:
- var key := str(entry)
- if not counts.has(key):
- counts[key] = 0
- order.append(key)
- counts[key] = int(counts[key]) + 1
- var summary: Array[String] = []
- for key in order:
- summary.append("%s×%d" % [key, int(counts[key])])
- return (
- "The launched process identity could not be captured in %d attempts over %.1f s "
- + "(pid %d, now alive=%s, refusals: %s%s)."
- ) % [
- attempts.size(),
- elapsed_ms / 1000.0,
- pid,
- "unknown" if PortResolver.capture_failed(snapshot) else ("yes" if alive else "no"),
- ", ".join(summary) if not summary.is_empty() else "none recorded",
- "" if commandline.is_empty() else "; command: " + commandline,
- ]
-
-
-func _capture_process_snapshot(pid: int) -> Variant:
- return PortResolver.capture_process_snapshot(pid)
-
-
-func _effect_prove(payload: Dictionary) -> Dictionary:
- var launch_grant = payload.get("grant")
- var launch_pid := int(launch_grant.process_id()) if launch_grant != null else -1
- ## The pid file is an untrusted hint. Reuse its ancestor snapshot only
- ## when it includes the launcher; the original PID read and all final
- ## capability, listener, identity, and lineage checks still follow.
- var hinted_pid := -1
- var first_snapshot: Variant = null
- var launch_snapshot: Variant = null
- if OS.get_name() == "Windows" and launch_pid > 1:
- hinted_pid = PortResolver.read_pid_file(str(payload.get("pid_file", "")))
- if hinted_pid > 1:
- first_snapshot = _capture_process_snapshot(hinted_pid)
- if PortResolver.process_descends_from(hinted_pid, launch_pid, first_snapshot):
- launch_snapshot = first_snapshot
- if launch_snapshot == null:
- launch_snapshot = _capture_process_snapshot(launch_pid)
- first_snapshot = null
- if PortResolver.capture_failed(launch_snapshot):
- return {"pending": true, "reason": "identity_unavailable"}
- var launch_alive := PortResolver.pid_alive(launch_pid, launch_snapshot)
- var launch_fingerprint := (
- PortResolver.process_fingerprint(launch_pid, launch_snapshot)
- if launch_alive
- else ""
- )
- var launch_disposition := _owned_process_disposition(
- launch_grant,
- launch_alive,
- launch_fingerprint,
- )
- if launch_disposition != "owned":
- return {
- "ok": false,
- "reason": "launch_%s" % launch_disposition,
- "message": (
- "The launched server process exited or changed identity before publishing capabilities."
- + startup_report_summary(str(payload.get("startup_report", "")))
- ),
- }
- var port := int(payload.http_port)
- var capability := _read_capability(port)
- if capability.is_empty() or str(capability.get("instance_nonce", "")) == str(payload.get("baseline_instance_id", "")):
- return {"pending": true, "reason": "capability_record"}
- if (
- str(capability.get("http", "")) != str(payload.get("http_capability", ""))
- or str(capability.get("websocket", "")) != str(payload.get("ws_capability", ""))
- ):
- return {"pending": true, "reason": "capability_pair"}
- var live := _probe_with_capability(port, capability, int(payload.timeout_ms))
- if not _authenticated_status_matches_record(live, capability):
- return {"pending": true, "reason": "authenticated_status"}
- var version := str(live.get("version", ""))
- var ws_port := int(live.get("ws_port", 0))
- if not _server_status_compatibility(
- version, str(payload.expected_version), ws_port, int(payload.expected_ws_port)
- ).get("compatible", false):
- return {"ok": false, "reason": "incompatible", "message": "The launched server does not match this plugin."}
- var pid := PortResolver.read_pid_file(str(payload.get("pid_file", "")))
- if pid <= 1:
- return {"pending": true, "reason": "pid_file"}
- if not PortResolver.find_all_pids_on_port(port).has(pid):
- return {"pending": true, "reason": "listener_pid"}
- if first_snapshot == null or pid != hinted_pid:
- first_snapshot = (
- launch_snapshot if pid == launch_pid else _capture_process_snapshot(pid)
- )
- if PortResolver.capture_failed(first_snapshot):
- return {"pending": true, "reason": "identity_unavailable"}
- if not PortResolver.pid_cmdline_is_godot_ai(pid, first_snapshot):
- return {"pending": true, "reason": "process_brand"}
- if not PortResolver.process_descends_from(pid, launch_pid, first_snapshot):
- return {"pending": true, "reason": "launch_lineage"}
- var fingerprint := PortResolver.process_fingerprint(pid, first_snapshot)
- if fingerprint.is_empty():
- return {"pending": true, "reason": "process_fingerprint"}
- ## Close every capture window before minting process authority. The same
- ## launch-lineage PID must still own the listener, the private capability
- ## record must still name the authenticated instance, and the process
- ## fingerprint must remain unchanged after the final probe.
- var final_capability := _read_capability(port)
- var final_live := _probe_with_capability(port, final_capability, int(payload.timeout_ms))
- var final_snapshot: Variant = _capture_process_snapshot(pid)
- if PortResolver.capture_failed(final_snapshot):
- return {"pending": true, "reason": "identity_unavailable"}
- if (
- PortResolver.read_pid_file(str(payload.get("pid_file", ""))) != pid
- or not PortResolver.find_all_pids_on_port(port).has(pid)
- or not PortResolver.process_descends_from(pid, launch_pid, final_snapshot)
- or not PortResolver.pid_cmdline_is_godot_ai(pid, final_snapshot)
- or PortResolver.process_fingerprint(pid, final_snapshot) != fingerprint
- or final_capability != capability
- or not _authenticated_status_matches_record(final_live, final_capability)
- or str(final_live.get("instance_id", "")) != str(live.get("instance_id", ""))
- ):
- return {"pending": true, "reason": "final_capture_window"}
- var final_version := str(final_live.get("version", ""))
- var final_ws_port := int(final_live.get("ws_port", 0))
- if not _server_status_compatibility(
- final_version,
- str(payload.expected_version),
- final_ws_port,
- int(payload.expected_ws_port),
- ).get("compatible", false):
- return {
- "ok": false,
- "reason": "incompatible",
- "message": "The launched server changed compatibility before authority was granted.",
- }
- return {
- "ok": true,
- "pid": pid,
- "fingerprint": fingerprint,
- "version": final_version,
- "transport": _transport_from(port, final_ws_port, final_live, final_capability),
- }
-
-
-func _effect_replace(payload: Dictionary) -> Dictionary:
- var target: Dictionary = payload.get("target", {})
- var port := int(target.get("port", 0))
- var capability := _read_capability(port)
- var live := _probe_with_capability(port, capability, int(payload.timeout_ms))
- if not _replacement_target_matches(target, live, capability):
- return {"ok": false, "reason": "replacement_target_changed", "message": "The authorized server changed before replacement."}
- var candidates: Array[int] = []
- for pid in PortResolver.find_all_pids_on_port(port):
- if PortResolver.pid_cmdline_is_godot_ai(pid):
- candidates.append(pid)
- if candidates.size() != 1:
- return {"ok": false, "reason": "replacement_unproven", "message": "The authorized server process could not be proven."}
- var pid := candidates[0]
- var fingerprint := PortResolver.process_fingerprint(pid)
- if fingerprint.is_empty():
- return {"ok": false, "reason": "replacement_unproven", "message": "The authorized server process could not be fingerprinted."}
- # Reprobe, then bind the kill to the captured PID + start fingerprint.
- live = _probe_with_capability(port, capability, int(payload.timeout_ms))
- if (
- not _replacement_target_matches(target, live, capability)
- or not PortResolver.find_all_pids_on_port(port).has(pid)
- or PortResolver.process_fingerprint(pid) != fingerprint
- ):
- return {"ok": false, "reason": "replacement_target_changed", "message": "The authorized server changed before replacement."}
- ## Launch our server before the kill so the port passes straight from
- ## the target to it (the server waits for the port); a bridge polling
- ## for a free port to spawn its own backend never sees one. The launch
- ## baseline is the record the target published, which the proof must
- ## see replaced.
- var launch_plan: Dictionary = payload.get("launch", {})
- var launch := {}
- if not launch_plan.is_empty():
- launch_plan = launch_plan.duplicate(true)
- launch_plan["baseline_instance_id"] = str(capability.get("instance_nonce", ""))
- launch = _effect_launch(launch_plan)
- if not bool(launch.get("ok", false)):
- return {
- "ok": false,
- "reason": str(launch.get("reason", "launch_failed")),
- "message": str(launch.get("message", "Server launch failed.")),
- }
- ## Kill only once the launched server is at its bind loop; a launch
- ## that spends seconds getting there (uvx installing the new version)
- ## would otherwise leave the port free for a bridge to spawn into.
- var ready := _wait_for_launch_port_wait(
- str(launch_plan.get("startup_report", "")),
- str(launch.get("launch_id", "")),
- int(launch.pid),
- str(launch.fingerprint),
- )
- if not bool(ready.get("ok", false)):
- PortResolver.kill_exact_processes(
- [{"pid": int(launch.pid), "fingerprint": str(launch.fingerprint)}], true
- )
- return {"ok": false, "reason": str(ready.reason), "message": str(ready.message)}
- if (
- not PortResolver.find_all_pids_on_port(port).has(pid)
- or PortResolver.process_fingerprint(pid) != fingerprint
- ):
- PortResolver.kill_exact_processes(
- [{"pid": int(launch.pid), "fingerprint": str(launch.fingerprint)}], true
- )
- return {"ok": false, "reason": "replacement_target_changed", "message": "The authorized server changed before replacement."}
- var killed := PortResolver.kill_exact_processes(
- [{"pid": pid, "fingerprint": fingerprint}],
- true,
- )
- if not killed.has(pid):
- if not launch.is_empty():
- PortResolver.kill_exact_processes(
- [{"pid": int(launch.pid), "fingerprint": str(launch.fingerprint)}], true
- )
- return {"ok": false, "reason": "replacement_failed"}
- if launch.is_empty():
- PortResolver.wait_for_port_free(port, 5.0)
- return {"ok": not PortResolver.is_port_in_use(port), "reason": ""}
- return {"ok": true, "reason": "", "launch": launch}
-
-
-## Poll the launched server's startup report until it says the port wait is
-## running, the process is gone, or the budget is spent. No report path
-## (a plan without one) keeps the old ordering: kill straight away.
-func _wait_for_launch_port_wait(
- startup_report: String, launch_id: String, pid: int, fingerprint: String
-) -> Dictionary:
- if startup_report.is_empty():
- return {"ok": true}
- var deadline := Time.get_ticks_msec() + REPLACEMENT_LAUNCH_READY_TIMEOUT_MS
- while true:
- var reached := launch_reached_port_wait(startup_report, launch_id)
- ## The phase counts only from a process still there to hold the
- ## port: one that exited after writing it must not cost the occupant.
- if PortResolver.process_fingerprint(pid) != fingerprint:
- return {
- "ok": false,
- "reason": "replacement_launch_exited",
- "message": (
- "The replacement server exited before it could wait for the port."
- + startup_report_summary(startup_report)
- ),
- }
- if reached:
- return {"ok": true}
- if Time.get_ticks_msec() >= deadline:
- return {
- "ok": false,
- "reason": "replacement_launch_stalled",
- "message": "The replacement server did not reach its port wait within %d s." % int(REPLACEMENT_LAUNCH_READY_TIMEOUT_MS / 1000),
- }
- OS.delay_msec(100)
- return {"ok": true}
-
-
-## Whether the startup report records the server of `launch_id` at its port
-## wait (the phase it writes before its first bind attempt when the plugin
-## asked it to wait for the port). A failure written later replaces the
-## phase; a report naming another launch, or none, is never this launch's.
-static func launch_reached_port_wait(startup_report: String, launch_id: String) -> bool:
- if startup_report.is_empty() or launch_id.is_empty() or not FileAccess.file_exists(startup_report):
- return false
- var file := FileAccess.open(startup_report, FileAccess.READ)
- if file == null:
- return false
- var raw := file.get_as_text().strip_edges()
- file.close()
- if raw.is_empty() or raw.length() > 8192:
- return false
- var parsed: Variant = JSON.parse_string(raw)
- if not (parsed is Dictionary):
- return false
- return (
- str(parsed.get("phase", "")) == STARTUP_PHASE_WAITING_FOR_PORT
- and str(parsed.get("launch_id", "")) == launch_id
- )
-
-
-func _effect_stop(payload: Dictionary) -> Dictionary:
- var grant = payload.get("grant")
- if grant == null:
- return {"ok": true}
- var pid := int(grant.process_id())
- var port := int(payload.get("http_port", 0))
- var alive := PortResolver.pid_alive(pid)
- var fingerprint := PortResolver.process_fingerprint(pid) if alive else ""
- var disposition := _owned_process_disposition(grant, alive, fingerprint)
- if disposition == "unproven":
- return {"ok": false, "reason": "process_authority_mismatch", "message": "Managed process identity no longer matches its stop grant."}
- var grants: Array[Dictionary] = []
- var server_grant := _launched_server_kill_grant(payload, pid)
- if not server_grant.is_empty():
- grants.append(server_grant)
- if disposition == "owned":
- grants.append({"pid": pid, "fingerprint": fingerprint})
- PortResolver.kill_exact_processes(grants, true)
- if port > 0 and not grants.is_empty():
- PortResolver.wait_for_port_free(port, 3.0)
- var targets_gone := true
- for exact in grants:
- if PortResolver.process_fingerprint(int(exact.pid)) == str(exact.fingerprint):
- targets_gone = false
- var port_free := port <= 0 or not PortResolver.is_port_in_use(port)
- ## Killing something of ours must also free its listener. When there was
- ## nothing of ours left to kill, whoever holds the port now is not ours to
- ## prove stopped; the next start's probe deals with that occupant.
- var stopped := targets_gone and (grants.is_empty() or port_free)
- return {
- "ok": stopped,
- "already_gone": grants.is_empty() and port_free,
- "reason": "" if stopped else "process_stop_failed",
- "message": "" if stopped else "A launched server process or listener could not be proven stopped.",
- }
-
-
-func _launched_server_kill_grant(payload: Dictionary, launch_pid: int) -> Dictionary:
- var launch: Dictionary = payload.get("launch", {})
- if launch.is_empty():
- return {}
- var server_pid := PortResolver.read_pid_file(str(payload.get("pid_file", "")))
- var port := int(payload.get("http_port", 0))
- if (
- server_pid <= 1
- or server_pid == launch_pid
- or not PortResolver.process_descends_from(server_pid, launch_pid)
- or not PortResolver.find_all_pids_on_port(port).has(server_pid)
- ):
- return {}
- var capability := _read_capability(port)
- if (
- str(capability.get("http", "")) != str(launch.get("http_capability", ""))
- or str(capability.get("websocket", "")) != str(launch.get("ws_capability", ""))
- ):
- return {}
- var live := _probe_with_capability(
- port, capability, int(payload.get("timeout_ms", DEFAULT_PROBE_TIMEOUT_MS))
- )
- if not _authenticated_status_matches_record(live, capability):
- return {}
- var exact := PortResolver.capture_process_kill_grant(server_pid, true)
- if exact.is_empty():
- return {}
- if (
- PortResolver.read_pid_file(str(payload.get("pid_file", ""))) != server_pid
- or not PortResolver.process_descends_from(server_pid, launch_pid)
- or not PortResolver.find_all_pids_on_port(port).has(server_pid)
- or PortResolver.process_fingerprint(server_pid) != str(exact.fingerprint)
- ):
- return {}
- return exact
-
-
-static func _owned_process_disposition(grant, alive: bool, fingerprint: String) -> String:
- if grant == null:
- return "gone"
- if not alive:
- return "gone"
- var pid := int(grant.process_id())
- if fingerprint.is_empty():
- return "unproven"
- if not grant.matches(pid, fingerprint):
- return "replaced"
- return "owned"
-
-
-func _prove_payload() -> Dictionary:
- var launch: Dictionary = _episode.get("launch", {})
- return {
- "http_port": int(_plan.http_port),
- "expected_ws_port": int(_plan.ws_port),
- "expected_version": str(_plan.expected_version),
- "timeout_ms": int(_plan.probe_timeout_ms),
- "pid_file": str(_plan.get("pid_file", "")),
- "startup_report": str(_plan.get("startup_report", "")),
- "http_capability": str(launch.get("http_capability", "")),
- "ws_capability": str(launch.get("ws_capability", "")),
- "baseline_instance_id": str(launch.get("baseline_instance_id", "")),
- "grant": _process_grant,
- }
-
-
-func _stop_payload(launch: Dictionary) -> Dictionary:
- return {
- "grant": _process_grant,
- "http_port": int(_plan.get("http_port", 0)),
- "pid_file": str(_plan.get("pid_file", "")),
- "timeout_ms": int(_plan.get("probe_timeout_ms", DEFAULT_PROBE_TIMEOUT_MS)),
- "launch": launch.duplicate(true),
- }
-
-
-func _probe_current_transport() -> Dictionary:
- if _transport == null:
- return {}
- return _probe_with_capability(
- _transport.http_port(),
- {"http": _transport.http_capability()},
- int(_plan.get("probe_timeout_ms", DEFAULT_PROBE_TIMEOUT_MS)),
- )
-
-
-func _read_capability(port: int) -> Dictionary:
- return TransportCapability.read_for_http_port(
- port, str(_plan.get("capability_path", ""))
- )
-
-
-static func probe_live_server_status(
- port: int, timeout_ms := DEFAULT_PROBE_TIMEOUT_MS, captured_path := ""
-) -> Dictionary:
- var capability := TransportCapability.read_for_http_port(port, captured_path)
- return _probe_with_capability(port, capability, timeout_ms)
-
-
-static func _probe_with_capability(port: int, capability: Dictionary, timeout_ms: int) -> Dictionary:
- var result := {"reachable": false, "name": "", "version": "", "ws_port": 0, "instance_id": "", "status_code": 0, "error": ""}
- var http_capability := str(capability.get("http", ""))
- if http_capability.is_empty():
- result.error = "missing_capability"
- return result
- ## Windows can otherwise spend the whole connect deadline on an unbound
- ## loopback port. This is only an unreachable probe; callers still check
- ## occupancy and prove any later listener before using it.
- if OS.get_name() == "Windows" and PortResolver.can_bind_local_port(port):
- result.error = "port_unbound"
- return result
- var client := HTTPClient.new()
- var error := client.connect_to_host("127.0.0.1", port)
- if error != OK:
- result.error = "connect_%d" % error
- return result
- var deadline := Time.get_ticks_msec() + maxi(1, timeout_ms)
- while client.get_status() in [HTTPClient.STATUS_RESOLVING, HTTPClient.STATUS_CONNECTING]:
- client.poll()
- if Time.get_ticks_msec() >= deadline:
- result.error = "connect_timeout"
- return result
- OS.delay_msec(10)
- if client.get_status() != HTTPClient.STATUS_CONNECTED:
- result.error = "connect_status_%d" % client.get_status()
- return result
- error = client.request(HTTPClient.METHOD_GET, STATUS_PATH, [
- "Accept: application/json", "Authorization: Bearer %s" % http_capability,
- ])
- if error != OK:
- result.error = "request_%d" % error
- return result
- var body := PackedByteArray()
- while true:
- var status := client.get_status()
- if status == HTTPClient.STATUS_REQUESTING:
- client.poll()
- elif status == HTTPClient.STATUS_BODY:
- client.poll()
- var chunk := client.read_response_body_chunk()
- if body.size() + chunk.size() > MAX_STATUS_BODY_BYTES:
- result.error = "response_too_large"
- return result
- body.append_array(chunk)
- elif status == HTTPClient.STATUS_CONNECTED:
- break
- else:
- result.error = "response_status_%d" % status
- return result
- if Time.get_ticks_msec() >= deadline:
- result.error = "response_timeout"
- return result
- OS.delay_msec(10)
- result.status_code = client.get_response_code()
- if int(result.status_code) != 200:
- result.error = "http_%d" % int(result.status_code)
- return result
- var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
- if not (parsed is Dictionary):
- result.error = "invalid_json"
- return result
- result.merge(project_status_payload(parsed), true)
- return result
-
-
-static func project_status_payload(parsed: Dictionary) -> Dictionary:
- var projected := {
- "reachable": true,
- "name": str(parsed.get("name", "")),
- "version": str(parsed.get("server_version", parsed.get("version", ""))),
- "ws_port": int(parsed.get("ws_port", 0)),
- "instance_id": str(parsed.get("instance_id", "")),
- "package_path": str(parsed.get("package_path", "")),
- }
- var leases: Variant = parsed.get("active_lease_count")
- if leases is int or leases is float:
- var numeric := float(leases)
- if is_finite(numeric) and numeric == floor(numeric):
- projected.active_lease_count = maxi(0, int(numeric))
- var telemetry: Variant = parsed.get("telemetry_enabled")
- if telemetry is bool:
- projected.telemetry_enabled = telemetry
- return projected
-
-
-static func _authenticated_status_matches_record(live: Dictionary, capability: Dictionary) -> bool:
- return (
- bool(live.get("reachable", false))
- and str(live.get("name", "")) == "godot-ai"
- and not str(capability.get("instance_nonce", "")).is_empty()
- and str(live.get("instance_id", "")) == str(capability.get("instance_nonce", ""))
- )
-
-
-static func _replacement_target_matches(
- target: Dictionary, live: Dictionary, capability: Dictionary
-) -> bool:
- return (
- _authenticated_status_matches_record(live, capability)
- and str(live.get("instance_id", "")) == str(target.get("instance_id", ""))
- and str(live.get("version", "")) == str(target.get("version", ""))
- )
-
-
-static func _transport_from(port: int, ws_port: int, live: Dictionary, capability: Dictionary):
- return Authority.TransportAuthority.new(
- port,
- ws_port,
- str(live.get("instance_id", "")),
- str(capability.get("http", "")),
- str(capability.get("websocket", "")),
- )
-
-
-static func _blocked_probe_result(
- reason: String, port: int, live: Dictionary, allow_replacement := false, detail := ""
-) -> Dictionary:
- var instance_id := str(live.get("instance_id", ""))
- var version := str(live.get("version", ""))
- var replaceable := (
- allow_replacement
- and str(live.get("name", "")) == "godot-ai"
- and not instance_id.is_empty()
- and not version.is_empty()
- )
- var message := "Port %d is occupied by another process." % port
- if not detail.is_empty():
- message = "Port %d is occupied by another process (%s)." % [port, detail]
- if replaceable:
- message = "Port %d is occupied by godot-ai v%s; choose Replace to continue." % [port, version]
- return {
- "outcome": "blocked",
- "reason": reason,
- "message": message,
- "target": {"instance_id": instance_id, "version": version, "port": port, "replaceable": replaceable},
- }
-
-
-static func generate_capability_pair() -> Dictionary:
- var http := _random_hex(32)
- var websocket := _random_hex(32)
- while websocket == http:
- websocket = _random_hex(32)
- return {"http": http, "websocket": websocket}
-
-
-static func spawn_capability_process(
- command: String, args: Array[String], extra_environment: Dictionary = {}
-) -> Dictionary:
- var pair := generate_capability_pair()
- var environment := extra_environment.duplicate(true)
- environment["GODOT_AI_HTTP_CAPABILITY"] = pair.http
- environment["GODOT_AI_WS_TOKEN"] = pair.websocket
- return {
- "pid": _create_process_with_environment(command, args, environment),
- "http": pair.http,
- "websocket": pair.websocket,
- }
-
-
-static func _random_hex(byte_count: int) -> String:
- return Crypto.new().generate_random_bytes(byte_count).hex_encode()
-
-
-static func _create_process_with_environment(command: String, args: Array[String], environment: Dictionary) -> int:
- var previous := {}
- PortResolver.lock_process_spawn()
- var full_command: Array = [command]
- full_command.append_array(args)
- var saved_uv_environment := (
- UvResolution.isolate_environment()
- if UvResolution.is_production_command(full_command)
- else {}
- )
- for key in environment:
- previous[key] = {"present": OS.has_environment(key), "value": OS.get_environment(key)}
- OS.set_environment(key, str(environment[key]))
- var pid := OS.create_process(command, args)
- for key in previous:
- if bool(previous[key].present):
- OS.set_environment(key, str(previous[key].value))
- else:
- OS.unset_environment(key)
- UvResolution.restore_environment(saved_uv_environment)
- PortResolver.unlock_process_spawn()
- return pid
-
-
-static func _server_flags(plan: Dictionary) -> Array[String]:
- var flags: Array[String] = [
- "--transport", "streamable-http",
- "--port", str(plan.http_port),
- "--ws-port", str(plan.ws_port),
- "--pid-file", str(plan.get("pid_file", "")),
- ]
- var startup_report := str(plan.get("startup_report", ""))
- if not startup_report.is_empty():
- flags.append_array(["--startup-report", startup_report])
- var excluded := str(plan.get("excluded_domains", ""))
- if not excluded.is_empty():
- flags.append_array(["--exclude-domains", excluded])
- var allow_hosts := str(plan.get("allow_hosts", ""))
- if not allow_hosts.is_empty():
- flags.append_array(["--allow-host", allow_hosts])
- return flags
-
-
-## " Server reported: : " from the launch's startup
-## report, or "" when there is none. Bounded and single-line: the report is a
-## file the spawned server wrote, so it is quoted, never interpreted.
-static func startup_report_summary(path: String, max_chars := 600) -> String:
- if path.is_empty() or not FileAccess.file_exists(path):
- return ""
- var file := FileAccess.open(path, FileAccess.READ)
- if file == null:
- return ""
- var raw := file.get_as_text().strip_edges()
- file.close()
- if raw.is_empty() or raw.length() > 8192:
- return ""
- var parsed: Variant = JSON.parse_string(raw)
- if not (parsed is Dictionary):
- return ""
- var error := str(parsed.get("error", "")).strip_edges()
- var message := str(parsed.get("message", "")).strip_edges()
- var hint := str(parsed.get("hint", "")).strip_edges()
- if error.is_empty() and message.is_empty():
- return ""
- var text := " Server reported: "
- if not error.is_empty():
- text += error + (": " if not message.is_empty() else "")
- text += message
- if not hint.is_empty():
- text += " " + hint
- text = text.replace("\n", " ").replace("\r", " ")
- if text.length() > max_chars:
- text = text.substr(0, max_chars - 1) + "…"
- return text
-## Untrusted, bounded, tokenless read of `/godot-ai/status`, used ONLY to
-## word the BLOCKED message when a pre-v4 godot-ai server holds the port. A
-## v3 server never wrote a v4 capability record, so the authenticated probe
-## cannot even name it, and the dock could only say "another process". This
-## peek is not a fallback: its result never enters the probe outcome, never
-## becomes a transport, and never contributes to replacement or kill
-## authority; `_blocked_probe_result` keeps `replaceable` false. It answers
-## one question, "is that a Godot AI 3.x server?", so the user is told to
-## quit the AI client whose bridge keeps it alive instead of hunting for a
-## foreign process. docs/server-lifecycle.md states the exception.
-static func _untrusted_pre_v4_occupant_version(port: int, timeout_ms: int) -> String:
- var client := HTTPClient.new()
- if client.connect_to_host("127.0.0.1", port) != OK:
- return ""
- var deadline := Time.get_ticks_msec() + maxi(1, timeout_ms)
- while client.get_status() in [HTTPClient.STATUS_RESOLVING, HTTPClient.STATUS_CONNECTING]:
- client.poll()
- if Time.get_ticks_msec() >= deadline:
- return ""
- OS.delay_msec(10)
- if client.get_status() != HTTPClient.STATUS_CONNECTED:
- return ""
- if client.request(HTTPClient.METHOD_GET, STATUS_PATH, ["Accept: application/json"]) != OK:
- return ""
- var body := PackedByteArray()
- while true:
- var status := client.get_status()
- if status == HTTPClient.STATUS_REQUESTING:
- client.poll()
- elif status == HTTPClient.STATUS_BODY:
- client.poll()
- var chunk := client.read_response_body_chunk()
- if body.size() + chunk.size() > MAX_STATUS_BODY_BYTES:
- return ""
- body.append_array(chunk)
- elif status == HTTPClient.STATUS_CONNECTED:
- break
- else:
- return ""
- if Time.get_ticks_msec() >= deadline:
- return ""
- OS.delay_msec(10)
- if client.get_response_code() != 200:
- return ""
- var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
- return pre_v4_version_from_status(parsed)
-
-
-## "3.2.4" for a payload that claims to be a godot-ai server below v4, else "".
-## Pure and bounded so the contract is unit-testable without a socket.
-static func pre_v4_version_from_status(parsed: Variant) -> String:
- if not (parsed is Dictionary):
- return ""
- if str(parsed.get("name", "")) != "godot-ai":
- return ""
- var version := str(parsed.get("server_version", "")).strip_edges()
- if version.length() > 32 or not version.begins_with("3."):
- return ""
- ## Every dot-separated component must be digits and non-empty: "3.",
- ## "3..2" and "3.2." are not versions and must not earn the stale-server
- ## wording or the slow post-update retry.
- for part in version.split("."):
- if part.is_empty():
- return ""
- for index in range(part.length()):
- var code := part.unicode_at(index)
- if code < 48 or code > 57:
- return ""
- return version
-
-
-static func stale_pre_v4_message(port: int, version: String) -> String:
- return (
- "Port %d is held by a Godot AI %s server that an AI client attached before the update "
- + "keeps alive. Quit and relaunch that client (restarting its MCP server is not enough); "
- + "the old server exits on its own within about three minutes, and the editor retries "
- + "meanwhile. Otherwise click Restart Server afterwards."
- ) % [port, version]
-
-
-static func active_lease_count(live: Dictionary) -> int:
- if str(live.get("name", "")) != "godot-ai":
- return 0
- return maxi(0, int(live.get("active_lease_count", 0)))
-
-
-static func _server_version_compatibility(actual_version: String, expected_version: String) -> Dictionary:
- return ServerVersionCheck.evaluate(actual_version, expected_version)
-
-
-static func _server_status_compatibility(actual_version: String, expected_version: String, actual_ws_port: int, expected_ws_port: int) -> Dictionary:
- var version := _server_version_compatibility(actual_version, expected_version)
- if not bool(version.compatible):
- return version
- if actual_ws_port != expected_ws_port:
- return {"compatible": false, "reason": "ws_port_mismatch"}
- return {"compatible": true, "reason": ""}
-
-
-func get_status_dict() -> Dictionary:
- var state := str(_episode.get("state", DORMANT))
- var reason := str(_episode.get("reason", ""))
- return {
- "episode_id": int(_episode.get("id", 0)),
- "episode_state": state,
- "phase": str(_episode.get("phase", "")),
- "ready_kind": str(_episode.get("ready_kind", "")),
- "state": _dock_state(state, reason),
- "expected_version": str(_episode.get("expected_version", _plan.get("expected_version", ""))),
- "actual_version": str(_episode.get("actual_version", "")),
- "actual_name": "godot-ai" if state == READY else "",
- "message": str(_episode.get("message", "")),
- "connection_blocked": state != READY,
- "can_recover_incompatible": can_recover_incompatible_server(),
- "conflict_port": int(_episode.get("blocked_target", {}).get("port", 0)),
- "conflict_version": str(_episode.get("blocked_target", {}).get("version", "")),
- ## Wording-only hint from the untrusted pre-v4 peek; never authority.
- "blocked_hint": str(_episode.get("blocked_target", {}).get("hint", "")),
- "keep_alive": bool(_plan.get("keep_alive", false)),
- "recovery_attempt": _endpoint_recovery_attempts,
- "recovery_limit": ENDPOINT_RECOVERY_DELAYS_SECONDS.size(),
- }
-
-
-func get_server_pid() -> int:
- return int(_process_grant.process_id()) if _process_grant != null else -1
-
-
-func is_connection_blocked() -> bool:
- return str(_episode.get("state")) != READY
-
-
-func has_managed_server() -> bool:
- return _process_grant != null and _process_grant.is_valid()
-
-
-func can_restart_managed_server() -> bool:
- return has_managed_server()
-
-
-func episode_snapshot() -> Dictionary:
- var snapshot := _episode.duplicate(true)
- var launch: Dictionary = snapshot.get("launch", {})
- launch.erase("http_capability")
- launch.erase("ws_capability")
- snapshot["launch"] = launch
- return snapshot
-
-
-func authority_snapshot() -> Dictionary:
- return {
- "transport": _transport.public_snapshot() if _transport != null else {},
- "owned_pid": get_server_pid(),
- "replacement_available": _replacement_authorization != null,
- }
-
-
-func _publish() -> void:
- snapshot_changed.emit(get_status_dict().duplicate(true))
-
-
-static func _dormant_episode(episode_id: int) -> Dictionary:
- return {
- "id": episode_id, "state": DORMANT, "phase": "", "reason": "",
- "message": "Server lifecycle is dormant.", "ready_kind": "", "effect": "",
- "expected_version": "", "actual_version": "", "blocked_target": {},
- "launch": {}, "after_stop": "",
- }
-
-
-static func _stopping_episode(episode_id: int, launch: Dictionary, after_stop: String) -> Dictionary:
- return {
- "id": episode_id, "state": STOPPING, "phase": "", "reason": "",
- "message": "Stopping the managed server.", "ready_kind": "", "effect": "",
- "expected_version": "", "actual_version": "", "blocked_target": {},
- "launch": launch, "after_stop": after_stop,
- }
-
-
-static func _dock_state(state: String, reason: String) -> int:
- match state:
- DORMANT:
- return ServerState.STOPPED
- STARTING:
- return ServerState.SPAWNING
- READY:
- return ServerState.READY
- RECOVERING, STOPPING:
- return ServerState.STOPPING
- BLOCKED:
- match reason:
- "incompatible": return ServerState.INCOMPATIBLE
- "no_command": return ServerState.NO_COMMAND
- "port_reserved": return ServerState.PORT_EXCLUDED
- "occupied", "replacement_unproven": return ServerState.FOREIGN_PORT
- _: return ServerState.CRASHED
- return ServerState.UNINITIALIZED
diff --git a/addons/godot_ai/utils/server_lifecycle.gd.uid b/addons/godot_ai/utils/server_lifecycle.gd.uid
deleted file mode 100644
index 8e62667..0000000
--- a/addons/godot_ai/utils/server_lifecycle.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bwfx8b0w2mgf6
diff --git a/addons/godot_ai/utils/server_version_check.gd b/addons/godot_ai/utils/server_version_check.gd
deleted file mode 100644
index b6f2172..0000000
--- a/addons/godot_ai/utils/server_version_check.gd
+++ /dev/null
@@ -1,64 +0,0 @@
-@tool
-class_name McpServerVersionCheck
-extends RefCounted
-
-## Published v4 compatibility value. Lifecycle owns the authenticated
-## handshake and episode transition; this class intentionally owns no timer,
-## connection, or manager reference.
-
-## First release whose attached bridge follows servers of the same major.
-const FIRST_BRIDGE_TOLERANT_VERSION := "4.0.4"
-
-
-## Whether clients attached at `from_version` can reconnect after updating
-## to `to_version` without restarting their bridge (#1024).
-static func attached_bridges_follow(from_version: String, to_version: String) -> bool:
- var from_tuple := version_tuple(from_version)
- var to_tuple := version_tuple(to_version)
- if from_tuple.is_empty() or to_tuple.is_empty():
- return false
- if int(from_tuple[0]) != int(to_tuple[0]):
- return false
- return compare(from_tuple, version_tuple(FIRST_BRIDGE_TOLERANT_VERSION)) >= 0
-
-
-## Leading numeric `major.minor.patch` of a version as `[major, minor, patch]`,
-## or `[]` when it does not start that way (a dev build, a malformed pin).
-## A suffix after the patch (`4.0.3+local.1`, `4.1.0-rc1`) is ignored; a
-## dangling separator (`4.0.3+`) is not a version.
-static func version_tuple(version: String) -> Array:
- var regex := RegEx.create_from_string("^(\\d+)\\.(\\d+)\\.(\\d+)(?:$|[.+-][0-9A-Za-z][0-9A-Za-z.+-]*$)")
- var found := regex.search(version.strip_edges())
- if found == null:
- return []
- return [int(found.get_string(1)), int(found.get_string(2)), int(found.get_string(3))]
-
-
-static func compare(a: Array, b: Array) -> int:
- for index in range(3):
- if int(a[index]) != int(b[index]):
- return -1 if int(a[index]) < int(b[index]) else 1
- return 0
-
-
-## `candidate` is a parseable version older than `reference` within the same
-## major version. Unparseable, equal, newer, or another major all read false:
-## a caller may only replace what it can prove is older than itself.
-static func is_older_same_major(candidate: String, reference: String) -> bool:
- var candidate_tuple := version_tuple(candidate)
- var reference_tuple := version_tuple(reference)
- if candidate_tuple.is_empty() or reference_tuple.is_empty():
- return false
- if int(candidate_tuple[0]) != int(reference_tuple[0]):
- return false
- return compare(candidate_tuple, reference_tuple) < 0
-
-
-static func evaluate(actual_version: String, expected_version: String) -> Dictionary:
- if actual_version.is_empty():
- return {"compatible": false, "reason": "missing_version"}
- var compatible := actual_version == expected_version
- return {
- "compatible": compatible,
- "reason": "" if compatible else "version_mismatch",
- }
diff --git a/addons/godot_ai/utils/server_version_check.gd.uid b/addons/godot_ai/utils/server_version_check.gd.uid
deleted file mode 100644
index 7baacfd..0000000
--- a/addons/godot_ai/utils/server_version_check.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ciqldbuaq8i8u
diff --git a/addons/godot_ai/utils/settings.gd b/addons/godot_ai/utils/settings.gd
deleted file mode 100644
index 9ce2c68..0000000
--- a/addons/godot_ai/utils/settings.gd
+++ /dev/null
@@ -1,135 +0,0 @@
-@tool
-class_name McpSettings
-extends RefCounted
-
-## Shared EditorSettings key constants for the godot_ai/* namespace.
-##
-## Centralised here so lightweight files (e.g. telemetry.gd) can reference
-## settings keys without pulling in the full client_configurator.gd dep tree.
-## All keys must keep their raw string values stable across releases because
-## they are persisted in the user's editor_settings-4.tres.
-
-const SETTING_HTTP_PORT := "godot_ai/http_port"
-## Comma-separated list of tool domains excluded from the server at spawn time.
-const SETTING_EXCLUDED_DOMAINS := "godot_ai/excluded_domains"
-const SETTING_TELEMETRY_ENABLED := "godot_ai/telemetry_enabled"
-## Comma-separated CIDRs / bare IPs passed to the server as `--allow-host`
-## at spawn time (#507, server core #421). Empty means loopback-only.
-const SETTING_ALLOW_HOSTS := "godot_ai/allow_remote_hosts"
-## Whether MCP log lines echo to the Godot console (dock "Log" toggle).
-## The dock's ring-buffer log panel keeps recording regardless.
-const SETTING_MCP_LOGGING := "godot_ai/mcp_logging"
-## Which scope CLI-configured clients register the server in. Claude Code's
-## `--scope user` writes the global block of ~/.claude.json, which that client
-## loads in EVERY workspace — so the server gets spawned in unrelated,
-## non-Godot projects. `project` writes /.mcp.json instead, so the
-## entry travels with the project. Defaults to `user` to preserve the
-## historical behaviour for existing installs.
-const SETTING_CLIENT_SCOPE := "godot_ai/mcp_client_scope"
-
-## Scopes accepted for SETTING_CLIENT_SCOPE. These are the values the Claude
-## Code CLI's `--scope` flag takes; an unrecognised setting falls back to
-## DEFAULT_CLIENT_SCOPE rather than passing junk to the CLI.
-const CLIENT_SCOPES := ["user", "project", "local"]
-const DEFAULT_CLIENT_SCOPE := "user"
-
-
-## Returns true if the string value is truthy
-## ("1", "true", "yes", "on", case-insensitive, whitespace-trimmed).
-static func truthy(value: String) -> bool:
- return value.strip_edges().to_lower() in ["1", "true", "yes", "on"]
-
-
-## Returns true if the named environment variable is set to a truthy value.
-static func env_truthy(var_name: String) -> bool:
- return truthy(OS.get_environment(var_name))
-
-
-## #691: EditorInterface / EditorSettings are not thread-safe, and the dock
-## runs Configure / Remove on a worker Thread (mcp_dock.gd
-## `_run_client_action_worker` -> McpClientConfigurator -> McpCliStrategy
-## `_format_args` -> client_scope()). Same contract as
-## McpClientConfigurator._editor_setting_lookup: the main thread does a live
-## read and refreshes the snapshot; worker threads read the snapshot only, so
-## a never-warmed value reads as null (unset) rather than touching
-## EditorInterface off-thread. Warmed on the main thread by
-## McpClientConfigurator.warm_env_snapshot() before each worker dispatch.
-##
-## Kept here rather than reusing the configurator's `_editor_setting_lookup`
-## so utils/settings.gd stays free of the client_configurator.gd dep tree —
-## see this file's header.
-static var _client_scope_snapshot: Variant = null
-static var _client_scope_mutex := Mutex.new()
-
-
-static func _client_scope_setting() -> Variant:
- if OS.get_thread_caller_id() == OS.get_main_thread_id():
- var live: Variant = null
- if Engine.is_editor_hint():
- var es := EditorInterface.get_editor_settings()
- if es != null and es.has_setting(SETTING_CLIENT_SCOPE):
- live = es.get_setting(SETTING_CLIENT_SCOPE)
- _client_scope_mutex.lock()
- _client_scope_snapshot = live
- _client_scope_mutex.unlock()
- return live
- _client_scope_mutex.lock()
- var cached: Variant = _client_scope_snapshot
- _client_scope_mutex.unlock()
- return cached
-
-
-## Main-thread pre-warm of the scope snapshot so the dock's Configure/Remove
-## worker resolves `{scope}` from a value captured while EditorSettings access
-## was safe. Idempotent; called from warm_env_snapshot().
-static func warm_client_scope() -> void:
- _client_scope_setting()
-
-
-## Returns the scope CLI clients should register the MCP server in, as
-## substituted into `{scope}` in a descriptor's cli_register_template /
-## cli_unregister_template. Safe to call from either thread (see
-## _client_scope_setting). Unrecognised values fall back to
-## DEFAULT_CLIENT_SCOPE so a hand-edited editor_settings-4.tres can never make
-## the plugin shell out with a bad flag.
-static func client_scope() -> String:
- var value: Variant = _client_scope_setting()
- ## String(null) is a hard error (#850) — an unset/never-warmed key must
- ## short-circuit before the cast.
- if value == null:
- return DEFAULT_CLIENT_SCOPE
- var raw := String(value).strip_edges().to_lower()
- if raw in CLIENT_SCOPES:
- return raw
- return DEFAULT_CLIENT_SCOPE
-
-
-## Returns true if telemetry should be active, checking in priority order:
-## 1. GODOT_AI_DISABLE_TELEMETRY / DISABLE_TELEMETRY env vars
-## 2. The godot_ai/telemetry_enabled EditorSetting written by the dock UI
-## Defaults to true when neither source has set a preference.
-static func telemetry_enabled() -> bool:
- if env_truthy("GODOT_AI_DISABLE_TELEMETRY") or env_truthy("DISABLE_TELEMETRY"):
- return false
- var es := EditorInterface.get_editor_settings()
- if es != null and es.has_setting(SETTING_TELEMETRY_ENABLED):
- return bool(es.get_setting(SETTING_TELEMETRY_ENABLED))
- return true
-
-
-## Returns whether MCP log lines should echo to the Godot console. Read at
-## plugin startup (to apply the persisted choice to the log buffer and
-## dispatcher) and by the dock's LogViewer toggle for its initial state.
-## Defaults to true when the user has never touched the toggle.
-static func mcp_logging_enabled() -> bool:
- var es := EditorInterface.get_editor_settings()
- if es != null and es.has_setting(SETTING_MCP_LOGGING):
- return bool(es.get_setting(SETTING_MCP_LOGGING))
- return true
-
-
-## Persist the dock "Log" toggle so the choice survives editor restarts (#626).
-static func set_mcp_logging_enabled(enabled: bool) -> void:
- var es := EditorInterface.get_editor_settings()
- if es != null:
- es.set_setting(SETTING_MCP_LOGGING, enabled)
diff --git a/addons/godot_ai/utils/settings.gd.uid b/addons/godot_ai/utils/settings.gd.uid
deleted file mode 100644
index b8b1547..0000000
--- a/addons/godot_ai/utils/settings.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://pefrtofs7ijw
diff --git a/addons/godot_ai/utils/structured_log_ring.gd b/addons/godot_ai/utils/structured_log_ring.gd
deleted file mode 100644
index 3e00b4e..0000000
--- a/addons/godot_ai/utils/structured_log_ring.gd
+++ /dev/null
@@ -1,156 +0,0 @@
-@tool
-class_name McpStructuredLogRing
-extends RefCounted
-
-## Head-indexed circular buffer of structured log entries shared by
-## game_log_buffer and editor_log_buffer.
-##
-## Once `_max_lines` (set in subclass `_init`) is reached, new appends
-## overwrite the oldest slot at `_head`, keeping append O(1) on overflow
-## — the previous slice() approach reallocated the full retained array
-## on every drop, which a chatty game would pay for thousands of times
-## per second.
-##
-## Lockless. Subclasses needing thread-safety (editor_log_buffer is
-## written from any thread a Godot Logger virtual can fire on) wrap each
-## public method with their own Mutex around the `_*_unlocked` helpers.
-## Keeping the base lockless means the hot game-side path (single thread,
-## called from _process) doesn't pay an unused mutex cost.
-##
-## Entry shape is owned by subclasses — `_append_entry` takes a
-## ready-built Dictionary so each buffer can carry the fields it needs
-## (game: `source/level/text`; editor: adds `path/line/function`).
-
-const VALID_LEVELS := ["info", "warn", "error"]
-
-var _max_lines: int
-var _storage: Array[Dictionary] = []
-## Next write position within `_storage`. While filling (before first
-## wrap) equals `_storage.size()`; once full, points at the oldest entry
-## (the one about to be overwritten).
-var _head := 0
-var _dropped_count := 0
-## Monotonic number of entries appended since this ring was created. Unlike
-## `_storage.size()` and `_dropped_count`, this intentionally survives clear()
-## so callers can use it as a stable "next entry to read" cursor.
-var _appended_total := 0
-
-
-func _init(max_lines: int) -> void:
- _max_lines = max_lines
-
-
-## Append `entry` to the ring, evicting the oldest slot when full.
-## Subclasses build the dict with their per-source shape and pass it in.
-func _append_entry(entry: Dictionary) -> void:
- if _storage.size() < _max_lines:
- _storage.append(entry)
- _head = _storage.size() % _max_lines
- else:
- ## Full — overwrite oldest in place, advance head, count the drop.
- _storage[_head] = entry
- _head = (_head + 1) % _max_lines
- _dropped_count += 1
- _appended_total += 1
-
-
-## Lockless slice. Subclasses with a mutex wrap their `get_range` /
-## `get_recent` overrides around this; the lockless base implementations
-## of those public methods just delegate here.
-func _get_range_unlocked(offset: int, count: int) -> Array[Dictionary]:
- var size := _storage.size()
- var start := maxi(0, offset)
- var stop := mini(size, start + count)
- var out: Array[Dictionary] = []
- for i in range(start, stop):
- out.append(_storage[_logical_to_physical(i)])
- return out
-
-
-func get_range(offset: int, count: int) -> Array[Dictionary]:
- return _get_range_unlocked(offset, count)
-
-
-func get_recent(count: int) -> Array[Dictionary]:
- var size := _storage.size()
- var start := maxi(0, size - count)
- return _get_range_unlocked(start, size - start)
-
-
-## Lockless cursor read. The cursor is the next sequence to read: calling
-## get_since(appended_total()) after a snapshot returns only later appends.
-func _get_since_unlocked(since_seq: int, limit: int = -1) -> Dictionary:
- var size := _storage.size()
- var oldest_seq := _appended_total - size
- var start_seq := mini(maxi(since_seq, oldest_seq), _appended_total)
- var start := start_seq - oldest_seq
- var available := maxi(0, size - start)
- var count := available
- if limit >= 0:
- count = mini(available, limit)
- var entries := _get_range_unlocked(start, count)
- var next_cursor := start_seq + entries.size()
- return {
- "cursor": since_seq,
- "oldest_cursor": oldest_seq,
- "next_cursor": next_cursor,
- "appended_total": _appended_total,
- "truncated": since_seq < oldest_seq,
- "has_more": next_cursor < _appended_total,
- "entries": entries,
- }
-
-
-func get_since(since_seq: int, limit: int = -1) -> Dictionary:
- return _get_since_unlocked(since_seq, limit)
-
-
-## Lockless accessors. Subclasses with a mutex use these under their lock
-## so the field reads stay encapsulated in the base instead of leaking
-## `_storage` / `_dropped_count` reach-through into the subclass.
-func _total_count_unlocked() -> int:
- return _storage.size()
-
-
-func _dropped_count_unlocked() -> int:
- return _dropped_count
-
-
-func _appended_total_unlocked() -> int:
- return _appended_total
-
-
-func total_count() -> int:
- return _total_count_unlocked()
-
-
-func dropped_count() -> int:
- return _dropped_count_unlocked()
-
-
-func appended_total() -> int:
- return _appended_total_unlocked()
-
-
-## Translate a logical index (0 = oldest retained) to a physical
-## `_storage` slot. Before the first wrap, storage-order is logical-
-## order. After wrapping, the oldest entry lives at `_head`.
-func _logical_to_physical(logical: int) -> int:
- if _storage.size() < _max_lines:
- return logical
- return (_head + logical) % _max_lines
-
-
-## Reset the ring to empty. Subclasses with a mutex wrap this with their
-## lock; subclasses that surface `clear` to callers (McpEditorLogBuffer)
-## return the prior size from their wrapper.
-func _clear_storage() -> void:
- _storage.clear()
- _head = 0
- _dropped_count = 0
-
-
-## Coerce unknown levels to "info" so a misbehaving sender can't poison
-## downstream filters with arbitrary strings.
-static func _coerce_level(level: String) -> String:
- return level if level in VALID_LEVELS else "info"
diff --git a/addons/godot_ai/utils/structured_log_ring.gd.uid b/addons/godot_ai/utils/structured_log_ring.gd.uid
deleted file mode 100644
index 57012ba..0000000
--- a/addons/godot_ai/utils/structured_log_ring.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c4yh3jqfn6dwe
diff --git a/addons/godot_ai/utils/surfaced_error_tracker.gd b/addons/godot_ai/utils/surfaced_error_tracker.gd
deleted file mode 100644
index 5bd3022..0000000
--- a/addons/godot_ai/utils/surfaced_error_tracker.gd
+++ /dev/null
@@ -1,642 +0,0 @@
-@tool
-class_name McpSurfacedErrorTracker
-extends RefCounted
-
-## Central source for "errors the agent should know exist".
-##
-## Editor log cursors only cover McpEditorLogBuffer. Runtime errors from the
-## game subprocess can land solely in the Debugger Errors tab, so this tracker
-## promotes visible Debugger-tab rows into a monotonic sequence before the
-## dispatcher stamps a watermark on each response envelope.
-
-const MAX_PROMOTED_DEBUGGER_ENTRIES := 500
-const MAX_PROMOTED_DEBUGGER_KEYS := 5000
-const DEBUGGER_REFRESH_MIN_INTERVAL_MS := 250
-const DEBUGGER_SCAN_AFTER_STOP_MS := 5000
-## #641: delays for the self-scheduled forced scans armed on run stop (and on
-## game-helper hello, via McpDebuggerPlugin). Two ticks: an early one for rows
-## the remote debugger delivers right around the event, and a late one past
-## Godot's per-frame Errors-tab insertion throttle for error floods.
-const DEFERRED_SCAN_DELAYS_SEC: Array[float] = [1.0, 5.0]
-## #635: cap on accounted per-key row-time signatures. The live Errors tab is
-## itself bounded, so this only guards a pathological flood of same-keyed rows
-## with distinct time texts; past the cap the set resets to the current scan.
-const MAX_ACCOUNTED_ROW_TIMES_PER_KEY := 512
-
-var _editor_log_buffer
-var _game_log_buffer
-var _debugger_errors_root: Node
-var _debugger_search_root_cache: Node
-var _promoted_debugger_keys: Dictionary = {}
-## #635: per-key set of Errors-tab row time texts already promoted, so a row
-## observed after a clear+repopulate that no scan saw as empty still counts as
-## new (see the re-promotion comment in refresh_debugger_errors).
-var _promoted_debugger_row_times: Dictionary = {}
-var _promoted_debugger_key_order: Array[String] = []
-var _promoted_debugger_entries: Array[Dictionary] = []
-var _debugger_promoted_total := 0
-var _run_seq := 0
-var _oldest_retained_debugger_sequence := 1
-var _last_debugger_refresh_msec := -DEBUGGER_REFRESH_MIN_INTERVAL_MS
-var _debugger_scan_active := false
-var _debugger_scan_until_msec := 0
-var _deferred_scans_scheduled_total := 0
-
-
-func _init(editor_log_buffer = null, game_log_buffer = null, debugger_errors_root: Node = null) -> void:
- _editor_log_buffer = editor_log_buffer
- _game_log_buffer = game_log_buffer
- _debugger_errors_root = debugger_errors_root
-
-
-func note_game_run_started(sticky_scan: bool = true) -> void:
- _run_seq += 1
- _debugger_scan_active = sticky_scan
- _debugger_scan_until_msec = 0
- if not sticky_scan:
- _debugger_scan_until_msec = Time.get_ticks_msec() + DEBUGGER_SCAN_AFTER_STOP_MS
- refresh_debugger_errors(true)
-
-
-func note_game_run_stopped() -> void:
- _debugger_scan_active = false
- _debugger_scan_until_msec = Time.get_ticks_msec() + DEBUGGER_SCAN_AFTER_STOP_MS
- schedule_deferred_scans()
-
-
-## #641: promotion into the watermark used to depend on a tool call arriving
-## while the scan gate was open (run active, or within DEBUGGER_SCAN_AFTER_STOP_MS
-## of stop). Boot parse errors that landed in the Errors tab with no tool call
-## in that window were never promoted, so the agent never got the
-## new_errors_since_last_call hint. These editor-side timers force a scan
-## regardless of tool-call cadence; the next stamped response then carries the
-## already-promoted count even after the gate closes. Scans are content-keyed
-## and idempotent, so a timer firing after an unrelated new run is harmless.
-func schedule_deferred_scans(delays: Array = DEFERRED_SCAN_DELAYS_SEC) -> void:
- var tree := Engine.get_main_loop() as SceneTree
- if tree == null:
- return
- for delay in delays:
- var timer := tree.create_timer(maxf(0.05, float(delay)))
- timer.timeout.connect(_on_deferred_scan_timeout)
- _deferred_scans_scheduled_total += 1
-
-
-func deferred_scans_scheduled_total() -> int:
- return _deferred_scans_scheduled_total
-
-
-func _on_deferred_scan_timeout() -> void:
- refresh_debugger_errors(true)
-
-
-func refresh_debugger_errors(force: bool = true) -> void:
- var now := Time.get_ticks_msec()
- if not force and not _should_scan_debugger_for_cached_watermark(now):
- return
- _last_debugger_refresh_msec = now
- var current_by_key: Dictionary = {}
- for entry in _raw_debugger_error_entries():
- if str(entry.get("level", "")) != "error":
- continue
- var key := _log_entry_key(entry)
- var info: Dictionary = current_by_key.get(key, {"count": 0, "entry": entry, "times": {}})
- info["count"] = int(info.get("count", 0)) + 1
- var time_text := _row_time_text(entry)
- if not time_text.is_empty():
- (info["times"] as Dictionary)[time_text] = true
- current_by_key[key] = info
- for key in _promoted_debugger_keys.keys():
- if not current_by_key.has(key):
- _promoted_debugger_keys[key] = 0
- for key in current_by_key.keys():
- var info: Dictionary = current_by_key[key]
- var current := int(info.get("count", 0))
- var stored := int(_promoted_debugger_keys.get(key, 0))
- ## #635: a count increase alone misses rows observed after a run
- ## boundary. Godot clears the Errors tab at run start; when the new run
- ## re-fires an error identical to one promoted before the clear, and no
- ## scan happened to observe the tab empty in between, the per-key count
- ## never dips — so the row kept its pre-run sequence and run-scoping
- ## (editor_entries_since against the run-start cursor) misclassified an
- ## in-run error as retained_recent. Each Errors-tab row carries its own
- ## time text; an unaccounted (key, time) signature is a row we have not
- ## promoted yet, so it earns a fresh sequence even at an equal or lower
- ## count. Boundary condition: rows with an empty time text, or a
- ## repopulated row whose time text is byte-identical to a pre-clear row,
- ## fall back to count-only dedup and can still be missed.
- var unseen_times := _unaccounted_row_times(key, info.get("times", {}))
- var delta := current - stored
- if delta <= 0 and not unseen_times.is_empty():
- delta = mini(unseen_times.size(), current)
- if delta <= 0:
- if current != stored:
- _promoted_debugger_keys[key] = current
- continue
- if not _promoted_debugger_keys.has(key):
- _promoted_debugger_key_order.append(key)
- _promoted_debugger_keys[key] = current
- _account_row_times(key, info.get("times", {}))
- _debugger_promoted_total += delta
- var source_entry: Dictionary = info.get("entry", {})
- var promoted := source_entry.duplicate(true)
- promoted["_debugger_key"] = key
- promoted["_debugger_occurrences"] = current
- promoted["_debugger_sequence"] = _debugger_promoted_total
- _remove_promoted_debugger_entry(key)
- _promoted_debugger_entries.append(promoted)
- _trim_promoted_debugger_entries()
- _trim_promoted_debugger_key_counts()
-
-
-## #645: promote an error record that has no Errors-tab row to scrape — e.g. a
-## boot-time parse error that parked the game in a remote-debugger break before
-## any surface got a record. The entry joins the same promoted sequence as
-## scraped Debugger rows, so run-scoping (editor_entries_since), the retained
-## fallback, and the response watermark all see it with no extra plumbing.
-## Re-recording the same key later (the same script still broken on the next
-## run) re-promotes it with a fresh sequence, mirroring how re-appearing
-## Errors-tab rows behave; scan reconciliation zeroes the key's count once the
-## break ends since the row never exists in the live tab.
-func record_synthetic_error(entry: Dictionary) -> void:
- var key := _log_entry_key(entry)
- var occurrences := int(_promoted_debugger_keys.get(key, 0)) + 1
- if not _promoted_debugger_keys.has(key):
- _promoted_debugger_key_order.append(key)
- _promoted_debugger_keys[key] = occurrences
- _debugger_promoted_total += 1
- var promoted := entry.duplicate(true)
- promoted["_debugger_key"] = key
- promoted["_debugger_occurrences"] = occurrences
- promoted["_debugger_sequence"] = _debugger_promoted_total
- promoted["_debugger_synthetic"] = true
- _remove_promoted_debugger_entry(key)
- _promoted_debugger_entries.append(promoted)
- _trim_promoted_debugger_entries()
- _trim_promoted_debugger_key_counts()
-
-
-## Monotonicity contract (#767): run_seq and the session-scoped components
-## (editor_ring, debugger_promoted, editor_ring_warn) must NEVER decrease
-## within an editor session, and the per-run components (game_error_warn,
-## game_warn) must never decrease within a run — they may reset only when
-## run_seq increments in the same stamp (the run boundary that rotates the
-## game buffer's counters). Released servers diff consecutive stamps
-## (websocket.py::_sync_error_watermark_for_session) and treat any other
-## decrease as a counter reset, counting the FULL current value as new — one
-## dip makes every old server out there over-report errors. Any future
-## hold/classification feature must therefore DEFER an increment until its
-## entry is released, never subtract an already-stamped one: a stamp like
-## `raw_total - currently_held_entries` is exactly the regression this
-## guards against. Producer-side coverage:
-## test_editor.gd::test_surfaced_error_tracker_watermark_components_never_decrease.
-func watermark(force_debugger_scan: bool = false) -> Dictionary:
- refresh_debugger_errors(force_debugger_scan)
- return {
- "run_seq": _run_seq,
- "editor_ring": _error_appended_total(),
- "debugger_promoted": _debugger_promoted_total,
- ## Historically misnamed: carries game-process ERROR counts only.
- "game_error_warn": _game_error_total(),
- ## Warn-level components, parallel to the error counts above. The server
- ## diffs these into `new_warnings_since_last_call` so a warning-only run
- ## surfaces instead of reading as clean. Debugger Errors-tab warning rows
- ## are not promoted here yet (buffers cover push_warning from the game and
- ## editor parse/@tool warnings) — tracked as a follow-up.
- "editor_ring_warn": _warn_appended_total(),
- "game_warn": _game_warn_total(),
- }
-
-
-static func stamp_watermark(response: Dictionary, tracker) -> void:
- if tracker == null:
- return
- if not tracker.has_method("watermark"):
- return
- response["error_watermark"] = tracker.watermark()
-
-
-func debugger_promoted_total(force_debugger_scan: bool = true) -> int:
- refresh_debugger_errors(force_debugger_scan)
- return _debugger_promoted_total
-
-
-func collect_editor_log_entries() -> Array[Dictionary]:
- refresh_debugger_errors(true)
- var entries: Array[Dictionary] = []
- var seen_keys: Dictionary = {}
- if _editor_log_buffer != null:
- for entry in _editor_log_buffer.get_range(0, _editor_log_buffer.total_count()):
- seen_keys[_log_entry_key(entry)] = true
- entries.append(entry)
- for entry in read_debugger_error_entries():
- var key := _log_entry_key(entry)
- if seen_keys.has(key):
- continue
- seen_keys[key] = true
- entries.append(entry)
- ## #645: synthesized break records have no live Errors-tab row to scrape —
- ## merge them from the promoted list so logs_read(source="editor") shows
- ## the record that run/game responses point at.
- for entry in _promoted_debugger_entries:
- if not bool(entry.get("_debugger_synthetic", false)):
- continue
- var key := _log_entry_key(entry)
- if seen_keys.has(key):
- continue
- seen_keys[key] = true
- entries.append(_strip_promotion_bookkeeping(entry))
- return entries
-
-
-static func _strip_promotion_bookkeeping(entry: Dictionary) -> Dictionary:
- var clean := entry.duplicate(true)
- for key in ["_debugger_key", "_debugger_occurrences", "_debugger_sequence", "_debugger_synthetic"]:
- clean.erase(key)
- return clean
-
-
-func editor_entries_since(editor_cursor: int, debugger_cursor: int, force_debugger_scan: bool = true) -> Dictionary:
- refresh_debugger_errors(force_debugger_scan)
- var entries: Array[Dictionary] = []
- var seen_keys: Dictionary = {}
- var truncated := false
- if _editor_log_buffer != null:
- var captured: Dictionary = _editor_log_buffer.get_since(maxi(0, editor_cursor), -1)
- truncated = bool(captured.get("truncated", false))
- for entry in captured.get("entries", []):
- seen_keys[_log_entry_key(entry)] = true
- entries.append(entry)
- if debugger_cursor < _oldest_retained_debugger_sequence - 1:
- truncated = true
- for entry in _promoted_debugger_entries:
- if int(entry.get("_debugger_sequence", 0)) <= debugger_cursor:
- continue
- var key := _log_entry_key(entry)
- if seen_keys.has(key):
- continue
- seen_keys[key] = true
- entries.append(entry)
- return {
- "entries": entries,
- "truncated": truncated,
- }
-
-
-func retained_recent_editor_entries() -> Array[Dictionary]:
- ## There is no shared timestamp across the editor logger ring and Godot's
- ## Debugger Errors tree. Preserve the pre-PR fallback contract: newest
- ## buffered editor entries first, then debugger-only rows that were not in
- ## the ring, so stale Debugger rows cannot outrank newer ring entries.
- var entries: Array[Dictionary] = []
- var seen_keys: Dictionary = {}
- if _editor_log_buffer != null:
- entries = _editor_log_buffer.get_recent(_editor_log_buffer.total_count())
- entries.reverse()
- for entry in entries:
- seen_keys[_log_entry_key(entry)] = true
- for entry in collect_editor_log_entries():
- var key := _log_entry_key(entry)
- if seen_keys.has(key):
- continue
- seen_keys[key] = true
- entries.append(entry)
- return entries
-
-
-func read_debugger_error_entries() -> Array[Dictionary]:
- var entries: Array[Dictionary] = []
- var seen_keys: Dictionary = {}
- for entry in _raw_debugger_error_entries():
- var key := _log_entry_key(entry)
- if seen_keys.has(key):
- continue
- seen_keys[key] = true
- entries.append(entry)
- return entries
-
-
-func locate_debugger_error_trees() -> Array[Tree]:
- var trees: Array[Tree] = []
- var root: Node = _debugger_errors_root
- ## #641: a deferred-scan timer can outlive an injected root (tests,
- ## teardown). A freed root must not fall through to the live editor UI —
- ## that would promote unrelated real errors into a tracker scoped to the
- ## dead root — so treat it as "nothing to scan".
- if root != null and not is_instance_valid(root):
- return trees
- if root == null:
- root = _debugger_search_root()
- if root == null:
- return trees
- _collect_debugger_error_trees(root, trees)
- return trees
-
-
-func clear_debugger_error_trees() -> int:
- var cleared := 0
- for tree in locate_debugger_error_trees():
- cleared += entries_from_debugger_error_tree(tree).size()
- if not _press_debugger_clear_button(tree):
- ## Synthetic roots in tests do not have Godot's Clear button.
- tree.clear()
- return cleared
-
-
-func _debugger_search_root() -> Node:
- if is_instance_valid(_debugger_search_root_cache):
- return _debugger_search_root_cache
- _debugger_search_root_cache = null
- var base := EditorInterface.get_base_control()
- if base == null:
- return null
- _debugger_search_root_cache = _find_first_of_class(base, "EditorDebuggerNode")
- if _debugger_search_root_cache == null:
- return base
- return _debugger_search_root_cache
-
-
-static func _find_first_of_class(node: Node, klass: String) -> Node:
- if node.get_class() == klass:
- return node
- for child in node.get_children():
- var found := _find_first_of_class(child, klass)
- if found != null:
- return found
- return null
-
-
-static func _collect_debugger_error_trees(node: Node, out: Array[Tree]) -> void:
- if node is Tree and _tree_has_debugger_errors(node as Tree):
- out.append(node as Tree)
- for child in node.get_children():
- if child is Node:
- _collect_debugger_error_trees(child as Node, out)
-
-
-static func _tree_has_debugger_errors(tree: Tree) -> bool:
- var root := tree.get_root()
- if root == null:
- return false
- var item := root.get_first_child()
- while item != null:
- if _is_debugger_error_item(item):
- return true
- item = item.get_next()
- return false
-
-
-static func _press_debugger_clear_button(tree: Tree) -> bool:
- var parent := tree.get_parent()
- if parent == null:
- return false
- var stack: Array[Node] = [parent]
- while not stack.is_empty():
- var node: Node = stack.pop_back()
- if node is BaseButton:
- for conn in node.get_signal_connection_list("pressed"):
- if str(conn.get("callable", "")).contains("_clear_errors_list"):
- node.emit_signal("pressed")
- return true
- for child in node.get_children():
- stack.push_back(child)
- return false
-
-
-static func entries_from_debugger_error_tree(tree: Tree) -> Array[Dictionary]:
- var entries: Array[Dictionary] = []
- var root := tree.get_root()
- if root == null:
- return entries
- var item := root.get_first_child()
- while item != null:
- if _is_debugger_error_item(item):
- entries.append(_entry_from_debugger_error_item(item))
- item = item.get_next()
- return entries
-
-
-static func _entry_from_debugger_error_item(item: TreeItem) -> Dictionary:
- var title := item.get_text(1)
- var loc := _location_from_metadata(item.get_metadata(0))
- var function := _function_from_title(title)
- return {
- "source": "editor",
- "level": "warn" if item.has_meta("_is_warning") else "error",
- "text": title,
- "path": str(loc.get("path", "")),
- "line": int(loc.get("line", 0)),
- "function": function,
- "details": _details_from_debugger_error_item(item, loc, function),
- }
-
-
-static func _details_from_debugger_error_item(item: TreeItem, loc: Dictionary, function: String) -> Dictionary:
- var children: Array[Dictionary] = []
- var child := item.get_first_child()
- while child != null:
- var child_loc := _location_from_metadata(child.get_metadata(0))
- children.append({
- "label": child.get_text(0),
- "text": child.get_text(1),
- "path": str(child_loc.get("path", "")),
- "line": int(child_loc.get("line", 0)),
- })
- child = child.get_next()
- return {
- "debugger_tab": "Errors",
- "time": item.get_text(0),
- "message": item.get_text(1),
- "error_type_name": "warning" if item.has_meta("_is_warning") else "error",
- "source": {
- "path": str(loc.get("path", "")),
- "line": int(loc.get("line", 0)),
- "function": function,
- },
- "resolved": {
- "path": str(loc.get("path", "")),
- "line": int(loc.get("line", 0)),
- "function": function,
- },
- "children": children,
- "frames": _frames_from_error_children(children),
- }
-
-
-static func _is_debugger_error_item(item: TreeItem) -> bool:
- return item.has_meta("_is_warning") or item.has_meta("_is_error")
-
-
-static func _frames_from_error_children(children: Array[Dictionary]) -> Array[Dictionary]:
- var start := -1
- for i in children.size():
- if str(children[i].label).contains("Stack Trace"):
- start = i
- break
- if start < 0:
- for i in children.size():
- if str(children[i].label).is_empty() and not str(children[i].path).is_empty():
- start = maxi(i - 1, 0)
- break
- if start < 0:
- return []
- var frames: Array[Dictionary] = []
- for i in range(start, children.size()):
- if str(children[i].path).is_empty():
- continue
- frames.append({
- "path": children[i].path,
- "line": children[i].line,
- "function": _function_from_frame_text(children[i].text),
- })
- return frames
-
-
-static func _location_from_metadata(meta: Variant) -> Dictionary:
- if meta is Array and meta.size() >= 2:
- return {"path": str(meta[0]), "line": int(meta[1])}
- return {"path": "", "line": 0}
-
-
-static func _function_from_title(title: String) -> String:
- var colon := title.find(": ")
- if colon <= 0:
- return ""
- return title.substr(0, colon)
-
-
-static func _function_from_frame_text(text: String) -> String:
- var marker := text.find(" @ ")
- if marker < 0:
- return ""
- var fn := text.substr(marker + 3).strip_edges()
- if fn.ends_with("()"):
- fn = fn.substr(0, fn.length() - 2)
- return fn
-
-
-## Shared one-line rendering of a compact editor-error entry for messages and
-## hints ("text (path:line)"). Single home so the debugger plugin, project
-## handler, and editor handler can't drift apart.
-static func format_editor_error_summary(entry: Dictionary) -> String:
- var text := str(entry.get("text", "editor error"))
- var path := str(entry.get("path", ""))
- var line := int(entry.get("line", 0))
- if not path.is_empty() and line > 0:
- return "%s (%s:%d)" % [text, path, line]
- if not path.is_empty():
- return "%s (%s)" % [text, path]
- return text
-
-
-static func _log_entry_key(entry: Dictionary) -> String:
- return "%s|%s|%s|%s" % [
- str(entry.get("level", "")),
- str(entry.get("text", "")),
- str(entry.get("path", "")),
- str(entry.get("line", 0)),
- ]
-
-
-func _error_appended_total() -> int:
- if _editor_log_buffer == null:
- return 0
- if _editor_log_buffer.has_method("error_appended_total"):
- return int(_editor_log_buffer.call("error_appended_total"))
- return 0
-
-
-func _game_error_total() -> int:
- if _game_log_buffer == null:
- return 0
- if _game_log_buffer.has_method("error_total"):
- return int(_game_log_buffer.call("error_total"))
- return 0
-
-
-func _warn_appended_total() -> int:
- if _editor_log_buffer == null:
- return 0
- if _editor_log_buffer.has_method("warn_appended_total"):
- return int(_editor_log_buffer.call("warn_appended_total"))
- return 0
-
-
-func _game_warn_total() -> int:
- if _game_log_buffer == null:
- return 0
- if _game_log_buffer.has_method("warn_total"):
- return int(_game_log_buffer.call("warn_total"))
- return 0
-
-
-func _should_scan_debugger_for_cached_watermark(now_msec: int) -> bool:
- if not _debugger_scan_active and now_msec > _debugger_scan_until_msec:
- return false
- return now_msec - _last_debugger_refresh_msec >= DEBUGGER_REFRESH_MIN_INTERVAL_MS
-
-
-func _trim_promoted_debugger_entries() -> void:
- while _promoted_debugger_entries.size() > MAX_PROMOTED_DEBUGGER_ENTRIES:
- _promoted_debugger_entries.pop_front()
- if _promoted_debugger_entries.is_empty():
- _oldest_retained_debugger_sequence = _debugger_promoted_total + 1
- else:
- _oldest_retained_debugger_sequence = int(_promoted_debugger_entries[0].get("_debugger_sequence", 1))
-
-
-func _trim_promoted_debugger_key_counts() -> void:
- while _promoted_debugger_key_order.size() > MAX_PROMOTED_DEBUGGER_KEYS:
- var key := _promoted_debugger_key_order.pop_front()
- _promoted_debugger_keys.erase(key)
- _promoted_debugger_row_times.erase(key)
-
-
-## #635: per-row time text from a scraped Errors-tab entry (column 0 of the
-## row, carried in details.time). Empty when the entry has no details — e.g.
-## synthetic records — which keeps those on count-only dedup.
-static func _row_time_text(entry: Dictionary) -> String:
- var details: Variant = entry.get("details", {})
- if details is Dictionary:
- return str((details as Dictionary).get("time", ""))
- return ""
-
-
-func _unaccounted_row_times(key: String, times: Dictionary) -> Array:
- var accounted: Dictionary = _promoted_debugger_row_times.get(key, {})
- var unseen := []
- for time_text in times.keys():
- if not accounted.has(time_text):
- unseen.append(time_text)
- return unseen
-
-
-func _account_row_times(key: String, times: Dictionary) -> void:
- if times.is_empty():
- return
- var accounted: Dictionary = _promoted_debugger_row_times.get(key, {})
- for time_text in times.keys():
- accounted[time_text] = true
- ## Enforce the bound AFTER merging: a pre-merge `>` check let the set
- ## reach the cap and keep growing (and a batch of new times could jump
- ## past it). Past the cap, reset to just this scan's times — the live
- ## Errors tab is itself bounded, so this only fires under a pathological
- ## same-key flood, where "recent scan only" is an acceptable memory of
- ## what was promoted (worst case: a re-observed ancient row re-promotes).
- if accounted.size() > MAX_ACCOUNTED_ROW_TIMES_PER_KEY:
- accounted = times.duplicate()
- _promoted_debugger_row_times[key] = accounted
-
-
-func _remove_promoted_debugger_entry(key: String) -> void:
- for i in range(_promoted_debugger_entries.size() - 1, -1, -1):
- if str(_promoted_debugger_entries[i].get("_debugger_key", "")) == key:
- _promoted_debugger_entries.remove_at(i)
- return
-
-
-func _raw_debugger_error_entries() -> Array[Dictionary]:
- var entries: Array[Dictionary] = []
- for tree in locate_debugger_error_trees():
- entries.append_array(entries_from_debugger_error_tree(tree))
- return entries
diff --git a/addons/godot_ai/utils/surfaced_error_tracker.gd.uid b/addons/godot_ai/utils/surfaced_error_tracker.gd.uid
deleted file mode 100644
index 433a191..0000000
--- a/addons/godot_ai/utils/surfaced_error_tracker.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://o0ulahkt83re
diff --git a/addons/godot_ai/utils/transport_capability.gd b/addons/godot_ai/utils/transport_capability.gd
deleted file mode 100644
index 2f9f7b6..0000000
--- a/addons/godot_ai/utils/transport_capability.gd
+++ /dev/null
@@ -1,305 +0,0 @@
-@tool
-class_name McpTransportCapability
-extends RefCounted
-
-## Reads Python's atomic, private capability record as one indivisible value.
-## This is a static boundary, not another state owner.
-
-const CAPABILITY_DIR_ENV := "GODOT_AI_CAPABILITY_DIR"
-const RECORD_VERSION := 1
-const MAX_RECORD_BYTES := 1024
-const _POSIX_PERMISSION_MASK := 0x1ff ## 0777
-const _GROUP_OTHER_PERMISSION_MASK := 0x3f ## 0077
-const _GROUP_OTHER_WRITE_MASK := 0x12 ## 0022
-const _MAX_LINK_HOPS := 8
-const _SYSTEM_TEMP_ROOTS: Array[String] = ["/tmp", "/private/tmp", "/var/tmp"]
-const _KEYS: Array[String] = [
- "version", "http", "websocket", "instance_nonce",
-]
-
-
-static func read_for_http_port(http_port: int, captured_path := "") -> Dictionary:
- var path := str(captured_path)
- if not path.is_empty() and path.get_file() != "http-%d.json" % http_port:
- return {}
- return _read_path(path if not path.is_empty() else path_for_http_port(http_port))
-
-
-static func _read_path(path: String) -> Dictionary:
- if path.is_empty() or not path.is_absolute_path():
- return {}
- var resolved := _resolve_trusted_path(path)
- if resolved.is_empty():
- return {}
- var directory := resolved.get_base_dir()
- if (
- not _safe_posix_ancestors(directory)
- or not _private_path(directory, true)
- or not _private_path(resolved, false)
- ):
- return {}
- var file := FileAccess.open(resolved, FileAccess.READ)
- if file == null:
- return {}
- var length := file.get_length()
- if length < 1 or length > MAX_RECORD_BYTES + 1:
- return {}
- var bytes := file.get_buffer(length)
- if bytes.size() != length:
- return {}
- if bytes[bytes.size() - 1] == 10:
- bytes.resize(bytes.size() - 1)
- if bytes.is_empty() or bytes.size() > MAX_RECORD_BYTES:
- return {}
- for byte in bytes:
- if byte > 127:
- return {}
- var raw := bytes.get_string_from_ascii()
- ## Godot resolves duplicate JSON keys. Tokens need no escapes, so reject
- ## escaping and require each literal key exactly once before parsing.
- if raw.contains("\\"):
- return {}
- for key in _KEYS:
- if raw.count("\"%s\"" % key) != 1:
- return {}
- var parsed: Variant = JSON.parse_string(raw)
- if not (parsed is Dictionary) or parsed.size() != _KEYS.size():
- return {}
- for key in _KEYS:
- if not parsed.has(key):
- return {}
- var version: Variant = parsed["version"]
- if not (version is int or version is float) or float(version) != RECORD_VERSION:
- return {}
- if (
- not (parsed["http"] is String)
- or not (parsed["websocket"] is String)
- or not (parsed["instance_nonce"] is String)
- ):
- return {}
- var http: String = parsed["http"]
- var websocket: String = parsed["websocket"]
- var nonce: String = parsed["instance_nonce"]
- if not is_http_capability(http) or not is_lower_hex(websocket, 64):
- return {}
- if not is_lower_hex(nonce, 32) or http == websocket:
- return {}
- return {
- "http": http,
- "websocket": websocket,
- "instance_nonce": nonce.to_lower(),
- }
-
-
-static func _private_path(path: String, directory: bool) -> bool:
- if directory:
- if DirAccess.open(path) == null:
- return false
- elif not FileAccess.file_exists(path):
- return false
- if OS.get_name() == "Windows":
- ## Godot's detectable link/reparse surface was checked by _path_has_link.
- ## v4 deliberately makes no Windows DACL secrecy/integrity claim.
- return true
- ## Godot exposes POSIX mode bits but not the owning UID. This proves only
- ## that group/other access is closed; it is not an owner-identity proof.
- var mode := FileAccess.get_unix_permissions(path) & _POSIX_PERMISSION_MASK
- return mode != 0 and (mode & _GROUP_OTHER_PERMISSION_MASK) == 0
-
-
-## Walk `path` from its root and return it with every accepted link component
-## replaced by its target, or "" when the path must not be trusted. A link is
-## followed only when the directory holding it is closed to group and other
-## writes, so only that directory's owner or root could have placed it; ostree
-## distributions need this for `/home -> /var/home` (#993). Godot exposes no
-## owning UID, so this is the strongest proof available here; Python, which
-## publishes the record, additionally requires such a link to be root-owned.
-## The record file itself is never followed, and a chain longer than
-## `_MAX_LINK_HOPS` fails closed. Windows keeps rejecting every detectable
-## link or reparse point.
-static func _resolve_trusted_path(path: String) -> String:
- if OS.get_name() == "Windows":
- return "" if _path_has_link(path) else path
- var remaining := path.simplify_path().split("/", false)
- var current := "/"
- var hops := 0
- while not remaining.is_empty():
- var part := remaining[0]
- remaining.remove_at(0)
- var candidate := current.path_join(part)
- var parent := DirAccess.open(current)
- if parent == null:
- return ""
- if parent.is_link(candidate):
- if (
- remaining.is_empty()
- or hops >= _MAX_LINK_HOPS
- or not _closed_to_group_and_other(current)
- ):
- return ""
- hops += 1
- var target := parent.read_link(candidate)
- if target.is_empty():
- return ""
- if not target.is_absolute_path():
- target = current.path_join(target)
- var target_parts := target.simplify_path().split("/", false)
- target_parts.append_array(remaining)
- remaining = target_parts
- current = "/"
- continue
- current = candidate
- return current
-
-
-static func _closed_to_group_and_other(directory: String) -> bool:
- var mode := FileAccess.get_unix_permissions(directory) & _POSIX_PERMISSION_MASK
- return mode != 0 and (mode & _GROUP_OTHER_WRITE_MASK) == 0
-
-
-static func _path_has_link(path: String) -> bool:
- var current := path
- while not current.is_empty():
- var parent := current.get_base_dir()
- if parent.is_empty() or parent == current:
- return false
- var directory := DirAccess.open(parent)
- if directory == null or directory.is_link(current):
- return true
- current = parent
- return false
-
-
-static func _safe_posix_ancestors(path: String) -> bool:
- if OS.get_name() == "Windows":
- return true
- var current := path.simplify_path()
- while not current.is_empty():
- if DirAccess.open(current) != null:
- var permissions := FileAccess.get_unix_permissions(current)
- if not _safe_posix_ancestor_mode(current, permissions):
- return false
- var parent := current.get_base_dir()
- if parent.is_empty() or parent == current:
- break
- current = parent
- return true
-
-
-static func _safe_posix_ancestor_mode(path: String, permissions: int) -> bool:
- var mode := permissions & _POSIX_PERMISSION_MASK
- if mode == 0:
- return false
- if (mode & _GROUP_OTHER_WRITE_MASK) == 0:
- return true
- ## Godot exposes no UID. Accept the conventional system temp boundary only
- ## when its sticky bit is present; Python additionally proves root ownership
- ## before it publishes a record below the same canonical roots.
- return (
- path.simplify_path() in _SYSTEM_TEMP_ROOTS
- and (permissions & FileAccess.UNIX_RESTRICTED_DELETE) != 0
- )
-
-
-static func is_http_capability(value: String) -> bool:
- var bytes := value.to_ascii_buffer()
- if bytes.size() < 32 or bytes.size() > 128:
- return false
- for byte in bytes:
- if not (
- (byte >= 48 and byte <= 57)
- or (byte >= 65 and byte <= 90)
- or (byte >= 97 and byte <= 122)
- or byte in [43, 45, 46, 47, 61, 95, 126]
- ):
- return false
- return true
-
-
-static func is_hex(value: String, length: int) -> bool:
- if value.length() != length:
- return false
- for index in range(length):
- var code := value.unicode_at(index)
- if not (
- (code >= 48 and code <= 57)
- or (code >= 65 and code <= 70)
- or (code >= 97 and code <= 102)
- ):
- return false
- return true
-
-
-static func is_lower_hex(value: String, length: int) -> bool:
- return is_hex(value, length) and value == value.to_lower()
-
-
-## Windows only: "" when this account can create and write the capability
-## directory, else a repair message. Python creates the directory with
-## ``mode=0o700`` on POSIX, but on Windows that mode yields a DACL of SYSTEM,
-## Administrators and OWNER RIGHTS alone; a directory first created by an
-## elevated process is then owned by Administrators and unusable from the
-## user's own unelevated editor, server and bridge (#988). Creating it here
-## first inherits the per-user %LOCALAPPDATA% DACL, and probing it before the
-## spawn turns a silent "proof timed out at capability_record" into a message
-## that names the directory and the fix.
-static func directory_write_problem(http_port: int) -> String:
- if OS.get_name() != "Windows":
- return ""
- var record := path_for_http_port(http_port)
- if record.is_empty():
- return ""
- return directory_write_problem_for(record.get_base_dir())
-
-
-static func directory_write_problem_for(directory: String) -> String:
- if DirAccess.open(directory) == null:
- var created := DirAccess.make_dir_recursive_absolute(directory)
- if created != OK:
- return windows_repair_hint(directory, error_string(created))
- var probe := directory.path_join(".access-probe-%d-%s" % [
- OS.get_process_id(), Crypto.new().generate_random_bytes(4).hex_encode(),
- ])
- var file := FileAccess.open(probe, FileAccess.WRITE)
- if file == null:
- return windows_repair_hint(directory, error_string(FileAccess.get_open_error()))
- file.close()
- DirAccess.remove_absolute(probe)
- return ""
-
-
-static func windows_repair_hint(directory: String, detail: String) -> String:
- return (
- "Godot AI cannot use the directory %s (%s): this Windows account cannot "
- + "access it. This can happen when an elevated (Run as administrator) "
- + "process created it. Check this directory's permissions and grant your "
- + "Windows account access, then reopen Godot and your AI clients without "
- + "Run as administrator."
- ) % [directory, detail]
-
-
-static func path_for_http_port(http_port: int) -> String:
- if http_port < 1 or http_port > 65535:
- return ""
- var override := OS.get_environment(CAPABILITY_DIR_ENV).strip_edges()
- if OS.get_name() == "Windows" and not override.is_empty():
- return ""
- var directory := override
- if directory.is_empty():
- if OS.get_name() == "Windows":
- directory = OS.get_environment("LOCALAPPDATA").strip_edges()
- if directory.is_empty():
- return ""
- directory = directory.path_join("godot-ai/capabilities")
- elif OS.get_name() == "macOS":
- directory = OS.get_environment("HOME").path_join(
- "Library/Application Support/godot-ai/capabilities"
- )
- else:
- directory = OS.get_environment("XDG_CONFIG_HOME").strip_edges()
- if directory.is_empty():
- directory = OS.get_environment("HOME").path_join(".config")
- directory = directory.path_join("godot-ai/capabilities")
- if directory.is_empty() or not directory.is_absolute_path():
- return ""
- return directory.simplify_path().path_join("http-%d.json" % http_port)
diff --git a/addons/godot_ai/utils/transport_capability.gd.uid b/addons/godot_ai/utils/transport_capability.gd.uid
deleted file mode 100644
index 9f165f0..0000000
--- a/addons/godot_ai/utils/transport_capability.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b7j8n63he8ljy
diff --git a/addons/godot_ai/utils/update_activation_runner.gd b/addons/godot_ai/utils/update_activation_runner.gd
deleted file mode 100644
index 2f302a2..0000000
--- a/addons/godot_ai/utils/update_activation_runner.gd
+++ /dev/null
@@ -1,391 +0,0 @@
-@tool
-extends Node
-
-## Compile this source into a GDScript with no resource_path before creating
-## the node. Its named callbacks must survive replacement of the file itself.
-## The caller verifies/stages/quiesces and owns the update lock. No plugin,
-## dock, handler, or suspended caller frame crosses the deferred handoff.
-const LIVE_ROOT := "res://addons/godot_ai"
-const PLUGIN_CFG := LIVE_ROOT + "/plugin.cfg"
-const STAGE_ROOT := "res://addons/.godot_ai_update/stage/addons/godot_ai"
-const INSTALLER_PATH := LIVE_ROOT + "/utils/update_installer.gd"
-const VERIFIER_PATH := LIVE_ROOT + "/utils/release_verifier.gd"
-const RECEIPT_PATH := "res://addons/.godot_ai_update/activation.json"
-const SCAN_TIMEOUT_MS := 60000
-
-enum Phase { IDLE, DRAIN, REQUEST_OLD_SCAN, WAIT_OLD_SCAN, SWAPPING, REQUEST_NEW_SCAN, WAIT_NEW_SCAN, ENABLING, DONE }
-
-var _phase := Phase.IDLE
-var _package: Dictionary = {}
-var _installer: Script
-var _verifier: Script
-var _frames := 0
-var _deadline := 0
-var _filesystem: EditorFileSystem
-var _failure := ""
-var _outcome := ""
-var _backup_root := ""
-var _retired_scripts: Array[Script] = []
-var _inspected_objects := {}
-var _inspection_budget := 100000
-## Copied by a caller when start() refuses, before the runner is freed.
-var refusal_reason := "The independent activation handoff was refused."
-
-
-func start(package: Dictionary) -> bool:
- if _phase != Phase.IDLE:
- refusal_reason = "The activation runner has already started."
- return false
- if not is_inside_tree():
- refusal_reason = "The activation runner must belong to the editor scene tree."
- return false
- if not str(get_script().resource_path).is_empty():
- refusal_reason = "The activation runner must be source-free before replacing its files."
- return false # A file-backed runner can be replaced by the scan it starts.
- if package.get("stage_root") != STAGE_ROOT:
- refusal_reason = "The update stage root does not match the verified activation location."
- return false
- if not package.get("record") is Dictionary:
- refusal_reason = "The update handoff requires an installer record Dictionary."
- return false
- var record: Dictionary = package.record
- for key in ["from_version", "to_version", "manifest_sha256", "expected_tree_sha256", "editor_nonce"]:
- if not record.get(key) is String or str(record[key]).is_empty():
- refusal_reason = "The update record requires a nonempty String field: %s." % key
- return false
- if not record.get("replace_owned_mismatches") is bool:
- refusal_reason = "The update record requires a boolean replace_owned_mismatches field."
- return false
- # Copy only values that belong to the existing installer's swap record.
- _package = {"stage_root": STAGE_ROOT, "record": {}}
- for key in ["from_version", "to_version", "manifest_sha256", "expected_tree_sha256", "editor_nonce", "replace_owned_mismatches"]:
- _package.record[key] = record[key]
- _installer = ResourceLoader.load(INSTALLER_PATH, "", ResourceLoader.CACHE_MODE_IGNORE_DEEP) as Script
- _verifier = ResourceLoader.load(VERIFIER_PATH, "", ResourceLoader.CACHE_MODE_IGNORE_DEEP) as Script
- if _installer == null or _verifier == null:
- refusal_reason = "The update installer or release verifier script could not be loaded."
- return false
- for method in ["swap", "verify_after_restart", "release_lock", "read_lock", "discard_stage"]:
- if not _declares(_installer, method):
- refusal_reason = "The update installer is missing its required %s method." % method
- return false
- if not _declares(_verifier, "hash_tree"):
- refusal_reason = "The release verifier is missing its required hash_tree method."
- return false
- var lock: Dictionary = _installer.call("read_lock")
- if int(lock.get("pid", 0)) != OS.get_process_id() or str(lock.get("fingerprint", "")).is_empty():
- refusal_reason = "The update lock does not identify this editor with a process fingerprint."
- return false
- _inspected_objects.clear()
- _inspection_budget = 100000
- if not _user_resources_can_keep_paths():
- return false
- refusal_reason = ""
- process_mode = Node.PROCESS_MODE_ALWAYS
- _phase = Phase.DRAIN
- set_process(false)
- _begin.call_deferred()
- return true
-
-
-func _declares(script: Script, method: String) -> bool:
- for entry in script.get_script_method_list():
- if str(entry.get("name", "")) == method:
- return true
- return false
-
-
-func _begin() -> void:
- _filesystem = EditorInterface.get_resource_filesystem()
- if _filesystem == null:
- _stop("The editor filesystem is unavailable; no update swap was attempted.")
- return
- _filesystem.sources_changed.connect(_scan_completed)
- EditorInterface.set_plugin_enabled(PLUGIN_CFG, false)
- if EditorInterface.is_plugin_enabled(PLUGIN_CFG):
- _stop("Godot could not disable the plugin; the verified stage was retained.")
- return
- _frames = 2
- set_process(true)
-
-
-func _process(_delta: float) -> void:
- if _phase == Phase.DRAIN:
- _frames -= 1
- if _frames <= 0:
- _phase = Phase.REQUEST_OLD_SCAN
- _deadline = Time.get_ticks_msec() + SCAN_TIMEOUT_MS
- elif _phase in [Phase.REQUEST_OLD_SCAN, Phase.WAIT_OLD_SCAN, Phase.REQUEST_NEW_SCAN, Phase.WAIT_NEW_SCAN]:
- if Time.get_ticks_msec() >= _deadline:
- _stop("The filesystem scan did not complete. The plugin remains disabled; the staged update and any backup were retained.")
- elif _phase in [Phase.REQUEST_OLD_SCAN, Phase.REQUEST_NEW_SCAN] and not _filesystem.is_scanning():
- _phase = Phase.WAIT_OLD_SCAN if _phase == Phase.REQUEST_OLD_SCAN else Phase.WAIT_NEW_SCAN
- # scan() may decline while a finished worker still awaits GUI
- # completion. Its sources_changed signal remains a valid barrier.
- _filesystem.scan()
-
-
-func _swap() -> void:
- set_process(false)
- # A cache lookup may resolve dependencies even when the requested script
- # is cached. Retain all old references while their complete tree exists;
- # never load a canonical path after the swap to recover an old script.
- if not _collect_cached_scripts(LIVE_ROOT):
- _stop("The previous script cache could not be captured safely; no update swap was attempted.")
- return
- var before: Dictionary = _verifier.call("hash_tree", LIVE_ROOT)
- if not bool(before.get("ok", false)):
- _stop("Could not hash the previous tree before activation; no swap was attempted.")
- return
- var swapped: Dictionary = _installer.call("swap", STAGE_ROOT, LIVE_ROOT, _package.record)
- if not bool(swapped.get("ok", false)):
- _failure = "Update swap failed: %s" % str(swapped.get("error", "unknown error"))
- var restored: Dictionary = _verifier.call("hash_tree", LIVE_ROOT)
- if not bool(restored.get("ok", false)) or restored.get("tree_sha256") != before.get("tree_sha256"):
- _stop(_failure + "; the original tree could not be proven restored. Explicit repair is required.")
- return
- _installer.call("discard_stage")
- _outcome = "previous_tree_retained"
- else:
- print("MCP | update to %s swapped in; reloading the plugin in this editor" % str(_package.record.to_version))
- var verified: Dictionary = _installer.call("verify_after_restart", LIVE_ROOT)
- _outcome = str(verified.get("status", ""))
- if _outcome != "success" and _outcome != "rolled_back":
- _stop("The installed tree could not be verified: %s" % str(verified.get("error", "unknown outcome")))
- return
- if _outcome == "rolled_back":
- _failure = "Update verification failed; the previous tree was restored: %s" % str(verified.get("error", ""))
- else:
- _backup_root = str(verified.get("backup_root", ""))
- var expected_backup := "res://addons/.godot_ai_update/backup/" + str(_package.record.from_version)
- if _backup_root != expected_backup:
- _stop("The update marker names an unexpected backup. The plugin remains disabled.")
- return
- var backup: Dictionary = _verifier.call("hash_tree", _backup_root)
- if not bool(backup.get("ok", false)) or backup.get("tree_sha256") != before.get("tree_sha256"):
- _stop("The retained backup could not be proven to match the previous tree. The plugin remains disabled.")
- return
- if _outcome == "success" and not _retire_cached_scripts(_backup_root):
- _stop("The previous script cache could not be retained safely. The plugin remains disabled.")
- return
- _release_scripts()
- _phase = Phase.REQUEST_NEW_SCAN
- _deadline = Time.get_ticks_msec() + SCAN_TIMEOUT_MS
- set_process(true)
-
-
-func _scan_completed(_sources_changed: bool) -> void:
- if _phase not in [Phase.WAIT_OLD_SCAN, Phase.WAIT_NEW_SCAN]:
- return
- if _filesystem.is_scanning():
- return # Another completion listener already started a newer scan.
- if Time.get_ticks_msec() >= _deadline:
- _stop("The filesystem scan completion arrived after the activation deadline. The plugin remains disabled.")
- return
- set_process(false)
- # sources_changed follows scan actions, documentation, and GUI updates.
- # filesystem_changed can occur earlier; a deferred completion can become
- # stale during a newer GUI scan. Consume this boundary synchronously.
- if _phase == Phase.WAIT_OLD_SCAN:
- _phase = Phase.SWAPPING
- _swap()
- else:
- _phase = Phase.ENABLING
- _filesystem.sources_changed.disconnect(_scan_completed)
- _enable()
-
-
-func _enable() -> void:
- if not _failure.is_empty() and str(_package.record.from_version).get_slice(".", 0).to_int() < 4:
- # A restored capsule must show Retry instead of automatically repeating
- # the failed update. This is its existing project-scoped status key.
- var settings := EditorInterface.get_editor_settings()
- if settings != null:
- var key := "godot_ai/v4_migration_bridge_status_" + ProjectSettings.globalize_path("res://").sha256_text()
- settings.set_setting(key, JSON.stringify({"error": _failure}))
- EditorInterface.set_plugin_enabled(PLUGIN_CFG, true)
- if not EditorInterface.is_plugin_enabled(PLUGIN_CFG):
- _stop("The scanned plugin could not be enabled. The installed tree and backup were retained.")
- return
- if _outcome == "success":
- var plugin := _find_plugin(get_tree().root)
- if plugin == null or str(plugin.get("_loaded_plugin_version")) != str(_package.record.to_version):
- EditorInterface.set_plugin_enabled(PLUGIN_CFG, false)
- _stop("The enabled plugin did not report the installed version. The update evidence was retained.")
- return
- var saved := ProjectSettings.save()
- if saved != OK:
- _stop("The plugin is enabled, but saving its enabled state failed: %s" % error_string(saved))
- return
- _phase = Phase.DONE
- _record(_outcome, _failure)
- if not _failure.is_empty():
- _show_failure(_failure)
- return
- print("MCP | update activation completed in editor PID %d" % OS.get_process_id())
- queue_free()
-
-
-func _user_resources_can_keep_paths() -> bool:
- # Retiring a Script changes resource_path for all its existing users.
- # Refuse serializable scene/resource references instead of silently making
- # a user's next save point into the updater's retained backup directory.
- var roots: Array = []
- if EditorInterface.has_method("get_open_scene_roots"):
- roots = EditorInterface.call("get_open_scene_roots")
- else:
- if EditorInterface.get_open_scenes().size() > 1:
- refusal_reason = "This Godot version cannot inspect every open scene safely. Close the other scene tabs before retrying the update."
- return false
- var current := EditorInterface.get_edited_scene_root()
- if current != null:
- roots.append(current)
- for root in roots:
- if not _inspect_user_node(root):
- return false
- var inspected := EditorInterface.get_inspector().get_edited_object()
- if inspected is Resource and not _inspect_user_value(inspected, 0):
- return false
- return true
-
-
-func _inspect_user_node(node: Node) -> bool:
- if not _inspect_user_value(node, 0):
- return false
- for child in node.get_children():
- if not _inspect_user_node(child):
- return false
- return true
-
-
-func _inspect_user_value(value: Variant, depth: int) -> bool:
- _inspection_budget -= 1
- if _inspection_budget < 0 or depth > 64:
- refusal_reason = "Open scene or resource data exceeded the safe inspection limit. Close the affected scenes or resources before retrying the update."
- return false
- if value is Script:
- if value.resource_path.begins_with(LIVE_ROOT + "/"):
- refusal_reason = "An open scene or resource references addon script %s. In-editor activation would change its saved script path. Close the scene or resource before retrying; the update was not installed." % value.resource_path
- return false
- return true
- if value is Resource or value is Node:
- var identity: int = value.get_instance_id()
- if _inspected_objects.has(identity):
- return true
- _inspected_objects[identity] = true
- for property in value.get_property_list():
- if int(property.usage) & PROPERTY_USAGE_STORAGE:
- if not _inspect_user_value(value.get(property.name), depth + 1):
- return false
- elif value is Array:
- for entry in value:
- if not _inspect_user_value(entry, depth + 1):
- return false
- elif value is Dictionary:
- for key in value:
- if not _inspect_user_value(key, depth + 1) or not _inspect_user_value(value[key], depth + 1):
- return false
- return true
-
-
-func _retire_cached_scripts(backup_root: String) -> bool:
- # All references were captured before replacement. Godot moves
- # both its resource and GDScript cache entries through take_over_path().
- # Existing UndoRedo callbacks keep the old compiled code and typed state;
- # canonical loads create a fresh graph, including fresh static variables.
- for script in _retired_scripts:
- var previous_path := script.resource_path
- var retained_path := backup_root + previous_path.trim_prefix(LIVE_ROOT)
- if not previous_path.begins_with(LIVE_ROOT + "/") or not FileAccess.file_exists(retained_path):
- return false
- if ResourceLoader.has_cached(retained_path):
- return false # Never displace a different retained generation.
- # A later update in this same editor must not prune source files still
- # referenced by an earlier generation's undo callbacks.
- get_tree().root.set_meta("godot_ai_retained_update_scripts", true)
- for script in _retired_scripts:
- var retained_path := backup_root + script.resource_path.trim_prefix(LIVE_ROOT)
- script.take_over_path(retained_path)
- if script.resource_path != retained_path:
- return false
- return true
-
-
-func _collect_cached_scripts(folder: String) -> bool:
- var directory := DirAccess.open(folder)
- if directory == null:
- return false
- for filename in directory.get_files():
- if not filename.ends_with(".gd"):
- continue
- var canonical := folder.path_join(filename)
- if ResourceLoader.has_cached(canonical):
- var script := ResourceLoader.load(canonical) as Script
- if script == null or script.resource_path != canonical:
- return false
- _retired_scripts.append(script)
- for child in directory.get_directories():
- if not _collect_cached_scripts(folder.path_join(child)):
- return false
- return true
-
-
-func _find_plugin(node: Node) -> Node:
- var script := node.get_script() as Script
- if script != null and script.resource_path == LIVE_ROOT + "/plugin.gd":
- return node
- for child in node.get_children():
- var found := _find_plugin(child)
- if found != null:
- return found
- return null
-
-
-func _release_scripts() -> void:
- if _installer != null:
- _installer.call("release_lock")
- _installer = null
- _verifier = null
-
-
-func _stop(message: String) -> void:
- _phase = Phase.DONE
- set_process(false)
- if _filesystem != null and _filesystem.sources_changed.is_connected(_scan_completed):
- _filesystem.sources_changed.disconnect(_scan_completed)
- _release_scripts()
- _record("activation_failed", message)
- _show_failure(message)
-
-
-func _record(status: String, message: String) -> void:
- var file := FileAccess.open(RECEIPT_PATH, FileAccess.WRITE)
- if file != null:
- file.store_string(JSON.stringify({"status": status, "error": message,
- "editor_pid": OS.get_process_id(), "from_version": _package.record.from_version,
- "to_version": _package.record.to_version, "unix": Time.get_unix_time_from_system()}))
- else:
- push_error("MCP | could not write update activation evidence: %s" % error_string(FileAccess.get_open_error()))
-
-
-func _show_failure(message: String) -> void:
- push_error("MCP | %s" % message)
- if DisplayServer.get_name() == "headless":
- queue_free()
- return
- var dialog := AcceptDialog.new()
- dialog.title = "Godot AI update needs attention"
- dialog.dialog_text = message + "\nUpdate evidence: res://addons/.godot_ai_update/"
- add_child(dialog)
- dialog.confirmed.connect(_dismiss_failure)
- dialog.canceled.connect(_dismiss_failure)
- dialog.popup_centered(Vector2i(620, 220))
-
-
-func _dismiss_failure() -> void:
- ## Godot may have left its shared progress dialog under the failure window.
- ## Return it before freeing our subtree and leaving the editor's pointer dangling.
- if is_inside_tree():
- for dialog in find_children("*", "ProgressDialog", true, false):
- dialog.reparent(get_tree().root)
- queue_free()
diff --git a/addons/godot_ai/utils/update_activation_runner.gd.uid b/addons/godot_ai/utils/update_activation_runner.gd.uid
deleted file mode 100644
index fd3597f..0000000
--- a/addons/godot_ai/utils/update_activation_runner.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://mj7jpaq6wt27
diff --git a/addons/godot_ai/utils/update_installer.gd b/addons/godot_ai/utils/update_installer.gd
deleted file mode 100644
index a74c893..0000000
--- a/addons/godot_ai/utils/update_installer.gd
+++ /dev/null
@@ -1,548 +0,0 @@
-@tool
-class_name McpUpdateInstaller
-extends RefCounted
-
-## The in-editor half of the lean self-updater: stage a verified archive
-## beside the live add-on, take the project-wide update lock, swap the two
-## trees with two directory renames, and after the restart verify the live
-## tree against the signed inventory, rolling back to the retained backup on
-## any mismatch. See docs/self-update.md.
-##
-## Everything lives under `res://addons/.godot_ai_update/` (dot-prefixed so
-## Godot's scanner ignores it; a `.gdignore` and a `.gitignore` are written
-## on first use) plus the live root the caller passes in. No function writes
-## anywhere else, no failure path deletes a backup, and every function fails
-## closed with a specific error string instead of raising.
-
-const UPDATE_ROOT := "res://addons/.godot_ai_update"
-const STAGE_DIR := UPDATE_ROOT + "/stage"
-const STAGE_PLUGIN_ROOT := STAGE_DIR + "/addons/godot_ai"
-const BACKUP_DIR := UPDATE_ROOT + "/backup"
-const QUARANTINE_DIR := UPDATE_ROOT + "/quarantine"
-const PENDING_FILE := UPDATE_ROOT + "/pending.json"
-const LOCK_FILE := UPDATE_ROOT + "/lock.json"
-const DEFAULT_LIVE_ROOT := "res://addons/godot_ai"
-const ENABLED_PLUGINS_SETTING := "editor_plugins/enabled"
-
-const STATUS_SWAPPED := "swapped"
-const STATUS_SUCCESS := "success"
-const STATUS_ROLLED_BACK := "rolled_back"
-const STATUS_REPAIR_REQUIRED := "repair_required"
-
-const PortResolver := preload("res://addons/godot_ai/utils/port_resolver.gd")
-const ReleaseVerifier := preload("res://addons/godot_ai/utils/release_verifier.gd")
-
-
-# ----- staging ----------------------------------------------------------
-
-
-## Extract `zip_path` into a fresh `stage/addons/godot_ai/` and prove the
-## extracted tree hashes to exactly the signed inventory. Returns
-## `{"ok", "error", "stage_root", "tree_sha256"}`; `stage_root` is the
-## `res://` path of the staged plugin directory.
-static func stage(zip_path: String, manifest: Dictionary) -> Dictionary:
- var result := {"ok": false, "error": "", "stage_root": "", "tree_sha256": ""}
- var validated := ReleaseVerifier.validate_manifest_dict(manifest)
- if not bool(validated.ok):
- result.error = "stage: %s" % str(validated.error)
- return result
- var normalised: Dictionary = validated.manifest
- var rooted := ensure_root()
- if not bool(rooted.ok):
- result.error = "stage: %s" % str(rooted.error)
- return result
- var stage_abs := _absolute(STAGE_DIR)
- if _exists(stage_abs):
- var removed := remove_under_update_root(STAGE_DIR)
- if not bool(removed.ok):
- result.error = "stage: cannot clear previous stage: %s" % str(removed.error)
- return result
- var reader := ZIPReader.new()
- var open_error := reader.open(_absolute(zip_path))
- if open_error != OK:
- result.error = "stage: cannot open archive %s: %s" % [zip_path, error_string(open_error)]
- return result
- var rows: Array = normalised["inventory"]
- var expected: Array = []
- for row in rows:
- expected.append(str(row["path"]))
- if Array(reader.get_files()) != expected:
- reader.close()
- result.error = "stage: archive entries do not match the signed inventory (run verify_archive for detail)"
- return result
- for row in rows:
- var path := str(row["path"])
- var target := stage_abs.path_join(path)
- var make_error := DirAccess.make_dir_recursive_absolute(target.get_base_dir())
- if make_error != OK:
- reader.close()
- result.error = "stage: cannot create %s: %s" % [target.get_base_dir(), error_string(make_error)]
- return result
- var write_error := _write_bytes(target, reader.read_file(path))
- if write_error != OK:
- reader.close()
- result.error = "stage: cannot write %s: %s" % [target, error_string(write_error)]
- return result
- reader.close()
- var expected_hash := ReleaseVerifier.inventory_tree_hash(normalised)
- var hashed := ReleaseVerifier.hash_tree(STAGE_PLUGIN_ROOT)
- if not bool(hashed.ok):
- remove_under_update_root(STAGE_DIR)
- result.error = "stage: %s" % str(hashed.error)
- return result
- if expected_hash.is_empty() or str(hashed.tree_sha256) != expected_hash:
- remove_under_update_root(STAGE_DIR)
- result.error = "stage: staged tree hash %s differs from the signed inventory hash %s" % [
- str(hashed.tree_sha256), expected_hash,
- ]
- return result
- result.stage_root = STAGE_PLUGIN_ROOT
- result.tree_sha256 = str(hashed.tree_sha256)
- result.ok = true
- return result
-
-
-## Remove `stage/` if present (for example when quiescence is refused after
-## staging). Never touches backups, quarantine, or the live tree.
-static func discard_stage() -> void:
- if _exists(_absolute(STAGE_DIR)):
- remove_under_update_root(STAGE_DIR)
-
-
-# ----- lock -------------------------------------------------------------
-
-
-## Take `lock.json` for this editor. A lock whose recorded process is alive
-## and carries a different fingerprint refuses; a dead or own lock is
-## replaced. Returns `{"ok", "error"}`.
-static func acquire_lock(pid: int, fingerprint: String) -> Dictionary:
- var result := {"ok": false, "error": ""}
- if pid <= 0 or fingerprint.is_empty():
- result.error = "lock: a positive pid and a non-empty fingerprint are required"
- return result
- var rooted := ensure_root()
- if not bool(rooted.ok):
- result.error = "lock: %s" % str(rooted.error)
- return result
- var existing := read_lock()
- if not existing.is_empty():
- var other_pid := int(existing.get("pid", 0))
- var other_fingerprint := str(existing.get("fingerprint", ""))
- if other_pid > 0 and other_fingerprint != fingerprint and PortResolver.pid_alive(other_pid):
- result.error = "lock: update lock is held by live process %d (%s) since %d" % [
- other_pid, other_fingerprint, int(existing.get("started_unix", 0)),
- ]
- return result
- var write_error := _write_json(LOCK_FILE, {
- "pid": pid,
- "fingerprint": fingerprint,
- "started_unix": int(Time.get_unix_time_from_system()),
- })
- if write_error != OK:
- result.error = "lock: cannot write %s: %s" % [LOCK_FILE, error_string(write_error)]
- return result
- result.ok = true
- return result
-
-
-## Drop `lock.json` when it belongs to this process or to a dead one. A
-## lock held by another live process is left in place.
-static func release_lock() -> void:
- var existing := read_lock()
- if existing.is_empty():
- if FileAccess.file_exists(_absolute(LOCK_FILE)):
- DirAccess.remove_absolute(_absolute(LOCK_FILE))
- return
- var pid := int(existing.get("pid", 0))
- if pid == OS.get_process_id() or not PortResolver.pid_alive(pid):
- DirAccess.remove_absolute(_absolute(LOCK_FILE))
-
-
-## The current lock record, or `{}` when there is none or it is unreadable.
-static func read_lock() -> Dictionary:
- return _read_json(LOCK_FILE)
-
-
-# ----- swap -------------------------------------------------------------
-
-
-## Replace the live tree with the staged one using two directory renames:
-## live -> `backup//`, then stage -> live. If the second rename
-## fails the first is undone. On success `pending.json` holds every key of
-## `record` verbatim plus `status: "swapped"`, `backup_root`, `live_root`,
-## `expected_tree_sha256` and `swapped_unix`. `record` must carry
-## `from_version`, `to_version`, `manifest_sha256`, `expected_tree_sha256`
-## and `editor_nonce`. Refuses when `backup//` already exists:
-## backups are pruned only after a verified success, so a leftover one means
-## an unresolved earlier update.
-static func swap(stage_root: String, live_root: String, record: Dictionary) -> Dictionary:
- var result := {"ok": false, "error": ""}
- for key in ["from_version", "to_version", "manifest_sha256", "expected_tree_sha256", "editor_nonce"]:
- if not record.has(key) or not record[key] is String or str(record[key]).is_empty():
- result.error = "swap: record is missing %s" % key
- return result
- var from_version := str(record["from_version"])
- var to_version := str(record["to_version"])
- if not _is_safe_segment(from_version) or not _is_safe_segment(to_version):
- result.error = "swap: versions must be plain path segments, got %s -> %s" % [from_version, to_version]
- return result
- if not _is_hex(str(record["expected_tree_sha256"]), 64):
- result.error = "swap: expected_tree_sha256 must be 64 lowercase hex characters"
- return result
- var stage_abs := _absolute(stage_root)
- var live_abs := _absolute(live_root)
- if not DirAccess.dir_exists_absolute(stage_abs):
- result.error = "swap: stage root does not exist: %s" % stage_abs
- return result
- if not DirAccess.dir_exists_absolute(live_abs):
- result.error = "swap: live root does not exist: %s" % live_abs
- return result
- if stage_abs.simplify_path() == live_abs.simplify_path():
- result.error = "swap: stage and live roots are the same directory"
- return result
- var pending := read_pending()
- if not pending.is_empty() and str(pending.get("status", "")) == STATUS_SWAPPED:
- result.error = "swap: an earlier swap is still pending verification (%s)" % PENDING_FILE
- return result
- var rooted := ensure_root()
- if not bool(rooted.ok):
- result.error = "swap: %s" % str(rooted.error)
- return result
- var backup_root := BACKUP_DIR + "/" + from_version
- var backup_abs := _absolute(backup_root)
- if _exists(backup_abs):
- result.error = "swap: backup already exists at %s; an earlier update was never resolved" % backup_root
- return result
- var make_error := DirAccess.make_dir_recursive_absolute(_absolute(BACKUP_DIR))
- if make_error != OK:
- result.error = "swap: cannot create %s: %s" % [BACKUP_DIR, error_string(make_error)]
- return result
- var first := DirAccess.rename_absolute(live_abs, backup_abs)
- if first != OK:
- result.error = "swap: cannot move live tree %s to %s: %s" % [live_abs, backup_abs, error_string(first)]
- return result
- var second := DirAccess.rename_absolute(stage_abs, live_abs)
- if second != OK:
- var restore := DirAccess.rename_absolute(backup_abs, live_abs)
- result.error = "swap: cannot move stage %s into %s: %s" % [stage_abs, live_abs, error_string(second)]
- if restore != OK:
- result.error += "; restoring the backup failed too (%s): live tree is at %s" % [
- error_string(restore), backup_abs,
- ]
- return result
- var marker := record.duplicate(true)
- marker["status"] = STATUS_SWAPPED
- marker["backup_root"] = backup_root
- marker["live_root"] = live_root
- marker["expected_tree_sha256"] = str(record["expected_tree_sha256"])
- marker["swapped_unix"] = int(Time.get_unix_time_from_system())
- ## The editor that verifies the swap should be this same Godot. On macOS
- ## the restart goes through LaunchServices, which has been seen to launch
- ## another installed copy of Godot; recording the version lets the next
- ## start explain that instead of silently refusing or verifying elsewhere.
- marker["godot_version"] = str(Engine.get_version_info().get("string", ""))
- var write_error := _write_json(PENDING_FILE, marker)
- if write_error != OK:
- var undo_stage := DirAccess.rename_absolute(live_abs, stage_abs)
- var undo_backup := DirAccess.rename_absolute(backup_abs, live_abs)
- result.error = "swap: cannot write %s: %s" % [PENDING_FILE, error_string(write_error)]
- if undo_stage != OK or undo_backup != OK:
- result.error += "; undoing the swap failed: new tree at %s, old tree at %s" % [
- live_abs if undo_stage != OK else stage_abs, backup_abs if undo_backup != OK else live_abs,
- ]
- return result
- ## The staged tree now lives; only its empty scaffold is left behind.
- discard_stage()
- result.ok = true
- return result
-
-
-# ----- pending marker ---------------------------------------------------
-
-
-## The `pending.json` record, `{}` when there is none. An unreadable marker
-## is reported as `status: "repair_required"` rather than ignored.
-static func read_pending() -> Dictionary:
- var absolute := _absolute(PENDING_FILE)
- if not FileAccess.file_exists(absolute):
- return {}
- var marker := _read_json(PENDING_FILE)
- if marker.is_empty():
- return {
- "status": STATUS_REPAIR_REQUIRED,
- "error": "pending marker %s exists but is not a JSON object; inspect it by hand" % PENDING_FILE,
- }
- return marker
-
-
-## Record in the success marker that the plugin repinned client configuration,
-## so later starts do not repeat the migration. No-op unless the marker
-## records success.
-static func record_clients_migrated() -> Error:
- var marker := read_pending()
- if str(marker.get("status", "")) != STATUS_SUCCESS:
- return ERR_UNAVAILABLE
- marker["clients_migrated"] = true
- return _write_json(PENDING_FILE, marker)
-
-
-## Remove `pending.json`.
-static func clear_pending() -> void:
- var absolute := _absolute(PENDING_FILE)
- if FileAccess.file_exists(absolute):
- DirAccess.remove_absolute(absolute)
-
-
-## After the restart: settle a `swapped` marker. Hash the live tree and
-## compare it to `expected_tree_sha256`. Equal: the marker becomes
-## `success`. Different: the live tree is renamed to
-## `quarantine//`, the backup is renamed back into place, and the
-## marker becomes `rolled_back` with the reason. No backup: the marker becomes
-## `repair_required` naming the exact paths and the live tree is left alone.
-## Nothing is ever deleted. A marker in any other status is returned
-## unchanged, and `{}` means there is nothing pending. Every marker key is
-## preserved verbatim.
-static func verify_after_restart(live_root: String) -> Dictionary:
- var marker := read_pending()
- if marker.is_empty():
- return {}
- if str(marker.get("status", "")) != STATUS_SWAPPED:
- ## A success marker whose client migration already ran is the durable
- ## record of the last update, not work for this start.
- if str(marker.get("status", "")) == STATUS_SUCCESS and bool(marker.get("clients_migrated", false)):
- return {}
- return marker
- var live_abs := _absolute(live_root)
- var expected := str(marker.get("expected_tree_sha256", ""))
- var hashed := ReleaseVerifier.hash_tree(live_abs)
- if bool(hashed.ok) and _is_hex(expected, 64) and str(hashed.tree_sha256) == expected:
- marker["status"] = STATUS_SUCCESS
- marker["error"] = ""
- marker["verified_unix"] = int(Time.get_unix_time_from_system())
- _write_json(PENDING_FILE, marker)
- return marker
- var reason := ""
- if not bool(hashed.ok):
- reason = "live tree could not be hashed: %s" % str(hashed.error)
- elif not _is_hex(expected, 64):
- reason = "pending marker carries no usable expected_tree_sha256"
- else:
- reason = "live tree hash %s does not match the expected %s" % [str(hashed.tree_sha256), expected]
- var backup_root := str(marker.get("backup_root", ""))
- var backup_abs := _absolute(backup_root) if not backup_root.is_empty() else ""
- if backup_abs.is_empty() or not DirAccess.dir_exists_absolute(backup_abs):
- return _settle_repair(marker, "%s; no backup exists at %s to restore, live tree left at %s" % [
- reason, backup_abs if not backup_abs.is_empty() else "(unrecorded)", live_abs,
- ])
- var to_version := str(marker.get("to_version", ""))
- var quarantine_root := QUARANTINE_DIR + "/" + (to_version if _is_safe_segment(to_version) else "unknown")
- var quarantine_abs := _absolute(quarantine_root)
- if _exists(quarantine_abs):
- return _settle_repair(marker, "%s; quarantine already exists at %s, live tree left at %s, backup at %s" % [
- reason, quarantine_abs, live_abs, backup_abs,
- ])
- if DirAccess.dir_exists_absolute(live_abs):
- var make_error := DirAccess.make_dir_recursive_absolute(_absolute(QUARANTINE_DIR))
- if make_error != OK:
- return _settle_repair(marker, "%s; cannot create %s (%s), live tree left at %s, backup at %s" % [
- reason, QUARANTINE_DIR, error_string(make_error), live_abs, backup_abs,
- ])
- var quarantine_error := DirAccess.rename_absolute(live_abs, quarantine_abs)
- if quarantine_error != OK:
- return _settle_repair(marker, "%s; cannot move live tree %s to %s (%s), backup at %s" % [
- reason, live_abs, quarantine_abs, error_string(quarantine_error), backup_abs,
- ])
- var restore_error := DirAccess.rename_absolute(backup_abs, live_abs)
- if restore_error != OK:
- var undo := DirAccess.rename_absolute(quarantine_abs, live_abs)
- return _settle_repair(marker, "%s; cannot restore backup %s to %s (%s); new tree is at %s" % [
- reason, backup_abs, live_abs, error_string(restore_error),
- live_abs if undo == OK else quarantine_abs,
- ])
- marker["status"] = STATUS_ROLLED_BACK
- marker["error"] = reason
- marker["quarantine_root"] = quarantine_root
- marker["verified_unix"] = int(Time.get_unix_time_from_system())
- _write_json(PENDING_FILE, marker)
- return marker
-
-
-# ----- retention --------------------------------------------------------
-
-
-## Remove every `backup//` except `keep_version`. Call only after a
-## verified success.
-static func prune_backups(keep_version: String) -> void:
- var backups := DirAccess.open(_absolute(BACKUP_DIR))
- if backups == null:
- return
- backups.include_hidden = true
- backups.include_navigational = false
- for name in backups.get_directories():
- if name != keep_version:
- remove_under_update_root(BACKUP_DIR + "/" + name)
-
-
-# ----- restart ----------------------------------------------------------
-
-
-## Add `plugin_cfg` to `editor_plugins/enabled` and save project.godot so the
-## next editor start loads the add-on, without enabling it in this process.
-static func persist_next_start_enabled(plugin_cfg: String) -> Error:
- if plugin_cfg.is_empty():
- return ERR_INVALID_PARAMETER
- var enabled := PackedStringArray()
- if ProjectSettings.has_setting(ENABLED_PLUGINS_SETTING):
- var current: Variant = ProjectSettings.get_setting(ENABLED_PLUGINS_SETTING)
- if current is PackedStringArray:
- enabled = current
- elif current is Array:
- for item in current:
- enabled.append(str(item))
- if not enabled.has(plugin_cfg):
- enabled.append(plugin_cfg)
- ProjectSettings.set_setting(ENABLED_PLUGINS_SETTING, enabled)
- return ProjectSettings.save()
-
-
-## Restart the editor, saving open scenes first.
-static func request_restart() -> void:
- if Engine.is_editor_hint():
- EditorInterface.restart_editor(true)
-
-
-# ----- update root ------------------------------------------------------
-
-
-## Create `res://addons/.godot_ai_update/` with its `.gdignore` and
-## `.gitignore` (`*`). Returns `{"ok", "error"}`.
-static func ensure_root() -> Dictionary:
- var result := {"ok": false, "error": ""}
- var root_abs := _absolute(UPDATE_ROOT)
- var make_error := DirAccess.make_dir_recursive_absolute(root_abs)
- if make_error != OK:
- result.error = "cannot create %s: %s" % [UPDATE_ROOT, error_string(make_error)]
- return result
- for marker in [[".gdignore", ""], [".gitignore", "*\n"]]:
- var path := root_abs.path_join(str(marker[0]))
- if FileAccess.file_exists(path):
- continue
- var write_error := _write_bytes(path, str(marker[1]).to_utf8_buffer())
- if write_error != OK:
- result.error = "cannot write %s: %s" % [path, error_string(write_error)]
- return result
- result.ok = true
- return result
-
-
-## Recursively remove a file or directory, refusing anything outside the
-## update root. Returns `{"ok", "error"}`.
-static func remove_under_update_root(path: String) -> Dictionary:
- var result := {"ok": false, "error": ""}
- var absolute := _absolute(path).simplify_path()
- var root_abs := _absolute(UPDATE_ROOT).simplify_path()
- if absolute == root_abs or not absolute.begins_with(root_abs + "/"):
- result.error = "refusing to remove %s: outside %s" % [absolute, UPDATE_ROOT]
- return result
- var remove_error := _remove_tree(absolute)
- if remove_error != OK:
- result.error = "cannot remove %s: %s" % [absolute, error_string(remove_error)]
- return result
- result.ok = true
- return result
-
-
-# ----- helpers ----------------------------------------------------------
-
-
-static func _settle_repair(marker: Dictionary, error: String) -> Dictionary:
- marker["status"] = STATUS_REPAIR_REQUIRED
- marker["error"] = error
- marker["verified_unix"] = int(Time.get_unix_time_from_system())
- _write_json(PENDING_FILE, marker)
- return marker
-
-
-static func _remove_tree(absolute: String) -> Error:
- var parent := DirAccess.open(absolute.get_base_dir())
- if parent == null:
- return DirAccess.get_open_error()
- if not DirAccess.dir_exists_absolute(absolute) or parent.is_link(absolute):
- return DirAccess.remove_absolute(absolute)
- var directory := DirAccess.open(absolute)
- if directory == null:
- return DirAccess.get_open_error()
- directory.include_hidden = true
- directory.include_navigational = false
- for name in directory.get_files():
- var file_error := DirAccess.remove_absolute(absolute.path_join(name))
- if file_error != OK:
- return file_error
- for name in directory.get_directories():
- var child_error := _remove_tree(absolute.path_join(name))
- if child_error != OK:
- return child_error
- return DirAccess.remove_absolute(absolute)
-
-
-static func _write_bytes(absolute: String, data: PackedByteArray) -> Error:
- var file := FileAccess.open(absolute, FileAccess.WRITE)
- if file == null:
- return FileAccess.get_open_error()
- file.store_buffer(data)
- var error := file.get_error()
- file.close()
- return error
-
-
-static func _write_json(path: String, data: Dictionary) -> Error:
- return _write_bytes(_absolute(path), (JSON.stringify(data, "\t") + "\n").to_utf8_buffer())
-
-
-static func _read_json(path: String) -> Dictionary:
- var absolute := _absolute(path)
- if not FileAccess.file_exists(absolute):
- return {}
- var bytes := FileAccess.get_file_as_bytes(absolute)
- if bytes.is_empty() or not ReleaseVerifier.is_valid_utf8(bytes):
- return {}
- var parsed: Variant = JSON.parse_string(bytes.get_string_from_utf8())
- if not parsed is Dictionary:
- return {}
- return parsed
-
-
-static func _exists(absolute: String) -> bool:
- return DirAccess.dir_exists_absolute(absolute) or FileAccess.file_exists(absolute)
-
-
-static func _is_safe_segment(name: String) -> bool:
- if name.is_empty() or name.length() > 64 or name == "." or name == ".." or name.begins_with("."):
- return false
- for index in name.length():
- var code := name.unicode_at(index)
- var ok := (
- (code >= 48 and code <= 57)
- or (code >= 65 and code <= 90)
- or (code >= 97 and code <= 122)
- or code == 45 or code == 46 or code == 95
- )
- if not ok:
- return false
- return true
-
-
-static func _is_hex(text: String, length: int) -> bool:
- if text.length() != length:
- return false
- for index in text.length():
- var code := text.unicode_at(index)
- if not ((code >= 48 and code <= 57) or (code >= 97 and code <= 102)):
- return false
- return true
-
-
-static func _absolute(path: String) -> String:
- if path.begins_with("res://") or path.begins_with("user://"):
- return ProjectSettings.globalize_path(path)
- return path
diff --git a/addons/godot_ai/utils/update_installer.gd.uid b/addons/godot_ai/utils/update_installer.gd.uid
deleted file mode 100644
index 337a3af..0000000
--- a/addons/godot_ai/utils/update_installer.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c30k72xyjmfrp
diff --git a/addons/godot_ai/utils/update_manager.gd b/addons/godot_ai/utils/update_manager.gd
deleted file mode 100644
index 4f6fabb..0000000
--- a/addons/godot_ai/utils/update_manager.gd
+++ /dev/null
@@ -1,533 +0,0 @@
-@tool
-class_name McpUpdateManager
-extends Node
-
-## v4-only release discovery and download preparation. Python authenticates
-## and extracts the three canonical assets before this manager asks the root to
-## quiesce or disable anything; the transaction actor alone mutates live code.
-
-const RELEASES_URL := "https://api.github.com/repos/hi-godot/godot-ai/releases/latest"
-const RELEASES_PAGE := "https://github.com/hi-godot/godot-ai/releases/latest"
-const REPOSITORY := "hi-godot/godot-ai"
-const ASSET_NAME := "godot-ai-v4-plugin.zip"
-const MANIFEST_NAME := "godot-ai-v4-plugin.manifest.json"
-const SIGNATURE_NAME := "godot-ai-v4-plugin.manifest.sig"
-const LEGACY_ASSET_NAME := "godot-ai-plugin.zip"
-const LEGACY_CHECKSUM_NAME := "godot-ai-plugin.zip.sha256"
-const LEGACY_SIGNATURE_NAME := "godot-ai-plugin.zip.sha256.sig"
-const MAX_RELEASE_METADATA_BYTES := 1024 * 1024
-const MAX_ARCHIVE_SIZE_BYTES := 64 * 1024 * 1024
-const MAX_MANIFEST_SIZE_BYTES := 1024 * 1024
-const MAX_REDIRECTS := 5
-const QUALIFICATION_SWITCH_ENV := "GODOT_AI_QUALIFICATION_RELEASE"
-const QUALIFICATION_URL_ENV := "GODOT_AI_QUALIFICATION_RELEASE_URL"
-const QUALIFICATION_ASSET_PREFIX_ENV := "GODOT_AI_QUALIFICATION_ASSET_PREFIX"
-const QUALIFICATION_TOKEN_ENV := "GODOT_AI_QUALIFICATION_TOKEN"
-const _ASSET_LIMITS := {
- ASSET_NAME: MAX_ARCHIVE_SIZE_BYTES,
- MANIFEST_NAME: MAX_MANIFEST_SIZE_BYTES,
- SIGNATURE_NAME: 512,
-}
-## Stable releases also carry one signed, temporary v3 migration capsule.
-## v4 never downloads it, but requires the exact six-name release envelope so
-## an unexpected extra executable asset remains a fail-closed condition.
-const _RELEASE_ASSET_LIMITS := {
- ASSET_NAME: MAX_ARCHIVE_SIZE_BYTES,
- MANIFEST_NAME: MAX_MANIFEST_SIZE_BYTES,
- SIGNATURE_NAME: 512,
- LEGACY_ASSET_NAME: 66 * 1024 * 1024,
- LEGACY_CHECKSUM_NAME: 1024,
- LEGACY_SIGNATURE_NAME: 512,
-}
-const ClientConfigurator := preload("res://addons/godot_ai/client_configurator.gd")
-const TransportCapability := preload("res://addons/godot_ai/utils/transport_capability.gd")
-
-## Kept byte-for-byte aligned with src/godot_ai/release_verify.py and the
-## standalone verifier. Python performs the mandatory strict verification;
-## embedding it here also lets release packaging fail on trust-anchor drift.
-const RELEASE_SIGNING_PUBLIC_KEY_PEM := """-----BEGIN PUBLIC KEY-----
-MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr4OmbONFTONGFcXSUQ2p
-e54YaUhWDA75wxeDWhOc476vsdo53YnXEFT7EPr2hUKqeNxv++LqKOkFuAsxSNZy
-wBe6P1tmQA4Og6Ezv4CGnZdEj1uhlDJFK9ShQ29oWfC6bf/84625SvvBxZos2Br9
-yPKl7h5wzqDoeUSpv+f0ynTiC0i/HAUo/NQBlkgGwkomK2Fr3pP1VDxxq2xvgHSk
-lU6Qcomr9WjJxI+HkDN5tRPPn0pDrg6YFx2J18OfD8KIa/kMGxuXOcHlPyRYpjyu
-qTtg2oL0NyUIG+1TmJ3DcN4GlKC55eOrkfJ04vudS5pxdnUIFRmkGBXZLdaetoPc
-ixtlD4w6gi8KIH1CTG+/TtHP1KVdOogCWDcjRCAmMJPFZe6eEKXmGQUZDb9wfnbx
-h++XiVe5tq83BTLWmaFTy+fZbNo12uhNCNS1LJ42/yj+S1xvo0yMbkkNr1hIYk0P
-584XnBQeBSVJDf3667NZXaxnWv94K9zbb+1OvOvPwhbOdgi2Ymcw5QEOQIavtg86
-XLLcWzG+SJsycz1imikjv6sStWh8WHneKSTMq6A7V6PBj7oJyEJp10696BDw287k
-YlH+9VGqowPEMXpWX57wOBKiWb4K1kw1LfxjT8W1e/pcX9pJqiv0DkjTXUxo9CDG
-1X1+ZXBBR3MkGuFAOCjy0x8CAwEAAQ==
------END PUBLIC KEY-----
-"""
-
-const _TRUSTED_PATHS := {
- "github.com": "/hi-godot/godot-ai/releases/download/",
- "www.github.com": "/hi-godot/godot-ai/releases/download/",
- "api.github.com": "/repos/hi-godot/godot-ai/releases/assets/",
- "objects.githubusercontent.com": "/github-production-release-asset-",
- "release-assets.githubusercontent.com": "/github-production-release-asset-",
-}
-
-signal update_check_completed(result: Dictionary)
-signal install_state_changed(state: Dictionary)
-signal activation_requested(package: Dictionary)
-
-var _check_request: HTTPRequest
-var _asset_request: HTTPRequest
-var _release: Dictionary = {}
-var _queue: Array[String] = []
-var _active_asset := ""
-var _redirect_count := 0
-var _qualification: Dictionary = {}
-var _download_root := ""
-
-
-func check_for_updates() -> void:
- _release.clear()
- _qualification = _qualification_from_environment()
- if bool(_qualification.get("invalid", false)):
- _qualification.clear()
- push_error("MCP | invalid private qualification release capability")
- return
- if ClientConfigurator.is_dev_checkout():
- return
- if _check_request != null:
- _check_request.queue_free()
- _check_request = HTTPRequest.new()
- _check_request.max_redirects = 0
- _check_request.body_size_limit = MAX_RELEASE_METADATA_BYTES
- _check_request.request_completed.connect(_on_check_completed)
- add_child(_check_request)
- var url := str(_qualification.get("release_url", RELEASES_URL))
- if _check_request.request(url, _request_headers(url, _qualification, true)) != OK:
- _check_request.queue_free()
- _check_request = null
-
-
-func start_install(preflight: Dictionary) -> void:
- if is_install_in_flight():
- install_state_changed.emit({
- "status_text": "Update already in progress",
- "button_disabled": true,
- })
- return
- if _release.is_empty():
- OS.shell_open(RELEASES_PAGE)
- return
- if not bool(preflight.get("ok", false)):
- install_state_changed.emit({
- "install_in_flight": false,
- "status_text": "Update blocked — resolve recovery state",
- "button_disabled": false,
- })
- return
- var directory := str(preflight.get("download_root", ""))
- if (
- directory.is_empty()
- or not directory.is_absolute_path()
- or not _directory_is_empty(directory)
- ):
- install_state_changed.emit({
- "install_in_flight": false,
- "status_text": "Update blocked — private download directory unavailable",
- "button_disabled": false,
- })
- return
- _download_root = directory
- _queue.assign([ASSET_NAME, MANIFEST_NAME, SIGNATURE_NAME])
- install_state_changed.emit({
- "install_in_flight": true,
- "status_text": "Downloading…",
- "button_disabled": true,
- })
- _download_next()
-
-
-func is_install_in_flight() -> bool:
- return _asset_request != null or not _download_root.is_empty()
-
-
-func has_install_candidate() -> bool:
- return not is_install_in_flight() and not _release.is_empty()
-
-
-static func _directory_is_empty(path: String) -> bool:
- var directory := DirAccess.open(path)
- if directory == null:
- return false
- directory.list_dir_begin()
- var first := directory.get_next()
- directory.list_dir_end()
- return first.is_empty()
-
-
-func cancel_install() -> void:
- if _asset_request != null:
- _asset_request.cancel_request()
- _asset_request.queue_free()
- _asset_request = null
- _queue.clear()
- _qualification.clear()
- discard_downloads()
-
-
-func _exit_tree() -> void:
- cancel_install()
-
-
-## Download roots are exclusively allocated by the Python transaction actor.
-## This manager never creates, reuses, or recursively deletes a predictable
-## user:// namespace; it removes only the three exact files it requested and
-## then the now-empty transaction directory.
-func discard_downloads() -> void:
- if _download_root.is_empty():
- return
- for name in [ASSET_NAME, MANIFEST_NAME, SIGNATURE_NAME]:
- DirAccess.remove_absolute(_download_root.path_join(name))
- DirAccess.remove_absolute(_download_root)
- _download_root = ""
-
-
-func _download_path(name: String) -> String:
- if _download_root.is_empty() or not _ASSET_LIMITS.has(name):
- return ""
- return _download_root.path_join(name)
-
-
-static func parse_releases_response(
- result: int,
- response_code: int,
- body: PackedByteArray,
- local_version: String = "",
- qualification: Dictionary = {},
-) -> Dictionary:
- var empty := {
- "has_update": false,
- "version": "",
- "tag": "",
- "channel": "",
- "label_text": "",
- "urls": {},
- "sizes": {},
- }
- if (
- result != HTTPRequest.RESULT_SUCCESS
- or response_code != 200
- or body.size() > MAX_RELEASE_METADATA_BYTES
- ):
- return empty
- var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
- if not parsed is Dictionary:
- return empty
- var tag := str(parsed.get("tag_name", ""))
- var version := tag.trim_prefix("v")
- var local := local_version if not local_version.is_empty() else ClientConfigurator.get_plugin_version()
- if not tag.begins_with("v4.") or not _is_newer(version, local):
- return empty
- var assets: Variant = parsed.get("assets", [])
- if not assets is Array or assets.size() != _RELEASE_ASSET_LIMITS.size():
- return empty
- var urls := {}
- var sizes := {}
- for value in assets:
- if not value is Dictionary:
- return empty
- var name := str(value.get("name", ""))
- if not _RELEASE_ASSET_LIMITS.has(name) or sizes.has(name):
- return empty
- var size := int(value.get("size", -1))
- if size <= 0 or size > int(_RELEASE_ASSET_LIMITS[name]):
- return empty
- if name in [SIGNATURE_NAME, LEGACY_SIGNATURE_NAME] and size != 512:
- return empty
- var url := str(value.get("browser_download_url", ""))
- if not _is_trusted_download_url(url, qualification):
- return empty
- sizes[name] = size
- if _ASSET_LIMITS.has(name):
- urls[name] = url
- if urls.size() != 3:
- return empty
- return {
- "has_update": true,
- "version": version,
- "tag": tag,
- "channel": "stable",
- "label_text": "Update available: %s" % tag,
- "urls": urls,
- "sizes": sizes,
- }
-
-
-static func _is_newer(remote: String, local: String) -> bool:
- var remote_parts := _version_parts(remote)
- var local_parts := _version_parts(local)
- if remote_parts.is_empty() or local_parts.is_empty():
- return false
- for index in 3:
- if remote_parts[index] != local_parts[index]:
- return remote_parts[index] > local_parts[index]
- return false
-
-
-static func _version_parts(version: String) -> Array[int]:
- var expression := RegEx.new()
- if expression.compile("^4\\.(\\d+)\\.(\\d+)$") != OK:
- return []
- var found := expression.search(version)
- if found == null:
- return []
- return [4, int(found.get_string(1)), int(found.get_string(2))]
-
-
-static func _https_url_parts(url: String) -> Dictionary:
- const SCHEME := "https://"
- if not url.begins_with(SCHEME) or url.contains("\\") or url.contains("#"):
- return {}
- for index in url.length():
- var code: int = url.unicode_at(index)
- if code <= 0x20 or code == 0x7f:
- return {}
- var rest := url.substr(SCHEME.length())
- var slash := rest.find("/")
- if slash < 0:
- return {}
- var authority := rest.substr(0, slash)
- var path := rest.substr(slash)
- if authority.is_empty() or authority.contains("@") or authority.count(":") > 1:
- return {}
- var host := authority
- var colon := authority.find(":")
- if colon >= 0:
- if authority.substr(colon + 1) != "443":
- return {}
- host = authority.substr(0, colon)
- if (
- host.is_empty()
- or host != host.to_lower()
- or host.begins_with(".")
- or host.ends_with(".")
- or host.contains("..")
- ):
- return {}
- for index in host.length():
- var code := host.unicode_at(index)
- if not (code >= 97 and code <= 122) and not (code >= 48 and code <= 57) and code not in [45, 46]:
- return {}
- var query := path.find("?")
- if query >= 0:
- path = path.substr(0, query)
- var lower := path.to_lower()
- for needle in ["/../", "/..", "%2e", "%2f", "%5c"]:
- if lower.contains(needle):
- return {}
- return {"host": host, "origin": SCHEME + host, "path": path}
-
-
-static func _is_trusted_download_url(url: String, qualification: Dictionary = {}) -> bool:
- var parts := _https_url_parts(url)
- if parts.is_empty():
- return false
- ## Current GitHub release assets use a slash-delimited repository ID;
- ## retain the older CDN namespace below for existing release URLs.
- if (
- parts.host == "release-assets.githubusercontent.com"
- and str(parts.path).begins_with("/github-production-release-asset/1208239711/")
- ):
- return true
- if (
- str(parts.origin) == str(qualification.get("asset_origin", ""))
- and str(parts.path).begins_with(str(qualification.get("asset_path", "")))
- and not str(qualification.get("asset_path", "")).is_empty()
- ):
- return true
- return _TRUSTED_PATHS.has(parts.host) and str(parts.path).begins_with(
- str(_TRUSTED_PATHS[parts.host])
- )
-
-
-static func _qualification_config(
- enabled: String, release_url: String, asset_prefix: String, token: String
-) -> Dictionary:
- if enabled.is_empty() and release_url.is_empty() and asset_prefix.is_empty() and token.is_empty():
- return {}
- var release := _https_url_parts(release_url)
- var assets := _https_url_parts(asset_prefix)
- if (
- enabled != "1"
- or release.is_empty()
- or assets.is_empty()
- or not asset_prefix.ends_with("/")
- or asset_prefix.contains("?")
- or release.origin != assets.origin
- or not TransportCapability.is_http_capability(token)
- ):
- return {"invalid": true}
- return {
- "asset_origin": str(assets.origin),
- "asset_path": str(assets.path),
- "release_url": release_url,
- "token": token,
- }
-
-
-static func _qualification_from_environment() -> Dictionary:
- return _qualification_config(
- OS.get_environment(QUALIFICATION_SWITCH_ENV),
- OS.get_environment(QUALIFICATION_URL_ENV),
- OS.get_environment(QUALIFICATION_ASSET_PREFIX_ENV),
- OS.get_environment(QUALIFICATION_TOKEN_ENV),
- )
-
-
-static func _request_headers(
- url: String, qualification: Dictionary, metadata: bool = false
-) -> PackedStringArray:
- var headers := PackedStringArray(["Accept: application/vnd.github+json"] if metadata else [])
- var authorized := url == str(qualification.get("release_url", ""))
- if not authorized:
- var parts := _https_url_parts(url)
- authorized = (
- not parts.is_empty()
- and parts.origin == qualification.get("asset_origin", "")
- and str(parts.path).begins_with(str(qualification.get("asset_path", "")))
- and not str(qualification.get("asset_path", "")).is_empty()
- )
- if authorized:
- headers.append("Authorization: Bearer " + str(qualification.token))
- return headers
-
-
-static func _redirect_url(headers: PackedStringArray) -> String:
- var location := ""
- for header in headers:
- var colon := header.find(":")
- if colon < 0 or header.substr(0, colon).strip_edges().to_lower() != "location":
- continue
- if not location.is_empty():
- return ""
- location = header.substr(colon + 1).strip_edges()
- return location
-
-
-func _on_check_completed(
- result: int,
- response_code: int,
- _headers: PackedStringArray,
- body: PackedByteArray
-) -> void:
- if _check_request != null:
- _check_request.queue_free()
- _check_request = null
- var release := parse_releases_response(result, response_code, body, "", _qualification)
- if not bool(release.get("has_update", false)):
- _release.clear()
- _qualification.clear()
- return
- _release = release.duplicate(true)
- update_check_completed.emit(release.duplicate(true))
-
-
-func _download_next() -> void:
- if _queue.is_empty():
- _finish_downloads()
- return
- _active_asset = _queue.pop_front()
- _redirect_count = 0
- _request_active_asset(str((_release.get("urls", {}) as Dictionary).get(_active_asset, "")))
-
-
-func _request_active_asset(url: String) -> void:
- _asset_request = HTTPRequest.new()
- ## Follow redirects ourselves so every hop remains inside the pinned GitHub
- ## release/CDN namespace; HTTPRequest's automatic mode exposes no hop hook.
- _asset_request.max_redirects = 0
- _asset_request.body_size_limit = int((_release.get("sizes", {}) as Dictionary).get(_active_asset, 0))
- _asset_request.download_file = _download_path(_active_asset)
- _asset_request.request_completed.connect(_on_asset_completed)
- add_child(_asset_request)
- if _asset_request.request(url, _request_headers(url, _qualification)) != OK:
- _fail_download("request failed")
-
-
-func _on_asset_completed(
- result: int,
- response_code: int,
- headers: PackedStringArray,
- _body: PackedByteArray
-) -> void:
- if _asset_request != null:
- _asset_request.queue_free()
- _asset_request = null
- ## With max_redirects = 0 Godot returns REDIRECT_LIMIT_REACHED on the
- ## first redirect, including its status and Location. Validate that hop
- ## ourselves; other transport failures must still fail closed.
- if (
- result in [HTTPRequest.RESULT_SUCCESS, HTTPRequest.RESULT_REDIRECT_LIMIT_REACHED]
- and response_code in [301, 302, 303, 307, 308]
- ):
- var redirect := _redirect_url(headers)
- DirAccess.remove_absolute(_download_path(_active_asset))
- if (
- _redirect_count >= MAX_REDIRECTS
- or not _is_trusted_download_url(redirect, _qualification)
- ):
- _fail_download("untrusted or excessive redirect")
- return
- _redirect_count += 1
- _request_active_asset(redirect)
- return
- if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
- _fail_download("download failed (%d)" % response_code)
- return
- var path := _download_path(_active_asset)
- var file := FileAccess.open(path, FileAccess.READ)
- var expected := int((_release.get("sizes", {}) as Dictionary).get(_active_asset, -1))
- if file == null:
- _fail_download("downloaded asset size differs from release metadata")
- return
- var actual_size := file.get_length()
- file.close()
- if actual_size != expected:
- _fail_download("downloaded asset size differs from release metadata")
- return
- _download_next()
-
-
-func _finish_downloads() -> void:
- var directory := _download_root
- var manifest_path := directory.path_join(MANIFEST_NAME)
- var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(manifest_path))
- if not parsed is Dictionary or str(parsed.get("source_commit", "")).is_empty():
- _fail_download("manifest is unreadable")
- return
- activation_requested.emit({
- "archive": directory.path_join(ASSET_NAME),
- "manifest": manifest_path,
- "signature": directory.path_join(SIGNATURE_NAME),
- "repository": REPOSITORY,
- "channel": str(_release.channel),
- "tag": str(_release.tag),
- "version": str(_release.version),
- "source": str(parsed.source_commit),
- "download_root": directory,
- })
- _qualification.clear()
-
-
-func _fail_download(reason: String) -> void:
- if _asset_request != null:
- _asset_request.queue_free()
- _asset_request = null
- _queue.clear()
- _qualification.clear()
- discard_downloads()
- push_error("MCP | v4 update preparation failed: %s" % reason)
- ## The click-time lock and any quiesced client work are the plugin's to
- ## release, and it only does so when it hears the install is over.
- install_state_changed.emit({
- "install_in_flight": false,
- "status_text": "Update preparation failed",
- "button_disabled": false,
- })
diff --git a/addons/godot_ai/utils/update_manager.gd.uid b/addons/godot_ai/utils/update_manager.gd.uid
deleted file mode 100644
index 4089a72..0000000
--- a/addons/godot_ai/utils/update_manager.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cegiyw3fjcwev
diff --git a/addons/godot_ai/utils/update_mixed_state.gd b/addons/godot_ai/utils/update_mixed_state.gd
deleted file mode 100644
index f212c95..0000000
--- a/addons/godot_ai/utils/update_mixed_state.gd
+++ /dev/null
@@ -1,141 +0,0 @@
-@tool
-extends RefCounted
-
-## Scanner that detects whether `addons/godot_ai/` is in a half-installed
-## state left behind by a self-update whose rollback couldn't restore the
-## previous addon contents (`UpdateReloadRunner.InstallStatus.FAILED_MIXED`).
-##
-## Without this surface the user sees "plugin won't start" with no actionable
-## context, re-runs the update, and compounds the mismatch (issue #354 /
-## audit-v2 #10). The dock paints a banner from `diagnose()` and
-## `editor_handler.gd::get_editor_state` includes the same Dictionary so an
-## MCP agent can see and report the state.
-
-const ADDON_DIR := "res://addons/godot_ai/"
-## Producer is `update_reload_runner.gd::INSTALL_BACKUP_SUFFIX`. Inlined as a
-## literal because old two-phase runners can parse this diagnostic script
-## against stale runner Script-object content during their mixed-snapshot
-## scan. `test_update_backup_suffix_stays_in_sync` guards against drift.
-const BACKUP_SUFFIX := ".update_backup"
-## Cap so a runaway addons tree (someone parented the wrong dir, an old
-## crashed install left thousands of artifacts) can't blow the
-## `editor_state` payload size or freeze the editor on first paint.
-const MAX_BACKUP_RESULTS := 200
-## TTL for the `diagnose()` cache. `editor_state` is one of the highest-
-## traffic MCP tools (agents poll it constantly) and a recursive
-## `DirAccess` walk on every call would put I/O on the 4ms `_process()`
-## budget. Mixed-state is rare and persistent across editor restarts, so
-## a few seconds of staleness is acceptable; the dock's Re-scan button
-## bypasses the cache via `force=true` for immediate feedback.
-const CACHE_TTL_MSEC := 5000
-
-static var _cache_value: Dictionary = {}
-static var _cache_timestamp_msec: int = -1
-
-
-## Walk `dir` recursively and return every `res://`-relative path that ends
-## in `.update_backup`, sorted ascending. Truncates at `MAX_BACKUP_RESULTS`
-## — the truncation flag is exposed via `diagnose()`.
-##
-## Walk order is deterministic: entries within each directory are sorted
-## alphabetically, subdirs pushed reverse-sorted so DFS pops them in
-## ascending order. Without this two scans of the same mixed tree could
-## return different 200-file slices when truncation kicks in (Godot's
-## `list_dir` order isn't guaranteed stable across filesystems).
-static func find_backups(dir: String = ADDON_DIR) -> Array:
- var results: Array = []
- var stack: Array = [dir]
- while not stack.is_empty():
- if results.size() >= MAX_BACKUP_RESULTS:
- break
- var current: String = stack.pop_back()
- var d := DirAccess.open(current)
- ## Missing dir, permission error, or unreadable junction — skip
- ## silently. A missing addons dir is the bare-clone case; mid-walk
- ## errors stay quiet so a single permission glitch can't block the
- ## diagnostic the rest of the scan would have produced.
- if d == null:
- continue
- var entries: Array = []
- d.list_dir_begin()
- while true:
- var entry := d.get_next()
- if entry.is_empty():
- break
- if entry == "." or entry == "..":
- continue
- entries.append({"name": entry, "is_dir": d.current_is_dir()})
- d.list_dir_end()
- entries.sort_custom(func(a, b): return a["name"] < b["name"])
- ## Push subdirs reverse-sorted so the next outer iteration pops
- ## them in ascending order — see method docstring for why this
- ## determinism matters for the truncated case.
- for i in range(entries.size() - 1, -1, -1):
- var entry: Dictionary = entries[i]
- var child_path := current.path_join(entry["name"])
- if entry["is_dir"] and not d.is_link(child_path):
- stack.append(child_path)
- for entry in entries:
- if entry["is_dir"]:
- continue
- if not String(entry["name"]).ends_with(BACKUP_SUFFIX):
- continue
- results.append(current.path_join(entry["name"]))
- if results.size() >= MAX_BACKUP_RESULTS:
- break
- results.sort()
- return results
-
-
-## Build the structured diagnostic Dictionary surfaced via `editor_state`
-## and the dock banner. Empty when the addons tree is clean — callers
-## gate banner visibility / response field on `is_empty()`.
-##
-## Cached for `CACHE_TTL_MSEC` when scanning the default `ADDON_DIR` so
-## per-`editor_state` polls don't re-walk the addons tree every frame.
-## Tests passing a custom `dir` always see a fresh scan (cache only
-## tracks the production path). `force=true` bypasses the cache — used
-## by the dock's Re-scan button so a manual fix is reflected immediately.
-static func diagnose(dir: String = ADDON_DIR, force: bool = false) -> Dictionary:
- var use_cache := dir == ADDON_DIR and not force
- if use_cache and _cache_timestamp_msec >= 0:
- if Time.get_ticks_msec() - _cache_timestamp_msec < CACHE_TTL_MSEC:
- return _cache_value.duplicate(true)
-
- var backups := find_backups(dir)
- var result: Dictionary = {}
- if not backups.is_empty():
- ## Most commonly produced by `_rollback_paths_written` returning
- ## FAILED_MIXED, but `_finalize_install_success` removes backups on
- ## a best-effort basis so a successful install can also leave them
- ## behind if the cleanup `remove_absolute` hit a permission error.
- ## The recovery action — delete the *.update_backup files — is the
- ## same in both cases, so the message acknowledges both
- ## possibilities rather than asserting the alarming one.
- result = {
- "addon_dir": dir,
- "backup_files": backups,
- "backup_count": backups.size(),
- "truncated": backups.size() >= MAX_BACKUP_RESULTS,
- "message": (
- "Found .update_backup files in addons/godot_ai/. This usually"
- + " means a self-update rollback couldn't restore the previous"
- + " addon contents (FAILED_MIXED) — the plugin may load a mix"
- + " of old and new files. Restore the addon from your VCS or a"
- + " fresh release ZIP, then delete the listed *.update_backup"
- + " files. If the plugin runs without issues these are likely"
- + " stale from a successful install and safe to delete."
- ),
- }
- if use_cache:
- _cache_value = result.duplicate(true)
- _cache_timestamp_msec = Time.get_ticks_msec()
- return result
-
-
-## Reset the `diagnose()` cache. Tests that flip the addons-tree state
-## between calls use this to avoid TTL-bound flakiness; the dock's
-## Re-scan button uses `force=true` instead.
-static func clear_cache() -> void:
- _cache_value = {}
- _cache_timestamp_msec = -1
diff --git a/addons/godot_ai/utils/update_mixed_state.gd.uid b/addons/godot_ai/utils/update_mixed_state.gd.uid
deleted file mode 100644
index 6d608d9..0000000
--- a/addons/godot_ai/utils/update_mixed_state.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dd5rti52vgs71
diff --git a/addons/godot_ai/utils/uv_cache_cleanup.gd b/addons/godot_ai/utils/uv_cache_cleanup.gd
deleted file mode 100644
index 86edbe1..0000000
--- a/addons/godot_ai/utils/uv_cache_cleanup.gd
+++ /dev/null
@@ -1,161 +0,0 @@
-@tool
-class_name McpUvCacheCleanup
-extends RefCounted
-
-## Sweeps stale `.tmp*` build venvs out of `%LOCALAPPDATA%\uv\cache\builds-v0`.
-##
-## Background
-## ----------
-## When an MCP client's attach launcher invokes
-## `uvx --from godot-ai==VERSION godot-ai attach ...`, uv builds an ephemeral venv under
-## `builds-v0\.tmpXXXXXX\`. To save disk it hard-links shared C extensions
-## (notably `pydantic_core/_pydantic_core.cp313-win_amd64.pyd`) from
-## `archive-v0\\Lib\site-packages\...` into the build venv.
-##
-## If the godot-ai server's own Python child has that same `.pyd` mapped via
-## `LoadLibrary` (it does — godot-ai imports pydantic), the file is locked
-## under BOTH paths because hard links share the inode and Windows tracks
-## handles per-file, not per-path. uv's post-install cleanup of the build
-## venv then dies with:
-##
-## Failed to install: pywin32-311-cp313-cp313-win_amd64.whl (pywin32==311)
-## Caused by: failed to remove directory `...\.tmpXXXXXX\Lib\site-packages\pywin32-311.data`
-## 다른 프로세스가 파일을 사용 중이기 때문에 ... (os error 32)
-##
-## (the `pywin32` mention is incidental — the actual lock is on the earlier
-## hard-linked `_pydantic_core.pyd`; pywin32 is just the last install step
-## in the wheel-resolution order that triggers the cleanup pass).
-##
-## What this does
-## --------------
-## After the plugin stops/restarts the managed server — i.e. the moment when
-## the archive-v0 `.pyd` mappings drop and the hard-linked builds-v0 copy
-## becomes deletable — sweep `builds-v0\` for `.tmp*` orphans:
-##
-## 1. Rename each `.tmpXXX` to `_dead_.tmpXXX`. Rename succeeds even when
-## AV scanners hold the file open without `FILE_SHARE_DELETE` (Defender
-## and Softcamp SDS both do this), so this step always advances.
-## 2. Recursively remove the renamed dir, swallowing per-file
-## access-denied. Anything still genuinely locked is left for the next
-## sweep — uv won't reuse the renamed name, so no future build collides.
-##
-## No-op on non-Windows (uv's hard-link strategy only causes this lock
-## pattern on NTFS) and when the cache directory doesn't exist.
-
-const DEAD_PREFIX := "_dead_"
-const TMP_PREFIX := ".tmp"
-
-
-## Live entrypoint. Resolves `%LOCALAPPDATA%\uv\cache\builds-v0` and runs
-## the sweep. Returns the same counts the testable `purge_directory` returns,
-## or all zeros on non-Windows / missing cache.
-static func purge_stale_builds() -> Dictionary:
- if OS.get_name() != "Windows":
- return _empty_result()
- var local_appdata := OS.get_environment("LOCALAPPDATA")
- if local_appdata.is_empty():
- return _empty_result()
- var builds_root := local_appdata.replace("\\", "/").path_join("uv/cache/builds-v0")
- return purge_directory(builds_root)
-
-
-## Pure-ish entrypoint that takes a directory path. Returns
-## `{ "scanned": int, "renamed": int, "deleted": int, "remaining": int }`.
-## - `scanned`: how many `.tmp*` subdirs we saw on entry.
-## - `renamed`: how many we successfully renamed to `_dead_*`.
-## - `deleted`: how many we then fully removed.
-## - `remaining`: how many `_dead_*` dirs are still on disk after the sweep
-## (left for the next call to retry).
-##
-## Errors are swallowed — the caller is on a server-stop hot path and
-## must not raise.
-static func purge_directory(builds_root: String) -> Dictionary:
- var result := _empty_result()
- if not DirAccess.dir_exists_absolute(builds_root):
- return result
- var dir := DirAccess.open(builds_root)
- if dir == null:
- return result
- dir.include_hidden = true
-
- ## Pass 1: collect names. Iterating + renaming in the same walk would
- ## confuse DirAccess's internal cursor on NTFS.
- var tmp_names: Array[String] = []
- var dead_names: Array[String] = []
- dir.list_dir_begin()
- var entry := dir.get_next()
- while entry != "":
- if dir.current_is_dir() and not (entry == "." or entry == ".."):
- if entry.begins_with(TMP_PREFIX):
- tmp_names.append(entry)
- elif entry.begins_with(DEAD_PREFIX):
- dead_names.append(entry)
- entry = dir.get_next()
- dir.list_dir_end()
- result.scanned = tmp_names.size()
-
- ## Pass 2: rename `.tmp*` → `_dead_.tmp*`. Rename works even on
- ## AV-locked files (Defender opens without FILE_SHARE_DELETE, but rename
- ## doesn't need delete share). Any rename failure is non-fatal.
- for name in tmp_names:
- var src := builds_root.path_join(name)
- var dst := builds_root.path_join(DEAD_PREFIX + name)
- if dir.rename(src, dst) == OK:
- result.renamed += 1
- dead_names.append(DEAD_PREFIX + name)
-
- ## Pass 3: best-effort recursive delete of every `_dead_*`, including
- ## ones left over from earlier sweeps that couldn't be cleaned then.
- for name in dead_names:
- var path := builds_root.path_join(name)
- if _remove_recursive(path):
- result.deleted += 1
-
- ## Final pass: count `_dead_*` survivors so the caller (and tests) can
- ## see how many genuinely-locked dirs we couldn't reach.
- var dir2 := DirAccess.open(builds_root)
- if dir2 != null:
- dir2.include_hidden = true
- dir2.list_dir_begin()
- var e := dir2.get_next()
- while e != "":
- if dir2.current_is_dir() and e.begins_with(DEAD_PREFIX):
- result.remaining += 1
- e = dir2.get_next()
- dir2.list_dir_end()
-
- return result
-
-
-## Recursive `rm -rf` that swallows access-denied per-file. Returns true
-## only when the target directory itself was removed.
-static func _remove_recursive(path: String) -> bool:
- var dir := DirAccess.open(path)
- if dir == null:
- ## Already gone, or unreadable — try a direct remove just in case
- ## (an empty dir handle-leak path) and report based on existence.
- DirAccess.remove_absolute(path)
- return not DirAccess.dir_exists_absolute(path)
- dir.include_hidden = true
- dir.list_dir_begin()
- var entry := dir.get_next()
- while entry != "":
- if entry == "." or entry == "..":
- entry = dir.get_next()
- continue
- var child := path.path_join(entry)
- if dir.current_is_dir():
- _remove_recursive(child)
- else:
- DirAccess.remove_absolute(child)
- entry = dir.get_next()
- dir.list_dir_end()
- ## Remove the (hopefully now empty) dir itself. If a hard-linked .pyd is
- ## still mapped by a surviving process, this fails silently and the
- ## caller sees `remaining > 0` so it can retry on the next sweep.
- DirAccess.remove_absolute(path)
- return not DirAccess.dir_exists_absolute(path)
-
-
-static func _empty_result() -> Dictionary:
- return { "scanned": 0, "renamed": 0, "deleted": 0, "remaining": 0 }
diff --git a/addons/godot_ai/utils/uv_cache_cleanup.gd.uid b/addons/godot_ai/utils/uv_cache_cleanup.gd.uid
deleted file mode 100644
index 321659d..0000000
--- a/addons/godot_ai/utils/uv_cache_cleanup.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d33ukg65qf7q0
diff --git a/addons/godot_ai/utils/uv_resolution_policy.gd b/addons/godot_ai/utils/uv_resolution_policy.gd
deleted file mode 100644
index dbaa8b1..0000000
--- a/addons/godot_ai/utils/uv_resolution_policy.gd
+++ /dev/null
@@ -1,97 +0,0 @@
-@tool
-extends RefCounted
-
-## One policy for every production uvx launch. Package identity JSON is a
-## compatibility check, not authentication: production code is resolved only
-## from the canonical PyPI index, with config files and installed tool envs
-## disabled. The explicit process-local qualification switch is the sole path
-## that leaves index selection to the qualification environment.
-
-const PathTemplate := preload("res://addons/godot_ai/clients/_path_template.gd")
-const PUBLIC_INDEX := "https://pypi.org/simple"
-const PUBLIC_FLAT_INDEX := "https://pypi.org/simple/godot-ai/"
-const QUALIFICATION_INDEX_ENV := "GODOT_AI_QUALIFICATION_PYTHON_INDEX"
-const _BASE_ARGS := [
- "--isolated",
- "--no-config",
- "--no-env-file",
- "--no-sources",
- "--no-build",
- "--index-strategy", "first-index",
- "--keyring-provider", "disabled",
-]
-
-## Known uv inputs capable of changing package source, candidate selection,
-## interpreter, or cache. Production processes spawned by Godot temporarily
-## clear them under the global process-spawn mutex; command-line policy remains
-## pinned as a second boundary and for client-owned attach launches.
-const _RESOLUTION_ENVIRONMENT := [
- "UV_INDEX", "UV_DEFAULT_INDEX", "UV_INDEX_URL", "UV_EXTRA_INDEX_URL",
- "UV_FIND_LINKS", "UV_INDEX_STRATEGY", "UV_KEYRING_PROVIDER",
- "UV_CONFIG_FILE", "UV_CONSTRAINT", "UV_BUILD_CONSTRAINT", "UV_OVERRIDE",
- "UV_NO_CONFIG", "UV_NO_SOURCES", "UV_NO_BUILD", "UV_NO_BINARY",
- "UV_NO_BINARY_PACKAGE", "UV_NO_VERIFY_HASHES", "UV_PRERELEASE",
- "UV_RESOLUTION", "UV_FORK_STRATEGY", "UV_EXCLUDE_NEWER",
- "UV_EXCLUDE_NEWER_PACKAGE", "UV_INSECURE_HOST", "UV_SYSTEM_CERTS",
- "UV_NATIVE_TLS", "UV_PYTHON", "UV_PYTHON_DOWNLOADS", "UV_PROJECT",
- "UV_WORKING_DIR", "UV_ENV_FILE", "UV_CACHE_DIR", "UV_TOOL_DIR",
- "UV_TOOL_BIN_DIR", "UV_LINK_MODE", "UV_REFRESH", "UV_REFRESH_PACKAGE",
- "UV_REINSTALL", "UV_REINSTALL_PACKAGE",
-]
-
-
-static func warm_environment() -> void:
- PathTemplate.warm_env_snapshot(PackedStringArray([QUALIFICATION_INDEX_ENV]))
-
-
-static func qualification_authorized() -> bool:
- return PathTemplate.env_lookup(QUALIFICATION_INDEX_ENV) == "1"
-
-
-static func args() -> Array[String]:
- var result: Array[String] = []
- result.assign(_BASE_ARGS)
- if not qualification_authorized():
- result.append_array([
- "--index", PUBLIC_INDEX,
- "--default-index", PUBLIC_INDEX,
- "--find-links", PUBLIC_FLAT_INDEX,
- ])
- return result
-
-
-static func is_production_command(command: Array) -> bool:
- if command.is_empty() or not str(command[0]).get_file().begins_with("uvx"):
- return false
- return (
- _option_equals(command, "--index", PUBLIC_INDEX)
- and _option_equals(command, "--default-index", PUBLIC_INDEX)
- and _option_equals(command, "--find-links", PUBLIC_FLAT_INDEX)
- )
-
-
-## Call only while the process-spawn mutex is held. Qualification commands do
-## not reach this function: their explicitly authorized UV_INDEX and credentials
-## must remain process-local and are never rendered into persisted argv.
-static func isolate_environment() -> Dictionary:
- var previous := {}
- for key in _RESOLUTION_ENVIRONMENT:
- previous[key] = {
- "present": OS.has_environment(key),
- "value": OS.get_environment(key),
- }
- OS.unset_environment(key)
- return previous
-
-
-static func restore_environment(previous: Dictionary) -> void:
- for key in previous:
- if bool(previous[key].present):
- OS.set_environment(key, str(previous[key].value))
- else:
- OS.unset_environment(key)
-
-
-static func _option_equals(command: Array, option: String, expected: String) -> bool:
- var index := command.find(option)
- return index >= 0 and index + 1 < command.size() and str(command[index + 1]) == expected
diff --git a/addons/godot_ai/utils/uv_resolution_policy.gd.uid b/addons/godot_ai/utils/uv_resolution_policy.gd.uid
deleted file mode 100644
index f729a81..0000000
--- a/addons/godot_ai/utils/uv_resolution_policy.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b3y73hby0e7j8
diff --git a/addons/godot_ai/utils/variant_serializer.gd b/addons/godot_ai/utils/variant_serializer.gd
deleted file mode 100644
index 8e17a16..0000000
--- a/addons/godot_ai/utils/variant_serializer.gd
+++ /dev/null
@@ -1,102 +0,0 @@
-@tool
-extends RefCounted
-
-## Converts Godot Variants into values that can be encoded as JSON.
-
-
-## Non-finite floats (NaN/INF) have no JSON representation: JSON.stringify
-## emits them as the bare tokens `inf`/`nan`, which are invalid JSON — the
-## server drops the whole frame and the pending request times out (#688).
-## Serialize them as null instead (the same choice web JSON.stringify makes),
-## applied uniformly across the v4 support floor — no version gate, so wire
-## output is identical on every supported engine.
-static func _safe_float(f: float) -> Variant:
- return f if is_finite(f) else null
-
-
-static func serialize(value: Variant) -> Variant:
- if value == null:
- return null
- match typeof(value):
- TYPE_BOOL, TYPE_INT, TYPE_STRING:
- return value
- TYPE_FLOAT:
- return _safe_float(value)
- TYPE_STRING_NAME:
- return str(value)
- # Integer vector types are listed separately from their float twins so
- # int components stay ints on the wire (no float coercion via
- # _safe_float's typed parameter).
- TYPE_VECTOR2I:
- return {"x": value.x, "y": value.y}
- TYPE_VECTOR2:
- return {"x": _safe_float(value.x), "y": _safe_float(value.y)}
- TYPE_VECTOR3I:
- return {"x": value.x, "y": value.y, "z": value.z}
- TYPE_VECTOR3:
- return {"x": _safe_float(value.x), "y": _safe_float(value.y), "z": _safe_float(value.z)}
- TYPE_VECTOR4I:
- return {"x": value.x, "y": value.y, "z": value.z, "w": value.w}
- TYPE_VECTOR4, TYPE_QUATERNION:
- return {
- "x": _safe_float(value.x),
- "y": _safe_float(value.y),
- "z": _safe_float(value.z),
- "w": _safe_float(value.w),
- }
- TYPE_COLOR:
- return {
- "r": _safe_float(value.r),
- "g": _safe_float(value.g),
- "b": _safe_float(value.b),
- "a": _safe_float(value.a),
- }
- TYPE_RECT2, TYPE_RECT2I, TYPE_AABB:
- return {
- "position": serialize(value.position),
- "size": serialize(value.size),
- }
- TYPE_PLANE:
- return {"normal": serialize(value.normal), "d": _safe_float(value.d)}
- TYPE_BASIS:
- return {
- "x": serialize(value.x),
- "y": serialize(value.y),
- "z": serialize(value.z),
- }
- TYPE_TRANSFORM2D:
- return {
- "x": serialize(value.x),
- "y": serialize(value.y),
- "origin": serialize(value.origin),
- }
- TYPE_TRANSFORM3D:
- return {
- "basis": serialize(value.basis),
- "origin": serialize(value.origin),
- }
- TYPE_PROJECTION:
- return {
- "x": serialize(value.x),
- "y": serialize(value.y),
- "z": serialize(value.z),
- "w": serialize(value.w),
- }
- TYPE_NODE_PATH:
- return str(value)
- TYPE_ARRAY, TYPE_PACKED_BYTE_ARRAY, TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY, TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY, TYPE_PACKED_STRING_ARRAY, TYPE_PACKED_VECTOR2_ARRAY, TYPE_PACKED_VECTOR3_ARRAY, TYPE_PACKED_VECTOR4_ARRAY, TYPE_PACKED_COLOR_ARRAY:
- var arr: Array = []
- for item in value:
- arr.append(serialize(item))
- return arr
- TYPE_DICTIONARY:
- var out := {}
- for key in value:
- out[str(key)] = serialize(value[key])
- return out
- TYPE_OBJECT:
- if value is Resource and value.resource_path:
- return value.resource_path
- return str(value)
- _:
- return str(value)
diff --git a/addons/godot_ai/utils/variant_serializer.gd.uid b/addons/godot_ai/utils/variant_serializer.gd.uid
deleted file mode 100644
index b8e0a28..0000000
--- a/addons/godot_ai/utils/variant_serializer.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cte37mtbd61n3
diff --git a/addons/godot_ai/utils/windows_port_reservation.gd b/addons/godot_ai/utils/windows_port_reservation.gd
deleted file mode 100644
index 23b4f43..0000000
--- a/addons/godot_ai/utils/windows_port_reservation.gd
+++ /dev/null
@@ -1,146 +0,0 @@
-@tool
-class_name McpWindowsPortReservation
-extends RefCounted
-
-## Detects whether Windows has reserved a TCP port range that covers the
-## plugin's server port. Hyper-V, WSL2, Docker Desktop, and Windows
-## Sandbox all grab port ranges at boot via the winnat service. When a
-## user's chosen port sits inside a reserved range, bind(2) fails with
-## WinError 10013 ("forbidden by its access permissions") rather than
-## 10048 ("address in use") — `netstat` shows nothing because no process
-## owns the port, making the failure invisible. See issue #146.
-
-const NETSH_ARGS := ["interface", "ipv4", "show", "excludedportrange", "protocol=tcp"]
-
-## Session-lifetime cache. winnat establishes its excluded-port ranges at
-## boot, so the table is effectively static for an editor session — while
-## a `netsh` spawn costs ~250ms (measured), which the old 2s TTL re-paid
-## on every startup walk (and could even re-pay *within* one walk when
-## server-command discovery ran long between the two netsh consumers).
-## Staleness risk is bounded: a mid-session winnat change (Docker/WSL2
-## start) at worst yields the same failure mode as the pre-#146 code for
-## the remainder of the session, and only if a spawn happens after it.
-static var _netsh_cache_text := ""
-static var _netsh_cache_valid := false
-static var _netsh_query_count := 0
-
-
-## Returns true if `port` falls inside a currently-reserved range on this
-## Windows host. No-op on non-Windows (returns false).
-static func is_port_excluded(port: int) -> bool:
- if OS.get_name() != "Windows":
- return false
- var cached := _get_cached_excluded_output()
- if bool(cached.get("hit", false)):
- return parse_excluded(str(cached.get("text", "")), port)
- var output: Array = []
- var exit_code := _execute_netsh_excluded_ranges(output)
- if exit_code != 0 or output.is_empty():
- return false
- var text := str(output[0])
- _store_excluded_output(text)
- return parse_excluded(text, port)
-
-
-static func _store_excluded_output(text: String) -> void:
- _netsh_cache_text = text
- _netsh_cache_valid = true
-
-
-static func _get_cached_excluded_output() -> Dictionary:
- if not _netsh_cache_valid:
- return {"hit": false, "text": ""}
- return {"hit": true, "text": _netsh_cache_text}
-
-
-static func _clear_cache_for_tests() -> void:
- _netsh_cache_text = ""
- _netsh_cache_valid = false
-
-
-static func netsh_query_count() -> int:
- return _netsh_query_count
-
-
-static func _execute_netsh_excluded_ranges(output: Array) -> int:
- _netsh_query_count += 1
- return OS.execute("netsh", NETSH_ARGS, output, true)
-
-
-## Parse the `netsh` excluded-port-range output and return true if `port`
-## sits inside any reserved range. Exposed for testing; the live check
-## uses `is_port_excluded`. Expected input format:
-##
-## Protocol tcp Port Exclusion Ranges
-##
-## Start Port End Port
-## ---------- --------
-## 80 80
-## 5040 5040
-## 8000 8099
-##
-## * - Administered port exclusions.
-static func parse_excluded(text: String, port: int) -> bool:
- return _ranges_contain(parse_excluded_ranges(text), port)
-
-
-## Parse the `netsh` excluded-port-range output once into inclusive ranges.
-static func parse_excluded_ranges(text: String) -> Array[Vector2i]:
- var ranges: Array[Vector2i] = []
- for line in text.split("\n"):
- var trimmed := line.strip_edges()
- if trimmed.is_empty() or trimmed.begins_with("-") or trimmed.begins_with("*"):
- continue
- var parts: PackedStringArray = trimmed.split(" ", false)
- if parts.size() < 2:
- continue
- if not parts[0].is_valid_int() or not parts[1].is_valid_int():
- continue
- var start_p := int(parts[0])
- var end_p := int(parts[1])
- ranges.append(Vector2i(start_p, end_p))
- return ranges
-
-
-static func _ranges_contain(ranges: Array[Vector2i], port: int) -> bool:
- for r in ranges:
- if port >= r.x and port <= r.y:
- return true
- return false
-
-
-## Return the first port in `start`..`start+span-1` that is not excluded by
-## Windows' port reservation table. Runs `netsh` once, unlike probing every
-## candidate with `is_port_excluded`, which keeps fallback port selection cheap
-## when Hyper-V / WSL2 / Docker reserve many adjacent ranges.
-static func suggest_non_excluded_port(start: int, span: int = 2048, max_port: int = 65535) -> int:
- if OS.get_name() != "Windows":
- return start
- var cached := _get_cached_excluded_output()
- if bool(cached.get("hit", false)):
- return suggest_non_excluded_port_from_output(str(cached.get("text", "")), start, span, max_port)
- var output: Array = []
- var exit_code := _execute_netsh_excluded_ranges(output)
- if exit_code != 0 or output.is_empty():
- return start
- var text := str(output[0])
- _store_excluded_output(text)
- return suggest_non_excluded_port_from_output(text, start, span, max_port)
-
-
-## Pure parser-backed helper for tests and for `suggest_non_excluded_port`.
-static func suggest_non_excluded_port_from_output(text: String, start: int, span: int = 2048, max_port: int = 65535) -> int:
- var ranges := parse_excluded_ranges(text)
- var limit := mini(start + span - 1, max_port)
- var p := start
- while p <= limit:
- var advanced := false
- for r in ranges:
- if p >= r.x and p <= r.y:
- p = r.y + 1
- advanced = true
- break
- if not advanced:
- return p
- return start
-
diff --git a/addons/godot_ai/utils/windows_port_reservation.gd.uid b/addons/godot_ai/utils/windows_port_reservation.gd.uid
deleted file mode 100644
index ffc5746..0000000
--- a/addons/godot_ai/utils/windows_port_reservation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bt7mxpjcdrobq
diff --git a/addons/godot_ai/vision_routing.gd b/addons/godot_ai/vision_routing.gd
deleted file mode 100644
index 5edc677..0000000
--- a/addons/godot_ai/vision_routing.gd
+++ /dev/null
@@ -1,1090 +0,0 @@
-@tool
-extends RefCounted
-
-## Vision Routing - route screenshot-tool images through a curated vision API.
-##
-## Models without image support (e.g. DeepSeek) cannot read the image blocks the
-## screenshot tool returns. When routing is enabled, every single-image capture is sent to a
-## vision model on a worker thread and the resulting text description is
-## returned to the AI instead:
-##
-## - Editor (non-game) screenshots are captured normally by
-## editor_handler.gd, then described on a worker thread; the reply is
-## deferred until the description is ready.
-## - Game screenshots are intercepted in mcp_debugger_plugin.gd the same way.
-## - On success the response keeps a valid but tiny 2x2 placeholder image and
-## carries the description as text metadata (`vision_description` plus a
-## `note`, which the server forwards to the model).
-## - On failure (missing key, network, API error) the original image payload
-## passes through unchanged, so the screenshot tool never breaks.
-##
-## Providers are curated: label, API dialect, endpoint shape, environment
-## variable and encrypted key slot are fixed in the PROVIDERS table. The model
-## id is NOT - it is required per provider and entered by the user (stored in
-## Editor Settings next to the key), because third-party model ids retire and
-## only the account holder gets notified. Switching providers switches to that
-## provider's entered model, so the software never guesses a model name:
-## - Groq (free tier) - OpenAI chat-completions
-## - Google Gemini (free tier, AI Studio key) - generateContent REST
-## - xAI Grok (paid) - OpenAI chat-completions
-## Both dialects are handled in this file.
-##
-## Settings live in Editor Settings (`vision_routing/enabled`,
-## `vision_routing/provider`, plus one encrypted key slot and one model-id
-## slot per provider). Keys are stored encrypted (AES-256-CBC, key derived
-## from this machine) rather than in plain text, and each provider's
-## environment variable (GROQ_API_KEY / GOOGLE_API_KEY / XAI_API_KEY) takes
-## priority over the stored key.
-##
-## UI: a "Vision Routing" section inside the Clients & Tools Settings tab.
-
-## Curated providers: label, dialect, endpoint shape, env var and key/model
-## setting slots are fixed here. The model id itself is user-entered per
-## provider (see `_resolved_model`), so provider rows carry a `model_setting`
-## slot and a `model_placeholder` suggestion instead of a baked-in id -
-## third-party model ids retire, and only the account holder gets notified
-## when one does.
-const PROVIDERS := {
- "groq": {
- "label": "Groq",
- "dialect": "openai",
- "model_setting": "vision_routing/groq_model",
- "model_placeholder": "qwen/qwen3.6-27b",
- "host": "api.groq.com",
- "port": 443,
- "path": "/openai/v1/chat/completions",
- "env": "GROQ_API_KEY",
- "setting": "vision_routing/api_key_enc",
- "placeholder": "gsk_...",
- "key_label": "Groq API key (free tier: console.groq.com)",
- "reasoning_effort": "none",
- },
- "google": {
- "label": "Google Gemini",
- "dialect": "gemini",
- "host": "generativelanguage.googleapis.com",
- "port": 443,
- "path": "/v1beta/models/{model}:generateContent",
- "model_setting": "vision_routing/google_model",
- "model_placeholder": "gemini-flash-latest",
- "env": "GOOGLE_API_KEY",
- "setting": "vision_routing/google_api_key_enc",
- "placeholder": "AIza...",
- "key_label": "Google AI Studio API key (free tier: aistudio.google.com)",
- },
- "grok": {
- "label": "xAI Grok",
- "dialect": "openai",
- "model_setting": "vision_routing/grok_model",
- "model_placeholder": "grok-4.5",
- "host": "api.x.ai",
- "port": 443,
- "path": "/v1/chat/completions",
- "env": "XAI_API_KEY",
- "setting": "vision_routing/grok_api_key_enc",
- "placeholder": "xai-...",
- "key_label": "xAI API key (api.x.ai)",
- },
-}
-const PROVIDER_ORDER := ["groq", "google", "grok"]
-
-const SETTING_ENABLED := "vision_routing/enabled"
-const SETTING_PROVIDER := "vision_routing/provider"
-const SETTING_API_KEY_ENC := "vision_routing/api_key_enc"
-const TAB_NAME := "Vision Routing"
-
-const MAX_IMAGE_EDGE := 1024
-const CONNECT_TIMEOUT_MS := 4000
-const REQUEST_TIMEOUT_MS := 8000
-## Total budget for the whole provider exchange (connect + request + body).
-## Kept under the server's 15s editor_screenshot window so the fallback
-## pass-through always lands before the server gives up on slow providers.
-const TOTAL_ROUTE_BUDGET_MS := 10000
-## Response body cap: the deadline bounds time but not bytes, and the socket
-## talks to third-party hosts - a faulty or hostile provider must not be able
-## to stream an unbounded body inside the window.
-const MAX_RESPONSE_BODY_BYTES := 1048576
-## Output-token budget shared by both dialects (OpenAI `max_tokens`,
-## Gemini `generationConfig.maxOutputTokens`) so routed descriptions
-## stay bounded.
-const MAX_OUTPUT_TOKENS := 512
-
-const _ENC_PREFIX := "v1"
-const _SALT := "vision_routing::v1::godot-ai"
-const _PLACEHOLDER_PNG := "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAALSURBVBhXY2BABwAAEgABp3qZbgAAAABJRU5ErkJggg=="
-
-## Plugin log buffer (McpLogBuffer), set by plugin.gd; null-safe.
-var log_buffer: Object = null
-
-var _active := true
-var _active_mutex := Mutex.new()
-var _pending: Dictionary = {} # request_id -> {payload, data, connection, provider_id, provider, params}
-var _threads: Dictionary = {} # request_id -> Thread ("_test" = ping thread)
-var _ping_context: Dictionary = {}
-var _ui_loading := false
-
-# UI references, kept in sync by _sync_ui_states().
-var _tab_provider: OptionButton = null
-var _tab_enable: CheckButton = null
-var _tab_key_label: Label = null
-var _tab_key_hint: Label = null
-var _tab_key_edit: LineEdit = null
-var _tab_model_label: Label = null
-var _tab_model_hint: Label = null
-var _tab_model_edit: LineEdit = null
-var _tab_status: Label = null
-var _tab_test_button: Button = null
-
-
-func _is_active() -> bool:
- _active_mutex.lock()
- var active := _active
- _active_mutex.unlock()
- return active
-
-
-func _set_active(active: bool) -> void:
- _active_mutex.lock()
- _active = active
- _active_mutex.unlock()
-
-
-## Plugin teardown: stop workers and join in-flight threads so Godot never
-## destroys a Thread mid-execution during a plugin reload.
-func shutdown() -> void:
- _set_active(false)
- ## Workers poll the synchronized active flag between HTTP polls, so this
- ## returns within one
- ## poll interval (~50ms) unless a request is mid-flight in the OS; worst
- ## case is a connect/request timeout.
- for rid in _threads:
- var thread: Thread = _threads[rid]
- if thread != null and thread.is_started():
- thread.wait_to_finish()
- _threads.clear()
- _pending.clear()
- _ping_context.clear()
- _tab_provider = null
- _tab_enable = null
- _tab_key_label = null
- _tab_key_hint = null
- _tab_key_edit = null
- _tab_status = null
- _tab_test_button = null
- _tab_model_label = null
- _tab_model_hint = null
- _tab_model_edit = null
-
-
-## Drain completed Thread return values on the main thread. Workers never
-## enqueue old-script Callables: shutdown can set the stop flag, join every
-## worker, and discard its result with no deferred callback surviving into an
-## exact-tree update or plugin reload.
-func poll_completed() -> void:
- if not _is_active():
- return
- for rid_value in _threads.keys():
- var rid := str(rid_value)
- var thread: Thread = _threads.get(rid)
- if thread == null or not thread.is_started():
- _threads.erase(rid)
- continue
- if thread.is_alive():
- continue
- var outcome: Variant = thread.wait_to_finish()
- _threads.erase(rid)
- if not _is_active():
- return
- if not outcome is Dictionary:
- if rid == "_test":
- _finish_ping_result({"ok": false, "error": "worker returned an invalid result"})
- else:
- _on_route_complete(rid, {"desc": "", "error": "worker returned an invalid result"})
- continue
- match str(outcome.get("kind", "")):
- "route":
- _on_route_complete(str(outcome.get("request_id", rid)), outcome.get("result"))
- "ping":
- _finish_ping_result(outcome.get("result", {}))
- _:
- _pending.erase(rid)
-
-
-# --- routing ------------------------------------------------------------------
-
-## Entry point called from editor_handler.take_screenshot when routing is
-## enabled. Runs the real capture (via `original`), then routes the image
-## through the selected provider's vision API on a worker thread. Returns the deferred-response sentinel
-## when the worker owns the reply, otherwise the capture result unchanged.
-func route_editor_screenshot(params: Dictionary, original: Callable, connection: Object) -> Dictionary:
- var rid := str(params.get("_request_id", ""))
- if rid.is_empty():
- ## No deferred channel (e.g. batch_execute / dispatch_direct) - keep
- ## the original synchronous result untouched.
- return original.call(params)
- var result := original.call(params)
- if not (result is Dictionary) or not result.has("data"):
- ## Deferred capture (source="game"): the frame arrives later through
- ## route_game_payload. Stash the caller params (e.g. user_prompt) by
- ## request id so the routed description keeps the agent's context.
- if result is Dictionary and result.get("_deferred", false):
- _pending[rid] = {"params": params}
- return result
- var data: Variant = result["data"]
- if not (data is Dictionary) or not data.has("image_base64"):
- return result
- if _start_route(rid, str(params.get("source", "viewport")), result, data, connection, params):
- ## Keep the deferred ledger inside the server's 15s editor_screenshot
- ## window (the worker itself is capped at TOTAL_ROUTE_BUDGET_MS), so
- ## a hung provider surfaces as a clean plugin timeout, not a
- ## server-side abort.
- return {"_deferred": true, "_deferred_timeout_ms": 13000}
- return result
-
-
-## Entry point called from McpDebuggerPlugin._on_screenshot_response before
-## the frame is sent. Returns true when a worker owns the reply (the caller
-## must NOT send the payload itself); false means pass through unchanged.
-func route_game_payload(connection: Object, rid: String, payload: Dictionary) -> bool:
- var data: Variant = payload.get("data")
- if not (data is Dictionary) or not data.has("image_base64"):
- ## Not a routeable frame - drop any params stashed by
- ## route_editor_screenshot for this request id.
- _pending.erase(rid)
- return false
- ## The editor handler stashed the caller params (user_prompt etc.) by
- ## request id when it deferred the capture; hand them to the prompt
- ## builder. Absent (older helper, direct call) = generic prompt.
- var params: Dictionary = _pending.get(rid, {}).get("params", {})
- return _start_route(rid, "game", payload, data, connection, params)
-
-
-## Returns true when a worker owns the reply; false means the caller should
-## pass the original payload through unchanged.
-func _start_route(rid: String, source: String, payload: Dictionary, data: Dictionary, connection: Object, params: Dictionary) -> bool:
- if not _is_active():
- return false
- var provider_id := _active_provider_id()
- var base_provider: Dictionary = PROVIDERS.get(provider_id, PROVIDERS["groq"])
- ## Worker-thread snapshot: the entered model id is resolved into the
- ## provider dict here, once, so the thread sees a stable copy and
- ## routed_via can report the exact model that was pinged.
- var provider := _provider_with_model(provider_id)
- var api_key := _resolved_api_key(provider_id)
- if api_key.is_empty():
- _log("vision routing: no API key for %s (set %s or paste one in the Vision Routing section) - screenshot %s passed through" % [provider_id, base_provider.get("env", ""), rid])
- _pending.erase(rid)
- return false
- if str(provider.get("model", "")).is_empty():
- ## Empty model behaves exactly like empty key: routing declines, the
- ## image passes through, and the log line says what is missing.
- _log("vision routing: no model id set for %s - screenshot %s passed through (set a model id in the Vision Routing section)" % [provider_id, rid])
- _pending.erase(rid)
- return false
- _pending[rid] = {"payload": payload, "data": data, "connection": connection, "provider_id": provider_id, "provider": provider, "params": params}
- var prompt := _build_prompt(params)
- var thread := Thread.new()
- var start_err := thread.start(_route_worker.bind(provider, str(data.get("image_base64", "")), prompt, api_key, rid))
- if start_err != OK:
- _pending.erase(rid)
- _log("vision routing: could not start worker for %s: %s - screenshot %s passed through" % [rid, error_string(start_err), source])
- return false
- _threads[rid] = thread
- _log("vision routing: routing %s screenshot %s (%d b64 chars) via %s (%s)" % [source, rid, str(data.get("image_base64", "")).length(), base_provider.get("label", provider_id), provider.get("model", "")])
- return true
-
-
-func _on_route_complete(rid: String, result: Variant) -> void:
- if not _is_active():
- _pending.erase(rid)
- return
- var entry: Dictionary = _pending.get(rid, {})
- _pending.erase(rid)
- if entry.is_empty():
- return
- var connection: Object = entry.get("connection")
- if connection == null or not is_instance_valid(connection):
- return
- var provider_id := str(entry.get("provider_id", "groq"))
- ## Provider snapshot taken at route start (with the entered model id);
- ## falls back to a fresh resolve if absent (direct test callers).
- var provider: Dictionary = entry.get("provider", {})
- if provider.is_empty():
- provider = _provider_with_model(provider_id)
- ## Workers return {"desc": ..., "error": ...}; plain strings are accepted
- ## for backwards compatibility (tests / older callers).
- var description_str := ""
- var error_str := ""
- if result is Dictionary:
- description_str = str(result.get("desc", ""))
- error_str = str(result.get("error", ""))
- elif result != null:
- description_str = str(result)
- if description_str.is_empty():
- if error_str.is_empty():
- error_str = "unknown error"
- _log("vision routing: %s failed for %s (%s) - returning original image with failure note" % [provider_id, rid, error_str])
- ## Append a short templated reason to the note so text-only agents
- ## (who otherwise just receive a useless image block) learn the
- ## feature is down and why.
- var failed_data: Dictionary = entry.get("data", {}).duplicate()
- var failure_note := "Vision routing unavailable (%s): %s" % [provider_id, error_str]
- var existing_note := str(failed_data.get("note", ""))
- failed_data["note"] = (existing_note + " | " if not existing_note.is_empty() else "") + failure_note
- _pass_through(connection, rid, {"data": failed_data})
- return
- var data: Dictionary = entry.get("data", {}).duplicate()
- var routed_via := "%s:%s" % [provider_id, provider.get("model", "")]
- ## The server forwards a fixed whitelist of metadata keys into the text
- ## result; `note` is the free-form one, so a self-attributing description
- ## rides there (plus an explicit `vision_description` key) so text-only
- ## models can read it. The label keeps provider text from reading as if
- ## the plugin said it. The image is replaced by a valid 2x2 placeholder
- ## so the payload stays well-formed.
- data["vision_description"] = description_str
- data["routed_via"] = routed_via
- var original_note := str(data.get("note", ""))
- var labeled := "Vision description (%s): %s" % [routed_via, description_str]
- data["note"] = (original_note + " | " if not original_note.is_empty() else "") + labeled
- data["image_base64"] = _PLACEHOLDER_PNG
- data["format"] = "png"
- _log("vision routing: description ready for %s (%d chars)" % [rid, description_str.length()])
- _pass_through(connection, rid, {"data": data})
-
-
-func _pass_through(connection: Object, rid: String, payload: Dictionary) -> void:
- if connection != null and is_instance_valid(connection):
- connection.send_deferred_response(rid, payload)
-
-
-func _build_prompt(params: Dictionary) -> String:
- var lines := PackedStringArray([
- "You are the vision module of a text-only AI agent driving the Godot editor through MCP.",
- "Describe this screenshot so the agent can act without seeing it. Report:",
- "- What is shown: Godot editor viewport, game window, 2D/3D scene, UI panel, dialog, or other.",
- "- Objects/nodes: what they are, position, color, size, and any labels or text (quote text exactly).",
- "- UI text: menus, buttons, error dialogs, console output, warnings, line numbers.",
- "- State: selected node outlines, gizmos, play/stop status, panels that are open.",
- "- Problems: errors, red highlights, missing textures, black screens, glitches, stretching.",
- "Be concise (under 200 words), factual, and use exact quotes instead of paraphrase. Do not give advice.",
- ])
- var user_prompt := str(params.get("user_prompt", ""))
- if not user_prompt.is_empty():
- lines.append("Context from the agent that requested this screenshot: %s" % user_prompt)
- return "\n".join(lines)
-
-
-# --- routing worker (thread) ---------------------------------------------------
-
-func _route_worker(provider: Dictionary, image_b64: String, prompt: String, api_key: String, rid: String) -> Dictionary:
- var result := _describe_blocking(provider, image_b64, prompt, api_key)
- return {"kind": "route", "request_id": rid, "result": result}
-
-
-## Returns {"desc": String, "error": String}; "desc" is empty on failure and
-## "error" carries the reason. Runs on a worker thread, so it never writes
-## shared state - everything it needs is passed in and returned. `provider`
-## is the route-start snapshot (includes the entered model id).
-func _describe_blocking(provider: Dictionary, image_b64: String, prompt: String, api_key: String) -> Dictionary:
- var b64 := _downscale_image_if_needed(image_b64)
- var body := _build_request_body(provider, prompt, b64)
- var headers := _build_headers(provider, api_key)
- var response := _http_post_json(str(provider.get("host", "")), int(provider.get("port", 443)), _resolve_path(provider), headers, body)
- return _parse_description(provider, response)
-
-
-## Provider paths may carry a {model} placeholder (Gemini's endpoint embeds
-## the model id); substitute the entered model id verbatim.
-func _resolve_path(provider: Dictionary) -> String:
- return str(provider.get("path", "")).replace("{model}", str(provider.get("model", "")))
-
-
-func _build_request_body(provider: Dictionary, prompt: String, image_b64: String) -> String:
- var model := str(provider.get("model", ""))
- if str(provider.get("dialect", "")) == "gemini":
- return JSON.stringify({
- "contents": [{
- "role": "user",
- "parts": [
- {"text": prompt},
- {"inline_data": {"mime_type": "image/png", "data": image_b64}},
- ],
- }],
- "generationConfig": {"maxOutputTokens": MAX_OUTPUT_TOKENS},
- })
- var payload := {
- "model": model,
- "messages": [{
- "role": "user",
- "content": [
- {"type": "text", "text": prompt},
- {"type": "image_url", "image_url": {"url": "data:image/png;base64," + image_b64}},
- ],
- }],
- "max_tokens": MAX_OUTPUT_TOKENS,
- "temperature": 0.2,
- }
- if provider.has("reasoning_effort"):
- payload["reasoning_effort"] = provider["reasoning_effort"]
- return JSON.stringify(payload)
-
-
-func _build_headers(provider: Dictionary, api_key: String) -> PackedStringArray:
- if str(provider.get("dialect", "")) == "gemini":
- return PackedStringArray([
- "Content-Type: application/json",
- "x-goog-api-key: %s" % api_key,
- ])
- return PackedStringArray([
- "Content-Type: application/json",
- "Authorization: Bearer %s" % api_key,
- ])
-
-
-func _parse_description(provider: Dictionary, response: Dictionary) -> Dictionary:
- var code: int = int(response.get("code", 0))
- var label := str(provider.get("label", str(provider.get("model", "?"))))
- if code == 0:
- return {"desc": "", "error": str(response.get("error", "HTTP request failed"))}
- if code != 200:
- return {"desc": "", "error": "%s HTTP %d: %s" % [label, code, _body_snippet(str(response.get("text", "")))]}
- var parsed: Variant = JSON.parse_string(str(response.get("text", "")))
- if not (parsed is Dictionary):
- return {"desc": "", "error": "%s response was not JSON: %s" % [label, _body_snippet(str(response.get("text", "")))]}
- if str(provider.get("dialect", "")) == "gemini":
- return _parse_gemini(parsed, label, response)
- return _parse_openai(parsed, label, response)
-
-
-func _parse_openai(parsed: Dictionary, label: String, response: Dictionary) -> Dictionary:
- var choices: Variant = parsed.get("choices")
- if not (choices is Array) or choices.is_empty():
- return {"desc": "", "error": "%s response had no choices: %s" % [label, _body_snippet(str(response.get("text", "")))]}
- if not (choices[0] is Dictionary):
- return {"desc": "", "error": "%s response choice was not an object: %s" % [label, _body_snippet(str(response.get("text", "")))]}
- var message: Variant = choices[0].get("message", {})
- if not (message is Dictionary):
- return {"desc": "", "error": "%s response message was not an object: %s" % [label, _body_snippet(str(response.get("text", "")))]}
- var content: Variant = message.get("content", "")
- if content == null:
- return {"desc": "", "error": "%s response content was null: %s" % [label, _body_snippet(str(response.get("text", "")))]}
- return {"desc": _strip_think(str(content)), "error": ""}
-
-
-func _parse_gemini(parsed: Dictionary, label: String, response: Dictionary) -> Dictionary:
- var candidates: Variant = parsed.get("candidates")
- if not (candidates is Array) or candidates.is_empty():
- var reason := ""
- var feedback: Variant = parsed.get("promptFeedback")
- if feedback is Dictionary:
- reason = str(feedback.get("blockReason", ""))
- var reason_part := ""
- if not reason.is_empty():
- reason_part = " (blocked: %s)" % reason
- return {"desc": "", "error": "%s response had no candidates%s: %s" % [label, reason_part, _body_snippet(str(response.get("text", "")))]}
- if not (candidates[0] is Dictionary):
- return {"desc": "", "error": "%s response candidate was not an object: %s" % [label, _body_snippet(str(response.get("text", "")))]}
- var content: Variant = candidates[0].get("content", {})
- if not (content is Dictionary):
- return {"desc": "", "error": "%s response content was missing: %s" % [label, _body_snippet(str(response.get("text", "")))]}
- var parts: Variant = content.get("parts")
- if not (parts is Array) or parts.is_empty():
- return {"desc": "", "error": "%s response had no parts: %s" % [label, _body_snippet(str(response.get("text", "")))]}
- var texts := PackedStringArray()
- for part in parts:
- if part is Dictionary:
- var part_text := str(part.get("text", ""))
- if not part_text.is_empty():
- texts.append(part_text)
- if texts.is_empty():
- return {"desc": "", "error": "%s response parts had no text: %s" % [label, _body_snippet(str(response.get("text", "")))]}
- return {"desc": _strip_think("\n".join(texts)), "error": ""}
-
-
-func _strip_think(text: String) -> String:
- var out := text.strip_edges()
- ## Reasoning models may wrap their answer in ... blocks.
- var think_end := out.rfind("")
- if think_end != -1:
- out = out.substr(think_end + "".length()).strip_edges()
- return out
-
-
-## Minimal vision call used by the "Test connection" button. Sends the same
-## image-bearing body shape as real routing (with the 2x2 placeholder PNG),
-## so one click validates key + model existence + image capability - a
-## text-only or retired model id fails loudly here instead of at screenshot
-## time. `provider` is the route-style snapshot (includes the model id).
-func _ping_blocking(provider: Dictionary, api_key: String) -> Dictionary:
- var body := _build_request_body(provider, "Reply with exactly: OK", _PLACEHOLDER_PNG)
- var response := _http_post_json(str(provider.get("host", "")), int(provider.get("port", 443)), _resolve_path(provider), _build_headers(provider, api_key), body)
- var code: int = int(response.get("code", 0))
- if code == 200:
- return {"ok": true, "error": ""}
- if code == 0:
- return {"ok": false, "error": str(response.get("error", "request failed"))}
- return {"ok": false, "error": "%s HTTP %d: %s" % [provider.get("label", "provider"), code, _body_snippet(str(response.get("text", "")))]}
-
-
-func _http_post_json(host: String, port: int, path: String, headers: PackedStringArray, body: String) -> Dictionary:
- if host.is_empty() or body.is_empty():
- return {"code": 0, "error": "invalid request (empty host or body)"}
- var http := HTTPClient.new()
- var connect_err := http.connect_to_host(host, port, TLSOptions.client())
- if connect_err != OK:
- http.close()
- return {"code": 0, "error": "connect_to_host failed: %s" % error_string(connect_err)}
- ## One total deadline for the whole exchange (see TOTAL_ROUTE_BUDGET_MS),
- ## so connect + request + body can never exceed it.
- var budget_deadline := Time.get_ticks_msec() + TOTAL_ROUTE_BUDGET_MS
- var deadline := mini(Time.get_ticks_msec() + CONNECT_TIMEOUT_MS, budget_deadline)
- var first_poll := true
- var connected := false
- var last_status := -1
- while Time.get_ticks_msec() < deadline:
- if not _is_active():
- http.close()
- return {"code": 0, "error": "aborted (plugin teardown)"}
- http.poll()
- var status := http.get_status()
- last_status = status
- if status == HTTPClient.STATUS_CONNECTED:
- connected = true
- break
- if status == HTTPClient.STATUS_DISCONNECTED and not first_poll:
- break
- first_poll = false
- OS.delay_msec(50)
- if not connected:
- http.close()
- return {"code": 0, "error": "could not connect (status %d)" % last_status}
- if http.request(HTTPClient.METHOD_POST, path, headers, body) != OK:
- http.close()
- return {"code": 0, "error": "request() failed"}
- deadline = mini(Time.get_ticks_msec() + REQUEST_TIMEOUT_MS, budget_deadline)
- var timed_out := false
- var status_at_timeout := -1
- while http.get_status() == HTTPClient.STATUS_REQUESTING:
- if not _is_active():
- http.close()
- return {"code": 0, "error": "aborted (plugin teardown)"}
- http.poll()
- if Time.get_ticks_msec() > deadline:
- timed_out = true
- status_at_timeout = http.get_status()
- break
- OS.delay_msec(50)
- if timed_out:
- http.close()
- return {"code": 0, "error": "request timed out (status %d)" % status_at_timeout}
- if not http.has_response():
- var st := http.get_status()
- http.close()
- return {"code": 0, "error": "no HTTP response (status %d)" % st}
- var code := http.get_response_code()
- var chunks := PackedByteArray()
- var body_deadline := mini(Time.get_ticks_msec() + REQUEST_TIMEOUT_MS, budget_deadline)
- while http.get_status() == HTTPClient.STATUS_BODY:
- if not _is_active():
- http.close()
- return {"code": 0, "error": "aborted (plugin teardown)"}
- chunks.append_array(http.read_response_body_chunk())
- if chunks.size() > MAX_RESPONSE_BODY_BYTES:
- http.close()
- return {"code": 0, "error": "response body exceeded %d bytes" % MAX_RESPONSE_BODY_BYTES}
- http.poll()
- if Time.get_ticks_msec() > body_deadline:
- break
- OS.delay_msec(10)
- http.close()
- return {"code": code, "text": chunks.get_string_from_utf8()}
-
-
-func _body_snippet(text: String) -> String:
- if text.is_empty():
- return "(empty body)"
- if text.length() > 300:
- return text.substr(0, 300) + "..."
- return text
-
-
-func _downscale_image_if_needed(image_b64: String) -> String:
- if image_b64.is_empty():
- return image_b64
- var raw := Marshalls.base64_to_raw(image_b64)
- if raw.is_empty():
- return image_b64
- var image := Image.new()
- if image.load_png_from_buffer(raw) != OK:
- return image_b64
- var width := image.get_width()
- var height := image.get_height()
- if width <= MAX_IMAGE_EDGE and height <= MAX_IMAGE_EDGE:
- return image_b64
- if width >= height:
- height = maxi(1, int(round(height * MAX_IMAGE_EDGE / float(width))))
- width = MAX_IMAGE_EDGE
- else:
- width = maxi(1, int(round(width * MAX_IMAGE_EDGE / float(height))))
- height = MAX_IMAGE_EDGE
- image.resize(width, height, Image.INTERPOLATE_BILINEAR)
- var out := image.save_png_to_buffer()
- if out.is_empty():
- return image_b64
- return Marshalls.raw_to_base64(out)
-
-
-func _ping_worker(provider: Dictionary, api_key: String) -> Dictionary:
- var result := _ping_blocking(provider, api_key)
- return {"kind": "ping", "result": result}
-
-
-func _finish_ping_result(result: Dictionary) -> void:
- var provider: Dictionary = _ping_context.get("provider", {})
- var status_label: Label = _ping_context.get("status_label")
- var key_source := str(_ping_context.get("key_source", "stored key"))
- _ping_context.clear()
- if _tab_test_button != null and is_instance_valid(_tab_test_button):
- _tab_test_button.disabled = false
- if status_label != null and is_instance_valid(status_label):
- var label := str(provider.get("label", "provider"))
- if result.get("ok", false):
- status_label.text = "OK - %s responded (%s)." % [label, key_source]
- else:
- status_label.text = "FAILED - %s (%s)." % [result.get("error", "unknown error"), key_source]
- _log("vision routing: ping result: %s" % status_label.text)
-
-
-# --- settings / key storage ---------------------------------------------------
-
-func _settings() -> EditorSettings:
- return EditorInterface.get_editor_settings()
-
-
-func is_routing_enabled() -> bool:
- var es := _settings()
- if es == null or not es.has_setting(SETTING_ENABLED):
- return false
- var value = es.get_setting(SETTING_ENABLED)
- return value != null and value
-
-
-func _active_provider_id() -> String:
- var es := _settings()
- if es == null:
- return "groq"
- var stored := ""
- if es.has_setting(SETTING_PROVIDER):
- stored = str(es.get_setting(SETTING_PROVIDER))
- if stored.is_empty() or not PROVIDERS.has(stored):
- return "groq"
- return stored
-
-
-func _provider_setting(provider_id: String) -> String:
- return str(PROVIDERS.get(provider_id, PROVIDERS["groq"]).get("setting", SETTING_API_KEY_ENC))
-
-
-func _resolved_api_key(provider_id: String) -> String:
- ## Environment variable takes priority over the stored (encrypted) key.
- var provider: Dictionary = PROVIDERS.get(provider_id, PROVIDERS["groq"])
- var env_key := OS.get_environment(str(provider.get("env", "")))
- if not env_key.is_empty():
- return env_key
- var es := _settings()
- if es == null:
- return ""
- var setting := _provider_setting(provider_id)
- var blob := ""
- if es.has_setting(setting):
- blob = str(es.get_setting(setting))
- if blob.is_empty():
- return ""
- return _decrypt(blob)
-
-
-func _decrypted_key(provider_id: String) -> String:
- var es := _settings()
- if es == null:
- return ""
- var setting := _provider_setting(provider_id)
- if not es.has_setting(setting):
- return ""
- return _decrypt(str(es.get_setting(setting)))
-
-
-func set_api_key(provider_id: String, plain: String) -> void:
- var es := _settings()
- if es == null:
- return
- if plain.is_empty():
- es.set_setting(_provider_setting(provider_id), "")
- return
- var blob := _encrypt(plain)
- if blob.is_empty():
- _log("vision routing: cannot store %s key - this machine reports no unique id; set %s instead" % [provider_id, PROVIDERS.get(provider_id, PROVIDERS["groq"]).get("env", "")])
- return
- es.set_setting(_provider_setting(provider_id), blob)
-
-
-## The model id is stored per provider next to the key (a plain Editor
-## Setting - it is not a secret). Empty clears the slot.
-func set_model_id(provider_id: String, model: String) -> void:
- var es := _settings()
- if es == null:
- return
- es.set_setting(_model_setting(provider_id), model.strip_edges())
-
-
-func _model_setting(provider_id: String) -> String:
- return str(PROVIDERS.get(provider_id, PROVIDERS["groq"]).get("model_setting", ""))
-
-
-func _resolved_model(provider_id: String) -> String:
- var es := _settings()
- if es == null:
- return ""
- var setting := _model_setting(provider_id)
- if not es.has_setting(setting):
- return ""
- return str(es.get_setting(setting)).strip_edges()
-
-
-## Route-start snapshot: the curated provider table plus the user's entered
-## model id. Used by the worker thread and by _on_route_complete for the
-## routed_via label, so it always reports the model that was actually pinged.
-func _provider_with_model(provider_id: String) -> Dictionary:
- var provider: Dictionary = PROVIDERS.get(provider_id, PROVIDERS["groq"]).duplicate()
- provider["model"] = _resolved_model(provider_id)
- return provider
-
-
-## Machine-derived key: not a password, but enough that a casually-opened
-## editor_settings-4.tres does not reveal the key in plain text. Separate
-## domain tags give AES and HMAC independent keys so neither reuses the
-## other's bytes.
-func _derive_key(tag: String) -> PackedByteArray:
- var parts := PackedStringArray([
- OS.get_unique_id(),
- OS.get_environment("USERNAME"),
- OS.get_environment("USERPROFILE"),
- OS.get_environment("USER"),
- OS.get_environment("HOME"),
- OS.get_name(),
- _SALT,
- tag,
- ])
- var ctx := HashingContext.new()
- ctx.start(HashingContext.HASH_SHA256)
- ctx.update(("|".join(parts)).to_utf8_buffer())
- return ctx.finish()
-
-
-func _encrypt(plain: String) -> String:
- if OS.get_unique_id().is_empty():
- ## Without a machine id the key would be near-constant across users
- ## on the same machine - refuse, and let callers fall back to the
- ## provider's environment variable.
- return ""
- var aes_key := _derive_key("aes")
- var mac_key := _derive_key("mac")
- var iv := Crypto.new().generate_random_bytes(16)
- var raw := plain.to_utf8_buffer()
- ## PKCS7 padding.
- var pad := 16 - (raw.size() % 16)
- var padded := raw.duplicate()
- for i in pad:
- padded.append(pad)
- var aes := AESContext.new()
- aes.start(AESContext.MODE_CBC_ENCRYPT, aes_key, iv)
- var cipher := aes.update(padded)
- aes.finish()
- var hmac := HMACContext.new()
- hmac.start(HashingContext.HASH_SHA256, mac_key)
- hmac.update(iv)
- hmac.update(cipher)
- var mac := hmac.finish()
- return "%s:%s:%s:%s" % [_ENC_PREFIX, Marshalls.raw_to_base64(iv), Marshalls.raw_to_base64(mac), Marshalls.raw_to_base64(cipher)]
-
-
-func _decrypt(blob: String) -> String:
- var parts := blob.split(":")
- if parts.size() != 4 or parts[0] != _ENC_PREFIX:
- return ""
- var iv := Marshalls.base64_to_raw(parts[1])
- var mac := Marshalls.base64_to_raw(parts[2])
- var cipher := Marshalls.base64_to_raw(parts[3])
- if iv.size() != 16 or cipher.is_empty() or cipher.size() % 16 != 0:
- return ""
- var aes_key := _derive_key("aes")
- var mac_key := _derive_key("mac")
- var hmac := HMACContext.new()
- hmac.start(HashingContext.HASH_SHA256, mac_key)
- hmac.update(iv)
- hmac.update(cipher)
- var expected := hmac.finish()
- if expected != mac:
- return ""
- var aes := AESContext.new()
- aes.start(AESContext.MODE_CBC_DECRYPT, aes_key, iv)
- var padded := aes.update(cipher)
- aes.finish()
- if padded.is_empty():
- return ""
- var pad := int(padded[padded.size() - 1])
- if pad < 1 or pad > 16 or pad > padded.size():
- return ""
- var raw := padded.slice(0, padded.size() - pad)
- return raw.get_string_from_utf8()
-
-
-# --- UI: tab in Clients & Tools -------------------------------------------------
-
-## Builds the "Vision Routing" section inside the Clients & Tools Settings
-## tab. Called by mcp_dock._build_settings_tab; `refresh_ui()` re-syncs the
-## controls from Editor Settings each time the window opens.
-func build_section(parent: VBoxContainer) -> void:
- var box := VBoxContainer.new()
- box.add_theme_constant_override("separation", 8)
- parent.add_child(box)
-
- var header := Label.new()
- header.text = "Vision Routing"
- header.add_theme_font_size_override("font_size", 18)
- box.add_child(header)
-
- var provider_row := HBoxContainer.new()
- var provider_label := Label.new()
- provider_label.text = "Provider"
- provider_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- provider_row.add_child(provider_label)
- _tab_provider = OptionButton.new()
- _tab_provider.tooltip_text = "Which vision provider routes the screenshots. The model id is required per provider and is yours to maintain."
- var active_provider := _active_provider_id()
- for provider_index in PROVIDER_ORDER.size():
- var provider_item: Dictionary = PROVIDERS[PROVIDER_ORDER[provider_index]]
- _tab_provider.add_item(str(provider_item.get("label", PROVIDER_ORDER[provider_index])), provider_index)
- if PROVIDER_ORDER[provider_index] == active_provider:
- _tab_provider.select(provider_index)
- _tab_provider.item_selected.connect(_on_provider_changed)
- provider_row.add_child(_tab_provider)
- box.add_child(provider_row)
-
- var enable_row := HBoxContainer.new()
- var enable_label := Label.new()
- enable_label.text = "Enable routing"
- enable_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- enable_row.add_child(enable_label)
- _tab_enable = CheckButton.new()
- _tab_enable.button_pressed = is_routing_enabled()
- _tab_enable.toggled.connect(_on_enable_toggled)
- enable_row.add_child(_tab_enable)
- box.add_child(enable_row)
-
- var description := Label.new()
- description.text = (
- "When enabled, every single-image screenshot the AI model takes through "
- + "the godot-ai screenshot tool is sent to the selected provider's vision model (see "
- + "the Provider dropdown) for a text description. The description is "
- + "returned to the AI instead of the raw image, so models without image "
- + "support (e.g. DeepSeek) can still \"see\" the editor and game. When "
- + "the connected model analyzes images itself, switch this off here so "
- + "screenshots pass through unchanged."
- )
- description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- description.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- box.add_child(description)
-
- _tab_key_label = Label.new()
- box.add_child(_tab_key_label)
-
- var key_row := HBoxContainer.new()
- _tab_key_edit = LineEdit.new()
- _tab_key_edit.secret = true
- _tab_key_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- ## Persist on commit (Enter / focus loss) instead of per keystroke, so
- ## pasting a key does not re-encrypt and rewrite Editor Settings dozens
- ## of times.
- _tab_key_edit.text_submitted.connect(_on_key_committed)
- _tab_key_edit.focus_exited.connect(func() -> void: _on_key_committed(_tab_key_edit.text))
- key_row.add_child(_tab_key_edit)
- var show_button := CheckButton.new()
- show_button.tooltip_text = "Show / hide key"
- show_button.toggled.connect(func(show: bool) -> void: _tab_key_edit.secret = not show)
- key_row.add_child(show_button)
- box.add_child(key_row)
-
- _tab_key_hint = Label.new()
- _tab_key_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- _tab_key_hint.add_theme_color_override("font_color", Color(0.55, 0.55, 0.55))
- box.add_child(_tab_key_hint)
-
- _tab_model_label = Label.new()
- box.add_child(_tab_model_label)
-
- var model_row := HBoxContainer.new()
- _tab_model_edit = LineEdit.new()
- _tab_model_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- ## Persist on commit (Enter / focus loss), same pattern as the key field.
- _tab_model_edit.text_submitted.connect(_on_model_committed)
- _tab_model_edit.focus_exited.connect(func() -> void: _on_model_committed(_tab_model_edit.text))
- model_row.add_child(_tab_model_edit)
- box.add_child(model_row)
-
- _tab_model_hint = Label.new()
- _tab_model_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- _tab_model_hint.add_theme_color_override("font_color", Color(0.55, 0.55, 0.55))
- box.add_child(_tab_model_hint)
-
- var test_row := HBoxContainer.new()
- var test_button := Button.new()
- test_button.text = "Test connection"
- test_button.pressed.connect(_on_test_connection)
- _tab_test_button = test_button
- test_row.add_child(test_button)
- _tab_status = Label.new()
- _tab_status.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- _tab_status.add_theme_color_override("font_color", Color(0.55, 0.55, 0.55))
- test_row.add_child(_tab_status)
- box.add_child(test_row)
-
- refresh_ui()
-
-
-## Re-syncs every control from Editor Settings. Called after the section is
-## built and again by mcp_dock each time the Clients & Tools window opens,
-## so a change made in another editor instance (or a hand-edit of
-## editor_settings-4.tres) is reflected.
-func refresh_ui() -> void:
- _sync_ui_states()
- _sync_provider_ui()
- _ui_loading = true
- if _tab_key_edit != null and is_instance_valid(_tab_key_edit):
- _tab_key_edit.text = _decrypted_key(_active_provider_id())
- if _tab_model_edit != null and is_instance_valid(_tab_model_edit):
- _tab_model_edit.text = _resolved_model(_active_provider_id())
- _ui_loading = false
-
-
-func _on_key_committed(_new_text: String) -> void:
- if _ui_loading:
- return
- set_api_key(_active_provider_id(), _tab_key_edit.text)
-
-
-func _on_model_committed(_new_text: String) -> void:
- if _ui_loading:
- return
- set_model_id(_active_provider_id(), _tab_model_edit.text)
-
-
-func _on_provider_changed(index: int) -> void:
- if index < 0 or index >= PROVIDER_ORDER.size():
- return
- var provider_id: String = PROVIDER_ORDER[index]
- var es := _settings()
- if es != null:
- es.set_setting(SETTING_PROVIDER, provider_id)
- _ui_loading = true
- if _tab_key_edit != null and is_instance_valid(_tab_key_edit):
- _tab_key_edit.text = _decrypted_key(provider_id)
- if _tab_model_edit != null and is_instance_valid(_tab_model_edit):
- _tab_model_edit.text = _resolved_model(provider_id)
- _ui_loading = false
- _sync_provider_ui()
- _log("vision routing: provider changed to %s (%s)" % [provider_id, PROVIDERS[provider_id].get("label", provider_id)])
-
-
-func _sync_provider_ui() -> void:
- var provider_id := _active_provider_id()
- var provider: Dictionary = PROVIDERS.get(provider_id, PROVIDERS["groq"])
- if _tab_provider != null and is_instance_valid(_tab_provider):
- var provider_index := PROVIDER_ORDER.find(provider_id)
- if provider_index != -1 and _tab_provider.selected != provider_index:
- _tab_provider.select(provider_index)
- if _tab_key_label != null and is_instance_valid(_tab_key_label):
- _tab_key_label.text = str(provider.get("key_label", "API key"))
- if _tab_key_hint != null and is_instance_valid(_tab_key_hint):
- var hint := (
- "Stored encrypted (AES-256, key derived from this machine) in Editor Settings "
- + "- not plain text, but local obfuscation only. You can also set the "
- + "%s environment variable; it takes priority over this field."
- ) % provider.get("env", "")
- if OS.get_unique_id().is_empty():
- hint += " This machine reports no unique id, so keys cannot be stored locally - use the %s environment variable." % provider.get("env", "")
- _tab_key_hint.text = hint
- if _tab_key_edit != null and is_instance_valid(_tab_key_edit):
- _tab_key_edit.placeholder_text = str(provider.get("placeholder", ""))
- if _tab_model_label != null and is_instance_valid(_tab_model_label):
- _tab_model_label.text = "Model id (required) - %s" % provider.get("label", provider_id)
- if _tab_model_edit != null and is_instance_valid(_tab_model_edit):
- _tab_model_edit.placeholder_text = str(provider.get("model_placeholder", ""))
- _tab_model_edit.tooltip_text = "The vision model this provider should ping. Yours to maintain: when a model id retires, replace it here."
- if _tab_model_hint != null and is_instance_valid(_tab_model_hint):
- _tab_model_hint.text = (
- "Required per provider; empty behaves like an empty key (routing "
- + "declines and screenshots pass through). Suggested id (as of "
- + "2026-08 - check your provider console, ids retire): %s."
- ) % provider.get("model_placeholder", "")
-
-
-func _on_test_connection() -> void:
- if _tab_status == null:
- return
- var provider_id := _active_provider_id()
- var provider := _provider_with_model(provider_id)
- var env_name := str(PROVIDERS.get(provider_id, PROVIDERS["groq"]).get("env", ""))
- if str(provider.get("model", "")).is_empty():
- _tab_status.text = "No model id set for %s - add one below." % provider.get("label", provider_id)
- return
- var api_key := _resolved_api_key(provider_id)
- if api_key.is_empty():
- _tab_status.text = "No key set - add one below or set %s." % env_name
- return
- var running: Thread = _threads.get("_test")
- if running != null and running.is_started() and running.is_alive():
- return
- var previous_status := _tab_status.text
- _tab_status.text = "Testing..."
- if _tab_test_button != null and is_instance_valid(_tab_test_button):
- _tab_test_button.disabled = true
- var key_source := "stored key"
- if not OS.get_environment(env_name).is_empty():
- key_source = "via %s" % env_name
- var thread := Thread.new()
- _threads["_test"] = thread
- _ping_context = {
- "provider": provider,
- "status_label": _tab_status,
- "key_source": key_source,
- }
- var start_err := thread.start(_ping_worker.bind(provider, api_key))
- if start_err != OK:
- ## A failed thread start must not leave the button disabled and the
- ## status stuck on "Testing..." forever.
- _threads.erase("_test")
- _ping_context.clear()
- _tab_status.text = previous_status
- if _tab_test_button != null and is_instance_valid(_tab_test_button):
- _tab_test_button.disabled = false
- _log("vision routing: could not start ping thread: %s" % error_string(start_err))
-
-
-func _on_enable_toggled(enabled: bool) -> void:
- var es := _settings()
- if es != null:
- es.set_setting(SETTING_ENABLED, enabled)
- _sync_ui_states()
-
-
-func _sync_ui_states() -> void:
- var enabled := is_routing_enabled()
- if _tab_enable != null and is_instance_valid(_tab_enable) and _tab_enable.button_pressed != enabled:
- _tab_enable.set_pressed_no_signal(enabled)
-
-
-# --- logging --------------------------------------------------------------------
-
-func _log(message: String) -> void:
- if log_buffer != null and is_instance_valid(log_buffer) and log_buffer.has_method("log"):
- log_buffer.log(message, false)
diff --git a/addons/godot_ai/vision_routing.gd.uid b/addons/godot_ai/vision_routing.gd.uid
deleted file mode 100644
index c4c5bc2..0000000
--- a/addons/godot_ai/vision_routing.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dp444q4ocpx45
diff --git a/addons/raytraced_audio/LICENSE b/addons/raytraced_audio/LICENSE
deleted file mode 100644
index 343dab8..0000000
--- a/addons/raytraced_audio/LICENSE
+++ /dev/null
@@ -1,9 +0,0 @@
-MIT License
-
-Copyright (c) 2025 Tienne_k
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/addons/raytraced_audio/README.md b/addons/raytraced_audio/README.md
deleted file mode 100644
index 25306be..0000000
--- a/addons/raytraced_audio/README.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# raytraced-audio
-Adds procedural audio effects to Godot like echo, ambient outdoor sounds, and muffle.
-
-Check out [this amazing video](https://youtu.be/u6EuAUjq92k?si=6W-sGozYBQITEgQo) by **Vercidium** for insights on how this works!
-
-No need to create and maintain zones while creating levels, just put the `RaytracedAudioListener` node in your scene and most features will be available for you to use!
-
-Additionally, by using `RaytracedAudioPlayer3D`s instead of regular `AudioPlayer3D`s, sounds will get automatically muffled behind walls.
-
-### Audio buses
-Right off the bat, this plugin creates 2 new audio buses for you to use across your project:
-a *"Reverb"* bus, and an *"Ambient"* bus.
-Note: both buses' names can be changed under `Project Settings > Raytraced Audio`.
-
-The reverb bus controls echo / reverb.
-For example, there will be a much bigger reverb in large enclosed rooms compared to small ones, or outside in the open.
-
-The ambient bus controls the strength and pan of sounds coming from outside.
-For example, in a room with a single opening leading outisde, sounds in this bus will appear to come from that opening, and will fade based on the player's distance to it.
-
-### Performace
-
-Because the rays need to gather different informations about the environment, the actual number of processed rays can go up to:
-
-`rays_count * (2 + n)` where `n` is the number of enabled `RaytracedAudioPlayer3D`s in the scene.
-
-That is:
-
-`(rays_count: configurable) * (1 ray that bounces around + 1 echo ray + (1 muffle ray per enabled RaytracedAudioPlayer3D))`
-
-Raytraced Audio also adds 2 performance monitors:
- - `raytraced_audio/raycast_updates`: How many raycast updates happened in the update tick
- - `raytraced_audio/enabled_players_count`: How many `RaytracedAudioPlayer3D`s are currently enabled in the scene
-
-
-### Installation
-
-#### Manual installation
-
-- Download or clone this repository
-- Copy the `addons/raytraced_audio` folder into your project's `addons/` folder
-- Enable the plugin in `Project Settings > Plugins > Raytraced Audio`
-
diff --git a/addons/raytraced_audio/audio_ray.gd b/addons/raytraced_audio/audio_ray.gd
deleted file mode 100644
index e259682..0000000
--- a/addons/raytraced_audio/audio_ray.gd
+++ /dev/null
@@ -1,139 +0,0 @@
-extends RayCast3D
-
-# dont you worry about this class, habibi
-# shhhhhh its okay
-# ( - ͜ʖ -)☞(ʘ_ʘ; )
-
-var cast_dist: float = 0.0
-var max_bounces: int = 1
-var ray_scatter: Callable
-
-var echo_dist: float = 0.0
-var echo_count: int = 0
-var bounces: int = 0
-var has_bounced_this_tick: bool = false
-var ray_casts_this_tick: int = 0
-var escaped: bool = false
-var escape_dir: Vector3 = Vector3.ZERO
-
-var _space_state: PhysicsDirectSpaceState3D
-
-
-func _init(raycast_dist: float, max_bounce_count: int) -> void:
- top_level = true
- enabled = false
- cast_dist = raycast_dist
- max_bounces = max_bounce_count
-
-
-func _enter_tree() -> void:
- owner = get_parent()
- # since the root can be custom, this assert may fail
- # assert(owner is RaytracedAudioListener)
-
-
-func _ready() -> void:
- _space_state = get_world_3d().direct_space_state
- reset()
-
-
-# TODO: optimize
-func update():
- # Reset if needed
- if escaped or bounces > max_bounces:
- reset()
-
- force_raycast_update()
- ray_casts_this_tick += 1
- bounces += 1
-
- # Escaped outside
- if !is_colliding():
- escaped = true
- global_position += target_position
- # Muffle ray: Check for line of sight with audio players
- for player: RaytracedAudioPlayer3D in get_tree().get_nodes_in_group(RaytracedAudioPlayer3D.ENABLED_GROUP_NAME):
- var has_line_of_sight: bool = _cast_ray(player.global_position).is_empty()
- player._lowpass_rays_count += int(has_line_of_sight)
- return
-
- # Bounce
- var hit_pos: Vector3 = get_collision_point()
- var normal: Vector3 = get_collision_normal()
- global_position = hit_pos + normal * 0.1
- target_position = target_position.bounce(normal)
- has_bounced_this_tick = true
-
- # Muffle ray: Check for line of sight with audio players
- for player: RaytracedAudioPlayer3D in get_tree().get_nodes_in_group(RaytracedAudioPlayer3D.ENABLED_GROUP_NAME):
- var has_line_of_sight: bool = _cast_ray(player.global_position).is_empty()
- player._lowpass_rays_count += int(has_line_of_sight)
- # Same as:
- # if has_line_of_sight:
- # player.lowpass_rays_count += 1
-
- # Echo ray: Check for line of light with listener
- # Optimization: no need to raycast for first echo bounce
- if bounces == 1:
- echo_dist = hit_pos.distance_to(owner.global_position)
- echo_count += 1
- escape_dir = target_position.normalized()
- elif _cast_ray(owner.global_position).is_empty():
- # The way is clear -> echo
- echo_dist = hit_pos.distance_to(owner.global_position)
- echo_count += 1
- escape_dir = owner.global_position.direction_to(hit_pos)
-
-
-# Return to the listener with a random direction
-func reset():
- var dir: Vector3 = ray_scatter.call()
-
- global_position = owner.global_position
- target_position = dir * cast_dist
-
- bounces = 0
- escaped = false
- escape_dir = dir
- reset_tick_stats()
-
-
-# Called after this ray is done ticking, in RaytracedAudioListener
-func reset_tick_stats() -> void:
- has_bounced_this_tick = false
- ray_casts_this_tick = 0
- echo_dist = 0.0
- echo_count = 0
-
-
-func set_scatter_model(model: RaytracedAudioListener.RayScatterModel) -> void:
- match model:
- RaytracedAudioListener.RayScatterModel.RANDOM:
- ray_scatter = _random_dir
- RaytracedAudioListener.RayScatterModel.XZ:
- ray_scatter = _random_dir_xz_plane
- _:
- push_error("Unknown ray scatter model: '", model, "'")
- ray_scatter = _random_dir
-
-
-
-func _cast_ray(to: Vector3) -> Dictionary:
- var params: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(
- global_position,
- to,
- 0b01
- )
- ray_casts_this_tick += 1
- return _space_state.intersect_ray(params)
-
-
-func _random_dir_xz_plane() -> Vector3:
- var yaw = randf_range(0.0, TAU)
- return Vector3(-sin(yaw), 0.0, -cos(yaw))
-
-func _random_dir() -> Vector3:
- var theta: float = randf_range(0.0, TAU)
- var y: float = randf_range(-1.0, 1.0)
- var k: float = sqrt(1.0 - y*y)
- return Vector3(k * cos(theta), k * sin(theta), y)
diff --git a/addons/raytraced_audio/audio_ray.gd.uid b/addons/raytraced_audio/audio_ray.gd.uid
deleted file mode 100644
index 08f6d6e..0000000
--- a/addons/raytraced_audio/audio_ray.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bqb6rv4pmub8c
diff --git a/addons/raytraced_audio/plugin.cfg b/addons/raytraced_audio/plugin.cfg
deleted file mode 100644
index cf5520e..0000000
--- a/addons/raytraced_audio/plugin.cfg
+++ /dev/null
@@ -1,7 +0,0 @@
-[plugin]
-
-name="Raytraced Audio"
-description="Adds procedural audio effects to Godot like echo, ambient outdoor sounds, and muffle"
-author="Tienne_k"
-version="1.0"
-script="plugin.gd"
diff --git a/addons/raytraced_audio/plugin.gd b/addons/raytraced_audio/plugin.gd
deleted file mode 100644
index 9daa715..0000000
--- a/addons/raytraced_audio/plugin.gd
+++ /dev/null
@@ -1,83 +0,0 @@
-@tool
-extends EditorPlugin
-
-
-func _enter_tree() -> void:
- _setup_settings()
-
- # For some reason, adding custom types like this fails to generate documentation sometimes??
- # add_custom_type(
- # "RaytracedAudioListener",
- # "AudioListener3D",
- # load("res://addons/raytraced_audio/raytraced_audio_listener.gd"),
- # null
- # )
- # add_custom_type(
- # "RaytracedAudioPlayer3D",
- # "AudioStreamPlayer3D",
- # load("res://addons/raytraced_audio/raytraced_audio_player_3d.gd"),
- # null
- # )
-
- _setup_audio_buses()
-
-
-func _exit_tree() -> void:
- # remove_custom_type("RaytracedAudioListener")
- # remove_custom_type("RaytracedAudioPlayer3D")
-
- _clean_up_settings()
- _clean_up_audio_buses()
-
-
-func _setup_settings() -> void:
- ProjectSettings.set_setting("raytraced_audio/reverb_bus", &"RaytracedReverb")
- ProjectSettings.set_setting("raytraced_audio/ambient_bus", &"RaytracedAmbient")
-
- if ProjectSettings.get_setting("audio/general/3d_panning_strength") < 1.0:
- print("[INFO] RaytracedAudio: I recommend setting Audio/General/3d_panning_strength in Project Settings to 1.0 or above")
-
-
-func _clean_up_settings() -> void:
- ProjectSettings.set_setting("raytraced_audio/reverb_bus", null)
- ProjectSettings.set_setting("raytraced_audio/ambient_bus", null)
-
-
-func _setup_audio_buses() -> void:
- print("[INFO] Raytraced Audio: setting up audio buses")
- _clean_up_audio_buses()
-
- # Reverb
- var i: int = AudioServer.bus_count
- AudioServer.add_bus()
- AudioServer.set_bus_name(i, ProjectSettings.get_setting("raytraced_audio/reverb_bus", &"RaytracedReverb"))
- AudioServer.set_bus_send(i, &"Master")
- var reverb: AudioEffectReverb = AudioEffectReverb.new()
- reverb.hipass = 1.0
- reverb.resource_name = "reverb"
- AudioServer.add_bus_effect(i, reverb)
-
- # Ambient
- i = AudioServer.bus_count
- AudioServer.add_bus()
- AudioServer.set_bus_name(i, ProjectSettings.get_setting("raytraced_audio/ambient_bus", &"RaytracedAmbient"))
- AudioServer.set_bus_send(i, &"Master")
- var panner: AudioEffectPanner = AudioEffectPanner.new()
- panner.resource_name = "pan"
- AudioServer.add_bus_effect(i, panner)
-
- # There's a bug in Godot that makes it so the ambient bus doesnt show properly in the editor
- # This is "fixable" by adding a temporary bus afterwards and deleting it immediately because ofc.
- # fuck you (respectfully)
- i = AudioServer.bus_count
- AudioServer.add_bus()
- AudioServer.remove_bus(i)
-
-
-func _clean_up_audio_buses() -> void:
- var i: int = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/reverb_bus", &"RaytracedReverb"))
- if i != -1:
- AudioServer.remove_bus(i)
- i = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/ambient_bus", &"RaytracedAmbient"))
- if i != -1:
- AudioServer.remove_bus(i)
diff --git a/addons/raytraced_audio/plugin.gd.uid b/addons/raytraced_audio/plugin.gd.uid
deleted file mode 100644
index b95ea1f..0000000
--- a/addons/raytraced_audio/plugin.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://8y7slc7nck0o
diff --git a/addons/raytraced_audio/raytraced_audio_listener.gd b/addons/raytraced_audio/raytraced_audio_listener.gd
deleted file mode 100644
index 5b463e2..0000000
--- a/addons/raytraced_audio/raytraced_audio_listener.gd
+++ /dev/null
@@ -1,356 +0,0 @@
-class_name RaytracedAudioListener extends AudioListener3D
-## 3D audio listener for raytraced audio
-##
-## [b]Audio buses[/b]
-## [br]This plugin creates 2 new audio buses for you to use across your project:
-## a [i]"Reverb"[/i] bus, and an [i]"Ambient"[/i] bus.
-## [br]Note: both buses' names can be changed under [code]Project Settings > Raytraced Audio[/code].
-## [br]
-## [br]The reverb bus controls echo / reverb.
-## [br]For example, there will be a much bigger reverb in large enclosed rooms compared to small ones, or outside in the open
-## [br]
-## [br]The ambient bus controls the strength and pan of sounds coming from outside.
-## [br]For example, in a room with a single opening leading outisde, sounds in this bus will appear to come from that opening, and will fade based on the player's distance to it
-## [br]
-## [br][b]Performace[/b]
-## [br]Raytraced Audio adds 2 performance monitors:
-## [br] - [code]raytraced_audio/raycast_updates[/code]: How many raycast updates happened in one update tick
-## [br] - [code]raytraced_audio/enabled_players_count[/code]: How many [RaytracedAudioPlayer3D]s are enabled in the scene
-## [br]
-## [br]Note: There should be only one [RaytracedAudioListener] in a given scene, just like how there should be only one AudioListener3D in a scene.
-## [br]See also [RaytracedAudioPlayer3D]
-
-## Speed of sound in m/s
-const SPEED_OF_SOUND: float = 343.0
-## All [RaytracedAudioListener]s will be in this group
-const GROUP_NAME: StringName = &"raytraced_audio_listener"
-
-const AudioRay: Script = preload("res://addons/raytraced_audio/audio_ray.gd")
-
-enum RayScatterModel {
- ## Rays will be shot out in a random 3d direction
- RANDOM,
- ## Rays will be shot out on the listener's XZ plane (i.e. [code]Vector3(random, 0, random)[/code])
- XZ,
-}
-
-## Emitted when this node is enabled
-signal enabled
-## Emitted when this node is disabled
-signal disabled
-## Emitted when any configuration for the rays are changed
-## [br]This includes: [member rays_count], [member max_raycast_dist], [member max_bounces], and [member ray_scatter_model]
-signal ray_configuration_changed
-
-## List of rays instanced by this node
-var rays: Array[AudioRay] = []
-
-## Enable or disable raycasting
-## [br]Disabled nodes can't be updated (see [method update]) even if [member auto_update] is set to [code]true[/code]
-@export var is_enabled: bool = true:
- set(v):
- if is_enabled == v:
- return
- is_enabled = v
-
- if is_enabled:
- if is_node_ready():
- setup()
- enabled.emit()
- else:
- disabled.emit()
- if is_node_ready():
- clear()
-
-## Whether to update automatically
-## [br]If set to [code]true[/code], this [RaytracedAudioListener] will update every [i]process[/i] frame
-@export var auto_update: bool = true:
- set(v):
- auto_update = v
- set_process(auto_update)
-
-## Number of rays to use
-## [br]More rays and more bounces mean a more accurate model of the environment can be made
-## [br] See also [member max_bounces]
-## [br]
-## [br][i]Technical note[/i]:
-## [br] Because the rays need to gather different informations about the environment,
-## the actual number of processed rays can go up to:
-## [br] [code]rays_count * (2 + n)[/code] where [code]n[/code] is the number of enabled [RaytracedAudioPlayer3D]s in the scene
-## [br] ([code]rays_count * (1 ray that bounces around + 1 echo ray + 1 muffle ray per enabled RaytracedAudioPlayer3D[/code])
-@export var rays_count: int = 4:
- set(v):
- if v == rays_count:
- return
- rays_count = maxi(v, 1)
- clear()
- setup()
- ray_configuration_changed.emit()
-
-## The maximum distance which any given ray instanced by this node will cast
-@export var max_raycast_dist: float = SPEED_OF_SOUND:
- set(v):
- max_raycast_dist = v
- _update_ray_configuration()
- ray_configuration_changed.emit()
-## [br]More rays and more bounces mean a more accurate model of the environment can be made
-## [br] See also [member rays_count]
-@export var max_bounces: int = 3:
- set(v):
- max_bounces = v
- _update_ray_configuration()
- ray_configuration_changed.emit()
-
-## Controls how rays will be instanced
-@export var ray_scatter_model: RayScatterModel = RayScatterModel.RANDOM:
- set(v):
- ray_scatter_model = v
- _update_ray_configuration()
- ray_configuration_changed.emit()
-
-## Custom root for ray position calculations
-@export var custom_root: Node3D
-
-@export_category("Muffle")
-## Enables [RaytracedAudioPlayer3D]s muffling behind walls
-@export var muffle_enabled: bool = true
-## The interpolation strength of the muffle
-## [br]See [member muffle_enabled]
-@export_range(1.0, 25.0, 0.1) var muffle_interpolation: float = 5.0
-@export_category("Echo")
-## Enables updates to the reverb audio bus
-## [br]See [code]Project Settings > Raytraced Audio > Reverb Bus[/code]
-@export var echo_enabled: bool = true
-## The "intensity" of the reverb
-## [br]More specifically, multiplies the reverb's room size by this amount
-## [br]The default is [code]2.0[/code] to account for sound waves coming back to the listener after hitting a wall
-@export var echo_room_size_multiplier: float = 2.0
-## The interpolation strength of the echo
-## [br]See [member echo_enabled]
-@export_range(1.0, 25.0, 0.1) var echo_interpolation: float = 5.0
-@export_category("Ambient")
-## Enables updates to the ambient audio bus
-## [br]See [code]Project Settings > Raytraced Audio > Ambient Bus[/code]
-@export var ambient_enabled: bool = true
-## The interpolation strength of ambient sounds' direction (pan)
-## [br]See [member ambient_enabled], [member ambient_pan_strength]
-@export_range(1.0, 25.0, 0.1) var ambient_pan_interpolation: float = 5.0
-## How strong the pan between right and left ear will be
-## Setting this to 0 disables panning completely
-## [br]See [member ambient_enabled], [member ambient_pan_interpolation]
-@export_range(0.0, 1.0, 0.01) var ambient_pan_strength: float = 1.0
-## The interpolation strength of ambient sounds' volume
-## [br]See [member ambient_enabled]
-@export_range(1.0, 25.0, 0.1) var ambient_volume_interpolation: float = 5.0
-## How smoothly the ambient sounds will fade away when the [AudioListener3D] is no longer "outside"
-## Values close to 1 will make sounds linger for longer, while values close to 0 will make them drop suddenly
-## [br]See [member ambient_enabled]
-@export_range(0.0, 1.0, 0.001) var ambient_volume_attenuation: float = 0.998
-
-## The size of the room based on data gathered from rays, in world units
-## [br]Used in reverb calculation
-var room_size: float = 0.0
-## How "outside" this [RaytracedAudioListener] is based on data gathered from rays, between 0 and 1
-var ambience: float = 0.0
-## The direction to "outside" based on data gathered from rays
-## [br]Should average out to 0 when this [RaytracedAudioListener] is completely outside
-var ambient_dir: Vector3 = Vector3.ZERO
-
-## For debugging purposes
-## [br]See the [code]raytraced_audio/raycast_updates[/code] performance monitor
-var ray_casts_this_tick: int = 0
-
-var _reverb_effect: AudioEffectReverb
-var _pan_effect: AudioEffectPanner
-
-# Keep track of whether we've set up debug monitors
-var _debug_monitors_setup: bool = false
-
-
-func _enter_tree() -> void:
- add_to_group(GROUP_NAME)
-
-
-func _ready() -> void:
- # Reverb effect
- var i: int = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/reverb_bus"))
- if i == -1:
- push_error("Failed to get reverb bus for raytraced audio. Disabling echo...")
- echo_enabled = false
- else:
- _reverb_effect = AudioServer.get_bus_effect(i, 0)
-
- # Pan effect
- i = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/ambient_bus"))
- if i == -1:
- push_error("Failed to get ambient bus for raytraced audio. Disabling ambience...")
- ambient_enabled = false
- else:
- _pan_effect = AudioServer.get_bus_effect(i, 0)
-
- if is_enabled:
- if custom_root:
- call_deferred("setup")
- else:
- setup()
-
- set_process(auto_update)
- if is_enabled:
- make_current()
-
- _setup_debug()
-
-
-func _exit_tree() -> void:
- _cleanup_debug()
-
-
-func _setup_debug() -> void:
- if _debug_monitors_setup:
- return
-
- _debug_monitors_setup = true
-
- Performance.add_custom_monitor(&"raytraced_audio/raycast_updates", func():
- # Check if this instance is still valid
- if not is_instance_valid(self):
- return 0
- return ray_casts_this_tick
- )
-
- Performance.add_custom_monitor(&"raytraced_audio/enabled_players_count", func():
- # Check if this instance is still valid and if we're in a scene tree
- if not is_instance_valid(self) or not is_inside_tree():
- return 0
- return get_tree().get_node_count_in_group(RaytracedAudioPlayer3D.ENABLED_GROUP_NAME)
- )
-
-
-func _cleanup_debug() -> void:
- if not _debug_monitors_setup:
- return
-
- _debug_monitors_setup = false
-
- # Remove the custom monitors
- Performance.remove_custom_monitor(&"raytraced_audio/raycast_updates")
- Performance.remove_custom_monitor(&"raytraced_audio/enabled_players_count")
-
-
-
-## Initiates this [RaytracedAudioListener]'s rays
-func setup() -> void:
- for __ in rays_count:
- var rc: AudioRay = AudioRay.new(max_raycast_dist, max_bounces)
- rc.set_scatter_model(ray_scatter_model)
- # Add to the custom root if present
- if custom_root:
- custom_root.add_child(rc, INTERNAL_MODE_BACK)
- else:
- add_child(rc, INTERNAL_MODE_BACK)
- rays.push_back(rc)
-
-
-## Clears all created rays
-func clear():
- for ray: AudioRay in rays:
- if custom_root:
- custom_root.remove_child(ray)
- else:
- remove_child(ray)
- ray.queue_free()
- rays.clear()
-
-
-func _process(delta: float) -> void:
- if !auto_update:
- set_process(false)
- return
- update(delta)
-
-
-## Updates this [RaytracedAudioListener]
-## [br]If you are updating this node manually (i.e. [member auto_update] is [code]false[/code]), this is the method to call
-func update(delta: float):
- ray_casts_this_tick = 0
- if !is_enabled:
- return
-
- var echo: float = 0.0 # Avg echo from all rays
- var echo_count: int = 0 # Number of echo rays that came back
- var bounces_this_tick: int = 0
- var escaped_count: int = 0
- var escaped_dir: Vector3 = Vector3.ZERO # Avg escape direction
- var escaped_strength: float = 0.0
-
- # Gather data
- for ray: AudioRay in rays:
- ray.update()
- ray_casts_this_tick += ray.ray_casts_this_tick
-
- echo += ray.echo_dist
- echo_count += ray.echo_count
- bounces_this_tick += int(ray.has_bounced_this_tick)
-
- if ray.escaped:
- escaped_count += 1
- escaped_strength += 1.0 / ray.bounces
- escaped_dir += ray.escape_dir
-
- ray.reset_tick_stats()
-
- echo = 0.0 if echo_count == 0 else (echo / float(echo_count))
- escaped_dir = Vector3.ZERO if escaped_count == 0 else (escaped_dir / float(escaped_count))
-
- if muffle_enabled:
- _update_muffle(delta)
- if echo_enabled:
- _update_echo(echo, echo_count, bounces_this_tick, delta)
- if ambient_enabled:
- _update_ambient(escaped_strength, escaped_dir, delta)
-
-
-func _update_muffle(delta: float) -> void:
- for player: RaytracedAudioPlayer3D in get_tree().get_nodes_in_group(RaytracedAudioPlayer3D.GROUP_NAME):
- player.update(self, delta)
-
-
-func _update_echo(echo: float, echo_count: int, bounces: int, delta: float) -> void:
- # Length -> echo delay
- room_size = lerpf(room_size, echo, 1.0 - exp(-delta * echo_interpolation))
- var e: float = (room_size * echo_room_size_multiplier) / SPEED_OF_SOUND
- # print("e = ", e)
- _reverb_effect.room_size = lerpf(_reverb_effect.room_size, clampf(e, 0.0, 1.0), 1.0 - exp(-delta * echo_interpolation))
- _reverb_effect.predelay_msec = lerpf(_reverb_effect.predelay_msec, e * 1000, 1.0 - exp(-delta * echo_interpolation))
- _reverb_effect.predelay_feedback = lerpf(_reverb_effect.predelay_feedback, clampf(e, 0.0, 0.98), 1.0 - exp(-delta * echo_interpolation))
- # More rays % -> echo strength
- var return_ratio: float = 0.0 if bounces == 0 else float(echo_count) / float(bounces)
- _reverb_effect.hipass = lerpf(_reverb_effect.hipass, 1.0 - return_ratio, 1.0 - exp(-delta * echo_interpolation))
-
-
-func _update_ambient(escaped_strength: float, escaped_dir: Vector3, delta: float) -> void:
- var ambience_ratio: float = float(escaped_strength) / float(rays_count)
-
- # More rays % -> louder
- if escaped_strength > 0:
- #should it use delta?
- #ambience = lerpf(ambience, 1.0, 1.0 - exp(-delta * ambience_ratio))
- ambience = lerpf(ambience, 1.0, ambience_ratio)
- else:
- ambience *= ambient_volume_attenuation
- var ambient_bus_idx: int = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/ambient_bus"))
- var volume: float = AudioServer.get_bus_volume_linear(ambient_bus_idx)
- AudioServer.set_bus_volume_linear(ambient_bus_idx, lerpf(volume, ambience, 1.0 - exp(-delta * ambient_volume_interpolation)))
-
- # Avg escape direction -> pan
- ambient_dir = ambient_dir.slerp(escaped_dir, 1.0 - exp(-delta * ambient_pan_interpolation))
- var target_pan: float = 0.0 if ambient_dir.is_zero_approx() else owner.transform.basis.x.dot(ambient_dir.normalized())
- _pan_effect.pan = target_pan * ambient_pan_strength
-
-
-# ✨ the name says it all ✨
-func _update_ray_configuration() -> void:
- for ray: AudioRay in rays:
- ray.cast_dist = max_raycast_dist
- ray.max_bounces = max_bounces
- ray.set_scatter_model(ray_scatter_model)
diff --git a/addons/raytraced_audio/raytraced_audio_listener.gd.uid b/addons/raytraced_audio/raytraced_audio_listener.gd.uid
deleted file mode 100644
index 0b66916..0000000
--- a/addons/raytraced_audio/raytraced_audio_listener.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bjpmu5sklv5h8
diff --git a/addons/raytraced_audio/raytraced_audio_player_3d.gd b/addons/raytraced_audio/raytraced_audio_player_3d.gd
deleted file mode 100644
index 5d53d10..0000000
--- a/addons/raytraced_audio/raytraced_audio_player_3d.gd
+++ /dev/null
@@ -1,238 +0,0 @@
-class_name RaytracedAudioPlayer3D extends AudioStreamPlayer3D
-## Audio stream player that allows for audio muffling
-##
-## You can already use the reverb and ambient audio buses (see the documentation for [RaytracedAudioListener] for more details)
-## to make use of raytraced audio effects, but using [RaytracedAudioPlayer3D]s allows you to use more capabilities.
-## Namely, muffling sounds behind walls.
-## [br]
-## [br][b][color=yellow]Warning[/color][/b]: [RaytracedAudioPlayer3D]s will default to using the reverb audio bus
-## [br]If you wish to use another bus instead, please set it after this node has been added to the scene tree (i.e. after [code]_enter_tree()[/code])
-## [br]
-## [br][i]Technical note[/i]:
-## [br]Currently, there is no way to muffle only one audio player (or apply any effect for that matter).
-## [br]So we create a new bus for every audio player that is audible from the [RaytracedAudioListener]
-## [br]Godot doesn't provide methods to calculate the volume of an audio at certain distances, so we calculate
-## that ourselves (see [method calculate_audible_distance_threshold()])
-
-## All [RaytracedAudioPlayer3D]s (regardless of state) will be in this group
-const GROUP_NAME: StringName = &"raytraced_audio_player_3d"
-## All [b]enabled[/b] [RaytracedAudioPlayer3D]s will be in this group
-const ENABLED_GROUP_NAME: StringName = &"enabled_raytraced_audio_player_3d"
-## The lowpass frequency when completely muffled
-const LOWPASS_MIN_HZ: float = 250.0
-## The lowpass frequency when completely not-muffled (?)
-const LOWPASS_MAX_HZ: float = 20000.0
-## The factor the nyquist frequency is multiplied with to get a safe max_cutoff
-const NYQUIST_MULTIPLIER: float = 0.95
-
-## Used in internal calculations
-const LOG2: float = log(2.0)
-## Used in internal calculations
-const LOG_MIN_HZ: float = log(LOWPASS_MIN_HZ) / LOG2
-## Used in internal calculations
-const LOG_MAX_HZ: float = log(LOWPASS_MAX_HZ) / LOG2
-
-## Emitted when this node is enabled
-## [br]See also [constant ENABLED_GROUP_NAME]
-signal enabled
-## Emitted when this node is disabled
-signal disabled
-## Emitted when the maximum audible distance for this node is changed
-## [br]See also [member AudioStreamPlayer3D.max_distance] and [member audibility_threshold_db]
-signal audible_distance_updated(distance: float)
-
-## The threshold (in decibels) at which sounds will be considered inaudible
-## [br]This is used to enable / disable this node when it's not audible to save resources
-## [br]The distance at which this node is no longer considered audible is automatically stored inside [member AudioStreamPlayer3D.max_distance]
-## (if not already set), also to save resources
-## [br]Though, setting [member AudioStreamPlayer3D.max_distance] doesn't update this field automatically
-## [br]See also [method get_volume_db_from_pos] and [method calculate_audible_distance_threshold]
-@export var audibility_threshold_db: float = -30.0:
- set(v):
- audibility_threshold_db = v
- # Max distance not configured
- if is_node_ready() and max_distance == 0.0:
- max_distance = calculate_audible_distance_threshold()
- audible_distance_updated.emit(max_distance)
-
-var _lowpass_rays_count: int = 0
-var _lowpass_max_safe_cutoff: float = LOWPASS_MAX_HZ
-var _is_enabled: bool = false
-
-
-func _enter_tree() -> void:
- add_to_group(GROUP_NAME)
- bus = ProjectSettings.get_setting("raytraced_audio/reverb_bus") # Fallback
-
-func _ready() -> void:
- # Max distance not configured
- if max_distance == 0.0:
- max_distance = calculate_audible_distance_threshold()
- audible_distance_updated.emit(max_distance)
-
- # Update the lowpass limits on start
- _lowpass_max_safe_cutoff = calculate_lowpass_limit()
-
-
-## Enables this node
-## [br]Note: you should almost never have to worry about enabling / disabling [RaytracedAudioPlayer3D]s manually
-## [br]See [method update]
-func enable():
- if _is_enabled:
- return
- _is_enabled = true
- var i: int = _create_bus()
- bus = AudioServer.get_bus_name(i)
- add_to_group(ENABLED_GROUP_NAME)
-
- enabled.emit()
-
-## Enables this node
-## [br]Note: you should almost never have to worry about enabling / disabling [RaytracedAudioPlayer3D]s manually
-## [br]See [method update]
-func disable():
- if !_is_enabled:
- return
-
- # Don't remove the fallback bus lol
- if bus == ProjectSettings.get_setting("raytraced_audio/reverb_bus"):
- _disable()
- return
-
- # Remove this node's specific bus
- var idx: int = AudioServer.get_bus_index(bus)
- if idx == -1:
- push_warning("audio bus ", bus, " not found")
- _disable()
- return
- _disable()
- AudioServer.remove_bus(idx)
-
-func _disable():
- _is_enabled = false
- bus = ProjectSettings.get_setting("raytraced_audio/reverb_bus") # Fallback
- remove_from_group(ENABLED_GROUP_NAME)
- _lowpass_rays_count = 0
-
- disabled.emit()
-
-
-# Returns the index of the created bus
-func _create_bus() -> int:
- var i: int = AudioServer.bus_count
- AudioServer.add_bus()
- AudioServer.set_bus_name(i, generate_bus_name())
- AudioServer.set_bus_send(i, ProjectSettings.get_setting("raytraced_audio/reverb_bus"))
- AudioServer.add_bus_effect(i, AudioEffectLowPassFilter.new())
- return i
-
-## Returns a name for the audio bus created for this node
-func generate_bus_name() -> String:
- return str("RTAudioPlayer3D_", name, randi())
-
-## ✨ the name says it all ✨
-func is_enabled() -> bool:
- return _is_enabled
-
-## Updates this [RaytracedAudioPlayer3D]
-## [br]This method will adjust the muffle of the played stream, as well as enable or disable it
-## based on whether it's audible from the given [RaytracedAudioListener]
-## [br]If you are updating this node manually, this is the method to call
-func update(listener: RaytracedAudioListener, delta: float) -> void:
- if _is_enabled:
- _update(listener.rays_count, listener.muffle_interpolation, delta)
-
- _lowpass_rays_count = 0
-
- # Enable based on position
- var dist_sq: float = global_position.distance_squared_to(listener.global_position)
- if dist_sq > max_distance*max_distance or !playing:
- disable()
- else:
- enable()
-
-
-func _update(rays_count: int, interpolation: float, delta: float):
- if bus == ProjectSettings.get_setting("raytraced_audio/reverb_bus"):
- _disable()
- return
-
- var idx: int = AudioServer.get_bus_index(bus)
- if idx == -1:
- push_error("audio bus ", bus, " not found")
- _disable()
- else:
- var ratio: float = float(_lowpass_rays_count) / float(rays_count)
- ratio = clamp(ratio, 0, 1)
- var lowpass: AudioEffectLowPassFilter = AudioServer.get_bus_effect(idx, 0)
-
- # Frequencies aren't linear, they scale logarithmically (log2 space) +1 octave = 2x the frequency
- # So we scale frequencies down before lerping, then scale them back up
- var log_t: float = lerpf(LOG_MIN_HZ, LOG_MAX_HZ, ratio)
- var log_hz: float = log(lowpass.cutoff_hz) / LOG2 # Scale current frequency down: log2(x) = ln(x) / ln(2)
-
- log_hz = lerpf(log_hz, log_t, 1.0 - exp(-delta * interpolation)) # Lerp in scaled down space
- lowpass.cutoff_hz = min(pow(2, log_hz), _lowpass_max_safe_cutoff) # Limit at max cutoff
-
-
-# Translated from the godot repo
-## Calculates the volume (in decibels) of the audio stream from the given position
-## [br]Please note that the final volume will vary depending on the stream that is being played
-func get_volume_db_from_pos(from_pos: Vector3) -> float:
- const CMP_EPSILON: float = 0.0001
-
- var dist: float = from_pos.distance_to(global_position)
- var vol: float = 0.0
- match attenuation_model:
- ATTENUATION_INVERSE_DISTANCE:
- vol = linear_to_db(1.0 / ((dist / unit_size) + CMP_EPSILON))
- ATTENUATION_INVERSE_SQUARE_DISTANCE:
- var d: float = (dist / unit_size)
- vol = linear_to_db(1.0 / (d*d + CMP_EPSILON))
- ATTENUATION_LOGARITHMIC:
- vol = -20.0 * log(dist / unit_size + CMP_EPSILON)
- ATTENUATION_DISABLED:
- pass
- _:
- push_error("Unknown attenuation type: '", attenuation_model, "'")
- vol = minf(vol + volume_db, max_db)
- return vol
-
-
-## Calculates the distance at which this [RaytracedAudioPlayer3D] is no longer considered audible
-## [br]See also [member audibility_threshold_db]
-func calculate_audible_distance_threshold() -> float:
- if max_distance > 0.0:
- return max_distance
-
- match attenuation_model:
- ATTENUATION_INVERSE_DISTANCE:
- var t_lin: float = db_to_linear(audibility_threshold_db - volume_db)
- # Unit_size / dist < T_lin
- return unit_size / t_lin
- ATTENUATION_INVERSE_SQUARE_DISTANCE:
- var t_lin: float = db_to_linear(audibility_threshold_db - volume_db)
- # (Unit_size / dist)^2 < T_lin
- return sqrt(unit_size*unit_size / t_lin)
- ATTENUATION_LOGARITHMIC:
- var t_db: float = audibility_threshold_db - volume_db
- # -20 * log(dist / Unit_size) > T_db
- return exp(t_db / -20.0) * unit_size
- ATTENUATION_DISABLED:
- return 0.0
- _:
- push_error("Unknown attenuation model: '", attenuation_model, "'")
- return 0.0
-
-
-## Gets the current sampling rate of the AudioServer and calculates
-## a safe cutoff_hz value for the lowpass filter.
-func calculate_lowpass_limit() -> float:
- var nyquist_frequency = AudioServer.get_mix_rate() * 0.5
- return nyquist_frequency * NYQUIST_MULTIPLIER
-
-
-## Checks wheter this [RaytracedAudioPlayer3D] is considered audible from the given position
-## [br]See also [member audibility_threshold_db]
-func is_audible(from_pos: Vector3) -> bool:
- return get_volume_db_from_pos(from_pos) >= audibility_threshold_db
\ No newline at end of file
diff --git a/addons/raytraced_audio/raytraced_audio_player_3d.gd.uid b/addons/raytraced_audio/raytraced_audio_player_3d.gd.uid
deleted file mode 100644
index 9b5235a..0000000
--- a/addons/raytraced_audio/raytraced_audio_player_3d.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dkb8ltjc6067n
diff --git a/addons/script-ide/LICENSE b/addons/script-ide/LICENSE
deleted file mode 100644
index ffc9cf2..0000000
--- a/addons/script-ide/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2023 Marius Hanl
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/addons/script-ide/README.md b/addons/script-ide/README.md
deleted file mode 100644
index 0acb613..0000000
--- a/addons/script-ide/README.md
+++ /dev/null
@@ -1,142 +0,0 @@
-# Script IDE
-
-Transforms the Script UI into an IDE like UI.
-Multiline Tabs, Splitting, Outline with unique icons that shows all members of the script.
-Quick search and function override functionality.
-
-## Features
-
-### Scripts as Tabs
-
-- Scripts are now shown as Multiline Tab
-- Script Tab can be split – showing the current Script as readonly CodeEdit next to the main one.
- The original script can be reopened by right-clicking it
-- Scripts can be opened in a Popup with a shortcut or when clicking the three dots on the top right of the Tabs for quick navigation between scripts
-- The currently edited script is automatically selected in the Filesystem Dock
-
-### Outline with all members
-
-- The Outline got an overhaul and showed more than just the methods of the script.
- It includes the following members (types) with a unique icon:
- - Classes (Red Square)
- - Constants (Red Circle)
- - Signals (Yellow)
- - Export variables (Orange)
- - (Static) Variables (Red)
- - Engine callback functions (Blue)
- - (Static) Functions (Green)
- - Setter functions (Green circle, with an arrow inside it pointing to the right)
- - Getter functions (Green circle, with an arrow inside it pointing to the left)
-- All different member types of the script can be hidden or made visible again by the outline filter.
- This allows fine control of what should be visible (e.g., only signals, (Godot) functions, ...)
-- A `Right Click` enables only the clicked filter, another `Right Click` will enable all filters again
-- The Outline can be opened in a Popup with a shortcut for quick navigation without the mouse
-- You can navigate through the Outline with the `Arrow` keys (or `Page up/Page down`) and scroll to the selected item by pressing `ENTER`
-
-### Quick search / override
-
-- Files can be quickly searched by the Quick Search Popup with `Shift`+`Shift`
-- You can find and quickly override any method from your super classes with `Alt`+`Ins`
-
-## Customization
-- The Outline is on the right side (can be changed to be on the left side again)
-- The Outline can be toggled via `File -> Toggle Scripts Panel`. This will hide or show it
-- The order in the Outline can be changed
-- There is also the possibility to hide private members, this is all members starting with a `_`
-- The Script ItemList is not visible by default but can be made visible again
-
-### Settings
-
-All settings can be changed in the `Editor Settings` under `Plugin` -> `Script Ide`.
-
-- `Open Outline Popup` = Shortcut to control how the Outline Popup should be triggered
- (default=CTRL+O or META+O)
-- `Outline Position Right` = Flag to control whether the outline should be on the right or on the left side of the script editor
- (default=true)
-- `Outline Order` = List which specifies the order of all different types in the Outline
-- `Hide Private Members` = Flag to control whether private members (methods/variables/constants starting with '_') should be hidden in the Outline or not
- (default=false)
-- `Open Script Popup` = Shortcut to control how the Script Popup should be triggered
- (default=CTRL+U or META+U)
-- `Script List Visible` = Flag to control whether the script list should still be visible or not (above the outline)
- (default=false)
-- `Script Tabs Singleline` = Flag to control whether the script tabs should be in a single line (instead of multiline)
- (default=false)
-- `Script Tabs Visible` = Flag to control whether the script tabs should be visible or not
- (default=true)
-- `Script Tabs Position Top` = Flag to control whether the script tabs should be on the top or on the bottom
- (default=true)
-- `Script Tabs Close Button Always` = Flag to control whether the script tabs should always have the close button or only the select tab
- (default=false)
-- `Auto Navigate in FileSystem Dock` = Flag to control whether the script that is currently edited should be automatically selected in the Filesystem Dock
- (default=true)
-- `Open Quick Search Popup` = Shortcut to control how the Quick Search Popup should be triggered
- (default=Shift+Shift, double press behavior is hardcoded for now)
-- `Open Quick Search Popup Scenes` = Shortcut to open the Quick Search Popup with the Scene Tab activated
- (default=CTRL+N or META+N)
-- `Open Quick Search Popup Scripts` = Shortcut to open the Quick Search Popup with the Script Tab activated
- (default=CTRL+SHIFT+N or META+SHIFT+N)
-- `Open Quick Search Popup Resources` = Shortcut to open the Quick Search Popup with the Resource Tab activated
- (default=CTRL+SHIFT+ALT+N or META+SHIFT+ALT+N)
-- `Open Override Popup` = Shortcut to control how the Override Popup should be triggered
- (default=Alt+Ins)
-- `Cycle Tab forward` = Shortcut to cycle the script tabs in the forward direction (only works in the 'Script' Editor Tab)
- (default=CTRL+TAB)
-- `Cycle Tab backward` = Shortcut to cycle the script tabs in the backward direction (only works in the 'Script' Editor Tab)
- (default=CTRL+SHIFT+TAB)
-- All outline visibility settings
-
-## Screenshots
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-## Videos
-
-- [Gamefromscratch](https://www.youtube.com/watch?v=ALshYw7K7Bs)
-
-## Links
-
-- [Asset Store](https://store.godotengine.org/asset/maran23/script-ide/)
-- [Asset Library](https://godotengine.org/asset-library/asset/2206)
-
-## Further notes
-
-- This plugin tries to modify as little as possible in the Engine.
- Since each version can introduce changes in the layout, every change can risk stability.
-- The plugin initialization will fail fast if something is unexpected. A log message is printed and nothing else will be changed.
-- The plugin code is aware of the structure in 'script_editor_plugin.cpp', which is the native part of the script tree structure that this plugin modifies
-- This plugin is written with performance in mind:
- - Everything that you might not use will also not consume any resources.
- - Example: The quick open menu will only rebuild (lazily) when you open it
- - There are no unneeded features.
- - Example: If the functionality is most likely not relevant for many users, it probably should be its own plugin
-
-### Contributing
-
-In order to open this project in the Engine, you need to create (or copy) a `project.godot` into it.
-Example file:
-```properties
-config_version=5
-
-[application]
-
-config/name="Script-IDE"
-config/features=PackedStringArray("4.7")
-config/icon="uid://eln8vh0yruef"
-
-[editor_plugins]
-
-enabled=PackedStringArray("res://addons/script-ide/plugin.cfg")
-```
\ No newline at end of file
diff --git a/addons/script-ide/outline/icon/class.svg b/addons/script-ide/outline/icon/class.svg
deleted file mode 100644
index 12f44f7..0000000
--- a/addons/script-ide/outline/icon/class.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/addons/script-ide/outline/icon/class.svg.import b/addons/script-ide/outline/icon/class.svg.import
deleted file mode 100644
index a54ae58..0000000
--- a/addons/script-ide/outline/icon/class.svg.import
+++ /dev/null
@@ -1,44 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://csik7oxvt7tq3"
-path="res://.godot/imported/class.svg-9e3f3363c9474e8c1dcf8ba4e40e95bf.ctex"
-metadata={
-"has_editor_variant": true,
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://addons/script-ide/outline/icon/class.svg"
-dest_files=["res://.godot/imported/class.svg-9e3f3363c9474e8c1dcf8ba4e40e95bf.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
-svg/scale=1.0
-editor/scale_with_editor_scale=true
-editor/convert_colors_with_editor_theme=false
diff --git a/addons/script-ide/outline/icon/constant.svg b/addons/script-ide/outline/icon/constant.svg
deleted file mode 100644
index 8be4457..0000000
--- a/addons/script-ide/outline/icon/constant.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/addons/script-ide/outline/icon/constant.svg.import b/addons/script-ide/outline/icon/constant.svg.import
deleted file mode 100644
index fc4310f..0000000
--- a/addons/script-ide/outline/icon/constant.svg.import
+++ /dev/null
@@ -1,44 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cawc456ja8vf5"
-path="res://.godot/imported/constant.svg-b36d0d3f8bf0f157e4f3b418b692e11b.ctex"
-metadata={
-"has_editor_variant": true,
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://addons/script-ide/outline/icon/constant.svg"
-dest_files=["res://.godot/imported/constant.svg-b36d0d3f8bf0f157e4f3b418b692e11b.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
-svg/scale=1.0
-editor/scale_with_editor_scale=true
-editor/convert_colors_with_editor_theme=false
diff --git a/addons/script-ide/outline/icon/engine_func.svg b/addons/script-ide/outline/icon/engine_func.svg
deleted file mode 100644
index dee80a2..0000000
--- a/addons/script-ide/outline/icon/engine_func.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/addons/script-ide/outline/icon/engine_func.svg.import b/addons/script-ide/outline/icon/engine_func.svg.import
deleted file mode 100644
index f88de0c..0000000
--- a/addons/script-ide/outline/icon/engine_func.svg.import
+++ /dev/null
@@ -1,44 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cupb0polhqrwj"
-path="res://.godot/imported/engine_func.svg-eb6c615eeef058e7cc5ec3b5a17fc008.ctex"
-metadata={
-"has_editor_variant": true,
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://addons/script-ide/outline/icon/engine_func.svg"
-dest_files=["res://.godot/imported/engine_func.svg-eb6c615eeef058e7cc5ec3b5a17fc008.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
-svg/scale=1.0
-editor/scale_with_editor_scale=true
-editor/convert_colors_with_editor_theme=false
diff --git a/addons/script-ide/outline/icon/export.svg b/addons/script-ide/outline/icon/export.svg
deleted file mode 100644
index a8e8721..0000000
--- a/addons/script-ide/outline/icon/export.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/addons/script-ide/outline/icon/export.svg.import b/addons/script-ide/outline/icon/export.svg.import
deleted file mode 100644
index 5041a77..0000000
--- a/addons/script-ide/outline/icon/export.svg.import
+++ /dev/null
@@ -1,44 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bvu2gnj8fv2kw"
-path="res://.godot/imported/export.svg-b7770a5f2f99766591abd69aca087dfc.ctex"
-metadata={
-"has_editor_variant": true,
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://addons/script-ide/outline/icon/export.svg"
-dest_files=["res://.godot/imported/export.svg-b7770a5f2f99766591abd69aca087dfc.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
-svg/scale=1.0
-editor/scale_with_editor_scale=true
-editor/convert_colors_with_editor_theme=false
diff --git a/addons/script-ide/outline/icon/func.svg b/addons/script-ide/outline/icon/func.svg
deleted file mode 100644
index 5388596..0000000
--- a/addons/script-ide/outline/icon/func.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/addons/script-ide/outline/icon/func.svg.import b/addons/script-ide/outline/icon/func.svg.import
deleted file mode 100644
index 78176a8..0000000
--- a/addons/script-ide/outline/icon/func.svg.import
+++ /dev/null
@@ -1,44 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://rni04cl446ov"
-path="res://.godot/imported/func.svg-d034576bf2eed84b270a8dc1d6d64df6.ctex"
-metadata={
-"has_editor_variant": true,
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://addons/script-ide/outline/icon/func.svg"
-dest_files=["res://.godot/imported/func.svg-d034576bf2eed84b270a8dc1d6d64df6.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
-svg/scale=1.0
-editor/scale_with_editor_scale=true
-editor/convert_colors_with_editor_theme=false
diff --git a/addons/script-ide/outline/icon/func_get.svg b/addons/script-ide/outline/icon/func_get.svg
deleted file mode 100644
index 2fc6857..0000000
--- a/addons/script-ide/outline/icon/func_get.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/addons/script-ide/outline/icon/func_get.svg.import b/addons/script-ide/outline/icon/func_get.svg.import
deleted file mode 100644
index 25f4825..0000000
--- a/addons/script-ide/outline/icon/func_get.svg.import
+++ /dev/null
@@ -1,44 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c2a3aowyhxj5x"
-path="res://.godot/imported/func_get.svg-033d200e05dd58beec3becc7967b8c7d.ctex"
-metadata={
-"has_editor_variant": true,
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://addons/script-ide/outline/icon/func_get.svg"
-dest_files=["res://.godot/imported/func_get.svg-033d200e05dd58beec3becc7967b8c7d.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
-svg/scale=1.0
-editor/scale_with_editor_scale=true
-editor/convert_colors_with_editor_theme=false
diff --git a/addons/script-ide/outline/icon/func_set.svg b/addons/script-ide/outline/icon/func_set.svg
deleted file mode 100644
index d4613a3..0000000
--- a/addons/script-ide/outline/icon/func_set.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/addons/script-ide/outline/icon/func_set.svg.import b/addons/script-ide/outline/icon/func_set.svg.import
deleted file mode 100644
index 26a86e8..0000000
--- a/addons/script-ide/outline/icon/func_set.svg.import
+++ /dev/null
@@ -1,44 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bvjkrti6kj6o2"
-path="res://.godot/imported/func_set.svg-a9087ff3e7ab6aec141e34555a32a254.ctex"
-metadata={
-"has_editor_variant": true,
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://addons/script-ide/outline/icon/func_set.svg"
-dest_files=["res://.godot/imported/func_set.svg-a9087ff3e7ab6aec141e34555a32a254.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
-svg/scale=1.0
-editor/scale_with_editor_scale=true
-editor/convert_colors_with_editor_theme=false
diff --git a/addons/script-ide/outline/icon/property.svg b/addons/script-ide/outline/icon/property.svg
deleted file mode 100644
index 144cda9..0000000
--- a/addons/script-ide/outline/icon/property.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/addons/script-ide/outline/icon/property.svg.import b/addons/script-ide/outline/icon/property.svg.import
deleted file mode 100644
index cb0ad65..0000000
--- a/addons/script-ide/outline/icon/property.svg.import
+++ /dev/null
@@ -1,44 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dbwlgnwv5e8kl"
-path="res://.godot/imported/property.svg-a9a1c76c01653424a37bad3c0caee2de.ctex"
-metadata={
-"has_editor_variant": true,
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://addons/script-ide/outline/icon/property.svg"
-dest_files=["res://.godot/imported/property.svg-a9a1c76c01653424a37bad3c0caee2de.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
-svg/scale=1.0
-editor/scale_with_editor_scale=true
-editor/convert_colors_with_editor_theme=false
diff --git a/addons/script-ide/outline/icon/signal.svg b/addons/script-ide/outline/icon/signal.svg
deleted file mode 100644
index d3f97b8..0000000
--- a/addons/script-ide/outline/icon/signal.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/addons/script-ide/outline/icon/signal.svg.import b/addons/script-ide/outline/icon/signal.svg.import
deleted file mode 100644
index 53a8f37..0000000
--- a/addons/script-ide/outline/icon/signal.svg.import
+++ /dev/null
@@ -1,44 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bnccvnaloqnte"
-path="res://.godot/imported/signal.svg-65741ce388cd0a6b19f1b195a997cce3.ctex"
-metadata={
-"has_editor_variant": true,
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://addons/script-ide/outline/icon/signal.svg"
-dest_files=["res://.godot/imported/signal.svg-65741ce388cd0a6b19f1b195a997cce3.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
-svg/scale=1.0
-editor/scale_with_editor_scale=true
-editor/convert_colors_with_editor_theme=false
diff --git a/addons/script-ide/outline/outline_button.gd b/addons/script-ide/outline/outline_button.gd
deleted file mode 100644
index 1ad8351..0000000
--- a/addons/script-ide/outline/outline_button.gd
+++ /dev/null
@@ -1,25 +0,0 @@
-## Button that is used as in the outline for a concrete outline type.
-@tool
-extends Button
-
-signal right_clicked
-
-func _init() -> void:
- toggle_mode = true
- icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
-
- add_theme_stylebox_override(&"normal", StyleBoxEmpty.new())
-
- add_theme_color_override(&"icon_pressed_color", Color.WHITE)
- add_theme_color_override(&"icon_hover_color", Color.WHITE)
- add_theme_color_override(&"icon_hover_pressed_color", Color.WHITE)
- add_theme_color_override(&"icon_focus_color", Color.WHITE)
-
-func _gui_input(event: InputEvent) -> void:
- if event is InputEventMouseButton && event.pressed:
- if (event.button_index == MOUSE_BUTTON_RIGHT):
- button_pressed = true
- on_right_click()
-
-func on_right_click():
- right_clicked.emit()
diff --git a/addons/script-ide/outline/outline_button.gd.uid b/addons/script-ide/outline/outline_button.gd.uid
deleted file mode 100644
index 7ed5cb8..0000000
--- a/addons/script-ide/outline/outline_button.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c4fvj2xt46lgx
diff --git a/addons/script-ide/outline/outline_container.gd b/addons/script-ide/outline/outline_container.gd
deleted file mode 100644
index 7cb0ad8..0000000
--- a/addons/script-ide/outline/outline_container.gd
+++ /dev/null
@@ -1,544 +0,0 @@
-## The outline container shows a filter box and
-## all script members with different color coding, depending of the type.
-@tool
-extends VBoxContainer
-
-const GETTER: StringName = &"get"
-const SETTER: StringName = &"set"
-const UNDERSCORE: StringName = &"_"
-const INLINE: StringName = &"@"
-const BUILT_IN_SCRIPT: StringName = &"::GDScript"
-
-#region Outline type name
-const PROPERTY: StringName = &"property"
-const TYPE: StringName = &"type"
-
-const ENGINE_FUNCS: StringName = &"Engine Callbacks"
-const FUNCS: StringName = &"Functions"
-const SIGNALS: StringName = &"Signals"
-const EXPORTED: StringName = &"Exported Properties"
-const PROPERTIES: StringName = &"Properties"
-const CLASSES: StringName = &"Classes"
-const CONSTANTS: StringName = &"Constants"
-
-const DEFAULT_ORDER: PackedStringArray = [ENGINE_FUNCS, FUNCS, SIGNALS, EXPORTED, PROPERTIES, CONSTANTS, CLASSES]
-#endregion
-
-const OutlineButton := preload("uid://c4fvj2xt46lgx")
-
-const Plugin := preload("uid://bc0b5v66xdidn")
-
-@onready var filter_box: HBoxContainer = %FilterBox
-@onready var outline: ItemList = %Outline
-
-var plugin: Plugin
-
-#region Existing Engine controls we modify
-var outline_filter_txt: LineEdit
-#endregion
-
-#region Outline icons and buttons
-var engine_func_icon: Texture2D
-var func_icon: Texture2D
-var func_get_icon: Texture2D
-var func_set_icon: Texture2D
-var property_icon: Texture2D
-var export_icon: Texture2D
-var signal_icon: Texture2D
-var constant_icon: Texture2D
-var class_icon: Texture2D
-
-var class_btn: Button
-var constant_btn: Button
-var signal_btn: Button
-var property_btn: Button
-var export_btn: Button
-var func_btn: Button
-var engine_func_btn: Button
-#endregion
-
-var is_hide_private_members: bool = false : set = set_hide_private_members
-var outline_order: PackedStringArray : set = set_outline_order
-
-var outline_type_order: Array[OutlineType] = []
-var outline_cache: OutlineCache
-
-func _ready() -> void:
- init_icons()
- init_outline_order()
- outline.item_selected.connect(find_in_outline_and_goto)
-
- if (plugin == null):
- return
-
- engine_func_btn = create_filter_btn(engine_func_icon, ENGINE_FUNCS)
- func_btn = create_filter_btn(func_icon, FUNCS)
- signal_btn = create_filter_btn(signal_icon, SIGNALS)
- export_btn = create_filter_btn(export_icon, EXPORTED)
- property_btn = create_filter_btn(property_icon, PROPERTIES)
- class_btn = create_filter_btn(class_icon, CLASSES)
- constant_btn = create_filter_btn(constant_icon, CONSTANTS)
-
-func update():
- update_outline_cache()
- update_outline()
-
-func tab_changed():
- var is_script: bool = get_current_script() != null
- visible = is_script
-
- update()
-
-func find_in_outline_and_goto(selected_idx: int):
- var script: Script = get_current_script()
- if (!script):
- return
-
- var text: String = outline.get_item_text(selected_idx)
- var metadata: Dictionary[StringName, StringName] = outline.get_item_metadata(selected_idx)
- var modifier: StringName = metadata[&"modifier"]
- var type: StringName = metadata[TYPE]
-
- var type_with_text: String = type + " " + text
- if (type == &"func"):
- type_with_text = type_with_text.substr(0, type_with_text.find("("))
-
- var source_code: String = script.get_source_code()
- var lines: PackedStringArray = source_code.split("\n")
-
- var index: int = 0
- for line: String in lines:
- # Easy case, like 'var abc'
- if (line.begins_with(type_with_text)):
- plugin.goto_line(index)
- return
-
- # We have an modifier, e.g. 'static'
- if (modifier != &"" && line.begins_with(modifier)):
- if (line.begins_with(modifier + " " + type_with_text)):
- plugin.goto_line(index)
- return
- # Special case: An 'enum' is treated different.
- elif (modifier == &"enum" && line.contains("enum " + text)):
- plugin.goto_line(index)
- return
-
- # Hard case, probably something like '@onready var abc'
- if (type == &"var" && line.contains(type_with_text)):
- plugin.goto_line(index)
- return
-
- index += 1
-
- push_error(type_with_text + " or " + modifier + " not found in source code")
-
-## Initializes all plugin icons, while respecting the editor settings.
-func init_icons():
- engine_func_icon = create_editor_texture(load_rel("icon/engine_func.svg"))
- func_icon = create_editor_texture(load_rel("icon/func.svg"))
- func_get_icon = create_editor_texture(load_rel("icon/func_get.svg"))
- func_set_icon = create_editor_texture(load_rel("icon/func_set.svg"))
- property_icon = create_editor_texture(load_rel("icon/property.svg"))
- export_icon = create_editor_texture(load_rel("icon/export.svg"))
- signal_icon = create_editor_texture(load_rel("icon/signal.svg"))
- constant_icon = create_editor_texture(load_rel("icon/constant.svg"))
- class_icon = create_editor_texture(load_rel("icon/class.svg"))
-
-func create_filter_btn(icon: Texture2D, type: StringName) -> Button:
- var btn: OutlineButton = OutlineButton.new()
- btn.icon = icon
- btn.tooltip_text = type
-
- var property: StringName = plugin.SCRIPT_IDE + type.to_lower().replace(" ", "_")
- btn.set_meta(PROPERTY, property)
- btn.set_meta(TYPE, type)
- btn.button_pressed = plugin.get_setting(property, true)
-
- btn.toggled.connect(on_filter_button_pressed.bind(btn))
- btn.right_clicked.connect(on_right_click.bind(btn))
-
- return btn
-
-func on_right_click(btn: OutlineButton):
- btn.button_pressed = true
-
- var pressed_state: bool = false
- for child: Node in filter_box.get_children():
- var other_btn: Button = child
-
- if (btn != other_btn):
- pressed_state = pressed_state || other_btn.button_pressed
-
- for child: Node in filter_box.get_children():
- var other_btn: Button = child
-
- if (btn != other_btn):
- other_btn.button_pressed = !pressed_state
-
- outline_filter_txt.grab_focus()
-
-func on_filter_button_pressed(pressed: bool, btn: Button):
- plugin.set_setting(btn.get_meta(PROPERTY), pressed)
-
- update_outline()
-
-## Initializes the outline type structure and sorts it based off the outline order.
-func init_outline_order():
- var outline_type: OutlineType = OutlineType.new()
- outline_type.type_name = ENGINE_FUNCS
- outline_type.add_to_outline = func(): add_to_outline_if_selected(engine_func_btn,
- func(): add_to_outline(outline_cache.engine_funcs, engine_func_icon, &"func"))
- outline_type_order.append(outline_type)
-
- outline_type = OutlineType.new()
- outline_type.type_name = FUNCS
- outline_type.add_to_outline = func(): add_to_outline_if_selected(func_btn,
- func(): add_to_outline_ext(outline_cache.funcs, get_func_icon, &"func", &"static"))
- outline_type_order.append(outline_type)
-
- outline_type = OutlineType.new()
- outline_type.type_name = SIGNALS
- outline_type.add_to_outline = func(): add_to_outline_if_selected(signal_btn,
- func(): add_to_outline(outline_cache.signals, signal_icon, &"signal"))
- outline_type_order.append(outline_type)
-
- outline_type = OutlineType.new()
- outline_type.type_name = EXPORTED
- outline_type.add_to_outline = func(): add_to_outline_if_selected(export_btn,
- func(): add_to_outline(outline_cache.exports, export_icon, &"var", &"@export"))
- outline_type_order.append(outline_type)
-
- outline_type = OutlineType.new()
- outline_type.type_name = PROPERTIES
- outline_type.add_to_outline = func(): add_to_outline_if_selected(property_btn,
- func(): add_to_outline(outline_cache.properties, property_icon, &"var"))
- outline_type_order.append(outline_type)
-
- outline_type = OutlineType.new()
- outline_type.type_name = CLASSES
- outline_type.add_to_outline = func(): add_to_outline_if_selected(class_btn,
- func(): add_to_outline(outline_cache.classes, class_icon, &"class"))
- outline_type_order.append(outline_type)
-
- outline_type = OutlineType.new()
- outline_type.type_name = CONSTANTS
- outline_type.add_to_outline = func(): add_to_outline_if_selected(constant_btn,
- func(): add_to_outline(outline_cache.constants, constant_icon, &"const", &"enum"))
- outline_type_order.append(outline_type)
-
-func add_to_outline_if_selected(btn: Button, action: Callable):
- if (btn.button_pressed):
- action.call()
-
-func update_outline_button_order():
- var all_buttons: Array[Button] = [engine_func_btn, func_btn, signal_btn, export_btn, property_btn, class_btn, constant_btn]
- all_buttons.sort_custom(sort_buttons_by_outline_order)
-
- for btn: Button in all_buttons:
- if (btn.get_parent() != null):
- filter_box.remove_child(btn)
-
- for btn: Button in all_buttons:
- filter_box.add_child(btn)
-
-func sort_buttons_by_outline_order(btn1: Button, btn2: Button) -> bool:
- return sort_by_outline_order(btn1.get_meta(TYPE), btn2.get_meta(TYPE))
-
-func sort_types_by_outline_order(type1: OutlineType, type2: OutlineType) -> bool:
- return sort_by_outline_order(type1.type_name, type2.type_name)
-
-func sort_by_outline_order(outline_type1: StringName, outline_type2: StringName) -> bool:
- return outline_order.find(outline_type1) < outline_order.find(outline_type2)
-
-func get_current_script() -> Script:
- var script_editor: ScriptEditor = EditorInterface.get_script_editor()
- return script_editor.get_current_script()
-
-func update_outline_cache():
- outline_cache = null
-
- var script: Script = get_current_script()
- if (!script):
- return
-
- # Check if built-in script. In this case we need to duplicate it for whatever reason.
- if (script.get_path().contains(BUILT_IN_SCRIPT)):
- script = script.duplicate()
-
- outline_cache = OutlineCache.new()
-
- # Collect all script members.
- for_each_script_member(script, func(array: Array[String], item: String): array.append(item))
-
- # Remove script members that only exist in the base script (which includes the base of the base etc.).
- # Note: The method that only collects script members without including the base script(s)
- # is not exposed to GDScript.
- var base_script: Script = script.get_base_script()
- if (base_script != null):
- for_each_script_member(base_script, func(array: Array[String], item: String): array.erase(item))
-
-func for_each_script_member(script: Script, consumer: Callable):
- # Functions / Methods
- for dict: Dictionary in script.get_script_method_list():
- var func_name: String = dict[&"name"]
-
- if (plugin.keywords.has(func_name)):
- func_name = create_function_signature(dict)
- consumer.call(outline_cache.engine_funcs, func_name)
- else:
- if (is_hide_private_members && func_name.begins_with(UNDERSCORE)):
- continue
-
- # Inline getter/setter will normally be shown as '@...getter', '@...setter'.
- # Since we already show the variable itself, we will skip those.
- if (func_name.begins_with(INLINE)):
- continue
-
- func_name = create_function_signature(dict)
- consumer.call(outline_cache.funcs, func_name)
-
- # Properties / Exported variables
- for dict: Dictionary in script.get_script_property_list():
- var property: String = dict[&"name"]
- if (is_hide_private_members && property.begins_with(UNDERSCORE)):
- continue
-
- var usage: int = dict[&"usage"]
-
- if (usage & PROPERTY_USAGE_SCRIPT_VARIABLE):
- if (usage & PROPERTY_USAGE_STORAGE && usage & PROPERTY_USAGE_EDITOR):
- consumer.call(outline_cache.exports, property)
- else:
- consumer.call(outline_cache.properties, property)
-
- # Static variables (are separated for whatever reason)
- for dict: Dictionary in script.get_property_list():
- var property: String = dict[&"name"]
- if (is_hide_private_members && property.begins_with(UNDERSCORE)):
- continue
-
- var usage: int = dict[&"usage"]
-
- if (usage & PROPERTY_USAGE_SCRIPT_VARIABLE):
- consumer.call(outline_cache.properties, property)
-
- # Signals
- for dict: Dictionary in script.get_script_signal_list():
- var signal_name: String = dict[&"name"]
-
- consumer.call(outline_cache.signals, signal_name)
-
- # Constants / Classes
- for name_key: String in script.get_script_constant_map():
- if (is_hide_private_members && name_key.begins_with(UNDERSCORE)):
- continue
-
- var object: Variant = script.get_script_constant_map().get(name_key)
- # Inner classes have no source code, while a const of type GDScript has.
- if (object is GDScript && !object.has_source_code()):
- consumer.call(outline_cache.classes, name_key)
- else:
- consumer.call(outline_cache.constants, name_key)
-
-func update_outline():
- outline.clear()
-
- if (outline_cache == null):
- return
-
- for outline_type: OutlineType in outline_type_order:
- outline_type.add_to_outline.call()
-
-func add_to_outline(items: Array[String], icon: Texture2D, type: StringName, modifier: StringName = &""):
- add_to_outline_ext(items, func(str: String): return icon, type, modifier)
-
-func add_to_outline_ext(items: Array[String], icon_callable: Callable, type: StringName, modifier: StringName = &""):
- var text: String = outline_filter_txt.get_text()
-
- if (is_sorted()):
- items = items.duplicate()
- items.sort_custom(func(str1: String, str2: String): return str1.naturalnocasecmp_to(str2) < 0)
-
- for item: String in items:
- if (text.is_empty() || text.is_subsequence_ofn(item)):
- var icon: Texture2D = icon_callable.call(item)
- outline.add_item(item, icon, true)
-
- var dict: Dictionary[StringName, StringName] = {
- TYPE: type,
- &"modifier": modifier
- }
- outline.set_item_metadata(outline.item_count - 1, dict)
-
-func get_func_icon(func_name: String) -> Texture2D:
- var icon: Texture2D = func_icon
- if (func_name.begins_with(GETTER)):
- icon = func_get_icon
- elif (func_name.begins_with(SETTER)):
- icon = func_set_icon
-
- return icon
-
-func save_restore_filter() -> Array[bool]:
- var button_flags: Array[bool] = []
- for child: Node in filter_box.get_children():
- var btn: Button = child
- button_flags.append(btn.button_pressed)
-
- btn.set_pressed_no_signal(true)
-
- return button_flags
-
-func restore_filter(button_flags: Array[bool]):
- var index: int = 0
- for flag: bool in button_flags:
- var btn: Button = filter_box.get_child(index)
- btn.set_pressed_no_signal(flag)
- index += 1
-
-func set_outline_order(new_outline_order: PackedStringArray):
- outline_order = new_outline_order
-
- if (filter_box == null):
- return
-
- outline_type_order.sort_custom(sort_types_by_outline_order)
-
- update_outline_button_order()
- update_outline()
-
-func set_hide_private_members(new_value: bool):
- is_hide_private_members = new_value
-
- if (filter_box == null):
- return
-
- update_outline_cache()
- update_outline()
-
-func update_filter_buttons():
- # Update filter buttons.
- for btn_node: Node in filter_box.get_children():
- var btn: Button = btn_node
- var property: StringName = btn.get_meta(PROPERTY)
-
- btn.button_pressed = plugin.get_setting(property, btn.button_pressed)
-
-func create_function_signature(method: Dictionary) -> String:
- var func_name: String = method[&"name"]
- func_name += "("
-
- var args: Array = method[&"args"]
- var default_args: Array = method[&"default_args"]
-
- var arg_index: int = 0
- var default_arg_index: int = 0
- var arg_str: String = ""
- for arg: Dictionary in args:
- if (arg_str != ""):
- arg_str += ", "
-
- arg_str += arg[&"name"]
- var type: String = get_type(arg)
- if (type != ""):
- arg_str += ": " + type
-
- if (args.size() - arg_index <= default_args.size()):
- var default_arg: Variant = default_args[default_arg_index]
- if (!default_arg):
- var type_hint: int = arg[&"type"]
- if (is_dictionary(type_hint)):
- default_arg = {}
- elif (is_array(type_hint)):
- default_arg = []
-
- arg_str += " = " + var_to_str(default_arg)
-
- default_arg_index += 1
-
- arg_index += 1
-
- func_name += arg_str + ")"
-
- var return_str: String = get_type(method[&"return"])
- if (return_str == ""):
- return func_name
-
- func_name += " -> " + return_str
-
- return func_name
-
-func get_type(dict: Dictionary) -> String:
- var type: String = dict[&"class_name"]
- if (type != &""):
- if (type.begins_with(&"res://")):
- type = type.substr(type.rfind(&".") + 1)
- return type
-
- var type_hint: int = dict[&"type"]
- if (type_hint == 0):
- return &""
-
- type = type_string(type_hint)
-
- if (is_dictionary(type_hint)):
- var generic: String = dict[&"hint_string"]
- if (generic != &""):
- var generic_parts: PackedStringArray = generic.split(";")
- if (generic_parts.size() == 2):
- return type + "[" + generic_parts[0] + ", " + generic_parts[1] + "]"
-
- if (is_array(type_hint)):
- var generic: String = dict[&"hint_string"]
- if (generic != &""):
- return type + "[" + generic + "]"
-
- return type
-
-func is_dictionary(type_hint: int) -> bool:
- return type_hint == 27
-
-func is_array(type_hint: int) -> bool:
- return type_hint == 28
-
-func reset_icons():
- init_icons()
- engine_func_btn.icon = engine_func_icon
- func_btn.icon = func_icon
- signal_btn.icon = signal_icon
- export_btn.icon = export_icon
- property_btn.icon = property_icon
- class_btn.icon = class_icon
- constant_btn.icon = constant_icon
- update_outline()
-
-func create_editor_texture(texture: Texture2D) -> Texture2D:
- var image: Image = texture.get_image().duplicate()
- image.adjust_bcs(1.0, 1.0, get_editor_icon_saturation())
-
- return ImageTexture.create_from_image(image)
-
-func load_rel(path: String) -> Variant:
- var script_path: String = get_script().get_path().get_base_dir()
- return load(script_path.path_join(path))
-
-func is_sorted() -> bool:
- return EditorInterface.get_editor_settings().get_setting("text_editor/script_list/sort_members_outline_alphabetically")
-
-func get_editor_icon_saturation() -> float:
- return EditorInterface.get_editor_settings().get_setting("interface/theme/icon_saturation")
-
-## Cache for everything inside we collected to show in the Outline.
-class OutlineCache:
- var classes: Array[String] = []
- var constants: Array[String] = []
- var signals: Array[String] = []
- var exports: Array[String] = []
- var properties: Array[String] = []
- var funcs: Array[String] = []
- var engine_funcs: Array[String] = []
-
-## Outline type for a concrete button with their items in the Outline.
-class OutlineType:
- var type_name: StringName
- var add_to_outline: Callable
diff --git a/addons/script-ide/outline/outline_container.gd.uid b/addons/script-ide/outline/outline_container.gd.uid
deleted file mode 100644
index d40252f..0000000
--- a/addons/script-ide/outline/outline_container.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://db0be00ai3tfi
diff --git a/addons/script-ide/outline/outline_container.tscn b/addons/script-ide/outline/outline_container.tscn
deleted file mode 100644
index 219e0bc..0000000
--- a/addons/script-ide/outline/outline_container.tscn
+++ /dev/null
@@ -1,22 +0,0 @@
-[gd_scene format=3 uid="uid://ux3ldi4ka8ji"]
-
-[ext_resource type="Script" uid="uid://db0be00ai3tfi" path="res://addons/script-ide/outline/outline_container.gd" id="1_w4vhe"]
-
-[node name="OutlineContainer" type="VBoxContainer" unique_id=181236283]
-offset_right = 174.0
-offset_bottom = 253.0
-size_flags_horizontal = 3
-size_flags_vertical = 3
-script = ExtResource("1_w4vhe")
-
-[node name="FilterBox" type="HBoxContainer" parent="." unique_id=1653721963]
-unique_name_in_owner = true
-layout_mode = 2
-theme_override_constants/separation = 2
-
-[node name="Outline" type="ItemList" parent="." unique_id=160258583]
-unique_name_in_owner = true
-auto_translate_mode = 2
-layout_mode = 2
-size_flags_vertical = 3
-allow_reselect = true
diff --git a/addons/script-ide/override/override_popup.gd b/addons/script-ide/override/override_popup.gd
deleted file mode 100644
index 1421707..0000000
--- a/addons/script-ide/override/override_popup.gd
+++ /dev/null
@@ -1,272 +0,0 @@
-## The override panel does show all functions that come from the super class (extends)
-## that can be overridden by the script this is called on.
-@tool
-extends PopupPanel
-
-const FUNC_META: StringName = &"func"
-
-const Plugin := preload("uid://bc0b5v66xdidn")
-const OutlineContainer := preload("uid://db0be00ai3tfi")
-
-@onready var filter_txt: LineEdit = %FilterTxt
-@onready var class_func_tree: Tree = %ClassFuncTree
-@onready var ok_btn: Button = %OkBtn
-@onready var cancel_btn: Button = %CancelBtn
-
-var plugin: Plugin
-
-var outline_container: OutlineContainer
-
-var selections: Dictionary[String, bool] = {} # Used as Set.
-var class_to_functions: Dictionary[StringName, PackedStringArray]
-
-func _ready() -> void:
- filter_txt.text_changed.connect(update_tree_filter.unbind(1))
-
- class_func_tree.multi_selected.connect(func(item: TreeItem, col: int, selected: bool): save_selection(selected, item))
- class_func_tree.item_activated.connect(generate_functions)
-
- cancel_btn.pressed.connect(hide)
- ok_btn.pressed.connect(generate_functions)
-
- about_to_popup.connect(on_show)
-
- filter_txt.gui_input.connect(navigate_on_tree)
-
-func navigate_on_tree(event: InputEvent):
- if (event.is_action_pressed(&"ui_select", true)):
- var selected: TreeItem = get_selected_tree_item()
- if (selected == null):
- return
-
- class_func_tree.accept_event()
-
- if (!selected.is_selectable(0)):
- selected.collapsed = !selected.collapsed
- return
-
- if (selected.is_selected(0)):
- selected.deselect(0)
- save_selection(false, selected)
- else:
- selected.select(0)
- save_selection(true, selected)
- elif (event.is_action_pressed(&"ui_text_submit", true)):
- class_func_tree.accept_event()
-
- if (selections.size() == 0):
- return
-
- generate_functions()
- elif (event.is_action_pressed(&"ui_down", true)):
- var selected: TreeItem = get_selected_tree_item()
- if (selected == null):
- return
- var item: TreeItem = selected.get_next_in_tree()
- if (item == null):
- return
-
- focus_tree_item(item)
- elif (event.is_action_pressed(&"ui_up", true)):
- var selected: TreeItem = get_selected_tree_item()
- if (selected == null):
- return
- var item: TreeItem = selected.get_prev_in_tree()
- if (item == null):
- return
-
- focus_tree_item(item)
- elif (event.is_action_pressed(&"ui_page_down", true)):
- var selected: TreeItem = get_selected_tree_item()
- if (selected == null):
- return
-
- var item: TreeItem = selected.get_next_in_tree()
- if (item == null):
- return
-
- for index: int in 4:
- var next: TreeItem = item.get_next_in_tree()
- if (next == null):
- break
- item = next
-
- focus_tree_item(item)
- elif (event.is_action_pressed(&"ui_page_up", true)):
- var selected: TreeItem = get_selected_tree_item()
- if (selected == null):
- return
-
- var item: TreeItem = selected.get_prev_in_tree()
- if (item == null):
- return
-
- for index: int in 4:
- var prev: TreeItem = item.get_prev_in_tree()
- if (prev == null):
- break
- item = prev
-
- focus_tree_item(item)
-
-func get_selected_tree_item() -> TreeItem:
- var selected: TreeItem = class_func_tree.get_selected()
- if (selected == null):
- selected = class_func_tree.get_root()
- return selected
-
-func focus_tree_item(item: TreeItem):
- var was_selected: bool = item.is_selected(0)
- item.select(0)
- item.deselect(0)
- if (was_selected):
- item.select(0)
-
- class_func_tree.ensure_cursor_is_visible()
- class_func_tree.accept_event()
-
-func update_tree_filter():
- update_tree()
-
-func save_selection(selected: bool, item: TreeItem):
- if (selected):
- selections[item.get_text(0)] = true
- else:
- selections.erase(item.get_text(0))
-
- ok_btn.disabled = selections.size() == 0
-
-func on_show():
- class_func_tree.clear()
- selections.clear()
- ok_btn.disabled = true
- filter_txt.text = &""
-
- var script: Script = EditorInterface.get_script_editor().get_current_script()
- class_to_functions = collect_all_class_functions(script)
- if (class_to_functions.is_empty()):
- return
-
- update_tree()
- filter_txt.grab_focus()
-
-func update_tree():
- class_func_tree.clear()
-
- var text: String = filter_txt.text
-
- var root: TreeItem = class_func_tree.create_item()
- for class_name_str: StringName in class_to_functions.keys():
- var class_item: TreeItem = root.create_child(0)
- class_item.set_selectable(0, false)
- class_item.set_text(0, class_name_str)
-
- for function: String in class_to_functions.get(class_name_str):
- var is_preselected: bool = selections.has(function)
- if (is_preselected || text.is_empty() || text.is_subsequence_ofn(function)):
- var func_item: TreeItem = class_item.create_child()
- func_item.set_text(0, function)
- if (plugin.keywords.has(function.get_slice("(", 0))):
- func_item.set_icon(0, outline_container.engine_func_icon)
- else:
- func_item.set_icon(0, outline_container.func_icon)
-
- if (is_preselected):
- func_item.select(0)
-
-func collect_all_class_functions(script: Script) -> Dictionary[StringName, PackedStringArray]:
- var existing_funcs: Dictionary[String, bool] = {} # Used as Set.
- for func_str: String in outline_container.outline_cache.engine_funcs:
- existing_funcs[func_str] = true
- for func_str: String in outline_container.outline_cache.funcs:
- existing_funcs[func_str] = true
-
- var class_to_functions: Dictionary[StringName, PackedStringArray] = collect_super_class_functions(script.get_base_script(), existing_funcs)
- var native_class_to_functions: Dictionary[StringName, PackedStringArray] = collect_native_class_functions(script.get_instance_base_type(), existing_funcs)
-
- return native_class_to_functions.merged(class_to_functions)
-
-func collect_super_class_functions(base_script: Script, existing_funcs: Dictionary[String, bool]) -> Dictionary[StringName, PackedStringArray]:
- var super_classes: Array[Script] = []
- while (base_script != null):
- super_classes.insert(0, base_script)
-
- base_script = base_script.get_base_script()
-
- var class_to_functions: Dictionary[StringName, PackedStringArray] = {}
- for super_class: Script in super_classes:
- var functions: PackedStringArray = collect_script_functions(super_class, existing_funcs)
- if (functions.is_empty()):
- continue
-
- class_to_functions[super_class.get_global_name()] = functions
-
- return class_to_functions
-
-func collect_native_class_functions(native_class: StringName, existing_funcs: Dictionary[String, bool]) -> Dictionary[StringName, PackedStringArray]:
- var super_native_classes: Array[StringName] = []
- while (native_class != &""):
- super_native_classes.insert(0, native_class)
-
- native_class = ClassDB.get_parent_class(native_class)
-
- var class_to_functions: Dictionary[StringName, PackedStringArray] = {}
- for super_native_class: StringName in super_native_classes:
- var functions: PackedStringArray = collect_class_functions(super_native_class, existing_funcs)
- if (functions.is_empty()):
- continue
-
- class_to_functions[super_native_class] = functions
-
- return class_to_functions
-
-func collect_class_functions(native_class: StringName, existing_funcs: Dictionary[String, bool]) -> PackedStringArray:
- var functions: PackedStringArray = []
-
- for method: Dictionary in ClassDB.class_get_method_list(native_class, true):
- if (method[&"flags"] & METHOD_FLAG_VIRTUAL <= 0):
- continue
-
- var func_name: String = method[&"name"]
- if (existing_funcs.has(func_name)):
- continue
-
- func_name = outline_container.create_function_signature(method)
- functions.append(func_name)
-
- return functions
-
-func collect_script_functions(super_class: Script, existing_funcs: Dictionary[String, bool]) -> PackedStringArray:
- var functions: PackedStringArray = []
-
- for method: Dictionary in super_class.get_script_method_list():
- var func_name: String = method[&"name"]
- if (existing_funcs.has(func_name)):
- continue
-
- existing_funcs[func_name] = true
-
- func_name = outline_container.create_function_signature(method)
- functions.append(func_name)
-
- return functions
-
-func generate_functions():
- if (selections.size() == 0):
- return
-
- var generated_text: String = ""
- for function: String in selections.keys():
- if (function.ends_with(")")):
- function = function + " -> void"
- generated_text += "\nfunc " + function + ":\n\tpass\n"
-
- var editor: CodeEdit = EditorInterface.get_script_editor().get_current_editor().get_base_editor()
- editor.text += generated_text
-
- plugin.goto_line(editor.get_line_count() - 1)
-
- # Deferred as otherwise we get weird errors in the console.
- # Probably due to this beeing called in a signal and auto unparent is true.
- # 100% Engine bug or at least weird behavior.
- hide.call_deferred()
diff --git a/addons/script-ide/override/override_popup.gd.uid b/addons/script-ide/override/override_popup.gd.uid
deleted file mode 100644
index b27ab69..0000000
--- a/addons/script-ide/override/override_popup.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c4w55j4jswg2t
diff --git a/addons/script-ide/override/override_popup.tscn b/addons/script-ide/override/override_popup.tscn
deleted file mode 100644
index 7028459..0000000
--- a/addons/script-ide/override/override_popup.tscn
+++ /dev/null
@@ -1,60 +0,0 @@
-[gd_scene format=3 uid="uid://bb1n82qxlqanh"]
-
-[ext_resource type="Script" uid="uid://c4w55j4jswg2t" path="res://addons/script-ide/override/override_popup.gd" id="1_54se3"]
-
-[node name="OverridePanel" type="PopupPanel" unique_id=145780904]
-oversampling_override = 1.0
-size = Vector2i(317, 122)
-visible = true
-script = ExtResource("1_54se3")
-
-[node name="PanelContainer" type="PanelContainer" parent="." unique_id=1028771242]
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-offset_left = 4.0
-offset_top = 4.0
-offset_right = -4.0
-offset_bottom = -4.0
-grow_horizontal = 2
-grow_vertical = 2
-
-[node name="MarginContainer" type="MarginContainer" parent="PanelContainer" unique_id=701240329]
-layout_mode = 2
-
-[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer" unique_id=46770551]
-layout_mode = 2
-theme_override_constants/separation = 4
-
-[node name="Label" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer" unique_id=927995895]
-layout_mode = 2
-text = "Select Functions to Override/Implement"
-
-[node name="FilterTxt" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer" unique_id=759695003]
-unique_name_in_owner = true
-layout_mode = 2
-placeholder_text = "Filter Methods"
-
-[node name="ClassFuncTree" type="Tree" parent="PanelContainer/MarginContainer/VBoxContainer" unique_id=1883825598]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_vertical = 3
-hide_root = true
-select_mode = 2
-
-[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer" unique_id=1036676649]
-layout_mode = 2
-theme_override_constants/separation = 4
-alignment = 2
-
-[node name="OkBtn" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer" unique_id=598122006]
-unique_name_in_owner = true
-custom_minimum_size = Vector2(64, 0)
-layout_mode = 2
-text = "Ok"
-
-[node name="CancelBtn" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer" unique_id=1978369329]
-unique_name_in_owner = true
-custom_minimum_size = Vector2(64, 0)
-layout_mode = 2
-text = "Cancel"
diff --git a/addons/script-ide/plugin.cfg b/addons/script-ide/plugin.cfg
deleted file mode 100644
index aaeb1f3..0000000
--- a/addons/script-ide/plugin.cfg
+++ /dev/null
@@ -1,7 +0,0 @@
-[plugin]
-
-name="Script-IDE"
-description="Transforms the Script UI into an IDE like UI. Tabs are used for navigating between scripts. The default Outline got an overhaul and now shows all members of the script (not just methods) with unique icons for faster navigation. Enhanced keyboard navigation for Scripts and Outline. Fast quick search functionality."
-author="Marius Hanl"
-version="2.2.5"
-script="plugin.gd"
diff --git a/addons/script-ide/plugin.gd b/addons/script-ide/plugin.gd
deleted file mode 100644
index 37dad96..0000000
--- a/addons/script-ide/plugin.gd
+++ /dev/null
@@ -1,940 +0,0 @@
-## Copyright (c) 2023-present Marius Hanl under the MIT License.
-## The editor plugin entrypoint for Script-IDE.
-##
-## Some features interfere with the editor code that is inside 'script_editor_plugin.cpp'.
-## That is, the original structure is changed by this plugin, in order to support everything.
-## The internals of the native C++ code are therefore important in order to make this plugin work
-## without interfering with the Engine itself (and their Node's).
-##
-## Script-IDE does not use global class_name's in order to not clutter projects using it.
-## Especially since this is an editor only plugin, we do not want this plugin in the final game.
-## Therefore, components that references the plugin is untyped.
-@tool
-extends EditorPlugin
-
-const BUILT_IN_SCRIPT: StringName = &"::GDScript"
-const QUICK_OPEN_INTERVAL: float = 400.0
-
-const MULTILINE_TAB_BAR: PackedScene = preload("uid://vjuhunm2uboy")
-const MultilineTabBar := preload("uid://l1rdargfn67o")
-
-const QUICK_OPEN_SCENE: PackedScene = preload("uid://d2pttchmj3n7q")
-const QuickOpenPopup := preload("uid://cgwp4rl1udqbb")
-
-const OVERRIDE_SCENE: PackedScene = preload("uid://bb1n82qxlqanh")
-const OverridePopup := preload("uid://c4w55j4jswg2t")
-
-const OUTLINE_CONTAINER_SCENE: PackedScene = preload("uid://ux3ldi4ka8ji")
-const OutlineContainer := preload("uid://db0be00ai3tfi")
-
-const SplitCodeEdit := preload("uid://boy48rhhyrph")
-
-#region Settings and Shortcuts
-## Editor setting path
-const SCRIPT_IDE: StringName = &"plugin/script_ide/"
-## Editor setting for the outline position
-const OUTLINE_POSITION_RIGHT: StringName = SCRIPT_IDE + &"outline_position_right"
-## Editor setting to control the order of the outline
-const OUTLINE_ORDER: StringName = SCRIPT_IDE + &"outline_order"
-## Editor setting to control whether private members (annotated with '_' should be hidden or not)
-const HIDE_PRIVATE_MEMBERS: StringName = SCRIPT_IDE + &"hide_private_members"
-## Editor setting to control whether we want to auto navigate to the script
-## in the filesystem (dock) when selected
-const AUTO_NAVIGATE_IN_FS: StringName = SCRIPT_IDE + &"auto_navigate_in_filesystem_dock"
-## Editor setting to control whether the script list should be visible or not
-const SCRIPT_LIST_VISIBLE: StringName = SCRIPT_IDE + &"script_list_visible"
-## Editor setting to control whether the script tabs should be visible or not.
-const SCRIPT_TABS_VISIBLE: StringName = SCRIPT_IDE + &"script_tabs_visible"
-## Editor setting to control where the script tabs should be.
-const SCRIPT_TABS_POSITION_TOP: StringName = SCRIPT_IDE + &"script_tabs_position_top"
-## Editor setting to control if all script tabs should have close button.
-const SCRIPT_TABS_CLOSE_BUTTON_ALWAYS: StringName = SCRIPT_IDE + &"script_tabs_close_button_always"
-## Editor setting to control if all tabs should be shown in a single line.
-const SCRIPT_TABS_SINGLELINE: StringName = SCRIPT_IDE + &"script_tabs_singleline"
-
-## Editor setting for the 'Open Outline Popup' shortcut
-const OPEN_OUTLINE_POPUP: StringName = SCRIPT_IDE + &"open_outline_popup"
-## Editor setting for the 'Open Scripts Popup' shortcut
-const OPEN_SCRIPTS_POPUP: StringName = SCRIPT_IDE + &"open_scripts_popup"
-
-## Editor setting for the 'Open Override Popup' shortcut
-const OPEN_OVERRIDE_POPUP: StringName = SCRIPT_IDE + &"open_override_popup"
-## Editor setting for the 'Tab cycle forward' shortcut
-const TAB_CYCLE_FORWARD: StringName = SCRIPT_IDE + &"tab_cycle_forward"
-## Editor setting for the 'Tab cycle backward' shortcut
-const TAB_CYCLE_BACKWARD: StringName = SCRIPT_IDE + &"tab_cycle_backward"
-
-## Editor setting for the 'Open Quick Search Popup (All)' shortcut
-const OPEN_QUICK_SEARCH_POPUP: StringName = SCRIPT_IDE + &"open_quick_search_popup"
-## Editor setting for the 'Open Quick Search Popup (Scenes)' shortcut
-const OPEN_QUICK_SEARCH_POPUP_SCENES: StringName = SCRIPT_IDE + &"open_quick_search_popup_scenes"
-## Editor setting for the 'Open Quick Search Popup (Scripts)' shortcut
-const OPEN_QUICK_SEARCH_POPUP_GDSCRIPTS: StringName = SCRIPT_IDE + &"open_quick_search_popup_scripts"
-## Editor setting for the 'Open Quick Search Popup (Resources)' shortcut
-const OPEN_QUICK_SEARCH_POPUP_RESOURCES: StringName = SCRIPT_IDE + &"open_quick_search_popup_resources"
-
-## Engine editor setting for the icon saturation, so our icons can react.
-const ICON_SATURATION: StringName = &"interface/theme/icon_saturation"
-## Engine editor setting for the show members functionality.
-const SHOW_MEMBERS: StringName = &"text_editor/script_list/show_members_overview"
-## Engine editor setting if monospace font should be used for the outline.
-const MONOSPACE_OUTLINE: StringName = &"interface/theme/use_monospace_font_for_editor_symbols"
-
-## We track the user setting, so we can restore it properly.
-var show_members: bool = true
-#endregion
-
-#region Editor settings
-var is_outline_right: bool = true
-var is_script_tabs_top: bool = true
-
-var is_auto_navigate_in_fs: bool = true
-var is_script_list_visible: bool = false
-
-var open_outline_popup_shc: Shortcut
-var open_scripts_popup_shc: Shortcut
-var open_quick_search_popup_shc: Shortcut
-var open_quick_search_popup_scenes_shc: Shortcut
-var open_quick_search_popup_gdscripts_shc: Shortcut
-var open_quick_search_popup_resources_shc: Shortcut
-var open_override_popup_shc: Shortcut
-var tab_cycle_forward_shc: Shortcut
-var tab_cycle_backward_shc: Shortcut
-#endregion
-
-#region Existing Engine controls we modify
-var script_editor_split_container: HSplitContainer
-var files_panel: Control
-
-var old_scripts_tab_container: TabContainer
-var old_scripts_tab_bar: TabBar
-
-var script_filter_txt: LineEdit
-var scripts_item_list: ItemList
-var script_panel_split_container: VSplitContainer
-
-var old_outline: Control
-var outline_filter_txt: LineEdit
-var sort_btn: Button
-#endregion
-
-#region Own controls we add
-var outline_container: OutlineContainer
-var outline_popup: PopupPanel
-var multiline_tab_bar: MultilineTabBar
-var scripts_popup: PopupPanel
-var quick_open_popup: QuickOpenPopup
-var override_popup: OverridePopup
-
-var tab_splitter: HSplitContainer
-#endregion
-
-#region Plugin variables
-var keywords: Dictionary[String, bool] = {} # Used as Set.
-
-var old_script_editor_base: ScriptEditorBase
-var old_script_type: StringName
-
-var is_script_changed: bool = false
-var file_to_navigate: String = &""
-
-var quick_open_tween: Tween
-
-var suppress_settings_sync: bool = false
-#endregion
-
-#region Plugin Enter / Exit setup
-## Change the Engine script UI and transform into an IDE like UI
-func _enter_tree() -> void:
- init_settings()
- init_shortcuts()
- setup_change_listener()
-
- # --- Files Panel - Start --- #
- var script_editor: ScriptEditor = EditorInterface.get_script_editor()
- script_editor_split_container = find_or_null(script_editor.find_children("*", "HSplitContainer", true, false))
- files_panel = script_editor_split_container.get_child(0)
-
- # The 'Filter Scripts' Panel
- var upper_files_panel: Control = files_panel.get_child(0)
- # The 'Filter Methods' Panel
- var lower_files_panel: Control = files_panel.get_child(1)
-
- # Change script item list visibility (based on settings).
- scripts_item_list = find_or_null(upper_files_panel.find_children("*", "ItemList", true, false))
- scripts_item_list.allow_reselect = true
- scripts_item_list.item_selected.connect(hide_scripts_popup.unbind(1))
- update_script_list_visibility()
-
- # Add script filter navigation.
- script_filter_txt = find_or_null(scripts_item_list.get_parent().find_children("*", "LineEdit", true, false))
- script_filter_txt.gui_input.connect(navigate_on_list.bind(scripts_item_list, select_script))
- # --- Files Panel - End --- #
-
- # --- Outline - Start --- #
- var item_lists: Array[Node] = lower_files_panel.find_children("*", "ItemList", true, false)
- if (item_lists.is_empty()):
- # Godot >= 4.8.dev4, outline is a Tree now.
- old_outline = find_or_null(lower_files_panel.find_children("*", "Tree", true, false))
- else:
- old_outline = find_or_null(item_lists)
- lower_files_panel.remove_child(old_outline)
-
- outline_container = OUTLINE_CONTAINER_SCENE.instantiate()
- outline_container.plugin = self
-
- # Add navigation to the filter and text filtering.
- outline_filter_txt = find_or_null(lower_files_panel.find_children("*", "LineEdit", true, false))
- outline_filter_txt.gui_input.connect(navigate_in_outline)
- outline_filter_txt.text_changed.connect(update_outline.unbind(1))
- outline_container.outline_filter_txt = outline_filter_txt
-
- lower_files_panel.add_child(outline_container)
-
- # Add callback when the sorting changed.
- sort_btn = find_or_null(lower_files_panel.find_children("*", "Button", true, false))
- sort_btn.pressed.connect(update_outline)
-
- outline_container.is_hide_private_members = get_setting(HIDE_PRIVATE_MEMBERS, outline_container.is_hide_private_members)
- outline_container.outline_order = get_outline_order()
-
- update_outline_font()
- update_outline_position()
- # --- Outline - End --- #
-
- # --- Tabs - Start --- #
- old_scripts_tab_container = find_or_null(script_editor.find_children("*", "TabContainer", true, false))
- old_scripts_tab_bar = old_scripts_tab_container.get_tab_bar()
- old_scripts_tab_bar.tab_changed.connect(on_tab_changed)
-
- var tab_container_parent: Control = old_scripts_tab_container.get_parent()
- tab_splitter = HSplitContainer.new()
- tab_splitter.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- tab_splitter.size_flags_vertical = Control.SIZE_EXPAND_FILL
-
- tab_container_parent.add_child(tab_splitter)
- tab_container_parent.move_child(tab_splitter, 0)
- old_scripts_tab_container.reparent(tab_splitter)
-
- # When something changed, we need to sync our own tab container.
- old_scripts_tab_container.child_order_changed.connect(notify_order_changed)
-
- multiline_tab_bar = MULTILINE_TAB_BAR.instantiate()
- multiline_tab_bar.plugin = self
- multiline_tab_bar.scripts_item_list = scripts_item_list
- multiline_tab_bar.script_filter_txt = script_filter_txt
- multiline_tab_bar.scripts_tab_container = old_scripts_tab_container
-
- tab_container_parent.add_theme_constant_override(&"separation", 0)
- tab_container_parent.add_child(multiline_tab_bar)
-
- multiline_tab_bar.split_toggled.connect(toggle_split_view)
-
- multiline_tab_bar.show_close_button_always = get_setting(SCRIPT_TABS_CLOSE_BUTTON_ALWAYS, multiline_tab_bar.show_close_button_always)
- multiline_tab_bar.is_singleline_tabs = get_setting(SCRIPT_TABS_SINGLELINE, multiline_tab_bar.is_singleline_tabs)
- multiline_tab_bar.visible = get_setting(SCRIPT_TABS_VISIBLE, true)
-
- update_tabs_position()
-
- # Create and set script popup.
- script_panel_split_container = scripts_item_list.get_parent().get_parent()
- create_set_scripts_popup()
- # --- Tabs - End --- #
-
- on_tab_changed(old_scripts_tab_bar.current_tab)
-
-## Restore the old Engine script UI and free everything we created
-func _exit_tree() -> void:
- var file_system: EditorFileSystem = EditorInterface.get_resource_filesystem()
- file_system.filesystem_changed.disconnect(schedule_update)
- get_editor_settings().settings_changed.disconnect(sync_settings)
-
- if (tab_splitter != null):
- var tab_container_parent: Control = tab_splitter.get_parent()
- old_scripts_tab_container.reparent(tab_container_parent)
- tab_container_parent.move_child(old_scripts_tab_container, 1)
- tab_splitter.free()
-
- if (script_editor_split_container != null):
- if (script_editor_split_container != files_panel.get_parent()):
- script_editor_split_container.add_child(files_panel)
-
- # Try to restore the previous split offset.
- if (is_outline_right):
- var split_offset: float = script_editor_split_container.get_child(1).size.x
- script_editor_split_container.split_offset = split_offset
-
- script_editor_split_container.move_child(files_panel, 0)
-
- outline_filter_txt.gui_input.disconnect(navigate_in_outline)
- outline_filter_txt.text_changed.disconnect(update_outline)
- sort_btn.pressed.disconnect(update_outline)
-
- var outline_parent: Control = outline_container.get_parent()
- outline_parent.remove_child(outline_container)
- outline_parent.add_child(old_outline)
- outline_parent.move_child(old_outline, outline_filter_txt.get_index() + 1)
-
- outline_container.free()
-
- if (old_scripts_tab_bar != null):
- old_scripts_tab_bar.tab_changed.disconnect(on_tab_changed)
-
- if (old_scripts_tab_container != null):
- old_scripts_tab_container.child_order_changed.disconnect(notify_order_changed)
- old_scripts_tab_container.get_parent().remove_theme_constant_override(&"separation")
- old_scripts_tab_container.get_parent().remove_child(multiline_tab_bar)
-
- if (multiline_tab_bar != null):
- multiline_tab_bar.free_tabs()
- multiline_tab_bar.free()
- scripts_popup.free()
-
- if (scripts_item_list != null):
- scripts_item_list.allow_reselect = false
- scripts_item_list.item_selected.disconnect(hide_scripts_popup)
- scripts_item_list.get_parent().visible = true
-
- if (script_filter_txt != null):
- script_filter_txt.gui_input.disconnect(navigate_on_list)
-
- if (outline_popup != null):
- outline_popup.free()
- if (quick_open_popup != null):
- quick_open_popup.free()
- if (override_popup != null):
- override_popup.free()
-
- if (!show_members):
- set_setting(SHOW_MEMBERS, show_members)
-
-func _set_window_layout(configuration: ConfigFile):
- script_editor_split_container.split_offset = configuration.get_value("ScriptEditor", "script_split_offset", 200)
-#endregion
-
-#region Plugin and Shortcut processing
-## Lazy pattern to update the editor only once per frame
-func _process(delta: float) -> void:
- update_editor()
- set_process(false)
-
-## Process the user defined shortcuts
-func _shortcut_input(event: InputEvent) -> void:
- if (!event.is_pressed() || event.is_echo()):
- return
-
- if (open_outline_popup_shc.matches_event(event)):
- get_viewport().set_input_as_handled()
- open_outline_popup()
- elif (open_scripts_popup_shc.matches_event(event)):
- get_viewport().set_input_as_handled()
- open_scripts_popup()
- elif (open_quick_search_popup_shc.matches_event(event)):
- if (quick_open_tween != null && quick_open_tween.is_running()):
- get_viewport().set_input_as_handled()
- if (quick_open_tween != null):
- quick_open_tween.kill()
-
- quick_open_tween = create_tween()
- quick_open_tween.tween_interval(0.2)
- quick_open_tween.tween_callback(open_quick_search_popup)
- quick_open_tween.tween_callback(func(): quick_open_tween = null)
- else:
- quick_open_tween = create_tween()
- quick_open_tween.tween_interval(QUICK_OPEN_INTERVAL / 1000.0)
- quick_open_tween.tween_callback(func(): quick_open_tween = null)
- elif (open_override_popup_shc.matches_event(event)):
- get_viewport().set_input_as_handled()
- open_override_popup()
- elif (open_quick_search_popup_scenes_shc.matches_event(event)):
- open_quick_search_popup(QuickOpenPopup.Category.SCENES)
- elif (open_quick_search_popup_gdscripts_shc.matches_event(event)):
- open_quick_search_popup(QuickOpenPopup.Category.GDSCRIPTS)
- elif (open_quick_search_popup_resources_shc.matches_event(event)):
- open_quick_search_popup(QuickOpenPopup.Category.RESOURCES)
-
-## May cancels the quick search shortcut timer.
-func _input(event: InputEvent) -> void:
- if (event is InputEventKey):
- if (!open_quick_search_popup_shc.matches_event(event)):
- if (quick_open_tween != null):
- quick_open_tween.kill()
- quick_open_tween = null
-#endregion
-
-#region Settings and Shortcut initialization
-
-## Setups the listener (connect) to the properties we need to react to.
-func setup_change_listener():
- # Update on filesystem changed (e.g. save operation).
- var file_system: EditorFileSystem = EditorInterface.get_resource_filesystem()
- file_system.filesystem_changed.connect(schedule_update)
-
- # Sync settings changes for this plugin.
- get_editor_settings().settings_changed.connect(sync_settings)
-
-## Initializes all settings.
-## Every setting can be changed while this plugin is active, which will override them.
-func init_settings():
- is_script_list_visible = get_setting(SCRIPT_LIST_VISIBLE, is_script_list_visible)
- is_script_tabs_top = get_setting(SCRIPT_TABS_POSITION_TOP, is_script_tabs_top)
- is_outline_right = get_setting(OUTLINE_POSITION_RIGHT, is_outline_right)
- is_auto_navigate_in_fs = get_setting(AUTO_NAVIGATE_IN_FS, is_auto_navigate_in_fs)
-
- # Users may disabled this, but with this plugin, we want to show the new Outline.
- # So we need to reenable it, but restore the old value on exit.
- show_members = get_setting(SHOW_MEMBERS, true)
- if (!show_members):
- set_setting(SHOW_MEMBERS, true)
-
-## Initializes all shortcuts.
-## Every shortcut can be changed while this plugin is active, which will override them.
-func init_shortcuts():
- var editor_settings: EditorSettings = get_editor_settings()
- if (!editor_settings.has_setting(OPEN_OUTLINE_POPUP)):
- var shortcut: Shortcut = Shortcut.new()
- var event: InputEventKey = InputEventKey.new()
- event.device = -1
- event.command_or_control_autoremap = true
- event.keycode = KEY_O
-
- shortcut.events = [ event ]
- editor_settings.set_setting(OPEN_OUTLINE_POPUP, shortcut)
-
- if (!editor_settings.has_setting(OPEN_SCRIPTS_POPUP)):
- var shortcut: Shortcut = Shortcut.new()
- var event: InputEventKey = InputEventKey.new()
- event.device = -1
- event.command_or_control_autoremap = true
- event.keycode = KEY_U
-
- shortcut.events = [ event ]
- editor_settings.set_setting(OPEN_SCRIPTS_POPUP, shortcut)
-
- if (!editor_settings.has_setting(OPEN_QUICK_SEARCH_POPUP)):
- var shortcut: Shortcut = Shortcut.new()
- var event: InputEventKey = InputEventKey.new()
- event.device = -1
- event.keycode = KEY_SHIFT
-
- shortcut.events = [ event ]
- editor_settings.set_setting(OPEN_QUICK_SEARCH_POPUP, shortcut)
-
- if (!editor_settings.has_setting(OPEN_QUICK_SEARCH_POPUP_SCENES)):
- var shortcut: Shortcut = Shortcut.new()
- var event: InputEventKey = InputEventKey.new()
- event.device = -1
- event.command_or_control_autoremap = true
- event.keycode = KEY_N
-
- shortcut.events = [ event ]
- editor_settings.set_setting(OPEN_QUICK_SEARCH_POPUP_SCENES, shortcut)
-
- if (!editor_settings.has_setting(OPEN_QUICK_SEARCH_POPUP_GDSCRIPTS)):
- var shortcut: Shortcut = Shortcut.new()
- var event: InputEventKey = InputEventKey.new()
- event.device = -1
- event.command_or_control_autoremap = true
- event.shift_pressed = true
- event.keycode = KEY_N
-
- shortcut.events = [ event ]
- editor_settings.set_setting(OPEN_QUICK_SEARCH_POPUP_GDSCRIPTS, shortcut)
-
- if (!editor_settings.has_setting(OPEN_QUICK_SEARCH_POPUP_RESOURCES)):
- var shortcut: Shortcut = Shortcut.new()
- var event: InputEventKey = InputEventKey.new()
- event.device = -1
- event.command_or_control_autoremap = true
- event.alt_pressed = true
- event.shift_pressed = true
- event.keycode = KEY_N
-
- shortcut.events = [ event ]
- editor_settings.set_setting(OPEN_QUICK_SEARCH_POPUP_RESOURCES, shortcut)
-
- if (!editor_settings.has_setting(OPEN_OVERRIDE_POPUP)):
- var shortcut: Shortcut = Shortcut.new()
- var event: InputEventKey = InputEventKey.new()
- event.device = -1
- event.keycode = KEY_INSERT
- event.alt_pressed = true
-
- shortcut.events = [ event ]
- editor_settings.set_setting(OPEN_OVERRIDE_POPUP, shortcut)
-
- if (!editor_settings.has_setting(TAB_CYCLE_FORWARD)):
- var shortcut: Shortcut = Shortcut.new()
- var event: InputEventKey = InputEventKey.new()
- event.device = -1
- event.keycode = KEY_TAB
- event.ctrl_pressed = true
-
- shortcut.events = [ event ]
- editor_settings.set_setting(TAB_CYCLE_FORWARD, shortcut)
-
- if (!editor_settings.has_setting(TAB_CYCLE_BACKWARD)):
- var shortcut: Shortcut = Shortcut.new()
- var event: InputEventKey = InputEventKey.new()
- event.device = -1
- event.keycode = KEY_TAB
- event.shift_pressed = true
- event.ctrl_pressed = true
-
- shortcut.events = [ event ]
- editor_settings.set_setting(TAB_CYCLE_BACKWARD, shortcut)
-
- open_outline_popup_shc = editor_settings.get_setting(OPEN_OUTLINE_POPUP)
- open_scripts_popup_shc = editor_settings.get_setting(OPEN_SCRIPTS_POPUP)
- open_quick_search_popup_shc = editor_settings.get_setting(OPEN_QUICK_SEARCH_POPUP)
- open_quick_search_popup_scenes_shc = editor_settings.get_setting(OPEN_QUICK_SEARCH_POPUP_SCENES)
- open_quick_search_popup_gdscripts_shc = editor_settings.get_setting(OPEN_QUICK_SEARCH_POPUP_GDSCRIPTS)
- open_quick_search_popup_resources_shc = editor_settings.get_setting(OPEN_QUICK_SEARCH_POPUP_RESOURCES)
- open_override_popup_shc = editor_settings.get_setting(OPEN_OVERRIDE_POPUP)
- tab_cycle_forward_shc = editor_settings.get_setting(TAB_CYCLE_FORWARD)
- tab_cycle_backward_shc = editor_settings.get_setting(TAB_CYCLE_BACKWARD)
-#endregion
-
-## Schedules an update on the next frame.
-func schedule_update():
- set_process(true)
-
-## Updates all parts of the editor that are needed to be synchronized with the file system change.
-func update_editor():
- if (file_to_navigate != &""):
- EditorInterface.select_file(file_to_navigate)
- EditorInterface.get_script_editor().get_current_editor().get_base_editor().grab_focus()
- file_to_navigate = &""
-
- update_keywords()
-
- if (is_script_changed):
- multiline_tab_bar.tab_changed()
- outline_container.tab_changed()
- is_script_changed = false
- else:
- # We saved / filesystem changed. so need to update everything.
- multiline_tab_bar.update_tabs()
- outline_container.update()
-
-func on_tab_changed(index: int):
- if (old_script_editor_base != null):
- old_script_editor_base.edited_script_changed.disconnect(update_selected_tab)
- old_script_editor_base = null
-
- var script_editor: ScriptEditor = EditorInterface.get_script_editor()
- var script_editor_base: ScriptEditorBase = script_editor.get_current_editor()
-
- if (script_editor_base != null):
- script_editor_base.edited_script_changed.connect(update_selected_tab)
-
- old_script_editor_base = script_editor_base
-
- if (!multiline_tab_bar.is_split()):
- multiline_tab_bar.set_split_disabled(script_editor_base == null)
-
- is_script_changed = true
-
- if (is_auto_navigate_in_fs && script_editor.get_current_script() != null):
- var file: String = script_editor.get_current_script().get_path()
-
- if (file.contains(BUILT_IN_SCRIPT)):
- # We navigate to the scene in case of a built-in script.
- file = file.get_slice(BUILT_IN_SCRIPT, 0)
-
- file_to_navigate = file
- else:
- file_to_navigate = &""
-
- schedule_update()
-
-func toggle_split_view():
- var script_editor: ScriptEditor = EditorInterface.get_script_editor()
- var split_script_editor_base: ScriptEditorBase = script_editor.get_current_editor()
-
- if (!multiline_tab_bar.is_split()):
- if (split_script_editor_base == null):
- return
-
- var base_editor: Control = split_script_editor_base.get_base_editor()
- if !(base_editor is CodeEdit):
- return
-
- multiline_tab_bar.set_split(true)
-
- var editor: CodeEdit = SplitCodeEdit.new_from(base_editor)
-
- var container: PanelContainer = PanelContainer.new()
- container.custom_minimum_size.x = 200
- container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
-
- container.add_child(editor)
- tab_splitter.add_child(container)
- else:
- multiline_tab_bar.set_split(false)
- tab_splitter.remove_child(tab_splitter.get_child(tab_splitter.get_child_count() - 1))
-
- if (split_script_editor_base == null):
- multiline_tab_bar.set_split_disabled(true)
-
-func navigate_in_outline(event: InputEvent):
- navigate_on_list(event, outline_container.outline, outline_container.find_in_outline_and_goto)
-
-func notify_order_changed():
- multiline_tab_bar.script_order_changed()
-
-func open_quick_search_popup(category: QuickOpenPopup.Category = QuickOpenPopup.Category.ALL):
- var pref_size: Vector2
- if (quick_open_popup == null):
- quick_open_popup = QUICK_OPEN_SCENE.instantiate()
- quick_open_popup.plugin = self
- quick_open_popup.set_unparent_when_invisible(true)
- pref_size = Vector2(500, 400) * get_editor_scale()
- else:
- pref_size = quick_open_popup.size
-
- quick_open_popup.popup_exclusive_on_parent(EditorInterface.get_script_editor(), get_center_editor_rect(pref_size))
- quick_open_popup.set_category(category)
-
-func open_override_popup():
- var script: Script = get_current_script()
- if (!script):
- return
-
- var pref_size: Vector2
- if (override_popup == null):
- override_popup = OVERRIDE_SCENE.instantiate()
- override_popup.plugin = self
- override_popup.outline_container = outline_container
- override_popup.set_unparent_when_invisible(true)
- pref_size = Vector2(500, 400) * get_editor_scale()
- else:
- pref_size = override_popup.size
-
- override_popup.popup_exclusive_on_parent(EditorInterface.get_script_editor(), get_center_editor_rect(pref_size))
-
-func hide_scripts_popup():
- if (scripts_popup != null && scripts_popup.visible):
- scripts_popup.hide.call_deferred()
-
-func create_set_scripts_popup():
- scripts_popup = PopupPanel.new()
- scripts_popup.popup_hide.connect(restore_scripts_list)
-
- # Need to be inside the tree, so it can be shown as popup for the tab container.
- var script_editor: ScriptEditor = EditorInterface.get_script_editor()
- script_editor.add_child(scripts_popup)
-
- multiline_tab_bar.set_popup(scripts_popup)
-
-func restore_scripts_list():
- script_filter_txt.text = &""
-
- update_script_list_visibility()
-
- scripts_item_list.get_parent().reparent(script_panel_split_container)
- script_panel_split_container.move_child(scripts_item_list.get_parent(), 0)
-
-func navigate_on_list(event: InputEvent, list: ItemList, submit: Callable):
- if (event.is_action_pressed(&"ui_text_submit")):
- list.accept_event()
-
- var index: int = get_list_index(list)
- if (index == -1):
- return
-
- submit.call(index)
- list.accept_event()
- elif (event.is_action_pressed(&"ui_down", true)):
- var index: int = get_list_index(list)
- if (index == list.item_count - 1):
- return
-
- navigate_list(list, index, 1)
- elif (event.is_action_pressed(&"ui_up", true)):
- var index: int = get_list_index(list)
- if (index <= 0):
- return
-
- navigate_list(list, index, -1)
- elif (event.is_action_pressed(&"ui_page_down", true)):
- var index: int = get_list_index(list)
- if (index == list.item_count - 1):
- return
-
- navigate_list(list, index, 5)
- elif (event.is_action_pressed(&"ui_page_up", true)):
- var index: int = get_list_index(list)
- if (index <= 0):
- return
-
- navigate_list(list, index, -5)
- elif (event is InputEventKey && list.item_count > 0 && !list.is_anything_selected()):
- list.select(0)
-
-func get_list_index(list: ItemList) -> int:
- var items: PackedInt32Array = list.get_selected_items()
-
- if (items.is_empty()):
- return -1
-
- return items[0]
-
-func navigate_list(list: ItemList, index: int, amount: int):
- index = clamp(index + amount, 0, list.item_count - 1)
-
- list.select(index)
- list.ensure_current_is_visible()
- list.accept_event()
-
-func get_center_editor_rect(pref_size: Vector2) -> Rect2i:
- var script_editor: ScriptEditor = EditorInterface.get_script_editor()
-
- var position: Vector2 = script_editor.global_position + script_editor.size / 2 - pref_size / 2
-
- return Rect2i(position, pref_size)
-
-func open_outline_popup():
- if (get_current_script() == null):
- return
-
- var button_flags: Array[bool] = outline_container.save_restore_filter()
-
- var old_text: String = outline_filter_txt.text
- outline_filter_txt.text = &""
-
- var pref_size: Vector2
- if (outline_popup == null):
- outline_popup = PopupPanel.new()
- outline_popup.set_unparent_when_invisible(true)
- pref_size = Vector2(500, 400) * get_editor_scale()
- else:
- pref_size = outline_popup.size
-
- var outline_initially_closed: bool = !files_panel.visible
- if (outline_initially_closed):
- files_panel.visible = true
-
- files_panel.reparent(outline_popup)
-
- outline_popup.popup_hide.connect(on_outline_popup_hidden.bind(outline_initially_closed, old_text, button_flags))
-
- outline_popup.popup_exclusive_on_parent(EditorInterface.get_script_editor(), get_center_editor_rect(pref_size))
-
- update_outline()
- outline_filter_txt.grab_focus()
-
-func on_outline_popup_hidden(outline_initially_closed: bool, old_text: String, button_flags: Array[bool]):
- outline_popup.popup_hide.disconnect(on_outline_popup_hidden)
-
- if outline_initially_closed:
- files_panel.visible = false
-
- files_panel.reparent(script_editor_split_container)
- if (!is_outline_right):
- script_editor_split_container.move_child(files_panel, 0)
-
- outline_filter_txt.text = old_text
-
- outline_container.restore_filter(button_flags)
-
- update_outline()
-
-func open_scripts_popup():
- multiline_tab_bar.show_popup()
-
-func get_current_script() -> Script:
- var script_editor: ScriptEditor = EditorInterface.get_script_editor()
- return script_editor.get_current_script()
-
-func select_script(selected_idx: int):
- hide_scripts_popup()
-
- scripts_item_list.item_selected.emit(selected_idx)
-
-func goto_line(index: int):
- if (outline_popup != null && outline_popup.visible):
- outline_popup.hide.call_deferred()
-
- EditorInterface.set_main_screen_editor("Script")
-
- var script_editor: ScriptEditor = EditorInterface.get_script_editor()
- script_editor.goto_line(index)
-
- var code_edit: CodeEdit = script_editor.get_current_editor().get_base_editor()
- code_edit.set_caret_line(index)
- code_edit.set_v_scroll(index)
- code_edit.set_caret_column(code_edit.get_line(index).length())
- code_edit.set_h_scroll(0)
-
- code_edit.grab_focus()
-
-func update_script_list_visibility():
- scripts_item_list.get_parent().visible = is_script_list_visible
-
-func sync_settings():
- if (suppress_settings_sync):
- return
-
- var changed_settings: PackedStringArray = get_editor_settings().get_changed_settings()
- for setting: String in changed_settings:
- if (setting == ICON_SATURATION):
- outline_container.reset_icons()
- elif (setting == SHOW_MEMBERS):
- show_members = get_setting(SHOW_MEMBERS, true)
- if (!show_members):
- set_setting(SHOW_MEMBERS, true)
- elif (setting == MONOSPACE_OUTLINE):
- update_outline_font()
-
- if (!setting.begins_with(SCRIPT_IDE)):
- continue
-
- match (setting):
- OUTLINE_POSITION_RIGHT:
- var new_outline_right: bool = get_setting(OUTLINE_POSITION_RIGHT, is_outline_right)
- if (new_outline_right != is_outline_right):
- is_outline_right = new_outline_right
-
- update_outline_position()
- SCRIPT_LIST_VISIBLE:
- var new_script_list_visible: bool = get_setting(SCRIPT_LIST_VISIBLE, is_script_list_visible)
- if (new_script_list_visible != is_script_list_visible):
- is_script_list_visible = new_script_list_visible
-
- update_script_list_visibility()
- SCRIPT_TABS_POSITION_TOP:
- var new_script_tabs_top: bool = get_setting(SCRIPT_TABS_POSITION_TOP, is_script_tabs_top)
- if (new_script_tabs_top != is_script_tabs_top):
- is_script_tabs_top = new_script_tabs_top
-
- update_tabs_position()
- OUTLINE_ORDER:
- var new_outline_order: PackedStringArray = get_outline_order()
- outline_container.outline_order = new_outline_order
- HIDE_PRIVATE_MEMBERS:
- var new_value: bool = get_setting(HIDE_PRIVATE_MEMBERS, outline_container.is_hide_private_members)
- outline_container.is_hide_private_members = new_value
- SCRIPT_TABS_CLOSE_BUTTON_ALWAYS:
- var new_value: bool = get_setting(SCRIPT_TABS_CLOSE_BUTTON_ALWAYS, multiline_tab_bar.show_close_button_always)
- multiline_tab_bar.show_close_button_always = new_value
- SCRIPT_TABS_SINGLELINE:
- var new_value: bool = get_setting(SCRIPT_TABS_SINGLELINE, multiline_tab_bar.is_singleline_tabs)
- multiline_tab_bar.is_singleline_tabs = new_value
- SCRIPT_TABS_VISIBLE:
- var new_value: bool = get_setting(SCRIPT_TABS_VISIBLE, multiline_tab_bar.visible)
- multiline_tab_bar.visible = new_value
- AUTO_NAVIGATE_IN_FS:
- is_auto_navigate_in_fs = get_setting(AUTO_NAVIGATE_IN_FS, is_auto_navigate_in_fs)
- OPEN_OUTLINE_POPUP:
- open_outline_popup_shc = get_shortcut(OPEN_OUTLINE_POPUP)
- OPEN_SCRIPTS_POPUP:
- open_scripts_popup_shc = get_shortcut(OPEN_SCRIPTS_POPUP)
- OPEN_QUICK_SEARCH_POPUP:
- open_quick_search_popup_shc = get_shortcut(OPEN_QUICK_SEARCH_POPUP)
- OPEN_QUICK_SEARCH_POPUP_SCENES:
- open_quick_search_popup_scenes_shc = get_shortcut(OPEN_QUICK_SEARCH_POPUP_SCENES)
- OPEN_QUICK_SEARCH_POPUP_GDSCRIPTS:
- open_quick_search_popup_gdscripts_shc = get_shortcut(OPEN_QUICK_SEARCH_POPUP_GDSCRIPTS)
- OPEN_QUICK_SEARCH_POPUP_RESOURCES:
- open_quick_search_popup_resources_shc = get_shortcut(OPEN_QUICK_SEARCH_POPUP_RESOURCES)
- OPEN_OVERRIDE_POPUP:
- open_override_popup_shc = get_shortcut(OPEN_OVERRIDE_POPUP)
- TAB_CYCLE_FORWARD:
- tab_cycle_forward_shc = get_shortcut(TAB_CYCLE_FORWARD)
- TAB_CYCLE_BACKWARD:
- tab_cycle_backward_shc = get_shortcut(TAB_CYCLE_BACKWARD)
- _:
- outline_container.update_filter_buttons()
-
-func update_selected_tab():
- multiline_tab_bar.update_selected_tab()
-
-func update_tabs_position():
- var tab_container_parent: Control = multiline_tab_bar.get_parent()
- if (is_script_tabs_top):
- tab_container_parent.move_child(multiline_tab_bar, 0)
- else:
- tab_container_parent.move_child(multiline_tab_bar, tab_container_parent.get_child_count() - 1)
-
-func update_outline():
- outline_container.update_outline()
-
-func update_outline_position():
- if (is_outline_right):
- # Try to restore the previous split offset.
- var split_offset: float = script_editor_split_container.get_child(1).size.x
- script_editor_split_container.split_offset = split_offset
- script_editor_split_container.move_child(files_panel, 1)
- else:
- script_editor_split_container.move_child(files_panel, 0)
-
-func update_outline_font():
- var monospace_outline: bool = get_setting(MONOSPACE_OUTLINE, true)
- if (monospace_outline):
- var font: Font = EditorInterface.get_script_editor().get_theme_font(&"source", &"EditorFonts")
- outline_container.outline.add_theme_font_override(&"font", font)
- else:
- outline_container.outline.remove_theme_font_override(&"font")
-
-func update_keywords():
- var script: Script = get_current_script()
- if (script == null):
- return
-
- var new_script_type: StringName = script.get_instance_base_type()
- if (old_script_type != new_script_type):
- old_script_type = new_script_type
-
- keywords.clear()
- keywords["_static_init"] = true
- register_virtual_methods(new_script_type)
-
-func register_virtual_methods(clazz: String):
- for method: Dictionary in ClassDB.class_get_method_list(clazz):
- if (method[&"flags"] & METHOD_FLAG_VIRTUAL > 0):
- keywords[method[&"name"]] = true
-
-func get_editor_scale() -> float:
- return EditorInterface.get_editor_scale()
-
-func get_editor_settings() -> EditorSettings:
- return EditorInterface.get_editor_settings()
-
-func get_setting(property: StringName, alt: bool) -> bool:
- var editor_settings: EditorSettings = get_editor_settings()
- if (editor_settings.has_setting(property)):
- return editor_settings.get_setting(property)
- else:
- editor_settings.set_setting(property, alt)
- return alt
-
-func set_setting(property: StringName, value: bool):
- var editor_settings: EditorSettings = get_editor_settings()
-
- suppress_settings_sync = true
- editor_settings.set_setting(property, value)
- suppress_settings_sync = false
-
-func get_shortcut(property: StringName) -> Shortcut:
- return get_editor_settings().get_setting(property)
-
-func get_outline_order() -> PackedStringArray:
- var new_outline_order: PackedStringArray
- var editor_settings: EditorSettings = get_editor_settings()
- if (editor_settings.has_setting(OUTLINE_ORDER)):
- new_outline_order = editor_settings.get_setting(OUTLINE_ORDER)
- else:
- new_outline_order = OutlineContainer.DEFAULT_ORDER
- editor_settings.set_setting(OUTLINE_ORDER, new_outline_order)
-
- return new_outline_order
-
-static func find_or_null(arr: Array[Node]) -> Control:
- if (arr.is_empty()):
- push_error("""Node that is needed for Script-IDE not found.
-Plugin will not work correctly.
-This might be due to some other plugins or changes in the Engine.
-Please report this to Script-IDE, so we can figure out a fix.""")
- return null
- return arr[0] as Control
diff --git a/addons/script-ide/plugin.gd.uid b/addons/script-ide/plugin.gd.uid
deleted file mode 100644
index 36a7330..0000000
--- a/addons/script-ide/plugin.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bc0b5v66xdidn
diff --git a/addons/script-ide/quickopen/quick_open_popup.gd b/addons/script-ide/quickopen/quick_open_popup.gd
deleted file mode 100644
index 9b86041..0000000
--- a/addons/script-ide/quickopen/quick_open_popup.gd
+++ /dev/null
@@ -1,282 +0,0 @@
-## Quick open panel to quickly access all resources that are in the project.
-## Initially shows all resources, but can be changed to more specific resources
-## or filtered down with text.
-@tool
-extends PopupPanel
-
-const ADDONS: StringName = &"res://addons"
-const SEPARATOR: StringName = &" - "
-const STRUCTURE_START: StringName = &"("
-const STRUCTURE_END: StringName = &")"
-
-const ORDER: Dictionary[StringName, int] = {
- &"PackedScene": 0,
- &"GDScript": 1,
- &"Object": 2
-}
-
-const Plugin := preload("uid://bc0b5v66xdidn")
-
-enum Category {
- ALL = 0, SCENES = 1, GDSCRIPTS = 2, RESOURCES = 3
-}
-
-#region UI
-@onready var filter_bar: TabBar = %FilterBar
-@onready var search_option_btn: OptionButton = %SearchOptionBtn
-@onready var filter_txt: LineEdit = %FilterTxt
-@onready var files_list: ItemList = %FilesList
-#endregion
-
-var plugin: Plugin
-
-var scenes: Array[FileData]
-var scripts: Array[FileData]
-var resources: Array[FileData]
-var others: Array[FileData]
-
-# For performance and memory considerations, we add all files into one reusable array.
-var all_files: Array[FileData]
-
-var is_rebuild_cache: bool = true
-
-#region Plugin and Shortcut processing
-func _ready() -> void:
- files_list.item_selected.connect(open_file)
- search_option_btn.item_selected.connect(rebuild_cache_and_ui.unbind(1))
- filter_txt.text_changed.connect(fill_files_list.unbind(1))
-
- filter_bar.tab_changed.connect(change_fill_files_list.unbind(1))
-
- about_to_popup.connect(on_show)
-
- var file_system: EditorFileSystem = EditorInterface.get_resource_filesystem()
- file_system.filesystem_changed.connect(schedule_rebuild)
-
- filter_txt.gui_input.connect(navigate_in_files_list)
-
-func _shortcut_input(event: InputEvent) -> void:
- if (!event.is_pressed() || event.is_echo()):
- return
-
- if (plugin.tab_cycle_forward_shc.matches_event(event)):
- get_viewport().set_input_as_handled()
-
- var new_tab: int = filter_bar.current_tab + 1
- if (new_tab == filter_bar.get_tab_count()):
- new_tab = 0
- filter_bar.current_tab = new_tab
- elif (plugin.tab_cycle_backward_shc.matches_event(event)):
- get_viewport().set_input_as_handled()
-
- var new_tab: int = filter_bar.current_tab - 1
- if (new_tab == -1):
- new_tab = filter_bar.get_tab_count() - 1
- filter_bar.current_tab = new_tab
-#endregion
-
-func navigate_in_files_list(event: InputEvent):
- plugin.navigate_on_list(event, files_list, open_file)
-
-func set_category(category: Category):
- var old_tab: int = filter_bar.current_tab
- filter_bar.current_tab = category
-
- # We need to manually trigger this when there is no tab change.
- if (old_tab == category):
- fill_files_list()
-
- focus_and_select_first()
-
-func open_file(index: int):
- var file: String = files_list.get_item_metadata(index)
-
- if (ResourceLoader.exists(file)):
- var res: Resource = load(file)
-
- if (res is Script):
- EditorInterface.edit_script(res)
- EditorInterface.set_main_screen_editor.call_deferred("Script")
- else:
- EditorInterface.edit_resource(res)
-
- if (res is PackedScene):
- EditorInterface.open_scene_from_path(file)
-
- # Need to be deferred as it does not work otherwise.
- var root: Node = EditorInterface.get_edited_scene_root()
- if (root is Node3D):
- EditorInterface.set_main_screen_editor.call_deferred("3D")
- else:
- EditorInterface.set_main_screen_editor.call_deferred("2D")
- else:
- # Text files (.txt, .md) will not be recognized, which seems to be a very bad
- # limitation from the Engine. The methods called by the Engine are also not exposed.
- # So we just select the file, which is better than nothing.
- EditorInterface.select_file(file)
-
- # Deferred as otherwise we get weird errors in the console.
- # Probably due to this beeing called in a signal and auto unparent is true.
- # 100% Engine bug or at least weird behavior.
- hide.call_deferred()
-
-func schedule_rebuild():
- is_rebuild_cache = true
-
-func on_show():
- if (search_option_btn.selected != 0):
- search_option_btn.selected = 0
-
- is_rebuild_cache = true
-
- if (is_rebuild_cache):
- rebuild_cache()
-
- filter_txt.select_all()
-
-func rebuild_cache():
- is_rebuild_cache = false
-
- all_files.clear()
- scenes.clear()
- scripts.clear()
- resources.clear()
- others.clear()
-
- build_file_cache()
-
-func rebuild_cache_and_ui():
- rebuild_cache()
- fill_files_list()
-
- focus_and_select_first()
-
-func focus_and_select_first():
- filter_txt.grab_focus()
-
- if (files_list.item_count > 0):
- files_list.select(0)
-
-func build_file_cache():
- var dir: EditorFileSystemDirectory = EditorInterface.get_resource_filesystem().get_filesystem()
- build_file_cache_dir(dir)
-
- scenes.sort_custom(sort_by_filter)
- scripts.sort_custom(sort_by_filter)
- resources.sort_custom(sort_by_filter)
- others.sort_custom(sort_by_filter)
-
- all_files.append_array(scenes)
- all_files.append_array(scripts)
- all_files.append_array(resources)
- all_files.append_array(others)
-
-func build_file_cache_dir(dir: EditorFileSystemDirectory):
- for index: int in dir.get_subdir_count():
- build_file_cache_dir(dir.get_subdir(index))
-
- for index: int in dir.get_file_count():
- var file: String = dir.get_file_path(index)
- if (search_option_btn.get_selected_id() == 0 && file.begins_with(ADDONS)):
- continue
-
- var last_delimiter: int = file.rfind(&"/")
-
- var file_name: String = file.substr(last_delimiter + 1)
- var file_structure: String = &""
- if (file_name.length() + 6 != file.length()):
- file_structure = SEPARATOR + STRUCTURE_START + file.substr(6, last_delimiter - 6) + STRUCTURE_END
-
- var file_data: FileData = FileData.new()
- file_data.file = file
- file_data.file_name = file_name
- file_data.file_name_structure = file_name + file_structure
- file_data.file_type = dir.get_file_type(index)
-
- # Needed, as otherwise we have no icon.
- if (file_data.file_type == &"Resource"):
- file_data.file_type = &"Object"
-
- match (file.get_extension()):
- &"tscn": scenes.append(file_data)
- &"gd": scripts.append(file_data)
- &"tres": resources.append(file_data)
- &"gdshader": resources.append(file_data)
- _: others.append(file_data)
-
-func change_fill_files_list():
- fill_files_list()
-
- focus_and_select_first()
-
-func fill_files_list():
- files_list.clear()
-
- if (filter_bar.current_tab == 0):
- fill_files_list_with(all_files, sort_by_order_filter)
- elif (filter_bar.current_tab == 1):
- fill_files_list_with(scenes, sort_by_filter)
- elif (filter_bar.current_tab == 2):
- fill_files_list_with(scripts, sort_by_filter)
- elif (filter_bar.current_tab == 3):
- fill_files_list_with(resources, sort_by_filter)
- elif (filter_bar.current_tab == 4):
- fill_files_list_with(others, sort_by_filter)
-
-func fill_files_list_with(files: Array[FileData], comparator: Callable):
- var filter_text: String = filter_txt.text
-
- if (!filter_text.is_empty()):
- files.sort_custom(comparator)
-
- for file_data: FileData in files:
- var file: String = file_data.file
- if (filter_text.is_empty() || filter_text.is_subsequence_ofn(file)):
- var icon: Texture2D = EditorInterface.get_base_control().get_theme_icon(file_data.file_type, &"EditorIcons")
-
- files_list.add_item(file_data.file_name_structure, icon)
- files_list.set_item_metadata(files_list.item_count - 1, file)
- files_list.set_item_tooltip(files_list.item_count - 1, file)
-
-func sort_by_order_filter(file_data1: FileData, file_data2: FileData) -> bool:
- var order1: int = ORDER.get(file_data1.file_type, ORDER.size())
- var order2: int = ORDER.get(file_data2.file_type, ORDER.size())
-
- if (order1 != order2):
- return order1 < order2
-
- return sort_by_filter(file_data1, file_data2)
-
-func sort_by_filter(file_data1: FileData, file_data2: FileData) -> bool:
- var filter_text: String = filter_txt.text
-
- var name1: String = file_data1.file_name
- var name2: String = file_data2.file_name
-
- for index: int in filter_text.length():
- var a_oob: bool = index >= name1.length()
- var b_oob: bool = index >= name2.length()
-
- if (a_oob && b_oob):
- return false
- elif (a_oob):
- return true
- elif (b_oob):
- return false
-
- var char: String = filter_text[index]
- var a_match: bool = char == name1[index]
- var b_match: bool = char == name2[index]
-
- if (a_match && !b_match):
- return true
- elif (b_match && !a_match):
- return false
-
- return name1 < name2
-
-class FileData:
- var file: String
- var file_name: String
- var file_name_structure: String
- var file_type: StringName
diff --git a/addons/script-ide/quickopen/quick_open_popup.gd.uid b/addons/script-ide/quickopen/quick_open_popup.gd.uid
deleted file mode 100644
index 77189c8..0000000
--- a/addons/script-ide/quickopen/quick_open_popup.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cgwp4rl1udqbb
diff --git a/addons/script-ide/quickopen/quick_open_popup.tscn b/addons/script-ide/quickopen/quick_open_popup.tscn
deleted file mode 100644
index b43decd..0000000
--- a/addons/script-ide/quickopen/quick_open_popup.tscn
+++ /dev/null
@@ -1,149 +0,0 @@
-[gd_scene format=3 uid="uid://d2pttchmj3n7q"]
-
-[ext_resource type="Script" uid="uid://cgwp4rl1udqbb" path="res://addons/script-ide/quickopen/quick_open_popup.gd" id="1_6d8qa"]
-
-[sub_resource type="Image" id="Image_70lqw"]
-data = {
-"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 16, 225, 225, 225, 134, 224, 224, 224, 209, 224, 224, 224, 245, 224, 224, 224, 245, 224, 224, 224, 208, 224, 224, 224, 131, 236, 236, 236, 13, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 73, 224, 224, 224, 228, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 225, 225, 225, 225, 68, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 73, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 183, 224, 224, 224, 198, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 198, 224, 224, 224, 189, 224, 224, 224, 255, 224, 224, 224, 254, 224, 224, 224, 65, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 16, 224, 224, 224, 228, 224, 224, 224, 255, 224, 224, 224, 120, 226, 226, 226, 60, 224, 224, 224, 255, 225, 225, 225, 109, 225, 225, 225, 110, 224, 224, 224, 255, 226, 226, 226, 60, 224, 224, 224, 128, 224, 224, 224, 255, 225, 225, 225, 223, 234, 234, 234, 12, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 134, 224, 224, 224, 255, 225, 225, 225, 183, 255, 255, 255, 0, 224, 224, 224, 153, 224, 224, 224, 243, 255, 255, 255, 4, 255, 255, 255, 4, 224, 224, 224, 244, 225, 225, 225, 151, 255, 255, 255, 1, 225, 225, 225, 191, 224, 224, 224, 255, 225, 225, 225, 127, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 209, 224, 224, 224, 255, 224, 224, 224, 72, 255, 255, 255, 0, 224, 224, 224, 216, 224, 224, 224, 198, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 199, 224, 224, 224, 214, 255, 255, 255, 0, 226, 226, 226, 78, 224, 224, 224, 255, 224, 224, 224, 206, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 243, 224, 224, 224, 255, 226, 226, 226, 78, 255, 255, 255, 0, 224, 224, 224, 244, 225, 225, 225, 151, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 152, 224, 224, 224, 242, 255, 255, 255, 1, 227, 227, 227, 81, 224, 224, 224, 255, 224, 224, 224, 241, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 245, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 229, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 147, 225, 225, 225, 149, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 230, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 244, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 208, 224, 224, 224, 255, 224, 224, 224, 147, 224, 224, 224, 161, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 235, 224, 224, 224, 235, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 160, 225, 225, 225, 150, 224, 224, 224, 255, 224, 224, 224, 205, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 131, 224, 224, 224, 255, 224, 224, 224, 189, 255, 255, 255, 1, 224, 224, 224, 152, 224, 224, 224, 243, 255, 255, 255, 4, 255, 255, 255, 4, 224, 224, 224, 244, 225, 225, 225, 151, 255, 255, 255, 2, 225, 225, 225, 199, 224, 224, 224, 255, 225, 225, 225, 127, 255, 255, 255, 0, 255, 255, 255, 0, 236, 236, 236, 13, 224, 224, 224, 225, 224, 224, 224, 255, 224, 224, 224, 128, 225, 225, 225, 67, 224, 224, 224, 255, 225, 225, 225, 110, 226, 226, 226, 111, 224, 224, 224, 255, 225, 225, 225, 67, 225, 225, 225, 135, 224, 224, 224, 255, 224, 224, 224, 221, 234, 234, 234, 12, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 68, 224, 224, 224, 254, 224, 224, 224, 255, 224, 224, 224, 194, 224, 224, 224, 220, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 219, 224, 224, 224, 196, 224, 224, 224, 255, 224, 224, 224, 253, 227, 227, 227, 62, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 66, 224, 224, 224, 225, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 220, 226, 226, 226, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 237, 237, 14, 224, 224, 224, 130, 224, 224, 224, 206, 224, 224, 224, 244, 224, 224, 224, 244, 224, 224, 224, 205, 225, 225, 225, 124, 230, 230, 230, 10, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
-"format": "RGBA8",
-"height": 16,
-"mipmaps": false,
-"width": 16
-}
-
-[sub_resource type="ImageTexture" id="ImageTexture_p6ab8"]
-image = SubResource("Image_70lqw")
-
-[sub_resource type="Image" id="Image_xbsxl"]
-data = {
-"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 25, 226, 226, 226, 70, 229, 229, 229, 39, 255, 255, 255, 0, 226, 226, 226, 103, 224, 224, 224, 219, 224, 224, 224, 156, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 25, 255, 255, 255, 0, 224, 224, 224, 74, 224, 224, 224, 177, 226, 226, 226, 111, 255, 255, 255, 0, 224, 224, 224, 98, 224, 224, 224, 255, 225, 225, 225, 182, 255, 255, 255, 0, 228, 228, 228, 46, 224, 224, 224, 255, 224, 224, 224, 197, 255, 255, 255, 0, 224, 224, 224, 57, 224, 224, 224, 255, 224, 224, 224, 187, 255, 255, 255, 0, 225, 225, 225, 42, 224, 224, 224, 255, 224, 224, 224, 232, 255, 255, 255, 6, 224, 224, 224, 8, 225, 225, 225, 182, 224, 224, 224, 153, 255, 255, 255, 7, 255, 255, 255, 0, 228, 228, 228, 37, 255, 255, 255, 7, 255, 255, 255, 0, 229, 229, 229, 19, 224, 224, 224, 237, 224, 224, 224, 198, 225, 225, 225, 17, 255, 255, 255, 0, 227, 227, 227, 71, 224, 224, 224, 48, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 228, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 73, 224, 224, 224, 226, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
-"format": "RGBA8",
-"height": 16,
-"mipmaps": false,
-"width": 16
-}
-
-[sub_resource type="ImageTexture" id="ImageTexture_bbwjp"]
-image = SubResource("Image_xbsxl")
-
-[sub_resource type="Image" id="Image_hmtyg"]
-data = {
-"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 33, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 31, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 95, 224, 224, 224, 57, 255, 255, 255, 0, 224, 224, 224, 99, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 93, 255, 255, 255, 0, 224, 224, 224, 57, 224, 224, 224, 90, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 93, 224, 224, 224, 255, 224, 224, 224, 254, 224, 224, 224, 165, 224, 224, 224, 217, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 214, 225, 225, 225, 167, 224, 224, 224, 254, 224, 224, 224, 254, 224, 224, 224, 88, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 228, 228, 228, 55, 224, 224, 224, 254, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 166, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 160, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 33, 224, 224, 224, 99, 224, 224, 224, 217, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 186, 224, 224, 224, 32, 224, 224, 224, 33, 224, 224, 224, 187, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 98, 224, 224, 224, 32, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 33, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 36, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 33, 255, 255, 255, 0, 255, 255, 255, 0, 229, 229, 229, 38, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 31, 226, 226, 226, 95, 224, 224, 224, 216, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 187, 225, 225, 225, 34, 226, 226, 226, 35, 224, 224, 224, 192, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 213, 226, 226, 226, 95, 231, 231, 231, 31, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 166, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 163, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 57, 224, 224, 224, 254, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 254, 227, 227, 227, 54, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 90, 224, 224, 224, 254, 224, 224, 224, 253, 224, 224, 224, 161, 225, 225, 225, 215, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 213, 224, 224, 224, 162, 224, 224, 224, 253, 224, 224, 224, 253, 226, 226, 226, 86, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 88, 225, 225, 225, 51, 255, 255, 255, 0, 224, 224, 224, 98, 224, 224, 224, 255, 224, 224, 224, 255, 226, 226, 226, 95, 255, 255, 255, 0, 227, 227, 227, 53, 226, 226, 226, 86, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 32, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 31, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
-"format": "RGBA8",
-"height": 16,
-"mipmaps": false,
-"width": 16
-}
-
-[sub_resource type="ImageTexture" id="ImageTexture_ghict"]
-image = SubResource("Image_hmtyg")
-
-[sub_resource type="Image" id="Image_whndc"]
-data = {
-"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 224, 224, 224, 99, 224, 224, 224, 213, 224, 224, 224, 212, 224, 224, 224, 97, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 224, 224, 224, 99, 224, 224, 224, 222, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 220, 224, 224, 224, 97, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 224, 224, 224, 99, 224, 224, 224, 222, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 213, 226, 226, 226, 87, 224, 224, 224, 88, 224, 224, 224, 214, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 220, 224, 224, 224, 97, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 199, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 89, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 224, 224, 224, 90, 224, 224, 224, 216, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 4, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 221, 224, 224, 224, 99, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 5, 225, 225, 225, 101, 225, 225, 225, 223, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 3, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 222, 225, 225, 225, 100, 225, 225, 225, 100, 225, 225, 225, 223, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 3, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 2, 226, 226, 226, 87, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 2, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 87, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 1, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 100, 255, 255, 255, 5, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 196, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 223, 225, 225, 225, 101, 255, 255, 255, 5, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 193, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 3, 225, 225, 225, 93, 224, 224, 224, 218, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 223, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 216, 225, 225, 225, 91, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 3, 225, 225, 225, 93, 224, 224, 224, 218, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 216, 225, 225, 225, 91, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 3, 225, 225, 225, 93, 224, 224, 224, 208, 225, 225, 225, 207, 225, 225, 225, 91, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
-"format": "RGBA8",
-"height": 16,
-"mipmaps": false,
-"width": 16
-}
-
-[sub_resource type="ImageTexture" id="ImageTexture_grjtr"]
-image = SubResource("Image_whndc")
-
-[sub_resource type="Image" id="Image_y5or7"]
-data = {
-"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 184, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 181, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 77, 225, 225, 225, 76, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 8, 224, 224, 224, 222, 224, 224, 224, 221, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 120, 224, 224, 224, 32, 224, 224, 224, 128, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 127, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 226, 226, 226, 35, 224, 224, 224, 248, 224, 224, 224, 195, 224, 224, 224, 247, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 247, 225, 225, 225, 34, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 178, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 181, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
-"format": "RGBA8",
-"height": 16,
-"mipmaps": false,
-"width": 16
-}
-
-[sub_resource type="ImageTexture" id="ImageTexture_xupch"]
-image = SubResource("Image_y5or7")
-
-[sub_resource type="Image" id="Image_00i5q"]
-data = {
-"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 68, 224, 224, 224, 184, 224, 224, 224, 240, 224, 224, 224, 232, 224, 224, 224, 186, 227, 227, 227, 62, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 129, 224, 224, 224, 254, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 122, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 68, 224, 224, 224, 254, 224, 224, 224, 254, 224, 224, 224, 123, 224, 224, 224, 32, 224, 224, 224, 33, 225, 225, 225, 125, 224, 224, 224, 254, 224, 224, 224, 254, 226, 226, 226, 69, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 184, 224, 224, 224, 255, 224, 224, 224, 123, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 125, 224, 224, 224, 255, 225, 225, 225, 174, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 240, 224, 224, 224, 255, 231, 231, 231, 31, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 35, 224, 224, 224, 255, 224, 224, 224, 233, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 232, 224, 224, 224, 255, 224, 224, 224, 32, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 228, 228, 228, 37, 224, 224, 224, 255, 224, 224, 224, 228, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 186, 224, 224, 224, 255, 224, 224, 224, 123, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 130, 224, 224, 224, 255, 224, 224, 224, 173, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 62, 224, 224, 224, 255, 224, 224, 224, 254, 225, 225, 225, 126, 225, 225, 225, 34, 227, 227, 227, 36, 224, 224, 224, 131, 224, 224, 224, 255, 224, 224, 224, 255, 226, 226, 226, 77, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 122, 224, 224, 224, 254, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 69, 225, 225, 225, 174, 224, 224, 224, 233, 224, 224, 224, 228, 224, 224, 224, 173, 226, 226, 226, 77, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 227, 225, 225, 225, 34, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 225, 225, 225, 34, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
-"format": "RGBA8",
-"height": 16,
-"mipmaps": false,
-"width": 16
-}
-
-[sub_resource type="ImageTexture" id="ImageTexture_w6vkw"]
-image = SubResource("Image_00i5q")
-
-[node name="QuickOpenPanel" type="PopupPanel" unique_id=1197081327]
-oversampling_override = 1.0
-size = Vector2i(627, 100)
-visible = true
-script = ExtResource("1_6d8qa")
-
-[node name="PanelContainer" type="PanelContainer" parent="." unique_id=170455275]
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-offset_left = 4.0
-offset_top = 4.0
-offset_right = -4.0
-offset_bottom = -4.0
-grow_horizontal = 2
-grow_vertical = 2
-size_flags_horizontal = 3
-size_flags_vertical = 3
-
-[node name="MarginContainer" type="MarginContainer" parent="PanelContainer" unique_id=811819428]
-layout_mode = 2
-theme_override_constants/margin_left = 5
-theme_override_constants/margin_top = 5
-theme_override_constants/margin_right = 5
-theme_override_constants/margin_bottom = 5
-
-[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer" unique_id=679259085]
-layout_mode = 2
-theme_override_constants/separation = 5
-
-[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer" unique_id=150235257]
-layout_mode = 2
-theme_override_constants/separation = 4
-
-[node name="FilterBar" type="TabBar" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer" unique_id=660360907]
-unique_name_in_owner = true
-layout_mode = 2
-current_tab = 0
-clip_tabs = false
-scrolling_enabled = false
-tab_count = 5
-tab_0/title = "All"
-tab_0/icon = SubResource("ImageTexture_p6ab8")
-tab_1/title = "Scene"
-tab_1/icon = SubResource("ImageTexture_bbwjp")
-tab_2/title = "GDScript"
-tab_2/icon = SubResource("ImageTexture_ghict")
-tab_3/title = "Resource"
-tab_3/icon = SubResource("ImageTexture_grjtr")
-tab_4/title = "Other"
-tab_4/icon = SubResource("ImageTexture_xupch")
-
-[node name="SearchOptionBtn" type="OptionButton" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer" unique_id=902993731]
-unique_name_in_owner = true
-layout_mode = 2
-selected = 0
-item_count = 2
-popup/item_0/text = "Project"
-popup/item_0/id = 0
-popup/item_1/text = "Project+Addons"
-popup/item_1/id = 1
-
-[node name="FilterTxt" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer" unique_id=721767691]
-unique_name_in_owner = true
-layout_mode = 2
-placeholder_text = "Filter files"
-right_icon = SubResource("ImageTexture_w6vkw")
-
-[node name="FilesList" type="ItemList" parent="PanelContainer/MarginContainer/VBoxContainer" unique_id=190949578]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_vertical = 3
-allow_reselect = true
diff --git a/addons/script-ide/split/split_code_edit.gd b/addons/script-ide/split/split_code_edit.gd
deleted file mode 100644
index 9934978..0000000
--- a/addons/script-ide/split/split_code_edit.gd
+++ /dev/null
@@ -1,47 +0,0 @@
-## The CodeEdit that is used when the editor is split, to show the split script.
-@tool
-extends CodeEdit
-
-var last_v_scroll: float
-
-func _ready() -> void:
- editable = false
- caret_draw_when_editable_disabled = true
- set_v_scroll.call_deferred(last_v_scroll)
-
-static func new_from(from_code_edit: CodeEdit) -> CodeEdit:
- var new_code_edit: CodeEdit = new()
-
- new_code_edit.text = from_code_edit.text
- new_code_edit.syntax_highlighter = from_code_edit.syntax_highlighter
- new_code_edit.highlight_all_occurrences = from_code_edit.highlight_all_occurrences
- new_code_edit.highlight_current_line = from_code_edit.highlight_current_line
-
- new_code_edit.use_default_word_separators = from_code_edit.use_default_word_separators
- new_code_edit.use_custom_word_separators = from_code_edit.use_custom_word_separators
- new_code_edit.custom_word_separators = from_code_edit.custom_word_separators
-
- new_code_edit.line_folding = from_code_edit.line_folding
- new_code_edit.line_length_guidelines = from_code_edit.line_length_guidelines
-
- new_code_edit.gutters_draw_line_numbers = from_code_edit.gutters_draw_line_numbers
- new_code_edit.gutters_draw_fold_gutter = from_code_edit.gutters_draw_fold_gutter
-
- new_code_edit.minimap_draw = from_code_edit.minimap_draw
- new_code_edit.minimap_width = from_code_edit.minimap_width
-
- new_code_edit.delimiter_strings = from_code_edit.delimiter_strings
- new_code_edit.delimiter_comments = from_code_edit.delimiter_comments
-
- new_code_edit.indent_automatic = from_code_edit.indent_automatic
- new_code_edit.indent_size = from_code_edit.indent_size
- new_code_edit.indent_use_spaces = from_code_edit.indent_use_spaces
- new_code_edit.indent_automatic_prefixes = from_code_edit.indent_automatic_prefixes
-
- new_code_edit.draw_control_chars = from_code_edit.draw_control_chars
- new_code_edit.draw_tabs = from_code_edit.draw_tabs
- new_code_edit.draw_spaces = from_code_edit.draw_spaces
-
- new_code_edit.last_v_scroll = from_code_edit.scroll_vertical
-
- return new_code_edit
diff --git a/addons/script-ide/split/split_code_edit.gd.uid b/addons/script-ide/split/split_code_edit.gd.uid
deleted file mode 100644
index 9d1b1b4..0000000
--- a/addons/script-ide/split/split_code_edit.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://boy48rhhyrph
diff --git a/addons/script-ide/tabbar/custom_tab.gd b/addons/script-ide/tabbar/custom_tab.gd
deleted file mode 100644
index 1864f4f..0000000
--- a/addons/script-ide/tabbar/custom_tab.gd
+++ /dev/null
@@ -1,84 +0,0 @@
-## Button that is used as a custom tab implementation for the multiline tab bar.
-@tool
-extends Button
-
-signal close_pressed
-signal right_clicked
-signal dragged_over
-signal dropped(source_index: int, target_index: int)
-
-var close_button: Button
-
-func _init() -> void:
- alignment = HORIZONTAL_ALIGNMENT_LEFT
- action_mode = ACTION_MODE_BUTTON_PRESS
- auto_translate_mode = Node.AUTO_TRANSLATE_MODE_DISABLED
- toggle_mode = true
-
-func show_close_button():
- if (close_button == null):
- close_button = create_close_button()
-
- add_child(close_button)
- close_button.set_anchors_and_offsets_preset(Control.PRESET_CENTER_RIGHT)
-
-func hide_close_button():
- if (close_button != null):
- remove_child(close_button)
-
-func create_close_button() -> Button:
- close_button = Button.new()
- close_button.icon = EditorInterface.get_editor_theme().get_icon(&"Close", &"EditorIcons")
- close_button.flat = true
- close_button.focus_mode = Control.FOCUS_NONE
- close_button.pressed.connect(on_close_pressed)
-
- return close_button
-
-func _gui_input(event: InputEvent) -> void:
- if event is InputEventMouseButton && event.pressed:
- if event.button_index == MOUSE_BUTTON_MIDDLE:
- on_close_pressed()
- elif (event.button_index == MOUSE_BUTTON_RIGHT):
- button_pressed = true
- on_right_click()
-
-func on_right_click():
- right_clicked.emit()
-
-func on_close_pressed() -> void:
- close_pressed.emit()
-
-func _get_drag_data(at_position: Vector2) -> Variant:
- var preview: Button = Button.new()
- preview.text = text
- preview.icon = icon
- preview.alignment = HORIZONTAL_ALIGNMENT_LEFT
- preview.auto_translate_mode = Node.AUTO_TRANSLATE_MODE_DISABLED
- preview.add_theme_stylebox_override(&"normal", get_theme_stylebox(&"normal"))
-
- set_drag_preview(preview)
-
- var drag_data: Dictionary[String, Variant]
- drag_data["type"] = "script_list_element"
- drag_data["script_list_element"] = EditorInterface.get_script_editor().get_current_editor()
- drag_data["index"] = get_index()
-
- return drag_data
-
-func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
- if !(data is Dictionary):
- return false
-
- var can_drop: bool = data.has("index")
-
- if (can_drop):
- dragged_over.emit()
-
- return can_drop
-
-func _drop_data(at_position: Vector2, data: Variant) -> void:
- if (!_can_drop_data(at_position, data)):
- return
-
- dropped.emit(data["index"], get_index())
diff --git a/addons/script-ide/tabbar/custom_tab.gd.uid b/addons/script-ide/tabbar/custom_tab.gd.uid
deleted file mode 100644
index ff37823..0000000
--- a/addons/script-ide/tabbar/custom_tab.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bppomxp4mri2o
diff --git a/addons/script-ide/tabbar/multiline_tab_bar.gd b/addons/script-ide/tabbar/multiline_tab_bar.gd
deleted file mode 100644
index fe06ccc..0000000
--- a/addons/script-ide/tabbar/multiline_tab_bar.gd
+++ /dev/null
@@ -1,503 +0,0 @@
-## Tab bar that can show tabs in multiple lines (wrap), when there is not enough horizontal space.
-@tool
-extends PanelContainer
-
-const CLOSE_BTN_SPACER: String = " "
-
-const CustomTab := preload("uid://bppomxp4mri2o")
-const Plugin := preload("uid://bc0b5v66xdidn")
-
-signal split_toggled
-
-@onready var multiline_tab_bar: HFlowContainer = %MultilineTabBar
-@onready var split_btn: Button = %SplitBtn
-@onready var popup_btn: Button = %PopupBtn
-
-#region Theme
-var tab_hovered: StyleBoxFlat
-var tab_focus: StyleBoxFlat
-var tab_selected: StyleBoxFlat
-var tab_unselected: StyleBoxFlat
-
-var font_selected_color: Color
-var font_unselected_color: Color
-var font_hovered_color: Color
-#endregion
-
-var plugin: Plugin
-
-var show_close_button_always: bool = false : set = set_show_close_button_always
-var is_singleline_tabs: bool = false : set = set_singleline_tabs
-
-var tab_group: ButtonGroup = ButtonGroup.new()
-
-# Existing Engine components, set from the plugin
-var script_filter_txt: LineEdit
-var scripts_item_list: ItemList
-var scripts_tab_container: TabContainer
-
-var popup: PopupPanel
-
-var suppress_theme_changed: bool
-
-var split: bool
-var split_path: String
-var split_icon: Texture2D
-
-var last_drag_over_tab: CustomTab
-var drag_marker: ColorRect
-var current_tab: CustomTab
-
-#region Plugin and related tab handling processing
-func _init() -> void:
- tab_group.pressed.connect(on_new_tab_selected)
-
-func _ready() -> void:
- popup_btn.pressed.connect(show_popup)
- split_btn.gui_input.connect(on_right_click)
- split_btn.toggled.connect(toggle_split.unbind(1))
- split_icon = split_btn.icon
-
- set_process(false)
-
- if (plugin == null):
- return
-
- schedule_update()
-
-func _notification(what: int) -> void:
- if (what == NOTIFICATION_DRAG_END || what == NOTIFICATION_MOUSE_EXIT):
- clear_drag_mark()
- return
-
- if (what == NOTIFICATION_THEME_CHANGED):
- if (suppress_theme_changed):
- return
-
- suppress_theme_changed = true
- add_theme_stylebox_override(&"panel", EditorInterface.get_editor_theme().get_stylebox(&"tabbar_background", &"TabContainer"))
- suppress_theme_changed = false
-
- tab_hovered = EditorInterface.get_editor_theme().get_stylebox(&"tab_hovered", &"TabContainer")
- tab_focus = EditorInterface.get_editor_theme().get_stylebox(&"tab_focus", &"TabContainer")
- tab_selected = EditorInterface.get_editor_theme().get_stylebox(&"tab_selected", &"TabContainer")
- tab_unselected = EditorInterface.get_editor_theme().get_stylebox(&"tab_unselected", &"TabContainer")
-
- if (drag_marker == null):
- drag_marker = ColorRect.new()
- drag_marker.set_anchors_and_offsets_preset(PRESET_LEFT_WIDE)
- drag_marker.mouse_filter = Control.MOUSE_FILTER_IGNORE
- drag_marker.custom_minimum_size.x = 4 * EditorInterface.get_editor_scale()
- drag_marker.color = EditorInterface.get_editor_theme().get_color(&"drop_mark_color", &"TabContainer")
-
- font_hovered_color = EditorInterface.get_editor_theme().get_color(&"font_hovered_color", &"TabContainer")
- font_selected_color = EditorInterface.get_editor_theme().get_color(&"font_selected_color", &"TabContainer")
- font_unselected_color = EditorInterface.get_editor_theme().get_color(&"font_unselected_color", &"TabContainer")
-
- # If this is called too early.
- if (multiline_tab_bar == null):
- return
-
- for tab: CustomTab in get_tabs():
- update_tab_style(tab)
-
-func _process(delta: float) -> void:
- sync_tabs_with_item_list()
-
- if (is_singleline_tabs):
- shift_singleline_tabs()
-
- set_process(false)
-
-func _shortcut_input(event: InputEvent) -> void:
- if (!event.is_pressed() || event.is_echo()):
- return
-
- if (!is_visible_in_tree()):
- return
-
- if (current_tab == null):
- return
-
- if (plugin.tab_cycle_forward_shc.matches_event(event)):
- get_viewport().set_input_as_handled()
-
- var tab_count: int = get_tab_count()
- if (tab_count <= 1):
- return
-
- var index: int = current_tab.get_index()
- var new_tab: int = index + 1
- if (new_tab == tab_count):
- new_tab = 0
-
- var tab: CustomTab = get_tab(new_tab)
- tab.button_pressed = true
- elif (plugin.tab_cycle_backward_shc.matches_event(event)):
- get_viewport().set_input_as_handled()
-
- var tab_count: int = get_tab_count()
- if (tab_count <= 1):
- return
-
- var index: int = current_tab.get_index()
- var new_tab: int = index - 1
- if (new_tab == -1):
- new_tab = tab_count - 1
-
- var tab: CustomTab = get_tab(new_tab)
- tab.button_pressed = true
-
-func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
- if !(data is Dictionary):
- return false
-
- var can_drop: bool = data.has("index") && data["index"] != get_tab_count() - 1
-
- if (can_drop):
- on_drag_over(get_tab(get_tab_count() - 1))
-
- return can_drop
-
-func _drop_data(at_position: Vector2, data: Variant) -> void:
- if (!_can_drop_data(at_position, data)):
- return
-
- on_drag_drop(data["index"], get_tab_count() - 1)
-#endregion
-
-func schedule_update():
- set_process(true)
-
-func set_split(value: bool) -> void:
- split = value
-
- if (split):
- var index: int = current_tab.get_index()
- split_path = scripts_item_list.get_item_tooltip(index)
-
- split_btn.text = scripts_item_list.get_item_text(index)
- split_btn.icon = scripts_item_list.get_item_icon(index)
- split_btn.tooltip_text = split_path
- else:
- split_btn.icon = split_icon
- split_btn.text = ""
-
-func is_split() -> bool:
- return split
-
-func toggle_split():
- split_toggled.emit()
-
-func set_split_disabled(value: bool):
- split_btn.disabled = value
-
-func on_right_click(event: InputEvent):
- if (!split_btn.button_pressed):
- return
-
- if !(event is InputEventMouseButton):
- return
-
- var mouse_event: InputEventMouseButton = event
-
- if (!mouse_event.is_pressed() || mouse_event.button_index != MOUSE_BUTTON_RIGHT):
- return
-
- split_btn.button_pressed = false
-
- if (split_path != null && ResourceLoader.exists(split_path)):
- var res: Resource = load(split_path)
-
- EditorInterface.edit_resource(res)
-
-func on_drag_drop(source_index: int, target_index: int):
- var child: Node = scripts_tab_container.get_child(source_index)
- scripts_tab_container.move_child(child, target_index);
-
- var tab: CustomTab = get_tab(target_index)
- tab.grab_focus()
-
-func on_drag_over(tab: CustomTab):
- if (last_drag_over_tab == tab):
- return
-
- # The drag marker should always be orphan when here.
- tab.add_child(drag_marker)
-
- last_drag_over_tab = tab
-
-func clear_drag_mark():
- if (last_drag_over_tab == null):
- return
-
- last_drag_over_tab = null
- if (drag_marker.get_parent() != null):
- drag_marker.get_parent().remove_child(drag_marker)
-
-func update_tabs():
- on_scripts_changed()
-
- for tab: CustomTab in get_tabs():
- update_tab(tab)
-
-func get_tabs() -> Array[Node]:
- return multiline_tab_bar.get_children()
-
-func update_selected_tab():
- update_tab(tab_group.get_pressed_button())
-
-func update_tab(tab: CustomTab):
- if (tab == null):
- return
-
- var index: int = tab.get_index()
-
- tab.text = scripts_item_list.get_item_text(index)
- tab.icon = scripts_item_list.get_item_icon(index)
- tab.tooltip_text = scripts_item_list.get_item_tooltip(index)
-
- update_icon_color(tab, scripts_item_list.get_item_icon_modulate(index))
-
- if (scripts_item_list.is_selected(index)):
- tab.button_pressed = true
- tab.text += CLOSE_BTN_SPACER
- elif (show_close_button_always):
- tab.text += CLOSE_BTN_SPACER
-
-func get_tab(index: int) -> CustomTab:
- if (index < 0 || index >= get_tab_count()):
- return null
-
- return multiline_tab_bar.get_child(index)
-
-func get_tab_count() -> int:
- return multiline_tab_bar.get_child_count()
-
-func add_tab() -> CustomTab:
- var tab: CustomTab = CustomTab.new()
- tab.button_group = tab_group
-
- if (show_close_button_always):
- tab.show_close_button()
-
- update_tab_style(tab)
-
- tab.close_pressed.connect(on_tab_close_pressed.bind(tab))
- tab.right_clicked.connect(on_tab_right_click.bind(tab))
- tab.mouse_exited.connect(clear_drag_mark)
- tab.dragged_over.connect(on_drag_over.bind(tab))
- tab.dropped.connect(on_drag_drop)
-
- multiline_tab_bar.add_child(tab)
- return tab
-
-func update_tab_style(tab: CustomTab):
- tab.add_theme_stylebox_override(&"normal", tab_unselected)
- tab.add_theme_stylebox_override(&"hover", tab_hovered)
- tab.add_theme_stylebox_override(&"hover_pressed", tab_hovered)
- tab.add_theme_stylebox_override(&"focus", tab_focus)
- tab.add_theme_stylebox_override(&"pressed", tab_selected)
-
- tab.add_theme_color_override(&"font_color", font_unselected_color)
- tab.add_theme_color_override(&"font_hover_color", font_hovered_color)
- tab.add_theme_color_override(&"font_pressed_color", font_selected_color)
-
-func update_icon_color(tab: CustomTab, color: Color):
- tab.add_theme_color_override(&"icon_normal_color", color)
- tab.add_theme_color_override(&"icon_hover_color", color)
- tab.add_theme_color_override(&"icon_hover_pressed_color", color)
- tab.add_theme_color_override(&"icon_pressed_color", color)
- tab.add_theme_color_override(&"icon_focus_color", color)
-
-
-func on_tab_right_click(tab: CustomTab):
- var index: int = tab.get_index()
- scripts_item_list.item_clicked.emit(index, scripts_item_list.get_local_mouse_position(), MOUSE_BUTTON_RIGHT)
-
-func on_new_tab_selected(tab: CustomTab):
- # Hide and show close button.
- if (!show_close_button_always):
- if (current_tab != null):
- current_tab.hide_close_button()
-
- if (tab != null):
- tab.show_close_button()
-
- update_script_text_filter()
-
- var index: int = tab.get_index()
- if (scripts_item_list != null && !scripts_item_list.is_selected(index)):
- scripts_item_list.select(index)
- scripts_item_list.item_selected.emit(index)
- scripts_item_list.ensure_current_is_visible()
-
- # Remove spacing from previous tab.
- if (!show_close_button_always && current_tab != null):
- update_tab(current_tab)
- current_tab = tab
-
-## Removes the script filter text and emits the signal so that the tabs stay
-## and we do not break anything there.
-func update_script_text_filter():
- if (script_filter_txt.text != &""):
- script_filter_txt.text = &""
- script_filter_txt.text_changed.emit(&"")
-
-func on_tab_close_pressed(tab: CustomTab) -> void:
- scripts_item_list.item_clicked.emit(tab.get_index(), scripts_item_list.get_local_mouse_position(), MOUSE_BUTTON_MIDDLE)
-
-func sync_tabs_with_item_list() -> void:
- if (get_tab_count() > scripts_item_list.item_count):
- for index: int in range(get_tab_count() - 1, scripts_item_list.item_count - 1, -1):
- var tab: CustomTab = get_tab(index)
-
- if (tab == current_tab):
- current_tab = null
-
- multiline_tab_bar.remove_child(tab)
- free_tab(tab)
-
- for index: int in scripts_item_list.item_count:
- var tab: CustomTab = get_tab(index)
- if (tab == null):
- tab = add_tab()
-
- update_tab(tab)
-
-func tab_changed():
- on_scripts_changed()
- update_script_text_filter()
-
- # When the tab change was not triggered by our component,
- # we need to sync the selection.
- update_tab(get_tab(scripts_tab_container.current_tab))
-
-func on_scripts_changed():
- update_script_text_filter()
-
- popup_btn.text = "(" + str(scripts_item_list.item_count) + ")"
-
-func script_order_changed() -> void:
- schedule_update()
-
-func set_popup(new_popup: PopupPanel) -> void:
- popup = new_popup
-
-func show_popup() -> void:
- if (popup == null):
- return
-
- scripts_item_list.get_parent().reparent(popup)
- scripts_item_list.get_parent().visible = true
-
- popup.size = Vector2(250 * get_editor_scale(), get_parent().size.y - size.y)
- popup.position = popup_btn.get_screen_position() - Vector2(popup.size.x, 0)
- popup.popup()
-
- script_filter_txt.grab_focus()
-
-func get_editor_scale() -> float:
- return EditorInterface.get_editor_scale()
-
-func set_show_close_button_always(new_value: bool):
- if (show_close_button_always == new_value):
- return
-
- show_close_button_always = new_value
-
- if (multiline_tab_bar == null):
- return
-
- for tab: CustomTab in get_tabs():
- tab.text = scripts_item_list.get_item_text(tab.get_index())
- if (show_close_button_always):
- tab.text += CLOSE_BTN_SPACER
- if (!tab.button_pressed):
- tab.show_close_button()
- else:
- if (!tab.button_pressed):
- tab.hide_close_button()
- else:
- tab.text += CLOSE_BTN_SPACER
-
-func free_tabs():
- if (drag_marker != null):
- drag_marker.free()
-
- for tab: CustomTab in get_tabs():
- free_tab(tab)
-
-func free_tab(tab: CustomTab):
- if (tab.close_button != null):
- tab.close_button.free()
- tab.free()
-
-#region Singeline handling
-func set_singleline_tabs(new_value: bool):
- if (is_singleline_tabs == new_value):
- return
-
- is_singleline_tabs = new_value
-
- if (is_singleline_tabs):
- multiline_tab_bar.item_rect_changed.connect(shift_singleline_tabs)
- tab_group.pressed.connect(shift_singleline_tabs.unbind(1))
-
- shift_singleline_tabs()
- else:
- multiline_tab_bar.item_rect_changed.disconnect(shift_singleline_tabs)
- tab_group.pressed.disconnect(shift_singleline_tabs)
-
- for tab: CustomTab in get_tabs():
- tab.visible = true
-
-func shift_singleline_tabs():
- if (current_tab == null):
- return
-
- var tabs: Array[Node] = get_tabs()
- var tab_count: int = tabs.size()
- if (tab_count == 0):
- return
-
- var tab_bar_width: float = multiline_tab_bar.size.x
- var current_index: int = current_tab.get_index()
- var first: int = current_index
- var last: int = current_index
- var used_width: float = current_tab.size.x
-
- # Pass 1: Expand through already-visible tabs first
- while (first > 0):
- var prev_tab: Node = tabs[first - 1]
- if (!prev_tab.visible || used_width + prev_tab.size.x > tab_bar_width):
- break
- first -= 1
- used_width += prev_tab.size.x
-
- while (last < tab_count - 1):
- var next_tab: Node = tabs[last + 1]
- if (!next_tab.visible || used_width + next_tab.size.x > tab_bar_width):
- break
- last += 1
- used_width += next_tab.size.x
-
- # Pass 2: Fill remaining space with any tabs
- while (first > 0):
- var prev_tab: Node = tabs[first - 1]
- if (used_width + prev_tab.size.x > tab_bar_width):
- break
- first -= 1
- used_width += prev_tab.size.x
-
- while (last < tab_count - 1):
- var next_tab: Node = tabs[last + 1]
- if (used_width + next_tab.size.x > tab_bar_width):
- break
- last += 1
- used_width += next_tab.size.x
-
- for index: int in tab_count:
- var tab: Node = tabs.get(index)
- tab.visible = index >= first && index <= last
-#endregion
diff --git a/addons/script-ide/tabbar/multiline_tab_bar.gd.uid b/addons/script-ide/tabbar/multiline_tab_bar.gd.uid
deleted file mode 100644
index b0d8a21..0000000
--- a/addons/script-ide/tabbar/multiline_tab_bar.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://l1rdargfn67o
diff --git a/addons/script-ide/tabbar/multiline_tab_bar.tscn b/addons/script-ide/tabbar/multiline_tab_bar.tscn
deleted file mode 100644
index 1c399a4..0000000
--- a/addons/script-ide/tabbar/multiline_tab_bar.tscn
+++ /dev/null
@@ -1,43 +0,0 @@
-[gd_scene format=3 uid="uid://vjuhunm2uboy"]
-
-[ext_resource type="Script" uid="uid://l1rdargfn67o" path="res://addons/script-ide/tabbar/multiline_tab_bar.gd" id="1_8jr3v"]
-
-[sub_resource type="DPITexture" id="DPITexture_split"]
-_source = ""
-
-[sub_resource type="DPITexture" id="DPITexture_scripts"]
-_source = ""
-
-[node name="MultilineTabContainer" type="PanelContainer" unique_id=635880120]
-anchors_preset = 10
-anchor_right = 1.0
-offset_bottom = 24.0
-grow_horizontal = 2
-size_flags_horizontal = 3
-script = ExtResource("1_8jr3v")
-
-[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=932291510]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_constants/separation = 0
-
-[node name="MultilineTabBar" type="HFlowContainer" parent="HBoxContainer" unique_id=2135340034]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_constants/h_separation = 0
-theme_override_constants/v_separation = 0
-
-[node name="SplitBtn" type="Button" parent="HBoxContainer" unique_id=813915450]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 8
-toggle_mode = true
-icon = SubResource("DPITexture_split")
-alignment = 0
-
-[node name="PopupBtn" type="Button" parent="HBoxContainer" unique_id=1586924518]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 8
-icon = SubResource("DPITexture_scripts")
diff --git a/assets/characters/player/3DGodotRobot.glb b/assets/characters/player/3DGodotRobot.glb
deleted file mode 100644
index ea3bfaa..0000000
Binary files a/assets/characters/player/3DGodotRobot.glb and /dev/null differ
diff --git a/assets/characters/player/3DGodotRobot.glb.import b/assets/characters/player/3DGodotRobot.glb.import
deleted file mode 100644
index 506d836..0000000
--- a/assets/characters/player/3DGodotRobot.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://cmuii2vrmfnc5"
-path="res://.godot/imported/3DGodotRobot.glb-c7060093cec96d07cab7f0bfb35f5061.scn"
-
-[deps]
-
-source_file="res://assets/characters/player/3DGodotRobot.glb"
-dest_files=["res://.godot/imported/3DGodotRobot.glb-c7060093cec96d07cab7f0bfb35f5061.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=1
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=0
diff --git a/assets/characters/player/3DGodotRobot_GodotPalette.png b/assets/characters/player/3DGodotRobot_GodotPalette.png
deleted file mode 100644
index 4f43542..0000000
Binary files a/assets/characters/player/3DGodotRobot_GodotPalette.png and /dev/null differ
diff --git a/assets/characters/player/3DGodotRobot_GodotPalette.png.import b/assets/characters/player/3DGodotRobot_GodotPalette.png.import
deleted file mode 100644
index 48f14ca..0000000
--- a/assets/characters/player/3DGodotRobot_GodotPalette.png.import
+++ /dev/null
@@ -1,44 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bdg48m6l86q8i"
-path.s3tc="res://.godot/imported/3DGodotRobot_GodotPalette.png-db2cb37c015bbbe580a404413e47cf86.s3tc.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "14aa95c725ca769a3e19739773da801f"
-}
-
-[deps]
-
-source_file="res://assets/characters/player/3DGodotRobot_GodotPalette.png"
-dest_files=["res://.godot/imported/3DGodotRobot_GodotPalette.png-db2cb37c015bbbe580a404413e47cf86.s3tc.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=true
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/assets/characters/player/ATTRIBUTIONS.md b/assets/characters/player/ATTRIBUTIONS.md
deleted file mode 100644
index 4ee7739..0000000
--- a/assets/characters/player/ATTRIBUTIONS.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Third-Party Player Assets
-
-The assets listed in this file are not covered by the project's MIT license.
-They remain available under the license identified below.
-
-## 3D Godot Robot Platformer Character
-
-- Creator: [AGChow](https://github.com/AGChow)
-- Source: [3D-Godot-Robot-Platformer-Character](https://github.com/AGChow/3D-Godot-Robot-Platformer-Character)
-- License: [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/)
-- Scope: the Godot Robot model, rig, animations, base palette, alternate color
- palettes, and the mesh resources imported from that model
-- Modifications: the character was integrated with multiplayer gameplay,
- equipment attachment points, palette selection, and project-specific scenes
- and scripts.
diff --git a/assets/characters/player/GodotRobotPaletteSwap/GodotGreenPalette.png b/assets/characters/player/GodotRobotPaletteSwap/GodotGreenPalette.png
deleted file mode 100644
index 43a2c47..0000000
Binary files a/assets/characters/player/GodotRobotPaletteSwap/GodotGreenPalette.png and /dev/null differ
diff --git a/assets/characters/player/GodotRobotPaletteSwap/GodotGreenPalette.png.import b/assets/characters/player/GodotRobotPaletteSwap/GodotGreenPalette.png.import
deleted file mode 100644
index 8e10faa..0000000
--- a/assets/characters/player/GodotRobotPaletteSwap/GodotGreenPalette.png.import
+++ /dev/null
@@ -1,41 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bbid6mowxhd5b"
-path.s3tc="res://.godot/imported/GodotGreenPalette.png-8c760c09d8ace0593bb63002a2ed8eae.s3tc.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://assets/characters/player/GodotRobotPaletteSwap/GodotGreenPalette.png"
-dest_files=["res://.godot/imported/GodotGreenPalette.png-8c760c09d8ace0593bb63002a2ed8eae.s3tc.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=true
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/assets/characters/player/GodotRobotPaletteSwap/GodotPalette.png b/assets/characters/player/GodotRobotPaletteSwap/GodotPalette.png
deleted file mode 100644
index 4f43542..0000000
Binary files a/assets/characters/player/GodotRobotPaletteSwap/GodotPalette.png and /dev/null differ
diff --git a/assets/characters/player/GodotRobotPaletteSwap/GodotPalette.png.import b/assets/characters/player/GodotRobotPaletteSwap/GodotPalette.png.import
deleted file mode 100644
index 18f6fee..0000000
--- a/assets/characters/player/GodotRobotPaletteSwap/GodotPalette.png.import
+++ /dev/null
@@ -1,41 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://fpmmv2oxjcdv"
-path.s3tc="res://.godot/imported/GodotPalette.png-a2d735eec81ad2647941b719336144cd.s3tc.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://assets/characters/player/GodotRobotPaletteSwap/GodotPalette.png"
-dest_files=["res://.godot/imported/GodotPalette.png-a2d735eec81ad2647941b719336144cd.s3tc.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=true
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/assets/characters/player/GodotRobotPaletteSwap/GodotRedPalette.png b/assets/characters/player/GodotRobotPaletteSwap/GodotRedPalette.png
deleted file mode 100644
index 52d840e..0000000
Binary files a/assets/characters/player/GodotRobotPaletteSwap/GodotRedPalette.png and /dev/null differ
diff --git a/assets/characters/player/GodotRobotPaletteSwap/GodotRedPalette.png.import b/assets/characters/player/GodotRobotPaletteSwap/GodotRedPalette.png.import
deleted file mode 100644
index 322f374..0000000
--- a/assets/characters/player/GodotRobotPaletteSwap/GodotRedPalette.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://brp1gy30s4rks"
-path="res://.godot/imported/GodotRedPalette.png-08bd2ae0a0cfe45c50f42f0c3a248d1b.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/characters/player/GodotRobotPaletteSwap/GodotRedPalette.png"
-dest_files=["res://.godot/imported/GodotRedPalette.png-08bd2ae0a0cfe45c50f42f0c3a248d1b.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/characters/player/GodotRobotPaletteSwap/GodotYellowPalette.png b/assets/characters/player/GodotRobotPaletteSwap/GodotYellowPalette.png
deleted file mode 100644
index 2819314..0000000
Binary files a/assets/characters/player/GodotRobotPaletteSwap/GodotYellowPalette.png and /dev/null differ
diff --git a/assets/characters/player/GodotRobotPaletteSwap/GodotYellowPalette.png.import b/assets/characters/player/GodotRobotPaletteSwap/GodotYellowPalette.png.import
deleted file mode 100644
index cc509ab..0000000
--- a/assets/characters/player/GodotRobotPaletteSwap/GodotYellowPalette.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cw6pxwst2gh85"
-path="res://.godot/imported/GodotYellowPalette.png-827e66defded795634af82b4a85f17de.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/characters/player/GodotRobotPaletteSwap/GodotYellowPalette.png"
-dest_files=["res://.godot/imported/GodotYellowPalette.png-827e66defded795634af82b4a85f17de.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/characters/player/meshes/godot_robot_bottom.tres b/assets/characters/player/meshes/godot_robot_bottom.tres
deleted file mode 100644
index 3904ca2..0000000
--- a/assets/characters/player/meshes/godot_robot_bottom.tres
+++ /dev/null
@@ -1,29 +0,0 @@
-[gd_resource type="ArrayMesh" format=4 uid="uid://bt25tokyr00xs"]
-
-[ext_resource type="Texture2D" uid="uid://bdg48m6l86q8i" path="res://assets/characters/player/3DGodotRobot_GodotPalette.png" id="1_d60ji"]
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_4omi2"]
-resource_name = "Material"
-cull_mode = 2
-albedo_texture = ExtResource("1_d60ji")
-roughness = 0.4
-
-[resource]
-resource_name = "3DGodotRobot_Cube_005"
-_surfaces = [{
-"aabb": AABB(-0.27069494, 0.4784211, -0.18616928, 0.54743254, 0.25276768, 0.40850395),
-"attribute_data": PackedByteArray("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"),
-"bone_aabbs": [AABB(-0.27069494, 0.4784211, -0.18616928, 0.54743254, 0.25276768, 0.40850395), AABB(-0.13383682, 0.5227308, 0.11079006, 0.3226096, 0.14872092, 0.11154461), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(-0.13383687, 0.4784211, -0.18616928, 0.4105745, 0.25276768, 0.40850395), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(-0.27069494, 0.4784211, -0.18616928, 0.41057438, 0.25276768, 0.40850395)],
-"format": 34359745559,
-"index_count": 1068,
-"index_data": PackedByteArray("EwBBAUIBEwBDAUEBAABEAUUBAABGAUQBpABHAaYApABIAUcBEABJAQ8AEABKAUkBAwBLAQUAAwAEAEsBBgAHAAgABgBMAQcATQFOAU8BTQESAE4BUAFRAaAAUAFSAVEBUwFUAVUBUwFWAVQBDgBXAVgBDgBZAVcBCQBaAVsBCQBcAVoBDQBdAQwADQBeAV0BXwFgAacAXwFhAWABAgBiAQEAAgBjAWIBZAFlAWYBZAFnAWUBLgAvADAALgAxAC8AaAHVANYAaAFpAdUAagEbAB0AagFrARsAMgAzADQAMgA1ADMAbAHRANIAbAFtAdEAJABuAW8BJAAlAG4BNgA3ADgANgA5ADcAcAHNAM4AcAFxAc0AcgEnACYAcgFzAScAOgA7ADwAOgA9ADsAdAHJAMoAdAF1AckAFgAXABkAFgAVABcAPgA/AEAAPgBBAD8AdgHFAMYAdgF3AcUAeAEYABQAeAF5ARgAQgBDAEQAQgBFAEMAegHBAMIAegF7AcEAIgAhACAAIgAjACEARgBHAEgARgBJAEcAfAG9AL4AfAF9Ab0AfgF/AYABfgGBAX8BSgBLAEwASgBNAEsAggG5ALoAggGDAbkAhAEeAB8AhAGFAR4ATgBPAFAATgBRAE8AhgG1ALYAhgGHAbUAUgBTAFQAUgBVAFMAVgBXAFgAVgBZAFcAiAGxALIAiAGJAbEAKgApACgAKgArACkALAArACoALAAtACsAWgBbAFwAWgBdAFsAigGtAK4AigGLAa0AjAEcABoAjAGNARwAXgBfAGAAXgBhAF8AjgGpAKoAjgGPAakAngCQAZ8AngCRAZABkgFkAJMBkgGUAWQAawBsAG0AawBuAGwAbwBwAHEAbwByAHAAcwB0AHUAcwB2AHQAdwB4AHkAdwB6AHgAewB8AH0AewB+AHwAfwCAAIEAfwCCAIAAgwCEAIUAgwCGAIQAhwCIAIkAhwCKAIgAiwCMAI0AiwCOAIwAjwCQAJEAjwCSAJAAkwCUAJUAkwCWAJQAlQGWAZcBlQGYAZYBmQGaAZsBmQGcAZoBnQGeAZ8BnQGgAZ4BoQGiAaMBoQGkAaIBpQGmAacBpQGoAaYBqQGqAasBqQGsAaoBCwCtAa4BCwCvAa0BsAGxAbIBsAGzAbEBtAG1AaEAtAG2AbUBtwG4AbkBtwG6AbgBuwG8Ab0BuwG+AbwBvwHAAcEBvwHCAcABwwHEAcUBwwERAMQBxgHHAcgBxgHJAccBygHLAcwBygHNAcsBzgHPAdABzgHRAc8B0gHTAdQB0gHVAdMB1gHXAdgB1gHZAdcB2gHbAdwB2gHdAdsB3gHfAeAB3gHhAd8B4gHjAeQB4gHlAeMB5gGaAOcB5gHoAZoA6QHqAZgA6QGXAOoB6gGXAOsBmQDpAewB7QHpAZkAlwDpAe0B7QGZAO4BlwDtAe8B7wHtAfABlwDvAfEB8gHzAfQB8gH1AfMB9gH3AfgB9gGbAPcB+QH6AfsB+QH8AfoB/QH+Af8B/QEAAv4BAQICAgMCAQIEAgICBQIGAgcCBQIIAgYCCQIKAgsCCQKiAAoCDAKcAA0CDAIOApwADwIQAhECDwISAhACEwKjABQCEwIVAqMAFgIKABcCFgIYAgoAGQIaAhsCGQIcAhoCHQIeAh8CHQIgAh4CIQIiAiMCIQIkAiICJQImAicCJQIoAiYCKQIqAqUAKQIrAioCLAItAi4CLAIvAi0CMAIxAjICMAIzAjECNAI1AjYCNAI3AjUCOAI5AjoCOAI7AjkCPAI9Aj4CPAI/Aj0CQAJBAkICQAJDAkECnQBEAkUCnQBGAkQCqABHAkgCqACrAEcCrABJAkoCrACvAEkCsABLAkwCsACzAEsCtABNAk4CtAC3AE0CuABPAlACuAC7AE8CvABRAlICvAC/AFECwABTAlQCwADDAFMCxABVAlYCxADHAFUCyABXAlgCyADLAFcCzABZAloCzADPAFkC0ABbAlwC0ADTAFsC1ABdAl4C1ADXAF0C2ADZANoA2ADbANkA3ABfAmAC3ADdAF8CYQLkAOIAYQLjAOQAYgJjAGMCYgJkAmMAZQJqAGkAZQJmAmoAZwJoAmkCZwLlAGgCagLgAOEAagLfAOAAawJlAGwCawJtAmUAbgJoAG8CbgJwAmgA5gBxAnIC5gDnAHECcwJ0AnUCcwLeAHQCYgBmAHYCYgBnAGYAdwJ4AuoAdwJ5AngC8QDyAPMA8QD0APIA9QD2APcA9QD4APYA+QD6APsA+QD8APoA/QD+AP8A/QAAAf4AAQECAQMBAQEEAQIBBQEGAQcBBQEIAQYBCQEKAQsBCQEMAQoBDQEOAQ8BDQEQAQ4BEQESARMBEQEUARIBFQEWARcBFQEYARYBGQEaARsBGQEcARoBHQF6Ah8BHQEeAXoCJgF7AnwCJgF9AnsCLAF+An8CLAEvAX4CgAKBAiQBgAKCAoECgwKEAiABgwKFAoQCKAEqASsBKAGGAioBLgGHAogCLgGJAocCigIjASUBigKLAiMBjAKNAiEBjAKOAo0CMAEtATEBMAGPAi0BKQGQApECKQEnAZACkgKTAiIBkgKUApMClQI0AZYClQKXAjQBNAGXApgCmQKVAjIBmgKVApkClwKVApoCmgKZApsClwKaApwCnAKaAp0ClwKcAjMBNQGeAp8CNQE2AZ4COwE8AT0BOwGgAjwBoQKiAugAoQKjAqICpALpAOwApAKlAukAPgGmAqcCPgGoAqYCOQE4AToBOQGpAjgBqgKrAu4AqgKsAqsCrQKuAusArQKvAq4CQAGwArECQAE/AbACNwGyArMCNwG0ArIC8AC1Au8A8ADtALUC"),
-"lods": [0.13629751, PackedByteArray("AAABAAIAAwAEAAUABgAHAAgACQAKAAsACwAKAAwADQALAAwADgALAA0ADgAJAAsADwAJAA4AEAAJAA8AEQAKAAkAEAARAAkAEQASAAoAEwARABAAFAAVABYAFgAVABcAFAAYABUAFgAXABkAGgAYABQAGQAXABsAGgAcABgAGQAbAB0AHQAbAB4AHQAeAB8AIAAcABoAIAAhABwAIgAhACAAIgAjACEAJAAjACIAJAAlACMAJgAlACQAJgAnACUAKAAnACYAKAApACcAKgApACgAKgArACkALAArACoALAAtACsALgAvADAALgAxAC8AMgAzADQAMgA1ADMANgA3ADgANgA5ADcAOgA7ADwAOgA9ADsAPgA/AEAAPgBBAD8AQgBDAEQAQgBFAEMARgBHAEgARgBJAEcASgBLAEwASgBNAEsATgBPAFAATgBRAE8AUgBTAFQAUgBVAFMAVgBXAFgAVgBZAFcAWgBbAFwAWgBdAFsAXgBfAGAAXgBhAF8AYgBjAGQAYgBlAGMAYgBmAGUAYgBnAGYAYgBkAGgAYgBoAGkAaQBoAGoAawBsAG0AawBuAGwAbwBwAHEAbwByAHAAcwB0AHUAcwB2AHQAdwB4AHkAdwB6AHgAewB8AH0AewB+AHwAfwCAAIEAfwCCAIAAgwCEAIUAgwCGAIQAhwCIAIkAhwCKAIgAiwCMAI0AiwCOAIwAjwCQAJEAjwCSAJAAkwCUAJUAkwCWAJQAlwCYAJkAmgCbAJwAnQCbAJoAngCdAJoAngCaAJ8AnwCaAKAAoQCiAKMApACiAKEAoQCjAKUApAChAKYApwChAKUApgChAKcAqACpAKoAqACrAKkArACtAK4ArACvAK0AsACxALIAsACzALEAtAC1ALYAtAC3ALUAuAC5ALoAuAC7ALkAvAC9AL4AvAC/AL0AwADBAMIAwADDAMEAxADFAMYAxADHAMUAyADJAMoAyADLAMkAzADNAM4AzADPAM0A0ADRANIA0ADTANEA1ADVANYA1ADXANUA2ADZANoA2ADbANkA3ADdAN4A3ADeAN8A3ADfAOAA3ADgAOEA4gDjAOQA4gDlAOMA5gDlAOIA5gDnAOUA6ADpAOoA6gDpAOsA6wDpAOwA6ADtAOkA7gDtAOgA7wDtAO4A8ADtAO8A8QDyAPMA8QD0APIA9QD2APcA9QD4APYA+QD6APsA+QD8APoA/QD+AP8A/QAAAf4AAQECAQMBAQEEAQIBBQEGAQcBBQEIAQYBCQEKAQsBCQEMAQoBDQEOAQ8BDQEQAQ4BEQESARMBEQEUARIBFQEWARcBFQEYARYBGQEaARsBGQEcARoBHQEeAR8BHwEeASABIAEeASEBIQEeASIBIgEeASMBIgEjASQBJAEjASUBJgEnASgBKQEnASYBKAEnASoBKAEqASsBLAEtAS4BLAEvAS0BLgEtATABMAEtATEBMgEzATQBNQE2ATcBNwE2ATgBNwE4ATkBOQE4AToBOwE8AT0BPgE8ATsBPgE/ATwBQAE/AT4B")],
-"material": SubResource("StandardMaterial3D_4omi2"),
-"name": "Material",
-"primitive": 3,
-"skin_data": PackedByteArray("AAABABEAFQD9/wEAAAAAAAAAEQABABUA+/8CAAAAAAAAAAEAEQAVAP7/AAAAAAAAAAARABUAAAD+/wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABUAAAAAAP7/AAAAAAAAAAARABUAAAD4/wYAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAFQAAAP7/AAAAAAAAAAARABUAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABEAFQAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABEAAQAVAPv/AgAAAAAAAAABABEAFQD9/wEAAAAAAAAAEQAAAAAA//8AAAAAAAAAAAAAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP7/AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAAAAAAAAA//8AAAAAAAAAABEAAAAAAPz/AgAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAAAEAFQARAP7/AAAAAAAAAAARABUAAADy/wwAAAAAAAAAEQAVAAAA//8AAAAAAAAAABEAFQAAAPj/BgAAAAAAAAARABUAAACl/1gAAAAAAAAAEQAVAAAA9v8IAAAAAAAAABUAEQAAAP//AAAAAAAAAAARABUAAAD+/wAAAAAAAAAAAQARABUA/v8AAAAAAAAAABUAEQAAAP7/AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP7/AAAAAAAAAAAVAAAAAAD+/wAAAAAAAAAAFQAAAAAA9v8IAAAAAAAAABUAAAAAAPr/BAAAAAAAAAARABUAAAD6/wMAAAAAAAAAFQARAAAA9f8JAAAAAAAAABEAFQAAAPb/CAAAAAAAAAARABUAAACl/1gAAAAAAAAAFQAAAAAA/v8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAAVABEAAAD+/wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAAAEAEQAVAPv/AwAAAAAAAAARAAAAAAD//wAAAAAAAAAAAQARABUA/f8BAAAAAAAAAAEAEQAVAP3/AQAAAAAAAAABABUAEQD+/wAAAAAAAAAAAQARABUA+/8DAAAAAAAAAAEAEQAVAP7/AAAAAAAAAAABABEAFQD+/wAAAAAAAAAAAQARABUA+P8GAAAAAAAAAAEAFQARAP7/AAAAAAAAAAARAAEAFQD7/wIAAAAAAAAAEQABABUA+/8CAAAAAAAAABUAAAAAAP//AAAAAAAAAAABABEAFQD4/wYAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD+/wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAAfv+AAAAAAAAAABUAAAAAAP7/AAAAAAAAAAAVAAAAAAD4/wYAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABUAAAAAAPj/BgAAAAAAAAARABUAAAD9/wEAAAAAAAAAFQAAAAAAfv+AAAAAAAAAABEAFQAAAPj/BgAAAAAAAAARABUAAAD4/wYAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAFQAAAP3/AQAAAAAAAAARABUAAAD+/wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABUAEQAAAP7/AAAAAAAAAAARABUAAAD+/wAAAAAAAAAAFQAAAAAA/v8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVABEAAADw/w4AAAAAAAAAEQAVAAAA//8AAAAAAAAAABUAEQAAAP7/AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABUAEQAAAPX/CQAAAAAAAAAVAAAAAAD+/wAAAAAAAAAAFQAAAAAA+v8EAAAAAAAAABEAFQAAAP7/AAAAAAAAAAARABUAAADy/wwAAAAAAAAAEQAVAAAA6v8UAAAAAAAAABEAFQAAAKX/WAAAAAAAAAARABUAAADl/xkAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAEQAAAP7/AAAAAAAAAAAVABEAAAD+/wAAAAAAAAAAAAAAAAAA//8AAAAAAAAAABEAFQAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAFQAAAP//AAAAAAAAAAARABUAAACl/1gAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABUAEQAAAPX/CQAAAAAAAAARABUAAADq/xQAAAAAAAAAEQAAAAAA/P8CAAAAAAAAABEAFQAAAOX/GQAAAAAAAAARABUAAADy/wwAAAAAAAAAEQAAAAAA/v8AAAAAAAAAABUAAAAAAP7/AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAARABUAAAD+/wAAAAAAAAAAEQAVAAAA//8AAAAAAAAAABEAFQAAAP//AAAAAAAAAAAVABEAAADw/w4AAAAAAAAAAAAAAAAA//8AAAAAAAAAABEAAAAAAP7/AAAAAAAAAAARAAAAAAD8/wIAAAAAAAAAEQAAAAAA//8AAAAAAAAAABUAAAAAAPr/BAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA/v8AAAAAAAAAABUAAAAAAP7/AAAAAAAAAAAVABEAAAD+/wAAAAAAAAAAFQARAAAA8P8OAAAAAAAAABEAFQAAAP7/AAAAAAAAAAAVABEAAAD+/wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAAAAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABUAAAAAAP7/AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAFQAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAARABUAAAD+/wAAAAAAAAAAFQAAAAAA+P8GAAAAAAAAABEAFQAAAPj/BgAAAAAAAAARABUAAAD9/wEAAAAAAAAAEQAVAAAA9v8IAAAAAAAAABEAFQAAAPr/AwAAAAAAAAARABUAAAD+/wAAAAAAAAAAFQAAAAAAfv+AAAAAAAAAABEAFQAAAPr/AwAAAAAAAAAVAAAAAAD2/wgAAAAAAAAAEQAVAAAA/f8BAAAAAAAAABUAAAAAAP7/AAAAAAAAAAAVAAAAAAD2/wgAAAAAAAAAFQAAAAAA/v8AAAAAAAAAABUAAAAAAH7/gAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA/v8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD+/wAAAAAAAAAAAQARABUA+P8GAAAAAAAAABUAAAAAAP//AAAAAAAAAAABABEAFQD+/wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAAAEAFQARAP7/AAAAAAAAAAABABEAFQD+/wAAAAAAAAAAFQARAAAA//8AAAAAAAAAAAEAEQAVAPj/BgAAAAAAAAABABEAFQD7/wMAAAAAAAAAFQARAAAA//8AAAAAAAAAAAEAFQARAP7/AAAAAAAAAAABABUAEQD+/wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAAAEAFQARAP7/AAAAAAAAAAARAAAAAAD+/wAAAAAAAAAAAQARABUA+/8DAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD+/wAAAAAAAAAAAAAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAAAAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAFQARAAAA/v8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARABUAAAD4/wYAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAFQAAAP7/AAAAAAAAAAARABUAAAD4/wYAAAAAAAAAEQAVAAAA9v8IAAAAAAAAABUAEQAAAP7/AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA/v8AAAAAAAAAABEAAAAAAP7/AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD+/wAAAAAAAAAAEQAVAAAA5f8ZAAAAAAAAABEAFQAAAOr/FAAAAAAAAAARABUAAADq/xQAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP7/AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAFQAAAOr/FAAAAAAAAAARABUAAADq/xQAAAAAAAAAFQARAAAA8P8OAAAAAAAAABEAAAAAAP//AAAAAAAAAAARABUAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQARAAAA/v8AAAAAAAAAAAAAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA/v8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA/v8AAAAAAAAAABUAEQAAAP7/AAAAAAAAAAAVABEAAADw/w4AAAAAAAAAFQARAAAA8P8OAAAAAAAAABUAEQAAAP7/AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABUAAAAAAP7/AAAAAAAAAAAVAAAAAAD+/wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAFQAAAOX/GQAAAAAAAAARABUAAADq/xQAAAAAAAAAEQAVAAAA6v8UAAAAAAAAABEAFQAAAOX/GQAAAAAAAAAAAAAAAAD//wAAAAAAAAAAFQARAAAA/v8AAAAAAAAAABUAEQAAAP7/AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAEQAVAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAVAAAA//8AAAAAAAAAABEAFQAAAOr/FAAAAAAAAAARABUAAAD+/wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAFQAAAOr/FAAAAAAAAAARAAAAAAD+/wAAAAAAAAAAEQAVAAAA5f8ZAAAAAAAAABEAFQAAAOX/GQAAAAAAAAARAAAAAAD+/wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAAAAAAAAAAP//AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAEQAAAPD/DgAAAAAAAAARABUAAAD//wAAAAAAAAAAEQAVAAAA//8AAAAAAAAAABUAEQAAAPD/DgAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAAAAAAAAAAP//AAAAAAAAAAAVABEAAAD+/wAAAAAAAAAAFQARAAAA8P8OAAAAAAAAABEAFQAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD+/wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAFQAAAOX/GQAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAVAAAA6v8UAAAAAAAAABEAFQAAAOr/FAAAAAAAAAARABUAAADq/xQAAAAAAAAAFQAAAAAA//8AAAAAAAAAABEAFQAAAP7/AAAAAAAAAAARABUAAADq/xQAAAAAAAAAFQAAAAAA/v8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABEAFQAAAOr/FAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP7/AAAAAAAAAAARABUAAADq/xQAAAAAAAAAEQAVAAAA5f8ZAAAAAAAAABEAFQAAAOr/FAAAAAAAAAAVAAAAAAD+/wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAARABUAAAD+/wAAAAAAAAAAEQAVAAAA6v8UAAAAAAAAABEAFQAAAOr/FAAAAAAAAAABABEAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAABABUAEQD+/wAAAAAAAAAAAQARABUA/v8AAAAAAAAAAAEAFQARAP7/AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAAAEAFQARAP7/AAAAAAAAAAABABEAAAD//wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAFQAAAP7/AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP7/AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAAAAAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAARAAEAFQD9/wEAAAAAAAAAFQAAAAAA//8AAAAAAAAAABEAFQAAAP//AAAAAAAAAAARABUAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD4/wYAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAARAAEAFQD9/wEAAAAAAAAAAQAVABEA/v8AAAAAAAAAABEAAAAAAP7/AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAAAAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARABUAAAD+/wAAAAAAAAAAEQAVAAAA/f8BAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD8/wIAAAAAAAAAEQAVAAAA/f8BAAAAAAAAABEAAAAAAP//AAAAAAAAAAAVABEAAAD+/wAAAAAAAAAAAQARABUA/v8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD+/wAAAAAAAAAAFQAAAAAA9v8IAAAAAAAAABUAAAAAAPr/BAAAAAAAAAARAAAAAAD+/wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAABABUAEQD+/wAAAAAAAAAAAQAVABEA/v8AAAAAAAAAABEAFQAAAP//AAAAAAAAAAABABUAEQD+/wAAAAAAAAAAAQAVABEA/v8AAAAAAAAAAAEAFQARAP7/AAAAAAAAAAABABEAFQD+/wAAAAAAAAAAFQAAAAAA/v8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA/v8AAAAAAAAAAAEAEQAVAP7/AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAEQAVAAAA+P8GAAAAAAAAABEAFQAAAPL/DAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA/v8AAAAAAAAAABUAAAAAAP7/AAAAAAAAAAAVAAAAAAD7/wMAAAAAAAAAFQAAAAAA+/8DAAAAAAAAABEAFQAAAPn/BAABAAAAAAAVABEAAAD//wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAFQAAAPn/BAABAAAAAAARABUAAAD+/wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAFQAAAP7/AAAAAAAAAAAVABEAAADw/w4AAAAAAAAAEQAVAAAA//8AAAAAAAAAABUAEQAAAPD/DgAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP7/AAAAAAAAAAARABUAAAD//wAAAAAAAAAAEQAAAAAA/v8AAAAAAAAAABEAFQAAAP//AAAAAAAAAAARABUAAAD+/wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAARABUAAAD//wAAAAAAAAAAEQAVAAAA//8AAAAAAAAAABEAFQAAAP//AAAAAAAAAAARAAAAAAD+/wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABEAFQAAAP7/AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAAAAAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAEQAAAAAA/v8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAAAAAAAAAAP//AAAAAAAAAAARABUAAAD+/wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARABUAAAD+/wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABEAFQAAAP7/AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAEQAVAAAA//8AAAAAAAAAAAAAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAAQARABUA/v8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAEQAVAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAFQAAAP7/AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAAQAVAAAA/v8AAAAAAAAAABEAFQAAAP//AAAAAAAAAAAVABEAAAD//wAAAAAAAAAAEQAVAAAA//8AAAAAAAAAAAAAAAAAAP//AAAAAAAAAAARABUAAAD+/wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAFQAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAVAAAA//8AAAAAAAAAAAEAFQAAAP7/AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAEQAAAP//AAAAAAAAAAARABUAAAD//wAAAAAAAAAAAQARABUA/v8AAAAAAAAAABEAFQAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAAAAAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAAAAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABEAAAAAAP7/AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA/v8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD+/wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAFQAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAAAAAAAAA//8AAAAAAAAAABEAFQAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAAQAVABEA/v8AAAAAAAAAAAEAEQAVAP7/AAAAAAAAAAARAAEAFQD9/wEAAAAAAAAAFQARAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAABABEAAAD//wAAAAAAAAAAAQAVAAAA/v8AAAAAAAAAAAEAFQARAP7/AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAAQAVABEA/v8AAAAAAAAAABUAEQAAAP//AAAAAAAAAAABABUAEQD+/wAAAAAAAAAAAQAVAAAA/v8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAARABUAAAD+/wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAEQABABUA/f8BAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAAQARABUA/v8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAAQARAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAAAAAAAAA//8AAAAAAAAAABEAFQAAAP7/AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAFQAAAAAA//8AAAAAAAAAABEAFQAAAP7/AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAFQAAAPn/BAABAAAAAAARABUAAAD5/wQAAQAAAAAAFQAAAAAA+/8DAAAAAAAAABUAAAAAAPv/AwAAAAAAAAAVAAAAAAD+/wAAAAAAAAAAFQAAAAAA/v8AAAAAAAAAABUAAAAAAP//AAAAAAAAAAAVAAAAAAD//wAAAAAAAAAAAQARABUA/v8AAAAAAAAAAAEAEQAVAP7/AAAAAAAAAAABABUAEQD+/wAAAAAAAAAAAQAVABEA/v8AAAAAAAAAAAEAFQARAP7/AAAAAAAAAAABABUAEQD+/wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD+/wAAAAAAAAAAEQAAAAAA/v8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAEQAVAAAA/f8BAAAAAAAAABEAFQAAAP3/AQAAAAAAAAARABUAAAD+/wAAAAAAAAAAEQAAAAAA/v8AAAAAAAAAABEAAAAAAP7/AAAAAAAAAAAVAAAAAAD+/wAAAAAAAAAAFQARAAAA/v8AAAAAAAAAABUAEQAAAPD/DgAAAAAAAAAVABEAAAD+/wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARABUAAAD+/wAAAAAAAAAAFQAAAAAA/v8AAAAAAAAAABUAAAAAAP7/AAAAAAAAAAARABUAAADl/xkAAAAAAAAAAAAAAAAA//8AAAAAAAAAABUAEQAAAP7/AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAEQAVAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARABUAAAD//wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAFQAAAP7/AAAAAAAAAAARAAAAAAD+/wAAAAAAAAAAEQAVAAAA5f8ZAAAAAAAAABEAFQAAAOX/GQAAAAAAAAAAAAAAAAD//wAAAAAAAAAAFQARAAAA8P8OAAAAAAAAABEAFQAAAP//AAAAAAAAAAAVABEAAADw/w4AAAAAAAAAAAAAAAAA//8AAAAAAAAAABEAFQAAAOX/GQAAAAAAAAARABUAAADl/xkAAAAAAAAAEQAAAAAA/v8AAAAAAAAAABEAFQAAAP7/AAAAAAAAAAARABUAAAD+/wAAAAAAAAAAEQAVAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARABUAAAD//wAAAAAAAAAAAAAAAAAA//8AAAAAAAAAABUAEQAAAP7/AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAEQAVAAAA5f8ZAAAAAAAAABUAAAAAAP7/AAAAAAAAAAAVAAAAAAD+/wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARAAAAAAD//wAAAAAAAAAAFQARAAAA/v8AAAAAAAAAABUAEQAAAPD/DgAAAAAAAAAVABEAAAD+/wAAAAAAAAAAFQAAAAAA/v8AAAAAAAAAABEAAAAAAP7/AAAAAAAAAAARAAAAAAD+/wAAAAAAAAAAFQARAAAA8P8OAAAAAAAAABEAFQAAAP//AAAAAAAAAAAVABEAAADw/w4AAAAAAAAAEQAAAAAA/v8AAAAAAAAAABEAFQAAAOX/GQAAAAAAAAAVAAAAAAD+/wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAAVABEAAADw/w4AAAAAAAAAEQAVAAAA//8AAAAAAAAAAAAAAAAAAP//AAAAAAAAAAAVABEAAAD+/wAAAAAAAAAAEQAAAAAA/v8AAAAAAAAAABEAAAAAAP7/AAAAAAAAAAAVAAAAAAD+/wAAAAAAAAAAFQARAAAA/v8AAAAAAAAAABUAEQAAAPD/DgAAAAAAAAAVABEAAAD+/wAAAAAAAAAAEQAAAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAAVAAAAAAD+/wAAAAAAAAAAFQAAAAAA/v8AAAAAAAAAABEAFQAAAP7/AAAAAAAAAAARABUAAADl/xkAAAAAAAAAAAAAAAAA//8AAAAAAAAAABUAEQAAAP7/AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAEQAVAAAA//8AAAAAAAAAABEAAAAAAP//AAAAAAAAAAARABUAAAD//wAAAAAAAAAAEQAVAAAA/v8AAAAAAAAAABEAFQAAAP7/AAAAAAAAAAARABUAAADl/xkAAAAAAAAAEQAVAAAA5f8ZAAAAAAAAABEAAAAAAP7/AAAAAAAAAAAAAAAAAAD//wAAAAAAAA=="),
-"uv_scale": Vector4(0, 0, 0, 0),
-"vertex_count": 694,
-"vertex_data": PackedByteArray("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qOhevTAvOz+NFb09bkYBPpiDNz8uwpE9GfPRPTAvOz/So3k9EJPFvTAvOz9FA8u80yz2vZiDNz8sYg+9ruhevTAvOz/yCGa9yqh3PTAvOz/yCGa90yz2vZiDNz8uwpE9qOhevTAvOz+NFb09EJPFvTAvOz/So3k9peOXPZiDNz/QWeE9GfPRPTAvOz/So3k9yqh3PTAvOz+NFb09ruhevTAvOz/yCGa9mYOLvZiDNz+8SJe9yqh3PTAvOz/yCGa9peOXPZiDNz+8SJe9GfPRPTAvOz9FA8u8EJPFvTAvOz/So3k9C46rT////z/zcatP////P/Nxq0////8/qs8LDv///7+qzwsO////v6rPCw7///+/VDALDv///79UMAsO////v1QwCw7///+/mXUpM////z+VXnor////P51jMDr///8/tlwfL////z+2XB8v////Pzdj0ED///8/x5zQQP///z/HnNBA////P6iatSH///8/aaF6K////z9Iox8v////P/9//v////+//3/+/////7//f/7/////v/9//v////+//3/+/////7//f/7/////v/9//v////+//3///////7//f/7/////v/9///////+//3/+/////7//f/7/////v/9//v////+//3/+/////7//f/7/////v/9//v////+//3/+/////7//f/7/////v/9//v////+//3/+/////7//f/7/////v/9//v////+//3/+/////7//f/7/////v/9//v////+//3/+/////78e9+cyAAClpB735zIAAKWkHvfnMgAApaQe9+cyAAClpAH1GFYAAOiQAfUYVgAA6JAB9RhWAADokAH1GFYAAOiQF9YCdf//Fu8X1gJ1//8W7xfWAnX//xbvF9YCdf//Fu/nsh93//9Y2+eyH3f//1jb57Ifd///WNvnsh93//9Y28KTlXb//6nKwpOVdv//qcrCk5V2//+pysKTlXb//6nKPGyVdqrq/788bJV2qur/vzxslXaq6v+/PGyVdqrq/78XTR93TMn/vxdNH3dMyf+/F00fd0zJ/78XTR93TMn/v+cpAnXQof+/5ykCddCh/7/nKQJ10KH/v+cpAnXQof+//QoYVi5e/7/9ChhWLl7/v/0KGFYuXv+//QoYVi5e/78t3v9/////Py3e/3////8/Ld7/f////z8t3v9/////P+AI5zKyNv+/4AjnMrI2/7/gCOcysjb/v+AI5zKyNv+/aQnCE1QV/79pCcITVBX/v2kJwhNUFf+/aQnCE1QV/7+V9sITAABVtZX2whMAAFW1lfbCEwAAqcqV9sITAABVtdFjTCb///8/13j1Kv///z8nh/Uq////P9xsyCz///8/0WNMJv///z/RY0wm////PyKTyCz///8/LZxMJv///z8tnEwm////PwAATMn///+/AABMyf///78AAEzJ////vwAATMn///+///+q6v///7///6rq////v///qur///+///+q6v///79MSf9/////P0xJ/3////8/TEn/f////z9MSf9/////P7K2/3////8/srb/f////z+ytv9/////P7K2/3////8/AACq6v///78AAKrq////vwAAqur///+/AACq6v///7///0zJ////v///TMn///+///9Myf///7///0zJ////v9Eh/3////8/0SH/f////z/RIf9/////P9Eh/3////8/U5X/f////z9Tlf9/////P1OV/3////8/U5X/f////z///9Ch////v///0KH///+////Qof///7///9Ch////vwAA0KH///+/AADQof///78AANCh////vwAA0KH///+/q2r/f////z+rav9/////P6tq/3////8/q2r/f////z//fwAA/v//P/9/AAD///8//38AAP///z+1oaka////v5KtxQj///+/eqtpIf///7+FuDcL////v9DAyBz///+/0MDIHP///78er0kj////v89FYRz///+/hFRpIf///79sUsUI////v+BQSSP///+/eUc3C////7/gUEkj////vy4/yBz///+/aQk87FQV/79pCTzsVBX/v2kJPOxUFf+/aQk87FQV/7/gCBfNsjb/v+AIF82yNv+/4AgXzbI2/7/gCBfNsjb/v/0K5qkuXv+//QrmqS5e/7/9CuapLl7/v/0K5qkuXv+/5yn8itCh/7/nKfyK0KH/v+cp/IrQof+/5yn8itCh/78XTd+ITMn/vxdN34hMyf+/F03fiEzJ/78XTd+ITMn/vzxsaYmq6v+/PGxpiarq/788bGmJqur/vzxsaYmq6v+/wpNpif//VbXCk2mJ//+pysKTaYn//6nKwpNpif//VbXnst+I//+lpOey34j//1jb57LfiP//WNvnst+I//+lpBfW/Ir//+iQF9b8iv//Fu8X1vyK//8W7xfW/Ir//+iQAfXmqQAAFu8B9eapAADokAH15qkAAOiQAfXmqQAAFu8e9xfNAAClpB73F80AAKWkHvcXzQAApaQe9xfNAAClpJX2POwAAKnKlfY87AAAqcqV9jzsAACpypX2POwAAFW1///Qof///7///9Ch////v///0KH///+////Qof///79Lpi0c////v0umLRz///+/x6wjE////7/0qigH////v/SqKAf///+/9KooB////7+zWS0c////v7NZLRz///+/s1ktHP///783UyMT////vwpVKAf///+/ClUoB////79AdoLF////PyOnzcr///8/vomCxf///z/ZmX/D////PyOnzcr///8/21jNyv///z8lZn/D////P9tYzcr///8/21jNyv///z8AANCh////vwAA0KH///+/AADQof///78AANCh////v6tq/3////8/q2r/f////z+rav9/////P6tq/3////8/Ld7/f////z8t3v9/////Py3e/3////8/Ld7/f////z8AAEzJ////vwAATMn///+/AABMyf///78AAEzJ////v///qur///+///+q6v///7///6rq////v///qur///+/TEn/f////z9MSf9/////P0xJ/3////8/TEn/f////z+ytv9/////P7K2/3////8/srb/f////z+ytv9/////PwAAqur///+/AACq6v///78AAKrq////vwAAqur///+///9Myf///7///0zJ////v///TMn///+///9Myf///7/RIf9/////P9Eh/3////8/0SH/f////z/RIf9/////P1OV/3////8/U5X/f////z9Tlf9/////P1OV/3////8/c2ei3v///z9zZ6Le////P3Nnot7///8/K2+d2P///z+4eVHa////P0aGUdr///8/i5ih3v///z/TkJ3Y////P4uYod7///8/YKcr7////79coXLn////v6ylt/n///+/XKFy5////7+spbf5////v6ylt/n///+/Ulq3+f///7+iXnLn////v55YK+////+/Ulq3+f///7+iXnLn////v6Jecuf///+//3/+/////7//f/7//v//v/9//v////+/MLXb2P///78wtdvY////v3+8JOb///+/e7pA9v///797ukD2////v3u6QPb///+/zkrb2P///7/OStvY////v85K29j///+/f0Mk5v///7+DRUD2////v4NFQPb///+/SKMfL////z9Iox8v////P0ijHy////8/C46rT////z8LjqtP////PwuOq0////8/4FBJI////7/gUEkj////v8ec0ED///8/x5zQQP///z+qzwsO////v1QwCw7///+/aaF6K////z9poXor////P2mheiv///8/Hq9JI////78er0kj////vx6vSSP///+/f7gXLP///79/uBcs////v3+4Fyz///+/f7gXLP///783Y9BA////Pzdj0ED///8/N2PQQP///z+ZdSkz////P5l1KTP///8/mXUpM////z+2XB8v////P7ZcHy////8/Lj/IHP///78uP8gc////vy4/yBz///+/83GrT////z/zcatP////P/9//v////+//3/+/////7//f/7/////v/9//v////+/lfY87AAAqcqV9jzsAACpyv9///////+//3///////78e9xfNAAClpB73F80AAKWk/3/+/////7//f/7/////vwH15qkAAOiQAfXmqQAA6JD/f/7/////v/9//v////+/F9b8iv//Fu8X1vyK//8W7+ey34j//1jb57LfiP//WNv/f/7/////v/9//v////+/wpNpif//qcrCk2mJ//+pyjxsaYmq6v+/PGxpiarq/7//f/7/////v/9//v////+//3/+/////7//f/7/////vxdN34hMyf+/F03fiEzJ/7//f/7/////v/9//v////+/5yn8itCh/7/nKfyK0KH/v/0K5qkuXv+//QrmqS5e/7/gCBfNsjb/v+AIF82yNv+//3/+/////7//f/7/////v2kJPOxUFf+/aQk87FQV/7/QwMgc////v9DAyBz///+/J4f1Kv///z8nh/Uq////PyeH9Sr///8/f0cXLP///79/Rxcs////v39HFyz///+/f0cXLP///79hnDA6////P2GcMDr///8/YZwwOv///z9hnDA6////P9VMZgr///+/1UxmCv///7/VTGYK////v9VMZgr///+/ZYopM////z9liikz////P2WKKTP///8/ZYopM////z/nU384////P+dTfzj///8/51N/OP///z/nU384////Py+6YRz///+/L7phHP///78vumEc////vy+6YRz///+/nWMwOv///z+dYzA6////P51jMDr///8/Fqx/OP///z8WrH84////Pxasfzj///8/Fqx/OP///z/PRWEc////v89FYRz///+/z0VhHP///78ps2YK////vymzZgr///+/KbNmCv///78ps2YK////v2Kg6gb///+/YqDqBv///79ioOoG////v2Kg6gb///+/UFt2Ev///79QW3YS////v1BbdhL///+/UFt2Ev///7+omrUh////P6iatSH///8/qJq1If///z+Iba8k////P4htryT///8/iG2vJP///z+Iba8k////P66kdhL///+/rqR2Ev///7+upHYS////v66kdhL///+/VmW1If///z9WZbUh////P1ZltSH///8/VmW1If///z/qhmMg////P+qGYyD///8/6oZjIP///z/qhmMg////P5xf6gb///+/nF/qBv///7+cX+oG////v5xf6gb///+/dpKvJP///z92kq8k////P3aSryT///8/dpKvJP///z9JXqka////v0leqRr///+/SV6pGv///79JXqka////vxR5YyD///8/FHljIP///z8UeWMg////PxR5YyD///8/taGpGv///7+1oaka////v7WhqRr///+//38AAP///z//fwAA/v//P/9/AAD+//8//38AAP///z//fwAA////P/9/AAD///8//38AAP7//z//fwAA////P/9/AAD+//8/zLD4Fv///7/MsPgW////v8yw+Bb///+/zLD4Fv///7+SrcUI////v5KtxQj///+/kq3FCP///78Gacww////PwZpzDD///8/BmnMMP///z8Gacww////P/iWzDD///8/+JbMMP///z/4lsww////P/iWzDD///8/xIiSLf///z/EiJIt////P8SIki3///8/xIiSLf///z8yT/gW////vzJP+Bb///+/Mk/4Fv///78yT/gW////v4RUaSH///+/hFRpIf///7+EVGkh////v3qraSH///+/eqtpIf///796q2kh////vzp3ki3///8/OneSLf///z86d5It////Pzp3ki3///8/bFLFCP///79sUsUI////v2xSxQj///+/lV56K////z+VXnor////P5Veeiv///8/EK3vIv///78Qre8i////vxCt7yL///+/EK3vIv///7/HdIY4////P8d0hjj///8/x3SGOP///z/HdIY4////P+5S7yL///+/7lLvIv///7/uUu8i////v+5S7yL///+/SJq7Nv///z9Imrs2////P0iauzb///8/SJq7Nv///z95RzcL////v3lHNwv///+/eUc3C////783i4Y4////PzeLhjj///8/N4uGOP///z83i4Y4////Pw9dES3///8/D10RLf///z8PXREt////Pw9dES3///8/urZJGv///7+6tkka////v7q2SRr///+/urZJGv///7+2Zbs2////P7Zluzb///8/tmW7Nv///z+2Zbs2////P++iES3///8/76IRLf///z/vohEt////P++iES3///8/RElJGv///79ESUka////v0RJSRr///+/RElJGv///7+FuDcL////v4W4Nwv///+/hbg3C////79pCTzsVBX/v2kJPOxUFf+/4AgXzbI2/7/gCBfNsjb/v/0K5qkuXv+//QrmqS5e/7/nKfyK0KH/v+cp/IrQof+/F03fiEzJ/78XTd+ITMn/vzxsaYmq6v+/PGxpiarq/7/Ck2mJ//9VtcKTaYn//6nK57LfiP//paTnst+I//+lpBfW/Ir//+iQF9b8iv//6JAB9eapAAAW7wH15qkAABbvHvcXzQAApaQe9xfNAAClpJX2POwAAFW1lfY87AAAqcpLpi0c////v0umLRz///+/s1ktHP///7/XePUq////P9d49Sr///8/13j1Kv///z8tnEwm////Py2cTCb///8/N1MjE////783UyMT////vzdTIxP///+/9KooB////7/cbMgs////P9xsyCz///8/3GzILP///z8ik8gs////PyKTyCz///8/IpPILP///z8KVSgH////vwpVKAf///+/x6wjE////7/HrCMT////v8esIxP///+/0WNMJv///z++iYLF////P76JgsX///8/vomCxf///z9zZ6Le////P2CnK+////+/YKcr7////79gpyvv////v1Jat/n///+/Ulq3+f///7/TkJ3Y////P9OQndj///8/05Cd2P///z8rb53Y////Pytvndj///8/K2+d2P///z+spbf5////v55YK+////+/nlgr7////7+eWCvv////v4uYod7///8/i5ih3v///z+4eVHa////P7h5Udr///8/uHlR2v///z+iXnLn////v1yhcuf///+/XKFy5////79GhlHa////P0aGUdr///8/RoZR2v///z//f/7/////v/9//v/+//+//3/+//7//7//f/7/////v/9//v////+//3/+//7//7//f/7/////v/9//v/+//+//3/+//7//78wtdvY////vzC129j///+/zkrb2P///79AdoLF////P0B2gsX///8/QHaCxf///z8jp83K////PyOnzcr///8/f0Mk5v///79/QyTm////v39DJOb///+/e7pA9v///78lZn/D////PyVmf8P///8/JWZ/w////z/ZmX/D////P9mZf8P///8/2Zl/w////z+DRUD2////v4NFQPb///+/f7wk5v///79/vCTm////v3+8JOb///+/21jNyv///z8=")
-}]
-blend_shape_mode = 0
diff --git a/assets/characters/player/meshes/godot_robot_chest.tres b/assets/characters/player/meshes/godot_robot_chest.tres
deleted file mode 100644
index 7d64390..0000000
--- a/assets/characters/player/meshes/godot_robot_chest.tres
+++ /dev/null
@@ -1,29 +0,0 @@
-[gd_resource type="ArrayMesh" format=4 uid="uid://dm4j7r8tq2x5c"]
-
-[ext_resource type="Texture2D" uid="uid://bdg48m6l86q8i" path="res://assets/characters/player/3DGodotRobot_GodotPalette.png" id="1_w11jc"]
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_4omi2"]
-resource_name = "Material"
-cull_mode = 2
-albedo_texture = ExtResource("1_w11jc")
-roughness = 0.4
-
-[resource]
-resource_name = "3DGodotRobot_Cube_004"
-_surfaces = [{
-"aabb": AABB(-0.25724164, 0.6743469, -0.20666228, 0.5205259, 0.34014213, 0.47521824),
-"attribute_data": PackedByteArray("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"),
-"bone_aabbs": [AABB(0, 0, 0, -1, -1, -1), AABB(-0.25724164, 0.6743469, -0.20666228, 0.5205258, 0.29895842, 0.47521824), AABB(-0.25724164, 0.6743469, -0.20666228, 0.5205258, 0.29895842, 0.47521824), AABB(-0.21168837, 0.7613885, -0.10322067, 0.4667986, 0.25310057, 0.23705861), AABB(-0.062284153, 0.8376294, -0.0349461, 0.1306109, 0.17685968, 0.13665348), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(-0.1615839, 0.6743469, -0.20666228, 0.41362312, 0.29895842, 0.47521824), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(-0.24906763, 0.6743469, -0.20666228, 0.41669416, 0.29895842, 0.47521824), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0.18553643, 0.68345, -0.10715394, 0.07774781, 0.16880333, 0.25047332), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(-0.25724164, 0.68345, -0.10715394, 0.07774781, 0.16880333, 0.25047332)],
-"format": 34359745559,
-"index_count": 984,
-"index_data": PackedByteArray("VgFXAVgBVgFZAVcBWgFbAVwBWgFdAVsBDABeAQ0ADAAOAF4BEwAVAF8BEwAUABUAYAFhAWIBYAFjAWEBZAFlAWYBZAFnAWUBaAFpASEAaAFqAWkBawFsAQYAawFtAWwBbgEqAG8BbgEnACoAcAEYABkAcAFxARgAcgEbABwAcgFzARsAdAEBAAIAdAF1AQEAAAB2AXcBAAADAHYBHgB4ARoAHgAdAHgBLgAvADAALgAxAC8AeQEQAAoAeQERABAAegEFAHsBegF8AQUAfQF+AX8BfQGAAX4BgQGCAYMBgQGEAYIBMgCFATQAMgCGAYUBhwGIATUAhwGJAYgBigEzAIsBigGMATMAIACNAY4BIACPAY0BkAGRAZIBkAEpAJEBQwCTAUUAQwCUAZMBRwCVAUkARwCWAZUBSwCXAU0ASwCYAZcBHwCZAZoBHwCbAZkBnAGdAZ4BnAGfAZ0BoAGhAaIBoAGjAaEBpAFMAKUBpAFOAEwApgEiAKcBpgEjACIAqAFIAKkBqAFKAEgAqgEHAKsBqgEEAAcArAFEAK0BrAFGAEQArgESAK8BrgEPABIATwBQAFEATwBSAFAAUwBUAFUAUwBWAFQAWQBaAFsAWQBcAFoAsAFXAFgAsAGxAVcAXQBeAF8AXQBgAF4AYQBiAGMAYQBkAGIAsgFnALMBsgG0AWcAcwC1AbYBcwC3AbUBuAG5AWkAuAG6AbkBdQC7AbwBdQC9AbsBvgG/AWUAvgHAAb8BdwDBAcIBdwDDAcEBxAHFAWgAxAHGAcUBxwHIAckBxwF2AMgBygFqAMsBygHMAWoAzQHOAc8BzQF0AM4B0AFmANEB0AHSAWYA0wHUAdUB0wF4ANQBgQCCAIMAgQCEAIIAhQCGAIcAhQCIAIYAiQCKAIsAiQCMAIoAjQCOAI8AjQCQAI4A1gE3ANcB1gE4ADcA2AEWABcA2AHZARYA2gHbAdwB2gELANsB3QEIAN4B3QEJAAgAkQCSAN8BkQCUAJIAlgDgAZcAlgDhAeABlwDgAZMAlwCTAJgA4gGVAOMB4gHkAZUAmQCaAJsAmQCcAJoAnQCeAJ8AnQCgAJ4AoQCiAKMAoQCkAKIApQCmAKcApQCoAKYAqQCqAKsAqQCsAKoArQCuAK8ArQCwAK4AsQCyALMAsQC0ALIAtQC2ALcAtQC4ALYAuQC6ALsAuQC8ALoAvQC+AL8AvQDAAL4AwQDlAeYBwQDEAOUB5wHoAekB5wHqAegByQDHAOsByQDLAMcA7AHtAe4B7AHvAe0B8AHGAPEB8AHCAMYA8gHFAPMB8gH0AcUA9QH2AfcB9QH4AfYB+QH6AdMA+QH7AfoB1gDXANgA1gDZANcA3QD8Ad8A3QD9AfwB5AD+Af8B5AAAAv4BAQICAgMCAQIEAgICBQLpAAYCBQLrAOkABwLSAAgCBwLUANIAKAAsAC0AKAAJAiwA4QDiAOAA4QDjAOIACgLlAAsCCgIMAuUADQI5AA4CDQJAADkADwIQAkEADwIRAhACEgITAhQCEgIVAhMCFgLuABcCFgLtAO4A8QDyAPMA8QD0APIA0QAYAtUA0QAZAhgC6AAaAuoA6AAbAhoCHAIdAh4CHAImAB0CHwIgAjYAHwIhAiACOwAiAiMCOwAkAiICJQI6ACYCJQInAjoAKAIkACkCKAIlACQAKgIrAisAKgIsAisCLQIuAi8CLQIwAi4CMQIyAjMCMQI0AjIC7AA1AjYC7AA3AjUC9QA4AjkC9QD4ADgC+QA6AjsC+QD8ADoC/QA8Aj0C/QAAATwCPgLvAPAAPgI/Au8AQAL2APcAQAJBAvYAQgLmAOcAQgJDAuYARAL+AP8ARAJFAv4ARgJCAD8ARgJHAkIASAL6APsASAJJAvoAAQECAQMBAQEEAQIBBQFKAgcBBQFLAkoCCgELAQwBCgENAQsBCAEGAUwCCAEJAQYBDgEPARABDgERAQ8BEgETARQBEgEVARMBTQJxAG8ATQJOAnEAfwBPAlACfwB+AE8CbgBRAlICbgBTAlECVAJVAlYCVAJ7AFUCVwJYAm0AVwJZAlgCWgJbAlwCWgJ6AFsCcABdAl4CcAByAF0CXwJ9AIAAXwJgAn0AYQJiAmsAYQJjAmICZAJ8AGUCZAJmAnwAZwJoAmwAZwJpAmgCagJ5AGsCagJsAnkAFgEXARgBFgEZARcBGgEbARwBGgEdARsBHgEfASABHgEhAR8BIgEjASQBIgElASMBbQI9AD4AbQI8AD0AbgJvAt4AbgJwAm8CcQLaAHICcQJzAtoAdALcAHUCdALbANwAJgEnASgBJgF2AicBKgF3AngCKgF5AncCKwEsAXoCKwEtASwBewIpAXwCewJ9AikBLgEvATABLgExAS8BMgEzATQBMgE1ATMBNgE3ATgBNgE5ATcBOgE7ATwBOgE9ATsBPgE/AUABPgFBAT8BQgFDAUQBQgFFAUMBRgFHAUgBRgFJAUcBSgFLAUwBSgFNAUsBTgFPAVABTgFRAU8BUgFTAVQBUgFVAVMBfgLMAH8CfgKAAswAgQLIAMoAgQLDAMgAggKDAoQCggKFAoMChgLPANAAhgLOAM8AhwKIAokChwKKAogCiwKMAs0AiwKNAowC"),
-"lods": [0.10994574, PackedByteArray("AAABAAIAAAADAAEABAAFAAYABAAHAAUABgAFAAgABQAJAAgACgALAAkADAALAA0ADAAOAAsADwAQAAoADwARABAADwASABEAEwAUABUAEwAVABYAEwAWABcAFwAWABgAFwAYABkAGgAbABwAGgAdABsAHgAdABoAHwAgACEAHwAiACAAHwAjACIAIAAkACUAJgAlACQAJwAkACAAKAAmACQAKQAkACcAJwAqACkAKwAoACQAKAArACwAKAAsAC0ALgAvADAALgAxAC8AMgAzADQANAAzADUAMgA2ADMAMwA3ADUAMwA4ADcAOQA6ADYAOQA7ADoAOwA8ADoAOgA8AD0AOgA9AD4APwBAADkAPwA5AEEAPwBBAEIAQwBEAEUAQwBGAEQARwBIAEkARwBKAEgASwBMAE0ASwBOAEwATwBQAFEATwBSAFAAUwBUAFUAUwBWAFQAVQBUAFcAVQBXAFgAWQBaAFsAWQBcAFoAXQBeAF8AXQBgAF4AYQBiAGMAYQBkAGIAZQBmAGcAZQBnAGgAaQBmAGUAaQBqAGYAaQBrAGoAaQBsAGsAaQBtAGwAbgBtAGkAbQBvAGwAbgBwAG0AcABvAG0AcABxAG8AcAByAHEAcwB0AHUAcwB2AHQAdQB0AHcAdAB4AHcAdwB4AHkAdwB5AHoAewB6AHkAewB5AHwAfQB7AHwAfgB7AH0AfwB+AH0AfwB9AIAAgQCCAIMAgQCEAIIAhQCGAIcAhQCIAIYAiQCKAIsAiQCMAIoAjQCOAI8AjQCQAI4AkQCSAJMAkQCUAJIAkwCSAJUAlgCRAJMAlgCTAJcAlwCTAJgAmQCaAJsAmQCcAJoAnQCeAJ8AnQCgAJ4AoQCiAKMAoQCkAKIApQCmAKcApQCoAKYAqQCqAKsAqQCsAKoArQCuAK8ArQCwAK4AsQCyALMAsQC0ALIAtQC2ALcAtQC4ALYAuQC6ALsAuQC8ALoAvQC+AL8AvQDAAL4AwQDCAMMAwQDEAMIAwQDFAMQAwwDCAMYAwQDHAMUAwwDGAMgAyQDHAMEAwwDIAMoAyQDLAMcAwwDKAMwAyQDNAMsAzgDDAMwAyQDOAM0AzgDMAM8AzgDQAM0AzgDPANAA0QDSANMA0QDUANIA0QDTANUA1gDXANgA1gDZANcA2QDaANcA2QDbANoA2wDcANoA3QDeAN8A3QDgAN4A3QDhAOAA4QDiAOAA4QDjAOIA5ADdAOUA5ADlAOYA5ADmAOcA6ADpAOoA6ADrAOkA7ADtAO4A7ADuAO8A7ADvAPAA8QDyAPMA8QD0APIA9QD2APcA9QD4APYA+QD6APsA+QD8APoA/QD+AP8A/QAAAf4AAQECAQMBAQEEAQIBBQEGAQcBBQEIAQYBCAEJAQYBCgELAQwBCgENAQsBDgEPARABDgERAQ8BEgETARQBEgEVARMBFgEXARgBFgEZARcBGgEbARwBGgEdARsBHgEfASABHgEhAR8BIgEjASQBIgElASMBJgEnASgBJgEpAScBKgEpASYBKgErASkBKwEsASkBKwEtASwBLgEvATABLgExAS8BMgEzATQBMgE1ATMBNgE3ATgBNgE5ATcBOgE7ATwBOgE9ATsBPgE/AUABPgFBAT8BQgFDAUQBQgFFAUMBRgFHAUgBRgFJAUcBSgFLAUwBSgFNAUsBTgFPAVABTgFRAU8BUgFTAVQBUgFVAVMB")],
-"material": SubResource("StandardMaterial3D_4omi2"),
-"name": "Material",
-"primitive": 3,
-"skin_data": PackedByteArray("AgABAA0ACQCSh2x4AAAAAAIAAQAJAA0AN8nHNgAAAAACAAEADQAAAFXGqTkAAAAAAgABAAkADQAFkPlvAAAAAAIAAQANAAkAfIiCdwAAAAADAAEAAgAJAMSiFTUkKAAAAgABAAkADQDuuhBFAAAAAAEAAgARAAkAsJFNbgAAAAADAAkAAAAAAP//AAAAAAAAAwAJAAAAAAD//wAAAAAAAAMAAQACAAkAxKIVNSQoAAADAAAAAAAAAP//AAAAAAAAAwABAAIAEQB8wPcpixUAAAMAAQACAAkAxKIVNSQoAAADAAEACQAAACP22wkAAAAAAQACABEACQCwkU1uAAAAAAMAAQACABEAfMD3KYsVAAADAAEAAgARAAaJhlFvJQIAAgABABEAAACdgV9+AgAAAAMAAgABAA0AF9DTJhMJAAADAAAAAAAAAP//AAAAAAAAAwAJAAAAAAD//wAAAAAAAAMAAQAJAAAAr/pPBQAAAAADAAEAAgAJAImWADh1MQAAAwABAAkAAAAj9tsJAAAAAAMAAQACABEAfMD3KYsVAAADAAEAAgAJAImWADh1MQAAAgABAA0AAABVxqk5AAAAAAMAAgABAA0AF9DTJhMJAAACAAEADQAJAJKHbHgAAAAAAQADAAIACQDDbvhSQj4AAAIAAQAJAA0AiK52UQAAAAACAAEACQANAO66EEUAAAAAAgABAAkADQCWzFwzBQAFAAIAAQANAAkAfIiCdwAAAAACAAEACQANAH+cf2MAAAAAAgABAAkADQAtytA1AAAAAAIAAQAJAA0AlsxcMwUABQACAAEADQAJAO66EEUAAAAAAwAJAAAAAAD//wAAAAAAAAMAAgANAAkAtuZIGQAAAAADAAIACQAAADvwww8AAAAAAwAJAAAAAAD//wAAAAAAAAMAAgANAAAAA977IQAAAAADAA0AAAAAAP//AAAAAAAAAwANAAAAAAD//wAAAAAAAAMAAQACABEABomGUW8lAgADAAEAAgAJAImWADh1MQAAAwABAAIAEQB8wPcpixUAAAEAAwACAAkAw274UkI+AAACAAEACQANADfJxzYAAAAAAwACAAkAAAA78MMPAAAAAAIAAQANAAAAVcapOQAAAAADAAIAAQANABfQ0yYTCQAAAgABAAkADQACyPs3AAAAAAMAAAAAAAAA//8AAAAAAAADAAkAAAAAAP//AAAAAAAAAgABAAkADQA3ycc2AAAAAAMAAgANAAAAA977IQAAAAACAAEACQAAAFXGqTkAAAAAAwACAAEACQDEva01jAwAAAMAAAAAAAAA//8AAAAAAAADAA0AAAAAAP//AAAAAAAAAgABAAkADQBSlKxrAAAAAAIAAQAJAAAAVcapOQAAAAACAAEACQANAAWQ+W8AAAAAAgABAAkADQCFmnhlAAAAAAIAAQANAAkAkodseAAAAAACAAEACQANAIWaeGUAAAAAAgABAAkADQAFkPlvAAAAAAIAAQANAAkAUpSsawAAAAABAAMAAgAJAMNu+FJCPgAAAgABAA0ACQBSlKxrAAAAAAIAAQANAAkAkodseAAAAAABAAIAEQAJABiN5nIAAAAAAwABAAIAEQAGiYZRbyUCAAEAAgARAAkAGI3mcgAAAAABAAMAAgAJAMNu+FJCPgAAAgABABEAAACdgV9+AgAAAAIAAQARAAAAnYFffgIAAAACAAEAEQAJABSJ6nYAAAAAAQACABEACQAYjeZyAAAAAAIAAQARAAkAYZOdbAAAAAACAAEACQANAH+cf2MAAAAAAgABAAkADQAPoe9eAAAAAAIAAQANAAkAfIiCdwAAAAACAAEACQANAOquE1EAAAAAAgABABEACQAVhOR7BQAAAAEAAgARAAkAsJFNbgAAAAABAAIAEQAJABiN5nIAAAAAAgABAA0ACQBhp51YAAAAAAIAAQANAAkAUpSsawAAAAACAAEAEQAJABSJ6nYAAAAAAgABAA0ACQBSlKxrAAAAAAIAAQAJAA0AoaxdUwAAAAACAAEACQANAIWaeGUAAAAAAgABAA0ACQBhp51YAAAAAAEAAgARAAkAsJFNbgAAAAACAAEAEQAJAGGTnWwAAAAAAgABABEAAACdgV9+AgAAAAIAAQARAAkAFYTkewUAAAACAAEAEQAJABWE5HsFAAAAAgABAA0ACQDN0TAuAAAAAAIAAQAJAAAAgtp8JQAAAAACAAEAEQAJAGGTnWwAAAAAAgABAAkADQAPoe9eAAAAAAIAAQAJAA0Aj9FvLgAAAAACAAEACQANAMzRMi4AAAAAAgABAA0ACQCP0W8uAAAAAAIAAQAJAA0AhbV5SgAAAAACAAEACQANAOquE1EAAAAAAgABAAkADQDN0TAuAAAAAAIAAQAVAA0AFYTkewUAAAACAAEADQAAAILafCUAAAAAAgABABUADQBhk51sAAAAAAIAAQARAAkAYZOdbAAAAAACAAEACQAAACDW3ikAAAAAAgABABEACQAUiep2AAAAAAIAAQAJAAAAgtp8JQAAAAACAAEADQAJAGGnnVgAAAAAAgABAA0ACQCT2WsmAAAAAAIAAQAJAA0ALNrSJQAAAAACAAEACQANAKGsXVMAAAAAAgABAAkADQBhp51YAAAAAAIAAQAJAA0Ak9lrJgAAAAACAAEADQAAACDW3ikAAAAAAgABABUADQAUiep2AAAAAAIAAQAVAA0AYZOdbAAAAAACAAEADQAAAILafCUAAAAAAwAJAAAAAAD//wAAAAAAAAMACQAAAAAA//8AAAAAAAADAAAAAAAAAP//AAAAAAAAAwAJAAAAAAD//wAAAAAAAAMAAQAJAAAAr/pPBQAAAAADAAkAAAAAAP//AAAAAAAAAwABAAkAAAAj9tsJAAAAAAMACQAAAAAA//8AAAAAAAADAAAAAAAAAP//AAAAAAAAAwAJAAAAAAD//wAAAAAAAAMACQAAAAAA//8AAAAAAAADAAkAAAAAAP//AAAAAAAAAwAJAAAAAAD//wAAAAAAAAMACQAAAAAA//8AAAAAAAADAAkAAAAAAP//AAAAAAAAAwAJAAAAAAD+/wAAAAAAAAMACQAAAAAA//8AAAAAAAADAAkAAAAAAP//AAAAAAAAAwAJAAAAAAD//wAAAAAAAAMACQAAAAAA//8AAAAAAAADAAkAAAAAAP//AAAAAAAAAwAJAAAAAAD//wAAAAAAAAMACQAAAAAA//8AAAAAAAADAAkAAAAAAP7/AAAAAAAAAwAJAAAAAAD//wAAAAAAAAMACQAAAAAA/v8AAAAAAAADAAkAAAAAAP//AAAAAAAAAwAJAAAAAAD//wAAAAAAAAMAAQAJAAAAI/bbCQAAAAADAAkAAAAAAP//AAAAAAAAAwAAAAAAAAD//wAAAAAAAAMACQAAAAAA//8AAAAAAAADAAkAAAAAAP//AAAAAAAAAwAJAAAAAAD//wAAAAAAAAMAAQAJAAAAr/pPBQAAAAADAAkAAAAAAP//AAAAAAAAAwAAAAAAAAD//wAAAAAAAAMACQAAAAAA//8AAAAAAAADAAkAAAAAAP//AAAAAAAAAwAJAAAAAAD//wAAAAAAAAQAAwAAAAAA/v8AAAAAAAAEAAMAAAAAAP3/AQAAAAAABAADAAAAAAD3/wcAAAAAAAQAAwAAAAAA8/8LAAAAAAAEAAMAAAAAAPP/CwAAAAAABAADAAAAAAD8/wIAAAAAAAQAAwAAAAAA/f8BAAAAAAAEAAMAAAAAAJT/agAAAAAABAADAAAAAACU/2oAAAAAAAQAAwAAAAAA9v8IAAAAAAAEAAMAAAAAAPz/AgAAAAAABAADAAAAAAD8/wIAAAAAAAQAAwAAAAAA/P8CAAAAAAAEAAMAAAAAANP/KwAAAAAABAADAAAAAAD2/wgAAAAAAAQAAwAAAAAA/v8AAAAAAAAEAAMAAAAAAP7/AAAAAAAABAADAAAAAABo/pYBAAAAAAQAAwAAAAAA0/8rAAAAAAAEAAMAAAAAAPz/AgAAAAAABAADAAAAAAD8/wIAAAAAAAQAAwAAAAAA/P8CAAAAAAAEAAMAAAAAAGj+lgEAAAAABAADAAAAAAD//wAAAAAAAAMAAgAJAAAAO/DDDwAAAAACAA0ACQAAAP7/AAAAAAAAAgABAAkADQAtytA1AAAAAAIACQAAAAAA//8AAAAAAAACAAEADQAJAK7fUCAAAAAAAgABAAkABADSxiU5BgAAAAIAAQANAAkAD8bvOQAAAAACAAEABAAJAB7KzDUTAAEAAgABAAkADQACyPs3AAAAAAIAAQAEAA0An8YXOUEABgACAAEACQANAAPI+zcAAAAAAgAEAAkADQDg/x4AAAAAAAIAAQAJAA0AD8bvOQAAAAADAAIADQAAAAPe+yEAAAAAAgAEAA0AAAD//wAAAAAAAAIAAQAJAA0Art9QIAAAAAABAAIACQAAABuB434AAAAAAwACAA0ACQC25kgZAAAAAAMAAQACAA0Ax6GlNZEoAAACAAEADQAJAO66EEUAAAAAAQACAA0AAABJqLVXAAAAAAMAAQACABUAS8LEKO4UAAADAAAAAAAAAP//AAAAAAAAAwABAA0AAABG9LgLAAAAAAMAAQACAA0Ax6GlNZEoAAADAA0AAAAAAP//AAAAAAAAAwACAA0ACQC25kgZAAAAAAMADQAAAAAA//8AAAAAAAADAAIAAQAJAMS9rTWMDAAAAwANAAAAAAD//wAAAAAAAAMAAAAAAAAA//8AAAAAAAADAAEADQAAAMb6OAUAAAAAAwABAAIADQAcixE+0TYAAAMAAQANAAAARvS4CwAAAAADAAEAAgAVAEvCxCjuFAAAAwABAAIADQAcixE+0TYAAAIAAQAJAAAAVcapOQAAAAACAAEACQANAFKUrGsAAAAAAQACABUADQAYjeZyAAAAAAIAAQAJAA0AiK52UQAAAAACAAEADQAJAO66EEUAAAAAAQACAAkAAAAbgeN+AAAAAAIAAQAJAA0AlsxcMwUABQABAAIAFQANAGyvjFAFAAAAAwABAAIAFQBLwsQo7hQAAAMAAQACAA0AHIsRPtE2AAABAAIAFQANABiN5nIAAAAAAgABABUAAACdgV9+AgAAAAEAAgANAAAASai1VwAAAAADAAEAAgAVAEvCxCjuFAAAAQACABUADQBsr4xQBQAAAAMAAQACAA0Ax6GlNZEoAAACAAEACQANAIiudlEAAAAAAgABAAkADQB8iIJ3AAAAAAIAAQAJAA0Af5x/YwAAAAABAAIACQAAABuB434AAAAAAQACAA0AAABJqLVXAAAAAAIAAQAVAAAAnYFffgIAAAABAAIAFQANALCRTW4AAAAAAQACABUADQBsr4xQBQAAAAEAAgAJAAAAG4HjfgAAAAABAAIAFQANALCRTW4AAAAAAgABAAkADQB8iIJ3AAAAAAEAAgANAAAASai1VwAAAAACAAEAFQAAAJ2BX34CAAAAAgABABUADQAUiep2AAAAAAIAAQAVAA0AYZOdbAAAAAABAAIAFQANABiN5nIAAAAAAgABAAkADQB/nH9jAAAAAAIAAQAVAA0AFYTkewUAAAACAAEACQANAOquE1EAAAAAAgABAAkADQB8iIJ3AAAAAAEAAgAVAA0AsJFNbgAAAAABAAIAFQANABiN5nIAAAAAAgABAAkADQBhp51YAAAAAAIAAQAVAA0AFInqdgAAAAACAAEACQANAFKUrGsAAAAAAgABAAkADQBSlKxrAAAAAAIAAQAJAA0AoaxdUwAAAAACAAEACQANAGGnnVgAAAAAAgABAAkADQCFmnhlAAAAAAEAAgAVAA0AsJFNbgAAAAACAAEAFQANAGGTnWwAAAAAAgABABUADQAVhOR7BQAAAAIAAQAVAAAAnYFffgIAAAADAA0AAAAAAP//AAAAAAAAAwANAAAAAAD//wAAAAAAAAMADQAAAAAA//8AAAAAAAADAAAAAAAAAP//AAAAAAAAAwABAA0AAADG+jgFAAAAAAMADQAAAAAA//8AAAAAAAADAA0AAAAAAP//AAAAAAAAAwABAA0AAABG9LgLAAAAAAMAAAAAAAAA//8AAAAAAAADAA0AAAAAAP//AAAAAAAAAwANAAAAAAD//wAAAAAAAAMADQAAAAAA//8AAAAAAAADAA0AAAAAAP//AAAAAAAAAwANAAAAAAD//wAAAAAAAAMADQAAAAAA/v8AAAAAAAADAA0AAAAAAP//AAAAAAAAAwANAAAAAAD//wAAAAAAAAMADQAAAAAA//8AAAAAAAADAA0AAAAAAP//AAAAAAAAAwANAAAAAAD//wAAAAAAAAMADQAAAAAA//8AAAAAAAADAA0AAAAAAP//AAAAAAAAAwANAAAAAAD//wAAAAAAAAMADQAAAAAA/v8AAAAAAAADAA0AAAAAAP//AAAAAAAAAwANAAAAAAD+/wAAAAAAAAMADQAAAAAA//8AAAAAAAADAA0AAAAAAP//AAAAAAAAAwABAA0AAABG9LgLAAAAAAMADQAAAAAA//8AAAAAAAADAA0AAAAAAP//AAAAAAAAAwAAAAAAAAD//wAAAAAAAAMADQAAAAAA//8AAAAAAAADAA0AAAAAAP//AAAAAAAAAwANAAAAAAD//wAAAAAAAAMAAQANAAAAxvo4BQAAAAADAAAAAAAAAP//AAAAAAAAAwANAAAAAAD//wAAAAAAAAMADQAAAAAA//8AAAAAAAADAA0AAAAAAP//AAAAAAAABAADAAAAAAD+/wAAAAAAAAQAAwAAAAAA+f8FAAAAAAAEAAMAAAAAAPv/AwAAAAAABAADAAAAAAD3/wcAAAAAAAQAAwAAAAAA+/8DAAAAAAAEAAMAAAAAAOf/FwAAAAAABAADAAAAAADY/yYAAAAAAAQAAwAAAAAA+f8FAAAAAAAEAAMAAAAAANj/JgAAAAAABAADAAAAAAD+/wAAAAAAAAQAAwAAAAAA/v8AAAAAAAAEAAMAAAAAAOf/FwAAAAAABAADAAAAAAD+/wAAAAAAAAQAAwAAAAAA/v8AAAAAAAAEAAMAAAAAAOv/EwAAAAAABAADAAAAAAD+/wAAAAAAAAQAAwAAAAAA6/8TAAAAAAAEAAMAAAAAAPv/AwAAAAAABAADAAAAAAD+/wAAAAAAAAQAAwAAAAAA/v8AAAAAAAAEAAMAAAAAAP7/AAAAAAAABAADAAAAAAD8/wIAAAAAAAQAAwAAAAAA//8AAAAAAAAEAAMAAAAAAPv/AwAAAAAAAgABAA0ACQBRra1SAAAAAAIAAQAJAA0AEszsMwAAAAACAAEADQAJAAnJ9DYAAAAAAgABAAkADQATs+tMAAAAAAIAAQANAAAAl6ZnWQAAAAADAAIAAQAJAJjPghrjFQAAAgABAA0AAADJsjVNAAAAAAMAAQACAAkAxKIVNSQoAAADAAAAAAAAAP//AAAAAAAAAwACAAEADQBGulIrZRoAAAMAAQACAAkAiZYAOHUxAAACAAEADQAJAAnJ9DYAAAAAAwACAAEADQBGulIrZRoAAAIAAQANAAkAUa2tUgAAAAACAAEACQANAF3CoT0AAAAAAgABAA0AAADJsjVNAAAAAAIAAQAJAA0AhNF6LgAAAAACAAEADQAAAJemZ1kAAAAAAgABAAkADQCE0XouAAAAAAIAAQAJAA0A7roQRQAAAAACAAEADQAAAMmyNU0AAAAAAgABAA0AAADJsjVNAAAAAAMAAgAJAA0A4PAeDwAAAAADAAIAAQAJAJjPghrjFQAAAwACAAkADQDg8B4PAAAAAAMACQAAAAAA//8AAAAAAAADAAEAAgAJAImWADh1MQAAAwABAAkAAACv+k8FAAAAAAMAAgABAA0ARrpSK2UaAAACAAEADQAJAAnJ9DYAAAAAAgABAA0ACQAJyfQ2AAAAAAIAAQAJAA0AEszsMwAAAAACAAEACQANABOz60wAAAAAAgABAA0ACQBRra1SAAAAAAIAAQANAAkAUa2tUgAAAAADAAEAAgAJAFl7MldyLQAAAQACAA0AAAAbgeN+AAAAAAIAAQANAAAAl6ZnWQAAAAADAAEAAgAJAFl7MldyLQAAAgABAAkADQCIrnZRAAAAAAIAAQANAAAAl6ZnWQAAAAACAAEACQANAF3CoT0AAAAAAQACAA0AAAAbgeN+AAAAAAMAAgAJAA0A4PAeDwAAAAACAAEACQANANfGJzkAAAAAAgABAAkADQDuuhBFAAAAAAMAAgAJAA0AdeOJHAAAAAACAAEADQAJAA/G7zkAAAAAAgABAAkADQACyPs3AAAAAAIAAQANAAAAVcapOQAAAAADAAIAAQANAM25VT3cCAAAAgABAA0ACQAPxu85AAAAAAMAAgABAA0AF9DTJhMJAAADAAkAAAAAAP//AAAAAAAAAwACAAEADQDNuVU93AgAAAIAAQAJAA0ALcrQNQAAAAACAAEACQANAJbMXDMFAAUAAgABAAkADQDXxic5AAAAAAMACQAAAAAA//8AAAAAAAADAAIACQANAHXjiRwAAAAAAwACAAkADQDg8B4PAAAAAAIAAQAJAA0A0JIubQAAAAACAAEADQAJAP6LAHQAAAAAAgABAA0ACQD+iwB0AAAAAAEAAgARAAkABJ35YgEAAAABAAIAEQAJAASd+WIBAAAAAQACAAMAEQBZlIhrHAAAAAEAAgANAAkAsYVNegAAAAABAAIADQAAABuB434AAAAAAgABAAkADQAfmN9nAAAAAAMAAQACAAkAWXsyV3ItAAABAAIAAwARAFmUiGscAAAAAwABAAIAEQAGiYZRbyUCAAEAAgADABEAB6E1XcEBAAABAAIADQAAABuB434AAAAAAQACAAMAEQAHoTVdwQEAAAMAAQACAAkAWXsyV3ItAAABAAIADQAJALGFTXoAAAAAAQACAAMAEQBZlIhrHAAAAAEAAgARAAkABJ35YgEAAAACAAEACQANAB+Y32cAAAAAAQACAA0ACQCxhU16AAAAAAEAAgARAAkABJ35YgEAAAACAAEADQAJAP6LAHQAAAAAAQACAA0ACQCxhU16AAAAAAEAAgADABEAB6E1XcEBAAACAAEADQAJAP6LAHQAAAAAAgABAAkADQDQki5tAAAAAAEAAgADABEAB6E1XcEBAAABAAIAAwARAFmUiGscAAAAAgABAA0ACQB8iIJ3AAAAAAIAAQAJAA0AD6HvXgAAAAACAAEACQAAAEewt08AAAAAAgABAAkAEQD3uwdEAAAAAAIAAQANAAkAzdEwLgAAAAACAAEACQAAAKy1UkoAAAAAAgABABEACQAUiep2AAAAAAIAAQAJABEA97sHRAAAAAACAAEACQANAOquE1EAAAAAAgABAA0ACQCFtXlKAAAAAAIAAQAJAA0Aq7pTRQAAAAACAAEADQAJAF7CoD0AAAAAAgABAA0ACQBhp51YAAAAAAIAAQAJAAAArLVSSgAAAAACAAEACQANAA+h714AAAAAAgABAAkAAABHsLdPAAAAAAIAAQANAAkAhbV5SgAAAAACAAEACQANAHbEhTsBAAEA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/osAdAAAAAABAAIAFQANAASd+WIBAAAAAgABAAkADQCSh2x4AAAAAAEAAgAVAA0ABJ35YgEAAAABAAIAFQANAGmUlGsAAAAAAQACAA0AAADhox1cAAAAAAEAAgAJAA0AsYVNegAAAAACAAEACQANAB+Y32cAAAAAAQACABUADQBplJRrAAAAAAEAAgAVAA0AJKLaXQAAAAABAAIAFQANACSi2l0AAAAAAQACAAkADQCxhU16AAAAAAEAAgAVAA0AaZSUawAAAAABAAIAFQANAASd+WIBAAAAAgABAAkADQAfmN9nAAAAAAEAAgAJAA0AsYVNegAAAAABAAIAFQANAASd+WIBAAAAAgABAAkADQD+iwB0AAAAAAEAAgAJAA0AsYVNegAAAAABAAIAFQANACSi2l0AAAAAAgABAAkADQD+iwB0AAAAAAIAAQAJAA0A0JIubQAAAAABAAIAFQANACSi2l0AAAAAAQACABUADQBplJRrAAAAAAIAAQANAAkAD6HvXgAAAAACAAEACQANAHyIgncAAAAAAgABAA0ACQAPoe9eAAAAAAIAAQANAAAAR7C3TwAAAAACAAEADQAVAPe7B0QAAAAAAgABAA0AAACstVJKAAAAAAIAAQANABUA97sHRAAAAAACAAEACQANAIW1eUoAAAAAAgABAAkADQCrulNFAAAAAAIAAQANAAkAD6HvXgAAAAACAAEAFQANABSJ6nYAAAAAAgABAAkADQBewqA9AAAAAAIAAQANAAAArLVSSgAAAAACAAEADQAJAA+h714AAAAAAgABAA0AAABHsLdPAAAAAAIAAQAVAA0AFYTkewUAAAACAAEACQANAGGnnVgAAAAAAgABAAkADQB2xIU7AQABAAIAAQAJAA0AXsKgPQAAAAACAAEADQAVAPe7B0QAAAAAAgABAA0AAABHsLdPAAAAAAIAAQANABUA97sHRAAAAAACAAEADQAAAKy1UkoAAAAAAgABAAkADQCrulNFAAAAAAIAAQANAAkAj9FvLgAAAAACAAEACQANAIW1eUoAAAAAAgABAA0AAACstVJKAAAAAAIAAQANAAAAINbeKQAAAAACAAEACQANAF7CoD0AAAAAAgABAAkADQCFtXlKAAAAAAIAAQAJAA0AzdEwLgAAAAACAAEADQAAAEewt08AAAAAAgABAAkADQBewqA9AAAAAAIAAQAJAA0Ak9lrJgAAAAACAAEACQANAHbEhTsBAAEAAwANAAAAAAD//wAAAAAAAAMAAgABAAkADbCsMUQeAAADAAEADQAAAMb6OAUAAAAAAwABAAIADQAcixE+0TYAAAMAAQACAA0Ax6GlNZEoAAADAAAAAAAAAP//AAAAAAAAAwACAAEADQA5yQAexRgAAAMAAgABAA0AOckAHsUYAAADAA0AAAAAAP//AAAAAAAAAwANAAAAAAD//wAAAAAAAAMADQAAAAAA//8AAAAAAAADAA0AAAAAAP//AAAAAAAAAwANAAAAAAD//wAAAAAAAAMADQAAAAAA//8AAAAAAAADAA0AAAAAAP//AAAAAAAAAwANAAAAAAD//wAAAAAAAAMADQAAAAAA//8AAAAAAAADAAIADQAAAAPe+yEAAAAAAgAEAA0AAAD//wAAAAAAAAIAAwANAAkAHp/gYAAAAAACAAEADQAJANfGJzkAAAAAAgABAAkADQACyPs3AAAAAAIAAQAJAA0AD8bvOQAAAAACAAEACQANAAPI+zcAAAAAAgABAAkADQAPxu85AAAAAAIAAwABAAkAo7+rJK8bAAACAAMADQAJAB6f4GAAAAAAAgABAAQADQCfxhc5QQAGAAIABAAJAA0A4P8eAAAAAAACAAEADQAJANfGJzkAAAAAAgABAAkADQAPxu85AAAAAAIAAQAJAA0Art9QIAAAAAACAAMAAQAJAKO/qySvGwAA"),
-"uv_scale": Vector4(0, 0, 0, 0),
-"vertex_count": 654,
-"vertex_data": PackedByteArray("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////v5y/JfT///+/vs5P8PMA1Mwj+goZ////vyP6Chn///+/YkAl9P///79iQCX0////vzQ9OOH///+/5sMizv///78YPCLO////v7W8x9H///+/5sMizv///79JQ8fR////vxg8Is7///+/GDwizv///788yNOk6P+88jzI06Tp/0uNPMjTpMr/xfI8yNOkzv9OjdmLYsD///8/Nq5Jw////z/Zi2LA////P8WeM73///8/JXRiwP///z/csRe8////P9yxF7z///8/4XActEEADzPIUUnD////PzlhM73///8/Ik4XvP///z8iThe8////PyJOF7z///8/nWcNif///z/hcBy0+ADNTEJq7JokAMxNnWcNif///z/PlU2X////P2GYDYn///8/z5VNl////z9hmA2J////P0az95r///8/1bh4jf///z9Gs/ea////P9W4eI3///8/V8jqo////z9+1bGS////P1fI6qP///8/ftWxkv///z8KzDdh////PwrMN2H///8/Csw3Yf///z8KzDdh////Pz/fhhP///+/P9+GE////78/34YT////vz/fhhP///+/wOOBJ////7/A44En////v2CxsGT///8/YLGwZP///z9gsbBk////P2CxsGT///8/CJS6Yf///z8IlLph////PwiUumH///8/CJS6Yf///z824QpM////vzbhCkz///+/NuEKTP///7824QpM////vwaljRD///+/h5fLC////79rlrQR////v2yZBRT///+/cLU7C////7+NkyUE////v3FsJQT///+/d2jLC////7/4Wo0Q////v45KOwv///+/k2m0Ef///7+SZgUU////v5NptBH///+/kmYFFP///78FlGwZ////P8qLhxf///8/b4/7Hv///z+zkWwW////P2eGfR7///8/JYSNE////z/Ze40T////P5d5fR7///8/j3D7Hv///z80dIcX////P0tubBb///8/+WtsGf///z/5a2wZ////P0tubBb///8//3+a2v///z//f5ra////P/9/mtr///8//3+a2v///z//f2Ql////P/9/ZCX///8//39kJf///z//f2Ql////Pw5/t6X///8/Dn+3pf///z8Of7el////Pw5/t6X///8/ZKX//////79kpf//////v2Sl//////+/ZKX//////7////9/Gg8AgP///3+tDgCA////f6LxAID///9/ZCUAgP7//38y6/7/////f4zq//////9/PPgAgP///3+a2gCAuCUO/////7+4JQ7/////v7glDv////+/uCUO/////79kpQAA////v2SlAAD///+/ZKUAAP///79kpQAA////v+uAFVv///8/64AVW////z/rgBVb////P+uAFVv///8/FNvrAP///78U2+sA////vxTb6wD///+/FNvrAP///7///+viAACJzv//6+IAAInO///r4gAAic7//+viAACJzv///78AAP/f////vwAA/9////+/AAD/3////78AAP/f//8MmwAAePL//wybAAB48v//DJsAAHjy//8MmwAAePLy5P9///948vLk/3///3jy8uT/f///ePLy5P9///948v+//3/////f/7//f////9//v/9/////3/+//3/////fE53/f///ic4Tnf9///+JzhOd/3///4nOE53/f///ic7/f///////v/9//v////+//3///////7//f///////v/9///////+//3/+/////7//f/7/////v/9///////+//3/+/////7//f///////v/9//v////+//3///////7//f///////v/9//v////+//3/+/////7//f/7/////v9AZxc2C/6NfUSNg05v/+17KFtbKuf9uYFEjYNN0/hUh0BnFzfP+aSArBbmZ////vysFuZn///+/KwW5mf///78rBbmZ////v1EMp67///+/wBnywP///7/AGfLA////v/NAv5n///8/py5RjP///z/zQL+Z////P7oZK4X///8/uhkrhf///z+6GSuF////P7oZK4X///8/iUldlyMAVGFGU3CkogByIClHeI3///8/KUd4jf///z9WEPvn8//ITUAxT/DB/+tMVhD75+n/NTJAMU/wC/8qM6c36qP///8/wjfTpDQAOQ3CN9OkFQCzcoAqsZL///8/gCqxkv///z/UJMG36P+8ctQkwbfp/0sN1CTBt87/Tg3UJMG3yv/FcoMGr+b///+/2wUKGf///7/bBQoZ////v4MGr+b///+/6yOnt////7+yEn+q////v7ISf6r///+/6yOnt////79KFZXP////vxEI4M3///+/EQjgzf///79KFZXP////v/QzN2H///8/9DM3Yf///z/0Mzdh////P/QzN2H///8/vyCGE////78+HIEn////v78ghhP///+/PhyBJ////78+HIEn////v55OsGT///8/nk6wZP///z+eTrBk////P55OsGT///8/9mu6Yf///z/2a7ph////P/ZrumH///8/9mu6Yf///z/IHgpM////v8geCkz///+/yB4KTP///7/IHgpM////v/9/mtr///8//3+a2v///z//f5ra////P/9/mtr///8//39kJf///z//f2Ql////P/9/ZCX///8//39kJf///z/wgLel////P/CAt6X///8/8IC3pf///z/wgLel////P2Sl//////+/ZKX//////79kpf//////v2Sl//////+/AAD/f+Tw/n8AAP9/UfH+fwAA/3+a2v5/AAD/f8wUAAAAAP9/jOoAAAAA/38S8f5/AAD/f6Lx/n8AAP9/mtr+f0baDv////+/RtoO/////79G2g7/////v0baDv////+/ZKUAAP///79kpQAA////v2SlAAD///+/ZKUAAP///78TfxVb////PxN/FVv///8/E38VW////z8TfxVb////P+ok6wD///+/6iTrAP///7/qJOsA////v+ok6wD///+/AADr4v//dTEAAOvi//91MQAA6+L//3UxAADr4v//dTEAAP+/////HwAA/7////8fAAD/v////x8AAP+/////HwAADJv//4YNAAAMm///hg0AAAyb//+GDQAADJv//4YNDBv/fwAAhg0MG/9/AACGDQwb/38AAIYNDBv/fwAAhg3/P/9/AAD/H/8//38AAP8f/z//fwAA/x//P/9/AAD/H+ti/38AAHUx62L/fwAAdTHrYv9/AAB1Meti/38AAHUxsZStn/D/O7KxlK2f8f/HzbGUrZ/H/zKysZStn8v/3M006dbKFQBY4DTp1soTAKKfNOnWyk0AYOA06dbKRQCQn9P6uZn///+/C7+/mf///z91tl2X2/+qnnW2XZff/1rhdbZdl4z/oJ51tl2XmP9z4VLi3uoNAMLNUuLe6gwAOLJS4t7qNADMzVLi3uowACSyvs5P8EYAxcy+zk/wPQATs77OT/DfAKCyrdxg03oA196t3GDTYwADoa3cYNNWAZWg5sMizv///7/mwyLO////v0TmK4X///8/ROYrhf///z+4rHCkSf+yoLiscKQ1/hLgHY8ctLr/O7Mdjxy0G/9ozbyV7Jru/zeyvJXsmtX/2M1usyudyf+rnirbwbcWAEKNLubFzXwAW6Au5sXNFwEgoC7mxc0LAZXfqO/75wsANrKo7/vnCgDGzajv++cYADCyqO/75xUAyc3Kwjjh////v8rCOOH///+/ysI44f///7/Kwjjh////v9mLYsD///8/2YtiwP///z/FnjO9////P8WeM73///8/xZ4zvf///z82rknD////PzauScP///8/Nq5Jw////z+cvyX0////v5y/JfT///+/nL8l9P///7+1vMfR////v7W8x9H///+/tbzH0f///7/PlU2X////P8+VTZf///8/RrP3mv///z9Gs/ea////P1fI6qP///8/V8jqo////z97+a/m////v3v5r+b///+/e/mv5v///78T3Ke3////vxPcp7f///+/E9ynt////78T3Ke3////v7Tqlc////+/tOqVz////7+06pXP////v7Tqlc////+/ftWxkv///z9+1bGS////PyP6Chn///+/I/oKGf///7/VuHiN////P9W4eI3///8/7ffgzf///7/t9+DN////v2GYDYn///8/YZgNif///z9M7X+q////v0ztf6r///+/wOOBJ////7/A44En////v2uWtBH///+/a5a0Ef///79rlrQR////vwWUbBn///8/BZRsGf///z8FlGwZ////P3C1Owv///+/cLU7C////79wtTsL////v2+P+x7///8/b4/7Hv///z9vj/se////PwaljRD///+/BqWNEP///78GpY0Q////v2eGfR7///8/Z4Z9Hv///z9nhn0e////P2yZBRT///+/bJkFFP///79smQUU////v7ORbBb///8/s5FsFv///z+zkWwW////P42TJQT///+/jZMlBP///7+NkyUE////v8qLhxf///8/youHF////z/Ki4cX////P4eXywv///+/h5fLC////7+Hl8sL////vyWEjRP///8/JYSNE////z8lhI0T////P9yxF7z///8/3LEXvP///z9X0VGM////P1fRUYz///8/rfOnrv///7+t86eu////v63zp67///+/PubywP///78+5vLA////v////38AAP7/////f9X0//////9/mtr///7//39p6v7//v//f5ra/v/+//9////+//9///////+//3///////7//f/7/////v/9//v////+//3/+/////7//f/7/////v/9//v////+//3/+/////7//f/7/////v/9//v////+//3/+/////7//f/7/////v/9//v////+//3///////7//f///////v/9///////+/TWutnw4Aw01Na62fDQA3Mk1rrZ8zACIyTWutnzcAzE3KFtbK6f+mH8oW1srr/1xgyhbWyrH/nh/zQL+Z////P/NAv5n///8/iUldlx8ApB6JSV2XZgCLHolJXZdyAF5hrB3e6vH/PDKsHd7q8v/GTawd3urO/9pNrB3e6sr/MjJAMU/wuP85M0AxT/Af/15NUSNg04T/JyFRI2DTqP5pXxg8Is7///+/RlNwpLUATF9GU3CkyQHsH0ZTcKQbAmFf4XActEQAw0zhcBy04wCWMkJq7JoRADIyQmrsmhAAx01CauyaKQAmMpBMK504AJoekEwrnTUAU2GQTCuddQBZYZBMK51/AH8ewjfTpBYAQg3CN9OkMACwctAZxc2B/3sg0BnFzef+3l9WEPvn9P84MlYQ++fm/85NND044f///780PTjh////vzQ9OOH///+/JXRiwP///z8ldGLA////PyV0YsD///8/OWEzvf///z85YTO9////PzlhM73///8/yFFJw////z/IUUnD////P8hRScP///8/YkAl9P///79iQCX0////v0lDx9H///+/SUPH0f///79JQ8fR////vy9qTZf///8/L2pNl////z8vak2X////Py9qTZf///8/uEz3mv///z+4TPea////P7hM95r///8/uEz3mv///z+nN+qj////P6c36qP///8/pzfqo////z+DBq/m////v4MGr+b///+/6yOnt////7/rI6e3////v0oVlc////+/ShWVz////7+AKrGS////P4AqsZL///8/2wUKGf///7/bBQoZ////vylHeI3///8/KUd4jf///z8RCODN////vxEI4M3///+/nWcNif///z+dZw2J////P7ISf6r///+/shJ/qv///7+/IIYT////v78ghhP///+/PhyBJ////7+TabQR////v5NptBH///+/+WtsGf///z/5a2wZ////P45KOwv///+/jko7C////7+OSjsL////v49w+x7///8/j3D7Hv///z+PcPse////P/hajRD///+/+FqNEP///7/4Wo0Q////v5d5fR7///8/l3l9Hv///z+XeX0e////P5JmBRT///+/kmYFFP///79LbmwW////P0tubBb///8/cWwlBP///79xbCUE////v3FsJQT///+/NHSHF////z80dIcX////PzR0hxf///8/d2jLC////793aMsL////v3doywv///+/2XuNE////z/Ze40T////P9l7jRP///8/Ik4XvP///z+nLlGM////P6cuUYz///8/py5RjP///z9RDKeu////v1EMp67///+/UQynrv///7/AGfLA////v8AZ8sD///+/AAD/f///AAAAAP9/IesAAAAA/3+a2gAAAAD/f///AAAAAP9///8AAAAA/3+VFQAAAAD/fwAAAAAAAP9/ZCUAAP9///////+//3///////7//f///////v/9///////+//3/+/////7//f/7/////v/9//v////+//3/+/////7//f/7/////v/9//v////+//3/+/////7//f/7/////v/9//v////+//3///////7//f///////v/9///////+/")
-}]
-blend_shape_mode = 0
diff --git a/assets/characters/player/meshes/godot_robot_face.tres b/assets/characters/player/meshes/godot_robot_face.tres
deleted file mode 100644
index 1763e93..0000000
--- a/assets/characters/player/meshes/godot_robot_face.tres
+++ /dev/null
@@ -1,49 +0,0 @@
-[gd_resource type="ArrayMesh" format=4 uid="uid://c6oak2v5p8s1a"]
-
-[ext_resource type="Texture2D" uid="uid://bdg48m6l86q8i" path="res://assets/characters/player/3DGodotRobot_GodotPalette.png" id="1_w11jc"]
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_4omi2"]
-resource_name = "Material"
-cull_mode = 2
-albedo_texture = ExtResource("1_w11jc")
-roughness = 0.4
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_uisdr"]
-resource_name = "Material.002"
-cull_mode = 2
-albedo_color = Color(0, 0, 0, 1)
-roughness = 0.5
-
-[resource]
-resource_name = "3DGodotRobot_Cube_003"
-_surfaces = [{
-"aabb": AABB(-0.3767797, 0.97855395, 0.019602431, 0.759602, 0.47523648, 0.38369668),
-"attribute_data": PackedByteArray("TvPePiyP6D5O894+XMnbPnLIyj5QZNQ+csjKPiyP6D5O894+LI/oPk7z3j5cyds+TvPePiyP6D5O894+pFbVPk7z3j6kVtU+TvPePnQ5wD5O894+dDnAPk7z3j6kVtU+TvPePiyP6D5O894+LI/oPk7z3j4sj+g+TvPePqRW1T5O894+gF7VPlLz3j7gTso+TvPePqRW1T5O894+dDnAPk7z3j50OcA+TvPePiyP6D5O894+pFbVPk7z3j4sj+g+TvPePoBe1T5O894+LI/oPk7z3j7gTso+TvPePoCw1D5O894+LI/oPk7z3j7gTso+TvPePqRW1T5O894+gLDUPk7z3j50OcA+TvPePnQ5wD5O894+4E7KPk7z3j6kVtU+mvzcPvCX2D5O894+LI/oPk7z3j4sj+g+TvPePqRW1T5O894+LI/oPk7z3j6AXtU+TvPePiyP6D5O894+gLDUPk7z3j6kVtU+TvPePiyP6D5O894+gF7VPnLIyj4sj+g+4t3UPlBk1D5O894+LI/oPnLIyj5QZNQ+TvPePoBe1T5O894+pFbVPlLz3j7gTso+TvPePnQ5wD5O894+dDnAPrwrQT+c7n4+vCtBP5zufj68K0E/nO5+PrwrQT+c7n4+vCtBP5zufj5OFjc/nO5+PrwrQT+c7n4+vCtBP5zufj68K0E/nO5+Pk4WNz+c7n4+vCtBP5zufj68K0E/nO5+PrwrQT+c7n4+vCtBP5zufj68K0E/nO5+PrwrQT+c7n4+vCtBP5zufj68K0E/nO5+Pk7z3j4sj+g+TvPePoBe1T5O894+gF7VPk7z3j4sj+g+yOk+PySvfz7I6T4/5I9pPsjpPj/kj2k+yOk+PySvfz5O894+dDnAPlLz3j7gTso+UvPePuBOyj5O894+dDnAPmTsQT9g0oA+ZOxBP+R5bT5k7EE/5HltPmTsQT9g0oA+ZOxBP2DSgD5k7EE/YNKAPsjpPj/kj2k+yOk+P+SPaT7I6T4/5I9pPsjpPj/kj2k+UvPePuBOyj5O894+dDnAPk7z3j50OcA+UvPePuBOyj4TulE//Ot7PhO6UT+kilU+E7pRP6SKVT4TulE//Ot7PsjpPj8kr38+yOk+PySvfz7I6T4/JK9/PsjpPj8kr38+TvPePoBe1T5O894+LI/oPk7z3j4sj+g+TvPePoBe1T5yyMo+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hLtePpnHMD+wT2w+DIszP7BPbD5TKTI/hLtePgyLMz+Eu14+UykyP7BPbD7H7DQ/sE9sPgyLMz+Eu14+x+w0P4S7Xj4MizM/sE9sPoFONj+wT2w+x+w0P4S7Xj6BTjY/hLtePsfsND+wT2w+O7A3P7BPbD6BTjY/hLtePjuwNz+Eu14+gU42P7BPbD70ETk/sE9sPjuwNz+Eu14+9BE5P4S7Xj47sDc/sE9sPq5zOj+0T2w+9BE5P4S7Xj6uczo/iLtePvQROT+wT2w+aNU7P7BPbD6uczo/iLtePmjVOz+Eu14+rnM6P7RPbD4iNz0/sE9sPmjVOz+Eu14+Ijc9P4S7Xj5o1Ts/sE9sPtyYPj+wT2w+Ijc9P4S7Xj7cmD4/hLtePiI3PT+wT2w+lvo/P7RPbD7cmD4/hLtePpb6Pz+Iu14+3Jg+P7BPbD7i3dQ+LI/oPk7z3j5cyds+om3XPlBk1D5O894+XMnbPk7z3j5kTtY+TvPePvBuyz5O894+dDnAPk7z3j50OcA+TvPePqDezD5O894+4E7KPk7z3j50OcA+TvPePnQ5wD5O894+8G7LPk7z3j5kTtY+TvPePiyP6D5O894+8G7LPk7z3j7gTso+TvPePmRO1j5O894+ZE7WPk7z3j6g3sw+TvPePvBuyz5O894+XMnbPqJt1z5QZNQ+4t3UPiyP6D5O894+4E7KPlLz3j7gTso+TvPePiyP6D5O894+4E7KPuLd1D4sj+g+4t3UPlBk1D5O894+LI/oPk7z3j6AsNQ+TvPePuBOyj5O894+4E7KPk7z3j50OcA+TvPePuBOyj5O894+dDnAPk7z3j7gTso+TvPePiyP6D5O894+gLDUPk7z3j4sj+g+TvPePqRW1T5O894+4E7KPk7z3j6AsNQ+TvPePuBOyj5O894+gLDUPk7z3j7gTso+mvzcPvCX2D5O894+4E7KPuLd1D5QZNQ+4t3UPiyP6D5O894+4E7KPlLz3j7gTso+TvPePuBOyj68K0E/nO5+PrwrQT+c7n4+vCtBP5zufj68K0E/nO5+Pk7z3j4sj+g+TvPePoBe1T68K0E/nO5+PrwrQT+c7n4+vCtBP5zufj68K0E/nO5+PrwrQT+c7n4+vCtBP5zufj7H7DQ/yFByPg2LMz/IUHI+x+w0P8hQcj4NizM/yFByPmnVOz/IUHI+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"),
-"bone_aabbs": [AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(-0.20278871, 0.9988315, 0.15357457, 0.41162002, 0.2169069, 0.21300079), AABB(0, 0, 0, -1, -1, -1), AABB(-0.3767797, 0.97855395, 0.019602431, 0.759602, 0.23718446, 0.36082765), AABB(-0.34772626, 0.97855395, 0.019602433, 0.7014951, 0.47523648, 0.35301), AABB(0, 0, 0, -1, -1, -1), AABB(-0.3767797, 1.0834067, 0.019602431, 0.759602, 0.37038374, 0.35301), AABB(-0.3767797, 0.97855395, 0.019602431, 0.759602, 0.47523648, 0.38369668), AABB(0.0030212952, 0.978554, 0.019602431, 0.34897542, 0.2371844, 0.38369668), AABB(0.0030212952, 0.97855395, 0.1272176, 0.1396981, 0.22913712, 0.2760815), AABB(0.0030212952, 0.9988315, 0.019602431, 0.37980103, 0.2169069, 0.28572243), AABB(0, 0, 0, -1, -1, -1), AABB(-0.34595412, 0.978554, 0.019602431, 0.3489854, 0.2371844, 0.3823875), AABB(-0.1366768, 0.97855395, 0.1272176, 0.1396981, 0.22913712, 0.2760815), AABB(-0.3767797, 0.9988315, 0.019602431, 0.379801, 0.2169069, 0.28572243)],
-"format": 34359745559,
-"index_count": 1596,
-"index_data": PackedByteArray("ogABAKMAogAAAAEApAAFAAYApAClAAUApgAJAKcApgCoAAkAqQCqAKsAqQAKAKoABACsAK0ABAAHAKwACACuAK8ACACwAK4AsQCyALMAsQC0ALIAAwC1AAIAAwC2ALUAEAC3ABIAEAARALcAFQAWABcAFQAYABYAuAATALkAuAAUABMAugC7ADEAugAwALsAGQC8AL0AGQAcALwAHQC+AL8AHQAiAL4AwADBAMIAwADDAMEAxAArACwAxAAtACsAHgDFAMYAHgAfAMUAGwAaAMcAGwDIABoALwDJAMoALwAyAMkAMwDLAMwAMwA0AMsAKgDNAC4AKgDOAM0ANQA2ADcANQDPADYA0AA7ADgA0ADRADsAPQDRANAAPQBBANEAPQDSAEEAPQDTANIA0wBCANIA0wBDAEIA1ADVANYA1ADXANUASgBLAEwASgBNAEsA2ADZADoA2AA5ANkA2gDbANwA2gDdANsATgBPAFAATgBRAE8AUgBTAFQAUgBVAFMAVgDeAN8AVgBZAN4AXABdAF4AXABfAF0AYABhAGIAYABjAGEAZABlAGYAZABnAGUAaABpAGoAaABrAGkAbABtAG4AbABvAG0APAA/AD4APABAAD8A4ADhAOIA4ADjAOEA5ADlAOYA5gDlAOcA5gDnAOgA5ADpAOUA6gDpAOQA6gDrAOkA7ADrAOoA7ADtAOsA7gDtAOwA7gDvAO0A8ADxAPIA8ADzAPEA9AD1APYA9AD3APUA+AD5APoA+AD7APkA/AD9AP4A/AD/AP0AAAEBAQIBAAEDAQEBBAEFAQYBBAEHAQUBCAEJAQoBCAELAQkBDAENAQ4BDAEPAQ0BEAEPAQwBEAERAQ8BEgETARQBEgEVARMBFgEXARgBFgEZARcBGgEbARwBGgEdARsBHgEfASABHgEhAR8BIgEjASQBIgElASMBJgEnASgBJgEpAScBKgErASwBKgEtASsBLgEvATABLgExAS8BMgEzATQBMgE1ATMBNgE3ATgBNgE5ATcBOgE5ATYBOgE7ATkBPAE9AT4BPAE/AT0BQAFBAUIBQAFDAUEBRAFFAUYBRAFHAUUBSAFJAUoBSAFLAUkBSQFLAUwBTQFIAU4BTwFIAU0BSwFIAU8BTwFNAVABSwFPAVEBUQFPAVIBSwFRAVMBVAFVAVYBVAFXAVUBWAFZAVoBWAFbAVkBXAFdAV4BXAFfAV0BXQFfAWABYQFcAWIBYwFcAWEBXwFcAWMBYwFhAWQBXwFjAWUBZQFjAWYBXwFlAWcBaAFpAWoBaAFrAWkBbAFtAW4BbAFvAW0BcAFxAXIBcAFzAXEBdAF1AXYBdAF3AXUBeAF5AXoBeAF7AXkBfAF9AX4BfAF/AX0BgAF+AX0BgAF9AYEBggGDAYQBggGFAYMBhgGHAYgBhgGJAYcBhgGIAYoBhgGKAYsBjAGGAYsBjAGLAY0BjgGMAY0BjgGPAYwBjgGNAZABjgGRAY8BkgGOAZABkgGRAY4BkgGTAZEBkgGQAZQBlQGSAZQBlQGUAZYBlwGYAZkBlwGaAZgBmwGYAZoBmwGcAZgBnQGeAZ8BnQGgAZ4BnQGhAaABnQGiAaEBowGkAaUBowGmAaQBpwGkAaYBpwGoAaQBpQGkAakBqAGpAaQBpQGpAaoBqAGrAakBrAGtAa4BrAGvAa0BrgGtAbABrgGwAbEBsgGzAbQBsgG1AbMBtgG3AbgBtgG5AbcBugG7AbwBugG9AbsBvgG7Ab0BvgG/AbsBwAHBAcIBwAHDAcEBxAHBAcMBxAHFAcEBxgHHAcgBxgHJAccBygHLAcwBygHNAcsBzgHPAdABzgHRAc8B0gHPAdEB0gHTAc8B1AHVAdYB1AHXAdUB2AHZAdoB2AHbAdkB3AHdAd4B3AHfAd0B3AHgAd8B3AHhAeAB4gHjAeQB4gHlAeMB4gHmAeUB4gHnAeYB6AHpAeoB6AHrAekB7AHtAe4B7AHvAe0B8AHuAe0B8AHtAfEB8gHzAfQB8gH1AfMB9gH3AfgB9gH5AfcB+gH4AfcB+gH3AfsB/AH9Af4B/AH/Af0BAAIBAgICAAIDAgECBAIFAgYCBAIHAgUCCAIJAgoCCAILAgkCDAINAg4CDAIPAg0CEAIRAhICEAITAhECFAIVAhYCFAIXAhUCGAIZAhoCGAIbAhkCHAIdAh4CHAIfAh0CIAIhAiICIAIjAiECJAIlAiYCJAInAiUCKAIpAioCKAIrAikCLAItAi4CLAIvAi0CMAIxAjICMAIzAjECNAI1AjYCNAI3AjUCOAI5AjoCOAI7AjkCPAI9Aj4CPAI/Aj0CQAJBAkICQAJDAkECRAJFAkYCRAJHAkUCSAJJAkoCSAJLAkkCTAJNAk4CTAJPAk0CUAJRAlICUAJTAlECVAJVAlYCVAJXAlUCWAJZAloCWAJbAlkCXAJdAnIAXAJeAl0CDABfAmACDAANAF8CYQJiAmMCYQJkAmICZQJmAmcCZQJoAmYCDgBpAg8ADgBqAmkCCwBrAmwCCwBtAmsCbgJvAnACbgJxAm8CcAByAnMCcABxAHICcwB0AnUCcwB3AHQCdgJ5AHoAdgJ4AHkAdQB0AHYAdQB3AnQAeAJ+AHkCeAKAAH4AegIkACYAegJ7AiQAfAIhAH0CfAJ+AiEAfwKAAoECfwIgAIACggKDAoQCggKFAoMCIwCGAocCIwCIAoYCiQKKAosCiQKMAooCfQCNAn8AfQCOAo0CgQCPAoMAgQCCAI8CKAAnACUAKAApACcAkAKEAJECkAKFAIQAkgKTApQCkgKVApMClgJ7AHwAlgKXAnsARQBEAJgCRQBGAEQAmQJHAJoCmQKbAkcAhgCHAIgAhgCJAIcAigCLAIwAigCNAIsAWgBXAFsAWgBYAFcAjgCPAJAAjgCRAI8AkgCTAJQAkgCVAJMAlgCXAJgAlgCZAJcAmgCbAJwAmgCdAJsAngCfAKAAngChAJ8AnAJIAEkAnAKdAkgAngKfAqACngKhAp8CogKjAqQCogKlAqMCpQKmAqMCpQKnAqYCqAKiAqQCqAKkAqkCqgKoAqkCqgKpAqsCrAKqAqsCrAKrAq0CrgKvArACrgKxAq8CsgKzArQCsgK1ArMCtgK3ArgCtgK5ArcCugK7ArwCugK9ArsCvgK/AsACvgLBAr8CwgLDAsQCwgLFAsMCxgLHAsgCxgLJAscCygLLAswCygLNAssCzgLPAtACzgLRAs8C0gLTAtQC0gLVAtMC1gLXAtgC1gLZAtcC2gLbAtwC2gLdAtsC3gLfAuAC3gLhAt8C4gLjAuQC4gLlAuMC5gLnAugC5gLpAucC6gLrAuwC6gLtAusC7gLvAvAC7gLxAu8C8gLzAvQC8gL1AvMC9gL3AvgC9gL5AvcC+gL7AvwC+gL9AvsC/gL/AgAD/gIBA/8CAgMDAwQDAgMFAwMDBgMHAwgDBgMJAwcDBwMJAwoDCwMGAwwDDQMGAwsDCQMGAw0DDQMLAw4DCQMNAw8DDwMNAxADCQMPAxEDEgMTAxQDEgMVAxMDFgMXAxgDFgMZAxcDGgMbAxwDGgMdAxsDGwMdAx4DHwMaAyADIQMaAx8DHQMaAyEDIQMfAyIDHQMhAyMDIwMhAyQDHQMjAyUDJgMnAygDJgMpAycDKgMrAywDKgMtAysDLgMvAzADLgMxAy8DMgMzAzQDMgM1AzMDNgM3AzgDNgM5AzcDOgM7AzwDOgM9AzsDPgM/A0ADPgNBAz8DQgNDA0QDQgNFA0MDRQNGA0MDRQNHA0YDSANJA0oDSANLA0kDTANNA04DTANPA00DUANRA1IDUANTA1EDVANVA1YDVANXA1UDWANZA1oDWANbA1kDXANdA14DXANfA10DYANhA2IDYANjA2EDZANlA2YDZANnA2UDaANpA2oDaANrA2kDbANtA24DbANvA20DcANxA3IDcANzA3EDdAN1A3YDdAN3A3UDeAN5A3oDeAN7A3kDfAN9A34DfAN/A30DgAOBA4IDgAODA4EDhAOFA4YDhAOHA4UDiAOJA4oDiAOLA4kDjAONA44DjAOPA40DkAORA5IDkAOTA5EDlAOVA5YDlAOXA5UDmAOZA5oDmAObA5kDnAOdA54DnAOfA50DoAOhA6IDoAOjA6EDpAOlA6YDpAOnA6UDqAOpA6oDqAOrA6kDrAOtA64DrAOvA60DsAOxA7IDsAOzA7EDtAO1A7YDtAO3A7UDuAO5A7oDuAO7A7kDvAO9A74DvAO/A70D"),
-"lods": [0.2997606, PackedByteArray("AAABAAIAAwAAAAIABAAFAAYABAAHAAUACAAJAAUACAAKAAkACwAJAAoADAAJAAsADAANAAkADgAMAA8AEAARABIAEQATABIAEQAUABMAFQAWABcAFQAYABYAGQAaABsAGQAcABoAGwAaAB0AHgAfAB0AHgAdACAAHQAhACAAHQAiACEAIwAgACEAIwAhACQAJQAkACYAJQAnACQAKAAnACUAKAApACcAKgArACwAKgAtACsAKgAsAC4ALwAwADEALwAyADAAMwA0ADUANQA0ADYANQA2ADcAOAA5ADoAOAA7ADkAOgA5ADwAPQA7ADgAOgA8AD4APAA/AD4APABAAD8APQBBADsAPQBCAEEAPQBDAEIAQwBEAEIAQwBFAEQARQBGAEQARgBHAEQARwBGAEgARwBIAEkASgBLAEwASgBNAEsATgBPAFAATgBRAE8AUgBTAFQAUgBVAFMAVgBXAFgAVgBZAFcAWgBYAFcAWgBXAFsAXABdAF4AXABfAF0AYABhAGIAYABjAGEAZABlAGYAZABnAGUAaABpAGoAaABrAGkAbABtAG4AbABvAG0AcABxAHIAcwB0AHUAdQB0AHYAcwB3AHQAeAB5AHoAeAB6AHsAeAB7AHwAfQB+AH8AfQCAAH4AgQCCAIMAggCEAIMAggCFAIQAhgCHAIgAhgCJAIcAigCLAIwAigCNAIsAjgCPAJAAjgCRAI8AkgCTAJQAkgCVAJMAlgCXAJgAlgCZAJcAmgCbAJwAmgCdAJsAngCfAKAAngChAJ8A")],
-"material": SubResource("StandardMaterial3D_4omi2"),
-"name": "Material",
-"primitive": 3,
-"skin_data": PackedByteArray("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BQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAAAAAD//wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAAAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAAAAAD//wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcAAAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAAAAAD9/wEAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAPz/AgAAAAAABQAIAAcAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAAAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAPz/AgAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAAAAAA/f8BAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAAAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAgAAAAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAADo/xYAAAAAAAUACAAAAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcAAAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAAAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcAAAAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcAAAAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAAAAAD//wAAAAAAAAUABwAAAAAA/v8AAAAAAAAFAAgABwAAAPz/AQAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABwAAAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA8v8MAAAAAAAFAAgABwAAAPz/AQAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAAAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAADo/xYAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAAAAAD+/wAAAAAAAAUABwAIAAAA8v8MAAAAAAAFAAcACAAAALn/RQAAAAAABQAHAAAAAAD9/wEAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAAAAAD+/wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD8/wEAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAPL/DAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABwAIAAAAuf9FAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAAAAAD+/wAAAAAAAAUABwAIAAAA9/8HAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAgAAAD3/wcAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAcACAAAAPr/BAAAAAAABQAHAAgAAAD0/woAAAAAAAUACAAHAAAA9v8GAAIAAAAFAAcACAAAAP3/AQAAAAAABQAHAAgAAAD6/wQAAAAAAAUABwAIAAAA/f8BAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAAAAAD//wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAgABwAAAMv/MwAAAAAABQAHAAgAAAD//wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAAAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAAAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUABwAAAAAA//8AAAAAAAAFAAcAAAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAAAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAAAAAD//wAAAAAAAAUABwAAAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAAAAAD//wAAAAAAAAUABwAAAAAA//8AAAAAAAAFAAcAAAAAAP//AAAAAAAABQAHAAgAAAD8/wIAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAAAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD7/wMAAAAAAAUABwAIAAAA+/8DAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcAAAAAAP//AAAAAAAABQAHAAgAAAD7/wMAAAAAAAUABwAAAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAAAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAAAAAD//wAAAAAAAAUABwAIAAAA+v8EAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAAAAAD//wAAAAAAAAUABwAIAAAA+/8DAAAAAAAFAAcAAAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAPv/AwAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUABwAAAAAA//8AAAAAAAAFAAcAAAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAPv/AwAAAAAABQAHAAgAAAD6/wQAAAAAAAUABwAIAAAA+v8EAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAcAAAD2/wYAAgAAAAUABwAIAAAA+v8EAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAAAAAD+/wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA+v8EAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAgAAAD9/wEAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAPT/CgAAAAAABQAHAAgAAAD9/wEAAAAAAAUABwAIAAAA//8AAAAAAAAFAAgAAAAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAAD9/wEAAAAAAAUABwAIAAAA+v8EAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAcAAADa/yQAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAgABwAAAEL/vAAAAAAABQAIAAAAAAD//wAAAAAAAAUACAAHAAAA2v8kAAAAAAAFAAgAAAAAAP//AAAAAAAABQAHAAAAAAD//wAAAAAAAAUACAAHAAAA2v8kAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAAAAAD//wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAgABwAAAEL/vAAAAAAABQAIAAcAAADL/zMAAAAAAAUACAAHAAAA2v8kAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAcAAADa/yQAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAALf/RwAAAAAABQAHAAgAAAD2/wgAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAAD2/wgAAAAAAAUACAAAAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAADo/xYAAAAAAAUACAAAAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUACAAAAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA6P8WAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAHAAAA/f8BAAAAAAAFAAgAAAAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA/f8BAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAAAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgAAAAAAP//AAAAAAAABQAIAAAAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUACAAAAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAAAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA/f8BAAAAAAAFAAgAAAAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABwAIAAAA/f8BAAAAAAAFAAcACAAAALf/RwAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA9v8IAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD3/wcAAAAAAAUABwAIAAAAt/9HAAAAAAAFAAgAAAAAAP//AAAAAAAABQAHAAgAAAD3/wcAAAAAAAUABwAIAAAAuf9FAAAAAAAFAAcACAAAAPb/CAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAAuf9FAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAAAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgABwAAAP3/AQAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAAAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUABwAAAAAA/v8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAcAAAD9/wEAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD8/wIAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP3/AQAAAAAABQAHAAgAAAD8/wIAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAC3/0cAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD9/wEAAAAAAAgABQANAAQA/v8AAAAAAAAIAAUADQAEAP7/AAAAAAAACAACAAQADwDI/zYAAAAAAAgABQANAAQA/v8AAAAAAAAIAAQAAgAFAP7/AAAAAAAACAACAAUADwDt/xEAAAAAAAgABAAFAAIA8v8MAAAAAAAIAAsADwAEAP7/AAAAAAAACAACAAQADwDI/zYAAAAAAAgAAgANAAQA9v8IAAAAAAAIAAsADwAEAP7/AAAAAAAACAAJAA0AAgDn/wkACQADAAgAAgAFAA8A7f8RAAAAAAAIAAQAAgAFAP7/AAAAAAAACAAFAAQADQD+/wAAAAAAAAgAAgAFAA8A7f8RAAAAAAAIAAIADQAEAPb/CAAAAAAACAAEAAIABQD+/wAAAAAAAAgABAACAAUA/v8AAAAAAAAIAAIABAAPAMj/NgAAAAAACAACAAUADwDt/xEAAAAAAAgABQANAAQA/v8AAAAAAAAIAAIABAAPAMj/NgAAAAAACAAFAA0ABAD+/wAAAAAAAAgAAgANAAQA9v8IAAAAAAAIAA0ABQAEAP7/AAAAAAAACAAFAA8ABADa/yMAAAAAAAgAAgANAAQA9v8IAAAAAAAIAAUADQAEALX/RgACAAAACAANAA4ABAD1/wgAAAAAAAgABAAFAA0A/v8AAAAAAAAIAA0ABQAPAPX/CQAAAAAACAAEAAUADQDi/xkAAwAAAAgADQAOAAQA9f8IAAAAAAAIAAQAAAAAAP//AAAAAAAACAANAA4AAAD9/wEAAAAAAAgABAAAAAAA//8AAAAAAAAIAAQABQANAOL/GQADAAAACAAEAA8ABwD+/wAAAAAAAAgADQAFAA8A9f8JAAAAAAAIAAQABQANAP7/AAAAAAAACAAFAAQADwC5/zIADgAEAAgABAAFAA0A4v8ZAAMAAAAIAA0ABQAPAPX/CQAAAAAACAANAA4AAAD9/wEAAAAAAAgADQAFAA8A9f8JAAAAAAAIAA0ADgAEAPX/CAAAAAAACAAFAA4ABADq/xQAAAAAAAgABAAFAA0A4v8ZAAMAAAAIAA0ADgAEAPX/CAAAAAAACAAFAA0ABAC1/0YAAgAAAAgADQAOAAAA/f8BAAAAAAAIAAQADQAOAOr/FAAAAAAACAANAA4AAAD9/wEAAAAAAAgABQANAAQA/v8AAAAAAAAIAAQABQANAP7/AAAAAAAACAAFAA0ABAD+/wAAAAAAAAgABQAEAA0A/v8AAAAAAAAIAAUADwAEANr/IwAAAAAACAAEAAUADQD8/wEAAAAAAAgABAAFAA0A/v8AAAAAAAAIAAcABQAEAP7/AAAAAAAACAAEAA8ABwD+/wAAAAAAAAgABQAPAAQA2v8jAAAAAAAIAAcABAAPAPn/BQAAAAAACAAFAA8ABADa/yMAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgAAAAAAP//AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAAAAAA//8AAAAAAAAFAAgAAAAAAP//AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABwAAAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcAAAAAAP3/AQAAAAAABQAHAAgAAAD8/wIAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAAAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABwAIAAAA/P8CAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAAAAAA/f8BAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAAAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAgAAAAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAADo/xYAAAAAAAUACAAAAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcAAAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAAAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAAAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcAAAAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcAAAAAAP7/AAAAAAAABQAIAAcAAAD8/wEAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAAAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAADy/wwAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgABwAAAPz/AQAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAAD//wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAAAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAADo/xYAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgAAAAAAP7/AAAAAAAABQAHAAgAAAC5/0UAAAAAAAUABwAIAAAA8v8MAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAAAAAD9/wEAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUACAAAAAAA/v8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAcAAAD8/wEAAAAAAAUABwAIAAAA8v8MAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAAC5/0UAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAPf/BwAAAAAABQAIAAAAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAPf/BwAAAAAABQAIAAcAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgAAAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAcAAAD+/wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAAAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcAAAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA+/8DAAAAAAAFAAcAAAAAAP//AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAAAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAAAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD7/wMAAAAAAAUABwAIAAAA//8AAAAAAAAFAAgAAAAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAPr/BAAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP3/AQAAAAAABQAHAAgAAAD//wAAAAAAAAUABwAIAAAA+v8EAAAAAAAFAAcAAAAAAP//AAAAAAAABQAIAAcAAADa/yQAAAAAAAUACAAAAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAAt/9HAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAAD2/wgAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAPb/CAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAAAAAA//8AAAAAAAAFAAcACAAAAOj/FgAAAAAABQAIAAAAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAAAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAOj/FgAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAAAAAD//wAAAAAAAAUACAAHAAAA/f8BAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAcAAAD9/wEAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAAAAAD//wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAAAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAgAAAAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAAAAAD//wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAgAAAAAAP//AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAIAAAAAAD//wAAAAAAAAUABwAIAAAA/f8BAAAAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAAD9/wEAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAALf/RwAAAAAABQAHAAgAAAD2/wgAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAALf/RwAAAAAABQAHAAgAAAD3/wcAAAAAAAUACAAAAAAA//8AAAAAAAAFAAcACAAAAPf/BwAAAAAABQAHAAgAAAD2/wgAAAAAAAUABwAIAAAAuf9FAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAC5/0UAAAAAAAUACAAAAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAHAAAA/f8BAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAAAAAA/v8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAAAAAD+/wAAAAAAAAUACAAHAAAA/f8BAAAAAAAFAAgABwAAAP7/AAAAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA/v8AAAAAAAAFAAgABwAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABwAIAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD+/wAAAAAAAAUABwAIAAAA/v8AAAAAAAAFAAcACAAAAP7/AAAAAAAABQAHAAgAAAD8/wIAAAAAAAUABwAIAAAA/f8BAAAAAAAFAAcACAAAAPz/AgAAAAAABQAHAAgAAAD+/wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAALf/RwAAAAAABQAIAAcAAAD//wAAAAAAAAUABwAIAAAA/f8BAAAAAAAFAAcACAAAAP7/AAAAAAAA"),
-"uv_scale": Vector4(0, 0, 0, 0),
-"vertex_count": 960,
-"vertex_data": PackedByteArray("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////v38jCr7///+/fyMKvv///7/RoqEu97cFzwCU3i4/r2rOAJTeLj+vas7RoqEu////PzmGmyn///8/tpdMRP///z+2l0xE////P6d0cT3///8/xXmbKf///z85hpsp////P0hoTET///8/LV2hLgdI+TCUP8FX////Py1doS7///8/lD/BV////z+UP8FX////P5Q/wVf///8/asDBV////z8rtPlF////Pyu0+UX///8/K7T5Rf///z9qwMFX////P/9/AAD+//fC/38AAP7/YK3/fwAA/v//v/9/AAD+/8fHMZ0GXPFuwOAxnQZc////P0GR8FpQbH/iQZHwWlBsf+JBkfBa////P/////////+///8AAP///7///wAA////v/////////+/AAAAAP///7//////////vwAAAAD///+/AAAAAP///78AAAAA////v/////////+//////////7//////////v///AAD///+///8AAP///7///wAA////v/////////+///8AAP///7///wAA////v1UIa6n///+/VQhrqf///79VCGup////v1UIa6n///+/7Yvj3P///z/ti+Pc////P+2L49z///8/7Yvj3P///z8hmBeA////PyGYF4D///8/IZgXgP///z8hmBeA////P7+EBOX///8/P3sE5f///z8/ewTl////P7+EBOX///8/P3sE5f///z8/ewTl////PwjaBI////8/CNoEj////z8I2gSP////PwjaBI////8/1wI6FP///7/XAjoU////v9cCOhT///+/1wI6FP///7+RlH7k////P5GUfuT///8/kZR+5P///z+RlH7k////P6BGXIv///8/oEZci////z+gRlyL////P6BGXIv///8/49plev///z/j2mV6////P+PaZXr///8/49plev///z//f/7/AACaxP9//v8AAITKtoKK+8ACZcGr4FXe0Ioz5ZjdlO4DjZTmmN2U7gONlOaY3ZTu////v6vgVd7///+/ft2Ovf///79+3Y69////v37djr3///+/XPamqv///79c9qaq////v/9/AAAAAAc9/38AAAAA+07/fwAAAAA3OP9/AAAAAP8/zWIGXA2RPh/NYgZcDZE+H81iBlz///8/vW7wWq6Tfx29bvBa////Px50Qd3///8/HnRB3f///z8edEHd////Px50Qd3///8/3WcXgP///z/dZxeA////P91nF4D///8/3WcXgP///z/2JQSP////P/YlBI////8/9iUEj////z/2JQSP////Pyf9OhT///+/J/06FP///78n/ToU////vyf9OhT///+/bWt+5P///z9ta37k////P21rfuT///8/bWt+5P///z/It/+L////P8i3/4v///8/yLf/i////z/It/+L////PxslZXr///8/GyVlev///z8bJWV6////PxslZXr///8/SH2K+0f9OjxIfYr7Vv2jOAxOKtj///+/DE4q2P///7/DVSL4////v8NVIvj///+/w1Ui+P///7/fQsfz////v99Cx/P///+/30LH8////7+MO5LM////v4w7ksz///+/8jyt5////7/yPK3n////v/I8ref///+/AVEy7P///78BUTLs////vwFRMuz///+/AVEy7P///7//f//////yOP9///////8/5R/C3mZ16hpmIpTu+3JqGWYilO77cmoZ/38AAP7/yLr/fwAA/v8DsdGioS73twXP0aKhLve3Bc85hpsp////PzmGmyn///8/V4txPf///z9Xi3E9////P1eLcT3///8/V4txPf///z8rtPlF////P7aXTET///8/tpdMRP///z8AlN4u////PwCU3i4/r2rO/38AAP7/YsX/fwAA/v//vzGdBlzxbsDgMZ0GXPFuwOBqwMFX////P2rAwVf///8/QZHwWlBsf+L/////////v/////////+//////////7//////////v/////////+//////////7//////////v/////////+///8AAP///7///wAA////v/////////+//////////7//////////v/////////+/v4QE5f///z+/hATl////P/Lk8mT///+/8uTyZP///7/y5PJk////v/Lk8mT///+//3//f////z//f/9/////P/9//3////8//3//f////z//f/9/////P/9//3////8//3//f////z//f/9/////P/9//3////8//3//f////z//f/9/////P/9//3////8/3Jq+GwwbeLLcmr4bDBt4styavhsMG3iy3Jq+GwwbeLL/P/+/////v/8//7////+//z//v////7//P/+/////v52/YUD/P/+fnb9hQP8//5+dv2FA/z//n52/YUD/P/+f/z//P////7//P/8/////v/8//z////+//z//P////7//f/9/////P/9//3////8//3//f////z//f/9/////P0DkImXyZIWNQOQiZfJkhY1A5CJl8mSFjUDkImXyZIWNDBsMm////78MGwyb////vwwbDJv///+/DBsMm////7//f/9/////P/9//3////8//3//f////z//f/9/////P/9//3////8//3//f////z9A5NyaDJuFjUDk3JoMm4WNQOTcmgybhY1A5NyaDJuFjf+//7////+//7//v////7//v/+/////v/+//7////+//3//f////z//f/9/////P/9//3////8//3//f////z+dv52//7//n52/nb//v/+fnb+dv/+//5+dv52//7//n/Nk8uT///+/82Ty5P///7/zZPLk////v/Nk8uT///+/3JpA5PLkeLLcmkDk8uR4styaQOTy5Hiy3JpA5PLkeLLy5Ayb////v/LkDJv///+/8uQMm////7/y5Ayb////vyJlQOTy5IXNImVA5PLkhc0iZUDk8uSFzSJlQOTy5IXN/7//P////7//v/8/////v/+//z////+//7//P////7//f/9/////P/9//3////8//3//f////z//f/9/////P/9//3////8//3//f////z9hQJ2//7//32FAnb//v//fYUCdv/+//99hQJ2//7//3wub8uT///+/C5vy5P///78Lm/Lk////vwub8uT///+/vhvcmgybePK+G9yaDJt48r4b3JoMm3jyvhvcmgybePL/f/9//v//P/9//3////8//3//f////z//f/9//v//P/9//3/+//8//3//f/7//z//f/9//v//P/9//3/+//8//3//f/7/AED/f/9//v//P/9//3////8//3//f/7//z//f/9/////P/9//3////8//3//f////z//f/9/////P74bImXyZHjyvhsiZfJkePK+GyJl8mR48r4bImXyZHjy//////7//7//////////v//////+//+///////7//7///////v//v//////+//+///////7//7//////////v//////+//+///////7//7///////v//v//////+//+/8mQMG////7/yZAwb////v/JkDBv///+/8mQMG////79hQGFA/z//32FAYUD/P//fYUBhQP8//99hQGFA/z//3wybDBv///+/DJsMG////78Mmwwb////vwybDBv///+/ImW+Gwwbhc0iZb4bDBuFzSJlvhsMG4XNImW+Gwwbhc0MG/Jk////vwwb8mT///+/DBvyZP///78MG/Jk////v/9//3////8//3//f////z//f/9/////P/9//3////8//3//f////z//f/9/////Pzy9wsI8PZ3ePL3Cwjw9nd48vcLCPD2d3jy9wsI8PZ3e/////zaOGwf/////1Efp4///////fwAA/////zw9nd7/////KrgVHP////8gq5AV/////yOFkQL/////5ZJzCf//////fwAA//////9//////////38AAP//////f/////////9/AAD//////3////////8ydOYF/////yV47QP/////8GCHD/9//3+OVcbq/3//f1aSKwn/f/9/PD2d3v9//3//fwAA/3//f3CqOBX/f/9/wsJhIf9//3////8//3//f////z//f/9/////P/9//3////8//3//f////z//f/9/////P/9//3//f////3//f/9/AAD/f/9//3////9//3//fwAA/3//f/9/AAD/f/9//38AAP9//3//fwAA/3//f/9/////f/9//38AAP///3//f///////f/9///////9//3///////3//f///////f/9///////9//3///+/g8GAOn3fw7+DwYA6fd/Dv4PBgDp938O/g8GAOn3fw/v///9mHEfz+////KJjq8/7/////f/7//v///w6fd/D/f/9/+pkB8/9//39Nd1kE/3//fw6fd/D/f/9//38AAP9//38EZv0M/3//f/Bghw//f/9/emG88P9//3/SiGkE/3//f7hZ2+z/f/9//38AAP9//3+EnkIP/3//f0amIxP+/wAACXYE+/7/AADVXmrv/v8AAP9////+/wAAuFnb7LjZRqa4WdvsuNlGprhZ2+y42UamuFnb7LjZRqa4Wdvs/3//f921EOX/f/9/iWE6D/9//39TwtTe/3//f/9/AAD/f/9/IUruGv9//3+rPSoh/v8AACuU6PX+/wAAI6ds7P7/AAD/f////v8AAFPC1N6qvas9U8LU3qq9qz1TwtTeqr2rPVPC1N6qvas9U8LU3v9/AAAAAP8//38AAP///7//fwAA////v/9/AAAAAP8//38AAAAA/z//fwAAAAD/P/7///////+//v///////7/+////////v/7///////+//v///////7/+////////v6OmpCb///+/o6akJv///7+jpqQm////v6OmpCb///+//3///wAA/7//f///////P/9///////8//3///wAA/7//f///////P/9///////8/qapU1f///7+pqlTV////v6mqVNX///+/qapU1f///7/+////////v/7///////+//v///////7/+////////v/7///////+//v///////7/yZAwbDBuFzfJkDBsMG4XN8mQMGwwbhc3yZAwbDBuFzf8//z//P//f/z//P/8//9//P/8//z//3/8//z//P//fDBvyZPJkePIMG/Jk8mR48gwb8mTyZHjyDBvyZPJkePIMGwybDJt48gwbDJsMm3jyDBsMmwybePIMGwybDJt48v8//7//v//f/z//v/+//9//P/+//7//3/8//7//v//f8mTy5PLkhc3yZPLk8uSFzfJk8uTy5IXN8mTy5PLkhc0Mm/Lk8uR4sgyb8uTy5HiyDJvy5PLkeLIMm/Lk8uR4sv+//7//v/+f/7//v/+//5//v/+//7//n/+//7//v/+f8uQMmwybhY3y5AybDJuFjfLkDJsMm4WN8uQMmwybhY3y5PJk8mSFjfLk8mTyZIWN8uTyZPJkhY3y5PJk8mSFjf+//z//P/+f/7//P/8//5//v/8//z//n/+//z//P/+fDJsMGwwbeLIMmwwbDBt4sgybDBsMG3iyDJsMGwwbeLI1AVIADBuFzTUBUgAMG4XNNQFSAAwbhc01AVIADBuFzeEA4QD/P//f4QDhAP8//9/hAOEA/z//3+EA4QD/P//fUgA1AfJkePJSADUB8mR48lIANQHyZHjyUgA1AfJkePJSAMn+DJt48lIAyf4Mm3jyUgDJ/gybePJSAMn+DJt48uEAHf//v//f4QAd//+//9/hAB3//7//3+EAHf//v//fNQGs//Lkhc01Aaz/8uSFzTUBrP/y5IXNNQGs//Lkhc3J/qz/8uR4ssn+rP/y5Hiyyf6s//LkeLLJ/qz/8uR4sh3/Hf//v/+fHf8d//+//58d/x3//7//nx3/Hf//v/+frP/J/gybhY2s/8n+DJuFjaz/yf4Mm4WNrP/J/gybhY2s/zUB8mSFjaz/NQHyZIWNrP81AfJkhY2s/zUB8mSFjR3/4QD/P/+fHf/hAP8//58d/+EA/z//nx3/4QD/P/+fyf5SAAwbebLJ/lIADBt4ssn+UgAMG3myyf5SAAwbeLK2gor7twLEw7aCivuvAorFtoKK+6gCW8fysSrY////v/KxKtj///+/O6oi+P///787qiL4////vzuqIvj///+/O6oi+P///78fvcfz////vx+9x/P///+/H73H8////78fvcfz////v3LEksz///+/csSSzP///78Mw63n////vwzDref///+/DMOt5////7/9rjLs////v/2uMuz///+//a4y7P///7/9rjLs////v/9//v8AAAzH/3/+/wAA/7+r4FXe0Ioz5avgVd7QijPlft2Ovf///7+Y3ZTuA42U5v9/AAAAADZF/38AAAAAnlItXaEuB0j5MC1doS4HSPkwxXmbKf///z/FeZsp////P8V5myn///8/p3RxPf///z+ndHE9////P6d0cT3///8/00v5Rf///z/TS/lF////P9NL+UX///8/00v5Rf///z9IaExE////P0hoTET///8/SGhMRP///z/+a94uv1CUMf5r3i6/UJQx/mveLr9QlDH+a94u////P/9/AAAAAJw6/38AAAAA/z/NYgZcDZE+H71u8Fquk38dvW7wWq6Tfx3/////////v/////////+//////////7//////////v1z2pqr///+/XPamqv///7///wAA////v/////////+//////////7//////////v///AAD///+///8AAP///78MG/Jk////vwwb8mT///+/DBvyZP///78MG/Jk////v/9//3////8//3//f////z//f/9/////P/9//3////8//3//f////z//f/9/////P/9//3////8//3//f////z//f/9/////P/9//3////8//3//f////z//f/9/////PyJlvhvy5IZNImW+G/Lkhk0iZb4b8uSGTSJlvhvy5IZN/7//v////7//v/+/////v/+//7////+//7//v////79hQGFA/7//X2FAYUD/v/9fYUBhQP+//19hQGFA/7//X/+//z////+//7//P////7//v/8/////v/+//z////+//3//f////z//f/9/////P/9//3////8//3//f////z++GyJlDJt5cr4bImUMm3lyvhsiZQybeXK+GyJlDJt5cvLkDJv///+/8uQMm////7/y5Ayb////v/LkDJv///+//3//f////z//f/9/////P/9//3////8//3//f////z++G9ya8mR5cr4b3JryZHlyvhvcmvJkeXK+G9ya8mR5cv8//7////+//z//v////7//P/+/////v/8//7////+//3//f////z//f/9/////P/9//3////8//3//f////z9hQJ2//z//X2FAnb//P/9fYUCdv/8//19hQJ2//z//Xwub8uT///+/C5vy5P///78Lm/Lk////vwub8uT///+/ImVA5Awbhk0iZUDkDBuGTSJlQOQMG4ZNImVA5Awbhk0MGwyb////vwwbDJv///+/DBsMm////78MGwyb////v9yaQOQMG3ky3JpA5AwbeTLcmkDkDBt5MtyaQOQMG3ky/z//P////7//P/8/////v/8//z////+//z//P////7//f/9/////P/9//3////8//3//f////z//f/9/////P52/nb//P/8fnb+dv/8//x+dv52//z//H52/nb//P/8f82Ty5P///7/zZPLk////v/Nk8uT///+/82Ty5P///7//f/9/////P/9//3////8//3//f////z//f/9/////P0Dk3JryZIYNQOTcmvJkhg1A5Nya8mSGDUDk3JryZIYN/3//fwAA/7//f/9/AAD/v/9//38AAP+//3//fwAA/7//f/9/AAD/v/9//38AAP+//3//fwAA/7//f/9/AAD/v/9//38AAP+//3//fwAA/7//f/9/AAD/v/9//38AAP6//3//f////z//f/9/////P/9//3////8//3//f////z9A5CJlDJuGDUDkImUMm4YNQOQiZQybhg1A5CJlDJuGDf////8AAP8//////wAA/z//////AAD/P/////8AAP8//////wAA/z//////AAD/P/////8AAP8//////wAA/z//////AAD/P/////8AAP8//////wAA/z//////AAD/PwybDBv///+/DJsMG////78Mmwwb////vwybDBv///+/nb9hQP+//x+dv2FA/7//H52/YUD/v/8fnb9hQP+//x/yZAwb////v/JkDBv///+/8mQMG////7/yZAwb////v9yavhvy5Hky3Jq+G/LkeTLcmr4b8uR5Mtyavhvy5Hky/3//f////z//f/9/////P/9//3////8//3//f////z/y5PJk////v/Lk8mT///+/8uTyZP///7/y5PJk////v8JCwsLCwmEhwkLCwsLCYSHCQsLCwsJhIcJCwsLCwmEhAAD/f/9/AAAAAP9//38AAAAA/3//fwAAAAD/f/9/AAAAAP9//38AAAAA/3//fwAADx/wYPBghw8PH/Bg8GCHDw8f8GDwYIcPDx/wYPBghw9GJkamRqYjE0YmRqZGpiMTRiZGpkamIxNGJkamRqYjE/7/AADTaxYK/v8AANtYkhP+/wAAqz0qIf7/AAD/fwAAVEKrPas9KiFUQqs9qz0qIVRCqz2rPSohVEKrPas9KiFbWaQm////v1tZpCb///+/W1mkJv///79bWaQm////v1VVVNX///+/VVVU1f///79VVVTV////v1VVVNX///+/DJsMG/LkeTIMmwwb8uR5MgybDBvy5HkyDJsMG/LkeTL/v/8//7//H/+//z//v/8f/7//P/+//x//v/8//7//H/Lk8mQMm4YN8uTyZAybhg3y5PJkDJuGDfLk8mQMm4YN8uQMm/Jkhg3y5Ayb8mSGDfLkDJvyZIYN8uQMm/Jkhg3/v/+//z//H/+//7//P/8f/7//v/8//x//v/+//z//Hwyb8uQMG3kyDJvy5AwbeTIMm/LkDBt5Mgyb8uQMG3ky8mTy5Awbhk3yZPLkDBuGTfJk8uQMG4ZN8mTy5Awbhk3/P/+//z//X/8//7//P/9f/z//v/8//1//P/+//z//XwwbDJvyZHlyDBsMm/JkeXIMGwyb8mR5cgwbDJvyZHlyDBvyZAybeXIMG/JkDJt5cgwb8mQMm3lyDBvyZAybeXL/P/8//7//X/8//z//v/9f/z//P/+//1//P/8//7//X/JkDBvy5IZN8mQMG/Lkhk3yZAwb8uSGTfJkDBvy5IZNyf5SAPLkeTLJ/lIA8uR5Msn+UgDy5Hkyyf5SAPLkeTId/+EA/7//Hx3/4QD/v/8fHf/hAP+//x8d/+EA/7//H6z/NQEMm4YNrP81AQybhg2s/zUBDJuGDaz/NQEMm4YNrP/J/vJkhg2s/8n+8mSGDaz/yf7yZIYNrP/J/vJkhg0d/x3//z//Hx3/Hf//P/8fHf8d//8//x8d/x3//z//H8n+rP8MG3kyyf6s/wwbeTLJ/qz/DBt5Msn+rP8MG3kyNQGs/wwbhk01Aaz/DBuGTTUBrP8MG4ZNNQGs/wwbhk3hAB3//z//X+EAHf//P/9f4QAd//8//1/hAB3//z//X1IAyf7yZHlyUgDJ/vJkeXJSAMn+8mR5clIAyf7yZHlyUgA1AQybeXJSADUBDJt5clIANQEMm3lyUgA1AQybeXLhAOEA/7//X+EA4QD/v/9f4QDhAP+//1/hAOEA/7//XzUBUgDy5IVNNQFSAPLkhk01AVIA8uSGTTUBUgDy5IVN")
-}, {
-"aabb": AABB(-0.22794539, 1.3019515, 0.29643035, 0.46193337, 0.10398173, 0.047971964),
-"attribute_data": PackedByteArray("vEnKPvBBRD/wLMA+jOslP7xJyj6M6yU/8CzAPvBBRD/wLMA+8EFEPyQQtj6M6yU/8CzAPozrJT8kELY+8EFEPyQQtj7wQUQ/WPOrPozrJT8kELY+jOslP1jzqz7wQUQ/WPOrPvBBRD+M1qE+jOslP1jzqz6M6yU/jNahPvBBRD+M1qE+8EFEP8C5lz6M6yU/jNahPozrJT/AuZc+8EFEP8C5lz7wQUQ/9JyNPozrJT/AuZc+jOslP/ScjT7wQUQ/9JyNPvBBRD8ogIM+jOslP/ScjT6M6yU/KICDPvBBRD8ogIM+8EFEP7jGcj6M6yU/KICDPozrJT+4xnI+8EFEP7jGcj7wQUQ/II1ePozrJT+4xnI+jOslPyCNXj7wQUQ/II1ePvBBRD+IU0o+jOslPyCNXj6M6yU/iFNKPvBBRD9qf4g+NCVMPxyNXj5E3UQ/3Kx7PrbQRj9oGyw+NCVMP2BtQT620EY/an+IPhK1Wj9MZow+Im1TPxyNXj4A/WE/3Kx7PpAJYD9oGyw+ErVaP2BtQT6QCWA/pE0kPiJtUz+IU0o+8EFEP/AZNj6M6yU/iFNKPozrJT/wGTY+8EFEP/AZNj7wQUQ/WOAhPozrJT/wGTY+jOslP1jgIT7wQUQ/emOdPrbQRj82g7o+ttBGP1jzqz5E3UQ/FBPJPiJtUz8yLMU+NCVMP5zTjj4ibVM/fLqSPjQlTD96Y50+kAlgP3y6kj4StVo/NoO6PpAJYD9Y86s+AP1hPzIsxT4StVo/vEnKPvBBRD/wLMA+jOslP/AswD7wQUQ/vEnKPozrJT/wLMA+8EFEPyQQtj6M6yU/JBC2PvBBRD/wLMA+jOslPyQQtj7wQUQ/WPOrPozrJT9Y86s+8EFEPyQQtj6M6yU/WPOrPvBBRD+M1qE+jOslP4zWoT7wQUQ/WPOrPozrJT+M1qE+8EFEP8C5lz6M6yU/wLmXPvBBRD+M1qE+jOslP8C5lz7wQUQ/9JyNPozrJT/0nI0+8EFEP8C5lz6M6yU/9JyNPvBBRD8ogIM+jOslPyiAgz7wQUQ/9JyNPozrJT8ogIM+8EFEP7jGcj6M6yU/uMZyPvBBRD8ogIM+jOslP7jGcj7wQUQ/II1ePozrJT8gjV4+8EFEP7jGcj6M6yU/II1ePvBBRD+IU0o+jOslP4hTSj7wQUQ/II1ePozrJT8cjV4+RN1EP2p/iD40JUw/3Kx7PrbQRj9qf4g+ErVaP0xmjD4ibVM/aBssPjQlTD9gbUE+ttBGP2gbLD4StVo/pE0kPiJtUz8cjV4+AP1hP2BtQT6QCWA/3Kx7PpAJYD+IU0o+8EFEP/AZNj6M6yU/8Bk2PvBBRD+IU0o+jOslP/AZNj7wQUQ/WOAhPozrJT9Y4CE+8EFEP/AZNj6M6yU/NoO6PrbQRj96Y50+ttBGP1jzqz5E3UQ/nNOOPiJtUz98upI+NCVMPxQTyT4ibVM/MizFPjQlTD82g7o+kAlgPzIsxT4StVo/emOdPpAJYD9Y86s+AP1hP3y6kj4StVo/"),
-"bone_aabbs": [AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(-0.22794539, 1.3019515, 0.29643035, 0.46193337, 0.10399175, 0.047971964), AABB(0, 0, 0, -1, -1, -1), AABB(-0.22794539, 1.3019515, 0.29643035, 0.46193337, 0.10399175, 0.047971964), AABB(-0.22794539, 1.3019515, 0.29643035, 0.46193337, 0.10399175, 0.047971964)],
-"format": 34359745559,
-"index_count": 264,
-"index_data": PackedByteArray("AAABAAIAAAADAAEABAAFAAYABAAHAAUACAAJAAoACAALAAkADAANAA4ADAAPAA0AEAARABIAEAATABEAFAAVABYAFAAXABUAGAAZABoAGAAbABkAHAAdAB4AHAAfAB0AIAAhACIAIAAjACEAJAAlACYAJAAnACUAKAApACoAKAArACkAKQArACwALQAoAC4ALwAoAC0AKwAoAC8ALwAtADAAKwAvADEAMQAvADIAKwAxADMANAA1ADYANAA3ADUAOAA5ADoAOAA7ADkAPAA9AD4APAA/AD0APQA/AEAAQQA8AEIAQwA8AEEAPwA8AEMAQwBBAEQAPwBDAEUARQBDAEYAPwBFAEcASABJAEoASABLAEkATABNAE4ATABPAE0AUABRAFIAUABTAFEAVABVAFYAVABXAFUAWABZAFoAWABbAFkAXABdAF4AXABfAF0AYABhAGIAYABjAGEAZABlAGYAZABnAGUAaABpAGoAaABrAGkAbABtAG4AbABvAG0AcABxAHIAcABzAHEAcQBzAHQAdQBwAHYAdwBwAHUAcwBwAHcAdwB1AHgAcwB3AHkAeQB3AHoAcwB5AHsAfAB9AH4AfAB/AH0AgACBAIIAgACDAIEAhACFAIYAhACHAIUAhQCHAIgAiQCEAIoAiwCEAIkAhwCEAIsAiwCJAIwAhwCLAI0AjQCLAI4AhwCNAI8A"),
-"material": SubResource("StandardMaterial3D_uisdr"),
-"name": "Material.002",
-"primitive": 3,
-"skin_data": PackedByteArray("BwAIAAUAAAD+/wAAAAAAAAcABQAIAAAA//8AAAAAAAAHAAUACAAAAP7/AAAAAAAABwAIAAUAAAD9/wEAAAAAAAcACAAFAAAA/f8BAAAAAAAHAAUACAAAAP//AAAAAAAABwAFAAgAAAD//wAAAAAAAAcACAAAAAAA/P8CAAAAAAAHAAgAAAAAAPz/AgAAAAAABwAFAAAAAAD+/wAAAAAAAAcABQAIAAAA//8AAAAAAAAHAAgABQAAAP//AAAAAAAABwAIAAUAAAD//wAAAAAAAAcACAAAAAAA//8AAAAAAAAHAAUAAAAAAP7/AAAAAAAABwAIAAUAAAD6/wQAAAAAAAcACAAFAAAA+v8EAAAAAAAHAAUAAAAAAP7/AAAAAAAABwAIAAAAAAD//wAAAAAAAAcACAAFAAAA+/8DAAAAAAAHAAgABQAAAPv/AwAAAAAABwAAAAAAAAD//wAAAAAAAAcABQAAAAAA/v8AAAAAAAAHAAgABQAAAP7/AAAAAAAABwAIAAUAAAD+/wAAAAAAAAcABQAIAAAA//8AAAAAAAAHAAAAAAAAAP//AAAAAAAABwAIAAAAAAD+/wAAAAAAAAcACAAAAAAA/v8AAAAAAAAHAAAAAAAAAP//AAAAAAAABwAFAAgAAAD//wAAAAAAAAcABQAAAAAA/v8AAAAAAAAHAAUAAAAAAP7/AAAAAAAABwAFAAgAAAC1/0kAAAAAAAcAAAAAAAAA//8AAAAAAAAHAAgABQAAAPX/CQAAAAAABwAIAAUAAAD1/wkAAAAAAAcABQAIAAAA/v8AAAAAAAAHAAUACAAAALX/SQAAAAAABwAFAAgAAAD1/wkAAAAAAAcABQAIAAAA//8AAAAAAAAHAAUACAAAAP7/AAAAAAAABwAFAAgAAAD//wAAAAAAAAcABQAIAAAA/v8AAAAAAAAHAAUACAAAAP7/AAAAAAAABwAIAAAAAAD//wAAAAAAAAcABQAAAAAA/v8AAAAAAAAHAAAAAAAAAP//AAAAAAAABwAFAAAAAAD+/wAAAAAAAAcAAAAAAAAA//8AAAAAAAAHAAUACAAAAP//AAAAAAAABwAFAAgAAAC1/0kAAAAAAAcABQAIAAAA9f8JAAAAAAAHAAUACAAAAP7/AAAAAAAABwAFAAgAAAD+/wAAAAAAAAcABQAIAAAAPP6TAS8AAAAHAAUACAAAADz+kwEvAAAABwAFAAgAAAD+/wAAAAAAAAcABQAIAAAA/v8AAAAAAAAHAAgABQAAAP7/AAAAAAAABwAFAAgAAAA8/pMBLwAAAAcACAAFAAAA/f8BAAAAAAAHAAgABQAAAP7/AAAAAAAABwAIAAUAAAD//wAAAAAAAAcACAAAAAAA/P8CAAAAAAAHAAgABQAAAPX/CQAAAAAABwAFAAgAAAD1/wkAAAAAAAcACAAAAAAA/v8AAAAAAAAHAAUAAAAAAP7/AAAAAAAABwAIAAUAAAD7/wMAAAAAAAcACAAFAAAA/v8AAAAAAAAHAAgABQAAAPr/BAAAAAAABwAIAAUAAAD+/wAAAAAAAAcABQAIAAAA//8AAAAAAAAHAAgABQAAAP3/AQAAAAAABwAFAAgAAAD+/wAAAAAAAAcACAAFAAAA/f8BAAAAAAAHAAUACAAAAP//AAAAAAAABwAIAAAAAAD8/wIAAAAAAAcABQAIAAAA//8AAAAAAAAHAAgAAAAAAPz/AgAAAAAABwAFAAAAAAD+/wAAAAAAAAcACAAFAAAA//8AAAAAAAAHAAUACAAAAP//AAAAAAAABwAIAAUAAAD//wAAAAAAAAcACAAAAAAA//8AAAAAAAAHAAgABQAAAPr/BAAAAAAABwAFAAAAAAD+/wAAAAAAAAcACAAFAAAA+v8EAAAAAAAHAAUAAAAAAP7/AAAAAAAABwAIAAUAAAD7/wMAAAAAAAcACAAAAAAA//8AAAAAAAAHAAgABQAAAPv/AwAAAAAABwAAAAAAAAD//wAAAAAAAAcACAAFAAAA/v8AAAAAAAAHAAUAAAAAAP7/AAAAAAAABwAIAAUAAAD+/wAAAAAAAAcABQAIAAAA//8AAAAAAAAHAAgAAAAAAP7/AAAAAAAABwAAAAAAAAD//wAAAAAAAAcACAAAAAAA/v8AAAAAAAAHAAAAAAAAAP//AAAAAAAABwAFAAAAAAD+/wAAAAAAAAcABQAIAAAA//8AAAAAAAAHAAUAAAAAAP7/AAAAAAAABwAFAAgAAAC1/0kAAAAAAAcACAAFAAAA9f8JAAAAAAAHAAAAAAAAAP//AAAAAAAABwAIAAUAAAD1/wkAAAAAAAcABQAIAAAA/v8AAAAAAAAHAAUACAAAAPX/CQAAAAAABwAFAAgAAAC1/0kAAAAAAAcABQAIAAAA/v8AAAAAAAAHAAUACAAAAP//AAAAAAAABwAFAAgAAAD//wAAAAAAAAcACAAAAAAA//8AAAAAAAAHAAUAAAAAAP7/AAAAAAAABwAFAAgAAAD+/wAAAAAAAAcABQAIAAAA/v8AAAAAAAAHAAAAAAAAAP//AAAAAAAABwAFAAgAAAC1/0kAAAAAAAcAAAAAAAAA//8AAAAAAAAHAAUACAAAAP//AAAAAAAABwAFAAAAAAD+/wAAAAAAAAcABQAIAAAA9f8JAAAAAAAHAAUACAAAAP7/AAAAAAAABwAFAAgAAAA8/pMBLwAAAAcABQAIAAAA/v8AAAAAAAAHAAUACAAAADz+kwEvAAAABwAFAAgAAAD+/wAAAAAAAAcACAAFAAAA/v8AAAAAAAAHAAUACAAAAP7/AAAAAAAABwAIAAUAAAD9/wEAAAAAAAcABQAIAAAAPP6TAS8AAAAHAAgABQAAAP7/AAAAAAAABwAIAAUAAAD1/wkAAAAAAAcABQAIAAAA9f8JAAAAAAAHAAgABQAAAP//AAAAAAAABwAIAAAAAAD8/wIAAAAAAAcACAAFAAAA+/8DAAAAAAAHAAgABQAAAPr/BAAAAAAABwAIAAAAAAD+/wAAAAAAAAcACAAFAAAA/v8AAAAAAAAHAAUAAAAAAP7/AAAAAAAA"),
-"uv_scale": Vector4(0, 0, 0, 0),
-"vertex_count": 144,
-"vertex_data": PackedByteArray("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8uQMm/JkePLy5Ayb8mR48vLkDJvyZHjy8uTyZAybePLy5PJkDJt48vLk8mQMm3jy8uTyZAybePL/v/8//7//3/+//z//v//f/7//P/+//9//v/8//7//3wybDBvy5IXNDJsMG/Lkhc0Mmwwb8uSFzQybDBvy5IXN8mQMG/LkeLLyZAwb8uR4svJkDBvy5Hiy8mQMG/LkeLL/P/8//7//n/8//z//v/+f/z//P/+//5//P/8//7//nwwb8mQMm4WNDBvyZAybhY0MG/JkDJuFjQwb8mQMm4WNDBsMm/JkhY0MGwyb8mSFjQwbDJvyZIWNDBsMm/JkhY3/f/9//v//v/9//3////+//3//f/7//7//f/9//v//v/9//3/+//+//3//f/7//7//f/9//v//v/9//3////+//3//f/7//7//f/9//v//v/9//3/+//+//3//f/7//7//P/+//z//n/8//7//P/+f/z//v/8//5//P/+//z//n/Jk8uQMG3iy8mTy5AwbeLLyZPLkDBt4svJk8uQMG3iy/v////7//z/+/////v//P/7///////8//v///////z/+/////v//P/7///////8//v////7//z/+/////v//P/7////+//8//v////7//z/+////////P/7////+//8/8mTy5PLkeTLyZPLk8uR5MvJk8uTy5Hky8mTy5PLkeTL/P/+//7//H/8//7//v/8f/z//v/+//x//P/+//7//HwwbDJsMm4YNDBsMmwybhg0MGwybDJuGDQwbDJsMm4YNDBvyZPJkhg0MG/Jk8mSGDQwb8mTyZIYNDBvyZPJkhg3/P/8//z//H/8//z//P/8f/z//P/8//x//P/8//z//H/JkDBsMG3ky8mQMGwwbeTLyZAwbDBt5MvJkDBsMG3kyDJsMGwwbhk0MmwwbDBuGTQybDBsMG4ZNDJsMGwwbhk3/v/8//z//X/+//z//P/9f/7//P/8//1//v/8//z//X/Lk8mTyZHly8uTyZPJkeXLy5PJk8mR5cvLk8mTyZHly8uQMmwybeXLy5AybDJt5cvLkDJsMm3ly8uQMmwybeXL/f/9/AAD/P/9//38AAP8//3//fwAA/z//f/9/AAD/P/9//38AAP8//3//fwAA/z//f/9/AAD/P/9//38AAP8//3//fwAA/z//f/9/AAD/P/9//38AAP8//3//fwAA/z//v/+//7//X/+//7//v/9f/7//v/+//1//v/+//7//Xwyb8uTy5IZNDJvy5PLkhk0Mm/Lk8uSGTQyb8uTy5IZN/v///wAA/7/+////AAD/v/7///8AAP+//v///wAA/7/+////AAD/v/7///8AAP+//v///wAA/7/+////AAD/v/7///8AAP+//v///wAA/7/+////AAD/v/7///8AAP+/")
-}]
-blend_shape_mode = 0
diff --git a/assets/characters/player/meshes/godot_robot_limbs_and_head.tres b/assets/characters/player/meshes/godot_robot_limbs_and_head.tres
deleted file mode 100644
index ccd23f2..0000000
--- a/assets/characters/player/meshes/godot_robot_limbs_and_head.tres
+++ /dev/null
@@ -1,29 +0,0 @@
-[gd_resource type="ArrayMesh" format=4 uid="uid://b3f7h1naq4w6e"]
-
-[ext_resource type="Texture2D" uid="uid://bdg48m6l86q8i" path="res://assets/characters/player/3DGodotRobot_GodotPalette.png" id="1_w11jc"]
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_4omi2"]
-resource_name = "Material"
-cull_mode = 2
-albedo_texture = ExtResource("1_w11jc")
-roughness = 0.4
-
-[resource]
-resource_name = "3DGodotRobot_Cube"
-_surfaces = [{
-"aabb": AABB(-0.8324551, 0.011430549, -0.3270204, 1.6709528, 1.738364, 0.6902065),
-"attribute_data": PackedByteArray("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eIxnP7zZZj5z9iA/BIQ+PgXhFj9MLhY+c/YgP0wuFj4F4RY/BIQ+PnP2ID+Ywps+c/YgP7yXhz5z9iA/vJeHPnP2ID+Ywps+eIxnP7zZZj4Kd10/BIQ+PniMZz8EhD4+CnddP7zZZj54jGc/TC4WPgp3XT9MLhY+wIFiPyCx2z3AgWI/MEPYPgp3XT9SGMQ+eIxnP1IYxD54jGc/du2vPgp3XT+Ywps+eIxnP5jCmz4Kd10/du2vPgp3XT+82WY+CnddP7yXhz4Kd10/vJeHPgp3XT+82WY+eIxnPwSEPj4Kd10/TC4WPniMZz9MLhY+CnddPwSEPj54jGc/UhjEPgp3XT927a8+eIxnP3btrz4Kd10/UhjEPlJsWD8wQ9g+nGFTP1IYxD4Kd10/UhjEPgp3XT927a8+nGFTP5jCmz4Kd10/mMKbPpxhUz927a8+CnddP7yXhz6cYVM/wNlmPgp3XT+82WY+nGFTP7yXhz4Kd10/BIQ+PpxhUz9MLhY+CnddP0wuFj6cYVM/BIQ+Pgp3XT9SGMQ+nGFTP3btrz4Kd10/du2vPpxhUz9SGMQ+CnddP5jCmz6cYVM/vJeHPgp3XT+8l4c+nGFTP5jCmz4Kd10/vNlmPpxhUz8EhD4+CnddPwSEPj6cYVM/wNlmPgp3XT9MLhY+nGFTP0wuFj5SbFg/ILHbPXP2ID+8l4c+c/YgP8DZZj5z9iA/wNlmPnP2ID+8l4c+CnddP5jCmz54jGc/mMKbPniMZz+Ywps+CnddP5jCmz4Kd10/vNlmPgp3XT+8l4c+CnddP7yXhz4Kd10/vNlmPnP2ID+Ywps+c/YgP7yXhz5z9iA/vJeHPnP2ID+Ywps+c/YgP8DZZj4F4RY/vNlmPgXhFj+82WY+c/YgP8DZZj4Kd10/vJeHPgp3XT+Ywps+CnddP5jCmz4Kd10/vJeHPgXhFj+Ywps+c/YgP5jCmz5z9iA/mMKbPgXhFj+Ywps+c/YgP7yXhz5z9iA/wNlmPnP2ID/A2WY+c/YgP7yXhz5z9iA/vJeHPnP2ID/A2WY+c/YgP8DZZj5z9iA/vJeHPgp3XT+Ywps+eIxnP5jCmz54jGc/mMKbPgp3XT+Ywps+CnddP7zZZj4Kd10/vJeHPgp3XT+8l4c+CnddP7zZZj5z9iA/mMKbPnP2ID+8l4c+c/YgP7yXhz5z9iA/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eIxnP3btrz5SbFg/MEPYPgp3XT9SGMQ+nGFTP1IYxD4Kd10/du2vPpxhUz+Ywps+nGFTP3btrz4Kd10/mMKbPgp3XT+8l4c+nGFTP8DZZj6cYVM/vJeHPgp3XT+82WY+CnddPwSEPj6cYVM/TC4WPpxhUz8EhD4+CnddP0wuFj4Kd10/UhjEPpxhUz927a8+nGFTP1IYxD4Kd10/du2vPgp3XT+Ywps+nGFTP7yXhz6cYVM/mMKbPgp3XT+8l4c+CnddP7zZZj6cYVM/BIQ+PpxhUz/A2WY+CnddPwSEPj4Kd10/TC4WPlJsWD8gsds9nGFTP0wuFj5z9iA/vJeHPnP2ID/A2WY+c/YgP7yXhz5z9iA/wNlmPgp3XT+Ywps+eIxnP5jCmz4Kd10/mMKbPniMZz+Ywps+CnddP7zZZj4Kd10/vJeHPgp3XT+82WY+CnddP7yXhz5z9iA/mMKbPnP2ID+8l4c+c/YgP5jCmz5z9iA/vJeHPnP2ID/A2WY+BeEWP7zZZj5z9iA/wNlmPgXhFj+82WY+CnddP7yXhz4Kd10/mMKbPgp3XT+8l4c+CnddP5jCmz4F4RY/mMKbPnP2ID+Ywps+BeEWP5jCmz5z9iA/mMKbPnP2ID+8l4c+c/YgP8DZZj5z9iA/vJeHPnP2ID/A2WY+c/YgP7yXhz5z9iA/wNlmPnP2ID+8l4c+c/YgP8DZZj4Kd10/mMKbPniMZz+Ywps+CnddP5jCmz54jGc/mMKbPgp3XT+82WY+CnddP7yXhz4Kd10/vNlmPgp3XT+8l4c+c/YgP5jCmz5z9iA/vJeHPnP2ID+Ywps+c/YgP7yXhz54jGc/vNlmPgp3XT+82WY+eIxnP7zZZj4Kd10/vNlmPgXhFj+Ywps+c/YgP5jCmz4F4RY/mMKbPnP2ID+Ywps+c/YgP8DZZj4F4RY/vNlmPnP2ID/A2WY+BeEWP7zZZj4Kd10/vJeHPgp3XT+Ywps+CnddP7yXhz4Kd10/mMKbPgp3XT+82WY+CnddP7yXhz4Kd10/vNlmPgp3XT+8l4c+c/YgP5jCmz5z9iA/vJeHPnP2ID+Ywps+c/YgP7yXhz4Kd10/vJeHPgp3XT+Ywps+CnddP7yXhz4Kd10/mMKbPnP2ID/A2WY+BeEWP7zZZj5z9iA/wNlmPgXhFj+82WY+eIxnP7zZZj4Kd10/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"),
-"bone_aabbs": [AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(0, 0, 0, -1, -1, -1), AABB(-0.34829003, 0.99242246, -0.3270204, 0.70262265, 0.45240438, 0.57711995), AABB(-0.39784884, 0.99242246, -0.3270204, 0.8017403, 0.757372, 0.6902065), AABB(-0.39784884, 1.0330851, -0.3270204, 0.8017403, 0.7167094, 0.6902065), AABB(-0.39784884, 1.0330851, -0.29384893, 0.8017403, 0.63939404, 0.6570351), AABB(-0.35115188, 0.99242246, -0.24022739, 0.70834637, 0.6186985, 0.6034135), AABB(0.13309029, 0.81245255, -0.011300141, 0.22700986, 0.16540486, 0.05885993), AABB(0.20872284, 0.81245255, -0.06373058, 0.28118175, 0.16540486, 0.163482), AABB(0.40291014, 0.82386726, -0.053554296, 0.21176812, 0.1343168, 0.1432743), AABB(0.5375689, 0.76555276, -0.11064109, 0.30092877, 0.25744808, 0.2574479), AABB(-0.35405755, 0.81245255, -0.011300141, 0.22700986, 0.16540486, 0.05885993), AABB(-0.483862, 0.81245255, -0.06373058, 0.28118178, 0.16540486, 0.163482), AABB(-0.60863566, 0.82386726, -0.053554296, 0.21176812, 0.1343168, 0.1432743), AABB(-0.8324551, 0.76555276, -0.11064109, 0.30092877, 0.25744808, 0.2574479), AABB(0.053841557, 0.26938796, -0.100343816, 0.21895915, 0.38650072, 0.23685315), AABB(0.043908834, 0.11187095, -0.10034387, 0.23685312, 0.35968155, 0.23685317), AABB(0.021863457, 0.011430549, -0.15571119, 0.28851116, 0.17981887, 0.44429904), AABB(0.021863457, 0.011430549, -0.15571119, 0.28851116, 0.1559344, 0.44429904), AABB(-0.2667581, 0.26938796, -0.100343816, 0.21895915, 0.38650072, 0.23685315), AABB(-0.27471936, 0.11187095, -0.10034387, 0.23685312, 0.35968155, 0.23685317), AABB(-0.30433202, 0.011430549, -0.15571119, 0.28851116, 0.17981887, 0.44429904), AABB(-0.30433202, 0.011430549, -0.15571119, 0.28851116, 0.1559344, 0.44429904)],
-"format": 34359745559,
-"index_count": 14304,
-"index_data": PackedByteArray("sQqyCrMKsQq0CrIKFAAVABYAFAAXABUAtQoZALYKtQoYABkAtwolACYAtwq4CiUAuQotAC4AuQq6Ci0Auwq8CmwAuwq9CrwKOAC+Cr8KOAA8AL4KwArBCsIKwArDCsEKxAoDAAQAxArFCgMAxgrHCsgKxgrJCscKygrLCswKygrNCssKUABRAFIAUABTAFEACQDOCgoACQDPCs4K0ArRCjsA0ArSCtEKPwDTCkAAPwDUCtMK1QrWCgIA1QrXCtYK2AohANkK2AraCiEAVABVAFYAVABXAFUA2wpnAGgA2wrcCmcAWABZAFoAWABbAFkA3QreCt8K3QrgCt4K4QriCuMK5ArjCuIK4QrlCuIK5grlCuEK5grnCuUK5AriCugK6AriCuwBYADkCugK6ArsAekKYADoCmEAYQDoCukKYQDpCl8A6grrCuwK6grtCusKSgDuCu8KSgBIAO4KSwDwCkwASwDxCvAK8grzCiwA8gr0CvMKRAD1CvYKRAD3CvUKLwD4CjAALwD5CvgK+gpcAF0A+gr7ClwA/AopAP0K/AoqACkA/godAP8K/gofAB0AYgBjAGQAYgBlAGMAHgAACxsAHgABCwALKwACCyQAKwADCwILSQAECwULSQAGCwQLBwsICzIABwsJCwgLCgsLCwwLCgsNCwsLXgAOCw8LXgAQCw4LZgARCxILZgBpABELIgAgABMLIgAjACAAAAAUCxULAAAWCxQLOgAXCxgLOgAZCxcLGgsbCxwLGgsdCxsLagAeCx8LagAgCx4LIQsiCyMLIQskCyILJQsmCycLJQtrACYLJwAoCygAJwApCygLKgsaABwAKgsrCxoA6AEsCy0L6AEuCywLbQAvCzALbQBwAC8LBgAxCwUABgAyCzELMwsHAAgAMws0CwcAcQByAHMAcQB0AHIANQtuAG8ANQs2C24AdQB2AHcAdQB4AHYA7QHuAe8B/gH/AQAC/gEBAv8BAgIDAgQCAgIFAgMCBgIHAggCBgIJAgcCCgILAgwCCgINAgsCDgIPAhACDgIRAg8CEgITAhQCEgIVAhMCNwsXAhgCJwIoAikCJwIqAigCKwIsAi0CKwIuAiwCLwIwAjECLwIyAjACMwI0AjUCMwI2AjQCOAsZAhoC8AHxATkLNwI4AjkCNwI6AjgCOwI8Aj0COwI+AjwCPwJAAkECPwJCAkACQwJEAkUCQwJGAkQCRwJIAkkCRwJKAkgCOgs7CyAC9gE8Cz0LSwJMAk0CSwJOAkwCTwJQAlECTwJSAlACUwJUAlUCUwJWAlQCHwI+CyIC+AH5AfcBVwJYAlkCVwJaAlgCWwJcAl0CWwJeAlwCXwJgAmECXwJiAmACYwJkAmUCYwJmAmQCZwJoAmkCZwJqAmgCawJsAm0CawJuAmwCbwJwAnECbwJyAnACcwJ0AnUCcwJ2AnQCdwJ4AnkCdwJ6AngCewJ8An0CewJ+AnwCfwKAAoECfwKCAoACgwKEAoUCgwKGAoQCIQIjAiQC+gE/C0ALhwKIAokChwKKAogCQQuMAo0CQQtCC4wClgKXApgClgKZApcCQwslAiYC/AH9AfsBmgKbApwCmgKdApsCRAtFC0YLRAtHC0ULngKfAqACngKhAp8CogJIC6QCogJJC0gLqgKrAqwCqgKtAqsCHQJKCx4C9AHzAfUBkwKUAksLkwKVApQC1wJMC9kC1wJNC0wLsAKxArICsAKzArECtAK1ArYCtAK3ArUC8gFOC08LuAK5AroCuAK7ArkCvAK9Ar4CvAK/Ar0CwALBAsICwALDAsECxALFAsYCxALHAsUCyALJAsoCyALLAskCzALNAs4CzALPAs0CGwIWAhwCUAvRAlELUAvTAtECUguPAlMLUguOAo8CVAvYAlULVAvaAtgCVgulAlcLVgujAqUC0gJYC9YC0gJZC1gLkQJaC4sCkQJbC1oLpgJcC10LpgKnAlwL0AJeC18L0AJgC14L+QJhC/sC+QJiC2EL9QJjC/cC9QJkC2ML2wJlC90C2wJmC2UL3wJnC+EC3wJoC2cLaQuuAmoLaQuvAq4CawupAmwLawuoAqkCbQvVAm4LbQvUAtUCbwuQAnALbwuSApACcQvcAnILcQveAtwCcwvgAnQLcwviAuACdQvkAnYLdQvmAuQCdwvoAngLdwvqAugC/QJ5C/8C/QJ6C3kLAQN7CwMDAQN8C3sL5wJ9C+kC5wJ+C30L4wJ/C+UC4wKAC38L6wKBC4IL6wLsAoEL7QLuAu8C7QKDC+4C8AKEC4UL8ALxAoQLhgvzAvQChgvyAvMChwv2AogLhwv4AvYCiQv6AooLiQv8AvoCiwv+AowLiwsAA/4CjQsCA44LjQsEAwIDjwuQC5ELjwuSC5ALFQMWAxcDFQMYAxYDHQOTC5QLHQMeA5MLlQuWCx8DBQOXC5gLmQsGA5oLmQubCwYDGQMaAxsDGQMcAxoDnAudC54LnAufC50LnQugC6ELDAOiC6MLpAsNA6ULpAumCw0DpwsxAzIDpwuoCzEDqQuqC6sLqQusC6oLrQuuC68LrQuwC64LOAOxC7ILOAM9A7ELswu0C7ULswu2C7QLNgO3CzADNgO4C7cLuQslAyADuQu6CyUDuwu8C70Luwu+C7wLQAO/C0IDQAPAC78LIQPBC8ILIQMnA8ELwwvECyYDDwPFC8YLxwvIC8kLxwvKC8gLywvMC80LywvOC8wLSANJA0oDSANLA0kDzwvQC9ELzwsqA9AL0gvTCykDEQPUC9UL1gvXCw4D1gvYC9cLTANNA04DTANPA00D2QvaC9sL2QvcC9oL3QveC98L3QstA94LKAMrAywDEwPgC+EL4gvjCxAD4gvkC+MLUANRA1IDUANTA1ED5QvmC+cL5QvoC+YL6QvqC+sL6QvsC+oLVANVA1YDVANXA1UD7QvuC+8L8AsUAxID8AvxCxQD8gtZA1oD8gvzC1kD9Av1C/YL9Av3C/UL+Av5C/oL+Av7C/kLWwP8C1gDWwNcA/wL/Qv+C/8L/QsvA/4LAAwBDC4DAgwDDAQMAgwFDAMMBgwHDAgMBgwIAwcMCwMIAwYMXQNeAwkMXQNhA14DJAMKDAsMJAMMDAoMDQwODA8MEAwJAwoDEAwRDAkDEgxgA18DEgwTDGADFAwVDBYMFAwXDBUMYgNjA2QDYgNlA2MDIgMYDBkMIgMaDBgMGwwcDCMDHQweDB8MIAwHAyEMIAwiDAcDZgNnA2gDZgNpA2cDIwwkDCUMIwwmDCQMJwwoDCkMJwwqDCgMKwwsDC0MKwwuDCwMLwwwDDEMLwwyDDAMMww0DDUMNgw3DDgMOQw6DDsMOQw8DDoMPQw+DD8MPQxADD4MQQxCDEMMQQxEDEIMRQxGDEcMSAxJDEoMSAxLDEkMTAxNDE4MTAxPDE0MUAxRDFIMUAxTDFEMVAxVDFYMVAxXDFUMWAxZDFoMWAxbDFkMXAxdDF4MXAxfDF0MYAxhDGIMYwxkDGUMYwxmDGQMZwxoDGkMZwxqDGgMawxsDG0MawxuDGwMbwxwDHEMbwxyDHAMcwx0DHUMcwx2DHQMdwx4DHkMegx7DHwMfQx+DH8MfQyADH4MgQyCDIMMgQyEDIIMhQyGDIcMhQyIDIYMiQyKDIsMiQyMDIoMjQyODI8MkAyRDJIMkwyUDJUMkwyWDJQMlwyYDJkMlwyaDJgMmwycDJ0MmwyeDJwMnwygDKEMnwyiDKAMowykDKUMpgynDKgMqQyqDKsMqQysDKoMrQyuDK8MrQywDK4MsQyyDLMMsQy0DLIMtQy2DLcMtQy4DLYMuQy6DLsMuQy8DLoMvQy+DL8MwAzBDMIMwAzDDMEMxAzFDMYMxAzHDMUMyAzJDMoMyAzLDMkMzAzNDM4MzAzPDM0M0AzRDNIM0AzTDNEM1AzVDNYM1AzXDNUM2AzZDNoM2wzcDN0M2wzeDNwM3wzgDOEM3wziDOAM4wzkDOUM4wzmDOQM5wzoDOkM5wzqDOgM6wzsDO0M7gzvDPAM8QzyDPMM8Qz0DPIM9Qz2DPcM9Qz4DPYM+Qz6DPsM+Qz8DPoM/Qz+DP8M/QwADf4MAQ0CDQMNAQ0EDQINBQ0GDQcNCA0JDQoNCw0MDQ0NCw0ODQwNDw0QDRENDw0SDRANEw0UDRUNEw0WDRQNFw1HAxgNFw1GA0cDGQ1sAxoNGQ1rA2wDGw00AxwNGw0zAzQDHQ1EAx4NHQ1FA0QDbQMfDSANbQNqAx8NIQ1BA0MDIQ0iDUEDOgMjDTsDOgMkDSMNbwMlDXADbwMmDSUNfgMnDYADfgMoDScNqgMpDawDqgMqDSkNpgMrDagDpgMsDSsNogMtDaQDogMuDS0NLw01AzANLw03AzUDMQ08AzINMQ05AzwDMw1xAzQNMw1uA3EDNQ0/AzYNNQ0+Az8DNw1zAzgNNw11A3MDcgM3DTgNcgM4DXQDOQ13AzoNOQ15A3cDOw17AzwNOw19A3sDPQ1/Az4NPQ2BA38DegM/DXwDegNADT8NdgNBDXgDdgNCDUENngNDDaADngNEDUMNRQ1GDUcNRQ1IDUYNSQ1KDUsNSQ1MDUoNTQ1ODU8NTQ1QDU4NUQ1SDVMNUQ1UDVINVQ1WDVcNVQ1YDVYNWQ1aDVsNWQ1cDVoNXQ1eDV8NXQ1gDV4NYQ1iDWMNYQ1kDWINZQ1mDWcNZQ1oDWYNaQ1qDWsNaQ1sDWoNbQ1uDW8NbQ1wDW4NcQ1yDXMNcQ10DXINdQ12DXcNdQ14DXYNeQ16DXsNeQ18DXoNfQ1+DX8NfQ2ADX4NgQ2CDYMNgQ2EDYINhQ2GDYcNhQ2IDYYNiQ2IDYUNig2FDYsNjA2LDY0Njg2HDY8Njg2PDZANkQ2SDYgNkw2UDZINkg2UDZUNkg2VDZYNlg2VDZcNlg2XDZgNlQ2ZDZcNlA2aDZUNlQ2aDZkNlA2bDZoNmg2cDZkNmw2dDZoNmg2dDZwNnQ2eDZwNmw2fDZ0NnQ2gDZ4Nnw2gDZ0Nnw2hDaANog2hDZ8Now2kDaUNow2mDaQNpw2oDakNpw2qDagNqg2rDagNqg2sDasNqA2rDa0NqA2tDa4Nrw2wDbENsg2zDbQNsg21DbMNtg23DbgNtg25DbcNug27DbwNug29DbsNvg2/DcANvg3BDb8Nwg3DDcQNwg3FDcMNxg3HDcgNxg3JDccNyg3LDcwNyg3NDcsNzg3PDdAN0Q3SDdMN0Q3UDdIN1Q3WDdcN1Q3YDdYN2Q3aDdsN2Q3cDdoN3Q3eDd8N3Q3gDd4N4Q3iDeMN5A3lDeYN5A3nDeUN6A3pDeoN6A3rDekN6w3sDekN6w3tDewN7g3vDfAN7w3xDfIN8Q3zDfQN9Q32DfcN+A35DfoN+A37DfkN/A39Df4N/A3/Df0NAA4BDgIOAw4EDgUOAw4GDgQOBw4IDgkOBw4KDggOCw4MDg0OCw4ODgwODw4QDhEODw4SDhAOEw4UDhUOEw4WDhQOFw4YDhkOFw4aDhgOGw4cDh0OGw4eDhwOHw4gDiEOHw4iDiAOIw4kDiUOIw4mDiQOJw4oDikOJw4qDigOKw4sDi0OKw4uDiwOLw4wDjEOMg4zDjQOMg41DjMONg43DjgONg45DjcOOg47DjwOOg49DjsOPg4/DkAOPg5BDj8OQg5DDkQOQg5FDkMORg5HDkgOSQ5KDksOSQ5MDkoOTQ5ODk8OTQ5QDk4OUQ6DA1IOUQ6CA4MDUw4zBjQGUw5UDjMGhgOHA4gDhgOJA4cDVQ6LA1YOVQ6NA4sDVw6QA44DVw5YDpADWQ5aDlsOWQ5cDloOXQ5eDl8OXQ5gDl4OYQ5iDmMOYQ5kDmIOZQ5mDmcOZQ5oDmYOaQ5qDmsOaQ5sDmoObQ5uDm8ObQ5wDm4OcQ5yDnMOcQ50DnIOdQ52DncOdQ54DnYOeQ56DnsOeQ58DnoOfQ5+Dn8OfQ6ADn4OgQ6CDoMOgQ6EDoIOhQ6GDocOhQ6IDoYOiQ6KDosOiQ6MDooOjQ6ODo8OjQ6QDo4OkQ6SDpMOkQ6UDpIOlQ6WDpcOlQ6YDpYOmQ6aDpsOmQ6cDpoOnA6dDpoOng6cDpkOnA6fDp0Ong6gDpwOoA6fDpwOoA6hDp8Oog6jDqQOog6lDqMOpg6nDqgOpg6pDqcOqg6rDqwOqg6tDqsOrg6vDrAOrg6xDq8OkgOTA5QDkgOVA5MDsg6zDrQOsg61DrMOtg63DrgOtg65DrcOug67DrwOug69DrsOvg6/DsAOvg7BDr8Owg7DDsQOwg7FDsMOxg7HDsgOxg7JDscOyg7LDswOyg7NDssOzg7PDtAOzg7RDs8O0g7TDtQO0g7VDtMO1g7XDtgO1g7ZDtcO2g7bDtwO2g7dDtsO3g7fDuAO3g7hDt8O4g7jDuQO4g7lDuMO5g7nDugO5g7pDucO6g7rDuwO6g7tDusO7g7vDvAO7g7xDu8O8g7zDvQO8g71DvMO9g73DvgO9g75DvcO+g77DvwO+g79DvsO/g7/DgAP/g4BD/8OAg8DDwQPAg8FDwMPBg8HDwgPBg8JDwcPCg8LDwwPCg8NDwsPDg8PDxAPDg8RDw8PEg8TDxQPEg8VDxMPFg8XDxgPFg8ZDxcPGg8bDxwPGg8dDxsPHg8fDyAPHg8hDx8PIg8jDyQPIg8lDyMPJg8nDygPJg8pDycPKg8rDywPKg8tDysPLg8vDzAPLg8xDy8PMg8zDzQPMg81DzMPNg83DzgPNg85DzcPOg87DzwPOg89DzsPPg8/D0APPg9BDz8PQg9DD0QPQg9FD0MPRg9HD0gPRg9JD0cPSg9LD0wPSg9ND0sPSw9ND04PTw9KD1APUQ9KD08PTQ9KD1EPUQ9PD1IPTQ9RD1MPUw9RD1QPTQ9TD1UPVg9XD1gPVg9ZD1cPWg9bD1wPWg9dD1sPXg9fD2APXg9hD18PYg9jD2QPYg9lD2MPZg9nD2gPZg9pD2cPag9rD2wPag9tD2sPbg9vD3APbg9xD28Pcg9zD3QPcg91D3MPdg93D3gPdg95D3cPeg97D3wPeg99D3sPfg9/D4APfg+BD38Pgg+DD4QPgg+FD4MPhg+HD4gPiA+HD4kPhg+KD4cPiw+KD4YPiw+MD4oPiA+JD40PjQ+JD44PjQ+OD48PkA+RD5IPkA+TD5EPlA+VD5YPlA+XD5UPmA+XD5QPmA+ZD5cPmg+bD5wPmg+dD5sPng+fD6APng+hD58P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CxUjBR8FCxUMFSMFNQU2BTcFNQU4BTYFOQU6BTsFOQU8BToFDRU+BT8FDRUOFT4FDxVEBUcFDxVFBUQFRAVFBUYFEBUPFUkFSAUPFRAVRQUPFUgFSAUQFUoFRQVIBUsFSwVIBUwFRQVLBU0FTgVPBVAFTgVRBU8FERVCBRIVERUTFUIFUgVTBVQFUgVVBVMFFBVXBVgFFBUVFVcFWgVbBVwFWgVdBVsFWwVdBV4FXwVaBWAFYQVaBV8FXQVaBWEFYQVfBWIFXQVhBWMFYwVhBWQFXQVjBWUFFhUXFRgVFhUZFRcVdgV3BXgFdgV5BXcFfgUaFRsVfgV/BRoVfwUcFRoVfwUdFRwVHhUbFR8VHhUfFSAVIBUfFYQFHxUhFYAFIRWBBSIVhQUeFSMVhQWDBR4VZgUkFSUVJhUlFWkFaQUlFScVaQUnFSgVKBUnFSkVKBUpFSoVKxVpBSgVKxUoFSwVJBUtFS4VJBUvFS0VbQUvFSQVLxUwFS0VLxUxFTAVcAUxFS8VMBVxBW8FMBUyFXEFMRUyFTAVMhVzBXEFMhUzFXMFMRU0FTIVcgU0FTEVdAU1FTQVNRU2FTMVNRU3FTYVOBVnBWgFOBU5FWcFegV7BXwFegV9BXsFOhU7FTwVOhU9FTsVPhVuBT8VPhVAFW4FkQVBFZMFkQVCFUEVQxVEFUUVQxVGFUQVlQVHFZcFlQVIFUcV0wVJFdUF0wVKFUkVSxVMFU0VSxVOFUwVmQVPFZsFmQVQFU8VggVRFVIVggWIBVEVUxVUFYcFVRVWFVcVVRVYFVYVoQWiBaMFoQWkBaIFWRVaFVsVWRVcFVoVWhWMBV0VXRWMBY0FXhVdFYoFWhVfFYwFXxVgFYwFjAVgFY8FXxVhFWAVYhVhFV8VYhWQBWEVpQWmBacFpQWoBaYFYxWJBYYFYxVkFYkFiwVlFWYViwWOBWUVqQWqBasFqQVnFaoFaBVpFWoVaBVrFWkVrAVsFW0VrAWtBWwVbhVvFXAVcRV1BXIVcRVzFXUFdBWvBbAFdBV1Fa8FrgV2FXcVrgWxBXYVeBV5FXoVeBV7FXkVsgV8FX0VsgW1BXwVfhV/FYAVbAWBFYIVgxVqBWsFgxWEFWoFhRWzBbQFhRWGFbMFtgWHFYgVtgW6BYcViRWKFYsViRWMFYoVjRW5BbgFjRW3BbkFjhWPFZAVjhWRFY8VkhWTFZQVkhWVFZMVlhWXFZgVlhWZFZcVmhWbFZwVnRWeFZ8VoBWhFaIVoBWjFaEVpBWlFaYVpBWnFaUVqBWpFaoVqBWrFakVrBWtFa4VrxWwFbEVrxWyFbAVsxW0FbUVsxW2FbQVtxW4FbkVtxW6FbgVuxW8Fb0VuxW+FbwVvxXAFcEVvxXCFcAVwxXEFcUVwxXGFcQVxxXIFckVyhXLFcwVyhXNFcsVzhXPFdAVzhXRFc8V0hXTFdQV0hXVFdMV1hXXFdgV1hXZFdcV2hXbFdwV2hXdFdsV3hXfFeAV4RXiFeMV5BXlFeYV5BXnFeUV6BXpFeoV6BXrFekV7BXtFe4V7BXvFe0V8BXxFfIV8BXzFfEV9BX1FfYV9xX4FfkV+hX7FfwV+hX9FfsV/hX/FQAW/hUBFv8VAhYDFgQWAhYFFgMWBhYHFggWBhYJFgcWChYLFgwWDRYOFg8WEBYRFhIWEBYTFhEWFBYVFhYWFBYXFhUWGBYZFhoWGBYbFhkWHBYdFh4WHBYfFh0WIBYhFiIWIBYjFiEWJBYlFiYWJxYoFikWJxYqFigWKxYsFi0WKxYuFiwWLxYwFjEWLxYyFjAWMxY0FjUWMxY2FjQWNxY4FjkWNxY6FjgWOxY8Fj0WOxY+FjwWPxZAFkEWQhZDFkQWQhZFFkMWRhZHFkgWRhZJFkcWShZLFkwWShZNFksWThZPFlAWThZRFk8WUhZTFlQWVRZWFlcWWBZZFloWWBZbFlkWXBZdFl4WXBZfFl0WYBZhFmIWYBZjFmEWZBZlFmYWZBZnFmUWaBZpFmoWaBZrFmkWbBZtFm4WbxZwFnEWchZzFnQWchZ1FnMWdhZ3FngWdhZ5FncWehZ7FnwWehZ9FnsWfhagBX8WfhafBaAFgBa9BYEWgBa8Bb0FghaSBYMWghaUBZIFhBaaBYUWhBacBZoFvgWGFocWvgW7BYYWiBadBZ4FiBaJFp0F1wWKFtkF1wWLFooWwAWMFsEFwAWNFowWwwXEBcUFwwXGBcQFxwXIBckFxwXKBcgFywXMBc0FywXOBcwFzwXQBdEFzwXSBdAFjhbUBY8WjhbWBdQFkBbYBZEWkBbaBdgFkhbCBZMWkha/BcIFlBaYBZUWlBaWBZgFlhaXFt0FlhaYFpcW3gWZFtsF3gXfBZkWmhbhBdwFmhabFuEFnBbgBZ0WnBbiBeAF4wXkBeUF4wXmBeQF5wXoBekF5wXqBegF6wXsBe0F6wXuBewF7wXwBfEF7wXyBfAFnhafFqAWnhahFp8WohajFqQWohalFqMWphanFqgWphapFqcWqharFqwWqhatFqsWrhavFrAWrhaxFq8WshazFrQWsha1FrMWtha3FrgWtha5FrcWuha7FrwWuha9FrsWvha/FsAWvhbBFr8WwhbDFsQWwhbFFsMWxhbHFsgWxhbJFscWyhbLFswWyhbNFssWzhbPFtAWzhbRFs8W0hbTFtQW0hbVFtMW1hbXFtgW1hbZFtcW2hbbFtwW2hbdFtsWVgXeFt8WVgVZBd4WQQXgFuEWQQVDBeAWPQXiFuMWPQVABeIWIgXkFuUWIgUkBeQWHgXmFucWHgUhBeYWFgXoFukWFgUZBegWDgXqFusWDgURBeoWAgXsFu0WAgUFBewW8wXuFvUF8wX0Be4W9gX4Be8W9gX3BfgF9ATwFvEW9AT1BPAW7ATyFvMW7ATvBPIW+QX6BfsF+QX8BfoF/QX+Bf8F/QUABv4FAQYCBgMGAQYEBgIGBQYGBgcGBQYIBgYGCQYKBgsGCQYMBgoGDQYOBg8GDQYQBg4GEQYSBhMGEQYUBhIGFQYWBhcGFQYYBhYGGQYaBhsGGQYcBhoGHQYeBh8GHQYgBh4GIQYiBiMGIQYkBiIGJQYmBicGJQYoBiYGtQC2ALcAtQC4ALYAuQC6ALsAuQC8ALoAvQC+AL8AvQD0Fr4A9RbBAPYW9RbAAMEAwgDDAMQAwgDFAMMAxgDHAMgAxgDJAMcAygDLAMwAygDNAMsAzgDPANAAzgDRAM8A0gDTANQA0gD3FtMA+BbWAPkW+BbVANYA1wD6FtkA1wD7FvoW2wDcAN0A2wDeANwA3wDgAOEA3wDiAOAA4wDkAOUA4wD8FuQA5AD8FucA/RbjAOkA6ADjAP0W/BbjAOgA6AD9FuoA/BboAOsA6wDoAOwA/BbrAOYA/hb/FgAX/hYBF/8WAAEBAQIBAAEDAQEBBAEFAQYBBAEHAQUBCAECFwoBCAEDFwIXDAEEFw4BDAEFFwQXEAEuBhIBEAERAS4GEwEUARUBEwEGFxQBHwEHFyEBHwEIFwcXLQEuAS8BLQEwAS4BMQEyATMBMQE0ATIBNQE2ATcBNQE4ATYBOQEJFzsBOQE8AQkXChdCAQsXChc+AUIBQgE+AQwXDRcKF0EBDhcKFw0XPgEKFw4XDhcNFw8XPgEOFz8BPwEOFxAXPgE/AUABKAERFxIXKAEpAREXExcbARQXExcYARsBFRcWFxcXFRcYFxYXGRfvAPAAGRcaF+8AGxccFzAGGxcdFxwXHhcqAR8XHhcgFyoB+AAhFyIX+AAjFyEXJBclFyYXJBcnFyUXKBcpFyoXKBceASkXKxcsFy0XKxcuFywXLxfuADAXLxf6AO4A+QAxF/0A+QAyFzEXMQYzFzQXMQY1FzMXNhcJATcXNhcLAQkB9AA4F/IA9AA5FzgXOhf+AP8AOhc7F/4APBcNAT0XPBcPAQ0BPhc/F0AXPhcWAT8XQRdCF/MAQRdDF0IXRBc6AUUXRBc9AToBGQFGFxoBGQFHF0YXJQFIF0kXJQFKF0gXSxcpBtgASxfaACkGkQNMF00XkQOPA0wXigNOF4wDigNPF04XMgZQF1EXMgY1BlAXhQNSF4QDhQNTF1IXVBdJBlUXVBdWF0kGVxc4BjYGVxdYFzgGWRdLBkwGWRdaF0sGWxc7BlwXWxddFzsGXhdABj4GXhdfF0AGYBdEBkIGYBdhF0QGQQZiF2MXQQY/BmIXZBdlFzwGZBdmF2UXOQZnF2gXOQY3BmcXaRdqF0gGaRdrF2oXRgZsF20XRgZuF2wXPQZvF3AXPQZxF28XRQZyF3MXRQZDBnIXdBd1F3YXdBc6BnUXSgZ3F3gXSgZNBncXeRd6F3sXeRdHBnoXfBf7AH0XfBd+F/sAJwF/F4AXJwGBF38XHAGCF4MXHAGEF4IXhReGFysGhReHF4YXLAb3AIgXLAaJF/cAKwGKFywBKwGLF4oXHQGMF40XHQGOF4wXjxeQF/EAjxeRF5AXkheTF5QXkheVF5MXlheXF5gXlhcvBpcXJAGZF5oXJAEmAZkXmxecF50XmxeeF5wXnxegFyoGnxehF6AXohejF6QXohelF6MXphenF6gXphepF6cX9QCqF+0A9QD2AKoXqxesF60XqxeuF6wXrxewF7EXrxcXAbAXQwFEAUUBQwFGAUQBshf8ALMXshe0F/wAtRe2F7cXtRe4F7YXRwFIAUkBRwFKAUgBuRciASABuRcjASIBuhctBrsXuhe8Fy0GvRe+F78XvRfAF74XeQB6AHsAeQB8AHoAwRd/AH0AwRfCF38AwxeFAIYAwxfEF4UAxRc3ADMAxRfGFzcAxxePAMgXxxeOAI8AyReOAMcXyReSAI4Ayhc+AEIAyhfLFz4AzBc5AM0XzBfOFzkAzxcPAAwAzxfQFw8A0RfSF9MX0RfUF9IXkwCUAJUAkwCWAJQADgDVF9YXDgAQANUX1xfrAdgX1xfZF+sBQQDaF9sXQQBDANoX3BfdF94X3BcNAN0X3xeYAOAX3xeXAJgA4RecAJ0A4RfiF5wA4xeMAOQX4xflF4wAoQCiAKMAoQCkAKIA5hfnF+gX5hfpF+cX6hdFAE8A6hfrF0UATQDsF+0XTQBOAOwX7hfvF/AX7hc0AO8X8RfyF/MX8RdGAPIXNQD0F/UXNQA2APQXpQCmAKcApQCoAKYA9hf3F4oA9hf4F/cX+ReCAIMA+Rf6F4IAmwCfAJ4AmwCgAJ8AgQD7F/wXgQD9F/sXiAD+F/8XiACJAP4XRwAAGAEYRwACGAAYMQADGAQYMQAFGAMYBhgHGOoBBhgIGAcYiwAJGI0AiwAKGAkYmgALGJkAmgAMGAsYDRgOGOkBDRgPGA4YPQAQGBEYPQASGBAYExgUGBUYExgWGBQYkQAXGBgYkQAZGBcYGBgXGBoYGBgaGJAAGxgcGB0YGxgeGBwYhAAfGCAYhACHAB8YgAAhGCIYgAB+ACEYIxgkGAEAIxglGCQYrQAmGK8ArQAnGCYYEQAoGAsAEQApGCgYKhgSACsYKhgTABIAqQCqAKsAqQCsAKoALBiuAC0YLBiwAK4AsQCyALMAsQC0ALIATgYuGFAGXwZgBmEGXwZiBmAGYwZkBmUGYwZmBmQGZwZoBmkGZwZqBmgGawZsBm0GawZuBmwGbwZwBnEGbwZyBnAGcwZ0BnUGcwZ2BnQGdwZ4Bi8YiAaJBooGiAaLBokGjAaNBo4GjAaPBo0GkAaRBpIGkAaTBpEGlAaVBpYGlAaXBpUGeQZ6BjAYVQYxGFYGmAaZBpoGmAabBpkGnAadBp4GnAafBp0GoAahBqIGoAajBqEGpAalBqYGpAanBqUGqAapBqoGqAarBqkGewaABjIYVwYzGFgGrAatBq4GrAavBq0GsAaxBrIGsAazBrEGtAa1BrYGtAa3BrUGgQaCBoMGWQY0GDUYuAa5BroGuAa7BrkGvAa9Br4GvAa/Br0GwAbBBsIGwAbDBsEGxAbFBsYGxAbHBsUGyAbJBsoGyAbLBskGzAbNBs4GzAbPBs0G0AbRBtIG0AbTBtEG1AbVBtYG1AbXBtUG2AbZBtoG2AbbBtkG3AbdBt4G3AbfBt0G4AbhBuIG4AbjBuEG5AblBuYG5AbnBuUGNhiEBjcYWwZaBlwG6AbpBuoG6AbrBukGMQc4GDkYMQc0BzgY7AbtBjoY7AbvBu0GhQaGBocGXQY7GF4G9wb4BvkG9wb6BvgG/QY8GP8G/QY9GDwYBQcGBwcHBQcIBwYHOAc+GD8YOAc7Bz4YCQcKBwsHCQcMBwoHfgZ/BnwGUwZUBkAYDQcOBw8HDQcQBw4HAgdBGEIYAgcDB0EYEQcSBxMHEQcUBxIHFQcWBxcHFQcYBxYHUQZSBk8GGQcaBxsHGQccBxoHHQceBx8HHQcgBx4HIQciByMHIQckByIHJQcmBycHJQcoByYHKQcqBysHKQcsByoHLQcuBy8HLQcwBy4HQxh9BkQYRRjzBvQGRRhGGPMGRxgyBzMHRxhIGDIHSRgEB/4GSRhKGAQHSxg5BzoHSxhMGDkHTRjxBk4YTRjuBvEGNQdPGFAYNQdRGE8YUhhTGFQYUhhVGFMY8gZWGFcY8gbwBlYYWgdYGFkYWgddB1gYVgdaGFsYVgdZB1oYPAdcGF0YPAc/B1wYQAdeGF8YQAdDB14YYBgABwEHYBhhGAAHYhj7BvwGYhhjGPsGZBj1BvYGZBhlGPUGZhg2BzcHZhhnGDYHaBg9Bz4HaBhpGD0HahhBB0IHahhrGEEHbBhFB0YHbBhtGEUHbhhJB0oHbhhvGEkHXgdwGHEYXgdhB3AYYgdyGHMYYgdlB3IYSAd0GHUYSAdLB3QYRAd2GHcYRAdHB3YYTAd4GE4HTAdNB3gYeRhPB3oYeRhQB08HUQdSB1MHUQd7GFIHfBhUB30YfBhVB1QHfhhXB1gHfhh/GFcHgBhbB1wHgBiBGFsHghhfB2AHghiDGF8HhBhjB2QHhBiFGGMHhhiHGIgYhhiJGIcYdQd2B3cHdQd4B3YHihiLGIwYihiNGIsYjhh6B48YkBiRGGgHkhiTGJQYkhhsB5MYiweMB40HiweOB4wHlRiWGJcYlRiYGJYYlhiBB5kYmhibGJwYnRieGJ8YnRhnB54YjwegGKEYjweQB6AYohijGKQYohh7B6MYpRimGKcYpRioGKYYlgepGKoYlgeXB6kYgAerGIIHgAesGKsYkwetGK4YkwevGK0YsBixGLIYsBizGLEYtBi1GLYYtBi3GLUYwwe4GLkYwwe6GLgYuxi8GL0Yuxi+GLwYvxiDB8AYagfBGMIYwxhrB8QYwxhpB2sHxRjGGMcYxRjIGMYYnQeeB58HnQegB54HhAfJGIYHhAfKGMkYhgfJGMsYhgfLGMwYzRiFB84YbwfPGNAY0RhwB9IY0RjTGHAHoQeiB6MHoQekB6IH1BjVGNYY1BjXGNUY2BiHB9kYcQfaGNsY3BhyB90Y3BjeGHIHpQemB6cHpQeoB6YH3xjgGOEY3xjiGOAY4xjkGOUY4xjmGOQYqQeqB6sHqQesB6oHcwfnGOgY6Rh0B+oY6RjrGHQHrQevB+wYrQeuB68H7RjuGO8Y7RjwGO4Y8RjyGPMY8Rj0GPIYsAf1GLEHsAf2GPUYiAf3GIoHiAf4GPcY+RiJB/oY+xj8GP0Y+xj+GPwY/xgAGQEZ/xgCGQAZAxn/GAEZsgcEGbQHsgcFGQQZfwcGGX0HfwcHGQYZCBl+BwkZChkLGQwZChluBwsZDRm2B7MHDRm1B7YHDhkPGRAZDhkRGQ8Ztwe4B7kHtwe6B7gHEhkTGXwHEhkUGRMZFRkWGXkHZgcXGRgZGRkaGRsZGRltBxoZuwe8B70Huwe+B7wHHBkdGR4ZHBkfGR0ZIBkhGSIZIBkjGSEZJBklGSYZJBknGSUZKBkpGSoZKBkrGSkZLBktGS4ZLxkwGTEZMhkzGTQZMhk1GTMZNhk3GTgZNhk5GTcZOhk7GTwZOhk9GTsZPhk/GUAZQRlCGUMZQRlEGUIZRRlGGUcZRRlIGUYZSRlKGUsZSRlMGUoZTRlOGU8ZTRlQGU4ZURlSGVMZURlUGVIZVRlWGVcZVRlYGVYZWRlaGVsZXBldGV4ZXBlfGV0ZYBlhGWIZYBljGWEZZBllGWYZZBlnGWUZaBlpGWoZaBlrGWkZbBltGW4ZbBlvGW0ZcBlxGXIZcxl0GXUZdhl3GXgZdhl5GXcZehl7GXwZehl9GXsZfhl/GYAZfhmBGX8ZghmDGYQZghmFGYMZhhmHGYgZiRmKGYsZjBmNGY4ZjBmPGY0ZkBmRGZIZkBmTGZEZlBmVGZYZlBmXGZUZmBmZGZoZmBmbGZkZnBmdGZ4ZnxmgGaEZohmjGaQZohmlGaMZphmnGagZphmpGacZqhmrGawZqhmtGasZrhmvGbAZrhmxGa8ZshmzGbQZshm1GbMZthm3GbgZuRm6GbsZuRm8GboZvRm+Gb8ZvRnAGb4ZwRnCGcMZwRnEGcIZxRnGGccZxRnIGcYZyRnKGcsZyRnMGcoZzRnOGc8ZzRnQGc4Z0RnSGdMZ1BnVGdYZ1BnXGdUZ2BnZGdoZ2BnbGdkZ3BndGd4Z3BnfGd0Z4BnhGeIZ4BnjGeEZ5BnlGeYZ5xnoGekZ6hnrGewZ6hntGesZ7hnvGfAZ7hnxGe8Z8hnzGfQZ8hn1GfMZ9hn3GfgZ9hn5GfcZ+hn7GfwZ+hn9GfsZ/hn/GQAaARoCGgMaBBoFGgYaBBoHGgUaCBoJGgoaCBoLGgkaDBoNGg4aDBoPGg0aEBrAB8EHEBoRGsAHEhrIB8YHEhoTGsgHFBqRB5IHFBoVGpEHFhrEB8UHFhoXGsQHyQcYGhkayQfHBxgavwcaGhsavwfCBxoaHBodGh4aHBqaBx0aHxogGiEaHxrKByAa2QciGiMa2QfcByIaCQgkGiUaCQgMCCQaBQgmGicaBQgICCYaAQgoGikaAQgECCgaKhqUB5UHKhorGpQHLBqbB5wHLBotGpsHLhrLB8wHLhovGssHMBqZB5gHMBoxGpkHMhrOB88HMhozGs4HzQczGjIazQfQBzMaNBrSB9MHNBo1GtIHNhrWB9cHNho3GtYHOBraB9sHOBo5GtoH1Qc6Gjsa1QfYBzoa0Qc8Gj0a0QfUBzwa/Qc+Gj8a/QcACD4aQBpBGkIaQBpDGkEaRBpFGkYaRBpHGkUaSBpJGkoaSBpLGkkaTBpNGk4aTBpPGk0aUBpRGlIaUBpTGlEaVBpVGlYaVBpXGlUaWBpZGloaWBpbGlkaXBpdGl4aXBpfGl0aYBphGmIaYBpjGmEaZBplGmYaZBpnGmUaaBppGmoaaBprGmkabBptGm4abBpvGm0acBpxGnIacBpzGnEadBp1GnYadBp3GnUaeBp5GnoaeBp7GnkafBp9Gn4afBp/Gn0agBqBGoIagxqAGoIahBqCGoUahhqFGocagBqIGoEaiRqIGoAaihqJGoAaiRqLGogajBqNGokajhqPGosajhqQGo8agRqRGpIagRqSGpMahxqUGpUahxqWGpQalhqXGpQalhqYGpcamRqaGpsamRqcGpoanRqeGp8anxqeGqAanxqgGqEanhqiGqAanhqjGqIanRqkGp4apRqkGp0apRqmGqQapxqoGqkaqhqrGqwaqhqtGqsarhqvGrAarhqxGq8ashqzGrQashq1GrMathq3Grgathq5Grcauhq7Grwauhq9Grsavhq/GsAavhrBGr8awhrDGsQaxRrGGscaxRrIGsYayRrKGssayRrMGsoazRrOGs8azRrQGs4a0RrSGtMa0RrUGtIa1RrWGtca1RrYGtYa2RraGtsa3BrdGt4a3BrfGt0a4BrhGuIa4BrjGuEa5BrlGuYa5BrnGuUa6BrpGuoa6hrrGuwa7RrqGuwa7RrsGu4a7xrtGu4a7xruGvAa7hrsGvEa7BryGvEa7hrxGvMa8BruGvMa8BrzGvQa9Rr2Gvca+Br5Gvoa+Br7Gvka/Br9Gv4a/Br/Gv0aABsBGwIbAxsEGwUbAxsGGwQbBxsIGwkbBxsKGwgbCxsMGw0bCxsOGwwbDxsQGxEbDxsSGxAbExsUGxUbFhsXGxgbFhsZGxcbGhsbGxwbGhsdGxsbHhsfGyAbHhshGx8bIhsjGyQbIhslGyMbJhsnGygbJhspGycbKhsrGywbKhstGysbLhsvGzAbLhsxGy8bMhszGzQbMhs1GzMbNhs3GzgbORs6GzsbORs8GzobPRs+Gz8bPRtAGz4bQRtCG0MbRBtFG0YbRBtHG0UbSBtJG0obSBtLG0kbTBtNG04bTBtPG00bUBvfB90HUBtRG98HUhviB1MbUhvhB+IH5QfmB+cH5QfoB+YHVBvrB+kHVBtVG+sHVhvuB1cbVhvtB+4HWBtZG1obWBtbG1kbXBtdG14bXBtfG10bYBthG2IbYBtjG2EbZBtlG2YbZBtnG2UbaBtpG2obaBtrG2kbbBttG24bbBtvG20bcBtxG3IbcBtzG3EbdBt1G3YbdBt3G3UbeBt5G3obeBt7G3kbfBt9G34bfBt/G30bgBuBG4IbgBuDG4EbhBuFG4YbhBuHG4UbiBuJG4obiBuLG4kbjBuNG44bjBuPG40bkBuRG5IbkBuTG5EblBuVG5YblBuXG5UbmBuZG5obmBubG5kbnBudG54bnBufG50bnhudG6AbnhugG6EbohujG6QbohulG6MbphunG6gbphupG6cbqhurG6wbqhutG6sbrhuvG7AbrhuxG68bshuzG7Qbshu1G7Mb8QfyB/MH8Qf0B/IHthu3G7gbthu5G7cbuhu7G7wbuhu9G7sbvhu/G8AbvhvBG78bwhvDG8QbwhvFG8MbxhvHG8gbxhvJG8cbyhvLG8wbyhvNG8sbzhvPG9AbzhvRG88b0hvTG9Qb0hvVG9Mb1hvXG9gb1hvZG9cb2hvbG9wb2hvdG9sb3hvfG+Ab3hvhG98b4hvjG+Qb4hvlG+Mb5hvnG+gb5hvpG+cb6hvrG+wb6hvtG+sb7hvvG/Ab7hvxG+8b8hvzG/Qb8hv1G/Mb9hv3G/gb9hv5G/cb+hv7G/wb+hv9G/sb/hv/GwAc/hsBHP8bAhwDHAQcAhwFHAMcBhwHHAgcBhwJHAccChwLHAwcChwNHAscDhwPHBAcDhwRHA8cEhwTHBQcEhwVHBMcFhwXHBgcFhwZHBccGhwbHBwcGhwdHBscHhwfHCAcHhwhHB8cIhwjHCQcIhwlHCMcJhwnHCgcJhwpHCccKhwrHCwcKhwtHCscLhwvHDAcLhwxHC8cMhwzHDQcMhw1HDMcNhw3HDgcNhw5HDccOhw7HDwcOhw9HDscPhw/HEAcPhxBHD8cQhxDHEQcQhxFHEMcRhxHHEgcRhxJHEccShxLHEwcShxNHEscThxPHFAcThxRHE8cTxxRHFIcUxxOHFQcVRxOHFMcURxOHFUcVRxTHFYcURxVHFccVxxVHFgcURxXHFkcWhxbHFwcWhxdHFscXhxfHGAcXhxhHF8cYhxjHGQcYhxlHGMcZhxnHGgcZhxpHGccahxrHGwcahxtHGscbhxvHHAcbhxxHG8cchxzHHQcchx1HHMcdhx3HHgcdhx5HHccehx7HHwcehx9HHscfhx/HIAcfhyBHH8cghyDHIQcghyFHIMchhyHHIgchhyJHIccihyLHIwcihyNHIscjhyPHJAcjhyRHI8ckhyTHJQckhyVHJMclhyXHJgclhyZHJccmhybHJwcmhydHJscnhyfHKAcnhyhHJ8cohyjHKQcohylHKMcphynHKgcphypHKccqhyrHKwcqhytHKscrhyvHLAcrhyxHK8cshyzHLQcshy1HLMcthy3HLgcthy5HLccuhy7HLwcuhy9HLscvhy/HMAcvhzBHL8cwhzDHMQcwhzFHMMcxhzHHMgcxhzJHMccyhzLHMwcyhzNHMsczhzPHNAczhzRHM8c0hzTHNQc0hzVHNMc1hzXHNgc1hzZHNcc2hzbHNwc2hzdHNsc3hzfHOAc3hzhHN8c4hzjHOQc4hzlHOMc5hznHOgc5hzpHOcc5xzpHOoc6xzmHOwc7RzmHOsc6RzmHO0c7RzrHO4c6RztHO8c7xztHPAc6RzvHPEc8hzzHPQc8hz1HPMc9hz3HPgc9hz5HPcc+hz7HPwc+hz9HPsc/hz/HAAd/hwBHf8cAh0DHQQdAh0FHQMdBh0HHQgdBh0JHQcdCh0LHQwdCh0NHQsdDh0PHRAdDh0RHQ8dEh0THRQdEh0VHRMdFh0XHRgdFh0ZHRcdGh0bHRwdGh0dHRsdHh0fHSAdHh0hHR8dIh0eHSAdIh0gHSMdJB0eHSIdJB0lHR4dJR0hHR4dJR0mHSEdJx0oHSkdJx0qHSgdKx0nHSkdKx0pHSwdLR0nHSsdLR0uHScdLh0qHScdLh0vHSodMB0xHTIdMB0zHTEdNB01HTYdNB03HTUdOB05HTodOB07HTkdPB09HT4dPB0/HT0dQB1BHUIdQB1DHUEdRB1FHUYdRB1HHUUdSB1JHUodSB1LHUkdTB1NHU4dTB1PHU0d9Qf2B1Ad9Qf4B/YHUB32B/oH+Qf1B1AdUB36B/sH+QdQHfcH9wdQHfsH9wf7B/wHUR3+B/8HUR1SHf4HUx0CCAMIUx1UHQIIVR0GCAcIVR1WHQYIVx0KCAsIVx1YHQoIWR1aHVsdWR1cHVodXR1eHV8dXR1gHV4dYR1iHWMdYR1kHWIdZR1mHWcdaB1pHWodax1sHW0dbR1sHW4dbx1wHXEdbx1yHXAdcx10HXUdcx12HXQddx14HXkdeh17HXwdeh19HXsdfh1/HYAdfh2BHX8dgh2DHYQdgh2FHYMdhh2HHYgdhh2JHYcdih2LHYwdih2NHYsdjh2PHZAdjh2RHY8dkh2THZQdlR2WHZcdlR2YHZYdmR2aHZsdmR2cHZodnR2eHZ8dnR2gHZ4doR2iHaMdoR2kHaIdpR2mHacdpR2oHaYdqR2qHasdrB2tHa4drx2wHbEdrx2yHbAdsx20HbUdsx22HbQdtx24Hbkdtx26Hbgdux28Hb0dux2+Hbwdvx3AHcEdwh3DHcQdxR3GHccdxR3IHcYdyR3KHcsdyR3MHcodzR3OHc8dzR3QHc4d0R3SHdMd0R3UHdId1R3WHdcd2B3ZHdod2x3cHd0d2x3eHdwd3x3gHeEd3x3iHeAd4x3kHeUd4x3mHeQd5x3oHekd5x3qHegd6x3sHe0d6x3uHewd7x3wHfEd8h3zHfQd8h31HfMd9h33Hfgd9h35Hfcd+h37Hfwd+h39Hfsd/h3/HQAe/h0BHv8dAh4DHgQeAh4FHgMeBh4HHggeBh4JHgceCh4LHgweDR4OHg8eDR4QHg4eER4SHhMeER4UHhIeFR4WHhceFR4YHhYeGR4aHhseGR4cHhoeHR4eHh8eIB4hHiIeIx4kHiUeIx4mHiQeJx4oHikeJx4qHigeKx4sHi0eKx4uHiweLx4wHjEeLx4yHjAeMx40HjUeMx42HjQeNx44HjkeOh47HjwePR4+Hj8ePx4+HkAeQR5CHkMeQR5EHkIeRR5GHkceRR5IHkYeSR5KHkseSR5MHkoeTR5OHk8eTR5QHk4eUR5SHlMeUR5UHlIeVR5WHlceVR5YHlYeWR5aHlseWR5cHloeXR5eHl8eXR5gHl4eYR5iHmMeYR5kHmIeZR5mHmceZR5oHmYeaR5qHmseaR5sHmoebR5uHm8ebR5wHm4ecR5yHnMecR50HnIedR52HncedR54HnYeeR56HnseeR58HnoefR5+Hn8efR6AHn4egR6CHoMegR6EHoIehR6GHocehR6IHoYeiR6KHoseiR6MHooejR6OHo8ejR6QHo4ekR6SHpMekR6UHpIelR6WHpcelR6YHpYemR6aHpsemR6cHpoenR6eHp8enR6gHp4eoR6iHqMeoR6kHqIepR6mHqcepR6oHqYeqR6qHqseqR6sHqoerR6uHq8erR6wHq4esR6yHrMesR60HrIetR62HrcetR64HrYeuR66HrseuR68HroevR6+Hr8evR7AHr4ewR7CHsMewR7EHsIexR7GHscexR7IHsYeyR7KHsseyR7MHsoezR7OHs8ezR7QHs4e0R7SHtMe0R7UHtIe1R7WHtce1R7YHtYe2R7aHtse2R7cHtoe3R7eHt8e3R7gHt4e4R7iHuMe4R7kHuIe5R7mHuce5R7oHuYe6R7qHuse6R7sHuoe7R7uHu8e7R7wHu4e8R7yHvMe8R70HvIe9R72Hvce9R74HvYe+R76Hvse+R78Hvoe/R7+Hv8e/R4AH/4eAR8CHwMfAR8EHwIfBR8GHwcfBR8IHwYfCR8KHwsfCR8MHwofDR8OHw8fDR8QHw4fER8SHxMfER8UHxIfFR8WHxcfFR8YHxYfGR8aHxsfGR8cHxofHR8eHx8fHR8gHx4fIR8iHyMfIR8kHyIfIh8kHyUfJh8hHycfKB8hHyYfJB8hHygfKB8mHykfJB8oHyofKh8oHysfJB8qHywfLR8uHy8fLR8wHy4fMR8yHzMfMR80HzIfNR82HzcfNR84HzYfOR86HzsfOR88HzofPR8+Hz8fPR9AHz4fQR9CH0MfQR9EH0IfRR9GH0cfRR9IH0YfSR9KH0sfSR9MH0ofTR9OH08fTR9QH04fUR9SH1MfUR9UH1IfVR9WH1cfVR9YH1YfWR9aH1sfWR9cH1ofXR9eH18fXR9gH14fYR9dH18fYR9fH2IfYx9kH2UfYx9mH2QfZh9nH2QfZh9oH2cfaR9qH2sfaR9sH2ofbR9uH28fbR9wH24fcR9yH3MfcR90H3IfdR92H3cfdR94H3YfeR96H3sfeR98H3offR9+H38ffR+AH34fgR+CH4MfgR+EH4IfhR+GH4cfhR+IH4YfiR+KH4sfiR+MH4ofjR+OH48fjR+QH44fkR+SH5MfkR+UH5IflR+WH5cflR+YH5YfmR+aH5sfmR+cH5ofnR+eH58fnR+gH54foR+iH6MfoR+kH6IfpR+mH6cfpR+oH6YfqR+qH6sfqR+sH6ofrR+uH68frR+wH64fsR+yH7MfsR+0H7IftR+2H7cftR+4H7YfuR+6H7sfuR+8H7ofvR++H78fvR/AH74fwR/CH8MfwR/EH8IfxR/GH8cfxR/IH8YfyR/KH8sfyR/MH8ofzR/OH88fzR/QH84f0R/SH9Mf0R/UH9If1R/WH9cf1R/YH9Yf2R/aH9sf2R/cH9of3R/eH98f3R/gH94f4R/iH+Mf4R/kH+If5R/mH+cf5R/oH+Yf6R/lH+cf6R/nH+of6x/sH+0f6x/uH+wf7x/rH+0f7x/tH/Af8R/uH+sf8R/yH+4f8x/0H/Uf8x/2H/Qf9x/4H/kf9x/6H/gf+x/8H/0f+x/+H/wf/x8AIAEg/x8CIAAgAyAEIAUgAyAGIAQgByAIIAkgByAKIAggCyAMIA0gCyAOIAwgDyAQIBEgESAQIBIgEyAUIBUgEyAWIBQgFyAYIBkgFyAaIBggGyAcIB0gHSAcIB4gDQgOCB8gHggfCCAIHgghCB8IIggjCCQIIgglCCMIJggnCCgIJggpCCcIKggrCCwIKggtCCsILggvCDAILggxCC8IMggzCDQIMgg1CDMINgg3CCAgRwhICEkIRwhKCEgISwhMCE0ISwhOCEwITwhQCFEITwhSCFAIUwhUCFUIUwhWCFQIOAg5CDoIFAgPCCEgVwhYCFkIVwhaCFgIWwhcCF0IWwheCFwIXwhgCGEIXwhiCGAIYwhkCGUIYwhmCGQIZwhoCGkIZwhqCGgIIiA/CEAIFggVCBcIawhsCG0IawhuCGwIbwhwCHEIbwhyCHAIcwh0CHUIcwh2CHQIIyBBCEIIGAgkIBkIdwh4CHkIdwh6CHgIewh8CH0Iewh+CHwIfwiACIEIfwiCCIAIgwiECIUIgwiGCIQIhwiICIkIhwiKCIgIiwiMCI0IiwiOCIwIjwiQCJEIjwiSCJAIkwiUCJUIkwiWCJQIlwiYCJkIlwiaCJgImwicCJ0ImwieCJwInwigCKEInwiiCKAIowikCKUIowimCKQIJSBDCEQIGggmIBsIpwioCKkIpwiqCKgIqwisCK0IqwiuCKwIsQiyCLMIsQi0CLIIJyBFCEYIHAgoIB0Iugi7CLwIugi9CLsIwwjECMUIwwjGCMQIyQjKCMsIyQjMCMoIwAjBCCkgwAjCCMEIzQjOCM8IzQjQCM4IPQg+CCogEggTCCsg0QjSCNMI0QjUCNIILCDHCC0gLCDICMcI1QjWCNcI1QjYCNYI2QjaCNsI2QjcCNoIEAgRCC4g3QjeCN8I3QjgCN4I4QjiCOMI4QjkCOII5QjmCOcI5QjoCOYI6QjqCOsI6QjsCOoI7QjuCO8I7QjwCO4I8QjyCPMI8Qj0CPIIOwg8CC8g9Qj2CPcI9Qj4CPYI+Qj6CPsI+Qj8CPoI/Qj+CP8I/QgACf4IAQkCCQMJAQkECQIJMCC1CLYIMCAxILUIrwgyILAIrwgzIDIgNCC+CL8INCA1IL4Itwg2ILkItwi4CDYgMwk3IDggMwk2CTcgLwk5IDogLwkyCTkgFQk7IDwgFQkYCTsgGQk9ID4gGQkcCT0gBQkGCQcJBQkICQYJCQkKCQsJCQkMCQoJDQkOCQ8JDQkQCQ4JEQkSCRMJEQkUCRIJPyAWCRcJPyBAIBYJQSAaCRsJQSBCIBoJQyAeCR8JQyBEIB4JRSAiCSMJRSBGICIJNwlHIEggNwk6CUcgOwlJIEogOwk+CUkgIQlLIEwgIQkkCUsgHQlNIE4gHQkgCU0gJQlPICcJJQlQIE8gJgkoCVEgJgkpCSgJKgkrCSwJKglSICsJUyAtCVQgUyAuCS0JVSAwCTEJVSBWIDAJVyA0CTUJVyBYIDQJWSA4CTkJWSBaIDgJWyA8CT0JWyBcIDwJXSBeIF8gXSBgIF4gXiBgIGEgYiBdIGMgZCBdIGIgYCBdIGQgZCBiIGUgYCBkIGYgZiBkIGcgYCBmIGggaSBqIGsgaSBsIGogbSBuIG8gbSBwIG4gcSByIHMgcSB0IHIgdSB2IHcgdSB4IHYgeSB6IHsgeSB8IHogfSB+IH8gfSCAIH4ggSCCIIMggSCEIIIghSCGIIcghSCIIIYgiSCKIIsgiSCMIIogjSCOII8gjSCQII4gkSCSIJMgkSCUIJIglSCWIJcgmCCZIJogmCCbIJkgnCCdIJ4gnCCfIJ0goCChIKIgoCCjIKEgpCClIKYgpCCnIKUgqCCpIKogqCCrIKkgrCCtIK4grCCvIK0gsCCxILIgsyC0ILUgsyC2ILQgtyC4ILkgtyC6ILgguyC8IL0guyC+ILwgvyDAIMEgvyDCIMAgwyDEIMUgxiDHIMggySDKIMsgySDMIMogzSDOIM8gzSDQIM4g0SDSINMg0SDUINIg1SDWINcg1SDYINYg2SDaINsg2SDcINog3SDeIN8g4CDhIOIg4yDkIOUg4yDmIOQg5yDoIOkg5yDqIOgg6yDsIO0g6yDuIOwg7yDwIPEg8iDzIPQg9SD2IPcg9SD4IPYg+SD6IPsg+SD8IPog/SD+IP8g/SAAIf4gASECIQMhASEEIQIhBSEGIQchBSEIIQYhCSEKIQshCSEMIQohDSEOIQ8hDSEQIQ4hESESIRMhESEUIRIhFSEWIRchFSEYIRYhGSEaIRshGSEcIRohHSEeIR8hHSEgIR4hISEiISMhISEkISIhJSEmISchKCEpISohKyEsIS0hKyEuISwhLyEwITEhLyEyITAhMyE0ITUhMyE2ITQhNyE4ITkhOiE7ITwhPSE+IT8hPSFAIT4hQSFCIUMhQSFEIUIhRSFGIUchRSFIIUYhSSFKIUshSSFMIUohTSFOIU8hTSFQIU4hUSFSIVMhVCFVIVYhVyFYIVkhVyFaIVghWyFcIV0hWyFeIVwhXyFgIWEhXyFiIWAhYyFkIWUhYyFmIWQhZyFoIWkhaiFrIWwhaiFtIWshbiFvIXAhbiFxIW8hciFzIXQhciF1IXMhdiF3IXghdiF5IXcheiF7IXwheiF9IXshfiF/IYAhfiGBIX8hgiGDIYQhhSGGIYchhSGIIYYhiSGKIYshiSGMIYohjSGOIY8hjSGQIY4hkSGSIZMhkSGUIZIhlSGWIZchlSGYIZYhmSGaIZshmSGcIZohnSGeIZ8hnSGgIZ4hoSGiIaMhoSGkIaIhpSGmIachpSGoIaYhqSGqIashqSGsIaohrSGuIa8hrSGwIa4hsSGyIbMhsSG0IbIhtSG2IbchtSG4IbYhuSG6IbshuSG8IbohvSG+Ib8hvSHAIb4hwSHCIcMhwSHEIcIhxSHGIcchxSHIIcYhySHKIcshySHMIcohzSHOIc8hzSHQIc4h0SHSIdMh0SHUIdIh1SHWIdch1SHYIdYh2SHaIdsh2SHcIdoh3SHeId8h3SHgId4h4SHiIeMh4SHkIeIh5SHmIech5SHoIeYh6SHqIesh6SHsIeoh7SHuIe8h7SHwIe4h8SHyIfMh8SH0IfIh9SH2Ifch9SH4IfYh+SH6Ifsh+SH8Ifoh/SH+If8h/SEAIv4hASICIgMiASIEIgIiPwlACUEJPwlCCUAJQAlCCUMJRAk/CUUJRgk/CUQJQgk/CUYJRglECUcJQglGCUgJSAlGCUkJQglICUoJBSJMCQYiBSJOCUwJUAlRCVIJUAlTCVEJByIIIgkiByJVCQgiXAldCV4JXAlfCV0JVglXCVgJVgkKIlcJYAlhCWIJYAljCWEJZAkLImYJZAllCQsibAltCW4JbAlvCW0JDCJnCQ0iDCIOImcJcAlxCXIJcAlzCXEJDyJ1CRAiDyJ3CXUJeQl6CXsJeQl8CXoJESJ+CRIiESKACX4JgQmCCYMJgQmECYIJEyKGCRQiEyKICYYJjAmNCY4JjAmPCY0JjQmPCZAJkQmMCZIJkwmMCZEJjwmMCZMJkwmRCZQJjwmTCZUJlQmTCZYJjwmVCZcJmAmZCZoJmAmbCZkJFSKKCRYiFSIXIooJnAmdCZ4JnAmfCZ0JoAmhCaIJoAmjCaEJGCIZIhoiGCKnCRkisAkbIrEJsAkcIhsiGyIcIqsJsgmwCbMJrgmwCbIJHCKwCa4JrgmyCbQJHCKuCawJrAmuCa8JHCKsCa0JtQm2CbcJtQm4CbYJHSKpCR4iHSKlCakJuQm6CbsJuQm8CboJ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"),
-"lods": [0.15730709, PackedByteArray("6AEBAAIAAgABAAMAAgADAAQABQDoAQIACQAEAAoAAADoAQUABgAAAAUABQAHAAgA6QEMAAEA6QENAAwACwANAOkBDgAPAAwADgAQAA8AEQASAAsAEQATABIAFAAVABYAFAAXABUAGAAZABoAGwAaABwAGwAdABoAHgAdABsAHgAfAB0AHwAgACEAIgAgAB8AIgAjACAAJAAlACYAJwAmACgAJAApACUAKgApACQAKwAqACQALAAtAC4ALwAuADAAMQAtACwAMgAxACwAMQDqAS0AMQAzAOoBMQA0ADMAMwA2ADcANQA2ADMAOAA5ADoAOAA8ADkAPwA4AEAAOgA5AOsBOgDrATsAPQDrATkAPQCuAOsBPQA5AD4ArQCwAK4ArQCuAK8AQQA+AEIAQQBDAD4ARABFAEYARwBGAEUARABIAEUASQBIAEQASgBIAEkASwBKAEwATQBOAEUATQBFAE8AUABRAFIAUABTAFEAVABVAFYAVABXAFUAWABZAFoAWABbAFkAWABaAFwAWABcAF0AXgDsAWAAYADsAWEAYQDsAV8AYgBjAGQAYgBlAGMAZgBnAGgAZgBpAGcAagBrAGwAbQBuAG8AbQBwAG4AcQByAHMAcQB0AHIAdQB2AHcAdQB4AHYA7QHuAe8B8AHxAe4B8gHvAfMB9AHzAfUB9gH3AfEB+AH5AfcB+gH7AfkB/AH9AfsB/gH/AQAC/gEBAv8BAgIDAgQCAgIFAgMCBgIHAggCBgIJAgcCCgILAgwCCgINAgsCDgIPAhACDgIRAg8CEgITAhQCEgIVAhMCFgIXAhgCFwIZAhoCGwIWAhwCHQIbAh4CGQIfAiACHwIhAiICIQIjAiQCIwIlAiYCJwIoAikCJwIqAigCKwIsAi0CKwIuAiwCLwIwAjECLwIyAjACMwI0AjUCMwI2AjQCNwI4AjkCNwI6AjgCOwI8Aj0COwI+AjwCPwJAAkECPwJCAkACQwJEAkUCQwJGAkQCRwJIAkkCRwJKAkgCSwJMAk0CSwJOAkwCTwJQAlECTwJSAlACUwJUAlUCUwJWAlQCVwJYAlkCVwJaAlgCWwJcAl0CWwJeAlwCXwJgAmECXwJiAmACYwJkAmUCYwJmAmQCZwJoAmkCZwJqAmgCawJsAm0CawJuAmwCbwJwAnECbwJyAnACcwJ0AnUCcwJ2AnQCdwJ4AnkCdwJ6AngCewJ8An0CewJ+AnwCfwKAAoECfwKCAoACgwKEAoUCgwKGAoQChwKIAokChwKKAogCiwKMAo0CjgKPAowCiwKQAowCkQKQAosCkQKSApACkwKUAo0CkwKVApQClgKXApgClgKZApcCmgKbApwCmgKdApsCngKfAqACngKhAp8CogKjAqQCowKlAqQCpgKjAqICpgKnAqMCpwKoAqkCqgKrAqwCqgKtAqsCqgKuAq0CqgKvAq4CsAKxArICsAKzArECtAK1ArYCtAK3ArUCuAK5AroCuAK7ArkCvAK9Ar4CvAK/Ar0CwALBAsICwALDAsECxALFAsYCxALHAsUCyALJAsoCyALLAskCzALNAs4CzALPAs0C0ALRAtIC0ALTAtEC0gLUAtUC0gLVAtYC1wLYAtkC1wLaAtgC2wLcAt0C2wLeAtwC3wLgAuEC3wLiAuAC4wLkAuUC4wLmAuQC5wLoAukC5wLqAugC6wLsAu0C7QLsAu4C7QLuAu8C8ALxAvIC8ALyAvMC8ALzAvQC9QL2AvcC9QL4AvYC+QL6AvsC+QL8AvoC/QL+Av8C/QIAA/4CAQMCAwMDAQMEAwIDBQMGAwcDCAMFAwcDCAMHAwkDCgMIAwkDCwMIAwoDDAMNAwYDDAMOAw0DDwMQAw4DEQMSAxADEwMUAxIDFQMWAxcDFQMYAxYDGQMaAxsDGQMcAxoDHQMeAx8DHgMgAx8DHgMhAyADIgMdAyMDJAMiAyMDIQMlAyADIAMlAyYDIQMnAyUDJwMoAyUDJQMoAykDJwMqAygDKgMrAygDKAMrAywDKgMtAysDLQMuAysDLQMvAy4DMAMxAzIDMwM0AzEDMAM1AzEDNgM1AzADNgM3AzUDOAM5AzoDOgM5AzsDOQM8AzsDOAM9AzkDPQM+Az8DQANBA0IDQgNBA0MDQANEA0EDQANFA0QDQQNGA0cDSANJA0oDSANLA0kDTANNA04DTANPA00DUANRA1IDUANTA1EDVANVA1YDVANXA1UDWANZA1oDWwNZA1gDWwNcA1kDXQNeA18DXwNeA2ADXQNhA14DYgNjA2QDYgNlA2MDZgNnA2gDZgNpA2cDagNrA2wDbQNqA24DbQNuA28DbwNuA3ADbgNxA3ADcgNzA3QDcgN1A3MDdgN3A3gDdgN5A3cDegN7A3wDegN9A3sDfgN/A4ADfgOBA38DggODA4QDhQOCA4QDhgOHA4gDhgOJA4cDigOLA4wDigONA4sDjgOPA5ADkQOPA44DkgOTA5QDkgOVA5MDlgOXA5gDmQOXA5YDmQOaA5cDmgObA5cDlwObA5wDmgOdA5sDngOfA6ADngOhA58DogOjA6QDogOlA6MDpgOnA6gDpgOpA6cDqgOrA6wDqgOtA6sDrgOvA7ADsQOyA68DswOwA7QDtQO0A7YDtwO4A7IDuQO6A7gDuwO8A7oDvQO+A7wDvwPAA8EDvwPCA8ADwwPEA8UDwwPGA8QDxwPIA8kDxwPKA8gDywPMA80DywPOA8wDzwPQA9EDzwPSA9AD0wPUA9UD0wPWA9QD1wPYA9kD2APaA9sD3APXA90D3gPcA98D2gPgA+ED4APiA+MD4gPkA+UD5APmA+cD6APpA+oD6APrA+kD7APtA+4D7APvA+0D8APxA/ID8APzA/ED9AP1A/YD9AP3A/UD+AP5A/oD+AP7A/kD/AP9A/4D/AP/A/0DAAQBBAIEAAQDBAEEBAQFBAYEBAQHBAUECAQJBAoECAQLBAkEDAQNBA4EDAQPBA0EEAQRBBIEEAQTBBEEFAQVBBYEFAQXBBUEGAQZBBoEGAQbBBkEHAQdBB4EHAQfBB0EIAQhBCIEIAQjBCEEJAQlBCYEJAQnBCUEKAQpBCoEKAQrBCkELAQtBC4ELAQvBC0EMAQxBDIEMAQzBDEENAQ1BDYENAQ3BDUEOAQ5BDoEOAQ7BDkEPAQ9BD4EPAQ/BD0EQARBBEIEQARDBEEERARFBEYERARHBEUESARJBEoESARLBEkETARNBE4ETARPBE0EUARPBEwEUARRBE8EUgRTBE4EUgRUBFMEVQRWBFcEVQRYBFYEWQRaBFsEWQRcBFoEXQReBF8EXQRgBF4EXwReBGEEXwRhBGIEXQRjBGQEXQRkBGUEZgRnBGgEZgRpBGcEagRrBGwEagRtBGsEbgRtBGoEbgRvBG0EcARxBHIEcARzBHEEdAR1BHYEdAR3BHUEeAR5BHoEeAR7BHkEfAR9BH4EfAR/BH0EgASBBIIEgASDBIEEhASFBIYEhASHBIUEiASJBIoEiASLBIkEjASNBI4EjASPBI0EkASRBJIEkASTBJEElASVBJYElASXBJUEmASZBJoEmASbBJkEnASdBJ4EnASfBJ0EoAShBKIEoASjBKEEpASjBKAEpASlBKMEpgSnBKgEpgSpBKcEqgSrBKwEqgStBKsErgSvBLAErgSxBK8EsgSzBLQEsgS1BLMEtgS3BLgEtgS5BLcEugS7BLwEugS9BLsEvgS/BMAEvgTBBL8EwgTDBMQEwgTFBMMExgTHBMgEyATHBMkEyATJBMoEywTMBM0EywTNBM4EywTOBM8E0ATRBNIE0ATTBNEE1ATVBNYE1ATXBNUE2ATZBNoE2ATbBNkE3ATdBN4E3ATfBN0E4AThBOIE4ATjBOEE4QTjBOQE5QTgBOYE5wTgBOUE4wTgBOcE5wTlBOgE4wTnBOkE6QTnBOoE4wTpBOsE7ATtBO4E7ATvBO0E8ATxBPIE8ATzBPEE9AT1BPYE9QT3BPYE9QT4BPcE+QT6BPsE+QT8BPoE/QT+BP8E/QQABf4EAQUCBQMFAgUEBQMFAgUFBQQFBgUHBQgFBgUJBQcFCgULBQwFCgUNBQsFDgUPBRAFDgURBQ8FEgUTBRQFEgUVBRMFFgUXBRgFFgUZBRcFGgUbBRwFGgUdBRsFHgUfBSAFHgUhBR8FIgUjBR8FIgUkBSMFJQUmBScFJQUoBSYFJgUoBSkFKgUlBSsFLAUlBSoFKAUlBSwFLAUqBS0FKAUsBS4FLgUsBS8FKAUuBTAFMQUyBTMFMQU0BTIFNQU2BTcFNQU4BTYFOQU6BTsFOQU8BToFPQU+BT8FPQVABT4FQQVCBT4FQQVDBUIFRAVFBUYFRwVFBUQFRQVHBUgFSAVHBUkFSAVJBUoFRQVIBUsFSwVIBUwFRQVLBU0FTgVPBVAFTgVRBU8FUgVTBVQFUgVVBVMFVgVXBVgFVgVZBVcFWgVbBVwFWgVdBVsFWwVdBV4FXwVaBWAFYQVaBV8FXQVaBWEFYQVfBWIFXQVhBWMFYwVhBWQFXQVjBWUFZgVnBWgFaQVmBWgFaQVoBWoFawVpBWoFbAVpBWsFbQVuBWcFbQVvBW4FcAVxBW8FcgVzBXEFdAV1BXMFdgV3BXgFdgV5BXcFegV7BXwFegV9BXsFfgV/BYAFfwWBBYAFfwWCBYEFgwV+BYQFhQWDBYQFggWGBYEFgQWGBYcFggWIBYYFiAWJBYYFhgWJBYoFiAWLBYkFiwWMBYkFiQWMBY0FiwWOBYwFjgWPBYwFjgWQBY8FkQWSBZMFkQWUBZIFlQWWBZcFlgWYBZcFmQWaBZsFmQWcBZoFmwWaBZ0FmwWdBZ4FnwWgBZ0FoQWiBaMFoQWkBaIFpQWmBacFpQWoBaYFqQWqBasFrAWqBakFrAWtBaoFrgWvBbAFrgWxBa8FsgWzBbQFsgW1BbMFtgW3BbgFuAW3BbkFtgW6BbcFuwW8Bb0FvgW7Bb8FvgW/BcAFwAW/BcEFvwXCBcEFwwXEBcUFwwXGBcQFxwXIBckFxwXKBcgFywXMBc0FywXOBcwFzwXQBdEFzwXSBdAF0wXUBdUF0wXWBdQF1wXYBdkF1wXaBdgF2wXcBd0F3gXcBdsF3gXfBdwF3wXgBdwF3AXgBeEF3wXiBeAF4wXkBeUF4wXmBeQF5wXoBekF5wXqBegF6wXsBe0F6wXuBewF7wXwBfEF7wXyBfAF8wX0BfUF9gX3BfgF+QX6BfsF+QX8BfoF/QX+Bf8F/QUABv4FAQYCBgMGAQYEBgIGBQYGBgcGBQYIBgYGCQYKBgsGCQYMBgoGDQYOBg8GDQYQBg4GEQYSBhMGEQYUBhIGFQYWBhcGFQYYBhYGGQYaBhsGGQYcBhoGHQYeBh8GHQYgBh4GIQYiBiMGIQYkBiIGJQYmBicGJQYoBiYGtQC2ALcAtQC4ALYAuQC6ALsAuQC8ALoAvQC+AL8AvQDAAL4AwADBAL4AwgDDAMQAwgDFAMMAxgDHAMgAxgDJAMcAygDLAMwAygDNAMsAzgDPANAAzgDRAM8A0gDTANQA0gDVANMA1QDWANMA1wDYANkA1wDaANgA2gApBtgA2wDcAN0A2wDeANwA3wDgAOEA3wDiAOAA4wDkAOUA4wDmAOQA5ADmAOcA5gDjAOgA6ADjAOkA6ADpAOoA5gDoAOsA6wDoAOwA7QDuAO8A7QDvAPAA9QD2AO0A9gD3AO0A9wDuAO0A+ADuAPcAKgbtAPAAKgbwACsGKwbwAPEAKgYrBvEA8gAqBvEA9AAqBvIA8gDxAPMA+QD6AO4A+QDuAPsALAb7APcALAb3APwA+QD7AC0GLQb8AP4A+QAtBv0A/QAtBv4A/QD+AP8AAAEBAQIBAAEDAQEBBAEFAQYBBAEHAQUBCAEJAQoBCAELAQkBDAENAQ4BDAEPAQ0BEAEuBhIBEAERAS4GEwEUARUBEwEWARQBFgEXAS8GFgEvBhkBGQEvBhoBLwYbARoBLwYYARsBMAYeARgBHAEeATAGHAEwBh0BHwEgASEBHwExBiABMQYiASABMQYjASIBJAEjATEGJQEkATEGJAEmAScBKAEpAScBJwEpASoBJwEqASsBKwEqASwBLQEuAS8BLQEwAS4BMQEyATMBMQE0ATIBNQE2ATcBNQE4ATYBOQE6ATsBOQE8AToBPAE9AToBPgE/AUABPgFBAT8BQQE+AUIBMgYzBjQGMgY1BjMGNgY3BjgGOQY3BjYGOgY7BjwGPQY6BjwGPgY/BkAGQQY/Bj4GQgZDBkQGRQZDBkIGRgZHBkgGRwZJBkgGSgZLBkwGSgZNBksGQwFEAUUBQwFGAUQBRwFIAUkBRwFKAUgBeQB6AHsAeQB8AHoAfQB+AH8AgAB+AH0AgQCAAIIAgQCCAIMAhACFAIYAhACHAIUAiACJAIYAiACGAIoAiwCKAIwAiwCMAI0AjgCPAJAAkQCOAJAAkQCSAI4AkwCUAJUAkwCWAJQAlwCYAJkAmgCXAJkAmwCcAJ0AmwCeAJwAmwCfAJ4AmwCgAJ8AoQCiAKMAoQCkAKIApQCmAKcApQCoAKYAqQCqAKsAqQCsAKoAsQCyALMAsQC0ALIATgZPBlAGUQZSBk8GUwZUBlIGVQZQBlYGVwZWBlgGWQZYBloGWwZaBlwGXQZcBl4GXwZgBmEGXwZiBmAGYwZkBmUGYwZmBmQGZwZoBmkGZwZqBmgGawZsBm0GawZuBmwGbwZwBnEGbwZyBnAGcwZ0BnUGcwZ2BnQGdwZ4BnkGeQZ6BnsGfAZ9BncGfgZ/BnwGewaABoEGgQaCBoMGgwaEBoUGhQaGBocGiAaJBooGiAaLBokGjAaNBo4GjAaPBo0GkAaRBpIGkAaTBpEGlAaVBpYGlAaXBpUGmAaZBpoGmAabBpkGnAadBp4GnAafBp0GoAahBqIGoAajBqEGpAalBqYGpAanBqUGqAapBqoGqAarBqkGrAatBq4GrAavBq0GsAaxBrIGsAazBrEGtAa1BrYGtAa3BrUGuAa5BroGuAa7BrkGvAa9Br4GvAa/Br0GwAbBBsIGwAbDBsEGxAbFBsYGxAbHBsUGyAbJBsoGyAbLBskGzAbNBs4GzAbPBs0G0AbRBtIG0AbTBtEG1AbVBtYG1AbXBtUG2AbZBtoG2AbbBtkG3AbdBt4G3AbfBt0G4AbhBuIG4AbjBuEG5AblBuYG5AbnBuUG6AbpBuoG6AbrBukG7AbtBu4G7AbvBu0G8AbuBvEG8gbwBvEG8gbxBvMG8gbzBvQG8Qb1BvYG9wb4BvkG9wb6BvgG+Ab6BvsG+Ab7BvwG/Qb+Bv8G/wYABwEH/QYCB/4GAgcDB/4G/gYDBwQHBQcGBwcHBQcIBwYHCQcKBwsHCQcMBwoHDQcOBw8HDQcQBw4HEQcSBxMHEQcUBxIHFQcWBxcHFQcYBxYHGQcaBxsHGQccBxoHHQceBx8HHQcgBx4HIQciByMHIQckByIHJQcmBycHJQcoByYHKQcqBysHKQcsByoHLQcuBy8HLQcwBy4HMQcyBzMHMQc0BzIHNQcxBzYHNQc2BzcHOAc5BzoHOAc7BzkHPAc9Bz4HPAc/Bz0HQAdBB0IHQAdDB0EHRAdFB0YHRAdHB0UHSAdJB0oHSAdLB0kHTAdNB04HTQdPB04HTQdQB08HUQdSB1MHUQdUB1IHUQdVB1QHVgdXB1gHVgdZB1cHWgdbB1wHWgddB1sHXgdfB2AHXgdhB18HYgdjB2QHYgdlB2MHZgdnB2gHaAdnB2kHaAdpB2oHagdpB2sHZgdsB2cHZgdtB2wHbgdtB2YHbwdrB3AHcQdwB3IHcwdyB3QHdQd2B3cHdQd4B3YHeQd6B3sHfAd5B3sHfQd5B3wHfQd+B3kHfwd+B30HfAd7B4AHgAd7B4EHgAeBB4IHggeDB4QHhAeFB4YHhgeHB4gHiAeJB4oHiweMB40HiweOB4wHjweQB5EHjweRB5IHkwePB5QHkweUB5UHlgeXB5gHmAeXB5kHlweaB5sHlwebB5wHnQeeB58HnQegB54HoQeiB6MHoQekB6IHpQemB6cHpQeoB6YHqQeqB6sHqQesB6oHrQeuB68HsAetB68HsAevB7EHsgezB7QHsge1B7MHtQe2B7MHtwe4B7kHtwe6B7gHuwe8B70Huwe+B7wHvwfAB8EHvwfCB8AHwwe/B8QHwwfEB8UHxgfHB8gHyQfHB8YHxwfKB8sHxwfLB8wHzQfOB88HzQfQB84H0QfSB9MH0QfUB9IH1QfWB9cH1QfYB9YH2QfaB9sH2QfcB9oH3QfeB98H4AfeB90H4QfiB+MH5AfhB+MH5QfmB+cH5QfoB+YH6QfqB+sH7AfqB+kH7QfuB+8H8AftB+8H8QfyB/MH8Qf0B/IH9Qf2B/cH9Qf4B/YH+Qf1B/cH9wf2B/oH9wf6B/sH9wf7B/wH/Qf+B/8H/QcACP4HAQgCCAMIAQgECAIIBQgGCAcIBQgICAYICQgKCAsICQgMCAoIDQgOCA8IEAgRCA4IEggTCBEIFAgPCBUIFggVCBcIGAgXCBkIGggZCBsIHAgbCB0IHggfCCAIHgghCB8IIggjCCQIIgglCCMIJggnCCgIJggpCCcIKggrCCwIKggtCCsILggvCDAILggxCC8IMggzCDQIMgg1CDMINgg3CDgIOAg5CDoIOwg8CDYIPQg+CDsIOgg/CEAIQAhBCEIIQghDCEQIRAhFCEYIRwhICEkIRwhKCEgISwhMCE0ISwhOCEwITwhQCFEITwhSCFAIUwhUCFUIUwhWCFQIVwhYCFkIVwhaCFgIWwhcCF0IWwheCFwIXwhgCGEIXwhiCGAIYwhkCGUIYwhmCGQIZwhoCGkIZwhqCGgIawhsCG0IawhuCGwIbwhwCHEIbwhyCHAIcwh0CHUIcwh2CHQIdwh4CHkIdwh6CHgIewh8CH0Iewh+CHwIfwiACIEIfwiCCIAIgwiECIUIgwiGCIQIhwiICIkIhwiKCIgIiwiMCI0IiwiOCIwIjwiQCJEIjwiSCJAIkwiUCJUIkwiWCJQIlwiYCJkIlwiaCJgImwicCJ0ImwieCJwInwigCKEInwiiCKAIowikCKUIowimCKQIpwioCKkIpwiqCKgIqwisCK0IqwiuCKwIrwirCK0IrwitCLAIsQiyCLMIsQi0CLIIsQizCLUIsQi1CLYItwi4CLYItwi2CLkIugi7CLwIugi9CLsIuwi9CL4Iuwi+CL8IwAjBCL4IwAjCCMEIwwjECMUIwwjGCMQIxgjHCMQIxgjICMcIyQjKCMsIyQjMCMoIzQjOCM8IzQjQCM4I0QjSCNMI0QjUCNII1QjWCNcI1QjYCNYI2QjaCNsI2QjcCNoI3QjeCN8I3QjgCN4I4QjiCOMI4QjkCOII5QjmCOcI5QjoCOYI6QjqCOsI6QjsCOoI7QjuCO8I7QjwCO4I8QjyCPMI8Qj0CPII9Qj2CPcI9Qj4CPYI+Qj6CPsI+Qj8CPoI/Qj+CP8I/QgACf4IAQkCCQMJAQkECQIJBQkGCQcJBQkICQYJCQkKCQsJCQkMCQoJDQkOCQ8JDQkQCQ4JEQkSCRMJEQkUCRIJFQkWCRcJFQkYCRYJGQkaCRsJGQkcCRoJHQkeCR8JHQkgCR4JIQkiCSMJIQkkCSIJJQkmCScJJgkoCScJJgkpCSgJKgkrCSwJKgktCSsJKgkuCS0JLwkwCTEJLwkyCTAJMwk0CTUJMwk2CTQJNwk4CTkJNwk6CTgJOwk8CT0JOwk+CTwJPwlACUEJPwlCCUAJQAlCCUMJRAk/CUUJRgk/CUQJQgk/CUYJRglECUcJQglGCUgJSAlGCUkJQglICUoJSwlMCU0JSwlOCUwJSwlNCU8JUAlRCVIJUAlTCVEJVAlVCVYJVglVCVcJVglXCVgJWQlUCVoJWQlaCVsJXAldCV4JXAlfCV0JYAlhCWIJYAljCWEJZAllCWYJZgllCWcJZglnCWgJaQlqCWgJaQloCWsJbAltCW4JbAlvCW0JcAlxCXIJcAlzCXEJdAl1CXYJdAl3CXUJeAl0CXYJeQl6CXsJeQl8CXoJfQl+CX8JfQmACX4JgQmCCYMJgQmECYIJhQmGCYcJhQmICYYJiQmGCYoJiQmKCYsJjAmNCY4JjAmPCY0JjQmPCZAJkQmMCZIJkwmMCZEJjwmMCZMJkwmRCZQJjwmTCZUJlQmTCZYJjwmVCZcJmAmZCZoJmAmbCZkJnAmdCZ4JnAmfCZ0JoAmhCaIJoAmjCaEJpAmlCaYJpAmnCaUJqAmlCakJqAmpCaoJqwmsCa0JqwmuCawJrAmuCa8JqwmwCa4JsAmrCbEJrgmwCbIJsgmwCbMJrgmyCbQJtQm2CbcJtQm4CbYJuQm6CbsJuQm8CboJvQm+Cb8JvQnACb4JwQnCCcMJwQnECcIJwgnECcUJxgnBCccJyAnBCcYJxAnBCcgJyAnGCckJxAnICcoJygnICcsJxAnKCcwJzQnOCc8JzQnQCc4J0QnQCc0J0gnPCdMJ0gnTCdQJ1AnTCdUJ1AnVCdYJ1gnVCdcJ2AnXCdkJ2gnZCdsJ3AndCd4J3AnfCd0J4AnhCeIJ4wngCeIJ5AngCeMJ5AnlCeAJ5gnlCeQJ4wniCecJ5wniCegJ4gnpCegJ5wnoCeoJ6gnoCesJ6gnrCewJ7AntCe4J7gnvCfAJ8AnxCfIJ8wn0CfUJ8wn2CfQJ9wn4CfkJ9wn5CfoJ+wn8Cf0J/Qn8Cf4J/wkACgEK/wkBCgIKAAoDCgQKAAoECgUKBgoHCggKBgoJCgcKCgoLCgwKCgoNCgsKDgoPChAKDgoRCg8KEgoOChAKEwoUChUKEwoWChQKFwoYChkKFwoaChgKGwocCh0KGwoeChwKGwofCh4KIAohCiIKIAojCiEKIwokCiUKJQokCiYKJwooCikKJwoqCigKKwosCi0KKwouCiwKLwowCjEKLwoyCjAKMwo0CjUKMwo2CjQKNwo4CjkKNwo6CjgKOwo8Cj0KOwo+CjwKPwpACkEKPwpBCkIKPwpCCkMKRAo/CkMKRQo/CkQKRApDCkYKRwpICkkKRwpKCkgKSwpMCk0KSwpOCkwKTwpQClEKTwpSClAKUwpUClUKUwpWClQKVwpYClkKVwpaClgKWwpcCl0KWwpeClwKXwpgCmEKXwpiCmAKYwpkCmUKYwpmCmQKZwpoCmkKZwpqCmgKawpsCm0KawpuCmwKbwpwCnEKbwpyCnAKcwp0CnUKcwp2CnQKdwp4CnkKdwp6CngKewp8Cn0Kewp+CnwKfwqACoEKfwqCCoAKgwqECoUKgwqGCoQKhwqICokKhwqKCogKiwqMCo0KiwqOCowKSwFMAU0BSwFOAUwBTwFQAVEBTwFSAVABUwFUAVUBVQFUAVYBUwFXAVQBWAFZAVoBWAFbAVkBXAFdAV4BXAFfAV0BYAFhAWIBYAFjAWEBZAFlAWYBZAFnAWUBaAFpAWoBagFpAWsBaAFsAWkBbQFuAW8BbwFuAXABbQFxAW4BcgFzAXQBcgF1AXMBdgF3AXgBdgF5AXcBegF7AXwBegF9AXsBfgF6AX8BgAF6AX4BfQF6AYABfQGAAYEBgQGAAYIBfQGBAYMBhAGPCoYBkAqEAYYBjwqNAYUBhwGEAZAKiAGHAZAKhwGJAYQBiQGKAYsBhAGRCo8KhAGSCpEKjAGPCpEKjAGNAY8KjgGMAYQBjgGPAYwBjAGQAY0BkQGQAYwBkgGRAYwBkgGMAZMBlAGVAYgBlAGWAZUBlwGYAZQBlwGZAZgBmQGaAZYBlgGaAYcBmQGbAZoBnAGdAZ4BnAGfAZ0BoAGhAaIBoAGjAaEBpAGlAaYBpAGnAaUBqAGpAaoBqAGrAakBrAGtAa4BrAGvAa0BsAGxAbIBsAGzAbEBsgGxAZMKsgGTCrQBsQG1AZMKsQG2AbUBtwG4AbYBlAq1AZUKlAqVCroBlAq6AbsBvAGUCrsBuQGUCrwBvQG+Ab8BvQHAAb4BvwG+AcEBwQG+AcIBwwHFAcEBwwHBAZYKwwGWCsQBxgHHAcgBxgHJAccBygHLAcwBygHNAcsBzgHPAdABzgHRAc8B0gHTAdQB1AHTAdUB0gHWAdMB2AGXCtcBlwrYAdkB2gGYCtwBmArdAdwBmAreAd0B3wHeAZgK2wHfAZgKmQqaCpsKmQqcCpoKnQqeCp8KoAqeCp0KoQqiCqMKpAqhCqMKpQqmCqcKqAqlCqcKqQqqCqsKqQqsCqoKrQquCq8KrQqwCq4K4AHhAeIB4AHjAeEB5AHlAeYB5AHnAeUB"), 0.28424123, PackedByteArray("AAABAAIAAgABAAMAAgADAAQABQAAAAIABgAAAAUABQAHAAgACQAEAAoACwAMAAEACwANAAwADgAPAAwADgAQAA8AEQASAAsAEQATABIAFAAVABYAFAAXABUAGAAZABoAGwAaABwAGwAdABoAHgAdABsAHgAfAB0AHwAgACEAIgAgAB8AIgAjACAAJAAlACYAJwAmACgAJAApACUAKgApACQAKwAqACQALAAtAC4ALwAuADAAMQAtACwAMgAxACwAMQAzAC0AMQA0ADMANQA2ADMAMwA2ADcAOAA5ADoAOgA5ADsAOAA8ADkAPQA7ADkAPQA5AD4APwA4AEAAQQA+AEIAQQBDAD4ARABFAEYARwBGAEUARABIAEUASQBIAEQASgBIAEkASwBKAEwATQBOAEUATQBFAE8AUABRAFIAUABTAFEAVABVAFYAVABXAFUAWABZAFoAWABbAFkAWABaAFwAWABcAF0AXgBfAGAAYABfAGEAYgBjAGQAYgBlAGMAZgBnAGgAZgBpAGcAagBrAGwAbQBuAG8AbQBwAG4AcQByAHMAcQB0AHIAdQB2AHcAdQB4AHYAtQC2ALcAtQC4ALYAuQC6ALsAuQC8ALoAvQC+AL8AvQDAAL4AwADBAL4AwgDDAMQAwgDFAMMAxgDHAMgAxgDJAMcAygDLAMwAygDNAMsAzgDPANAAzgDRAM8A0gDTANQA0gDVANMA1QDWANMA1wDYANkA1wDaANgA2wDcAN0A2wDeANwA3wDgAOEA3wDiAOAA4wDkAOUA4wDmAOQA5ADmAOcA5gDjAOgA6ADjAOkA6ADpAOoA5gDoAOsA6wDoAOwA7QDuAO8A7QDvAPAA7QDwAPEA8gDxAPMA8gDtAPEA9ADtAPIA9QD2AO0A9gD3AO0A9wDuAO0A+ADuAPcA+QD6AO4A+QDuAPsA+wD3APwA+QD7AP0A+wD8AP4A/QD7AP4A/QD+AP8AAAEBAQIBAAEDAQEBBAEFAQYBBAEHAQUBCAEJAQoBCAELAQkBDAENAQ4BDAEPAQ0BEAERARIBEwEUARUBEwEWARQBFgEXARgBFgEYARkBGQEYARoBGAEbARoBHAEYAR0BHAEeARgBHwEgASEBHwEiASABHwEjASIBJAEjAR8BJQEkAR8BJAEmAScBKAEpAScBJwEpASoBJwEqASsBKwEqASwBLQEuAS8BLQEwAS4BMQEyATMBMQE0ATIBNQE2ATcBNQE4ATYBOQE6ATsBOQE8AToBPAE9AToBPgE/AUABPgFBAT8BQQE+AUIBQwFEAUUBQwFGAUQBRwFIAUkBRwFKAUgBeQB6AHsAeQB8AHoAfQB+AH8AgAB+AH0AgQCAAIIAgQCCAIMAhACFAIYAhACHAIUAiACJAIYAiACGAIoAiwCKAIwAiwCMAI0AjgCPAJAAkQCOAJAAkQCSAI4AkwCUAJUAkwCWAJQAlwCYAJkAmgCXAJkAmwCcAJ0AmwCeAJwAmwCfAJ4AmwCgAJ8AoQCiAKMAoQCkAKIApQCmAKcApQCoAKYAqQCqAKsAqQCsAKoArQCuAK8ArQCwAK4AsQCyALMAsQC0ALIASwFMAU0BSwFOAUwBTwFQAVEBTwFSAVABUwFUAVUBVQFUAVYBUwFXAVQBWAFZAVoBWAFbAVkBXAFdAV4BXAFfAV0BYAFhAWIBYAFjAWEBZAFlAWYBZAFnAWUBaAFpAWoBagFpAWsBaAFsAWkBbQFuAW8BbwFuAXABbQFxAW4BcgFzAXQBcgF1AXMBdgF3AXgBdgF5AXcBegF7AXwBegF9AXsBfgF6AX8BgAF6AX4BfQF6AYABfQGAAYEBgQGAAYIBfQGBAYMBhAGFAYYBhwGEAYYBiAGHAYYBhwGJAYQBiQGKAYsBhAGMAYUBjAGNAYUBjgGMAYQBjgGPAYwBjAGQAY0BkQGQAYwBkgGRAYwBkgGMAZMBlAGVAYgBlAGWAZUBlwGYAZQBlwGZAZgBmQGaAZYBlgGaAYcBmQGbAZoBnAGdAZ4BnAGfAZ0BoAGhAaIBoAGjAaEBpAGlAaYBpAGnAaUBqAGpAaoBqAGrAakBrAGtAa4BrAGvAa0BsAGxAbIBsAGzAbEBsgGxAbQBsQG1AbQBsQG2AbUBtwG4AbYBuQG1AboBuQG6AbsBvAG5AbsBvQG+Ab8BvQHAAb4BvwG+AcEBwQG+AcIBwwHBAcQBwwHFAcEBxgHHAcgBxgHJAccBygHLAcwBygHNAcsBzgHPAdABzgHRAc8B0gHTAdQB1AHTAdUB0gHWAdMB1wHYAdkB2gHbAdwB2wHdAdwB2wHeAd0B3wHeAdsB4AHhAeIB4AHjAeEB5AHlAeYB5AHnAeUB"), 0.44526583, PackedByteArray("AAABAAIAAgABAAMAAgADAAQABQAAAAIABgAAAAUABQAHAAgACQAEAAoACwAMAAEACwANAAwADgAPAAwADgAQAA8AEQASAAsAEQATABIAFAAVABYAFAAXABUAGAAZABoAGwAaABwAGwAdABoAHgAdABsAHgAfAB0AHwAgACEAIgAgAB8AIgAjACAAJAAlACYAJwAmACgAJAApACUAKgApACQAKwAqACQALAAtAC4ALwAuADAAMQAtACwAMgAxACwAMQAzAC0AMQA0ADMANQA2ADMAMwA2ADcAOAA5ADoAOgA5ADsAOAA8ADkAPQA7ADkAPQA5AD4APwA4AEAAQQA+AEIAQQBDAD4ARABFAEYARwBGAEUARABIAEUASQBIAEQASgBIAEkASwBKAEwATQBOAEUATQBFAE8AUABRAFIAUABTAFEAVABVAFYAVABXAFUAWABZAFoAWABbAFkAWABaAFwAWABcAF0AXgBfAGAAYABfAGEAYgBjAGQAYgBlAGMAZgBnAGgAZgBpAGcAagBrAGwAbQBuAG8AbQBwAG4AcQByAHMAcQB0AHIAdQB2AHcAdQB4AHYAeQB6AHsAeQB8AHoAfQB+AH8AgAB+AH0AgQCAAIIAgQCCAIMAhACFAIYAhACHAIUAiACJAIYAiACGAIoAiwCKAIwAiwCMAI0AjgCPAJAAkQCOAJAAkQCSAI4AkwCUAJUAkwCWAJQAlwCYAJkAmgCXAJkAmwCcAJ0AmwCeAJwAmwCfAJ4AmwCgAJ8AoQCiAKMAoQCkAKIApQCmAKcApQCoAKYAqQCqAKsAqQCsAKoArQCuAK8ArQCwAK4AsQCyALMAsQC0ALIA")],
-"material": SubResource("StandardMaterial3D_4omi2"),
-"name": "Material",
-"primitive": 3,
-"skin_data": PackedByteArray("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AAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA/P8CAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA+v8EAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAoAAAAAAP3/AQAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA/P8CAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAPr/BAAAAAAACQAKAAAAAAD8/wIAAAAAAAkACgAAAAAA+P8GAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA+P8GAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD4/wYAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAAAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAKAAAAAAD9/wEAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAKAAAAAAD9/wEAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD8/wIAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAKAAAAAAD8/wIAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAPr/BAAAAAAACQAKAAAAAAD6/wQAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAPz/AgAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA+P8GAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAYABQAAAAAAEYHtfgAAAAAGAAUAAAAAAPOBC34AAAAABgAFAAAAAADjgRt+AAAAAAYABQAAAAAASJK2bQAAAAAGAAUAAAAAAD2CwX0AAAAABgAFAAAAAADjgRt+AAAAAAYABQAAAAAAeoCEfwAAAAAGAAUAAAAAAEiStm0AAAAABgAFAAAAAADPgS9+AAAAAAYABQAAAAAAeoCEfwAAAAAGAAUAAAAAAOmAFX8AAAAABgAFAAAAAAA9gsF9AAAAAAYABQAAAAAA44EbfgAAAAAGAAUAAAAAAOmAFX8AAAAABgAFAAAAAAB6gIR/AAAAAAYABQAAAAAA84ELfgAAAAAGAAUAAAAAABGB7X4AAAAABgAFAAAAAADpgBV/AAAAAAYABQAAAAAA84ELfgAAAAAGAAUAAAAAAM+BL34AAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAALAAAAAABP/68AAAAAAAwACwAAAAAA6f8VAAAAAAAMAAsAAAAAAPr/BAAAAAAADAAAAAAAAAD//wAAAAAAAAwACwAAAAAAe/6DAQAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAsAAAAAAPf/BwAAAAAADAAAAAAAAAD//wAAAAAAAAwACwAAAAAA6f8VAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwACwAAAAAA9/8HAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwACwAAAAAA5/8XAAAAAAAMAAsAAAAAAHv+gwEAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAALAAAAAAD7/wMAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAsAAAAAAPb/CAAAAAAADAALAAAAAADp/xUAAAAAAAwACwAAAAAAT/+vAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwACwAAAAAA+/8DAAAAAAAMAAsAAAAAAPv+AwEAAAAADAALAAAAAAD3/wcAAAAAAAwACwAAAAAABf75AQAAAAAMAAsAAAAAAPr/BAAAAAAADAALAAAAAAAi+9wEAAAAAAwACwAAAAAAT/+vAAAAAAAMAAsAAAAAAOPoGxcAAAAADAAAAAAAAAD//wAAAAAAAAwACwAAAAAAleVpGgAAAAAMAAsAAAAAAHv+gwEAAAAADAALAAAAAAAw7M4TAAAAAAwACwAAAAAA9v8IAAAAAAAMAAsAAAAAAAX++QEAAAAADAALAAAAAAD7/wMAAAAAAAwACwAAAAAA9P8KAAAAAAAMAAAAAAAAAP//AAAAAAAADAALAAAAAADn/xcAAAAAAAwACwAAAAAA9/8HAAAAAAAMAAsAAAAAAPf/BwAAAAAADAAAAAAAAAD//wAAAAAAAAwACwAAAAAA+/8DAAAAAAAMAAsAAAAAAOf/FwAAAAAADAALAAAAAAAw7M4TAAAAAAwAAAAAAAAA//8AAAAAAAAMAAsAAAAAALP6SwUAAAAADAALAAAAAABP/68AAAAAAAwACwAAAAAA9P8KAAAAAAAMAAsAAAAAAPb/CAAAAAAADAALAAAAAAAi+9wEAAAAAAwACwAAAAAA9/8HAAAAAAAMAAsAAAAAALP6SwUAAAAADAALAAAAAADn/xcAAAAAAAwACwAAAAAA+/4DAQAAAAAMAAsAAAAAAHv+gwEAAAAADAALAAAAAADj6BsXAAAAAAwACwAAAAAA+v8EAAAAAAAMAAsAAAAAAJXlaRoAAAAADAALAAAAAAAw7M4TAAAAAAsADAAAAAAAtaZJWQAAAAAMAAsAAAAAAJXlaRoAAAAACwAMAAAAAADBpz1YAAAAAAwACwAAAAAA9P8KAAAAAAALAAwAAAAAAMalOFoAAAAADAALAAAAAAAF/vkBAAAAAAsADAAAAAAAQqm8VgAAAAAMAAsAAAAAAJXlaRoAAAAACwAMAAAAAABqp5RYAAAAAAwACwAAAAAA4+gbFwAAAAALAAwAAAAAALWmSVkAAAAADAALAAAAAAD7/gMBAAAAAAsADAAAAAAAMafNWAAAAAAMAAsAAAAAALP6SwUAAAAACwAMAAAAAADfph9ZAAAAAAsADAAAAAAAQqm8VgAAAAALAAwAAAAAAEWruVQAAAAACwAMAAAAAADGpThaAAAAAAsADAAAAAAAMafNWAAAAAALAAwAAAAAAN+mH1kAAAAACwAMAAAAAABFq7lUAAAAAAsADAAAAAAAaqeUWAAAAAALAAwAAAAAALWmSVkAAAAACwAMAAAAAADBpz1YAAAAAAsADAAAAAAAMafNWAAAAAAMAAsAAAAAAOPoGxcAAAAACwAMAAAAAABFq7lUAAAAAAwACwAAAAAAIvvcBAAAAAALAAwAAAAAAGqnlFgAAAAADAALAAAAAAAF/vkBAAAAAAsADAAAAAAA36YfWQAAAAAMAAsAAAAAAPv+AwEAAAAACwAMAAAAAADGpThaAAAAAAwACwAAAAAAs/pLBQAAAAALAAwAAAAAAMGnPVgAAAAADAALAAAAAAAw7M4TAAAAAAsADAAAAAAAMafNWAAAAAAMAAsAAAAAACL73AQAAAAACwAMAAAAAABCqbxWAAAAAAwACwAAAAAA9P8KAAAAAAALAAwAAAAAAEWruVQAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgARAAAAAAD4/wYAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAEQAAAAAA/v8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASABEAAAAAAPj/BgAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAEQAAAAAA/v8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAEQAAAAAA/v8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASABEAAAAAAPj/BgAAAAAAEgAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD5/wUAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAPz/AgAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD5/wUAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAPH/DQAAAAAAEQASAAAAAAD+/wAAAAAAABEAAAAAAAAA//8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAADx/w0AAAAAABEAEgAAAAAAAPH+DgAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAADs/xIAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAADx/g4AAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAADx/g4AAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAADx/g4AAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA7f8RAAAAAAARABIAAAAAAADx/g4AAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAAAPH+DgAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAADx/w0AAAAAABEAEgAAAAAA+f8FAAAAAAARABIAAAAAAOz/EgAAAAAAEQASAAAAAACv/08AAAAAABEAEgAAAAAA7P8SAAAAAAARABIAAAAAAPn/BQAAAAAAEQASAAAAAAD+/wAAAAAAABEAAAAAAAAA//8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA/P8CAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA/P8CAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAPn/BQAAAAAAEQAAAAAAAAD//wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAPT/CgAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA/P8CAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAAr/9PAAAAAAARAAAAAAAAAP//AAAAAAAAEQASAAAAAAD5/wUAAAAAABEAEgAAAAAAY/+bAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD0/woAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAEgAAAAAAr/9PAAAAAAARABIAAAAAAPH/DQAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAAAAAAAAA//8AAAAAAAARABIAAAAAANL/LAAAAAAAEQAAAAAAAAD//wAAAAAAABEAEgAAAAAAY/+bAAAAAAARABIAAAAAAPz/AgAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD0/woAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAGP/mwAAAAAAEQASAAAAAACv/08AAAAAABEAEgAAAAAA0v8sAAAAAAARAAAAAAAAAP//AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA0v8sAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD8/wIAAAAAABEAEgAAAAAA+f8FAAAAAAARABIAAAAAAP7/AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAAUABMAAAAAANPUKysAAAAAFAATAAAAAADT1CsrAAAAABMAFAAAAAAAF73nQgAAAAATABQAAAAAAKvBUz4AAAAAEwAUAAAAAACrwVM+AAAAABMAFAAAAAAALtvQJAAAAAATABQAAAAAABja5iUAAAAAEwAUAAAAAAAY2uYlAAAAABMAFAAAAAAALtvQJAAAAAAGAAUAAAAAAEiStm0AAAAABgAFAAAAAADPgS9+AAAAAAYABQAAAAAAEYHtfgAAAAAGAAUAAAAAAD2CwX0AAAAABQAGAAcAAAAKn/RgAAAAAAUABgAHAAAALKDSXwAAAAAFAAYABwAAAKmfVWAAAAAABQAGAAcAAADfox9cAAAAAAUABgAHAAAALKDSXwAAAAAFAAYABwAAAJiYZmcAAAAABQAGAAcAAABCnrxhAAAAAAUABgAHAAAAqZ9VYAAAAAAFAAYABwAAAHSiil0AAAAABQAGAAcAAABCnrxhAAAAAAUABgAHAAAAmJhmZwAAAAAFAAYABwAAALigRl8AAAAABQAGAAcAAAC4oEZfAAAAAAUABgAHAAAAqZ9VYAAAAAAFAAYABwAAAEKevGEAAAAABQAGAAcAAAAKn/RgAAAAAAUABgAHAAAA36MfXAAAAAAFAAYABwAAAAqf9GAAAAAABQAGAAcAAAB0oopdAAAAAAUABgAHAAAAuKBGXwAAAAAFAAYABwAAACyg0l8AAAAABQAGAAcAAAB0oopdAAAAAAUABgAHAAAAmJhmZwAAAAAFAAYABwAAAN+jH1wAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD9/wEAAAAAAA0ADgAAAAAA/P8CAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD6/wQAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAPz/AgAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD6/wQAAAAAAA0ADgAAAAAA/P8CAAAAAAANAA4AAAAAAPj/BgAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA+P8GAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA+P8GAAAAAAANAA4AAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAA4AAAAAAP3/AQAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/P8CAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAPz/AgAAAAAADQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA+v8EAAAAAAANAA4AAAAAAPr/BAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD8/wIAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD9/wEAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAA4AAAAAAPj/BgAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAABgAFAAAAAAARge1+AAAAAAYABQAAAAAA44EbfgAAAAAGAAUAAAAAAPOBC34AAAAABgAFAAAAAABIkrZtAAAAAAYABQAAAAAAEYHtfgAAAAAGAAUAAAAAAM+BL34AAAAABgAFAAAAAAA9gsF9AAAAAAYABQAAAAAASJK2bQAAAAAGAAUAAAAAAD2CwX0AAAAABgAFAAAAAADjgRt+AAAAAAYABQAAAAAASJK2bQAAAAAGAAUAAAAAAHqAhH8AAAAABgAFAAAAAAA9gsF9AAAAAAYABQAAAAAA6YAVfwAAAAAGAAUAAAAAAHqAhH8AAAAABgAFAAAAAADPgS9+AAAAAAYABQAAAAAA44EbfgAAAAAGAAUAAAAAAHqAhH8AAAAABgAFAAAAAADpgBV/AAAAAAYABQAAAAAA84ELfgAAAAAGAAUAAAAAABGB7X4AAAAABgAFAAAAAADpgBV/AAAAAAYABQAAAAAAz4EvfgAAAAAGAAUAAAAAAPOBC34AAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAPAAAAAABP/68AAAAAABAADwAAAAAA+v8EAAAAAAAQAA8AAAAAAOn/FQAAAAAAEAAAAAAAAAD//wAAAAAAABAADwAAAAAAe/6DAQAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAADwAAAAAA9/8HAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAPAAAAAADn/xcAAAAAABAADwAAAAAA9/8HAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAADwAAAAAA+/8DAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAPAAAAAADp/xUAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAPAAAAAAD2/wgAAAAAABAADwAAAAAAT/+vAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAPAAAAAAD7/wMAAAAAABAAAAAAAAAA//8AAAAAAAAQAA8AAAAAAPf/BwAAAAAAEAAAAAAAAAD//wAAAAAAABAADwAAAAAA5/8XAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAPAAAAAAB7/oMBAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAPAAAAAAD3/wcAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAADwAAAAAA6f8VAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAADwAAAAAA+/8DAAAAAAAQAA8AAAAAAPv+AwEAAAAAEAAPAAAAAAAF/vkBAAAAABAADwAAAAAA9/8HAAAAAAAQAA8AAAAAAPr/BAAAAAAAEAAPAAAAAAAi+9wEAAAAABAADwAAAAAA4+gbFwAAAAAQAA8AAAAAAE//rwAAAAAAEAAAAAAAAAD//wAAAAAAABAADwAAAAAAleVpGgAAAAAQAA8AAAAAADDszhMAAAAAEAAPAAAAAAB7/oMBAAAAABAADwAAAAAA9v8IAAAAAAAQAA8AAAAAAAX++QEAAAAAEAAPAAAAAAD0/woAAAAAABAADwAAAAAA+/8DAAAAAAAQAA8AAAAAAOf/FwAAAAAAEAAPAAAAAAAw7M4TAAAAABAADwAAAAAAs/pLBQAAAAAQAAAAAAAAAP//AAAAAAAAEAAPAAAAAABP/68AAAAAABAADwAAAAAA9P8KAAAAAAAQAA8AAAAAACL73AQAAAAAEAAPAAAAAAD2/wgAAAAAABAADwAAAAAA9/8HAAAAAAAQAA8AAAAAALP6SwUAAAAAEAAPAAAAAAD7/gMBAAAAABAADwAAAAAA5/8XAAAAAAAQAA8AAAAAAHv+gwEAAAAAEAAPAAAAAADj6BsXAAAAABAADwAAAAAAleVpGgAAAAAQAA8AAAAAAPr/BAAAAAAAEAAPAAAAAAAw7M4TAAAAAA8AEAAAAAAAtaZJWQAAAAAPABAAAAAAAMGnPVgAAAAAEAAPAAAAAACV5WkaAAAAABAADwAAAAAA9P8KAAAAAAAPABAAAAAAAMalOFoAAAAADwAQAAAAAABCqbxWAAAAABAADwAAAAAABf75AQAAAAAQAA8AAAAAAJXlaRoAAAAADwAQAAAAAABqp5RYAAAAAA8AEAAAAAAAtaZJWQAAAAAQAA8AAAAAAOPoGxcAAAAAEAAPAAAAAAD7/gMBAAAAAA8AEAAAAAAAMafNWAAAAAAPABAAAAAAAN+mH1kAAAAAEAAPAAAAAACz+ksFAAAAAA8AEAAAAAAAQqm8VgAAAAAPABAAAAAAAMalOFoAAAAADwAQAAAAAABFq7lUAAAAAA8AEAAAAAAAMafNWAAAAAAPABAAAAAAAN+mH1kAAAAADwAQAAAAAABFq7lUAAAAAA8AEAAAAAAAtaZJWQAAAAAPABAAAAAAAGqnlFgAAAAADwAQAAAAAADBpz1YAAAAAA8AEAAAAAAAMafNWAAAAAAQAA8AAAAAAOPoGxcAAAAADwAQAAAAAABFq7lUAAAAAA8AEAAAAAAAaqeUWAAAAAAQAA8AAAAAACL73AQAAAAAEAAPAAAAAAAF/vkBAAAAAA8AEAAAAAAA36YfWQAAAAAPABAAAAAAAMalOFoAAAAAEAAPAAAAAAD7/gMBAAAAABAADwAAAAAAs/pLBQAAAAAPABAAAAAAAMGnPVgAAAAADwAQAAAAAAAxp81YAAAAABAADwAAAAAAMOzOEwAAAAAQAA8AAAAAACL73AQAAAAADwAQAAAAAABCqbxWAAAAAA8AEAAAAAAARau5VAAAAAAQAA8AAAAAAPT/CgAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWABUAAAAAAPj/BgAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAVAAAAAAD4/wYAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAAAAAAAAA//8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWABUAAAAAAPj/BgAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD5/wUAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD8/wIAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAWAAAAAAD5/wUAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAO//DwAAAAAAFQAWAAAAAAAA8f4OAAAAABUAFgAAAAAA1v8oAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAAAPH+DgAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAADx/w0AAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAADx/g4AAAAAFQAWAAAAAADs/xIAAAAAABUAFgAAAAAAAPH+DgAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAAA8f4OAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAADx/g4AAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAAAPH+DgAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA8f8NAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA8f8NAAAAAAAVABYAAAAAAOz/EgAAAAAAFQAWAAAAAAD5/wUAAAAAABUAFgAAAAAAr/9PAAAAAAAVABYAAAAAAOz/EgAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA+f8FAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/P8CAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/P8CAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD5/wUAAAAAABUAAAAAAAAA//8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD0/woAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD8/wIAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAACv/08AAAAAABUAAAAAAAAA//8AAAAAAAAVABYAAAAAAGP/mwAAAAAAFQAWAAAAAAD5/wUAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAPT/CgAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAWAAAAAACv/08AAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAPH/DQAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA0v8sAAAAAAAVABYAAAAAAGP/mwAAAAAAFQAAAAAAAAD//wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/P8CAAAAAAAVABYAAAAAAPT/CgAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAAY/+bAAAAAAAVABYAAAAAANL/LAAAAAAAFQAWAAAAAACv/08AAAAAABUAAAAAAAAA//8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAADS/ywAAAAAABUAFgAAAAAA/P8CAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD5/wUAAAAAABUAFgAAAAAA/v8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAA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BgAAAAUACAAHAAAA//8AAAAAAAAFAAcACAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD8/wIAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAAAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA/P8CAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkACgAAAAAA/P8CAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAAAAAAAAD//wAAAAAAAAkACgAAAAAA/f8BAAAAAAAJAAAAAAAAAP//AAAAAAAACQAKAAAAAAD8/wIAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD8/wIAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAKAAAAAAD8/wIAAAAAAAkACgAAAAAA/P8CAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAAAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACQAKAAAAAAD8/wIAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA/P8CAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA/f8BAAAAAAAJAAoAAAAAAPz/AgAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP//AAAAAAAACQAKAAAAAAD+/wAAAAAAAAkACgAAAAAA/v8AAAAAAAAJAAoAAAAAAP7/AAAAAAAACQAKAAAAAAD//wAAAAAAAAkACgAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAKAAAAAAD//wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAP//AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAKAAAAAAD9/wEAAAAAAAsACgAAAAAA5/8XAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD//wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAPb/CAAAAAAACwAAAAAAAAD//wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD9/wEAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAP3/AQAAAAAACwAAAAAAAAD//wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAB+/4AAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAAr/9AAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAKAAAAAAD9/wEAAAAAAAsACgAAAAAAyv80AAAAAAALAAoAAAAAAJH+bQEAAAAACwAKAAAAAADK/zQAAAAAAAsACgAAAAAA9v8IAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/f8BAAAAAAALAAoAAAAAAP//AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAH7/gAAAAAAACwAAAAAAAAD//wAAAAAAAAsACgAAAAAACv/0AAAAAAALAAoAAAAAAMr/NAAAAAAACwAKAAAAAACR/m0BAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsACgAAAAAA//8AAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD//wAAAAAAAAsACgAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAKAAAAAADK/zQAAAAAAAsACgAAAAAA/f8BAAAAAAALAAoAAAAAAPb/CAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD//wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD9/wEAAAAAAAsACgAAAAAA/f8BAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD9/wEAAAAAAAsACgAAAAAA9v8IAAAAAAALAAoAAAAAAP7/AAAAAAAACwAAAAAAAAD//wAAAAAAAAsACgAAAAAA/f8BAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/f8BAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD2/wgAAAAAAAsACgAAAAAA9v8IAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/f8BAAAAAAALAAoAAAAAAPv/AwAAAAAACwAAAAAAAAD//wAAAAAAAAsACgAAAAAA7P8SAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA9v8IAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD2/wgAAAAAAAsACgAAAAAA/f8BAAAAAAALAAoAAAAAAPb/CAAAAAAACwAKAAAAAAD+/wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA+/8DAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/f8BAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAoAAAAAAP7/AAAAAAAACwAKAAAAAADs/xIAAAAAAAsACgAAAAAA+/8DAAAAAAALAAoAAAAAAPz/AgAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAP7/AAAAAAAACwAAAAAAAAD//wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAPz/AgAAAAAACwAKAAAAAAD+/wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAoAAAAAAPz/AgAAAAAA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//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsACgAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAoAAAAAAP7/AAAAAAAACwAAAAAAAAD//wAAAAAAAAsACgAAAAAA/v8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAKAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAoAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAKAAAAAAD+/wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACQAAAAAAAAD//wAAAAAAAAkAAAAAAAAA//8AAAAAAAAJAAAAAAAAAP//AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD//wAAAAAAAAoACwAAAAAA7v4QAQAAAAAKAAsAAAAAAO//DwAAAAAACgALAAAAAADw/w4AAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAPj/BgAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAAu/9DAAAAAAAKAAsAAAAAAKH/XQAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/f8BAAAAAAAKAAsAAAAAAPv/AwAAAAAACgAAAAAAAAD//wAAAAAAAAoACwAAAAAA//8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA8P8OAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA7/8PAAAAAAAKAAsAAAAAALv/QwAAAAAACgALAAAAAAD9/wEAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP//AAAAAAAACgAAAAAAAAD//wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP//AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA7f8RAAAAAAAKAAsAAAAAAP7/AAAAAAAACgAAAAAAAAD//wAAAAAAAAoACwAAAAAA7/8PAAAAAAAKAAsAAAAAAOv/EwAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP//AAAAAAAACgALAAAAAAD3/wcAAAAAAAoACwAAAAAA/f8BAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD//wAAAAAAAAoACwAAAAAA/f8BAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAADv/w8AAAAAAAoACwAAAAAA7f8RAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA//8AAAAAAAAKAAsAAAAAAP3/AQAAAAAACgALAAAAAAD9/wEAAAAAAAoACwAAAAAA9/8HAAAAAAAKAAsAAAAAAPv/AwAAAAAACgALAAAAAAD+/wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAsAAAAAAOv/EwAAAAAACgALAAAAAAD3/wcAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP//AAAAAAAACgALAAAAAAD3/wcAAAAAAAoACwAAAAAA+/8DAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD//wAAAAAAAAoACwAAAAAA/f8BAAAAAAAKAAsAAAAAAOf/FwAAAAAACgALAAAAAADt/xEAAAAAAAoACwAAAAAA9/8HAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD0/woAAAAAAAoACwAAAAAA/f8BAAAAAAAKAAsAAAAAAPv/AwAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA9/8HAAAAAAAKAAsAAAAAAPf/BwAAAAAACgALAAAAAAD//wAAAAAAAAoACwAAAAAA+/8DAAAAAAAKAAAAAAAAAP//AAAAAAAACgALAAAAAADn/xcAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAAAAAAAAP//AAAAAAAACgALAAAAAAD0/woAAAAAAAoACwAAAAAA7f8RAAAAAAAKAAsAAAAAAPv/AwAAAAAACgALAAAAAADn/xcAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP//AAAAAAAACgALAAAAAAD5/wUAAAAAAAoACwAAAAAA9P8KAAAAAAAKAAsAAAAAALH/TQAAAAAACgALAAAAAAD3/wcAAAAAAAoACwAAAAAA1f0pAgAAAAAKAAsAAAAAAPf/BwAAAAAACgALAAAAAACu/1AAAAAAAAoACwAAAAAA+/8DAAAAAAAKAAsAAAAAAHX/iQAAAAAACgALAAAAAAD//wAAAAAAAAoACwAAAAAA//8AAAAAAAAKAAsAAAAAAPv/AwAAAAAACgAAAAAAAAD//wAAAAAAAAoACwAAAAAA+f8FAAAAAAAKAAsAAAAAAOf/FwAAAAAACgALAAAAAAD7/wMAAAAAAAoACwAAAAAA1f0pAgAAAAAKAAsAAAAAAPT/CgAAAAAACgALAAAAAACp/1UAAAAAAAoACwAAAAAA9/8HAAAAAAAKAAsAAAAAALf/RwAAAAAACgAAAAAAAAD//wAAAAAAAAoACwAAAAAAsf9NAAAAAAAKAAsAAAAAAPv/AwAAAAAACgALAAAAAAD//wAAAAAAAAoACwAAAAAA//8AAAAAAAAKAAsAAAAAAK7/UAAAAAAACgALAAAAAAB1/4kAAAAAAAoACwAAAAAA//8AAAAAAAAKAAsAAAAAAK7/UAAAAAAACgALAAAAAAD9/wEAAAAAAAoACwAAAAAA//8AAAAAAAAKAAsAAAAAAP//AAAAAAAACgALAAAAAAD7/wMAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAsAAAAAAP//AAAAAAAACgALAAAAAAD5/wUAAAAAAAoACwAAAAAAqf9VAAAAAAAKAAsAAAAAAO7/EAAAAAAACgALAAAAAADV/SkCAAAAAAoACwAAAAAA9v8IAAAAAAAKAAsAAAAAALH/TQAAAAAACgALAAAAAADL/zMAAAAAAAoACwAAAAAAt/9HAAAAAAAKAAsAAAAAAOL8HAMAAAAACgALAAAAAACu/1AAAAAAAAoACwAAAAAA//8AAAAAAAAKAAsAAAAAAP//AAAAAAAACgALAAAAAAD//wAAAAAAAAoACwAAAAAA+f8FAAAAAAAKAAsAAAAAAP//AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA//8AAAAAAAAKAAsAAAAAANX9KQIAAAAACgALAAAAAADi/BwDAAAAAAoACwAAAAAAsf9NAAAAAAAKAAsAAAAAAO7/EAAAAAAACgAAAAAAAAD//wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP//AAAAAAAACgALAAAAAAD2/wgAAAAAAAoACwAAAAAA/f8BAAAAAAAKAAsAAAAAAO7/EAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA4vwcAwAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAADL/zMAAAAAAAoACwAAAAAAz/8vAAAAAAAKAAsAAAAAAP//AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA//8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD//wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP//AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA7v8QAAAAAAAKAAsAAAAAAM//LwAAAAAACgALAAAAAADi/BwDAAAAAAoACwAAAAAA/f8BAAAAAAAKAAsAAAAAAP3/AQAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA//8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD//wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAPv/AwAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA+f8FAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD4/wYAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA9/8HAAAAAAAKAAsAAAAAAP3/AQAAAAAACgALAAAAAABh/50AAAAAAAoACwAAAAAAz/8vAAAAAAAKAAsAAAAAAOf/FwAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA+P8GAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAAy/8wAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAPn/BQAAAAAACgALAAAAAAD7/wMAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAsAAAAAAPf/BwAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAOf/FwAAAAAACgALAAAAAAD9/wEAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAM//LwAAAAAACgALAAAAAAC9/kEBAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAGH/nQAAAAAACgALAAAAAAD3/wcAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA5/8XAAAAAAAKAAsAAAAAAPD/DgAAAAAACgALAAAAAABh/50AAAAAAAoACwAAAAAA7v4QAQAAAAAKAAsAAAAAALv/QwAAAAAACgALAAAAAAD7/wMAAAAAAAoACwAAAAAA//8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD5/wUAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAPv/AwAAAAAACgAAAAAAAAD//wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAPf/BwAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAL3+QQEAAAAACgALAAAAAACF/nkBAAAAAAoACwAAAAAAYf+dAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAC9/kEBAAAAAAoACwAAAAAA8P8OAAAAAAAKAAsAAAAAAPj/BgAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA+f8FAAAAAAAKAAsAAAAAALv/QwAAAAAACgALAAAAAAAy/8wAAAAAAAoACwAAAAAAof9dAAAAAAAKAAsAAAAAAPj/BgAAAAAACgALAAAAAACV/mkBAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP//AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA9v8IAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA9v8IAAAAAAAKAAsAAAAAAKn/VQAAAAAACgALAAAAAAD//wAAAAAAAAoACwAAAAAA//8AAAAAAAAKAAsAAAAAAKn/VQAAAAAACgALAAAAAAD7/wMAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP3/AQAAAAAACgALAAAAAAD7/wMAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAsAAAAAAP//AAAAAAAACgALAAAAAAD+/wAAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD9/wEAAAAAAAoACwAAAAAAvf5BAQAAAAAKAAsAAAAAAJX+aQEAAAAACgALAAAAAAAy/8wAAAAAAAoACwAAAAAAhf55AQAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAAMv/MAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAC9/kEBAAAAAAoACwAAAAAAy/8zAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD9/wEAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAALf/RwAAAAAACgALAAAAAAD9/wEAAAAAAAoACwAAAAAAdf+JAAAAAAAKAAsAAAAAAMv/MwAAAAAACgAAAAAAAAD//wAAAAAAAAoACwAAAAAAdf+JAAAAAAAKAAsAAAAAAPf/BwAAAAAACgALAAAAAAC3/0cAAAAAAAoACwAAAAAA6/8TAAAAAAAKAAsAAAAAAPf/BwAAAAAACgALAAAAAAD9/wEAAAAAAAoAAAAAAAAA//8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD9/wEAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAOv/EwAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAAHP/iAAAAAAAKAAsAAAAAAOf/FwAAAAAACgALAAAAAAD7/wMAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD7/wMAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAPj/BgAAAAAACgALAAAAAACV/mkBAAAAAAoACwAAAAAAof9dAAAAAAAKAAsAAAAAAIX+eQEAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA9/4HAQAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD7/wMAAAAAAAoACwAAAAAAHP/iAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD9/wEAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD//wAAAAAAAAoACwAAAAAA+P8GAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD7/wMAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAPv/AwAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAPj/BgAAAAAACgALAAAAAADu/hABAAAAAAoACwAAAAAA9/4HAQAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP3/AQAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP7/AAAAAAAACgALAAAAAAD+/wAAAAAAAAoACwAAAAAA/v8AAAAAAAAKAAsAAAAAAP3/AQAAAAAACgALAAAAAADn/xcAAAAAAAoACwAAAAAAHP/iAAAAAAAKAAsAAAAAAO7+EAEAAAAACgALAAAAAAD3/gcBAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAsAAAAAAPr/BAAAAAAADAAAAAAAAAD//wAAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAsAAAAAAPf/BwAAAAAADAALAAAAAADp/xUAAAAAAAwAAAAAAAAA//8AAAAAAAAMAAAAAAAAAP//AAAAAAAADAAAAAAAAAD//wAAAAAAAAwACwAAAAAA9/8HAAAAAAAMAAsAAAAAAPb/CAAAAAAADAAAAAAAAAD//wAAAAAAAAwACwAAAAAAXp+gYAAAAAAMAAsAAAAAAE6ZsGYAAAAADAALAAAAAAC/hD97AAAAAAsADAAAAAAADpzwYwAAAAALAAwAAAAAAJmeZWEAAAAACwAMAAAAAAA2n8hgAAAAAAwACwAAAAAATpmwZgAAAAAMAAsAAAAAAD/GvzkAAAAACwAMAAAAAAA2n8hgAAAAAAsADAAAAAAAmZ5lYQAAAAAMAAsAAAAAAD/GvzkAAAAADAALAAAAAABOmbBmAAAAAAsADAAAAAAAmZ5lYQAAAAALAAwAAAAAAA6c8GMAAAAADAALAAAAAABen6BgAAAAAAsADAAAAAAAE4TrewAAAAALAAwAAAAAADafyGAAAAAACwAMAAAAAAAThOt7AAAAAAwACwAAAAAAP8a/OQAAAAAMAAsAAAAAAL+EP3sAAAAACwAMAAAAAAAThOt7AAAAAAwACwAAAAAAXp+gYAAAAAALAAwAAAAAAA6c8GMAAAAACwAMAAAAAACZnmVhAAAAAAsADAAAAAAAaKqWVQAAAAALAAwAAAAAAMalOFoAAAAACwAMAAAAAABoqpZVAAAAAAsADAAAAAAAtaZJWQAAAAALAAwAAAAAAGiqllUAAAAACwAMAAAAAABoqpZVAAAAAAsADAAAAAAADpzwYwAAAAAMAAsAAAAAAL+EP3sAAAAADAALAAAAAABOmbBmAAAAAAwACwAAAAAAXp+gYAAAAAALAAwAAAAAABOE63sAAAAACwAMAAAAAAA2n8hgAAAAAAwACwAAAAAAv4Q/ewAAAAAMAAsAAAAAAD/GvzkAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAALAAAAAAAAAP//AAAAAAAACwAAAAAAAAD//wAAAAAAAAsAAAAAAAAA//8AAAAAAAASABMAAAAAAEz+sgEAAAAAEgATAAAAAAD+/wAAAAAAABIAEwAAAAAAkv9sAAAAAAASABMAAAAAAPH/DQAAAAAAEgATAAAAAAD6/wQAAAAAABIAEwAAAAAALv/QAAAAAAASABMAAAAAAPz/AgAAAAAAEgATAAAAAACB/30AAAAAABIAAAAAAAAA//8AAAAAAAASABMAAAAAAHj/hgAAAAAAEgATAAAAAADc/yIAAAAAABIAEwAAAAAA+v8EAAAAAAASABMAAAAAAP7/AAAAAAAAEgATAAAAAABr/5MAAAAAABIAEwAAAAAA5v8YAAAAAAASABMAAAAAAJL/bAAAAAAAEgATAAAAAAD8/wIAAAAAABIAEwAAAAAA8f8NAAAAAAASABMAAAAAAP7/AAAAAAAAEgATAAAAAAAu/9AAAAAAABIAEwAAAAAA3P8iAAAAAAASABMAAAAAAIH/fQAAAAAAEgATAAAAAAD6/wQAAAAAABIAEwAAAAAAeP+GAAAAAAASABMAAAAAAOb/GAAAAAAAEgATAAAAAAD6/wQAAAAAABIAAAAAAAAA//8AAAAAAAASABMAAAAAAGv/kwAAAAAAEgATAAAAAAD+/wAAAAAAABIAEwAAAAAA//8AAAAAAAASABMAAAAAAOb/GAAAAAAAEgAAAAAAAAD//wAAAAAAABIAEwAAAAAA/v8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgATAAAAAAD+/wAAAAAAABIAEwAAAAAAbv+QAAAAAAASABMAAAAAAPz/AgAAAAAAEgATAAAAAAD0/woAAAAAABIAEwAAAAAA+v8EAAAAAAASABMAAAAAAPz/AgAAAAAAEgATAAAAAADc/yIAAAAAABIAEwAAAAAA/v8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAEwAAAAAA5v8YAAAAAAASABMAAAAAAP7/AAAAAAAAEgATAAAAAAD+/wAAAAAAABIAAAAAAAAA//8AAAAAAAASABMAAAAAAP//AAAAAAAAEgATAAAAAABM/rIBAAAAABIAEwAAAAAA9P8KAAAAAAASABMAAAAAAP7/AAAAAAAAEgATAAAAAAD8/wIAAAAAABIAEwAAAAAA/v8AAAAAAAASABMAAAAAAPr/BAAAAAAAEgATAAAAAABu/5AAAAAAABIAEwAAAAAA3P8iAAAAAAASABMAAAAAAP7/AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAEwAAAAAA/P8CAAAAAAASABMAAAAAAPT/CgAAAAAAEgATAAAAAAD7/wMAAAAAABIAEwAAAAAAbv+QAAAAAAASABMAAAAAAOb6GAUAAAAAEgATAAAAAAD+/wAAAAAAABIAEwAAAAAA/v8AAAAAAAASABMAAAAAAPz/AgAAAAAAEgATAAAAAAD8/wIAAAAAABIAEwAAAAAA/v8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAEwAAAAAA/v8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgATAAAAAAD+/wAAAAAAABIAEwAAAAAA//8AAAAAAAASABMAAAAAAEz+sgEAAAAAEgATAAAAAADm+hgFAAAAABIAEwAAAAAA9P8KAAAAAAASABMAAAAAAG7/kAAAAAAAEgATAAAAAAD8/wIAAAAAABIAEwAAAAAA/v8AAAAAAAASABMAAAAAAPv/AwAAAAAAEgATAAAAAAD8/wIAAAAAABIAEwAAAAAA/v8AAAAAAAASABMAAAAAAP7/AAAAAAAAEgATAAAAAAD+/wAAAAAAABIAAAAAAAAA//8AAAAAAAASABMAAAAAAP7/AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASABMAAAAAAP7/AAAAAAAAEgATAAAAAAD//wAAAAAAABIAEwAAAAAA//8AAAAAAAASABMAAAAAAEz+sgEAAAAAEgATAAAAAAD+/wAAAAAAABIAEwAAAAAA5voYBQAAAAASABMAAAAAAPv/AwAAAAAAEgATAAAAAAD1/wkAAAAAABIAEwAAAAAA/P8CAAAAAAASABMAAAAAAP7/AAAAAAAAEgATAAAAAAD+/wAAAAAAABIAAAAAAAAA//8AAAAAAAASABMAAAAAAP7/AAAAAAAAEgATAAAAAAD+/wAAAAAAABIAAAAAAAAA//8AAAAAAAASABMAAAAAAP//AAAAAAAAEgATAAAAAAD+/wAAAAAAABIAEwAAAAAA/v8AAAAAAAASABMAAAAAAOb6GAUAAAAAEgATAAAAAAD+/wAAAAAAABIAEwAAAAAA+/8DAAAAAAASABMAAAAAAP7/AAAAAAAAEgATAAAAAAD8/wIAAAAAABIAEwAAAAAA/v8AAAAAAAASABMAAAAAAP7/AAAAAAAAEgATAAAAAAD1/wkAAAAAABIAEwAAAAAA/v8AAAAAAAASABMAAAAAAP7/AAAAAAAAEgAAAAAAAAD//wAA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//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAEwAAAAAAXf+hAAAAAAASAAAAAAAAAP//AAAAAAAAEgATAAAAAAD+/wAAAAAAABIAAAAAAAAA//8AAAAAAAASABMAAAAAAP7/AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAEwAAAAAA7f8RAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASABMAAAAAAOn+FQEAAAAAEgAAAAAAAAD//wAAAAAAABIAEwAAAAAAnv9gAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAAAAAAAAA//8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD9/wEAAAAAABEAEgAAAAAA/f8BAAAAAAARABIAAAAAAOz/EgAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA7/8PAAAAAAARABIAAAAAAFr+pAEAAAAAEQASAAAAAADW/ygAAAAAABEAEgAAAAAAAPH+DgAAAAARABIAAAAAAPn/BQAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA/f8BAAAAAAARABIAAAAAANb/KAAAAAAAEQASAAAAAADv/w8AAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD5/wUAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP3/AQAAAAAAEQASAAAAAAD2/wgAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAADW/ygAAAAAABEAEgAAAAAA/f8BAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD9/wEAAAAAABEAEgAAAAAA9v8IAAAAAAARABIAAAAAAPr/BAAAAAAAEQASAAAAAAD9/wEAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAADV/ykAAAAAABEAEgAAAAAA/f8BAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAADV/ykAAAAAABEAEgAAAAAA/f8BAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAPz/AgAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA1f8pAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAACE+noFAAAAABEAEgAAAAAA+f8FAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAADV/ykAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD5/wUAAAAAABEAEgAAAAAAWv6kAQAAAAARABIAAAAAAIT6egUAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAACE+noFAAAAABEAEgAAAAAAWv6kAQAAAAARABIAAAAAAADx/g4AAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP//AAAAAAAAEQASAAAAAAD+/wAAAAAAABEAEgAAAAAA//8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD//wAAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD9/wEAAAAAABEAEgAAAAAA/v8AAAAAAAARABIAAAAAAP7/AAAAAAAAEQASAAAAAAD+/wAAAAAAABIAEQAAAAAAvv1AAgAAAAASABEAAAAAAPT/CgAAAAAAEgARAAAAAAAp/9UAAAAAABIAEQAAAAAA1/8nAAAAAAASABEAAAAAAJP/awAAAAAAEgARAAAAAAD4/wYAAAAAABIAEQAAAAAAxf45AQAAAAASABEAAAAAAD7+wAEAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA//8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD//wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAANf/JwAAAAAAEgARAAAAAAC+/UACAAAAABIAAAAAAAAA//8AAAAAAAASABEAAAAAAD7+wAEAAAAAEgARAAAAAACT/2sAAAAAABIAEQAAAAAACP/2AAAAAAASABEAAAAAAMX+OQEAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAAgP9+AAAAAAASABEAAAAAAPj/BgAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD//wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAPz/AgAAAAAAEgARAAAAAADX/ycAAAAAABIAEQAAAAAACP/2AAAAAAASABEAAAAAAN//HwAAAAAAEgARAAAAAAA+/sABAAAAABIAEQAAAAAA5P8aAAAAAAASABEAAAAAAPj/BgAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAPz/AgAAAAAAEgARAAAAAAD//wAAAAAAABIAEQAAAAAA//8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD//wAAAAAAABIAEQAAAAAA1/8nAAAAAAASABEAAAAAAP//AAAAAAAAEgARAAAAAAD0/woAAAAAABIAEQAAAAAA/P8CAAAAAAASABEAAAAAAD7+wAEAAAAAEgARAAAAAAD8/wIAAAAAABIAEQAAAAAA+P8GAAAAAAASABEAAAAAAN//HwAAAAAAEgAAAAAAAAD//wAAAAAAABIAEQAAAAAA//8AAAAAAAASABEAAAAAAP//AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP//AAAAAAAAEgARAAAAAAD//wAAAAAAABIAEQAAAAAA/P8CAAAAAAASABEAAAAAAPD/DgAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/f8BAAAAAAASABEAAAAAAP//AAAAAAAAEgARAAAAAAD//wAAAAAAABIAEQAAAAAA//8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD8/wIAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP//AAAAAAAAEgARAAAAAADr/xMAAAAAABIAEQAAAAAA3/8fAAAAAAASABEAAAAAAP3/AQAAAAAAEgARAAAAAAD8/wIAAAAAABIAEQAAAAAA9v8IAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA//8AAAAAAAASABEAAAAAAPH/DQAAAAAAEgARAAAAAAD//wAAAAAAABIAEQAAAAAA//8AAAAAAAASABEAAAAAAP//AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA6/8TAAAAAAASABEAAAAAAPz/AgAAAAAAEgARAAAAAADk/xoAAAAAABIAEQAAAAAA9v8IAAAAAAASABEAAAAAAN//HwAAAAAAEgARAAAAAAD//wAAAAAAABIAEQAAAAAA6/8TAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA9f8JAAAAAAASABEAAAAAAPb/CAAAAAAAEgARAAAAAAD//wAAAAAAABIAEQAAAAAA/f8BAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAADx/w0AAAAAABIAEQAAAAAA/P8CAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD6/wQAAAAAABIAEQAAAAAA//8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD//wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAPX/CQAAAAAAEgARAAAAAADr/xMAAAAAABIAEQAAAAAA//8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD2/wgAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP3/AQAAAAAAEgAAAAAAAAD//wAAAAAAABIAEQAAAAAA8P8OAAAAAAASABEAAAAAAP//AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP//AAAAAAAAEgARAAAAAAD8/wIAAAAAABIAEQAAAAAA+v8EAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD8/wIAAAAAABIAAAAAAAAA//8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA//8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAADt/xEAAAAAABIAEQAAAAAA9f8JAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD//wAAAAAAABIAEQAAAAAA/v8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/P8CAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAPX/CQAAAAAAEgARAAAAAAD8/wIAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAO3/EQAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP//AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAABT/6gAAAAAAEgARAAAAAADt/xEAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP//AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/f8BAAAAAAASABEA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AAAAABIAEQAAAAAA6P8WAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAAVf+pAAAAAAASABEAAAAAAPz/AgAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAOj/FgAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD8/wIAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD//wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP//AAAAAAAAEgARAAAAAAD//wAAAAAAABIAEQAAAAAA5P8aAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD0/woAAAAAABIAEQAAAAAA5P8aAAAAAAASABEAAAAAAAj/9gAAAAAAEgARAAAAAAD//wAAAAAAABIAEQAAAAAAKf/VAAAAAAASABEAAAAAAAj/9gAAAAAAEgAAAAAAAAD//wAAAAAAABIAEQAAAAAA9P8KAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAID/fgAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP3/AQAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAP3/AQAAAAAAEgAAAAAAAAD//wAAAAAAABIAEQAAAAAA/v8AAAAAAAASAAAAAAAAAP//AAAAAAAAEgAAAAAAAAD//wAAAAAAABIAEQAAAAAA+v8EAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAADw/w4AAAAAABIAEQAAAAAA+v8EAAAAAAASABEAAAAAAPH/DQAAAAAAEgAAAAAAAAD//wAAAAAAABIAEQAAAAAA/v8AAAAAAAASABEAAAAAAPH/DQAAAAAAEgARAAAAAAD+/wAAAAAAABIAEQAAAAAA8P8OAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD+/wAAAAAAABIAAAAAAAAA//8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAACA/34AAAAAABIAAAAAAAAA//8AAAAAAAASABEAAAAAAP7/AAAAAAAAEgARAAAAAAD+/wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAARAAAAAAAAAP//AAAAAAAAEQAAAAAAAAD//wAAAAAAABEAAAAAAAAA//8AAAAAAAATABQAAAAAAIrOdDEAAAAAEwAUAAAAAACKznQxAAAAABMAFAAAAAAArc5RMQAAAAATABQAAAAAAGCznkwAAAAAEwAUAAAAAABgs55MAAAAABMAFAAAAAAAF7/nQAAAAAAUABMAAAAAACPr2xQAAAAAFAAAAAAAAAD//wAAAAAAABMAFAAAAAAAis50MQAAAAATABQAAAAAAGCznkwAAAAAEwAUAAAAAADgyx40AAAAABMAFAAAAAAAitt0JAAAAAATABQAAAAAANfOJzEAAAAAEwAUAAAAAADa2CQnAAAAABMAFAAAAAAAV9+nIAAAAAATABQAAAAAANPfKyAAAAAAEwAUAAAAAADT3ysgAAAAABMAFAAAAAAAf95/IQAAAAATABQAAAAAAFffpyAAAAAAEwAUAAAAAAB/3n8hAAAAABMAFAAAAAAALtvQJAAAAAAUAAAAAAAAAP//AAAAAAAAEwAUAAAAAADb4CMfAAAAABMAFAAAAAAAV9+nIAAAAAATABQAAAAAADrcxCMAAAAAEwAUAAAAAAC94EEfAAAAABMAFAAAAAAA2tgkJwAAAAATABQAAAAAAIrbdCQAAAAAEwAUAAAAAACa2GQnAAAAABMAFAAAAAAA2+AjHwAAAAATABQAAAAAANjhJh4AAAAAEwAUAAAAAAAY2uYlAAAAABMAFAAAAAAAetiEJwAAAAATABQAAAAAAGvRky4AAAAAEwAUAAAAAADa2CQnAAAAABMAFAAAAAAA4MseNAAAAAATABQAAAAAAJrYZCcAAAAAEwAUAAAAAADk4xocAAAAABMAFAAAAAAAveBBHwAAAAATABQAAAAAAI7ccCMAAAAAEwAUAAAAAAC+2kAlAAAAABMAFAAAAAAAOtzEIwAAAAATABQAAAAAAELfvCAAAAAAEwAUAAAAAAAQ3u4hAAAAABMAFAAAAAAAV9+nIAAAAAATABQAAAAAAJrYZCcAAAAAEwAUAAAAAABr0ZMuAAAAABMAFAAAAAAAHNniJgAAAAATABQAAAAAANfOJzEAAAAAEwAUAAAAAAC94EEfAAAAABMAFAAAAAAA5OMaHAAAAAATABQAAAAAAIrbdCQAAAAAEwAUAAAAAAAQ3u4hAAAAABMAFAAAAAAAOtzEIwAAAAATABQAAAAAAI7ccCMAAAAAEwAUAAAAAADY4SYeAAAAABMAFAAAAAAAV9+nIAAAAAATABQAAAAAAELfvCAAAAAAEwAUAAAAAADb4CMfAAAAABMAFAAAAAAAetiEJwAAAAATABQAAAAAAIHZfSYAAAAAEwAUAAAAAADXpidZAAAAABMAFAAAAAAAktRsKwAAAAATABQAAAAAAMXeOSEAAAAAEwAUAAAAAAB/3n8hAAAAABMAFAAAAAAAAt78IQAAAAATABQAAAAAANPfKyAAAAAAEwAUAAAAAABX36cgAAAAABMAFAAAAAAABqD4XwAAAAAUABMAAAAAANPUKysAAAAAFAATAAAAAAA5nsVhAAAAABMAFAAAAAAA16YnWQAAAAATABQAAAAAAH/efyEAAAAAEwAUAAAAAADT3ysgAAAAABMAFAAAAAAAV9+nIAAAAAATABQAAAAAANfYJycAAAAAEwAUAAAAAABE1ropAAAAABQAEwAAAAAAr6lPVgAAAAATABQAAAAAAObZGCYAAAAAEwAUAAAAAAAGoPhfAAAAABQAEwAAAAAAFuXoGgAAAAAUAAAAAAAAAP//AAAAAAAAEwAUAAAAAACS1GwrAAAAABMAFAAAAAAA19gnJwAAAAATABQAAAAAAALe/CEAAAAAEwAUAAAAAAAu29AkAAAAABMAFAAAAAAA5tkYJgAAAAAUAAAAAAAAAP//AAAAAAAABgAFAAAAAADfhB97AAAAAAYABQAAAAAAuoBEfwAAAAAGAAUAAAAAAN+EH3sAAAAABgAFAAAAAADHhTd6AAAAAAYABQAAAAAAx4U3egAAAAAGAAUAAAAAABGA7X8AAAAABgAFAAAAAAC6gER/AAAAAAYABQAAAAAAEYDtfwAAAAAFAAYABwAAAMOfOGACAAAABQAGAAcAAADcnSJiAAAAAAUABgAHAAAAhp14YgAAAAAFAAYABwAAAG2OkXEAAAAABQAGAAcAAACDn3tgAAAAAAUABgAAAAAAAKL+XQAAAAAFAAYABwAAALWhSV4AAAAABQAGAAAAAACclWJqAAAAAAUABgAHAAAA1qEoXgAAAAAFAAYAAAAAAACi/l0AAAAABQAGAAcAAADcnSJiAAAAAAUABgAHAAAA1qEoXgAAAAAFAAYABwAAAIadeGIAAAAABQAGAAcAAADQni5hAAAAAAUABgAHAAAA1qEoXgAAAAAFAAYABwAAANydImIAAAAABQAGAAcAAADQni5hAAAAAAUABgAHAAAA1qEoXgAAAAAFAAYAAAAAAJyVYmoAAAAABQAGAAcAAACDn3tgAAAAAAUABgAHAAAAbY6RcQAAAAAFAAYABwAAALWhSV4AAAAABQAGAAcAAADcnSJiAAAAAAUABgAHAAAAw584YAIAAAAFAAYABwAAAMOfOGACAAAABQAGAAcAAAC1oUleAAAAAAUABgAHAAAAbY6RcQAAAAAFAAYAAAAAAJyVYmoAAAAABQAGAAcAAADQni5hAAAAAAUABgAHAAAAg597YAAAAAAFAAYABwAAANCeLmEAAAAABQAGAAcAAACGnXhiAAAAAAUABgAHAAAAg597YAAAAAAFAAYAAAAAAACi/l0AAAAABQAGAAAAAACclWJqAAAAAAUABgAHAAAAtaFJXgAAAAAFAAYAAAAAAACi/l0AAAAABQAGAAcAAABtjpFxAAAAAAUABgAHAAAAhp14YgAAAAAFAAYABwAAAMOfOGACAAAAEwAUAAAAAAD91gEpAAAAABMAFAAAAAAAq8FTPgAAAAATABQAAAAAAHrYhCcAAAAAEwAUAAAAAABC37wgAAAAABMAFAAAAAAA9t4IIQAAAAATABQAAAAAANjhJh4AAAAAEwAUAAAAAACO3HAjAAAAABMAFAAAAAAAk91rIgAAAAATABQAAAAAABDe7iEAAAAAFAATAAAAAAD6rQRSAAAAABMAFAAAAAAA5OMaHAAAAAATABQAAAAAANfOJzEAAAAAFAATAAAAAACztktJAAAAABMAFAAAAAAAgdl9JgAAAAATABQAAAAAABDe7iEAAAAAEwAUAAAAAABC37wgAAAAABMAFAAAAAAAGNrmJQAAAAATABQAAAAAANLZLCYAAAAAEwAUAAAAAACO3HAjAAAAABMAFAAAAAAAF73nQgAAAAATABQAAAAAANnmJRkAAAAAEwAUAAAAAADk4xocAAAAABQAEwAAAAAAs7ZLSQAAAAATABQAAAAAAODLHjQAAAAAFAATAAAAAADa1CQrAAAAABMAFAAAAAAAa9GTLgAAAAATABQAAAAAANLZLCYAAAAAEwAUAAAAAAB62IQnAAAAABMAFAAAAAAA/dYBKQAAAAATABQAAAAAANjhJh4AAAAAEwAUAAAAAADF3zkgAAAAABQAEwAAAAAA2tQkKwAAAAATABQAAAAAANfOJzEAAAAAFAATAAAAAAD6rQRSAAAAABMAFAAAAAAA4MseNAAAAAAUABMAAAAAAD3vwRAAAAAAFAATAAAAAAD6rQRSAAAAABQAEwAAAAAA2tQkKwAAAAATABQAAAAAAObZGCYAAAAAEwAUAAAAAADF3zkgAAAAABMAFAAAAAAAAt78IQAAAAATABQAAAAAAIDffiAAAAAAEwAUAAAAAADX2CcnAAAAABMAFAAAAAAA/dYBKQAAAAATABQAAAAAAJLUbCsAAAAAEwAUAAAAAADS2SwmAAAAABQAEwAAAAAA2tQkKwAAAAATABQAAAAAAAag+F8AAAAAEwAUAAAAAACA334gAAAAABMAFAAAAAAA5tkYJgAAAAATABQAAAAAABe950IAAAAAEwAUAAAAAAAH3vchAAAAABMAFAAAAAAA0tksJgAAAAATABQAAAAAANfYJycAAAAAEwAUAAAAAADXpidZAAAAABQAEwAAAAAAUtSsKwAAAAATABQAAAAAAKvBUz4AAAAAFAATAAAAAADT1CsrAAAAABMAFAAAAAAAF73nQgAAAAATABQAAAAAAAag+F8AAAAAFAATAAAAAAD6rQRSAAAAABMAFAAAAAAAAt78IQAAAAATABQAAAAAAJLUbCsAAAAAEwAUAAAAAADXpidZAAAAABMAFAAAAAAA/dYBKQAAAAAFAAYABAAAAMz/MgAAAAAABQAEAAgAAAD9/wEAAAAAAAUABAAIAAAA/v8AAAAAAAAFAAYABAAIAMH/PQAAAAAABQAGAAcAAAC0uUpGAAAAAAUABgAHAAAAA6j7VwAAAAAFAAYACAAHADHzzQwAAAAABQAGAAcACABS46wcAAAAAAUABgAIAAcA7qsQVAAAAAAFAAYABwAAAB6g4F8AAAAABQAIAAQAAAAa/8EAIwAAAAUABAAIAAAAHv7gAQAAAAAFAAQACAAAAP7/AAAAAAAABQAEAAcACAC4/0YAAAAAAAUABAAIAAcA8/8LAAAAAAAFAAQACAAAAP7/AAAAAAAABQAEAAgAAAAe/uABAAAAAAUACAAAAAAA//8AAAAAAAAFAAYABAAIAMH/PQAAAAAABQAEAAcACAC4/0YAAAAAAAUABgAHAAAA4I4ecQAAAAAFAAYABwAAAESKunUAAAAABQAGAAcAAACwi050AAAAAAUABgAHAAAAKJXWagAAAAAFAAQABwAIALj/RgAAAAAABQAGAAQACADB/z0AAAAAAAUABwAIAAAA//8AAAAAAAAFAAQACAAAAP7/AAAAAAAABQAIAAAAAAD//wAAAAAAAAUABAAIAAcA8/8LAAAAAAAFAAQABwAIALj/RgAAAAAABQAGAAQAAABS96UIBgAAAAUABgAEAAgAwf89AAAAAAAFAAYABAAAAMz/MgAAAAAABQAGAAQAAAA76sMVAAAAAAUABgAEAAAAbfGODgMAAAAFAAYABAAHAMDpOBYGAAAABQAGAAgABwAr+9MEAAAAAAUACAAHAAAA//8AAAAAAAAFAAgABwAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABgAIAAAARsO4PAAAAAAFAAYABwAAADWRyW4AAAAABQAGAAcAAADzlAtrAAAAAAUABgAIAAcA7qsQVAAAAAAFAAYABwAAAB6g4F8AAAAABQAGAAcAAABGiLh3AAAAAAUABgAHAAAARoi4dwAAAAAFAAYABwAAAB6g4F8AAAAABQAGAAcACABS46wcAAAAAAUABgAIAAcA7qsQVAAAAAAFAAYACAAAAEbDuDwAAAAABQAGAAcAAAADqPtXAAAAAAUABgAHAAAAKJXWagAAAAAFAAYABwAAAOCOHnEAAAAABQAGAAcAAAAeoOBfAAAAAAUABgAIAAcA7qsQVAAAAAAFAAgABwAAAP//AAAAAAAABQAGAAgABwD09goJAAAAAAUABgAIAAcAMfPNDAAAAAAFAAYABAAAADvqwxUAAAAABQAGAAcAAAC0uUpGAAAAAAUABgAHAAAAtLlKRgAAAAAFAAYABAAAADvqwxUAAAAABQAGAAQAAACBwHw/AQAAAAUABgAIAAcAMfPNDAAAAAAFAAgABwAAAP//AAAAAAAABQAGAAcAAADgjh5xAAAAAAUABgAHAAAARoi4dwAAAAAFAAYABwAAAAOo+1cAAAAABQAGAAgAAABGw7g8AAAAAAUABgAIAAcAGczkMwAAAAAFAAYABwAIAFLjrBwAAAAABQAGAAgABwAZzOQzAAAAAAUABgAHAAAA85QLawAAAAAFAAYACAAAAEbDuDwAAAAABQAIAAcAAAD//wAAAAAAAAUACAAHAAAA//8AAAAAAAAFAAYABAAAAG3xjg4DAAAABQAGAAQAAAA76sMVAAAAAAUABgAEAAAAAfL1DQgAAAAFAAYABAAAAMz/MgAAAAAABQAGAAQAAABS96UIBgAAAAUABAAIAAAA/v8AAAAAAAAFAAQACAAHAPP/CwAAAAAABQAHAAgAAAD//wAAAAAAAAUABgAHAAAARoi4dwAAAAAFAAYABwAAALCLTnQAAAAABQAGAAcAAACXhGd7AAAAAAUABgAHAAAA4I4ecQAAAAAFAAgABAAAABr/wQAjAAAABQAEAAAAAABR/q0BAAAAAAUABAAIAAAA/v8AAAAAAAAFAAQACAAAAB7+4AEAAAAABQAEAAgABwDz/wsAAAAAAAUABAAIAAAAHv7gAQAAAAAFAAQACAAAAPf/BwAAAAAABQAEAAgAAAD+/wAAAAAAAAUABgAHAAgAUuOsHAAAAAAFAAYACAAHADHzzQwAAAAABQAGAAcAAAADqPtXAAAAAAUABgAHAAAAtLlKRgAAAAAFAAYABAAAAE/7rwQAAAAABQAEAAgAAAD+/wAAAAAAAAUABgAEAAAAzP8yAAAAAAAFAAcACAAAAP//AAAAAAAABQAIAAcAAAD//wAAAAAAAAUABgAEAAAAUvelCAYAAAAFAAYABAAAAG3xjg4DAAAABQAGAAQAAABt8Y4OAwAAAAUABgAEAAAAUvelCAYAAAAFAAgABwAAAP//AAAAAAAABQAHAAgAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA/P8CAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA/P8CAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD9/wEAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA/P8CAAAAAAANAAAAAAAAAP//AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAPz/AgAAAAAADQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAPz/AgAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA/f8BAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD8/wIAAAAAAA0AAAAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD8/wIAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADQAOAAAAAAD8/wIAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA/P8CAAAAAAANAA4AAAAAAP7/AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAA4AAAAAAPz/AgAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA//8AAAAAAAANAA4AAAAAAP7/AAAAAAAADQAOAAAAAAD+/wAAAAAAAA0ADgAAAAAA/v8AAAAAAAANAA4AAAAAAP//AAAAAAAADQAOAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAA4AAAAAAP//AAAAAAAADwAOAAAAAAD//wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD//wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAOAAAAAAD+/wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAAAAAAAAD//wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD+/wAAAAAAAA8ADgAAAAAA5/8XAAAAAAAPAA4AAAAAAP3/AQAAAAAADwAOAAAAAAD+/wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD+/wAAAAAAAA8ADgAAAAAA9v8IAAAAAAAPAA4AAAAAAP3/AQAAAAAADwAAAAAAAAD//wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAB+/4AAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD+/wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAA4AAAAAAP3/AQAAAAAADwAOAAAAAADK/zQAAAAAAA8ADgAAAAAACv/0AAAAAAAPAA4AAAAAABr+5AEAAAAADwAOAAAAAADt/xEAAAAAAA8ADgAAAAAAkf5tAQAAAAAPAA4AAAAAAPb/CAAAAAAADwAOAAAAAADK/zQAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD//wAAAAAAAA8ADgAAAAAA/f8BAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD+/wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAA4AAAAAAH7/gAAAAAAADwAOAAAAAAAK//QAAAAAAA8ADgAAAAAAyv80AAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAACR/m0BAAAAAA8ADgAAAAAA/v8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAA4AAAAAAP//AAAAAAAADwAOAAAAAAD+/wAAAAAAAA8ADgAAAAAA//8AAAAAAAAPAA4AAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAOAAAAAAD+/wAAAAAAAA8ADgAAAAAAyv80AAAAAAAPAA4AAAAAAPb/CAAAAAAADwAOAAAAAAD9/wEAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAOAAAAAAD//wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD+/wAAAAAAAA8ADgAAAAAA/f8BAAAAAAAPAA4AAAAAAP3/AQAAAAAADwAOAAAAAAD//wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAOAAAAAAD+/wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAP3/AQAAAAAADwAOAAAAAAD+/wAAAAAAAA8ADgAAAAAA9v8IAAAAAAAPAAAAAAAAAP//AAAAAAAADwAOAAAAAAD9/wEAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD9/wEAAAAAAA8ADgAAAAAA9v8IAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD+/wAAAAAAAA8ADgAAAAAA9v8IAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAAAAAAAAD//wAAAAAAAA8ADgAAAAAA9v8IAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD+/wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD+/wAAAAAAAA8ADgAAAAAA+/8DAAAAAAAPAA4AAAAAAP3/AQAAAAAADwAOAAAAAAD+/wAAAAAAAA8ADgAAAAAA9v8IAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD2/wgAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAAAAAAAAD//wAAAAAAAA8ADgAAAAAA+/8DAAAAAAAPAA4AAAAAAOz/EgAAAAAADwAOAAAAAAD9/wEAAAAAAA8ADgAAAAAA9v8IAAAAAAAPAA4AAAAAAP3/AQAAAAAADwAOAAAAAAC+/kABAAAAAA8ADgAAAAAA/v8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAPv/AwAAAAAADwAOAAAAAAD+/wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAP3/AQAAAAAADwAOAAAAAAD+/wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD+/wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAAAAAAAAD//wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD+/wAA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AAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8ADgAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAA4AAAAAAP7/AAAAAAAADwAOAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAOAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8ADgAAAAAA/v8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA0AAAAAAAAA//8AAAAAAAANAAAAAAAAAP//AAAAAAAADQAAAAAAAAD//wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD//wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAO7+EAEAAAAADgAPAAAAAADv/w8AAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAPD/DgAAAAAADgAPAAAAAAD4/wYAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAKH/XQAAAAAADgAPAAAAAAC7/0MAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAPv/AwAAAAAADgAPAAAAAAD9/wEAAAAAAA4AAAAAAAAA//8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD//wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAPD/DgAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA7/8PAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAC7/0MAAAAAAA4ADwAAAAAA/f8BAAAAAAAOAA8AAAAAAP//AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAA8AAAAAAP//AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAO3/EQAAAAAADgAAAAAAAAD//wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAO//DwAAAAAADgAPAAAAAADr/xMAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD//wAAAAAAAA4ADwAAAAAA9/8HAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD9/wEAAAAAAA4ADwAAAAAA//8AAAAAAAAOAA8AAAAAAP3/AQAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA7f8RAAAAAAAOAA8AAAAAAO//DwAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP3/AQAAAAAADgAPAAAAAAD//wAAAAAAAA4ADwAAAAAA/f8BAAAAAAAOAA8AAAAAAPv/AwAAAAAADgAPAAAAAAD3/wcAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAPAAAAAAD3/wcAAAAAAA4ADwAAAAAA6/8TAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD//wAAAAAAAA4ADwAAAAAA+/8DAAAAAAAOAA8AAAAAAPf/BwAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA//8AAAAAAAAOAA8AAAAAAOf/FwAAAAAADgAPAAAAAAD9/wEAAAAAAA4ADwAAAAAA7f8RAAAAAAAOAA8AAAAAAPf/BwAAAAAADgAPAAAAAAD0/woAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP3/AQAAAAAADgAPAAAAAAD7/wMAAAAAAA4ADwAAAAAA9/8HAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD3/wcAAAAAAA4ADwAAAAAA+/8DAAAAAAAOAA8AAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAOf/FwAAAAAADgAAAAAAAAD//wAAAAAAAA4ADwAAAAAA9P8KAAAAAAAOAA8AAAAAAPv/AwAAAAAADgAPAAAAAADt/xEAAAAAAA4ADwAAAAAA5/8XAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD5/wUAAAAAAA4ADwAAAAAA//8AAAAAAAAOAA8AAAAAAPT/CgAAAAAADgAPAAAAAACx/00AAAAAAA4ADwAAAAAA1f0pAgAAAAAOAA8AAAAAAPf/BwAAAAAADgAPAAAAAAD3/wcAAAAAAA4ADwAAAAAArv9QAAAAAAAOAA8AAAAAAHX/iQAAAAAADgAPAAAAAAD7/wMAAAAAAA4ADwAAAAAA//8AAAAAAAAOAA8AAAAAAPv/AwAAAAAADgAPAAAAAAD//wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAA8AAAAAAOf/FwAAAAAADgAPAAAAAAD5/wUAAAAAAA4ADwAAAAAA+/8DAAAAAAAOAA8AAAAAANX9KQIAAAAADgAPAAAAAACp/1UAAAAAAA4ADwAAAAAA9P8KAAAAAAAOAA8AAAAAAPf/BwAAAAAADgAPAAAAAAC3/0cAAAAAAA4ADwAAAAAAsf9NAAAAAAAOAAAAAAAAAP//AAAAAAAADgAPAAAAAAD7/wMAAAAAAA4ADwAAAAAA//8AAAAAAAAOAA8AAAAAAK7/UAAAAAAADgAPAAAAAAD//wAAAAAAAA4ADwAAAAAAdf+JAAAAAAAOAA8AAAAAAP//AAAAAAAADgAPAAAAAAD9/wEAAAAAAA4ADwAAAAAArv9QAAAAAAAOAA8AAAAAAP//AAAAAAAADgAPAAAAAAD7/wMAAAAAAA4ADwAAAAAA//8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAPAAAAAAD5/wUAAAAAAA4ADwAAAAAA//8AAAAAAAAOAA8AAAAAAKn/VQAAAAAADgAPAAAAAADu/xAAAAAAAA4ADwAAAAAA9v8IAAAAAAAOAA8AAAAAANX9KQIAAAAADgAPAAAAAACx/00AAAAAAA4ADwAAAAAAy/8zAAAAAAAOAA8AAAAAAOL8HAMAAAAADgAPAAAAAAC3/0cAAAAAAA4ADwAAAAAArv9QAAAAAAAOAA8AAAAAAP//AAAAAAAADgAPAAAAAAD//wAAAAAAAA4ADwAAAAAA//8AAAAAAAAOAA8AAAAAAPn/BQAAAAAADgAPAAAAAAD//wAAAAAAAA4ADwAAAAAA//8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAADV/SkCAAAAAA4ADwAAAAAA4vwcAwAAAAAOAA8AAAAAAO7/EAAAAAAADgAPAAAAAACx/00AAAAAAA4AAAAAAAAA//8AAAAAAAAOAA8AAAAAAP//AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA9v8IAAAAAAAOAA8AAAAAAP3/AQAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA7v8QAAAAAAAOAA8AAAAAAOL8HAMAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAAz/8vAAAAAAAOAA8AAAAAAMv/MwAAAAAADgAPAAAAAAD//wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD//wAAAAAAAA4ADwAAAAAA//8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA//8AAAAAAAAOAA8AAAAAAO7/EAAAAAAADgAPAAAAAADP/y8AAAAAAA4ADwAAAAAA/f8BAAAAAAAOAA8AAAAAAOL8HAMAAAAADgAPAAAAAAD9/wEAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD//wAAAAAAAA4ADwAAAAAA//8AAAAAAAAOAA8AAAAAAPv/AwAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAPn/BQAAAAAADgAPAAAAAAD4/wYAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA9/8HAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD9/wEAAAAAAA4ADwAAAAAAYf+dAAAAAAAOAA8AAAAAAOf/FwAAAAAADgAPAAAAAADP/y8AAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAPj/BgAAAAAADgAPAAAAAAAy/8wAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD7/wMAAAAAAA4ADwAAAAAA+f8FAAAAAAAOAAAAAAAAAP//AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA9/8HAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAADn/xcAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP3/AQAAAAAADgAPAAAAAADP/y8AAAAAAA4ADwAAAAAAvf5BAQAAAAAOAA8AAAAAAGH/nQAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA9/8HAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAOf/FwAAAAAADgAPAAAAAADw/w4AAAAAAA4ADwAAAAAA7v4QAQAAAAAOAA8AAAAAAGH/nQAAAAAADgAPAAAAAAC7/0MAAAAAAA4ADwAAAAAA+/8DAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD//wAAAAAAAA4ADwAAAAAA+f8FAAAAAAAOAA8AAAAAAPv/AwAAAAAADgAPAAAAAAD+/wAAAAAAAA4AAAAAAAAA//8AAAAAAAAOAA8AAAAAAPf/BwAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAL3+QQEAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAAhf55AQAAAAAOAA8AAAAAAGH/nQAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA8P8OAAAAAAAOAA8AAAAAAL3+QQEAAAAADgAPAAAAAAD4/wYAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAALv/QwAAAAAADgAPAAAAAAD5/wUAAAAAAA4ADwAAAAAAMv/MAAAAAAAOAA8AAAAAAKH/XQAAAAAADgAPAAAAAACV/mkBAAAAAA4ADwAAAAAA+P8GAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD//wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD2/wgAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAPb/CAAAAAAADgAPAAAAAAD//wAAAAAAAA4ADwAAAAAAqf9VAAAAAAAOAA8AAAAAAP//AAAAAAAADgAPAAAAAACp/1UAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAPv/AwAAAAAADgAPAAAAAAD9/wEAAAAAAA4ADwAAAAAA+/8DAAAAAAAOAA8AAAAAAP//AAAAAAAADgAAAAAAAAD//wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAAAAAAAAAP//AAAAAAAADgAPAAAAAAD9/wEAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAL3+QQEAAAAADgAPAAAAAACV/mkBAAAAAA4ADwAAAAAAhf55AQAAAAAOAA8AAAAAADL/zAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAADL/zAAAAAAADgAPAAAAAAC9/kEBAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAMv/MwAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP3/AQAAAAAADgAPAAAAAAC3/0cAAAAAAA4ADwAAAAAA/f8BAAAAAAAOAA8AAAAAAMv/MwAAAAAADgAPAAAAAAB1/4kAAAAAAA4AAAAAAAAA//8AAAAAAAAOAA8AAAAAAHX/iQAAAAAADgAPAAAAAAC3/0cAAAAAAA4ADwAAAAAA9/8HAAAAAAAOAA8AAAAAAOv/EwAAAAAADgAPAAAAAAD3/wcAAAAAAA4AAAAAAAAA//8AAAAAAAAOAA8AAAAAAP3/AQAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/f8BAAAAAAAOAA8AAAAAAOv/EwAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAABz/4gAAAAAADgAPAAAAAAD7/wMAAAAAAA4ADwAAAAAA5/8XAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD7/wMAAAAAAA4ADwAAAAAA+P8GAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAPf+BwEAAAAADgAPAAAAAAD9/wEAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAPv/AwAAAAAADgAPAAAAAAAc/+IAAAAAAA4ADwAAAAAAlf5pAQAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAAof9dAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/f8BAAAAAAAOAA8AAAAAAPv/AwAAAAAADgAPAAAAAACF/nkBAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAACV/mkBAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP//AAAAAAAADgAPAAAAAAD4/wYAAAAAAA4ADwAAAAAA+/8DAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA+/8DAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD4/wYAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAO7+EAEAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA9/4HAQAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/f8BAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD+/wAAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAP7/AAAAAAAADgAPAAAAAAD9/wEAAAAAAA4ADwAAAAAA/v8AAAAAAAAOAA8AAAAAAOf/FwAAAAAADgAPAAAAAADu/hABAAAAAA4ADwAAAAAAHP/iAAAAAAAOAA8AAAAAAPf+BwEAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAADwAAAAAA9v8IAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAAAAAAAAD//wAAAAAAABAAAAAAAAAA//8AAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAPAAAAAAD3/wcAAAAAABAADwAAAAAA+v8EAAAAAAAQAAAAAAAAAP//AAAAAAAAEAAPAAAAAADp/xUAAAAAABAAAAAAAAAA//8AAAAAAAAQAA8AAAAAAPf/BwAAAAAAEAAPAAAAAABen6BgAAAAABAADwAAAAAATpmwZgAAAAAQAA8AAAAAAL+EP3sAAAAADwAQAAAAAAAOnPBjAAAAAA8AEAAAAAAAmZ5lYQAAAAAPABAAAAAAADafyGAAAAAAEAAPAAAAAABOmbBmAAAAABAADwAAAAAAP8a/OQAAAAAPABAAAAAAADafyGAAAAAADwAQAAAAAACZnmVhAAAAABAADwAAAAAAP8a/OQAAAAAQAA8AAAAAAE6ZsGYAAAAADwAQAAAAAACZnmVhAAAAAA8AEAAAAAAADpzwYwAAAAAQAA8AAAAAAF6foGAAAAAADwAQAAAAAAAThOt7AAAAAA8AEAAAAAAANp/IYAAAAAAPABAAAAAAABOE63sAAAAAEAAPAAAAAAA/xr85AAAAABAADwAAAAAAv4Q/ewAAAAAPABAAAAAAABOE63sAAAAAEAAPAAAAAABen6BgAAAAAA8AEAAAAAAADpzwYwAAAAAPABAAAAAAAJmeZWEAAAAADwAQAAAAAABoqpZVAAAAAA8AEAAAAAAAxqU4WgAAAAAPABAAAAAAAGiqllUAAAAADwAQAAAAAABoqpZVAAAAAA8AEAAAAAAAaKqWVQAAAAAPABAAAAAAALWmSVkAAAAADwAQAAAAAAAOnPBjAAAAABAADwAAAAAAv4Q/ewAAAAAQAA8AAAAAAE6ZsGYAAAAAEAAPAAAAAABen6BgAAAAAA8AEAAAAAAAE4TrewAAAAAPABAAAAAAADafyGAAAAAAEAAPAAAAAAC/hD97AAAAABAADwAAAAAAP8a/OQAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAAA8AAAAAAAAA//8AAAAAAAAPAAAAAAAAAP//AAAAAAAADwAAAAAAAAD//wAAAAAAABYAFwAAAAAATP6yAQAAAAAWABcAAAAAAJL/bAAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA8f8NAAAAAAAWABcAAAAAAPr/BAAAAAAAFgAXAAAAAAD8/wIAAAAAABYAFwAAAAAALv/QAAAAAAAWABcAAAAAAIH/fQAAAAAAFgAAAAAAAAD//wAAAAAAABYAFwAAAAAA3P8iAAAAAAAWABcAAAAAAHj/hgAAAAAAFgAXAAAAAAD6/wQAAAAAABYAFwAAAAAA/v8AAAAAAAAWABcAAAAAAOb/GAAAAAAAFgAXAAAAAABr/5MAAAAAABYAFwAAAAAAkv9sAAAAAAAWABcAAAAAAPz/AgAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA8f8NAAAAAAAWABcAAAAAAC7/0AAAAAAAFgAXAAAAAADc/yIAAAAAABYAFwAAAAAA+v8EAAAAAAAWABcAAAAAAIH/fQAAAAAAFgAXAAAAAAB4/4YAAAAAABYAFwAAAAAA5v8YAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAXAAAAAAD6/wQAAAAAABYAFwAAAAAAa/+TAAAAAAAWABcAAAAAAP//AAAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA5v8YAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAFwAAAAAA/v8AAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAXAAAAAABu/5AAAAAAABYAFwAAAAAA9P8KAAAAAAAWABcAAAAAAPz/AgAAAAAAFgAXAAAAAAD6/wQAAAAAABYAFwAAAAAA/P8CAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAXAAAAAADc/yIAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA5v8YAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAXAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWABcAAAAAAEz+sgEAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA9P8KAAAAAAAWABcAAAAAAPz/AgAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAAbv+QAAAAAAAWABcAAAAAAPr/BAAAAAAAFgAXAAAAAADc/yIAAAAAABYAFwAAAAAA/v8AAAAAAAAWABcAAAAAAPz/AgAAAAAAFgAAAAAAAAD//wAAAAAAABYAFwAAAAAA9P8KAAAAAAAWABcAAAAAAPv/AwAAAAAAFgAXAAAAAADm+hgFAAAAABYAFwAAAAAAbv+QAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA/P8CAAAAAAAWABcAAAAAAPz/AgAAAAAAFgAXAAAAAAD+/wAAAAAAABYAAAAAAAAA//8AAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWABcAAAAAAP//AAAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAATP6yAQAAAAAWABcAAAAAAPT/CgAAAAAAFgAXAAAAAADm+hgFAAAAABYAFwAAAAAAbv+QAAAAAAAWABcAAAAAAPz/AgAAAAAAFgAXAAAAAAD7/wMAAAAAABYAFwAAAAAA/v8AAAAAAAAWABcAAAAAAPz/AgAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA/v8AAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAFwAAAAAA/v8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAFwAAAAAA/v8AAAAAAAAWABcAAAAAAP//AAAAAAAAFgAXAAAAAAD//wAAAAAAABYAFwAAAAAATP6yAQAAAAAWABcAAAAAAOb6GAUAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA+/8DAAAAAAAWABcAAAAAAPX/CQAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA/P8CAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAFwAAAAAA/v8AAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAFwAAAAAA//8AAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA5voYBQAAAAAWABcAAAAAAP7/AAAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA+/8DAAAAAAAWABcAAAAAAPz/AgAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA9f8JAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA/v8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAFwAAAAAA/v8AAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA9f8JAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAXAAAAAAD5/wUAAAAAABYAFwAAAAAA/v8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA8P8OAAAAAAAWABcAAAAAAP3/AQAAAAAAFgAXAAAAAAD//wAAAAAAABYAFwAAAAAA/v8AAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAXAAAAAACL/XMCAAAAABYAFwAAAAAA/v8AAAAAAAAWABcAAAAAAPX/CQAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA/v8AAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAFwAAAAAA/f8BAAAAAAAWABcAAAAAAPn/BQAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA//8AAAAAAAAWABcAAAAAAP//AAAAAAAAFgAXAAAAAADw/w4AAAAAABYAFwAAAAAATP6yAQAAAAAWABcAAAAAAP7/AAAAAAAAFgAXAAAAAACL/XMCAAAAABYAFwAAAAAAi/1zAgAAAAAWABcAAAAAAP7/AAAAAAAAFgAXAAAAAAC5/kUB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AAAAAP7/AAAAAAAAFgAXAAAAAAD+/wAAAAAAABYAFwAAAAAA/v8AAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAXAAAAAADt/xEAAAAAABYAFwAAAAAA/v8AAAAAAAAWABcAAAAAAPn/BQAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAFwAAAAAAXf+hAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWABcAAAAAAP7/AAAAAAAAFgAXAAAAAADt/xEAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAFwAAAAAA6f4VAQAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAFwAAAAAAnv9gAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAAAAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAAAAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAPn/BQAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD9/wEAAAAAABUAFgAAAAAA/f8BAAAAAAAVABYAAAAAAOz/EgAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAAWv6kAQAAAAAVABYAAAAAAADx/g4AAAAAFQAWAAAAAAD5/wUAAAAAABUAFgAAAAAA/f8BAAAAAAAVABYAAAAAANb/KAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA7/8PAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA/f8BAAAAAAAVABYAAAAAAPb/CAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP3/AQAAAAAAFQAWAAAAAADW/ygAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP3/AQAAAAAAFQAWAAAAAAD2/wgAAAAAABUAFgAAAAAA+v8EAAAAAAAVABYAAAAAAP3/AQAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA1f8pAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD9/wEAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAPz/AgAAAAAAFQAWAAAAAADV/ykAAAAAABUAFgAAAAAA/f8BAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAPz/AgAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAANX/KQAAAAAAFQAWAAAAAADt/xEAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAACE+noFAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD5/wUAAAAAABUAFgAAAAAA1f8pAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA//8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAACE+noFAAAAABUAFgAAAAAA+f8FAAAAAAAVABYAAAAAAO3/EQAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD5/wUAAAAAABUAFgAAAAAAWv6kAQAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAACE+noFAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAACE+noFAAAAABUAFgAAAAAAWv6kAQAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD//wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP//AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA//8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP3/AQAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD//wAAAAAAABYAFQAAAAAAvv1AAgAAAAAWABUAAAAAAPT/CgAAAAAAFgAVAAAAAADX/ycAAAAAABYAFQAAAAAAKf/VAAAAAAAWABUAAAAAAJP/awAAAAAAFgAVAAAAAAD4/wYAAAAAABYAFQAAAAAAPv7AAQAAAAAWABUAAAAAAMX+OQEAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA//8AAAAAAAAWAAAAAAAAAP//AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAL79QAIAAAAAFgAVAAAAAADX/ycAAAAAABYAAAAAAAAA//8AAAAAAAAWABUAAAAAAD7+wAEAAAAAFgAVAAAAAAAI//YAAAAAABYAFQAAAAAAk/9rAAAAAAAWABUAAAAAAMX+OQEAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA+P8GAAAAAAAWABUAAAAAAID/fgAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAANf/JwAAAAAAFgAVAAAAAAD8/wIAAAAAABYAFQAAAAAACP/2AAAAAAAWABUAAAAAAN//HwAAAAAAFgAVAAAAAADk/xoAAAAAABYAFQAAAAAAPv7AAQAAAAAWABUAAAAAAPj/BgAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/P8CAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA//8AAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA1/8nAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD8/wIAAAAAABYAFQAAAAAA9P8KAAAAAAAWABUAAAAAAD7+wAEAAAAAFgAVAAAAAAD8/wIAAAAAABYAFQAAAAAA3/8fAAAAAAAWABUAAAAAAPj/BgAAAAAAFgAAAAAAAAD//wAAAAAAABYAFQAAAAAA//8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA/P8CAAAAAAAWABUAAAAAAPD/DgAAAAAAFgAVAAAAAAD9/wEAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD8/wIAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAOv/EwAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA3/8fAAAAAAAWABUAAAAAAP3/AQAAAAAAFgAVAAAAAAD2/wgAAAAAABYAFQAAAAAA/P8CAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA8f8NAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA//8AAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/P8CAAAAAAAWABUAAAAAAOv/EwAAAAAAFgAVAAAAAADk/xoAAAAAABYAFQAAAAAA9v8IAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAADf/x8AAAAAABYAFQAAAAAA6/8TAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD1/wkAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAPb/CAAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP3/AQAAAAAAFgAVAAAAAADx/w0AAAAAABYAFQAAAAAA/P8CAAAAAAAWABUAAAAAAPr/BAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA//8AAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAOv/EwAAAAAAFgAVAAAAAAD1/wkAAAAAABYAFQAAAAAA//8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA9v8IAAAAAAAWABUAAAAAAP3/AQAAAAAAFgAAAAAAAAD//wAAAAAAABYAFQAAAAAA//8AAAAAAAAWABUAAAAAAPD/DgAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAPz/AgAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA+v8EAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAFQAAAAAA/P8CAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD1/wkAAAAAABYAFQAAAAAA7f8RAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAFQAAAAAA/P8CAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAPX/CQAAAAAAFgAVAAAAAAD8/wIAAAAAABYAFQAAAAAA7f8RAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAO3/EQAAAAAAFgAVAAAAAAAU/+oAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/f8BAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA7f8RAAAAAAAWABUAAAAAAOj/FgAAAAAAFgAVAAAAAAAU/+oAAAAAABYAFQAAAAAA/P8CAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA//8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAAAAAAAAD//wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP3/AQAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD9/wEAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAOv/EwAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/f8BAAAAAAAWABUAAAAAABT/6gAAAAAAFgAVAAAAAABV/6kAAAAAABYAFQAAAAAA8P8OAAAAAAAWABUAAAAAAOj/FgAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA5v8YAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/f8BAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP3/AQAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA6/8TAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAAU/+oAAAAAABYAFQAAAAAA8P8OAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAOb/GAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA8P8OAAAAAAAWABUAAAAAACn/1QAAAAAAFgAVAAAAAAC+/UACAAAAABYAFQAAAAAAVf+pAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAADF/jkBAAAAABYAFQAAAAAAk/9rAAAAAAAWABUAAAAAAOb/GAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA6/8TAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAPD/DgAAAAAAFgAVAAAAAAC+/UACAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAJP/awAAAAAAFgAAAAAAAAD//wAAAAAAABYAFQAAAAAA//8AAAAAAAAWABUAAAAAAOb/GAAAAAAAFgAVAAAAAACA/34AAAAAABYAFQAAAAAAxf45AQAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAADr/xMAAAAAABUAFgAAAAAA/f8BAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAPb/CAAAAAAAFQAWAAAAAAD9/wEAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP3/AQAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD9/wEAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD5/wUAAAAAABUAFgAAAAAA+v8EAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD6/wQAAAAAABUAFgAAAAAA9v8IAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAFgAAAAAA/v8AAAAAAAAVABYAAAAAAPr/BAAAAAAAFQAWAAAAAAD9/wEAAAAAABUAFgAAAAAA/v8AAAAAAAAVAAAAAAAAAP//AAAAAAAAFQAAAAAAAAD//wAAAAAAABUAFgAAAAAA0v8sAAAAAAAVABYAAAAAAP7/AAAAAAAAFQAWAAAAAAD+/wAAAAAAABUAAAAAAAAA//8AAAAAAAAWABUAAAAAAFX/qQAAAAAAFgAAAAAAAAD//wAAAAAAABYAFQAAAAAAKf/VAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAADo/xYAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAFX/qQAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/P8CAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAADo/xYAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/P8CAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD//wAAAAAAABYAFQAAAAAA//8AAAAAAAAWABUAAAAAAP//AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA5P8aAAAAAAAWABUAAAAAAPT/CgAAAAAAFgAVAAAAAADk/xoAAAAAABYAFQAAAAAA//8AAAAAAAAWABUAAAAAAAj/9gAAAAAAFgAVAAAAAAAp/9UAAAAAABYAFQAAAAAACP/2AAAAAAAWABUAAAAAAPT/CgAAAAAAFgAAAAAAAAD//wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAACA/34AAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/v8AAAAAAAAWABUAAAAAAP3/AQAAAAAAFgAVAAAAAAD+/wAAAAAAABYAFQAAAAAA/f8BAAAAAAAWABUAAAAAAP7/AAAAAAAAFgAAAAAAAAD//wAA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"),
-"uv_scale": Vector4(0, 0, 0, 0),
-"vertex_count": 9396,
-"vertex_data": PackedByteArray("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/iCAvsyEBT62Vy+9v7V6vcyEBT62Vy+9/jSAvQbKFT4ka1c9XbqZvQbKFT6Ysx29zqYjvgbKFT4tkE8+ER7evQbKFT7zVTg+/iCAvsyEBT5t9Q8+Oid+vgbKFT72Cj09i2RxvgbKFT6x2u09RyKJvsyEBT6teIg9v7V6vcyEBT62Vy+9ER7evQbKFT7zVTg+tLKevQbKFT56Rvk9yJFevsyEBT69IVY+i2RxvgbKFT6x2u09/iCAvsyEBT5t9Q8+/iCAvsyEBT62Vy+9X+huvgbKFT4sizS9yJFevsyEBT7mDOG9/jSAvQbKFT4ka1c9v7V6vcyEBT5t9Q8+UcDeDf///7+fVpcI////vw7BMBP///+/ErAKI////78SsAoj////v8zV3BHTuG/3zNXcEdO4b/eB1/gZwLhz94HX+Bn///+/Wr/pLf///79av+kt////vzIq3BH///+/7E8KI////7/wPjAT////v6RA6S3///+/7E8KI////7+kQOkt////vzIq3BErR48IfSj4GT5Hiwh9KPgZ////vznDz6SWT4rrOcPPpGNKMe85w8+k2VLW6DnDz6T3R+TwB+Er4sZkoOYH4Svis2mQ573jzumGd7Ds//9r5f9///+9487pUn+67rb/+No+f5P///9r5f9//v+2//ja4H0a////a+X/f/5/q/4IJVd/337//2vl/3/+f///a+X///+/BqVHgDZ+7/4vlkGcUlqn/C+WQZyLVgz50p33nl1PJ/XSnfeeQU1i8pOa/3//f///k5r/f/9///8GpUeAKH+f/0STfMT///8/YKSzzP///z9gpLPM////PwitmcL///8/CK2Zwv///z9XcoHG////P6eNgcb///8/nluzzP///z+6bHzE////P/ZSmcL///8/9lKZwv///z+eW7PM////PwqjEjD///8/aHdfKf///z8vkw5B////P92NUUD///8/CqMSMP///z/PbA5B////P/RcEjD///8/6K1aP////z/orVo/////PxZSWj////8/9FwSMP///z8WUlo/////P325VvL///+/tEye2////7+BRlby////v31Euez///+/SrOe2////7+Bu7ns////v0qzntv///+/Zb320v///79lvfbS////v5lC9tL///+/mUL20v///7+0TJ7b////vy7bxbyDXYHlLtvFvBtWI+gu28W8y2H64y7bxbyvUWzpNdu7fP///z8127t8////PzXbu3z///8/Ndu7fP///z+6/DVb////v7r8NVv///+/uvw1W////7+6/DVb////v+X/cKLlf+b/5f9wohB/0v//f/7/xQf/v/9///9I5f8//3//////Qjb/f///Afz/P47dGIDOf/L/jt0YgNV/3v+O3RiAqoD0/47dGICmf4f/B6WrfsB9q3+Tmv9//3/+f5Oa/3////8/B6Wrfv///z//fwAAhvD/v/9/AAD+/7PL/38AAP7//7/4mYJXFpGe49yRzFUekZXj3JHMVf///z/4mYJX////P7m/71f///8/ub/vV////z+5v+9X////P7m/71f///8/7te5P////7/u17k/////v+7XuT////+/7te5P////7/FPM+kaLB0FMU8z6Sbtc0QxTzPpAe4Gg/FPM+kJa0oF/ceK+I4m14ZQRzO6ayARBH3HiviS5ZuGEEczulDh/0SAABr5f9/AABIAPjawIBrAEgA+NoeguQALGL3nqGw1wrPaUGcrKVXA89pQZyIolYALGL3nr2ynA34WkeA1oBfAPhaR4DIgQ8Ba2X/f/9/AAD3Wqt+PoJTgGtl/3//fwCA91qrfv///z//fwAAVQX/P/9/AAAAAFky/38AAAAASzT/fwAAeA//P/9/AAArJP8/0CTFvHuifRrQJMW846nbF9AkxbxPrpIW0CTFvDOeBBxTAQgl////v1MBCCWngB+BAABr5f///78AAGvl/38AgHAiGIAwgAwAySS7fP///z/JJLt8////P3AiGIBYgHcAcCIYgCmAIABwIhiAVH8KAEQDNVv///+/RAM1W////79EAzVb////v0QDNVv///+/GQBwokCAFQAZAHCiGYAYABkAcKLqf1UAGQBwou6ALABFQO9X////P0VA71f///8/RUDvV////z9FQO9X////PwZmglfobmAcIm7MVeBuaRwGZoJX////PyJuzFX///8/ECi5P////78QKLk/////vxAouT////+/ECi5P////7///5vaAACw0v//m9oAALDS//+b2gAAsNL//5vaAACw0v//ZLIAAMzm//9ksgAAzOb//2SyAADM5v//ZLIAAMzm///3kgAAgvb///eSAACC9v//95IAAIL2Bu3/f///gvYG7f9///+C9prN/3///8zmms3/f///zOaazf9////M5prN/3///8zmYqX/f///sNJipf9///+w0mKl/3///7DSYqX/f///sNKcWv9/m9r/v5xa/3+b2v+/nFr/f5va/7+cWv9/m9r/v2Qy/39ksv+/ZDL/f2Sy/79kMv9/ZLL/v2Qy/39ksv+/+BL/f/eS/7/4Ev9/95L/v/gS/3/3kv+/AAD3kgdt/78AAPeSB23/vxmdBZ////8/m5qJrf///z8ZnQWf////P5uaia3///8/AABksppN/78AAGSymk3/vwAAZLKaTf+/AABksppN/78AAJvaYyX/vwAAm9pjJf+/AACb2mMl/78AAJvaYyX/v/9/AAD///8//38AAP7//z//fwAA////P/9/AAD+//8//38AAP7//z//fwAA////P/9/AAD///8//38AAP///z//fwAA/v//P/9/AAD///8/jXDNzf///z+8Zorf////P0OaquH///8/Q5qq4f///z+xqxbN////P+itxrP///8/2bstuP///z/orcaz////P41wzc3///8/jXDNzf///z9BZ2nX////P4Vyy+X///8/D0F4tP///z+8Zorf////P2JW58z///8/U1hQxP///z8PQXi0////P1hQ2LD///8/WFDYsP///z9QvnKT////P1C+cpP///8/UL5yk////z9QvnKT////PyngGo////8/KeAaj////z8p4BqP////PyngGo////8/zggu3f///798GTbh////v84ILt3///+/fBk24f///7/K9aXc////v3flP9////+/yvWl3P///7935T/f////v+Tw1Z////+/5PDVn////7/k8NWf////v80GhLb///+/zQaEtv///7/NBoS2////vxUfi8L///+/UTOKzP///78fXczh////vwsxW7z///+/CzFbvP///78fXczh////v/9bJvT///+/Q0093////7+xZnD4////v0jzGbr///+/lMlZz////79I8xm6////v5TJWc////+/lMlZz////7+7usXO////v83K6b////+/u7rFzv///7+3sGXi////v1+hBOb///+/X6EE5v///79Qlr77////v9ehf/b///+/16F/9v///7/8BuuY////v/wG65j///+//AbrmP///7/8BuuY////v+sY+4b///8/6xj7hv///z/rGPuG////P+sY+4b///8/2j6Djv///z/aPoOO////P9o+g47///8/2j6Djv///z90Y0ae////Py9nXq3///8/dGNGnv///z90Y0ae////Py9nXq3///8//3/+//7/mLr/f/7//v+Yuv9//v/+/5i6/3/+//7/mLr/f/7//v+YulXLB+f///+/VcsH5////79Vywfn////v1XLB+f///+//TCQ5v///7/9MJDm////v/0wkOb///+//TCQ5v///78AAJva//9OLQAAm9r//04tAACb2v//Ti0AAJva//9OLQAAZLL//zIZAABksv//MhkAAGSy//8yGQAAZLL//zIZAAD3kv//fAkAAPeS//98CfgS/38AAHwJ+BL/fwAAfAkAAPeS//98CWQy/38AADIZZDL/fwAAMhlkMv9/AAAyGWQy/38AADIZnFr/fwAATi2cWv9/AABOLZxa/38AAE4tnFr/fwAATi1ipf9/YyX/P2Kl/39jJf8/YqX/f2Ml/z9ipf9/YyX/P5rN/3+aTf8/ms3/f5pN/z+azf9/mk3/P5rN/3+aTf8/Bu3/fwdt/z////eS95L/P///95L3kv8////3kveS/z8G7f9/B23/P+ViBZ////8/5WIFn////z9jZYmt////P2Nlia3///8/5WIFn////z///2SyZLL/P///ZLJksv8///9ksmSy/z///2SyZLL/P///m9qb2v8///+b2pva/z///5vam9r/P///m9qb2v8//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/79lbijN////P7tlquH///8/Q5mK3////z+9mGnX////P5yp58z///8/cY/Nzf///z9xj83N////PzOHd+X///8/JleHw////z9NVBbN////P2VuKM3///8/ZW4ozf///z8lRC24////PyVELbj///8/FlLGs////z8WUsaz////P8Wx7cL///8/xbHtwv///z+rp1DE////P+++eLT///8/7754tP///z+mr9iw////P6unUMT///8/pq/YsP///z+uQXKT////P65BcpP///8/rkFyk////z+uQXKT////P9UfGo////8/1R8aj////z/VHxqP////P9UfGo////8/MPcu3f///7+C5jbh////v4LmNuH///+/MPcu3f///780CqXc////v4caP9////+/hxo/3////780CqXc////vxoP1Z////+/Gg/Vn////78aD9Wf////vxoP1Z////+/MfmEtv///7/p4IvC////v63Misz///+/MfmEtv///7+tzIrM////v8W6dcz///+/3cQaxP///7/zzlu8////v/POW7z///+//6Mm9P///7/foszh////v02ZcPj///+//6Mm9P///7+2DBm6////v6sjbcX///+/tgwZuv///7+2DBm6////v2884sX///+/ajZZz////78xNem/////v0NFxc7///+/MTXpv////78C+euY////vwL565j///+/AvnrmP///78C+euY////vxPn+4b///8/E+f7hv///z8T5/uG////PxPn+4b///8/JMGDjv///z8kwYOO////PyTBg47///8/JMGDjv///z+KnEae////P8+YXq3///8/z5herf///z/PmF6t////P4qcRp7///8//3///wAAmTr/f///AACZOv9///8AAJk6n14E5v///79HT2Xi////v59eBOb///+/rmm++////78nXn/2////v0dPZeL///+/qTQH5////7+pNAfn////v6k0B+f///+/qTQH5////78Bz5Dm////vwHPkOb///+/Ac+Q5v///78Bz5Dm////v1+plwj///+/rT/eDf///7+3dpbS////PyFyUUD///8//3///wr3/z/D9xmcgloAgMP3GZyCWgCAw/cZnIJaAIAg7BmcgdoAgCDsGZyB2gCAw/flY/9/vpIg7OVj/39ArSDs5WP/f0CtIOzlY/9/QK3l496TfSX+/+XjOoiCWv//5eM6iIJa///l4zqIglr//+XjxHf/f0Dt5eMgbP9/vdLl4yBs/3+90uXjIGz/f73Siebs1nylAICJ5uzWfKUAgInm7NZ8pQCAiebs1nylAIDdIDbwfKUAgN0gNvCCWgCA3SA28HylAIDdIDbwgloAgDYSL758pQCANhIvvnylAIA2Ei++fKUAgDYSL76CWgCAyO0vvoJaAIDI7S++gloAgMjtL758pQCAyO0vvoJaAIAh3zbwfKUAgCHfNvCCWgCAId828HylAIAh3zbwgloAgHUZ7NZ8pQCAdRns1nylAIB1GezWgloAgHUZ7NZ8pQCAOwjlY/9/vpI7CBmcgloAgDsIGZyCWgCA3hMZnIHaAIDeExmcgdoAgDsI5WP/f76SOwjlY/9/vpLeE+Vj/39Ard4T5WP/f0CtGhw6iHyl//8aHN6Tgdr//xkcxHf/f0DtGhw6iHyl//8ZHMR3/39A7RkcxHf/f0DtGhwgbP9/vdIaHCBs/3+90vkrL76B2gCA+SsvvoHaAID5Ky++fSUAgPkrL76B2gCABdQvvoHaAIAF1C++gdoAgAXUL759JQCABdQvvoHaAIClsDbwfSUAgKWwNvCB2gCApbA28H0lAIClsDbwgdoAgHc97NZ9JQCAdz3s1oHaAIB3PezWfSUAgHc97NaB2gCAh8Ls1n0lAICHwuzWfSUAgIfC7NZ9JQCAh8Ls1oHaAIBZTzbwfSUAgFlPNvCB2gCAWU828H0lAIBZTzbwgdoAgM7B+Kt9Jf7/zsH4q30l/v/OwfirfSX//87B+Kt9Jf7/yI9Yz4Ha/v/Ij1jPfSX+/8iPWM+B2v7/yI9Yz30l/v/tVne9gdr//+1Wd719Jf//7VZ3vYHa///tVne9fSX//xGpdr2B2v//Eal2vYHa//8RqXa9gdr//xGpdr19Jf//NnBYz4Ha/v82cFjPfSX+/zZwWM+B2v7/NnBYz30l/v8vPvirfSX//y8++Kt9Jf//Lz74q4Ha/v8vPvirfSX+/xGpdZmCWv//Eal1mYJa//8RqXWZfKX//xGpdZmCWv7/NnDdoIJa//82cN2gfKX//zZw3aCCWv7/NnDdoHyl/v8vPjaSglr//y8+NpKCWv//Lz42koJa/v8vPjaSfKX+/87BNpJ8pf7/zsE2knyl/v/OwTaSfKX//87BNpJ8pf7/yI/doIJa///Ij92gfKX//8iP3aCCWv7/yI/doHyl/v/tVnWZfKX//+1WdZl8pf//7VZ1mXyl///tVnWZglr//xGpiWb/f0DtEamJZv9/QO0RqYlm/39A7RGpiWb/f0DtNnAhX/9/QO02cCFf/39A7TZwIV//f0DtNnAhX/9/QO0vPsht/39A7S8+yG3/f0DtLz7Ibf9/QO0vPsht/39A7c7ByG3/f0DtzsHIbf9/QO3Owcht/39A7c7ByG3/f0DtyI8hX/9/QO3IjyFf/39A7ciPIV//f0DtyI8hX/9/QO3tVolm/39A7e1WiWb/f0Dt7VaJZv9/QO3tVolm/39A7c/BBlT/f73Sz8EGVP9/vdLPwQZU/3+90s/BBlT/f73SwrTID////78Zt+cY////vxm35xj///+/VcEtKf///79VwS0p////v/e6TRD///+/wrTID////7/3uk0Q////v4fCEin/f0Cth8ISKf9/QK2HwhIp/39Are1Wh0L/f73S7VaHQv9/vdLtVodC/3+90u1Wh0L/f73SEamHQv9/vdIRqYdC/3+90hGph0L/f73SEamHQv9/vdIvPgZU/3+90i8+BlT/f73SLz4GVP9/vdIvPgZU/3+90siPwjT///8/TZD4Ov///z/Ij8I0////P02Q+Dr///8/55gZN////z/nmBk3////PyypVUH///8/LKlVQf///z93PRIp/39ArXc9Ein/f0Ctdz0SKf9/QK13PRIp/39Arak+LSn///+/qT4tKf///7/5K89B/39Arfkrz0H/f0Ct+SvPQf9/QK35K89B/39ArQXUz0H/f0CtBdTPQf9/QK0F1M9B/39ArQXUz0H/f0CtieYSKf9/vpKJ5hIp/3++konmEin/f76SieYSKf9/vpLdIMgP/3++kt0gyA//f76S3SDID/9/vpLdIMgP/3++kjYSz0H/f76SNhLPQf9/vpI2Es9B/3++kjYSz0H/f76SyO3PQf9/vpLI7c9B/3++ksjtz0H/f76SyO3PQf9/vpIh38gP/3++kiHfyA//f76SId/ID/9/vpIh38gP/3++knUZEin/f76SdRkSKf9/vpJ1GRIp/3++knUZEin/f76SNnDCNP///z+xb/g6////PxdnGTf///8/sW/4Ov///z/SVlVB////P9JWVUH///8/F2cZN////z88S8gP////vwdFTRD///+/PEvID////78HRU0Q////vwAAgdr///+/AACB2v///78AAIHa////vwAAgdr///+/fKX/f////z98pf9/////P3yl/3////8/fKX/f////z///4Ha////v///gdr///+///+B2v///7///4Ha////v30l/3////8/fSX/f////z99Jf9/////P30l/3////8//38AAMJq/7//fwAAglr/v/9/AADPjv+//38AAM+O/7//fwAAfKX/v/9/AADPjv+//38AAHyl/7//fwAA/3//v/9/AADDav+//38AAIJa/7///3yl////v///fKX///+///98pf///7///3yl////v4Ja/3////8/glr/f////z+CWv9/////P4Ja/3////8/AAB8pf///78AAHyl////vwAAfKX///+/AAB8pf///7+B2v9/////P4Ha/3////8/gdr/f////z+B2v9/////P/7//v+M18ar/v///4zXxqsAAP7/jNc41P////+poqnuAAD+/6miqe4AAP7/qaKp7v////+poqnu/v///6miVJH+////qaJUkf7/AAByKMar/v///1VdVJEAAAAAcig41AAA/v9yKMarAAD+/1Vdqe4AAP//cig41AAA/v9VXanu+0sZI4zXxqv7SxkjjNfGq/tLGSOM18ar+0sZI4zXxqsOXVs0jNfGqw5dWzSM18arDl1bNIzXxqsOXVs0jNfGq/9//3+M18ar/3//f4zXxqv/f/9/jNfGq/9//39VXVSR/3//f1VdVJH/f/9/jNc41P9//3+M1zjU/3//f6miqe7/f/9/VV1Ukf9//39VXVSR/3//f1VdVJH/f/9/cig41P9//39yKMar/3//f3Ioxqv/f/9/cig41P9//39yKDjU/3//f3IoONT/f/9/VV2p7v9//39VXanuaDisWP///z94Pwho////P3g/CGj///8/5Erra////z/kSutr////PzE96lP///8/aDisWP///z8xPepT////P3g/9pf///8/MT0UrP///z9pOFKn////P2k4Uqf///8/MT0UrP///z94P/aX////P+RKE5T///8/5EoTlP///z9SJ9y3////v0cYn77///+/Uifct////79HGJ++////vxQsMb3///+/FCwxvf///78UFOPK////vxQU48r///+/+0vl3HIoxqv7S+XccijGq/tL5dxyKMar+0vl3HIoxqsOXaLLcijGqw5dostyKMarDl2iy3IoxqsOXaLLcijGq5mjEc1yKDjUmaMRzXIoONSZoxHNcig41JmjEc1yKDjUKLOA3HIoONQos4Dccig41CizgNxyKDjUKLOA3HIoONQs4QGtVV2p7qrTEp9VXanuqtMSn1Vdqe4s4QGtVV2p7izhAa1VXanuLOH9Uqmiqe4s4f1SqaKp7qrT7GCpoqnuqtPsYKmiqe4s4f1SqaKp7iizfiOM1zjUKLN+I4zXONQos34jjNc41CizfiOM1zjUmaPtMozXONSZo+0yjNc41Jmj7TKM1zjUmaPtMozXONRHGF9B////vxQUGzX///+/FBQbNf///79HGF9B////vxQszUL///+/UiciSP///79SJyJI////vxQszUL///+/ZacAAP///79lpwAA////v2WnAAD///+/ZacAAP///7//f5jY////P/9/mNj///8//3+Y2P///z//f5jY////P/9/Zif///8//39mJ////z//f2Yn////P/9/Zif///8/XNz//////79c3P//////v1zc//////+/XNz//////7+p6fX38YeU8Knp9fd9lA7rMen0+FaqheEx6fT4gJ875o8xjrFFsf3mjzGOsZ+vr+WPMY6xIb3f6o8xjrE/qm7hCIhVljer/fQJh8yWsLST7wiIVZbTngX8CYfMlvO80+pXm6bkWBurzX+kftuAJD/SV5um5Fgbq81/pH7bgCQ/0ln2WXaFijX7WfZZdn6PaftZ9ll25Ydx8Fn2WXYbnwv8AAD/f6aHAIAAAP9//3/QkQAA/3+howCAAAD/f/9/l4kAAP9//3/QkQAA/3//fwCAAAD/f/9/l4kAAP9/XVwAgP9/oaP///8//3+ho////z//f6Gj////P/9/oaP///8/Zaf//////79lp///////v2Wn//////+/Zaf//////7//f11c////P/9/XVz///8//39dXP///z//f11c////P1zcAAD///+/XNwAAP///79c3AAA////v1zcAAD///+/GZw7CAAAQK0ZnDsIAABAreVjOwh9Jf+/GZzeEwAAQO0ZnN4TAABA7eVjOwh9Jf+/5WPeE4Ja/7/lY94Tglr/v+Vj3hOCWv+/3pMZHP7/QO06iBkc/v9ArTqIGRz+/0CtxHcZHIHa/7/Edxkcgdr/vyBsGRx8pf+/IGwZHHyl/78gbBkcfKX/v+zWdRkAAECt7NZ1GQAAQK3s1nUZAABArezWdRkAAECtNvAh3wAAQK028CHfAAC90jbwId8AAECtNvAh3wAAvdIvvsjtAAC90i++yO0AAL3SL77I7QAAvdIvvsjtAAC90i++NhIAAL3SL742EgAAvdIvvjYSAAC90i++NhIAAL3SNvDdIAAAvdI28N0gAAC90jbw3SAAAECtNvDdIAAAvdLs1onmAABArezWieYAAECt7NaJ5gAAQK3s1onmAABAreVjw/d9Jf+/GZzD9wAAQK0ZnMP3AABArd6T5eP+/0DtGZwg7AAAvpLlYyDsglr/v+Vjw/d9Jf+/5WMg7IJa/7/lYyDsglr/vzqI5OP+/73S3pPl4/7/QO3Ed+Xjgdr/vzqI5OP+/73SIGzl43yl/7/Ed+Xjgdr/vyBs5eN8pf+/IGzl43yl/78vvgXUAAC+ki++BdQAAL6SL74F1AAAvpIvvgXUAAC+ki+++SsAAEDtL775KwAAQO0vvvkrAAC+ki+++SsAAEDtNvBZTwAAQO028FlPAAC+kjbwWU8AAEDtNvBZTwAAvpLs1ofCAABA7ezWh8IAAEDt7NaHwgAAQO3s1ofCAABA7ezWdz0AAL6S7NZ3PQAAvpLs1nc9AAC+kuzWdz0AAL6SNvClsAAAQO028KWwAAC+kjbwpbAAAEDtNvClsAAAvpL4qy8+//++kvirLz7//76S+KsvPv7/QO34qy8+/v++kljPNnD//76SWM82cP7/QO1YzzZw/v++kljPNnD+/0Dtd70Rqf7/vpJ3vRGp/v++kne9Ean+/76Sd70Rqf7/vpJ3ve1W/v9A7Xe97Vb+/0Dtd73tVv7/QO13ve1W/v9A7VjPyI///0DtWM/Ij/7/QO1Yz8iP/v++kljPyI/+/0Dt+KvPwf//vpL4q8/B/v++kvirz8H+/0Dt+KvPwf7/vpJ1me1W/v9ArXWZ7Vb//73SdZntVv7/QK11me1W/v+90t2gyI/+/73S3aDIj///QK3doMiP/v+90t2gyI/+/0CtNpLPwf7/vdI2ks/B/v+90jaSz8H+/73SNpLPwf7/vdI2ki8+/v9ArTaSLz7+/0CtNpIvPv//vdI2ki8+/v9Ard2gNnD+/73S3aA2cP//QK3doDZw/v+90t2gNnD+/0CtdZkRqf//vdJ1mRGp/v+90nWZEan+/0CtdZkRqf7/vdKJZu1Wgdr/v4lm7VaB2v+/iWbtVoHa/7+JZu1Wgdr/vyFfyI+B2v+/IV/Ij4Ha/78hX8iPgdr/vyFfyI+B2v+/yG3PwYHa/7/Ibc/Bgdr/v8htz8GB2v+/yG3PwYHa/7/IbS8+gdr/v8htLz6B2v+/yG0vPoHa/7/IbS8+gdr/vyFfNnCB2v+/IV82cIHa/78hXzZwgdr/vyFfNnCB2v+/iWYRqYHa/7+JZhGpgdr/v4lmEamB2v+/iWYRqYHa/78GVC8+fKX/vwZULz58pf+/BlQvPnyl/78GVC8+fKX/v8gPPEv///+/6BjlSP///7/oGOVI////v8gPPEv///+/yA88S////7/IDzxL////vxIpdz2CWv+/Eil3PYJa/78SKXc9glr/v4dCEal8pf+/h0IRqXyl/7+HQhGpfKX/v4dCEal8pf+/h0LtVnyl/7+HQu1WfKX/v4dC7VZ8pf+/h0LtVnyl/7/oGBm3////v8gPwrT///+/yA/CtP///7/oGBm3////v8gPwrT///+/yA/CtP///78SKYfCglr/vxIph8KCWv+/EimHwoJa/78GVM/BfKX/vwZUz8F8pf+/BlTPwXyl/78GVM/BfKX/v8I0NnD///8/wjQ2cP///z/CNDZw////P8I0NnD///8/GTcXZ////z8ZNxdn////P89BBdSCWv+/z0EF1IJa/7/PQQXUglr/v89BBdSCWv+/z0H5K4Ja/7/PQfkrglr/v89B+SuCWv+/z0H5K4Ja/78SKXUZfSX/vxIpdRl9Jf+/Eil1GX0l/78SKXUZfSX/v8gPId99Jf+/yA8h330l/7/IDyHffSX/v8gPId99Jf+/z0HI7X0l/7/PQcjtfSX/v89ByO19Jf+/z0HI7X0l/7/PQTYSfSX/v89BNhJ9Jf+/z0E2En0l/7/PQTYSfSX/v8gP3SB9Jf+/yA/dIH0l/7/ID90gfSX/v8gP3SB9Jf+/EimJ5n0l/78SKYnmfSX/vxIpieZ9Jf+/EimJ5n0l/7/4OkyQ////P/g6TJD///8/+DpMkP///z/4OkyQ////Py0pqj7///+/LSmqPv///78tKao+////vy0pqj7///+/TRD3uv///79NEPe6////v00Q97r///+/TRD3uv///7/4OrJv////P/g6sm////8/+Dqyb////z/4OrJv////P8I0yI////8/GjfnmP///z8aN+eY////P8I0yI////8/wjTIj////z/CNMiP////Py0pVMH///+/LSlUwf///78tKVTB////vy0pVMH///+/VUHSVv///z9VQdJW////P1VB0lb///8/VUHSVv///z9VQSyp////P1VBLKn///8/VUEsqf///z9VQSyp////P00QB0X///+/TRAHRf///79NEAdF////v00QB0X///+/gdr+/////799Jf7/////v4Ha/v////+/fSX+/////7//f4Ja////P/9/glr///8//3+CWv///z//f4Ja////P30lAAD///+/fSUAAP///799JQAA////v30lAAD///+//3+B2v///z//f4Ha////P/9/gdr///8//3+B2v///z8AAP9//3+dygAA/3//f73SAAD/f/9//78AAP9//3+XuAAA/3//f0CtAAD/f/9/l7gAAP9//39ArQAA/3//f/+/AAD/f/9/ncoAAP9//3+90nylAAD///+/faUAAP///798pQAA////v32lAAD///+//398pf///z//f3yl////P/9/fKX///8//398pf///z98pf//////v3yl/v////+/fKX//////798pf7/////v/9/fSX///8//399Jf///z//f30l////P/9/fSX///8//3/+/////7//f/7//v//v/9//v/+//+//3/+//7//7//f/7/////v/9//v////+//3/+/////7//f/7/////v/9//v////+//3/+//7//7//f/7//v//v/9//v/+//+////y5AAAhc1U5nrrBADVzFTmeut+APfM///y5AAAhc2HHh+z8mT/v4ceH7PyZP+/hx4fs/Jk/7+HHh+z8mT/v2P9cEUAAI2cY/1wRRMAdeN4607CCAKimpPmP7AOAMDxk+Y/sH4AII4gM4aeDJv/vyAzhp4Mm/+/IDOGngyb/78gM4aeDJv/v6dNT5v/v/+/p01Pm/+//7+nTU+b/7//v6dNT5v/v/+/vs9qmfn/wfFwxWN9/v9w477Paplp/z+OsL2GlJX/3ORwxWN96/+JnGZrhp7y5P+/ZmuGnvLk/79ma4ae8uT/v2Zrhp7y5P+/mJSGnv//hc2YlIae//+FzZiUhp7//4XNmJSGnv//hc0Mm/9///+FzYSUqZn6/ymzhJSpmYD/B7MMm/9///+FzVeyT5v////fV7JPm////99Xsk+b/v//31eyT5v///+f8mT/f/Lk/797a62ZUuYBwHtrrZkP5j7A8mT/f/Lk/7/ezIae//948t7Mhp7//3jy3syGnv//hY3ezIae//948iA2G3+btf+/HC6qlnucQcD1PTOTXLEsvyA2G3+atQDA7hycfQyb/78cLqqWfJwFwO4cnH0Mm/+//38AAP///z//fwAA/v//P/9/AAD///8//38AAP7//z//fwAA/v//P/9/AAD+//8//38AAP///z//fwAA////P/9/AAD///8//38AAP7//z//fwAA////P/9/AAD+//8/d+EfswAAePJ34R+zAAB48nfhH7MAAHjyd+EfswAAhY2HHmbrDBv/v4ceZusMG/+/hx5m6wwb/7+HHmbrDBv/v6/kp80AAP+fr+SnzQAA/5+v5KfNAAD/n6/kp80AAP/fYwIQY/Jk/7+qFhuugmP5v6oWG66DY7y/YwIQY/Jk/7/jAN5JY0r/vzQT9L3nTS7A4wDeSWRK/r//f/7//v//v/9//v/+//+//3/+//7//7//f/7//v//v/9//v/+//+//3/+//7//7//f/7//v//v/9//v/+//+//3/+//7//7//f/7//v//v3fhZusAAHiyd+Fm6wAAeLJ34WbrAAB4snfhZusAAIXNTxunzf8//79PG6fN/z//v08bp83/P/+/Txunzf8//78AAPLkDBv/v64Ze+usGf2/rhl76+8Zv78AAPLkDBv/v/9/AAD+//8//38AAP7//z//fwAA/v//P/9/AAD+//8//38AAP7//z//fwAA/v//P/9/AAD+//8//38AAP7//z//fwAA/v//P/9/AAD+//8//38AAP7//z//fwAA/v//P////398pQCA////f4JaAID///9/gloAgP///3//f/Cc////f/9/QK3///9//39Arf7//3//f0Ct////f4HaAID///9/fSUAgP///398pf//////f4Ha//////9//3/+/////398pf//////f/9/QO3///9//39A7f7//3//f73STN+h74JaAIBM36HvfKUAgEzfoe+CWgCATN+h73ylAICNICPvgloAgI0gI+98pQCAjSAj74JaAICNICPvfKUAgAAA/398pQCAAAD/f/+/AIAAAP9/fKUAgAAA/3+B2gCAAAD/f4Ha//8AAP9//39ArQAA/3//f76SAAD/f/9/QK0AAP9/fKX//wAA/399Jf7/AAD/f4Ha//8AAP9/fKX//wAA/398pf//AAD/f/9/QO0AAP9//3//3wAA/3//f0DtAAD/f/9/QO0AAP9//3+90gAA/3//f73Sbj5J2P///79TM/fI////v24+Sdj///+/UzP3yP///79KWG2+////P/dIUrP///8/Slhtvv///z/3SFKz////P4OQVMX///8/ApHuv////z+DkFTF////PwKR7r////8/iqZAv////z+KpkC/////Pwe3UrP///8/B7dSs////z9dkLGgglr//12QsaB8pf//XZCxoIJa//9dkLGgfKX//yRvjaCCWv//JG+NoHyl//8kb42gglr//yRvjaB8pf//LHHZXv9/QO0scdle/39A7Sxx2V7/f0DtJo/yXv9/QO0mj/Je/39A7UeOli//f73SqHLsLv9/vdKocuwu/3+90keOli//f73Sla9HDv9/QK0TUVcN/39ArRNRVw3/f0Ctla9HDv9/QK3x3icP/3++kiUh0g7/f76SJSHSDv9/vpIlIdIO/3++kvHeJw//f76SvsBz2f///7+rzPfI////v6vM98j///+/vsBz2f///78PwPvu////v6m6e+////+/qbp77////78PwPvu////v4Ha/3////8/gdr/f////z+B2v9/////P4Ha/3////8///98pf///7///3yl////v///fKX///+///98pf///78AAHyl////vwAAfKX///+/AAB8pf///78AAHyl////v3yl/3////8/fKX/f////z98pf9/////P3yl/3////8/y0Tw7v///7/vP/vu////v8tE8O7///+/7z/77v///7/xbsvE////P/xu7r////8/8W7LxP///z/8bu6/////P/9//v///////3///wAAQO3/f/7///++kv9//v8AAJ2K/3/+/wAAvpL/f/7//v+W+P9///8AAGD1/3/+//7/QO0AAIHa////vwAAgdr///+/AACB2v///78AAIHa////v4Ja/3////8/glr/f////z+CWv9/////P4Ja/3////8///+B2v///7///4Ha////v///gdr///+///+B2v///799Jf9/////P30l/3////8/fSX/f////z99Jf9/////P0zf1Xj//3jyTN/VeP//ePJM39V4//948tX4TF8AAHjy1fhMXwAAePLV+ExfAAB48pEXNzT/P/+/kRc3NP8//7+RFzc0/z//v5EXNzT/P/+/ieV1FQAAeLKJ5XUVAAB4sonldRUAAIXNieV1FQAAeLJt6Dc0AAD/n23oNzQAAP+fbeg3NAAA/59t6Dc0AAD/33UadRUMG/+/dRp1FQwb/791GnUV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/39BLS8+BlT/f0EtLz4GVP9/QS0vPgZU/39BLRGph0L/f0EtEamHQv9/QS3nmBk3////PxGph0L/f0EtyI/CNP///z/nmBk3////P8iPwjT///8/TZD4Ov///z9NkPg6////PyypVUH///8/LKlVQf///z/tVodC/39BLe1Wh0L/f0Et7VaHQv9/QS3tVodC/39BLdJWVUH///8/0lZVQf///z8Zt+gY////v/e6TRD///+/GbfoGP///79VwS0p////v1XBLSn///+/wrTID////7/CtMgP////v/e6TRD///+/z8EGVP9/QS3PwQZU/39BLc/BBlT/f0Etz8EGVP9/QS2HwhIp/3++UofCEin/f75Sh8ISKf9/vlKHwhIp/3++Unc9Ein/f75Sdz0SKf9/vlJ3PRIp/3++Unc9Ein/f75SBdTPQf9/vlIF1M9B/3++UgXUz0H/f75SBdTPQf9/vlL5K89B/3++Uvkrz0H/f75S+SvPQf9/vlL5K89B/3++UnUZEin/f0BtdRkSKf9/QG11GRIp/39AbXUZEin/f0BtId/ID/9/QG0h38gP/39AbSHfyA//f0BtId/ID/9/QG3I7c9B/39Abcjtz0H/f0BtyO3PQf9/QG3I7c9B/39AbTYSz0H/f0BtNhLPQf9/QG02Es9B/39AbTYSz0H/f0Bt3SDID/9/QG3dIMgP/39Abd0gyA//f0Bt3SDID/9/QG2J5hIp/39AbYnmEin/f0BtieYSKf9/QG2J5hIp/39AbeVI5xj///+/qT4tKf///7+pPi0p////v+VI5xj///+/PEvID////78HRU0Q////vwdFTRD///+/NnDCNP///z+xb/g6////P7Fv+Dr///8/NnDCNP///z///4Ha////v///gdr///+///+B2v///7///4Ha////v4Ja/3////8/glr/f////z+CWv9/////P4Ja/3////8/AACB2v///78AAIHa////vwAAgdr///+/AACB2v///7+B2v9/////P4Ha/3////8/gdr/f////z+B2v9/////P/9/AAA8lf8//38AAP9//z//fwAAfKX/P/9/AAAvcf8//38AAIJa/z//fwAAL3H/P/9/AAAvcf8//38AAIJa/z//fwAAO5X/P/9/AAB8pf8/AAB8pf///78AAHyl////vwAAfKX///+/AAB8pf///798pf9/////P3yl/3////8/fKX/f////z98pf9/////P///fKX///+///98pf///7///3yl////v///fKX///+/fSX/f////z99Jf9/////P30l/3////8/fSX/f////z8AAP7/cijGKwAA//9VXapuAAD+/3IoOFQAAP7/qaKqbv7///+poqpuAAD+/6miqm4AAP//cig4VP7//v9yKMYr/v/+/1VdVRH+//7/jNc4VP7/AACM1zhU/v///4zXxiv+/wAAjNfGK/7///+polUR/v/+/6miVREDtBkjcig4VAO0GSNyKDhUA7QZI3IoOFQDtBkjcig4VP9//39yKMYr/3//f3IoOFT/f/9/VV2qbv9//39yKMYr/3//f1VdVRH/f/9/VV1VEf9//39VXVUR/3//f1Vdqm7/f/9/VV2qbv9//39VXapu/3//f6miqm7/f/9/jNc4VP9//3+M1zhU/3//f/7//z//f/9/jNfGK/9//3+polUR/3//f6miVRH/f/9/qaJVEfCiWzRyKDhU8KJbNHIoOFTwols0cig4VPCiWzRyKDhUhsAIaP///z+GwAho////Pxq162v///8/GrXra////z+Wx6xY////P83C6lP///8/zcLqU////z+GwPaX////P4bA9pf///8/GrUTlP///z8atROU////P5XHUqf///8/zcIUrP///z/NwhSs////PwO05dyM1zhUA7Tl3IzXOFQDtOXcjNc4VAO05dyM1zhU8KKiy4zXOFTwoqLLjNc4VPCiosuM1zhU8KKiy4zXOFRlXBHNjNfGK2VcEc2M18YrZVwRzYzXxitlXBHNjNfGK9ZMgNyM18Yr1kyA3IzXxivWTIDcjNfGK9ZMgNyM18YrVCwSn6miVRFULBKfqaJVEVQsEp+polUR0h4BramiVRHSHgGtqaJVEdIe/VJVXVURVCzsYFVdVRHSHv1SVV1VEVQs7GBVXVURVCzsYFVdVRHWTH4jcijGK9ZMfiNyKMYr1kx+I3IoxivWTH4jcijGK2Vc7TJyKMYrZVztMnIoxitlXO0ycijGK2Vc7TJyKMYrt+efvv///7/q6+PK////v+rr48r///+/t+efvv///7+s2Ny3////v+rTMb3///+/6tMxvf///7/q6xs1////v7fnX0H///+/6usbNf///7+3519B////v6zYIkj///+/6tPNQv///7/q081C////v2WnAAD///+/ZacAAP///79lpwAA////v2WnAAD///+//3+Y2P///z//f5jY////P/9/mNj///8//3+Y2P///z//f2Yn////P/9/Zif///8//39mJ////z//f2Yn////P1zc//////+/XNz//////79c3P//////v1zc//////+/VRb19w14ag/NFvT4qFV5HlUW9feBa/AUzRb0+H5gwxkNoA0gDSAGUA2gDSANIAZQDaANIA0gBlANoA0gDSAGUG/OjrG5TgEZb86OsV9QTxpvzo6xv1WQHm/OjrHdQh8V9XjMlgtDKxX2d1WWK2H5A/V4zJZOS2sQ9ndVlsdUAQunZKbkpuRTMqdkpuSm5FMyf1t+237bvy1/W37bftu/LaYJWXZ5dckEpglZdoBwlQSmCVl242DzA6YJWXYZeI0P////f6aH/n////9//3+1eP///3//f7V4////f6Gj/n////9//39ndv///3//f2d2////f6aH/n////9/oaP+f/9/oaP///8//3+ho////z//f6Gj////P/9/oaP///8/Zaf//////79lp///////v2Wn//////+/Zaf//////7//f11c////P/9/XVz///8//39dXP///z//f11c////P1zcAAD///+/XNwAAP///79c3AAA////v1zcAAD///+/5WM7CP7/vlLlYzsI/v++UuVj3hP+/74SGZw7CIHa/z8ZnDsIgdr/Pxmc3hN8pf8/GZzeE3yl/z/lY94T/v++EiBsGRwAAL4SIGwZHAAAvhIgbBkcAAC+EsR3GRwAAL5SxHcZHAAAvlI6iBkcfSX/PzqIGRx9Jf8/3pMZHIJa/z/ekxkcglr/PxIpdRn+/75SEil1Gf7/vlISKXUZ/v++UhIpdRn+/75SyA8h3///vlLIDyHf//9BLcgPId/+/0EtyA8h3/7/vlLPQcjt/v9BLc9ByO3+/0Etz0HI7f7/QS3PQcjt//++Us9BNhL+/0Etz0E2Ev7/QS3PQTYS/v9BLc9BNhL+/0EtyA/dIP//QS3ID90g/v9BLcgP3SD+/0EtyA/dIP//vlISKYnm/v++UhIpieb+/75SEimJ5v7/vlISKYnm/v++UuVjw/f+/75S5WPD9/7/vlLlYyDs/v9AbeVjIOz+/0Bt5WMg7P7/QG0ZnMP3gdr/Pxmcw/eB2v8/GZwg7Hyl/z8ZnCDsfKX/PyBs5eMAAL4SIGzl4wAAvhLEd+TjAABBLcR35OMAAEEtOojl430l/z86iOXjfSX/P96T5eOCWv8/3pPl44Ja/z/PQQXU/v9Abc9BBdT+/0Btz0EF1P7/QG3PQQXU/v9Abc9B+Sv//74Sz0H5K///vhLPQfkr/v++Es9B+Sv+/0BtyA9ZT///vhLID1lP/v9AbcgPWU/+/0BtyA9ZT/7/vhISKYfC/v++EhIph8L+/74SEimHwv7/vhISKYfC/v++EhIpdz3//74SEil3Pf//vhISKXc9//9AbRIpdz3+/74SyA+lsP//vhLID6Ww/v9AbcgPpbD+/0BtyA+lsP7/vhIGVC8+AABAbQZULz4AAEBtBlQvPgAAQG0GVC8+AAC+EqYwNnAAAEBtpjA2cAAAvhKmMDZwAAC+EqYwNnAAAEBth0IRqQAAQG2HQhGpAABAbYdCEakAAEBth0IRqQAAQG2HQu1WAAC+EodC7VYAAL4Sh0LtVgAAvhKHQu1WAAC+EqYwyI8AAL4SpjDIjwAAvhKmMMiPAAC+EqYwyI8AAEBtBlTPwQAAvhIGVM/BAAC+EgZUz8EAAEBtBlTPwQAAvhKJZu1WAAC+Uolm7VYAAEEtiWbtVgAAQS2JZu1WAAC+UiFfyI8AAEEtIV/IjwAAvlIhX8iPAAC+UiFfyI8AAEEtyG3PwQAAQS3Ibc/BAABBLchtz8EAAEEtyG3PwQAAQS3IbS8+AAC+UshtLz4AAL5SyG0vPgAAvlLIbS8+AABBLSFfNnAAAEEtIV82cAAAQS0hXzZwAAC+UiFfNnAAAEEtiWYRqQAAQS2JZhGpAABBLYlmEakAAEEtiWYRqQAAvlJ1me1WfSX/P3WZ7VZ9Jf8/dZntVn0l/z91me1WfSX/P92gyI99Jf8/3aDIj30l/z/doMiPfSX/P92gyI99Jf8/NpLPwX0l/z82ks/BfSX/PzaSz8F9Jf8/NpLPwX0l/z82ki8+fSX/PzaSLz59Jf8/NpIvPn0l/z82ki8+fSX/P92gNnB9Jf8/3aA2cH0l/z/doDZwfSX/P92gNnB9Jf8/dZkRqX0l/z91mRGpfSX/P3WZEal9Jf8/dZkRqX0l/z/4qy8+glr/P/irLz6CWv8/+KsvPoJa/z/4qy8+glr/Pxbn5Uj///+/FuflSP///78W5+VI////vxbn5Uj///+/NvA8S////7828DxL////v3e9EamCWv8/d70RqYJa/z93vRGpglr/P3e9EamCWv8/5MjnmP///z/kyOeY////PzzLyI////8/PMvIj////z88y8iP////P3e97VaCWv8/d73tVoJa/z93ve1Wglr/P3e97VaCWv8/5cgXZ////z/lyBdn////PzzLNnD///8/PMs2cP///z88yzZw////PxbnGbf///+/FucZt////78W5xm3////vxbnGbf///+/NvDCtP///7828MK0////v/irz8GCWv8/+KvPwYJa/z/4q8/Bglr/P/irz8GCWv8/7NaHwnyl/z/s1ofCfKX/P+zWh8J8pf8/7NaHwnyl/z/s1nc9fKX/P+zWdz18pf8/7NZ3PXyl/z/s1nc9fKX/Py++BdR8pf8/L74F1Hyl/z8vvgXUfKX/Py++BdR8pf8/L775K3yl/z8vvvkrfKX/Py+++St8pf8/L775K3yl/z/s1nUZgdr/P+zWdRmB2v8/7NZ1GYHa/z/s1nUZgdr/PzbwId+B2v8/NvAh34Ha/z828CHfgdr/PzbwId+B2v8/L77I7YHa/z8vvsjtgdr/Py++yO2B2v8/L77I7YHa/z8vvjYSgdr/Py++NhKB2v8/L742EoHa/z8vvjYSgdr/Pzbw3SCB2v8/NvDdIIHa/z828N0ggdr/Pzbw3SCB2v8/7NaJ5oHa/z/s1onmgdr/P+zWieaB2v8/7NaJ5oHa/z8GxUyQ////PwbFTJD///8/BsVMkP///z8GxUyQ////P9HWqj7///+/0daqPv///7/R1qo+////v9HWqj7///+/se/3uv///7+x7/e6////v7Hv97r///+/se/3uv///78HxbJv////PwfFsm////8/B8Wyb////z8HxbJv////P9HWVMH///+/0dZUwf///7/R1lTB////v9HWVMH///+/qb7SVv///z+pvtJW////P6m+0lb///8/qb7SVv///z+pviyp////P6m+LKn///8/qb4sqf///z+pviyp////P7HvB0X///+/se8HRf///7+x7wdF////v7HvB0X///+/fSX+/////7+B2v7/////v4Ha/v////+/fSX+/////7//f4Ja////P/9/glr///8//3+CWv///z//f4Ja////P4HaAAD///+/gdoAAP///7+B2gAA////v4HaAAD///+//3+B2v///z//f4Ha////P/9/gdr///8//3+B2v///z/+//9//39hNf7//3//f2dH/v//f/9/QS3+//9//39nR/7//3//f75S////f/9/Z0f///9//39nR////3//f75S////f/9/YTX///9//39BLXylAAD///+/gloAAP///7+CWgAA////v3ylAAD///+//398pf///z//f3yl////P/9/fKX///8//398pf///z98pf7/////v4Ja/v////+/glr+/////798pf7/////v/9/fSX///8//399Jf///z//f30l////P/9/fSX///8//3/+/wAA/z//f/7/AAD/P/9//v8AAP8//3/+/wAA/z//f/7/AAD/P/9//v8AAP8//3/+/wAA/z//f/7/AAD/P/9//v8AAP8//3/+/wAA/z//f/7/AAD/P/9//v8AAP8/AADy5P//eTKqGXrr+v8pMwAA8uT//3kyqhl664D/BzMAAPLk//95MnfhH7MMm/8/d+Efswyb/z934R+zDJv/P3fhH7MMm/8/mwJwRez/dWOGFE7C9v1cZWsZP7D6/8Fxaxk/sPD/Pg5rGT+wgP/ecZsCcEX+/3FjmwJwRf7/jhybAnBF6/+JHN7Mhp7yZP8/3syGnvJk/z/ezIae8mT/P97Mhp7yZP8/V7JPm/8//z9Xsk+b/z//P1eyT5v/P/8/V7JPm/8//z9AMGqZBQA9DkAwapmVAL9xQDBqmX4AIA5OQoaUaQAiG446Y30AAHFjjjpjfQAAjhyOOmN9EgCKHI46Y30TAHVjmJSGngwb/z+YlIaeDBv/P5iUhp4MG/8/mJSGngwb/z9ma4aeAAB5MmZrhp4AAHkyZmuGngAAeTJma4aeAAB5MvJk/38AAHkyemupmQQA1UzyZP9/AAB5MnprqZl+APdM8mT/fwAAeTKnTU+bAAD/X6dNT5sAAP9fp01PmwAA/1+nTU+bAAD/Hwyb/38MG/8/g5StmawZ/T8Mm/9/DBv/P4OUrZnvGcA/IDOGngAAeXIgM4aeAAB5ciAzhp4AAIYNIDOGngAAeXLeyRt/Y0r/PwnCM5PeTSZA3skbf2RK/j8JwjOTok7SQBDjnH3yZP8/4tGqloJj+T8Q45x98mT/P/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v4ceH7P//4YNhx4fs///hg2HHh+z//95coceH7P//4YNd+Fm6/Lk/z934Wbr8uT/P3fhZuvy5P8/d+Fm6/Lk/z9PG6fN////H08bp83///8fTxunzf///x9PG6fN////X5v9EGMMm/8/yuz0vVyxLD+b/RBjDJv/P1TpG657nEJAG//eSZu1/z/K7PS9F7LQPxv/3kmatQBA/3///wAA/z//f///AAD/P/9///8AAP8//3///wAA/z//f///AAD/P/9///8AAP8//3///wAA/z//f///AAD/P/9///8AAP8//3///wAA/z+HHmbr//+GTYceZuv//4ZNhx5m6///eTKHHmbr//+GTa/kp83/v/8/r+Snzf+//z+v5KfN/7//P6/kp83/v/8////y5PLk/z9Q5nvrUuYBQP//8uTy5P8/UOZ76w/mP0D/fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/78AAP9//39AbQAA/3//f/5/AAD/f4Ha/n8AAP9//3++UgAA/3//f75SAAD/f3yl/n8AAP9/gdr+fwAA/3/+/wAAAAD/f3ylAAAAAP9/fKUAAAAA/3//fwAAAAD/f/9/vhIAAP9//3++EgAA/3//f0EtAAD/f/9/QS2yIKHvfKX+f7Igoe+CWv5/siCh74Ja/n+yIKHvfKX+f////3//f0Bt////f3yl/n////9/gdr+f////398pf5/////f/9/vlL///9//3++Uv///3//f75S////f/+//n////9/gdr+f////3+B2v5/////f30lAAD+//9/gdoAAP///399JQAA////f3ylAAD///9//3++Ev///3//f74S////f/9/vhL///9//39BLf///3//f0Etcd8j73yl/n9x3yPvglr+f3HfI++CWv5/cd8j73yl/n+QwUnY////v5DBSdj///+/q8z3yP///7+rzPfI////v7Snbb7///8/tKdtvv///z8Ht1Kz////Pwe3UrP///8/e29Uxf///z90WUC/////P/xu7r////8//G7uv////z90WUC/////P/dIUrP///8/90hSs////z+hb7GggloAAKFvsaCCWgAAoW+xoIJaAAChb7GgfKUAANqQjaB8pQAA2pCNoIJaAADakI2ggloAANqQjaB8pQAA2HDyXv9/vhLSjtle/3++Ethw8l7/f74S0o7ZXv9/vhLYcPJe/3++Erdxli//f0EtVo3sLv9/QS23cZYv/39BLVaN7C7/f0EtaVBHDv9/vlLrrlcN/3++UmlQRw7/f75S665XDf9/vlINIScP/39AbQ0hJw//f0BtDSEnD/9/QG3Z3tIO/39Abdne0g7/f0BtQD9z2f///79TM/fI////v1Mz98j///+/QD9z2f///79VRXvv////v+8/++7///+/7z/77v///799Jf9/////P30l/3////8/fSX/f////z99Jf9/////PwAAfKX///+/AAB8pf///78AAHyl////vwAAfKX///+///98pf///7///3yl////v///fKX///+///98pf///7+CWv9/////P4Ja/3////8/glr/f////z+CWv9/////PzO78O7///+/D8D77v///78PwPvu////vzO78O7///+/DZHLxP///z8Cke6/////PwKR7r////8/DZHLxP///z//f///AABoB/9///8AAL4S/3//////lAf/f/////+eCv9//////wAA/3/+//7/QG3/f/7//v9hdf9//v8AAL4S//+B2v///7///4Ha////v///gdr///+///+B2v///798pf9/////P3yl/3////8/fKX/f////z98pf9/////PwAAgdr///+/AACB2v///78AAIHa////vwAAgdr///+/gdr/f////z+B2v9/////P4Ha/3////8/gdr/f////z+yINV4AACGDbIg1XgAAIYNsiDVeAAAhg2yINV4AACGDSkHTF///4YNKQdMX///hg0pB0xf//+GDSkHTF///4YNbeg3NP+//z9t6Dc0/7//P23oNzT/v/8/beg3NP+//z91GnUV//+GTXUadRX//4ZNdRp1Ff//hk11GnUV//95MpEXNzT///9fkRc3NP///1+RFzc0////H5EXNzT///9fieV1FfLk/z+J5XUV8uT/P4nldRXy5P8/ieV1FfLk/z91GhRQ//+GDXUaFFD//4YNdRoUUP//eXJ1GhRQ/v+GDeoviWUAAHly6i+JZQAAeXLqL4llAACGDeoviWUAAHlyyEtuaAAA/1/IS25oAAD/H8hLbmgAAP9fyEtuaAAA/x+KaollAAB5MopqiWUAAHkyimqJZQAAeTKKaollAAB5MnSViWUMG/8/dJWJZQwb/z90lYllDBv/P3SViWUMG/8/NrRuaP8//z82tG5o/z//Pza0bmj/P/8/NrRuaP8//z8U0Ill8mT/PxTQiWXyZP8/FNCJZfJk/z8U0Ill8mT/P4nlFFAMm/8/ieUUUAyb/z+J5RRQDJv/P4nlFFAMm/8/u2Wq4f///z9DmYrf////P+pkMNb///8/eHbt4////z+tzIrM////v7uyPd////+/36LM4f///79DRcXO////v/9///8AAJk6n14E5v///7++yL9Iv0hfZHzSgFV+VDhqvsi/SL9IX2R80oBVXVRFarBtNI7QIOIl5mw3jKtCFhYNbr6OeyesImVsvovzOTAa9DrzuvO6eh30OvO687p6HX0zfbN9s74ZfTN9s32zvhkKM/VMq1OUHgoz9Uz1R9wXpzaIRgA/EhinNohGqUq9HammVNmtIzYpqaZU2WwuWC6pplTZZDSjL6mmVNm5IM0lNQ6w7cY6Miw3DObs6kvdIgQKs+yRS1kivg4M7jA0lC9trN4M////v22s3gz///+/bazeDP///79trN4M////vwfhK+Iha9XnB+Er4u9w8OgvlkGcrVkK/C+WQZx2Xaj/YKSzzP///z9gpLPM////P/9/AAD+/+u5/38AAP7/WLL/fwAA/v/prQqjEjD///8/CqMSMP///z+WiF8p////P5aIXyn///8/lohfKf///z+WiF8p////PxKwCiP///+/ErAKI////7//fwAAQ+r/v/9/AACt9f+//38AANPb/7//fwAA////v2ettvb///+/Z6229v///79nrbb2////v2ettvb///+/Wr/pLf///79av+kt////v92NUUD///8/3Y1RQP///z/djVFA////P+itWj////8/6K1aP////z8OwTAT////vw7BMBP///+/DsEwE////7+r/gglV3/ffqv+CCX///+/q/4IJVd/336Tmv9//3/+f5Oa/3//f/5//3/+/+MD/7//f/7/9Aj/v/9//v8AAP+//3/+/7sN/7//f///AABNyP9///////k+/3//////ITn/f/////+xN/9///8AAGzC/3///wAAvMn/f///AAD/v/9//////zI//3///1jx/z+GjtrO////P4aO2s7///8/ho7azv///z+GjtrO////P0qzntv///+/SrOe2////79lvfbS////v2W99tL///+/RJN8xP///z9Ek3zE////P0STfMT///8/fblW8v///799uVby////v325VvL///+/CK2Zwv///z8IrZnC////P+X/cKK+f+n/5f9wohSAqf+Tmv9//3///5Oa/3//f///tv/42rd+Zv+2//ja23/B////a+X/f/7///9r5f9//v8GpUeA0X5c/walR4CEf+3/gbu57P///7+Bu7ns////v4G7uez///+/p42Bxv///z+njYHG////P6eNgcb///8/R4mW0v///z9HiZbS////P0eJltL///8/R4mW0v///z//f/7/JBD/v/9//v8AAP+//3/+/7Ya/78Hpat+wH2rfwelq37Afat///9r5f9//n9RwN4N////v1HA3g3///+/UcDeDf///78vkw5B////Py+TDkH///8/L5MOQf///z8ksXfx////vySxd/H///+/JLF38f///78ksXfx////v/9/AAAm+f+//38AAJbo/7//fwAA/v//v92Mbiz///8/3YxuLP///z/djG4s////P92Mbiz///8//38AAP7/vcP/fwAA/v+OyP9/AAD+//+/0p33nuBP0/XSnfee41F3+L3jzum7eAHtvePO6RpyRetfqZcI////v1+plwj///+/X6mXCP///7/4mYJXFpGe4/iZglcWkZ7jzNXcEdO4b/fM1dwR////v4HX+BnAuHP3gdf4GcC4c/fckcxVHpGV49yRzFUekZXjOwgZnIJaAIDeExmcgdoAgCDsGZyB2gCAGhzek4Ha//8aHN6Tgdr//+Xj3pN9Jf7/5ePek30l/v8aHDqIfKX//+XjxHf/f0Dt5ePEd/9/QO0Zt+cY////vxm35xj///+/GhwgbP9/vdLlSOgY////v+VI6Bj///+/5UjoGP///7/lSOgY////v8iPwjT///8/yI/CNP///z/eE+Vj/39ArYfCEin/f0CtPEvID////788S8gP////v8P35WP/f76Sw/flY/9/vpKxb/g6////P7Fv+Dr///8/VcEtKf///79VwS0p////vwdFTRD///+/B0VNEP///79NkPg6////P02Q+Dr///8/F2cZN////z8XZxk3////P8K0yA////+/wrTID////7/nmBk3////P+eYGTf///8/NnDCNP///z82cMI0////PzZwwjT///8/glr/f////z+CWv9/////P///fKX///+///98pf///78AAIHa////vwAAgdr///+/fKX/f////z98pf9/////P6k+LSn///+/qT4tKf///78sqVVB////PyypVUH///8/0lZVQf///z/SVlVB////P/e6TRD///+/97pNEP///78AAIHa////vwAAgdr///+/fKX/f////z98pf9/////P///gdr///+///+B2v///799Jf9/////P30l/3////8/AAB8pf///78AAHyl////v4Ha/3////8/gdr/f////z99Jf9/////P30l/3////8///+B2v///7///4Ha////v/9/AADDav+//38AAP9//7//fwAA/3//v/9/AADPjv+//38AAP9//7//fwAAw2r/v///fKX///+///98pf///7+CWv9/////P4Ja/3////8/AAB8pf///78AAHyl////v4Ha/3////8/gdr/f////z8AAP//jNfGqwAA//+M18arAAD//4zXxqsAAP//jNfGq/9//3+M18ar/3//f4zXxqv/f/9/jNfGq/9//3+M18ar/v/+/4zXxqv+//7/jNfGq/7///+M18ar/v///4zXxqv+////jNfGq/9//3+M18ar/3//f6miVJH/f/9/jNfGq/9//3+M18ar/3//f6miVJH/f/9/qaJUkf7///+polSR/v///6miVJH+////qaJUkf7///+polSR/v///6miVJF4Pwho////P3g/CGj///8//3//f6miVJH/f/9/qaJUkf9//3+polSR/3//f6miVJH+//7/qaJUkf7//v+polSR/v/+/6miVJH+//7/qaJUkXg/9pf///8/eD/2l////z//f/9/qaJUkf9//3+polSR/3//f6miVJH/f/9/qaJUkWg4rFj///8/aDisWP///z//f/9/VV1Ukf9//39VXVSR/v/+/1VdVJH+//7/VV1Ukf7//v9VXVSR/v/+/1VdVJFSJ9y3////v1In3Lf///+//3//f1VdVJH/f/9/VV1Ukf9//39VXVSR/3//f1VdVJH+////VV1Ukf7///9VXVSR/v/+/1VdVJH+//7/VV1Ukf7//v9VXVSR/v/+/1VdVJH/////cijGq/////9yKMar/////3Ioxqv/////cijGq/9//39yKMar/3//f3Ioxqv/f/9/cijGq/9//39yKMar/3//f3IoxqsAAP7/cijGqwAA/v9yKMar/v8AAHIoxqv+/wAAcijGq/7/AAByKMar/3//f3Ioxqv/f/9/cijGq/9//39yKMar/3//f3Ioxqv/f/9/cig41P9//39yKDjU/3//f3IoONQAAP//cig41AAA//9yKDjUAAAAAHIoONQAAAAAcig41AAAAAByKDjU/3//f3IoONT/f/9/cig41P9//39yKDjU/3//f3IoONQAAP//cig41AAA//9yKDjUAAD//3IoONQAAP//cig41P7///9VXanu/v///1Vdqe7+////VV2p7gAA/v9VXanuAAD+/1Vdqe6q0xKfVV2p7qrTEp9VXanu/3//f1Vdqe7/f/9/VV2p7v9//39VXanu/3//f1Vdqe4AAP//VV2p7gAA//9VXanuAAD//1Vdqe4AAP//VV2p7izhAa1VXanu/3//f1Vdqe7/f/9/VV2p7v9//39VXanu/3//f1Vdqe7/f/9/VV2p7v9//3+poqnu/3//f6miqe7/f/9/qaKp7v9//3+poqnu/////6miqe7/////qaKp7v////+poqnuLOH9Uqmiqe7/f/9/qaKp7v9//3+poqnu/3//f6miqe7/f/9/qaKp7v9//3+poqnu/3//f6miqe4AAP7/qaKp7gAA/v+poqnuqtPsYKmiqe6q0+xgqaKp7v7//v+M1zjU/v/+/4zXONT+//7/jNc41P7//v+M1zjU/3//f4zXONT/f/9/jNc41P9//3+M1zjU/3//f4zXONT/f/9/jNc41P7//v+M1zjU/v/+/4zXONT+//7/jNc41AAA/v+M1zjUAAD+/4zXONQAAP7/jNc41P9//3+M1zjU/3//f4zXONT/f/9/jNc41P9//3+M1zjU5q5qkamiVJHmrmqRqaJUkeauapGpolSR5q5qkamiVJEH8AHVqaJUkQfwAdWpolSRB/AB1amiVJEH8AHVqaJUkcj6wBj///+/yPrAGP///7/I+sAY////v8j6wBj///+/+/dr6qmiVJH792vqqaJUkfv3a+qpolSRk5UCiKmiVJGTlQKIqaJUkZOVAoipolSRoWgTe////z+haBN7////P6FoE3v///8/oWgTe////z/9qvePqaJUkf2q94+polSR/ar3j6miVJH9qvePqaJUkZPuGNGpolSRk+4Y0amiVJGT7hjRqaJUkZPuGNGpolSRB4mLk4zXxqsHiYuTjNfGqweJi5OM18araaYn04zXxqtppifTjNfGq2mmJ9OM18araaYn04zXxqssklinjNfGqyySWKeM18arLJJYp4zXxqssklinjNfGq/3UIOyM18ar/dQg7IzXxqv91CDsjNfGq/3UIOyM18ar3ZMBq4zXxqvdkwGrjNfGq92TAauM18ar3ZMBq4zXxqul2NHtjNfGq6XY0e2M18arpdjR7YzXxqul2NHtjNfGq0jKLueM18arSMou54zXxqtIyi7njNfGq0jKLueM18arc+z39ozXxqtz7Pf2jNfGq3Ps9/aM18ar0m1Yp4zXONTSbVinjNc41NJtWKeM1zjU0m1Yp4zXONQCKyDsjNc41AIrIOyM1zjUAisg7IzXONQCKyDsjNc41CFsAauM1zjUIWwBq4zXONQhbAGrjNc41CFsAauM1zjUWSfR7YzXONRZJ9HtjNc41Fkn0e2M1zjUWSfR7YzXONS2NS7njNc41LY1LueM1zjUtjUu54zXONS2NS7njNc41IsT9/aM1zjUixP39ozXONSLE/f2jNc41Pd2i5OM1zjU93aLk4zXONT3douTjNc41JVZJ9OM1zjUlVkn04zXONSVWSfTjNc41JVZJ9OM1zjUGFFqkamiqe4YUWqRqaKp7hhRapGpoqnuGFFqkamiqe73DwHVqaKp7vcPAdWpoqnu9w8B1amiqe73DwHVqaKp7qIVvcWpoqnuohW9xamiqe6iFb3FqaKp7qIVvcWpoqnuAwhr6qmiqe4DCGvqqaKp7gMIa+qpoqnua2oDiKmiqe5ragOIqaKp7mtqA4ipoqnuJyf+oKmiqe4nJ/6gqaKp7icn/qCpoqnuJyf+oKmiqe4BVfePqaKp7gFV94+poqnuAVX3j6miqe4BVfePqaKp7msRGNGpoqnuaxEY0amiqe5rERjRqaKp7msRGNGpoqnuohVBOlVdqe6iFUE6VV2p7qIVQTpVXanuohVBOlVdqe4DCJMVVV2p7gMIkxVVXanuAwiTFVVdqe5ravt3VV2p7mtq+3dVXanua2r7d1Vdqe4nJwBfVV2p7icnAF9VXanuJycAX1Vdqe4nJwBfVV2p7gFVB3BVXanuAVUHcFVdqe4BVQdwVV2p7gFVB3BVXanuaxHmLlVdqe5rEeYuVV2p7msR5i5VXanuaxHmLlVdqe4YUZRuVV2p7hhRlG5VXanuGFGUblVdqe4YUZRuVV2p7vcP/SpVXanu9w/9KlVdqe73D/0qVV2p7vcP/SpVXanu93ZzbHIoONT3dnNscig41Pd2c2xyKDjUlVnXLHIoONSVWdcscig41JVZ1yxyKDjUlVnXLHIoONTSbaZYcig41NJtplhyKDjU0m2mWHIoONTSbaZYcig41AEr3hNyKDjUASveE3IoONQBK94Tcig41AEr3hNyKDjUIWz9VHIoONQhbP1Ucig41CFs/VRyKDjUIWz9VHIoONRZJy0Scig41FknLRJyKDjUWSctEnIoONRZJy0Scig41LY10BhyKDjUtjXQGHIoONS2NdAYcig41LY10BhyKDjUixMHCXIoONSLEwcJcig41IsTBwlyKDjU/dTeE3Ioxqv91N4TcijGq/3U3hNyKMar/dTeE3Ioxqvdk/1UcijGq92T/VRyKMar3ZP9VHIoxqvdk/1UcijGq6XYLRJyKMarpdgtEnIoxqul2C0ScijGq6XYLRJyKMarSMrQGHIoxqtIytAYcijGq0jK0BhyKMarSMrQGHIoxqtz7AcJcijGq3PsBwlyKMarc+wHCXIoxqsHiXNscijGqweJc2xyKMarB4lzbHIoxqtpptcscijGq2mm1yxyKMaraabXLHIoxqtpptcscijGqyySplhyKMarLJKmWHIoxqsskqZYcijGqyySplhyKMar5q6UblVdVJHmrpRuVV1UkeaulG5VXVSR5q6UblVdVJEH8P0qVV1UkQfw/SpVXVSRB/D9KlVdVJEH8P0qVV1Ukcj6Puf///+/yPo+5////7/I+j7n////v8j6Puf///+/+/eTFVVdVJH795MVVV1Ukfv3kxVVXVSRk5X7d1VdVJGTlft3VV1UkZOV+3dVXVSRoWjrhP///z+haOuE////P6Fo64T///8/oWjrhP///z/9qgdwVV1Ukf2qB3BVXVSR/aoHcFVdVJH9qgdwVV1UkZPu5i5VXVSRk+7mLlVdVJGT7uYuVV1UkZPu5i5VXVSRFBTjyv///78UFOPK////vxQUGzX///+/FBQbNf///7/kSutr////P+RK62v///8/FCwxvf///78ULDG9////v0cYX0H///+/RxhfQf///79HGJ++////v0cYn77///+/aThSp////z9pOFKn////P1InIkj///+/UiciSP///79c3P//////v1zc//////+/XNwAAP///79c3AAA////v/9/XVz///8//39dXP///z9lp///////v2Wn//////+/MT3qU////z8xPepT////PzE9FKz///8/MT0UrP///z8ULM1C////vxQszUL///+/5EoTlP///z/kShOU////P2WnAAD///+/ZacAAP///7//f5jY////P/9/mNj///8//39mJ////z//f2Yn////P1zc//////+/XNz//////7//f2Yn////P/9/Zif///8//3+Y2P///z//f5jY////P/9/oaP///8//3+ho////z8rxhsa////vyvGGxr///+/K8YbGv///78rxhsa////v8+YSEpyKMarz5hISnIoxqvPmEhKcijGq8+YSEpyKMarL2dISnIoONQvZ0hKcig41C9nSEpyKDjUL2dISnIoONS+RVxqVV2p7r5FXGpVXanuvkVcalVdqe6+RVxqVV2p7r5FopWpoqnuvkWilamiqe6+RaKVqaKp7r5FopWpoqnuL2e2tYzXONQvZ7a1jNc41C9ntrWM1zjUL2e2tYzXONTPmLa1jNfGq8+YtrWM18arz5i2tYzXxqvPmLa1jNfGq5ipJST///+/mKklJP///7+YqSUk////v5ipJST///+/GprSuf///z8amtK5////Pxqa0rn///8/GprSuf///z/XrGomcijGq9esaiZyKMar16xqJnIoxqvXrGomcijGqydTaiZyKDjUJ1NqJnIoONQnU2omcig41CdTaiZyKDjU/yDXWFVdqe7/INdYVV2p7v8g11hVXanu/yDXWFVdqe7/ICanqaKp7v8gJqepoqnu/yAmp6miqe7/ICanqaKp7idTlNmM1zjUJ1OU2YzXONQnU5TZjNc41CdTlNmM1zjU16yU2YzXxqvXrJTZjNfGq9eslNmM18ar16yU2YzXxqskpJgp////PySkmCn///8/JKSYKf///z8kpJgp////P/9///8AAL3S/3///30l/7//f///AAC+0v9///+CWv+//3///30l/7//f///AAC90v9///8AAL3S/3///wAAQO3/f///AABA7f9///8AAL3S/3///wAAvtL/f///AAC90v9///+CWv+//3///3yl/7//f///fKX/v/9////huf+//3////+//7//f///fKX/v/9/////v/+//3///3yl/7//f///////v/9///////+//3////7//7//f///////3/9////////f/3//////QO3/f/////8O4/9//////0Dt/3//////QO3/f/////9A7bGgXZB9Jf+/saBdkH0l/7+xoF2QfSX/v7GgXZB9Jf+//38AAH0l/7//fwAAHUb/v/9/AAB9Jf+//38AAP8//7//fwAAglr/v/9/AACCWv+//38AAIJa/7//fwAAglr/v/9/AAB9Jf+//38AAH0l/7//fwAAfSX/v/9//v99Jf+//3/+/30l/7//f/7/fSX/v/9//v99Jf+/jKAkb30l/7+MoCRvfSX/v4ygJG99Jf+/jKAkb30l/7//fwAAfSX/v/9/AAB9Jf+//38AAH0l/7//fwAAfSX/v/9//v+CWv+//3/+/4Ja/7//f/7/glr/v/9//v+CWv+/bb5KWP///z9tvkpY////P22+Slj///8/bb5KWP///z//f/7/glr/v/9//v+CWv+//3/+/4Ja/7//f/7/glr/v0nYbj7///+/SdhuPv///79J2G4+////v0nYbj7///+//38AAIJa/7//fwAAglr/v/9/AACCWv+/y8Txbv///z/LxPFu////P8vE8W7///8/y8Txbv///z//fwAAfKX/v/9/AAB8pf+//38AAHyl/7//fwAAfKX/v3vvqrr///+/e++quv///79776q6////v3vvqrr///+//38AAHyl/7//fwAAfKX/v/9/AAB8pf+//38AAHyl/7//fwAAfKX/v/9/AAB8pf+//38AAHyl/7+h70zfgdr/v6HvTN+B2v+/oe9M34Ha/7+h70zfgdr/v/9/AACB2v+//38AAP///7//fwAAgdr/v/9/AAD///+//38AAP//vdL/fwAA//+90v9/AACB2v+//38AAP//vtL/fwAAgdr/v/9/AAD//0Dt/38AAP//vtL/fwAA//9A7f9/AAD//0Dt/3/+/4Ha/7//f/7/gdr/v/9//v+B2v+/I++NIIHa/78j740ggdr/vyPvjSCB2v+/I++NIIHa/7//fwAAgdr/v/9/AACB2v+//38AAIHa/7//fwAAgdr/v/9//v/+/0Ct/3/+//7/QK3/f/7//v9ArdMOJSH+/73S0w4lIf7/vdLTDiUh//9ArdMOJSH+/73S/38AAP7/QK3/fwAA/v9Arf9/AAD+/0Ct/38AAP7/QK0nD/He//9ArScP8d7+/73SJw/x3v7/QK0nD/He/v+90lcNElH+/0DtVw0SUf7/QO1XDRJR/v9A7VcNElH//76S/38AAP7/QO3/fwAA/v9A7f9/AAD+/0Dt/38AAP7/QO1HDpWv/v9A7UcOla/+/0DtRw6Vr///QO1HDpWv/v9A7f9/AAD+/0Dt/38AAP7/QO3/fwAA/v9A7f9/AAD+/0Dt/38AAAAAvpL/fwAAAAC+kv9/AAAAAL6S/38AAAAAvpL/f/7/AABA7f9//v8AAEDt/3/+/wAAQO3/f/7/AABA7ZYvR44AAL6Sli9HjgAAvpKWL0eOAAC+kpYvR44AAL6S/38AAAAAvpL/fwAAAAC+kv9/AAAAAL6S/38AAAAAvpL/fwAAAABA7f9/AAAAAEDt/38AAAAAQO3/f/7/AAC+kv9//v8AAL6S/3/+/wAAvpL/f/7/AAC+kuwup3IAAL6S7C6ncgAAvpLsLqdyAAC+kuwup3IAAL6S/3/+/wAAQK3/f/7/AABArf9//v8AAECt/3/+/wAAQK3yXiaPAABArfJeJo8AAECt8l4mjwAAvdLyXiaPAABArf9/AAAAAECt/38AAAAAQK3/fwAAAABArf9/AAAAAECt/38AAAAAQK3/fwAAAABArf9/AAAAAECt2V4scQAAvdLZXixxAAC90tleLHEAAL3S2V4scQAAQK3/fwAAAABArf9/AAAAAECt/38AAAAAQK3/fwAAAABAranp9ffwjfDtqen19+KaPejxXw0g8d/4r/FfDSDx3/ivCYfMlkW58OwJh8yWg6+N8lebpuRYG6vNV5um5Fgbq833yKvM////v/fIq8z///+/98irzP///7/3yKvM////v1KzB7f///8/UrMHt////z9Sswe3////P1KzB7f///8/UrP3SP///z9Ss/dI////P1Kz90j///8/UrP3SP///z/77g/A////v/vuD8D///+/++4PwP///7/77g/A////v0C/iqb///8/QL+Kpv///z9Av4qm////P0C/iqb///8/c9m+wP///79z2b7A////v3PZvsD///+/c9m+wP///7/w7stE////v/Duy0T///+/8O7LRP///7/w7stE////v1TFg5D///8/VMWDkP///z9UxYOQ////P1TFg5D///8/fKX//////798pf//////v3yl//////+/fKX//////7//f4Ha////P/9/gdr///8//3+B2v///z//f4Ha////P/9/fSX///8//399Jf///z//f30l////P/9/fSX///8/gdr//////7+B2v//////v4Ha//////+/gdr//////7/uv/tu////P+6/+27///8/7r/7bv///z/uv/tu////P/vu7z////+/++7vP////7/77u8/////v/vu7z////+/7r8Dkf///z/uvwOR////P+6/A5H///8/7r8Dkf///z/3yFMz////v/fIUzP///+/98hTM////7/3yFMz////v////3//f+HD////f/9/arj///9//3+90v///3//f0+6////f/9/YLX///9//39gtf///3//f+HD////f/9/arj///9//3+90v9/gVr///8//3+BWv///z//f4Fa////P/9/gVr///8/gdoAAP///7+B2gAA////v4HaAAD///+/gdoAAP///7//f32l////P/9/faX///8//399pf///z//f32l////P3ylAAD///+/fKUAAP///798pQAA////v3ylAAD///+/y8INYf///z/Lwg1h////P8vCDWH///8/y8INYf///z8JvwVy////Pwm/BXL///8/Cb8Fcv///z8JvwVy////P/9/beVt5f///39t5W3l////f23lbeX///9/beWRGv//9dIqR////7/10ipH////v/XSKkf///+/9dIqR////7+evjFM////P56+MUz///8/nr4xTP///z+evjFM////P/9/uMBGP////3+4wLjA////f7jAuMD///9/uMBGP///DOEyvf///78M4TK9////vwzhMr3///+/DOEyvf///7+53D46////v7ncPjr///+/udw+Ov///7+53D46////v/9/iJuIm/7//3+Im4ib/v//f4ibdmT///9/iJuIm/7/KccIrf///z8pxwit////PynHCK3///8/KccIrf///z8F8vTA////vwXy9MD///+/BfL0wP///78F8vTA////v/9/dmT/fzry/392ZP9/OvL/f3Zk/3868v9/dmT/fzry2/NruP///7/b82u4////v9vza7j///+/2/NruP///78Jv/mN////Pwm/+Y3///8/Cb/5jf///z8Jv/mN////P/9/Rj//f6Lf/39GP/9/ot//f0Y//3+i3/9/Rj//f6LfDOHMQv///78M4cxC////vwzhzEL///+/DOHMQv///789urlc////Pz26uVz///8/Pbq5XP///z89urlc////P/9/kRr/f0jN/3+RGv9/SM3/f5Ea/39Izf9/kRr/f0jN9dLUuP///7/10tS4////v/XS1Lj///+/9dLUuP///78wzF/B////vzDMX8H///+/MMxfwf///78wzF/B////v5GaAAD/f7aykZoAAP9/trKRmgAA/3+2spGaAAD/f7ayKcf2Uv///z8px/ZS////PynH9lL///8/Kcf2Uv///z8wzJ8+////vzDMnz7///+/MMyfPv///78wzJ8+////v7lAAAD/f1yguUAAAP9/XKC5QAAA/39coLlAAAD/f1ygk8fcc////z+Tx9xz////P5PH3HP///8/k8fcc////z8F8go/////vwXyCj////+/BfIKP////78F8go/////v3XkAAD/f8SNdeQAAP9/xI115AAA/3/EjXXkAAD/f8SNbWX+/////79tZf7/////v21l/v////+/bWX+/////7+53MDF////v7ncwMX///+/udzAxf///7+53MDF////v3Xk//92ZACAdeT//3ZkAIB15P//dmQAgHXk//+ImwCA/v//f////7/+//9/////v/7//3////+//v//f////7/MwvGe////P8zC8Z7///8/zMLxnv///z/MwvGe////P0W///+5wACARb///7nAAIBFv///ucAAgEW///+5wACAk8cijP///z+TxyKM////P5PHIoz///8/k8cijP///z+evs2z////P56+zbP///8/nr7Ns////z+evs2z////P5Ga//+RGgCAkZr//5EaAICRmv//beUAgJGa//+RGgCAPbpFo////z89ukWj////Pz26RaP///8/PbpFo////z/b85NH////v9vzk0f///+/2/OTR////7/b85NH////v/7//3//f/8//v//f/9//z/+//9//3//P/7//3//f/8//v//f/9//z/+//9//3//P/7//3//f/8//v//f/9//z/+//9//3//P/7//3//f/8//v//f/9//z/+//9//3//P/9/RT////8//39FP////z//f0U/////P/9/RT////8/uUAAAP///7+5QAAA////v7lAAAD///+/uUAAAP///7//f23l////P/9/beX///8//39t5f///z//f23l////P0W///////+/Rb///////79Fv///////v0W///////+//392ZP///z//f3Zk////P/9/dmT///8//392ZP///z+RmgAA////v5GaAAD///+/kZoAAP///7+RmgAA////v3Xk//////+/deT//////7915P//////v3Xk//////+//3+Im////z//f4ib////P/9/iJv///8//3+Im////z//f5Ea////P/9/kRr///8//3+RGv///z//f5Ea////P3XkAAD///+/deQAAP///7915AAA////v3XkAAD///+//3+5wP///z//f7nA////P/9/ucD///8//3+5wP///z9FvwAA////v0W/AAD///+/Rb8AAP///79FvwAA////v////3////+/////f////7////9/////v////3////+/////f////7////9/////v////3////+/////f////7////9/////v////3////+//v//f////7/+//9/////v/7//3////+//v//f////7////9/////v////3////+/////f////7////9/////v////3////+/////f////7/+//9/////P/7//3////8//v//f////z/+//9/////P////3////+/////f////7////9/////v////3////+/////f////7////9/////v////3////+/////f////7////9/////v////3////+/////f////7////9/////v6UYh5P///8/pRiHk////z+lGIeT////P6UYh5P///8//39GP////z//f0Y/////P/9/Rj////8//39GP////z+Rmv//////v5Ga//////+/kZr//////7+Rmv//////v/9/uMD///8//3+4wP///z//f7jA////P/9/uMD///8/deQAAP///7915AAA////v3XkAAD///+/deQAAP///7//f5Ea////P/9/kRr///8//3+RGv///z//f5Ea////P/9/iJv///8//3+Im////z//f4ib////P/9/iJv///8/deT//////7915P//////v3Xk//////+/deT//////7+RmgAA////v5GaAAD///+/kZoAAP///7+RmgAA////v/9/dmT///8//392ZP///z//f3Zk////P/9/dmT///8/Rb///////79Fv///////v0W///////+/Rb///////7//f23l////P/9/beX///8//39t5f///z//f23l////PwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//v+cNgGT///+/5w2AZP///7/nDYBk////v+cNgGT///+/fhsYcv///z9+Gxhy////P34bGHL///8/fhsYcv///z+HE6WY////v4cTpZj///+/hxOlmP///7+HE6WY////v34b5o3///8/fhvmjf///z9+G+aN////P34b5o3///8/PwWcZ////78/BZxn////vz8FnGf///+/PwWcZ////7+lGHds////P6UYd2z///8/pRh3bP///z+lGHds////P2MYPoX///8/Yxg+hf///z9jGD6F////P2MYPoX///8/PwVimP///78/BWKY////vz8FYpj///+/PwVimP///7+HE1ln////v4cTWWf///+/hxNZZ////7+HE1ln////v2MYwHr///8/YxjAev///z9jGMB6////P2MYwHr///8/5w1+m////7/nDX6b////v+cNfpv///+/5w1+m////7////9//38AgP///3//fwCA////f/9/AID///9//38AgP7//3//fwCA/v//f/9/AID+//9//38AgP7//3//fwCA////f/9/AID///9//38AgP///3//fwCA////f/9/AID///9//38AgP///3//fwCA////f/9/AID///9//38AgP///3//fwCA////f/9/AID///9//38AgP///3//fwCA/v//f/9/AID+//9//38AgP7//3//fwCA/v//f/9/AID///9//38AgP///3//fwCA////f/9/AID///9//38AgCSkZ9b///8/JKRn1v///z8kpGfW////PySkZ9b///8/K8bj5f///78rxuPl////vyvG4+X///+/K8bj5f///78amixG////PxqaLEb///8/GposRv///z8amixG////P5ip2dv///+/mKnZ2////7+Yqdnb////v5ip2dv///+/6wReF////7/rBF4X////v+sEXhf///+/6wReF////7/AmDaF////P8CYNoX///8/wJg2hf///z/AmDaF////P8CYyHr///8/wJjIev///z/AmMh6////P8CYyHr///8/6wSg6P///7/rBKDo////v+sEoOj///+/6wSg6P///78AAP9/YI0AgAAA/3//f5aFAAD/f/9/cIoAAP9/nnIAgAAA/39mcQCAAAD/f/9/AID/f6Gj////P/9/oaP///8/Zaf//////79lp///////v/9/XVz///8//39dXP///z9c3AAA////v1zcAAD///+/V61tP/7/PpJXrW0///8+kletbT/+/z6SV61tP/7/v+2DqezF/v+/7YOp7MX+/z6Sg6nsxf7/PpKDqezF/v+/7e6oLt7///+/7qgu3v///7/uqC7e////v+6oLt7///+/s5QI4/7/v+2zlAjj/v+/7bOUCOP+/7/ts5T2HP7/v+2zlPYc/v+/7bOU9hz+/7/t7xzWUf///7/vHNZR////v+8c1lH///+/7xzWUf///7+DqRI6/v+/7YOpEjr+/z6Sg6kSOv7/PpKDqRI6/v+/7VetkcD+/7/tV62RwP7/v+1XrZHA//+/7VetkcD+/7/tmoicHP7/v6yaiJwc/v+/rJqInBz+/7+skaSUef7/v6yRpJR5//+/rJGklHn+/7+skaSUef//v6xRkZM5/v++rFGRkzn+/z/TUZGTOf7/P9NRkZM5/v++rO2SEcH+/7+s7ZIRwf7/v6ztkhHB/v+/rO2SEcH+/7+s7ZLtPv7/P9Ptku0+/v8/0+2S7T7+/z/T7ZLtPv//v6xRkWvG/v+/rFGRa8b+/z/TUZFrxv7/P9NRkWvG/v+/rKKXa7H+/z/Topdrsf7/P9Oil2ux/v8/06KXa7H+/z/Tmohi4/7/vqyaiGLj/v++rJqIYuP+/76srW6TOX7Z/7+tbpM5ftn/v61ukzl+2f+/rW6TOX7Z/78RbRHBftn/vxFtEcF+2f+/EW0RwX7Z/78RbRHBftn/vxFt7T5+2f+/EW3tPn7Z/78Rbe0+ftn/vxFt7T5+2f+/rW5rxn7Z/7+tbmvGftn/v61ua8Z+2f+/rW5rxn7Z/79caGuxftn/v1xoa7F+2f+/XGhrsX7Z/79caGuxftn/v2R3YuN+2f+/ZHdi437Z/79kd2Ljftn/v2R3nBx+2f+/ZHecHH7Z/79kd5wcftn/v21blHl+2f+/bVuUeX7Z/79tW5R5ftn/v21blHl+2f+/p1JtP32k/7+nUm0/faT/v6dSbT99pP+/p1JtP32k/797VuzFfaT/v3tW7MV+pP+/e1bsxX6k/797VuzFfaT/v3xH8bB9pP+/fEfxsH2k/798R/GwfaT/v3xH8bB9pP+/S2sI432k/79LawjjfaT/v0trCON9pP+/S2v2HH6k/79La/YcfqT/v0tr9hx+pP+/AymteX2k/78DKa15faT/vwMprXl9pP+/AymteX2k/797VhI6fqT/v3tWEjp9pP+/e1YSOn2k/797VhI6fqT/v6dSkcB9pP+/p1KRwH2k/7+nUpHAfaT/v6dSkcB9pP+/8jB7x4Fb/7/yMHvHgVv/v/Iwe8eBW/+/8jB7x4Fb/78JY0rrgVv/vwljSuuBW/+/CWNK64Fb/78JY7QUgVv/vwljtBSBW/+/CWO0FIFb/79SBvxWgVv/v1IG/FaBW/+/Ugb8VoFb/79SBvxWgVv/v+xFhCmBW/+/7EWEKYFb/7/sRYQpgVv/v+xFhCmBW/+/kUCm0oFb/7+RQKbSgVv/v5FAptKBW/+/kUCm0oFb/7+RQFgtgVv/v5FAWC2BW/+/kUBYLYFb/7+RQFgtgVv/v+xFetaBW/+/7EV61oFb/7/sRXrWgVv/v+xFetaBW/+/YmObCIAm/79iY5sIgCb/v2JjmwiAJv+/agaSJIAm/79qBpIkgCb/v2oGkiSAJv+/agaSJIAm/79sRlERgCb/v2xGURGAJv+/bEZREYAm/79sRlERgCb/vxJBEe2AJv+/EkER7YAm/78SQRHtgCb/vxJBEe2AJv+/EkHtEoAm/78SQe0SgCb/vxJB7RKAJv+/EkHtEoAm/79sRq3ugCb/v2xGre6AJv+/bEat7oAm/79sRq3ugCb/v2sxXOiAJv+/azFc6IAm/79rMVzogCb/v2sxXOiAJv+/Y2Nk94Am/79jY2T3gCb/v2NjZPeAJv+/7L4R7QAAP9PsvhHtAAA/0+y+Ee0AAD/T7L4R7QAAP9Psvu0SAAA/0+y+7RIAAD/T7L7tEgAAP9Psvu0SAAA/05K5re4AAL+skrmt7gAAv6ySua3uAAC/rJK5re4AAL+sk85c6AAAv6yTzlzoAAA/05POXOgAAL+sk85c6AAAP9OcnGP3AAC/rJycY/cAAL+snJxj9wAAv6ycnJsIAAC/rJycmwgAAL+snJybCAAAv6yT+ZIkAAA/05P5kiQAAD/Tk/mSJAAAv6yT+ZIkAAA/05K5UREAAL+skrlREQAAv6ySuVERAAC/rJK5UREAAL+sbb9YLQAAPpJtv1gtAAA+km2/WC0AAL/tbb9YLQAAPpISunrWAAA+khK6etYAAD6SErp61gAAv+0SunrWAAA+ks6hEdf///8/zqER1////z/OoRHX////P86hEdf///8/9ZxK6wAAv+31nErrAAC/7fWcSusAAL/t9Zy0FAAAv+31nLQUAAC/7fWctBQAAL/tGS4iY////z8ZLiJj////PxkuImP///8/GS4iY////z8SuoQpAAA+khK6hCkAAD6SErqEKQAAPpISuoQpAAC/7W2/ptIAAD6Sbb+m0gAAPpJtv6bSAAA+km2/ptIAAD6ScdO+5P///79x077k////v3HTvuT///+/cdO+5P///7+TzqIXAAA/05POohcAAD/Tk86iFwAAP9OTzqIXAAA/02sxoheAJv+/azGiF4Am/79rMaIXgCb/v2sxoheAJv+/8jCDOIFb/7/yMIM4gVv/v/IwgziBW/+/8jCDOIFb/798Rw1PfaT/v3xHDU99pP+/fEcNT32k/798Rw1PfaT/v1xok05+2f+/XGiTTn7Z/79caJNOftn/v1xok05+2f+/opeTTv7/P9Oil5NO/v8/06KXk07+/z/TopeTTv7/P9N6+irZ////v3r6Ktn///+/evoq2f///796+irZ////vy2bblP///8/LZtuU////z8tm25T////Py2bblP///8/lPls2wAAP9OU+WzbAAA/05T5bNsAAD/TlPls2wAAP9NqBmzbgCb/v2oGbNuAJv+/agZs24Am/79qBmzbgCb/v1IGA6mBW/+/UgYDqYFb/79SBgOpgVv/v1IGA6mBW/+/AylRhn2k/78DKVGGfqT/vwMpUYZ+pP+/AylRhn6k/79tW2qGftn/v21baoZ+2f+/bVtqhn7Z/79tW2qGftn/v5Gkaob//z/TkaRqhv7/P9ORpGqG//8/05Gkaob+/z/TefrTJv///795+tMm////v3n60yb///+/efrTJv///7/7yYyZ////P/vJjJn///8/+8mMmf///z/7yYyZ////PxTH1Kv///8/FMfUq////z8Ux9Sr////PxTH1Kv///8/Lef+/9GYAIAt5/7/0ZgAgC3n/v/RmACALef+/9GYAIB7zTR2////P3vNNHb///8/e800dv///z97zTR2////PxrFaov///8/GsVqi////z8axWqL////PxrFaov///8/csP+/3NDAIByw/7/i7wAgHLD/v+LvACAcsP+/3NDAIBy5vxJ////v3Lm/En///+/cub8Sf///79y5vxJ////v37BbmL///8/fsFuYv///z9+wW5i////P37BbmL///8/k2L+/5LiAICTYv7/kuIAgJNi/v+S4gCAk2L+/5LiAIAz9oKy////vzP2grL///+/M/aCsv///78z9oKy////vynUFUf///+/KdQVR////78p1BVH////vynUFUf///+//3+S4mwd////f5LibB3///9/kuJsHf///3+S4mwd//+N2mBP////v43aYE////+/jdpgT////7+N2mBP////vynU6rj///+/KdTquP///78p1Oq4////vynU6rj///+//3+LvHND////f4u8c0P///9/i7xzQ////3+LvHND///8yXJm////P/zJcmb///8//MlyZv///z/8yXJm////P37BkJ3///8/fsGQnf///z9+wZCd////P37BkJ3///8//3/QmNCY////f9CYLmf///9/0JguZ////3/QmNCY//8z9nxN////vzP2fE3///+/M/Z8Tf///78z9nxN////v5P0G0X///+/k/QbRf///7+T9BtF////v5P0G0X///+//38uZ/9/lvP/fy5n/3+W8/9/Lmf/f5bz/38uZ/9/lvN7zcqJ////P3vNyon///8/e83Kif///z97zcqJ////PxrFlHT///8/GsWUdP///z8axZR0////PxrFlHT///8//39zQ/9/uOH/f3ND/3+44f9/c0P/f7jh/39zQ/9/uOFfz3Gl////P1/PcaX///8/X89xpf///z9fz3Gl////PxTHKlT///8/FMcqVP///z8UxypU////PxTHKlT///8//39sHf9/tc7/f2wd/3+1zv9/bB3/f7XO/39sHf9/tc4t5wAA////vy3nAAD///+/LecAAP///78t5wAA////v27ifkH///+/buJ+Qf///79u4n5B////v27ifkH///+/k2IAAP9/SbGTYgAA/39JsZNiAAD/f0mxk2IAAP9/SbH///9/////v////3////+/////f////7////9/////v////3////+/////f////7////9/////v////3////+/////f////7////9/////v////3////+/////f////7////9/////v////3////+/cuYCtv///79y5gK2////v3LmArb///+/cuYCtv///7+MPAAA/39Fnow8AAD/f0WejDwAAP9/RZ6MPAAA/39Fno3anrD///+/jdqesP///7+N2p6w////v43anrD///+/k/Tjuv///7+T9OO6////v5P047r///+/k/Tjuv///78t5wAA/39ojC3nAAD/f2iMLecAAP9/aIwt5wAA/39ojG7if77///+/buJ/vv///79u4n++////v27if77///+/X8+NWv///z9fz41a////P1/PjVr///8/X8+NWv///z/+//9//38AAP7//3//fwAA/v//f/9/AAD+//9//38AAP7//3//fwAA/v//f/9/AAD+//9//38AAP7//3//fwAA/v//f/9/AAD+//9//38AAP7//3//fwAA/v//f/9/AAD/f4u8////P/9/i7z///8//3+LvP///z//f4u8////P/9/c0P///8//39zQ////z//f3ND////P/9/c0P///8/Lef//////78t5///////vy3n//////+/Lef//////7+MPAAA////v4w8AAD///+/jDwAAP///7+MPAAA////v/9/kuL///8//3+S4v///z//f5Li////P/9/kuL///8//38uZ////z//fy5n////P/9/Lmf///8//38uZ////z+TYgAA////v5NiAAD///+/k2IAAP///7+TYgAA////v2ud//////+/a53//////79rnf//////v2ud//////+//3/QmP///z//f9CY////P/9/0Jj///8//3/QmP///z//f2wd////P/9/bB3///8//39sHf///z//f2wd////P3LD/v////+/csP+/////79yw/7/////v3LD/v////+//39zQ////z//f3ND////P/9/c0P///8//39zQ////z////9/////v////3////+/////f////7////9/////v////3////+/////f////7/+//9/////P/7//3////8//v//f////z/+//9/////P////3////+/////f////7////9/////v////3////+/////f////7////9/////v////3////+/////f////7////9/////v////3////+/////f////7////9/////vwoBXYX///+/CgFdhf///78KAV2F////vwoBXYX///+//3+LvP///z//f4u8////P/9/i7z///8//3+LvP///z8t5wAA////vy3nAAD///+/LecAAP///78t5wAA////v3LD//////+/csP//////79yw///////v3LD//////+//39sHf///z//f2wd////P/9/bB3///8//39sHf///z//f9CY////P/9/0Jj///8//3/QmP///z//f9CY////P2ud//////+/a53//////79rnf//////v2ud//////+/a50AAP///79rnQAA////v2udAAD///+/a50AAP///7//fy5n////P/9/Lmf///8//38uZ////z//fy5n////P/9/kuL///8//3+S4v///z//f5Li////P/9/kuL///8/csMAAP///79ywwAA////v3LDAAD///+/csMAAP///78t5///////vy3n//////+/Lef//////78t5///////v4QGnKH///+/hAacof///7+EBpyh////v4QGnKH///+/+gUrff///z/6BSt9////P/oFK33///8/+gUrff///z/6BdOC////P/oF04L///8/+gXTgv///z/6BdOC////PwoBoXr///+/CgGhev///78KAaF6////vwoBoXr///+/1AL6hf///7/UAvqF////v9QC+oX///+/1AL6hf///78OBRp8////Pw4FGnz///8/DgUafP///z8OBRp8////P10FCYH///8/XQUJgf///z9dBQmB////P10FCYH///8/5AMNhf///7/kAw2F////v+QDDYX///+/5AMNhf///7/kA/F6////v+QD8Xr///+/5APxev///7/kA/F6////v10F9X7///8/XQX1fv///z9dBfV+////P10F9X7///8/DgXkg////z8OBeSD////Pw4F5IP///8/DgXkg////z/UAgR6////v9QCBHr///+/1AIEev///7/UAgR6////v////3/+////////f/////////9//v///////3//////////f/7///////9//v///////3/+////////f/////////9//////////3/+////////f/////////9//v///////3//////////f/7///////9//////////3/+////////f/////////9//////72menr///8/vaZ6ev///z+9pnp6////P72menr///8/NJuOrP///z80m46s////PzSbjqz///8/NJuOrP///z9w0zkb////v3DTORv///+/cNM5G////79w0zkb////v76mhIX///8/vqaEhf///z++poSF////P76mhIX///8/CB09rv///78IHT2u////vwgdPa7///+/CB09rv///7/Aodwo////P8Ch3Cj///8/wKHcKP///z/Aodwo////P92owiH///+/3ajCIf///7/dqMIh////v92owiH///+/Li70nP///z8uLvSc////Py4u9Jz///8/Li70nP///z8ZnDsIAABArRmcw/cAAECtGZwg7AAAvpIZnCDsAAC+khmc3hMAAEDt3pPl4/7/QO3ekxkc/v9A7d6TGRz+/0DtOojk4/7/vdI6iBkc/v9ArcR35eOB2v+/xHcZHIHa/7/oGOVI////v+gY5Uj///+/6BgZt////7/oGBm3////vxIph8KCWv+/Eil3PYJa/7/lYzsIfSX/v+Vjw/d9Jf+/GjfnmP///z8aN+eY////Pxk3F2f///8/GTcXZ////z//f3yl////P/9/fKX///8/fKUAAP///798pQAA////v4Ha/v////+/gdr+/////7//f4Ja////P/9/glr///8/fSX+/////799Jf7/////v/9/glr///8//3+CWv///z99JQAA////v30lAAD///+//3+B2v///z//f4Ha////P3yl//////+/fKX//////7//f30l////P/9/fSX///8//3+B2v///z//f4Ha////P30lAAD///+/fSUAAP///78AAP9//3+dygAA/3//f5e4AAD/f/9//78AAP9//3+XuAAA/3//f/+/AAD/f/9/ncp9pQAA////v32lAAD///+//398pf///z//f3yl////P3yl/v////+/fKX+/////7//f30l////P/9/fSX///8/AAD/f/9///8AAP9//3///wAA/3//f///AAD/f/9///8AAP9//3///wAA/3//f///AAD/f/9///8AAP9//3///wAA/3//f///AAD/f/9///8AAP9//3/+/wAA/3//f/7/txjnX////7+3GOdf////v7cY51////+/txjnX////79+JEKR////P34kQpH///8/fiRCkf///z9+JEKR////P50he3n///8/nSF7ef///z+dIXt5////P50he3n///8/QxF9pP///79DEX2k////v0MRfaT///+/QxF9pP///79DEYFb////v0MRgVv///+/QxGBW////79DEYFb////v50hg4b///8/nSGDhv///z+dIYOG////P50hg4b///8/fiS8bv///z9+JLxu////P34kvG7///8/fiS8bv///z+3GBeg////v7cYF6D///+/txgXoP///7+3GBeg////v4QGYl7///+/hAZiXv///7+EBmJe////v4QGYl7///+/FyBIZ////z8XIEhn////PxcgSGf///8/FyBIZ////z8XILaY////Pxcgtpj///8/FyC2mP///z8XILaY////PzsI5WN8pf//OwjlY3yl//87COVjfKX//3UZEimCWv//dRkSKYJa//91GRIpglr//3UZEimCWv//Id/ID4Ja//8h38gPfKX//yHfyA+CWv7/Id/ID3yl/v/I7c9Bglr//8jtz0GCWv//yO3PQYJa/v/I7c9BfKX+/zYSz0F8pf//NhLPQXyl//82Es9Bglr+/zYSz0F8pf7/3SDID4Ja///dIMgPfKX//90gyA+CWv7/3SDID3yl/v+J5hIpfKX//4nmEil8pf//ieYSKXyl//+J5hIpfKX//8P35WOCWv7/w/flY4Ja/v/D9+Vjglr+/wXUz0GB2v//BdTPQYHa//8F1M9Bgdr+/wXUz0F9Jf//+SvPQX0l/v/5K89BfSX+//krz0F9Jf7/+SvPQX0l/v9ZT8gPgdr//1lPyA99Jf7/WU/ID4Ha/v9ZT8gPfSX+/4fCEimB2v7/h8ISKYHa/v+HwhIpgdr+/4fCEimB2v//IOzlY4Ha/v8g7OVjgdr+/yDs5WOB2v7/3hPlY30l///eE+VjfSX//94T5WN9Jf//dz0SKX0l/v93PRIpfSX+/3c9Eil9Jf7/dz0SKX0l/v+lsMgPgdr+/6WwyA99Jf7/pbDID4Ha/v+lsMgPfSX+/zA+BlSB2gCAMD4GVIHaAIAwPgZUgdoAgDA+BlSB2gCAN3CmMH0lAIA3cKYwgdoAgDdwpjB9JQCAN3CmMIHaAIARqYhCfSUAgBGpiEJ9JQCAEamIQn0lAIARqYhCfSUAgOXjIGyB2gCA5eMgbIHaAIDl4yBsgdoAgBkcIGx9JQCAGRwgbH0lAIAZHCBsfSUAgO1WiEKB2gCA7VaIQoHaAIDtVohCgdoAgO1WiEKB2gCAyI+mMH0lAIDIj6YwgdoAgMiPpjB9JQCAyI+mMIHaAIDPwQZUfSUAgM/BBlR9JQCAz8EGVH0lAIDPwQZUfSUAgOXjxHeCWgCA5ePEd4JaAIDl48R3gloAgBkcxHeCWgCAGRzEd4JaAIAZHMR3gloAgO1WiWZ8pQCA7VaJZnylAIDtVolmgloAgO1WiWZ8pQCAyI8hX3ylAIDIjyFfgloAgMiPIV98pQCAyI8hX4JaAIDOwchtfKUAgM7ByG2CWgCAzsHIbXylAIDOwchtgloAgDA+yG2CWgCAMD7IbYJaAIAwPshtfKUAgDA+yG2CWgCANnAhX3ylAIA2cCFfgloAgDZwIV98pQCANnAhX4JaAIARqYlmgloAgBGpiWaCWgCAEamJZoJaAIARqYlmgloAgO1WdZn/f76S7VZ1mf9/vpLtVnWZ/3++ku1WdZn/f76SyI/doP9/vpLIj92g/3++ksiP3aD/f76SyI/doP9/vpLOwTaS/3++ks7BNpL/f76SzsE2kv9/vpLOwTaS/3++kjA+NpL/f76SMD42kv9/vpIwPjaS/3++kjA+NpL/f76SN3DdoP9/vpI3cN2g/3++kjdw3aD/f76SN3DdoP9/vpIRqXWZ/3++khGpdZn/f76SEal1mf9/vpIRqXWZ/3++kuXjOoj/f76S5eM6iP9/vpLl4zqI/3++khkcOoj/f76SGRw6iP9/vpIZHDqI/3++ki8++Kv/f0CtLz74q/9/QK0vPvir/39ArS8++Kv/f0Ct5UgX5////7/lSBfn////v+VIF+f///+/5UgX5////78RqXe9/39ArRGpd73/f0CtEal3vf9/QK0RqXe9/39AreXj3pP/f0Ct5ePek/9/QK3l496T/39ArRkc3pP/f0CtGRzek/9/QK0ZHN6T/39Are1Wd73/f0Ct7VZ3vf9/QK3tVne9/39Are1Wd73/f0CtGbcX5////78Ztxfn////vxm3F+f///+/GbcX5////7/Pwfir/39Arc/B+Kv/f0Ctz8H4q/9/QK3Pwfir/39ArTZwPMv///8/NnA8y////z82cDzL////PzZwPMv///8/h8Ls1v9/vdKHwuzW/3+90ofC7Nb/f73Sh8Ls1v9/vdIg7Bmc/3+90iDsGZz/f73SIOwZnP9/vdLeExmc/3+90t4TGZz/f73S3hMZnP9/vdJ3PezW/3+90nc97Nb/f73Sdz3s1v9/vdJ3PezW/3+90sK0NvD///+/wrQ28P///7/CtDbw////v8K0NvD///+/BdQvvv9/vdIF1C++/3+90gXUL77/f73SBdQvvv9/vdL5Ky++/3+90vkrL77/f73S+Ssvvv9/vdL5Ky++/3+90jsIGZz/f0DtOwgZnP9/QO07CBmc/39A7XUZ7Nb/f0DtdRns1v9/QO11GezW/39A7XUZ7Nb/f0DtId828P9/QO0h3zbw/39A7SHfNvD/f0DtId828P9/QO3I7S++/39A7cjtL77/f0DtyO0vvv9/QO3I7S++/39A7TYSL77/f0DtNhIvvv9/QO02Ei++/39A7TYSL77/f0Dt3SA28P9/QO3dIDbw/39A7d0gNvD/f0Dt3SA28P9/QO2J5uzW/39A7Ynm7Nb/f0Dtiebs1v9/QO2J5uzW/39A7cP3GZz/f0Dtw/cZnP9/QO3D9xmc/39A7U2QBsX///8/TZAGxf///z9NkAbF////P02QBsX///8/qT7R1v///7+pPtHW////v6k+0db///+/qT7R1v///7/3urHv////v/e6se////+/97qx7////7/3urHv////v7JvBsX///8/sm8Gxf///z+ybwbF////P7JvBsX///8/55jlyP///z/nmOXI////P+eY5cj///8/55jlyP///z88Szbw////vzxLNvD///+/PEs28P///788Szbw////vxdn5cj///8/F2flyP///z8XZ+XI////Pxdn5cj///8/yI88y////z/IjzzL////P8iPPMv///8/yI88y////z99pf9/////P32l/3////8/faX/f////z99pf9/////PwAAfKX///+/AAB8pf///78AAHyl////vwAAfKX///+//v+B2v///7/+/4Ha////v/7/gdr///+//v+B2v///7+CWv9/////P4Ja/3////8/glr/f////z+CWv9/////P1TB0db///+/VMHR1v///79UwdHW////v1TB0db///+/0lapvv///z/SVqm+////P9JWqb7///8/0lapvv///z8sqam+////Pyypqb7///8/LKmpvv///z8sqam+////PwdFse////+/B0Wx7////78HRbHv////vwdFse////+//v+B2v///7/+/4Ha////v/7/gdr///+//v+B2v///7+CWv9/////P4Ja/3////8/glr/f////z+CWv9/////PwAAfSX///+/AAB9Jf///78AAH0l////vwAAfSX///+/gdr/f////z+B2v9/////P4Ha/3////8/gdr/f////z///3yl////v///fKX///+///98pf///7///3yl////v30l/3////8/fSX/f////z99Jf9/////P30l/3////8/gdr/f////z+B2v9/////P4Ha/3////8/gdr/f////z8AAIHa////vwAAgdr///+/AACB2v///78AAIHa////v/9//v87lf+//3/+/zuV/7//f/7//3//v/9//v98pf+//3/+/y9x/7//f/7/L3H/v/9//v+CWv+//3/+//9//7//f/7/L3H/v/9//v8vcf+//3/+//9//7//f/7/glr/v/9//v87lf+//3/+/zuV/7//f/7/fKX/v/9//v//f/+/AAB8pf///78AAHyl////vwAAfKX///+/AAB8pf///798pf9/////P3yl/3////8/fKX/f////z98pf9/////P///fKX///+///98pf///7///3yl////v///fKX///+/fSX/f////z99Jf9/////P30l/3////8/fSX/f////z9U5nrrAQAps1Tmeut4AOuyeOtOwi0AwOR4607CVgAom3jrTsKDAenkk+Y/sAQAPY6T5j+wlQC+8b7Papnx/z6Ovs9qmYD/3vGwvYaU2/8+m7C9hpT2/VzlsL2GlHv+FZuElKmZ/P/VzISUqZmG/xPNe2utmVPm/r97a62Z1uXCv/U9M5NCshbA9T0zkyCy2L/1PTOTB7KgwBwuqpZ7nP2/HC6qlkucx7+qFhuuhGMBwKoWG66zYzfA/3/+//7//7//f/7//v//vzQT9L27Teq/NBP0vaJO0sA0E/S9901ev64Ze+usGQDArhl76ygaO8D///9/gloAgP///3+CWgCA////f4JaAID///9/gloAgAAA/3//vwCAAAD/f/9/vpIAAP9/gdoAgAAA/3+B2gCAAAD/f/9/vpIAAP9//3++kgAA/3//f76SAAD/f4HaAIAAAP9/gdoAgAAA/3//f0Ct////f4HaAID///9//3++kv///3//f76S////f/9/vpL///9//3/5mf///3//f76S////f/9/vpL///9//39Arf///3//f0Ct////f/7///////9/gdoAgP///3/+////////f/Kz//////9//7///////38qi///////f/9/vdL///9//39A7f///3//f73S////f/9/vdL///9//3+90v///3+CWgCA////f4JaAIAAAP9/gloAgAAA/3+CWgCAAAD/f4JaAIAAAP9/gloAgP///399JQCA////f30lAID///9/fSUAgG4+Sdj///+/bj5J2P///78AAP9/fSUAgAAA/399JQCAAAD/f30lAIAAAP9/fSUAgEpYbb7///8/Slhtvv///z/LRPDu////v8tE8O7///+/AAD/f30l/v8AAP9/fSX+/wAA/399Jf7/AAD/f30l/v+DkFTF////P4OQVMX///8/AAD/f4Ha//8AAP9/gdr//wAA/399Jf7/AAD/f30l/v/+//9/gdr+//7//3+B2v7//v//f4Ha/v/+//9/gdr+/wAA/398pf//AAD/f/9/QO0AAP9/fKX//wAA/398pf//AAD/f/9/QO0AAP9/fKX//wAA/3//f/jZAAD/f/9/vdIAAP9//3+90gAA/3//f73SAAD/f3yl//8AAP9/fKX//wAA/3//f0DtAAD/f/9/QO0scdle/39A7f7//3//f0Dt/v//f/9/QO3+//9//39A7f7//3//f0DtJo/yXv9/QO0mj/Je/39A7f///3//f73S////f/9/vdL///9//3+90v7//3//f73S/v//f/9/vdL+//9//3+90qhy7C7/f73SqHLsLv9/vdJHjpYv/3+90keOli//f73SAAD/f/9/QK0AAP9//39ArQAA/3//f0CtAAD/f/9/QK2Vr0cO/39ArZWvRw7/f0CtAAD/f/9/QK0AAP9//39ArQAA/3//f0Ct/v//f/9/QK3+//9//39Arf///3//f0Ct////f/9/QK0TUVcN/39ArRNRVw3/f0Ct8d4nD/9/vpLx3icP/3++kgAA/3//f76SAAD/f/9/vpIAAP9//3++kgAA/3//f76SJSHSDv9/vpLj7D3BfKUAgOPsPcF8pQCA4+w9wXylAIDj7D3BgloAgIkR37uCWgCAiRHfu3ylAICJEd+7fKUAgIkR37uCWgCAphe/0IJaAICmF7/QgloAgKYXv9B8pQCAphe/0IJaAIDcCD+egloAgNwIP56CWgCA3Ag/noJaAIAi9z+efKUAgCL3P558pQCAIvc/nnylAIBC3AH6fKUAgELcAfqCWgCAQtwB+nylAIBC3AH6gloAgHXu37uCWgCAde7fu3ylAIB17t+7fKUAgHXu37uCWgCAGxM8wXylAIAbEzzBfKUAgBsTPMF8pQCAGxM8wYJaAICb6j+efSUAgJvqP559JQCAm+o/nn0lAIC5qQH6fSUAgLmpAfp9JQCAuakB+n0lAIC5qQH6fSUAgKjV37uB2gCAqNXfu30lAICo1d+7fSUAgKjV37uB2gCAIS48wX0lAIAhLjzBgdoAgCEuPMF9JQCAIS48wYHaAIDd0T3BgdoAgN3RPcGB2gCA3dE9wYHaAIDd0T3BgdoAgFYq37uB2gCAVirfu30lAIBWKt+7fSUAgFYq37uB2gCAGTm/0IHaAIAZOb/QfSUAgBk5v9CB2gCAGTm/0H0lAIBjFT+efSUAgGMVP559JQCAYxU/nn0lAIAfxFWqfSX+/x/EVaqB2v//H8RVqoHa/v8fxFWqfSX+/z1BIa59Jf//PUEhroHa//89QSGufSX+/z1BIa6B2v//wb4hroHa///BviGugdr//8G+Ia59Jf//wb4hroHa/v/fO1WqfSX//987VaqB2v7/3ztVqoHa/v/fO1WqfSX+/8BQGbmB2v//wFAZuYHa///AUBm5fSX//8BQGbmB2v//QB5jlX0l//9AHmOVfSX//0AeY5V9Jf//v+FjlYHa/v+/4WOVgdr+/7/hY5WB2v7//YVF1n0l///9hUXWfSX///2FRdZ9Jf///YVF1n0l///BvhuTglr//8G+G5N8pf//wb4bk4Ja/v/BvhuTfKX+/987iJF8pf//3zuIkYJa///fO4iRglr+/987iJF8pf//wFCml3yl///AUKaXglr//8BQppd8pf//wFCml4Ja/v9AHtuIfKX//0Ae24h8pf//QB7biHyl//+/4duIglr//7/h24iCWv//v+HbiIJa///9hbujfKX///2Fu6N8pf///YW7o3yl///9hbujfKX//x/EiJF8pf//H8SIkYJa//8fxIiRglr//x/EiJF8pf//PUEbk3yl//89QRuTfKX//z1BG5OCWv7/PUEbk3yl/v/AUFho/39A7cBQWGj/f0DtwFBYaP9/QO3AUFho/39A7UAeI3f/f0DtQB4jd/9/QO1AHiN3/39A7b/hI3f/f0Dtv+Ejd/9/QO2/4SN3/39A7f2FQ1z/f0Dt/YVDXP9/QO39hUNc/39A7f2FQ1z/f0DtH8R2bv9/QO0fxHZu/39A7R/Edm7/f0DtH8R2bv9/QO09QeNs/39A7T1B42z/f0DtPUHjbP9/QO09QeNs/39A7cG+42z/f0Dtwb7jbP9/QO3BvuNs/39A7cG+42z/f0Dt3zt2bv9/QO3fO3Zu/39A7d87dm7/f0Dt3zt2bv9/QO2/4Ztq/3+90r/hm2r/f73Sv+Gbav9/vdL9hbkp/3+90v2FuSn/f73S/YW5Kf9/vdL9hbkp/3+90h/EqVX/f73SH8SpVf9/vdIfxKlV/3+90h/EqVX/f73SPUHdUf9/vdI9Qd1R/3+90j1B3VH/f73SPUHdUf9/vdLBvt1R/3+90sG+3VH/f73Swb7dUf9/vdLBvt1R/3+90t87qVX/f73S3zupVf9/vdLfO6lV/3+90t87qVX/f73SwFDlRv9/vdLAUOVG/3+90sBQ5Ub/f73SwFDlRv9/vdJAHptq/3+90kAem2r/f73SQB6bav9/vdIhLsI+/39ArSEuwj7/f0CtIS7CPv9/QK0hLsI+/39Ard3RwT7/f0Ct3dHBPv9/QK3d0cE+/39Ard3RwT7/f0CtViofRP9/QK1WKh9E/39ArVYqH0T/f0CtViofRP9/QK0ZOT8v/39ArRk5Py//f0CtGTk/L/9/QK0ZOT8v/39ArWMVv2H/f0CtYxW/Yf9/QK1jFb9h/39ArZvqv2H/f0Ctm+q/Yf9/QK2b6r9h/39Arbmp/QX/f0Ctuan9Bf9/QK25qf0F/39Arbmp/QX/f0CtqNUfRP9/QK2o1R9E/39ArajVH0T/f0CtqNUfRP9/QK3j7ME+/3++kuPswT7/f76S4+zBPv9/vpLj7ME+/3++kokRH0T/f76SiREfRP9/vpKJER9E/3++kokRH0T/f76Sphc/L/9/vpKmFz8v/3++kqYXPy//f76Sphc/L/9/vpLcCL9h/3++ktwIv2H/f76S3Ai/Yf9/vpIi979h/3++kiL3v2H/f76SIve/Yf9/vpJC3P0F/3++kkLc/QX/f76SQtz9Bf9/vpJC3P0F/3++knXuH0T/f76Sde4fRP9/vpJ17h9E/3++knXuH0T/f76SGxPCPv9/vpIbE8I+/3++khsTwj7/f76SGxPCPv9/vpIHt1Kz////Pwe3UrP///8/q8z3yP///7+rzPfI////v1Mz98j///+/UzP3yP///78Cke6/////PwKR7r////8/vsBz2f///7++wHPZ////v4qmQL////8/iqZAv////z/xbsvE////P/Fuy8T///8/qbp77////7+punvv////v+8/++7///+/7z/77v///7/8bu6/////P/xu7r////8/D8D77v///78PwPvu////v/dIUrP///8/90hSs////z//f/7///9nh/9//v///2eH/3/+////////f/7/AACdiv9///8AAGD1/3//////////f/7//v+W+P9//v//////WOg/L/9/vpJY6D8v/3++kljoPy//f76SWOg/L/9/vpLlxj8v/39AreXGPy//f0Ct5cY/L/9/QK3lxj8v/39ArT6v5Ub/f73SPq/lRv9/vdI+r+VG/3+90j6v5Ub/f73SPq9YaP9/QO0+r1ho/39A7T6vWGj/f0DtPq9YaP9/QO0+r6aXfKX+/z6vppd8pf//Pq+ml3yl/v8+r6aXfKX//z6vGbl9Jf//Pq8ZuX0l//8+rxm5fSX//z6vGbmB2v//5ca/0IHaAIDlxr/QfSUAgOXGv9CB2gCA5ca/0H0lAIBY6L/QgloAgFjov9CCWgCAWOi/0IJaAIBY6L/QgloAgLwj/QX/f76SvCP9Bf9/vpK8I/0F/3++krwj/QX/f76SRVb9Bf9/QK1FVv0F/39ArUVW/QX/f0CtRVb9Bf9/QK0Berkp/3+90gF6uSn/f73SAXq5Kf9/vdIBerkp/3+90gF6Q1z/f0DtAXpDXP9/QO0BekNc/39A7QF6Q1z/f0DtAXq7o3yl//8Berujglr//wF6u6N8pf//AXq7o4Ja//8BekXWfSX//wF6RdZ9Jf//AXpF1n0l//8BekXWfSX//0VWAfp9JQCARVYB+oHaAIBFVgH6fSUAgEVWAfqB2gCAvCMB+oJaAIC8IwH6gloAgLwjAfqCWgCAvCMB+oJaAIAAAPLkDBv/vwAA8uQMG/+/4wDeSWNK/7/jAN5JZEoAwGMCEGPyZP+/YwIQY/Jk/7/uHJx9DJv/v+4cnH0Mm/+/IDYbf5u1/78gNht/mrX+v/Jk/3/y5P+/8mT/f/Lk/78Mm/9///+FzQyb/3///4XNcMVjff7/jZxwxWN97P9040zf1Xj//3jy1fhMXwAAePJj/XBFAABw42P9cEUSAImc///y5AAAhc3///LkAACFzf//95IAAIL2Bu3/f///gvYG7f9///+C9vgS/3/3kv+/AAD3kgdt/78AAPeSB23/vxmdBZ////8/GZ0Fn////z//fwAA/v//P/9/AAD///8/94UA7P///z/3hQDs////P/eFAOz///8/94UA7P///z/OCC7d////v84ILt3///+/yvWl3P///7/K9aXc////v80GhLb///+/SPMZuv///79I8xm6////v3RjRp7///8//3/+//7/mLr/f/7//v+Yuv9//v/+/5i6/3/+//7/mLr/f/7//v+Yuv9//v/+/5i6/3/+//7/mLpfoQTm////v1+hBOb///+/H13M4f///78fXczh////v4988+3///8/j3zz7f///z+PfPPt////P4988+3///8/Q5qq4f///z9Dmqrh////PxleGuz///+/GV4a7P///78ZXhrs////v1CWvvv///+/UJa++////79Qlr77////v4Vyy+X///8/hXLL5f///z+Fcsvl////P+2P2+T///8/7Y/b5P///z/tj9vk////P+2P2+T///8/sWZw+P///7+xZnD4////v7FmcPj///+/XZ5g8P///79dnmDw////v12eYPD///+/XZ5g8P///7+8Zorf////P7xmit////8/D0F4tP///z8PQXi0////P1PcbcX///+/U9xtxf///79T3G3F////v3wZNuH///+/fBk24f///7/orcaz////P+itxrP///8/WFDYsP///z9YUNiw////P3flP9////+/d+U/3////78VH4vC////vxUfi8L///+/FR+Lwv///7/Zuy24////P9m7Lbj///8/2bstuP///z8vZ16t////Py9nXq3///8/CzFbvP///78LMVu8////v83K6b////+/zcrpv////7/Nyum/////v5uaia3///8/f6R+24AkP9J/pH7bgCQ/0giIVZYlpt73CIhVloKvjfLxXw0g8d/4r/FfDSDx3/ivMen0+FSkIeQx6fT44po96Mfz5uzQtL3cx/Pm7Fm+q9fH8+bsLKxV4YItgFWOq7+Vgi2AVXer0JVVWVTZrtsL1lVZVNmay1vQ8ZG+jvLXmN3xkb6O7dAB4fGRvo4o3xfaCsXzugtFhOIKxfO6C0WE4vTM9UzbsNTk9Mz1TCe91OmBzH2zgUxA5oHMfbOBTEDmGJM3jMDHAeUYkzeMZciz5BiTN4yl0NLgQDe/SD+3n5tAN79IP7efm0DxDO5MxEfUQPEM7tDDh9RA8Qzu+b2I18nxsO0aw+bUyfGw7cXLZ9DJ8bDtAb6L15mTvovcyQbkmZO+i0i94+mZk76Lr9DY4FfJiEb8vDjmV8mIRoqxkt9OkjSO+9YQ3k6SNI5q2ePcTpI0jubQ/uBVWVTZidBt0VVZVNlF3zHa+vWz7De6gdr69bPsUbiI2/r1s+wGv+TXYlbnzP///z9iVufM////P2JW58z///8/t7Bl4v///7+3sGXi////v7ewZeL///+//1sm9P///7//Wyb0////v/9bJvT///+/FJsw1v///z8UmzDW////PxSbMNb///8/QWdp1////z9BZ2nX////P9ehf/b///+/16F/9v///79DTT3f////v0NNPd////+/Q0093////7+xqxbN////P7GrFs3///8/sasWzf///z/LeHfl////P8t4d+X///8/y3h35f///z/LeHfl////PzlFdcz///+/OUV1zP///785RXXM////v7u6xc7///+/u7rFzv///7+Gie3j////P4aJ7eP///8/hont4////z+Gie3j////P5mRKM3///8/mZEozf///z+ZkSjN////P4/D4sX///+/j8Pixf///7+Pw+LF////v4/D4sX///+/ITsaxP///78hOxrE////vyE7GsT///+/ITsaxP///7+NcM3N////P6GxAMT///8/obEAxP///z+hsQDE////P6GxAMT///8/UTOKzP///79RM4rM////v1Ezisz///+/U1hQxP///z9TWFDE////P1NYUMT///8/2KiHw////z/YqIfD////P9ioh8P///8/2KiHw////z+UyVnP////vzlO7cL///8/OU7twv///z85Tu3C////P5FT3gz///+/kVPeDP///7+RU94M////v5FT3gz///+/9x4r4t2UKRj3HiviD48OF89pQZxRpvQDz2lBnHOp8gaeW7PM////P55bs8z///8//38AAAAA1jz/fwAAAADqLf9/AAC7Ff8/9FwSMP///z/0XBIw////P2h3Xyn///8/aHdfKf///z9od18p////P+xPCiP///+/7E8KI////7+XUrb2////v5dStvb///+/l1K29v///7+XUrb2////v6RA6S3///+/pEDpLf///78hclFA////PyFyUUD///8/IXJRQP///z8WUlo/////PxZSWj////8/8D4wE////7/wPjAT////v/A+MBP///+/UwEIJaeAH4FTAQglp4Afgckku3z///8/ySS7fP///z9rZf9//38AgGtl/3//fwCAa2X/f////z94cdrO////P3hx2s7///8/eHHazv///z94cdrO////P7RMntv///+/tEye2////7+ZQvbS////v5lC9tL///+/umx8xP///z+6bHzE////P7psfMT///8/gUZW8v///7+BRlby////v4FGVvL///+/9lKZwv///z/2UpnC////P2tl/3//fwAAa2X/f/9/AABrZf9//38AAEgA+NpHgZgASAD42iOAPQAAAGvl/38AAAAAa+X/fwAAAABr5f9/AAD4WkeALYGiAPhaR4B6gBEAfUS57P///799RLns////v31Euez///+/V3KBxv///z9XcoHG////P1dygcb///8/t3aW0v///z+3dpbS////P7d2ltL///8/91qrfj6CU4D3Wqt+PoJTgAAAa+X/fwCAAABr5f9/AICtP94N////v60/3g3///+/rT/eDf///7/PbA5B////P89sDkH///8/z2wOQf///z/aTnfx////v9pOd/H///+/2k538f///7/aTnfx////v/9/AACBA/8//38AAAAAFD7/fwAAaBf/P/9/AAAAAHA3IXNuLP///z8hc24s////PyFzbiz///8/IXNuLP///z8sYveeHrArCixi954brocHQRzO6XiIThNBHM7p5I25FJ9Wlwj///+/n1aXCP///7+fVpcI////vwZmglfobmAcBmaCV+huYBwyKtwRK0ePCDIq3BErR48IfSj4GT5Hiwh9KPgZPkeLCCJuzFXgbmkcIm7MVeBuaRw7CBmcfKX+f8P3GZx8pf5/IOwZnH0l/n/eExmcfSX+f+Tj3pN9JQAAGRzek4HaAAAZHDqIfKUAABkcOoh8pQAA5ePEd/9/vhLl48R3/3++EuVI5xj///+/5UjnGP///78RqYdC/39BLRkcIGz/f0EtGbfoGP///78Zt+gY////vzZwwjT///8/NnDCNP///z/eE+Vj/3++UsK0yA////+/wrTID////7/D9+Vj/39AbcP35WP/f0BtTZD4Ov///z9NkPg6////P6k+LSn///+/qT4tKf///7/3uk0Q////v/e6TRD///+/sW/4Ov///z+xb/g6////P+eYGTf///8/55gZN////z88S8gP////vzxLyA////+/PEvID////78XZxk3////PxdnGTf///8/F2cZN////z8XZxk3////P8iPwjT///8/yI/CNP///z98pf9/////P3yl/3////8/AAB8pf///78AAHyl////v///gdr///+///+B2v///7+CWv9/////P4Ja/3////8/VcEtKf///79VwS0p////v9JWVUH///8/0lZVQf///z8sqVVB////PyypVUH///8/B0VNEP///78HRU0Q////v///gdr///+///+B2v///7+CWv9/////P4Ja/3////8/AACB2v///78AAIHa////v4Ha/3////8/gdr/f////z///3yl////v///fKX///+/fSX/f////z99Jf9/////P4Ha/3////8/gdr/f////z8AAIHa////vwAAgdr///+//38AADuV/z//fwAAL3H/P/9/AAD/f/8//38AAP9//z//fwAAO5X/P/9/AAD/f/8/AAB8pf///78AAHyl////v3yl/3////8/fKX/f////z///3yl////v///fKX///+/fSX/f////z99Jf9/////P/////9yKDhU/////3IoOFT/////cig4VP////9yKDhU/3//f3IoOFT/f/9/cig4VP9//39yKDhU/3//f3IoOFT/f/9/cig4VP9//39yKDhUAAD+/3IoOFQAAP7/cig4VAAA//9yKDhUAAD//3IoOFQAAP//cig4VP9//39yKDhU/3//f1Vdqm7/f/9/cig4VP9//39yKDhU/3//f1Vdqm7+////VV2qbv7///9VXapu/v///1Vdqm4AAP//VV2qbgAA//9VXapuAAD//1Vdqm6GwAho////P4bACGj///8//3//f1Vdqm7/f/9/VV2qbv9//39VXapuAAD+/1Vdqm4AAP7/VV2qbgAA/v9VXapuAAD+/1Vdqm6GwPaX////P4bA9pf///8//3//f1Vdqm7/f/9/VV2qbpbHrFj///8/lsesWP///z+Wx6xY////P/9//3+poqpu/3//f6miqm7/f/9/qaKqbv9//3+poqpuAAD+/6miqm4AAP7/qaKqbgAA/v+poqpuAAD+/6miqm6s2Ny3////v6zY3Lf///+/rNjct////7//f/9/qaKqbv9//3+poqpu/3//f6miqm7/f/9/qaKqbv9//3+poqpu/3//f6miqm7+////qaKqbv7///+poqpuAAD+/6miqm4AAP7/qaKqbgAA//+M1zhUAAD//4zXOFQAAP//jNc4VAAA//+M1zhU/3//f/7//z//f/9/jNc4VP9//3+M18Yr/3//f4zXxiv/f/9/jNc4VP9//3+M1zhU/v/+/4zXOFT+//7/jNc4VP7/AACM1zhU/v8AAIzXOFT+/wAAjNc4VP9//3+M1zhU/3//f4zXOFT/f/9/jNc4VP9//3+M1zhU/3//f4zXxiv/f/9/jNfGK/7///+M18Yr/v///4zXxiv+/wAAjNfGK/7/AACM18Yr/v8AAIzXxiv/f/9/jNfGK/9//3+M18Yr/3//f4zXxiv/f/9/jNfGK/////+M18Yr/////4zXxiv/////jNfGK/////+M18Yr/v///6miVRH+////qaJVEf7//v+polUR/v/+/6miVRH+//7/qaJVEVQsEp+polUR/3//f6miVRH/f/9/qaJVEf9//3+polUR/3//f6miVRH+////qaJVEf7///+polUR/v///6miVRH+////qaJVEdIeAa2polUR0h4BramiVRH/f/9/qaJVEf9//3+polUR/3//f6miVRH/f/9/qaJVEf9//39VXVUR/3//f1VdVRH/f/9/VV1VEf9//39VXVUR/////1VdVRH/////VV1VEf////9VXVUR/////1VdVRH/////VV1VEdIe/VJVXVUR0h79UlVdVRH/f/9/VV1VEf9//39VXVUR/3//f1VdVRH/f/9/VV1VEf7//v9VXVUR/v/+/1VdVRH+//7/VV1VEVQs7GBVXVURAAD+/3IoxisAAP7/cijGKwAA/v9yKMYrAAD+/3Ioxiv/f/9/cijGK/9//39yKMYr/3//f3Ioxiv/f/9/cijGK/9//39yKMYrAAD+/3IoxisAAP7/cijGK/7//v9yKMYr/v/+/3Ioxiv+//7/cijGK/9//39yKMYr/3//f3Ioxiv/f/9/cijGK/9//39yKMYrGFFqkVVdqm4YUWqRVV2qbhhRapFVXapuGFFqkVVdqm73DwHVVV2qbvcPAdVVXapu9w8B1VVdqm73DwHVVV2qbjYFwBj///+/NgXAGP///782BcAY////vzYFwBj///+/Awhr6lVdqm4DCGvqVV2qbgMIa+pVXapua2oCiFVdqm5ragKIVV2qbmtqAohVXapuXZcTe////z9dlxN7////P12XE3v///8/XZcTe////z8BVfePVV2qbgFV949VXapuAVX3j1Vdqm4BVfePVV2qbmsRGNFVXapuaxEY0VVdqm5rERjRVV2qbmsRGNFVXapu93aLk3IoOFT3douTcig4VPd2i5NyKDhUlVkn03IoOFSVWSfTcig4VJVZJ9NyKDhUlVkn03IoOFTSbVincig4VNJtWKdyKDhU0m1Yp3IoOFTSbVincig4VAErIOxyKDhUASsg7HIoOFQBKyDscig4VAErIOxyKDhUIWwBq3IoOFQhbAGrcig4VCFsAatyKDhUIWwBq3IoOFRZJ9Htcig4VFkn0e1yKDhUWSfR7XIoOFRZJ9Htcig4VLY1LudyKDhUtjUu53IoOFS2NS7ncig4VLY1LudyKDhUixP39nIoOFSLE/f2cig4VIsT9/ZyKDhULJJYp3IoxissklincijGKyySWKdyKMYrLJJYp3Ioxiv81CDscijGK/zUIOxyKMYr/NQg7HIoxiv81CDscijGK92TAatyKMYr3ZMBq3IoxivdkwGrcijGK92TAatyKMYrpdjR7XIoxiul2NHtcijGK6XY0e1yKMYrpdjR7XIoxitIyi7ncijGK0jKLudyKMYrSMou53IoxitIyi7ncijGK3Ps9/ZyKMYrc+z39nIoxitz7Pf2cijGKweJi5NyKMYrB4mLk3IoxisHiYuTcijGK2mmJ9NyKMYraaYn03IoxitppifTcijGK2mmJ9NyKMYr5q5qkVVdVRHmrmqRVV1VEeauapFVXVUR5q5qkVVdVREH8AHVVV1VEQfwAdVVXVURB/AB1VVdVREH8AHVVV1VEVzqvcVVXVURXOq9xVVdVRFc6r3FVV1VEVzqvcVVXVUR+/dr6lVdVRH792vqVV1VEfv3a+pVXVURk5UDiFVdVRGTlQOIVV1VEZOVA4hVXVUR19j+oFVdVRHX2P6gVV1VEdfY/qBVXVUR19j+oFVdVRH9qvePVV1VEf2q949VXVUR/ar3j1VdVRH9qvePVV1VEZPuGNFVXVURk+4Y0VVdVRGT7hjRVV1VEZPuGNFVXVURXOpBOqmiVRFc6kE6qaJVEVzqQTqpolURXOpBOqmiVRH795MVqaJVEfv3kxWpolUR+/eTFamiVRGTlft3qaJVEZOV+3epolURk5X7d6miVRHX2ABfqaJVEdfYAF+polUR19gAX6miVRHX2ABfqaJVEf2qB3CpolUR/aoHcKmiVRH9qgdwqaJVEf2qB3CpolURk+7mLqmiVRGT7uYuqaJVEZPu5i6polURk+7mLqmiVRHmrpRuqaJVEeaulG6polUR5q6UbqmiVRHmrpRuqaJVEQfw/SqpolURB/D9KqmiVREH8P0qqaJVEQfw/SqpolURB4lzbIzXxisHiXNsjNfGKweJc2yM18YraabXLIzXxitpptcsjNfGK2mm1yyM18YraabXLIzXxisskqZYjNfGKyySpliM18YrLJKmWIzXxisskqZYjNfGK/3U3hOM18Yr/dTeE4zXxiv91N4TjNfGK/3U3hOM18Yr3ZP9VIzXxivdk/1UjNfGK92T/VSM18Yr3ZP9VIzXxiul2C0SjNfGK6XYLRKM18YrpdgtEozXxiul2C0SjNfGK0jK0BiM18YrSMrQGIzXxitIytAYjNfGK0jK0BiM18Yrc+wHCYzXxitz7AcJjNfGK3PsBwmM18YrASveE4zXOFQBK94TjNc4VAEr3hOM1zhUASveE4zXOFQhbP1UjNc4VCFs/VSM1zhUIWz9VIzXOFQhbP1UjNc4VFknLRKM1zhUWSctEozXOFRZJy0SjNc4VFknLRKM1zhUtjXQGIzXOFS2NdAYjNc4VLY10BiM1zhUtjXQGIzXOFSLEwcJjNc4VIsTBwmM1zhUixMHCYzXOFT3dnNsjNc4VPd2c2yM1zhU93ZzbIzXOFSVWdcsjNc4VJVZ1yyM1zhUlVnXLIzXOFSVWdcsjNc4VNJtpliM1zhU0m2mWIzXOFTSbaZYjNc4VNJtpliM1zhUGFGUbqmiqm4YUZRuqaKqbhhRlG6poqpuGFGUbqmiqm73D/0qqaKqbvcP/Sqpoqpu9w/9Kqmiqm73D/0qqaKqbjYFPuf///+/NgU+5////782BT7n////vzYFPuf///+/AwiTFamiqm4DCJMVqaKqbgMIkxWpoqpua2r7d6miqm5ravt3qaKqbmtq+3epoqpuXZfrhP///z9dl+uE////P12X64T///8/XZfrhP///z8BVQdwqaKqbgFVB3CpoqpuAVUHcKmiqm4BVQdwqaKqbmsR5i6poqpuaxHmLqmiqm5rEeYuqaKqbmsR5i6poqpu6uvjyv///7/q6+PK////v+rrGzX///+/6usbNf///78atetr////Pxq162v///8/6tMxvf///7/q0zG9////v7fnX0H///+/t+dfQf///7+355++////v7fnn77///+/lcdSp////z+Vx1Kn////P5XHUqf///8/rNgiSP///7+s2CJI////v6zYIkj///+/XNz//////79c3P//////v1zcAAD///+/XNwAAP///7//f11c////P/9/XVz///8/Zaf//////79lp///////v83C6lP///8/zcLqU////z/NwhSs////P83CFKz///8/6tPNQv///7/q081C////vxq1E5T///8/GrUTlP///z9lpwAA////v2WnAAD///+//3+Y2P///z//f5jY////P/9/Zif///8//39mJ////z9c3P//////v1zc//////+//39mJ////z//f2Yn////P/9/mNj///8//3+Y2P///z//f6Gj////P/9/oaP///8/0zkbGv///7/TORsa////v9M5Gxr///+/0zkbGv///78vZ0hKjNc4VC9nSEqM1zhUL2dISozXOFQvZ0hKjNc4VM+YSEqM18Yrz5hISozXxivPmEhKjNfGK8+YSEqM18YrQLpcaqmiVRFAulxqqaJVEUC6XGqpolURQLpcaqmiVRFAuqKVVV1VEUC6opVVXVURQLqilVVdVRFAuqKVVV1VEc+YtrVyKMYrz5i2tXIoxivPmLa1cijGK8+YtrVyKMYrL2e2tXIoOFQvZ7a1cig4VC9ntrVyKDhUL2e2tXIoOFRmViUk////v2ZWJST///+/ZlYlJP///79mViUk////v+Rl0rn///8/5GXSuf///z/kZdK5////P+Rl0rn///8/J1NqJozXOFQnU2omjNc4VCdTaiaM1zhUJ1NqJozXOFTXrGomjNfGK9esaiaM18Yr16xqJozXxivXrGomjNfGK//e11ipolUR/97XWKmiVRH/3tdYqaJVEf/e11ipolUR/94mp1VdVRH/3ianVV1VEf/eJqdVXVUR/94mp1VdVRHXrJTZcijGK9eslNlyKMYr16yU2XIoxivXrJTZcijGKydTlNlyKDhUJ1OU2XIoOFQnU5TZcig4VCdTlNlyKDhU2luYKf///z/aW5gp////P9pbmCn///8/2luYKf///z//f////v//P/9///98pf8//3///3yl/z//f///gdr/P/9///98pf8//3///4Ja/z//f///glr/P/9///99Jf8//3////7//z//f/////9BLf9//////0Et/3//////QC3/f/////++Ev9//////74S/3//////vhL/f/////++Ev9//////74S/3///3yl/z//f///fKX/P/9///98pf8//3///30l/z//f///fSX/P/9///99Jf8//3///30l/z//f///fSX/P01fXZCB2v8/TV9dkIHa/z9NX12Qgdr/P01fXZCB2v8//38AAP7//z//fwAA4bn/P/9/AAD/v/8//38AAHyl/z//fwAAfKX/P/9/AAB8pf8//38AAHyl/z//fwAA/v//P/9/AAD//0At/38AAP//QC3/fwAAgdr/P/9/AACB2v8//38AAIHa/z//f/7/gdr/P/9//v+B2v8//3/+/4Ha/z//f/7/gdr/P3JfJG+B2v8/cl8kb4Ha/z9yXyRvgdr/P3JfJG+B2v8//38AAIHa/z//fwAAgdr/P/9/AACB2v8//38AAIHa/z+RQUpY////P5FBSlj///8/kUFKWP///z+RQUpY////P/9//v98pf8//3/+/3yl/z//f/7/fKX/P/9//v98pf8/tSduPv///7+1J24+////v7Unbj7///+/tSduPv///7//fwAAfKX/P/9/AAB8pf8//38AAHyl/z8zO/Fu////PzM78W7///8/Mzvxbv///z8zO/Fu////P/9/AACCWv8//38AAIJa/z//fwAAglr/P/9/AACCWv8//3/+/4Ja/z//f/7/glr/P/9//v+CWv8//3/+/4Ja/z+DEKm6////v4MQqbr///+/gxCpuv///7+DEKm6////v/9/AACCWv8//38AAIJa/z//fwAAglr/P/9/AACCWv8//38AAIJa/z//fwAAglr/P/9/AACCWv8//3/+/4Ja/z//f/7/glr/P/9//v+CWv8//3/+/4Ja/z9dEEzffSX/P10QTN99Jf8/XRBM330l/z9dEEzffSX/P/9/AAB9Jf8//38AAH0l/z//fwAAfSX/P/9/AAB9Jf8//38AAH0l/z//fwAAfSX/P/9/AAAAAEAt/38AAAAAQC3/fwAAAAD/H/9/AAAAAEEt/38AAAAA/x//fwAAAABBLf9/AAAAAP8f/38AAAAAvhL/fwAAAAC+Ev9/AAAAAL4S/38AAAAAvhL/f/7/fSX/P/9//v99Jf8//3/+/30l/z/bEI0gfSX/P9sQjSB9Jf8/2xCNIH0l/z/bEI0gfSX/P/9/AAB9Jf8//38AAH0l/z//fwAAfSX/P/9/AAB9Jf8//3/+/wAAvlL/f/7/AAC+Uv9//v8AAL5S/3/+/wAAvlL/f/7/AAC+Uv9//v8AAL5S/3/+/wAAvlIr8SUhAABBLSvxJSEAAEEtK/ElIQAAQS0r8SUhAAC+Uv9//v8AAL5S/3/+/wAAvlL/f/7/AAC+Uv9//v8AAL5S1/Dx3gAAvlLX8PHeAABBLdfw8d4AAEEt1/Dx3gAAvlL/f///AAC+Ev9///8AAL4S/3///wAAvhL/f/7/AAC+Ev9//v8AAL4S/3/+/wAAvhL/f/7/AAC+EqfyE1EAAEBtp/ITUQAAQG2n8hNRAABAbafyE1EAAEBt/3/+/wAAQG3/f/7/AABAbf9//v8AAEBt/3/+/wAAQG238ZWvAABAbbfxla8AAL4St/GVrwAAvhK38ZWvAABAbf9/AAD+/0Bt/38AAP7/QG3/fwAA/v9Abf9/AAD+/0Bt/3/+//7/vhL/f/7//v++Ev9//v/+/74S/3/+//7/vhJo0EeO/v9AbWjQR47+/0BtaNBHjv7/QG1o0EeO/v9Abf9/AAD+/0Bt/38AAP7/QG3/fwAA/v9Abf9/AAD+/0Bt/38AAP//vhL/fwAA//++Ev9/AAD//74SEtGncv7/QG0S0ady/v9AbRLRp3L+/0BtEtGncv7/QG0MoSaP//++UgyhJo///75SDKEmj/7/vlIMoSaP//9BLf9/AAD+/75S/38AAP7/vlL/fwAA/v++Uv9//v/+/0Et/3/+//7/QC3/f/7//v9ALf9//v/+/0EtJaEscf7/QS0loSxx//9BLSWhLHH+/75SJaEscf7/QS3/fwAA/v9ALf9/AAD+/0At/38AAP7/QC3/fwAA/v9BLVUW9fcOcg4SVRb19xxlwRcNoA0gDSAGUA2gDSANIAZQ9XjMlrlGDhP1eMyWe1BxDadkpuSm5FMyp2Sm5KbkUzIHN6vM////vwc3q8z///+/BzerzP///78HN6vM////v6xMB7f///8/rEwHt////z+sTAe3////P6xMB7f///8/rEz3SP///z+sTPdI////P6xM90j///8/rEz3SP///z8DEQ/A////vwMRD8D///+/AxEPwP///78DEQ/A////v75Aiqb///8/vkCKpv///z++QIqm////P75Aiqb///8/iya+wP///7+LJr7A////v4smvsD///+/iya+wP///78OEctE////vw4Ry0T///+/DhHLRP///78OEctE////v6o6g5D///8/qjqDkP///z+qOoOQ////P6o6g5D///8/fKX//////798pf//////v3yl//////+/fKX//////7//f4Ha////P/9/gdr///8//3+B2v///z//f4Ha////P/9/fSX///8//399Jf///z//f30l////P/9/fSX///8/gdr//////7+B2v//////v4Ha//////+/gdr//////78QQPtu////PxBA+27///8/EED7bv///z8QQPtu////PwMR7z////+/AxHvP////78DEe8/////vwMR7z////+/EEADkf///z8QQAOR////PxBAA5H///8/EEADkf///z8IN1Mz////vwg3UzP///+/CDdTM////78IN1Mz////vwAA/3//f5c4AAD/f/9/lzgAAP9//3//PwAA/3//f0EtAAD/f/9/nkoAAP9//3+vRQAA/3//f5RHAAD/f/9//z8AAP9//3+XOAAA/3//f0EtAAD/f/9/nUoAAP9//3+eSgAA/3//f/8/AAD/f/9/vlL/f4Fa////P/9/gVr///8//3+BWv///z//f4Fa////P4HaAAD///+/gdoAAP///7+B2gAA////v4HaAAD///+//399pf///z//f32l////P/9/faX///8//399pf///z98pQAA////v3ylAAD///+/fKUAAP///798pQAA////vzM9QJ7///8/Mz1Anv///z8zPUCe////PzM9QJ7///8/9UD7sv///z/1QPuy////P/VA+7L///8/9UD7sv///z+RGv7/kZr+f5Ea/v+Rmv5/kRr+/21l/n+RGv7/kZr+f9U4M3T///8/1TgzdP///z/VODN0////P9U4M3T///8/YEGRjf///z9gQZGN////P2BBkY3///8/YEGRjf///z9GP/7/uUD+f0Y//v+5QP5/Rj/+/0W//n9GP/7/uUD+f0AezEL///+/QB7MQv///79AHsxC////v0AezEL///+/wUWEXf///z/BRYRd////P8FFhF3///8/wUWEXf///z92ZP7/iRv+f3Zk/v+JG/5/dmT+/4kb/n92ZP7/deT+f8wL1Lj///+/zAvUuP///7/MC9S4////v8wL1Lj///+/+zIKP////7/7Mgo/////v/syCj////+/+zIKP////7//f3XkdeQAAP9/deR15AAA/3915HXkAAD/f3XkdeQAAEgsk0f///+/SCyTR////79ILJNH////v0gsk0f///+/+zL0wP///7/7MvTA////v/sy9MD///+/+zL0wP///7//f0W/uUAAAP9/Rb+5QAAA/39Fv0W/AAD/f0W/uUAAADM9vmH///8/Mz2+Yf///z8zPb5h////PzM9vmH///8/wUV6ov///z/BRXqi////P8FFeqL///8/wUV6ov///z//f5GabWUAAP9/kZptZQAA/3+Rmm1lAAD/f5GabWUAAMwLKkf///+/zAsqR////7/MCypH////v8wLKkf///+/kQ2fPv///7+RDZ8+////v5ENnz7///+/kQ2fPv///7//f21l/39IDf9/bWX/f0gN/39tZf9/SA3/f21l/39IDdU4y4v///8/1TjLi////z/VOMuL////P9U4y4v///8/YEFtcv///z9gQW1y////P2BBbXL///8/YEFtcv///z//f7lA/3+iH/9/uUD/f6If/3+5QP9/oh//f7lA/3+iH2s4R6z///8/azhHrP///z9rOEes////P2s4R6z///8/9EADTf///z/0QANN////P/RAA03///8/9EADTf///z//f4kb/387Mv9/iRv/fzsy/3+JG/9/OzL/f4kb/387Mm3lAAD///+/beUAAP///79t5QAA////v23lAAD///+/eyI+Ov///797Ij46////v3siPjr///+/eyI+Ov///792ZAAA/3/ETXZkAAD/f8RNdmQAAP9/xE12ZAAA/3/ETQAA/3////+/AAD/f////78AAP9/////vwAA/3////+/QB4yvf///79AHjK9////v0AeMr3///+/QB4yvf///7+5wAAA/39cYLnAAAD/f1xgucAAAP9/XGC5wAAA/39cYEgsarj///+/SCxquP///79ILGq4////v0gsarj///+/kQ1fwf///7+RDV/B////v5ENX8H///+/kQ1fwf///7+RGgAA/3+2cpEaAAD/f7ZykRoAAP9/tnKRGgAA/3+2cnsiwMX///+/eyLAxf///797IsDF////v3siwMX///+/azi3U////z9rOLdT////P2s4t1P///8/azi3U////z8AAP9//3/+/wAA/3//f/7/AAD/f/9//v8AAP9//3/+/wAA/3//f/7/AAD/f/9//v8AAP9//3/+/wAA/3//f/7/AAD/f/9//v8AAP9//3/+/wAA/3//f/7/AAD/f/9//v//f0W/////P/9/Rb////8//39Fv////z//f0W/////P/9/uUD///8//3+5QP///z//f7lA////P/9/uUD///8/kRr+/////7+RGv7/////v5Ea/v////+/kRr+/////79FPwAA////v0U/AAD///+/RT8AAP///79FPwAA////v/9/deT///8//3915P///z//f3Xk////P/9/deT///8//39tZf///z//f21l////P/9/bWX///8//39tZf///z+ImwAA////v4ibAAD///+/iJsAAP///7+ImwAA////v3Zk/v////+/dmT+/////792ZP7/////v3Zk/v////+//3+Rmv///z//f5Ga////P/9/kZr///8//3+Rmv///z//f4kb////P/9/iRv///8//3+JG////z//f4kb////P7nA/v////+/ucD+/////7+5wP7/////v7nA/v////+//3+5QP///z//f7lA////P/9/uUD///8//3+5QP///z8AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////8/AAD/f////z8AAP9/////PwAA/3////8/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////PwAA/3////8/AAD/f////z8AAP9/////PwAA/3////8/AAD/f////z8AAP9/////PwAA/3////8/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/4fqlmP///7/h+qWY////v+H6pZj///+/4fqlmP///7//f0W/////P/9/Rb////8//39Fv////z//f0W/////P23lAAD///+/beUAAP///79t5QAA////v23lAAD///+/uMD//////7+4wP//////v7jA//////+/uMD//////7//f4kb////P/9/iRv///8//3+JG////z//f4kb////P/9/kZr///8//3+Rmv///z//f5Ga////P/9/kZr///8/dmT+/////792ZP7/////v3Zk/v////+/dmT+/////7+ImwAA////v4ibAAD///+/iJsAAP///7+ImwAA////v/9/bWX///8//39tZf///z//f21l////P/9/bWX///8//3915P///z//f3Xk////P/9/deT///8//3915P///z+5wAAA////v7nAAAD///+/ucAAAP///7+5wAAA////v23l//////+/beX//////79t5f//////v23l//////+/////f/9/AAD///9//38AAP///3//fwAA////f/9/AAD///9//38AAP///3//fwAA////f/9/AAD///9//38AAP///3//fwAA////f/9/AAD///9//38AAP///3//fwAAgORocv///z+A5Ghy////P4DkaHL///8/gORocv///z+A5JaN////P4Dklo3///8/gOSWjf///z+A5JaN////P+H6WWf///+/4fpZZ////7/h+lln////v+H6WWf///+/Z/J+m////79n8n6b////v2fyfpv///+/Z/J+m////7+c59ts////P5zn22z///8/nOfbbP///z+c59ts////P1nnHYX///8/Wecdhf///z9Z5x2F////P1nnHYX///8/2uximP///7/a7GKY////v9rsYpj///+/2uximP///7/b7Jxn////v9vsnGf///+/2+ycZ////7/b7Jxn////v1nn4Xr///8/Wefhev///z9Z5+F6////P1nn4Xr///8/m+cjk////z+b5yOT////P5vnI5P///8/m+cjk////z9n8oBk////v2fygGT///+/Z/KAZP///79n8oBk////vwAA/3//f/5/AAD/f/9//n8AAP9//3/+fwAA/3//f/5/AAD/f/9//n8AAP9//3/+fwAA/3//f/5/AAD/f/9//n8AAP9//3/+fwAA/3//f/5/AAD/f/9//n8AAP9//3/+fwAA/3//f/5/AAD/f/9//n8AAP9//3/+fwAA/3//f/5/AAD/f/9//n8AAP9//3/+f9pbZ9b///8/2ltn1v///z/aW2fW////P9pbZ9b///8/0znj5f///7/TOePl////v9M54+X///+/0znj5f///7/kZSxG////P+RlLEb///8/5GUsRv///z/kZSxG////P2ZW2dv///+/ZlbZ2////79mVtnb////v2ZW2dv///+/E/teF////78T+14X////vxP7Xhf///+/E/teF////78+ZzaF////Pz5nNoX///8/Pmc2hf///z8+ZzaF////Pz5nyHr///8/PmfIev///z8+Z8h6////Pz5nyHr///8/E/ug6P///78T+6Do////vxP7oOj///+/E/ug6P///7////9//39oev9/oaP///8/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MF4R1////z8wXhHX////PzBeEdf///8/MF4R1////z8JY0rr/v8/EgljSuv+/z8SCWNK6/7/PxIJY7QU/v/AbQljtBT+/8BtCWO0FP7/wG3k0SJj////P+TRImP///8/5NEiY////z/k0SJj////P+xFhCn+/z8S7EWEKf//PxLsRYQp/v8/EuxFhCn//8BtkUCm0v7/PxKRQKbS/v8/EpFAptL+/z8SkUCm0v7/PxKNLL7k////v40svuT///+/jSy+5P///7+NLL7k////v2sxohf//z9TazGiF///P1NrMaIX//+/LGsxohf+/z9Tk86iF37Z/z+TzqIXftn/P5POohd+2f8/k86iF37Z/z8Mz4M4faT/PwzPgzh9pP8/DM+DOH2k/z8Mz4M4faT/P4K4DU+BW/8/grgNT4Fb/z+CuA1PgVv/P4K4DU+BW/8/opeTToAm/z+il5NOgCb/P6KXk06AJv8/opeTToAm/z9caJNOAAC/LFxok04AAL8sXGiTTgAAvyxcaJNOAAC/LIQFKtn///+/hAUq2f///7+EBSrZ////v4QFKtn///+/0WRuU////z/RZG5T////P9FkblP///8/0WRuU////z9qBmzb/v+/LGoGbNv+/78sagZs2/7/vyxqBmzb//+/LJT5bNt+2f8/lPls237Z/z+U+Wzbftn/P5T5bNt+2f8/rPkDqX2k/z+s+QOpfaT/P6z5A6l9pP8/rPkDqX2k/z/71lGGgVv/P/vWUYaBW/8/+9ZRhoFb/z/71lGGgVv/P5GkaoaAJv8/kaRqhoAm/z+RpGqGgCb/P5GkaoaAJv8/bVtqhgAAP1NtW2qGAAC/LG1baoYAAL8sbVtqhgAAP1OFBdMm////v4UF0yb///+/hQXTJv///7+FBdMm////vwM2jJn///8/AzaMmf///z8DNoyZ////PwM2jJn///8/6jjUq////z/qONSr////P+o41Kv///8/6jjUq////z/RGP7/LWf+f9EY/v8tZ/5/0Rj+/y1n/n/RGP7/LWf+f4MyNHb///8/gzI0dv///z+DMjR2////P4MyNHb///8/5Dpqi////z/kOmqL////P+Q6aov///8/5Dpqi////z+MPP7/i7z+f4w8/v9zQ/5/jDz+/4u8/n+MPP7/c0P+f4wZ/En///+/jBn8Sf///7+MGfxJ////v4wZ/En///+/gD5uYv///z+APm5i////P4A+bmL///8/gD5uYv///z9rnf//bB3+f2ud//9sHf5/a53//2wd/n9rnf//kuL+f8sJgrL///+/ywmCsv///7/LCYKy////v8sJgrL///+/1SsVR////7/VKxVH////v9UrFUf///+/1SsVR////7//f5LikuIAAP9/kuKS4gAA/3+S4pLiAAD/f5LikuIAAHElYE////+/cSVgT////79xJWBP////v3ElYE////+/1SvquP///7/VK+q4////v9Ur6rj///+/1SvquP///7//f4u8i7wAAP9/i7yLvAAA/3+LvIu8AAD/f4u8i7wAAAI2cmb///8/AjZyZv///z8CNnJm////PwI2cmb///8/gD6Qnf///z+APpCd////P4A+kJ3///8/gD6Qnf///z//f9CYLmcAAP9/0JjQmAAA/3/QmC5nAAD/f9CY0JgAAMsJfE3///+/ywl8Tf///7/LCXxN////v8sJfE3///+/awsbRf///79rCxtF////v2sLG0X///+/awsbRf///7//fy5n/39oDP9/Lmf/f2gM/38uZ/9/aAz/fy5n/39oDIMyyon///8/gzLKif///z+DMsqJ////P4Myyon///8/5DqUdP///z/kOpR0////P+Q6lHT///8/5DqUdP///z//f3ND/39GHv9/c0P/f0Ye/39zQ/9/Rh7/f3ND/39GHp8wcaX///8/nzBxpf///z+fMHGl////P58wcaX///8/6jgqVP///z/qOCpU////P+o4KlT///8/6jgqVP///z//f2wd/39JMf9/bB3/f0kx/39sHf9/STH/f2wd/39JMS3nAAD///+/LecAAP///78t5wAA////vy3nAAD///+/kB1+Qf///7+QHX5B////v5AdfkH///+/kB1+Qf///79rnQAA/3+1TmudAAD/f7VOa50AAP9/tU5rnQAA/3+1TgAA/3////8/AAD/f////z8AAP9/////PwAA/3////8/jBkCtv///7+MGQK2////v4wZArb///+/jBkCtv///79ywwAA/3+5YXLDAAD/f7lhcsMAAP9/uWFywwAA/3+5YXElnrD///+/cSWesP///79xJZ6w////v3ElnrD///+/awvjuv///79rC+O6////v2sL47r///+/awvjuv///7/RGAAA/3+Wc9EYAAD/f5Zz0RgAAP9/lnPRGAAA/3+Wc5AdgL7///+/kB2Avv///7+QHYC+////v5AdgL7///+/nzCNWv///z+fMI1a////P58wjVr///8/nzCNWv///z8AAP9//3/+/wAA/3//f/7/AAD/f/9//v8AAP9//3/+/wAA/3//f/7/AAD/f/9//v8AAP9//3/+/wAA/3//f/7/AAD/f/9//v8AAP9//3/+/wAA/3//f/7/AAD/f/9//v//f4u8////P/9/i7z///8//3+LvP///z//f4u8////P/9/c0P///8//39zQ////z//f3ND////P/9/c0P///8/0Rj+/////7/RGP7/////v9EY/v////+/0Rj+/////79ywwAA////v3LDAAD///+/csMAAP///79ywwAA////v/9/kuL///8//3+S4v///z//f5Li////P/9/kuL///8//38uZ////z//fy5n////P/9/Lmf///8//38uZ////z9rnQAA////v2udAAD///+/a50AAP///79rnQAA////v2ud/v////+/a53+/////79rnf7/////v2ud/v////+//3/QmP///z//f9CY////P/9/0Jj///8//3/QmP///z//f2wd////P/9/bB3///8//39sHf///z//f2wd////P4w8/v////+/jDz+/////7+MPP7/////v4w8/v////+//39zQ////z//f3ND////P/9/c0P///8//39zQ////z8AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////PwAA/3////8/AAD/f////z8AAP9/////PwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/AAD/f////z8AAP9/////PwAA/3////8/AAD/f////z8AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////vwAA/3////+/AAD/f////78AAP9/////v/T+XYX///+/9P5dhf///7/0/l2F////v/T+XYX///+//3+LvP///z//f4u8////P/9/i7z///8//3+LvP///z8t5wAA////vy3nAAD///+/LecAAP///78t5wAA////v3LD//////+/csP//////79yw///////v3LD//////+//39sHf///z//f2wd////P/9/bB3///8//39sHf///z//f9CY////P/9/0Jj///8//3/QmP///z//f9CY////P5Ni/v////+/k2L+/////7+TYv7/////v5Ni/v////+/k2IAAP///7+TYgAA////v5NiAAD///+/k2IAAP///7//fy5n////P/9/Lmf///8//38uZ////z//fy5n////P/9/kuL///8//3+S4v///z//f5Li////P/9/kuL///8/jDwAAP///7+MPAAA////v4w8AAD///+/jDwAAP///78t5///////vy3n//////+/Lef//////78t5///////v3r5nKH///+/evmcof///796+Zyh////v3r5nKH///+/BPorff///z8E+it9////PwT6K33///8/BPorff///z8E+tOC////PwT604L///8/BPrTgv///z8E+tOC////P/T+oXr///+/9P6hev///7/0/qF6////v/T+oXr///+/Kv36hf///78q/fqF////vyr9+oX///+/Kv36hf///7/w+hp8////P/D6Gnz///8/8PoafP///z/w+hp8////P6H6CYH///8/ofoJgf///z+h+gmB////P6H6CYH///8/GvwNhf///78a/A2F////vxr8DYX///+/GvwNhf///78a/PF6////vxr88Xr///+/Gvzxev///78a/PF6////v6H69X7///8/ofr1fv///z+h+vV+////P6H69X7///8/8Prkg////z/w+uSD////P/D65IP///8/8Prkg////z8q/QR6////vyr9BHr///+/Kv0Eev///78q/QR6////vwAA/3///wAAAAD/f///AAAAAP9///8AAAAA/3///wAAAAD/f///AAAAAP9///8AAAAA/3/+/wAAAAD/f///AAAAAP9///8AAAAA/3/+/wAAAAD/f///AAAAAP9///8AAAAA/3/+/wAAAAD/f///AAAAAP9/AAAAAAAA/38AAAAAAAD/fwAAAAAAAP9/AAAAAAAA/38AAAAAAAD/fwAAAAAAAP9/AAAAAAAA/38AAAAAAAD/f/7/AAAAAP9//v8AAAAA/3///wAAAAD/f/7/AABBWXp6////P0FZenr///8/QVl6ev///z9BWXp6////P8pkjqz///8/ymSOrP///z/KZI6s////P8pkjqz///8/jiw5G////7+OLDkb////v44sORv///+/jiw5G////79AWYSF////P0BZhIX///8/QFmEhf///z9AWYSF////P/biPa7///+/9uI9rv///7/24j2u////v/biPa7///+/Pl7cKP///z8+Xtwo////Pz5e3Cj///8/Pl7cKP///z8hV8Ih////vyFXwiH///+/IVfCIf///78hV8Ih////v9DR9Zz///8/0NH1nP///z/Q0fWc////P9DR9Zz///8/5WM7CP7/vlLlY8P3/v++UuVj3hP+/74SIGzl4wAAvhLEd+TjAABBLcR3GRwAAL5SOojl430l/z86iBkcfSX/P96T5eOCWv8/3pMZHIJa/z88yzZw////PxmcIOx8pf8/GZzeE3yl/z828MK0////vzbwwrT///+/GZw7CIHa/z8ZnMP3gdr/P+TI55j///8/5MjnmP///z828DxL////vzbwPEv///+/5cgXZ////z/lyBdn////PzzLyI////8//398pf///z//f3yl////P3ylAAD///+/fKUAAP///799Jf7/////v30l/v////+//3+CWv///z//f4Ja////P4Ha/v////+/gdr+/////7//f4Ja////P/9/glr///8/gdoAAP///7+B2gAA////v/9/gdr///8//3+B2v///z98pf7/////v3yl/v////+//399Jf///z//f30l////P/9/gdr///8//3+B2v///z+B2gAA////v4HaAAD///+//v//f/9/YTX+//9//3//P/7//3//f/8/////f/9//z////9//39hNf///3//f/8/gloAAP///7+CWgAA////v/9/fKX///8//398pf///z+CWv7/////v4Ja/v////+//399Jf///z//f30l////P////3//fwAA////f/9/AAD///9//38AAP///3//fwAA////f/9/AAD///9//38AAP///3//fwAA////f/9/AAD///9//38AAP///3//fwAA////f/9/AAD///9//38AAEfn51////+/R+fnX////79H5+df////v0fn51////+/gNtCkf///z+A20KR////P4DbQpH///8/gNtCkf///z9h3nt5////P2Hee3n///8/Yd57ef///z9h3nt5////P7vufaT///+/u+59pP///7+77n2k////v7vufaT///+/u+6BW////7+77oFb////v7vugVv///+/u+6BW////79h3oOG////P2Heg4b///8/Yd6Dhv///z9h3oOG////P4DbvG7///8/gNu8bv///z+A27xu////P4DbvG7///8/R+cXoP///79H5xeg////v0fnF6D///+/R+cXoP///796+WJe////v3r5Yl7///+/evliXv///796+WJe////v+ffSGf///8/599IZ////z/n30hn////P+ffSGf///8/59+2mP///z/n37aY////P+fftpj///8/59+2mP///z/D9+VjgloAAMP35WOCWgAAw/flY4JaAACJ5hIpfKUAAInmEil8pQAAieYSKXylAACJ5hIpfKUAAN0gyA98pQAA3SDID4JaAADdIMgPgloAAN0gyA98pQAANhLPQXylAAA2Es9BfKUAADYSz0GCWgAANhLPQXylAADI7c9BgloAAMjtz0GCWgAAyO3PQYJaAADI7c9BfKUAACHfyA98pQAAId/ID4JaAAAh38gPgloAACHfyA98pQAAdRkSKYJaAAB1GRIpgloAAHUZEimCWgAAdRkSKYJaAAA7COVjfKUAADsI5WN8pQAAOwjlY3ylAAD5K89BfSUAAPkrz0F9JQAA+SvPQX0lAAD5K89BfSUAAAXUz0GB2gAABdTPQYHaAAAF1M9BgdoAAAXUz0GB2gAApbDID30lAAClsMgPgdoAAKWwyA+B2gAApbDID30lAAB3PRIpfSUAAHc9Eil9JQAAdz0SKX0lAAB3PRIpfSUAAN4T5WN9JQAA3hPlY30lAADeE+VjfSUAACDs5WOB2gAAIOzlY4HaAAAg7OVjgdoAAIfCEimB2gAAh8ISKYHaAACHwhIpgdoAAIfCEimB2gAAWU/ID30lAABZT8gPgdoAAFlPyA+B2gAAWU/ID30lAADOwQZUfSX+f87BBlR9Jf5/zsEGVH0l/n/OwQZUfSX+f8iPpjCB2v5/yI+mMH0l/n/Ij6YwfSX+f8iPpjCB2v5/7VaIQoHa/n/tVohCgdr+f+1WiEKB2v5/7VaIQoHa/n8ZHCBsfSX+fxkcIGx9Jf5/GRwgbH0l/n/l4yBsgdr+f+XjIGyB2v5/5eMgbIHa/n8RqYhCfSX+fxGpiEJ9Jf5/EamIQn0l/n8RqYhCfSX+fzZwpjCB2v5/NnCmMH0l/n82cKYwfSX+fzZwpjCB2v5/Lz4GVIHa/n8vPgZUgdr+fy8+BlSB2v5/Lz4GVIHa/n8ZHMR3fKX+fxkcxHd8pf5/GRzEd3yl/n/l48R3fKX+f+XjxHd8pf5/5ePEd3yl/n8RqYlmglr+fxGpiWaCWv5/EamJZoJa/n8RqYlmfKX+fzZwIV+CWv5/NnAhX3yl/n82cCFffKX+fzZwIV+CWv5/MD7IbYJa/n8wPshtfKX+fzA+yG18pf5/MD7IbYJa/n/OwchtfKX+f87ByG18pf5/zsHIbXyl/n/Owchtglr+f8iPIV+CWv5/yI8hX3yl/n/IjyFffKX+f8iPIV+CWv5/7VaJZnyl/n/tVolmfKX+f+1WiWZ8pf5/7VaJZnyl/n8RqXWZ/39AbRGpdZn/f0BtEal1mf9/QG0RqXWZ/39AbTZw3aD/f0BtNnDdoP9/QG02cN2g/39AbTZw3aD/f0BtMD42kv9/QG0wPjaS/39AbTA+NpL/f0BtMD42kv9/QG3OwTaS/39Abc7BNpL/f0BtzsE2kv9/QG3OwTaS/39AbceP3aD/f0Btx4/doP9/QG3Hj92g/39AbceP3aD/f0Bt7VZ1mf9/QG3tVnWZ/39Abe1WdZn/f0Bt7VZ1mf9/QG0ZHDqI/39AbRkcOoj/f0BtGRw6iP9/QG3l4zqI/39AbeXjOoj/f0Bt5eM6iP9/QG3Pwfir/3++Us/B+Kv/f75Sz8H4q/9/vlLPwfir/3++Uhm3F+f///+/GbcX5////78Ztxfn////vxm3F+f///+/7VZ3vf9/vlLtVne9/3++Uu1Wd73/f75S7VZ3vf9/vlIZHN6T/3++Uhkc3pP/f75SGRzek/9/vlLl496T/3++UuXj3pP/f75S5ePek/9/vlIRqXe9/3++UhGpd73/f75SEal3vf9/vlIRqXe9/3++UuVIF+f///+/5UgX5////7/lSBfn////v+VIF+f///+/Lz74q/9/vlIvPvir/3++Ui8++Kv/f75SLz74q/9/vlLIjzzL////P8iPPMv///8/yI88y////z/IjzzL////P3c97Nb/f0Etdz3s1v9/QS13PezW/39BLXc97Nb/f0Et3hMZnP9/QS3eExmc/39BLd4TGZz/f0EtIOwZnP9/QS0g7Bmc/39BLSDsGZz/f0Eth8Ls1v9/QS2HwuzW/39BLYfC7Nb/f0Eth8Ls1v9/QS08Szbw////vzxLNvD///+/PEs28P///788Szbw////v/krL77/f0Et+Ssvvv9/QS35Ky++/39BLfkrL77/f0EtBdQvvv9/QS0F1C++/39BLQXUL77/f0EtBdQvvv9/QS3D9xmc/3++EsP3GZz/f74Sw/cZnP9/vhKJ5uzW/3++Eonm7Nb/f74Siebs1v9/vhKJ5uzW/3++Et0gNvD/f74S3SA28P9/vhLdIDbw/3++Et0gNvD/f74SNhIvvv9/vhI2Ei++/3++EjYSL77/f74SNhIvvv9/vhLI7S++/3++EsjtL77/f74SyO0vvv9/vhLI7S++/3++EiHfNvD/f74SId828P9/vhIh3zbw/3++EiHfNvD/f74SdRns1v9/vhJ1GezW/3++EnUZ7Nb/f74SdRns1v9/vhI7CBmc/3++EjsIGZz/f74SOwgZnP9/vhKxbwbF////P7FvBsX///8/sW8Gxf///z+xbwbF////P1XB0db///+/VcHR1v///79VwdHW////v1XB0db///+/B0Wx7////78HRbHv////vwdFse////+/B0Wx7////79MkAbF////P0yQBsX///8/TJAGxf///z9MkAbF////Pxdn5cj///8/F2flyP///z8XZ+XI////Pxdn5cj///8/wrQ28P///7/CtDbw////v8K0NvD///+/wrQ28P///7/nmOXI////P+eY5cj///8/55jlyP///z/nmOXI////PzZwPMv///8/NnA8y////z82cDzL////PzZwPMv///8/gVr/f////z+BWv9/////P4Fa/3////8/gVr/f////z///3yl////v///fKX///+///98pf///7///3yl////vwAAgdr///+/AACB2v///78AAIHa////vwAAgdr///+/fKX/f////z98pf9/////P3yl/3////8/fKX/f////z+qPtHW////v6o+0db///+/qj7R1v///7+qPtHW////vyypqb7///8/LKmpvv///z8sqam+////Pyypqb7///8/0lapvv///z/SVqm+////P9JWqb7///8/0lapvv///z/3urHv////v/e6se////+/97qx7////7/3urHv////vwAAgdr///+/AACB2v///78AAIHa////vwAAgdr///+/fKX/f////z98pf9/////P3yl/3////8/fKX/f////z/+/30l////v/7/fSX///+//v99Jf///7/+/30l////v30l/3////8/fSX/f////z99Jf9/////P30l/3////8/AAB8pf///78AAHyl////vwAAfKX///+/AAB8pf///7+B2v9/////P4Ha/3////8/gdr/f////z+B2v9/////P30l/3////8/fSX/f////z99Jf9/////P30l/3////8//v+B2v///7/+/4Ha////v/7/gdr///+//v+B2v///7//f/7/w2r/P/9//v/Dav8//3/+/4Ja/z//f/7//3//P/9//v/Pjv8//3/+/8+O/z//f/7//3//P/9//v98pf8//3///8+O/z//f///z47/P/9///98pf8//3////9//z//f/7/w2r/P/9//v/Dav8//3/+//9//z//f/7/glr/P///fKX///+///98pf///7///3yl////v///fKX///+/glr/f////z+CWv9/////P4Ja/3////8/glr/f////z8AAHyl////vwAAfKX///+/AAB8pf///78AAHyl////v4Ha/3////8/gdr/f////z+B2v9/////P4Ha/3////8/qhl66/3/1UyqGXrrhv8TTYYUTsLR/z4bhhROwqj/1mSGFE7Ce/4VG2sZP7Bp/0AOQDBqmQ0AwHFOQoaUIwDAZE5ChpSDAelkTkKGlAgCohp6a6mZAgApM3prqZl4AOsyg5StmasZAECDlK2ZKBo8QAnCM5O8Teg/CcIzk/dNXj/i0aqWg2MBQOLRqpazYzdA4tGqloNjvT9U6Ruuepz9P1TpG658nAVAVOkbrkucxz//f///AAD/P/9///8AAP8/yuz0vUOyFEDK7PS9B7KgQFDme+tS5v4/UOZ769blwz8AAP9//3/+fwAA/3/ys/5/AAD/f/+//n8AAP9//v8AAAAA/3+B2v5/AAD/f/KzAAAAAP9//3/+fwAA/398pf5/AAD/f/9/DmMAAP9//38FZgAA/3//f0BtAAD/f/9/vlIAAP9//3++UgAA/3//fwAAAAD/f/9/vhIAAP9//78AAAAA/3+B2gAAAAD/f/9/AAAAAP9//3++Ev///3//v/5/////f/9/QG3///9/gdr+f////3+B2v5/////f/9/QG3///9//39Abf///3//f0Bt////f/9/vlL+//9/fKX+f/7//398pf5//v//f3yl/n/+//9/fKX+fwAA/3+B2v5/AAD/f4Ha/n+QwUnY////v5DBSdj///+//v//f4Ha/n/+//9/gdr+f/7//3+B2v5//v//f4Ha/n+0p22+////P7Snbb7///8/M7vw7v///78zu/Du////v/7//3+B2gAA/v//f4HaAAD+//9/gdoAAP7//3+B2gAAe29Uxf///z97b1TF////P3tvVMX///8/////f30lAAD///9/fSUAAP7//3+B2gAA/v//f4HaAAAAAP9/fSUAAAAA/399JQAAAAD/f30lAAAAAP9/fSUAAP///398pQAA////f/9/vhL///9/fKUAAP///398pQAA////f/9/QS3///9//39BLf///3//f74S////f/9/QS3///9/fKUAAP///3//f0Et////f/9/QS3///9/gloAAP///3+CWgAA////f4JaAAD///9/gloAAP///3//f74S////f/9/vhLSjtle/3++EtKO2V7/f74S2HDyXv9/vhIAAP9//39BLQAA/3//f0EtAAD/f/9/QS0AAP9//39BLQAA/3//f0EtVo3sLv9/QS1Wjewu/39BLf7//3//f0Et/v//f/9/QS3+//9//39BLf7//3//f0EtAAD/f/9/QS0AAP9//39BLQAA/3//f0EtAAD/f/9/QS23cZYv/39BLbdxli//f0Et////f/9/vlL///9//3++Uv///3//f75S////f/9/vlJpUEcO/3++UmlQRw7/f75S////f/9/vlL///9//3++UgAA/3//f75SAAD/f/9/vlIAAP9//3++UgAA/3//f75SAAD/f/9/vlLrrlcN/3++UuuuVw3/f75SDSEnD/9/QG3+//9//39Abf7//3//f0Bt/v//f/9/QG3+//9//39Abdne0g7/f0Bt2d7SDv9/QG0bEz3Bglr+fxsTPcGCWv5/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3Ai/Yf9/QG28I/0F/39Abbwj/QX/f0BtvCP9Bf9/QG28I/0F/39AbYkRH0T/f0BtiREfRP9/QG2JER9E/39AbYkRH0T/f0Bt4+zCPv9/QG3j7MI+/39AbePswj7/f0Bt4+zCPv9/QG33SFKz////P/dIUrP///8/UzP3yP///79TM/fI////v6vM98j///+/q8z3yP///7/8bu6/////P/xu7r////8/QD9z2f///79AP3PZ////v3RZQL////8/dFlAv////z8NkcvE////Pw2Ry8T///8/VUV77////79VRXvv////v1VFe+////+/D8D77v///78PwPvu////vwKR7r////8/ApHuv////z/vP/vu////v+8/++7///+/B7dSs////z8Ht1Kz////P/9//////68F/3/+//7/YXX/f/7///8AAP9//v8AAGgH/3/+/wAAaAf/f/7///8AAKYXPy//f0Btphc/L/9/QG2mFz8v/39AbaYXPy//f0BtGTk/L/9/vlIZOT8v/3++Uhk5Py//f75SGTk/L/9/vlLAUOVG/39BLcBQ5Ub/f0EtwFDlRv9/QS3AUOVG/39BLcBQWGj/f74SwFBYaP9/vhLAUFho/3++EsBQWGj/f74SwFCml4JaAADAUKaXgloAAMBQppd8pQAAwFCml4JaAADAUBm5gdoAAMBQGbmB2gAAwFAZuYHaAADAUBm5gdoAABk5v9B9Jf5/GTm/0IHa/n8ZOb/Qgdr+fxk5v9B9Jf5/phe/0Hyl/n+mF7/QfKX+f6YXv9B8pf5/phe/0Hyl/n9C3P0F/39AbULc/QX/f0BtQtz9Bf9/QG1C3P0F/39Abbmp/QX/f75Suan9Bf9/vlK5qf0F/3++Urmp/QX/f75S/YW5Kf9/QS39hbkp/39BLf2FuSn/f0Et/YW5Kf9/QS39hUNc/3++Ev2FQ1z/f74S/YVDXP9/vhL9hUNc/3++Ev2Fu6OCWgAA/YW7o3ylAAD9hbujfKUAAP2Fu6OCWgAA/YVF1oHaAAD9hUXWgdoAAP2FRdaB2gAA/YVF1oHaAAC5qQH6gdr+f7mpAfp9Jf5/uakB+n0l/n+5qQH6gdr+f0LcAfp8pf5/QtwB+nyl/n9C3AH6fKX+f0LcAfp8pf5////y5PLk/z////Lk8uT/Pxv/3kmbtf8/G//eSZq1/j+b/RBjDJv/P5v9EGMMm/8/EOOcffJk/z8Q45x98mT/P97JG39jSv8/3skbf2RKAEAMm/9/DBv/Pwyb/38MG/8/8mT/fwAAeTIAAPLk//95MgAA95L//3wJ+BL/fwAAfAn4Ev9/AAB8CQbt/38Hbf8/Bu3/fwdt/z////eS95L/P+ViBZ////8//38AAAAA/7//fwAAAAD/vwd6AOz///8/B3oA7P///z8HegDs////Pwd6AOz///8/MPcu3f///78w9y7d////vzQKpdz///+/NAql3P///78x+YS2////vzH5hLb///+/tgwZuv///7+KnEae////P4qcRp7///8//3///wAAmTr/f///AACZOv9///8AAJk6/3///wAAmTr/f///AACZOv9///8AAJk6/3///wAAmTr/f///AACZOp9eBOb///+/36LM4f///7/foszh////v2+D8+3///8/b4Pz7f///z9vg/Pt////P2+D8+3///8/u2Wq4f///z+7Zarh////P+WhGuz///+/5aEa7P///7/loRrs////v+WhGuz///+/rmm++////7+uab77////v65pvvv///+/eY3L5f///z95jcvl////P3mNy+X///8/eY3L5f///z8RcNvk////PxFw2+T///8/EXDb5P///z8RcNvk////P02ZcPj///+/TZlw+P///79NmXD4////v6FhYPD///+/oWFg8P///7+hYWDw////v6FhYPD///+/Q5mK3////z9DmYrf////P+++eLT///8/7754tP///z+rI23F////v6sjbcX///+/qyNtxf///7+C5jbh////v4LmNuH///+/FlLGs////z8WUsaz////P6av2LD///8/pq/YsP///z+HGj/f////v4caP9////+/6eCLwv///7/p4IvC////v+ngi8L///+/JUQtuP///z8lRC24////P8+YXq3///8/885bvP///7/zzlu8////vzE16b////+/MTXpv////79jZYmt////P2Nlia3///8/f1t+237bvy1/W37bftu/LfZ3VZbZWSAI9ndVlnxQcQ0NoA0gDSAGUA2gDSANIAZQzRb0+Kpb3RvNFvT4HGXBFzcM5uwuS0EjNwzm7NJTqR43DObspUFTKHzSgFVwVD9qfNKAVYdULmqpplTZUCTzKammVNlkNKMvDW6+jgwoZiINbr6O1iDnJQ1uvo4RL/0e9DrzuvO6eh30OvO687p6HQoz9UwjTyobCjP1TNdCKhZ9M32zfbO+GX0zfbN9s74Z5mw3jD44/RrmbDeMmTdLG+ZsN4xZLywfvsi/SL9IX2S+yL9Iv0hfZL4ODO6yO7crvg4M7i48dyu+DgzuBUJ2KDUOsO3kPBgrNQ6w7f1Bcyg1DrDtOTSXL2VsvosiNvgbZWy+i08vJh9lbL6LtkIbFqc2iEYCQ8YZpzaIRnRObCCwbTSOAynuIbBtNI6UJhsjsG00jhgvAB+pplTZdS+RLqmmVNm5IM0lBAqz7MdFfSUECrPsrUd2JAQKs+z4QBoonKnnzP///z+cqefM////P5yp58z///8/R09l4v///79HT2Xi////v/+jJvT///+//6Mm9P///7/qZDDW////P+pkMNb///8/6mQw1v///z+9mGnX////P72Yadf///8/vZhp1////z8nXn/2////vydef/b///+/J15/9v///7+7sj3f////v7uyPd////+/u7I93////79NVBbN////P01UFs3///8/TVQWzf///z8zh3fl////PzOHd+X///8/M4d35f///z/FunXM////v8W6dcz///+/xbp1zP///79DRcXO////v0NFxc7///+/eHbt4////z94du3j////P3h27eP///8/ZW4ozf///z9vPOLF////v2884sX///+/bzzixf///7/dxBrE////v93EGsT///+/3cQaxP///79xj83N////P3GPzc3///8/XU4AxP///z9dTgDE////P11OAMT///8/XU4AxP///z+tzIrM////v6unUMT///8/q6dQxP///z8mV4fD////PyZXh8P///8/JleHw////z9qNlnP////v2o2Wc////+/ajZZz////7/Fse3C////P8Wx7cL///8/")
-}]
-blend_shape_mode = 0
diff --git a/assets/characters/zombie.glb b/assets/characters/zombie.glb
deleted file mode 100644
index 4e23604..0000000
Binary files a/assets/characters/zombie.glb and /dev/null differ
diff --git a/assets/characters/zombie.glb.import b/assets/characters/zombie.glb.import
deleted file mode 100644
index a0fdee3..0000000
--- a/assets/characters/zombie.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://cxuh415268dqx"
-path="res://.godot/imported/zombie.glb-c1ff1897ad200769b737542af2d89583.scn"
-
-[deps]
-
-source_file="res://assets/characters/zombie.glb"
-dest_files=["res://.godot/imported/zombie.glb-c1ff1897ad200769b737542af2d89583.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=1
diff --git a/assets/characters/zombie_albedo.png b/assets/characters/zombie_albedo.png
deleted file mode 100644
index 1ca0e38..0000000
Binary files a/assets/characters/zombie_albedo.png and /dev/null differ
diff --git a/assets/characters/zombie_albedo.png.import b/assets/characters/zombie_albedo.png.import
deleted file mode 100644
index 8d28110..0000000
--- a/assets/characters/zombie_albedo.png.import
+++ /dev/null
@@ -1,44 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bwb82vwfq64hd"
-path.s3tc="res://.godot/imported/zombie_albedo.png-1bfc7d4dee960d06e936fe021a01a9cf.s3tc.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "ced1028fd11a8ba268b702dda970d3db"
-}
-
-[deps]
-
-source_file="res://assets/characters/zombie_albedo.png"
-dest_files=["res://.godot/imported/zombie_albedo.png-1bfc7d4dee960d06e936fe021a01a9cf.s3tc.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=true
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/assets/characters/zombie_idle.glb b/assets/characters/zombie_idle.glb
deleted file mode 100644
index d0b6822..0000000
Binary files a/assets/characters/zombie_idle.glb and /dev/null differ
diff --git a/assets/characters/zombie_idle.glb.import b/assets/characters/zombie_idle.glb.import
deleted file mode 100644
index 361b4f7..0000000
--- a/assets/characters/zombie_idle.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="animation_library"
-importer_version=1
-type="AnimationLibrary"
-uid="uid://dj2grhmw35lxn"
-path="res://.godot/imported/zombie_idle.glb-f72f25f88e75b45744fe5d02ace97514.res"
-
-[deps]
-
-source_file="res://assets/characters/zombie_idle.glb"
-dest_files=["res://.godot/imported/zombie_idle.glb-f72f25f88e75b45744fe5d02ace97514.res"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=1
diff --git a/assets/characters/zombie_jump.glb b/assets/characters/zombie_jump.glb
deleted file mode 100644
index 4a29fc4..0000000
Binary files a/assets/characters/zombie_jump.glb and /dev/null differ
diff --git a/assets/characters/zombie_jump.glb.import b/assets/characters/zombie_jump.glb.import
deleted file mode 100644
index 09ac876..0000000
--- a/assets/characters/zombie_jump.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="animation_library"
-importer_version=1
-type="AnimationLibrary"
-uid="uid://bcj3urpoubxqe"
-path="res://.godot/imported/zombie_jump.glb-6822615b39ee9b579be64717feb5bacf.res"
-
-[deps]
-
-source_file="res://assets/characters/zombie_jump.glb"
-dest_files=["res://.godot/imported/zombie_jump.glb-6822615b39ee9b579be64717feb5bacf.res"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=1
diff --git a/assets/characters/zombie_run.glb b/assets/characters/zombie_run.glb
deleted file mode 100644
index 7b9ac17..0000000
Binary files a/assets/characters/zombie_run.glb and /dev/null differ
diff --git a/assets/characters/zombie_run.glb.import b/assets/characters/zombie_run.glb.import
deleted file mode 100644
index d4aca3f..0000000
--- a/assets/characters/zombie_run.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="animation_library"
-importer_version=1
-type="AnimationLibrary"
-uid="uid://wncmdkjlpsw6"
-path="res://.godot/imported/zombie_run.glb-37d140be64ca389ededa374a8cad0ece.res"
-
-[deps]
-
-source_file="res://assets/characters/zombie_run.glb"
-dest_files=["res://.godot/imported/zombie_run.glb-37d140be64ca389ededa374a8cad0ece.res"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=1
diff --git a/assets/environment/ATTRIBUTIONS.md b/assets/environment/ATTRIBUTIONS.md
deleted file mode 100644
index d8e4ac3..0000000
--- a/assets/environment/ATTRIBUTIONS.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# Third-Party Environment Assets
-
-The assets listed in this file are not covered by the project's MIT license.
-Each asset remains available under the license identified below.
-
-## Prototype Textures 1.0
-
-- Creator and distributor: [Kenney](https://www.kenney.nl/)
-- Creation date: 2020-04-08
-- License: [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/)
-- Scope: `prototype_dark.png` and `prototype_orange.png`
-- Attribution is appreciated but not required.
diff --git a/assets/environment/textures/prototype_dark.png b/assets/environment/textures/prototype_dark.png
deleted file mode 100644
index e81fa1a..0000000
Binary files a/assets/environment/textures/prototype_dark.png and /dev/null differ
diff --git a/assets/environment/textures/prototype_dark.png.import b/assets/environment/textures/prototype_dark.png.import
deleted file mode 100644
index 588bf21..0000000
--- a/assets/environment/textures/prototype_dark.png.import
+++ /dev/null
@@ -1,41 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bod55c64uwadg"
-path.s3tc="res://.godot/imported/prototype_dark.png-2a219fc5061b436fd73861c822099449.s3tc.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://assets/environment/textures/prototype_dark.png"
-dest_files=["res://.godot/imported/prototype_dark.png-2a219fc5061b436fd73861c822099449.s3tc.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=true
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/assets/environment/textures/prototype_orange.png b/assets/environment/textures/prototype_orange.png
deleted file mode 100644
index 979733a..0000000
Binary files a/assets/environment/textures/prototype_orange.png and /dev/null differ
diff --git a/assets/environment/textures/prototype_orange.png.import b/assets/environment/textures/prototype_orange.png.import
deleted file mode 100644
index 060122a..0000000
--- a/assets/environment/textures/prototype_orange.png.import
+++ /dev/null
@@ -1,41 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://deemrlaoume8q"
-path.s3tc="res://.godot/imported/prototype_orange.png-30085a93e948d863898a4d4925bbf62e.s3tc.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://assets/environment/textures/prototype_orange.png"
-dest_files=["res://.godot/imported/prototype_orange.png-30085a93e948d863898a4d4925bbf62e.s3tc.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=true
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/assets/fonts/Kurland.ttf b/assets/fonts/Kurland.ttf
deleted file mode 100644
index 0908cf3..0000000
Binary files a/assets/fonts/Kurland.ttf and /dev/null differ
diff --git a/assets/fonts/Kurland.ttf.import b/assets/fonts/Kurland.ttf.import
deleted file mode 100644
index 770d5d6..0000000
--- a/assets/fonts/Kurland.ttf.import
+++ /dev/null
@@ -1,36 +0,0 @@
-[remap]
-
-importer="font_data_dynamic"
-type="FontFile"
-uid="uid://diapabmalpcrj"
-path="res://.godot/imported/Kurland.ttf-14f9cbbd8657b37f475042f6a32feeab.fontdata"
-
-[deps]
-
-source_file="res://assets/fonts/Kurland.ttf"
-dest_files=["res://.godot/imported/Kurland.ttf-14f9cbbd8657b37f475042f6a32feeab.fontdata"]
-
-[params]
-
-Rendering=null
-antialiasing=1
-generate_mipmaps=false
-disable_embedded_bitmaps=true
-multichannel_signed_distance_field=false
-msdf_pixel_range=8
-msdf_size=48
-allow_system_fallback=true
-force_autohinter=false
-modulate_color_glyphs=false
-hinting=1
-subpixel_positioning=1
-keep_rounding_remainders=true
-oversampling=0.0
-Fallbacks=null
-fallbacks=[]
-Compress=null
-compress=true
-preload=[]
-language_support={}
-script_support={}
-opentype_features={}
diff --git a/assets/items/ATTRIBUTIONS.md b/assets/items/ATTRIBUTIONS.md
deleted file mode 100644
index 75fe4eb..0000000
--- a/assets/items/ATTRIBUTIONS.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Third-Party Item Assets
-
-The assets listed in this file are not covered by the project's MIT license.
-Each asset remains available under the license identified in its section.
-
-## Google Poly Hats
-
-- Creator: Poly by Google
-- Distributor: Poly Pizza
-- License: [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/)
-- Scope: hat models and their rendered inventory icons
-- Modifications: files were renamed, geometry scale and origin were normalized,
- drop-item collision and display scale were adjusted, and inventory icons were
- derived from the models.
-
-| Asset | Source |
-| --- | --- |
-| Fedora | https://poly.pizza/m/8TpZrCG3aRf |
-| Headphones | https://poly.pizza/m/4QQ-QHSQhOI |
-| Pirate Hat | https://poly.pizza/m/9QEds6cYAy6 |
-| Sheriff Hat | https://poly.pizza/m/ayNYjI4jcW4 |
-| Sombrero | https://poly.pizza/m/7fGDqHvHap1 |
-| Wizard Hat | https://poly.pizza/m/7VVumyY7L_u |
-
-## Quaternius Items
-
-- Creator: [Quaternius](https://www.patreon.com/quaternius)
-- License: [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/)
-- Scope: backpack, weapon, and miscellaneous item models and icons
-- Modifications: files were selected from the original bundle, renamed where
- needed, and adjusted for use as inventory items and world drops.
diff --git a/assets/items/backpacks/icons/backpack.png b/assets/items/backpacks/icons/backpack.png
deleted file mode 100644
index 11c3daa..0000000
Binary files a/assets/items/backpacks/icons/backpack.png and /dev/null differ
diff --git a/assets/items/backpacks/icons/backpack.png.import b/assets/items/backpacks/icons/backpack.png.import
deleted file mode 100644
index 4b535cf..0000000
--- a/assets/items/backpacks/icons/backpack.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://mtwe8ayrubi8"
-path="res://.godot/imported/backpack.png-f81b6361a20bd805df400b77fae207f7.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/items/backpacks/icons/backpack.png"
-dest_files=["res://.godot/imported/backpack.png-f81b6361a20bd805df400b77fae207f7.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/items/backpacks/models/backpack.fbx b/assets/items/backpacks/models/backpack.fbx
deleted file mode 100644
index 9342526..0000000
Binary files a/assets/items/backpacks/models/backpack.fbx and /dev/null differ
diff --git a/assets/items/backpacks/models/backpack.fbx.import b/assets/items/backpacks/models/backpack.fbx.import
deleted file mode 100644
index 11c3523..0000000
--- a/assets/items/backpacks/models/backpack.fbx.import
+++ /dev/null
@@ -1,46 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://cc8gkp6u5tr2e"
-path="res://.godot/imported/backpack.fbx-38899ad32ee1cca0c49a1ac759278fed.scn"
-
-[deps]
-
-source_file="res://assets/items/backpacks/models/backpack.fbx"
-dest_files=["res://.godot/imported/backpack.fbx-38899ad32ee1cca0c49a1ac759278fed.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=true
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-fbx/importer=0
-fbx/allow_geometry_helper_nodes=false
-fbx/embedded_image_handling=1
-fbx/naming_version=2
diff --git a/assets/items/hats/icons/fedora.png b/assets/items/hats/icons/fedora.png
deleted file mode 100644
index 67510af..0000000
Binary files a/assets/items/hats/icons/fedora.png and /dev/null differ
diff --git a/assets/items/hats/icons/fedora.png.import b/assets/items/hats/icons/fedora.png.import
deleted file mode 100644
index 7edd759..0000000
--- a/assets/items/hats/icons/fedora.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://buqrcwl17sf0t"
-path="res://.godot/imported/fedora.png-b4b9b244c6420c604072ea1e3334a9d2.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/items/hats/icons/fedora.png"
-dest_files=["res://.godot/imported/fedora.png-b4b9b244c6420c604072ea1e3334a9d2.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/items/hats/icons/headphones.png b/assets/items/hats/icons/headphones.png
deleted file mode 100644
index da894fb..0000000
Binary files a/assets/items/hats/icons/headphones.png and /dev/null differ
diff --git a/assets/items/hats/icons/headphones.png.import b/assets/items/hats/icons/headphones.png.import
deleted file mode 100644
index 545f368..0000000
--- a/assets/items/hats/icons/headphones.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bma36fnmgr8c5"
-path="res://.godot/imported/headphones.png-ba884009c705e3c1ba2a0fbda564140d.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/items/hats/icons/headphones.png"
-dest_files=["res://.godot/imported/headphones.png-ba884009c705e3c1ba2a0fbda564140d.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/items/hats/icons/pirate_hat.png b/assets/items/hats/icons/pirate_hat.png
deleted file mode 100644
index 8dc2f31..0000000
Binary files a/assets/items/hats/icons/pirate_hat.png and /dev/null differ
diff --git a/assets/items/hats/icons/pirate_hat.png.import b/assets/items/hats/icons/pirate_hat.png.import
deleted file mode 100644
index 22f7e27..0000000
--- a/assets/items/hats/icons/pirate_hat.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ftm86yfr6qra"
-path="res://.godot/imported/pirate_hat.png-3d5463144aac6b014930175384aef23f.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/items/hats/icons/pirate_hat.png"
-dest_files=["res://.godot/imported/pirate_hat.png-3d5463144aac6b014930175384aef23f.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/items/hats/icons/sheriff_hat.png b/assets/items/hats/icons/sheriff_hat.png
deleted file mode 100644
index caad0f2..0000000
Binary files a/assets/items/hats/icons/sheriff_hat.png and /dev/null differ
diff --git a/assets/items/hats/icons/sheriff_hat.png.import b/assets/items/hats/icons/sheriff_hat.png.import
deleted file mode 100644
index 8c492dd..0000000
--- a/assets/items/hats/icons/sheriff_hat.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c5wuuj6g6awq2"
-path="res://.godot/imported/sheriff_hat.png-e04b03a425a81e8943dbfb94f35b5d4d.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/items/hats/icons/sheriff_hat.png"
-dest_files=["res://.godot/imported/sheriff_hat.png-e04b03a425a81e8943dbfb94f35b5d4d.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/items/hats/icons/sombrero.png b/assets/items/hats/icons/sombrero.png
deleted file mode 100644
index d068379..0000000
Binary files a/assets/items/hats/icons/sombrero.png and /dev/null differ
diff --git a/assets/items/hats/icons/sombrero.png.import b/assets/items/hats/icons/sombrero.png.import
deleted file mode 100644
index 8b37437..0000000
--- a/assets/items/hats/icons/sombrero.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c8spyduboiuuw"
-path="res://.godot/imported/sombrero.png-b5f6b9a75a958b9c8ef9116aeef72e6a.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/items/hats/icons/sombrero.png"
-dest_files=["res://.godot/imported/sombrero.png-b5f6b9a75a958b9c8ef9116aeef72e6a.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/items/hats/icons/wizard_hat.png b/assets/items/hats/icons/wizard_hat.png
deleted file mode 100644
index 4efce25..0000000
Binary files a/assets/items/hats/icons/wizard_hat.png and /dev/null differ
diff --git a/assets/items/hats/icons/wizard_hat.png.import b/assets/items/hats/icons/wizard_hat.png.import
deleted file mode 100644
index 17680db..0000000
--- a/assets/items/hats/icons/wizard_hat.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bueuqyppakkg6"
-path="res://.godot/imported/wizard_hat.png-829c6a01ee5165a82a21e87b3799a539.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/items/hats/icons/wizard_hat.png"
-dest_files=["res://.godot/imported/wizard_hat.png-829c6a01ee5165a82a21e87b3799a539.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/items/hats/models/fedora.glb b/assets/items/hats/models/fedora.glb
deleted file mode 100644
index dc1ec73..0000000
Binary files a/assets/items/hats/models/fedora.glb and /dev/null differ
diff --git a/assets/items/hats/models/fedora.glb.import b/assets/items/hats/models/fedora.glb.import
deleted file mode 100644
index 28bd2ac..0000000
--- a/assets/items/hats/models/fedora.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://i1ar5au3vy7a"
-path="res://.godot/imported/fedora.glb-3919f5d4fbe8b41d682e930f290c05a3.scn"
-
-[deps]
-
-source_file="res://assets/items/hats/models/fedora.glb"
-dest_files=["res://.godot/imported/fedora.glb-3919f5d4fbe8b41d682e930f290c05a3.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=0
diff --git a/assets/items/hats/models/headphones.glb b/assets/items/hats/models/headphones.glb
deleted file mode 100644
index ded8ddc..0000000
Binary files a/assets/items/hats/models/headphones.glb and /dev/null differ
diff --git a/assets/items/hats/models/headphones.glb.import b/assets/items/hats/models/headphones.glb.import
deleted file mode 100644
index 348589e..0000000
--- a/assets/items/hats/models/headphones.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://bcfswa3gh780w"
-path="res://.godot/imported/headphones.glb-a1b36e3e7d36b145c75ce76570b35be2.scn"
-
-[deps]
-
-source_file="res://assets/items/hats/models/headphones.glb"
-dest_files=["res://.godot/imported/headphones.glb-a1b36e3e7d36b145c75ce76570b35be2.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=0
diff --git a/assets/items/hats/models/pirate_hat.glb b/assets/items/hats/models/pirate_hat.glb
deleted file mode 100644
index 1b35caa..0000000
Binary files a/assets/items/hats/models/pirate_hat.glb and /dev/null differ
diff --git a/assets/items/hats/models/pirate_hat.glb.import b/assets/items/hats/models/pirate_hat.glb.import
deleted file mode 100644
index 5490332..0000000
--- a/assets/items/hats/models/pirate_hat.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://dogjmlb8ci3gr"
-path="res://.godot/imported/pirate_hat.glb-68df7963aa96bb7877fccaefee25f3f3.scn"
-
-[deps]
-
-source_file="res://assets/items/hats/models/pirate_hat.glb"
-dest_files=["res://.godot/imported/pirate_hat.glb-68df7963aa96bb7877fccaefee25f3f3.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=0
diff --git a/assets/items/hats/models/pirate_hat_PirateHat_BaseColor.png b/assets/items/hats/models/pirate_hat_PirateHat_BaseColor.png
deleted file mode 100644
index 412a3b3..0000000
Binary files a/assets/items/hats/models/pirate_hat_PirateHat_BaseColor.png and /dev/null differ
diff --git a/assets/items/hats/models/pirate_hat_PirateHat_BaseColor.png.import b/assets/items/hats/models/pirate_hat_PirateHat_BaseColor.png.import
deleted file mode 100644
index 2651f0b..0000000
--- a/assets/items/hats/models/pirate_hat_PirateHat_BaseColor.png.import
+++ /dev/null
@@ -1,44 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ce05jcktlvujb"
-path.s3tc="res://.godot/imported/pirate_hat_PirateHat_BaseColor.png-5d0b26d32693be25e19bd236b27c16b9.s3tc.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "88c054594a6f283dabff78978fac8b4c"
-}
-
-[deps]
-
-source_file="res://assets/items/hats/models/pirate_hat_PirateHat_BaseColor.png"
-dest_files=["res://.godot/imported/pirate_hat_PirateHat_BaseColor.png-5d0b26d32693be25e19bd236b27c16b9.s3tc.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=true
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/assets/items/hats/models/sheriff_hat.glb b/assets/items/hats/models/sheriff_hat.glb
deleted file mode 100644
index 3260bcf..0000000
Binary files a/assets/items/hats/models/sheriff_hat.glb and /dev/null differ
diff --git a/assets/items/hats/models/sheriff_hat.glb.import b/assets/items/hats/models/sheriff_hat.glb.import
deleted file mode 100644
index bd2e997..0000000
--- a/assets/items/hats/models/sheriff_hat.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://crcqxt31qmp7k"
-path="res://.godot/imported/sheriff_hat.glb-a3d4c0ae956f0f148d5cbbe95e69bfd7.scn"
-
-[deps]
-
-source_file="res://assets/items/hats/models/sheriff_hat.glb"
-dest_files=["res://.godot/imported/sheriff_hat.glb-a3d4c0ae956f0f148d5cbbe95e69bfd7.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=0
diff --git a/assets/items/hats/models/sheriff_hat_SheriffHat_BaseColor.png b/assets/items/hats/models/sheriff_hat_SheriffHat_BaseColor.png
deleted file mode 100644
index 2d1603f..0000000
Binary files a/assets/items/hats/models/sheriff_hat_SheriffHat_BaseColor.png and /dev/null differ
diff --git a/assets/items/hats/models/sheriff_hat_SheriffHat_BaseColor.png.import b/assets/items/hats/models/sheriff_hat_SheriffHat_BaseColor.png.import
deleted file mode 100644
index af28b41..0000000
--- a/assets/items/hats/models/sheriff_hat_SheriffHat_BaseColor.png.import
+++ /dev/null
@@ -1,44 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dhgajn6rckvrq"
-path.s3tc="res://.godot/imported/sheriff_hat_SheriffHat_BaseColor.png-70bd428203ba984543b2304a79668f80.s3tc.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "5b2de4116c8c3bb00441985f79e47e99"
-}
-
-[deps]
-
-source_file="res://assets/items/hats/models/sheriff_hat_SheriffHat_BaseColor.png"
-dest_files=["res://.godot/imported/sheriff_hat_SheriffHat_BaseColor.png-70bd428203ba984543b2304a79668f80.s3tc.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=true
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/assets/items/hats/models/sombrero.glb b/assets/items/hats/models/sombrero.glb
deleted file mode 100644
index c3f8b51..0000000
Binary files a/assets/items/hats/models/sombrero.glb and /dev/null differ
diff --git a/assets/items/hats/models/sombrero.glb.import b/assets/items/hats/models/sombrero.glb.import
deleted file mode 100644
index fde223a..0000000
--- a/assets/items/hats/models/sombrero.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://b0kmc4gvkiqfw"
-path="res://.godot/imported/sombrero.glb-a2cabe0dc73a33d77ef407ac33af4b17.scn"
-
-[deps]
-
-source_file="res://assets/items/hats/models/sombrero.glb"
-dest_files=["res://.godot/imported/sombrero.glb-a2cabe0dc73a33d77ef407ac33af4b17.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=0
diff --git a/assets/items/hats/models/wizard_hat.glb b/assets/items/hats/models/wizard_hat.glb
deleted file mode 100644
index e26a6b4..0000000
Binary files a/assets/items/hats/models/wizard_hat.glb and /dev/null differ
diff --git a/assets/items/hats/models/wizard_hat.glb.import b/assets/items/hats/models/wizard_hat.glb.import
deleted file mode 100644
index ac820db..0000000
--- a/assets/items/hats/models/wizard_hat.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://bry1poskgbmg0"
-path="res://.godot/imported/wizard_hat.glb-9084e83fe203386835e5c95127437fe2.scn"
-
-[deps]
-
-source_file="res://assets/items/hats/models/wizard_hat.glb"
-dest_files=["res://.godot/imported/wizard_hat.glb-9084e83fe203386835e5c95127437fe2.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=0
diff --git a/assets/items/misc/icons/bone.png b/assets/items/misc/icons/bone.png
deleted file mode 100644
index c8763ac..0000000
Binary files a/assets/items/misc/icons/bone.png and /dev/null differ
diff --git a/assets/items/misc/icons/bone.png.import b/assets/items/misc/icons/bone.png.import
deleted file mode 100644
index 3c05c22..0000000
--- a/assets/items/misc/icons/bone.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c3cn1r74r6np3"
-path="res://.godot/imported/bone.png-bae71026f86dcbaa67d4c19cb3aa8366.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/items/misc/icons/bone.png"
-dest_files=["res://.godot/imported/bone.png-bae71026f86dcbaa67d4c19cb3aa8366.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/items/misc/icons/chalice.png b/assets/items/misc/icons/chalice.png
deleted file mode 100644
index 82fbbff..0000000
Binary files a/assets/items/misc/icons/chalice.png and /dev/null differ
diff --git a/assets/items/misc/icons/chalice.png.import b/assets/items/misc/icons/chalice.png.import
deleted file mode 100644
index 708bfda..0000000
--- a/assets/items/misc/icons/chalice.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ce2svvyome08u"
-path="res://.godot/imported/chalice.png-f7350fc92839f5ac5798c61f1e5d47e8.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/items/misc/icons/chalice.png"
-dest_files=["res://.godot/imported/chalice.png-f7350fc92839f5ac5798c61f1e5d47e8.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/items/misc/icons/chicken_leg.png b/assets/items/misc/icons/chicken_leg.png
deleted file mode 100644
index 89b813d..0000000
Binary files a/assets/items/misc/icons/chicken_leg.png and /dev/null differ
diff --git a/assets/items/misc/icons/chicken_leg.png.import b/assets/items/misc/icons/chicken_leg.png.import
deleted file mode 100644
index 38b52ba..0000000
--- a/assets/items/misc/icons/chicken_leg.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ch6ljv3j7jner"
-path="res://.godot/imported/chicken_leg.png-2ab263dfb4e8f8ea94bb4d07fc178944.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/items/misc/icons/chicken_leg.png"
-dest_files=["res://.godot/imported/chicken_leg.png-2ab263dfb4e8f8ea94bb4d07fc178944.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/items/misc/models/bone.fbx b/assets/items/misc/models/bone.fbx
deleted file mode 100644
index 652cc58..0000000
Binary files a/assets/items/misc/models/bone.fbx and /dev/null differ
diff --git a/assets/items/misc/models/bone.fbx.import b/assets/items/misc/models/bone.fbx.import
deleted file mode 100644
index 6511993..0000000
--- a/assets/items/misc/models/bone.fbx.import
+++ /dev/null
@@ -1,46 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://deg1rygdirpe3"
-path="res://.godot/imported/bone.fbx-5aa4b9b1b3a9af9df1294cbf20fc9c47.scn"
-
-[deps]
-
-source_file="res://assets/items/misc/models/bone.fbx"
-dest_files=["res://.godot/imported/bone.fbx-5aa4b9b1b3a9af9df1294cbf20fc9c47.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=true
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-fbx/importer=0
-fbx/allow_geometry_helper_nodes=false
-fbx/embedded_image_handling=1
-fbx/naming_version=2
diff --git a/assets/items/misc/models/chalice.fbx b/assets/items/misc/models/chalice.fbx
deleted file mode 100644
index 27e42a4..0000000
Binary files a/assets/items/misc/models/chalice.fbx and /dev/null differ
diff --git a/assets/items/misc/models/chalice.fbx.import b/assets/items/misc/models/chalice.fbx.import
deleted file mode 100644
index f95a7c4..0000000
--- a/assets/items/misc/models/chalice.fbx.import
+++ /dev/null
@@ -1,46 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://fnf4qo0ikbtb"
-path="res://.godot/imported/chalice.fbx-c226801191cfb92a227e72ddd4f39acf.scn"
-
-[deps]
-
-source_file="res://assets/items/misc/models/chalice.fbx"
-dest_files=["res://.godot/imported/chalice.fbx-c226801191cfb92a227e72ddd4f39acf.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=true
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-fbx/importer=0
-fbx/allow_geometry_helper_nodes=false
-fbx/embedded_image_handling=1
-fbx/naming_version=2
diff --git a/assets/items/misc/models/chicken_leg.fbx b/assets/items/misc/models/chicken_leg.fbx
deleted file mode 100644
index e1037f9..0000000
Binary files a/assets/items/misc/models/chicken_leg.fbx and /dev/null differ
diff --git a/assets/items/misc/models/chicken_leg.fbx.import b/assets/items/misc/models/chicken_leg.fbx.import
deleted file mode 100644
index 625fa7c..0000000
--- a/assets/items/misc/models/chicken_leg.fbx.import
+++ /dev/null
@@ -1,46 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://bfscxiykkwo4f"
-path="res://.godot/imported/chicken_leg.fbx-b32027d7625307d7e496b9f4a7556b05.scn"
-
-[deps]
-
-source_file="res://assets/items/misc/models/chicken_leg.fbx"
-dest_files=["res://.godot/imported/chicken_leg.fbx-b32027d7625307d7e496b9f4a7556b05.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=true
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-fbx/importer=0
-fbx/allow_geometry_helper_nodes=false
-fbx/embedded_image_handling=1
-fbx/naming_version=2
diff --git a/assets/items/weapons/icons/axe.png b/assets/items/weapons/icons/axe.png
deleted file mode 100644
index 7dd84aa..0000000
Binary files a/assets/items/weapons/icons/axe.png and /dev/null differ
diff --git a/assets/items/weapons/icons/axe.png.import b/assets/items/weapons/icons/axe.png.import
deleted file mode 100644
index 71ae612..0000000
--- a/assets/items/weapons/icons/axe.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://crmxy7sixrmam"
-path="res://.godot/imported/axe.png-357de16c8cd3ed7488158129a5b85a18.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/items/weapons/icons/axe.png"
-dest_files=["res://.godot/imported/axe.png-357de16c8cd3ed7488158129a5b85a18.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/items/weapons/icons/sword.png b/assets/items/weapons/icons/sword.png
deleted file mode 100644
index a78c4fe..0000000
Binary files a/assets/items/weapons/icons/sword.png and /dev/null differ
diff --git a/assets/items/weapons/icons/sword.png.import b/assets/items/weapons/icons/sword.png.import
deleted file mode 100644
index 44aaa1a..0000000
--- a/assets/items/weapons/icons/sword.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dxuj4fxkibvko"
-path="res://.godot/imported/sword.png-83f5c60ef6562a62b7ad8c21805ebcfa.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/items/weapons/icons/sword.png"
-dest_files=["res://.godot/imported/sword.png-83f5c60ef6562a62b7ad8c21805ebcfa.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/items/weapons/icons/sword_big.png b/assets/items/weapons/icons/sword_big.png
deleted file mode 100644
index 92f0d5a..0000000
Binary files a/assets/items/weapons/icons/sword_big.png and /dev/null differ
diff --git a/assets/items/weapons/icons/sword_big.png.import b/assets/items/weapons/icons/sword_big.png.import
deleted file mode 100644
index 8b76951..0000000
--- a/assets/items/weapons/icons/sword_big.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ctcaj3n8li4nx"
-path="res://.godot/imported/sword_big.png-2c860196e2850f24014d1b502587cac6.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/items/weapons/icons/sword_big.png"
-dest_files=["res://.godot/imported/sword_big.png-2c860196e2850f24014d1b502587cac6.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/assets/items/weapons/models/axe.fbx b/assets/items/weapons/models/axe.fbx
deleted file mode 100644
index ef43ec4..0000000
Binary files a/assets/items/weapons/models/axe.fbx and /dev/null differ
diff --git a/assets/items/weapons/models/axe.fbx.import b/assets/items/weapons/models/axe.fbx.import
deleted file mode 100644
index cfb64fd..0000000
--- a/assets/items/weapons/models/axe.fbx.import
+++ /dev/null
@@ -1,46 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://dghg7ospl0imy"
-path="res://.godot/imported/axe.fbx-7ba7f27fa273c75fd3e474124f1dca5b.scn"
-
-[deps]
-
-source_file="res://assets/items/weapons/models/axe.fbx"
-dest_files=["res://.godot/imported/axe.fbx-7ba7f27fa273c75fd3e474124f1dca5b.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=true
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-fbx/importer=0
-fbx/allow_geometry_helper_nodes=false
-fbx/embedded_image_handling=1
-fbx/naming_version=2
diff --git a/assets/items/weapons/models/sword.fbx b/assets/items/weapons/models/sword.fbx
deleted file mode 100644
index 5a1b998..0000000
Binary files a/assets/items/weapons/models/sword.fbx and /dev/null differ
diff --git a/assets/items/weapons/models/sword.fbx.import b/assets/items/weapons/models/sword.fbx.import
deleted file mode 100644
index 159bf7c..0000000
--- a/assets/items/weapons/models/sword.fbx.import
+++ /dev/null
@@ -1,46 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://difodi4a2r6vc"
-path="res://.godot/imported/sword.fbx-f137f723d5df7cdf778af311839849a2.scn"
-
-[deps]
-
-source_file="res://assets/items/weapons/models/sword.fbx"
-dest_files=["res://.godot/imported/sword.fbx-f137f723d5df7cdf778af311839849a2.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=true
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-fbx/importer=0
-fbx/allow_geometry_helper_nodes=false
-fbx/embedded_image_handling=1
-fbx/naming_version=2
diff --git a/assets/items/weapons/models/sword_big.fbx b/assets/items/weapons/models/sword_big.fbx
deleted file mode 100644
index cee9822..0000000
Binary files a/assets/items/weapons/models/sword_big.fbx and /dev/null differ
diff --git a/assets/items/weapons/models/sword_big.fbx.import b/assets/items/weapons/models/sword_big.fbx.import
deleted file mode 100644
index c32bcb8..0000000
--- a/assets/items/weapons/models/sword_big.fbx.import
+++ /dev/null
@@ -1,46 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://dj7318sc4o37l"
-path="res://.godot/imported/sword_big.fbx-aace044831b01db9507995c40257e487.scn"
-
-[deps]
-
-source_file="res://assets/items/weapons/models/sword_big.fbx"
-dest_files=["res://.godot/imported/sword_big.fbx-aace044831b01db9507995c40257e487.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=true
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-fbx/importer=0
-fbx/allow_geometry_helper_nodes=false
-fbx/embedded_image_handling=1
-fbx/naming_version=2
diff --git a/assets/ui/close.svg b/assets/ui/close.svg
deleted file mode 100644
index ce26a9b..0000000
--- a/assets/ui/close.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/assets/ui/close.svg.import b/assets/ui/close.svg.import
deleted file mode 100644
index 761eb19..0000000
--- a/assets/ui/close.svg.import
+++ /dev/null
@@ -1,43 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bm6aanpydbyqk"
-path="res://.godot/imported/close.svg-48602a5fd3dfc407a0a61efbc721d36a.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://assets/ui/close.svg"
-dest_files=["res://.godot/imported/close.svg-48602a5fd3dfc407a0a61efbc721d36a.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
-svg/scale=1.0
-editor/scale_with_editor_scale=false
-editor/convert_colors_with_editor_theme=false
diff --git a/default_bus_layout.tres b/default_bus_layout.tres
deleted file mode 100644
index a87c555..0000000
--- a/default_bus_layout.tres
+++ /dev/null
@@ -1,37 +0,0 @@
-[gd_resource type="AudioBusLayout" format=3 uid="uid://c8prq6lu7oaae"]
-
-[sub_resource type="AudioEffectCapture" id="AudioEffectCapture_voice"]
-buffer_length = 0.2
-
-[sub_resource type="AudioEffectReverb" id="AudioEffectReverb_j3pel"]
-resource_name = "reverb"
-hipass = 1.0
-
-[sub_resource type="AudioEffectPanner" id="AudioEffectPanner_g28q7"]
-resource_name = "pan"
-
-[resource]
-bus/1/name = &"mic"
-bus/1/solo = false
-bus/1/mute = true
-bus/1/bypass_fx = false
-bus/1/volume_db = 0.0
-bus/1/send = &"Master"
-bus/1/effect/0/effect = SubResource("AudioEffectCapture_voice")
-bus/1/effect/0/enabled = true
-bus/2/name = &"RaytracedReverb"
-bus/2/solo = false
-bus/2/mute = false
-bus/2/bypass_fx = false
-bus/2/volume_db = 0.0
-bus/2/send = &"Master"
-bus/2/effect/0/effect = SubResource("AudioEffectReverb_j3pel")
-bus/2/effect/0/enabled = true
-bus/3/name = &"RaytracedAmbient"
-bus/3/solo = false
-bus/3/mute = false
-bus/3/bypass_fx = false
-bus/3/volume_db = 0.0
-bus/3/send = &"Master"
-bus/3/effect/0/effect = SubResource("AudioEffectPanner_g28q7")
-bus/3/effect/0/enabled = true
diff --git a/export_presets.cfg b/export_presets.cfg
deleted file mode 100644
index a5e5d5b..0000000
--- a/export_presets.cfg
+++ /dev/null
@@ -1,74 +0,0 @@
-[runnable_presets]
-
-"Windows Desktop"="Windows Desktop"
-
-[preset.0]
-
-name="Windows Desktop"
-platform="Windows Desktop"
-dedicated_server=false
-custom_features=""
-export_filter="all_resources"
-include_filter=""
-exclude_filter=""
-export_path="output/agame.exe"
-patches=PackedStringArray()
-patch_delta_encoding=false
-patch_delta_compression_level_zstd=19
-patch_delta_min_reduction=0.1
-patch_delta_include_filters="*"
-patch_delta_exclude_filters=""
-encryption_include_filters=""
-encryption_exclude_filters=""
-seed=0
-encrypt_pck=false
-encrypt_directory=false
-script_export_mode=2
-
-[preset.0.options]
-
-custom_template/debug=""
-custom_template/release=""
-debug/export_console_wrapper=0
-binary_format/embed_pck=true
-texture_format/s3tc_bptc=true
-texture_format/etc2_astc=false
-shader_baker/enabled=false
-binary_format/architecture="x86_64"
-codesign/enable=false
-codesign/timestamp=true
-codesign/timestamp_server_url=""
-codesign/digest_algorithm=1
-codesign/description=""
-codesign/custom_options=PackedStringArray()
-application/modify_resources=true
-application/icon=""
-application/console_wrapper_icon=""
-application/icon_interpolation=4
-application/file_version=""
-application/product_version=""
-application/company_name=""
-application/product_name=""
-application/file_description=""
-application/copyright=""
-application/trademarks=""
-application/export_angle=0
-application/export_d3d12=0
-application/d3d12_agility_sdk_multiarch=true
-ssh_remote_deploy/enabled=false
-ssh_remote_deploy/host="user@host_ip"
-ssh_remote_deploy/port="22"
-ssh_remote_deploy/extra_args_ssh=""
-ssh_remote_deploy/extra_args_scp=""
-ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
-$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
-$trigger = New-ScheduledTaskTrigger -Once -At 00:00
-$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
-$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
-Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
-Start-ScheduledTask -TaskName godot_remote_debug
-while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
-Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
-ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
-Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
-Remove-Item -Recurse -Force '{temp_dir}'"
diff --git a/icon.png b/icon.png
deleted file mode 100644
index 8653358..0000000
Binary files a/icon.png and /dev/null differ
diff --git a/icon.png.import b/icon.png.import
deleted file mode 100644
index 0bd9290..0000000
--- a/icon.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://qsdtdrh44ejn"
-path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://icon.png"
-dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/project.godot b/project.godot
index d6b900d..61448f9 100644
--- a/project.godot
+++ b/project.godot
@@ -8,133 +8,25 @@
config_version=5
-[animation]
-
-compatibility/default_parent_skeleton_in_mesh_instance_3d=true
-
[application]
config/name="agame"
-config/tags=PackedStringArray("3d", "network")
-run/main_scene="res://scenes/level/level.tscn"
config/features=PackedStringArray("4.7", "Forward Plus")
-config/icon="res://icon.png"
-
-[audio]
-
-buses/default_bus_layout="uid://c8prq6lu7oaae"
-driver/enable_input=true
-driver/mix_rate=48000
-
-[autoload]
-
-Network="*res://scripts/autoload/network.gd"
-ItemDatabase="*res://scripts/autoload/item_database.gd"
-_mcp_game_helper="*res://addons/godot_ai/runtime/game_helper.gd"
+config/icon="res://icon.svg"
[display]
-window/size/viewport_width=1280
-window/size/viewport_height=720
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
-[editor_plugins]
+[dotnet]
-enabled=PackedStringArray("res://addons/godot_ai/plugin.cfg", "res://addons/raytraced_audio/plugin.cfg", "res://addons/script-ide/plugin.cfg")
-
-[global_group]
-
-Players="包含全部玩家节点的分组"
-
-[gui]
-
-theme/custom_font="res://assets/fonts/Kurland.ttf"
-
-[input]
-
-move_left={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
-]
-}
-move_right={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
-]
-}
-move_forward={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
-]
-}
-move_backward={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
-]
-}
-jump={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
-]
-}
-shift={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-]
-}
-pause={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-]
-}
-toggle_chat={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":84,"key_label":0,"unicode":116,"location":0,"echo":false,"script":null)
-]
-}
-inventory={
-"deadzone": 0.2,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":73,"key_label":0,"unicode":105,"location":0,"echo":false,"script":null)
-]
-}
-pickup={
-"deadzone": 0.2,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
-]
-}
-attack={
-"deadzone": 0.2,
-"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(53, 16),"global_position":Vector2(62, 64),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
-]
-}
-toggle_camera={
-"deadzone": 0.2,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":86,"key_label":0,"unicode":118,"location":0,"echo":false,"script":null)
-]
-}
-talk={
-"deadzone": 0.2,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null)
-]
-}
-
-[layer_names]
-
-3d_physics/layer_1="player"
-3d_physics/layer_2="world"
-3d/physics_engine="Jolt Physics"
+project/assembly_name="agame"
[physics]
3d/physics_engine="Jolt Physics"
-[raytraced_audio]
-
-reverb_bus=&"RaytracedReverb"
-ambient_bus=&"RaytracedAmbient"
-
[rendering]
rendering_device/driver.windows="d3d12"
-anti_aliasing/quality/msaa_3d=2
diff --git a/run_headless_server.sh b/run_headless_server.sh
deleted file mode 100644
index 3b7d31c..0000000
--- a/run_headless_server.sh
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/usr/bin/env bash
-
-find_godot() {
- if [[ -n "${GODOT_BIN:-}" && -x "$GODOT_BIN" ]]; then
- printf '%s\n' "$GODOT_BIN"
- return 0
- fi
-
- if command -v godot >/dev/null 2>&1; then
- command -v godot
- return 0
- fi
-
- return 1
-}
-
-stop_server() {
- local server_pid="$1"
-
- if kill -0 "$server_pid" 2>/dev/null; then
- echo
- echo "Stopping Dedicated Server..."
- kill "$server_pid" 2>/dev/null || true
-
- for _ in {1..20}; do
- if ! kill -0 "$server_pid" 2>/dev/null; then
- return
- fi
- sleep 0.1
- done
-
- kill -KILL "$server_pid" 2>/dev/null || true
- fi
-}
-
-main() {
- local godot_bin
- if ! godot_bin="$(find_godot)"; then
- echo "Error: Godot 4 was not found." >&2
- echo "Install Godot 4, add it to PATH, or set GODOT_BIN to its executable path." >&2
- return 127
- fi
-
- local project_dir
- project_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
-
- echo "Starting Dedicated Server (Headless Mode) with: $godot_bin"
-
- "$godot_bin" --headless --path "$project_dir" &
- local server_pid=$!
- local stopping=false
-
- trap 'stopping=true; stop_server "$server_pid"' INT TERM EXIT
-
- local exit_code=0
- wait "$server_pid" 2>/dev/null || exit_code=$?
- trap - INT TERM EXIT
-
- if [[ "$stopping" == true ]]; then
- return 0
- fi
- return "$exit_code"
-}
-
-main "$@"
diff --git a/scenes/items/backpacks/backpack.tscn b/scenes/items/backpacks/backpack.tscn
deleted file mode 100644
index a974dd4..0000000
--- a/scenes/items/backpacks/backpack.tscn
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_scene load_steps=5 format=3]
-
-[ext_resource type="PackedScene" path="res://assets/items/backpacks/models/backpack.fbx" id="1_model"]
-[ext_resource type="Script" path="res://scripts/items/item_rigid_body_3d.gd" id="2_script"]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_item"]
-size = Vector3(0.48024, 0.42777, 0.36171)
-
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_item"]
-properties/0/path = NodePath(".:position")
-properties/0/spawn = true
-properties/0/replication_mode = 2
-properties/1/path = NodePath(".:rotation")
-properties/1/spawn = true
-properties/1/replication_mode = 2
-
-[node name="Backpack" type="RigidBody3D"]
-collision_layer = 4
-collision_mask = 7
-linear_damp = 1.0
-angular_damp = 1.0
-script = ExtResource("2_script")
-item_id = "backpack"
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-shape = SubResource("BoxShape3D_item")
-
-[node name="Model" parent="." instance=ExtResource("1_model")]
-transform = Transform3D(0.45, 0, 0, 0, 0.45, 0, 0, 0, 0.45, 0, 0, 0)
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
-replication_config = SubResource("SceneReplicationConfig_item")
diff --git a/scenes/items/hats/fedora.tscn b/scenes/items/hats/fedora.tscn
deleted file mode 100644
index 761fbdb..0000000
--- a/scenes/items/hats/fedora.tscn
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_scene load_steps=5 format=3]
-
-[ext_resource type="PackedScene" path="res://assets/items/hats/models/fedora.glb" id="1_model"]
-[ext_resource type="Script" path="res://scripts/items/item_rigid_body_3d.gd" id="2_script"]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_hat"]
-size = Vector3(0.6525, 0.236667, 0.585665)
-
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_hat"]
-properties/0/path = NodePath(".:position")
-properties/0/spawn = true
-properties/0/replication_mode = 2
-properties/1/path = NodePath(".:rotation")
-properties/1/spawn = true
-properties/1/replication_mode = 2
-
-[node name="Fedora" type="RigidBody3D"]
-collision_layer = 4
-collision_mask = 7
-linear_damp = 1.0
-angular_damp = 1.0
-script = ExtResource("2_script")
-item_id = "fedora"
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-position = Vector3(0, 0.118333, 0)
-shape = SubResource("BoxShape3D_hat")
-
-[node name="Model" parent="." instance=ExtResource("1_model")]
-transform = Transform3D(0.45, 0, 0, 0, 0.45, 0, 0, 0, 0.45, 0, 0, 0)
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
-replication_config = SubResource("SceneReplicationConfig_hat")
diff --git a/scenes/items/hats/headphones.tscn b/scenes/items/hats/headphones.tscn
deleted file mode 100644
index 46793c1..0000000
--- a/scenes/items/hats/headphones.tscn
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_scene load_steps=5 format=3]
-
-[ext_resource type="PackedScene" path="res://assets/items/hats/models/headphones.glb" id="1_model"]
-[ext_resource type="Script" path="res://scripts/items/item_rigid_body_3d.gd" id="2_script"]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_hat"]
-size = Vector3(0.5625, 0.418734, 0.192537)
-
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_hat"]
-properties/0/path = NodePath(".:position")
-properties/0/spawn = true
-properties/0/replication_mode = 2
-properties/1/path = NodePath(".:rotation")
-properties/1/spawn = true
-properties/1/replication_mode = 2
-
-[node name="Headphones" type="RigidBody3D"]
-collision_layer = 4
-collision_mask = 7
-linear_damp = 1.0
-angular_damp = 1.0
-script = ExtResource("2_script")
-item_id = "headphones"
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-shape = SubResource("BoxShape3D_hat")
-
-[node name="Model" parent="." instance=ExtResource("1_model")]
-transform = Transform3D(0.45, 0, 0, 0, 0.45, 0, 0, 0, 0.45, 0, 0, 0)
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
-replication_config = SubResource("SceneReplicationConfig_hat")
diff --git a/scenes/items/hats/pirate_hat.tscn b/scenes/items/hats/pirate_hat.tscn
deleted file mode 100644
index fc0bfdb..0000000
--- a/scenes/items/hats/pirate_hat.tscn
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_scene load_steps=5 format=3]
-
-[ext_resource type="PackedScene" path="res://assets/items/hats/models/pirate_hat.glb" id="1_model"]
-[ext_resource type="Script" path="res://scripts/items/item_rigid_body_3d.gd" id="2_script"]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_hat"]
-size = Vector3(0.563666, 0.434373, 0.6075)
-
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_hat"]
-properties/0/path = NodePath(".:position")
-properties/0/spawn = true
-properties/0/replication_mode = 2
-properties/1/path = NodePath(".:rotation")
-properties/1/spawn = true
-properties/1/replication_mode = 2
-
-[node name="PirateHat" type="RigidBody3D"]
-collision_layer = 4
-collision_mask = 7
-linear_damp = 1.0
-angular_damp = 1.0
-script = ExtResource("2_script")
-item_id = "pirate_hat"
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-position = Vector3(0, 0.217186, 0)
-shape = SubResource("BoxShape3D_hat")
-
-[node name="Model" parent="." instance=ExtResource("1_model")]
-transform = Transform3D(0.45, 0, 0, 0, 0.45, 0, 0, 0, 0.45, 0, 0, 0)
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
-replication_config = SubResource("SceneReplicationConfig_hat")
diff --git a/scenes/items/hats/sheriff_hat.tscn b/scenes/items/hats/sheriff_hat.tscn
deleted file mode 100644
index 37e3c8d..0000000
--- a/scenes/items/hats/sheriff_hat.tscn
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_scene load_steps=5 format=3]
-
-[ext_resource type="PackedScene" path="res://assets/items/hats/models/sheriff_hat.glb" id="1_model"]
-[ext_resource type="Script" path="res://scripts/items/item_rigid_body_3d.gd" id="2_script"]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_hat"]
-size = Vector3(0.608453, 0.271051, 0.675)
-
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_hat"]
-properties/0/path = NodePath(".:position")
-properties/0/spawn = true
-properties/0/replication_mode = 2
-properties/1/path = NodePath(".:rotation")
-properties/1/spawn = true
-properties/1/replication_mode = 2
-
-[node name="SheriffHat" type="RigidBody3D"]
-collision_layer = 4
-collision_mask = 7
-linear_damp = 1.0
-angular_damp = 1.0
-script = ExtResource("2_script")
-item_id = "sheriff_hat"
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-position = Vector3(0, 0.135525, 0)
-shape = SubResource("BoxShape3D_hat")
-
-[node name="Model" parent="." instance=ExtResource("1_model")]
-transform = Transform3D(0.45, 0, 0, 0, 0.45, 0, 0, 0, 0.45, 0, 0, 0)
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
-replication_config = SubResource("SceneReplicationConfig_hat")
diff --git a/scenes/items/hats/sombrero.tscn b/scenes/items/hats/sombrero.tscn
deleted file mode 100644
index d7ee494..0000000
--- a/scenes/items/hats/sombrero.tscn
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_scene load_steps=5 format=3]
-
-[ext_resource type="PackedScene" path="res://assets/items/hats/models/sombrero.glb" id="1_model"]
-[ext_resource type="Script" path="res://scripts/items/item_rigid_body_3d.gd" id="2_script"]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_hat"]
-size = Vector3(0.719778, 0.220565, 0.72)
-
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_hat"]
-properties/0/path = NodePath(".:position")
-properties/0/spawn = true
-properties/0/replication_mode = 2
-properties/1/path = NodePath(".:rotation")
-properties/1/spawn = true
-properties/1/replication_mode = 2
-
-[node name="Sombrero" type="RigidBody3D"]
-collision_layer = 4
-collision_mask = 7
-linear_damp = 1.0
-angular_damp = 1.0
-script = ExtResource("2_script")
-item_id = "sombrero"
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-position = Vector3(0, 0.110282, 0)
-shape = SubResource("BoxShape3D_hat")
-
-[node name="Model" parent="." instance=ExtResource("1_model")]
-transform = Transform3D(0.45, 0, 0, 0, 0.45, 0, 0, 0, 0.45, 0, 0, 0)
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
-replication_config = SubResource("SceneReplicationConfig_hat")
diff --git a/scenes/items/hats/wizard_hat.tscn b/scenes/items/hats/wizard_hat.tscn
deleted file mode 100644
index 34a41d5..0000000
--- a/scenes/items/hats/wizard_hat.tscn
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_scene load_steps=5 format=3]
-
-[ext_resource type="PackedScene" path="res://assets/items/hats/models/wizard_hat.glb" id="1_model"]
-[ext_resource type="Script" path="res://scripts/items/item_rigid_body_3d.gd" id="2_script"]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_hat"]
-size = Vector3(0.582189, 0.382592, 0.585)
-
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_hat"]
-properties/0/path = NodePath(".:position")
-properties/0/spawn = true
-properties/0/replication_mode = 2
-properties/1/path = NodePath(".:rotation")
-properties/1/spawn = true
-properties/1/replication_mode = 2
-
-[node name="WizardHat" type="RigidBody3D"]
-collision_layer = 4
-collision_mask = 7
-linear_damp = 1.0
-angular_damp = 1.0
-script = ExtResource("2_script")
-item_id = "wizard_hat"
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-position = Vector3(0, 0.191296, 0)
-shape = SubResource("BoxShape3D_hat")
-
-[node name="Model" parent="." instance=ExtResource("1_model")]
-transform = Transform3D(0.45, 0, 0, 0, 0.45, 0, 0, 0, 0.45, 0, 0, 0)
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
-replication_config = SubResource("SceneReplicationConfig_hat")
diff --git a/scenes/items/misc/bone.tscn b/scenes/items/misc/bone.tscn
deleted file mode 100644
index ded4a07..0000000
--- a/scenes/items/misc/bone.tscn
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_scene load_steps=5 format=3]
-
-[ext_resource type="PackedScene" path="res://assets/items/misc/models/bone.fbx" id="1_model"]
-[ext_resource type="Script" path="res://scripts/items/item_rigid_body_3d.gd" id="2_script"]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_item"]
-size = Vector3(0.45792, 0.154665, 0.08)
-
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_item"]
-properties/0/path = NodePath(".:position")
-properties/0/spawn = true
-properties/0/replication_mode = 2
-properties/1/path = NodePath(".:rotation")
-properties/1/spawn = true
-properties/1/replication_mode = 2
-
-[node name="Bone" type="RigidBody3D"]
-collision_layer = 4
-collision_mask = 7
-linear_damp = 1.0
-angular_damp = 1.0
-script = ExtResource("2_script")
-item_id = "bone"
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-shape = SubResource("BoxShape3D_item")
-
-[node name="Model" parent="." instance=ExtResource("1_model")]
-transform = Transform3D(0.45, 0, 0, 0, 0.45, 0, 0, 0, 0.45, 0, 0, 0)
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
-replication_config = SubResource("SceneReplicationConfig_item")
diff --git a/scenes/items/misc/chalice.tscn b/scenes/items/misc/chalice.tscn
deleted file mode 100644
index 0defbfd..0000000
--- a/scenes/items/misc/chalice.tscn
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_scene load_steps=5 format=3]
-
-[ext_resource type="PackedScene" path="res://assets/items/misc/models/chalice.fbx" id="1_model"]
-[ext_resource type="Script" path="res://scripts/items/item_rigid_body_3d.gd" id="2_script"]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_item"]
-size = Vector3(0.218565, 0.33606, 0.21852)
-
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_item"]
-properties/0/path = NodePath(".:position")
-properties/0/spawn = true
-properties/0/replication_mode = 2
-properties/1/path = NodePath(".:rotation")
-properties/1/spawn = true
-properties/1/replication_mode = 2
-
-[node name="Chalice" type="RigidBody3D"]
-collision_layer = 4
-collision_mask = 7
-linear_damp = 1.0
-angular_damp = 1.0
-script = ExtResource("2_script")
-item_id = "chalice"
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-position = Vector3(-0.001463, 0.16605, 0)
-shape = SubResource("BoxShape3D_item")
-
-[node name="Model" parent="." instance=ExtResource("1_model")]
-transform = Transform3D(0.45, 0, 0, 0, 0.45, 0, 0, 0, 0.45, 0, 0, 0)
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
-replication_config = SubResource("SceneReplicationConfig_item")
diff --git a/scenes/items/misc/chicken_leg.tscn b/scenes/items/misc/chicken_leg.tscn
deleted file mode 100644
index 49255ea..0000000
--- a/scenes/items/misc/chicken_leg.tscn
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_scene format=3]
-
-[ext_resource type="PackedScene" path="res://assets/items/misc/models/chicken_leg.fbx" id="1_model"]
-[ext_resource type="Script" path="res://scripts/items/item_rigid_body_3d.gd" id="2_script"]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_item"]
-size = Vector3(0.46521, 0.193365, 0.20349)
-
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_item"]
-properties/0/path = NodePath(".:position")
-properties/0/spawn = true
-properties/0/replication_mode = 2
-properties/1/path = NodePath(".:rotation")
-properties/1/spawn = true
-properties/1/replication_mode = 2
-
-[node name="ChickenLeg" type="RigidBody3D"]
-collision_layer = 4
-collision_mask = 7
-linear_damp = 1.0
-angular_damp = 1.0
-script = ExtResource("2_script")
-item_id = "chicken_leg"
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-position = Vector3(-0.02961, -0.001237, 0)
-shape = SubResource("BoxShape3D_item")
-
-[node name="Model" parent="." instance=ExtResource("1_model")]
-transform = Transform3D(0.45, 0, 0, 0, 0.45, 0, 0, 0, 0.45, 0, 0, 0)
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
-replication_config = SubResource("SceneReplicationConfig_item")
diff --git a/scenes/items/weapons/axe.tscn b/scenes/items/weapons/axe.tscn
deleted file mode 100644
index 6868cdd..0000000
--- a/scenes/items/weapons/axe.tscn
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_scene format=3]
-
-[ext_resource type="PackedScene" uid="uid://dghg7ospl0imy" path="res://assets/items/weapons/models/axe.fbx" id="1_model"]
-[ext_resource type="Script" path="res://scripts/items/item_rigid_body_3d.gd" id="2_script"]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_item"]
-size = Vector3(0.31175, 0.78725, 0.08)
-
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_item"]
-properties/0/path = NodePath(".:position")
-properties/0/spawn = true
-properties/0/replication_mode = 2
-properties/1/path = NodePath(".:rotation")
-properties/1/spawn = true
-properties/1/replication_mode = 2
-
-[node name="Axe" type="RigidBody3D"]
-collision_layer = 4
-collision_mask = 7
-linear_damp = 1.0
-angular_damp = 1.0
-script = ExtResource("2_script")
-item_id = "axe"
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-position = Vector3(0.119475, 0.065875, 0)
-shape = SubResource("BoxShape3D_item")
-
-[node name="Model" parent="." instance=ExtResource("1_model")]
-transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, 0, 0)
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
-replication_config = SubResource("SceneReplicationConfig_item")
diff --git a/scenes/items/weapons/sword.tscn b/scenes/items/weapons/sword.tscn
deleted file mode 100644
index e7753a9..0000000
--- a/scenes/items/weapons/sword.tscn
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_scene format=3]
-
-[ext_resource type="PackedScene" path="res://assets/items/weapons/models/sword.fbx" id="1_model"]
-[ext_resource type="Script" path="res://scripts/items/item_rigid_body_3d.gd" id="2_script"]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_item"]
-size = Vector3(0.18704, 0.805735, 0.08)
-
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_item"]
-properties/0/path = NodePath(".:position")
-properties/0/spawn = true
-properties/0/replication_mode = 2
-properties/1/path = NodePath(".:rotation")
-properties/1/spawn = true
-properties/1/replication_mode = 2
-
-[node name="Sword" type="RigidBody3D"]
-collision_layer = 4
-collision_mask = 7
-linear_damp = 1.0
-angular_damp = 1.0
-script = ExtResource("2_script")
-item_id = "sword"
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-position = Vector3(0, 0.268817, 0)
-shape = SubResource("BoxShape3D_item")
-
-[node name="Model" parent="." instance=ExtResource("1_model")]
-transform = Transform3D(0.35, 0, 0, 0, 0.35, 0, 0, 0, 0.35, 0, 0, 0)
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
-replication_config = SubResource("SceneReplicationConfig_item")
diff --git a/scenes/items/weapons/sword_big.tscn b/scenes/items/weapons/sword_big.tscn
deleted file mode 100644
index 7eafda5..0000000
--- a/scenes/items/weapons/sword_big.tscn
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_scene format=3]
-
-[ext_resource type="PackedScene" path="res://assets/items/weapons/models/sword_big.fbx" id="1_model"]
-[ext_resource type="Script" path="res://scripts/items/item_rigid_body_3d.gd" id="2_script"]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_item"]
-size = Vector3(0.12996, 0.82038, 0.08)
-
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_item"]
-properties/0/path = NodePath(".:position")
-properties/0/spawn = true
-properties/0/replication_mode = 2
-properties/1/path = NodePath(".:rotation")
-properties/1/spawn = true
-properties/1/replication_mode = 2
-
-[node name="SwordBig" type="RigidBody3D"]
-collision_layer = 4
-collision_mask = 7
-linear_damp = 1.0
-angular_damp = 1.0
-script = ExtResource("2_script")
-item_id = "sword_big"
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-position = Vector3(0, 0.36504, 0)
-shape = SubResource("BoxShape3D_item")
-
-[node name="Model" parent="." instance=ExtResource("1_model")]
-transform = Transform3D(0.3, 0, 0, 0, 0.3, 0, 0, 0, 0.3, 0, 0, 0)
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
-replication_config = SubResource("SceneReplicationConfig_item")
diff --git a/scenes/level/arena.tscn b/scenes/level/arena.tscn
deleted file mode 100644
index ee9e4f8..0000000
--- a/scenes/level/arena.tscn
+++ /dev/null
@@ -1,279 +0,0 @@
-[gd_scene load_steps=17 format=3]
-
-[ext_resource type="Texture2D" path="res://assets/environment/textures/prototype_dark.png" id="1_floor"]
-[ext_resource type="Texture2D" path="res://assets/environment/textures/prototype_orange.png" id="2_orange"]
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"]
-texture_filter = 5
-albedo_texture = ExtResource("1_floor")
-roughness = 0.85
-uv1_triplanar = true
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_orange"]
-texture_filter = 5
-albedo_texture = ExtResource("2_orange")
-roughness = 0.78
-uv1_triplanar = true
-
-[sub_resource type="BoxMesh" id="BoxMesh_floor"]
-material = SubResource("StandardMaterial3D_floor")
-size = Vector3(100, 0.4, 100)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_floor"]
-size = Vector3(100, 0.4, 100)
-
-[sub_resource type="BoxMesh" id="BoxMesh_boundary"]
-material = SubResource("StandardMaterial3D_floor")
-size = Vector3(100, 3, 1)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_boundary"]
-size = Vector3(100, 3, 1)
-
-[sub_resource type="BoxMesh" id="BoxMesh_diagonal_wall"]
-material = SubResource("StandardMaterial3D_orange")
-size = Vector3(9, 2.5, 1.2)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_diagonal_wall"]
-size = Vector3(9, 2.5, 1.2)
-
-[sub_resource type="BoxMesh" id="BoxMesh_low_cover"]
-material = SubResource("StandardMaterial3D_orange")
-size = Vector3(6, 1.4, 2.4)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_low_cover"]
-size = Vector3(6, 1.4, 2.4)
-
-[sub_resource type="CylinderMesh" id="CylinderMesh_pillar"]
-material = SubResource("StandardMaterial3D_orange")
-top_radius = 1.5
-bottom_radius = 1.5
-height = 4.5
-radial_segments = 20
-
-[sub_resource type="CylinderShape3D" id="CylinderShape3D_pillar"]
-radius = 1.5
-height = 4.5
-
-[sub_resource type="CylinderMesh" id="CylinderMesh_center_pad_outer"]
-material = SubResource("StandardMaterial3D_orange")
-top_radius = 8
-bottom_radius = 8
-height = 0.025
-radial_segments = 48
-
-[sub_resource type="CylinderMesh" id="CylinderMesh_center_pad_inner"]
-material = SubResource("StandardMaterial3D_floor")
-top_radius = 6.7
-bottom_radius = 6.7
-height = 0.035
-radial_segments = 48
-
-[node name="Arena" type="Node3D"]
-
-[node name="Decoration" type="Node3D" parent="."]
-
-[node name="CenterPadOuter" type="MeshInstance3D" parent="Decoration"]
-position = Vector3(0, 0.0125, 0)
-cast_shadow = 0
-mesh = SubResource("CylinderMesh_center_pad_outer")
-
-[node name="CenterPadInner" type="MeshInstance3D" parent="Decoration"]
-position = Vector3(0, 0.03, 0)
-cast_shadow = 0
-mesh = SubResource("CylinderMesh_center_pad_inner")
-
-[node name="Geometry" type="StaticBody3D" parent="."]
-collision_layer = 2
-collision_mask = 0
-
-[node name="GroundMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(0, -0.2, 0)
-mesh = SubResource("BoxMesh_floor")
-
-[node name="GroundCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(0, -0.2, 0)
-shape = SubResource("BoxShape3D_floor")
-
-[node name="BoundaryNorthMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(0, 1.5, -49.5)
-mesh = SubResource("BoxMesh_boundary")
-
-[node name="BoundaryNorthCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(0, 1.5, -49.5)
-shape = SubResource("BoxShape3D_boundary")
-
-[node name="BoundarySouthMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(0, 1.5, 49.5)
-mesh = SubResource("BoxMesh_boundary")
-
-[node name="BoundarySouthCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(0, 1.5, 49.5)
-shape = SubResource("BoxShape3D_boundary")
-
-[node name="BoundaryEastMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(49.5, 1.5, 0)
-rotation = Vector3(0, 1.5707963, 0)
-mesh = SubResource("BoxMesh_boundary")
-
-[node name="BoundaryEastCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(49.5, 1.5, 0)
-rotation = Vector3(0, 1.5707963, 0)
-shape = SubResource("BoxShape3D_boundary")
-
-[node name="BoundaryWestMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(-49.5, 1.5, 0)
-rotation = Vector3(0, 1.5707963, 0)
-mesh = SubResource("BoxMesh_boundary")
-
-[node name="BoundaryWestCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(-49.5, 1.5, 0)
-rotation = Vector3(0, 1.5707963, 0)
-shape = SubResource("BoxShape3D_boundary")
-
-[node name="DiagonalNortheastMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(16, 1.25, -16)
-rotation = Vector3(0, 0.7853982, 0)
-mesh = SubResource("BoxMesh_diagonal_wall")
-
-[node name="DiagonalNortheastCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(16, 1.25, -16)
-rotation = Vector3(0, 0.7853982, 0)
-shape = SubResource("BoxShape3D_diagonal_wall")
-
-[node name="DiagonalNorthwestMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(-16, 1.25, -16)
-rotation = Vector3(0, -0.7853982, 0)
-mesh = SubResource("BoxMesh_diagonal_wall")
-
-[node name="DiagonalNorthwestCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(-16, 1.25, -16)
-rotation = Vector3(0, -0.7853982, 0)
-shape = SubResource("BoxShape3D_diagonal_wall")
-
-[node name="DiagonalSoutheastMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(16, 1.25, 16)
-rotation = Vector3(0, -0.7853982, 0)
-mesh = SubResource("BoxMesh_diagonal_wall")
-
-[node name="DiagonalSoutheastCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(16, 1.25, 16)
-rotation = Vector3(0, -0.7853982, 0)
-shape = SubResource("BoxShape3D_diagonal_wall")
-
-[node name="DiagonalSouthwestMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(-16, 1.25, 16)
-rotation = Vector3(0, 0.7853982, 0)
-mesh = SubResource("BoxMesh_diagonal_wall")
-
-[node name="DiagonalSouthwestCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(-16, 1.25, 16)
-rotation = Vector3(0, 0.7853982, 0)
-shape = SubResource("BoxShape3D_diagonal_wall")
-
-[node name="OuterDiagonalNortheastMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(34, 1.25, -22)
-rotation = Vector3(0, -0.7853982, 0)
-mesh = SubResource("BoxMesh_diagonal_wall")
-
-[node name="OuterDiagonalNortheastCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(34, 1.25, -22)
-rotation = Vector3(0, -0.7853982, 0)
-shape = SubResource("BoxShape3D_diagonal_wall")
-
-[node name="OuterDiagonalNorthwestMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(-34, 1.25, -22)
-rotation = Vector3(0, 0.7853982, 0)
-mesh = SubResource("BoxMesh_diagonal_wall")
-
-[node name="OuterDiagonalNorthwestCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(-34, 1.25, -22)
-rotation = Vector3(0, 0.7853982, 0)
-shape = SubResource("BoxShape3D_diagonal_wall")
-
-[node name="OuterDiagonalSoutheastMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(34, 1.25, 22)
-rotation = Vector3(0, 0.7853982, 0)
-mesh = SubResource("BoxMesh_diagonal_wall")
-
-[node name="OuterDiagonalSoutheastCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(34, 1.25, 22)
-rotation = Vector3(0, 0.7853982, 0)
-shape = SubResource("BoxShape3D_diagonal_wall")
-
-[node name="OuterDiagonalSouthwestMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(-34, 1.25, 22)
-rotation = Vector3(0, -0.7853982, 0)
-mesh = SubResource("BoxMesh_diagonal_wall")
-
-[node name="OuterDiagonalSouthwestCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(-34, 1.25, 22)
-rotation = Vector3(0, -0.7853982, 0)
-shape = SubResource("BoxShape3D_diagonal_wall")
-
-[node name="LowCoverNorthMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(0, 0.7, -32)
-mesh = SubResource("BoxMesh_low_cover")
-
-[node name="LowCoverNorthCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(0, 0.7, -32)
-shape = SubResource("BoxShape3D_low_cover")
-
-[node name="LowCoverSouthMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(0, 0.7, 32)
-mesh = SubResource("BoxMesh_low_cover")
-
-[node name="LowCoverSouthCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(0, 0.7, 32)
-shape = SubResource("BoxShape3D_low_cover")
-
-[node name="LowCoverEastMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(32, 0.7, 0)
-rotation = Vector3(0, 1.5707963, 0)
-mesh = SubResource("BoxMesh_low_cover")
-
-[node name="LowCoverEastCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(32, 0.7, 0)
-rotation = Vector3(0, 1.5707963, 0)
-shape = SubResource("BoxShape3D_low_cover")
-
-[node name="LowCoverWestMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(-32, 0.7, 0)
-rotation = Vector3(0, 1.5707963, 0)
-mesh = SubResource("BoxMesh_low_cover")
-
-[node name="LowCoverWestCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(-32, 0.7, 0)
-rotation = Vector3(0, 1.5707963, 0)
-shape = SubResource("BoxShape3D_low_cover")
-
-[node name="PillarNortheastMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(40, 2.25, -40)
-mesh = SubResource("CylinderMesh_pillar")
-
-[node name="PillarNortheastCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(40, 2.25, -40)
-shape = SubResource("CylinderShape3D_pillar")
-
-[node name="PillarNorthwestMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(-40, 2.25, -40)
-mesh = SubResource("CylinderMesh_pillar")
-
-[node name="PillarNorthwestCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(-40, 2.25, -40)
-shape = SubResource("CylinderShape3D_pillar")
-
-[node name="PillarSoutheastMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(40, 2.25, 40)
-mesh = SubResource("CylinderMesh_pillar")
-
-[node name="PillarSoutheastCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(40, 2.25, 40)
-shape = SubResource("CylinderShape3D_pillar")
-
-[node name="PillarSouthwestMesh" type="MeshInstance3D" parent="Geometry"]
-position = Vector3(-40, 2.25, 40)
-mesh = SubResource("CylinderMesh_pillar")
-
-[node name="PillarSouthwestCollision" type="CollisionShape3D" parent="Geometry"]
-position = Vector3(-40, 2.25, 40)
-shape = SubResource("CylinderShape3D_pillar")
diff --git a/scenes/level/level.tscn b/scenes/level/level.tscn
deleted file mode 100644
index 99beebe..0000000
--- a/scenes/level/level.tscn
+++ /dev/null
@@ -1,88 +0,0 @@
-[gd_scene format=3 uid="uid://dugaivbj1o66n"]
-
-[ext_resource type="Script" uid="uid://d0dgljwwl463n" path="res://scripts/level/level.gd" id="1_e1sh7"]
-[ext_resource type="PackedScene" uid="uid://cffjduipbb3s5" path="res://scenes/level/player.tscn" id="1_uvcbi"]
-[ext_resource type="PackedScene" path="res://scenes/level/arena.tscn" id="2_arena"]
-[ext_resource type="PackedScene" uid="uid://bclq8vh1x2goy" path="res://scenes/ui/inventory_ui.tscn" id="4_x8g2h"]
-[ext_resource type="PackedScene" uid="uid://ccencq0lcd3ph" path="res://scenes/ui/multiplayer_chat_ui.tscn" id="5_x9g3h"]
-[ext_resource type="PackedScene" uid="uid://bx7mq8t8hjn7q" path="res://scenes/ui/main_menu_ui.tscn" id="6_x9g3h"]
-[ext_resource type="PackedScene" path="res://scenes/ui/pause_menu_ui.tscn" id="8_pause"]
-[ext_resource type="PackedScene" path="res://scenes/ui/player_list_ui.tscn" id="9_player_list"]
-
-[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_main"]
-sky_top_color = Color(0.22, 0.46, 0.72, 1)
-sky_horizon_color = Color(0.66, 0.8, 0.92, 1)
-sky_curve = 0.12
-ground_bottom_color = Color(0.12, 0.14, 0.17, 1)
-ground_horizon_color = Color(0.52, 0.61, 0.68, 1)
-ground_curve = 0.08
-sun_angle_max = 20.0
-sun_curve = 0.1
-
-[sub_resource type="Sky" id="Sky_main"]
-sky_material = SubResource("ProceduralSkyMaterial_main")
-
-[sub_resource type="Environment" id="Environment_qb4jd"]
-background_mode = 2
-sky = SubResource("Sky_main")
-ambient_light_source = 3
-reflected_light_source = 2
-tonemap_mode = 2
-ssao_enabled = true
-ssao_radius = 0.7
-ssao_intensity = 0.85
-ssao_power = 1.1
-ssao_detail = 0.35
-fog_enabled = true
-fog_light_color = Color(0.66, 0.76, 0.84, 1)
-fog_light_energy = 0.8
-fog_sun_scatter = 0.08
-fog_density = 0.0025
-fog_aerial_perspective = 0.45
-fog_sky_affect = 0.65
-
-[node name="Level" type="Node3D" unique_id=1359265925]
-script = ExtResource("1_e1sh7")
-player_scene = ExtResource("1_uvcbi")
-
-[node name="ItemMultiplayerSpawner" type="MultiplayerSpawner" parent="." unique_id=150451317]
-unique_name_in_owner = true
-_spawnable_scenes = PackedStringArray("res://scenes/items/hats/fedora.tscn", "res://scenes/items/hats/headphones.tscn", "res://scenes/items/hats/pirate_hat.tscn", "res://scenes/items/hats/sheriff_hat.tscn", "res://scenes/items/hats/sombrero.tscn", "res://scenes/items/hats/wizard_hat.tscn", "res://scenes/items/backpacks/backpack.tscn", "res://scenes/items/weapons/sword.tscn", "res://scenes/items/weapons/sword_big.tscn", "res://scenes/items/weapons/axe.tscn", "res://scenes/items/misc/chicken_leg.tscn", "res://scenes/items/misc/bone.tscn", "res://scenes/items/misc/chalice.tscn")
-spawn_path = NodePath("../Environment/ItemContainer")
-
-[node name="Environment" type="Node3D" parent="." unique_id=523076069]
-unique_name_in_owner = true
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.2542782, 0)
-
-[node name="Arena" parent="Environment" unique_id=318485284 instance=ExtResource("2_arena")]
-
-[node name="WorldEnvironment" type="WorldEnvironment" parent="Environment" unique_id=824936187]
-environment = SubResource("Environment_qb4jd")
-
-[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Environment" unique_id=2096374065]
-transform = Transform3D(1, 0, 0, 0, -0.50000024, 0.8660253, 0, -0.8660253, -0.50000024, 0, 9.597253, 0)
-light_color = Color(1, 0.94, 0.84, 1)
-light_energy = 1.15
-shadow_enabled = true
-shadow_blur = 0.5
-
-[node name="ItemContainer" type="Node3D" parent="Environment" unique_id=1445003613]
-
-[node name="PlayersContainer" type="Node3D" parent="." unique_id=1732952398 groups=["Players"]]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.6891646, 0)
-
-[node name="MainMenuUI" parent="." unique_id=2006763256 instance=ExtResource("6_x9g3h")]
-visible = false
-
-[node name="MultiplayerChatUI" parent="." unique_id=1655363280 instance=ExtResource("5_x9g3h")]
-visible = false
-anchors_preset = -1
-
-[node name="InventoryUI" parent="." unique_id=2001567843 instance=ExtResource("4_x8g2h")]
-visible = false
-
-[node name="PlayerListUI" parent="." unique_id=517544055 instance=ExtResource("9_player_list")]
-visible = false
-
-[node name="PauseMenuUI" parent="." unique_id=358154633 instance=ExtResource("8_pause")]
-visible = false
diff --git a/scenes/level/player.tscn b/scenes/level/player.tscn
deleted file mode 100644
index fb9c9de..0000000
--- a/scenes/level/player.tscn
+++ /dev/null
@@ -1,4912 +0,0 @@
-[gd_scene format=3 uid="uid://cffjduipbb3s5"]
-
-[ext_resource type="Script" uid="uid://c2si8gkbnde0c" path="res://scripts/player/player.gd" id="1_tdh26"]
-[ext_resource type="Script" uid="uid://v8j54rbc0ik" path="res://scripts/player/3d_godot_robot.gd" id="2_mexc5"]
-[ext_resource type="PackedScene" uid="uid://cxuh415268dqx" path="res://assets/characters/zombie.glb" id="3_0potc"]
-[ext_resource type="ArrayMesh" uid="uid://dm4j7r8tq2x5c" path="res://assets/characters/player/meshes/godot_robot_chest.tres" id="3_chest"]
-[ext_resource type="ArrayMesh" uid="uid://c6oak2v5p8s1a" path="res://assets/characters/player/meshes/godot_robot_face.tres" id="3_face"]
-[ext_resource type="ArrayMesh" uid="uid://bt25tokyr00xs" path="res://assets/characters/player/meshes/godot_robot_bottom.tres" id="3_hbshb"]
-[ext_resource type="ArrayMesh" uid="uid://b3f7h1naq4w6e" path="res://assets/characters/player/meshes/godot_robot_limbs_and_head.tres" id="3_limbs"]
-[ext_resource type="AnimationLibrary" uid="uid://dj2grhmw35lxn" path="res://assets/characters/zombie_idle.glb" id="4_fesw4"]
-[ext_resource type="Script" uid="uid://bj7yrijm7bppq" path="res://scripts/player/spring_arm_offset.gd" id="4_ygggd"]
-[ext_resource type="AnimationLibrary" uid="uid://bcj3urpoubxqe" path="res://assets/characters/zombie_jump.glb" id="5_qce6l"]
-[ext_resource type="Script" uid="uid://3i8mgirdlpli" path="res://scripts/player/voice_chat.gd" id="5_voice"]
-[ext_resource type="AnimationLibrary" uid="uid://wncmdkjlpsw6" path="res://assets/characters/zombie_run.glb" id="6_bfcyu"]
-[ext_resource type="PackedScene" uid="uid://difodi4a2r6vc" path="res://assets/items/weapons/models/sword.fbx" id="16_sword"]
-[ext_resource type="FontFile" uid="uid://diapabmalpcrj" path="res://assets/fonts/Kurland.ttf" id="16_xgf8p"]
-[ext_resource type="PackedScene" uid="uid://dj7318sc4o37l" path="res://assets/items/weapons/models/sword_big.fbx" id="17_sword_big"]
-[ext_resource type="PackedScene" uid="uid://cc8gkp6u5tr2e" path="res://assets/items/backpacks/models/backpack.fbx" id="27_backpack"]
-[ext_resource type="PackedScene" uid="uid://dghg7ospl0imy" path="res://assets/items/weapons/models/axe.fbx" id="28_axe"]
-[ext_resource type="PackedScene" uid="uid://i1ar5au3vy7a" path="res://assets/items/hats/models/fedora.glb" id="34_fedora"]
-[ext_resource type="PackedScene" uid="uid://bcfswa3gh780w" path="res://assets/items/hats/models/headphones.glb" id="39_headphones"]
-[ext_resource type="PackedScene" uid="uid://dogjmlb8ci3gr" path="res://assets/items/hats/models/pirate_hat.glb" id="41_pirate"]
-[ext_resource type="PackedScene" uid="uid://crcqxt31qmp7k" path="res://assets/items/hats/models/sheriff_hat.glb" id="42_sheriff"]
-[ext_resource type="PackedScene" uid="uid://b0kmc4gvkiqfw" path="res://assets/items/hats/models/sombrero.glb" id="43_sombrero"]
-[ext_resource type="PackedScene" uid="uid://bry1poskgbmg0" path="res://assets/items/hats/models/wizard_hat.glb" id="45_wizard"]
-
-[sub_resource type="CompressedTexture2D" id="CompressedTexture2D_bkbtv"]
-load_path = "res://.godot/imported/GodotPalette.png-a2d735eec81ad2647941b719336144cd.s3tc.ctex"
-
-[sub_resource type="CompressedTexture2D" id="CompressedTexture2D_hbshb"]
-load_path = "res://.godot/imported/GodotYellowPalette.png-827e66defded795634af82b4a85f17de.ctex"
-
-[sub_resource type="CompressedTexture2D" id="CompressedTexture2D_a6urh"]
-load_path = "res://.godot/imported/GodotGreenPalette.png-8c760c09d8ace0593bb63002a2ed8eae.s3tc.ctex"
-
-[sub_resource type="CompressedTexture2D" id="CompressedTexture2D_xgf8p"]
-load_path = "res://.godot/imported/GodotRedPalette.png-08bd2ae0a0cfe45c50f42f0c3a248d1b.ctex"
-
-[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_mx45w"]
-radius = 0.35796
-height = 1.73092
-
-[sub_resource type="CylinderShape3D" id="CylinderShape3D_2l1sl"]
-height = 1.2
-radius = 0.9
-
-[sub_resource type="Skin" id="Skin_ixatg"]
-resource_name = "Skin"
-bind_count = 25
-bind/0/name = &"spine"
-bind/0/bone = -1
-bind/0/pose = Transform3D(1.7286919, 1.228834e-08, -4.119691e-07, 6.308929e-14, 1.7279234, 0.051541287, 4.121523e-07, -0.051541023, 1.7279234, 6.6867883e-10, -0.90731674, -0.0028046384)
-bind/1/name = &"spine.001"
-bind/1/bone = -1
-bind/1/pose = Transform3D(1.7286919, 3.652057e-08, -4.1053107e-07, 5.5059744e-14, 1.7218918, 0.1531786, 4.1215227e-07, -0.15317835, 1.7218918, -1.5775791e-08, -1.1696228, 0.06616844)
-bind/2/name = &"spine.002"
-bind/2/bone = -1
-bind/2/pose = Transform3D(1.7286919, 7.596554e-09, 4.1208216e-07, 6.423636e-14, 1.7283981, -0.031862002, -4.1215227e-07, 0.031862248, 1.7283981, -1.8464368e-08, -1.3386163, -0.07744517)
-bind/3/name = &"spine.003"
-bind/3/bone = -1
-bind/3/pose = Transform3D(1.7286919, 2.4916505e-15, 4.9075165e-14, 6.770537e-14, 1.7276264, -0.060672753, -4.6784906e-14, 0.060673, 1.7276269, -3.1679212e-15, -1.4752913, -0.10205484)
-bind/4/name = &"spine.004"
-bind/4/bone = -1
-bind/4/pose = Transform3D(1.7286918, 1.0016064e-08, 4.1203037e-07, 6.8824666e-14, 1.7281809, -0.04201018, -4.1215213e-07, 0.04201043, 1.728181, -1.748471e-08, -1.6522585, -0.073336214)
-bind/5/name = &"Head"
-bind/5/bone = -1
-bind/5/pose = Transform3D(1.7286918, 3.2474419e-09, 4.1213943e-07, 6.825112e-14, 1.7286378, -0.013620507, -4.1215233e-07, 0.013620757, 1.7286379, -1.046933e-08, -1.7919029, -0.043911554)
-bind/6/name = &"HeadTop"
-bind/6/bone = -1
-bind/6/pose = Transform3D(1.7286919, 2.2879041e-23, 2.4566188e-14, 6.794085e-14, 1.7286917, 2.5276518e-07, -7.369856e-14, 1.6099694e-09, 1.7286918, 7.518607e-17, -2.4383504, -0.0247)
-bind/7/name = &"Eyes"
-bind/7/bone = -1
-bind/7/pose = Transform3D(1.7286919, 3.8384714e-16, 2.4566188e-14, 6.7557e-14, 1.7286917, 2.205658e-07, -7.369856e-14, 3.3809357e-08, 1.7286918, -5.3182437e-15, -2.3352458, -0.33774993)
-bind/8/name = &"Chin"
-bind/8/bone = -1
-bind/8/pose = Transform3D(1.7286919, 3.8384772e-16, 4.9132376e-14, 6.7557e-14, 1.7286917, 2.157359e-07, -7.369856e-14, 3.7029295e-08, 1.7286918, -1.3146357e-14, -1.8812395, -0.44784912)
-bind/9/name = &"shoulder.L"
-bind/9/bone = -1
-bind/9/pose = Transform3D(0.062363714, 0.0009910803, 1.7275664, 1.7272093, -0.03519252, -0.062330734, 0.035133883, 1.7283329, -0.0022595725, -0.02984303, -0.23477109, -1.5585104)
-bind/10/name = &"upper_arm.L"
-bind/10/bone = -1
-bind/10/pose = Transform3D(-0.04670579, -1.7185075, -0.1814522, 1.7273996, -0.05145183, 0.042660467, -0.0478099, -0.18016367, 1.718613, 1.5551078, -0.4445584, 0.1543937)
-bind/11/name = &"forearm.L"
-bind/11/bone = -1
-bind/11/pose = Transform3D(0.04427163, -1.7228309, 0.1351607, 1.7280729, 0.04308315, -0.016865848, 0.013440024, 0.13554442, 1.7233175, 1.5133405, -0.8085302, -0.15431961)
-bind/12/name = &"hand.L"
-bind/12/bone = -1
-bind/12/pose = Transform3D(-0.016060425, -1.72696, 0.075669736, 1.7276287, -0.013476561, 0.059110943, -0.058461946, 0.07617257, 1.7260231, 1.5526775, -0.9844358, -0.059822656)
-bind/13/name = &"shoulder.R"
-bind/13/bone = -1
-bind/13/pose = Transform3D(0.062363714, -0.0009910803, -1.7275664, -1.7272093, -0.03519252, -0.062330734, -0.035133883, 1.7283329, -0.0022595725, 0.02984303, -0.23477109, -1.5585104)
-bind/14/name = &"upper_arm.R"
-bind/14/bone = -1
-bind/14/pose = Transform3D(-0.04670579, 1.7185075, 0.1814522, -1.7273996, -0.05145183, 0.042660467, 0.0478099, -0.18016367, 1.718613, -1.5551078, -0.4445584, 0.1543937)
-bind/15/name = &"forearm.R"
-bind/15/bone = -1
-bind/15/pose = Transform3D(0.04427163, 1.7228309, -0.1351607, -1.7280729, 0.04308315, -0.016865848, -0.013440024, 0.13554442, 1.7233175, -1.5133405, -0.8085302, -0.15431961)
-bind/16/name = &"hand.R"
-bind/16/bone = -1
-bind/16/pose = Transform3D(-0.016060425, 1.72696, -0.075669736, -1.7276287, -0.013476561, 0.059110943, 0.058461946, 0.07617257, 1.7260231, -1.5526775, -0.9844358, -0.059822656)
-bind/17/name = &"thigh.L"
-bind/17/bone = -1
-bind/17/pose = Transform3D(1.7268807, 0.073044926, -0.016516602, 0.07307894, -1.7271452, 0.0011185404, -0.016725611, -0.0018360749, -1.7285122, -0.31663314, 0.96033895, 0.032057114)
-bind/18/name = &"shin.L"
-bind/18/bone = -1
-bind/18/pose = Transform3D(1.7283901, -0.020132244, 0.0012627903, -0.020114088, -1.7284207, 0.022917511, 0.0007241622, -0.022952422, -1.7284391, -0.28492603, 0.6013631, 0.036419213)
-bind/19/name = &"foot.L"
-bind/19/bone = -1
-bind/19/pose = Transform3D(1.7285073, 2.304878e-06, 1.3418098e-06, 0.00024878836, -0.8692461, 1.4941536, -0.0001103447, -1.4942492, -0.86921436, -0.28799674, 0.10416775, 0.24485752)
-bind/20/name = &"toe.L"
-bind/20/bone = -1
-bind/20/pose = Transform3D(-1.7285072, 3.7426173e-06, 5.044031e-07, 0.00027310083, -0.23088573, 1.7131013, 1.2918732e-05, 1.7132018, 0.23089294, 0.28799596, -0.22982834, -0.1698006)
-bind/21/name = &"thigh.R"
-bind/21/bone = -1
-bind/21/pose = Transform3D(1.7268815, -0.07304493, 0.016516564, -0.07307884, -1.7271451, 0.0011185757, 0.016724084, -0.0018360103, -1.7285125, 0.31663328, 0.960339, 0.032056835)
-bind/22/name = &"shin.R"
-bind/22/bone = -1
-bind/22/pose = Transform3D(1.7283912, 0.020132262, -0.0012627882, 0.020114277, -1.7284206, 0.022917645, -0.0007256448, -0.02295245, -1.7284397, 0.28492624, 0.60136306, 0.03641898)
-bind/23/name = &"foot.R"
-bind/23/bone = -1
-bind/23/pose = Transform3D(1.7285084, -2.2989864e-06, -1.337335e-06, -0.00024743358, -0.86924607, 1.4941543, 0.00010972232, -1.4942495, -0.86921436, 0.28799692, 0.10416796, 0.24485745)
-bind/24/name = &"toe.R"
-bind/24/bone = -1
-bind/24/pose = Transform3D(-1.7285082, -3.7497166e-06, -5.0536016e-07, -0.00027160736, -0.23088583, 1.7131019, -1.2863285e-05, 1.7132021, 0.23089302, -0.28799617, -0.22982818, -0.16980062)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_bkbtv"]
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hbshb"]
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_a6urh"]
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_xgf8p"]
-
-[sub_resource type="Animation" id="Animation_tvij7"]
-resource_name = "Attack1"
-length = 1.2083334
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, -3.5255984e-16, 0.86128783, 0.029855084, 0.2, 1, -3.5255984e-16, 0.86128783, 0.029855084, 0.23333333, 1, -3.3590648e-16, 0.84410876, 0.029855082, 0.33333334, 1, -2.1378197e-16, 0.77539235, 0.029855082, 0.5, 1, 1.7479609e-16, 0.8016382, 0.029855086, 0.7, 1, -2.1378197e-16, 0.7672801, 0.029855086, 0.73333335, 1, -2.1378197e-16, 0.77062047, 0.029855084, 1.2083334, 1, -3.5255984e-16, 0.86128783, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.014909217, 1.1919607e-07, 1.7773131e-09, 0.99988884, 0.033333335, 1, 0.014909217, 1.1919607e-07, 1.7773131e-09, 0.99988884, 0.06666667, 1, 0.0047756433, 1.19207975e-07, 5.6929633e-10, 0.9999887, 0.2, 1, -0.049272265, 1.1906452e-07, -5.873719e-09, 0.9987854, 0.23333333, 1, 0.018507296, 1.191889e-07, 2.2062376e-09, 0.9998288, 0.33333334, 1, 0.30023092, 1.1370979e-07, 3.5790315e-08, 0.9538666, 0.5, 1, 0.22763968, 1.16079526e-07, 2.7136773e-08, 0.97374547, 0.7, 1, 0.29737714, 1.13816306e-07, 3.5450118e-08, 0.95476013, 0.73333335, 1, 0.2865163, 1.1421152e-07, 3.4155406e-08, 0.9580754, 1.1666666, 1, 0.038810037, 1.191195e-07, 4.626512e-09, 0.9992466, 1.2083334, 1, 0.014909217, 1.1919607e-07, 1.7773131e-09, 0.99988884)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.029448895, -1.5216656e-14, 7.0211663e-09, 0.9995663, 0.2, 1, 0.029448895, -1.9813585e-14, 7.021166e-09, 0.9995663, 0.23333333, 1, 0.0058035213, -0.19420053, 0.030944297, 0.9804566, 0.26666668, 1, -0.028077956, -0.4559865, 0.072657764, 0.88657135, 0.3, 1, -0.06066618, -0.6876699, 0.10957463, 0.7151386, 0.33333334, 1, -0.08577213, -0.8472078, 0.13499571, 0.5066146, 0.36666667, 1, -0.06556299, -0.92642725, 0.12618037, 0.3485866, 0.4, 1, -0.03940362, -0.98156834, 0.110860765, 0.15060204, 0.43333334, 1, -0.009162456, -0.9934607, 0.08887518, -0.07108546, 0.46666667, 1, -0.02186778, 0.95432234, -0.062082566, 0.29143852, 0.5, 1, -0.049800497, 0.87328833, -0.03418774, 0.48344454, 0.53333336, 1, -0.055606972, 0.7596389, -0.023595551, 0.6475337, 0.56666666, 1, -0.06017243, 0.5230635, -0.005306892, 0.85015047, 0.6, 1, -0.057374515, 0.14466786, 0.018895805, 0.9876347, 0.6333333, 1, -0.044123113, -0.28373557, 0.04125583, 0.95699805, 0.6666667, 1, -0.027247125, -0.5954951, 0.053911906, 0.8010847, 0.7, 1, -0.015975729, -0.74297786, 0.05825529, 0.6665846, 0.73333335, 1, -0.015454127, -0.80587876, 0.061299365, 0.5886961, 0.76666665, 1, -0.019184925, -0.8498127, 0.06464119, 0.52275425, 0.8, 1, -0.023754524, -0.89794064, 0.068302035, 0.43413508, 0.8333333, 1, -0.029224982, -0.94597304, 0.07195564, 0.31480706, 0.8666667, 1, -0.03609294, -0.98689294, 0.07506822, 0.138219, 0.9, 1, -0.04298363, -0.99230975, 0.07548026, -0.08818463, 0.93333334, 1, 0.04814022, 0.9362522, -0.07121623, 0.34065026, 0.96666664, 1, 0.05008327, 0.810956, -0.06168556, 0.579687, 1, 1, 0.04859135, 0.639845, -0.048669968, 0.76542056, 1.0333333, 1, 0.04531861, 0.47977257, -0.036494017, 0.87546146, 1.0666667, 1, 0.04153795, 0.3410506, -0.02594209, 0.9387685, 1.1, 1, 0.037926126, 0.22762947, -0.017314676, 0.972855, 1.1333333, 1, 0.0347636, 0.13764541, -0.010470011, 0.98981595, 1.1666666, 1, 0.032093946, 0.0666712, -0.0050713485, 0.99724585, 1.2, 1, 0.029988851, 0.013343775, -0.0010149895, 0.99946064, 1.2083334, 1, 0.029448895, -1.5216656e-14, 7.0211663e-09, 0.9995663)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -3.6572172e-16, 0.15150139, -0.011741015, 0.2, 1, -6.760607e-15, 0.1515015, -0.011740994, 0.23333333, 1, 0.004143388, 0.17172183, -0.013456344, 0.33333334, 1, 0.02071693, 0.25260323, -0.020317677, 0.5, 1, 0.015312495, 0.2262288, -0.018080276, 0.7, 1, 0.015312441, 0.22622885, -0.018080384, 0.73333335, 1, 0.014546825, 0.22249253, -0.017763402, 1.2083334, 1, -3.6572172e-16, 0.15150139, -0.011741015)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, 0.00540027, -1.1920752e-07, -6.4376265e-10, 0.9999854, 0.033333335, 1, 0.00540027, -1.1920752e-07, -6.4376265e-10, 0.9999854, 0.06666667, 1, 0.020537116, -1.1918412e-07, -2.4482152e-09, 0.9997891, 0.2, 1, 0.10116695, -1.18597626e-07, -1.206004e-08, 0.9948695, 0.23333333, 1, 0.077152334, 0.19426781, 0.02147392, 0.977674, 0.26666668, 1, 0.030839512, 0.45610684, 0.050417, 0.8879605, 0.3, 1, -0.019495336, 0.68809056, 0.07605993, 0.7213642, 0.33333334, 1, -0.06380537, 0.8484105, 0.093781315, 0.51704323, 0.36666667, 1, -0.05608356, 0.9261713, 0.12809804, 0.35021776, 0.4, 1, -0.044414517, 0.97605836, 0.16284207, 0.13718607, 0.43333334, 1, -0.029249141, 0.97573566, 0.19150579, -0.10202888, 0.46666667, 1, 0.012282562, -0.9180798, -0.20907332, 0.33655187, 0.5, 1, -0.0039219605, -0.8176368, -0.21436751, 0.53432333, 0.53333336, 1, -0.042938363, -0.6785766, -0.18567175, 0.7093773, 0.56666666, 1, -0.08630909, -0.4673785, -0.13898236, 0.86878765, 0.6, 1, -0.11416239, -0.17137188, -0.0722234, 0.9759009, 0.6333333, 1, -0.11739433, 0.19811723, 0.013609711, 0.97302777, 0.6666667, 1, -0.100173645, 0.5864144, 0.10958983, 0.7962873, 0.7, 1, -0.08975473, 0.72411346, 0.16007297, 0.6648161, 0.73333335, 1, -0.07938585, 0.78742915, 0.17829484, 0.5846915, 0.76666665, 1, -0.06760649, 0.834535, 0.18740186, 0.5136742, 0.8, 1, -0.052068785, 0.8849187, 0.19686535, 0.4188697, 0.8333333, 1, -0.031666316, 0.9333426, 0.20544489, 0.29267967, 0.8666667, 1, -0.0030295935, 0.9709883, 0.21090974, 0.11264925, 0.9, 1, 0.031362414, 0.971699, 0.20781994, -0.1078352, 0.93333334, 1, -0.045831397, -0.90768003, -0.19248797, 0.37008756, 0.96666664, 1, -0.036784317, -0.76219475, -0.16163549, 0.62576354, 1, 1, -0.02614268, -0.5786991, -0.12272232, 0.8058307, 1.0333333, 1, -0.017459502, -0.42368782, -0.089849696, 0.90117204, 1.0666667, 1, -0.01045111, -0.29600623, -0.062772796, 0.95306385, 1.1, 1, -0.0050052647, -0.19540292, -0.041438263, 0.97983456, 1.1333333, 1, -0.0008258647, -0.11741534, -0.024899758, 0.99277043, 1.1666666, 1, 0.0024046157, -0.056678332, -0.012019508, 0.99831724, 1.2, 1, 0.0048022745, -0.011341845, -0.002405192, 0.99992126, 1.2083334, 1, 0.00540027, -1.1920752e-07, -6.4376265e-10, 0.9999854)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.008212382, -4.9427408e-14, -1.9579842e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.444091e-08, 0.5406087, 0.3122278)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.97907e-08, 0.06115723, 0.4185525)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939635, 1.1920836e-07, -4.696411e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732144, 0.0764982, -0.028223984)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.5230665, -0.45780188, -0.52192616, 0.49438047)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435951, 0.22304288, -8.365756e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, -0.35644147, 0.617847, -0.43310678, 0.5510291, 0.033333335, 1, -0.35644147, 0.617847, -0.43310678, 0.5510291, 0.06666667, 1, -0.29833958, 0.6479036, -0.38080388, 0.5883902, 0.1, 1, -0.21490303, 0.6801487, -0.3041675, 0.6314243, 0.16666667, 1, -0.034747303, 0.7124452, -0.13347234, 0.68804044, 0.2, 1, 0.052443337, 0.7113615, -0.048521854, 0.6991854, 0.23333333, 1, 0.08761721, 0.70789033, -0.013827167, 0.7007306, 0.33333334, 1, 0.14106724, 0.69920343, 0.039372135, 0.6997604, 0.36666667, 1, 0.11648315, 0.70371664, 0.014830945, 0.7007102, 0.4, 1, 0.09120551, 0.70743686, -0.010273812, 0.7007918, 0.43333334, 1, 0.065295786, 0.710297, -0.035872962, 0.6999485, 0.46666667, 1, 0.03882398, 0.71223456, -0.061889693, 0.69812924, 0.5, 1, 0.01186835, 0.71319324, -0.088240005, 0.6952901, 0.53333336, 1, 0.03347871, 0.7125059, -0.067126274, 0.6976451, 0.56666666, 1, 0.0547932, 0.71118426, -0.046211563, 0.6993419, 0.6, 1, 0.07576993, 0.7092562, -0.025540497, 0.7004016, 0.6333333, 1, 0.096370175, 0.7067519, -0.0051549594, 0.7008481, 0.6666667, 1, 0.11655882, 0.7037041, 0.014906297, 0.7007085, 0.7, 1, 0.13617839, 0.700172, 0.034481842, 0.7000184, 0.73333335, 1, 0.117411196, 0.7035624, 0.015755108, 0.70069003, 0.76666665, 1, 0.08508339, 0.70819926, -0.016334658, 0.70067674, 0.8, 1, 0.05174073, 0.7114129, -0.0492123, 0.6991372, 0.8333333, 1, 0.017531179, 0.7130765, -0.08271616, 0.69596887, 0.8666667, 1, -0.01751228, 0.71307695, -0.1167984, 0.69106644, 0.96666664, 1, -0.123956636, 0.7024387, -0.21883449, 0.66582733, 1.0333333, 1, -0.19360468, 0.6865149, -0.28434405, 0.64059585, 1.0666667, 1, -0.22721913, 0.6761339, -0.31558403, 0.62579656, 1.1, 1, -0.2597832, 0.6643028, -0.34559956, 0.60973394, 1.1333333, 1, -0.29114303, 0.6511692, -0.37426168, 0.592573, 1.1666666, 1, -0.32116923, 0.63689554, -0.40146714, 0.5744899, 1.2083334, 1, -0.35644147, 0.617847, -0.43310678, 0.5510291)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.017577726, -0.09135095, 0.027113229, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.020934898, 0.017021658, -0.017671391, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732144, 0.0764982, -0.028223984)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.5230665, 0.45780188, 0.52192616, 0.49438047)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435951, 0.22304288, -8.365756e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, -0.35644147, -0.617847, 0.43310678, 0.5510291, 0.033333335, 1, -0.35644147, -0.617847, 0.43310678, 0.5510291, 0.06666667, 1, -0.29547748, -0.6492138, 0.37820363, 0.5900649, 0.1, 1, -0.20753406, -0.68243325, 0.29732034, 0.6346772, 0.16666667, 1, -0.017169118, -0.71308535, 0.11646593, 0.6911224, 0.2, 1, 0.07414377, -0.7094281, 0.027146207, 0.7003411, 0.23333333, 1, 0.0844755, -0.7082721, 0.016936129, 0.7006623, 0.33333334, 1, 0.034779396, -0.7124436, 0.06585262, 0.69776636, 0.36666667, 1, 0.0583898, -0.71089816, 0.042673666, 0.6995666, 0.4, 1, 0.0822804, -0.70853055, 0.019107176, 0.70060647, 0.43333334, 1, 0.10638365, -0.70531416, -0.004784899, 0.70085067, 0.5, 1, 0.15494132, -0.6962605, -0.05327727, 0.69883907, 0.56666666, 1, 0.11605667, -0.7037872, -0.014406213, 0.7007189, 0.6333333, 1, 0.07744853, -0.70907503, 0.02388265, 0.7004599, 0.6666667, 1, 0.058340948, -0.7109022, 0.042721737, 0.6995636, 0.7, 1, 0.0394956, -0.7121978, 0.061231405, 0.69818705, 0.73333335, 1, 0.0152388755, -0.7131293, 0.08495304, 0.6956993, 0.76666665, 1, -0.011128709, -0.7132052, 0.11060794, 0.6920841, 0.8, 1, -0.03790327, -0.71228427, 0.13651915, 0.68744236, 0.8333333, 1, -0.0649959, -0.71032465, 0.16259442, 0.681746, 0.96666664, 1, -0.17442463, -0.6916369, 0.2664068, 0.6482607, 1.0666667, 1, -0.25389588, -0.6665752, 0.3401917, 0.61276764, 1.1, 1, -0.27926874, -0.65634936, 0.36343786, 0.59927243, 1.1333333, 1, -0.30394408, -0.64529335, 0.38588947, 0.58506733, 1.1666666, 1, -0.3278515, -0.63348156, 0.4074885, 0.5702347, 1.2083334, 1, -0.35644147, -0.617847, 0.43310678, 0.5510291)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577725, 0.09135095, -0.027113236, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934897, -0.017021656, 0.017671384, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.276084, 0.06599521, -0.0034686895)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.9914148, 0.020339562, -0.007486248, 0.12894534, 0.033333335, 1, 0.9914148, 0.020339562, -0.007486248, 0.12894534, 0.06666667, 1, 0.9931716, 0.02044594, -0.007191766, 0.114632376, 0.2, 1, 0.9990461, 0.02094128, -0.0055965125, 0.037910976, 0.23333333, 1, 0.99972737, 0.021096954, -0.00498121, 0.008691377, 0.26666668, 1, -0.9994794, -0.021249251, 0.004290139, 0.023895647, 0.33333334, 1, -0.9958006, -0.021485956, 0.0028955908, 0.088945396, 0.5, 1, -0.9826467, -0.021668633, 0.00081807753, 0.18421566, 0.7, 1, -0.99545234, -0.021497048, 0.0028132305, 0.09276099, 0.73333335, 1, -0.99670744, -0.021452835, 0.0031287393, 0.07813125, 0.9, 1, -0.99975014, -0.02116567, 0.0046813055, 0.005449229, 0.93333334, 1, 0.99972314, 0.02109445, -0.004990365, 0.009160178, 1.2083334, 1, 0.9914148, 0.020339562, -0.007486248, 0.12894534)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.24962775, 0.008717708, 0.024974002, 0.96798056)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.6087138, 0.0033154194, -0.0047912123, 0.7933685)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608398, 0.06599527, -0.0034688246)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.9914148, -0.020339629, 0.007486256, 0.12894534, 0.033333335, 1, 0.9914148, -0.020339629, 0.007486256, 0.12894534, 0.06666667, 1, 0.9857682, -0.020037396, 0.008257418, 0.16670787, 0.2, 1, 0.9312824, -0.01793191, 0.012160991, 0.36365318, 0.23333333, 1, 0.90107095, -0.016956255, 0.01348312, 0.4331305, 0.33333334, 1, 0.7636817, -0.013077729, 0.01724994, 0.64523005, 0.36666667, 1, 0.7817801, -0.01356946, 0.016870987, 0.62317824, 0.4, 1, 0.7994808, -0.01405702, 0.01647273, 0.6003013, 0.43333334, 1, 0.816723, -0.014539038, 0.01605539, 0.5766233, 0.5, 1, 0.8495969, -0.015481034, 0.015164937, 0.5269871, 0.56666666, 1, 0.8234693, -0.01472972, 0.015883418, 0.5669473, 0.6333333, 1, 0.79595095, -0.013959199, 0.016554484, 0.60497385, 0.6666667, 1, 0.7817455, -0.013568482, 0.01687174, 0.62322176, 0.7, 1, 0.7673445, -0.013176741, 0.01717541, 0.6408697, 0.73333335, 1, 0.7810838, -0.013550464, 0.016886074, 0.62405074, 0.76666665, 1, 0.8036201, -0.014172103, 0.016375314, 0.5947485, 0.8, 1, 0.82539254, -0.014784354, 0.015833436, 0.5641436, 0.8333333, 1, 0.8462786, -0.015384379, 0.015261196, 0.5322997, 1.0666667, 1, 0.9583863, -0.018886501, 0.01061505, 0.28465134, 1.1, 1, 0.9685987, -0.019281622, 0.009883409, 0.24768357, 1.1333333, 1, 0.97729206, -0.019644288, 0.00914607, 0.210786, 1.1666666, 1, 0.98448944, -0.019974299, 0.008406139, 0.17410062, 1.2083334, 1, 0.9914148, -0.020339629, 0.007486256, 0.12894534)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, 0.24962777, -0.008717655, -0.024974115, 0.9679805, 0.2, 1, 0.24962775, -0.008717656, -0.024974115, 0.9679805, 0.23333333, 1, 0.30763415, -0.009519653, -0.02426058, 0.9511478, 0.33333334, 1, 0.52726597, -0.012363693, -0.020533213, 0.84936225, 0.5, 1, 0.4588906, -0.011514221, -0.021884957, 0.8881486, 0.6666667, 1, 0.5139405, -0.012201006, -0.020811887, 0.85748655, 0.7, 1, 0.5246111, -0.012331396, -0.020589354, 0.85100377, 0.73333335, 1, 0.5144816, -0.0122076385, -0.020800726, 0.85716224, 1.1666666, 1, 0.27371925, -0.009053078, -0.024690181, 0.9614501, 1.2083334, 1, 0.24962777, -0.008717655, -0.024974115, 0.9679805)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.6087138, -0.0033154439, 0.004791257, 0.7933685, 0.2, 1, -0.6087138, -0.0033154439, 0.004791257, 0.7933685, 0.23333333, 1, -0.6126465, -0.00333936, 0.004774613, 0.79033566, 0.33333334, 1, -0.6282255, -0.003434196, 0.0047068596, 0.7780095, 0.36666667, 1, -0.62721723, -0.0034280536, 0.004711335, 0.77882266, 0.4, 1, -0.62620765, -0.0034219036, 0.0047158017, 0.77963454, 0.46666667, 1, -0.6241854, -0.0034095868, 0.0047247135, 0.7812546, 0.5, 1, -0.62317264, -0.0034034199, 0.0047291573, 0.78206265, 0.56666666, 1, -0.62479246, -0.0034132844, 0.004722043, 0.78076917, 0.6, 1, -0.6256015, -0.0034182116, 0.004718479, 0.78012115, 0.6333333, 1, -0.6264097, -0.0034231343, 0.0047149095, 0.7794723, 0.6666667, 1, -0.6272172, -0.0034280533, 0.0047113346, 0.7788226, 0.7, 1, -0.6280239, -0.003432968, 0.004707755, 0.77817225, 0.73333335, 1, -0.6272592, -0.0034283095, 0.004711149, 0.77878875, 0.76666665, 1, -0.6259692, -0.0034204514, 0.004716856, 0.77982605, 0.8333333, 1, -0.6233837, -0.003404705, 0.004728232, 0.7818944, 0.9, 1, -0.6207914, -0.0033889215, 0.0047395574, 0.78395414, 1, 1, -0.61689, -0.0033651763, 0.0047564483, 0.78702784, 1.0333333, 1, -0.61558616, -0.0033572426, 0.004762052, 0.78804815, 1.0666667, 1, -0.6142807, -0.003349301, 0.0047676433, 0.7890662, 1.1, 1, -0.6129735, -0.003341349, 0.0047732205, 0.790082, 1.1666666, 1, -0.6103542, -0.0033254186, 0.0047843363, 0.7921072, 1.2, 1, -0.6090421, -0.00331744, 0.004789875, 0.7931165, 1.2083334, 1, -0.6087138, -0.0033154439, 0.004791257, 0.7933685)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.345526e-08, -0.98076427, 0.19519576, 1.8277876e-06)
-
-[sub_resource type="Animation" id="Animation_u3ehh"]
-resource_name = "Crouch"
-length = 0.001
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, -8.7991577e-16, 0.2727054, 0.029855082)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.11576636, 1.1840781e-07, 1.3800421e-08, 0.9932765)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.029448887, -1.005882e-14, 7.0211645e-09, 0.9995663)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -2.8642387e-16, 0.24077763, -0.010776748)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, 0.005400267, -1.1920752e-07, -6.4376315e-10, 0.9999854)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.42158678, -3.9179096e-14, -1.0051409e-07, 0.9067881)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.44409e-08, 0.5406087, 0.31222773)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.979069e-08, 0.06115741, 0.41855246)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939635, 1.1920836e-07, -4.696411e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.2673215, 0.0764982, -0.028223976)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.26588887, -0.21744573, -0.6894263, 0.63773966)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435912, 0.22304296, -8.365366e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, 0.43178263, 0.56775814, 0.34050336, 0.6125944)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.017577725, -0.09135095, 0.027113233, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.24138683, -0.15772206, 0.32360753, 0.901185)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.2673215, 0.0764982, -0.028223976)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.26588887, 0.21744573, 0.6894263, 0.63773966)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435912, 0.22304296, -8.365366e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, 0.43178263, -0.56775814, -0.34050336, 0.6125944)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577725, 0.09135095, -0.027113233, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.24138686, 0.15772204, -0.32360753, 0.901185)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.27608398, 0.06599524, -0.0034686623)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.3386138, 0.15598227, -0.1232318, 0.9196871)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.95794934, 0.016111892, 0.0034331626, 0.2864641)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.6421059, 0.0035225437, -0.0046419622, 0.7665939)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608398, 0.06599533, -0.0034688022)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.33861378, -0.15598226, 0.12323183, 0.919687)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, 0.95794934, -0.016111897, -0.0034332296, 0.28646424)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.64210594, -0.003522557, 0.0046420232, 0.7665938)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.2334564e-08, -0.98076427, 0.19519576, 1.8298152e-06)
-
-[sub_resource type="Animation" id="Animation_qx7mb"]
-resource_name = "Dive"
-length = 0.46666667
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, -3.5255984e-16, 0.86128783, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.72261816, 8.2403155e-08, 8.614283e-08, 0.69124746)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.0022589967, -7.424636e-15, 5.3858834e-10, 0.99999744)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, 2.8055988e-14, 0.15150145, -0.011741022)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, -0.2699511, -1.1478348e-07, 3.218068e-08, 0.96287405)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, -0.43278387, -6.8965775e-14, 1.031837e-07, 0.9014977)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.444091e-08, 0.5406087, 0.3122278)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.979073e-08, 0.06115747, 0.41855264)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939635, 1.1920836e-07, -4.696411e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732144, 0.0764982, -0.028223986)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, 0.5710791, 0.55685353, -0.43909425, 0.41349623)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435972, 0.22304294, -8.363007e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, -0.35644144, 0.6178471, -0.43310678, 0.5510291)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.017577734, -0.09135095, 0.027113242, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.0209349, 0.017021654, -0.017671386, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732144, 0.0764982, -0.028223986)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, 0.5710791, -0.55685353, 0.43909425, 0.41349623)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435972, 0.22304294, -8.363007e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, -0.35644144, -0.617847, 0.43310678, 0.5510291)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577723, 0.09135095, -0.027113244, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.0209349, -0.01702166, 0.017671384, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.276084, 0.065995224, -0.0034686788)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.9966472, 0.068862714, 0.0023796745, 0.044121984)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.0996995, 0.0077853147, 0.026304739, 0.9946394)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.14418001, 0.0005871008, -0.0058200182, 0.9895342)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.276084, 0.06599521, -0.003468812)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.9946539, -0.068902455, -0.000110543915, 0.07691637)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, -0.0061842757, -0.0049533797, -0.026989246, 0.99960434)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.09268992, -0.00028483433, 0.0058466312, 0.9956778)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.108305e-08, -0.9807643, 0.19519575, 1.8271913e-06)
-
-[sub_resource type="Animation" id="Animation_65yf6"]
-resource_name = "Emote1"
-length = 1.9166666
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, -3.5255984e-16, 0.86128783, 0.029855084, 0.5, 1, -8.795539e-12, 0.86252505, 0.029855082, 0.53333336, 1, -1.681828e-11, 0.8636537, 0.029855084, 0.56666666, 1, -2.6578559e-11, 0.86502683, 0.029855084, 0.6, 1, -3.7819192e-11, 0.86660814, 0.029855084, 0.6333333, 1, -5.0282733e-11, 0.8683614, 0.029855084, 0.6666667, 1, -6.371164e-11, 0.8702506, 0.029855084, 0.76666665, 1, -1.06636755e-10, 0.8762891, 0.029855084, 0.8, 1, -1.2064467e-10, 0.8782598, 0.029855084, 0.8333333, 1, -1.3407359e-10, 0.8801489, 0.029855084, 0.8666667, 1, -1.4621498e-10, 0.8818569, 0.029855084, 0.9, 1, -1.5700492e-10, 0.8833748, 0.029855084, 0.93333334, 1, -1.6618604e-10, 0.88466644, 0.029855084, 0.96666664, 1, -1.7350098e-10, 0.8856955, 0.029855084, 1.1666666, 1, 1.666139e-11, 0.88440627, 0.029855084, 1.2, 1, 2.5809278e-11, 0.8831194, 0.029855084, 1.2333333, 1, 3.6149e-11, 0.8816648, 0.029855082, 1.2666667, 1, 4.745249e-11, 0.8800747, 0.029855082, 1.3, 1, 5.949112e-11, 0.87838113, 0.029855084, 1.4, 1, 9.737959e-11, 0.87305105, 0.029855084, 1.4333333, 1, 1.0955295e-10, 0.87133855, 0.029855084, 1.4666667, 1, 1.2108872e-10, 0.86971575, 0.029855084, 1.5, 1, 1.3175823e-10, 0.8682148, 0.029855084, 1.5333333, 1, 1.4091216e-10, 0.86692697, 0.029855084, 1.5666667, 1, 1.489502e-10, 0.8657963, 0.029855084, 1.6333333, 1, 1.619231e-10, 0.8639713, 0.029855082, 1.7, 1, 1.7092355e-10, 0.8627051, 0.029855084, 1.8, 1, 1.7852263e-10, 0.8616361, 0.029855084, 1.9166666, 1, -3.5255984e-16, 0.86128783, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.014909217, 1.1919607e-07, 1.7773131e-09, 0.99988884, 0.46666667, 1, 0.01490921, 1.4860219e-05, 0.0009886171, 0.99988836, 0.5, 1, 0.014909177, 3.609509e-05, 0.0024127453, 0.99988604, 0.53333336, 1, 0.014909063, 6.8915724e-05, 0.004613881, 0.9998783, 0.56666666, 1, 0.014908823, 0.00010884794, 0.0072919577, 0.9998623, 0.6, 1, 0.014908416, 0.00015483909, 0.010376383, 0.9998351, 0.6333333, 1, 0.014907799, 0.00020583556, 0.013796492, 0.9997937, 0.6666667, 1, 0.01490694, 0.00026078336, 0.017481599, 0.9997361, 0.7, 1, 0.014905814, 0.0003188912, 0.021378636, 0.9996604, 0.76666665, 1, 0.014902838, 0.0004364096, 0.029260077, 0.9994607, 0.8, 1, 0.014901049, 0.00049370946, 0.033102926, 0.9993408, 0.8333333, 1, 0.01489913, 0.0005486304, 0.036786232, 0.999212, 0.8666667, 1, 0.014897221, 0.00059827586, 0.04011574, 0.9990839, 0.9, 1, 0.014895382, 0.00064238586, 0.043074008, 0.9989607, 0.93333334, 1, 0.014893717, 0.00067991123, 0.04559067, 0.99884903, 0.96666664, 1, 0.014892322, 0.00070980313, 0.047595378, 0.99875546, 1, 1, 0.014891297, 0.00073101337, 0.049017843, 0.9986866, 1.1, 1, 0.0148916915, 0.0007227043, 0.04846059, 0.99871385, 1.1333333, 1, 0.01489271, 0.0007013684, 0.04702969, 0.9987823, 1.1666666, 1, 0.014894052, 0.0006723532, 0.045083776, 0.99887204, 1.2, 1, 0.01489569, 0.0006349641, 0.04257625, 0.9989821, 1.2333333, 1, 0.01489743, 0.0005926928, 0.039741304, 0.9990988, 1.2666667, 1, 0.014899196, 0.0005464732, 0.036641553, 0.9992173, 1.3, 1, 0.014900922, 0.0004972396, 0.033339668, 0.9993329, 1.3333334, 1, 0.014902543, 0.00044592694, 0.029898364, 0.99944174, 1.3666667, 1, 0.014904012, 0.00039371717, 0.02639688, 0.9995404, 1.4, 1, 0.014905283, 0.00034223893, 0.022944462, 0.9996256, 1.4333333, 1, 0.014906343, 0.00029242985, 0.019603986, 0.99969673, 1.4666667, 1, 0.014907194, 0.00024522722, 0.016438317, 0.9997538, 1.5, 1, 0.014907843, 0.00020156857, 0.013510322, 0.9997976, 1.5333333, 1, 0.014908305, 0.00016411261, 0.010998318, 0.9998284, 1.5666667, 1, 0.0149086295, 0.00013122612, 0.008792765, 0.9998502, 1.6, 1, 0.014908856, 0.00010265615, 0.0068767047, 0.9998653, 1.6333333, 1, 0.014909002, 7.814941e-05, 0.005233144, 0.9998752, 1.6666666, 1, 0.014909099, 5.745254e-05, 0.0038450973, 0.99988157, 1.7, 1, 0.014909152, 4.1328916e-05, 0.0027637575, 0.9998851, 1.7666667, 1, 0.014909197, 1.7978498e-05, 0.0011977487, 0.9998882, 1.8666667, 1, 0.014909206, 1.9083104e-06, 0.00011999377, 0.99988884, 1.9166666, 1, 0.014909217, 1.1919607e-07, 1.7773131e-09, 0.99988884)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.029448895, -1.5216656e-14, 7.0211663e-09, 0.9995663, 0.43333334, 1, 0.029448893, 4.164648e-05, 0.00070181093, 0.99956614, 0.46666667, 1, 0.029448789, 0.00016273769, 0.0027423736, 0.9995626, 0.5, 1, 0.029448235, 0.000397222, 0.006693773, 0.9995439, 0.53333336, 1, 0.029446471, 0.00075987476, 0.012805, 0.99948406, 0.56666666, 1, 0.029442836, 0.0012013623, 0.020244712, 0.9993607, 0.6, 1, 0.029436622, 0.0017100783, 0.02881732, 0.99914974, 0.6333333, 1, 0.029427186, 0.0022743507, 0.038326137, 0.99882936, 0.6666667, 1, 0.029414011, 0.002882454, 0.048573557, 0.9983823, 0.7, 1, 0.029396696, 0.0035255402, 0.05941049, 0.9977945, 0.76666665, 1, 0.029351044, 0.0048250845, 0.08130968, 0.99624497, 0.8, 1, 0.029323637, 0.00545788, 0.09197319, 0.9953147, 0.8333333, 1, 0.029294208, 0.0060636746, 0.10218175, 0.9943159, 0.8666667, 1, 0.029264962, 0.006610416, 0.11139515, 0.9933232, 0.9, 1, 0.02923688, 0.0070954924, 0.119569376, 0.9923699, 0.93333334, 1, 0.029211437, 0.007507582, 0.12651367, 0.99150634, 0.96666664, 1, 0.029190147, 0.007835422, 0.13203827, 0.9907838, 1, 1, 0.029174501, 0.0080677895, 0.13595392, 0.99025273, 1.0333333, 1, 0.0291688, 0.008150296, 0.1373443, 0.99006027, 1.0666667, 1, 0.02917115, 0.008116129, 0.1367686, 0.99014026, 1.1, 1, 0.029180648, 0.007976773, 0.13442022, 0.99046266, 1.1333333, 1, 0.02919621, 0.0077429553, 0.13048, 0.9909907, 1.1666666, 1, 0.029216643, 0.0074246754, 0.12511659, 0.9916841, 1.2, 1, 0.029241703, 0.0070139496, 0.11819528, 0.992535, 1.2333333, 1, 0.029268343, 0.006548956, 0.11035947, 0.9934392, 1.2666667, 1, 0.02929539, 0.006039844, 0.10178019, 0.99435717, 1.3, 1, 0.029321803, 0.005496846, 0.09262986, 0.9952537, 1.3333334, 1, 0.029346688, 0.0049302685, 0.08308219, 0.9960984, 1.3666667, 1, 0.029369233, 0.0043532057, 0.073357865, 0.99686366, 1.4, 1, 0.029388718, 0.003783856, 0.063763484, 0.99752504, 1.4333333, 1, 0.029404974, 0.0032327222, 0.054476075, 0.9980768, 1.4666667, 1, 0.029418021, 0.0027103028, 0.045672562, 0.9985196, 1.5, 1, 0.029428042, 0.0022270908, 0.037529733, 0.99885964, 1.5333333, 1, 0.029435074, 0.0018126378, 0.030545602, 0.9990983, 1.5666667, 1, 0.029440053, 0.0014488267, 0.024414858, 0.99926734, 1.6, 1, 0.029443476, 0.0011328578, 0.019090319, 0.99938357, 1.6333333, 1, 0.029445741, 0.00086191343, 0.014524513, 0.9994605, 1.6666666, 1, 0.029447176, 0.00063316966, 0.010669845, 0.9995092, 1.7, 1, 0.029447993, 0.00045504145, 0.0076681217, 0.9995369, 1.7333333, 1, 0.029448451, 0.0003106416, 0.0052347737, 0.9995526, 1.7666667, 1, 0.029448695, 0.00019715687, 0.003322389, 0.99956083, 1.8, 1, 0.029448811, 0.00011177737, 0.0018836182, 0.9995646, 1.8333334, 1, 0.029448844, 5.1696366e-05, 0.0008711733, 0.9995659, 1.9166666, 1, 0.029448895, -1.5216656e-14, 7.0211663e-09, 0.9995663)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -3.6572172e-16, 0.15150139, -0.011741015)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, 0.00540027, -1.1920752e-07, -6.4376265e-10, 0.9999854, 0.43333334, 1, 0.0054002698, -1.3575101e-05, 0.00045285598, 0.99998534, 0.46666667, 1, 0.0054002623, -5.269448e-05, 0.0017694145, 0.9999839, 0.5, 1, 0.00540022, -0.00012843656, 0.004318506, 0.9999761, 0.53333336, 1, 0.0054000854, -0.0002455283, 0.008259218, 0.9999513, 0.56666666, 1, 0.0053998088, -0.00038801858, 0.0130547155, 0.9999002, 0.6, 1, 0.0053993347, -0.0005521542, 0.018578686, 0.99981266, 0.6333333, 1, 0.0053986176, -0.0007341734, 0.024704522, 0.99968004, 0.6666667, 1, 0.005397618, -0.00093030743, 0.03130539, 0.99949485, 0.7, 1, 0.0053963037, -0.0011377223, 0.038285915, 0.9992516, 0.76666665, 1, 0.00539284, -0.0015570915, 0.052399747, 0.9986105, 0.8, 1, 0.005390759, -0.0017614798, 0.059278414, 0.99822545, 0.8333333, 1, 0.0053885253, -0.0019573069, 0.065868944, 0.9978119, 0.8666667, 1, 0.005386305, -0.0021342314, 0.071823314, 0.99740064, 0.9, 1, 0.005384169, -0.0022913541, 0.07711128, 0.9970054, 0.93333334, 1, 0.005382233, -0.0024249612, 0.081607826, 0.9966471, 0.96666664, 1, 0.005380613, -0.002531345, 0.085188165, 0.9963471, 1, 1, 0.005379421, -0.0026068033, 0.08772771, 0.99612653, 1.0666667, 1, 0.0053791893, -0.00262251, 0.08825632, 0.9960798, 1.1, 1, 0.005379913, -0.002577245, 0.08673292, 0.9962138, 1.1333333, 1, 0.0053810976, -0.0025013303, 0.084178, 0.9964331, 1.1666666, 1, 0.0053826505, -0.0023980602, 0.08070247, 0.99672085, 1.2, 1, 0.005384558, -0.002264925, 0.076221816, 0.99707377, 1.2333333, 1, 0.005386583, -0.0021143372, 0.07115379, 0.99744856, 1.2666667, 1, 0.0053886385, -0.0019496109, 0.06560995, 0.99782896, 1.3, 1, 0.0053906427, -0.00177407, 0.059702132, 0.9982002, 1.3333334, 1, 0.005392531, -0.0015910474, 0.05354253, 0.9985498, 1.3666667, 1, 0.005394241, -0.001404765, 0.04727322, 0.9988665, 1.4, 1, 0.0053957207, -0.0012210532, 0.04109041, 0.99914014, 1.4333333, 1, 0.005396955, -0.0010432722, 0.035107207, 0.9993685, 1.4666667, 1, 0.005397945, -0.0008747812, 0.029436653, 0.9995518, 1.5, 1, 0.0053987075, -0.0007189389, 0.024191804, 0.9996925, 1.5333333, 1, 0.0053992416, -0.0005852487, 0.019692477, 0.9997914, 1.5666667, 1, 0.00539962, -0.00046787638, 0.015742324, 0.9998615, 1.6, 1, 0.005399881, -0.00036591938, 0.012310969, 0.99990964, 1.6333333, 1, 0.0054000537, -0.00027847206, 0.009367934, 0.9999416, 1.6666666, 1, 0.0054001627, -0.00020462775, 0.006882714, 0.99996173, 1.7, 1, 0.005400226, -0.00014710784, 0.004946887, 0.9999732, 1.7333333, 1, 0.005400262, -0.00010047214, 0.003377366, 0.9999798, 1.7666667, 1, 0.00540028, -6.3815605e-05, 0.0021436946, 0.9999832, 1.8333334, 1, 0.00540029, -1.6822898e-05, 0.00056216045, 0.9999853, 1.9166666, 1, 0.00540027, -1.1920752e-07, -6.4376265e-10, 0.9999854)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.008212382, -4.9427408e-14, -1.9579842e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.444091e-08, 0.5406087, 0.3122278, 0.06666667, 1, 7.444091e-08, 0.5406087, 0.3122278, 0.1, 1, 7.444291e-08, 0.5416716, 0.31223616, 0.13333334, 1, 7.4448536e-08, 0.54466677, 0.31225976, 0.16666667, 1, 7.445907e-08, 0.55027056, 0.31230393, 0.2, 1, 7.4473945e-08, 0.5581931, 0.31236634, 0.23333333, 1, 7.4490465e-08, 0.5669853, 0.31243563, 0.26666668, 1, 7.450753e-08, 0.5760673, 0.3125072, 0.3, 1, 7.452405e-08, 0.58485943, 0.3125765, 0.33333334, 1, 7.453893e-08, 0.59278214, 0.3126389, 0.36666667, 1, 7.454946e-08, 0.59838593, 0.31268308, 0.4, 1, 7.455509e-08, 0.601381, 0.31270668, 0.43333334, 1, 7.492961e-08, 0.60244375, 0.31271505, 1.5, 1, 8.945824e-08, 0.602444, 0.31271502, 1.5333333, 1, 1.01376564e-07, 0.6010593, 0.31270412, 1.5666667, 1, 7.752871e-08, 0.5978931, 0.31267917, 1.6, 1, 6.8579325e-08, 0.59324294, 0.3126425, 1.6333333, 1, 7.974423e-08, 0.58740574, 0.3125965, 1.6666666, 1, 7.07909e-08, 0.58067775, 0.3125435, 1.7333333, 1, 7.821397e-08, 0.5660355, 0.31242815, 1.7666667, 1, 7.708319e-08, 0.559011, 0.3123728, 1.8, 1, 7.5487854e-08, 0.5525799, 0.3123221, 1.8333334, 1, 7.5849975e-08, 0.5470396, 0.31227845, 1.8666667, 1, 7.47719e-08, 0.5432801, 0.31224883, 1.9, 1, 7.44655e-08, 0.5413013, 0.31223324, 1.9166666, 1, 7.444091e-08, 0.5406087, 0.3122278)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225, 0.06666667, 1, 0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225, 0.1, 1, 0.0072130496, 1.1920619e-07, -8.598626e-10, 0.999974, 0.13333334, 1, 0.016448319, 1.1919318e-07, -1.9607926e-09, 0.9998648, 0.16666667, 1, 0.03374563, 1.191414e-07, -4.022793e-09, 0.9994305, 0.2, 1, 0.058262624, 1.190068e-07, -6.9454473e-09, 0.9983014, 0.23333333, 1, 0.08547559, 1.1877301e-07, -1.0189485e-08, 0.9963403, 0.26666668, 1, 0.11353271, 1.1843851e-07, -1.3534155e-08, 0.9935343, 0.3, 1, 0.14059508, 1.180252e-07, -1.6760243e-08, 0.99006724, 0.33333334, 1, 0.16485006, 1.1757834e-07, -1.965166e-08, 0.98631865, 0.36666667, 1, 0.18189083, 1.17220736e-07, -2.1683077e-08, 0.9833188, 0.4, 1, 0.1909648, 1.1701546e-07, -2.276478e-08, 0.9815969, 0.43333334, 1, 0.194177, 1.1694069e-07, -2.324257e-08, 0.98096657, 1.5, 1, 0.19417702, 1.1694032e-07, -2.3147704e-08, 0.98096657, 1.5333333, 1, 0.18999131, 1.1627386e-07, -2.1135415e-08, 0.9817858, 1.5666667, 1, 0.18039632, 1.17434155e-07, -1.9611734e-08, 0.98359406, 1.6, 1, 0.1662478, 1.1792616e-07, -1.9059282e-08, 0.9860841, 1.6333333, 1, 0.14840658, 1.1756309e-07, -1.8634042e-08, 0.98892653, 1.6666666, 1, 0.12774707, 1.18470915e-07, -1.6166254e-08, 0.9918068, 1.7333333, 1, 0.08253418, 1.18721516e-07, -1.0026305e-08, 0.99658823, 1.7666667, 1, 0.06079359, 1.18948705e-07, -7.4337603e-09, 0.99815047, 1.8, 1, 0.040891092, 1.1909945e-07, -5.259142e-09, 0.9991636, 1.8333334, 1, 0.023764854, 1.1917586e-07, -2.89121e-09, 0.9997176, 1.8666667, 1, 0.012169952, 1.19200536e-07, -1.4624155e-09, 0.99992603, 1.9, 1, 0.0060721664, 1.192071e-07, -7.2385864e-10, 0.9999816, 1.9166666, 1, 0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.97907e-08, 0.06115723, 0.4185525)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939635, 1.1920836e-07, -4.696411e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732144, 0.0764982, -0.028223984)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.5230665, -0.45780188, -0.52192616, 0.49438047)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435951, 0.22304288, -8.365756e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, -0.35644147, 0.617847, -0.43310678, 0.5510291, 0.4, 1, -0.35644147, 0.617847, -0.43310678, 0.5510291, 0.43333334, 1, -0.35386992, 0.6197127, -0.43036795, 0.5527347, 0.46666667, 1, -0.3462601, 0.6251296, -0.4222566, 0.5576884, 0.5, 1, -0.3311579, 0.63543016, -0.4061317, 0.56711555, 0.53333336, 1, -0.3062561, 0.6511694, -0.3794676, 0.58154106, 0.56666666, 1, -0.27372432, 0.6695429, -0.34450006, 0.59842044, 0.6, 1, -0.23345345, 0.689106, -0.30101985, 0.6164572, 0.6333333, 1, -0.18559942, 0.7081217, -0.24909404, 0.63408893, 0.6666667, 1, -0.13067912, 0.72468054, -0.18917948, 0.6495939, 0.7, 1, -0.069249704, 0.73692566, -0.12178102, 0.6612976, 0.76666665, 1, 0.057368953, 0.7421912, 0.018359732, 0.6674758, 0.8, 1, 0.11757325, 0.7352929, 0.08556709, 0.66196615, 0.8333333, 1, 0.17274867, 0.7234405, 0.147498, 0.651948, 0.8666667, 1, 0.21897429, 0.7092134, 0.19964552, 0.639694, 0.9, 1, 0.25738472, 0.69420964, 0.2431708, 0.6266531, 0.93333334, 1, 0.28798085, 0.6800447, 0.27797633, 0.6142763, 0.96666664, 1, 0.31089014, 0.66806614, 0.3041211, 0.603776, 1, 1, 0.32628855, 0.65931594, 0.3217369, 0.5960904, 1.0333333, 1, 0.33157265, 0.6561786, 0.3277901, 0.59333205, 1.0666667, 1, 0.32939047, 0.65748274, 0.3252898, 0.5944788, 1.1, 1, 0.32029176, 0.6627923, 0.31487232, 0.59914523, 1.1333333, 1, 0.30455366, 0.67150134, 0.29688224, 0.6067899, 1.1666666, 1, 0.2822192, 0.68286884, 0.27141237, 0.6167478, 1.2, 1, 0.25126895, 0.69680065, 0.23622835, 0.62891096, 1.2333333, 1, 0.21384971, 0.7109919, 0.19385219, 0.6412334, 1.2666667, 1, 0.17029563, 0.72408366, 0.14473754, 0.65249777, 1.3, 1, 0.121263325, 0.73466814, 0.089698754, 0.661447, 1.3333334, 1, 0.06777316, 0.7414381, 0.029947406, 0.666918, 1.3666667, 1, 0.01151249, 0.7433584, -0.03258147, 0.6680002, 1.4, 1, -0.04418566, 0.74008375, -0.094169386, 0.6644215, 1.4333333, 1, -0.09739966, 0.7321169, -0.15271525, 0.65665543, 1.4666667, 1, -0.14646213, 0.72047734, -0.20643164, 0.64563715, 1.5, 1, -0.19002044, 0.70654887, -0.25390244, 0.63262516, 1.5333333, 1, -0.22524948, 0.69268316, -0.2921372, 0.6197649, 1.5666667, 1, -0.25471196, 0.6792015, -0.3239983, 0.60731566, 1.6, 1, -0.27910262, 0.666669, -0.35029107, 0.59577703, 1.6333333, 1, -0.2990746, 0.6554329, -0.37176102, 0.5854536, 1.6666666, 1, -0.31522706, 0.64567274, -0.389084, 0.57650006, 1.7, 1, -0.3272857, 0.63797754, -0.40199152, 0.5694485, 1.7333333, 1, -0.33680585, 0.6316467, -0.4121663, 0.5636517, 1.7666667, 1, -0.34411842, 0.6266264, -0.41997206, 0.5590576, 1.8, 1, -0.34951633, 0.62283075, -0.42572856, 0.5555857, 1.8333334, 1, -0.35325888, 0.6201535, -0.42971697, 0.5531376, 1.8666667, 1, -0.3552285, 0.61872935, -0.43181512, 0.5518356, 1.9, 1, -0.35615307, 0.6180572, -0.4327997, 0.5512212, 1.9166666, 1, -0.35644147, 0.617847, -0.43310678, 0.5510291)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.017577726, -0.09135095, 0.027113229, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.020934898, 0.017021658, -0.017671391, 0.9994798, 0.4, 1, 0.020934898, 0.017021658, -0.017671391, 0.9994798, 0.43333334, 1, 0.02100327, 0.017035583, -0.016559042, 0.9994972, 0.46666667, 1, 0.021201985, 0.017075956, -0.013323577, 0.99954057, 0.5, 1, 0.021586288, 0.017153658, -0.0070556677, 0.9995949, 0.53333336, 1, 0.022179697, 0.017272644, 0.0026506246, 0.9996013, 0.56666666, 1, 0.022900168, 0.017415466, 0.014481287, 0.9994812, 0.6, 1, 0.023727393, 0.017577205, 0.028128073, 0.99916816, 0.6333333, 1, 0.024640923, 0.017752983, 0.043278266, 0.99860144, 0.6666667, 1, 0.025620295, 0.017938059, 0.05961501, 0.9977314, 0.7, 1, 0.026649898, 0.018128794, 0.07689732, 0.99651796, 0.76666665, 1, 0.028708186, 0.018497936, 0.111787796, 0.9931451, 0.8, 1, 0.029698657, 0.018669607, 0.12874511, 0.9910571, 0.8333333, 1, 0.030639045, 0.018828886, 0.14494912, 0.9887854, 0.8666667, 1, 0.031480502, 0.018968262, 0.15953675, 0.98650765, 0.9, 1, 0.032221235, 0.019088436, 0.17244895, 0.98430634, 0.93333334, 1, 0.03284608, 0.019187935, 0.18339364, 0.9823034, 0.96666664, 1, 0.03334015, 0.019265367, 0.19208252, 0.98062307, 1, 1, 0.03368863, 0.01931932, 0.1982297, 0.979386, 1.0333333, 1, 0.033812005, 0.019338284, 0.20040993, 0.97893757, 1.0666667, 1, 0.03376095, 0.019330446, 0.19950728, 0.9791239, 1.1, 1, 0.033552278, 0.019298276, 0.19582257, 0.9798752, 1.1333333, 1, 0.033201072, 0.019243686, 0.18963346, 0.98110485, 1.1666666, 1, 0.032720827, 0.019168127, 0.18119591, 0.9827156, 1.2, 1, 0.032097183, 0.019068474, 0.17028187, 0.9846879, 1.2333333, 1, 0.031386215, 0.01895279, 0.15789804, 0.9867745, 1.2666667, 1, 0.030602075, 0.01882269, 0.14431012, 0.98888016, 1.3, 1, 0.02975941, 0.018680006, 0.12978882, 0.99091893, 1.3333334, 1, 0.028873475, 0.018526856, 0.11460991, 0.99281806, 1.3666667, 1, 0.027964432, 0.018366449, 0.09912686, 0.99451226, 1.4, 1, 0.027061466, 0.018203916, 0.083837174, 0.9959456, 1.4333333, 1, 0.026181966, 0.018042602, 0.069029026, 0.99710786, 1.4666667, 1, 0.025343636, 0.01788613, 0.054990053, 0.99800503, 1.5, 1, 0.024564458, 0.017738381, 0.042006947, 0.99865776, 1.5333333, 1, 0.02389364, 0.017609414, 0.030878913, 0.9990823, 1.5666667, 1, 0.023302877, 0.017494505, 0.021116301, 0.99935234, 1.6, 1, 0.022788431, 0.017393436, 0.012643119, 0.9995091, 1.6333333, 1, 0.022346342, 0.017305842, 0.0053825313, 0.999586, 1.6666666, 1, 0.021972476, 0.017231232, -0.00074273837, 0.9996098, 1.7, 1, 0.021681001, 0.017172728, -0.005508782, 0.9996023, 1.7333333, 1, 0.021444457, 0.017125038, -0.009370553, 0.9995794, 1.7666667, 1, 0.021258406, 0.017087394, -0.012404228, 0.99955106, 1.8, 1, 0.02111835, 0.017058978, -0.014685731, 0.99952364, 1.8333334, 1, 0.021019757, 0.017038936, -0.016290713, 0.9995011, 1.9, 1, 0.020942621, 0.01702323, -0.017545767, 0.9994818, 1.9166666, 1, 0.020934898, 0.017021658, -0.017671391, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732144, 0.0764982, -0.028223984)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.5230665, 0.45780188, 0.52192616, 0.49438047)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435951, 0.22304288, -8.365756e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, -0.35644147, -0.617847, 0.43310678, 0.5510291, 0.43333334, 1, -0.35588554, -0.61825186, 0.43251473, 0.55139923, 0.46666667, 1, -0.35426676, -0.619426, 0.43079063, 0.55247253, 0.5, 1, -0.35112327, -0.62168574, 0.42744136, 0.5545387, 0.53333336, 1, -0.34623677, -0.62514603, 0.42223167, 0.5577034, 0.56666666, 1, -0.34024894, -0.62930024, 0.41584265, 0.5615041, 0.6, 1, -0.33329776, -0.6340062, 0.4084186, 0.56581175, 0.6333333, 1, -0.32552448, -0.6391238, 0.40010768, 0.5704985, 0.6666667, 1, -0.31707454, -0.6445168, 0.39106297, 0.5754404, 0.7, 1, -0.30805787, -0.65008074, 0.3814, 0.58054245, 0.76666665, 1, -0.28960013, -0.66087544, 0.3615828, 0.59045184, 0.8, 1, -0.28050238, -0.6659107, 0.35179752, 0.5950798, 0.8333333, 1, -0.27172843, -0.6705933, 0.34235004, 0.59938705, 0.8666667, 1, -0.26375967, -0.67470133, 0.33376068, 0.60316896, 0.9, 1, -0.25665012, -0.67825204, 0.32609043, 0.60644037, 0.93333334, 1, -0.25058195, -0.6811986, 0.3195386, 0.6091571, 0.96666664, 1, -0.24573645, -0.6834964, 0.31430355, 0.611277, 1, 1, -0.24229285, -0.6851001, 0.3105813, 0.61275715, 1.0333333, 1, -0.24106836, -0.68566453, 0.3092574, 0.6132782, 1.0666667, 1, -0.24157554, -0.6854312, 0.30980572, 0.61306274, 1.1, 1, -0.24364269, -0.68447435, 0.3120405, 0.61217964, 1.1333333, 1, -0.24710466, -0.6828525, 0.31578207, 0.6106828, 1.1666666, 1, -0.25180405, -0.68061125, 0.32085854, 0.6086155, 1.2, 1, -0.2578473, -0.6776616, 0.32738253, 0.60589635, 1.2333333, 1, -0.26465797, -0.674245, 0.3347294, 0.60274875, 1.2666667, 1, -0.27207538, -0.6704112, 0.34272385, 0.59921956, 1.3, 1, -0.27993962, -0.6662161, 0.3511919, 0.5953606, 1.3333334, 1, -0.28809223, -0.6617229, 0.35996166, 0.5912304, 1.3666667, 1, -0.29633805, -0.65702593, 0.36882246, 0.5869162, 1.4, 1, -0.30441394, -0.65227455, 0.37749147, 0.58255506, 1.4333333, 1, -0.31217325, -0.6475654, 0.3858118, 0.57823545, 1.4666667, 1, -0.319474, -0.64300317, 0.39363238, 0.57405305, 1.5, 1, -0.3261786, -0.63869894, 0.40080744, 0.5701093, 1.5333333, 1, -0.3318905, -0.634944, 0.4069147, 0.56667036, 1.5666667, 1, -0.33687505, -0.6315999, 0.4122402, 0.5636089, 1.6, 1, -0.3411814, -0.62865937, 0.41683796, 0.56091774, 1.6333333, 1, -0.344857, -0.62611157, 0.42076004, 0.5585867, 1.6666666, 1, -0.34794748, -0.6239418, 0.424056, 0.556602, 1.7, 1, -0.35034615, -0.62224025, 0.426613, 0.55504584, 1.7333333, 1, -0.35228541, -0.62085354, 0.42867973, 0.5537777, 1.7666667, 1, -0.35380623, -0.61975884, 0.43030012, 0.55277675, 1.8, 1, -0.35494855, -0.61893237, 0.43151686, 0.5520212, 1.8333334, 1, -0.35575137, -0.6183495, 0.4323719, 0.5514884, 1.9, 1, -0.35637867, -0.6178928, 0.43303996, 0.5510709, 1.9166666, 1, -0.35644147, -0.617847, 0.43310678, 0.5510291)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577725, 0.09135095, -0.027113236, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934897, -0.017021656, 0.017671384, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.276084, 0.06599521, -0.0034686895)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.9914148, 0.020339562, -0.007486248, 0.12894534, 0.46666667, 1, 0.9915057, 0.01956544, -0.0074991835, 0.12836489, 0.5, 1, 0.99163485, 0.01845026, -0.007517807, 0.12752852, 0.53333336, 1, 0.99183065, 0.016726565, -0.00754656, 0.12623526, 0.56666666, 1, 0.9920626, 0.014629254, -0.007581495, 0.12466098, 0.6, 1, 0.99232125, 0.012213548, -0.007621666, 0.12284675, 0.6333333, 1, 0.99259746, 0.009534724, -0.0076661278, 0.120833695, 0.6666667, 1, 0.99288243, 0.00664808, -0.0077139367, 0.118663035, 0.7, 1, 0.99316967, 0.0035951445, -0.0077643907, 0.11636573, 0.76666665, 1, 0.993706, -0.0025801186, -0.007866087, 0.111713745, 0.8, 1, 0.9939459, -0.0055915383, -0.007915508, 0.109442696, 0.8333333, 1, 0.99416256, -0.008478209, -0.007962776, 0.10726418, 0.8666667, 1, 0.99434716, -0.011087855, -0.008005418, 0.10529346, 0.9, 1, 0.9945023, -0.013406734, -0.008043237, 0.1035413, 0.93333334, 1, 0.9946277, -0.015379598, -0.008075361, 0.10204983, 0.96666664, 1, 0.9947232, -0.016951233, -0.0081009185, 0.1008612, 1, 1, 0.9947886, -0.018066464, -0.008119036, 0.10001746, 1.1, 1, 0.99476314, -0.017629564, -0.00811194, 0.10034801, 1.1333333, 1, 0.9946966, -0.016507745, -0.00809371, 0.10119663, 1.1666666, 1, 0.9946029, -0.014982209, -0.008068897, 0.102350295, 1.2, 1, 0.9944768, -0.013016547, -0.008036879, 0.1038362, 1.2333333, 1, 0.99432695, -0.010794365, -0.008000625, 0.105515145, 1.2666667, 1, 0.99415433, -0.008364814, -0.00796092, 0.107349776, 1.3, 1, 0.9939602, -0.0057770647, -0.007918549, 0.10930272, 1.3333334, 1, 0.9937468, -0.0030802884, -0.0078743035, 0.11133665, 1.3666667, 1, 0.993518, -0.0003366017, -0.007829195, 0.11340461, 1.4, 1, 0.99328095, 0.0023683894, -0.007784631, 0.11544212, 1.4333333, 1, 0.9930407, 0.0049854433, -0.00774143, 0.11741216, 1.4666667, 1, 0.99280304, 0.007465347, -0.007700412, 0.119277775, 1.5, 1, 0.99257475, 0.009758886, -0.0076624104, 0.12100218, 1.5333333, 1, 0.9923723, 0.011726437, -0.007629757, 0.12248081, 1.5666667, 1, 0.99218965, 0.013453857, -0.0076010493, 0.12377838, 1.6, 1, 0.99202704, 0.014954466, -0.007576079, 0.124905124, 1.6333333, 1, 0.9918849, 0.016241608, -0.007554641, 0.12587129, 1.6666666, 1, 0.9917628, 0.017328605, -0.0075365203, 0.12668702, 1.7, 1, 0.9916664, 0.018175391, -0.0075223944, 0.1273223, 1.7666667, 1, 0.9915248, 0.019401679, -0.0075019207, 0.12824209, 1.8666667, 1, 0.99142593, 0.020245608, -0.0074878186, 0.12887491, 1.9166666, 1, 0.9914148, 0.020339562, -0.007486248, 0.12894534)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.24962775, 0.008717708, 0.024974002, 0.96798056, 0.43333334, 1, 0.24916592, 0.008692585, 0.024979748, 0.9680996, 0.46666667, 1, 0.24782296, 0.008619535, 0.024996424, 0.96844447, 0.5, 1, 0.24522129, 0.008478051, 0.025028579, 0.969107, 0.53333336, 1, 0.24119556, 0.008259191, 0.025077928, 0.97011733, 0.56666666, 1, 0.23629063, 0.007992647, 0.025137402, 0.9713244, 0.6, 1, 0.23063257, 0.007685328, 0.025205119, 0.972684, 0.6333333, 1, 0.22434808, 0.007344174, 0.025279228, 0.9741535, 0.6666667, 1, 0.21756439, 0.006976152, 0.02535791, 0.97569156, 0.7, 1, 0.2103774, 0.0065865032, 0.025439803, 0.977267, 0.76666665, 1, 0.19580531, 0.005797263, 0.025601223, 0.9802915, 0.8, 1, 0.18868361, 0.0054119327, 0.025677871, 0.9816873, 0.8333333, 1, 0.18184827, 0.005042337, 0.025750063, 0.9829765, 0.8666667, 1, 0.1756628, 0.0047080778, 0.02581424, 0.9841007, 0.9, 1, 0.1701617, 0.0044109607, 0.025870394, 0.98506665, 0.93333334, 1, 0.16547813, 0.004158116, 0.025917526, 0.9858641, 0.96666664, 1, 0.16174506, 0.0039566616, 0.025954649, 0.9864833, 1, 1, 0.15909518, 0.0038137021, 0.025980756, 0.986914, 1.0666667, 1, 0.15854338, 0.003783938, 0.025986169, 0.98700273, 1.1, 1, 0.16013342, 0.0038697096, 0.02597055, 0.98674613, 1.1333333, 1, 0.16279866, 0.0040135104, 0.025944212, 0.98631, 1.1666666, 1, 0.16642153, 0.0042090365, 0.025908083, 0.9857053, 1.2, 1, 0.17108758, 0.004460954, 0.025861004, 0.9849063, 1.2333333, 1, 0.17635882, 0.0047456785, 0.025807071, 0.9839762, 1.2666667, 1, 0.18211716, 0.0050568706, 0.025747254, 0.9829267, 1.3, 1, 0.18824448, 0.0053881835, 0.02568255, 0.9817716, 1.3333334, 1, 0.19462296, 0.0057332744, 0.025614046, 0.9805269, 1.3666667, 1, 0.20110452, 0.0060841525, 0.025543232, 0.9792178, 1.4, 1, 0.20748618, 0.006429826, 0.025472315, 0.9778851, 1.4333333, 1, 0.21365184, 0.0067639956, 0.02540268, 0.9765562, 1.4666667, 1, 0.2194861, 0.0070803836, 0.02533576, 0.975261, 1.5, 1, 0.22487445, 0.0073727435, 0.025273066, 0.97403204, 1.5333333, 1, 0.22949082, 0.0076233298, 0.02521867, 0.9729543, 1.5666667, 1, 0.23353894, 0.007843165, 0.025170455, 0.97199, 1.6, 1, 0.23705187, 0.008034003, 0.025128221, 0.9711388, 1.6333333, 1, 0.24006219, 0.008197588, 0.025091732, 0.9703986, 1.6666666, 1, 0.24260233, 0.008335657, 0.02506074, 0.96976626, 1.7, 1, 0.24457958, 0.00844316, 0.025036475, 0.96926916, 1.7333333, 1, 0.24618205, 0.008530295, 0.025016729, 0.96886325, 1.7666667, 1, 0.24744111, 0.008598767, 0.025001159, 0.9685422, 1.8333334, 1, 0.24905449, 0.008686522, 0.024981137, 0.9681283, 1.9166666, 1, 0.24962775, 0.008717708, 0.024974002, 0.96798056)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.6087138, 0.0033154194, -0.0047912123, 0.7933685, 0.43333334, 1, -0.6082812, 0.0033316796, -0.00478545, 0.79370016, 0.46666667, 1, -0.60702246, 0.0033789477, -0.004768691, 0.7946632, 0.5, 1, -0.60458106, 0.0034704483, -0.0047362074, 0.796522, 0.53333336, 1, -0.60079485, 0.0036118594, -0.0046858955, 0.79938143, 0.56666666, 1, -0.5961682, 0.0037838644, -0.0046245153, 0.80283743, 0.6, 1, -0.5908131, 0.003981882, -0.0045536063, 0.8067859, 0.6333333, 1, -0.58484256, 0.0042013186, -0.004474717, 0.8111237, 0.6666667, 1, -0.5783715, 0.004437577, -0.0043894136, 0.81574965, 0.7, 1, -0.5714863, 0.0046871966, -0.0042988746, 0.82058704, 0.76666665, 1, -0.55743563, 0.0051910933, -0.0041148057, 0.8301938, 0.8, 1, -0.5505255, 0.0054362593, -0.004024616, 0.8347909, 0.8333333, 1, -0.5438671, 0.0056708786, -0.0039379173, 0.839143, 0.8666667, 1, -0.5378202, 0.0058826003, -0.0038593498, 0.8430302, 0.9, 1, -0.5324252, 0.006070423, -0.003789391, 0.84644675, 0.93333334, 1, -0.5278194, 0.0062299813, -0.0037297653, 0.84932566, 0.96666664, 1, -0.5241402, 0.0063569224, -0.0036821994, 0.8516004, 1, 1, -0.52152413, 0.0064469036, -0.0036484145, 0.85320437, 1.0333333, 1, -0.52059346, 0.0064788577, -0.0036364023, 0.8537724, 1.0666667, 1, -0.5209789, 0.0064656264, -0.0036413767, 0.8535373, 1.1, 1, -0.5225496, 0.0064116614, -0.0036616537, 0.852577, 1.1333333, 1, -0.5251793, 0.006321117, -0.0036956277, 0.85096014, 1.1666666, 1, -0.52874804, 0.0061978716, -0.0037417784, 0.84874797, 1.2, 1, -0.5333343, 0.006038848, -0.0038011698, 0.84587455, 1.2333333, 1, -0.5385017, 0.005858806, -0.0038681966, 0.8425952, 1.2666667, 1, -0.5441296, 0.0056616613, -0.003941331, 0.83897287, 1.3, 1, -0.55009854, 0.0054513523, -0.0040190504, 0.83507234, 1.3333334, 1, -0.5562904, 0.005231847, -0.004099842, 0.83096147, 1.3666667, 1, -0.56255907, 0.005008194, -0.004181818, 0.8267313, 1.4, 1, -0.5687081, 0.004787411, -0.004262406, 0.82251453, 1.4333333, 1, -0.57462686, 0.00457356, -0.0043401443, 0.8183913, 1.4666667, 1, -0.58020735, 0.0043707127, -0.004413594, 0.81444514, 1.5, 1, -0.5853436, 0.004182954, -0.0044813314, 0.81076217, 1.5333333, 1, -0.58973014, 0.004021788, -0.0045392844, 0.80757767, 1.5666667, 1, -0.59356636, 0.0038802198, -0.0045900447, 0.8047627, 1.6, 1, -0.5968872, 0.0037571904, -0.004634047, 0.80230296, 1.6333333, 1, -0.59972703, 0.003651633, -0.0046717203, 0.80018276, 1.6666666, 1, -0.60211897, 0.003562469, -0.0047034835, 0.79838455, 1.7, 1, -0.6039782, 0.0034930015, -0.0047281934, 0.79697925, 1.7333333, 1, -0.6054832, 0.0034366688, -0.004748207, 0.79583657, 1.7666667, 1, -0.6066644, 0.0033923837, -0.004763925, 0.7949365, 1.8, 1, -0.6075524, 0.0033590589, -0.004775745, 0.79425824, 1.8666667, 1, -0.6085087, 0.00332313, -0.0047884807, 0.79352593, 1.9166666, 1, -0.6087138, 0.0033154194, -0.0047912123, 0.7933685)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608398, 0.06599527, -0.0034688246)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.9914148, -0.020339629, 0.007486256, 0.12894534, 0.46666667, 1, 0.99139327, -0.021358175, 0.0073845517, 0.1289523, 0.5, 1, 0.9913604, -0.022825377, 0.007238025, 0.12896204, 0.53333336, 1, 0.99130535, -0.025093019, 0.007011522, 0.12897655, 0.56666666, 1, 0.99123144, -0.027851883, 0.006735885, 0.12899329, 0.6, 1, 0.99113685, -0.03102918, 0.006418352, 0.12901138, 0.6333333, 1, 0.9910201, -0.034552038, 0.00606617, 0.12902988, 0.6666667, 1, 0.99088037, -0.038347565, 0.0056865923, 0.12904796, 0.7, 1, 0.9907168, -0.042361055, 0.0052850675, 0.12906507, 0.76666665, 1, 0.9903368, -0.050476957, 0.004472644, 0.12909327, 0.8, 1, 0.9901276, -0.054433577, 0.004076344, 0.1291039, 0.8333333, 1, 0.9899123, -0.05822559, 0.003696391, 0.12911218, 0.8666667, 1, 0.98970515, -0.061653025, 0.0033528458, 0.129118, 0.9, 1, 0.98951125, -0.06469805, 0.0030475333, 0.12912188, 0.93333334, 1, 0.98933893, -0.06728835, 0.0027877463, 0.12912428, 0.96666664, 1, 0.98919684, -0.06935158, 0.0025807724, 0.12912554, 1, 1, 0.98909336, -0.07081549, 0.0024338933, 0.12912609, 1.1, 1, 0.9891342, -0.07024203, 0.0024914336, 0.12912594, 1.1333333, 1, 0.9892373, -0.068769395, 0.0026391775, 0.12912524, 1.1666666, 1, 0.98937416, -0.066766635, 0.0028400747, 0.12912387, 1.2, 1, 0.9895445, -0.06418572, 0.0030989095, 0.12912132, 1.2333333, 1, 0.9897291, -0.061267596, 0.0033914838, 0.12911743, 1.2666667, 1, 0.989921, -0.058076635, 0.0037113167, 0.12911189, 1.3, 1, 0.9901142, -0.054677315, 0.0040519265, 0.1291045, 1.3333334, 1, 0.99030316, -0.05113416, 0.0044068284, 0.1290952, 1.3666667, 1, 0.9904825, -0.047528766, 0.0047678407, 0.12908407, 1.4, 1, 0.9906466, -0.043973595, 0.0051236995, 0.12907138, 1.4333333, 1, 0.99079335, -0.040533427, 0.0054679303, 0.12905756, 1.4666667, 1, 0.99092144, -0.03727305, 0.005794067, 0.12904304, 1.5, 1, 0.99103034, -0.03425727, 0.0060956427, 0.12902835, 1.5333333, 1, 0.9911166, -0.031669807, 0.0063543166, 0.12901483, 1.5666667, 1, 0.9911866, -0.029397892, 0.0065813903, 0.12900221, 1.6, 1, 0.99124336, -0.027424108, 0.006778628, 0.12899074, 1.6333333, 1, 0.9912889, -0.02573097, 0.0069477903, 0.12898052, 1.6666666, 1, 0.99132514, -0.024301015, 0.0070906356, 0.12897158, 1.7, 1, 0.99135196, -0.023186995, 0.0072019063, 0.1289644, 1.7666667, 1, 0.99138856, -0.021573633, 0.0073630335, 0.12895375, 1.9, 1, 0.9914142, -0.02036908, 0.007483316, 0.12894554, 1.9166666, 1, 0.9914148, -0.020339629, 0.007486256, 0.12894534)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, 0.24962777, -0.008717655, -0.024974115, 0.9679805)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.6087138, -0.0033154439, 0.004791257, 0.7933685)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.345526e-08, -0.98076427, 0.19519576, 1.8277876e-06)
-
-[sub_resource type="Animation" id="Animation_k86pu"]
-resource_name = "Emote2"
-length = 2.5
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, -3.5255984e-16, 0.86128783, 0.029855084, 0.06666667, 1, -3.5255984e-16, 0.86128783, 0.029855084, 0.1, 1, -9.23681e-11, 0.85979277, 0.029422898, 0.13333334, 1, -3.615942e-10, 0.8554065, 0.028162345, 0.16666667, 1, -8.8429186e-10, 0.84683186, 0.025713248, 0.2, 1, -1.6976913e-09, 0.83326656, 0.021895455, 0.23333333, 1, -2.693671e-09, 0.8164033, 0.017213155, 0.26666668, 1, -3.8486005e-09, 0.7965391, 0.01177436, 0.3, 1, -5.138847e-09, 0.77397084, 0.00568709, 0.33333334, 1, -6.5407812e-09, 0.7489952, -0.0009406437, 0.36666667, 1, -8.044371e-09, 0.72140354, -0.008072969, 0.4, 1, -9.588756e-09, 0.6922951, -0.015421722, 0.43333334, 1, -1.1150305e-08, 0.6619667, -0.022878893, 0.46666667, 1, -1.2705388e-08, 0.63071513, -0.030336468, 0.56666666, 1, -1.7004577e-08, 0.5348636, -0.05123568, 0.6, 1, -1.8216562e-08, 0.5035699, -0.057254817, 0.6333333, 1, -1.9280296e-08, 0.47313416, -0.062626265, 0.6666667, 1, -2.017215e-08, 0.4438532, -0.06724204, 0.7, 1, -2.0787564e-08, 0.41670564, -0.07063418, 0.73333335, 1, -8.376556e-09, 0.39160332, -0.0729466, 0.76666665, 1, -4.8840144e-11, 0.36880195, -0.074443705, 0.8, 1, -1.3922728e-10, 0.34845346, -0.07542994, 0.8333333, 1, -2.0774492e-10, 0.3306061, -0.07624981, 0.8666667, 1, -2.5266972e-10, 0.3160656, -0.076872036, 0.9, 1, -2.8407568e-10, 0.30398443, -0.077364184, 0.93333334, 1, -3.0475036e-10, 0.29434857, -0.07773837, 0.96666664, 1, -3.1747688e-10, 0.28714406, -0.07800666, 1, 1, -3.2503422e-10, 0.28235698, -0.07818116, 1.0333333, 1, 2.4724194e-09, 0.280765, -0.078254834, 1.7666667, 1, 3.3398186e-09, 0.28036702, -0.077497974, 1.8, 1, 3.2220597e-09, 0.28152573, -0.076905034, 1.8333334, 1, 3.4231604e-09, 0.28616086, -0.075922914, 1.8666667, 1, 3.7747157e-09, 0.2993094, -0.07405552, 1.9, 1, 4.23809e-09, 0.31799152, -0.07155387, 1.9333333, 1, 4.8125988e-09, 0.34163976, -0.068437755, 1.9666667, 1, 5.497561e-09, 0.36968666, -0.06472695, 2, 1, 6.2922876e-09, 0.4015646, -0.06044129, 2.0333333, 1, 7.231999e-09, 0.4374153, -0.05542873, 2.0666666, 1, 8.27943e-09, 0.47539455, -0.049900673, 2.1, 1, 9.4338874e-09, 0.51493454, -0.04387696, 2.1333334, 1, 1.0694698e-08, 0.5554679, -0.037377343, 2.1666667, 1, 1.2061167e-08, 0.5964267, -0.030421691, 2.2, 1, 2.8651579e-09, 0.6368182, -0.022897478, 2.2333333, 1, 1.7864936e-09, 0.67593247, -0.015030328, 2.2666667, 1, 3.4307501e-09, 0.7132021, -0.00701906, 2.3, 1, 5.074852e-09, 0.74805963, 0.0009196113, 2.3333333, 1, 6.666919e-09, 0.77993757, 0.008551064, 2.3666666, 1, 8.085821e-09, 0.8067074, 0.015303514, 2.4, 1, 9.297038e-09, 0.828795, 0.021044824, 2.4333334, 1, 1.0248671e-08, 0.8456327, 0.025540272, 2.4666667, 1, 8.655683e-09, 0.8566528, 0.02855524, 2.5, 1, -3.5255984e-16, 0.86128783, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.014909217, 1.1919607e-07, 1.7773131e-09, 0.99988884)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.029448895, -1.5216656e-14, 7.0211663e-09, 0.9995663)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -3.6572172e-16, 0.15150139, -0.011741015, 0.06666667, 1, -3.6572172e-16, 0.15150139, -0.011741015, 0.1, 1, -6.7093033e-12, 0.15265854, -0.011700376, 0.13333334, 1, -2.6251707e-11, 0.1560293, -0.011581996, 0.16666667, 1, -6.4167664e-11, 0.1625694, -0.0113523165, 0.2, 1, -1.2308726e-10, 0.17273192, -0.010995409, 0.23333333, 1, -1.9512916e-10, 0.18515837, -0.010559006, 0.26666668, 1, -2.7854707e-10, 0.19954686, -0.010053694, 0.3, 1, -3.7159154e-10, 0.21559548, -0.009490075, 0.33333334, 1, -4.7251014e-10, 0.23300235, -0.008878755, 0.36666667, 1, -5.8042887e-10, 0.25161687, -0.0082250275, 0.46666667, 1, -9.1293356e-10, 0.30896947, -0.0062108515, 0.5, 1, -1.0208533e-09, 0.32758388, -0.0055571217, 0.53333336, 1, -1.1217753e-09, 0.34499088, -0.004945801, 0.56666666, 1, -1.2148166e-09, 0.36103958, -0.004382183, 0.6, 1, -1.2982325e-09, 0.37542808, -0.0038768707, 0.6333333, 1, -1.3702752e-09, 0.3878544, -0.0034404672, 0.6666667, 1, -1.4291942e-09, 0.39801684, -0.0030835643, 0.7, 1, -1.4671144e-09, 0.40455696, -0.0028538783, 0.73333335, 1, -1.3333378e-09, 0.40792778, -0.0027355014, 0.76666665, 1, -1.1982585e-09, 0.40225887, -0.0029345928, 0.8, 1, -1.0878654e-09, 0.3832176, -0.0036033082, 0.8333333, 1, -8.837435e-10, 0.3480092, -0.0048397966, 0.8666667, 1, -6.0463523e-10, 0.2998672, -0.0065305154, 0.9, 1, -3.067831e-10, 0.24849178, -0.008334781, 0.93333334, 1, -1.5177595e-11, 0.19819401, -0.010101198, 0.96666664, 1, 2.4518548e-10, 0.15328543, -0.011678356, 1, 1, 4.4930948e-10, 0.11807719, -0.012914854, 1.0333333, 1, 3.2403646e-10, 0.10442491, -0.013394308, 1.0666667, 1, 2.927166e-10, 0.101011835, -0.013514168, 1.4333333, 1, 2.927182e-10, 0.101011954, -0.01351417, 1.4666667, 1, 2.7736657e-10, 0.10365942, -0.013421186, 1.5, 1, 2.1597076e-10, 0.1142493, -0.0130492775, 1.5333333, 1, 5.410708e-11, 0.14216845, -0.012068776, 1.5666667, 1, -1.579882e-10, 0.17875224, -0.010783985, 1.6, 1, -4.0357262e-10, 0.22111225, -0.009296335, 1.6666666, 1, -9.2823693e-10, 0.31160876, -0.0061181537, 1.7, 1, -1.1570772e-09, 0.35108072, -0.0047319313, 1.7333333, 1, -1.3356833e-09, 0.38188794, -0.0036500068, 1.7666667, 1, -1.3451008e-09, 0.40114245, -0.002973805, 1.8, 1, -1.2339201e-09, 0.4084103, -0.002718564, 1.8333334, 1, -1.2182767e-09, 0.4057116, -0.002813332, 1.8666667, 1, -1.1744519e-09, 0.39815253, -0.0030788062, 1.9, 1, -1.1129955e-09, 0.38755202, -0.0034510836, 1.9333333, 1, -1.0362412e-09, 0.37431285, -0.0039160303, 1.9666667, 1, -9.465223e-10, 0.35883766, -0.00445951, 2, 1, -8.4617385e-10, 0.34152916, -0.0050673764, 2.0333333, 1, -7.363238e-10, 0.3225816, -0.0057328017, 2.0666666, 1, -6.2284733e-10, 0.30300835, -0.006420199, 2.1333334, 1, -3.9435138e-10, 0.2635966, -0.0078043146, 2.1666667, 1, -2.8400401e-10, 0.24456313, -0.008472753, 2.2, 1, 7.42407e-10, 0.22711177, -0.009085633, 2.2333333, 1, 1.0650997e-09, 0.21134874, -0.0096392175, 2.2666667, 1, 1.1463418e-09, 0.19733565, -0.010131348, 2.3, 1, 1.2172786e-09, 0.18510002, -0.010561055, 2.3333333, 1, 1.2779463e-09, 0.17463535, -0.010928563, 2.3666666, 1, 1.3250026e-09, 0.16651897, -0.011213608, 2.4, 1, 1.361904e-09, 0.16015385, -0.011437143, 2.4333334, 1, 1.3886915e-09, 0.15553358, -0.011599406, 2.4666667, 1, 1.122985e-09, 0.15265162, -0.011700624, 2.5, 1, -3.6572172e-16, 0.15150139, -0.011741015)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, 0.00540027, -1.1920752e-07, -6.4376265e-10, 0.9999854)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.008212382, -4.9427408e-14, -1.9579842e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.444091e-08, 0.5406087, 0.3122278, 0.13333334, 1, 7.4438994e-08, 0.5395874, 0.31221974, 0.16666667, 1, 7.443622e-08, 0.5381123, 0.31220812, 0.2, 1, 7.443192e-08, 0.5358203, 0.31219006, 0.23333333, 1, 7.442665e-08, 0.5330175, 0.31216797, 0.26666668, 1, 7.4420555e-08, 0.52977216, 0.3121424, 0.3, 1, 7.4413755e-08, 0.52615243, 0.31211388, 0.33333334, 1, 7.440639e-08, 0.5222265, 0.31208298, 0.36666667, 1, 7.43985e-08, 0.51802796, 0.3120499, 0.46666667, 1, 7.437419e-08, 0.50509226, 0.31194794, 0.5, 1, 7.4366305e-08, 0.50089395, 0.31191486, 0.53333336, 1, 7.435893e-08, 0.49696776, 0.31188393, 0.56666666, 1, 7.435213e-08, 0.49334803, 0.3118554, 0.6, 1, 7.434603e-08, 0.49010277, 0.31182984, 0.6333333, 1, 7.434077e-08, 0.4873001, 0.31180775, 0.6666667, 1, 7.433646e-08, 0.48500812, 0.3117897, 0.7, 1, 7.433369e-08, 0.48353302, 0.31177807, 0.9, 1, 7.4335986e-08, 0.48475504, 0.3117877, 0.96666664, 1, 7.433848e-08, 0.48608494, 0.31179816, 1.8333334, 1, 7.434086e-08, 0.48735172, 0.31180814, 1.8666667, 1, 7.434315e-08, 0.48856923, 0.31181774, 1.9, 1, 7.4346396e-08, 0.4902992, 0.31183136, 1.9333333, 1, 7.435052e-08, 0.49248904, 0.31184864, 1.9666667, 1, 7.435539e-08, 0.4950862, 0.31186908, 2, 1, 7.436094e-08, 0.4980381, 0.31189236, 2.0333333, 1, 7.436718e-08, 0.5013579, 0.31191853, 2.0666666, 1, 7.437378e-08, 0.5048748, 0.3119462, 2.1, 1, 7.438066e-08, 0.5085362, 0.31197506, 2.1333334, 1, 7.4387714e-08, 0.5122896, 0.31200466, 2.1666667, 1, 7.439483e-08, 0.5160824, 0.31203452, 2.2, 1, 7.440187e-08, 0.5198226, 0.31206402, 2.2333333, 1, 7.4408675e-08, 0.5234446, 0.31209257, 2.2666667, 1, 7.441516e-08, 0.5268959, 0.31211978, 2.3, 1, 7.442122e-08, 0.5301237, 0.3121452, 2.3333333, 1, 7.442676e-08, 0.53307563, 0.31216842, 2.3666666, 1, 7.443142e-08, 0.5355545, 0.31218797, 2.4, 1, 7.443526e-08, 0.53759986, 0.3122041, 2.4333334, 1, 7.443819e-08, 0.53915906, 0.31221637, 2.4666667, 1, 7.44401e-08, 0.54017955, 0.3122244, 2.5, 1, 7.444091e-08, 0.5406087, 0.3122278)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225, 0.1, 1, 0.0033317525, 1.19208636e-07, -3.9717582e-10, 0.99999446, 0.13333334, 1, 0.0015606292, 1.1920916e-07, -1.8604142e-10, 0.9999988, 0.16666667, 1, -0.0018765643, 1.192091e-07, 2.2370401e-10, 0.99999833, 0.2, 1, -0.0072209714, 1.19206184e-07, 8.60807e-10, 0.99997395, 0.23333333, 1, -0.0137600405, 1.19198006e-07, 1.6403249e-09, 0.99990535, 0.26666668, 1, -0.021335743, 1.1918216e-07, 2.543419e-09, 0.9997724, 0.3, 1, -0.029789276, 1.191564e-07, 3.551159e-09, 0.99955624, 0.33333334, 1, -0.038961112, 1.1911879e-07, 4.6445265e-09, 0.99924076, 0.36666667, 1, -0.048771236, 1.1906742e-07, 5.813985e-09, 0.99881005, 0.46666667, 1, -0.07897128, 1.18836994e-07, 9.41411e-09, 0.99687696, 0.5, 1, -0.08875626, 1.1873882e-07, 1.0580572e-08, 0.9960534, 0.53333336, 1, -0.097893484, 1.1863672e-07, 1.1669814e-08, 0.995197, 0.56666666, 1, -0.106305555, 1.18533784e-07, 1.26726105e-08, 0.9943335, 0.6, 1, -0.113836385, 1.1843437e-07, 1.35703555e-08, 0.9934995, 0.6333333, 1, -0.120330855, 1.1834311e-07, 1.43445575e-08, 0.9927339, 0.6666667, 1, -0.12563474, 1.18264744e-07, 1.497683e-08, 0.9920766, 0.7, 1, -0.12904395, 1.1821257e-07, 1.5383238e-08, 0.9916389, 0.73333335, 1, -0.13080007, 1.1818514e-07, 1.5592585e-08, 0.9914089, 0.8, 1, -0.13055308, 1.1818899e-07, 1.5563144e-08, 0.9914413, 0.8333333, 1, -0.12939651, 1.18207076e-07, 1.542527e-08, 0.99159294, 0.8666667, 1, -0.12781522, 1.1823151e-07, 1.5236765e-08, 0.991798, 0.9, 1, -0.12612778, 1.1825728e-07, 1.5035607e-08, 0.99201405, 0.93333334, 1, -0.124475725, 1.1828216e-07, 1.4838665e-08, 0.99222267, 0.96666664, 1, -0.123000674, 1.1830408e-07, 1.4662826e-08, 0.9924066, 1, 1, -0.12184428, 1.183211e-07, 1.4524972e-08, 0.99254924, 1.8333334, 1, -0.12004511, 1.1834724e-07, 1.4310493e-08, 0.9927685, 1.8666667, 1, -0.11723241, 1.1838728e-07, 1.39751934e-08, 0.9931045, 1.9, 1, -0.11323249, 1.184426e-07, 1.3498367e-08, 0.9935686, 1.9333333, 1, -0.10816401, 1.185099e-07, 1.2894157e-08, 0.9941331, 1.9666667, 1, -0.10214584, 1.1858576e-07, 1.2176734e-08, 0.9947695, 2, 1, -0.09529724, 1.1866676e-07, 1.1360317e-08, 0.99544895, 2.0333333, 1, -0.0875838, 1.18751196e-07, 1.0440804e-08, 0.9961572, 2.0666666, 1, -0.07940258, 1.1883291e-07, 9.465527e-09, 0.9968427, 2.1, 1, -0.070876114, 1.189095e-07, 8.449092e-09, 0.99748516, 2.2, 1, -0.044551987, 1.1909091e-07, 5.3110116e-09, 0.99900705, 2.2333333, 1, -0.03609759, 1.191316e-07, 4.3031685e-09, 0.99934834, 2.2666667, 1, -0.028041584, 1.19162436e-07, 3.3428176e-09, 0.99960685, 2.3, 1, -0.020507902, 1.1918424e-07, 2.4447326e-09, 0.9997898, 2.3333333, 1, -0.013620116, 1.1919823e-07, 1.6236444e-09, 0.99990726, 2.3666666, 1, -0.007839098, 1.1920564e-07, 9.344934e-10, 0.9999693, 2.4, 1, -0.003070962, 1.19208735e-07, 3.6608733e-10, 0.99999535, 2.4333334, 1, 0.0005625679, 1.1920929e-07, -6.7063244e-11, 0.99999994, 2.4666667, 1, 0.0029399488, 1.1920879e-07, -3.5046918e-10, 0.9999957, 2.5, 1, 0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.97907e-08, 0.06115723, 0.4185525, 0.13333334, 1, 9.979221e-08, 0.061960794, 0.41855884, 0.16666667, 1, 9.9794384e-08, 0.06312138, 0.41856796, 0.2, 1, 9.9797774e-08, 0.064924724, 0.41858217, 0.23333333, 1, 9.9801916e-08, 0.06712989, 0.41859955, 0.26666668, 1, 9.980672e-08, 0.069683224, 0.4186197, 0.3, 1, 9.981207e-08, 0.072531134, 0.41864213, 0.33333334, 1, 9.9817875e-08, 0.075620085, 0.41866648, 0.36666667, 1, 9.982408e-08, 0.07892332, 0.41869253, 0.46666667, 1, 9.98432e-08, 0.08910111, 0.4187727, 0.5, 1, 9.984941e-08, 0.09240435, 0.41879874, 0.53333336, 1, 9.985521e-08, 0.0954933, 0.41882306, 0.56666666, 1, 9.986056e-08, 0.09834122, 0.4188455, 0.6, 1, 9.986535e-08, 0.10089456, 0.41886562, 0.6333333, 1, 9.9869496e-08, 0.10309972, 0.418883, 0.6666667, 1, 9.9872885e-08, 0.10490306, 0.4188972, 0.7, 1, 9.9875066e-08, 0.10606364, 0.41890636, 0.93333334, 1, 9.987222e-08, 0.10454935, 0.4188944, 1.0333333, 1, 9.9870284e-08, 0.10351891, 0.4188863, 1.8666667, 1, 9.986762e-08, 0.102101155, 0.41887513, 1.9, 1, 9.986507e-08, 0.10074002, 0.41886443, 1.9333333, 1, 9.986183e-08, 0.09901712, 0.41885084, 1.9666667, 1, 9.985799e-08, 0.09697377, 0.41883475, 2, 1, 9.985363e-08, 0.09465126, 0.41881645, 2.0333333, 1, 9.984872e-08, 0.09203923, 0.41879585, 2.0666666, 1, 9.984352e-08, 0.0892722, 0.41877404, 2.1, 1, 9.983811e-08, 0.086391486, 0.41875136, 2.2, 1, 9.982143e-08, 0.07751146, 0.41868138, 2.2333333, 1, 9.981607e-08, 0.07466176, 0.4186589, 2.2666667, 1, 9.9810975e-08, 0.07194641, 0.41863754, 2.3, 1, 9.98062e-08, 0.06940676, 0.41861752, 2.3333333, 1, 9.980184e-08, 0.06708425, 0.41859922, 2.3666666, 1, 9.979818e-08, 0.06513386, 0.41858387, 2.4, 1, 9.9795145e-08, 0.063524626, 0.41857114, 2.4333334, 1, 9.979284e-08, 0.062297862, 0.4185615, 2.5, 1, 9.97907e-08, 0.06115723, 0.4185525)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939635, 1.1920836e-07, -4.696411e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732144, 0.0764982, -0.028223984)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.5230665, -0.45780188, -0.52192616, 0.49438047)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435951, 0.22304288, -8.365756e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, -0.35644147, 0.617847, -0.43310678, 0.5510291, 0.1, 1, -0.35674307, 0.617627, -0.43342796, 0.550828, 0.13333334, 1, -0.3576212, 0.6169851, -0.43436295, 0.5502412, 0.16666667, 1, -0.35932294, 0.61573505, -0.43617442, 0.5490986, 0.2, 1, -0.36196196, 0.6137806, -0.43898278, 0.54731256, 0.23333333, 1, -0.3651799, 0.6113714, -0.44240555, 0.5451111, 0.26666668, 1, -0.36889276, 0.60855526, -0.44635248, 0.5425385, 0.3, 1, -0.3730168, 0.6053813, -0.45073384, 0.5396398, 0.33333334, 1, -0.3774688, 0.60189974, -0.45546013, 0.5364608, 0.36666667, 1, -0.38220477, 0.598132, -0.46048403, 0.5330217, 0.46666667, 1, -0.3966211, 0.58624524, -0.47575128, 0.5221771, 0.5, 1, -0.40123996, 0.5822994, -0.4806344, 0.5185793, 0.53333336, 1, -0.4055305, 0.5785724, -0.48516676, 0.51518166, 0.56666666, 1, -0.40946165, 0.5751047, -0.48931602, 0.51202124, 0.6, 1, -0.4129653, 0.5719704, -0.49301147, 0.5091653, 0.6333333, 1, -0.41597506, 0.56924474, -0.49618402, 0.5066819, 0.6666667, 1, -0.41842517, 0.5670029, -0.49876523, 0.5046398, 0.7, 1, -0.41999632, 0.56555444, -0.5004198, 0.50332046, 0.73333335, 1, -0.4208044, 0.564806, -0.50127053, 0.50263876, 0.8, 1, -0.42069793, 0.5649048, -0.5011585, 0.5027287, 0.8333333, 1, -0.4201751, 0.565389, -0.500608, 0.5031698, 0.8666667, 1, -0.41945952, 0.56605023, -0.49985456, 0.503772, 0.9, 1, -0.418695, 0.5667549, -0.49904943, 0.5044138, 0.93333334, 1, -0.4179454, 0.56744355, -0.4982599, 0.5050412, 0.96666664, 1, -0.41727537, 0.56805766, -0.497554, 0.5056005, 1, 1, -0.41674945, 0.56853837, -0.49700004, 0.5060384, 1.8333334, 1, -0.41591984, 0.569295, -0.4961259, 0.50672776, 1.8666667, 1, -0.41461426, 0.57048094, -0.49474984, 0.50780827, 1.9, 1, -0.412754, 0.57216054, -0.49278867, 0.50933856, 1.9333333, 1, -0.4103912, 0.57427716, -0.49029672, 0.5112671, 1.9666667, 1, -0.40757728, 0.57677335, -0.48732746, 0.51354194, 2, 1, -0.4043638, 0.5795918, -0.48393464, 0.51611096, 2.0333333, 1, -0.40073094, 0.5827377, -0.4800965, 0.51897883, 2.0666666, 1, -0.39686117, 0.5860418, -0.4760052, 0.5219917, 2.1, 1, -0.39280993, 0.5894502, -0.4717189, 0.52510023, 2.2, 1, -0.38018358, 0.5997481, -0.45834044, 0.5344967, 2.2333333, 1, -0.37608993, 0.6029842, -0.45399666, 0.53745097, 2.2666667, 1, -0.3721714, 0.60603595, -0.44983596, 0.54023755, 2.3, 1, -0.3684911, 0.6088618, -0.4459257, 0.54281855, 2.3333333, 1, -0.36511287, 0.6114219, -0.44233426, 0.5451572, 2.3666666, 1, -0.36226755, 0.61355317, -0.43930784, 0.54710454, 2.4, 1, -0.35991377, 0.61529917, -0.43680328, 0.5487002, 2.4333334, 1, -0.35811582, 0.61662257, -0.43488955, 0.5499098, 2.4666667, 1, -0.35693744, 0.6174851, -0.433635, 0.5506982, 2.5, 1, -0.35644147, 0.617847, -0.43310678, 0.5510291)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.017577726, -0.09135095, 0.027113229, 0.9952944, 0.06666667, 1, -0.017577726, -0.09135095, 0.027113229, 0.9952944, 0.1, 1, -0.014555428, -0.091268204, 0.02739049, 0.9953432, 0.13333334, 1, -0.005703743, -0.09102104, 0.02820101, 0.9954333, 0.16666667, 1, 0.01156849, -0.09051803, 0.029776054, 0.99538237, 0.2, 1, 0.038928565, -0.08966523, 0.03225327, 0.9946882, 0.23333333, 1, 0.07301318, -0.08850654, 0.035308883, 0.99276835, 0.26666668, 1, 0.11314635, -0.08700446, 0.038863193, 0.98899835, 0.3, 1, 0.15853083, -0.08512421, 0.042825162, 0.98274505, 0.33333334, 1, 0.2082443, -0.08283906, 0.047093727, 0.97342396, 0.36666667, 1, 0.26179117, -0.08010611, 0.051605593, 0.9604089, 0.4, 1, 0.31621796, -0.077027865, 0.056096643, 0.9438888, 0.43333334, 1, 0.37016135, -0.07366225, 0.06044832, 0.9240674, 0.46666667, 1, 0.42233574, -0.07008945, 0.0645569, 0.9014169, 0.5, 1, 0.47156996, -0.0664097, 0.06833649, 0.8766651, 0.53333336, 1, 0.515491, -0.06285286, 0.07162152, 0.85158026, 0.56666666, 1, 0.55412674, -0.05949095, 0.07443757, 0.8269604, 0.6, 1, 0.58715266, -0.05642851, 0.076785095, 0.80384815, 0.6333333, 1, 0.6143709, -0.05376239, 0.07867484, 0.7832421, 0.6666667, 1, 0.6356709, -0.051579222, 0.08012309, 0.7660564, 0.7, 1, 0.64886683, -0.050181255, 0.08100597, 0.75491166, 0.73333335, 1, 0.6555543, -0.04945887, 0.08144894, 0.74911183, 0.76666665, 1, 0.6569983, -0.04930161, 0.08154417, 0.74784577, 0.8, 1, 0.65468097, -0.049553704, 0.081391215, 0.7498752, 0.8333333, 1, 0.65037066, -0.050019544, 0.081105754, 0.7536166, 0.8666667, 1, 0.6444046, -0.0506579, 0.080708586, 0.7587245, 0.9, 1, 0.6379561, -0.051339567, 0.08027669, 0.76415443, 0.93333334, 1, 0.63156474, -0.05200689, 0.079845995, 0.7694452, 0.96666664, 1, 0.6257922, -0.05260263, 0.079454795, 0.77414733, 1, 1, 0.62122005, -0.05306986, 0.07914347, 0.77782106, 1.0333333, 1, 0.61943537, -0.053251233, 0.07902168, 0.779243, 1.7666667, 1, 0.61898875, -0.053296536, 0.078991175, 0.7795979, 1.8, 1, 0.6179796, -0.05339869, 0.07892215, 0.7803981, 1.8333334, 1, 0.61393255, -0.05380641, 0.07864474, 0.78358585, 1.8666667, 1, 0.6022701, -0.054964274, 0.077839926, 0.7925848, 1.9, 1, 0.58527267, -0.056607775, 0.07665303, 0.805218, 1.9333333, 1, 0.56309175, -0.058677495, 0.07508047, 0.8208822, 1.9666667, 1, 0.5358754, -0.06110757, 0.07311629, 0.8389026, 2, 1, 0.503794, -0.063826695, 0.07075506, 0.858552, 2.0333333, 1, 0.4661759, -0.06682822, 0.06792726, 0.87954533, 2.0666666, 1, 0.4249295, -0.069903284, 0.06475844, 0.9001972, 2.1, 1, 0.38069543, -0.07296676, 0.06128602, 0.9197776, 2.1333334, 1, 0.33423537, -0.0759395, 0.057561427, 0.9376602, 2.1666667, 1, 0.28642717, -0.07875115, 0.05365056, 0.9533517, 2.2, 1, 0.23892367, -0.08130832, 0.04968986, 0.9663516, 2.2333333, 1, 0.192868, -0.08357138, 0.04578153, 0.9765869, 2.2666667, 1, 0.14917293, -0.08552783, 0.04201327, 0.98420906, 2.3, 1, 0.10868229, -0.087178946, 0.03847019, 0.989499, 2.3333333, 1, 0.07215722, -0.08853695, 0.03523256, 0.99283093, 2.3666666, 1, 0.04213174, -0.08956088, 0.032541856, 0.9945576, 2.4, 1, 0.017695265, -0.090333015, 0.030332657, 0.9952923, 2.4333334, 1, -0.00070105353, -0.09087816, 0.0286581, 0.99544936, 2.4666667, 1, -0.012609804, -0.091214515, 0.02756885, 0.9953698, 2.5, 1, -0.017577726, -0.09135095, 0.027113229, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.020934898, 0.017021658, -0.017671391, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732144, 0.0764982, -0.028223984)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.5230665, 0.45780188, 0.52192616, 0.49438047)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435951, 0.22304288, -8.365756e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, -0.35644147, -0.617847, 0.43310678, 0.5510291, 0.1, 1, -0.35674307, -0.61762697, 0.433428, 0.5508279, 0.13333334, 1, -0.35762128, -0.6169851, 0.434363, 0.55024105, 0.16666667, 1, -0.35932297, -0.61573505, 0.4361745, 0.5490985, 0.2, 1, -0.36196205, -0.6137806, 0.43898287, 0.5473125, 0.23333333, 1, -0.36517993, -0.61137134, 0.44240552, 0.54511106, 0.26666668, 1, -0.36889276, -0.60855526, 0.44635248, 0.5425385, 0.3, 1, -0.3730168, -0.6053813, 0.45073384, 0.5396398, 0.33333334, 1, -0.3774688, -0.60189974, 0.45546013, 0.5364608, 0.36666667, 1, -0.38220483, -0.5981321, 0.46048406, 0.5330216, 0.46666667, 1, -0.39662117, -0.58624524, 0.47575134, 0.5221771, 0.5, 1, -0.40124002, -0.58229935, 0.48063448, 0.51857924, 0.53333336, 1, -0.4055305, -0.5785724, 0.4851668, 0.51518166, 0.56666666, 1, -0.40946165, -0.5751047, 0.48931602, 0.51202124, 0.6, 1, -0.4129653, -0.5719704, 0.49301147, 0.5091653, 0.6333333, 1, -0.41597506, -0.56924474, 0.49618408, 0.5066819, 0.6666667, 1, -0.41842517, -0.5670029, 0.49876523, 0.5046398, 0.7, 1, -0.4199963, -0.5655544, 0.50041974, 0.5033204, 0.73333335, 1, -0.42080444, -0.564806, 0.5012706, 0.5026387, 0.8, 1, -0.42069784, -0.56490487, 0.5011583, 0.50272876, 0.8333333, 1, -0.42017508, -0.565389, 0.500608, 0.5031698, 0.93333334, 1, -0.41794538, -0.56744355, 0.49825984, 0.5050412, 0.96666664, 1, -0.4172754, -0.5680576, 0.49755406, 0.5056005, 1, 1, -0.4167495, -0.5685384, 0.49700004, 0.5060385, 1.8333334, 1, -0.41592, -0.5692949, 0.49612597, 0.50672764, 1.8666667, 1, -0.41461426, -0.5704809, 0.49474984, 0.50780815, 1.9, 1, -0.41275406, -0.5721606, 0.49278873, 0.50933856, 1.9333333, 1, -0.4103912, -0.57427716, 0.49029672, 0.5112671, 1.9666667, 1, -0.4075773, -0.57677335, 0.48732746, 0.51354194, 2, 1, -0.40436393, -0.5795918, 0.48393467, 0.5161109, 2.0333333, 1, -0.40073097, -0.5827376, 0.48009646, 0.51897883, 2.0666666, 1, -0.39686117, -0.58604175, 0.4760052, 0.52199167, 2.1, 1, -0.39280987, -0.5894501, 0.47171897, 0.52510023, 2.2, 1, -0.38018355, -0.5997481, 0.4583404, 0.5344967, 2.2333333, 1, -0.37608993, -0.6029842, 0.45399666, 0.53745097, 2.2666667, 1, -0.3721714, -0.60603595, 0.44983593, 0.54023755, 2.3, 1, -0.3684911, -0.6088618, 0.44592568, 0.54281855, 2.3333333, 1, -0.36511287, -0.6114219, 0.44233426, 0.5451572, 2.3666666, 1, -0.36226755, -0.61355317, 0.43930784, 0.54710454, 2.4, 1, -0.35991374, -0.6152992, 0.43680322, 0.5487003, 2.4333334, 1, -0.3581158, -0.61662257, 0.43488947, 0.5499099, 2.4666667, 1, -0.35693744, -0.6174851, 0.43363494, 0.5506982, 2.5, 1, -0.35644147, -0.617847, 0.43310678, 0.5510291)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577725, 0.09135095, -0.027113236, 0.9952944, 0.06666667, 1, -0.017577725, 0.09135095, -0.027113236, 0.9952944, 0.1, 1, -0.014555426, 0.091268204, -0.027390493, 0.9953432, 0.13333334, 1, -0.0057037403, 0.09102103, -0.02820101, 0.9954333, 0.16666667, 1, 0.011568494, 0.09051801, -0.029776054, 0.99538237, 0.2, 1, 0.038928576, 0.08966523, -0.03225327, 0.9946882, 0.23333333, 1, 0.07301319, 0.08850654, -0.035308883, 0.99276835, 0.26666668, 1, 0.11314635, 0.08700446, -0.038863193, 0.98899835, 0.3, 1, 0.15853085, 0.08512421, -0.04282516, 0.98274505, 0.33333334, 1, 0.2082443, 0.08283906, -0.047093716, 0.97342396, 0.36666667, 1, 0.26179117, 0.08010611, -0.05160559, 0.9604089, 0.4, 1, 0.31621802, 0.07702787, -0.05609665, 0.9438888, 0.43333334, 1, 0.3701614, 0.073662244, -0.06044832, 0.9240673, 0.46666667, 1, 0.4223358, 0.07008946, -0.06455691, 0.90141696, 0.5, 1, 0.47156996, 0.0664097, -0.06833649, 0.8766651, 0.53333336, 1, 0.515491, 0.06285286, -0.07162152, 0.85158026, 0.56666666, 1, 0.55412674, 0.059490956, -0.07443757, 0.8269604, 0.6, 1, 0.58715266, 0.056428514, -0.076785095, 0.80384815, 0.6333333, 1, 0.6143709, 0.053762387, -0.07867484, 0.7832421, 0.6666667, 1, 0.6356709, 0.05157921, -0.08012309, 0.7660564, 0.7, 1, 0.64886683, 0.050181262, -0.08100596, 0.75491184, 0.73333335, 1, 0.6555543, 0.04945887, -0.08144895, 0.7491119, 0.76666665, 1, 0.6569983, 0.049301606, -0.081544176, 0.74784577, 0.8, 1, 0.65468097, 0.049553696, -0.08139122, 0.7498752, 0.8333333, 1, 0.65037066, 0.05001955, -0.08110576, 0.7536166, 0.8666667, 1, 0.6444046, 0.0506579, -0.0807086, 0.75872445, 0.9, 1, 0.63795614, 0.05133957, -0.0802767, 0.76415443, 0.93333334, 1, 0.6315647, 0.052006897, -0.07984599, 0.76944524, 0.96666664, 1, 0.62579215, 0.052602634, -0.07945479, 0.7741474, 1, 1, 0.62122005, 0.053069863, -0.07914347, 0.77782106, 1.0333333, 1, 0.61943537, 0.053251233, -0.07902168, 0.779243, 1.7666667, 1, 0.61898875, 0.053296536, -0.078991175, 0.7795979, 1.8, 1, 0.6179796, 0.05339869, -0.07892215, 0.7803981, 1.8333334, 1, 0.61393255, 0.05380641, -0.078644745, 0.78358585, 1.8666667, 1, 0.6022702, 0.05496428, -0.07783993, 0.7925848, 1.9, 1, 0.58527267, 0.05660777, -0.07665301, 0.80521786, 1.9333333, 1, 0.56309175, 0.058677495, -0.07508046, 0.8208822, 1.9666667, 1, 0.5358754, 0.061107565, -0.07311629, 0.8389026, 2, 1, 0.5037941, 0.06382668, -0.07075508, 0.858552, 2.0333333, 1, 0.4661759, 0.06682823, -0.06792727, 0.87954533, 2.0666666, 1, 0.4249295, 0.069903284, -0.06475844, 0.90019715, 2.1, 1, 0.38069552, 0.07296676, -0.061286025, 0.91977763, 2.1333334, 1, 0.33423534, 0.0759395, -0.057561427, 0.93766016, 2.1666667, 1, 0.28642705, 0.078751154, -0.053650573, 0.9533518, 2.2, 1, 0.23892365, 0.08130832, -0.04968986, 0.9663516, 2.2333333, 1, 0.192868, 0.08357138, -0.045781527, 0.9765869, 2.2666667, 1, 0.14917293, 0.08552783, -0.042013265, 0.98420906, 2.3, 1, 0.10868229, 0.087178946, -0.038470183, 0.989499, 2.3333333, 1, 0.07215724, 0.08853695, -0.035232563, 0.99283093, 2.3666666, 1, 0.042131744, 0.08956088, -0.032541856, 0.9945576, 2.4, 1, 0.017695267, 0.090333015, -0.030332655, 0.9952923, 2.4333334, 1, -0.00070105307, 0.09087816, -0.028658094, 0.99544936, 2.4666667, 1, -0.012609804, 0.0912145, -0.027568853, 0.9953698, 2.5, 1, -0.017577725, 0.09135095, -0.027113236, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934897, -0.017021656, 0.017671384, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.276084, 0.06599521, -0.0034686895)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.9914148, 0.020339562, -0.007486248, 0.12894534, 0.06666667, 1, 0.9914148, 0.020339562, -0.007486248, 0.12894534, 0.1, 1, 0.9911603, 0.020318728, -0.0075253537, 0.13088885, 0.13333334, 1, 0.99039066, 0.020257488, -0.0076400735, 0.13659343, 0.16666667, 1, 0.98878944, 0.02013651, -0.007863927, 0.14774342, 0.2, 1, 0.98598564, 0.019941926, -0.008218105, 0.16543053, 0.23333333, 1, 0.9820355, 0.019693084, -0.008657562, 0.18746601, 0.26666668, 1, 0.976718, 0.0193896, -0.009173131, 0.21345228, 0.3, 1, 0.96979964, 0.019031039, -0.009755005, 0.24296391, 0.33333334, 1, 0.9610511, 0.018617282, -0.01039282, 0.2755472, 0.36666667, 1, 0.950036, 0.01814257, -0.011089554, 0.3114153, 0.4, 1, 0.9369168, 0.017621802, -0.01181132, 0.34890798, 0.43333334, 1, 0.92164224, 0.01705938, -0.0125463195, 0.38746238, 0.46666667, 1, 0.9042324, 0.016461164, -0.013283109, 0.42651677, 0.56666666, 1, 0.8409749, 0.014627894, -0.015340483, 0.5406588, 0.6, 1, 0.8173336, 0.013995668, -0.01597298, 0.57577336, 0.6333333, 1, 0.7930044, 0.013385069, -0.016561953, 0.6088436, 0.6666667, 1, 0.7683715, 0.012807108, -0.017104754, 0.6396473, 0.7, 1, 0.7443575, 0.012297998, -0.017591476, 0.6674363, 0.73333335, 1, 0.7209895, 0.011849532, -0.018029917, 0.69261, 0.76666665, 1, 0.69859254, 0.011448232, -0.018421832, 0.715191, 0.8, 1, 0.67744696, 0.011079287, -0.018769303, 0.73524874, 0.8333333, 1, 0.6577827, 0.010726494, -0.019074531, 0.7528899, 0.8666667, 1, 0.6406208, 0.010408712, -0.01932888, 0.7675435, 0.9, 1, 0.6255857, 0.010125009, -0.019542262, 0.779845, 0.93333334, 1, 0.6129705, 0.009882756, -0.01971504, 0.789798, 0.96666664, 1, 0.6030651, 0.0096893, -0.019847106, 0.79738617, 1, 1, 0.5961532, 0.009551939, -0.019937724, 0.80256635, 1.0333333, 1, 0.5937479, 0.009503278, -0.01996914, 0.80434716, 1.7666667, 1, 0.5931457, 0.0094911, -0.019976964, 0.8047912, 1.8, 1, 0.59424555, 0.009517158, -0.019964864, 0.8039795, 1.8333334, 1, 0.59863365, 0.009621215, -0.019916097, 0.8007176, 1.8666667, 1, 0.6111551, 0.009919006, -0.019772517, 0.7912017, 1.9, 1, 0.6290208, 0.010346126, -0.01955589, 0.7770736, 1.9333333, 1, 0.65162754, 0.010890471, -0.01926085, 0.75821626, 1.9666667, 1, 0.6782814, 0.01153823, -0.018880684, 0.73446906, 2, 1, 0.7082053, 0.0122739095, -0.01840803, 0.7056599, 2.0333333, 1, 0.7414535, 0.013103518, -0.017818592, 0.6706397, 2.0666666, 1, 0.7755469, 0.013969579, -0.01713251, 0.63090295, 2.1, 1, 0.8094216, 0.014848724, -0.016352467, 0.5868126, 2.1333334, 1, 0.8420625, 0.015702786, -0.0154952295, 0.5389287, 2.2, 1, 0.89988714, 0.017250512, -0.013601098, 0.4355693, 2.2333333, 1, 0.9234134, 0.017941952, -0.012593508, 0.38318032, 2.2666667, 1, 0.9429505, 0.018545102, -0.011597491, 0.33221382, 2.3, 1, 0.958575, 0.019056367, -0.01064186, 0.28400278, 2.3333333, 1, 0.97057486, 0.019476797, -0.009754979, 0.23981231, 2.3666666, 1, 0.9789286, 0.019792877, -0.009009342, 0.2030416, 2.4, 1, 0.98470306, 0.020029731, -0.00839267, 0.17288217, 2.4333334, 1, 0.9884424, 0.020195978, -0.007922544, 0.15003663, 2.4666667, 1, 0.99058455, 0.020298121, -0.007615338, 0.13517489, 2.5, 1, 0.9914148, 0.020339562, -0.007486248, 0.12894534)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.24962775, 0.008717708, 0.024974002, 0.96798056, 0.06666667, 1, 0.24962775, 0.008717708, 0.024974002, 0.96798056, 0.1, 1, 0.25308737, 0.008766062, 0.024934728, 0.9670824, 0.13333334, 1, 0.26321372, 0.008907205, 0.024817716, 0.9643772, 0.16666667, 1, 0.28291553, 0.009180154, 0.024581214, 0.9587859, 0.2, 1, 0.31401443, 0.009606453, 0.024183696, 0.9490615, 0.23333333, 1, 0.35249743, 0.010126033, 0.023649525, 0.93545914, 0.26666668, 1, 0.3973807, 0.0107204355, 0.022964817, 0.9173038, 0.3, 1, 0.447501, 0.011368543, 0.022116916, 0.8939376, 0.33333334, 1, 0.501527, 0.012047169, 0.021096563, 0.8648009, 0.36666667, 1, 0.5587098, 0.0127404835, 0.01988366, 0.8290269, 0.4, 1, 0.6154967, 0.01340019, 0.018525805, 0.7878078, 0.46666667, 1, 0.72170776, 0.014538743, 0.01547861, 0.6918721, 0.5, 1, 0.76865906, 0.014990796, 0.013858669, 0.6393329, 0.53333336, 1, 0.80957794, 0.015350083, 0.012261978, 0.58668363, 0.56666666, 1, 0.8447223, 0.015625322, 0.010712895, 0.5348696, 0.6, 1, 0.87432337, 0.015825657, 0.009240201, 0.4849978, 0.6333333, 1, 0.89878434, 0.015962124, 0.007868015, 0.43802977, 0.6666667, 1, 0.9186236, 0.016046632, 0.006615262, 0.3947525, 0.7, 1, 0.9337365, 0.016089713, 0.0055466825, 0.3575565, 0.73333335, 1, 0.9454802, 0.016106067, 0.0046242313, 0.32524827, 0.76666665, 1, 0.9546491, 0.016110422, 0.0038265768, 0.29727283, 0.8, 1, 0.9618871, 0.016113527, 0.0031303805, 0.27295378, 0.8333333, 1, 0.9677146, 0.016122686, 0.002510903, 0.25152004, 0.8666667, 1, 0.9720789, 0.0161404, 0.0019998515, 0.23409016, 0.9, 1, 0.97544086, 0.016160749, 0.0015726108, 0.21966258, 0.93333334, 1, 0.9779458, 0.016182357, 0.001230271, 0.20822783, 0.96666664, 1, 0.9797111, 0.016203308, 0.0009735137, 0.19975644, 1, 1, 0.98082733, 0.01622117, 0.0008026575, 0.19420096, 1.0333333, 1, 0.98118454, 0.016228668, 0.0007459354, 0.19238788, 1.7666667, 1, 0.9812733, 0.016230535, 0.00073175447, 0.1919345, 1.8, 1, 0.98106813, 0.016232925, 0.0007607264, 0.19298029, 1.8333334, 1, 0.9802362, 0.016242303, 0.00087660534, 0.1971613, 1.8666667, 1, 0.9776529, 0.016268104, 0.0012217945, 0.20959195, 1.9, 1, 0.97348094, 0.016301349, 0.0017395992, 0.22817996, 1.9333333, 1, 0.9673356, 0.016335946, 0.0024328441, 0.25296074, 1.9666667, 1, 0.95869064, 0.016363537, 0.003304751, 0.28396058, 2, 1, 0.94686824, 0.016373243, 0.0043584076, 0.3211752, 2.0333333, 1, 0.93009007, 0.016348505, 0.0056646317, 0.36692408, 2.0666666, 1, 0.90792495, 0.016269747, 0.0071595353, 0.4187557, 2.1, 1, 0.87918603, 0.016115291, 0.008833983, 0.47612423, 2.1333334, 1, 0.8426367, 0.015860945, 0.010669898, 0.53814304, 2.1666667, 1, 0.797069, 0.015481082, 0.012639167, 0.6035575, 2.2, 1, 0.7401653, 0.014936678, 0.014745701, 0.6720974, 2.2333333, 1, 0.67381227, 0.014231579, 0.01684085, 0.73857355, 2.2666667, 1, 0.6006227, 0.0133881215, 0.018810399, 0.7991992, 2.3, 1, 0.524292, 0.0124507435, 0.020562425, 0.85119927, 2.3333333, 1, 0.4493402, 0.011482973, 0.022034004, 0.89301515, 2.3666666, 1, 0.38437623, 0.010611102, 0.02313641, 0.9228256, 2.4, 1, 0.33006364, 0.009861121, 0.02394645, 0.9436034, 2.4333334, 1, 0.28843057, 0.009274052, 0.02450309, 0.95714235, 2.4666667, 1, 0.2611548, 0.008883907, 0.024838826, 0.9649365, 2.5, 1, 0.24962775, 0.008717708, 0.024974002, 0.96798056)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.6087138, 0.0033154194, -0.0047912123, 0.7933685, 0.06666667, 1, -0.6087138, 0.0033154194, -0.0047912123, 0.7933685, 0.1, 1, -0.609925, 0.0033230565, -0.004786343, 0.79243773, 0.13333334, 1, -0.6134493, 0.003345287, -0.004772079, 0.7897126, 0.16666667, 1, -0.62026143, 0.0033882859, -0.004744099, 0.7843736, 0.2, 1, -0.6307998, 0.0034548838, -0.004699721, 0.77592385, 0.23333333, 1, -0.64358443, 0.0035358104, -0.004644046, 0.76535285, 0.26666668, 1, -0.6582277, 0.003628688, -0.004577702, 0.7527963, 0.3, 1, -0.6743353, 0.0037310948, -0.004501383, 0.7384021, 0.33333334, 1, -0.69151413, 0.003840607, -0.0044159056, 0.7223393, 0.36666667, 1, -0.7095298, 0.003955804, -0.0043214257, 0.7046511, 0.46666667, 1, -0.7622333, 0.004295137, -0.004012723, 0.6472757, 0.5, 1, -0.7783316, 0.004399584, -0.0039073853, 0.627826, 0.53333336, 1, -0.79288113, 0.004494356, -0.0038070192, 0.60934794, 0.56666666, 1, -0.80585885, 0.004579216, -0.003712976, 0.5920785, 0.6, 1, -0.81712943, 0.004653186, -0.003627534, 0.5764241, 0.6333333, 1, -0.82657903, 0.004715415, -0.0035529744, 0.56278986, 0.6666667, 1, -0.8341054, 0.0047651283, -0.0034915407, 0.5515735, 0.7, 1, -0.8388481, 0.004796526, -0.0034518419, 0.5443335, 0.73333335, 1, -0.8412649, 0.004812546, -0.0034313058, 0.5405908, 0.76666665, 1, -0.84178865, 0.004815462, -0.0034265113, 0.53977484, 0.8, 1, -0.8409474, 0.0048083253, -0.0034328187, 0.54108477, 0.8333333, 1, -0.8393856, 0.0047950908, -0.0034444635, 0.54350436, 0.8666667, 1, -0.8372358, 0.004776904, -0.0034603626, 0.54681027, 0.9, 1, -0.83492416, 0.0047573876, -0.0034772926, 0.5503335, 0.93333334, 1, -0.83264387, 0.0047381753, -0.0034938273, 0.5537774, 0.96666664, 1, -0.83059376, 0.0047209356, -0.003508557, 0.5568478, 1, 1, -0.82897675, 0.0047073592, -0.0035200836, 0.5592522, 1.0333333, 1, -0.82834744, 0.0047020805, -0.0035245488, 0.560184, 1.9, 1, -0.8273763, 0.0046779388, -0.0035291996, 0.56161755, 1.9333333, 1, -0.82643384, 0.004661769, -0.0035351356, 0.5630034, 1.9666667, 1, -0.8249813, 0.004641162, -0.0035450158, 0.56513, 2, 1, -0.8229096, 0.0046159364, -0.0035597463, 0.5681425, 2.0333333, 1, -0.81979316, 0.004584171, -0.003582774, 0.5726303, 2.0666666, 1, -0.81570756, 0.004547099, -0.0036133337, 0.57843536, 2.1, 1, -0.81051373, 0.0045043672, -0.003652254, 0.58569086, 2.1333334, 1, -0.8040587, 0.0044555487, -0.0037003087, 0.5945218, 2.1666667, 1, -0.7961729, 0.004400131, -0.0037582007, 0.6050415, 2.2, 1, -0.7860915, 0.004335172, -0.0038308874, 0.6180831, 2.2333333, 1, -0.77202797, 0.0042496463, -0.00392827, 0.63556224, 2.2666667, 1, -0.75327355, 0.004139068, -0.0040509813, 0.657682, 2.3, 1, -0.7300162, 0.0040042386, -0.0041929297, 0.6834053, 2.3333333, 1, -0.7035481, 0.003852392, -0.004342348, 0.71062404, 2.3666666, 1, -0.67641646, 0.0036979865, -0.004483596, 0.73649645, 2.4, 1, -0.6513143, 0.003555722, -0.004604666, 0.75878584, 2.4333334, 1, -0.6302849, 0.0034369156, -0.0046995822, 0.7763421, 2.4666667, 1, -0.6154115, 0.0033530921, -0.004763362, 0.78818446, 2.5, 1, -0.6087138, 0.0033154194, -0.0047912123, 0.7933685)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608398, 0.06599527, -0.0034688246)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.9914148, -0.020339629, 0.007486256, 0.12894534, 0.06666667, 1, 0.9914148, -0.020339629, 0.007486256, 0.12894534, 0.1, 1, 0.99116015, -0.020324798, 0.007526013, 0.13088882, 0.13333334, 1, 0.9903901, -0.020280788, 0.0076426007, 0.13659327, 0.16666667, 1, 0.98878837, -0.02019273, 0.007870024, 0.14774305, 0.2, 1, 0.98598355, -0.02004747, 0.008229554, 0.16542982, 0.23333333, 1, 0.9820323, -0.019856855, 0.008675326, 0.18746491, 0.26666668, 1, 0.97671354, -0.019618148, 0.00919792, 0.2134507, 0.3, 1, 0.96979386, -0.019328486, 0.009787269, 0.24296182, 0.33333334, 1, 0.96104413, -0.018985227, 0.010432732, 0.27554482, 0.36666667, 1, 0.95002794, -0.018578146, 0.0111368, 0.31141257, 0.4, 1, 0.9369078, -0.018118735, 0.011865222, 0.34890497, 0.43333334, 1, 0.9216326, -0.017608656, 0.012605898, 0.3874591, 0.46666667, 1, 0.9042221, -0.017051067, 0.013347093, 0.4265133, 0.56666666, 1, 0.840965, -0.015227618, 0.0154059585, 0.54065603, 0.6, 1, 0.8173244, -0.014553196, 0.01603387, 0.5757709, 0.6333333, 1, 0.7929964, -0.013874946, 0.016615493, 0.60884166, 0.6666667, 1, 0.768365, -0.013201972, 0.017147975, 0.6396458, 0.7, 1, 0.7443532, -0.012557525, 0.017620003, 0.6674355, 0.73333335, 1, 0.720988, -0.0119399335, 0.018040078, 0.6926098, 0.76666665, 1, 0.69859403, -0.011355908, 0.018410236, 0.71519125, 0.8, 1, 0.67745143, -0.010810973, 0.018733261, 0.7352495, 0.8333333, 1, 0.6577896, -0.010309387, 0.019012464, 0.7528912, 0.8666667, 1, 0.64062953, -0.00987549, 0.019240635, 0.76754564, 0.9, 1, 0.62559605, -0.009498099, 0.019429285, 0.7798474, 0.93333334, 1, 0.6129821, -0.009183339, 0.019579863, 0.7898008, 0.96666664, 1, 0.6030776, -0.008937358, 0.019693356, 0.7973893, 1, 1, 0.5961661, -0.008766317, 0.01977016, 0.8025698, 1.0333333, 1, 0.593761, -0.008706914, 0.019796444, 0.80435073, 1.7666667, 1, 0.59315896, -0.008692052, 0.019802988, 0.8047949, 1.8, 1, 0.59425867, -0.008719283, 0.01979111, 0.80398315, 1.8333334, 1, 0.5986465, -0.008828046, 0.019743232, 0.8007214, 1.8666667, 1, 0.61116785, -0.009139434, 0.019602219, 0.7912056, 1.9, 1, 0.6290332, -0.0095864665, 0.019389395, 0.77707744, 1.9333333, 1, 0.65163964, -0.010156982, 0.019099416, 0.75822026, 1.9666667, 1, 0.6782929, -0.010837128, 0.018725578, 0.73447305, 2, 1, 0.7082162, -0.011611378, 0.01826055, 0.7056639, 2.0333333, 1, 0.7414638, -0.01248681, 0.017680341, 0.6706437, 2.0666666, 1, 0.7755563, -0.013403738, 0.017004658, 0.63090706, 2.1, 1, 0.8094301, -0.014338087, 0.016236078, 0.58681655, 2.1333334, 1, 0.84207034, -0.015250866, 0.015391178, 0.5389323, 2.2, 1, 0.8998931, -0.016920876, 0.013523415, 0.43557227, 2.2333333, 1, 0.9234185, -0.017671745, 0.012529204, 0.3831829, 2.2666667, 1, 0.94295454, -0.018331397, 0.011546155, 0.33221596, 2.3, 1, 0.95857817, -0.018895015, 0.010602755, 0.28400442, 2.3333333, 1, 0.9705772, -0.019362547, 0.009727053, 0.23981345, 2.3666666, 1, 0.9789302, -0.019717097, 0.008990699, 0.20304239, 2.4, 1, 0.984704, -0.019985124, 0.0083816275, 0.17288263, 2.4333334, 1, 0.9884429, -0.020174732, 0.0079172505, 0.15003683, 2.4666667, 1, 0.9905847, -0.020291932, 0.0076137814, 0.13517492, 2.5, 1, 0.9914148, -0.020339629, 0.007486256, 0.12894534)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, 0.24962777, -0.008717655, -0.024974115, 0.9679805, 0.06666667, 1, 0.24962777, -0.008717655, -0.024974115, 0.9679805, 0.1, 1, 0.25308734, -0.008766008, -0.02493484, 0.9670824, 0.13333334, 1, 0.26321363, -0.008907151, -0.024817817, 0.96437716, 0.16666667, 1, 0.28291556, -0.009180106, -0.024581304, 0.95878583, 0.2, 1, 0.3140144, -0.009606408, -0.024183765, 0.94906163, 0.23333333, 1, 0.3524974, -0.010125995, -0.023649564, 0.93545914, 0.26666668, 1, 0.39738068, -0.010720404, -0.022964824, 0.91730386, 0.3, 1, 0.44750088, -0.011368519, -0.02211689, 0.8939377, 0.33333334, 1, 0.5015269, -0.012047154, -0.021096498, 0.86480093, 0.36666667, 1, 0.55870956, -0.012740474, -0.019883553, 0.82902706, 0.4, 1, 0.6154966, -0.013400193, -0.018525653, 0.7878078, 0.46666667, 1, 0.72170776, -0.014538764, -0.015478369, 0.6918722, 0.5, 1, 0.768659, -0.01499083, -0.013858398, 0.63933307, 0.53333336, 1, 0.80957794, -0.015350126, -0.012261672, 0.58668375, 0.56666666, 1, 0.84472215, -0.015625369, -0.010712556, 0.53486973, 0.6, 1, 0.87432325, -0.015825707, -0.009239833, 0.484998, 0.6333333, 1, 0.89878434, -0.01596218, -0.007867624, 0.43802977, 0.6666667, 1, 0.91862357, -0.016046697, -0.00661485, 0.39475244, 0.7, 1, 0.93373644, -0.016089778, -0.0055462606, 0.35755652, 0.73333335, 1, 0.9454802, -0.016106136, -0.0046237987, 0.32524827, 0.76666665, 1, 0.9546491, -0.016104635, -0.003829116, 0.29727274, 0.8, 1, 0.9618875, -0.016091323, -0.003141264, 0.2729536, 0.8333333, 1, 0.9677155, -0.016069941, -0.0025373048, 0.25151956, 0.8666667, 1, 0.9720806, -0.016045533, -0.0020476624, 0.23408931, 0.9, 1, 0.97544336, -0.016020725, -0.0016433741, 0.21966133, 0.93333334, 1, 0.97794896, -0.015997974, -0.0013235783, 0.20822614, 0.96666664, 1, 0.9797151, -0.01597923, -0.0010869992, 0.1997545, 1, 1, 0.98083186, -0.015965918, -0.00093198795, 0.19419868, 1.0333333, 1, 0.98118925, -0.015961317, -0.0008814143, 0.1923855, 1.7666667, 1, 0.98127806, -0.015960159, -0.0008687704, 0.19193211, 1.8, 1, 0.98107284, -0.015962321, -0.0008976927, 0.19297789, 1.8333334, 1, 0.98024094, -0.015970794, -0.0010133723, 0.19715889, 1.8666667, 1, 0.97765774, -0.015994303, -0.0013579319, 0.2095896, 1.9, 1, 0.9734859, -0.016024737, -0.0018747195, 0.2281776, 1.9333333, 1, 0.96734047, -0.0160564, -0.0025664873, 0.25295848, 1.9666667, 1, 0.95869553, -0.016081396, -0.0034363628, 0.28395852, 2, 1, 0.9468732, -0.01608935, -0.0044873157, 0.3211732, 2.0333333, 1, 0.93009484, -0.01606485, -0.005789792, 0.36692235, 2.0666666, 1, 0.9079298, -0.015988903, -0.0072798887, 0.41875404, 2.1, 1, 0.87919086, -0.015840614, -0.008948295, 0.47612253, 2.1333334, 1, 0.84264135, -0.015596605, -0.010776762, 0.5381415, 2.1666667, 1, 0.79707325, -0.0152320685, -0.012737016, 0.6035562, 2.2, 1, 0.7401691, -0.014710253, -0.014832543, 0.6720963, 2.2333333, 1, 0.67381555, -0.014033676, -0.016915182, 0.73857266, 2.2666667, 1, 0.6006255, -0.013223054, -0.018871285, 0.7991984, 2.3, 1, 0.5242943, -0.012320759, -0.020609625, 0.85119855, 2.3333333, 1, 0.4493418, -0.011387924, -0.02206805, 0.8930147, 2.3666666, 1, 0.38437736, -0.010546807, -0.023159241, 0.9228253, 2.4, 1, 0.33006427, -0.009822595, -0.023960046, 0.9436033, 2.4333334, 1, 0.2884308, -0.009255352, -0.02450968, 0.95714235, 2.4666667, 1, 0.2611548, -0.008878276, -0.024840856, 0.96493644, 2.5, 1, 0.24962777, -0.008717655, -0.024974115, 0.9679805)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.6087138, -0.0033154439, 0.004791257, 0.7933685, 0.06666667, 1, -0.6087138, -0.0033154439, 0.004791257, 0.7933685, 0.1, 1, -0.60992503, -0.00332308, 0.004786387, 0.7924378, 0.13333334, 1, -0.61344934, -0.0033453053, 0.00477212, 0.7897126, 0.16666667, 1, -0.62026143, -0.0033882943, 0.0047441334, 0.7843736, 0.2, 1, -0.6307998, -0.003454877, 0.0046997447, 0.77592385, 0.23333333, 1, -0.6435845, -0.0035357852, 0.0046440577, 0.7653529, 0.26666668, 1, -0.6582276, -0.003628641, 0.0045776987, 0.7527963, 0.3, 1, -0.6743353, -0.003731024, 0.0045013637, 0.7384021, 0.33333334, 1, -0.69151413, -0.00384051, 0.004415869, 0.7223393, 0.36666667, 1, -0.7095299, -0.003955679, 0.0043213703, 0.70465106, 0.46666667, 1, -0.7622334, -0.0042949263, 0.00401261, 0.6472756, 0.5, 1, -0.7783316, -0.0043993457, 0.003907253, 0.62782586, 0.53333336, 1, -0.7928812, -0.0044940924, 0.0038068695, 0.6093479, 0.56666666, 1, -0.80585885, -0.004578929, 0.00371281, 0.5920785, 0.6, 1, -0.8171295, -0.0046528787, 0.003627354, 0.57642406, 0.6333333, 1, -0.82657903, -0.00471509, 0.0035527828, 0.5627897, 0.6666667, 1, -0.83410543, -0.004764788, 0.0034913395, 0.5515734, 0.7, 1, -0.83884823, -0.004796177, 0.003451634, 0.5443334, 0.73333335, 1, -0.84126496, -0.0048121926, 0.0034310953, 0.5405908, 0.76666665, 1, -0.8417887, -0.004815666, 0.0034266156, 0.5397749, 0.8, 1, -0.8409474, -0.0048100874, 0.0034338054, 0.5410847, 0.8333333, 1, -0.83938557, -0.0047997367, 0.003447085, 0.5435043, 0.8666667, 1, -0.83723587, -0.004785499, 0.0034652217, 0.5468102, 0.9, 1, -0.83492416, -0.004770202, 0.003484542, 0.5503334, 0.93333334, 1, -0.8326439, -0.004755127, 0.0035034202, 0.55377734, 0.96666664, 1, -0.83059365, -0.0047415826, 0.003520243, 0.55684763, 1, 1, -0.8289767, -0.0047309077, 0.0035334122, 0.559252, 1.0333333, 1, -0.8283473, -0.0047267536, 0.0035385154, 0.5601838, 1.9, 1, -0.82737595, -0.0047182934, 0.00354276, 0.5616175, 1.9333333, 1, -0.8264336, -0.0047108335, 0.0035482429, 0.56300336, 1.9666667, 1, -0.824981, -0.004699815, 0.0035575102, 0.5651299, 2, 1, -0.8229093, -0.0046845684, 0.0035714505, 0.56814235, 2.0333333, 1, -0.81979275, -0.004662334, 0.003593416, 0.5726302, 2.0666666, 1, -0.8157071, -0.004633724, 0.003622681, 0.5784352, 2.1, 1, -0.81051326, -0.0045978935, 0.0036600553, 0.58569074, 2.1333334, 1, -0.80405813, -0.0045539173, 0.003706295, 0.5945217, 2.1666667, 1, -0.7961724, -0.0045007784, 0.0037620822, 0.60504144, 2.2, 1, -0.78609097, -0.0044336715, 0.0038322387, 0.6180831, 2.2333333, 1, -0.7720275, -0.00434178, 0.0039274446, 0.63556236, 2.2666667, 1, -0.75327307, -0.0042210375, 0.004048692, 0.657682, 2.3, 1, -0.7300159, -0.004072982, 0.0041899923, 0.6834053, 2.3333333, 1, -0.7035477, -0.0039059187, 0.0043394207, 0.71062404, 2.3666666, 1, -0.6764163, -0.0037359172, 0.004481236, 0.73649645, 2.4, 1, -0.6513142, -0.0035794373, 0.004603047, 0.7587857, 2.4333334, 1, -0.63028485, -0.0034488782, 0.004698717, 0.7763421, 2.4666667, 1, -0.6154115, -0.0033568172, 0.004763103, 0.78818446, 2.5, 1, -0.6087138, -0.0033154439, 0.004791257, 0.7933685)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.345526e-08, -0.98076427, 0.19519576, 1.8277876e-06)
-tracks/31/type = "method"
-tracks/31/imported = false
-tracks/31/enabled = true
-tracks/31/path = NodePath("..")
-tracks/31/interp = 1
-tracks/31/loop_wrap = true
-tracks/31/keys = {
-"times": PackedFloat32Array(1.1),
-"transitions": PackedFloat32Array(1),
-"values": [{
-"args": [],
-"method": &"pickup"
-}]
-}
-
-[sub_resource type="Animation" id="Animation_4w2md"]
-resource_name = "Fall-loop"
-length = 0.8333333
-loop_mode = 1
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, 8.262648e-18, 0.9068298, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.014909217, 1.1919607e-07, 1.7773131e-09, 0.99988884)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.029448895, -1.5216656e-14, 7.0211663e-09, 0.9995663)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -2.8837438e-16, 0.17607887, -0.01087787)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, 0.22300889, -1.1620713e-07, -2.6584726e-08, 0.97481644, 0.1, 1, 0.22426595, -1.1617438e-07, -2.6435076e-08, 0.974528, 0.13333334, 1, 0.22571188, -1.1613643e-07, -2.6262834e-08, 0.9741941, 0.16666667, 1, 0.22759388, -1.1608665e-07, -2.6038492e-08, 0.97375625, 0.2, 1, 0.22982386, -1.1602707e-07, -2.5772358e-08, 0.9732323, 0.26666668, 1, 0.23442562, -1.159021e-07, -2.5222334e-08, 0.97213405, 0.3, 1, 0.23656484, -1.1584305e-07, -2.4966246e-08, 0.9716158, 0.33333334, 1, 0.23844177, -1.1579076e-07, -2.4741341e-08, 0.97115684, 0.36666667, 1, 0.23973763, -1.1575441e-07, -2.4585946e-08, 0.97083783, 0.46666667, 1, 0.23990458, -1.1572693e-07, -2.8780947e-08, 0.97079647, 0.5, 1, 0.23883304, -1.1575705e-07, -2.8909483e-08, 0.97106063, 0.53333336, 1, 0.23725347, -1.1580118e-07, -2.9098857e-08, 0.9714478, 0.56666666, 1, 0.23541918, -1.15852e-07, -2.9318578e-08, 0.97189397, 0.6, 1, 0.23341458, -1.1590702e-07, -2.9558462e-08, 0.97237736, 0.6666667, 1, 0.22923271, -1.16020175e-07, -3.005809e-08, 0.97337174, 0.7, 1, 0.22730993, -1.1607141e-07, -3.0287453e-08, 0.9738225, 0.73333335, 1, 0.22564094, -1.16115515e-07, -3.048637e-08, 0.97421056, 0.76666665, 1, 0.224311, -1.1615043e-07, -3.0644767e-08, 0.9745177, 0.8333333, 1, 0.22300889, -1.1620713e-07, -2.6584726e-08, 0.97481644)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.008212389, -5.0393683e-14, -1.957986e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.444091e-08, 0.54060847, 0.3122278)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.0039396435, 1.19208366e-07, -4.696421e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.983691e-08, 0.085749224, 0.41874632)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.0039396426, 1.1920836e-07, -4.69642e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732144, 0.07649832, -0.02822398)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.50531465, -0.47724745, -0.50417954, 0.5125378)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435968, 0.2230429, -8.365942e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, 0.20515038, 0.7138943, 0.18402466, 0.6437416, 0.033333335, 1, 0.20515038, 0.7138943, 0.18402466, 0.6437416, 0.06666667, 1, 0.20620966, 0.71354866, 0.18522082, 0.64344317, 0.1, 1, 0.20880675, 0.7126922, 0.18815415, 0.64270335, 0.13333334, 1, 0.21300662, 0.7112796, 0.19289942, 0.64148223, 0.16666667, 1, 0.21846297, 0.7093931, 0.19906741, 0.63984966, 0.2, 1, 0.22491369, 0.70708746, 0.20636402, 0.6378519, 0.23333333, 1, 0.23159282, 0.7046133, 0.21392433, 0.63570535, 0.26666668, 1, 0.23816226, 0.7020925, 0.22136585, 0.6335156, 0.3, 1, 0.2442894, 0.6996625, 0.22831114, 0.6314025, 0.33333334, 1, 0.24964674, 0.6974747, 0.23438765, 0.6294982, 0.36666667, 1, 0.2533345, 0.69593424, 0.2385726, 0.6281563, 0.4, 1, 0.25528446, 0.69510823, 0.24078616, 0.62743646, 0.43333334, 1, 0.25541034, 0.6950546, 0.2409291, 0.62738967, 0.46666667, 1, 0.25380906, 0.695734, 0.23911124, 0.6279817, 0.5, 1, 0.25076133, 0.6970122, 0.23565239, 0.62909526, 0.53333336, 1, 0.24625693, 0.6988659, 0.23054238, 0.6307093, 0.56666666, 1, 0.24101058, 0.7009724, 0.22459391, 0.6325419, 0.6, 1, 0.23525909, 0.7032172, 0.21807665, 0.634493, 0.6666667, 1, 0.22320531, 0.70770603, 0.20443113, 0.6383882, 0.7, 1, 0.21764088, 0.70968103, 0.19813783, 0.640099, 0.73333335, 1, 0.2128008, 0.7113496, 0.19266677, 0.64154273, 0.76666665, 1, 0.2089375, 0.71264875, 0.18830183, 0.64266586, 0.8, 1, 0.20630352, 0.7135179, 0.18532686, 0.64341664, 0.8333333, 1, 0.20515038, 0.7138943, 0.18402466, 0.6437416)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.017577728, -0.09135095, 0.027113236, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.020934895, 0.017021656, -0.017671391, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732144, 0.07649832, -0.02822398)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.50531465, 0.47724745, 0.50417954, 0.5125378)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435951, 0.22304288, -8.365756e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, 0.20036232, -0.71542966, -0.17861949, 0.6450663, 0.033333335, 1, 0.20036232, -0.71542966, -0.17861949, 0.6450663, 0.06666667, 1, 0.2021981, -0.71484613, -0.18069154, 0.6445631, 0.1, 1, 0.20669842, -0.71338844, -0.1857728, 0.6433049, 0.13333334, 1, 0.21397091, -0.7109504, -0.19398923, 0.6411975, 0.16666667, 1, 0.22340788, -0.707633, -0.20466027, 0.6383249, 0.2, 1, 0.23454621, -0.7034908, -0.2172691, 0.63473064, 0.23333333, 1, 0.24604699, -0.6989513, -0.23030423, 0.6307836, 0.26666668, 1, 0.25732037, -0.69423735, -0.24309775, 0.62667716, 0.3, 1, 0.26779452, -0.689618, -0.25499892, 0.6226466, 0.33333334, 1, 0.27691582, -0.68540317, -0.26537466, 0.6189641, 0.36666667, 1, 0.28317052, -0.6824076, -0.2724959, 0.61634433, 0.4, 1, 0.28647056, -0.6807922, -0.27625528, 0.6149307, 0.43333334, 1, 0.2866834, -0.68068707, -0.27649784, 0.61483866, 0.46666667, 1, 0.28397423, -0.6820164, -0.27341127, 0.6160021, 0.5, 1, 0.27880856, -0.68450576, -0.26752895, 0.61817956, 0.53333336, 1, 0.27114865, -0.688089, -0.25881302, 0.6213113, 0.56666666, 1, 0.26219475, -0.6921166, -0.24863449, 0.6248275, 0.6, 1, 0.25234377, -0.69635093, -0.23744813, 0.62851924, 0.6666667, 1, 0.2315984, -0.7046112, -0.21393073, 0.63570356, 0.7, 1, 0.22198749, -0.70814353, -0.20305346, 0.63876736, 0.73333335, 1, 0.21361461, -0.71107227, -0.19358653, 0.6413028, 0.76666665, 1, 0.20692444, -0.7133143, -0.18602811, 0.6432407, 0.8, 1, 0.2023606, -0.7147942, -0.18087499, 0.6445182, 0.8333333, 1, 0.20036232, -0.71542966, -0.17861949, 0.6450663)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577725, 0.09135095, -0.027113236, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934898, -0.017021658, 0.017671369, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.27608398, 0.06599515, -0.0034687247)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.8666671, 0.015914718, -0.014623773, 0.49841854)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.7817597, 0.01512382, 0.0133708995, 0.62325305, 0.033333335, 1, 0.7817597, 0.01512382, 0.0133708995, 0.62325305, 0.06666667, 1, 0.7827919, 0.015133007, 0.013331514, 0.6219568, 0.1, 1, 0.78531575, 0.015155379, 0.013234697, 0.61876845, 0.13333334, 1, 0.789377, 0.015191087, 0.013077394, 0.6135816, 0.16666667, 1, 0.79461485, 0.015236603, 0.012871671, 0.6067862, 0.2, 1, 0.8007517, 0.015289136, 0.012626428, 0.5986681, 0.23333333, 1, 0.80704165, 0.015342057, 0.012370174, 0.59016573, 0.26666668, 1, 0.8131635, 0.015392621, 0.012115788, 0.5817056, 0.3, 1, 0.8188149, 0.015438441, 0.011876417, 0.57372713, 0.33333334, 1, 0.82370967, 0.015477435, 0.011665424, 0.56668043, 0.36666667, 1, 0.8270535, 0.015503687, 0.011519261, 0.5617913, 0.4, 1, 0.82881314, 0.0155173745, 0.011441667, 0.5591933, 0.43333334, 1, 0.8289265, 0.015518254, 0.011436648, 0.5590253, 0.46666667, 1, 0.82748234, 0.015507033, 0.0115004, 0.5611599, 0.5, 1, 0.8247225, 0.015485422, 0.0116213355, 0.5652061, 0.53333336, 1, 0.8206177, 0.015452879, 0.011799112, 0.5711467, 0.56666666, 1, 0.8157977, 0.01541408, 0.01200477, 0.57800734, 0.6, 1, 0.81046593, 0.015370452, 0.012228498, 0.5854563, 0.6666667, 1, 0.79913235, 0.015275359, 0.012691595, 0.60082704, 0.7, 1, 0.79382837, 0.015229807, 0.012902766, 0.6078142, 0.73333335, 1, 0.7891784, 0.015189348, 0.013085129, 0.61383677, 0.76666665, 1, 0.7854426, 0.015156498, 0.013229815, 0.6186075, 0.8, 1, 0.7828832, 0.015133819, 0.013328018, 0.6218418, 0.8333333, 1, 0.7817597, 0.01512382, 0.0133708995, 0.62325305)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.6157354, 0.0033574183, -0.0047597843, 0.7879315)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608398, 0.06599527, -0.0034687985)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.98520267, -0.020007102, 0.008317317, 0.17001861, 0.033333335, 1, 0.98520267, -0.020007102, 0.008317317, 0.17001861, 0.06666667, 1, 0.98503715, -0.020198816, 0.0093977265, 0.17089662, 0.1, 1, 0.9846226, -0.020669071, 0.012048757, 0.17304964, 0.13333334, 1, 0.98392576, -0.021430302, 0.016342795, 0.1765328, 0.16666667, 1, 0.98297167, -0.022420675, 0.021934342, 0.18106075, 0.2, 1, 0.9817718, -0.023593657, 0.028564068, 0.18641801, 0.23333333, 1, 0.98044634, -0.024810769, 0.035451576, 0.1919705, 0.26666668, 1, 0.97905916, -0.026010595, 0.042249598, 0.19743785, 0.3, 1, 0.9776897, -0.02713214, 0.04861164, 0.20254278, 0.33333334, 1, 0.97643185, -0.028114878, 0.054192264, 0.20701128, 0.36666667, 1, 0.97553277, -0.028792541, 0.05804377, 0.2100901, 0.4, 1, 0.9750464, -0.029151259, 0.060083598, 0.21171904, 0.43333334, 1, 0.9750147, -0.029174427, 0.060215384, 0.21182425, 0.46666667, 1, 0.9754151, -0.028879814, 0.05853996, 0.2104865, 0.5, 1, 0.97616297, -0.028319588, 0.055355486, 0.20794156, 0.53333336, 1, 0.97723436, -0.02749283, 0.050659243, 0.20418331, 0.56666666, 1, 0.97843176, -0.026531657, 0.045204442, 0.1998102, 0.6, 1, 0.97968245, -0.025480025, 0.039242465, 0.19502088, 0.6666667, 1, 0.9820972, -0.023282785, 0.026806276, 0.18499875, 0.7, 1, 0.98311895, -0.022271344, 0.02109086, 0.18037824, 0.73333335, 1, 0.98396075, -0.02139296, 0.016132075, 0.17636195, 0.76666665, 1, 0.9846013, -0.020692747, 0.012182282, 0.17315798, 0.8, 1, 0.9850223, -0.020215802, 0.009493477, 0.17097445, 0.8333333, 1, 0.98520267, -0.020007102, 0.008317317, 0.17001861)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, -0.0061842776, -0.0049533797, -0.026989248, 0.99960434)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.6157354, -0.0033574435, 0.00475984, 0.7879315)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.34592e-08, -0.98076427, 0.19519576, 1.8277873e-06)
-
-[sub_resource type="Animation" id="Animation_8gm5a"]
-resource_name = "Fall2"
-length = 0.5416667
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, -3.5255984e-16, 0.86128783, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.3361403, 1.12272794e-07, 4.0070983e-08, 0.9418119, 0.033333335, 1, 0.3361403, 1.12272794e-07, 4.0070983e-08, 0.9418119, 0.06666667, 1, 0.36479366, 1.11104015e-07, 3.6138065e-08, 0.93108845, 0.1, 1, 0.43319893, 1.0780424e-07, 2.742676e-08, 0.9012983, 0.13333334, 1, 0.5410176, 1.0075909e-07, 3.078363e-08, 0.8410113, 0.16666667, 1, 0.67479813, 8.797683e-08, 8.044217e-08, 0.73800236, 0.2, 1, 0.8349163, 6.607753e-08, 6.818596e-08, 0.550377, 0.26666668, 1, 0.9999943, 1.0242394e-09, 7.758596e-08, 0.0033851415, 0.3, 1, -0.96564674, 3.1020278e-08, -1.1798414e-07, 0.2598585, 0.33333334, 1, -0.8830051, 5.616367e-08, -1.1943064e-07, 0.46936345, 0.4, 1, -0.6173977, 9.408427e-08, -9.426951e-08, 0.78665113, 0.43333334, 1, -0.44319797, 1.0720086e-07, -7.555852e-08, 0.8964238, 0.46666667, 1, -0.26434427, 1.1594483e-07, -9.697104e-08, 0.96442837, 0.5, 1, -0.11024471, 1.1998777e-07, -1.1408469e-07, 0.9939045, 0.5416667, 1, -0.032219768, 1.1914742e-07, -3.8409684e-09, 0.99948084)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, -0.040281147, -7.458426e-15, -9.6037756e-09, 0.99918836, 0.033333335, 1, -0.040281147, -7.458426e-15, -9.6037756e-09, 0.99918836, 0.06666667, 1, -0.015551566, 1.7421915e-10, -9.621524e-09, 0.9998791, 0.1, 1, 0.031662334, 5.063686e-10, -9.639041e-09, 0.9994987, 0.13333334, 1, 0.085898146, 6.636558e-10, -2.047035e-09, 0.996304, 0.16666667, 1, 0.11871002, 2.2362586e-16, 2.8302672e-08, 0.992929, 0.5416667, 1, 0.11871002, 9.7277245e-15, 2.8302672e-08, 0.992929)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -3.6572162e-16, 0.1515015, -0.011740991)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, -0.26628256, -1.149052e-07, 3.1743355e-08, 0.963895, 0.033333335, 1, -0.26628256, -1.149052e-07, 3.1743355e-08, 0.963895, 0.06666667, 1, -0.18543836, -1.17141674e-07, 2.2105976e-08, 0.9826559, 0.1, 1, -0.024499387, -1.1917348e-07, 2.9205536e-09, 0.99969983, 0.13333334, 1, 0.16245498, -1.1762568e-07, -1.9366139e-08, 0.98671603, 0.16666667, 1, 0.27045596, -1.147666e-07, -3.2240848e-08, 0.96273243, 0.5416667, 1, 0.27045596, -1.1476658e-07, -3.224085e-08, 0.96273243)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.008212382, -4.5612623e-14, -1.9579842e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.4440926e-08, 0.5406087, 0.31222785)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939637, 1.19208366e-07, -4.696413e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.979069e-08, 0.06115723, 0.41855246)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939637, 1.1920836e-07, -4.696413e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732147, 0.076497965, -0.028223995)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.14296252, -0.124028794, -0.6830659, 0.7054074, 0.033333335, 1, -0.14296252, -0.124028794, -0.6830659, 0.7054074, 0.06666667, 1, -0.087574854, -0.07109008, -0.69138783, 0.71362436, 0.1, 1, 0.018728314, 0.031009262, -0.69478315, 0.7183065, 0.13333334, 1, 0.13995701, 0.14780095, -0.6791919, 0.70517045, 0.16666667, 1, 0.21151301, 0.21746859, -0.6592948, 0.6879681, 0.5416667, 1, 0.21151294, 0.21746854, -0.65929484, 0.6879681)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435868, 0.22304307, -8.355929e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, 0.4757424, 0.5314648, 0.38833487, 0.583447, 0.4, 1, 0.4757424, 0.5314648, 0.38833487, 0.583447, 0.43333334, 1, 0.43611738, 0.5644354, 0.3451839, 0.6099692, 0.46666667, 1, 0.3279343, 0.6334389, 0.23031422, 0.66194385, 0.5, 1, 0.13531812, 0.700339, 0.033621773, 0.70006, 0.53333336, 1, 0.033012956, 0.71252775, -0.06758233, 0.69760096, 0.5416667, 1, 0.0072450624, 0.71325535, -0.09274517, 0.69470334)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.017577726, -0.09135095, 0.02711323, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.020934897, 0.01702166, -0.017671373, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732147, 0.076497965, -0.028223995)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.14296252, 0.124028794, 0.6830659, 0.7054074, 0.033333335, 1, -0.14296252, 0.124028794, 0.6830659, 0.7054074, 0.06666667, 1, -0.087574854, 0.07109008, 0.69138783, 0.71362436, 0.1, 1, 0.01872831, -0.031009264, 0.69478315, 0.7183065, 0.13333334, 1, 0.13995701, -0.14780095, 0.6791919, 0.70517045, 0.16666667, 1, 0.21151301, -0.21746859, 0.6592948, 0.6879681, 0.5416667, 1, 0.21151294, -0.21746854, 0.65929484, 0.6879681)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435868, 0.22304307, -8.355929e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, 0.4757424, -0.5314648, -0.38833487, 0.583447, 0.4, 1, 0.47574234, -0.5314648, -0.38833487, 0.583447, 0.43333334, 1, 0.43611735, -0.5644354, -0.3451839, 0.6099692, 0.46666667, 1, 0.3279343, -0.6334389, -0.23031422, 0.6619439, 0.5, 1, 0.13531813, -0.70033896, -0.033621803, 0.70005995, 0.53333336, 1, 0.033012956, -0.71252775, 0.06758233, 0.6976009, 0.5416667, 1, 0.0072450624, -0.71325535, 0.09274517, 0.69470334)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577728, 0.091350965, -0.027113233, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934893, -0.017021656, 0.01767137, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.27608398, 0.06599509, -0.0034686744)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, -0.98291206, -0.028672548, 0.17575793, 0.04659399, 0.033333335, 1, -0.98291206, -0.028672548, 0.17575793, 0.04659399, 0.06666667, 1, -0.9830053, -0.027166799, 0.15923406, -0.08721883, 0.1, 1, 0.9277843, 0.022600109, -0.116732255, 0.35366553, 0.13333334, 1, 0.7736817, 0.0150856385, -0.05578494, 0.63093346, 0.16666667, 1, 0.652041, 0.010162921, -0.019061517, 0.75787586, 0.4, 1, 0.6520409, 0.010162922, -0.019061517, 0.757876, 0.43333334, 1, 0.7131395, 0.011737302, -0.018148245, 0.700689, 0.46666667, 1, 0.84399986, 0.015298594, -0.015310485, 0.53590643, 0.5, 1, 0.97464114, 0.019529745, -0.009372518, 0.22272307, 0.5416667, 1, 0.99964744, 0.021063607, -0.0051235585, 0.0153314425)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, -0.0061842515, 0.0049533867, 0.026989238, 0.99960434, 0.033333335, 1, -0.0061842515, 0.0049533867, 0.026989238, 0.99960434, 0.06666667, 1, 0.12309134, 0.006878732, 0.026218433, 0.99202514, 0.1, 1, 0.3774071, 0.010406717, 0.023315141, 0.9256954, 0.13333334, 1, 0.64113545, 0.013611792, 0.017881908, 0.76709867, 0.16666667, 1, 0.7635159, 0.014856514, 0.014064934, 0.6454649, 0.4, 1, 0.7635159, 0.014856514, 0.014064935, 0.6454649, 0.43333334, 1, 0.70495546, 0.014287453, 0.016033232, 0.70892644, 0.46666667, 1, 0.5347309, 0.01238733, 0.020440781, 0.84468424, 0.5, 1, 0.20826085, 0.008100127, 0.02545913, 0.9777084, 0.53333336, 1, 0.037003424, 0.0056060366, 0.026782122, 0.99894047, 0.5416667, 1, -0.00618425, 0.004953394, 0.026989238, 0.99960434)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, 0.034755703, -0.00044480208, -0.0058323527, 0.99937874, 0.4, 1, 0.034755703, -0.00044480382, -0.005832357, 0.99937874, 0.43333334, 1, 0.007725915, -0.000290045, -0.0058431244, 0.99995303, 0.46666667, 1, -0.058115996, 8.7684246e-05, -0.00585147, 0.9982927, 0.5, 1, -0.15875317, 0.0006671086, -0.0058148084, 0.987301, 0.5416667, 1, -0.22507437, 0.001050366, -0.0057571344, 0.97432405)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608395, 0.06599515, -0.0034688094)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.9785101, -0.0018029467, 0.108257145, 0.17548588, 0.033333335, 1, 0.9785101, -0.0018029467, 0.108257145, 0.17548588, 0.06666667, 1, 0.95618355, -0.003218773, 0.09712547, 0.27616912, 0.1, 1, 0.88189334, -0.0059272377, 0.07208881, 0.46586722, 0.13333334, 1, 0.74898124, -0.008762611, 0.039207485, 0.6613722, 0.16666667, 1, 0.6520409, -0.010163177, 0.01906203, 0.757876, 0.4, 1, 0.652041, -0.01016318, 0.019062027, 0.75787586, 0.43333334, 1, 0.7131395, -0.011737488, 0.018148746, 0.700689, 0.46666667, 1, 0.84399986, -0.015298681, 0.015310892, 0.53590643, 0.5, 1, 0.97464114, -0.019529888, 0.009372615, 0.22272304, 0.5416667, 1, 0.99964744, -0.021063602, 0.005123568, 0.015331441)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, 0.22242075, -0.008664698, 0.021864852, 0.9746671, 0.033333335, 1, 0.22242075, -0.008664698, 0.021864852, 0.9746671, 0.06666667, 1, 0.3462975, -0.016452814, 0.022961488, 0.93769944, 0.1, 1, 0.5782422, -0.031308837, 0.023991004, 0.81491125, 0.13333334, 1, 0.79824746, -0.045902416, 0.023088688, 0.60013413, 0.16666667, 1, 0.88936085, -0.05223994, 0.021616314, 0.45369712, 0.4, 1, 0.8893609, -0.052239954, 0.021616315, 0.45369714, 0.43333334, 1, 0.83669114, -0.049773477, 0.017169569, 0.54513836, 0.46666667, 1, 0.6582897, -0.0408718, 0.004870561, 0.7516386, 0.5, 1, 0.25460747, -0.019392196, -0.01620946, 0.96671414, 0.53333336, 1, 0.046526615, -0.007901518, -0.0249608, 0.9985739, 0.5416667, 1, -0.0061842757, -0.0049533797, -0.026989248, 0.99960434)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, 0.034755606, 0.00044504632, 0.0058322623, 0.99937874, 0.4, 1, 0.03475565, 0.00044504774, 0.0058322567, 0.99937874, 0.43333334, 1, 0.0077258814, 0.00029027282, 0.0058430256, 0.99995303, 0.46666667, 1, -0.05811605, -8.749041e-05, 0.005851378, 0.9982927, 0.5, 1, -0.15875319, -0.0006669695, 0.005814728, 0.987301, 0.5416667, 1, -0.2250744, -0.0010502654, 0.00575706, 0.97432405)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.363865e-08, -0.98076427, 0.19519576, 1.8287006e-06)
-
-[sub_resource type="Animation" id="Animation_vmng1"]
-resource_name = "GroundSlide"
-length = 0.041666668
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, -2.1378197e-16, 0.45035523, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, -0.08513875, 0.6991121, -0.10603003, 0.7019624)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.16469905, 0.04146031, -0.044853132, 0.9844509)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, 1.4901165e-08, 0.17607911, -0.010877882)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, 0.014661303, -0.22484665, 0.039734524, 0.97347337)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.008212378, -1.4900705e-08, -2.0803583e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 8.934208e-08, 0.5406087, 0.31222782)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939636, 1.1175784e-07, -4.9899374e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 1.14738064e-07, 0.08574934, 0.4187463)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939639, 1.1920836e-07, -4.6964155e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732144, 0.0764982, -0.028223956)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.50531465, -0.47724745, -0.50417954, 0.5125378)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435947, 0.22304305, -8.3657666e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, 0.16719651, 0.72488046, 0.14125101, 0.65317833)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, 0.46970814, -0.06655456, 0.06819537, 0.877664)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.020934902, 0.017021656, -0.017671369, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.2673215, 0.0764982, -0.028224017)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.5053147, 0.4772475, 0.5041796, 0.5125377)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009436009, 0.22304311, -8.3664556e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, -0.35865533, -0.651457, 0.35795948, 0.5646549)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, 0.36424488, 0.07404734, -0.05997601, 0.92641544)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934911, -0.017021645, 0.017671376, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.276084, 0.06599518, -0.0034687202)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.5476743, 0.17483693, -0.033058766, 0.8175525)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.85994565, 0.015806697, 0.009957945, 0.5100436)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.5833524, 0.016259857, -0.037341587, 0.8111975)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608395, 0.06599524, -0.0034688131)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.9203205, -0.3907106, 0.010159706, 0.015882004)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, -0.006184278, -0.0049533797, -0.026989233, 0.99960434)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.4266454, 0.25283802, -0.43233952, 0.75307983)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.366195e-08, -0.98076427, 0.19519576, 1.8282212e-06)
-
-[sub_resource type="Animation" id="Animation_8wq35"]
-resource_name = "Idle-loop"
-length = 1.4583334
-loop_mode = 1
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, -3.5255984e-16, 0.86128783, 0.029855084, 0.53333336, 1, -3.5255984e-16, 0.86128783, 0.029855084, 0.56666666, 1, -8.61063e-17, 0.86286175, 0.029855084, 0.93333334, 1, 4.434488e-17, 0.885946, 0.029855084, 0.96666664, 1, 3.601821e-17, 0.8875199, 0.029855084, 1.0666667, 1, 3.601821e-17, 0.8875199, 0.029855084, 1.1, 1, 4.1569326e-17, 0.8840223, 0.029855084, 1.2, 1, -3.5811095e-16, 0.86303663, 0.029855084, 1.2333333, 1, -3.5255984e-16, 0.86128783, 0.029855084, 1.4583334, 1, -3.5255984e-16, 0.86128783, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.014909217, 1.1919607e-07, 1.7773131e-09, 0.99988884)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.029448895, -1.5216656e-14, 7.0211663e-09, 0.9995663)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -3.6572172e-16, 0.15150139, -0.011741015, 0.53333336, 1, -3.6572172e-16, 0.15150139, -0.011741015, 0.56666666, 1, -3.562917e-16, 0.15274325, -0.011697403, 0.93333334, 1, -3.0472159e-16, 0.17095645, -0.011057768, 0.96666664, 1, -3.0049574e-16, 0.17219831, -0.011014151, 1.1, 1, 4.118945e-16, 0.17271571, -0.010995986, 1.2, 1, -1.7102112e-15, 0.17582016, -0.010886956, 1.2333333, 1, -2.0721191e-15, 0.1736211, -0.010964184, 1.4583334, 1, -3.6572172e-16, 0.15150139, -0.011741015)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, 0.00540027, -1.1920752e-07, -6.4376265e-10, 0.9999854)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.008212382, -4.9427408e-14, -1.9579842e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.444091e-08, 0.5406087, 0.3122278)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.97907e-08, 0.06115723, 0.4185525, 0.53333336, 1, 9.97907e-08, 0.06115723, 0.4185525, 0.56666666, 1, 9.979573e-08, 0.06383435, 0.4185736, 0.93333334, 1, 9.986949e-08, 0.10309817, 0.41888297, 0.96666664, 1, 9.987452e-08, 0.10577529, 0.41890407, 1.0666667, 1, 9.987452e-08, 0.10577529, 0.41890407, 1.1, 1, 9.98695e-08, 0.10310513, 0.41888303, 1.2, 1, 9.9839404e-08, 0.0870844, 0.41875678, 1.2333333, 1, 9.983228e-08, 0.083290085, 0.4187269, 1.4583334, 1, 9.97907e-08, 0.06115723, 0.4185525)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939635, 1.1920836e-07, -4.696411e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732144, 0.0764982, -0.028223984, 0.53333336, 1, 0.26732144, 0.0764982, -0.028223984, 0.56666666, 1, 0.2673214, 0.07875019, -0.028144896, 0.93333334, 1, 0.26732144, 0.11177941, -0.026984932, 0.96666664, 1, 0.26732144, 0.1140314, -0.026905844, 1.0666667, 1, 0.26732144, 0.1140314, -0.026905844, 1.1, 1, 0.26732144, 0.10902699, -0.027081596, 1.2, 1, 0.26732144, 0.07900043, -0.028136108, 1.2333333, 1, 0.26732144, 0.0764982, -0.028223984, 1.4583334, 1, 0.26732144, 0.0764982, -0.028223984)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.5230665, -0.45780188, -0.52192616, 0.49438047, 0.53333336, 1, -0.5230665, -0.45780188, -0.52192616, 0.49438047, 0.56666666, 1, -0.5221871, -0.45879805, -0.521045, 0.49531522, 0.7, 1, -0.5174654, -0.4640866, -0.51631725, 0.5002697, 0.73333335, 1, -0.51627666, -0.46540233, -0.5151279, 0.5015001, 0.76666665, 1, -0.51508456, -0.46671548, -0.5139357, 0.5027273, 0.8333333, 1, -0.5126905, -0.46933395, -0.51154256, 0.5051717, 0.93333334, 1, -0.5090745, -0.47324207, -0.50793105, 0.50881344, 0.96666664, 1, -0.50816596, -0.4742154, -0.5070241, 0.5097193, 1.1, 1, -0.5077868, -0.47462055, -0.5066458, 0.5100962, 1.1333333, 1, -0.50702757, -0.47543, -0.5058882, 0.5108489, 1.1666666, 1, -0.50626713, -0.4762384, -0.50512946, 0.5116003, 1.2, 1, -0.5055053, -0.4770458, -0.5043697, 0.5123505, 1.2333333, 1, -0.5071224, -0.475329, -0.50598305, 0.5107547, 1.4583334, 1, -0.5230665, -0.45780188, -0.52192616, 0.49438047)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435951, 0.22304288, -8.365756e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, -0.35644147, 0.617847, -0.43310678, 0.5510291, 0.53333336, 1, -0.3564415, 0.617847, -0.43310678, 0.5510291, 0.56666666, 1, -0.35841507, 0.616403, -0.43520808, 0.54970896, 0.93333334, 1, -0.38686317, 0.5943613, -0.46542108, 0.52957976, 0.96666664, 1, -0.388766, 0.592802, -0.46743673, 0.528157, 1.0666667, 1, -0.38876596, 0.592802, -0.46743673, 0.528157, 1.1, 1, -0.38453, 0.59625834, -0.46294862, 0.53131086, 1.2, 1, -0.3586348, 0.6162416, -0.43544197, 0.5495615, 1.2333333, 1, -0.3564415, 0.617847, -0.43310687, 0.5510291, 1.4583334, 1, -0.35644147, 0.617847, -0.43310678, 0.5510291)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.017577726, -0.09135095, 0.027113229, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.020934898, 0.017021658, -0.017671391, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732144, 0.0764982, -0.028223984, 0.53333336, 1, -0.26732144, 0.0764982, -0.028223984, 0.56666666, 1, -0.2673214, 0.07875019, -0.028144896, 0.93333334, 1, -0.26732144, 0.11177941, -0.026984932, 0.96666664, 1, -0.26732144, 0.1140314, -0.026905844, 1.0666667, 1, -0.26732144, 0.1140314, -0.026905844, 1.1, 1, -0.26732144, 0.10902699, -0.027081596, 1.2, 1, -0.26732144, 0.07900043, -0.028136108, 1.2333333, 1, -0.26732144, 0.0764982, -0.028223984, 1.4583334, 1, -0.26732144, 0.0764982, -0.028223984)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.5230665, 0.45780188, 0.52192616, 0.49438047, 0.53333336, 1, -0.5230665, 0.45780188, 0.52192616, 0.49438047, 0.56666666, 1, -0.5221871, 0.45879805, 0.521045, 0.49531522, 0.7, 1, -0.5174654, 0.4640866, 0.51631725, 0.5002697, 0.73333335, 1, -0.51627666, 0.46540233, 0.5151279, 0.5015001, 0.76666665, 1, -0.51508456, 0.46671548, 0.5139357, 0.5027273, 0.8333333, 1, -0.5126905, 0.46933395, 0.51154256, 0.5051717, 0.93333334, 1, -0.5090745, 0.47324207, 0.50793105, 0.50881344, 0.96666664, 1, -0.50816596, 0.4742154, 0.5070241, 0.5097193, 1.1, 1, -0.5077868, 0.47462055, 0.5066458, 0.5100962, 1.1333333, 1, -0.50702757, 0.47543, 0.5058882, 0.5108489, 1.1666666, 1, -0.50626713, 0.4762384, 0.50512946, 0.5116003, 1.2, 1, -0.5055053, 0.4770458, 0.5043697, 0.5123505, 1.2333333, 1, -0.5071224, 0.475329, 0.50598305, 0.5107547, 1.4583334, 1, -0.5230665, 0.45780188, 0.52192616, 0.49438047)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435951, 0.22304288, -8.365756e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, -0.35644147, -0.617847, 0.43310678, 0.5510291, 0.53333336, 1, -0.3564415, -0.617847, 0.43310678, 0.5510291, 0.56666666, 1, -0.3584152, -0.61640286, 0.43520808, 0.549709, 0.93333334, 1, -0.3868631, -0.5943614, 0.46542108, 0.5295798, 0.96666664, 1, -0.38876587, -0.59280205, 0.4674367, 0.52815706, 1.0666667, 1, -0.38876587, -0.59280205, 0.4674367, 0.52815706, 1.1, 1, -0.38452995, -0.5962584, 0.46294862, 0.5313109, 1.2, 1, -0.3586348, -0.6162416, 0.4354419, 0.5495615, 1.2333333, 1, -0.35644147, -0.617847, 0.43310675, 0.5510291, 1.4583334, 1, -0.35644147, -0.617847, 0.43310678, 0.5510291)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577725, 0.09135095, -0.027113236, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934897, -0.017021656, 0.017671384, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.276084, 0.06599521, -0.0034686895)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.9914148, 0.020339562, -0.007486248, 0.12894534, 0.53333336, 1, 0.9914149, 0.020339565, -0.007486249, 0.12894535, 0.56666666, 1, 0.9922723, 0.020390477, -0.007347116, 0.12217165, 0.93333334, 1, 0.99951947, 0.021027852, -0.005267253, 0.022157969, 0.96666664, 1, 0.9996475, 0.021063605, -0.0051235585, 0.015331443, 1.0666667, 1, 0.99964744, 0.021063605, -0.0051235585, 0.015331443, 1.1, 1, 0.9992992, 0.020982733, -0.005442907, 0.030516438, 1.2, 1, 0.9923657, 0.02039614, -0.0073314775, 0.12141057, 1.2333333, 1, 0.9914149, 0.020339563, -0.0074862484, 0.12894535, 1.4583334, 1, 0.9914148, 0.020339562, -0.007486248, 0.12894534)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.24962775, 0.008717708, 0.024974002, 0.96798056, 0.53333336, 1, 0.24962775, 0.008717708, 0.024974002, 0.96798056, 0.56666666, 1, 0.23470156, 0.008507558, 0.02514228, 0.9717051, 0.8666667, 1, 0.050389726, 0.005814905, 0.026698522, 0.99835587, 0.9, 1, 0.029763343, 0.0055026794, 0.026814524, 0.9991821, 0.93333334, 1, 0.009200018, 0.005189267, 0.026918732, 0.9995818, 0.96666664, 1, -0.006184251, 0.004953394, 0.026989235, 0.9996043, 1.0666667, 1, -0.006184252, 0.004953394, 0.026989238, 0.99960434, 1.1, 1, 0.028190304, 0.0054787793, 0.026822899, 0.9992277, 1.2, 1, 0.2329469, 0.008482774, 0.02516163, 0.97212696, 1.2333333, 1, 0.24962774, 0.008717708, 0.024974002, 0.96798056, 1.4583334, 1, 0.24962775, 0.008717708, 0.024974002, 0.96798056)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.6087138, 0.0033154194, -0.0047912123, 0.7933685, 0.53333336, 1, -0.6087138, 0.0033154194, -0.0047912123, 0.7933685, 0.56666666, 1, -0.60208535, 0.0032755802, -0.004819153, 0.79841053, 0.93333334, 1, -0.50003713, 0.0026649819, -0.0051904623, 0.8659844, 0.96666664, 1, -0.49280763, 0.0026219035, -0.005212944, 0.8701188, 1.0666667, 1, -0.49280763, 0.0026219033, -0.005212944, 0.87011874, 1.1, 1, -0.5088516, 0.0027175352, -0.0051624123, 0.8608346, 1.2, 1, -0.60133445, 0.0032710687, -0.0048222863, 0.7989763, 1.2333333, 1, -0.6087138, 0.0033154194, -0.0047912123, 0.7933685, 1.4583334, 1, -0.6087138, 0.0033154194, -0.0047912123, 0.7933685)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608398, 0.06599527, -0.0034688246)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.9914148, -0.020339629, 0.007486256, 0.12894534, 0.53333336, 1, 0.9914149, -0.020339629, 0.007486257, 0.12894535, 0.56666666, 1, 0.99227226, -0.020390542, 0.0073471246, 0.122171625, 0.93333334, 1, 0.99951947, -0.021027852, 0.0052672625, 0.022157969, 0.96666664, 1, 0.9996475, -0.021063603, 0.0051235687, 0.015331443, 1.0666667, 1, 0.99964744, -0.021063602, 0.0051235682, 0.015331443, 1.1, 1, 0.9992992, -0.02098274, 0.0054429164, 0.030516438, 1.2, 1, 0.9923657, -0.020396203, 0.0073314863, 0.12141057, 1.2333333, 1, 0.9914149, -0.02033963, 0.007486257, 0.12894535, 1.4583334, 1, 0.9914148, -0.020339629, 0.007486256, 0.12894534)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, 0.24962777, -0.008717655, -0.024974115, 0.9679805, 0.53333336, 1, 0.24962777, -0.008717655, -0.024974115, 0.9679805, 0.56666666, 1, 0.23470156, -0.008507507, -0.025142383, 0.971705, 0.8666667, 1, 0.05038972, -0.005814881, -0.026698554, 0.99835587, 0.9, 1, 0.029763322, -0.005502659, -0.026814546, 0.9991821, 0.93333334, 1, 0.009199989, -0.00518925, -0.026918747, 0.9995818, 0.96666664, 1, -0.006184277, -0.0049533793, -0.026989244, 0.9996043, 1.0666667, 1, -0.006184278, -0.0049533797, -0.026989248, 0.99960434, 1.1, 1, 0.028190285, -0.0054787598, -0.026822923, 0.9992277, 1.2, 1, 0.2329469, -0.008482723, -0.025161732, 0.9721269, 1.2333333, 1, 0.24962777, -0.008717655, -0.024974115, 0.9679805, 1.4583334, 1, 0.24962777, -0.008717655, -0.024974115, 0.9679805)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.6087138, -0.0033154439, 0.004791257, 0.7933685, 0.53333336, 1, -0.6087138, -0.0033154439, 0.004791257, 0.7933685, 0.56666666, 1, -0.60208535, -0.003275603, 0.004819195, 0.79841053, 0.93333334, 1, -0.5000372, -0.0026649856, 0.005190468, 0.8659843, 0.96666664, 1, -0.49280772, -0.0026219054, 0.0052129477, 0.87011874, 1.0666667, 1, -0.49280775, -0.0026219056, 0.0052129477, 0.87011874, 1.1, 1, -0.5088516, -0.0027175404, 0.0051624216, 0.8608346, 1.2, 1, -0.6013344, -0.0032710917, 0.0048223287, 0.7989763, 1.2333333, 1, -0.6087138, -0.0033154439, 0.004791257, 0.7933685, 1.4583334, 1, -0.6087138, -0.0033154439, 0.004791257, 0.7933685)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.345526e-08, -0.98076427, 0.19519576, 1.8277876e-06)
-
-[sub_resource type="Animation" id="Animation_8ggfx"]
-resource_name = "Jump"
-length = 0.5
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, 8.262648e-18, 0.9068298, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.014909217, 1.1919607e-07, 1.7773131e-09, 0.99988884)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, -0.043985315, 0.20582266, -0.027685057, 0.9772082)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -2.8837438e-16, 0.17607887, -0.01087787)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, -0.068927854, -0.21698947, -0.021374147, 0.9735028)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.008212389, -5.792348e-10, -2.3119988e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.630356e-08, 0.54060894, 0.3122278)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.0039396435, 1.18656764e-07, -8.210637e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.890558e-08, 0.08574958, 0.4187463)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.0039396426, 1.17696345e-07, -8.248474e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732144, 0.0764982, -0.028223986)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.109697916, -0.091833636, -0.6881343, 0.7113397)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009436084, 0.22304288, -8.367177e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, -0.34372842, 0.6594548, -0.3450093, 0.5726594)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, 0.23621511, -0.08144723, 0.049461834, 0.96701723)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.3290232, -0.06537503, -0.05169043, 0.940637)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732144, 0.0764982, -0.028223986)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, 0.69086313, -0.6636652, 0.19729972, 0.20815751)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435847, 0.22304289, -8.364649e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, -0.36031657, -0.6540689, 0.41849035, 0.5169445)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, 0.37653837, 0.059514288, -0.022063365, 0.9242241)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.0209349, -0.017021654, 0.017671373, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.27608398, 0.06599515, -0.0034687321)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.6687599, 0.010595652, -0.018858617, 0.74316376)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.7879306, 0.015136864, 0.01308933, 0.6154388)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.4361496, 0.0022748008, -0.0053592985, 0.8998554)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608398, 0.06599527, -0.0034687947)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, -0.9908752, 0.021598771, -0.0019309307, 0.13302681)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, 0.2630357, -0.008906655, -0.024817241, 0.9644258)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.10279692, -0.0003433643, 0.0058446517, 0.9946852)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.345675e-08, -0.98076427, 0.19519576, 1.8277866e-06)
-
-[sub_resource type="Animation" id="Animation_3uju1"]
-resource_name = "Jump2"
-length = 0.20833333
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, -3.5255984e-16, 0.86128783, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.014909217, 1.1919607e-07, 1.7773131e-09, 0.99988884)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, -0.040281154, -1.8958568e-14, -9.6037756e-09, 0.99918836)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -3.6572183e-16, 0.15150127, -0.0117410235)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, -0.17830786, -1.172989e-07, 2.1255953e-08, 0.98397475)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.0082123745, -4.5079178e-14, -1.9579807e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.444091e-08, 0.5406087, 0.3122278)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.9790704e-08, 0.061157588, 0.41855252)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939635, 1.1920836e-07, -4.696411e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732147, 0.0764982, -0.028224003)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.40822673, -0.3386472, -0.6060591, 0.5927575)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435951, 0.22304285, -8.3777384e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, -0.4255274, 0.5724613, -0.49394768, 0.49722254)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.017577725, -0.09135095, 0.027113235, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.020934897, 0.017021658, -0.017671378, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732147, 0.0764982, -0.028224003)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.40822673, 0.3386472, 0.6060591, 0.5927575)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435951, 0.22304285, -8.3777384e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, -0.42552736, -0.5724613, 0.49394768, 0.49722254)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577725, 0.09135095, -0.027113238, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934904, -0.017021656, 0.017671378, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.276084, 0.06599521, -0.0034686895)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.983949, 0.01825712, -0.17714365, 0.011452232)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, -0.0061842515, 0.004953388, 0.026989238, 0.99960434)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, 0.03475563, -0.00044481579, -0.005832363, 0.99937874)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608398, 0.06599527, -0.0034688246)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.994035, -0.019486463, 0.106504306, 0.013101434)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, -0.006184277, -0.0049533774, -0.026989248, 0.99960434)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, 0.034755684, 0.00044505406, 0.00583226, 0.99937874)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.2980725e-08, -0.98076427, 0.19519576, 1.8277991e-06)
-
-[sub_resource type="Animation" id="Animation_5fqsh"]
-resource_name = "Jump3"
-length = 0.7916667
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, -3.5255984e-16, 0.86128783, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.014909217, 1.1919607e-07, 1.7773131e-09, 0.99988884, 0.16666667, 1, 0.014909219, 1.1919609e-07, 1.7773133e-09, 0.99988896, 0.23333333, 1, 0.13370335, 1.18138985e-07, 1.5938683e-08, 0.9910214, 0.26666668, 1, 0.2698172, 1.1478802e-07, 3.2164724e-08, 0.9629116, 0.3, 1, 0.48330086, 1.04362314e-07, 5.761396e-08, 0.8754544, 0.33333334, 1, 0.6986415, 8.529092e-08, 8.3284554e-08, 0.715472, 0.36666667, 1, 0.8406299, 6.509826e-08, 6.44458e-08, 0.54161, 0.4, 1, 0.93246704, 4.393593e-08, 5.274975e-08, 0.3612552, 0.46666667, 1, 0.999763, -2.6065323e-09, 1.199436e-07, -0.021770112, 0.5, 1, -0.97512126, 2.6481684e-08, -1.2001739e-07, 0.22167264, 0.53333336, 1, -0.9138672, 4.862237e-08, -1.238157e-07, 0.40601358, 0.56666666, 1, -0.8220295, 6.839814e-08, -1.3253668e-07, 0.569445, 0.6, 1, -0.6995502, 8.5594586e-08, -1.108683e-07, 0.71458346, 0.6333333, 1, -0.5384507, 1.0045252e-07, -6.4188434e-08, 0.84265697, 0.6666667, 1, -0.32280058, 1.1282764e-07, -3.8480916e-08, 0.946467, 0.7, 1, -0.066778, 1.1894319e-07, -7.960641e-09, 0.99776787, 0.73333335, 1, 0.14784606, 1.17899205e-07, 1.7624556e-08, 0.98901045, 0.76666665, 1, 0.2821758, 1.14364965e-07, 3.3637914e-08, 0.9593628, 0.7916667, 1, 0.3361403, 1.12272794e-07, 4.0070983e-08, 0.9418119)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, -0.040281154, -1.8958568e-14, -9.6037756e-09, 0.99918836)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -3.6572183e-16, 0.15150127, -0.0117410235)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, -0.17830786, -1.172989e-07, 2.1255953e-08, 0.98397475, 0.23333333, 1, -0.17830788, -1.17298896e-07, 2.1255957e-08, 0.9839748, 0.26666668, 1, -0.19604215, -1.1687341e-07, 1.9174898e-08, 0.9805955, 0.33333334, 1, -0.26628256, -1.149052e-07, 3.174336e-08, 0.963895, 0.7916667, 1, -0.26628256, -1.149052e-07, 3.1743355e-08, 0.963895)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.0082123745, -4.5079178e-14, -1.9579807e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.444091e-08, 0.5406087, 0.3122278)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.9790704e-08, 0.061157588, 0.41855252)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939635, 1.1920836e-07, -4.696411e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732147, 0.0764982, -0.028224003)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.50531465, -0.47724745, -0.50417954, 0.5125378, 0.16666667, 1, -0.50531465, -0.47724745, -0.50417954, 0.5125378, 0.2, 1, -0.3727505, -0.35055047, -0.6012531, 0.61373144, 0.23333333, 1, -0.22261931, -0.20284599, -0.66473174, 0.68368554, 0.26666668, 1, -0.14296252, -0.12402879, -0.6830659, 0.7054074, 0.7916667, 1, -0.14296252, -0.124028794, -0.6830659, 0.7054074)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.00943597, 0.22304308, -8.3540406e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, 0.33864194, 0.6277796, 0.24152003, 0.657938, 0.16666667, 1, 0.33864197, 0.62777966, 0.24152006, 0.65793806, 0.23333333, 1, 0.45019865, 0.55326927, 0.3604412, 0.60107946, 0.26666668, 1, 0.47574237, 0.5314648, 0.38833487, 0.583447, 0.33333334, 1, 0.4757424, 0.5314648, 0.38833487, 0.583447, 0.36666667, 1, 0.42381287, 0.57373196, 0.33191633, 0.6172892, 0.4, 1, 0.3284142, 0.63319016, 0.23081583, 0.6617692, 0.43333334, 1, 0.23921509, 0.6719835, 0.13863602, 0.6870185, 0.46666667, 1, 0.19133464, 0.68715113, 0.08994691, 0.6950712, 0.6, 1, 0.19133464, 0.68715113, 0.0899469, 0.69507116, 0.6333333, 1, 0.20096098, 0.6843978, 0.099694416, 0.6937402, 0.6666667, 1, 0.23905173, 0.6720416, 0.13846897, 0.6870523, 0.73333335, 1, 0.40057832, 0.5901886, 0.3070167, 0.63004375, 0.76666665, 1, 0.45294672, 0.55102175, 0.36342835, 0.5992781, 0.7916667, 1, 0.4757424, 0.5314648, 0.38833487, 0.583447)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.01757772, -0.09135095, 0.027113259, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.020934904, 0.017021667, -0.017671376, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732147, 0.0764982, -0.028224003)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.50531465, 0.47724745, 0.50417954, 0.5125378, 0.16666667, 1, -0.50531465, 0.47724745, 0.50417954, 0.5125378, 0.2, 1, -0.3727505, 0.35055047, 0.6012531, 0.61373144, 0.23333333, 1, -0.22261931, 0.20284599, 0.66473174, 0.68368554, 0.26666668, 1, -0.14296252, 0.12402879, 0.6830659, 0.7054074, 0.7916667, 1, -0.14296252, 0.124028794, 0.6830659, 0.7054074)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435987, 0.22304308, -8.3422274e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, 0.36631826, -0.612043, -0.27063885, 0.64650524, 0.16666667, 1, 0.36631826, -0.612043, -0.27063885, 0.64650524, 0.23333333, 1, 0.45511997, -0.5492281, -0.365793, 0.5978376, 0.26666668, 1, 0.47574234, -0.53146493, -0.38833487, 0.583447, 0.33333334, 1, 0.47574234, -0.53146493, -0.38833487, 0.583447, 0.36666667, 1, 0.4238129, -0.57373184, -0.33191636, 0.6172891, 0.4, 1, 0.32841426, -0.63319016, -0.23081586, 0.6617692, 0.43333334, 1, 0.2392151, -0.6719834, -0.13863602, 0.6870185, 0.46666667, 1, 0.1913347, -0.687151, -0.08994695, 0.6950713, 0.6, 1, 0.19133468, -0.687151, -0.08994695, 0.6950713, 0.6333333, 1, 0.20096105, -0.6843977, -0.09969446, 0.69374025, 0.6666667, 1, 0.23905177, -0.67204154, -0.13846903, 0.68705225, 0.73333335, 1, 0.40057838, -0.5901886, -0.30701667, 0.63004375, 0.76666665, 1, 0.45294678, -0.5510217, -0.36342835, 0.5992781, 0.7916667, 1, 0.4757424, -0.5314648, -0.38833487, 0.583447)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577725, 0.09135095, -0.027113236, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934902, -0.017021656, 0.017671347, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.276084, 0.06599521, -0.0034686895)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.983949, 0.01825712, -0.17714365, 0.011452232, 0.16666667, 1, 0.9839491, 0.018257122, -0.17714366, 0.0114522325, 0.2, 1, 0.9836194, 0.025330193, -0.17627116, -0.027923783, 0.23333333, 1, -0.9817146, -0.03236273, 0.17511658, 0.067255, 0.26666668, 1, -0.98085266, -0.034427166, 0.17472237, 0.078835644, 0.33333334, 1, -0.98291206, -0.02867255, 0.17575793, 0.046594005, 0.7916667, 1, -0.98291206, -0.028672548, 0.17575793, 0.04659399)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, -0.0061842515, 0.004953388, 0.026989238, 0.99960434)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, 0.03475563, -0.00044481579, -0.005832363, 0.99937874)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608398, 0.06599527, -0.0034688246)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.994035, -0.019486463, 0.106504306, 0.013101434, 0.16666667, 1, 0.994035, -0.019486463, 0.106504306, 0.013101434, 0.23333333, 1, 0.98374647, -0.005358866, 0.10813919, 0.14324819, 0.26666668, 1, 0.9785101, -0.0018029504, 0.10825713, 0.17548586, 0.7916667, 1, 0.9785101, -0.0018029467, 0.108257145, 0.17548588)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, -0.006184277, -0.0049533774, -0.026989248, 0.99960434, 0.16666667, 1, -0.006184277, -0.0049533774, -0.026989248, 0.99960434, 0.23333333, 1, 0.17728442, -0.007954502, 0.012104314, 0.98405313, 0.26666668, 1, 0.2224207, -0.008664694, 0.02186484, 0.9746671, 0.7916667, 1, 0.22242075, -0.008664698, 0.021864852, 0.9746671)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, 0.034755684, 0.00044505406, 0.00583226, 0.99937874)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.2980725e-08, -0.98076427, 0.19519576, 1.8277991e-06)
-
-[sub_resource type="Animation" id="Animation_oggc5"]
-resource_name = "Kick"
-length = 1.2916666
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, -2.1378197e-16, 0.7618319, 0.029855082, 0.16666667, 1, -7.749126e-12, 0.76292205, 0.029855082, 0.2, 1, -5.114304e-11, 0.7690266, 0.029855084, 0.23333333, 1, -1.503291e-10, 0.78297985, 0.029855084, 0.26666668, 1, -3.2390488e-10, 0.807398, 0.029855084, 0.3, 1, -7.4840845e-10, 0.8448974, 0.029855082, 0.33333334, 1, -1.7583169e-09, 0.89809406, 0.029855084, 0.36666667, 1, -2.4934328e-09, 1.0015084, 0.029855082, 0.4, 1, -3.3358178e-09, 1.1200129, 0.029855084, 0.43333334, 1, -2.2649327e-09, 1.2116854, 0.029855086, 0.46666667, 1, 4.761485e-12, 1.2558731, 0.029855084, 0.5, 1, 2.3810074e-11, 1.2531935, 0.029855082, 0.53333336, 1, 9.7843324e-11, 1.2427787, 0.029855082, 0.56666666, 1, 2.321186e-10, 1.2238891, 0.029855084, 0.6, 1, 4.3930398e-10, 1.1947428, 0.029855086, 0.6333333, 1, 7.284475e-10, 1.1540669, 0.029855084, 0.6666667, 1, 1.1049807e-09, 1.1010971, 0.02985508, 0.7, 1, 1.5509942e-09, 1.0383532, 0.029855082, 0.73333335, 1, 2.0029822e-09, 0.97476864, 0.029855084, 0.76666665, 1, 2.4301867e-09, 0.9146706, 0.029855084, 0.8, 1, 2.8018525e-09, 0.86238563, 0.029855086, 0.8333333, 1, 3.0872214e-09, 0.8222406, 0.029855084, 0.8666667, 1, 6.174432e-10, 0.8069359, 0.029855084, 0.9, 1, -6.948035e-12, 0.8040871, 0.029855082, 0.93333334, 1, -2.4151705e-11, 0.8065073, 0.029855082, 0.96666664, 1, -5.22732e-11, 0.81046337, 0.029855084, 1, 1, -8.932837e-11, 0.8156762, 0.029855084, 1.0333333, 1, -1.3432348e-10, 0.822006, 0.029855084, 1.0666667, 1, -1.8229605e-10, 0.82875466, 0.029855084, 1.1, 1, -2.3126111e-10, 0.8356429, 0.029855084, 1.1333333, 1, -2.7923377e-10, 0.8423915, 0.029855082, 1.1666666, 1, -3.2422853e-10, 0.8487213, 0.029855084, 1.2, 1, -3.612834e-10, 0.8539341, 0.029855084, 1.2333333, 1, -3.894053e-10, 0.85789025, 0.029855084, 1.2666667, 1, -2.4118663e-10, 0.86031044, 0.029855084, 1.2916666, 1, -3.5255984e-16, 0.86128783, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.29341197, -0.021893617, -0.07111711, 0.9530858, 0.1, 1, 0.29341197, -0.021893617, -0.07111711, 0.9530858, 0.13333334, 1, 0.2916825, -0.021824865, -0.07089379, 0.95363474, 0.16666667, 1, 0.2847549, -0.02154913, -0.06999813, 0.95579845, 0.2, 1, 0.26293483, -0.020678256, -0.06716928, 0.9622505, 0.23333333, 1, 0.22840151, -0.01929073, -0.06266219, 0.9713568, 0.26666668, 1, 0.17984891, -0.017319942, -0.05626051, 0.98193145, 0.3, 1, 0.11603289, -0.014694085, -0.047730975, 0.991989, 0.33333334, 1, 0.0359328, -0.011341751, -0.03684165, 0.9986105, 0.36666667, 1, -0.05979789, -0.007253262, -0.023561073, 0.9979061, 0.4, 1, -0.1437832, -0.0035876818, -0.011654221, 0.9895341, 0.43333334, 1, -0.19980326, -0.0010953951, -0.0035585607, 0.97982895, 0.46666667, 1, -0.22359912, 1.1619234e-07, -2.6737977e-08, 0.97468126, 0.5, 1, -0.22179817, 1.1624632e-07, -2.6854789e-08, 0.9750926, 0.53333336, 1, -0.21569721, 1.164303e-07, -2.7532398e-08, 0.9764603, 0.56666666, 1, -0.20449497, 1.1675719e-07, -2.8873611e-08, 0.97886765, 0.6, 1, -0.18686551, 1.17240205e-07, -3.104141e-08, 0.9823855, 0.6333333, 1, -0.16175619, -4.903346e-05, -0.0007548361, 0.9868305, 0.6666667, 1, -0.12839527, -0.000245568, -0.0037729724, 0.99171597, 0.7, 1, -0.08808278, -0.0007389769, -0.01135012, 0.9960483, 0.73333335, 1, -0.046828248, -0.0013433279, -0.020630974, 0.99868906, 0.76666665, 1, -0.007654488, -0.0019822256, -0.03044235, 0.9995053, 0.8, 1, 0.026443228, -0.0025804772, -0.03962953, 0.99886113, 0.8333333, 1, 0.052536987, -0.0030645912, -0.04706393, 0.99750465, 0.8666667, 1, 0.062425405, -0.003255681, -0.04999844, 0.99679124, 0.9, 1, 0.06404141, -0.0032468461, -0.04986276, 0.9966956, 0.93333334, 1, 0.061950333, -0.0031085552, -0.04773906, 0.9969321, 0.96666664, 1, 0.058550686, -0.0028837433, -0.04428669, 0.99729747, 1, 1, 0.05407867, -0.0025880525, -0.039745856, 0.997742, 1.0333333, 1, 0.048650414, -0.00222919, -0.03423489, 0.9982265, 1.0666667, 1, 0.042860374, -0.0018464744, -0.02835763, 0.99867684, 1.1, 1, 0.036947437, -0.0014557048, -0.02235669, 0.9990661, 1.1333333, 1, 0.03115128, -0.0010727224, -0.01647533, 0.9993784, 1.1666666, 1, 0.025712358, -0.0007134053, -0.010957397, 0.9996091, 1.2, 1, 0.021231694, -0.00041743857, -0.0064123194, 0.999754, 1.2333333, 1, 0.017830554, -0.00019280575, -0.0029626996, 0.9998367, 1.2666667, 1, 0.01574963, -5.5379784e-05, -0.000852286, 0.99987566, 1.2916666, 1, 0.014909217, 1.1919607e-07, 1.7773131e-09, 0.99988884)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.026690235, 0.4224016, 0.012444671, 0.9059303, 0.1, 1, 0.026690235, 0.4224016, 0.012444671, 0.9059303, 0.13333334, 1, 0.02720291, 0.41953182, 0.011998299, 0.9072536, 0.16666667, 1, 0.029250704, 0.40800905, 0.010211602, 0.91245204, 0.2, 1, 0.03584017, 0.3702759, 0.0044215545, 0.9282197, 0.23333333, 1, 0.046571746, 0.3066225, -0.0051454934, 0.9506773, 0.26666668, 1, 0.061942276, 0.21041092, -0.01916279, 0.9754605, 0.3, 1, 0.08189611, 0.075545885, -0.03798169, 0.9930475, 0.33333334, 1, 0.10541187, -0.101451665, -0.061286792, 0.98733985, 0.36666667, 1, 0.12876984, -0.30623743, -0.086243734, 0.9392545, 0.4, 1, 0.14366499, -0.46497446, -0.10391502, 0.86738735, 0.43333334, 1, 0.1508768, -0.5591008, -0.113548696, 0.80731, 0.46666667, 1, 0.15301962, -0.5948908, -0.11684524, 0.7804083, 0.5, 1, 0.15197168, -0.5920009, -0.11590251, 0.7829471, 0.53333336, 1, 0.14881028, -0.5799703, -0.11290865, 0.7929324, 0.56666666, 1, 0.14390448, -0.5566835, -0.10806884, 0.81099707, 0.6, 1, 0.13690098, -0.5179936, -0.10096507, 0.83830005, 0.6333333, 1, 0.12726714, -0.45938733, -0.091068745, 0.8743415, 0.6666667, 1, 0.11425858, -0.37599877, -0.077732414, 0.9162574, 0.7, 1, 0.09754678, -0.26741415, -0.06085829, 0.9566978, 0.73333335, 1, 0.07940935, -0.15250047, -0.043000646, 0.984169, 0.76666665, 1, 0.061635036, -0.0430367, -0.025914114, 0.9968338, 0.8, 1, 0.04609306, 0.05022429, -0.011277609, 0.99761003, 0.8333333, 1, 0.034465082, 0.11857255, -0.00050059846, 0.99234694, 0.8666667, 1, 0.030190077, 0.14323626, 0.0034210726, 0.98922205, 0.9, 1, 0.029130127, 0.14667077, 0.004321181, 0.9887469, 0.93333334, 1, 0.029157411, 0.14028585, 0.0041330727, 0.989673, 0.96666664, 1, 0.029198408, 0.13009356, 0.003832791, 0.99106425, 1, 1, 0.029247269, 0.116768315, 0.0034402045, 0.9927225, 1.0333333, 1, 0.029299289, 0.10062505, 0.0029646, 0.9944885, 1.0666667, 1, 0.029346216, 0.08339326, 0.002456923, 0.99608153, 1.1, 1, 0.029385056, 0.065778, 0.0019379482, 0.9973997, 1.1333333, 1, 0.029414216, 0.04849341, 0.0014287148, 0.9983893, 1.1666666, 1, 0.029433554, 0.03226084, 0.00095047464, 0.9990456, 1.2, 1, 0.029443651, 0.018881379, 0.0005562931, 0.999388, 1.2333333, 1, 0.029447779, 0.008724241, 0.00025704707, 0.9995283, 1.2666667, 1, 0.029448804, 0.0025097383, 7.395382e-05, 0.9995632, 1.2916666, 1, 0.029448895, -1.5216656e-14, 7.0211663e-09, 0.9995663)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -2.8837448e-16, 0.17607875, -0.010877871, 0.53333336, 1, -4.6507367e-09, 0.17506465, -0.010913493, 0.56666666, 1, 1.3610206e-08, 0.173854, -0.010955995, 0.6, 1, 1.6786764e-08, 0.172114, -0.011017099, 0.6333333, 1, -2.9438882e-09, 0.16981056, -0.011098004, 0.6666667, 1, -1.4848126e-08, 0.16693226, -0.01119912, 0.73333335, 1, 9.14213e-11, 0.1603097, -0.011431665, 0.76666665, 1, 6.0698766e-09, 0.15720515, -0.011540698, 0.8, 1, 1.5026139e-08, 0.1545221, -0.011634927, 0.8333333, 1, 1.5038015e-08, 0.15247323, -0.011706879, 1.2916666, 1, -3.6572172e-16, 0.15150139, -0.011741015)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, -0.008463163, -0.40884823, -0.054729696, 0.9109205, 0.1, 1, -0.008463163, -0.40884823, -0.054729696, 0.9109205, 0.13333334, 1, -0.0060665505, -0.4053219, -0.053122517, 0.91260916, 0.16666667, 1, 0.003520761, -0.39112163, -0.046681628, 0.9191477, 0.2, 1, 0.027469445, -0.34634137, -0.030678414, 0.93720436, 0.23333333, 1, 0.056773245, -0.27367452, -0.011127051, 0.96008086, 0.26666668, 1, 0.08663361, -0.16905221, 0.008902328, 0.98175186, 0.3, 1, 0.111012466, -0.03061372, 0.025630655, 0.9930167, 0.33333334, 1, 0.122633785, 0.13970734, 0.034545414, 0.9819621, 0.36666667, 1, 0.1167912, 0.32284495, 0.025042571, 0.9388843, 0.4, 1, 0.1062273, 0.46364895, 0.00980509, 0.87957346, 0.43333334, 1, 0.09533295, 0.5476986, -0.0029074112, 0.83122164, 0.46666667, 1, 0.08685777, 0.5801021, -0.0090740705, 0.80984885, 0.5, 1, 0.08098511, 0.5774519, -0.008748359, 0.81235105, 0.53333336, 1, 0.07522173, 0.56554204, -0.008084356, 0.8212421, 0.56666666, 1, 0.068779714, 0.54235303, -0.0075211185, 0.83729684, 0.6, 1, 0.061004087, 0.5039006, -0.0072948514, 0.86157393, 0.6333333, 1, 0.051226962, 0.44593546, -0.00771368, 0.8935647, 0.6666667, 1, 0.03878003, 0.36396322, -0.009149183, 0.93056077, 0.7, 1, 0.023768835, 0.25791976, -0.011926098, 0.9658004, 0.73333335, 1, 0.008453809, 0.14600724, -0.015041157, 0.98913306, 0.76666665, 1, -0.005759415, 0.039388075, -0.018056203, 0.99904424, 0.8, 1, -0.017635906, -0.05166697, -0.020632055, 0.99829555, 0.8333333, 1, -0.026209967, -0.11867486, -0.022524795, 0.9923316, 0.8666667, 1, -0.029241335, -0.14292282, -0.023188716, 0.98903006, 0.9, 1, -0.029347796, -0.14624673, -0.022925332, 0.9885469, 0.93333334, 1, -0.027829388, -0.13986722, -0.021925291, 0.9895363, 0.96666664, 1, -0.025410166, -0.12970088, -0.020331638, 0.991019, 1, 1, -0.022250036, -0.11641713, -0.018249303, 0.9927835, 1.0333333, 1, -0.018423676, -0.10032692, -0.015727034, 0.99465966, 1.0666667, 1, -0.014340648, -0.08315025, -0.01303445, 0.9963486, 1.1, 1, -0.010168096, -0.06558939, -0.010281644, 0.9977419, 1.1333333, 1, -0.0060751773, -0.048356254, -0.007580214, 0.99878293, 1.1666666, 1, -0.0022325902, -0.032170486, -0.0050429637, 0.9994672, 1.2, 1, 0.0009336544, -0.018828735, -0.0029515373, 0.99981797, 1.2333333, 1, 0.0033366953, -0.00870001, -0.0013637801, 0.99995565, 1.2666667, 1, 0.004806679, -0.0025028503, -0.00039232563, 0.99998534, 1.2916666, 1, 0.00540027, -1.1920752e-07, -6.4376265e-10, 0.9999854)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.008212412, 1.8625363e-09, -1.9426876e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 5.9539765e-08, 0.5406087, 0.31222785)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939666, 1.229263e-07, 1.4076622e-09, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 1.14738086e-07, 0.085749105, 0.41874638, 0.53333336, 1, 9.387453e-08, 0.08473452, 0.4187383, 0.56666666, 1, 8.791179e-08, 0.08352311, 0.41872877, 0.6, 1, 9.684924e-08, 0.08178222, 0.41871503, 0.6333333, 1, 1.1174605e-07, 0.0794777, 0.41869685, 0.6666667, 1, 9.9819715e-08, 0.07659769, 0.4186742, 0.73333335, 1, 7.0004944e-08, 0.06997084, 0.418622, 0.76666665, 1, 8.192002e-08, 0.06686449, 0.41859752, 0.8, 1, 1.0426674e-07, 0.06417985, 0.41857636, 0.8333333, 1, 1.2214427e-07, 0.06212971, 0.41856018, 1.2916666, 1, 9.97907e-08, 0.06115723, 0.4185525)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939665, 1.2105632e-07, 3.2629548e-09, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732144, 0.0764982, -0.028223956)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.11569178, -0.08802772, -0.70438296, 0.69477427, 0.1, 1, -0.11569178, -0.08802772, -0.70438296, 0.69477427, 0.13333334, 1, -0.119283035, -0.091570325, -0.7037837, 0.69431627, 0.16666667, 1, -0.13361607, -0.10571598, -0.70120376, 0.69230354, 0.2, 1, -0.17782529, -0.14941685, -0.6913162, 0.68420374, 0.23333333, 1, -0.24483256, -0.21586311, -0.6705198, 0.6662306, 0.26666668, 1, -0.33277604, -0.30349293, -0.63150615, 0.6311515, 0.3, 1, -0.4365922, -0.40768582, -0.5647364, 0.5694317, 0.33333334, 1, -0.54614884, -0.5188892, -0.45963183, 0.47033405, 0.36666667, 1, -0.6441147, -0.6204185, -0.30766255, 0.3248707, 0.4, 1, -0.69698685, -0.6777936, -0.15410805, 0.176227, 0.43333334, 1, -0.71248305, -0.6969688, -0.04367937, 0.068517536, 0.46666667, 1, -0.71386045, -0.6999606, 0.005027469, 0.020813288, 0.5, 1, -0.71410805, -0.69955957, -6.0397695e-05, 0.025810687, 0.53333336, 1, -0.7146715, -0.6979608, -0.01708902, 0.042467356, 0.56666666, 1, -0.71471286, -0.69448006, -0.045089133, 0.0696424, 0.6, 1, -0.712843, -0.68762904, -0.08537382, 0.10831706, 0.6333333, 1, -0.70686615, -0.6752537, -0.13854395, 0.15867704, 0.6666667, 1, -0.69382775, -0.6546058, -0.20427965, 0.21991852, 0.73333335, 1, -0.6405913, -0.58673835, -0.3501525, 0.35039148, 0.76666665, 1, -0.604968, -0.54588205, -0.4142173, 0.40552518, 0.8, 1, -0.56901485, -0.5063955, -0.4666437, 0.44947693, 0.8333333, 1, -0.53846544, -0.47388083, -0.5046007, 0.4804895, 0.8666667, 1, -0.52618814, -0.46105435, -0.51850176, 0.49164093, 0.9, 1, -0.5230665, -0.45780188, -0.52192616, 0.49438047, 1.2916666, 1, -0.5230665, -0.45780188, -0.52192616, 0.49438047)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435845, 0.22304283, -8.364037e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, -0.32758233, 0.66762245, -0.33097452, 0.5808838, 0.1, 1, -0.32758233, 0.66762245, -0.33097452, 0.5808838, 0.13333334, 1, -0.32410732, 0.6692996, -0.32831392, 0.58241093, 0.16666667, 1, -0.3101294, 0.6758451, -0.3175924, 0.5883777, 0.2, 1, -0.27587166, 0.6905699, -0.29118782, 0.60184526, 0.23333333, 1, -0.23544787, 0.70565337, -0.25980812, 0.6157251, 0.26666668, 1, -0.195851, 0.7181497, -0.22884917, 0.6273208, 0.3, 1, -0.16451879, 0.7265084, -0.20420352, 0.6351537, 0.33333334, 1, -0.1490586, 0.73014885, -0.19199562, 0.63859373, 0.36666667, 1, -0.24011017, 0.7043932, -0.26894102, 0.6114312, 0.4, 1, -0.38482848, 0.6359445, -0.38890126, 0.5442771, 0.43333334, 1, -0.4953651, 0.5543224, -0.4780498, 0.46776962, 0.46666667, 1, -0.5450327, 0.50556666, -0.51735127, 0.423898, 0.5, 1, -0.5442288, 0.50496095, -0.51759195, 0.42535642, 0.53333336, 1, -0.54029185, 0.5068488, -0.516966, 0.42887688, 0.56666666, 1, -0.53231245, 0.5121057, -0.5141927, 0.43588668, 0.6, 1, -0.5190004, 0.5216289, -0.50812894, 0.44760138, 0.6333333, 1, -0.49750406, 0.5373912, -0.49629712, 0.46625072, 0.6666667, 1, -0.4630184, 0.5619592, -0.47448194, 0.49465406, 0.7, 1, -0.40864956, 0.5971254, -0.4364616, 0.53474116, 0.76666665, 1, -0.27835545, 0.6602359, -0.33974287, 0.60924685, 0.8, 1, -0.21631938, 0.68113333, -0.29202023, 0.6356002, 0.8333333, 1, -0.16676107, 0.69401073, -0.25361717, 0.65285397, 0.8666667, 1, -0.14687222, 0.69808906, -0.23937906, 0.65863335, 0.9, 1, -0.14488763, 0.6982059, -0.24010378, 0.6586854, 0.93333334, 1, -0.15337059, 0.6961423, -0.24965602, 0.65538937, 0.96666664, 1, -0.16800697, 0.69255996, -0.26439273, 0.649793, 1, 1, -0.18767197, 0.68734735, -0.28327522, 0.64194083, 1.0333333, 1, -0.21182609, 0.68022484, -0.30570722, 0.6316383, 1.0666667, 1, -0.2375165, 0.67168283, -0.3291285, 0.61976004, 1.1, 1, -0.26352268, 0.661978, -0.3524627, 0.6067216, 1.1333333, 1, -0.28867772, 0.6515344, -0.37471125, 0.5930934, 1.1666666, 1, -0.31188357, 0.64093745, -0.39496678, 0.5795939, 1.2, 1, -0.33061862, 0.6316839, -0.41111666, 0.5680228, 1.2333333, 1, -0.34460276, 0.62434936, -0.42306742, 0.5589731, 1.2666667, 1, -0.35305443, 0.6197346, -0.4302439, 0.55332804, 1.2916666, 1, -0.35644147, 0.617847, -0.43310678, 0.5510291)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, 0.24611148, -0.080936134, 0.050293844, 0.964546, 0.46666667, 1, 0.24553841, -0.08096601, 0.05024576, 0.9646921, 0.5, 1, 0.24324529, -0.081085175, 0.05005322, 0.9652729, 0.53333336, 1, 0.23556039, -0.08148072, 0.049406726, 0.967177, 0.56666666, 1, 0.22288537, -0.0821202, 0.04833638, 0.97017634, 0.6, 1, 0.20454183, -0.08301751, 0.04677846, 0.97420865, 0.6333333, 1, 0.1800668, -0.084163584, 0.04468361, 0.979028, 0.6666667, 1, 0.1492233, -0.08552571, 0.04201768, 0.9842014, 0.73333335, 1, 0.07757888, -0.08834321, 0.035715636, 0.992422, 0.76666665, 1, 0.043923054, -0.08950215, 0.032703158, 0.99448013, 0.8, 1, 0.014900406, -0.09041786, 0.030078938, 0.9953381, 0.8333333, 1, -0.0071604657, -0.091062225, 0.028067818, 0.99542385, 0.8666667, 1, -0.0154944025, -0.09129401, 0.027304398, 0.995329, 0.9, 1, -0.017577728, -0.09135096, 0.027113236, 0.9952944, 1.2916666, 1, -0.017577726, -0.09135095, 0.027113229, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.020934897, 0.017021656, -0.017671376, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732147, 0.07649832, -0.028223924)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, 0.7112019, -0.69949067, 0.061088573, 0.034246903, 0.1, 1, 0.7112019, -0.69949067, 0.061088573, 0.034246903, 0.13333334, 1, 0.71153146, -0.69967073, 0.05712241, 0.030346457, 0.16666667, 1, 0.7126283, -0.7001735, 0.041241247, 0.014736456, 0.2, 1, 0.7137729, -0.6995106, -0.008262089, -0.033839665, 0.23333333, 1, -0.7087678, 0.69184387, 0.08478354, 0.10868357, 0.26666668, 1, -0.68862313, 0.66837186, 0.18798454, 0.20913911, 0.3, 1, -0.64096314, 0.61709386, 0.3141754, 0.3311425, 0.33333334, 1, -0.5511342, 0.5239931, 0.45364195, 0.46464118, 0.36666667, 1, -0.40570232, 0.37658584, 0.5873207, 0.5904602, 0.4, 1, -0.24830121, 0.21930993, 0.6692432, 0.6651039, 0.43333334, 1, -0.13084382, 0.10297911, 0.7017264, 0.692716, 0.46666667, 1, -0.07786685, 0.05074065, 0.70961875, 0.6984293, 0.5, 1, -0.082220346, 0.05496513, 0.709359, 0.6978743, 0.53333336, 1, -0.09676886, 0.069039285, 0.7082956, 0.69583535, 0.56666666, 1, -0.120621495, 0.09198439, 0.70590055, 0.691877, 0.6, 1, -0.15480542, 0.12462039, 0.70104474, 0.68486595, 0.6333333, 1, -0.19972596, 0.16712706, 0.6920676, 0.6732166, 0.6666667, 1, -0.25503987, 0.21892518, 0.6768175, 0.6549386, 0.73333335, 1, -0.37747148, 0.3303787, 0.62487215, 0.5982475, 0.76666665, 1, -0.431419, 0.37834167, 0.59281516, 0.5650713, 0.8, 1, -0.47582415, 0.41721547, 0.5613576, 0.5332921, 0.8333333, 1, -0.50820214, 0.44512936, 0.5350678, 0.50724053, 0.8666667, 1, -0.52012336, 0.45529342, 0.52458483, 0.4969812, 0.9, 1, -0.5230665, 0.45780188, 0.52192616, 0.49438047, 1.2916666, 1, -0.5230665, 0.45780188, 0.52192616, 0.49438047)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435969, 0.2230429, -8.36446e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, -0.46689266, -0.5788274, 0.45105153, 0.49348015, 0.1, 1, -0.46689266, -0.5788274, 0.45105153, 0.49348015, 0.13333334, 1, -0.46182543, -0.58287835, 0.44672945, 0.49739617, 0.16666667, 1, -0.441209, -0.5986356, 0.42910454, 0.51267874, 0.2, 1, -0.3887191, -0.6339775, 0.38396603, 0.5473027, 0.23333333, 1, -0.32448587, -0.6691329, 0.32827988, 0.5824109, 0.26666668, 1, -0.26021805, -0.6966466, 0.2721388, 0.6106641, 0.3, 1, -0.20902795, -0.7136786, 0.22715087, 0.6287866, 0.33333334, 1, -0.18400216, -0.72053677, 0.20507574, 0.6363286, 0.36666667, 1, -0.24657363, -0.701592, 0.26017, 0.6158585, 0.4, 1, -0.34688643, -0.6577991, 0.34775117, 0.5709985, 0.43333334, 1, -0.42772847, -0.60834056, 0.41754687, 0.5221348, 0.46666667, 1, -0.46674004, -0.5788557, 0.45112795, 0.4935215, 0.5, 1, -0.4661294, -0.57896864, 0.45143342, 0.49368677, 0.53333336, 1, -0.4635063, -0.5798164, 0.45172843, 0.49489012, 0.56666666, 1, -0.45847148, -0.5817756, 0.45130646, 0.49765807, 0.6, 1, -0.4503933, -0.5851902, 0.44966817, 0.5024907, 0.6333333, 1, -0.43729442, -0.5913054, 0.44512916, 0.51087344, 0.6666667, 1, -0.4157156, -0.60207516, 0.43467903, 0.5251098, 0.7, 1, -0.38100237, -0.61943877, 0.41396642, 0.54750764, 0.73333335, 1, -0.34108806, -0.63747096, 0.38861388, 0.5711996, 0.76666665, 1, -0.29980794, -0.65383023, 0.36131778, 0.59335554, 0.8, 1, -0.26141134, -0.66698784, 0.33525786, 0.6118771, 0.8333333, 1, -0.23040687, -0.67618823, 0.3139724, 0.6253827, 0.8666667, 1, -0.21779937, -0.6794453, 0.30594203, 0.63033086, 0.9, 1, -0.21667525, -0.67941016, 0.3066725, 0.63040125, 0.93333334, 1, -0.22232777, -0.6773872, 0.31282046, 0.62758297, 0.96666664, 1, -0.2320029, -0.67399734, 0.3223417, 0.62289494, 1, 1, -0.2449424, -0.669318, 0.33455247, 0.61651546, 1.0333333, 1, -0.26076612, -0.6632919, 0.3490584, 0.6084433, 1.0666667, 1, -0.27758482, -0.6564534, 0.36424103, 0.5994533, 1.1, 1, -0.29463828, -0.6490415, 0.37943846, 0.58988136, 1.1333333, 1, -0.31119463, -0.64136666, 0.3940268, 0.5801289, 1.1666666, 1, -0.32655498, -0.6338091, 0.40742412, 0.5706606, 1.2, 1, -0.33905497, -0.62734085, 0.41822243, 0.56265014, 1.2333333, 1, -0.34844533, -0.62228584, 0.42628273, 0.55644345, 1.2666667, 1, -0.35414818, -0.6191325, 0.43115455, 0.55259377, 1.2916666, 1, -0.35644147, -0.617847, 0.43310678, 0.5510291)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, 0.22110535, 0.0822087, -0.048185624, 0.9705835, 0.46666667, 1, 0.22058332, 0.08223461, -0.04814139, 0.9707023, 0.5, 1, 0.21849446, 0.082338005, -0.047964368, 0.9711746, 0.53333336, 1, 0.2114982, 0.08268113, -0.047370456, 0.9727222, 0.56666666, 1, 0.1999706, 0.08323599, -0.046388555, 0.97515726, 0.6, 1, 0.18330964, 0.08401505, -0.04496218, 0.97842604, 0.6333333, 1, 0.16111286, 0.08501138, -0.04304873, 0.9823251, 0.6666667, 1, 0.1331854, 0.086198226, -0.040620137, 0.98649967, 0.73333335, 1, 0.06843428, 0.08866844, -0.034900364, 0.9930945, 0.76666665, 1, 0.038030878, 0.08969432, -0.032172326, 0.99472284, 0.8, 1, 0.01180349, 0.09051101, -0.029797414, 0.9953796, 0.8333333, 1, -0.008148578, 0.09109003, -0.02797738, 0.9954163, 0.8666667, 1, -0.015691996, 0.09129942, -0.027286269, 0.9953259, 0.9, 1, -0.017577723, 0.09135096, -0.027113229, 0.9952944, 1.2916666, 1, -0.017577725, 0.09135095, -0.027113236, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934897, -0.017021656, 0.017671382, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.276084, 0.06599533, -0.0034686225)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.8370536, 0.079046085, -0.12651776, 0.5263898, 0.1, 1, 0.8370536, 0.079046085, -0.12651776, 0.5263898, 0.13333334, 1, 0.83858067, 0.078910835, -0.12619749, 0.5240515, 0.16666667, 1, 0.84462184, 0.07836355, -0.12490637, 0.51465684, 0.2, 1, 0.86294734, 0.07663691, -0.120788254, 0.48462257, 0.23333333, 1, 0.889922, 0.07387261, -0.11390751, 0.43543863, 0.26666668, 1, 0.9233184, 0.06972449, -0.103546575, 0.36318022, 0.3, 1, 0.9583909, 0.063729055, -0.088826336, 0.26369554, 0.33333334, 1, 0.987137, 0.055322934, -0.068784654, 0.13329917, 0.36666667, 1, 0.99775547, 0.04420603, -0.0432692, -0.025642948, 0.4, 1, -0.98608446, -0.033509005, 0.0203984, 0.16155067, 0.43333334, 1, -0.9682616, -0.025571665, 0.00508676, 0.24857548, 0.46666667, 1, -0.9588909, -0.021340173, -0.0013620778, 0.28296822, 0.5, 1, -0.9600333, -0.020146627, -0.0011365779, 0.27915737, 0.53333336, 1, -0.9626659, -0.019619172, -0.0006767525, 0.2699797, 0.56666666, 1, -0.9675875, -0.01948176, -0.00016454408, 0.2517833, 0.6, 1, -0.9751221, -0.019661907, 0.0003208443, 0.22079447, 0.6333333, 1, -0.9846452, -0.020863064, 0.0007131027, 0.17331523, 0.6666667, 1, -0.99408746, -0.02454921, 0.0009578779, 0.10576635, 0.7, 1, -0.9992856, -0.033120874, 0.0010601874, 0.01817657, 0.73333335, 1, 0.99634165, 0.043344367, -0.0011126811, 0.07364404, 0.76666665, 1, 0.9854643, 0.0537965, -0.0011584457, 0.16113622, 0.8, 1, 0.96958417, 0.06321624, -0.0012432282, 0.23645012, 0.8333333, 1, 0.9535614, 0.07052997, -0.0014146083, 0.2928212, 0.8666667, 1, 0.9466477, 0.07334161, -0.0017971337, 0.31380895, 0.9, 1, 0.9459478, 0.07315653, -0.0023044776, 0.31595176, 0.93333334, 1, 0.94863725, 0.07097191, -0.00287374, 0.30828893, 0.96666664, 1, 0.9528976, 0.067389965, -0.0034673985, 0.29569045, 1, 1, 0.95824647, 0.06264401, -0.0040738382, 0.27896774, 1.0333333, 1, 0.96433264, 0.05683893, -0.0046738037, 0.25847685, 1.0666667, 1, 0.97030795, 0.050611842, -0.005249872, 0.23646042, 1.1, 1, 0.9758495, 0.044224896, -0.005789525, 0.21384226, 1.1333333, 1, 0.98072517, 0.037945382, -0.006280782, 0.19156972, 1.1666666, 1, 0.98479486, 0.032044355, -0.006712311, 0.17060879, 1.2, 1, 0.9877756, 0.02718686, -0.007044251, 0.15333223, 1.2333333, 1, 0.9898157, 0.02350179, -0.007286433, 0.14021257, 1.2666667, 1, 0.9909694, 0.021248613, -0.007430004, 0.13218522, 1.2916666, 1, 0.9914148, 0.020339562, -0.007486248, 0.12894534)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.5993888, 0.013226979, 0.01887605, 0.8001262, 0.1, 1, 0.5993888, 0.013226979, 0.01887605, 0.8001262, 0.13333334, 1, 0.5945954, 0.013125649, 0.01898265, 0.8036938, 0.16666667, 1, 0.57521117, 0.012715679, 0.019402243, 0.81767595, 0.2, 1, 0.52142143, 0.011633092, 0.020488402, 0.852974, 0.23333333, 1, 0.44623977, 0.010204766, 0.021826237, 0.89458907, 0.26666668, 1, 0.35512316, 0.008588493, 0.023201488, 0.9344921, 0.3, 1, 0.25617224, 0.0069926316, 0.024432901, 0.9662971, 0.33333334, 1, 0.16002493, 0.005675963, 0.025411854, 0.98676956, 0.36666667, 1, 0.086854406, 0.005110812, 0.026100472, 0.995866, 0.4, 1, 0.0350221, 0.004916323, 0.026569564, 0.9990213, 0.43333334, 1, 0.0051451293, 0.0049100267, 0.026856259, 0.9996141, 0.46666667, 1, -0.006003825, 0.0049553597, 0.026998367, 0.99960524, 0.5, 1, -0.0052821185, 0.0049632187, 0.027034856, 0.9996083, 0.53333336, 1, -0.00022726401, 0.0050351415, 0.027030338, 0.9996219, 0.56666666, 1, 0.0113548525, 0.0052101784, 0.026981078, 0.999558, 0.6, 1, 0.031692922, 0.005524548, 0.026871068, 0.9991212, 0.6333333, 1, 0.06573717, 0.006088327, 0.026655007, 0.9974624, 0.6666667, 1, 0.121048644, 0.0070790276, 0.026234386, 0.9922746, 0.7, 1, 0.2066518, 0.00872056, 0.025414322, 0.9780456, 0.76666665, 1, 0.3921032, 0.0122599425, 0.022888063, 0.9195548, 0.8, 1, 0.47125, 0.013739582, 0.021458926, 0.8816316, 0.8333333, 1, 0.53036886, 0.014828417, 0.020231431, 0.84739596, 0.8666667, 1, 0.5523606, 0.015230181, 0.019736141, 0.8332324, 0.9, 1, 0.55314165, 0.015232245, 0.01971898, 0.83271456, 0.93333334, 1, 0.5414859, 0.014989631, 0.019986736, 0.8403385, 0.96666664, 1, 0.522126, 0.014585092, 0.02041783, 0.85249925, 1, 1, 0.49605453, 0.014037352, 0.020972546, 0.86792463, 1.0333333, 1, 0.4635256, 0.013349426, 0.021625256, 0.88571906, 1.0666667, 1, 0.42801142, 0.012592962, 0.022290751, 0.9034107, 1.1, 1, 0.39102584, 0.0117995, 0.02293449, 0.9200182, 1.1333333, 1, 0.3542075, 0.0110041965, 0.023527965, 0.9348061, 1.1666666, 1, 0.3192953, 0.010245292, 0.02404908, 0.9472947, 1.2, 1, 0.2904439, 0.00961476, 0.024450304, 0.9565313, 1.2333333, 1, 0.26849174, 0.00913302, 0.024738263, 0.962921, 1.2666667, 1, 0.25504905, 0.0088371895, 0.024907349, 0.96656686, 1.2916666, 1, 0.24962775, 0.008717708, 0.024974002, 0.96798056)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.7872299, 0.0045673708, -0.0037833394, 0.61663115, 0.1, 1, -0.7872299, 0.0045673708, -0.0037833394, 0.61663115, 0.13333334, 1, -0.786158, 0.004784013, -0.0037526726, 0.6179958, 0.16666667, 1, -0.78184605, 0.0056504263, -0.0036298886, 0.62343544, 0.2, 1, -0.7667511, 0.0077205347, -0.003361856, 0.6418893, 0.23333333, 1, -0.7401258, 0.010104065, -0.0030947574, 0.67238545, 0.26666668, 1, -0.69821846, 0.0123723885, -0.002902204, 0.7157719, 0.3, 1, -0.63615274, 0.014042841, -0.0028571812, 0.7714301, 0.33333334, 1, -0.5478702, 0.014566237, -0.0030292445, 0.836431, 0.36666667, 1, -0.42698783, 0.011403147, -0.0038125878, 0.9041775, 0.4, 1, -0.30969876, 0.006706609, -0.004744448, 0.9507992, 0.43333334, 1, -0.2210933, 0.0027632958, -0.0054545193, 0.9752335, 0.46666667, 1, -0.17162387, 0.00073724636, -0.0057988153, 0.9851453, 0.5, 1, -0.15517777, 0.00063468044, -0.0058108214, 0.9878693, 0.53333336, 1, -0.14479022, 0.0005728965, -0.0058171763, 0.9894451, 0.56666666, 1, -0.13804936, 0.0005325544, -0.0058210217, 0.99040806, 0.6, 1, -0.13447326, 0.0005082198, -0.0058234083, 0.99090004, 0.6333333, 1, -0.14309146, 0.00053733244, -0.0058228355, 0.9896922, 0.6666667, 1, -0.18229692, 0.000699648, -0.005809598, 0.9832262, 0.7, 1, -0.28411075, 0.0011383242, -0.00572832, 0.95877373, 0.73333335, 1, -0.40935138, 0.0016827565, -0.0055330796, 0.9123585, 0.76666665, 1, -0.535273, 0.0022341413, -0.0052144723, 0.8446601, 0.8, 1, -0.64259386, 0.0027076162, -0.004822159, 0.7661871, 0.8333333, 1, -0.71835953, 0.0030448118, -0.004457124, 0.6956511, 0.8666667, 1, -0.74427927, 0.0031627072, -0.0043104775, 0.6678472, 0.9, 1, -0.7477289, 0.0031932106, -0.0042867274, 0.6639827, 0.93333334, 1, -0.74163115, 0.0031991818, -0.0043152533, 0.6707865, 0.96666664, 1, -0.73232293, 0.0032098629, -0.004357072, 0.680936, 1, 1, -0.72025245, 0.0032238276, -0.004408919, 0.69369054, 1.0333333, 1, -0.70556486, 0.0032401672, -0.004468756, 0.7086239, 1.0666667, 1, -0.6896508, 0.003256395, -0.004529965, 0.7241206, 1.1, 1, -0.6731056, 0.0032715974, -0.004589888, 0.73952496, 1.1333333, 1, -0.65658695, 0.0032851044, -0.0046461816, 0.75422883, 1.1666666, 1, -0.64081234, 0.0032964968, -0.004696839, 0.7676761, 1.2, 1, -0.6276212, 0.0033049611, -0.0047369874, 0.77849746, 1.2333333, 1, -0.6174918, 0.0033107998, -0.004766514, 0.786556, 1.2666667, 1, -0.611246, 0.003314125, -0.004784172, 0.79141927, 1.2916666, 1, -0.6087138, 0.0033154194, -0.0047912123, 0.7933685)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.276084, 0.06599521, -0.003468812)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, -0.915512, -0.024903487, -0.025501743, 0.40070862, 0.13333334, 1, -0.9156229, -0.024527645, -0.025246695, 0.40049434, 0.16666667, 1, -0.9160643, -0.023024224, -0.024226453, 0.39963648, 0.2, 1, -0.92015594, -0.01918415, -0.021674197, 0.39048088, 0.23333333, 1, -0.930001, -0.014248164, -0.018480169, 0.3668157, 0.26666668, 1, -0.94688225, -0.008576808, -0.014928515, 0.32111937, 0.3, 1, -0.9697566, -0.002534118, -0.011287803, 0.24379967, 0.33333334, 1, -0.99234843, 0.0034799618, -0.007802269, 0.12317319, 0.36666667, 1, -0.9932884, 0.008517464, -0.008673622, -0.11502408, 0.4, 1, 0.9342724, -0.011825329, 0.011924454, 0.35616437, 0.43333334, 1, 0.85706747, -0.013629584, 0.01471407, 0.51481366, 0.46666667, 1, 0.8166004, -0.014772101, 0.016042545, 0.57679147, 0.5, 1, 0.82061064, -0.015849385, 0.01595564, 0.571045, 0.53333336, 1, 0.831263, -0.016696492, 0.015685495, 0.55540717, 0.56666666, 1, 0.8451352, -0.017370498, 0.01530625, 0.53405106, 0.6, 1, 0.8609202, -0.017835218, 0.014857245, 0.5082101, 0.6333333, 1, 0.87740815, -0.018125156, 0.014351385, 0.47918725, 0.7, 1, 0.90864635, -0.018531043, 0.013182813, 0.4169468, 0.73333335, 1, 0.92232925, -0.018682979, 0.012588065, 0.38574788, 0.76666665, 1, 0.9344858, -0.01880605, 0.012002083, 0.35530064, 0.8, 1, 0.94508225, -0.018904893, 0.011434793, 0.32608515, 0.8333333, 1, 0.95414716, -0.018985143, 0.010895478, 0.2985366, 0.8666667, 1, 0.9615028, -0.019056447, 0.010410255, 0.27393577, 0.9, 1, 0.96762514, -0.019135216, 0.009964208, 0.25146824, 0.93333334, 1, 0.9727292, -0.019228093, 0.009554491, 0.23094812, 0.96666664, 1, 0.97697556, -0.019336844, 0.009179765, 0.21227477, 1, 1, 0.98048323, -0.019458627, 0.008840373, 0.19543767, 1.0333333, 1, 0.9832472, -0.019592226, 0.008548611, 0.18101959, 1.0666667, 1, 0.98549235, -0.019727714, 0.008291697, 0.16836584, 1.1, 1, 0.9872935, -0.019861432, 0.00806953, 0.15745488, 1.1333333, 1, 0.9887145, -0.019989496, 0.007881914, 0.14826316, 1.1666666, 1, 0.98980856, -0.020107802, 0.007728576, 0.14076583, 1.2, 1, 0.99054515, -0.02020433, 0.007620237, 0.13547683, 1.2333333, 1, 0.99103975, -0.020277215, 0.007544909, 0.13180338, 1.2666667, 1, 0.9913141, -0.02032169, 0.007502153, 0.12971972, 1.2916666, 1, 0.9914148, -0.020339629, 0.007486256, 0.12894534)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, 0.5379629, -0.012522536, -0.02027447, 0.84263164, 0.1, 1, 0.5379629, -0.012522536, -0.02027447, 0.84263164, 0.13333334, 1, 0.54187614, -0.012643352, -0.020214992, 0.8401201, 0.16666667, 1, 0.5574109, -0.013123856, -0.019972725, 0.82989275, 0.2, 1, 0.5944394, -0.01433881, -0.019353323, 0.8037797, 0.23333333, 1, 0.63620114, -0.015834175, -0.018577605, 0.771137, 0.26666668, 1, 0.6750801, -0.01742298, -0.017772334, 0.73732454, 0.3, 1, 0.704471, -0.01894051, -0.017102078, 0.7092739, 0.33333334, 1, 0.71855545, -0.0202408, -0.016756868, 0.6949731, 0.36666667, 1, 0.59259236, -0.018395135, -0.020066634, 0.8050424, 0.4, 1, 0.33987278, -0.013189729, -0.02440061, 0.9400623, 0.43333334, 1, 0.10453449, -0.007767763, -0.026506359, 0.99413764, 0.46666667, 1, -0.0074554095, -0.0050399825, -0.026952889, 0.99959624, 0.5, 1, 0.013594861, -0.005599279, -0.026855469, 0.9995312, 0.53333336, 1, 0.06695535, -0.0069687692, -0.026555255, 0.9973783, 0.56666666, 1, 0.133032, -0.00858151, -0.02607674, 0.9907315, 0.6, 1, 0.2035568, -0.010185314, -0.025433162, 0.9786798, 0.6333333, 1, 0.27115333, -0.011544464, -0.024683312, 0.9621504, 0.6666667, 1, 0.32962412, -0.012445891, -0.023925329, 0.9437271, 0.7, 1, 0.37523073, -0.012756116, -0.023261111, 0.9265518, 0.73333335, 1, 0.41201654, -0.012766661, -0.022675907, 0.9108047, 0.76666665, 1, 0.44062427, -0.012577025, -0.022188993, 0.89732933, 0.8, 1, 0.46159095, -0.01228322, -0.021813935, 0.88673955, 0.8333333, 1, 0.4753288, -0.011977873, -0.02155982, 0.8794625, 0.8666667, 1, 0.47985607, -0.011836709, -0.021474738, 0.8770045, 0.9, 1, 0.47734207, -0.011755558, -0.021523565, 0.8783753, 0.93333334, 1, 0.46826446, -0.0116410125, -0.021696126, 0.8832452, 0.96666664, 1, 0.4532853, -0.0114506865, -0.021973846, 0.891021, 1, 1, 0.4332909, -0.011194147, -0.02233128, 0.9009079, 1.0333333, 1, 0.408617, -0.01087377, -0.022752108, 0.91235757, 1.0666667, 1, 0.38192475, -0.010522634, -0.023183053, 0.9238428, 1.1, 1, 0.35433102, -0.010154844, -0.023602962, 0.934767, 1.1333333, 1, 0.3270103, -0.009786054, -0.023993967, 0.94466543, 1.1666666, 1, 0.3011849, -0.00943334, -0.024341632, 0.95320845, 1.2, 1, 0.27984053, -0.009138888, -0.024613278, 0.9596874, 1.2333333, 1, 0.26359734, -0.008913061, -0.024810683, 0.9642725, 1.2666667, 1, 0.25364572, -0.0087739695, -0.024927706, 0.9669362, 1.2916666, 1, 0.24962777, -0.008717655, -0.024974115, 0.9679805)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.15718846, -0.0006623844, 0.0058093313, 0.98755133, 0.1, 1, -0.15718846, -0.0006623844, 0.0058093313, 0.98755133, 0.13333334, 1, -0.15895306, -0.00067269587, 0.00580831, 0.9872689, 0.16666667, 1, -0.16600627, -0.0007139198, 0.0058040377, 0.98610735, 0.2, 1, -0.18798405, -0.0008424747, 0.0057888166, 0.9821547, 0.23333333, 1, -0.22217438, -0.0010427267, 0.005759228, 0.97498935, 0.26666668, 1, -0.26917058, -0.0013185132, 0.0057065347, 0.96307474, 0.3, 1, -0.32921672, -0.0016718039, 0.0056181597, 0.9442363, 0.33333334, 1, -0.40202284, -0.0021016414, 0.0054773274, 0.91561085, 0.36666667, 1, -0.4856612, -0.0025976212, 0.005265465, 0.8741274, 0.4, 1, -0.5561113, -0.0030174344, 0.005039813, 0.8310871, 0.43333334, 1, -0.6014817, -0.003288878, 0.004868188, 0.79886496, 0.46666667, 1, -0.6143962, -0.0033674221, 0.0048127533, 0.78897583, 0.5, 1, -0.5888861, -0.0032191477, 0.004906319, 0.80819476, 0.56666666, 1, -0.47034097, -0.0026438334, 0.0050485907, 0.88246644, 0.6, 1, -0.4248818, -0.002537995, 0.004857485, 0.9052323, 0.6333333, 1, -0.40782976, -0.00255154, 0.0038788738, 0.9130462, 0.6666667, 1, -0.425922, -0.002464299, 0.0009889891, 0.904756, 0.7, 1, -0.47092786, -0.0017955482, -0.0054918765, 0.88215286, 0.73333335, 1, -0.5248882, -0.0008775356, -0.013325024, 0.85106647, 0.76666665, 1, -0.5800277, 0.00014984555, -0.021513239, 0.81431264, 0.8, 1, -0.6295284, 0.001143119, -0.029075159, 0.7764324, 0.8333333, 1, -0.66777927, 0.0019598932, -0.03508201, 0.7435296, 0.8666667, 1, -0.68243146, 0.0022862654, -0.03742376, 0.7299874, 0.9, 1, -0.68482196, 0.0022724676, -0.037291605, 0.7277522, 0.93333334, 1, -0.6817653, 0.0020364698, -0.03551916, 0.7307054, 0.96666664, 1, -0.6767355, 0.0016504397, -0.032618992, 0.7355014, 1, 1, -0.6700454, 0.0011413475, -0.028792495, 0.7417607, 1.0333333, 1, -0.66182566, 0.0005224901, -0.024138324, 0.74926895, 1.0666667, 1, -0.6529521, -0.00013765525, -0.01917036, 0.7571566, 1.1, 1, -0.643782, -0.0008115121, -0.01409575, 0.76507866, 1.1333333, 1, -0.63469094, -0.001471498, -0.009122213, 0.77271086, 1.1666666, 1, -0.6260717, -0.002090059, -0.004457788, 0.77975, 1.2, 1, -0.61891073, -0.0025988093, -0.0006192126, 0.7854569, 1.2333333, 1, -0.6134386, -0.0029845114, 0.0022923043, 0.7897335, 1.2666667, 1, -0.61007524, -0.0032202844, 0.004072613, 0.79232657, 1.2916666, 1, -0.6087138, -0.0033154439, 0.004791257, 0.7933685)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.52837e-08, -0.98076427, 0.19519576, 1.8274237e-06)
-
-[sub_resource type="Animation" id="Animation_g1v5p"]
-resource_name = "LongJump"
-length = 2.0833333
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, -8.7991577e-16, 0.2727054, 0.029855082, 0.033333335, 1, -2.8858535e-10, 0.3133028, 0.029855087, 0.06666667, 1, -8.7767954e-10, 0.39617515, 0.029855087, 0.1, 1, -1.6349166e-09, 0.5027013, 0.029855086, 0.13333334, 1, -2.437451e-09, 0.61559963, 0.029855082, 0.16666667, 1, -3.1719574e-09, 0.7189279, 0.029855076, 0.2, 1, -3.74145e-09, 0.79904276, 0.029855072, 0.23333333, 1, -4.160016e-09, 0.8579258, 0.029855074, 0.26666668, 1, -2.6064848e-09, 0.8930291, 0.02985508, 0.3, 1, 8.262648e-18, 0.9068298, 0.029855084, 2.0833333, 1, 8.262648e-18, 0.9068298, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.2669898, 1.1488195e-07, 3.1827668e-08, 0.9636994, 0.033333335, 1, 0.2413867, 1.1559138e-07, 3.499929e-08, 0.970429, 0.06666667, 1, 0.19885835, 1.1658304e-07, 4.0168455e-08, 0.98002833, 0.1, 1, 0.16156417, 1.1700217e-07, 3.7405467e-08, 0.98686224, 0.13333334, 1, 0.14445183, 1.1686334e-07, 2.8607241e-08, 0.98951185, 0.2, 1, 0.16480643, 1.1304738e-07, 4.8189552e-08, 0.9863259, 0.23333333, 1, 0.0873189, 1.1302127e-07, 6.352482e-08, 0.9961804, 0.26666668, 1, -0.020160053, 1.15617866e-07, 3.73012e-08, 0.99979675, 0.3, 1, -0.09979234, 1.18617194e-07, -1.209195e-08, 0.99500835, 0.33333334, 1, -0.09627263, 1.1867019e-07, -1.2455447e-08, 0.995355, 0.36666667, 1, -0.08595638, 1.1881913e-07, -1.3668828e-08, 0.9962989, 0.4, 1, -0.07003658, 1.1902431e-07, -1.5572928e-08, 0.9975445, 0.43333334, 1, -0.04815033, 1.1925687e-07, -1.820275e-08, 0.9988401, 0.46666667, 1, -0.01994492, 1.1938994e-07, -1.6080666e-08, 0.99980116, 0.5, 1, 0.014909219, 1.1919609e-07, 1.7773133e-09, 0.99988896, 0.53333336, 1, 0.056586705, 1.19166515e-07, -3.1941223e-09, 0.9983977, 0.56666666, 1, 0.09401326, 1.1896272e-07, -7.663778e-09, 0.995571, 0.6, 1, 0.119647086, 1.18560195e-07, 3.6500936e-10, 0.9928165, 0.6333333, 1, 0.13112034, 1.1818012e-07, 1.5630759e-08, 0.99136645, 2.0833333, 1, 0.13112034, 1.1818011e-07, 1.5630759e-08, 0.99136645)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.029448887, -1.9874674e-14, 7.021166e-09, 0.9995663, 0.033333335, 1, 0.035265446, 4.0841844e-11, 7.021046e-09, 0.999378, 0.06666667, 1, 0.044828832, 1.08043775e-10, 7.020332e-09, 0.9989947, 0.1, 1, 0.053158656, -1.7205217e-09, 1.6374988e-08, 0.99858606, 0.13333334, 1, 0.052270073, -4.5601274e-09, 3.042218e-08, 0.9986331, 0.16666667, 1, 0.031155162, -4.718077e-09, 3.047152e-08, 0.9995146, 0.23333333, 1, -0.062043685, -5.3681637e-09, 3.0423784e-08, 0.99807346, 0.26666668, 1, -0.09571132, -3.3138956e-09, 7.5983255e-09, 0.99540913, 0.3, 1, -0.10957023, 1.2736048e-11, -2.6556535e-08, 0.99397916, 0.33333334, 1, -0.10230357, 6.3761024e-11, -2.6555815e-08, 0.99475324, 0.36666667, 1, -0.082511395, 2.0278502e-10, -2.6555778e-08, 0.99659014, 0.4, 1, -0.055566177, 3.9195946e-10, -2.6552653e-08, 0.9984551, 0.43333334, 1, -0.022984836, 6.20485e-10, -2.6541484e-08, 0.99973583, 0.46666667, 1, 0.013683716, 6.4697936e-10, -1.869831e-08, 0.9999064, 0.5, 1, 0.052860267, -1.1590279e-14, 1.2602876e-08, 0.998602, 0.53333336, 1, 0.09135913, 2.7076708e-10, 1.2590698e-08, 0.9958181, 0.56666666, 1, 0.1233079, 4.9569704e-10, 1.257324e-08, 0.99236846, 0.6, 1, 0.14414838, 3.6025524e-10, 2.2139321e-08, 0.9895561, 0.6333333, 1, 0.1530668, -2.9245215e-13, 3.65036e-08, 0.9882159, 0.73333335, 1, 0.15180823, -9.180502e-12, 3.650502e-08, 0.98841, 0.8, 1, 0.15003878, -2.166411e-11, 3.6506915e-08, 0.9886802, 0.8333333, 1, 0.14895445, -2.931114e-11, 3.6508016e-08, 0.9888441, 0.96666664, 1, 0.14381722, -6.553193e-11, 3.6512635e-08, 0.9896043, 1, 1, 0.14247976, -7.4957165e-11, 3.6513676e-08, 0.9897977, 1.0333333, 1, 0.14118408, -8.4090034e-11, 3.6514624e-08, 0.9899834, 1.0666667, 1, 0.13995118, -9.277512e-11, 3.6515452e-08, 0.99015844, 1.1, 1, 0.13880222, -1.00864726e-10, 3.6516184e-08, 0.9903202, 1.1333333, 1, 0.1377582, -1.0821441e-10, 3.6516816e-08, 0.990466, 1.2, 1, 0.13613228, -1.196674e-10, 3.6517708e-08, 0.9906907, 1.4666667, 1, 0.13743968, 1.5873114e-11, 3.222918e-08, 0.9905102, 1.5666667, 1, 0.1401855, 3.5212597e-11, 3.2227426e-08, 0.9901253, 1.6, 1, 0.14123546, 4.261101e-11, 3.2226687e-08, 0.98997605, 1.6333333, 1, 0.14232604, 5.0294973e-11, 3.2225884e-08, 0.9898199, 1.6666666, 1, 0.14344464, 5.8178944e-11, 3.222502e-08, 0.98965836, 1.7, 1, 0.14457564, 6.6146824e-11, 3.22241e-08, 0.98949385, 1.7333333, 1, 0.14569704, 7.405157e-11, 3.2223152e-08, 0.98932934, 1.7666667, 1, 0.1467963, 8.1803196e-11, 3.222218e-08, 0.98916674, 1.8, 1, 0.1478609, 8.931111e-11, 3.22212e-08, 0.98900825, 1.8333334, 1, 0.14887835, 9.648406e-11, 3.222023e-08, 0.98885554, 1.9, 1, 0.15065265, 1.0900016e-10, 3.2218452e-08, 0.9885868, 1.9666667, 1, 0.15203811, 1.1876816e-10, 3.221699e-08, 0.9883747, 2.0666666, 1, 0.15304959, 5.0117202e-11, 3.4788517e-08, 0.98821855, 2.0833333, 1, 0.15310699, -9.212447e-15, 3.650355e-08, 0.98820966)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, 6.8190036e-15, 0.24077775, -0.010776717, 0.033333335, 1, -9.133596e-12, 0.23663558, -0.010783165, 0.06666667, 1, -2.7750172e-11, 0.22818023, -0.010796424, 0.1, 1, -5.1683206e-11, 0.21731152, -0.010813449, 0.13333334, 1, -7.705474e-11, 0.20579264, -0.010831447, 0.16666667, 1, -1.0026852e-10, 0.19525018, -0.010847881, 0.2, 1, -1.1826232e-10, 0.18707609, -0.01086067, 0.23333333, 1, -1.314578e-10, 0.18106839, -0.010870061, 0.26666668, 1, -8.234219e-11, 0.17748691, -0.010875662, 0.3, 1, 2.2448982e-14, 0.17607887, -0.01087787, 2.0833333, 1, 2.1027908e-14, 0.17607875, -0.010877871)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, 0.0054002786, -1.1920754e-07, -6.4376726e-10, 0.9999855, 0.033333335, 1, -0.005262878, -1.19193885e-07, -1.9148334e-09, 0.9999862, 0.06666667, 1, -0.027082134, -1.1912372e-07, -4.5152837e-09, 0.9996333, 0.1, 1, -0.055157587, -1.18949814e-07, -7.860455e-09, 0.9984777, 0.13333334, 1, -0.084899485, -1.186626e-07, -1.14030705e-08, 0.99638957, 0.16666667, 1, -0.11205688, -1.18307256e-07, -1.4636834e-08, 0.9937018, 0.2, 1, -0.13301675, -1.17971766e-07, -1.7131967e-08, 0.99111384, 0.23333333, 1, -0.14835931, -1.17692124e-07, -1.8958037e-08, 0.9889335, 0.26666668, 1, -0.15747733, -1.1759791e-07, -4.175884e-09, 0.98752266, 0.3, 1, -0.16105393, -1.1765306e-07, 1.9199122e-08, 0.9869456, 0.5, 1, -0.16105393, -1.17653045e-07, 1.9199124e-08, 0.9869456, 0.53333336, 1, -0.12680382, -1.18291524e-07, 2.3366635e-08, 0.99192786, 0.56666666, 1, -0.07005881, -1.1903416e-07, 3.016996e-08, 0.9975429, 0.6, 1, -0.020557415, -1.1927955e-07, 2.0131658e-08, 0.99978876, 0.6333333, 1, 0.0054002707, -1.1920753e-07, -6.437627e-10, 0.99998546, 2.0833333, 1, 0.0054002707, -1.1920752e-07, -6.437627e-10, 0.9999854)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.4215868, -3.526119e-14, -1.005141e-07, 0.90678805, 0.033333335, 1, 0.37859744, 8.80112e-11, -1.0098549e-07, 0.92556155, 0.06666667, 1, 0.30577332, 2.3299243e-10, -1.0127708e-07, 0.95210433, 0.1, 1, 0.24100249, 1.0352993e-09, -8.323431e-08, 0.9705245, 0.13333334, 1, 0.21118721, 2.1072761e-09, -5.6258926e-08, 0.97744566, 0.16666667, 1, 0.2292024, 2.074477e-09, -5.6306398e-08, 0.9733788, 0.23333333, 1, 0.3076513, 1.921532e-09, -5.6286524e-08, 0.9514992, 0.26666668, 1, 0.33565962, 1.1288095e-09, -6.698892e-08, 0.94198334, 0.3, 1, 0.3486579, -4.6624034e-14, -8.31265e-08, 0.9372501, 0.5, 1, 0.34865788, -4.4539222e-14, -8.31265e-08, 0.9372501, 0.53333336, 1, 0.28067717, 1.4010991e-10, -8.398358e-08, 0.9598022, 0.56666666, 1, 0.16454107, 3.7136327e-10, -8.4453646e-08, 0.98637027, 0.6, 1, 0.0619021, 3.0038064e-10, -5.1339917e-08, 0.9980822, 0.6333333, 1, 0.008212382, -4.4365665e-14, -1.9579829e-09, 0.99996626, 2.0833333, 1, 0.008212382, -4.5079178e-14, -1.9579824e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.444091e-08, 0.5406088, 0.3122278)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.9790704e-08, 0.06115735, 0.41855252, 0.033333335, 1, 9.979365e-08, 0.06273191, 0.4185649, 0.06666667, 1, 9.97997e-08, 0.06594579, 0.41859025, 0.1, 1, 9.9807465e-08, 0.070076935, 0.41862282, 0.13333334, 1, 9.9815686e-08, 0.07445521, 0.4186573, 0.16666667, 1, 9.9823204e-08, 0.07846242, 0.41868883, 0.2, 1, 9.982906e-08, 0.081569396, 0.41871333, 0.23333333, 1, 9.983334e-08, 0.08385278, 0.41873133, 0.26666668, 1, 9.9835894e-08, 0.08521402, 0.41874206, 2.0833333, 1, 9.98369e-08, 0.08574934, 0.4187463)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939635, 1.1920836e-07, -4.696411e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732144, 0.07649814, -0.028223995)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, 0.72313094, 0.6855802, -0.061265383, 0.05751596, 0.06666667, 1, 0.723131, 0.6855802, -0.061265387, 0.057515956, 0.1, 1, 0.719455, 0.6917926, -0.04955666, 0.036764342, 0.13333334, 1, 0.7131058, 0.7003062, -0.03193833, 0.005598295, 0.5, 1, 0.7131058, 0.7003062, -0.03193834, 0.0055983053, 0.53333336, 1, 0.7128404, 0.7002428, -0.03739662, 0.010959373, 0.56666666, 1, 0.71231365, 0.70004964, -0.046358768, 0.019765172, 0.6, 1, 0.71176416, 0.6997916, -0.054146074, 0.027419943, 0.6333333, 1, 0.7114415, 0.69962263, -0.058231913, 0.03143748, 2.0833333, 1, 0.7114415, 0.6996226, -0.058231913, 0.03143748)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009436027, 0.22304288, -8.364693e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, -0.46064812, 0.5445997, -0.52420676, 0.46521157, 0.033333335, 1, -0.43948752, 0.5712238, -0.48172328, 0.4984946, 0.06666667, 1, -0.42599088, 0.5908077, -0.43783063, 0.52705073, 0.1, 1, -0.49875936, 0.5426638, -0.480716, 0.4750445, 0.13333334, 1, -0.5838398, 0.46060932, -0.54943, 0.38091585, 0.16666667, 1, -0.5149954, 0.5364788, -0.4918649, 0.45281246, 0.2, 1, -0.26224768, 0.69588506, -0.27391776, 0.6098683, 0.23333333, 1, 0.032842334, 0.74293417, -0.01174424, 0.6684552, 0.26666668, 1, 0.23068102, 0.7069768, 0.16777067, 0.64716554, 0.3, 1, 0.2939152, 0.6831133, 0.22583443, 0.6292606, 0.33333334, 1, 0.25943986, 0.6969368, 0.19413291, 0.6397519, 0.36666667, 1, 0.16339305, 0.72548795, 0.1063665, 0.66004264, 0.4, 1, 0.03418306, 0.7428738, -0.010537762, 0.66847515, 0.43333334, 1, -0.10653778, 0.73598886, -0.13642432, 0.65449107, 0.46666667, 1, -0.23549683, 0.70538723, -0.2504414, 0.6198784, 0.5, 1, -0.3334487, 0.6647119, -0.336077, 0.57794666, 0.53333336, 1, -0.38782582, 0.63452435, -0.3831948, 0.547843, 0.56666666, 1, -0.42149425, 0.6126765, -0.41219348, 0.52637124, 0.6, 1, -0.43905455, 0.60021746, -0.4272592, 0.5142176, 0.6333333, 1, -0.44531626, 0.5956534, -0.4325585, 0.50968975, 0.7, 1, -0.44601947, 0.59615004, -0.4322056, 0.5087929, 0.73333335, 1, -0.44660294, 0.5967814, -0.43170658, 0.507964, 0.76666665, 1, -0.44730034, 0.5976038, -0.4310444, 0.5069448, 0.8, 1, -0.44809574, 0.59859306, -0.430238, 0.50575864, 0.8333333, 1, -0.44897366, 0.5997248, -0.42930648, 0.50442916, 0.8666667, 1, -0.44993094, 0.6009998, -0.42824727, 0.5029569, 0.9, 1, -0.45092416, 0.60234565, -0.42712083, 0.5014131, 0.93333334, 1, -0.45193774, 0.60373855, -0.42594644, 0.49982172, 1, 1, -0.45396584, 0.6065721, -0.42353275, 0.4965938, 1.0333333, 1, -0.45493355, 0.60794526, -0.42235142, 0.4950328, 1.0666667, 1, -0.45584744, 0.6092505, -0.42122182, 0.49354753, 1.1, 1, -0.4566935, 0.6104656, -0.42016453, 0.49216324, 1.1333333, 1, -0.4574575, 0.61156845, -0.41920006, 0.49090528, 1.1666666, 1, -0.45812547, 0.6125374, -0.4183489, 0.48979923, 1.2, 1, -0.4586376, 0.6132842, -0.41769034, 0.4889467, 1.2333333, 1, -0.4590115, 0.6138311, -0.4172068, 0.48832208, 1.4, 1, -0.45859587, 0.61332375, -0.41764668, 0.4889736, 1.4333333, 1, -0.45813504, 0.61271286, -0.41818014, 0.48971492, 1.4666667, 1, -0.45758393, 0.61198217, -0.41881642, 0.4905993, 1.5, 1, -0.45695174, 0.61114407, -0.41954377, 0.49161065, 1.5333333, 1, -0.45623073, 0.6101882, -0.42037037, 0.49276018, 1.5666667, 1, -0.45545697, 0.6091624, -0.42125392, 0.49398932, 1.6, 1, -0.45463976, 0.60807896, -0.42218292, 0.49528205, 1.6333333, 1, -0.45378873, 0.6069506, -0.42314613, 0.49662274, 1.7, 1, -0.452026, 0.60461336, -0.42512724, 0.4993816, 1.7333333, 1, -0.45114374, 0.60344356, -0.42611185, 0.5007535, 1.8, 1, -0.44943443, 0.60117704, -0.42800608, 0.5033941, 1.8333334, 1, -0.44862786, 0.60010725, -0.42889428, 0.5046327, 1.8666667, 1, -0.447889, 0.5991277, -0.42970416, 0.50576276, 1.9, 1, -0.4472162, 0.5982355, -0.43043905, 0.5067883, 1.9333333, 1, -0.44661966, 0.5974444, -0.43108836, 0.50769466, 1.9666667, 1, -0.4461098, 0.5967684, -0.43164167, 0.5084674, 2, 1, -0.4456971, 0.5962211, -0.43208855, 0.5090914, 2.0833333, 1, -0.44525388, 0.5956332, -0.43256748, 0.5097602)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.017577717, -0.09135095, 0.027113236, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.24138688, -0.15772204, 0.32360756, 0.9011849, 0.033333335, 1, 0.15675147, -0.08977367, 0.19130155, 0.96476597, 0.06666667, 1, 0.06689193, -0.01883338, 0.052619994, 0.99619377, 0.1, 1, 0.06689194, -0.018833384, 0.05262, 0.99619377, 0.13333334, 1, 0.12859657, -0.067423016, 0.14766562, 0.978321, 0.16666667, 1, 0.10414684, -0.048106786, 0.10991085, 0.9872987, 0.2, 1, 0.06256124, -0.015442686, 0.045978405, 0.99686193, 0.23333333, 1, 0.03639476, 0.0049911835, 0.005928193, 0.9993075, 0.26666668, 1, 0.023922518, 0.014699204, -0.013114388, 0.99951977, 0.3, 1, 0.020934895, 0.017021649, -0.017671373, 0.9994797, 2.0833333, 1, 0.020934897, 0.01702166, -0.017671378, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732144, 0.07649814, -0.028223995)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, 0.72313094, -0.6855802, 0.061265383, 0.05751596, 0.06666667, 1, 0.723131, -0.6855802, 0.061265387, 0.057515956, 0.1, 1, 0.719455, -0.6917926, 0.04955664, 0.036764316, 0.13333334, 1, 0.71310574, -0.70030624, 0.03193829, 0.0055982484, 0.5, 1, 0.7131058, -0.7003062, 0.03193834, 0.0055983053, 0.53333336, 1, 0.71207464, -0.69994277, 0.049896516, 0.023242326, 0.56666666, 1, 0.70939934, -0.6983811, 0.0793251, 0.052191626, 0.6, 1, 0.70608467, -0.6960501, 0.10480645, 0.077293985, 0.6333333, 1, 0.7039778, -0.6944651, 0.11813183, 0.090434656, 2.0833333, 1, 0.7039778, -0.69446504, 0.11813181, 0.09043463)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009436027, 0.22304288, -8.364693e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, -0.46064815, -0.54459965, 0.5242069, 0.46521142, 0.033333335, 1, -0.43948752, -0.57122374, 0.48172337, 0.49849448, 0.06666667, 1, -0.42599088, -0.5908077, 0.4378307, 0.5270507, 0.1, 1, -0.4987594, -0.5426638, 0.480716, 0.47504446, 0.13333334, 1, -0.5838398, -0.46060932, 0.54943, 0.3809158, 0.16666667, 1, -0.5149954, -0.5364788, 0.4918649, 0.45281246, 0.2, 1, -0.26224768, -0.69588506, 0.27391776, 0.6098683, 0.23333333, 1, 0.032842334, -0.74293417, 0.01174424, 0.6684552, 0.26666668, 1, 0.23068102, -0.7069768, -0.16777067, 0.64716554, 0.3, 1, 0.29391497, -0.6831134, -0.22583424, 0.6292607, 0.33333334, 1, 0.25943875, -0.6969372, -0.19413191, 0.6397522, 0.36666667, 1, 0.16338962, -0.7254886, -0.10636339, 0.6600431, 0.4, 1, 0.03417773, -0.742874, 0.010542564, 0.6684751, 0.43333334, 1, -0.106543496, -0.7359881, 0.13642938, 0.65448993, 0.46666667, 1, -0.23550071, -0.705386, 0.25044477, 0.619877, 0.5, 1, -0.3334487, -0.6647119, 0.336077, 0.57794666, 0.53333336, 1, -0.387819, -0.6345285, 0.38318896, 0.54784703, 0.56666666, 1, -0.42148328, -0.6126841, 0.41218406, 0.5263785, 0.6, 1, -0.43904707, -0.60022295, 0.42725277, 0.5142229, 0.6333333, 1, -0.44532102, -0.59564984, 0.43256286, 0.5096861, 0.7, 1, -0.4460588, -0.5961201, 0.43224314, 0.5087614, 0.73333335, 1, -0.44665566, -0.5967409, 0.4317594, 0.50792015, 0.76666665, 1, -0.44736427, -0.5975544, 0.43111163, 0.5068894, 0.8, 1, -0.44816887, -0.59853613, 0.4303188, 0.50569254, 0.8333333, 1, -0.4490542, -0.59966165, 0.4294, 0.50435305, 0.8666667, 1, -0.4500167, -0.6009318, 0.42835236, 0.50287193, 0.9, 1, -0.4510138, -0.6022739, 0.42723668, 0.50132, 0.93333334, 1, -0.45202997, -0.6036639, 0.42607227, 0.49972123, 0.96666664, 1, -0.45305035, -0.60507834, 0.4248788, 0.49809992, 1, 1, -0.45406005, -0.6064941, 0.4236761, 0.49648052, 1.0333333, 1, -0.4550271, -0.6078668, 0.42250192, 0.49491456, 1.0666667, 1, -0.45593992, -0.60917205, 0.4213787, 0.49342495, 1.1, 1, -0.45678458, -0.61038744, 0.42032695, 0.492037, 1.1333333, 1, -0.45754692, -0.6114909, 0.41936716, 0.4907759, 1.1666666, 1, -0.4582131, -0.61246043, 0.4185199, 0.48966724, 1.2, 1, -0.45872352, -0.6132081, 0.41786402, 0.48881304, 1.2333333, 1, -0.4590962, -0.6137556, 0.41738248, 0.48818707, 1.4, 1, -0.45867503, -0.61325276, 0.41781464, 0.48884487, 1.4333333, 1, -0.45821136, -0.6126444, 0.41834214, 0.48959085, 1.4666667, 1, -0.45765686, -0.6119168, 0.4189713, 0.49048066, 1.5, 1, -0.45702094, -0.611082, 0.4196907, 0.49149814, 1.5333333, 1, -0.45629564, -0.6101301, 0.42050812, 0.4926547, 1.5666667, 1, -0.4555172, -0.60910827, 0.42138177, 0.49389124, 1.6, 1, -0.4546952, -0.6080293, 0.42230046, 0.49519196, 1.6333333, 1, -0.45383903, -0.60690546, 0.42325282, 0.496541, 1.7, 1, -0.45206583, -0.6045777, 0.42521164, 0.49931693, 1.7666667, 1, -0.45030597, -0.60226715, 0.4271375, 0.5020482, 1.8, 1, -0.449459, -0.6011551, 0.4280581, 0.5033544, 1.8333334, 1, -0.44864738, -0.60008967, 0.428936, 0.50460064, 1.8666667, 1, -0.44790432, -0.599114, 0.42973673, 0.50573766, 1.9, 1, -0.44722757, -0.59822524, 0.43046328, 0.50676966, 1.9333333, 1, -0.44662762, -0.59743726, 0.4311052, 0.5076817, 1.9666667, 1, -0.44611484, -0.5967639, 0.43165228, 0.5084592, 2, 1, -0.44569972, -0.59621876, 0.43209407, 0.509087, 2.0833333, 1, -0.4452539, -0.59563315, 0.43256754, 0.5097602)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577725, 0.09135095, -0.027113233, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.24138688, 0.15772206, -0.32360756, 0.9011849, 0.033333335, 1, 0.15675148, 0.08977367, -0.19130157, 0.96476597, 0.06666667, 1, 0.06689194, 0.01883338, -0.052620016, 0.99619377, 0.1, 1, 0.06689193, 0.018833388, -0.05262001, 0.99619377, 0.13333334, 1, 0.12859657, 0.067423016, -0.14766562, 0.978321, 0.16666667, 1, 0.10414685, 0.048106775, -0.10991085, 0.9872987, 0.2, 1, 0.06256124, 0.0154426815, -0.045978412, 0.99686193, 0.23333333, 1, 0.036394756, -0.0049911756, -0.0059281997, 0.9993075, 0.26666668, 1, 0.023922514, -0.014699195, 0.013114383, 0.99951977, 0.3, 1, 0.020934897, -0.017021645, 0.01767138, 0.9994797, 2.0833333, 1, 0.020934897, -0.017021654, 0.017671373, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.27608398, 0.06599527, -0.0034686723, 0.033333335, 1, 0.27608398, 0.062051013, -0.008854466, 0.06666667, 1, 0.27608398, 0.053999446, -0.019848589, 0.1, 1, 0.276084, 0.04364977, -0.033980716, 0.13333334, 1, 0.27608404, 0.032681044, -0.048958216, 0.16666667, 1, 0.276084, 0.022642177, -0.06266615, 0.2, 1, 0.276084, 0.0148585485, -0.07329446, 0.23333333, 1, 0.27608398, 0.009137734, -0.08110609, 0.26666668, 1, 0.27608398, 0.005727225, -0.085763015, 0.3, 1, 0.276084, 0.0056678667, -0.08584405, 0.33333334, 1, 0.27608398, 0.010793834, -0.07884483, 0.36666667, 1, 0.27608404, 0.023016904, -0.062154412, 0.4, 1, 0.27608404, 0.03701442, -0.043041173, 0.43333334, 1, 0.276084, 0.050420474, -0.02473555, 0.46666667, 1, 0.27608398, 0.060869273, -0.01046794, 0.5, 1, 0.27608398, 0.06599515, -0.0034687321, 2.0833333, 1, 0.276084, 0.06599509, -0.0034687193)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.33861393, 0.15598229, -0.12323185, 0.91968703, 0.033333335, 1, 0.369442, 0.16036502, -0.13406312, 0.90544075, 0.06666667, 1, 0.45258006, 0.17243302, -0.15881374, 0.8603583, 0.1, 1, 0.5942902, 0.19202363, -0.1927104, 0.7568414, 0.13333334, 1, 0.77041006, 0.21414165, -0.21968023, 0.55888486, 0.16666667, 1, 0.91831803, 0.22868817, -0.21264483, 0.24326116, 0.2, 1, 0.95745564, 0.23031065, -0.16690592, -0.0487667, 0.23333333, 1, -0.9368719, -0.2232559, 0.12310074, 0.23932022, 0.26666668, 1, -0.91052616, -0.21729341, 0.09467916, 0.33876494, 0.3, 1, -0.9029904, -0.21318534, 0.0867714, 0.36281013, 0.33333334, 1, -0.9191199, -0.20522897, 0.10025227, 0.32101277, 0.36666667, 1, -0.9524691, -0.18333526, 0.13378322, 0.20320623, 0.4, 1, -0.9723166, -0.15328687, 0.17132677, 0.041841175, 0.43333334, 1, 0.96203965, 0.120045535, -0.2023511, 0.13828544, 0.46666667, 1, 0.920977, 0.091077946, -0.21910428, 0.30903015, 0.5, 1, 0.86500585, 0.07516915, -0.21803498, 0.445618, 0.53333336, 1, 0.81870466, 0.0752362, -0.18900703, 0.5369717, 0.56666666, 1, 0.7781975, 0.077367716, -0.14960077, 0.6050145, 0.6, 1, 0.7486634, 0.079633065, -0.11670498, 0.6477204, 0.6333333, 1, 0.7325478, 0.080947734, -0.09955666, 0.66851306, 0.6666667, 1, 0.72749823, 0.08083947, -0.09948162, 0.6740288, 0.7, 1, 0.7230416, 0.08065488, -0.09950203, 0.6788262, 0.73333335, 1, 0.7190012, 0.08042149, -0.099584416, 0.6831198, 0.76666665, 1, 0.71536446, 0.08014568, -0.09972331, 0.68693936, 0.8, 1, 0.71211725, 0.07983366, -0.099913, 0.6903137, 0.8333333, 1, 0.7092442, 0.079491526, -0.10014762, 0.6932707, 0.8666667, 1, 0.7068378, 0.07912127, -0.10043033, 0.69572556, 0.9, 1, 0.70474994, 0.07873821, -0.10073926, 0.69783914, 0.93333334, 1, 0.7029621, 0.078348085, -0.10106818, 0.6996364, 0.96666664, 1, 0.7014549, 0.077956535, -0.1014107, 0.7011417, 1, 1, 0.70020837, 0.077569135, -0.1017604, 0.702379, 1.0333333, 1, 0.6992678, 0.07719828, -0.10210663, 0.70330596, 1.0666667, 1, 0.6985247, 0.07684801, -0.10244028, 0.7040339, 1.1333333, 1, 0.6975453, 0.076230876, -0.10304345, 0.70498335, 1.2333333, 1, 0.6970428, 0.07563535, -0.1036464, 0.7054559, 1.3666667, 1, 0.697999, 0.07566367, -0.10366229, 0.7045045, 1.4, 1, 0.6989869, 0.07580919, -0.10355605, 0.70352435, 1.4333333, 1, 0.7002435, 0.07599437, -0.1034204, 0.70227367, 1.4666667, 1, 0.701741, 0.07621517, -0.10325794, 0.7007772, 1.5, 1, 0.70345193, 0.07646757, -0.10307138, 0.69905967, 1.5333333, 1, 0.7053941, 0.0767543, -0.10285828, 0.69709975, 1.5666667, 1, 0.70746803, 0.07706072, -0.10262919, 0.69499487, 1.6, 1, 0.70964634, 0.077382825, -0.102386855, 0.6927706, 1.6333333, 1, 0.7119018, 0.077716626, -0.10213401, 0.69045264, 1.6666666, 1, 0.71420777, 0.07805823, -0.101873465, 0.68806684, 1.7, 1, 0.7165317, 0.07840277, -0.10160884, 0.68564636, 1.7333333, 1, 0.7188282, 0.07874359, -0.10134531, 0.68323845, 1.7666667, 1, 0.7210717, 0.07907686, -0.101085804, 0.6808702, 1.8, 1, 0.72323734, 0.07939885, -0.1008334, 0.67856926, 1.8333334, 1, 0.7253004, 0.07970584, -0.100591235, 0.6763636, 1.8666667, 1, 0.72717947, 0.07998568, -0.10036913, 0.67434293, 1.9, 1, 0.7288825, 0.08023953, -0.10016662, 0.67250186, 1.9333333, 1, 0.73038566, 0.08046373, -0.09998685, 0.67086893, 1.9666667, 1, 0.7316654, 0.0806547, -0.099833034, 0.66947293, 2, 1, 0.7326981, 0.08080889, -0.09970848, 0.6683426, 2.0666666, 1, 0.7336892, 0.08095697, -0.09958847, 0.6672543, 2.0833333, 1, 0.7338038, 0.080974095, -0.09957455, 0.66712826)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.9579494, 0.016111903, 0.0034331607, 0.28646398, 0.033333335, 1, 0.94714344, 0.034600623, -0.011526727, 0.3187306, 0.06666667, 1, 0.91283387, 0.06845608, -0.038572088, 0.4006999, 0.1, 1, 0.8383156, 0.10116412, -0.0640024, 0.53188014, 0.13333334, 1, 0.7046625, 0.11669156, -0.07453728, 0.695901, 0.16666667, 1, 0.49982616, 0.10045036, -0.05810715, 0.8583164, 0.2, 1, 0.28655297, 0.06551939, -0.026121307, 0.9554645, 0.23333333, 1, 0.12475397, 0.03409174, 0.0016471692, 0.9916005, 0.26666668, 1, 0.029659266, 0.013480871, 0.019581577, 0.99927735, 0.3, 1, 0.011139904, 0.0054209526, 0.026902938, 0.99956137, 0.33333334, 1, 0.08035087, 0.0072735003, 0.026477184, 0.9963884, 0.36666667, 1, 0.26154923, 0.012004551, 0.024704367, 0.9647993, 0.4, 1, 0.46806216, 0.017164003, 0.021451399, 0.8832684, 0.43333334, 1, 0.64175093, 0.021257175, 0.017412238, 0.7664207, 0.46666667, 1, 0.752724, 0.023700047, 0.013916161, 0.6577622, 0.5, 1, 0.79647785, 0.024610812, 0.012250387, 0.6040425, 0.53333336, 1, 0.72925556, 0.023211684, 0.014759449, 0.68368834, 0.56666666, 1, 0.598116, 0.020279482, 0.018610219, 0.8009368, 0.6, 1, 0.4697334, 0.017235503, 0.021474957, 0.8823789, 0.6333333, 1, 0.39933693, 0.015511087, 0.022756949, 0.91639054, 2.0833333, 1, 0.39933693, 0.015511089, 0.022756947, 0.91639054)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.755419, -0.01618355, 0.054700073, 0.6527543, 0.033333335, 1, -0.7331433, -0.013664719, 0.06810784, 0.67651725, 0.06666667, 1, -0.70835364, -0.01822907, 0.10469819, 0.6978117, 0.1, 1, -0.69922346, -0.031698167, 0.15229431, 0.69777375, 0.13333334, 1, -0.68752605, -0.045526795, 0.18540898, 0.7006131, 0.16666667, 1, -0.61523986, -0.04063738, 0.1645421, 0.7699055, 0.2, 1, -0.4413101, -0.027729915, 0.110259525, 0.89012325, 0.23333333, 1, -0.27398038, -0.013982849, 0.053239983, 0.9601587, 0.26666668, 1, -0.15948367, -0.004025783, 0.012251582, 0.98711634, 0.3, 1, -0.11203977, 0.0003965372, -0.0058404864, 0.99368656, 0.5, 1, -0.11203976, 0.00039654595, -0.005840472, 0.9936865, 0.53333336, 1, -0.19921218, 0.00090603274, -0.005775459, 0.979939, 0.56666666, 1, -0.3414521, 0.0017418625, -0.0055661397, 0.9398811, 0.6, 1, -0.4586651, 0.0024353, -0.005286564, 0.88859016, 0.6333333, 1, -0.5164506, 0.0027789548, -0.005107688, 0.8562973, 2.0833333, 1, -0.5164506, 0.0027789543, -0.0051076883, 0.8562973)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608404, 0.06599533, -0.003468816, 0.033333335, 1, -0.276084, 0.062051006, -0.008854592, 0.06666667, 1, -0.276084, 0.053999458, -0.019848716, 0.1, 1, -0.27608398, 0.043649793, -0.03398084, 0.13333334, 1, -0.27608395, 0.03268103, -0.048958316, 0.16666667, 1, -0.276084, 0.022642117, -0.0626662, 0.2, 1, -0.27608395, 0.01485849, -0.07329449, 0.23333333, 1, -0.27608398, 0.009137677, -0.08110611, 0.26666668, 1, -0.27608395, 0.0057272147, -0.085763045, 0.3, 1, -0.27608392, 0.0056678914, -0.085844085, 0.33333334, 1, -0.27608398, 0.010793721, -0.07884488, 0.36666667, 1, -0.27608404, 0.02301693, -0.06215447, 0.4, 1, -0.276084, 0.037014447, -0.043041263, 0.43333334, 1, -0.276084, 0.05042052, -0.024735663, 0.46666667, 1, -0.27608398, 0.060869377, -0.010468063, 0.5, 1, -0.27608398, 0.06599521, -0.0034688674, 2.0833333, 1, -0.276084, 0.06599515, -0.0034688548)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.33861378, -0.15598227, 0.123231865, 0.9196871, 0.033333335, 1, 0.36976528, -0.16082112, 0.13446295, 0.9051685, 0.06666667, 1, 0.45302296, -0.17376707, 0.15997376, 0.8596417, 0.1, 1, 0.5939132, -0.19435069, 0.1947456, 0.7560219, 0.13333334, 1, 0.7685912, -0.21764271, 0.22268827, 0.55884635, 0.16666667, 1, 0.9156856, -0.2338057, 0.21677798, 0.24466753, 0.2, 1, 0.95502913, -0.23699287, 0.1712466, -0.049277775, 0.23333333, 1, -0.9342227, 0.23025513, -0.12686929, 0.24107015, 0.26666668, 1, -0.90824586, 0.22365941, -0.097689375, 0.3398863, 0.3, 1, -0.90072745, 0.21925862, -0.088790685, 0.36432397, 0.33333334, 1, -0.91372126, 0.21368983, -0.10047279, 0.3306891, 0.36666667, 1, -0.94023466, 0.20090966, -0.13022466, 0.24214816, 0.4, 1, -0.9622401, 0.18209058, -0.16442853, 0.11789974, 0.43333334, 1, -0.967601, 0.15774286, -0.19465022, -0.031255502, 0.46666667, 1, 0.9496479, -0.13004822, 0.21292315, 0.1895262, 0.5, 1, 0.9107701, -0.10305116, 0.21305642, 0.33835676, 0.53333336, 1, 0.8603762, -0.08643104, 0.18164597, 0.4682812, 0.56666666, 1, 0.8007116, -0.07584615, 0.13737807, 0.5781312, 0.6, 1, 0.74947137, -0.06988738, 0.09978452, 0.65073156, 0.6333333, 1, 0.72052395, -0.067410536, 0.0801066, 0.68548083, 0.6666667, 1, 0.71537215, -0.06681506, 0.08001663, 0.6909238, 0.7, 1, 0.7108227, -0.06628889, 0.08001344, 0.69565415, 0.73333335, 1, 0.7066962, -0.06581144, 0.0800668, 0.6998847, 0.76666665, 1, 0.7029801, -0.06538114, 0.08017173, 0.70364535, 0.8, 1, 0.6996601, -0.06499628, 0.080323115, 0.70696497, 0.8333333, 1, 0.6967206, -0.06465499, 0.08051569, 0.7098714, 0.8666667, 1, 0.6942555, -0.064367965, 0.08075251, 0.71228176, 0.9, 1, 0.6921148, -0.0641181, 0.081013784, 0.7143548, 0.93333334, 1, 0.6902798, -0.06390327, 0.081293926, 0.7161156, 0.96666664, 1, 0.6887307, -0.063721225, 0.081587225, 0.7175884, 1, 1, 0.68744737, -0.063569665, 0.08188794, 0.71879727, 1.0333333, 1, 0.6864765, -0.06345407, 0.082186945, 0.71970063, 1.0666667, 1, 0.6857074, -0.06336182, 0.082475856, 0.7204086, 1.1333333, 1, 0.6846886, -0.06323775, 0.08299965, 0.72132766, 1.3666667, 1, 0.685131, -0.063281186, 0.083543144, 0.7208409, 1.4, 1, 0.68614185, -0.06339849, 0.08345428, 0.7198788, 1.4333333, 1, 0.68742764, -0.06354774, 0.083340816, 0.718651, 1.4666667, 1, 0.68896013, -0.06372572, 0.08320499, 0.717182, 1.5, 1, 0.69071114, -0.06392914, 0.08304897, 0.71549565, 1.5333333, 1, 0.6926992, -0.06416021, 0.08287083, 0.71357113, 1.5666667, 1, 0.69482225, -0.06440713, 0.082679324, 0.71150404, 1.6, 1, 0.69705236, -0.064666666, 0.082476765, 0.70931935, 1.6333333, 1, 0.69936186, -0.0649356, 0.082265474, 0.70704234, 1.6666666, 1, 0.70172346, -0.0652108, 0.082047805, 0.70469844, 1.7, 1, 0.70410377, -0.06548837, 0.08182676, 0.70232016, 1.7333333, 1, 0.7064561, -0.06576289, 0.08160661, 0.69995385, 1.7666667, 1, 0.7087547, -0.06603131, 0.08138988, 0.6976262, 1.8, 1, 0.71097386, -0.06629061, 0.081179135, 0.69536453, 1.8333334, 1, 0.71308804, -0.0665378, 0.080976926, 0.6931962, 1.8666667, 1, 0.715014, -0.06676313, 0.08079157, 0.6912095, 1.9, 1, 0.71675956, -0.06696747, 0.080622494, 0.68939906, 1.9333333, 1, 0.71830064, -0.067147955, 0.080472454, 0.68779325, 1.9666667, 1, 0.7196127, -0.067301705, 0.08034412, 0.6864203, 2, 1, 0.72067165, -0.06742585, 0.08024015, 0.6853085, 2.0666666, 1, 0.721688, -0.06754503, 0.08014, 0.68423814, 2.0833333, 1, 0.7218056, -0.06755881, 0.080128394, 0.6841141)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, 0.95794934, -0.016111903, -0.003433241, 0.2864642, 0.033333335, 1, 0.94708234, -0.034875553, 0.011702817, 0.31887588, 0.06666667, 1, 0.9125694, -0.06924458, 0.039078966, 0.40111724, 0.1, 1, 0.8376831, -0.102452666, 0.06483166, 0.53252923, 0.13333334, 1, 0.7036397, -0.11822343, 0.075522676, 0.6965707, 0.16666667, 1, 0.49874935, -0.101767614, 0.058952242, 0.85873, 0.2, 1, 0.28585333, -0.06636139, 0.02665905, 0.9556011, 0.23333333, 1, 0.124422476, -0.034501918, -0.0013892492, 0.9916284, 0.26666668, 1, 0.029566577, -0.013603101, -0.019507252, 0.99927986, 0.3, 1, 0.011464743, -0.005430076, -0.026898596, 0.99955773, 0.33333334, 1, 0.08196987, -0.00731845, -0.026452499, 0.9962568, 0.36666667, 1, 0.2674552, -0.012161354, -0.024611756, 0.9631792, 0.4, 1, 0.47883284, -0.017434355, -0.021218196, 0.8774765, 0.43333334, 1, 0.65529555, -0.021574836, -0.017008232, 0.7548729, 0.46666667, 1, 0.7666956, -0.024004035, -0.013379429, 0.64142233, 0.5, 1, 0.8100465, -0.02489017, -0.011658861, 0.5857212, 0.53333336, 1, 0.7253756, -0.02312787, -0.014867375, 0.687804, 0.56666666, 1, 0.5506693, -0.019161759, -0.019743145, 0.83427, 0.6, 1, 0.37706685, -0.014942976, -0.02312309, 0.9257768, 0.6333333, 1, 0.28316092, -0.012575615, -0.024501419, 0.95867693, 2.0833333, 1, 0.2831609, -0.012575614, -0.024501419, 0.95867693)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.7554191, 0.01618354, -0.054699898, 0.6527543, 0.033333335, 1, -0.7314131, 0.013488855, -0.068292096, 0.6783724, 0.06666667, 1, -0.7047161, 0.018595144, -0.10550898, 0.7013539, 0.1, 1, -0.6963385, 0.0323908, -0.15312158, 0.7004409, 0.13333334, 1, -0.68842304, 0.046183348, -0.185996, 0.6995329, 0.16666667, 1, -0.621329, 0.041571617, -0.1665382, 0.7645176, 0.2, 1, -0.45292073, 0.02859575, -0.11266926, 0.8839405, 0.23333333, 1, -0.28343165, 0.014549464, -0.05501982, 0.95730233, 0.26666668, 1, -0.16354257, 0.0042417063, -0.012981529, 0.9864418, 0.3, 1, -0.11203981, -0.00039638524, 0.0058403797, 0.99368656, 0.5, 1, -0.11203981, -0.00039638122, 0.0058403765, 0.9936865, 0.53333336, 1, -0.21229614, -0.0009817177, 0.0057626097, 0.9771879, 0.56666666, 1, -0.3759133, -0.0019429994, 0.0054973243, 0.9266365, 0.6, 1, -0.5086104, -0.0027288976, 0.005137746, 0.8609772, 0.6333333, 1, -0.57270837, -0.0031109385, 0.0049082907, 0.8197386, 2.0833333, 1, -0.57270837, -0.0031109387, 0.004908289, 0.8197386)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -7.227587e-08, -0.9807644, 0.1951958, 1.8272193e-06)
-
-[sub_resource type="Animation" id="Animation_2l1sl"]
-resource_name = "Pickup"
-tracks/0/type = "method"
-tracks/0/imported = false
-tracks/0/enabled = true
-tracks/0/path = NodePath("..")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = {
-"times": PackedFloat32Array(0.46666667),
-"transitions": PackedFloat32Array(1),
-"values": [{
-"args": [],
-"method": &"pickup"
-}]
-}
-
-[sub_resource type="Animation" id="Animation_pnmcy"]
-length = 0.001
-tracks/0/type = "position_3d"
-tracks/0/imported = false
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, -3.5255984e-16, 0.86128783, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = false
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.029448897, -1.52228e-14, 7.0211668e-09, 0.9995664)
-tracks/2/type = "position_3d"
-tracks/2/imported = false
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.276084, 0.06599521, -0.0034686895)
-tracks/3/type = "position_3d"
-tracks/3/imported = false
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -5.176247e-09, 0.37479457, -3.1070382e-09)
-tracks/4/type = "position_3d"
-tracks/4/imported = false
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, 8.311794e-09, 0.3344567, -5.9434146e-09)
-tracks/5/type = "position_3d"
-tracks/5/imported = false
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, -0.27608398, 0.06599527, -0.0034688246)
-tracks/6/type = "position_3d"
-tracks/6/imported = false
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, -4.7838054e-09, 0.37479454, -3.107509e-09)
-tracks/7/type = "position_3d"
-tracks/7/imported = false
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, -3.6572172e-16, 0.15150139, -0.011741015)
-tracks/8/type = "position_3d"
-tracks/8/imported = false
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, -8.702834e-16, 0.1386815, 3.7252903e-09)
-tracks/9/type = "position_3d"
-tracks/9/imported = false
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 7.444091e-08, 0.5406087, 0.3122278)
-tracks/10/type = "position_3d"
-tracks/10/imported = false
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 9.97907e-08, 0.06115723, 0.4185525)
-tracks/11/type = "position_3d"
-tracks/11/imported = false
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, 0.26732144, 0.0764982, -0.028223984)
-tracks/12/type = "position_3d"
-tracks/12/imported = false
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435951, 0.22304288, -8.365756e-05)
-tracks/13/type = "position_3d"
-tracks/13/imported = false
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, -1.577885e-07, 0.28038162, -1.090596e-08)
-tracks/14/type = "position_3d"
-tracks/14/imported = false
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -3.7235083e-07, 0.22562927, 2.1787496e-08)
-
-[sub_resource type="Animation" id="Animation_y12g4"]
-resource_name = "Run-loop"
-length = 0.5
-loop_mode = 1
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, 6.474662e-10, 0.86128783, 0.029855084, 0.06666667, 1, 4.040187e-10, 0.84088504, 0.029855084, 0.1, 1, 1.0424197e-10, 0.8466233, 0.029855084, 0.13333334, 1, -8.287555e-11, 0.87294656, 0.029855084, 0.16666667, 1, -3.690559e-10, 0.9132056, 0.029855084, 0.2, 1, -5.2315313e-10, 0.9348836, 0.029855082, 0.23333333, 1, -2.2467112e-10, 0.8928939, 0.029855084, 0.26666668, 1, 6.345137e-11, 0.8523616, 0.029855084, 0.3, 1, 1.6316108e-10, 0.8383347, 0.029855084, 0.33333334, 1, 1.8129022e-10, 0.8357843, 0.029855084, 0.36666667, 1, 2.7193706e-11, 0.8574623, 0.029855084, 0.4, 1, -2.2596579e-10, 0.8930761, 0.029855084, 0.43333334, 1, -1.87118e-10, 0.9240446, 0.029855084, 0.46666667, 1, 1.9812409e-10, 0.92450005, 0.029855082, 0.5, 1, 6.474662e-10, 0.86128783, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.014909219, 1.1919609e-07, 1.7773133e-09, 0.99988896)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.027944015, 0.31502873, -0.018694492, 0.94848645, 0.033333335, 1, 0.028653441, 0.2304506, -0.01367544, 0.972566, 0.06666667, 1, 0.029344156, 0.084123515, -0.004992066, 0.99601066, 0.1, 1, 0.029402677, -0.055916943, 0.0033182476, 0.9979969, 0.13333334, 1, 0.029098228, -0.15359694, 0.009114797, 0.98766303, 0.16666667, 1, 0.02878988, -0.21000472, 0.0124621615, 0.97719693, 0.2, 1, 0.028482389, -0.25364745, 0.015052015, 0.96676016, 0.23333333, 1, 0.028134482, -0.29490784, 0.017500492, 0.95495117, 0.26666668, 1, 0.028343467, -0.27093977, 0.016078172, 0.96204466, 0.3, 1, 0.029051641, -0.16341613, 0.009697477, 0.9860818, 0.33333334, 1, 0.029448915, 1.1559793e-10, 3.096957e-09, 0.9995664, 0.36666667, 1, 0.029279418, 0.106947884, -0.006346522, 0.9938132, 0.4, 1, 0.028983787, 0.17671771, -0.01048682, 0.98377883, 0.43333334, 1, 0.028662942, 0.22908832, -0.013594606, 0.9728886, 0.46666667, 1, 0.028312325, 0.27465478, -0.016298616, 0.9609878, 0.5, 1, 0.027944015, 0.31502873, -0.018694492, 0.94848645)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, 3.8677803e-09, 0.15150139, -0.011741015)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, 0.08131089, -0.31806067, 0.016164348, 0.9444388, 0.033333335, 1, 0.083419785, -0.23252529, 0.011817305, 0.96893424, 0.06666667, 1, 0.08546681, -0.0848673, 0.004313091, 0.99271065, 0.1, 1, 0.085641265, 0.056178227, -0.0028550667, 0.9947369, 0.13333334, 1, 0.08474509, 0.15445226, -0.007849509, 0.9843278, 0.16666667, 1, 0.083828285, 0.21167544, -0.010757685, 0.9736789, 0.2, 1, 0.0829098, 0.25606084, -0.013013413, 0.9630106, 0.23333333, 1, 0.0818758, 0.29776788, -0.015133035, 0.95100033, 0.26666668, 1, 0.08249638, 0.27356502, -0.01390301, 0.95820844, 0.3, 1, 0.084598094, 0.16501571, -0.008386357, 0.9826203, 0.33333334, 1, 0.08577708, -5.633481e-08, 9.279793e-09, 0.99631435, 0.36666667, 1, 0.085274085, -0.10799758, 0.0054886127, 0.990472, 0.4, 1, 0.08439655, -0.17845847, 0.009069544, 0.9802794, 0.43333334, 1, 0.08344403, -0.23134404, 0.011757275, 0.96921563, 0.46666667, 1, 0.08240335, -0.27733964, 0.014094845, 0.95712787, 0.5, 1, 0.08131089, -0.31806067, 0.016164348, 0.9444388)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.008212386, -2.4667662e-10, -1.929252e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.490657e-08, 0.54060847, 0.3122278)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.00393964, 1.19189565e-07, -4.730641e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.960407e-08, 0.085749224, 0.4187463)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939639, 1.1918951e-07, -4.5851042e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732144, 0.0764982, -0.028223984)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, 0.10058353, 0.12409981, -0.70669866, 0.6892454, 0.033333335, 1, -0.16258316, -0.134338, -0.6950588, 0.6873233, 0.06666667, 1, -0.4036084, -0.3744809, -0.5887616, 0.5917974, 0.1, 1, -0.50531465, -0.47724748, -0.50417954, 0.5125378, 0.2, 1, -0.50531465, -0.47724745, -0.50417954, 0.5125378, 0.23333333, 1, -0.59479445, -0.56892586, -0.39466387, 0.4083914, 0.26666668, 1, -0.62517, -0.6005149, -0.34453222, 0.36033586, 0.3, 1, -0.5809652, -0.55464274, -0.4147522, 0.42758796, 0.33333334, 1, -0.50531465, -0.47724745, -0.50417954, 0.5125378, 0.36666667, 1, -0.4619471, -0.4332855, -0.54419196, 0.55020344, 0.4, 1, -0.41077268, -0.38168454, -0.5837857, 0.5871771, 0.43333334, 1, -0.30317432, -0.27393913, -0.6462394, 0.64452887, 0.46666667, 1, -0.12022991, -0.092504606, -0.7036226, 0.6941923, 0.5, 1, 0.10058353, 0.12409981, -0.70669866, 0.6892454)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.00943596, 0.223043, -8.3778796e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, -0.18810356, 0.7194769, -0.20869708, 0.63515013, 0.033333335, 1, -0.17773679, 0.7181614, -0.2144791, 0.6376932, 0.06666667, 1, -0.16604567, 0.71706575, -0.21908224, 0.6405065, 0.1, 1, -0.15827046, 0.716999, -0.21948016, 0.6424107, 0.13333334, 1, -0.15269406, 0.7185995, -0.21359348, 0.6439543, 0.16666667, 1, -0.14057909, 0.7220443, -0.20020284, 0.6471541, 0.2, 1, -0.1062678, 0.73031324, -0.16248329, 0.654942, 0.23333333, 1, -0.02754, 0.7416017, -0.075796425, 0.6659756, 0.26666668, 1, -0.016774097, 0.74233824, -0.06389842, 0.6667604, 0.33333334, 1, -0.15997304, 0.71652657, -0.22117868, 0.6419332, 0.36666667, 1, -0.1513545, 0.7187174, -0.21319184, 0.64427197, 0.4, 1, -0.13787074, 0.7223589, -0.19918579, 0.6476993, 0.43333334, 1, -0.12764834, 0.7258645, -0.18520793, 0.65001893, 0.46666667, 1, -0.13661739, 0.7267166, -0.18284585, 0.6479091, 0.5, 1, -0.18810356, 0.7194769, -0.20869708, 0.63515013)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, 0.47090578, -0.06646144, 0.068286166, 0.877022, 0.06666667, 1, 0.32094604, -0.076745644, 0.05648211, 0.9422917, 0.1, 1, 0.29169342, -0.07845332, 0.054085143, 0.9517536, 0.13333334, 1, 0.3203271, -0.07678275, 0.056431696, 0.94250226, 0.16666667, 1, 0.37529677, -0.07332479, 0.060857214, 0.92199373, 0.2, 1, 0.4602298, -0.0672851, 0.067474745, 0.8826713, 0.23333333, 1, 0.5321871, -0.06142801, 0.07284729, 0.84124714, 0.26666668, 1, 0.51645154, -0.06277204, 0.071692385, 0.8509981, 0.3, 1, 0.40911138, -0.07102584, 0.06352523, 0.9074953, 0.33333334, 1, 0.28184772, -0.079007775, 0.053271934, 0.9547156, 0.36666667, 1, 0.2555262, -0.08044076, 0.05108239, 0.9620948, 0.4, 1, 0.24159506, -0.08117061, 0.0499145, 0.9656873, 0.43333334, 1, 0.2360088, -0.08145778, 0.04944445, 0.9670676, 0.46666667, 1, 0.2838672, -0.07889486, 0.053438976, 0.95411706, 0.5, 1, 0.47090578, -0.06646144, 0.068286166, 0.877022)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.020934897, 0.01702166, -0.017671373, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732144, 0.0764982, -0.02822399)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.64165753, 0.6178258, 0.31275475, 0.32977468, 0.033333335, 1, -0.6071099, 0.5816935, 0.37544304, 0.3899907, 0.06666667, 1, -0.54497737, 0.5176906, 0.46102023, 0.4716531, 0.1, 1, -0.4840445, 0.45565587, 0.524634, 0.53182507, 0.13333334, 1, -0.43756002, 0.40866178, 0.56398684, 0.5687316, 0.16666667, 1, -0.38309097, 0.3538763, 0.6023132, 0.604344, 0.2, 1, -0.2183096, 0.18953112, 0.67961824, 0.67419434, 0.23333333, 1, -0.009942766, -0.01599012, 0.7137515, 0.70014596, 0.26666668, 1, -0.031541474, 0.0052086264, 0.71312356, 0.70030916, 0.3, 1, -0.2880417, 0.25885427, 0.65312475, 0.6507338, 0.33333334, 1, -0.50531465, 0.47724745, 0.50417954, 0.5125378, 0.43333334, 1, -0.50531465, 0.47724748, 0.50417954, 0.5125378, 0.46666667, 1, -0.537536, 0.51008266, 0.46967548, 0.47987068, 0.5, 1, -0.64165753, 0.6178258, 0.31275475, 0.32977468)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435973, 0.223043, -8.366026e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, 0.019411085, -0.7434065, 0.023822214, 0.66813374, 0.06666667, 1, -0.12461438, -0.726179, 0.18254313, 0.6510095, 0.1, 1, -0.15566668, -0.71763533, 0.21718931, 0.6431145, 0.13333334, 1, -0.14542578, -0.72030854, 0.20719531, 0.6458151, 0.16666667, 1, -0.13029791, -0.72431606, 0.1911505, 0.64949995, 0.2, 1, -0.124993004, -0.72738034, 0.17925693, 0.65050876, 0.23333333, 1, -0.16243222, -0.7234164, 0.19585797, 0.6418132, 0.26666668, 1, -0.18292359, -0.7188325, 0.21159202, 0.6364335, 0.3, 1, -0.17211105, -0.7175736, 0.21697618, 0.63905185, 0.33333334, 1, -0.15997307, -0.71652657, 0.22117868, 0.6419332, 0.36666667, 1, -0.15656687, -0.7174669, 0.21778023, 0.642884, 0.4, 1, -0.14664297, -0.72035325, 0.20690717, 0.6455823, 0.43333334, 1, -0.12346559, -0.72642654, 0.18140495, 0.65127033, 0.46666667, 1, -0.07435583, -0.73615295, 0.12739822, 0.66054505, 0.5, 1, 0.019411085, -0.7434065, 0.023822213, 0.66813374)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, 0.56535786, 0.05846987, -0.0752423, 0.8193232, 0.033333335, 1, 0.46610916, 0.066833384, -0.06792219, 0.8795806, 0.06666667, 1, 0.3464622, 0.075180665, -0.05854904, 0.93321157, 0.1, 1, 0.26869792, 0.07973256, -0.05218088, 0.9584996, 0.13333334, 1, 0.24644648, 0.08091867, -0.05032191, 0.9644605, 0.16666667, 1, 0.23670119, 0.08142238, -0.04950277, 0.9668984, 0.2, 1, 0.23530892, 0.08149355, -0.049385473, 0.9672382, 0.23333333, 1, 0.37935007, 0.073056296, -0.061179247, 0.9203334, 0.26666668, 1, 0.43443733, 0.06921366, -0.06549498, 0.8956473, 0.33333334, 1, 0.28184772, 0.07900774, -0.05327195, 0.95471555, 0.36666667, 1, 0.30145273, 0.077893704, -0.054888, 0.9487077, 0.4, 1, 0.34787178, 0.07509209, -0.058662564, 0.93268704, 0.43333334, 1, 0.41816357, 0.07038717, -0.06423215, 0.9033599, 0.46666667, 1, 0.49816728, 0.06428815, -0.07033606, 0.861829, 0.5, 1, 0.56535786, 0.058469865, -0.0752423, 0.8193231)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934904, -0.017021647, 0.017671384, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.276084, 0.06599521, -0.0034686895)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.69024545, 0.011143977, -0.018550321, 0.7232517, 0.033333335, 1, 0.799559, 0.014061864, -0.016467428, 0.6001972, 0.06666667, 1, 0.9475759, 0.018515779, -0.011247123, 0.31879565, 0.1, 1, 0.9995521, 0.0210284, -0.0052081514, 0.02065255, 0.13333334, 1, -0.9835823, -0.021655641, 0.0009078668, 0.17915393, 0.16666667, 1, -0.9627582, -0.021651471, -0.0011006522, 0.26949355, 0.2, 1, -0.9511829, -0.02159436, -0.0019645763, 0.30786526, 0.23333333, 1, -0.94107795, -0.021527307, -0.0026355672, 0.33749366, 0.26666668, 1, -0.9785381, -0.021681182, 0.0003438905, 0.204922, 0.3, 1, 0.99206996, 0.02039485, -0.0073431977, 0.12380387, 0.33333334, 1, 0.86062294, 0.015813095, -0.0148219485, 0.5087814, 0.36666667, 1, 0.77980095, 0.013524704, -0.016909989, 0.6256531, 0.4, 1, 0.71850395, 0.011885053, -0.018088888, 0.695186, 0.43333334, 1, 0.6823543, 0.010945874, -0.018667603, 0.7307014, 0.46666667, 1, 0.67286223, 0.010701158, -0.01880775, 0.7394513, 0.5, 1, 0.69024545, 0.011143977, -0.018550321, 0.7232517)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.38629484, 0.0105743995, 0.02312136, 0.9220249, 0.033333335, 1, 0.36679536, 0.010317178, 0.023421982, 0.9299495, 0.06666667, 1, 0.32213604, 0.009717924, 0.024063386, 0.9463377, 0.1, 1, 0.25741422, 0.008827002, 0.024881585, 0.9659405, 0.13333334, 1, 0.18212958, 0.007759887, 0.025676453, 0.9829086, 0.16666667, 1, 0.11073001, 0.0067191087, 0.026281735, 0.99348027, 0.2, 1, 0.07937123, 0.0062534465, 0.02650316, 0.9964732, 0.23333333, 1, 0.26074585, 0.0088750515, 0.024841044, 0.9650471, 0.26666668, 1, 0.54113567, 0.012546586, 0.020229056, 0.84059834, 0.3, 1, 0.7747233, 0.015076969, 0.013560433, 0.63197523, 0.33333334, 1, 0.87487924, 0.015867596, 0.009125151, 0.48399517, 0.36666667, 1, 0.87284863, 0.01585453, 0.009232709, 0.48764607, 0.4, 1, 0.8613833, 0.015777571, 0.00982264, 0.50761557, 0.43333334, 1, 0.8324511, 0.015562249, 0.011196947, 0.5537667, 0.46666667, 1, 0.7300275, 0.014639276, 0.015142243, 0.6830932, 0.5, 1, 0.38629484, 0.0105743995, 0.02312136, 0.9220249)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.38435712, 0.001967616, -0.0054841186, 0.9231661, 0.06666667, 1, -0.5953317, 0.0032447428, -0.0048528044, 0.8034589, 0.1, 1, -0.641413, 0.0035314336, -0.004657768, 0.7671735, 0.13333334, 1, -0.6334537, 0.0034885453, -0.0046958714, 0.77375853, 0.16666667, 1, -0.6116886, 0.0033595962, -0.004791412, 0.79107696, 0.2, 1, -0.5520002, 0.00300034, -0.0050235717, 0.83382344, 0.23333333, 1, -0.36018068, 0.0018544844, -0.0055466536, 0.9328643, 0.26666668, 1, -0.26103222, 0.00126939, -0.005709355, 0.96531236, 0.3, 1, -0.330221, 0.0016832455, -0.005607565, 0.94388556, 0.33333334, 1, -0.4446436, 0.0023675098, -0.0053627626, 0.8956884, 0.36666667, 1, -0.5671169, 0.0031003624, -0.0049808715, 0.82361645, 0.4, 1, -0.6645938, 0.0036869776, -0.004565873, 0.74718183, 0.43333334, 1, -0.72451735, 0.0040475708, -0.0042457166, 0.6892317, 0.46666667, 1, -0.6863924, 0.0038046704, -0.0044498937, 0.7272078, 0.5, 1, -0.38435718, 0.0019676161, -0.005484119, 0.9231662)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608398, 0.06599527, -0.0034688246)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, -0.93619317, 0.021490235, 0.0029385616, 0.3508161, 0.033333335, 1, -0.9982878, 0.021371542, -0.0036178448, 0.05432892, 0.06666667, 1, 0.94613916, -0.018490689, 0.011319214, 0.32303348, 0.1, 1, 0.82195324, -0.0146963, 0.015915072, 0.5691429, 0.13333334, 1, 0.74357474, -0.012547288, 0.017639946, 0.6683024, 0.16666667, 1, 0.69088215, -0.0111656, 0.018538151, 0.7226435, 0.2, 1, 0.67299247, -0.01070544, 0.018805528, 0.73933274, 0.23333333, 1, 0.6816019, -0.010923221, 0.01868052, 0.7314032, 0.26666668, 1, 0.76723397, -0.013176981, 0.01717842, 0.6410018, 0.3, 1, 0.9005311, -0.016991708, 0.013450477, 0.4342512, 0.33333334, 1, 0.99011123, -0.020259282, 0.00765677, 0.13860308, 0.36666667, 1, 0.99775016, -0.021397807, 0.0034189168, -0.06344362, 0.4, 1, -0.980619, 0.021669341, -0.0005624206, 0.1947214, 0.43333334, 1, -0.9609455, 0.021645725, 0.0012444372, 0.27588725, 0.46666667, 1, -0.94577324, 0.021559883, 0.0023320667, 0.32410318, 0.5, 1, -0.93619317, 0.021490235, 0.0029385616, 0.3508161)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, 0.38197926, -0.010525162, -0.023185031, 0.9238201, 0.06666667, 1, 0.8019547, -0.015319474, -0.012498591, 0.5970576, 0.1, 1, 0.87386584, -0.0158611, -0.00917897, 0.48582172, 0.13333334, 1, 0.8687334, -0.01582752, -0.0094477935, 0.4949369, 0.16666667, 1, 0.8538479, -0.015724225, -0.010195409, 0.5201852, 0.2, 1, 0.80973357, -0.015375582, -0.012180752, 0.58646977, 0.23333333, 1, 0.5708106, -0.012892538, -0.019565398, 0.8207473, 0.26666668, 1, 0.3590288, -0.010213657, -0.023538293, 0.9329738, 0.3, 1, 0.31311917, -0.009595718, -0.024185035, 0.9493574, 0.33333334, 1, 0.2953053, -0.009352029, -0.024418097, 0.9550451, 0.36666667, 1, 0.23413454, -0.008499802, -0.025145188, 0.9718419, 0.4, 1, 0.15974419, -0.007435711, -0.025881767, 0.98679113, 0.43333334, 1, 0.10070549, -0.006570436, -0.026355557, 0.99454546, 0.46666667, 1, 0.13523544, -0.0070793433, -0.02608961, 0.99044466, 0.5, 1, 0.38197926, -0.010525162, -0.023185031, 0.9238201)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.23430176, -0.0011092826, 0.0057399175, 0.97214633, 0.033333335, 1, -0.287564, -0.0014282155, 0.0056739873, 0.95774364, 0.06666667, 1, -0.38818106, -0.0020291342, 0.005495534, 0.9215646, 0.1, 1, -0.5071621, -0.0027408653, 0.005184925, 0.8618307, 0.13333334, 1, -0.61943406, -0.0034148954, 0.004772297, 0.7850268, 0.16666667, 1, -0.7074463, -0.0039462633, 0.0043436037, 0.70674276, 0.2, 1, -0.7411551, -0.004146463, 0.0041452963, 0.67130834, 0.23333333, 1, -0.5442665, -0.0029340796, 0.0050495043, 0.8388921, 0.26666668, 1, -0.43975627, -0.0023003975, 0.0053546857, 0.8980983, 0.33333334, 1, -0.64313823, -0.0035387923, 0.004648642, 0.7657278, 0.36666667, 1, -0.6397687, -0.0035241905, 0.004666649, 0.76854527, 0.4, 1, -0.62280643, -0.003424962, 0.0047433856, 0.7823541, 0.43333334, 1, -0.5823742, -0.003182632, 0.004910236, 0.8128998, 0.46666667, 1, -0.47979647, -0.0025674414, 0.0052569443, 0.8773603, 0.5, 1, -0.23430176, -0.0011092826, 0.0057399175, 0.97214633)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.345563e-08, -0.9807644, 0.1951958, 1.8277877e-06)
-
-[sub_resource type="Animation" id="Animation_r0kuy"]
-resource_name = "Sprint-loop"
-length = 0.5
-loop_mode = 1
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, 6.474662e-10, 0.86128783, 0.029855084, 0.06666667, 1, 4.8796733e-10, 0.7936464, 0.029855082, 0.1, 1, 9.7002704e-11, 0.7885934, 0.029855084, 0.13333334, 1, -1.076958e-10, 0.81739, 0.02985509, 0.16666667, 1, -4.2076378e-10, 0.8614319, 0.029855095, 0.2, 1, -5.3349425e-10, 0.8851467, 0.029855099, 0.23333333, 1, -2.2467089e-10, 0.87320286, 0.02985509, 0.26666668, 1, 1.5640629e-10, 0.8392849, 0.029855082, 0.3, 1, 6.4646155e-10, 0.80037165, 0.02985508, 0.33333334, 1, 1.6682418e-09, 0.776736, 0.029855082, 0.36666667, 1, 1.4996665e-09, 0.80045086, 0.029855086, 0.4, 1, 1.2227216e-09, 0.83941096, 0.029855091, 0.43333334, 1, 9.539777e-10, 0.8732893, 0.029855097, 0.46666667, 1, 7.580234e-10, 0.8851179, 0.029855095, 0.5, 1, 6.474662e-10, 0.86128783, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.17156997, 1.1744167e-07, 2.0452733e-08, 0.985172, 0.06666667, 1, 0.19396456, 1.16965325e-07, 2.1784652e-08, 0.98100865, 0.1, 1, 0.19461966, 1.16912275e-07, 2.4383539e-08, 0.9808789, 0.13333334, 1, 0.18262936, 1.17143955e-07, 2.582749e-08, 0.98318183, 0.16666667, 1, 0.16423595, 1.174646e-07, 2.8028769e-08, 0.9864211, 0.2, 1, 0.1543106, 1.1775629e-07, 2.008536e-08, 0.98802245, 0.23333333, 1, 0.1636805, 1.1827072e-07, -2.5363502e-08, 0.9865134, 0.26666668, 1, 0.17984566, 1.1841028e-07, -5.5329654e-08, 0.9836949, 0.3, 1, 0.19369859, 1.18165595e-07, -5.832479e-08, 0.9810611, 0.33333334, 1, 0.19954792, 1.1812639e-07, -6.437725e-08, 0.9798881, 0.36666667, 1, 0.18968645, 1.18324614e-07, -6.320375e-08, 0.9818448, 0.4, 1, 0.17344026, 1.1862436e-07, -6.126157e-08, 0.98484445, 0.43333334, 1, 0.15927532, 1.1879066e-07, -5.4996956e-08, 0.98723423, 0.46666667, 1, 0.15578055, 1.1853358e-07, -3.3714382e-08, 0.9877918, 0.5, 1, 0.17156997, 1.1744167e-07, 2.0452733e-08, 0.985172)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.14086297, -1.1886887e-14, 3.358435e-08, 0.9900291)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, 1.870354e-09, 0.1515015, -0.011740961)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, 0.0054002786, -1.19207535e-07, -6.4376543e-10, 0.9999855, 0.033333335, 1, -0.020832539, -1.19149604e-07, -3.770512e-09, 0.99978304, 0.06666667, 1, -0.0480191, -1.5388022e-07, 1.9330882e-08, 0.9988465, 0.1, 1, -0.054555625, -1.7679535e-07, 3.5895273e-08, 0.99851084, 0.13333334, 1, -0.037276637, -1.7622762e-07, 3.7436862e-08, 0.999305, 0.16666667, 1, -0.011404143, -1.7527977e-07, 3.9722366e-08, 0.999935, 0.2, 1, 0.0020393606, -1.3042516e-07, 7.429808e-09, 0.999998, 0.23333333, 1, 0.0054002753, -1.1920754e-07, -6.437643e-10, 0.9999855, 0.26666668, 1, -0.008039867, -1.191881e-07, -2.2458342e-09, 0.9999677, 0.33333334, 1, -0.061761543, -6.077377e-08, -5.2543104e-08, 0.998091, 0.36666667, 1, -0.047848314, -6.0861154e-08, -5.090189e-08, 0.99885464, 0.4, 1, -0.024961133, -6.097909e-08, -4.818301e-08, 0.99968845, 0.43333334, 1, -0.0050503807, -8.430519e-08, -2.8239329e-08, 0.9999873, 0.46666667, 1, 0.0054002726, -1.19207535e-07, -6.4376043e-10, 0.9999855, 0.5, 1, 0.005400278, -1.1920752e-07, -6.437654e-10, 0.9999854)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, -0.1140721, -5.520537e-14, 2.7196904e-08, 0.9934725)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.444092e-08, 0.5406087, 0.31222782)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.9836896e-08, 0.08574934, 0.41874626)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939635, 1.1920836e-07, -4.696411e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.2673214, 0.07649821, -0.028224103)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.11184215, -0.08423091, -0.7050044, 0.6952448, 0.06666667, 1, -0.06792164, -0.040968545, -0.71058184, 0.6991292, 0.1, 1, -0.0693737, -0.04239727, -0.7104416, 0.69904405, 0.13333334, 1, -0.09958481, -0.07214721, -0.70684004, 0.6966024, 0.16666667, 1, -0.1454215, -0.11737523, -0.6988508, 0.6904225, 0.2, 1, -0.1698593, -0.14153448, -0.69331646, 0.6858776, 0.23333333, 1, -0.1759372, -0.14754818, -0.6917991, 0.6846092, 0.26666668, 1, -0.15242337, -0.124293946, -0.6973572, 0.68921053, 0.33333334, 1, -0.05689408, -0.030121833, -0.7115497, 0.6996805, 0.36666667, 1, -0.081831954, -0.054659653, -0.70911455, 0.69819224, 0.4, 1, -0.12256828, -0.0948116, -0.70321894, 0.69388103, 0.43333334, 1, -0.1576647, -0.12947482, -0.6961909, 0.6882561, 0.46666667, 1, -0.1632194, -0.13496704, -0.6949096, 0.6872002, 0.5, 1, -0.11184215, -0.08423091, -0.7050044, 0.6952448)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435957, 0.223043, -8.380843e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, -0.29190543, 0.6839745, -0.29986897, 0.59753555)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.017577725, -0.09135095, 0.02711322, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.020934893, 0.017021654, -0.017671378, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.2673214, 0.07649821, -0.028224103)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.11184215, 0.08423091, 0.7050044, 0.6952448, 0.06666667, 1, -0.06792164, 0.040968545, 0.71058184, 0.6991292, 0.1, 1, -0.0693737, 0.04239727, 0.7104416, 0.69904405, 0.13333334, 1, -0.09958481, 0.07214721, 0.70684004, 0.6966024, 0.16666667, 1, -0.1454215, 0.11737523, 0.6988508, 0.6904225, 0.2, 1, -0.1698593, 0.14153448, 0.69331646, 0.6858776, 0.23333333, 1, -0.1759372, 0.14754818, 0.6917991, 0.6846092, 0.26666668, 1, -0.15242335, 0.124293946, 0.6973572, 0.68921053, 0.33333334, 1, -0.05689408, 0.030121833, 0.7115497, 0.6996805, 0.36666667, 1, -0.081831954, 0.054659653, 0.70911455, 0.69819224, 0.4, 1, -0.12256828, 0.0948116, 0.70321894, 0.69388103, 0.43333334, 1, -0.1576647, 0.1294748, 0.6961909, 0.6882561, 0.46666667, 1, -0.1632194, 0.13496704, 0.6949096, 0.6872002, 0.5, 1, -0.11184215, 0.08423091, 0.7050044, 0.6952448)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435957, 0.223043, -8.380843e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, -0.29190543, -0.6839745, 0.29986897, 0.59753555)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577725, 0.09135095, -0.02711322, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934893, -0.017021654, 0.017671378, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.276084, 0.06599515, -0.0034686914)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.69024533, 0.011143983, -0.01855032, 0.72325176, 0.033333335, 1, 0.7476419, 0.0126517415, -0.017563231, 0.6637493, 0.06666667, 1, 0.860785, 0.015764033, -0.014843286, 0.50850797, 0.1, 1, 0.96119195, 0.01899267, -0.01039888, 0.27502975, 0.13333334, 1, 0.9996411, 0.02105649, -0.005097216, 0.015757196, 0.16666667, 1, -0.98109335, -0.021667602, 0.00061254855, 0.19231771, 0.2, 1, -0.954914, -0.021616742, -0.0016999987, 0.2960901, 0.23333333, 1, -0.94048285, -0.021522785, -0.0026738832, 0.33914825, 0.26666668, 1, -0.9785378, -0.021681184, 0.00034386688, 0.20492332, 0.3, 1, 0.99207026, 0.02039487, -0.007343151, 0.12380111, 0.33333334, 1, 0.86062294, 0.0158131, -0.014821949, 0.5087814, 0.36666667, 1, 0.77973676, 0.013522959, -0.01691136, 0.6257329, 0.4, 1, 0.71843845, 0.011883337, -0.018090006, 0.6952538, 0.43333334, 1, 0.6823227, 0.010945062, -0.018668076, 0.7307308, 0.46666667, 1, 0.67286223, 0.010701157, -0.018807752, 0.73945135, 0.5, 1, 0.69024533, 0.011143983, -0.01855032, 0.72325176)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.38629484, 0.0105743995, 0.023121357, 0.9220249, 0.06666667, 1, 0.51177907, 0.012182877, 0.02085662, 0.8587775, 0.1, 1, 0.49797747, 0.012013447, 0.021136314, 0.8668491, 0.13333334, 1, 0.38529155, 0.010567523, 0.02313903, 0.9224442, 0.16666667, 1, 0.19813848, 0.007989308, 0.025522668, 0.97980917, 0.2, 1, 0.097002216, 0.006515709, 0.026382327, 0.9949131, 0.23333333, 1, 0.26074585, 0.0088750515, 0.024841037, 0.965047, 0.26666668, 1, 0.5411358, 0.012546586, 0.020229055, 0.84059834, 0.3, 1, 0.7747235, 0.015076969, 0.013560433, 0.6319751, 0.33333334, 1, 0.87487924, 0.015867593, 0.009125153, 0.48399517, 0.36666667, 1, 0.87284863, 0.015854528, 0.009232711, 0.48764607, 0.4, 1, 0.8613833, 0.015777567, 0.0098226415, 0.50761557, 0.43333334, 1, 0.83245116, 0.0155622475, 0.011196947, 0.55376667, 0.46666667, 1, 0.73002756, 0.014639279, 0.015142241, 0.6830932, 0.5, 1, 0.38629484, 0.0105743995, 0.023121357, 0.9220249)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.38435712, 0.001967616, -0.0054841186, 0.9231661, 0.06666667, 1, -0.7032959, 0.003898783, -0.0043457123, 0.71087337, 0.1, 1, -0.74306077, 0.0041520223, -0.0041176477, 0.66919845, 0.13333334, 1, -0.6705963, 0.0037192078, -0.004520358, 0.7417995, 0.16666667, 1, -0.52973527, 0.0028773672, -0.00509683, 0.84814286, 0.2, 1, -0.37427545, 0.0019520707, -0.0055114217, 0.92729914, 0.23333333, 1, -0.27431658, 0.0013514826, -0.0056869346, 0.96162164, 0.26666668, 1, -0.26103222, 0.0012690738, -0.0057094, 0.96531236, 0.3, 1, -0.330221, 0.0016826127, -0.0056076534, 0.94388556, 0.33333334, 1, -0.4446436, 0.0023675098, -0.005362763, 0.8956884, 0.36666667, 1, -0.5671169, 0.0031004408, -0.0049807327, 0.82361645, 0.4, 1, -0.6645938, 0.003687046, -0.004565752, 0.74718183, 0.43333334, 1, -0.72451735, 0.0040476, -0.0042456654, 0.6892317, 0.46666667, 1, -0.6863924, 0.0038046704, -0.0044498937, 0.7272078, 0.5, 1, -0.38435718, 0.0019676161, -0.005484119, 0.9231662)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608398, 0.06599521, -0.0034688117)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, -0.93619317, 0.02149024, 0.0029385602, 0.3508161, 0.033333335, 1, -0.9982878, 0.021371542, -0.003617845, 0.054328915, 0.06666667, 1, 0.9461393, -0.018490689, 0.0113192145, 0.32303345, 0.1, 1, 0.8219532, -0.014696299, 0.015915073, 0.56914294, 0.13333334, 1, 0.74357474, -0.01254729, 0.017639946, 0.6683024, 0.16666667, 1, 0.69088215, -0.011165598, 0.018538157, 0.7226435, 0.2, 1, 0.67299247, -0.010705439, 0.018805524, 0.73933274, 0.23333333, 1, 0.6816019, -0.010923222, 0.018680522, 0.7314032, 0.26666668, 1, 0.7352872, -0.012322704, 0.017794913, 0.6774099, 0.3, 1, 0.8238833, -0.014728484, 0.015875304, 0.5663457, 0.33333334, 1, 0.91324824, -0.017343158, 0.012959485, 0.40682787, 0.36666667, 1, 0.98182076, -0.019843806, 0.008662925, 0.18857135, 0.4, 1, 0.9988862, -0.02131446, 0.0038698493, -0.041918654, 0.43333334, 1, -0.97436696, 0.021675957, 8.846149e-05, 0.22391766, 0.46666667, 1, -0.9446273, 0.021551834, 0.0024088202, 0.327428, 0.5, 1, -0.93619317, 0.02149024, 0.0029385602, 0.3508161)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, 0.38197923, -0.010525161, -0.023185031, 0.9238201, 0.06666667, 1, 0.8019547, -0.015319474, -0.012498593, 0.5970576, 0.1, 1, 0.87386584, -0.015861098, -0.00917897, 0.48582172, 0.13333334, 1, 0.8687334, -0.01582752, -0.0094477935, 0.4949369, 0.16666667, 1, 0.8538479, -0.015724227, -0.010195411, 0.5201851, 0.2, 1, 0.8097335, -0.015375579, -0.012180751, 0.58646977, 0.23333333, 1, 0.5708106, -0.012892539, -0.0195654, 0.82074744, 0.26666668, 1, 0.4184811, -0.010997409, -0.022596408, 0.90787774, 0.33333334, 1, 0.5416634, -0.012548532, -0.020224143, 0.8402585, 0.36666667, 1, 0.45298886, -0.01144692, -0.02199321, 0.89117134, 0.4, 1, 0.29312307, -0.009323185, -0.02444828, 0.9557166, 0.43333334, 1, 0.14776382, -0.007262, -0.025986508, 0.9886546, 0.46666667, 1, 0.13523544, -0.0070793424, -0.02608961, 0.99044466, 0.5, 1, 0.38197923, -0.010525161, -0.023185031, 0.9238201)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.23430178, -0.0011092827, 0.0057399175, 0.97214633, 0.033333335, 1, -0.287564, -0.0014282156, 0.0056739873, 0.95774364, 0.06666667, 1, -0.38818103, -0.0020291342, 0.005495534, 0.9215646, 0.1, 1, -0.5071621, -0.0027408653, 0.005184925, 0.8618307, 0.13333334, 1, -0.61943406, -0.0034148954, 0.004772297, 0.7850268, 0.16666667, 1, -0.7074463, -0.0039462633, 0.0043436037, 0.70674276, 0.2, 1, -0.7411551, -0.004146463, 0.0041452963, 0.67130834, 0.23333333, 1, -0.5442665, -0.0029340796, 0.0050495043, 0.8388921, 0.26666668, 1, -0.47144473, -0.0024889715, 0.005269764, 0.8818763, 0.33333334, 1, -0.7685629, -0.004303868, 0.0039525954, 0.6397475, 0.36666667, 1, -0.71657526, -0.0039947415, 0.0042756293, 0.6974852, 0.4, 1, -0.6027538, -0.0033126208, 0.0048264554, 0.79790574, 0.43333334, 1, -0.4539075, -0.0024249812, 0.0053239614, 0.8910296, 0.46666667, 1, -0.31386185, -0.0015913713, 0.0056242226, 0.94945073, 0.5, 1, -0.23430178, -0.0011092827, 0.0057399175, 0.97214633)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.3456355e-08, -0.9807644, 0.1951958, 1.8277865e-06)
-
-[sub_resource type="Animation" id="Animation_83k71"]
-resource_name = "T-pose"
-length = 0.041666668
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, 8.262648e-18, 0.9068298, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.014909217, 1.1919607e-07, 1.7773131e-09, 0.99988884)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.029448895, -1.5216656e-14, 7.0211663e-09, 0.9995663)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -2.8837438e-16, 0.17607887, -0.01087787)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, 0.00540027, -1.1920752e-07, -6.4376265e-10, 0.9999854)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.008212382, -4.9427408e-14, -1.9579842e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 7.444091e-08, 0.54060894, 0.3122278)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 9.9836896e-08, 0.08574934, 0.41874626)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939635, 1.1920836e-07, -4.696411e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732144, 0.07649832, -0.02822398)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.50531465, -0.47724745, -0.50417954, 0.5125378)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435958, 0.22304296, -8.377851e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, 0.019411093, 0.7434065, -0.023822222, 0.66813374)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.017577725, -0.09135095, 0.027113236, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.020934897, 0.017021656, -0.017671376, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732144, 0.07649832, -0.02822398)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.50531465, 0.47724745, 0.50417954, 0.5125378)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435958, 0.22304296, -8.377851e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, 0.019411093, -0.7434065, 0.023822222, 0.66813374)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577725, 0.09135095, -0.027113236, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934897, -0.017021656, 0.017671376, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.27608398, 0.06599521, -0.003468678)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.99964744, 0.021063605, -0.0051235585, 0.0153314425)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, -0.006184252, 0.004953394, 0.026989238, 0.99960434)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.49280763, 0.0026219033, -0.005212944, 0.87011874)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608398, 0.06599527, -0.0034688096)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.99964744, -0.021063602, 0.0051235682, 0.0153314425)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, -0.006184278, -0.0049533797, -0.026989248, 0.99960434)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.4928077, -0.0026219054, 0.0052129473, 0.8701187)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.345749e-08, -0.98076427, 0.19519576, 1.8277871e-06)
-
-[sub_resource type="Animation" id="Animation_0qpnv"]
-resource_name = "WallJump"
-length = 0.25
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, 4.9895998e-17, 0.88615024, 0.029855084, 0.033333335, 1, -1.7827123e-11, 0.8886581, 0.029855084, 0.06666667, 1, -4.861006e-11, 0.89298856, 0.029855084, 0.1, 1, -8.092999e-11, 0.89753526, 0.029855084, 0.13333334, 1, -1.0810169e-10, 0.90135765, 0.029855084, 0.16666667, 1, -1.2817346e-10, 0.90418124, 0.029855084, 0.2, 1, -1.3960752e-10, 0.9057898, 0.029855084, 0.25, 1, 8.262648e-18, 0.9068298, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, 0.010542435, 0.7070284, -0.010542387, 0.70702803, 0.033333335, 1, 0.052229345, 0.6410371, 0.018857872, 0.76549846, 0.06666667, 1, 0.12490093, 0.5029993, 0.04928235, 0.85379314, 0.1, 1, 0.19331855, 0.33914274, 0.059258316, 0.9187484, 0.13333334, 1, 0.23955892, 0.19423516, 0.046865094, 0.950099, 0.16666667, 1, 0.2627809, 0.09036814, 0.02890614, 0.9601793, 0.2, 1, 0.269612, 0.034917634, 0.012774389, 0.96225107, 0.23333333, 1, 0.27152508, 0.0084123565, 0.0034932948, 0.9623883, 0.25, 1, 0.27172834, 1.14723946e-07, 3.2392535e-08, 0.96237403)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.029308788, 0.005770953, 0.09724895, 0.9948118, 0.033333335, 1, 0.02184548, 0.041825064, 0.080801904, 0.9956126, 0.06666667, 1, 0.0057685766, 0.078293934, 0.05549504, 0.9953678, 0.1, 1, -0.016082687, 0.090707526, 0.032264754, 0.99522483, 0.13333334, 1, -0.040832054, 0.0752538, 0.01616461, 0.996197, 0.16666667, 1, -0.06557145, 0.048580505, 0.0062309518, 0.9966452, 0.2, 1, -0.08446952, 0.021845711, 0.0020822089, 0.99618447, 0.23333333, 1, -0.094798, 0.006063135, 0.0004178021, 0.99547803, 0.25, 1, -0.09853702, -1.967219e-11, -2.2825814e-08, 0.9951334)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -1.4758672e-08, 0.15150139, -0.011741011, 0.033333335, 1, 2.0986835e-08, 0.1544821, -0.011636333, 0.06666667, 1, 1.2016299e-08, 0.15962869, -0.011455587, 0.1, 1, 6.404394e-11, 0.16503228, -0.011265821, 0.13333334, 1, 3.017942e-09, 0.16957527, -0.0111062825, 0.16666667, 1, 1.4919411e-08, 0.17293133, -0.010988418, 0.2, 1, 2.987396e-09, 0.17484292, -0.010921277, 0.25, 1, 6.817051e-15, 0.17607863, -0.010877895)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, -0.17176126, -0.558007, -0.21476267, 0.7829453, 0.033333335, 1, -0.15467392, -0.5267745, -0.19355023, 0.81309474, 0.06666667, 1, -0.13768965, -0.4575613, -0.15424718, 0.8648047, 0.1, 1, -0.1312254, -0.3612247, -0.10956328, 0.91667473, 0.13333334, 1, -0.13615249, -0.25111777, -0.068349995, 0.9558927, 0.16666667, 1, -0.14356735, -0.14173864, -0.034889746, 0.9788163, 0.2, 1, -0.15045595, -0.060130186, -0.014091808, 0.9866858, 0.23333333, 1, -0.15421139, -0.015858704, -0.0035490522, 0.9879042, 0.25, 1, -0.1556274, -1.1754841e-07, 1.8604686e-08, 0.98781586)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.008212397, -4.183208e-09, -2.9236933e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 8.934206e-08, 0.5406087, 0.31222773)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939621, 1.2341029e-07, -3.2470746e-09, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 8.488954e-08, 0.06115747, 0.41855252, 0.033333335, 1, 9.681607e-08, 0.0641398, 0.41857603, 0.06666667, 1, 9.81296e-08, 0.06928943, 0.4186166, 0.1, 1, 9.9629865e-08, 0.07469632, 0.41865918, 0.13333334, 1, 1.028049e-07, 0.07924203, 0.41869497, 0.16666667, 1, 9.983097e-08, 0.08259981, 0.4187214, 0.2, 1, 9.834447e-08, 0.08451282, 0.4187366, 0.25, 1, 9.983692e-08, 0.08574934, 0.41874635)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.00393962, 1.0850557e-07, -2.3744644e-09, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732144, 0.0764982, -0.028223986)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.50531465, -0.47724748, -0.50417954, 0.5125378, 0.033333335, 1, -0.46275744, -0.4386366, -0.5395045, 0.5498987, 0.06666667, 1, -0.38680694, -0.36538002, -0.592662, 0.6046731, 0.1, 1, -0.30442297, -0.28310868, -0.6377751, 0.6483973, 0.13333334, 1, -0.23606472, -0.21260884, -0.6674253, 0.673509, 0.16666667, 1, -0.18814553, -0.16260037, -0.6846498, 0.6851402, 0.2, 1, -0.16361715, -0.1366126, -0.6930698, 0.6886369, 0.23333333, 1, -0.15235004, -0.12455097, -0.6968901, 0.6896526, 0.25, 1, -0.14894563, -0.120857276, -0.6981083, 0.6898214)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009435987, 0.22304288, -8.318185e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, 0.21073309, 0.7094884, 0.13362557, 0.6590615, 0.033333335, 1, 0.055293307, 0.7363752, -0.006026932, 0.67428327, 0.06666667, 1, -0.14899543, 0.72380936, -0.1863764, 0.64742893, 0.1, 1, -0.2859554, 0.6843515, -0.30846986, 0.5955997, 0.13333334, 1, -0.3364613, 0.66400164, -0.3603439, 0.5621814, 0.16666667, 1, -0.35870057, 0.6539132, -0.39295122, 0.53788555, 0.2, 1, -0.3618555, 0.6528605, -0.40893397, 0.52498275, 0.23333333, 1, -0.36124057, 0.6534674, -0.41625816, 0.51885915, 0.25, 1, -0.3603166, 0.6540689, -0.41849035, 0.51694465)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.017577726, -0.09135095, 0.027113229, 0.9952944, 0.033333335, 1, 0.012799422, -0.090481125, 0.029887956, 0.9953673, 0.06666667, 1, 0.066668406, -0.08873033, 0.034742642, 0.99321467, 0.1, 1, 0.12506983, -0.08536523, 0.0375896, 0.98775375, 0.13333334, 1, 0.17641503, -0.08014669, 0.036030836, 0.9803857, 0.16666667, 1, 0.21659493, -0.0731495, 0.029308798, 0.973076, 0.2, 1, 0.2411928, -0.066829704, 0.021285158, 0.9679395, 0.23333333, 1, 0.2535837, -0.06318386, 0.016369924, 0.965109, 0.25, 1, 0.25775504, -0.06180992, 0.0144316545, 0.96412325)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.03823498, 0.028526127, 0.11746642, 0.99193054, 0.033333335, 1, 0.09159065, 0.011498777, 0.09664653, 0.99102896, 0.06666667, 1, 0.14514811, -0.0054822797, 0.061803244, 0.98746264, 0.1, 1, 0.16225956, -0.011791903, 0.028105494, 0.9862773, 0.13333334, 1, 0.13749537, -0.006409607, 0.0037213736, 0.9904747, 0.16666667, 1, 0.095759, 0.0021321545, -0.01047651, 0.99534714, 0.2, 1, 0.054473642, 0.010382755, -0.0155732045, 0.9983398, 0.23333333, 1, 0.03021666, 0.015191218, -0.017341327, 0.9992775, 0.25, 1, 0.020934874, 0.01702166, -0.017671386, 0.9994798)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.26732144, 0.076498084, -0.02822399)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.50531465, 0.47724745, 0.50417954, 0.5125378, 0.033333335, 1, -0.3478744, 0.3185912, 0.62331647, 0.6236664, 0.06666667, 1, -0.16140008, 0.13316807, 0.69533443, 0.687551, 0.1, 1, -0.059254967, 0.0328777, 0.71030205, 0.7006275, 0.13333334, 1, -0.054176178, 0.02899721, 0.70796114, 0.7035732, 0.16666667, 1, -0.050555896, 0.027287947, 0.70355844, 0.7083114, 0.2, 1, -0.04860443, 0.027201308, 0.69899476, 0.7129544, 0.23333333, 1, -0.047676425, 0.027374672, 0.69626683, 0.7156745, 0.25, 1, -0.047383107, 0.027504738, 0.6952103, 0.71671546)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435923, 0.22304288, -8.37745e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, -0.27335936, -0.6697356, 0.3441069, 0.5985978, 0.033333335, 1, -0.28152928, -0.6713126, 0.35132158, 0.5887733, 0.06666667, 1, -0.2928619, -0.6712449, 0.3611232, 0.57728, 0.1, 1, -0.304222, -0.66904503, 0.36706376, 0.5701684, 0.13333334, 1, -0.31495476, -0.6655837, 0.36644757, 0.56878644, 0.16666667, 1, -0.32673383, -0.6627444, 0.36014554, 0.5694824, 0.2, 1, -0.3363633, -0.6608467, 0.3520596, 0.57113516, 0.23333333, 1, -0.341744, -0.6598191, 0.34701934, 0.5722127, 0.25, 1, -0.34372842, -0.6594548, 0.3450093, 0.5726594)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577725, 0.09135095, -0.027113229, 0.9952944, 0.033333335, 1, 0.052209314, 0.086711854, -0.02633867, 0.9945157, 0.06666667, 1, 0.12583643, 0.08144642, -0.02568858, 0.9883683, 0.1, 1, 0.16582648, 0.07889284, -0.026730672, 0.9826306, 0.13333334, 1, 0.17036262, 0.07973574, -0.030071937, 0.98168963, 0.16666667, 1, 0.1721263, 0.08140467, -0.035166305, 0.9810755, 0.2, 1, 0.17193261, 0.08313439, -0.04002258, 0.98077834, 0.23333333, 1, 0.17155436, 0.08416173, -0.04285369, 0.9806373, 0.25, 1, 0.17133467, 0.08455842, -0.04393175, 0.98059386)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934897, -0.017021654, 0.017671384, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.276084, 0.06599521, -0.0034687473)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.60871226, 0.15789737, -0.14535801, 0.7638121, 0.033333335, 1, 0.8366778, 0.17893218, -0.24282286, 0.45715487, 0.06666667, 1, 0.9362896, 0.16938242, -0.30222353, 0.05772668, 0.1, 1, -0.92564464, -0.14257334, 0.30526137, 0.1722509, 0.13333334, 1, -0.9279271, -0.11861103, 0.28793615, 0.20487933, 0.16666667, 1, -0.93476737, -0.08398533, 0.26167336, 0.22512998, 0.2, 1, -0.9418772, -0.05072268, 0.23579197, 0.23387365, 0.23333333, 1, -0.9455924, -0.031142656, 0.22035044, 0.23734136, 0.25, 1, -0.9469591, -0.02364096, 0.21439156, 0.23821384)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.93035203, 0.015995665, 0.005694488, 0.36627445, 0.033333335, 1, 0.7008628, 0.014265977, 0.016076017, 0.71297216, 0.06666667, 1, 0.33211672, 0.00982614, 0.023933286, 0.94288343, 0.1, 1, 0.09567118, 0.0064945417, 0.026401509, 0.9950417, 0.13333334, 1, 0.059780516, 0.0059602414, 0.026636647, 0.9978384, 0.16666667, 1, 0.038757462, 0.005644241, 0.026758129, 0.9988744, 0.2, 1, 0.031015027, 0.005527307, 0.026799873, 0.9991444, 0.23333333, 1, 0.028249754, 0.005485454, 0.02681439, 0.9992262, 0.25, 1, 0.027696837, 0.005477074, 0.026817271, 0.9992416)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.49280766, 0.002621906, -0.005212948, 0.8701188, 0.033333335, 1, -0.39473307, 0.0020444952, -0.005472626, 0.91877735, 0.06666667, 1, -0.27039516, 0.0013170699, -0.0056983014, 0.96273166, 0.1, 1, -0.17971368, 0.0007894547, -0.0057982877, 0.9837016, 0.13333334, 1, -0.13916568, 0.0005543311, -0.005826791, 0.9902519, 0.16666667, 1, -0.1153446, 0.00041625477, -0.005839105, 0.9933083, 0.2, 1, -0.1065623, 0.00036528247, -0.0058429893, 0.9942888, 0.23333333, 1, -0.1034243, 0.00034702694, -0.0058443453, 0.9946201, 0.25, 1, -0.102796875, 0.0003433549, -0.005844647, 0.9946852)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608398, 0.06599521, -0.0034688246)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.9633582, 0.26788223, 0.0007352557, 0.013397454, 0.06666667, 1, 0.76155794, -0.016025718, 0.075233534, 0.6435158, 0.1, 1, 0.6409167, -0.087009035, 0.08453856, 0.7579633, 0.13333334, 1, 0.6456313, -0.07380023, 0.07321476, 0.7565404, 0.2, 1, 0.6600261, -0.02889422, 0.03469407, 0.74988467, 0.23333333, 1, 0.66380346, -0.015611384, 0.023293208, 0.74738127, 0.25, 1, 0.6651975, -0.010504751, 0.018909294, 0.7463541)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, -0.006184278, -0.0049533797, -0.02698924, 0.99960434, 0.033333335, 1, 0.3962288, -0.010664337, -0.022976182, 0.91780233, 0.06666667, 1, 0.7472301, -0.014732236, -0.014566258, 0.66424245, 0.1, 1, 0.8868972, -0.015845235, -0.008481992, 0.4616171, 0.13333334, 1, 0.90608865, -0.015952488, -0.0073554907, 0.42272314, 0.16666667, 1, 0.9165218, -0.016001292, -0.006690282, 0.3996087, 0.2, 1, 0.9202119, -0.016016658, -0.0064444556, 0.39103976, 0.23333333, 1, 0.92151016, -0.016021809, -0.0063565397, 0.38797167, 0.25, 1, 0.9217684, -0.016022818, -0.006338955, 0.38735777)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.28285727, -0.0013898644, 0.0056790444, 0.95914423, 0.06666667, 1, -0.43186814, -0.0022593588, 0.0053791776, 0.90191793, 0.1, 1, -0.45339411, -0.0023779809, 0.0053183017, 0.89129114, 0.13333334, 1, -0.41592437, -0.0021463633, 0.0054099113, 0.9093807, 0.2, 1, -0.28342736, -0.001351084, 0.0056609786, 0.95897615, 0.23333333, 1, -0.2432889, -0.0011126222, 0.0057140207, 0.96993643, 0.25, 1, -0.2277964, -0.0010208347, 0.005731782, 0.9736914)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -5.4695303e-08, -0.9807644, 0.1951958, 1.8219346e-06)
-
-[sub_resource type="Animation" id="Animation_v13o6"]
-resource_name = "WallSlide"
-length = 0.041666668
-tracks/0/type = "position_3d"
-tracks/0/imported = true
-tracks/0/enabled = true
-tracks/0/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/0/interp = 1
-tracks/0/loop_wrap = true
-tracks/0/keys = PackedFloat32Array(0, 1, 4.9895998e-17, 0.88615024, 0.029855084)
-tracks/1/type = "rotation_3d"
-tracks/1/imported = true
-tracks/1/enabled = true
-tracks/1/path = NodePath("RobotArmature/Skeleton3D:spine")
-tracks/1/interp = 1
-tracks/1/loop_wrap = true
-tracks/1/keys = PackedFloat32Array(0, 1, -0.10238464, -0.69629157, 0.123199366, 0.69965523)
-tracks/2/type = "rotation_3d"
-tracks/2/imported = true
-tracks/2/enabled = true
-tracks/2/path = NodePath("RobotArmature/Skeleton3D:spine.001")
-tracks/2/interp = 1
-tracks/2/loop_wrap = true
-tracks/2/keys = PackedFloat32Array(0, 1, 0.029308787, 0.005770954, 0.09724893, 0.9948117)
-tracks/3/type = "position_3d"
-tracks/3/imported = true
-tracks/3/enabled = true
-tracks/3/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/3/interp = 1
-tracks/3/loop_wrap = true
-tracks/3/keys = PackedFloat32Array(0, 1, -5.9462156e-08, 0.15150139, -0.011741011)
-tracks/4/type = "rotation_3d"
-tracks/4/imported = true
-tracks/4/enabled = true
-tracks/4/path = NodePath("RobotArmature/Skeleton3D:spine.004")
-tracks/4/interp = 1
-tracks/4/loop_wrap = true
-tracks/4/keys = PackedFloat32Array(0, 1, -0.17176123, -0.55800694, -0.21476263, 0.78294533)
-tracks/5/type = "rotation_3d"
-tracks/5/imported = true
-tracks/5/enabled = true
-tracks/5/path = NodePath("RobotArmature/Skeleton3D:Head")
-tracks/5/interp = 1
-tracks/5/loop_wrap = true
-tracks/5/keys = PackedFloat32Array(0, 1, 0.008212381, -1.3721294e-08, -3.9333803e-09, 0.99996626)
-tracks/6/type = "position_3d"
-tracks/6/imported = true
-tracks/6/enabled = true
-tracks/6/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/6/interp = 1
-tracks/6/loop_wrap = true
-tracks/6/keys = PackedFloat32Array(0, 1, 6.326504e-08, 0.5406087, 0.3122278)
-tracks/7/type = "rotation_3d"
-tracks/7/imported = true
-tracks/7/enabled = true
-tracks/7/path = NodePath("RobotArmature/Skeleton3D:Eyes")
-tracks/7/interp = 1
-tracks/7/loop_wrap = true
-tracks/7/keys = PackedFloat32Array(0, 1, 0.003939636, 1.12958425e-07, -2.3569233e-09, 0.99999225)
-tracks/8/type = "position_3d"
-tracks/8/imported = true
-tracks/8/enabled = true
-tracks/8/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/8/interp = 1
-tracks/8/loop_wrap = true
-tracks/8/keys = PackedFloat32Array(0, 1, 8.302691e-08, 0.06115747, 0.41855258)
-tracks/9/type = "rotation_3d"
-tracks/9/imported = true
-tracks/9/enabled = true
-tracks/9/path = NodePath("RobotArmature/Skeleton3D:Chin")
-tracks/9/interp = 1
-tracks/9/loop_wrap = true
-tracks/9/keys = PackedFloat32Array(0, 1, 0.003939635, 1.1295108e-07, -4.942926e-10, 0.99999225)
-tracks/10/type = "position_3d"
-tracks/10/imported = true
-tracks/10/enabled = true
-tracks/10/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/10/interp = 1
-tracks/10/loop_wrap = true
-tracks/10/keys = PackedFloat32Array(0, 1, 0.26732156, 0.0764982, -0.02822399)
-tracks/11/type = "rotation_3d"
-tracks/11/imported = true
-tracks/11/enabled = true
-tracks/11/path = NodePath("RobotArmature/Skeleton3D:shoulder.L")
-tracks/11/interp = 1
-tracks/11/loop_wrap = true
-tracks/11/keys = PackedFloat32Array(0, 1, -0.50531465, -0.47724745, -0.50417954, 0.5125378)
-tracks/12/type = "position_3d"
-tracks/12/imported = true
-tracks/12/enabled = true
-tracks/12/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/12/interp = 1
-tracks/12/loop_wrap = true
-tracks/12/keys = PackedFloat32Array(0, 1, 0.009436, 0.22304305, -8.3303654e-05)
-tracks/13/type = "rotation_3d"
-tracks/13/imported = true
-tracks/13/enabled = true
-tracks/13/path = NodePath("RobotArmature/Skeleton3D:upper_arm.L")
-tracks/13/interp = 1
-tracks/13/loop_wrap = true
-tracks/13/keys = PackedFloat32Array(0, 1, 0.21073304, 0.7094884, 0.13362555, 0.6590615)
-tracks/14/type = "rotation_3d"
-tracks/14/imported = true
-tracks/14/enabled = true
-tracks/14/path = NodePath("RobotArmature/Skeleton3D:forearm.L")
-tracks/14/interp = 1
-tracks/14/loop_wrap = true
-tracks/14/keys = PackedFloat32Array(0, 1, -0.017577726, -0.09135095, 0.027113229, 0.9952944)
-tracks/15/type = "rotation_3d"
-tracks/15/imported = true
-tracks/15/enabled = true
-tracks/15/path = NodePath("RobotArmature/Skeleton3D:hand.L")
-tracks/15/interp = 1
-tracks/15/loop_wrap = true
-tracks/15/keys = PackedFloat32Array(0, 1, 0.038234975, 0.028526124, 0.11746638, 0.9919305)
-tracks/16/type = "position_3d"
-tracks/16/imported = true
-tracks/16/enabled = true
-tracks/16/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/16/interp = 1
-tracks/16/loop_wrap = true
-tracks/16/keys = PackedFloat32Array(0, 1, -0.2673215, 0.0764982, -0.028223988)
-tracks/17/type = "rotation_3d"
-tracks/17/imported = true
-tracks/17/enabled = true
-tracks/17/path = NodePath("RobotArmature/Skeleton3D:shoulder.R")
-tracks/17/interp = 1
-tracks/17/loop_wrap = true
-tracks/17/keys = PackedFloat32Array(0, 1, -0.50531465, 0.47724745, 0.50417954, 0.5125378)
-tracks/18/type = "position_3d"
-tracks/18/imported = true
-tracks/18/enabled = true
-tracks/18/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/18/interp = 1
-tracks/18/loop_wrap = true
-tracks/18/keys = PackedFloat32Array(0, 1, -0.009435916, 0.22304298, -8.389395e-05)
-tracks/19/type = "rotation_3d"
-tracks/19/imported = true
-tracks/19/enabled = true
-tracks/19/path = NodePath("RobotArmature/Skeleton3D:upper_arm.R")
-tracks/19/interp = 1
-tracks/19/loop_wrap = true
-tracks/19/keys = PackedFloat32Array(0, 1, -0.27335933, -0.66973555, 0.34410688, 0.5985979)
-tracks/20/type = "rotation_3d"
-tracks/20/imported = true
-tracks/20/enabled = true
-tracks/20/path = NodePath("RobotArmature/Skeleton3D:forearm.R")
-tracks/20/interp = 1
-tracks/20/loop_wrap = true
-tracks/20/keys = PackedFloat32Array(0, 1, -0.017577725, 0.09135095, -0.027113236, 0.9952944)
-tracks/21/type = "rotation_3d"
-tracks/21/imported = true
-tracks/21/enabled = true
-tracks/21/path = NodePath("RobotArmature/Skeleton3D:hand.R")
-tracks/21/interp = 1
-tracks/21/loop_wrap = true
-tracks/21/keys = PackedFloat32Array(0, 1, 0.020934897, -0.017021658, 0.017671376, 0.9994798)
-tracks/22/type = "position_3d"
-tracks/22/imported = true
-tracks/22/enabled = true
-tracks/22/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/22/interp = 1
-tracks/22/loop_wrap = true
-tracks/22/keys = PackedFloat32Array(0, 1, 0.27608407, 0.06599515, -0.0034687456)
-tracks/23/type = "rotation_3d"
-tracks/23/imported = true
-tracks/23/enabled = true
-tracks/23/path = NodePath("RobotArmature/Skeleton3D:thigh.L")
-tracks/23/interp = 1
-tracks/23/loop_wrap = true
-tracks/23/keys = PackedFloat32Array(0, 1, 0.60871226, 0.15789737, -0.14535803, 0.7638121)
-tracks/24/type = "rotation_3d"
-tracks/24/imported = true
-tracks/24/enabled = true
-tracks/24/path = NodePath("RobotArmature/Skeleton3D:shin.L")
-tracks/24/interp = 1
-tracks/24/loop_wrap = true
-tracks/24/keys = PackedFloat32Array(0, 1, 0.930352, 0.015995678, 0.0056945034, 0.36627445)
-tracks/25/type = "rotation_3d"
-tracks/25/imported = true
-tracks/25/enabled = true
-tracks/25/path = NodePath("RobotArmature/Skeleton3D:foot.L")
-tracks/25/interp = 1
-tracks/25/loop_wrap = true
-tracks/25/keys = PackedFloat32Array(0, 1, -0.49280763, 0.002621907, -0.0052129375, 0.87011874)
-tracks/26/type = "position_3d"
-tracks/26/imported = true
-tracks/26/enabled = true
-tracks/26/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/26/interp = 1
-tracks/26/loop_wrap = true
-tracks/26/keys = PackedFloat32Array(0, 1, -0.27608404, 0.06599509, -0.0034688215)
-tracks/27/type = "rotation_3d"
-tracks/27/imported = true
-tracks/27/enabled = true
-tracks/27/path = NodePath("RobotArmature/Skeleton3D:thigh.R")
-tracks/27/interp = 1
-tracks/27/loop_wrap = true
-tracks/27/keys = PackedFloat32Array(0, 1, 0.96335834, 0.26788226, 0.0007352557, 0.013397456)
-tracks/28/type = "rotation_3d"
-tracks/28/imported = true
-tracks/28/enabled = true
-tracks/28/path = NodePath("RobotArmature/Skeleton3D:shin.R")
-tracks/28/interp = 1
-tracks/28/loop_wrap = true
-tracks/28/keys = PackedFloat32Array(0, 1, -0.006184278, -0.0049533797, -0.026989248, 0.99960434)
-tracks/29/type = "rotation_3d"
-tracks/29/imported = true
-tracks/29/enabled = true
-tracks/29/path = NodePath("RobotArmature/Skeleton3D:foot.R")
-tracks/29/interp = 1
-tracks/29/loop_wrap = true
-tracks/29/keys = PackedFloat32Array(0, 1, -0.28285727, -0.0013898577, 0.0056790407, 0.95914423)
-tracks/30/type = "rotation_3d"
-tracks/30/imported = true
-tracks/30/enabled = true
-tracks/30/path = NodePath("RobotArmature/Skeleton3D:toe.R")
-tracks/30/interp = 1
-tracks/30/loop_wrap = true
-tracks/30/keys = PackedFloat32Array(0, 1, -6.200256e-08, -0.98076427, 0.19519576, 1.82048e-06)
-
-[sub_resource type="AnimationLibrary" id="AnimationLibrary_135rq"]
-_data = {
-&"Attack1": SubResource("Animation_tvij7"),
-&"Crouch": SubResource("Animation_u3ehh"),
-&"Dive": SubResource("Animation_qx7mb"),
-&"Emote1": SubResource("Animation_65yf6"),
-&"Emote2": SubResource("Animation_k86pu"),
-&"Fall": SubResource("Animation_4w2md"),
-&"Fall2": SubResource("Animation_8gm5a"),
-&"GroundSlide": SubResource("Animation_vmng1"),
-&"Idle": SubResource("Animation_8wq35"),
-&"Jump": SubResource("Animation_8ggfx"),
-&"Jump2": SubResource("Animation_3uju1"),
-&"Jump3": SubResource("Animation_5fqsh"),
-&"Kick": SubResource("Animation_oggc5"),
-&"LongJump": SubResource("Animation_g1v5p"),
-&"Pickup": SubResource("Animation_2l1sl"),
-&"RESET": SubResource("Animation_pnmcy"),
-&"Run": SubResource("Animation_y12g4"),
-&"Sprint": SubResource("Animation_r0kuy"),
-&"T-pose": SubResource("Animation_83k71"),
-&"WallJump": SubResource("Animation_0qpnv"),
-&"WallSlide": SubResource("Animation_v13o6")
-}
-
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_wb3a5"]
-properties/0/path = NodePath(".:position")
-properties/0/spawn = true
-properties/0/replication_mode = 1
-properties/1/path = NodePath("PlayerNick/Nickname:text")
-properties/1/spawn = true
-properties/1/replication_mode = 1
-properties/2/path = NodePath("GodotRobot3D:rotation")
-properties/2/spawn = true
-properties/2/replication_mode = 1
-properties/3/path = NodePath("GodotRobot3D/RobotArmature/Skeleton3D/LeftHandAttach/Sword:visible")
-properties/3/spawn = true
-properties/3/replication_mode = 2
-properties/4/path = NodePath("GodotRobot3D/RobotArmature/Skeleton3D/LeftHandAttach/SwordBig:visible")
-properties/4/spawn = true
-properties/4/replication_mode = 2
-properties/5/path = NodePath("GodotRobot3D/RobotArmature/Skeleton3D/BackAttach/Backpack:visible")
-properties/5/spawn = true
-properties/5/replication_mode = 2
-properties/6/path = NodePath("GodotRobot3D/RobotArmature/Skeleton3D/LeftHandAttach/Axe:visible")
-properties/6/spawn = true
-properties/6/replication_mode = 2
-properties/7/path = NodePath("GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach/Fedora:visible")
-properties/7/spawn = true
-properties/7/replication_mode = 2
-properties/8/path = NodePath("GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach/Headphones:visible")
-properties/8/spawn = true
-properties/8/replication_mode = 2
-properties/9/path = NodePath("GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach/PirateHat:visible")
-properties/9/spawn = true
-properties/9/replication_mode = 2
-properties/10/path = NodePath("GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach/SheriffHat:visible")
-properties/10/spawn = true
-properties/10/replication_mode = 2
-properties/11/path = NodePath("GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach/Sombrero:visible")
-properties/11/spawn = true
-properties/11/replication_mode = 2
-properties/12/path = NodePath("GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach/WizardHat:visible")
-properties/12/spawn = true
-properties/12/replication_mode = 2
-properties/13/path = NodePath("GodotRobot3D/InfrontArea3D:rotation")
-properties/13/spawn = true
-properties/13/replication_mode = 2
-
-[node name="Player" type="CharacterBody3D" unique_id=1972894128 node_paths=PackedStringArray("_body", "_spring_arm_offset")]
-transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0)
-collision_mask = 7
-script = ExtResource("1_tdh26")
-_body = NodePath("GodotRobot3D")
-_spring_arm_offset = NodePath("SpringArmOffset")
-blue_texture = SubResource("CompressedTexture2D_bkbtv")
-yellow_texture = SubResource("CompressedTexture2D_hbshb")
-green_texture = SubResource("CompressedTexture2D_a6urh")
-red_texture = SubResource("CompressedTexture2D_xgf8p")
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=256726009]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0024593314, 0.828, 0.010000005)
-shape = SubResource("CapsuleShape3D_mx45w")
-
-[node name="GodotRobot3D" type="Node3D" parent="." unique_id=614086565 node_paths=PackedStringArray("_character", "animation_player")]
-script = ExtResource("2_mexc5")
-_character = NodePath("..")
-animation_player = NodePath("AnimationPlayer")
-
-[node name="FirstPersonCameraSource" type="Node3D" parent="GodotRobot3D" unique_id=1850216777]
-transform = Transform3D(0.5784721, 5.3845817e-15, -1.3791846e-07, 1.0866753e-09, 0.578454, 0.0045578484, 1.3791423e-07, -0.00455786, 0.5784541, 7.771561e-16, 0.996163, 0.017233532)
-
-[node name="FirstPersonCameraAnchor" type="Node3D" parent="GodotRobot3D/FirstPersonCameraSource" unique_id=1061916657]
-transform = Transform3D(-1, -4.2919936e-08, 3.9609992e-07, -1.9330494e-07, 0.9215918, -0.38816008, -3.483829e-07, -0.38816008, -0.9215918, -2.6863398e-07, 1.2807279, -0.5394225)
-
-[node name="InfrontArea3D" type="Area3D" parent="GodotRobot3D" unique_id=1374521860]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.9)
-collision_layer = 4
-collision_mask = 7
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="GodotRobot3D/InfrontArea3D" unique_id=227783224]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0)
-shape = SubResource("CylinderShape3D_2l1sl")
-
-[node name="zombie" parent="GodotRobot3D" unique_id=191730779 instance=ExtResource("3_0potc")]
-transform = Transform3D(-0.405, 0, 6.11533e-08, 0, 0.405, 0, -6.11533e-08, 0, -0.405, 0.030783774, 0, -0.029726265)
-visible = false
-
-[node name="AnimationPlayer" type="AnimationPlayer" parent="GodotRobot3D/zombie" unique_id=1360715266]
-libraries/zombie_idle = ExtResource("4_fesw4")
-libraries/zombie_jump = ExtResource("5_qce6l")
-libraries/zombie_run = ExtResource("6_bfcyu")
-
-[node name="RobotArmature" type="Node3D" parent="GodotRobot3D" unique_id=91632620]
-transform = Transform3D(0.5784721, 0, 0, 0, 0.5784721, 0, 0, 0, 0.5784721, 0, 0, 0)
-
-[node name="Skeleton3D" type="Skeleton3D" parent="GodotRobot3D/RobotArmature" unique_id=1538827268]
-bones/0/name = "spine"
-bones/0/parent = -1
-bones/0/rest = Transform3D(1, 8.6597404e-15, 2.3841866e-07, 7.1084703e-09, 0.99955547, -0.029815122, -2.3831262e-07, 0.029815119, 0.9995556, 8.262648e-18, 0.9068298, 0.029855084)
-bones/0/enabled = true
-bones/0/position = Vector3(-3.5255984e-16, 0.86128783, 0.029855084)
-bones/0/rotation = Quaternion(0.014909217, 1.19196066e-07, 1.777313e-09, 0.99988884)
-bones/0/scale = Vector3(1, 1, 1.0000001)
-bones/1/name = "spine.001"
-bones/1/parent = 0
-bones/1/rest = Transform3D(1, -1.4036242e-08, 4.1350073e-10, 1.4036241e-08, 0.9982655, -0.058872245, 4.1356157e-10, 0.058872245, 0.9982655, -3.1527922e-15, 0.26417285, 5.8078435e-09)
-bones/1/enabled = true
-bones/1/position = Vector3(-3.1527922e-15, 0.26417285, 5.8078435e-09)
-bones/1/rotation = Quaternion(0.029448897, -1.5222805e-14, 7.0211668e-09, 0.9995664)
-bones/1/scale = Vector3(1, 0.99999994, 0.99999994)
-bones/2/name = "spine.002"
-bones/2/parent = 1
-bones/2/rest = Transform3D(1, 2.5499647e-08, -4.754696e-07, 2.5499642e-08, 0.994264, 0.10695327, 4.754696e-07, -0.10695326, 0.99426407, -3.6950847e-15, 0.1695983, 7.450581e-09)
-bones/2/enabled = true
-bones/2/position = Vector3(-3.6950847e-15, 0.1695983, 7.450581e-09)
-bones/2/rotation = Quaternion(-0.053553484, -2.3807644e-07, -1.7789096e-15, 0.998565)
-bones/2/scale = Vector3(1, 0.99999994, 1)
-bones/3/name = "spine.003"
-bones/3/parent = 2
-bones/3/rest = Transform3D(1, -3.974821e-09, 2.3838544e-07, -1.3322676e-15, 0.9998612, 0.016671615, -2.3841856e-07, -0.016671615, 0.99986106, 2.2764752e-15, 0.1381715, 1.86161e-09)
-bones/3/enabled = true
-bones/3/position = Vector3(2.2764752e-15, 0.1381715, 1.86161e-09)
-bones/3/rotation = Quaternion(-0.008336097, 1.19205154e-07, 9.937394e-10, 0.99996525)
-bones/3/scale = Vector3(1, 1.0000001, 1)
-bones/4/name = "spine.004"
-bones/4/parent = 3
-bones/4/rest = Transform3D(1, 6.661338e-16, -2.3841852e-07, -2.575012e-09, 0.99994165, -0.010800382, 2.3840462e-07, 0.010800382, 0.99994165, -2.8837438e-16, 0.17607887, -0.01087787)
-bones/4/enabled = true
-bones/4/position = Vector3(-3.6572172e-16, 0.15150139, -0.011741015)
-bones/4/rotation = Quaternion(0.0054002707, -1.1920754e-07, -6.437626e-10, 0.9999854)
-bones/4/scale = Vector3(1, 0.99999994, 0.99999994)
-bones/5/name = "Head"
-bones/5/parent = 4
-bones/5/rest = Transform3D(1, 3.9158357e-09, -3.2258283e-11, -3.9158374e-09, 0.99986506, -0.016424209, -3.2060576e-11, 0.016424209, 0.99986506, -8.702834e-16, 0.1386815, 3.7252903e-09)
-bones/5/enabled = true
-bones/5/position = Vector3(-8.702834e-16, 0.1386815, 3.7252903e-09)
-bones/5/rotation = Quaternion(0.008212382, -4.9429143e-14, -1.9579842e-09, 0.99996626)
-bones/5/scale = Vector3(1, 0.99999994, 0.99999994)
-bones/6/name = "HeadTop"
-bones/6/parent = 5
-bones/6/rest = Transform3D(1, 1.8785549e-09, 2.3841119e-07, -2.220446e-16, 0.99996895, -0.007879229, -2.3841858e-07, 0.007879229, 0.99996895, 2.1688166e-17, 0.646177, 1.8785546e-09)
-bones/6/enabled = true
-bones/6/position = Vector3(2.1688166e-17, 0.646177, 1.8785546e-09)
-bones/6/rotation = Quaternion(0.0039396454, 1.19208366e-07, -4.6964244e-10, 0.99999225)
-bones/6/scale = Vector3(1, 1, 1)
-bones/7/name = "Eyes"
-bones/7/parent = 5
-bones/7/rest = Transform3D(1, 1.8785502e-09, 2.3841119e-07, 0, 0.99996895, -0.007879211, -2.3841858e-07, 0.007879211, 0.99996895, 7.444091e-08, 0.54060894, 0.3122278)
-bones/7/enabled = true
-bones/7/position = Vector3(7.444091e-08, 0.5406087, 0.3122278)
-bones/7/rotation = Quaternion(0.003939636, 1.19208366e-07, -4.696412e-10, 0.99999225)
-bones/7/scale = Vector3(1, 1, 1)
-bones/8/name = "Chin"
-bones/8/parent = 5
-bones/8/rest = Transform3D(1, 1.8785498e-09, 2.3841118e-07, 0, 0.99996895, -0.007879209, -2.3841856e-07, 0.007879209, 0.99996895, 9.9836896e-08, 0.08574934, 0.41874626)
-bones/8/enabled = true
-bones/8/position = Vector3(9.97907e-08, 0.06115723, 0.4185525)
-bones/8/rotation = Quaternion(0.003939635, 1.1920835e-07, -4.6964105e-10, 0.99999225)
-bones/8/scale = Vector3(1, 1, 1)
-bones/9/name = "shoulder.L"
-bones/9/parent = 3
-bones/9/rest = Transform3D(0.03607571, 0.9991423, 0.020323925, -0.03450185, -0.019079806, 0.9992225, 0.9987533, -0.036748875, 0.033783942, 0.26732144, 0.07649832, -0.02822398)
-bones/9/enabled = true
-bones/9/position = Vector3(0.26732144, 0.0764982, -0.028223984)
-bones/9/rotation = Quaternion(-0.50531465, -0.47724745, -0.50417954, 0.5125378)
-bones/9/scale = Vector3(1, 0.9999999, 1)
-bones/10/name = "upper_arm.L"
-bones/10/parent = 9
-bones/10/rest = Transform3D(-0.106441274, 0.06069351, 0.99246496, -0.0029721898, 0.99811137, -0.061357595, -0.99431473, -0.009480772, -0.10605984, 0.009435958, 0.22304296, -8.377851e-05)
-bones/10/enabled = true
-bones/10/position = Vector3(0.009435951, 0.22304288, -8.365756e-05)
-bones/10/rotation = Quaternion(0.019411093, 0.7434065, -0.023822218, 0.6681337)
-bones/10/scale = Vector3(1.0000001, 0.99999994, 1)
-bones/11/name = "forearm.L"
-bones/11/parent = 10
-bones/11/rest = Transform3D(0.9818399, -0.050759822, -0.18279535, 0.057182796, 0.9979118, 0.030036377, 0.18088901, -0.03994366, 0.98269194, -1.577885e-07, 0.28038162, -1.090596e-08)
-bones/11/enabled = true
-bones/11/position = Vector3(-1.577885e-07, 0.28038162, -1.090596e-08)
-bones/11/rotation = Quaternion(-0.017577725, -0.09135095, 0.027113236, 0.9952944)
-bones/11/scale = Vector3(1.0000001, 1, 0.9999999)
-bones/12/name = "hand.L"
-bones/12/parent = 11
-bones/12/rest = Transform3D(0.998796, 0.03603705, 0.033285704, -0.03461167, 0.99849886, -0.0424496, -0.034765497, 0.041246414, 0.998544, -3.7235083e-07, 0.22562927, 2.1787496e-08)
-bones/12/enabled = true
-bones/12/position = Vector3(-3.7235083e-07, 0.22562927, 2.1787496e-08)
-bones/12/rotation = Quaternion(0.020934898, 0.017021658, -0.017671376, 0.9994797)
-bones/12/scale = Vector3(1, 0.99999994, 1)
-bones/13/name = "shoulder.R"
-bones/13/parent = 3
-bones/13/rest = Transform3D(0.03607571, -0.9991423, -0.020323925, 0.03450185, -0.019079806, 0.9992225, -0.9987533, -0.036748875, 0.033783942, -0.26732144, 0.07649832, -0.02822398)
-bones/13/enabled = true
-bones/13/position = Vector3(-0.26732144, 0.07649832, -0.02822398)
-bones/13/rotation = Quaternion(-0.50531465, 0.47724745, 0.50417954, 0.5125378)
-bones/13/scale = Vector3(1, 0.9999999, 1)
-bones/14/name = "upper_arm.R"
-bones/14/parent = 13
-bones/14/rest = Transform3D(-0.106441274, -0.06069351, -0.99246496, 0.0029721898, 0.99811137, -0.061357595, 0.99431473, -0.009480772, -0.10605984, -0.009435958, 0.22304296, -8.377851e-05)
-bones/14/enabled = true
-bones/14/position = Vector3(-0.009435958, 0.22304296, -8.377851e-05)
-bones/14/rotation = Quaternion(0.019411093, -0.7434065, 0.023822218, 0.6681337)
-bones/14/scale = Vector3(1.0000001, 0.99999994, 1)
-bones/15/name = "forearm.R"
-bones/15/parent = 14
-bones/15/rest = Transform3D(0.9818399, 0.050759822, 0.18279535, -0.057182796, 0.9979118, 0.030036377, -0.18088901, -0.03994366, 0.98269194, 1.577885e-07, 0.28038162, -1.090596e-08)
-bones/15/enabled = true
-bones/15/position = Vector3(1.577885e-07, 0.28038162, -1.090596e-08)
-bones/15/rotation = Quaternion(-0.017577725, 0.09135095, -0.027113236, 0.9952944)
-bones/15/scale = Vector3(1.0000001, 1, 0.9999999)
-bones/16/name = "hand.R"
-bones/16/parent = 15
-bones/16/rest = Transform3D(0.998796, -0.03603705, -0.033285704, 0.03461167, 0.99849886, -0.0424496, 0.034765497, 0.041246414, 0.998544, 3.7235083e-07, 0.22562927, 2.1787496e-08)
-bones/16/enabled = true
-bones/16/position = Vector3(3.7235083e-07, 0.22562927, 2.1787496e-08)
-bones/16/rotation = Quaternion(0.020934898, -0.017021658, 0.017671376, 0.9994797)
-bones/16/scale = Vector3(1, 0.99999994, 1)
-bones/17/name = "thigh.L"
-bones/17/parent = 0
-bones/17/rest = Transform3D(0.9991689, 0.04226948, -0.009598177, 0.041959822, -0.99864304, -0.030869666, -0.010890561, 0.030436253, -0.999534, 0.27608398, 0.06599521, -0.003468678)
-bones/17/enabled = true
-bones/17/position = Vector3(0.276084, 0.06599521, -0.0034686895)
-bones/17/rotation = Quaternion(0.99964744, 0.021063607, -0.005123559, 0.015331446)
-bones/17/scale = Vector3(1.0001088, 1.0000005, 1.0000566)
-bones/18/name = "shin.L"
-bones/18/parent = 17
-bones/18/rest = Transform3D(0.99849397, -0.05401836, 0.009569052, 0.053895835, 0.9984665, 0.012630984, -0.010236682, -0.012096228, 0.9998745, -5.176247e-09, 0.37479457, -3.1070382e-09)
-bones/18/enabled = true
-bones/18/position = Vector3(-5.176247e-09, 0.37479457, -3.1070382e-09)
-bones/18/rotation = Quaternion(-0.00618425, 0.004953393, 0.02698923, 0.99960434)
-bones/18/scale = Vector3(0.9999999, 0.9999998, 1)
-bones/19/name = "foot.L"
-bones/19/parent = 18
-bones/19/rest = Transform3D(0.9999319, 0.0064875726, 0.009700692, -0.011655949, 0.5142269, 0.857575, 0.00057522254, -0.85762966, 0.5142675, 8.311794e-09, 0.3344567, -5.9434146e-09)
-bones/19/enabled = true
-bones/19/position = Vector3(8.311794e-09, 0.3344567, -5.9434146e-09)
-bones/19/rotation = Quaternion(-0.49280763, 0.0026219038, -0.0052129445, 0.87011874)
-bones/19/scale = Vector3(0.99999994, 1, 1)
-bones/20/name = "toe.L"
-bones/20/parent = 19
-bones/20/rest = Transform3D(-1, 5.912003e-07, 3.609609e-06, -8.3590555e-07, 0.9237972, -0.38288227, -3.560906e-06, -0.38288218, -0.9237973, 4.999042e-08, 0.25146922, -2.9747445e-08)
-bones/20/enabled = true
-bones/20/position = Vector3(4.999042e-08, 0.25146922, -2.9747445e-08)
-bones/20/rotation = Quaternion(-6.237619e-08, 0.98076427, -0.19519582, 1.8277875e-06)
-bones/20/scale = Vector3(1, 1, 1.0000001)
-bones/21/name = "thigh.R"
-bones/21/parent = 0
-bones/21/rest = Transform3D(0.9991683, -0.04226947, 0.009598193, -0.041959785, -0.99864304, -0.030869655, 0.010890574, 0.030436251, -0.99953365, -0.27608398, 0.06599527, -0.0034688096)
-bones/21/enabled = true
-bones/21/position = Vector3(-0.27608398, 0.06599527, -0.0034688246)
-bones/21/rotation = Quaternion(0.99964744, -0.021063603, 0.0051235687, 0.015331446)
-bones/21/scale = Vector3(1.0001082, 1.0000005, 1.0000563)
-bones/22/name = "shin.R"
-bones/22/parent = 21
-bones/22/rest = Transform3D(0.99849397, 0.054018386, -0.009569023, -0.053895857, 0.99846655, 0.012631038, 0.010236655, -0.012096282, 0.9998746, -4.7838054e-09, 0.37479454, -3.107509e-09)
-bones/22/enabled = true
-bones/22/position = Vector3(-4.7838054e-09, 0.37479454, -3.107509e-09)
-bones/22/rotation = Quaternion(-0.0061842767, -0.004953379, -0.026989242, 0.99960434)
-bones/22/scale = Vector3(0.9999999, 0.9999999, 1.0000001)
-bones/23/name = "foot.R"
-bones/23/parent = 22
-bones/23/rest = Transform3D(0.9999319, -0.0064875754, -0.009700698, 0.011655956, 0.5142268, 0.85757494, -0.000575223, -0.8576297, 0.5142673, 2.4934515e-08, 0.33445665, -1.3652842e-08)
-bones/23/enabled = true
-bones/23/position = Vector3(2.4934515e-08, 0.33445665, -1.3652842e-08)
-bones/23/rotation = Quaternion(-0.4928077, -0.0026219054, 0.0052129473, 0.8701187)
-bones/23/scale = Vector3(0.99999994, 1, 0.9999999)
-bones/24/name = "toe.R"
-bones/24/parent = 23
-bones/24/rest = Transform3D(-1, -5.890791e-07, -3.6100305e-06, 8.380264e-07, 0.92379737, -0.38288212, 3.5604837e-06, -0.38288215, -0.92379725, -1.9070942e-08, 0.2514693, -1.659479e-08)
-bones/24/enabled = true
-bones/24/position = Vector3(-1.9070942e-08, 0.2514693, -1.659479e-08)
-bones/24/rotation = Quaternion(6.3457485e-08, 0.9807643, -0.19519576, -1.8277873e-06)
-bones/24/scale = Vector3(1, 1.0000001, 1)
-
-[node name="Bottom" type="MeshInstance3D" parent="GodotRobot3D/RobotArmature/Skeleton3D" unique_id=1889401933]
-mesh = ExtResource("3_hbshb")
-skin = SubResource("Skin_ixatg")
-surface_material_override/0 = SubResource("StandardMaterial3D_bkbtv")
-
-[node name="Chest" type="MeshInstance3D" parent="GodotRobot3D/RobotArmature/Skeleton3D" unique_id=210452499]
-mesh = ExtResource("3_chest")
-skin = SubResource("Skin_ixatg")
-surface_material_override/0 = SubResource("StandardMaterial3D_hbshb")
-
-[node name="Face" type="MeshInstance3D" parent="GodotRobot3D/RobotArmature/Skeleton3D" unique_id=1210679125]
-mesh = ExtResource("3_face")
-skin = SubResource("Skin_ixatg")
-surface_material_override/0 = SubResource("StandardMaterial3D_a6urh")
-
-[node name="LimbsAndHead" type="MeshInstance3D" parent="GodotRobot3D/RobotArmature/Skeleton3D" unique_id=117538645]
-mesh = ExtResource("3_limbs")
-skin = SubResource("Skin_ixatg")
-surface_material_override/0 = SubResource("StandardMaterial3D_xgf8p")
-
-[node name="LeftHandAttach" type="BoneAttachment3D" parent="GodotRobot3D/RobotArmature/Skeleton3D" unique_id=1883396408]
-transform = Transform3D(-0.009290438, 0.9993846, -0.033818543, -0.9989986, -0.0077957786, 0.044063445, 0.04377265, 0.034194008, 0.9984562, 0.99623203, 1.5005425, 0.025426911)
-bone_name = "hand.L"
-bone_idx = 12
-
-[node name="Sword" parent="GodotRobot3D/RobotArmature/Skeleton3D/LeftHandAttach" unique_id=1261523624 instance=ExtResource("16_sword")]
-transform = Transform3D(0.60000014, -1.8626451e-09, 2.689194e-08, -1.4086254e-08, 0, -0.5999999, -2.6077032e-08, 0.6, 2.2351742e-08, 0.0015582442, 0.21649343, 0.15789248)
-visible = false
-
-[node name="SwordBig" parent="GodotRobot3D/RobotArmature/Skeleton3D/LeftHandAttach" unique_id=778398978 instance=ExtResource("17_sword_big")]
-transform = Transform3D(0.55, -7.450581e-09, -2.8405339e-08, 2.6251655e-08, -1.8626451e-09, -0.55, 0, 0.55, 5.5879354e-09, -0.0023369193, 0.21792626, 0.025737152)
-visible = false
-
-[node name="Axe" parent="GodotRobot3D/RobotArmature/Skeleton3D/LeftHandAttach" unique_id=352347370 instance=ExtResource("28_axe")]
-transform = Transform3D(0.75, -1.1175871e-08, 1.0128133e-08, -1.3154931e-08, 0, -0.7499999, -7.450581e-09, 0.7499998, 1.8626451e-09, 0.013932824, 0.23088384, 0.3978039)
-visible = false
-
-[node name="BackAttach" type="BoneAttachment3D" parent="GodotRobot3D/RobotArmature/Skeleton3D" unique_id=1338054728]
-transform = Transform3D(1, 7.1054274e-15, -2.3841848e-07, 4.3943658e-09, 0.99983, 0.018431313, 2.38378e-07, -0.018431328, 0.9998302, 8.123062e-17, 1.2942743, 0.05275946)
-bone_name = "spine.002"
-bone_idx = 2
-
-[node name="Backpack" parent="GodotRobot3D/RobotArmature/Skeleton3D/BackAttach" unique_id=1163041908 instance=ExtResource("27_backpack")]
-transform = Transform3D(-1.1, 2.8865799e-15, -1.4210855e-14, -6.661338e-16, 1.1000006, -1.8626451e-09, 1.4210855e-14, -1.8626451e-09, -1.1000006, -9.863218e-09, -0.029237866, -0.3850029)
-visible = false
-
-[node name="HeadAttach" type="BoneAttachment3D" parent="GodotRobot3D/RobotArmature/Skeleton3D" unique_id=1466029568]
-transform = Transform3D(1, 9.325873e-15, -2.3841854e-07, 1.878527e-09, 0.9999688, 0.007879116, 2.384112e-07, -0.007879136, 0.9999689, 1.6014702e-15, 1.7220589, 0.029791467)
-bone_name = "Head"
-bone_idx = 5
-
-[node name="FirstPersonCameraRemote" type="RemoteTransform3D" parent="GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach" unique_id=229670266]
-remote_path = NodePath("../../../../FirstPersonCameraSource")
-
-[node name="Fedora" parent="GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach" unique_id=525657311 instance=ExtResource("34_fedora")]
-transform = Transform3D(-4.8082526e-08, -6.661338e-16, -1.1, 1.8626451e-09, 1.1, -4.440892e-16, 1.1, 0, -4.8082512e-08, -1.1953105e-11, 0.93114185, -5.0127506e-05)
-visible = false
-
-[node name="Headphones" parent="GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach" unique_id=1013512068 instance=ExtResource("39_headphones")]
-transform = Transform3D(1.3033442, 0, 0, 0, 1.3033442, 0, 0, 0, 1.3033442, 0, 0.7998626, 0)
-visible = false
-
-[node name="PirateHat" parent="GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach" unique_id=1392735862 instance=ExtResource("41_pirate")]
-transform = Transform3D(1.3331646, 0.0055106063, -0.6993711, -0.0055106045, 1.5054717, 0.0013576727, 0.6993713, 0.0013576644, 1.3331753, 0.22902387, 0.19673097, 0.07076195)
-visible = false
-
-[node name="SheriffHat" parent="GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach" unique_id=1012174585 instance=ExtResource("42_sheriff")]
-transform = Transform3D(1.3335581, 0, 0, 0, 1.3335581, 0, 0, 0, 1.3335582, 0, 0.88481236, 0)
-visible = false
-
-[node name="Sombrero" parent="GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach" unique_id=966944071 instance=ExtResource("43_sombrero")]
-transform = Transform3D(1.6, -6.661338e-16, 5.684342e-14, 4.440892e-16, 1.6, 1.8626451e-09, -5.684342e-14, -2.7939677e-09, 1.6, -1.6552981e-10, 0.906646, -0.00069428235)
-visible = false
-
-[node name="WizardHat" parent="GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach" unique_id=265283568 instance=ExtResource("45_wizard")]
-transform = Transform3D(1.6, -1.6993842e-23, 1.440293e-14, 0, 1.6, 1.8878208e-09, -1.440293e-14, -1.8878208e-09, 1.6, -2.9316993e-11, 0.91060805, -0.00012296438)
-visible = false
-
-[node name="AnimationPlayer" type="AnimationPlayer" parent="GodotRobot3D" unique_id=1646389398]
-libraries/ = SubResource("AnimationLibrary_135rq")
-
-[node name="SpringArmOffset" type="Node3D" parent="." unique_id=1408823775 node_paths=PackedStringArray("_spring_arm", "_first_person_anchor")]
-transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0)
-script = ExtResource("4_ygggd")
-_spring_arm = NodePath("SpringArm3D")
-_first_person_anchor = NodePath("../GodotRobot3D/FirstPersonCameraSource/FirstPersonCameraAnchor")
-
-[node name="SpringArm3D" type="SpringArm3D" parent="SpringArmOffset" unique_id=1915471732]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
-spring_length = 5.0
-
-[node name="Camera3D" type="Camera3D" parent="SpringArmOffset/SpringArm3D" unique_id=1087327110]
-current = true
-near = 0.01
-
-[node name="FirstPersonHUD" type="CanvasLayer" parent="." unique_id=1104668221]
-layer = 10
-visible = false
-
-[node name="Crosshair" type="Control" parent="FirstPersonHUD" unique_id=1073328902]
-layout_mode = 3
-anchors_preset = 8
-anchor_left = 0.5
-anchor_top = 0.5
-anchor_right = 0.5
-anchor_bottom = 0.5
-offset_left = -6.0
-offset_top = -6.0
-offset_right = 6.0
-offset_bottom = 6.0
-grow_horizontal = 2
-grow_vertical = 2
-mouse_filter = 2
-
-[node name="Horizontal" type="ColorRect" parent="FirstPersonHUD/Crosshair" unique_id=1435305648]
-layout_mode = 0
-offset_left = 2.0
-offset_top = 5.0
-offset_right = 10.0
-offset_bottom = 7.0
-mouse_filter = 2
-
-[node name="Vertical" type="ColorRect" parent="FirstPersonHUD/Crosshair" unique_id=150511760]
-layout_mode = 0
-offset_left = 5.0
-offset_top = 2.0
-offset_right = 7.0
-offset_bottom = 10.0
-mouse_filter = 2
-
-[node name="PlayerNick" type="Node3D" parent="." unique_id=438524062]
-
-[node name="Nickname" type="Label3D" parent="PlayerNick" unique_id=978269633]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
-billboard = 1
-outline_modulate = Color(0, 0, 0, 0.301961)
-text = "player name"
-font = ExtResource("16_xgf8p")
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." unique_id=1634180657]
-replication_config = SubResource("SceneReplicationConfig_wb3a5")
-
-[node name="Voice" type="Node" parent="." unique_id=44300263]
-script = ExtResource("5_voice")
diff --git a/scenes/ui/inventory_slot_ui.tscn b/scenes/ui/inventory_slot_ui.tscn
deleted file mode 100644
index 0f5c538..0000000
--- a/scenes/ui/inventory_slot_ui.tscn
+++ /dev/null
@@ -1,64 +0,0 @@
-[gd_scene load_steps=3 format=3 uid="uid://bglwdpf2mf7g0"]
-
-[ext_resource type="Script" uid="uid://bi0mlil0a0e5i" path="res://scripts/ui/inventory_slot_ui.gd" id="1_5k8m7"]
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_slot"]
-bg_color = Color(0.105, 0.115, 0.135, 0.88)
-border_width_left = 1
-border_width_top = 1
-border_width_right = 1
-border_width_bottom = 1
-border_color = Color(0.52, 0.57, 0.64, 0.38)
-corner_radius_top_left = 3
-corner_radius_top_right = 3
-corner_radius_bottom_right = 3
-corner_radius_bottom_left = 3
-
-[node name="InventorySlotUI" type="Control" unique_id=1288455076]
-layout_mode = 3
-anchors_preset = 0
-offset_right = 64.0
-offset_bottom = 64.0
-script = ExtResource("1_5k8m7")
-
-[node name="Background" type="Panel" parent="." unique_id=1237187040]
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-mouse_filter = 2
-theme_override_styles/panel = SubResource("StyleBoxFlat_slot")
-
-[node name="ItemIcon" type="TextureRect" parent="." unique_id=1347257846]
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-offset_left = 8.0
-offset_top = 8.0
-offset_right = -8.0
-offset_bottom = -8.0
-grow_horizontal = 2
-grow_vertical = 2
-mouse_filter = 2
-expand_mode = 1
-stretch_mode = 5
-
-[node name="QuantityLabel" type="Label" parent="." unique_id=1224415972]
-layout_mode = 1
-anchors_preset = 3
-anchor_left = 1.0
-anchor_top = 1.0
-anchor_right = 1.0
-anchor_bottom = 1.0
-offset_left = -20.0
-offset_top = -20.0
-grow_horizontal = 0
-grow_vertical = 0
-mouse_filter = 2
-theme_override_font_sizes/font_size = 14
-text = "99"
-horizontal_alignment = 2
-vertical_alignment = 2
diff --git a/scenes/ui/inventory_ui.tscn b/scenes/ui/inventory_ui.tscn
deleted file mode 100644
index 64b2398..0000000
--- a/scenes/ui/inventory_ui.tscn
+++ /dev/null
@@ -1,205 +0,0 @@
-[gd_scene load_steps=5 format=3 uid="uid://bclq8vh1x2goy"]
-
-[ext_resource type="Script" uid="uid://c2w12kgcvtww4" path="res://scripts/ui/inventory_ui.gd" id="1_h7x4v"]
-[ext_resource type="Texture2D" path="res://assets/ui/close.svg" id="2_close"]
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"]
-bg_color = Color(0.055, 0.06, 0.07, 0.82)
-border_width_left = 1
-border_width_top = 1
-border_width_right = 1
-border_width_bottom = 1
-border_color = Color(0.55, 0.58, 0.62, 0.45)
-corner_radius_top_left = 6
-corner_radius_top_right = 6
-corner_radius_bottom_right = 6
-corner_radius_bottom_left = 6
-
-[sub_resource type="StyleBoxLine" id="StyleBoxLine_separator"]
-color = Color(0.65, 0.68, 0.72, 0.42)
-
-[node name="InventoryUI" type="Control" unique_id=1878305014]
-process_mode = 3
-layout_mode = 3
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-script = ExtResource("1_h7x4v")
-
-[node name="SafeArea" type="MarginContainer" parent="."]
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-theme_override_constants/margin_left = 16
-theme_override_constants/margin_top = 16
-theme_override_constants/margin_right = 16
-theme_override_constants/margin_bottom = 16
-
-[node name="CenterContainer" type="CenterContainer" parent="SafeArea"]
-layout_mode = 2
-
-[node name="InventoryPanels" type="HBoxContainer" parent="SafeArea/CenterContainer"]
-layout_mode = 2
-theme_override_constants/separation = 10
-
-[node name="EquipmentPanel" type="PanelContainer" parent="SafeArea/CenterContainer/InventoryPanels"]
-custom_minimum_size = Vector2(132, 0)
-layout_mode = 2
-theme_override_styles/panel = SubResource("StyleBoxFlat_panel")
-
-[node name="MarginContainer" type="MarginContainer" parent="SafeArea/CenterContainer/InventoryPanels/EquipmentPanel"]
-layout_mode = 2
-theme_override_constants/margin_left = 10
-theme_override_constants/margin_top = 10
-theme_override_constants/margin_right = 10
-theme_override_constants/margin_bottom = 10
-
-[node name="VBoxContainer" type="VBoxContainer" parent="SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer"]
-layout_mode = 2
-theme_override_constants/separation = 6
-
-[node name="Title" type="Label" parent="SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer"]
-custom_minimum_size = Vector2(0, 28)
-layout_mode = 2
-theme_override_font_sizes/font_size = 20
-text = "Equipment"
-horizontal_alignment = 1
-vertical_alignment = 1
-
-[node name="HSeparator" type="HSeparator" parent="SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer"]
-layout_mode = 2
-theme_override_styles/separator = SubResource("StyleBoxLine_separator")
-
-[node name="HatLabel" type="Label" parent="SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer"]
-layout_mode = 2
-theme_override_font_sizes/font_size = 15
-text = "Hat"
-horizontal_alignment = 1
-
-[node name="HatSlotContainer" type="CenterContainer" parent="SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer"]
-custom_minimum_size = Vector2(0, 70)
-layout_mode = 2
-size_flags_vertical = 3
-
-[node name="WeaponLabel" type="Label" parent="SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer"]
-layout_mode = 2
-theme_override_font_sizes/font_size = 15
-text = "Weapon"
-horizontal_alignment = 1
-
-[node name="WeaponSlotContainer" type="CenterContainer" parent="SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer"]
-custom_minimum_size = Vector2(0, 70)
-layout_mode = 2
-size_flags_vertical = 3
-
-[node name="BackpackLabel" type="Label" parent="SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer"]
-layout_mode = 2
-theme_override_font_sizes/font_size = 15
-text = "Backpack"
-horizontal_alignment = 1
-
-[node name="BackpackSlotContainer" type="CenterContainer" parent="SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer"]
-custom_minimum_size = Vector2(0, 70)
-layout_mode = 2
-size_flags_vertical = 3
-
-[node name="InventoryPanel" type="PanelContainer" parent="SafeArea/CenterContainer/InventoryPanels" unique_id=486763135]
-custom_minimum_size = Vector2(296, 0)
-layout_mode = 2
-theme_override_styles/panel = SubResource("StyleBoxFlat_panel")
-
-[node name="MarginContainer" type="MarginContainer" parent="SafeArea/CenterContainer/InventoryPanels/InventoryPanel" unique_id=1913204593]
-layout_mode = 2
-theme_override_constants/margin_left = 10
-theme_override_constants/margin_top = 10
-theme_override_constants/margin_right = 10
-theme_override_constants/margin_bottom = 10
-
-[node name="VBoxContainer" type="VBoxContainer" parent="SafeArea/CenterContainer/InventoryPanels/InventoryPanel/MarginContainer" unique_id=599957956]
-layout_mode = 2
-theme_override_constants/separation = 6
-
-[node name="TitleBar" type="HBoxContainer" parent="SafeArea/CenterContainer/InventoryPanels/InventoryPanel/MarginContainer/VBoxContainer" unique_id=145161483]
-custom_minimum_size = Vector2(0, 28)
-layout_mode = 2
-
-[node name="TitleSpacer" type="Control" parent="SafeArea/CenterContainer/InventoryPanels/InventoryPanel/MarginContainer/VBoxContainer/TitleBar"]
-custom_minimum_size = Vector2(28, 0)
-layout_mode = 2
-mouse_filter = 2
-
-[node name="Title" type="Label" parent="SafeArea/CenterContainer/InventoryPanels/InventoryPanel/MarginContainer/VBoxContainer/TitleBar" unique_id=860210156]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 20
-text = "Inventory"
-horizontal_alignment = 1
-vertical_alignment = 1
-
-[node name="CloseButton" type="Button" parent="SafeArea/CenterContainer/InventoryPanels/InventoryPanel/MarginContainer/VBoxContainer/TitleBar" unique_id=698056211]
-custom_minimum_size = Vector2(28, 28)
-layout_mode = 2
-focus_mode = 0
-mouse_default_cursor_shape = 2
-theme_override_colors/icon_normal_color = Color(0.95, 0.97, 0.95, 1)
-theme_override_colors/icon_hover_color = Color(1, 0.55, 0.55, 1)
-theme_override_colors/icon_pressed_color = Color(0.82, 0.3, 0.3, 1)
-icon = ExtResource("2_close")
-flat = true
-tooltip_text = "Close inventory"
-
-[node name="HSeparator" type="HSeparator" parent="SafeArea/CenterContainer/InventoryPanels/InventoryPanel/MarginContainer/VBoxContainer" unique_id=609650692]
-layout_mode = 2
-theme_override_styles/separator = SubResource("StyleBoxLine_separator")
-
-[node name="GridContainer" type="GridContainer" parent="SafeArea/CenterContainer/InventoryPanels/InventoryPanel/MarginContainer/VBoxContainer" unique_id=789467804]
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 3
-theme_override_constants/h_separation = 6
-theme_override_constants/v_separation = 6
-columns = 4
-
-[node name="ItemTooltip" type="Control" parent="." unique_id=1254507357]
-visible = false
-layout_mode = 1
-anchors_preset = 0
-offset_right = 280.0
-offset_bottom = 1.0
-mouse_filter = 2
-
-[node name="Panel" type="Panel" parent="ItemTooltip" unique_id=323031125]
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-mouse_filter = 2
-theme_override_styles/panel = SubResource("StyleBoxFlat_panel")
-
-[node name="MarginContainer" type="MarginContainer" parent="ItemTooltip/Panel" unique_id=413423816]
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-mouse_filter = 2
-theme_override_constants/margin_left = 8
-theme_override_constants/margin_top = 8
-theme_override_constants/margin_right = 8
-theme_override_constants/margin_bottom = 8
-
-[node name="TooltipText" type="RichTextLabel" parent="ItemTooltip/Panel/MarginContainer" unique_id=571796113]
-layout_mode = 2
-mouse_filter = 2
-bbcode_enabled = true
-text = ""
-fit_content = true
-scroll_active = false
diff --git a/scenes/ui/main_menu_ui.tscn b/scenes/ui/main_menu_ui.tscn
deleted file mode 100644
index eb29ac2..0000000
--- a/scenes/ui/main_menu_ui.tscn
+++ /dev/null
@@ -1,198 +0,0 @@
-[gd_scene format=3 uid="uid://bx7mq8t8hjn7q"]
-
-[ext_resource type="Script" uid="uid://c9x8y7z6a5b4c3d2" path="res://scripts/ui/main_menu_ui.gd" id="2_script"]
-
-[sub_resource type="StyleBoxLine" id="StyleBoxLine_npnbc"]
-color = Color(0, 0, 0, 0)
-
-[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_button_normal"]
-content_margin_left = 12.0
-content_margin_top = 4.0
-content_margin_right = 12.0
-content_margin_bottom = 4.0
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_hover"]
-content_margin_left = 10.0
-content_margin_top = 4.0
-content_margin_right = 10.0
-content_margin_bottom = 4.0
-bg_color = Color(0.22, 0.25, 0.29, 0.92)
-corner_radius_top_left = 5
-corner_radius_top_right = 5
-corner_radius_bottom_right = 5
-corner_radius_bottom_left = 5
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_pressed"]
-content_margin_left = 10.0
-content_margin_top = 4.0
-content_margin_right = 10.0
-content_margin_bottom = 4.0
-bg_color = Color(0.14, 0.16, 0.19, 0.96)
-corner_radius_top_left = 5
-corner_radius_top_right = 5
-corner_radius_bottom_right = 5
-corner_radius_bottom_left = 5
-
-[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_button_focus"]
-
-[node name="MainMenuUI" type="Control" unique_id=1809417860]
-layout_mode = 3
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-script = ExtResource("2_script")
-
-[node name="ColorRect" type="ColorRect" parent="." unique_id=702505842]
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-color = Color(0.0901961, 0.0901961, 0.0901961, 1)
-
-[node name="MainContainer" type="VBoxContainer" parent="." unique_id=473263213]
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-offset_left = 24.0
-offset_top = 24.0
-offset_right = -24.0
-offset_bottom = -24.0
-grow_horizontal = 2
-grow_vertical = 2
-theme_override_constants/separation = 50
-alignment = 1
-
-[node name="Label" type="Label" parent="MainContainer" unique_id=293700381]
-layout_mode = 2
-size_flags_horizontal = 6
-size_flags_vertical = 0
-theme_override_font_sizes/font_size = 70
-text = "3D Multiplayer Template"
-
-[node name="MainMenu" type="VBoxContainer" parent="MainContainer" unique_id=792964549]
-layout_direction = 1
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 4
-theme_override_constants/separation = 20
-alignment = 1
-
-[node name="HSeparator1" type="HSeparator" parent="MainContainer/MainMenu" unique_id=1429481219]
-layout_mode = 2
-theme_override_constants/separation = 10
-theme_override_styles/separator = SubResource("StyleBoxLine_npnbc")
-
-[node name="Option1" type="HBoxContainer" parent="MainContainer/MainMenu" unique_id=414194156]
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 4
-alignment = 1
-
-[node name="Label" type="Label" parent="MainContainer/MainMenu/Option1" unique_id=848326644]
-layout_mode = 2
-size_flags_horizontal = 4
-theme_override_font_sizes/font_size = 35
-text = "Nickname : "
-
-[node name="NickInput" type="LineEdit" parent="MainContainer/MainMenu/Option1" unique_id=1087637066]
-layout_mode = 2
-theme_override_font_sizes/font_size = 35
-placeholder_text = "YourNick"
-max_length = 20
-expand_to_text_length = true
-
-[node name="Option2" type="HBoxContainer" parent="MainContainer/MainMenu" unique_id=1404573203]
-layout_mode = 2
-size_flags_vertical = 4
-alignment = 1
-
-[node name="Label" type="Label" parent="MainContainer/MainMenu/Option2" unique_id=1101934130]
-layout_mode = 2
-size_flags_horizontal = 4
-theme_override_font_sizes/font_size = 35
-text = "Player Skin : "
-
-[node name="SkinInput" type="OptionButton" parent="MainContainer/MainMenu/Option2" unique_id=2039868006]
-layout_mode = 2
-size_flags_horizontal = 4
-theme_override_font_sizes/font_size = 35
-
-[node name="Option3" type="HBoxContainer" parent="MainContainer/MainMenu" unique_id=1469914367]
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 4
-alignment = 1
-
-[node name="Label" type="Label" parent="MainContainer/MainMenu/Option3" unique_id=492922165]
-layout_mode = 2
-size_flags_horizontal = 4
-theme_override_font_sizes/font_size = 35
-text = "IP Address : "
-
-[node name="AddressInput" type="LineEdit" parent="MainContainer/MainMenu/Option3" unique_id=2119648065]
-layout_mode = 2
-theme_override_font_sizes/font_size = 35
-placeholder_text = "127.0.0.1"
-alignment = 1
-expand_to_text_length = true
-
-[node name="HSeparator2" type="HSeparator" parent="MainContainer/MainMenu" unique_id=1512408463]
-layout_mode = 2
-theme_override_constants/separation = 30
-theme_override_styles/separator = SubResource("StyleBoxLine_npnbc")
-
-[node name="Buttons" type="HBoxContainer" parent="MainContainer/MainMenu" unique_id=1105640710]
-layout_mode = 2
-theme_override_constants/separation = 30
-alignment = 1
-
-[node name="Host" type="Button" parent="MainContainer/MainMenu/Buttons" unique_id=18102936]
-layout_mode = 2
-mouse_default_cursor_shape = 2
-theme_override_colors/font_pressed_color = Color(0.42, 0.62, 0.82, 1)
-theme_override_colors/font_hover_color = Color(0.55, 0.75, 0.95, 1)
-theme_override_font_sizes/font_size = 35
-theme_override_styles/normal = SubResource("StyleBoxEmpty_button_normal")
-theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
-theme_override_styles/pressed = SubResource("StyleBoxFlat_button_pressed")
-theme_override_styles/focus = SubResource("StyleBoxEmpty_button_focus")
-text = "HOST"
-flat = true
-
-[node name="Join" type="Button" parent="MainContainer/MainMenu/Buttons" unique_id=1615937929]
-layout_mode = 2
-mouse_default_cursor_shape = 2
-theme_override_colors/font_pressed_color = Color(0.42, 0.62, 0.82, 1)
-theme_override_colors/font_hover_color = Color(0.55, 0.75, 0.95, 1)
-theme_override_font_sizes/font_size = 35
-theme_override_styles/normal = SubResource("StyleBoxEmpty_button_normal")
-theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
-theme_override_styles/pressed = SubResource("StyleBoxFlat_button_pressed")
-theme_override_styles/focus = SubResource("StyleBoxEmpty_button_focus")
-text = "JOIN"
-flat = true
-
-[node name="Option4" type="HBoxContainer" parent="MainContainer/MainMenu" unique_id=1327970814]
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 4
-alignment = 1
-
-[node name="Quit" type="Button" parent="MainContainer/MainMenu/Option4" unique_id=1022123460]
-layout_mode = 2
-mouse_default_cursor_shape = 2
-theme_override_colors/font_pressed_color = Color(0.858824, 0, 0, 0.862745)
-theme_override_colors/font_hover_color = Color(0.858807, 4.71681e-06, 1.15514e-06, 0.862745)
-theme_override_font_sizes/font_size = 35
-theme_override_styles/focus = SubResource("StyleBoxEmpty_button_focus")
-text = "QUIT"
-flat = true
-
-[connection signal="pressed" from="MainContainer/MainMenu/Buttons/Host" to="." method="_on_host_pressed"]
-[connection signal="pressed" from="MainContainer/MainMenu/Buttons/Join" to="." method="_on_join_pressed"]
-[connection signal="pressed" from="MainContainer/MainMenu/Option4/Quit" to="." method="_on_quit_pressed"]
diff --git a/scenes/ui/multiplayer_chat_ui.tscn b/scenes/ui/multiplayer_chat_ui.tscn
deleted file mode 100644
index 2d60072..0000000
--- a/scenes/ui/multiplayer_chat_ui.tscn
+++ /dev/null
@@ -1,76 +0,0 @@
-[gd_scene load_steps=3 format=3 uid="uid://ccencq0lcd3ph"]
-
-[ext_resource type="Script" uid="uid://c8x9y2z1a3b4c5d6" path="res://scripts/ui/multiplayer_chat_ui.gd" id="1_h7x4v"]
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_input"]
-content_margin_left = 10.0
-content_margin_top = 6.0
-content_margin_right = 10.0
-content_margin_bottom = 6.0
-bg_color = Color(0.055, 0.06, 0.07, 0.82)
-border_width_left = 1
-border_width_top = 1
-border_width_right = 1
-border_width_bottom = 1
-border_color = Color(0.55, 0.58, 0.62, 0.45)
-corner_radius_top_left = 6
-corner_radius_top_right = 6
-corner_radius_bottom_right = 6
-corner_radius_bottom_left = 6
-
-[node name="MultiplayerChatUI" type="Control"]
-layout_mode = 3
-anchors_preset = 0
-anchor_top = 1.0
-anchor_bottom = 1.0
-offset_left = 16.0
-offset_top = -284.0
-offset_right = 496.0
-offset_bottom = -16.0
-grow_vertical = 0
-mouse_filter = 2
-script = ExtResource("1_h7x4v")
-
-[node name="ChatLayout" type="VBoxContainer" parent="."]
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-theme_override_constants/separation = 8
-mouse_filter = 2
-
-[node name="Chat" type="RichTextLabel" parent="ChatLayout"]
-layout_mode = 2
-size_flags_vertical = 3
-mouse_filter = 2
-theme_override_colors/default_color = Color(0.96, 0.96, 0.96, 1)
-theme_override_colors/font_outline_color = Color(0, 0, 0, 0.95)
-theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.85)
-theme_override_constants/outline_size = 4
-theme_override_constants/shadow_offset_x = 2
-theme_override_constants/shadow_offset_y = 2
-theme_override_font_sizes/normal_font_size = 18
-bbcode_enabled = true
-scroll_active = false
-vertical_alignment = 2
-
-[node name="Message" type="LineEdit" parent="ChatLayout"]
-custom_minimum_size = Vector2(0, 38)
-layout_mode = 2
-mouse_filter = 2
-theme_override_colors/font_color = Color(1, 1, 1, 1)
-theme_override_colors/font_placeholder_color = Color(0.72, 0.72, 0.72, 1)
-theme_override_colors/font_outline_color = Color(0, 0, 0, 0.9)
-theme_override_constants/outline_size = 2
-theme_override_font_sizes/font_size = 17
-theme_override_styles/normal = SubResource("StyleBoxFlat_input")
-theme_override_styles/focus = SubResource("StyleBoxFlat_input")
-placeholder_text = "Say to all..."
-max_length = 100
-keep_editing_on_text_submit = true
-
-[node name="FeedTimer" type="Timer" parent="."]
-wait_time = 6.0
-one_shot = true
diff --git a/scenes/ui/pause_menu_ui.tscn b/scenes/ui/pause_menu_ui.tscn
deleted file mode 100644
index 578f573..0000000
--- a/scenes/ui/pause_menu_ui.tscn
+++ /dev/null
@@ -1,127 +0,0 @@
-[gd_scene load_steps=6 format=3]
-
-[ext_resource type="Script" path="res://scripts/ui/pause_menu_ui.gd" id="2_script"]
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"]
-bg_color = Color(0.055, 0.06, 0.07, 0.82)
-corner_radius_top_left = 6
-corner_radius_top_right = 6
-corner_radius_bottom_right = 6
-corner_radius_bottom_left = 6
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_hover"]
-content_margin_left = 10.0
-content_margin_top = 4.0
-content_margin_right = 10.0
-content_margin_bottom = 4.0
-bg_color = Color(0.22, 0.25, 0.29, 0.92)
-corner_radius_top_left = 5
-corner_radius_top_right = 5
-corner_radius_bottom_right = 5
-corner_radius_bottom_left = 5
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_pressed"]
-content_margin_left = 10.0
-content_margin_top = 4.0
-content_margin_right = 10.0
-content_margin_bottom = 4.0
-bg_color = Color(0.14, 0.16, 0.19, 0.96)
-corner_radius_top_left = 5
-corner_radius_top_right = 5
-corner_radius_bottom_right = 5
-corner_radius_bottom_left = 5
-
-[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_button_focus"]
-
-[node name="PauseMenuUI" type="Control"]
-process_mode = 3
-z_index = 100
-layout_mode = 3
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-mouse_filter = 0
-script = ExtResource("2_script")
-
-[node name="Overlay" type="ColorRect" parent="."]
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-color = Color(0, 0, 0, 0.58)
-
-[node name="Panel" type="PanelContainer" parent="Overlay"]
-custom_minimum_size = Vector2(360, 0)
-layout_mode = 1
-anchors_preset = 8
-anchor_left = 0.5
-anchor_top = 0.5
-anchor_right = 0.5
-anchor_bottom = 0.5
-offset_left = -180.0
-offset_top = -178.0
-offset_right = 180.0
-offset_bottom = 178.0
-grow_horizontal = 2
-grow_vertical = 2
-theme_override_styles/panel = SubResource("StyleBoxFlat_panel")
-
-[node name="MarginContainer" type="MarginContainer" parent="Overlay/Panel"]
-layout_mode = 2
-theme_override_constants/margin_left = 40
-theme_override_constants/margin_top = 32
-theme_override_constants/margin_right = 40
-theme_override_constants/margin_bottom = 32
-
-[node name="Buttons" type="VBoxContainer" parent="Overlay/Panel/MarginContainer"]
-layout_mode = 2
-theme_override_constants/separation = 24
-alignment = 1
-
-[node name="Title" type="Label" parent="Overlay/Panel/MarginContainer/Buttons"]
-layout_mode = 2
-theme_override_font_sizes/font_size = 52
-text = "PAUSED"
-horizontal_alignment = 1
-
-[node name="Resume" type="Button" parent="Overlay/Panel/MarginContainer/Buttons"]
-layout_mode = 2
-mouse_default_cursor_shape = 2
-theme_override_colors/font_hover_color = Color(0.55, 0.75, 0.95, 1)
-theme_override_colors/font_pressed_color = Color(0.42, 0.62, 0.82, 1)
-theme_override_font_sizes/font_size = 32
-theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
-theme_override_styles/pressed = SubResource("StyleBoxFlat_button_pressed")
-theme_override_styles/focus = SubResource("StyleBoxEmpty_button_focus")
-text = "RESUME"
-flat = true
-
-[node name="MainMenu" type="Button" parent="Overlay/Panel/MarginContainer/Buttons"]
-layout_mode = 2
-mouse_default_cursor_shape = 2
-theme_override_colors/font_hover_color = Color(0.55, 0.75, 0.95, 1)
-theme_override_colors/font_pressed_color = Color(0.42, 0.62, 0.82, 1)
-theme_override_font_sizes/font_size = 32
-theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
-theme_override_styles/pressed = SubResource("StyleBoxFlat_button_pressed")
-theme_override_styles/focus = SubResource("StyleBoxEmpty_button_focus")
-text = "MAIN MENU"
-flat = true
-
-[node name="Quit" type="Button" parent="Overlay/Panel/MarginContainer/Buttons"]
-layout_mode = 2
-mouse_default_cursor_shape = 2
-theme_override_colors/font_hover_color = Color(0.86, 0.1, 0.1, 1)
-theme_override_colors/font_pressed_color = Color(0.86, 0.1, 0.1, 1)
-theme_override_font_sizes/font_size = 32
-theme_override_styles/focus = SubResource("StyleBoxEmpty_button_focus")
-text = "QUIT"
-flat = true
-
-[connection signal="pressed" from="Overlay/Panel/MarginContainer/Buttons/Resume" to="." method="_on_resume_pressed"]
-[connection signal="pressed" from="Overlay/Panel/MarginContainer/Buttons/MainMenu" to="." method="_on_main_menu_pressed"]
-[connection signal="pressed" from="Overlay/Panel/MarginContainer/Buttons/Quit" to="." method="_on_quit_pressed"]
diff --git a/scenes/ui/player_list_ui.tscn b/scenes/ui/player_list_ui.tscn
deleted file mode 100644
index b57987f..0000000
--- a/scenes/ui/player_list_ui.tscn
+++ /dev/null
@@ -1,105 +0,0 @@
-[gd_scene load_steps=4 format=3]
-
-[ext_resource type="Script" path="res://scripts/ui/player_list_ui.gd" id="2_script"]
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"]
-bg_color = Color(0.055, 0.06, 0.07, 0.82)
-border_width_left = 1
-border_width_top = 1
-border_width_right = 1
-border_width_bottom = 1
-border_color = Color(0.55, 0.58, 0.62, 0.45)
-corner_radius_top_left = 6
-corner_radius_top_right = 6
-corner_radius_bottom_right = 6
-corner_radius_bottom_left = 6
-
-[sub_resource type="StyleBoxLine" id="StyleBoxLine_separator"]
-color = Color(0.65, 0.68, 0.72, 0.42)
-
-[node name="PlayerListUI" type="Control"]
-z_index = 80
-layout_mode = 3
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-mouse_filter = 2
-script = ExtResource("2_script")
-
-[node name="SafeArea" type="MarginContainer" parent="."]
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-theme_override_constants/margin_left = 16
-theme_override_constants/margin_top = 16
-theme_override_constants/margin_right = 16
-theme_override_constants/margin_bottom = 16
-mouse_filter = 2
-
-[node name="PositionContainer" type="Control" parent="SafeArea"]
-layout_mode = 2
-mouse_filter = 2
-
-[node name="Panel" type="PanelContainer" parent="SafeArea/PositionContainer"]
-layout_mode = 1
-anchor_top = 0.2
-anchor_bottom = 0.2
-grow_vertical = 2
-theme_override_styles/panel = SubResource("StyleBoxFlat_panel")
-mouse_filter = 2
-
-[node name="MarginContainer" type="MarginContainer" parent="SafeArea/PositionContainer/Panel"]
-layout_mode = 2
-theme_override_constants/margin_left = 22
-theme_override_constants/margin_top = 18
-theme_override_constants/margin_right = 22
-theme_override_constants/margin_bottom = 20
-mouse_filter = 2
-
-[node name="Content" type="VBoxContainer" parent="SafeArea/PositionContainer/Panel/MarginContainer"]
-layout_mode = 2
-theme_override_constants/separation = 8
-mouse_filter = 2
-
-[node name="Title" type="Label" parent="SafeArea/PositionContainer/Panel/MarginContainer/Content"]
-layout_mode = 2
-theme_override_colors/font_color = Color(0.95, 0.96, 0.97, 1)
-theme_override_font_sizes/font_size = 26
-text = "ONLINE PLAYERS"
-horizontal_alignment = 1
-
-[node name="Count" type="Label" parent="SafeArea/PositionContainer/Panel/MarginContainer/Content"]
-layout_mode = 2
-theme_override_colors/font_color = Color(0.65, 0.68, 0.72, 1)
-theme_override_font_sizes/font_size = 15
-text = "0 online"
-horizontal_alignment = 1
-
-[node name="HSeparator" type="HSeparator" parent="SafeArea/PositionContainer/Panel/MarginContainer/Content"]
-layout_mode = 2
-theme_override_styles/separator = SubResource("StyleBoxLine_separator")
-
-[node name="PlayerScroll" type="ScrollContainer" parent="SafeArea/PositionContainer/Panel/MarginContainer/Content"]
-custom_minimum_size = Vector2(0, 30)
-layout_mode = 2
-mouse_filter = 2
-horizontal_scroll_mode = 0
-vertical_scroll_mode = 1
-
-[node name="Players" type="RichTextLabel" parent="SafeArea/PositionContainer/Panel/MarginContainer/Content/PlayerScroll"]
-custom_minimum_size = Vector2(0, 30)
-layout_mode = 2
-size_flags_horizontal = 3
-mouse_filter = 2
-theme_override_colors/default_color = Color(0.92, 0.95, 0.93, 1)
-theme_override_colors/font_outline_color = Color(0, 0, 0, 0.8)
-theme_override_constants/outline_size = 2
-theme_override_font_sizes/normal_font_size = 18
-bbcode_enabled = true
-fit_content = true
-scroll_active = false
diff --git a/scripts/autoload/item_database.gd b/scripts/autoload/item_database.gd
deleted file mode 100644
index 12f6a32..0000000
--- a/scripts/autoload/item_database.gd
+++ /dev/null
@@ -1,179 +0,0 @@
-extends Node
-
-const FEDORA_ICON: Texture2D = preload("res://assets/items/hats/icons/fedora.png")
-const HEADPHONES_ICON: Texture2D = preload("res://assets/items/hats/icons/headphones.png")
-const PIRATE_HAT_ICON: Texture2D = preload("res://assets/items/hats/icons/pirate_hat.png")
-const SHERIFF_HAT_ICON: Texture2D = preload("res://assets/items/hats/icons/sheriff_hat.png")
-const SOMBRERO_ICON: Texture2D = preload("res://assets/items/hats/icons/sombrero.png")
-const WIZARD_HAT_ICON: Texture2D = preload("res://assets/items/hats/icons/wizard_hat.png")
-const BACKPACK_ICON: Texture2D = preload("res://assets/items/backpacks/icons/backpack.png")
-const SWORD_ICON: Texture2D = preload("res://assets/items/weapons/icons/sword.png")
-const SWORD_BIG_ICON: Texture2D = preload("res://assets/items/weapons/icons/sword_big.png")
-const AXE_ICON: Texture2D = preload("res://assets/items/weapons/icons/axe.png")
-const CHICKEN_LEG_ICON: Texture2D = preload("res://assets/items/misc/icons/chicken_leg.png")
-const BONE_ICON: Texture2D = preload("res://assets/items/misc/icons/bone.png")
-const CHALICE_ICON: Texture2D = preload("res://assets/items/misc/icons/chalice.png")
-
-var items: Dictionary = {}
-
-
-func _ready():
- _create_sample_items()
-
-
-func get_item(item_id: String) -> Item:
- return items.get(item_id)
-
-
-func get_all_items() -> Dictionary:
- return items
-
-
-func _create_sample_items():
- _create_hat_item("fedora", "Fedora", "A timeless felt fedora.", "res://scenes/items/hats/fedora.tscn", FEDORA_ICON)
- _create_hat_item(
- "headphones",
- "Headphones",
- "Comfortable over-ear headphones.",
- "res://scenes/items/hats/headphones.tscn",
- HEADPHONES_ICON
- )
- _create_hat_item(
- "pirate_hat",
- "Pirate Hat",
- "A weathered hat for a daring pirate.",
- "res://scenes/items/hats/pirate_hat.tscn",
- PIRATE_HAT_ICON
- )
- _create_hat_item(
- "sheriff_hat",
- "Sheriff Hat",
- "A frontier hat with a sheriff badge.",
- "res://scenes/items/hats/sheriff_hat.tscn",
- SHERIFF_HAT_ICON
- )
- _create_hat_item(
- "sombrero", "Sombrero", "A broad and colorful sombrero.", "res://scenes/items/hats/sombrero.tscn", SOMBRERO_ICON
- )
- _create_hat_item(
- "wizard_hat",
- "Wizard Hat",
- "A pointed hat filled with arcane style.",
- "res://scenes/items/hats/wizard_hat.tscn",
- WIZARD_HAT_ICON
- )
- _create_item(
- "backpack",
- "Backpack",
- "A sturdy backpack worn on the back.",
- Item.ItemType.BACKPACK,
- "res://scenes/items/backpacks/backpack.tscn",
- BACKPACK_ICON,
- true,
- 60
- )
- _create_item(
- "sword",
- "Sword",
- "A balanced hand sword.",
- Item.ItemType.WEAPON,
- "res://scenes/items/weapons/sword.tscn",
- SWORD_ICON,
- true,
- 80
- )
- _create_item(
- "sword_big",
- "Big Sword",
- "A large two-handed sword.",
- Item.ItemType.WEAPON,
- "res://scenes/items/weapons/sword_big.tscn",
- SWORD_BIG_ICON,
- true,
- 120
- )
- _create_item(
- "axe",
- "Small Axe",
- "A compact axe with a sharp steel head.",
- Item.ItemType.WEAPON,
- "res://scenes/items/weapons/axe.tscn",
- AXE_ICON,
- true,
- 95
- )
- _create_item(
- "chicken_leg",
- "Chicken Leg",
- "A cooked chicken leg.",
- Item.ItemType.MISC,
- "res://scenes/items/misc/chicken_leg.tscn",
- CHICKEN_LEG_ICON,
- false,
- 5
- )
- _create_item(
- "bone",
- "Bone",
- "A weathered bone.",
- Item.ItemType.MISC,
- "res://scenes/items/misc/bone.tscn",
- BONE_ICON,
- false,
- 2
- )
- _create_item(
- "chalice",
- "Chalice",
- "A decorative golden chalice.",
- Item.ItemType.MISC,
- "res://scenes/items/misc/chalice.tscn",
- CHALICE_ICON,
- false,
- 35
- )
-
-
-func _create_item(
- item_id: String,
- item_name: String,
- item_description: String,
- item_type: Item.ItemType,
- scene_path: String,
- item_icon: Texture2D,
- equipable: bool,
- item_value: int
-) -> void:
- var item := Item.new()
- item.id = item_id
- item.name = item_name
- item.description = item_description
- item.item_type = item_type
- item.rarity = Item.ItemRarity.COMMON
- item.stackable = not equipable
- item.max_stack = 99 if item.stackable else 1
- item.value = item_value
- item.icon = item_icon
- item.scene_path = scene_path
- if equipable:
- item.context_options.append(Item.ContextOptions.EQUIP)
- item.context_options.append(Item.ContextOptions.DROP)
- items[item.id] = item
-
-
-func _create_hat_item(
- item_id: String, item_name: String, item_description: String, scene_path: String, item_icon: Texture2D
-) -> void:
- var hat := Item.new()
- hat.id = item_id
- hat.name = item_name
- hat.description = item_description
- hat.item_type = Item.ItemType.HAT
- hat.rarity = Item.ItemRarity.COMMON
- hat.stackable = false
- hat.value = 25
- hat.icon = item_icon
- hat.scene_path = scene_path
- hat.context_options.append(Item.ContextOptions.EQUIP)
- hat.context_options.append(Item.ContextOptions.DROP)
- items[hat.id] = hat
diff --git a/scripts/autoload/item_database.gd.uid b/scripts/autoload/item_database.gd.uid
deleted file mode 100644
index 73c023d..0000000
--- a/scripts/autoload/item_database.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bsraoqq3rst1j
diff --git a/scripts/autoload/network.gd b/scripts/autoload/network.gd
deleted file mode 100644
index 9271f70..0000000
--- a/scripts/autoload/network.gd
+++ /dev/null
@@ -1,202 +0,0 @@
-extends Node
-
-signal player_connected(peer_id, player_info)
-signal server_disconnected
-
-const SERVER_ADDRESS: String = "127.0.0.1"
-const SERVER_PORT: int = 8080
-const MAX_PLAYERS: int = 10
-const MAX_NICK_LENGTH := 24
-const MAX_ADDRESS_LENGTH := 253
-
-var players = {}
-var player_info = {"nick": "host", "skin": Character.SkinColor.BLUE}
-var _session_active := false
-
-
-func _ready() -> void:
- multiplayer.server_disconnected.connect(_on_server_disconnected)
- multiplayer.connection_failed.connect(_on_connection_failed)
- multiplayer.peer_disconnected.connect(_on_player_disconnected)
- multiplayer.peer_connected.connect(_on_player_connected)
- multiplayer.connected_to_server.connect(_on_connected_ok)
-
-
-func start_host(nickname: String, skin_color_str: String):
- var peer = ENetMultiplayerPeer.new()
- var error = peer.create_server(SERVER_PORT, MAX_PLAYERS)
- if error:
- return error
-
- peer.host.compress(ENetConnection.COMPRESS_RANGE_CODER)
- multiplayer.multiplayer_peer = peer
- _session_active = true
-
- player_info["nick"] = sanitize_nickname(nickname, "Host_" + str(multiplayer.get_unique_id()))
- player_info["skin"] = skin_str_to_e(skin_color_str)
-
- if DisplayServer.get_name() == "headless":
- return
-
- players[1] = player_info
- player_connected.emit(1, player_info)
-
-
-func join_game(nickname: String, skin_color_str: String, address: String = SERVER_ADDRESS):
- address = sanitize_address(address)
- if address.is_empty():
- return ERR_INVALID_PARAMETER
-
- var peer = ENetMultiplayerPeer.new()
- var error = peer.create_client(address, SERVER_PORT)
- if error:
- return error
-
- peer.host.compress(ENetConnection.COMPRESS_RANGE_CODER)
- multiplayer.multiplayer_peer = peer
- _session_active = true
-
- player_info["nick"] = sanitize_nickname(nickname, "Player_" + str(multiplayer.get_unique_id()))
- player_info["skin"] = skin_str_to_e(skin_color_str)
-
-
-func _on_connected_ok():
- var peer_id = multiplayer.get_unique_id()
- players[peer_id] = player_info
- player_connected.emit(peer_id, player_info)
- _register_player.rpc_id(1, player_info)
-
-
-func _on_player_connected(id):
- if not multiplayer.is_server():
- return
- for peer_id in players:
- _sync_registered_player.rpc_id(id, peer_id, players[peer_id])
-
-
-@rpc("any_peer", "reliable")
-func _register_player(new_player_info):
- if not multiplayer.is_server():
- return
- if not (new_player_info is Dictionary):
- return
- var new_player_id = multiplayer.get_remote_sender_id()
- if new_player_id == 0:
- return
- if players.has(new_player_id):
- return
- var sanitized_info = sanitize_player_info(new_player_info, "Player_" + str(new_player_id))
- players[new_player_id] = sanitized_info
- player_connected.emit(new_player_id, sanitized_info)
- _sync_registered_player.rpc(new_player_id, sanitized_info)
-
-
-func _on_player_disconnected(id):
- players.erase(id)
-
-
-func _on_connection_failed():
- _finish_session()
-
-
-func _on_server_disconnected():
- _finish_session()
-
-
-func leave_game() -> void:
- var peer := multiplayer.multiplayer_peer
- if peer:
- peer.close()
- _finish_session()
-
-
-func _finish_session() -> void:
- var had_session := _session_active or multiplayer.multiplayer_peer != null or not players.is_empty()
- _session_active = false
- multiplayer.multiplayer_peer = null
- players.clear()
- if had_session:
- server_disconnected.emit()
-
-
-func skin_str_to_e(s):
- match str(s).strip_edges().to_lower():
- "blue":
- return Character.SkinColor.BLUE
- "yellow":
- return Character.SkinColor.YELLOW
- "green":
- return Character.SkinColor.GREEN
- "red":
- return Character.SkinColor.RED
- _:
- return Character.SkinColor.BLUE
-
-
-@rpc("authority", "reliable")
-func _sync_registered_player(peer_id: int, registered_player_info: Dictionary):
- if multiplayer.is_server():
- return
- if players.has(peer_id):
- return
- var sanitized_info = sanitize_player_info(registered_player_info, "Player_" + str(peer_id))
- players[peer_id] = sanitized_info
- player_connected.emit(peer_id, sanitized_info)
-
-
-func sanitize_player_info(info: Dictionary, fallback_nick: String) -> Dictionary:
- return {
- "nick": sanitize_nickname(str(info.get("nick", "")), fallback_nick),
- "skin": sanitize_skin_value(info.get("skin", Character.SkinColor.BLUE))
- }
-
-
-func sanitize_nickname(nickname: String, fallback: String) -> String:
- var clean := ""
- var last_was_space := false
- for i in range(nickname.length()):
- var codepoint := nickname.unicode_at(i)
- if codepoint <= 31 or codepoint == 127:
- if not last_was_space:
- clean += " "
- last_was_space = true
- continue
-
- var character := nickname.substr(i, 1)
- if character == " ":
- if last_was_space:
- continue
- last_was_space = true
- else:
- last_was_space = false
- clean += character
-
- clean = clean.strip_edges()
- if clean.is_empty():
- clean = fallback.strip_edges()
- if clean.length() > MAX_NICK_LENGTH:
- clean = clean.substr(0, MAX_NICK_LENGTH).strip_edges()
- if clean.is_empty():
- clean = "Player"
- return clean
-
-
-func sanitize_address(address: String) -> String:
- var clean = address.strip_edges()
- if clean.is_empty():
- return SERVER_ADDRESS
- if clean.length() > MAX_ADDRESS_LENGTH:
- return ""
- if clean.contains("://") or clean.contains("/") or clean.contains("\\") or clean.contains(":"):
- return ""
- return clean
-
-
-func sanitize_skin_value(value) -> Character.SkinColor:
- if value is int:
- match value:
- Character.SkinColor.BLUE, Character.SkinColor.YELLOW, Character.SkinColor.GREEN, Character.SkinColor.RED:
- return value
- _:
- return Character.SkinColor.BLUE
- return skin_str_to_e(str(value))
diff --git a/scripts/autoload/network.gd.uid b/scripts/autoload/network.gd.uid
deleted file mode 100644
index 5d7ca85..0000000
--- a/scripts/autoload/network.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cedubc87s78fo
diff --git a/scripts/inventory/inventory_slot.gd b/scripts/inventory/inventory_slot.gd
deleted file mode 100644
index 006d04f..0000000
--- a/scripts/inventory/inventory_slot.gd
+++ /dev/null
@@ -1,57 +0,0 @@
-class_name InventorySlot
-extends RefCounted
-
-var item_id: String = ""
-var quantity: int = 0
-
-
-func is_empty() -> bool:
- return item_id.is_empty() or quantity <= 0
-
-
-func add_item(item: Item, amount: int = 1) -> int:
- var max_quantity = item.max_stack if item.stackable else 1
- if is_empty():
- item_id = item.id
- quantity = min(amount, max_quantity)
- return amount - quantity
- if item_id == item.id and item.stackable:
- var space_available = item.max_stack - quantity
- var amount_to_add = min(amount, space_available)
- quantity += amount_to_add
- return amount - amount_to_add
- return amount
-
-
-func remove_item(amount: int = 1) -> int:
- var removed = min(amount, quantity)
- quantity -= removed
- if quantity <= 0:
- clear()
- return removed
-
-
-func clear() -> void:
- item_id = ""
- quantity = 0
-
-
-func to_dict() -> Dictionary:
- return {"item_id": item_id, "quantity": quantity}
-
-
-func from_dict(data: Dictionary) -> void:
- item_id = data.get("item_id", "")
- quantity = data.get("quantity", 0)
-
- if is_empty():
- clear()
- return
-
- var item = ItemDatabase.get_item(item_id)
- if not item:
- clear()
- return
-
- var max_quantity = item.max_stack if item.stackable else 1
- quantity = clampi(quantity, 1, max_quantity)
diff --git a/scripts/inventory/inventory_slot.gd.uid b/scripts/inventory/inventory_slot.gd.uid
deleted file mode 100644
index 15a27af..0000000
--- a/scripts/inventory/inventory_slot.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c7dfwkeylor2j
diff --git a/scripts/inventory/item.gd b/scripts/inventory/item.gd
deleted file mode 100644
index 2ceb54b..0000000
--- a/scripts/inventory/item.gd
+++ /dev/null
@@ -1,27 +0,0 @@
-class_name Item
-extends Resource
-
-enum ItemType { WEAPON, CONSUMABLE, TOOL, MISC, HAT, BACKPACK }
-
-enum ItemRarity { COMMON, UNCOMMON, RARE, EPIC, LEGENDARY }
-
-enum ContextOptions {
- DROP,
- EQUIP,
- UNEQUIP,
-}
-
-@export var id: String = ""
-@export var name: String = ""
-@export var description: String = ""
-@export var icon: Texture2D
-
-@export var stackable: bool = true
-@export var max_stack: int = 99
-
-@export var item_type: ItemType = ItemType.MISC
-@export var rarity: ItemRarity = ItemRarity.COMMON
-@export var value: int = 0
-@export var context_options: Array[Item.ContextOptions] = []
-
-@export var scene_path: String = ""
diff --git a/scripts/inventory/item.gd.uid b/scripts/inventory/item.gd.uid
deleted file mode 100644
index 002ec4b..0000000
--- a/scripts/inventory/item.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bbq3lkpgmwr1t
diff --git a/scripts/inventory/player_inventory.gd b/scripts/inventory/player_inventory.gd
deleted file mode 100644
index 5ad7fb1..0000000
--- a/scripts/inventory/player_inventory.gd
+++ /dev/null
@@ -1,267 +0,0 @@
-class_name PlayerInventory
-extends RefCounted
-
-const BASE_INVENTORY_SIZE := 16
-const BACKPACK_BONUS_SLOTS := 4
-const MAX_INVENTORY_SIZE := BASE_INVENTORY_SIZE + BACKPACK_BONUS_SLOTS
-var slots: Array[InventorySlot] = []
-var equipped_weapon: InventorySlot = InventorySlot.new()
-var equipped_hat: InventorySlot = InventorySlot.new()
-var equipped_backpack: InventorySlot = InventorySlot.new()
-
-
-func _init():
- _initialize_slots()
-
-
-func _initialize_slots():
- slots.clear()
- for i in range(MAX_INVENTORY_SIZE):
- slots.append(InventorySlot.new())
-
-
-func get_active_slot_count() -> int:
- return MAX_INVENTORY_SIZE if not equipped_backpack.is_empty() else BASE_INVENTORY_SIZE
-
-
-func is_slot_active(index: int) -> bool:
- return index >= 0 and index < get_active_slot_count()
-
-
-func get_slot(index: int) -> InventorySlot:
- if index >= 0 and index < slots.size():
- return slots[index]
- return null
-
-
-func get_equipped_slot(item_type: Item.ItemType) -> InventorySlot:
- match item_type:
- Item.ItemType.WEAPON:
- return equipped_weapon
- Item.ItemType.HAT:
- return equipped_hat
- Item.ItemType.BACKPACK:
- return equipped_backpack
- _:
- return null
-
-
-func equip_from_slot(index: int, item_type: Item.ItemType) -> bool:
- if not is_slot_active(index):
- return false
-
- var backpack_slot: InventorySlot = get_slot(index)
- var equipped_slot: InventorySlot = get_equipped_slot(item_type)
- if not backpack_slot or backpack_slot.is_empty() or not equipped_slot:
- return false
-
- var item: Item = ItemDatabase.get_item(backpack_slot.item_id)
- if not item or item.item_type != item_type:
- return false
-
- var new_item_id: String = backpack_slot.item_id
- var new_quantity: int = backpack_slot.quantity
- backpack_slot.item_id = equipped_slot.item_id
- backpack_slot.quantity = equipped_slot.quantity
- equipped_slot.item_id = new_item_id
- equipped_slot.quantity = new_quantity
- if backpack_slot.is_empty():
- backpack_slot.clear()
- return true
-
-
-func unequip_to_slot(item_type: Item.ItemType, destination_index: int = -1) -> bool:
- var equipped_slot: InventorySlot = get_equipped_slot(item_type)
- if not equipped_slot or equipped_slot.is_empty():
- return false
- if item_type == Item.ItemType.BACKPACK:
- return _unequip_backpack_to_slot(equipped_slot, destination_index)
-
- if destination_index < 0:
- destination_index = get_first_empty_slot()
- if not is_slot_active(destination_index):
- return false
- var destination: InventorySlot = get_slot(destination_index)
- if not destination or not destination.is_empty():
- return false
-
- destination.item_id = equipped_slot.item_id
- destination.quantity = equipped_slot.quantity
- equipped_slot.clear()
- return true
-
-
-func _unequip_backpack_to_slot(equipped_slot: InventorySlot, destination_index: int) -> bool:
- if destination_index < 0:
- destination_index = _get_first_empty_base_slot()
- if destination_index < 0 or destination_index >= BASE_INVENTORY_SIZE:
- return false
-
- var destination := get_slot(destination_index)
- if not destination or not destination.is_empty():
- return false
-
- var occupied_bonus_slots: Array[int] = []
- for i in range(BASE_INVENTORY_SIZE, MAX_INVENTORY_SIZE):
- if not slots[i].is_empty():
- occupied_bonus_slots.append(i)
-
- var available_base_slots: Array[int] = []
- for i in range(BASE_INVENTORY_SIZE):
- if i != destination_index and slots[i].is_empty():
- available_base_slots.append(i)
- if occupied_bonus_slots.size() > available_base_slots.size():
- return false
-
- for i in range(occupied_bonus_slots.size()):
- var source := slots[occupied_bonus_slots[i]]
- var target := slots[available_base_slots[i]]
- target.item_id = source.item_id
- target.quantity = source.quantity
- source.clear()
-
- destination.item_id = equipped_slot.item_id
- destination.quantity = equipped_slot.quantity
- equipped_slot.clear()
- return true
-
-
-func _get_first_empty_base_slot() -> int:
- for i in range(BASE_INVENTORY_SIZE):
- if slots[i].is_empty():
- return i
- return -1
-
-
-func add_item(item: Item, quantity: int = 1) -> int:
- var remaining = quantity
- var active_slot_count := get_active_slot_count()
-
- if item.stackable:
- for i in range(active_slot_count):
- var slot := slots[i]
- if slot.item_id == item.id:
- remaining = slot.add_item(item, remaining)
- if remaining <= 0:
- break
-
- if remaining > 0:
- for i in range(active_slot_count):
- var slot := slots[i]
- if slot.is_empty():
- remaining = slot.add_item(item, remaining)
- if remaining <= 0:
- break
-
- return remaining
-
-
-func remove_item(item_id: String, quantity: int = 1) -> int:
- var removed = 0
- for i in range(get_active_slot_count()):
- var slot := slots[i]
- if slot.item_id == item_id:
- var slot_removed = slot.remove_item(quantity - removed)
- removed += slot_removed
- if removed >= quantity:
- break
- return removed
-
-
-func move_item(from_index: int, to_index: int, quantity: int = -1) -> bool:
- if (
- from_index == to_index
- or not is_slot_active(from_index)
- or not is_slot_active(to_index)
- or quantity == 0
- or quantity < -1
- ):
- return false
-
- var from_slot = get_slot(from_index)
- var to_slot = get_slot(to_index)
-
- if not from_slot or not to_slot or from_slot.is_empty():
- return false
-
- var move_amount = from_slot.quantity if quantity == -1 else quantity
- move_amount = min(move_amount, from_slot.quantity)
-
- var item = ItemDatabase.get_item(from_slot.item_id)
- if not item:
- return false
-
- var remaining = to_slot.add_item(item, move_amount)
- var moved_amount = move_amount - remaining
- if moved_amount > 0:
- from_slot.remove_item(moved_amount)
-
- return moved_amount > 0
-
-
-func swap_items(from_index: int, to_index: int) -> bool:
- if not is_slot_active(from_index) or not is_slot_active(to_index):
- return false
-
- var from_slot = get_slot(from_index)
- var to_slot = get_slot(to_index)
-
- if not from_slot or not to_slot:
- return false
-
- if from_slot.item_id == to_slot.item_id and not from_slot.is_empty() and not to_slot.is_empty():
- var item = ItemDatabase.get_item(from_slot.item_id)
- if item and item.stackable:
- var total_quantity = from_slot.quantity + to_slot.quantity
- if total_quantity <= item.max_stack:
- to_slot.quantity = total_quantity
- from_slot.clear()
- return true
- var space_available = item.max_stack - to_slot.quantity
- var amount_to_move = min(space_available, from_slot.quantity)
- to_slot.quantity += amount_to_move
- from_slot.quantity -= amount_to_move
- if from_slot.quantity <= 0:
- from_slot.clear()
- return true
-
- var temp_item_id = from_slot.item_id
- var temp_quantity = from_slot.quantity
-
- from_slot.item_id = to_slot.item_id
- from_slot.quantity = to_slot.quantity
-
- to_slot.item_id = temp_item_id
- to_slot.quantity = temp_quantity
-
- return true
-
-
-func get_first_empty_slot() -> int:
- for i in range(get_active_slot_count()):
- if slots[i].is_empty():
- return i
- return -1
-
-
-func to_dict() -> Dictionary:
- var data = []
- for slot in slots:
- data.append(slot.to_dict())
- return {
- "slots": data,
- "equipped_weapon": equipped_weapon.to_dict(),
- "equipped_hat": equipped_hat.to_dict(),
- "equipped_backpack": equipped_backpack.to_dict()
- }
-
-
-func from_dict(data: Dictionary) -> void:
- var slots_data = data.get("slots", [])
- for i in range(min(slots_data.size(), slots.size())):
- slots[i].from_dict(slots_data[i])
- for i in range(slots_data.size(), slots.size()):
- slots[i].clear()
- equipped_weapon.from_dict(data.get("equipped_weapon", {}))
- equipped_hat.from_dict(data.get("equipped_hat", {}))
- equipped_backpack.from_dict(data.get("equipped_backpack", {}))
diff --git a/scripts/inventory/player_inventory.gd.uid b/scripts/inventory/player_inventory.gd.uid
deleted file mode 100644
index 54e8924..0000000
--- a/scripts/inventory/player_inventory.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dncl3vbyhhee3
diff --git a/scripts/items/item_rigid_body_3d.gd b/scripts/items/item_rigid_body_3d.gd
deleted file mode 100644
index d5e599a..0000000
--- a/scripts/items/item_rigid_body_3d.gd
+++ /dev/null
@@ -1,4 +0,0 @@
-class_name ItemRigidBody3D
-extends RigidBody3D
-
-@export var item_id: String = ""
diff --git a/scripts/items/item_rigid_body_3d.gd.uid b/scripts/items/item_rigid_body_3d.gd.uid
deleted file mode 100644
index f0c7956..0000000
--- a/scripts/items/item_rigid_body_3d.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://co2jpexoehl55
diff --git a/scripts/level/level.gd b/scripts/level/level.gd
deleted file mode 100644
index 4c5a054..0000000
--- a/scripts/level/level.gd
+++ /dev/null
@@ -1,487 +0,0 @@
-extends Node3D
-
-const MAX_CHAT_MESSAGE_LENGTH := 160
-const MIN_NICKNAME_HEIGHT := 2.0
-const MAX_NICKNAME_HEIGHT := 8.0
-
-@export var player_scene: PackedScene
-
-var chat_visible := false
-var inventory_visible := false
-var player_list_visible := false
-
-var _player_nickname_heights: Dictionary = {}
-var _nickname_heights_requested := false
-var _nickname_height_requesters: Dictionary = {}
-
-@onready var players_container: Node3D = $PlayersContainer
-@onready var main_menu: MainMenuUI = $MainMenuUI
-@onready var multiplayer_chat: MultiplayerChatUI = $MultiplayerChatUI
-@onready var inventory_ui: InventoryUI = $InventoryUI
-@onready var player_list_ui: PlayerListUI = $PlayerListUI
-@onready var pause_menu: PauseMenuUI = $PauseMenuUI
-
-
-func _ready():
- after_ready()
-
- if DisplayServer.get_name() == "headless":
- Network.start_host("", "")
-
- pause_menu.hide_menu()
- main_menu.show_menu()
- multiplayer_chat.set_process_input(true)
-
- main_menu.host_pressed.connect(_on_host_pressed)
- main_menu.join_pressed.connect(_on_join_pressed)
- main_menu.quit_pressed.connect(_on_quit_pressed)
- pause_menu.resume_pressed.connect(_on_pause_resume_pressed)
- pause_menu.main_menu_pressed.connect(_on_pause_main_menu_pressed)
- pause_menu.quit_pressed.connect(_on_pause_quit_pressed)
-
- if inventory_ui:
- inventory_ui.inventory_closed.connect(_on_inventory_closed)
-
- if multiplayer_chat:
- multiplayer_chat.message_sent.connect(_on_chat_message_sent)
-
- Network.server_disconnected.connect(_on_server_disconnected)
- Network.connect("player_connected", Callable(self, "_on_player_connected"))
- multiplayer.peer_disconnected.connect(_remove_player)
- _update_mouse_mode()
-
-
-func _process(_delta: float) -> void:
- var can_show_player_list := (
- not main_menu.is_menu_visible() and not pause_menu.is_menu_visible() and multiplayer.has_multiplayer_peer()
- )
- if Input.is_key_pressed(KEY_TAB) and can_show_player_list:
- if not player_list_visible:
- _show_player_list()
- elif player_list_visible:
- _hide_player_list()
-
-
-func after_ready():
- var ip_address: String
- if OS.has_feature("windows"):
- if OS.has_environment("COMPUTERNAME"):
- ip_address = IP.resolve_hostname(str(OS.get_environment("COMPUTERNAME")), IP.TYPE_IPV4)
- elif OS.has_feature("x11"):
- if OS.has_environment("HOSTNAME"):
- ip_address = IP.resolve_hostname(str(OS.get_environment("HOSTNAME")), IP.TYPE_IPV4)
- elif OS.has_feature("OSX"):
- if OS.has_environment("HOSTNAME"):
- ip_address = IP.resolve_hostname(str(OS.get_environment("HOSTNAME")), IP.TYPE_IPV4)
- main_menu.address_input.text = ip_address
-
-
-func _on_server_disconnected():
- _reset_session_ui()
-
-
-func _reset_session_ui() -> void:
- for child in players_container.get_children():
- child.queue_free()
-
- var loot_root := get_node_or_null("Environment/ItemContainer")
- if loot_root:
- for child in loot_root.get_children():
- child.queue_free()
-
- chat_visible = false
- inventory_visible = false
- _hide_player_list()
- _player_nickname_heights.clear()
- _nickname_heights_requested = false
- _nickname_height_requesters.clear()
- multiplayer_chat.close_chat()
- multiplayer_chat.clear_chat()
- if inventory_ui:
- inventory_ui.close_inventory()
- inventory_ui.current_player = null
- _hide_pause_menu(false)
- main_menu.show_menu()
- _update_mouse_mode()
-
-
-func _on_player_connected(peer_id, player_info):
- var player = _add_player(peer_id, player_info)
- if multiplayer.is_server() and player:
- player.call_deferred("_sync_inventory_to_owner")
- if multiplayer.is_server():
- if peer_id != 1:
- call_deferred("_sync_nickname_heights_to_peer", peer_id)
- elif not _nickname_heights_requested:
- _nickname_heights_requested = true
- call_deferred("_request_nickname_heights")
- _refresh_player_list()
-
-
-func _on_host_pressed(nickname: String, skin: String):
- var error = Network.start_host(nickname, skin)
- if error:
- push_warning("Failed to host game. Error: " + str(error))
- main_menu.show_menu()
- _update_mouse_mode()
- return
- main_menu.hide_menu()
- _update_mouse_mode()
-
-
-func _on_join_pressed(nickname: String, skin: String, address: String):
- var error = Network.join_game(nickname, skin, address)
- if error:
- push_warning("Failed to join game. Error: " + str(error))
- main_menu.show_menu()
- _update_mouse_mode()
- return
- main_menu.hide_menu()
- _update_mouse_mode()
-
-
-func _add_player(id: int, player_info: Dictionary) -> Character:
- if DisplayServer.get_name() == "headless" and id == 1:
- return null
-
- if players_container.has_node(str(id)):
- return players_container.get_node(str(id)) as Character
-
- var player = player_scene.instantiate()
- player.name = str(id)
- player.position = get_spawn_point(id)
- players_container.add_child(player, true)
-
- var nick = Network.sanitize_nickname(str(player_info.get("nick", "")), "Player_" + str(id))
- player.nickname.text = nick
-
- var skin_enum = Network.sanitize_skin_value(player_info.get("skin", Character.SkinColor.BLUE))
- player.set_player_skin(skin_enum)
- _apply_player_nickname_height(id)
- return player
-
-
-func get_spawn_point(id: int) -> Vector3:
- var spawn_angle := fmod(float(id) * 2.399963229728653, 2.0 * PI)
- var spawn_point := Vector2.from_angle(spawn_angle) * 10
- return Vector3(spawn_point.x, 0, spawn_point.y)
-
-
-func _remove_player(id):
- _player_nickname_heights.erase(id)
- if not players_container.has_node(str(id)):
- call_deferred("_refresh_player_list")
- return
- var player_node = players_container.get_node(str(id))
- if player_node:
- player_node.queue_free()
- call_deferred("_refresh_player_list")
-
-
-func _on_quit_pressed() -> void:
- Network.leave_game()
- get_tree().quit()
-
-
-func toggle_chat():
- if main_menu.is_menu_visible() or is_gameplay_input_blocked():
- return
-
- multiplayer_chat.toggle_chat()
- chat_visible = multiplayer_chat.is_chat_visible()
- _update_mouse_mode()
-
-
-func is_chat_visible() -> bool:
- return multiplayer_chat.is_chat_visible()
-
-
-func _input(event):
- if event.is_action_pressed("pause"):
- _handle_pause_action()
- get_viewport().set_input_as_handled()
- return
-
- if is_gameplay_input_blocked():
- return
-
- if event.is_action_pressed("toggle_chat"):
- toggle_chat()
- elif chat_visible and multiplayer_chat.message.has_focus():
- pass
- elif event.is_action_pressed("inventory"):
- toggle_inventory()
- elif event is InputEventKey and event.pressed and event.keycode == KEY_F1:
- _debug_add_item()
- elif event is InputEventKey and event.pressed and event.keycode == KEY_F2:
- _debug_print_inventory()
-
-
-func _on_chat_message_sent(message_text: String) -> void:
- chat_visible = multiplayer_chat.is_chat_visible()
- _update_mouse_mode()
-
- var trimmed_message = _sanitize_chat_message(message_text)
- if trimmed_message.is_empty():
- return
-
- if multiplayer.is_server():
- _broadcast_chat_message(multiplayer.get_unique_id(), trimmed_message)
- else:
- submit_chat_message.rpc_id(1, trimmed_message)
-
-
-@rpc("any_peer", "reliable")
-func submit_chat_message(message_text: String):
- if not multiplayer.is_server():
- return
- var sender_id = multiplayer.get_remote_sender_id()
- var trimmed_message = _sanitize_chat_message(message_text)
- if trimmed_message.is_empty():
- return
- _broadcast_chat_message(sender_id, trimmed_message)
-
-
-func _broadcast_chat_message(sender_id: int, message_text: String):
- var player_info = Network.players.get(sender_id, {})
- var nick = Network.sanitize_nickname(str(player_info.get("nick", "")), "Player_" + str(sender_id))
- show_chat_message.rpc(nick, message_text)
-
-
-@rpc("authority", "call_local", "reliable")
-func show_chat_message(nick: String, msg: String):
- multiplayer_chat.add_message(nick, msg)
-
-
-func _sanitize_chat_message(message_text: String) -> String:
- var clean = message_text.strip_edges()
- if clean.length() > MAX_CHAT_MESSAGE_LENGTH:
- clean = clean.substr(0, MAX_CHAT_MESSAGE_LENGTH)
- return clean
-
-
-func toggle_inventory():
- if main_menu.is_menu_visible() or is_gameplay_input_blocked():
- return
-
- var local_player = _get_local_player()
- if not local_player:
- return
-
- inventory_visible = !inventory_visible
- if inventory_visible:
- inventory_ui.open_inventory(local_player)
- else:
- inventory_ui.close_inventory()
- _update_mouse_mode()
-
-
-func is_inventory_visible() -> bool:
- return inventory_visible
-
-
-func _show_player_list() -> void:
- if main_menu.is_menu_visible() or pause_menu.is_menu_visible() or not multiplayer.has_multiplayer_peer():
- return
- player_list_visible = true
- player_list_ui.show_players(Network.players, multiplayer.get_unique_id())
-
-
-func _hide_player_list() -> void:
- player_list_visible = false
- if player_list_ui:
- player_list_ui.hide_players()
-
-
-func _refresh_player_list() -> void:
- if player_list_visible and player_list_ui:
- player_list_ui.refresh_players(Network.players, multiplayer.get_unique_id())
-
-
-func register_player_nickname_height(player_id: int, height: float) -> void:
- if not multiplayer.is_server() or player_id <= 0:
- return
- var normalized_height := _normalize_nickname_height(height)
- _store_and_apply_player_nickname_height(player_id, normalized_height)
- sync_player_nickname_height.rpc(player_id, normalized_height)
-
-
-@rpc("authority", "reliable")
-func sync_player_nickname_height(player_id: int, height: float) -> void:
- if multiplayer.get_remote_sender_id() != 1:
- return
- _store_and_apply_player_nickname_height(player_id, _normalize_nickname_height(height))
-
-
-func _request_nickname_heights() -> void:
- if multiplayer.is_server() or not multiplayer.has_multiplayer_peer():
- return
- request_nickname_heights.rpc_id(1)
-
-
-@rpc("any_peer", "reliable")
-func request_nickname_heights() -> void:
- if not multiplayer.is_server():
- return
- var requester_id := multiplayer.get_remote_sender_id()
- if requester_id <= 0 or _nickname_height_requesters.has(requester_id):
- return
- _nickname_height_requesters[requester_id] = true
- _sync_nickname_heights_to_peer(requester_id)
-
-
-func _sync_nickname_heights_to_peer(peer_id: int) -> void:
- if not multiplayer.is_server() or peer_id <= 0:
- return
- for child in players_container.get_children():
- var player := child as Character
- if not player:
- continue
- var player_id := str(player.name).to_int()
- var height := float(_player_nickname_heights.get(player_id, player.get_current_nickname_height()))
- sync_player_nickname_height.rpc_id(peer_id, player_id, _normalize_nickname_height(height))
-
-
-func _store_and_apply_player_nickname_height(player_id: int, height: float) -> void:
- if player_id <= 0:
- return
- _player_nickname_heights[player_id] = height
- _apply_player_nickname_height(player_id)
-
-
-func _apply_player_nickname_height(player_id: int) -> void:
- if not _player_nickname_heights.has(player_id):
- return
- var player := players_container.get_node_or_null(str(player_id)) as Character
- if player:
- player.apply_synced_nickname_height(float(_player_nickname_heights[player_id]))
-
-
-func _normalize_nickname_height(height: float) -> float:
- if is_nan(height) or is_inf(height):
- return MIN_NICKNAME_HEIGHT
- return clampf(height, MIN_NICKNAME_HEIGHT, MAX_NICKNAME_HEIGHT)
-
-
-func _on_inventory_closed():
- inventory_visible = false
- _update_mouse_mode()
-
-
-func is_gameplay_input_blocked() -> bool:
- return pause_menu.is_menu_visible()
-
-
-func is_camera_input_blocked() -> bool:
- return is_gameplay_input_blocked() or inventory_visible or multiplayer_chat.is_chat_visible()
-
-
-func _handle_pause_action() -> void:
- if inventory_visible:
- _close_inventory()
- return
- if multiplayer_chat.is_chat_visible():
- _close_chat()
- return
- if pause_menu.is_menu_visible():
- _hide_pause_menu()
- return
- if main_menu.is_menu_visible() or not multiplayer.has_multiplayer_peer():
- return
- _show_pause_menu()
-
-
-func _show_pause_menu() -> void:
- pause_menu.show_menu()
- _update_mouse_mode()
-
-
-func _hide_pause_menu(restore_mouse_mode: bool = true) -> void:
- if not pause_menu.is_menu_visible():
- return
- pause_menu.hide_menu()
- if restore_mouse_mode:
- _update_mouse_mode()
-
-
-func _close_chat() -> void:
- multiplayer_chat.close_chat()
- chat_visible = false
- _update_mouse_mode()
-
-
-func _close_inventory() -> void:
- inventory_ui.close_inventory()
- inventory_visible = false
- _update_mouse_mode()
-
-
-func _update_mouse_mode() -> void:
- if DisplayServer.get_name() == "headless":
- return
- var ui_requires_cursor := (
- main_menu.is_menu_visible()
- or pause_menu.is_menu_visible()
- or inventory_visible
- or not multiplayer.has_multiplayer_peer()
- )
- if ui_requires_cursor:
- Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
- else:
- Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
-
-
-func _on_pause_resume_pressed() -> void:
- _hide_pause_menu()
-
-
-func _on_pause_main_menu_pressed() -> void:
- _hide_pause_menu(false)
- Network.leave_game()
- if not main_menu.is_menu_visible():
- _reset_session_ui()
-
-
-func _on_pause_quit_pressed() -> void:
- Network.leave_game()
- get_tree().quit()
-
-
-func update_local_inventory_display():
- if inventory_ui:
- inventory_ui.refresh_display()
-
-
-func _get_local_player() -> Character:
- var local_player_id = multiplayer.get_unique_id()
- if players_container.has_node(str(local_player_id)):
- return players_container.get_node(str(local_player_id)) as Character
- return null
-
-
-func _debug_add_item():
- if not OS.is_debug_build() or not multiplayer.is_server():
- return
- var local_player = _get_local_player()
- if local_player:
- var test_items: Array[String] = []
- for item_id in ItemDatabase.get_all_items():
- test_items.append(str(item_id))
- if test_items.is_empty():
- return
- var random_item = test_items[randi() % test_items.size()]
- local_player.request_add_item.rpc_id(1, random_item, 1)
-
-
-func _debug_print_inventory():
- var local_player = _get_local_player()
- if local_player and local_player.get_inventory():
- var inventory = local_player.get_inventory()
- print("=== Inventory Debug ===")
- for i in range(inventory.slots.size()):
- var slot = inventory.get_slot(i)
- if slot and not slot.is_empty():
- print("Slot ", i, ": ", slot.item_id, " x", slot.quantity)
- print("=====================")
- else:
- print("No inventory found for local player")
diff --git a/scripts/level/level.gd.uid b/scripts/level/level.gd.uid
deleted file mode 100644
index 40b945a..0000000
--- a/scripts/level/level.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d0dgljwwl463n
diff --git a/scripts/player/3d_godot_robot.gd b/scripts/player/3d_godot_robot.gd
deleted file mode 100644
index d7b1511..0000000
--- a/scripts/player/3d_godot_robot.gd
+++ /dev/null
@@ -1,43 +0,0 @@
-#class_name Body
-extends Node3D
-
-# 线性插值速度
-const LERP_VELOCITY: float = 0.15
-
-@export_category("Objects")
-@export var _character: CharacterBody3D = null
-@export var animation_player: AnimationPlayer = null
-
-var _current_state: StringName = &""
-
-
-func apply_rotation(_velocity: Vector3) -> void:
- var new_rotation_y = lerp_angle(rotation.y, atan2(-_velocity.x, -_velocity.z), LERP_VELOCITY)
- rotation.y = new_rotation_y
-
-
-func get_movement_animation(_velocity: Vector3) -> StringName:
- if not _character.is_on_floor():
- if _velocity.y < 0:
- return &"Fall"
- if _current_state == &"Jump2":
- return &"Jump2"
- return &"Jump"
-
- if _velocity:
- if _character._is_running() and _character.is_on_floor():
- return &"Sprint"
- return &"Run"
-
- return &"Idle"
-
-
-func play_animation_state(state: StringName, restart: bool = false) -> void:
- if not animation_player:
- return
- if not animation_player.has_animation(state):
- return
- if not restart and _current_state == state and animation_player.is_playing():
- return
- _current_state = state
- animation_player.play(state)
diff --git a/scripts/player/3d_godot_robot.gd.uid b/scripts/player/3d_godot_robot.gd.uid
deleted file mode 100644
index aeb9531..0000000
--- a/scripts/player/3d_godot_robot.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://v8j54rbc0ik
diff --git a/scripts/player/player.gd b/scripts/player/player.gd
deleted file mode 100644
index 141e77f..0000000
--- a/scripts/player/player.gd
+++ /dev/null
@@ -1,872 +0,0 @@
-class_name Character
-extends CharacterBody3D
-
-enum SkinColor { BLUE, YELLOW, GREEN, RED }
-
-@export var 正常速度 = 6.0
-@export var 冲刺速度 = 10.0
-@export var 跳跃初速度 = 7.5
-@export var 下落重力倍率 = 1.6
-@export var 昵称基准高度 := 2.0
-@export var 服务器每秒动画请求数 := 10.0
-@export var 服务器动画请求突发上限 := 6.0
-@export var 拾取动画延迟毫秒 := 1000
-@export var 拾取动画时间窗口毫秒 := 2500
-@export var 拾取请求冷却毫秒 := 1000
-const 允许动画状态 := {
- &"Idle": true,
- &"Run": true,
- &"Sprint": true,
- &"Jump": true,
- &"Jump2": true,
- &"Fall": true,
- &"Attack1": true,
- &"Emote2": true
-}
-const HAT_NODES_BY_ITEM := {
- "fedora": "Fedora",
- "headphones": "Headphones",
- "pirate_hat": "PirateHat",
- "sheriff_hat": "SheriffHat",
- "sombrero": "Sombrero",
- "wizard_hat": "WizardHat"
-}
-const WEAPON_NODES_BY_ITEM := {"sword": "Sword", "sword_big": "SwordBig", "axe": "Axe"}
-const BACKPACK_NODES_BY_ITEM := {"backpack": "Backpack"}
-const HEAD_EQUIPMENT_PATH := "GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach/"
-const HAND_EQUIPMENT_PATH := "GodotRobot3D/RobotArmature/Skeleton3D/LeftHandAttach/"
-const BACK_EQUIPMENT_PATH := "GodotRobot3D/RobotArmature/Skeleton3D/BackAttach/"
-const FIRST_PERSON_HIDDEN_BONES: Array[StringName] = [&"Head", &"HeadTop"]
-
-@export var skin_color: SkinColor = SkinColor.BLUE
-
-@export_category("Nickname")
-@export_range(0.0, 1.0, 0.01) var nickname_clearance: float = 0.2
-
-@export_category("Objects")
-@export var _body: Node3D = null
-@export var _spring_arm_offset: SpringArmCharacter = null
-
-@export_category("Skin Colors")
-@export var blue_texture: CompressedTexture2D
-@export var yellow_texture: CompressedTexture2D
-@export var green_texture: CompressedTexture2D
-@export var red_texture: CompressedTexture2D
-
-var player_inventory: PlayerInventory
-var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
-var can_double_jump = true
-var has_double_jumped = false
-var is_attacking := false
-var is_collecting := false
-
-var _current_speed: float
-var _spawn_point = Vector3(0, 5, 0)
-var _animation_sequence := 0
-var _last_applied_animation_sequence := 0
-var _last_requested_animation: StringName = &""
-var _appearance_sync_requesters: Dictionary = {}
-var _server_animation_request_tokens := 服务器动画请求突发上限
-var _last_server_animation_token_update_msec := 0
-var _server_pickup_animation_started_msec := -1
-var _last_server_pickup_request_msec := -拾取请求冷却毫秒
-var _pickup_area_camera_yaw_offset := 0.0
-var _equipped_hat_visual_id := ""
-
-@onready var nickname: Label3D = $PlayerNick/Nickname
-
-@onready var _bottom_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/Bottom")
-@onready var _chest_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/Chest")
-@onready var _face_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/Face")
-@onready var _limbs_head_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/LimbsAndHead")
-@onready var _skeleton: Skeleton3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D")
-@onready var _pickup_area: Area3D = $GodotRobot3D/InfrontArea3D
-@onready var _first_person_hud: CanvasLayer = $FirstPersonHUD
-
-
-func _enter_tree():
- set_multiplayer_authority(str(name).to_int())
- $SpringArmOffset/SpringArm3D/Camera3D.current = is_multiplayer_authority()
-
-
-func _ready():
- if multiplayer.is_server():
- player_inventory = PlayerInventory.new()
- _add_starting_items()
- call_deferred("_sync_equipment_appearance")
- if not is_multiplayer_authority():
- call_deferred("_sync_inventory_to_owner")
-
- set_player_skin(skin_color)
- var animation_player := get_node_or_null("GodotRobot3D/AnimationPlayer") as AnimationPlayer
- if animation_player:
- animation_player.animation_finished.connect(_on_animation_finished)
- _body.play_animation_state(&"Idle")
- nickname.visible = true
- _set_nickname_height(昵称基准高度)
- call_deferred("_update_nickname_height")
- if not multiplayer.is_server():
- call_deferred("_request_equipment_appearance")
- if is_multiplayer_authority() and _spring_arm_offset:
- _pickup_area_camera_yaw_offset = wrapf(
- _pickup_area.global_rotation.y - _spring_arm_offset.global_rotation.y, -PI, PI
- )
- _spring_arm_offset.perspective_changed.connect(_on_camera_perspective_changed)
- _on_camera_perspective_changed(_spring_arm_offset.is_first_person)
-
-
-func _on_camera_perspective_changed(first_person: bool) -> void:
- if not is_multiplayer_authority():
- return
- _body.visible = true
- _bottom_mesh.visible = not first_person
- _chest_mesh.visible = not first_person
- _face_mesh.visible = not first_person
- _limbs_head_mesh.visible = true
- nickname.visible = not first_person
- _first_person_hud.visible = first_person
- for bone_name in FIRST_PERSON_HIDDEN_BONES:
- var bone_index := _skeleton.find_bone(bone_name)
- if bone_index >= 0:
- _skeleton.set_bone_pose_scale(bone_index, Vector3.ZERO if first_person else Vector3.ONE)
- if first_person:
- _align_pickup_area_with_camera()
- else:
- _pickup_area.rotation = Vector3.ZERO
- call_deferred("_refresh_nickname_height_after_perspective_change")
-
-
-func _refresh_nickname_height_after_perspective_change() -> void:
- if not is_multiplayer_authority() or _is_local_first_person():
- return
- var height := _calculate_nickname_height(_equipped_hat_visual_id)
- _set_nickname_height(height)
- nickname.visible = true
- if multiplayer.is_server():
- _broadcast_nickname_height(height)
-
-
-func _align_pickup_area_with_camera() -> void:
- var pickup_rotation := _pickup_area.global_rotation
- pickup_rotation.y = wrapf(_spring_arm_offset.global_rotation.y + _pickup_area_camera_yaw_offset, -PI, PI)
- _pickup_area.global_rotation = pickup_rotation
-
-
-func _physics_process(delta):
- if not multiplayer.has_multiplayer_peer() or not is_multiplayer_authority():
- return
-
- var current_scene = get_tree().get_current_scene()
- var should_freeze = false
- if current_scene:
- if current_scene.has_method("is_gameplay_input_blocked") and current_scene.is_gameplay_input_blocked():
- should_freeze = true
- elif current_scene.has_method("is_chat_visible") and current_scene.is_chat_visible():
- should_freeze = true
- elif current_scene.has_method("is_inventory_visible") and current_scene.is_inventory_visible():
- should_freeze = true
-
- if is_attacking or is_collecting:
- velocity.x = 0
- velocity.z = 0
- _apply_gravity(delta)
- move_and_slide()
- return
-
- if should_freeze:
- _freeze()
- _apply_gravity(delta)
- move_and_slide()
- _request_animation(_body.get_movement_animation(velocity))
- return
-
- if Input.is_action_just_pressed("pickup") and is_on_floor() and _has_collectible_item_in_front():
- is_collecting = true
- _request_animation(&"Emote2", true)
- return
-
- if Input.is_action_just_pressed("attack") and is_on_floor():
- _start_attack()
- return
-
- if is_on_floor():
- can_double_jump = true
- has_double_jumped = false
-
- if Input.is_action_just_pressed("jump"):
- velocity.y = 跳跃初速度
- can_double_jump = true
- _request_animation(&"Jump", true)
- else:
- _apply_gravity(delta)
-
- if can_double_jump and not has_double_jumped and Input.is_action_just_pressed("jump"):
- velocity.y = 跳跃初速度
- has_double_jumped = true
- can_double_jump = false
- _request_animation(&"Jump2", true)
-
- _move()
- var collided = move_and_slide()
- if collided:
- _push_collided_items()
-
- _request_animation(_body.get_movement_animation(velocity))
-
-
-func _apply_gravity(delta: float) -> void:
- if is_on_floor():
- return
- var gravity_multiplier = 下落重力倍率 if velocity.y < 0 else 1.0
- velocity.y -= gravity * gravity_multiplier * delta
-
-
-func _start_attack() -> void:
- if is_attacking or is_collecting or not is_on_floor():
- return
- is_attacking = true
- velocity.x = 0
- velocity.z = 0
- _request_animation(&"Attack1", true)
-
-
-func _on_animation_finished(animation_name: StringName) -> void:
- match animation_name:
- &"Attack1":
- is_attacking = false
- &"Emote2":
- is_collecting = false
-
-
-func _request_animation(state: StringName, restart: bool = false) -> void:
- if not 允许动画状态.has(state):
- return
- if not restart and _last_requested_animation == state:
- return
- _last_requested_animation = state
- _body.play_animation_state(state, restart)
- if multiplayer.is_server():
- request_animation_state(state)
- else:
- request_animation_state.rpc_id(1, state)
-
-
-@rpc("any_peer", "call_local", "reliable")
-func request_animation_state(state: StringName) -> void:
- if not multiplayer.is_server() or not _is_owner_request():
- return
- if not 允许动画状态.has(state):
- return
- if not _server_consume_animation_request_token():
- return
- if state == &"Emote2":
- if not _is_grounded_on_server() or not _has_collectible_item_in_front():
- return
- _server_pickup_animation_started_msec = Time.get_ticks_msec()
- else:
- _server_pickup_animation_started_msec = -1
- _server_publish_animation(state)
-
-
-func _server_consume_animation_request_token() -> bool:
- var now := Time.get_ticks_msec()
- if _last_server_animation_token_update_msec == 0:
- _last_server_animation_token_update_msec = now
- else:
- var elapsed_seconds := (now - _last_server_animation_token_update_msec) / 1000.0
- _server_animation_request_tokens = minf(
- 服务器动画请求突发上限,
- _server_animation_request_tokens + elapsed_seconds * 服务器每秒动画请求数
- )
- _last_server_animation_token_update_msec = now
-
- if _server_animation_request_tokens < 1.0:
- return false
- _server_animation_request_tokens -= 1.0
- return true
-
-
-func _server_publish_animation(state: StringName) -> void:
- if not multiplayer.is_server():
- return
- _animation_sequence += 1
- sync_animation_state.rpc(state, _animation_sequence)
- sync_animation_state(state, _animation_sequence)
-
-
-@rpc("any_peer", "call_local", "reliable")
-func sync_animation_state(state: StringName, sequence: int) -> void:
- var sender_id := multiplayer.get_remote_sender_id()
- if sender_id != 1 and not (sender_id == 0 and multiplayer.is_server()):
- return
- if sequence <= _last_applied_animation_sequence:
- return
- _last_applied_animation_sequence = sequence
-
- if not 允许动画状态.has(state):
- return
-
- # The owner already played the input-driven state immediately.
- if is_multiplayer_authority():
- return
- _body.play_animation_state(state, true)
-
-
-func _push_collided_items() -> void:
- for i in get_slide_collision_count():
- var c = get_slide_collision(i)
- if c.get_collider() is RigidBody3D:
- apply_force_to_server_object.rpc_id(1, c.get_collider().name, -c.get_normal())
-
-
-func _process(_delta):
- if not multiplayer.has_multiplayer_peer():
- return
- if not is_multiplayer_authority():
- return
- var first_person := _spring_arm_offset != null and _spring_arm_offset.is_first_person
- if first_person:
- _align_pickup_area_with_camera()
- var camera_input_blocked := false
- var current_scene := get_tree().get_current_scene()
- if current_scene and current_scene.has_method("is_camera_input_blocked"):
- camera_input_blocked = current_scene.is_camera_input_blocked()
- _first_person_hud.visible = (
- first_person and not camera_input_blocked and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED
- )
- _check_out_of_bounds()
-
-
-func _freeze():
- velocity.x = 0
- velocity.z = 0
- _current_speed = 0
- _request_animation(&"Idle")
-
-
-func _move() -> void:
- var input_direction: Vector2 = Vector2.ZERO
- if is_multiplayer_authority():
- input_direction = Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
-
- var direction: Vector3 = transform.basis * Vector3(input_direction.x, 0, input_direction.y).normalized()
-
- _is_running()
- direction = direction.rotated(Vector3.UP, _spring_arm_offset.rotation.y)
-
- if direction:
- velocity.x = direction.x * _current_speed
- velocity.z = direction.z * _current_speed
- _body.apply_rotation(velocity)
- return
-
- velocity.x = move_toward(velocity.x, 0, _current_speed)
- velocity.z = move_toward(velocity.z, 0, _current_speed)
-
-
-func _is_running() -> bool:
- if Input.is_action_pressed("shift"):
- _current_speed = 冲刺速度
- return true
- _current_speed = 正常速度
- return false
-
-
-func _check_out_of_bounds():
- if global_transform.origin.y < -15.0:
- _reset_position_after_fall()
-
-
-func _reset_position_after_fall():
- global_transform.origin = _spawn_point
- velocity = Vector3.ZERO
-
-
-func _get_texture_from_name(color: SkinColor) -> CompressedTexture2D:
- match color:
- SkinColor.BLUE:
- return blue_texture
- SkinColor.GREEN:
- return green_texture
- SkinColor.RED:
- return red_texture
- SkinColor.YELLOW:
- return yellow_texture
- _:
- return blue_texture
-
-
-func set_player_skin(skin_name: SkinColor) -> void:
- var texture = _get_texture_from_name(skin_name)
-
- _set_mesh_texture(_bottom_mesh, texture)
- _set_mesh_texture(_chest_mesh, texture)
- _set_mesh_texture(_face_mesh, texture)
- _set_mesh_texture(_limbs_head_mesh, texture)
-
-
-func _set_mesh_texture(mesh_instance: MeshInstance3D, texture: CompressedTexture2D) -> void:
- if mesh_instance:
- var new_material := StandardMaterial3D.new()
- new_material.albedo_texture = texture
- mesh_instance.set_surface_override_material(0, new_material)
-
-
-@rpc("any_peer", "call_local", "reliable")
-func sync_inventory_to_owner(inventory_data: Dictionary):
- var sender_id = multiplayer.get_remote_sender_id()
- if sender_id != 1 and not (sender_id == 0 and multiplayer.is_server()):
- return
-
- if not is_multiplayer_authority():
- return
-
- if not player_inventory:
- player_inventory = PlayerInventory.new()
- player_inventory.from_dict(inventory_data)
-
- var level_scene = get_tree().get_current_scene()
- if (
- level_scene
- and get_multiplayer_authority() == multiplayer.get_unique_id()
- and level_scene.has_method("update_local_inventory_display")
- ):
- level_scene.update_local_inventory_display()
-
-
-@rpc("any_peer", "call_local", "reliable")
-func request_move_item(from_slot: int, to_slot: int, quantity: int = -1):
- if not multiplayer.is_server():
- return
-
- var requesting_client = multiplayer.get_remote_sender_id()
- if not _is_owner_request():
- push_warning(
- (
- "Client "
- + str(requesting_client)
- + " tried to modify inventory for player "
- + str(get_multiplayer_authority())
- )
- )
- return
-
- if not player_inventory:
- return
-
- if not player_inventory.is_slot_active(from_slot) or not player_inventory.is_slot_active(to_slot):
- push_warning("Invalid slot indices: from=" + str(from_slot) + " to=" + str(to_slot))
- return
-
- if quantity != -1 and quantity <= 0:
- push_warning("Invalid move quantity: " + str(quantity))
- return
-
- var success = false
- if quantity == -1:
- success = player_inventory.move_item(from_slot, to_slot)
- if not success:
- success = player_inventory.swap_items(from_slot, to_slot)
- else:
- success = player_inventory.move_item(from_slot, to_slot, quantity)
-
- if success:
- _sync_inventory_to_owner()
-
-
-@rpc("any_peer", "call_local", "reliable")
-func request_add_item(item_id: String, quantity: int = 1):
- if not multiplayer.is_server():
- return
-
- var requesting_client = multiplayer.get_remote_sender_id()
- var is_local_server_call = requesting_client == 0 and multiplayer.get_unique_id() == 1
- if requesting_client != 1 and not is_local_server_call:
- push_warning(
- "Client " + str(requesting_client) + " tried to add items to player " + str(get_multiplayer_authority())
- )
- return
-
- if not player_inventory:
- return
-
- if quantity <= 0:
- push_warning("Invalid quantity: " + str(quantity))
- return
-
- var item = ItemDatabase.get_item(item_id)
- if not item:
- push_warning("Item not found: " + item_id)
- return
-
- var remaining = player_inventory.add_item(item, quantity)
- var added = quantity - remaining
-
- if added > 0:
- _sync_inventory_to_owner()
-
-
-func request_add_single_item(item_id: String) -> bool:
- if not multiplayer.is_server():
- return false
-
- if player_inventory == null:
- return false
-
- var item = ItemDatabase.get_item(item_id)
- if not item:
- push_warning("Item not found: " + item_id)
- return false
-
- var remaining = player_inventory.add_item(item, 1)
-
- if remaining == 0:
- _sync_inventory_to_owner()
- return true
- return false
-
-
-@rpc("any_peer", "call_local", "reliable")
-func request_remove_item(item_id: String, quantity: int = 1):
- if not multiplayer.is_server():
- return
-
- var requesting_client = multiplayer.get_remote_sender_id()
- if not _is_owner_request():
- push_warning(
- (
- "Client "
- + str(requesting_client)
- + " tried to remove items from player "
- + str(get_multiplayer_authority())
- )
- )
- return
-
- if not player_inventory:
- return
-
- if quantity <= 0:
- push_warning("Invalid quantity: " + str(quantity))
- return
-
- var removed = player_inventory.remove_item(item_id, quantity)
-
- if removed > 0:
- _sync_inventory_to_owner()
-
-
-@rpc("authority", "call_local", "reliable")
-func add_world_item(scene_path: String, player_position: Vector3) -> void:
- var item_container = get_node_or_null("/root/Level/Environment/ItemContainer")
- if not item_container:
- push_warning("ItemContainer not found at /root/Level/Environment/ItemContainer")
- return
- if scene_path.is_empty() or not ResourceLoader.exists(scene_path):
- push_warning("Cannot add world item: invalid scene path '" + scene_path + "'")
- return
- var packed_scene = load(scene_path) as PackedScene
- if not packed_scene:
- push_warning("Cannot add world item: scene path is not a PackedScene '" + scene_path + "'")
- return
- var instance_item := packed_scene.instantiate() as Node3D
- if not instance_item:
- push_warning("Cannot add world item: scene root is not a Node3D '" + scene_path + "'")
- return
- item_container.add_child(instance_item, true)
- instance_item.global_position = player_position
-
-
-func get_inventory() -> PlayerInventory:
- return player_inventory
-
-
-func _sync_inventory_to_owner() -> void:
- if not multiplayer.is_server() or not player_inventory:
- return
- var owner_id = get_multiplayer_authority()
- if owner_id == 1:
- sync_inventory_to_owner(player_inventory.to_dict())
- else:
- sync_inventory_to_owner.rpc_id(owner_id, player_inventory.to_dict())
-
-
-@rpc("any_peer", "call_local", "reliable")
-func request_equip_item(from_slot: int, item_type: Item.ItemType) -> void:
- if not multiplayer.is_server() or not _is_owner_request():
- return
- if not player_inventory or not player_inventory.is_slot_active(from_slot):
- return
- if item_type != Item.ItemType.WEAPON and item_type != Item.ItemType.HAT and item_type != Item.ItemType.BACKPACK:
- return
- if player_inventory.equip_from_slot(from_slot, item_type):
- _sync_inventory_to_owner()
- _sync_equipment_appearance()
-
-
-@rpc("any_peer", "call_local", "reliable")
-func request_unequip_item(item_type: Item.ItemType, destination_slot: int = -1) -> void:
- if not multiplayer.is_server() or not _is_owner_request():
- return
- if not player_inventory:
- return
- if item_type != Item.ItemType.WEAPON and item_type != Item.ItemType.HAT and item_type != Item.ItemType.BACKPACK:
- return
- if destination_slot < -1 or destination_slot >= PlayerInventory.MAX_INVENTORY_SIZE:
- return
- if destination_slot >= 0 and not player_inventory.is_slot_active(destination_slot):
- return
- if player_inventory.unequip_to_slot(item_type, destination_slot):
- _sync_inventory_to_owner()
- _sync_equipment_appearance()
-
-
-func _is_owner_request() -> bool:
- var sender := multiplayer.get_remote_sender_id()
- return sender == get_multiplayer_authority() or (sender == 0 and multiplayer.is_server())
-
-
-func _sync_equipment_appearance() -> void:
- if not multiplayer.is_server() or not player_inventory:
- return
- var weapon_id := player_inventory.equipped_weapon.item_id
- var hat_id := player_inventory.equipped_hat.item_id
- var backpack_id := player_inventory.equipped_backpack.item_id
- var nickname_height := _calculate_nickname_height(hat_id)
- _broadcast_nickname_height(nickname_height)
- sync_equipment_appearance.rpc(weapon_id, hat_id, backpack_id)
- sync_equipment_appearance(weapon_id, hat_id, backpack_id)
-
-
-func _request_equipment_appearance() -> void:
- if multiplayer.is_server() or not multiplayer.has_multiplayer_peer():
- return
- request_equipment_appearance.rpc_id(1)
-
-
-@rpc("any_peer", "reliable")
-func request_equipment_appearance() -> void:
- if not multiplayer.is_server() or not player_inventory:
- return
- var requester_id := multiplayer.get_remote_sender_id()
- if requester_id <= 0:
- return
- if _appearance_sync_requesters.has(requester_id):
- return
- _appearance_sync_requesters[requester_id] = true
- _sync_equipment_appearance_to_peer(requester_id)
-
-
-func _sync_equipment_appearance_to_peer(peer_id: int) -> void:
- if not multiplayer.is_server() or not player_inventory or peer_id <= 0:
- return
- var weapon_id := player_inventory.equipped_weapon.item_id
- var hat_id := player_inventory.equipped_hat.item_id
- var backpack_id := player_inventory.equipped_backpack.item_id
- sync_equipment_appearance.rpc_id(peer_id, weapon_id, hat_id, backpack_id)
-
-
-@rpc("any_peer", "reliable")
-func sync_equipment_appearance(weapon_id: String, hat_id: String, backpack_id: String) -> void:
- var sender := multiplayer.get_remote_sender_id()
- if sender != 1 and not (sender == 0 and multiplayer.is_server()):
- return
- _set_equipment_visibility(weapon_id, hat_id, backpack_id)
-
-
-func _set_equipment_visibility(weapon_id: String, hat_id: String, backpack_id: String) -> void:
- _equipped_hat_visual_id = hat_id
- _set_equipment_nodes_visibility(HEAD_EQUIPMENT_PATH, HAT_NODES_BY_ITEM, hat_id)
- _set_equipment_nodes_visibility(HAND_EQUIPMENT_PATH, WEAPON_NODES_BY_ITEM, weapon_id)
- _set_equipment_nodes_visibility(BACK_EQUIPMENT_PATH, BACKPACK_NODES_BY_ITEM, backpack_id)
-
-
-func _broadcast_nickname_height(height: float) -> void:
- if not multiplayer.is_server():
- return
- var level_scene := get_tree().get_current_scene()
- if level_scene and level_scene.has_method("register_player_nickname_height"):
- level_scene.register_player_nickname_height(get_multiplayer_authority(), height)
-
-
-func _set_equipment_nodes_visibility(parent_path: String, nodes_by_item: Dictionary, equipped_item_id: String) -> void:
- for item_id in nodes_by_item:
- var equipment := get_node_or_null(parent_path + str(nodes_by_item[item_id])) as Node3D
- if equipment:
- equipment.visible = item_id == equipped_item_id
-
-
-func _update_nickname_height(hat_id: String = "") -> void:
- if not nickname:
- return
- nickname.visible = not _is_local_first_person()
- _set_nickname_height(_calculate_nickname_height(hat_id))
-
-
-func _calculate_nickname_height(hat_id: String) -> float:
- var target_height := 拾取动画延迟毫秒
- var equipped_hat := _get_hat_node(hat_id)
- if equipped_hat:
- var hat_top := _get_visual_top(equipped_hat)
- if hat_top > -INF and hat_top < INF:
- target_height = max(拾取动画延迟毫秒, hat_top + nickname_clearance)
- return target_height
-
-
-func _set_nickname_height(height: float) -> void:
- var nickname_position := nickname.position
- nickname_position.y = height
- nickname.position = nickname_position
-
-
-func apply_synced_nickname_height(height: float) -> void:
- if not nickname:
- return
- nickname.visible = not _is_local_first_person()
- if height > -INF and height < INF:
- _set_nickname_height(maxf(昵称基准高度, height))
- else:
- _set_nickname_height(昵称基准高度)
-
-
-func _is_local_first_person() -> bool:
- return is_multiplayer_authority() and _spring_arm_offset != null and _spring_arm_offset.is_first_person
-
-
-func get_current_nickname_height() -> float:
- return nickname.position.y if nickname else 昵称基准高度
-
-
-func _get_hat_node(hat_id: String) -> Node3D:
- if HAT_NODES_BY_ITEM.has(hat_id):
- var requested_hat_path := HEAD_EQUIPMENT_PATH + str(HAT_NODES_BY_ITEM[hat_id])
- var requested_hat := get_node_or_null(requested_hat_path) as Node3D
- if requested_hat:
- return requested_hat
- return null
-
-
-func _get_visual_top(root: Node3D) -> float:
- var visual_top := -INF
- if root is MeshInstance3D:
- visual_top = _get_mesh_top(root as MeshInstance3D)
-
- for child in root.find_children("*", "MeshInstance3D", true, false):
- var mesh_instance := child as MeshInstance3D
- if mesh_instance:
- visual_top = max(visual_top, _get_mesh_top(mesh_instance))
- return visual_top
-
-
-func _get_mesh_top(mesh_instance: MeshInstance3D) -> float:
- var visual_top := -INF
- var mesh_bounds := mesh_instance.get_aabb()
- for endpoint_index in range(8):
- var endpoint_global := mesh_instance.to_global(mesh_bounds.get_endpoint(endpoint_index))
- visual_top = max(visual_top, to_local(endpoint_global).y)
- return visual_top
-
-
-func _add_starting_items():
- if not player_inventory:
- return
-
- var backpack := ItemDatabase.get_item("backpack")
- if backpack:
- player_inventory.add_item(backpack, 1)
-
- var starting_item_ids: Array[String] = [
- "fedora",
- "headphones",
- "pirate_hat",
- "sheriff_hat",
- "sombrero",
- "wizard_hat",
- "sword",
- "sword_big",
- "axe",
- "chicken_leg",
- "bone",
- "chalice"
- ]
-
- for item_id in starting_item_ids:
- var item = ItemDatabase.get_item(item_id)
- if item:
- player_inventory.add_item(item, 1)
-
-
-func pickup() -> void:
- if multiplayer.is_server():
- request_pickup()
- else:
- request_pickup.rpc_id(1)
-
-
-@rpc("any_peer", "call_local", "reliable")
-func request_pickup() -> void:
- if not multiplayer.is_server() or not _is_owner_request():
- return
- var now := Time.get_ticks_msec()
- if now - _last_server_pickup_request_msec < 拾取请求冷却毫秒:
- return
- _last_server_pickup_request_msec = now
-
- var animation_elapsed := now - _server_pickup_animation_started_msec
- if (
- _server_pickup_animation_started_msec < 0
- or animation_elapsed < 拾取动画时间窗口毫秒
- or animation_elapsed > 拾取动画时间窗口毫秒
- ):
- return
- if not _is_grounded_on_server():
- return
- if not _has_collectible_item_in_front():
- return
- _server_pickup_animation_started_msec = -1
- _server_pickup()
-
-
-func _server_pickup() -> void:
- for item in _get_collectible_items_in_front():
- var result := request_add_single_item(item.item_id)
- if result and item.is_inside_tree():
- item.queue_free()
-
-
-func _has_collectible_item_in_front() -> bool:
- return not _get_collectible_items_in_front().is_empty()
-
-
-func _get_collectible_items_in_front() -> Array[ItemRigidBody3D]:
- var collectible_items: Array[ItemRigidBody3D] = []
- var pickup_area := get_node_or_null("GodotRobot3D/InfrontArea3D") as Area3D
- if not pickup_area:
- return collectible_items
-
- for body in pickup_area.get_overlapping_bodies():
- var item := body as ItemRigidBody3D
- if item and not item.item_id.is_empty() and ItemDatabase.get_item(item.item_id):
- collectible_items.append(item)
- return collectible_items
-
-
-@rpc("any_peer", "call_local", "reliable")
-func apply_force_to_server_object(object_name: String, normal: Vector3) -> void:
- var object_node = get_node_or_null("/root/Level/Environment/ItemContainer")
- if object_node:
- for n in object_node.get_children():
- if n.name == object_name and n is RigidBody3D:
- n.apply_force(normal * 100)
-
-
-func _is_grounded_on_server() -> bool:
- if not multiplayer.is_server() or not is_inside_tree():
- return false
- var query := PhysicsRayQueryParameters3D.new()
- query.from = global_position + Vector3.UP * 0.15
- query.to = global_position + Vector3.DOWN * 0.3
- query.collision_mask = 2
- query.exclude = [get_rid()]
- query.collide_with_areas = false
- return not get_world_3d().direct_space_state.intersect_ray(query).is_empty()
diff --git a/scripts/player/player.gd.uid b/scripts/player/player.gd.uid
deleted file mode 100644
index bacd93f..0000000
--- a/scripts/player/player.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c2si8gkbnde0c
diff --git a/scripts/player/spring_arm_offset.gd b/scripts/player/spring_arm_offset.gd
deleted file mode 100644
index ed7125f..0000000
--- a/scripts/player/spring_arm_offset.gd
+++ /dev/null
@@ -1,92 +0,0 @@
-class_name SpringArmCharacter
-extends Node3D
-
-signal perspective_changed(first_person: bool)
-
-const MOUSE_SENSIBILITY: float = 0.005
-
-@export_category("Objects")
-@export var _spring_arm: SpringArm3D = null
-@export var _first_person_anchor: Node3D = null
-
-@export_category("Camera Perspective")
-@export_range(0.0, 10.0, 0.05) var third_person_distance := 5.0
-@export_range(0.0, 1.0, 0.05) var first_person_distance := 0.0
-@export_range(0.0, 3.0, 0.05) var third_person_height := 2.0
-@export_range(1.0, 179.0, 1.0) var third_person_fov := 75.0
-@export_range(1.0, 179.0, 1.0) var first_person_fov := 90.0
-
-var is_first_person := false
-var _yaw := 0.0
-var _pitch := 0.0
-
-@onready var _camera: Camera3D = _spring_arm.get_node_or_null("Camera3D") as Camera3D
-
-
-func _ready() -> void:
- _yaw = rotation.y
- _pitch = _spring_arm.rotation.x if _spring_arm else 0.0
- _apply_axis_lock()
- _apply_perspective()
-
- # 进入直接切换第一人称
- is_first_person = not is_first_person
- _apply_perspective()
- perspective_changed.emit(is_first_person)
- get_viewport().set_input_as_handled()
-
-
-func _process(_delta: float) -> void:
- _apply_axis_lock()
- if is_first_person and _spring_arm and _first_person_anchor:
- _spring_arm.global_position = _first_person_anchor.global_position
-
-
-func _unhandled_input(event: InputEvent) -> void:
- if not is_multiplayer_authority():
- return
-
- var current_scene := get_tree().get_current_scene()
- if current_scene and current_scene.has_method("is_camera_input_blocked"):
- if current_scene.is_camera_input_blocked():
- return
-
- #if event.is_action_pressed("toggle_camera"):
- #is_first_person = not is_first_person
- #_apply_perspective()
- #perspective_changed.emit(is_first_person)
- #get_viewport().set_input_as_handled()
- #return
-
- if not (event is InputEventMouseMotion):
- return
- if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
- return
-
- _yaw = wrapf(_yaw - event.relative.x * MOUSE_SENSIBILITY, -PI, PI)
- _pitch = clampf(_pitch - event.relative.y * MOUSE_SENSIBILITY, -PI / 4.0, PI / 24.0)
- _apply_axis_lock()
-
-
-func _apply_axis_lock() -> void:
- rotation = Vector3(0.0, _yaw, 0.0)
- if _spring_arm:
- _spring_arm.rotation = Vector3(_pitch, 0.0, 0.0)
- if _camera:
- _camera.rotation = Vector3.ZERO
-
-
-func _apply_perspective() -> void:
- if not _spring_arm:
- return
- if is_first_person:
- _spring_arm.spring_length = first_person_distance
- if _first_person_anchor:
- _spring_arm.global_position = _first_person_anchor.global_position
- if _camera:
- _camera.fov = first_person_fov
- else:
- _spring_arm.spring_length = third_person_distance
- _spring_arm.position = Vector3(0.0, third_person_height, 0.0)
- if _camera:
- _camera.fov = third_person_fov
diff --git a/scripts/player/spring_arm_offset.gd.uid b/scripts/player/spring_arm_offset.gd.uid
deleted file mode 100644
index 9a19aec..0000000
--- a/scripts/player/spring_arm_offset.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bj7yrijm7bppq
diff --git a/scripts/player/voice_chat.gd b/scripts/player/voice_chat.gd
deleted file mode 100644
index 5130ab9..0000000
--- a/scripts/player/voice_chat.gd
+++ /dev/null
@@ -1,55 +0,0 @@
-extends Node
-## 最简近距离语音:按住 C(talk)采集麦克风 → 广播给所有对端 →
-## 在说话者位置的 RaytracedAudioPlayer3D 回放。
-## 距离衰减/声像由 3D 音频完成,墙体闷音、环境混响由 raytraced_audio 插件完成。
-## 依赖:default_bus_layout.tres 的 mic 总线(静音 + AudioEffectCapture)、
-## RaytracedReverb/RaytracedAmbient 总线(插件用)、player.tscn 根节点下的 Voice 节点。
-
-@export var max_distance := 25.0
-
-var _capture: AudioEffectCapture
-var _voice_3d: RaytracedAudioPlayer3D
-var _playback: AudioStreamGeneratorPlayback
-
-
-func _ready() -> void:
- _capture = AudioServer.get_bus_effect(AudioServer.get_bus_index("mic"), 0)
- _voice_3d = RaytracedAudioPlayer3D.new()
- var gen := AudioStreamGenerator.new()
- gen.mix_rate = 48000.0 # 与 audio/driver/mix_rate 一致,否则变速变调
- _voice_3d.stream = gen
- _voice_3d.position = Vector3(0, 1.5, 0) # 嘴部高度
- _voice_3d.max_distance = max_distance
- add_child(_voice_3d)
- if is_multiplayer_authority() and DisplayServer.get_name() != "headless":
- var mic := AudioStreamPlayer.new()
- mic.stream = AudioStreamMicrophone.new()
- mic.bus = "mic"
- add_child(mic)
- mic.play()
- # 监听器只建在本地玩家身上(多个监听器会互相抢 current)
- var listener := RaytracedAudioListener.new()
- listener.position = Vector3(0, 1.6, 0) # 头部高度
- add_child(listener)
- listener.owner = get_parent() # 插件的 ambient 计算会读 owner,代码创建的节点必须手动指定
- else:
- _voice_3d.play()
- _playback = _voice_3d.get_stream_playback()
-
-
-func _process(_delta: float) -> void:
- if not is_multiplayer_authority() or _capture == null:
- return
- if not Input.is_action_pressed("talk"):
- _capture.clear_buffer() # 松开时丢弃积压,避免下次开口带旧音频
- return
- var frames := _capture.get_buffer(_capture.get_frames_available())
- if not frames.is_empty():
- receive_voice.rpc(frames)
-
-
-@rpc("any_peer", "call_remote", "unreliable_ordered", 1)
-func receive_voice(frames: PackedVector2Array) -> void:
- if not _playback:
- _playback = _voice_3d.get_stream_playback()
- _playback.push_buffer(frames)
diff --git a/scripts/player/voice_chat.gd.uid b/scripts/player/voice_chat.gd.uid
deleted file mode 100644
index 69ba5a6..0000000
--- a/scripts/player/voice_chat.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://3i8mgirdlpli
diff --git a/scripts/ui/inventory_slot_ui.gd b/scripts/ui/inventory_slot_ui.gd
deleted file mode 100644
index 292eeeb..0000000
--- a/scripts/ui/inventory_slot_ui.gd
+++ /dev/null
@@ -1,147 +0,0 @@
-class_name InventorySlotUI
-extends Control
-
-signal slot_clicked(slot_index: int, button: int)
-signal slot_double_clicked(slot_index: int)
-signal item_hovered(slot_index: int, item: Item)
-signal item_unhovered
-
-enum TYPE {
- INVENTORY,
- WEAPON,
- HAT,
- BACKPACK,
-}
-var slot_index: int = 0
-var slot_type: TYPE = TYPE.INVENTORY
-var inventory_data: InventorySlot
-var parent_inventory: Control
-
-var _is_mouse_hovering := false
-
-@onready var background: Panel = $Background
-@onready var item_icon: TextureRect = $ItemIcon
-@onready var quantity_label: Label = $QuantityLabel
-
-
-func _ready():
- gui_input.connect(_on_gui_input)
- mouse_entered.connect(_on_mouse_entered)
- mouse_exited.connect(_on_mouse_exited)
-
- update_display()
-
-
-func set_slot_data(slot_data: InventorySlot, index: int):
- inventory_data = slot_data
- slot_index = index
- update_display()
- if is_node_ready() and _is_mouse_hovering:
- _emit_current_hover_state()
-
-
-func update_display():
- if not inventory_data or inventory_data.is_empty():
- _show_empty_slot()
- else:
- _show_item_slot()
-
-
-func _show_empty_slot():
- if item_icon:
- item_icon.texture = null
- if quantity_label:
- quantity_label.visible = false
- if background:
- background.modulate = Color.WHITE
-
-
-func _show_item_slot():
- var item = ItemDatabase.get_item(inventory_data.item_id)
- if not item:
- _show_empty_slot()
- return
-
- item_icon.texture = item.icon
-
- if item.stackable and inventory_data.quantity > 1:
- quantity_label.text = str(inventory_data.quantity)
- quantity_label.visible = true
- else:
- quantity_label.visible = false
-
-
-func _on_gui_input(event: InputEvent):
- if event is InputEventMouseButton:
- if event.pressed:
- if event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
- slot_double_clicked.emit(slot_index)
- accept_event()
- elif event.button_index == MOUSE_BUTTON_RIGHT:
- slot_clicked.emit(slot_index, event.button_index)
- accept_event()
-
-
-func _on_mouse_entered():
- _is_mouse_hovering = true
- _emit_current_hover_state()
- background.modulate = Color(1.2, 1.2, 1.2)
-
-
-func _on_mouse_exited():
- _is_mouse_hovering = false
- item_unhovered.emit()
-
- background.modulate = Color.WHITE
-
-
-func _emit_current_hover_state() -> void:
- if inventory_data and not inventory_data.is_empty():
- var item := ItemDatabase.get_item(inventory_data.item_id)
- if item:
- item_hovered.emit(slot_index, item)
- return
- item_unhovered.emit()
-
-
-func _can_drop_data(_position: Vector2, data) -> bool:
- return data is Dictionary and data.has("slot_index") and data.has("inventory_type")
-
-
-func _drop_data(_position: Vector2, data):
- if parent_inventory and parent_inventory.has_method("handle_item_drop") and slot_type == TYPE.INVENTORY:
- parent_inventory.handle_item_drop(data.slot_index, slot_index)
- elif parent_inventory and parent_inventory.has_method("handle_weapon_equip") and slot_type == TYPE.WEAPON:
- parent_inventory.handle_weapon_equip(data.slot_index, data)
- elif parent_inventory and parent_inventory.has_method("handle_hat_equip") and slot_type == TYPE.HAT:
- parent_inventory.handle_hat_equip(data.slot_index, data)
- elif parent_inventory and parent_inventory.has_method("handle_backpack_equip") and slot_type == TYPE.BACKPACK:
- parent_inventory.handle_backpack_equip(data.slot_index, data)
-
-
-func _get_drag_data(_position: Vector2):
- if not inventory_data or inventory_data.is_empty():
- return null
-
- var item = ItemDatabase.get_item(inventory_data.item_id)
- if not item:
- return null
-
- var preview = Control.new()
- var preview_icon = TextureRect.new()
- preview_icon.texture = item.icon
- preview_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
- preview_icon.size = Vector2(32, 32)
- preview.add_child(preview_icon)
-
- preview.modulate = Color(1, 1, 1, 0.8)
- set_drag_preview(preview)
-
- item_icon.modulate = Color(0.5, 0.5, 0.5)
-
- return {"slot_index": slot_index, "inventory_type": item.item_type}
-
-
-func _notification(what):
- if what == NOTIFICATION_DRAG_END:
- item_icon.modulate = Color.WHITE
diff --git a/scripts/ui/inventory_slot_ui.gd.uid b/scripts/ui/inventory_slot_ui.gd.uid
deleted file mode 100644
index 3d96b9b..0000000
--- a/scripts/ui/inventory_slot_ui.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bi0mlil0a0e5i
diff --git a/scripts/ui/inventory_ui.gd b/scripts/ui/inventory_ui.gd
deleted file mode 100644
index 9fa527c..0000000
--- a/scripts/ui/inventory_ui.gd
+++ /dev/null
@@ -1,422 +0,0 @@
-class_name InventoryUI
-extends Control
-
-signal inventory_closed
-
-const SAFE_AREA_MARGIN := 16.0
-const TOOLTIP_MAX_WIDTH := 280.0
-const TOOLTIP_MAX_HEIGHT := 240.0
-const TOOLTIP_SCREEN_MARGIN := 10.0
-const TOOLTIP_HORIZONTAL_PADDING := 16.0
-const TOOLTIP_VERTICAL_PADDING := 16.0
-
-const SLOT_INDEX_WEAPON := -1
-const SLOT_INDEX_HAT := -2
-const SLOT_INDEX_BACKPACK := -3
-
-var current_player: Character
-var slot_ui_scene: PackedScene
-var slot_uis: Array[InventorySlotUI] = []
-var current_item: Item
-var current_slot_index: int
-var weapon_slot_ui: InventorySlotUI
-var hat_slot_ui: InventorySlotUI
-var backpack_slot_ui: InventorySlotUI
-var _tooltip_layout_request := 0
-var _tooltip_should_be_visible := false
-
-@onready var grid_container: GridContainer = get_node(
- "SafeArea/CenterContainer/InventoryPanels/InventoryPanel/MarginContainer/VBoxContainer/GridContainer"
-)
-@onready var close_button: Button = get_node(
- "SafeArea/CenterContainer/InventoryPanels/InventoryPanel/MarginContainer/VBoxContainer/TitleBar/CloseButton"
-)
-@onready var tooltip: Control = $ItemTooltip
-@onready var tooltip_label: RichTextLabel = $ItemTooltip/Panel/MarginContainer/TooltipText
-@onready var inventory_panels: HBoxContainer = $SafeArea/CenterContainer/InventoryPanels
-
-
-func _ready():
- slot_ui_scene = preload("res://scenes/ui/inventory_slot_ui.tscn")
- grid_container.columns = 4
- close_button.pressed.connect(_on_close_pressed)
- tooltip.visible = false
- _create_slot_uis()
- _create_hat_slot_ui()
- _create_weapon_slot_ui()
- _create_backpack_slot_ui()
- resized.connect(_update_responsive_layout)
- call_deferred("_update_responsive_layout")
-
-
-func _create_hat_slot_ui():
- hat_slot_ui = slot_ui_scene.instantiate() as InventorySlotUI
- hat_slot_ui.custom_minimum_size = Vector2(64, 64)
- hat_slot_ui.parent_inventory = self
- hat_slot_ui.slot_type = InventorySlotUI.TYPE.HAT
- hat_slot_ui.slot_clicked.connect(_on_slot_clicked)
- hat_slot_ui.item_hovered.connect(_on_item_hovered)
- hat_slot_ui.item_unhovered.connect(_on_item_unhovered)
- (
- get_node(
- "SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer/HatSlotContainer"
- )
- . add_child(hat_slot_ui)
- )
-
-
-func _create_weapon_slot_ui():
- weapon_slot_ui = slot_ui_scene.instantiate() as InventorySlotUI
- weapon_slot_ui.custom_minimum_size = Vector2(64, 64)
- weapon_slot_ui.parent_inventory = self
- weapon_slot_ui.slot_type = InventorySlotUI.TYPE.WEAPON
- weapon_slot_ui.slot_clicked.connect(_on_slot_clicked)
- weapon_slot_ui.item_hovered.connect(_on_item_hovered)
- weapon_slot_ui.item_unhovered.connect(_on_item_unhovered)
- (
- get_node(
- "SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer/WeaponSlotContainer"
- )
- . add_child(weapon_slot_ui)
- )
-
-
-func _create_backpack_slot_ui() -> void:
- backpack_slot_ui = slot_ui_scene.instantiate() as InventorySlotUI
- backpack_slot_ui.custom_minimum_size = Vector2(64, 64)
- backpack_slot_ui.parent_inventory = self
- backpack_slot_ui.slot_type = InventorySlotUI.TYPE.BACKPACK
- backpack_slot_ui.slot_clicked.connect(_on_slot_clicked)
- backpack_slot_ui.item_hovered.connect(_on_item_hovered)
- backpack_slot_ui.item_unhovered.connect(_on_item_unhovered)
- (
- get_node(
- "SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer/BackpackSlotContainer"
- )
- . add_child(backpack_slot_ui)
- )
-
-
-func _create_slot_uis():
- for child in grid_container.get_children():
- child.queue_free()
- slot_uis.clear()
-
- for i in range(PlayerInventory.MAX_INVENTORY_SIZE):
- var slot_ui = slot_ui_scene.instantiate() as InventorySlotUI
- slot_ui.custom_minimum_size = Vector2(64, 64)
- slot_ui.parent_inventory = self
- slot_ui.visible = i < PlayerInventory.BASE_INVENTORY_SIZE
-
- slot_ui.slot_clicked.connect(_on_slot_clicked)
- slot_ui.slot_double_clicked.connect(_on_slot_double_clicked)
- slot_ui.item_hovered.connect(_on_item_hovered)
- slot_ui.item_unhovered.connect(_on_item_unhovered)
-
- slot_ui.set_slot_data(null, i)
-
- grid_container.add_child(slot_ui)
- slot_uis.append(slot_ui)
-
-
-func update_inventory_display():
- if not current_player or not current_player.get_inventory():
- return
-
- var player_inventory = current_player.get_inventory()
- var active_slot_count := player_inventory.get_active_slot_count()
- for i in range(slot_uis.size()):
- slot_uis[i].visible = i < active_slot_count
- if i < active_slot_count:
- slot_uis[i].set_slot_data(player_inventory.get_slot(i), i)
- else:
- slot_uis[i].set_slot_data(null, i)
- weapon_slot_ui.set_slot_data(player_inventory.equipped_weapon, SLOT_INDEX_WEAPON)
- hat_slot_ui.set_slot_data(player_inventory.equipped_hat, SLOT_INDEX_HAT)
- backpack_slot_ui.set_slot_data(player_inventory.equipped_backpack, SLOT_INDEX_BACKPACK)
- call_deferred("_update_responsive_layout")
-
-
-func _on_slot_clicked(slot_index: int, button: int):
- if button == MOUSE_BUTTON_RIGHT:
- _handle_right_click(slot_index)
-
-
-func _on_slot_double_clicked(slot_index: int) -> void:
- if not current_player or not current_player.get_inventory():
- return
- if slot_index < 0 or not current_player.get_inventory().is_slot_active(slot_index):
- return
-
- var slot := current_player.get_inventory().get_slot(slot_index)
- if not slot or slot.is_empty():
- return
-
- var item := ItemDatabase.get_item(slot.item_id)
- if not item or not item.context_options.has(Item.ContextOptions.EQUIP):
- return
- if (
- item.item_type != Item.ItemType.WEAPON
- and item.item_type != Item.ItemType.HAT
- and item.item_type != Item.ItemType.BACKPACK
- ):
- return
-
- current_player.request_equip_item.rpc_id(1, slot_index, item.item_type)
-
-
-func _handle_right_click(slot_index: int):
- if not current_player or not current_player.get_inventory():
- return
-
- var player_inventory = current_player.get_inventory()
- current_slot_index = slot_index
- var slot: InventorySlot
- if slot_index == SLOT_INDEX_WEAPON:
- slot = player_inventory.equipped_weapon
- elif slot_index == SLOT_INDEX_HAT:
- slot = player_inventory.equipped_hat
- elif slot_index == SLOT_INDEX_BACKPACK:
- slot = player_inventory.equipped_backpack
- else:
- slot = player_inventory.get_slot(slot_index)
- if slot and not slot.is_empty():
- current_item = ItemDatabase.get_item(slot.item_id)
- if current_item:
- _hide_tooltip()
- var context_menu = PopupMenu.new()
- add_child(context_menu)
- context_menu.popup_hide.connect(context_menu.queue_free)
- context_menu.id_pressed.connect(_on_item_selected)
- if slot_index == SLOT_INDEX_WEAPON or slot_index == SLOT_INDEX_HAT or slot_index == SLOT_INDEX_BACKPACK:
- context_menu.add_item(
- _get_context_menu_string(Item.ContextOptions.UNEQUIP), Item.ContextOptions.UNEQUIP
- )
- else:
- for item_option in current_item.context_options:
- context_menu.add_item(_get_context_menu_string(item_option), item_option)
-
- context_menu.set_position(get_viewport().get_mouse_position())
- context_menu.popup()
-
-
-func _on_item_selected(index: int):
- if not current_player or not current_player.get_inventory():
- return
-
- if index == Item.ContextOptions.EQUIP:
- current_player.request_equip_item.rpc_id(1, current_slot_index, current_item.item_type)
- elif index == Item.ContextOptions.UNEQUIP:
- current_player.request_unequip_item.rpc_id(1, current_item.item_type)
- elif index == Item.ContextOptions.DROP:
- if current_item.scene_path.is_empty() or not ResourceLoader.exists(current_item.scene_path):
- push_warning(
- "Cannot drop item '" + current_item.id + "': invalid scene path '" + current_item.scene_path + "'"
- )
- return
- current_player.add_world_item.rpc_id(
- 1, current_item.scene_path, current_player.get_node("GodotRobot3D/InfrontArea3D").global_position
- )
- current_player.request_remove_item.rpc_id(1, current_item.id, 1)
- refresh_display()
-
-
-func _on_item_hovered(_slot_index: int, item: Item):
- _show_tooltip(item)
-
-
-func _on_item_unhovered():
- _hide_tooltip()
-
-
-func _show_tooltip(item: Item):
- if not item:
- return
-
- var tooltip_content = "[font_size=18][b][color=#FFD700]" + item.name + "[/color][/b][/font_size]\n"
- tooltip_content += "[color=#CCCCCC]" + item.description + "[/color]\n"
- tooltip_content += "[color=#87CEEB]Type:[/color] " + _get_item_type_string(item.item_type) + "\n"
- tooltip_content += "[color=#FF69B4]Rarity:[/color] " + _get_rarity_string(item.rarity) + "\n"
- tooltip_content += "[color=#FFD700]Value:[/color] " + str(item.value) + " gold"
-
- if item.stackable:
- tooltip_content += "\n[color=#98FB98]Max Stack:[/color] " + str(item.max_stack)
-
- tooltip_label.text = tooltip_content
- _tooltip_should_be_visible = true
- _queue_tooltip_layout()
-
-
-func _hide_tooltip():
- _tooltip_should_be_visible = false
- _tooltip_layout_request += 1
- tooltip.visible = false
- tooltip.modulate = Color.WHITE
-
-
-func _queue_tooltip_layout() -> void:
- _set_tooltip_width()
- _tooltip_layout_request += 1
- var request_id := _tooltip_layout_request
- tooltip.modulate = Color(1.0, 1.0, 1.0, 0.0)
- tooltip.visible = true
- _apply_tooltip_layout_after_frame(request_id)
-
-
-func _apply_tooltip_layout_after_frame(request_id: int) -> void:
- await get_tree().process_frame
- if request_id != _tooltip_layout_request or not _tooltip_should_be_visible or not tooltip or not tooltip_label:
- return
-
- var tooltip_size := tooltip.size
- var available_height := minf(TOOLTIP_MAX_HEIGHT, maxf(1.0, size.y - TOOLTIP_SCREEN_MARGIN * 2.0))
- tooltip_size.y = minf(float(tooltip_label.get_content_height()) + TOOLTIP_VERTICAL_PADDING, available_height)
- tooltip.size = tooltip_size
- _position_tooltip_smartly()
- tooltip.modulate = Color.WHITE
-
-
-func _set_tooltip_width() -> void:
- var tooltip_size := tooltip.size
- var available_width := maxf(1.0, size.x - TOOLTIP_SCREEN_MARGIN * 2.0)
- tooltip_size.x = minf(TOOLTIP_MAX_WIDTH, available_width)
- tooltip.size = tooltip_size
- var label_minimum_size := tooltip_label.custom_minimum_size
- label_minimum_size.x = maxf(1.0, tooltip_size.x - TOOLTIP_HORIZONTAL_PADDING)
- tooltip_label.custom_minimum_size = label_minimum_size
- var label_size := tooltip_label.size
- label_size.x = label_minimum_size.x
- tooltip_label.size = label_size
-
-
-func _position_tooltip_smartly():
- var mouse_pos = get_global_mouse_position()
- var tooltip_size = tooltip.size
-
- var viewport_size = get_viewport().get_visible_rect().size
- var tooltip_pos = mouse_pos + Vector2.ONE * TOOLTIP_SCREEN_MARGIN
- if tooltip_pos.x + tooltip_size.x > viewport_size.x - TOOLTIP_SCREEN_MARGIN:
- tooltip_pos.x = mouse_pos.x - tooltip_size.x - TOOLTIP_SCREEN_MARGIN
- if tooltip_pos.y + tooltip_size.y > viewport_size.y - TOOLTIP_SCREEN_MARGIN:
- tooltip_pos.y = mouse_pos.y - tooltip_size.y - TOOLTIP_SCREEN_MARGIN
-
- var max_x := maxf(TOOLTIP_SCREEN_MARGIN, viewport_size.x - tooltip_size.x - TOOLTIP_SCREEN_MARGIN)
- var max_y := maxf(TOOLTIP_SCREEN_MARGIN, viewport_size.y - tooltip_size.y - TOOLTIP_SCREEN_MARGIN)
- tooltip_pos.x = clampf(tooltip_pos.x, TOOLTIP_SCREEN_MARGIN, max_x)
- tooltip_pos.y = clampf(tooltip_pos.y, TOOLTIP_SCREEN_MARGIN, max_y)
-
- tooltip.global_position = tooltip_pos
-
-
-func _update_responsive_layout() -> void:
- if not inventory_panels or not tooltip:
- return
- var available_size := Vector2(
- maxf(1.0, size.x - SAFE_AREA_MARGIN * 2.0), maxf(1.0, size.y - SAFE_AREA_MARGIN * 2.0)
- )
- var content_size := inventory_panels.size
- if content_size.x > 0.0 and content_size.y > 0.0:
- var scale_factor := minf(1.0, minf(available_size.x / content_size.x, available_size.y / content_size.y))
- inventory_panels.pivot_offset = content_size * 0.5
- inventory_panels.scale = Vector2.ONE * scale_factor
-
- _set_tooltip_width()
- if _tooltip_should_be_visible:
- _queue_tooltip_layout()
-
-
-func _get_item_type_string(type: Item.ItemType) -> String:
- var type_name := "Unknown"
- match type:
- Item.ItemType.WEAPON:
- type_name = "Weapon"
- Item.ItemType.HAT:
- type_name = "Hat"
- Item.ItemType.BACKPACK:
- type_name = "Backpack"
- Item.ItemType.CONSUMABLE:
- type_name = "Consumable"
- Item.ItemType.TOOL:
- type_name = "Tool"
- Item.ItemType.MISC:
- type_name = "Miscellaneous"
- return type_name
-
-
-func _get_rarity_string(rarity: Item.ItemRarity) -> String:
- match rarity:
- Item.ItemRarity.COMMON:
- return "Common"
- Item.ItemRarity.UNCOMMON:
- return "Uncommon"
- Item.ItemRarity.RARE:
- return "Rare"
- Item.ItemRarity.EPIC:
- return "Epic"
- Item.ItemRarity.LEGENDARY:
- return "Legendary"
- _:
- return "Unknown"
-
-
-func _get_context_menu_string(context: Item.ContextOptions) -> String:
- match context:
- Item.ContextOptions.DROP:
- return "Drop"
- Item.ContextOptions.EQUIP:
- return "Equip"
- Item.ContextOptions.UNEQUIP:
- return "Unequip"
- _:
- return "Unknown"
-
-
-func handle_item_drop(from_slot: int, to_slot: int):
- if not current_player:
- return
- if from_slot == SLOT_INDEX_WEAPON:
- current_player.request_unequip_item.rpc_id(1, Item.ItemType.WEAPON, to_slot)
- elif from_slot == SLOT_INDEX_HAT:
- current_player.request_unequip_item.rpc_id(1, Item.ItemType.HAT, to_slot)
- elif from_slot == SLOT_INDEX_BACKPACK:
- current_player.request_unequip_item.rpc_id(1, Item.ItemType.BACKPACK, to_slot)
- else:
- current_player.request_move_item.rpc_id(1, from_slot, to_slot)
-
-
-func _on_close_pressed():
- _hide_tooltip()
- inventory_closed.emit()
- visible = false
-
-
-func open_inventory(player: Character = null):
- _hide_tooltip()
- if player:
- current_player = player
- update_inventory_display()
- visible = true
-
-
-func close_inventory():
- _hide_tooltip()
- visible = false
-
-
-func refresh_display():
- update_inventory_display()
-
-
-func handle_weapon_equip(from_slot: int, item: Dictionary):
- if current_player and from_slot >= 0 and item.inventory_type == Item.ItemType.WEAPON:
- current_player.request_equip_item.rpc_id(1, from_slot, Item.ItemType.WEAPON)
-
-
-func handle_hat_equip(from_slot: int, item: Dictionary):
- if current_player and from_slot >= 0 and item.inventory_type == Item.ItemType.HAT:
- current_player.request_equip_item.rpc_id(1, from_slot, Item.ItemType.HAT)
-
-
-func handle_backpack_equip(from_slot: int, item: Dictionary) -> void:
- if current_player and from_slot >= 0 and item.inventory_type == Item.ItemType.BACKPACK:
- current_player.request_equip_item.rpc_id(1, from_slot, Item.ItemType.BACKPACK)
diff --git a/scripts/ui/inventory_ui.gd.uid b/scripts/ui/inventory_ui.gd.uid
deleted file mode 100644
index 8d1b79f..0000000
--- a/scripts/ui/inventory_ui.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c2w12kgcvtww4
diff --git a/scripts/ui/main_menu_ui.gd b/scripts/ui/main_menu_ui.gd
deleted file mode 100644
index b617201..0000000
--- a/scripts/ui/main_menu_ui.gd
+++ /dev/null
@@ -1,71 +0,0 @@
-class_name MainMenuUI
-extends Control
-
-signal host_pressed(nickname: String, skin: String)
-signal join_pressed(nickname: String, skin: String, address: String)
-signal quit_pressed
-
-const SKIN_OPTIONS: Array[String] = ["Blue", "Yellow", "Green", "Red"]
-const SAFE_AREA_MARGIN := 24.0
-
-@onready var skin_input: OptionButton = $MainContainer/MainMenu/Option2/SkinInput
-@onready var nick_input: LineEdit = $MainContainer/MainMenu/Option1/NickInput
-@onready var address_input: LineEdit = $MainContainer/MainMenu/Option3/AddressInput
-@onready var main_container: VBoxContainer = $MainContainer
-
-
-func _ready() -> void:
- skin_input.clear()
- for skin_name in SKIN_OPTIONS:
- skin_input.add_item(skin_name)
- skin_input.select(0)
- resized.connect(_update_responsive_layout)
- call_deferred("_update_responsive_layout")
-
-
-func _on_host_pressed() -> void:
- var nickname = nick_input.text.strip_edges()
- var skin = get_skin()
- host_pressed.emit(nickname, skin)
-
-
-func _on_join_pressed() -> void:
- var nickname = nick_input.text.strip_edges()
- var skin = get_skin()
- var address = address_input.text.strip_edges()
- join_pressed.emit(nickname, skin, address)
-
-
-func _on_quit_pressed():
- quit_pressed.emit()
-
-
-func show_menu():
- show()
- call_deferred("_update_responsive_layout")
-
-
-func hide_menu():
- hide()
-
-
-func is_menu_visible() -> bool:
- return visible
-
-
-func _update_responsive_layout() -> void:
- if not main_container:
- return
- var available_size := Vector2(
- maxf(1.0, size.x - SAFE_AREA_MARGIN * 2.0), maxf(1.0, size.y - SAFE_AREA_MARGIN * 2.0)
- )
- var content_size := main_container.get_combined_minimum_size()
- if content_size.x <= 0.0 or content_size.y <= 0.0:
- return
- var scale_factor := minf(1.0, minf(available_size.x / content_size.x, available_size.y / content_size.y))
- main_container.pivot_offset = main_container.size * 0.5
- main_container.scale = Vector2.ONE * scale_factor
-
-
-func get_skin() -> String:
- return skin_input.get_item_text(skin_input.selected).to_lower()
diff --git a/scripts/ui/main_menu_ui.gd.uid b/scripts/ui/main_menu_ui.gd.uid
deleted file mode 100644
index 80cb1ff..0000000
--- a/scripts/ui/main_menu_ui.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c9x8y7z6a5b4c3d2
diff --git a/scripts/ui/multiplayer_chat_ui.gd b/scripts/ui/multiplayer_chat_ui.gd
deleted file mode 100644
index b6ac27d..0000000
--- a/scripts/ui/multiplayer_chat_ui.gd
+++ /dev/null
@@ -1,155 +0,0 @@
-class_name MultiplayerChatUI
-extends Control
-
-signal message_sent(message_text: String)
-
-const MAX_CHAT_MESSAGES: int = 100
-const MAX_VISIBLE_MESSAGES: int = 8
-const FEED_VISIBLE_SECONDS: float = 6.0
-const FEED_FADE_SECONDS: float = 0.35
-const SAFE_AREA_MARGIN := 16.0
-const MIN_CHAT_WIDTH := 280.0
-const MAX_CHAT_WIDTH := 480.0
-const MAX_CHAT_HEIGHT := 268.0
-
-var chat_visible: bool = false
-var chat_history: Array[String] = []
-var fade_tween: Tween
-
-@onready var chat: RichTextLabel = $ChatLayout/Chat
-@onready var message: LineEdit = $ChatLayout/Message
-@onready var feed_timer: Timer = $FeedTimer
-
-
-func _ready() -> void:
- message.text_submitted.connect(_on_message_submitted)
- feed_timer.timeout.connect(_on_feed_timer_timeout)
- feed_timer.wait_time = FEED_VISIBLE_SECONDS
- get_viewport().size_changed.connect(_update_responsive_layout)
- _update_responsive_layout()
- show()
- clear_chat()
-
-
-func _update_responsive_layout() -> void:
- var viewport_size := get_viewport().get_visible_rect().size
- var available_width := maxf(1.0, viewport_size.x - SAFE_AREA_MARGIN * 2.0)
- var preferred_width := clampf(viewport_size.x * 0.42, MIN_CHAT_WIDTH, MAX_CHAT_WIDTH)
- var target_width := minf(available_width, preferred_width)
- var target_height := minf(MAX_CHAT_HEIGHT, maxf(1.0, viewport_size.y - SAFE_AREA_MARGIN * 2.0))
-
- offset_left = SAFE_AREA_MARGIN
- offset_right = SAFE_AREA_MARGIN + target_width
- offset_bottom = -SAFE_AREA_MARGIN
- offset_top = -SAFE_AREA_MARGIN - target_height
-
-
-func toggle_chat() -> void:
- if chat_visible:
- close_chat()
- return
-
- chat_visible = true
- _stop_feed_animation()
- _show_feed()
- message.show()
- await get_tree().process_frame
- if not chat_visible:
- return
- message.grab_focus()
-
-
-func close_chat() -> void:
- chat_visible = false
- message.text = ""
- message.release_focus()
- message.hide()
-
- if chat_history.is_empty():
- chat.hide()
- else:
- _show_feed_temporarily()
-
-
-func is_chat_visible() -> bool:
- return chat_visible
-
-
-func _on_message_submitted(_submitted_text: String) -> void:
- var message_text := message.text.strip_edges()
- if message_text.is_empty():
- return
-
- close_chat()
- message_sent.emit(message_text)
-
-
-func add_message(nick: String, msg: String) -> void:
- var formatted_message := _escape_bbcode("%s: %s" % [nick, msg])
- chat_history.append(formatted_message)
- while chat_history.size() > MAX_CHAT_MESSAGES:
- chat_history.pop_front()
-
- _render_chat_history()
- if chat_visible:
- _stop_feed_animation()
- _show_feed()
- else:
- _show_feed_temporarily()
-
-
-func _render_chat_history() -> void:
- chat.clear()
- var first_message := maxi(0, chat_history.size() - MAX_VISIBLE_MESSAGES)
- for index in range(first_message, chat_history.size()):
- chat.append_text(chat_history[index])
- if index < chat_history.size() - 1:
- chat.append_text("\n")
-
-
-func _show_feed_temporarily() -> void:
- _stop_feed_animation()
- _show_feed()
- feed_timer.start()
-
-
-func _show_feed() -> void:
- chat.modulate.a = 1.0
- chat.show()
-
-
-func _on_feed_timer_timeout() -> void:
- if chat_visible:
- return
-
- _stop_fade_tween()
- fade_tween = create_tween()
- fade_tween.tween_property(chat, "modulate:a", 0.0, FEED_FADE_SECONDS)
- fade_tween.tween_callback(chat.hide)
-
-
-func _stop_feed_animation() -> void:
- feed_timer.stop()
- _stop_fade_tween()
-
-
-func _stop_fade_tween() -> void:
- if fade_tween and fade_tween.is_valid():
- fade_tween.kill()
- fade_tween = null
-
-
-func _escape_bbcode(text: String) -> String:
- return text.replace("[", "[lb]")
-
-
-func clear_chat() -> void:
- _stop_feed_animation()
- chat_history.clear()
- chat.clear()
- chat.modulate.a = 1.0
- chat.hide()
- message.text = ""
- message.release_focus()
- message.hide()
- chat_visible = false
diff --git a/scripts/ui/multiplayer_chat_ui.gd.uid b/scripts/ui/multiplayer_chat_ui.gd.uid
deleted file mode 100644
index 9e54909..0000000
--- a/scripts/ui/multiplayer_chat_ui.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c8x9y2z1a3b4c5d6
diff --git a/scripts/ui/pause_menu_ui.gd b/scripts/ui/pause_menu_ui.gd
deleted file mode 100644
index 66a4baa..0000000
--- a/scripts/ui/pause_menu_ui.gd
+++ /dev/null
@@ -1,57 +0,0 @@
-class_name PauseMenuUI
-extends Control
-
-signal resume_pressed
-signal main_menu_pressed
-signal quit_pressed
-
-const SAFE_AREA_MARGIN := 16.0
-
-@onready var resume_button: Button = $Overlay/Panel/MarginContainer/Buttons/Resume
-@onready var panel: PanelContainer = $Overlay/Panel
-
-
-func _ready() -> void:
- resized.connect(_update_responsive_layout)
- call_deferred("_update_responsive_layout")
- hide()
-
-
-func show_menu() -> void:
- show()
- call_deferred("_update_responsive_layout")
- resume_button.grab_focus()
-
-
-func hide_menu() -> void:
- hide()
-
-
-func is_menu_visible() -> bool:
- return visible
-
-
-func _update_responsive_layout() -> void:
- if not panel:
- return
- var available_size := Vector2(
- maxf(1.0, size.x - SAFE_AREA_MARGIN * 2.0), maxf(1.0, size.y - SAFE_AREA_MARGIN * 2.0)
- )
- var panel_size := panel.size
- if panel_size.x <= 0.0 or panel_size.y <= 0.0:
- return
- var scale_factor := minf(1.0, minf(available_size.x / panel_size.x, available_size.y / panel_size.y))
- panel.pivot_offset = panel_size * 0.5
- panel.scale = Vector2.ONE * scale_factor
-
-
-func _on_resume_pressed() -> void:
- resume_pressed.emit()
-
-
-func _on_main_menu_pressed() -> void:
- main_menu_pressed.emit()
-
-
-func _on_quit_pressed() -> void:
- quit_pressed.emit()
diff --git a/scripts/ui/pause_menu_ui.gd.uid b/scripts/ui/pause_menu_ui.gd.uid
deleted file mode 100644
index f66db2d..0000000
--- a/scripts/ui/pause_menu_ui.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://wbiqr2q6c47q
diff --git a/scripts/ui/player_list_ui.gd b/scripts/ui/player_list_ui.gd
deleted file mode 100644
index 2b392ce..0000000
--- a/scripts/ui/player_list_ui.gd
+++ /dev/null
@@ -1,109 +0,0 @@
-class_name PlayerListUI
-extends Control
-
-const PLAYER_ROW_HEIGHT := 28.0
-const MAX_VISIBLE_ROWS := 8
-const RESERVED_VERTICAL_SPACE := 190.0
-const SAFE_AREA_MARGIN := 16.0
-
-var _player_count := 0
-
-@onready var panel: PanelContainer = $SafeArea/PositionContainer/Panel
-@onready var count_label: Label = $SafeArea/PositionContainer/Panel/MarginContainer/Content/Count
-@onready var player_scroll: ScrollContainer = $SafeArea/PositionContainer/Panel/MarginContainer/Content/PlayerScroll
-@onready
-var players_label: RichTextLabel = $SafeArea/PositionContainer/Panel/MarginContainer/Content/PlayerScroll/Players
-
-
-func _ready() -> void:
- resized.connect(_update_layout)
- _update_layout()
- hide()
-
-
-func _unhandled_input(event: InputEvent) -> void:
- if not visible or not (event is InputEventMouseButton):
- return
- var mouse_event := event as InputEventMouseButton
- if not mouse_event.pressed:
- return
- if mouse_event.button_index == MOUSE_BUTTON_WHEEL_UP:
- player_scroll.scroll_vertical -= int(PLAYER_ROW_HEIGHT)
- elif mouse_event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
- player_scroll.scroll_vertical += int(PLAYER_ROW_HEIGHT)
-
-
-func show_players(players: Dictionary, local_peer_id: int) -> void:
- refresh_players(players, local_peer_id)
- player_scroll.scroll_vertical = 0
- show()
- call_deferred("_apply_panel_scale")
-
-
-func hide_players() -> void:
- hide()
-
-
-func refresh_players(players: Dictionary, local_peer_id: int) -> void:
- var peer_ids: Array[int] = []
- for peer_id_value in players:
- var peer_id := int(peer_id_value)
- if peer_id > 0:
- peer_ids.append(peer_id)
- peer_ids.sort()
- _player_count = peer_ids.size()
-
- count_label.text = "%d online" % _player_count
- players_label.clear()
- for index in range(peer_ids.size()):
- var peer_id := peer_ids[index]
- var player_info: Dictionary = players.get(peer_id, {})
- var nickname := Network.sanitize_nickname(str(player_info.get("nick", "")), "Player_" + str(peer_id))
- var local_marker := " [color=#c7ccd4](you)[/color]" if peer_id == local_peer_id else ""
- players_label.append_text(
- (
- "[color=#a8adb5]>[/color] [b]%s[/b]%s [color=#9aa0a8]#%d[/color]"
- % [_escape_bbcode(nickname), local_marker, peer_id]
- )
- )
- if index < peer_ids.size() - 1:
- players_label.append_text("\n")
- _update_layout()
-
-
-func _update_layout() -> void:
- if not panel or not player_scroll:
- return
- var panel_minimum_size := panel.custom_minimum_size
- panel_minimum_size.x = maxf(0.0, minf(420.0, size.x - 32.0))
- panel.custom_minimum_size = panel_minimum_size
-
- var visible_rows := maxi(1, mini(_player_count, MAX_VISIBLE_ROWS))
- var desired_height := float(visible_rows) * PLAYER_ROW_HEIGHT
- var viewport_limit := maxf(30.0, size.y - RESERVED_VERTICAL_SPACE)
- var scroll_minimum_size := player_scroll.custom_minimum_size
- scroll_minimum_size.y = minf(desired_height, viewport_limit)
- player_scroll.custom_minimum_size = scroll_minimum_size
-
- var players_minimum_size := players_label.custom_minimum_size
- players_minimum_size.y = float(maxi(1, _player_count)) * PLAYER_ROW_HEIGHT
- players_label.custom_minimum_size = players_minimum_size
- call_deferred("_apply_panel_scale")
-
-
-func _apply_panel_scale() -> void:
- if not panel:
- return
- var available_size := Vector2(
- maxf(1.0, size.x - SAFE_AREA_MARGIN * 2.0), maxf(1.0, size.y - SAFE_AREA_MARGIN * 2.0)
- )
- var panel_size := panel.size
- if panel_size.x <= 0.0 or panel_size.y <= 0.0:
- return
- var scale_factor := minf(1.0, minf(available_size.x / panel_size.x, available_size.y / panel_size.y))
- panel.pivot_offset = panel_size * 0.5
- panel.scale = Vector2.ONE * scale_factor
-
-
-func _escape_bbcode(text: String) -> String:
- return text.replace("[", "[lb]")
diff --git a/scripts/ui/player_list_ui.gd.uid b/scripts/ui/player_list_ui.gd.uid
deleted file mode 100644
index 65adc49..0000000
--- a/scripts/ui/player_list_ui.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dpxhd38sedg8u