gdharness 1.3.45 → 1.3.47
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 +318 -201
- package/build/godot/addons/gdharness_editor/animation_reads.gd +8 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +190 -0
- package/build/godot/addons/gdharness_editor/scene_uids.gd +25 -13
- package/build/godot/addons/gdharness_editor/tool_executor.gd +4 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +20 -6
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +89 -3
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +61 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +21 -2
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +41 -9
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +22 -3
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +7 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +9 -0
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +29 -2
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +90 -30
- package/build/godot/operations/animation_reads.gd +8 -0
- package/build/godot/operations/editor_tools.gd +2 -0
- package/build/godot/operations/export_presets.gd +32 -5
- package/build/godot/operations/patterns.gd +27 -2
- package/build/godot/operations/project_diagnostics.gd +2 -1
- package/build/godot/operations/project_validation.gd +4 -2
- package/build/keeper.js +3 -2
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -68,6 +68,124 @@ var init_errors = __esm(() => {
|
|
|
68
68
|
};
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
// src/paths.ts
|
|
72
|
+
import { realpathSync, statSync } from "node:fs";
|
|
73
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
74
|
+
function realAsFarAsItExists(path) {
|
|
75
|
+
const missing = [];
|
|
76
|
+
let at = path;
|
|
77
|
+
for (;; ) {
|
|
78
|
+
try {
|
|
79
|
+
return join(realpathSync.native(at), ...missing);
|
|
80
|
+
} catch {
|
|
81
|
+
const parent = dirname(at);
|
|
82
|
+
if (parent === at) {
|
|
83
|
+
return path;
|
|
84
|
+
}
|
|
85
|
+
missing.unshift(basename(at));
|
|
86
|
+
at = parent;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function isSameDirectory(onePath, otherPath) {
|
|
91
|
+
return relative(realAsFarAsItExists(resolve(onePath)), realAsFarAsItExists(resolve(otherPath))) === "";
|
|
92
|
+
}
|
|
93
|
+
function isWithinRoot(rootPath, candidatePath) {
|
|
94
|
+
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
95
|
+
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
96
|
+
}
|
|
97
|
+
function keptInside(given) {
|
|
98
|
+
const at = given.indexOf("::");
|
|
99
|
+
const inner = at === -1 ? "" : given.slice(at + 2);
|
|
100
|
+
return KEPT_ID.test(inner) ? { file: given.slice(0, at), inner } : { file: given, inner: undefined };
|
|
101
|
+
}
|
|
102
|
+
function resolveWithinProject(projectPath, candidatePath, options = {}) {
|
|
103
|
+
const trimmed = candidatePath.trim();
|
|
104
|
+
if (trimmed === "") {
|
|
105
|
+
return { ok: false, reason: "Path is empty." };
|
|
106
|
+
}
|
|
107
|
+
if (trimmed.includes("\x00")) {
|
|
108
|
+
return { ok: false, reason: "Path contains a null byte." };
|
|
109
|
+
}
|
|
110
|
+
const root = resolve(projectPath);
|
|
111
|
+
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : isAbsolute(trimmed) && isWithinRoot(root, trimmed) ? relative(root, resolve(trimmed)).split(sep).join("/") : trimmed;
|
|
112
|
+
if (withoutScheme.startsWith("uid://")) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
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.`
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
119
|
+
if (scheme) {
|
|
120
|
+
const elsewhere = scheme[0].toLowerCase() === "user://" ? " A game's user:// is a directory of its own outside the project: a running game's is under userDir in editor_status runtimes and in editor_run start's runtime, where a file reader of your own reads it." : "";
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.${elsewhere}`
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
127
|
+
const tail = withoutScheme.split(/[\\/]+/).filter((step) => step !== "" && !step.endsWith(":"));
|
|
128
|
+
const besideIt = isAbsolute(trimmed) && isWithinRoot(realAsFarAsItExists(dirname(resolve(trimmed))), realAsFarAsItExists(root));
|
|
129
|
+
const example = tail.slice(besideIt ? -1 : -2).join("/") || "scenes/main.tscn";
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
reason: `Path '${trimmed}' is absolute and outside the project; give a path inside it, such as '${example}'.`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const kept = keptInside(trimmed);
|
|
136
|
+
if (kept.inner !== undefined) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
reason: `Path '${trimmed}' names the resource ${kept.inner} kept inside ${kept.file}, not a file. resource_edit read reads one by that path; this argument takes a file.`
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (withoutScheme.includes(":")) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const absolutePath = resolve(root, withoutScheme);
|
|
149
|
+
if (!isWithinRoot(root, absolutePath)) {
|
|
150
|
+
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
151
|
+
}
|
|
152
|
+
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
153
|
+
if (relativePath === "" && options.root !== true) {
|
|
154
|
+
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
155
|
+
}
|
|
156
|
+
return { ok: true, absolutePath, relativePath };
|
|
157
|
+
}
|
|
158
|
+
function exportedBuild(projectPath, candidatePath) {
|
|
159
|
+
const found = resolveWithinProject(projectPath, candidatePath);
|
|
160
|
+
if (!found.ok) {
|
|
161
|
+
return found;
|
|
162
|
+
}
|
|
163
|
+
let real;
|
|
164
|
+
try {
|
|
165
|
+
real = realpathSync.native(found.absolutePath);
|
|
166
|
+
} catch {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
reason: `There is no ${found.relativePath} in the project to start: export the build first, with project_export run.`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (!isWithinRoot(realAsFarAsItExists(projectPath), real)) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
reason: `${found.relativePath} leads outside the project, to ${real}, so it is not started.`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (!statSync(real).isFile()) {
|
|
179
|
+
return { ok: false, reason: `${found.relativePath} is not a file, so there is nothing to start.` };
|
|
180
|
+
}
|
|
181
|
+
return { ok: true, absolutePath: real, relativePath: found.relativePath };
|
|
182
|
+
}
|
|
183
|
+
var RESOURCE_SCHEME = "res://", OTHER_SCHEME, KEPT_ID;
|
|
184
|
+
var init_paths = __esm(() => {
|
|
185
|
+
OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
186
|
+
KEPT_ID = /^[A-Za-z0-9_]+$/;
|
|
187
|
+
});
|
|
188
|
+
|
|
71
189
|
// src/spawn.ts
|
|
72
190
|
import {
|
|
73
191
|
execFile,
|
|
@@ -243,7 +361,7 @@ var init_windows_ask = __esm(() => {
|
|
|
243
361
|
|
|
244
362
|
// src/process-children.ts
|
|
245
363
|
import { readFileSync } from "node:fs";
|
|
246
|
-
import { basename } from "node:path";
|
|
364
|
+
import { basename as basename2 } from "node:path";
|
|
247
365
|
async function startTimesOf(pids) {
|
|
248
366
|
const began = new Map;
|
|
249
367
|
if (pids.length === 0) {
|
|
@@ -345,7 +463,7 @@ function listingFailure(error) {
|
|
|
345
463
|
const failed = error;
|
|
346
464
|
const how = failed.killed === true ? `was killed after the ${ASK_TIMEOUT_MS}ms budget${failed.signal ? ` (${failed.signal})` : ""}` : failed.signal ? `was ended by ${failed.signal}` : failed.code !== undefined && failed.code !== null ? `exited with ${failed.code}` : "failed";
|
|
347
465
|
const said = typeof failed.stderr === "string" ? failed.stderr.trim() : "";
|
|
348
|
-
const asked =
|
|
466
|
+
const asked = basename2((failed.cmd ?? "").split(" ")[0] ?? "") || "the listing";
|
|
349
467
|
return `${asked} ${how}${said === "" ? ", with nothing on stderr" : `; stderr: ${said.slice(0, 400)}`}`;
|
|
350
468
|
}
|
|
351
469
|
function whyTheProcessTreeFailed() {
|
|
@@ -440,11 +558,21 @@ function ancestorsIn(tree, pid, root) {
|
|
|
440
558
|
}
|
|
441
559
|
return chain;
|
|
442
560
|
}
|
|
561
|
+
function editorsOfProjectIn(tree, projectPath) {
|
|
562
|
+
const found = [];
|
|
563
|
+
for (const [pid, listed] of tree) {
|
|
564
|
+
const read = readCommandLine(listed.command);
|
|
565
|
+
if (read.editor && read.oneShot === null && read.projectPath !== null && /godot/i.test(read.executable) && !CONSOLE_WRAPPER.test(read.executable) && isSameDirectory(read.projectPath, projectPath)) {
|
|
566
|
+
found.push(pid);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return found.sort((a, b) => a - b);
|
|
570
|
+
}
|
|
443
571
|
function readCommandLine(command) {
|
|
444
572
|
const all = commandWords(command);
|
|
445
573
|
const separator = all.findIndex((word) => word === "--" || word === "++");
|
|
446
574
|
const words = separator === -1 ? all : all.slice(0, separator);
|
|
447
|
-
const executable = words[0] === undefined ? "" :
|
|
575
|
+
const executable = words[0] === undefined ? "" : basename2(words[0].replaceAll("\\", "/"));
|
|
448
576
|
const at = words.indexOf("--path");
|
|
449
577
|
let projectPath = null;
|
|
450
578
|
if (at !== -1 && at + 1 < words.length) {
|
|
@@ -507,8 +635,9 @@ async function askPosix() {
|
|
|
507
635
|
});
|
|
508
636
|
return stdout;
|
|
509
637
|
}
|
|
510
|
-
var ticksAnswered, ASK_TIMEOUT_MS = 15000, lastListingFailure, ONE_SHOT_OPTIONS;
|
|
638
|
+
var ticksAnswered, ASK_TIMEOUT_MS = 15000, lastListingFailure, ONE_SHOT_OPTIONS, CONSOLE_WRAPPER;
|
|
511
639
|
var init_process_children = __esm(() => {
|
|
640
|
+
init_paths();
|
|
512
641
|
init_spawn();
|
|
513
642
|
init_windows_ask();
|
|
514
643
|
ONE_SHOT_OPTIONS = [
|
|
@@ -532,13 +661,14 @@ var init_process_children = __esm(() => {
|
|
|
532
661
|
"--convert-3to4",
|
|
533
662
|
"--validate-conversion-3to4"
|
|
534
663
|
];
|
|
664
|
+
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
535
665
|
});
|
|
536
666
|
|
|
537
667
|
// src/bridge-announce.ts
|
|
538
668
|
import { mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
|
|
539
|
-
import { dirname, join } from "node:path";
|
|
669
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
540
670
|
function announcementPath(projectPath) {
|
|
541
|
-
return
|
|
671
|
+
return join2(projectPath, ".godot", "gdharness-bridge.json");
|
|
542
672
|
}
|
|
543
673
|
function readAnnouncement(path) {
|
|
544
674
|
try {
|
|
@@ -585,7 +715,7 @@ function announcementIsOurs(path) {
|
|
|
585
715
|
function announceBridge(projectPath, bridge) {
|
|
586
716
|
const path = announcementPath(projectPath);
|
|
587
717
|
try {
|
|
588
|
-
mkdirSync(
|
|
718
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
589
719
|
const announcement = {
|
|
590
720
|
protocol: BRIDGE_ANNOUNCE_PROTOCOL,
|
|
591
721
|
host: bridge.host,
|
|
@@ -652,15 +782,15 @@ import {
|
|
|
652
782
|
openSync,
|
|
653
783
|
readFileSync as readFileSync3,
|
|
654
784
|
rmSync as rmSync2,
|
|
655
|
-
statSync,
|
|
785
|
+
statSync as statSync2,
|
|
656
786
|
writeFileSync as writeFileSync2,
|
|
657
787
|
writeSync
|
|
658
788
|
} from "node:fs";
|
|
659
789
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
660
|
-
import { join as
|
|
790
|
+
import { join as join3 } from "node:path";
|
|
661
791
|
import { setTimeout as delay } from "node:timers/promises";
|
|
662
792
|
function helperAssembly(directory = tmpdir2()) {
|
|
663
|
-
return
|
|
793
|
+
return join3(directory, `gdharness-desktop-${DIGEST}.dll`);
|
|
664
794
|
}
|
|
665
795
|
async function ensureHelperCompiled(directory = tmpdir2(), budgetMs = HELPER_COMPILE_MS, wait = true) {
|
|
666
796
|
const assembly = helperAssembly(directory);
|
|
@@ -686,7 +816,7 @@ async function ensureHelperCompiled(directory = tmpdir2(), budgetMs = HELPER_COM
|
|
|
686
816
|
}
|
|
687
817
|
try {
|
|
688
818
|
const holder = Number(readFileSync3(lock, "utf8").trim());
|
|
689
|
-
const old = Date.now() -
|
|
819
|
+
const old = Date.now() - statSync2(lock).mtimeMs > budgetMs;
|
|
690
820
|
if (old || Number.isInteger(holder) && holder > 0 && !alive(holder)) {
|
|
691
821
|
rmSync2(lock, { force: true });
|
|
692
822
|
continue;
|
|
@@ -700,8 +830,8 @@ async function ensureHelperCompiled(directory = tmpdir2(), budgetMs = HELPER_COM
|
|
|
700
830
|
return `the desktop helper was not compiled within ${String(budgetMs)} ms`;
|
|
701
831
|
}
|
|
702
832
|
async function compileHelper(directory, assembly, budgetMs) {
|
|
703
|
-
const scratch = mkdtempSync(
|
|
704
|
-
const source =
|
|
833
|
+
const scratch = mkdtempSync(join3(directory, "gdharness-desktop-source-"));
|
|
834
|
+
const source = join3(scratch, "helper.cs");
|
|
705
835
|
writeFileSync2(source, SOURCE, "utf8");
|
|
706
836
|
const literal = (path) => `'${path.replaceAll("'", "''")}'`;
|
|
707
837
|
const script = [
|
|
@@ -8135,124 +8265,6 @@ var init_config_formats = __esm(() => {
|
|
|
8135
8265
|
init_dist();
|
|
8136
8266
|
});
|
|
8137
8267
|
|
|
8138
|
-
// src/paths.ts
|
|
8139
|
-
import { realpathSync, statSync as statSync2 } from "node:fs";
|
|
8140
|
-
import { basename as basename2, dirname as dirname2, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
|
|
8141
|
-
function realAsFarAsItExists(path) {
|
|
8142
|
-
const missing = [];
|
|
8143
|
-
let at = path;
|
|
8144
|
-
for (;; ) {
|
|
8145
|
-
try {
|
|
8146
|
-
return join3(realpathSync.native(at), ...missing);
|
|
8147
|
-
} catch {
|
|
8148
|
-
const parent = dirname2(at);
|
|
8149
|
-
if (parent === at) {
|
|
8150
|
-
return path;
|
|
8151
|
-
}
|
|
8152
|
-
missing.unshift(basename2(at));
|
|
8153
|
-
at = parent;
|
|
8154
|
-
}
|
|
8155
|
-
}
|
|
8156
|
-
}
|
|
8157
|
-
function isSameDirectory(onePath, otherPath) {
|
|
8158
|
-
return relative(realAsFarAsItExists(resolve(onePath)), realAsFarAsItExists(resolve(otherPath))) === "";
|
|
8159
|
-
}
|
|
8160
|
-
function isWithinRoot(rootPath, candidatePath) {
|
|
8161
|
-
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
8162
|
-
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
8163
|
-
}
|
|
8164
|
-
function keptInside(given) {
|
|
8165
|
-
const at = given.indexOf("::");
|
|
8166
|
-
const inner = at === -1 ? "" : given.slice(at + 2);
|
|
8167
|
-
return KEPT_ID.test(inner) ? { file: given.slice(0, at), inner } : { file: given, inner: undefined };
|
|
8168
|
-
}
|
|
8169
|
-
function resolveWithinProject(projectPath, candidatePath, options = {}) {
|
|
8170
|
-
const trimmed = candidatePath.trim();
|
|
8171
|
-
if (trimmed === "") {
|
|
8172
|
-
return { ok: false, reason: "Path is empty." };
|
|
8173
|
-
}
|
|
8174
|
-
if (trimmed.includes("\x00")) {
|
|
8175
|
-
return { ok: false, reason: "Path contains a null byte." };
|
|
8176
|
-
}
|
|
8177
|
-
const root = resolve(projectPath);
|
|
8178
|
-
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : isAbsolute(trimmed) && isWithinRoot(root, trimmed) ? relative(root, resolve(trimmed)).split(sep).join("/") : trimmed;
|
|
8179
|
-
if (withoutScheme.startsWith("uid://")) {
|
|
8180
|
-
return {
|
|
8181
|
-
ok: false,
|
|
8182
|
-
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.`
|
|
8183
|
-
};
|
|
8184
|
-
}
|
|
8185
|
-
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
8186
|
-
if (scheme) {
|
|
8187
|
-
const elsewhere = scheme[0].toLowerCase() === "user://" ? " A game's user:// is a directory of its own outside the project: a running game's is under userDir in editor_status runtimes and in editor_run start's runtime, where a file reader of your own reads it." : "";
|
|
8188
|
-
return {
|
|
8189
|
-
ok: false,
|
|
8190
|
-
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.${elsewhere}`
|
|
8191
|
-
};
|
|
8192
|
-
}
|
|
8193
|
-
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
8194
|
-
const tail = withoutScheme.split(/[\\/]+/).filter((step) => step !== "" && !step.endsWith(":"));
|
|
8195
|
-
const besideIt = isAbsolute(trimmed) && isWithinRoot(realAsFarAsItExists(dirname2(resolve(trimmed))), realAsFarAsItExists(root));
|
|
8196
|
-
const example = tail.slice(besideIt ? -1 : -2).join("/") || "scenes/main.tscn";
|
|
8197
|
-
return {
|
|
8198
|
-
ok: false,
|
|
8199
|
-
reason: `Path '${trimmed}' is absolute and outside the project; give a path inside it, such as '${example}'.`
|
|
8200
|
-
};
|
|
8201
|
-
}
|
|
8202
|
-
const kept = keptInside(trimmed);
|
|
8203
|
-
if (kept.inner !== undefined) {
|
|
8204
|
-
return {
|
|
8205
|
-
ok: false,
|
|
8206
|
-
reason: `Path '${trimmed}' names the resource ${kept.inner} kept inside ${kept.file}, not a file. resource_edit read reads one by that path; this argument takes a file.`
|
|
8207
|
-
};
|
|
8208
|
-
}
|
|
8209
|
-
if (withoutScheme.includes(":")) {
|
|
8210
|
-
return {
|
|
8211
|
-
ok: false,
|
|
8212
|
-
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
8213
|
-
};
|
|
8214
|
-
}
|
|
8215
|
-
const absolutePath = resolve(root, withoutScheme);
|
|
8216
|
-
if (!isWithinRoot(root, absolutePath)) {
|
|
8217
|
-
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
8218
|
-
}
|
|
8219
|
-
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
8220
|
-
if (relativePath === "" && options.root !== true) {
|
|
8221
|
-
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
8222
|
-
}
|
|
8223
|
-
return { ok: true, absolutePath, relativePath };
|
|
8224
|
-
}
|
|
8225
|
-
function exportedBuild(projectPath, candidatePath) {
|
|
8226
|
-
const found = resolveWithinProject(projectPath, candidatePath);
|
|
8227
|
-
if (!found.ok) {
|
|
8228
|
-
return found;
|
|
8229
|
-
}
|
|
8230
|
-
let real;
|
|
8231
|
-
try {
|
|
8232
|
-
real = realpathSync.native(found.absolutePath);
|
|
8233
|
-
} catch {
|
|
8234
|
-
return {
|
|
8235
|
-
ok: false,
|
|
8236
|
-
reason: `There is no ${found.relativePath} in the project to start: export the build first, with project_export run.`
|
|
8237
|
-
};
|
|
8238
|
-
}
|
|
8239
|
-
if (!isWithinRoot(realAsFarAsItExists(projectPath), real)) {
|
|
8240
|
-
return {
|
|
8241
|
-
ok: false,
|
|
8242
|
-
reason: `${found.relativePath} leads outside the project, to ${real}, so it is not started.`
|
|
8243
|
-
};
|
|
8244
|
-
}
|
|
8245
|
-
if (!statSync2(real).isFile()) {
|
|
8246
|
-
return { ok: false, reason: `${found.relativePath} is not a file, so there is nothing to start.` };
|
|
8247
|
-
}
|
|
8248
|
-
return { ok: true, absolutePath: real, relativePath: found.relativePath };
|
|
8249
|
-
}
|
|
8250
|
-
var RESOURCE_SCHEME = "res://", OTHER_SCHEME, KEPT_ID;
|
|
8251
|
-
var init_paths = __esm(() => {
|
|
8252
|
-
OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
8253
|
-
KEPT_ID = /^[A-Za-z0-9_]+$/;
|
|
8254
|
-
});
|
|
8255
|
-
|
|
8256
8268
|
// src/runner.ts
|
|
8257
8269
|
import { existsSync as existsSync2 } from "node:fs";
|
|
8258
8270
|
import { dirname as dirname3, join as join4 } from "node:path";
|
|
@@ -9905,6 +9917,29 @@ function cachedClasses(projectPath) {
|
|
|
9905
9917
|
}
|
|
9906
9918
|
return listed;
|
|
9907
9919
|
}
|
|
9920
|
+
function cachedEntries(projectPath) {
|
|
9921
|
+
const text = readWhole(join6(projectPath, ".godot", "global_script_class_cache.cfg"));
|
|
9922
|
+
if (text === null) {
|
|
9923
|
+
return null;
|
|
9924
|
+
}
|
|
9925
|
+
const listed = new Map;
|
|
9926
|
+
for (const entry of text.matchAll(/\{[^{}]*\}/g)) {
|
|
9927
|
+
const named = /"class":\s*&"([^"]+)"/.exec(entry[0])?.[1];
|
|
9928
|
+
if (named !== undefined) {
|
|
9929
|
+
listed.set(named, entry[0]);
|
|
9930
|
+
}
|
|
9931
|
+
}
|
|
9932
|
+
return listed;
|
|
9933
|
+
}
|
|
9934
|
+
function cacheChangesBetween(before, after) {
|
|
9935
|
+
if (before === null || after === null) {
|
|
9936
|
+
return {};
|
|
9937
|
+
}
|
|
9938
|
+
const added = [...after.keys()].filter((name) => !before.has(name)).sort();
|
|
9939
|
+
const removed = [...before.keys()].filter((name) => !after.has(name)).sort();
|
|
9940
|
+
const changed = [...after.keys()].filter((name) => before.has(name) && before.get(name) !== after.get(name)).sort();
|
|
9941
|
+
return { added, removed, changed };
|
|
9942
|
+
}
|
|
9908
9943
|
function addonOf(path) {
|
|
9909
9944
|
return /^res:\/\/addons\/([^/]+)\//.exec(path)?.[1];
|
|
9910
9945
|
}
|
|
@@ -9921,6 +9956,20 @@ function withAddonPathsCounted(paths) {
|
|
|
9921
9956
|
}
|
|
9922
9957
|
return { own, inAddons };
|
|
9923
9958
|
}
|
|
9959
|
+
function classesWithAddonsCounted(classes, listed) {
|
|
9960
|
+
const kept = [];
|
|
9961
|
+
const inAddons = {};
|
|
9962
|
+
for (const entry of classes) {
|
|
9963
|
+
const path = entry !== null && typeof entry === "object" ? entry["path"] : "";
|
|
9964
|
+
const addon = addonOf(typeof path === "string" ? path : "");
|
|
9965
|
+
if (addon === undefined || listed.has(addon)) {
|
|
9966
|
+
kept.push(entry);
|
|
9967
|
+
} else {
|
|
9968
|
+
inAddons[addon] = (inAddons[addon] ?? 0) + 1;
|
|
9969
|
+
}
|
|
9970
|
+
}
|
|
9971
|
+
return { classes: kept, inAddons };
|
|
9972
|
+
}
|
|
9924
9973
|
function withAddonClassesCounted(rebuilt, listed) {
|
|
9925
9974
|
const counted = { ...rebuilt };
|
|
9926
9975
|
for (const key of ["added", "changed"]) {
|
|
@@ -10978,9 +11027,10 @@ function rowOf(name) {
|
|
|
10978
11027
|
function rowRuns(share, row) {
|
|
10979
11028
|
return "only" in share ? row !== null && share.only.includes(row) : row === null || !share.besides.includes(row);
|
|
10980
11029
|
}
|
|
10981
|
-
function
|
|
11030
|
+
function skippedAsDeclaredIn(printed) {
|
|
10982
11031
|
const lines = printed.flatMap((text) => stripVTControlCharacters(text).split(/\r?\n/));
|
|
10983
11032
|
const found = [];
|
|
11033
|
+
const unrunnable = [];
|
|
10984
11034
|
let suite = null;
|
|
10985
11035
|
let lastCase = null;
|
|
10986
11036
|
for (let at = 0;at < lines.length; at += 1) {
|
|
@@ -10996,7 +11046,15 @@ function mismatchedRowsIn(printed) {
|
|
|
10996
11046
|
lastCase = caseLine[3] === "STARTED" ? lastCase : caseLine[2] ?? null;
|
|
10997
11047
|
continue;
|
|
10998
11048
|
}
|
|
10999
|
-
if (suite === null || lastCase === null
|
|
11049
|
+
if (suite === null || lastCase === null) {
|
|
11050
|
+
continue;
|
|
11051
|
+
}
|
|
11052
|
+
const declared = UNRUNNABLE_CASE.exec(line)?.[1];
|
|
11053
|
+
if (declared !== undefined) {
|
|
11054
|
+
unrunnable.push({ suite, name: lastCase, reason: declared });
|
|
11055
|
+
continue;
|
|
11056
|
+
}
|
|
11057
|
+
if (!MISMATCHED_ROW.test(line)) {
|
|
11000
11058
|
continue;
|
|
11001
11059
|
}
|
|
11002
11060
|
const parts = [line.trim()];
|
|
@@ -11006,7 +11064,7 @@ function mismatchedRowsIn(printed) {
|
|
|
11006
11064
|
}
|
|
11007
11065
|
found.push({ suite, name: lastCase, reason: parts.join(" ").replaceAll(/\[\/?color[^\]]*\]/g, "") });
|
|
11008
11066
|
}
|
|
11009
|
-
return found;
|
|
11067
|
+
return { mismatched: found, unrunnable };
|
|
11010
11068
|
}
|
|
11011
11069
|
function readRun(printed) {
|
|
11012
11070
|
const lines = stripVTControlCharacters(printed).replaceAll(`\r
|
|
@@ -11140,7 +11198,7 @@ function readRun(printed) {
|
|
|
11140
11198
|
leftOut,
|
|
11141
11199
|
engineVersion,
|
|
11142
11200
|
orphansLeft,
|
|
11143
|
-
|
|
11201
|
+
...skippedAsDeclaredIn(lines)
|
|
11144
11202
|
};
|
|
11145
11203
|
}
|
|
11146
11204
|
function withNestedErrors(printed, piped) {
|
|
@@ -11265,7 +11323,7 @@ function judgeEngine(printed, handed, end) {
|
|
|
11265
11323
|
if (printed.executed === null || printed.executed === 0) {
|
|
11266
11324
|
faults.push(printed.executed === null ? "the run never printed its summary" : "the run executed no test case at all, whatever it exited with");
|
|
11267
11325
|
}
|
|
11268
|
-
for (const row of printed.mismatched) {
|
|
11326
|
+
for (const row of [...printed.mismatched, ...printed.unrunnable]) {
|
|
11269
11327
|
faults.push(`${row.suite} > ${row.name} never ran: gdUnit4 skipped it, saying "${row.reason}"`);
|
|
11270
11328
|
failing.add(row.suite);
|
|
11271
11329
|
}
|
|
@@ -11432,7 +11490,7 @@ ${String(error.warning)}`);
|
|
|
11432
11490
|
const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text, warning: debt.warning })));
|
|
11433
11491
|
return { printed: unexpected, neverPrinted };
|
|
11434
11492
|
}
|
|
11435
|
-
var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, MISMATCHED_ROW, BANNER, LEFT_OUT, DRIVER_FALLBACK, MACHINE_WARNED, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_ORPHANS = 101, GDUNIT_EXITS, GODOT_EXITS, TAIL = 60, MOST_NAMED = 8;
|
|
11493
|
+
var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, MISMATCHED_ROW, UNRUNNABLE_CASE, BANNER, LEFT_OUT, DRIVER_FALLBACK, MACHINE_WARNED, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_ORPHANS = 101, GDUNIT_EXITS, GODOT_EXITS, TAIL = 60, MOST_NAMED = 8;
|
|
11436
11494
|
var init_suite_judge = __esm(() => {
|
|
11437
11495
|
init_engine_leaks();
|
|
11438
11496
|
init_junit();
|
|
@@ -11444,6 +11502,7 @@ var init_suite_judge = __esm(() => {
|
|
|
11444
11502
|
WARNING_LINE = /^\s*(?:SCRIPT |USER )?WARNING: (.*?)\s*$/;
|
|
11445
11503
|
EXECUTED = /^Executed test cases\s*:\s*\((\d+)\//;
|
|
11446
11504
|
MISMATCHED_ROW = /^\s*The test data set at index \(\d+\) does not match the expected test parameters/;
|
|
11505
|
+
UNRUNNABLE_CASE = /^\s*Reason: '((?:Unknown test case argument's \[.*\] found|Fuzzer argument '.+' must be declared before the test config argument\(s\) \[.*\])\.)'\s*$/;
|
|
11447
11506
|
BANNER = /^\s*Godot Engine v(\S+?)(?:\s+-\s+\S+)?\s*$/;
|
|
11448
11507
|
LEFT_OUT = /^gdharness left out (res:\/\/.+?\.gd) > ([^:\s]+):(-?\d+)\s*$/;
|
|
11449
11508
|
DRIVER_FALLBACK = /^\s*WARNING: Your video card drivers .*, switching to /;
|
|
@@ -14231,7 +14290,8 @@ async function operationOutcome(engine, operation, params, projectPath) {
|
|
|
14231
14290
|
...afterAnswered
|
|
14232
14291
|
};
|
|
14233
14292
|
}
|
|
14234
|
-
const
|
|
14293
|
+
const onTheWayOut = answered === -1 ? problems : after;
|
|
14294
|
+
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)}). ${onTheWayOut.length === 0 ? "It printed no error or warning on the way out." : `What it reported on the way out is under ${answered === -1 ? "engine_messages" : "engine_messages_after_answer"}.`}`;
|
|
14235
14295
|
return {
|
|
14236
14296
|
ok: true,
|
|
14237
14297
|
payload: answer.payload,
|
|
@@ -14794,7 +14854,7 @@ var init_ports = __esm(() => {
|
|
|
14794
14854
|
import { existsSync as existsSync16, realpathSync as realpathSync2 } from "node:fs";
|
|
14795
14855
|
import { readFile, realpath } from "node:fs/promises";
|
|
14796
14856
|
import { createConnection } from "node:net";
|
|
14797
|
-
import { dirname as dirname8, join as join20, resolve as resolve6 } from "node:path";
|
|
14857
|
+
import { dirname as dirname8, extname as extname2, join as join20, resolve as resolve6 } from "node:path";
|
|
14798
14858
|
import { setTimeout as delay4 } from "node:timers/promises";
|
|
14799
14859
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14800
14860
|
function initializeTimeoutMs() {
|
|
@@ -15485,6 +15545,11 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
15485
15545
|
} catch {
|
|
15486
15546
|
throw new ArgumentRefusal(`Script file does not exist: res://${contained.relativePath}`);
|
|
15487
15547
|
}
|
|
15548
|
+
if (!scriptPath.toLowerCase().endsWith(".gd")) {
|
|
15549
|
+
const said = `res://${contained.relativePath} is not a GDScript file, and the language server reads .gd files only`;
|
|
15550
|
+
const kind = extname2(scriptPath).toLowerCase();
|
|
15551
|
+
throw new ArgumentRefusal(kind === ".tscn" || kind === ".scn" ? `${said}: scene_tree reads a scene's nodes, and with properties the script each carries.` : kind === ".tres" || kind === ".res" ? `${said}: resource_edit read reads a resource.` : `${said}.`);
|
|
15552
|
+
}
|
|
15488
15553
|
if (!isWithinRoot(projectPath, scriptPath)) {
|
|
15489
15554
|
throw new ArgumentRefusal("scriptPath resolves outside the project root boundary.");
|
|
15490
15555
|
}
|
|
@@ -15633,7 +15698,7 @@ var init_lsp_client = __esm(() => {
|
|
|
15633
15698
|
|
|
15634
15699
|
// src/resources.ts
|
|
15635
15700
|
import { readFileSync as readFileSync17, realpathSync as realpathSync3 } from "node:fs";
|
|
15636
|
-
import { extname as
|
|
15701
|
+
import { extname as extname3, resolve as resolve8 } from "node:path";
|
|
15637
15702
|
function ensureProjectPath(getProjectPath) {
|
|
15638
15703
|
const projectPath = getProjectPath();
|
|
15639
15704
|
if (!projectPath) {
|
|
@@ -15692,7 +15757,7 @@ function parseGodotUri(uri) {
|
|
|
15692
15757
|
}
|
|
15693
15758
|
function ensureAllowedExtension(kind, filePath) {
|
|
15694
15759
|
const allowed = EXTENSIONS[kind];
|
|
15695
|
-
if (!allowed.includes(
|
|
15760
|
+
if (!allowed.includes(extname3(filePath).toLowerCase())) {
|
|
15696
15761
|
throw new Refusal(`godot://${kind}/ reads only ${allowed.join(", ")} files.`);
|
|
15697
15762
|
}
|
|
15698
15763
|
}
|
|
@@ -16040,6 +16105,25 @@ function shippedRuntimeDigest() {
|
|
|
16040
16105
|
function installedEditorDigest(projectPath) {
|
|
16041
16106
|
return digestMarker(projectPath, ADDONS[0]);
|
|
16042
16107
|
}
|
|
16108
|
+
function editorUpgradeNote(sameCode, editors) {
|
|
16109
|
+
if (sameCode) {
|
|
16110
|
+
return ` 1. Nothing for the editor: this upgrade left the editor addons the same code as before,
|
|
16111
|
+
` + ` so it gives an open editor nothing to restart for. editor_status says addonIsStale
|
|
16112
|
+
` + " if the editor is running other code.";
|
|
16113
|
+
}
|
|
16114
|
+
if (editors === null) {
|
|
16115
|
+
return ` 1. An editor of this project that is open is still running the addons it loaded at
|
|
16116
|
+
` + ` startup. Restart it with the editor_launch restart tool, which closes and reopens
|
|
16117
|
+
` + " the window.";
|
|
16118
|
+
}
|
|
16119
|
+
if (editors.length === 0) {
|
|
16120
|
+
return ` 1. Nothing for the editor: none of this project is running, and the next one opened loads
|
|
16121
|
+
these addons.`;
|
|
16122
|
+
}
|
|
16123
|
+
const which = editors.length === 1 ? `The open editor (pid ${editors[0]}) is` : `The open editors (pids ${editors.join(", ")}) are`;
|
|
16124
|
+
return ` 1. ${which} still running the addons loaded at startup. Restart with the
|
|
16125
|
+
` + " editor_launch restart tool, which closes and reopens the window.";
|
|
16126
|
+
}
|
|
16043
16127
|
function installedRuntimeDigest(projectPath) {
|
|
16044
16128
|
return digestMarker(projectPath, RUNTIME_ADDON);
|
|
16045
16129
|
}
|
|
@@ -16511,6 +16595,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
16511
16595
|
},
|
|
16512
16596
|
nodePath: { type: "string", description: "Target node, relative to the AnimationPlayer root." },
|
|
16513
16597
|
property: { type: "string", description: "For property tracks: the property to animate." },
|
|
16598
|
+
update: {
|
|
16599
|
+
type: "string",
|
|
16600
|
+
enum: ["continuous", "discrete", "capture"],
|
|
16601
|
+
description: "For property tracks: how the value moves between keys. continuous blends toward the next key, so a bool or an int changes halfway to it; discrete holds each key's value until the next; capture blends from the value the property holds when playing starts. Without it a new track is made as the editor makes one: as the RESET animation's track for the same property, otherwise continuous for a float, vector, colour or transform and discrete for anything else. The answer gives the track's update."
|
|
16602
|
+
},
|
|
16514
16603
|
method: {
|
|
16515
16604
|
type: "string",
|
|
16516
16605
|
description: "For method tracks: the method to call. A method track holds every method key for its node, so a key calling another method joins the node's track, with existingTrack true. The answer's methodFound says whether the target node has it; a method it lacks is still added, while a nodePath that reaches no node is refused, since it may be written next, and until it exists each call the track makes fails. A key whose args the method cannot take (too few or too many, or an object where it takes a value or the reverse) is written too, and answers argumentsFit false with misfit saying why. remove_track: only the keys calling this method."
|
|
@@ -16651,7 +16740,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
16651
16740
|
include: {
|
|
16652
16741
|
type: "array",
|
|
16653
16742
|
items: { type: "string", enum: [...PROJECT_INFO_SECTIONS] },
|
|
16654
|
-
description: "Extra sections: registered autoloads, the classes the project's scripts declare with class_name, each with its script, read from the files, the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets (runnable marks the one preset of its platform the editor's one-click deploy uses), the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, each file a scene, resource or script refers to that does not exist, each input action a script reads by a name written in the call that neither the project nor the engine defines, with every line reading it, and each signal connection in a scene whose method or signal the node at its end does not have, judged from the node's type and scripts as their files are written."
|
|
16743
|
+
description: "Extra sections: registered autoloads, the classes the project's scripts declare with class_name, each with its script, read from the files (those an addon declares counted per addon under inAddons, unless addonClasses names the addon), the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets (runnable marks the one preset of its platform the editor's one-click deploy uses), the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, each file a scene, resource or script refers to that does not exist, each input action a script reads by a name written in the call that neither the project nor the engine defines, with every line reading it, and each signal connection in a scene whose method or signal the node at its end does not have, judged from the node's type and scripts as their files are written."
|
|
16744
|
+
},
|
|
16745
|
+
addonClasses: {
|
|
16746
|
+
type: "array",
|
|
16747
|
+
items: { type: "string" },
|
|
16748
|
+
description: "For classes: the addons, by their folder under res://addons, whose classes are listed by name and script like the project's own, rather than counted under inAddons. Default none: a game with two large addons answered 686 classes, 19 of them its own."
|
|
16655
16749
|
},
|
|
16656
16750
|
preset: { type: "string", description: "For validation: the export preset to validate against." },
|
|
16657
16751
|
detail: {
|
|
@@ -17046,7 +17140,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17046
17140
|
path: {
|
|
17047
17141
|
type: ["string", "array"],
|
|
17048
17142
|
items: { type: "string" },
|
|
17049
|
-
description: 'A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry "suite.gd:test_name" runs that case alone, as gdharness test reads it and as tests runs it, so it cannot share the list with a whole suite or a directory. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. A script named that declares no test_ function at the top level is refused the same way, and so is a directory holding no suite. A script that declares test_ functions and does not extend GdUnitTestSuite, which gdUnit4 never runs and never mentions, is refused when named, and under a directory run whole it is named under notSuites and the run is not called passed. A case declared there that gdUnit4 never ran, in a suite that passed, comes back under testsNotRun,
|
|
17143
|
+
description: 'A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry "suite.gd:test_name" runs that case alone, as gdharness test reads it and as tests runs it, so it cannot share the list with a whole suite or a directory. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. A script named that declares no test_ function at the top level is refused the same way, and so is a directory holding no suite. A script that declares test_ functions and does not extend GdUnitTestSuite, which gdUnit4 never runs and never mentions, is refused when named, and under a directory run whole it is named under notSuites and the run is not called passed. A case declared there that gdUnit4 never ran, in a suite that passed, comes back under testsNotRun, a row it skipped for data that does not fit the case under rowsMismatched, and a case it skipped because it cannot run it as declared (a parameter nothing fills, or a fuzzer after a setting) under casesUnrunnable, each with the reason gdUnit4 gave; none of those runs is called passed. Default tests and test, those the project has, which is where gdharness test looks.'
|
|
17050
17144
|
},
|
|
17051
17145
|
ignore: {
|
|
17052
17146
|
type: "array",
|
|
@@ -17144,7 +17238,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17144
17238
|
name: "scene_node",
|
|
17145
17239
|
title: "Scene nodes",
|
|
17146
17240
|
description: `One node in a scene file: add, read, set, duplicate, reparent or delete it, paint TileMap cells, or bake a navigation region. Any ClassDB node type or script class can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties, and another scene is instanced by an add with instancePath. A set with properties {"name": "Boss"} renames a node, and answers nodePath as its new path. A rename or a reparent carries the node paths naming the node, as the editor's rename does: NodePath values on nodes (in arrays, dictionaries, built-in resources, %unique paths) and the tracks its players' animations hold, saving a library kept in a .tres of its own, and answers each rewrite under retargeted and each path it left under notRetargeted. The move is followed into every other .tscn that instances or inherits this scene at any depth: the editable children, values, added nodes and connections named by the old path are rewritten, since Godot finds a moved node's values by unique_id but editable children only by path and drops the values kept below them on the next save, and that scene's node paths and tracks are carried as above. A delete rewrites no path, and answers under stillNaming what named the deleted node and now names nothing. 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. A node this scene inherits takes values of this scene's own, and so does a node inside an instance of another scene once that instance's editableChildren is true; neither can be moved, renamed, copied or deleted, since the other scene makes it again on load. ${WITHOUT_AN_EDITOR}`,
|
|
17147
|
-
answer: "get answers nodeType, the node's engine class, with scriptClass when its script declares a class_name; nodePath, its path from the root however it was named; setHere, the properties this scene's own file sets on it; and valuesFrom, the scene every other value comes from, when another scene makes the node. A set that gives a property the value the node has without it (what the scene it comes from gives, or its default) leaves it out of the file, as the editor does, and answers it under notStored, with notStoredNote saying what it follows now. retargeted lists each path a rename or a reparent rewrote, as {scenePath, nodePath, property or animation and track, from, to}, and notRetargeted each it left, with a reason: a track kept in another scene or an imported file, or a value another scene gives that the move left naming nothing. followed names the other scenes a move rewrote, and notFollowed any left as they were, with why. scriptsNaming lists each line of a script that reads a node a rename, a reparent or a delete took by the path it had, as {scenePath, nodePath, script, line, from, to}: the script is on the node at nodePath, or is one that node's script extends, in this scene or one instancing or inheriting it, and to is the path that reaches the node from there now, absent after a delete. It reads $Path, %Unique, and a string handed to get_node, get_node_or_null or has_node on the node itself; a path built at run time is not seen. Scripts are left as they were, as the editor's own rename leaves them, so each line listed fails until it is changed. A 2D bake_navigation that read no obstacle says in its note that all its outlines enclose is walkable, and names the nodes giving obstacles that sit under no region. stillNaming lists each node path and track, here and in the scenes instancing or inheriting this one, that named a deleted node, and deletedWithIt each node of this scene's own that went with it, the nodes inside an instance going with the instance. groups lists, sorted, every group a node is in, on a get, an add that gave addGroups and every set that changes them; get adds groupsHere, the groups this file puts it in, when another scene puts it in others, and layers, each layer mask it answers (collision_layer, collision_mask, visibility_layer) as the layers it holds, {layer, name} with the name project.godot gives the layer. Each of these lists is answered only when it names anything. add and set answer each property given under properties as the node holds it once written; alsoChanged gives the other properties the engine changed in step with a set, as a Control's offsets with its size, and those a reparent changed to keep the node where it is drawn, as {was, now}; namesNoNode, each node path written naming no node; sharedWith, for a property written through a resource built into the scene, each other {node, property} holding it, which changed too; setByScript, what a script that is not a tool set as a headless write ran, which the file does not hold; loadsAs, a value given that the file holds and the node's own script changes as the scene loads, as it loads. get adds placement, a Node3D's position, rotation_degrees and scale from its transform. " + ALSO_SAVED,
|
|
17241
|
+
answer: "get answers nodeType, the node's engine class, with scriptClass when its script declares a class_name; nodePath, its path from the root however it was named; setHere, the properties this scene's own file sets on it; and valuesFrom, the scene every other value comes from, when another scene makes the node. A set that gives a property the value the node has without it (what the scene it comes from gives, or its default) leaves it out of the file, as the editor does, and answers it under notStored, with notStoredNote saying what it follows now. retargeted lists each path a rename or a reparent rewrote, as {scenePath, nodePath, property or animation and track, from, to}, and notRetargeted each it left, with a reason: a track kept in another scene or an imported file, or a value another scene gives that the move left naming nothing. followed names the other scenes a move rewrote, and notFollowed any left as they were, with why. scriptsNaming lists each line of a script that reads a node a rename, a reparent or a delete took by the path it had, as {scenePath, nodePath, script, line, from, to}: the script is on the node at nodePath, or is one that node's script extends, in this scene or one instancing or inheriting it, and to is the path that reaches the node from there now, absent after a delete. It reads $Path, %Unique, and a string handed to get_node, get_node_or_null or has_node on the node itself; a path built at run time is not seen. Scripts are left as they were, as the editor's own rename leaves them, so each line listed fails until it is changed. A 2D bake_navigation that read no obstacle says in its note that all its outlines enclose is walkable, and names the nodes giving obstacles that sit under no region. stillNaming lists each node path and track, here and in the scenes instancing or inheriting this one, that named a deleted node; a property typed as a node, or an array or dictionary of nodes, holds the node itself, so the one that held a deleted node holds null there, as the editor saves it, and its entry adds cleared: true. deletedWithIt lists each node of this scene's own that went with it, the nodes inside an instance going with the instance. groups lists, sorted, every group a node is in, on a get, an add that gave addGroups and every set that changes them; get adds groupsHere, the groups this file puts it in, when another scene puts it in others, and layers, each layer mask it answers (collision_layer, collision_mask, visibility_layer) as the layers it holds, {layer, name} with the name project.godot gives the layer. Each of these lists is answered only when it names anything. add and set answer each property given under properties as the node holds it once written; alsoChanged gives the other properties the engine changed in step with a set, as a Control's offsets with its size, and those a reparent changed to keep the node where it is drawn or that the new parent sets, as a container gives a Control its layout_mode, as {was, now}; namesNoNode, each node path written naming no node; sharedWith, for a property written through a resource built into the scene, each other {node, property} holding it, which changed too; setByScript, what a script that is not a tool set as a headless write ran, which the file does not hold; loadsAs, a value given that the file holds and the node's own script changes as the scene loads, as it loads. get adds placement, a Node3D's position, rotation_degrees and scale from its transform. " + ALSO_SAVED,
|
|
17148
17242
|
parameters: {
|
|
17149
17243
|
projectPath: PROJECT_PATH,
|
|
17150
17244
|
scenePath: SCENE_PATH,
|
|
@@ -17186,7 +17280,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
17186
17280
|
ops: ["set"],
|
|
17187
17281
|
description: "set: groups to take the node out of. One it is not in is refused, and so is one another scene puts it in (the scene that makes it, instances it or is inherited here), which only that scene can take it out of."
|
|
17188
17282
|
},
|
|
17189
|
-
newName: {
|
|
17283
|
+
newName: {
|
|
17284
|
+
type: "string",
|
|
17285
|
+
ops: ["duplicate"],
|
|
17286
|
+
description: "duplicate: the copy's name. The copy has the nodes under the original and their connections, as the editor's Duplicate makes it: each connection keeps its flags, binds and unbinds, and one between two copied nodes joins the copies."
|
|
17287
|
+
},
|
|
17190
17288
|
newParentPath: {
|
|
17191
17289
|
type: "string",
|
|
17192
17290
|
ops: ["reparent"],
|
|
@@ -17258,7 +17356,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17258
17356
|
{
|
|
17259
17357
|
name: "scene_signal",
|
|
17260
17358
|
title: "Scene signals",
|
|
17261
|
-
description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connection carries binds, the values passed after the signal's own, and unbinds, how many of the signal's own are dropped first, as the editor's connect dialog offers them; each is answered only when there is one. A connect of a pair already connected says alreadyConnected, with flagsBefore, bindsBefore and unbindsBefore when it changed them, and keeps the binds and unbinds it had unless the call gives new ones. A connect to a method the target does not have is still written, as the editor's own dialog allows, since the method may be written next; methodFound false says so, and until the method exists the signal fails each time it is emitted. One whose method cannot take what the signal passes, with its binds and unbinds (too few or too many arguments, an object where the method takes a value or the reverse, or an engine class unrelated to the one it takes), is written too, and answers argumentsFit false with misfit saying why; list gives each connection its methodFound, and misfit when it has one. list marks editable false on a connection a scene this one instances or inherits makes, on its own root as well as inside it, and names that scene under madeIn; only that scene can change it, so a disconnect of one is refused. ${WITHOUT_AN_EDITOR} ${ALSO_SAVED}`,
|
|
17359
|
+
description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connection carries binds, the values passed after the signal's own, and unbinds, how many of the signal's own are dropped first, as the editor's connect dialog offers them; each is answered only when there is one. A connect of a pair already connected says alreadyConnected, with flagsBefore, bindsBefore and unbindsBefore when it changed them, and keeps the binds and unbinds it had unless the call gives new ones. A connect to a method the target does not have is still written, as the editor's own dialog allows, since the method may be written next; methodFound false says so, and until the method exists the signal fails each time it is emitted. That holds only without binds and unbinds: the engine makes a connection that binds or unbinds arguments only to a method its target has, and a scene loads without one aimed elsewhere, so such a connect is refused. A file can hold one already, written by hand or before its method was renamed: the engine loads the scene without it and a save drops it, so every tool that writes a .tscn puts it back as the file held it and says so under unmadeConnections, a write to a binary .scn holding one is refused, and a disconnect takes it out. One whose method cannot take what the signal passes, with its binds and unbinds (too few or too many arguments, an object where the method takes a value or the reverse, or an engine class unrelated to the one it takes), is written too, and answers argumentsFit false with misfit saying why; list gives each connection its methodFound, and misfit when it has one. list marks editable false on a connection a scene this one instances or inherits makes, on its own root as well as inside it, and names that scene under madeIn; only that scene can change it, so a disconnect of one is refused. ${WITHOUT_AN_EDITOR} ${ALSO_SAVED}`,
|
|
17262
17360
|
parameters: {
|
|
17263
17361
|
projectPath: PROJECT_PATH,
|
|
17264
17362
|
scenePath: SCENE_PATH,
|
|
@@ -18065,7 +18163,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18065
18163
|
property: {
|
|
18066
18164
|
type: "string",
|
|
18067
18165
|
ops: ["property", "find"],
|
|
18068
|
-
description: `property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A key holding a colon, a bracket or a quote is written in double quotes, 'stock:"base:dew"', as a refusal listing the keys writes it. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A vector, a colour, a rectangle or a transform is stepped into by its components as GDScript names them, so one axis is one number: "size:x", "global_position:y", "modulate:a", "global_transform:origin:z"; a name the value does not have is refused, naming the ones it has. It also answers the calls that read it with no arguments, by its type, such as "linear_velocity:length()" or "global_transform:get_origin()"; one it does not answer is refused, naming those it does. A string answers such calls too, "text:length()" or "text:strip_edges()", and "text:to_utf32_buffer():to_int32_array()" gives its code points, which tell a non-breaking space (160) from the space an answer shows in its place. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control's theme is read where it is drawn: 'get_theme_stylebox("panel"):content_margin_top' or 'get_theme_constant("separation")'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.`
|
|
18166
|
+
description: `property: which one to read, or a constant the node's script or a script it extends declares. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A key holding a colon, a bracket or a quote is written in double quotes, 'stock:"base:dew"', as a refusal listing the keys writes it. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A vector, a colour, a rectangle or a transform is stepped into by its components as GDScript names them, so one axis is one number: "size:x", "global_position:y", "modulate:a", "global_transform:origin:z"; a name the value does not have is refused, naming the ones it has. It also answers the calls that read it with no arguments, by its type, such as "linear_velocity:length()" or "global_transform:get_origin()"; one it does not answer is refused, naming those it does. A string answers such calls too, "text:length()" or "text:strip_edges()", and "text:to_utf32_buffer():to_int32_array()" gives its code points, which tell a non-breaking space (160) from the space an answer shows in its place. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control's theme is read where it is drawn: 'get_theme_stylebox("panel"):content_margin_top' or 'get_theme_constant("separation")'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.`
|
|
18069
18167
|
},
|
|
18070
18168
|
depth: {
|
|
18071
18169
|
type: "integer",
|
|
@@ -18532,7 +18630,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18532
18630
|
says: {
|
|
18533
18631
|
type: "string",
|
|
18534
18632
|
ops: ["until"],
|
|
18535
|
-
description: 'until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Matched the way runtime_inspect find matches says: case-insensitive, a plain word anywhere in the text, a pattern with * or ? a glob over the whole of it, and alternatives separated by | met by whichever appears first ("*won*|*lost*"); on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took, and says under looking_ms how long the looks took between them.'
|
|
18633
|
+
description: 'until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Matched the way runtime_inspect find matches says: case-insensitive, a plain word anywhere in the text, a pattern with * or ? a glob over the whole of it, and alternatives separated by | met by whichever appears first ("*won*|*lost*"); on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took, and says under looking_ms how long the looks took between them. A look on a large screen is read about 2ms to a frame, so no frame grows by more, except the last look as the time runs out, which is read whole; longest_hold_ms is the longest the looks held one frame, which tells a long frame the game logs apart from one the wait caused.'
|
|
18536
18634
|
},
|
|
18537
18635
|
includeHidden: {
|
|
18538
18636
|
type: "boolean",
|
|
@@ -41762,6 +41860,30 @@ function cutTestRunNote(run) {
|
|
|
41762
41860
|
}
|
|
41763
41861
|
var init_cut_test_run = () => {};
|
|
41764
41862
|
|
|
41863
|
+
// src/godot-variant.ts
|
|
41864
|
+
var FLAG_64, MOST_MESSAGE_BYTES;
|
|
41865
|
+
var init_godot_variant = __esm(() => {
|
|
41866
|
+
FLAG_64 = 1 << 16;
|
|
41867
|
+
MOST_MESSAGE_BYTES = (8 << 20) + 4;
|
|
41868
|
+
});
|
|
41869
|
+
|
|
41870
|
+
// src/game-debugger.ts
|
|
41871
|
+
var PAUSE_MS = 30000, STEP_MS = 1e4, RUNTIME_ADDON2 = "res://addons/gdharness_runtime/", RUNTIME_AUTOLOAD2, READ_MS = 5000, EVALUATE_MS = 60000, REPLIES;
|
|
41872
|
+
var init_game_debugger = __esm(() => {
|
|
41873
|
+
init_godot_variant();
|
|
41874
|
+
init_setup();
|
|
41875
|
+
RUNTIME_AUTOLOAD2 = `${RUNTIME_ADDON2}runtime_autoload.gd`;
|
|
41876
|
+
REPLIES = new Set([
|
|
41877
|
+
"debug_enter",
|
|
41878
|
+
"debug_exit",
|
|
41879
|
+
"stack_dump",
|
|
41880
|
+
"stack_frame_vars",
|
|
41881
|
+
"stack_frame_var",
|
|
41882
|
+
"evaluation_return",
|
|
41883
|
+
"scene:inspect_object"
|
|
41884
|
+
]);
|
|
41885
|
+
});
|
|
41886
|
+
|
|
41765
41887
|
// src/dap_client.ts
|
|
41766
41888
|
import { createConnection as createConnection3 } from "node:net";
|
|
41767
41889
|
import { setTimeout as delay6 } from "node:timers/promises";
|
|
@@ -41804,11 +41926,13 @@ class GodotDAPClient {
|
|
|
41804
41926
|
onBreakpointsChanged = null;
|
|
41805
41927
|
scopesPatienceMs;
|
|
41806
41928
|
stepPatienceMs;
|
|
41929
|
+
evaluatePatienceMs;
|
|
41807
41930
|
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1", patience = {}) {
|
|
41808
41931
|
this.port = port;
|
|
41809
41932
|
this.host = host;
|
|
41810
41933
|
this.scopesPatienceMs = patience.scopesMs ?? SCOPES_TIMEOUT_MS;
|
|
41811
41934
|
this.stepPatienceMs = patience.stepMs ?? STEP_LANDS_WITHIN_MS;
|
|
41935
|
+
this.evaluatePatienceMs = patience.evaluateMs ?? EVALUATE_MS;
|
|
41812
41936
|
this.pendingRequests = new Map;
|
|
41813
41937
|
}
|
|
41814
41938
|
async connect() {
|
|
@@ -42312,7 +42436,7 @@ class GodotDAPClient {
|
|
|
42312
42436
|
if (members?.variables?.some((variable) => variable["name"] === "self") !== true) {
|
|
42313
42437
|
throw new ReadRefusal(`Frame ${frame} has no instance of its script to evaluate on, as a static function called on its class has none, and the engine lets the game go on rather than answer one there, so it was not asked. Evaluate in a frame below it, or read this one's locals with debug_state variables`);
|
|
42314
42438
|
}
|
|
42315
|
-
const deadline = Date.now() + this.
|
|
42439
|
+
const deadline = Date.now() + this.evaluatePatienceMs;
|
|
42316
42440
|
while (Date.now() < deadline) {
|
|
42317
42441
|
try {
|
|
42318
42442
|
const answered = await this.sendRequest("evaluate", { expression, frameId: frame, context: "watch" }, VARIABLES_POLL_TIMEOUT_MS);
|
|
@@ -42324,7 +42448,7 @@ class GodotDAPClient {
|
|
|
42324
42448
|
} catch {}
|
|
42325
42449
|
await delay6(200);
|
|
42326
42450
|
}
|
|
42327
|
-
throw new ReadRefusal(`What ${expression} comes to did not arrive from the game within ${this.
|
|
42451
|
+
throw new ReadRefusal(`What ${expression} comes to did not arrive from the game within ${this.evaluatePatienceMs / 1000}s. It comes from the game through the editor, and the first expression a game evaluates loads the script of every global class in the project first, static initialisers included, so one of those may still be running: ask again`);
|
|
42328
42452
|
}
|
|
42329
42453
|
isConnected() {
|
|
42330
42454
|
return this.connected;
|
|
@@ -42471,6 +42595,7 @@ var init_dap_client = __esm(() => {
|
|
|
42471
42595
|
init_answer_json();
|
|
42472
42596
|
init_errors();
|
|
42473
42597
|
init_framing();
|
|
42598
|
+
init_game_debugger();
|
|
42474
42599
|
init_ports();
|
|
42475
42600
|
ReadRefusal = class ReadRefusal extends Refusal {
|
|
42476
42601
|
};
|
|
@@ -44040,7 +44165,7 @@ import {
|
|
|
44040
44165
|
statSync as statSync17,
|
|
44041
44166
|
writeFileSync as writeFileSync19
|
|
44042
44167
|
} from "node:fs";
|
|
44043
|
-
import { basename as basename10, dirname as dirname22, extname as
|
|
44168
|
+
import { basename as basename10, dirname as dirname22, extname as extname4, join as join38, relative as relative8 } from "node:path";
|
|
44044
44169
|
function filesUnder(directory) {
|
|
44045
44170
|
return readdirSync17(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
44046
44171
|
const path = join38(directory, entry.name);
|
|
@@ -44160,10 +44285,10 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
44160
44285
|
if (owner !== undefined && owner !== null) {
|
|
44161
44286
|
return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
|
|
44162
44287
|
}
|
|
44163
|
-
if (
|
|
44288
|
+
if (extname4(from).toLowerCase() !== extname4(to).toLowerCase()) {
|
|
44164
44289
|
return {
|
|
44165
44290
|
ok: false,
|
|
44166
|
-
reason: `${from} would become ${to}, and the engine picks how to read a file by its extension: give newPath the extension it has, ${
|
|
44291
|
+
reason: `${from} would become ${to}, and the engine picks how to read a file by its extension: give newPath the extension it has, ${extname4(from) || "none"}.`
|
|
44167
44292
|
};
|
|
44168
44293
|
}
|
|
44169
44294
|
}
|
|
@@ -44320,30 +44445,6 @@ var init_file_move = __esm(() => {
|
|
|
44320
44445
|
SIDECARS = [".uid", ".import"];
|
|
44321
44446
|
});
|
|
44322
44447
|
|
|
44323
|
-
// src/godot-variant.ts
|
|
44324
|
-
var FLAG_64, MOST_MESSAGE_BYTES;
|
|
44325
|
-
var init_godot_variant = __esm(() => {
|
|
44326
|
-
FLAG_64 = 1 << 16;
|
|
44327
|
-
MOST_MESSAGE_BYTES = (8 << 20) + 4;
|
|
44328
|
-
});
|
|
44329
|
-
|
|
44330
|
-
// src/game-debugger.ts
|
|
44331
|
-
var PAUSE_MS = 30000, STEP_MS = 1e4, RUNTIME_ADDON2 = "res://addons/gdharness_runtime/", RUNTIME_AUTOLOAD2, READ_MS = 5000, REPLIES;
|
|
44332
|
-
var init_game_debugger = __esm(() => {
|
|
44333
|
-
init_godot_variant();
|
|
44334
|
-
init_setup();
|
|
44335
|
-
RUNTIME_AUTOLOAD2 = `${RUNTIME_ADDON2}runtime_autoload.gd`;
|
|
44336
|
-
REPLIES = new Set([
|
|
44337
|
-
"debug_enter",
|
|
44338
|
-
"debug_exit",
|
|
44339
|
-
"stack_dump",
|
|
44340
|
-
"stack_frame_vars",
|
|
44341
|
-
"stack_frame_var",
|
|
44342
|
-
"evaluation_return",
|
|
44343
|
-
"scene:inspect_object"
|
|
44344
|
-
]);
|
|
44345
|
-
});
|
|
44346
|
-
|
|
44347
44448
|
// node_modules/ws/lib/constants.js
|
|
44348
44449
|
var require_constants = __commonJS(function(exports, module) {
|
|
44349
44450
|
var BINARY_TYPES = ["nodebuffer", "arraybuffer", "fragments"];
|
|
@@ -49221,7 +49322,7 @@ import {
|
|
|
49221
49322
|
statSync as statSync20,
|
|
49222
49323
|
writeFileSync as writeFileSync22
|
|
49223
49324
|
} from "node:fs";
|
|
49224
|
-
import { basename as basename13, dirname as dirname25, extname as
|
|
49325
|
+
import { basename as basename13, dirname as dirname25, extname as extname5, isAbsolute as isAbsolute8, join as join44, normalize as normalize2, relative as relative10 } from "node:path";
|
|
49225
49326
|
import { setTimeout as delay7 } from "node:timers/promises";
|
|
49226
49327
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
49227
49328
|
function editorRestartTimeoutMs() {
|
|
@@ -49273,7 +49374,7 @@ function writtenFileRefusal(path, rule) {
|
|
|
49273
49374
|
if (!isAbsolute8(path)) {
|
|
49274
49375
|
return `${named} ${path} is not absolute; name the whole path, so where it is written does not depend on this server's directory.`;
|
|
49275
49376
|
}
|
|
49276
|
-
if (!rule.extensions.includes(
|
|
49377
|
+
if (!rule.extensions.includes(extname5(path).toLowerCase())) {
|
|
49277
49378
|
return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
|
|
49278
49379
|
}
|
|
49279
49380
|
const directory = realAsFarAsItExists(dirname25(path));
|
|
@@ -49305,7 +49406,7 @@ function movieRefusal(engineArgs, projects, headless) {
|
|
|
49305
49406
|
extensions: [".avi", ".png"],
|
|
49306
49407
|
projects,
|
|
49307
49408
|
writes: (where) => {
|
|
49308
|
-
if (
|
|
49409
|
+
if (extname5(where).toLowerCase() !== ".png") {
|
|
49309
49410
|
return [where];
|
|
49310
49411
|
}
|
|
49311
49412
|
const base = where.slice(0, -".png".length);
|
|
@@ -49620,16 +49721,6 @@ function endedPreviousRun(ended) {
|
|
|
49620
49721
|
function previousRunLeft(ended) {
|
|
49621
49722
|
return (ended?.ending === "refused" || ended?.ending === "lingering") && ended.pid !== null ? ended.pid : undefined;
|
|
49622
49723
|
}
|
|
49623
|
-
function editorsOfProjectIn(tree, projectPath) {
|
|
49624
|
-
const found = [];
|
|
49625
|
-
for (const [pid, listed] of tree) {
|
|
49626
|
-
const read = readCommandLine(listed.command);
|
|
49627
|
-
if (read.editor && read.oneShot === null && read.projectPath !== null && /godot/i.test(read.executable) && !CONSOLE_WRAPPER.test(read.executable) && isSameDirectory(read.projectPath, projectPath)) {
|
|
49628
|
-
found.push(pid);
|
|
49629
|
-
}
|
|
49630
|
-
}
|
|
49631
|
-
return found.sort((a, b) => a - b);
|
|
49632
|
-
}
|
|
49633
49724
|
function engineNamesOf(executable) {
|
|
49634
49725
|
return CONSOLE_WRAPPER.test(executable) ? [executable, executable.replace(CONSOLE_WRAPPER, "$1")] : [executable];
|
|
49635
49726
|
}
|
|
@@ -52643,6 +52734,9 @@ class GodotServer {
|
|
|
52643
52734
|
if (forValidation.length > 0 && !include.includes("validation")) {
|
|
52644
52735
|
return this.createErrorResponse(`${forValidation.join(" and ")} ${forValidation.length === 1 ? "is" : "are"} read only by the validation section, which this call does not include: add "validation" to include, or leave ${forValidation.length === 1 ? "it" : "them"} out.`);
|
|
52645
52736
|
}
|
|
52737
|
+
if (args["addonClasses"] !== undefined && !include.includes("classes")) {
|
|
52738
|
+
return this.createErrorResponse('addonClasses is read only by the classes section, which this call does not include: add "classes" to include, or leave it out.');
|
|
52739
|
+
}
|
|
52646
52740
|
const project = this.project(args);
|
|
52647
52741
|
if (!project.ok) {
|
|
52648
52742
|
return project.response;
|
|
@@ -52673,6 +52767,23 @@ class GodotServer {
|
|
|
52673
52767
|
...compiledScripts !== undefined && (section === "health" || section === "validation") ? { compiled: compiledScripts } : {}
|
|
52674
52768
|
};
|
|
52675
52769
|
const outcome = await this.operation(spec.operation, params, project.value.path);
|
|
52770
|
+
if (section === "classes" && outcome.ok && Array.isArray(outcome.payload["classes"])) {
|
|
52771
|
+
const declared = outcome.payload["classes"];
|
|
52772
|
+
const asked = readStringArray(args, "addonClasses") ?? [];
|
|
52773
|
+
const addons = Object.keys(classesWithAddonsCounted(declared, new Set).inAddons).sort();
|
|
52774
|
+
const unknownAddons = asked.filter((addon) => !addons.includes(addon));
|
|
52775
|
+
if (unknownAddons.length > 0) {
|
|
52776
|
+
return this.createErrorResponse(`addonClasses names ${unknownAddons.join(", ")}, and no folder of that name under res://addons declares a class. ${addons.length === 0 ? "No addon declares one." : `The addons declaring classes are ${addons.join(", ")}.`}`);
|
|
52777
|
+
}
|
|
52778
|
+
const counted = classesWithAddonsCounted(declared, new Set(asked));
|
|
52779
|
+
info[section] = {
|
|
52780
|
+
...outcome.payload,
|
|
52781
|
+
classes: counted.classes,
|
|
52782
|
+
...Object.keys(counted.inAddons).length > 0 ? { inAddons: counted.inAddons } : {},
|
|
52783
|
+
...engineExtras(outcome)
|
|
52784
|
+
};
|
|
52785
|
+
continue;
|
|
52786
|
+
}
|
|
52676
52787
|
info[section] = outcome.ok ? { ...outcome.payload, ...engineExtras(outcome) } : { error: outcome.message, ...engineExtras(outcome) };
|
|
52677
52788
|
}
|
|
52678
52789
|
return this.jsonTextResponse(info);
|
|
@@ -52887,7 +52998,7 @@ ${platform}`;
|
|
|
52887
52998
|
if (pack && debug) {
|
|
52888
52999
|
return this.createErrorResponse("A pack is exported as a release: the command-line exporter has no debug pack, so debug and pack cannot both be true.", ["Leave debug out for a pack", "Or leave pack out for a debug build"]);
|
|
52889
53000
|
}
|
|
52890
|
-
const extension =
|
|
53001
|
+
const extension = extname5(output.relativePath).toLowerCase();
|
|
52891
53002
|
if (pack && extension !== ".pck" && extension !== ".zip") {
|
|
52892
53003
|
return this.createErrorResponse(`A pack is written as a .pck or a .zip, and ${output.relativePath} is neither: the exporter picks the format by the extension.`, ["Give outputPath ending in .pck, or .zip for a zip of the resources"]);
|
|
52893
53004
|
}
|
|
@@ -53408,10 +53519,14 @@ ${error.text}`;
|
|
|
53408
53519
|
test: `${row.suite} > ${row.name}`,
|
|
53409
53520
|
reason: row.reason
|
|
53410
53521
|
}));
|
|
53522
|
+
const casesUnrunnable = casePrinted.unrunnable.map((one) => ({
|
|
53523
|
+
test: `${one.suite} > ${one.name}`,
|
|
53524
|
+
reason: one.reason
|
|
53525
|
+
}));
|
|
53411
53526
|
return this.jsonTextResponse({
|
|
53412
|
-
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && undeclaredPrinted.length === 0 && declaredNotPrinted.length === 0 && countedLeaks.length === 0 && missing.length === 0 && testsNotRun.length === 0 && casesNotRun.length === 0 && rowsMismatched.length === 0 && notSuites.length === 0,
|
|
53527
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && undeclaredPrinted.length === 0 && declaredNotPrinted.length === 0 && countedLeaks.length === 0 && missing.length === 0 && testsNotRun.length === 0 && casesNotRun.length === 0 && rowsMismatched.length === 0 && casesUnrunnable.length === 0 && notSuites.length === 0,
|
|
53413
53528
|
verdict: [
|
|
53414
|
-
verdict === "passed" && undeclaredPrinted.length + declaredNotPrinted.length + countedLeaks.length + testsNotRun.length + casesNotRun.length + rowsMismatched.length + notSuites.length > 0 ? undefined : verdict,
|
|
53529
|
+
verdict === "passed" && undeclaredPrinted.length + declaredNotPrinted.length + countedLeaks.length + testsNotRun.length + casesNotRun.length + rowsMismatched.length + casesUnrunnable.length + notSuites.length > 0 ? undefined : verdict,
|
|
53415
53530
|
...[false, true].map((warning) => {
|
|
53416
53531
|
const count = undeclaredPrinted.filter((error) => error.warning === warning).length;
|
|
53417
53532
|
const word = warning ? "warning" : "error";
|
|
@@ -53425,12 +53540,14 @@ ${error.text}`;
|
|
|
53425
53540
|
countedLeaks.length > 0 ? "the engine exited holding what it should have freed" : undefined,
|
|
53426
53541
|
testsNotRun.length + casesNotRun.length > 0 ? `not run: ${[...testsNotRun, ...casesNotRun].join(", ")}` : undefined,
|
|
53427
53542
|
rowsMismatched.length > 0 ? `${rowsMismatched.length === 1 ? "one row" : `${String(rowsMismatched.length)} rows`} skipped for data that does not fit the case` : undefined,
|
|
53543
|
+
casesUnrunnable.length > 0 ? `${casesUnrunnable.length === 1 ? "one case" : `${String(casesUnrunnable.length)} cases`} skipped as gdUnit4 cannot run ${casesUnrunnable.length === 1 ? "it" : "them"} as declared` : undefined,
|
|
53428
53544
|
notSuites.length > 0 ? `${notSuites.length === 1 ? "one script declares" : `${String(notSuites.length)} scripts declare`} cases gdUnit4 never runs` : undefined
|
|
53429
53545
|
].filter((part) => part !== undefined).join("; "),
|
|
53430
53546
|
...missing.length > 0 ? { missing } : {},
|
|
53431
53547
|
...notSuites.length > 0 ? { notSuites } : {},
|
|
53432
53548
|
...testsNotRun.length + casesNotRun.length > 0 ? { testsNotRun: [...testsNotRun, ...casesNotRun] } : {},
|
|
53433
53549
|
...rowsMismatched.length > 0 ? { rowsMismatched } : {},
|
|
53550
|
+
...casesUnrunnable.length > 0 ? { casesUnrunnable } : {},
|
|
53434
53551
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
53435
53552
|
exitCode,
|
|
53436
53553
|
exitSignal: run.exitSignal ?? undefined,
|
|
@@ -53464,6 +53581,7 @@ ${error.text}`;
|
|
|
53464
53581
|
testsNotRun.length > 0 ? `${testsNotRun.join(", ")} ${testsNotRun.length === 1 ? "was" : "were"} asked for and not run, so the run is not called passed: gdUnit4 runs a test_ function only in a script that extends GdUnitTestSuite, and leaves out a suite whose script does not load.` : undefined,
|
|
53465
53582
|
casesNotRun.length > 0 ? `${casesNotRun.join(", ")} ${casesNotRun.length === 1 ? "is a case" : "are cases"} of a suite that ran and passed, and gdUnit4 never ran ${casesNotRun.length === 1 ? "it" : "them"} and said nothing of it, so the run is not called passed, as gdharness test judges it.` : undefined,
|
|
53466
53583
|
rowsMismatched.length > 0 ? `gdUnit4 skipped what rowsMismatched names because the row's data does not fit the case's parameters, though it printed ${rowsMismatched.length === 1 ? "the row" : "each row"} passed first; each entry carries gdUnit4's reason. The run is not called passed, as gdharness test judges it.` : undefined,
|
|
53584
|
+
casesUnrunnable.length > 0 ? `gdUnit4 skipped what casesUnrunnable names because it cannot run ${casesUnrunnable.length === 1 ? "that case" : "those cases"} as declared, though it printed ${casesUnrunnable.length === 1 ? "it" : "each"} passed first; each entry carries gdUnit4's reason. A parameter is filled only by _test_parameters, a fuzzer or a setting gdUnit4 names (timeout, do_skip, skip_reason, fuzzer_iterations, fuzzer_seed), and fuzzers come before the settings. The run is not called passed, as gdharness test judges it.` : undefined,
|
|
53467
53585
|
notSuites.length > 0 ? "What notSuites names declares cases gdUnit4 never runs, so the run is not called passed, as gdharness test judges it." : undefined,
|
|
53468
53586
|
briefed > 0 ? `${briefed} of the ${failed.length} failing cases, those with the largest reports, are named with their first message only, since their reports would have made the answer too large to deliver; pass one case's suite path to see its reports.` : undefined,
|
|
53469
53587
|
notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
|
|
@@ -53998,7 +54116,7 @@ ${error.text}`;
|
|
|
53998
54116
|
return { ...game, keeper: game.keeper };
|
|
53999
54117
|
}
|
|
54000
54118
|
async askTheKeptGame(keeper, request, projectPath) {
|
|
54001
|
-
const answered = await askTheKeepersDebugger(keeper, request, KEEPER_DEBUG_MS);
|
|
54119
|
+
const answered = await askTheKeepersDebugger(keeper, request, request.op === "evaluate" ? EVALUATE_MS + 2 * READ_MS : KEEPER_DEBUG_MS);
|
|
54002
54120
|
if ("unreached" in answered) {
|
|
54003
54121
|
return this.createErrorResponse(`The debugger of the running game did not answer: ${String(answered.unreached)}.`, [
|
|
54004
54122
|
"editor_run start starts the game again, with a debugger",
|
|
@@ -57522,6 +57640,7 @@ ${error.text}`;
|
|
|
57522
57640
|
async settleRenameOnDisk(projectPath, changed, asked) {
|
|
57523
57641
|
const report = {};
|
|
57524
57642
|
let ok = true;
|
|
57643
|
+
const cacheBefore = asked.rebuildCache ? cachedEntries(projectPath) : null;
|
|
57525
57644
|
if (asked.moved) {
|
|
57526
57645
|
const engine = await this.engine();
|
|
57527
57646
|
const sidecarsBefore = new Set(projectFiles(projectPath, [".uid", ".import"]));
|
|
@@ -57536,7 +57655,7 @@ ${error.text}`;
|
|
|
57536
57655
|
}
|
|
57537
57656
|
if (asked.rebuildCache) {
|
|
57538
57657
|
const rebuilt = await this.rebuildClassCache(projectPath);
|
|
57539
|
-
report["classCache"] = rebuilt.ok ? rebuilt.payload : { rebuilt: false, problem: rebuilt.message };
|
|
57658
|
+
report["classCache"] = rebuilt.ok ? { ...rebuilt.payload, ...cacheChangesBetween(cacheBefore, cachedEntries(projectPath)) } : { rebuilt: false, problem: rebuilt.message };
|
|
57540
57659
|
ok &&= rebuilt.ok;
|
|
57541
57660
|
}
|
|
57542
57661
|
if (changed.length > 0) {
|
|
@@ -58102,7 +58221,7 @@ async function runGodotServer() {
|
|
|
58102
58221
|
const server = new GodotServer;
|
|
58103
58222
|
await server.run();
|
|
58104
58223
|
}
|
|
58105
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000,
|
|
58224
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", 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, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, SLOW_ANSWER_MS = 1000, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_TIMEOUT_MS = 600000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
58106
58225
|
var init_server = __esm(() => {
|
|
58107
58226
|
init_dist2();
|
|
58108
58227
|
init_stdio();
|
|
@@ -58171,7 +58290,6 @@ var init_server = __esm(() => {
|
|
|
58171
58290
|
init_update_check();
|
|
58172
58291
|
__dirname2 = dirname25(fileURLToPath5(import.meta.url));
|
|
58173
58292
|
KEEPER_DEBUG_MS = PAUSE_MS + STEP_MS + 2 * READ_MS;
|
|
58174
|
-
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
58175
58293
|
LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
58176
58294
|
VALUE_NAMES = {
|
|
58177
58295
|
string: "a string",
|
|
@@ -58461,6 +58579,7 @@ async function pipe(port) {
|
|
|
58461
58579
|
|
|
58462
58580
|
// src/cli.ts
|
|
58463
58581
|
init_paths();
|
|
58582
|
+
init_process_children();
|
|
58464
58583
|
|
|
58465
58584
|
// src/prompt.ts
|
|
58466
58585
|
import process12 from "node:process";
|
|
@@ -60317,10 +60436,8 @@ async function upgrade() {
|
|
|
60317
60436
|
}
|
|
60318
60437
|
console.log(`
|
|
60319
60438
|
On ${version}. What is left for you:`);
|
|
60320
|
-
|
|
60321
|
-
|
|
60322
|
-
` + " if the editor is running other code." : ` 1. The open editor is still running the addons it loaded at startup. Restart it with the
|
|
60323
|
-
` + " editor_launch restart tool, which closes and reopens the window.");
|
|
60439
|
+
const tree = await processTree();
|
|
60440
|
+
console.log(editorUpgradeNote(digestBefore !== null && digestBefore === installedEditorDigest(projectPath), tree === undefined ? null : editorsOfProjectIn(tree, projectPath)));
|
|
60324
60441
|
const serving = await serverServing(projectPath);
|
|
60325
60442
|
console.log(harnessNote({ moved, written, byHand }, [launch.command, ...launch.args].join(" "), serving === null ? null : { server: serving, isThisVersion: serving.version === version }));
|
|
60326
60443
|
const trailing = worktreeCheckoutsOf(projectPath).flatMap((path) => {
|