@beremaran/godot-agent-loop 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +925 -0
  3. package/addons/godot_agent_loop/LICENSE +23 -0
  4. package/addons/godot_agent_loop/README.md +31 -0
  5. package/addons/godot_agent_loop/plugin.cfg +8 -0
  6. package/addons/godot_agent_loop/plugin.gd +417 -0
  7. package/agent-plugin/.claude-plugin/plugin.json +19 -0
  8. package/agent-plugin/.codex-plugin/plugin.json +37 -0
  9. package/agent-plugin/.mcp.json +14 -0
  10. package/agent-plugin/adapter-manifest.json +45 -0
  11. package/agent-plugin/pi/extension.ts +136 -0
  12. package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
  13. package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
  14. package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
  15. package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
  16. package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
  17. package/build/authoring-session-manager.js +237 -0
  18. package/build/domain-tool-registries.js +207 -0
  19. package/build/editor-bridge-protocol.js +1 -0
  20. package/build/editor-connection.js +112 -0
  21. package/build/editor-mutation-guard.js +30 -0
  22. package/build/editor-plugin-installer.js +220 -0
  23. package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
  24. package/build/game-command-service.js +49 -0
  25. package/build/game-connection.js +337 -0
  26. package/build/godot-executable.js +156 -0
  27. package/build/godot-process-manager.js +112 -0
  28. package/build/godot-subprocess.js +23 -0
  29. package/build/headless-operation-runner.js +68 -0
  30. package/build/headless-operation-service.js +65 -0
  31. package/build/index.js +478 -0
  32. package/build/interaction-server-installer.js +234 -0
  33. package/build/opencode-setup.js +199 -0
  34. package/build/project-support.js +219 -0
  35. package/build/runtime-protocol.js +202 -0
  36. package/build/scripts/godot_operations.gd +1534 -0
  37. package/build/scripts/mcp_editor_plugin.gd +417 -0
  38. package/build/scripts/mcp_interaction_server.gd +1255 -0
  39. package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
  40. package/build/scripts/mcp_runtime/command_params.gd +339 -0
  41. package/build/scripts/mcp_runtime/core_domain.gd +591 -0
  42. package/build/scripts/mcp_runtime/input_domain.gd +695 -0
  43. package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
  44. package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
  45. package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
  46. package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
  47. package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
  48. package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
  49. package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
  50. package/build/scripts/mcp_runtime/system_domain.gd +277 -0
  51. package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
  52. package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
  53. package/build/scripts/validate_script.gd +28 -0
  54. package/build/server-instructions.js +11 -0
  55. package/build/session-timing.js +20 -0
  56. package/build/tool-argument-validation.js +120 -0
  57. package/build/tool-definitions.js +2727 -0
  58. package/build/tool-handlers/game-tool-handlers.js +1345 -0
  59. package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
  60. package/build/tool-handlers/project-handler-services.js +1458 -0
  61. package/build/tool-handlers/project-tool-handlers.js +1506 -0
  62. package/build/tool-handlers/visual-regression-service.js +139 -0
  63. package/build/tool-manifest.js +1193 -0
  64. package/build/tool-mutation-policy.js +122 -0
  65. package/build/tool-registry.js +34 -0
  66. package/build/tool-surface.js +70 -0
  67. package/build/utils.js +273 -0
  68. package/package.json +110 -0
  69. package/product.json +52 -0
  70. package/scripts/prepare-package.js +42 -0
