gdharness 1.1.10 → 1.1.12
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 +117 -17
- package/build/godot/addons/gdharness_editor/bridge_client.gd +2 -2
- package/build/godot/addons/gdharness_editor/reading.gd +13 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +1 -1
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +1 -1
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +1 -1
- package/build/godot/addons/gdharness_runtime/error_report.gd +61 -15
- package/build/godot/addons/gdharness_runtime/reading.gd +13 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +13 -2
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +208 -18
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +4 -3
- package/build/godot/addons/gdharness_runtime/serialisation.gd +1 -1
- package/build/godot/operations/import_pipeline.gd +1 -1
- package/build/godot/operations/reading.gd +13 -0
- package/build/godot/operations/serialisation.gd +1 -1
- package/build/index.js +118 -15
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -17209,7 +17209,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17209
17209
|
says: {
|
|
17210
17210
|
type: "string",
|
|
17211
17211
|
ops: ["click"],
|
|
17212
|
-
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), 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. Several matches are refused with each one listed, and index picks one."
|
|
17212
|
+
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), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it, is not on screen. The matches are ranked the way a person picks what to press: a button before text that cannot be pressed, then a control whose whole text 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."
|
|
17213
17213
|
},
|
|
17214
17214
|
action: {
|
|
17215
17215
|
type: "string",
|
|
@@ -17247,7 +17247,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17247
17247
|
index: {
|
|
17248
17248
|
type: "number",
|
|
17249
17249
|
ops: ["click", "choose"],
|
|
17250
|
-
description: "click: which of several controls saying the words, counting from 0 in the order a refusal lists them. choose: the item to take, by where it is in the list, when text will not do."
|
|
17250
|
+
description: "click: which of several controls saying the words, counting from 0 in the order a refusal lists them, best matches first. choose: the item to take, by where it is in the list, when text will not do."
|
|
17251
17251
|
},
|
|
17252
17252
|
shift: { type: "boolean", ops: ["key"], description: "key: hold Shift with it. Default false." },
|
|
17253
17253
|
ctrl: { type: "boolean", ops: ["key"], description: "key: hold Ctrl with it. Default false." },
|
|
@@ -17340,7 +17340,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17340
17340
|
says: {
|
|
17341
17341
|
type: "string",
|
|
17342
17342
|
ops: ["until"],
|
|
17343
|
-
description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put.
|
|
17343
|
+
description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Matched the way runtime_inspect find matches says: case-insensitive, a plain word anywhere in the text, a pattern with * or ? a glob over the whole of it; on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took."
|
|
17344
17344
|
},
|
|
17345
17345
|
includeHidden: {
|
|
17346
17346
|
type: "boolean",
|
|
@@ -32197,6 +32197,28 @@ function errorReportOf(pid, directories = runtimeDirectories()) {
|
|
|
32197
32197
|
}
|
|
32198
32198
|
return null;
|
|
32199
32199
|
}
|
|
32200
|
+
function consoleBoundaryAt(bytes) {
|
|
32201
|
+
const wanted = Buffer.from(CONSOLE_BOUNDARY);
|
|
32202
|
+
for (let at = bytes.indexOf(wanted);at !== -1; at = bytes.indexOf(wanted, at + 1)) {
|
|
32203
|
+
if (at === 0 || bytes[at - 1] === 10) {
|
|
32204
|
+
return at;
|
|
32205
|
+
}
|
|
32206
|
+
}
|
|
32207
|
+
return -1;
|
|
32208
|
+
}
|
|
32209
|
+
function reportAlreadyInTranscript(transcript, report) {
|
|
32210
|
+
const boundary = consoleBoundaryAt(report);
|
|
32211
|
+
if (boundary === -1) {
|
|
32212
|
+
return 0;
|
|
32213
|
+
}
|
|
32214
|
+
const inTranscript = consoleBoundaryAt(transcript);
|
|
32215
|
+
if (inTranscript === -1) {
|
|
32216
|
+
return boundary;
|
|
32217
|
+
}
|
|
32218
|
+
const copied = transcript.subarray(inTranscript);
|
|
32219
|
+
const end = boundary + copied.length;
|
|
32220
|
+
return end <= report.length && report.subarray(boundary, end).equals(copied) ? end : report.length;
|
|
32221
|
+
}
|
|
32200
32222
|
function processAlive(pid) {
|
|
32201
32223
|
try {
|
|
32202
32224
|
process.kill(pid, 0);
|
|
@@ -32641,7 +32663,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
|
|
|
32641
32663
|
});
|
|
32642
32664
|
});
|
|
32643
32665
|
}
|
|
32644
|
-
var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, 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, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1, LATE_ANSWER_MS, LATE_KEPT = 50, lateAnswers;
|
|
32666
|
+
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, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1, LATE_ANSWER_MS, LATE_KEPT = 50, lateAnswers;
|
|
32645
32667
|
var init_runtime_client = __esm(() => {
|
|
32646
32668
|
init_paths();
|
|
32647
32669
|
init_process_children();
|
|
@@ -39230,6 +39252,7 @@ class GodotServer {
|
|
|
39230
39252
|
bridgeStartupError = null;
|
|
39231
39253
|
bridgeRetry = null;
|
|
39232
39254
|
successorWatch = null;
|
|
39255
|
+
consoleWatch = null;
|
|
39233
39256
|
lastProjectPath = null;
|
|
39234
39257
|
shutdownInitiated = false;
|
|
39235
39258
|
launchedEditor = null;
|
|
@@ -39402,10 +39425,30 @@ class GodotServer {
|
|
|
39402
39425
|
this.successorWatch = null;
|
|
39403
39426
|
}
|
|
39404
39427
|
}
|
|
39428
|
+
watchThePlayedConsole() {
|
|
39429
|
+
if (this.consoleWatch !== null) {
|
|
39430
|
+
return;
|
|
39431
|
+
}
|
|
39432
|
+
const watch = setInterval(() => {
|
|
39433
|
+
const run = this.activeProcess;
|
|
39434
|
+
if (run?.throughEditor !== true || run.transcript === null) {
|
|
39435
|
+
clearInterval(watch);
|
|
39436
|
+
this.consoleWatch = null;
|
|
39437
|
+
return;
|
|
39438
|
+
}
|
|
39439
|
+
this.drainErrorReport(run);
|
|
39440
|
+
}, CONSOLE_WATCH_MS);
|
|
39441
|
+
watch.unref();
|
|
39442
|
+
this.consoleWatch = watch;
|
|
39443
|
+
}
|
|
39405
39444
|
async cleanup() {
|
|
39406
39445
|
this.logDebug("Cleaning up resources");
|
|
39407
39446
|
this.stopTryingTheBridge();
|
|
39408
39447
|
this.stopWatchingForASuccessor();
|
|
39448
|
+
if (this.consoleWatch !== null) {
|
|
39449
|
+
clearInterval(this.consoleWatch);
|
|
39450
|
+
this.consoleWatch = null;
|
|
39451
|
+
}
|
|
39409
39452
|
withdrawBridge(this.announcedAt);
|
|
39410
39453
|
this.announcedAt = null;
|
|
39411
39454
|
this.activeProcess = null;
|
|
@@ -40973,7 +41016,16 @@ class GodotServer {
|
|
|
40973
41016
|
}
|
|
40974
41017
|
writeToTranscript(line) {
|
|
40975
41018
|
const run = this.activeProcess;
|
|
40976
|
-
if (run?.throughEditor !== true
|
|
41019
|
+
if (run?.throughEditor !== true) {
|
|
41020
|
+
return;
|
|
41021
|
+
}
|
|
41022
|
+
if (run.consoleFromReport === true || line.startsWith(CONSOLE_BOUNDARY)) {
|
|
41023
|
+
this.drainErrorReport(run, line);
|
|
41024
|
+
if (run.consoleFromReport === true) {
|
|
41025
|
+
return;
|
|
41026
|
+
}
|
|
41027
|
+
}
|
|
41028
|
+
if (run.transcript === null) {
|
|
40977
41029
|
return;
|
|
40978
41030
|
}
|
|
40979
41031
|
try {
|
|
@@ -41526,6 +41578,7 @@ class GodotServer {
|
|
|
41526
41578
|
seenPlaying: readBoolean(playAnswer, "playing") === true
|
|
41527
41579
|
};
|
|
41528
41580
|
this.activeProcess = played;
|
|
41581
|
+
this.watchThePlayedConsole();
|
|
41529
41582
|
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
41530
41583
|
sweepTranscripts();
|
|
41531
41584
|
return this.jsonTextResponse({
|
|
@@ -41543,7 +41596,7 @@ class GodotServer {
|
|
|
41543
41596
|
reason: one.reason
|
|
41544
41597
|
})),
|
|
41545
41598
|
runtime: await this.runtimeUp(projectPath, alreadyPlaying, runtimeWaitMs, false),
|
|
41546
|
-
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it." + endedToStartThis(ended)
|
|
41599
|
+
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter and the runtime's report of it, and editor_run stop ends it." + endedToStartThis(ended)
|
|
41547
41600
|
});
|
|
41548
41601
|
}
|
|
41549
41602
|
drainEditorOutput(game) {
|
|
@@ -41555,26 +41608,35 @@ class GodotServer {
|
|
|
41555
41608
|
return;
|
|
41556
41609
|
}
|
|
41557
41610
|
if (game.transcript === null) {
|
|
41558
|
-
|
|
41559
|
-
|
|
41611
|
+
const heard = this.dapClient.getOutput(true);
|
|
41612
|
+
if (game.consoleFromReport !== true) {
|
|
41613
|
+
for (const line of heard) {
|
|
41614
|
+
game.log.append("stdout", line.endsWith(`
|
|
41560
41615
|
`) ? line : `${line}
|
|
41561
41616
|
`);
|
|
41617
|
+
}
|
|
41562
41618
|
}
|
|
41563
41619
|
}
|
|
41564
|
-
if (!this.dapClient.isConnected()) {
|
|
41620
|
+
if (!this.dapClient.isConnected() && game.consoleFromReport !== true) {
|
|
41565
41621
|
game.consoleLost = true;
|
|
41566
41622
|
}
|
|
41567
41623
|
this.recordWhatItBrokeOn(game);
|
|
41568
41624
|
}
|
|
41569
|
-
drainErrorReport(game) {
|
|
41625
|
+
drainErrorReport(game, heard) {
|
|
41626
|
+
if (game.errorReport === null) {
|
|
41627
|
+
return;
|
|
41628
|
+
}
|
|
41570
41629
|
if (game.errorReport === undefined) {
|
|
41571
|
-
const its = this.announcedPidOf(game);
|
|
41630
|
+
const its = this.announcedPidOf(game) ?? (heard?.startsWith(CONSOLE_BOUNDARY) === true ? this.announcedPidNamedBy(game, heard) : undefined);
|
|
41572
41631
|
const found = its === undefined ? null : errorReportOf(its);
|
|
41573
41632
|
if (found === null) {
|
|
41633
|
+
if (heard?.startsWith(CONSOLE_BOUNDARY) === true) {
|
|
41634
|
+
game.errorReport = null;
|
|
41635
|
+
}
|
|
41574
41636
|
return;
|
|
41575
41637
|
}
|
|
41576
41638
|
game.errorReport = found;
|
|
41577
|
-
game.errorReportOffset =
|
|
41639
|
+
game.errorReportOffset = this.reportTakenBefore(game, found);
|
|
41578
41640
|
}
|
|
41579
41641
|
let handle;
|
|
41580
41642
|
try {
|
|
@@ -41590,8 +41652,22 @@ class GodotServer {
|
|
|
41590
41652
|
}
|
|
41591
41653
|
const buffer = Buffer.alloc(size - from);
|
|
41592
41654
|
const read = readSync2(handle, buffer, 0, buffer.length, from);
|
|
41593
|
-
|
|
41594
|
-
|
|
41655
|
+
let reported = buffer.subarray(0, read);
|
|
41656
|
+
if (game.consoleFromReport !== true) {
|
|
41657
|
+
const boundary = consoleBoundaryAt(reported);
|
|
41658
|
+
if (boundary !== -1) {
|
|
41659
|
+
game.boundaryInReportSince ??= Date.now();
|
|
41660
|
+
if (heard?.startsWith(CONSOLE_BOUNDARY) === true || game.pickedUpPlaying === true || this.dapClient?.isConnected() !== true || Date.now() - game.boundaryInReportSince >= BOUNDARY_WAIT_MS) {
|
|
41661
|
+
game.consoleFromReport = true;
|
|
41662
|
+
} else {
|
|
41663
|
+
reported = reported.subarray(0, boundary);
|
|
41664
|
+
}
|
|
41665
|
+
}
|
|
41666
|
+
}
|
|
41667
|
+
game.errorReportOffset = from + reported.length;
|
|
41668
|
+
if (reported.length === 0) {
|
|
41669
|
+
return;
|
|
41670
|
+
}
|
|
41595
41671
|
if (game.transcript !== null) {
|
|
41596
41672
|
appendFileSync(game.transcript, reported);
|
|
41597
41673
|
} else {
|
|
@@ -41601,9 +41677,32 @@ class GodotServer {
|
|
|
41601
41677
|
closeSync2(handle);
|
|
41602
41678
|
}
|
|
41603
41679
|
}
|
|
41680
|
+
announcedPidNamedBy(run, line) {
|
|
41681
|
+
const named = /runtime-(\d+)\.json$/.exec(line.trim())?.[1];
|
|
41682
|
+
if (named === undefined) {
|
|
41683
|
+
return;
|
|
41684
|
+
}
|
|
41685
|
+
const before = run.announcedBefore ?? new Set;
|
|
41686
|
+
const theOne = this.announcedOfTheRunsProject(run, runtimesAnnounced().running).find((one) => one.pid === Number(named) && !before.has(one.pid) && this.isTheGameOf(run, one, run.projectPath));
|
|
41687
|
+
if (theOne !== undefined) {
|
|
41688
|
+
this.tie(run, theOne);
|
|
41689
|
+
}
|
|
41690
|
+
return run.announcedPid;
|
|
41691
|
+
}
|
|
41692
|
+
reportTakenBefore(game, report) {
|
|
41693
|
+
if (game.pickedUpPlaying !== true || game.transcript === null) {
|
|
41694
|
+
return 0;
|
|
41695
|
+
}
|
|
41696
|
+
try {
|
|
41697
|
+
return reportAlreadyInTranscript(readFileSync19(game.transcript), readFileSync19(report));
|
|
41698
|
+
} catch (error) {
|
|
41699
|
+
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
41700
|
+
return 0;
|
|
41701
|
+
}
|
|
41702
|
+
}
|
|
41604
41703
|
recordWhatItBrokeOn(game) {
|
|
41605
41704
|
const halt = this.dapClient?.whereItStopped() ?? null;
|
|
41606
|
-
if (halt?.reason !== "exception" || halt.text === "" || game.brokeOn === halt.text || game.errorReport
|
|
41705
|
+
if (halt?.reason !== "exception" || halt.text === "" || game.brokeOn === halt.text || typeof game.errorReport === "string") {
|
|
41607
41706
|
return;
|
|
41608
41707
|
}
|
|
41609
41708
|
game.brokeOn = halt.text;
|
|
@@ -41815,6 +41914,7 @@ class GodotServer {
|
|
|
41815
41914
|
pickedUpPlaying: true
|
|
41816
41915
|
};
|
|
41817
41916
|
this.activeProcess = picked;
|
|
41917
|
+
this.watchThePlayedConsole();
|
|
41818
41918
|
}
|
|
41819
41919
|
announcedPidOf(run, announced) {
|
|
41820
41920
|
if (run.announcedPid !== undefined) {
|
|
@@ -42233,7 +42333,7 @@ class GodotServer {
|
|
|
42233
42333
|
}));
|
|
42234
42334
|
}
|
|
42235
42335
|
if (run.pickedUpPlaying === true) {
|
|
42236
|
-
notes.push(run.transcript
|
|
42336
|
+
notes.push(run.transcript !== null ? "The editor was already playing this when this server reached it, and what it printed before that has been read back from its transcript. It is the run the editor is holding now, which is the one the debug_* tools answer for." : run.consoleFromReport === true ? "The editor was already playing this when this server reached it, and the server that started it left no transcript, so what is below was read back from the file the game's runtime writes its console to, which begins where the runtime started: the engine's own start-up before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for." : "The editor was already playing this when this server reached it, and the server that started it left no transcript, so this is not the run from its start: what it printed before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for.");
|
|
42237
42337
|
}
|
|
42238
42338
|
if (run.consoleLost === true) {
|
|
42239
42339
|
const back = this.dapClient?.isConnected() === true;
|
|
@@ -42961,7 +43061,7 @@ async function runGodotServer() {
|
|
|
42961
43061
|
const server = new GodotServer;
|
|
42962
43062
|
await server.run();
|
|
42963
43063
|
}
|
|
42964
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
43064
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
42965
43065
|
var init_server3 = __esm(() => {
|
|
42966
43066
|
init_mcp();
|
|
42967
43067
|
init_stdio2();
|
|
@@ -243,7 +243,7 @@ func announced_url() -> String:
|
|
|
243
243
|
var file: FileAccess = FileAccess.open(ANNOUNCEMENT, FileAccess.READ)
|
|
244
244
|
if file == null:
|
|
245
245
|
return ""
|
|
246
|
-
var said: Variant =
|
|
246
|
+
var said: Variant = Read.json_or_null(file.get_as_text())
|
|
247
247
|
file.close()
|
|
248
248
|
if not said is Dictionary:
|
|
249
249
|
return ""
|
|
@@ -481,7 +481,7 @@ func _on_reconnect_timer() -> void:
|
|
|
481
481
|
|
|
482
482
|
|
|
483
483
|
func _handle_message(json_string: String) -> void:
|
|
484
|
-
var parsed: Variant =
|
|
484
|
+
var parsed: Variant = Read.json_or_null(json_string)
|
|
485
485
|
if not parsed is Dictionary:
|
|
486
486
|
push_error("[gdharness] The server sent something that is not a message: %s" % json_string)
|
|
487
487
|
return
|
|
@@ -69,3 +69,16 @@ static func as_bool(value: Variant, fallback: bool = false) -> bool:
|
|
|
69
69
|
var text: String = value
|
|
70
70
|
return not text.is_empty()
|
|
71
71
|
return fallback
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## [param text] read as JSON, or null when it does not parse, without printing anything.
|
|
75
|
+
##
|
|
76
|
+
## [method JSON.parse_string] prints an engine error for text that does not parse, and that error
|
|
77
|
+
## lands in the log of whatever is running it. A caller sending the word "five" where a number was
|
|
78
|
+
## wanted was refused properly, and the game's own log then held an error it never made, so its
|
|
79
|
+
## verdict read `clean: false`. Text that is not JSON is an answer here, not a fault.
|
|
80
|
+
static func json_or_null(text: String) -> Variant:
|
|
81
|
+
var json: JSON = JSON.new()
|
|
82
|
+
if json.parse(text) != OK:
|
|
83
|
+
return null
|
|
84
|
+
return json.data
|
|
@@ -256,7 +256,7 @@ func fitted(value: Variant, type: int) -> Variant:
|
|
|
256
256
|
|
|
257
257
|
if rebuilt is String and type != TYPE_STRING:
|
|
258
258
|
var text: String = rebuilt
|
|
259
|
-
var parsed: Variant =
|
|
259
|
+
var parsed: Variant = Read.json_or_null(text)
|
|
260
260
|
# A string that does not read as the type wanted is handed back as the string it is, so
|
|
261
261
|
# whoever asked can refuse it. type_convert answers 0 for "not an index" and true for any
|
|
262
262
|
# text at all, and a caller who sent a word got back a number they never sent: get_child
|
|
@@ -86,7 +86,7 @@ func _parse_json_maybe(value: Variant) -> Variant:
|
|
|
86
86
|
if typeof(value) != TYPE_STRING:
|
|
87
87
|
return value
|
|
88
88
|
var text: String = value
|
|
89
|
-
var parsed: Variant =
|
|
89
|
+
var parsed: Variant = Read.json_or_null(text)
|
|
90
90
|
if parsed == null and text != "null":
|
|
91
91
|
return value
|
|
92
92
|
return parsed
|
|
@@ -127,7 +127,7 @@ func _parse_properties_arg(raw_properties: Variant) -> Dictionary:
|
|
|
127
127
|
var text: String = str(raw_properties)
|
|
128
128
|
if text.strip_edges().is_empty():
|
|
129
129
|
return {}
|
|
130
|
-
var parsed: Variant =
|
|
130
|
+
var parsed: Variant = Read.json_or_null(text)
|
|
131
131
|
if typeof(parsed) == TYPE_DICTIONARY:
|
|
132
132
|
return parsed
|
|
133
133
|
return {}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
extends Logger
|
|
2
2
|
|
|
3
|
-
##
|
|
4
|
-
## in the lines the engine itself prints
|
|
3
|
+
## The console of a game the editor plays, errors and prints in the order the engine made them,
|
|
4
|
+
## written to a file beside the announcement in the lines the engine itself prints.
|
|
5
5
|
##
|
|
6
6
|
## A game the editor plays prints to the editor's own stderr, which nobody reads: a `push_error`
|
|
7
7
|
## raised in one reached neither `editor_output` nor the run's transcript, and the run was
|
|
@@ -9,14 +9,49 @@ extends Logger
|
|
|
9
9
|
## what the game prints and not what it reports, so the report is taken where it is made, in
|
|
10
10
|
## the game, and left where the server already looks. The lines are the engine's own so the
|
|
11
11
|
## server reads them the way it reads a run it started itself.
|
|
12
|
+
##
|
|
13
|
+
## Prints as well, once [method take_the_console] is called: the adapter's prints and this file's
|
|
14
|
+
## errors are two streams, and read side by side a warning raised before a print was put after
|
|
15
|
+
## it. One file written from inside the game has one order, the engine's.
|
|
12
16
|
|
|
13
17
|
var _path: String
|
|
18
|
+
var _file: FileAccess = null
|
|
19
|
+
var _console: bool = false
|
|
20
|
+
## Held across a write, because the engine logs from whichever thread printed.
|
|
21
|
+
var _mutex: Mutex = Mutex.new()
|
|
22
|
+
## Set while writing, so a failure the write itself logs does not come straight back in here.
|
|
23
|
+
var _writing: bool = false
|
|
14
24
|
|
|
15
25
|
|
|
16
26
|
func _init(path: String) -> void:
|
|
17
27
|
_path = path
|
|
18
28
|
|
|
19
29
|
|
|
30
|
+
## Writes every print from here on as well as the errors, and prints [param boundary], which is
|
|
31
|
+
## therefore the first print in the file and the last one the server takes from the adapter.
|
|
32
|
+
func take_the_console(boundary: String) -> void:
|
|
33
|
+
_mutex.lock()
|
|
34
|
+
_console = true
|
|
35
|
+
_mutex.unlock()
|
|
36
|
+
print(boundary)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Lets go of the file, once the Logger has been removed and nothing more will be written.
|
|
40
|
+
func close() -> void:
|
|
41
|
+
_mutex.lock()
|
|
42
|
+
if _file != null:
|
|
43
|
+
_file.close()
|
|
44
|
+
_file = null
|
|
45
|
+
_mutex.unlock()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
func _log_message(message: String, _error: bool) -> void:
|
|
49
|
+
_mutex.lock()
|
|
50
|
+
if _console:
|
|
51
|
+
_write(message if message.ends_with("\n") else message + "\n")
|
|
52
|
+
_mutex.unlock()
|
|
53
|
+
|
|
54
|
+
|
|
20
55
|
# The engine's own signature for a Logger, eight parameters and not this project's to shape.
|
|
21
56
|
# gdlint:ignore = function-arguments-number
|
|
22
57
|
func _log_error(
|
|
@@ -45,18 +80,29 @@ func _log_error(
|
|
|
45
80
|
]
|
|
46
81
|
for backtrace: ScriptBacktrace in script_backtraces:
|
|
47
82
|
lines.append(backtrace.format(3))
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
83
|
+
_mutex.lock()
|
|
84
|
+
_write("\n".join(lines) + "\n")
|
|
85
|
+
_mutex.unlock()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## Appends [param text] and flushes it, so the file is whole on disk after every entry: the last
|
|
89
|
+
## thing a game reports is the one a caller most needs, and the game may be gone the moment after.
|
|
90
|
+
## Held open rather than opened for each entry, because a game printing every frame would open the
|
|
91
|
+
## file sixty times a second. Called with the mutex held.
|
|
92
|
+
func _write(text: String) -> void:
|
|
93
|
+
if _writing:
|
|
57
94
|
return
|
|
58
|
-
|
|
59
|
-
|
|
95
|
+
_writing = true
|
|
96
|
+
if _file == null:
|
|
97
|
+
_file = (
|
|
98
|
+
FileAccess.open(_path, FileAccess.READ_WRITE)
|
|
99
|
+
if FileAccess.file_exists(_path)
|
|
100
|
+
else FileAccess.open(_path, FileAccess.WRITE)
|
|
101
|
+
)
|
|
102
|
+
if _file != null:
|
|
103
|
+
_file.seek_end()
|
|
104
|
+
if _file != null:
|
|
60
105
|
# A line the disk would not take is a report lost, and nothing here may report that.
|
|
61
|
-
var _stored: bool =
|
|
62
|
-
|
|
106
|
+
var _stored: bool = _file.store_string(text)
|
|
107
|
+
_file.flush()
|
|
108
|
+
_writing = false
|
|
@@ -69,3 +69,16 @@ static func as_bool(value: Variant, fallback: bool = false) -> bool:
|
|
|
69
69
|
var text: String = value
|
|
70
70
|
return not text.is_empty()
|
|
71
71
|
return fallback
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## [param text] read as JSON, or null when it does not parse, without printing anything.
|
|
75
|
+
##
|
|
76
|
+
## [method JSON.parse_string] prints an engine error for text that does not parse, and that error
|
|
77
|
+
## lands in the log of whatever is running it. A caller sending the word "five" where a number was
|
|
78
|
+
## wanted was refused properly, and the game's own log then held an error it never made, so its
|
|
79
|
+
## verdict read `clean: false`. Text that is not JSON is an answer here, not a fault.
|
|
80
|
+
static func json_or_null(text: String) -> Variant:
|
|
81
|
+
var json: JSON = JSON.new()
|
|
82
|
+
if json.parse(text) != OK:
|
|
83
|
+
return null
|
|
84
|
+
return json.data
|
|
@@ -114,11 +114,13 @@ func _ready() -> void:
|
|
|
114
114
|
func _exit_tree() -> void:
|
|
115
115
|
if _errors != null:
|
|
116
116
|
OS.remove_logger(_errors)
|
|
117
|
+
_errors.close()
|
|
117
118
|
_errors = null
|
|
118
119
|
_cleanup()
|
|
119
120
|
|
|
120
121
|
|
|
121
|
-
## The engine's error reports, written to a file for the server, for a game the editor plays
|
|
122
|
+
## The engine's error reports, written to a file for the server, for a game the editor plays, and
|
|
123
|
+
## its prints as well from the line announcing the runtime on.
|
|
122
124
|
##
|
|
123
125
|
## Only for one the editor plays: a game the server starts itself has its stderr in the run's
|
|
124
126
|
## transcript already, and a game somebody runs from a terminal has it on the terminal. The
|
|
@@ -235,7 +237,16 @@ func _start_server() -> void:
|
|
|
235
237
|
|
|
236
238
|
_port = _server.get_local_port()
|
|
237
239
|
_announce(bind_address)
|
|
238
|
-
|
|
240
|
+
var listening: String = (
|
|
241
|
+
"[gdharness] runtime listening on %s:%d, announced at %s" % [bind_address, _port, _announcement]
|
|
242
|
+
)
|
|
243
|
+
# The line the server tells the two streams apart by: it takes the prints before it from the
|
|
244
|
+
# debug adapter and everything from it on from the file, which has it first. After the
|
|
245
|
+
# announcement, so a server reading the line finds the file beside it.
|
|
246
|
+
if _errors != null:
|
|
247
|
+
_errors.take_the_console(listening)
|
|
248
|
+
else:
|
|
249
|
+
print(listening)
|
|
239
250
|
|
|
240
251
|
|
|
241
252
|
## Whether the engine was told to run a script rather than the game.
|
|
@@ -18,15 +18,27 @@ const LISTED_CANDIDATES: int = 10
|
|
|
18
18
|
## Matched the way a find matches `says`, against a control's own words, and only among controls
|
|
19
19
|
## the player can see. A match inside a button presses the button, because the label is what says
|
|
20
20
|
## the words and the button is what takes the click: aimed at the label, the pointer lands on the
|
|
21
|
-
## button and the answer would call the click a miss.
|
|
22
|
-
##
|
|
21
|
+
## button and the answer would call the click a miss.
|
|
22
|
+
##
|
|
23
|
+
## The matches are ranked the way a person reads a screen for the thing to press: a button before
|
|
24
|
+
## text that cannot be pressed, and a control whose words are exactly these before one saying them
|
|
25
|
+
## as part of a sentence. A plain word is matched anywhere in a text, so on a screen full of prose
|
|
26
|
+
## "Back" was said by ten labels as well as the one button reading "Back", and the refusal listing
|
|
27
|
+
## eleven sent the caller to an index that moved every time the prose did. Several matches at the
|
|
28
|
+
## best rank are refused rather than guessed, each named with the [param which] that picks it,
|
|
29
|
+
## counting from 0 in rank order.
|
|
30
|
+
##
|
|
31
|
+
## A control with something drawn over its centre that a pointer there would reach instead is not
|
|
32
|
+
## on screen for a player, and a click at it lands on the thing on top. The screen a game shows
|
|
33
|
+
## over its hall covers the hall's drawers without hiding them, so their words matched as well.
|
|
23
34
|
static func control_saying(root: Node, root_path: String, wanted: String, which: Variant) -> Dictionary:
|
|
24
35
|
var reached: Dictionary = Values.node_at(root, root_path)
|
|
25
36
|
if reached.has("message"):
|
|
26
37
|
return reached
|
|
27
|
-
var
|
|
38
|
+
var matched: Array[Control] = []
|
|
28
39
|
# The words of the control that matched, which a button pressed on a label's behalf has none of.
|
|
29
40
|
var said: Array[String] = []
|
|
41
|
+
var exact: Array[bool] = []
|
|
30
42
|
var hidden: int = 0
|
|
31
43
|
var pending: Array[Node] = [reached["node"]]
|
|
32
44
|
while not pending.is_empty():
|
|
@@ -35,32 +47,68 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
35
47
|
if not Queries.shown(node):
|
|
36
48
|
hidden += 1
|
|
37
49
|
else:
|
|
38
|
-
var
|
|
39
|
-
var target: Control = _pressed_through(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
var control: Control = node
|
|
51
|
+
var target: Control = _pressed_through(control)
|
|
52
|
+
var words: String = Words.said_by(node)
|
|
53
|
+
var whole: bool = _says_exactly(words, wanted)
|
|
54
|
+
var at: int = matched.find(target)
|
|
55
|
+
if at == -1:
|
|
56
|
+
matched.append(target)
|
|
57
|
+
said.append(words)
|
|
58
|
+
exact.append(whole)
|
|
59
|
+
elif whole and not exact[at]:
|
|
60
|
+
said[at] = words
|
|
61
|
+
exact[at] = true
|
|
43
62
|
var children: Array[Node] = node.get_children(true)
|
|
44
63
|
for index: int in range(children.size() - 1, -1, -1):
|
|
45
64
|
pending.push_front(children[index])
|
|
46
65
|
|
|
66
|
+
var drawn: Array[Node] = _pointer_takers(root)
|
|
67
|
+
var covered: int = 0
|
|
68
|
+
var cover: Node = null
|
|
69
|
+
var open: Array[int] = []
|
|
70
|
+
for index: int in matched.size():
|
|
71
|
+
var over: Node = _cover_of(matched[index], drawn)
|
|
72
|
+
if over == null:
|
|
73
|
+
open.append(index)
|
|
74
|
+
else:
|
|
75
|
+
covered += 1
|
|
76
|
+
if cover == null:
|
|
77
|
+
cover = over
|
|
78
|
+
# Stable, so matches of one rank keep the order they are drawn in.
|
|
79
|
+
open.sort_custom(
|
|
80
|
+
func(a: int, b: int) -> bool:
|
|
81
|
+
var first: int = _rank(matched[a], exact[a])
|
|
82
|
+
var second: int = _rank(matched[b], exact[b])
|
|
83
|
+
return first > second or (first == second and a < b)
|
|
84
|
+
)
|
|
85
|
+
var targets: Array[Control] = []
|
|
86
|
+
var ranks: Array[int] = []
|
|
87
|
+
var lines: Array[String] = []
|
|
88
|
+
for index: int in open:
|
|
89
|
+
targets.append(matched[index])
|
|
90
|
+
ranks.append(_rank(matched[index], exact[index]))
|
|
91
|
+
lines.append(said[index])
|
|
92
|
+
|
|
47
93
|
var under: String = "" if root_path == "/root" else " under %s" % root_path
|
|
48
94
|
if targets.is_empty():
|
|
49
95
|
return {
|
|
50
96
|
"type": "error",
|
|
51
97
|
"message":
|
|
52
98
|
(
|
|
53
|
-
'no control on screen%s says "%s", so nothing was clicked%s'
|
|
54
|
-
% [under, wanted, _hidden_note(hidden)]
|
|
99
|
+
'no control on screen%s says "%s", so nothing was clicked%s%s'
|
|
100
|
+
% [under, wanted, _hidden_note(hidden), _covered_note(covered, cover)]
|
|
55
101
|
)
|
|
56
102
|
}
|
|
57
|
-
|
|
103
|
+
var best: int = ranks.count(ranks[0])
|
|
104
|
+
if which == null and best > 1:
|
|
105
|
+
var order: String = "" if best == targets.size() else ", best matches first"
|
|
58
106
|
return {
|
|
59
107
|
"type": "error",
|
|
60
108
|
"message":
|
|
61
109
|
(
|
|
62
|
-
|
|
63
|
-
% [
|
|
110
|
+
"%s, so which to click is not clear; pass index, counting from 0%s: %s"
|
|
111
|
+
% [_described(best, ranks[0], wanted, under), order, _candidates(targets, lines)]
|
|
64
112
|
)
|
|
65
113
|
}
|
|
66
114
|
var index: int = 0 if which == null else Read.as_int(which, -1)
|
|
@@ -71,13 +119,58 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
71
119
|
"message":
|
|
72
120
|
(
|
|
73
121
|
'index %s is not one of the %d control%s on screen%s saying "%s": %s'
|
|
74
|
-
% [str(which), targets.size(), plural, under, wanted, _candidates(targets,
|
|
122
|
+
% [str(which), targets.size(), plural, under, wanted, _candidates(targets, lines)]
|
|
75
123
|
)
|
|
76
124
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
125
|
+
var found: Dictionary = {"says": wanted, "index": index, "of": targets.size()}
|
|
126
|
+
if which == null and targets.size() > 1:
|
|
127
|
+
var others: String = (
|
|
128
|
+
"the other one on screen %s" % _passed_over(ranks[0], true)
|
|
129
|
+
if targets.size() == 2
|
|
130
|
+
else "the other %d on screen %s" % [targets.size() - 1, _passed_over(ranks[0], false)]
|
|
131
|
+
)
|
|
132
|
+
found["picked"] = "%s; %s" % [_described(1, ranks[0], wanted, under), others]
|
|
133
|
+
if covered > 0:
|
|
134
|
+
found["covered"] = covered
|
|
135
|
+
return {"path": str(targets[index].get_path()), "found": found}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
## How good a match [param target] is for a click: a button over what cannot be pressed, and then
|
|
139
|
+
## the whole of its words over a part of them.
|
|
140
|
+
static func _rank(target: Control, whole: bool) -> int:
|
|
141
|
+
return (2 if target is BaseButton else 0) + (1 if whole else 0)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
## Whether [param said] is the whole of what [param wanted] asks for. A glob is matched against the
|
|
145
|
+
## whole text already, so every match of one is whole.
|
|
146
|
+
static func _says_exactly(said: String, wanted: String) -> bool:
|
|
147
|
+
var words: String = Queries.as_said(wanted)
|
|
148
|
+
if words.contains("*") or words.contains("?"):
|
|
149
|
+
return true
|
|
150
|
+
return said.strip_edges().nocasecmp_to(words.strip_edges()) == 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
## [param count] matches of [param rank], as the start of a sentence.
|
|
154
|
+
static func _described(count: int, rank: int, wanted: String, under: String) -> String:
|
|
155
|
+
var what: String = "button" if rank >= 2 else "control"
|
|
156
|
+
var how: String = '"%s"' % wanted
|
|
157
|
+
if not Queries.as_said(wanted).contains("*") and not Queries.as_said(wanted).contains("?"):
|
|
158
|
+
how = ('exactly "%s"' if rank % 2 == 1 else '"%s" as part of more') % wanted
|
|
159
|
+
if count == 1:
|
|
160
|
+
return "the one %s on screen%s saying %s" % [what, under, how]
|
|
161
|
+
return "%d %ss on screen%s say %s" % [count, what, under, how]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
## Why the matches below [param rank] were passed over, said of one of them when [param one].
|
|
165
|
+
static func _passed_over(rank: int, one: bool) -> String:
|
|
166
|
+
var says: String = "says" if one else "say"
|
|
167
|
+
match rank:
|
|
168
|
+
3:
|
|
169
|
+
return "cannot be pressed or %s it as part of more" % says
|
|
170
|
+
2:
|
|
171
|
+
return "cannot be pressed"
|
|
172
|
+
_:
|
|
173
|
+
return "%s it as part of more" % says
|
|
81
174
|
|
|
82
175
|
|
|
83
176
|
## What to add to a refusal that found nothing on screen, when words matched controls out of sight.
|
|
@@ -87,6 +180,15 @@ static func _hidden_note(hidden: int) -> String:
|
|
|
87
180
|
return "; %d hidden control%s" % [hidden, " says it" if hidden == 1 else "s say it"]
|
|
88
181
|
|
|
89
182
|
|
|
183
|
+
## What to add to a refusal that found nothing on screen, when the controls saying it are covered.
|
|
184
|
+
static func _covered_note(covered: int, cover: Node) -> String:
|
|
185
|
+
if covered == 0:
|
|
186
|
+
return ""
|
|
187
|
+
if covered == 1:
|
|
188
|
+
return "; 1 control says it under %s, which is drawn over it" % cover.get_path()
|
|
189
|
+
return "; %d controls say it under what is drawn over them, such as %s" % [covered, cover.get_path()]
|
|
190
|
+
|
|
191
|
+
|
|
90
192
|
## [param control], or the button holding it when it is not one itself, which is what a click on
|
|
91
193
|
## its words means.
|
|
92
194
|
static func _pressed_through(control: Control) -> Control:
|
|
@@ -99,6 +201,94 @@ static func _pressed_through(control: Control) -> Control:
|
|
|
99
201
|
return control
|
|
100
202
|
|
|
101
203
|
|
|
204
|
+
## Everything in the tree [param root] heads that a pointer can land on: each control on screen
|
|
205
|
+
## that does not let the pointer through, and each window embedded in another.
|
|
206
|
+
static func _pointer_takers(root: Node) -> Array[Node]:
|
|
207
|
+
var found: Array[Node] = []
|
|
208
|
+
var pending: Array[Node] = [root]
|
|
209
|
+
while not pending.is_empty():
|
|
210
|
+
var node: Node = pending.pop_back()
|
|
211
|
+
var control: Control = node as Control
|
|
212
|
+
var window: Window = node as Window
|
|
213
|
+
if control != null:
|
|
214
|
+
if not control.is_visible_in_tree():
|
|
215
|
+
continue
|
|
216
|
+
if control.get_mouse_filter_with_override() != Control.MOUSE_FILTER_IGNORE:
|
|
217
|
+
found.append(control)
|
|
218
|
+
elif window != null and window != root:
|
|
219
|
+
if not window.visible:
|
|
220
|
+
continue
|
|
221
|
+
if window.is_embedded():
|
|
222
|
+
found.append(window)
|
|
223
|
+
pending.append_array(node.get_children(true))
|
|
224
|
+
return found
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
## What a pointer at [param target]'s centre reaches instead of it, or null when nothing does.
|
|
228
|
+
##
|
|
229
|
+
## The engine's own order for which control takes the pointer: an embedded window over everything
|
|
230
|
+
## in the viewport that holds it, then the higher canvas layer, then the later in the tree. A
|
|
231
|
+
## control holding the target or held by it is part of the same click. A target whose centre its
|
|
232
|
+
## own container clips away is judged not covered, since the click scrolls it into view first and
|
|
233
|
+
## what is drawn there now is not what it will land on.
|
|
234
|
+
static func _cover_of(target: Control, drawn: Array[Node]) -> Node:
|
|
235
|
+
var viewport: Viewport = target.get_viewport()
|
|
236
|
+
var point: Vector2 = target.get_global_transform_with_canvas() * (target.size * 0.5)
|
|
237
|
+
if _clipped_away(target, point):
|
|
238
|
+
return null
|
|
239
|
+
var layer: int = _layer_of(target)
|
|
240
|
+
for other: Node in drawn:
|
|
241
|
+
var window: Window = other as Window
|
|
242
|
+
if window != null:
|
|
243
|
+
if (
|
|
244
|
+
window.get_parent() != null
|
|
245
|
+
and window.get_parent().get_viewport() == viewport
|
|
246
|
+
and not window.is_ancestor_of(target)
|
|
247
|
+
and Rect2(window.position, window.size).has_point(point)
|
|
248
|
+
):
|
|
249
|
+
return window
|
|
250
|
+
continue
|
|
251
|
+
var control: Control = other
|
|
252
|
+
if control == target or control.is_ancestor_of(target) or target.is_ancestor_of(control):
|
|
253
|
+
continue
|
|
254
|
+
if control.get_viewport() != viewport:
|
|
255
|
+
continue
|
|
256
|
+
var its_layer: int = _layer_of(control)
|
|
257
|
+
if its_layer < layer or (its_layer == layer and not control.is_greater_than(target)):
|
|
258
|
+
continue
|
|
259
|
+
if _holds(control, point) and not _clipped_away(control, point):
|
|
260
|
+
return control
|
|
261
|
+
return null
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
## Whether [param control] holds [param point], given in its viewport's coordinates.
|
|
265
|
+
static func _holds(control: Control, point: Vector2) -> bool:
|
|
266
|
+
var placed: Transform2D = control.get_global_transform_with_canvas()
|
|
267
|
+
# A control scaled to nothing holds no point, and inverting its transform is an engine error.
|
|
268
|
+
if is_zero_approx(placed.determinant()):
|
|
269
|
+
return false
|
|
270
|
+
# The rectangle, which is the engine's own answer unless a script overrides `_has_point`; the
|
|
271
|
+
# engine does not offer the overridden answer to scripts.
|
|
272
|
+
return Rect2(Vector2.ZERO, control.size).has_point(placed.affine_inverse() * point)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
## Whether a container clipping what it holds cuts [param point] off from [param control].
|
|
276
|
+
static func _clipped_away(control: Control, point: Vector2) -> bool:
|
|
277
|
+
var walk: Node = control.get_parent()
|
|
278
|
+
while walk != null:
|
|
279
|
+
var holder: Control = walk as Control
|
|
280
|
+
if holder != null and holder.clip_contents and not _holds(holder, point):
|
|
281
|
+
return true
|
|
282
|
+
walk = walk.get_parent()
|
|
283
|
+
return false
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
## The canvas layer [param control] is drawn on, 0 for the viewport's own canvas.
|
|
287
|
+
static func _layer_of(control: Control) -> int:
|
|
288
|
+
var layer: CanvasLayer = control.get_canvas_layer_node()
|
|
289
|
+
return 0 if layer == null else layer.layer
|
|
290
|
+
|
|
291
|
+
|
|
102
292
|
## The controls a click by words could mean, each with its index, its path and the words that
|
|
103
293
|
## matched, [param said] holding those in the same order.
|
|
104
294
|
static func _candidates(targets: Array[Control], said: Array[String]) -> String:
|
|
@@ -7,7 +7,6 @@ const Paths = preload("runtime_paths.gd")
|
|
|
7
7
|
const Queries = preload("runtime_queries.gd")
|
|
8
8
|
const Read = preload("reading.gd")
|
|
9
9
|
const Values = preload("runtime_values.gd")
|
|
10
|
-
const Words = preload("runtime_words.gd")
|
|
11
10
|
|
|
12
11
|
## The longest one wait may last, whatever the request says: past this the server has long
|
|
13
12
|
## since given up on the reply.
|
|
@@ -284,7 +283,9 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
|
|
|
284
283
|
|
|
285
284
|
|
|
286
285
|
## Whether anything under [param node_path] says [param said], the node itself included, and shown
|
|
287
|
-
## unless hidden ones are wanted too.
|
|
286
|
+
## unless hidden ones are wanted too. Matched as a find and a click match `says`, a glob over the
|
|
287
|
+
## whole text when it has `*` or `?`: read as literal words, a glob a click had just pressed by was
|
|
288
|
+
## waited on for the whole timeout with its label on screen, and answered as words never arriving.
|
|
288
289
|
func _anything_says(node_path: String, said: String, include_hidden: bool) -> bool:
|
|
289
290
|
var root: Node = _host.get_tree().root.get_node_or_null(node_path)
|
|
290
291
|
if root == null:
|
|
@@ -292,7 +293,7 @@ func _anything_says(node_path: String, said: String, include_hidden: bool) -> bo
|
|
|
292
293
|
var pending: Array[Node] = [root]
|
|
293
294
|
while not pending.is_empty():
|
|
294
295
|
var node: Node = pending.pop_back()
|
|
295
|
-
if
|
|
296
|
+
if Queries.says(node, said) and (include_hidden or Queries.shown(node)):
|
|
296
297
|
return true
|
|
297
298
|
pending.append_array(node.get_children(true))
|
|
298
299
|
return false
|
|
@@ -256,7 +256,7 @@ func fitted(value: Variant, type: int) -> Variant:
|
|
|
256
256
|
|
|
257
257
|
if rebuilt is String and type != TYPE_STRING:
|
|
258
258
|
var text: String = rebuilt
|
|
259
|
-
var parsed: Variant =
|
|
259
|
+
var parsed: Variant = Read.json_or_null(text)
|
|
260
260
|
# A string that does not read as the type wanted is handed back as the string it is, so
|
|
261
261
|
# whoever asked can refuse it. type_convert answers 0 for "not an index" and true for any
|
|
262
262
|
# text at all, and a caller who sent a word got back a number they never sent: get_child
|
|
@@ -645,7 +645,7 @@ static func _images_of(scene_path: String) -> Array[String]:
|
|
|
645
645
|
text = bytes.slice(20, mini(20 + length, bytes.size())).get_string_from_utf8()
|
|
646
646
|
if text.is_empty():
|
|
647
647
|
return images
|
|
648
|
-
var parsed: Variant =
|
|
648
|
+
var parsed: Variant = Read.json_or_null(text)
|
|
649
649
|
if not parsed is Dictionary:
|
|
650
650
|
return images
|
|
651
651
|
var document: Dictionary = parsed
|
|
@@ -69,3 +69,16 @@ static func as_bool(value: Variant, fallback: bool = false) -> bool:
|
|
|
69
69
|
var text: String = value
|
|
70
70
|
return not text.is_empty()
|
|
71
71
|
return fallback
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## [param text] read as JSON, or null when it does not parse, without printing anything.
|
|
75
|
+
##
|
|
76
|
+
## [method JSON.parse_string] prints an engine error for text that does not parse, and that error
|
|
77
|
+
## lands in the log of whatever is running it. A caller sending the word "five" where a number was
|
|
78
|
+
## wanted was refused properly, and the game's own log then held an error it never made, so its
|
|
79
|
+
## verdict read `clean: false`. Text that is not JSON is an answer here, not a fault.
|
|
80
|
+
static func json_or_null(text: String) -> Variant:
|
|
81
|
+
var json: JSON = JSON.new()
|
|
82
|
+
if json.parse(text) != OK:
|
|
83
|
+
return null
|
|
84
|
+
return json.data
|
|
@@ -256,7 +256,7 @@ func fitted(value: Variant, type: int) -> Variant:
|
|
|
256
256
|
|
|
257
257
|
if rebuilt is String and type != TYPE_STRING:
|
|
258
258
|
var text: String = rebuilt
|
|
259
|
-
var parsed: Variant =
|
|
259
|
+
var parsed: Variant = Read.json_or_null(text)
|
|
260
260
|
# A string that does not read as the type wanted is handed back as the string it is, so
|
|
261
261
|
# whoever asked can refuse it. type_convert answers 0 for "not an index" and true for any
|
|
262
262
|
# text at all, and a caller who sent a word got back a number they never sent: get_child
|
package/build/index.js
CHANGED
|
@@ -25210,6 +25210,29 @@ function errorReportOf(pid, directories = runtimeDirectories()) {
|
|
|
25210
25210
|
}
|
|
25211
25211
|
return null;
|
|
25212
25212
|
}
|
|
25213
|
+
var CONSOLE_BOUNDARY = "[gdharness] runtime listening on ";
|
|
25214
|
+
function consoleBoundaryAt(bytes) {
|
|
25215
|
+
const wanted = Buffer.from(CONSOLE_BOUNDARY);
|
|
25216
|
+
for (let at = bytes.indexOf(wanted);at !== -1; at = bytes.indexOf(wanted, at + 1)) {
|
|
25217
|
+
if (at === 0 || bytes[at - 1] === 10) {
|
|
25218
|
+
return at;
|
|
25219
|
+
}
|
|
25220
|
+
}
|
|
25221
|
+
return -1;
|
|
25222
|
+
}
|
|
25223
|
+
function reportAlreadyInTranscript(transcript, report) {
|
|
25224
|
+
const boundary = consoleBoundaryAt(report);
|
|
25225
|
+
if (boundary === -1) {
|
|
25226
|
+
return 0;
|
|
25227
|
+
}
|
|
25228
|
+
const inTranscript = consoleBoundaryAt(transcript);
|
|
25229
|
+
if (inTranscript === -1) {
|
|
25230
|
+
return boundary;
|
|
25231
|
+
}
|
|
25232
|
+
const copied = transcript.subarray(inTranscript);
|
|
25233
|
+
const end = boundary + copied.length;
|
|
25234
|
+
return end <= report.length && report.subarray(boundary, end).equals(copied) ? end : report.length;
|
|
25235
|
+
}
|
|
25213
25236
|
var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
|
|
25214
25237
|
function processAlive(pid) {
|
|
25215
25238
|
try {
|
|
@@ -30849,7 +30872,7 @@ var TOOL_SPECS = [
|
|
|
30849
30872
|
says: {
|
|
30850
30873
|
type: "string",
|
|
30851
30874
|
ops: ["click"],
|
|
30852
|
-
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), 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. Several matches are refused with each one listed, and index picks one."
|
|
30875
|
+
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), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it, is not on screen. The matches are ranked the way a person picks what to press: a button before text that cannot be pressed, then a control whose whole text 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."
|
|
30853
30876
|
},
|
|
30854
30877
|
action: {
|
|
30855
30878
|
type: "string",
|
|
@@ -30887,7 +30910,7 @@ var TOOL_SPECS = [
|
|
|
30887
30910
|
index: {
|
|
30888
30911
|
type: "number",
|
|
30889
30912
|
ops: ["click", "choose"],
|
|
30890
|
-
description: "click: which of several controls saying the words, counting from 0 in the order a refusal lists them. choose: the item to take, by where it is in the list, when text will not do."
|
|
30913
|
+
description: "click: which of several controls saying the words, counting from 0 in the order a refusal lists them, best matches first. choose: the item to take, by where it is in the list, when text will not do."
|
|
30891
30914
|
},
|
|
30892
30915
|
shift: { type: "boolean", ops: ["key"], description: "key: hold Shift with it. Default false." },
|
|
30893
30916
|
ctrl: { type: "boolean", ops: ["key"], description: "key: hold Ctrl with it. Default false." },
|
|
@@ -30980,7 +31003,7 @@ var TOOL_SPECS = [
|
|
|
30980
31003
|
says: {
|
|
30981
31004
|
type: "string",
|
|
30982
31005
|
ops: ["until"],
|
|
30983
|
-
description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put.
|
|
31006
|
+
description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Matched the way runtime_inspect find matches says: case-insensitive, a plain word anywhere in the text, a pattern with * or ? a glob over the whole of it; on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took."
|
|
30984
31007
|
},
|
|
30985
31008
|
includeHidden: {
|
|
30986
31009
|
type: "boolean",
|
|
@@ -31469,6 +31492,8 @@ function outsideTheirRanges(spec, args) {
|
|
|
31469
31492
|
}
|
|
31470
31493
|
var BRIDGE_RETRY_MS = 2000;
|
|
31471
31494
|
var SUCCESSOR_CHECK_MS = 1e4;
|
|
31495
|
+
var CONSOLE_WATCH_MS = 250;
|
|
31496
|
+
var BOUNDARY_WAIT_MS = 2000;
|
|
31472
31497
|
var HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
|
|
31473
31498
|
var HANDOVER_POLL_MS = 250;
|
|
31474
31499
|
var CACHE_WRITE_MS = 2000;
|
|
@@ -31687,6 +31712,7 @@ class GodotServer {
|
|
|
31687
31712
|
bridgeStartupError = null;
|
|
31688
31713
|
bridgeRetry = null;
|
|
31689
31714
|
successorWatch = null;
|
|
31715
|
+
consoleWatch = null;
|
|
31690
31716
|
lastProjectPath = null;
|
|
31691
31717
|
shutdownInitiated = false;
|
|
31692
31718
|
launchedEditor = null;
|
|
@@ -31859,10 +31885,30 @@ class GodotServer {
|
|
|
31859
31885
|
this.successorWatch = null;
|
|
31860
31886
|
}
|
|
31861
31887
|
}
|
|
31888
|
+
watchThePlayedConsole() {
|
|
31889
|
+
if (this.consoleWatch !== null) {
|
|
31890
|
+
return;
|
|
31891
|
+
}
|
|
31892
|
+
const watch = setInterval(() => {
|
|
31893
|
+
const run = this.activeProcess;
|
|
31894
|
+
if (run?.throughEditor !== true || run.transcript === null) {
|
|
31895
|
+
clearInterval(watch);
|
|
31896
|
+
this.consoleWatch = null;
|
|
31897
|
+
return;
|
|
31898
|
+
}
|
|
31899
|
+
this.drainErrorReport(run);
|
|
31900
|
+
}, CONSOLE_WATCH_MS);
|
|
31901
|
+
watch.unref();
|
|
31902
|
+
this.consoleWatch = watch;
|
|
31903
|
+
}
|
|
31862
31904
|
async cleanup() {
|
|
31863
31905
|
this.logDebug("Cleaning up resources");
|
|
31864
31906
|
this.stopTryingTheBridge();
|
|
31865
31907
|
this.stopWatchingForASuccessor();
|
|
31908
|
+
if (this.consoleWatch !== null) {
|
|
31909
|
+
clearInterval(this.consoleWatch);
|
|
31910
|
+
this.consoleWatch = null;
|
|
31911
|
+
}
|
|
31866
31912
|
withdrawBridge(this.announcedAt);
|
|
31867
31913
|
this.announcedAt = null;
|
|
31868
31914
|
this.activeProcess = null;
|
|
@@ -33430,7 +33476,16 @@ class GodotServer {
|
|
|
33430
33476
|
}
|
|
33431
33477
|
writeToTranscript(line) {
|
|
33432
33478
|
const run = this.activeProcess;
|
|
33433
|
-
if (run?.throughEditor !== true
|
|
33479
|
+
if (run?.throughEditor !== true) {
|
|
33480
|
+
return;
|
|
33481
|
+
}
|
|
33482
|
+
if (run.consoleFromReport === true || line.startsWith(CONSOLE_BOUNDARY)) {
|
|
33483
|
+
this.drainErrorReport(run, line);
|
|
33484
|
+
if (run.consoleFromReport === true) {
|
|
33485
|
+
return;
|
|
33486
|
+
}
|
|
33487
|
+
}
|
|
33488
|
+
if (run.transcript === null) {
|
|
33434
33489
|
return;
|
|
33435
33490
|
}
|
|
33436
33491
|
try {
|
|
@@ -33983,6 +34038,7 @@ class GodotServer {
|
|
|
33983
34038
|
seenPlaying: readBoolean(playAnswer, "playing") === true
|
|
33984
34039
|
};
|
|
33985
34040
|
this.activeProcess = played;
|
|
34041
|
+
this.watchThePlayedConsole();
|
|
33986
34042
|
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
33987
34043
|
sweepTranscripts();
|
|
33988
34044
|
return this.jsonTextResponse({
|
|
@@ -34000,7 +34056,7 @@ class GodotServer {
|
|
|
34000
34056
|
reason: one.reason
|
|
34001
34057
|
})),
|
|
34002
34058
|
runtime: await this.runtimeUp(projectPath, alreadyPlaying, runtimeWaitMs, false),
|
|
34003
|
-
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it." + endedToStartThis(ended)
|
|
34059
|
+
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter and the runtime's report of it, and editor_run stop ends it." + endedToStartThis(ended)
|
|
34004
34060
|
});
|
|
34005
34061
|
}
|
|
34006
34062
|
drainEditorOutput(game) {
|
|
@@ -34012,26 +34068,35 @@ class GodotServer {
|
|
|
34012
34068
|
return;
|
|
34013
34069
|
}
|
|
34014
34070
|
if (game.transcript === null) {
|
|
34015
|
-
|
|
34016
|
-
|
|
34071
|
+
const heard = this.dapClient.getOutput(true);
|
|
34072
|
+
if (game.consoleFromReport !== true) {
|
|
34073
|
+
for (const line of heard) {
|
|
34074
|
+
game.log.append("stdout", line.endsWith(`
|
|
34017
34075
|
`) ? line : `${line}
|
|
34018
34076
|
`);
|
|
34077
|
+
}
|
|
34019
34078
|
}
|
|
34020
34079
|
}
|
|
34021
|
-
if (!this.dapClient.isConnected()) {
|
|
34080
|
+
if (!this.dapClient.isConnected() && game.consoleFromReport !== true) {
|
|
34022
34081
|
game.consoleLost = true;
|
|
34023
34082
|
}
|
|
34024
34083
|
this.recordWhatItBrokeOn(game);
|
|
34025
34084
|
}
|
|
34026
|
-
drainErrorReport(game) {
|
|
34085
|
+
drainErrorReport(game, heard) {
|
|
34086
|
+
if (game.errorReport === null) {
|
|
34087
|
+
return;
|
|
34088
|
+
}
|
|
34027
34089
|
if (game.errorReport === undefined) {
|
|
34028
|
-
const its = this.announcedPidOf(game);
|
|
34090
|
+
const its = this.announcedPidOf(game) ?? (heard?.startsWith(CONSOLE_BOUNDARY) === true ? this.announcedPidNamedBy(game, heard) : undefined);
|
|
34029
34091
|
const found = its === undefined ? null : errorReportOf(its);
|
|
34030
34092
|
if (found === null) {
|
|
34093
|
+
if (heard?.startsWith(CONSOLE_BOUNDARY) === true) {
|
|
34094
|
+
game.errorReport = null;
|
|
34095
|
+
}
|
|
34031
34096
|
return;
|
|
34032
34097
|
}
|
|
34033
34098
|
game.errorReport = found;
|
|
34034
|
-
game.errorReportOffset =
|
|
34099
|
+
game.errorReportOffset = this.reportTakenBefore(game, found);
|
|
34035
34100
|
}
|
|
34036
34101
|
let handle;
|
|
34037
34102
|
try {
|
|
@@ -34047,8 +34112,22 @@ class GodotServer {
|
|
|
34047
34112
|
}
|
|
34048
34113
|
const buffer = Buffer.alloc(size - from);
|
|
34049
34114
|
const read = readSync2(handle, buffer, 0, buffer.length, from);
|
|
34050
|
-
|
|
34051
|
-
|
|
34115
|
+
let reported = buffer.subarray(0, read);
|
|
34116
|
+
if (game.consoleFromReport !== true) {
|
|
34117
|
+
const boundary = consoleBoundaryAt(reported);
|
|
34118
|
+
if (boundary !== -1) {
|
|
34119
|
+
game.boundaryInReportSince ??= Date.now();
|
|
34120
|
+
if (heard?.startsWith(CONSOLE_BOUNDARY) === true || game.pickedUpPlaying === true || this.dapClient?.isConnected() !== true || Date.now() - game.boundaryInReportSince >= BOUNDARY_WAIT_MS) {
|
|
34121
|
+
game.consoleFromReport = true;
|
|
34122
|
+
} else {
|
|
34123
|
+
reported = reported.subarray(0, boundary);
|
|
34124
|
+
}
|
|
34125
|
+
}
|
|
34126
|
+
}
|
|
34127
|
+
game.errorReportOffset = from + reported.length;
|
|
34128
|
+
if (reported.length === 0) {
|
|
34129
|
+
return;
|
|
34130
|
+
}
|
|
34052
34131
|
if (game.transcript !== null) {
|
|
34053
34132
|
appendFileSync(game.transcript, reported);
|
|
34054
34133
|
} else {
|
|
@@ -34058,9 +34137,32 @@ class GodotServer {
|
|
|
34058
34137
|
closeSync2(handle);
|
|
34059
34138
|
}
|
|
34060
34139
|
}
|
|
34140
|
+
announcedPidNamedBy(run, line) {
|
|
34141
|
+
const named = /runtime-(\d+)\.json$/.exec(line.trim())?.[1];
|
|
34142
|
+
if (named === undefined) {
|
|
34143
|
+
return;
|
|
34144
|
+
}
|
|
34145
|
+
const before = run.announcedBefore ?? new Set;
|
|
34146
|
+
const theOne = this.announcedOfTheRunsProject(run, runtimesAnnounced().running).find((one) => one.pid === Number(named) && !before.has(one.pid) && this.isTheGameOf(run, one, run.projectPath));
|
|
34147
|
+
if (theOne !== undefined) {
|
|
34148
|
+
this.tie(run, theOne);
|
|
34149
|
+
}
|
|
34150
|
+
return run.announcedPid;
|
|
34151
|
+
}
|
|
34152
|
+
reportTakenBefore(game, report) {
|
|
34153
|
+
if (game.pickedUpPlaying !== true || game.transcript === null) {
|
|
34154
|
+
return 0;
|
|
34155
|
+
}
|
|
34156
|
+
try {
|
|
34157
|
+
return reportAlreadyInTranscript(readFileSync17(game.transcript), readFileSync17(report));
|
|
34158
|
+
} catch (error) {
|
|
34159
|
+
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
34160
|
+
return 0;
|
|
34161
|
+
}
|
|
34162
|
+
}
|
|
34061
34163
|
recordWhatItBrokeOn(game) {
|
|
34062
34164
|
const halt = this.dapClient?.whereItStopped() ?? null;
|
|
34063
|
-
if (halt?.reason !== "exception" || halt.text === "" || game.brokeOn === halt.text || game.errorReport
|
|
34165
|
+
if (halt?.reason !== "exception" || halt.text === "" || game.brokeOn === halt.text || typeof game.errorReport === "string") {
|
|
34064
34166
|
return;
|
|
34065
34167
|
}
|
|
34066
34168
|
game.brokeOn = halt.text;
|
|
@@ -34272,6 +34374,7 @@ class GodotServer {
|
|
|
34272
34374
|
pickedUpPlaying: true
|
|
34273
34375
|
};
|
|
34274
34376
|
this.activeProcess = picked;
|
|
34377
|
+
this.watchThePlayedConsole();
|
|
34275
34378
|
}
|
|
34276
34379
|
announcedPidOf(run, announced) {
|
|
34277
34380
|
if (run.announcedPid !== undefined) {
|
|
@@ -34690,7 +34793,7 @@ class GodotServer {
|
|
|
34690
34793
|
}));
|
|
34691
34794
|
}
|
|
34692
34795
|
if (run.pickedUpPlaying === true) {
|
|
34693
|
-
notes.push(run.transcript
|
|
34796
|
+
notes.push(run.transcript !== null ? "The editor was already playing this when this server reached it, and what it printed before that has been read back from its transcript. It is the run the editor is holding now, which is the one the debug_* tools answer for." : run.consoleFromReport === true ? "The editor was already playing this when this server reached it, and the server that started it left no transcript, so what is below was read back from the file the game's runtime writes its console to, which begins where the runtime started: the engine's own start-up before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for." : "The editor was already playing this when this server reached it, and the server that started it left no transcript, so this is not the run from its start: what it printed before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for.");
|
|
34694
34797
|
}
|
|
34695
34798
|
if (run.consoleLost === true) {
|
|
34696
34799
|
const back = this.dapClient?.isConnected() === true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.12",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|