gdharness 1.2.21 → 1.2.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -8002,6 +8002,11 @@ class GameLog {
8002
8002
  transcript: null
8003
8003
  };
8004
8004
  seen = { error: 0, warning: 0, info: 0 };
8005
+ crashed = {
8006
+ stdout: null,
8007
+ stderr: null,
8008
+ transcript: null
8009
+ };
8005
8010
  byScript = new Set;
8006
8011
  append(source, chunk) {
8007
8012
  const decoded = typeof chunk === "string" ? chunk : this.decoders[source].write(chunk);
@@ -8026,6 +8031,21 @@ class GameLog {
8026
8031
  if (line.trim() === "") {
8027
8032
  return;
8028
8033
  }
8034
+ const report = this.crashed[source];
8035
+ if (report !== null) {
8036
+ const said = line.trim();
8037
+ if (!CRASH_FILLER.test(said)) {
8038
+ report.push(said);
8039
+ }
8040
+ return;
8041
+ }
8042
+ if (CRASH.test(line)) {
8043
+ const detail = [];
8044
+ this.entries.push({ index: this.entries.length, severity: "error", source, text: line.trim(), detail });
8045
+ this.crashed[source] = detail;
8046
+ this.lastHeadline[source] = null;
8047
+ return;
8048
+ }
8029
8049
  const owner = this.lastHeadline[source];
8030
8050
  if (owner !== null && /^\s/.test(line)) {
8031
8051
  owner.detail.push(line.trim());
@@ -8096,9 +8116,11 @@ function answersTo(entry, severity, contains) {
8096
8116
  const needle = contains.toLowerCase();
8097
8117
  return [entry.text, ...entry.detail].some((line) => line.toLowerCase().includes(needle));
8098
8118
  }
8099
- var HEADLINE, COLOUR_CODE, STREAMS, ADMITTED, FRAME, LONGEST_CYCLE = 64;
8119
+ var HEADLINE, CRASH, CRASH_FILLER, COLOUR_CODE, STREAMS, ADMITTED, FRAME, LONGEST_CYCLE = 64;
8100
8120
  var init_game_log = __esm(() => {
8101
8121
  HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
8122
+ CRASH = /^(?:CrashHandlerException|handle_crash): Program crashed with signal \d+/;
8123
+ CRASH_FILLER = /^(?:=+|-- END OF .* BACKTRACE --|\[\d+\] .* - no debug info in .*)$/;
8102
8124
  COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
8103
8125
  STREAMS = ["stdout", "stderr", "transcript"];
8104
8126
  ADMITTED = {
@@ -11082,7 +11104,7 @@ var init_tool_definitions = __esm(() => {
11082
11104
  {
11083
11105
  name: "project_settings",
11084
11106
  title: "Project settings",
11085
- description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot.",
11107
+ description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
11086
11108
  parameters: {
11087
11109
  projectPath: PROJECT_PATH,
11088
11110
  setting: {
@@ -11400,7 +11422,7 @@ var init_tool_definitions = __esm(() => {
11400
11422
  name: "scene_tree",
11401
11423
  title: "Scene tree",
11402
11424
  effect: "read",
11403
- description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. Needs the editor connected.",
11425
+ description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. Needs the editor connected. For the nodes of a game that is running, runtime_inspect tree, which needs no editor.",
11404
11426
  parameters: {
11405
11427
  projectPath: PROJECT_PATH,
11406
11428
  scenePath: SCENE_PATH,
@@ -11924,7 +11946,7 @@ var init_tool_definitions = __esm(() => {
11924
11946
  requires: []
11925
11947
  }
11926
11948
  },
11927
- answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked.",
11949
+ answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked.",
11928
11950
  defaultOperation: "start"
11929
11951
  },
11930
11952
  {
@@ -43558,6 +43580,14 @@ function runtimeVerdict(endpoint, after) {
43558
43580
  }
43559
43581
  const state = after.playingSeen === false ? "the editor has not yet reported the game as playing, which a restarted editor does not until its scan is over" : "the game is still running";
43560
43582
  const sized = after.sizedToMs === undefined ? "" : halfAsLongAgain(after.sizedToMs) > after.budgetMs ? `, the longest a start waits unasked though the slowest of its last boots took ${after.sizedToMs}ms to announce,` : `, half as long again as the ${after.sizedToMs}ms the slowest of its last boots took,`;
43583
+ if (!after.running && after.runtimeCameUp === true) {
43584
+ return {
43585
+ listening: false,
43586
+ mayYetAnnounce: false,
43587
+ note: "the game's runtime came up and the game quit before this start could reach it, so there is nothing to talk to: editor_output has what it printed, and a scene that is to be inspected has to stay running past its first frame",
43588
+ heldAt: null
43589
+ };
43590
+ }
43561
43591
  return {
43562
43592
  listening: false,
43563
43593
  mayYetAnnounce: after.running,
@@ -44863,7 +44893,7 @@ class GodotServer {
44863
44893
  const { op: _op, ...arguments_ } = args;
44864
44894
  const bridge = async (command, extra = {}) => {
44865
44895
  const contained = this.containProjectFiles({ ...arguments_, ...extra });
44866
- return contained.ok ? await this.handleViaBridge(command, contained.value) : contained.response;
44896
+ return contained.ok ? await this.handleViaBridge(command, contained.value, tool) : contained.response;
44867
44897
  };
44868
44898
  switch (tool) {
44869
44899
  case "project_info":
@@ -47119,15 +47149,20 @@ class GodotServer {
47119
47149
  note: `${fresh.length} games of this project announced at once and none could be told from the process tree as this run's own game rather than one it started, so none is taken as its runtime. editor_status lists them under runtimes.`
47120
47150
  };
47121
47151
  }
47152
+ const running = endpoint !== null || going !== null && await this.runStillGoing(going);
47153
+ if (going !== null && !running) {
47154
+ this.drainTranscript(going);
47155
+ }
47122
47156
  return runtimeVerdict(endpoint, {
47123
47157
  addon: true,
47124
47158
  budgetMs,
47125
47159
  ...budget.sizedToMs === undefined ? {} : { sizedToMs: budget.sizedToMs },
47126
47160
  heldAt: this.dapClient?.whereItStopped() ?? null,
47127
- running: endpoint !== null || going !== null && await this.runStillGoing(going),
47161
+ running,
47128
47162
  withArgs,
47129
47163
  servesAProject: this.ownProject !== null,
47130
- ...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
47164
+ ...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {},
47165
+ runtimeCameUp: going !== null && !running && going.log.all.some((entry) => entry.text.startsWith(RUNTIME_LISTENING_LINE))
47131
47166
  });
47132
47167
  }
47133
47168
  announceBudget(args, projectPath, scene) {
@@ -47983,6 +48018,8 @@ class GodotServer {
47983
48018
  await boot.settled;
47984
48019
  const errors = boot.log.count("error");
47985
48020
  const warnings = boot.log.count("warning");
48021
+ const floor = engineArgs.includes("--verbose") ? "info" : "warning";
48022
+ const printed = boot.log.count("info");
47986
48023
  return this.jsonTextResponse({
47987
48024
  booted: !hung && boot.exitCode === 0 && errors === 0,
47988
48025
  hung,
@@ -47996,7 +48033,8 @@ class GodotServer {
47996
48033
  argsNote: engineFlagsNote(userArgs),
47997
48034
  ...scanWaitAnswer(scanned),
47998
48035
  savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
47999
- ...foldedEntriesAnswer(boot.log, engineArgs.includes("--verbose") ? "info" : "warning")
48036
+ printedLinesNotShown: floor === "warning" && printed > 0 ? printed : undefined,
48037
+ ...foldedEntriesAnswer(boot.log, floor)
48000
48038
  });
48001
48039
  }
48002
48040
  async handleRunProfile(args) {
@@ -49063,9 +49101,11 @@ class GodotServer {
49063
49101
  const holds = held["classes"].map(String);
49064
49102
  return { unseen: unseenByEditor(projectPath, holds), stillHeld: heldButGone(projectPath, holds) };
49065
49103
  }
49066
- async handleViaBridge(toolName, args) {
49104
+ async handleViaBridge(toolName, args, askedAs) {
49067
49105
  if (!this.godotBridge.isConnected()) {
49106
+ const instead = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
49068
49107
  return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
49108
+ ...instead === undefined ? [] : [instead],
49069
49109
  "Open the project in the editor with editor_launch, or by hand",
49070
49110
  "Enable the addon under Project > Project Settings > Plugins",
49071
49111
  "editor_status says whether the bridge is up and what it is listening on"
@@ -49378,7 +49418,7 @@ async function runGodotServer() {
49378
49418
  const server = new GodotServer;
49379
49419
  await server.run();
49380
49420
  }
49381
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, 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, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
49421
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, EDITOR_FREE_INSTEAD, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
49382
49422
  var init_server = __esm(() => {
49383
49423
  init_dist();
49384
49424
  init_stdio();
@@ -49442,6 +49482,9 @@ var init_server = __esm(() => {
49442
49482
  object: "an object"
49443
49483
  };
49444
49484
  HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
49485
+ EDITOR_FREE_INSTEAD = {
49486
+ scene_tree: "runtime_inspect tree reads the nodes of a game that is running, and needs no editor; scene_tree reads a scene file through the editor"
49487
+ };
49445
49488
  OPS_ELSEWHERE = {
49446
49489
  runtime_invoke: { get: "runtime_inspect property", property: "runtime_inspect property" }
49447
49490
  };
@@ -284,6 +284,15 @@ static func _code_of(line: String) -> String:
284
284
  return bare if mark == -1 else bare.substr(0, mark)
285
285
 
286
286
 
287
+ # [param content] with each line's comment taken off and its strings kept, which is where the paths
288
+ # a script loads are written.
289
+ static func _code_lines(content: String) -> String:
290
+ var kept: Array[String] = []
291
+ for line: String in content.split("\n"):
292
+ kept.append(Patterns.without_comment(line))
293
+ return "\n".join(kept)
294
+
295
+
287
296
  # The comment on [param line], strings emptied first so a # inside one does not start it.
288
297
  static func _comment_of(line: String) -> String:
289
298
  var bare: String = Patterns.compiled(STRING_LITERAL).sub(line, '""', true)
@@ -361,6 +370,11 @@ func _analyze_resource(path: String, current_depth: int, walk: DependencyWalk) -
361
370
  if file:
362
371
  var content: String = file.get_as_text()
363
372
  file.close()
373
+ # A script's comments name paths as examples, and read as references they were answered as
374
+ # dependencies, one of a file that does not exist: the shipped serialisation module's comment
375
+ # about `["res://addons/x/plugin.cfg"]` was a dependency of every project with the addon.
376
+ if path.get_extension() == "gd":
377
+ content = _code_lines(content)
364
378
 
365
379
  for pattern: String in REFERENCE_PATTERNS:
366
380
  var regex: RegEx = Patterns.compiled(pattern)
@@ -59,7 +59,7 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
59
59
  if _triple_quotes(raw) % 2 == 1:
60
60
  in_multiline_string = false
61
61
  continue
62
- var stripped: String = _without_comment(raw)
62
+ var stripped: String = Patterns.without_comment(raw)
63
63
 
64
64
  if stripped.is_empty():
65
65
  continue
@@ -101,7 +101,7 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
101
101
  if _can_wrap(header):
102
102
  while _bracket_depth(stripped) > 0 and i + 1 < lines.size():
103
103
  i += 1
104
- stripped = (stripped + " " + _without_comment(lines[i])).strip_edges()
104
+ stripped = (stripped + " " + Patterns.without_comment(lines[i])).strip_edges()
105
105
  header = Patterns.without_annotations(stripped)
106
106
  var declaration: RegExMatch = Patterns.declared_class(header)
107
107
  if declaration != null:
@@ -508,28 +508,6 @@ func _up_to(text: String, needle: String) -> String:
508
508
  return text
509
509
 
510
510
 
511
- # The line with any trailing comment taken off, quote-aware so a `#` inside a string stays.
512
- #
513
- # The only one of these scanners that does not count brackets, on purpose: a `#` starts a comment
514
- # wherever it is, and there is no depth at which it does not. Giving this one the depth its
515
- # neighbours have puts the comment into the value of `var table := { # keyed by name`, because the
516
- # brace opens before the `#` and closes on a later line. Held by a case, since the argument for the
517
- # difference is not visible from the code and reads like an oversight beside its neighbours.
518
- func _without_comment(line: String) -> String:
519
- var quote: String = ""
520
- for i: int in range(line.length()):
521
- var ch: String = line[i]
522
- if not quote.is_empty():
523
- if ch == quote and line[i - 1] != "\\":
524
- quote = ""
525
- continue
526
- if ch == "#":
527
- return line.substr(0, i).strip_edges()
528
- if ch == '"' or ch == "'":
529
- quote = ch
530
- return line.strip_edges()
531
-
532
-
533
511
  # Where the bracket opened at [from] closes, or -1 if it does not.
534
512
  #
535
513
  # `rfind(")")` took the last one on the line, which is the right answer until something after the
@@ -84,3 +84,25 @@ static func without_annotations(line: String) -> String:
84
84
  rest = rest.substr(found.get_end()).strip_edges()
85
85
  found = annotation.search(rest)
86
86
  return rest
87
+
88
+
89
+ # The line with any trailing comment taken off, quote-aware so a `#` inside a string stays.
90
+ #
91
+ # The only one of the scanners here that does not count brackets, on purpose: a `#` starts a comment
92
+ # wherever it is, and there is no depth at which it does not. Giving this one the depth its
93
+ # neighbours have puts the comment into the value of `var table := { # keyed by name`, because the
94
+ # brace opens before the `#` and closes on a later line. Held by a case, since the argument for the
95
+ # difference is not visible from the code and reads like an oversight beside its neighbours.
96
+ static func without_comment(line: String) -> String:
97
+ var quote: String = ""
98
+ for i: int in range(line.length()):
99
+ var ch: String = line[i]
100
+ if not quote.is_empty():
101
+ if ch == quote and line[i - 1] != "\\":
102
+ quote = ""
103
+ continue
104
+ if ch == "#":
105
+ return line.substr(0, i).strip_edges()
106
+ if ch == '"' or ch == "'":
107
+ quote = ch
108
+ return line.strip_edges()
@@ -82,6 +82,11 @@ func _settings_under(prefix: String) -> Dictionary:
82
82
  "type": type_string(kind),
83
83
  "value": _values.serialize_value(value),
84
84
  }
85
+ # As a single get says it, so a family read can be told apart into what the project sets
86
+ # and the engine's defaults: "input/" is ninety actions the engine ships and the project's few.
87
+ var default_value: Variant = ProjectSettings.property_get_revert(name)
88
+ if default_value != null:
89
+ described["at_default"] = value == default_value
85
90
  found.append(described)
86
91
 
87
92
  found.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["setting"] < b["setting"])
package/build/index.js CHANGED
@@ -28147,6 +28147,8 @@ import { dirname as dirname7, join as join10 } from "node:path";
28147
28147
  // src/game-log.ts
28148
28148
  import { StringDecoder } from "node:string_decoder";
28149
28149
  var HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
28150
+ var CRASH = /^(?:CrashHandlerException|handle_crash): Program crashed with signal \d+/;
28151
+ var CRASH_FILLER = /^(?:=+|-- END OF .* BACKTRACE --|\[\d+\] .* - no debug info in .*)$/;
28150
28152
  var COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
28151
28153
  function announced(line) {
28152
28154
  const match = HEADLINE.exec(line);
@@ -28261,6 +28263,11 @@ class GameLog {
28261
28263
  transcript: null
28262
28264
  };
28263
28265
  seen = { error: 0, warning: 0, info: 0 };
28266
+ crashed = {
28267
+ stdout: null,
28268
+ stderr: null,
28269
+ transcript: null
28270
+ };
28264
28271
  byScript = new Set;
28265
28272
  append(source, chunk) {
28266
28273
  const decoded = typeof chunk === "string" ? chunk : this.decoders[source].write(chunk);
@@ -28285,6 +28292,21 @@ class GameLog {
28285
28292
  if (line.trim() === "") {
28286
28293
  return;
28287
28294
  }
28295
+ const report = this.crashed[source];
28296
+ if (report !== null) {
28297
+ const said = line.trim();
28298
+ if (!CRASH_FILLER.test(said)) {
28299
+ report.push(said);
28300
+ }
28301
+ return;
28302
+ }
28303
+ if (CRASH.test(line)) {
28304
+ const detail = [];
28305
+ this.entries.push({ index: this.entries.length, severity: "error", source, text: line.trim(), detail });
28306
+ this.crashed[source] = detail;
28307
+ this.lastHeadline[source] = null;
28308
+ return;
28309
+ }
28288
28310
  const owner = this.lastHeadline[source];
28289
28311
  if (owner !== null && /^\s/.test(line)) {
28290
28312
  owner.detail.push(line.trim());
@@ -34114,7 +34136,7 @@ var TOOL_SPECS = [
34114
34136
  {
34115
34137
  name: "project_settings",
34116
34138
  title: "Project settings",
34117
- description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot.",
34139
+ description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
34118
34140
  parameters: {
34119
34141
  projectPath: PROJECT_PATH2,
34120
34142
  setting: {
@@ -34432,7 +34454,7 @@ var TOOL_SPECS = [
34432
34454
  name: "scene_tree",
34433
34455
  title: "Scene tree",
34434
34456
  effect: "read",
34435
- description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. Needs the editor connected.",
34457
+ description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. Needs the editor connected. For the nodes of a game that is running, runtime_inspect tree, which needs no editor.",
34436
34458
  parameters: {
34437
34459
  projectPath: PROJECT_PATH2,
34438
34460
  scenePath: SCENE_PATH,
@@ -34956,7 +34978,7 @@ var TOOL_SPECS = [
34956
34978
  requires: []
34957
34979
  }
34958
34980
  },
34959
- answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked.",
34981
+ answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked.",
34960
34982
  defaultOperation: "start"
34961
34983
  },
34962
34984
  {
@@ -36211,6 +36233,7 @@ function endedToStartThis(ended) {
36211
36233
  var LONGEST_BOOT_NOTED_MS = 5 * 60000;
36212
36234
  var PROFILE_SEND_MS = 5000;
36213
36235
  var MOST_GROUPS = 20;
36236
+ var RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ";
36214
36237
  function runtimeVerdict(endpoint, after) {
36215
36238
  if (endpoint !== null) {
36216
36239
  return { listening: true, pid: endpoint.pid, port: endpoint.port };
@@ -36232,6 +36255,14 @@ function runtimeVerdict(endpoint, after) {
36232
36255
  }
36233
36256
  const state = after.playingSeen === false ? "the editor has not yet reported the game as playing, which a restarted editor does not until its scan is over" : "the game is still running";
36234
36257
  const sized = after.sizedToMs === undefined ? "" : halfAsLongAgain(after.sizedToMs) > after.budgetMs ? `, the longest a start waits unasked though the slowest of its last boots took ${after.sizedToMs}ms to announce,` : `, half as long again as the ${after.sizedToMs}ms the slowest of its last boots took,`;
36258
+ if (!after.running && after.runtimeCameUp === true) {
36259
+ return {
36260
+ listening: false,
36261
+ mayYetAnnounce: false,
36262
+ note: "the game's runtime came up and the game quit before this start could reach it, so there is nothing to talk to: editor_output has what it printed, and a scene that is to be inspected has to stay running past its first frame",
36263
+ heldAt: null
36264
+ };
36265
+ }
36235
36266
  return {
36236
36267
  listening: false,
36237
36268
  mayYetAnnounce: after.running,
@@ -36417,6 +36448,9 @@ function engineFlagsNote(args) {
36417
36448
  const named = among.join(", ");
36418
36449
  return `${named} ${among.length === 1 ? "is a flag" : "are flags"} the engine reads, and args goes to the game after --, where the engine does not read ${among.length === 1 ? "it" : "them"}. engineArgs hands the engine its own flags; ${among.length === 1 ? "leave it" : "leave them"} in args only for a game that reads ${among.length === 1 ? "that flag" : "those flags"} itself.`;
36419
36450
  }
36451
+ var EDITOR_FREE_INSTEAD = {
36452
+ scene_tree: "runtime_inspect tree reads the nodes of a game that is running, and needs no editor; scene_tree reads a scene file through the editor"
36453
+ };
36420
36454
  var OPS_ELSEWHERE = {
36421
36455
  runtime_invoke: { get: "runtime_inspect property", property: "runtime_inspect property" }
36422
36456
  };
@@ -37607,7 +37641,7 @@ class GodotServer {
37607
37641
  const { op: _op, ...arguments_ } = args;
37608
37642
  const bridge = async (command, extra = {}) => {
37609
37643
  const contained = this.containProjectFiles({ ...arguments_, ...extra });
37610
- return contained.ok ? await this.handleViaBridge(command, contained.value) : contained.response;
37644
+ return contained.ok ? await this.handleViaBridge(command, contained.value, tool) : contained.response;
37611
37645
  };
37612
37646
  switch (tool) {
37613
37647
  case "project_info":
@@ -39863,15 +39897,20 @@ class GodotServer {
39863
39897
  note: `${fresh.length} games of this project announced at once and none could be told from the process tree as this run's own game rather than one it started, so none is taken as its runtime. editor_status lists them under runtimes.`
39864
39898
  };
39865
39899
  }
39900
+ const running = endpoint !== null || going !== null && await this.runStillGoing(going);
39901
+ if (going !== null && !running) {
39902
+ this.drainTranscript(going);
39903
+ }
39866
39904
  return runtimeVerdict(endpoint, {
39867
39905
  addon: true,
39868
39906
  budgetMs,
39869
39907
  ...budget.sizedToMs === undefined ? {} : { sizedToMs: budget.sizedToMs },
39870
39908
  heldAt: this.dapClient?.whereItStopped() ?? null,
39871
- running: endpoint !== null || going !== null && await this.runStillGoing(going),
39909
+ running,
39872
39910
  withArgs,
39873
39911
  servesAProject: this.ownProject !== null,
39874
- ...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
39912
+ ...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {},
39913
+ runtimeCameUp: going !== null && !running && going.log.all.some((entry) => entry.text.startsWith(RUNTIME_LISTENING_LINE))
39875
39914
  });
39876
39915
  }
39877
39916
  announceBudget(args, projectPath, scene) {
@@ -40727,6 +40766,8 @@ class GodotServer {
40727
40766
  await boot.settled;
40728
40767
  const errors = boot.log.count("error");
40729
40768
  const warnings = boot.log.count("warning");
40769
+ const floor = engineArgs.includes("--verbose") ? "info" : "warning";
40770
+ const printed = boot.log.count("info");
40730
40771
  return this.jsonTextResponse({
40731
40772
  booted: !hung && boot.exitCode === 0 && errors === 0,
40732
40773
  hung,
@@ -40740,7 +40781,8 @@ class GodotServer {
40740
40781
  argsNote: engineFlagsNote(userArgs),
40741
40782
  ...scanWaitAnswer(scanned),
40742
40783
  savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
40743
- ...foldedEntriesAnswer(boot.log, engineArgs.includes("--verbose") ? "info" : "warning")
40784
+ printedLinesNotShown: floor === "warning" && printed > 0 ? printed : undefined,
40785
+ ...foldedEntriesAnswer(boot.log, floor)
40744
40786
  });
40745
40787
  }
40746
40788
  async handleRunProfile(args) {
@@ -41807,9 +41849,11 @@ class GodotServer {
41807
41849
  const holds = held["classes"].map(String);
41808
41850
  return { unseen: unseenByEditor(projectPath, holds), stillHeld: heldButGone(projectPath, holds) };
41809
41851
  }
41810
- async handleViaBridge(toolName, args) {
41852
+ async handleViaBridge(toolName, args, askedAs) {
41811
41853
  if (!this.godotBridge.isConnected()) {
41854
+ const instead = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
41812
41855
  return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
41856
+ ...instead === undefined ? [] : [instead],
41813
41857
  "Open the project in the editor with editor_launch, or by hand",
41814
41858
  "Enable the addon under Project > Project Settings > Plugins",
41815
41859
  "editor_status says whether the bridge is up and what it is listening on"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.21",
3
+ "version": "1.2.23",
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",