@@ -0,0 +1,277 @@
1
+ extends "res://mcp_runtime/runtime_domain.gd"
2
+
3
+ # Window, engine, localization, and resource/project-state commands.
4
+
5
+ const MAX_RESOURCE_PATH_BYTES: int = 4096
6
+ const MAX_THREADED_LOAD_FRAMES: int = 600
7
+
8
+ var _preloaded_resources: Dictionary = {}
9
+
10
+ func register_commands() -> void:
11
+ register_command("window", _cmd_window)
12
+ register_command("os_info", _cmd_os_info)
13
+ register_command("time_scale", _cmd_time_scale)
14
+ register_command("process_mode", _cmd_process_mode)
15
+ register_command("world_settings", _cmd_world_settings)
16
+ register_command("locale", _cmd_locale)
17
+ register_command("resource", _cmd_resource)
18
+
19
+
20
+ func _cmd_window(params: Dictionary) -> void:
21
+ var reader := CommandParams.new(params)
22
+ var action: String = reader.optional_enum("action", "get", ["get", "set"])
23
+ var width: int = reader.optional_int("width", -1, 1)
24
+ var height: int = reader.optional_int("height", -1, 1)
25
+ var fullscreen: bool = reader.optional_bool("fullscreen", false)
26
+ var borderless: bool = reader.optional_bool("borderless", false)
27
+ var title: String = reader.optional_string("title")
28
+ var position: Dictionary = reader.optional_dictionary("position")
29
+ var vsync: bool = reader.optional_bool("vsync", false)
30
+ if params_invalid(reader):
31
+ return
32
+ var win: Window = get_tree().root
33
+ if action == "get":
34
+ respond({"success": true, "size": {"x": win.size.x, "y": win.size.y}, "position": {"x": win.position.x, "y": win.position.y}, "fullscreen": win.mode == Window.MODE_FULLSCREEN, "borderless": win.borderless, "title": win.title})
35
+ return
36
+ if DisplayServer.get_name() == "headless" and (reader.has_param("borderless") or reader.has_param("vsync")):
37
+ respond_limit(
38
+ "borderless and vsync changes are unavailable with Godot's headless display driver",
39
+ {"reason": "display_feature_unavailable", "display_driver": DisplayServer.get_name()},
40
+ )
41
+ return
42
+ if reader.has_param("width") != reader.has_param("height"):
43
+ reader.fail("width and height must be provided together", {"param": "width", "reason": "missing_pair", "paired_with": "height"})
44
+ send_params_error(reader)
45
+ return
46
+ if reader.has_param("width"):
47
+ win.size = Vector2i(width, height)
48
+ if reader.has_param("fullscreen"):
49
+ win.mode = Window.MODE_FULLSCREEN if fullscreen else Window.MODE_WINDOWED
50
+ if reader.has_param("borderless"):
51
+ win.borderless = borderless
52
+ if reader.has_param("title"):
53
+ win.title = title
54
+ if reader.has_param("position"):
55
+ win.position = Vector2i(CommandParams.json_int(position, "x"), CommandParams.json_int(position, "y"))
56
+ if reader.has_param("vsync"):
57
+ DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED if vsync else DisplayServer.VSYNC_DISABLED)
58
+ respond({"success": true, "action": action, "size": {"x": win.size.x, "y": win.size.y}})
59
+
60
+
61
+ func _cmd_os_info(_params: Dictionary) -> void:
62
+ var screen_size: Vector2i = DisplayServer.screen_get_size()
63
+ respond({"success": true, "os_name": OS.get_name(), "locale": OS.get_locale(), "screen_size": {"x": screen_size.x, "y": screen_size.y}, "video_adapter": RenderingServer.get_video_adapter_name(), "rendering_method": RenderingServer.get_current_rendering_method(), "processor_count": OS.get_processor_count()})
64
+
65
+
66
+ func _cmd_time_scale(params: Dictionary) -> void:
67
+ var reader := CommandParams.new(params)
68
+ var action: String = reader.optional_enum("action", "get", ["get", "set"])
69
+ var time_scale: float = reader.optional_number("time_scale", 1.0, 0.0)
70
+ if action == "set" and not reader.has_param("time_scale"):
71
+ reader.fail("time_scale is required for set", {"param": "time_scale", "reason": "missing"})
72
+ if params_invalid(reader):
73
+ return
74
+ if action == "set":
75
+ Engine.time_scale = time_scale
76
+ respond({
77
+ "success": true,
78
+ "time_scale": Engine.time_scale,
79
+ "fixed_fps": _configured_fixed_fps(),
80
+ "ticks_msec": Time.get_ticks_msec(),
81
+ "fps": Engine.get_frames_per_second(),
82
+ })
83
+
84
+
85
+ func _configured_fixed_fps() -> int:
86
+ var configured: String = OS.get_environment("GODOT_MCP_FIXED_FPS")
87
+ if configured.is_valid_int():
88
+ return int(configured)
89
+ return 0
90
+
91
+
92
+ func _cmd_process_mode(params: Dictionary) -> void:
93
+ var reader := CommandParams.new(params)
94
+ var node: Node = require_node(reader)
95
+ var mode: String = reader.optional_enum("mode", "inherit", ["inherit", "pausable", "when_paused", "always", "disabled"])
96
+ if params_invalid(reader):
97
+ return
98
+ var modes: Dictionary = {
99
+ "inherit": Node.PROCESS_MODE_INHERIT,
100
+ "pausable": Node.PROCESS_MODE_PAUSABLE,
101
+ "when_paused": Node.PROCESS_MODE_WHEN_PAUSED,
102
+ "always": Node.PROCESS_MODE_ALWAYS,
103
+ "disabled": Node.PROCESS_MODE_DISABLED,
104
+ }
105
+ node.process_mode = modes[mode]
106
+ respond({"success": true, "node_path": str(node.get_path()), "mode": mode})
107
+
108
+
109
+ func _cmd_world_settings(params: Dictionary) -> void:
110
+ var reader := CommandParams.new(params)
111
+ var action: String = reader.optional_enum("action", "get", ["get", "set"])
112
+ var gravity: float = reader.optional_number("gravity", 9.8, 0.0)
113
+ var gravity_direction: Dictionary = reader.optional_dictionary("gravity_direction")
114
+ var physics_fps: int = reader.optional_int("physics_fps", 60, 1)
115
+ if params_invalid(reader):
116
+ return
117
+
118
+ # ProjectSettings alone only seeds *new* worlds; the running space keeps the
119
+ # gravity it was created with. Both are written so the change is observable
120
+ # in the live simulation and still survives into any later-created world.
121
+ var space: RID = get_viewport().find_world_3d().space
122
+ if action == "set":
123
+ if reader.has_param("gravity"):
124
+ ProjectSettings.set_setting("physics/3d/default_gravity", gravity)
125
+ PhysicsServer3D.area_set_param(space, PhysicsServer3D.AREA_PARAM_GRAVITY, gravity)
126
+ if reader.has_param("gravity_direction"):
127
+ var direction := Vector3(
128
+ CommandParams.json_float(gravity_direction, "x"),
129
+ CommandParams.json_float(gravity_direction, "y"),
130
+ CommandParams.json_float(gravity_direction, "z"),
131
+ )
132
+ ProjectSettings.set_setting("physics/3d/default_gravity_vector", direction)
133
+ PhysicsServer3D.area_set_param(space, PhysicsServer3D.AREA_PARAM_GRAVITY_VECTOR, direction)
134
+ if reader.has_param("physics_fps"):
135
+ Engine.physics_ticks_per_second = physics_fps
136
+
137
+ var vector: Vector3 = ProjectSettings.get_setting("physics/3d/default_gravity_vector", Vector3(0, -1, 0))
138
+ respond({
139
+ "success": true,
140
+ "gravity": ProjectSettings.get_setting("physics/3d/default_gravity"),
141
+ "gravity_direction": {"x": vector.x, "y": vector.y, "z": vector.z},
142
+ "physics_fps": Engine.physics_ticks_per_second,
143
+ })
144
+
145
+
146
+ func _cmd_locale(params: Dictionary) -> void:
147
+ var reader := CommandParams.new(params)
148
+ var action: String = reader.optional_enum("action", "get", ["get", "set", "translate"])
149
+ var locale_name: String = reader.optional_string("locale", "en")
150
+ var key: String = reader.optional_string("key")
151
+ if action == "translate" and not reader.has_param("key"):
152
+ reader.fail("key is required for translate", {"param": "key", "reason": "missing"})
153
+ if params_invalid(reader):
154
+ return
155
+ match action:
156
+ "get":
157
+ respond({"success": true, "locale": TranslationServer.get_locale()})
158
+ "set":
159
+ TranslationServer.set_locale(locale_name)
160
+ respond({"success": true, "action": action, "locale": locale_name})
161
+ "translate":
162
+ respond({"success": true, "key": key, "translated": tr(key)})
163
+
164
+
165
+ func _cmd_resource(params: Dictionary) -> void:
166
+ var reader := CommandParams.new(params)
167
+ var action: String = reader.optional_enum("action", "load", ["load", "preload", "save", "exists"])
168
+ var resource_path: String = reader.required_resource_path("path")
169
+ _validate_project_resource_path(reader, resource_path)
170
+ if params_invalid(reader):
171
+ return
172
+ match action:
173
+ "load":
174
+ if not _require_existing_resource(reader, resource_path):
175
+ return
176
+ var cached_before: bool = ResourceLoader.has_cached(resource_path)
177
+ var resource: Resource = ResourceLoader.load(resource_path)
178
+ if resource == null:
179
+ respond({"error": "Failed to load resource: %s" % resource_path})
180
+ return
181
+ respond(_resource_result(action, resource_path, resource, cached_before))
182
+ "preload":
183
+ if not _require_existing_resource(reader, resource_path):
184
+ return
185
+ var cached_before: bool = ResourceLoader.has_cached(resource_path)
186
+ var request_error: int = ResourceLoader.load_threaded_request(
187
+ resource_path, "", true, ResourceLoader.CACHE_MODE_REUSE
188
+ )
189
+ if request_error != OK:
190
+ reader.fail("Failed to start threaded resource load", godot_error_data(request_error))
191
+ send_params_error(reader)
192
+ return
193
+ var status: int = ResourceLoader.load_threaded_get_status(resource_path)
194
+ var waited_frames: int = 0
195
+ while status == ResourceLoader.THREAD_LOAD_IN_PROGRESS and waited_frames < MAX_THREADED_LOAD_FRAMES:
196
+ if cancellation_requested():
197
+ respond({"error": "Resource preload cancelled", "error_data": {"path": resource_path}})
198
+ return
199
+ await get_tree().process_frame
200
+ waited_frames += 1
201
+ status = ResourceLoader.load_threaded_get_status(resource_path)
202
+ if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
203
+ respond_timeout("Resource preload timed out", {"path": resource_path})
204
+ return
205
+ if status != ResourceLoader.THREAD_LOAD_LOADED:
206
+ respond({"error": "Failed to preload resource: %s" % resource_path, "error_data": {"path": resource_path, "status": status}})
207
+ return
208
+ var resource: Resource = ResourceLoader.load_threaded_get(resource_path)
209
+ if resource == null:
210
+ respond({"error": "Failed to preload resource: %s" % resource_path})
211
+ return
212
+ _preloaded_resources[resource_path] = resource
213
+ var result: Dictionary = _resource_result(action, resource_path, resource, cached_before)
214
+ result["waited_frames"] = waited_frames
215
+ respond(result)
216
+ "save":
217
+ var node: Node = require_node(reader)
218
+ var property: String = reader.required_string("property")
219
+ if params_invalid(reader):
220
+ return
221
+ if not _object_has_property(node, property):
222
+ reader.fail("Property not found", {"param": "property", "reason": "property_not_found", "value": property})
223
+ send_params_error(reader)
224
+ return
225
+ var property_value: Variant = node.get(property)
226
+ if not property_value is Resource:
227
+ reader.fail("Property is not a Resource", {"param": "property", "reason": "invalid_value", "value": property})
228
+ send_params_error(reader)
229
+ return
230
+ var resource: Resource = property_value
231
+ var err: int = ResourceSaver.save(resource, resource_path)
232
+ if err != OK:
233
+ reader.fail("Failed to save resource", godot_error_data(err))
234
+ send_params_error(reader)
235
+ return
236
+ respond(_resource_result(action, resource_path, resource, ResourceLoader.has_cached(resource_path)))
237
+ "exists":
238
+ respond({"success": true, "action": action, "path": resource_path, "exists": ResourceLoader.exists(resource_path)})
239
+
240
+
241
+ func _validate_project_resource_path(reader: CommandParams, resource_path: String) -> void:
242
+ if reader.failed():
243
+ return
244
+ var relative_path: String = resource_path.trim_prefix("res://")
245
+ var segments: PackedStringArray = relative_path.replace("\\", "/").split("/", false)
246
+ if not resource_path.begins_with("res://") or relative_path.is_empty() or segments.has(".."):
247
+ reader.fail("path must stay within the project", {"param": "path", "reason": "path_outside_project", "value": resource_path})
248
+ return
249
+ if resource_path.to_utf8_buffer().size() > MAX_RESOURCE_PATH_BYTES:
250
+ reader.fail("path exceeds the resource path limit", {"param": "path", "reason": "limit_exceeded", "max_bytes": MAX_RESOURCE_PATH_BYTES})
251
+
252
+
253
+ func _require_existing_resource(reader: CommandParams, resource_path: String) -> bool:
254
+ if ResourceLoader.exists(resource_path):
255
+ return true
256
+ reader.fail("Resource not found", {"param": "path", "reason": "resource_not_found", "value": resource_path})
257
+ send_params_error(reader)
258
+ return false
259
+
260
+
261
+ func _object_has_property(object: Object, property: String) -> bool:
262
+ for property_info: Dictionary in object.get_property_list():
263
+ if str(property_info.get("name", "")) == property:
264
+ return true
265
+ return false
266
+
267
+
268
+ func _resource_result(action: String, resource_path: String, resource: Resource, cached_before: bool) -> Dictionary:
269
+ return {
270
+ "success": true,
271
+ "action": action,
272
+ "path": resource_path,
273
+ "type": resource.get_class(),
274
+ "resource_name": resource.resource_name,
275
+ "cached_before": cached_before,
276
+ "cached_after": ResourceLoader.has_cached(resource_path),
277
+ }