gdharness 1.2.38 → 1.2.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -8577,9 +8577,616 @@ var init_scratch = __esm(() => {
8577
8577
  ];
8578
8578
  });
8579
8579
 
8580
+ // src/junit.ts
8581
+ function decoded(text) {
8582
+ return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/g, (whole, entity) => {
8583
+ if (entity.startsWith("#")) {
8584
+ const code = entity.startsWith("#x") ? Number.parseInt(entity.slice(2), 16) : Number.parseInt(entity.slice(1), 10);
8585
+ if (!Number.isInteger(code) || code < 0 || code > 1114111) {
8586
+ throw new MalformedReportError(`Not a character: ${whole}`);
8587
+ }
8588
+ return String.fromCodePoint(code);
8589
+ }
8590
+ const named = ENTITIES[entity];
8591
+ if (named === undefined) {
8592
+ throw new MalformedReportError(`Unknown entity ${whole}`);
8593
+ }
8594
+ return named;
8595
+ });
8596
+ }
8597
+ function tagEnd(source, from) {
8598
+ let quote = null;
8599
+ for (let at = from + 1;at < source.length; at += 1) {
8600
+ const character = source[at];
8601
+ if (quote !== null) {
8602
+ if (character === quote) {
8603
+ quote = null;
8604
+ }
8605
+ } else if (character === '"' || character === "'") {
8606
+ quote = character;
8607
+ } else if (character === ">") {
8608
+ return at;
8609
+ }
8610
+ }
8611
+ return -1;
8612
+ }
8613
+ function parseXml(source) {
8614
+ const root = { name: "", attributes: {}, children: [], text: "" };
8615
+ const open = [root];
8616
+ let at = 0;
8617
+ const current = () => {
8618
+ const element = open.at(-1);
8619
+ if (element === undefined) {
8620
+ throw new MalformedReportError("A close tag with nothing open");
8621
+ }
8622
+ return element;
8623
+ };
8624
+ while (at < source.length) {
8625
+ const next = source.indexOf("<", at);
8626
+ if (next === -1) {
8627
+ current().text += decoded(source.slice(at));
8628
+ break;
8629
+ }
8630
+ current().text += decoded(source.slice(at, next));
8631
+ at = next;
8632
+ if (source.startsWith("<![CDATA[", at)) {
8633
+ const end = source.indexOf("]]>", at);
8634
+ if (end === -1) {
8635
+ throw new MalformedReportError("CDATA that never ends");
8636
+ }
8637
+ current().text += source.slice(at + "<![CDATA[".length, end);
8638
+ at = end + "]]>".length;
8639
+ continue;
8640
+ }
8641
+ if (source.startsWith("<!--", at)) {
8642
+ const end = source.indexOf("-->", at);
8643
+ if (end === -1) {
8644
+ throw new MalformedReportError("A comment that never ends");
8645
+ }
8646
+ at = end + "-->".length;
8647
+ continue;
8648
+ }
8649
+ if (source.startsWith("<?", at) || source.startsWith("<!", at)) {
8650
+ const end = source.indexOf(">", at);
8651
+ if (end === -1) {
8652
+ throw new MalformedReportError("A declaration that never ends");
8653
+ }
8654
+ at = end + 1;
8655
+ continue;
8656
+ }
8657
+ const end = tagEnd(source, at);
8658
+ if (end === -1) {
8659
+ throw new MalformedReportError("A tag that never ends");
8660
+ }
8661
+ const tag = source.slice(at + 1, end);
8662
+ at = end + 1;
8663
+ if (tag.startsWith("/")) {
8664
+ const closing = tag.slice(1).trim();
8665
+ const element = current();
8666
+ if (element === root || element.name !== closing) {
8667
+ throw new MalformedReportError(`</${closing}> closes <${element.name || "nothing"}>`);
8668
+ }
8669
+ open.pop();
8670
+ continue;
8671
+ }
8672
+ const selfClosing = tag.endsWith("/");
8673
+ const body = selfClosing ? tag.slice(0, -1) : tag;
8674
+ const nameMatch = /^([^\s/>]+)/.exec(body);
8675
+ if (!nameMatch?.[1]) {
8676
+ throw new MalformedReportError(`A tag with no name: <${tag}>`);
8677
+ }
8678
+ const attributes = {};
8679
+ let read = nameMatch[0].length;
8680
+ ATTRIBUTE.lastIndex = read;
8681
+ let attribute = ATTRIBUTE.exec(body);
8682
+ while (attribute) {
8683
+ attributes[attribute[1] ?? ""] = decoded(attribute[2] ?? attribute[3] ?? "");
8684
+ read = ATTRIBUTE.lastIndex;
8685
+ attribute = ATTRIBUTE.exec(body);
8686
+ }
8687
+ if (body.slice(read).trim() !== "") {
8688
+ throw new MalformedReportError(`Unreadable attributes in <${tag}>`);
8689
+ }
8690
+ const element = { name: nameMatch[1], attributes, children: [], text: "" };
8691
+ current().children.push(element);
8692
+ if (!selfClosing) {
8693
+ open.push(element);
8694
+ }
8695
+ }
8696
+ if (open.length !== 1) {
8697
+ throw new MalformedReportError(`<${current().name}> is never closed`);
8698
+ }
8699
+ return root;
8700
+ }
8701
+ function count(attributes, name) {
8702
+ const value = Number(attributes[name] ?? "0");
8703
+ return Number.isFinite(value) ? value : 0;
8704
+ }
8705
+ function ignoredYetRan(report, ignored) {
8706
+ const found = [];
8707
+ for (const entry of ignored) {
8708
+ const bare = entry.replace("res://", "");
8709
+ const at = bare.lastIndexOf(":");
8710
+ const suiteNamed = at === -1 ? null : bare.slice(0, at);
8711
+ const testNamed = at === -1 ? bare : bare.slice(at + 1);
8712
+ for (const suite of report.suites) {
8713
+ const suiteMatches = suiteNamed === null || suite.name === suiteNamed || (suite.path ?? "").includes(suiteNamed);
8714
+ const ran = suiteMatches ? suite.cases.find((one) => one.name === testNamed) : undefined;
8715
+ if (ran !== undefined) {
8716
+ found.push({ entry, ran: `${suite.name}:${ran.name}` });
8717
+ }
8718
+ }
8719
+ }
8720
+ return found;
8721
+ }
8722
+ function caseOf(suite, element) {
8723
+ const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
8724
+ const detail = child.text.trim();
8725
+ return { kind, message: child.attributes["message"] ?? null, detail: detail === "" ? null : detail };
8726
+ });
8727
+ const reports = [...of("failure"), ...of("error"), ...of("skipped")];
8728
+ const status = reports.some((report) => report.kind === "error") ? "error" : reports.some((report) => report.kind === "failure") ? "failed" : reports.length > 0 ? "skipped" : "passed";
8729
+ return {
8730
+ suite: suite.attributes["name"] ?? "",
8731
+ name: element.attributes["name"] ?? "",
8732
+ status,
8733
+ time: count(element.attributes, "time"),
8734
+ message: reports[0]?.message ?? null,
8735
+ detail: reports[0]?.detail ?? null,
8736
+ reports
8737
+ };
8738
+ }
8739
+ function failedAnAssertion(entry) {
8740
+ return entry.reports.some((report) => report.kind === "failure");
8741
+ }
8742
+ function pathOf(suite) {
8743
+ const directory = suite.attributes["package"];
8744
+ const name = suite.attributes["name"];
8745
+ return directory && name ? `res://${directory}/${name}.gd` : null;
8746
+ }
8747
+ function parseJUnit(xml) {
8748
+ const document = parseXml(xml);
8749
+ const top = document.children.find((child) => child.name === "testsuites" || child.name === "testsuite");
8750
+ if (top === undefined) {
8751
+ throw new MalformedReportError("No <testsuites> or <testsuite> in the report");
8752
+ }
8753
+ const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
8754
+ const suites = suiteElements.map((suite) => {
8755
+ const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
8756
+ const failures = cases.filter(failedAnAssertion).length;
8757
+ const errors = cases.filter((entry) => entry.status === "error").length;
8758
+ const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
8759
+ return {
8760
+ name: suite.attributes["name"] ?? "",
8761
+ path: pathOf(suite),
8762
+ tests: cases.length,
8763
+ discovered: count(suite.attributes, "tests"),
8764
+ failures,
8765
+ errors,
8766
+ hookFailures: Math.max(0, count(suite.attributes, "failures") - written("failure")),
8767
+ hookErrors: Math.max(0, count(suite.attributes, "errors") - written("error")),
8768
+ skipped: cases.filter((entry) => entry.status === "skipped").length,
8769
+ time: count(suite.attributes, "time"),
8770
+ cases
8771
+ };
8772
+ });
8773
+ const all = suites.flatMap((suite) => suite.cases);
8774
+ return {
8775
+ tests: all.length,
8776
+ failures: all.filter(failedAnAssertion).length,
8777
+ errors: all.filter((entry) => entry.status === "error").length,
8778
+ hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
8779
+ hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
8780
+ skipped: all.filter((entry) => entry.status === "skipped").length,
8781
+ time: suites.reduce((sum, suite) => sum + suite.time, 0),
8782
+ suites
8783
+ };
8784
+ }
8785
+ function hookFailuresPrinted(printed) {
8786
+ return printed.flatMap((line) => {
8787
+ const found = HOOK_FAILURE.exec(line);
8788
+ return found === null ? [] : [{ hook: found[2] ?? "", path: found[3] ?? "", line: Number(found[4]), message: found[1] ?? "" }];
8789
+ });
8790
+ }
8791
+ function whyNoReport(printed, asked) {
8792
+ const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
8793
+ if (missing !== undefined) {
8794
+ return `nothing at ${missing[1] ?? asked}`;
8795
+ }
8796
+ return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
8797
+ }
8798
+ function scriptErrorsPrinted(printed) {
8799
+ return discoveryListing(printed)?.errors ?? [];
8800
+ }
8801
+ function discoveryListing(printed) {
8802
+ const start = printed.findIndex((line) => line.includes(DISCOVERY_FAILED));
8803
+ if (start < 0) {
8804
+ return null;
8805
+ }
8806
+ const found = [];
8807
+ let message = null;
8808
+ let to = start + 1;
8809
+ for (const line of printed.slice(start + 1)) {
8810
+ const at = /^\s*at (res:\/\/.+):(\d+)\s*$/.exec(line);
8811
+ if (at !== null && message !== null) {
8812
+ found.push({ path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]), message });
8813
+ message = null;
8814
+ } else if (at === null && message === null && /^ {2}\S/.test(line)) {
8815
+ message = line.trim();
8816
+ } else {
8817
+ break;
8818
+ }
8819
+ to += 1;
8820
+ }
8821
+ const errors = found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : (a.line ?? 0) - (b.line ?? 0));
8822
+ return { errors, from: start, to };
8823
+ }
8824
+ function unspelled(text) {
8825
+ return text.replace(/<(?:LF|CR|TAB|BS|FF|VT|BEL|ESC|DEL|0x[0-9A-F]{2})>/g, (spelling) => spelling.startsWith("<0x") ? String.fromCharCode(Number.parseInt(spelling.slice(3, 5), 16)) : SPELLED[spelling] ?? spelling);
8826
+ }
8827
+ function readDiff(printed) {
8828
+ let merged = "";
8829
+ let actual = "";
8830
+ let background = null;
8831
+ let last = 0;
8832
+ const take = (text) => {
8833
+ merged += text;
8834
+ if (background === null) {
8835
+ actual += text;
8836
+ } else if (background === "added") {
8837
+ actual += unspelled(text);
8838
+ }
8839
+ };
8840
+ for (const match of printed.matchAll(COLOUR)) {
8841
+ take(printed.slice(last, match.index));
8842
+ last = match.index + match[0].length;
8843
+ const codes = (match[1] ?? "").split(";");
8844
+ if (codes[0] === "48" && codes[1] === "2") {
8845
+ background = Number(codes[2]) > Number(codes[3]) ? "missing" : "added";
8846
+ } else if (codes[0] === "0" || codes[0] === "") {
8847
+ background = null;
8848
+ }
8849
+ }
8850
+ take(printed.slice(last));
8851
+ return { merged, actual };
8852
+ }
8853
+ function withActualsPrinted(failed, printed) {
8854
+ const diffs = [...printed.replaceAll(`\r
8855
+ `, `
8856
+ `).matchAll(PRINTED_ACTUAL)].map((match) => ({
8857
+ suffix: match[3] ?? "",
8858
+ at: match[5] ?? "",
8859
+ ...readDiff(match[1] ?? "")
8860
+ }));
8861
+ const used = new Set;
8862
+ return failed.map((entry) => {
8863
+ if (entry.detail === null) {
8864
+ return entry;
8865
+ }
8866
+ let detail = entry.detail;
8867
+ let recovered = false;
8868
+ for (const [index, diff] of diffs.entries()) {
8869
+ if (used.has(index)) {
8870
+ continue;
8871
+ }
8872
+ const wrong = ` but was
8873
+ '${diff.merged.replaceAll("\r", "")}'${diff.suffix}
8874
+ at ${diff.at}`;
8875
+ if (detail.includes(wrong)) {
8876
+ detail = detail.replace(wrong, () => ` but was
8877
+ '${diff.actual}'${diff.suffix}
8878
+ at ${diff.at}`);
8879
+ recovered = true;
8880
+ used.add(index);
8881
+ }
8882
+ }
8883
+ const quoted = QUOTED_EQUALITY.exec(detail);
8884
+ if (quoted !== null) {
8885
+ const alike = quoted[1] === quoted[2];
8886
+ const note = recovered ? alike ? RENDERED_AWAY_NOTE : null : alike ? BOTH_SIDES_ALIKE_NOTE : VALUE_NOT_RECOVERED_NOTE;
8887
+ detail = note === null ? detail : `${detail}
8888
+ ${note}`;
8889
+ }
8890
+ if (quoted !== null && masked(quoted[1] ?? "", quoted[2] ?? "")) {
8891
+ const end = quoted[0].length - `
8892
+ at `.length;
8893
+ detail = detail.slice(0, end).replaceAll(MASKED_BRACKET, "[") + detail.slice(end);
8894
+ }
8895
+ return detail === entry.detail ? entry : { ...entry, detail };
8896
+ });
8897
+ }
8898
+ function shortened(detail) {
8899
+ if (detail === null || detail.length <= LONGEST_DETAIL) {
8900
+ return detail;
8901
+ }
8902
+ return `${detail.slice(0, LONGEST_DETAIL)}
8903
+ [${detail.length - LONGEST_DETAIL} more characters left out]`;
8904
+ }
8905
+ function foldedReports(reports) {
8906
+ const first = new Map;
8907
+ for (const report of reports) {
8908
+ const key = JSON.stringify([report.kind, report.message, report.detail]);
8909
+ const seen = first.get(key);
8910
+ if (seen === undefined) {
8911
+ first.set(key, { report, times: 1 });
8912
+ } else {
8913
+ seen.times += 1;
8914
+ }
8915
+ }
8916
+ return [...first.values()].map(({ report, times }) => {
8917
+ const kept = { ...report, detail: shortened(report.detail) };
8918
+ return times === 1 ? kept : { ...kept, times };
8919
+ });
8920
+ }
8921
+ function failedCases(report, printed) {
8922
+ const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
8923
+ const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
8924
+ let next = 0;
8925
+ return cases.map(({ entry, path }) => {
8926
+ const reports = read.slice(next, next + entry.reports.length);
8927
+ next += entry.reports.length;
8928
+ const folded = foldedReports(reports);
8929
+ const shown = folded.slice(0, MOST_REPORTS_SHOWN);
8930
+ return {
8931
+ suite: entry.suite,
8932
+ name: entry.name,
8933
+ status: entry.status,
8934
+ time: entry.time,
8935
+ path,
8936
+ message: reports[0]?.message ?? null,
8937
+ detail: shortened(reports[0]?.detail ?? null),
8938
+ ...reports.length > 1 ? { reports: shown, reportCount: reports.length } : {},
8939
+ ...folded.length > shown.length ? { reportsLeftOut: folded.length - shown.length } : {}
8940
+ };
8941
+ });
8942
+ }
8943
+ function failedWithin(failed, budget) {
8944
+ const sizes = failed.map((entry) => JSON.stringify(entry).length);
8945
+ const smallestFirst = sizes.map((_, index) => index).sort((a, b) => (sizes[a] ?? 0) - (sizes[b] ?? 0));
8946
+ const whole = new Set;
8947
+ let spent = 0;
8948
+ for (const index of smallestFirst) {
8949
+ const size = sizes[index] ?? 0;
8950
+ if (spent + size > budget) {
8951
+ break;
8952
+ }
8953
+ spent += size;
8954
+ whole.add(index);
8955
+ }
8956
+ const kept = failed.map((entry, index) => {
8957
+ if (whole.has(index)) {
8958
+ return entry;
8959
+ }
8960
+ const { detail: _detail, reports: _reports, reportsLeftOut: _left, ...named } = entry;
8961
+ return { ...named, message: entry.message?.slice(0, 500) ?? null, detail: null };
8962
+ });
8963
+ return { failed: kept, briefed: failed.length - whole.size };
8964
+ }
8965
+ function masked(expected, actual) {
8966
+ const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
8967
+ return `${expected}${actual}`.includes(MASKED_BRACKET) && allSpelled(expected) && allSpelled(actual);
8968
+ }
8969
+ function orphansPrinted(printed) {
8970
+ const suites = [];
8971
+ let total = null;
8972
+ let running = null;
8973
+ for (const line of printed) {
8974
+ const started = RUNNING_SUITE.exec(line);
8975
+ if (started?.[1] !== undefined) {
8976
+ running = started[1];
8977
+ continue;
8978
+ }
8979
+ const counted = ORPHANS.exec(line);
8980
+ if (counted?.[1] === undefined) {
8981
+ continue;
8982
+ }
8983
+ const orphans = Number(counted[1]);
8984
+ if (line.includes(OVERALL)) {
8985
+ total = orphans;
8986
+ running = null;
8987
+ continue;
8988
+ }
8989
+ if (running !== null && orphans > 0) {
8990
+ suites.push({ path: running, orphans });
8991
+ }
8992
+ running = null;
8993
+ }
8994
+ return {
8995
+ total: total ?? suites.reduce((sum, suite) => sum + suite.orphans, 0),
8996
+ suites
8997
+ };
8998
+ }
8999
+ function finishesASuite(line) {
9000
+ return SUITE_COUNTS.test(line) && !line.includes(OVERALL);
9001
+ }
9002
+ function suitesPrinted(printed) {
9003
+ const finished = [];
9004
+ let running = null;
9005
+ let failed = [];
9006
+ for (const line of printed) {
9007
+ const started = RUNNING_SUITE.exec(line);
9008
+ if (started?.[1] !== undefined) {
9009
+ running = started[1];
9010
+ failed = [];
9011
+ continue;
9012
+ }
9013
+ if (running === null) {
9014
+ continue;
9015
+ }
9016
+ const failing = CASE_FAILED.exec(line)?.[1];
9017
+ if (failing !== undefined) {
9018
+ failed.push(failing);
9019
+ continue;
9020
+ }
9021
+ const counted = finishesASuite(line) ? SUITE_COUNTS.exec(line) : null;
9022
+ if (counted !== null) {
9023
+ finished.push({
9024
+ path: running,
9025
+ cases: Number(counted[1]),
9026
+ errors: Number(counted[2]),
9027
+ failures: Number(counted[3]),
9028
+ failed
9029
+ });
9030
+ running = null;
9031
+ }
9032
+ }
9033
+ return { finished, running };
9034
+ }
9035
+ function testProgressMessage(printed, elapsedMs) {
9036
+ const { finished, running } = suitesPrinted(printed);
9037
+ const failing = finished.filter((suite) => suite.errors + suite.failures > 0).length;
9038
+ const seconds = Math.floor(elapsedMs / 1000);
9039
+ const took = seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
9040
+ const done = finished.length === 0 ? "No suite has finished" : `${finished.length === 1 ? "One suite has" : `${finished.length} suites have`} finished${failing === 0 ? "" : `, ${failing} with failures`}`;
9041
+ return `${done}${running === null ? "" : `; running ${running}`}; ${took} in.`;
9042
+ }
9043
+ 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, COLOUR, 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.]', MOST_REPORTS_SHOWN = 20, LONGEST_DETAIL = 20000, MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary", SUITE_COUNTS, CASE_FAILED;
9044
+ var init_junit = __esm(() => {
9045
+ MalformedReportError = class MalformedReportError extends Error {
9046
+ constructor(message) {
9047
+ super(message);
9048
+ this.name = "MalformedReportError";
9049
+ }
9050
+ };
9051
+ ENTITIES = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
9052
+ ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
9053
+ HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
9054
+ NOTHING_RAN = /no test cases found/i;
9055
+ NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
9056
+ ESCAPE = String.fromCharCode(27);
9057
+ PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)(${IGNORING_CASE})?((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
9058
+ QUOTED_EQUALITY = new RegExp(`^Expecting(?: error message)?:\\n '([\\s\\S]*)'\\n but was\\n '([\\s\\S]*)'(?:${IGNORING_CASE})?\\n\\tat `);
9059
+ COLOUR = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
9060
+ SPELLED = {
9061
+ "<LF>": `
9062
+ `,
9063
+ "<CR>": "\r",
9064
+ "<TAB>": "\t",
9065
+ "<BS>": "\b",
9066
+ "<FF>": "\f",
9067
+ "<VT>": "\v",
9068
+ "<BEL>": "\x07",
9069
+ "<ESC>": ESCAPE,
9070
+ "<DEL>": ""
9071
+ };
9072
+ PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
9073
+ RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
9074
+ ORPHANS = /(\d+)\s+orphans/;
9075
+ SUITE_COUNTS = /(\d+)\s+test cases\s*\|\s*(\d+)\s+errors\s*\|\s*(\d+)\s+failures/;
9076
+ CASE_FAILED = /^\s*res:\/\/.+?\.(?:gd|cs) > (.+?) (?:FAILED|ERROR)(?:\s|$)/;
9077
+ });
9078
+
9079
+ // src/script-origins.ts
9080
+ import { join as join9 } from "node:path";
9081
+ function unresolvedClassIn(message) {
9082
+ return UNRESOLVED.exec(message)?.[1] ?? null;
9083
+ }
9084
+ async function originsOf(errors, declared, check) {
9085
+ const listed = new Set(errors.map((error) => error.path));
9086
+ const origins = [];
9087
+ const unconfirmed = [];
9088
+ const asked = new Set;
9089
+ const waiting = errors.map((error) => unresolvedClassIn(error.message)).filter((name) => name !== null);
9090
+ while (waiting.length > 0 && asked.size < MOST_CHECKED) {
9091
+ const name = waiting.shift() ?? "";
9092
+ const path = declared.get(name);
9093
+ if (path === undefined || listed.has(path) || asked.has(path)) {
9094
+ continue;
9095
+ }
9096
+ asked.add(path);
9097
+ const checked = await check(path);
9098
+ const own = checked.errors.filter((error) => error.path === path);
9099
+ if (own.length === 0) {
9100
+ unconfirmed.push({
9101
+ path,
9102
+ declares: name,
9103
+ why: checked.failure === null ? "the engine's check of it found no error" : `the engine's check of it did not finish (${checked.failure})`
9104
+ });
9105
+ continue;
9106
+ }
9107
+ for (const error of own) {
9108
+ origins.push({ ...error, declares: name });
9109
+ const deeper = unresolvedClassIn(error.message);
9110
+ if (deeper !== null) {
9111
+ waiting.push(deeper);
9112
+ }
9113
+ }
9114
+ }
9115
+ return { origins, unconfirmed };
9116
+ }
9117
+ function notCompiledIn(entry) {
9118
+ return NOT_COMPILED.exec(entry.text)?.[1];
9119
+ }
9120
+ function compileErrorsIn(entries) {
9121
+ return entries.flatMap((entry) => {
9122
+ const at = entry.severity === "error" ? reloadedAt(entry) : null;
9123
+ return at === null ? [] : [{ ...at, message: entry.text }];
9124
+ });
9125
+ }
9126
+ function reloadedAt(entry) {
9127
+ for (const line of entry.detail) {
9128
+ const at = RELOADED_AT.exec(line.trim());
9129
+ if (at !== null) {
9130
+ return { path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]) };
9131
+ }
9132
+ }
9133
+ return null;
9134
+ }
9135
+ function checkWithEngine(godotPath, projectPath) {
9136
+ return async (path) => {
9137
+ const logDir = scratchDirectory("tests");
9138
+ try {
9139
+ const ran = await runEngine(godotPath, [
9140
+ "--headless",
9141
+ "--log-file",
9142
+ join9(logDir, "engine.log"),
9143
+ "--path",
9144
+ projectPath,
9145
+ "--check-only",
9146
+ "-s",
9147
+ path
9148
+ ], { timeout: { ms: 60000, said: "minute" } });
9149
+ const errors = ran.log.select({ severity: "error", sinceLastCall: false, limit: 50 }).entries.flatMap((entry) => {
9150
+ const at = reloadedAt(entry);
9151
+ return at === null ? [] : [{ ...at, message: entry.text }];
9152
+ });
9153
+ return { errors, failure: ran.failure === null ? null : howItEnded(ran) };
9154
+ } finally {
9155
+ discard(logDir);
9156
+ }
9157
+ };
9158
+ }
9159
+ function withoutListed(entries, errors) {
9160
+ if (errors.length === 0) {
9161
+ return [...entries];
9162
+ }
9163
+ const listing = discoveryListing(entries.map((entry) => entry.text));
9164
+ const paths = new Set(errors.map((error) => error.path));
9165
+ return entries.filter((entry, at) => {
9166
+ if (listing !== null && at >= listing.from && at < listing.to) {
9167
+ return false;
9168
+ }
9169
+ const reloaded = reloadedAt(entry);
9170
+ if (reloaded !== null && errors.some((error) => error.path === reloaded.path && error.line === reloaded.line && error.message === entry.text)) {
9171
+ return false;
9172
+ }
9173
+ const failedToLoad = /^Failed to load script "(res:\/\/[^"]+)" with error "[^"]+"\.$/.exec(entry.text);
9174
+ return failedToLoad === null || !paths.has(failedToLoad[1] ?? "");
9175
+ });
9176
+ }
9177
+ var UNRESOLVED, MOST_CHECKED = 8, RELOADED_AT, NOT_COMPILED;
9178
+ var init_script_origins = __esm(() => {
9179
+ init_engine_run();
9180
+ init_junit();
9181
+ init_scratch();
9182
+ UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
9183
+ RELOADED_AT = /GDScript::reload \((res:\/\/.+):(\d+)\)$/;
9184
+ NOT_COMPILED = /^Failed to load script "(res:\/\/[^"]+)" with error "(?:Parse error|Compilation failed)"/;
9185
+ });
9186
+
8580
9187
  // src/headless.ts
