gdharness 1.1.59 → 1.1.60

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
@@ -9183,6 +9183,65 @@ function enclosingFunction(source, line, code = codeOf(source)) {
9183
9183
  }
9184
9184
  return null;
9185
9185
  }
9186
+ function afterHeader(text, from) {
9187
+ let depth = 0;
9188
+ for (let at = text.indexOf("(", from);at !== -1 && at < text.length; at += 1) {
9189
+ if (text[at] === "(") {
9190
+ depth += 1;
9191
+ } else if (text[at] === ")") {
9192
+ depth -= 1;
9193
+ if (depth === 0) {
9194
+ const colon = text.indexOf(":", at);
9195
+ return colon === -1 ? -1 : colon + 1;
9196
+ }
9197
+ }
9198
+ }
9199
+ return -1;
9200
+ }
9201
+ function bodyCallsSuper(code, line, lambda) {
9202
+ const lines = code.split(`
9203
+ `);
9204
+ const first = lines[line - 1];
9205
+ if (first === undefined) {
9206
+ return false;
9207
+ }
9208
+ const header = lambda ? LAMBDA_HEADER.exec(first) : FUNC_LINE.exec(first);
9209
+ if (header !== null) {
9210
+ const body = afterHeader(first, header.index);
9211
+ if (body !== -1 && first.slice(body).trim() !== "") {
9212
+ return SUPER_CALL.test(first.slice(body));
9213
+ }
9214
+ }
9215
+ const indent = indentOf(first);
9216
+ let lambdaIndent = null;
9217
+ for (let index = line - 1;index < lines.length; index += 1) {
9218
+ const text = lines[index] ?? "";
9219
+ if (text.trim() === "") {
9220
+ continue;
9221
+ }
9222
+ const at = indentOf(text);
9223
+ if (at < indent) {
9224
+ break;
9225
+ }
9226
+ if (lambdaIndent !== null) {
9227
+ if (at > lambdaIndent) {
9228
+ continue;
9229
+ }
9230
+ lambdaIndent = null;
9231
+ }
9232
+ const inner = LAMBDA_HEADER.exec(text);
9233
+ if (SUPER_CALL.test(inner === null ? text : text.slice(0, inner.index))) {
9234
+ return true;
9235
+ }
9236
+ if (inner !== null) {
9237
+ const body = afterHeader(text, inner.index);
9238
+ if (body !== -1 && text.slice(body).trim() === "") {
9239
+ lambdaIndent = at;
9240
+ }
9241
+ }
9242
+ }
9243
+ return false;
9244
+ }
9186
9245
  function callSitesOf(source, name) {
9187
9246
  const regions = regionsOf(source);
9188
9247
  const code = codeOf(source, regions);
@@ -9208,7 +9267,7 @@ function collectEnumValues(text, from, into) {
9208
9267
  function isIdentifier(name) {
9209
9268
  return new RegExp(`^${NAME}$`, "u").test(name) && !KEYWORDS.has(name);
9210
9269
  }
9211
- var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, FUNC_LINE, KEYWORDS;
9270
+ var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, FUNC_LINE, SUPER_CALL, LAMBDA_HEADER, KEYWORDS;
9212
9271
  var init_gdscript_source = __esm(() => {
9213
9272
  IDENTIFIER_START = /[\p{L}_]/u;
9214
9273
  IDENTIFIER_PART = /[\p{L}\p{N}_]/u;
@@ -9218,6 +9277,8 @@ var init_gdscript_source = __esm(() => {
9218
9277
  EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
9219
9278
  EXTENDS_CLAUSE = /\bextends(?=\s)/u;
9220
9279
  FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
9280
+ SUPER_CALL = /(?<![\p{L}\p{N}_.])super\s*[.(]/u;
9281
+ LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
9221
9282
  KEYWORDS = new Set([
9222
9283
  "and",
9223
9284
  "as",
@@ -18218,7 +18279,7 @@ var init_tool_definitions = __esm(() => {
18218
18279
  },
18219
18280
  wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
18220
18281
  profile: {
18221
- summary: "where the run spends its time by GDScript function, while it runs or after it ended: 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, sorted by self time, 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 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. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
18282
+ summary: "where the run spends its time by GDScript function, while it runs or after it ended: 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, sorted by self time, 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. 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. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
18222
18283
  requires: []
18223
18284
  }
18224
18285
  },
@@ -34131,7 +34192,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
34131
34192
  ok: false,
34132
34193
  reason: "busy",
34133
34194
  requestId: id,
34134
- message: `${still} Nothing was cancelled: a call that takes longer than the wait goes on and does everything it was asked, so read the state back rather than repeating it. runtime_invoke op result with requestId ${id} answers with its reply once it comes, and timeoutMs waits longer next time. A game held at a breakpoint instead is one debug_state stack answers about.`
34195
+ message: `${still} Nothing was cancelled: a call that takes longer than the wait goes on and does everything it was asked, so read the state back rather than repeating it. runtime_invoke op result with requestId ${id} answers with its reply once it comes. A game held at a breakpoint instead is one debug_state stack answers about.`
34135
34196
  });
34136
34197
  }, timeoutMs);
34137
34198
  socket.setEncoding("utf8");
@@ -42325,25 +42386,36 @@ function signatureParts(signature) {
42325
42386
  return { script: parts.slice(0, -2).join("::"), line: Number.isFinite(line) ? line : 0, function: name };
42326
42387
  }
42327
42388
  function profiledFunctions(totals, limit, sourceOf = () => null) {
42328
- const rows = rowsOf(totals);
42389
+ const rows = rowsOf(totals, sourceOf);
42329
42390
  const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
42330
42391
  return {
42331
42392
  functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
42332
42393
  omitted: Math.max(0, rows.length - limit),
42333
42394
  scriptMs,
42334
- counted: rows.length
42395
+ counted: rows.length,
42396
+ superCallers: rows.filter((row) => row.selfIncludesSuper === true).length
42335
42397
  };
42336
42398
  }
42337
- function rowsOf(totals) {
42399
+ function rowsOf(totals, sourceOf) {
42400
+ const codes = new Map;
42401
+ const codeOfScript = (script) => {
42402
+ if (!codes.has(script)) {
42403
+ const source = sourceOf(script);
42404
+ codes.set(script, source === null ? null : codeOf(source));
42405
+ }
42406
+ return codes.get(script) ?? null;
42407
+ };
42338
42408
  return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
42339
42409
  const parts = signatureParts(signature);
42410
+ const code = codeOfScript(parts.script);
42340
42411
  return {
42341
42412
  script: parts.script,
42342
42413
  function: parts.function,
42343
42414
  line: parts.line,
42344
42415
  calls: sum.calls,
42345
42416
  selfMs: ms(sum.selfSeconds),
42346
- totalMs: ms(sum.totalSeconds)
42417
+ totalMs: ms(sum.totalSeconds),
42418
+ ...code !== null && bodyCallsSuper(code, parts.line, parts.function.endsWith("(lambda)")) ? { selfIncludesSuper: true } : {}
42347
42419
  };
42348
42420
  }).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
42349
42421
  }
@@ -42359,17 +42431,24 @@ function placedLambda(row, sourceOf) {
42359
42431
  return within === null ? row : { ...row, within };
42360
42432
  }
42361
42433
  function callersOf(totals, name, scripts) {
42362
- const rows = rowsOf(totals);
42434
+ const listed = [...scripts];
42435
+ const sources = new Map(listed);
42436
+ const rows = rowsOf(totals, (script) => sources.get(script) ?? null);
42363
42437
  const bare = bareName(name);
42364
42438
  const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
42365
42439
  const sites = [];
42366
- for (const [script, source] of scripts) {
42440
+ for (const [script, source] of listed) {
42367
42441
  for (const site of callSitesOf(source, bare)) {
42368
42442
  const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
42369
42443
  sites.push({
42370
42444
  script,
42371
42445
  ...site,
42372
- ...within === undefined ? {} : { withinCalls: within.calls, withinSelfMs: within.selfMs, withinTotalMs: within.totalMs }
42446
+ ...within === undefined ? {} : {
42447
+ withinCalls: within.calls,
42448
+ withinSelfMs: within.selfMs,
42449
+ withinTotalMs: within.totalMs,
42450
+ ...within.selfIncludesSuper === true ? { withinSelfIncludesSuper: true } : {}
42451
+ }
42373
42452
  });
42374
42453
  }
42375
42454
  }
@@ -42406,8 +42485,14 @@ import { setTimeout as delay4 } from "node:timers/promises";
42406
42485
  import { fileURLToPath as fileURLToPath5 } from "node:url";
42407
42486
  import { promisify as promisify5 } from "node:util";
42408
42487
  function editorRestartTimeoutMs() {
42409
- const override = Number.parseInt(envValue("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS") ?? "", 10);
42410
- return Number.isInteger(override) && override > 0 ? override : 90000;
42488
+ return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
42489
+ }
42490
+ function waitFromEnv(variable, fallback) {
42491
+ const override = Number.parseInt(envValue(variable) ?? "", 10);
42492
+ return Number.isInteger(override) && override > 0 ? override : fallback;
42493
+ }
42494
+ function greetingWaitMs() {
42495
+ return waitFromEnv("GDHARNESS_GREETING_WAIT_MS", 1e4);
42411
42496
  }
42412
42497
  function restartUnderway(note, now = Date.now()) {
42413
42498
  return alive(note.byPid) && now - Date.parse(note.quitAt) < editorRestartTimeoutMs() + 30000;
@@ -42458,6 +42543,14 @@ function captureDetail(payload) {
42458
42543
  function patienceForFrames(frames, atLeast) {
42459
42544
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
42460
42545
  }
42546
+ function longerWaitNote(tool, op, waitedMs) {
42547
+ const spec = TOOL_SPECS.find((one) => one.name === tool);
42548
+ if (spec !== undefined && argumentsOf(spec, op).includes("timeoutMs")) {
42549
+ return "timeoutMs waits longer next time.";
42550
+ }
42551
+ const how = tool === "runtime_wait" && op === "frames" ? `the wait is counted from the number of frames, at ${SLOWEST_FRAME_RATE} a second on top of GDHARNESS_RUNTIME_TIMEOUT_MS, and this game fell below that rate, so ask for fewer frames at a time` : "the wait is GDHARNESS_RUNTIME_TIMEOUT_MS, which is set in the environment the server is started with";
42552
+ return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
42553
+ }
42461
42554
  function projectSpelling(projectPath, absolutePath) {
42462
42555
  return `res://${relative5(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
42463
42556
  }
@@ -42537,7 +42630,10 @@ function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
42537
42630
  function noCodeWillCome(run) {
42538
42631
  return !exited(run);
42539
42632
  }
42540
- async function untilGone(pids, withinMs = STOP_WAIT_MS) {
42633
+ function stopWaitMs() {
42634
+ return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
42635
+ }
42636
+ async function untilGone(pids, withinMs = stopWaitMs()) {
42541
42637
  const gone = () => pids.every((pid) => !alive(pid));
42542
42638
  const deadline = Date.now() + withinMs;
42543
42639
  while (!gone() && Date.now() < deadline) {
@@ -42592,7 +42688,7 @@ function stopVerdict(stop) {
42592
42688
  return {
42593
42689
  stopped: false,
42594
42690
  stillGoing: stop.endedPid,
42595
- note: `${told} ${STOP_WAIT_MS / 1000} seconds later, so it has not stopped. editor_status says when it has gone, and editor_run stop asks again.`,
42691
+ note: `${told} ${stopWaitMs() / 1000} seconds later, so it has not stopped. editor_status says when it has gone, and editor_run stop asks again.`,
42596
42692
  withChildren: true
42597
42693
  };
42598
42694
  }
@@ -42710,7 +42806,7 @@ function endedToStartThis(ended) {
42710
42806
  return ` ${which} was not ended here: the editor could not be asked to stop it. The editor stops a play before starting the next, so it is not running beside this one if this start reached the editor.`;
42711
42807
  }
42712
42808
  if (ended.ending === "lingering") {
42713
- return ` ${which} was told to end to start this one and was still running ${STOP_WAIT_MS / 1000} seconds later, so it is running beside this one: editor_status says when it has gone.`;
42809
+ return ` ${which} was told to end to start this one and was still running ${stopWaitMs() / 1000} seconds later, so it is running beside this one: editor_status says when it has gone.`;
42714
42810
  }
42715
42811
  return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
42716
42812
  }
@@ -42827,6 +42923,9 @@ function outsideTheirRanges(spec, args) {
42827
42923
  }
42828
42924
  return complaints;
42829
42925
  }
42926
+ function scanWaitMs() {
42927
+ return waitFromEnv("GDHARNESS_SCAN_WAIT_MS", 30000);
42928
+ }
42830
42929
  function scanWaitIn(payload) {
42831
42930
  const waited = readNumber(payload, "waitedForEditorScanMs");
42832
42931
  const note = readString(payload, "scanNote");
@@ -42885,7 +42984,7 @@ function scanWaitAnswer(scanned, started = "game") {
42885
42984
  if (scanned.waitedMs === 0) {
42886
42985
  return {};
42887
42986
  }
42888
- const still = `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so`;
42987
+ const still = `The editor was still scanning the project after ${scanWaitMs() / 1000} seconds, so`;
42889
42988
  return {
42890
42989
  waitedForEditorScanMs: scanned.waitedMs,
42891
42990
  scanNote: !scanned.stillScanning ? undefined : started === "game" ? `${still} this game started during the scan and may have read a class cache being rewritten. If ` + 'editor_output shows "Could not find type" for a class that is declared, editor_rescan ' + "waits for the scan to finish, and a start after it reads the finished cache." : `${still} the engine answering this started during the scan and may have read a class cache being ` + "rewritten, and a class cache it wrote can be written over when the scan finishes. If " + 'engine_messages shows "Could not find type" for a class that is declared, editor_rescan waits ' + "for the scan to finish, and the same call after it reads the finished cache."
@@ -43140,14 +43239,20 @@ function profileAnswer(reading, limit) {
43140
43239
  const table = profiledFunctions(totals, limit, reading.sourceOf);
43141
43240
  const notes = [];
43142
43241
  const addonNote = reading.runtimeAddon ? "" : " This project has no gdharness runtime addon, which switches the profiler off as the game quits so the engine sends its totals: setup installs it.";
43143
- if (totals.complete) {} else if (reading.running && totals.frames === 0) {
43242
+ const totalsWithheld = reading.runtimeAddon ? " The runtime addon switches the profiler off as the game quits, and did not here: either it was no longer running in this game by then, which a project can arrange for some scenes; or the game ended without taking its tree down, as a crash or a kill does; or the totals did not reach the debugger before the game exited, which the game then says in its output." : addonNote;
43243
+ if (totals.complete && reading.running) {
43244
+ notes.push(`The engine's totals arrived while the game is still running, which it sends only when the profiler is switched off, so ${reading.through === "editor" ? "the editor's Profiler tab or the game itself, through EngineDebugger," : "the game itself, through EngineDebugger,"} switched it off before the game ended. They cover the run up to then and nothing after.`);
43245
+ } else if (totals.complete) {} else if (reading.running && totals.frames === 0) {
43144
43246
  notes.push(`The profiler is on and the game has not finished its first frame yet: a frame is sent as it ends, so a scene doing its work in _ready shows nothing until that returns, and the engine's own totals arrive when the game quits.${addonNote}`);
43145
43247
  } else if (reading.running) {
43146
43248
  notes.push(`The run is still going, so these are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} summed so far${reading.through === "gdharness" ? ", written at most half a second behind" : ""}.`);
43147
43249
  } else if (totals.frames === 0) {
43148
- notes.push(`The game connected and ended before a frame of the profile reached ${reading.through === "editor" ? "the editor" : "this server"}: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${addonNote}`);
43250
+ notes.push(`The game connected and ended before a frame of the profile reached ${reading.through === "editor" ? "the editor" : "this server"}: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${totalsWithheld}`);
43149
43251
  } else {
43150
- notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals, so what the game did after the last of them is not in it: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${addonNote}`);
43252
+ 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.`);
43253
+ }
43254
+ if (table.superCallers > 0) {
43255
+ 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.`);
43151
43256
  }
43152
43257
  notes.push(...cappedNotes(totals));
43153
43258
  if (totals.unreadable > 0) {
@@ -43884,7 +43989,7 @@ class GodotServer {
43884
43989
  }
43885
43990
  async dispatch(tool, op, args) {
43886
43991
  if (tool !== "editor_status" && this.godotBridge.isArriving()) {
43887
- await this.waitForBridge(() => !this.godotBridge.isArriving(), Date.now() + GREETING_WAIT_MS);
43992
+ await this.waitForBridge(() => !this.godotBridge.isArriving(), Date.now() + greetingWaitMs());
43888
43993
  }
43889
43994
  if (ENGINE_PASSES[tool]?.[op] !== undefined) {
43890
43995
  return op === "reimport" ? await this.handleReimport(args) : await this.handleRefreshUids(args);
@@ -44044,7 +44149,7 @@ class GodotServer {
44044
44149
  depth: depth ?? 3,
44045
44150
  include_properties: readBoolean(args, "includeProperties") ?? false,
44046
44151
  properties: readArray(args, "properties") ?? []
44047
- }, patience);
44152
+ }, { tool: "runtime_inspect", op, patience });
44048
44153
  }
44049
44154
  case "find":
44050
44155
  return await this.handleFindRuntimeNodes(args, patience);
@@ -44054,17 +44159,17 @@ class GodotServer {
44054
44159
  root: readNonEmptyString(args, "nodePath") ?? "/root",
44055
44160
  include_hidden: readBoolean(args, "includeHidden") ?? false,
44056
44161
  limit: readPositiveNumber(args, "limit") ?? 500
44057
- }, patience);
44162
+ }, { tool: "runtime_inspect", op, patience });
44058
44163
  case "rect":
44059
- return await this.handleRuntimeCommand("get_rect", { ...whichGame(args), path: readNonEmptyString(args, "nodePath") ?? "" }, patience);
44164
+ return await this.handleRuntimeCommand("get_rect", { ...whichGame(args), path: readNonEmptyString(args, "nodePath") ?? "" }, { tool: "runtime_inspect", op, patience });
44060
44165
  case "property":
44061
44166
  return await this.handleRuntimeCommand("get_property", {
44062
44167
  ...whichGame(args),
44063
44168
  path: readNonEmptyString(args, "nodePath") ?? "",
44064
44169
  property: readNonEmptyString(args, "property") ?? ""
44065
- }, patience);
44170
+ }, { tool: "runtime_inspect", op, patience });
44066
44171
  default:
44067
- return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, patience);
44172
+ return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, { tool: "runtime_inspect", op, patience });
44068
44173
  }
44069
44174
  }
44070
44175
  case "runtime_invoke": {
@@ -44077,16 +44182,16 @@ class GodotServer {
44077
44182
  path: readNonEmptyString(args, "nodePath") ?? "",
44078
44183
  property: readString(args, "property") ?? "",
44079
44184
  value: args["value"]
44080
- }, patience) : await this.handleRuntimeCommand("call_method", {
44185
+ }, { tool: "runtime_invoke", op, patience }) : await this.handleRuntimeCommand("call_method", {
44081
44186
  ...whichGame(args),
44082
44187
  path: readNonEmptyString(args, "nodePath") ?? "",
44083
44188
  method: readString(args, "method") ?? "",
44084
44189
  args: readArray(args, "args") ?? [],
44085
44190
  ...args["properties"] === undefined ? {} : { properties: args["properties"] }
44086
- }, patience);
44191
+ }, { tool: "runtime_invoke", op, patience });
44087
44192
  }
44088
44193
  case "runtime_capture":
44089
- return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args);
44194
+ return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args, { tool: "runtime_capture", op });
44090
44195
  case "runtime_input":
44091
44196
  if (op === "click") {
44092
44197
  if (readNonEmptyString(args, "nodePath") === undefined && readNonEmptyString(args, "says") === undefined) {
@@ -44099,7 +44204,7 @@ class GodotServer {
44099
44204
  double: readBoolean(args, "doubleClick") ?? false,
44100
44205
  ...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
44101
44206
  ...args["index"] === undefined ? {} : { index: args["index"] }
44102
- });
44207
+ }, { tool: "runtime_input", op });
44103
44208
  }
44104
44209
  if (op === "choose") {
44105
44210
  return await this.handleRuntimeCommand("choose", {
@@ -44107,9 +44212,9 @@ class GodotServer {
44107
44212
  path: readNonEmptyString(args, "nodePath") ?? "",
44108
44213
  text: readString(args, "text") ?? "",
44109
44214
  ...args["index"] === undefined ? {} : { index: args["index"] }
44110
- });
44215
+ }, { tool: "runtime_input", op });
44111
44216
  }
44112
- return await this.handleRuntimeCommand(`inject_${op}`, args);
44217
+ return await this.handleRuntimeCommand(`inject_${op}`, args, { tool: "runtime_input", op });
44113
44218
  case "runtime_wait":
44114
44219
  return await this.handleRuntimeWait(op, args);
44115
44220
  case "debug_breakpoint": {
@@ -44338,7 +44443,7 @@ class GodotServer {
44338
44443
  const answered = Date.now();
44339
44444
  if (status["scanning"] === true || status["importing"] === true || status["pending"] === true) {
44340
44445
  sawAScan = true;
44341
- if (Date.now() - started >= SCAN_WAIT_MS) {
44446
+ if (Date.now() - started >= scanWaitMs()) {
44342
44447
  return { waitedMs: Date.now() - started, stillScanning: true };
44343
44448
  }
44344
44449
  await new Promise((settle) => setTimeout(settle, 100));
@@ -45685,7 +45790,7 @@ class GodotServer {
45685
45790
  ]);
45686
45791
  }
45687
45792
  if (!hasSaidWhoItIs(before)) {
45688
- return this.createErrorResponse(`The editor has connected and has not said who it is within ${GREETING_WAIT_MS / 1000}s, so whether this server or the editor itself restarts it cannot be told.`, ["editor_status says when it has, under greeting while it has not", "Then ask for the restart again"]);
45793
+ return this.createErrorResponse(`The editor has connected and has not said who it is within ${greetingWaitMs() / 1000}s, so whether this server or the editor itself restarts it cannot be told.`, ["editor_status says when it has, under greeting while it has not", "Then ask for the restart again"]);
45689
45794
  }
45690
45795
  const mine = before.openedByAServer === true && before.projectPath !== undefined;
45691
45796
  if (!mine && args["hidden"] === true) {
@@ -46300,7 +46405,7 @@ class GodotServer {
46300
46405
  game.brokeOn = halt.text;
46301
46406
  game.log.record("error", halt.text);
46302
46407
  }
46303
- async untilTheEditorStopsPlaying(withinMs = STOP_WAIT_MS) {
46408
+ async untilTheEditorStopsPlaying(withinMs = stopWaitMs()) {
46304
46409
  const deadline = Date.now() + withinMs;
46305
46410
  for (;; ) {
46306
46411
  if ((await this.editorPlayingState())?.playing === false) {
@@ -46394,7 +46499,7 @@ class GodotServer {
46394
46499
  running.endedThrough = "signal";
46395
46500
  }
46396
46501
  clearTheRecord();
46397
- running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${STOP_WAIT_MS / 1000} seconds later.` : landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
46502
+ running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${stopWaitMs() / 1000} seconds later.` : landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
46398
46503
  return gone ? "gone" : "lingering";
46399
46504
  }
46400
46505
  async endThroughTheKeeper(run, pid, keeper) {
@@ -46414,7 +46519,7 @@ class GodotServer {
46414
46519
  run.exitCode = null;
46415
46520
  run.exitSignal = "SIGTERM";
46416
46521
  }
46417
- run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${STOP_WAIT_MS / 1000} seconds later.` : asked === "signalled" ? `gdharness ended pid ${pid} through the keeper holding it.` : `gdharness asked the keeper holding pid ${pid} to end it, and it had already gone.`);
46522
+ run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${stopWaitMs() / 1000} seconds later.` : asked === "signalled" ? `gdharness ended pid ${pid} through the keeper holding it.` : `gdharness asked the keeper holding pid ${pid} to end it, and it had already gone.`);
46418
46523
  return gone ? "gone" : "lingering";
46419
46524
  }
46420
46525
  spawnGame(godotPath, cmdArgs, env) {
@@ -47339,7 +47444,7 @@ class GodotServer {
47339
47444
  ...landed ? { endedThrough: "signal" } : {},
47340
47445
  ...childrenFields(ended),
47341
47446
  exitedBeforeStop: !landed,
47342
- note: `${!gone ? `pid ${pid} was told to end and was still running ${STOP_WAIT_MS / 1000} seconds later, so it has not stopped: a game of this project that no server here was holding,` : landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" : " editor_status says when it has gone."} ${aboutTheChildren(ended, false)}`
47447
+ note: `${!gone ? `pid ${pid} was told to end and was still running ${stopWaitMs() / 1000} seconds later, so it has not stopped: a game of this project that no server here was holding,` : landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" : " editor_status says when it has gone."} ${aboutTheChildren(ended, false)}`
47343
47448
  });
47344
47449
  }
47345
47450
  async whatTheRunStarted(run) {
@@ -48075,7 +48180,8 @@ class GodotServer {
48075
48180
  }
48076
48181
  return `The game this server started (${which}) is still coming up and has not announced its runtime after a wait of ${Math.min(budgetMs, remaining)}ms, so there is nothing to talk to yet. It is still running: ask again in a moment rather than starting another.`;
48077
48182
  }
48078
- async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
48183
+ async handleRuntimeCommand(command, args, asked) {
48184
+ const timeoutMs = asked.patience ?? this.runtimeTimeoutMs();
48079
48185
  const { op: _op, projectPath, pid, outputPath, ...params } = asParams(args);
48080
48186
  const waited = typeof pid === "number" ? null : await this.ownRunOnItsWay(typeof projectPath === "string" ? projectPath : this.ownProject, timeoutMs);
48081
48187
  if (waited !== null) {
@@ -48128,7 +48234,7 @@ ${staleNote}`);
48128
48234
  requestId: reply.requestId,
48129
48235
  command,
48130
48236
  waitedMs: timeoutMs,
48131
- note: reply.message,
48237
+ note: `${reply.message} ${longerWaitNote(asked.tool, asked.op, timeoutMs)}`,
48132
48238
  ...answeredBy,
48133
48239
  ...stale
48134
48240
  });
@@ -48228,13 +48334,13 @@ ${staleNote}`}`
48228
48334
  limit: readPositiveNumber(args, "limit") ?? 100,
48229
48335
  ...property === undefined ? {} : { property },
48230
48336
  ...includeHidden === undefined ? {} : { include_hidden: includeHidden }
48231
- }, patience);
48337
+ }, { tool: "runtime_inspect", op: "find", patience });
48232
48338
  }
48233
48339
  async handleRuntimeWait(op, args) {
48234
48340
  if (op === "frames") {
48235
48341
  const frames = readPositiveNumber(args, "frames") ?? 1;
48236
48342
  const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
48237
- return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
48343
+ return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, { tool: "runtime_wait", op, patience: waited });
48238
48344
  }
48239
48345
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
48240
48346
  const asked = readNonEmptyString(args, "nodePath");
@@ -48248,7 +48354,7 @@ ${staleNote}`}`
48248
48354
  path: nodePath,
48249
48355
  signal: readString(args, "signal") ?? "",
48250
48356
  timeout_ms: timeoutMs
48251
- }, patience) : await this.handleRuntimeCommand("wait_until", {
48357
+ }, { tool: "runtime_wait", op, patience }) : await this.handleRuntimeCommand("wait_until", {
48252
48358
  ...whichGame(args),
48253
48359
  path: nodePath,
48254
48360
  property: readString(args, "property") ?? "",
@@ -48256,7 +48362,7 @@ ${staleNote}`}`
48256
48362
  ...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
48257
48363
  ...readBoolean(args, "includeHidden") === undefined ? {} : { include_hidden: readBoolean(args, "includeHidden") },
48258
48364
  timeout_ms: timeoutMs
48259
- }, patience);
48365
+ }, { tool: "runtime_wait", op, patience });
48260
48366
  }
48261
48367
  }
48262
48368
  async function runGodotServer() {
@@ -48264,7 +48370,7 @@ async function runGodotServer() {
48264
48370
  const server = new GodotServer;
48265
48371
  await server.run();
48266
48372
  }
48267
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, 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, 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. 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 "';
48373
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, 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, 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, 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, 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. 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 "';
48268
48374
  var init_server3 = __esm(() => {
48269
48375
  init_mcp();
48270
48376
  init_stdio2();
@@ -34,6 +34,14 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
34
34
 
35
35
 
36
36
  func _process(_delta: float) -> void:
37
+ _count_a_play_begun()
38
+
39
+
40
+ ## Counts a play the editor has begun since the last look, whoever began it. Looked at every frame
41
+ ## and before the count is read: read alone, a play begun in this frame went uncounted until this
42
+ ## node's turn came round, and a server asking before then was told the earlier play's number with
43
+ ## "playing", which it took for the earlier play still going.
44
+ func _count_a_play_begun() -> void:
37
45
  var playing: bool = EditorInterface.is_playing_scene()
38
46
  if playing and not _was_playing:
39
47
  if _awaited:
@@ -231,6 +239,7 @@ static func _leave() -> void:
231
239
 
232
240
 
233
241
  func playing_status(_args: Dictionary) -> Dictionary:
242
+ _count_a_play_begun()
234
243
  var playing: bool = EditorInterface.is_playing_scene()
235
244
  # The debugger's port with it, read now rather than remembered: it is taken again before every
236
245
  # play, so the one from when this editor greeted the server is a number it has moved off.
@@ -17,6 +17,11 @@ const Read = preload("../reading.gd")
17
17
  ## the server keeps this below the queue the project sets; this is that for the engine's default.
18
18
  const FRAME_FUNCTIONS: int = 1536
19
19
  const DEBUGGER_CLASS: String = "ScriptEditorDebugger"
20
+ ## What a quitting game sends after its profile's totals, and the answer it waits for. Kept in step
21
+ ## with the runtime addon's runtime_autoload.gd.
22
+ const PROFILE_CAPTURE: String = "gdharness"
23
+ const PROFILE_SENT: String = "gdharness:profile_sent"
24
+ const PROFILE_RECEIVED: String = "gdharness:profile_received"
20
25
 
21
26
 
22
27
  ## The editor's debugger sessions, numbered as the editor numbers its debugger tabs.
@@ -32,6 +37,17 @@ class Sessions:
32
37
  if joined != OK:
33
38
  push_error("gdharness could not watch debugger session %d start: %d" % [session_id, joined])
34
39
 
40
+ func _has_capture(capture: String) -> bool:
41
+ return capture == PROFILE_CAPTURE
42
+
43
+ ## Answers a quitting game that its profile's totals arrived, which it waits for before it exits.
44
+ ## The editor reads a session's messages in order, so the totals sent before this have been read.
45
+ func _capture(message: String, _data: Array, session_id: int) -> bool:
46
+ if message != PROFILE_SENT:
47
+ return false
48
+ get_session(session_id).send_message(PROFILE_RECEIVED, [])
49
+ return true
50
+
35
51
 
36
52
  var _editor_plugin: EditorPlugin = null
37
53
  var _sessions: Sessions = null
@@ -27,9 +27,21 @@ const BIND_ADDRESS_SETTING: String = "gdharness/runtime/bind_address"
27
27
  ## expects. A fixed port is for a client that cannot read the announcement.
28
28
  const PORT_SETTING: String = "gdharness/runtime/port"
29
29
 
30
- ## How long a profiled game waits as it leaves for its profile to be sent: the debugger's thread
31
- ## sends every 6.9 ms, so this is many of its turns.
32
- const PROFILE_SEND_MS: int = 100
30
+ ## The debugger messages that confirm a profile's totals arrived: the game sends the first after
31
+ ## them, and the debugger answers with the second once it has read that far. Kept in step with
32
+ ## `PROFILE_SENT` and `PROFILE_RECEIVED` in src/script-profile.ts and the editor addon's
33
+ ## profile_tools.gd.
34
+ const PROFILE_CAPTURE: String = "gdharness"
35
+ const PROFILE_SENT: String = "gdharness:profile_sent"
36
+ const PROFILE_RECEIVED: String = "gdharness:profile_received"
37
+
38
+ ## The longest a quitting game waits for that answer. A debugger that never gives one, such as an
39
+ ## editor without the gdharness addon, holds every profiled exit this long.
40
+ const PROFILE_CONFIRM_MS: int = 3000
41
+
42
+ ## How many calls of `EngineDebugger.line_poll` read the debugger's messages once: it reads them
43
+ ## on every 2048th call and only counts the others.
44
+ const POLLS_PER_READ: int = 2048
33
45
 
34
46
  ## How the engine is told to run a script instead of the game: `godot -s thing.gd`.
35
47
  const SCRIPT_FLAGS: PackedStringArray = ["-s", "--script"]
@@ -90,6 +102,7 @@ var _commands: Dictionary = {}
90
102
  ## Empty when the markers cannot be read, as in a copy made by hand.
91
103
  var _loaded_version: String = _read_marker(VERSION_MARKER)
92
104
  var _loaded_digest: String = _read_marker(DIGEST_MARKER)
105
+ var _profile_received: bool = false
93
106
 
94
107
 
95
108
  func _init() -> void:
@@ -125,12 +138,14 @@ func _ready() -> void:
125
138
  # even be un-paused over the socket. A debug server has to stay responsive while the game
126
139
  # is frozen, to inspect, capture, inject or resume it.
127
140
  process_mode = Node.PROCESS_MODE_ALWAYS
141
+ var watched: int = get_tree().root.tree_exiting.connect(_send_the_profile)
142
+ if watched != OK:
143
+ push_error("gdharness could not watch for the game quitting: %d" % watched)
128
144
  _report_errors()
129
145
  _start_server()
130
146
 
131
147
 
132
148
  func _exit_tree() -> void:
133
- _send_the_profile()
134
149
  if _errors != null:
135
150
  OS.remove_logger(_errors)
136
151
  _errors.close()
@@ -138,16 +153,51 @@ func _exit_tree() -> void:
138
153
  _cleanup()
139
154
 
140
155
 
141
- ## A profiled run's totals, sent as the game leaves the tree.
156
+ ## A profiled run's totals, sent as the game quits.
142
157
  ##
143
158
  ## The engine sends its script profiler's totals only when the profiler is switched off, and at
144
159
  ## exit it is torn down without being switched off, so a scene that did its work in _ready and
145
- ## quit took its whole profile with it, last frame included. The wait is for the debugger's own
146
- ## thread, which sends what is queued every few milliseconds and stops without emptying the queue.
160
+ ## quit took its whole profile with it, last frame included.
161
+ ##
162
+ ## The totals go out on the debugger's own thread, which stops without emptying its queue, so the
163
+ ## game waits until the debugger says it has read them. A fixed wait of 100 ms lost the totals of
164
+ ## games quitting under load, whose debugger thread did not get its turn in time.
165
+ ##
166
+ ## On the root leaving rather than this node: the root leaves only as the game quits, and after
167
+ ## every other node's _exit_tree, so what those do is counted. A project that sent this node away
168
+ ## mid-run had the profiler switched off then, and its run answered as complete with none of the
169
+ ## frames after it.
147
170
  func _send_the_profile() -> void:
148
- if EngineDebugger.is_active() and EngineDebugger.is_profiling("servers"):
149
- EngineDebugger.profiler_enable("servers", false)
150
- OS.delay_msec(PROFILE_SEND_MS)
171
+ if not (EngineDebugger.is_active() and EngineDebugger.is_profiling("servers")):
172
+ return
173
+ _profile_received = false
174
+ if not EngineDebugger.has_capture(PROFILE_CAPTURE):
175
+ EngineDebugger.register_message_capture(PROFILE_CAPTURE, _on_debugger_message)
176
+ EngineDebugger.profiler_enable("servers", false)
177
+ EngineDebugger.send_message(PROFILE_SENT, [])
178
+ var deadline: int = Time.get_ticks_msec() + PROFILE_CONFIRM_MS
179
+ while not _profile_received and Time.get_ticks_msec() < deadline:
180
+ for _poll: int in POLLS_PER_READ:
181
+ EngineDebugger.line_poll()
182
+ OS.delay_msec(5)
183
+ EngineDebugger.unregister_message_capture(PROFILE_CAPTURE)
184
+ if not _profile_received:
185
+ print(
186
+ (
187
+ (
188
+ "[gdharness] the debugger did not confirm the profiler's totals within %d ms, so the"
189
+ + " profile may be the frames that arrived without them"
190
+ )
191
+ % PROFILE_CONFIRM_MS
192
+ )
193
+ )
194
+
195
+
196
+ func _on_debugger_message(message: String, _data: Array) -> bool:
197
+ if PROFILE_CAPTURE + ":" + message == PROFILE_RECEIVED:
198
+ _profile_received = true
199
+ return true
200
+ return false
151
201
 
152
202
 
153
203
  ## The engine's error reports, written to a file for the server, for a game the editor plays, and
@@ -331,6 +331,7 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
331
331
  var looks: int = 0
332
332
  var looked_usec: int = 0
333
333
  var next_look: int = 0
334
+ var looked_at_frame: int = 0
334
335
  var found: bool = false
335
336
  while true:
336
337
  if Time.get_ticks_usec() >= next_look:
@@ -340,13 +341,19 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
340
341
  var took: int = Time.get_ticks_usec() - looking
341
342
  looked_usec += took
342
343
  next_look = Time.get_ticks_usec() + took * LOOKS_APART
344
+ looked_at_frame = frames
343
345
  if found or Time.get_ticks_msec() - started >= timeout_ms:
344
346
  break
345
347
  await _host.get_tree().process_frame
346
348
  frames += 1
347
349
  # One more look at the end, so words that arrived between two paced looks are not answered as
348
- # never having come.
350
+ # never having come. Taken once a whole frame has run since the last look, which is two frame
351
+ # starts: a first look that outlasted the timeout otherwise left none, so the wait answered with
352
+ # the game not having run at all, and a command sent during that look not yet read.
349
353
  if not found:
354
+ while frames - looked_at_frame < 2:
355
+ await _host.get_tree().process_frame
356
+ frames += 1
350
357
  var last: int = Time.get_ticks_usec()
351
358
  found = _anything_says(node_path, said, include_hidden)
352
359
  looks += 1
package/build/index.js CHANGED
@@ -25964,7 +25964,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
25964
25964
  ok: false,
25965
25965
  reason: "busy",
25966
25966
  requestId: id,
25967
- message: `${still} Nothing was cancelled: a call that takes longer than the wait goes on and does everything it was asked, so read the state back rather than repeating it. runtime_invoke op result with requestId ${id} answers with its reply once it comes, and timeoutMs waits longer next time. A game held at a breakpoint instead is one debug_state stack answers about.`
25967
+ message: `${still} Nothing was cancelled: a call that takes longer than the wait goes on and does everything it was asked, so read the state back rather than repeating it. runtime_invoke op result with requestId ${id} answers with its reply once it comes. A game held at a breakpoint instead is one debug_state stack answers about.`
25968
25968
  });
25969
25969
  }, timeoutMs);
25970
25970
  socket.setEncoding("utf8");
@@ -26552,6 +26552,67 @@ function enclosingFunction(source, line, code = codeOf(source)) {
26552
26552
  }
26553
26553
  return null;
26554
26554
  }
26555
+ var SUPER_CALL = /(?<![\p{L}\p{N}_.])super\s*[.(]/u;
26556
+ var LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
26557
+ function afterHeader(text, from) {
26558
+ let depth = 0;
26559
+ for (let at = text.indexOf("(", from);at !== -1 && at < text.length; at += 1) {
26560
+ if (text[at] === "(") {
26561
+ depth += 1;
26562
+ } else if (text[at] === ")") {
26563
+ depth -= 1;
26564
+ if (depth === 0) {
26565
+ const colon = text.indexOf(":", at);
26566
+ return colon === -1 ? -1 : colon + 1;
26567
+ }
26568
+ }
26569
+ }
26570
+ return -1;
26571
+ }
26572
+ function bodyCallsSuper(code, line, lambda) {
26573
+ const lines = code.split(`
26574
+ `);
26575
+ const first = lines[line - 1];
26576
+ if (first === undefined) {
26577
+ return false;
26578
+ }
26579
+ const header = lambda ? LAMBDA_HEADER.exec(first) : FUNC_LINE.exec(first);
26580
+ if (header !== null) {
26581
+ const body = afterHeader(first, header.index);
26582
+ if (body !== -1 && first.slice(body).trim() !== "") {
26583
+ return SUPER_CALL.test(first.slice(body));
26584
+ }
26585
+ }
26586
+ const indent = indentOf(first);
26587
+ let lambdaIndent = null;
26588
+ for (let index = line - 1;index < lines.length; index += 1) {
26589
+ const text = lines[index] ?? "";
26590
+ if (text.trim() === "") {
26591
+ continue;
26592
+ }
26593
+ const at = indentOf(text);
26594
+ if (at < indent) {
26595
+ break;
26596
+ }
26597
+ if (lambdaIndent !== null) {
26598
+ if (at > lambdaIndent) {
26599
+ continue;
26600
+ }
26601
+ lambdaIndent = null;
26602
+ }
26603
+ const inner = LAMBDA_HEADER.exec(text);
26604
+ if (SUPER_CALL.test(inner === null ? text : text.slice(0, inner.index))) {
26605
+ return true;
26606
+ }
26607
+ if (inner !== null) {
26608
+ const body = afterHeader(text, inner.index);
26609
+ if (body !== -1 && text.slice(body).trim() === "") {
26610
+ lambdaIndent = at;
26611
+ }
26612
+ }
26613
+ }
26614
+ return false;
26615
+ }
26555
26616
  function callSitesOf(source, name) {
26556
26617
  const regions = regionsOf(source);
26557
26618
  const code = codeOf(source, regions);
@@ -33123,25 +33184,36 @@ function signatureParts(signature) {
33123
33184
  }
33124
33185
  var ms = (seconds) => Math.round(seconds * 1e6) / 1000;
33125
33186
  function profiledFunctions(totals, limit, sourceOf = () => null) {
33126
- const rows = rowsOf(totals);
33187
+ const rows = rowsOf(totals, sourceOf);
33127
33188
  const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
33128
33189
  return {
33129
33190
  functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
33130
33191
  omitted: Math.max(0, rows.length - limit),
33131
33192
  scriptMs,
33132
- counted: rows.length
33193
+ counted: rows.length,
33194
+ superCallers: rows.filter((row) => row.selfIncludesSuper === true).length
33133
33195
  };
33134
33196
  }
33135
- function rowsOf(totals) {
33197
+ function rowsOf(totals, sourceOf) {
33198
+ const codes = new Map;
33199
+ const codeOfScript = (script) => {
33200
+ if (!codes.has(script)) {
33201
+ const source = sourceOf(script);
33202
+ codes.set(script, source === null ? null : codeOf(source));
33203
+ }
33204
+ return codes.get(script) ?? null;
33205
+ };
33136
33206
  return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
33137
33207
  const parts = signatureParts(signature);
33208
+ const code = codeOfScript(parts.script);
33138
33209
  return {
33139
33210
  script: parts.script,
33140
33211
  function: parts.function,
33141
33212
  line: parts.line,
33142
33213
  calls: sum.calls,
33143
33214
  selfMs: ms(sum.selfSeconds),
33144
- totalMs: ms(sum.totalSeconds)
33215
+ totalMs: ms(sum.totalSeconds),
33216
+ ...code !== null && bodyCallsSuper(code, parts.line, parts.function.endsWith("(lambda)")) ? { selfIncludesSuper: true } : {}
33145
33217
  };
33146
33218
  }).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
33147
33219
  }
@@ -33158,17 +33230,24 @@ function placedLambda(row, sourceOf) {
33158
33230
  }
33159
33231
  var CALL_SITES = 40;
33160
33232
  function callersOf(totals, name, scripts) {
33161
- const rows = rowsOf(totals);
33233
+ const listed = [...scripts];
33234
+ const sources = new Map(listed);
33235
+ const rows = rowsOf(totals, (script) => sources.get(script) ?? null);
33162
33236
  const bare = bareName(name);
33163
33237
  const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
33164
33238
  const sites = [];
33165
- for (const [script, source] of scripts) {
33239
+ for (const [script, source] of listed) {
33166
33240
  for (const site of callSitesOf(source, bare)) {
33167
33241
  const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
33168
33242
  sites.push({
33169
33243
  script,
33170
33244
  ...site,
33171
- ...within === undefined ? {} : { withinCalls: within.calls, withinSelfMs: within.selfMs, withinTotalMs: within.totalMs }
33245
+ ...within === undefined ? {} : {
33246
+ withinCalls: within.calls,
33247
+ withinSelfMs: within.selfMs,
33248
+ withinTotalMs: within.totalMs,
33249
+ ...within.selfIncludesSuper === true ? { withinSelfIncludesSuper: true } : {}
33250
+ }
33172
33251
  });
33173
33252
  }
33174
33253
  }
@@ -34199,7 +34278,7 @@ var TOOL_SPECS = [
34199
34278
  },
34200
34279
  wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
34201
34280
  profile: {
34202
- summary: "where the run spends its time by GDScript function, while it runs or after it ended: 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, sorted by self time, 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 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. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
34281
+ summary: "where the run spends its time by GDScript function, while it runs or after it ended: 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, sorted by self time, 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. 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. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
34203
34282
  requires: []
34204
34283
  }
34205
34284
  },
@@ -34828,10 +34907,15 @@ var FEEDBACK_NOTICE_EVERY = 250;
34828
34907
  var run4 = promisify5(execFile5);
34829
34908
  var __dirname2 = dirname15(fileURLToPath4(import.meta.url));
34830
34909
  function editorRestartTimeoutMs() {
34831
- const override = Number.parseInt(envValue("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS") ?? "", 10);
34832
- return Number.isInteger(override) && override > 0 ? override : 90000;
34910
+ return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
34911
+ }
34912
+ function waitFromEnv(variable, fallback) {
34913
+ const override = Number.parseInt(envValue(variable) ?? "", 10);
34914
+ return Number.isInteger(override) && override > 0 ? override : fallback;
34915
+ }
34916
+ function greetingWaitMs() {
34917
+ return waitFromEnv("GDHARNESS_GREETING_WAIT_MS", 1e4);
34833
34918
  }
34834
- var GREETING_WAIT_MS = 1e4;
34835
34919
  function restartUnderway(note, now = Date.now()) {
34836
34920
  return alive(note.byPid) && now - Date.parse(note.quitAt) < editorRestartTimeoutMs() + 30000;
34837
34921
  }
@@ -34883,6 +34967,14 @@ function captureDetail(payload) {
34883
34967
  function patienceForFrames(frames, atLeast) {
34884
34968
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
34885
34969
  }
34970
+ function longerWaitNote(tool, op, waitedMs) {
34971
+ const spec = TOOL_SPECS.find((one) => one.name === tool);
34972
+ if (spec !== undefined && argumentsOf(spec, op).includes("timeoutMs")) {
34973
+ return "timeoutMs waits longer next time.";
34974
+ }
34975
+ const how = tool === "runtime_wait" && op === "frames" ? `the wait is counted from the number of frames, at ${SLOWEST_FRAME_RATE} a second on top of GDHARNESS_RUNTIME_TIMEOUT_MS, and this game fell below that rate, so ask for fewer frames at a time` : "the wait is GDHARNESS_RUNTIME_TIMEOUT_MS, which is set in the environment the server is started with";
34976
+ return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
34977
+ }
34886
34978
  function projectSpelling(projectPath, absolutePath) {
34887
34979
  return `res://${relative4(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
34888
34980
  }
@@ -34963,10 +35055,12 @@ function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
34963
35055
  function noCodeWillCome(run) {
34964
35056
  return !exited(run);
34965
35057
  }
34966
- var STOP_WAIT_MS = 1e4;
35058
+ function stopWaitMs() {
35059
+ return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
35060
+ }
34967
35061
  var KEEPER_ANSWER_MS = 15000;
34968
35062
  var EXIT_REPORTED_WITHIN_MS = 2000;
34969
- async function untilGone(pids, withinMs = STOP_WAIT_MS) {
35063
+ async function untilGone(pids, withinMs = stopWaitMs()) {
34970
35064
  const gone = () => pids.every((pid) => !alive(pid));
34971
35065
  const deadline = Date.now() + withinMs;
34972
35066
  while (!gone() && Date.now() < deadline) {
@@ -35021,7 +35115,7 @@ function stopVerdict(stop) {
35021
35115
  return {
35022
35116
  stopped: false,
35023
35117
  stillGoing: stop.endedPid,
35024
- note: `${told} ${STOP_WAIT_MS / 1000} seconds later, so it has not stopped. editor_status says when it has gone, and editor_run stop asks again.`,
35118
+ note: `${told} ${stopWaitMs() / 1000} seconds later, so it has not stopped. editor_status says when it has gone, and editor_run stop asks again.`,
35025
35119
  withChildren: true
35026
35120
  };
35027
35121
  }
@@ -35140,7 +35234,7 @@ function endedToStartThis(ended) {
35140
35234
  return ` ${which} was not ended here: the editor could not be asked to stop it. The editor stops a play before starting the next, so it is not running beside this one if this start reached the editor.`;
35141
35235
  }
35142
35236
  if (ended.ending === "lingering") {
35143
- return ` ${which} was told to end to start this one and was still running ${STOP_WAIT_MS / 1000} seconds later, so it is running beside this one: editor_status says when it has gone.`;
35237
+ return ` ${which} was told to end to start this one and was still running ${stopWaitMs() / 1000} seconds later, so it is running beside this one: editor_status says when it has gone.`;
35144
35238
  }
35145
35239
  return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
35146
35240
  }
@@ -35272,7 +35366,9 @@ var BOUNDARY_WAIT_MS = 2000;
35272
35366
  var HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
35273
35367
  var HANDOVER_POLL_MS = 250;
35274
35368
  var CACHE_WRITE_MS = 2000;
35275
- var SCAN_WAIT_MS = 30000;
35369
+ function scanWaitMs() {
35370
+ return waitFromEnv("GDHARNESS_SCAN_WAIT_MS", 30000);
35371
+ }
35276
35372
  var STREAMS_GRACE_MS = 2000;
35277
35373
  function scanWaitIn(payload) {
35278
35374
  const waited = readNumber(payload, "waitedForEditorScanMs");
@@ -35332,7 +35428,7 @@ function scanWaitAnswer(scanned, started = "game") {
35332
35428
  if (scanned.waitedMs === 0) {
35333
35429
  return {};
35334
35430
  }
35335
- const still = `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so`;
35431
+ const still = `The editor was still scanning the project after ${scanWaitMs() / 1000} seconds, so`;
35336
35432
  return {
35337
35433
  waitedForEditorScanMs: scanned.waitedMs,
35338
35434
  scanNote: !scanned.stillScanning ? undefined : started === "game" ? `${still} this game started during the scan and may have read a class cache being rewritten. If ` + 'editor_output shows "Could not find type" for a class that is declared, editor_rescan ' + "waits for the scan to finish, and a start after it reads the finished cache." : `${still} the engine answering this started during the scan and may have read a class cache being ` + "rewritten, and a class cache it wrote can be written over when the scan finishes. If " + 'engine_messages shows "Could not find type" for a class that is declared, editor_rescan waits ' + "for the scan to finish, and the same call after it reads the finished cache."
@@ -35634,14 +35730,20 @@ function profileAnswer(reading, limit) {
35634
35730
  const table = profiledFunctions(totals, limit, reading.sourceOf);
35635
35731
  const notes = [];
35636
35732
  const addonNote = reading.runtimeAddon ? "" : " This project has no gdharness runtime addon, which switches the profiler off as the game quits so the engine sends its totals: setup installs it.";
35637
- if (totals.complete) {} else if (reading.running && totals.frames === 0) {
35733
+ const totalsWithheld = reading.runtimeAddon ? " The runtime addon switches the profiler off as the game quits, and did not here: either it was no longer running in this game by then, which a project can arrange for some scenes; or the game ended without taking its tree down, as a crash or a kill does; or the totals did not reach the debugger before the game exited, which the game then says in its output." : addonNote;
35734
+ if (totals.complete && reading.running) {
35735
+ notes.push(`The engine's totals arrived while the game is still running, which it sends only when the profiler is switched off, so ${reading.through === "editor" ? "the editor's Profiler tab or the game itself, through EngineDebugger," : "the game itself, through EngineDebugger,"} switched it off before the game ended. They cover the run up to then and nothing after.`);
35736
+ } else if (totals.complete) {} else if (reading.running && totals.frames === 0) {
35638
35737
  notes.push(`The profiler is on and the game has not finished its first frame yet: a frame is sent as it ends, so a scene doing its work in _ready shows nothing until that returns, and the engine's own totals arrive when the game quits.${addonNote}`);
35639
35738
  } else if (reading.running) {
35640
35739
  notes.push(`The run is still going, so these are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} summed so far${reading.through === "gdharness" ? ", written at most half a second behind" : ""}.`);
35641
35740
  } else if (totals.frames === 0) {
35642
- notes.push(`The game connected and ended before a frame of the profile reached ${reading.through === "editor" ? "the editor" : "this server"}: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${addonNote}`);
35741
+ notes.push(`The game connected and ended before a frame of the profile reached ${reading.through === "editor" ? "the editor" : "this server"}: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${totalsWithheld}`);
35643
35742
  } else {
35644
- notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals, so what the game did after the last of them is not in it: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${addonNote}`);
35743
+ 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.`);
35744
+ }
35745
+ if (table.superCallers > 0) {
35746
+ 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.`);
35645
35747
  }
35646
35748
  notes.push(...cappedNotes(totals));
35647
35749
  if (totals.unreadable > 0) {
@@ -36381,7 +36483,7 @@ class GodotServer {
36381
36483
  }
36382
36484
  async dispatch(tool, op, args) {
36383
36485
  if (tool !== "editor_status" && this.godotBridge.isArriving()) {
36384
- await this.waitForBridge(() => !this.godotBridge.isArriving(), Date.now() + GREETING_WAIT_MS);
36486
+ await this.waitForBridge(() => !this.godotBridge.isArriving(), Date.now() + greetingWaitMs());
36385
36487
  }
36386
36488
  if (ENGINE_PASSES[tool]?.[op] !== undefined) {
36387
36489
  return op === "reimport" ? await this.handleReimport(args) : await this.handleRefreshUids(args);
@@ -36541,7 +36643,7 @@ class GodotServer {
36541
36643
  depth: depth ?? 3,
36542
36644
  include_properties: readBoolean(args, "includeProperties") ?? false,
36543
36645
  properties: readArray(args, "properties") ?? []
36544
- }, patience);
36646
+ }, { tool: "runtime_inspect", op, patience });
36545
36647
  }
36546
36648
  case "find":
36547
36649
  return await this.handleFindRuntimeNodes(args, patience);
@@ -36551,17 +36653,17 @@ class GodotServer {
36551
36653
  root: readNonEmptyString(args, "nodePath") ?? "/root",
36552
36654
  include_hidden: readBoolean(args, "includeHidden") ?? false,
36553
36655
  limit: readPositiveNumber(args, "limit") ?? 500
36554
- }, patience);
36656
+ }, { tool: "runtime_inspect", op, patience });
36555
36657
  case "rect":
36556
- return await this.handleRuntimeCommand("get_rect", { ...whichGame(args), path: readNonEmptyString(args, "nodePath") ?? "" }, patience);
36658
+ return await this.handleRuntimeCommand("get_rect", { ...whichGame(args), path: readNonEmptyString(args, "nodePath") ?? "" }, { tool: "runtime_inspect", op, patience });
36557
36659
  case "property":
36558
36660
  return await this.handleRuntimeCommand("get_property", {
36559
36661
  ...whichGame(args),
36560
36662
  path: readNonEmptyString(args, "nodePath") ?? "",
36561
36663
  property: readNonEmptyString(args, "property") ?? ""
36562
- }, patience);
36664
+ }, { tool: "runtime_inspect", op, patience });
36563
36665
  default:
36564
- return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, patience);
36666
+ return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, { tool: "runtime_inspect", op, patience });
36565
36667
  }
36566
36668
  }
36567
36669
  case "runtime_invoke": {
@@ -36574,16 +36676,16 @@ class GodotServer {
36574
36676
  path: readNonEmptyString(args, "nodePath") ?? "",
36575
36677
  property: readString(args, "property") ?? "",
36576
36678
  value: args["value"]
36577
- }, patience) : await this.handleRuntimeCommand("call_method", {
36679
+ }, { tool: "runtime_invoke", op, patience }) : await this.handleRuntimeCommand("call_method", {
36578
36680
  ...whichGame(args),
36579
36681
  path: readNonEmptyString(args, "nodePath") ?? "",
36580
36682
  method: readString(args, "method") ?? "",
36581
36683
  args: readArray(args, "args") ?? [],
36582
36684
  ...args["properties"] === undefined ? {} : { properties: args["properties"] }
36583
- }, patience);
36685
+ }, { tool: "runtime_invoke", op, patience });
36584
36686
  }
36585
36687
  case "runtime_capture":
36586
- return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args);
36688
+ return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args, { tool: "runtime_capture", op });
36587
36689
  case "runtime_input":
36588
36690
  if (op === "click") {
36589
36691
  if (readNonEmptyString(args, "nodePath") === undefined && readNonEmptyString(args, "says") === undefined) {
@@ -36596,7 +36698,7 @@ class GodotServer {
36596
36698
  double: readBoolean(args, "doubleClick") ?? false,
36597
36699
  ...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
36598
36700
  ...args["index"] === undefined ? {} : { index: args["index"] }
36599
- });
36701
+ }, { tool: "runtime_input", op });
36600
36702
  }
36601
36703
  if (op === "choose") {
36602
36704
  return await this.handleRuntimeCommand("choose", {
@@ -36604,9 +36706,9 @@ class GodotServer {
36604
36706
  path: readNonEmptyString(args, "nodePath") ?? "",
36605
36707
  text: readString(args, "text") ?? "",
36606
36708
  ...args["index"] === undefined ? {} : { index: args["index"] }
36607
- });
36709
+ }, { tool: "runtime_input", op });
36608
36710
  }
36609
- return await this.handleRuntimeCommand(`inject_${op}`, args);
36711
+ return await this.handleRuntimeCommand(`inject_${op}`, args, { tool: "runtime_input", op });
36610
36712
  case "runtime_wait":
36611
36713
  return await this.handleRuntimeWait(op, args);
36612
36714
  case "debug_breakpoint": {
@@ -36835,7 +36937,7 @@ class GodotServer {
36835
36937
  const answered = Date.now();
36836
36938
  if (status["scanning"] === true || status["importing"] === true || status["pending"] === true) {
36837
36939
  sawAScan = true;
36838
- if (Date.now() - started >= SCAN_WAIT_MS) {
36940
+ if (Date.now() - started >= scanWaitMs()) {
36839
36941
  return { waitedMs: Date.now() - started, stillScanning: true };
36840
36942
  }
36841
36943
  await new Promise((settle) => setTimeout(settle, 100));
@@ -38182,7 +38284,7 @@ class GodotServer {
38182
38284
  ]);
38183
38285
  }
38184
38286
  if (!hasSaidWhoItIs(before)) {
38185
- return this.createErrorResponse(`The editor has connected and has not said who it is within ${GREETING_WAIT_MS / 1000}s, so whether this server or the editor itself restarts it cannot be told.`, ["editor_status says when it has, under greeting while it has not", "Then ask for the restart again"]);
38287
+ return this.createErrorResponse(`The editor has connected and has not said who it is within ${greetingWaitMs() / 1000}s, so whether this server or the editor itself restarts it cannot be told.`, ["editor_status says when it has, under greeting while it has not", "Then ask for the restart again"]);
38186
38288
  }
38187
38289
  const mine = before.openedByAServer === true && before.projectPath !== undefined;
38188
38290
  if (!mine && args["hidden"] === true) {
@@ -38797,7 +38899,7 @@ class GodotServer {
38797
38899
  game.brokeOn = halt.text;
38798
38900
  game.log.record("error", halt.text);
38799
38901
  }
38800
- async untilTheEditorStopsPlaying(withinMs = STOP_WAIT_MS) {
38902
+ async untilTheEditorStopsPlaying(withinMs = stopWaitMs()) {
38801
38903
  const deadline = Date.now() + withinMs;
38802
38904
  for (;; ) {
38803
38905
  if ((await this.editorPlayingState())?.playing === false) {
@@ -38891,7 +38993,7 @@ class GodotServer {
38891
38993
  running.endedThrough = "signal";
38892
38994
  }
38893
38995
  clearTheRecord();
38894
- running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${STOP_WAIT_MS / 1000} seconds later.` : landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
38996
+ running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${stopWaitMs() / 1000} seconds later.` : landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
38895
38997
  return gone ? "gone" : "lingering";
38896
38998
  }
38897
38999
  async endThroughTheKeeper(run, pid, keeper) {
@@ -38911,7 +39013,7 @@ class GodotServer {
38911
39013
  run.exitCode = null;
38912
39014
  run.exitSignal = "SIGTERM";
38913
39015
  }
38914
- run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${STOP_WAIT_MS / 1000} seconds later.` : asked === "signalled" ? `gdharness ended pid ${pid} through the keeper holding it.` : `gdharness asked the keeper holding pid ${pid} to end it, and it had already gone.`);
39016
+ run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${stopWaitMs() / 1000} seconds later.` : asked === "signalled" ? `gdharness ended pid ${pid} through the keeper holding it.` : `gdharness asked the keeper holding pid ${pid} to end it, and it had already gone.`);
38915
39017
  return gone ? "gone" : "lingering";
38916
39018
  }
38917
39019
  spawnGame(godotPath, cmdArgs, env) {
@@ -39836,7 +39938,7 @@ class GodotServer {
39836
39938
  ...landed ? { endedThrough: "signal" } : {},
39837
39939
  ...childrenFields(ended),
39838
39940
  exitedBeforeStop: !landed,
39839
- note: `${!gone ? `pid ${pid} was told to end and was still running ${STOP_WAIT_MS / 1000} seconds later, so it has not stopped: a game of this project that no server here was holding,` : landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" : " editor_status says when it has gone."} ${aboutTheChildren(ended, false)}`
39941
+ note: `${!gone ? `pid ${pid} was told to end and was still running ${stopWaitMs() / 1000} seconds later, so it has not stopped: a game of this project that no server here was holding,` : landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" : " editor_status says when it has gone."} ${aboutTheChildren(ended, false)}`
39840
39942
  });
39841
39943
  }
39842
39944
  async whatTheRunStarted(run) {
@@ -40572,7 +40674,8 @@ class GodotServer {
40572
40674
  }
40573
40675
  return `The game this server started (${which}) is still coming up and has not announced its runtime after a wait of ${Math.min(budgetMs, remaining)}ms, so there is nothing to talk to yet. It is still running: ask again in a moment rather than starting another.`;
40574
40676
  }
40575
- async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
40677
+ async handleRuntimeCommand(command, args, asked) {
40678
+ const timeoutMs = asked.patience ?? this.runtimeTimeoutMs();
40576
40679
  const { op: _op, projectPath, pid, outputPath, ...params } = asParams(args);
40577
40680
  const waited = typeof pid === "number" ? null : await this.ownRunOnItsWay(typeof projectPath === "string" ? projectPath : this.ownProject, timeoutMs);
40578
40681
  if (waited !== null) {
@@ -40625,7 +40728,7 @@ ${staleNote}`);
40625
40728
  requestId: reply.requestId,
40626
40729
  command,
40627
40730
  waitedMs: timeoutMs,
40628
- note: reply.message,
40731
+ note: `${reply.message} ${longerWaitNote(asked.tool, asked.op, timeoutMs)}`,
40629
40732
  ...answeredBy,
40630
40733
  ...stale
40631
40734
  });
@@ -40725,13 +40828,13 @@ ${staleNote}`}`
40725
40828
  limit: readPositiveNumber(args, "limit") ?? 100,
40726
40829
  ...property === undefined ? {} : { property },
40727
40830
  ...includeHidden === undefined ? {} : { include_hidden: includeHidden }
40728
- }, patience);
40831
+ }, { tool: "runtime_inspect", op: "find", patience });
40729
40832
  }
40730
40833
  async handleRuntimeWait(op, args) {
40731
40834
  if (op === "frames") {
40732
40835
  const frames = readPositiveNumber(args, "frames") ?? 1;
40733
40836
  const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
40734
- return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
40837
+ return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, { tool: "runtime_wait", op, patience: waited });
40735
40838
  }
40736
40839
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
40737
40840
  const asked = readNonEmptyString(args, "nodePath");
@@ -40745,7 +40848,7 @@ ${staleNote}`}`
40745
40848
  path: nodePath,
40746
40849
  signal: readString(args, "signal") ?? "",
40747
40850
  timeout_ms: timeoutMs
40748
- }, patience) : await this.handleRuntimeCommand("wait_until", {
40851
+ }, { tool: "runtime_wait", op, patience }) : await this.handleRuntimeCommand("wait_until", {
40749
40852
  ...whichGame(args),
40750
40853
  path: nodePath,
40751
40854
  property: readString(args, "property") ?? "",
@@ -40753,7 +40856,7 @@ ${staleNote}`}`
40753
40856
  ...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
40754
40857
  ...readBoolean(args, "includeHidden") === undefined ? {} : { include_hidden: readBoolean(args, "includeHidden") },
40755
40858
  timeout_ms: timeoutMs
40756
- }, patience);
40859
+ }, { tool: "runtime_wait", op, patience });
40757
40860
  }
40758
40861
  }
40759
40862
  async function runGodotServer() {
package/build/keeper.js CHANGED
@@ -550,6 +550,7 @@ var DECLARATION_LINE = new RegExp(`^(${ANNOTATIONS})(static\\s+)?(func|var|const
550
550
  var CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
551
551
  var EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
552
552
  var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
553
+ var LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
553
554
  var KEYWORDS = new Set([
554
555
  "and",
555
556
  "as",
@@ -7767,6 +7768,8 @@ function queuedMessagesOf(projectGodot) {
7767
7768
  const set = projectGodot === null ? undefined : parseProjectGodot(projectGodot)["network"]?.[QUEUE_SETTING];
7768
7769
  return typeof set === "number" && Number.isInteger(set) && set > 0 ? set : QUEUE_DEFAULT;
7769
7770
  }
7771
+ var PROFILE_SENT = "gdharness:profile_sent";
7772
+ var PROFILE_RECEIVED = "gdharness:profile_received";
7770
7773
  var UNNAMED_PREFIX = "#";
7771
7774
  function profilerOn(frameFunctions) {
7772
7775
  return framedCommand("profiler:servers", [true, [frameFunctions, false]]);
@@ -7931,6 +7934,9 @@ class ProfileListener {
7931
7934
  if ("message" in received && received.message === "debug_enter") {
7932
7935
  socket.write(framedCommand("continue", []));
7933
7936
  }
7937
+ if ("message" in received && received.message === PROFILE_SENT) {
7938
+ socket.write(framedCommand(PROFILE_RECEIVED, []));
7939
+ }
7934
7940
  aggregate.take(received, Date.now());
7935
7941
  frames ||= "message" in received && (received.message === "servers:profile_frame" || received.message === "servers:profile_total");
7936
7942
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.59",
3
+ "version": "1.1.60",
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",