gdharness 1.3.28 → 1.3.29

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
@@ -12072,6 +12072,37 @@ function editorArguments(projectPath, ports, logFile, hidden, silent) {
12072
12072
  logFile
12073
12073
  ];
12074
12074
  }
12075
+ function startedAs(commandLine) {
12076
+ const words = commandLineWords(commandLine);
12077
+ const hidden = words.includes("--headless");
12078
+ const driver = words.indexOf("--audio-driver");
12079
+ return { hidden, silent: hidden || driver >= 0 && words[driver + 1] === "Dummy" };
12080
+ }
12081
+ function commandLineWords(line) {
12082
+ const words = [];
12083
+ let word = "";
12084
+ let inWord = false;
12085
+ let quoted = false;
12086
+ for (const character of line) {
12087
+ if (character === '"') {
12088
+ quoted = !quoted;
12089
+ inWord = true;
12090
+ } else if (!quoted && /\s/.test(character)) {
12091
+ if (inWord) {
12092
+ words.push(word);
12093
+ }
12094
+ word = "";
12095
+ inWord = false;
12096
+ } else {
12097
+ word += character;
12098
+ inWord = true;
12099
+ }
12100
+ }
12101
+ if (inWord) {
12102
+ words.push(word);
12103
+ }
12104
+ return words;
12105
+ }
12075
12106
  function userDataIn(home, variables = process.env) {
12076
12107
  const moved = ["APPDATA", "XDG_DATA_HOME"];
12077
12108
  const carried = {};
@@ -17229,19 +17260,19 @@ var init_tool_definitions = __esm(() => {
17229
17260
  {
17230
17261
  name: "editor_launch",
17231
17262
  title: "Open, restart or close the editor, or bind its debug adapter again",
17232
- description: "Opens the Godot editor on a project, or restarts or closes the one already connected, or binds its debug adapter again. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; an editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead. open answers once the process has started, and the editor dials in after its import; a call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. Godot tries each server port once, as the editor starts, so a restart binds a debug adapter that came back without its port again, in the editor that came back. rebind does the same for the connected editor without restarting it, whoever opened it; a debug session open on the adapter ends with it and is opened again by the next debug call. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so in editor_status rather than waiting for an editor that is not coming, and open finishes it. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
17233
- answer: "close answers settingsDropped, naming what its save took out of project.godot and left out when it took nothing, and settingsNote when the file could not be read to tell; an editor stopped before it connected is answered with beforeConnecting. close and restart name under savedScenes the open scenes that had unsaved changes and were saved on the way out, an empty list when none had; a scene that would not save refuses the close or restart and leaves the editor running. Both answer tookMs, how long the editor took to go or to come back. restart answers with the version that came back, and says under settingsDropped which keys its save dropped, because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart answers lspListening and dapListening once the editor's servers are up. When either came back without its port, serversSaid gives the lines the editor's log panel has about its servers, the one place Godot says why a port would not bind, and when it was the debug adapter, which is bound again in the editor that came back, debugAdapterRebound says whether that took; neither is there when both came up. Godot clears that panel whenever a game is played, unless its run/output/always_clear_output_on_play setting is off, so after a play the lines from the editor's own start are gone. rebind answers rebound, with lspListening, dapListening and serversSaid, the lines that rebind made the panel print. A restart that a server's end interrupted shows as restartInterrupted in editor_status, and the open that finishes it says so under finishesRestart.",
17263
+ description: "Opens the Godot editor on a project, or restarts or closes the one already connected, or binds its debug adapter again. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; an editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead. open answers once the process has started, and the editor dials in after its import; a call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out. An editor a server opened is started again by the server, hidden or visible and silent or not as it was started unless hidden or silent says otherwise, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. Godot tries each server port once, as the editor starts, so a restart binds a debug adapter that came back without its port again, in the editor that came back. rebind does the same for the connected editor without restarting it, whoever opened it; a debug session open on the adapter ends with it and is opened again by the next debug call. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so in editor_status rather than waiting for an editor that is not coming, and open finishes it. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
17264
+ answer: "close answers settingsDropped, naming what its save took out of project.godot and left out when it took nothing, and settingsNote when the file could not be read to tell; an editor stopped before it connected is answered with beforeConnecting. close and restart name under savedScenes the open scenes that had unsaved changes and were saved on the way out, an empty list when none had; a scene that would not save refuses the close or restart and leaves the editor running. Both answer tookMs, how long the editor took to go or to come back. restart answers with the version that came back, and hidden, whether it came back with no window, and says under settingsDropped which keys its save dropped, because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart answers lspListening and dapListening once the editor's servers are up. When either came back without its port, serversSaid gives the lines the editor's log panel has about its servers, the one place Godot says why a port would not bind, and when it was the debug adapter, which is bound again in the editor that came back, debugAdapterRebound says whether that took; neither is there when both came up. Godot clears that panel whenever a game is played, unless its run/output/always_clear_output_on_play setting is off, so after a play the lines from the editor's own start are gone. rebind answers rebound, with lspListening, dapListening and serversSaid, the lines that rebind made the panel print. A restart that a server's end interrupted shows as restartInterrupted in editor_status, and the open that finishes it says so under finishesRestart.",
17234
17265
  parameters: {
17235
17266
  projectPath: { ...PROJECT_PATH, ops: ["open"] },
17236
17267
  hidden: {
17237
17268
  type: "boolean",
17238
17269
  ops: ["open", "restart"],
17239
- description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false: one visible editor, shown without taking the focus. On restart only for an editor a server opened; one opened by hand comes back as it was, so hidden is refused there."
17270
+ description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false on open: one visible editor, shown without taking the focus. On restart only for an editor a server opened, where the default is how the editor being replaced was started, read from its process; one whose start cannot be read is left running and refused until hidden is passed. One opened by hand comes back as it was, so hidden is refused there."
17240
17271
  },
17241
17272
  silent: {
17242
17273
  type: "boolean",
17243
17274
  ops: ["open", "restart"],
17244
- description: "Open the editor with no sound out of the machine, started with --audio-driver Dummy, so a tool script, a preview or a scene playing in its viewport is heard by nobody. Default true: pass false to hear it. The answer's arguments are what the editor was started with. The editor hands no audio driver to a game it plays, so editor_run's own silent decides that game's sound. A hidden editor is headless and silent either way. On restart only for an editor a server opened; one opened by hand comes back as it was."
17275
+ description: "Open the editor with no sound out of the machine, started with --audio-driver Dummy, so a tool script, a preview or a scene playing in its viewport is heard by nobody. Default true on open, and on restart how the editor being replaced was started: pass false to hear it. The answer's arguments are what the editor was started with. The editor hands no audio driver to a game it plays, so editor_run's own silent decides that game's sound. A hidden editor is headless and silent either way. On restart only for an editor a server opened; one opened by hand comes back as it was."
17245
17276
  }
17246
17277
  },
17247
17278
  requires: [],
@@ -17418,7 +17449,7 @@ var init_tool_definitions = __esm(() => {
17418
17449
  requires: []
17419
17450
  }
17420
17451
  },
17421
- answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. runtime carries addonIsStale and staleNote when the game announced on a runtime addon other than this server's, which no runtime_* call is then sent to, and staleNote says what brings it into step. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). exitedBeforeStop is true when the run was already over when the stop came, so nothing was ended and exitCode, when anything collected it, is the one it ended with. 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, and durationMs runs from that engine's start to the check's answer, after its exit, so it holds no import that came first. start and check: " + IMPORTED_FIRST + " An exported build carries its own imports and is not asked. waitedForEditorScanMs and scanNote are left out when no editor scan was waited for. A silent windowed run has --audio-driver Dummy in its arguments; a headless run is silent without it, and one the editor plays with sound has no arguments of its own. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The functions of gdharness's own runtime are not among them: the runtime addon's, which answer its tools inside the game, and those of res://gdharness/loader.gd, which starts it. Their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
17452
+ answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. runtime carries addonIsStale and staleNote when the game announced on a runtime addon other than this server's, which no runtime_* call is then sent to, and staleNote says what brings it into step. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). exitedBeforeStop is true when the run was already over when the stop came, so nothing was ended and exitCode, when anything collected it, is the one it ended with. 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, and durationMs runs from that engine's start to the check's answer, after its exit, so it holds no import that came first. start and check: " + IMPORTED_FIRST + " An exported build carries its own imports and is not asked. waitedForEditorScanMs and scanNote are left out when no editor scan was waited for. A silent windowed run has --audio-driver Dummy in its arguments; a headless run is silent without it, and one the editor plays with sound has no arguments of its own. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The functions of gdharness's own runtime are not among them: the runtime addon's, which answer its tools inside the game, and those of res://gdharness/loader.gd, which starts it. Their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. firstFrame and lastFrame are the game's own numbers, counted from 0, for the first and the last of them, in a run this server started; a firstFrame above 0 is frames run before the profiler came on, and a gap between the two is frames that never arrived, which the note says. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
17422
17453
  defaultOperation: "start"
17423
17454
  },
17424
17455
  {
@@ -17662,7 +17693,7 @@ var init_tool_definitions = __esm(() => {
17662
17693
  },
17663
17694
  find: {
17664
17695
  effect: "read",
17665
- summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each, hidden ones included unless includeHidden says otherwise; with it false, a control drawn wholly outside the viewport or a ScrollContainer is left out too. A match drawn under a matching node saying the same words, as an outline or a glow copy is, names that node under copyOf, and the answer counts the rest under distinct",
17696
+ summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each, hidden ones included unless includeHidden says otherwise; with it false, a control drawn wholly outside the viewport or a ScrollContainer is left out too. A match drawn under a matching node drawing the same words, as an outline or a glow copy is, names that node under copyOf (a name given only for a screen reader is not drawn, so a node holding one is never what a copy copies), and the answer counts the rest under distinct",
17666
17697
  requires: []
17667
17698
  },
17668
17699
  rect: {
@@ -48345,7 +48376,7 @@ function totalsSince(now, mark) {
48345
48376
  }
48346
48377
  }
48347
48378
  shrunk += Object.keys(mark.functions).filter((signature) => now.functions[signature] === undefined && (mark.functions[signature]?.calls ?? 0) > 0).length;
48348
- const { dropped: droppedInAll, ...kept } = now;
48379
+ const { dropped: droppedInAll, firstFrame: _first, lastFrame: _last, ...kept } = now;
48349
48380
  const dropped = (droppedInAll ?? 0) - (mark.dropped ?? 0);
48350
48381
  return {
48351
48382
  totals: {
@@ -49830,6 +49861,15 @@ function profileAnswer(reading, limit) {
49830
49861
  } else {
49831
49862
  notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals, which it sends only when the profiler is switched off.${totalsWithheld} Not in them is any script time after the last frame sent: a frame still queued as the game exited, since the engine stops sending without emptying its queue, and what scripts did as it quit, such as in _exit_tree. That can be nothing: a scene that does its work in _ready and quits has all of it in its first frame.`);
49832
49863
  }
49864
+ const unprofiled = totals.firstFrame ?? 0;
49865
+ if (reading.through === "gdharness" && unprofiled > 0) {
49866
+ notes.push(`The profiler came on at the game's frame ${unprofiled}, so ${unprofiled === 1 ? "its first frame is" : `its first ${unprofiled} frames are`} not in these, the main scene's _ready among them${reading.runtimeAddon === true ? "" : ": without the runtime addon nothing holds the game's start until the profiler is on"}.`);
49867
+ }
49868
+ const unarrived = totals.firstFrame === undefined || totals.lastFrame === undefined ? 0 : totals.lastFrame - totals.firstFrame + 1 - totals.frames - totals.unreadable;
49869
+ if (unarrived > 0) {
49870
+ const howMany = unarrived === 1 ? "One" : String(unarrived);
49871
+ notes.push(`${howMany} of the game's frames between the first and the last that arrived did not arrive, so ${unarrived === 1 ? "its" : "their"} script time is not in these.`);
49872
+ }
49833
49873
  if (table.superCallers > 0) {
49834
49874
  notes.push(`${table.superCallers === 1 ? "One function calls" : `${table.superCallers} functions call`} through super, marked selfIncludesSuper: the engine takes every other call out of the caller's self time, and not a super call, so ${table.superCallers === 1 ? "its" : "their"} selfMs holds the overridden function's time, which that function's own row counts again. Self time therefore cannot be added along an override chain, and scriptMs counts the time under a super call once more for each level above it.`);
49835
49875
  }
@@ -49853,6 +49893,8 @@ function profileAnswer(reading, limit) {
49853
49893
  connected: true,
49854
49894
  complete: totals.complete,
49855
49895
  frames: totals.frames,
49896
+ firstFrame: totals.firstFrame,
49897
+ lastFrame: totals.lastFrame,
49856
49898
  coveredMs: reading.coveredMs,
49857
49899
  scriptMs: table.scriptMs,
49858
49900
  perCallOverheadUs: table.perCallUs ?? undefined,
@@ -53601,8 +53643,21 @@ ${error.text}`;
53601
53643
  "editor_launch restart without hidden restarts it as it is"
53602
53644
  ]);
53603
53645
  }
53646
+ let hidden = args["hidden"] === true;
53647
+ let silent = resolveSilent(args["silent"]);
53648
+ if (mine && (typeof args["hidden"] !== "boolean" || typeof args["silent"] !== "boolean")) {
53649
+ const was = before.editorPid === undefined ? null : await runningAs(before.editorPid);
53650
+ if (was?.kind === "commandLine") {
53651
+ const as = startedAs(was.text);
53652
+ hidden = typeof args["hidden"] === "boolean" ? args["hidden"] : as.hidden;
53653
+ silent = typeof args["silent"] === "boolean" ? args["silent"] : as.silent;
53654
+ } else if (typeof args["hidden"] !== "boolean") {
53655
+ const why = was === null ? "the operating system would not say" : was.unread ?? "it gave no command line";
53656
+ return this.createErrorResponse(`How the editor (pid ${before.editorPid ?? "unknown"}) was started could not be read (${why}), so whether it was hidden cannot be told and it was left running.`, ["Pass hidden: true to bring it back with no window, or hidden: false to bring it back visible"]);
53657
+ }
53658
+ }
53604
53659
  const settingsBefore = this.settingKeysOf(before.projectPath);
53605
- const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, args["hidden"] === true, resolveSilent(args["silent"])) : await this.handleViaBridge("restart_editor", {});
53660
+ const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, hidden, silent) : await this.handleViaBridge("restart_editor", {});
53606
53661
  if (asked.isError === true) {
53607
53662
  return asked;
53608
53663
  }
@@ -53621,6 +53676,7 @@ ${error.text}`;
53621
53676
  const after = this.settingKeysOf(before.projectPath);
53622
53677
  return this.jsonTextResponse({
53623
53678
  restarted: true,
53679
+ hidden: mine && hidden,
53624
53680
  editorPid: now.editorPid,
53625
53681
  addonVersion: now.addonVersion,
53626
53682
  serverVersion: SERVER_VERSION,
@@ -133,6 +133,21 @@ func _reload_script(path: String) -> void:
133
133
 
134
134
  func _reload_scene(path: String) -> void:
135
135
  var edited_scene: Node = EditorInterface.get_edited_scene_root()
136
- if edited_scene and edited_scene.scene_file_path == path:
137
- print("[gdharness] Scene changed on disk, reloading: ", path)
138
- EditorInterface.reload_scene_from_path(path)
136
+ if not edited_scene or edited_scene.scene_file_path != path:
137
+ return
138
+ # A reload from the file drops the edits the editor holds, without a prompt. gdharness refuses to
139
+ # write over them, so what is seen here is a write by somebody else or one the editor's tab has
140
+ # already taken in, made just before an edit inside the same poll, and neither is worth the edit.
141
+ if EditorInterface.get_unsaved_scenes().has(path):
142
+ push_warning(
143
+ (
144
+ (
145
+ "gdharness: %s changed on disk while the editor holds unsaved changes to it, so it "
146
+ + "was not reloaded; saving it writes the editor's copy over the file"
147
+ )
148
+ % path
149
+ )
150
+ )
151
+ return
152
+ print("[gdharness] Scene changed on disk, reloading: ", path)
153
+ EditorInterface.reload_scene_from_path(path)
@@ -20,21 +20,27 @@ extends RefCounted
20
20
  ## A label is asked about first and read directly, because a screen is mostly labels and this runs
21
21
  ## for each of them on every look a wait takes.
22
22
  static func said_by(node: Node) -> String:
23
- var label: Label = node as Label
24
- if label != null:
25
- return _label_says(label)
26
- if draws_a_list(node):
27
- return "\n".join(lines_said_by(node))
28
- var own: String = _text_of(node)
23
+ var own: String = drawn_by(node)
29
24
  # A control showing a picture rather than words is named for a player who cannot see it, and that
30
25
  # name is the one the words a caller asks by are meant for: the rail's "Out" tab is a square with
31
26
  # an icon on it, and a click on "Out" found only the words around it.
32
- if own.is_empty() and node is Control:
27
+ if own.is_empty() and node is Control and not node is Label and not draws_a_list(node):
33
28
  var control: Control = node
34
29
  return control.atr(control.accessibility_name).strip_edges()
35
30
  return own
36
31
 
37
32
 
33
+ ## The words [param node] draws: what [method said_by] answers, without the name a control is given
34
+ ## for a player who cannot see it, which nothing on the screen shows.
35
+ static func drawn_by(node: Node) -> String:
36
+ var label: Label = node as Label
37
+ if label != null:
38
+ return _label_says(label)
39
+ if draws_a_list(node):
40
+ return "\n".join(lines_said_by(node))
41
+ return _text_of(node)
42
+
43
+
38
44
  ## Every line [param node] draws for a player to read, one to an entry: a label's one line, or each
39
45
  ## item of a control that draws a list of them. Tab titles, list items, tree rows, menu titles and
40
46
  ## the items of an open menu are held by the control rather than in any `text`, so a screen read as
@@ -55,10 +61,12 @@ static func lines_said_by(node: Node) -> Array[String]:
55
61
  return lines
56
62
 
57
63
 
58
- ## The node [param node] is drawn as a copy of, or null: the nearest node above it that says
59
- ## anything, when it says the same words, followed up while that one is a copy in turn. A word drawn
64
+ ## The node [param node] is drawn as a copy of, or null: the nearest node above it that draws any
65
+ ## words, when it draws the same ones, followed up while that one is a copy in turn. A word drawn
60
66
  ## with an outline or a glow is often a label holding copies of itself, offset or tinted, and a
61
- ## player reads one sign where a find counted four.
67
+ ## player reads one sign where a find counted four. Drawn words only: a page's root named for a
68
+ ## screen reader by the title its heading shows draws nothing, and taken for the original it left
69
+ ## the heading read as a copy of words nobody reads.
62
70
  static func copied_from(node: Node) -> Node:
63
71
  var chain: Array[Node] = copied_through(node)
64
72
  return null if chain.is_empty() else chain.back()
@@ -68,12 +76,12 @@ static func copied_from(node: Node) -> Node:
68
76
  ## [method copied_from], which is the last of them.
69
77
  static func copied_through(node: Node) -> Array[Node]:
70
78
  var chain: Array[Node] = []
71
- var words: String = said_by(node)
79
+ var words: String = drawn_by(node)
72
80
  if words.is_empty():
73
81
  return chain
74
82
  var above: Node = node.get_parent()
75
83
  while above != null:
76
- var said: String = said_by(above)
84
+ var said: String = drawn_by(above)
77
85
  if not said.is_empty():
78
86
  if said != words:
79
87
  break
package/build/keeper.js CHANGED
@@ -9795,6 +9795,8 @@ class ProfileAggregate {
9795
9795
  complete = false;
9796
9796
  frames = 0;
9797
9797
  lastFrameAt = null;
9798
+ firstFrame = null;
9799
+ lastFrame = null;
9798
9800
  unreadable = 0;
9799
9801
  cappedFrames = 0;
9800
9802
  totalCapped = false;
@@ -9826,6 +9828,11 @@ class ProfileAggregate {
9826
9828
  this.frames += 1;
9827
9829
  this.lastFrameAt = now;
9828
9830
  this.cappedFrames += carried >= this.frameFunctions ? 1 : 0;
9831
+ const numbered = received.data[0];
9832
+ if (typeof numbered === "number") {
9833
+ this.firstFrame ??= numbered;
9834
+ this.lastFrame = numbered;
9835
+ }
9829
9836
  }
9830
9837
  } else if (received.message === "servers:profile_total") {
9831
9838
  const whole = new Map;
@@ -9884,7 +9891,8 @@ class ProfileAggregate {
9884
9891
  frameFunctions: this.frameFunctions,
9885
9892
  cappedFrames: this.cappedFrames,
9886
9893
  totalCapped: this.totalCapped,
9887
- ...this.dropped > 0 ? { dropped: this.dropped } : {}
9894
+ ...this.dropped > 0 ? { dropped: this.dropped } : {},
9895
+ ...this.firstFrame === null || this.lastFrame === null ? {} : { firstFrame: this.firstFrame, lastFrame: this.lastFrame }
9888
9896
  };
9889
9897
  }
9890
9898
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.28",
3
+ "version": "1.3.29",
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",