gdharness 1.2.30 → 1.2.31
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/build/cli.js +78 -22
- package/build/godot/addons/gdharness_editor/property_values.gd +80 -54
- package/build/godot/addons/gdharness_editor/serialisation.gd +46 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +78 -3
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +19 -4
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +5 -4
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +1 -47
- package/build/godot/addons/gdharness_runtime/serialisation.gd +46 -0
- package/build/godot/operations/audio_buses.gd +14 -1
- package/build/godot/operations/class_cache.gd +146 -35
- package/build/godot/operations/classdb_queries.gd +19 -12
- package/build/godot/operations/project_config.gd +16 -6
- package/build/godot/operations/serialisation.gd +46 -0
- package/build/index.js +78 -22
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -29053,6 +29053,9 @@ class GodotBridge extends EventEmitter {
|
|
|
29053
29053
|
const info = this.connectionInfo;
|
|
29054
29054
|
return this.socket?.readyState === import_websocket.default.OPEN && info !== null && (info.greeted !== true || info.identifying === true);
|
|
29055
29055
|
}
|
|
29056
|
+
requestsInFlight() {
|
|
29057
|
+
return [...this.pendingRequests.values()].filter((pending) => pending.counted).map((pending) => ({ tool: pending.toolName, startedAt: pending.startedAt })).sort((a, b) => a.startedAt - b.startedAt);
|
|
29058
|
+
}
|
|
29056
29059
|
getStatus() {
|
|
29057
29060
|
const identifying = this.connectionInfo?.identifying === true;
|
|
29058
29061
|
const editor = identifying ? null : this.connectionInfo;
|
|
@@ -29074,18 +29077,19 @@ class GodotBridge extends EventEmitter {
|
|
|
29074
29077
|
syncsBreakpoints: editor?.syncsBreakpoints,
|
|
29075
29078
|
turnedAway: this.turnedAway ?? undefined,
|
|
29076
29079
|
identifying: identifying ? true : undefined,
|
|
29077
|
-
pendingRequests: this.
|
|
29080
|
+
pendingRequests: this.requestsInFlight().length,
|
|
29078
29081
|
queuedResources: this.resourceQueues.size,
|
|
29079
29082
|
listeningSince: this.listeningSince ?? undefined,
|
|
29080
29083
|
portWanted: this.wantedPort !== 0 && this.boundPort !== null && this.boundPort !== this.wantedPort ? this.wantedPort : undefined
|
|
29081
29084
|
};
|
|
29082
29085
|
}
|
|
29083
|
-
invokeTool(toolName, args) {
|
|
29086
|
+
invokeTool(toolName, args, options = {}) {
|
|
29087
|
+
const counted = options.counted ?? true;
|
|
29084
29088
|
const resourceKey = this.getResourceKey(args);
|
|
29085
29089
|
if (!resourceKey) {
|
|
29086
|
-
return this.invokeToolDirect(toolName, args);
|
|
29090
|
+
return this.invokeToolDirect(toolName, args, counted);
|
|
29087
29091
|
}
|
|
29088
|
-
return this.enqueueResourceRequest(resourceKey, () => this.invokeToolDirect(toolName, args, resourceKey));
|
|
29092
|
+
return this.enqueueResourceRequest(resourceKey, () => this.invokeToolDirect(toolName, args, counted, resourceKey));
|
|
29089
29093
|
}
|
|
29090
29094
|
getRequestPathname(url) {
|
|
29091
29095
|
try {
|
|
@@ -29237,7 +29241,7 @@ class GodotBridge extends EventEmitter {
|
|
|
29237
29241
|
return;
|
|
29238
29242
|
}
|
|
29239
29243
|
}
|
|
29240
|
-
invokeToolDirect(toolName, args, resourceKey) {
|
|
29244
|
+
invokeToolDirect(toolName, args, counted, resourceKey) {
|
|
29241
29245
|
if (!this.isConnected()) {
|
|
29242
29246
|
return Promise.reject(new Error("Godot is not connected"));
|
|
29243
29247
|
}
|
|
@@ -29259,7 +29263,8 @@ class GodotBridge extends EventEmitter {
|
|
|
29259
29263
|
resolve,
|
|
29260
29264
|
reject,
|
|
29261
29265
|
startedAt: Date.now(),
|
|
29262
|
-
resourceKey
|
|
29266
|
+
resourceKey,
|
|
29267
|
+
counted
|
|
29263
29268
|
});
|
|
29264
29269
|
this.emitBridgeEvent("tool_start", {
|
|
29265
29270
|
tool: toolName,
|
|
@@ -31352,11 +31357,26 @@ function shownDiagnostic(entry) {
|
|
|
31352
31357
|
...code === undefined || code === null || code === 0 || code === "" ? {} : { code }
|
|
31353
31358
|
};
|
|
31354
31359
|
}
|
|
31355
|
-
function
|
|
31360
|
+
function linesIn(content) {
|
|
31361
|
+
const lines = content.split(`
|
|
31362
|
+
`);
|
|
31363
|
+
return Math.max(1, content.endsWith(`
|
|
31364
|
+
`) ? lines.length - 1 : lines.length);
|
|
31365
|
+
}
|
|
31366
|
+
function lastLineOf(whole, lastOfDocument) {
|
|
31367
|
+
const end = shownPosition(whole["end"]);
|
|
31368
|
+
if (end === null) {
|
|
31369
|
+
return null;
|
|
31370
|
+
}
|
|
31371
|
+
const start = shownPosition(whole["start"]);
|
|
31372
|
+
const exclusive = end.column === 1 && (start === null || end.line > start.line);
|
|
31373
|
+
return Math.min(exclusive ? end.line - 1 : end.line, lastOfDocument);
|
|
31374
|
+
}
|
|
31375
|
+
function shownSymbol(entry, lastOfDocument) {
|
|
31356
31376
|
const symbol = asRecord(entry);
|
|
31357
31377
|
const whole = asRecord(symbol["range"] ?? asRecord(symbol["location"])["range"]);
|
|
31358
31378
|
const at = shownPosition(asRecord(symbol["selectionRange"] ?? whole)["start"]);
|
|
31359
|
-
const last =
|
|
31379
|
+
const last = lastLineOf(whole, lastOfDocument);
|
|
31360
31380
|
const children = Array.isArray(symbol["children"]) ? symbol["children"] : [];
|
|
31361
31381
|
const { documentation, native_class: nativeClass, deprecated, detail } = symbol;
|
|
31362
31382
|
return {
|
|
@@ -31364,11 +31384,11 @@ function shownSymbol(entry) {
|
|
|
31364
31384
|
kind: named(symbol["kind"], SYMBOL_KINDS),
|
|
31365
31385
|
...typeof detail === "string" && detail !== "" ? { detail } : {},
|
|
31366
31386
|
...at === null ? {} : { line: at.line, column: at.column },
|
|
31367
|
-
...last === null ? {} : { lastLine: last
|
|
31387
|
+
...last === null ? {} : { lastLine: last },
|
|
31368
31388
|
...typeof documentation === "string" && documentation !== "" ? { documentation } : {},
|
|
31369
31389
|
...typeof nativeClass === "string" && nativeClass !== "" ? { nativeClass } : {},
|
|
31370
31390
|
...deprecated === true ? { deprecated } : {},
|
|
31371
|
-
...children.length > 0 ? { children: children.map(shownSymbol) } : {}
|
|
31391
|
+
...children.length > 0 ? { children: children.map((child) => shownSymbol(child, lastOfDocument)) } : {}
|
|
31372
31392
|
};
|
|
31373
31393
|
}
|
|
31374
31394
|
var MOST_COMPLETIONS = 100;
|
|
@@ -31473,7 +31493,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
31473
31493
|
}
|
|
31474
31494
|
case "lsp_get_symbols": {
|
|
31475
31495
|
const symbols = await client.getDocumentSymbols(scriptPath, content);
|
|
31476
|
-
return asToolResponse({ symbols: symbols.map(shownSymbol) });
|
|
31496
|
+
return asToolResponse({ symbols: symbols.map((symbol) => shownSymbol(symbol, linesIn(content))) });
|
|
31477
31497
|
}
|
|
31478
31498
|
default:
|
|
31479
31499
|
return asToolResponse({ error: `Unknown LSP tool: ${toolName}` });
|
|
@@ -34298,7 +34318,7 @@ var NODE_PATH = {
|
|
|
34298
34318
|
};
|
|
34299
34319
|
var PROPERTIES = {
|
|
34300
34320
|
type: "object",
|
|
34301
|
-
description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds.',
|
|
34321
|
+
description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. A key may also be a colon path into one of the components of a vector, a colour, a rectangle or a transform, as GDScript names them, so one axis is written alone: "position:x", "modulate:a", "region_rect:size:y"; a component the value does not have is refused, naming the ones it has. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds.',
|
|
34302
34322
|
additionalProperties: true
|
|
34303
34323
|
};
|
|
34304
34324
|
var XY = {
|
|
@@ -34753,7 +34773,11 @@ var TOOL_SPECS = [
|
|
|
34753
34773
|
requires: ["projectPath", "scenePath"],
|
|
34754
34774
|
operations: {
|
|
34755
34775
|
create: { effect: "destructive", summary: "a new scene with one root node", requires: [] },
|
|
34756
|
-
save: {
|
|
34776
|
+
save: {
|
|
34777
|
+
effect: "destructive",
|
|
34778
|
+
summary: "save the scene as it is in the editor, or as the file loads when the editor does not have it open; from says which",
|
|
34779
|
+
requires: []
|
|
34780
|
+
},
|
|
34757
34781
|
save_as: { effect: "destructive", summary: "save a copy under newPath", requires: ["newPath"] }
|
|
34758
34782
|
},
|
|
34759
34783
|
defaultOperation: "create"
|
|
@@ -34801,7 +34825,11 @@ var TOOL_SPECS = [
|
|
|
34801
34825
|
requires: ["projectPath", "scenePath"],
|
|
34802
34826
|
operations: {
|
|
34803
34827
|
add: { effect: "additive", summary: "add a node of any class", requires: ["nodeType", "nodeName"] },
|
|
34804
|
-
get: {
|
|
34828
|
+
get: {
|
|
34829
|
+
effect: "read",
|
|
34830
|
+
summary: "read a node's properties, and a TileMapLayer's or TileMap's cells as set_tilemap_cells takes them, the first 1000 with cellCount for all",
|
|
34831
|
+
requires: ["nodePath"]
|
|
34832
|
+
},
|
|
34805
34833
|
set: {
|
|
34806
34834
|
effect: "destructive",
|
|
34807
34835
|
summary: "set properties on a node",
|
|
@@ -34828,7 +34856,7 @@ var TOOL_SPECS = [
|
|
|
34828
34856
|
{
|
|
34829
34857
|
name: "scene_signal",
|
|
34830
34858
|
title: "Scene signals",
|
|
34831
|
-
description: "Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. Needs the editor connected.",
|
|
34859
|
+
description: "Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connect of a pair already connected says alreadyConnected, with flagsBefore when it changed the flags. Needs the editor connected.",
|
|
34832
34860
|
parameters: {
|
|
34833
34861
|
projectPath: PROJECT_PATH2,
|
|
34834
34862
|
scenePath: SCENE_PATH,
|
|
@@ -38206,7 +38234,7 @@ class GodotServer {
|
|
|
38206
38234
|
const { op: _op, ...arguments_ } = args;
|
|
38207
38235
|
const bridge = async (command, extra = {}) => {
|
|
38208
38236
|
const contained = this.containProjectFiles({ ...arguments_, ...extra });
|
|
38209
|
-
return contained.ok ? await this.handleViaBridge(command, contained.value, tool) : contained.response;
|
|
38237
|
+
return contained.ok ? await this.handleViaBridge(command, contained.value, op === "" ? tool : `${tool} ${op}`) : contained.response;
|
|
38210
38238
|
};
|
|
38211
38239
|
switch (tool) {
|
|
38212
38240
|
case "project_info":
|
|
@@ -38936,8 +38964,30 @@ class GodotServer {
|
|
|
38936
38964
|
]
|
|
38937
38965
|
};
|
|
38938
38966
|
}
|
|
38967
|
+
const dropped = await this.optionsTheImporterDropped(project.value.path, params, written.payload);
|
|
38968
|
+
if (dropped !== null) {
|
|
38969
|
+
return dropped;
|
|
38970
|
+
}
|
|
38939
38971
|
return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
|
|
38940
38972
|
}
|
|
38973
|
+
async optionsTheImporterDropped(projectPath, params, written) {
|
|
38974
|
+
const listed = written["updated_options"];
|
|
38975
|
+
const updated = Array.isArray(listed) ? listed.map(String) : [];
|
|
38976
|
+
const { options: _options, ...where } = params;
|
|
38977
|
+
const reread = await this.operation("get_import_options", where, projectPath);
|
|
38978
|
+
if (!reread.ok || updated.length === 0) {
|
|
38979
|
+
return null;
|
|
38980
|
+
}
|
|
38981
|
+
const held = reread.payload["params"];
|
|
38982
|
+
const kept = held !== null && typeof held === "object" ? Object.keys(held) : [];
|
|
38983
|
+
const gone = updated.filter((name) => !kept.includes(name));
|
|
38984
|
+
if (gone.length === 0) {
|
|
38985
|
+
return null;
|
|
38986
|
+
}
|
|
38987
|
+
const applied = updated.filter((name) => kept.includes(name));
|
|
38988
|
+
const named = gone.join(", ");
|
|
38989
|
+
return this.createErrorResponse(`${named} ${gone.length === 1 ? "is not an option" : "are not options"} of this resource's importer: the reimport left ${gone.length === 1 ? "it" : "them"} out of the import file, so ${gone.length === 1 ? "it changed" : "they changed"} nothing.${applied.length === 0 ? "" : ` ${applied.join(", ")} ${applied.length === 1 ? "was" : "were"} applied.`} Its options are: ${kept.join(", ")}.`, ["project_import options lists every option with the value it holds"]);
|
|
38990
|
+
}
|
|
38941
38991
|
async reimport(projectPath, resourcePath, force, timeoutMs) {
|
|
38942
38992
|
const ask = resourcePath === undefined ? { includeUpToDate: force } : { resourcePath };
|
|
38943
38993
|
const before = await this.operation("get_import_status", ask, projectPath);
|
|
@@ -40037,7 +40087,7 @@ class GodotServer {
|
|
|
40037
40087
|
}
|
|
40038
40088
|
try {
|
|
40039
40089
|
const askedAt = Date.now();
|
|
40040
|
-
const asked = this.godotBridge.invokeTool("playing_status", {});
|
|
40090
|
+
const asked = this.godotBridge.invokeTool("playing_status", {}, { counted: false });
|
|
40041
40091
|
const answer = await Promise.race([asked, delay4(PLAYING_STATUS_MS).then(() => null)]);
|
|
40042
40092
|
if (answer === null) {
|
|
40043
40093
|
this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
|
|
@@ -40062,7 +40112,7 @@ class GodotServer {
|
|
|
40062
40112
|
}
|
|
40063
40113
|
try {
|
|
40064
40114
|
const answer = await Promise.race([
|
|
40065
|
-
this.godotBridge.invokeTool("scan_status", {}),
|
|
40115
|
+
this.godotBridge.invokeTool("scan_status", {}, { counted: false }),
|
|
40066
40116
|
delay4(PLAYING_STATUS_MS).then(() => null)
|
|
40067
40117
|
]);
|
|
40068
40118
|
if (answer === null) {
|
|
@@ -40130,10 +40180,14 @@ class GodotServer {
|
|
|
40130
40180
|
this.getEditorStatusPayload(),
|
|
40131
40181
|
this.editorServersListening()
|
|
40132
40182
|
]);
|
|
40183
|
+
const answeredAt = Date.now();
|
|
40184
|
+
const inFlight = this.godotBridge.requestsInFlight().map((request) => ({ tool: request.tool, forMs: answeredAt - request.startedAt }));
|
|
40133
40185
|
return this.jsonTextResponse({
|
|
40134
40186
|
editor: {
|
|
40135
40187
|
...editorStatus,
|
|
40136
40188
|
...servers,
|
|
40189
|
+
pendingRequests: inFlight.length,
|
|
40190
|
+
pendingRequestTools: inFlight.length === 0 ? undefined : inFlight,
|
|
40137
40191
|
debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
|
|
40138
40192
|
scan
|
|
40139
40193
|
},
|
|
@@ -42721,14 +42775,16 @@ class GodotServer {
|
|
|
42721
42775
|
if (!(answered instanceof BridgeTimeout)) {
|
|
42722
42776
|
return answered;
|
|
42723
42777
|
}
|
|
42724
|
-
return this.createErrorResponse(`The editor took ${toolName} in and did not answer within ${String(answered.waitedMs / 1000)}s. An editor importing files answers nothing until the import is over, and a hidden one imports on the thread that answers.`, [
|
|
42778
|
+
return this.createErrorResponse(`The editor took ${askedAs ?? toolName} in and did not answer within ${String(answered.waitedMs / 1000)}s. An editor importing files answers nothing until the import is over, and a hidden one imports on the thread that answers.`, [
|
|
42725
42779
|
"editor_status says whether it is scanning or importing",
|
|
42726
42780
|
"The editor still carries the request out once it is free, so read what it changed before asking again"
|
|
42727
42781
|
]);
|
|
42728
42782
|
}
|
|
42729
42783
|
async askViaBridge(toolName, args, askedAs) {
|
|
42784
|
+
const named = askedAs ?? toolName;
|
|
42730
42785
|
if (!this.godotBridge.isConnected()) {
|
|
42731
|
-
const
|
|
42786
|
+
const tool = askedAs?.split(" ")[0];
|
|
42787
|
+
const alternative = tool !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, tool) ? EDITOR_FREE_INSTEAD[tool] : undefined;
|
|
42732
42788
|
const refused = await this.noEditorYet(args, alternative === undefined ? [] : [alternative]);
|
|
42733
42789
|
if (refused !== null) {
|
|
42734
42790
|
return refused;
|
|
@@ -42752,9 +42808,9 @@ class GodotServer {
|
|
|
42752
42808
|
if (message.includes(`Unknown tool: ${toolName}`)) {
|
|
42753
42809
|
const status = this.godotBridge.getStatus();
|
|
42754
42810
|
const remedy = addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
|
|
42755
|
-
return this.createErrorResponse(`The editor's gdharness addon is older than this ${SERVER_VERSION} server and has no ${
|
|
42811
|
+
return this.createErrorResponse(`The editor's gdharness addon is older than this ${SERVER_VERSION} server and has no ${named}, so it cannot answer this call. ${remedy ?? "Restart it with editor_launch restart to load the addon the project holds."}`, ["editor_status says which addon the editor holds"]);
|
|
42756
42812
|
}
|
|
42757
|
-
return this.createErrorResponse(`The editor answered ${
|
|
42813
|
+
return this.createErrorResponse(`The editor answered ${named} with an error: ${message}`);
|
|
42758
42814
|
}
|
|
42759
42815
|
}
|
|
42760
42816
|
runtimeTimeoutMs() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.31",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|