gdharness 1.1.22 → 1.1.23
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 +452 -219
- package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +7 -1
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +61 -8
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +105 -49
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +41 -19
- package/build/godot/operations/classdb_queries.gd +28 -17
- package/build/godot/operations/project_diagnostics.gd +2 -1
- package/build/index.js +442 -124
- package/build/keeper.js +4 -0
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -662,7 +662,7 @@ function environmentFor(options, variables = process.env) {
|
|
|
662
662
|
const base = options.savesIn === undefined ? { ...variables } : userDataIn(options.savesIn, variables);
|
|
663
663
|
return { ...base, ...options.env, GDHARNESS_RUNTIME_DIR: options.runtimeDirectory };
|
|
664
664
|
}
|
|
665
|
-
var OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", RESERVED_VARIABLE_PREFIX = "GDHARNESS_", SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
|
|
665
|
+
var OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", EDITOR_PID_VARIABLE = "GDHARNESS_EDITOR_PID", RESERVED_VARIABLE_PREFIX = "GDHARNESS_", SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
|
|
666
666
|
|
|
667
667
|
// src/godot-path.ts
|
|
668
668
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -8446,23 +8446,24 @@ function sameCodeNote(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
|
8446
8446
|
}
|
|
8447
8447
|
return `The editor loaded the ${addonVersion} addons, which are the same code this ${serverVersion} server ships, so nothing needs restarting; addonVersion changes at the next editor start.`;
|
|
8448
8448
|
}
|
|
8449
|
-
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
8449
|
+
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
|
|
8450
8450
|
if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
|
|
8451
8451
|
return;
|
|
8452
8452
|
}
|
|
8453
8453
|
const reported = addonVersion ?? "";
|
|
8454
|
+
const restart = onDisk !== undefined && onDisk !== shippedDigest ? `The project's own addons are not this server's either, so a restart would load the same code again: gdharness upgrade in the project installs the ${serverVersion} addons, and editor_launch restart then picks them up.` : "Restart it with editor_launch restart to pick this one up.";
|
|
8454
8455
|
if (reported === serverVersion) {
|
|
8455
|
-
return `The editor is running a different build of the ${reported} addon from the one this server ships.
|
|
8456
|
+
return `The editor is running a different build of the ${reported} addon from the one this server ships. ${restart}`;
|
|
8456
8457
|
}
|
|
8457
8458
|
const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
|
|
8458
8459
|
const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
|
|
8459
8460
|
if (reported !== "" && isNewer(reported, serverVersion)) {
|
|
8460
8461
|
return `${both} This server is the older half: reconnect it in your harness so it spawns ${reported}.`;
|
|
8461
8462
|
}
|
|
8462
|
-
return `${both}
|
|
8463
|
+
return `${both} ${restart}`;
|
|
8463
8464
|
}
|
|
8464
|
-
function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
8465
|
-
const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest);
|
|
8465
|
+
function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
|
|
8466
|
+
const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk);
|
|
8466
8467
|
if (staleNote === undefined || typeof answer !== "object" || answer === null || Array.isArray(answer)) {
|
|
8467
8468
|
return answer;
|
|
8468
8469
|
}
|
|
@@ -8608,6 +8609,73 @@ var init_issues = __esm(() => {
|
|
|
8608
8609
|
ENHANCEMENT_URL = `${NEW_ISSUE}?template=feature_request.md`;
|
|
8609
8610
|
});
|
|
8610
8611
|
|
|
8612
|
+
// src/paths.ts
|
|
8613
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
8614
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
8615
|
+
function realPathOr(path) {
|
|
8616
|
+
try {
|
|
8617
|
+
return realpathSync2.native(path);
|
|
8618
|
+
} catch {
|
|
8619
|
+
return path;
|
|
8620
|
+
}
|
|
8621
|
+
}
|
|
8622
|
+
function isSameDirectory(onePath, otherPath) {
|
|
8623
|
+
return relative(realPathOr(resolve(onePath)), realPathOr(resolve(otherPath))) === "";
|
|
8624
|
+
}
|
|
8625
|
+
function isWithinRoot(rootPath, candidatePath) {
|
|
8626
|
+
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
8627
|
+
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
8628
|
+
}
|
|
8629
|
+
function resolveWithinProject(projectPath, candidatePath) {
|
|
8630
|
+
const trimmed = candidatePath.trim();
|
|
8631
|
+
if (trimmed === "") {
|
|
8632
|
+
return { ok: false, reason: "Path is empty." };
|
|
8633
|
+
}
|
|
8634
|
+
if (trimmed.includes("\x00")) {
|
|
8635
|
+
return { ok: false, reason: "Path contains a null byte." };
|
|
8636
|
+
}
|
|
8637
|
+
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
8638
|
+
if (withoutScheme.startsWith("uid://")) {
|
|
8639
|
+
return {
|
|
8640
|
+
ok: false,
|
|
8641
|
+
reason: `Path '${trimmed}' is a UID, and a file is judged here by where it is on disk, which a UID does not say. Give the file's res:// path.`
|
|
8642
|
+
};
|
|
8643
|
+
}
|
|
8644
|
+
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
8645
|
+
if (scheme) {
|
|
8646
|
+
return {
|
|
8647
|
+
ok: false,
|
|
8648
|
+
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
|
|
8649
|
+
};
|
|
8650
|
+
}
|
|
8651
|
+
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
8652
|
+
return {
|
|
8653
|
+
ok: false,
|
|
8654
|
+
reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
|
|
8655
|
+
};
|
|
8656
|
+
}
|
|
8657
|
+
if (withoutScheme.includes(":")) {
|
|
8658
|
+
return {
|
|
8659
|
+
ok: false,
|
|
8660
|
+
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
8661
|
+
};
|
|
8662
|
+
}
|
|
8663
|
+
const root = resolve(projectPath);
|
|
8664
|
+
const absolutePath = resolve(root, withoutScheme);
|
|
8665
|
+
if (!isWithinRoot(root, absolutePath)) {
|
|
8666
|
+
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
8667
|
+
}
|
|
8668
|
+
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
8669
|
+
if (relativePath === "") {
|
|
8670
|
+
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
8671
|
+
}
|
|
8672
|
+
return { ok: true, absolutePath, relativePath };
|
|
8673
|
+
}
|
|
8674
|
+
var RESOURCE_SCHEME = "res://", OTHER_SCHEME;
|
|
8675
|
+
var init_paths = __esm(() => {
|
|
8676
|
+
OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
8677
|
+
});
|
|
8678
|
+
|
|
8611
8679
|
// src/class-cache.ts
|
|
8612
8680
|
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync3 } from "node:fs";
|
|
8613
8681
|
import { join as join9 } from "node:path";
|
|
@@ -16050,80 +16118,13 @@ var init_types = __esm(() => {
|
|
|
16050
16118
|
};
|
|
16051
16119
|
});
|
|
16052
16120
|
|
|
16053
|
-
// src/paths.ts
|
|
16054
|
-
import { realpathSync as realpathSync2 } from "node:fs";
|
|
16055
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
16056
|
-
function realPathOr(path) {
|
|
16057
|
-
try {
|
|
16058
|
-
return realpathSync2.native(path);
|
|
16059
|
-
} catch {
|
|
16060
|
-
return path;
|
|
16061
|
-
}
|
|
16062
|
-
}
|
|
16063
|
-
function isSameDirectory(onePath, otherPath) {
|
|
16064
|
-
return relative(realPathOr(resolve(onePath)), realPathOr(resolve(otherPath))) === "";
|
|
16065
|
-
}
|
|
16066
|
-
function isWithinRoot(rootPath, candidatePath) {
|
|
16067
|
-
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
16068
|
-
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
16069
|
-
}
|
|
16070
|
-
function resolveWithinProject(projectPath, candidatePath) {
|
|
16071
|
-
const trimmed = candidatePath.trim();
|
|
16072
|
-
if (trimmed === "") {
|
|
16073
|
-
return { ok: false, reason: "Path is empty." };
|
|
16074
|
-
}
|
|
16075
|
-
if (trimmed.includes("\x00")) {
|
|
16076
|
-
return { ok: false, reason: "Path contains a null byte." };
|
|
16077
|
-
}
|
|
16078
|
-
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
16079
|
-
if (withoutScheme.startsWith("uid://")) {
|
|
16080
|
-
return {
|
|
16081
|
-
ok: false,
|
|
16082
|
-
reason: `Path '${trimmed}' is a UID, and a file is judged here by where it is on disk, which a UID does not say. Give the file's res:// path.`
|
|
16083
|
-
};
|
|
16084
|
-
}
|
|
16085
|
-
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
16086
|
-
if (scheme) {
|
|
16087
|
-
return {
|
|
16088
|
-
ok: false,
|
|
16089
|
-
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
|
|
16090
|
-
};
|
|
16091
|
-
}
|
|
16092
|
-
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
16093
|
-
return {
|
|
16094
|
-
ok: false,
|
|
16095
|
-
reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
|
|
16096
|
-
};
|
|
16097
|
-
}
|
|
16098
|
-
if (withoutScheme.includes(":")) {
|
|
16099
|
-
return {
|
|
16100
|
-
ok: false,
|
|
16101
|
-
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
16102
|
-
};
|
|
16103
|
-
}
|
|
16104
|
-
const root = resolve(projectPath);
|
|
16105
|
-
const absolutePath = resolve(root, withoutScheme);
|
|
16106
|
-
if (!isWithinRoot(root, absolutePath)) {
|
|
16107
|
-
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
16108
|
-
}
|
|
16109
|
-
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
16110
|
-
if (relativePath === "") {
|
|
16111
|
-
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
16112
|
-
}
|
|
16113
|
-
return { ok: true, absolutePath, relativePath };
|
|
16114
|
-
}
|
|
16115
|
-
var RESOURCE_SCHEME = "res://", OTHER_SCHEME;
|
|
16116
|
-
var init_paths = __esm(() => {
|
|
16117
|
-
OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
16118
|
-
});
|
|
16119
|
-
|
|
16120
16121
|
// src/resources.ts
|
|
16121
|
-
import { readFileSync as readFileSync8 } from "node:fs";
|
|
16122
|
+
import { readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
|
|
16122
16123
|
import { extname, resolve as resolve2 } from "node:path";
|
|
16123
16124
|
function ensureProjectPath(getProjectPath) {
|
|
16124
16125
|
const projectPath = getProjectPath();
|
|
16125
16126
|
if (!projectPath) {
|
|
16126
|
-
throw new Refusal("
|
|
16127
|
+
throw new Refusal("This server knows no project to read from: start it with GDHARNESS_PROJECT, connect an editor, or make any tool call naming a projectPath that holds project.godot.");
|
|
16127
16128
|
}
|
|
16128
16129
|
return resolve2(projectPath);
|
|
16129
16130
|
}
|
|
@@ -16139,7 +16140,22 @@ function resolveProjectFile(projectPath, resourcePath) {
|
|
|
16139
16140
|
if (!contained.ok) {
|
|
16140
16141
|
throw new Refusal(contained.reason);
|
|
16141
16142
|
}
|
|
16142
|
-
|
|
16143
|
+
let root;
|
|
16144
|
+
try {
|
|
16145
|
+
root = realpathSync3.native(projectPath);
|
|
16146
|
+
} catch {
|
|
16147
|
+
throw new Refusal(`The project directory ${projectPath} is not there.`);
|
|
16148
|
+
}
|
|
16149
|
+
let real;
|
|
16150
|
+
try {
|
|
16151
|
+
real = realpathSync3.native(contained.absolutePath);
|
|
16152
|
+
} catch {
|
|
16153
|
+
throw new Refusal(`No file at ${resourcePath} in the project.`);
|
|
16154
|
+
}
|
|
16155
|
+
if (!isWithinRoot(root, real)) {
|
|
16156
|
+
throw new Refusal(`${resourcePath} leads outside the project through a link, so it is not read.`);
|
|
16157
|
+
}
|
|
16158
|
+
return real;
|
|
16143
16159
|
}
|
|
16144
16160
|
function parseGodotUri(uri) {
|
|
16145
16161
|
if (uri === "godot://project/info") {
|
|
@@ -17395,13 +17411,13 @@ var init_tool_definitions = __esm(() => {
|
|
|
17395
17411
|
},
|
|
17396
17412
|
{
|
|
17397
17413
|
name: "editor_launch",
|
|
17398
|
-
description: "Opens the Godot editor on a project, or restarts the one already connected. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart.",
|
|
17414
|
+
description: "Opens the Godot editor on a project, or restarts the one already connected. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
17399
17415
|
parameters: {
|
|
17400
17416
|
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
17401
17417
|
hidden: {
|
|
17402
17418
|
type: "boolean",
|
|
17403
17419
|
ops: ["open", "restart"],
|
|
17404
|
-
description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false: one visible editor, shown without taking the focus."
|
|
17420
|
+
description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false: one visible editor, shown without taking the focus. On restart only for an editor a server opened; one opened by hand comes back as it was, so hidden is refused there."
|
|
17405
17421
|
}
|
|
17406
17422
|
},
|
|
17407
17423
|
requires: [],
|
|
@@ -17489,7 +17505,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17489
17505
|
requires: ["projectPath"]
|
|
17490
17506
|
},
|
|
17491
17507
|
stop: {
|
|
17492
|
-
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. A game that started processes of its own keeps them unless andChildren says to end those too. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled",
|
|
17508
|
+
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. A game that started processes of its own keeps them unless andChildren says to end those too. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, and false with the number under stillGoing when what was told to end was still running ten seconds later",
|
|
17493
17509
|
requires: []
|
|
17494
17510
|
},
|
|
17495
17511
|
check: {
|
|
@@ -17550,7 +17566,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17550
17566
|
},
|
|
17551
17567
|
{
|
|
17552
17568
|
name: "editor_status",
|
|
17553
|
-
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: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. 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. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. 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. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait.",
|
|
17569
|
+
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: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. 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. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. 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. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. 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. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs.",
|
|
17554
17570
|
parameters: {},
|
|
17555
17571
|
requires: []
|
|
17556
17572
|
},
|
|
@@ -17589,6 +17605,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17589
17605
|
"physics2d",
|
|
17590
17606
|
"physics3d",
|
|
17591
17607
|
"audio",
|
|
17608
|
+
"visual",
|
|
17592
17609
|
"animation",
|
|
17593
17610
|
"ui"
|
|
17594
17611
|
],
|
|
@@ -17628,7 +17645,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17628
17645
|
property: {
|
|
17629
17646
|
type: "string",
|
|
17630
17647
|
ops: ["property", "find"],
|
|
17631
|
-
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
17648
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
17632
17649
|
},
|
|
17633
17650
|
depth: {
|
|
17634
17651
|
type: "integer",
|
|
@@ -17667,13 +17684,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
17667
17684
|
limit: {
|
|
17668
17685
|
type: "integer",
|
|
17669
17686
|
minimum: 1,
|
|
17687
|
+
maximum: 5000,
|
|
17670
17688
|
ops: ["find", "text"],
|
|
17671
|
-
description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with omitted saying how many lines that left behind. A screen whose dialog sits under a long list is a screen read with omitted greater than zero, so raise this or point root at the dialog rather than reading the answer as what is on screen."
|
|
17689
|
+
description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with omitted saying how many lines that left behind. At most 5000 for either. A screen whose dialog sits under a long list is a screen read with omitted greater than zero, so raise this or point root at the dialog rather than reading the answer as what is on screen."
|
|
17672
17690
|
},
|
|
17673
17691
|
includeHidden: {
|
|
17674
17692
|
type: "boolean",
|
|
17675
17693
|
ops: ["text", "find"],
|
|
17676
|
-
description: "text: read hidden nodes as well, for checking that something is not showing. Default false. find: default true, since a find means the node whether or not it is drawn; false answers with what the player can actually see, which is how a panel that keeps a label for every line and hides the ones that do not is read. A node counts as hidden when
|
|
17694
|
+
description: "text: read hidden nodes as well, for checking that something is not showing. Default false. find: default true, since a find means the node whether or not it is drawn; false answers with what the player can actually see, which is how a panel that keeps a label for every line and hides the ones that do not is read. A node counts as hidden when the engine would not draw it: a hidden Control, Node2D or Node3D above it hides it, a plain Node in between breaks that chain as it does for the engine, and a hidden canvas layer or window hides everything in it. How many matches were left out comes back under hidden."
|
|
17677
17695
|
},
|
|
17678
17696
|
metrics: {
|
|
17679
17697
|
type: "array",
|
|
@@ -17772,7 +17790,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17772
17790
|
},
|
|
17773
17791
|
{
|
|
17774
17792
|
name: "runtime_capture",
|
|
17775
|
-
description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
17793
|
+
description: "A picture of the running game: the whole screen or one viewport, as an image. The whole screen includes the game's own dialogs and popups where they sit, also when a project opens them as windows of their own (subwindows not embedded). Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
17776
17794
|
parameters: {
|
|
17777
17795
|
projectPath: RUNNING_PROJECT_PATH,
|
|
17778
17796
|
pid: RUNNING_PID,
|
|
@@ -17946,7 +17964,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17946
17964
|
},
|
|
17947
17965
|
value: {
|
|
17948
17966
|
ops: ["until"],
|
|
17949
|
-
description: "until: the value to wait for, fitted to the property's type. On a property holding an object, whether declared to or untyped and holding one or null, the text null means null. One declared to hold an object refuses any value other than null, since it can never hold it."
|
|
17967
|
+
description: "until: the value to wait for, fitted to the property's type and compared at the precision the property keeps, so -1.1 meets a 32-bit float holding -1.10000002. A value the property would have to cut is refused: a fraction on a whole number, a number other than 0 or 1 on a bool. On a property holding an object, whether declared to or untyped and holding one or null, the text null means null. One declared to hold an object refuses any value other than null, since it can never hold it."
|
|
17950
17968
|
},
|
|
17951
17969
|
says: {
|
|
17952
17970
|
type: "string",
|
|
@@ -32870,6 +32888,12 @@ function parseAnnouncement(file, pid) {
|
|
|
32870
32888
|
}
|
|
32871
32889
|
};
|
|
32872
32890
|
}
|
|
32891
|
+
function dialAddress(address) {
|
|
32892
|
+
if (address === "*" || address === "0.0.0.0") {
|
|
32893
|
+
return "127.0.0.1";
|
|
32894
|
+
}
|
|
32895
|
+
return address === "::" ? "::1" : address;
|
|
32896
|
+
}
|
|
32873
32897
|
function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
32874
32898
|
const running = new Map;
|
|
32875
32899
|
const unspoken = new Map;
|
|
@@ -33159,7 +33183,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
|
|
|
33159
33183
|
let late = false;
|
|
33160
33184
|
let buffered = "";
|
|
33161
33185
|
let welcomed = false;
|
|
33162
|
-
const socket = createConnection({ port: endpoint.port, host: endpoint.address });
|
|
33186
|
+
const socket = createConnection({ port: endpoint.port, host: dialAddress(endpoint.address) });
|
|
33163
33187
|
let lateTimer = null;
|
|
33164
33188
|
const finish = (reply) => {
|
|
33165
33189
|
if (late) {
|
|
@@ -37372,7 +37396,10 @@ function mayYetConnect(listeningSince, now = Date.now()) {
|
|
|
37372
37396
|
return listeningSince === undefined || now - listeningSince.getTime() < CONNECT_WINDOW_MS;
|
|
37373
37397
|
}
|
|
37374
37398
|
function theEditorHasComeBack(status, startedAt) {
|
|
37375
|
-
return status
|
|
37399
|
+
return hasSaidWhoItIs(status) && (status.connectedAt?.getTime() ?? 0) > startedAt;
|
|
37400
|
+
}
|
|
37401
|
+
function hasSaidWhoItIs(status) {
|
|
37402
|
+
return status.connected && status.addonVersion !== undefined;
|
|
37376
37403
|
}
|
|
37377
37404
|
function anEditorIsStillComing(listeningSince, launchedEditorIsAlive, now = Date.now()) {
|
|
37378
37405
|
return mayYetConnect(listeningSince, now) || launchedEditorIsAlive;
|
|
@@ -37531,6 +37558,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
37531
37558
|
dapPort: this.connectionInfo?.dapPort,
|
|
37532
37559
|
debugPort: this.connectionInfo?.debugPort,
|
|
37533
37560
|
openedByAServer: this.connectionInfo?.openedByAServer,
|
|
37561
|
+
startedByAnEditor: this.connectionInfo?.startedByAnEditor,
|
|
37534
37562
|
syncsBreakpoints: this.connectionInfo?.syncsBreakpoints,
|
|
37535
37563
|
pendingRequests: this.pendingRequests.size,
|
|
37536
37564
|
queuedResources: this.resourceQueues.size,
|
|
@@ -37676,6 +37704,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
37676
37704
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
37677
37705
|
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
37678
37706
|
this.connectionInfo.openedByAServer = message.opened_by_a_server === true;
|
|
37707
|
+
this.connectionInfo.startedByAnEditor = typeof message.started_by_an_editor === "boolean" ? message.started_by_an_editor : undefined;
|
|
37679
37708
|
this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
|
|
37680
37709
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
37681
37710
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
@@ -37999,6 +38028,7 @@ function parseJUnit(xml) {
|
|
|
37999
38028
|
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
38000
38029
|
const failures = cases.filter((entry) => entry.status === "failed").length;
|
|
38001
38030
|
const errors = cases.filter((entry) => entry.status === "error").length;
|
|
38031
|
+
const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
|
|
38002
38032
|
return {
|
|
38003
38033
|
name: suite.attributes["name"] ?? "",
|
|
38004
38034
|
path: pathOf(suite),
|
|
@@ -38006,8 +38036,8 @@ function parseJUnit(xml) {
|
|
|
38006
38036
|
discovered: count(suite.attributes, "tests"),
|
|
38007
38037
|
failures,
|
|
38008
38038
|
errors,
|
|
38009
|
-
hookFailures: Math.max(0, count(suite.attributes, "failures") -
|
|
38010
|
-
hookErrors: Math.max(0, count(suite.attributes, "errors") -
|
|
38039
|
+
hookFailures: Math.max(0, count(suite.attributes, "failures") - written("failure")),
|
|
38040
|
+
hookErrors: Math.max(0, count(suite.attributes, "errors") - written("error")),
|
|
38011
38041
|
skipped: cases.filter((entry) => entry.status === "skipped").length,
|
|
38012
38042
|
time: count(suite.attributes, "time"),
|
|
38013
38043
|
cases
|
|
@@ -38201,7 +38231,7 @@ var init_junit = __esm(() => {
|
|
|
38201
38231
|
});
|
|
38202
38232
|
|
|
38203
38233
|
// src/lsp_client.ts
|
|
38204
|
-
import { existsSync as existsSync15, realpathSync as
|
|
38234
|
+
import { existsSync as existsSync15, realpathSync as realpathSync4 } from "node:fs";
|
|
38205
38235
|
import { readFile, realpath } from "node:fs/promises";
|
|
38206
38236
|
import { createConnection as createConnection3 } from "node:net";
|
|
38207
38237
|
import { dirname as dirname13, join as join19, resolve as resolve4 } from "node:path";
|
|
@@ -38217,7 +38247,7 @@ function diagnosticsKey(uri) {
|
|
|
38217
38247
|
return decoded;
|
|
38218
38248
|
}
|
|
38219
38249
|
try {
|
|
38220
|
-
return
|
|
38250
|
+
return realpathSync4.native(fileURLToPath3(decoded)).toLowerCase();
|
|
38221
38251
|
} catch {
|
|
38222
38252
|
return decoded.toLowerCase();
|
|
38223
38253
|
}
|
|
@@ -38917,7 +38947,7 @@ function projectStructure(projectPath) {
|
|
|
38917
38947
|
visit(join20(directory, entry.name));
|
|
38918
38948
|
} else if (entry.isFile()) {
|
|
38919
38949
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
38920
|
-
if (extension
|
|
38950
|
+
if (SCENE_EXTENSIONS.has(extension)) {
|
|
38921
38951
|
structure.scenes += 1;
|
|
38922
38952
|
} else if (extension === "gd" || extension === "cs") {
|
|
38923
38953
|
structure.scripts += 1;
|
|
@@ -38959,8 +38989,11 @@ function scriptsWithoutUid(projectPath) {
|
|
|
38959
38989
|
visit(projectPath, "");
|
|
38960
38990
|
return found.sort();
|
|
38961
38991
|
}
|
|
38992
|
+
function isBinary(bytes) {
|
|
38993
|
+
return bytes.subarray(0, 8000).includes(0);
|
|
38994
|
+
}
|
|
38962
38995
|
function searchProject(projectPath, options) {
|
|
38963
|
-
const extensions = new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
|
|
38996
|
+
const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
|
|
38964
38997
|
const result = {
|
|
38965
38998
|
query: options.query,
|
|
38966
38999
|
results: [],
|
|
@@ -38968,13 +39001,7 @@ function searchProject(projectPath, options) {
|
|
|
38968
39001
|
};
|
|
38969
39002
|
const regex = options.regex ? new RegExp(options.query, options.caseSensitive ? "" : "i") : null;
|
|
38970
39003
|
const needle = options.caseSensitive ? options.query : options.query.toLowerCase();
|
|
38971
|
-
const full = () =>
|
|
38972
|
-
if (result.summary.total_matches >= options.maxResults) {
|
|
38973
|
-
result.summary.truncated = true;
|
|
38974
|
-
return true;
|
|
38975
|
-
}
|
|
38976
|
-
return false;
|
|
38977
|
-
};
|
|
39004
|
+
const full = () => result.summary.truncated;
|
|
38978
39005
|
const visit = (directory) => {
|
|
38979
39006
|
if (steppedOver(directory)) {
|
|
38980
39007
|
return;
|
|
@@ -38992,21 +39019,26 @@ function searchProject(projectPath, options) {
|
|
|
38992
39019
|
continue;
|
|
38993
39020
|
}
|
|
38994
39021
|
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
38995
|
-
if (!entry.isFile() || !extensions.has(extension)) {
|
|
39022
|
+
if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
|
|
39023
|
+
continue;
|
|
39024
|
+
}
|
|
39025
|
+
const bytes = readFileSync17(entryPath);
|
|
39026
|
+
if (extensions === null && isBinary(bytes)) {
|
|
38996
39027
|
continue;
|
|
38997
39028
|
}
|
|
38998
39029
|
result.summary.files_searched += 1;
|
|
38999
39030
|
const matches = [];
|
|
39000
|
-
for (const [index, line] of
|
|
39001
|
-
|
|
39002
|
-
if (
|
|
39003
|
-
|
|
39031
|
+
for (const [index, line] of bytes.toString("utf8").split(/\r?\n/).entries()) {
|
|
39032
|
+
const found = regex ? regex.exec(line) : (options.caseSensitive ? line : line.toLowerCase()).includes(needle) ? [options.query] : null;
|
|
39033
|
+
if (found === null) {
|
|
39034
|
+
continue;
|
|
39004
39035
|
}
|
|
39005
|
-
|
|
39006
|
-
|
|
39007
|
-
|
|
39008
|
-
result.summary.total_matches += 1;
|
|
39036
|
+
if (result.summary.total_matches >= options.maxResults) {
|
|
39037
|
+
result.summary.truncated = true;
|
|
39038
|
+
break;
|
|
39009
39039
|
}
|
|
39040
|
+
matches.push({ line: index + 1, content: line.trim(), match: found[0] ?? "" });
|
|
39041
|
+
result.summary.total_matches += 1;
|
|
39010
39042
|
}
|
|
39011
39043
|
if (matches.length > 0) {
|
|
39012
39044
|
const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
|
|
@@ -39018,10 +39050,40 @@ function searchProject(projectPath, options) {
|
|
|
39018
39050
|
visit(projectPath);
|
|
39019
39051
|
return result;
|
|
39020
39052
|
}
|
|
39021
|
-
var SKIPPED, ASSET_EXTENSIONS, CARRIES_A_UID;
|
|
39053
|
+
var SKIPPED, SCENE_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID;
|
|
39022
39054
|
var init_project_scan = __esm(() => {
|
|
39023
39055
|
SKIPPED = new Set(["node_modules"]);
|
|
39024
|
-
|
|
39056
|
+
SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
|
|
39057
|
+
ASSET_EXTENSIONS = new Set([
|
|
39058
|
+
"png",
|
|
39059
|
+
"jpg",
|
|
39060
|
+
"jpeg",
|
|
39061
|
+
"webp",
|
|
39062
|
+
"svg",
|
|
39063
|
+
"bmp",
|
|
39064
|
+
"tga",
|
|
39065
|
+
"exr",
|
|
39066
|
+
"hdr",
|
|
39067
|
+
"ktx",
|
|
39068
|
+
"dds",
|
|
39069
|
+
"ttf",
|
|
39070
|
+
"otf",
|
|
39071
|
+
"woff",
|
|
39072
|
+
"woff2",
|
|
39073
|
+
"fnt",
|
|
39074
|
+
"wav",
|
|
39075
|
+
"mp3",
|
|
39076
|
+
"ogg",
|
|
39077
|
+
"glb",
|
|
39078
|
+
"gltf",
|
|
39079
|
+
"obj",
|
|
39080
|
+
"fbx",
|
|
39081
|
+
"blend",
|
|
39082
|
+
"dae",
|
|
39083
|
+
"ogv",
|
|
39084
|
+
"gdshader",
|
|
39085
|
+
"shader"
|
|
39086
|
+
]);
|
|
39025
39087
|
CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
39026
39088
|
});
|
|
39027
39089
|
|
|
@@ -39075,7 +39137,7 @@ import {
|
|
|
39075
39137
|
openSync as openSync2,
|
|
39076
39138
|
readdirSync as readdirSync8,
|
|
39077
39139
|
readFileSync as readFileSync19,
|
|
39078
|
-
realpathSync as
|
|
39140
|
+
realpathSync as realpathSync5,
|
|
39079
39141
|
rmSync as rmSync10,
|
|
39080
39142
|
statSync as statSync7,
|
|
39081
39143
|
writeFileSync as writeFileSync13
|
|
@@ -39090,7 +39152,7 @@ function realAsFarAsItExists(path) {
|
|
|
39090
39152
|
let at = path;
|
|
39091
39153
|
for (;; ) {
|
|
39092
39154
|
try {
|
|
39093
|
-
return join22(
|
|
39155
|
+
return join22(realpathSync5.native(at), ...missing);
|
|
39094
39156
|
} catch {
|
|
39095
39157
|
const parent = dirname15(at);
|
|
39096
39158
|
if (parent === at) {
|
|
@@ -39209,7 +39271,9 @@ function recordAt(path) {
|
|
|
39209
39271
|
pid,
|
|
39210
39272
|
transcript,
|
|
39211
39273
|
startedAt,
|
|
39274
|
+
...typeof fields["startedBy"] === "number" ? { startedBy: fields["startedBy"] } : {},
|
|
39212
39275
|
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
39276
|
+
...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
|
|
39213
39277
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
39214
39278
|
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
39215
39279
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
@@ -39218,7 +39282,8 @@ function recordAt(path) {
|
|
|
39218
39282
|
}
|
|
39219
39283
|
async function runningAs(pid) {
|
|
39220
39284
|
if (process.platform === "win32") {
|
|
39221
|
-
const
|
|
39285
|
+
const read = await readWindowsCommandLine(pid);
|
|
39286
|
+
const answer = "answer" in read ? read.answer : null;
|
|
39222
39287
|
if (answer !== null) {
|
|
39223
39288
|
const [, ...rest] = answer.split(`
|
|
39224
39289
|
`);
|
|
@@ -39230,7 +39295,11 @@ async function runningAs(pid) {
|
|
|
39230
39295
|
}
|
|
39231
39296
|
}
|
|
39232
39297
|
const image = await windowsImage(pid);
|
|
39233
|
-
return image === null ? null : {
|
|
39298
|
+
return image === null ? null : {
|
|
39299
|
+
kind: "image",
|
|
39300
|
+
text: image,
|
|
39301
|
+
unread: "unread" in read ? read.unread : "Windows gave an empty command line"
|
|
39302
|
+
};
|
|
39234
39303
|
}
|
|
39235
39304
|
try {
|
|
39236
39305
|
const began = await startedAt(pid, null);
|
|
@@ -39281,13 +39350,18 @@ function listeningPidInNetstat(printed, port) {
|
|
|
39281
39350
|
}
|
|
39282
39351
|
return null;
|
|
39283
39352
|
}
|
|
39284
|
-
async function
|
|
39285
|
-
|
|
39286
|
-
|
|
39287
|
-
|
|
39288
|
-
|
|
39289
|
-
|
|
39353
|
+
async function readWindowsCommandLine(pid, ask = askWindows) {
|
|
39354
|
+
const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`;
|
|
39355
|
+
let unread = "";
|
|
39356
|
+
for (const withinMs of [15000, 45000]) {
|
|
39357
|
+
try {
|
|
39358
|
+
const answer = (await ask(query, withinMs)).trim();
|
|
39359
|
+
return answer === "" ? { unread: "Windows gave no command line for it" } : { answer };
|
|
39360
|
+
} catch (error) {
|
|
39361
|
+
unread = errorMessage(error);
|
|
39362
|
+
}
|
|
39290
39363
|
}
|
|
39364
|
+
return { unread };
|
|
39291
39365
|
}
|
|
39292
39366
|
async function startedAt(pid, windowsAnswer) {
|
|
39293
39367
|
if (process.platform === "win32") {
|
|
@@ -39315,36 +39389,43 @@ async function windowsImage(pid) {
|
|
|
39315
39389
|
return null;
|
|
39316
39390
|
}
|
|
39317
39391
|
}
|
|
39318
|
-
async function
|
|
39319
|
-
return
|
|
39392
|
+
async function whyNotStillTheRecordedRun(record) {
|
|
39393
|
+
return whyNotTheRun(record, await runningAs(record.pid), "confirmed");
|
|
39320
39394
|
}
|
|
39321
39395
|
async function couldStillBeTheRecordedRun(record) {
|
|
39322
39396
|
return judgeRun(record, await runningAs(record.pid), "possible");
|
|
39323
39397
|
}
|
|
39324
39398
|
function judgeRun(record, running, needed) {
|
|
39399
|
+
return whyNotTheRun(record, running, needed) === null;
|
|
39400
|
+
}
|
|
39401
|
+
function whyNotTheRun(record, running, needed) {
|
|
39325
39402
|
if (running === null) {
|
|
39326
|
-
return
|
|
39403
|
+
return "the operating system would not describe that process";
|
|
39327
39404
|
}
|
|
39328
|
-
|
|
39329
|
-
|
|
39405
|
+
const after = running.startedAt === undefined ? 0 : running.startedAt - (record.startedBy ?? record.startedAt);
|
|
39406
|
+
if (after > SAME_RUN_WINDOW_MS) {
|
|
39407
|
+
return `that process started ${Math.round(after / 1000)} seconds after the run had`;
|
|
39330
39408
|
}
|
|
39331
39409
|
const engine = record.command === undefined ? null : basename4(record.command);
|
|
39332
39410
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
39333
39411
|
const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
|
|
39334
39412
|
if (wanted !== null && !said.includes(wanted)) {
|
|
39335
|
-
return
|
|
39413
|
+
return `that process is not ${engine ?? "the engine"}, which the run was started with`;
|
|
39336
39414
|
}
|
|
39337
39415
|
if (running.kind === "commandLine" && readCommandLine(running.text).editor) {
|
|
39338
|
-
return
|
|
39416
|
+
return "that process is an editor";
|
|
39339
39417
|
}
|
|
39340
39418
|
if (record.projectPath === "") {
|
|
39341
|
-
return wanted !== null;
|
|
39419
|
+
return wanted !== null ? null : "the record names neither the project nor the engine";
|
|
39342
39420
|
}
|
|
39343
39421
|
if (running.kind === "image") {
|
|
39344
|
-
|
|
39422
|
+
if (wanted === null) {
|
|
39423
|
+
return "the operating system named only its executable, and the record names no engine";
|
|
39424
|
+
}
|
|
39425
|
+
return needed === "possible" ? null : `the operating system named only its executable and not its command line (${running.unread ?? "no reason given"}), which is not enough to signal it on`;
|
|
39345
39426
|
}
|
|
39346
39427
|
const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
|
|
39347
|
-
return said.includes(project)
|
|
39428
|
+
return said.includes(project) ? null : `that process's command line does not name ${record.projectPath}`;
|
|
39348
39429
|
}
|
|
39349
39430
|
async function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
|
|
39350
39431
|
return judgeAnnouncedGame(await runningAs(pid), projectPath, announcedAt);
|
|
@@ -39405,6 +39486,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
39405
39486
|
}
|
|
39406
39487
|
var execFileAsync, SHARED_NOTE = "run.json", SAME_RUN_WINDOW_MS = 1e4;
|
|
39407
39488
|
var init_run_record = __esm(() => {
|
|
39489
|
+
init_errors();
|
|
39408
39490
|
init_paths();
|
|
39409
39491
|
init_process_children();
|
|
39410
39492
|
init_runtime_client();
|
|
@@ -39434,6 +39516,13 @@ import { basename as basename5, dirname as dirname16, extname as extname2, isAbs
|
|
|
39434
39516
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
39435
39517
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
39436
39518
|
import { promisify as promisify5 } from "node:util";
|
|
39519
|
+
function editorRestartTimeoutMs() {
|
|
39520
|
+
const override = Number.parseInt(envValue("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS") ?? "", 10);
|
|
39521
|
+
return Number.isInteger(override) && override > 0 ? override : 90000;
|
|
39522
|
+
}
|
|
39523
|
+
function restartUnderway(note, now = Date.now()) {
|
|
39524
|
+
return alive(note.byPid) && now - Date.parse(note.quitAt) < editorRestartTimeoutMs() + 30000;
|
|
39525
|
+
}
|
|
39437
39526
|
function captureDestinationRefusal(outputPath, projects) {
|
|
39438
39527
|
if (/^(res|user):\/\//.test(outputPath)) {
|
|
39439
39528
|
return `outputPath ${outputPath} is a Godot path; a capture is saved outside the project, at an absolute path on this machine.`;
|
|
@@ -39584,22 +39673,31 @@ function stopVerdict(stop) {
|
|
|
39584
39673
|
withChildren: false
|
|
39585
39674
|
};
|
|
39586
39675
|
}
|
|
39676
|
+
if (stop.ending === "lingering") {
|
|
39677
|
+
const told = stop.throughEditor ? stop.endedPid === null ? "The editor was asked to stop the scene it is playing and was still playing it" : `The editor was asked to stop the scene it is playing, and its game, pid ${stop.endedPid}, was still running` : `pid ${stop.endedPid ?? "unknown"} was told to end and was still running`;
|
|
39678
|
+
return {
|
|
39679
|
+
stopped: false,
|
|
39680
|
+
stillGoing: stop.endedPid,
|
|
39681
|
+
note: `${told} ${STOP_WAIT_MS / 1000} seconds later, so it has not stopped. editor_status says when it has gone, and editor_run stop asks again.`,
|
|
39682
|
+
withChildren: true
|
|
39683
|
+
};
|
|
39684
|
+
}
|
|
39587
39685
|
return {
|
|
39588
39686
|
stopped: true,
|
|
39589
39687
|
endedPid: stop.endedPid,
|
|
39590
39688
|
clean: stop.clean,
|
|
39591
|
-
note: `${stop.throughEditor ? stop.endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."}${stop.exitSignal === null ? "" : ` It was ended by ${stop.exitSignal} and so has no exit code.`}
|
|
39689
|
+
note: `${stop.throughEditor ? stop.endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."}${stop.exitSignal === null ? "" : ` It was ended by ${stop.exitSignal} and so has no exit code.`}`,
|
|
39592
39690
|
withChildren: true
|
|
39593
39691
|
};
|
|
39594
39692
|
}
|
|
39595
39693
|
function endedPreviousRun(ended) {
|
|
39596
|
-
if (ended
|
|
39694
|
+
if (ended?.ending !== "gone") {
|
|
39597
39695
|
return;
|
|
39598
39696
|
}
|
|
39599
39697
|
return ended.pid ?? true;
|
|
39600
39698
|
}
|
|
39601
39699
|
function previousRunLeft(ended) {
|
|
39602
|
-
return ended?.ending === "refused" && ended.pid !== null ? ended.pid : undefined;
|
|
39700
|
+
return (ended?.ending === "refused" || ended?.ending === "lingering") && ended.pid !== null ? ended.pid : undefined;
|
|
39603
39701
|
}
|
|
39604
39702
|
function engineNamesOf(executable) {
|
|
39605
39703
|
return CONSOLE_WRAPPER.test(executable) ? [executable, executable.replace(CONSOLE_WRAPPER, "$1")] : [executable];
|
|
@@ -39678,8 +39776,10 @@ function endedToStartThis(ended) {
|
|
|
39678
39776
|
if (ended.ending === "unreached") {
|
|
39679
39777
|
return ` ${which} was not ended here: the editor could not be asked to stop it. The editor stops a play before starting the next, so it is not running beside this one if this start reached the editor.`;
|
|
39680
39778
|
}
|
|
39681
|
-
|
|
39682
|
-
|
|
39779
|
+
if (ended.ending === "lingering") {
|
|
39780
|
+
return ` ${which} was told to end to start this one and was still running ${STOP_WAIT_MS / 1000} seconds later, so it is running beside this one: editor_status says when it has gone.`;
|
|
39781
|
+
}
|
|
39782
|
+
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
39683
39783
|
}
|
|
39684
39784
|
function runtimeVerdict(endpoint, after) {
|
|
39685
39785
|
if (endpoint !== null) {
|
|
@@ -40009,6 +40109,7 @@ class GodotServer {
|
|
|
40009
40109
|
shutdownInitiated = false;
|
|
40010
40110
|
launchedEditor = null;
|
|
40011
40111
|
launchedFrom = null;
|
|
40112
|
+
launchedDropped = null;
|
|
40012
40113
|
ownProject;
|
|
40013
40114
|
announcedAt = null;
|
|
40014
40115
|
handingOverFrom = null;
|
|
@@ -40016,9 +40117,12 @@ class GodotServer {
|
|
|
40016
40117
|
constructor() {
|
|
40017
40118
|
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
40018
40119
|
this.godotBridge = getDefaultBridge(this.ownProject);
|
|
40120
|
+
this.godotBridge.on("godot_connected", () => {
|
|
40121
|
+
this.settleTheLaunchReading();
|
|
40122
|
+
});
|
|
40019
40123
|
this.mcp = new McpServer({ name: "gdharness", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
40020
40124
|
this.setupToolHandlers();
|
|
40021
|
-
setupResourceHandlers(this.mcp, () => this.lastProjectPath);
|
|
40125
|
+
setupResourceHandlers(this.mcp, () => this.ourProject() ?? this.lastProjectPath);
|
|
40022
40126
|
this.mcp.server.onerror = (error) => {
|
|
40023
40127
|
console.error("[MCP Error]", error);
|
|
40024
40128
|
};
|
|
@@ -40284,8 +40388,9 @@ class GodotServer {
|
|
|
40284
40388
|
if (!checked.ok) {
|
|
40285
40389
|
return checked.response;
|
|
40286
40390
|
}
|
|
40287
|
-
|
|
40288
|
-
|
|
40391
|
+
const named = args["projectPath"];
|
|
40392
|
+
if (typeof named === "string" && existsSync17(join23(named, "project.godot"))) {
|
|
40393
|
+
this.lastProjectPath = named;
|
|
40289
40394
|
}
|
|
40290
40395
|
this.updates.refresh();
|
|
40291
40396
|
const answer = await withCallSignal(extra.signal, () => this.answered(spec.name, checked.op ?? "", args));
|
|
@@ -40325,6 +40430,17 @@ class GodotServer {
|
|
|
40325
40430
|
return answer;
|
|
40326
40431
|
}
|
|
40327
40432
|
const moved = this.projectHasMovedOn();
|
|
40433
|
+
if (moved !== null && !isNewer(moved, SERVER_VERSION)) {
|
|
40434
|
+
this.noticedUpdate = true;
|
|
40435
|
+
this.callsSinceNotice = 0;
|
|
40436
|
+
return this.saying(answer, {
|
|
40437
|
+
project_addons_behind_this_server: {
|
|
40438
|
+
server_is: SERVER_VERSION,
|
|
40439
|
+
project_is: moved,
|
|
40440
|
+
what_to_do: `The project's gdharness addons are ${moved}, older than this ${SERVER_VERSION} server, so an ` + "editor restart loads the old code and reconnecting spawns this same server again. Tell the " + "user, and offer to run gdharness upgrade in the project, which replaces the addons with " + "this server's; ask first, since it changes files in their project. editor_launch restart " + "then loads them."
|
|
40441
|
+
}
|
|
40442
|
+
});
|
|
40443
|
+
}
|
|
40328
40444
|
if (moved !== null) {
|
|
40329
40445
|
this.noticedUpdate = true;
|
|
40330
40446
|
this.callsSinceNotice = 0;
|
|
@@ -40357,7 +40473,7 @@ class GodotServer {
|
|
|
40357
40473
|
return this.saying(answer, {
|
|
40358
40474
|
update_available: {
|
|
40359
40475
|
...notice,
|
|
40360
|
-
what_to_do: "Tell the user a newer gdharness is out, with what changed, and offer to take it. " + "Only run the upgrade command if they say yes: it
|
|
40476
|
+
what_to_do: "Tell the user a newer gdharness is out, with what changed, and offer to take it. " + "Only run the upgrade command if they say yes: it replaces the addons in their project, " + "and afterwards the MCP server has to be reconnected and the editor restarted with " + "editor_launch restart."
|
|
40361
40477
|
}
|
|
40362
40478
|
});
|
|
40363
40479
|
}
|
|
@@ -40374,6 +40490,11 @@ class GodotServer {
|
|
|
40374
40490
|
const installed = installedAddonVersion(this.ownProject);
|
|
40375
40491
|
return installed === null || installed === SERVER_VERSION ? null : installed;
|
|
40376
40492
|
}
|
|
40493
|
+
editorCodeOnDisk() {
|
|
40494
|
+
const status = this.godotBridge.getStatus();
|
|
40495
|
+
const project = (status.connected ? status.projectPath : undefined) ?? this.ownProject;
|
|
40496
|
+
return project === null || project === "" ? undefined : installedEditorDigest(project);
|
|
40497
|
+
}
|
|
40377
40498
|
configDisagreement() {
|
|
40378
40499
|
return this.ownProject === null ? null : configDisagrees(this.ownProject, SERVER_VERSION);
|
|
40379
40500
|
}
|
|
@@ -41325,6 +41446,10 @@ class GodotServer {
|
|
|
41325
41446
|
}
|
|
41326
41447
|
return this.jsonTextResponse(info);
|
|
41327
41448
|
}
|
|
41449
|
+
fileTypes(args) {
|
|
41450
|
+
const named = readStringArray(args, "fileTypes");
|
|
41451
|
+
return named === undefined ? {} : { fileTypes: named };
|
|
41452
|
+
}
|
|
41328
41453
|
handleSearchProject(args) {
|
|
41329
41454
|
const project = this.project(args);
|
|
41330
41455
|
if (!project.ok) {
|
|
@@ -41333,16 +41458,7 @@ class GodotServer {
|
|
|
41333
41458
|
try {
|
|
41334
41459
|
return this.jsonTextResponse(searchProject(project.value.path, {
|
|
41335
41460
|
query: readString(args, "query") ?? "",
|
|
41336
|
-
fileTypes
|
|
41337
|
-
"gd",
|
|
41338
|
-
"tscn",
|
|
41339
|
-
"tres",
|
|
41340
|
-
"gdshader",
|
|
41341
|
-
"cfg",
|
|
41342
|
-
"md",
|
|
41343
|
-
"txt",
|
|
41344
|
-
"json"
|
|
41345
|
-
],
|
|
41461
|
+
...this.fileTypes(args),
|
|
41346
41462
|
regex: readBoolean(args, "regex") ?? false,
|
|
41347
41463
|
caseSensitive: readBoolean(args, "caseSensitive") ?? false,
|
|
41348
41464
|
maxResults: readPositiveNumber(args, "maxResults") ?? 100
|
|
@@ -41653,7 +41769,7 @@ class GodotServer {
|
|
|
41653
41769
|
warnings: diagnostics.length - errors,
|
|
41654
41770
|
diagnostics,
|
|
41655
41771
|
...disagrees
|
|
41656
|
-
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
|
|
41772
|
+
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
|
|
41657
41773
|
}
|
|
41658
41774
|
whereTheFileDisagrees(diagnostics, args) {
|
|
41659
41775
|
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
|
@@ -41804,7 +41920,7 @@ class GodotServer {
|
|
|
41804
41920
|
}
|
|
41805
41921
|
breakpointsAtRisk() {
|
|
41806
41922
|
const status = this.godotBridge.getStatus();
|
|
41807
|
-
if (!status
|
|
41923
|
+
if (!hasSaidWhoItIs(status) || status.syncsBreakpoints === true) {
|
|
41808
41924
|
return {};
|
|
41809
41925
|
}
|
|
41810
41926
|
return {
|
|
@@ -41859,26 +41975,34 @@ class GodotServer {
|
|
|
41859
41975
|
restartSettled(this.ownProject);
|
|
41860
41976
|
return null;
|
|
41861
41977
|
}
|
|
41862
|
-
return owed;
|
|
41978
|
+
return { note: owed, underway: restartUnderway(owed) };
|
|
41863
41979
|
}
|
|
41864
41980
|
async getEditorStatusPayload() {
|
|
41865
41981
|
const status = this.godotBridge.getStatus();
|
|
41866
41982
|
const launchedIsUp = !status.connected && await this.launchedEditorIsUp();
|
|
41867
41983
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
41868
|
-
const
|
|
41984
|
+
const greeted = hasSaidWhoItIs(status);
|
|
41985
|
+
const stale = greeted && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
41869
41986
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
41870
41987
|
return {
|
|
41871
41988
|
...status,
|
|
41872
41989
|
serverVersion: SERVER_VERSION,
|
|
41873
|
-
addonIsStale:
|
|
41990
|
+
addonIsStale: greeted ? stale : undefined,
|
|
41991
|
+
greeting: status.connected && !greeted ? "The editor has connected and has not yet said who it is: its project, pid, addon version and ports arrive with its greeting a moment after the socket opens, later while it imports. Ask again for them." : undefined,
|
|
41874
41992
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
41875
|
-
mayYetConnect: status.connected ? undefined : unfinished !== null ?
|
|
41876
|
-
restartInterrupted: unfinished === null ? undefined : {
|
|
41877
|
-
quitEditorPid: unfinished.editorPid,
|
|
41878
|
-
quitAt: unfinished.quitAt,
|
|
41879
|
-
byServerPid: unfinished.byPid,
|
|
41993
|
+
mayYetConnect: status.connected ? undefined : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
41994
|
+
restartInterrupted: unfinished === null || unfinished.underway ? undefined : {
|
|
41995
|
+
quitEditorPid: unfinished.note.editorPid,
|
|
41996
|
+
quitAt: unfinished.note.quitAt,
|
|
41997
|
+
byServerPid: unfinished.note.byPid,
|
|
41880
41998
|
note: "A restart begun by a previous gdharness server asked this project's editor to quit, and that server was ended before it could start the editor again, so nothing is coming. editor_launch open starts one, and clears this."
|
|
41881
41999
|
},
|
|
42000
|
+
restartUnderway: unfinished === null || !unfinished.underway ? undefined : {
|
|
42001
|
+
quitEditorPid: unfinished.note.editorPid,
|
|
42002
|
+
quitAt: unfinished.note.quitAt,
|
|
42003
|
+
byServerPid: unfinished.note.byPid,
|
|
42004
|
+
note: `A restart of this project's editor is under way: gdharness server pid ${unfinished.note.byPid} asked it to quit at ${unfinished.note.quitAt} and is starting it again. Wait for it rather than opening another.`
|
|
42005
|
+
},
|
|
41882
42006
|
awaitingLaunchedEditor: status.connected || !launchedIsUp ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
41883
42007
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
41884
42008
|
startupError: this.bridgeStartupError,
|
|
@@ -41888,8 +42012,9 @@ class GodotServer {
|
|
|
41888
42012
|
port: this.godotBridge.configuredPort,
|
|
41889
42013
|
note: `The gdharness server this one replaced, pid ${this.handingOverFrom}, still holds port ${this.godotBridge.configuredPort} and is standing down; this server takes the port when it lets go, and takes another after ${HANDOVER_MS / 1000} seconds if it does not.`
|
|
41890
42014
|
},
|
|
41891
|
-
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
|
|
41892
|
-
addonNote:
|
|
42015
|
+
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()) : undefined,
|
|
42016
|
+
addonNote: greeted ? sameCodeNote(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
|
|
42017
|
+
startedByAnEditorNote: status.startedByAnEditor === true ? "Another editor started this one: Godot's own restart, or the project manager opening a project. None of the arguments a server gave the editor before it reached this one, so it serves the ports its editor settings name (and none at all while another editor holds those, when the script and debug tools reach that editor instead), its console is not captured, and it restarts itself. Close it and use editor_launch open to have an editor this server started." : undefined,
|
|
41893
42018
|
...this.breakpointsAtRisk(),
|
|
41894
42019
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
41895
42020
|
announcedAt: this.announcedAt ?? undefined,
|
|
@@ -42028,17 +42153,33 @@ class GodotServer {
|
|
|
42028
42153
|
"editor_status says whether the bridge is up and what has reached it"
|
|
42029
42154
|
]);
|
|
42030
42155
|
}
|
|
42031
|
-
|
|
42156
|
+
if (!hasSaidWhoItIs(before)) {
|
|
42157
|
+
const settled = await this.waitForBridge(() => hasSaidWhoItIs(this.godotBridge.getStatus()) || !this.godotBridge.isConnected(), Date.now() + GREETING_WAIT_MS);
|
|
42158
|
+
if (!settled) {
|
|
42159
|
+
return this.createErrorResponse(`The editor has connected and has not said who it is within ${GREETING_WAIT_MS / 1000}s, so whether this server or the editor itself restarts it cannot be told.`, [
|
|
42160
|
+
"editor_status says when it has, under greeting while it has not",
|
|
42161
|
+
"Then ask for the restart again"
|
|
42162
|
+
]);
|
|
42163
|
+
}
|
|
42164
|
+
return this.handleRestartEditor(args);
|
|
42165
|
+
}
|
|
42032
42166
|
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
42167
|
+
if (!mine && args["hidden"] === true) {
|
|
42168
|
+
return this.createErrorResponse("This editor was opened by hand, and restarts itself as it was, so it cannot come back hidden.", [
|
|
42169
|
+
"Close it, then editor_launch open with hidden: true starts a hidden one this server can restart",
|
|
42170
|
+
"editor_launch restart without hidden restarts it as it is"
|
|
42171
|
+
]);
|
|
42172
|
+
}
|
|
42173
|
+
const settingsBefore = this.settingKeysOf(before.projectPath);
|
|
42033
42174
|
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, args["hidden"] === true) : await this.handleViaBridge("restart_editor", {});
|
|
42034
42175
|
if (asked.isError === true) {
|
|
42035
42176
|
return asked;
|
|
42036
42177
|
}
|
|
42037
42178
|
const startedAt = before.connectedAt?.getTime() ?? 0;
|
|
42038
42179
|
const began = Date.now();
|
|
42039
|
-
const back = await this.waitForBridge(() => theEditorHasComeBack(this.godotBridge.getStatus(), startedAt), began +
|
|
42180
|
+
const back = await this.waitForBridge(() => theEditorHasComeBack(this.godotBridge.getStatus(), startedAt), began + editorRestartTimeoutMs());
|
|
42040
42181
|
if (!back) {
|
|
42041
|
-
return this.createErrorResponse(`The editor was asked to restart and has not come back within ${
|
|
42182
|
+
return this.createErrorResponse(`The editor was asked to restart and has not come back within ${editorRestartTimeoutMs() / 1000}s.`, [
|
|
42042
42183
|
"It may be asking what to do about an unsaved scene: look at the editor window",
|
|
42043
42184
|
"An addon that no longer parses stops the editor reaching this server",
|
|
42044
42185
|
"editor_status says whether anything has reached the bridge since"
|
|
@@ -42052,18 +42193,27 @@ class GodotServer {
|
|
|
42052
42193
|
addonVersion: now.addonVersion,
|
|
42053
42194
|
serverVersion: SERVER_VERSION,
|
|
42054
42195
|
addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
|
|
42055
|
-
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
|
|
42196
|
+
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()),
|
|
42056
42197
|
...this.whatTheEditorDropped(settingsBefore, after),
|
|
42057
42198
|
tookMs: Date.now() - began
|
|
42058
42199
|
});
|
|
42059
42200
|
}
|
|
42060
|
-
|
|
42201
|
+
settleTheLaunchReading() {
|
|
42061
42202
|
const watched = this.launchedFrom;
|
|
42062
|
-
|
|
42063
|
-
|
|
42203
|
+
const status = this.godotBridge.getStatus();
|
|
42204
|
+
if (watched === null || !hasSaidWhoItIs(status)) {
|
|
42205
|
+
return;
|
|
42064
42206
|
}
|
|
42065
42207
|
this.launchedFrom = null;
|
|
42066
|
-
|
|
42208
|
+
if (status.editorPid === undefined || status.editorPid !== this.launchedEditor?.pid) {
|
|
42209
|
+
return;
|
|
42210
|
+
}
|
|
42211
|
+
this.launchedDropped = this.whatTheEditorDropped(watched.before, this.settingKeysOf(watched.projectPath));
|
|
42212
|
+
}
|
|
42213
|
+
whatTheLaunchedEditorDropped() {
|
|
42214
|
+
const dropped = this.launchedDropped ?? {};
|
|
42215
|
+
this.launchedDropped = null;
|
|
42216
|
+
return dropped;
|
|
42067
42217
|
}
|
|
42068
42218
|
whatTheEditorDropped(before, after) {
|
|
42069
42219
|
return settingsDroppedReport(before, after);
|
|
@@ -42089,7 +42239,14 @@ class GodotServer {
|
|
|
42089
42239
|
restartSettled(projectPath);
|
|
42090
42240
|
return asked;
|
|
42091
42241
|
}
|
|
42092
|
-
await this.waitForBridge(() => !alive(editorPid), Date.now() +
|
|
42242
|
+
const gone = await this.waitForBridge(() => !alive(editorPid), Date.now() + editorRestartTimeoutMs());
|
|
42243
|
+
if (!gone) {
|
|
42244
|
+
restartSettled(projectPath);
|
|
42245
|
+
return this.createErrorResponse(`The editor was asked to go and pid ${editorPid ?? "unknown"} is still running after ${editorRestartTimeoutMs() / 1000}s, so no replacement was started: it would find the language server and debug adapter ports still held.`, [
|
|
42246
|
+
"It may be asking what to do about unsaved changes, or a script may be holding up its exit",
|
|
42247
|
+
`editor_launch open starts one on ${projectPath} once it has gone`
|
|
42248
|
+
]);
|
|
42249
|
+
}
|
|
42093
42250
|
const opened = await this.openAnEditor(engine.value, projectPath, ports, hidden);
|
|
42094
42251
|
restartSettled(projectPath);
|
|
42095
42252
|
if (opened.error !== null) {
|
|
@@ -42120,6 +42277,10 @@ class GodotServer {
|
|
|
42120
42277
|
"Another project wants its own gdharness server, which is its own harness session"
|
|
42121
42278
|
]);
|
|
42122
42279
|
}
|
|
42280
|
+
const refused = this.anEditorThatCannotConnect(project.value.path);
|
|
42281
|
+
if (refused !== null) {
|
|
42282
|
+
return refused;
|
|
42283
|
+
}
|
|
42123
42284
|
const engine = await this.engine();
|
|
42124
42285
|
if (!engine.ok) {
|
|
42125
42286
|
return engine.response;
|
|
@@ -42144,6 +42305,27 @@ class GodotServer {
|
|
|
42144
42305
|
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
|
|
42145
42306
|
});
|
|
42146
42307
|
}
|
|
42308
|
+
anEditorThatCannotConnect(projectPath) {
|
|
42309
|
+
if (this.ownProject !== null && !isSameDirectory(this.ownProject, projectPath)) {
|
|
42310
|
+
return this.createErrorResponse(`This server serves ${this.ownProject}, and an editor of ${projectPath} would dial that project's own server rather than this one.`, [
|
|
42311
|
+
"Open it from that project's own gdharness session",
|
|
42312
|
+
`editor_launch with projectPath ${this.ownProject} opens the editor this server serves`
|
|
42313
|
+
]);
|
|
42314
|
+
}
|
|
42315
|
+
const report = inspectProject(projectPath);
|
|
42316
|
+
const installed = report.addons.some((addon) => addon.name === "gdharness_editor" && addon.installed);
|
|
42317
|
+
if (!installed || !report.pluginsEnabled.includes("gdharness_editor")) {
|
|
42318
|
+
return this.createErrorResponse(`The gdharness editor addon is not ${installed ? "enabled" : "installed"} in ${projectPath}, so an editor opened on it would never reach this server.`, installed ? [
|
|
42319
|
+
"gdharness setup in the project enables it",
|
|
42320
|
+
"Or enable it under Project Settings, Plugins, in an editor opened by hand"
|
|
42321
|
+
] : ["gdharness setup in the project installs and enables it"]);
|
|
42322
|
+
}
|
|
42323
|
+
const owed = restartOwed(projectPath);
|
|
42324
|
+
if (owed !== null && restartUnderway(owed)) {
|
|
42325
|
+
return this.createErrorResponse(`A restart of this project's editor is under way: gdharness server pid ${owed.byPid} asked it to quit at ${owed.quitAt} and is starting it again, so opening another would put two editors on one project.`, ["editor_status says when it has connected"]);
|
|
42326
|
+
}
|
|
42327
|
+
return null;
|
|
42328
|
+
}
|
|
42147
42329
|
async openAnEditor(engine, projectPath, ports, hidden) {
|
|
42148
42330
|
clearEditorLog(projectPath);
|
|
42149
42331
|
const launched = await launchOutsideTheTree({
|
|
@@ -42591,23 +42773,30 @@ class GodotServer {
|
|
|
42591
42773
|
pid: running.pid,
|
|
42592
42774
|
transcript: running.transcript ?? "",
|
|
42593
42775
|
startedAt: running.startedAt,
|
|
42776
|
+
...running.startedBy === undefined ? {} : { startedBy: running.startedBy },
|
|
42594
42777
|
projectPath: running.projectPath ?? "",
|
|
42595
42778
|
arguments: [],
|
|
42596
42779
|
...running.command === undefined ? {} : { command: running.command }
|
|
42597
42780
|
};
|
|
42598
|
-
|
|
42781
|
+
const why = await whyNotStillTheRecordedRun(startedAs);
|
|
42782
|
+
if (why !== null) {
|
|
42599
42783
|
clearTheRecord();
|
|
42600
42784
|
running.endedHere = null;
|
|
42601
|
-
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the process the run was started as, so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
|
|
42785
|
+
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the process the run was started as (${why}), so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
|
|
42602
42786
|
return "refused";
|
|
42603
42787
|
}
|
|
42604
42788
|
const signalled = await runningAs(running.pid);
|
|
42605
42789
|
const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
|
|
42606
42790
|
let landed = true;
|
|
42791
|
+
let declined = null;
|
|
42607
42792
|
try {
|
|
42608
42793
|
process.kill(running.pid);
|
|
42609
|
-
} catch {
|
|
42794
|
+
} catch (error) {
|
|
42610
42795
|
landed = false;
|
|
42796
|
+
const code = error.code;
|
|
42797
|
+
if (code !== "ESRCH") {
|
|
42798
|
+
declined = code ?? errorMessage(error);
|
|
42799
|
+
}
|
|
42611
42800
|
}
|
|
42612
42801
|
const gone = await untilGone([running.pid]);
|
|
42613
42802
|
if (gone) {
|
|
@@ -42618,7 +42807,7 @@ class GodotServer {
|
|
|
42618
42807
|
running.exitSignal = "SIGTERM";
|
|
42619
42808
|
}
|
|
42620
42809
|
clearTheRecord();
|
|
42621
|
-
running.log.record("info", landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
|
|
42810
|
+
running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${STOP_WAIT_MS / 1000} seconds later.` : landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
|
|
42622
42811
|
return gone ? "gone" : "lingering";
|
|
42623
42812
|
}
|
|
42624
42813
|
spawnGame(godotPath, cmdArgs, env) {
|
|
@@ -42702,12 +42891,13 @@ class GodotServer {
|
|
|
42702
42891
|
command: godotPath,
|
|
42703
42892
|
args: cmdArgs,
|
|
42704
42893
|
...env === undefined ? {} : { env },
|
|
42705
|
-
run: { transcript: transcript.path, startedAt, projectPath },
|
|
42894
|
+
run: { transcript: transcript.path, startedAt, projectPath, ...this.servedBy() },
|
|
42706
42895
|
...desktop === undefined ? {} : { desktop }
|
|
42707
42896
|
});
|
|
42708
42897
|
if ("error" in launched) {
|
|
42709
42898
|
return launched;
|
|
42710
42899
|
}
|
|
42900
|
+
const startedBy = Date.now();
|
|
42711
42901
|
sweepTranscripts();
|
|
42712
42902
|
return {
|
|
42713
42903
|
pid: launched.pid,
|
|
@@ -42716,6 +42906,7 @@ class GodotServer {
|
|
|
42716
42906
|
readOffset: 0,
|
|
42717
42907
|
projectPath,
|
|
42718
42908
|
startedAt,
|
|
42909
|
+
startedBy,
|
|
42719
42910
|
command: godotPath,
|
|
42720
42911
|
exitCode: null,
|
|
42721
42912
|
exitSignal: null,
|
|
@@ -42935,8 +43126,9 @@ class GodotServer {
|
|
|
42935
43126
|
}
|
|
42936
43127
|
async adoptRecordedRun() {
|
|
42937
43128
|
const project = this.ourProject();
|
|
42938
|
-
const
|
|
42939
|
-
|
|
43129
|
+
const found = project === null ? [] : [readRunRecord(project), this.startedForUs(project)];
|
|
43130
|
+
const record = found.reduce((newest, one) => one !== null && (newest === null || one.startedAt > newest.startedAt) ? one : newest, null);
|
|
43131
|
+
if (record === null || !(this.couldBeOurs(record.projectPath) || this.couldBeOurs(record.servedBy ?? ""))) {
|
|
42940
43132
|
return null;
|
|
42941
43133
|
}
|
|
42942
43134
|
const adopted = {
|
|
@@ -42946,6 +43138,7 @@ class GodotServer {
|
|
|
42946
43138
|
readOffset: 0,
|
|
42947
43139
|
projectPath: record.projectPath === "" ? null : record.projectPath,
|
|
42948
43140
|
startedAt: record.startedAt,
|
|
43141
|
+
...record.startedBy === undefined ? {} : { startedBy: record.startedBy },
|
|
42949
43142
|
...record.command === undefined ? {} : { command: record.command },
|
|
42950
43143
|
exitCode: null,
|
|
42951
43144
|
exitSignal: null,
|
|
@@ -42966,6 +43159,13 @@ class GodotServer {
|
|
|
42966
43159
|
this.activeProcess = adopted;
|
|
42967
43160
|
return adopted;
|
|
42968
43161
|
}
|
|
43162
|
+
servedBy() {
|
|
43163
|
+
const mine = this.ourProject();
|
|
43164
|
+
return mine === null ? {} : { servedBy: mine };
|
|
43165
|
+
}
|
|
43166
|
+
startedForUs(project) {
|
|
43167
|
+
return everyRunRecord().find((one) => one.servedBy !== undefined && isSameDirectory(one.servedBy, project) && !(one.projectPath !== "" && isSameDirectory(one.projectPath, project))) ?? null;
|
|
43168
|
+
}
|
|
42969
43169
|
nothingOfOursIsRunning() {
|
|
42970
43170
|
const status = this.godotBridge.getStatus();
|
|
42971
43171
|
const noEditor = !status.connected ? " No editor is connected, so a game the editor is playing cannot be seen from here at all:" + " editor_status says whether one is on its way, and a run of yours that is still up is" + " reachable again once it is." : "";
|
|
@@ -43132,22 +43332,20 @@ class GodotServer {
|
|
|
43132
43332
|
}
|
|
43133
43333
|
async handleEditorConsole(args) {
|
|
43134
43334
|
const status = this.godotBridge.getStatus();
|
|
43135
|
-
const
|
|
43136
|
-
|
|
43137
|
-
|
|
43138
|
-
|
|
43139
|
-
"editor_launch opens one, and an editor opened here has its console captured"
|
|
43140
|
-
]);
|
|
43335
|
+
const greeted = hasSaidWhoItIs(status);
|
|
43336
|
+
const launched = greeted ? null : this.launchedEditor?.pid ?? null;
|
|
43337
|
+
if (!greeted && launched === null && status.connected) {
|
|
43338
|
+
return this.createErrorResponse("The editor has connected and has not yet said who it is, so whose console to read cannot be told yet.", ["Ask again in a moment; editor_status shows greeting until it has"]);
|
|
43141
43339
|
}
|
|
43142
|
-
const
|
|
43143
|
-
if (!
|
|
43340
|
+
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
43341
|
+
if (projectPath === undefined || !greeted && launched === null) {
|
|
43144
43342
|
return this.createErrorResponse("No editor is connected, so there is no console to read.", [
|
|
43145
43343
|
"editor_status says whether one is connected and whether it may yet be",
|
|
43146
43344
|
"editor_launch opens one, and an editor opened here has its console captured"
|
|
43147
43345
|
]);
|
|
43148
43346
|
}
|
|
43149
|
-
const editorPid =
|
|
43150
|
-
const console2 = editorConsole(projectPath, editorPid,
|
|
43347
|
+
const editorPid = greeted ? status.editorPid ?? null : launched;
|
|
43348
|
+
const console2 = editorConsole(projectPath, editorPid, greeted ? status.openedByAServer === true : true);
|
|
43151
43349
|
if ("kind" in console2) {
|
|
43152
43350
|
return this.createErrorResponse(theConsoleWasNotCaptured(console2, projectPath), [
|
|
43153
43351
|
"editor_launch restart replaces this editor with one whose console is captured",
|
|
@@ -43329,6 +43527,7 @@ class GodotServer {
|
|
|
43329
43527
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
43330
43528
|
endedPid: verdict.endedPid,
|
|
43331
43529
|
notSignalled: verdict.notSignalled,
|
|
43530
|
+
stillGoing: verdict.stillGoing,
|
|
43332
43531
|
...childrenFields(ended),
|
|
43333
43532
|
exitedBeforeStop: !wasRunning,
|
|
43334
43533
|
exitCode: stopped.exitCode,
|
|
@@ -43386,12 +43585,13 @@ class GodotServer {
|
|
|
43386
43585
|
await announcementEnded(pid);
|
|
43387
43586
|
}
|
|
43388
43587
|
return this.jsonTextResponse({
|
|
43389
|
-
stopped:
|
|
43588
|
+
stopped: gone,
|
|
43390
43589
|
through: "gdharness",
|
|
43391
|
-
endedPid: pid,
|
|
43590
|
+
endedPid: gone ? pid : undefined,
|
|
43591
|
+
stillGoing: gone ? undefined : pid,
|
|
43392
43592
|
...childrenFields(ended),
|
|
43393
43593
|
exitedBeforeStop: !landed,
|
|
43394
|
-
note: `${landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" :
|
|
43594
|
+
note: `${!gone ? `pid ${pid} was told to end and was still running ${STOP_WAIT_MS / 1000} seconds later, so it has not stopped: a game of this project that no server here was holding,` : landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" : " editor_status says when it has gone."} ${aboutTheChildren(ended, false)}`
|
|
43395
43595
|
});
|
|
43396
43596
|
}
|
|
43397
43597
|
async whatTheRunStarted(run) {
|
|
@@ -43538,7 +43738,8 @@ class GodotServer {
|
|
|
43538
43738
|
}
|
|
43539
43739
|
}
|
|
43540
43740
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
43541
|
-
const
|
|
43741
|
+
const declaredNow = before === null || after === null ? null : declaredClasses(projectPath);
|
|
43742
|
+
const lost = before === null || after === null || declaredNow === null ? [] : [...before.keys()].filter((name) => !after.has(name) && declaredNow.has(name));
|
|
43542
43743
|
if (lost.length > 0) {
|
|
43543
43744
|
this.noteAShortListEditor(projectPath);
|
|
43544
43745
|
}
|
|
@@ -43599,7 +43800,7 @@ class GodotServer {
|
|
|
43599
43800
|
}
|
|
43600
43801
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
43601
43802
|
const unseen = checked.unseen;
|
|
43602
|
-
const broughtIn = blind.unseen.map((one) => one.className).filter((name) => !unseen.some((still) => still.className === name));
|
|
43803
|
+
const broughtIn = busy ? [] : blind.unseen.map((one) => one.className).filter((name) => !unseen.some((still) => still.className === name));
|
|
43603
43804
|
const dependents = busy || projectPath === "" || broughtIn.length === 0 ? null : await this.reloadDependents(projectPath, broughtIn);
|
|
43604
43805
|
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
43605
43806
|
const notes = [];
|
|
@@ -43840,7 +44041,7 @@ class GodotServer {
|
|
|
43840
44041
|
]);
|
|
43841
44042
|
}
|
|
43842
44043
|
try {
|
|
43843
|
-
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
|
|
44044
|
+
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
|
|
43844
44045
|
} catch (error) {
|
|
43845
44046
|
return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
|
|
43846
44047
|
}
|
|
@@ -44118,10 +44319,11 @@ class GodotServer {
|
|
|
44118
44319
|
}
|
|
44119
44320
|
}
|
|
44120
44321
|
async function runGodotServer() {
|
|
44322
|
+
delete process.env[EDITOR_PID_VARIABLE];
|
|
44121
44323
|
const server = new GodotServer;
|
|
44122
44324
|
await server.run();
|
|
44123
44325
|
}
|
|
44124
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2,
|
|
44326
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
44125
44327
|
var init_server3 = __esm(() => {
|
|
44126
44328
|
init_mcp();
|
|
44127
44329
|
init_stdio2();
|
|
@@ -44923,27 +45125,37 @@ function registered(harness, projectPath) {
|
|
|
44923
45125
|
}
|
|
44924
45126
|
function recordedEnginePath(projectPath) {
|
|
44925
45127
|
for (const harness of HARNESSES) {
|
|
44926
|
-
if (harness.scope !== "project"
|
|
44927
|
-
continue;
|
|
44928
|
-
}
|
|
44929
|
-
const path = configPath(harness, projectPath);
|
|
44930
|
-
if (!existsSync4(path)) {
|
|
45128
|
+
if (harness.scope !== "project") {
|
|
44931
45129
|
continue;
|
|
44932
45130
|
}
|
|
44933
|
-
|
|
44934
|
-
try {
|
|
44935
|
-
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
44936
|
-
} catch {
|
|
44937
|
-
continue;
|
|
44938
|
-
}
|
|
44939
|
-
const entry = reach(reach(parsed, harness.container), SERVER_KEY);
|
|
44940
|
-
const named = reach(reach(entry, harness.shape === "opencode" ? "environment" : "env"), "GODOT_PATH");
|
|
45131
|
+
const named = reach(entryEnvironment(harness, projectPath), "GODOT_PATH");
|
|
44941
45132
|
if (typeof named === "string" && named !== "" && existsSync4(named)) {
|
|
44942
45133
|
return named;
|
|
44943
45134
|
}
|
|
44944
45135
|
}
|
|
44945
45136
|
return null;
|
|
44946
45137
|
}
|
|
45138
|
+
function entryEnvironment(harness, projectPath) {
|
|
45139
|
+
if (harness.toml || harness.yaml || harness.snippet) {
|
|
45140
|
+
return;
|
|
45141
|
+
}
|
|
45142
|
+
const path = configPath(harness, projectPath);
|
|
45143
|
+
if (!existsSync4(path)) {
|
|
45144
|
+
return;
|
|
45145
|
+
}
|
|
45146
|
+
let parsed;
|
|
45147
|
+
try {
|
|
45148
|
+
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
45149
|
+
} catch {
|
|
45150
|
+
return;
|
|
45151
|
+
}
|
|
45152
|
+
const entry = reach(reach(parsed, harness.container), SERVER_KEY);
|
|
45153
|
+
return reach(entry, harness.shape === "opencode" ? "environment" : "env");
|
|
45154
|
+
}
|
|
45155
|
+
function servedProject(harness, projectPath) {
|
|
45156
|
+
const named = reach(entryEnvironment(harness, projectPath), "GDHARNESS_PROJECT");
|
|
45157
|
+
return typeof named === "string" && named !== "" ? named : null;
|
|
45158
|
+
}
|
|
44947
45159
|
function reach(held, key) {
|
|
44948
45160
|
return typeof held === "object" && held !== null && !Array.isArray(held) ? held[key] : undefined;
|
|
44949
45161
|
}
|
|
@@ -45081,6 +45293,7 @@ function taken(harness, path, held, stripped) {
|
|
|
45081
45293
|
// src/cli.ts
|
|
45082
45294
|
init_headless();
|
|
45083
45295
|
init_issues();
|
|
45296
|
+
init_paths();
|
|
45084
45297
|
|
|
45085
45298
|
// src/prompt.ts
|
|
45086
45299
|
import process8 from "node:process";
|
|
@@ -45279,7 +45492,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
45279
45492
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
45280
45493
|
| What a property reads right now, on a node whose path you already hold | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
45281
45494
|
| What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
|
|
45282
|
-
| Which control holds the focus, or anything only a method answers | the same path with a step written as a call, \`get_viewport():gui_get_focus_owner\`, which calls a method taking no arguments and walks into what it returned |
|
|
45495
|
+
| Which control holds the focus, or anything only a method answers | the same path with a step written as a call, \`get_viewport():gui_get_focus_owner\`, which calls a method taking no arguments that only reads and walks into what it returned |
|
|
45283
45496
|
| Press a button | \`runtime_input click\` with \`says\` and the words on it, which finds it and presses it in the same frame and says what was under the pointer. A \`find\` and then a click by the path it answered is two calls over a generated path, and a panel that rebuilds between them frees the button first; \`nodePath\` narrows where \`says\` looks, and several matches are refused with an \`index\` for each |
|
|
45284
45497
|
| Press the next thing after a click that changed screen | \`runtime_wait until\` with \`says\` and a word the new screen shows (no \`nodePath\` needed: without one the whole game is searched), then the click. A click sent straight after a screen change lands on a control the new screen is about to free, answers \`landed: true\`, and does nothing; a frame count is a number nobody can pick correctly across a cold start and a warm one, and waiting for the words answered in about 130ms where twenty frames had not been enough |
|
|
45285
45498
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
@@ -45680,12 +45893,14 @@ async function uninstall() {
|
|
|
45680
45893
|
const named = namedHarnesses();
|
|
45681
45894
|
const from = named.length > 0 ? named : HARNESSES;
|
|
45682
45895
|
for (const harness of from) {
|
|
45683
|
-
|
|
45684
|
-
|
|
45896
|
+
if (harness.scope === "home" && named.length === 0) {
|
|
45897
|
+
if (registered(harness, projectPath)) {
|
|
45898
|
+
console.log(`${harness.name}: left alone. Its config is machine-wide and may serve another project; pass --${harness.id} to remove it.`);
|
|
45899
|
+
}
|
|
45685
45900
|
continue;
|
|
45686
45901
|
}
|
|
45687
|
-
|
|
45688
|
-
|
|
45902
|
+
const removal = disconnect(harness, projectPath);
|
|
45903
|
+
if (removal.action === "absent") {
|
|
45689
45904
|
continue;
|
|
45690
45905
|
}
|
|
45691
45906
|
if (removal.action === "manual") {
|
|
@@ -45705,15 +45920,19 @@ async function uninstall() {
|
|
|
45705
45920
|
said2(outcome, `disabling ${name}`);
|
|
45706
45921
|
console.log(`${name}: ${outcome.ok ? String(outcome.payload["action"]) : "failed"}`);
|
|
45707
45922
|
}
|
|
45708
|
-
if (before.
|
|
45923
|
+
if (before.runtimeAutoloadPath !== null) {
|
|
45709
45924
|
said2(await setRuntime(godot, projectPath, false), "removing the runtime autoload");
|
|
45710
45925
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload removed`);
|
|
45711
45926
|
}
|
|
45927
|
+
const loader = before.runtimeLoaderAutoload;
|
|
45928
|
+
if (loader !== null && loader.name !== RUNTIME_AUTOLOAD.name) {
|
|
45929
|
+
console.log(`${loader.name} names res://${loader.path}, which brings the runtime up its own way. That line is this project's, so it is left in place; the addon it brings up is gone now, so take the entry out unless that file does nothing without it.`);
|
|
45930
|
+
}
|
|
45712
45931
|
const addons = removeAddons(projectPath);
|
|
45713
45932
|
for (const path of addons) {
|
|
45714
45933
|
console.log(`removed ${path}`);
|
|
45715
45934
|
}
|
|
45716
|
-
const anything = addons.length > 0 || before.
|
|
45935
|
+
const anything = addons.length > 0 || before.runtimeAutoloadPath !== null || enabled.length > 0;
|
|
45717
45936
|
console.log(anything ? `
|
|
45718
45937
|
Reconnect your harness so it stops spawning a server that is no longer installed.` : `
|
|
45719
45938
|
Nothing of gdharness was in this project.`);
|
|
@@ -45722,13 +45941,13 @@ async function upgrade() {
|
|
|
45722
45941
|
const projectPath = projectArgument(1);
|
|
45723
45942
|
const version = getLocalVersion();
|
|
45724
45943
|
const before = inspectProject(projectPath);
|
|
45725
|
-
const
|
|
45726
|
-
if (
|
|
45944
|
+
const present = before.addons.find((addon) => addon.installed);
|
|
45945
|
+
if (present === undefined) {
|
|
45727
45946
|
const where = projectPath === process.cwd() ? "" : ` ${projectPath}`;
|
|
45728
45947
|
throw new UsageError(`gdharness is not installed in ${projectPath}. Run gdharness setup${where} instead.`);
|
|
45729
45948
|
}
|
|
45730
45949
|
const godot = await engine(projectPath);
|
|
45731
|
-
console.log(
|
|
45950
|
+
console.log(present.version === version ? `already ${version}; reinstalling` : `${present.version ?? "an unmarked copy"} -> ${version}`);
|
|
45732
45951
|
const digestBefore = installedEditorDigest(projectPath);
|
|
45733
45952
|
for (const addon of installAddons(projectPath)) {
|
|
45734
45953
|
console.log(`${addon.replaced ? "replaced" : "installed"} ${addon.path}`);
|
|
@@ -45742,7 +45961,21 @@ async function upgrade() {
|
|
|
45742
45961
|
}
|
|
45743
45962
|
said2(await runOperation(godot, "refresh_class_cache", {}, projectPath), "rebuilding the class list");
|
|
45744
45963
|
const launch = launchFor(version, godot.godotPath, projectPath);
|
|
45745
|
-
const
|
|
45964
|
+
const named = namedHarnesses();
|
|
45965
|
+
const already = HARNESSES.filter((harness) => {
|
|
45966
|
+
if (!registered(harness, projectPath)) {
|
|
45967
|
+
return false;
|
|
45968
|
+
}
|
|
45969
|
+
if (harness.scope !== "home" || named.includes(harness)) {
|
|
45970
|
+
return true;
|
|
45971
|
+
}
|
|
45972
|
+
const serves = servedProject(harness, projectPath);
|
|
45973
|
+
if (serves !== null && isSameDirectory(serves, projectPath)) {
|
|
45974
|
+
return true;
|
|
45975
|
+
}
|
|
45976
|
+
console.log(`${harness.name}: left alone. Its config is machine-wide and ${serves === null ? "names no project" : `serves ${serves}`}; pass --${harness.id} to point it at this one.`);
|
|
45977
|
+
return false;
|
|
45978
|
+
});
|
|
45746
45979
|
const moved = [];
|
|
45747
45980
|
let written = 0;
|
|
45748
45981
|
let byHand = 0;
|