@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,1506 @@
|
|
|
1
|
+
import { join, dirname, basename } from 'path';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { execFile } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { normalizeParameters, validatePath, createErrorResponse, errorMessage, isGodot44OrLater, generateGodotProjectFeatures, generateCsprojContent, generateCsharpScriptSource, toDotnetIdentifier, isValidCsharpIdentifier, PathSecurity, } from '../utils.js';
|
|
6
|
+
import { GODOT_VERSION_OPTIONS } from '../godot-subprocess.js';
|
|
7
|
+
import { ProjectConfigurationService, ProjectExportService, ProjectFileIOService, ImportPipelineService, ProjectIntegrityService, ExportReadinessService, DotnetWorkflowService, AddonManagementService, ProjectTestService, SceneOperationService, ScriptValidationService, } from './project-handler-services.js';
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const CI_EXPORT_PLATFORMS = new Set(['windows', 'linux', 'macos', 'web']);
|
|
10
|
+
const DOCKER_BASE_IMAGES = new Set(['ubuntu:22.04', 'ubuntu:24.04']);
|
|
11
|
+
const GODOT_EXPORT_VERSION = /^4\.\d+(?:\.\d+)?(?:-stable)?$/;
|
|
12
|
+
const EXPORT_PRESET_NAME = /^[A-Za-z0-9][A-Za-z0-9 _./-]{0,127}$/;
|
|
13
|
+
const INPUT_EVENT_KEY_PREFIX = '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';
|
|
14
|
+
const INPUT_EVENT_KEY_SUFFIX = ',"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)';
|
|
15
|
+
/** Merge one physical key into an existing one-line project.godot input entry. */
|
|
16
|
+
export function mergeInputMapAction(content, actionName, deadzone, physicalKeycode) {
|
|
17
|
+
const escapedName = actionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
18
|
+
const inputSection = /(^|\n)\[input\][^\n]*(?:\n|$)([\s\S]*?)(?=\n\[[^\n]+\](?:\n|$)|$)/.exec(content);
|
|
19
|
+
const newEvent = physicalKeycode === undefined
|
|
20
|
+
? ''
|
|
21
|
+
: `${INPUT_EVENT_KEY_PREFIX},"physical_keycode":${physicalKeycode}${INPUT_EVENT_KEY_SUFFIX}`;
|
|
22
|
+
if (inputSection) {
|
|
23
|
+
const sectionBody = inputSection[2];
|
|
24
|
+
const actionPattern = new RegExp(`^${escapedName}\\s*=\\s*(\\{[^\\r\\n]*\\})[ \\t]*$`, 'm');
|
|
25
|
+
const existing = actionPattern.exec(sectionBody);
|
|
26
|
+
if (existing) {
|
|
27
|
+
const block = existing[1];
|
|
28
|
+
const existingDeadzone = /"deadzone"\s*:\s*([\d.eE+-]+)/.exec(block)?.[1] ?? String(deadzone);
|
|
29
|
+
const existingEvents = /"events"\s*:\s*\[([\s\S]*?)\]\s*(?:,|\})/.exec(block)?.[1] ?? '';
|
|
30
|
+
const duplicate = physicalKeycode !== undefined
|
|
31
|
+
&& new RegExp(`"physical_keycode"\\s*:\\s*${physicalKeycode}\\b`).test(existingEvents);
|
|
32
|
+
if (!newEvent || duplicate) {
|
|
33
|
+
return { content, existed: true, eventAdded: false };
|
|
34
|
+
}
|
|
35
|
+
const mergedEvents = newEvent && !duplicate
|
|
36
|
+
? (existingEvents.trim() ? `${existingEvents}, ${newEvent}` : newEvent)
|
|
37
|
+
: existingEvents;
|
|
38
|
+
const replacement = mergedEvents.trim()
|
|
39
|
+
? `${actionName}={"deadzone": ${existingDeadzone}, "events": [${mergedEvents}]}`
|
|
40
|
+
: `${actionName}={"deadzone": ${existingDeadzone}}`;
|
|
41
|
+
const updatedBody = sectionBody.replace(actionPattern, replacement);
|
|
42
|
+
const bodyStart = inputSection.index + inputSection[0].indexOf(sectionBody);
|
|
43
|
+
return {
|
|
44
|
+
content: `${content.slice(0, bodyStart)}${updatedBody}${content.slice(bodyStart + sectionBody.length)}`,
|
|
45
|
+
existed: true,
|
|
46
|
+
eventAdded: Boolean(newEvent) && !duplicate,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const events = newEvent ? `, "events": [${newEvent}]` : '';
|
|
51
|
+
const inputLine = `${actionName}={"deadzone": ${deadzone}${events}}`;
|
|
52
|
+
const updated = content.includes('[input]')
|
|
53
|
+
? content.replace('[input]', `[input]\n\n${inputLine}`)
|
|
54
|
+
: `${content}\n[input]\n\n${inputLine}\n`;
|
|
55
|
+
return { content: updated, existed: false, eventAdded: Boolean(newEvent) };
|
|
56
|
+
}
|
|
57
|
+
function validateGodotExportVersion(version) {
|
|
58
|
+
if (typeof version !== 'string' || !GODOT_EXPORT_VERSION.test(version)) {
|
|
59
|
+
return 'godotVersion must be a supported Godot 4 version such as "4.3-stable".';
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/** Implements project, scene, file, script, and export tools. */
|
|
64
|
+
export class ProjectToolHandlers {
|
|
65
|
+
context;
|
|
66
|
+
fileIO;
|
|
67
|
+
configuration;
|
|
68
|
+
scriptValidation;
|
|
69
|
+
exportService;
|
|
70
|
+
sceneOperations;
|
|
71
|
+
projectTests;
|
|
72
|
+
importPipeline;
|
|
73
|
+
projectIntegrity;
|
|
74
|
+
exportReadiness;
|
|
75
|
+
dotnetWorkflow;
|
|
76
|
+
addonManagement;
|
|
77
|
+
constructor(context) {
|
|
78
|
+
const pathSecurity = context.pathSecurity ?? new PathSecurity();
|
|
79
|
+
this.context = {
|
|
80
|
+
...context,
|
|
81
|
+
pathSecurity,
|
|
82
|
+
getGodotPath: () => context.executable.path,
|
|
83
|
+
detectGodotPath: async () => { await context.executable.detect(); },
|
|
84
|
+
executeOperation: (operation, params, projectPath) => context.operations.execute(operation, params, projectPath),
|
|
85
|
+
headlessOp: (operation, args, argsFn) => {
|
|
86
|
+
const { projectPath, params } = argsFn(args);
|
|
87
|
+
return context.operations.run(operation, projectPath, params);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
const serviceContext = {
|
|
91
|
+
executable: context.executable,
|
|
92
|
+
operations: context.operations,
|
|
93
|
+
pathSecurity,
|
|
94
|
+
projectSupport: context.projectSupport,
|
|
95
|
+
};
|
|
96
|
+
this.fileIO = new ProjectFileIOService(serviceContext);
|
|
97
|
+
this.configuration = new ProjectConfigurationService(serviceContext);
|
|
98
|
+
this.scriptValidation = new ScriptValidationService(serviceContext);
|
|
99
|
+
this.exportService = new ProjectExportService(serviceContext);
|
|
100
|
+
this.sceneOperations = new SceneOperationService(serviceContext);
|
|
101
|
+
this.projectTests = new ProjectTestService(serviceContext);
|
|
102
|
+
this.importPipeline = new ImportPipelineService(serviceContext);
|
|
103
|
+
this.projectIntegrity = new ProjectIntegrityService(serviceContext);
|
|
104
|
+
this.exportReadiness = new ExportReadinessService(serviceContext);
|
|
105
|
+
this.dotnetWorkflow = new DotnetWorkflowService(serviceContext);
|
|
106
|
+
this.addonManagement = new AddonManagementService(serviceContext);
|
|
107
|
+
}
|
|
108
|
+
projectRelativePath(projectPath, relativePath) {
|
|
109
|
+
const resolved = this.context.pathSecurity.resolveProjectPath(projectPath, relativePath);
|
|
110
|
+
if (!resolved)
|
|
111
|
+
throw new Error(`Path is outside the project: ${relativePath}`);
|
|
112
|
+
return resolved;
|
|
113
|
+
}
|
|
114
|
+
/** Converts the runner's structured process status into a tool error. */
|
|
115
|
+
headlessFailure(action, result) {
|
|
116
|
+
if (result.exitCode === 0 && result.signal === null)
|
|
117
|
+
return null;
|
|
118
|
+
const status = result.signal
|
|
119
|
+
? `terminated by signal ${result.signal}`
|
|
120
|
+
: `exited with code ${result.exitCode}`;
|
|
121
|
+
const output = result.stderr || result.stdout;
|
|
122
|
+
return createErrorResponse(`${action} (${status})${output ? `: ${output}` : '.'}`);
|
|
123
|
+
}
|
|
124
|
+
async handleListProjects(args) {
|
|
125
|
+
// Normalize parameters to camelCase
|
|
126
|
+
args = normalizeParameters(args);
|
|
127
|
+
if (!args.directory) {
|
|
128
|
+
return createErrorResponse('Directory is required');
|
|
129
|
+
}
|
|
130
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.directory)) {
|
|
131
|
+
return createErrorResponse('Invalid directory path');
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
this.context.logDebug(`Listing Godot projects in directory: ${args.directory}`);
|
|
135
|
+
if (!existsSync(args.directory)) {
|
|
136
|
+
return createErrorResponse(`Directory does not exist: ${args.directory}`);
|
|
137
|
+
}
|
|
138
|
+
const recursive = args.recursive === true;
|
|
139
|
+
const projects = this.context.projectSupport.findGodotProjects(args.directory, recursive);
|
|
140
|
+
return {
|
|
141
|
+
content: [
|
|
142
|
+
{
|
|
143
|
+
type: 'text',
|
|
144
|
+
text: JSON.stringify(projects, null, 2),
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return createErrorResponse(`Failed to list projects: ${errorMessage(error)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get the structure of a Godot project asynchronously by counting files recursively
|
|
155
|
+
* @param projectPath Path to the Godot project
|
|
156
|
+
* @returns Promise resolving to an object with counts of scenes, scripts, assets, and other files
|
|
157
|
+
*/
|
|
158
|
+
async handleGetProjectInfo(args) {
|
|
159
|
+
// Normalize parameters to camelCase
|
|
160
|
+
args = normalizeParameters(args);
|
|
161
|
+
if (!args.projectPath) {
|
|
162
|
+
return createErrorResponse('Project path is required');
|
|
163
|
+
}
|
|
164
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath)) {
|
|
165
|
+
return createErrorResponse('Invalid project path');
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
// Ensure godotPath is set
|
|
169
|
+
if (!this.context.getGodotPath()) {
|
|
170
|
+
await this.context.detectGodotPath();
|
|
171
|
+
if (!this.context.getGodotPath()) {
|
|
172
|
+
return createErrorResponse('Could not find a valid Godot executable path');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Check if the project directory exists and contains a project.godot file
|
|
176
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
177
|
+
if (!existsSync(projectFile)) {
|
|
178
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
179
|
+
}
|
|
180
|
+
this.context.logDebug(`Getting project info for: ${args.projectPath}`);
|
|
181
|
+
// Get Godot version
|
|
182
|
+
const { stdout } = await execFileAsync(this.context.getGodotPath(), ['--version'], GODOT_VERSION_OPTIONS);
|
|
183
|
+
// Get project structure using the recursive method
|
|
184
|
+
const projectStructure = await this.context.projectSupport.getProjectStructureAsync(args.projectPath);
|
|
185
|
+
// Extract project name from project.godot file
|
|
186
|
+
let projectName = basename(args.projectPath);
|
|
187
|
+
try {
|
|
188
|
+
const projectFileContent = readFileSync(projectFile, 'utf8');
|
|
189
|
+
const configNameMatch = /config\/name="([^"]+)"/.exec(projectFileContent);
|
|
190
|
+
if (configNameMatch && configNameMatch[1]) {
|
|
191
|
+
projectName = configNameMatch[1];
|
|
192
|
+
this.context.logDebug(`Found project name in config: ${projectName}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
this.context.logDebug(`Error reading project file: ${error}`);
|
|
197
|
+
// Continue with default project name if extraction fails
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
content: [
|
|
201
|
+
{
|
|
202
|
+
type: 'text',
|
|
203
|
+
text: JSON.stringify({
|
|
204
|
+
name: projectName,
|
|
205
|
+
path: args.projectPath,
|
|
206
|
+
godotVersion: stdout.trim(),
|
|
207
|
+
isDotnet: this.context.projectSupport.isDotnetProject(args.projectPath),
|
|
208
|
+
structure: projectStructure,
|
|
209
|
+
}, null, 2),
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
return createErrorResponse(`Failed to get project info: ${errorMessage(error)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Handle the create_scene tool
|
|
220
|
+
*/
|
|
221
|
+
async handleCreateScene(args) {
|
|
222
|
+
// Normalize parameters to camelCase
|
|
223
|
+
args = normalizeParameters(args);
|
|
224
|
+
if (!args.projectPath || !args.scenePath) {
|
|
225
|
+
return createErrorResponse('Project path and scene path are required');
|
|
226
|
+
}
|
|
227
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath) || !validatePath(args.scenePath)) {
|
|
228
|
+
return createErrorResponse('Invalid path');
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
// Check if the project directory exists and contains a project.godot file
|
|
232
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
233
|
+
if (!existsSync(projectFile)) {
|
|
234
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
235
|
+
}
|
|
236
|
+
// Prepare parameters for the operation (already in camelCase)
|
|
237
|
+
const params = {
|
|
238
|
+
scenePath: args.scenePath,
|
|
239
|
+
rootNodeType: args.rootNodeType || 'Node2D',
|
|
240
|
+
};
|
|
241
|
+
// Execute the operation
|
|
242
|
+
const result = await this.context.executeOperation('create_scene', params, args.projectPath);
|
|
243
|
+
const failure = this.headlessFailure('Failed to create scene', result);
|
|
244
|
+
if (failure)
|
|
245
|
+
return failure;
|
|
246
|
+
const { stdout } = result;
|
|
247
|
+
return {
|
|
248
|
+
content: [
|
|
249
|
+
{
|
|
250
|
+
type: 'text',
|
|
251
|
+
text: `Scene created successfully at: ${args.scenePath}\n\nOutput: ${stdout}`,
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
return createErrorResponse(`Failed to create scene: ${errorMessage(error)}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Handle the add_node tool
|
|
262
|
+
*/
|
|
263
|
+
async handleAddNode(args) {
|
|
264
|
+
// Normalize parameters to camelCase
|
|
265
|
+
args = normalizeParameters(args);
|
|
266
|
+
if (!args.projectPath || !args.scenePath || !args.nodeType || !args.nodeName) {
|
|
267
|
+
return createErrorResponse('Missing required parameters');
|
|
268
|
+
}
|
|
269
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath) || !validatePath(args.scenePath)) {
|
|
270
|
+
return createErrorResponse('Invalid path');
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
// Check if the project directory exists and contains a project.godot file
|
|
274
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
275
|
+
if (!existsSync(projectFile)) {
|
|
276
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
277
|
+
}
|
|
278
|
+
// Check if the scene file exists
|
|
279
|
+
const scenePath = this.projectRelativePath(args.projectPath, args.scenePath);
|
|
280
|
+
if (!existsSync(scenePath)) {
|
|
281
|
+
return createErrorResponse(`Scene file does not exist: ${args.scenePath}`);
|
|
282
|
+
}
|
|
283
|
+
// Prepare parameters for the operation (already in camelCase)
|
|
284
|
+
const params = {
|
|
285
|
+
scenePath: args.scenePath,
|
|
286
|
+
nodeType: args.nodeType,
|
|
287
|
+
nodeName: args.nodeName,
|
|
288
|
+
};
|
|
289
|
+
// Add optional parameters
|
|
290
|
+
if (args.parentNodePath) {
|
|
291
|
+
params.parentNodePath = args.parentNodePath;
|
|
292
|
+
}
|
|
293
|
+
if (args.properties) {
|
|
294
|
+
params.properties = args.properties;
|
|
295
|
+
}
|
|
296
|
+
// Execute the operation
|
|
297
|
+
const result = await this.context.executeOperation('add_node', params, args.projectPath);
|
|
298
|
+
const failure = this.headlessFailure('Failed to add node', result);
|
|
299
|
+
if (failure)
|
|
300
|
+
return failure;
|
|
301
|
+
const { stdout } = result;
|
|
302
|
+
return {
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: 'text',
|
|
306
|
+
text: `Node '${args.nodeName}' of type '${args.nodeType}' added successfully to '${args.scenePath}'.\n\nOutput: ${stdout}`,
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
return createErrorResponse(`Failed to add node: ${errorMessage(error)}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Handle the load_sprite tool
|
|
317
|
+
*/
|
|
318
|
+
async handleLoadSprite(args) {
|
|
319
|
+
// Normalize parameters to camelCase
|
|
320
|
+
args = normalizeParameters(args);
|
|
321
|
+
if (!args.projectPath || !args.scenePath || !args.nodePath || !args.texturePath) {
|
|
322
|
+
return createErrorResponse('Missing required parameters');
|
|
323
|
+
}
|
|
324
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath) ||
|
|
325
|
+
!validatePath(args.scenePath) ||
|
|
326
|
+
!validatePath(args.nodePath) ||
|
|
327
|
+
!validatePath(args.texturePath)) {
|
|
328
|
+
return createErrorResponse('Invalid path');
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
// Check if the project directory exists and contains a project.godot file
|
|
332
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
333
|
+
if (!existsSync(projectFile)) {
|
|
334
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
335
|
+
}
|
|
336
|
+
// Check if the scene file exists
|
|
337
|
+
const scenePath = this.projectRelativePath(args.projectPath, args.scenePath);
|
|
338
|
+
if (!existsSync(scenePath)) {
|
|
339
|
+
return createErrorResponse(`Scene file does not exist: ${args.scenePath}`);
|
|
340
|
+
}
|
|
341
|
+
// Check if the texture file exists
|
|
342
|
+
const texturePath = this.projectRelativePath(args.projectPath, args.texturePath);
|
|
343
|
+
if (!existsSync(texturePath)) {
|
|
344
|
+
return createErrorResponse(`Texture file does not exist: ${args.texturePath}`);
|
|
345
|
+
}
|
|
346
|
+
// Prepare parameters for the operation (already in camelCase)
|
|
347
|
+
const params = {
|
|
348
|
+
scenePath: args.scenePath,
|
|
349
|
+
nodePath: args.nodePath,
|
|
350
|
+
texturePath: args.texturePath,
|
|
351
|
+
};
|
|
352
|
+
// Execute the operation
|
|
353
|
+
const result = await this.context.executeOperation('load_sprite', params, args.projectPath);
|
|
354
|
+
const failure = this.headlessFailure('Failed to load sprite', result);
|
|
355
|
+
if (failure)
|
|
356
|
+
return failure;
|
|
357
|
+
const { stdout } = result;
|
|
358
|
+
return {
|
|
359
|
+
content: [
|
|
360
|
+
{
|
|
361
|
+
type: 'text',
|
|
362
|
+
text: `Sprite loaded successfully with texture: ${args.texturePath}\n\nOutput: ${stdout}`,
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
return createErrorResponse(`Failed to load sprite: ${errorMessage(error)}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Handle the export_mesh_library tool
|
|
373
|
+
*/
|
|
374
|
+
async handleExportMeshLibrary(args) {
|
|
375
|
+
// Normalize parameters to camelCase
|
|
376
|
+
args = normalizeParameters(args);
|
|
377
|
+
if (!args.projectPath || !args.scenePath || !args.outputPath) {
|
|
378
|
+
return createErrorResponse('Missing required parameters');
|
|
379
|
+
}
|
|
380
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath) ||
|
|
381
|
+
!validatePath(args.scenePath) ||
|
|
382
|
+
!validatePath(args.outputPath)) {
|
|
383
|
+
return createErrorResponse('Invalid path');
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
// Check if the project directory exists and contains a project.godot file
|
|
387
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
388
|
+
if (!existsSync(projectFile)) {
|
|
389
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
390
|
+
}
|
|
391
|
+
// Check if the scene file exists
|
|
392
|
+
const scenePath = this.projectRelativePath(args.projectPath, args.scenePath);
|
|
393
|
+
if (!existsSync(scenePath)) {
|
|
394
|
+
return createErrorResponse(`Scene file does not exist: ${args.scenePath}`);
|
|
395
|
+
}
|
|
396
|
+
// Prepare parameters for the operation (already in camelCase)
|
|
397
|
+
const params = {
|
|
398
|
+
scenePath: args.scenePath,
|
|
399
|
+
outputPath: args.outputPath,
|
|
400
|
+
};
|
|
401
|
+
// Add optional parameters
|
|
402
|
+
if (args.meshItemNames && Array.isArray(args.meshItemNames)) {
|
|
403
|
+
params.meshItemNames = args.meshItemNames;
|
|
404
|
+
}
|
|
405
|
+
// Execute the operation
|
|
406
|
+
const result = await this.context.executeOperation('export_mesh_library', params, args.projectPath);
|
|
407
|
+
const failure = this.headlessFailure('Failed to export mesh library', result);
|
|
408
|
+
if (failure)
|
|
409
|
+
return failure;
|
|
410
|
+
const { stdout } = result;
|
|
411
|
+
return {
|
|
412
|
+
content: [
|
|
413
|
+
{
|
|
414
|
+
type: 'text',
|
|
415
|
+
text: `MeshLibrary exported successfully to: ${args.outputPath}\n\nOutput: ${stdout}`,
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
return createErrorResponse(`Failed to export mesh library: ${errorMessage(error)}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Handle the save_scene tool
|
|
426
|
+
*/
|
|
427
|
+
async handleSaveScene(args) {
|
|
428
|
+
// Normalize parameters to camelCase
|
|
429
|
+
args = normalizeParameters(args);
|
|
430
|
+
if (!args.projectPath || !args.scenePath) {
|
|
431
|
+
return createErrorResponse('Missing required parameters');
|
|
432
|
+
}
|
|
433
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath) || !validatePath(args.scenePath)) {
|
|
434
|
+
return createErrorResponse('Invalid path');
|
|
435
|
+
}
|
|
436
|
+
// If newPath is provided, validate it
|
|
437
|
+
if (args.newPath && !validatePath(args.newPath)) {
|
|
438
|
+
return createErrorResponse('Invalid new path');
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
// Check if the project directory exists and contains a project.godot file
|
|
442
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
443
|
+
if (!existsSync(projectFile)) {
|
|
444
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
445
|
+
}
|
|
446
|
+
// Check if the scene file exists
|
|
447
|
+
const scenePath = this.projectRelativePath(args.projectPath, args.scenePath);
|
|
448
|
+
if (!existsSync(scenePath)) {
|
|
449
|
+
return createErrorResponse(`Scene file does not exist: ${args.scenePath}`);
|
|
450
|
+
}
|
|
451
|
+
// Prepare parameters for the operation (already in camelCase)
|
|
452
|
+
const params = {
|
|
453
|
+
scenePath: args.scenePath,
|
|
454
|
+
};
|
|
455
|
+
// Add optional parameters
|
|
456
|
+
if (args.newPath) {
|
|
457
|
+
params.newPath = args.newPath;
|
|
458
|
+
}
|
|
459
|
+
// Execute the operation
|
|
460
|
+
const result = await this.context.executeOperation('save_scene', params, args.projectPath);
|
|
461
|
+
const failure = this.headlessFailure('Failed to save scene', result);
|
|
462
|
+
if (failure)
|
|
463
|
+
return failure;
|
|
464
|
+
const { stdout } = result;
|
|
465
|
+
const savePath = args.newPath || args.scenePath;
|
|
466
|
+
return {
|
|
467
|
+
content: [
|
|
468
|
+
{
|
|
469
|
+
type: 'text',
|
|
470
|
+
text: `Scene saved successfully to: ${savePath}\n\nOutput: ${stdout}`,
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
return createErrorResponse(`Failed to save scene: ${errorMessage(error)}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Handle the get_uid tool
|
|
481
|
+
*/
|
|
482
|
+
async handleGetUid(args) {
|
|
483
|
+
// Normalize parameters to camelCase
|
|
484
|
+
args = normalizeParameters(args);
|
|
485
|
+
if (!args.projectPath || !args.filePath) {
|
|
486
|
+
return createErrorResponse('Missing required parameters');
|
|
487
|
+
}
|
|
488
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath) || !validatePath(args.filePath)) {
|
|
489
|
+
return createErrorResponse('Invalid path');
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
// Ensure godotPath is set
|
|
493
|
+
if (!this.context.getGodotPath()) {
|
|
494
|
+
await this.context.detectGodotPath();
|
|
495
|
+
if (!this.context.getGodotPath()) {
|
|
496
|
+
return createErrorResponse('Could not find a valid Godot executable path');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Check if the project directory exists and contains a project.godot file
|
|
500
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
501
|
+
if (!existsSync(projectFile)) {
|
|
502
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
503
|
+
}
|
|
504
|
+
// Check if the file exists
|
|
505
|
+
const filePath = this.projectRelativePath(args.projectPath, args.filePath);
|
|
506
|
+
if (!existsSync(filePath)) {
|
|
507
|
+
return createErrorResponse(`File does not exist: ${args.filePath}`);
|
|
508
|
+
}
|
|
509
|
+
// Get Godot version to check if UIDs are supported
|
|
510
|
+
const { stdout: versionOutput } = await execFileAsync(this.context.getGodotPath(), ['--version'], GODOT_VERSION_OPTIONS);
|
|
511
|
+
const version = versionOutput.trim();
|
|
512
|
+
if (!isGodot44OrLater(version)) {
|
|
513
|
+
return createErrorResponse(`UIDs are only supported in Godot 4.4 or later. Current version: ${version}`);
|
|
514
|
+
}
|
|
515
|
+
// Prepare parameters for the operation (already in camelCase)
|
|
516
|
+
const params = {
|
|
517
|
+
filePath: args.filePath,
|
|
518
|
+
};
|
|
519
|
+
// Execute the operation
|
|
520
|
+
const result = await this.context.executeOperation('get_uid', params, args.projectPath);
|
|
521
|
+
const failure = this.headlessFailure('Failed to get UID', result);
|
|
522
|
+
if (failure)
|
|
523
|
+
return failure;
|
|
524
|
+
const { stdout } = result;
|
|
525
|
+
return {
|
|
526
|
+
content: [
|
|
527
|
+
{
|
|
528
|
+
type: 'text',
|
|
529
|
+
text: `UID for ${args.filePath}: ${stdout.trim()}`,
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
return createErrorResponse(`Failed to get UID: ${errorMessage(error)}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Handle the game_screenshot tool
|
|
540
|
+
*/
|
|
541
|
+
async handleReadScene(args) {
|
|
542
|
+
args = normalizeParameters(args || {});
|
|
543
|
+
if (!args.projectPath || !args.scenePath) {
|
|
544
|
+
return createErrorResponse('projectPath and scenePath are required.');
|
|
545
|
+
}
|
|
546
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath) || !validatePath(args.scenePath)) {
|
|
547
|
+
return createErrorResponse('Invalid path.');
|
|
548
|
+
}
|
|
549
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
550
|
+
if (!existsSync(projectFile)) {
|
|
551
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
552
|
+
}
|
|
553
|
+
const scenePath = this.projectRelativePath(args.projectPath, args.scenePath);
|
|
554
|
+
if (!existsSync(scenePath)) {
|
|
555
|
+
return createErrorResponse(`Scene file does not exist: ${args.scenePath}`);
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
const result = await this.context.executeOperation('read_scene', {
|
|
559
|
+
scenePath: args.scenePath,
|
|
560
|
+
}, args.projectPath);
|
|
561
|
+
const failure = this.headlessFailure('Failed to read scene', result);
|
|
562
|
+
if (failure)
|
|
563
|
+
return failure;
|
|
564
|
+
const { stdout, stderr } = result;
|
|
565
|
+
// Extract JSON from the SCENE_JSON_START/END markers
|
|
566
|
+
const startMarker = 'SCENE_JSON_START';
|
|
567
|
+
const endMarker = 'SCENE_JSON_END';
|
|
568
|
+
const startIdx = stdout.indexOf(startMarker);
|
|
569
|
+
const endIdx = stdout.indexOf(endMarker);
|
|
570
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
571
|
+
const jsonStr = stdout.substring(startIdx + startMarker.length, endIdx).trim();
|
|
572
|
+
try {
|
|
573
|
+
const parsed = JSON.parse(jsonStr);
|
|
574
|
+
return {
|
|
575
|
+
content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }],
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
return {
|
|
580
|
+
content: [{ type: 'text', text: `Raw scene data:\n${jsonStr}` }],
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
content: [{ type: 'text', text: `Scene read output:\n${stdout}\n${stderr ? 'Errors:\n' + stderr : ''}` }],
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
return createErrorResponse(`Failed to read scene: ${errorMessage(error)}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Handle the modify_scene_node tool
|
|
594
|
+
*/
|
|
595
|
+
async handleModifySceneNode(args) {
|
|
596
|
+
args = normalizeParameters(args || {});
|
|
597
|
+
if (!args.projectPath || !args.scenePath || !args.nodePath || !args.properties)
|
|
598
|
+
return createErrorResponse('projectPath, scenePath, nodePath, and properties are required.');
|
|
599
|
+
return this.sceneOperations.run('modify_node', args, { scenePath: args.scenePath, nodePath: args.nodePath, properties: args.properties });
|
|
600
|
+
}
|
|
601
|
+
async handleRemoveSceneNode(args) {
|
|
602
|
+
args = normalizeParameters(args || {});
|
|
603
|
+
if (!args.projectPath || !args.scenePath || !args.nodePath)
|
|
604
|
+
return createErrorResponse('projectPath, scenePath, and nodePath are required.');
|
|
605
|
+
return this.sceneOperations.run('remove_node', args, { scenePath: args.scenePath, nodePath: args.nodePath });
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Handle the read_project_settings tool - Parse project.godot as JSON
|
|
609
|
+
*/
|
|
610
|
+
async handleReadProjectSettings(args) {
|
|
611
|
+
return this.configuration.read(args);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Handle the modify_project_settings tool - Change a project.godot setting
|
|
615
|
+
*/
|
|
616
|
+
async handleModifyProjectSettings(args) {
|
|
617
|
+
return this.configuration.modify(args);
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Handle the list_project_files tool - List files with extension filtering
|
|
621
|
+
*/
|
|
622
|
+
async handleListProjectFiles(args) {
|
|
623
|
+
args = normalizeParameters(args || {});
|
|
624
|
+
if (!args.projectPath) {
|
|
625
|
+
return createErrorResponse('projectPath is required.');
|
|
626
|
+
}
|
|
627
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath)) {
|
|
628
|
+
return createErrorResponse('Invalid path.');
|
|
629
|
+
}
|
|
630
|
+
if (!existsSync(args.projectPath)) {
|
|
631
|
+
return createErrorResponse(`Directory does not exist: ${args.projectPath}`);
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
const baseDir = args.subdirectory
|
|
635
|
+
? this.projectRelativePath(args.projectPath, args.subdirectory)
|
|
636
|
+
: args.projectPath;
|
|
637
|
+
if (!existsSync(baseDir)) {
|
|
638
|
+
return createErrorResponse(`Subdirectory does not exist: ${args.subdirectory}`);
|
|
639
|
+
}
|
|
640
|
+
const files = [];
|
|
641
|
+
const extensions = args.extensions;
|
|
642
|
+
const scanDir = (dir, relativeTo) => {
|
|
643
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
644
|
+
for (const entry of entries) {
|
|
645
|
+
if (entry.name.startsWith('.'))
|
|
646
|
+
continue;
|
|
647
|
+
const fullPath = join(dir, entry.name);
|
|
648
|
+
const relativePath = fullPath.substring(relativeTo.length + 1).replace(/\\/g, '/');
|
|
649
|
+
if (entry.isDirectory()) {
|
|
650
|
+
scanDir(fullPath, relativeTo);
|
|
651
|
+
}
|
|
652
|
+
else if (entry.isFile()) {
|
|
653
|
+
if (extensions && extensions.length > 0) {
|
|
654
|
+
const ext = '.' + entry.name.split('.').pop();
|
|
655
|
+
if (extensions.includes(ext)) {
|
|
656
|
+
files.push(relativePath);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
files.push(relativePath);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
scanDir(baseDir, args.projectPath);
|
|
666
|
+
files.sort((left, right) => left.localeCompare(right, 'en'));
|
|
667
|
+
const cursor = args.cursor ?? 0;
|
|
668
|
+
const limit = args.limit ?? 1000;
|
|
669
|
+
const page = files.slice(cursor, cursor + limit);
|
|
670
|
+
const nextCursor = cursor + page.length < files.length ? cursor + page.length : null;
|
|
671
|
+
return {
|
|
672
|
+
content: [{
|
|
673
|
+
type: 'text',
|
|
674
|
+
text: JSON.stringify({ count: page.length, total: files.length, files: page, nextCursor }, null, 2),
|
|
675
|
+
}],
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
return createErrorResponse(`Failed to list project files: ${errorMessage(error)}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async handleAttachScript(args) {
|
|
683
|
+
args = normalizeParameters(args || {});
|
|
684
|
+
if (!args.projectPath || !args.scenePath || !args.nodePath || !args.scriptPath)
|
|
685
|
+
return createErrorResponse('projectPath, scenePath, nodePath, and scriptPath are required.');
|
|
686
|
+
return this.context.headlessOp('attach_script', args, a => ({
|
|
687
|
+
projectPath: a.projectPath,
|
|
688
|
+
params: { scenePath: a.scenePath, nodePath: a.nodePath, scriptPath: a.scriptPath },
|
|
689
|
+
}));
|
|
690
|
+
}
|
|
691
|
+
async handleCreateResource(args) {
|
|
692
|
+
args = normalizeParameters(args || {});
|
|
693
|
+
if (!args.projectPath || !args.resourceType || !args.resourcePath)
|
|
694
|
+
return createErrorResponse('projectPath, resourceType, and resourcePath are required.');
|
|
695
|
+
return this.context.headlessOp('create_resource', args, a => ({
|
|
696
|
+
projectPath: a.projectPath,
|
|
697
|
+
params: { resourceType: a.resourceType, resourcePath: a.resourcePath, ...(a.properties ? { properties: a.properties } : {}) },
|
|
698
|
+
}));
|
|
699
|
+
}
|
|
700
|
+
// --- File I/O handlers ---
|
|
701
|
+
async handleReadFile(args) {
|
|
702
|
+
return this.fileIO.read(args);
|
|
703
|
+
}
|
|
704
|
+
async handleWriteFile(args) {
|
|
705
|
+
return this.fileIO.write(args);
|
|
706
|
+
}
|
|
707
|
+
async handleDeleteFile(args) {
|
|
708
|
+
return this.fileIO.delete(args);
|
|
709
|
+
}
|
|
710
|
+
async handleCreateDirectory(args) {
|
|
711
|
+
return this.fileIO.createDirectory(args);
|
|
712
|
+
}
|
|
713
|
+
// --- Error/Log capture handlers ---
|
|
714
|
+
async handleCreateProject(args) {
|
|
715
|
+
args = normalizeParameters(args || {});
|
|
716
|
+
if (!args.projectPath || !args.projectName)
|
|
717
|
+
return createErrorResponse('projectPath and projectName are required.');
|
|
718
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath, true))
|
|
719
|
+
return createErrorResponse('Invalid path.');
|
|
720
|
+
try {
|
|
721
|
+
if (!existsSync(args.projectPath)) {
|
|
722
|
+
mkdirSync(args.projectPath, { recursive: true });
|
|
723
|
+
}
|
|
724
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
725
|
+
if (existsSync(projectFile))
|
|
726
|
+
return createErrorResponse('A project.godot already exists at this path.');
|
|
727
|
+
const isDotnet = args.dotnet === true;
|
|
728
|
+
const assemblyName = toDotnetIdentifier(args.projectName);
|
|
729
|
+
const features = generateGodotProjectFeatures(isDotnet);
|
|
730
|
+
let content = `; Engine configuration file.\n; Generated by Godot Agent Loop.\n\nconfig_version=5\n\n[application]\n\nconfig/name=${JSON.stringify(args.projectName)}\nconfig/features=${features}\n`;
|
|
731
|
+
if (isDotnet) {
|
|
732
|
+
content += `\n[dotnet]\n\nproject/assembly_name=${JSON.stringify(assemblyName)}\n`;
|
|
733
|
+
}
|
|
734
|
+
writeFileSync(projectFile, content, 'utf8');
|
|
735
|
+
if (isDotnet) {
|
|
736
|
+
const sdkVersion = (await this.context.projectSupport.detectGodotNetSdkVersion()) ?? undefined;
|
|
737
|
+
writeFileSync(this.projectRelativePath(args.projectPath, `${assemblyName}.csproj`), generateCsprojContent(args.projectName, sdkVersion), 'utf8');
|
|
738
|
+
}
|
|
739
|
+
return { content: [{ type: 'text', text: `Project "${args.projectName}" created at ${args.projectPath}${isDotnet ? ' (Godot .NET / C#)' : ''}` }] };
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
return createErrorResponse(`Failed to create project: ${errorMessage(error)}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
async handleCreateCsharpScript(args) {
|
|
746
|
+
args = normalizeParameters(args || {});
|
|
747
|
+
if (!args.projectPath || !args.scriptPath)
|
|
748
|
+
return createErrorResponse('projectPath and scriptPath are required.');
|
|
749
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath) || !validatePath(args.scriptPath))
|
|
750
|
+
return createErrorResponse('Invalid path.');
|
|
751
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
752
|
+
if (!existsSync(projectFile))
|
|
753
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
754
|
+
if (!this.context.projectSupport.isDotnetProject(args.projectPath))
|
|
755
|
+
return createErrorResponse('Not a Godot .NET project (no .csproj found). Use create_project with dotnet: true first.');
|
|
756
|
+
if (!/\.cs$/i.test(args.scriptPath))
|
|
757
|
+
return createErrorResponse('scriptPath must end with .cs');
|
|
758
|
+
const fileBase = basename(args.scriptPath).replace(/\.cs$/i, '');
|
|
759
|
+
if (!isValidCsharpIdentifier(fileBase))
|
|
760
|
+
return createErrorResponse(`Invalid C# script file name "${fileBase}.cs": the name must be a valid class name (letters, digits, underscore; not starting with a digit), because Godot requires the class name to match the file name.`);
|
|
761
|
+
if (args.className && args.className !== fileBase)
|
|
762
|
+
return createErrorResponse(`className "${args.className}" must match the script file name "${fileBase}" for Godot to attach the script.`);
|
|
763
|
+
try {
|
|
764
|
+
const fullPath = this.projectRelativePath(args.projectPath, args.scriptPath);
|
|
765
|
+
const dir = dirname(fullPath);
|
|
766
|
+
if (!existsSync(dir))
|
|
767
|
+
mkdirSync(dir, { recursive: true });
|
|
768
|
+
let source = args.source;
|
|
769
|
+
if (!source) {
|
|
770
|
+
source = generateCsharpScriptSource({
|
|
771
|
+
className: fileBase,
|
|
772
|
+
baseClass: args.baseClass,
|
|
773
|
+
namespaceName: args.namespaceName,
|
|
774
|
+
methods: Array.isArray(args.methods) ? args.methods : undefined,
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
writeFileSync(fullPath, source, 'utf8');
|
|
778
|
+
return { content: [{ type: 'text', text: `C# script created at ${args.scriptPath}` }] };
|
|
779
|
+
}
|
|
780
|
+
catch (error) {
|
|
781
|
+
return createErrorResponse(`create_csharp_script failed: ${errorMessage(error)}`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
async handleManageAutoloads(args) {
|
|
785
|
+
args = normalizeParameters(args || {});
|
|
786
|
+
if (!args.projectPath || !args.action)
|
|
787
|
+
return createErrorResponse('projectPath and action are required.');
|
|
788
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath))
|
|
789
|
+
return createErrorResponse('Invalid path.');
|
|
790
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
791
|
+
if (!existsSync(projectFile))
|
|
792
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
793
|
+
try {
|
|
794
|
+
let content = readFileSync(projectFile, 'utf8');
|
|
795
|
+
if (args.action === 'list') {
|
|
796
|
+
const autoloads = {};
|
|
797
|
+
const autoloadMatch = /\[autoload\]([\s\S]*?)(?=\n\[|$)/.exec(content);
|
|
798
|
+
if (autoloadMatch) {
|
|
799
|
+
for (const line of autoloadMatch[1].split('\n')) {
|
|
800
|
+
const kv = /^([^=]+)=(.*)$/.exec(line.trim());
|
|
801
|
+
if (kv)
|
|
802
|
+
autoloads[kv[1].trim()] = kv[2].trim();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return { content: [{ type: 'text', text: JSON.stringify(autoloads, null, 2) }] };
|
|
806
|
+
}
|
|
807
|
+
else if (args.action === 'add') {
|
|
808
|
+
if (!args.name || !args.path)
|
|
809
|
+
return createErrorResponse('name and path are required for add action.');
|
|
810
|
+
const autoloadLine = `${args.name}="*${args.path}"`;
|
|
811
|
+
if (content.includes('[autoload]')) {
|
|
812
|
+
content = content.replace('[autoload]', `[autoload]\n\n${autoloadLine}`);
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
content += `\n[autoload]\n\n${autoloadLine}\n`;
|
|
816
|
+
}
|
|
817
|
+
writeFileSync(projectFile, content, 'utf8');
|
|
818
|
+
return { content: [{ type: 'text', text: `Autoload "${args.name}" added: ${args.path}` }] };
|
|
819
|
+
}
|
|
820
|
+
else if (args.action === 'remove') {
|
|
821
|
+
if (!args.name)
|
|
822
|
+
return createErrorResponse('name is required for remove action.');
|
|
823
|
+
const pattern = new RegExp(`\\n?${args.name}\\s*=.*\\n?`, 'g');
|
|
824
|
+
content = content.replace(pattern, '\n');
|
|
825
|
+
writeFileSync(projectFile, content, 'utf8');
|
|
826
|
+
return { content: [{ type: 'text', text: `Autoload "${args.name}" removed.` }] };
|
|
827
|
+
}
|
|
828
|
+
return createErrorResponse('Invalid action. Use "list", "add", or "remove".');
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
return createErrorResponse(`Failed to manage autoloads: ${errorMessage(error)}`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async handleManageInputMap(args) {
|
|
835
|
+
args = normalizeParameters(args || {});
|
|
836
|
+
if (!args.projectPath || !args.action)
|
|
837
|
+
return createErrorResponse('projectPath and action are required.');
|
|
838
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath))
|
|
839
|
+
return createErrorResponse('Invalid path.');
|
|
840
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
841
|
+
if (!existsSync(projectFile))
|
|
842
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
843
|
+
try {
|
|
844
|
+
let content = readFileSync(projectFile, 'utf8');
|
|
845
|
+
if (args.action === 'list') {
|
|
846
|
+
const actions = {};
|
|
847
|
+
const inputMatch = /\[input\]([\s\S]*?)(?=\n\[|$)/.exec(content);
|
|
848
|
+
if (inputMatch) {
|
|
849
|
+
for (const line of inputMatch[1].split('\n')) {
|
|
850
|
+
const kv = /^([^=]+)=(.*)$/.exec(line.trim());
|
|
851
|
+
if (kv)
|
|
852
|
+
actions[kv[1].trim()] = kv[2].trim();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return { content: [{ type: 'text', text: JSON.stringify(actions, null, 2) }] };
|
|
856
|
+
}
|
|
857
|
+
else if (args.action === 'add') {
|
|
858
|
+
if (!args.actionName)
|
|
859
|
+
return createErrorResponse('actionName is required for add action.');
|
|
860
|
+
const deadzone = args.deadzone !== undefined ? args.deadzone : 0.5;
|
|
861
|
+
const keycode = args.key ? this.context.projectSupport.keyNameToScancode(args.key) : undefined;
|
|
862
|
+
const merged = mergeInputMapAction(content, args.actionName, deadzone, keycode);
|
|
863
|
+
content = merged.content;
|
|
864
|
+
writeFileSync(projectFile, content, 'utf8');
|
|
865
|
+
const outcome = merged.existed ? (merged.eventAdded ? 'updated with key binding' : 'already configured') : 'added';
|
|
866
|
+
return { content: [{ type: 'text', text: `Input action "${args.actionName}" ${outcome}.` }] };
|
|
867
|
+
}
|
|
868
|
+
else if (args.action === 'remove') {
|
|
869
|
+
if (!args.actionName)
|
|
870
|
+
return createErrorResponse('actionName is required for remove action.');
|
|
871
|
+
const pattern = new RegExp(`\\n?${args.actionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=.*\\n?`, 'g');
|
|
872
|
+
content = content.replace(pattern, '\n');
|
|
873
|
+
writeFileSync(projectFile, content, 'utf8');
|
|
874
|
+
return { content: [{ type: 'text', text: `Input action "${args.actionName}" removed.` }] };
|
|
875
|
+
}
|
|
876
|
+
return createErrorResponse('Invalid action. Use "list", "add", or "remove".');
|
|
877
|
+
}
|
|
878
|
+
catch (error) {
|
|
879
|
+
return createErrorResponse(`Failed to manage input map: ${errorMessage(error)}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
async handleManageExportPresets(args) {
|
|
883
|
+
args = normalizeParameters(args || {});
|
|
884
|
+
if (!args.projectPath || !args.action)
|
|
885
|
+
return createErrorResponse('projectPath and action are required.');
|
|
886
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath))
|
|
887
|
+
return createErrorResponse('Invalid path.');
|
|
888
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
889
|
+
if (!existsSync(projectFile))
|
|
890
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
891
|
+
const presetsFile = join(args.projectPath, 'export_presets.cfg');
|
|
892
|
+
try {
|
|
893
|
+
if (args.action === 'list') {
|
|
894
|
+
if (!existsSync(presetsFile))
|
|
895
|
+
return { content: [{ type: 'text', text: JSON.stringify({ presets: [] }, null, 2) }] };
|
|
896
|
+
const content = readFileSync(presetsFile, 'utf8');
|
|
897
|
+
const presets = [];
|
|
898
|
+
const nameMatches = content.matchAll(/name="([^"]+)"/g);
|
|
899
|
+
const platformMatches = content.matchAll(/platform="([^"]+)"/g);
|
|
900
|
+
const names = [...nameMatches].map(m => m[1]);
|
|
901
|
+
const platforms = [...platformMatches].map(m => m[1]);
|
|
902
|
+
for (let i = 0; i < names.length; i++) {
|
|
903
|
+
presets.push({ name: names[i], platform: platforms[i] || 'unknown' });
|
|
904
|
+
}
|
|
905
|
+
return { content: [{ type: 'text', text: JSON.stringify({ presets }, null, 2) }] };
|
|
906
|
+
}
|
|
907
|
+
else if (args.action === 'add') {
|
|
908
|
+
if (!args.name || !args.platform)
|
|
909
|
+
return createErrorResponse('name and platform are required for add action.');
|
|
910
|
+
const runnable = args.runnable ? 'true' : 'false';
|
|
911
|
+
const presetBlock = `\n[preset.${Date.now()}]\n\nname="${args.name}"\nplatform="${args.platform}"\nrunnable=${runnable}\n`;
|
|
912
|
+
let content = existsSync(presetsFile) ? readFileSync(presetsFile, 'utf8') : '';
|
|
913
|
+
content += presetBlock;
|
|
914
|
+
writeFileSync(presetsFile, content, 'utf8');
|
|
915
|
+
return { content: [{ type: 'text', text: `Export preset "${args.name}" added for platform "${args.platform}".` }] };
|
|
916
|
+
}
|
|
917
|
+
else if (args.action === 'remove') {
|
|
918
|
+
if (!args.name)
|
|
919
|
+
return createErrorResponse('name is required for remove action.');
|
|
920
|
+
if (!existsSync(presetsFile))
|
|
921
|
+
return createErrorResponse('No export_presets.cfg file found.');
|
|
922
|
+
let content = readFileSync(presetsFile, 'utf8');
|
|
923
|
+
// Remove the preset section containing the given name
|
|
924
|
+
const pattern = new RegExp(`\\[preset\\.[^\\]]+\\]\\s*\\n[\\s\\S]*?name="${args.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"[\\s\\S]*?(?=\\[preset\\.|$)`, 'g');
|
|
925
|
+
content = content.replace(pattern, '');
|
|
926
|
+
writeFileSync(presetsFile, content, 'utf8');
|
|
927
|
+
return { content: [{ type: 'text', text: `Export preset "${args.name}" removed.` }] };
|
|
928
|
+
}
|
|
929
|
+
return createErrorResponse('Invalid action. Use "list", "add", or "remove".');
|
|
930
|
+
}
|
|
931
|
+
catch (error) {
|
|
932
|
+
return createErrorResponse(`Failed to manage export presets: ${errorMessage(error)}`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
// --- Advanced runtime handlers ---
|
|
936
|
+
async handleExportProject(args) {
|
|
937
|
+
return this.exportService.export(args);
|
|
938
|
+
}
|
|
939
|
+
async handleRunProjectTests(args) {
|
|
940
|
+
if (args.action !== 'discover' && args.action !== 'run') {
|
|
941
|
+
return createErrorResponse('action must be discover or run.');
|
|
942
|
+
}
|
|
943
|
+
return this.projectTests.execute(args);
|
|
944
|
+
}
|
|
945
|
+
async handleManageImportPipeline(args) {
|
|
946
|
+
if (!['inspect', 'change', 'reimport', 'dependencies'].includes(String(args.action))) {
|
|
947
|
+
return createErrorResponse('action must be inspect, change, reimport, or dependencies.');
|
|
948
|
+
}
|
|
949
|
+
return this.importPipeline.execute(args);
|
|
950
|
+
}
|
|
951
|
+
async handleAnalyzeProjectIntegrity(args) {
|
|
952
|
+
if (!['analyze', 'preview_rename', 'assets', 'localization', 'accessibility', 'extensions', 'leaks'].includes(args.action)) {
|
|
953
|
+
return createErrorResponse('action must be analyze, preview_rename, assets, localization, accessibility, extensions, or leaks.');
|
|
954
|
+
}
|
|
955
|
+
return this.projectIntegrity.execute(args);
|
|
956
|
+
}
|
|
957
|
+
async handleVerifyExportReadiness(args) {
|
|
958
|
+
if (args.action !== 'inspect' && args.action !== 'export_smoke') {
|
|
959
|
+
return createErrorResponse('action must be inspect or export_smoke.');
|
|
960
|
+
}
|
|
961
|
+
return this.exportReadiness.execute(args);
|
|
962
|
+
}
|
|
963
|
+
async handleVerifyDotnetProject(args) {
|
|
964
|
+
if (!['inspect', 'restore', 'build', 'run'].includes(String(args.action))) {
|
|
965
|
+
return createErrorResponse('action must be inspect, restore, build, or run.');
|
|
966
|
+
}
|
|
967
|
+
return this.dotnetWorkflow.execute(args);
|
|
968
|
+
}
|
|
969
|
+
async handleManageAddon(args) {
|
|
970
|
+
if (!['inspect', 'install', 'update', 'remove', 'enable', 'disable'].includes(String(args.action))) {
|
|
971
|
+
return createErrorResponse('action must be inspect, install, update, remove, enable, or disable.');
|
|
972
|
+
}
|
|
973
|
+
return this.addonManagement.execute(args);
|
|
974
|
+
}
|
|
975
|
+
async handleRenameFile(args) {
|
|
976
|
+
return this.fileIO.rename(args);
|
|
977
|
+
}
|
|
978
|
+
async handleManageResource(args) {
|
|
979
|
+
args = normalizeParameters(args || {});
|
|
980
|
+
if (!args.projectPath || !args.resourcePath || !args.action)
|
|
981
|
+
return createErrorResponse('projectPath, resourcePath, and action are required.');
|
|
982
|
+
return this.context.headlessOp('manage_resource', args, a => ({
|
|
983
|
+
projectPath: a.projectPath,
|
|
984
|
+
params: { resourcePath: a.resourcePath, action: a.action, ...(a.properties ? { properties: a.properties } : {}) },
|
|
985
|
+
}));
|
|
986
|
+
}
|
|
987
|
+
async handleValidateScript(args) {
|
|
988
|
+
return this.scriptValidation.validate(args);
|
|
989
|
+
}
|
|
990
|
+
async handleValidateScripts(args) {
|
|
991
|
+
args = normalizeParameters(args || {});
|
|
992
|
+
if (!args.projectPath)
|
|
993
|
+
return createErrorResponse('projectPath is required.');
|
|
994
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath))
|
|
995
|
+
return createErrorResponse('Invalid path.');
|
|
996
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
997
|
+
if (!existsSync(projectFile))
|
|
998
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
999
|
+
if (!this.context.getGodotPath()) {
|
|
1000
|
+
await this.context.detectGodotPath();
|
|
1001
|
+
if (!this.context.getGodotPath())
|
|
1002
|
+
return createErrorResponse('Could not find a valid Godot executable path');
|
|
1003
|
+
}
|
|
1004
|
+
let scope;
|
|
1005
|
+
let candidates;
|
|
1006
|
+
const explicit = Array.isArray(args.scriptPaths) && args.scriptPaths.length > 0;
|
|
1007
|
+
if (explicit) {
|
|
1008
|
+
scope = 'explicit';
|
|
1009
|
+
candidates = args.scriptPaths.map((p) => String(p));
|
|
1010
|
+
}
|
|
1011
|
+
else if (args.scope === undefined || args.scope === 'changed') {
|
|
1012
|
+
scope = 'changed';
|
|
1013
|
+
const changed = await this.context.projectSupport.listChangedGdFiles(args.projectPath);
|
|
1014
|
+
if (changed.error)
|
|
1015
|
+
return createErrorResponse(changed.error);
|
|
1016
|
+
candidates = changed.files;
|
|
1017
|
+
}
|
|
1018
|
+
else if (args.scope === 'all') {
|
|
1019
|
+
scope = 'all';
|
|
1020
|
+
candidates = this.context.projectSupport.listAllGdFiles(args.projectPath);
|
|
1021
|
+
}
|
|
1022
|
+
else {
|
|
1023
|
+
return createErrorResponse(`Invalid scope "${args.scope}". Use "changed" or "all", or pass scriptPaths.`);
|
|
1024
|
+
}
|
|
1025
|
+
const results = [];
|
|
1026
|
+
let filesWithErrors = 0;
|
|
1027
|
+
const toCheck = [];
|
|
1028
|
+
for (const rel of candidates) {
|
|
1029
|
+
if (!/\.gd$/i.test(rel) || !validatePath(rel)) {
|
|
1030
|
+
if (explicit)
|
|
1031
|
+
results.push({ scriptPath: rel, checked: false, error: 'Not a valid .gd path' });
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
if (!existsSync(join(args.projectPath, rel))) {
|
|
1035
|
+
if (explicit)
|
|
1036
|
+
results.push({ scriptPath: rel, checked: false, error: 'Script does not exist' });
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
toCheck.push(rel);
|
|
1040
|
+
}
|
|
1041
|
+
const MAX_BATCH = 60;
|
|
1042
|
+
if (toCheck.length > MAX_BATCH)
|
|
1043
|
+
return createErrorResponse(`Too many scripts to validate (${toCheck.length} > ${MAX_BATCH}). Narrow the scope or pass an explicit scriptPaths list.`);
|
|
1044
|
+
for (const rel of toCheck) {
|
|
1045
|
+
const check = await this.context.projectSupport.runGdScriptCheck(args.projectPath, join(args.projectPath, rel));
|
|
1046
|
+
if (!check.completed) {
|
|
1047
|
+
results.push({ scriptPath: rel, checked: false, error: check.error });
|
|
1048
|
+
}
|
|
1049
|
+
else {
|
|
1050
|
+
if (check.errors.length > 0)
|
|
1051
|
+
filesWithErrors++;
|
|
1052
|
+
results.push({ scriptPath: rel, checked: true, valid: check.errors.length === 0, errorCount: check.errors.length, errors: check.errors });
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
content: [
|
|
1057
|
+
{
|
|
1058
|
+
type: 'text',
|
|
1059
|
+
text: JSON.stringify({
|
|
1060
|
+
scope,
|
|
1061
|
+
fileCount: results.length,
|
|
1062
|
+
filesWithErrors,
|
|
1063
|
+
allValid: filesWithErrors === 0 && results.every(r => r.checked),
|
|
1064
|
+
results,
|
|
1065
|
+
}, null, 2),
|
|
1066
|
+
},
|
|
1067
|
+
],
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
async handleCreateScript(args) {
|
|
1071
|
+
args = normalizeParameters(args || {});
|
|
1072
|
+
if (!args.projectPath || !args.scriptPath)
|
|
1073
|
+
return createErrorResponse('projectPath and scriptPath are required.');
|
|
1074
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath) || !validatePath(args.scriptPath))
|
|
1075
|
+
return createErrorResponse('Invalid path.');
|
|
1076
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
1077
|
+
if (!existsSync(projectFile))
|
|
1078
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
1079
|
+
try {
|
|
1080
|
+
const fullPath = this.projectRelativePath(args.projectPath, args.scriptPath);
|
|
1081
|
+
const dir = dirname(fullPath);
|
|
1082
|
+
if (!existsSync(dir))
|
|
1083
|
+
mkdirSync(dir, { recursive: true });
|
|
1084
|
+
let source = args.source;
|
|
1085
|
+
if (!source) {
|
|
1086
|
+
const ext = args.extends || 'Node';
|
|
1087
|
+
const lines = [`extends ${ext}`, ''];
|
|
1088
|
+
if (args.className)
|
|
1089
|
+
lines.splice(1, 0, `class_name ${args.className}`);
|
|
1090
|
+
if (args.methods && Array.isArray(args.methods)) {
|
|
1091
|
+
for (const m of args.methods) {
|
|
1092
|
+
lines.push('', `func ${m}():`);
|
|
1093
|
+
lines.push('\tpass');
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
source = lines.join('\n') + '\n';
|
|
1097
|
+
}
|
|
1098
|
+
writeFileSync(fullPath, source, 'utf8');
|
|
1099
|
+
return { content: [{ type: 'text', text: `Script created at ${args.scriptPath}` }] };
|
|
1100
|
+
}
|
|
1101
|
+
catch (error) {
|
|
1102
|
+
return createErrorResponse(`create_script failed: ${errorMessage(error)}`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
async handleManageSceneSignals(args) {
|
|
1106
|
+
args = normalizeParameters(args || {});
|
|
1107
|
+
if (!args.projectPath || !args.scenePath || !args.action)
|
|
1108
|
+
return createErrorResponse('projectPath, scenePath, and action are required.');
|
|
1109
|
+
return this.sceneOperations.run('manage_scene_signals', args, {
|
|
1110
|
+
scenePath: args.scenePath, action: args.action,
|
|
1111
|
+
...(args.signalName ? { signalName: args.signalName } : {}),
|
|
1112
|
+
...(args.sourcePath ? { sourcePath: args.sourcePath } : {}),
|
|
1113
|
+
...(args.targetPath ? { targetPath: args.targetPath } : {}),
|
|
1114
|
+
...(args.method ? { method: args.method } : {}),
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
async handleManageLayers(args) {
|
|
1118
|
+
args = normalizeParameters(args || {});
|
|
1119
|
+
if (!args.projectPath || !args.action)
|
|
1120
|
+
return createErrorResponse('projectPath and action are required.');
|
|
1121
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath))
|
|
1122
|
+
return createErrorResponse('Invalid path.');
|
|
1123
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
1124
|
+
if (!existsSync(projectFile))
|
|
1125
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
1126
|
+
try {
|
|
1127
|
+
let content = readFileSync(projectFile, 'utf8');
|
|
1128
|
+
// Keys live inside [layer_names] as `<type>/layer_<n>`, so the section
|
|
1129
|
+
// header supplies the `layer_names/` prefix of the engine setting path.
|
|
1130
|
+
// Older versions wrote the prefix into the key as well, producing
|
|
1131
|
+
// `layer_names/layer_names/...`, which the engine never resolved; those
|
|
1132
|
+
// stale keys are still listed and are repaired by the next `set`.
|
|
1133
|
+
const sectionLayers = () => {
|
|
1134
|
+
const section = /\[layer_names\]([\s\S]*?)(?=\n\[|$)/.exec(content);
|
|
1135
|
+
if (!section)
|
|
1136
|
+
return [];
|
|
1137
|
+
const layers = [];
|
|
1138
|
+
const entry = /^(?:layer_names\/)?([\w_]+)\/layer_(\d+)\s*=\s*"([^"]*)"$/;
|
|
1139
|
+
for (const line of section[1].split('\n')) {
|
|
1140
|
+
const match = entry.exec(line.trim());
|
|
1141
|
+
if (match)
|
|
1142
|
+
layers.push({ type: match[1], layer: parseInt(match[2]), name: match[3] });
|
|
1143
|
+
}
|
|
1144
|
+
return layers;
|
|
1145
|
+
};
|
|
1146
|
+
if (args.action === 'list') {
|
|
1147
|
+
return { content: [{ type: 'text', text: JSON.stringify({ layers: sectionLayers() }, null, 2) }] };
|
|
1148
|
+
}
|
|
1149
|
+
else if (args.action === 'set') {
|
|
1150
|
+
if (!args.layerType || !args.layer || !args.name)
|
|
1151
|
+
return createErrorResponse('layerType, layer, and name are required for set.');
|
|
1152
|
+
const key = `${args.layerType}/layer_${args.layer}`;
|
|
1153
|
+
const settingLine = `${key}="${args.name}"`;
|
|
1154
|
+
const existingRegex = new RegExp(`^(?:layer_names\\/)?${key.replace(/\//g, '\\/')}\\s*=\\s*"[^"]*"$`, 'm');
|
|
1155
|
+
if (existingRegex.test(content)) {
|
|
1156
|
+
content = content.replace(existingRegex, settingLine);
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1159
|
+
if (!content.includes('[layer_names]'))
|
|
1160
|
+
content += '\n[layer_names]\n';
|
|
1161
|
+
content = content.replace('[layer_names]', `[layer_names]\n${settingLine}`);
|
|
1162
|
+
}
|
|
1163
|
+
writeFileSync(projectFile, content, 'utf8');
|
|
1164
|
+
return { content: [{ type: 'text', text: `Layer set: layer_names/${settingLine}` }] };
|
|
1165
|
+
}
|
|
1166
|
+
return createErrorResponse(`Unknown action: ${args.action}`);
|
|
1167
|
+
}
|
|
1168
|
+
catch (error) {
|
|
1169
|
+
return createErrorResponse(`manage_layers failed: ${errorMessage(error)}`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
async handleManagePlugins(args) {
|
|
1173
|
+
args = normalizeParameters(args || {});
|
|
1174
|
+
if (!args.projectPath || !args.action)
|
|
1175
|
+
return createErrorResponse('projectPath and action are required.');
|
|
1176
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath))
|
|
1177
|
+
return createErrorResponse('Invalid path.');
|
|
1178
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
1179
|
+
if (!existsSync(projectFile))
|
|
1180
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
1181
|
+
try {
|
|
1182
|
+
let content = readFileSync(projectFile, 'utf8');
|
|
1183
|
+
if (args.action === 'list') {
|
|
1184
|
+
const enabledSetting = /\[editor_plugins\][\s\S]*?^enabled=PackedStringArray\(([^\n]*)\)/m.exec(content)?.[1] ?? '';
|
|
1185
|
+
const plugins = [...enabledSetting.matchAll(/"res:\/\/addons\/([^/]+)\/plugin\.cfg"/g)]
|
|
1186
|
+
.map(match => match[1]);
|
|
1187
|
+
const addonsDir = join(args.projectPath, 'addons');
|
|
1188
|
+
const available = [];
|
|
1189
|
+
if (existsSync(addonsDir)) {
|
|
1190
|
+
const entries = readdirSync(addonsDir, { withFileTypes: true });
|
|
1191
|
+
for (const e of entries) {
|
|
1192
|
+
if (e.isDirectory())
|
|
1193
|
+
available.push(e.name);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return { content: [{ type: 'text', text: JSON.stringify({ enabled: plugins, available }, null, 2) }] };
|
|
1197
|
+
}
|
|
1198
|
+
else if (args.action === 'enable' || args.action === 'disable') {
|
|
1199
|
+
if (!args.pluginName)
|
|
1200
|
+
return createErrorResponse('pluginName is required.');
|
|
1201
|
+
const pluginPath = `res://addons/${args.pluginName}/plugin.cfg`;
|
|
1202
|
+
const sectionPattern = /\[editor_plugins\][\s\S]*?(?=\n\[|$)/;
|
|
1203
|
+
const section = sectionPattern.exec(content);
|
|
1204
|
+
const current = section
|
|
1205
|
+
? [...section[0].matchAll(/"(res:\/\/addons\/[^"]+\/plugin\.cfg)"/g)].map(match => match[1])
|
|
1206
|
+
: [];
|
|
1207
|
+
const enabled = current.filter(value => value !== pluginPath);
|
|
1208
|
+
if (args.action === 'enable')
|
|
1209
|
+
enabled.push(pluginPath);
|
|
1210
|
+
const setting = `enabled=PackedStringArray(${[...new Set(enabled)].sort().map(value => JSON.stringify(value)).join(', ')})`;
|
|
1211
|
+
if (!section)
|
|
1212
|
+
content += `\n[editor_plugins]\n\n${setting}\n`;
|
|
1213
|
+
else {
|
|
1214
|
+
const replacement = /^enabled=.*$/m.test(section[0])
|
|
1215
|
+
? section[0].replace(/^enabled=.*$/m, setting)
|
|
1216
|
+
: `${section[0].trimEnd()}\n${setting}\n`;
|
|
1217
|
+
content = content.slice(0, section.index) + replacement + content.slice(section.index + section[0].length);
|
|
1218
|
+
}
|
|
1219
|
+
writeFileSync(projectFile, content, 'utf8');
|
|
1220
|
+
return { content: [{ type: 'text', text: `Plugin ${args.pluginName} ${args.action}d.` }] };
|
|
1221
|
+
}
|
|
1222
|
+
return createErrorResponse(`Unknown action: ${args.action}`);
|
|
1223
|
+
}
|
|
1224
|
+
catch (error) {
|
|
1225
|
+
return createErrorResponse(`manage_plugins failed: ${errorMessage(error)}`);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
async handleManageShader(args) {
|
|
1229
|
+
args = normalizeParameters(args || {});
|
|
1230
|
+
if (!args.projectPath || !args.shaderPath || !args.action)
|
|
1231
|
+
return createErrorResponse('projectPath, shaderPath, and action are required.');
|
|
1232
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath) || !validatePath(args.shaderPath))
|
|
1233
|
+
return createErrorResponse('Invalid path.');
|
|
1234
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
1235
|
+
if (!existsSync(projectFile))
|
|
1236
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
1237
|
+
const fullPath = this.projectRelativePath(args.projectPath, args.shaderPath);
|
|
1238
|
+
try {
|
|
1239
|
+
if (args.action === 'read') {
|
|
1240
|
+
if (!existsSync(fullPath))
|
|
1241
|
+
return createErrorResponse(`Shader not found: ${args.shaderPath}`);
|
|
1242
|
+
const source = readFileSync(fullPath, 'utf8');
|
|
1243
|
+
return { content: [{ type: 'text', text: source }] };
|
|
1244
|
+
}
|
|
1245
|
+
else if (args.action === 'create') {
|
|
1246
|
+
const dir = dirname(fullPath);
|
|
1247
|
+
if (!existsSync(dir))
|
|
1248
|
+
mkdirSync(dir, { recursive: true });
|
|
1249
|
+
let source = args.source;
|
|
1250
|
+
if (!source) {
|
|
1251
|
+
const type = args.shaderType || 'spatial';
|
|
1252
|
+
source = `shader_type ${type};\n\nvoid fragment() {\n\t// Called for every pixel the material is visible on.\n}\n`;
|
|
1253
|
+
}
|
|
1254
|
+
writeFileSync(fullPath, source, 'utf8');
|
|
1255
|
+
return { content: [{ type: 'text', text: `Shader created at ${args.shaderPath}` }] };
|
|
1256
|
+
}
|
|
1257
|
+
return createErrorResponse(`Unknown action: ${args.action}`);
|
|
1258
|
+
}
|
|
1259
|
+
catch (error) {
|
|
1260
|
+
return createErrorResponse(`manage_shader failed: ${errorMessage(error)}`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
async handleManageThemeResource(args) {
|
|
1264
|
+
args = normalizeParameters(args || {});
|
|
1265
|
+
if (!args.projectPath || !args.resourcePath || !args.action)
|
|
1266
|
+
return createErrorResponse('projectPath, resourcePath, and action are required.');
|
|
1267
|
+
return this.context.headlessOp('manage_theme_resource', args, a => ({
|
|
1268
|
+
projectPath: a.projectPath,
|
|
1269
|
+
params: { resourcePath: a.resourcePath, action: a.action, ...(a.properties ? { properties: a.properties } : {}) },
|
|
1270
|
+
}));
|
|
1271
|
+
}
|
|
1272
|
+
async handleSetMainScene(args) {
|
|
1273
|
+
args = normalizeParameters(args || {});
|
|
1274
|
+
if (!args.projectPath || !args.scenePath)
|
|
1275
|
+
return createErrorResponse('projectPath and scenePath are required.');
|
|
1276
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath))
|
|
1277
|
+
return createErrorResponse('Invalid path.');
|
|
1278
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
1279
|
+
if (!existsSync(projectFile))
|
|
1280
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
1281
|
+
try {
|
|
1282
|
+
let content = readFileSync(projectFile, 'utf8');
|
|
1283
|
+
const resPath = args.scenePath.startsWith('res://') ? args.scenePath : `res://${args.scenePath}`;
|
|
1284
|
+
const settingLine = `run/main_scene="${resPath}"`;
|
|
1285
|
+
const existingRegex = /run\/main_scene="[^"]*"/;
|
|
1286
|
+
if (existingRegex.test(content)) {
|
|
1287
|
+
content = content.replace(existingRegex, settingLine);
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
if (content.includes('[application]')) {
|
|
1291
|
+
content = content.replace('[application]', `[application]\n\n${settingLine}`);
|
|
1292
|
+
}
|
|
1293
|
+
else {
|
|
1294
|
+
content += `\n[application]\n\n${settingLine}\n`;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
writeFileSync(projectFile, content, 'utf8');
|
|
1298
|
+
return { content: [{ type: 'text', text: `Main scene set to ${resPath}` }] };
|
|
1299
|
+
}
|
|
1300
|
+
catch (error) {
|
|
1301
|
+
return createErrorResponse(`set_main_scene failed: ${errorMessage(error)}`);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
async handleManageSceneStructure(args) {
|
|
1305
|
+
args = normalizeParameters(args || {});
|
|
1306
|
+
if (!args.projectPath || !args.scenePath || !args.action || !args.nodePath)
|
|
1307
|
+
return createErrorResponse('projectPath, scenePath, action, and nodePath are required.');
|
|
1308
|
+
return this.sceneOperations.run('manage_scene_structure', args, {
|
|
1309
|
+
scenePath: args.scenePath, action: args.action, nodePath: args.nodePath,
|
|
1310
|
+
...(args.newName ? { newName: args.newName } : {}),
|
|
1311
|
+
...(args.newParentPath ? { newParentPath: args.newParentPath } : {}),
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
async handleManageTranslations(args) {
|
|
1315
|
+
args = normalizeParameters(args || {});
|
|
1316
|
+
if (!args.projectPath || !args.action)
|
|
1317
|
+
return createErrorResponse('projectPath and action are required.');
|
|
1318
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath))
|
|
1319
|
+
return createErrorResponse('Invalid path.');
|
|
1320
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
1321
|
+
if (!existsSync(projectFile))
|
|
1322
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
1323
|
+
try {
|
|
1324
|
+
let content = readFileSync(projectFile, 'utf8');
|
|
1325
|
+
// The engine setting is `internationalization/locale/translations`, so the
|
|
1326
|
+
// key inside [internationalization] must be `locale/translations`. A bare
|
|
1327
|
+
// `translations=` key is never read by the engine; it is still matched here
|
|
1328
|
+
// so projects written by older versions list correctly and are repaired on
|
|
1329
|
+
// the next add.
|
|
1330
|
+
const listRegex = /^(?:locale\/)?translations=PackedStringArray\(([^)]*)\)$/m;
|
|
1331
|
+
if (args.action === 'list') {
|
|
1332
|
+
const match = listRegex.exec(content);
|
|
1333
|
+
const translations = match ? match[1].split(',').map(s => s.trim().replace(/"/g, '')).filter(Boolean) : [];
|
|
1334
|
+
return { content: [{ type: 'text', text: JSON.stringify({ translations }, null, 2) }] };
|
|
1335
|
+
}
|
|
1336
|
+
else if (args.action === 'add') {
|
|
1337
|
+
if (!args.translationPath)
|
|
1338
|
+
return createErrorResponse('translationPath is required.');
|
|
1339
|
+
const resPath = args.translationPath.startsWith('res://') ? args.translationPath : `res://${args.translationPath}`;
|
|
1340
|
+
const match = listRegex.exec(content);
|
|
1341
|
+
if (match) {
|
|
1342
|
+
const existing = match[1];
|
|
1343
|
+
const newVal = existing ? `${existing}, "${resPath}"` : `"${resPath}"`;
|
|
1344
|
+
content = content.replace(listRegex, `locale/translations=PackedStringArray(${newVal})`);
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
if (!content.includes('[internationalization]'))
|
|
1348
|
+
content += '\n[internationalization]\n';
|
|
1349
|
+
content = content.replace('[internationalization]', `[internationalization]\n\nlocale/translations=PackedStringArray("${resPath}")`);
|
|
1350
|
+
}
|
|
1351
|
+
writeFileSync(projectFile, content, 'utf8');
|
|
1352
|
+
return { content: [{ type: 'text', text: `Translation added: ${resPath}` }] };
|
|
1353
|
+
}
|
|
1354
|
+
else if (args.action === 'remove') {
|
|
1355
|
+
if (!args.translationPath)
|
|
1356
|
+
return createErrorResponse('translationPath is required.');
|
|
1357
|
+
const resPath = args.translationPath.startsWith('res://') ? args.translationPath : `res://${args.translationPath}`;
|
|
1358
|
+
content = content.replace(new RegExp(`,?\\s*"${resPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`), '');
|
|
1359
|
+
writeFileSync(projectFile, content, 'utf8');
|
|
1360
|
+
return { content: [{ type: 'text', text: `Translation removed: ${resPath}` }] };
|
|
1361
|
+
}
|
|
1362
|
+
return createErrorResponse(`Unknown action: ${args.action}`);
|
|
1363
|
+
}
|
|
1364
|
+
catch (error) {
|
|
1365
|
+
return createErrorResponse(`manage_translations failed: ${errorMessage(error)}`);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
async handleManageCiPipeline(args) {
|
|
1369
|
+
args = normalizeParameters(args || {});
|
|
1370
|
+
if (!args.projectPath || !args.action)
|
|
1371
|
+
return createErrorResponse('projectPath and action are required.');
|
|
1372
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath))
|
|
1373
|
+
return createErrorResponse('Invalid path.');
|
|
1374
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
1375
|
+
if (!existsSync(projectFile))
|
|
1376
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
1377
|
+
const workflowDir = join(args.projectPath, '.github', 'workflows');
|
|
1378
|
+
const workflowPath = join(workflowDir, 'godot-export.yml');
|
|
1379
|
+
try {
|
|
1380
|
+
if (args.action === 'read') {
|
|
1381
|
+
if (!existsSync(workflowPath))
|
|
1382
|
+
return createErrorResponse('No workflow file found at .github/workflows/godot-export.yml');
|
|
1383
|
+
const content = readFileSync(workflowPath, 'utf8');
|
|
1384
|
+
return { content: [{ type: 'text', text: content }] };
|
|
1385
|
+
}
|
|
1386
|
+
else if (args.action === 'create') {
|
|
1387
|
+
const godotVersion = args.godotVersion || '4.3-stable';
|
|
1388
|
+
const versionError = validateGodotExportVersion(godotVersion);
|
|
1389
|
+
if (versionError)
|
|
1390
|
+
return createErrorResponse(versionError);
|
|
1391
|
+
const platforms = args.platforms || ['linux'];
|
|
1392
|
+
if (!Array.isArray(platforms) || platforms.length === 0 || !platforms.every(platform => typeof platform === 'string' && CI_EXPORT_PLATFORMS.has(platform))) {
|
|
1393
|
+
return createErrorResponse('platforms must be a non-empty array containing only: windows, linux, macos, web.');
|
|
1394
|
+
}
|
|
1395
|
+
if (new Set(platforms).size !== platforms.length) {
|
|
1396
|
+
return createErrorResponse('platforms must not contain duplicates.');
|
|
1397
|
+
}
|
|
1398
|
+
if (!existsSync(workflowDir))
|
|
1399
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
1400
|
+
const exportSteps = platforms.map((p) => ` - name: Export ${p}\n run: godot --headless --export-release "${p}" build/${p}/game`).join('\n');
|
|
1401
|
+
const workflow = `name: Godot Export\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\njobs:\n export:\n runs-on: ubuntu-latest\n container:\n image: barichello/godot-ci:${godotVersion}\n steps:\n - uses: actions/checkout@v4\n - name: Setup export templates\n run: |\n mkdir -p ~/.local/share/godot/export_templates/${godotVersion}\n mv /root/.local/share/godot/export_templates/${godotVersion}/* ~/.local/share/godot/export_templates/${godotVersion}/ || true\n${exportSteps}\n - uses: actions/upload-artifact@v4\n with:\n name: game-builds\n path: build/\n`;
|
|
1402
|
+
writeFileSync(workflowPath, workflow, 'utf8');
|
|
1403
|
+
return { content: [{ type: 'text', text: `CI pipeline created at .github/workflows/godot-export.yml for platforms: ${platforms.join(', ')}` }] };
|
|
1404
|
+
}
|
|
1405
|
+
return createErrorResponse(`Unknown action: ${args.action}`);
|
|
1406
|
+
}
|
|
1407
|
+
catch (error) {
|
|
1408
|
+
return createErrorResponse(`manage_ci_pipeline failed: ${errorMessage(error)}`);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
async handleManageDockerExport(args) {
|
|
1412
|
+
args = normalizeParameters(args || {});
|
|
1413
|
+
if (!args.projectPath || !args.action)
|
|
1414
|
+
return createErrorResponse('projectPath and action are required.');
|
|
1415
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath))
|
|
1416
|
+
return createErrorResponse('Invalid path.');
|
|
1417
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
1418
|
+
if (!existsSync(projectFile))
|
|
1419
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
1420
|
+
const dockerfilePath = join(args.projectPath, 'Dockerfile');
|
|
1421
|
+
try {
|
|
1422
|
+
if (args.action === 'read') {
|
|
1423
|
+
if (!existsSync(dockerfilePath))
|
|
1424
|
+
return createErrorResponse('No Dockerfile found in project root.');
|
|
1425
|
+
const content = readFileSync(dockerfilePath, 'utf8');
|
|
1426
|
+
return { content: [{ type: 'text', text: content }] };
|
|
1427
|
+
}
|
|
1428
|
+
else if (args.action === 'create') {
|
|
1429
|
+
const godotVersion = args.godotVersion || '4.3-stable';
|
|
1430
|
+
const baseImage = args.baseImage || 'ubuntu:22.04';
|
|
1431
|
+
let exportPreset = args.exportPreset || 'Linux/X11';
|
|
1432
|
+
const versionError = validateGodotExportVersion(godotVersion);
|
|
1433
|
+
if (versionError)
|
|
1434
|
+
return createErrorResponse(versionError);
|
|
1435
|
+
if (typeof baseImage !== 'string' || !DOCKER_BASE_IMAGES.has(baseImage)) {
|
|
1436
|
+
return createErrorResponse('baseImage must be one of: ubuntu:22.04, ubuntu:24.04.');
|
|
1437
|
+
}
|
|
1438
|
+
if (typeof exportPreset !== 'string' || !EXPORT_PRESET_NAME.test(exportPreset)) {
|
|
1439
|
+
return createErrorResponse('exportPreset may contain only letters, digits, spaces, _, ., /, and - (up to 128 characters).');
|
|
1440
|
+
}
|
|
1441
|
+
const exportPresetName = exportPreset;
|
|
1442
|
+
exportPreset = JSON.stringify(exportPreset).slice(1, -1);
|
|
1443
|
+
const dockerfile = `FROM ${baseImage}\n\nARG GODOT_VERSION=${godotVersion}\n\nRUN apt-get update && apt-get install -y \\\n wget unzip ca-certificates \\\n && rm -rf /var/lib/apt/lists/*\n\nRUN wget -q https://github.com/godotengine/godot/releases/download/\${GODOT_VERSION}/Godot_v\${GODOT_VERSION}_linux.x86_64.zip \\\n && unzip Godot_v\${GODOT_VERSION}_linux.x86_64.zip \\\n && mv Godot_v\${GODOT_VERSION}_linux.x86_64 /usr/local/bin/godot \\\n && rm Godot_v\${GODOT_VERSION}_linux.x86_64.zip\n\nRUN wget -q https://github.com/godotengine/godot/releases/download/\${GODOT_VERSION}/Godot_v\${GODOT_VERSION}_export_templates.tpz \\\n && mkdir -p /root/.local/share/godot/export_templates/\${GODOT_VERSION} \\\n && unzip Godot_v\${GODOT_VERSION}_export_templates.tpz \\\n && mv templates/* /root/.local/share/godot/export_templates/\${GODOT_VERSION}/ \\\n && rm -rf templates Godot_v\${GODOT_VERSION}_export_templates.tpz\n\nWORKDIR /game\nCOPY . .\n\nRUN mkdir -p build\nCMD ["godot", "--headless", "--export-release", "${exportPreset}", "build/game"]\n`;
|
|
1444
|
+
writeFileSync(dockerfilePath, dockerfile, 'utf8');
|
|
1445
|
+
return { content: [{ type: 'text', text: `Dockerfile created for headless Godot export (preset: ${exportPresetName})` }] };
|
|
1446
|
+
}
|
|
1447
|
+
return createErrorResponse(`Unknown action: ${args.action}`);
|
|
1448
|
+
}
|
|
1449
|
+
catch (error) {
|
|
1450
|
+
return createErrorResponse(`manage_docker_export failed: ${errorMessage(error)}`);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Handle the update_project_uids tool
|
|
1455
|
+
*/
|
|
1456
|
+
async handleUpdateProjectUids(args) {
|
|
1457
|
+
// Normalize parameters to camelCase
|
|
1458
|
+
args = normalizeParameters(args);
|
|
1459
|
+
if (!args.projectPath) {
|
|
1460
|
+
return createErrorResponse('Project path is required');
|
|
1461
|
+
}
|
|
1462
|
+
if (!this.context.pathSecurity.isProjectPathAllowed(args.projectPath)) {
|
|
1463
|
+
return createErrorResponse('Invalid project path');
|
|
1464
|
+
}
|
|
1465
|
+
try {
|
|
1466
|
+
// Ensure godotPath is set
|
|
1467
|
+
if (!this.context.getGodotPath()) {
|
|
1468
|
+
await this.context.detectGodotPath();
|
|
1469
|
+
if (!this.context.getGodotPath()) {
|
|
1470
|
+
return createErrorResponse('Could not find a valid Godot executable path');
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
// Check if the project directory exists and contains a project.godot file
|
|
1474
|
+
const projectFile = join(args.projectPath, 'project.godot');
|
|
1475
|
+
if (!existsSync(projectFile)) {
|
|
1476
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`);
|
|
1477
|
+
}
|
|
1478
|
+
// Get Godot version to check if UIDs are supported
|
|
1479
|
+
const { stdout: versionOutput } = await execFileAsync(this.context.getGodotPath(), ['--version'], GODOT_VERSION_OPTIONS);
|
|
1480
|
+
const version = versionOutput.trim();
|
|
1481
|
+
if (!isGodot44OrLater(version)) {
|
|
1482
|
+
return createErrorResponse(`UIDs are only supported in Godot 4.4 or later. Current version: ${version}`);
|
|
1483
|
+
}
|
|
1484
|
+
// The operation runs with --path at the project root, so res:// already
|
|
1485
|
+
// covers it. Passing the absolute OS path as project_path used to make
|
|
1486
|
+
// the operation scan res://<absolute-path> — nothing — and report
|
|
1487
|
+
// success while generating no UIDs.
|
|
1488
|
+
const result = await this.context.executeOperation('resave_resources', {}, args.projectPath);
|
|
1489
|
+
const failure = this.headlessFailure('Failed to update project UIDs', result);
|
|
1490
|
+
if (failure)
|
|
1491
|
+
return failure;
|
|
1492
|
+
const { stdout } = result;
|
|
1493
|
+
return {
|
|
1494
|
+
content: [
|
|
1495
|
+
{
|
|
1496
|
+
type: 'text',
|
|
1497
|
+
text: `Project UIDs updated successfully.\n\nOutput: ${stdout}`,
|
|
1498
|
+
},
|
|
1499
|
+
],
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
catch (error) {
|
|
1503
|
+
return createErrorResponse(`Failed to update project UIDs: ${errorMessage(error)}`);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|