gdharness 1.1.20 → 1.1.22
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 +1194 -668
- 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 -467
- 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 +8 -4
- 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 +28 -8
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +7 -297
- 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 +24 -9
- 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/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 +25 -10
- 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 +1025 -496
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -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,72 @@ 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
|
+
|
|
7898
|
+
// src/reimport.ts
|
|
7899
|
+
import { createHash } from "node:crypto";
|
|
7900
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, rmSync as rmSync3, statSync as statSync2, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7901
|
+
import { basename as basename2, join as join5 } from "node:path";
|
|
7902
|
+
function forceNextImport(projectPath, resourcePath) {
|
|
7903
|
+
const relative = resourcePath.replace(/^res:\/\//, "");
|
|
7904
|
+
const sidecar = join5(projectPath, `${relative}.import`);
|
|
7905
|
+
if (!existsSync5(sidecar)) {
|
|
7906
|
+
return;
|
|
7907
|
+
}
|
|
7908
|
+
const { atime, mtimeMs } = statSync2(sidecar);
|
|
7909
|
+
writeFileSync3(sidecar, readFileSync3(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
|
|
7910
|
+
utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
|
|
7911
|
+
rmSync3(importRecord(projectPath, resourcePath), { force: true });
|
|
7912
|
+
}
|
|
7913
|
+
function importRecord(projectPath, resourcePath) {
|
|
7914
|
+
const digest = createHash("md5").update(resourcePath).digest("hex");
|
|
7915
|
+
return join5(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
|
|
7916
|
+
}
|
|
7917
|
+
var init_reimport = () => {};
|
|
7918
|
+
|
|
7853
7919
|
// src/alive.ts
|
|
7854
7920
|
import process5 from "node:process";
|
|
7855
7921
|
function alive(pid) {
|
|
@@ -7866,12 +7932,12 @@ function alive(pid) {
|
|
|
7866
7932
|
var init_alive = () => {};
|
|
7867
7933
|
|
|
7868
7934
|
// src/scratch.ts
|
|
7869
|
-
import { existsSync as
|
|
7935
|
+
import { existsSync as existsSync6, mkdtempSync, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
|
|
7870
7936
|
import { tmpdir } from "node:os";
|
|
7871
|
-
import { join as
|
|
7937
|
+
import { join as join6 } from "node:path";
|
|
7872
7938
|
import process6 from "node:process";
|
|
7873
7939
|
function scratchDirectory(kind) {
|
|
7874
|
-
return mkdtempSync(
|
|
7940
|
+
return mkdtempSync(join6(tmpdir(), `gdharness-${kind}-${process6.pid}-`));
|
|
7875
7941
|
}
|
|
7876
7942
|
function sweepAbandonedScratch(directory = tmpdir()) {
|
|
7877
7943
|
let entries;
|
|
@@ -7887,7 +7953,7 @@ function sweepAbandonedScratch(directory = tmpdir()) {
|
|
|
7887
7953
|
continue;
|
|
7888
7954
|
}
|
|
7889
7955
|
try {
|
|
7890
|
-
remove(
|
|
7956
|
+
remove(join6(directory, entry));
|
|
7891
7957
|
} catch {}
|
|
7892
7958
|
}
|
|
7893
7959
|
}
|
|
@@ -7913,7 +7979,7 @@ function discard(...paths) {
|
|
|
7913
7979
|
return left;
|
|
7914
7980
|
}
|
|
7915
7981
|
function remove(path) {
|
|
7916
|
-
|
|
7982
|
+
rmSync4(path, SWEPT);
|
|
7917
7983
|
}
|
|
7918
7984
|
function tryAgain(path, attempt) {
|
|
7919
7985
|
if (attempt === 0) {
|
|
@@ -7925,7 +7991,7 @@ function tryAgain(path, attempt) {
|
|
|
7925
7991
|
lastTryOnExit();
|
|
7926
7992
|
}
|
|
7927
7993
|
const after = AGAIN_AFTER_MS[attempt];
|
|
7928
|
-
if (after === undefined || !
|
|
7994
|
+
if (after === undefined || !existsSync6(path)) {
|
|
7929
7995
|
waiting.delete(path);
|
|
7930
7996
|
if (after === undefined) {
|
|
7931
7997
|
atExit.add(path);
|
|
@@ -7972,10 +8038,8 @@ var init_scratch = __esm(() => {
|
|
|
7972
8038
|
});
|
|
7973
8039
|
|
|
7974
8040
|
// src/headless.ts
|
|
7975
|
-
import {
|
|
7976
|
-
import {
|
|
7977
|
-
import { join as join6 } from "node:path";
|
|
7978
|
-
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";
|
|
7979
8043
|
function snakeCased(params) {
|
|
7980
8044
|
const result = emptyRecord();
|
|
7981
8045
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -7984,34 +8048,55 @@ function snakeCased(params) {
|
|
|
7984
8048
|
}
|
|
7985
8049
|
return result;
|
|
7986
8050
|
}
|
|
7987
|
-
function
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
return parsed;
|
|
7994
|
-
}
|
|
7995
|
-
} catch {}
|
|
7996
|
-
}
|
|
7997
|
-
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
|
+
};
|
|
7998
8057
|
}
|
|
7999
|
-
function
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
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
|
+
});
|
|
8004
8068
|
}
|
|
8005
|
-
function
|
|
8006
|
-
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));
|
|
8007
8075
|
if (own.length > 0) {
|
|
8008
|
-
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("; ");
|
|
8009
8082
|
}
|
|
8010
|
-
const
|
|
8011
|
-
|
|
8012
|
-
|
|
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;
|
|
8013
8099
|
}
|
|
8014
|
-
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
8015
8100
|
}
|
|
8016
8101
|
function librariesNotCopied(messages) {
|
|
8017
8102
|
const found = new Set;
|
|
@@ -8023,99 +8108,141 @@ function librariesNotCopied(messages) {
|
|
|
8023
8108
|
}
|
|
8024
8109
|
return [...found];
|
|
8025
8110
|
}
|
|
8026
|
-
function extensionRetryNote(libraries) {
|
|
8027
|
-
|
|
8111
|
+
function extensionRetryNote(libraries, stillNotLoaded = []) {
|
|
8112
|
+
const second = stillNotLoaded.length === 0 ? "so the import was run again and loaded the extension, and the messages are from that second run." : `so the import was run again, and it still could not load ${stillNotLoaded.join(" and ")}: this answer is from an import without ${stillNotLoaded.length === 1 ? "it" : "them"}, and its messages say what the engine hit.`;
|
|
8113
|
+
return `The first import could not load ${libraries.join(" and ")}: on Windows an engine running as the editor ` + "loads a GDExtension from a ~ copy beside the library, and the editor open on this project holds its own " + "copy there, so the first engine started beside it cannot make one. That failed attempt moves the " + `editor's copy aside, ${second} The editor's copy is left beside the library as a ~RF….TMP file, which ` + "can be deleted once the editor has exited.";
|
|
8028
8114
|
}
|
|
8029
|
-
async function runImport(godotPath, projectPath,
|
|
8030
|
-
const
|
|
8115
|
+
async function runImport(godotPath, projectPath, options = {}) {
|
|
8116
|
+
const once = options.once ?? importOnce;
|
|
8117
|
+
const pass = () => {
|
|
8118
|
+
for (const target of options.forcing ?? []) {
|
|
8119
|
+
forceNextImport(projectPath, target);
|
|
8120
|
+
}
|
|
8121
|
+
return once(godotPath, projectPath);
|
|
8122
|
+
};
|
|
8123
|
+
const first = await pass();
|
|
8031
8124
|
const collided = librariesNotCopied(first.messages);
|
|
8032
8125
|
if (collided.length === 0) {
|
|
8033
8126
|
return first;
|
|
8034
8127
|
}
|
|
8035
|
-
|
|
8128
|
+
const second = await pass();
|
|
8129
|
+
const still = librariesNotCopied(second.messages);
|
|
8130
|
+
return {
|
|
8131
|
+
...second,
|
|
8132
|
+
librariesRetried: collided,
|
|
8133
|
+
...still.length === 0 ? {} : { librariesNotLoaded: still },
|
|
8134
|
+
extensionNote: extensionRetryNote(collided, still)
|
|
8135
|
+
};
|
|
8036
8136
|
}
|
|
8037
8137
|
async function importOnce(godotPath, projectPath) {
|
|
8038
8138
|
const logDir = scratchDirectory("import");
|
|
8139
|
+
let ran;
|
|
8039
8140
|
try {
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
messages: problems(failed.stderr)
|
|
8049
|
-
};
|
|
8050
|
-
}
|
|
8051
|
-
return {
|
|
8052
|
-
ok: false,
|
|
8053
|
-
message: `the import pass could not be run: ${error instanceof Error ? error.message : String(error)}`,
|
|
8054
|
-
messages: []
|
|
8055
|
-
};
|
|
8141
|
+
ran = await runEngine(godotPath, [
|
|
8142
|
+
"--headless",
|
|
8143
|
+
"--log-file",
|
|
8144
|
+
join7(logDir, "engine.log"),
|
|
8145
|
+
"--path",
|
|
8146
|
+
projectPath,
|
|
8147
|
+
"--import"
|
|
8148
|
+
]);
|
|
8056
8149
|
} finally {
|
|
8057
8150
|
discard(logDir);
|
|
8058
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
|
+
};
|
|
8059
8161
|
}
|
|
8060
8162
|
async function runOperation(engine, operation, params, projectPath) {
|
|
8061
8163
|
const paramsDir = scratchDirectory("params");
|
|
8062
|
-
const paramsFile =
|
|
8063
|
-
|
|
8164
|
+
const paramsFile = join7(paramsDir, `${operation}.json`);
|
|
8165
|
+
const answerFile = join7(paramsDir, "answer.json");
|
|
8166
|
+
writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
8064
8167
|
const args = [
|
|
8065
8168
|
"--headless",
|
|
8066
8169
|
"--log-file",
|
|
8067
|
-
|
|
8170
|
+
join7(paramsDir, "engine.log"),
|
|
8068
8171
|
"--path",
|
|
8069
8172
|
projectPath,
|
|
8070
8173
|
"--script",
|
|
8071
8174
|
engine.script,
|
|
8072
8175
|
operation,
|
|
8073
8176
|
`@file:${paramsFile}`,
|
|
8177
|
+
answerFile,
|
|
8074
8178
|
...engine.debug ? ["--debug-godot"] : []
|
|
8075
8179
|
];
|
|
8076
|
-
let
|
|
8077
|
-
let
|
|
8180
|
+
let ran;
|
|
8181
|
+
let answer;
|
|
8078
8182
|
try {
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
messages: problems(failed.stderr)
|
|
8087
|
-
};
|
|
8088
|
-
}
|
|
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") {
|
|
8089
8190
|
return {
|
|
8090
8191
|
ok: false,
|
|
8091
|
-
message: `${operation} could not be
|
|
8092
|
-
|
|
8192
|
+
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
8193
|
+
...reported(problems)
|
|
8093
8194
|
};
|
|
8094
|
-
} finally {
|
|
8095
|
-
discard(paramsDir);
|
|
8096
8195
|
}
|
|
8097
|
-
|
|
8098
|
-
|
|
8196
|
+
if (answer.kind === "none") {
|
|
8197
|
+
const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
|
|
8099
8198
|
return {
|
|
8100
8199
|
ok: false,
|
|
8101
|
-
message: `${operation}
|
|
8102
|
-
|
|
8200
|
+
message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
|
|
8201
|
+
...reported(problems)
|
|
8103
8202
|
};
|
|
8104
8203
|
}
|
|
8105
|
-
|
|
8204
|
+
const fault = scriptFault(problems, engine.script);
|
|
8205
|
+
if (fault !== undefined) {
|
|
8206
|
+
return {
|
|
8207
|
+
ok: false,
|
|
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
|
+
};
|
|
8106
8219
|
}
|
|
8107
|
-
|
|
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)}`
|
|
8232
|
+
};
|
|
8233
|
+
}
|
|
8234
|
+
}
|
|
8235
|
+
var OWN_ERROR = "[ERROR] ", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
|
|
8108
8236
|
var init_headless = __esm(() => {
|
|
8109
|
-
|
|
8110
|
-
|
|
8237
|
+
init_engine_run();
|
|
8238
|
+
init_reimport();
|
|
8111
8239
|
init_scratch();
|
|
8112
|
-
run3 = promisify3(execFile3);
|
|
8113
8240
|
});
|
|
8114
8241
|
|
|
8115
8242
|
// src/update-check.ts
|
|
8116
|
-
import { existsSync as
|
|
8243
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8117
8244
|
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
8118
|
-
import { join as
|
|
8245
|
+
import { join as join8 } from "node:path";
|
|
8119
8246
|
function packageUrl(root) {
|
|
8120
8247
|
return `${root.replace(/\/+$/, "")}/gdharness/latest`;
|
|
8121
8248
|
}
|
|
@@ -8137,28 +8264,28 @@ function cacheDirectory(environment) {
|
|
|
8137
8264
|
};
|
|
8138
8265
|
const home = set("HOME") ?? homedir3();
|
|
8139
8266
|
if (process.platform === "win32") {
|
|
8140
|
-
return
|
|
8267
|
+
return join8(set("LOCALAPPDATA") ?? home, "gdharness");
|
|
8141
8268
|
}
|
|
8142
8269
|
if (process.platform === "darwin") {
|
|
8143
|
-
return
|
|
8270
|
+
return join8(home, "Library", "Caches", "gdharness");
|
|
8144
8271
|
}
|
|
8145
|
-
return
|
|
8272
|
+
return join8(set("XDG_CACHE_HOME") ?? join8(home, ".cache"), "gdharness");
|
|
8146
8273
|
}
|
|
8147
8274
|
function cacheFile(environment = process.env) {
|
|
8148
8275
|
try {
|
|
8149
8276
|
const directory = cacheDirectory(environment);
|
|
8150
8277
|
mkdirSync3(directory, { recursive: true, mode: 448 });
|
|
8151
|
-
return
|
|
8278
|
+
return join8(directory, "update-check.json");
|
|
8152
8279
|
} catch {
|
|
8153
|
-
return
|
|
8280
|
+
return join8(tmpdir2(), "gdharness-update-check.json");
|
|
8154
8281
|
}
|
|
8155
8282
|
}
|
|
8156
8283
|
function readCache(path) {
|
|
8157
8284
|
try {
|
|
8158
|
-
if (!
|
|
8285
|
+
if (!existsSync8(path)) {
|
|
8159
8286
|
return null;
|
|
8160
8287
|
}
|
|
8161
|
-
const parsed = JSON.parse(
|
|
8288
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
8162
8289
|
if (typeof parsed !== "object" || parsed === null) {
|
|
8163
8290
|
return null;
|
|
8164
8291
|
}
|
|
@@ -8175,7 +8302,7 @@ function readCache(path) {
|
|
|
8175
8302
|
}
|
|
8176
8303
|
function writeCache(path, entry) {
|
|
8177
8304
|
try {
|
|
8178
|
-
|
|
8305
|
+
writeFileSync5(path, JSON.stringify(entry), { encoding: "utf8", mode: 384 });
|
|
8179
8306
|
} catch {}
|
|
8180
8307
|
}
|
|
8181
8308
|
function parts(version) {
|
|
@@ -8306,7 +8433,7 @@ var init_update_check = __esm(() => {
|
|
|
8306
8433
|
});
|
|
8307
8434
|
|
|
8308
8435
|
// src/server-version.ts
|
|
8309
|
-
import { readFileSync as
|
|
8436
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
8310
8437
|
function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
8311
8438
|
if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
|
|
8312
8439
|
return addonDigest !== shippedDigest;
|
|
@@ -8348,7 +8475,7 @@ var init_server_version = __esm(() => {
|
|
|
8348
8475
|
GODOT_DEBUG_MODE_DEFAULT = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE;
|
|
8349
8476
|
SERVER_VERSION = (() => {
|
|
8350
8477
|
try {
|
|
8351
|
-
const pkg = JSON.parse(
|
|
8478
|
+
const pkg = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
|
|
8352
8479
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
8353
8480
|
} catch {
|
|
8354
8481
|
return "0.0.0";
|
|
@@ -8357,7 +8484,7 @@ var init_server_version = __esm(() => {
|
|
|
8357
8484
|
});
|
|
8358
8485
|
|
|
8359
8486
|
// src/issues.ts
|
|
8360
|
-
import { createHash } from "node:crypto";
|
|
8487
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
8361
8488
|
import process7 from "node:process";
|
|
8362
8489
|
function feedbackNotice(every) {
|
|
8363
8490
|
return JSON.stringify({
|
|
@@ -8376,7 +8503,7 @@ function runtime() {
|
|
|
8376
8503
|
function defectSignature(where, message) {
|
|
8377
8504
|
const normalised = `${where}
|
|
8378
8505
|
${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
|
|
8379
|
-
return
|
|
8506
|
+
return createHash2("sha256").update(normalised).digest("hex").slice(0, 8);
|
|
8380
8507
|
}
|
|
8381
8508
|
function facts(where, message, godotVersion) {
|
|
8382
8509
|
return [
|
|
@@ -8482,8 +8609,11 @@ var init_issues = __esm(() => {
|
|
|
8482
8609
|
});
|
|
8483
8610
|
|
|
8484
8611
|
// src/class-cache.ts
|
|
8485
|
-
import { existsSync as
|
|
8486
|
-
import { join as
|
|
8612
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync3 } from "node:fs";
|
|
8613
|
+
import { join as join9 } from "node:path";
|
|
8614
|
+
function isToolScript(source) {
|
|
8615
|
+
return TOOL.test(codeOf(source));
|
|
8616
|
+
}
|
|
8487
8617
|
function declaredClasses(projectPath) {
|
|
8488
8618
|
const declared = new Map;
|
|
8489
8619
|
eachScript(projectPath, (script, source) => {
|
|
@@ -8496,18 +8626,18 @@ function declaredClasses(projectPath) {
|
|
|
8496
8626
|
}
|
|
8497
8627
|
function eachScript(projectPath, each) {
|
|
8498
8628
|
const visit = (directory, prefix) => {
|
|
8499
|
-
if (
|
|
8629
|
+
if (existsSync9(join9(directory, ".gdignore"))) {
|
|
8500
8630
|
return;
|
|
8501
8631
|
}
|
|
8502
8632
|
for (const entry of readdirSync3(directory, { withFileTypes: true })) {
|
|
8503
8633
|
if (entry.name.startsWith(".")) {
|
|
8504
8634
|
continue;
|
|
8505
8635
|
}
|
|
8506
|
-
const path =
|
|
8636
|
+
const path = join9(directory, entry.name);
|
|
8507
8637
|
if (entry.isDirectory()) {
|
|
8508
8638
|
visit(path, `${prefix}${entry.name}/`);
|
|
8509
8639
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
8510
|
-
each(`res://${prefix}${entry.name}`,
|
|
8640
|
+
each(`res://${prefix}${entry.name}`, readFileSync7(path, "utf8"));
|
|
8511
8641
|
}
|
|
8512
8642
|
}
|
|
8513
8643
|
};
|
|
@@ -8523,7 +8653,7 @@ function scriptsNaming(projectPath, classes) {
|
|
|
8523
8653
|
}
|
|
8524
8654
|
const code = codeOf(source);
|
|
8525
8655
|
if (classesNamedIn(code, named).length > 0) {
|
|
8526
|
-
naming.push({ script, tool:
|
|
8656
|
+
naming.push({ script, tool: TOOL.test(code) });
|
|
8527
8657
|
}
|
|
8528
8658
|
});
|
|
8529
8659
|
return naming.sort((a, b) => a.script.localeCompare(b.script));
|
|
@@ -8556,12 +8686,12 @@ function codeOf(source) {
|
|
|
8556
8686
|
return code;
|
|
8557
8687
|
}
|
|
8558
8688
|
function cachedClasses(projectPath) {
|
|
8559
|
-
const cache =
|
|
8560
|
-
if (!
|
|
8689
|
+
const cache = join9(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
8690
|
+
if (!existsSync9(cache)) {
|
|
8561
8691
|
return null;
|
|
8562
8692
|
}
|
|
8563
8693
|
const listed = new Map;
|
|
8564
|
-
const text =
|
|
8694
|
+
const text = readFileSync7(cache, "utf8");
|
|
8565
8695
|
for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
|
|
8566
8696
|
listed.set(entry[1] ?? "", entry[2] ?? "");
|
|
8567
8697
|
}
|
|
@@ -8604,32 +8734,33 @@ function missingMemberIn(message) {
|
|
|
8604
8734
|
if (called !== null) {
|
|
8605
8735
|
return { kind: "static method", member: called[1] ?? "", type: called[2] ?? "" };
|
|
8606
8736
|
}
|
|
8607
|
-
const inBase = /Cannot find member "([A-Za-z_]\w*)" in base "([A-Za-z_]\w*)((
|
|
8737
|
+
const inBase = /Cannot find member "([A-Za-z_]\w*)" in base "([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?"/.exec(message);
|
|
8608
8738
|
if (inBase === null) {
|
|
8609
8739
|
return null;
|
|
8610
8740
|
}
|
|
8611
8741
|
const member = inBase[1] ?? "";
|
|
8612
8742
|
const type = inBase[2] ?? "";
|
|
8613
|
-
const
|
|
8614
|
-
const enumName = within.at(-1);
|
|
8743
|
+
const enumName = inBase[3];
|
|
8615
8744
|
return enumName === undefined ? { kind: "member", member, type } : { kind: "enum member", member, type, enum: enumName };
|
|
8616
8745
|
}
|
|
8617
8746
|
function declaresMember(source, missing) {
|
|
8618
8747
|
const name = missing.member.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8748
|
+
const top = String.raw`^(?:@\w+(?:\([^)]*\))?\s+)*`;
|
|
8749
|
+
const declared = (kinds) => new RegExp(String.raw`${top}(?:${kinds})\s+${name}\b`, "m").test(source);
|
|
8619
8750
|
if (missing.kind === "method") {
|
|
8620
|
-
return new RegExp(String.raw
|
|
8751
|
+
return new RegExp(String.raw`${top}(?:static\s+)?func\s+${name}\s*\(`, "m").test(source);
|
|
8621
8752
|
}
|
|
8622
8753
|
if (missing.kind === "static method") {
|
|
8623
|
-
return new RegExp(String.raw
|
|
8754
|
+
return new RegExp(String.raw`${top}static\s+func\s+${name}\s*\(`, "m").test(source);
|
|
8624
8755
|
}
|
|
8625
8756
|
if (missing.kind === "enum member") {
|
|
8626
8757
|
return enumValues(source, missing.enum ?? "").includes(missing.member);
|
|
8627
8758
|
}
|
|
8628
|
-
const kinds = missing.kind === "property" ?
|
|
8629
|
-
return
|
|
8759
|
+
const kinds = missing.kind === "property" ? String.raw`(?:static\s+)?(?:var|func)|const|enum|class|signal` : String.raw`static\s+(?:var|func)|const|enum|class`;
|
|
8760
|
+
return declared(kinds) || enumValues(source, null).includes(missing.member);
|
|
8630
8761
|
}
|
|
8631
8762
|
function enumValues(source, named) {
|
|
8632
|
-
const opening = named === null ?
|
|
8763
|
+
const opening = named === null ? /^enum\s*\{([^}]*)\}/gm : new RegExp(String.raw`^enum\s+${named.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\s*\{([^}]*)\}`, "gm");
|
|
8633
8764
|
const values = [];
|
|
8634
8765
|
for (const [, body] of source.matchAll(opening)) {
|
|
8635
8766
|
const uncommented = (body ?? "").split(`
|
|
@@ -8672,21 +8803,22 @@ function classesNamedIn(source, known) {
|
|
|
8672
8803
|
return [...named].sort();
|
|
8673
8804
|
}
|
|
8674
8805
|
function contradictedDiagnostics(messages, classes, sourceOf) {
|
|
8806
|
+
const keyOf = (missing) => [missing.type, missing.enum ?? "", missing.member].join(".");
|
|
8807
|
+
const denials = messages.map((message) => missingMemberIn(message)).filter((missing) => missing !== null);
|
|
8808
|
+
const calledOnTheClass = new Set(denials.filter((missing) => missing.kind === "static method").map((missing) => keyOf(missing)));
|
|
8675
8809
|
const read = new Map;
|
|
8676
8810
|
const found = new Map;
|
|
8677
|
-
for (const
|
|
8678
|
-
const
|
|
8679
|
-
const
|
|
8680
|
-
if (
|
|
8811
|
+
for (const missing of denials) {
|
|
8812
|
+
const declaredIn = classes.get(missing.type);
|
|
8813
|
+
const key = keyOf(missing);
|
|
8814
|
+
if (declaredIn === undefined || missing.kind === "method" && calledOnTheClass.has(key)) {
|
|
8681
8815
|
continue;
|
|
8682
8816
|
}
|
|
8683
8817
|
if (!read.has(declaredIn)) {
|
|
8684
8818
|
read.set(declaredIn, sourceOf(declaredIn));
|
|
8685
8819
|
}
|
|
8686
8820
|
const source = read.get(declaredIn) ?? null;
|
|
8687
|
-
|
|
8688
|
-
const already = found.get(key);
|
|
8689
|
-
if (source !== null && declaresMember(source, missing) && already?.kind !== "static method") {
|
|
8821
|
+
if (source !== null && declaresMember(source, missing)) {
|
|
8690
8822
|
found.set(key, { ...missing, declaredIn });
|
|
8691
8823
|
}
|
|
8692
8824
|
}
|
|
@@ -8728,6 +8860,19 @@ function classRemedy(entries) {
|
|
|
8728
8860
|
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.";
|
|
8729
8861
|
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.";
|
|
8730
8862
|
}
|
|
8863
|
+
function notReloadedNote(broughtIn, notReloaded) {
|
|
8864
|
+
const tried = notReloaded.filter((one) => one.tool !== true).map((one) => one.scriptPath);
|
|
8865
|
+
const tools = notReloaded.filter((one) => one.tool === true).map((one) => one.scriptPath);
|
|
8866
|
+
const all = [...tried, ...tools];
|
|
8867
|
+
const remedies = [];
|
|
8868
|
+
if (tried.length > 0) {
|
|
8869
|
+
remedies.push(`For ${tried.join(", ")}: editor_rescan with reloadScript on ${tried.length === 1 ? "it" : "each"}, or editor_launch restart.`);
|
|
8870
|
+
}
|
|
8871
|
+
if (tools.length > 0) {
|
|
8872
|
+
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.`);
|
|
8873
|
+
}
|
|
8874
|
+
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(" ")}`;
|
|
8875
|
+
}
|
|
8731
8876
|
function failedReloadNote(failed) {
|
|
8732
8877
|
const missing = (before, after) => before.filter((name) => !after.includes(name));
|
|
8733
8878
|
const lost = [
|
|
@@ -8736,9 +8881,19 @@ function failedReloadNote(failed) {
|
|
|
8736
8881
|
];
|
|
8737
8882
|
const unchanged = lost.length === 0 && missing(failed.heldAfter, failed.heldBefore).length === 0 && missing(failed.constantsAfter, failed.constantsBefore).length === 0;
|
|
8738
8883
|
const answered = failed.codeName === "" ? `error ${failed.code}` : `error ${failed.code}, ${failed.codeName}`;
|
|
8739
|
-
const
|
|
8884
|
+
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.";
|
|
8885
|
+
let copy;
|
|
8886
|
+
if (failed.restored === null) {
|
|
8887
|
+
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.`;
|
|
8888
|
+
} else if (!failed.restored) {
|
|
8889
|
+
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.`;
|
|
8890
|
+
} else if (unchanged) {
|
|
8891
|
+
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.";
|
|
8892
|
+
} else {
|
|
8893
|
+
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.`;
|
|
8894
|
+
}
|
|
8740
8895
|
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.`;
|
|
8741
|
-
return `${failed.script} did not compile
|
|
8896
|
+
return `${failed.script} did not compile: Godot answered ${answered}. ${copy} ` + `script_diagnostics on it gives the errors.${order}`;
|
|
8742
8897
|
}
|
|
8743
8898
|
function uncachedClassNote(types) {
|
|
8744
8899
|
if (types.length === 0) {
|
|
@@ -8748,9 +8903,9 @@ function uncachedClassNote(types) {
|
|
|
8748
8903
|
return `${types.join(", ")} ${one ? "is declared" : "are declared"} in this project and missing from ` + `.godot/global_script_class_cache.cfg, so a game launched now would not resolve ${one ? "it" : "them"} ` + "either. Try editor_rescan first: it asks the editor already running and starts nothing, and it " + "cleared this in 314ms in a second project with a 31-worker fan-out still importing. It is not " + "always enough, because a file another engine has already imported reads as settled and the " + "walk does not look inside it. project_import refresh_classes rewrites the cache from the " + "declarations on disk and does reach that case, at the cost of a short headless engine, which " + "is worth knowing when something else is already running against this project.";
|
|
8749
8904
|
}
|
|
8750
8905
|
function cacheWrittenAt(projectPath) {
|
|
8751
|
-
const cache =
|
|
8906
|
+
const cache = join9(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
8752
8907
|
try {
|
|
8753
|
-
return
|
|
8908
|
+
return statSync3(cache).mtimeMs;
|
|
8754
8909
|
} catch {
|
|
8755
8910
|
return null;
|
|
8756
8911
|
}
|
|
@@ -8762,14 +8917,14 @@ function declaredSince(projectPath, paths, names, since) {
|
|
|
8762
8917
|
return false;
|
|
8763
8918
|
}
|
|
8764
8919
|
try {
|
|
8765
|
-
return
|
|
8920
|
+
return statSync3(join9(projectPath, path.slice("res://".length))).mtimeMs > since;
|
|
8766
8921
|
} catch {
|
|
8767
8922
|
return false;
|
|
8768
8923
|
}
|
|
8769
8924
|
});
|
|
8770
8925
|
}
|
|
8771
8926
|
function cachedAtMissingPaths(cached, projectPath) {
|
|
8772
|
-
return [...cached].filter(([, path]) => path.startsWith("res://") && !
|
|
8927
|
+
return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync9(join9(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
|
|
8773
8928
|
}
|
|
8774
8929
|
function staleAgainst(cached, projectPath) {
|
|
8775
8930
|
return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
|
|
@@ -8786,9 +8941,10 @@ function unseenByEditor(projectPath, editorHolds) {
|
|
|
8786
8941
|
const held = new Set(editorHolds);
|
|
8787
8942
|
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);
|
|
8788
8943
|
}
|
|
8789
|
-
var DECLARATION;
|
|
8944
|
+
var DECLARATION, TOOL;
|
|
8790
8945
|
var init_class_cache = __esm(() => {
|
|
8791
8946
|
DECLARATION = /^(?:@[a-z_]+(?:\([^)\n]*\))?\s+)*class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m;
|
|
8947
|
+
TOOL = /^[ \t]*(?:@[a-z_]+(?:\([^)\n]*\))?[ \t]+)*@tool\b/m;
|
|
8792
8948
|
});
|
|
8793
8949
|
|
|
8794
8950
|
// node_modules/zod/v4/core/util.js
|
|
@@ -15895,11 +16051,11 @@ var init_types = __esm(() => {
|
|
|
15895
16051
|
});
|
|
15896
16052
|
|
|
15897
16053
|
// src/paths.ts
|
|
15898
|
-
import { realpathSync } from "node:fs";
|
|
16054
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
15899
16055
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
15900
16056
|
function realPathOr(path) {
|
|
15901
16057
|
try {
|
|
15902
|
-
return
|
|
16058
|
+
return realpathSync2.native(path);
|
|
15903
16059
|
} catch {
|
|
15904
16060
|
return path;
|
|
15905
16061
|
}
|
|
@@ -15920,6 +16076,12 @@ function resolveWithinProject(projectPath, candidatePath) {
|
|
|
15920
16076
|
return { ok: false, reason: "Path contains a null byte." };
|
|
15921
16077
|
}
|
|
15922
16078
|
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
16079
|
+
if (withoutScheme.startsWith("uid://")) {
|
|
16080
|
+
return {
|
|
16081
|
+
ok: false,
|
|
16082
|
+
reason: `Path '${trimmed}' is a UID, and a file is judged here by where it is on disk, which a UID does not say. Give the file's res:// path.`
|
|
16083
|
+
};
|
|
16084
|
+
}
|
|
15923
16085
|
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
15924
16086
|
if (scheme) {
|
|
15925
16087
|
return {
|
|
@@ -15956,7 +16118,7 @@ var init_paths = __esm(() => {
|
|
|
15956
16118
|
});
|
|
15957
16119
|
|
|
15958
16120
|
// src/resources.ts
|
|
15959
|
-
import { readFileSync as
|
|
16121
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
15960
16122
|
import { extname, resolve as resolve2 } from "node:path";
|
|
15961
16123
|
function ensureProjectPath(getProjectPath) {
|
|
15962
16124
|
const projectPath = getProjectPath();
|
|
@@ -16122,7 +16284,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
16122
16284
|
const parsedUri = parseGodotUri(uri);
|
|
16123
16285
|
if (parsedUri.kind === "project-info") {
|
|
16124
16286
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
16125
|
-
const rawProject =
|
|
16287
|
+
const rawProject = readFileSync8(projectFilePath, "utf-8");
|
|
16126
16288
|
const parsedProject = parseProjectGodot(rawProject);
|
|
16127
16289
|
return {
|
|
16128
16290
|
mimeType: "application/json",
|
|
@@ -16131,7 +16293,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
16131
16293
|
}
|
|
16132
16294
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
16133
16295
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
16134
|
-
const text =
|
|
16296
|
+
const text = readFileSync8(filePath, "utf-8");
|
|
16135
16297
|
return {
|
|
16136
16298
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
16137
16299
|
text
|
|
@@ -16260,42 +16422,42 @@ function readParams(params, key) {
|
|
|
16260
16422
|
|
|
16261
16423
|
// src/setup.ts
|
|
16262
16424
|
import { spawnSync } from "node:child_process";
|
|
16263
|
-
import { createHash as
|
|
16425
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
16264
16426
|
import {
|
|
16265
16427
|
cpSync,
|
|
16266
|
-
existsSync as
|
|
16428
|
+
existsSync as existsSync10,
|
|
16267
16429
|
mkdirSync as mkdirSync4,
|
|
16268
16430
|
readdirSync as readdirSync4,
|
|
16269
|
-
readFileSync as
|
|
16431
|
+
readFileSync as readFileSync9,
|
|
16270
16432
|
rmdirSync,
|
|
16271
|
-
rmSync as
|
|
16272
|
-
statSync as
|
|
16273
|
-
writeFileSync as
|
|
16433
|
+
rmSync as rmSync5,
|
|
16434
|
+
statSync as statSync4,
|
|
16435
|
+
writeFileSync as writeFileSync6
|
|
16274
16436
|
} from "node:fs";
|
|
16275
|
-
import { dirname as
|
|
16437
|
+
import { dirname as dirname5, join as join10 } from "node:path";
|
|
16276
16438
|
import { fileURLToPath } from "node:url";
|
|
16277
16439
|
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
16278
|
-
const hash =
|
|
16440
|
+
const hash = createHash3("sha256");
|
|
16279
16441
|
const walk = (directory, relative) => {
|
|
16280
16442
|
for (const name of readdirSync4(directory).sort()) {
|
|
16281
16443
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
16282
16444
|
continue;
|
|
16283
16445
|
}
|
|
16284
|
-
const path =
|
|
16446
|
+
const path = join10(directory, name);
|
|
16285
16447
|
const inside = `${relative}/${name}`;
|
|
16286
|
-
if (
|
|
16448
|
+
if (statSync4(path).isDirectory()) {
|
|
16287
16449
|
walk(path, inside);
|
|
16288
16450
|
} else {
|
|
16289
16451
|
hash.update(`${inside}
|
|
16290
16452
|
`);
|
|
16291
|
-
hash.update(
|
|
16453
|
+
hash.update(readFileSync9(path));
|
|
16292
16454
|
hash.update(`
|
|
16293
16455
|
`);
|
|
16294
16456
|
}
|
|
16295
16457
|
}
|
|
16296
16458
|
};
|
|
16297
16459
|
for (const name of EDITOR_PLUGINS) {
|
|
16298
|
-
walk(
|
|
16460
|
+
walk(join10(from, name), name);
|
|
16299
16461
|
}
|
|
16300
16462
|
return hash.digest("hex");
|
|
16301
16463
|
}
|
|
@@ -16310,58 +16472,58 @@ function shippedEditorDigest() {
|
|
|
16310
16472
|
return shippedDigest ?? undefined;
|
|
16311
16473
|
}
|
|
16312
16474
|
function installedEditorDigest(projectPath) {
|
|
16313
|
-
const marker =
|
|
16314
|
-
if (!
|
|
16475
|
+
const marker = join10(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
|
|
16476
|
+
if (!existsSync10(marker)) {
|
|
16315
16477
|
return null;
|
|
16316
16478
|
}
|
|
16317
|
-
const said =
|
|
16479
|
+
const said = readFileSync9(marker, "utf8").trim();
|
|
16318
16480
|
return said === "" ? null : said;
|
|
16319
16481
|
}
|
|
16320
16482
|
function shippedAddonsDirectory() {
|
|
16321
|
-
return
|
|
16483
|
+
return join10(dirname5(fileURLToPath(import.meta.url)), "godot", "addons");
|
|
16322
16484
|
}
|
|
16323
16485
|
function shippedOperationsScript() {
|
|
16324
|
-
return
|
|
16486
|
+
return join10(dirname5(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
|
|
16325
16487
|
}
|
|
16326
16488
|
function installAddons(projectPath, from = shippedAddonsDirectory()) {
|
|
16327
16489
|
const installed = [];
|
|
16328
16490
|
for (const name of ADDONS) {
|
|
16329
|
-
const source =
|
|
16330
|
-
if (!
|
|
16491
|
+
const source = join10(from, name);
|
|
16492
|
+
if (!existsSync10(join10(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
|
|
16331
16493
|
throw new Error(`The package holds no ${name} addon at ${source}.`);
|
|
16332
16494
|
}
|
|
16333
|
-
const target =
|
|
16334
|
-
const replaced =
|
|
16335
|
-
|
|
16336
|
-
mkdirSync4(
|
|
16495
|
+
const target = join10(projectPath, "addons", name);
|
|
16496
|
+
const replaced = existsSync10(target);
|
|
16497
|
+
rmSync5(target, { recursive: true, force: true });
|
|
16498
|
+
mkdirSync4(dirname5(target), { recursive: true });
|
|
16337
16499
|
cpSync(source, target, { recursive: true });
|
|
16338
|
-
|
|
16500
|
+
writeFileSync6(join10(target, VERSION_MARKER), `${SERVER_VERSION}
|
|
16339
16501
|
`);
|
|
16340
16502
|
installed.push({ name, path: target, replaced });
|
|
16341
16503
|
}
|
|
16342
|
-
|
|
16504
|
+
writeFileSync6(join10(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
|
|
16343
16505
|
`);
|
|
16344
16506
|
return installed;
|
|
16345
16507
|
}
|
|
16346
16508
|
function installedAddonVersion(projectPath) {
|
|
16347
|
-
const marker =
|
|
16348
|
-
if (!
|
|
16509
|
+
const marker = join10(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
16510
|
+
if (!existsSync10(marker)) {
|
|
16349
16511
|
return null;
|
|
16350
16512
|
}
|
|
16351
|
-
const said =
|
|
16513
|
+
const said = readFileSync9(marker, "utf8").trim();
|
|
16352
16514
|
return said === "" ? null : said;
|
|
16353
16515
|
}
|
|
16354
16516
|
function removeAddons(projectPath) {
|
|
16355
16517
|
const removed = [];
|
|
16356
16518
|
for (const name of ADDONS) {
|
|
16357
|
-
const target =
|
|
16358
|
-
if (
|
|
16359
|
-
|
|
16519
|
+
const target = join10(projectPath, "addons", name);
|
|
16520
|
+
if (existsSync10(target)) {
|
|
16521
|
+
rmSync5(target, { recursive: true, force: true });
|
|
16360
16522
|
removed.push(target);
|
|
16361
16523
|
}
|
|
16362
16524
|
}
|
|
16363
|
-
const addons =
|
|
16364
|
-
if (removed.length > 0 &&
|
|
16525
|
+
const addons = join10(projectPath, "addons");
|
|
16526
|
+
if (removed.length > 0 && existsSync10(addons) && readdirSync4(addons).length === 0) {
|
|
16365
16527
|
rmdirSync(addons);
|
|
16366
16528
|
removed.push(addons);
|
|
16367
16529
|
}
|
|
@@ -16385,7 +16547,7 @@ function autoloadIsOurs(named) {
|
|
|
16385
16547
|
return named === null || named === `res://${RUNTIME_AUTOLOAD.path}`;
|
|
16386
16548
|
}
|
|
16387
16549
|
async function setRuntime(engine, projectPath, on) {
|
|
16388
|
-
return on ? await runOperation(engine, "add_autoload", { name: RUNTIME_AUTOLOAD.name, path: `res://${RUNTIME_AUTOLOAD.path}`,
|
|
16550
|
+
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);
|
|
16389
16551
|
}
|
|
16390
16552
|
function loaderAmong(projectPath, named) {
|
|
16391
16553
|
let byName = null;
|
|
@@ -16395,7 +16557,7 @@ function loaderAmong(projectPath, named) {
|
|
|
16395
16557
|
}
|
|
16396
16558
|
let source = null;
|
|
16397
16559
|
try {
|
|
16398
|
-
source =
|
|
16560
|
+
source = readFileSync9(join10(projectPath, path), "utf8");
|
|
16399
16561
|
} catch {}
|
|
16400
16562
|
if (source?.includes(RUNTIME_AUTOLOAD.path)) {
|
|
16401
16563
|
return { name: entry, path, exists: true };
|
|
@@ -16437,10 +16599,10 @@ function enabledPlugins(settings) {
|
|
|
16437
16599
|
function inspectProject(projectPath) {
|
|
16438
16600
|
const problems = [];
|
|
16439
16601
|
const addons = ADDONS.map((name) => {
|
|
16440
|
-
const directory =
|
|
16441
|
-
const marker =
|
|
16442
|
-
const installed =
|
|
16443
|
-
const version =
|
|
16602
|
+
const directory = join10(projectPath, "addons", name);
|
|
16603
|
+
const marker = join10(directory, VERSION_MARKER);
|
|
16604
|
+
const installed = existsSync10(directory);
|
|
16605
|
+
const version = existsSync10(marker) ? readFileSync9(marker, "utf8").trim() : null;
|
|
16444
16606
|
const current = version === SERVER_VERSION;
|
|
16445
16607
|
if (!installed) {
|
|
16446
16608
|
problems.push(`addons/${name} is not installed; run gdharness setup`);
|
|
@@ -16449,7 +16611,7 @@ function inspectProject(projectPath) {
|
|
|
16449
16611
|
}
|
|
16450
16612
|
return { name, installed, version, current };
|
|
16451
16613
|
});
|
|
16452
|
-
const settings = parseProjectGodot(
|
|
16614
|
+
const settings = parseProjectGodot(readFileSync9(join10(projectPath, "project.godot"), "utf8"));
|
|
16453
16615
|
const pluginsEnabled = enabledPlugins(settings);
|
|
16454
16616
|
for (const name of EDITOR_PLUGINS) {
|
|
16455
16617
|
if (!pluginsEnabled.includes(name)) {
|
|
@@ -16465,7 +16627,7 @@ function inspectProject(projectPath) {
|
|
|
16465
16627
|
const tracked = trackedByGit(projectPath, [...named.values()]);
|
|
16466
16628
|
const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
|
|
16467
16629
|
for (const [name, path] of named) {
|
|
16468
|
-
if (!
|
|
16630
|
+
if (!existsSync10(join10(projectPath, path))) {
|
|
16469
16631
|
if (!addonMissing(path)) {
|
|
16470
16632
|
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`);
|
|
16471
16633
|
}
|
|
@@ -16625,7 +16787,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16625
16787
|
};
|
|
16626
16788
|
PROPERTIES = {
|
|
16627
16789
|
type: "object",
|
|
16628
|
-
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.',
|
|
16790
|
+
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.',
|
|
16629
16791
|
additionalProperties: true
|
|
16630
16792
|
};
|
|
16631
16793
|
XY = {
|
|
@@ -16780,11 +16942,18 @@ var init_tool_definitions = __esm(() => {
|
|
|
16780
16942
|
},
|
|
16781
16943
|
value: {
|
|
16782
16944
|
blank: true,
|
|
16783
|
-
description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}. "" writes an empty string.'
|
|
16945
|
+
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.'
|
|
16784
16946
|
},
|
|
16785
16947
|
name: { type: "string", description: "Autoload name." },
|
|
16786
16948
|
path: { type: "string", description: "Autoload script or scene inside the project." },
|
|
16787
|
-
enabled: {
|
|
16949
|
+
enabled: {
|
|
16950
|
+
type: "boolean",
|
|
16951
|
+
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."
|
|
16952
|
+
},
|
|
16953
|
+
global: {
|
|
16954
|
+
type: "boolean",
|
|
16955
|
+
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.`
|
|
16956
|
+
},
|
|
16788
16957
|
scenePath: SCENE_PATH,
|
|
16789
16958
|
actionName: { type: "string", description: 'Input action name, such as "jump".' },
|
|
16790
16959
|
events: INPUT_EVENTS,
|
|
@@ -16796,9 +16965,15 @@ var init_tool_definitions = __esm(() => {
|
|
|
16796
16965
|
},
|
|
16797
16966
|
pluginName: { type: "string", description: "Folder name under addons/." },
|
|
16798
16967
|
busName: { type: "string", description: "Audio bus name." },
|
|
16799
|
-
parentBusIndex: {
|
|
16968
|
+
parentBusIndex: {
|
|
16969
|
+
type: "number",
|
|
16970
|
+
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."
|
|
16971
|
+
},
|
|
16800
16972
|
busIndex: { type: "number", description: "Audio bus index." },
|
|
16801
|
-
effectIndex: {
|
|
16973
|
+
effectIndex: {
|
|
16974
|
+
type: "number",
|
|
16975
|
+
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."
|
|
16976
|
+
},
|
|
16802
16977
|
effectType: { type: "string", description: 'Effect class, such as "AudioEffectReverb".' },
|
|
16803
16978
|
volumeDb: { type: "number", description: "Bus volume in decibels." }
|
|
16804
16979
|
},
|
|
@@ -16817,7 +16992,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16817
16992
|
disable_plugin: { summary: "disable an addon", requires: ["pluginName"] },
|
|
16818
16993
|
add_audio_bus: { summary: "add an audio bus", requires: ["busName"] },
|
|
16819
16994
|
set_audio_bus_effect: {
|
|
16820
|
-
summary: "
|
|
16995
|
+
summary: "put an effect in a slot on a bus",
|
|
16821
16996
|
requires: ["busIndex", "effectIndex", "effectType"]
|
|
16822
16997
|
},
|
|
16823
16998
|
set_audio_bus_volume: { summary: "set a bus volume", requires: ["busIndex", "volumeDb"] }
|
|
@@ -16856,7 +17031,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
16856
17031
|
type: "boolean",
|
|
16857
17032
|
description: "forward: include the engine's own res://. resources. Default false."
|
|
16858
17033
|
},
|
|
16859
|
-
fileTypes: {
|
|
17034
|
+
fileTypes: {
|
|
17035
|
+
type: "array",
|
|
17036
|
+
items: { type: "string" },
|
|
17037
|
+
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.'
|
|
17038
|
+
}
|
|
16860
17039
|
},
|
|
16861
17040
|
requires: ["projectPath", "resourcePath"]
|
|
16862
17041
|
},
|
|
@@ -16901,7 +17080,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
16901
17080
|
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",
|
|
16902
17081
|
requires: []
|
|
16903
17082
|
},
|
|
16904
|
-
uid: {
|
|
17083
|
+
uid: {
|
|
17084
|
+
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",
|
|
17085
|
+
requires: ["resourcePath"]
|
|
17086
|
+
},
|
|
16905
17087
|
refresh_uids: {
|
|
16906
17088
|
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",
|
|
16907
17089
|
requires: []
|
|
@@ -16962,11 +17144,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
16962
17144
|
},
|
|
16963
17145
|
{
|
|
16964
17146
|
name: "scene_create",
|
|
16965
|
-
description: "Creates a scene file, saves one, or saves a copy under a new path. Needs the editor connected.",
|
|
17147
|
+
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.",
|
|
16966
17148
|
parameters: {
|
|
16967
17149
|
projectPath: PROJECT_PATH,
|
|
16968
17150
|
scenePath: SCENE_PATH,
|
|
16969
|
-
rootNodeType: { type: "string", description: "create: the root node class. Default
|
|
17151
|
+
rootNodeType: { type: "string", description: "create: the root node class. Default Node." },
|
|
16970
17152
|
newPath: { type: "string", description: "save_as: where the copy goes." }
|
|
16971
17153
|
},
|
|
16972
17154
|
requires: ["projectPath", "scenePath"],
|
|
@@ -16990,7 +17172,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16990
17172
|
},
|
|
16991
17173
|
{
|
|
16992
17174
|
name: "scene_node",
|
|
16993
|
-
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.",
|
|
17175
|
+
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.",
|
|
16994
17176
|
parameters: {
|
|
16995
17177
|
projectPath: PROJECT_PATH,
|
|
16996
17178
|
scenePath: SCENE_PATH,
|
|
@@ -17008,7 +17190,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
17008
17190
|
type: "boolean",
|
|
17009
17191
|
description: "get: include properties still at their default. Default false."
|
|
17010
17192
|
},
|
|
17011
|
-
layer: {
|
|
17193
|
+
layer: {
|
|
17194
|
+
type: "number",
|
|
17195
|
+
description: "set_tilemap_cells: the layer of a TileMap. Default 0. A TileMapLayer is one layer."
|
|
17196
|
+
},
|
|
17012
17197
|
cells: TILEMAP_CELLS
|
|
17013
17198
|
},
|
|
17014
17199
|
requires: ["projectPath", "scenePath"],
|
|
@@ -17019,7 +17204,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
17019
17204
|
duplicate: { summary: "copy a node and its children", requires: ["nodePath", "newName"] },
|
|
17020
17205
|
reparent: { summary: "move a node under another parent", requires: ["nodePath", "newParentPath"] },
|
|
17021
17206
|
delete: { summary: "remove a node and its children", requires: ["nodePath"] },
|
|
17022
|
-
set_tilemap_cells: {
|
|
17207
|
+
set_tilemap_cells: {
|
|
17208
|
+
summary: "place tiles in a TileMapLayer or TileMap",
|
|
17209
|
+
requires: ["nodePath", "cells"]
|
|
17210
|
+
}
|
|
17023
17211
|
}
|
|
17024
17212
|
},
|
|
17025
17213
|
{
|
|
@@ -17107,13 +17295,16 @@ var init_tool_definitions = __esm(() => {
|
|
|
17107
17295
|
},
|
|
17108
17296
|
{
|
|
17109
17297
|
name: "script_edit",
|
|
17110
|
-
description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type.
|
|
17298
|
+
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.",
|
|
17111
17299
|
parameters: {
|
|
17112
17300
|
projectPath: PROJECT_PATH,
|
|
17113
17301
|
scriptPath: SCRIPT_PATH,
|
|
17114
|
-
className: { type: "string", description: "create: a class_name for the script." },
|
|
17115
|
-
extends: { type: "string", description: "create: the base class. Default Node." },
|
|
17116
|
-
content: {
|
|
17302
|
+
className: { type: "string", description: "create: a class_name for the script. Not with content." },
|
|
17303
|
+
extends: { type: "string", description: "create: the base class. Default Node. Not with content." },
|
|
17304
|
+
content: {
|
|
17305
|
+
type: "string",
|
|
17306
|
+
description: "create: the whole file, written as given, instead of a template, className and extends."
|
|
17307
|
+
},
|
|
17117
17308
|
template: {
|
|
17118
17309
|
type: "string",
|
|
17119
17310
|
enum: ["singleton", "state_machine", "component", "resource"],
|
|
@@ -17311,7 +17502,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17311
17502
|
},
|
|
17312
17503
|
{
|
|
17313
17504
|
name: "editor_output",
|
|
17314
|
-
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.",
|
|
17505
|
+
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.",
|
|
17315
17506
|
parameters: {
|
|
17316
17507
|
severity: {
|
|
17317
17508
|
type: "string",
|
|
@@ -17375,7 +17566,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17375
17566
|
},
|
|
17376
17567
|
reloadScript: {
|
|
17377
17568
|
type: "string",
|
|
17378
|
-
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."
|
|
17569
|
+
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."
|
|
17379
17570
|
}
|
|
17380
17571
|
},
|
|
17381
17572
|
requires: ["projectPath"]
|
|
@@ -17521,7 +17712,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17521
17712
|
},
|
|
17522
17713
|
{
|
|
17523
17714
|
name: "runtime_invoke",
|
|
17524
|
-
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.",
|
|
17715
|
+
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.",
|
|
17525
17716
|
parameters: {
|
|
17526
17717
|
projectPath: RUNNING_PROJECT_PATH,
|
|
17527
17718
|
pid: RUNNING_PID,
|
|
@@ -17533,12 +17724,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
17533
17724
|
property: {
|
|
17534
17725
|
type: "string",
|
|
17535
17726
|
ops: ["set"],
|
|
17536
|
-
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
|
|
17727
|
+
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.'
|
|
17537
17728
|
},
|
|
17538
17729
|
value: {
|
|
17539
17730
|
ops: ["set"],
|
|
17540
17731
|
blank: true,
|
|
17541
|
-
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.`
|
|
17732
|
+
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.`
|
|
17542
17733
|
},
|
|
17543
17734
|
method: {
|
|
17544
17735
|
type: "string",
|
|
@@ -17548,7 +17739,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17548
17739
|
args: {
|
|
17549
17740
|
type: "array",
|
|
17550
17741
|
ops: ["call"],
|
|
17551
|
-
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.`
|
|
17742
|
+
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.`
|
|
17552
17743
|
},
|
|
17553
17744
|
timeoutMs: {
|
|
17554
17745
|
type: "integer",
|
|
@@ -17626,7 +17817,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17626
17817
|
says: {
|
|
17627
17818
|
type: "string",
|
|
17628
17819
|
ops: ["click"],
|
|
17629
|
-
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:
|
|
17820
|
+
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."
|
|
17630
17821
|
},
|
|
17631
17822
|
action: {
|
|
17632
17823
|
type: "string",
|
|
@@ -17659,7 +17850,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17659
17850
|
replace: {
|
|
17660
17851
|
type: "boolean",
|
|
17661
17852
|
ops: ["text"],
|
|
17662
|
-
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.'
|
|
17853
|
+
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.'
|
|
17663
17854
|
},
|
|
17664
17855
|
index: {
|
|
17665
17856
|
type: "number",
|
|
@@ -17703,11 +17894,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
17703
17894
|
requires: [],
|
|
17704
17895
|
operations: {
|
|
17705
17896
|
click: {
|
|
17706
|
-
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
|
|
17897
|
+
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",
|
|
17707
17898
|
requires: []
|
|
17708
17899
|
},
|
|
17709
17900
|
choose: {
|
|
17710
|
-
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
|
|
17901
|
+
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",
|
|
17711
17902
|
requires: ["nodePath"]
|
|
17712
17903
|
},
|
|
17713
17904
|
action: { summary: "press an action, or hold it", requires: ["action"] },
|
|
@@ -17716,7 +17907,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17716
17907
|
requires: ["keycode"]
|
|
17717
17908
|
},
|
|
17718
17909
|
text: {
|
|
17719
|
-
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",
|
|
17910
|
+
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",
|
|
17720
17911
|
requires: ["text"]
|
|
17721
17912
|
},
|
|
17722
17913
|
mouse_click: {
|
|
@@ -17755,7 +17946,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17755
17946
|
},
|
|
17756
17947
|
value: {
|
|
17757
17948
|
ops: ["until"],
|
|
17758
|
-
description: "until: the value to wait for, fitted to the property's type. On a property declared to
|
|
17949
|
+
description: "until: the value to wait for, fitted to the property's type. On a property holding an object, whether declared to or untyped and holding one or null, the text null means null. One declared to hold an object refuses any value other than null, since it can never hold it."
|
|
17759
17950
|
},
|
|
17760
17951
|
says: {
|
|
17761
17952
|
type: "string",
|
|
@@ -17804,7 +17995,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17804
17995
|
},
|
|
17805
17996
|
{
|
|
17806
17997
|
name: "debug_control",
|
|
17807
|
-
description: "Continues or steps the debugged game through the editor's debug adapter
|
|
17998
|
+
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.",
|
|
17808
17999
|
parameters: {},
|
|
17809
18000
|
requires: [],
|
|
17810
18001
|
operations: {
|
|
@@ -17815,12 +18006,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
17815
18006
|
},
|
|
17816
18007
|
{
|
|
17817
18008
|
name: "debug_state",
|
|
17818
|
-
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.",
|
|
18009
|
+
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.",
|
|
17819
18010
|
parameters: {
|
|
17820
18011
|
frameId: {
|
|
17821
18012
|
type: "number",
|
|
17822
18013
|
ops: ["variables"],
|
|
17823
|
-
description: "variables: which frame, from a stack answer. Default the innermost."
|
|
18014
|
+
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."
|
|
17824
18015
|
}
|
|
17825
18016
|
},
|
|
17826
18017
|
requires: [],
|
|
@@ -32587,10 +32778,10 @@ var init_stdio2 = __esm(() => {
|
|
|
32587
32778
|
});
|
|
32588
32779
|
|
|
32589
32780
|
// src/runtime-client.ts
|
|
32590
|
-
import { existsSync as
|
|
32781
|
+
import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync11, statSync as statSync5, unlinkSync } from "node:fs";
|
|
32591
32782
|
import { createConnection } from "node:net";
|
|
32592
32783
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
32593
|
-
import { dirname as
|
|
32784
|
+
import { dirname as dirname8, join as join13, resolve as resolve3 } from "node:path";
|
|
32594
32785
|
import { setTimeout as delay } from "node:timers/promises";
|
|
32595
32786
|
function runtimeDirectory(variables = process.env) {
|
|
32596
32787
|
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
@@ -32598,20 +32789,20 @@ function runtimeDirectory(variables = process.env) {
|
|
|
32598
32789
|
return explicit;
|
|
32599
32790
|
}
|
|
32600
32791
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
32601
|
-
return
|
|
32792
|
+
return join13(perUser ?? tmpdir3(), "gdharness");
|
|
32602
32793
|
}
|
|
32603
32794
|
function runtimeDirectories(variables = process.env) {
|
|
32604
32795
|
const candidates = [runtimeDirectory(variables)];
|
|
32605
32796
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
32606
32797
|
for (const base of fallbacks) {
|
|
32607
|
-
candidates.push(
|
|
32798
|
+
candidates.push(join13(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
32608
32799
|
}
|
|
32609
32800
|
return [...new Set(candidates.map((path) => resolve3(path)))];
|
|
32610
32801
|
}
|
|
32611
32802
|
function errorReportOf(pid, directories = runtimeDirectories()) {
|
|
32612
32803
|
for (const directory of directories) {
|
|
32613
|
-
const path =
|
|
32614
|
-
if (
|
|
32804
|
+
const path = join13(directory, `runtime-${pid}.log`);
|
|
32805
|
+
if (existsSync12(path)) {
|
|
32615
32806
|
return path;
|
|
32616
32807
|
}
|
|
32617
32808
|
}
|
|
@@ -32642,7 +32833,7 @@ function reportAlreadyInTranscript(transcript, report) {
|
|
|
32642
32833
|
function parseAnnouncement(file, pid) {
|
|
32643
32834
|
let fields;
|
|
32644
32835
|
try {
|
|
32645
|
-
fields = asParams(JSON.parse(
|
|
32836
|
+
fields = asParams(JSON.parse(readFileSync11(file, "utf8")));
|
|
32646
32837
|
} catch {
|
|
32647
32838
|
return { kind: "rubbish" };
|
|
32648
32839
|
}
|
|
@@ -32659,7 +32850,7 @@ function parseAnnouncement(file, pid) {
|
|
|
32659
32850
|
};
|
|
32660
32851
|
let announcedAt;
|
|
32661
32852
|
try {
|
|
32662
|
-
announcedAt =
|
|
32853
|
+
announcedAt = statSync5(file).mtimeMs;
|
|
32663
32854
|
} catch {}
|
|
32664
32855
|
const when = announcedAt === undefined ? {} : { announcedAt };
|
|
32665
32856
|
if (protocol !== RUNTIME_PROTOCOL) {
|
|
@@ -32700,7 +32891,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
|
|
|
32700
32891
|
function wentAway(file, pid) {
|
|
32701
32892
|
let project = "";
|
|
32702
32893
|
try {
|
|
32703
|
-
const fields = asParams(JSON.parse(
|
|
32894
|
+
const fields = asParams(JSON.parse(readFileSync11(file, "utf8")));
|
|
32704
32895
|
project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
|
|
32705
32896
|
} catch {
|
|
32706
32897
|
return;
|
|
@@ -32743,7 +32934,7 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
|
32743
32934
|
}).map((one) => one.file));
|
|
32744
32935
|
}
|
|
32745
32936
|
function announcedIn(directory) {
|
|
32746
|
-
if (!
|
|
32937
|
+
if (!existsSync12(directory)) {
|
|
32747
32938
|
return [];
|
|
32748
32939
|
}
|
|
32749
32940
|
const found = [];
|
|
@@ -32752,20 +32943,20 @@ function announcedIn(directory) {
|
|
|
32752
32943
|
for (const entry of readdirSync6(directory)) {
|
|
32753
32944
|
const report = ERROR_REPORT_PATTERN.exec(entry);
|
|
32754
32945
|
if (report) {
|
|
32755
|
-
sweepErrorReport(
|
|
32946
|
+
sweepErrorReport(join13(directory, entry), Number.parseInt(report[1] ?? "", 10));
|
|
32756
32947
|
continue;
|
|
32757
32948
|
}
|
|
32758
32949
|
const match = ANNOUNCEMENT_PATTERN.exec(entry);
|
|
32759
32950
|
if (!match) {
|
|
32760
32951
|
continue;
|
|
32761
32952
|
}
|
|
32762
|
-
const file =
|
|
32953
|
+
const file = join13(directory, entry);
|
|
32763
32954
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
32764
32955
|
const running = alive(pid);
|
|
32765
32956
|
entries.push({ file, pid, running });
|
|
32766
32957
|
if (running) {
|
|
32767
32958
|
try {
|
|
32768
|
-
candidates.push({ file, pid, writtenAt:
|
|
32959
|
+
candidates.push({ file, pid, writtenAt: statSync5(file).mtimeMs });
|
|
32769
32960
|
} catch {}
|
|
32770
32961
|
}
|
|
32771
32962
|
}
|
|
@@ -32787,16 +32978,16 @@ function announcedIn(directory) {
|
|
|
32787
32978
|
found.push(announced);
|
|
32788
32979
|
}
|
|
32789
32980
|
const held = new Set(entries.map((one) => one.file));
|
|
32790
|
-
const here =
|
|
32981
|
+
const here = join13(directory, ".");
|
|
32791
32982
|
for (const file of verdicts.keys()) {
|
|
32792
|
-
if (
|
|
32983
|
+
if (dirname8(file) === here && !held.has(file)) {
|
|
32793
32984
|
verdicts.delete(file);
|
|
32794
32985
|
}
|
|
32795
32986
|
}
|
|
32796
32987
|
return found;
|
|
32797
32988
|
}
|
|
32798
32989
|
async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
32799
|
-
const files = directories.map((directory) =>
|
|
32990
|
+
const files = directories.map((directory) => join13(directory, `runtime-${pid}.json`)).filter((file) => existsSync12(file));
|
|
32800
32991
|
const first = files[0];
|
|
32801
32992
|
if (first === undefined) {
|
|
32802
32993
|
return;
|
|
@@ -32822,7 +33013,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
|
|
|
32822
33013
|
return;
|
|
32823
33014
|
}
|
|
32824
33015
|
try {
|
|
32825
|
-
if (now -
|
|
33016
|
+
if (now - statSync5(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
|
|
32826
33017
|
unlinkSync(path);
|
|
32827
33018
|
}
|
|
32828
33019
|
} catch {}
|
|
@@ -32831,13 +33022,13 @@ async function announcedSince(projectPath, before, waiting = {}) {
|
|
|
32831
33022
|
const directories = waiting.directories ?? runtimeDirectories();
|
|
32832
33023
|
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
32833
33024
|
for (;; ) {
|
|
32834
|
-
const fresh = discoverRuntimes(directories).
|
|
32835
|
-
if (fresh
|
|
33025
|
+
const fresh = discoverRuntimes(directories).filter((endpoint) => !before.has(endpoint.pid) && isSameDirectory(endpoint.project.path, projectPath) && (waiting.accept?.(endpoint) ?? true));
|
|
33026
|
+
if (fresh.length > 0) {
|
|
32836
33027
|
return fresh;
|
|
32837
33028
|
}
|
|
32838
33029
|
const left = until - Date.now();
|
|
32839
33030
|
if (left <= 0 || waiting.giveUp?.() === true) {
|
|
32840
|
-
return
|
|
33031
|
+
return [];
|
|
32841
33032
|
}
|
|
32842
33033
|
await delay(Math.min(LOOK_EVERY_MS, left));
|
|
32843
33034
|
}
|
|
@@ -33096,14 +33287,14 @@ var init_runtime_client = __esm(() => {
|
|
|
33096
33287
|
});
|
|
33097
33288
|
|
|
33098
33289
|
// src/boot-note.ts
|
|
33099
|
-
import { mkdirSync as mkdirSync6, readFileSync as
|
|
33100
|
-
import { dirname as
|
|
33290
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
|
|
33291
|
+
import { dirname as dirname9, join as join14 } from "node:path";
|
|
33101
33292
|
function bootNotePath(projectPath) {
|
|
33102
|
-
return
|
|
33293
|
+
return join14(projectPath, ".godot", "gdharness-boot.json");
|
|
33103
33294
|
}
|
|
33104
33295
|
function readBootNote(projectPath) {
|
|
33105
33296
|
try {
|
|
33106
|
-
const noted = readNumber(asParams(JSON.parse(
|
|
33297
|
+
const noted = readNumber(asParams(JSON.parse(readFileSync12(bootNotePath(projectPath), "utf8"))), "announcedAfterMs");
|
|
33107
33298
|
return noted !== undefined && Number.isFinite(noted) && noted > 0 ? noted : null;
|
|
33108
33299
|
} catch {
|
|
33109
33300
|
return null;
|
|
@@ -33112,8 +33303,8 @@ function readBootNote(projectPath) {
|
|
|
33112
33303
|
function writeBootNote(projectPath, announcedAfterMs) {
|
|
33113
33304
|
try {
|
|
33114
33305
|
const path = bootNotePath(projectPath);
|
|
33115
|
-
mkdirSync6(
|
|
33116
|
-
|
|
33306
|
+
mkdirSync6(dirname9(path), { recursive: true });
|
|
33307
|
+
writeFileSync8(path, JSON.stringify({ announcedAfterMs: Math.round(announcedAfterMs), at: new Date().toISOString() }, null, 2), "utf8");
|
|
33117
33308
|
} catch {}
|
|
33118
33309
|
}
|
|
33119
33310
|
function halfAsLongAgain(lastBootMs) {
|
|
@@ -33131,10 +33322,10 @@ var init_boot_note = __esm(() => {
|
|
|
33131
33322
|
});
|
|
33132
33323
|
|
|
33133
33324
|
// src/breakpoint-note.ts
|
|
33134
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
33135
|
-
import { dirname as
|
|
33325
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
33326
|
+
import { dirname as dirname10, isAbsolute as isAbsolute2, join as join15, relative as relative2 } from "node:path";
|
|
33136
33327
|
function breakpointNotePath(projectPath) {
|
|
33137
|
-
return
|
|
33328
|
+
return join15(projectPath, ".godot", "gdharness-breakpoints.json");
|
|
33138
33329
|
}
|
|
33139
33330
|
function writeBreakpointNote(projectPath, held) {
|
|
33140
33331
|
const path = breakpointNotePath(projectPath);
|
|
@@ -33142,7 +33333,7 @@ function writeBreakpointNote(projectPath, held) {
|
|
|
33142
33333
|
const inside = held.map((one) => ({ scriptPath: realPathOr(one.scriptPath), lines: one.lines })).filter((one) => one.lines.length > 0 && withinProject(root, one.scriptPath));
|
|
33143
33334
|
try {
|
|
33144
33335
|
if (inside.length === 0) {
|
|
33145
|
-
|
|
33336
|
+
rmSync7(path, { force: true });
|
|
33146
33337
|
return;
|
|
33147
33338
|
}
|
|
33148
33339
|
const note = {
|
|
@@ -33151,8 +33342,8 @@ function writeBreakpointNote(projectPath, held) {
|
|
|
33151
33342
|
lines: [...one.lines]
|
|
33152
33343
|
}))
|
|
33153
33344
|
};
|
|
33154
|
-
mkdirSync7(
|
|
33155
|
-
|
|
33345
|
+
mkdirSync7(dirname10(path), { recursive: true });
|
|
33346
|
+
writeFileSync9(path, `${JSON.stringify(note, null, 2)}
|
|
33156
33347
|
`, "utf8");
|
|
33157
33348
|
} catch (error) {
|
|
33158
33349
|
console.error(`[SERVER] Could not record the breakpoints at ${path}: ${errorMessage(error)}`);
|
|
@@ -33160,7 +33351,7 @@ function writeBreakpointNote(projectPath, held) {
|
|
|
33160
33351
|
}
|
|
33161
33352
|
function readBreakpointNote(projectPath) {
|
|
33162
33353
|
try {
|
|
33163
|
-
const parsed = JSON.parse(
|
|
33354
|
+
const parsed = JSON.parse(readFileSync13(breakpointNotePath(projectPath), "utf8"));
|
|
33164
33355
|
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
|
|
33165
33356
|
return [];
|
|
33166
33357
|
}
|
|
@@ -33175,7 +33366,7 @@ function readBreakpointNote(projectPath) {
|
|
|
33175
33366
|
}
|
|
33176
33367
|
const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
|
|
33177
33368
|
if (lines.length > 0) {
|
|
33178
|
-
held.push({ scriptPath: realPathOr(
|
|
33369
|
+
held.push({ scriptPath: realPathOr(join15(projectPath, script)), lines });
|
|
33179
33370
|
}
|
|
33180
33371
|
}
|
|
33181
33372
|
return held;
|
|
@@ -33193,14 +33384,14 @@ var init_breakpoint_note = __esm(() => {
|
|
|
33193
33384
|
});
|
|
33194
33385
|
|
|
33195
33386
|
// src/class-note.ts
|
|
33196
|
-
import { mkdirSync as mkdirSync8, readFileSync as
|
|
33197
|
-
import { dirname as
|
|
33387
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "node:fs";
|
|
33388
|
+
import { dirname as dirname11, join as join16 } from "node:path";
|
|
33198
33389
|
function classNotePath(projectPath) {
|
|
33199
|
-
return
|
|
33390
|
+
return join16(projectPath, ".godot", "gdharness-classes.json");
|
|
33200
33391
|
}
|
|
33201
33392
|
function readClassNote(projectPath) {
|
|
33202
33393
|
try {
|
|
33203
|
-
const fields = asParams(JSON.parse(
|
|
33394
|
+
const fields = asParams(JSON.parse(readFileSync14(classNotePath(projectPath), "utf8")));
|
|
33204
33395
|
const rebuiltWith = readArray(fields, "rebuiltWith");
|
|
33205
33396
|
if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
|
|
33206
33397
|
return null;
|
|
@@ -33217,24 +33408,24 @@ function readClassNote(projectPath) {
|
|
|
33217
33408
|
function writeClassNote(projectPath, note) {
|
|
33218
33409
|
try {
|
|
33219
33410
|
const path = classNotePath(projectPath);
|
|
33220
|
-
mkdirSync8(
|
|
33221
|
-
|
|
33411
|
+
mkdirSync8(dirname11(path), { recursive: true });
|
|
33412
|
+
writeFileSync10(path, `${JSON.stringify(note, null, 2)}
|
|
33222
33413
|
`, "utf8");
|
|
33223
33414
|
} catch {}
|
|
33224
33415
|
}
|
|
33225
33416
|
var init_class_note = () => {};
|
|
33226
33417
|
|
|
33227
33418
|
// src/config-pin.ts
|
|
33228
|
-
import { existsSync as
|
|
33229
|
-
import { join as
|
|
33419
|
+
import { existsSync as existsSync13, readFileSync as readFileSync15 } from "node:fs";
|
|
33420
|
+
import { join as join17 } from "node:path";
|
|
33230
33421
|
function versionsNamedInConfig(projectPath) {
|
|
33231
|
-
const config =
|
|
33232
|
-
if (!
|
|
33422
|
+
const config = join17(projectPath, ".mcp.json");
|
|
33423
|
+
if (!existsSync13(config)) {
|
|
33233
33424
|
return [];
|
|
33234
33425
|
}
|
|
33235
33426
|
let text;
|
|
33236
33427
|
try {
|
|
33237
|
-
text =
|
|
33428
|
+
text = readFileSync15(config, "utf8");
|
|
33238
33429
|
JSON.parse(text);
|
|
33239
33430
|
} catch {
|
|
33240
33431
|
return [];
|
|
@@ -33383,6 +33574,9 @@ var init_ports = __esm(() => {
|
|
|
33383
33574
|
// src/dap_client.ts
|
|
33384
33575
|
import { createConnection as createConnection2 } from "node:net";
|
|
33385
33576
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
33577
|
+
function fileKey(path) {
|
|
33578
|
+
return process.platform === "win32" ? path.replaceAll("\\", "/").toLowerCase() : path;
|
|
33579
|
+
}
|
|
33386
33580
|
function said(body, field) {
|
|
33387
33581
|
const value = body?.[field];
|
|
33388
33582
|
return typeof value === "string" ? value : "";
|
|
@@ -33399,17 +33593,25 @@ class GodotDAPClient {
|
|
|
33399
33593
|
outputBuffer = [];
|
|
33400
33594
|
onOutput = null;
|
|
33401
33595
|
maxOutputLines = 1000;
|
|
33596
|
+
outputDropped = 0;
|
|
33597
|
+
outputTaken = 0;
|
|
33402
33598
|
initialized = false;
|
|
33403
33599
|
attached = false;
|
|
33404
33600
|
lastThreadId = 1;
|
|
33405
33601
|
halt = null;
|
|
33406
33602
|
holdKnown = false;
|
|
33603
|
+
stops = 0;
|
|
33407
33604
|
breakpoints = new Map;
|
|
33605
|
+
spelled = new Map;
|
|
33408
33606
|
inEditor = new Map;
|
|
33409
33607
|
onBreakpointsChanged = null;
|
|
33410
|
-
|
|
33608
|
+
scopesPatienceMs;
|
|
33609
|
+
stepPatienceMs;
|
|
33610
|
+
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1", patience = {}) {
|
|
33411
33611
|
this.port = port;
|
|
33412
33612
|
this.host = host;
|
|
33613
|
+
this.scopesPatienceMs = patience.scopesMs ?? SCOPES_TIMEOUT_MS;
|
|
33614
|
+
this.stepPatienceMs = patience.stepMs ?? STEP_LANDS_WITHIN_MS;
|
|
33413
33615
|
this.pendingRequests = new Map;
|
|
33414
33616
|
}
|
|
33415
33617
|
async connect() {
|
|
@@ -33570,6 +33772,7 @@ class GodotDAPClient {
|
|
|
33570
33772
|
}
|
|
33571
33773
|
}
|
|
33572
33774
|
if (this.outputBuffer.length > this.maxOutputLines) {
|
|
33775
|
+
this.outputDropped += this.outputBuffer.length - this.maxOutputLines;
|
|
33573
33776
|
this.outputBuffer = this.outputBuffer.slice(this.outputBuffer.length - this.maxOutputLines);
|
|
33574
33777
|
}
|
|
33575
33778
|
return;
|
|
@@ -33586,6 +33789,7 @@ class GodotDAPClient {
|
|
|
33586
33789
|
text: said(body, "text")
|
|
33587
33790
|
};
|
|
33588
33791
|
this.holdKnown = true;
|
|
33792
|
+
this.stops += 1;
|
|
33589
33793
|
return;
|
|
33590
33794
|
}
|
|
33591
33795
|
if (eventName === "continued") {
|
|
@@ -33602,20 +33806,24 @@ class GodotDAPClient {
|
|
|
33602
33806
|
if (typeof path !== "string" || typeof line !== "number") {
|
|
33603
33807
|
return;
|
|
33604
33808
|
}
|
|
33809
|
+
const key = fileKey(path);
|
|
33810
|
+
if (!this.spelled.has(key)) {
|
|
33811
|
+
this.spelled.set(key, path);
|
|
33812
|
+
}
|
|
33605
33813
|
if (said(body, "reason") === "removed") {
|
|
33606
|
-
this.inEditor.get(
|
|
33607
|
-
const held = this.breakpoints.get(
|
|
33814
|
+
this.inEditor.get(key)?.delete(line);
|
|
33815
|
+
const held = this.breakpoints.get(key);
|
|
33608
33816
|
if (held?.delete(line) === true) {
|
|
33609
33817
|
if (held.size === 0) {
|
|
33610
|
-
this.breakpoints.delete(
|
|
33818
|
+
this.breakpoints.delete(key);
|
|
33611
33819
|
}
|
|
33612
33820
|
this.onBreakpointsChanged?.(this.breakpointsHeld());
|
|
33613
33821
|
}
|
|
33614
33822
|
return;
|
|
33615
33823
|
}
|
|
33616
|
-
const known = this.inEditor.get(
|
|
33824
|
+
const known = this.inEditor.get(key) ?? new Set;
|
|
33617
33825
|
known.add(line);
|
|
33618
|
-
this.inEditor.set(
|
|
33826
|
+
this.inEditor.set(key, known);
|
|
33619
33827
|
return;
|
|
33620
33828
|
}
|
|
33621
33829
|
if (eventName === "terminated" || eventName === "exited") {
|
|
@@ -33698,24 +33906,44 @@ class GodotDAPClient {
|
|
|
33698
33906
|
getOutput(clear = false) {
|
|
33699
33907
|
const lines = [...this.outputBuffer];
|
|
33700
33908
|
if (clear) {
|
|
33909
|
+
this.outputTaken += lines.length;
|
|
33701
33910
|
this.outputBuffer = [];
|
|
33702
33911
|
}
|
|
33703
33912
|
return lines;
|
|
33704
33913
|
}
|
|
33914
|
+
outputSoFar() {
|
|
33915
|
+
const output = this.getOutput(false);
|
|
33916
|
+
const notShown = this.outputDropped + this.outputTaken;
|
|
33917
|
+
if (notShown === 0) {
|
|
33918
|
+
return { lines: output.length, output };
|
|
33919
|
+
}
|
|
33920
|
+
const where = [
|
|
33921
|
+
this.outputDropped > 0 ? `${this.outputDropped} fell out of this buffer, which keeps the last ${this.maxOutputLines}` : "",
|
|
33922
|
+
this.outputTaken > 0 ? `${this.outputTaken} were moved into the run's log by an earlier read` : ""
|
|
33923
|
+
].filter((part) => part !== "");
|
|
33924
|
+
return {
|
|
33925
|
+
lines: output.length,
|
|
33926
|
+
output,
|
|
33927
|
+
notShown,
|
|
33928
|
+
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.`
|
|
33929
|
+
};
|
|
33930
|
+
}
|
|
33705
33931
|
async setBreakpoint(filePath, line) {
|
|
33706
|
-
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
33932
|
+
const wanted = new Set(this.breakpoints.get(fileKey(filePath)) ?? []);
|
|
33707
33933
|
wanted.add(line);
|
|
33708
33934
|
return await this.sendBreakpoints(filePath, wanted);
|
|
33709
33935
|
}
|
|
33710
33936
|
async removeBreakpoint(filePath, line) {
|
|
33711
|
-
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
33937
|
+
const wanted = new Set(this.breakpoints.get(fileKey(filePath)) ?? []);
|
|
33712
33938
|
wanted.delete(line);
|
|
33713
33939
|
return await this.sendBreakpoints(filePath, wanted, line);
|
|
33714
33940
|
}
|
|
33715
33941
|
async sendBreakpoints(filePath, mine, clearing) {
|
|
33716
33942
|
await this.ensureConnected();
|
|
33717
33943
|
await this.initialize();
|
|
33718
|
-
const
|
|
33944
|
+
const key = fileKey(filePath);
|
|
33945
|
+
this.spelled.set(key, filePath);
|
|
33946
|
+
const lines = new Set([...mine, ...this.inEditor.get(key) ?? []]);
|
|
33719
33947
|
if (clearing !== undefined) {
|
|
33720
33948
|
lines.delete(clearing);
|
|
33721
33949
|
}
|
|
@@ -33725,14 +33953,14 @@ class GodotDAPClient {
|
|
|
33725
33953
|
breakpoints: sorted.map((breakpointLine) => ({ line: breakpointLine }))
|
|
33726
33954
|
});
|
|
33727
33955
|
if (mine.size === 0) {
|
|
33728
|
-
this.breakpoints.delete(
|
|
33956
|
+
this.breakpoints.delete(key);
|
|
33729
33957
|
} else {
|
|
33730
|
-
this.breakpoints.set(
|
|
33958
|
+
this.breakpoints.set(key, new Set(mine));
|
|
33731
33959
|
}
|
|
33732
33960
|
if (lines.size === 0) {
|
|
33733
|
-
this.inEditor.delete(
|
|
33961
|
+
this.inEditor.delete(key);
|
|
33734
33962
|
} else {
|
|
33735
|
-
this.inEditor.set(
|
|
33963
|
+
this.inEditor.set(key, lines);
|
|
33736
33964
|
}
|
|
33737
33965
|
return answer;
|
|
33738
33966
|
}
|
|
@@ -33741,11 +33969,11 @@ class GodotDAPClient {
|
|
|
33741
33969
|
}
|
|
33742
33970
|
breakpointsInEditor() {
|
|
33743
33971
|
const theirs = [];
|
|
33744
|
-
for (const [
|
|
33745
|
-
const mine = this.breakpoints.get(
|
|
33972
|
+
for (const [key, lines] of this.inEditor) {
|
|
33973
|
+
const mine = this.breakpoints.get(key);
|
|
33746
33974
|
const rest = Array.from(lines).filter((line) => !mine?.has(line)).sort((a, b) => a - b);
|
|
33747
33975
|
if (rest.length > 0) {
|
|
33748
|
-
theirs.push({ scriptPath, lines: rest });
|
|
33976
|
+
theirs.push({ scriptPath: this.spelled.get(key) ?? key, lines: rest });
|
|
33749
33977
|
}
|
|
33750
33978
|
}
|
|
33751
33979
|
return theirs;
|
|
@@ -33753,12 +33981,13 @@ class GodotDAPClient {
|
|
|
33753
33981
|
async reapplyBreakpoints() {
|
|
33754
33982
|
const applied = [];
|
|
33755
33983
|
const refused = [];
|
|
33756
|
-
for (const [
|
|
33984
|
+
for (const [key, lines] of Array.from(this.breakpoints)) {
|
|
33985
|
+
const filePath = this.spelled.get(key) ?? key;
|
|
33757
33986
|
try {
|
|
33758
33987
|
await this.sendBreakpoints(filePath, lines);
|
|
33759
33988
|
applied.push({ scriptPath: filePath, lines: Array.from(lines).sort((a, b) => a - b) });
|
|
33760
33989
|
} catch (error) {
|
|
33761
|
-
this.breakpoints.delete(
|
|
33990
|
+
this.breakpoints.delete(key);
|
|
33762
33991
|
refused.push({
|
|
33763
33992
|
scriptPath: filePath,
|
|
33764
33993
|
reason: error instanceof Error ? error.message : String(error)
|
|
@@ -33771,19 +34000,21 @@ class GodotDAPClient {
|
|
|
33771
34000
|
return { applied, refused };
|
|
33772
34001
|
}
|
|
33773
34002
|
breakpointsHeld() {
|
|
33774
|
-
return Array.from(this.breakpoints, ([
|
|
33775
|
-
scriptPath,
|
|
34003
|
+
return Array.from(this.breakpoints, ([key, lines]) => ({
|
|
34004
|
+
scriptPath: this.spelled.get(key) ?? key,
|
|
33776
34005
|
lines: Array.from(lines).sort((a, b) => a - b)
|
|
33777
34006
|
}));
|
|
33778
34007
|
}
|
|
33779
34008
|
holdBreakpoints(held) {
|
|
33780
34009
|
for (const { scriptPath, lines } of held) {
|
|
33781
|
-
const
|
|
34010
|
+
const key = fileKey(scriptPath);
|
|
34011
|
+
const fileBreakpoints = this.breakpoints.get(key) ?? new Set;
|
|
33782
34012
|
for (const line of lines) {
|
|
33783
34013
|
fileBreakpoints.add(line);
|
|
33784
34014
|
}
|
|
33785
34015
|
if (fileBreakpoints.size > 0) {
|
|
33786
|
-
this.breakpoints.set(
|
|
34016
|
+
this.breakpoints.set(key, fileBreakpoints);
|
|
34017
|
+
this.spelled.set(key, scriptPath);
|
|
33787
34018
|
}
|
|
33788
34019
|
}
|
|
33789
34020
|
}
|
|
@@ -33794,15 +34025,16 @@ class GodotDAPClient {
|
|
|
33794
34025
|
this.halt = null;
|
|
33795
34026
|
this.holdKnown = true;
|
|
33796
34027
|
}
|
|
33797
|
-
async
|
|
34028
|
+
async step(command) {
|
|
33798
34029
|
await this.attach();
|
|
33799
|
-
const resolvedThreadId = await this.resolveThreadId(
|
|
33800
|
-
|
|
33801
|
-
|
|
33802
|
-
|
|
33803
|
-
|
|
33804
|
-
|
|
33805
|
-
|
|
34030
|
+
const resolvedThreadId = await this.resolveThreadId();
|
|
34031
|
+
const before = this.stops;
|
|
34032
|
+
await this.sendRequest(command, { threadId: resolvedThreadId });
|
|
34033
|
+
const deadline = Date.now() + this.stepPatienceMs;
|
|
34034
|
+
while (this.stops === before && this.connected && Date.now() < deadline) {
|
|
34035
|
+
await delay2(50);
|
|
34036
|
+
}
|
|
34037
|
+
return this.stops !== before;
|
|
33806
34038
|
}
|
|
33807
34039
|
async getStackTrace(threadId) {
|
|
33808
34040
|
await this.attach();
|
|
@@ -33828,16 +34060,16 @@ class GodotDAPClient {
|
|
|
33828
34060
|
} catch {}
|
|
33829
34061
|
await delay2(200);
|
|
33830
34062
|
}
|
|
33831
|
-
return
|
|
34063
|
+
return null;
|
|
33832
34064
|
}
|
|
33833
34065
|
async getScopes(frameId) {
|
|
33834
34066
|
await this.attach();
|
|
33835
34067
|
const frames = await this.getStackTrace();
|
|
33836
34068
|
const frame = frameId === undefined ? frames[0] : frames.find((each) => each["id"] === frameId);
|
|
33837
34069
|
if (frame === undefined) {
|
|
33838
|
-
|
|
34070
|
+
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`);
|
|
33839
34071
|
}
|
|
33840
|
-
const deadline = Date.now() +
|
|
34072
|
+
const deadline = Date.now() + this.scopesPatienceMs;
|
|
33841
34073
|
let scopes = [];
|
|
33842
34074
|
while (scopes.length === 0 && Date.now() < deadline) {
|
|
33843
34075
|
try {
|
|
@@ -33849,6 +34081,9 @@ class GodotDAPClient {
|
|
|
33849
34081
|
} catch {}
|
|
33850
34082
|
await delay2(200);
|
|
33851
34083
|
}
|
|
34084
|
+
if (scopes.length === 0) {
|
|
34085
|
+
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`);
|
|
34086
|
+
}
|
|
33852
34087
|
const named = [];
|
|
33853
34088
|
for (const scope of scopes) {
|
|
33854
34089
|
const reference = scope["variablesReference"];
|
|
@@ -33912,9 +34147,8 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
33912
34147
|
try {
|
|
33913
34148
|
switch (toolName) {
|
|
33914
34149
|
case "dap_get_output": {
|
|
33915
|
-
const output = client.getOutput(false);
|
|
33916
34150
|
return {
|
|
33917
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
34151
|
+
content: [{ type: "text", text: JSON.stringify(client.outputSoFar(), null, 2) }]
|
|
33918
34152
|
};
|
|
33919
34153
|
}
|
|
33920
34154
|
case "dap_set_breakpoint": {
|
|
@@ -33941,19 +34175,13 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
33941
34175
|
}
|
|
33942
34176
|
case "dap_step_over":
|
|
33943
34177
|
case "dap_step_into": {
|
|
33944
|
-
|
|
33945
|
-
|
|
33946
|
-
|
|
33947
|
-
|
|
33948
|
-
|
|
33949
|
-
return {
|
|
33950
|
-
content: [
|
|
33951
|
-
{
|
|
33952
|
-
type: "text",
|
|
33953
|
-
text: JSON.stringify({ stepped: true, stack: await client.getStackTrace() }, null, 2)
|
|
33954
|
-
}
|
|
33955
|
-
]
|
|
34178
|
+
const heldAgain = await client.step(toolName === "dap_step_into" ? "stepIn" : "next");
|
|
34179
|
+
const answer = heldAgain ? { stepped: true, heldAgain, stack: await client.getStackTrace() } : {
|
|
34180
|
+
stepped: true,
|
|
34181
|
+
heldAgain,
|
|
34182
|
+
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.`
|
|
33956
34183
|
};
|
|
34184
|
+
return { content: [{ type: "text", text: JSON.stringify(answer, null, 2) }] };
|
|
33957
34185
|
}
|
|
33958
34186
|
case "dap_get_stack_trace": {
|
|
33959
34187
|
const stack = await client.getStackTrace();
|
|
@@ -33964,8 +34192,14 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
33964
34192
|
case "dap_get_variables": {
|
|
33965
34193
|
const frameId = typeof safeArgs.frameId === "number" ? safeArgs.frameId : undefined;
|
|
33966
34194
|
const scopes = await client.getScopes(frameId);
|
|
34195
|
+
const missing = scopes.filter((scope) => scope.variables === null).map((scope) => scope.name);
|
|
34196
|
+
const answer = missing.length === 0 ? { scopes } : {
|
|
34197
|
+
scopes,
|
|
34198
|
+
valuesMissing: missing,
|
|
34199
|
+
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.`
|
|
34200
|
+
};
|
|
33967
34201
|
return {
|
|
33968
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
34202
|
+
content: [{ type: "text", text: JSON.stringify(answer, null, 2) }]
|
|
33969
34203
|
};
|
|
33970
34204
|
}
|
|
33971
34205
|
default:
|
|
@@ -33973,18 +34207,22 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
33973
34207
|
}
|
|
33974
34208
|
} catch (error) {
|
|
33975
34209
|
const message = error instanceof Error ? error.message : String(error);
|
|
33976
|
-
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.";
|
|
34210
|
+
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.";
|
|
33977
34211
|
return {
|
|
33978
34212
|
isError: true,
|
|
33979
|
-
content: [
|
|
34213
|
+
content: [
|
|
34214
|
+
{ type: "text", text: `DAP tool '${toolName}' failed: ${message}.${cure === "" ? "" : ` ${cure}`}` }
|
|
34215
|
+
]
|
|
33980
34216
|
};
|
|
33981
34217
|
}
|
|
33982
34218
|
}
|
|
33983
|
-
var DEFAULT_DAP_PORT = 6006, DAP_REQUEST_TIMEOUT_MS = 1e4, SCOPES_TIMEOUT_MS = 20000, VARIABLES_POLL_TIMEOUT_MS = 2000;
|
|
34219
|
+
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;
|
|
33984
34220
|
var init_dap_client = __esm(() => {
|
|
33985
34221
|
init_errors();
|
|
33986
34222
|
init_framing();
|
|
33987
34223
|
init_ports();
|
|
34224
|
+
ReadRefusal = class ReadRefusal extends Refusal {
|
|
34225
|
+
};
|
|
33988
34226
|
});
|
|
33989
34227
|
|
|
33990
34228
|
// src/desktop.ts
|
|
@@ -33994,25 +34232,25 @@ var init_desktop = () => {};
|
|
|
33994
34232
|
// src/editor-log.ts
|
|
33995
34233
|
import {
|
|
33996
34234
|
closeSync,
|
|
33997
|
-
existsSync as
|
|
34235
|
+
existsSync as existsSync14,
|
|
33998
34236
|
mkdirSync as mkdirSync9,
|
|
33999
34237
|
openSync,
|
|
34000
|
-
readFileSync as
|
|
34238
|
+
readFileSync as readFileSync16,
|
|
34001
34239
|
readSync,
|
|
34002
|
-
rmSync as
|
|
34003
|
-
statSync as
|
|
34004
|
-
writeFileSync as
|
|
34240
|
+
rmSync as rmSync8,
|
|
34241
|
+
statSync as statSync6,
|
|
34242
|
+
writeFileSync as writeFileSync11
|
|
34005
34243
|
} from "node:fs";
|
|
34006
|
-
import { dirname as
|
|
34244
|
+
import { dirname as dirname12, join as join18 } from "node:path";
|
|
34007
34245
|
function editorLogPath(projectPath) {
|
|
34008
|
-
return
|
|
34246
|
+
return join18(projectPath, ".godot", "gdharness-editor.log");
|
|
34009
34247
|
}
|
|
34010
34248
|
function editorLogNotePath(projectPath) {
|
|
34011
|
-
return
|
|
34249
|
+
return join18(projectPath, ".godot", "gdharness-editor.json");
|
|
34012
34250
|
}
|
|
34013
34251
|
function readEditorLogNote(projectPath) {
|
|
34014
34252
|
try {
|
|
34015
|
-
const fields = asParams(JSON.parse(
|
|
34253
|
+
const fields = asParams(JSON.parse(readFileSync16(editorLogNotePath(projectPath), "utf8")));
|
|
34016
34254
|
const pid = readNumber(fields, "pid");
|
|
34017
34255
|
const startedAt = readString(fields, "startedAt");
|
|
34018
34256
|
return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
|
|
@@ -34023,16 +34261,16 @@ function readEditorLogNote(projectPath) {
|
|
|
34023
34261
|
function clearEditorLog(projectPath) {
|
|
34024
34262
|
try {
|
|
34025
34263
|
const path = editorLogPath(projectPath);
|
|
34026
|
-
mkdirSync9(
|
|
34027
|
-
|
|
34028
|
-
|
|
34264
|
+
mkdirSync9(dirname12(path), { recursive: true });
|
|
34265
|
+
rmSync8(path, { force: true });
|
|
34266
|
+
rmSync8(editorLogNotePath(projectPath), { force: true });
|
|
34029
34267
|
} catch {}
|
|
34030
34268
|
}
|
|
34031
34269
|
function writeEditorLogNote(projectPath, pid) {
|
|
34032
34270
|
try {
|
|
34033
34271
|
const path = editorLogNotePath(projectPath);
|
|
34034
|
-
mkdirSync9(
|
|
34035
|
-
|
|
34272
|
+
mkdirSync9(dirname12(path), { recursive: true });
|
|
34273
|
+
writeFileSync11(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
|
|
34036
34274
|
} catch {}
|
|
34037
34275
|
}
|
|
34038
34276
|
function theConsoleWasNotCaptured(why, projectPath) {
|
|
@@ -34053,7 +34291,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
|
34053
34291
|
return { kind: "older server", notedPid: noted?.pid ?? null };
|
|
34054
34292
|
}
|
|
34055
34293
|
const path = editorLogPath(projectPath);
|
|
34056
|
-
if (!
|
|
34294
|
+
if (!existsSync14(path)) {
|
|
34057
34295
|
return { kind: "not written" };
|
|
34058
34296
|
}
|
|
34059
34297
|
const log = new GameLog;
|
|
@@ -34063,9 +34301,9 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
|
34063
34301
|
return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
|
|
34064
34302
|
}
|
|
34065
34303
|
function theFirstOf(path) {
|
|
34066
|
-
const whole =
|
|
34304
|
+
const whole = statSync6(path).size;
|
|
34067
34305
|
if (whole <= MOST_CONSOLE_BYTES) {
|
|
34068
|
-
return { text:
|
|
34306
|
+
return { text: readFileSync16(path, "utf8") };
|
|
34069
34307
|
}
|
|
34070
34308
|
const handle = openSync(path, "r");
|
|
34071
34309
|
try {
|
|
@@ -37759,13 +37997,17 @@ function parseJUnit(xml) {
|
|
|
37759
37997
|
const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
|
|
37760
37998
|
const suites = suiteElements.map((suite) => {
|
|
37761
37999
|
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
38000
|
+
const failures = cases.filter((entry) => entry.status === "failed").length;
|
|
38001
|
+
const errors = cases.filter((entry) => entry.status === "error").length;
|
|
37762
38002
|
return {
|
|
37763
38003
|
name: suite.attributes["name"] ?? "",
|
|
37764
38004
|
path: pathOf(suite),
|
|
37765
38005
|
tests: cases.length,
|
|
37766
38006
|
discovered: count(suite.attributes, "tests"),
|
|
37767
|
-
failures
|
|
37768
|
-
errors
|
|
38007
|
+
failures,
|
|
38008
|
+
errors,
|
|
38009
|
+
hookFailures: Math.max(0, count(suite.attributes, "failures") - failures),
|
|
38010
|
+
hookErrors: Math.max(0, count(suite.attributes, "errors") - errors),
|
|
37769
38011
|
skipped: cases.filter((entry) => entry.status === "skipped").length,
|
|
37770
38012
|
time: count(suite.attributes, "time"),
|
|
37771
38013
|
cases
|
|
@@ -37776,11 +38018,19 @@ function parseJUnit(xml) {
|
|
|
37776
38018
|
tests: all.length,
|
|
37777
38019
|
failures: all.filter((entry) => entry.status === "failed").length,
|
|
37778
38020
|
errors: all.filter((entry) => entry.status === "error").length,
|
|
38021
|
+
hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
|
|
38022
|
+
hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
|
|
37779
38023
|
skipped: all.filter((entry) => entry.status === "skipped").length,
|
|
37780
38024
|
time: suites.reduce((sum, suite) => sum + suite.time, 0),
|
|
37781
38025
|
suites
|
|
37782
38026
|
};
|
|
37783
38027
|
}
|
|
38028
|
+
function hookFailuresPrinted(printed) {
|
|
38029
|
+
return printed.flatMap((line) => {
|
|
38030
|
+
const found = HOOK_FAILURE.exec(line);
|
|
38031
|
+
return found === null ? [] : [{ hook: found[2] ?? "", path: found[3] ?? "", line: Number(found[4]), message: found[1] ?? "" }];
|
|
38032
|
+
});
|
|
38033
|
+
}
|
|
37784
38034
|
function whyNoReport(printed, asked) {
|
|
37785
38035
|
const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
|
|
37786
38036
|
if (missing !== undefined) {
|
|
@@ -37845,13 +38095,17 @@ function withActualsPrinted(failed, printed) {
|
|
|
37845
38095
|
at: match[5] ?? "",
|
|
37846
38096
|
...readDiff(match[1] ?? "")
|
|
37847
38097
|
}));
|
|
38098
|
+
const used = new Set;
|
|
37848
38099
|
return failed.map((entry) => {
|
|
37849
38100
|
if (entry.detail === null) {
|
|
37850
38101
|
return entry;
|
|
37851
38102
|
}
|
|
37852
38103
|
let detail = entry.detail;
|
|
37853
38104
|
let recovered = false;
|
|
37854
|
-
for (const diff of diffs) {
|
|
38105
|
+
for (const [index, diff] of diffs.entries()) {
|
|
38106
|
+
if (used.has(index)) {
|
|
38107
|
+
continue;
|
|
38108
|
+
}
|
|
37855
38109
|
const wrong = ` but was
|
|
37856
38110
|
'${diff.merged.replaceAll("\r", "")}'${diff.suffix}
|
|
37857
38111
|
at ${diff.at}`;
|
|
@@ -37860,6 +38114,7 @@ function withActualsPrinted(failed, printed) {
|
|
|
37860
38114
|
'${diff.actual}'${diff.suffix}
|
|
37861
38115
|
at ${diff.at}`);
|
|
37862
38116
|
recovered = true;
|
|
38117
|
+
used.add(index);
|
|
37863
38118
|
}
|
|
37864
38119
|
}
|
|
37865
38120
|
const quoted = QUOTED_EQUALITY.exec(detail);
|
|
@@ -37911,7 +38166,7 @@ function orphansPrinted(printed) {
|
|
|
37911
38166
|
suites
|
|
37912
38167
|
};
|
|
37913
38168
|
}
|
|
37914
|
-
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";
|
|
38169
|
+
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";
|
|
37915
38170
|
var init_junit = __esm(() => {
|
|
37916
38171
|
MalformedReportError = class MalformedReportError extends Error {
|
|
37917
38172
|
constructor(message) {
|
|
@@ -37921,8 +38176,9 @@ var init_junit = __esm(() => {
|
|
|
37921
38176
|
};
|
|
37922
38177
|
ENTITIES = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
|
|
37923
38178
|
ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
|
|
38179
|
+
HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
|
|
37924
38180
|
NOTHING_RAN = /no test cases found/i;
|
|
37925
|
-
NO_SUCH_PATH = /given directory or file does not exists:\s*(\
|
|
38181
|
+
NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
|
|
37926
38182
|
ESCAPE = String.fromCharCode(27);
|
|
37927
38183
|
PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)(${IGNORING_CASE})?((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
|
|
37928
38184
|
QUOTED_EQUALITY = new RegExp(`^Expecting(?: error message)?:\\n '([\\s\\S]*)'\\n but was\\n '([\\s\\S]*)'(?:${IGNORING_CASE})?\\n\\tat `);
|
|
@@ -37940,15 +38196,15 @@ var init_junit = __esm(() => {
|
|
|
37940
38196
|
"<DEL>": ""
|
|
37941
38197
|
};
|
|
37942
38198
|
PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
|
|
37943
|
-
RUNNING_SUITE = /Run Test Suite:\s*(
|
|
38199
|
+
RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
|
|
37944
38200
|
ORPHANS = /(\d+)\s+orphans/;
|
|
37945
38201
|
});
|
|
37946
38202
|
|
|
37947
38203
|
// src/lsp_client.ts
|
|
37948
|
-
import { realpathSync as
|
|
38204
|
+
import { existsSync as existsSync15, realpathSync as realpathSync3 } from "node:fs";
|
|
37949
38205
|
import { readFile, realpath } from "node:fs/promises";
|
|
37950
38206
|
import { createConnection as createConnection3 } from "node:net";
|
|
37951
|
-
import { dirname as
|
|
38207
|
+
import { dirname as dirname13, join as join19, resolve as resolve4 } from "node:path";
|
|
37952
38208
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
37953
38209
|
function diagnosticsKey(uri) {
|
|
37954
38210
|
let decoded;
|
|
@@ -37961,7 +38217,7 @@ function diagnosticsKey(uri) {
|
|
|
37961
38217
|
return decoded;
|
|
37962
38218
|
}
|
|
37963
38219
|
try {
|
|
37964
|
-
return
|
|
38220
|
+
return realpathSync3.native(fileURLToPath3(decoded)).toLowerCase();
|
|
37965
38221
|
} catch {
|
|
37966
38222
|
return decoded.toLowerCase();
|
|
37967
38223
|
}
|
|
@@ -37978,8 +38234,10 @@ class GodotLSPClient {
|
|
|
37978
38234
|
connectPromise = null;
|
|
37979
38235
|
initialized = false;
|
|
37980
38236
|
rootPath = null;
|
|
38237
|
+
servedWorkspace = null;
|
|
37981
38238
|
diagnosticsWaiters = new Map;
|
|
37982
38239
|
documentVersions = new Map;
|
|
38240
|
+
documentTurns = new Map;
|
|
37983
38241
|
constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1") {
|
|
37984
38242
|
this.port = port;
|
|
37985
38243
|
this.host = host;
|
|
@@ -38076,7 +38334,7 @@ class GodotLSPClient {
|
|
|
38076
38334
|
if (this.initialized) {
|
|
38077
38335
|
return;
|
|
38078
38336
|
}
|
|
38079
|
-
const rootPath = this.rootPath ??
|
|
38337
|
+
const rootPath = this.rootPath ?? dirname13(resolve4(filePath));
|
|
38080
38338
|
await this.initialize(rootPath);
|
|
38081
38339
|
}
|
|
38082
38340
|
async sendRequest(method, params) {
|
|
@@ -38151,6 +38409,10 @@ class GodotLSPClient {
|
|
|
38151
38409
|
}
|
|
38152
38410
|
}
|
|
38153
38411
|
}
|
|
38412
|
+
if (message["method"] === "gdscript_client/changeWorkspace") {
|
|
38413
|
+
const path = message["params"]?.["path"];
|
|
38414
|
+
this.servedWorkspace = typeof path === "string" ? path : null;
|
|
38415
|
+
}
|
|
38154
38416
|
if (message["method"] === "textDocument/publishDiagnostics") {
|
|
38155
38417
|
const params = message["params"];
|
|
38156
38418
|
const paramsObject = params && typeof params === "object" ? params : null;
|
|
@@ -38253,10 +38515,14 @@ class GodotLSPClient {
|
|
|
38253
38515
|
this.sendNotification("textDocument/didClose", { textDocument: { uri } });
|
|
38254
38516
|
} catch {}
|
|
38255
38517
|
}
|
|
38518
|
+
forgetServedWorkspace() {
|
|
38519
|
+
this.servedWorkspace = null;
|
|
38520
|
+
}
|
|
38256
38521
|
async initialize(rootPath) {
|
|
38257
38522
|
await this.ensureConnected();
|
|
38258
38523
|
const resolvedRootPath = resolve4(rootPath);
|
|
38259
38524
|
const rootUri = pathToFileURL(resolvedRootPath).href;
|
|
38525
|
+
this.forgetServedWorkspace();
|
|
38260
38526
|
const result = await this.sendRequest("initialize", {
|
|
38261
38527
|
processId: process.pid,
|
|
38262
38528
|
rootPath: resolvedRootPath,
|
|
@@ -38282,60 +38548,82 @@ class GodotLSPClient {
|
|
|
38282
38548
|
}
|
|
38283
38549
|
]
|
|
38284
38550
|
});
|
|
38551
|
+
const served = this.servedWorkspace;
|
|
38552
|
+
if (served !== null && !isSameDirectory(served, resolvedRootPath)) {
|
|
38553
|
+
throw new AnotherProjectsServer(this.port, served, resolvedRootPath);
|
|
38554
|
+
}
|
|
38285
38555
|
this.sendNotification("initialized", {});
|
|
38286
38556
|
this.initialized = true;
|
|
38287
38557
|
this.rootPath = resolvedRootPath;
|
|
38288
38558
|
return result;
|
|
38289
38559
|
}
|
|
38290
|
-
async
|
|
38291
|
-
await this.ensureConnected();
|
|
38292
|
-
await this.ensureInitializedForFile(filePath);
|
|
38560
|
+
async withDocument(filePath, work) {
|
|
38293
38561
|
const key = diagnosticsKey(this.toFileUri(filePath));
|
|
38294
|
-
const
|
|
38295
|
-
|
|
38296
|
-
|
|
38297
|
-
|
|
38298
|
-
}
|
|
38299
|
-
const timer = setTimeout(() => {
|
|
38300
|
-
this.diagnosticsWaiters.delete(key);
|
|
38301
|
-
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."));
|
|
38302
|
-
}, DIAGNOSTICS_TIMEOUT_MS);
|
|
38303
|
-
this.diagnosticsWaiters.set(key, {
|
|
38304
|
-
resolve: resolveDiagnostics,
|
|
38305
|
-
reject: rejectDiagnostics,
|
|
38306
|
-
timer
|
|
38307
|
-
});
|
|
38562
|
+
const before = this.documentTurns.get(key) ?? Promise.resolve();
|
|
38563
|
+
let done = () => {};
|
|
38564
|
+
const mine = new Promise((finished) => {
|
|
38565
|
+
done = finished;
|
|
38308
38566
|
});
|
|
38309
|
-
|
|
38567
|
+
const turn = before.then(() => mine);
|
|
38568
|
+
this.documentTurns.set(key, turn);
|
|
38569
|
+
await before;
|
|
38310
38570
|
try {
|
|
38311
|
-
|
|
38312
|
-
return await diagnosticsPromise;
|
|
38313
|
-
} catch (error) {
|
|
38314
|
-
const waiter = this.diagnosticsWaiters.get(key);
|
|
38315
|
-
if (waiter) {
|
|
38316
|
-
clearTimeout(waiter.timer);
|
|
38317
|
-
this.diagnosticsWaiters.delete(key);
|
|
38318
|
-
}
|
|
38319
|
-
throw error;
|
|
38571
|
+
return await work();
|
|
38320
38572
|
} finally {
|
|
38321
|
-
|
|
38322
|
-
|
|
38573
|
+
done();
|
|
38574
|
+
if (this.documentTurns.get(key) === turn) {
|
|
38575
|
+
this.documentTurns.delete(key);
|
|
38323
38576
|
}
|
|
38324
38577
|
}
|
|
38325
38578
|
}
|
|
38326
|
-
async
|
|
38579
|
+
async getDiagnostics(filePath, content) {
|
|
38327
38580
|
await this.ensureConnected();
|
|
38328
38581
|
await this.ensureInitializedForFile(filePath);
|
|
38329
|
-
|
|
38330
|
-
|
|
38331
|
-
|
|
38332
|
-
|
|
38333
|
-
|
|
38334
|
-
|
|
38582
|
+
return this.withDocument(filePath, async () => {
|
|
38583
|
+
const key = diagnosticsKey(this.toFileUri(filePath));
|
|
38584
|
+
const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
|
|
38585
|
+
const timer = setTimeout(() => {
|
|
38586
|
+
this.diagnosticsWaiters.delete(key);
|
|
38587
|
+
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."));
|
|
38588
|
+
}, DIAGNOSTICS_TIMEOUT_MS);
|
|
38589
|
+
this.diagnosticsWaiters.set(key, {
|
|
38590
|
+
resolve: resolveDiagnostics,
|
|
38591
|
+
reject: rejectDiagnostics,
|
|
38592
|
+
timer
|
|
38593
|
+
});
|
|
38335
38594
|
});
|
|
38336
|
-
|
|
38337
|
-
|
|
38338
|
-
|
|
38595
|
+
let opened = null;
|
|
38596
|
+
try {
|
|
38597
|
+
opened = this.syncDocument(filePath, content);
|
|
38598
|
+
return await diagnosticsPromise;
|
|
38599
|
+
} catch (error) {
|
|
38600
|
+
const waiter = this.diagnosticsWaiters.get(key);
|
|
38601
|
+
if (waiter) {
|
|
38602
|
+
clearTimeout(waiter.timer);
|
|
38603
|
+
this.diagnosticsWaiters.delete(key);
|
|
38604
|
+
}
|
|
38605
|
+
throw error;
|
|
38606
|
+
} finally {
|
|
38607
|
+
if (opened !== null) {
|
|
38608
|
+
this.closeDocument(opened);
|
|
38609
|
+
}
|
|
38610
|
+
}
|
|
38611
|
+
});
|
|
38612
|
+
}
|
|
38613
|
+
async getCompletions(filePath, content, line, character) {
|
|
38614
|
+
await this.ensureConnected();
|
|
38615
|
+
await this.ensureInitializedForFile(filePath);
|
|
38616
|
+
const result = await this.withDocument(filePath, async () => {
|
|
38617
|
+
const uri = this.syncDocument(filePath, content);
|
|
38618
|
+
try {
|
|
38619
|
+
return await this.sendRequest("textDocument/completion", {
|
|
38620
|
+
textDocument: { uri },
|
|
38621
|
+
position: { line, character }
|
|
38622
|
+
});
|
|
38623
|
+
} finally {
|
|
38624
|
+
this.closeDocument(uri);
|
|
38625
|
+
}
|
|
38626
|
+
});
|
|
38339
38627
|
if (Array.isArray(result)) {
|
|
38340
38628
|
return result;
|
|
38341
38629
|
}
|
|
@@ -38350,28 +38638,31 @@ class GodotLSPClient {
|
|
|
38350
38638
|
async getHover(filePath, content, line, character) {
|
|
38351
38639
|
await this.ensureConnected();
|
|
38352
38640
|
await this.ensureInitializedForFile(filePath);
|
|
38353
|
-
|
|
38354
|
-
|
|
38355
|
-
|
|
38356
|
-
textDocument
|
|
38357
|
-
|
|
38358
|
-
|
|
38359
|
-
|
|
38360
|
-
|
|
38361
|
-
|
|
38641
|
+
return this.withDocument(filePath, async () => {
|
|
38642
|
+
const uri = this.syncDocument(filePath, content);
|
|
38643
|
+
try {
|
|
38644
|
+
return await this.sendRequest("textDocument/hover", {
|
|
38645
|
+
textDocument: { uri },
|
|
38646
|
+
position: { line, character }
|
|
38647
|
+
});
|
|
38648
|
+
} finally {
|
|
38649
|
+
this.closeDocument(uri);
|
|
38650
|
+
}
|
|
38651
|
+
});
|
|
38362
38652
|
}
|
|
38363
38653
|
async getDocumentSymbols(filePath, content) {
|
|
38364
38654
|
await this.ensureConnected();
|
|
38365
38655
|
await this.ensureInitializedForFile(filePath);
|
|
38366
|
-
const
|
|
38367
|
-
|
|
38368
|
-
|
|
38369
|
-
|
|
38370
|
-
|
|
38371
|
-
|
|
38372
|
-
|
|
38373
|
-
|
|
38374
|
-
|
|
38656
|
+
const result = await this.withDocument(filePath, async () => {
|
|
38657
|
+
const uri = this.syncDocument(filePath, content);
|
|
38658
|
+
try {
|
|
38659
|
+
return await this.sendRequest("textDocument/documentSymbol", {
|
|
38660
|
+
textDocument: { uri }
|
|
38661
|
+
});
|
|
38662
|
+
} finally {
|
|
38663
|
+
this.closeDocument(uri);
|
|
38664
|
+
}
|
|
38665
|
+
});
|
|
38375
38666
|
return Array.isArray(result) ? result : [];
|
|
38376
38667
|
}
|
|
38377
38668
|
isConnected() {
|
|
@@ -38403,36 +38694,39 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
38403
38694
|
try {
|
|
38404
38695
|
projectPath = await realpath(requestedProjectPath);
|
|
38405
38696
|
} catch {
|
|
38406
|
-
throw new
|
|
38697
|
+
throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
|
|
38698
|
+
}
|
|
38699
|
+
if (!existsSync15(join19(projectPath, "project.godot"))) {
|
|
38700
|
+
throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
|
|
38407
38701
|
}
|
|
38408
38702
|
const contained = resolveWithinProject(projectPath, scriptPathValue);
|
|
38409
38703
|
if (!contained.ok) {
|
|
38410
|
-
throw new
|
|
38704
|
+
throw new ArgumentRefusal(contained.reason);
|
|
38411
38705
|
}
|
|
38412
38706
|
let scriptPath;
|
|
38413
38707
|
try {
|
|
38414
38708
|
scriptPath = await realpath(contained.absolutePath);
|
|
38415
38709
|
} catch {
|
|
38416
|
-
throw new
|
|
38710
|
+
throw new ArgumentRefusal(`Script file does not exist: ${contained.absolutePath}`);
|
|
38417
38711
|
}
|
|
38418
38712
|
if (!isWithinRoot(projectPath, scriptPath)) {
|
|
38419
|
-
throw new
|
|
38713
|
+
throw new ArgumentRefusal("scriptPath resolves outside the project root boundary.");
|
|
38420
38714
|
}
|
|
38421
38715
|
return { projectPath, scriptPath };
|
|
38422
38716
|
}
|
|
38423
38717
|
async function handleLSPTool(client, toolName, args) {
|
|
38424
38718
|
try {
|
|
38425
38719
|
if (!args || typeof args !== "object") {
|
|
38426
|
-
throw new
|
|
38720
|
+
throw new ArgumentRefusal("Tool arguments must be an object.");
|
|
38427
38721
|
}
|
|
38428
38722
|
const parsedArgs = args;
|
|
38429
38723
|
const projectPathValue = parsedArgs["projectPath"];
|
|
38430
38724
|
const scriptPathValue = parsedArgs["scriptPath"];
|
|
38431
38725
|
if (typeof projectPathValue !== "string" || projectPathValue.length === 0) {
|
|
38432
|
-
throw new
|
|
38726
|
+
throw new ArgumentRefusal("Missing required argument: projectPath");
|
|
38433
38727
|
}
|
|
38434
38728
|
if (typeof scriptPathValue !== "string" || scriptPathValue.length === 0) {
|
|
38435
|
-
throw new
|
|
38729
|
+
throw new ArgumentRefusal("Missing required argument: scriptPath");
|
|
38436
38730
|
}
|
|
38437
38731
|
const { projectPath, scriptPath } = await resolveLSPPaths(projectPathValue, scriptPathValue);
|
|
38438
38732
|
const content = await readFile(scriptPath, "utf8");
|
|
@@ -38446,7 +38740,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
38446
38740
|
const line = Number(parsedArgs["line"]);
|
|
38447
38741
|
const character = Number(parsedArgs["character"]);
|
|
38448
38742
|
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
38449
|
-
throw new
|
|
38743
|
+
throw new ArgumentRefusal("Arguments line and character must be numbers.");
|
|
38450
38744
|
}
|
|
38451
38745
|
const completions = await client.getCompletions(scriptPath, content, line, character);
|
|
38452
38746
|
return asToolResponse({ completions });
|
|
@@ -38455,7 +38749,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
38455
38749
|
const line = Number(parsedArgs["line"]);
|
|
38456
38750
|
const character = Number(parsedArgs["character"]);
|
|
38457
38751
|
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
38458
|
-
throw new
|
|
38752
|
+
throw new ArgumentRefusal("Arguments line and character must be numbers.");
|
|
38459
38753
|
}
|
|
38460
38754
|
const hover = await client.getHover(scriptPath, content, line, character);
|
|
38461
38755
|
return asToolResponse({ hover });
|
|
@@ -38469,20 +38763,31 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
38469
38763
|
}
|
|
38470
38764
|
} catch (error) {
|
|
38471
38765
|
return asToolResponse({
|
|
38472
|
-
error: normalizeLSPError(error, client.port)
|
|
38766
|
+
error: normalizeLSPError(error, client.port),
|
|
38767
|
+
...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
|
|
38768
|
+
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {}
|
|
38473
38769
|
});
|
|
38474
38770
|
}
|
|
38475
38771
|
}
|
|
38476
|
-
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
38772
|
+
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, ArgumentRefusal, AnotherProjectsServer;
|
|
38477
38773
|
var init_lsp_client = __esm(() => {
|
|
38478
38774
|
init_errors();
|
|
38479
38775
|
init_framing();
|
|
38480
38776
|
init_paths();
|
|
38481
38777
|
init_ports();
|
|
38778
|
+
ArgumentRefusal = class ArgumentRefusal extends Refusal {
|
|
38779
|
+
};
|
|
38780
|
+
AnotherProjectsServer = class AnotherProjectsServer extends Refusal {
|
|
38781
|
+
serves;
|
|
38782
|
+
constructor(port, serves, asked) {
|
|
38783
|
+
super(`The language server on port ${port} belongs to the editor of ${serves}, not ${asked}.`);
|
|
38784
|
+
this.serves = serves;
|
|
38785
|
+
}
|
|
38786
|
+
};
|
|
38482
38787
|
});
|
|
38483
38788
|
|
|
38484
38789
|
// src/outside.ts
|
|
38485
|
-
import { spawn as
|
|
38790
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
38486
38791
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
38487
38792
|
function changesFrom(env, base) {
|
|
38488
38793
|
const changes = {};
|
|
@@ -38515,7 +38820,7 @@ function readLaunched(printed) {
|
|
|
38515
38820
|
}
|
|
38516
38821
|
async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
|
|
38517
38822
|
return await new Promise((resolve) => {
|
|
38518
|
-
const launcher =
|
|
38823
|
+
const launcher = spawn3(runtime, [keeper, "launch"], {
|
|
38519
38824
|
stdio: ["pipe", "pipe", "pipe"],
|
|
38520
38825
|
windowsHide: true
|
|
38521
38826
|
});
|
|
@@ -38550,8 +38855,8 @@ var init_outside = __esm(() => {
|
|
|
38550
38855
|
});
|
|
38551
38856
|
|
|
38552
38857
|
// src/process-time.ts
|
|
38553
|
-
import { execFile as
|
|
38554
|
-
import { promisify as
|
|
38858
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
38859
|
+
import { promisify as promisify3 } from "node:util";
|
|
38555
38860
|
async function cpuSecondsOf(pid) {
|
|
38556
38861
|
try {
|
|
38557
38862
|
return process.platform === "win32" ? await secondsOnWindows(pid) : await askPosix2(pid);
|
|
@@ -38565,7 +38870,7 @@ async function secondsOnWindows(pid) {
|
|
|
38565
38870
|
return Number.isFinite(seconds) ? seconds : undefined;
|
|
38566
38871
|
}
|
|
38567
38872
|
async function askPosix2(pid) {
|
|
38568
|
-
const { stdout } = await
|
|
38873
|
+
const { stdout } = await run3("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
|
|
38569
38874
|
return secondsFromClock(stdout.trim());
|
|
38570
38875
|
}
|
|
38571
38876
|
function secondsFromClock(printed) {
|
|
@@ -38583,20 +38888,20 @@ function secondsFromClock(printed) {
|
|
|
38583
38888
|
const extra = days === null ? 0 : Number(days) * 86400;
|
|
38584
38889
|
return Number.isFinite(extra) ? seconds + extra : undefined;
|
|
38585
38890
|
}
|
|
38586
|
-
var
|
|
38891
|
+
var run3, ASK_TIMEOUT_MS2 = 6000;
|
|
38587
38892
|
var init_process_time = __esm(() => {
|
|
38588
38893
|
init_windows_ask();
|
|
38589
|
-
|
|
38894
|
+
run3 = promisify3(execFile3);
|
|
38590
38895
|
});
|
|
38591
38896
|
|
|
38592
38897
|
// src/project-scan.ts
|
|
38593
|
-
import { existsSync as
|
|
38594
|
-
import { join as
|
|
38898
|
+
import { existsSync as existsSync16, readdirSync as readdirSync7, readFileSync as readFileSync17 } from "node:fs";
|
|
38899
|
+
import { join as join20 } from "node:path";
|
|
38595
38900
|
function skipped(name) {
|
|
38596
38901
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
38597
38902
|
}
|
|
38598
38903
|
function steppedOver(directory) {
|
|
38599
|
-
return
|
|
38904
|
+
return existsSync16(join20(directory, ".gdignore"));
|
|
38600
38905
|
}
|
|
38601
38906
|
function projectStructure(projectPath) {
|
|
38602
38907
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
@@ -38609,7 +38914,7 @@ function projectStructure(projectPath) {
|
|
|
38609
38914
|
continue;
|
|
38610
38915
|
}
|
|
38611
38916
|
if (entry.isDirectory()) {
|
|
38612
|
-
visit(
|
|
38917
|
+
visit(join20(directory, entry.name));
|
|
38613
38918
|
} else if (entry.isFile()) {
|
|
38614
38919
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
38615
38920
|
if (extension === "tscn") {
|
|
@@ -38639,14 +38944,14 @@ function scriptsWithoutUid(projectPath) {
|
|
|
38639
38944
|
}
|
|
38640
38945
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
38641
38946
|
if (entry.isDirectory()) {
|
|
38642
|
-
visit(
|
|
38947
|
+
visit(join20(directory, entry.name), spelled);
|
|
38643
38948
|
continue;
|
|
38644
38949
|
}
|
|
38645
38950
|
if (!entry.isFile()) {
|
|
38646
38951
|
continue;
|
|
38647
38952
|
}
|
|
38648
38953
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
38649
|
-
if (CARRIES_A_UID.has(extension) && !
|
|
38954
|
+
if (CARRIES_A_UID.has(extension) && !existsSync16(join20(directory, `${entry.name}.uid`))) {
|
|
38650
38955
|
found.push(spelled);
|
|
38651
38956
|
}
|
|
38652
38957
|
}
|
|
@@ -38681,7 +38986,7 @@ function searchProject(projectPath, options) {
|
|
|
38681
38986
|
if (skipped(entry.name)) {
|
|
38682
38987
|
continue;
|
|
38683
38988
|
}
|
|
38684
|
-
const entryPath =
|
|
38989
|
+
const entryPath = join20(directory, entry.name);
|
|
38685
38990
|
if (entry.isDirectory()) {
|
|
38686
38991
|
visit(entryPath);
|
|
38687
38992
|
continue;
|
|
@@ -38692,7 +38997,7 @@ function searchProject(projectPath, options) {
|
|
|
38692
38997
|
}
|
|
38693
38998
|
result.summary.files_searched += 1;
|
|
38694
38999
|
const matches = [];
|
|
38695
|
-
for (const [index, line] of
|
|
39000
|
+
for (const [index, line] of readFileSync17(entryPath, "utf8").split(`
|
|
38696
39001
|
`).entries()) {
|
|
38697
39002
|
if (full()) {
|
|
38698
39003
|
break;
|
|
@@ -38720,37 +39025,16 @@ var init_project_scan = __esm(() => {
|
|
|
38720
39025
|
CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
38721
39026
|
});
|
|
38722
39027
|
|
|
38723
|
-
// src/reimport.ts
|
|
38724
|
-
import { createHash as createHash3 } from "node:crypto";
|
|
38725
|
-
import { existsSync as existsSync14, readFileSync as readFileSync16, rmSync as rmSync8, statSync as statSync6, utimesSync, writeFileSync as writeFileSync11 } from "node:fs";
|
|
38726
|
-
import { basename as basename3, join as join19 } from "node:path";
|
|
38727
|
-
function forceNextImport(projectPath, resourcePath) {
|
|
38728
|
-
const relative = resourcePath.replace(/^res:\/\//, "");
|
|
38729
|
-
const sidecar = join19(projectPath, `${relative}.import`);
|
|
38730
|
-
if (!existsSync14(sidecar)) {
|
|
38731
|
-
return;
|
|
38732
|
-
}
|
|
38733
|
-
const { atime, mtimeMs } = statSync6(sidecar);
|
|
38734
|
-
writeFileSync11(sidecar, readFileSync16(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
|
|
38735
|
-
utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
|
|
38736
|
-
rmSync8(importRecord(projectPath, resourcePath), { force: true });
|
|
38737
|
-
}
|
|
38738
|
-
function importRecord(projectPath, resourcePath) {
|
|
38739
|
-
const digest = createHash3("md5").update(resourcePath).digest("hex");
|
|
38740
|
-
return join19(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
|
|
38741
|
-
}
|
|
38742
|
-
var init_reimport = () => {};
|
|
38743
|
-
|
|
38744
39028
|
// src/restart-note.ts
|
|
38745
|
-
import { mkdirSync as mkdirSync10, readFileSync as
|
|
38746
|
-
import { dirname as
|
|
39029
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync18, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "node:fs";
|
|
39030
|
+
import { dirname as dirname14, join as join21 } from "node:path";
|
|
38747
39031
|
function restartNotePath(projectPath) {
|
|
38748
|
-
return
|
|
39032
|
+
return join21(projectPath, ".godot", "gdharness-restart.json");
|
|
38749
39033
|
}
|
|
38750
39034
|
function noteRestartBegun(note) {
|
|
38751
39035
|
const path = restartNotePath(note.projectPath);
|
|
38752
39036
|
try {
|
|
38753
|
-
mkdirSync10(
|
|
39037
|
+
mkdirSync10(dirname14(path), { recursive: true });
|
|
38754
39038
|
writeFileSync12(path, `${JSON.stringify(note, null, 2)}
|
|
38755
39039
|
`, "utf8");
|
|
38756
39040
|
} catch (error) {
|
|
@@ -38759,7 +39043,7 @@ function noteRestartBegun(note) {
|
|
|
38759
39043
|
}
|
|
38760
39044
|
function restartOwed(projectPath) {
|
|
38761
39045
|
try {
|
|
38762
|
-
const parsed = JSON.parse(
|
|
39046
|
+
const parsed = JSON.parse(readFileSync18(restartNotePath(projectPath), "utf8"));
|
|
38763
39047
|
if (typeof parsed !== "object" || parsed === null) {
|
|
38764
39048
|
return null;
|
|
38765
39049
|
}
|
|
@@ -38784,31 +39068,31 @@ var init_restart_note = __esm(() => {
|
|
|
38784
39068
|
});
|
|
38785
39069
|
|
|
38786
39070
|
// src/run-record.ts
|
|
38787
|
-
import { execFile as
|
|
39071
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
38788
39072
|
import { createHash as createHash4 } from "node:crypto";
|
|
38789
39073
|
import {
|
|
38790
39074
|
mkdirSync as mkdirSync11,
|
|
38791
39075
|
openSync as openSync2,
|
|
38792
39076
|
readdirSync as readdirSync8,
|
|
38793
|
-
readFileSync as
|
|
38794
|
-
realpathSync as
|
|
39077
|
+
readFileSync as readFileSync19,
|
|
39078
|
+
realpathSync as realpathSync4,
|
|
38795
39079
|
rmSync as rmSync10,
|
|
38796
39080
|
statSync as statSync7,
|
|
38797
39081
|
writeFileSync as writeFileSync13
|
|
38798
39082
|
} from "node:fs";
|
|
38799
|
-
import { basename as basename4, dirname as
|
|
38800
|
-
import { promisify as
|
|
39083
|
+
import { basename as basename4, dirname as dirname15, join as join22, resolve as resolve5 } from "node:path";
|
|
39084
|
+
import { promisify as promisify4 } from "node:util";
|
|
38801
39085
|
function runsDirectory() {
|
|
38802
|
-
return
|
|
39086
|
+
return join22(runtimeDirectory(), "runs");
|
|
38803
39087
|
}
|
|
38804
39088
|
function realAsFarAsItExists(path) {
|
|
38805
39089
|
const missing = [];
|
|
38806
39090
|
let at = path;
|
|
38807
39091
|
for (;; ) {
|
|
38808
39092
|
try {
|
|
38809
|
-
return
|
|
39093
|
+
return join22(realpathSync4.native(at), ...missing);
|
|
38810
39094
|
} catch {
|
|
38811
|
-
const parent =
|
|
39095
|
+
const parent = dirname15(at);
|
|
38812
39096
|
if (parent === at) {
|
|
38813
39097
|
return path;
|
|
38814
39098
|
}
|
|
@@ -38823,15 +39107,18 @@ function projectKey(projectPath) {
|
|
|
38823
39107
|
return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
38824
39108
|
}
|
|
38825
39109
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
38826
|
-
return
|
|
39110
|
+
return join22(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
38827
39111
|
}
|
|
38828
39112
|
function openTranscript(startedAt) {
|
|
38829
39113
|
mkdirSync11(runsDirectory(), { recursive: true });
|
|
38830
|
-
const path =
|
|
39114
|
+
const path = join22(runsDirectory(), `run-${startedAt}.log`);
|
|
38831
39115
|
return { path, fd: openSync2(path, "a") };
|
|
38832
39116
|
}
|
|
39117
|
+
function noteIsAbout(note, playing) {
|
|
39118
|
+
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
39119
|
+
}
|
|
38833
39120
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
38834
|
-
return
|
|
39121
|
+
return join22(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
38835
39122
|
}
|
|
38836
39123
|
function writeEditorRunNote(note) {
|
|
38837
39124
|
mkdirSync11(runsDirectory(), { recursive: true });
|
|
@@ -38839,7 +39126,7 @@ function writeEditorRunNote(note) {
|
|
|
38839
39126
|
}
|
|
38840
39127
|
function readEditorRunNote(projectPath) {
|
|
38841
39128
|
for (const directory of runtimeDirectories()) {
|
|
38842
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
39129
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join22(directory, "runs", "editor-run.json"));
|
|
38843
39130
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
38844
39131
|
return note;
|
|
38845
39132
|
}
|
|
@@ -38849,7 +39136,7 @@ function readEditorRunNote(projectPath) {
|
|
|
38849
39136
|
function editorNoteAt(path) {
|
|
38850
39137
|
let parsed;
|
|
38851
39138
|
try {
|
|
38852
|
-
parsed = JSON.parse(
|
|
39139
|
+
parsed = JSON.parse(readFileSync19(path, "utf8"));
|
|
38853
39140
|
} catch {
|
|
38854
39141
|
return null;
|
|
38855
39142
|
}
|
|
@@ -38863,7 +39150,9 @@ function editorNoteAt(path) {
|
|
|
38863
39150
|
if (typeof projectPath !== "string" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
38864
39151
|
return null;
|
|
38865
39152
|
}
|
|
38866
|
-
|
|
39153
|
+
const play = fields["play"];
|
|
39154
|
+
const which = typeof play === "object" && play !== null && typeof play["play"] === "number" && typeof play["editorPid"] === "number" ? play : undefined;
|
|
39155
|
+
return { projectPath, transcript, startedAt, ...which === undefined ? {} : { play: which } };
|
|
38867
39156
|
}
|
|
38868
39157
|
function readRunRecord(projectPath) {
|
|
38869
39158
|
for (const directory of runtimeDirectories()) {
|
|
@@ -38871,7 +39160,7 @@ function readRunRecord(projectPath) {
|
|
|
38871
39160
|
if (own !== null) {
|
|
38872
39161
|
return own;
|
|
38873
39162
|
}
|
|
38874
|
-
const shared = recordAt(
|
|
39163
|
+
const shared = recordAt(join22(directory, "runs", SHARED_NOTE));
|
|
38875
39164
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
38876
39165
|
return shared;
|
|
38877
39166
|
}
|
|
@@ -38883,13 +39172,13 @@ function everyRunRecord() {
|
|
|
38883
39172
|
for (const directory of runtimeDirectories()) {
|
|
38884
39173
|
let entries;
|
|
38885
39174
|
try {
|
|
38886
|
-
entries = readdirSync8(
|
|
39175
|
+
entries = readdirSync8(join22(directory, "runs"));
|
|
38887
39176
|
} catch {
|
|
38888
39177
|
continue;
|
|
38889
39178
|
}
|
|
38890
39179
|
for (const entry of entries) {
|
|
38891
39180
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
38892
|
-
const record = recordAt(
|
|
39181
|
+
const record = recordAt(join22(directory, "runs", entry));
|
|
38893
39182
|
if (record !== null) {
|
|
38894
39183
|
found.push(record);
|
|
38895
39184
|
}
|
|
@@ -38901,7 +39190,7 @@ function everyRunRecord() {
|
|
|
38901
39190
|
function recordAt(path) {
|
|
38902
39191
|
let parsed;
|
|
38903
39192
|
try {
|
|
38904
|
-
parsed = JSON.parse(
|
|
39193
|
+
parsed = JSON.parse(readFileSync19(path, "utf8"));
|
|
38905
39194
|
} catch {
|
|
38906
39195
|
return null;
|
|
38907
39196
|
}
|
|
@@ -38947,7 +39236,7 @@ async function runningAs(pid) {
|
|
|
38947
39236
|
const began = await startedAt(pid, null);
|
|
38948
39237
|
const when = began === null ? {} : { startedAt: began };
|
|
38949
39238
|
if (process.platform === "linux") {
|
|
38950
|
-
const raw =
|
|
39239
|
+
const raw = readFileSync19(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
38951
39240
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
38952
39241
|
}
|
|
38953
39242
|
const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
|
|
@@ -39075,7 +39364,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
39075
39364
|
}
|
|
39076
39365
|
function clearRunRecord(projectPath) {
|
|
39077
39366
|
for (const directory of runtimeDirectories()) {
|
|
39078
|
-
const shared =
|
|
39367
|
+
const shared = join22(directory, "runs", SHARED_NOTE);
|
|
39079
39368
|
const sharedRecord = recordAt(shared);
|
|
39080
39369
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
39081
39370
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
@@ -39105,7 +39394,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
39105
39394
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
39106
39395
|
continue;
|
|
39107
39396
|
}
|
|
39108
|
-
const path =
|
|
39397
|
+
const path = join22(directory, entry);
|
|
39109
39398
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
39110
39399
|
continue;
|
|
39111
39400
|
}
|
|
@@ -39120,31 +39409,31 @@ var init_run_record = __esm(() => {
|
|
|
39120
39409
|
init_process_children();
|
|
39121
39410
|
init_runtime_client();
|
|
39122
39411
|
init_windows_ask();
|
|
39123
|
-
execFileAsync =
|
|
39412
|
+
execFileAsync = promisify4(execFile4);
|
|
39124
39413
|
});
|
|
39125
39414
|
|
|
39126
39415
|
// src/server.ts
|
|
39127
|
-
import { execFile as
|
|
39416
|
+
import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
|
|
39128
39417
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
39129
39418
|
import {
|
|
39130
39419
|
appendFileSync,
|
|
39131
39420
|
closeSync as closeSync2,
|
|
39132
39421
|
constants,
|
|
39133
39422
|
copyFileSync,
|
|
39134
|
-
existsSync as
|
|
39423
|
+
existsSync as existsSync17,
|
|
39135
39424
|
fstatSync,
|
|
39136
39425
|
mkdirSync as mkdirSync12,
|
|
39137
39426
|
openSync as openSync3,
|
|
39138
39427
|
readdirSync as readdirSync9,
|
|
39139
|
-
readFileSync as
|
|
39428
|
+
readFileSync as readFileSync20,
|
|
39140
39429
|
readSync as readSync2,
|
|
39141
39430
|
rmdirSync as rmdirSync2,
|
|
39142
39431
|
statSync as statSync8
|
|
39143
39432
|
} from "node:fs";
|
|
39144
|
-
import { basename as basename5, dirname as
|
|
39433
|
+
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";
|
|
39145
39434
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
39146
39435
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
39147
|
-
import { promisify as
|
|
39436
|
+
import { promisify as promisify5 } from "node:util";
|
|
39148
39437
|
function captureDestinationRefusal(outputPath, projects) {
|
|
39149
39438
|
if (/^(res|user):\/\//.test(outputPath)) {
|
|
39150
39439
|
return `outputPath ${outputPath} is a Godot path; a capture is saved outside the project, at an absolute path on this machine.`;
|
|
@@ -39155,16 +39444,16 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
39155
39444
|
if (extname2(outputPath).toLowerCase() !== ".png") {
|
|
39156
39445
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
39157
39446
|
}
|
|
39158
|
-
const directory = realPathOr(
|
|
39159
|
-
if (!
|
|
39160
|
-
return `outputPath's directory ${
|
|
39447
|
+
const directory = realPathOr(dirname16(outputPath));
|
|
39448
|
+
if (!existsSync17(directory)) {
|
|
39449
|
+
return `outputPath's directory ${dirname16(outputPath)} does not exist; a capture creates no directories.`;
|
|
39161
39450
|
}
|
|
39162
|
-
const where =
|
|
39451
|
+
const where = join23(directory, basename5(outputPath));
|
|
39163
39452
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
39164
39453
|
if (inside !== undefined) {
|
|
39165
39454
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
39166
39455
|
}
|
|
39167
|
-
if (
|
|
39456
|
+
if (existsSync17(where)) {
|
|
39168
39457
|
return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
|
|
39169
39458
|
}
|
|
39170
39459
|
return null;
|
|
@@ -39195,16 +39484,18 @@ function describeHalt(halt) {
|
|
|
39195
39484
|
function exited(run) {
|
|
39196
39485
|
return run.exitCode !== null || run.exitSignal !== null;
|
|
39197
39486
|
}
|
|
39198
|
-
function endingOf(failed) {
|
|
39199
|
-
return {
|
|
39200
|
-
exitCode: typeof failed.code === "number" ? failed.code : null,
|
|
39201
|
-
exitSignal: typeof failed.signal === "string" ? failed.signal : null,
|
|
39202
|
-
failure: typeof failed.code === "string" ? failed.code : null
|
|
39203
|
-
};
|
|
39204
|
-
}
|
|
39205
39487
|
function howItExited(run) {
|
|
39206
39488
|
return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}, with no exit code`;
|
|
39207
39489
|
}
|
|
39490
|
+
function errorsUnread(run) {
|
|
39491
|
+
return run.throughEditor && typeof run.errorReport !== "string";
|
|
39492
|
+
}
|
|
39493
|
+
function cleanVerdict(run) {
|
|
39494
|
+
if (run.consoleLost === true || errorsUnread(run)) {
|
|
39495
|
+
return;
|
|
39496
|
+
}
|
|
39497
|
+
return run.log.count("error") === 0;
|
|
39498
|
+
}
|
|
39208
39499
|
function stillRunning(run) {
|
|
39209
39500
|
if (run === null || exited(run) || run.endedUnwatched === true) {
|
|
39210
39501
|
return false;
|
|
@@ -39220,6 +39511,11 @@ function stillRunning(run) {
|
|
|
39220
39511
|
}
|
|
39221
39512
|
return true;
|
|
39222
39513
|
}
|
|
39514
|
+
function playOf(answer) {
|
|
39515
|
+
const play = readNumber(answer, "play");
|
|
39516
|
+
const editorPid = readNumber(answer, "editorPid");
|
|
39517
|
+
return play === undefined || editorPid === undefined ? null : { play, editorPid };
|
|
39518
|
+
}
|
|
39223
39519
|
function runIsUp(run, editorSays, now = Date.now()) {
|
|
39224
39520
|
if (run?.throughEditor !== true) {
|
|
39225
39521
|
return stillRunning(run);
|
|
@@ -39267,11 +39563,19 @@ function stopVerdict(stop) {
|
|
|
39267
39563
|
return {
|
|
39268
39564
|
stopped: true,
|
|
39269
39565
|
endedPid: stop.endedPid,
|
|
39270
|
-
clean: stop.
|
|
39566
|
+
clean: stop.clean,
|
|
39271
39567
|
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.",
|
|
39272
39568
|
withChildren: false
|
|
39273
39569
|
};
|
|
39274
39570
|
}
|
|
39571
|
+
if (stop.ending === "unreached") {
|
|
39572
|
+
return {
|
|
39573
|
+
stopped: false,
|
|
39574
|
+
notSignalled: stop.endedPid,
|
|
39575
|
+
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.",
|
|
39576
|
+
withChildren: false
|
|
39577
|
+
};
|
|
39578
|
+
}
|
|
39275
39579
|
if (stop.ending === "refused") {
|
|
39276
39580
|
return {
|
|
39277
39581
|
stopped: false,
|
|
@@ -39283,13 +39587,13 @@ function stopVerdict(stop) {
|
|
|
39283
39587
|
return {
|
|
39284
39588
|
stopped: true,
|
|
39285
39589
|
endedPid: stop.endedPid,
|
|
39286
|
-
clean: stop.
|
|
39590
|
+
clean: stop.clean,
|
|
39287
39591
|
note: `${stop.throughEditor ? stop.endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."}${stop.exitSignal === null ? "" : ` It was ended by ${stop.exitSignal} and so has no exit code.`}${stop.ending === "gone" ? "" : ` It had not exited ${STOP_WAIT_MS / 1000} seconds after being told to, so it may still be going: editor_status says whether it is.`}`,
|
|
39288
39592
|
withChildren: true
|
|
39289
39593
|
};
|
|
39290
39594
|
}
|
|
39291
39595
|
function endedPreviousRun(ended) {
|
|
39292
|
-
if (ended === null || ended.ending === "refused") {
|
|
39596
|
+
if (ended === null || ended.ending === "refused" || ended.ending === "unreached") {
|
|
39293
39597
|
return;
|
|
39294
39598
|
}
|
|
39295
39599
|
return ended.pid ?? true;
|
|
@@ -39371,6 +39675,9 @@ function endedToStartThis(ended) {
|
|
|
39371
39675
|
if (ended.ending === "refused") {
|
|
39372
39676
|
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.`;
|
|
39373
39677
|
}
|
|
39678
|
+
if (ended.ending === "unreached") {
|
|
39679
|
+
return ` ${which} was not ended here: the editor could not be asked to stop it. The editor stops a play before starting the next, so it is not running beside this one if this start reached the editor.`;
|
|
39680
|
+
}
|
|
39374
39681
|
const after = ended.ending === "lingering" ? ` It had not exited ${STOP_WAIT_MS / 1000} seconds after being told to, so it may still be running beside this one: editor_status says whether it is.` : "";
|
|
39375
39682
|
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.${after}`;
|
|
39376
39683
|
}
|
|
@@ -39496,19 +39803,58 @@ function withScanWait(outcome, scanned) {
|
|
|
39496
39803
|
if (scanned.waitedMs === 0) {
|
|
39497
39804
|
return outcome;
|
|
39498
39805
|
}
|
|
39499
|
-
const said = scanWaitAnswer(scanned);
|
|
39806
|
+
const said = scanWaitAnswer(scanned, "engine");
|
|
39500
39807
|
if (outcome.ok) {
|
|
39501
39808
|
return { ...outcome, payload: { ...outcome.payload, ...said } };
|
|
39502
39809
|
}
|
|
39503
39810
|
return said.scanNote === undefined ? outcome : { ...outcome, message: `${outcome.message} ${said.scanNote}` };
|
|
39504
39811
|
}
|
|
39505
|
-
function
|
|
39812
|
+
function notRunNote(notRun, failFast) {
|
|
39813
|
+
if (notRun <= 0) {
|
|
39814
|
+
return;
|
|
39815
|
+
}
|
|
39816
|
+
const cases = `${notRun} case${notRun === 1 ? "" : "s"}`;
|
|
39817
|
+
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.`;
|
|
39818
|
+
}
|
|
39819
|
+
function exportAnswer(ending, asked, before, after) {
|
|
39820
|
+
const { log } = ending;
|
|
39821
|
+
const written = after !== null && after !== before;
|
|
39822
|
+
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
39823
|
+
const verdict = {
|
|
39824
|
+
exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
|
|
39825
|
+
...asked,
|
|
39826
|
+
exitCode: ending.exitCode,
|
|
39827
|
+
exitSignal: ending.exitSignal ?? undefined,
|
|
39828
|
+
failure: ending.failure ?? undefined,
|
|
39829
|
+
errors: log.count("error"),
|
|
39830
|
+
warnings: log.count("warning"),
|
|
39831
|
+
entries: forAnswer(problems.entries),
|
|
39832
|
+
...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
|
|
39833
|
+
};
|
|
39834
|
+
if (verdict.exported) {
|
|
39835
|
+
return { content: [{ type: "text", text: JSON.stringify(verdict, null, 2) }] };
|
|
39836
|
+
}
|
|
39837
|
+
const stale = !written && after !== null ? ` The ${asked.outputPath} there is from before this export, which did not write it.` : "";
|
|
39838
|
+
const why = ending.exitSignal === null ? stale : ` The engine was ${howItEnded(ending)}, so it has no exit code.${stale}`;
|
|
39839
|
+
return {
|
|
39840
|
+
content: [
|
|
39841
|
+
{
|
|
39842
|
+
type: "text",
|
|
39843
|
+
text: `Export with preset '${asked.preset}' did not produce ${asked.outputPath}.${why}`
|
|
39844
|
+
},
|
|
39845
|
+
{ type: "text", text: JSON.stringify(verdict, null, 2) }
|
|
39846
|
+
],
|
|
39847
|
+
isError: true
|
|
39848
|
+
};
|
|
39849
|
+
}
|
|
39850
|
+
function scanWaitAnswer(scanned, started = "game") {
|
|
39506
39851
|
if (scanned.waitedMs === 0) {
|
|
39507
39852
|
return {};
|
|
39508
39853
|
}
|
|
39854
|
+
const still = `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so`;
|
|
39509
39855
|
return {
|
|
39510
39856
|
waitedForEditorScanMs: scanned.waitedMs,
|
|
39511
|
-
scanNote: scanned.stillScanning ?
|
|
39857
|
+
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."
|
|
39512
39858
|
};
|
|
39513
39859
|
}
|
|
39514
39860
|
function changedSettings(before, after) {
|
|
@@ -39563,16 +39909,17 @@ function importStatuses(payload) {
|
|
|
39563
39909
|
};
|
|
39564
39910
|
});
|
|
39565
39911
|
}
|
|
39566
|
-
function escapingPropertyValue(
|
|
39567
|
-
if (typeof
|
|
39912
|
+
function escapingPropertyValue(value) {
|
|
39913
|
+
if (typeof value === "string") {
|
|
39914
|
+
return /^(res|uid):\/\//.test(value) && value.split(/[/\\]/).includes("..") ? JSON.stringify(value) : undefined;
|
|
39915
|
+
}
|
|
39916
|
+
if (typeof value !== "object" || value === null) {
|
|
39568
39917
|
return;
|
|
39569
39918
|
}
|
|
39570
|
-
for (const
|
|
39571
|
-
|
|
39572
|
-
|
|
39573
|
-
|
|
39574
|
-
if (value.split(/[/\\]/).includes("..")) {
|
|
39575
|
-
return JSON.stringify(value);
|
|
39919
|
+
for (const item of Object.values(value)) {
|
|
39920
|
+
const found = escapingPropertyValue(item);
|
|
39921
|
+
if (found !== undefined) {
|
|
39922
|
+
return found;
|
|
39576
39923
|
}
|
|
39577
39924
|
}
|
|
39578
39925
|
return;
|
|
@@ -39593,7 +39940,7 @@ function aboveTheRunner(entry) {
|
|
|
39593
39940
|
}
|
|
39594
39941
|
function elsewhereIn(projectPath, asked) {
|
|
39595
39942
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
39596
|
-
if (wanted === "" ||
|
|
39943
|
+
if (wanted === "" || existsSync17(join23(projectPath, wanted))) {
|
|
39597
39944
|
return "";
|
|
39598
39945
|
}
|
|
39599
39946
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -39607,7 +39954,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
39607
39954
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
39608
39955
|
}
|
|
39609
39956
|
function hasMainScene(projectFile) {
|
|
39610
|
-
const scene = parseProjectGodot(
|
|
39957
|
+
const scene = parseProjectGodot(readFileSync20(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
39611
39958
|
return typeof scene === "string" && scene !== "";
|
|
39612
39959
|
}
|
|
39613
39960
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -39615,13 +39962,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
39615
39962
|
return false;
|
|
39616
39963
|
}
|
|
39617
39964
|
try {
|
|
39618
|
-
return
|
|
39965
|
+
return readFileSync20(join23(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
39619
39966
|
} catch {
|
|
39620
39967
|
return false;
|
|
39621
39968
|
}
|
|
39622
39969
|
}
|
|
39623
39970
|
function editorPlaysHeadless(projectFile) {
|
|
39624
|
-
const runArgs = parseProjectGodot(
|
|
39971
|
+
const runArgs = parseProjectGodot(readFileSync20(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
39625
39972
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
39626
39973
|
}
|
|
39627
39974
|
function camelCased(params) {
|
|
@@ -39640,7 +39987,7 @@ function needsItsOwnProcess(asked) {
|
|
|
39640
39987
|
class GodotServer {
|
|
39641
39988
|
mcp;
|
|
39642
39989
|
locator = new GodotLocator;
|
|
39643
|
-
operationsScript =
|
|
39990
|
+
operationsScript = join23(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
39644
39991
|
godotBridge;
|
|
39645
39992
|
tools = buildToolDefinitions();
|
|
39646
39993
|
activeProcess = null;
|
|
@@ -40140,6 +40487,9 @@ class GodotServer {
|
|
|
40140
40487
|
const contained = this.containProjectFiles(asks);
|
|
40141
40488
|
return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
|
|
40142
40489
|
}
|
|
40490
|
+
if (headless === "create_script") {
|
|
40491
|
+
return await this.handleCreateScript(answerable);
|
|
40492
|
+
}
|
|
40143
40493
|
if (headless !== "refresh_class_cache") {
|
|
40144
40494
|
return await this.headless(headless, answerable);
|
|
40145
40495
|
}
|
|
@@ -40310,7 +40660,7 @@ class GodotServer {
|
|
|
40310
40660
|
return await this.handleRuntimeCommand("click", {
|
|
40311
40661
|
...whichGame(args),
|
|
40312
40662
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
40313
|
-
button:
|
|
40663
|
+
button: args["button"] ?? "left",
|
|
40314
40664
|
double: readBoolean(args, "doubleClick") ?? false,
|
|
40315
40665
|
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
40316
40666
|
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
@@ -40382,8 +40732,8 @@ class GodotServer {
|
|
|
40382
40732
|
if (path === undefined) {
|
|
40383
40733
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
40384
40734
|
}
|
|
40385
|
-
const file =
|
|
40386
|
-
if (!
|
|
40735
|
+
const file = join23(path, "project.godot");
|
|
40736
|
+
if (!existsSync17(file)) {
|
|
40387
40737
|
return {
|
|
40388
40738
|
ok: false,
|
|
40389
40739
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -40593,13 +40943,30 @@ class GodotServer {
|
|
|
40593
40943
|
}
|
|
40594
40944
|
contained[key] = `res://${location.relativePath}`;
|
|
40595
40945
|
}
|
|
40596
|
-
const escaping = escapingPropertyValue(args["properties"]);
|
|
40946
|
+
const escaping = escapingPropertyValue([args["properties"], args["track"]]);
|
|
40597
40947
|
if (escaping !== undefined) {
|
|
40598
40948
|
return {
|
|
40599
40949
|
ok: false,
|
|
40600
40950
|
response: this.createErrorResponse(`The property value ${escaping} resolves outside the project directory.`, PATH_SOLUTIONS)
|
|
40601
40951
|
};
|
|
40602
40952
|
}
|
|
40953
|
+
const sources = args["sources"];
|
|
40954
|
+
if (Array.isArray(sources)) {
|
|
40955
|
+
const resolved = [];
|
|
40956
|
+
for (const source of sources) {
|
|
40957
|
+
const texture = typeof source === "object" && source !== null ? source["texture"] : undefined;
|
|
40958
|
+
if (typeof texture !== "string") {
|
|
40959
|
+
resolved.push(source);
|
|
40960
|
+
continue;
|
|
40961
|
+
}
|
|
40962
|
+
const location = resolveWithinProject(projectPath, texture);
|
|
40963
|
+
if (!location.ok) {
|
|
40964
|
+
return { ok: false, response: this.createErrorResponse(location.reason, PATH_SOLUTIONS) };
|
|
40965
|
+
}
|
|
40966
|
+
resolved.push({ ...source, texture: `res://${location.relativePath}` });
|
|
40967
|
+
}
|
|
40968
|
+
contained["sources"] = resolved;
|
|
40969
|
+
}
|
|
40603
40970
|
const pluginName = readNonEmptyString(args, "pluginName");
|
|
40604
40971
|
if (pluginName !== undefined) {
|
|
40605
40972
|
const plugin = resolveWithinProject(projectPath, `addons/${pluginName}/plugin.cfg`);
|
|
@@ -40647,11 +41014,12 @@ class GodotServer {
|
|
|
40647
41014
|
return this.answer(withScanWait({
|
|
40648
41015
|
ok: true,
|
|
40649
41016
|
messages: imported.messages,
|
|
41017
|
+
...imported.messagesOmitted === undefined ? {} : { messagesOmitted: imported.messagesOmitted },
|
|
40650
41018
|
payload: {
|
|
40651
41019
|
uidsCreated: given,
|
|
40652
41020
|
stillWithoutUid: after,
|
|
40653
41021
|
note: uidsLeftNote(after.length),
|
|
40654
|
-
...imported.
|
|
41022
|
+
...imported.extensionNote === undefined ? {} : { extensionNote: imported.extensionNote }
|
|
40655
41023
|
}
|
|
40656
41024
|
}, scanned));
|
|
40657
41025
|
}
|
|
@@ -40697,16 +41065,25 @@ class GodotServer {
|
|
|
40697
41065
|
if (changed.length === 0) {
|
|
40698
41066
|
return written;
|
|
40699
41067
|
}
|
|
40700
|
-
const
|
|
41068
|
+
const adopting = {};
|
|
41069
|
+
const taken = await this.editorTakes("adopt_project_settings", { settings: changed }, adopting);
|
|
41070
|
+
if (taken["editorNote"] !== undefined) {
|
|
41071
|
+
return this.jsonTextResponse({ ...answer, ...taken });
|
|
41072
|
+
}
|
|
41073
|
+
const adopted = Array.isArray(adopting["adopted"]) ? adopting["adopted"].map(String) : [];
|
|
41074
|
+
const passedOver = changed.filter((name) => !adopted.includes(name));
|
|
40701
41075
|
return this.jsonTextResponse({
|
|
40702
41076
|
...answer,
|
|
40703
|
-
|
|
40704
|
-
...
|
|
41077
|
+
editorAdopted: adopted,
|
|
41078
|
+
...passedOver.length > 0 ? { editorPassedOver: passedOver } : {}
|
|
40705
41079
|
});
|
|
40706
41080
|
}
|
|
40707
|
-
async editorTakes(command, args) {
|
|
41081
|
+
async editorTakes(command, args, answered = {}) {
|
|
40708
41082
|
try {
|
|
40709
|
-
await this.godotBridge.invokeTool(command, args);
|
|
41083
|
+
const result = await this.godotBridge.invokeTool(command, args);
|
|
41084
|
+
if (typeof result === "object" && result !== null) {
|
|
41085
|
+
Object.assign(answered, result);
|
|
41086
|
+
}
|
|
40710
41087
|
return {};
|
|
40711
41088
|
} catch (error) {
|
|
40712
41089
|
const message = errorMessage(error);
|
|
@@ -40744,7 +41121,19 @@ class GodotServer {
|
|
|
40744
41121
|
}
|
|
40745
41122
|
const applied = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
|
|
40746
41123
|
if (!applied.ok) {
|
|
40747
|
-
|
|
41124
|
+
const listed = written.payload["updated_options"];
|
|
41125
|
+
const updated = Array.isArray(listed) ? listed.map(String) : [];
|
|
41126
|
+
const which = updated.length === 0 ? "The options were" : `${updated.join(", ")} ${updated.length === 1 ? "was" : "were"}`;
|
|
41127
|
+
return {
|
|
41128
|
+
...applied.response,
|
|
41129
|
+
content: [
|
|
41130
|
+
{
|
|
41131
|
+
type: "text",
|
|
41132
|
+
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:`
|
|
41133
|
+
},
|
|
41134
|
+
...applied.response.content
|
|
41135
|
+
]
|
|
41136
|
+
};
|
|
40748
41137
|
}
|
|
40749
41138
|
return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
|
|
40750
41139
|
}
|
|
@@ -40762,7 +41151,14 @@ class GodotServer {
|
|
|
40762
41151
|
response: this.createErrorResponse(`${missingSource[0]} is not on disk, so there is nothing to import.`)
|
|
40763
41152
|
};
|
|
40764
41153
|
}
|
|
40765
|
-
const
|
|
41154
|
+
const never = listed.find((one) => one.status === "not_imported");
|
|
41155
|
+
if (resourcePath !== undefined && never !== undefined) {
|
|
41156
|
+
return {
|
|
41157
|
+
ok: false,
|
|
41158
|
+
response: this.createErrorResponse(`${never.path} is not something the engine imports: ${never.reason ?? "it has no importer"}.`)
|
|
41159
|
+
};
|
|
41160
|
+
}
|
|
41161
|
+
const targets = listed.filter((one) => one.status !== "missing_source" && one.status !== "not_imported" && (force || one.status !== "up_to_date")).map((one) => one.path);
|
|
40766
41162
|
if (targets.length === 0) {
|
|
40767
41163
|
return {
|
|
40768
41164
|
ok: true,
|
|
@@ -40796,19 +41192,14 @@ class GodotServer {
|
|
|
40796
41192
|
if (!engine.ok) {
|
|
40797
41193
|
return { ok: false, response: engine.response };
|
|
40798
41194
|
}
|
|
40799
|
-
for (const target of targets) {
|
|
40800
|
-
forceNextImport(projectPath, target);
|
|
40801
|
-
}
|
|
40802
41195
|
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
40803
|
-
const imported = await runImport(engine.value, projectPath);
|
|
41196
|
+
const imported = await runImport(engine.value, projectPath, { forcing: targets });
|
|
40804
41197
|
if (!imported.ok) {
|
|
40805
41198
|
return { ok: false, response: this.answer(withScanWait(imported, scanned)) };
|
|
40806
41199
|
}
|
|
40807
|
-
|
|
40808
|
-
|
|
40809
|
-
|
|
40810
|
-
if (imported.librariesRetried !== undefined) {
|
|
40811
|
-
extra["extensionNote"] = extensionRetryNote(imported.librariesRetried);
|
|
41200
|
+
Object.assign(extra, engineExtras(imported));
|
|
41201
|
+
if (imported.extensionNote !== undefined) {
|
|
41202
|
+
extra["extensionNote"] = imported.extensionNote;
|
|
40812
41203
|
}
|
|
40813
41204
|
}
|
|
40814
41205
|
const after = await this.operation("get_import_status", resourcePath === undefined ? { includeUpToDate: true } : { resourcePath }, projectPath);
|
|
@@ -40889,17 +41280,15 @@ class GodotServer {
|
|
|
40889
41280
|
return withScanWait(outcome, scanned);
|
|
40890
41281
|
}
|
|
40891
41282
|
answer(outcome) {
|
|
41283
|
+
const extras = engineExtras(outcome);
|
|
40892
41284
|
if (!outcome.ok) {
|
|
40893
41285
|
const response = this.createErrorResponse(outcome.message);
|
|
40894
|
-
if (
|
|
40895
|
-
response.content.push({
|
|
40896
|
-
type: "text",
|
|
40897
|
-
text: JSON.stringify({ engine_messages: outcome.messages }, null, 2)
|
|
40898
|
-
});
|
|
41286
|
+
if (Object.keys(extras).length > 0) {
|
|
41287
|
+
response.content.push({ type: "text", text: JSON.stringify(extras, null, 2) });
|
|
40899
41288
|
}
|
|
40900
41289
|
return response;
|
|
40901
41290
|
}
|
|
40902
|
-
return this.jsonTextResponse(
|
|
41291
|
+
return this.jsonTextResponse({ ...outcome.payload, ...extras });
|
|
40903
41292
|
}
|
|
40904
41293
|
async handleProjectInfo(args) {
|
|
40905
41294
|
const include = readStringArray(args, "include") ?? [];
|
|
@@ -40915,7 +41304,7 @@ class GodotServer {
|
|
|
40915
41304
|
if (!engine.ok) {
|
|
40916
41305
|
return engine.response;
|
|
40917
41306
|
}
|
|
40918
|
-
const application = parseProjectGodot(
|
|
41307
|
+
const application = parseProjectGodot(readFileSync20(project.value.file, "utf8"))["application"] ?? {};
|
|
40919
41308
|
const name = application["config/name"];
|
|
40920
41309
|
const mainScene = application["run/main_scene"];
|
|
40921
41310
|
const info = {
|
|
@@ -40932,7 +41321,7 @@ class GodotServer {
|
|
|
40932
41321
|
continue;
|
|
40933
41322
|
}
|
|
40934
41323
|
const outcome = await this.operation(spec.operation, spec.params(args, detailed), project.value.path);
|
|
40935
|
-
info[section] = outcome.ok ?
|
|
41324
|
+
info[section] = outcome.ok ? { ...outcome.payload, ...engineExtras(outcome) } : { error: outcome.message, ...engineExtras(outcome) };
|
|
40936
41325
|
}
|
|
40937
41326
|
return this.jsonTextResponse(info);
|
|
40938
41327
|
}
|
|
@@ -40976,18 +41365,18 @@ class GodotServer {
|
|
|
40976
41365
|
if (!engine.ok) {
|
|
40977
41366
|
return engine.response;
|
|
40978
41367
|
}
|
|
40979
|
-
const folder =
|
|
41368
|
+
const folder = dirname16(output.absolutePath);
|
|
40980
41369
|
try {
|
|
40981
41370
|
mkdirSync12(folder, { recursive: true });
|
|
40982
41371
|
} catch (error) {
|
|
40983
|
-
return this.createErrorResponse(`Cannot create ${
|
|
41372
|
+
return this.createErrorResponse(`Cannot create ${dirname16(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
40984
41373
|
}
|
|
40985
41374
|
const debug = readBoolean(args, "debug") ?? false;
|
|
40986
41375
|
const exportLogs = scratchDirectory("export");
|
|
40987
41376
|
const exportArgs = [
|
|
40988
41377
|
"--headless",
|
|
40989
41378
|
"--log-file",
|
|
40990
|
-
|
|
41379
|
+
join23(exportLogs, "engine.log"),
|
|
40991
41380
|
"--path",
|
|
40992
41381
|
project.value.path,
|
|
40993
41382
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -40995,55 +41384,18 @@ class GodotServer {
|
|
|
40995
41384
|
output.absolutePath
|
|
40996
41385
|
];
|
|
40997
41386
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
40998
|
-
const
|
|
40999
|
-
|
|
41387
|
+
const writtenAt = () => existsSync17(output.absolutePath) ? statSync8(output.absolutePath).mtimeMs : null;
|
|
41388
|
+
const before = writtenAt();
|
|
41389
|
+
let ending;
|
|
41000
41390
|
try {
|
|
41001
|
-
|
|
41002
|
-
timeout: 300000,
|
|
41003
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
41004
|
-
signal: callSignal()
|
|
41005
|
-
});
|
|
41006
|
-
log.append("stdout", stdout);
|
|
41007
|
-
log.append("stderr", stderr);
|
|
41008
|
-
} catch (error) {
|
|
41009
|
-
if (!(error instanceof Error && ("stdout" in error) && ("stderr" in error))) {
|
|
41010
|
-
return this.createErrorResponse(`Export could not be run: ${errorMessage(error)}`);
|
|
41011
|
-
}
|
|
41012
|
-
const failed = error;
|
|
41013
|
-
log.append("stdout", failed.stdout);
|
|
41014
|
-
log.append("stderr", failed.stderr);
|
|
41015
|
-
ending = endingOf(failed);
|
|
41391
|
+
ending = await runEngine(engine.value, exportArgs, { timeout: { ms: 300000, said: "five minutes" } });
|
|
41016
41392
|
} finally {
|
|
41017
41393
|
discard(exportLogs);
|
|
41018
41394
|
}
|
|
41019
|
-
|
|
41020
|
-
|
|
41021
|
-
const verdict = {
|
|
41022
|
-
exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync15(output.absolutePath),
|
|
41023
|
-
preset,
|
|
41024
|
-
outputPath: output.relativePath,
|
|
41025
|
-
debug,
|
|
41026
|
-
exitCode: ending.exitCode,
|
|
41027
|
-
exitSignal: ending.exitSignal ?? undefined,
|
|
41028
|
-
failure: ending.failure ?? undefined,
|
|
41029
|
-
errors: log.count("error"),
|
|
41030
|
-
warnings: log.count("warning"),
|
|
41031
|
-
entries: forAnswer(problems.entries)
|
|
41032
|
-
};
|
|
41033
|
-
if (!verdict.exported) {
|
|
41034
|
-
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.`;
|
|
41035
|
-
return {
|
|
41036
|
-
content: [
|
|
41037
|
-
{
|
|
41038
|
-
type: "text",
|
|
41039
|
-
text: `Export with preset '${preset}' did not produce ${output.relativePath}.${why}`
|
|
41040
|
-
},
|
|
41041
|
-
{ type: "text", text: JSON.stringify(verdict, null, 2) }
|
|
41042
|
-
],
|
|
41043
|
-
isError: true
|
|
41044
|
-
};
|
|
41395
|
+
if (ending.exitSignal === null && ending.exitCode === null) {
|
|
41396
|
+
return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
|
|
41045
41397
|
}
|
|
41046
|
-
return
|
|
41398
|
+
return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt());
|
|
41047
41399
|
}
|
|
41048
41400
|
async handleRunTests(args) {
|
|
41049
41401
|
const project = this.project(args);
|
|
@@ -41055,7 +41407,7 @@ class GodotServer {
|
|
|
41055
41407
|
return contained.response;
|
|
41056
41408
|
}
|
|
41057
41409
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
41058
|
-
if (!
|
|
41410
|
+
if (!existsSync17(join23(project.value.path, runner))) {
|
|
41059
41411
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
41060
41412
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
41061
41413
|
]);
|
|
@@ -41121,21 +41473,22 @@ class GodotServer {
|
|
|
41121
41473
|
if (timedOut && run.process.pid !== undefined) {
|
|
41122
41474
|
await untilGone([run.process.pid]);
|
|
41123
41475
|
}
|
|
41124
|
-
|
|
41476
|
+
await run.settled;
|
|
41477
|
+
const reportsDir = join23(project.value.path, ".godot", ...ours.split("/"));
|
|
41125
41478
|
let report = null;
|
|
41126
41479
|
let reportProblem = null;
|
|
41127
41480
|
try {
|
|
41128
|
-
const written =
|
|
41481
|
+
const written = existsSync17(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
41129
41482
|
const newest = written.at(-1);
|
|
41130
41483
|
if (newest !== undefined) {
|
|
41131
|
-
report = parseJUnit(
|
|
41484
|
+
report = parseJUnit(readFileSync20(join23(reportsDir, newest, "results.xml"), "utf8"));
|
|
41132
41485
|
}
|
|
41133
41486
|
} catch (error) {
|
|
41134
41487
|
reportProblem = errorMessage(error);
|
|
41135
41488
|
} finally {
|
|
41136
41489
|
discard(reportsDir, userData);
|
|
41137
41490
|
try {
|
|
41138
|
-
rmdirSync2(
|
|
41491
|
+
rmdirSync2(dirname16(reportsDir));
|
|
41139
41492
|
} catch {}
|
|
41140
41493
|
}
|
|
41141
41494
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
@@ -41148,7 +41501,11 @@ class GodotServer {
|
|
|
41148
41501
|
105: "script errors"
|
|
41149
41502
|
};
|
|
41150
41503
|
const exitCode = run.exitCode;
|
|
41151
|
-
const printed = run.log.select({
|
|
41504
|
+
const printed = run.log.select({
|
|
41505
|
+
severity: "info",
|
|
41506
|
+
sinceLastCall: false,
|
|
41507
|
+
limit: Number.POSITIVE_INFINITY
|
|
41508
|
+
}).entries;
|
|
41152
41509
|
const said = printed.map((entry) => entry.text);
|
|
41153
41510
|
const nothingRan = timedOut ? null : whyNoReport(said, asked);
|
|
41154
41511
|
const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
|
|
@@ -41164,7 +41521,7 @@ class GodotServer {
|
|
|
41164
41521
|
...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
|
|
41165
41522
|
};
|
|
41166
41523
|
if (report === null) {
|
|
41167
|
-
const note = nothingRan !== null ? `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : `The test run
|
|
41524
|
+
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}).`;
|
|
41168
41525
|
return {
|
|
41169
41526
|
content: [
|
|
41170
41527
|
{ type: "text", text: note },
|
|
@@ -41201,10 +41558,21 @@ class GodotServer {
|
|
|
41201
41558
|
orphans: suite.orphans,
|
|
41202
41559
|
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().`
|
|
41203
41560
|
}));
|
|
41204
|
-
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
|
|
41561
|
+
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);
|
|
41562
|
+
const hooksPrinted = hookFailuresPrinted(said);
|
|
41563
|
+
const hookFailures = report.suites.filter((suite) => suite.hookFailures + suite.hookErrors > 0).flatMap((suite) => {
|
|
41564
|
+
const printedHere = hooksPrinted.filter((hook) => hook.path === suite.path);
|
|
41565
|
+
return printedHere.length > 0 ? printedHere.map((hook) => ({ suite: suite.name, ...hook })) : [
|
|
41566
|
+
{
|
|
41567
|
+
suite: suite.name,
|
|
41568
|
+
path: suite.path,
|
|
41569
|
+
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."
|
|
41570
|
+
}
|
|
41571
|
+
];
|
|
41572
|
+
});
|
|
41205
41573
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
41206
41574
|
return this.jsonTextResponse({
|
|
41207
|
-
passed: !timedOut && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
41575
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0,
|
|
41208
41576
|
verdict,
|
|
41209
41577
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
41210
41578
|
exitCode,
|
|
@@ -41215,13 +41583,14 @@ class GodotServer {
|
|
|
41215
41583
|
skipped: report.skipped,
|
|
41216
41584
|
time: report.time,
|
|
41217
41585
|
failed,
|
|
41586
|
+
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
41218
41587
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
41219
41588
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
41220
41589
|
notRun: notRun > 0 ? notRun : undefined,
|
|
41221
41590
|
...printedLines,
|
|
41222
41591
|
savesNote,
|
|
41223
41592
|
...scanned,
|
|
41224
|
-
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
|
|
41593
|
+
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),
|
|
41225
41594
|
suites: unclean.map((suite) => ({
|
|
41226
41595
|
name: suite.name,
|
|
41227
41596
|
path: suite.path,
|
|
@@ -41229,6 +41598,7 @@ class GodotServer {
|
|
|
41229
41598
|
notRun: suite.discovered > suite.tests ? suite.discovered - suite.tests : undefined,
|
|
41230
41599
|
failures: suite.failures,
|
|
41231
41600
|
errors: suite.errors,
|
|
41601
|
+
hookFailures: suite.hookFailures + suite.hookErrors > 0 ? suite.hookFailures + suite.hookErrors : undefined,
|
|
41232
41602
|
skipped: suite.skipped,
|
|
41233
41603
|
orphans: orphansIn(suite.path) > 0 ? orphansIn(suite.path) : undefined,
|
|
41234
41604
|
time: suite.time
|
|
@@ -41242,7 +41612,7 @@ class GodotServer {
|
|
|
41242
41612
|
}
|
|
41243
41613
|
async godotVersion(godotPath) {
|
|
41244
41614
|
try {
|
|
41245
|
-
const { stdout } = await
|
|
41615
|
+
const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
|
|
41246
41616
|
this.lastGodotVersion = stdout.trim();
|
|
41247
41617
|
return this.lastGodotVersion;
|
|
41248
41618
|
} catch (error) {
|
|
@@ -41254,8 +41624,18 @@ class GodotServer {
|
|
|
41254
41624
|
const answer = await this.handleLSP("lsp_get_diagnostics", args);
|
|
41255
41625
|
const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
41256
41626
|
if (payload["error"] !== undefined) {
|
|
41257
|
-
const reason = payload["error"];
|
|
41258
|
-
|
|
41627
|
+
const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
|
|
41628
|
+
if (payload["refusedArguments"] === true) {
|
|
41629
|
+
return this.createErrorResponse(reason);
|
|
41630
|
+
}
|
|
41631
|
+
if (typeof payload["serves"] === "string") {
|
|
41632
|
+
return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
41633
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
41634
|
+
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
41635
|
+
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
41636
|
+
]);
|
|
41637
|
+
}
|
|
41638
|
+
return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
41259
41639
|
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
41260
41640
|
"GDHARNESS_LSP_PORT points this server at another one"
|
|
41261
41641
|
]);
|
|
@@ -41286,11 +41666,11 @@ class GodotServer {
|
|
|
41286
41666
|
const cached = cachedClasses(projectPath);
|
|
41287
41667
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
41288
41668
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
41289
|
-
if (!contained.ok || !
|
|
41669
|
+
if (!contained.ok || !existsSync17(contained.absolutePath)) {
|
|
41290
41670
|
return null;
|
|
41291
41671
|
}
|
|
41292
41672
|
try {
|
|
41293
|
-
return
|
|
41673
|
+
return readFileSync20(contained.absolutePath, "utf8");
|
|
41294
41674
|
} catch {
|
|
41295
41675
|
return null;
|
|
41296
41676
|
}
|
|
@@ -41299,7 +41679,7 @@ class GodotServer {
|
|
|
41299
41679
|
const notes = [];
|
|
41300
41680
|
if (contradicted.length > 0) {
|
|
41301
41681
|
const dependsOn = new Set;
|
|
41302
|
-
for (const one of contradicted) {
|
|
41682
|
+
for (const one of contradicted.filter((entry) => entry.kind === "method" || entry.kind === "property")) {
|
|
41303
41683
|
const source = cached === null ? null : this.sourceOfClass(projectPath, one.declaredIn);
|
|
41304
41684
|
if (source === null || cached === null) {
|
|
41305
41685
|
continue;
|
|
@@ -41339,17 +41719,18 @@ class GodotServer {
|
|
|
41339
41719
|
});
|
|
41340
41720
|
}
|
|
41341
41721
|
classesAScriptNames(projectPath, scriptPath) {
|
|
41342
|
-
const
|
|
41722
|
+
const resourcePath = scriptPath.startsWith("res://") ? scriptPath : `res://${scriptPath.replace(/^\/+/, "")}`;
|
|
41723
|
+
const source = this.sourceOfClass(projectPath, resourcePath);
|
|
41343
41724
|
const known = cachedClasses(projectPath) ?? declaredClasses(projectPath);
|
|
41344
|
-
return source === null ? [] : classesNamedIn(source, known).filter((name) => known.get(name) !==
|
|
41725
|
+
return source === null ? [] : classesNamedIn(source, known).filter((name) => known.get(name) !== resourcePath);
|
|
41345
41726
|
}
|
|
41346
41727
|
sourceOfClass(projectPath, resourcePath) {
|
|
41347
41728
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
41348
|
-
if (!contained.ok || !
|
|
41729
|
+
if (!contained.ok || !existsSync17(contained.absolutePath)) {
|
|
41349
41730
|
return null;
|
|
41350
41731
|
}
|
|
41351
41732
|
try {
|
|
41352
|
-
return
|
|
41733
|
+
return readFileSync20(contained.absolutePath, "utf8");
|
|
41353
41734
|
} catch {
|
|
41354
41735
|
return null;
|
|
41355
41736
|
}
|
|
@@ -41537,7 +41918,8 @@ class GodotServer {
|
|
|
41537
41918
|
return {
|
|
41538
41919
|
playing: readBoolean(asParams(answer), "playing") ?? false,
|
|
41539
41920
|
scene: readString(asParams(answer), "scenePath") ?? "",
|
|
41540
|
-
debugPort: readNumber(asParams(answer), "debugPort")
|
|
41921
|
+
debugPort: readNumber(asParams(answer), "debugPort"),
|
|
41922
|
+
play: playOf(asParams(answer))
|
|
41541
41923
|
};
|
|
41542
41924
|
} catch {
|
|
41543
41925
|
return null;
|
|
@@ -41633,7 +42015,7 @@ class GodotServer {
|
|
|
41633
42015
|
return null;
|
|
41634
42016
|
}
|
|
41635
42017
|
try {
|
|
41636
|
-
return settingKeys(
|
|
42018
|
+
return settingKeys(readFileSync20(join23(projectPath, "project.godot"), "utf8"));
|
|
41637
42019
|
} catch {
|
|
41638
42020
|
return null;
|
|
41639
42021
|
}
|
|
@@ -41796,6 +42178,13 @@ class GodotServer {
|
|
|
41796
42178
|
if (!project.ok) {
|
|
41797
42179
|
return project.response;
|
|
41798
42180
|
}
|
|
42181
|
+
const open = this.godotBridge.isConnected() ? this.godotBridge.getStatus().projectPath : undefined;
|
|
42182
|
+
if (op !== "check" && open !== undefined && open !== "" && !isSameDirectory(open, project.value.path)) {
|
|
42183
|
+
return this.createErrorResponse(`This start names ${project.value.path}, and the editor on this bridge has ${open} open.`, [
|
|
42184
|
+
"editor_status names the project this server's editor is showing",
|
|
42185
|
+
"One server serves one editor: start a second server for the other project"
|
|
42186
|
+
]);
|
|
42187
|
+
}
|
|
41799
42188
|
const scene = readNonEmptyString(args, "scene");
|
|
41800
42189
|
const sceneToRun = scene !== undefined ? resolveWithinProject(project.value.path, scene) : null;
|
|
41801
42190
|
if (sceneToRun && !sceneToRun.ok) {
|
|
@@ -41890,7 +42279,7 @@ class GodotServer {
|
|
|
41890
42279
|
}
|
|
41891
42280
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
41892
42281
|
const budgetMs = budget.ms;
|
|
41893
|
-
if (!
|
|
42282
|
+
if (!existsSync17(join23(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
41894
42283
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
41895
42284
|
}
|
|
41896
42285
|
let asking = null;
|
|
@@ -41899,7 +42288,7 @@ class GodotServer {
|
|
|
41899
42288
|
let editorAsks = 0;
|
|
41900
42289
|
const waitBegan = Date.now();
|
|
41901
42290
|
const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
|
|
41902
|
-
const
|
|
42291
|
+
const fresh = await announcedSince(projectPath, before, {
|
|
41903
42292
|
budgetMs,
|
|
41904
42293
|
accept: (one) => this.isTheGameOf(this.activeProcess, one, projectPath),
|
|
41905
42294
|
giveUp: () => {
|
|
@@ -41923,11 +42312,19 @@ class GodotServer {
|
|
|
41923
42312
|
}
|
|
41924
42313
|
});
|
|
41925
42314
|
const asked = editorAsks === 0 ? "editor not asked" : `editor asked ${editorAsks === 1 ? "once" : `${editorAsks} times`}, last said ${lastSaid()}`;
|
|
41926
|
-
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${
|
|
42315
|
+
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${fresh.length === 0 ? "nothing announced" : `pid ${fresh.map((one) => one.pid).join(", ")} announced`}, ${asked}`);
|
|
41927
42316
|
const going = await this.currentRun();
|
|
42317
|
+
const endpoint = await this.theRunsAmong(going, fresh);
|
|
41928
42318
|
if (endpoint !== null && going !== null && going.announcedPid === undefined) {
|
|
41929
42319
|
this.tie(going, endpoint);
|
|
41930
42320
|
}
|
|
42321
|
+
if (endpoint === null && fresh.length > 1) {
|
|
42322
|
+
return {
|
|
42323
|
+
listening: true,
|
|
42324
|
+
pids: fresh.map((one) => one.pid),
|
|
42325
|
+
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.`
|
|
42326
|
+
};
|
|
42327
|
+
}
|
|
41931
42328
|
return runtimeVerdict(endpoint, {
|
|
41932
42329
|
addon: true,
|
|
41933
42330
|
budgetMs,
|
|
@@ -42011,7 +42408,13 @@ class GodotServer {
|
|
|
42011
42408
|
};
|
|
42012
42409
|
this.activeProcess = played;
|
|
42013
42410
|
this.watchThePlayedConsole();
|
|
42014
|
-
|
|
42411
|
+
const which = playOf(playAnswer);
|
|
42412
|
+
writeEditorRunNote({
|
|
42413
|
+
projectPath,
|
|
42414
|
+
transcript: transcript.path,
|
|
42415
|
+
startedAt,
|
|
42416
|
+
...which === null ? {} : { play: which }
|
|
42417
|
+
});
|
|
42015
42418
|
sweepTranscripts();
|
|
42016
42419
|
return this.jsonTextResponse({
|
|
42017
42420
|
started: true,
|
|
@@ -42126,7 +42529,7 @@ class GodotServer {
|
|
|
42126
42529
|
return 0;
|
|
42127
42530
|
}
|
|
42128
42531
|
try {
|
|
42129
|
-
return reportAlreadyInTranscript(
|
|
42532
|
+
return reportAlreadyInTranscript(readFileSync20(game.transcript), readFileSync20(report));
|
|
42130
42533
|
} catch (error) {
|
|
42131
42534
|
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
42132
42535
|
return 0;
|
|
@@ -42140,6 +42543,18 @@ class GodotServer {
|
|
|
42140
42543
|
game.brokeOn = halt.text;
|
|
42141
42544
|
game.log.record("error", halt.text);
|
|
42142
42545
|
}
|
|
42546
|
+
async untilTheEditorStopsPlaying(withinMs = STOP_WAIT_MS) {
|
|
42547
|
+
const deadline = Date.now() + withinMs;
|
|
42548
|
+
for (;; ) {
|
|
42549
|
+
if ((await this.editorPlayingState())?.playing === false) {
|
|
42550
|
+
return true;
|
|
42551
|
+
}
|
|
42552
|
+
if (Date.now() >= deadline) {
|
|
42553
|
+
return false;
|
|
42554
|
+
}
|
|
42555
|
+
await delay3(100);
|
|
42556
|
+
}
|
|
42557
|
+
}
|
|
42143
42558
|
async endActiveGame(reason, going) {
|
|
42144
42559
|
const running = this.activeProcess;
|
|
42145
42560
|
const project = running?.projectPath ?? this.ourProject();
|
|
@@ -42158,8 +42573,16 @@ class GodotServer {
|
|
|
42158
42573
|
running.endedHere = reason;
|
|
42159
42574
|
const announced = this.announcedPidOf(running);
|
|
42160
42575
|
if (running.throughEditor) {
|
|
42161
|
-
await this.handleViaBridge("stop_playing", {});
|
|
42162
|
-
|
|
42576
|
+
const asked = await this.handleViaBridge("stop_playing", {});
|
|
42577
|
+
if (asked.isError === true) {
|
|
42578
|
+
running.endedHere = null;
|
|
42579
|
+
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"}).`);
|
|
42580
|
+
return "unreached";
|
|
42581
|
+
}
|
|
42582
|
+
if (announced === undefined) {
|
|
42583
|
+
return await this.untilTheEditorStopsPlaying() ? "gone" : "lingering";
|
|
42584
|
+
}
|
|
42585
|
+
return await untilGone([announced]) ? "gone" : "lingering";
|
|
42163
42586
|
}
|
|
42164
42587
|
if (running.pid === null) {
|
|
42165
42588
|
return "gone";
|
|
@@ -42199,7 +42622,7 @@ class GodotServer {
|
|
|
42199
42622
|
return gone ? "gone" : "lingering";
|
|
42200
42623
|
}
|
|
42201
42624
|
spawnGame(godotPath, cmdArgs, env) {
|
|
42202
|
-
const child =
|
|
42625
|
+
const child = spawn4(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
42203
42626
|
const cancelled = callSignal();
|
|
42204
42627
|
if (cancelled !== undefined) {
|
|
42205
42628
|
const end = () => {
|
|
@@ -42239,7 +42662,6 @@ class GodotServer {
|
|
|
42239
42662
|
});
|
|
42240
42663
|
child.on("exit", (code, signal) => {
|
|
42241
42664
|
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
42242
|
-
log.finish();
|
|
42243
42665
|
started.exitCode = code;
|
|
42244
42666
|
started.exitSignal = code === null ? signal : null;
|
|
42245
42667
|
});
|
|
@@ -42249,7 +42671,28 @@ class GodotServer {
|
|
|
42249
42671
|
`);
|
|
42250
42672
|
started.exitCode = -1;
|
|
42251
42673
|
});
|
|
42252
|
-
|
|
42674
|
+
const settled = new Promise((resolve) => {
|
|
42675
|
+
let open = 2;
|
|
42676
|
+
const done = () => {
|
|
42677
|
+
log.finish();
|
|
42678
|
+
resolve();
|
|
42679
|
+
};
|
|
42680
|
+
const closed = () => {
|
|
42681
|
+
open -= 1;
|
|
42682
|
+
if (open === 0) {
|
|
42683
|
+
done();
|
|
42684
|
+
}
|
|
42685
|
+
};
|
|
42686
|
+
child.stdout.once("close", closed);
|
|
42687
|
+
child.stderr.once("close", closed);
|
|
42688
|
+
child.once("exit", () => {
|
|
42689
|
+
setTimeout(done, STREAMS_GRACE_MS).unref();
|
|
42690
|
+
});
|
|
42691
|
+
child.once("error", () => {
|
|
42692
|
+
done();
|
|
42693
|
+
});
|
|
42694
|
+
});
|
|
42695
|
+
return Object.assign(started, { settled });
|
|
42253
42696
|
}
|
|
42254
42697
|
async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
|
|
42255
42698
|
const startedAt = Date.now();
|
|
@@ -42331,7 +42774,8 @@ class GodotServer {
|
|
|
42331
42774
|
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
42332
42775
|
}
|
|
42333
42776
|
const project = this.godotBridge.getStatus().projectPath ?? null;
|
|
42334
|
-
const
|
|
42777
|
+
const recorded = project === null ? null : readEditorRunNote(project);
|
|
42778
|
+
const note = recorded !== null && noteIsAbout(recorded, playing.play) ? recorded : null;
|
|
42335
42779
|
const picked = {
|
|
42336
42780
|
pid: null,
|
|
42337
42781
|
log: new GameLog,
|
|
@@ -42388,6 +42832,31 @@ class GodotServer {
|
|
|
42388
42832
|
const its = await this.tiedThroughTheTree(run, running);
|
|
42389
42833
|
return its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
|
|
42390
42834
|
}
|
|
42835
|
+
async theRunsAmong(run, fresh) {
|
|
42836
|
+
if (fresh.length === 1) {
|
|
42837
|
+
return fresh[0] ?? null;
|
|
42838
|
+
}
|
|
42839
|
+
if (fresh.length === 0 || run === null) {
|
|
42840
|
+
return null;
|
|
42841
|
+
}
|
|
42842
|
+
const pid = run.announcedPid ?? await this.tiedThroughTheTree(run, fresh);
|
|
42843
|
+
return fresh.find((one) => one.pid === pid) ?? null;
|
|
42844
|
+
}
|
|
42845
|
+
async tieThePlayedGame(run) {
|
|
42846
|
+
if (!run.throughEditor || run.errorReport !== undefined || run.announcedPid !== undefined) {
|
|
42847
|
+
return;
|
|
42848
|
+
}
|
|
42849
|
+
const announced = runtimesAnnounced().running;
|
|
42850
|
+
if (this.announcedPidOf(run, announced) !== undefined || !stillRunning(run)) {
|
|
42851
|
+
return;
|
|
42852
|
+
}
|
|
42853
|
+
const among = this.announcedOfTheRunsProject(run, announced).map((one) => one.pid).sort((one, other) => one - other).join(",");
|
|
42854
|
+
if (among === "" || run.treeReadFor === among) {
|
|
42855
|
+
return;
|
|
42856
|
+
}
|
|
42857
|
+
run.treeReadFor = among;
|
|
42858
|
+
await this.tiedThroughTheTree(run, announced);
|
|
42859
|
+
}
|
|
42391
42860
|
async tiedThroughTheTree(run, announced) {
|
|
42392
42861
|
if (run.announcedPid !== undefined) {
|
|
42393
42862
|
return run.announcedPid;
|
|
@@ -42588,6 +43057,7 @@ class GodotServer {
|
|
|
42588
43057
|
resolve(false);
|
|
42589
43058
|
});
|
|
42590
43059
|
});
|
|
43060
|
+
await boot.settled;
|
|
42591
43061
|
const errors = boot.log.count("error");
|
|
42592
43062
|
const warnings = boot.log.count("warning");
|
|
42593
43063
|
return this.jsonTextResponse({
|
|
@@ -42729,6 +43199,7 @@ class GodotServer {
|
|
|
42729
43199
|
if (!run) {
|
|
42730
43200
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
42731
43201
|
}
|
|
43202
|
+
await this.tieThePlayedGame(run);
|
|
42732
43203
|
this.drainEditorOutput(run);
|
|
42733
43204
|
this.drainTranscript(run);
|
|
42734
43205
|
const going = await this.runStillGoing(run);
|
|
@@ -42764,7 +43235,7 @@ class GodotServer {
|
|
|
42764
43235
|
notes.push(endedWithoutACode({
|
|
42765
43236
|
throughEditor: run.throughEditor,
|
|
42766
43237
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
42767
|
-
couldAnnounce: run.projectPath !== null &&
|
|
43238
|
+
couldAnnounce: run.projectPath !== null && existsSync17(join23(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
42768
43239
|
endedHere: typeof run.endedHere === "string",
|
|
42769
43240
|
pickedUp: run.pickedUp === true
|
|
42770
43241
|
}));
|
|
@@ -42772,6 +43243,9 @@ class GodotServer {
|
|
|
42772
43243
|
if (run.pickedUpPlaying === true) {
|
|
42773
43244
|
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.");
|
|
42774
43245
|
}
|
|
43246
|
+
if (errorsUnread(run)) {
|
|
43247
|
+
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.");
|
|
43248
|
+
}
|
|
42775
43249
|
if (run.consoleLost === true) {
|
|
42776
43250
|
const back = this.dapClient?.isConnected() === true;
|
|
42777
43251
|
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.`);
|
|
@@ -42787,8 +43261,9 @@ class GodotServer {
|
|
|
42787
43261
|
pid: run.pid ?? announced ?? run.announcedPid ?? null,
|
|
42788
43262
|
errors: run.log.count("error"),
|
|
42789
43263
|
warnings: run.log.count("warning"),
|
|
42790
|
-
clean: run
|
|
43264
|
+
clean: cleanVerdict(run),
|
|
42791
43265
|
consoleLost: run.consoleLost === true ? true : undefined,
|
|
43266
|
+
errorsUnread: errorsUnread(run) ? true : undefined,
|
|
42792
43267
|
...hold,
|
|
42793
43268
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
42794
43269
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
@@ -42823,6 +43298,7 @@ class GodotServer {
|
|
|
42823
43298
|
if (elsewhere !== null) {
|
|
42824
43299
|
return elsewhere;
|
|
42825
43300
|
}
|
|
43301
|
+
await this.tieThePlayedGame(stopped);
|
|
42826
43302
|
this.drainEditorOutput(stopped);
|
|
42827
43303
|
this.drainTranscript(stopped);
|
|
42828
43304
|
const wasRunning = await this.runStillGoing(stopped);
|
|
@@ -42831,7 +43307,7 @@ class GodotServer {
|
|
|
42831
43307
|
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
42832
43308
|
this.logDebug("Stopping the running game");
|
|
42833
43309
|
const ending = await this.endActiveGame("editor_run stop", wasRunning);
|
|
42834
|
-
const refused = ending === "refused";
|
|
43310
|
+
const refused = ending === "refused" || ending === "unreached";
|
|
42835
43311
|
const ended = children === null || refused ? null : endChildrenAmong(children);
|
|
42836
43312
|
if (wasRunning && !refused) {
|
|
42837
43313
|
for (const pid of new Set([endedPid, announcedPid])) {
|
|
@@ -42846,7 +43322,7 @@ class GodotServer {
|
|
|
42846
43322
|
endedPid,
|
|
42847
43323
|
throughEditor: stopped.throughEditor,
|
|
42848
43324
|
exitSignal: stopped.exitSignal,
|
|
42849
|
-
|
|
43325
|
+
clean: cleanVerdict(stopped)
|
|
42850
43326
|
});
|
|
42851
43327
|
return this.jsonTextResponse({
|
|
42852
43328
|
stopped: verdict.stopped,
|
|
@@ -42860,6 +43336,7 @@ class GodotServer {
|
|
|
42860
43336
|
errors: stopped.log.count("error"),
|
|
42861
43337
|
warnings: stopped.log.count("warning"),
|
|
42862
43338
|
clean: verdict.clean,
|
|
43339
|
+
errorsUnread: errorsUnread(stopped) ? true : undefined,
|
|
42863
43340
|
note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
|
|
42864
43341
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
42865
43342
|
});
|
|
@@ -43072,7 +43549,12 @@ class GodotServer {
|
|
|
43072
43549
|
const stillAtMissingPaths = atMissingPaths.filter((name) => !dropped.includes(name));
|
|
43073
43550
|
const reloading = readNonEmptyString(args, "reloadScript");
|
|
43074
43551
|
let reloaded = {};
|
|
43075
|
-
|
|
43552
|
+
const reloadingSource = reloading === undefined || projectPath === "" ? null : this.sourceOfClass(projectPath, reloading);
|
|
43553
|
+
if (reloading !== undefined && reloadingSource !== null && isToolScript(reloadingSource)) {
|
|
43554
|
+
reloaded = {
|
|
43555
|
+
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.`
|
|
43556
|
+
};
|
|
43557
|
+
} else if (reloading !== undefined) {
|
|
43076
43558
|
try {
|
|
43077
43559
|
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
|
|
43078
43560
|
const methods = readArray(answer, "methods");
|
|
@@ -43080,29 +43562,36 @@ class GodotServer {
|
|
|
43080
43562
|
const found = readArray(answer, field);
|
|
43081
43563
|
return found ? { [field]: found.map(String) } : {};
|
|
43082
43564
|
};
|
|
43565
|
+
const asResource = (path) => path.startsWith("res://") ? path : `res://${path.replace(/^\/+/, "")}`;
|
|
43566
|
+
const script = asResource(readString(answer, "script") ?? reloading);
|
|
43567
|
+
const spelling = script === asResource(reloading) ? {} : { as: script };
|
|
43083
43568
|
const failed = answer["failed"];
|
|
43084
43569
|
if (typeof failed === "number" && failed !== 0) {
|
|
43085
43570
|
reloaded = {
|
|
43571
|
+
...spelling,
|
|
43086
43572
|
...listed("heldBefore"),
|
|
43087
43573
|
...listed("heldConstantsBefore"),
|
|
43088
43574
|
problem: failedReloadNote({
|
|
43089
|
-
script
|
|
43575
|
+
script,
|
|
43090
43576
|
code: failed,
|
|
43091
43577
|
codeName: readString(answer, "failedAs") ?? "",
|
|
43092
43578
|
heldBefore: listed("heldBefore")["heldBefore"] ?? [],
|
|
43093
43579
|
heldAfter: methods?.map(String) ?? [],
|
|
43094
43580
|
constantsBefore: listed("heldConstantsBefore")["heldConstantsBefore"] ?? [],
|
|
43095
43581
|
constantsAfter: listed("constants")["constants"] ?? [],
|
|
43096
|
-
names: projectPath === "" ? [] : this.classesAScriptNames(projectPath,
|
|
43582
|
+
names: projectPath === "" ? [] : this.classesAScriptNames(projectPath, script),
|
|
43583
|
+
restored: typeof answer["restored"] === "boolean" ? answer["restored"] : null,
|
|
43584
|
+
restoredAs: readString(answer, "restoredAs") ?? ""
|
|
43097
43585
|
})
|
|
43098
43586
|
};
|
|
43099
43587
|
} else {
|
|
43100
43588
|
reloaded = methods ? {
|
|
43589
|
+
...spelling,
|
|
43101
43590
|
methods: methods.map(String),
|
|
43102
43591
|
...listed("heldBefore"),
|
|
43103
43592
|
...listed("constants"),
|
|
43104
43593
|
...listed("heldConstantsBefore")
|
|
43105
|
-
} : { problem: `the editor would not say what ${
|
|
43594
|
+
} : { problem: `the editor would not say what ${script} has after reloading it` };
|
|
43106
43595
|
}
|
|
43107
43596
|
} catch (error) {
|
|
43108
43597
|
reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
|
|
@@ -43124,7 +43613,7 @@ class GodotServer {
|
|
|
43124
43613
|
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.");
|
|
43125
43614
|
}
|
|
43126
43615
|
if (dependents !== null && dependents.notReloaded.length > 0) {
|
|
43127
|
-
notes.push(
|
|
43616
|
+
notes.push(notReloadedNote(broughtIn, dependents.notReloaded));
|
|
43128
43617
|
}
|
|
43129
43618
|
for (const group of uidsDuplicatedOnDisk) {
|
|
43130
43619
|
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.`);
|
|
@@ -43139,6 +43628,7 @@ class GodotServer {
|
|
|
43139
43628
|
stillWorking: busy,
|
|
43140
43629
|
waitedMs: Date.now() - started,
|
|
43141
43630
|
waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
|
|
43631
|
+
reloadedAs: reloaded.as,
|
|
43142
43632
|
heldBeforeReload: reloaded.heldBefore,
|
|
43143
43633
|
reloadedMethods: reloaded.methods,
|
|
43144
43634
|
heldConstantsBeforeReload: reloaded.heldConstantsBefore,
|
|
@@ -43165,7 +43655,8 @@ class GodotServer {
|
|
|
43165
43655
|
if (tool) {
|
|
43166
43656
|
notReloaded.push({
|
|
43167
43657
|
scriptPath,
|
|
43168
|
-
problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it"
|
|
43658
|
+
problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it",
|
|
43659
|
+
tool: true
|
|
43169
43660
|
});
|
|
43170
43661
|
continue;
|
|
43171
43662
|
}
|
|
@@ -43173,9 +43664,10 @@ class GodotServer {
|
|
|
43173
43664
|
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { scriptPath }));
|
|
43174
43665
|
const failed = answer["failed"];
|
|
43175
43666
|
if (typeof failed === "number" && failed !== 0) {
|
|
43667
|
+
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";
|
|
43176
43668
|
notReloaded.push({
|
|
43177
43669
|
scriptPath,
|
|
43178
|
-
problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}`
|
|
43670
|
+
problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}${left}`
|
|
43179
43671
|
});
|
|
43180
43672
|
} else if (readArray(answer, "methods") === undefined) {
|
|
43181
43673
|
notReloaded.push({
|
|
@@ -43247,6 +43739,35 @@ class GodotServer {
|
|
|
43247
43739
|
}
|
|
43248
43740
|
};
|
|
43249
43741
|
}
|
|
43742
|
+
async handleCreateScript(args) {
|
|
43743
|
+
const project = this.project(args);
|
|
43744
|
+
if (!project.ok) {
|
|
43745
|
+
return project.response;
|
|
43746
|
+
}
|
|
43747
|
+
const contained = this.containProjectFiles(args);
|
|
43748
|
+
if (!contained.ok) {
|
|
43749
|
+
return contained.response;
|
|
43750
|
+
}
|
|
43751
|
+
const { op: _op, projectPath: _projectPath, ...params } = contained.value;
|
|
43752
|
+
const created = await this.operation("create_script", params, project.value.path);
|
|
43753
|
+
const declared = created.ok ? readString(created.payload, "class_name") ?? "" : "";
|
|
43754
|
+
if (!created.ok || declared === "") {
|
|
43755
|
+
return this.answer(created);
|
|
43756
|
+
}
|
|
43757
|
+
const rebuilt = await this.rebuildClassCache(project.value.path);
|
|
43758
|
+
const registered = cachedClasses(project.value.path)?.get(declared) === readString(created.payload, "full_path");
|
|
43759
|
+
const answered = this.answer({
|
|
43760
|
+
...created,
|
|
43761
|
+
payload: {
|
|
43762
|
+
...created.payload,
|
|
43763
|
+
registered,
|
|
43764
|
+
...registered ? {} : {
|
|
43765
|
+
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}`
|
|
43766
|
+
}
|
|
43767
|
+
}
|
|
43768
|
+
});
|
|
43769
|
+
return this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
43770
|
+
}
|
|
43250
43771
|
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
43251
43772
|
const first = answered.content[0];
|
|
43252
43773
|
if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
|
|
@@ -43375,7 +43896,7 @@ class GodotServer {
|
|
|
43375
43896
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
43376
43897
|
const run = await this.currentRun();
|
|
43377
43898
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
43378
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
43899
|
+
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)) {
|
|
43379
43900
|
return null;
|
|
43380
43901
|
}
|
|
43381
43902
|
const already = runtimesAnnounced().running;
|
|
@@ -43399,11 +43920,15 @@ class GodotServer {
|
|
|
43399
43920
|
const seconds = Math.round((Date.now() - run.startedAt) / 1000);
|
|
43400
43921
|
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.`;
|
|
43401
43922
|
}
|
|
43402
|
-
const
|
|
43923
|
+
const fresh = await announcedSince(project, new Set(already.map((one) => one.pid)), {
|
|
43403
43924
|
budgetMs: Math.min(budgetMs, remaining),
|
|
43404
43925
|
accept: (one) => this.isTheGameOf(this.activeProcess, one, project),
|
|
43405
43926
|
giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
|
|
43406
43927
|
});
|
|
43928
|
+
const endpoint = await this.theRunsAmong(run, fresh);
|
|
43929
|
+
if (fresh.length > 0 && endpoint === null) {
|
|
43930
|
+
return null;
|
|
43931
|
+
}
|
|
43407
43932
|
if (endpoint !== null) {
|
|
43408
43933
|
const tiedMeanwhile = () => run.announcedPid !== undefined;
|
|
43409
43934
|
if (!tiedMeanwhile()) {
|
|
@@ -43455,7 +43980,7 @@ class GodotServer {
|
|
|
43455
43980
|
}
|
|
43456
43981
|
}
|
|
43457
43982
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
43458
|
-
const screenshotPath = screenshotDir ?
|
|
43983
|
+
const screenshotPath = screenshotDir ? join23(screenshotDir, "capture.png") : null;
|
|
43459
43984
|
try {
|
|
43460
43985
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
43461
43986
|
if (!reply.ok && reply.requestId !== undefined) {
|
|
@@ -43496,7 +44021,7 @@ class GodotServer {
|
|
|
43496
44021
|
type: "text",
|
|
43497
44022
|
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
|
|
43498
44023
|
},
|
|
43499
|
-
{ type: "image", data:
|
|
44024
|
+
{ type: "image", data: readFileSync20(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
43500
44025
|
]
|
|
43501
44026
|
};
|
|
43502
44027
|
} finally {
|
|
@@ -43596,7 +44121,7 @@ async function runGodotServer() {
|
|
|
43596
44121
|
const server = new GodotServer;
|
|
43597
44122
|
await server.run();
|
|
43598
44123
|
}
|
|
43599
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250,
|
|
44124
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, 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/";
|
|
43600
44125
|
var init_server3 = __esm(() => {
|
|
43601
44126
|
init_mcp();
|
|
43602
44127
|
init_stdio2();
|
|
@@ -43612,6 +44137,7 @@ var init_server3 = __esm(() => {
|
|
|
43612
44137
|
init_dap_client();
|
|
43613
44138
|
init_desktop();
|
|
43614
44139
|
init_editor_log();
|
|
44140
|
+
init_engine_run();
|
|
43615
44141
|
init_errors();
|
|
43616
44142
|
init_game_log();
|
|
43617
44143
|
init_godot_bridge();
|
|
@@ -43627,7 +44153,6 @@ var init_server3 = __esm(() => {
|
|
|
43627
44153
|
init_process_children();
|
|
43628
44154
|
init_process_time();
|
|
43629
44155
|
init_project_scan();
|
|
43630
|
-
init_reimport();
|
|
43631
44156
|
init_resources();
|
|
43632
44157
|
init_restart_note();
|
|
43633
44158
|
init_run_record();
|
|
@@ -43637,8 +44162,8 @@ var init_server3 = __esm(() => {
|
|
|
43637
44162
|
init_setup();
|
|
43638
44163
|
init_tool_definitions();
|
|
43639
44164
|
init_update_check();
|
|
43640
|
-
|
|
43641
|
-
__dirname2 =
|
|
44165
|
+
run4 = promisify5(execFile5);
|
|
44166
|
+
__dirname2 = dirname16(fileURLToPath5(import.meta.url));
|
|
43642
44167
|
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
43643
44168
|
LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
43644
44169
|
VALUE_NAMES = {
|
|
@@ -43687,8 +44212,8 @@ var init_server3 = __esm(() => {
|
|
|
43687
44212
|
init_bridge_announce();
|
|
43688
44213
|
init_errors();
|
|
43689
44214
|
init_godot_path();
|
|
43690
|
-
import { existsSync as
|
|
43691
|
-
import { join as
|
|
44215
|
+
import { existsSync as existsSync18 } from "node:fs";
|
|
44216
|
+
import { join as join24, resolve as resolve6 } from "node:path";
|
|
43692
44217
|
|
|
43693
44218
|
// src/harnesses.ts
|
|
43694
44219
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -44587,8 +45112,8 @@ init_setup();
|
|
|
44587
45112
|
// src/skill.ts
|
|
44588
45113
|
init_setup();
|
|
44589
45114
|
init_tool_definitions();
|
|
44590
|
-
import { existsSync as
|
|
44591
|
-
import { basename as
|
|
45115
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
45116
|
+
import { basename as basename3, dirname as dirname6, join as join11 } from "node:path";
|
|
44592
45117
|
|
|
44593
45118
|
// src/tool-reference.ts
|
|
44594
45119
|
init_headless_operations();
|
|
@@ -44684,7 +45209,7 @@ function renderToolsMarkdown() {
|
|
|
44684
45209
|
|
|
44685
45210
|
// src/skill.ts
|
|
44686
45211
|
var SKILL_NAME = "gdharness";
|
|
44687
|
-
var SHARED_SKILLS =
|
|
45212
|
+
var SHARED_SKILLS = join11(".agents", "skills");
|
|
44688
45213
|
function skillMarkdown(version) {
|
|
44689
45214
|
return `---
|
|
44690
45215
|
name: ${SKILL_NAME}
|
|
@@ -44721,9 +45246,10 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
44721
45246
|
- Read \`editor_output\` after every run. It returns the engine's errors and warnings as entries
|
|
44722
45247
|
with their backtraces, and a \`clean\` verdict, so a run that printed an error is one call away
|
|
44723
45248
|
from being known. For a run the editor plays, the errors come from the game through the runtime
|
|
44724
|
-
addon, so a played game without the addon
|
|
44725
|
-
|
|
44726
|
-
answer for state. Never decide a run has ended by watching the
|
|
45249
|
+
addon, so a played game without the addon answers \`errorsUnread\` and no \`clean\`, since
|
|
45250
|
+
what it raised reached nothing the server reads. For a long run it also names a transcript file:
|
|
45251
|
+
read that for output and this answer for state. Never decide a run has ended by watching the
|
|
45252
|
+
file stop growing, or a process
|
|
44727
45253
|
list, or a timeout; a bench between prints looks exactly like a bench that died to everything else.
|
|
44728
45254
|
\`endedBy\` says whether gdharness ended it, and \`running\` is asked of the operating system
|
|
44729
45255
|
whenever there is a process to ask about: every run started through \`editor_run\`, and every
|
|
@@ -44875,27 +45401,27 @@ whatever the engine said on stderr comes back under \`engine_messages\`.
|
|
|
44875
45401
|
function skillFiles(version) {
|
|
44876
45402
|
return new Map([
|
|
44877
45403
|
["SKILL.md", skillMarkdown(version)],
|
|
44878
|
-
[
|
|
45404
|
+
[join11("references", "tools.md"), renderToolsMarkdown()]
|
|
44879
45405
|
]);
|
|
44880
45406
|
}
|
|
44881
45407
|
function skillDirectories(harnesses, projectPath, exists) {
|
|
44882
|
-
const directories = [
|
|
45408
|
+
const directories = [join11(projectPath, SHARED_SKILLS, SKILL_NAME)];
|
|
44883
45409
|
for (const harness of harnesses) {
|
|
44884
45410
|
if (harness.skills === undefined) {
|
|
44885
45411
|
continue;
|
|
44886
45412
|
}
|
|
44887
|
-
const own =
|
|
44888
|
-
if ((!harness.skills.shared || exists(own)) && !directories.includes(
|
|
44889
|
-
directories.push(
|
|
45413
|
+
const own = join11(projectPath, harness.skills.dir);
|
|
45414
|
+
if ((!harness.skills.shared || exists(own)) && !directories.includes(join11(own, SKILL_NAME))) {
|
|
45415
|
+
directories.push(join11(own, SKILL_NAME));
|
|
44890
45416
|
}
|
|
44891
45417
|
}
|
|
44892
45418
|
return directories;
|
|
44893
45419
|
}
|
|
44894
45420
|
function everySkillDirectory(projectPath, harnesses) {
|
|
44895
|
-
const directories = new Set([
|
|
45421
|
+
const directories = new Set([join11(projectPath, SHARED_SKILLS, SKILL_NAME)]);
|
|
44896
45422
|
for (const harness of harnesses) {
|
|
44897
45423
|
if (harness.skills !== undefined) {
|
|
44898
|
-
directories.add(
|
|
45424
|
+
directories.add(join11(projectPath, harness.skills.dir, SKILL_NAME));
|
|
44899
45425
|
}
|
|
44900
45426
|
}
|
|
44901
45427
|
return [...directories];
|
|
@@ -44903,52 +45429,52 @@ function everySkillDirectory(projectPath, harnesses) {
|
|
|
44903
45429
|
function removeSkill(directories, projectPath) {
|
|
44904
45430
|
const removed = [];
|
|
44905
45431
|
for (const directory of directories) {
|
|
44906
|
-
if (
|
|
44907
|
-
|
|
44908
|
-
pruneEmpty(
|
|
45432
|
+
if (existsSync11(directory)) {
|
|
45433
|
+
rmSync6(directory, { recursive: true, force: true });
|
|
45434
|
+
pruneEmpty(dirname6(directory), projectPath);
|
|
44909
45435
|
removed.push(directory);
|
|
44910
45436
|
}
|
|
44911
45437
|
}
|
|
44912
45438
|
return removed;
|
|
44913
45439
|
}
|
|
44914
45440
|
function pruneEmpty(directory, projectPath) {
|
|
44915
|
-
const shared =
|
|
45441
|
+
const shared = join11(projectPath, ".agents");
|
|
44916
45442
|
let path = directory;
|
|
44917
|
-
while (path !== projectPath && (
|
|
45443
|
+
while (path !== projectPath && (basename3(path) === "skills" || path === shared)) {
|
|
44918
45444
|
if (readdirSync5(path).length > 0) {
|
|
44919
45445
|
return;
|
|
44920
45446
|
}
|
|
44921
|
-
|
|
44922
|
-
path =
|
|
45447
|
+
rmSync6(path, { recursive: true, force: true });
|
|
45448
|
+
path = dirname6(path);
|
|
44923
45449
|
}
|
|
44924
45450
|
}
|
|
44925
45451
|
function writeSkill(directories, version) {
|
|
44926
45452
|
const files = skillFiles(version);
|
|
44927
45453
|
return directories.map((directory) => {
|
|
44928
|
-
const replaced =
|
|
44929
|
-
|
|
45454
|
+
const replaced = existsSync11(directory);
|
|
45455
|
+
rmSync6(directory, { recursive: true, force: true });
|
|
44930
45456
|
for (const [name, contents] of files) {
|
|
44931
|
-
const path =
|
|
44932
|
-
mkdirSync5(
|
|
44933
|
-
|
|
45457
|
+
const path = join11(directory, name);
|
|
45458
|
+
mkdirSync5(dirname6(path), { recursive: true });
|
|
45459
|
+
writeFileSync7(path, contents, "utf8");
|
|
44934
45460
|
}
|
|
44935
45461
|
return { path: directory, replaced };
|
|
44936
45462
|
});
|
|
44937
45463
|
}
|
|
44938
45464
|
|
|
44939
45465
|
// src/version.ts
|
|
44940
|
-
import { readFileSync as
|
|
44941
|
-
import { dirname as
|
|
45466
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
45467
|
+
import { dirname as dirname7, join as join12 } from "node:path";
|
|
44942
45468
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
44943
45469
|
function getLocalVersion() {
|
|
44944
|
-
const currentDirectory =
|
|
45470
|
+
const currentDirectory = dirname7(fileURLToPath2(import.meta.url));
|
|
44945
45471
|
const packageCandidates = [
|
|
44946
|
-
|
|
44947
|
-
|
|
45472
|
+
join12(currentDirectory, "..", "package.json"),
|
|
45473
|
+
join12(currentDirectory, "..", "..", "package.json")
|
|
44948
45474
|
];
|
|
44949
45475
|
for (const packagePath of packageCandidates) {
|
|
44950
45476
|
try {
|
|
44951
|
-
const value = JSON.parse(
|
|
45477
|
+
const value = JSON.parse(readFileSync10(packagePath, "utf-8"));
|
|
44952
45478
|
if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
|
|
44953
45479
|
return value.version;
|
|
44954
45480
|
}
|
|
@@ -44966,7 +45492,7 @@ class UsageError extends Refusal {
|
|
|
44966
45492
|
function projectArgument(at) {
|
|
44967
45493
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
44968
45494
|
const projectPath = resolve6(named ?? ".");
|
|
44969
|
-
if (!
|
|
45495
|
+
if (!existsSync18(join24(projectPath, "project.godot"))) {
|
|
44970
45496
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
44971
45497
|
}
|
|
44972
45498
|
return projectPath;
|
|
@@ -45005,7 +45531,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
45005
45531
|
return;
|
|
45006
45532
|
}
|
|
45007
45533
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
45008
|
-
if (!
|
|
45534
|
+
if (!existsSync18(join24(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
45009
45535
|
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}`);
|
|
45010
45536
|
}
|
|
45011
45537
|
}
|
|
@@ -45137,7 +45663,7 @@ async function setup() {
|
|
|
45137
45663
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
45138
45664
|
}
|
|
45139
45665
|
if (!args.includes("--no-skill")) {
|
|
45140
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
45666
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync18);
|
|
45141
45667
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
45142
45668
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
45143
45669
|
}
|
|
@@ -45231,7 +45757,7 @@ async function upgrade() {
|
|
|
45231
45757
|
moved.push(took.from);
|
|
45232
45758
|
}
|
|
45233
45759
|
}
|
|
45234
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
45760
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync18), version)) {
|
|
45235
45761
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
45236
45762
|
}
|
|
45237
45763
|
console.log(`
|