gdharness 0.6.3 → 0.6.4
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
CHANGED
|
@@ -157,8 +157,8 @@ function runArguments(options) {
|
|
|
157
157
|
}
|
|
158
158
|
return args;
|
|
159
159
|
}
|
|
160
|
-
function editorArguments(projectPath) {
|
|
161
|
-
return ["-e", "--path", projectPath];
|
|
160
|
+
function editorArguments(projectPath, ports) {
|
|
161
|
+
return ["-e", "--path", projectPath, "--lsp-port", String(ports.lsp), "--dap-port", String(ports.dap)];
|
|
162
162
|
}
|
|
163
163
|
function userDataIn(home, variables = process.env) {
|
|
164
164
|
const moved = ["APPDATA", "XDG_DATA_HOME"];
|
|
@@ -30791,10 +30791,11 @@ var init_framing = __esm(() => {
|
|
|
30791
30791
|
});
|
|
30792
30792
|
|
|
30793
30793
|
// src/ports.ts
|
|
30794
|
-
|
|
30794
|
+
import { createServer } from "node:net";
|
|
30795
|
+
function portFromEnvOrNull(variable) {
|
|
30795
30796
|
const raw = process.env[variable]?.trim();
|
|
30796
30797
|
if (!raw) {
|
|
30797
|
-
return
|
|
30798
|
+
return null;
|
|
30798
30799
|
}
|
|
30799
30800
|
const parsed = Number.parseInt(raw, 10);
|
|
30800
30801
|
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535 || String(parsed) !== raw) {
|
|
@@ -30802,6 +30803,36 @@ function portFromEnv(variable, fallback) {
|
|
|
30802
30803
|
}
|
|
30803
30804
|
return parsed;
|
|
30804
30805
|
}
|
|
30806
|
+
function portFromEnv(variable, fallback) {
|
|
30807
|
+
return portFromEnvOrNull(variable) ?? fallback;
|
|
30808
|
+
}
|
|
30809
|
+
async function freePort(preferred, host = "127.0.0.1") {
|
|
30810
|
+
const asked = await bindable(preferred, host);
|
|
30811
|
+
if (asked !== null) {
|
|
30812
|
+
return asked;
|
|
30813
|
+
}
|
|
30814
|
+
const spare = await bindable(0, host);
|
|
30815
|
+
if (spare === null) {
|
|
30816
|
+
throw new Refusal2(`No port could be bound on ${host}.`);
|
|
30817
|
+
}
|
|
30818
|
+
return spare;
|
|
30819
|
+
}
|
|
30820
|
+
function bindable(port, host) {
|
|
30821
|
+
return new Promise((resolve) => {
|
|
30822
|
+
const probe = createServer();
|
|
30823
|
+
probe.once("error", () => {
|
|
30824
|
+
resolve(null);
|
|
30825
|
+
});
|
|
30826
|
+
probe.once("listening", () => {
|
|
30827
|
+
const bound = probe.address();
|
|
30828
|
+
const taken = typeof bound === "object" && bound !== null ? bound.port : port;
|
|
30829
|
+
probe.close(() => {
|
|
30830
|
+
resolve(taken);
|
|
30831
|
+
});
|
|
30832
|
+
});
|
|
30833
|
+
probe.listen(port, host);
|
|
30834
|
+
});
|
|
30835
|
+
}
|
|
30805
30836
|
var init_ports = __esm(() => {
|
|
30806
30837
|
init_errors();
|
|
30807
30838
|
});
|
|
@@ -34189,6 +34220,9 @@ function resolveDefaultBridgeHost() {
|
|
|
34189
34220
|
const host = process.env["GDHARNESS_BRIDGE_HOST"]?.trim();
|
|
34190
34221
|
return host === undefined || host === "" ? DEFAULT_HOST : host;
|
|
34191
34222
|
}
|
|
34223
|
+
function servedPort(said) {
|
|
34224
|
+
return typeof said === "number" && Number.isInteger(said) && said > 0 && said <= 65535 ? said : undefined;
|
|
34225
|
+
}
|
|
34192
34226
|
function getDefaultBridge(mayMove = false) {
|
|
34193
34227
|
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, mayMove);
|
|
34194
34228
|
return defaultBridge;
|
|
@@ -34329,6 +34363,9 @@ var init_godot_bridge = __esm(() => {
|
|
|
34329
34363
|
lastPongAt: this.connectionInfo?.lastPongAt,
|
|
34330
34364
|
addonVersion: this.connectionInfo?.addonVersion,
|
|
34331
34365
|
editorPid: this.connectionInfo?.editorPid,
|
|
34366
|
+
lspPort: this.connectionInfo?.lspPort,
|
|
34367
|
+
dapPort: this.connectionInfo?.dapPort,
|
|
34368
|
+
debugPort: this.connectionInfo?.debugPort,
|
|
34332
34369
|
pendingRequests: this.pendingRequests.size,
|
|
34333
34370
|
queuedResources: this.resourceQueues.size
|
|
34334
34371
|
};
|
|
@@ -34462,6 +34499,9 @@ var init_godot_bridge = __esm(() => {
|
|
|
34462
34499
|
this.connectionInfo.projectPath = message.project_path;
|
|
34463
34500
|
this.connectionInfo.addonVersion = message.addon_version ?? "";
|
|
34464
34501
|
this.connectionInfo.editorPid = message.editor_pid;
|
|
34502
|
+
this.connectionInfo.lspPort = servedPort(message.lsp_port);
|
|
34503
|
+
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
34504
|
+
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
34465
34505
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
34466
34506
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
34467
34507
|
}
|
|
@@ -35368,10 +35408,10 @@ function asToolResponse(payload) {
|
|
|
35368
35408
|
]
|
|
35369
35409
|
};
|
|
35370
35410
|
}
|
|
35371
|
-
function normalizeLSPError(error) {
|
|
35411
|
+
function normalizeLSPError(error, port) {
|
|
35372
35412
|
if (error instanceof Error) {
|
|
35373
35413
|
if (error.message.includes("ECONNREFUSED") || error.message.includes("Failed to connect to Godot LSP") || error.message.includes("socket closed")) {
|
|
35374
|
-
return `Godot LSP is unavailable on port ${
|
|
35414
|
+
return `Godot LSP is unavailable on port ${port}. Start the Godot editor and enable Language Server in Editor Settings, or set GDHARNESS_LSP_PORT to the port it serves.`;
|
|
35375
35415
|
}
|
|
35376
35416
|
return error.message;
|
|
35377
35417
|
}
|
|
@@ -35449,7 +35489,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
35449
35489
|
}
|
|
35450
35490
|
} catch (error) {
|
|
35451
35491
|
return asToolResponse({
|
|
35452
|
-
error: normalizeLSPError(error)
|
|
35492
|
+
error: normalizeLSPError(error, client.port)
|
|
35453
35493
|
});
|
|
35454
35494
|
}
|
|
35455
35495
|
}
|
|
@@ -36412,7 +36452,7 @@ class GodotServer {
|
|
|
36412
36452
|
return {
|
|
36413
36453
|
ok: false,
|
|
36414
36454
|
response: this.createErrorResponse(`The editor is playing the game but its debug adapter did not answer: ${errorMessage(error)}`, [
|
|
36415
|
-
|
|
36455
|
+
`This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
|
|
36416
36456
|
"GDHARNESS_DAP_PORT points this server at another one"
|
|
36417
36457
|
])
|
|
36418
36458
|
};
|
|
@@ -36779,7 +36819,8 @@ class GodotServer {
|
|
|
36779
36819
|
if (payload["error"] !== undefined) {
|
|
36780
36820
|
const reason = payload["error"];
|
|
36781
36821
|
return this.createErrorResponse(`Diagnostics unavailable: ${typeof reason === "string" ? reason : JSON.stringify(reason)}`, [
|
|
36782
|
-
|
|
36822
|
+
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
36823
|
+
"GDHARNESS_LSP_PORT points this server at another one"
|
|
36783
36824
|
]);
|
|
36784
36825
|
}
|
|
36785
36826
|
const diagnostics = readArray(payload, "diagnostics") ?? [];
|
|
@@ -36796,16 +36837,33 @@ class GodotServer {
|
|
|
36796
36837
|
});
|
|
36797
36838
|
}
|
|
36798
36839
|
async handleLSP(toolName, args) {
|
|
36799
|
-
this.
|
|
36840
|
+
const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
|
|
36841
|
+
if (this.lspClient !== null && this.lspClient.port !== port) {
|
|
36842
|
+
await this.lspClient.disconnect();
|
|
36843
|
+
this.lspClient = null;
|
|
36844
|
+
}
|
|
36845
|
+
this.lspClient ??= new GodotLSPClient(port);
|
|
36800
36846
|
return handleLSPTool(this.lspClient, toolName, args);
|
|
36801
36847
|
}
|
|
36802
36848
|
async handleDAP(toolName, args) {
|
|
36803
36849
|
return handleDAPTool(this.dap(), toolName, args);
|
|
36804
36850
|
}
|
|
36805
36851
|
dap() {
|
|
36806
|
-
this.
|
|
36852
|
+
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
36853
|
+
if (this.dapClient !== null && this.dapClient.port !== port) {
|
|
36854
|
+
this.dapClient = null;
|
|
36855
|
+
}
|
|
36856
|
+
this.dapClient ??= new GodotDAPClient(port);
|
|
36807
36857
|
return this.dapClient;
|
|
36808
36858
|
}
|
|
36859
|
+
editorServes(field, variable, fallback) {
|
|
36860
|
+
const named = portFromEnvOrNull(variable);
|
|
36861
|
+
if (named !== null) {
|
|
36862
|
+
return named;
|
|
36863
|
+
}
|
|
36864
|
+
const status = this.godotBridge.getStatus();
|
|
36865
|
+
return (status.connected ? status[field] : undefined) ?? fallback;
|
|
36866
|
+
}
|
|
36809
36867
|
getEditorStatusPayload() {
|
|
36810
36868
|
const status = this.godotBridge.getStatus();
|
|
36811
36869
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
@@ -36833,7 +36891,8 @@ class GodotServer {
|
|
|
36833
36891
|
const answer = await this.godotBridge.invokeTool("playing_status", {});
|
|
36834
36892
|
return {
|
|
36835
36893
|
playing: readBoolean(asParams(answer), "playing") ?? false,
|
|
36836
|
-
scene: readString(asParams(answer), "scenePath") ?? ""
|
|
36894
|
+
scene: readString(asParams(answer), "scenePath") ?? "",
|
|
36895
|
+
debugPort: readNumber(asParams(answer), "debugPort")
|
|
36837
36896
|
};
|
|
36838
36897
|
} catch {
|
|
36839
36898
|
return null;
|
|
@@ -36851,15 +36910,19 @@ class GodotServer {
|
|
|
36851
36910
|
problem: reply.ok ? null : reply.message
|
|
36852
36911
|
};
|
|
36853
36912
|
}));
|
|
36913
|
+
const playing = await this.editorPlayingState();
|
|
36854
36914
|
return this.jsonTextResponse({
|
|
36855
|
-
editor:
|
|
36915
|
+
editor: {
|
|
36916
|
+
...this.getEditorStatusPayload(),
|
|
36917
|
+
debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort
|
|
36918
|
+
},
|
|
36856
36919
|
godot: {
|
|
36857
36920
|
path: godotPath,
|
|
36858
36921
|
version: godotPath === null ? null : await this.godotVersion(godotPath)
|
|
36859
36922
|
},
|
|
36860
36923
|
game: {
|
|
36861
36924
|
processActive: this.activeProcess !== null,
|
|
36862
|
-
playingInEditor:
|
|
36925
|
+
playingInEditor: playing,
|
|
36863
36926
|
runtimeConnected: games.some((game) => game.reachable),
|
|
36864
36927
|
runtimes: games
|
|
36865
36928
|
}
|
|
@@ -36918,10 +36981,10 @@ class GodotServer {
|
|
|
36918
36981
|
return project.response;
|
|
36919
36982
|
}
|
|
36920
36983
|
if (this.godotBridge.isConnected()) {
|
|
36921
|
-
return this.createErrorResponse("An editor is already connected to this server
|
|
36984
|
+
return this.createErrorResponse("An editor is already connected to this server.", [
|
|
36922
36985
|
"editor_status says which editor is answering, and for which project",
|
|
36923
36986
|
"editor_launch restart replaces the connected editor rather than joining it",
|
|
36924
|
-
"
|
|
36987
|
+
"Another project wants its own gdharness server, which is its own harness session"
|
|
36925
36988
|
]);
|
|
36926
36989
|
}
|
|
36927
36990
|
const engine = await this.engine();
|
|
@@ -36929,10 +36992,16 @@ class GodotServer {
|
|
|
36929
36992
|
return engine.response;
|
|
36930
36993
|
}
|
|
36931
36994
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
36932
|
-
const
|
|
36995
|
+
const ports = await this.portsForAnEditor();
|
|
36996
|
+
const editor = spawn(engine.value, editorArguments(project.value.path, ports), {
|
|
36933
36997
|
stdio: "ignore",
|
|
36934
36998
|
detached: true,
|
|
36935
|
-
env: {
|
|
36999
|
+
env: {
|
|
37000
|
+
...process.env,
|
|
37001
|
+
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
37002
|
+
GDHARNESS_LSP_PORT: String(ports.lsp),
|
|
37003
|
+
GDHARNESS_DAP_PORT: String(ports.dap)
|
|
37004
|
+
}
|
|
36936
37005
|
});
|
|
36937
37006
|
const started = await new Promise((resolve) => {
|
|
36938
37007
|
editor.once("spawn", () => {
|
|
@@ -36949,9 +37018,16 @@ class GodotServer {
|
|
|
36949
37018
|
return this.jsonTextResponse({
|
|
36950
37019
|
launched: true,
|
|
36951
37020
|
pid: editor.pid ?? null,
|
|
36952
|
-
projectPath: project.value.path
|
|
37021
|
+
projectPath: project.value.path,
|
|
37022
|
+
lspPort: ports.lsp,
|
|
37023
|
+
dapPort: ports.dap
|
|
36953
37024
|
});
|
|
36954
37025
|
}
|
|
37026
|
+
async portsForAnEditor() {
|
|
37027
|
+
const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
|
|
37028
|
+
const dap = portFromEnvOrNull("GDHARNESS_DAP_PORT") ?? await freePort(DEFAULT_DAP_PORT);
|
|
37029
|
+
return { lsp, dap };
|
|
37030
|
+
}
|
|
36955
37031
|
async handleRunProject(args, op) {
|
|
36956
37032
|
const project = this.project(args);
|
|
36957
37033
|
if (!project.ok) {
|
|
@@ -37019,7 +37095,7 @@ class GodotServer {
|
|
|
37019
37095
|
await this.dap().connect();
|
|
37020
37096
|
} catch (error) {
|
|
37021
37097
|
return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
|
|
37022
|
-
|
|
37098
|
+
`This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
|
|
37023
37099
|
"GDHARNESS_DAP_PORT points this server at another one"
|
|
37024
37100
|
]);
|
|
37025
37101
|
}
|
|
@@ -37041,6 +37117,7 @@ class GodotServer {
|
|
|
37041
37117
|
through: "editor",
|
|
37042
37118
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
37043
37119
|
refreshedClasses,
|
|
37120
|
+
debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
|
|
37044
37121
|
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it."
|
|
37045
37122
|
});
|
|
37046
37123
|
}
|
|
@@ -37223,7 +37300,7 @@ class GodotServer {
|
|
|
37223
37300
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
37224
37301
|
const { op: _op, projectPath, ...params } = asParams(args);
|
|
37225
37302
|
const announced = runtimesAnnounced();
|
|
37226
|
-
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : undefined, announced.unspoken);
|
|
37303
|
+
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken);
|
|
37227
37304
|
if ("problem" in choice) {
|
|
37228
37305
|
return this.createErrorResponse(choice.problem);
|
|
37229
37306
|
}
|
|
@@ -37331,6 +37408,7 @@ var init_server3 = __esm(() => {
|
|
|
37331
37408
|
init_junit();
|
|
37332
37409
|
init_lsp_client();
|
|
37333
37410
|
init_paths();
|
|
37411
|
+
init_ports();
|
|
37334
37412
|
init_project_scan();
|
|
37335
37413
|
init_resources();
|
|
37336
37414
|
init_runtime_client();
|
|
@@ -38654,6 +38732,9 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
38654
38732
|
- Read \`editor_output\` after every run. It returns the engine's errors and warnings as entries
|
|
38655
38733
|
with their backtraces, and a \`clean\` verdict, so a run that printed an error is one call away
|
|
38656
38734
|
from being known.
|
|
38735
|
+
- One server, one editor. A second project is a second harness session with its own server, and
|
|
38736
|
+
that works: each editor is opened on its own language server and debug adapter ports, and says
|
|
38737
|
+
where it serves. Two editors on one project is the thing to refuse.
|
|
38657
38738
|
|
|
38658
38739
|
## Measuring a running game
|
|
38659
38740
|
|
|
@@ -18,6 +18,17 @@ const ANNOUNCE_PROTOCOL: int = 1
|
|
|
18
18
|
const RECONNECT_DELAY: float = 3.0
|
|
19
19
|
const MAX_RECONNECT_DELAY: float = 30.0
|
|
20
20
|
|
|
21
|
+
## Where Godot keeps the three ports an editor serves. None of them is per editor: the settings
|
|
22
|
+
## file is one for every editor on the machine, so two open at once want the same three and the
|
|
23
|
+
## second one binds nothing.
|
|
24
|
+
const LSP_SETTING: String = "network/language_server/remote_port"
|
|
25
|
+
const DAP_SETTING: String = "network/debug_adapter/remote_port"
|
|
26
|
+
const DEBUGGER_SETTING: String = "network/debug/remote_port"
|
|
27
|
+
## What a server that opened this editor put in the environment, matching the ports it named on
|
|
28
|
+
## the command line. Kept in step with `editorArguments` in src/launch.ts.
|
|
29
|
+
const LSP_ASKED: String = "GDHARNESS_LSP_PORT"
|
|
30
|
+
const DAP_ASKED: String = "GDHARNESS_DAP_PORT"
|
|
31
|
+
|
|
21
32
|
## How long one attempt is given before the address is called a bad one.
|
|
22
33
|
##
|
|
23
34
|
## A socket pointed at a port nothing holds gives up by itself, in thirty seconds on Windows. One
|
|
@@ -62,6 +73,7 @@ var _connecting_for: float = 0.0
|
|
|
62
73
|
func _ready() -> void:
|
|
63
74
|
_project_path = ProjectSettings.globalize_path("res://")
|
|
64
75
|
version_at_load = _loaded_version()
|
|
76
|
+
_keep_the_ports_this_editor_was_given()
|
|
65
77
|
|
|
66
78
|
_reconnect_timer = Timer.new()
|
|
67
79
|
_reconnect_timer.one_shot = true
|
|
@@ -243,18 +255,73 @@ func _handle_connect() -> void:
|
|
|
243
255
|
# somebody restarts it, and nothing else can tell the two apart.
|
|
244
256
|
# The process id with it, because a restarted editor is a different process from the one
|
|
245
257
|
# whoever started it is holding, and nothing else says which one is now on the other end.
|
|
258
|
+
# The three ports with that, because a server that assumes the defaults talks to whichever
|
|
259
|
+
# editor took them, which on a machine running two is not this one.
|
|
246
260
|
_send_message(
|
|
247
261
|
{
|
|
248
262
|
"type": "godot_ready",
|
|
249
263
|
"project_path": _project_path,
|
|
250
264
|
"addon_version": version_at_load,
|
|
251
|
-
"editor_pid": OS.get_process_id()
|
|
265
|
+
"editor_pid": OS.get_process_id(),
|
|
266
|
+
"lsp_port": _serves(LSP_ASKED, LSP_SETTING),
|
|
267
|
+
"dap_port": _serves(DAP_ASKED, DAP_SETTING),
|
|
268
|
+
"debug_port": _serving(DEBUGGER_SETTING)
|
|
252
269
|
}
|
|
253
270
|
)
|
|
254
271
|
|
|
255
272
|
connected.emit()
|
|
256
273
|
|
|
257
274
|
|
|
275
|
+
## Writes the ports this editor was started on into the settings it reads them from.
|
|
276
|
+
##
|
|
277
|
+
## The command line moved the language server and the debug adapter for this run and the engine
|
|
278
|
+
## keeps that override to itself: the setting still reads whatever it read before, so an editor
|
|
279
|
+
## that restarts itself comes back on the old number and lands on top of whichever editor holds
|
|
280
|
+
## it. Writing it here is what makes the move survive a restart, and what leaves one place either
|
|
281
|
+
## side has to read.
|
|
282
|
+
func _keep_the_ports_this_editor_was_given() -> void:
|
|
283
|
+
if not Engine.is_editor_hint():
|
|
284
|
+
return
|
|
285
|
+
_keep_port(LSP_ASKED, LSP_SETTING)
|
|
286
|
+
_keep_port(DAP_ASKED, DAP_SETTING)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
func _keep_port(variable: String, setting: String) -> void:
|
|
290
|
+
var port: int = _asked_for(variable)
|
|
291
|
+
if port < 1 or port == _serving(setting):
|
|
292
|
+
return
|
|
293
|
+
var settings: EditorSettings = EditorInterface.get_editor_settings()
|
|
294
|
+
if settings != null:
|
|
295
|
+
settings.set_setting(setting, port)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
## What this editor serves: what it was told to when a server opened it, and what its settings
|
|
299
|
+
## say otherwise. The first is the one that counts, because the settings are the thing the command
|
|
300
|
+
## line was overriding.
|
|
301
|
+
func _serves(variable: String, setting: String) -> int:
|
|
302
|
+
var asked: int = _asked_for(variable)
|
|
303
|
+
return asked if asked > 0 else _serving(setting)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
## The port this variable names, or 0 for anything that is not one.
|
|
307
|
+
static func _asked_for(variable: String) -> int:
|
|
308
|
+
var said: String = OS.get_environment(variable)
|
|
309
|
+
if not said.is_valid_int():
|
|
310
|
+
return 0
|
|
311
|
+
var port: int = int(said)
|
|
312
|
+
return port if port >= 1 and port <= 65535 else 0
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
## What the settings say this editor serves on, or 0 when there is nothing to ask.
|
|
316
|
+
func _serving(setting: String) -> int:
|
|
317
|
+
if not Engine.is_editor_hint():
|
|
318
|
+
return 0
|
|
319
|
+
var settings: EditorSettings = EditorInterface.get_editor_settings()
|
|
320
|
+
if settings == null or not settings.has_setting(setting):
|
|
321
|
+
return 0
|
|
322
|
+
return int(settings.get_setting(setting))
|
|
323
|
+
|
|
324
|
+
|
|
258
325
|
## The version marker beside this addon, or "" when the copy was not installed by gdharness.
|
|
259
326
|
##
|
|
260
327
|
## Read once, when this copy loads, and never again: an upgrade replaces the files under a
|
|
@@ -10,6 +10,10 @@ extends Node
|
|
|
10
10
|
|
|
11
11
|
## What the display server calls itself when the engine was started with no display at all.
|
|
12
12
|
const HEADLESS_DISPLAY: String = "headless"
|
|
13
|
+
## Where Godot keeps the port its own debugger listens on for a game it is playing. One setting
|
|
14
|
+
## for every editor on the machine, and bound only while a game runs.
|
|
15
|
+
const DEBUGGER_SETTING: String = "network/debug/remote_port"
|
|
16
|
+
const LOOPBACK: String = "127.0.0.1"
|
|
13
17
|
|
|
14
18
|
var _editor_plugin: EditorPlugin = null
|
|
15
19
|
|
|
@@ -24,6 +28,8 @@ func play_scene(args: Dictionary) -> Dictionary:
|
|
|
24
28
|
if EditorInterface.is_playing_scene():
|
|
25
29
|
EditorInterface.stop_playing_scene()
|
|
26
30
|
|
|
31
|
+
var debugger: int = _a_port_for_the_debugger()
|
|
32
|
+
|
|
27
33
|
if scene_path.is_empty():
|
|
28
34
|
EditorInterface.play_main_scene()
|
|
29
35
|
else:
|
|
@@ -35,10 +41,43 @@ func play_scene(args: Dictionary) -> Dictionary:
|
|
|
35
41
|
return {
|
|
36
42
|
"ok": true,
|
|
37
43
|
"playing": EditorInterface.is_playing_scene(),
|
|
38
|
-
"scenePath": EditorInterface.get_playing_scene()
|
|
44
|
+
"scenePath": EditorInterface.get_playing_scene(),
|
|
45
|
+
"debugPort": debugger
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
|
|
49
|
+
## Gives this editor's debugger a port of its own, and answers the one the game will be sent to.
|
|
50
|
+
##
|
|
51
|
+
## The editor binds this while a game runs, out of a setting shared by every editor on the
|
|
52
|
+
## machine, and Godot takes --lsp-port and --dap-port on its command line but nothing at all for
|
|
53
|
+
## this one. So two editors playing at once want the same number, and both ways that can go are
|
|
54
|
+
## wrong: a bind that fails leaves a game with no debugger behind it, which is every debug tool
|
|
55
|
+
## and the whole console gone, and a bind that succeeds anyway leaves two editors on one port with
|
|
56
|
+
## the games going to whichever one the operating system picks.
|
|
57
|
+
##
|
|
58
|
+
## Asked of the operating system each time rather than tested first, which is how the runtime
|
|
59
|
+
## addon takes its own port: a port that was free a moment ago is not a port that is still free,
|
|
60
|
+
## and nothing this can ask distinguishes the editor's own debugger from another editor's.
|
|
61
|
+
func _a_port_for_the_debugger() -> int:
|
|
62
|
+
var settings: EditorSettings = EditorInterface.get_editor_settings()
|
|
63
|
+
if settings == null or not settings.has_setting(DEBUGGER_SETTING):
|
|
64
|
+
return 0
|
|
65
|
+
var spare: int = _any_free_port()
|
|
66
|
+
if spare > 0:
|
|
67
|
+
settings.set_setting(DEBUGGER_SETTING, spare)
|
|
68
|
+
return spare
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## A port the operating system handed out, or 0 when it would not give one.
|
|
72
|
+
static func _any_free_port() -> int:
|
|
73
|
+
var probe: TCPServer = TCPServer.new()
|
|
74
|
+
if probe.listen(0, LOOPBACK) != OK:
|
|
75
|
+
return 0
|
|
76
|
+
var port: int = probe.get_local_port()
|
|
77
|
+
probe.stop()
|
|
78
|
+
return port
|
|
79
|
+
|
|
80
|
+
|
|
42
81
|
func stop_playing(_args: Dictionary) -> Dictionary:
|
|
43
82
|
var was_playing: bool = EditorInterface.is_playing_scene()
|
|
44
83
|
if was_playing:
|
|
@@ -88,6 +127,17 @@ func restart_editor(_args: Dictionary) -> Dictionary:
|
|
|
88
127
|
|
|
89
128
|
func playing_status(_args: Dictionary) -> Dictionary:
|
|
90
129
|
var playing: bool = EditorInterface.is_playing_scene()
|
|
130
|
+
# The debugger's port with it, read now rather than remembered: it is taken again before every
|
|
131
|
+
# play, so the one from when this editor greeted the server is a number it has moved off.
|
|
132
|
+
var settings: EditorSettings = EditorInterface.get_editor_settings()
|
|
133
|
+
var debugger: int = (
|
|
134
|
+
int(settings.get_setting(DEBUGGER_SETTING))
|
|
135
|
+
if settings != null and settings.has_setting(DEBUGGER_SETTING)
|
|
136
|
+
else 0
|
|
137
|
+
)
|
|
91
138
|
return {
|
|
92
|
-
"ok": true,
|
|
139
|
+
"ok": true,
|
|
140
|
+
"playing": playing,
|
|
141
|
+
"scenePath": EditorInterface.get_playing_scene() if playing else "",
|
|
142
|
+
"debugPort": debugger
|
|
93
143
|
}
|
|
@@ -435,6 +435,16 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
435
435
|
)
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
if not params.has("index") and str(params.get("text", "")).is_empty():
|
|
439
|
+
return {
|
|
440
|
+
"type": "error",
|
|
441
|
+
"message":
|
|
442
|
+
(
|
|
443
|
+
"%s needs the item named, by text or index. It holds: %s"
|
|
444
|
+
% [node_path, ", ".join(_items_of(menu))]
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
|
|
438
448
|
var index: int = _wanted_item(menu, params)
|
|
439
449
|
if index < 0:
|
|
440
450
|
return {
|
package/build/index.js
CHANGED
|
@@ -24861,10 +24861,11 @@ class FrameReader {
|
|
|
24861
24861
|
}
|
|
24862
24862
|
|
|
24863
24863
|
// src/ports.ts
|
|
24864
|
-
|
|
24864
|
+
import { createServer } from "node:net";
|
|
24865
|
+
function portFromEnvOrNull(variable) {
|
|
24865
24866
|
const raw = process.env[variable]?.trim();
|
|
24866
24867
|
if (!raw) {
|
|
24867
|
-
return
|
|
24868
|
+
return null;
|
|
24868
24869
|
}
|
|
24869
24870
|
const parsed = Number.parseInt(raw, 10);
|
|
24870
24871
|
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535 || String(parsed) !== raw) {
|
|
@@ -24872,6 +24873,36 @@ function portFromEnv(variable, fallback) {
|
|
|
24872
24873
|
}
|
|
24873
24874
|
return parsed;
|
|
24874
24875
|
}
|
|
24876
|
+
function portFromEnv(variable, fallback) {
|
|
24877
|
+
return portFromEnvOrNull(variable) ?? fallback;
|
|
24878
|
+
}
|
|
24879
|
+
async function freePort(preferred, host = "127.0.0.1") {
|
|
24880
|
+
const asked = await bindable(preferred, host);
|
|
24881
|
+
if (asked !== null) {
|
|
24882
|
+
return asked;
|
|
24883
|
+
}
|
|
24884
|
+
const spare = await bindable(0, host);
|
|
24885
|
+
if (spare === null) {
|
|
24886
|
+
throw new Refusal(`No port could be bound on ${host}.`);
|
|
24887
|
+
}
|
|
24888
|
+
return spare;
|
|
24889
|
+
}
|
|
24890
|
+
function bindable(port, host) {
|
|
24891
|
+
return new Promise((resolve) => {
|
|
24892
|
+
const probe = createServer();
|
|
24893
|
+
probe.once("error", () => {
|
|
24894
|
+
resolve(null);
|
|
24895
|
+
});
|
|
24896
|
+
probe.once("listening", () => {
|
|
24897
|
+
const bound = probe.address();
|
|
24898
|
+
const taken = typeof bound === "object" && bound !== null ? bound.port : port;
|
|
24899
|
+
probe.close(() => {
|
|
24900
|
+
resolve(taken);
|
|
24901
|
+
});
|
|
24902
|
+
});
|
|
24903
|
+
probe.listen(port, host);
|
|
24904
|
+
});
|
|
24905
|
+
}
|
|
24875
24906
|
|
|
24876
24907
|
// src/dap_client.ts
|
|
24877
24908
|
var DEFAULT_DAP_PORT = 6006;
|
|
@@ -25476,6 +25507,9 @@ function resolveDefaultBridgeHost() {
|
|
|
25476
25507
|
const host = process.env["GDHARNESS_BRIDGE_HOST"]?.trim();
|
|
25477
25508
|
return host === undefined || host === "" ? DEFAULT_HOST : host;
|
|
25478
25509
|
}
|
|
25510
|
+
function servedPort(said) {
|
|
25511
|
+
return typeof said === "number" && Number.isInteger(said) && said > 0 && said <= 65535 ? said : undefined;
|
|
25512
|
+
}
|
|
25479
25513
|
|
|
25480
25514
|
class GodotBridge extends EventEmitter {
|
|
25481
25515
|
httpServer = null;
|
|
@@ -25608,6 +25642,9 @@ class GodotBridge extends EventEmitter {
|
|
|
25608
25642
|
lastPongAt: this.connectionInfo?.lastPongAt,
|
|
25609
25643
|
addonVersion: this.connectionInfo?.addonVersion,
|
|
25610
25644
|
editorPid: this.connectionInfo?.editorPid,
|
|
25645
|
+
lspPort: this.connectionInfo?.lspPort,
|
|
25646
|
+
dapPort: this.connectionInfo?.dapPort,
|
|
25647
|
+
debugPort: this.connectionInfo?.debugPort,
|
|
25611
25648
|
pendingRequests: this.pendingRequests.size,
|
|
25612
25649
|
queuedResources: this.resourceQueues.size
|
|
25613
25650
|
};
|
|
@@ -25741,6 +25778,9 @@ class GodotBridge extends EventEmitter {
|
|
|
25741
25778
|
this.connectionInfo.projectPath = message.project_path;
|
|
25742
25779
|
this.connectionInfo.addonVersion = message.addon_version ?? "";
|
|
25743
25780
|
this.connectionInfo.editorPid = message.editor_pid;
|
|
25781
|
+
this.connectionInfo.lspPort = servedPort(message.lsp_port);
|
|
25782
|
+
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
25783
|
+
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
25744
25784
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
25745
25785
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
25746
25786
|
}
|
|
@@ -26013,8 +26053,8 @@ function runArguments(options) {
|
|
|
26013
26053
|
}
|
|
26014
26054
|
return args;
|
|
26015
26055
|
}
|
|
26016
|
-
function editorArguments(projectPath) {
|
|
26017
|
-
return ["-e", "--path", projectPath];
|
|
26056
|
+
function editorArguments(projectPath, ports) {
|
|
26057
|
+
return ["-e", "--path", projectPath, "--lsp-port", String(ports.lsp), "--dap-port", String(ports.dap)];
|
|
26018
26058
|
}
|
|
26019
26059
|
function userDataIn(home, variables = process.env) {
|
|
26020
26060
|
const moved = ["APPDATA", "XDG_DATA_HOME"];
|
|
@@ -26911,10 +26951,10 @@ function asToolResponse(payload) {
|
|
|
26911
26951
|
]
|
|
26912
26952
|
};
|
|
26913
26953
|
}
|
|
26914
|
-
function normalizeLSPError(error) {
|
|
26954
|
+
function normalizeLSPError(error, port) {
|
|
26915
26955
|
if (error instanceof Error) {
|
|
26916
26956
|
if (error.message.includes("ECONNREFUSED") || error.message.includes("Failed to connect to Godot LSP") || error.message.includes("socket closed")) {
|
|
26917
|
-
return `Godot LSP is unavailable on port ${
|
|
26957
|
+
return `Godot LSP is unavailable on port ${port}. Start the Godot editor and enable Language Server in Editor Settings, or set GDHARNESS_LSP_PORT to the port it serves.`;
|
|
26918
26958
|
}
|
|
26919
26959
|
return error.message;
|
|
26920
26960
|
}
|
|
@@ -26992,7 +27032,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
26992
27032
|
}
|
|
26993
27033
|
} catch (error) {
|
|
26994
27034
|
return asToolResponse({
|
|
26995
|
-
error: normalizeLSPError(error)
|
|
27035
|
+
error: normalizeLSPError(error, client.port)
|
|
26996
27036
|
});
|
|
26997
27037
|
}
|
|
26998
27038
|
}
|
|
@@ -29185,7 +29225,7 @@ class GodotServer {
|
|
|
29185
29225
|
return {
|
|
29186
29226
|
ok: false,
|
|
29187
29227
|
response: this.createErrorResponse(`The editor is playing the game but its debug adapter did not answer: ${errorMessage(error)}`, [
|
|
29188
|
-
|
|
29228
|
+
`This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
|
|
29189
29229
|
"GDHARNESS_DAP_PORT points this server at another one"
|
|
29190
29230
|
])
|
|
29191
29231
|
};
|
|
@@ -29552,7 +29592,8 @@ class GodotServer {
|
|
|
29552
29592
|
if (payload["error"] !== undefined) {
|
|
29553
29593
|
const reason = payload["error"];
|
|
29554
29594
|
return this.createErrorResponse(`Diagnostics unavailable: ${typeof reason === "string" ? reason : JSON.stringify(reason)}`, [
|
|
29555
|
-
|
|
29595
|
+
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
29596
|
+
"GDHARNESS_LSP_PORT points this server at another one"
|
|
29556
29597
|
]);
|
|
29557
29598
|
}
|
|
29558
29599
|
const diagnostics = readArray(payload, "diagnostics") ?? [];
|
|
@@ -29569,16 +29610,33 @@ class GodotServer {
|
|
|
29569
29610
|
});
|
|
29570
29611
|
}
|
|
29571
29612
|
async handleLSP(toolName, args) {
|
|
29572
|
-
this.
|
|
29613
|
+
const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
|
|
29614
|
+
if (this.lspClient !== null && this.lspClient.port !== port) {
|
|
29615
|
+
await this.lspClient.disconnect();
|
|
29616
|
+
this.lspClient = null;
|
|
29617
|
+
}
|
|
29618
|
+
this.lspClient ??= new GodotLSPClient(port);
|
|
29573
29619
|
return handleLSPTool(this.lspClient, toolName, args);
|
|
29574
29620
|
}
|
|
29575
29621
|
async handleDAP(toolName, args) {
|
|
29576
29622
|
return handleDAPTool(this.dap(), toolName, args);
|
|
29577
29623
|
}
|
|
29578
29624
|
dap() {
|
|
29579
|
-
this.
|
|
29625
|
+
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
29626
|
+
if (this.dapClient !== null && this.dapClient.port !== port) {
|
|
29627
|
+
this.dapClient = null;
|
|
29628
|
+
}
|
|
29629
|
+
this.dapClient ??= new GodotDAPClient(port);
|
|
29580
29630
|
return this.dapClient;
|
|
29581
29631
|
}
|
|
29632
|
+
editorServes(field, variable, fallback) {
|
|
29633
|
+
const named = portFromEnvOrNull(variable);
|
|
29634
|
+
if (named !== null) {
|
|
29635
|
+
return named;
|
|
29636
|
+
}
|
|
29637
|
+
const status = this.godotBridge.getStatus();
|
|
29638
|
+
return (status.connected ? status[field] : undefined) ?? fallback;
|
|
29639
|
+
}
|
|
29582
29640
|
getEditorStatusPayload() {
|
|
29583
29641
|
const status = this.godotBridge.getStatus();
|
|
29584
29642
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
@@ -29606,7 +29664,8 @@ class GodotServer {
|
|
|
29606
29664
|
const answer = await this.godotBridge.invokeTool("playing_status", {});
|
|
29607
29665
|
return {
|
|
29608
29666
|
playing: readBoolean(asParams(answer), "playing") ?? false,
|
|
29609
|
-
scene: readString(asParams(answer), "scenePath") ?? ""
|
|
29667
|
+
scene: readString(asParams(answer), "scenePath") ?? "",
|
|
29668
|
+
debugPort: readNumber(asParams(answer), "debugPort")
|
|
29610
29669
|
};
|
|
29611
29670
|
} catch {
|
|
29612
29671
|
return null;
|
|
@@ -29624,15 +29683,19 @@ class GodotServer {
|
|
|
29624
29683
|
problem: reply.ok ? null : reply.message
|
|
29625
29684
|
};
|
|
29626
29685
|
}));
|
|
29686
|
+
const playing = await this.editorPlayingState();
|
|
29627
29687
|
return this.jsonTextResponse({
|
|
29628
|
-
editor:
|
|
29688
|
+
editor: {
|
|
29689
|
+
...this.getEditorStatusPayload(),
|
|
29690
|
+
debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort
|
|
29691
|
+
},
|
|
29629
29692
|
godot: {
|
|
29630
29693
|
path: godotPath,
|
|
29631
29694
|
version: godotPath === null ? null : await this.godotVersion(godotPath)
|
|
29632
29695
|
},
|
|
29633
29696
|
game: {
|
|
29634
29697
|
processActive: this.activeProcess !== null,
|
|
29635
|
-
playingInEditor:
|
|
29698
|
+
playingInEditor: playing,
|
|
29636
29699
|
runtimeConnected: games.some((game) => game.reachable),
|
|
29637
29700
|
runtimes: games
|
|
29638
29701
|
}
|
|
@@ -29691,10 +29754,10 @@ class GodotServer {
|
|
|
29691
29754
|
return project.response;
|
|
29692
29755
|
}
|
|
29693
29756
|
if (this.godotBridge.isConnected()) {
|
|
29694
|
-
return this.createErrorResponse("An editor is already connected to this server
|
|
29757
|
+
return this.createErrorResponse("An editor is already connected to this server.", [
|
|
29695
29758
|
"editor_status says which editor is answering, and for which project",
|
|
29696
29759
|
"editor_launch restart replaces the connected editor rather than joining it",
|
|
29697
|
-
"
|
|
29760
|
+
"Another project wants its own gdharness server, which is its own harness session"
|
|
29698
29761
|
]);
|
|
29699
29762
|
}
|
|
29700
29763
|
const engine = await this.engine();
|
|
@@ -29702,10 +29765,16 @@ class GodotServer {
|
|
|
29702
29765
|
return engine.response;
|
|
29703
29766
|
}
|
|
29704
29767
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
29705
|
-
const
|
|
29768
|
+
const ports = await this.portsForAnEditor();
|
|
29769
|
+
const editor = spawn(engine.value, editorArguments(project.value.path, ports), {
|
|
29706
29770
|
stdio: "ignore",
|
|
29707
29771
|
detached: true,
|
|
29708
|
-
env: {
|
|
29772
|
+
env: {
|
|
29773
|
+
...process.env,
|
|
29774
|
+
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
29775
|
+
GDHARNESS_LSP_PORT: String(ports.lsp),
|
|
29776
|
+
GDHARNESS_DAP_PORT: String(ports.dap)
|
|
29777
|
+
}
|
|
29709
29778
|
});
|
|
29710
29779
|
const started = await new Promise((resolve) => {
|
|
29711
29780
|
editor.once("spawn", () => {
|
|
@@ -29722,9 +29791,16 @@ class GodotServer {
|
|
|
29722
29791
|
return this.jsonTextResponse({
|
|
29723
29792
|
launched: true,
|
|
29724
29793
|
pid: editor.pid ?? null,
|
|
29725
|
-
projectPath: project.value.path
|
|
29794
|
+
projectPath: project.value.path,
|
|
29795
|
+
lspPort: ports.lsp,
|
|
29796
|
+
dapPort: ports.dap
|
|
29726
29797
|
});
|
|
29727
29798
|
}
|
|
29799
|
+
async portsForAnEditor() {
|
|
29800
|
+
const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
|
|
29801
|
+
const dap = portFromEnvOrNull("GDHARNESS_DAP_PORT") ?? await freePort(DEFAULT_DAP_PORT);
|
|
29802
|
+
return { lsp, dap };
|
|
29803
|
+
}
|
|
29728
29804
|
async handleRunProject(args, op) {
|
|
29729
29805
|
const project = this.project(args);
|
|
29730
29806
|
if (!project.ok) {
|
|
@@ -29792,7 +29868,7 @@ class GodotServer {
|
|
|
29792
29868
|
await this.dap().connect();
|
|
29793
29869
|
} catch (error) {
|
|
29794
29870
|
return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
|
|
29795
|
-
|
|
29871
|
+
`This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
|
|
29796
29872
|
"GDHARNESS_DAP_PORT points this server at another one"
|
|
29797
29873
|
]);
|
|
29798
29874
|
}
|
|
@@ -29814,6 +29890,7 @@ class GodotServer {
|
|
|
29814
29890
|
through: "editor",
|
|
29815
29891
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
29816
29892
|
refreshedClasses,
|
|
29893
|
+
debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
|
|
29817
29894
|
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it."
|
|
29818
29895
|
});
|
|
29819
29896
|
}
|
|
@@ -29996,7 +30073,7 @@ class GodotServer {
|
|
|
29996
30073
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
29997
30074
|
const { op: _op, projectPath, ...params } = asParams(args);
|
|
29998
30075
|
const announced = runtimesAnnounced();
|
|
29999
|
-
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : undefined, announced.unspoken);
|
|
30076
|
+
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken);
|
|
30000
30077
|
if ("problem" in choice) {
|
|
30001
30078
|
return this.createErrorResponse(choice.problem);
|
|
30002
30079
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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",
|