gdharness 0.13.40 → 0.13.41
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
|
@@ -16061,7 +16061,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16061
16061
|
},
|
|
16062
16062
|
{
|
|
16063
16063
|
name: "editor_status",
|
|
16064
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
16064
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
16065
16065
|
parameters: {},
|
|
16066
16066
|
requires: []
|
|
16067
16067
|
},
|
|
@@ -36447,7 +36447,7 @@ function secondsFromClock(printed) {
|
|
|
36447
36447
|
const extra = days === null ? 0 : Number(days) * 86400;
|
|
36448
36448
|
return Number.isFinite(extra) ? seconds + extra : undefined;
|
|
36449
36449
|
}
|
|
36450
|
-
var run5, ASK_TIMEOUT_MS =
|
|
36450
|
+
var run5, ASK_TIMEOUT_MS = 6000;
|
|
36451
36451
|
var init_process_time = __esm(() => {
|
|
36452
36452
|
run5 = promisify5(execFile5);
|
|
36453
36453
|
});
|
|
@@ -36676,7 +36676,18 @@ function parseAnnouncement(file, pid) {
|
|
|
36676
36676
|
if (protocol !== RUNTIME_PROTOCOL) {
|
|
36677
36677
|
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
|
|
36678
36678
|
}
|
|
36679
|
-
|
|
36679
|
+
const editorPid = readNumber(fields, "editor_pid");
|
|
36680
|
+
return {
|
|
36681
|
+
kind: "runtime",
|
|
36682
|
+
endpoint: {
|
|
36683
|
+
pid,
|
|
36684
|
+
port,
|
|
36685
|
+
address,
|
|
36686
|
+
project: named,
|
|
36687
|
+
file,
|
|
36688
|
+
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
|
|
36689
|
+
}
|
|
36690
|
+
};
|
|
36680
36691
|
}
|
|
36681
36692
|
function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
36682
36693
|
const running = new Map;
|
|
@@ -36750,7 +36761,7 @@ async function announcedSince(projectPath, before, waiting = {}) {
|
|
|
36750
36761
|
const directories = waiting.directories ?? runtimeDirectories();
|
|
36751
36762
|
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
36752
36763
|
for (;; ) {
|
|
36753
|
-
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
|
|
36764
|
+
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
|
|
36754
36765
|
if (fresh !== undefined) {
|
|
36755
36766
|
return fresh;
|
|
36756
36767
|
}
|
|
@@ -37425,6 +37436,16 @@ function hasMainScene(projectFile) {
|
|
|
37425
37436
|
const scene = parseProjectGodot(readFileSync17(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
37426
37437
|
return typeof scene === "string" && scene !== "";
|
|
37427
37438
|
}
|
|
37439
|
+
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
37440
|
+
if (projectPath === null) {
|
|
37441
|
+
return false;
|
|
37442
|
+
}
|
|
37443
|
+
try {
|
|
37444
|
+
return readFileSync17(join19(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
|
|
37445
|
+
} catch {
|
|
37446
|
+
return false;
|
|
37447
|
+
}
|
|
37448
|
+
}
|
|
37428
37449
|
function editorPlaysHeadless(projectFile) {
|
|
37429
37450
|
const runArgs = parseProjectGodot(readFileSync17(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
37430
37451
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
@@ -38862,7 +38883,7 @@ class GodotServer {
|
|
|
38862
38883
|
const located = await this.locator.find();
|
|
38863
38884
|
const announced = runtimesAnnounced();
|
|
38864
38885
|
const pinged = Promise.all(announced.running.map(async (endpoint) => {
|
|
38865
|
-
const held = this.knownHoldOn(endpoint);
|
|
38886
|
+
const held = this.knownHoldOn(endpoint, announced.running);
|
|
38866
38887
|
const reply = held === null ? await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS) : {
|
|
38867
38888
|
ok: false,
|
|
38868
38889
|
message: `The game is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is. debug_control continue lets it go.`
|
|
@@ -38871,6 +38892,7 @@ class GodotServer {
|
|
|
38871
38892
|
pid: endpoint.pid,
|
|
38872
38893
|
port: endpoint.port,
|
|
38873
38894
|
project: endpoint.project,
|
|
38895
|
+
editorPid: endpoint.editorPid,
|
|
38874
38896
|
reachable: reply.ok,
|
|
38875
38897
|
problem: reply.ok ? null : reply.message
|
|
38876
38898
|
};
|
|
@@ -39166,6 +39188,7 @@ class GodotServer {
|
|
|
39166
39188
|
let editorSaysGoing = true;
|
|
39167
39189
|
const endpoint = await announcedSince(projectPath, before, {
|
|
39168
39190
|
budgetMs,
|
|
39191
|
+
accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
|
|
39169
39192
|
giveUp: () => {
|
|
39170
39193
|
if (this.dapClient?.isStopped() === true) {
|
|
39171
39194
|
return true;
|
|
@@ -39471,18 +39494,39 @@ class GodotServer {
|
|
|
39471
39494
|
};
|
|
39472
39495
|
this.activeProcess = picked;
|
|
39473
39496
|
}
|
|
39474
|
-
announcedPidOf(run) {
|
|
39497
|
+
announcedPidOf(run, announced) {
|
|
39475
39498
|
if (!run.throughEditor || run.announcedPid !== undefined) {
|
|
39476
39499
|
return run.announcedPid;
|
|
39477
39500
|
}
|
|
39478
39501
|
const before = run.announcedBefore ?? new Set;
|
|
39479
|
-
const fresh = this.allAnnouncedForOurProject(runtimesAnnounced().running).filter((one) => !before.has(one.pid));
|
|
39480
|
-
const
|
|
39502
|
+
const fresh = this.allAnnouncedForOurProject(announced ?? runtimesAnnounced().running).filter((one) => !before.has(one.pid));
|
|
39503
|
+
const candidates = fresh.filter((one) => this.playedByOurEditor(one, run.projectPath));
|
|
39504
|
+
const theOne = candidates.length === 1 ? candidates[0] : undefined;
|
|
39481
39505
|
if (theOne !== undefined) {
|
|
39482
39506
|
run.announcedPid = theOne.pid;
|
|
39483
39507
|
}
|
|
39484
39508
|
return run.announcedPid;
|
|
39485
39509
|
}
|
|
39510
|
+
playedByOurEditor(endpoint, projectPath) {
|
|
39511
|
+
const editor = this.godotBridge.getStatus().editorPid;
|
|
39512
|
+
if (editor === undefined) {
|
|
39513
|
+
return true;
|
|
39514
|
+
}
|
|
39515
|
+
if (endpoint.editorPid !== undefined) {
|
|
39516
|
+
return endpoint.editorPid === editor;
|
|
39517
|
+
}
|
|
39518
|
+
return !runtimeAddonAnnouncesEditors(projectPath ?? this.ownProject);
|
|
39519
|
+
}
|
|
39520
|
+
isTheGameOf(run, endpoint, projectPath) {
|
|
39521
|
+
if (run === null) {
|
|
39522
|
+
return false;
|
|
39523
|
+
}
|
|
39524
|
+
if (run.throughEditor) {
|
|
39525
|
+
return this.playedByOurEditor(endpoint, projectPath);
|
|
39526
|
+
}
|
|
39527
|
+
const editor = this.godotBridge.getStatus().editorPid;
|
|
39528
|
+
return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
|
|
39529
|
+
}
|
|
39486
39530
|
adoptRecordedRun() {
|
|
39487
39531
|
const record = readRunRecord();
|
|
39488
39532
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
@@ -39626,6 +39670,9 @@ class GodotServer {
|
|
|
39626
39670
|
if (announced !== undefined && !alive(announced)) {
|
|
39627
39671
|
return false;
|
|
39628
39672
|
}
|
|
39673
|
+
if (announced !== undefined && editorSays === undefined) {
|
|
39674
|
+
return stillRunning(run);
|
|
39675
|
+
}
|
|
39629
39676
|
return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
|
|
39630
39677
|
}
|
|
39631
39678
|
async handleGetDebugOutput(args, waitedMs) {
|
|
@@ -39870,13 +39917,13 @@ class GodotServer {
|
|
|
39870
39917
|
const override = Number.parseInt(envValue("GDHARNESS_RUNTIME_TIMEOUT_MS") ?? "", 10);
|
|
39871
39918
|
return Number.isInteger(override) && override > 0 ? override : 1e4;
|
|
39872
39919
|
}
|
|
39873
|
-
knownHoldOn(endpoint) {
|
|
39920
|
+
knownHoldOn(endpoint, announced) {
|
|
39874
39921
|
const run = this.activeProcess;
|
|
39875
39922
|
const session = this.dapClient;
|
|
39876
39923
|
if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
|
|
39877
39924
|
return null;
|
|
39878
39925
|
}
|
|
39879
|
-
if (this.announcedPidOf(run) !== endpoint.pid || !session.holdIsKnown()) {
|
|
39926
|
+
if (this.announcedPidOf(run, announced) !== endpoint.pid || !session.holdIsKnown()) {
|
|
39880
39927
|
return null;
|
|
39881
39928
|
}
|
|
39882
39929
|
return session.whereItStopped();
|
|
@@ -39921,7 +39968,7 @@ class GodotServer {
|
|
|
39921
39968
|
if ("problem" in choice) {
|
|
39922
39969
|
return this.createErrorResponse(choice.problem);
|
|
39923
39970
|
}
|
|
39924
|
-
const held = this.knownHoldOn(choice.endpoint);
|
|
39971
|
+
const held = this.knownHoldOn(choice.endpoint, announced.running);
|
|
39925
39972
|
if (held !== null) {
|
|
39926
39973
|
return this.createErrorResponse(`The game (pid ${choice.endpoint.pid}) is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is.`, [
|
|
39927
39974
|
"debug_control continue lets it go, after which this call answers",
|
|
@@ -43,6 +43,12 @@ const DAP_ASKED: String = "GDHARNESS_DAP_PORT"
|
|
|
43
43
|
## What a server that opened this editor says about itself, so that being opened by one is stated
|
|
44
44
|
## rather than guessed at from the ports. Kept in step with `OPENED_BY_A_SERVER` in src/launch.ts.
|
|
45
45
|
const OPENED_BY_A_SERVER: String = "GDHARNESS_OPENED_BY_A_SERVER"
|
|
46
|
+
## Put into this editor's own environment as it loads, so every game it plays inherits it and
|
|
47
|
+
## the runtime addon can announce which editor played it. A game of the same project that some
|
|
48
|
+
## other server started has no such variable, or another editor's number, and that is what
|
|
49
|
+
## tells the two apart when both are announced at once. Kept in step with `EDITOR_PID_VARIABLE`
|
|
50
|
+
## in the runtime autoload.
|
|
51
|
+
const EDITOR_PID_VARIABLE: String = "GDHARNESS_EDITOR_PID"
|
|
46
52
|
|
|
47
53
|
## How long one attempt is given before the address is called a bad one.
|
|
48
54
|
##
|
|
@@ -93,6 +99,7 @@ func _ready() -> void:
|
|
|
93
99
|
_project_path = ProjectSettings.globalize_path("res://")
|
|
94
100
|
version_at_load = _loaded_version()
|
|
95
101
|
_keep_breakpoints_through_sessions()
|
|
102
|
+
OS.set_environment(EDITOR_PID_VARIABLE, str(OS.get_process_id()))
|
|
96
103
|
|
|
97
104
|
_reconnect_timer = Timer.new()
|
|
98
105
|
_reconnect_timer.one_shot = true
|
|
@@ -30,6 +30,12 @@ const SCRIPT_FLAGS: PackedStringArray = ["-s", "--script"]
|
|
|
30
30
|
## Serve a script run anyway, for somebody driving a `-s` script rather than a game.
|
|
31
31
|
const SCRIPT_RUNS_SETTING: String = "gdharness/runtime/serve_script_runs"
|
|
32
32
|
|
|
33
|
+
## The variable the editor addon puts into its own environment, which every game the editor plays
|
|
34
|
+
## inherits: it says which editor played this game, so a server can tell the editor's game from
|
|
35
|
+
## another of the same project that some other server started. Kept in step with
|
|
36
|
+
## `EDITOR_PID_VARIABLE` in the editor addon's bridge client.
|
|
37
|
+
const EDITOR_PID_VARIABLE: String = "GDHARNESS_EDITOR_PID"
|
|
38
|
+
|
|
33
39
|
var values: Values = Values.new()
|
|
34
40
|
|
|
35
41
|
# The modules are members and not locals of _init, because a Callable holds its object by id
|
|
@@ -250,7 +256,7 @@ func _announce(bind_address: String) -> void:
|
|
|
250
256
|
## What the announcement file and the welcome both carry: enough to pick this game out of
|
|
251
257
|
## several and to know whether the server speaks its protocol.
|
|
252
258
|
func _identity(bind_address: String) -> Dictionary:
|
|
253
|
-
|
|
259
|
+
var identity: Dictionary = {
|
|
254
260
|
"protocol": PROTOCOL,
|
|
255
261
|
"pid": OS.get_process_id(),
|
|
256
262
|
"port": _port,
|
|
@@ -262,6 +268,10 @@ func _identity(bind_address: String) -> Dictionary:
|
|
|
262
268
|
},
|
|
263
269
|
"godot": Engine.get_version_info().get("string", ""),
|
|
264
270
|
}
|
|
271
|
+
var played_by: String = OS.get_environment(EDITOR_PID_VARIABLE)
|
|
272
|
+
if played_by.is_valid_int():
|
|
273
|
+
identity["editor_pid"] = played_by.to_int()
|
|
274
|
+
return identity
|
|
265
275
|
|
|
266
276
|
|
|
267
277
|
func _send_welcome(client: StreamPeerTCP) -> void:
|
package/build/index.js
CHANGED
|
@@ -27713,7 +27713,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
27713
27713
|
import { execFile as execFile3 } from "node:child_process";
|
|
27714
27714
|
import { promisify as promisify3 } from "node:util";
|
|
27715
27715
|
var run3 = promisify3(execFile3);
|
|
27716
|
-
var ASK_TIMEOUT_MS =
|
|
27716
|
+
var ASK_TIMEOUT_MS = 6000;
|
|
27717
27717
|
async function cpuSecondsOf(pid) {
|
|
27718
27718
|
try {
|
|
27719
27719
|
return process.platform === "win32" ? await askWindows(pid) : await askPosix(pid);
|
|
@@ -28270,7 +28270,18 @@ function parseAnnouncement(file, pid) {
|
|
|
28270
28270
|
if (protocol !== RUNTIME_PROTOCOL) {
|
|
28271
28271
|
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
|
|
28272
28272
|
}
|
|
28273
|
-
|
|
28273
|
+
const editorPid = readNumber(fields, "editor_pid");
|
|
28274
|
+
return {
|
|
28275
|
+
kind: "runtime",
|
|
28276
|
+
endpoint: {
|
|
28277
|
+
pid,
|
|
28278
|
+
port,
|
|
28279
|
+
address,
|
|
28280
|
+
project: named,
|
|
28281
|
+
file,
|
|
28282
|
+
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
|
|
28283
|
+
}
|
|
28284
|
+
};
|
|
28274
28285
|
}
|
|
28275
28286
|
function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
28276
28287
|
const running = new Map;
|
|
@@ -28349,7 +28360,7 @@ async function announcedSince(projectPath, before, waiting = {}) {
|
|
|
28349
28360
|
const directories = waiting.directories ?? runtimeDirectories();
|
|
28350
28361
|
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
28351
28362
|
for (;; ) {
|
|
28352
|
-
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
|
|
28363
|
+
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
|
|
28353
28364
|
if (fresh !== undefined) {
|
|
28354
28365
|
return fresh;
|
|
28355
28366
|
}
|
|
@@ -29505,7 +29516,7 @@ var TOOL_SPECS = [
|
|
|
29505
29516
|
},
|
|
29506
29517
|
{
|
|
29507
29518
|
name: "editor_status",
|
|
29508
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
29519
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
29509
29520
|
parameters: {},
|
|
29510
29521
|
requires: []
|
|
29511
29522
|
},
|
|
@@ -30174,6 +30185,16 @@ function hasMainScene(projectFile) {
|
|
|
30174
30185
|
const scene = parseProjectGodot(readFileSync13(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
30175
30186
|
return typeof scene === "string" && scene !== "";
|
|
30176
30187
|
}
|
|
30188
|
+
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
30189
|
+
if (projectPath === null) {
|
|
30190
|
+
return false;
|
|
30191
|
+
}
|
|
30192
|
+
try {
|
|
30193
|
+
return readFileSync13(join13(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
30194
|
+
} catch {
|
|
30195
|
+
return false;
|
|
30196
|
+
}
|
|
30197
|
+
}
|
|
30177
30198
|
function editorPlaysHeadless(projectFile) {
|
|
30178
30199
|
const runArgs = parseProjectGodot(readFileSync13(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
30179
30200
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
@@ -31611,7 +31632,7 @@ class GodotServer {
|
|
|
31611
31632
|
const located = await this.locator.find();
|
|
31612
31633
|
const announced = runtimesAnnounced();
|
|
31613
31634
|
const pinged = Promise.all(announced.running.map(async (endpoint) => {
|
|
31614
|
-
const held = this.knownHoldOn(endpoint);
|
|
31635
|
+
const held = this.knownHoldOn(endpoint, announced.running);
|
|
31615
31636
|
const reply = held === null ? await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS) : {
|
|
31616
31637
|
ok: false,
|
|
31617
31638
|
message: `The game is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is. debug_control continue lets it go.`
|
|
@@ -31620,6 +31641,7 @@ class GodotServer {
|
|
|
31620
31641
|
pid: endpoint.pid,
|
|
31621
31642
|
port: endpoint.port,
|
|
31622
31643
|
project: endpoint.project,
|
|
31644
|
+
editorPid: endpoint.editorPid,
|
|
31623
31645
|
reachable: reply.ok,
|
|
31624
31646
|
problem: reply.ok ? null : reply.message
|
|
31625
31647
|
};
|
|
@@ -31915,6 +31937,7 @@ class GodotServer {
|
|
|
31915
31937
|
let editorSaysGoing = true;
|
|
31916
31938
|
const endpoint = await announcedSince(projectPath, before, {
|
|
31917
31939
|
budgetMs,
|
|
31940
|
+
accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
|
|
31918
31941
|
giveUp: () => {
|
|
31919
31942
|
if (this.dapClient?.isStopped() === true) {
|
|
31920
31943
|
return true;
|
|
@@ -32220,18 +32243,39 @@ class GodotServer {
|
|
|
32220
32243
|
};
|
|
32221
32244
|
this.activeProcess = picked;
|
|
32222
32245
|
}
|
|
32223
|
-
announcedPidOf(run) {
|
|
32246
|
+
announcedPidOf(run, announced) {
|
|
32224
32247
|
if (!run.throughEditor || run.announcedPid !== undefined) {
|
|
32225
32248
|
return run.announcedPid;
|
|
32226
32249
|
}
|
|
32227
32250
|
const before = run.announcedBefore ?? new Set;
|
|
32228
|
-
const fresh = this.allAnnouncedForOurProject(runtimesAnnounced().running).filter((one) => !before.has(one.pid));
|
|
32229
|
-
const
|
|
32251
|
+
const fresh = this.allAnnouncedForOurProject(announced ?? runtimesAnnounced().running).filter((one) => !before.has(one.pid));
|
|
32252
|
+
const candidates = fresh.filter((one) => this.playedByOurEditor(one, run.projectPath));
|
|
32253
|
+
const theOne = candidates.length === 1 ? candidates[0] : undefined;
|
|
32230
32254
|
if (theOne !== undefined) {
|
|
32231
32255
|
run.announcedPid = theOne.pid;
|
|
32232
32256
|
}
|
|
32233
32257
|
return run.announcedPid;
|
|
32234
32258
|
}
|
|
32259
|
+
playedByOurEditor(endpoint, projectPath) {
|
|
32260
|
+
const editor = this.godotBridge.getStatus().editorPid;
|
|
32261
|
+
if (editor === undefined) {
|
|
32262
|
+
return true;
|
|
32263
|
+
}
|
|
32264
|
+
if (endpoint.editorPid !== undefined) {
|
|
32265
|
+
return endpoint.editorPid === editor;
|
|
32266
|
+
}
|
|
32267
|
+
return !runtimeAddonAnnouncesEditors(projectPath ?? this.ownProject);
|
|
32268
|
+
}
|
|
32269
|
+
isTheGameOf(run, endpoint, projectPath) {
|
|
32270
|
+
if (run === null) {
|
|
32271
|
+
return false;
|
|
32272
|
+
}
|
|
32273
|
+
if (run.throughEditor) {
|
|
32274
|
+
return this.playedByOurEditor(endpoint, projectPath);
|
|
32275
|
+
}
|
|
32276
|
+
const editor = this.godotBridge.getStatus().editorPid;
|
|
32277
|
+
return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
|
|
32278
|
+
}
|
|
32235
32279
|
adoptRecordedRun() {
|
|
32236
32280
|
const record = readRunRecord();
|
|
32237
32281
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
@@ -32375,6 +32419,9 @@ class GodotServer {
|
|
|
32375
32419
|
if (announced !== undefined && !alive(announced)) {
|
|
32376
32420
|
return false;
|
|
32377
32421
|
}
|
|
32422
|
+
if (announced !== undefined && editorSays === undefined) {
|
|
32423
|
+
return stillRunning(run);
|
|
32424
|
+
}
|
|
32378
32425
|
return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
|
|
32379
32426
|
}
|
|
32380
32427
|
async handleGetDebugOutput(args, waitedMs) {
|
|
@@ -32619,13 +32666,13 @@ class GodotServer {
|
|
|
32619
32666
|
const override = Number.parseInt(envValue("GDHARNESS_RUNTIME_TIMEOUT_MS") ?? "", 10);
|
|
32620
32667
|
return Number.isInteger(override) && override > 0 ? override : 1e4;
|
|
32621
32668
|
}
|
|
32622
|
-
knownHoldOn(endpoint) {
|
|
32669
|
+
knownHoldOn(endpoint, announced) {
|
|
32623
32670
|
const run = this.activeProcess;
|
|
32624
32671
|
const session = this.dapClient;
|
|
32625
32672
|
if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
|
|
32626
32673
|
return null;
|
|
32627
32674
|
}
|
|
32628
|
-
if (this.announcedPidOf(run) !== endpoint.pid || !session.holdIsKnown()) {
|
|
32675
|
+
if (this.announcedPidOf(run, announced) !== endpoint.pid || !session.holdIsKnown()) {
|
|
32629
32676
|
return null;
|
|
32630
32677
|
}
|
|
32631
32678
|
return session.whereItStopped();
|
|
@@ -32670,7 +32717,7 @@ class GodotServer {
|
|
|
32670
32717
|
if ("problem" in choice) {
|
|
32671
32718
|
return this.createErrorResponse(choice.problem);
|
|
32672
32719
|
}
|
|
32673
|
-
const held = this.knownHoldOn(choice.endpoint);
|
|
32720
|
+
const held = this.knownHoldOn(choice.endpoint, announced.running);
|
|
32674
32721
|
if (held !== null) {
|
|
32675
32722
|
return this.createErrorResponse(`The game (pid ${choice.endpoint.pid}) is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is.`, [
|
|
32676
32723
|
"debug_control continue lets it go, after which this call answers",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.41",
|
|
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",
|