8581
9188
  import { existsSync as existsSync9, readFileSync as readFileSync5, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "node:fs";
8582
- import { dirname as dirname6, join as join9 } from "node:path";
9189
+ import { dirname as dirname6, join as join10 } from "node:path";
8583
9190
  function snakeCased(params) {
8584
9191
  const result = emptyRecord();
8585
9192
  for (const [key, value] of Object.entries(params)) {
@@ -8595,15 +9202,22 @@ function engineExtras(outcome) {
8595
9202
  ...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
8596
9203
  ...before.length > 0 ? {
8597
9204
  engine_messages_before_operation: before,
8598
- engine_messages_before_operation_note: BEFORE_OPERATION_NOTE
9205
+ engine_messages_before_operation_note: withNotCompiled(BEFORE_OPERATION_NOTE, before)
8599
9206
  } : {},
8600
9207
  ...outcome.beforeOperationOmitted === undefined ? {} : { engine_messages_before_operation_omitted: outcome.beforeOperationOmitted },
8601
9208
  ...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
8602
- ...after.length > 0 ? { engine_messages_after_answer: after, engine_messages_after_answer_note: AFTER_ANSWER_NOTE } : {},
9209
+ ...after.length > 0 ? {
9210
+ engine_messages_after_answer: after,
9211
+ engine_messages_after_answer_note: withNotCompiled(AFTER_ANSWER_NOTE, after)
9212
+ } : {},
8603
9213
  ...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
8604
9214
  ...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
8605
9215
  };
8606
9216
  }
9217
+ function withNotCompiled(note, entries) {
9218
+ const path = entries.map(notCompiledIn).find((found) => found !== undefined);
9219
+ return path === undefined ? note : `${note} ${path} did not compile, so every run of this project starts its autoloads without it until it is fixed; its errors are among these.`;
9220
+ }
8607
9221
  function problemsIn(log) {
8608
9222
  return problemsAmong(log.everything());
8609
9223
  }
@@ -8704,7 +9318,7 @@ async function importOnce(godotPath, projectPath) {
8704
9318
  ran = await runEngine(godotPath, [
8705
9319
  "--headless",
8706
9320
  "--log-file",
8707
- join9(logDir, "engine.log"),
9321
+ join10(logDir, "engine.log"),
8708
9322
  "--path",
8709
9323
  projectPath,
8710
9324
  "--import"
@@ -8726,13 +9340,13 @@ async function importOnce(godotPath, projectPath) {
8726
9340
  }
8727
9341
  async function runOperation(engine, operation, params, projectPath) {
8728
9342
  const paramsDir = scratchDirectory("params");
8729
- const paramsFile = join9(paramsDir, `${operation}.json`);
8730
- const answerFile = join9(paramsDir, "answer.json");
9343
+ const paramsFile = join10(paramsDir, `${operation}.json`);
9344
+ const answerFile = join10(paramsDir, "answer.json");
8731
9345
  writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
8732
9346
  const args = [
8733
9347
  "--headless",
8734
9348
  "--log-file",
8735
- join9(paramsDir, "engine.log"),
9349
+ join10(paramsDir, "engine.log"),
8736
9350
  "--path",
8737
9351
  projectPath,
8738
9352
  "--script",
@@ -8841,12 +9455,13 @@ var init_headless = __esm(() => {
8841
9455
  init_library_copies();
8842
9456
  init_reimport();
8843
9457
  init_scratch();
9458
+ init_script_origins();
8844
9459
  });
8845
9460
 
8846
9461
  // src/update-check.ts
8847
9462
  import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
8848
9463
  import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
8849
- import { join as join10 } from "node:path";
9464
+ import { join as join11 } from "node:path";
8850
9465
  function packageUrl(root) {
8851
9466
  return `${root.replace(/\/+$/, "")}/gdharness/latest`;
8852
9467
  }
@@ -8868,20 +9483,20 @@ function cacheDirectory(environment) {
8868
9483
  };
8869
9484
  const home = set("HOME") ?? homedir4();
8870
9485
  if (process.platform === "win32") {
8871
- return join10(set("LOCALAPPDATA") ?? home, "gdharness");
9486
+ return join11(set("LOCALAPPDATA") ?? home, "gdharness");
8872
9487
  }
8873
9488
  if (process.platform === "darwin") {
8874
- return join10(home, "Library", "Caches", "gdharness");
9489
+ return join11(home, "Library", "Caches", "gdharness");
8875
9490
  }
8876
- return join10(set("XDG_CACHE_HOME") ?? join10(home, ".cache"), "gdharness");
9491
+ return join11(set("XDG_CACHE_HOME") ?? join11(home, ".cache"), "gdharness");
8877
9492
  }
8878
9493
  function cacheFile(environment = process.env) {
8879
9494
  try {
8880
9495
  const directory = cacheDirectory(environment);
8881
9496
  mkdirSync3(directory, { recursive: true, mode: 448 });
8882
- return join10(directory, "update-check.json");
9497
+ return join11(directory, "update-check.json");
8883
9498
  } catch {
8884
- return join10(tmpdir3(), "gdharness-update-check.json");
9499
+ return join11(tmpdir3(), "gdharness-update-check.json");
8885
9500
  }
8886
9501
  }
8887
9502
  function readCache(path) {
@@ -9897,7 +10512,7 @@ var init_gdscript_source = __esm(() => {
9897
10512
 
9898
10513
  // src/class-cache.ts
9899
10514
  import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync6 } from "node:fs";
9900
- import { join as join11, posix } from "node:path";
10515
+ import { join as join12, posix } from "node:path";
9901
10516
  function isToolScript(source) {
9902
10517
  return TOOL.test(codeOf(source));
9903
10518
  }
@@ -9913,14 +10528,14 @@ function declaredClasses(projectPath) {
9913
10528
  }
9914
10529
  function eachScript(projectPath, each) {
9915
10530
  const visit = (directory, prefix) => {
9916
- if (existsSync11(join11(directory, ".gdignore"))) {
10531
+ if (existsSync11(join12(directory, ".gdignore"))) {
9917
10532
  return;
9918
10533
  }
9919
10534
  for (const entry of readdirSync6(directory, { withFileTypes: true })) {
9920
10535
  if (entry.name.startsWith(".")) {
9921
10536
  continue;
9922
10537
  }
9923
- const path = join11(directory, entry.name);
10538
+ const path = join12(directory, entry.name);
9924
10539
  if (entry.isDirectory()) {
9925
10540
  visit(path, `${prefix}${entry.name}/`);
9926
10541
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
@@ -9946,7 +10561,7 @@ function scriptsNaming(projectPath, classes) {
9946
10561
  return naming.sort((a, b) => a.script.localeCompare(b.script));
9947
10562
  }
9948
10563
  function cachedClasses(projectPath) {
9949
- const cache = join11(projectPath, ".godot", "global_script_class_cache.cfg");
10564
+ const cache = join12(projectPath, ".godot", "global_script_class_cache.cfg");
9950
10565
  if (!existsSync11(cache)) {
9951
10566
  return null;
9952
10567
  }
@@ -10217,7 +10832,7 @@ function uncachedClassNote(types) {
10217
10832
  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.";
10218
10833
  }
10219
10834
  function cacheWrittenAt(projectPath) {
10220
- const cache = join11(projectPath, ".godot", "global_script_class_cache.cfg");
10835
+ const cache = join12(projectPath, ".godot", "global_script_class_cache.cfg");
10221
10836
  try {
10222
10837
  return statSync6(cache).mtimeMs;
10223
10838
  } catch {
@@ -10231,14 +10846,14 @@ function declaredSince(projectPath, paths, names, since) {
10231
10846
  return false;
10232
10847
  }
10233
10848
  try {
10234
- return statSync6(join11(projectPath, path.slice("res://".length))).mtimeMs > since;
10849
+ return statSync6(join12(projectPath, path.slice("res://".length))).mtimeMs > since;
10235
10850
  } catch {
10236
10851
  return false;
10237
10852
  }
10238
10853
  });
10239
10854
  }
10240
10855
  function cachedAtMissingPaths(cached, projectPath) {
10241
- return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync11(join11(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
10856
+ return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync11(join12(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
10242
10857
  }
10243
10858
  function staleAgainst(cached, projectPath) {
10244
10859
  return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
@@ -10264,7 +10879,7 @@ var init_class_cache = __esm(() => {
10264
10879
 
10265
10880
  // src/project-scan.ts
10266
10881
  import { existsSync as existsSync12, readdirSync as readdirSync7, readFileSync as readFileSync9 } from "node:fs";
10267
- import { extname, join as join12 } from "node:path";
10882
+ import { extname, join as join13 } from "node:path";
10268
10883
  function skipped(name) {
10269
10884
  return name.startsWith(".") || SKIPPED.has(name);
10270
10885
  }
@@ -10272,10 +10887,10 @@ function steppedOver(directory) {
10272
10887
  return gdignored(directory) || installedByUs(directory);
10273
10888
  }
10274
10889
  function gdignored(directory) {
10275
- return existsSync12(join12(directory, ".gdignore"));
10890
+ return existsSync12(join13(directory, ".gdignore"));
10276
10891
  }
10277
10892
  function installedByUs(directory) {
10278
- return existsSync12(join12(directory, VERSION_MARKER));
10893
+ return existsSync12(join13(directory, VERSION_MARKER));
10279
10894
  }
10280
10895
  function projectStructure(projectPath) {
10281
10896
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
@@ -10288,7 +10903,7 @@ function projectStructure(projectPath) {
10288
10903
  continue;
10289
10904
  }
10290
10905
  if (entry.isDirectory()) {
10291
- visit(join12(directory, entry.name));
10906
+ visit(join13(directory, entry.name));
10292
10907
  } else if (entry.isFile()) {
10293
10908
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
10294
10909
  if (SCENE_EXTENSIONS.has(extension)) {
@@ -10318,7 +10933,7 @@ function projectFiles(projectPath, extensions) {
10318
10933
  }
10319
10934
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
10320
10935
  if (entry.isDirectory()) {
10321
- visit(join12(directory, entry.name), spelled);
10936
+ visit(join13(directory, entry.name), spelled);
10322
10937
  } else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
10323
10938
  found.push(spelled);
10324
10939
  }
@@ -10339,14 +10954,14 @@ function scriptsWithoutUid(projectPath) {
10339
10954
  }
10340
10955
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
10341
10956
  if (entry.isDirectory()) {
10342
- visit(join12(directory, entry.name), spelled);
10957
+ visit(join13(directory, entry.name), spelled);
10343
10958
  continue;
10344
10959
  }
10345
10960
  if (!entry.isFile()) {
10346
10961
  continue;
10347
10962
  }
10348
10963
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
10349
- if (CARRIES_A_UID.has(extension) && !existsSync12(join12(directory, `${entry.name}.uid`))) {
10964
+ if (CARRIES_A_UID.has(extension) && !existsSync12(join13(directory, `${entry.name}.uid`))) {
10350
10965
  found.push(spelled);
10351
10966
  }
10352
10967
  }
@@ -10367,7 +10982,7 @@ function projectTexts(projectPath) {
10367
10982
  if (skipped(entry.name)) {
10368
10983
  continue;
10369
10984
  }
10370
- const absolute = join12(directory, entry.name);
10985
+ const absolute = join13(directory, entry.name);
10371
10986
  const spelled = `${prefix}${entry.name}`;
10372
10987
  if (entry.isDirectory()) {
10373
10988
  visit(absolute, `${spelled}/`);
@@ -10397,7 +11012,7 @@ function ignoredTexts(projectPath) {
10397
11012
  if (skipped(entry.name)) {
10398
11013
  continue;
10399
11014
  }
10400
- const absolute = join12(directory, entry.name);
11015
+ const absolute = join13(directory, entry.name);
10401
11016
  const spelled = `${prefix}${entry.name}`;
10402
11017
  if (entry.isDirectory()) {
10403
11018
  visit(absolute, `${spelled}/`, under);
@@ -10437,7 +11052,7 @@ function searchProject(projectPath, options) {
10437
11052
  if (skipped(entry.name)) {
10438
11053
  continue;
10439
11054
  }
10440
- const entryPath = join12(directory, entry.name);
11055
+ const entryPath = join13(directory, entry.name);
10441
11056
  if (entry.isDirectory()) {
10442
11057
  visit(entryPath);
10443
11058
  continue;
@@ -10891,7 +11506,7 @@ import {
10891
11506
  statSync as statSync7,
10892
11507
  writeFileSync as writeFileSync6
10893
11508
  } from "node:fs";
10894
- import { dirname as dirname7, join as join13 } from "node:path";
11509
+ import { dirname as dirname7, join as join14 } from "node:path";
10895
11510
  import { fileURLToPath } from "node:url";
10896
11511
  function editorAddonDigest(from = shippedAddonsDirectory()) {
10897
11512
  return addonsDigest(EDITOR_PLUGINS, from);
@@ -10906,7 +11521,7 @@ function addonsDigest(names, from) {
10906
11521
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
10907
11522
  continue;
10908
11523
  }
10909
- const path = join13(directory, name);
11524
+ const path = join14(directory, name);
10910
11525
  const inside = `${relative}/${name}`;
10911
11526
  if (statSync7(path).isDirectory()) {
10912
11527
  walk(path, inside);
@@ -10920,7 +11535,7 @@ function addonsDigest(names, from) {
10920
11535
  }
10921
11536
  };
10922
11537
  for (const name of names) {
10923
- walk(join13(from, name), name);
11538
+ walk(join14(from, name), name);
10924
11539
  }
10925
11540
  return hash.digest("hex");
10926
11541
  }
@@ -10951,7 +11566,7 @@ function installedRuntimeDigest(projectPath) {
10951
11566
  return digestMarker(projectPath, RUNTIME_ADDON);
10952
11567
  }
10953
11568
  function digestMarker(projectPath, addon) {
10954
- const marker = join13(projectPath, "addons", addon, DIGEST_MARKER);
11569
+ const marker = join14(projectPath, "addons", addon, DIGEST_MARKER);
10955
11570
  if (!existsSync13(marker)) {
10956
11571
  return null;
10957
11572
  }
@@ -10959,35 +11574,35 @@ function digestMarker(projectPath, addon) {
10959
11574
  return said === "" ? null : said;
10960
11575
  }
10961
11576
  function shippedAddonsDirectory() {
10962
- return join13(dirname7(fileURLToPath(import.meta.url)), "godot", "addons");
11577
+ return join14(dirname7(fileURLToPath(import.meta.url)), "godot", "addons");
10963
11578
  }
10964
11579
  function shippedOperationsScript() {
10965
- return join13(dirname7(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
11580
+ return join14(dirname7(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
10966
11581
  }
10967
11582
  function installAddons(projectPath, from = shippedAddonsDirectory()) {
10968
11583
  const installed = [];
10969
11584
  for (const name of ADDONS) {
10970
- const source = join13(from, name);
10971
- if (!existsSync13(join13(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
11585
+ const source = join14(from, name);
11586
+ if (!existsSync13(join14(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
10972
11587
  throw new Error(`The package holds no ${name} addon at ${source}.`);
10973
11588
  }
10974
- const target = join13(projectPath, "addons", name);
11589
+ const target = join14(projectPath, "addons", name);
10975
11590
  const replaced = existsSync13(target);
10976
11591
  rmSync5(target, { recursive: true, force: true });
10977
11592
  mkdirSync4(dirname7(target), { recursive: true });
10978
11593
  cpSync(source, target, { recursive: true });
10979
- writeFileSync6(join13(target, VERSION_MARKER), `${SERVER_VERSION}
11594
+ writeFileSync6(join14(target, VERSION_MARKER), `${SERVER_VERSION}
10980
11595
  `);
10981
11596
  installed.push({ name, path: target, replaced });
10982
11597
  }
10983
- writeFileSync6(join13(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
11598
+ writeFileSync6(join14(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
10984
11599
  `);
10985
- writeFileSync6(join13(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
11600
+ writeFileSync6(join14(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
10986
11601
  `);
10987
11602
  return installed;
10988
11603
  }
10989
11604
  function installedAddonVersion(projectPath) {
10990
- const marker = join13(projectPath, "addons", ADDONS[0], VERSION_MARKER);
11605
+ const marker = join14(projectPath, "addons", ADDONS[0], VERSION_MARKER);
10991
11606
  if (!existsSync13(marker)) {
10992
11607
  return null;
10993
11608
  }
@@ -10997,13 +11612,13 @@ function installedAddonVersion(projectPath) {
10997
11612
  function removeAddons(projectPath) {
10998
11613
  const removed = [];
10999
11614
  for (const name of ADDONS) {
11000
- const target = join13(projectPath, "addons", name);
11615
+ const target = join14(projectPath, "addons", name);
11001
11616
  if (existsSync13(target)) {
11002
11617
  rmSync5(target, { recursive: true, force: true });
11003
11618
  removed.push(target);
11004
11619
  }
11005
11620
  }
11006
- const addons = join13(projectPath, "addons");
11621
+ const addons = join14(projectPath, "addons");
11007
11622
  if (removed.length > 0 && existsSync13(addons) && readdirSync8(addons).length === 0) {
11008
11623
  rmdirSync(addons);
11009
11624
  removed.push(addons);
@@ -11038,7 +11653,7 @@ function loaderAmong(projectPath, named) {
11038
11653
  }
11039
11654
  let source = null;
11040
11655
  try {
11041
- source = readFileSync11(join13(projectPath, path), "utf8");
11656
+ source = readFileSync11(join14(projectPath, path), "utf8");
11042
11657
  } catch {}
11043
11658
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
11044
11659
  return { name: entry, path, exists: true };
@@ -11080,8 +11695,8 @@ function enabledPlugins(settings) {
11080
11695
  function inspectProject(projectPath) {
11081
11696
  const problems = [];
11082
11697
  const addons = ADDONS.map((name) => {
11083
- const directory = join13(projectPath, "addons", name);
11084
- const marker = join13(directory, VERSION_MARKER);
11698
+ const directory = join14(projectPath, "addons", name);
11699
+ const marker = join14(directory, VERSION_MARKER);
11085
11700
  const installed = existsSync13(directory);
11086
11701
  const version = existsSync13(marker) ? readFileSync11(marker, "utf8").trim() : null;
11087
11702
  const current = version === SERVER_VERSION;
@@ -11092,7 +11707,7 @@ function inspectProject(projectPath) {
11092
11707
  }
11093
11708
  return { name, installed, version, current };
11094
11709
  });
11095
- const settings = parseProjectGodot(readFileSync11(join13(projectPath, "project.godot"), "utf8"));
11710
+ const settings = parseProjectGodot(readFileSync11(join14(projectPath, "project.godot"), "utf8"));
11096
11711
  const pluginsEnabled = enabledPlugins(settings);
11097
11712
  for (const name of EDITOR_PLUGINS) {
11098
11713
  if (!pluginsEnabled.includes(name)) {
@@ -11108,7 +11723,7 @@ function inspectProject(projectPath) {
11108
11723
  const tracked = trackedByGit(projectPath, [...named.values()]);
11109
11724
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
11110
11725
  for (const [name, path] of named) {
11111
- if (!existsSync13(join13(projectPath, path))) {
11726
+ if (!existsSync13(join14(projectPath, path))) {
11112
11727
  if (!addonMissing(path)) {
11113
11728
  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`);
11114
11729
  }
@@ -11256,7 +11871,7 @@ function annotationsOf(spec) {
11256
11871
  openWorldHint: false
11257
11872
  };
11258
11873
  }
11259
- var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "Through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
11874
+ var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "Through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR2, TOOL_SPECS, SPECS_BY_NAME;
11260
11875
  var init_tool_definitions = __esm(() => {
11261
11876
  init_launch();
11262
11877
  PROJECT_INFO_SECTIONS = [
@@ -11412,7 +12027,7 @@ var init_tool_definitions = __esm(() => {
11412
12027
  additionalProperties: false
11413
12028
  }
11414
12029
  };
11415
- COLOUR = {
12030
+ COLOUR2 = {
11416
12031
  type: "object",
11417
12032
  description: "set_theme_color: r, g and b from 0 to 1, with a optional and opaque by default.",
11418
12033
  properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" }, a: { type: "number" } },
@@ -11424,7 +12039,7 @@ var init_tool_definitions = __esm(() => {
11424
12039
  name: "project_info",
11425
12040
  title: "Project info",
11426
12041
  effect: "read",
11427
- description: "What a project is: its name and main scene from project.godot, the Godot that answers, and how many scenes, scripts and assets it holds, with optional sections on top.",
12042
+ description: "What a project is: its name and main scene from project.godot, the Godot that answers, and how many scenes, scripts and assets it holds, other counting every remaining file, leaving out the addons gdharness installed and any folder holding a .gdignore, with optional sections on top.",
11428
12043
  parameters: {
11429
12044
  projectPath: PROJECT_PATH,
11430
12045
  include: {
@@ -11666,7 +12281,7 @@ var init_tool_definitions = __esm(() => {
11666
12281
  },
11667
12282
  reimport: {
11668
12283
  effect: "destructive",
11669
- 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. The editor scans first, since its reimport passes over a file it has not taken in, such as one an installer has just written; what that scan imported is named under importedByTheScan and is not imported again unless force asks. Answered from a second status read: reimported lists what is current now, notReimported what is not, one entry per status and reason with the paths sharing them, and via says which of the two did the work. A whole-project reimport also answers missingSource, the .import files whose source is not beside them, empty when there are none, whether or not anything needed reimporting; one resourcePath is not a walk of the project and leaves it out. stillImporting is an editor that had not finished within timeoutMs",
12284
+ 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. The editor scans first, since its reimport passes over a file it has not taken in, such as one an installer has just written; what that scan imported is named under importedByTheScan and is not imported again unless force asks. Answered from a second status read: reimported lists what is current now, notReimported what is not, one entry per status and reason with the paths sharing them, and via says which of the two did the work. When nothing needs reimporting, nothing runs, and the answer has the two lists empty, a note saying so, and no via. A whole-project reimport also answers missingSource, the .import files whose source is not beside them, empty when there are none, whether or not anything needed reimporting; one resourcePath is not a walk of the project and leaves it out. stillImporting is an editor that had not finished within timeoutMs",
11670
12285
  requires: []
11671
12286
  },
11672
12287
  uid: {
@@ -11725,7 +12340,8 @@ var init_tool_definitions = __esm(() => {
11725
12340
  name: "project_test",
11726
12341
  title: "Run tests",
11727
12342
  effect: "additive",
11728
- description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
12343
+ description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
12344
+ answer: "time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for.",
11729
12345
  parameters: {
11730
12346
  projectPath: PROJECT_PATH,
11731
12347
  path: {
@@ -11750,7 +12366,7 @@ var init_tool_definitions = __esm(() => {
11750
12366
  timeoutMs: {
11751
12367
  type: "integer",
11752
12368
  minimum: 1,
11753
- description: "How long the run may take before it is killed. Default 600000. A run killed at the limit answers timedOut and silentForMs, how long it had printed nothing; it is called hung only when that was half the limit, or thirty seconds of a longer one, and otherwise it was still running and wants a longer timeoutMs or a narrower path."
12369
+ description: "How long the run may take before it is killed. Default 600000. A run killed at the limit answers timedOut and silentForMs, how long it had printed nothing; it is called hung only when that was half the limit, or thirty seconds of a longer one, and otherwise it was still running and wants a longer timeoutMs or a narrower path, unless it ended repeating one script error, which the verdict names instead. A run whose autoload did not compile is stopped as soon as the engine says so, and names the script that did not under scriptErrors."
11754
12370
  },
11755
12371
  printed: {
11756
12372
  type: "string",
@@ -12210,7 +12826,7 @@ var init_tool_definitions = __esm(() => {
12210
12826
  ops: ["set_theme_color"],
12211
12827
  description: 'set_theme_color: such as "font_color".'
12212
12828
  },
12213
- color: { ...COLOUR, ops: ["set_theme_color"] },
12829
+ color: { ...COLOUR2, ops: ["set_theme_color"] },
12214
12830
  fontSizeName: {
12215
12831
  type: "string",
12216
12832
  ops: ["set_theme_font_size"],
@@ -12421,7 +13037,7 @@ var init_tool_definitions = __esm(() => {
12421
13037
  requires: []
12422
13038
  }
12423
13039
  },
12424
- answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
13040
+ answer: "start: scene is the scene the game started in, the project's main scene when none was given. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
12425
13041
  defaultOperation: "start"
12426
13042
  },
12427
13043
  {
@@ -12589,7 +13205,7 @@ var init_tool_definitions = __esm(() => {
12589
13205
  property: {
12590
13206
  type: "string",
12591
13207
  ops: ["property", "find"],
12592
- description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A 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. 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.'
13208
+ 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. 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.`
12593
13209
  },
12594
13210
  depth: {
12595
13211
  type: "integer",
@@ -12809,7 +13425,7 @@ var init_tool_definitions = __esm(() => {
12809
13425
  name: "runtime_input",
12810
13426
  title: "Send input to the game",
12811
13427
  description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button, mouse motion or drag. All of it works headless, where the viewport is the project's window size and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
12812
- answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed 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. choose: what was chosen, read before the press, which is heard rather than assumed; 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 the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again, for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
13428
+ answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, found.item names the tab, list entry, tree row or menu bar title pressed (kind, index where its control numbers them, says, and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed 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. choose: what was chosen, read before the press, which is heard rather than assumed; 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 the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again, for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
12813
13429
  parameters: {
12814
13430
  projectPath: RUNNING_PROJECT_PATH,
12815
13431
  pid: RUNNING_PID,
@@ -12821,7 +13437,7 @@ var init_tool_definitions = __esm(() => {
12821
13437
  says: {
12822
13438
  type: "string",
12823
13439
  ops: ["click"],
12824
- 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. A match saying the words only as part of more is named under found.partOf, and is refused when another control says exactly the words (hidden, off the screen, covered, or on screen taking clicks without being a button), since it would stand in for that one, and when it lets the pointer through to what is under it; index presses it anyway. Words found only inside longer words (\"out\" in \"Scout\") rank after every other match and are refused unless index is given; words starting a longer word (\"Recruit\" in \"Recruits\") count. An empty field says its placeholder, and a control with no text of its own, a picture on a button, says its accessibility name. An item of an open menu is drawn by the menu rather than held as a control, so a click finds none, and the refusal names the menu and the choose call that picks it."
13440
+ 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. A match saying the words only as part of more is named under found.partOf, and is refused when another control says exactly the words (hidden, off the screen, covered, or on screen taking clicks without being a button), since it would stand in for that one, and when it lets the pointer through to what is under it; index presses it anyway. Words found only inside longer words (\"out\" in \"Scout\") rank after every other match and are refused unless index is given; words starting a longer word (\"Recruit\" in \"Recruits\") count. An empty field says its placeholder, and a control with no text of its own, a picture on a button, says its accessibility name. A tab, a list entry, a tree row and a menu bar title each count as a match of their own and are pressed where they are drawn, after their control scrolls them into view; a disabled one is refused. An item of an open menu is drawn by the menu rather than held as a control, so a click finds none, and the refusal names the menu and the choose call that picks it."
12825
13441
  },
12826
13442
  action: {
12827
13443
  type: "string",
@@ -34899,11 +35515,50 @@ var init_stdio = __esm(() => {
34899
35515
  swallowsErrorsAfterClose = Symbol("swallowsErrorsAfterClose");
34900
35516
  });
34901
35517
 
35518
+ // src/autoload-failure.ts
35519
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
35520
+ import { join as join17 } from "node:path";
35521
+ function scriptsAutoloaded(projectPath) {
35522
+ const scripts = new Map;
35523
+ let settings;
35524
+ try {
35525
+ settings = parseProjectGodot(readFileSync13(join17(projectPath, "project.godot"), "utf8"));
35526
+ } catch {
35527
+ return scripts;
35528
+ }
35529
+ for (const [name, path] of autoloadPaths(settings)) {
35530
+ if (!path.endsWith(".tscn")) {
35531
+ scripts.set(`res://${path}`, name);
35532
+ continue;
35533
+ }
35534
+ const scene = join17(projectPath, path);
35535
+ if (!existsSync15(scene)) {
35536
+ continue;
35537
+ }
35538
+ try {
35539
+ for (const named of readFileSync13(scene, "utf8").matchAll(/^\[ext_resource\b[^\]]*\btype="Script"[^\]]*\bpath="(res:\/\/[^"]+)"/gm)) {
35540
+ scripts.set(named[1] ?? "", name);
35541
+ }
35542
+ } catch {}
35543
+ }
35544
+ return scripts;
35545
+ }
35546
+ function autoloadNotCompiled(entry, autoloaded) {
35547
+ const path = notCompiledIn(entry);
35548
+ const name = path === undefined ? undefined : autoloaded.get(path);
35549
+ return path === undefined || name === undefined ? null : { name, path };
35550
+ }
35551
+ var init_autoload_failure = __esm(() => {
35552
+ init_resources();
35553
+ init_script_origins();
35554
+ init_setup();
35555
+ });
35556
+
34902
35557
  // src/runtime-client.ts
34903
- import { existsSync as existsSync15, readdirSync as readdirSync10, readFileSync as readFileSync13, statSync as statSync8, unlinkSync as unlinkSync2 } from "node:fs";
35558
+ import { existsSync as existsSync16, readdirSync as readdirSync10, readFileSync as readFileSync14, statSync as statSync8, unlinkSync as unlinkSync2 } from "node:fs";
34904
35559
  import { createConnection } from "node:net";
34905
35560
  import { tmpdir as tmpdir4 } from "node:os";
34906
- import { dirname as dirname10, join as join16, resolve as resolve4 } from "node:path";
35561
+ import { dirname as dirname10, join as join18, resolve as resolve4 } from "node:path";
34907
35562
  import { setTimeout as delay } from "node:timers/promises";
34908
35563
  function runtimeDirectory(variables = process.env) {
34909
35564
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
@@ -34911,20 +35566,20 @@ function runtimeDirectory(variables = process.env) {
34911
35566
  return explicit;
34912
35567
  }
34913
35568
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
34914
- return join16(perUser ?? tmpdir4(), "gdharness");
35569
+ return join18(perUser ?? tmpdir4(), "gdharness");
34915
35570
  }
34916
35571
  function runtimeDirectories(variables = process.env) {
34917
35572
  const candidates = [runtimeDirectory(variables)];
34918
35573
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
34919
35574
  for (const base of fallbacks) {
34920
- candidates.push(join16(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
35575
+ candidates.push(join18(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
34921
35576
  }
34922
35577
  return [...new Set(candidates.map((path) => resolve4(path)))];
34923
35578
  }
34924
35579
  function errorReportOf(pid, directories = runtimeDirectories()) {
34925
35580
  for (const directory of directories) {
34926
- const path = join16(directory, `runtime-${pid}.log`);
34927
- if (existsSync15(path)) {
35581
+ const path = join18(directory, `runtime-${pid}.log`);
35582
+ if (existsSync16(path)) {
34928
35583
  return path;
34929
35584
  }
34930
35585
  }
@@ -34955,7 +35610,7 @@ function reportAlreadyInTranscript(transcript, report) {
34955
35610
  function parseAnnouncement(file, pid) {
34956
35611
  let fields;
34957
35612
  try {
34958
- fields = asParams(JSON.parse(readFileSync13(file, "utf8")));
35613
+ fields = asParams(JSON.parse(readFileSync14(file, "utf8")));
34959
35614
  } catch {
34960
35615
  return { kind: "rubbish" };
34961
35616
  }
@@ -35021,7 +35676,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
35021
35676
  function wentAway(file, pid) {
35022
35677
  let project = "";
35023
35678
  try {
35024
- const fields = asParams(JSON.parse(readFileSync13(file, "utf8")));
35679
+ const fields = asParams(JSON.parse(readFileSync14(file, "utf8")));
35025
35680
  project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
35026
35681
  } catch {
35027
35682
  return;
@@ -35064,7 +35719,7 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
35064
35719
  }).map((one) => one.file));
35065
35720
  }
35066
35721
  function announcedIn(directory) {
35067
- if (!existsSync15(directory)) {
35722
+ if (!existsSync16(directory)) {
35068
35723
  return [];
35069
35724
  }
35070
35725
  const found = [];
@@ -35073,14 +35728,14 @@ function announcedIn(directory) {
35073
35728
  for (const entry of readdirSync10(directory)) {
35074
35729
  const report = ERROR_REPORT_PATTERN.exec(entry);
35075
35730
  if (report) {
35076
- sweepErrorReport(join16(directory, entry), Number.parseInt(report[1] ?? "", 10));
35731
+ sweepErrorReport(join18(directory, entry), Number.parseInt(report[1] ?? "", 10));
35077
35732
  continue;
35078
35733
  }
35079
35734
  const match = ANNOUNCEMENT_PATTERN.exec(entry);
35080
35735
  if (!match) {
35081
35736
  continue;
35082
35737
  }
35083
- const file = join16(directory, entry);
35738
+ const file = join18(directory, entry);
35084
35739
  const pid = Number.parseInt(match[1] ?? "", 10);
35085
35740
  const running = alive(pid);
35086
35741
  entries.push({ file, pid, running });
@@ -35108,7 +35763,7 @@ function announcedIn(directory) {
35108
35763
  found.push(announced);
35109
35764
  }
35110
35765
  const held = new Set(entries.map((one) => one.file));
35111
- const here = join16(directory, ".");
35766
+ const here = join18(directory, ".");
35112
35767
  for (const file of verdicts.keys()) {
35113
35768
  if (dirname10(file) === here && !held.has(file)) {
35114
35769
  verdicts.delete(file);
@@ -35117,7 +35772,7 @@ function announcedIn(directory) {
35117
35772
  return found;
35118
35773
  }
35119
35774
  async function announcementEnded(pid, directories = runtimeDirectories()) {
35120
- const files = directories.map((directory) => join16(directory, `runtime-${pid}.json`)).filter((file) => existsSync15(file));
35775
+ const files = directories.map((directory) => join18(directory, `runtime-${pid}.json`)).filter((file) => existsSync16(file));
35121
35776
  const first = files[0];
35122
35777
  if (first === undefined) {
35123
35778
  return;
@@ -35281,7 +35936,36 @@ function keepLate(id, answer) {
35281
35936
  function lateAnswer(id) {
35282
35937
  return lateAnswers.get(id);
35283
35938
  }
35284
- function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = false) {
35939
+ function placeOf(endpoint) {
35940
+ return `${dialAddress(endpoint.address)}:${endpoint.port}`;
35941
+ }
35942
+ function waitForAPlace(game, granted) {
35943
+ const line = untaken.get(game) ?? { held: 0, waiting: [] };
35944
+ untaken.set(game, line);
35945
+ const grant = () => {
35946
+ line.held += 1;
35947
+ let given = false;
35948
+ granted(() => {
35949
+ if (given) {
35950
+ return;
35951
+ }
35952
+ given = true;
35953
+ line.held -= 1;
35954
+ const next = line.waiting.shift();
35955
+ if (next !== undefined) {
35956
+ next();
35957
+ } else if (line.held === 0 && untaken.get(game) === line) {
35958
+ untaken.delete(game);
35959
+ }
35960
+ });
35961
+ };
35962
+ if (line.held < UNTAKEN_PER_GAME) {
35963
+ grant();
35964
+ } else {
35965
+ line.waiting.push(grant);
35966
+ }
35967
+ }
35968
+ function runtimeRequest(endpoint, command, params, timeoutMs, afterTheWait = "drop") {
35285
35969
  const id = nextRequestId++;
35286
35970
  const sentAt = Date.now();
35287
35971
  return new Promise((settle) => {
@@ -35289,16 +35973,32 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
35289
35973
  let late = false;
35290
35974
  let buffered = "";
35291
35975
  let welcomed = false;
35292
- const socket = createConnection({ port: endpoint.port, host: dialAddress(endpoint.address) });
35976
+ let socket = null;
35977
+ let refusals = 0;
35978
+ let redial = null;
35979
+ let giveBackThePlace = null;
35980
+ const taken = () => {
35981
+ giveBackThePlace?.();
35982
+ giveBackThePlace = null;
35983
+ };
35293
35984
  let lateTimer = null;
35294
35985
  const finish = (reply) => {
35986
+ taken();
35987
+ if (redial !== null) {
35988
+ clearTimeout(redial);
35989
+ redial = null;
35990
+ }
35295
35991
  if (late) {
35296
35992
  if (lateTimer !== null) {
35297
35993
  clearTimeout(lateTimer);
35298
35994
  }
35299
- socket.destroy();
35300
- keepLate(id, reply.ok || reply.reason === "error" ? { state: "answered", command, sentAt, answeredAt: Date.now(), reply } : { state: "never", command, sentAt, why: reply.message });
35995
+ socket?.destroy();
35301
35996
  late = false;
35997
+ if (typeof afterTheWait === "function") {
35998
+ afterTheWait(reply);
35999
+ return;
36000
+ }
36001
+ keepLate(id, reply.ok || reply.reason === "error" ? { state: "answered", command, sentAt, answeredAt: Date.now(), reply } : { state: "never", command, sentAt, why: reply.message });
35302
36002
  return;
35303
36003
  }
35304
36004
  if (done) {
@@ -35306,12 +36006,24 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
35306
36006
  }
35307
36007
  done = true;
35308
36008
  clearTimeout(timer);
35309
- socket.destroy();
36009
+ socket?.destroy();
35310
36010
  settle(reply);
35311
36011
  };
35312
36012
  const timer = setTimeout(() => {
36013
+ if (socket === null) {
36014
+ finish(refusals > 0 ? {
36015
+ ok: false,
36016
+ reason: "refused",
36017
+ message: `The game (${describe2(endpoint)}) is running but nothing answered on its port: it refused ${refusals === 1 ? "the connection" : `all ${refusals} connections tried`} within ${timeoutMs}ms. Nothing listens there while a game is starting or shutting down, and the engine refuses connections while more are waiting for the game than the eight it queues, which several servers calling one game at once, or a long frame or a breakpoint holding it, can make it do. Nothing was sent, so nothing ran.`
36018
+ } : {
36019
+ ok: false,
36020
+ reason: "busy",
36021
+ message: `'${command}' was not sent to the game (${describe2(endpoint)}) within ${timeoutMs}ms: ${UNTAKEN_PER_GAME} connections from this server were already waiting for the game to take them, which it does between frames, so a long frame or a breakpoint holds them. Nothing was sent, so nothing ran.`
36022
+ });
36023
+ return;
36024
+ }
35313
36025
  const still = welcomed ? `The game (${describe2(endpoint)}) has the request and did not answer '${command}' within ${timeoutMs}ms.` : `The game (${describe2(endpoint)}) did not answer '${command}' within ${timeoutMs}ms and had not taken the request: its frame has not ended since it was sent, which a long frame or a breakpoint does, and the request runs when it does.`;
35314
- if (!keepListening) {
36026
+ if (afterTheWait === "drop") {
35315
36027
  finish({
35316
36028
  ok: false,
35317
36029
  reason: "busy",
@@ -35321,7 +36033,9 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
35321
36033
  }
35322
36034
  done = true;
35323
36035
  late = true;
35324
- keepLate(id, { state: "waiting", command, sentAt });
36036
+ if (afterTheWait === "keep") {
36037
+ keepLate(id, { state: "waiting", command, sentAt });
36038
+ }
35325
36039
  lateTimer = setTimeout(() => {
35326
36040
  finish({
35327
36041
  ok: false,
@@ -35331,6 +36045,15 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
35331
36045
  }, LATE_ANSWER_MS);
35332
36046
  lateTimer.unref();
35333
36047
  socket.unref();
36048
+ if (afterTheWait !== "keep") {
36049
+ settle({
36050
+ ok: false,
36051
+ reason: "busy",
36052
+ stillWithTheGame: true,
36053
+ message: `${still} Nothing was cancelled: the game runs it when it gets to it, and what it answers then is not kept, so ask again once the game answers. A game held at a breakpoint instead is one debug_state stack answers about.`
36054
+ });
36055
+ return;
36056
+ }
35334
36057
  settle({
35335
36058
  ok: false,
35336
36059
  reason: "busy",
@@ -35338,71 +36061,112 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
35338
36061
  message: `${still} Nothing was cancelled: a call that takes longer than the wait goes on and does everything it was asked, so read the state back rather than repeating it. runtime_invoke op result with requestId ${id} answers with its reply once it comes. A game held at a breakpoint instead is one debug_state stack answers about.`
35339
36062
  });
35340
36063
  }, timeoutMs);
35341
- socket.setEncoding("utf8");
35342
- socket.on("connect", () => {
35343
- socket.write(`${JSON.stringify({ id, command, params })}
36064
+ const open = () => {
36065
+ const opened = createConnection({ port: endpoint.port, host: dialAddress(endpoint.address) });
36066
+ socket = opened;
36067
+ opened.setEncoding("utf8");
36068
+ opened.on("connect", () => {
36069
+ opened.write(`${JSON.stringify({ id, command, params })}
35344
36070
  `);
35345
- });
35346
- socket.on("data", (chunk) => {
35347
- buffered += chunk;
35348
- let newline = buffered.indexOf(`
36071
+ });
36072
+ opened.on("data", (chunk) => {
36073
+ buffered += chunk;
36074
+ let newline = buffered.indexOf(`
35349
36075
  `);
35350
- while (newline !== -1 && (!done || late)) {
35351
- const line = buffered.slice(0, newline).trim();
35352
- buffered = buffered.slice(newline + 1);
35353
- newline = buffered.indexOf(`
36076
+ while (newline !== -1 && (!done || late)) {
36077
+ const line = buffered.slice(0, newline).trim();
36078
+ buffered = buffered.slice(newline + 1);
36079
+ newline = buffered.indexOf(`
35354
36080
  `);
35355
- if (line === "") {
35356
- continue;
35357
- }
35358
- let message;
35359
- try {
35360
- message = asParams(JSON.parse(line));
35361
- } catch {
35362
- continue;
35363
- }
35364
- if (!welcomed) {
35365
- welcomed = true;
35366
- if (readString(message, "type") === "welcome") {
35367
- const spoken = readNumber(message, "protocol");
35368
- if (spoken !== RUNTIME_PROTOCOL) {
35369
- finish({
35370
- ok: false,
35371
- reason: "protocol",
35372
- message: `The game (${describe2(endpoint)}) speaks runtime protocol ${spoken ?? "unknown"} and this server speaks ${RUNTIME_PROTOCOL}. Reinstall the runtime addon into the project.`
35373
- });
35374
- return;
36081
+ if (line === "") {
36082
+ continue;
36083
+ }
36084
+ let message;
36085
+ try {
36086
+ message = asParams(JSON.parse(line));
36087
+ } catch {
36088
+ continue;
36089
+ }
36090
+ if (!welcomed) {
36091
+ welcomed = true;
36092
+ taken();
36093
+ if (readString(message, "type") === "welcome") {
36094
+ const spoken = readNumber(message, "protocol");
36095
+ if (spoken !== RUNTIME_PROTOCOL) {
36096
+ finish({
36097
+ ok: false,
36098
+ reason: "protocol",
36099
+ message: `The game (${describe2(endpoint)}) speaks runtime protocol ${spoken ?? "unknown"} and this server speaks ${RUNTIME_PROTOCOL}. Reinstall the runtime addon into the project.`
36100
+ });
36101
+ return;
36102
+ }
36103
+ continue;
35375
36104
  }
36105
+ }
36106
+ if (message["id"] !== id) {
35376
36107
  continue;
35377
36108
  }
36109
+ if (readString(message, "type") === "error") {
36110
+ finish({
36111
+ ok: false,
36112
+ reason: "error",
36113
+ message: readString(message, "message") ?? "unknown error"
36114
+ });
36115
+ return;
36116
+ }
36117
+ finish({ ok: true, payload: message, endpoint });
35378
36118
  }
35379
- if (message["id"] !== id) {
35380
- continue;
36119
+ });
36120
+ opened.on("error", (error) => {
36121
+ if (socket !== opened) {
36122
+ return;
35381
36123
  }
35382
- if (readString(message, "type") === "error") {
35383
- finish({ ok: false, reason: "error", message: readString(message, "message") ?? "unknown error" });
36124
+ if (error.code === "ECONNREFUSED" && !done) {
36125
+ if (alive(endpoint.pid)) {
36126
+ refusals += 1;
36127
+ socket = null;
36128
+ opened.destroy();
36129
+ redial = setTimeout(() => {
36130
+ redial = null;
36131
+ open();
36132
+ }, REDIAL_MS);
36133
+ return;
36134
+ }
36135
+ finish({
36136
+ ok: false,
36137
+ reason: "refused",
36138
+ message: `The game (${describe2(endpoint)}) has ended: its port refused the connection and its process is gone.`
36139
+ });
35384
36140
  return;
35385
36141
  }
35386
- finish({ ok: true, payload: message, endpoint });
35387
- }
35388
- });
35389
- socket.on("error", (error) => {
35390
- finish({
35391
- ok: false,
35392
- reason: "refused",
35393
- message: `The game (${describe2(endpoint)}) is running but nothing answered on its port: ${error.code ?? error.message}. It may still be starting, or be shutting down.`
36142
+ finish({
36143
+ ok: false,
36144
+ reason: "refused",
36145
+ message: `The game (${describe2(endpoint)}) is running but nothing answered on its port: ${error.code ?? error.message}. It may still be starting or be shutting down.`
36146
+ });
35394
36147
  });
35395
- });
35396
- socket.on("close", () => {
35397
- finish({
35398
- ok: false,
35399
- reason: "refused",
35400
- message: `The game (${describe2(endpoint)}) closed the connection before answering '${command}'.`
36148
+ opened.on("close", () => {
36149
+ if (socket !== opened) {
36150
+ return;
36151
+ }
36152
+ finish({
36153
+ ok: false,
36154
+ reason: "refused",
36155
+ message: `The game (${describe2(endpoint)}) closed the connection before answering '${command}'.`
36156
+ });
35401
36157
  });
36158
+ };
36159
+ waitForAPlace(placeOf(endpoint), (giveBack) => {
36160
+ if (done) {
36161
+ giveBack();
36162
+ return;
36163
+ }
36164
+ giveBackThePlace = giveBack;
36165
+ open();
35402
36166
  });
35403
36167
  });
35404
36168
  }
35405
- var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, CONSOLE_BOUNDARY = "[gdharness] runtime listening on ", ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, judging = null, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1, LATE_ANSWER_MS, LATE_KEPT = 50, lateAnswers;
36169
+ var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, CONSOLE_BOUNDARY = "[gdharness] runtime listening on ", ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, judging = null, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1, LATE_ANSWER_MS, LATE_KEPT = 50, lateAnswers, UNTAKEN_PER_GAME = 4, REDIAL_MS = 50, untaken;
35406
36170
  var init_runtime_client = __esm(() => {
35407
36171
  init_alive();
35408
36172
  init_launch();
@@ -35415,17 +36179,18 @@ var init_runtime_client = __esm(() => {
35415
36179
  verdicts = new Map;
35416
36180
  LATE_ANSWER_MS = 30 * 60000;
35417
36181
  lateAnswers = new Map;
36182
+ untaken = new Map;
35418
36183
  });
35419
36184
 
35420
36185
  // src/boot-note.ts
35421
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "node:fs";
35422
- import { dirname as dirname11, join as join17 } from "node:path";
36186
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
36187
+ import { dirname as dirname11, join as join19 } from "node:path";
35423
36188
  function bootNotePath(projectPath) {
35424
- return join17(projectPath, ".godot", "gdharness-boot.json");
36189
+ return join19(projectPath, ".godot", "gdharness-boot.json");
35425
36190
  }
35426
36191
  function readScenes(projectPath) {
35427
36192
  try {
35428
- return asParams(asParams(JSON.parse(readFileSync14(bootNotePath(projectPath), "utf8")))["scenes"]);
36193
+ return asParams(asParams(JSON.parse(readFileSync15(bootNotePath(projectPath), "utf8")))["scenes"]);
35429
36194
  } catch {
35430
36195
  return {};
35431
36196
  }
@@ -35467,10 +36232,10 @@ var init_boot_note = __esm(() => {
35467
36232
  });
35468
36233
 
35469
36234
  // src/breakpoint-note.ts
35470
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync15, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
35471
- import { dirname as dirname12, isAbsolute as isAbsolute4, join as join18, relative as relative3 } from "node:path";
36235
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
36236
+ import { dirname as dirname12, isAbsolute as isAbsolute4, join as join20, relative as relative3 } from "node:path";
35472
36237
  function breakpointNotePath(projectPath) {
35473
- return join18(projectPath, ".godot", "gdharness-breakpoints.json");
36238
+ return join20(projectPath, ".godot", "gdharness-breakpoints.json");
35474
36239
  }
35475
36240
  function writeBreakpointNote(projectPath, held) {
35476
36241
  const path = breakpointNotePath(projectPath);
@@ -35496,7 +36261,7 @@ function writeBreakpointNote(projectPath, held) {
35496
36261
  }
35497
36262
  function readBreakpointNote(projectPath) {
35498
36263
  try {
35499
- const parsed = JSON.parse(readFileSync15(breakpointNotePath(projectPath), "utf8"));
36264
+ const parsed = JSON.parse(readFileSync16(breakpointNotePath(projectPath), "utf8"));
35500
36265
  if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
35501
36266
  return [];
35502
36267
  }
@@ -35511,7 +36276,7 @@ function readBreakpointNote(projectPath) {
35511
36276
  }
35512
36277
  const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
35513
36278
  if (lines.length > 0) {
35514
- held.push({ scriptPath: realPathOr(join18(projectPath, script)), lines });
36279
+ held.push({ scriptPath: realPathOr(join20(projectPath, script)), lines });
35515
36280
  }
35516
36281
  }
35517
36282
  return held;
@@ -35560,14 +36325,14 @@ var init_whole_file = __esm(() => {
35560
36325
  });
35561
36326
 
35562
36327
  // src/class-note.ts
35563
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync16 } from "node:fs";
35564
- import { dirname as dirname13, join as join19 } from "node:path";
36328
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync17 } from "node:fs";
36329
+ import { dirname as dirname13, join as join21 } from "node:path";
35565
36330
  function classNotePath(projectPath) {
35566
- return join19(projectPath, ".godot", "gdharness-classes.json");
36331
+ return join21(projectPath, ".godot", "gdharness-classes.json");
35567
36332
  }
35568
36333
  function readClassNote(projectPath) {
35569
36334
  try {
35570
- const fields = asParams(JSON.parse(readFileSync16(classNotePath(projectPath), "utf8")));
36335
+ const fields = asParams(JSON.parse(readFileSync17(classNotePath(projectPath), "utf8")));
35571
36336
  const rebuiltWith = readArray(fields, "rebuiltWith");
35572
36337
  if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
35573
36338
  return null;
@@ -35609,16 +36374,16 @@ var init_class_note = __esm(() => {
35609
36374
  });
35610
36375
 
35611
36376
  // src/config-pin.ts
35612
- import { existsSync as existsSync16, readFileSync as readFileSync17 } from "node:fs";
35613
- import { join as join20 } from "node:path";
36377
+ import { existsSync as existsSync17, readFileSync as readFileSync18 } from "node:fs";
36378
+ import { join as join22 } from "node:path";
35614
36379
  function versionsNamedInConfig(projectPath) {
35615
- const config = join20(projectPath, ".mcp.json");
35616
- if (!existsSync16(config)) {
36380
+ const config = join22(projectPath, ".mcp.json");
36381
+ if (!existsSync17(config)) {
35617
36382
  return [];
35618
36383
  }
35619
36384
  let text;
35620
36385
  try {
35621
- text = readFileSync17(config, "utf8");
36386
+ text = readFileSync18(config, "utf8");
35622
36387
  JSON.parse(text);
35623
36388
  } catch {
35624
36389
  return [];
@@ -35639,10 +36404,10 @@ var init_config_pin = __esm(() => {
35639
36404
  });
35640
36405
 
35641
36406
  // src/cut-test-run.ts
35642
- import { existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync18, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "node:fs";
35643
- import { dirname as dirname14, join as join21 } from "node:path";
36407
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync19, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "node:fs";
36408
+ import { dirname as dirname14, join as join23 } from "node:path";
35644
36409
  function keptAt(projectPath) {
35645
- return join21(projectPath, ".godot", "gdharness-reports", "cut-run.json");
36410
+ return join23(projectPath, ".godot", "gdharness-reports", "cut-run.json");
35646
36411
  }
35647
36412
  function keepCutTestRun(projectPath, run) {
35648
36413
  const path = keptAt(projectPath);
@@ -35654,11 +36419,11 @@ function keepCutTestRun(projectPath, run) {
35654
36419
  }
35655
36420
  function takeCutTestRun(projectPath) {
35656
36421
  const path = keptAt(projectPath);
35657
- if (!existsSync17(path)) {
36422
+ if (!existsSync18(path)) {
35658
36423
  return null;
35659
36424
  }
35660
36425
  try {
35661
- const run = JSON.parse(readFileSync18(path, "utf8"));
36426
+ const run = JSON.parse(readFileSync19(path, "utf8"));
35662
36427
  rmSync9(path, { force: true });
35663
36428
  return typeof run.cutAt === "string" && Array.isArray(run.paths) && run.paths.every((path) => typeof path === "string") ? run : null;
35664
36429
  } catch {
@@ -36471,27 +37236,27 @@ var init_dap_client = __esm(() => {
36471
37236
  import { createHash as createHash4 } from "node:crypto";
36472
37237
  import {
36473
37238
  closeSync,
36474
- existsSync as existsSync18,
37239
+ existsSync as existsSync19,
36475
37240
  mkdtempSync as mkdtempSync2,
36476
37241
  openSync,
36477
- readFileSync as readFileSync19,
37242
+ readFileSync as readFileSync20,
36478
37243
  rmSync as rmSync10,
36479
37244
  statSync as statSync9,
36480
37245
  writeFileSync as writeFileSync12,
36481
37246
  writeSync
36482
37247
  } from "node:fs";
36483
37248
  import { tmpdir as tmpdir5 } from "node:os";
36484
- import { join as join22 } from "node:path";
37249
+ import { join as join24 } from "node:path";
36485
37250
  import { setTimeout as delay3 } from "node:timers/promises";
36486
37251
  function helperAssembly(directory = tmpdir5()) {
36487
- return join22(directory, `gdharness-desktop-${DIGEST}.dll`);
37252
+ return join24(directory, `gdharness-desktop-${DIGEST}.dll`);
36488
37253
  }
36489
37254
  async function ensureHelperCompiled(directory = tmpdir5(), budgetMs = HELPER_COMPILE_MS, wait = true) {
36490
37255
  const assembly = helperAssembly(directory);
36491
37256
  const lock = `${assembly}.compiling`;
36492
37257
  const until = Date.now() + budgetMs;
36493
37258
  while (Date.now() < until) {
36494
- if (existsSync18(assembly)) {
37259
+ if (existsSync19(assembly)) {
36495
37260
  return null;
36496
37261
  }
36497
37262
  let mine = false;
@@ -36509,7 +37274,7 @@ async function ensureHelperCompiled(directory = tmpdir5(), budgetMs = HELPER_COM
36509
37274
  }
36510
37275
  }
36511
37276
  try {
36512
- const holder = Number(readFileSync19(lock, "utf8").trim());
37277
+ const holder = Number(readFileSync20(lock, "utf8").trim());
36513
37278
  const old = Date.now() - statSync9(lock).mtimeMs > budgetMs;
36514
37279
  if (old || Number.isInteger(holder) && holder > 0 && !alive(holder)) {
36515
37280
  rmSync10(lock, { force: true });
@@ -36524,8 +37289,8 @@ async function ensureHelperCompiled(directory = tmpdir5(), budgetMs = HELPER_COM
36524
37289
  return `the desktop helper was not compiled within ${String(budgetMs)} ms`;
36525
37290
  }
36526
37291
  async function compileHelper(directory, assembly, budgetMs) {
36527
- const scratch = mkdtempSync2(join22(directory, "gdharness-desktop-source-"));
36528
- const source = join22(scratch, "helper.cs");
37292
+ const scratch = mkdtempSync2(join24(directory, "gdharness-desktop-source-"));
37293
+ const source = join24(scratch, "helper.cs");
36529
37294
  writeFileSync12(source, SOURCE, "utf8");
36530
37295
  const literal = (path) => `'${path.replaceAll("'", "''")}'`;
36531
37296
  const script = [
@@ -36563,7 +37328,7 @@ async function compileHelper(directory, assembly, budgetMs) {
36563
37328
  });
36564
37329
  compiler.once("close", (code) => {
36565
37330
  clearTimeout(timer);
36566
- resolve(existsSync18(assembly) ? null : `Windows PowerShell could not compile it${code === null ? " in time" : ""}: ${complained.trim() || `exit code ${String(code)}`}`);
37331
+ resolve(existsSync19(assembly) ? null : `Windows PowerShell could not compile it${code === null ? " in time" : ""}: ${complained.trim() || `exit code ${String(code)}`}`);
36567
37332
  });
36568
37333
  });
36569
37334
  } finally {
@@ -36730,25 +37495,25 @@ var init_desktop = __esm(() => {
36730
37495
  // src/editor-log.ts
36731
37496
  import {
36732
37497
  closeSync as closeSync2,
36733
- existsSync as existsSync19,
37498
+ existsSync as existsSync20,
36734
37499
  mkdirSync as mkdirSync10,
36735
37500
  openSync as openSync2,
36736
- readFileSync as readFileSync20,
37501
+ readFileSync as readFileSync21,
36737
37502
  readSync,
36738
37503
  rmSync as rmSync11,
36739
37504
  statSync as statSync10,
36740
37505
  writeFileSync as writeFileSync13
36741
37506
  } from "node:fs";
36742
- import { dirname as dirname15, join as join23 } from "node:path";
37507
+ import { dirname as dirname15, join as join25 } from "node:path";
36743
37508
  function editorLogPath(projectPath) {
36744
- return join23(projectPath, ".godot", "gdharness-editor.log");
37509
+ return join25(projectPath, ".godot", "gdharness-editor.log");
36745
37510
  }
36746
37511
  function editorLogNotePath(projectPath) {
36747
- return join23(projectPath, ".godot", "gdharness-editor.json");
37512
+ return join25(projectPath, ".godot", "gdharness-editor.json");
36748
37513
  }
36749
37514
  function readEditorLogNote(projectPath) {
36750
37515
  try {
36751
- const fields = asParams(JSON.parse(readFileSync20(editorLogNotePath(projectPath), "utf8")));
37516
+ const fields = asParams(JSON.parse(readFileSync21(editorLogNotePath(projectPath), "utf8")));
36752
37517
  const pid = readNumber(fields, "pid");
36753
37518
  const startedAt = readString(fields, "startedAt");
36754
37519
  return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
@@ -36789,7 +37554,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
36789
37554
  return { kind: "older server", notedPid: noted?.pid ?? null };
36790
37555
  }
36791
37556
  const path = editorLogPath(projectPath);
36792
- if (!existsSync19(path)) {
37557
+ if (!existsSync20(path)) {
36793
37558
  return { kind: "not written" };
36794
37559
  }
36795
37560
  const log = new GameLog;
@@ -36847,7 +37612,7 @@ function foldProgress(text) {
36847
37612
  function theFirstOf(path) {
36848
37613
  const whole = statSync10(path).size;
36849
37614
  if (whole <= MOST_CONSOLE_BYTES) {
36850
- return { text: readFileSync20(path, "utf8") };
37615
+ return { text: readFileSync21(path, "utf8") };
36851
37616
  }
36852
37617
  const handle = openSync2(path, "r");
36853
37618
  try {
@@ -37008,7 +37773,7 @@ var init_editor_log = __esm(() => {
37008
37773
  });
37009
37774
 
37010
37775
  // src/rename.ts
37011
- import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync21, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync14 } from "node:fs";
37776
+ import { existsSync as existsSync21, mkdirSync as mkdirSync11, readFileSync as readFileSync22, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync14 } from "node:fs";
37012
37777
  import { dirname as dirname16, posix as posix2 } from "node:path";
37013
37778
 
37014
37779
  class Script {
@@ -37713,11 +38478,11 @@ function rewritesOf(plan, texts, move) {
37713
38478
  }
37714
38479
  function applyRewrites(rewrites) {
37715
38480
  for (const rewrite of rewrites) {
37716
- const now = readFileSync21(rewrite.absolute, "utf8");
38481
+ const now = readFileSync22(rewrite.absolute, "utf8");
37717
38482
  if (now !== rewrite.before) {
37718
38483
  throw new Error(`${rewrite.path} changed on disk while the rename was being worked out; nothing was written`);
37719
38484
  }
37720
- if (rewrite.writeTo !== rewrite.absolute && existsSync20(rewrite.writeTo)) {
38485
+ if (rewrite.writeTo !== rewrite.absolute && existsSync21(rewrite.writeTo)) {
37721
38486
  throw new Error(`${rewrite.writeTo} already exists; nothing was written`);
37722
38487
  }
37723
38488
  }
@@ -37731,7 +38496,7 @@ function applyRewrites(rewrites) {
37731
38496
  let movedUid = false;
37732
38497
  if (rewrite.writeTo !== rewrite.absolute) {
37733
38498
  unlinkSync3(rewrite.absolute);
37734
- if (existsSync20(`${rewrite.absolute}.uid`)) {
38499
+ if (existsSync21(`${rewrite.absolute}.uid`)) {
37735
38500
  renameSync3(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
37736
38501
  movedUid = true;
37737
38502
  }
@@ -37777,10 +38542,10 @@ var init_rename = __esm(() => {
37777
38542
  });
37778
38543
 
37779
38544
  // src/lsp_client.ts
37780
- import { existsSync as existsSync21, realpathSync as realpathSync4 } from "node:fs";
38545
+ import { existsSync as existsSync22, realpathSync as realpathSync4 } from "node:fs";
37781
38546
  import { readFile, realpath } from "node:fs/promises";
37782
38547
  import { createConnection as createConnection3 } from "node:net";
37783
- import { dirname as dirname17, join as join24, resolve as resolve5 } from "node:path";
38548
+ import { dirname as dirname17, join as join26, resolve as resolve5 } from "node:path";
37784
38549
  import { setTimeout as delay4 } from "node:timers/promises";
37785
38550
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
37786
38551
  function initializeTimeoutMs() {
@@ -38458,7 +39223,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
38458
39223
  } catch {
38459
39224
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
38460
39225
  }
38461
- if (!existsSync21(join24(projectPath, "project.godot"))) {
39226
+ if (!existsSync22(join26(projectPath, "project.godot"))) {
38462
39227
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
38463
39228
  }
38464
39229
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -38618,8 +39383,8 @@ var init_lsp_client = __esm(() => {
38618
39383
  });
38619
39384
 
38620
39385
  // src/rename-symbol.ts
38621
- import { existsSync as existsSync22, mkdirSync as mkdirSync12, readdirSync as readdirSync11, realpathSync as realpathSync5, rmSync as rmSync12, writeFileSync as writeFileSync15 } from "node:fs";
38622
- import { basename as basename7, dirname as dirname18, join as join25, relative as relative4, resolve as resolve6 } from "node:path";
39386
+ import { existsSync as existsSync23, mkdirSync as mkdirSync12, readdirSync as readdirSync11, realpathSync as realpathSync5, rmSync as rmSync12, writeFileSync as writeFileSync15 } from "node:fs";
39387
+ import { basename as basename7, dirname as dirname18, join as join27, relative as relative4, resolve as resolve6 } from "node:path";
38623
39388
  function resourcePathOf(projectPath, absolute) {
38624
39389
  const inside = relative4(onDiskSpelling(projectPath), onDiskSpelling(absolute));
38625
39390
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
@@ -38632,7 +39397,7 @@ function onDiskSpelling(path) {
38632
39397
  let at = resolve6(path);
38633
39398
  for (;; ) {
38634
39399
  try {
38635
- return join25(realpathSync5.native(at), ...missing.reverse());
39400
+ return join27(realpathSync5.native(at), ...missing.reverse());
38636
39401
  } catch {
38637
39402
  const parent = dirname18(at);
38638
39403
  if (parent === at) {
@@ -38685,7 +39450,7 @@ async function renameSymbol(request, services) {
38685
39450
  reason: `newScriptPath must be a .gd path inside the project, and ${request.newScriptPath} is not.`
38686
39451
  };
38687
39452
  }
38688
- if (existsSync22(request.newScriptPath)) {
39453
+ if (existsSync23(request.newScriptPath)) {
38689
39454
  return { ok: false, reason: `${moveTo} already exists, so the script cannot be moved there.` };
38690
39455
  }
38691
39456
  }
@@ -38861,7 +39626,7 @@ async function renameSymbol(request, services) {
38861
39626
  scripts,
38862
39627
  moves: new Map([[declaringPath, moveTo]]),
38863
39628
  named: [declaringPath],
38864
- exists: (path) => existsSync22(`${projectPath}/${path.slice("res://".length)}`)
39629
+ exists: (path) => existsSync23(`${projectPath}/${path.slice("res://".length)}`)
38865
39630
  }, plan);
38866
39631
  }
38867
39632
  plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? [] : [declaringPath]);
@@ -39060,15 +39825,15 @@ ${mention.kind}`;
39060
39825
  };
39061
39826
  }
39062
39827
  function writeLeftAloneDetail(projectPath, symbol, detail) {
39063
- const directory = join25(projectPath, ".godot", "gdharness-renames");
39828
+ const directory = join27(projectPath, ".godot", "gdharness-renames");
39064
39829
  mkdirSync12(directory, { recursive: true });
39065
39830
  detailWrites += 1;
39066
- const path = join25(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
39831
+ const path = join27(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
39067
39832
  writeFileSync15(path, `${JSON.stringify(detail, null, 2)}
39068
39833
  `, "utf8");
39069
39834
  const kept = readdirSync11(directory).filter((name) => name.endsWith(".json")).sort().reverse();
39070
39835
  for (const old of kept.slice(10)) {
39071
- rmSync12(join25(directory, old), { force: true });
39836
+ rmSync12(join27(directory, old), { force: true });
39072
39837
  }
39073
39838
  return path;
39074
39839
  }
@@ -39109,18 +39874,18 @@ var init_rename_symbol = __esm(() => {
39109
39874
 
39110
39875
  // src/file-move.ts
39111
39876
  import {
39112
- existsSync as existsSync23,
39877
+ existsSync as existsSync24,
39113
39878
  mkdirSync as mkdirSync13,
39114
39879
  readdirSync as readdirSync12,
39115
- readFileSync as readFileSync22,
39880
+ readFileSync as readFileSync23,
39116
39881
  renameSync as renameSync4,
39117
39882
  statSync as statSync11,
39118
39883
  writeFileSync as writeFileSync16
39119
39884
  } from "node:fs";
39120
- import { basename as basename8, dirname as dirname19, extname as extname3, join as join26, relative as relative5 } from "node:path";
39885
+ import { basename as basename8, dirname as dirname19, extname as extname3, join as join28, relative as relative5 } from "node:path";
39121
39886
  function filesUnder(directory) {
39122
39887
  return readdirSync12(directory, { withFileTypes: true }).flatMap((entry) => {
39123
- const path = join26(directory, entry.name);
39888
+ const path = join28(directory, entry.name);
39124
39889
  return entry.isDirectory() ? filesUnder(path) : [path];
39125
39890
  });
39126
39891
  }
@@ -39141,8 +39906,8 @@ function unseenByTheEngine(projectPath, path) {
39141
39906
  return `${path} is under ${step}, and the engine passes over every directory whose name starts with a dot`;
39142
39907
  }
39143
39908
  if (at < steps.length - 1) {
39144
- directory = join26(directory, step);
39145
- if (existsSync23(join26(directory, ".gdignore"))) {
39909
+ directory = join28(directory, step);
39910
+ if (existsSync24(join28(directory, ".gdignore"))) {
39146
39911
  return `${path} is under a directory holding a .gdignore, which the engine does not load`;
39147
39912
  }
39148
39913
  }
@@ -39154,12 +39919,12 @@ function installedAddonOf(projectPath, path) {
39154
39919
  if (steps[0] !== "addons" || steps[1] === undefined) {
39155
39920
  return null;
39156
39921
  }
39157
- return existsSync23(join26(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
39922
+ return existsSync24(join28(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
39158
39923
  }
39159
39924
  function binaryResources(projectPath) {
39160
39925
  const found = [];
39161
39926
  const visit = (directory, prefix) => {
39162
- if (existsSync23(join26(directory, ".gdignore"))) {
39927
+ if (existsSync24(join28(directory, ".gdignore"))) {
39163
39928
  return;
39164
39929
  }
39165
39930
  for (const entry of readdirSync12(directory, { withFileTypes: true })) {
@@ -39168,7 +39933,7 @@ function binaryResources(projectPath) {
39168
39933
  }
39169
39934
  const path = `${prefix}${entry.name}`;
39170
39935
  if (entry.isDirectory()) {
39171
- visit(join26(directory, entry.name), `${path}/`);
39936
+ visit(join28(directory, entry.name), `${path}/`);
39172
39937
  } else if (/\.(scn|res)$/i.test(entry.name)) {
39173
39938
  found.push(path);
39174
39939
  }
@@ -39178,7 +39943,7 @@ function binaryResources(projectPath) {
39178
39943
  return found;
39179
39944
  }
39180
39945
  function targetPathOf(projectPath, absolute) {
39181
- const inside = relative5(onDiskSpelling(projectPath), join26(onDiskSpelling(dirname19(absolute)), basename8(absolute)));
39946
+ const inside = relative5(onDiskSpelling(projectPath), join28(onDiskSpelling(dirname19(absolute)), basename8(absolute)));
39182
39947
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
39183
39948
  return null;
39184
39949
  }
@@ -39196,7 +39961,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
39196
39961
  if (to === null) {
39197
39962
  return { ok: false, reason: `newPath must be a path inside the project, and ${absoluteTo} is not.` };
39198
39963
  }
39199
- if (!existsSync23(absoluteFrom)) {
39964
+ if (!existsSync24(absoluteFrom)) {
39200
39965
  return { ok: false, reason: `There is nothing at ${from} to move.` };
39201
39966
  }
39202
39967
  if (from === to) {
@@ -39225,7 +39990,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
39225
39990
  if (isDirectory && to.startsWith(`${from}/`)) {
39226
39991
  return { ok: false, reason: `${from} cannot be moved into itself.` };
39227
39992
  }
39228
- if (existsSync23(absoluteTo) && !sameFile(absoluteFrom, absoluteTo)) {
39993
+ if (existsSync24(absoluteTo) && !sameFile(absoluteFrom, absoluteTo)) {
39229
39994
  return { ok: false, reason: `${to} already exists, so nothing was moved there.` };
39230
39995
  }
39231
39996
  const unseen = unseenByTheEngine(projectPath, to);
@@ -39233,7 +39998,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
39233
39998
  return { ok: false, reason: `${unseen}, so nothing there could load what was moved.` };
39234
39999
  }
39235
40000
  if (!isDirectory) {
39236
- const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync23(join26(projectPath, path.slice("res://".length))));
40001
+ const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync24(join28(projectPath, path.slice("res://".length))));
39237
40002
  if (owner !== undefined && owner !== null) {
39238
40003
  return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
39239
40004
  }
@@ -39255,7 +40020,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
39255
40020
  } else {
39256
40021
  moves.set(from, to);
39257
40022
  for (const sidecar of SIDECARS) {
39258
- if (existsSync23(`${absoluteFrom}${sidecar}`)) {
40023
+ if (existsSync24(`${absoluteFrom}${sidecar}`)) {
39259
40024
  moves.set(`${from}${sidecar}`, `${to}${sidecar}`);
39260
40025
  }
39261
40026
  }
@@ -39267,12 +40032,12 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
39267
40032
  scripts,
39268
40033
  moves,
39269
40034
  named: [from],
39270
- exists: (path) => existsSync23(join26(projectPath, path.slice("res://".length)))
40035
+ exists: (path) => existsSync24(join28(projectPath, path.slice("res://".length)))
39271
40036
  }, { edits: [], mentions: [] });
39272
40037
  plan = mentionIgnored(plan, ignoredTexts(projectPath), null, [from]);
39273
40038
  const binaries = [];
39274
40039
  for (const path of binaryResources(projectPath)) {
39275
- const bytes = readFileSync22(join26(projectPath, path.slice("res://".length)));
40040
+ const bytes = readFileSync23(join28(projectPath, path.slice("res://".length)));
39276
40041
  const names = [...moves.keys()].find((moving) => bytes.includes(moving));
39277
40042
  if (names !== undefined) {
39278
40043
  binaries.push({
@@ -39291,7 +40056,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
39291
40056
  }));
39292
40057
  const left = leftAloneReport(mentions);
39293
40058
  const rewrites = rewritesOf(plan, texts, null);
39294
- const absoluteOf = (path) => join26(projectPath, path.slice("res://".length));
40059
+ const absoluteOf = (path) => join28(projectPath, path.slice("res://".length));
39295
40060
  const rewritten = new Map(rewrites.map((rewrite) => [rewrite.path, rewrite]));
39296
40061
  const moved = [...moves].map(([source, target]) => {
39297
40062
  const rewrite = rewritten.get(source);
@@ -39333,18 +40098,18 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
39333
40098
  }
39334
40099
  function applyFileMove(outcome) {
39335
40100
  for (const rewrite of outcome.inPlace) {
39336
- if (readFileSync22(rewrite.absolute, "utf8") !== rewrite.before) {
40101
+ if (readFileSync23(rewrite.absolute, "utf8") !== rewrite.before) {
39337
40102
  throw new Error(`${rewrite.path} changed on disk while the move was being worked out; nothing was written`);
39338
40103
  }
39339
40104
  }
39340
40105
  for (const file of outcome.moved) {
39341
- if (!existsSync23(file.absoluteFrom)) {
40106
+ if (!existsSync24(file.absoluteFrom)) {
39342
40107
  throw new Error(`${file.from} is gone from disk; nothing was written`);
39343
40108
  }
39344
- if (file.text !== null && readFileSync22(file.absoluteFrom, "utf8") !== file.text.before) {
40109
+ if (file.text !== null && readFileSync23(file.absoluteFrom, "utf8") !== file.text.before) {
39345
40110
  throw new Error(`${file.from} changed on disk while the move was being worked out; nothing was written`);
39346
40111
  }
39347
- if (existsSync23(file.absoluteTo) && !sameFile(file.absoluteFrom, file.absoluteTo)) {
40112
+ if (existsSync24(file.absoluteTo) && !sameFile(file.absoluteFrom, file.absoluteTo)) {
39348
40113
  throw new Error(`${file.to} already exists; nothing was written`);
39349
40114
  }
39350
40115
  }
@@ -42773,701 +43538,202 @@ var init_godot_bridge = __esm(() => {
42773
43538
  this.emitBridgeEvent("tool_start", {
42774
43539
  tool: toolName,
42775
43540
  id: requestId,
42776
- args
42777
- });
42778
- try {
42779
- this.sendMessage(message);
42780
- } catch (error) {
42781
- clearTimeout(timeout);
42782
- this.pendingRequests.delete(requestId);
42783
- reject(toError(error));
42784
- }
42785
- });
42786
- }
42787
- sendMessage(message) {
42788
- if (this.socket?.readyState !== import_websocket.default.OPEN) {
42789
- throw new Refusal("Godot is not connected");
42790
- }
42791
- this.socket.send(JSON.stringify(message));
42792
- }
42793
- startKeepalive() {
42794
- this.stopKeepalive();
42795
- this.pingInterval = setInterval(() => {
42796
- if (!this.isConnected()) {
42797
- return;
42798
- }
42799
- try {
42800
- const ping = { type: "ping" };
42801
- this.sendMessage(ping);
42802
- } catch (error) {
42803
- this.log("warn", `Failed to send ping: ${errorMessage(error)}`);
42804
- }
42805
- }, KEEPALIVE_INTERVAL_MS);
42806
- }
42807
- stopKeepalive() {
42808
- if (!this.pingInterval) {
42809
- return;
42810
- }
42811
- clearInterval(this.pingInterval);
42812
- this.pingInterval = null;
42813
- }
42814
- belongsElsewhere(theirProject) {
42815
- if (this.ownProject === null || theirProject.trim() === "") {
42816
- return false;
42817
- }
42818
- return !isSameDirectory(this.ownProject, theirProject);
42819
- }
42820
- sendElsewhere(theirProject) {
42821
- const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
42822
- this.log("warn", `Turning away an editor from another project: ${said}`);
42823
- const stranger = this.socket;
42824
- this.socket = null;
42825
- this.connectionInfo = null;
42826
- this.stopKeepalive();
42827
- const fits = closeReason(said, "start") === said;
42828
- stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
42829
- }
42830
- elsewhereUnlessOneShot(theirProject, pid, openedByAServer) {
42831
- const known = pid === undefined ? undefined : this.oneShots.get(pid);
42832
- if (pid !== undefined && known !== undefined) {
42833
- this.turnAway(pid, known);
42834
- return;
42835
- }
42836
- const info = this.connectionInfo;
42837
- const socket = this.socket;
42838
- const reader = this.oneShotReader;
42839
- if (pid === undefined || openedByAServer || info === null || reader === null) {
42840
- this.sendElsewhere(theirProject);
42841
- return;
42842
- }
42843
- info.identifying = true;
42844
- reader(pid).catch(() => {
42845
- return;
42846
- }).then((option) => {
42847
- if (this.socket !== socket || this.connectionInfo !== info) {
42848
- return;
42849
- }
42850
- if (typeof option === "string") {
42851
- this.oneShots.set(pid, option);
42852
- this.turnAway(pid, option);
42853
- return;
42854
- }
42855
- this.sendElsewhere(theirProject);
42856
- });
42857
- }
42858
- identify(pid) {
42859
- const known = this.oneShots.get(pid);
42860
- if (known !== undefined) {
42861
- this.turnAway(pid, known);
42862
- return;
42863
- }
42864
- const info = this.connectionInfo;
42865
- const socket = this.socket;
42866
- const reader = this.oneShotReader;
42867
- if (info === null || reader === null) {
42868
- return;
42869
- }
42870
- info.identifying = true;
42871
- reader(pid).catch(() => {
42872
- return;
42873
- }).then((option) => {
42874
- if (this.socket !== socket || this.connectionInfo !== info) {
42875
- return;
42876
- }
42877
- if (typeof option === "string") {
42878
- this.oneShots.set(pid, option);
42879
- this.turnAway(pid, option);
42880
- return;
42881
- }
42882
- info.identifying = false;
42883
- this.log("info", `Godot editor identified: pid ${pid}`);
42884
- this.emitBridgeEvent("godot_connected", { projectPath: info.projectPath });
42885
- });
42886
- }
42887
- turnAway(pid, option) {
42888
- const said = `pid ${pid} was started with ${option}, which does one job and quits, so it is not an editor`;
42889
- this.log("info", `Turning away an engine run: ${said}`);
42890
- this.turnedAway = { pid, option, at: new Date };
42891
- const run = this.socket;
42892
- this.socket = null;
42893
- this.connectionInfo = null;
42894
- this.stopKeepalive();
42895
- run?.close(ONE_SHOT_CLOSE_CODE, closeReason(said, "start"));
42896
- }
42897
- handleDisconnect(disconnectedSocket, reason) {
42898
- if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
42899
- this.log("debug", "Ignoring stale Godot socket disconnect event");
42900
- return;
42901
- }
42902
- this.stopKeepalive();
42903
- this.socket = null;
42904
- this.connectionInfo = null;
42905
- this.emitBridgeEvent("godot_disconnected", {});
42906
- this.rejectAllPending(reason);
42907
- this.resourceQueues.clear();
42908
- }
42909
- emitBridgeEvent(eventName, payload) {
42910
- this.emit(eventName, payload);
42911
- }
42912
- rejectAllPending(error) {
42913
- for (const pending of this.pendingRequests.values()) {
42914
- clearTimeout(pending.timeout);
42915
- pending.reject(error);
42916
- }
42917
- this.pendingRequests.clear();
42918
- }
42919
- enqueueResourceRequest(resourceKey, task) {
42920
- const previous = this.resourceQueues.get(resourceKey) ?? Promise.resolve();
42921
- const taskPromise = previous.catch(() => {
42922
- return;
42923
- }).then(task);
42924
- const tail = taskPromise.then(() => {
42925
- return;
42926
- }, () => {
42927
- return;
42928
- });
42929
- this.resourceQueues.set(resourceKey, tail);
42930
- return taskPromise.finally(() => {
42931
- if (this.resourceQueues.get(resourceKey) === tail) {
42932
- this.resourceQueues.delete(resourceKey);
42933
- }
42934
- });
42935
- }
42936
- getResourceKey(args) {
42937
- const scenePath = this.getStringArg(args, "scenePath") ?? this.getStringArg(args, "scene_path");
42938
- if (scenePath) {
42939
- return `scene:${scenePath}`;
42940
- }
42941
- const resourcePath = this.getStringArg(args, "resourcePath") ?? this.getStringArg(args, "resource_path");
42942
- if (resourcePath) {
42943
- return `resource:${resourcePath}`;
42944
- }
42945
- return;
42946
- }
42947
- getStringArg(args, key) {
42948
- const value = args[key];
42949
- return typeof value === "string" && value.length > 0 ? value : undefined;
42950
- }
42951
- isIncomingMessage(value) {
42952
- if (!value || typeof value !== "object") {
42953
- return false;
42954
- }
42955
- const message = value;
42956
- const type = message["type"];
42957
- if (type !== "tool_result" && type !== "pong" && type !== "godot_ready") {
42958
- return false;
42959
- }
42960
- if (type === "pong") {
42961
- return true;
42962
- }
42963
- if (type === "godot_ready") {
42964
- return typeof message["project_path"] === "string";
42965
- }
42966
- return typeof message["id"] === "string" && typeof message["success"] === "boolean" && (message["error"] === undefined || typeof message["error"] === "string") && (message["busy"] === undefined || typeof message["busy"] === "boolean");
42967
- }
42968
- log(level, message) {
42969
- console.error(`[${new Date().toISOString()}] [GodotBridge:${level.toUpperCase()}] ${message}`);
42970
- }
42971
- };
42972
- });
42973
-
42974
- // src/junit.ts
42975
- function decoded(text) {
42976
- return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/g, (whole, entity) => {
42977
- if (entity.startsWith("#")) {
42978
- const code = entity.startsWith("#x") ? Number.parseInt(entity.slice(2), 16) : Number.parseInt(entity.slice(1), 10);
42979
- if (!Number.isInteger(code) || code < 0 || code > 1114111) {
42980
- throw new MalformedReportError(`Not a character: ${whole}`);
42981
- }
42982
- return String.fromCodePoint(code);
42983
- }
42984
- const named = ENTITIES[entity];
42985
- if (named === undefined) {
42986
- throw new MalformedReportError(`Unknown entity ${whole}`);
42987
- }
42988
- return named;
42989
- });
42990
- }
42991
- function tagEnd(source, from) {
42992
- let quote = null;
42993
- for (let at = from + 1;at < source.length; at += 1) {
42994
- const character = source[at];
42995
- if (quote !== null) {
42996
- if (character === quote) {
42997
- quote = null;
42998
- }
42999
- } else if (character === '"' || character === "'") {
43000
- quote = character;
43001
- } else if (character === ">") {
43002
- return at;
43003
- }
43004
- }
43005
- return -1;
43006
- }
43007
- function parseXml(source) {
43008
- const root = { name: "", attributes: {}, children: [], text: "" };
43009
- const open = [root];
43010
- let at = 0;
43011
- const current = () => {
43012
- const element = open.at(-1);
43013
- if (element === undefined) {
43014
- throw new MalformedReportError("A close tag with nothing open");
43015
- }
43016
- return element;
43017
- };
43018
- while (at < source.length) {
43019
- const next = source.indexOf("<", at);
43020
- if (next === -1) {
43021
- current().text += decoded(source.slice(at));
43022
- break;
43023
- }
43024
- current().text += decoded(source.slice(at, next));
43025
- at = next;
43026
- if (source.startsWith("<![CDATA[", at)) {
43027
- const end = source.indexOf("]]>", at);
43028
- if (end === -1) {
43029
- throw new MalformedReportError("CDATA that never ends");
43030
- }
43031
- current().text += source.slice(at + "<![CDATA[".length, end);
43032
- at = end + "]]>".length;
43033
- continue;
43034
- }
43035
- if (source.startsWith("<!--", at)) {
43036
- const end = source.indexOf("-->", at);
43037
- if (end === -1) {
43038
- throw new MalformedReportError("A comment that never ends");
43039
- }
43040
- at = end + "-->".length;
43041
- continue;
43042
- }
43043
- if (source.startsWith("<?", at) || source.startsWith("<!", at)) {
43044
- const end = source.indexOf(">", at);
43045
- if (end === -1) {
43046
- throw new MalformedReportError("A declaration that never ends");
43047
- }
43048
- at = end + 1;
43049
- continue;
43050
- }
43051
- const end = tagEnd(source, at);
43052
- if (end === -1) {
43053
- throw new MalformedReportError("A tag that never ends");
43054
- }
43055
- const tag = source.slice(at + 1, end);
43056
- at = end + 1;
43057
- if (tag.startsWith("/")) {
43058
- const closing = tag.slice(1).trim();
43059
- const element = current();
43060
- if (element === root || element.name !== closing) {
43061
- throw new MalformedReportError(`</${closing}> closes <${element.name || "nothing"}>`);
43062
- }
43063
- open.pop();
43064
- continue;
43065
- }
43066
- const selfClosing = tag.endsWith("/");
43067
- const body = selfClosing ? tag.slice(0, -1) : tag;
43068
- const nameMatch = /^([^\s/>]+)/.exec(body);
43069
- if (!nameMatch?.[1]) {
43070
- throw new MalformedReportError(`A tag with no name: <${tag}>`);
43071
- }
43072
- const attributes = {};
43073
- let read = nameMatch[0].length;
43074
- ATTRIBUTE.lastIndex = read;
43075
- let attribute = ATTRIBUTE.exec(body);
43076
- while (attribute) {
43077
- attributes[attribute[1] ?? ""] = decoded(attribute[2] ?? attribute[3] ?? "");
43078
- read = ATTRIBUTE.lastIndex;
43079
- attribute = ATTRIBUTE.exec(body);
43080
- }
43081
- if (body.slice(read).trim() !== "") {
43082
- throw new MalformedReportError(`Unreadable attributes in <${tag}>`);
43083
- }
43084
- const element = { name: nameMatch[1], attributes, children: [], text: "" };
43085
- current().children.push(element);
43086
- if (!selfClosing) {
43087
- open.push(element);
43088
- }
43089
- }
43090
- if (open.length !== 1) {
43091
- throw new MalformedReportError(`<${current().name}> is never closed`);
43092
- }
43093
- return root;
43094
- }
43095
- function count(attributes, name) {
43096
- const value = Number(attributes[name] ?? "0");
43097
- return Number.isFinite(value) ? value : 0;
43098
- }
43099
- function ignoredYetRan(report, ignored) {
43100
- const found = [];
43101
- for (const entry of ignored) {
43102
- const bare = entry.replace("res://", "");
43103
- const at = bare.lastIndexOf(":");
43104
- const suiteNamed = at === -1 ? null : bare.slice(0, at);
43105
- const testNamed = at === -1 ? bare : bare.slice(at + 1);
43106
- for (const suite of report.suites) {
43107
- const suiteMatches = suiteNamed === null || suite.name === suiteNamed || (suite.path ?? "").includes(suiteNamed);
43108
- const ran = suiteMatches ? suite.cases.find((one) => one.name === testNamed) : undefined;
43109
- if (ran !== undefined) {
43110
- found.push({ entry, ran: `${suite.name}:${ran.name}` });
43111
- }
43112
- }
43113
- }
43114
- return found;
43115
- }
43116
- function caseOf(suite, element) {
43117
- const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
43118
- const detail = child.text.trim();
43119
- return { kind, message: child.attributes["message"] ?? null, detail: detail === "" ? null : detail };
43120
- });
43121
- const reports = [...of("failure"), ...of("error"), ...of("skipped")];
43122
- const status = reports.some((report) => report.kind === "error") ? "error" : reports.some((report) => report.kind === "failure") ? "failed" : reports.length > 0 ? "skipped" : "passed";
43123
- return {
43124
- suite: suite.attributes["name"] ?? "",
43125
- name: element.attributes["name"] ?? "",
43126
- status,
43127
- time: count(element.attributes, "time"),
43128
- message: reports[0]?.message ?? null,
43129
- detail: reports[0]?.detail ?? null,
43130
- reports
43131
- };
43132
- }
43133
- function failedAnAssertion(entry) {
43134
- return entry.reports.some((report) => report.kind === "failure");
43135
- }
43136
- function pathOf(suite) {
43137
- const directory = suite.attributes["package"];
43138
- const name = suite.attributes["name"];
43139
- return directory && name ? `res://${directory}/${name}.gd` : null;
43140
- }
43141
- function parseJUnit(xml) {
43142
- const document = parseXml(xml);
43143
- const top = document.children.find((child) => child.name === "testsuites" || child.name === "testsuite");
43144
- if (top === undefined) {
43145
- throw new MalformedReportError("No <testsuites> or <testsuite> in the report");
43146
- }
43147
- const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
43148
- const suites = suiteElements.map((suite) => {
43149
- const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
43150
- const failures = cases.filter(failedAnAssertion).length;
43151
- const errors = cases.filter((entry) => entry.status === "error").length;
43152
- const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
43153
- return {
43154
- name: suite.attributes["name"] ?? "",
43155
- path: pathOf(suite),
43156
- tests: cases.length,
43157
- discovered: count(suite.attributes, "tests"),
43158
- failures,
43159
- errors,
43160
- hookFailures: Math.max(0, count(suite.attributes, "failures") - written("failure")),
43161
- hookErrors: Math.max(0, count(suite.attributes, "errors") - written("error")),
43162
- skipped: cases.filter((entry) => entry.status === "skipped").length,
43163
- time: count(suite.attributes, "time"),
43164
- cases
43165
- };
43166
- });
43167
- const all = suites.flatMap((suite) => suite.cases);
43168
- return {
43169
- tests: all.length,
43170
- failures: all.filter(failedAnAssertion).length,
43171
- errors: all.filter((entry) => entry.status === "error").length,
43172
- hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
43173
- hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
43174
- skipped: all.filter((entry) => entry.status === "skipped").length,
43175
- time: suites.reduce((sum, suite) => sum + suite.time, 0),
43176
- suites
43177
- };
43178
- }
43179
- function hookFailuresPrinted(printed) {
43180
- return printed.flatMap((line) => {
43181
- const found = HOOK_FAILURE.exec(line);
43182
- return found === null ? [] : [{ hook: found[2] ?? "", path: found[3] ?? "", line: Number(found[4]), message: found[1] ?? "" }];
43183
- });
43184
- }
43185
- function whyNoReport(printed, asked) {
43186
- const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
43187
- if (missing !== undefined) {
43188
- return `nothing at ${missing[1] ?? asked}`;
43189
- }
43190
- return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
43191
- }
43192
- function scriptErrorsPrinted(printed) {
43193
- return discoveryListing(printed)?.errors ?? [];
43194
- }
43195
- function discoveryListing(printed) {
43196
- const start = printed.findIndex((line) => line.includes(DISCOVERY_FAILED));
43197
- if (start < 0) {
43198
- return null;
43199
- }
43200
- const found = [];
43201
- let message = null;
43202
- let to = start + 1;
43203
- for (const line of printed.slice(start + 1)) {
43204
- const at = /^\s*at (res:\/\/.+):(\d+)\s*$/.exec(line);
43205
- if (at !== null && message !== null) {
43206
- found.push({ path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]), message });
43207
- message = null;
43208
- } else if (at === null && message === null && /^ {2}\S/.test(line)) {
43209
- message = line.trim();
43210
- } else {
43211
- break;
43212
- }
43213
- to += 1;
43214
- }
43215
- const errors = found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : (a.line ?? 0) - (b.line ?? 0));
43216
- return { errors, from: start, to };
43217
- }
43218
- function unspelled(text) {
43219
- return text.replace(/<(?:LF|CR|TAB|BS|FF|VT|BEL|ESC|DEL|0x[0-9A-F]{2})>/g, (spelling) => spelling.startsWith("<0x") ? String.fromCharCode(Number.parseInt(spelling.slice(3, 5), 16)) : SPELLED[spelling] ?? spelling);
43220
- }
43221
- function readDiff(printed) {
43222
- let merged = "";
43223
- let actual = "";
43224
- let background = null;
43225
- let last = 0;
43226
- const take = (text) => {
43227
- merged += text;
43228
- if (background === null) {
43229
- actual += text;
43230
- } else if (background === "added") {
43231
- actual += unspelled(text);
43541
+ args
43542
+ });
43543
+ try {
43544
+ this.sendMessage(message);
43545
+ } catch (error) {
43546
+ clearTimeout(timeout);
43547
+ this.pendingRequests.delete(requestId);
43548
+ reject(toError(error));
43549
+ }
43550
+ });
43232
43551
  }
43233
- };
43234
- for (const match of printed.matchAll(COLOUR2)) {
43235
- take(printed.slice(last, match.index));
43236
- last = match.index + match[0].length;
43237
- const codes = (match[1] ?? "").split(";");
43238
- if (codes[0] === "48" && codes[1] === "2") {
43239
- background = Number(codes[2]) > Number(codes[3]) ? "missing" : "added";
43240
- } else if (codes[0] === "0" || codes[0] === "") {
43241
- background = null;
43552
+ sendMessage(message) {
43553
+ if (this.socket?.readyState !== import_websocket.default.OPEN) {
43554
+ throw new Refusal("Godot is not connected");
43555
+ }
43556
+ this.socket.send(JSON.stringify(message));
43242
43557
  }
43243
- }
43244
- take(printed.slice(last));
43245
- return { merged, actual };
43246
- }
43247
- function withActualsPrinted(failed, printed) {
43248
- const diffs = [...printed.replaceAll(`\r
43249
- `, `
43250
- `).matchAll(PRINTED_ACTUAL)].map((match) => ({
43251
- suffix: match[3] ?? "",
43252
- at: match[5] ?? "",
43253
- ...readDiff(match[1] ?? "")
43254
- }));
43255
- const used = new Set;
43256
- return failed.map((entry) => {
43257
- if (entry.detail === null) {
43258
- return entry;
43558
+ startKeepalive() {
43559
+ this.stopKeepalive();
43560
+ this.pingInterval = setInterval(() => {
43561
+ if (!this.isConnected()) {
43562
+ return;
43563
+ }
43564
+ try {
43565
+ const ping = { type: "ping" };
43566
+ this.sendMessage(ping);
43567
+ } catch (error) {
43568
+ this.log("warn", `Failed to send ping: ${errorMessage(error)}`);
43569
+ }
43570
+ }, KEEPALIVE_INTERVAL_MS);
43259
43571
  }
43260
- let detail = entry.detail;
43261
- let recovered = false;
43262
- for (const [index, diff] of diffs.entries()) {
43263
- if (used.has(index)) {
43264
- continue;
43265
- }
43266
- const wrong = ` but was
43267
- '${diff.merged.replaceAll("\r", "")}'${diff.suffix}
43268
- at ${diff.at}`;
43269
- if (detail.includes(wrong)) {
43270
- detail = detail.replace(wrong, () => ` but was
43271
- '${diff.actual}'${diff.suffix}
43272
- at ${diff.at}`);
43273
- recovered = true;
43274
- used.add(index);
43572
+ stopKeepalive() {
43573
+ if (!this.pingInterval) {
43574
+ return;
43275
43575
  }
43576
+ clearInterval(this.pingInterval);
43577
+ this.pingInterval = null;
43276
43578
  }
43277
- const quoted = QUOTED_EQUALITY.exec(detail);
43278
- if (quoted !== null) {
43279
- const alike = quoted[1] === quoted[2];
43280
- const note = recovered ? alike ? RENDERED_AWAY_NOTE : null : alike ? BOTH_SIDES_ALIKE_NOTE : VALUE_NOT_RECOVERED_NOTE;
43281
- detail = note === null ? detail : `${detail}
43282
- ${note}`;
43283
- }
43284
- if (quoted !== null && masked(quoted[1] ?? "", quoted[2] ?? "")) {
43285
- const end = quoted[0].length - `
43286
- at `.length;
43287
- detail = detail.slice(0, end).replaceAll(MASKED_BRACKET, "[") + detail.slice(end);
43579
+ belongsElsewhere(theirProject) {
43580
+ if (this.ownProject === null || theirProject.trim() === "") {
43581
+ return false;
43582
+ }
43583
+ return !isSameDirectory(this.ownProject, theirProject);
43288
43584
  }
43289
- return detail === entry.detail ? entry : { ...entry, detail };
43290
- });
43291
- }
43292
- function shortened(detail) {
43293
- if (detail === null || detail.length <= LONGEST_DETAIL) {
43294
- return detail;
43295
- }
43296
- return `${detail.slice(0, LONGEST_DETAIL)}
43297
- [${detail.length - LONGEST_DETAIL} more characters left out]`;
43298
- }
43299
- function foldedReports(reports) {
43300
- const first = new Map;
43301
- for (const report of reports) {
43302
- const key = JSON.stringify([report.kind, report.message, report.detail]);
43303
- const seen = first.get(key);
43304
- if (seen === undefined) {
43305
- first.set(key, { report, times: 1 });
43306
- } else {
43307
- seen.times += 1;
43585
+ sendElsewhere(theirProject) {
43586
+ const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
43587
+ this.log("warn", `Turning away an editor from another project: ${said}`);
43588
+ const stranger = this.socket;
43589
+ this.socket = null;
43590
+ this.connectionInfo = null;
43591
+ this.stopKeepalive();
43592
+ const fits = closeReason(said, "start") === said;
43593
+ stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
43308
43594
  }
43309
- }
43310
- return [...first.values()].map(({ report, times }) => {
43311
- const kept = { ...report, detail: shortened(report.detail) };
43312
- return times === 1 ? kept : { ...kept, times };
43313
- });
43314
- }
43315
- function failedCases(report, printed) {
43316
- const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
43317
- const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
43318
- let next = 0;
43319
- return cases.map(({ entry, path }) => {
43320
- const reports = read.slice(next, next + entry.reports.length);
43321
- next += entry.reports.length;
43322
- const folded = foldedReports(reports);
43323
- const shown = folded.slice(0, MOST_REPORTS_SHOWN);
43324
- return {
43325
- suite: entry.suite,
43326
- name: entry.name,
43327
- status: entry.status,
43328
- time: entry.time,
43329
- path,
43330
- message: reports[0]?.message ?? null,
43331
- detail: shortened(reports[0]?.detail ?? null),
43332
- ...reports.length > 1 ? { reports: shown, reportCount: reports.length } : {},
43333
- ...folded.length > shown.length ? { reportsLeftOut: folded.length - shown.length } : {}
43334
- };
43335
- });
43336
- }
43337
- function failedWithin(failed, budget) {
43338
- const sizes = failed.map((entry) => JSON.stringify(entry).length);
43339
- const smallestFirst = sizes.map((_, index) => index).sort((a, b) => (sizes[a] ?? 0) - (sizes[b] ?? 0));
43340
- const whole = new Set;
43341
- let spent = 0;
43342
- for (const index of smallestFirst) {
43343
- const size = sizes[index] ?? 0;
43344
- if (spent + size > budget) {
43345
- break;
43595
+ elsewhereUnlessOneShot(theirProject, pid, openedByAServer) {
43596
+ const known = pid === undefined ? undefined : this.oneShots.get(pid);
43597
+ if (pid !== undefined && known !== undefined) {
43598
+ this.turnAway(pid, known);
43599
+ return;
43600
+ }
43601
+ const info = this.connectionInfo;
43602
+ const socket = this.socket;
43603
+ const reader = this.oneShotReader;
43604
+ if (pid === undefined || openedByAServer || info === null || reader === null) {
43605
+ this.sendElsewhere(theirProject);
43606
+ return;
43607
+ }
43608
+ info.identifying = true;
43609
+ reader(pid).catch(() => {
43610
+ return;
43611
+ }).then((option) => {
43612
+ if (this.socket !== socket || this.connectionInfo !== info) {
43613
+ return;
43614
+ }
43615
+ if (typeof option === "string") {
43616
+ this.oneShots.set(pid, option);
43617
+ this.turnAway(pid, option);
43618
+ return;
43619
+ }
43620
+ this.sendElsewhere(theirProject);
43621
+ });
43346
43622
  }
43347
- spent += size;
43348
- whole.add(index);
43349
- }
43350
- const kept = failed.map((entry, index) => {
43351
- if (whole.has(index)) {
43352
- return entry;
43623
+ identify(pid) {
43624
+ const known = this.oneShots.get(pid);
43625
+ if (known !== undefined) {
43626
+ this.turnAway(pid, known);
43627
+ return;
43628
+ }
43629
+ const info = this.connectionInfo;
43630
+ const socket = this.socket;
43631
+ const reader = this.oneShotReader;
43632
+ if (info === null || reader === null) {
43633
+ return;
43634
+ }
43635
+ info.identifying = true;
43636
+ reader(pid).catch(() => {
43637
+ return;
43638
+ }).then((option) => {
43639
+ if (this.socket !== socket || this.connectionInfo !== info) {
43640
+ return;
43641
+ }
43642
+ if (typeof option === "string") {
43643
+ this.oneShots.set(pid, option);
43644
+ this.turnAway(pid, option);
43645
+ return;
43646
+ }
43647
+ info.identifying = false;
43648
+ this.log("info", `Godot editor identified: pid ${pid}`);
43649
+ this.emitBridgeEvent("godot_connected", { projectPath: info.projectPath });
43650
+ });
43353
43651
  }
43354
- const { detail: _detail, reports: _reports, reportsLeftOut: _left, ...named } = entry;
43355
- return { ...named, message: entry.message?.slice(0, 500) ?? null, detail: null };
43356
- });
43357
- return { failed: kept, briefed: failed.length - whole.size };
43358
- }
43359
- function masked(expected, actual) {
43360
- const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
43361
- return `${expected}${actual}`.includes(MASKED_BRACKET) && allSpelled(expected) && allSpelled(actual);
43362
- }
43363
- function orphansPrinted(printed) {
43364
- const suites = [];
43365
- let total = null;
43366
- let running = null;
43367
- for (const line of printed) {
43368
- const started = RUNNING_SUITE.exec(line);
43369
- if (started?.[1] !== undefined) {
43370
- running = started[1];
43371
- continue;
43652
+ turnAway(pid, option) {
43653
+ const said = `pid ${pid} was started with ${option}, which does one job and quits, so it is not an editor`;
43654
+ this.log("info", `Turning away an engine run: ${said}`);
43655
+ this.turnedAway = { pid, option, at: new Date };
43656
+ const run = this.socket;
43657
+ this.socket = null;
43658
+ this.connectionInfo = null;
43659
+ this.stopKeepalive();
43660
+ run?.close(ONE_SHOT_CLOSE_CODE, closeReason(said, "start"));
43372
43661
  }
43373
- const counted = ORPHANS.exec(line);
43374
- if (counted?.[1] === undefined) {
43375
- continue;
43662
+ handleDisconnect(disconnectedSocket, reason) {
43663
+ if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
43664
+ this.log("debug", "Ignoring stale Godot socket disconnect event");
43665
+ return;
43666
+ }
43667
+ this.stopKeepalive();
43668
+ this.socket = null;
43669
+ this.connectionInfo = null;
43670
+ this.emitBridgeEvent("godot_disconnected", {});
43671
+ this.rejectAllPending(reason);
43672
+ this.resourceQueues.clear();
43376
43673
  }
43377
- const orphans = Number(counted[1]);
43378
- if (line.includes(OVERALL)) {
43379
- total = orphans;
43380
- running = null;
43381
- continue;
43674
+ emitBridgeEvent(eventName, payload) {
43675
+ this.emit(eventName, payload);
43382
43676
  }
43383
- if (running !== null && orphans > 0) {
43384
- suites.push({ path: running, orphans });
43677
+ rejectAllPending(error) {
43678
+ for (const pending of this.pendingRequests.values()) {
43679
+ clearTimeout(pending.timeout);
43680
+ pending.reject(error);
43681
+ }
43682
+ this.pendingRequests.clear();
43385
43683
  }
43386
- running = null;
43387
- }
43388
- return {
43389
- total: total ?? suites.reduce((sum, suite) => sum + suite.orphans, 0),
43390
- suites
43391
- };
43392
- }
43393
- function finishesASuite(line) {
43394
- return SUITE_COUNTS.test(line) && !line.includes(OVERALL);
43395
- }
43396
- function suitesPrinted(printed) {
43397
- const finished = [];
43398
- let running = null;
43399
- let failed = [];
43400
- for (const line of printed) {
43401
- const started = RUNNING_SUITE.exec(line);
43402
- if (started?.[1] !== undefined) {
43403
- running = started[1];
43404
- failed = [];
43405
- continue;
43684
+ enqueueResourceRequest(resourceKey, task) {
43685
+ const previous = this.resourceQueues.get(resourceKey) ?? Promise.resolve();
43686
+ const taskPromise = previous.catch(() => {
43687
+ return;
43688
+ }).then(task);
43689
+ const tail = taskPromise.then(() => {
43690
+ return;
43691
+ }, () => {
43692
+ return;
43693
+ });
43694
+ this.resourceQueues.set(resourceKey, tail);
43695
+ return taskPromise.finally(() => {
43696
+ if (this.resourceQueues.get(resourceKey) === tail) {
43697
+ this.resourceQueues.delete(resourceKey);
43698
+ }
43699
+ });
43406
43700
  }
43407
- if (running === null) {
43408
- continue;
43701
+ getResourceKey(args) {
43702
+ const scenePath = this.getStringArg(args, "scenePath") ?? this.getStringArg(args, "scene_path");
43703
+ if (scenePath) {
43704
+ return `scene:${scenePath}`;
43705
+ }
43706
+ const resourcePath = this.getStringArg(args, "resourcePath") ?? this.getStringArg(args, "resource_path");
43707
+ if (resourcePath) {
43708
+ return `resource:${resourcePath}`;
43709
+ }
43710
+ return;
43409
43711
  }
43410
- const failing = CASE_FAILED.exec(line)?.[1];
43411
- if (failing !== undefined) {
43412
- failed.push(failing);
43413
- continue;
43712
+ getStringArg(args, key) {
43713
+ const value = args[key];
43714
+ return typeof value === "string" && value.length > 0 ? value : undefined;
43414
43715
  }
43415
- const counted = finishesASuite(line) ? SUITE_COUNTS.exec(line) : null;
43416
- if (counted !== null) {
43417
- finished.push({
43418
- path: running,
43419
- cases: Number(counted[1]),
43420
- errors: Number(counted[2]),
43421
- failures: Number(counted[3]),
43422
- failed
43423
- });
43424
- running = null;
43716
+ isIncomingMessage(value) {
43717
+ if (!value || typeof value !== "object") {
43718
+ return false;
43719
+ }
43720
+ const message = value;
43721
+ const type = message["type"];
43722
+ if (type !== "tool_result" && type !== "pong" && type !== "godot_ready") {
43723
+ return false;
43724
+ }
43725
+ if (type === "pong") {
43726
+ return true;
43727
+ }
43728
+ if (type === "godot_ready") {
43729
+ return typeof message["project_path"] === "string";
43730
+ }
43731
+ return typeof message["id"] === "string" && typeof message["success"] === "boolean" && (message["error"] === undefined || typeof message["error"] === "string") && (message["busy"] === undefined || typeof message["busy"] === "boolean");
43425
43732
  }
43426
- }
43427
- return { finished, running };
43428
- }
43429
- function testProgressMessage(printed, elapsedMs) {
43430
- const { finished, running } = suitesPrinted(printed);
43431
- const failing = finished.filter((suite) => suite.errors + suite.failures > 0).length;
43432
- const seconds = Math.floor(elapsedMs / 1000);
43433
- const took = seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
43434
- const done = finished.length === 0 ? "No suite has finished" : `${finished.length === 1 ? "One suite has" : `${finished.length} suites have`} finished${failing === 0 ? "" : `, ${failing} with failures`}`;
43435
- return `${done}${running === null ? "" : `; running ${running}`}; ${took} in.`;
43436
- }
43437
- 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.]', MOST_REPORTS_SHOWN = 20, LONGEST_DETAIL = 20000, MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary", SUITE_COUNTS, CASE_FAILED;
43438
- var init_junit = __esm(() => {
43439
- MalformedReportError = class MalformedReportError extends Error {
43440
- constructor(message) {
43441
- super(message);
43442
- this.name = "MalformedReportError";
43733
+ log(level, message) {
43734
+ console.error(`[${new Date().toISOString()}] [GodotBridge:${level.toUpperCase()}] ${message}`);
43443
43735
  }
43444
43736
  };
43445
- ENTITIES = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
43446
- ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
43447
- HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
43448
- NOTHING_RAN = /no test cases found/i;
43449
- NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
43450
- ESCAPE = String.fromCharCode(27);
43451
- PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)(${IGNORING_CASE})?((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
43452
- QUOTED_EQUALITY = new RegExp(`^Expecting(?: error message)?:\\n '([\\s\\S]*)'\\n but was\\n '([\\s\\S]*)'(?:${IGNORING_CASE})?\\n\\tat `);
43453
- COLOUR2 = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
43454
- SPELLED = {
43455
- "<LF>": `
43456
- `,
43457
- "<CR>": "\r",
43458
- "<TAB>": "\t",
43459
- "<BS>": "\b",
43460
- "<FF>": "\f",
43461
- "<VT>": "\v",
43462
- "<BEL>": "\x07",
43463
- "<ESC>": ESCAPE,
43464
- "<DEL>": ""
43465
- };
43466
- PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
43467
- RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
43468
- ORPHANS = /(\d+)\s+orphans/;
43469
- SUITE_COUNTS = /(\d+)\s+test cases\s*\|\s*(\d+)\s+errors\s*\|\s*(\d+)\s+failures/;
43470
- CASE_FAILED = /^\s*res:\/\/.+?\.(?:gd|cs) > (.+?) (?:FAILED|ERROR)(?:\s|$)/;
43471
43737
  });
43472
43738
 
43473
43739
  // src/keeper-channel.ts
@@ -43537,10 +43803,10 @@ var init_keeper_channel = () => {};
43537
43803
 
43538
43804
  // src/lifecycle-log.ts
43539
43805
  import { appendFileSync, mkdirSync as mkdirSync14, renameSync as renameSync5, statSync as statSync12 } from "node:fs";
43540
- import { dirname as dirname20, join as join27 } from "node:path";
43806
+ import { dirname as dirname20, join as join29 } from "node:path";
43541
43807
  import process11 from "node:process";
43542
43808
  function lifecycleLogPath() {
43543
- return join27(runtimeDirectory(), "servers.log");
43809
+ return join29(runtimeDirectory(), "servers.log");
43544
43810
  }
43545
43811
  function writeLifecycle(event, fields = {}) {
43546
43812
  const path = lifecycleLogPath();
@@ -43860,10 +44126,10 @@ var init_prompts = __esm(() => {
43860
44126
  });
43861
44127
 
43862
44128
  // src/restart-note.ts
43863
- import { mkdirSync as mkdirSync15, readFileSync as readFileSync23, rmSync as rmSync13, writeFileSync as writeFileSync17 } from "node:fs";
43864
- import { dirname as dirname21, join as join28 } from "node:path";
44129
+ import { mkdirSync as mkdirSync15, readFileSync as readFileSync24, rmSync as rmSync13, writeFileSync as writeFileSync17 } from "node:fs";
44130
+ import { dirname as dirname21, join as join30 } from "node:path";
43865
44131
  function restartNotePath(projectPath) {
43866
- return join28(projectPath, ".godot", "gdharness-restart.json");
44132
+ return join30(projectPath, ".godot", "gdharness-restart.json");
43867
44133
  }
43868
44134
  function noteRestartBegun(note) {
43869
44135
  const path = restartNotePath(note.projectPath);
@@ -43877,7 +44143,7 @@ function noteRestartBegun(note) {
43877
44143
  }
43878
44144
  function restartOwed(projectPath) {
43879
44145
  try {
43880
- const parsed = JSON.parse(readFileSync23(restartNotePath(projectPath), "utf8"));
44146
+ const parsed = JSON.parse(readFileSync24(restartNotePath(projectPath), "utf8"));
43881
44147
  if (typeof parsed !== "object" || parsed === null) {
43882
44148
  return null;
43883
44149
  }
@@ -43903,17 +44169,17 @@ var init_restart_note = __esm(() => {
43903
44169
 
43904
44170
  // src/run-record.ts
43905
44171
  import { createHash as createHash5 } from "node:crypto";
43906
- import { mkdirSync as mkdirSync16, openSync as openSync3, readdirSync as readdirSync13, readFileSync as readFileSync24, realpathSync as realpathSync6, rmSync as rmSync14, statSync as statSync13 } from "node:fs";
43907
- import { basename as basename9, dirname as dirname22, join as join29, resolve as resolve7 } from "node:path";
44172
+ import { mkdirSync as mkdirSync16, openSync as openSync3, readdirSync as readdirSync13, readFileSync as readFileSync25, realpathSync as realpathSync6, rmSync as rmSync14, statSync as statSync13 } from "node:fs";
44173
+ import { basename as basename9, dirname as dirname22, join as join31, resolve as resolve7 } from "node:path";
43908
44174
  function runsDirectory() {
43909
- return join29(runtimeDirectory(), "runs");
44175
+ return join31(runtimeDirectory(), "runs");
43910
44176
  }
43911
44177
  function realAsFarAsItExists(path) {
43912
44178
  const missing = [];
43913
44179
  let at = path;
43914
44180
  for (;; ) {
43915
44181
  try {
43916
- return join29(realpathSync6.native(at), ...missing);
44182
+ return join31(realpathSync6.native(at), ...missing);
43917
44183
  } catch {
43918
44184
  const parent = dirname22(at);
43919
44185
  if (parent === at) {
@@ -43930,10 +44196,10 @@ function projectKey(projectPath) {
43930
44196
  return createHash5("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
43931
44197
  }
43932
44198
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
43933
- return join29(directory, "runs", `note-${projectKey(projectPath)}.json`);
44199
+ return join31(directory, "runs", `note-${projectKey(projectPath)}.json`);
43934
44200
  }
43935
44201
  function latestRunPath(servedBy, directory = runtimeDirectory()) {
43936
- return join29(directory, "runs", `latest-${projectKey(servedBy)}.json`);
44202
+ return join31(directory, "runs", `latest-${projectKey(servedBy)}.json`);
43937
44203
  }
43938
44204
  function writeLatestRun(servedBy, latest) {
43939
44205
  try {
@@ -43943,7 +44209,7 @@ function writeLatestRun(servedBy, latest) {
43943
44209
  }
43944
44210
  function readLatestRun(servedBy, directory = runtimeDirectory()) {
43945
44211
  try {
43946
- const fields = JSON.parse(readFileSync24(latestRunPath(servedBy, directory), "utf8"));
44212
+ const fields = JSON.parse(readFileSync25(latestRunPath(servedBy, directory), "utf8"));
43947
44213
  if (typeof fields !== "object" || fields === null) {
43948
44214
  return null;
43949
44215
  }
@@ -43958,20 +44224,20 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
43958
44224
  }
43959
44225
  function openTranscript(startedAt) {
43960
44226
  mkdirSync16(runsDirectory(), { recursive: true });
43961
- const path = join29(runsDirectory(), `run-${startedAt}.log`);
44227
+ const path = join31(runsDirectory(), `run-${startedAt}.log`);
43962
44228
  return { path, fd: openSync3(path, "a") };
43963
44229
  }
43964
44230
  function profilePath(startedAt) {
43965
- return join29(runsDirectory(), `run-${startedAt}.profile.json`);
44231
+ return join31(runsDirectory(), `run-${startedAt}.profile.json`);
43966
44232
  }
43967
44233
  function profileMarkPath(startedAt) {
43968
- return join29(runsDirectory(), `run-${startedAt}.profile-mark.json`);
44234
+ return join31(runsDirectory(), `run-${startedAt}.profile-mark.json`);
43969
44235
  }
43970
44236
  function noteIsAbout(note, playing) {
43971
44237
  return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
43972
44238
  }
43973
44239
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
43974
- return join29(directory, "runs", `editor-${projectKey(projectPath)}.json`);
44240
+ return join31(directory, "runs", `editor-${projectKey(projectPath)}.json`);
43975
44241
  }
43976
44242
  function writeEditorRunNote(note) {
43977
44243
  mkdirSync16(runsDirectory(), { recursive: true });
@@ -43982,7 +44248,7 @@ function writeEditorRunNote(note) {
43982
44248
  }
43983
44249
  function readEditorRunNote(projectPath) {
43984
44250
  for (const directory of runtimeDirectories()) {
43985
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join29(directory, "runs", "editor-run.json"));
44251
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join31(directory, "runs", "editor-run.json"));
43986
44252
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
43987
44253
  return note;
43988
44254
  }
@@ -43992,7 +44258,7 @@ function readEditorRunNote(projectPath) {
43992
44258
  function editorNoteAt(path) {
43993
44259
  let parsed;
43994
44260
  try {
43995
- parsed = JSON.parse(readFileSync24(path, "utf8"));
44261
+ parsed = JSON.parse(readFileSync25(path, "utf8"));
43996
44262
  } catch {
43997
44263
  return null;
43998
44264
  }
@@ -44016,7 +44282,7 @@ function readRunRecord(projectPath) {
44016
44282
  if (own !== null) {
44017
44283
  return own;
44018
44284
  }
44019
- const shared = recordAt(join29(directory, "runs", SHARED_NOTE));
44285
+ const shared = recordAt(join31(directory, "runs", SHARED_NOTE));
44020
44286
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
44021
44287
  return shared;
44022
44288
  }
@@ -44028,13 +44294,13 @@ function everyRunRecord() {
44028
44294
  for (const directory of runtimeDirectories()) {
44029
44295
  let entries;
44030
44296
  try {
44031
- entries = readdirSync13(join29(directory, "runs"));
44297
+ entries = readdirSync13(join31(directory, "runs"));
44032
44298
  } catch {
44033
44299
  continue;
44034
44300
  }
44035
44301
  for (const entry of entries) {
44036
44302
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
44037
- const record = recordAt(join29(directory, "runs", entry));
44303
+ const record = recordAt(join31(directory, "runs", entry));
44038
44304
  if (record !== null) {
44039
44305
  found.push(record);
44040
44306
  }
@@ -44046,7 +44312,7 @@ function everyRunRecord() {
44046
44312
  function recordAt(path) {
44047
44313
  let parsed;
44048
44314
  try {
44049
- parsed = JSON.parse(readFileSync24(path, "utf8"));
44315
+ parsed = JSON.parse(readFileSync25(path, "utf8"));
44050
44316
  } catch {
44051
44317
  return null;
44052
44318
  }
@@ -44099,7 +44365,7 @@ async function runningAs(pid) {
44099
44365
  const began = await startedAt(pid, null);
44100
44366
  const when = began === null ? {} : { startedAt: began };
44101
44367
  if (process.platform === "linux") {
44102
- const raw = readFileSync24(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
44368
+ const raw = readFileSync25(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
44103
44369
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
44104
44370
  }
44105
44371
  const args = (await runHidden("ps", ["-p", String(pid), "-o", "args="], { timeout: 15000 })).stdout.trim();
@@ -44242,7 +44508,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
44242
44508
  }
44243
44509
  function clearRunRecord(projectPath) {
44244
44510
  for (const directory of runtimeDirectories()) {
44245
- const shared = join29(directory, "runs", SHARED_NOTE);
44511
+ const shared = join31(directory, "runs", SHARED_NOTE);
44246
44512
  const sharedRecord = recordAt(shared);
44247
44513
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
44248
44514
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -44277,7 +44543,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
44277
44543
  if (!transcript && !entry.endsWith(".partial")) {
44278
44544
  continue;
44279
44545
  }
44280
- const path = join29(directory, entry);
44546
+ const path = join31(directory, entry);
44281
44547
  if (kept.has(path) || ageMs(path, now) < keepMs) {
44282
44548
  continue;
44283
44549
  }
@@ -44297,109 +44563,11 @@ var init_run_record = __esm(() => {
44297
44563
  init_windows_ask();
44298
44564
  });
44299
44565
 
44300
- // src/script-origins.ts
44301
- import { join as join30 } from "node:path";
44302
- function unresolvedClassIn(message) {
44303
- return UNRESOLVED.exec(message)?.[1] ?? null;
44304
- }
44305
- async function originsOf(errors, declared, check) {
44306
- const listed = new Set(errors.map((error) => error.path));
44307
- const origins = [];
44308
- const unconfirmed = [];
44309
- const asked = new Set;
44310
- const waiting = errors.map((error) => unresolvedClassIn(error.message)).filter((name) => name !== null);
44311
- while (waiting.length > 0 && asked.size < MOST_CHECKED) {
44312
- const name = waiting.shift() ?? "";
44313
- const path = declared.get(name);
44314
- if (path === undefined || listed.has(path) || asked.has(path)) {
44315
- continue;
44316
- }
44317
- asked.add(path);
44318
- const checked = await check(path);
44319
- const own = checked.errors.filter((error) => error.path === path);
44320
- if (own.length === 0) {
44321
- unconfirmed.push({
44322
- path,
44323
- declares: name,
44324
- why: checked.failure === null ? "the engine's check of it found no error" : `the engine's check of it did not finish (${checked.failure})`
44325
- });
44326
- continue;
44327
- }
44328
- for (const error of own) {
44329
- origins.push({ ...error, declares: name });
44330
- const deeper = unresolvedClassIn(error.message);
44331
- if (deeper !== null) {
44332
- waiting.push(deeper);
44333
- }
44334
- }
44335
- }
44336
- return { origins, unconfirmed };
44337
- }
44338
- function reloadedAt(entry) {
44339
- for (const line of entry.detail) {
44340
- const at = RELOADED_AT.exec(line.trim());
44341
- if (at !== null) {
44342
- return { path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]) };
44343
- }
44344
- }
44345
- return null;
44346
- }
44347
- function checkWithEngine(godotPath, projectPath) {
44348
- return async (path) => {
44349
- const logDir = scratchDirectory("tests");
44350
- try {
44351
- const ran = await runEngine(godotPath, [
44352
- "--headless",
44353
- "--log-file",
44354
- join30(logDir, "engine.log"),
44355
- "--path",
44356
- projectPath,
44357
- "--check-only",
44358
- "-s",
44359
- path
44360
- ], { timeout: { ms: 60000, said: "minute" } });
44361
- const errors = ran.log.select({ severity: "error", sinceLastCall: false, limit: 50 }).entries.flatMap((entry) => {
44362
- const at = reloadedAt(entry);
44363
- return at === null ? [] : [{ ...at, message: entry.text }];
44364
- });
44365
- return { errors, failure: ran.failure === null ? null : howItEnded(ran) };
44366
- } finally {
44367
- discard(logDir);
44368
- }
44369
- };
44370
- }
44371
- function withoutListed(entries, errors) {
44372
- if (errors.length === 0) {
44373
- return [...entries];
44374
- }
44375
- const listing = discoveryListing(entries.map((entry) => entry.text));
44376
- const paths = new Set(errors.map((error) => error.path));
44377
- return entries.filter((entry, at) => {
44378
- if (listing !== null && at >= listing.from && at < listing.to) {
44379
- return false;
44380
- }
44381
- const reloaded = reloadedAt(entry);
44382
- if (reloaded !== null && errors.some((error) => error.path === reloaded.path && error.line === reloaded.line && error.message === entry.text)) {
44383
- return false;
44384
- }
44385
- const failedToLoad = /^Failed to load script "(res:\/\/[^"]+)" with error "[^"]+"\.$/.exec(entry.text);
44386
- return failedToLoad === null || !paths.has(failedToLoad[1] ?? "");
44387
- });
44388
- }
44389
- var UNRESOLVED, MOST_CHECKED = 8, RELOADED_AT;
44390
- var init_script_origins = __esm(() => {
44391
- init_engine_run();
44392
- init_junit();
44393
- init_scratch();
44394
- UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
44395
- RELOADED_AT = /GDScript::reload \((res:\/\/.+):(\d+)\)$/;
44396
- });
44397
-
44398
44566
  // src/script-profile.ts
44399
- import { readFileSync as readFileSync25 } from "node:fs";
44567
+ import { readFileSync as readFileSync26 } from "node:fs";
44400
44568
  function readProfileFile(path) {
44401
44569
  try {
44402
- const parsed = JSON.parse(readFileSync25(path, "utf8"));
44570
+ const parsed = JSON.parse(readFileSync26(path, "utf8"));
44403
44571
  return typeof parsed === "object" && typeof parsed.connected === "boolean" ? parsed : null;
44404
44572
  } catch {
44405
44573
  return null;
@@ -44427,12 +44595,16 @@ function signatureParts(signature) {
44427
44595
  function profiledFunctions(totals, limit, sourceOf = () => null) {
44428
44596
  const perCallUs = perCallCostOf(totals);
44429
44597
  const rows = rowsOf(totals, sourceOf).map((row) => withOverhead(row, perCallUs));
44430
- const scriptMs = ms(Object.entries(totals.functions).reduce((sum, [signature, one]) => sum + (one.calls > 0 && !isCalibration(signature) ? one.selfSeconds : 0), 0));
44598
+ const called = Object.entries(totals.functions).filter(([signature, one]) => one.calls > 0 && !isCalibration(signature));
44599
+ const addon = called.filter(([signature]) => isRuntimeAddon(signature));
44600
+ const selfOf = (entries) => ms(entries.reduce((sum, [, one]) => sum + one.selfSeconds, 0));
44431
44601
  return {
44432
44602
  functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
44433
44603
  omitted: Math.max(0, rows.length - limit),
44434
- scriptMs,
44604
+ scriptMs: selfOf(called.filter(([signature]) => !isRuntimeAddon(signature))),
44435
44605
  counted: rows.length,
44606
+ addonMs: selfOf(addon),
44607
+ addonFunctions: addon.length,
44436
44608
  superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
44437
44609
  perCallUs,
44438
44610
  overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length,
@@ -44440,6 +44612,9 @@ function profiledFunctions(totals, limit, sourceOf = () => null) {
44440
44612
  overheadReach: rows.findLastIndex((row) => row.overheadMs !== undefined) + 1
44441
44613
  };
44442
44614
  }
44615
+ function isRuntimeAddon(signature) {
44616
+ return signatureParts(signature).script.startsWith(RUNTIME_ADDON2);
44617
+ }
44443
44618
  function isCalibration(signature) {
44444
44619
  const parts = signatureParts(signature);
44445
44620
  return parts.script.endsWith(RUNTIME_SCRIPT) && (parts.function === PROFILER_PROBE || parts.function === PROFILER_CALIBRATOR);
@@ -44449,7 +44624,7 @@ function writeProfileMark(path, mark) {
44449
44624
  }
44450
44625
  function readProfileMark(path) {
44451
44626
  try {
44452
- const parsed = JSON.parse(readFileSync25(path, "utf8"));
44627
+ const parsed = JSON.parse(readFileSync26(path, "utf8"));
44453
44628
  return typeof parsed === "object" && typeof parsed.totals === "object" ? parsed : null;
44454
44629
  } catch {
44455
44630
  return null;
@@ -44515,7 +44690,7 @@ function rowsOf(totals, sourceOf) {
44515
44690
  }
44516
44691
  return codes.get(script) ?? null;
44517
44692
  };
44518
- return Object.entries(totals.functions).filter(([signature, sum]) => sum.calls > 0 && !isCalibration(signature)).map(([signature, sum]) => {
44693
+ return Object.entries(totals.functions).filter(([signature, sum]) => sum.calls > 0 && !isRuntimeAddon(signature) && !isCalibration(signature)).map(([signature, sum]) => {
44519
44694
  const parts = signatureParts(signature);
44520
44695
  const code = codeOfScript(parts.script);
44521
44696
  const property = propertyOf(parts.function);
@@ -44591,7 +44766,7 @@ function callersOf(totals, name, scripts) {
44591
44766
  property
44592
44767
  };
44593
44768
  }
44594
- var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler", CALL_SITES = 40;
44769
+ var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler", RUNTIME_ADDON2 = "res://addons/gdharness_runtime/", CALL_SITES = 40;
44595
44770
  var init_script_profile = __esm(() => {
44596
44771
  init_gdscript_source();
44597
44772
  init_godot_variant();
@@ -44622,18 +44797,18 @@ import {
44622
44797
  closeSync as closeSync3,
44623
44798
  constants,
44624
44799
  copyFileSync,
44625
- existsSync as existsSync24,
44800
+ existsSync as existsSync25,
44626
44801
  fstatSync,
44627
44802
  mkdirSync as mkdirSync17,
44628
44803
  openSync as openSync4,
44629
44804
  readdirSync as readdirSync14,
44630
- readFileSync as readFileSync26,
44805
+ readFileSync as readFileSync27,
44631
44806
  readSync as readSync2,
44632
44807
  rmdirSync as rmdirSync2,
44633
44808
  statSync as statSync14,
44634
44809
  writeFileSync as writeFileSync18
44635
44810
  } from "node:fs";
44636
- import { basename as basename10, dirname as dirname23, extname as extname4, isAbsolute as isAbsolute5, join as join31, normalize as normalize2, relative as relative6 } from "node:path";
44811
+ import { basename as basename10, dirname as dirname23, extname as extname4, isAbsolute as isAbsolute5, join as join32, normalize as normalize2, relative as relative6 } from "node:path";
44637
44812
  import { setTimeout as delay5 } from "node:timers/promises";
44638
44813
  import { fileURLToPath as fileURLToPath5 } from "node:url";
44639
44814
  function editorRestartTimeoutMs() {
@@ -44670,15 +44845,15 @@ function writtenFileRefusal(path, rule) {
44670
44845
  return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
44671
44846
  }
44672
44847
  const directory = realPathOr(dirname23(path));
44673
- if (!existsSync24(directory)) {
44848
+ if (!existsSync25(directory)) {
44674
44849
  return `${named}'s directory ${dirname23(path)} does not exist; ${what} creates no directories.`;
44675
44850
  }
44676
- const where = join31(directory, basename10(path));
44851
+ const where = join32(directory, basename10(path));
44677
44852
  const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
44678
44853
  if (inside !== undefined) {
44679
44854
  return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
44680
44855
  }
44681
- const taken = rule.writes(where).find((file) => existsSync24(file));
44856
+ const taken = rule.writes(where).find((file) => existsSync25(file));
44682
44857
  if (taken !== undefined) {
44683
44858
  return taken === where ? `${named} ${path} already exists; ${what} is not written over a file. Name a new one.` : `${named} ${path} would write ${taken}, which already exists; ${what} is not written over a file. Name a new one.`;
44684
44859
  }
@@ -45150,7 +45325,12 @@ function unlistedValues(schema, value, path) {
45150
45325
  const fields = schema;
45151
45326
  const listed = fields["enum"];
45152
45327
  if (Array.isArray(listed) && typeof value === "string") {
45153
- return listed.includes(value) ? [] : [`${path} as one of ${listed.join(", ")}, not ${JSON.stringify(value)}`];
45328
+ if (listed.includes(value)) {
45329
+ return [];
45330
+ }
45331
+ return [
45332
+ listed.length === 1 ? `${path} only as ${String(listed[0])}, not ${JSON.stringify(value)}` : `${path} as one of ${listed.join(", ")}, not ${JSON.stringify(value)}`
45333
+ ];
45154
45334
  }
45155
45335
  if (Array.isArray(value)) {
45156
45336
  return value.flatMap((element, index) => unlistedValues(fields["items"], element, `${path}[${index}]`));
@@ -45214,7 +45394,7 @@ function notRunNote(notRun, failFast) {
45214
45394
  function exportPresetRefusal(projectPath, preset) {
45215
45395
  let text;
45216
45396
  try {
45217
- text = readFileSync26(join31(projectPath, "export_presets.cfg"), "utf8");
45397
+ text = readFileSync27(join32(projectPath, "export_presets.cfg"), "utf8");
45218
45398
  } catch {
45219
45399
  return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
45220
45400
  }
@@ -45280,7 +45460,7 @@ function filesIn(folder) {
45280
45460
  try {
45281
45461
  for (const entry of readdirSync14(folder, { withFileTypes: true })) {
45282
45462
  if (entry.isFile()) {
45283
- found.set(entry.name, statSync14(join31(folder, entry.name)).mtimeMs);
45463
+ found.set(entry.name, statSync14(join32(folder, entry.name)).mtimeMs);
45284
45464
  }
45285
45465
  }
45286
45466
  } catch {}
@@ -45310,7 +45490,7 @@ function unsavedInEditorNote(scripts) {
45310
45490
  return `${named} ${one ? "is" : "are"} open in the editor's script ${one ? "tab" : "tabs"} with unsaved edits, and saving ${one ? "that tab" : "them"} there writes ${one ? "its" : "their"} text over what was written here: close ${one ? "it" : "them"} without saving, or save first and edit again.`;
45311
45491
  }
45312
45492
  function editorAddonMissing(projectPath) {
45313
- if (!existsSync24(join31(projectPath, "project.godot"))) {
45493
+ if (!existsSync25(join32(projectPath, "project.godot"))) {
45314
45494
  return null;
45315
45495
  }
45316
45496
  const report = inspectProject(projectPath);
@@ -45476,8 +45656,35 @@ function scriptErrorsNote(errors, {
45476
45656
  const opening = unnamed === 0 ? `No tests ran: gdUnit4 could not load ${scripts === 1 ? "one script" : `${scripts} scripts`} while looking for suites, and it runs no suite at all when one fails to load.` : `No tests ran: ${scripts + unnamed} scripts did not compile, and gdUnit4 runs no suite at all when one fails to load. It could not load ${scripts === 1 ? "one" : scripts} of them while looking for suites, and ${unnamed === 1 ? "the one it did not name comes" : `the ${unnamed} it did not name come`} first.`;
45477
45657
  return [opening, named, following, rest, after].filter((part) => part !== "").join(" ");
45478
45658
  }
45479
- function timedOutVerdict(timeoutMs, silentForMs) {
45659
+ function autoloadNotCompiledNote(at, errors, stoppedEarly) {
45660
+ const first = errors.scriptErrors[0];
45661
+ const where = first === undefined ? "" : `, and the first error the engine raised compiling was at ${first.path}${first.line === null ? "" : `:${first.line}`}: ${first.message}`;
45662
+ const others = errors.scriptErrors.length - 1;
45663
+ const more = others > 0 ? ` ${others === 1 ? "One more error is" : `${others} more errors are`} under scriptErrors.` : "";
45664
+ const through = errors.failedThroughDependency.length;
45665
+ const dependents = through > 0 ? ` ${through === 1 ? "One script" : `${through} scripts`} failed only through ${first === undefined ? "it" : "that"}, listed under failedThroughDependency.` : "";
45666
+ const ended = stoppedEarly ? "The run was stopped as soon as the engine said so rather than left to its timeout" : "The run ended before that was read, and what its suites said is set aside";
45667
+ const stop = /[.!?]\)?$/.test(where) ? "" : ".";
45668
+ return `No result counts: the autoload ${at.name} (${at.path}) did not compile${where}${stop}${more}${dependents} Every suite runs with the autoloads, so a case calling into a script that did not compile fails the same way on every pass, and one waiting in a loop for the call to succeed never ends. ${ended}. script_diagnostics checks a script without running anything.`;
45669
+ }
45670
+ function repeatingScriptError(entries, byScript) {
45671
+ const last = entries.at(-1);
45672
+ if (last === undefined || !byScript(last)) {
45673
+ return null;
45674
+ }
45675
+ const whole = last.detail.length > 0 ? last : entries.findLast((entry) => entry.text === last.text && entry.detail.length > 0 && byScript(entry)) ?? last;
45676
+ const where = whole.detail[0] ?? null;
45677
+ const times = entries.filter((entry) => entry.text === last.text && (entry.detail[0] ?? null) === where && byScript(entry)).length;
45678
+ return times >= LOOPING_AFTER ? { text: last.text, where: where?.replace(/^at: /, "") ?? null, times } : null;
45679
+ }
45680
+ function timedOutVerdict(timeoutMs, silentForMs, repeating = null) {
45480
45681
  const quiet = Math.round(silentForMs);
45682
+ if (repeating !== null) {
45683
+ return {
45684
+ verdict: `timed out after ${timeoutMs} ms repeating one script error, printed ${repeating.times} times: ${repeating.text}${repeating.where === null ? "" : ` (${repeating.where})`}. A line that fails on every pass of a loop does this, and a longer timeoutMs would not end it`,
45685
+ hung: false
45686
+ };
45687
+ }
45481
45688
  return quiet >= Math.min(STALLED_AFTER_MS, timeoutMs / 2) ? {
45482
45689
  verdict: `hung: killed after ${timeoutMs} ms, having printed nothing for the last ${quiet} ms`,
45483
45690
  hung: true
@@ -45548,7 +45755,7 @@ function profileFrameFunctions(projectPath) {
45548
45755
  let projectGodot = null;
45549
45756
  if (projectPath !== null) {
45550
45757
  try {
45551
- projectGodot = readFileSync26(join31(projectPath, "project.godot"), "utf8");
45758
+ projectGodot = readFileSync27(join32(projectPath, "project.godot"), "utf8");
45552
45759
  } catch {}
45553
45760
  }
45554
45761
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -45572,7 +45779,7 @@ function projectSourceOf(projectPath) {
45572
45779
  let source = null;
45573
45780
  if (inside.ok) {
45574
45781
  try {
45575
- source = readFileSync26(inside.absolutePath, "utf8");
45782
+ source = readFileSync27(inside.absolutePath, "utf8");
45576
45783
  } catch {}
45577
45784
  }
45578
45785
  read.set(script, source);
@@ -45623,6 +45830,9 @@ function profileAnswer(reading, limit) {
45623
45830
  if (reading.coveredMs > 0 && table.scriptMs > reading.coveredMs) {
45624
45831
  notes.push(`scriptMs, ${table.scriptMs}ms, is more than the ${reading.coveredMs}ms the profile covers.${table.superCallers > 0 ? " The super calls above, counted twice, can account for some or all of that; whatever they do not is" : " One thread cannot spend more time than passes, so that is"} functions running on several threads at once, through WorkerThreadPool or Thread. The engine adds every thread's time to the same function and does not say which thread a call ran on, so a function's selfMs is summed over every thread it ran on rather than taken out of frames, and a function that waits for the others, through wait_for_group_task_completion or wait_to_finish, has the wait in its own selfMs as well.`);
45625
45832
  }
45833
+ if (table.addonFunctions > 0) {
45834
+ notes.push(`The gdharness runtime addon's own functions took ${table.addonMs}ms of self time over ${table.addonFunctions === 1 ? "one function" : `${table.addonFunctions} functions`}, answering tools such as runtime_invoke and sending the profile. They are left out of functions, functionsCounted and scriptMs and summed under runtimeAddonMs, since they are not the game's work, though that time was spent in the game's frames.`);
45835
+ }
45626
45836
  notes.push(...cappedNotes(totals));
45627
45837
  if (totals.unreadable > 0) {
45628
45838
  notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
@@ -45637,6 +45847,8 @@ function profileAnswer(reading, limit) {
45637
45847
  scriptMs: table.scriptMs,
45638
45848
  perCallOverheadUs: table.perCallUs ?? undefined,
45639
45849
  functionsCounted: table.counted,
45850
+ runtimeAddonMs: table.addonFunctions > 0 ? table.addonMs : undefined,
45851
+ runtimeAddonFunctions: table.addonFunctions > 0 ? table.addonFunctions : undefined,
45640
45852
  omitted: table.omitted > 0 ? table.omitted : undefined,
45641
45853
  functions: table.functions,
45642
45854
  callers: reading.callersOf === undefined ? undefined : callersAnswer(totals, reading.callersOf),
@@ -45722,7 +45934,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
45722
45934
  }
45723
45935
  function gdUnitFailsOnScriptErrors(projectPath) {
45724
45936
  try {
45725
- return settingKeys(readFileSync26(join31(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
45937
+ return settingKeys(readFileSync27(join32(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
45726
45938
  } catch {
45727
45939
  return true;
45728
45940
  }
@@ -45737,7 +45949,7 @@ function uncaughtNote(uncaught) {
45737
45949
  }
45738
45950
  function elsewhereIn(projectPath, asked) {
45739
45951
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
45740
- if (wanted === "" || existsSync24(join31(projectPath, wanted))) {
45952
+ if (wanted === "" || existsSync25(join32(projectPath, wanted))) {
45741
45953
  return "";
45742
45954
  }
45743
45955
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -45840,7 +46052,7 @@ function isDirectory(path) {
45840
46052
  }
45841
46053
  }
45842
46054
  function scriptsUnder(directory, prefix) {
45843
- if (existsSync24(join31(directory, ".gdignore"))) {
46055
+ if (existsSync25(join32(directory, ".gdignore"))) {
45844
46056
  return [];
45845
46057
  }
45846
46058
  const found = [];
@@ -45850,29 +46062,29 @@ function scriptsUnder(directory, prefix) {
45850
46062
  }
45851
46063
  const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
45852
46064
  if (entry.isDirectory()) {
45853
- found.push(...scriptsUnder(join31(directory, entry.name), path));
46065
+ found.push(...scriptsUnder(join32(directory, entry.name), path));
45854
46066
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
45855
46067
  found.push(path);
45856
46068
  }
45857
46069
  }
45858
46070
  return found;
45859
46071
  }
45860
- function hasMainScene(projectFile) {
45861
- const scene = parseProjectGodot(readFileSync26(projectFile, "utf8"))["application"]?.["run/main_scene"];
45862
- return typeof scene === "string" && scene !== "";
46072
+ function mainSceneOf(projectFile) {
46073
+ const scene = parseProjectGodot(readFileSync27(projectFile, "utf8"))["application"]?.["run/main_scene"];
46074
+ return typeof scene === "string" && scene !== "" ? scene : null;
45863
46075
  }
45864
46076
  function runtimeAddonAnnouncesEditors(projectPath) {
45865
46077
  if (projectPath === null) {
45866
46078
  return false;
45867
46079
  }
45868
46080
  try {
45869
- return readFileSync26(join31(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
46081
+ return readFileSync27(join32(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
45870
46082
  } catch {
45871
46083
  return false;
45872
46084
  }
45873
46085
  }
45874
46086
  function editorPlaysHeadless(projectFile) {
45875
- const runArgs = parseProjectGodot(readFileSync26(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
46087
+ const runArgs = parseProjectGodot(readFileSync27(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
45876
46088
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
45877
46089
  }
45878
46090
  function camelCased(params) {
@@ -45891,7 +46103,7 @@ function needsItsOwnProcess(asked) {
45891
46103
  class GodotServer {
45892
46104
  stdio = null;
45893
46105
  locator = new GodotLocator;
45894
- operationsScript = join31(__dirname2, "godot", "operations", "godot_operations.gd");
46106
+ operationsScript = join32(__dirname2, "godot", "operations", "godot_operations.gd");
45895
46107
  godotBridge;
45896
46108
  tools = buildToolDefinitions();
45897
46109
  activeProcess = null;
@@ -46152,14 +46364,14 @@ class GodotServer {
46152
46364
  }
46153
46365
  noteTheStart() {
46154
46366
  const cwd = process.cwd();
46155
- const mcpJson = join31(cwd, ".mcp.json");
46367
+ const mcpJson = join32(cwd, ".mcp.json");
46156
46368
  const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
46157
46369
  const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
46158
46370
  writeLifecycle("started", {
46159
46371
  argv: process.argv.slice(1),
46160
46372
  cwd,
46161
46373
  project: this.ownProject,
46162
- mcpJson: existsSync24(mcpJson) ? mcpJson : null
46374
+ mcpJson: existsSync25(mcpJson) ? mcpJson : null
46163
46375
  });
46164
46376
  if (predecessor !== null) {
46165
46377
  writeLifecycle("replaced", { project: this.ownProject, predecessor });
@@ -46300,7 +46512,7 @@ class GodotServer {
46300
46512
  return checked.response;
46301
46513
  }
46302
46514
  const named = args["projectPath"];
46303
- if (typeof named === "string" && existsSync24(join31(named, "project.godot"))) {
46515
+ if (typeof named === "string" && existsSync25(join32(named, "project.godot"))) {
46304
46516
  this.lastProjectPath = named;
46305
46517
  }
46306
46518
  this.updates.refresh();
@@ -46336,7 +46548,7 @@ class GodotServer {
46336
46548
  let notSaved;
46337
46549
  try {
46338
46550
  savedAt = answer.content.map((item, index) => {
46339
- const file = join31(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
46551
+ const file = join32(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
46340
46552
  writeFileSync18(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
46341
46553
  return file;
46342
46554
  });
@@ -46954,8 +47166,8 @@ class GodotServer {
46954
47166
  if (path === undefined) {
46955
47167
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
46956
47168
  }
46957
- const file = join31(path, "project.godot");
46958
- if (!existsSync24(file)) {
47169
+ const file = join32(path, "project.godot");
47170
+ if (!existsSync25(file)) {
46959
47171
  return {
46960
47172
  ok: false,
46961
47173
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -47615,7 +47827,7 @@ class GodotServer {
47615
47827
  if (!engine.ok) {
47616
47828
  return engine.response;
47617
47829
  }
47618
- const application = parseProjectGodot(readFileSync26(project.value.file, "utf8"))["application"] ?? {};
47830
+ const application = parseProjectGodot(readFileSync27(project.value.file, "utf8"))["application"] ?? {};
47619
47831
  const name = application["config/name"];
47620
47832
  const mainScene = application["run/main_scene"];
47621
47833
  const info = {
@@ -47713,7 +47925,7 @@ class GodotServer {
47713
47925
  const exportArgs = [
47714
47926
  "--headless",
47715
47927
  "--log-file",
47716
- join31(exportLogs, "engine.log"),
47928
+ join32(exportLogs, "engine.log"),
47717
47929
  "--path",
47718
47930
  project.value.path,
47719
47931
  debug ? "--export-debug" : "--export-release",
@@ -47721,7 +47933,7 @@ class GodotServer {
47721
47933
  output.absolutePath
47722
47934
  ];
47723
47935
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
47724
- const writtenAt = () => existsSync24(output.absolutePath) ? statSync14(output.absolutePath).mtimeMs : null;
47936
+ const writtenAt = () => existsSync25(output.absolutePath) ? statSync14(output.absolutePath).mtimeMs : null;
47725
47937
  const caches = editorCacheDirectory(engine.value);
47726
47938
  const exportOnce = async () => {
47727
47939
  const before = writtenAt();
@@ -47804,10 +48016,10 @@ class GodotServer {
47804
48016
  if (movie !== null) {
47805
48017
  return this.createErrorResponse(movie);
47806
48018
  }
47807
- const missing = paths.value.filter((path) => !existsSync24(join31(project.value.path, path.slice("res://".length))));
48019
+ const missing = paths.value.filter((path) => !existsSync25(join32(project.value.path, path.slice("res://".length))));
47808
48020
  const asked = paths.value.filter((path) => !missing.includes(path));
47809
48021
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
47810
- const noRunner = !existsSync24(join31(project.value.path, runner));
48022
+ const noRunner = !existsSync25(join32(project.value.path, runner));
47811
48023
  if (asked.length === 0) {
47812
48024
  const verdict = `nothing at ${missing.join(" and ")}`;
47813
48025
  const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
@@ -47839,6 +48051,7 @@ class GodotServer {
47839
48051
  if (!engine.ok) {
47840
48052
  return engine.response;
47841
48053
  }
48054
+ const began = Date.now();
47842
48055
  const classes = await this.rebuildClassCache(project.value.path);
47843
48056
  if (!classes.ok) {
47844
48057
  return this.answer(classes);
@@ -47877,35 +48090,51 @@ class GodotServer {
47877
48090
  lastSaid = Date.now();
47878
48091
  });
47879
48092
  const reporting = this.reportTestProgress(run.log);
48093
+ const autoloaded = scriptsAutoloaded(project.value.path);
47880
48094
  let silentForMs = 0;
48095
+ const stopped = { for: null };
47881
48096
  const timedOut = await new Promise((resolve) => {
48097
+ let read = 0;
48098
+ const watching = setInterval(() => {
48099
+ const entries = run.log.all;
48100
+ for (const entry of entries.slice(read)) {
48101
+ stopped.for ??= autoloadNotCompiled(entry, autoloaded);
48102
+ }
48103
+ read = entries.length;
48104
+ if (stopped.for !== null) {
48105
+ clearInterval(watching);
48106
+ run.process.kill();
48107
+ }
48108
+ }, 100);
47882
48109
  const timer = setTimeout(() => {
48110
+ clearInterval(watching);
47883
48111
  silentForMs = Date.now() - lastSaid;
47884
48112
  run.process.kill();
47885
48113
  resolve(true);
47886
48114
  }, timeoutMs);
47887
- run.process.once("exit", () => {
48115
+ const ended = () => {
48116
+ clearInterval(watching);
47888
48117
  clearTimeout(timer);
47889
48118
  resolve(false);
47890
- });
47891
- run.process.once("error", () => {
47892
- clearTimeout(timer);
47893
- resolve(false);
47894
- });
48119
+ };
48120
+ run.process.once("exit", ended);
48121
+ run.process.once("error", ended);
47895
48122
  });
47896
48123
  clearInterval(reporting);
47897
- if (timedOut && run.process.pid !== undefined) {
48124
+ if ((timedOut || stopped.for !== null) && run.process.pid !== undefined) {
47898
48125
  await untilGone([run.process.pid]);
47899
48126
  }
47900
48127
  await run.settled;
47901
- const reportsDir = join31(project.value.path, ".godot", ...ours.split("/"));
48128
+ const tookMs = Date.now() - began;
48129
+ const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
48130
+ const reportsDir = join32(project.value.path, ".godot", ...ours.split("/"));
47902
48131
  let report = null;
47903
48132
  let reportProblem = null;
47904
48133
  try {
47905
- const written = existsSync24(reportsDir) ? readdirSync14(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
48134
+ const written = existsSync25(reportsDir) && notCompiled === null ? readdirSync14(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
47906
48135
  const newest = written.at(-1);
47907
48136
  if (newest !== undefined) {
47908
- report = parseJUnit(readFileSync26(join31(reportsDir, newest, "results.xml"), "utf8"));
48137
+ report = parseJUnit(readFileSync27(join32(reportsDir, newest, "results.xml"), "utf8"));
47909
48138
  }
47910
48139
  } catch (error) {
47911
48140
  reportProblem = errorMessage(error);
@@ -47937,14 +48166,14 @@ class GodotServer {
47937
48166
  limit: Number.POSITIVE_INFINITY
47938
48167
  }).entries;
47939
48168
  const said = printed.map((entry) => entry.text);
47940
- const nothingRan = timedOut ? null : whyNoReport(said, asked.join(" and "));
47941
- const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
48169
+ const nothingRan = timedOut || notCompiled !== null ? null : whyNoReport(said, asked.join(" and "));
48170
+ const cutShort = timedOut && notCompiled === null ? timedOutVerdict(timeoutMs, silentForMs, repeatingScriptError(run.log.all, (entry) => run.log.raisedByScript(entry))) : null;
47942
48171
  const hung = cutShort?.hung ?? false;
47943
- const printedErrors = timedOut || exitCode !== 105 ? [] : scriptErrorsPrinted(said);
48172
+ const printedErrors = notCompiled !== null ? compileErrorsIn(run.log.all) : timedOut || exitCode !== 105 ? [] : scriptErrorsPrinted(said);
47944
48173
  const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
47945
48174
  const scriptErrors = [...declaring.origins, ...printedErrors];
47946
48175
  const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
47947
- const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (uncaught.length > 0 ? `${uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`} that failed no case` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? howItExited(run);
48176
+ const ranAs = notCompiled !== null ? `the autoload ${notCompiled.name} (${notCompiled.path}) did not compile, so no result counts` : cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (uncaught.length > 0 ? `${uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`} that failed no case` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? howItExited(run);
47948
48177
  const nothingAt = missing.join(" and ");
47949
48178
  const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
47950
48179
  const missingNote = missing.length === 0 ? undefined : `Nothing at ${nothingAt}, so ${missing.length === 1 ? "it was" : "they were"} left out and only ${asked.join(", ")} ran; a run of fewer paths than were asked for is not called a pass.${elsewhereIn(project.value.path, missing[0] ?? "")}`;
@@ -47972,7 +48201,7 @@ class GodotServer {
47972
48201
  }),
47973
48202
  ...running === null ? {} : { suiteRunning: running }
47974
48203
  };
47975
- const said = nothingRan !== null ? `No tests ran: ${ranAs}.` : scriptErrors.length > 0 ? scriptErrorsNote(printedErrors, declaring) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${ranAs}.` : `The test run wrote no report (${ranAs}).${finishedBeforeTheEnd(finished.length, failing.length, running)}`;
48204
+ const said = notCompiled !== null ? autoloadNotCompiledNote(notCompiled, foldedScriptErrors(scriptErrors), stopped.for !== null) : nothingRan !== null ? `No tests ran: ${ranAs}.` : scriptErrors.length > 0 ? scriptErrorsNote(printedErrors, declaring) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${ranAs}.` : `The test run wrote no report (${ranAs}).${finishedBeforeTheEnd(finished.length, failing.length, running)}`;
47976
48205
  const note = missingNote === undefined ? said : `${said} ${missingNote}`;
47977
48206
  if (callSignal()?.aborted === true) {
47978
48207
  keepCutTestRun(project.value.path, { cutAt: new Date().toISOString(), paths: asked, ...atTheEnd });
@@ -47986,6 +48215,7 @@ class GodotServer {
47986
48215
  passed: false,
47987
48216
  verdict,
47988
48217
  tests: 0,
48218
+ tookMs,
47989
48219
  exitCode,
47990
48220
  exitSignal: run.exitSignal ?? undefined,
47991
48221
  hung,
@@ -48043,6 +48273,7 @@ class GodotServer {
48043
48273
  errors: report.errors,
48044
48274
  skipped: report.skipped,
48045
48275
  time: report.time,
48276
+ tookMs,
48046
48277
  failed,
48047
48278
  failedWithoutDetail: briefed > 0 ? briefed : undefined,
48048
48279
  hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
@@ -48271,11 +48502,11 @@ class GodotServer {
48271
48502
  const cached = cachedClasses(projectPath);
48272
48503
  const sourceOf = (resourcePath) => {
48273
48504
  const contained = resolveWithinProject(projectPath, resourcePath);
48274
- if (!contained.ok || !existsSync24(contained.absolutePath)) {
48505
+ if (!contained.ok || !existsSync25(contained.absolutePath)) {
48275
48506
  return null;
48276
48507
  }
48277
48508
  try {
48278
- return readFileSync26(contained.absolutePath, "utf8");
48509
+ return readFileSync27(contained.absolutePath, "utf8");
48279
48510
  } catch {
48280
48511
  return null;
48281
48512
  }
@@ -48365,11 +48596,11 @@ class GodotServer {
48365
48596
  }
48366
48597
  sourceOfClass(projectPath, resourcePath) {
48367
48598
  const contained = resolveWithinProject(projectPath, resourcePath);
48368
- if (!contained.ok || !existsSync24(contained.absolutePath)) {
48599
+ if (!contained.ok || !existsSync25(contained.absolutePath)) {
48369
48600
  return null;
48370
48601
  }
48371
48602
  try {
48372
- return readFileSync26(contained.absolutePath, "utf8");
48603
+ return readFileSync27(contained.absolutePath, "utf8");
48373
48604
  } catch {
48374
48605
  return null;
48375
48606
  }
@@ -48527,7 +48758,7 @@ class GodotServer {
48527
48758
  ...Array.isArray(stack) ? {
48528
48759
  stack: stack.map((frame) => ({
48529
48760
  ...frame,
48530
- ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join31(projectPath, frame["script"].slice("res://".length)) } : {}
48761
+ ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join32(projectPath, frame["script"].slice("res://".length)) } : {}
48531
48762
  }))
48532
48763
  } : {}
48533
48764
  });
@@ -48806,7 +49037,7 @@ class GodotServer {
48806
49037
  return null;
48807
49038
  }
48808
49039
  try {
48809
- return settingKeys(readFileSync26(join31(projectPath, "project.godot"), "utf8"));
49040
+ return settingKeys(readFileSync27(join32(projectPath, "project.godot"), "utf8"));
48810
49041
  } catch {
48811
49042
  return null;
48812
49043
  }
@@ -49149,7 +49380,7 @@ class GodotServer {
49149
49380
  if (!asked.ok) {
49150
49381
  return asked.response;
49151
49382
  }
49152
- if (exported === null && !sceneToRun && !hasMainScene(project.value.file)) {
49383
+ if (exported === null && !sceneToRun && mainSceneOf(project.value.file) === null) {
49153
49384
  return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
49154
49385
  "Pass scene to run one scene",
49155
49386
  "Choose the main scene with project_settings set_main_scene"
@@ -49226,6 +49457,7 @@ class GodotServer {
49226
49457
  alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
49227
49458
  pid: started.pid,
49228
49459
  executable: exported?.ok === true ? exported.relativePath : undefined,
49460
+ scene: exported !== null ? undefined : sceneArgument === null ? mainSceneOf(project.value.file) : `res://${sceneArgument}`,
49229
49461
  arguments: cmdArgs,
49230
49462
  argsNote: engineFlagsNote(given.value),
49231
49463
  transcript: started.transcript,
@@ -49243,7 +49475,7 @@ class GodotServer {
49243
49475
  }
49244
49476
  async runtimeUp(projectPath, before, budget, withArgs) {
49245
49477
  const budgetMs = budget.ms;
49246
- if (!existsSync24(join31(projectPath, RUNTIME_AUTOLOAD.path))) {
49478
+ if (!existsSync25(join32(projectPath, RUNTIME_AUTOLOAD.path))) {
49247
49479
  return runtimeVerdict(null, {
49248
49480
  addon: false,
49249
49481
  budgetMs,
@@ -49523,7 +49755,7 @@ class GodotServer {
49523
49755
  return 0;
49524
49756
  }
49525
49757
  try {
49526
- return reportAlreadyInTranscript(readFileSync26(game.transcript), readFileSync26(report));
49758
+ return reportAlreadyInTranscript(readFileSync27(game.transcript), readFileSync27(report));
49527
49759
  } catch (error) {
49528
49760
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
49529
49761
  return 0;
@@ -50225,7 +50457,7 @@ class GodotServer {
50225
50457
  };
50226
50458
  const going = await this.runStillGoing(run);
50227
50459
  const projectPath = run.projectPath;
50228
- const projectGone = projectPath !== null && !existsSync24(join31(projectPath, "project.godot"));
50460
+ const projectGone = projectPath !== null && !existsSync25(join32(projectPath, "project.godot"));
50229
50461
  if (projectGone && callersName !== undefined) {
50230
50462
  return this.createErrorResponse(`callersOf reads the project's scripts, and the run's project at ${projectPath} has no project.godot any more.`, ["editor_run profile without callersOf answers with the profile itself"]);
50231
50463
  }
@@ -50572,7 +50804,7 @@ class GodotServer {
50572
50804
  notes.push(endedWithoutACode({
50573
50805
  throughEditor: run.throughEditor,
50574
50806
  announced: announced !== undefined || run.announcedPid !== undefined,
50575
- couldAnnounce: run.projectPath !== null && existsSync24(join31(run.projectPath, RUNTIME_AUTOLOAD.path)),
50807
+ couldAnnounce: run.projectPath !== null && existsSync25(join32(run.projectPath, RUNTIME_AUTOLOAD.path)),
50576
50808
  endedHere: typeof run.endedHere === "string",
50577
50809
  pickedUp: run.pickedUp === true
50578
50810
  }));
@@ -51202,7 +51434,7 @@ class GodotServer {
51202
51434
  if (!scriptPath.ok) {
51203
51435
  return this.createErrorResponse(scriptPath.reason);
51204
51436
  }
51205
- if (!existsSync24(scriptPath.absolutePath)) {
51437
+ if (!existsSync25(scriptPath.absolutePath)) {
51206
51438
  return this.createErrorResponse(`Script file does not exist: ${scriptPath.absolutePath}`);
51207
51439
  }
51208
51440
  const asked = readNonEmptyString(args, "newScriptPath");
@@ -51616,7 +51848,7 @@ class GodotServer {
51616
51848
  }
51617
51849
  const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
51618
51850
  const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
51619
- if (declared.test(readFileSync26(scriptPath, "utf8"))) {
51851
+ if (declared.test(readFileSync27(scriptPath, "utf8"))) {
51620
51852
  return null;
51621
51853
  }
51622
51854
  const checked = await this.classesTheEditorCannotSee(args);
@@ -51784,7 +52016,7 @@ class GodotServer {
51784
52016
  async ownRunOnItsWay(projectPath, budgetMs) {
51785
52017
  const run = await this.currentRun();
51786
52018
  const project = projectPath ?? run?.projectPath ?? null;
51787
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync24(join31(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
52019
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync25(join32(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
51788
52020
  return null;
51789
52021
  }
51790
52022
  const already = runtimesAnnounced().running;
@@ -51874,7 +52106,7 @@ class GodotServer {
51874
52106
  }
51875
52107
  }
51876
52108
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
51877
- const screenshotPath = screenshotDir ? join31(screenshotDir, "capture.png") : null;
52109
+ const screenshotPath = screenshotDir ? join32(screenshotDir, "capture.png") : null;
51878
52110
  const staleNote = this.runtimeStaleNote(choice.endpoint);
51879
52111
  const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
51880
52112
  const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
@@ -51893,8 +52125,10 @@ ${staleNote}`);
51893
52125
  }
51894
52126
  this.callsOnGames.add(onGame);
51895
52127
  const alongside = () => onGame.alongside.size === 0 ? {} : { ranAlongside: [...onGame.alongside], alongsideNote: ALONGSIDE_NOTE };
52128
+ let savedLater = false;
51896
52129
  try {
51897
- const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
52130
+ const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, expectsScreenshot ? () => discard(screenshotDir) : "keep");
52131
+ savedLater = !reply.ok && reply.stillWithTheGame === true;
51898
52132
  if (!reply.ok && reply.requestId !== undefined) {
51899
52133
  return this.jsonTextResponse({
51900
52134
  pending: true,
@@ -51940,12 +52174,14 @@ Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}${staleNote === undefine
51940
52174
 
51941
52175
  ${staleNote}`}`
51942
52176
  },
51943
- { type: "image", data: readFileSync26(screenshotPath).toString("base64"), mimeType: "image/png" }
52177
+ { type: "image", data: readFileSync27(screenshotPath).toString("base64"), mimeType: "image/png" }
51944
52178
  ]
51945
52179
  };
51946
52180
  } finally {
51947
52181
  this.callsOnGames.delete(onGame);
51948
- discard(screenshotDir);
52182
+ if (!savedLater) {
52183
+ discard(screenshotDir);
52184
+ }
51949
52185
  }
51950
52186
  }
51951
52187
  handleLateAnswer(args) {
@@ -52055,12 +52291,13 @@ async function runGodotServer() {
52055
52291
  const server = new GodotServer;
52056
52292
  await server.run();
52057
52293
  }
52058
- 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, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, 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_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, 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, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
52294
+ 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, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, 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_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, 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, 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, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
52059
52295
  var init_server = __esm(() => {
52060
52296
  init_dist();
52061
52297
  init_stdio();
52062
52298
  init_alive();
52063
52299
  init_answer_json();
52300
+ init_autoload_failure();
52064
52301
  init_boot_note();
52065
52302
  init_breakpoint_note();
52066
52303
  init_bridge_announce();
@@ -52174,8 +52411,8 @@ var init_server = __esm(() => {
52174
52411
  init_bridge_announce();
52175
52412
  init_errors();
52176
52413
  init_godot_path();
52177
- import { existsSync as existsSync25 } from "node:fs";
52178
- import { join as join32, resolve as resolve8 } from "node:path";
52414
+ import { existsSync as existsSync26 } from "node:fs";
52415
+ import { join as join33, resolve as resolve8 } from "node:path";
52179
52416
 
52180
52417
  // src/harnesses.ts
52181
52418
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
@@ -53141,9 +53378,9 @@ init_setup();
53141
53378
  init_tool_definitions();
53142
53379
  init_tool_reference();
53143
53380
  import { existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync as readdirSync9, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
53144
- import { basename as basename6, dirname as dirname8, join as join14 } from "node:path";
53381
+ import { basename as basename6, dirname as dirname8, join as join15 } from "node:path";
53145
53382
  var SKILL_NAME = "gdharness";
53146
- var SHARED_SKILLS = join14(".agents", "skills");
53383
+ var SHARED_SKILLS = join15(".agents", "skills");
53147
53384
  function skillMarkdown(version) {
53148
53385
  return `---
53149
53386
  name: ${SKILL_NAME}
@@ -53368,27 +53605,27 @@ what they report while starting comes back under \`engine_messages_before_operat
53368
53605
  function skillFiles(version) {
53369
53606
  return new Map([
53370
53607
  ["SKILL.md", skillMarkdown(version)],
53371
- [join14("references", "tools.md"), renderToolsMarkdown()]
53608
+ [join15("references", "tools.md"), renderToolsMarkdown()]
53372
53609
  ]);
53373
53610
  }
53374
53611
  function skillDirectories(harnesses, projectPath, exists) {
53375
- const directories = [join14(projectPath, SHARED_SKILLS, SKILL_NAME)];
53612
+ const directories = [join15(projectPath, SHARED_SKILLS, SKILL_NAME)];
53376
53613
  for (const harness of harnesses) {
53377
53614
  if (harness.skills === undefined) {
53378
53615
  continue;
53379
53616
  }
53380
- const own = join14(projectPath, harness.skills.dir);
53381
- if ((!harness.skills.shared || exists(own)) && !directories.includes(join14(own, SKILL_NAME))) {
53382
- directories.push(join14(own, SKILL_NAME));
53617
+ const own = join15(projectPath, harness.skills.dir);
53618
+ if ((!harness.skills.shared || exists(own)) && !directories.includes(join15(own, SKILL_NAME))) {
53619
+ directories.push(join15(own, SKILL_NAME));
53383
53620
  }
53384
53621
  }
53385
53622
  return directories;
53386
53623
  }
53387
53624
  function everySkillDirectory(projectPath, harnesses) {
53388
- const directories = new Set([join14(projectPath, SHARED_SKILLS, SKILL_NAME)]);
53625
+ const directories = new Set([join15(projectPath, SHARED_SKILLS, SKILL_NAME)]);
53389
53626
  for (const harness of harnesses) {
53390
53627
  if (harness.skills !== undefined) {
53391
- directories.add(join14(projectPath, harness.skills.dir, SKILL_NAME));
53628
+ directories.add(join15(projectPath, harness.skills.dir, SKILL_NAME));
53392
53629
  }
53393
53630
  }
53394
53631
  return [...directories];
@@ -53405,7 +53642,7 @@ function removeSkill(directories, projectPath) {
53405
53642
  return removed;
53406
53643
  }
53407
53644
  function pruneEmpty(directory, projectPath) {
53408
- const shared = join14(projectPath, ".agents");
53645
+ const shared = join15(projectPath, ".agents");
53409
53646
  let path = directory;
53410
53647
  while (path !== projectPath && (basename6(path) === "skills" || path === shared)) {
53411
53648
  if (readdirSync9(path).length > 0) {
@@ -53421,7 +53658,7 @@ function writeSkill(directories, version) {
53421
53658
  const replaced = existsSync14(directory);
53422
53659
  rmSync6(directory, { recursive: true, force: true });
53423
53660
  for (const [name, contents] of files) {
53424
- const path = join14(directory, name);
53661
+ const path = join15(directory, name);
53425
53662
  mkdirSync5(dirname8(path), { recursive: true });
53426
53663
  writeFileSync7(path, contents, "utf8");
53427
53664
  }
@@ -53431,13 +53668,13 @@ function writeSkill(directories, version) {
53431
53668
 
53432
53669
  // src/version.ts
53433
53670
  import { readFileSync as readFileSync12 } from "node:fs";
53434
- import { dirname as dirname9, join as join15 } from "node:path";
53671
+ import { dirname as dirname9, join as join16 } from "node:path";
53435
53672
  import { fileURLToPath as fileURLToPath2 } from "node:url";
53436
53673
  function getLocalVersion() {
53437
53674
  const currentDirectory = dirname9(fileURLToPath2(import.meta.url));
53438
53675
  const packageCandidates = [
53439
- join15(currentDirectory, "..", "package.json"),
53440
- join15(currentDirectory, "..", "..", "package.json")
53676
+ join16(currentDirectory, "..", "package.json"),
53677
+ join16(currentDirectory, "..", "..", "package.json")
53441
53678
  ];
53442
53679
  for (const packagePath of packageCandidates) {
53443
53680
  try {
@@ -53459,7 +53696,7 @@ class UsageError extends Refusal {
53459
53696
  function projectArgument(at) {
53460
53697
  const named = args.slice(at).find((value) => !value.startsWith("--"));
53461
53698
  const projectPath = resolve8(named ?? ".");
53462
- if (!existsSync25(join32(projectPath, "project.godot"))) {
53699
+ if (!existsSync26(join33(projectPath, "project.godot"))) {
53463
53700
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
53464
53701
  }
53465
53702
  return projectPath;
@@ -53504,7 +53741,7 @@ async function registerRuntime(godot, projectPath) {
53504
53741
  return;
53505
53742
  }
53506
53743
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
53507
- if (!existsSync25(join32(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
53744
+ if (!existsSync26(join33(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
53508
53745
  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}`);
53509
53746
  }
53510
53747
  }
@@ -53636,7 +53873,7 @@ async function setup() {
53636
53873
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
53637
53874
  }
53638
53875
  if (!args.includes("--no-skill")) {
53639
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync25);
53876
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync26);
53640
53877
  for (const written of writeSkill(directories, getLocalVersion())) {
53641
53878
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
53642
53879
  }
@@ -53750,7 +53987,7 @@ async function upgrade() {
53750
53987
  moved.push(took.from);
53751
53988
  }
53752
53989
  }
53753
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync25), version)) {
53990
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync26), version)) {
53754
53991
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
53755
53992
  }
53756
53993
  console.log(`