gdharness 1.1.21 → 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 +1470 -726
- package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
- package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
- package/build/godot/addons/gdharness_editor/reading.gd +17 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
- package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
- package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +15 -5
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +89 -16
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +107 -343
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +61 -28
- package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
- package/build/godot/operations/audio_buses.gd +47 -11
- package/build/godot/operations/class_cache.gd +18 -2
- package/build/godot/operations/classdb_queries.gd +28 -17
- package/build/godot/operations/dependencies.gd +128 -17
- package/build/godot/operations/file_walk.gd +20 -0
- package/build/godot/operations/gdscript_analysis.gd +59 -20
- package/build/godot/operations/gdscript_authoring.gd +127 -60
- package/build/godot/operations/godot_operations.gd +24 -5
- package/build/godot/operations/import_pipeline.gd +179 -19
- package/build/godot/operations/logger.gd +2 -2
- package/build/godot/operations/plugins.gd +88 -70
- package/build/godot/operations/project_config.gd +41 -6
- package/build/godot/operations/project_diagnostics.gd +27 -11
- package/build/godot/operations/reading.gd +17 -0
- package/build/godot/operations/resource_files.gd +60 -17
- package/build/godot/operations/serialisation.gd +116 -7
- package/build/index.js +1385 -553
- 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";
|
|
@@ -7712,6 +7712,14 @@ function alongside(execPath, name) {
|
|
|
7712
7712
|
}
|
|
7713
7713
|
var init_runner = () => {};
|
|
7714
7714
|
|
|
7715
|
+
// src/dictionary.ts
|
|
7716
|
+
function emptyRecord() {
|
|
7717
|
+
return Object.create(null);
|
|
7718
|
+
}
|
|
7719
|
+
function dictionary(entries) {
|
|
7720
|
+
return Object.assign(emptyRecord(), entries);
|
|
7721
|
+
}
|
|
7722
|
+
|
|
7715
7723
|
// src/call-signal.ts
|
|
7716
7724
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7717
7725
|
function withCallSignal(signal, answer) {
|
|
@@ -7725,14 +7733,6 @@ var init_call_signal = __esm(() => {
|
|
|
7725
7733
|
current = new AsyncLocalStorage;
|
|
7726
7734
|
});
|
|
7727
7735
|
|
|
7728
|
-
// src/dictionary.ts
|
|
7729
|
-
function emptyRecord() {
|
|
7730
|
-
return Object.create(null);
|
|
7731
|
-
}
|
|
7732
|
-
function dictionary(entries) {
|
|
7733
|
-
return Object.assign(emptyRecord(), entries);
|
|
7734
|
-
}
|
|
7735
|
-
|
|
7736
7736
|
// src/game-log.ts
|
|
7737
7737
|
import { StringDecoder } from "node:string_decoder";
|
|
7738
7738
|
function announced(line) {
|
|
@@ -7850,6 +7850,51 @@ var init_game_log = __esm(() => {
|
|
|
7850
7850
|
};
|
|
7851
7851
|
});
|
|
7852
7852
|
|
|
7853
|
+
// src/engine-run.ts
|
|
7854
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
7855
|
+
function runEngine(executable, args, options = {}) {
|
|
7856
|
+
return new Promise((resolve) => {
|
|
7857
|
+
const log = new GameLog;
|
|
7858
|
+
let failure = null;
|
|
7859
|
+
const signal = callSignal();
|
|
7860
|
+
const child = spawn2(executable, [...args], {
|
|
7861
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7862
|
+
windowsHide: true,
|
|
7863
|
+
...signal === undefined ? {} : { signal }
|
|
7864
|
+
});
|
|
7865
|
+
child.stdout.on("data", (chunk) => {
|
|
7866
|
+
log.append("stdout", chunk);
|
|
7867
|
+
});
|
|
7868
|
+
child.stderr.on("data", (chunk) => {
|
|
7869
|
+
log.append("stderr", chunk);
|
|
7870
|
+
});
|
|
7871
|
+
const { timeout } = options;
|
|
7872
|
+
const timer = timeout === undefined ? undefined : setTimeout(() => {
|
|
7873
|
+
failure ??= `it ran past its ${timeout.said}`;
|
|
7874
|
+
child.kill();
|
|
7875
|
+
}, timeout.ms);
|
|
7876
|
+
child.on("error", (error) => {
|
|
7877
|
+
failure ??= error.name === "AbortError" ? "the call was cancelled" : `it could not be started: ${error.message}`;
|
|
7878
|
+
});
|
|
7879
|
+
child.on("close", (code, closedBy) => {
|
|
7880
|
+
clearTimeout(timer);
|
|
7881
|
+
log.finish();
|
|
7882
|
+
const started = child.pid !== undefined;
|
|
7883
|
+
resolve({ log, exitCode: started ? code : null, exitSignal: closedBy, failure });
|
|
7884
|
+
});
|
|
7885
|
+
});
|
|
7886
|
+
}
|
|
7887
|
+
function howItEnded(run) {
|
|
7888
|
+
if (run.failure !== null) {
|
|
7889
|
+
return run.exitSignal === null ? run.failure : `ended by ${run.exitSignal}, because ${run.failure}`;
|
|
7890
|
+
}
|
|
7891
|
+
return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}`;
|
|
7892
|
+
}
|
|
7893
|
+
var init_engine_run = __esm(() => {
|
|
7894
|
+
init_call_signal();
|
|
7895
|
+
init_game_log();
|
|
7896
|
+
});
|
|
7897
|
+
|
|
7853
7898
|
// src/reimport.ts
|
|
7854
7899
|
import { createHash } from "node:crypto";
|
|
7855
7900
|
import { existsSync as existsSync5, readFileSync as readFileSync3, rmSync as rmSync3, statSync as statSync2, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
@@ -7993,10 +8038,8 @@ var init_scratch = __esm(() => {
|
|
|
7993
8038
|
});
|
|
7994
8039
|
|
|
7995
8040
|
// src/headless.ts
|
|
7996
|
-
import {
|
|
7997
|
-
import {
|
|
7998
|
-
import { join as join7 } from "node:path";
|
|
7999
|
-
import { promisify as promisify3 } from "node:util";
|
|
8041
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4, realpathSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
8042
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
8000
8043
|
function snakeCased(params) {
|
|
8001
8044
|
const result = emptyRecord();
|
|
8002
8045
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -8005,34 +8048,55 @@ function snakeCased(params) {
|
|
|
8005
8048
|
}
|
|
8006
8049
|
return result;
|
|
8007
8050
|
}
|
|
8008
|
-
function
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
return parsed;
|
|
8015
|
-
}
|
|
8016
|
-
} catch {}
|
|
8017
|
-
}
|
|
8018
|
-
return null;
|
|
8051
|
+
function engineExtras(outcome) {
|
|
8052
|
+
return {
|
|
8053
|
+
...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
|
|
8054
|
+
...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
|
|
8055
|
+
...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
|
|
8056
|
+
};
|
|
8019
8057
|
}
|
|
8020
|
-
function
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8058
|
+
function problemsIn(log) {
|
|
8059
|
+
return log.everything().flatMap((entry) => {
|
|
8060
|
+
if (entry.severity !== "info") {
|
|
8061
|
+
return [entry];
|
|
8062
|
+
}
|
|
8063
|
+
if (entry.source === "stderr" && entry.text.startsWith(OWN_ERROR)) {
|
|
8064
|
+
return [{ ...entry, severity: "error", text: entry.text.slice(OWN_ERROR.length) }];
|
|
8065
|
+
}
|
|
8066
|
+
return [];
|
|
8067
|
+
});
|
|
8025
8068
|
}
|
|
8026
|
-
function
|
|
8027
|
-
const
|
|
8069
|
+
function reported(problems) {
|
|
8070
|
+
const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
|
|
8071
|
+
return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
|
|
8072
|
+
}
|
|
8073
|
+
function reason(log, problems) {
|
|
8074
|
+
const own = log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
|
|
8028
8075
|
if (own.length > 0) {
|
|
8029
|
-
return own.join("; ");
|
|
8076
|
+
return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
|
|
8077
|
+
}
|
|
8078
|
+
const errors = problems.filter((entry) => entry.severity === "error").map((entry) => entry.text);
|
|
8079
|
+
if (errors.length > 0) {
|
|
8080
|
+
const rest = errors.length - ERRORS_NAMED;
|
|
8081
|
+
return rest > 0 ? `${errors.slice(0, ERRORS_NAMED).join("; ")}; and ${rest} more error${rest === 1 ? "" : "s"}` : errors.join("; ");
|
|
8030
8082
|
}
|
|
8031
|
-
const
|
|
8032
|
-
|
|
8033
|
-
|
|
8083
|
+
const printed = log.everything().filter((entry) => entry.source === "stdout");
|
|
8084
|
+
return printed.at(-1)?.text ?? "no output at all";
|
|
8085
|
+
}
|
|
8086
|
+
function scriptFault(problems, script) {
|
|
8087
|
+
const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
|
|
8088
|
+
const directories = [...new Set([spelled(dirname4(script)), spelled(resolvedOr(dirname4(script)))])];
|
|
8089
|
+
return problems.find((entry) => {
|
|
8090
|
+
const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
|
|
8091
|
+
return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
|
|
8092
|
+
});
|
|
8093
|
+
}
|
|
8094
|
+
function resolvedOr(path) {
|
|
8095
|
+
try {
|
|
8096
|
+
return realpathSync.native(path);
|
|
8097
|
+
} catch {
|
|
8098
|
+
return path;
|
|
8034
8099
|
}
|
|
8035
|
-
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
8036
8100
|
}
|
|
8037
8101
|
function librariesNotCopied(messages) {
|
|
8038
8102
|
const found = new Set;
|
|
@@ -8072,30 +8136,33 @@ async function runImport(godotPath, projectPath, options = {}) {
|
|
|
8072
8136
|
}
|
|
8073
8137
|
async function importOnce(godotPath, projectPath) {
|
|
8074
8138
|
const logDir = scratchDirectory("import");
|
|
8139
|
+
let ran;
|
|
8075
8140
|
try {
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
messages: problems(failed.stderr)
|
|
8085
|
-
};
|
|
8086
|
-
}
|
|
8087
|
-
return {
|
|
8088
|
-
ok: false,
|
|
8089
|
-
message: `the import pass could not be run: ${error instanceof Error ? error.message : String(error)}`,
|
|
8090
|
-
messages: []
|
|
8091
|
-
};
|
|
8141
|
+
ran = await runEngine(godotPath, [
|
|
8142
|
+
"--headless",
|
|
8143
|
+
"--log-file",
|
|
8144
|
+
join7(logDir, "engine.log"),
|
|
8145
|
+
"--path",
|
|
8146
|
+
projectPath,
|
|
8147
|
+
"--import"
|
|
8148
|
+
]);
|
|
8092
8149
|
} finally {
|
|
8093
8150
|
discard(logDir);
|
|
8094
8151
|
}
|
|
8152
|
+
const problems = problemsIn(ran.log);
|
|
8153
|
+
if (ran.exitCode === 0 && ran.failure === null) {
|
|
8154
|
+
return { ok: true, ...reported(problems) };
|
|
8155
|
+
}
|
|
8156
|
+
return {
|
|
8157
|
+
ok: false,
|
|
8158
|
+
message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
|
|
8159
|
+
...reported(problems)
|
|
8160
|
+
};
|
|
8095
8161
|
}
|
|
8096
8162
|
async function runOperation(engine, operation, params, projectPath) {
|
|
8097
8163
|
const paramsDir = scratchDirectory("params");
|
|
8098
8164
|
const paramsFile = join7(paramsDir, `${operation}.json`);
|
|
8165
|
+
const answerFile = join7(paramsDir, "answer.json");
|
|
8099
8166
|
writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
8100
8167
|
const args = [
|
|
8101
8168
|
"--headless",
|
|
@@ -8107,50 +8174,73 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
8107
8174
|
engine.script,
|
|
8108
8175
|
operation,
|
|
8109
8176
|
`@file:${paramsFile}`,
|
|
8177
|
+
answerFile,
|
|
8110
8178
|
...engine.debug ? ["--debug-godot"] : []
|
|
8111
8179
|
];
|
|
8112
|
-
let
|
|
8113
|
-
let
|
|
8180
|
+
let ran;
|
|
8181
|
+
let answer;
|
|
8114
8182
|
try {
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
messages: problems(failed.stderr)
|
|
8123
|
-
};
|
|
8124
|
-
}
|
|
8183
|
+
ran = await runEngine(engine.godotPath, args);
|
|
8184
|
+
answer = answerIn(answerFile);
|
|
8185
|
+
} finally {
|
|
8186
|
+
discard(paramsDir);
|
|
8187
|
+
}
|
|
8188
|
+
const problems = problemsIn(ran.log);
|
|
8189
|
+
if (answer.kind === "unreadable") {
|
|
8125
8190
|
return {
|
|
8126
8191
|
ok: false,
|
|
8127
|
-
message: `${operation} could not be
|
|
8128
|
-
|
|
8192
|
+
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
8193
|
+
...reported(problems)
|
|
8194
|
+
};
|
|
8195
|
+
}
|
|
8196
|
+
if (answer.kind === "none") {
|
|
8197
|
+
const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
|
|
8198
|
+
return {
|
|
8199
|
+
ok: false,
|
|
8200
|
+
message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
|
|
8201
|
+
...reported(problems)
|
|
8129
8202
|
};
|
|
8130
|
-
} finally {
|
|
8131
|
-
discard(paramsDir);
|
|
8132
8203
|
}
|
|
8133
|
-
const
|
|
8134
|
-
if (
|
|
8204
|
+
const fault = scriptFault(problems, engine.script);
|
|
8205
|
+
if (fault !== undefined) {
|
|
8135
8206
|
return {
|
|
8136
8207
|
ok: false,
|
|
8137
|
-
message: `${operation}
|
|
8138
|
-
|
|
8208
|
+
message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
|
|
8209
|
+
...reported(problems)
|
|
8210
|
+
};
|
|
8211
|
+
}
|
|
8212
|
+
const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under engine_messages.`;
|
|
8213
|
+
return {
|
|
8214
|
+
ok: true,
|
|
8215
|
+
payload: answer.payload,
|
|
8216
|
+
...reported(problems),
|
|
8217
|
+
...afterAnswer === undefined ? {} : { afterAnswer }
|
|
8218
|
+
};
|
|
8219
|
+
}
|
|
8220
|
+
function answerIn(file) {
|
|
8221
|
+
if (!existsSync7(file)) {
|
|
8222
|
+
return { kind: "none" };
|
|
8223
|
+
}
|
|
8224
|
+
const text = readFileSync4(file, "utf8");
|
|
8225
|
+
try {
|
|
8226
|
+
const parsed = JSON.parse(text);
|
|
8227
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { kind: "answer", payload: parsed } : { kind: "unreadable", why: `it is JSON but not an object: ${text.slice(0, 200)}` };
|
|
8228
|
+
} catch (error) {
|
|
8229
|
+
return {
|
|
8230
|
+
kind: "unreadable",
|
|
8231
|
+
why: `${error instanceof Error ? error.message : String(error)}: ${text.slice(0, 200)}`
|
|
8139
8232
|
};
|
|
8140
8233
|
}
|
|
8141
|
-
return { ok: true, payload, messages: problems(stderr) };
|
|
8142
8234
|
}
|
|
8143
|
-
var
|
|
8235
|
+
var OWN_ERROR = "[ERROR] ", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
|
|
8144
8236
|
var init_headless = __esm(() => {
|
|
8145
|
-
|
|
8146
|
-
init_game_log();
|
|
8237
|
+
init_engine_run();
|
|
8147
8238
|
init_reimport();
|
|
8148
8239
|
init_scratch();
|
|
8149
|
-
run3 = promisify3(execFile3);
|
|
8150
8240
|
});
|
|
8151
8241
|
|
|
8152
8242
|
// src/update-check.ts
|
|
8153
|
-
import { existsSync as
|
|
8243
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8154
8244
|
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
8155
8245
|
import { join as join8 } from "node:path";
|
|
8156
8246
|
function packageUrl(root) {
|
|
@@ -8192,10 +8282,10 @@ function cacheFile(environment = process.env) {
|
|
|
8192
8282
|
}
|
|
8193
8283
|
function readCache(path) {
|
|
8194
8284
|
try {
|
|
8195
|
-
if (!
|
|
8285
|
+
if (!existsSync8(path)) {
|
|
8196
8286
|
return null;
|
|
8197
8287
|
}
|
|
8198
|
-
const parsed = JSON.parse(
|
|
8288
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
8199
8289
|
if (typeof parsed !== "object" || parsed === null) {
|
|
8200
8290
|
return null;
|
|
8201
8291
|
}
|
|
@@ -8343,7 +8433,7 @@ var init_update_check = __esm(() => {
|
|
|
8343
8433
|
});
|
|
8344
8434
|
|
|
8345
8435
|
// src/server-version.ts
|
|
8346
|
-
import { readFileSync as
|
|
8436
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
8347
8437
|
function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
8348
8438
|
if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
|
|
8349
8439
|
return addonDigest !== shippedDigest;
|
|
@@ -8356,23 +8446,24 @@ function sameCodeNote(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
|
8356
8446
|
}
|
|
8357
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.`;
|
|
8358
8448
|
}
|
|
8359
|
-
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
8449
|
+
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
|
|
8360
8450
|
if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
|
|
8361
8451
|
return;
|
|
8362
8452
|
}
|
|
8363
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.";
|
|
8364
8455
|
if (reported === serverVersion) {
|
|
8365
|
-
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}`;
|
|
8366
8457
|
}
|
|
8367
8458
|
const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
|
|
8368
8459
|
const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
|
|
8369
8460
|
if (reported !== "" && isNewer(reported, serverVersion)) {
|
|
8370
8461
|
return `${both} This server is the older half: reconnect it in your harness so it spawns ${reported}.`;
|
|
8371
8462
|
}
|
|
8372
|
-
return `${both}
|
|
8463
|
+
return `${both} ${restart}`;
|
|
8373
8464
|
}
|
|
8374
|
-
function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
8375
|
-
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);
|
|
8376
8467
|
if (staleNote === undefined || typeof answer !== "object" || answer === null || Array.isArray(answer)) {
|
|
8377
8468
|
return answer;
|
|
8378
8469
|
}
|
|
@@ -8385,7 +8476,7 @@ var init_server_version = __esm(() => {
|
|
|
8385
8476
|
GODOT_DEBUG_MODE_DEFAULT = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE;
|
|
8386
8477
|
SERVER_VERSION = (() => {
|
|
8387
8478
|
try {
|
|
8388
|
-
const pkg = JSON.parse(
|
|
8479
|
+
const pkg = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
|
|
8389
8480
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
8390
8481
|
} catch {
|
|
8391
8482
|
return "0.0.0";
|
|
@@ -8518,9 +8609,79 @@ var init_issues = __esm(() => {
|
|
|
8518
8609
|
ENHANCEMENT_URL = `${NEW_ISSUE}?template=feature_request.md`;
|
|
8519
8610
|
});
|
|
8520
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
|
+
|
|
8521
8679
|
// src/class-cache.ts
|
|
8522
|
-
import { existsSync as
|
|
8680
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync3 } from "node:fs";
|
|
8523
8681
|
import { join as join9 } from "node:path";
|
|
8682
|
+
function isToolScript(source) {
|
|
8683
|
+
return TOOL.test(codeOf(source));
|
|
8684
|
+
}
|
|
8524
8685
|
function declaredClasses(projectPath) {
|
|
8525
8686
|
const declared = new Map;
|
|
8526
8687
|
eachScript(projectPath, (script, source) => {
|
|
@@ -8533,7 +8694,7 @@ function declaredClasses(projectPath) {
|
|
|
8533
8694
|
}
|
|
8534
8695
|
function eachScript(projectPath, each) {
|
|
8535
8696
|
const visit = (directory, prefix) => {
|
|
8536
|
-
if (
|
|
8697
|
+
if (existsSync9(join9(directory, ".gdignore"))) {
|
|
8537
8698
|
return;
|
|
8538
8699
|
}
|
|
8539
8700
|
for (const entry of readdirSync3(directory, { withFileTypes: true })) {
|
|
@@ -8544,7 +8705,7 @@ function eachScript(projectPath, each) {
|
|
|
8544
8705
|
if (entry.isDirectory()) {
|
|
8545
8706
|
visit(path, `${prefix}${entry.name}/`);
|
|
8546
8707
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
8547
|
-
each(`res://${prefix}${entry.name}`,
|
|
8708
|
+
each(`res://${prefix}${entry.name}`, readFileSync7(path, "utf8"));
|
|
8548
8709
|
}
|
|
8549
8710
|
}
|
|
8550
8711
|
};
|
|
@@ -8560,7 +8721,7 @@ function scriptsNaming(projectPath, classes) {
|
|
|
8560
8721
|
}
|
|
8561
8722
|
const code = codeOf(source);
|
|
8562
8723
|
if (classesNamedIn(code, named).length > 0) {
|
|
8563
|
-
naming.push({ script, tool:
|
|
8724
|
+
naming.push({ script, tool: TOOL.test(code) });
|
|
8564
8725
|
}
|
|
8565
8726
|
});
|
|
8566
8727
|
return naming.sort((a, b) => a.script.localeCompare(b.script));
|
|
@@ -8594,11 +8755,11 @@ function codeOf(source) {
|
|
|
8594
8755
|
}
|
|
8595
8756
|
function cachedClasses(projectPath) {
|
|
8596
8757
|
const cache = join9(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
8597
|
-
if (!
|
|
8758
|
+
if (!existsSync9(cache)) {
|
|
8598
8759
|
return null;
|
|
8599
8760
|
}
|
|
8600
8761
|
const listed = new Map;
|
|
8601
|
-
const text =
|
|
8762
|
+
const text = readFileSync7(cache, "utf8");
|
|
8602
8763
|
for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
|
|
8603
8764
|
listed.set(entry[1] ?? "", entry[2] ?? "");
|
|
8604
8765
|
}
|
|
@@ -8767,6 +8928,19 @@ function classRemedy(entries) {
|
|
|
8767
8928
|
const oneAtATime = declaring.length === 1 ? "" : " Take them one at a time: a rescan that brings in a class also reloads every script naming it and lists them under dependentsReloaded, so a script listed there needs no call of its own.";
|
|
8768
8929
|
return `For ${entries.map(written).join(", ")}, run ${reload} first.${oneAtATime} In the one project ` + "that has reproduced a stale enum member, three times, the reload cleared it at once each time, " + "and the one plain editor_rescan tried left the diagnostic standing.";
|
|
8769
8930
|
}
|
|
8931
|
+
function notReloadedNote(broughtIn, notReloaded) {
|
|
8932
|
+
const tried = notReloaded.filter((one) => one.tool !== true).map((one) => one.scriptPath);
|
|
8933
|
+
const tools = notReloaded.filter((one) => one.tool === true).map((one) => one.scriptPath);
|
|
8934
|
+
const all = [...tried, ...tools];
|
|
8935
|
+
const remedies = [];
|
|
8936
|
+
if (tried.length > 0) {
|
|
8937
|
+
remedies.push(`For ${tried.join(", ")}: editor_rescan with reloadScript on ${tried.length === 1 ? "it" : "each"}, or editor_launch restart.`);
|
|
8938
|
+
}
|
|
8939
|
+
if (tools.length > 0) {
|
|
8940
|
+
remedies.push(`For ${tools.join(", ")}, ${tools.length === 1 ? "a @tool script" : "@tool scripts"}, which ${tools.length === 1 ? "runs" : "run"} inside the editor and ${tools.length === 1 ? "is" : "are"} not reloaded from inside a call: editor_launch restart.`);
|
|
8941
|
+
}
|
|
8942
|
+
return `The scan brought in ${broughtIn.join(", ")}, and ${all.join(", ")} ${all.length === 1 ? "names one of them and" : "name them and"} could not be reloaded, so the editor still holds what it compiled while it could not see the class and reports that to the language server. ${remedies.join(" ")}`;
|
|
8943
|
+
}
|
|
8770
8944
|
function failedReloadNote(failed) {
|
|
8771
8945
|
const missing = (before, after) => before.filter((name) => !after.includes(name));
|
|
8772
8946
|
const lost = [
|
|
@@ -8775,7 +8949,17 @@ function failedReloadNote(failed) {
|
|
|
8775
8949
|
];
|
|
8776
8950
|
const unchanged = lost.length === 0 && missing(failed.heldAfter, failed.heldBefore).length === 0 && missing(failed.constantsAfter, failed.constantsBefore).length === 0;
|
|
8777
8951
|
const answered = failed.codeName === "" ? `error ${failed.code}` : `error ${failed.code}, ${failed.codeName}`;
|
|
8778
|
-
const
|
|
8952
|
+
const unusable = "A failed compile leaves the copy the editor holds unusable, measured on 4.7.2: the editor can make no instance of it, and one it already made has lost its methods, while its member lists read as before.";
|
|
8953
|
+
let copy;
|
|
8954
|
+
if (failed.restored === null) {
|
|
8955
|
+
copy = `${unusable} This editor's addon does not put it back, so it stays that way until the script compiles: reload it then, or restart the editor with editor_launch restart.`;
|
|
8956
|
+
} else if (!failed.restored) {
|
|
8957
|
+
copy = `${unusable} The editor compiled it again from the text it was built from, and that failed too${failed.restoredAs === "" ? "" : ` (${failed.restoredAs})`}, so it stays that way until the script compiles: reload it then, or restart the editor with editor_launch restart.`;
|
|
8958
|
+
} else if (unchanged) {
|
|
8959
|
+
copy = "The copy the editor holds was compiled again from the text it was built from, which puts it back as it was, with the members under heldBeforeReload and heldConstantsBeforeReload.";
|
|
8960
|
+
} else {
|
|
8961
|
+
copy = `The copy the editor holds was compiled again from the text it was built from and changed even so${lost.length === 0 ? "" : `, and has lost ${lost.join(", ")}`}: anything diagnosed against it now is answered from what is left, and reloading it once it compiles rebuilds it.`;
|
|
8962
|
+
}
|
|
8779
8963
|
const order = failed.names.length === 0 ? "" : ` If they deny a member of ${failed.names.join(" or ")} that its file declares, the editor's copy of that class is behind: reload that class first with reloadScript and then this script, the order that cleared it in the one project that has met this.`;
|
|
8780
8964
|
return `${failed.script} did not compile: Godot answered ${answered}. ${copy} ` + `script_diagnostics on it gives the errors.${order}`;
|
|
8781
8965
|
}
|
|
@@ -8808,7 +8992,7 @@ function declaredSince(projectPath, paths, names, since) {
|
|
|
8808
8992
|
});
|
|
8809
8993
|
}
|
|
8810
8994
|
function cachedAtMissingPaths(cached, projectPath) {
|
|
8811
|
-
return [...cached].filter(([, path]) => path.startsWith("res://") && !
|
|
8995
|
+
return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync9(join9(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
|
|
8812
8996
|
}
|
|
8813
8997
|
function staleAgainst(cached, projectPath) {
|
|
8814
8998
|
return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
|
|
@@ -8825,9 +9009,10 @@ function unseenByEditor(projectPath, editorHolds) {
|
|
|
8825
9009
|
const held = new Set(editorHolds);
|
|
8826
9010
|
return [...declaredClasses(projectPath)].filter(([name]) => !held.has(name)).map(([className, path]) => ({ className, path })).sort((one, other) => one.className < other.className ? -1 : one.className > other.className ? 1 : 0);
|
|
8827
9011
|
}
|
|
8828
|
-
var DECLARATION;
|
|
9012
|
+
var DECLARATION, TOOL;
|
|
8829
9013
|
var init_class_cache = __esm(() => {
|
|
8830
9014
|
DECLARATION = /^(?:@[a-z_]+(?:\([^)\n]*\))?\s+)*class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m;
|
|
9015
|
+
TOOL = /^[ \t]*(?:@[a-z_]+(?:\([^)\n]*\))?[ \t]+)*@tool\b/m;
|
|
8831
9016
|
});
|
|
8832
9017
|
|
|
8833
9018
|
// node_modules/zod/v4/core/util.js
|
|
@@ -15933,74 +16118,13 @@ var init_types = __esm(() => {
|
|
|
15933
16118
|
};
|
|
15934
16119
|
});
|
|
15935
16120
|
|
|
15936
|
-
// src/paths.ts
|
|
15937
|
-
import { realpathSync } from "node:fs";
|
|
15938
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
15939
|
-
function realPathOr(path) {
|
|
15940
|
-
try {
|
|
15941
|
-
return realpathSync.native(path);
|
|
15942
|
-
} catch {
|
|
15943
|
-
return path;
|
|
15944
|
-
}
|
|
15945
|
-
}
|
|
15946
|
-
function isSameDirectory(onePath, otherPath) {
|
|
15947
|
-
return relative(realPathOr(resolve(onePath)), realPathOr(resolve(otherPath))) === "";
|
|
15948
|
-
}
|
|
15949
|
-
function isWithinRoot(rootPath, candidatePath) {
|
|
15950
|
-
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
15951
|
-
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
15952
|
-
}
|
|
15953
|
-
function resolveWithinProject(projectPath, candidatePath) {
|
|
15954
|
-
const trimmed = candidatePath.trim();
|
|
15955
|
-
if (trimmed === "") {
|
|
15956
|
-
return { ok: false, reason: "Path is empty." };
|
|
15957
|
-
}
|
|
15958
|
-
if (trimmed.includes("\x00")) {
|
|
15959
|
-
return { ok: false, reason: "Path contains a null byte." };
|
|
15960
|
-
}
|
|
15961
|
-
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
15962
|
-
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
15963
|
-
if (scheme) {
|
|
15964
|
-
return {
|
|
15965
|
-
ok: false,
|
|
15966
|
-
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
|
|
15967
|
-
};
|
|
15968
|
-
}
|
|
15969
|
-
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
15970
|
-
return {
|
|
15971
|
-
ok: false,
|
|
15972
|
-
reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
|
|
15973
|
-
};
|
|
15974
|
-
}
|
|
15975
|
-
if (withoutScheme.includes(":")) {
|
|
15976
|
-
return {
|
|
15977
|
-
ok: false,
|
|
15978
|
-
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
15979
|
-
};
|
|
15980
|
-
}
|
|
15981
|
-
const root = resolve(projectPath);
|
|
15982
|
-
const absolutePath = resolve(root, withoutScheme);
|
|
15983
|
-
if (!isWithinRoot(root, absolutePath)) {
|
|
15984
|
-
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
15985
|
-
}
|
|
15986
|
-
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
15987
|
-
if (relativePath === "") {
|
|
15988
|
-
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
15989
|
-
}
|
|
15990
|
-
return { ok: true, absolutePath, relativePath };
|
|
15991
|
-
}
|
|
15992
|
-
var RESOURCE_SCHEME = "res://", OTHER_SCHEME;
|
|
15993
|
-
var init_paths = __esm(() => {
|
|
15994
|
-
OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
15995
|
-
});
|
|
15996
|
-
|
|
15997
16121
|
// src/resources.ts
|
|
15998
|
-
import { readFileSync as
|
|
16122
|
+
import { readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
|
|
15999
16123
|
import { extname, resolve as resolve2 } from "node:path";
|
|
16000
16124
|
function ensureProjectPath(getProjectPath) {
|
|
16001
16125
|
const projectPath = getProjectPath();
|
|
16002
16126
|
if (!projectPath) {
|
|
16003
|
-
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.");
|
|
16004
16128
|
}
|
|
16005
16129
|
return resolve2(projectPath);
|
|
16006
16130
|
}
|
|
@@ -16016,7 +16140,22 @@ function resolveProjectFile(projectPath, resourcePath) {
|
|
|
16016
16140
|
if (!contained.ok) {
|
|
16017
16141
|
throw new Refusal(contained.reason);
|
|
16018
16142
|
}
|
|
16019
|
-
|
|
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;
|
|
16020
16159
|
}
|
|
16021
16160
|
function parseGodotUri(uri) {
|
|
16022
16161
|
if (uri === "godot://project/info") {
|
|
@@ -16161,7 +16300,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
16161
16300
|
const parsedUri = parseGodotUri(uri);
|
|
16162
16301
|
if (parsedUri.kind === "project-info") {
|
|
16163
16302
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
16164
|
-
const rawProject =
|
|
16303
|
+
const rawProject = readFileSync8(projectFilePath, "utf-8");
|
|
16165
16304
|
const parsedProject = parseProjectGodot(rawProject);
|
|
16166
16305
|
return {
|
|
16167
16306
|
mimeType: "application/json",
|
|
@@ -16170,7 +16309,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
16170
16309
|
}
|
|
16171
16310
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
16172
16311
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
16173
|
-
const text =
|
|
16312
|
+
const text = readFileSync8(filePath, "utf-8");
|
|
16174
16313
|
return {
|
|
16175
16314
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
16176
16315
|
text
|
|
@@ -16302,16 +16441,16 @@ import { spawnSync } from "node:child_process";
|
|
|
16302
16441
|
import { createHash as createHash3 } from "node:crypto";
|
|
16303
16442
|
import {
|
|
16304
16443
|
cpSync,
|
|
16305
|
-
existsSync as
|
|
16444
|
+
existsSync as existsSync10,
|
|
16306
16445
|
mkdirSync as mkdirSync4,
|
|
16307
16446
|
readdirSync as readdirSync4,
|
|
16308
|
-
readFileSync as
|
|
16447
|
+
readFileSync as readFileSync9,
|
|
16309
16448
|
rmdirSync,
|
|
16310
16449
|
rmSync as rmSync5,
|
|
16311
16450
|
statSync as statSync4,
|
|
16312
16451
|
writeFileSync as writeFileSync6
|
|
16313
16452
|
} from "node:fs";
|
|
16314
|
-
import { dirname as
|
|
16453
|
+
import { dirname as dirname5, join as join10 } from "node:path";
|
|
16315
16454
|
import { fileURLToPath } from "node:url";
|
|
16316
16455
|
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
16317
16456
|
const hash = createHash3("sha256");
|
|
@@ -16327,7 +16466,7 @@ function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
|
16327
16466
|
} else {
|
|
16328
16467
|
hash.update(`${inside}
|
|
16329
16468
|
`);
|
|
16330
|
-
hash.update(
|
|
16469
|
+
hash.update(readFileSync9(path));
|
|
16331
16470
|
hash.update(`
|
|
16332
16471
|
`);
|
|
16333
16472
|
}
|
|
@@ -16350,29 +16489,29 @@ function shippedEditorDigest() {
|
|
|
16350
16489
|
}
|
|
16351
16490
|
function installedEditorDigest(projectPath) {
|
|
16352
16491
|
const marker = join10(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
|
|
16353
|
-
if (!
|
|
16492
|
+
if (!existsSync10(marker)) {
|
|
16354
16493
|
return null;
|
|
16355
16494
|
}
|
|
16356
|
-
const said =
|
|
16495
|
+
const said = readFileSync9(marker, "utf8").trim();
|
|
16357
16496
|
return said === "" ? null : said;
|
|
16358
16497
|
}
|
|
16359
16498
|
function shippedAddonsDirectory() {
|
|
16360
|
-
return join10(
|
|
16499
|
+
return join10(dirname5(fileURLToPath(import.meta.url)), "godot", "addons");
|
|
16361
16500
|
}
|
|
16362
16501
|
function shippedOperationsScript() {
|
|
16363
|
-
return join10(
|
|
16502
|
+
return join10(dirname5(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
|
|
16364
16503
|
}
|
|
16365
16504
|
function installAddons(projectPath, from = shippedAddonsDirectory()) {
|
|
16366
16505
|
const installed = [];
|
|
16367
16506
|
for (const name of ADDONS) {
|
|
16368
16507
|
const source = join10(from, name);
|
|
16369
|
-
if (!
|
|
16508
|
+
if (!existsSync10(join10(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
|
|
16370
16509
|
throw new Error(`The package holds no ${name} addon at ${source}.`);
|
|
16371
16510
|
}
|
|
16372
16511
|
const target = join10(projectPath, "addons", name);
|
|
16373
|
-
const replaced =
|
|
16512
|
+
const replaced = existsSync10(target);
|
|
16374
16513
|
rmSync5(target, { recursive: true, force: true });
|
|
16375
|
-
mkdirSync4(
|
|
16514
|
+
mkdirSync4(dirname5(target), { recursive: true });
|
|
16376
16515
|
cpSync(source, target, { recursive: true });
|
|
16377
16516
|
writeFileSync6(join10(target, VERSION_MARKER), `${SERVER_VERSION}
|
|
16378
16517
|
`);
|
|
@@ -16384,23 +16523,23 @@ function installAddons(projectPath, from = shippedAddonsDirectory()) {
|
|
|
16384
16523
|
}
|
|
16385
16524
|
function installedAddonVersion(projectPath) {
|
|
16386
16525
|
const marker = join10(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
16387
|
-
if (!
|
|
16526
|
+
if (!existsSync10(marker)) {
|
|
16388
16527
|
return null;
|
|
16389
16528
|
}
|
|
16390
|
-
const said =
|
|
16529
|
+
const said = readFileSync9(marker, "utf8").trim();
|
|
16391
16530
|
return said === "" ? null : said;
|
|
16392
16531
|
}
|
|
16393
16532
|
function removeAddons(projectPath) {
|
|
16394
16533
|
const removed = [];
|
|
16395
16534
|
for (const name of ADDONS) {
|
|
16396
16535
|
const target = join10(projectPath, "addons", name);
|
|
16397
|
-
if (
|
|
16536
|
+
if (existsSync10(target)) {
|
|
16398
16537
|
rmSync5(target, { recursive: true, force: true });
|
|
16399
16538
|
removed.push(target);
|
|
16400
16539
|
}
|
|
16401
16540
|
}
|
|
16402
16541
|
const addons = join10(projectPath, "addons");
|
|
16403
|
-
if (removed.length > 0 &&
|
|
16542
|
+
if (removed.length > 0 && existsSync10(addons) && readdirSync4(addons).length === 0) {
|
|
16404
16543
|
rmdirSync(addons);
|
|
16405
16544
|
removed.push(addons);
|
|
16406
16545
|
}
|
|
@@ -16424,7 +16563,7 @@ function autoloadIsOurs(named) {
|
|
|
16424
16563
|
return named === null || named === `res://${RUNTIME_AUTOLOAD.path}`;
|
|
16425
16564
|
}
|
|
16426
16565
|
async function setRuntime(engine, projectPath, on) {
|
|
16427
|
-
return on ? await runOperation(engine, "add_autoload", { name: RUNTIME_AUTOLOAD.name, path: `res://${RUNTIME_AUTOLOAD.path}`,
|
|
16566
|
+
return on ? await runOperation(engine, "add_autoload", { name: RUNTIME_AUTOLOAD.name, path: `res://${RUNTIME_AUTOLOAD.path}`, global: true }, projectPath) : await runOperation(engine, "remove_autoload", { name: RUNTIME_AUTOLOAD.name }, projectPath);
|
|
16428
16567
|
}
|
|
16429
16568
|
function loaderAmong(projectPath, named) {
|
|
16430
16569
|
let byName = null;
|
|
@@ -16434,7 +16573,7 @@ function loaderAmong(projectPath, named) {
|
|
|
16434
16573
|
}
|
|
16435
16574
|
let source = null;
|
|
16436
16575
|
try {
|
|
16437
|
-
source =
|
|
16576
|
+
source = readFileSync9(join10(projectPath, path), "utf8");
|
|
16438
16577
|
} catch {}
|
|
16439
16578
|
if (source?.includes(RUNTIME_AUTOLOAD.path)) {
|
|
16440
16579
|
return { name: entry, path, exists: true };
|
|
@@ -16478,8 +16617,8 @@ function inspectProject(projectPath) {
|
|
|
16478
16617
|
const addons = ADDONS.map((name) => {
|
|
16479
16618
|
const directory = join10(projectPath, "addons", name);
|
|
16480
16619
|
const marker = join10(directory, VERSION_MARKER);
|
|
16481
|
-
const installed =
|
|
16482
|
-
const version =
|
|
16620
|
+
const installed = existsSync10(directory);
|
|
16621
|
+
const version = existsSync10(marker) ? readFileSync9(marker, "utf8").trim() : null;
|
|
16483
16622
|
const current = version === SERVER_VERSION;
|
|
16484
16623
|
if (!installed) {
|
|
16485
16624
|
problems.push(`addons/${name} is not installed; run gdharness setup`);
|
|
@@ -16488,7 +16627,7 @@ function inspectProject(projectPath) {
|
|
|
16488
16627
|
}
|
|
16489
16628
|
return { name, installed, version, current };
|
|
16490
16629
|
});
|
|
16491
|
-
const settings = parseProjectGodot(
|
|
16630
|
+
const settings = parseProjectGodot(readFileSync9(join10(projectPath, "project.godot"), "utf8"));
|
|
16492
16631
|
const pluginsEnabled = enabledPlugins(settings);
|
|
16493
16632
|
for (const name of EDITOR_PLUGINS) {
|
|
16494
16633
|
if (!pluginsEnabled.includes(name)) {
|
|
@@ -16504,7 +16643,7 @@ function inspectProject(projectPath) {
|
|
|
16504
16643
|
const tracked = trackedByGit(projectPath, [...named.values()]);
|
|
16505
16644
|
const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
|
|
16506
16645
|
for (const [name, path] of named) {
|
|
16507
|
-
if (!
|
|
16646
|
+
if (!existsSync10(join10(projectPath, path))) {
|
|
16508
16647
|
if (!addonMissing(path)) {
|
|
16509
16648
|
problems.push(`the ${name} autoload names res://${path}, which is not in this project, so it boots with a missing script; restore that file, or point the autoload at one that is here`);
|
|
16510
16649
|
}
|
|
@@ -16664,7 +16803,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16664
16803
|
};
|
|
16665
16804
|
PROPERTIES = {
|
|
16666
16805
|
type: "object",
|
|
16667
|
-
description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another.',
|
|
16806
|
+
description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds.',
|
|
16668
16807
|
additionalProperties: true
|
|
16669
16808
|
};
|
|
16670
16809
|
XY = {
|
|
@@ -16819,11 +16958,18 @@ var init_tool_definitions = __esm(() => {
|
|
|
16819
16958
|
},
|
|
16820
16959
|
value: {
|
|
16821
16960
|
blank: true,
|
|
16822
|
-
description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}. "" writes an empty string.'
|
|
16961
|
+
description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}, and infinity as {"_type": "float", "value": "inf"}, the form a read answers with. "" writes an empty string.'
|
|
16823
16962
|
},
|
|
16824
16963
|
name: { type: "string", description: "Autoload name." },
|
|
16825
16964
|
path: { type: "string", description: "Autoload script or scene inside the project." },
|
|
16826
|
-
enabled: {
|
|
16965
|
+
enabled: {
|
|
16966
|
+
type: "boolean",
|
|
16967
|
+
description: "Autoloads: Godot 4 loads every autoload in the list, so false is refused. Use global: false to keep the name out of the global scope, or remove_autoload to stop it loading."
|
|
16968
|
+
},
|
|
16969
|
+
global: {
|
|
16970
|
+
type: "boolean",
|
|
16971
|
+
description: `Autoloads: make the name a global variable, the * in project.godot and the editor's "Global Variable" column. Default true. The autoload loads either way.`
|
|
16972
|
+
},
|
|
16827
16973
|
scenePath: SCENE_PATH,
|
|
16828
16974
|
actionName: { type: "string", description: 'Input action name, such as "jump".' },
|
|
16829
16975
|
events: INPUT_EVENTS,
|
|
@@ -16835,9 +16981,15 @@ var init_tool_definitions = __esm(() => {
|
|
|
16835
16981
|
},
|
|
16836
16982
|
pluginName: { type: "string", description: "Folder name under addons/." },
|
|
16837
16983
|
busName: { type: "string", description: "Audio bus name." },
|
|
16838
|
-
parentBusIndex: {
|
|
16984
|
+
parentBusIndex: {
|
|
16985
|
+
type: "number",
|
|
16986
|
+
description: "Audio buses: the bus to send to. Default 0, Master. The new bus goes right after it, so every later bus moves one along, and the answer lists them all."
|
|
16987
|
+
},
|
|
16839
16988
|
busIndex: { type: "number", description: "Audio bus index." },
|
|
16840
|
-
effectIndex: {
|
|
16989
|
+
effectIndex: {
|
|
16990
|
+
type: "number",
|
|
16991
|
+
description: "Slot on the bus for the effect: the effect there is replaced, and one past the last adds one. Any other slot is refused."
|
|
16992
|
+
},
|
|
16841
16993
|
effectType: { type: "string", description: 'Effect class, such as "AudioEffectReverb".' },
|
|
16842
16994
|
volumeDb: { type: "number", description: "Bus volume in decibels." }
|
|
16843
16995
|
},
|
|
@@ -16856,7 +17008,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16856
17008
|
disable_plugin: { summary: "disable an addon", requires: ["pluginName"] },
|
|
16857
17009
|
add_audio_bus: { summary: "add an audio bus", requires: ["busName"] },
|
|
16858
17010
|
set_audio_bus_effect: {
|
|
16859
|
-
summary: "
|
|
17011
|
+
summary: "put an effect in a slot on a bus",
|
|
16860
17012
|
requires: ["busIndex", "effectIndex", "effectType"]
|
|
16861
17013
|
},
|
|
16862
17014
|
set_audio_bus_volume: { summary: "set a bus volume", requires: ["busIndex", "volumeDb"] }
|
|
@@ -16895,7 +17047,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
16895
17047
|
type: "boolean",
|
|
16896
17048
|
description: "forward: include the engine's own res://. resources. Default false."
|
|
16897
17049
|
},
|
|
16898
|
-
fileTypes: {
|
|
17050
|
+
fileTypes: {
|
|
17051
|
+
type: "array",
|
|
17052
|
+
items: { type: "string" },
|
|
17053
|
+
description: 'reverse: extensions to look in, plus "godot" for project.godot, "cfg" for plugin.cfg and export_presets.cfg, and "import" for import sidecars. Default all of tscn, tres, gd, gdshader, godot, cfg and import, since an autoload, the main scene and a plugin script are named only in the settings files.'
|
|
17054
|
+
}
|
|
16899
17055
|
},
|
|
16900
17056
|
requires: ["projectPath", "resourcePath"]
|
|
16901
17057
|
},
|
|
@@ -16940,7 +17096,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
16940
17096
|
summary: "reimport what status says is outdated or failed, one resource with resourcePath, or what is current as well with force. Through the open editor when one serves the project, so it reloads what it holds, waiting for it to finish; otherwise through the engine's own import pass. Answered from a second status read: reimported lists what is current now, notReimported what is not, with its status and reason, and via says which of the two did the work. stillImporting is an editor that had not finished within timeoutMs",
|
|
16941
17097
|
requires: []
|
|
16942
17098
|
},
|
|
16943
|
-
uid: {
|
|
17099
|
+
uid: {
|
|
17100
|
+
summary: "the UID of one file and where it was read: a script's or shader's .uid file, an imported file's .import, or a scene's or resource's own header",
|
|
17101
|
+
requires: ["resourcePath"]
|
|
17102
|
+
},
|
|
16944
17103
|
refresh_uids: {
|
|
16945
17104
|
summary: "import the project so every script and shader has its .uid sidecar, and name under uidsCreated the ones this made and under stillWithoutUid the ones the engine would not import: it writes no scene and no script, so a project whose sidecars are all present is left untouched",
|
|
16946
17105
|
requires: []
|
|
@@ -17001,11 +17160,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
17001
17160
|
},
|
|
17002
17161
|
{
|
|
17003
17162
|
name: "scene_create",
|
|
17004
|
-
description: "Creates a scene file, saves one, or saves a copy under a new path. Needs the editor connected.",
|
|
17163
|
+
description: "Creates a scene file where none is, saves one, or saves a copy under a new path. A save of a scene open in the editor saves the editor's copy, unsaved changes included. Needs the editor connected.",
|
|
17005
17164
|
parameters: {
|
|
17006
17165
|
projectPath: PROJECT_PATH,
|
|
17007
17166
|
scenePath: SCENE_PATH,
|
|
17008
|
-
rootNodeType: { type: "string", description: "create: the root node class. Default
|
|
17167
|
+
rootNodeType: { type: "string", description: "create: the root node class. Default Node." },
|
|
17009
17168
|
newPath: { type: "string", description: "save_as: where the copy goes." }
|
|
17010
17169
|
},
|
|
17011
17170
|
requires: ["projectPath", "scenePath"],
|
|
@@ -17029,7 +17188,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17029
17188
|
},
|
|
17030
17189
|
{
|
|
17031
17190
|
name: "scene_node",
|
|
17032
|
-
description: "One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. Needs the editor connected.",
|
|
17191
|
+
description: "One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. A write is refused while the editor holds unsaved changes to the scene, and for a node that belongs to a scene this one instances or inherits. Needs the editor connected.",
|
|
17033
17192
|
parameters: {
|
|
17034
17193
|
projectPath: PROJECT_PATH,
|
|
17035
17194
|
scenePath: SCENE_PATH,
|
|
@@ -17047,7 +17206,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
17047
17206
|
type: "boolean",
|
|
17048
17207
|
description: "get: include properties still at their default. Default false."
|
|
17049
17208
|
},
|
|
17050
|
-
layer: {
|
|
17209
|
+
layer: {
|
|
17210
|
+
type: "number",
|
|
17211
|
+
description: "set_tilemap_cells: the layer of a TileMap. Default 0. A TileMapLayer is one layer."
|
|
17212
|
+
},
|
|
17051
17213
|
cells: TILEMAP_CELLS
|
|
17052
17214
|
},
|
|
17053
17215
|
requires: ["projectPath", "scenePath"],
|
|
@@ -17058,7 +17220,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
17058
17220
|
duplicate: { summary: "copy a node and its children", requires: ["nodePath", "newName"] },
|
|
17059
17221
|
reparent: { summary: "move a node under another parent", requires: ["nodePath", "newParentPath"] },
|
|
17060
17222
|
delete: { summary: "remove a node and its children", requires: ["nodePath"] },
|
|
17061
|
-
set_tilemap_cells: {
|
|
17223
|
+
set_tilemap_cells: {
|
|
17224
|
+
summary: "place tiles in a TileMapLayer or TileMap",
|
|
17225
|
+
requires: ["nodePath", "cells"]
|
|
17226
|
+
}
|
|
17062
17227
|
}
|
|
17063
17228
|
},
|
|
17064
17229
|
{
|
|
@@ -17146,13 +17311,16 @@ var init_tool_definitions = __esm(() => {
|
|
|
17146
17311
|
},
|
|
17147
17312
|
{
|
|
17148
17313
|
name: "script_edit",
|
|
17149
|
-
description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type.
|
|
17314
|
+
description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there. modify makes every addition or none, and answers with the line each one is on in the file written.",
|
|
17150
17315
|
parameters: {
|
|
17151
17316
|
projectPath: PROJECT_PATH,
|
|
17152
17317
|
scriptPath: SCRIPT_PATH,
|
|
17153
|
-
className: { type: "string", description: "create: a class_name for the script." },
|
|
17154
|
-
extends: { type: "string", description: "create: the base class. Default Node." },
|
|
17155
|
-
content: {
|
|
17318
|
+
className: { type: "string", description: "create: a class_name for the script. Not with content." },
|
|
17319
|
+
extends: { type: "string", description: "create: the base class. Default Node. Not with content." },
|
|
17320
|
+
content: {
|
|
17321
|
+
type: "string",
|
|
17322
|
+
description: "create: the whole file, written as given, instead of a template, className and extends."
|
|
17323
|
+
},
|
|
17156
17324
|
template: {
|
|
17157
17325
|
type: "string",
|
|
17158
17326
|
enum: ["singleton", "state_machine", "component", "resource"],
|
|
@@ -17243,13 +17411,13 @@ var init_tool_definitions = __esm(() => {
|
|
|
17243
17411
|
},
|
|
17244
17412
|
{
|
|
17245
17413
|
name: "editor_launch",
|
|
17246
|
-
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.",
|
|
17247
17415
|
parameters: {
|
|
17248
17416
|
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
17249
17417
|
hidden: {
|
|
17250
17418
|
type: "boolean",
|
|
17251
17419
|
ops: ["open", "restart"],
|
|
17252
|
-
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."
|
|
17253
17421
|
}
|
|
17254
17422
|
},
|
|
17255
17423
|
requires: [],
|
|
@@ -17337,7 +17505,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17337
17505
|
requires: ["projectPath"]
|
|
17338
17506
|
},
|
|
17339
17507
|
stop: {
|
|
17340
|
-
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",
|
|
17341
17509
|
requires: []
|
|
17342
17510
|
},
|
|
17343
17511
|
check: {
|
|
@@ -17350,7 +17518,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17350
17518
|
},
|
|
17351
17519
|
{
|
|
17352
17520
|
name: "editor_output",
|
|
17353
|
-
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
|
|
17521
|
+
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
|
|
17354
17522
|
parameters: {
|
|
17355
17523
|
severity: {
|
|
17356
17524
|
type: "string",
|
|
@@ -17398,7 +17566,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17398
17566
|
},
|
|
17399
17567
|
{
|
|
17400
17568
|
name: "editor_status",
|
|
17401
|
-
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.",
|
|
17402
17570
|
parameters: {},
|
|
17403
17571
|
requires: []
|
|
17404
17572
|
},
|
|
@@ -17414,7 +17582,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17414
17582
|
},
|
|
17415
17583
|
reloadScript: {
|
|
17416
17584
|
type: "string",
|
|
17417
|
-
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching, and what it held before under heldBeforeReload. Its constants come back the same way, under heldConstantsBeforeReload and reloadedConstants, with each enum value as Kind.SHORT: an enum member is held as a constant and never shows among the methods. A script that does not compile is not reloaded: reloadProblem gives Godot's error, says whether the copy the editor holds survived, and names the classes the file uses, whose held copies being behind is what failed one before."
|
|
17585
|
+
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching, and what it held before under heldBeforeReload. Its constants come back the same way, under heldConstantsBeforeReload and reloadedConstants, with each enum value as Kind.SHORT: an enum member is held as a constant and never shows among the methods. A script that does not compile is not reloaded: reloadProblem gives Godot's error, says whether the copy the editor holds survived, and names the classes the file uses, whose held copies being behind is what failed one before. A @tool script is not reloaded either, since it runs inside the editor and reloading one from inside a call can take the editor down: reloadProblem says so, and editor_launch restart recompiles it. A path spelled in another case than the file is reloaded under the spelling on disk, which is the one the editor holds its copy under, and that spelling comes back under reloadedAs."
|
|
17418
17586
|
}
|
|
17419
17587
|
},
|
|
17420
17588
|
requires: ["projectPath"]
|
|
@@ -17437,6 +17605,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17437
17605
|
"physics2d",
|
|
17438
17606
|
"physics3d",
|
|
17439
17607
|
"audio",
|
|
17608
|
+
"visual",
|
|
17440
17609
|
"animation",
|
|
17441
17610
|
"ui"
|
|
17442
17611
|
],
|
|
@@ -17476,7 +17645,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17476
17645
|
property: {
|
|
17477
17646
|
type: "string",
|
|
17478
17647
|
ops: ["property", "find"],
|
|
17479
|
-
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.'
|
|
17480
17649
|
},
|
|
17481
17650
|
depth: {
|
|
17482
17651
|
type: "integer",
|
|
@@ -17515,13 +17684,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
17515
17684
|
limit: {
|
|
17516
17685
|
type: "integer",
|
|
17517
17686
|
minimum: 1,
|
|
17687
|
+
maximum: 5000,
|
|
17518
17688
|
ops: ["find", "text"],
|
|
17519
|
-
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."
|
|
17520
17690
|
},
|
|
17521
17691
|
includeHidden: {
|
|
17522
17692
|
type: "boolean",
|
|
17523
17693
|
ops: ["text", "find"],
|
|
17524
|
-
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."
|
|
17525
17695
|
},
|
|
17526
17696
|
metrics: {
|
|
17527
17697
|
type: "array",
|
|
@@ -17560,7 +17730,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17560
17730
|
},
|
|
17561
17731
|
{
|
|
17562
17732
|
name: "runtime_invoke",
|
|
17563
|
-
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
|
|
17733
|
+
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. Errors and warnings the engine reported while a called method ran come back under errors, the first 20, with errors_not_shown counting the rest. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
|
|
17564
17734
|
parameters: {
|
|
17565
17735
|
projectPath: RUNNING_PROJECT_PATH,
|
|
17566
17736
|
pid: RUNNING_PID,
|
|
@@ -17572,12 +17742,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
17572
17742
|
property: {
|
|
17573
17743
|
type: "string",
|
|
17574
17744
|
ops: ["set"],
|
|
17575
|
-
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, and a write that leaves the property as it was
|
|
17745
|
+
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. A map keyed by numbers is stepped into by the number, "_game:units:3". The answer reads back off the same holder, and a write that leaves the property as it was is refused and says what it still holds: the engine not taking it and a setter keeping the value look the same, so the refusal names neither. A write of the value the property already holds, at its own precision, as a float given in 64 bits to a property keeping 32, is not refused: it answers unchanged: true.'
|
|
17576
17746
|
},
|
|
17577
17747
|
value: {
|
|
17578
17748
|
ops: ["set"],
|
|
17579
17749
|
blank: true,
|
|
17580
|
-
description: `set: the value, fitted to the property's type. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key.`
|
|
17750
|
+
description: `set: the value, fitted to the property's type. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
|
|
17581
17751
|
},
|
|
17582
17752
|
method: {
|
|
17583
17753
|
type: "string",
|
|
@@ -17587,7 +17757,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17587
17757
|
args: {
|
|
17588
17758
|
type: "array",
|
|
17589
17759
|
ops: ["call"],
|
|
17590
|
-
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths.`
|
|
17760
|
+
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths.`
|
|
17591
17761
|
},
|
|
17592
17762
|
timeoutMs: {
|
|
17593
17763
|
type: "integer",
|
|
@@ -17620,7 +17790,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17620
17790
|
},
|
|
17621
17791
|
{
|
|
17622
17792
|
name: "runtime_capture",
|
|
17623
|
-
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.",
|
|
17624
17794
|
parameters: {
|
|
17625
17795
|
projectPath: RUNNING_PROJECT_PATH,
|
|
17626
17796
|
pid: RUNNING_PID,
|
|
@@ -17665,7 +17835,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17665
17835
|
says: {
|
|
17666
17836
|
type: "string",
|
|
17667
17837
|
ops: ["click"],
|
|
17668
|
-
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it, is not on screen. The matches are ranked the way a person picks what to press:
|
|
17838
|
+
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one."
|
|
17669
17839
|
},
|
|
17670
17840
|
action: {
|
|
17671
17841
|
type: "string",
|
|
@@ -17698,7 +17868,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17698
17868
|
replace: {
|
|
17699
17869
|
type: "boolean",
|
|
17700
17870
|
ops: ["text"],
|
|
17701
|
-
description: 'text: true writes over what the field already says, which is what filling one in means. Default false types at the caret, so a field reading 2.1 typed "0.3" at reads 2.10.3. The answer says what the field holds afterwards either way.'
|
|
17871
|
+
description: 'text: true writes over what the field already says, which is what filling one in means. Default false types at the caret, so a field reading 2.1 typed "0.3" at reads 2.10.3. The answer says what the field holds afterwards either way. A read-only field is refused.'
|
|
17702
17872
|
},
|
|
17703
17873
|
index: {
|
|
17704
17874
|
type: "number",
|
|
@@ -17742,11 +17912,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
17742
17912
|
requires: [],
|
|
17743
17913
|
operations: {
|
|
17744
17914
|
click: {
|
|
17745
|
-
summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the
|
|
17915
|
+
summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room",
|
|
17746
17916
|
requires: []
|
|
17747
17917
|
},
|
|
17748
17918
|
choose: {
|
|
17749
|
-
summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the item takes the focus and
|
|
17919
|
+
summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the menu is opened, the item is looked for in it as it stands then, since a game can fill a menu as it opens, and the item takes the focus and ui_accept presses it, which is the engine's own path, needs no window, and does not depend on what ui_accept is bound to. A disabled or hidden button in front of the menu is refused, and so is a press the menu did not take, which is heard rather than assumed. Answers with what was chosen, read before the press, what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick), and what the button in front of it shows now when it is still there",
|
|
17750
17920
|
requires: ["nodePath"]
|
|
17751
17921
|
},
|
|
17752
17922
|
action: { summary: "press an action, or hold it", requires: ["action"] },
|
|
@@ -17755,7 +17925,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17755
17925
|
requires: ["keycode"]
|
|
17756
17926
|
},
|
|
17757
17927
|
text: {
|
|
17758
|
-
summary: "type a string into the field being edited, a character at a time, and say what it landed in and what it holds afterwards, a secret field as its mask",
|
|
17928
|
+
summary: "type a string into the field being edited, a character at a time, and say what it landed in and what it holds afterwards, a secret field as its mask. The field is the focus of the dialog that has the focus when one does, which is where the keys go",
|
|
17759
17929
|
requires: ["text"]
|
|
17760
17930
|
},
|
|
17761
17931
|
mouse_click: {
|
|
@@ -17794,7 +17964,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17794
17964
|
},
|
|
17795
17965
|
value: {
|
|
17796
17966
|
ops: ["until"],
|
|
17797
|
-
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."
|
|
17798
17968
|
},
|
|
17799
17969
|
says: {
|
|
17800
17970
|
type: "string",
|
|
@@ -17843,7 +18013,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17843
18013
|
},
|
|
17844
18014
|
{
|
|
17845
18015
|
name: "debug_control",
|
|
17846
|
-
description: "Continues or steps the debugged game through the editor's debug adapter
|
|
18016
|
+
description: "Continues or steps the debugged game through the editor's debug adapter. A step waits for the game to stop again and answers with the stack where it ended up; one that has not stopped the game again within ten seconds answers heldAgain false and no stack. There is no pause and no step_out: Godot's adapter answers a pause by reporting the game stopped and leaving it running, and implements no stepOut at all, so hold the game where you want it with a breakpoint and step over or into from there.",
|
|
17847
18017
|
parameters: {},
|
|
17848
18018
|
requires: [],
|
|
17849
18019
|
operations: {
|
|
@@ -17854,12 +18024,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
17854
18024
|
},
|
|
17855
18025
|
{
|
|
17856
18026
|
name: "debug_state",
|
|
17857
|
-
description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the debug adapter's console output so far. A stop is reported by the editor's adapter only to the sessions connected when it happened, so a server that connected afterwards, which is every server after a reconnect, is shown a thread and no frames about a game still sitting at its breakpoint. That server asks the runtime instead: a held game accepts a connection and never answers, and the refusal then says held rather than running, and that the stack cannot be read from here. debug_control continue lets it go, and the next stop is one this session is told about, with its stack.",
|
|
18027
|
+
description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the debug adapter's console output so far, its last thousand lines, with any earlier ones counted under notShown. A stop is reported by the editor's adapter only to the sessions connected when it happened, so a server that connected afterwards, which is every server after a reconnect, is shown a thread and no frames about a game still sitting at its breakpoint. That server asks the runtime instead: a held game accepts a connection and never answers, and the refusal then says held rather than running, and that the stack cannot be read from here. debug_control continue lets it go, and the next stop is one this session is told about, with its stack.",
|
|
17858
18028
|
parameters: {
|
|
17859
18029
|
frameId: {
|
|
17860
18030
|
type: "number",
|
|
17861
18031
|
ops: ["variables"],
|
|
17862
|
-
description: "variables: which frame, from a stack answer. Default the innermost."
|
|
18032
|
+
description: "variables: which frame, from a stack answer. Default the innermost. A frame the stack no longer holds is refused, and a scope whose values did not arrive in time answers variables null and is named under valuesMissing."
|
|
17863
18033
|
}
|
|
17864
18034
|
},
|
|
17865
18035
|
requires: [],
|
|
@@ -32626,10 +32796,10 @@ var init_stdio2 = __esm(() => {
|
|
|
32626
32796
|
});
|
|
32627
32797
|
|
|
32628
32798
|
// src/runtime-client.ts
|
|
32629
|
-
import { existsSync as
|
|
32799
|
+
import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync11, statSync as statSync5, unlinkSync } from "node:fs";
|
|
32630
32800
|
import { createConnection } from "node:net";
|
|
32631
32801
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
32632
|
-
import { dirname as
|
|
32802
|
+
import { dirname as dirname8, join as join13, resolve as resolve3 } from "node:path";
|
|
32633
32803
|
import { setTimeout as delay } from "node:timers/promises";
|
|
32634
32804
|
function runtimeDirectory(variables = process.env) {
|
|
32635
32805
|
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
@@ -32650,7 +32820,7 @@ function runtimeDirectories(variables = process.env) {
|
|
|
32650
32820
|
function errorReportOf(pid, directories = runtimeDirectories()) {
|
|
32651
32821
|
for (const directory of directories) {
|
|
32652
32822
|
const path = join13(directory, `runtime-${pid}.log`);
|
|
32653
|
-
if (
|
|
32823
|
+
if (existsSync12(path)) {
|
|
32654
32824
|
return path;
|
|
32655
32825
|
}
|
|
32656
32826
|
}
|
|
@@ -32681,7 +32851,7 @@ function reportAlreadyInTranscript(transcript, report) {
|
|
|
32681
32851
|
function parseAnnouncement(file, pid) {
|
|
32682
32852
|
let fields;
|
|
32683
32853
|
try {
|
|
32684
|
-
fields = asParams(JSON.parse(
|
|
32854
|
+
fields = asParams(JSON.parse(readFileSync11(file, "utf8")));
|
|
32685
32855
|
} catch {
|
|
32686
32856
|
return { kind: "rubbish" };
|
|
32687
32857
|
}
|
|
@@ -32718,6 +32888,12 @@ function parseAnnouncement(file, pid) {
|
|
|
32718
32888
|
}
|
|
32719
32889
|
};
|
|
32720
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
|
+
}
|
|
32721
32897
|
function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
32722
32898
|
const running = new Map;
|
|
32723
32899
|
const unspoken = new Map;
|
|
@@ -32739,7 +32915,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
|
|
|
32739
32915
|
function wentAway(file, pid) {
|
|
32740
32916
|
let project = "";
|
|
32741
32917
|
try {
|
|
32742
|
-
const fields = asParams(JSON.parse(
|
|
32918
|
+
const fields = asParams(JSON.parse(readFileSync11(file, "utf8")));
|
|
32743
32919
|
project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
|
|
32744
32920
|
} catch {
|
|
32745
32921
|
return;
|
|
@@ -32782,7 +32958,7 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
|
32782
32958
|
}).map((one) => one.file));
|
|
32783
32959
|
}
|
|
32784
32960
|
function announcedIn(directory) {
|
|
32785
|
-
if (!
|
|
32961
|
+
if (!existsSync12(directory)) {
|
|
32786
32962
|
return [];
|
|
32787
32963
|
}
|
|
32788
32964
|
const found = [];
|
|
@@ -32828,14 +33004,14 @@ function announcedIn(directory) {
|
|
|
32828
33004
|
const held = new Set(entries.map((one) => one.file));
|
|
32829
33005
|
const here = join13(directory, ".");
|
|
32830
33006
|
for (const file of verdicts.keys()) {
|
|
32831
|
-
if (
|
|
33007
|
+
if (dirname8(file) === here && !held.has(file)) {
|
|
32832
33008
|
verdicts.delete(file);
|
|
32833
33009
|
}
|
|
32834
33010
|
}
|
|
32835
33011
|
return found;
|
|
32836
33012
|
}
|
|
32837
33013
|
async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
32838
|
-
const files = directories.map((directory) => join13(directory, `runtime-${pid}.json`)).filter((file) =>
|
|
33014
|
+
const files = directories.map((directory) => join13(directory, `runtime-${pid}.json`)).filter((file) => existsSync12(file));
|
|
32839
33015
|
const first = files[0];
|
|
32840
33016
|
if (first === undefined) {
|
|
32841
33017
|
return;
|
|
@@ -32870,13 +33046,13 @@ async function announcedSince(projectPath, before, waiting = {}) {
|
|
|
32870
33046
|
const directories = waiting.directories ?? runtimeDirectories();
|
|
32871
33047
|
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
32872
33048
|
for (;; ) {
|
|
32873
|
-
const fresh = discoverRuntimes(directories).
|
|
32874
|
-
if (fresh
|
|
33049
|
+
const fresh = discoverRuntimes(directories).filter((endpoint) => !before.has(endpoint.pid) && isSameDirectory(endpoint.project.path, projectPath) && (waiting.accept?.(endpoint) ?? true));
|
|
33050
|
+
if (fresh.length > 0) {
|
|
32875
33051
|
return fresh;
|
|
32876
33052
|
}
|
|
32877
33053
|
const left = until - Date.now();
|
|
32878
33054
|
if (left <= 0 || waiting.giveUp?.() === true) {
|
|
32879
|
-
return
|
|
33055
|
+
return [];
|
|
32880
33056
|
}
|
|
32881
33057
|
await delay(Math.min(LOOK_EVERY_MS, left));
|
|
32882
33058
|
}
|
|
@@ -33007,7 +33183,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
|
|
|
33007
33183
|
let late = false;
|
|
33008
33184
|
let buffered = "";
|
|
33009
33185
|
let welcomed = false;
|
|
33010
|
-
const socket = createConnection({ port: endpoint.port, host: endpoint.address });
|
|
33186
|
+
const socket = createConnection({ port: endpoint.port, host: dialAddress(endpoint.address) });
|
|
33011
33187
|
let lateTimer = null;
|
|
33012
33188
|
const finish = (reply) => {
|
|
33013
33189
|
if (late) {
|
|
@@ -33135,14 +33311,14 @@ var init_runtime_client = __esm(() => {
|
|
|
33135
33311
|
});
|
|
33136
33312
|
|
|
33137
33313
|
// src/boot-note.ts
|
|
33138
|
-
import { mkdirSync as mkdirSync6, readFileSync as
|
|
33139
|
-
import { dirname as
|
|
33314
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
|
|
33315
|
+
import { dirname as dirname9, join as join14 } from "node:path";
|
|
33140
33316
|
function bootNotePath(projectPath) {
|
|
33141
33317
|
return join14(projectPath, ".godot", "gdharness-boot.json");
|
|
33142
33318
|
}
|
|
33143
33319
|
function readBootNote(projectPath) {
|
|
33144
33320
|
try {
|
|
33145
|
-
const noted = readNumber(asParams(JSON.parse(
|
|
33321
|
+
const noted = readNumber(asParams(JSON.parse(readFileSync12(bootNotePath(projectPath), "utf8"))), "announcedAfterMs");
|
|
33146
33322
|
return noted !== undefined && Number.isFinite(noted) && noted > 0 ? noted : null;
|
|
33147
33323
|
} catch {
|
|
33148
33324
|
return null;
|
|
@@ -33151,7 +33327,7 @@ function readBootNote(projectPath) {
|
|
|
33151
33327
|
function writeBootNote(projectPath, announcedAfterMs) {
|
|
33152
33328
|
try {
|
|
33153
33329
|
const path = bootNotePath(projectPath);
|
|
33154
|
-
mkdirSync6(
|
|
33330
|
+
mkdirSync6(dirname9(path), { recursive: true });
|
|
33155
33331
|
writeFileSync8(path, JSON.stringify({ announcedAfterMs: Math.round(announcedAfterMs), at: new Date().toISOString() }, null, 2), "utf8");
|
|
33156
33332
|
} catch {}
|
|
33157
33333
|
}
|
|
@@ -33170,8 +33346,8 @@ var init_boot_note = __esm(() => {
|
|
|
33170
33346
|
});
|
|
33171
33347
|
|
|
33172
33348
|
// src/breakpoint-note.ts
|
|
33173
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
33174
|
-
import { dirname as
|
|
33349
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
33350
|
+
import { dirname as dirname10, isAbsolute as isAbsolute2, join as join15, relative as relative2 } from "node:path";
|
|
33175
33351
|
function breakpointNotePath(projectPath) {
|
|
33176
33352
|
return join15(projectPath, ".godot", "gdharness-breakpoints.json");
|
|
33177
33353
|
}
|
|
@@ -33190,7 +33366,7 @@ function writeBreakpointNote(projectPath, held) {
|
|
|
33190
33366
|
lines: [...one.lines]
|
|
33191
33367
|
}))
|
|
33192
33368
|
};
|
|
33193
|
-
mkdirSync7(
|
|
33369
|
+
mkdirSync7(dirname10(path), { recursive: true });
|
|
33194
33370
|
writeFileSync9(path, `${JSON.stringify(note, null, 2)}
|
|
33195
33371
|
`, "utf8");
|
|
33196
33372
|
} catch (error) {
|
|
@@ -33199,7 +33375,7 @@ function writeBreakpointNote(projectPath, held) {
|
|
|
33199
33375
|
}
|
|
33200
33376
|
function readBreakpointNote(projectPath) {
|
|
33201
33377
|
try {
|
|
33202
|
-
const parsed = JSON.parse(
|
|
33378
|
+
const parsed = JSON.parse(readFileSync13(breakpointNotePath(projectPath), "utf8"));
|
|
33203
33379
|
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
|
|
33204
33380
|
return [];
|
|
33205
33381
|
}
|
|
@@ -33232,14 +33408,14 @@ var init_breakpoint_note = __esm(() => {
|
|
|
33232
33408
|
});
|
|
33233
33409
|
|
|
33234
33410
|
// src/class-note.ts
|
|
33235
|
-
import { mkdirSync as mkdirSync8, readFileSync as
|
|
33236
|
-
import { dirname as
|
|
33411
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "node:fs";
|
|
33412
|
+
import { dirname as dirname11, join as join16 } from "node:path";
|
|
33237
33413
|
function classNotePath(projectPath) {
|
|
33238
33414
|
return join16(projectPath, ".godot", "gdharness-classes.json");
|
|
33239
33415
|
}
|
|
33240
33416
|
function readClassNote(projectPath) {
|
|
33241
33417
|
try {
|
|
33242
|
-
const fields = asParams(JSON.parse(
|
|
33418
|
+
const fields = asParams(JSON.parse(readFileSync14(classNotePath(projectPath), "utf8")));
|
|
33243
33419
|
const rebuiltWith = readArray(fields, "rebuiltWith");
|
|
33244
33420
|
if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
|
|
33245
33421
|
return null;
|
|
@@ -33256,7 +33432,7 @@ function readClassNote(projectPath) {
|
|
|
33256
33432
|
function writeClassNote(projectPath, note) {
|
|
33257
33433
|
try {
|
|
33258
33434
|
const path = classNotePath(projectPath);
|
|
33259
|
-
mkdirSync8(
|
|
33435
|
+
mkdirSync8(dirname11(path), { recursive: true });
|
|
33260
33436
|
writeFileSync10(path, `${JSON.stringify(note, null, 2)}
|
|
33261
33437
|
`, "utf8");
|
|
33262
33438
|
} catch {}
|
|
@@ -33264,16 +33440,16 @@ function writeClassNote(projectPath, note) {
|
|
|
33264
33440
|
var init_class_note = () => {};
|
|
33265
33441
|
|
|
33266
33442
|
// src/config-pin.ts
|
|
33267
|
-
import { existsSync as
|
|
33443
|
+
import { existsSync as existsSync13, readFileSync as readFileSync15 } from "node:fs";
|
|
33268
33444
|
import { join as join17 } from "node:path";
|
|
33269
33445
|
function versionsNamedInConfig(projectPath) {
|
|
33270
33446
|
const config = join17(projectPath, ".mcp.json");
|
|
33271
|
-
if (!
|
|
33447
|
+
if (!existsSync13(config)) {
|
|
33272
33448
|
return [];
|
|
33273
33449
|
}
|
|
33274
33450
|
let text;
|
|
33275
33451
|
try {
|
|
33276
|
-
text =
|
|
33452
|
+
text = readFileSync15(config, "utf8");
|
|
33277
33453
|
JSON.parse(text);
|
|
33278
33454
|
} catch {
|
|
33279
33455
|
return [];
|
|
@@ -33422,6 +33598,9 @@ var init_ports = __esm(() => {
|
|
|
33422
33598
|
// src/dap_client.ts
|
|
33423
33599
|
import { createConnection as createConnection2 } from "node:net";
|
|
33424
33600
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
33601
|
+
function fileKey(path) {
|
|
33602
|
+
return process.platform === "win32" ? path.replaceAll("\\", "/").toLowerCase() : path;
|
|
33603
|
+
}
|
|
33425
33604
|
function said(body, field) {
|
|
33426
33605
|
const value = body?.[field];
|
|
33427
33606
|
return typeof value === "string" ? value : "";
|
|
@@ -33438,17 +33617,25 @@ class GodotDAPClient {
|
|
|
33438
33617
|
outputBuffer = [];
|
|
33439
33618
|
onOutput = null;
|
|
33440
33619
|
maxOutputLines = 1000;
|
|
33620
|
+
outputDropped = 0;
|
|
33621
|
+
outputTaken = 0;
|
|
33441
33622
|
initialized = false;
|
|
33442
33623
|
attached = false;
|
|
33443
33624
|
lastThreadId = 1;
|
|
33444
33625
|
halt = null;
|
|
33445
33626
|
holdKnown = false;
|
|
33627
|
+
stops = 0;
|
|
33446
33628
|
breakpoints = new Map;
|
|
33629
|
+
spelled = new Map;
|
|
33447
33630
|
inEditor = new Map;
|
|
33448
33631
|
onBreakpointsChanged = null;
|
|
33449
|
-
|
|
33632
|
+
scopesPatienceMs;
|
|
33633
|
+
stepPatienceMs;
|
|
33634
|
+
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1", patience = {}) {
|
|
33450
33635
|
this.port = port;
|
|
33451
33636
|
this.host = host;
|
|
33637
|
+
this.scopesPatienceMs = patience.scopesMs ?? SCOPES_TIMEOUT_MS;
|
|
33638
|
+
this.stepPatienceMs = patience.stepMs ?? STEP_LANDS_WITHIN_MS;
|
|
33452
33639
|
this.pendingRequests = new Map;
|
|
33453
33640
|
}
|
|
33454
33641
|
async connect() {
|
|
@@ -33609,6 +33796,7 @@ class GodotDAPClient {
|
|
|
33609
33796
|
}
|
|
33610
33797
|
}
|
|
33611
33798
|
if (this.outputBuffer.length > this.maxOutputLines) {
|
|
33799
|
+
this.outputDropped += this.outputBuffer.length - this.maxOutputLines;
|
|
33612
33800
|
this.outputBuffer = this.outputBuffer.slice(this.outputBuffer.length - this.maxOutputLines);
|
|
33613
33801
|
}
|
|
33614
33802
|
return;
|
|
@@ -33625,6 +33813,7 @@ class GodotDAPClient {
|
|
|
33625
33813
|
text: said(body, "text")
|
|
33626
33814
|
};
|
|
33627
33815
|
this.holdKnown = true;
|
|
33816
|
+
this.stops += 1;
|
|
33628
33817
|
return;
|
|
33629
33818
|
}
|
|
33630
33819
|
if (eventName === "continued") {
|
|
@@ -33641,20 +33830,24 @@ class GodotDAPClient {
|
|
|
33641
33830
|
if (typeof path !== "string" || typeof line !== "number") {
|
|
33642
33831
|
return;
|
|
33643
33832
|
}
|
|
33833
|
+
const key = fileKey(path);
|
|
33834
|
+
if (!this.spelled.has(key)) {
|
|
33835
|
+
this.spelled.set(key, path);
|
|
33836
|
+
}
|
|
33644
33837
|
if (said(body, "reason") === "removed") {
|
|
33645
|
-
this.inEditor.get(
|
|
33646
|
-
const held = this.breakpoints.get(
|
|
33838
|
+
this.inEditor.get(key)?.delete(line);
|
|
33839
|
+
const held = this.breakpoints.get(key);
|
|
33647
33840
|
if (held?.delete(line) === true) {
|
|
33648
33841
|
if (held.size === 0) {
|
|
33649
|
-
this.breakpoints.delete(
|
|
33842
|
+
this.breakpoints.delete(key);
|
|
33650
33843
|
}
|
|
33651
33844
|
this.onBreakpointsChanged?.(this.breakpointsHeld());
|
|
33652
33845
|
}
|
|
33653
33846
|
return;
|
|
33654
33847
|
}
|
|
33655
|
-
const known = this.inEditor.get(
|
|
33848
|
+
const known = this.inEditor.get(key) ?? new Set;
|
|
33656
33849
|
known.add(line);
|
|
33657
|
-
this.inEditor.set(
|
|
33850
|
+
this.inEditor.set(key, known);
|
|
33658
33851
|
return;
|
|
33659
33852
|
}
|
|
33660
33853
|
if (eventName === "terminated" || eventName === "exited") {
|
|
@@ -33737,24 +33930,44 @@ class GodotDAPClient {
|
|
|
33737
33930
|
getOutput(clear = false) {
|
|
33738
33931
|
const lines = [...this.outputBuffer];
|
|
33739
33932
|
if (clear) {
|
|
33933
|
+
this.outputTaken += lines.length;
|
|
33740
33934
|
this.outputBuffer = [];
|
|
33741
33935
|
}
|
|
33742
33936
|
return lines;
|
|
33743
33937
|
}
|
|
33938
|
+
outputSoFar() {
|
|
33939
|
+
const output = this.getOutput(false);
|
|
33940
|
+
const notShown = this.outputDropped + this.outputTaken;
|
|
33941
|
+
if (notShown === 0) {
|
|
33942
|
+
return { lines: output.length, output };
|
|
33943
|
+
}
|
|
33944
|
+
const where = [
|
|
33945
|
+
this.outputDropped > 0 ? `${this.outputDropped} fell out of this buffer, which keeps the last ${this.maxOutputLines}` : "",
|
|
33946
|
+
this.outputTaken > 0 ? `${this.outputTaken} were moved into the run's log by an earlier read` : ""
|
|
33947
|
+
].filter((part) => part !== "");
|
|
33948
|
+
return {
|
|
33949
|
+
lines: output.length,
|
|
33950
|
+
output,
|
|
33951
|
+
notShown,
|
|
33952
|
+
note: `${notShown} earlier ${notShown === 1 ? "line is" : "lines are"} not in this answer: ${where.join(", and ")}. editor_output reads what the server kept of the run's console.`
|
|
33953
|
+
};
|
|
33954
|
+
}
|
|
33744
33955
|
async setBreakpoint(filePath, line) {
|
|
33745
|
-
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
33956
|
+
const wanted = new Set(this.breakpoints.get(fileKey(filePath)) ?? []);
|
|
33746
33957
|
wanted.add(line);
|
|
33747
33958
|
return await this.sendBreakpoints(filePath, wanted);
|
|
33748
33959
|
}
|
|
33749
33960
|
async removeBreakpoint(filePath, line) {
|
|
33750
|
-
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
33961
|
+
const wanted = new Set(this.breakpoints.get(fileKey(filePath)) ?? []);
|
|
33751
33962
|
wanted.delete(line);
|
|
33752
33963
|
return await this.sendBreakpoints(filePath, wanted, line);
|
|
33753
33964
|
}
|
|
33754
33965
|
async sendBreakpoints(filePath, mine, clearing) {
|
|
33755
33966
|
await this.ensureConnected();
|
|
33756
33967
|
await this.initialize();
|
|
33757
|
-
const
|
|
33968
|
+
const key = fileKey(filePath);
|
|
33969
|
+
this.spelled.set(key, filePath);
|
|
33970
|
+
const lines = new Set([...mine, ...this.inEditor.get(key) ?? []]);
|
|
33758
33971
|
if (clearing !== undefined) {
|
|
33759
33972
|
lines.delete(clearing);
|
|
33760
33973
|
}
|
|
@@ -33764,14 +33977,14 @@ class GodotDAPClient {
|
|
|
33764
33977
|
breakpoints: sorted.map((breakpointLine) => ({ line: breakpointLine }))
|
|
33765
33978
|
});
|
|
33766
33979
|
if (mine.size === 0) {
|
|
33767
|
-
this.breakpoints.delete(
|
|
33980
|
+
this.breakpoints.delete(key);
|
|
33768
33981
|
} else {
|
|
33769
|
-
this.breakpoints.set(
|
|
33982
|
+
this.breakpoints.set(key, new Set(mine));
|
|
33770
33983
|
}
|
|
33771
33984
|
if (lines.size === 0) {
|
|
33772
|
-
this.inEditor.delete(
|
|
33985
|
+
this.inEditor.delete(key);
|
|
33773
33986
|
} else {
|
|
33774
|
-
this.inEditor.set(
|
|
33987
|
+
this.inEditor.set(key, lines);
|
|
33775
33988
|
}
|
|
33776
33989
|
return answer;
|
|
33777
33990
|
}
|
|
@@ -33780,11 +33993,11 @@ class GodotDAPClient {
|
|
|
33780
33993
|
}
|
|
33781
33994
|
breakpointsInEditor() {
|
|
33782
33995
|
const theirs = [];
|
|
33783
|
-
for (const [
|
|
33784
|
-
const mine = this.breakpoints.get(
|
|
33996
|
+
for (const [key, lines] of this.inEditor) {
|
|
33997
|
+
const mine = this.breakpoints.get(key);
|
|
33785
33998
|
const rest = Array.from(lines).filter((line) => !mine?.has(line)).sort((a, b) => a - b);
|
|
33786
33999
|
if (rest.length > 0) {
|
|
33787
|
-
theirs.push({ scriptPath, lines: rest });
|
|
34000
|
+
theirs.push({ scriptPath: this.spelled.get(key) ?? key, lines: rest });
|
|
33788
34001
|
}
|
|
33789
34002
|
}
|
|
33790
34003
|
return theirs;
|
|
@@ -33792,12 +34005,13 @@ class GodotDAPClient {
|
|
|
33792
34005
|
async reapplyBreakpoints() {
|
|
33793
34006
|
const applied = [];
|
|
33794
34007
|
const refused = [];
|
|
33795
|
-
for (const [
|
|
34008
|
+
for (const [key, lines] of Array.from(this.breakpoints)) {
|
|
34009
|
+
const filePath = this.spelled.get(key) ?? key;
|
|
33796
34010
|
try {
|
|
33797
34011
|
await this.sendBreakpoints(filePath, lines);
|
|
33798
34012
|
applied.push({ scriptPath: filePath, lines: Array.from(lines).sort((a, b) => a - b) });
|
|
33799
34013
|
} catch (error) {
|
|
33800
|
-
this.breakpoints.delete(
|
|
34014
|
+
this.breakpoints.delete(key);
|
|
33801
34015
|
refused.push({
|
|
33802
34016
|
scriptPath: filePath,
|
|
33803
34017
|
reason: error instanceof Error ? error.message : String(error)
|
|
@@ -33810,19 +34024,21 @@ class GodotDAPClient {
|
|
|
33810
34024
|
return { applied, refused };
|
|
33811
34025
|
}
|
|
33812
34026
|
breakpointsHeld() {
|
|
33813
|
-
return Array.from(this.breakpoints, ([
|
|
33814
|
-
scriptPath,
|
|
34027
|
+
return Array.from(this.breakpoints, ([key, lines]) => ({
|
|
34028
|
+
scriptPath: this.spelled.get(key) ?? key,
|
|
33815
34029
|
lines: Array.from(lines).sort((a, b) => a - b)
|
|
33816
34030
|
}));
|
|
33817
34031
|
}
|
|
33818
34032
|
holdBreakpoints(held) {
|
|
33819
34033
|
for (const { scriptPath, lines } of held) {
|
|
33820
|
-
const
|
|
34034
|
+
const key = fileKey(scriptPath);
|
|
34035
|
+
const fileBreakpoints = this.breakpoints.get(key) ?? new Set;
|
|
33821
34036
|
for (const line of lines) {
|
|
33822
34037
|
fileBreakpoints.add(line);
|
|
33823
34038
|
}
|
|
33824
34039
|
if (fileBreakpoints.size > 0) {
|
|
33825
|
-
this.breakpoints.set(
|
|
34040
|
+
this.breakpoints.set(key, fileBreakpoints);
|
|
34041
|
+
this.spelled.set(key, scriptPath);
|
|
33826
34042
|
}
|
|
33827
34043
|
}
|
|
33828
34044
|
}
|
|
@@ -33833,15 +34049,16 @@ class GodotDAPClient {
|
|
|
33833
34049
|
this.halt = null;
|
|
33834
34050
|
this.holdKnown = true;
|
|
33835
34051
|
}
|
|
33836
|
-
async
|
|
33837
|
-
await this.attach();
|
|
33838
|
-
const resolvedThreadId = await this.resolveThreadId(threadId);
|
|
33839
|
-
await this.sendRequest("next", { threadId: resolvedThreadId });
|
|
33840
|
-
}
|
|
33841
|
-
async stepInto(threadId) {
|
|
34052
|
+
async step(command) {
|
|
33842
34053
|
await this.attach();
|
|
33843
|
-
const resolvedThreadId = await this.resolveThreadId(
|
|
33844
|
-
|
|
34054
|
+
const resolvedThreadId = await this.resolveThreadId();
|
|
34055
|
+
const before = this.stops;
|
|
34056
|
+
await this.sendRequest(command, { threadId: resolvedThreadId });
|
|
34057
|
+
const deadline = Date.now() + this.stepPatienceMs;
|
|
34058
|
+
while (this.stops === before && this.connected && Date.now() < deadline) {
|
|
34059
|
+
await delay2(50);
|
|
34060
|
+
}
|
|
34061
|
+
return this.stops !== before;
|
|
33845
34062
|
}
|
|
33846
34063
|
async getStackTrace(threadId) {
|
|
33847
34064
|
await this.attach();
|
|
@@ -33867,16 +34084,16 @@ class GodotDAPClient {
|
|
|
33867
34084
|
} catch {}
|
|
33868
34085
|
await delay2(200);
|
|
33869
34086
|
}
|
|
33870
|
-
return
|
|
34087
|
+
return null;
|
|
33871
34088
|
}
|
|
33872
34089
|
async getScopes(frameId) {
|
|
33873
34090
|
await this.attach();
|
|
33874
34091
|
const frames = await this.getStackTrace();
|
|
33875
34092
|
const frame = frameId === undefined ? frames[0] : frames.find((each) => each["id"] === frameId);
|
|
33876
34093
|
if (frame === undefined) {
|
|
33877
|
-
|
|
34094
|
+
throw new ReadRefusal(frames.length === 0 ? "The adapter gave no stack, so there is no frame to read variables from" : `The stack the adapter gives now has no frame ${String(frameId)}; its frames are ${frames.map((each) => String(each["id"])).join(", ")}. Read the stack again for the frames the game is at now`);
|
|
33878
34095
|
}
|
|
33879
|
-
const deadline = Date.now() +
|
|
34096
|
+
const deadline = Date.now() + this.scopesPatienceMs;
|
|
33880
34097
|
let scopes = [];
|
|
33881
34098
|
while (scopes.length === 0 && Date.now() < deadline) {
|
|
33882
34099
|
try {
|
|
@@ -33888,6 +34105,9 @@ class GodotDAPClient {
|
|
|
33888
34105
|
} catch {}
|
|
33889
34106
|
await delay2(200);
|
|
33890
34107
|
}
|
|
34108
|
+
if (scopes.length === 0) {
|
|
34109
|
+
throw new ReadRefusal(`The scopes of frame ${String(frame["id"])} did not arrive from the game within ${this.scopesPatienceMs / 1000}s. They come from the game through the editor, so a busy machine can take longer: ask again`);
|
|
34110
|
+
}
|
|
33891
34111
|
const named = [];
|
|
33892
34112
|
for (const scope of scopes) {
|
|
33893
34113
|
const reference = scope["variablesReference"];
|
|
@@ -33951,9 +34171,8 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
33951
34171
|
try {
|
|
33952
34172
|
switch (toolName) {
|
|
33953
34173
|
case "dap_get_output": {
|
|
33954
|
-
const output = client.getOutput(false);
|
|
33955
34174
|
return {
|
|
33956
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
34175
|
+
content: [{ type: "text", text: JSON.stringify(client.outputSoFar(), null, 2) }]
|
|
33957
34176
|
};
|
|
33958
34177
|
}
|
|
33959
34178
|
case "dap_set_breakpoint": {
|
|
@@ -33980,19 +34199,13 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
33980
34199
|
}
|
|
33981
34200
|
case "dap_step_over":
|
|
33982
34201
|
case "dap_step_into": {
|
|
33983
|
-
|
|
33984
|
-
|
|
33985
|
-
|
|
33986
|
-
|
|
33987
|
-
|
|
33988
|
-
return {
|
|
33989
|
-
content: [
|
|
33990
|
-
{
|
|
33991
|
-
type: "text",
|
|
33992
|
-
text: JSON.stringify({ stepped: true, stack: await client.getStackTrace() }, null, 2)
|
|
33993
|
-
}
|
|
33994
|
-
]
|
|
34202
|
+
const heldAgain = await client.step(toolName === "dap_step_into" ? "stepIn" : "next");
|
|
34203
|
+
const answer = heldAgain ? { stepped: true, heldAgain, stack: await client.getStackTrace() } : {
|
|
34204
|
+
stepped: true,
|
|
34205
|
+
heldAgain,
|
|
34206
|
+
note: `The game had not stopped again ${client.stepPatienceMs / 1000}s after the step: the step is still running, it went on without stopping anywhere, or the game was not held when it was asked. debug_state stack answers once it stops.`
|
|
33995
34207
|
};
|
|
34208
|
+
return { content: [{ type: "text", text: JSON.stringify(answer, null, 2) }] };
|
|
33996
34209
|
}
|
|
33997
34210
|
case "dap_get_stack_trace": {
|
|
33998
34211
|
const stack = await client.getStackTrace();
|
|
@@ -34003,8 +34216,14 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
34003
34216
|
case "dap_get_variables": {
|
|
34004
34217
|
const frameId = typeof safeArgs.frameId === "number" ? safeArgs.frameId : undefined;
|
|
34005
34218
|
const scopes = await client.getScopes(frameId);
|
|
34219
|
+
const missing = scopes.filter((scope) => scope.variables === null).map((scope) => scope.name);
|
|
34220
|
+
const answer = missing.length === 0 ? { scopes } : {
|
|
34221
|
+
scopes,
|
|
34222
|
+
valuesMissing: missing,
|
|
34223
|
+
note: `The values of ${missing.join(", ")} did not arrive from the game within ${client.scopesPatienceMs / 1000}s, so they are unknown rather than empty. Ask again.`
|
|
34224
|
+
};
|
|
34006
34225
|
return {
|
|
34007
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
34226
|
+
content: [{ type: "text", text: JSON.stringify(answer, null, 2) }]
|
|
34008
34227
|
};
|
|
34009
34228
|
}
|
|
34010
34229
|
default:
|
|
@@ -34012,18 +34231,22 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
34012
34231
|
}
|
|
34013
34232
|
} catch (error) {
|
|
34014
34233
|
const message = error instanceof Error ? error.message : String(error);
|
|
34015
|
-
const cure = toolName === "dap_set_breakpoint" || toolName === "dap_remove_breakpoint" ? "Breakpoints need the editor open, not a running game: editor_status says whether one is connected, and the path is the script as the project spells it." : "The debug tools answer for a game the editor is playing: start one with editor_run.";
|
|
34234
|
+
const cure = error instanceof ReadRefusal ? "" : toolName === "dap_set_breakpoint" || toolName === "dap_remove_breakpoint" ? "Breakpoints need the editor open, not a running game: editor_status says whether one is connected, and the path is the script as the project spells it." : "The debug tools answer for a game the editor is playing: start one with editor_run.";
|
|
34016
34235
|
return {
|
|
34017
34236
|
isError: true,
|
|
34018
|
-
content: [
|
|
34237
|
+
content: [
|
|
34238
|
+
{ type: "text", text: `DAP tool '${toolName}' failed: ${message}.${cure === "" ? "" : ` ${cure}`}` }
|
|
34239
|
+
]
|
|
34019
34240
|
};
|
|
34020
34241
|
}
|
|
34021
34242
|
}
|
|
34022
|
-
var DEFAULT_DAP_PORT = 6006, DAP_REQUEST_TIMEOUT_MS = 1e4, SCOPES_TIMEOUT_MS = 20000, VARIABLES_POLL_TIMEOUT_MS = 2000;
|
|
34243
|
+
var DEFAULT_DAP_PORT = 6006, DAP_REQUEST_TIMEOUT_MS = 1e4, SCOPES_TIMEOUT_MS = 20000, STEP_LANDS_WITHIN_MS = 1e4, VARIABLES_POLL_TIMEOUT_MS = 2000, ReadRefusal;
|
|
34023
34244
|
var init_dap_client = __esm(() => {
|
|
34024
34245
|
init_errors();
|
|
34025
34246
|
init_framing();
|
|
34026
34247
|
init_ports();
|
|
34248
|
+
ReadRefusal = class ReadRefusal extends Refusal {
|
|
34249
|
+
};
|
|
34027
34250
|
});
|
|
34028
34251
|
|
|
34029
34252
|
// src/desktop.ts
|
|
@@ -34033,16 +34256,16 @@ var init_desktop = () => {};
|
|
|
34033
34256
|
// src/editor-log.ts
|
|
34034
34257
|
import {
|
|
34035
34258
|
closeSync,
|
|
34036
|
-
existsSync as
|
|
34259
|
+
existsSync as existsSync14,
|
|
34037
34260
|
mkdirSync as mkdirSync9,
|
|
34038
34261
|
openSync,
|
|
34039
|
-
readFileSync as
|
|
34262
|
+
readFileSync as readFileSync16,
|
|
34040
34263
|
readSync,
|
|
34041
34264
|
rmSync as rmSync8,
|
|
34042
34265
|
statSync as statSync6,
|
|
34043
34266
|
writeFileSync as writeFileSync11
|
|
34044
34267
|
} from "node:fs";
|
|
34045
|
-
import { dirname as
|
|
34268
|
+
import { dirname as dirname12, join as join18 } from "node:path";
|
|
34046
34269
|
function editorLogPath(projectPath) {
|
|
34047
34270
|
return join18(projectPath, ".godot", "gdharness-editor.log");
|
|
34048
34271
|
}
|
|
@@ -34051,7 +34274,7 @@ function editorLogNotePath(projectPath) {
|
|
|
34051
34274
|
}
|
|
34052
34275
|
function readEditorLogNote(projectPath) {
|
|
34053
34276
|
try {
|
|
34054
|
-
const fields = asParams(JSON.parse(
|
|
34277
|
+
const fields = asParams(JSON.parse(readFileSync16(editorLogNotePath(projectPath), "utf8")));
|
|
34055
34278
|
const pid = readNumber(fields, "pid");
|
|
34056
34279
|
const startedAt = readString(fields, "startedAt");
|
|
34057
34280
|
return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
|
|
@@ -34062,7 +34285,7 @@ function readEditorLogNote(projectPath) {
|
|
|
34062
34285
|
function clearEditorLog(projectPath) {
|
|
34063
34286
|
try {
|
|
34064
34287
|
const path = editorLogPath(projectPath);
|
|
34065
|
-
mkdirSync9(
|
|
34288
|
+
mkdirSync9(dirname12(path), { recursive: true });
|
|
34066
34289
|
rmSync8(path, { force: true });
|
|
34067
34290
|
rmSync8(editorLogNotePath(projectPath), { force: true });
|
|
34068
34291
|
} catch {}
|
|
@@ -34070,7 +34293,7 @@ function clearEditorLog(projectPath) {
|
|
|
34070
34293
|
function writeEditorLogNote(projectPath, pid) {
|
|
34071
34294
|
try {
|
|
34072
34295
|
const path = editorLogNotePath(projectPath);
|
|
34073
|
-
mkdirSync9(
|
|
34296
|
+
mkdirSync9(dirname12(path), { recursive: true });
|
|
34074
34297
|
writeFileSync11(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
|
|
34075
34298
|
} catch {}
|
|
34076
34299
|
}
|
|
@@ -34092,7 +34315,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
|
34092
34315
|
return { kind: "older server", notedPid: noted?.pid ?? null };
|
|
34093
34316
|
}
|
|
34094
34317
|
const path = editorLogPath(projectPath);
|
|
34095
|
-
if (!
|
|
34318
|
+
if (!existsSync14(path)) {
|
|
34096
34319
|
return { kind: "not written" };
|
|
34097
34320
|
}
|
|
34098
34321
|
const log = new GameLog;
|
|
@@ -34104,7 +34327,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
|
34104
34327
|
function theFirstOf(path) {
|
|
34105
34328
|
const whole = statSync6(path).size;
|
|
34106
34329
|
if (whole <= MOST_CONSOLE_BYTES) {
|
|
34107
|
-
return { text:
|
|
34330
|
+
return { text: readFileSync16(path, "utf8") };
|
|
34108
34331
|
}
|
|
34109
34332
|
const handle = openSync(path, "r");
|
|
34110
34333
|
try {
|
|
@@ -37173,7 +37396,10 @@ function mayYetConnect(listeningSince, now = Date.now()) {
|
|
|
37173
37396
|
return listeningSince === undefined || now - listeningSince.getTime() < CONNECT_WINDOW_MS;
|
|
37174
37397
|
}
|
|
37175
37398
|
function theEditorHasComeBack(status, startedAt) {
|
|
37176
|
-
return status
|
|
37399
|
+
return hasSaidWhoItIs(status) && (status.connectedAt?.getTime() ?? 0) > startedAt;
|
|
37400
|
+
}
|
|
37401
|
+
function hasSaidWhoItIs(status) {
|
|
37402
|
+
return status.connected && status.addonVersion !== undefined;
|
|
37177
37403
|
}
|
|
37178
37404
|
function anEditorIsStillComing(listeningSince, launchedEditorIsAlive, now = Date.now()) {
|
|
37179
37405
|
return mayYetConnect(listeningSince, now) || launchedEditorIsAlive;
|
|
@@ -37332,6 +37558,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
37332
37558
|
dapPort: this.connectionInfo?.dapPort,
|
|
37333
37559
|
debugPort: this.connectionInfo?.debugPort,
|
|
37334
37560
|
openedByAServer: this.connectionInfo?.openedByAServer,
|
|
37561
|
+
startedByAnEditor: this.connectionInfo?.startedByAnEditor,
|
|
37335
37562
|
syncsBreakpoints: this.connectionInfo?.syncsBreakpoints,
|
|
37336
37563
|
pendingRequests: this.pendingRequests.size,
|
|
37337
37564
|
queuedResources: this.resourceQueues.size,
|
|
@@ -37477,6 +37704,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
37477
37704
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
37478
37705
|
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
37479
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;
|
|
37480
37708
|
this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
|
|
37481
37709
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
37482
37710
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
@@ -37798,13 +38026,18 @@ function parseJUnit(xml) {
|
|
|
37798
38026
|
const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
|
|
37799
38027
|
const suites = suiteElements.map((suite) => {
|
|
37800
38028
|
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
38029
|
+
const failures = cases.filter((entry) => entry.status === "failed").length;
|
|
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);
|
|
37801
38032
|
return {
|
|
37802
38033
|
name: suite.attributes["name"] ?? "",
|
|
37803
38034
|
path: pathOf(suite),
|
|
37804
38035
|
tests: cases.length,
|
|
37805
38036
|
discovered: count(suite.attributes, "tests"),
|
|
37806
|
-
failures
|
|
37807
|
-
errors
|
|
38037
|
+
failures,
|
|
38038
|
+
errors,
|
|
38039
|
+
hookFailures: Math.max(0, count(suite.attributes, "failures") - written("failure")),
|
|
38040
|
+
hookErrors: Math.max(0, count(suite.attributes, "errors") - written("error")),
|
|
37808
38041
|
skipped: cases.filter((entry) => entry.status === "skipped").length,
|
|
37809
38042
|
time: count(suite.attributes, "time"),
|
|
37810
38043
|
cases
|
|
@@ -37815,11 +38048,19 @@ function parseJUnit(xml) {
|
|
|
37815
38048
|
tests: all.length,
|
|
37816
38049
|
failures: all.filter((entry) => entry.status === "failed").length,
|
|
37817
38050
|
errors: all.filter((entry) => entry.status === "error").length,
|
|
38051
|
+
hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
|
|
38052
|
+
hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
|
|
37818
38053
|
skipped: all.filter((entry) => entry.status === "skipped").length,
|
|
37819
38054
|
time: suites.reduce((sum, suite) => sum + suite.time, 0),
|
|
37820
38055
|
suites
|
|
37821
38056
|
};
|
|
37822
38057
|
}
|
|
38058
|
+
function hookFailuresPrinted(printed) {
|
|
38059
|
+
return printed.flatMap((line) => {
|
|
38060
|
+
const found = HOOK_FAILURE.exec(line);
|
|
38061
|
+
return found === null ? [] : [{ hook: found[2] ?? "", path: found[3] ?? "", line: Number(found[4]), message: found[1] ?? "" }];
|
|
38062
|
+
});
|
|
38063
|
+
}
|
|
37823
38064
|
function whyNoReport(printed, asked) {
|
|
37824
38065
|
const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
|
|
37825
38066
|
if (missing !== undefined) {
|
|
@@ -37884,13 +38125,17 @@ function withActualsPrinted(failed, printed) {
|
|
|
37884
38125
|
at: match[5] ?? "",
|
|
37885
38126
|
...readDiff(match[1] ?? "")
|
|
37886
38127
|
}));
|
|
38128
|
+
const used = new Set;
|
|
37887
38129
|
return failed.map((entry) => {
|
|
37888
38130
|
if (entry.detail === null) {
|
|
37889
38131
|
return entry;
|
|
37890
38132
|
}
|
|
37891
38133
|
let detail = entry.detail;
|
|
37892
38134
|
let recovered = false;
|
|
37893
|
-
for (const diff of diffs) {
|
|
38135
|
+
for (const [index, diff] of diffs.entries()) {
|
|
38136
|
+
if (used.has(index)) {
|
|
38137
|
+
continue;
|
|
38138
|
+
}
|
|
37894
38139
|
const wrong = ` but was
|
|
37895
38140
|
'${diff.merged.replaceAll("\r", "")}'${diff.suffix}
|
|
37896
38141
|
at ${diff.at}`;
|
|
@@ -37899,6 +38144,7 @@ function withActualsPrinted(failed, printed) {
|
|
|
37899
38144
|
'${diff.actual}'${diff.suffix}
|
|
37900
38145
|
at ${diff.at}`);
|
|
37901
38146
|
recovered = true;
|
|
38147
|
+
used.add(index);
|
|
37902
38148
|
}
|
|
37903
38149
|
}
|
|
37904
38150
|
const quoted = QUOTED_EQUALITY.exec(detail);
|
|
@@ -37950,7 +38196,7 @@ function orphansPrinted(printed) {
|
|
|
37950
38196
|
suites
|
|
37951
38197
|
};
|
|
37952
38198
|
}
|
|
37953
|
-
var MalformedReportError, ENTITIES, ATTRIBUTE, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR2, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary";
|
|
38199
|
+
var MalformedReportError, ENTITIES, ATTRIBUTE, HOOK_FAILURE, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR2, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary";
|
|
37954
38200
|
var init_junit = __esm(() => {
|
|
37955
38201
|
MalformedReportError = class MalformedReportError extends Error {
|
|
37956
38202
|
constructor(message) {
|
|
@@ -37960,8 +38206,9 @@ var init_junit = __esm(() => {
|
|
|
37960
38206
|
};
|
|
37961
38207
|
ENTITIES = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
|
|
37962
38208
|
ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
|
|
38209
|
+
HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
|
|
37963
38210
|
NOTHING_RAN = /no test cases found/i;
|
|
37964
|
-
NO_SUCH_PATH = /given directory or file does not exists:\s*(\
|
|
38211
|
+
NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
|
|
37965
38212
|
ESCAPE = String.fromCharCode(27);
|
|
37966
38213
|
PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)(${IGNORING_CASE})?((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
|
|
37967
38214
|
QUOTED_EQUALITY = new RegExp(`^Expecting(?: error message)?:\\n '([\\s\\S]*)'\\n but was\\n '([\\s\\S]*)'(?:${IGNORING_CASE})?\\n\\tat `);
|
|
@@ -37979,15 +38226,15 @@ var init_junit = __esm(() => {
|
|
|
37979
38226
|
"<DEL>": ""
|
|
37980
38227
|
};
|
|
37981
38228
|
PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
|
|
37982
|
-
RUNNING_SUITE = /Run Test Suite:\s*(
|
|
38229
|
+
RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
|
|
37983
38230
|
ORPHANS = /(\d+)\s+orphans/;
|
|
37984
38231
|
});
|
|
37985
38232
|
|
|
37986
38233
|
// src/lsp_client.ts
|
|
37987
|
-
import { realpathSync as
|
|
38234
|
+
import { existsSync as existsSync15, realpathSync as realpathSync4 } from "node:fs";
|
|
37988
38235
|
import { readFile, realpath } from "node:fs/promises";
|
|
37989
38236
|
import { createConnection as createConnection3 } from "node:net";
|
|
37990
|
-
import { dirname as
|
|
38237
|
+
import { dirname as dirname13, join as join19, resolve as resolve4 } from "node:path";
|
|
37991
38238
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
37992
38239
|
function diagnosticsKey(uri) {
|
|
37993
38240
|
let decoded;
|
|
@@ -38000,7 +38247,7 @@ function diagnosticsKey(uri) {
|
|
|
38000
38247
|
return decoded;
|
|
38001
38248
|
}
|
|
38002
38249
|
try {
|
|
38003
|
-
return
|
|
38250
|
+
return realpathSync4.native(fileURLToPath3(decoded)).toLowerCase();
|
|
38004
38251
|
} catch {
|
|
38005
38252
|
return decoded.toLowerCase();
|
|
38006
38253
|
}
|
|
@@ -38017,8 +38264,10 @@ class GodotLSPClient {
|
|
|
38017
38264
|
connectPromise = null;
|
|
38018
38265
|
initialized = false;
|
|
38019
38266
|
rootPath = null;
|
|
38267
|
+
servedWorkspace = null;
|
|
38020
38268
|
diagnosticsWaiters = new Map;
|
|
38021
38269
|
documentVersions = new Map;
|
|
38270
|
+
documentTurns = new Map;
|
|
38022
38271
|
constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1") {
|
|
38023
38272
|
this.port = port;
|
|
38024
38273
|
this.host = host;
|
|
@@ -38115,7 +38364,7 @@ class GodotLSPClient {
|
|
|
38115
38364
|
if (this.initialized) {
|
|
38116
38365
|
return;
|
|
38117
38366
|
}
|
|
38118
|
-
const rootPath = this.rootPath ??
|
|
38367
|
+
const rootPath = this.rootPath ?? dirname13(resolve4(filePath));
|
|
38119
38368
|
await this.initialize(rootPath);
|
|
38120
38369
|
}
|
|
38121
38370
|
async sendRequest(method, params) {
|
|
@@ -38190,6 +38439,10 @@ class GodotLSPClient {
|
|
|
38190
38439
|
}
|
|
38191
38440
|
}
|
|
38192
38441
|
}
|
|
38442
|
+
if (message["method"] === "gdscript_client/changeWorkspace") {
|
|
38443
|
+
const path = message["params"]?.["path"];
|
|
38444
|
+
this.servedWorkspace = typeof path === "string" ? path : null;
|
|
38445
|
+
}
|
|
38193
38446
|
if (message["method"] === "textDocument/publishDiagnostics") {
|
|
38194
38447
|
const params = message["params"];
|
|
38195
38448
|
const paramsObject = params && typeof params === "object" ? params : null;
|
|
@@ -38292,10 +38545,14 @@ class GodotLSPClient {
|
|
|
38292
38545
|
this.sendNotification("textDocument/didClose", { textDocument: { uri } });
|
|
38293
38546
|
} catch {}
|
|
38294
38547
|
}
|
|
38548
|
+
forgetServedWorkspace() {
|
|
38549
|
+
this.servedWorkspace = null;
|
|
38550
|
+
}
|
|
38295
38551
|
async initialize(rootPath) {
|
|
38296
38552
|
await this.ensureConnected();
|
|
38297
38553
|
const resolvedRootPath = resolve4(rootPath);
|
|
38298
38554
|
const rootUri = pathToFileURL(resolvedRootPath).href;
|
|
38555
|
+
this.forgetServedWorkspace();
|
|
38299
38556
|
const result = await this.sendRequest("initialize", {
|
|
38300
38557
|
processId: process.pid,
|
|
38301
38558
|
rootPath: resolvedRootPath,
|
|
@@ -38321,60 +38578,82 @@ class GodotLSPClient {
|
|
|
38321
38578
|
}
|
|
38322
38579
|
]
|
|
38323
38580
|
});
|
|
38581
|
+
const served = this.servedWorkspace;
|
|
38582
|
+
if (served !== null && !isSameDirectory(served, resolvedRootPath)) {
|
|
38583
|
+
throw new AnotherProjectsServer(this.port, served, resolvedRootPath);
|
|
38584
|
+
}
|
|
38324
38585
|
this.sendNotification("initialized", {});
|
|
38325
38586
|
this.initialized = true;
|
|
38326
38587
|
this.rootPath = resolvedRootPath;
|
|
38327
38588
|
return result;
|
|
38328
38589
|
}
|
|
38329
|
-
async
|
|
38330
|
-
await this.ensureConnected();
|
|
38331
|
-
await this.ensureInitializedForFile(filePath);
|
|
38590
|
+
async withDocument(filePath, work) {
|
|
38332
38591
|
const key = diagnosticsKey(this.toFileUri(filePath));
|
|
38333
|
-
const
|
|
38334
|
-
|
|
38335
|
-
|
|
38336
|
-
|
|
38337
|
-
}
|
|
38338
|
-
const timer = setTimeout(() => {
|
|
38339
|
-
this.diagnosticsWaiters.delete(key);
|
|
38340
|
-
rejectDiagnostics(new Error(`Godot published no diagnostics for ${key} within ${DIAGNOSTICS_TIMEOUT_MS}ms. ` + "The language server may not be running, or may not have this file in its workspace."));
|
|
38341
|
-
}, DIAGNOSTICS_TIMEOUT_MS);
|
|
38342
|
-
this.diagnosticsWaiters.set(key, {
|
|
38343
|
-
resolve: resolveDiagnostics,
|
|
38344
|
-
reject: rejectDiagnostics,
|
|
38345
|
-
timer
|
|
38346
|
-
});
|
|
38592
|
+
const before = this.documentTurns.get(key) ?? Promise.resolve();
|
|
38593
|
+
let done = () => {};
|
|
38594
|
+
const mine = new Promise((finished) => {
|
|
38595
|
+
done = finished;
|
|
38347
38596
|
});
|
|
38348
|
-
|
|
38597
|
+
const turn = before.then(() => mine);
|
|
38598
|
+
this.documentTurns.set(key, turn);
|
|
38599
|
+
await before;
|
|
38349
38600
|
try {
|
|
38350
|
-
|
|
38351
|
-
return await diagnosticsPromise;
|
|
38352
|
-
} catch (error) {
|
|
38353
|
-
const waiter = this.diagnosticsWaiters.get(key);
|
|
38354
|
-
if (waiter) {
|
|
38355
|
-
clearTimeout(waiter.timer);
|
|
38356
|
-
this.diagnosticsWaiters.delete(key);
|
|
38357
|
-
}
|
|
38358
|
-
throw error;
|
|
38601
|
+
return await work();
|
|
38359
38602
|
} finally {
|
|
38360
|
-
|
|
38361
|
-
|
|
38603
|
+
done();
|
|
38604
|
+
if (this.documentTurns.get(key) === turn) {
|
|
38605
|
+
this.documentTurns.delete(key);
|
|
38362
38606
|
}
|
|
38363
38607
|
}
|
|
38364
38608
|
}
|
|
38365
|
-
async
|
|
38609
|
+
async getDiagnostics(filePath, content) {
|
|
38366
38610
|
await this.ensureConnected();
|
|
38367
38611
|
await this.ensureInitializedForFile(filePath);
|
|
38368
|
-
|
|
38369
|
-
|
|
38370
|
-
|
|
38371
|
-
|
|
38372
|
-
|
|
38373
|
-
|
|
38612
|
+
return this.withDocument(filePath, async () => {
|
|
38613
|
+
const key = diagnosticsKey(this.toFileUri(filePath));
|
|
38614
|
+
const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
|
|
38615
|
+
const timer = setTimeout(() => {
|
|
38616
|
+
this.diagnosticsWaiters.delete(key);
|
|
38617
|
+
rejectDiagnostics(new Error(`Godot published no diagnostics for ${key} within ${DIAGNOSTICS_TIMEOUT_MS}ms. ` + "The language server may not be running, or may not have this file in its workspace."));
|
|
38618
|
+
}, DIAGNOSTICS_TIMEOUT_MS);
|
|
38619
|
+
this.diagnosticsWaiters.set(key, {
|
|
38620
|
+
resolve: resolveDiagnostics,
|
|
38621
|
+
reject: rejectDiagnostics,
|
|
38622
|
+
timer
|
|
38623
|
+
});
|
|
38374
38624
|
});
|
|
38375
|
-
|
|
38376
|
-
|
|
38377
|
-
|
|
38625
|
+
let opened = null;
|
|
38626
|
+
try {
|
|
38627
|
+
opened = this.syncDocument(filePath, content);
|
|
38628
|
+
return await diagnosticsPromise;
|
|
38629
|
+
} catch (error) {
|
|
38630
|
+
const waiter = this.diagnosticsWaiters.get(key);
|
|
38631
|
+
if (waiter) {
|
|
38632
|
+
clearTimeout(waiter.timer);
|
|
38633
|
+
this.diagnosticsWaiters.delete(key);
|
|
38634
|
+
}
|
|
38635
|
+
throw error;
|
|
38636
|
+
} finally {
|
|
38637
|
+
if (opened !== null) {
|
|
38638
|
+
this.closeDocument(opened);
|
|
38639
|
+
}
|
|
38640
|
+
}
|
|
38641
|
+
});
|
|
38642
|
+
}
|
|
38643
|
+
async getCompletions(filePath, content, line, character) {
|
|
38644
|
+
await this.ensureConnected();
|
|
38645
|
+
await this.ensureInitializedForFile(filePath);
|
|
38646
|
+
const result = await this.withDocument(filePath, async () => {
|
|
38647
|
+
const uri = this.syncDocument(filePath, content);
|
|
38648
|
+
try {
|
|
38649
|
+
return await this.sendRequest("textDocument/completion", {
|
|
38650
|
+
textDocument: { uri },
|
|
38651
|
+
position: { line, character }
|
|
38652
|
+
});
|
|
38653
|
+
} finally {
|
|
38654
|
+
this.closeDocument(uri);
|
|
38655
|
+
}
|
|
38656
|
+
});
|
|
38378
38657
|
if (Array.isArray(result)) {
|
|
38379
38658
|
return result;
|
|
38380
38659
|
}
|
|
@@ -38389,28 +38668,31 @@ class GodotLSPClient {
|
|
|
38389
38668
|
async getHover(filePath, content, line, character) {
|
|
38390
38669
|
await this.ensureConnected();
|
|
38391
38670
|
await this.ensureInitializedForFile(filePath);
|
|
38392
|
-
|
|
38393
|
-
|
|
38394
|
-
|
|
38395
|
-
textDocument
|
|
38396
|
-
|
|
38397
|
-
|
|
38398
|
-
|
|
38399
|
-
|
|
38400
|
-
|
|
38671
|
+
return this.withDocument(filePath, async () => {
|
|
38672
|
+
const uri = this.syncDocument(filePath, content);
|
|
38673
|
+
try {
|
|
38674
|
+
return await this.sendRequest("textDocument/hover", {
|
|
38675
|
+
textDocument: { uri },
|
|
38676
|
+
position: { line, character }
|
|
38677
|
+
});
|
|
38678
|
+
} finally {
|
|
38679
|
+
this.closeDocument(uri);
|
|
38680
|
+
}
|
|
38681
|
+
});
|
|
38401
38682
|
}
|
|
38402
38683
|
async getDocumentSymbols(filePath, content) {
|
|
38403
38684
|
await this.ensureConnected();
|
|
38404
38685
|
await this.ensureInitializedForFile(filePath);
|
|
38405
|
-
const
|
|
38406
|
-
|
|
38407
|
-
|
|
38408
|
-
|
|
38409
|
-
|
|
38410
|
-
|
|
38411
|
-
|
|
38412
|
-
|
|
38413
|
-
|
|
38686
|
+
const result = await this.withDocument(filePath, async () => {
|
|
38687
|
+
const uri = this.syncDocument(filePath, content);
|
|
38688
|
+
try {
|
|
38689
|
+
return await this.sendRequest("textDocument/documentSymbol", {
|
|
38690
|
+
textDocument: { uri }
|
|
38691
|
+
});
|
|
38692
|
+
} finally {
|
|
38693
|
+
this.closeDocument(uri);
|
|
38694
|
+
}
|
|
38695
|
+
});
|
|
38414
38696
|
return Array.isArray(result) ? result : [];
|
|
38415
38697
|
}
|
|
38416
38698
|
isConnected() {
|
|
@@ -38442,36 +38724,39 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
38442
38724
|
try {
|
|
38443
38725
|
projectPath = await realpath(requestedProjectPath);
|
|
38444
38726
|
} catch {
|
|
38445
|
-
throw new
|
|
38727
|
+
throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
|
|
38728
|
+
}
|
|
38729
|
+
if (!existsSync15(join19(projectPath, "project.godot"))) {
|
|
38730
|
+
throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
|
|
38446
38731
|
}
|
|
38447
38732
|
const contained = resolveWithinProject(projectPath, scriptPathValue);
|
|
38448
38733
|
if (!contained.ok) {
|
|
38449
|
-
throw new
|
|
38734
|
+
throw new ArgumentRefusal(contained.reason);
|
|
38450
38735
|
}
|
|
38451
38736
|
let scriptPath;
|
|
38452
38737
|
try {
|
|
38453
38738
|
scriptPath = await realpath(contained.absolutePath);
|
|
38454
38739
|
} catch {
|
|
38455
|
-
throw new
|
|
38740
|
+
throw new ArgumentRefusal(`Script file does not exist: ${contained.absolutePath}`);
|
|
38456
38741
|
}
|
|
38457
38742
|
if (!isWithinRoot(projectPath, scriptPath)) {
|
|
38458
|
-
throw new
|
|
38743
|
+
throw new ArgumentRefusal("scriptPath resolves outside the project root boundary.");
|
|
38459
38744
|
}
|
|
38460
38745
|
return { projectPath, scriptPath };
|
|
38461
38746
|
}
|
|
38462
38747
|
async function handleLSPTool(client, toolName, args) {
|
|
38463
38748
|
try {
|
|
38464
38749
|
if (!args || typeof args !== "object") {
|
|
38465
|
-
throw new
|
|
38750
|
+
throw new ArgumentRefusal("Tool arguments must be an object.");
|
|
38466
38751
|
}
|
|
38467
38752
|
const parsedArgs = args;
|
|
38468
38753
|
const projectPathValue = parsedArgs["projectPath"];
|
|
38469
38754
|
const scriptPathValue = parsedArgs["scriptPath"];
|
|
38470
38755
|
if (typeof projectPathValue !== "string" || projectPathValue.length === 0) {
|
|
38471
|
-
throw new
|
|
38756
|
+
throw new ArgumentRefusal("Missing required argument: projectPath");
|
|
38472
38757
|
}
|
|
38473
38758
|
if (typeof scriptPathValue !== "string" || scriptPathValue.length === 0) {
|
|
38474
|
-
throw new
|
|
38759
|
+
throw new ArgumentRefusal("Missing required argument: scriptPath");
|
|
38475
38760
|
}
|
|
38476
38761
|
const { projectPath, scriptPath } = await resolveLSPPaths(projectPathValue, scriptPathValue);
|
|
38477
38762
|
const content = await readFile(scriptPath, "utf8");
|
|
@@ -38485,7 +38770,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
38485
38770
|
const line = Number(parsedArgs["line"]);
|
|
38486
38771
|
const character = Number(parsedArgs["character"]);
|
|
38487
38772
|
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
38488
|
-
throw new
|
|
38773
|
+
throw new ArgumentRefusal("Arguments line and character must be numbers.");
|
|
38489
38774
|
}
|
|
38490
38775
|
const completions = await client.getCompletions(scriptPath, content, line, character);
|
|
38491
38776
|
return asToolResponse({ completions });
|
|
@@ -38494,7 +38779,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
38494
38779
|
const line = Number(parsedArgs["line"]);
|
|
38495
38780
|
const character = Number(parsedArgs["character"]);
|
|
38496
38781
|
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
38497
|
-
throw new
|
|
38782
|
+
throw new ArgumentRefusal("Arguments line and character must be numbers.");
|
|
38498
38783
|
}
|
|
38499
38784
|
const hover = await client.getHover(scriptPath, content, line, character);
|
|
38500
38785
|
return asToolResponse({ hover });
|
|
@@ -38508,20 +38793,31 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
38508
38793
|
}
|
|
38509
38794
|
} catch (error) {
|
|
38510
38795
|
return asToolResponse({
|
|
38511
|
-
error: normalizeLSPError(error, client.port)
|
|
38796
|
+
error: normalizeLSPError(error, client.port),
|
|
38797
|
+
...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
|
|
38798
|
+
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {}
|
|
38512
38799
|
});
|
|
38513
38800
|
}
|
|
38514
38801
|
}
|
|
38515
|
-
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
38802
|
+
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, ArgumentRefusal, AnotherProjectsServer;
|
|
38516
38803
|
var init_lsp_client = __esm(() => {
|
|
38517
38804
|
init_errors();
|
|
38518
38805
|
init_framing();
|
|
38519
38806
|
init_paths();
|
|
38520
38807
|
init_ports();
|
|
38808
|
+
ArgumentRefusal = class ArgumentRefusal extends Refusal {
|
|
38809
|
+
};
|
|
38810
|
+
AnotherProjectsServer = class AnotherProjectsServer extends Refusal {
|
|
38811
|
+
serves;
|
|
38812
|
+
constructor(port, serves, asked) {
|
|
38813
|
+
super(`The language server on port ${port} belongs to the editor of ${serves}, not ${asked}.`);
|
|
38814
|
+
this.serves = serves;
|
|
38815
|
+
}
|
|
38816
|
+
};
|
|
38521
38817
|
});
|
|
38522
38818
|
|
|
38523
38819
|
// src/outside.ts
|
|
38524
|
-
import { spawn as
|
|
38820
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
38525
38821
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
38526
38822
|
function changesFrom(env, base) {
|
|
38527
38823
|
const changes = {};
|
|
@@ -38554,7 +38850,7 @@ function readLaunched(printed) {
|
|
|
38554
38850
|
}
|
|
38555
38851
|
async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
|
|
38556
38852
|
return await new Promise((resolve) => {
|
|
38557
|
-
const launcher =
|
|
38853
|
+
const launcher = spawn3(runtime, [keeper, "launch"], {
|
|
38558
38854
|
stdio: ["pipe", "pipe", "pipe"],
|
|
38559
38855
|
windowsHide: true
|
|
38560
38856
|
});
|
|
@@ -38589,8 +38885,8 @@ var init_outside = __esm(() => {
|
|
|
38589
38885
|
});
|
|
38590
38886
|
|
|
38591
38887
|
// src/process-time.ts
|
|
38592
|
-
import { execFile as
|
|
38593
|
-
import { promisify as
|
|
38888
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
38889
|
+
import { promisify as promisify3 } from "node:util";
|
|
38594
38890
|
async function cpuSecondsOf(pid) {
|
|
38595
38891
|
try {
|
|
38596
38892
|
return process.platform === "win32" ? await secondsOnWindows(pid) : await askPosix2(pid);
|
|
@@ -38604,7 +38900,7 @@ async function secondsOnWindows(pid) {
|
|
|
38604
38900
|
return Number.isFinite(seconds) ? seconds : undefined;
|
|
38605
38901
|
}
|
|
38606
38902
|
async function askPosix2(pid) {
|
|
38607
|
-
const { stdout } = await
|
|
38903
|
+
const { stdout } = await run3("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
|
|
38608
38904
|
return secondsFromClock(stdout.trim());
|
|
38609
38905
|
}
|
|
38610
38906
|
function secondsFromClock(printed) {
|
|
@@ -38622,20 +38918,20 @@ function secondsFromClock(printed) {
|
|
|
38622
38918
|
const extra = days === null ? 0 : Number(days) * 86400;
|
|
38623
38919
|
return Number.isFinite(extra) ? seconds + extra : undefined;
|
|
38624
38920
|
}
|
|
38625
|
-
var
|
|
38921
|
+
var run3, ASK_TIMEOUT_MS2 = 6000;
|
|
38626
38922
|
var init_process_time = __esm(() => {
|
|
38627
38923
|
init_windows_ask();
|
|
38628
|
-
|
|
38924
|
+
run3 = promisify3(execFile3);
|
|
38629
38925
|
});
|
|
38630
38926
|
|
|
38631
38927
|
// src/project-scan.ts
|
|
38632
|
-
import { existsSync as
|
|
38633
|
-
import { join as
|
|
38928
|
+
import { existsSync as existsSync16, readdirSync as readdirSync7, readFileSync as readFileSync17 } from "node:fs";
|
|
38929
|
+
import { join as join20 } from "node:path";
|
|
38634
38930
|
function skipped(name) {
|
|
38635
38931
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
38636
38932
|
}
|
|
38637
38933
|
function steppedOver(directory) {
|
|
38638
|
-
return
|
|
38934
|
+
return existsSync16(join20(directory, ".gdignore"));
|
|
38639
38935
|
}
|
|
38640
38936
|
function projectStructure(projectPath) {
|
|
38641
38937
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
@@ -38648,10 +38944,10 @@ function projectStructure(projectPath) {
|
|
|
38648
38944
|
continue;
|
|
38649
38945
|
}
|
|
38650
38946
|
if (entry.isDirectory()) {
|
|
38651
|
-
visit(
|
|
38947
|
+
visit(join20(directory, entry.name));
|
|
38652
38948
|
} else if (entry.isFile()) {
|
|
38653
38949
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
38654
|
-
if (extension
|
|
38950
|
+
if (SCENE_EXTENSIONS.has(extension)) {
|
|
38655
38951
|
structure.scenes += 1;
|
|
38656
38952
|
} else if (extension === "gd" || extension === "cs") {
|
|
38657
38953
|
structure.scripts += 1;
|
|
@@ -38678,14 +38974,14 @@ function scriptsWithoutUid(projectPath) {
|
|
|
38678
38974
|
}
|
|
38679
38975
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
38680
38976
|
if (entry.isDirectory()) {
|
|
38681
|
-
visit(
|
|
38977
|
+
visit(join20(directory, entry.name), spelled);
|
|
38682
38978
|
continue;
|
|
38683
38979
|
}
|
|
38684
38980
|
if (!entry.isFile()) {
|
|
38685
38981
|
continue;
|
|
38686
38982
|
}
|
|
38687
38983
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
38688
|
-
if (CARRIES_A_UID.has(extension) && !
|
|
38984
|
+
if (CARRIES_A_UID.has(extension) && !existsSync16(join20(directory, `${entry.name}.uid`))) {
|
|
38689
38985
|
found.push(spelled);
|
|
38690
38986
|
}
|
|
38691
38987
|
}
|
|
@@ -38693,8 +38989,11 @@ function scriptsWithoutUid(projectPath) {
|
|
|
38693
38989
|
visit(projectPath, "");
|
|
38694
38990
|
return found.sort();
|
|
38695
38991
|
}
|
|
38992
|
+
function isBinary(bytes) {
|
|
38993
|
+
return bytes.subarray(0, 8000).includes(0);
|
|
38994
|
+
}
|
|
38696
38995
|
function searchProject(projectPath, options) {
|
|
38697
|
-
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));
|
|
38698
38997
|
const result = {
|
|
38699
38998
|
query: options.query,
|
|
38700
38999
|
results: [],
|
|
@@ -38702,13 +39001,7 @@ function searchProject(projectPath, options) {
|
|
|
38702
39001
|
};
|
|
38703
39002
|
const regex = options.regex ? new RegExp(options.query, options.caseSensitive ? "" : "i") : null;
|
|
38704
39003
|
const needle = options.caseSensitive ? options.query : options.query.toLowerCase();
|
|
38705
|
-
const full = () =>
|
|
38706
|
-
if (result.summary.total_matches >= options.maxResults) {
|
|
38707
|
-
result.summary.truncated = true;
|
|
38708
|
-
return true;
|
|
38709
|
-
}
|
|
38710
|
-
return false;
|
|
38711
|
-
};
|
|
39004
|
+
const full = () => result.summary.truncated;
|
|
38712
39005
|
const visit = (directory) => {
|
|
38713
39006
|
if (steppedOver(directory)) {
|
|
38714
39007
|
return;
|
|
@@ -38720,27 +39013,32 @@ function searchProject(projectPath, options) {
|
|
|
38720
39013
|
if (skipped(entry.name)) {
|
|
38721
39014
|
continue;
|
|
38722
39015
|
}
|
|
38723
|
-
const entryPath =
|
|
39016
|
+
const entryPath = join20(directory, entry.name);
|
|
38724
39017
|
if (entry.isDirectory()) {
|
|
38725
39018
|
visit(entryPath);
|
|
38726
39019
|
continue;
|
|
38727
39020
|
}
|
|
38728
39021
|
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
38729
|
-
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)) {
|
|
38730
39027
|
continue;
|
|
38731
39028
|
}
|
|
38732
39029
|
result.summary.files_searched += 1;
|
|
38733
39030
|
const matches = [];
|
|
38734
|
-
for (const [index, line] of
|
|
38735
|
-
|
|
38736
|
-
if (
|
|
38737
|
-
|
|
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;
|
|
38738
39035
|
}
|
|
38739
|
-
|
|
38740
|
-
|
|
38741
|
-
|
|
38742
|
-
result.summary.total_matches += 1;
|
|
39036
|
+
if (result.summary.total_matches >= options.maxResults) {
|
|
39037
|
+
result.summary.truncated = true;
|
|
39038
|
+
break;
|
|
38743
39039
|
}
|
|
39040
|
+
matches.push({ line: index + 1, content: line.trim(), match: found[0] ?? "" });
|
|
39041
|
+
result.summary.total_matches += 1;
|
|
38744
39042
|
}
|
|
38745
39043
|
if (matches.length > 0) {
|
|
38746
39044
|
const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
|
|
@@ -38752,23 +39050,53 @@ function searchProject(projectPath, options) {
|
|
|
38752
39050
|
visit(projectPath);
|
|
38753
39051
|
return result;
|
|
38754
39052
|
}
|
|
38755
|
-
var SKIPPED, ASSET_EXTENSIONS, CARRIES_A_UID;
|
|
39053
|
+
var SKIPPED, SCENE_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID;
|
|
38756
39054
|
var init_project_scan = __esm(() => {
|
|
38757
39055
|
SKIPPED = new Set(["node_modules"]);
|
|
38758
|
-
|
|
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
|
+
]);
|
|
38759
39087
|
CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
38760
39088
|
});
|
|
38761
39089
|
|
|
38762
39090
|
// src/restart-note.ts
|
|
38763
|
-
import { mkdirSync as mkdirSync10, readFileSync as
|
|
38764
|
-
import { dirname as
|
|
39091
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync18, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "node:fs";
|
|
39092
|
+
import { dirname as dirname14, join as join21 } from "node:path";
|
|
38765
39093
|
function restartNotePath(projectPath) {
|
|
38766
|
-
return
|
|
39094
|
+
return join21(projectPath, ".godot", "gdharness-restart.json");
|
|
38767
39095
|
}
|
|
38768
39096
|
function noteRestartBegun(note) {
|
|
38769
39097
|
const path = restartNotePath(note.projectPath);
|
|
38770
39098
|
try {
|
|
38771
|
-
mkdirSync10(
|
|
39099
|
+
mkdirSync10(dirname14(path), { recursive: true });
|
|
38772
39100
|
writeFileSync12(path, `${JSON.stringify(note, null, 2)}
|
|
38773
39101
|
`, "utf8");
|
|
38774
39102
|
} catch (error) {
|
|
@@ -38777,7 +39105,7 @@ function noteRestartBegun(note) {
|
|
|
38777
39105
|
}
|
|
38778
39106
|
function restartOwed(projectPath) {
|
|
38779
39107
|
try {
|
|
38780
|
-
const parsed = JSON.parse(
|
|
39108
|
+
const parsed = JSON.parse(readFileSync18(restartNotePath(projectPath), "utf8"));
|
|
38781
39109
|
if (typeof parsed !== "object" || parsed === null) {
|
|
38782
39110
|
return null;
|
|
38783
39111
|
}
|
|
@@ -38802,31 +39130,31 @@ var init_restart_note = __esm(() => {
|
|
|
38802
39130
|
});
|
|
38803
39131
|
|
|
38804
39132
|
// src/run-record.ts
|
|
38805
|
-
import { execFile as
|
|
39133
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
38806
39134
|
import { createHash as createHash4 } from "node:crypto";
|
|
38807
39135
|
import {
|
|
38808
39136
|
mkdirSync as mkdirSync11,
|
|
38809
39137
|
openSync as openSync2,
|
|
38810
39138
|
readdirSync as readdirSync8,
|
|
38811
|
-
readFileSync as
|
|
38812
|
-
realpathSync as
|
|
39139
|
+
readFileSync as readFileSync19,
|
|
39140
|
+
realpathSync as realpathSync5,
|
|
38813
39141
|
rmSync as rmSync10,
|
|
38814
39142
|
statSync as statSync7,
|
|
38815
39143
|
writeFileSync as writeFileSync13
|
|
38816
39144
|
} from "node:fs";
|
|
38817
|
-
import { basename as basename4, dirname as
|
|
38818
|
-
import { promisify as
|
|
39145
|
+
import { basename as basename4, dirname as dirname15, join as join22, resolve as resolve5 } from "node:path";
|
|
39146
|
+
import { promisify as promisify4 } from "node:util";
|
|
38819
39147
|
function runsDirectory() {
|
|
38820
|
-
return
|
|
39148
|
+
return join22(runtimeDirectory(), "runs");
|
|
38821
39149
|
}
|
|
38822
39150
|
function realAsFarAsItExists(path) {
|
|
38823
39151
|
const missing = [];
|
|
38824
39152
|
let at = path;
|
|
38825
39153
|
for (;; ) {
|
|
38826
39154
|
try {
|
|
38827
|
-
return
|
|
39155
|
+
return join22(realpathSync5.native(at), ...missing);
|
|
38828
39156
|
} catch {
|
|
38829
|
-
const parent =
|
|
39157
|
+
const parent = dirname15(at);
|
|
38830
39158
|
if (parent === at) {
|
|
38831
39159
|
return path;
|
|
38832
39160
|
}
|
|
@@ -38841,15 +39169,18 @@ function projectKey(projectPath) {
|
|
|
38841
39169
|
return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
38842
39170
|
}
|
|
38843
39171
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
38844
|
-
return
|
|
39172
|
+
return join22(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
38845
39173
|
}
|
|
38846
39174
|
function openTranscript(startedAt) {
|
|
38847
39175
|
mkdirSync11(runsDirectory(), { recursive: true });
|
|
38848
|
-
const path =
|
|
39176
|
+
const path = join22(runsDirectory(), `run-${startedAt}.log`);
|
|
38849
39177
|
return { path, fd: openSync2(path, "a") };
|
|
38850
39178
|
}
|
|
39179
|
+
function noteIsAbout(note, playing) {
|
|
39180
|
+
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
39181
|
+
}
|
|
38851
39182
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
38852
|
-
return
|
|
39183
|
+
return join22(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
38853
39184
|
}
|
|
38854
39185
|
function writeEditorRunNote(note) {
|
|
38855
39186
|
mkdirSync11(runsDirectory(), { recursive: true });
|
|
@@ -38857,7 +39188,7 @@ function writeEditorRunNote(note) {
|
|
|
38857
39188
|
}
|
|
38858
39189
|
function readEditorRunNote(projectPath) {
|
|
38859
39190
|
for (const directory of runtimeDirectories()) {
|
|
38860
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
39191
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join22(directory, "runs", "editor-run.json"));
|
|
38861
39192
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
38862
39193
|
return note;
|
|
38863
39194
|
}
|
|
@@ -38867,7 +39198,7 @@ function readEditorRunNote(projectPath) {
|
|
|
38867
39198
|
function editorNoteAt(path) {
|
|
38868
39199
|
let parsed;
|
|
38869
39200
|
try {
|
|
38870
|
-
parsed = JSON.parse(
|
|
39201
|
+
parsed = JSON.parse(readFileSync19(path, "utf8"));
|
|
38871
39202
|
} catch {
|
|
38872
39203
|
return null;
|
|
38873
39204
|
}
|
|
@@ -38881,7 +39212,9 @@ function editorNoteAt(path) {
|
|
|
38881
39212
|
if (typeof projectPath !== "string" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
38882
39213
|
return null;
|
|
38883
39214
|
}
|
|
38884
|
-
|
|
39215
|
+
const play = fields["play"];
|
|
39216
|
+
const which = typeof play === "object" && play !== null && typeof play["play"] === "number" && typeof play["editorPid"] === "number" ? play : undefined;
|
|
39217
|
+
return { projectPath, transcript, startedAt, ...which === undefined ? {} : { play: which } };
|
|
38885
39218
|
}
|
|
38886
39219
|
function readRunRecord(projectPath) {
|
|
38887
39220
|
for (const directory of runtimeDirectories()) {
|
|
@@ -38889,7 +39222,7 @@ function readRunRecord(projectPath) {
|
|
|
38889
39222
|
if (own !== null) {
|
|
38890
39223
|
return own;
|
|
38891
39224
|
}
|
|
38892
|
-
const shared = recordAt(
|
|
39225
|
+
const shared = recordAt(join22(directory, "runs", SHARED_NOTE));
|
|
38893
39226
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
38894
39227
|
return shared;
|
|
38895
39228
|
}
|
|
@@ -38901,13 +39234,13 @@ function everyRunRecord() {
|
|
|
38901
39234
|
for (const directory of runtimeDirectories()) {
|
|
38902
39235
|
let entries;
|
|
38903
39236
|
try {
|
|
38904
|
-
entries = readdirSync8(
|
|
39237
|
+
entries = readdirSync8(join22(directory, "runs"));
|
|
38905
39238
|
} catch {
|
|
38906
39239
|
continue;
|
|
38907
39240
|
}
|
|
38908
39241
|
for (const entry of entries) {
|
|
38909
39242
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
38910
|
-
const record = recordAt(
|
|
39243
|
+
const record = recordAt(join22(directory, "runs", entry));
|
|
38911
39244
|
if (record !== null) {
|
|
38912
39245
|
found.push(record);
|
|
38913
39246
|
}
|
|
@@ -38919,7 +39252,7 @@ function everyRunRecord() {
|
|
|
38919
39252
|
function recordAt(path) {
|
|
38920
39253
|
let parsed;
|
|
38921
39254
|
try {
|
|
38922
|
-
parsed = JSON.parse(
|
|
39255
|
+
parsed = JSON.parse(readFileSync19(path, "utf8"));
|
|
38923
39256
|
} catch {
|
|
38924
39257
|
return null;
|
|
38925
39258
|
}
|
|
@@ -38938,7 +39271,9 @@ function recordAt(path) {
|
|
|
38938
39271
|
pid,
|
|
38939
39272
|
transcript,
|
|
38940
39273
|
startedAt,
|
|
39274
|
+
...typeof fields["startedBy"] === "number" ? { startedBy: fields["startedBy"] } : {},
|
|
38941
39275
|
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
39276
|
+
...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
|
|
38942
39277
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
38943
39278
|
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
38944
39279
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
@@ -38947,7 +39282,8 @@ function recordAt(path) {
|
|
|
38947
39282
|
}
|
|
38948
39283
|
async function runningAs(pid) {
|
|
38949
39284
|
if (process.platform === "win32") {
|
|
38950
|
-
const
|
|
39285
|
+
const read = await readWindowsCommandLine(pid);
|
|
39286
|
+
const answer = "answer" in read ? read.answer : null;
|
|
38951
39287
|
if (answer !== null) {
|
|
38952
39288
|
const [, ...rest] = answer.split(`
|
|
38953
39289
|
`);
|
|
@@ -38959,13 +39295,17 @@ async function runningAs(pid) {
|
|
|
38959
39295
|
}
|
|
38960
39296
|
}
|
|
38961
39297
|
const image = await windowsImage(pid);
|
|
38962
|
-
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
|
+
};
|
|
38963
39303
|
}
|
|
38964
39304
|
try {
|
|
38965
39305
|
const began = await startedAt(pid, null);
|
|
38966
39306
|
const when = began === null ? {} : { startedAt: began };
|
|
38967
39307
|
if (process.platform === "linux") {
|
|
38968
|
-
const raw =
|
|
39308
|
+
const raw = readFileSync19(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
38969
39309
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
38970
39310
|
}
|
|
38971
39311
|
const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
|
|
@@ -39010,13 +39350,18 @@ function listeningPidInNetstat(printed, port) {
|
|
|
39010
39350
|
}
|
|
39011
39351
|
return null;
|
|
39012
39352
|
}
|
|
39013
|
-
async function
|
|
39014
|
-
|
|
39015
|
-
|
|
39016
|
-
|
|
39017
|
-
|
|
39018
|
-
|
|
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
|
+
}
|
|
39019
39363
|
}
|
|
39364
|
+
return { unread };
|
|
39020
39365
|
}
|
|
39021
39366
|
async function startedAt(pid, windowsAnswer) {
|
|
39022
39367
|
if (process.platform === "win32") {
|
|
@@ -39044,36 +39389,43 @@ async function windowsImage(pid) {
|
|
|
39044
39389
|
return null;
|
|
39045
39390
|
}
|
|
39046
39391
|
}
|
|
39047
|
-
async function
|
|
39048
|
-
return
|
|
39392
|
+
async function whyNotStillTheRecordedRun(record) {
|
|
39393
|
+
return whyNotTheRun(record, await runningAs(record.pid), "confirmed");
|
|
39049
39394
|
}
|
|
39050
39395
|
async function couldStillBeTheRecordedRun(record) {
|
|
39051
39396
|
return judgeRun(record, await runningAs(record.pid), "possible");
|
|
39052
39397
|
}
|
|
39053
39398
|
function judgeRun(record, running, needed) {
|
|
39399
|
+
return whyNotTheRun(record, running, needed) === null;
|
|
39400
|
+
}
|
|
39401
|
+
function whyNotTheRun(record, running, needed) {
|
|
39054
39402
|
if (running === null) {
|
|
39055
|
-
return
|
|
39403
|
+
return "the operating system would not describe that process";
|
|
39056
39404
|
}
|
|
39057
|
-
|
|
39058
|
-
|
|
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`;
|
|
39059
39408
|
}
|
|
39060
39409
|
const engine = record.command === undefined ? null : basename4(record.command);
|
|
39061
39410
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
39062
39411
|
const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
|
|
39063
39412
|
if (wanted !== null && !said.includes(wanted)) {
|
|
39064
|
-
return
|
|
39413
|
+
return `that process is not ${engine ?? "the engine"}, which the run was started with`;
|
|
39065
39414
|
}
|
|
39066
39415
|
if (running.kind === "commandLine" && readCommandLine(running.text).editor) {
|
|
39067
|
-
return
|
|
39416
|
+
return "that process is an editor";
|
|
39068
39417
|
}
|
|
39069
39418
|
if (record.projectPath === "") {
|
|
39070
|
-
return wanted !== null;
|
|
39419
|
+
return wanted !== null ? null : "the record names neither the project nor the engine";
|
|
39071
39420
|
}
|
|
39072
39421
|
if (running.kind === "image") {
|
|
39073
|
-
|
|
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`;
|
|
39074
39426
|
}
|
|
39075
39427
|
const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
|
|
39076
|
-
return said.includes(project)
|
|
39428
|
+
return said.includes(project) ? null : `that process's command line does not name ${record.projectPath}`;
|
|
39077
39429
|
}
|
|
39078
39430
|
async function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
|
|
39079
39431
|
return judgeAnnouncedGame(await runningAs(pid), projectPath, announcedAt);
|
|
@@ -39093,7 +39445,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
39093
39445
|
}
|
|
39094
39446
|
function clearRunRecord(projectPath) {
|
|
39095
39447
|
for (const directory of runtimeDirectories()) {
|
|
39096
|
-
const shared =
|
|
39448
|
+
const shared = join22(directory, "runs", SHARED_NOTE);
|
|
39097
39449
|
const sharedRecord = recordAt(shared);
|
|
39098
39450
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
39099
39451
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
@@ -39123,7 +39475,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
39123
39475
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
39124
39476
|
continue;
|
|
39125
39477
|
}
|
|
39126
|
-
const path =
|
|
39478
|
+
const path = join22(directory, entry);
|
|
39127
39479
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
39128
39480
|
continue;
|
|
39129
39481
|
}
|
|
@@ -39134,35 +39486,43 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
39134
39486
|
}
|
|
39135
39487
|
var execFileAsync, SHARED_NOTE = "run.json", SAME_RUN_WINDOW_MS = 1e4;
|
|
39136
39488
|
var init_run_record = __esm(() => {
|
|
39489
|
+
init_errors();
|
|
39137
39490
|
init_paths();
|
|
39138
39491
|
init_process_children();
|
|
39139
39492
|
init_runtime_client();
|
|
39140
39493
|
init_windows_ask();
|
|
39141
|
-
execFileAsync =
|
|
39494
|
+
execFileAsync = promisify4(execFile4);
|
|
39142
39495
|
});
|
|
39143
39496
|
|
|
39144
39497
|
// src/server.ts
|
|
39145
|
-
import { execFile as
|
|
39498
|
+
import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
|
|
39146
39499
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
39147
39500
|
import {
|
|
39148
39501
|
appendFileSync,
|
|
39149
39502
|
closeSync as closeSync2,
|
|
39150
39503
|
constants,
|
|
39151
39504
|
copyFileSync,
|
|
39152
|
-
existsSync as
|
|
39505
|
+
existsSync as existsSync17,
|
|
39153
39506
|
fstatSync,
|
|
39154
39507
|
mkdirSync as mkdirSync12,
|
|
39155
39508
|
openSync as openSync3,
|
|
39156
39509
|
readdirSync as readdirSync9,
|
|
39157
|
-
readFileSync as
|
|
39510
|
+
readFileSync as readFileSync20,
|
|
39158
39511
|
readSync as readSync2,
|
|
39159
39512
|
rmdirSync as rmdirSync2,
|
|
39160
39513
|
statSync as statSync8
|
|
39161
39514
|
} from "node:fs";
|
|
39162
|
-
import { basename as basename5, dirname as
|
|
39515
|
+
import { basename as basename5, dirname as dirname16, extname as extname2, isAbsolute as isAbsolute3, join as join23, normalize as normalize2, relative as relative3 } from "node:path";
|
|
39163
39516
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
39164
39517
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
39165
|
-
import { promisify as
|
|
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
|
+
}
|
|
39166
39526
|
function captureDestinationRefusal(outputPath, projects) {
|
|
39167
39527
|
if (/^(res|user):\/\//.test(outputPath)) {
|
|
39168
39528
|
return `outputPath ${outputPath} is a Godot path; a capture is saved outside the project, at an absolute path on this machine.`;
|
|
@@ -39173,16 +39533,16 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
39173
39533
|
if (extname2(outputPath).toLowerCase() !== ".png") {
|
|
39174
39534
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
39175
39535
|
}
|
|
39176
|
-
const directory = realPathOr(
|
|
39177
|
-
if (!
|
|
39178
|
-
return `outputPath's directory ${
|
|
39536
|
+
const directory = realPathOr(dirname16(outputPath));
|
|
39537
|
+
if (!existsSync17(directory)) {
|
|
39538
|
+
return `outputPath's directory ${dirname16(outputPath)} does not exist; a capture creates no directories.`;
|
|
39179
39539
|
}
|
|
39180
|
-
const where =
|
|
39540
|
+
const where = join23(directory, basename5(outputPath));
|
|
39181
39541
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
39182
39542
|
if (inside !== undefined) {
|
|
39183
39543
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
39184
39544
|
}
|
|
39185
|
-
if (
|
|
39545
|
+
if (existsSync17(where)) {
|
|
39186
39546
|
return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
|
|
39187
39547
|
}
|
|
39188
39548
|
return null;
|
|
@@ -39213,16 +39573,18 @@ function describeHalt(halt) {
|
|
|
39213
39573
|
function exited(run) {
|
|
39214
39574
|
return run.exitCode !== null || run.exitSignal !== null;
|
|
39215
39575
|
}
|
|
39216
|
-
function endingOf(failed) {
|
|
39217
|
-
return {
|
|
39218
|
-
exitCode: typeof failed.code === "number" ? failed.code : null,
|
|
39219
|
-
exitSignal: typeof failed.signal === "string" ? failed.signal : null,
|
|
39220
|
-
failure: typeof failed.code === "string" ? failed.code : null
|
|
39221
|
-
};
|
|
39222
|
-
}
|
|
39223
39576
|
function howItExited(run) {
|
|
39224
39577
|
return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}, with no exit code`;
|
|
39225
39578
|
}
|
|
39579
|
+
function errorsUnread(run) {
|
|
39580
|
+
return run.throughEditor && typeof run.errorReport !== "string";
|
|
39581
|
+
}
|
|
39582
|
+
function cleanVerdict(run) {
|
|
39583
|
+
if (run.consoleLost === true || errorsUnread(run)) {
|
|
39584
|
+
return;
|
|
39585
|
+
}
|
|
39586
|
+
return run.log.count("error") === 0;
|
|
39587
|
+
}
|
|
39226
39588
|
function stillRunning(run) {
|
|
39227
39589
|
if (run === null || exited(run) || run.endedUnwatched === true) {
|
|
39228
39590
|
return false;
|
|
@@ -39238,6 +39600,11 @@ function stillRunning(run) {
|
|
|
39238
39600
|
}
|
|
39239
39601
|
return true;
|
|
39240
39602
|
}
|
|
39603
|
+
function playOf(answer) {
|
|
39604
|
+
const play = readNumber(answer, "play");
|
|
39605
|
+
const editorPid = readNumber(answer, "editorPid");
|
|
39606
|
+
return play === undefined || editorPid === undefined ? null : { play, editorPid };
|
|
39607
|
+
}
|
|
39241
39608
|
function runIsUp(run, editorSays, now = Date.now()) {
|
|
39242
39609
|
if (run?.throughEditor !== true) {
|
|
39243
39610
|
return stillRunning(run);
|
|
@@ -39285,11 +39652,19 @@ function stopVerdict(stop) {
|
|
|
39285
39652
|
return {
|
|
39286
39653
|
stopped: true,
|
|
39287
39654
|
endedPid: stop.endedPid,
|
|
39288
|
-
clean: stop.
|
|
39655
|
+
clean: stop.clean,
|
|
39289
39656
|
note: "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running.",
|
|
39290
39657
|
withChildren: false
|
|
39291
39658
|
};
|
|
39292
39659
|
}
|
|
39660
|
+
if (stop.ending === "unreached") {
|
|
39661
|
+
return {
|
|
39662
|
+
stopped: false,
|
|
39663
|
+
notSignalled: stop.endedPid,
|
|
39664
|
+
note: "Nothing was ended: the editor could not be asked to stop the scene it is playing, so the game may still be running. editor_status says whether the editor is connected and playing.",
|
|
39665
|
+
withChildren: false
|
|
39666
|
+
};
|
|
39667
|
+
}
|
|
39293
39668
|
if (stop.ending === "refused") {
|
|
39294
39669
|
return {
|
|
39295
39670
|
stopped: false,
|
|
@@ -39298,22 +39673,31 @@ function stopVerdict(stop) {
|
|
|
39298
39673
|
withChildren: false
|
|
39299
39674
|
};
|
|
39300
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
|
+
}
|
|
39301
39685
|
return {
|
|
39302
39686
|
stopped: true,
|
|
39303
39687
|
endedPid: stop.endedPid,
|
|
39304
|
-
clean: stop.
|
|
39305
|
-
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.`}
|
|
39688
|
+
clean: stop.clean,
|
|
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.`}`,
|
|
39306
39690
|
withChildren: true
|
|
39307
39691
|
};
|
|
39308
39692
|
}
|
|
39309
39693
|
function endedPreviousRun(ended) {
|
|
39310
|
-
if (ended
|
|
39694
|
+
if (ended?.ending !== "gone") {
|
|
39311
39695
|
return;
|
|
39312
39696
|
}
|
|
39313
39697
|
return ended.pid ?? true;
|
|
39314
39698
|
}
|
|
39315
39699
|
function previousRunLeft(ended) {
|
|
39316
|
-
return ended?.ending === "refused" && ended.pid !== null ? ended.pid : undefined;
|
|
39700
|
+
return (ended?.ending === "refused" || ended?.ending === "lingering") && ended.pid !== null ? ended.pid : undefined;
|
|
39317
39701
|
}
|
|
39318
39702
|
function engineNamesOf(executable) {
|
|
39319
39703
|
return CONSOLE_WRAPPER.test(executable) ? [executable, executable.replace(CONSOLE_WRAPPER, "$1")] : [executable];
|
|
@@ -39389,8 +39773,13 @@ function endedToStartThis(ended) {
|
|
|
39389
39773
|
if (ended.ending === "refused") {
|
|
39390
39774
|
return ` ${which} was not ended: that pid no longer answers as the process the run was started as, so nothing was signalled. If it is still the game, it is running beside this one and you have to end it yourself; if it is not, it belongs to something else.`;
|
|
39391
39775
|
}
|
|
39392
|
-
|
|
39393
|
-
|
|
39776
|
+
if (ended.ending === "unreached") {
|
|
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.`;
|
|
39778
|
+
}
|
|
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.`;
|
|
39394
39783
|
}
|
|
39395
39784
|
function runtimeVerdict(endpoint, after) {
|
|
39396
39785
|
if (endpoint !== null) {
|
|
@@ -39514,19 +39903,58 @@ function withScanWait(outcome, scanned) {
|
|
|
39514
39903
|
if (scanned.waitedMs === 0) {
|
|
39515
39904
|
return outcome;
|
|
39516
39905
|
}
|
|
39517
|
-
const said = scanWaitAnswer(scanned);
|
|
39906
|
+
const said = scanWaitAnswer(scanned, "engine");
|
|
39518
39907
|
if (outcome.ok) {
|
|
39519
39908
|
return { ...outcome, payload: { ...outcome.payload, ...said } };
|
|
39520
39909
|
}
|
|
39521
39910
|
return said.scanNote === undefined ? outcome : { ...outcome, message: `${outcome.message} ${said.scanNote}` };
|
|
39522
39911
|
}
|
|
39523
|
-
function
|
|
39912
|
+
function notRunNote(notRun, failFast) {
|
|
39913
|
+
if (notRun <= 0) {
|
|
39914
|
+
return;
|
|
39915
|
+
}
|
|
39916
|
+
const cases = `${notRun} case${notRun === 1 ? "" : "s"}`;
|
|
39917
|
+
return failFast ? `This run stopped at the first failure in each suite it failed in, so ${cases} never ran and count as neither passed nor failed. Leave failFast out to run every case.` : `${cases} counted in ${notRun === 1 ? "its suite" : "their suites"} never ran and count as neither passed nor failed. failFast was not set, so it was not that: the suites below with notRun are the ones to read.`;
|
|
39918
|
+
}
|
|
39919
|
+
function exportAnswer(ending, asked, before, after) {
|
|
39920
|
+
const { log } = ending;
|
|
39921
|
+
const written = after !== null && after !== before;
|
|
39922
|
+
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
39923
|
+
const verdict = {
|
|
39924
|
+
exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
|
|
39925
|
+
...asked,
|
|
39926
|
+
exitCode: ending.exitCode,
|
|
39927
|
+
exitSignal: ending.exitSignal ?? undefined,
|
|
39928
|
+
failure: ending.failure ?? undefined,
|
|
39929
|
+
errors: log.count("error"),
|
|
39930
|
+
warnings: log.count("warning"),
|
|
39931
|
+
entries: forAnswer(problems.entries),
|
|
39932
|
+
...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
|
|
39933
|
+
};
|
|
39934
|
+
if (verdict.exported) {
|
|
39935
|
+
return { content: [{ type: "text", text: JSON.stringify(verdict, null, 2) }] };
|
|
39936
|
+
}
|
|
39937
|
+
const stale = !written && after !== null ? ` The ${asked.outputPath} there is from before this export, which did not write it.` : "";
|
|
39938
|
+
const why = ending.exitSignal === null ? stale : ` The engine was ${howItEnded(ending)}, so it has no exit code.${stale}`;
|
|
39939
|
+
return {
|
|
39940
|
+
content: [
|
|
39941
|
+
{
|
|
39942
|
+
type: "text",
|
|
39943
|
+
text: `Export with preset '${asked.preset}' did not produce ${asked.outputPath}.${why}`
|
|
39944
|
+
},
|
|
39945
|
+
{ type: "text", text: JSON.stringify(verdict, null, 2) }
|
|
39946
|
+
],
|
|
39947
|
+
isError: true
|
|
39948
|
+
};
|
|
39949
|
+
}
|
|
39950
|
+
function scanWaitAnswer(scanned, started = "game") {
|
|
39524
39951
|
if (scanned.waitedMs === 0) {
|
|
39525
39952
|
return {};
|
|
39526
39953
|
}
|
|
39954
|
+
const still = `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so`;
|
|
39527
39955
|
return {
|
|
39528
39956
|
waitedForEditorScanMs: scanned.waitedMs,
|
|
39529
|
-
scanNote: scanned.stillScanning ?
|
|
39957
|
+
scanNote: !scanned.stillScanning ? undefined : started === "game" ? `${still} this game started during the scan and may have read a class cache being rewritten. If ` + 'editor_output shows "Could not find type" for a class that is declared, editor_rescan ' + "waits for the scan to finish, and a start after it reads the finished cache." : `${still} the engine answering this started during the scan and may have read a class cache being ` + "rewritten, and a class cache it wrote can be written over when the scan finishes. If " + 'engine_messages shows "Could not find type" for a class that is declared, editor_rescan waits ' + "for the scan to finish, and the same call after it reads the finished cache."
|
|
39530
39958
|
};
|
|
39531
39959
|
}
|
|
39532
39960
|
function changedSettings(before, after) {
|
|
@@ -39581,16 +40009,17 @@ function importStatuses(payload) {
|
|
|
39581
40009
|
};
|
|
39582
40010
|
});
|
|
39583
40011
|
}
|
|
39584
|
-
function escapingPropertyValue(
|
|
39585
|
-
if (typeof
|
|
40012
|
+
function escapingPropertyValue(value) {
|
|
40013
|
+
if (typeof value === "string") {
|
|
40014
|
+
return /^(res|uid):\/\//.test(value) && value.split(/[/\\]/).includes("..") ? JSON.stringify(value) : undefined;
|
|
40015
|
+
}
|
|
40016
|
+
if (typeof value !== "object" || value === null) {
|
|
39586
40017
|
return;
|
|
39587
40018
|
}
|
|
39588
|
-
for (const
|
|
39589
|
-
|
|
39590
|
-
|
|
39591
|
-
|
|
39592
|
-
if (value.split(/[/\\]/).includes("..")) {
|
|
39593
|
-
return JSON.stringify(value);
|
|
40019
|
+
for (const item of Object.values(value)) {
|
|
40020
|
+
const found = escapingPropertyValue(item);
|
|
40021
|
+
if (found !== undefined) {
|
|
40022
|
+
return found;
|
|
39594
40023
|
}
|
|
39595
40024
|
}
|
|
39596
40025
|
return;
|
|
@@ -39611,7 +40040,7 @@ function aboveTheRunner(entry) {
|
|
|
39611
40040
|
}
|
|
39612
40041
|
function elsewhereIn(projectPath, asked) {
|
|
39613
40042
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
39614
|
-
if (wanted === "" ||
|
|
40043
|
+
if (wanted === "" || existsSync17(join23(projectPath, wanted))) {
|
|
39615
40044
|
return "";
|
|
39616
40045
|
}
|
|
39617
40046
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -39625,7 +40054,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
39625
40054
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
39626
40055
|
}
|
|
39627
40056
|
function hasMainScene(projectFile) {
|
|
39628
|
-
const scene = parseProjectGodot(
|
|
40057
|
+
const scene = parseProjectGodot(readFileSync20(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
39629
40058
|
return typeof scene === "string" && scene !== "";
|
|
39630
40059
|
}
|
|
39631
40060
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -39633,13 +40062,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
39633
40062
|
return false;
|
|
39634
40063
|
}
|
|
39635
40064
|
try {
|
|
39636
|
-
return
|
|
40065
|
+
return readFileSync20(join23(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
39637
40066
|
} catch {
|
|
39638
40067
|
return false;
|
|
39639
40068
|
}
|
|
39640
40069
|
}
|
|
39641
40070
|
function editorPlaysHeadless(projectFile) {
|
|
39642
|
-
const runArgs = parseProjectGodot(
|
|
40071
|
+
const runArgs = parseProjectGodot(readFileSync20(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
39643
40072
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
39644
40073
|
}
|
|
39645
40074
|
function camelCased(params) {
|
|
@@ -39658,7 +40087,7 @@ function needsItsOwnProcess(asked) {
|
|
|
39658
40087
|
class GodotServer {
|
|
39659
40088
|
mcp;
|
|
39660
40089
|
locator = new GodotLocator;
|
|
39661
|
-
operationsScript =
|
|
40090
|
+
operationsScript = join23(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
39662
40091
|
godotBridge;
|
|
39663
40092
|
tools = buildToolDefinitions();
|
|
39664
40093
|
activeProcess = null;
|
|
@@ -39680,6 +40109,7 @@ class GodotServer {
|
|
|
39680
40109
|
shutdownInitiated = false;
|
|
39681
40110
|
launchedEditor = null;
|
|
39682
40111
|
launchedFrom = null;
|
|
40112
|
+
launchedDropped = null;
|
|
39683
40113
|
ownProject;
|
|
39684
40114
|
announcedAt = null;
|
|
39685
40115
|
handingOverFrom = null;
|
|
@@ -39687,9 +40117,12 @@ class GodotServer {
|
|
|
39687
40117
|
constructor() {
|
|
39688
40118
|
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
39689
40119
|
this.godotBridge = getDefaultBridge(this.ownProject);
|
|
40120
|
+
this.godotBridge.on("godot_connected", () => {
|
|
40121
|
+
this.settleTheLaunchReading();
|
|
40122
|
+
});
|
|
39690
40123
|
this.mcp = new McpServer({ name: "gdharness", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
39691
40124
|
this.setupToolHandlers();
|
|
39692
|
-
setupResourceHandlers(this.mcp, () => this.lastProjectPath);
|
|
40125
|
+
setupResourceHandlers(this.mcp, () => this.ourProject() ?? this.lastProjectPath);
|
|
39693
40126
|
this.mcp.server.onerror = (error) => {
|
|
39694
40127
|
console.error("[MCP Error]", error);
|
|
39695
40128
|
};
|
|
@@ -39955,8 +40388,9 @@ class GodotServer {
|
|
|
39955
40388
|
if (!checked.ok) {
|
|
39956
40389
|
return checked.response;
|
|
39957
40390
|
}
|
|
39958
|
-
|
|
39959
|
-
|
|
40391
|
+
const named = args["projectPath"];
|
|
40392
|
+
if (typeof named === "string" && existsSync17(join23(named, "project.godot"))) {
|
|
40393
|
+
this.lastProjectPath = named;
|
|
39960
40394
|
}
|
|
39961
40395
|
this.updates.refresh();
|
|
39962
40396
|
const answer = await withCallSignal(extra.signal, () => this.answered(spec.name, checked.op ?? "", args));
|
|
@@ -39996,6 +40430,17 @@ class GodotServer {
|
|
|
39996
40430
|
return answer;
|
|
39997
40431
|
}
|
|
39998
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
|
+
}
|
|
39999
40444
|
if (moved !== null) {
|
|
40000
40445
|
this.noticedUpdate = true;
|
|
40001
40446
|
this.callsSinceNotice = 0;
|
|
@@ -40028,7 +40473,7 @@ class GodotServer {
|
|
|
40028
40473
|
return this.saying(answer, {
|
|
40029
40474
|
update_available: {
|
|
40030
40475
|
...notice,
|
|
40031
|
-
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."
|
|
40032
40477
|
}
|
|
40033
40478
|
});
|
|
40034
40479
|
}
|
|
@@ -40045,6 +40490,11 @@ class GodotServer {
|
|
|
40045
40490
|
const installed = installedAddonVersion(this.ownProject);
|
|
40046
40491
|
return installed === null || installed === SERVER_VERSION ? null : installed;
|
|
40047
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
|
+
}
|
|
40048
40498
|
configDisagreement() {
|
|
40049
40499
|
return this.ownProject === null ? null : configDisagrees(this.ownProject, SERVER_VERSION);
|
|
40050
40500
|
}
|
|
@@ -40158,6 +40608,9 @@ class GodotServer {
|
|
|
40158
40608
|
const contained = this.containProjectFiles(asks);
|
|
40159
40609
|
return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
|
|
40160
40610
|
}
|
|
40611
|
+
if (headless === "create_script") {
|
|
40612
|
+
return await this.handleCreateScript(answerable);
|
|
40613
|
+
}
|
|
40161
40614
|
if (headless !== "refresh_class_cache") {
|
|
40162
40615
|
return await this.headless(headless, answerable);
|
|
40163
40616
|
}
|
|
@@ -40328,7 +40781,7 @@ class GodotServer {
|
|
|
40328
40781
|
return await this.handleRuntimeCommand("click", {
|
|
40329
40782
|
...whichGame(args),
|
|
40330
40783
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
40331
|
-
button:
|
|
40784
|
+
button: args["button"] ?? "left",
|
|
40332
40785
|
double: readBoolean(args, "doubleClick") ?? false,
|
|
40333
40786
|
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
40334
40787
|
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
@@ -40400,8 +40853,8 @@ class GodotServer {
|
|
|
40400
40853
|
if (path === undefined) {
|
|
40401
40854
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
40402
40855
|
}
|
|
40403
|
-
const file =
|
|
40404
|
-
if (!
|
|
40856
|
+
const file = join23(path, "project.godot");
|
|
40857
|
+
if (!existsSync17(file)) {
|
|
40405
40858
|
return {
|
|
40406
40859
|
ok: false,
|
|
40407
40860
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -40611,13 +41064,30 @@ class GodotServer {
|
|
|
40611
41064
|
}
|
|
40612
41065
|
contained[key] = `res://${location.relativePath}`;
|
|
40613
41066
|
}
|
|
40614
|
-
const escaping = escapingPropertyValue(args["properties"]);
|
|
41067
|
+
const escaping = escapingPropertyValue([args["properties"], args["track"]]);
|
|
40615
41068
|
if (escaping !== undefined) {
|
|
40616
41069
|
return {
|
|
40617
41070
|
ok: false,
|
|
40618
41071
|
response: this.createErrorResponse(`The property value ${escaping} resolves outside the project directory.`, PATH_SOLUTIONS)
|
|
40619
41072
|
};
|
|
40620
41073
|
}
|
|
41074
|
+
const sources = args["sources"];
|
|
41075
|
+
if (Array.isArray(sources)) {
|
|
41076
|
+
const resolved = [];
|
|
41077
|
+
for (const source of sources) {
|
|
41078
|
+
const texture = typeof source === "object" && source !== null ? source["texture"] : undefined;
|
|
41079
|
+
if (typeof texture !== "string") {
|
|
41080
|
+
resolved.push(source);
|
|
41081
|
+
continue;
|
|
41082
|
+
}
|
|
41083
|
+
const location = resolveWithinProject(projectPath, texture);
|
|
41084
|
+
if (!location.ok) {
|
|
41085
|
+
return { ok: false, response: this.createErrorResponse(location.reason, PATH_SOLUTIONS) };
|
|
41086
|
+
}
|
|
41087
|
+
resolved.push({ ...source, texture: `res://${location.relativePath}` });
|
|
41088
|
+
}
|
|
41089
|
+
contained["sources"] = resolved;
|
|
41090
|
+
}
|
|
40621
41091
|
const pluginName = readNonEmptyString(args, "pluginName");
|
|
40622
41092
|
if (pluginName !== undefined) {
|
|
40623
41093
|
const plugin = resolveWithinProject(projectPath, `addons/${pluginName}/plugin.cfg`);
|
|
@@ -40665,6 +41135,7 @@ class GodotServer {
|
|
|
40665
41135
|
return this.answer(withScanWait({
|
|
40666
41136
|
ok: true,
|
|
40667
41137
|
messages: imported.messages,
|
|
41138
|
+
...imported.messagesOmitted === undefined ? {} : { messagesOmitted: imported.messagesOmitted },
|
|
40668
41139
|
payload: {
|
|
40669
41140
|
uidsCreated: given,
|
|
40670
41141
|
stillWithoutUid: after,
|
|
@@ -40715,16 +41186,25 @@ class GodotServer {
|
|
|
40715
41186
|
if (changed.length === 0) {
|
|
40716
41187
|
return written;
|
|
40717
41188
|
}
|
|
40718
|
-
const
|
|
41189
|
+
const adopting = {};
|
|
41190
|
+
const taken = await this.editorTakes("adopt_project_settings", { settings: changed }, adopting);
|
|
41191
|
+
if (taken["editorNote"] !== undefined) {
|
|
41192
|
+
return this.jsonTextResponse({ ...answer, ...taken });
|
|
41193
|
+
}
|
|
41194
|
+
const adopted = Array.isArray(adopting["adopted"]) ? adopting["adopted"].map(String) : [];
|
|
41195
|
+
const passedOver = changed.filter((name) => !adopted.includes(name));
|
|
40719
41196
|
return this.jsonTextResponse({
|
|
40720
41197
|
...answer,
|
|
40721
|
-
|
|
40722
|
-
...
|
|
41198
|
+
editorAdopted: adopted,
|
|
41199
|
+
...passedOver.length > 0 ? { editorPassedOver: passedOver } : {}
|
|
40723
41200
|
});
|
|
40724
41201
|
}
|
|
40725
|
-
async editorTakes(command, args) {
|
|
41202
|
+
async editorTakes(command, args, answered = {}) {
|
|
40726
41203
|
try {
|
|
40727
|
-
await this.godotBridge.invokeTool(command, args);
|
|
41204
|
+
const result = await this.godotBridge.invokeTool(command, args);
|
|
41205
|
+
if (typeof result === "object" && result !== null) {
|
|
41206
|
+
Object.assign(answered, result);
|
|
41207
|
+
}
|
|
40728
41208
|
return {};
|
|
40729
41209
|
} catch (error) {
|
|
40730
41210
|
const message = errorMessage(error);
|
|
@@ -40762,7 +41242,19 @@ class GodotServer {
|
|
|
40762
41242
|
}
|
|
40763
41243
|
const applied = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
|
|
40764
41244
|
if (!applied.ok) {
|
|
40765
|
-
|
|
41245
|
+
const listed = written.payload["updated_options"];
|
|
41246
|
+
const updated = Array.isArray(listed) ? listed.map(String) : [];
|
|
41247
|
+
const which = updated.length === 0 ? "The options were" : `${updated.join(", ")} ${updated.length === 1 ? "was" : "were"}`;
|
|
41248
|
+
return {
|
|
41249
|
+
...applied.response,
|
|
41250
|
+
content: [
|
|
41251
|
+
{
|
|
41252
|
+
type: "text",
|
|
41253
|
+
text: `${which} written to the import file, and the reimport that applies them failed, so the resource is still imported the old way until the editor's next scan or project_import reimport imports it. Why the reimport failed:`
|
|
41254
|
+
},
|
|
41255
|
+
...applied.response.content
|
|
41256
|
+
]
|
|
41257
|
+
};
|
|
40766
41258
|
}
|
|
40767
41259
|
return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
|
|
40768
41260
|
}
|
|
@@ -40780,7 +41272,14 @@ class GodotServer {
|
|
|
40780
41272
|
response: this.createErrorResponse(`${missingSource[0]} is not on disk, so there is nothing to import.`)
|
|
40781
41273
|
};
|
|
40782
41274
|
}
|
|
40783
|
-
const
|
|
41275
|
+
const never = listed.find((one) => one.status === "not_imported");
|
|
41276
|
+
if (resourcePath !== undefined && never !== undefined) {
|
|
41277
|
+
return {
|
|
41278
|
+
ok: false,
|
|
41279
|
+
response: this.createErrorResponse(`${never.path} is not something the engine imports: ${never.reason ?? "it has no importer"}.`)
|
|
41280
|
+
};
|
|
41281
|
+
}
|
|
41282
|
+
const targets = listed.filter((one) => one.status !== "missing_source" && one.status !== "not_imported" && (force || one.status !== "up_to_date")).map((one) => one.path);
|
|
40784
41283
|
if (targets.length === 0) {
|
|
40785
41284
|
return {
|
|
40786
41285
|
ok: true,
|
|
@@ -40819,9 +41318,7 @@ class GodotServer {
|
|
|
40819
41318
|
if (!imported.ok) {
|
|
40820
41319
|
return { ok: false, response: this.answer(withScanWait(imported, scanned)) };
|
|
40821
41320
|
}
|
|
40822
|
-
|
|
40823
|
-
extra["engine_messages"] = imported.messages;
|
|
40824
|
-
}
|
|
41321
|
+
Object.assign(extra, engineExtras(imported));
|
|
40825
41322
|
if (imported.extensionNote !== undefined) {
|
|
40826
41323
|
extra["extensionNote"] = imported.extensionNote;
|
|
40827
41324
|
}
|
|
@@ -40904,17 +41401,15 @@ class GodotServer {
|
|
|
40904
41401
|
return withScanWait(outcome, scanned);
|
|
40905
41402
|
}
|
|
40906
41403
|
answer(outcome) {
|
|
41404
|
+
const extras = engineExtras(outcome);
|
|
40907
41405
|
if (!outcome.ok) {
|
|
40908
41406
|
const response = this.createErrorResponse(outcome.message);
|
|
40909
|
-
if (
|
|
40910
|
-
response.content.push({
|
|
40911
|
-
type: "text",
|
|
40912
|
-
text: JSON.stringify({ engine_messages: outcome.messages }, null, 2)
|
|
40913
|
-
});
|
|
41407
|
+
if (Object.keys(extras).length > 0) {
|
|
41408
|
+
response.content.push({ type: "text", text: JSON.stringify(extras, null, 2) });
|
|
40914
41409
|
}
|
|
40915
41410
|
return response;
|
|
40916
41411
|
}
|
|
40917
|
-
return this.jsonTextResponse(
|
|
41412
|
+
return this.jsonTextResponse({ ...outcome.payload, ...extras });
|
|
40918
41413
|
}
|
|
40919
41414
|
async handleProjectInfo(args) {
|
|
40920
41415
|
const include = readStringArray(args, "include") ?? [];
|
|
@@ -40930,7 +41425,7 @@ class GodotServer {
|
|
|
40930
41425
|
if (!engine.ok) {
|
|
40931
41426
|
return engine.response;
|
|
40932
41427
|
}
|
|
40933
|
-
const application = parseProjectGodot(
|
|
41428
|
+
const application = parseProjectGodot(readFileSync20(project.value.file, "utf8"))["application"] ?? {};
|
|
40934
41429
|
const name = application["config/name"];
|
|
40935
41430
|
const mainScene = application["run/main_scene"];
|
|
40936
41431
|
const info = {
|
|
@@ -40947,10 +41442,14 @@ class GodotServer {
|
|
|
40947
41442
|
continue;
|
|
40948
41443
|
}
|
|
40949
41444
|
const outcome = await this.operation(spec.operation, spec.params(args, detailed), project.value.path);
|
|
40950
|
-
info[section] = outcome.ok ?
|
|
41445
|
+
info[section] = outcome.ok ? { ...outcome.payload, ...engineExtras(outcome) } : { error: outcome.message, ...engineExtras(outcome) };
|
|
40951
41446
|
}
|
|
40952
41447
|
return this.jsonTextResponse(info);
|
|
40953
41448
|
}
|
|
41449
|
+
fileTypes(args) {
|
|
41450
|
+
const named = readStringArray(args, "fileTypes");
|
|
41451
|
+
return named === undefined ? {} : { fileTypes: named };
|
|
41452
|
+
}
|
|
40954
41453
|
handleSearchProject(args) {
|
|
40955
41454
|
const project = this.project(args);
|
|
40956
41455
|
if (!project.ok) {
|
|
@@ -40959,16 +41458,7 @@ class GodotServer {
|
|
|
40959
41458
|
try {
|
|
40960
41459
|
return this.jsonTextResponse(searchProject(project.value.path, {
|
|
40961
41460
|
query: readString(args, "query") ?? "",
|
|
40962
|
-
fileTypes
|
|
40963
|
-
"gd",
|
|
40964
|
-
"tscn",
|
|
40965
|
-
"tres",
|
|
40966
|
-
"gdshader",
|
|
40967
|
-
"cfg",
|
|
40968
|
-
"md",
|
|
40969
|
-
"txt",
|
|
40970
|
-
"json"
|
|
40971
|
-
],
|
|
41461
|
+
...this.fileTypes(args),
|
|
40972
41462
|
regex: readBoolean(args, "regex") ?? false,
|
|
40973
41463
|
caseSensitive: readBoolean(args, "caseSensitive") ?? false,
|
|
40974
41464
|
maxResults: readPositiveNumber(args, "maxResults") ?? 100
|
|
@@ -40991,18 +41481,18 @@ class GodotServer {
|
|
|
40991
41481
|
if (!engine.ok) {
|
|
40992
41482
|
return engine.response;
|
|
40993
41483
|
}
|
|
40994
|
-
const folder =
|
|
41484
|
+
const folder = dirname16(output.absolutePath);
|
|
40995
41485
|
try {
|
|
40996
41486
|
mkdirSync12(folder, { recursive: true });
|
|
40997
41487
|
} catch (error) {
|
|
40998
|
-
return this.createErrorResponse(`Cannot create ${
|
|
41488
|
+
return this.createErrorResponse(`Cannot create ${dirname16(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
40999
41489
|
}
|
|
41000
41490
|
const debug = readBoolean(args, "debug") ?? false;
|
|
41001
41491
|
const exportLogs = scratchDirectory("export");
|
|
41002
41492
|
const exportArgs = [
|
|
41003
41493
|
"--headless",
|
|
41004
41494
|
"--log-file",
|
|
41005
|
-
|
|
41495
|
+
join23(exportLogs, "engine.log"),
|
|
41006
41496
|
"--path",
|
|
41007
41497
|
project.value.path,
|
|
41008
41498
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -41010,55 +41500,18 @@ class GodotServer {
|
|
|
41010
41500
|
output.absolutePath
|
|
41011
41501
|
];
|
|
41012
41502
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
41013
|
-
const
|
|
41014
|
-
|
|
41503
|
+
const writtenAt = () => existsSync17(output.absolutePath) ? statSync8(output.absolutePath).mtimeMs : null;
|
|
41504
|
+
const before = writtenAt();
|
|
41505
|
+
let ending;
|
|
41015
41506
|
try {
|
|
41016
|
-
|
|
41017
|
-
timeout: 300000,
|
|
41018
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
41019
|
-
signal: callSignal()
|
|
41020
|
-
});
|
|
41021
|
-
log.append("stdout", stdout);
|
|
41022
|
-
log.append("stderr", stderr);
|
|
41023
|
-
} catch (error) {
|
|
41024
|
-
if (!(error instanceof Error && ("stdout" in error) && ("stderr" in error))) {
|
|
41025
|
-
return this.createErrorResponse(`Export could not be run: ${errorMessage(error)}`);
|
|
41026
|
-
}
|
|
41027
|
-
const failed = error;
|
|
41028
|
-
log.append("stdout", failed.stdout);
|
|
41029
|
-
log.append("stderr", failed.stderr);
|
|
41030
|
-
ending = endingOf(failed);
|
|
41507
|
+
ending = await runEngine(engine.value, exportArgs, { timeout: { ms: 300000, said: "five minutes" } });
|
|
41031
41508
|
} finally {
|
|
41032
41509
|
discard(exportLogs);
|
|
41033
41510
|
}
|
|
41034
|
-
|
|
41035
|
-
|
|
41036
|
-
const verdict = {
|
|
41037
|
-
exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync15(output.absolutePath),
|
|
41038
|
-
preset,
|
|
41039
|
-
outputPath: output.relativePath,
|
|
41040
|
-
debug,
|
|
41041
|
-
exitCode: ending.exitCode,
|
|
41042
|
-
exitSignal: ending.exitSignal ?? undefined,
|
|
41043
|
-
failure: ending.failure ?? undefined,
|
|
41044
|
-
errors: log.count("error"),
|
|
41045
|
-
warnings: log.count("warning"),
|
|
41046
|
-
entries: forAnswer(problems.entries)
|
|
41047
|
-
};
|
|
41048
|
-
if (!verdict.exported) {
|
|
41049
|
-
const why = ending.exitSignal === null ? ending.failure === null ? "" : ` The engine was ended by Node (${ending.failure}), so it has no exit code.` : ` The engine was ended by ${ending.exitSignal}, so it has no exit code: the export ran past its five minutes or the call was cancelled.`;
|
|
41050
|
-
return {
|
|
41051
|
-
content: [
|
|
41052
|
-
{
|
|
41053
|
-
type: "text",
|
|
41054
|
-
text: `Export with preset '${preset}' did not produce ${output.relativePath}.${why}`
|
|
41055
|
-
},
|
|
41056
|
-
{ type: "text", text: JSON.stringify(verdict, null, 2) }
|
|
41057
|
-
],
|
|
41058
|
-
isError: true
|
|
41059
|
-
};
|
|
41511
|
+
if (ending.exitSignal === null && ending.exitCode === null) {
|
|
41512
|
+
return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
|
|
41060
41513
|
}
|
|
41061
|
-
return
|
|
41514
|
+
return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt());
|
|
41062
41515
|
}
|
|
41063
41516
|
async handleRunTests(args) {
|
|
41064
41517
|
const project = this.project(args);
|
|
@@ -41070,7 +41523,7 @@ class GodotServer {
|
|
|
41070
41523
|
return contained.response;
|
|
41071
41524
|
}
|
|
41072
41525
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
41073
|
-
if (!
|
|
41526
|
+
if (!existsSync17(join23(project.value.path, runner))) {
|
|
41074
41527
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
41075
41528
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
41076
41529
|
]);
|
|
@@ -41136,21 +41589,22 @@ class GodotServer {
|
|
|
41136
41589
|
if (timedOut && run.process.pid !== undefined) {
|
|
41137
41590
|
await untilGone([run.process.pid]);
|
|
41138
41591
|
}
|
|
41139
|
-
|
|
41592
|
+
await run.settled;
|
|
41593
|
+
const reportsDir = join23(project.value.path, ".godot", ...ours.split("/"));
|
|
41140
41594
|
let report = null;
|
|
41141
41595
|
let reportProblem = null;
|
|
41142
41596
|
try {
|
|
41143
|
-
const written =
|
|
41597
|
+
const written = existsSync17(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
41144
41598
|
const newest = written.at(-1);
|
|
41145
41599
|
if (newest !== undefined) {
|
|
41146
|
-
report = parseJUnit(
|
|
41600
|
+
report = parseJUnit(readFileSync20(join23(reportsDir, newest, "results.xml"), "utf8"));
|
|
41147
41601
|
}
|
|
41148
41602
|
} catch (error) {
|
|
41149
41603
|
reportProblem = errorMessage(error);
|
|
41150
41604
|
} finally {
|
|
41151
41605
|
discard(reportsDir, userData);
|
|
41152
41606
|
try {
|
|
41153
|
-
rmdirSync2(
|
|
41607
|
+
rmdirSync2(dirname16(reportsDir));
|
|
41154
41608
|
} catch {}
|
|
41155
41609
|
}
|
|
41156
41610
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
@@ -41163,7 +41617,11 @@ class GodotServer {
|
|
|
41163
41617
|
105: "script errors"
|
|
41164
41618
|
};
|
|
41165
41619
|
const exitCode = run.exitCode;
|
|
41166
|
-
const printed = run.log.select({
|
|
41620
|
+
const printed = run.log.select({
|
|
41621
|
+
severity: "info",
|
|
41622
|
+
sinceLastCall: false,
|
|
41623
|
+
limit: Number.POSITIVE_INFINITY
|
|
41624
|
+
}).entries;
|
|
41167
41625
|
const said = printed.map((entry) => entry.text);
|
|
41168
41626
|
const nothingRan = timedOut ? null : whyNoReport(said, asked);
|
|
41169
41627
|
const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
|
|
@@ -41179,7 +41637,7 @@ class GodotServer {
|
|
|
41179
41637
|
...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
|
|
41180
41638
|
};
|
|
41181
41639
|
if (report === null) {
|
|
41182
|
-
const note = nothingRan !== null ? `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : `The test run
|
|
41640
|
+
const note = nothingRan !== null ? `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${verdict}.` : `The test run wrote no report (${verdict}).`;
|
|
41183
41641
|
return {
|
|
41184
41642
|
content: [
|
|
41185
41643
|
{ type: "text", text: note },
|
|
@@ -41216,10 +41674,21 @@ class GodotServer {
|
|
|
41216
41674
|
orphans: suite.orphans,
|
|
41217
41675
|
message: `${suite.orphans} node${suite.orphans === 1 ? "" : "s"} were still in the tree when this suite finished. A node built in a test and neither freed nor added to the tree is the usual cause; free it, or add it with auto_free().`
|
|
41218
41676
|
}));
|
|
41219
|
-
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
|
|
41677
|
+
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.hookFailures > 0 || suite.hookErrors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
|
|
41678
|
+
const hooksPrinted = hookFailuresPrinted(said);
|
|
41679
|
+
const hookFailures = report.suites.filter((suite) => suite.hookFailures + suite.hookErrors > 0).flatMap((suite) => {
|
|
41680
|
+
const printedHere = hooksPrinted.filter((hook) => hook.path === suite.path);
|
|
41681
|
+
return printedHere.length > 0 ? printedHere.map((hook) => ({ suite: suite.name, ...hook })) : [
|
|
41682
|
+
{
|
|
41683
|
+
suite: suite.name,
|
|
41684
|
+
path: suite.path,
|
|
41685
|
+
message: "gdUnit4 counted a failure in this suite that none of its cases carry, which is what a failed before() or after() leaves, and printed no line saying which."
|
|
41686
|
+
}
|
|
41687
|
+
];
|
|
41688
|
+
});
|
|
41220
41689
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
41221
41690
|
return this.jsonTextResponse({
|
|
41222
|
-
passed: !timedOut && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
41691
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0,
|
|
41223
41692
|
verdict,
|
|
41224
41693
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
41225
41694
|
exitCode,
|
|
@@ -41230,13 +41699,14 @@ class GodotServer {
|
|
|
41230
41699
|
skipped: report.skipped,
|
|
41231
41700
|
time: report.time,
|
|
41232
41701
|
failed,
|
|
41702
|
+
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
41233
41703
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
41234
41704
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
41235
41705
|
notRun: notRun > 0 ? notRun : undefined,
|
|
41236
41706
|
...printedLines,
|
|
41237
41707
|
savesNote,
|
|
41238
41708
|
...scanned,
|
|
41239
|
-
note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRun
|
|
41709
|
+
note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true),
|
|
41240
41710
|
suites: unclean.map((suite) => ({
|
|
41241
41711
|
name: suite.name,
|
|
41242
41712
|
path: suite.path,
|
|
@@ -41244,6 +41714,7 @@ class GodotServer {
|
|
|
41244
41714
|
notRun: suite.discovered > suite.tests ? suite.discovered - suite.tests : undefined,
|
|
41245
41715
|
failures: suite.failures,
|
|
41246
41716
|
errors: suite.errors,
|
|
41717
|
+
hookFailures: suite.hookFailures + suite.hookErrors > 0 ? suite.hookFailures + suite.hookErrors : undefined,
|
|
41247
41718
|
skipped: suite.skipped,
|
|
41248
41719
|
orphans: orphansIn(suite.path) > 0 ? orphansIn(suite.path) : undefined,
|
|
41249
41720
|
time: suite.time
|
|
@@ -41257,7 +41728,7 @@ class GodotServer {
|
|
|
41257
41728
|
}
|
|
41258
41729
|
async godotVersion(godotPath) {
|
|
41259
41730
|
try {
|
|
41260
|
-
const { stdout } = await
|
|
41731
|
+
const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
|
|
41261
41732
|
this.lastGodotVersion = stdout.trim();
|
|
41262
41733
|
return this.lastGodotVersion;
|
|
41263
41734
|
} catch (error) {
|
|
@@ -41269,8 +41740,18 @@ class GodotServer {
|
|
|
41269
41740
|
const answer = await this.handleLSP("lsp_get_diagnostics", args);
|
|
41270
41741
|
const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
41271
41742
|
if (payload["error"] !== undefined) {
|
|
41272
|
-
const reason = payload["error"];
|
|
41273
|
-
|
|
41743
|
+
const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
|
|
41744
|
+
if (payload["refusedArguments"] === true) {
|
|
41745
|
+
return this.createErrorResponse(reason);
|
|
41746
|
+
}
|
|
41747
|
+
if (typeof payload["serves"] === "string") {
|
|
41748
|
+
return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
41749
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
41750
|
+
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
41751
|
+
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
41752
|
+
]);
|
|
41753
|
+
}
|
|
41754
|
+
return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
41274
41755
|
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
41275
41756
|
"GDHARNESS_LSP_PORT points this server at another one"
|
|
41276
41757
|
]);
|
|
@@ -41288,7 +41769,7 @@ class GodotServer {
|
|
|
41288
41769
|
warnings: diagnostics.length - errors,
|
|
41289
41770
|
diagnostics,
|
|
41290
41771
|
...disagrees
|
|
41291
|
-
}, 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()));
|
|
41292
41773
|
}
|
|
41293
41774
|
whereTheFileDisagrees(diagnostics, args) {
|
|
41294
41775
|
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
|
@@ -41301,11 +41782,11 @@ class GodotServer {
|
|
|
41301
41782
|
const cached = cachedClasses(projectPath);
|
|
41302
41783
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
41303
41784
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
41304
|
-
if (!contained.ok || !
|
|
41785
|
+
if (!contained.ok || !existsSync17(contained.absolutePath)) {
|
|
41305
41786
|
return null;
|
|
41306
41787
|
}
|
|
41307
41788
|
try {
|
|
41308
|
-
return
|
|
41789
|
+
return readFileSync20(contained.absolutePath, "utf8");
|
|
41309
41790
|
} catch {
|
|
41310
41791
|
return null;
|
|
41311
41792
|
}
|
|
@@ -41361,11 +41842,11 @@ class GodotServer {
|
|
|
41361
41842
|
}
|
|
41362
41843
|
sourceOfClass(projectPath, resourcePath) {
|
|
41363
41844
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
41364
|
-
if (!contained.ok || !
|
|
41845
|
+
if (!contained.ok || !existsSync17(contained.absolutePath)) {
|
|
41365
41846
|
return null;
|
|
41366
41847
|
}
|
|
41367
41848
|
try {
|
|
41368
|
-
return
|
|
41849
|
+
return readFileSync20(contained.absolutePath, "utf8");
|
|
41369
41850
|
} catch {
|
|
41370
41851
|
return null;
|
|
41371
41852
|
}
|
|
@@ -41439,7 +41920,7 @@ class GodotServer {
|
|
|
41439
41920
|
}
|
|
41440
41921
|
breakpointsAtRisk() {
|
|
41441
41922
|
const status = this.godotBridge.getStatus();
|
|
41442
|
-
if (!status
|
|
41923
|
+
if (!hasSaidWhoItIs(status) || status.syncsBreakpoints === true) {
|
|
41443
41924
|
return {};
|
|
41444
41925
|
}
|
|
41445
41926
|
return {
|
|
@@ -41494,26 +41975,34 @@ class GodotServer {
|
|
|
41494
41975
|
restartSettled(this.ownProject);
|
|
41495
41976
|
return null;
|
|
41496
41977
|
}
|
|
41497
|
-
return owed;
|
|
41978
|
+
return { note: owed, underway: restartUnderway(owed) };
|
|
41498
41979
|
}
|
|
41499
41980
|
async getEditorStatusPayload() {
|
|
41500
41981
|
const status = this.godotBridge.getStatus();
|
|
41501
41982
|
const launchedIsUp = !status.connected && await this.launchedEditorIsUp();
|
|
41502
41983
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
41503
|
-
const
|
|
41984
|
+
const greeted = hasSaidWhoItIs(status);
|
|
41985
|
+
const stale = greeted && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
41504
41986
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
41505
41987
|
return {
|
|
41506
41988
|
...status,
|
|
41507
41989
|
serverVersion: SERVER_VERSION,
|
|
41508
|
-
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,
|
|
41509
41992
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
41510
|
-
mayYetConnect: status.connected ? undefined : unfinished !== null ?
|
|
41511
|
-
restartInterrupted: unfinished === null ? undefined : {
|
|
41512
|
-
quitEditorPid: unfinished.editorPid,
|
|
41513
|
-
quitAt: unfinished.quitAt,
|
|
41514
|
-
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,
|
|
41515
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."
|
|
41516
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
|
+
},
|
|
41517
42006
|
awaitingLaunchedEditor: status.connected || !launchedIsUp ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
41518
42007
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
41519
42008
|
startupError: this.bridgeStartupError,
|
|
@@ -41523,8 +42012,9 @@ class GodotServer {
|
|
|
41523
42012
|
port: this.godotBridge.configuredPort,
|
|
41524
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.`
|
|
41525
42014
|
},
|
|
41526
|
-
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
|
|
41527
|
-
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,
|
|
41528
42018
|
...this.breakpointsAtRisk(),
|
|
41529
42019
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
41530
42020
|
announcedAt: this.announcedAt ?? undefined,
|
|
@@ -41553,7 +42043,8 @@ class GodotServer {
|
|
|
41553
42043
|
return {
|
|
41554
42044
|
playing: readBoolean(asParams(answer), "playing") ?? false,
|
|
41555
42045
|
scene: readString(asParams(answer), "scenePath") ?? "",
|
|
41556
|
-
debugPort: readNumber(asParams(answer), "debugPort")
|
|
42046
|
+
debugPort: readNumber(asParams(answer), "debugPort"),
|
|
42047
|
+
play: playOf(asParams(answer))
|
|
41557
42048
|
};
|
|
41558
42049
|
} catch {
|
|
41559
42050
|
return null;
|
|
@@ -41649,7 +42140,7 @@ class GodotServer {
|
|
|
41649
42140
|
return null;
|
|
41650
42141
|
}
|
|
41651
42142
|
try {
|
|
41652
|
-
return settingKeys(
|
|
42143
|
+
return settingKeys(readFileSync20(join23(projectPath, "project.godot"), "utf8"));
|
|
41653
42144
|
} catch {
|
|
41654
42145
|
return null;
|
|
41655
42146
|
}
|
|
@@ -41662,17 +42153,33 @@ class GodotServer {
|
|
|
41662
42153
|
"editor_status says whether the bridge is up and what has reached it"
|
|
41663
42154
|
]);
|
|
41664
42155
|
}
|
|
41665
|
-
|
|
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
|
+
}
|
|
41666
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);
|
|
41667
42174
|
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, args["hidden"] === true) : await this.handleViaBridge("restart_editor", {});
|
|
41668
42175
|
if (asked.isError === true) {
|
|
41669
42176
|
return asked;
|
|
41670
42177
|
}
|
|
41671
42178
|
const startedAt = before.connectedAt?.getTime() ?? 0;
|
|
41672
42179
|
const began = Date.now();
|
|
41673
|
-
const back = await this.waitForBridge(() => theEditorHasComeBack(this.godotBridge.getStatus(), startedAt), began +
|
|
42180
|
+
const back = await this.waitForBridge(() => theEditorHasComeBack(this.godotBridge.getStatus(), startedAt), began + editorRestartTimeoutMs());
|
|
41674
42181
|
if (!back) {
|
|
41675
|
-
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.`, [
|
|
41676
42183
|
"It may be asking what to do about an unsaved scene: look at the editor window",
|
|
41677
42184
|
"An addon that no longer parses stops the editor reaching this server",
|
|
41678
42185
|
"editor_status says whether anything has reached the bridge since"
|
|
@@ -41686,18 +42193,27 @@ class GodotServer {
|
|
|
41686
42193
|
addonVersion: now.addonVersion,
|
|
41687
42194
|
serverVersion: SERVER_VERSION,
|
|
41688
42195
|
addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
|
|
41689
|
-
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
|
|
42196
|
+
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()),
|
|
41690
42197
|
...this.whatTheEditorDropped(settingsBefore, after),
|
|
41691
42198
|
tookMs: Date.now() - began
|
|
41692
42199
|
});
|
|
41693
42200
|
}
|
|
41694
|
-
|
|
42201
|
+
settleTheLaunchReading() {
|
|
41695
42202
|
const watched = this.launchedFrom;
|
|
41696
|
-
|
|
41697
|
-
|
|
42203
|
+
const status = this.godotBridge.getStatus();
|
|
42204
|
+
if (watched === null || !hasSaidWhoItIs(status)) {
|
|
42205
|
+
return;
|
|
41698
42206
|
}
|
|
41699
42207
|
this.launchedFrom = null;
|
|
41700
|
-
|
|
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;
|
|
41701
42217
|
}
|
|
41702
42218
|
whatTheEditorDropped(before, after) {
|
|
41703
42219
|
return settingsDroppedReport(before, after);
|
|
@@ -41723,7 +42239,14 @@ class GodotServer {
|
|
|
41723
42239
|
restartSettled(projectPath);
|
|
41724
42240
|
return asked;
|
|
41725
42241
|
}
|
|
41726
|
-
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
|
+
}
|
|
41727
42250
|
const opened = await this.openAnEditor(engine.value, projectPath, ports, hidden);
|
|
41728
42251
|
restartSettled(projectPath);
|
|
41729
42252
|
if (opened.error !== null) {
|
|
@@ -41754,6 +42277,10 @@ class GodotServer {
|
|
|
41754
42277
|
"Another project wants its own gdharness server, which is its own harness session"
|
|
41755
42278
|
]);
|
|
41756
42279
|
}
|
|
42280
|
+
const refused = this.anEditorThatCannotConnect(project.value.path);
|
|
42281
|
+
if (refused !== null) {
|
|
42282
|
+
return refused;
|
|
42283
|
+
}
|
|
41757
42284
|
const engine = await this.engine();
|
|
41758
42285
|
if (!engine.ok) {
|
|
41759
42286
|
return engine.response;
|
|
@@ -41778,6 +42305,27 @@ class GodotServer {
|
|
|
41778
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."
|
|
41779
42306
|
});
|
|
41780
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
|
+
}
|
|
41781
42329
|
async openAnEditor(engine, projectPath, ports, hidden) {
|
|
41782
42330
|
clearEditorLog(projectPath);
|
|
41783
42331
|
const launched = await launchOutsideTheTree({
|
|
@@ -41812,6 +42360,13 @@ class GodotServer {
|
|
|
41812
42360
|
if (!project.ok) {
|
|
41813
42361
|
return project.response;
|
|
41814
42362
|
}
|
|
42363
|
+
const open = this.godotBridge.isConnected() ? this.godotBridge.getStatus().projectPath : undefined;
|
|
42364
|
+
if (op !== "check" && open !== undefined && open !== "" && !isSameDirectory(open, project.value.path)) {
|
|
42365
|
+
return this.createErrorResponse(`This start names ${project.value.path}, and the editor on this bridge has ${open} open.`, [
|
|
42366
|
+
"editor_status names the project this server's editor is showing",
|
|
42367
|
+
"One server serves one editor: start a second server for the other project"
|
|
42368
|
+
]);
|
|
42369
|
+
}
|
|
41815
42370
|
const scene = readNonEmptyString(args, "scene");
|
|
41816
42371
|
const sceneToRun = scene !== undefined ? resolveWithinProject(project.value.path, scene) : null;
|
|
41817
42372
|
if (sceneToRun && !sceneToRun.ok) {
|
|
@@ -41906,7 +42461,7 @@ class GodotServer {
|
|
|
41906
42461
|
}
|
|
41907
42462
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
41908
42463
|
const budgetMs = budget.ms;
|
|
41909
|
-
if (!
|
|
42464
|
+
if (!existsSync17(join23(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
41910
42465
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
41911
42466
|
}
|
|
41912
42467
|
let asking = null;
|
|
@@ -41915,7 +42470,7 @@ class GodotServer {
|
|
|
41915
42470
|
let editorAsks = 0;
|
|
41916
42471
|
const waitBegan = Date.now();
|
|
41917
42472
|
const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
|
|
41918
|
-
const
|
|
42473
|
+
const fresh = await announcedSince(projectPath, before, {
|
|
41919
42474
|
budgetMs,
|
|
41920
42475
|
accept: (one) => this.isTheGameOf(this.activeProcess, one, projectPath),
|
|
41921
42476
|
giveUp: () => {
|
|
@@ -41939,11 +42494,19 @@ class GodotServer {
|
|
|
41939
42494
|
}
|
|
41940
42495
|
});
|
|
41941
42496
|
const asked = editorAsks === 0 ? "editor not asked" : `editor asked ${editorAsks === 1 ? "once" : `${editorAsks} times`}, last said ${lastSaid()}`;
|
|
41942
|
-
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${
|
|
42497
|
+
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${fresh.length === 0 ? "nothing announced" : `pid ${fresh.map((one) => one.pid).join(", ")} announced`}, ${asked}`);
|
|
41943
42498
|
const going = await this.currentRun();
|
|
42499
|
+
const endpoint = await this.theRunsAmong(going, fresh);
|
|
41944
42500
|
if (endpoint !== null && going !== null && going.announcedPid === undefined) {
|
|
41945
42501
|
this.tie(going, endpoint);
|
|
41946
42502
|
}
|
|
42503
|
+
if (endpoint === null && fresh.length > 1) {
|
|
42504
|
+
return {
|
|
42505
|
+
listening: true,
|
|
42506
|
+
pids: fresh.map((one) => one.pid),
|
|
42507
|
+
note: `${fresh.length} games of this project announced at once and none could be told from the process tree as this run's own game rather than one it started, so none is taken as its runtime. editor_status lists them under runtimes.`
|
|
42508
|
+
};
|
|
42509
|
+
}
|
|
41947
42510
|
return runtimeVerdict(endpoint, {
|
|
41948
42511
|
addon: true,
|
|
41949
42512
|
budgetMs,
|
|
@@ -42027,7 +42590,13 @@ class GodotServer {
|
|
|
42027
42590
|
};
|
|
42028
42591
|
this.activeProcess = played;
|
|
42029
42592
|
this.watchThePlayedConsole();
|
|
42030
|
-
|
|
42593
|
+
const which = playOf(playAnswer);
|
|
42594
|
+
writeEditorRunNote({
|
|
42595
|
+
projectPath,
|
|
42596
|
+
transcript: transcript.path,
|
|
42597
|
+
startedAt,
|
|
42598
|
+
...which === null ? {} : { play: which }
|
|
42599
|
+
});
|
|
42031
42600
|
sweepTranscripts();
|
|
42032
42601
|
return this.jsonTextResponse({
|
|
42033
42602
|
started: true,
|
|
@@ -42142,7 +42711,7 @@ class GodotServer {
|
|
|
42142
42711
|
return 0;
|
|
42143
42712
|
}
|
|
42144
42713
|
try {
|
|
42145
|
-
return reportAlreadyInTranscript(
|
|
42714
|
+
return reportAlreadyInTranscript(readFileSync20(game.transcript), readFileSync20(report));
|
|
42146
42715
|
} catch (error) {
|
|
42147
42716
|
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
42148
42717
|
return 0;
|
|
@@ -42156,6 +42725,18 @@ class GodotServer {
|
|
|
42156
42725
|
game.brokeOn = halt.text;
|
|
42157
42726
|
game.log.record("error", halt.text);
|
|
42158
42727
|
}
|
|
42728
|
+
async untilTheEditorStopsPlaying(withinMs = STOP_WAIT_MS) {
|
|
42729
|
+
const deadline = Date.now() + withinMs;
|
|
42730
|
+
for (;; ) {
|
|
42731
|
+
if ((await this.editorPlayingState())?.playing === false) {
|
|
42732
|
+
return true;
|
|
42733
|
+
}
|
|
42734
|
+
if (Date.now() >= deadline) {
|
|
42735
|
+
return false;
|
|
42736
|
+
}
|
|
42737
|
+
await delay3(100);
|
|
42738
|
+
}
|
|
42739
|
+
}
|
|
42159
42740
|
async endActiveGame(reason, going) {
|
|
42160
42741
|
const running = this.activeProcess;
|
|
42161
42742
|
const project = running?.projectPath ?? this.ourProject();
|
|
@@ -42174,8 +42755,16 @@ class GodotServer {
|
|
|
42174
42755
|
running.endedHere = reason;
|
|
42175
42756
|
const announced = this.announcedPidOf(running);
|
|
42176
42757
|
if (running.throughEditor) {
|
|
42177
|
-
await this.handleViaBridge("stop_playing", {});
|
|
42178
|
-
|
|
42758
|
+
const asked = await this.handleViaBridge("stop_playing", {});
|
|
42759
|
+
if (asked.isError === true) {
|
|
42760
|
+
running.endedHere = null;
|
|
42761
|
+
running.log.record("warning", `This run was not ended here: the editor could not be asked to stop it (${asked.content[0]?.text ?? "no reason given"}).`);
|
|
42762
|
+
return "unreached";
|
|
42763
|
+
}
|
|
42764
|
+
if (announced === undefined) {
|
|
42765
|
+
return await this.untilTheEditorStopsPlaying() ? "gone" : "lingering";
|
|
42766
|
+
}
|
|
42767
|
+
return await untilGone([announced]) ? "gone" : "lingering";
|
|
42179
42768
|
}
|
|
42180
42769
|
if (running.pid === null) {
|
|
42181
42770
|
return "gone";
|
|
@@ -42184,23 +42773,30 @@ class GodotServer {
|
|
|
42184
42773
|
pid: running.pid,
|
|
42185
42774
|
transcript: running.transcript ?? "",
|
|
42186
42775
|
startedAt: running.startedAt,
|
|
42776
|
+
...running.startedBy === undefined ? {} : { startedBy: running.startedBy },
|
|
42187
42777
|
projectPath: running.projectPath ?? "",
|
|
42188
42778
|
arguments: [],
|
|
42189
42779
|
...running.command === undefined ? {} : { command: running.command }
|
|
42190
42780
|
};
|
|
42191
|
-
|
|
42781
|
+
const why = await whyNotStillTheRecordedRun(startedAs);
|
|
42782
|
+
if (why !== null) {
|
|
42192
42783
|
clearTheRecord();
|
|
42193
42784
|
running.endedHere = null;
|
|
42194
|
-
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.`);
|
|
42195
42786
|
return "refused";
|
|
42196
42787
|
}
|
|
42197
42788
|
const signalled = await runningAs(running.pid);
|
|
42198
42789
|
const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
|
|
42199
42790
|
let landed = true;
|
|
42791
|
+
let declined = null;
|
|
42200
42792
|
try {
|
|
42201
42793
|
process.kill(running.pid);
|
|
42202
|
-
} catch {
|
|
42794
|
+
} catch (error) {
|
|
42203
42795
|
landed = false;
|
|
42796
|
+
const code = error.code;
|
|
42797
|
+
if (code !== "ESRCH") {
|
|
42798
|
+
declined = code ?? errorMessage(error);
|
|
42799
|
+
}
|
|
42204
42800
|
}
|
|
42205
42801
|
const gone = await untilGone([running.pid]);
|
|
42206
42802
|
if (gone) {
|
|
@@ -42211,11 +42807,11 @@ class GodotServer {
|
|
|
42211
42807
|
running.exitSignal = "SIGTERM";
|
|
42212
42808
|
}
|
|
42213
42809
|
clearTheRecord();
|
|
42214
|
-
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}.`);
|
|
42215
42811
|
return gone ? "gone" : "lingering";
|
|
42216
42812
|
}
|
|
42217
42813
|
spawnGame(godotPath, cmdArgs, env) {
|
|
42218
|
-
const child =
|
|
42814
|
+
const child = spawn4(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
42219
42815
|
const cancelled = callSignal();
|
|
42220
42816
|
if (cancelled !== undefined) {
|
|
42221
42817
|
const end = () => {
|
|
@@ -42255,7 +42851,6 @@ class GodotServer {
|
|
|
42255
42851
|
});
|
|
42256
42852
|
child.on("exit", (code, signal) => {
|
|
42257
42853
|
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
42258
|
-
log.finish();
|
|
42259
42854
|
started.exitCode = code;
|
|
42260
42855
|
started.exitSignal = code === null ? signal : null;
|
|
42261
42856
|
});
|
|
@@ -42265,7 +42860,28 @@ class GodotServer {
|
|
|
42265
42860
|
`);
|
|
42266
42861
|
started.exitCode = -1;
|
|
42267
42862
|
});
|
|
42268
|
-
|
|
42863
|
+
const settled = new Promise((resolve) => {
|
|
42864
|
+
let open = 2;
|
|
42865
|
+
const done = () => {
|
|
42866
|
+
log.finish();
|
|
42867
|
+
resolve();
|
|
42868
|
+
};
|
|
42869
|
+
const closed = () => {
|
|
42870
|
+
open -= 1;
|
|
42871
|
+
if (open === 0) {
|
|
42872
|
+
done();
|
|
42873
|
+
}
|
|
42874
|
+
};
|
|
42875
|
+
child.stdout.once("close", closed);
|
|
42876
|
+
child.stderr.once("close", closed);
|
|
42877
|
+
child.once("exit", () => {
|
|
42878
|
+
setTimeout(done, STREAMS_GRACE_MS).unref();
|
|
42879
|
+
});
|
|
42880
|
+
child.once("error", () => {
|
|
42881
|
+
done();
|
|
42882
|
+
});
|
|
42883
|
+
});
|
|
42884
|
+
return Object.assign(started, { settled });
|
|
42269
42885
|
}
|
|
42270
42886
|
async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
|
|
42271
42887
|
const startedAt = Date.now();
|
|
@@ -42275,12 +42891,13 @@ class GodotServer {
|
|
|
42275
42891
|
command: godotPath,
|
|
42276
42892
|
args: cmdArgs,
|
|
42277
42893
|
...env === undefined ? {} : { env },
|
|
42278
|
-
run: { transcript: transcript.path, startedAt, projectPath },
|
|
42894
|
+
run: { transcript: transcript.path, startedAt, projectPath, ...this.servedBy() },
|
|
42279
42895
|
...desktop === undefined ? {} : { desktop }
|
|
42280
42896
|
});
|
|
42281
42897
|
if ("error" in launched) {
|
|
42282
42898
|
return launched;
|
|
42283
42899
|
}
|
|
42900
|
+
const startedBy = Date.now();
|
|
42284
42901
|
sweepTranscripts();
|
|
42285
42902
|
return {
|
|
42286
42903
|
pid: launched.pid,
|
|
@@ -42289,6 +42906,7 @@ class GodotServer {
|
|
|
42289
42906
|
readOffset: 0,
|
|
42290
42907
|
projectPath,
|
|
42291
42908
|
startedAt,
|
|
42909
|
+
startedBy,
|
|
42292
42910
|
command: godotPath,
|
|
42293
42911
|
exitCode: null,
|
|
42294
42912
|
exitSignal: null,
|
|
@@ -42347,7 +42965,8 @@ class GodotServer {
|
|
|
42347
42965
|
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
42348
42966
|
}
|
|
42349
42967
|
const project = this.godotBridge.getStatus().projectPath ?? null;
|
|
42350
|
-
const
|
|
42968
|
+
const recorded = project === null ? null : readEditorRunNote(project);
|
|
42969
|
+
const note = recorded !== null && noteIsAbout(recorded, playing.play) ? recorded : null;
|
|
42351
42970
|
const picked = {
|
|
42352
42971
|
pid: null,
|
|
42353
42972
|
log: new GameLog,
|
|
@@ -42404,6 +43023,31 @@ class GodotServer {
|
|
|
42404
43023
|
const its = await this.tiedThroughTheTree(run, running);
|
|
42405
43024
|
return its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
|
|
42406
43025
|
}
|
|
43026
|
+
async theRunsAmong(run, fresh) {
|
|
43027
|
+
if (fresh.length === 1) {
|
|
43028
|
+
return fresh[0] ?? null;
|
|
43029
|
+
}
|
|
43030
|
+
if (fresh.length === 0 || run === null) {
|
|
43031
|
+
return null;
|
|
43032
|
+
}
|
|
43033
|
+
const pid = run.announcedPid ?? await this.tiedThroughTheTree(run, fresh);
|
|
43034
|
+
return fresh.find((one) => one.pid === pid) ?? null;
|
|
43035
|
+
}
|
|
43036
|
+
async tieThePlayedGame(run) {
|
|
43037
|
+
if (!run.throughEditor || run.errorReport !== undefined || run.announcedPid !== undefined) {
|
|
43038
|
+
return;
|
|
43039
|
+
}
|
|
43040
|
+
const announced = runtimesAnnounced().running;
|
|
43041
|
+
if (this.announcedPidOf(run, announced) !== undefined || !stillRunning(run)) {
|
|
43042
|
+
return;
|
|
43043
|
+
}
|
|
43044
|
+
const among = this.announcedOfTheRunsProject(run, announced).map((one) => one.pid).sort((one, other) => one - other).join(",");
|
|
43045
|
+
if (among === "" || run.treeReadFor === among) {
|
|
43046
|
+
return;
|
|
43047
|
+
}
|
|
43048
|
+
run.treeReadFor = among;
|
|
43049
|
+
await this.tiedThroughTheTree(run, announced);
|
|
43050
|
+
}
|
|
42407
43051
|
async tiedThroughTheTree(run, announced) {
|
|
42408
43052
|
if (run.announcedPid !== undefined) {
|
|
42409
43053
|
return run.announcedPid;
|
|
@@ -42482,8 +43126,9 @@ class GodotServer {
|
|
|
42482
43126
|
}
|
|
42483
43127
|
async adoptRecordedRun() {
|
|
42484
43128
|
const project = this.ourProject();
|
|
42485
|
-
const
|
|
42486
|
-
|
|
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 ?? ""))) {
|
|
42487
43132
|
return null;
|
|
42488
43133
|
}
|
|
42489
43134
|
const adopted = {
|
|
@@ -42493,6 +43138,7 @@ class GodotServer {
|
|
|
42493
43138
|
readOffset: 0,
|
|
42494
43139
|
projectPath: record.projectPath === "" ? null : record.projectPath,
|
|
42495
43140
|
startedAt: record.startedAt,
|
|
43141
|
+
...record.startedBy === undefined ? {} : { startedBy: record.startedBy },
|
|
42496
43142
|
...record.command === undefined ? {} : { command: record.command },
|
|
42497
43143
|
exitCode: null,
|
|
42498
43144
|
exitSignal: null,
|
|
@@ -42513,6 +43159,13 @@ class GodotServer {
|
|
|
42513
43159
|
this.activeProcess = adopted;
|
|
42514
43160
|
return adopted;
|
|
42515
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
|
+
}
|
|
42516
43169
|
nothingOfOursIsRunning() {
|
|
42517
43170
|
const status = this.godotBridge.getStatus();
|
|
42518
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." : "";
|
|
@@ -42604,6 +43257,7 @@ class GodotServer {
|
|
|
42604
43257
|
resolve(false);
|
|
42605
43258
|
});
|
|
42606
43259
|
});
|
|
43260
|
+
await boot.settled;
|
|
42607
43261
|
const errors = boot.log.count("error");
|
|
42608
43262
|
const warnings = boot.log.count("warning");
|
|
42609
43263
|
return this.jsonTextResponse({
|
|
@@ -42678,22 +43332,20 @@ class GodotServer {
|
|
|
42678
43332
|
}
|
|
42679
43333
|
async handleEditorConsole(args) {
|
|
42680
43334
|
const status = this.godotBridge.getStatus();
|
|
42681
|
-
const
|
|
42682
|
-
|
|
42683
|
-
|
|
42684
|
-
|
|
42685
|
-
"editor_launch opens one, and an editor opened here has its console captured"
|
|
42686
|
-
]);
|
|
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"]);
|
|
42687
43339
|
}
|
|
42688
|
-
const
|
|
42689
|
-
if (!
|
|
43340
|
+
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
43341
|
+
if (projectPath === undefined || !greeted && launched === null) {
|
|
42690
43342
|
return this.createErrorResponse("No editor is connected, so there is no console to read.", [
|
|
42691
43343
|
"editor_status says whether one is connected and whether it may yet be",
|
|
42692
43344
|
"editor_launch opens one, and an editor opened here has its console captured"
|
|
42693
43345
|
]);
|
|
42694
43346
|
}
|
|
42695
|
-
const editorPid =
|
|
42696
|
-
const console2 = editorConsole(projectPath, editorPid,
|
|
43347
|
+
const editorPid = greeted ? status.editorPid ?? null : launched;
|
|
43348
|
+
const console2 = editorConsole(projectPath, editorPid, greeted ? status.openedByAServer === true : true);
|
|
42697
43349
|
if ("kind" in console2) {
|
|
42698
43350
|
return this.createErrorResponse(theConsoleWasNotCaptured(console2, projectPath), [
|
|
42699
43351
|
"editor_launch restart replaces this editor with one whose console is captured",
|
|
@@ -42745,6 +43397,7 @@ class GodotServer {
|
|
|
42745
43397
|
if (!run) {
|
|
42746
43398
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
42747
43399
|
}
|
|
43400
|
+
await this.tieThePlayedGame(run);
|
|
42748
43401
|
this.drainEditorOutput(run);
|
|
42749
43402
|
this.drainTranscript(run);
|
|
42750
43403
|
const going = await this.runStillGoing(run);
|
|
@@ -42780,7 +43433,7 @@ class GodotServer {
|
|
|
42780
43433
|
notes.push(endedWithoutACode({
|
|
42781
43434
|
throughEditor: run.throughEditor,
|
|
42782
43435
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
42783
|
-
couldAnnounce: run.projectPath !== null &&
|
|
43436
|
+
couldAnnounce: run.projectPath !== null && existsSync17(join23(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
42784
43437
|
endedHere: typeof run.endedHere === "string",
|
|
42785
43438
|
pickedUp: run.pickedUp === true
|
|
42786
43439
|
}));
|
|
@@ -42788,6 +43441,9 @@ class GodotServer {
|
|
|
42788
43441
|
if (run.pickedUpPlaying === true) {
|
|
42789
43442
|
notes.push(run.transcript !== null ? "The editor was already playing this when this server reached it, and what it printed before that has been read back from its transcript. It is the run the editor is holding now, which is the one the debug_* tools answer for." : run.consoleFromReport === true ? "The editor was already playing this when this server reached it, and the server that started it left no transcript, so what is below was read back from the file the game's runtime writes its console to, which begins where the runtime started: the engine's own start-up before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for." : "The editor was already playing this when this server reached it, and the server that started it left no transcript, so this is not the run from its start: what it printed before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for.");
|
|
42790
43443
|
}
|
|
43444
|
+
if (errorsUnread(run)) {
|
|
43445
|
+
notes.push("The editor plays this run and no report from its game's runtime addon has been found, so what the game raised with push_error and the engine's own errors are not counted here: the editor's debugger relays what a game prints and not what it reports. errors counts what was heard, and clean is left out rather than said. A game with the runtime addon writes that report once it announces itself.");
|
|
43446
|
+
}
|
|
42791
43447
|
if (run.consoleLost === true) {
|
|
42792
43448
|
const back = this.dapClient?.isConnected() === true;
|
|
42793
43449
|
notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
|
|
@@ -42803,8 +43459,9 @@ class GodotServer {
|
|
|
42803
43459
|
pid: run.pid ?? announced ?? run.announcedPid ?? null,
|
|
42804
43460
|
errors: run.log.count("error"),
|
|
42805
43461
|
warnings: run.log.count("warning"),
|
|
42806
|
-
clean: run
|
|
43462
|
+
clean: cleanVerdict(run),
|
|
42807
43463
|
consoleLost: run.consoleLost === true ? true : undefined,
|
|
43464
|
+
errorsUnread: errorsUnread(run) ? true : undefined,
|
|
42808
43465
|
...hold,
|
|
42809
43466
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
42810
43467
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
@@ -42839,6 +43496,7 @@ class GodotServer {
|
|
|
42839
43496
|
if (elsewhere !== null) {
|
|
42840
43497
|
return elsewhere;
|
|
42841
43498
|
}
|
|
43499
|
+
await this.tieThePlayedGame(stopped);
|
|
42842
43500
|
this.drainEditorOutput(stopped);
|
|
42843
43501
|
this.drainTranscript(stopped);
|
|
42844
43502
|
const wasRunning = await this.runStillGoing(stopped);
|
|
@@ -42847,7 +43505,7 @@ class GodotServer {
|
|
|
42847
43505
|
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
42848
43506
|
this.logDebug("Stopping the running game");
|
|
42849
43507
|
const ending = await this.endActiveGame("editor_run stop", wasRunning);
|
|
42850
|
-
const refused = ending === "refused";
|
|
43508
|
+
const refused = ending === "refused" || ending === "unreached";
|
|
42851
43509
|
const ended = children === null || refused ? null : endChildrenAmong(children);
|
|
42852
43510
|
if (wasRunning && !refused) {
|
|
42853
43511
|
for (const pid of new Set([endedPid, announcedPid])) {
|
|
@@ -42862,13 +43520,14 @@ class GodotServer {
|
|
|
42862
43520
|
endedPid,
|
|
42863
43521
|
throughEditor: stopped.throughEditor,
|
|
42864
43522
|
exitSignal: stopped.exitSignal,
|
|
42865
|
-
|
|
43523
|
+
clean: cleanVerdict(stopped)
|
|
42866
43524
|
});
|
|
42867
43525
|
return this.jsonTextResponse({
|
|
42868
43526
|
stopped: verdict.stopped,
|
|
42869
43527
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
42870
43528
|
endedPid: verdict.endedPid,
|
|
42871
43529
|
notSignalled: verdict.notSignalled,
|
|
43530
|
+
stillGoing: verdict.stillGoing,
|
|
42872
43531
|
...childrenFields(ended),
|
|
42873
43532
|
exitedBeforeStop: !wasRunning,
|
|
42874
43533
|
exitCode: stopped.exitCode,
|
|
@@ -42876,6 +43535,7 @@ class GodotServer {
|
|
|
42876
43535
|
errors: stopped.log.count("error"),
|
|
42877
43536
|
warnings: stopped.log.count("warning"),
|
|
42878
43537
|
clean: verdict.clean,
|
|
43538
|
+
errorsUnread: errorsUnread(stopped) ? true : undefined,
|
|
42879
43539
|
note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
|
|
42880
43540
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
42881
43541
|
});
|
|
@@ -42925,12 +43585,13 @@ class GodotServer {
|
|
|
42925
43585
|
await announcementEnded(pid);
|
|
42926
43586
|
}
|
|
42927
43587
|
return this.jsonTextResponse({
|
|
42928
|
-
stopped:
|
|
43588
|
+
stopped: gone,
|
|
42929
43589
|
through: "gdharness",
|
|
42930
|
-
endedPid: pid,
|
|
43590
|
+
endedPid: gone ? pid : undefined,
|
|
43591
|
+
stillGoing: gone ? undefined : pid,
|
|
42931
43592
|
...childrenFields(ended),
|
|
42932
43593
|
exitedBeforeStop: !landed,
|
|
42933
|
-
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)}`
|
|
42934
43595
|
});
|
|
42935
43596
|
}
|
|
42936
43597
|
async whatTheRunStarted(run) {
|
|
@@ -43077,7 +43738,8 @@ class GodotServer {
|
|
|
43077
43738
|
}
|
|
43078
43739
|
}
|
|
43079
43740
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
43080
|
-
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));
|
|
43081
43743
|
if (lost.length > 0) {
|
|
43082
43744
|
this.noteAShortListEditor(projectPath);
|
|
43083
43745
|
}
|
|
@@ -43088,7 +43750,12 @@ class GodotServer {
|
|
|
43088
43750
|
const stillAtMissingPaths = atMissingPaths.filter((name) => !dropped.includes(name));
|
|
43089
43751
|
const reloading = readNonEmptyString(args, "reloadScript");
|
|
43090
43752
|
let reloaded = {};
|
|
43091
|
-
|
|
43753
|
+
const reloadingSource = reloading === undefined || projectPath === "" ? null : this.sourceOfClass(projectPath, reloading);
|
|
43754
|
+
if (reloading !== undefined && reloadingSource !== null && isToolScript(reloadingSource)) {
|
|
43755
|
+
reloaded = {
|
|
43756
|
+
problem: `${reloading} is a @tool script, which runs inside the editor, so it is not reloaded from inside a call: editor_launch restart recompiles it.`
|
|
43757
|
+
};
|
|
43758
|
+
} else if (reloading !== undefined) {
|
|
43092
43759
|
try {
|
|
43093
43760
|
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
|
|
43094
43761
|
const methods = readArray(answer, "methods");
|
|
@@ -43096,29 +43763,36 @@ class GodotServer {
|
|
|
43096
43763
|
const found = readArray(answer, field);
|
|
43097
43764
|
return found ? { [field]: found.map(String) } : {};
|
|
43098
43765
|
};
|
|
43766
|
+
const asResource = (path) => path.startsWith("res://") ? path : `res://${path.replace(/^\/+/, "")}`;
|
|
43767
|
+
const script = asResource(readString(answer, "script") ?? reloading);
|
|
43768
|
+
const spelling = script === asResource(reloading) ? {} : { as: script };
|
|
43099
43769
|
const failed = answer["failed"];
|
|
43100
43770
|
if (typeof failed === "number" && failed !== 0) {
|
|
43101
43771
|
reloaded = {
|
|
43772
|
+
...spelling,
|
|
43102
43773
|
...listed("heldBefore"),
|
|
43103
43774
|
...listed("heldConstantsBefore"),
|
|
43104
43775
|
problem: failedReloadNote({
|
|
43105
|
-
script
|
|
43776
|
+
script,
|
|
43106
43777
|
code: failed,
|
|
43107
43778
|
codeName: readString(answer, "failedAs") ?? "",
|
|
43108
43779
|
heldBefore: listed("heldBefore")["heldBefore"] ?? [],
|
|
43109
43780
|
heldAfter: methods?.map(String) ?? [],
|
|
43110
43781
|
constantsBefore: listed("heldConstantsBefore")["heldConstantsBefore"] ?? [],
|
|
43111
43782
|
constantsAfter: listed("constants")["constants"] ?? [],
|
|
43112
|
-
names: projectPath === "" ? [] : this.classesAScriptNames(projectPath,
|
|
43783
|
+
names: projectPath === "" ? [] : this.classesAScriptNames(projectPath, script),
|
|
43784
|
+
restored: typeof answer["restored"] === "boolean" ? answer["restored"] : null,
|
|
43785
|
+
restoredAs: readString(answer, "restoredAs") ?? ""
|
|
43113
43786
|
})
|
|
43114
43787
|
};
|
|
43115
43788
|
} else {
|
|
43116
43789
|
reloaded = methods ? {
|
|
43790
|
+
...spelling,
|
|
43117
43791
|
methods: methods.map(String),
|
|
43118
43792
|
...listed("heldBefore"),
|
|
43119
43793
|
...listed("constants"),
|
|
43120
43794
|
...listed("heldConstantsBefore")
|
|
43121
|
-
} : { problem: `the editor would not say what ${
|
|
43795
|
+
} : { problem: `the editor would not say what ${script} has after reloading it` };
|
|
43122
43796
|
}
|
|
43123
43797
|
} catch (error) {
|
|
43124
43798
|
reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
|
|
@@ -43126,7 +43800,7 @@ class GodotServer {
|
|
|
43126
43800
|
}
|
|
43127
43801
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
43128
43802
|
const unseen = checked.unseen;
|
|
43129
|
-
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));
|
|
43130
43804
|
const dependents = busy || projectPath === "" || broughtIn.length === 0 ? null : await this.reloadDependents(projectPath, broughtIn);
|
|
43131
43805
|
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
43132
43806
|
const notes = [];
|
|
@@ -43140,7 +43814,7 @@ class GodotServer {
|
|
|
43140
43814
|
notes.push("The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Rescan again on its own, which is the measured cure and needs no change to the declaring script, or restart the editor with editor_launch restart.");
|
|
43141
43815
|
}
|
|
43142
43816
|
if (dependents !== null && dependents.notReloaded.length > 0) {
|
|
43143
|
-
notes.push(
|
|
43817
|
+
notes.push(notReloadedNote(broughtIn, dependents.notReloaded));
|
|
43144
43818
|
}
|
|
43145
43819
|
for (const group of uidsDuplicatedOnDisk) {
|
|
43146
43820
|
notes.push(`${group.join(" and ")} name one UID in their .uid files, so Godot keeps one path for it and every engine warns "UID duplicate detected". Delete the .uid of the one that is a copy and rescan: the editor writes it a UID of its own.`);
|
|
@@ -43155,6 +43829,7 @@ class GodotServer {
|
|
|
43155
43829
|
stillWorking: busy,
|
|
43156
43830
|
waitedMs: Date.now() - started,
|
|
43157
43831
|
waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
|
|
43832
|
+
reloadedAs: reloaded.as,
|
|
43158
43833
|
heldBeforeReload: reloaded.heldBefore,
|
|
43159
43834
|
reloadedMethods: reloaded.methods,
|
|
43160
43835
|
heldConstantsBeforeReload: reloaded.heldConstantsBefore,
|
|
@@ -43181,7 +43856,8 @@ class GodotServer {
|
|
|
43181
43856
|
if (tool) {
|
|
43182
43857
|
notReloaded.push({
|
|
43183
43858
|
scriptPath,
|
|
43184
|
-
problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it"
|
|
43859
|
+
problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it",
|
|
43860
|
+
tool: true
|
|
43185
43861
|
});
|
|
43186
43862
|
continue;
|
|
43187
43863
|
}
|
|
@@ -43189,9 +43865,10 @@ class GodotServer {
|
|
|
43189
43865
|
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { scriptPath }));
|
|
43190
43866
|
const failed = answer["failed"];
|
|
43191
43867
|
if (typeof failed === "number" && failed !== 0) {
|
|
43868
|
+
const left = answer["restored"] === true ? "" : answer["restored"] === false ? "; compiling it again from the text it was built from failed too, so the editor holds it unusable until it compiles" : "; a failed compile leaves the copy the editor holds unusable until it compiles, and this editor does not put it back";
|
|
43192
43869
|
notReloaded.push({
|
|
43193
43870
|
scriptPath,
|
|
43194
|
-
problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}`
|
|
43871
|
+
problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}${left}`
|
|
43195
43872
|
});
|
|
43196
43873
|
} else if (readArray(answer, "methods") === undefined) {
|
|
43197
43874
|
notReloaded.push({
|
|
@@ -43263,6 +43940,35 @@ class GodotServer {
|
|
|
43263
43940
|
}
|
|
43264
43941
|
};
|
|
43265
43942
|
}
|
|
43943
|
+
async handleCreateScript(args) {
|
|
43944
|
+
const project = this.project(args);
|
|
43945
|
+
if (!project.ok) {
|
|
43946
|
+
return project.response;
|
|
43947
|
+
}
|
|
43948
|
+
const contained = this.containProjectFiles(args);
|
|
43949
|
+
if (!contained.ok) {
|
|
43950
|
+
return contained.response;
|
|
43951
|
+
}
|
|
43952
|
+
const { op: _op, projectPath: _projectPath, ...params } = contained.value;
|
|
43953
|
+
const created = await this.operation("create_script", params, project.value.path);
|
|
43954
|
+
const declared = created.ok ? readString(created.payload, "class_name") ?? "" : "";
|
|
43955
|
+
if (!created.ok || declared === "") {
|
|
43956
|
+
return this.answer(created);
|
|
43957
|
+
}
|
|
43958
|
+
const rebuilt = await this.rebuildClassCache(project.value.path);
|
|
43959
|
+
const registered = cachedClasses(project.value.path)?.get(declared) === readString(created.payload, "full_path");
|
|
43960
|
+
const answered = this.answer({
|
|
43961
|
+
...created,
|
|
43962
|
+
payload: {
|
|
43963
|
+
...created.payload,
|
|
43964
|
+
registered,
|
|
43965
|
+
...registered ? {} : {
|
|
43966
|
+
registrationNote: rebuilt.ok ? `The class list was rebuilt and does not hold ${declared} at this script, so nothing started next resolves it by name. project_import refresh_classes says what the list holds and why.` : `The class list could not be rebuilt, so nothing started next resolves ${declared} by name: ${rebuilt.message}`
|
|
43967
|
+
}
|
|
43968
|
+
}
|
|
43969
|
+
});
|
|
43970
|
+
return this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
43971
|
+
}
|
|
43266
43972
|
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
43267
43973
|
const first = answered.content[0];
|
|
43268
43974
|
if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
|
|
@@ -43335,7 +44041,7 @@ class GodotServer {
|
|
|
43335
44041
|
]);
|
|
43336
44042
|
}
|
|
43337
44043
|
try {
|
|
43338
|
-
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()));
|
|
43339
44045
|
} catch (error) {
|
|
43340
44046
|
return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
|
|
43341
44047
|
}
|
|
@@ -43391,7 +44097,7 @@ class GodotServer {
|
|
|
43391
44097
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
43392
44098
|
const run = await this.currentRun();
|
|
43393
44099
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
43394
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
44100
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync17(join23(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
43395
44101
|
return null;
|
|
43396
44102
|
}
|
|
43397
44103
|
const already = runtimesAnnounced().running;
|
|
@@ -43415,11 +44121,15 @@ class GodotServer {
|
|
|
43415
44121
|
const seconds = Math.round((Date.now() - run.startedAt) / 1000);
|
|
43416
44122
|
return `The game this server started (${which}) has been running for ${seconds}s and has not announced its runtime, so there is nothing to talk to. A game that keeps the runtime off, as a project's own loader can for some scenes, never announces; editor_output says what it has printed.`;
|
|
43417
44123
|
}
|
|
43418
|
-
const
|
|
44124
|
+
const fresh = await announcedSince(project, new Set(already.map((one) => one.pid)), {
|
|
43419
44125
|
budgetMs: Math.min(budgetMs, remaining),
|
|
43420
44126
|
accept: (one) => this.isTheGameOf(this.activeProcess, one, project),
|
|
43421
44127
|
giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
|
|
43422
44128
|
});
|
|
44129
|
+
const endpoint = await this.theRunsAmong(run, fresh);
|
|
44130
|
+
if (fresh.length > 0 && endpoint === null) {
|
|
44131
|
+
return null;
|
|
44132
|
+
}
|
|
43423
44133
|
if (endpoint !== null) {
|
|
43424
44134
|
const tiedMeanwhile = () => run.announcedPid !== undefined;
|
|
43425
44135
|
if (!tiedMeanwhile()) {
|
|
@@ -43471,7 +44181,7 @@ class GodotServer {
|
|
|
43471
44181
|
}
|
|
43472
44182
|
}
|
|
43473
44183
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
43474
|
-
const screenshotPath = screenshotDir ?
|
|
44184
|
+
const screenshotPath = screenshotDir ? join23(screenshotDir, "capture.png") : null;
|
|
43475
44185
|
try {
|
|
43476
44186
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
43477
44187
|
if (!reply.ok && reply.requestId !== undefined) {
|
|
@@ -43512,7 +44222,7 @@ class GodotServer {
|
|
|
43512
44222
|
type: "text",
|
|
43513
44223
|
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
|
|
43514
44224
|
},
|
|
43515
|
-
{ type: "image", data:
|
|
44225
|
+
{ type: "image", data: readFileSync20(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
43516
44226
|
]
|
|
43517
44227
|
};
|
|
43518
44228
|
} finally {
|
|
@@ -43609,10 +44319,11 @@ class GodotServer {
|
|
|
43609
44319
|
}
|
|
43610
44320
|
}
|
|
43611
44321
|
async function runGodotServer() {
|
|
44322
|
+
delete process.env[EDITOR_PID_VARIABLE];
|
|
43612
44323
|
const server = new GodotServer;
|
|
43613
44324
|
await server.run();
|
|
43614
44325
|
}
|
|
43615
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250,
|
|
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/";
|
|
43616
44327
|
var init_server3 = __esm(() => {
|
|
43617
44328
|
init_mcp();
|
|
43618
44329
|
init_stdio2();
|
|
@@ -43628,6 +44339,7 @@ var init_server3 = __esm(() => {
|
|
|
43628
44339
|
init_dap_client();
|
|
43629
44340
|
init_desktop();
|
|
43630
44341
|
init_editor_log();
|
|
44342
|
+
init_engine_run();
|
|
43631
44343
|
init_errors();
|
|
43632
44344
|
init_game_log();
|
|
43633
44345
|
init_godot_bridge();
|
|
@@ -43652,8 +44364,8 @@ var init_server3 = __esm(() => {
|
|
|
43652
44364
|
init_setup();
|
|
43653
44365
|
init_tool_definitions();
|
|
43654
44366
|
init_update_check();
|
|
43655
|
-
|
|
43656
|
-
__dirname2 =
|
|
44367
|
+
run4 = promisify5(execFile5);
|
|
44368
|
+
__dirname2 = dirname16(fileURLToPath5(import.meta.url));
|
|
43657
44369
|
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
43658
44370
|
LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
43659
44371
|
VALUE_NAMES = {
|
|
@@ -43702,8 +44414,8 @@ var init_server3 = __esm(() => {
|
|
|
43702
44414
|
init_bridge_announce();
|
|
43703
44415
|
init_errors();
|
|
43704
44416
|
init_godot_path();
|
|
43705
|
-
import { existsSync as
|
|
43706
|
-
import { join as
|
|
44417
|
+
import { existsSync as existsSync18 } from "node:fs";
|
|
44418
|
+
import { join as join24, resolve as resolve6 } from "node:path";
|
|
43707
44419
|
|
|
43708
44420
|
// src/harnesses.ts
|
|
43709
44421
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -44413,27 +45125,37 @@ function registered(harness, projectPath) {
|
|
|
44413
45125
|
}
|
|
44414
45126
|
function recordedEnginePath(projectPath) {
|
|
44415
45127
|
for (const harness of HARNESSES) {
|
|
44416
|
-
if (harness.scope !== "project"
|
|
45128
|
+
if (harness.scope !== "project") {
|
|
44417
45129
|
continue;
|
|
44418
45130
|
}
|
|
44419
|
-
const
|
|
44420
|
-
if (!existsSync4(path)) {
|
|
44421
|
-
continue;
|
|
44422
|
-
}
|
|
44423
|
-
let parsed;
|
|
44424
|
-
try {
|
|
44425
|
-
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
44426
|
-
} catch {
|
|
44427
|
-
continue;
|
|
44428
|
-
}
|
|
44429
|
-
const entry = reach(reach(parsed, harness.container), SERVER_KEY);
|
|
44430
|
-
const named = reach(reach(entry, harness.shape === "opencode" ? "environment" : "env"), "GODOT_PATH");
|
|
45131
|
+
const named = reach(entryEnvironment(harness, projectPath), "GODOT_PATH");
|
|
44431
45132
|
if (typeof named === "string" && named !== "" && existsSync4(named)) {
|
|
44432
45133
|
return named;
|
|
44433
45134
|
}
|
|
44434
45135
|
}
|
|
44435
45136
|
return null;
|
|
44436
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
|
+
}
|
|
44437
45159
|
function reach(held, key) {
|
|
44438
45160
|
return typeof held === "object" && held !== null && !Array.isArray(held) ? held[key] : undefined;
|
|
44439
45161
|
}
|
|
@@ -44571,6 +45293,7 @@ function taken(harness, path, held, stripped) {
|
|
|
44571
45293
|
// src/cli.ts
|
|
44572
45294
|
init_headless();
|
|
44573
45295
|
init_issues();
|
|
45296
|
+
init_paths();
|
|
44574
45297
|
|
|
44575
45298
|
// src/prompt.ts
|
|
44576
45299
|
import process8 from "node:process";
|
|
@@ -44602,8 +45325,8 @@ init_setup();
|
|
|
44602
45325
|
// src/skill.ts
|
|
44603
45326
|
init_setup();
|
|
44604
45327
|
init_tool_definitions();
|
|
44605
|
-
import { existsSync as
|
|
44606
|
-
import { basename as basename3, dirname as
|
|
45328
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
45329
|
+
import { basename as basename3, dirname as dirname6, join as join11 } from "node:path";
|
|
44607
45330
|
|
|
44608
45331
|
// src/tool-reference.ts
|
|
44609
45332
|
init_headless_operations();
|
|
@@ -44736,9 +45459,10 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
44736
45459
|
- Read \`editor_output\` after every run. It returns the engine's errors and warnings as entries
|
|
44737
45460
|
with their backtraces, and a \`clean\` verdict, so a run that printed an error is one call away
|
|
44738
45461
|
from being known. For a run the editor plays, the errors come from the game through the runtime
|
|
44739
|
-
addon, so a played game without the addon
|
|
44740
|
-
|
|
44741
|
-
answer for state. Never decide a run has ended by watching the
|
|
45462
|
+
addon, so a played game without the addon answers \`errorsUnread\` and no \`clean\`, since
|
|
45463
|
+
what it raised reached nothing the server reads. For a long run it also names a transcript file:
|
|
45464
|
+
read that for output and this answer for state. Never decide a run has ended by watching the
|
|
45465
|
+
file stop growing, or a process
|
|
44742
45466
|
list, or a timeout; a bench between prints looks exactly like a bench that died to everything else.
|
|
44743
45467
|
\`endedBy\` says whether gdharness ended it, and \`running\` is asked of the operating system
|
|
44744
45468
|
whenever there is a process to ask about: every run started through \`editor_run\`, and every
|
|
@@ -44768,7 +45492,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
44768
45492
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
44769
45493
|
| What a property reads right now, on a node whose path you already hold | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
44770
45494
|
| What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
|
|
44771
|
-
| 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 |
|
|
44772
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 |
|
|
44773
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 |
|
|
44774
45498
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
@@ -44918,9 +45642,9 @@ function everySkillDirectory(projectPath, harnesses) {
|
|
|
44918
45642
|
function removeSkill(directories, projectPath) {
|
|
44919
45643
|
const removed = [];
|
|
44920
45644
|
for (const directory of directories) {
|
|
44921
|
-
if (
|
|
45645
|
+
if (existsSync11(directory)) {
|
|
44922
45646
|
rmSync6(directory, { recursive: true, force: true });
|
|
44923
|
-
pruneEmpty(
|
|
45647
|
+
pruneEmpty(dirname6(directory), projectPath);
|
|
44924
45648
|
removed.push(directory);
|
|
44925
45649
|
}
|
|
44926
45650
|
}
|
|
@@ -44934,17 +45658,17 @@ function pruneEmpty(directory, projectPath) {
|
|
|
44934
45658
|
return;
|
|
44935
45659
|
}
|
|
44936
45660
|
rmSync6(path, { recursive: true, force: true });
|
|
44937
|
-
path =
|
|
45661
|
+
path = dirname6(path);
|
|
44938
45662
|
}
|
|
44939
45663
|
}
|
|
44940
45664
|
function writeSkill(directories, version) {
|
|
44941
45665
|
const files = skillFiles(version);
|
|
44942
45666
|
return directories.map((directory) => {
|
|
44943
|
-
const replaced =
|
|
45667
|
+
const replaced = existsSync11(directory);
|
|
44944
45668
|
rmSync6(directory, { recursive: true, force: true });
|
|
44945
45669
|
for (const [name, contents] of files) {
|
|
44946
45670
|
const path = join11(directory, name);
|
|
44947
|
-
mkdirSync5(
|
|
45671
|
+
mkdirSync5(dirname6(path), { recursive: true });
|
|
44948
45672
|
writeFileSync7(path, contents, "utf8");
|
|
44949
45673
|
}
|
|
44950
45674
|
return { path: directory, replaced };
|
|
@@ -44952,18 +45676,18 @@ function writeSkill(directories, version) {
|
|
|
44952
45676
|
}
|
|
44953
45677
|
|
|
44954
45678
|
// src/version.ts
|
|
44955
|
-
import { readFileSync as
|
|
44956
|
-
import { dirname as
|
|
45679
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
45680
|
+
import { dirname as dirname7, join as join12 } from "node:path";
|
|
44957
45681
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
44958
45682
|
function getLocalVersion() {
|
|
44959
|
-
const currentDirectory =
|
|
45683
|
+
const currentDirectory = dirname7(fileURLToPath2(import.meta.url));
|
|
44960
45684
|
const packageCandidates = [
|
|
44961
45685
|
join12(currentDirectory, "..", "package.json"),
|
|
44962
45686
|
join12(currentDirectory, "..", "..", "package.json")
|
|
44963
45687
|
];
|
|
44964
45688
|
for (const packagePath of packageCandidates) {
|
|
44965
45689
|
try {
|
|
44966
|
-
const value = JSON.parse(
|
|
45690
|
+
const value = JSON.parse(readFileSync10(packagePath, "utf-8"));
|
|
44967
45691
|
if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
|
|
44968
45692
|
return value.version;
|
|
44969
45693
|
}
|
|
@@ -44981,7 +45705,7 @@ class UsageError extends Refusal {
|
|
|
44981
45705
|
function projectArgument(at) {
|
|
44982
45706
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
44983
45707
|
const projectPath = resolve6(named ?? ".");
|
|
44984
|
-
if (!
|
|
45708
|
+
if (!existsSync18(join24(projectPath, "project.godot"))) {
|
|
44985
45709
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
44986
45710
|
}
|
|
44987
45711
|
return projectPath;
|
|
@@ -45020,7 +45744,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
45020
45744
|
return;
|
|
45021
45745
|
}
|
|
45022
45746
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
45023
|
-
if (!
|
|
45747
|
+
if (!existsSync18(join24(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
45024
45748
|
console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
|
|
45025
45749
|
}
|
|
45026
45750
|
}
|
|
@@ -45152,7 +45876,7 @@ async function setup() {
|
|
|
45152
45876
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
45153
45877
|
}
|
|
45154
45878
|
if (!args.includes("--no-skill")) {
|
|
45155
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
45879
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync18);
|
|
45156
45880
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
45157
45881
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
45158
45882
|
}
|
|
@@ -45169,12 +45893,14 @@ async function uninstall() {
|
|
|
45169
45893
|
const named = namedHarnesses();
|
|
45170
45894
|
const from = named.length > 0 ? named : HARNESSES;
|
|
45171
45895
|
for (const harness of from) {
|
|
45172
|
-
|
|
45173
|
-
|
|
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
|
+
}
|
|
45174
45900
|
continue;
|
|
45175
45901
|
}
|
|
45176
|
-
|
|
45177
|
-
|
|
45902
|
+
const removal = disconnect(harness, projectPath);
|
|
45903
|
+
if (removal.action === "absent") {
|
|
45178
45904
|
continue;
|
|
45179
45905
|
}
|
|
45180
45906
|
if (removal.action === "manual") {
|
|
@@ -45194,15 +45920,19 @@ async function uninstall() {
|
|
|
45194
45920
|
said2(outcome, `disabling ${name}`);
|
|
45195
45921
|
console.log(`${name}: ${outcome.ok ? String(outcome.payload["action"]) : "failed"}`);
|
|
45196
45922
|
}
|
|
45197
|
-
if (before.
|
|
45923
|
+
if (before.runtimeAutoloadPath !== null) {
|
|
45198
45924
|
said2(await setRuntime(godot, projectPath, false), "removing the runtime autoload");
|
|
45199
45925
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload removed`);
|
|
45200
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
|
+
}
|
|
45201
45931
|
const addons = removeAddons(projectPath);
|
|
45202
45932
|
for (const path of addons) {
|
|
45203
45933
|
console.log(`removed ${path}`);
|
|
45204
45934
|
}
|
|
45205
|
-
const anything = addons.length > 0 || before.
|
|
45935
|
+
const anything = addons.length > 0 || before.runtimeAutoloadPath !== null || enabled.length > 0;
|
|
45206
45936
|
console.log(anything ? `
|
|
45207
45937
|
Reconnect your harness so it stops spawning a server that is no longer installed.` : `
|
|
45208
45938
|
Nothing of gdharness was in this project.`);
|
|
@@ -45211,13 +45941,13 @@ async function upgrade() {
|
|
|
45211
45941
|
const projectPath = projectArgument(1);
|
|
45212
45942
|
const version = getLocalVersion();
|
|
45213
45943
|
const before = inspectProject(projectPath);
|
|
45214
|
-
const
|
|
45215
|
-
if (
|
|
45944
|
+
const present = before.addons.find((addon) => addon.installed);
|
|
45945
|
+
if (present === undefined) {
|
|
45216
45946
|
const where = projectPath === process.cwd() ? "" : ` ${projectPath}`;
|
|
45217
45947
|
throw new UsageError(`gdharness is not installed in ${projectPath}. Run gdharness setup${where} instead.`);
|
|
45218
45948
|
}
|
|
45219
45949
|
const godot = await engine(projectPath);
|
|
45220
|
-
console.log(
|
|
45950
|
+
console.log(present.version === version ? `already ${version}; reinstalling` : `${present.version ?? "an unmarked copy"} -> ${version}`);
|
|
45221
45951
|
const digestBefore = installedEditorDigest(projectPath);
|
|
45222
45952
|
for (const addon of installAddons(projectPath)) {
|
|
45223
45953
|
console.log(`${addon.replaced ? "replaced" : "installed"} ${addon.path}`);
|
|
@@ -45231,7 +45961,21 @@ async function upgrade() {
|
|
|
45231
45961
|
}
|
|
45232
45962
|
said2(await runOperation(godot, "refresh_class_cache", {}, projectPath), "rebuilding the class list");
|
|
45233
45963
|
const launch = launchFor(version, godot.godotPath, projectPath);
|
|
45234
|
-
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
|
+
});
|
|
45235
45979
|
const moved = [];
|
|
45236
45980
|
let written = 0;
|
|
45237
45981
|
let byHand = 0;
|
|
@@ -45246,7 +45990,7 @@ async function upgrade() {
|
|
|
45246
45990
|
moved.push(took.from);
|
|
45247
45991
|
}
|
|
45248
45992
|
}
|
|
45249
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
45993
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync18), version)) {
|
|
45250
45994
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
45251
45995
|
}
|
|
45252
45996
|
console.log(`
|