gdharness 1.0.18 → 1.0.20
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 +47 -19
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +251 -0
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +74 -230
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +81 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +4 -3
- package/build/index.js +47 -19
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -16409,7 +16409,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16409
16409
|
},
|
|
16410
16410
|
{
|
|
16411
16411
|
name: "editor_status",
|
|
16412
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
16412
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
16413
16413
|
parameters: {},
|
|
16414
16414
|
requires: []
|
|
16415
16415
|
},
|
|
@@ -16569,7 +16569,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16569
16569
|
value: {
|
|
16570
16570
|
ops: ["set"],
|
|
16571
16571
|
blank: true,
|
|
16572
|
-
description: `set: the value, fitted to the property's type. "" writes an empty string.`
|
|
16572
|
+
description: `set: the value, fitted to the property's type. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance.`
|
|
16573
16573
|
},
|
|
16574
16574
|
method: {
|
|
16575
16575
|
type: "string",
|
|
@@ -16579,7 +16579,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16579
16579
|
args: {
|
|
16580
16580
|
type: "array",
|
|
16581
16581
|
ops: ["call"],
|
|
16582
|
-
description:
|
|
16582
|
+
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused.`
|
|
16583
16583
|
}
|
|
16584
16584
|
},
|
|
16585
16585
|
requires: ["nodePath"],
|
|
@@ -38110,6 +38110,9 @@ function stillRunning(run) {
|
|
|
38110
38110
|
if (run?.exitCode !== null || run.endedUnwatched === true) {
|
|
38111
38111
|
return false;
|
|
38112
38112
|
}
|
|
38113
|
+
if (typeof run.endedHere === "string") {
|
|
38114
|
+
return false;
|
|
38115
|
+
}
|
|
38113
38116
|
if (!run.throughEditor && run.process === null && run.pid !== null) {
|
|
38114
38117
|
return alive(run.pid);
|
|
38115
38118
|
}
|
|
@@ -38122,6 +38125,9 @@ function runIsUp(run, editorSays, now = Date.now()) {
|
|
|
38122
38125
|
if (run?.throughEditor !== true) {
|
|
38123
38126
|
return stillRunning(run);
|
|
38124
38127
|
}
|
|
38128
|
+
if (typeof run.endedHere === "string") {
|
|
38129
|
+
return false;
|
|
38130
|
+
}
|
|
38125
38131
|
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
38126
38132
|
return false;
|
|
38127
38133
|
}
|
|
@@ -38130,6 +38136,22 @@ function runIsUp(run, editorSays, now = Date.now()) {
|
|
|
38130
38136
|
}
|
|
38131
38137
|
return editorSays ?? stillRunning(run);
|
|
38132
38138
|
}
|
|
38139
|
+
function noCodeWillCome(run) {
|
|
38140
|
+
return run.exitCode === null && run.process === null;
|
|
38141
|
+
}
|
|
38142
|
+
function endedWithoutACode(run) {
|
|
38143
|
+
if (!run.throughEditor) {
|
|
38144
|
+
return run.endedHere ? "This run was started by another server, so nothing here held it and its exit code was never collected. Everything it printed is below, read back from its transcript." : "This run was started by another server, the one this server replaced or one running beside it, and it ended with nothing here waiting on it, so its exit code was never collected. Everything it printed is below, read back from its transcript.";
|
|
38145
|
+
}
|
|
38146
|
+
const kept = "A game the editor plays is the editor's own child, so its exit code stays with the editor and none was collected here.";
|
|
38147
|
+
if (run.endedHere) {
|
|
38148
|
+
return `${kept} What it printed is below.`;
|
|
38149
|
+
}
|
|
38150
|
+
if (!run.announced && run.couldAnnounce) {
|
|
38151
|
+
return `The editor stopped playing this run before its game announced a runtime, so the game ended while it was starting or was closed in the editor. ${kept} What it printed is below, and editor_output with op "editor" has what the editor said about it.`;
|
|
38152
|
+
}
|
|
38153
|
+
return `The editor stopped playing this run. ${kept} What it printed is below.`;
|
|
38154
|
+
}
|
|
38133
38155
|
function endedPreviousRun(ended) {
|
|
38134
38156
|
if (ended === null) {
|
|
38135
38157
|
return;
|
|
@@ -39871,6 +39893,7 @@ class GodotServer {
|
|
|
39871
39893
|
pid: endpoint.pid,
|
|
39872
39894
|
port: endpoint.port,
|
|
39873
39895
|
project: endpoint.project,
|
|
39896
|
+
...this.ownProject === null ? {} : { ofThisProject: isSameDirectory(endpoint.project.path, this.ownProject) },
|
|
39874
39897
|
editorPid: endpoint.editorPid,
|
|
39875
39898
|
reachable: reply.ok,
|
|
39876
39899
|
problem: reply.ok ? null : reply.message
|
|
@@ -39902,7 +39925,7 @@ class GodotServer {
|
|
|
39902
39925
|
processActive: active,
|
|
39903
39926
|
playingInEditor: playing,
|
|
39904
39927
|
...hold,
|
|
39905
|
-
runtimeConnected: games.some((game) => game.reachable),
|
|
39928
|
+
runtimeConnected: games.some((game) => game.reachable && game.ofThisProject !== false),
|
|
39906
39929
|
runtimes: games,
|
|
39907
39930
|
unreadable: announced.unspoken.map((game) => ({
|
|
39908
39931
|
pid: game.pid,
|
|
@@ -40126,7 +40149,7 @@ class GodotServer {
|
|
|
40126
40149
|
if (before !== null && await this.runStillGoing(before)) {
|
|
40127
40150
|
this.logDebug("Ending the running game before starting another");
|
|
40128
40151
|
ended = { pid: before.pid ?? this.announcedPidOf(before) ?? null };
|
|
40129
|
-
await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
|
|
40152
|
+
await this.endActiveGame("editor_run start, which ends the run that was going before it starts another", true);
|
|
40130
40153
|
}
|
|
40131
40154
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
40132
40155
|
const wereHereFirst = this.everyRuntimeOfOurs();
|
|
@@ -40367,12 +40390,11 @@ class GodotServer {
|
|
|
40367
40390
|
game.brokeOn = halt.text;
|
|
40368
40391
|
game.log.record("error", halt.text);
|
|
40369
40392
|
}
|
|
40370
|
-
async endActiveGame(reason) {
|
|
40393
|
+
async endActiveGame(reason, going) {
|
|
40371
40394
|
const running = this.activeProcess;
|
|
40372
|
-
this.activeProcess = null;
|
|
40373
40395
|
const recorded = running?.process === null ? readRunRecord() : null;
|
|
40374
40396
|
clearRunRecord((record) => this.couldBeOurs(record.projectPath));
|
|
40375
|
-
if (!running) {
|
|
40397
|
+
if (!running || !going) {
|
|
40376
40398
|
return;
|
|
40377
40399
|
}
|
|
40378
40400
|
running.endedHere = reason;
|
|
@@ -40536,7 +40558,8 @@ class GodotServer {
|
|
|
40536
40558
|
return;
|
|
40537
40559
|
}
|
|
40538
40560
|
const going = this.activeProcess;
|
|
40539
|
-
|
|
40561
|
+
const givesWay = going !== null && !stillRunning(going) && typeof going.endedHere === "string";
|
|
40562
|
+
if (going !== null && !givesWay) {
|
|
40540
40563
|
if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
|
|
40541
40564
|
try {
|
|
40542
40565
|
await this.dap().connect();
|
|
@@ -40922,7 +40945,7 @@ class GodotServer {
|
|
|
40922
40945
|
const going = await this.runStillGoing(run);
|
|
40923
40946
|
if (run.endedUnwatched !== true && !going) {
|
|
40924
40947
|
run.log.finish();
|
|
40925
|
-
run.endedUnwatched = run
|
|
40948
|
+
run.endedUnwatched = noCodeWillCome(run);
|
|
40926
40949
|
}
|
|
40927
40950
|
const severity = readString(args, "severity");
|
|
40928
40951
|
const selected = run.log.select({
|
|
@@ -40943,8 +40966,18 @@ class GodotServer {
|
|
|
40943
40966
|
if (waitedMs !== undefined) {
|
|
40944
40967
|
notes.push(`The wait of ${waitedMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
|
|
40945
40968
|
}
|
|
40969
|
+
if (typeof run.endedHere === "string") {
|
|
40970
|
+
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
40971
|
+
} else if (!stillRunning(run) && !run.throughEditor && run.exitCode !== null) {
|
|
40972
|
+
notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : "Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
|
|
40973
|
+
}
|
|
40946
40974
|
if (run.endedUnwatched === true) {
|
|
40947
|
-
notes.push(
|
|
40975
|
+
notes.push(endedWithoutACode({
|
|
40976
|
+
throughEditor: run.throughEditor,
|
|
40977
|
+
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
40978
|
+
couldAnnounce: run.projectPath !== null && existsSync15(join23(run.projectPath, RUNTIME_AUTOLOAD2.path)),
|
|
40979
|
+
endedHere: typeof run.endedHere === "string"
|
|
40980
|
+
}));
|
|
40948
40981
|
}
|
|
40949
40982
|
if (run.pickedUpPlaying === true) {
|
|
40950
40983
|
notes.push(run.transcript === null ? "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." : "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.");
|
|
@@ -40953,11 +40986,6 @@ class GodotServer {
|
|
|
40953
40986
|
const back = this.dapClient?.isConnected() === true;
|
|
40954
40987
|
notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
|
|
40955
40988
|
}
|
|
40956
|
-
if (typeof run.endedHere === "string") {
|
|
40957
|
-
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
40958
|
-
} else if (!stillRunning(run) && !run.throughEditor && run.exitCode !== null) {
|
|
40959
|
-
notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : "Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
|
|
40960
|
-
}
|
|
40961
40989
|
if (run.transcript !== null && selected.omitted > 0) {
|
|
40962
40990
|
notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
|
|
40963
40991
|
}
|
|
@@ -40996,7 +41024,7 @@ class GodotServer {
|
|
|
40996
41024
|
const endedPid = stopped.pid ?? announcedPid;
|
|
40997
41025
|
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
40998
41026
|
this.logDebug("Stopping the running game");
|
|
40999
|
-
await this.endActiveGame("editor_run stop");
|
|
41027
|
+
await this.endActiveGame("editor_run stop", wasRunning);
|
|
41000
41028
|
const ended = children === null ? null : endChildrenAmong(children);
|
|
41001
41029
|
if (wasRunning) {
|
|
41002
41030
|
for (const pid of new Set([endedPid, announcedPid])) {
|
|
@@ -41015,12 +41043,12 @@ class GodotServer {
|
|
|
41015
41043
|
childrenLeft: ended.left.length === 0 ? undefined : ended.left,
|
|
41016
41044
|
childrenUnknown: ended.unknown === false ? undefined : true
|
|
41017
41045
|
},
|
|
41018
|
-
exitedBeforeStop:
|
|
41046
|
+
exitedBeforeStop: !wasRunning,
|
|
41019
41047
|
exitCode: stopped.exitCode,
|
|
41020
41048
|
errors: stopped.log.count("error"),
|
|
41021
41049
|
warnings: stopped.log.count("warning"),
|
|
41022
41050
|
clean: stopped.log.count("error") === 0,
|
|
41023
|
-
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : `${stopped.throughEditor ? "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."} ${aboutTheChildren(ended, stopped.throughEditor)}`,
|
|
41051
|
+
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : `${stopped.throughEditor ? endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."} ${aboutTheChildren(ended, stopped.throughEditor)}`,
|
|
41024
41052
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
41025
41053
|
});
|
|
41026
41054
|
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Colon paths through what a node holds: "/root/Main" with "_game:run:day" reaches the day off the
|
|
4
|
+
## run off the game the node keeps. Every op that reads, writes, calls or waits on a game's state
|
|
5
|
+
## walks the same way, so the walk and what it says when a step is not there live in one place.
|
|
6
|
+
|
|
7
|
+
const Values = preload("runtime_values.gd")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## What [param reaching] names something on, which is the node itself until the name has a colon
|
|
11
|
+
## in it, and the last name along that path.
|
|
12
|
+
##
|
|
13
|
+
## A game's state does not sit on nodes, it hangs off them: the speed of the clock is a property of
|
|
14
|
+
## a [RefCounted] held by a [RefCounted] held by the root, and that is what an agent asks about.
|
|
15
|
+
## What a game does hangs off them the same way, so the last name is a property to read, a property
|
|
16
|
+
## to write or a method to call, and the walk to it is one walk.
|
|
17
|
+
##
|
|
18
|
+
## Colons, because that is the separator [method Object.get_indexed] already takes. Walked a step
|
|
19
|
+
## at a time rather than handed to that method, which answers null for a path that goes wrong
|
|
20
|
+
## halfway along and for one that ends on null.
|
|
21
|
+
##
|
|
22
|
+
## A step written as a call, "get_viewport()", calls a method of that name that takes no arguments
|
|
23
|
+
## and walks into what it returned. Some of what hangs off a node is only reachable by asking:
|
|
24
|
+
## which control holds the focus is a question for the viewport a control is in, and the viewport
|
|
25
|
+
## is a method's answer rather than a property. Spelled with the brackets so a reader sees a call
|
|
26
|
+
## where one happens, and so a method can never shadow a property of the same name.
|
|
27
|
+
##
|
|
28
|
+
## Answers with a `message` instead when a step along the way is not there or holds something that
|
|
29
|
+
## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
|
|
30
|
+
## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle.
|
|
31
|
+
static func walk_to(node: Node, node_path: String, reaching: String) -> Dictionary:
|
|
32
|
+
var parts: PackedStringArray = reaching.split(":")
|
|
33
|
+
var holder: Variant = node
|
|
34
|
+
var called: String = node_path
|
|
35
|
+
for step: int in parts.size() - 1:
|
|
36
|
+
var named: String = parts[step]
|
|
37
|
+
if not can_read(holder, named):
|
|
38
|
+
return {"type": "error", "message": nothing_there(holder, named, called)}
|
|
39
|
+
holder = read_under(holder, named)
|
|
40
|
+
called = "%s:%s" % [called, named]
|
|
41
|
+
if not _can_hold(holder):
|
|
42
|
+
var wanted: String = parts[step + 1]
|
|
43
|
+
return {"type": "error", "message": "%s holds no object to read %s off" % [called, wanted]}
|
|
44
|
+
return {"holder": holder, "name": parts[parts.size() - 1], "called": called}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## The object [param given] names for a method's argument, as `{"object": ...}`, or as
|
|
48
|
+
## `{"message": ...}` saying why it names none.
|
|
49
|
+
##
|
|
50
|
+
## A colon path read from [param node], the node the call was made on, the way the method's own
|
|
51
|
+
## path is: "_ladder:shop:_wares:-15". One beginning with a slash starts at the node it names
|
|
52
|
+
## instead, "/root/Main/Hud" or "/root/Main:_game:run". The record runtime_inspect answers an object
|
|
53
|
+
## with is not accepted, because its id is a 64-bit number and does not survive being sent as JSON:
|
|
54
|
+
## a rounded id names another object or none, and the path names the one meant.
|
|
55
|
+
static func object_at(root: Node, node: Node, node_path: String, given: Variant) -> Dictionary:
|
|
56
|
+
if given is Dictionary:
|
|
57
|
+
return {
|
|
58
|
+
"message":
|
|
59
|
+
(
|
|
60
|
+
"an object is named by its path, not by the record runtime_inspect answered with:"
|
|
61
|
+
+ " its id does not survive the trip as a number"
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
var path: String = given
|
|
65
|
+
var start: Node = node
|
|
66
|
+
var start_path: String = node_path
|
|
67
|
+
var reaching: String = path
|
|
68
|
+
if path.begins_with("/"):
|
|
69
|
+
var colon: int = path.find(":")
|
|
70
|
+
start_path = path if colon == -1 else path.substr(0, colon)
|
|
71
|
+
var standing: Dictionary = Values.node_at(root, start_path)
|
|
72
|
+
if standing.has("message"):
|
|
73
|
+
return {"message": str(standing["message"])}
|
|
74
|
+
start = standing["node"]
|
|
75
|
+
if colon == -1:
|
|
76
|
+
return {"object": start}
|
|
77
|
+
reaching = path.substr(colon + 1)
|
|
78
|
+
if reaching.is_empty():
|
|
79
|
+
return {"message": "%s names nothing past the node" % path}
|
|
80
|
+
var reached: Dictionary = walk_to(start, start_path, reaching)
|
|
81
|
+
if reached.has("message"):
|
|
82
|
+
return {"message": str(reached["message"])}
|
|
83
|
+
var holder: Variant = reached["holder"]
|
|
84
|
+
var name: String = reached["name"]
|
|
85
|
+
if not can_read(holder, name):
|
|
86
|
+
return {"message": nothing_there(holder, name, str(reached["called"]))}
|
|
87
|
+
var value: Variant = read_under(holder, name)
|
|
88
|
+
if not value is Object or not is_instance_valid(value):
|
|
89
|
+
return {
|
|
90
|
+
"message": "%s:%s holds %s, not an object" % [reached["called"], name, type_string(typeof(value))]
|
|
91
|
+
}
|
|
92
|
+
return {"object": value}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
## Whether a step can be taken into [param value] at all: an object, a list or a map can be walked
|
|
96
|
+
## into, and a number or a string is where a path ends whether the caller meant it to or not.
|
|
97
|
+
static func _can_hold(value: Variant) -> bool:
|
|
98
|
+
return value is Object or value is Array or value is Dictionary
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
## What is wrong with reading [param named] off [param holder], or "" when nothing is.
|
|
102
|
+
##
|
|
103
|
+
## Public because a wait asks the same question before it starts waiting, and used to answer it by
|
|
104
|
+
## waiting: a property nobody has spends the whole timeout and comes back "not met", which reads
|
|
105
|
+
## exactly like a game that never reached the state. The read refuses a mistyped name in one call
|
|
106
|
+
## and says so, and the two tools disagreeing about one typo is worth more than the duplication.
|
|
107
|
+
static func nothing_under(holder: Variant, named: String, called: String) -> String:
|
|
108
|
+
return "" if can_read(holder, named) else nothing_there(holder, named, called)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
## Whether [param named] is something [param holder] has.
|
|
112
|
+
##
|
|
113
|
+
## Three kinds of holder, because the state a game keeps is not all properties of objects: a roster
|
|
114
|
+
## is a list and an in-tray is a map, and a path that could not step into either stopped at the
|
|
115
|
+
## first one. An index reads the way a property does, and a negative one counts from the end the
|
|
116
|
+
## way GDScript's own does, so reading the last of something does not mean asking how many first.
|
|
117
|
+
static func can_read(holder: Variant, named: String) -> bool:
|
|
118
|
+
if holder is Array:
|
|
119
|
+
var items: Array = holder
|
|
120
|
+
return _index_in(named, items.size()) != -1
|
|
121
|
+
if holder is Dictionary:
|
|
122
|
+
var map: Dictionary = holder
|
|
123
|
+
return map.has(named) or map.has(StringName(named))
|
|
124
|
+
if holder is Object:
|
|
125
|
+
var object: Object = holder
|
|
126
|
+
var method: String = method_of(named)
|
|
127
|
+
if not method.is_empty():
|
|
128
|
+
return _arguments_required(object, method) == 0
|
|
129
|
+
return _has_property(object, named)
|
|
130
|
+
return false
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
## The method a step written as a call names, "get_viewport()" being get_viewport, or "" for a
|
|
134
|
+
## step that is a name.
|
|
135
|
+
static func method_of(named: String) -> String:
|
|
136
|
+
return named.trim_suffix("()") if named.ends_with("()") else ""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
## How many arguments [param method] on [param object] has to be given, or -1 when there is no such
|
|
140
|
+
## method. Read off the method list rather than [method Object.has_method], because a path can only
|
|
141
|
+
## call a method with nothing, and a method with a required argument called with nothing raises
|
|
142
|
+
## inside the game rather than answering.
|
|
143
|
+
static func _arguments_required(object: Object, method: String) -> int:
|
|
144
|
+
for entry: Dictionary in object.get_method_list():
|
|
145
|
+
if str(entry.get("name", "")) != method:
|
|
146
|
+
continue
|
|
147
|
+
var params: Array = entry.get("args", [])
|
|
148
|
+
var defaults: Array = entry.get("default_args", [])
|
|
149
|
+
return params.size() - defaults.size()
|
|
150
|
+
return -1
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
## Put [param value] where [param named] points. Only ever called once [method can_read] agrees,
|
|
154
|
+
## and lists and maps are reference types, so writing into one reaches the game's own copy.
|
|
155
|
+
static func write(holder: Variant, named: String, value: Variant) -> void:
|
|
156
|
+
if holder is Array:
|
|
157
|
+
var items: Array = holder
|
|
158
|
+
items[_index_in(named, items.size())] = value
|
|
159
|
+
return
|
|
160
|
+
if holder is Dictionary:
|
|
161
|
+
var map: Dictionary = holder
|
|
162
|
+
# Written back under the key it was found under, since a map keyed by StringName and one
|
|
163
|
+
# keyed by String both read the same way and a write to the wrong one adds a second entry.
|
|
164
|
+
if map.has(named):
|
|
165
|
+
map[named] = value
|
|
166
|
+
else:
|
|
167
|
+
map[StringName(named)] = value
|
|
168
|
+
return
|
|
169
|
+
var object: Object = holder
|
|
170
|
+
object.set(named, value)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
## What [param holder] holds under [param named], or answers to it when [param named] is a call.
|
|
174
|
+
## Only ever called once [method can_read] agrees.
|
|
175
|
+
static func read_under(holder: Variant, named: String) -> Variant:
|
|
176
|
+
if holder is Array:
|
|
177
|
+
var items: Array = holder
|
|
178
|
+
return items[_index_in(named, items.size())]
|
|
179
|
+
if holder is Dictionary:
|
|
180
|
+
var map: Dictionary = holder
|
|
181
|
+
if map.has(named):
|
|
182
|
+
return map[named]
|
|
183
|
+
return map[StringName(named)]
|
|
184
|
+
var object: Object = holder
|
|
185
|
+
var method: String = method_of(named)
|
|
186
|
+
if not method.is_empty():
|
|
187
|
+
return object.call(method)
|
|
188
|
+
return object.get(named)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
## Where [param named] lands in a list of [param size], or -1 for a step that is not in it.
|
|
192
|
+
##
|
|
193
|
+
## -1 for "nowhere" is safe because every answer this gives is an index into a list that long, so a
|
|
194
|
+
## real one is never negative by the time it is returned.
|
|
195
|
+
static func _index_in(named: String, size: int) -> int:
|
|
196
|
+
if not named.is_valid_int():
|
|
197
|
+
return -1
|
|
198
|
+
var index: int = int(named)
|
|
199
|
+
if index < 0:
|
|
200
|
+
index += size
|
|
201
|
+
return index if index >= 0 and index < size else -1
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
## Why a step could not be taken, said in the terms of what was actually there.
|
|
205
|
+
##
|
|
206
|
+
## "has no property 0" about a list is a true sentence that sends somebody looking for a property,
|
|
207
|
+
## so a list says how long it is and a map says what it is keyed by. The step is named rather than
|
|
208
|
+
## the whole path, because the step is the typo.
|
|
209
|
+
static func nothing_there(holder: Variant, named: String, called_as: String) -> String:
|
|
210
|
+
if holder is Array:
|
|
211
|
+
var items: Array = holder
|
|
212
|
+
return "%s is a list of %d, so there is no %s in it" % [called_as, items.size(), named]
|
|
213
|
+
if holder is Dictionary:
|
|
214
|
+
var map: Dictionary = holder
|
|
215
|
+
var keys: Array = map.keys()
|
|
216
|
+
# Eight of them, because a map keyed by something unexpected is told by the first few and a
|
|
217
|
+
# map of two hundred would bury the sentence saying which key was missing.
|
|
218
|
+
var some: Array[String] = []
|
|
219
|
+
for key: Variant in keys.slice(0, 8):
|
|
220
|
+
some.append(str(key))
|
|
221
|
+
var rest: String = ""
|
|
222
|
+
if keys.size() > 8:
|
|
223
|
+
rest = " and %d more" % [keys.size() - 8]
|
|
224
|
+
return "%s has no key %s; it is keyed by %s%s" % [called_as, named, ", ".join(some), rest]
|
|
225
|
+
var object: Object = holder
|
|
226
|
+
var method: String = method_of(named)
|
|
227
|
+
if not method.is_empty():
|
|
228
|
+
var required: int = _arguments_required(object, method)
|
|
229
|
+
if required == -1:
|
|
230
|
+
return "%s has no method %s" % [called_as, method]
|
|
231
|
+
return (
|
|
232
|
+
"%s.%s takes %d argument%s, and a path can only call a method that takes none"
|
|
233
|
+
% [called_as, method, required, "" if required == 1 else "s"]
|
|
234
|
+
)
|
|
235
|
+
# A method spelled as a property is the likeliest thing behind a name the object has no property
|
|
236
|
+
# for, and the caller is one pair of brackets away from what they meant.
|
|
237
|
+
if object.has_method(named):
|
|
238
|
+
return (
|
|
239
|
+
"%s has no property %s; it has a method of that name, which a path calls as %s()"
|
|
240
|
+
% [called_as, named, named]
|
|
241
|
+
)
|
|
242
|
+
return "%s has no property %s" % [called_as, named]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
## Whether [param holder] declares [param named]. Asked of the list rather than read, because a
|
|
246
|
+
## property read back as null answers it wrongly.
|
|
247
|
+
static func _has_property(holder: Object, named: String) -> bool:
|
|
248
|
+
for entry: Dictionary in holder.get_property_list():
|
|
249
|
+
if str(entry["name"]) == named:
|
|
250
|
+
return true
|
|
251
|
+
return false
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://cna71d704esno
|
|
@@ -3,6 +3,7 @@ extends RefCounted
|
|
|
3
3
|
## What the server asks about the running tree: its shape, one node, the nodes matching a
|
|
4
4
|
## question, where one is on screen, a property set, a method called, the metrics.
|
|
5
5
|
|
|
6
|
+
const Paths = preload("runtime_paths.gd")
|
|
6
7
|
const Read = preload("reading.gd")
|
|
7
8
|
const Values = preload("runtime_values.gd")
|
|
8
9
|
|
|
@@ -182,19 +183,19 @@ func _found(node: Node, wanted_property: String) -> Dictionary:
|
|
|
182
183
|
if wanted_property.is_empty():
|
|
183
184
|
return entry
|
|
184
185
|
entry["property"] = wanted_property
|
|
185
|
-
# Through a path as well, for the reason [method walk_to] gives. A step that is not there on
|
|
186
|
+
# Through a path as well, for the reason [method Paths.walk_to] gives. A step that is not there on
|
|
186
187
|
# this node reads as not having the property rather than as a refusal, which is the rule this
|
|
187
188
|
# whole function is written to: a find matches nodes of several classes on purpose.
|
|
188
|
-
var reached: Dictionary = walk_to(node, str(entry["path"]), wanted_property)
|
|
189
|
+
var reached: Dictionary = Paths.walk_to(node, str(entry["path"]), wanted_property)
|
|
189
190
|
if reached.has("message"):
|
|
190
191
|
entry["has_property"] = false
|
|
191
192
|
return entry
|
|
192
193
|
var holder: Variant = reached["holder"]
|
|
193
194
|
var named: String = reached["name"]
|
|
194
|
-
var has: bool =
|
|
195
|
+
var has: bool = Paths.can_read(holder, named)
|
|
195
196
|
entry["has_property"] = has
|
|
196
197
|
if has:
|
|
197
|
-
entry["value"] = _values.serialize(read_under(holder, named))
|
|
198
|
+
entry["value"] = _values.serialize(Paths.read_under(holder, named))
|
|
198
199
|
return entry
|
|
199
200
|
|
|
200
201
|
|
|
@@ -548,7 +549,7 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
548
549
|
return standing
|
|
549
550
|
var node: Node = standing["node"]
|
|
550
551
|
|
|
551
|
-
var reached: Dictionary = walk_to(node, node_path, property)
|
|
552
|
+
var reached: Dictionary = Paths.walk_to(node, node_path, property)
|
|
552
553
|
if reached.has("message"):
|
|
553
554
|
return reached
|
|
554
555
|
|
|
@@ -556,213 +557,17 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
556
557
|
var named: String = reached["name"]
|
|
557
558
|
# Asked of the holder rather than read and compared to null, because a property the holder does
|
|
558
559
|
# not have and a property that is null both read as null.
|
|
559
|
-
if not
|
|
560
|
-
return {"type": "error", "message":
|
|
560
|
+
if not Paths.can_read(holder, named):
|
|
561
|
+
return {"type": "error", "message": Paths.nothing_there(holder, named, str(reached["called"]))}
|
|
561
562
|
|
|
562
563
|
return {
|
|
563
564
|
"type": "property",
|
|
564
565
|
"path": node_path,
|
|
565
566
|
"property": property,
|
|
566
|
-
"value": _values.serialize(read_under(holder, named)),
|
|
567
|
+
"value": _values.serialize(Paths.read_under(holder, named)),
|
|
567
568
|
}
|
|
568
569
|
|
|
569
570
|
|
|
570
|
-
## What [param reaching] names something on, which is the node itself until the name has a colon
|
|
571
|
-
## in it, and the last name along that path.
|
|
572
|
-
##
|
|
573
|
-
## A game's state does not sit on nodes, it hangs off them: the speed of the clock is a property of
|
|
574
|
-
## a [RefCounted] held by a [RefCounted] held by the root, and that is what an agent asks about.
|
|
575
|
-
## What a game does hangs off them the same way, so the last name is a property to read, a property
|
|
576
|
-
## to write or a method to call, and the walk to it is one walk.
|
|
577
|
-
##
|
|
578
|
-
## Colons, because that is the separator [method Object.get_indexed] already takes. Walked a step
|
|
579
|
-
## at a time rather than handed to that method, which answers null for a path that goes wrong
|
|
580
|
-
## halfway along and for one that ends on null.
|
|
581
|
-
##
|
|
582
|
-
## A step written as a call, "get_viewport()", calls a method of that name that takes no arguments
|
|
583
|
-
## and walks into what it returned. Some of what hangs off a node is only reachable by asking:
|
|
584
|
-
## which control holds the focus is a question for the viewport a control is in, and the viewport
|
|
585
|
-
## is a method's answer rather than a property. Spelled with the brackets so a reader sees a call
|
|
586
|
-
## where one happens, and so a method can never shadow a property of the same name.
|
|
587
|
-
##
|
|
588
|
-
## Answers with a `message` instead when a step along the way is not there or holds something that
|
|
589
|
-
## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
|
|
590
|
-
## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle.
|
|
591
|
-
static func walk_to(node: Node, node_path: String, reaching: String) -> Dictionary:
|
|
592
|
-
var parts: PackedStringArray = reaching.split(":")
|
|
593
|
-
var holder: Variant = node
|
|
594
|
-
var called: String = node_path
|
|
595
|
-
for step: int in parts.size() - 1:
|
|
596
|
-
var named: String = parts[step]
|
|
597
|
-
if not _can_read(holder, named):
|
|
598
|
-
return {"type": "error", "message": _nothing_there(holder, named, called)}
|
|
599
|
-
holder = read_under(holder, named)
|
|
600
|
-
called = "%s:%s" % [called, named]
|
|
601
|
-
if not _can_hold(holder):
|
|
602
|
-
var wanted: String = parts[step + 1]
|
|
603
|
-
return {"type": "error", "message": "%s holds no object to read %s off" % [called, wanted]}
|
|
604
|
-
return {"holder": holder, "name": parts[parts.size() - 1], "called": called}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
## Whether a step can be taken into [param value] at all: an object, a list or a map can be walked
|
|
608
|
-
## into, and a number or a string is where a path ends whether the caller meant it to or not.
|
|
609
|
-
static func _can_hold(value: Variant) -> bool:
|
|
610
|
-
return value is Object or value is Array or value is Dictionary
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
## Whether [param named] is something [param holder] has.
|
|
614
|
-
##
|
|
615
|
-
## Three kinds of holder, because the state a game keeps is not all properties of objects: a roster
|
|
616
|
-
## is a list and an in-tray is a map, and a path that could not step into either stopped at the
|
|
617
|
-
## first one. An index reads the way a property does, and a negative one counts from the end the
|
|
618
|
-
## way GDScript's own does, so reading the last of something does not mean asking how many first.
|
|
619
|
-
## What is wrong with reading [param named] off [param holder], or "" when nothing is.
|
|
620
|
-
##
|
|
621
|
-
## Public because a wait asks the same question before it starts waiting, and used to answer it by
|
|
622
|
-
## waiting: a property nobody has spends the whole timeout and comes back "not met", which reads
|
|
623
|
-
## exactly like a game that never reached the state. The read refuses a mistyped name in one call
|
|
624
|
-
## and says so, and the two tools disagreeing about one typo is worth more than the duplication.
|
|
625
|
-
static func nothing_under(holder: Variant, named: String, called: String) -> String:
|
|
626
|
-
return "" if _can_read(holder, named) else _nothing_there(holder, named, called)
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
static func _can_read(holder: Variant, named: String) -> bool:
|
|
630
|
-
if holder is Array:
|
|
631
|
-
var items: Array = holder
|
|
632
|
-
return _index_in(named, items.size()) != -1
|
|
633
|
-
if holder is Dictionary:
|
|
634
|
-
var map: Dictionary = holder
|
|
635
|
-
return map.has(named) or map.has(StringName(named))
|
|
636
|
-
if holder is Object:
|
|
637
|
-
var object: Object = holder
|
|
638
|
-
var method: String = _called(named)
|
|
639
|
-
if not method.is_empty():
|
|
640
|
-
return _arguments_required(object, method) == 0
|
|
641
|
-
return _has_property(object, named)
|
|
642
|
-
return false
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
## The method a step written as a call names, "get_viewport()" being get_viewport, or "" for a
|
|
646
|
-
## step that is a name.
|
|
647
|
-
static func _called(named: String) -> String:
|
|
648
|
-
return named.trim_suffix("()") if named.ends_with("()") else ""
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
## How many arguments [param method] on [param object] has to be given, or -1 when there is no such
|
|
652
|
-
## method. Read off the method list rather than [method Object.has_method], because a path can only
|
|
653
|
-
## call a method with nothing, and a method with a required argument called with nothing raises
|
|
654
|
-
## inside the game rather than answering.
|
|
655
|
-
static func _arguments_required(object: Object, method: String) -> int:
|
|
656
|
-
for entry: Dictionary in object.get_method_list():
|
|
657
|
-
if str(entry.get("name", "")) != method:
|
|
658
|
-
continue
|
|
659
|
-
var params: Array = entry.get("args", [])
|
|
660
|
-
var defaults: Array = entry.get("default_args", [])
|
|
661
|
-
return params.size() - defaults.size()
|
|
662
|
-
return -1
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
## Put [param value] where [param named] points. Only ever called once [method _can_read] agrees,
|
|
666
|
-
## and lists and maps are reference types, so writing into one reaches the game's own copy.
|
|
667
|
-
static func _write(holder: Variant, named: String, value: Variant) -> void:
|
|
668
|
-
if holder is Array:
|
|
669
|
-
var items: Array = holder
|
|
670
|
-
items[_index_in(named, items.size())] = value
|
|
671
|
-
return
|
|
672
|
-
if holder is Dictionary:
|
|
673
|
-
var map: Dictionary = holder
|
|
674
|
-
# Written back under the key it was found under, since a map keyed by StringName and one
|
|
675
|
-
# keyed by String both read the same way and a write to the wrong one adds a second entry.
|
|
676
|
-
if map.has(named):
|
|
677
|
-
map[named] = value
|
|
678
|
-
else:
|
|
679
|
-
map[StringName(named)] = value
|
|
680
|
-
return
|
|
681
|
-
var object: Object = holder
|
|
682
|
-
object.set(named, value)
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
## What [param holder] holds under [param named], or answers to it when [param named] is a call.
|
|
686
|
-
## Only ever called once [method _can_read] agrees.
|
|
687
|
-
static func read_under(holder: Variant, named: String) -> Variant:
|
|
688
|
-
if holder is Array:
|
|
689
|
-
var items: Array = holder
|
|
690
|
-
return items[_index_in(named, items.size())]
|
|
691
|
-
if holder is Dictionary:
|
|
692
|
-
var map: Dictionary = holder
|
|
693
|
-
if map.has(named):
|
|
694
|
-
return map[named]
|
|
695
|
-
return map[StringName(named)]
|
|
696
|
-
var object: Object = holder
|
|
697
|
-
var method: String = _called(named)
|
|
698
|
-
if not method.is_empty():
|
|
699
|
-
return object.call(method)
|
|
700
|
-
return object.get(named)
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
## Where [param named] lands in a list of [param size], or -1 for a step that is not in it.
|
|
704
|
-
##
|
|
705
|
-
## -1 for "nowhere" is safe because every answer this gives is an index into a list that long, so a
|
|
706
|
-
## real one is never negative by the time it is returned.
|
|
707
|
-
static func _index_in(named: String, size: int) -> int:
|
|
708
|
-
if not named.is_valid_int():
|
|
709
|
-
return -1
|
|
710
|
-
var index: int = int(named)
|
|
711
|
-
if index < 0:
|
|
712
|
-
index += size
|
|
713
|
-
return index if index >= 0 and index < size else -1
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
## Why a step could not be taken, said in the terms of what was actually there.
|
|
717
|
-
##
|
|
718
|
-
## "has no property 0" about a list is a true sentence that sends somebody looking for a property,
|
|
719
|
-
## so a list says how long it is and a map says what it is keyed by. The step is named rather than
|
|
720
|
-
## the whole path, because the step is the typo.
|
|
721
|
-
static func _nothing_there(holder: Variant, named: String, called: String) -> String:
|
|
722
|
-
if holder is Array:
|
|
723
|
-
var items: Array = holder
|
|
724
|
-
return "%s is a list of %d, so there is no %s in it" % [called, items.size(), named]
|
|
725
|
-
if holder is Dictionary:
|
|
726
|
-
var map: Dictionary = holder
|
|
727
|
-
var keys: Array = map.keys()
|
|
728
|
-
# Eight of them, because a map keyed by something unexpected is told by the first few and a
|
|
729
|
-
# map of two hundred would bury the sentence saying which key was missing.
|
|
730
|
-
var some: Array[String] = []
|
|
731
|
-
for key: Variant in keys.slice(0, 8):
|
|
732
|
-
some.append(str(key))
|
|
733
|
-
var rest: String = ""
|
|
734
|
-
if keys.size() > 8:
|
|
735
|
-
rest = " and %d more" % [keys.size() - 8]
|
|
736
|
-
return "%s has no key %s; it is keyed by %s%s" % [called, named, ", ".join(some), rest]
|
|
737
|
-
var object: Object = holder
|
|
738
|
-
var method: String = _called(named)
|
|
739
|
-
if not method.is_empty():
|
|
740
|
-
var required: int = _arguments_required(object, method)
|
|
741
|
-
if required == -1:
|
|
742
|
-
return "%s has no method %s" % [called, method]
|
|
743
|
-
return (
|
|
744
|
-
"%s.%s takes %d argument%s, and a path can only call a method that takes none"
|
|
745
|
-
% [called, method, required, "" if required == 1 else "s"]
|
|
746
|
-
)
|
|
747
|
-
# A method spelled as a property is the likeliest thing behind a name the object has no property
|
|
748
|
-
# for, and the caller is one pair of brackets away from what they meant.
|
|
749
|
-
if object.has_method(named):
|
|
750
|
-
return (
|
|
751
|
-
"%s has no property %s; it has a method of that name, which a path calls as %s()"
|
|
752
|
-
% [called, named, named]
|
|
753
|
-
)
|
|
754
|
-
return "%s has no property %s" % [called, named]
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
## Whether [param holder] declares [param named]. Its own function because three ops ask it and a
|
|
758
|
-
## property read back as null answers it wrongly.
|
|
759
|
-
static func _has_property(holder: Object, named: String) -> bool:
|
|
760
|
-
for entry: Dictionary in holder.get_property_list():
|
|
761
|
-
if str(entry["name"]) == named:
|
|
762
|
-
return true
|
|
763
|
-
return false
|
|
764
|
-
|
|
765
|
-
|
|
766
571
|
func set_property(params: Dictionary) -> Dictionary:
|
|
767
572
|
var node_path: String = str(params.get("path", ""))
|
|
768
573
|
var property: String = str(params.get("property", ""))
|
|
@@ -776,10 +581,10 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
776
581
|
return standing
|
|
777
582
|
var node: Node = standing["node"]
|
|
778
583
|
|
|
779
|
-
# Through a path as well, for the reason [method walk_to] gives, and read back off the same
|
|
584
|
+
# Through a path as well, for the reason [method Paths.walk_to] gives, and read back off the same
|
|
780
585
|
# holder afterwards: a set that does not take says so by answering with the old value, which is
|
|
781
586
|
# how a typed container refusing a write is told from one accepting it.
|
|
782
|
-
var reached: Dictionary = walk_to(node, node_path, property)
|
|
587
|
+
var reached: Dictionary = Paths.walk_to(node, node_path, property)
|
|
783
588
|
if reached.has("message"):
|
|
784
589
|
return reached
|
|
785
590
|
|
|
@@ -787,7 +592,7 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
787
592
|
var named: String = reached["name"]
|
|
788
593
|
# A call can be walked through and read, and is not a place: what it returns is the method's
|
|
789
594
|
# to hand out, and writing "into" it would set nothing the game keeps.
|
|
790
|
-
if not
|
|
595
|
+
if not Paths.method_of(named).is_empty():
|
|
791
596
|
return {
|
|
792
597
|
"type": "error",
|
|
793
598
|
"message":
|
|
@@ -796,31 +601,42 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
796
601
|
% [reached["called"], named]
|
|
797
602
|
)
|
|
798
603
|
}
|
|
799
|
-
if not
|
|
800
|
-
return {"type": "error", "message":
|
|
801
|
-
var old_value: Variant = read_under(holder, named)
|
|
604
|
+
if not Paths.can_read(holder, named):
|
|
605
|
+
return {"type": "error", "message": Paths.nothing_there(holder, named, str(reached["called"]))}
|
|
606
|
+
var old_value: Variant = Paths.read_under(holder, named)
|
|
802
607
|
# The same rule the call path holds: a value that cannot become what the property holds is
|
|
803
608
|
# refused rather than written. Writing it means the engine picks something, and what it picks
|
|
804
609
|
# for a word where a number goes is zero, which the answer then reports as the new value.
|
|
805
|
-
var
|
|
806
|
-
var
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
"
|
|
813
|
-
% [reached["called"], named,
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
610
|
+
var slot: Dictionary = Values.slot_declared(holder, named)
|
|
611
|
+
var wanted: int = slot["type"] if slot["type"] != TYPE_NIL else typeof(old_value)
|
|
612
|
+
var given: Variant
|
|
613
|
+
if wanted == TYPE_OBJECT and (value is String or value is Dictionary):
|
|
614
|
+
var named_object: Dictionary = _object_named(node, node_path, value, str(slot["class"]))
|
|
615
|
+
if named_object.has("message"):
|
|
616
|
+
return {
|
|
617
|
+
"type": "error",
|
|
618
|
+
"message": "%s.%s holds an object: %s" % [reached["called"], named, named_object["message"]]
|
|
619
|
+
}
|
|
620
|
+
given = named_object["object"]
|
|
621
|
+
else:
|
|
622
|
+
given = _values.fitted(value, wanted)
|
|
623
|
+
if not Values.acceptable(given, wanted):
|
|
624
|
+
return {
|
|
625
|
+
"type": "error",
|
|
626
|
+
"message":
|
|
627
|
+
(
|
|
628
|
+
"%s.%s holds %s and the value given is %s, which cannot become one."
|
|
629
|
+
% [reached["called"], named, type_string(wanted), type_string(typeof(given))]
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
Paths.write(holder, named, given)
|
|
817
633
|
|
|
818
634
|
return {
|
|
819
635
|
"type": "property_set",
|
|
820
636
|
"path": node_path,
|
|
821
637
|
"property": property,
|
|
822
638
|
"old_value": _values.serialize(old_value),
|
|
823
|
-
"new_value": _values.serialize(read_under(holder, named))
|
|
639
|
+
"new_value": _values.serialize(Paths.read_under(holder, named))
|
|
824
640
|
}
|
|
825
641
|
|
|
826
642
|
|
|
@@ -837,10 +653,10 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
837
653
|
return standing
|
|
838
654
|
var node: Node = standing["node"]
|
|
839
655
|
|
|
840
|
-
# Through a path as well, for the reason [method walk_to] gives. What a game does hangs off its
|
|
656
|
+
# Through a path as well, for the reason [method Paths.walk_to] gives. What a game does hangs off its
|
|
841
657
|
# nodes as much as its state does, so reading `_game:run:day` while being unable to call
|
|
842
658
|
# `_game:run:advance` answers half of what a node holds and refuses the other half.
|
|
843
|
-
var reached: Dictionary = walk_to(node, node_path, method)
|
|
659
|
+
var reached: Dictionary = Paths.walk_to(node, node_path, method)
|
|
844
660
|
if reached.has("message"):
|
|
845
661
|
return reached
|
|
846
662
|
|
|
@@ -862,8 +678,8 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
862
678
|
# The method a call ends on is the one being called whether or not it carries the brackets a
|
|
863
679
|
# step along the way would: "get_viewport():gui_get_focus_owner()" is the same call as without
|
|
864
680
|
# the last pair.
|
|
865
|
-
if not
|
|
866
|
-
named =
|
|
681
|
+
if not Paths.method_of(named).is_empty():
|
|
682
|
+
named = Paths.method_of(named)
|
|
867
683
|
if not holder.has_method(named):
|
|
868
684
|
return {"type": "error", "message": "%s has no method %s" % [reached["called"], named]}
|
|
869
685
|
|
|
@@ -874,6 +690,17 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
874
690
|
var deserialized_args: Array = []
|
|
875
691
|
for index: int in args.size():
|
|
876
692
|
var wants: int = _values.parameter_type(holder, named, index)
|
|
693
|
+
if wants == TYPE_OBJECT and (args[index] is String or args[index] is Dictionary):
|
|
694
|
+
var declared: String = _values.parameter_class(holder, named, index)
|
|
695
|
+
var named_object: Dictionary = _object_named(node, node_path, args[index], declared)
|
|
696
|
+
if named_object.has("message"):
|
|
697
|
+
return {
|
|
698
|
+
"type": "error",
|
|
699
|
+
"message":
|
|
700
|
+
"%s.%s argument %d: %s" % [reached["called"], named, index + 1, named_object["message"]]
|
|
701
|
+
}
|
|
702
|
+
deserialized_args.append(named_object["object"])
|
|
703
|
+
continue
|
|
877
704
|
var given: Variant = _values.fitted(args[index], wants)
|
|
878
705
|
if not Values.acceptable(given, wants):
|
|
879
706
|
return {
|
|
@@ -891,6 +718,23 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
891
718
|
return {"type": "method_result", "path": node_path, "method": method, "result": _values.serialize(result)}
|
|
892
719
|
|
|
893
720
|
|
|
721
|
+
## The object [param given] names for a slot or a parameter declared as [param declared], as
|
|
722
|
+
## `{"object": ...}`, or `{"message": ...}` saying why it names none that fits.
|
|
723
|
+
##
|
|
724
|
+
## JSON cannot carry an object, so one the game holds is named by its path, read from the node the
|
|
725
|
+
## call or the write was made on, and handed over as that instance rather than a copy: a method
|
|
726
|
+
## given a piece of the game's state acts on the piece the game keeps, and a slot written with one
|
|
727
|
+
## holds the game's own.
|
|
728
|
+
func _object_named(node: Node, node_path: String, given: Variant, declared: String) -> Dictionary:
|
|
729
|
+
var found: Dictionary = Paths.object_at(_host.get_tree().root, node, node_path, given)
|
|
730
|
+
if found.has("message"):
|
|
731
|
+
return found
|
|
732
|
+
var instance: Object = found["object"]
|
|
733
|
+
if not declared.is_empty() and not Values.is_a(instance, declared):
|
|
734
|
+
return {"message": "%s is %s, not a %s" % [str(given), Values.class_of(instance), declared]}
|
|
735
|
+
return found
|
|
736
|
+
|
|
737
|
+
|
|
894
738
|
func get_metrics(params: Dictionary) -> Dictionary:
|
|
895
739
|
var metrics: Array = params.get("metrics", [])
|
|
896
740
|
var all: Dictionary = {
|
|
@@ -954,13 +798,13 @@ func _serialize_node_tree(
|
|
|
954
798
|
func _named_properties_of(node: Node, path: String, named: Array[String]) -> Dictionary:
|
|
955
799
|
var properties: Dictionary = {}
|
|
956
800
|
for wanted: String in named:
|
|
957
|
-
var reached: Dictionary = walk_to(node, path, wanted)
|
|
801
|
+
var reached: Dictionary = Paths.walk_to(node, path, wanted)
|
|
958
802
|
if reached.has("message"):
|
|
959
803
|
continue
|
|
960
804
|
var holder: Variant = reached["holder"]
|
|
961
805
|
var name: String = reached["name"]
|
|
962
|
-
if
|
|
963
|
-
properties[wanted] = _values.serialize(read_under(holder, name))
|
|
806
|
+
if Paths.can_read(holder, name):
|
|
807
|
+
properties[wanted] = _values.serialize(Paths.read_under(holder, name))
|
|
964
808
|
return properties
|
|
965
809
|
|
|
966
810
|
|
|
@@ -79,6 +79,87 @@ func parameter_type(object: Object, method: String, index: int) -> int:
|
|
|
79
79
|
return TYPE_NIL
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
## The class a method declares for argument [param index], or "" when it names none: an untyped
|
|
83
|
+
## parameter, a Variant, or a type that is not an object.
|
|
84
|
+
func parameter_class(object: Object, method: String, index: int) -> String:
|
|
85
|
+
for entry: Dictionary in object.get_method_list():
|
|
86
|
+
if entry.get("name", "") != method:
|
|
87
|
+
continue
|
|
88
|
+
var params: Array = entry.get("args", [])
|
|
89
|
+
if index < 0 or index >= params.size():
|
|
90
|
+
return ""
|
|
91
|
+
var parameter: Dictionary = params[index]
|
|
92
|
+
return str(parameter.get("class_name", ""))
|
|
93
|
+
return ""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
## What the slot [param named] on [param holder] declares it holds, as `{"type": int, "class":
|
|
97
|
+
## String}`: a property's declaration, or the element type of a typed list or the value type of a
|
|
98
|
+
## typed map. TYPE_NIL when nothing is declared.
|
|
99
|
+
##
|
|
100
|
+
## Asked rather than read off the value in the slot, because an object slot can be empty: a
|
|
101
|
+
## property typed `Gear` holding null reads as TYPE_NIL, and anything written into it passed the
|
|
102
|
+
## check and was then dropped by the engine, which answered with the slot still null.
|
|
103
|
+
static func slot_declared(holder: Variant, named: String) -> Dictionary:
|
|
104
|
+
if holder is Array:
|
|
105
|
+
var items: Array = holder
|
|
106
|
+
if items.is_typed():
|
|
107
|
+
return {
|
|
108
|
+
"type": items.get_typed_builtin(),
|
|
109
|
+
"class": _typed_class(items.get_typed_script(), items.get_typed_class_name()),
|
|
110
|
+
}
|
|
111
|
+
elif holder is Dictionary:
|
|
112
|
+
var map: Dictionary = holder
|
|
113
|
+
if map.is_typed_value():
|
|
114
|
+
return {
|
|
115
|
+
"type": map.get_typed_value_builtin(),
|
|
116
|
+
"class": _typed_class(map.get_typed_value_script(), map.get_typed_value_class_name()),
|
|
117
|
+
}
|
|
118
|
+
elif holder is Object:
|
|
119
|
+
var object: Object = holder
|
|
120
|
+
for entry: Dictionary in object.get_property_list():
|
|
121
|
+
if str(entry.get("name", "")) == named:
|
|
122
|
+
return {
|
|
123
|
+
"type": Read.as_int(entry.get("type", TYPE_NIL), TYPE_NIL),
|
|
124
|
+
"class": str(entry.get("class_name", "")),
|
|
125
|
+
}
|
|
126
|
+
return {"type": TYPE_NIL, "class": ""}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
## The class a typed container names: its script's global name where it has one, since the engine
|
|
130
|
+
## class of a script class is only what the script extends.
|
|
131
|
+
static func _typed_class(script: Variant, engine_class: StringName) -> String:
|
|
132
|
+
if script is Script:
|
|
133
|
+
var typed: Script = script
|
|
134
|
+
if not typed.get_global_name().is_empty():
|
|
135
|
+
return str(typed.get_global_name())
|
|
136
|
+
return str(engine_class)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
## Whether [param object] is a [param declared]: an engine class, or a script class by its global
|
|
140
|
+
## name anywhere along the script's inheritance.
|
|
141
|
+
static func is_a(object: Object, declared: String) -> bool:
|
|
142
|
+
if object.is_class(declared):
|
|
143
|
+
return true
|
|
144
|
+
var script: Variant = object.get_script()
|
|
145
|
+
while script is Script:
|
|
146
|
+
var current: Script = script
|
|
147
|
+
if current.get_global_name() == StringName(declared):
|
|
148
|
+
return true
|
|
149
|
+
script = current.get_base_script()
|
|
150
|
+
return false
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
## What [param object] is, by its script class where it has one.
|
|
154
|
+
static func class_of(object: Object) -> String:
|
|
155
|
+
var script: Variant = object.get_script()
|
|
156
|
+
if script is Script:
|
|
157
|
+
var current: Script = script
|
|
158
|
+
if not current.get_global_name().is_empty():
|
|
159
|
+
return str(current.get_global_name())
|
|
160
|
+
return object.get_class()
|
|
161
|
+
|
|
162
|
+
|
|
82
163
|
## A value from the wire fitted to the type a property or parameter declares.
|
|
83
164
|
func fitted(value: Variant, type: int) -> Variant:
|
|
84
165
|
return _shared.fitted(value, type)
|
|
@@ -3,6 +3,7 @@ extends RefCounted
|
|
|
3
3
|
## The commands that take time: they let the game run and answer when what was waited for has
|
|
4
4
|
## happened, or when the time ran out, so the caller never sleeps for a guessed length.
|
|
5
5
|
|
|
6
|
+
const Paths = preload("runtime_paths.gd")
|
|
6
7
|
const Queries = preload("runtime_queries.gd")
|
|
7
8
|
const Read = preload("reading.gd")
|
|
8
9
|
const Values = preload("runtime_values.gd")
|
|
@@ -207,15 +208,15 @@ static func _not_comparable(current: Variant, wanted: Variant, node_path: String
|
|
|
207
208
|
## its nodes hold: a run object built afresh each day would leave a wait watching the day before. A
|
|
208
209
|
## step that has gone in the meantime ends the wait with what went.
|
|
209
210
|
static func _watched(node: Node, node_path: String, property: String) -> Dictionary:
|
|
210
|
-
var reached: Dictionary =
|
|
211
|
+
var reached: Dictionary = Paths.walk_to(node, node_path, property)
|
|
211
212
|
if reached.has("message"):
|
|
212
213
|
return reached
|
|
213
214
|
var holder: Variant = reached["holder"]
|
|
214
215
|
var named: String = reached["name"]
|
|
215
|
-
var missing: String =
|
|
216
|
+
var missing: String = Paths.nothing_under(holder, named, str(reached["called"]))
|
|
216
217
|
if not missing.is_empty():
|
|
217
218
|
return {"type": "error", "message": missing}
|
|
218
|
-
return {"value":
|
|
219
|
+
return {"value": Paths.read_under(holder, named)}
|
|
219
220
|
|
|
220
221
|
|
|
221
222
|
## Waits until something under [param node_path] has [param said] written on it.
|
package/build/index.js
CHANGED
|
@@ -30280,7 +30280,7 @@ var TOOL_SPECS = [
|
|
|
30280
30280
|
},
|
|
30281
30281
|
{
|
|
30282
30282
|
name: "editor_status",
|
|
30283
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
30283
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
30284
30284
|
parameters: {},
|
|
30285
30285
|
requires: []
|
|
30286
30286
|
},
|
|
@@ -30440,7 +30440,7 @@ var TOOL_SPECS = [
|
|
|
30440
30440
|
value: {
|
|
30441
30441
|
ops: ["set"],
|
|
30442
30442
|
blank: true,
|
|
30443
|
-
description: `set: the value, fitted to the property's type. "" writes an empty string.`
|
|
30443
|
+
description: `set: the value, fitted to the property's type. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance.`
|
|
30444
30444
|
},
|
|
30445
30445
|
method: {
|
|
30446
30446
|
type: "string",
|
|
@@ -30450,7 +30450,7 @@ var TOOL_SPECS = [
|
|
|
30450
30450
|
args: {
|
|
30451
30451
|
type: "array",
|
|
30452
30452
|
ops: ["call"],
|
|
30453
|
-
description:
|
|
30453
|
+
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused.`
|
|
30454
30454
|
}
|
|
30455
30455
|
},
|
|
30456
30456
|
requires: ["nodePath"],
|
|
@@ -30797,6 +30797,9 @@ function stillRunning(run) {
|
|
|
30797
30797
|
if (run?.exitCode !== null || run.endedUnwatched === true) {
|
|
30798
30798
|
return false;
|
|
30799
30799
|
}
|
|
30800
|
+
if (typeof run.endedHere === "string") {
|
|
30801
|
+
return false;
|
|
30802
|
+
}
|
|
30800
30803
|
if (!run.throughEditor && run.process === null && run.pid !== null) {
|
|
30801
30804
|
return alive(run.pid);
|
|
30802
30805
|
}
|
|
@@ -30810,6 +30813,9 @@ function runIsUp(run, editorSays, now = Date.now()) {
|
|
|
30810
30813
|
if (run?.throughEditor !== true) {
|
|
30811
30814
|
return stillRunning(run);
|
|
30812
30815
|
}
|
|
30816
|
+
if (typeof run.endedHere === "string") {
|
|
30817
|
+
return false;
|
|
30818
|
+
}
|
|
30813
30819
|
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
30814
30820
|
return false;
|
|
30815
30821
|
}
|
|
@@ -30818,6 +30824,22 @@ function runIsUp(run, editorSays, now = Date.now()) {
|
|
|
30818
30824
|
}
|
|
30819
30825
|
return editorSays ?? stillRunning(run);
|
|
30820
30826
|
}
|
|
30827
|
+
function noCodeWillCome(run) {
|
|
30828
|
+
return run.exitCode === null && run.process === null;
|
|
30829
|
+
}
|
|
30830
|
+
function endedWithoutACode(run) {
|
|
30831
|
+
if (!run.throughEditor) {
|
|
30832
|
+
return run.endedHere ? "This run was started by another server, so nothing here held it and its exit code was never collected. Everything it printed is below, read back from its transcript." : "This run was started by another server, the one this server replaced or one running beside it, and it ended with nothing here waiting on it, so its exit code was never collected. Everything it printed is below, read back from its transcript.";
|
|
30833
|
+
}
|
|
30834
|
+
const kept = "A game the editor plays is the editor's own child, so its exit code stays with the editor and none was collected here.";
|
|
30835
|
+
if (run.endedHere) {
|
|
30836
|
+
return `${kept} What it printed is below.`;
|
|
30837
|
+
}
|
|
30838
|
+
if (!run.announced && run.couldAnnounce) {
|
|
30839
|
+
return `The editor stopped playing this run before its game announced a runtime, so the game ended while it was starting or was closed in the editor. ${kept} What it printed is below, and editor_output with op "editor" has what the editor said about it.`;
|
|
30840
|
+
}
|
|
30841
|
+
return `The editor stopped playing this run. ${kept} What it printed is below.`;
|
|
30842
|
+
}
|
|
30821
30843
|
function endedPreviousRun(ended) {
|
|
30822
30844
|
if (ended === null) {
|
|
30823
30845
|
return;
|
|
@@ -32604,6 +32626,7 @@ class GodotServer {
|
|
|
32604
32626
|
pid: endpoint.pid,
|
|
32605
32627
|
port: endpoint.port,
|
|
32606
32628
|
project: endpoint.project,
|
|
32629
|
+
...this.ownProject === null ? {} : { ofThisProject: isSameDirectory(endpoint.project.path, this.ownProject) },
|
|
32607
32630
|
editorPid: endpoint.editorPid,
|
|
32608
32631
|
reachable: reply.ok,
|
|
32609
32632
|
problem: reply.ok ? null : reply.message
|
|
@@ -32635,7 +32658,7 @@ class GodotServer {
|
|
|
32635
32658
|
processActive: active,
|
|
32636
32659
|
playingInEditor: playing,
|
|
32637
32660
|
...hold,
|
|
32638
|
-
runtimeConnected: games.some((game) => game.reachable),
|
|
32661
|
+
runtimeConnected: games.some((game) => game.reachable && game.ofThisProject !== false),
|
|
32639
32662
|
runtimes: games,
|
|
32640
32663
|
unreadable: announced.unspoken.map((game) => ({
|
|
32641
32664
|
pid: game.pid,
|
|
@@ -32859,7 +32882,7 @@ class GodotServer {
|
|
|
32859
32882
|
if (before !== null && await this.runStillGoing(before)) {
|
|
32860
32883
|
this.logDebug("Ending the running game before starting another");
|
|
32861
32884
|
ended = { pid: before.pid ?? this.announcedPidOf(before) ?? null };
|
|
32862
|
-
await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
|
|
32885
|
+
await this.endActiveGame("editor_run start, which ends the run that was going before it starts another", true);
|
|
32863
32886
|
}
|
|
32864
32887
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
32865
32888
|
const wereHereFirst = this.everyRuntimeOfOurs();
|
|
@@ -33100,12 +33123,11 @@ class GodotServer {
|
|
|
33100
33123
|
game.brokeOn = halt.text;
|
|
33101
33124
|
game.log.record("error", halt.text);
|
|
33102
33125
|
}
|
|
33103
|
-
async endActiveGame(reason) {
|
|
33126
|
+
async endActiveGame(reason, going) {
|
|
33104
33127
|
const running = this.activeProcess;
|
|
33105
|
-
this.activeProcess = null;
|
|
33106
33128
|
const recorded = running?.process === null ? readRunRecord() : null;
|
|
33107
33129
|
clearRunRecord((record) => this.couldBeOurs(record.projectPath));
|
|
33108
|
-
if (!running) {
|
|
33130
|
+
if (!running || !going) {
|
|
33109
33131
|
return;
|
|
33110
33132
|
}
|
|
33111
33133
|
running.endedHere = reason;
|
|
@@ -33269,7 +33291,8 @@ class GodotServer {
|
|
|
33269
33291
|
return;
|
|
33270
33292
|
}
|
|
33271
33293
|
const going = this.activeProcess;
|
|
33272
|
-
|
|
33294
|
+
const givesWay = going !== null && !stillRunning(going) && typeof going.endedHere === "string";
|
|
33295
|
+
if (going !== null && !givesWay) {
|
|
33273
33296
|
if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
|
|
33274
33297
|
try {
|
|
33275
33298
|
await this.dap().connect();
|
|
@@ -33655,7 +33678,7 @@ class GodotServer {
|
|
|
33655
33678
|
const going = await this.runStillGoing(run);
|
|
33656
33679
|
if (run.endedUnwatched !== true && !going) {
|
|
33657
33680
|
run.log.finish();
|
|
33658
|
-
run.endedUnwatched = run
|
|
33681
|
+
run.endedUnwatched = noCodeWillCome(run);
|
|
33659
33682
|
}
|
|
33660
33683
|
const severity = readString(args, "severity");
|
|
33661
33684
|
const selected = run.log.select({
|
|
@@ -33676,8 +33699,18 @@ class GodotServer {
|
|
|
33676
33699
|
if (waitedMs !== undefined) {
|
|
33677
33700
|
notes.push(`The wait of ${waitedMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
|
|
33678
33701
|
}
|
|
33702
|
+
if (typeof run.endedHere === "string") {
|
|
33703
|
+
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
33704
|
+
} else if (!stillRunning(run) && !run.throughEditor && run.exitCode !== null) {
|
|
33705
|
+
notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : "Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
|
|
33706
|
+
}
|
|
33679
33707
|
if (run.endedUnwatched === true) {
|
|
33680
|
-
notes.push(
|
|
33708
|
+
notes.push(endedWithoutACode({
|
|
33709
|
+
throughEditor: run.throughEditor,
|
|
33710
|
+
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
33711
|
+
couldAnnounce: run.projectPath !== null && existsSync10(join16(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
33712
|
+
endedHere: typeof run.endedHere === "string"
|
|
33713
|
+
}));
|
|
33681
33714
|
}
|
|
33682
33715
|
if (run.pickedUpPlaying === true) {
|
|
33683
33716
|
notes.push(run.transcript === null ? "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." : "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.");
|
|
@@ -33686,11 +33719,6 @@ class GodotServer {
|
|
|
33686
33719
|
const back = this.dapClient?.isConnected() === true;
|
|
33687
33720
|
notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
|
|
33688
33721
|
}
|
|
33689
|
-
if (typeof run.endedHere === "string") {
|
|
33690
|
-
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
33691
|
-
} else if (!stillRunning(run) && !run.throughEditor && run.exitCode !== null) {
|
|
33692
|
-
notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : "Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
|
|
33693
|
-
}
|
|
33694
33722
|
if (run.transcript !== null && selected.omitted > 0) {
|
|
33695
33723
|
notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
|
|
33696
33724
|
}
|
|
@@ -33729,7 +33757,7 @@ class GodotServer {
|
|
|
33729
33757
|
const endedPid = stopped.pid ?? announcedPid;
|
|
33730
33758
|
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
33731
33759
|
this.logDebug("Stopping the running game");
|
|
33732
|
-
await this.endActiveGame("editor_run stop");
|
|
33760
|
+
await this.endActiveGame("editor_run stop", wasRunning);
|
|
33733
33761
|
const ended = children === null ? null : endChildrenAmong(children);
|
|
33734
33762
|
if (wasRunning) {
|
|
33735
33763
|
for (const pid of new Set([endedPid, announcedPid])) {
|
|
@@ -33748,12 +33776,12 @@ class GodotServer {
|
|
|
33748
33776
|
childrenLeft: ended.left.length === 0 ? undefined : ended.left,
|
|
33749
33777
|
childrenUnknown: ended.unknown === false ? undefined : true
|
|
33750
33778
|
},
|
|
33751
|
-
exitedBeforeStop:
|
|
33779
|
+
exitedBeforeStop: !wasRunning,
|
|
33752
33780
|
exitCode: stopped.exitCode,
|
|
33753
33781
|
errors: stopped.log.count("error"),
|
|
33754
33782
|
warnings: stopped.log.count("warning"),
|
|
33755
33783
|
clean: stopped.log.count("error") === 0,
|
|
33756
|
-
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : `${stopped.throughEditor ? "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."} ${aboutTheChildren(ended, stopped.throughEditor)}`,
|
|
33784
|
+
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : `${stopped.throughEditor ? endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."} ${aboutTheChildren(ended, stopped.throughEditor)}`,
|
|
33757
33785
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
33758
33786
|
});
|
|
33759
33787
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
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",
|