@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.
- package/LICENSE +23 -0
- package/README.md +925 -0
- package/addons/godot_agent_loop/LICENSE +23 -0
- package/addons/godot_agent_loop/README.md +31 -0
- package/addons/godot_agent_loop/plugin.cfg +8 -0
- package/addons/godot_agent_loop/plugin.gd +417 -0
- package/agent-plugin/.claude-plugin/plugin.json +19 -0
- package/agent-plugin/.codex-plugin/plugin.json +37 -0
- package/agent-plugin/.mcp.json +14 -0
- package/agent-plugin/adapter-manifest.json +45 -0
- package/agent-plugin/pi/extension.ts +136 -0
- package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
- package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
- package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
- package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
- package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
- package/build/authoring-session-manager.js +237 -0
- package/build/domain-tool-registries.js +207 -0
- package/build/editor-bridge-protocol.js +1 -0
- package/build/editor-connection.js +112 -0
- package/build/editor-mutation-guard.js +30 -0
- package/build/editor-plugin-installer.js +220 -0
- package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
- package/build/game-command-service.js +49 -0
- package/build/game-connection.js +337 -0
- package/build/godot-executable.js +156 -0
- package/build/godot-process-manager.js +112 -0
- package/build/godot-subprocess.js +23 -0
- package/build/headless-operation-runner.js +68 -0
- package/build/headless-operation-service.js +65 -0
- package/build/index.js +478 -0
- package/build/interaction-server-installer.js +234 -0
- package/build/opencode-setup.js +199 -0
- package/build/project-support.js +219 -0
- package/build/runtime-protocol.js +202 -0
- package/build/scripts/godot_operations.gd +1534 -0
- package/build/scripts/mcp_editor_plugin.gd +417 -0
- package/build/scripts/mcp_interaction_server.gd +1255 -0
- package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
- package/build/scripts/mcp_runtime/command_params.gd +339 -0
- package/build/scripts/mcp_runtime/core_domain.gd +591 -0
- package/build/scripts/mcp_runtime/input_domain.gd +695 -0
- package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
- package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
- package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
- package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
- package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
- package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
- package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
- package/build/scripts/mcp_runtime/system_domain.gd +277 -0
- package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
- package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
- package/build/scripts/validate_script.gd +28 -0
- package/build/server-instructions.js +11 -0
- package/build/session-timing.js +20 -0
- package/build/tool-argument-validation.js +120 -0
- package/build/tool-definitions.js +2727 -0
- package/build/tool-handlers/game-tool-handlers.js +1345 -0
- package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
- package/build/tool-handlers/project-handler-services.js +1458 -0
- package/build/tool-handlers/project-tool-handlers.js +1506 -0
- package/build/tool-handlers/visual-regression-service.js +139 -0
- package/build/tool-manifest.js +1193 -0
- package/build/tool-mutation-policy.js +122 -0
- package/build/tool-registry.js +34 -0
- package/build/tool-surface.js +70 -0
- package/build/utils.js +273 -0
- package/package.json +110 -0
- package/product.json +52 -0
- 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
|
+
}
|