@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,346 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { execFile } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
import { createErrorResponse, errorMessage, normalizeParameters, validatePath } from '../utils.js';
|
|
8
|
+
import { GODOT_VERSION_OPTIONS } from '../godot-subprocess.js';
|
|
9
|
+
import { deterministicSessionArguments, deterministicSessionEnvironment } from '../session-timing.js';
|
|
10
|
+
import { describeCatalogTool, searchToolCatalog } from '../tool-surface.js';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
/** Implements editor launch, project runtime, and Godot version tools. */
|
|
13
|
+
export class LifecycleToolHandlers {
|
|
14
|
+
context;
|
|
15
|
+
processGeneration = 0;
|
|
16
|
+
constructor(context) {
|
|
17
|
+
this.context = context;
|
|
18
|
+
}
|
|
19
|
+
async handleGodotTools(args) {
|
|
20
|
+
args = normalizeParameters(args || {});
|
|
21
|
+
if (args.action === 'search') {
|
|
22
|
+
const results = searchToolCatalog(typeof args.query === 'string' ? args.query : '', typeof args.domain === 'string' ? args.domain : undefined, typeof args.limit === 'number' ? args.limit : 20);
|
|
23
|
+
return { content: [{ type: 'text', text: JSON.stringify({ results, count: results.length }, null, 2) }] };
|
|
24
|
+
}
|
|
25
|
+
if (args.action === 'describe') {
|
|
26
|
+
if (typeof args.toolName !== 'string')
|
|
27
|
+
return createErrorResponse('toolName is required for godot_tools describe.');
|
|
28
|
+
const tool = describeCatalogTool(args.toolName);
|
|
29
|
+
if (!tool)
|
|
30
|
+
return createErrorResponse(`Unknown Godot tool: ${args.toolName}`);
|
|
31
|
+
return { content: [{ type: 'text', text: JSON.stringify(tool, null, 2) }] };
|
|
32
|
+
}
|
|
33
|
+
if (args.action === 'call') {
|
|
34
|
+
if (typeof args.toolName !== 'string')
|
|
35
|
+
return createErrorResponse('toolName is required for godot_tools call.');
|
|
36
|
+
if (args.toolName === 'godot_tools')
|
|
37
|
+
return createErrorResponse('godot_tools cannot call itself.');
|
|
38
|
+
if (!this.context.dispatchTool)
|
|
39
|
+
return createErrorResponse('Expanded tool dispatch is unavailable.');
|
|
40
|
+
try {
|
|
41
|
+
return await this.context.dispatchTool(args.toolName, args.arguments ?? {});
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return createErrorResponse(`godot_tools call failed: ${errorMessage(error)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return createErrorResponse('action must be search, describe, or call.');
|
|
48
|
+
}
|
|
49
|
+
async handleLaunchEditor(args) {
|
|
50
|
+
args = normalizeParameters(args);
|
|
51
|
+
if (!args.projectPath)
|
|
52
|
+
return createErrorResponse('Project path is required');
|
|
53
|
+
if (!validatePath(args.projectPath))
|
|
54
|
+
return createErrorResponse('Invalid project path');
|
|
55
|
+
if (!this.context.isPathAllowed(args.projectPath))
|
|
56
|
+
return createErrorResponse('Project path is outside the allowed roots');
|
|
57
|
+
try {
|
|
58
|
+
const godotPath = await this.requireGodotPath();
|
|
59
|
+
if (!godotPath)
|
|
60
|
+
return createErrorResponse('Could not find a valid Godot executable path');
|
|
61
|
+
if (!existsSync(join(args.projectPath, 'project.godot')))
|
|
62
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
63
|
+
this.context.logDebug(`Launching Godot editor for project: ${args.projectPath}`);
|
|
64
|
+
const editorPlugin = this.context.installEditorPlugin?.(args.projectPath);
|
|
65
|
+
const editorArgs = ['-e', '--path', args.projectPath];
|
|
66
|
+
const editorProcess = spawn(godotPath, editorArgs, {
|
|
67
|
+
stdio: 'pipe', env: { ...process.env, ...this.context.getRuntimeEnvironment(), ...(this.context.getEditorEnvironment?.() ?? {}) },
|
|
68
|
+
});
|
|
69
|
+
editorProcess.on('error', (err) => { console.error('Failed to start Godot editor:', err); });
|
|
70
|
+
const editorEnvironment = this.context.getEditorEnvironment?.() ?? {};
|
|
71
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
72
|
+
launched: true,
|
|
73
|
+
project_path: args.projectPath,
|
|
74
|
+
editor_plugin: true,
|
|
75
|
+
plugin_owned: editorPlugin?.owned ?? false,
|
|
76
|
+
plugin_distribution: editorPlugin?.distribution ?? 'unavailable',
|
|
77
|
+
editor_protocol_version: editorPlugin?.protocolVersion ?? null,
|
|
78
|
+
editor_bridge_port: editorEnvironment.GODOT_MCP_EDITOR_PORT ?? null,
|
|
79
|
+
}, null, 2) }] };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return createErrorResponse(`Failed to launch Godot editor: ${this.errorMessage(error)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async handleEditorControl(args) {
|
|
86
|
+
args = normalizeParameters(args || {});
|
|
87
|
+
const allowed = ['inspect', 'select', 'save', 'reload', 'open_scene', 'set_property', 'rename_node', 'undo', 'redo'];
|
|
88
|
+
if (!args.projectPath || !allowed.includes(args.action)) {
|
|
89
|
+
return createErrorResponse('projectPath and a valid editor action are required.');
|
|
90
|
+
}
|
|
91
|
+
if (!validatePath(args.projectPath) || !this.context.isPathAllowed(args.projectPath)) {
|
|
92
|
+
return createErrorResponse('Project path is outside the allowed roots');
|
|
93
|
+
}
|
|
94
|
+
const params = {};
|
|
95
|
+
for (const key of ['nodePaths', 'scenePath', 'nodePath', 'property', 'value', 'name']) {
|
|
96
|
+
if (args[key] !== undefined)
|
|
97
|
+
params[key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)] = args[key];
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
if (!this.context.sendEditorCommand)
|
|
101
|
+
return createErrorResponse('Editor bridge is not configured. Launch the editor through launch_editor first.');
|
|
102
|
+
const result = await this.context.sendEditorCommand(args.action, params, 15_000);
|
|
103
|
+
if (result.error)
|
|
104
|
+
return createErrorResponse(`editor_control failed: ${typeof result.error === 'string' ? result.error : JSON.stringify(result.error)}`);
|
|
105
|
+
return { content: [{ type: 'text', text: JSON.stringify({ project_path: args.projectPath, action: args.action, ...result }, null, 2) }] };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return createErrorResponse(`editor_control failed: ${this.errorMessage(error)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async handleRunProject(args) {
|
|
112
|
+
args = normalizeParameters(args);
|
|
113
|
+
if (!args.projectPath)
|
|
114
|
+
return createErrorResponse('Project path is required');
|
|
115
|
+
if (!validatePath(args.projectPath))
|
|
116
|
+
return createErrorResponse('Invalid project path');
|
|
117
|
+
if (!this.context.isPathAllowed(args.projectPath)) {
|
|
118
|
+
return createErrorResponse(`Project path is outside the allowed roots (GODOT_MCP_ALLOWED_DIRS): ${args.projectPath}`);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const godotPath = await this.requireGodotPath();
|
|
122
|
+
if (!godotPath)
|
|
123
|
+
return createErrorResponse('Could not find a valid Godot executable path');
|
|
124
|
+
if (!existsSync(join(args.projectPath, 'project.godot')))
|
|
125
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
126
|
+
// The game and authoring harness both depend on the generated runtime
|
|
127
|
+
// installation. Give the user-facing run exclusive ownership before it
|
|
128
|
+
// injects and launches the project.
|
|
129
|
+
this.context.stopAuthoringSession?.();
|
|
130
|
+
if (this.context.getActiveProcess()) {
|
|
131
|
+
this.context.logDebug('Killing existing Godot process before starting a new one');
|
|
132
|
+
this.processGeneration += 1;
|
|
133
|
+
this.context.disconnectFromGame();
|
|
134
|
+
const existingProjectPath = this.context.getConnectedProjectPath();
|
|
135
|
+
if (existingProjectPath)
|
|
136
|
+
this.context.removeInteractionServer(existingProjectPath);
|
|
137
|
+
const stopped = this.context.stopProjectProcess();
|
|
138
|
+
// The old process still holds the interaction port until it exits;
|
|
139
|
+
// starting the replacement immediately made its server fail to listen
|
|
140
|
+
// and the relaunch never became reachable.
|
|
141
|
+
if (stopped)
|
|
142
|
+
await this.waitForProcessExit(stopped, 10_000);
|
|
143
|
+
}
|
|
144
|
+
this.context.injectInteractionServer(args.projectPath);
|
|
145
|
+
// No `-d`: that starts Godot's *local stdout debugger*, which breaks into an
|
|
146
|
+
// interactive `debug>` prompt on any script error and blocks the main loop
|
|
147
|
+
// forever. The game then stops answering every runtime command, so a single
|
|
148
|
+
// bad script hung the whole session. Errors still print with a full
|
|
149
|
+
// backtrace without it, and the editor binary already runs projects as a
|
|
150
|
+
// debug build.
|
|
151
|
+
const commandArgs = [...deterministicSessionArguments(), '--path', args.projectPath];
|
|
152
|
+
if (args.scene && (!this.context.isRelativePathAllowed || this.context.isRelativePathAllowed(args.projectPath, args.scene)))
|
|
153
|
+
commandArgs.push(args.scene);
|
|
154
|
+
this.context.logDebug(`Running Godot project: ${args.projectPath}`);
|
|
155
|
+
const processGeneration = ++this.processGeneration;
|
|
156
|
+
this.context.startProjectProcess(godotPath, commandArgs, () => { this.handleProjectExit(processGeneration); }, {
|
|
157
|
+
...this.context.getRuntimeEnvironment(),
|
|
158
|
+
...deterministicSessionEnvironment(),
|
|
159
|
+
});
|
|
160
|
+
this.context.connectToGame(args.projectPath).catch(error => {
|
|
161
|
+
this.context.logDebug(`Failed to connect to game interaction server: ${error}`);
|
|
162
|
+
});
|
|
163
|
+
return {
|
|
164
|
+
content: [{
|
|
165
|
+
type: 'text',
|
|
166
|
+
text: `Godot project started in debug mode. Use get_debug_output to see output. Game interaction server connecting on port ${this.context.getInteractionPort()}...`,
|
|
167
|
+
}],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
return createErrorResponse(`Failed to run Godot project: ${this.errorMessage(error)}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async handleVerifyProject(args) {
|
|
175
|
+
args = normalizeParameters(args || {});
|
|
176
|
+
const teardown = args.teardown !== false;
|
|
177
|
+
const started = await this.handleRunProject({ projectPath: args.projectPath, ...(args.scene ? { scene: args.scene } : {}) });
|
|
178
|
+
if (started.isError === true)
|
|
179
|
+
return started;
|
|
180
|
+
const evidence = {
|
|
181
|
+
project_path: args.projectPath,
|
|
182
|
+
started: true,
|
|
183
|
+
assertions: [],
|
|
184
|
+
screenshot: null,
|
|
185
|
+
teardown,
|
|
186
|
+
};
|
|
187
|
+
let passed = true;
|
|
188
|
+
try {
|
|
189
|
+
const deadline = Date.now() + 60_000;
|
|
190
|
+
while (!this.context.isGameConnected()) {
|
|
191
|
+
if (!this.context.getActiveProcess())
|
|
192
|
+
throw new Error('Godot exited before the verification runtime connected');
|
|
193
|
+
if (Date.now() > deadline)
|
|
194
|
+
throw new Error('Timed out waiting for the verification runtime connection');
|
|
195
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
196
|
+
}
|
|
197
|
+
const waited = await this.context.sendGameCommand('wait', {
|
|
198
|
+
frames: args.waitFrames ?? 2, frame_type: 'render',
|
|
199
|
+
}, 30_000);
|
|
200
|
+
if ('error' in waited)
|
|
201
|
+
throw new Error(`Frame wait failed: ${waited.error.message}`);
|
|
202
|
+
const assertionEvidence = evidence.assertions;
|
|
203
|
+
for (const assertion of (args.assertions ?? [])) {
|
|
204
|
+
const result = await this.evaluateVerificationAssertion(assertion);
|
|
205
|
+
assertionEvidence.push(result);
|
|
206
|
+
if (result.passed !== true)
|
|
207
|
+
passed = false;
|
|
208
|
+
}
|
|
209
|
+
if (args.captureScreenshot === true) {
|
|
210
|
+
const screenshot = await this.context.sendGameCommand('screenshot', {}, 30_000);
|
|
211
|
+
if ('error' in screenshot) {
|
|
212
|
+
evidence.screenshot = { captured: false, error: screenshot.error.message };
|
|
213
|
+
passed = false;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
const result = screenshot.result;
|
|
217
|
+
const bytes = Buffer.from(result.data ?? '', 'base64');
|
|
218
|
+
evidence.screenshot = {
|
|
219
|
+
captured: bytes.length > 0,
|
|
220
|
+
width: result.width,
|
|
221
|
+
height: result.height,
|
|
222
|
+
bytes: bytes.length,
|
|
223
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
224
|
+
};
|
|
225
|
+
if (bytes.length === 0)
|
|
226
|
+
passed = false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
passed = false;
|
|
232
|
+
evidence.workflow_error = error instanceof Error ? error.message : String(error);
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
if (teardown && this.context.getActiveProcess()) {
|
|
236
|
+
const stopped = await this.handleStopProject();
|
|
237
|
+
evidence.stopped = stopped.isError !== true;
|
|
238
|
+
if (stopped.isError === true)
|
|
239
|
+
passed = false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
evidence.passed = passed;
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: 'text', text: JSON.stringify(evidence, null, 2) }],
|
|
245
|
+
...(passed ? {} : { isError: true }),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
async evaluateVerificationAssertion(assertion) {
|
|
249
|
+
if (assertion.kind === 'node_exists') {
|
|
250
|
+
if (!assertion.nodePath)
|
|
251
|
+
return { ...assertion, passed: false, error: 'nodePath is required' };
|
|
252
|
+
const response = await this.context.sendGameCommand('get_node_info', { node_path: assertion.nodePath });
|
|
253
|
+
return 'error' in response
|
|
254
|
+
? { ...assertion, passed: false, error: response.error.message }
|
|
255
|
+
: { ...assertion, passed: true };
|
|
256
|
+
}
|
|
257
|
+
if (assertion.kind === 'group_count') {
|
|
258
|
+
if (!assertion.group || assertion.count === undefined) {
|
|
259
|
+
return { ...assertion, passed: false, error: 'group and count are required' };
|
|
260
|
+
}
|
|
261
|
+
const response = await this.context.sendGameCommand('get_nodes_in_group', { group: assertion.group });
|
|
262
|
+
if ('error' in response)
|
|
263
|
+
return { ...assertion, passed: false, error: response.error.message };
|
|
264
|
+
const result = response.result;
|
|
265
|
+
const actual = result.nodes?.length ?? 0;
|
|
266
|
+
return { ...assertion, actual, passed: actual === assertion.count };
|
|
267
|
+
}
|
|
268
|
+
if (assertion.kind === 'log_contains') {
|
|
269
|
+
if (typeof assertion.text !== 'string')
|
|
270
|
+
return { ...assertion, passed: false, error: 'text is required' };
|
|
271
|
+
const output = this.context.getActiveProcess()?.output.join('\n') ?? '';
|
|
272
|
+
return { ...assertion, passed: output.includes(assertion.text) };
|
|
273
|
+
}
|
|
274
|
+
return { ...assertion, passed: false, error: `Unknown assertion kind: ${String(assertion.kind)}` };
|
|
275
|
+
}
|
|
276
|
+
async handleGetDebugOutput() {
|
|
277
|
+
const activeProcess = this.context.getActiveProcess();
|
|
278
|
+
if (!activeProcess)
|
|
279
|
+
return createErrorResponse('No active Godot process.');
|
|
280
|
+
return { content: [{ type: 'text', text: JSON.stringify({ output: activeProcess.output, errors: activeProcess.errors }, null, 2) }] };
|
|
281
|
+
}
|
|
282
|
+
async handleStopProject() {
|
|
283
|
+
if (!this.context.getActiveProcess())
|
|
284
|
+
return createErrorResponse('No active Godot process to stop.');
|
|
285
|
+
this.context.logDebug('Stopping active Godot process');
|
|
286
|
+
// Invalidate the child's eventual exit callback before performing the same
|
|
287
|
+
// cleanup synchronously. A late callback must never tear down a replacement.
|
|
288
|
+
this.processGeneration += 1;
|
|
289
|
+
this.context.disconnectFromGame();
|
|
290
|
+
const stoppedProcess = this.context.stopProjectProcess();
|
|
291
|
+
const projectPath = this.context.getConnectedProjectPath();
|
|
292
|
+
if (projectPath) {
|
|
293
|
+
this.context.removeInteractionServer(projectPath);
|
|
294
|
+
this.context.clearConnectedProjectPath();
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
content: [{
|
|
298
|
+
type: 'text',
|
|
299
|
+
text: JSON.stringify({ message: 'Godot project stopped', finalOutput: stoppedProcess.output, finalErrors: stoppedProcess.errors }, null, 2),
|
|
300
|
+
}],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async handleGetGodotVersion() {
|
|
304
|
+
try {
|
|
305
|
+
const godotPath = await this.requireGodotPath();
|
|
306
|
+
if (!godotPath)
|
|
307
|
+
return createErrorResponse('Could not find a valid Godot executable path');
|
|
308
|
+
this.context.logDebug('Getting Godot version');
|
|
309
|
+
const { stdout } = await execFileAsync(godotPath, ['--version'], GODOT_VERSION_OPTIONS);
|
|
310
|
+
return { content: [{ type: 'text', text: stdout.trim() }] };
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
return createErrorResponse(`Failed to get Godot version: ${this.errorMessage(error)}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
waitForProcessExit(record, timeoutMs) {
|
|
317
|
+
return new Promise(resolve => {
|
|
318
|
+
const child = record.process;
|
|
319
|
+
if (typeof child.once !== 'function' || child.exitCode !== null || child.signalCode !== null) {
|
|
320
|
+
resolve();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
324
|
+
child.once('exit', () => {
|
|
325
|
+
clearTimeout(timer);
|
|
326
|
+
resolve();
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
async requireGodotPath() {
|
|
331
|
+
return this.context.executable.requirePath();
|
|
332
|
+
}
|
|
333
|
+
handleProjectExit(processGeneration) {
|
|
334
|
+
if (processGeneration !== this.processGeneration)
|
|
335
|
+
return;
|
|
336
|
+
this.context.disconnectFromGame();
|
|
337
|
+
const projectPath = this.context.getConnectedProjectPath();
|
|
338
|
+
if (projectPath) {
|
|
339
|
+
this.context.removeInteractionServer(projectPath);
|
|
340
|
+
this.context.clearConnectedProjectPath();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
errorMessage(error) {
|
|
344
|
+
return error instanceof Error ? error.message : 'Unknown error';
|
|
345
|
+
}
|
|
346
|
+
}
|