gdharness 1.1.57 → 1.1.58

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
@@ -9155,6 +9155,44 @@ ${line.text}`;
9155
9155
  const { body } = parseBody(null, null, 0, 0, 0);
9156
9156
  return { className, body };
9157
9157
  }
9158
+ function indentOf(text) {
9159
+ return /^[ \t]*/.exec(text)?.[0].length ?? 0;
9160
+ }
9161
+ function enclosingFunction(source, line, code = codeOf(source)) {
9162
+ const lines = code.split(`
9163
+ `);
9164
+ const at = lines[line - 1];
9165
+ if (at === undefined) {
9166
+ return null;
9167
+ }
9168
+ const own = FUNC_LINE.exec(at);
9169
+ if (own?.[1] !== undefined) {
9170
+ return own[1];
9171
+ }
9172
+ let indent = indentOf(at);
9173
+ for (let index = line - 2;index >= 0 && indent > 0; index -= 1) {
9174
+ const text = lines[index] ?? "";
9175
+ if (text.trim() === "" || indentOf(text) >= indent) {
9176
+ continue;
9177
+ }
9178
+ indent = indentOf(text);
9179
+ const declared = FUNC_LINE.exec(text);
9180
+ if (declared?.[1] !== undefined) {
9181
+ return declared[1];
9182
+ }
9183
+ }
9184
+ return null;
9185
+ }
9186
+ function callSitesOf(source, name) {
9187
+ const regions = regionsOf(source);
9188
+ const code = codeOf(source, regions);
9189
+ const lines = new Lines(source);
9190
+ return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).map((one) => {
9191
+ const line = lines.positionOf(one.offset).line + 1;
9192
+ const after = code.slice(one.offset + name.length).trimStart();
9193
+ return { line, within: enclosingFunction(source, line, code), called: after.startsWith("(") };
9194
+ });
9195
+ }
9158
9196
  function collectEnumValues(text, from, into) {
9159
9197
  const close = text.indexOf("}");
9160
9198
  const inside = text.slice(1, close === -1 ? text.length : close);
@@ -9170,7 +9208,7 @@ function collectEnumValues(text, from, into) {
9170
9208
  function isIdentifier(name) {
9171
9209
  return new RegExp(`^${NAME}$`, "u").test(name) && !KEYWORDS.has(name);
9172
9210
  }
9173
- 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, KEYWORDS;
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;
9174
9212
  var init_gdscript_source = __esm(() => {
9175
9213
  IDENTIFIER_START = /[\p{L}_]/u;
9176
9214
  IDENTIFIER_PART = /[\p{L}\p{N}_]/u;
@@ -9179,6 +9217,7 @@ var init_gdscript_source = __esm(() => {
9179
9217
  CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
9180
9218
  EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
9181
9219
  EXTENDS_CLAUSE = /\bextends(?=\s)/u;
9220
+ FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
9182
9221
  KEYWORDS = new Set([
9183
9222
  "and",
9184
9223
  "as",
@@ -18138,7 +18177,12 @@ var init_tool_definitions = __esm(() => {
18138
18177
  profile: {
18139
18178
  type: "boolean",
18140
18179
  ops: ["start"],
18141
- description: "start: profile the run's GDScript with the engine's own script profiler from its start, for editor_run profile to answer which functions took the time. A run this server starts dials the process that keeps it as its debugger, on loopback, with breakpoints and error breaks skipped so it never stops to wait; a game the editor plays is profiled through the editor's debugger session. Needs an engine built with debugging, which the editor binary is and a release export template is not. Default false."
18180
+ description: "start: profile the run's GDScript with the engine's own script profiler from its start, for editor_run profile to answer which functions took the time. A run this server starts dials the process that keeps it as its debugger, on loopback, with breakpoints and error breaks skipped so it never stops to wait; a game the editor plays is profiled through the editor's debugger session. Needs an engine built with debugging, which the editor binary is and a release export template is not. The start answers profile.on and, under profile.totals, how the numbers will arrive. Default false."
18181
+ },
18182
+ callersOf: {
18183
+ type: "string",
18184
+ ops: ["profile"],
18185
+ description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too and a call made through a string is not."
18142
18186
  },
18143
18187
  limit: {
18144
18188
  type: "integer",
@@ -18174,7 +18218,7 @@ var init_tool_definitions = __esm(() => {
18174
18218
  },
18175
18219
  wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
18176
18220
  profile: {
18177
- 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, 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. 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",
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",
18178
18222
  requires: []
18179
18223
  }
18180
18224
  },
@@ -42270,8 +42314,18 @@ function signatureParts(signature) {
42270
42314
  const line = Number(parts.at(-2));
42271
42315
  return { script: parts.slice(0, -2).join("::"), line: Number.isFinite(line) ? line : 0, function: name };
42272
42316
  }
42273
- function profiledFunctions(totals, limit) {
42274
- const rows = Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
42317
+ function profiledFunctions(totals, limit, sourceOf = () => null) {
42318
+ const rows = rowsOf(totals);
42319
+ const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
42320
+ return {
42321
+ functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
42322
+ omitted: Math.max(0, rows.length - limit),
42323
+ scriptMs,
42324
+ counted: rows.length
42325
+ };
42326
+ }
42327
+ function rowsOf(totals) {
42328
+ return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
42275
42329
  const parts = signatureParts(signature);
42276
42330
  return {
42277
42331
  script: parts.script,
@@ -42282,16 +42336,39 @@ function profiledFunctions(totals, limit) {
42282
42336
  totalMs: ms(sum.totalSeconds)
42283
42337
  };
42284
42338
  }).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
42285
- const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
42286
- return {
42287
- functions: rows.slice(0, limit),
42288
- omitted: Math.max(0, rows.length - limit),
42289
- scriptMs,
42290
- counted: rows.length
42291
- };
42292
42339
  }
42293
- var ms = (seconds) => Math.round(seconds * 1e6) / 1000;
42340
+ function bareName(fn) {
42341
+ return fn.slice(fn.lastIndexOf(".") + 1);
42342
+ }
42343
+ function placedLambda(row, sourceOf) {
42344
+ if (!row.function.endsWith("(lambda)")) {
42345
+ return row;
42346
+ }
42347
+ const source = sourceOf(row.script);
42348
+ const within = source === null ? null : enclosingFunction(source, row.line);
42349
+ return within === null ? row : { ...row, within };
42350
+ }
42351
+ function callersOf(totals, name, scripts) {
42352
+ const rows = rowsOf(totals);
42353
+ const bare = bareName(name);
42354
+ const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
42355
+ const sites = [];
42356
+ for (const [script, source] of scripts) {
42357
+ for (const site of callSitesOf(source, bare)) {
42358
+ const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
42359
+ sites.push({
42360
+ script,
42361
+ ...site,
42362
+ ...within === undefined ? {} : { withinCalls: within.calls, withinSelfMs: within.selfMs, withinTotalMs: within.totalMs }
42363
+ });
42364
+ }
42365
+ }
42366
+ sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
42367
+ return { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
42368
+ }
42369
+ var ms = (seconds) => Math.round(seconds * 1e6) / 1000, CALL_SITES = 40;
42294
42370
  var init_script_profile = __esm(() => {
42371
+ init_gdscript_source();
42295
42372
  init_godot_variant();
42296
42373
  });
42297
42374
 
@@ -42425,13 +42502,19 @@ function playOf(answer) {
42425
42502
  const editorPid = readNumber(answer, "editorPid");
42426
42503
  return play === undefined || editorPid === undefined ? null : { play, editorPid };
42427
42504
  }
42428
- function runIsUp(run, editorSays, now = Date.now()) {
42505
+ function playedSince(run, playing) {
42506
+ return run.throughEditor && run.play !== undefined && playing !== null && (playing.play !== run.play.play || playing.editorPid !== run.play.editorPid);
42507
+ }
42508
+ function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
42429
42509
  if (run?.throughEditor !== true) {
42430
42510
  return stillRunning(run);
42431
42511
  }
42432
42512
  if (typeof run.endedHere === "string") {
42433
42513
  return false;
42434
42514
  }
42515
+ if (playedSince(run, editorPlay)) {
42516
+ return false;
42517
+ }
42435
42518
  if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
42436
42519
  return false;
42437
42520
  }
@@ -42975,6 +43058,43 @@ function aboveTheRunner(entry) {
42975
43058
  ]
42976
43059
  };
42977
43060
  }
43061
+ function runtimeAddonRegistered(projectPath) {
43062
+ const registered = inspectProject(projectPath);
43063
+ return registered.runtimeAutoloadPath !== null || registered.runtimeLoaderAutoload !== null;
43064
+ }
43065
+ function profileOnStart(profiled, through, runtimeAddon) {
43066
+ if (!profiled) {
43067
+ return;
43068
+ }
43069
+ const debuggerOf = through === "editor" ? "the editor's debugger session" : "the process that keeps the run";
43070
+ return {
43071
+ on: true,
43072
+ totals: `On from the game's start, through ${debuggerOf}. editor_run profile answers at any time: each frame is added as it ends, so a scene still inside a long _ready shows nothing until that returns, and the engine's own totals replace the sum when the game quits` + (runtimeAddon ? "." : ", which takes the runtime addon: without it the frames that arrived before the exit are what is kept.")
43073
+ };
43074
+ }
43075
+ function projectSourceOf(projectPath) {
43076
+ const read = new Map;
43077
+ return (script) => {
43078
+ if (!read.has(script)) {
43079
+ const inside = resolveWithinProject(projectPath, script);
43080
+ let source = null;
43081
+ if (inside.ok) {
43082
+ try {
43083
+ source = readFileSync23(inside.absolutePath, "utf8");
43084
+ } catch {}
43085
+ }
43086
+ read.set(script, source);
43087
+ }
43088
+ return read.get(script) ?? null;
43089
+ };
43090
+ }
43091
+ function projectScripts(projectPath) {
43092
+ const scripts = [];
43093
+ eachScript(projectPath, (script, source) => {
43094
+ scripts.push([script, source]);
43095
+ });
43096
+ return scripts;
43097
+ }
42978
43098
  function profileAnswer(reading, limit) {
42979
43099
  const { totals } = reading;
42980
43100
  if (totals === null) {
@@ -42986,10 +43106,12 @@ function profileAnswer(reading, limit) {
42986
43106
  note: reading.running ? "The game has not connected to the profiler yet: it dials its debugger once, as it starts, before any script runs. Ask again in a moment." : "The game never connected to the profiler, so nothing was measured. A game dials its debugger once, as it starts, and an engine built without debugging (a release export template) refuses to: profiling needs the editor binary or a debug template."
42987
43107
  };
42988
43108
  }
42989
- const table = profiledFunctions(totals, limit);
43109
+ const table = profiledFunctions(totals, limit, reading.sourceOf);
42990
43110
  const notes = [];
42991
43111
  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.";
42992
- if (totals.complete) {} else if (reading.running) {
43112
+ if (totals.complete) {} else if (reading.running && totals.frames === 0) {
43113
+ 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}`);
43114
+ } else if (reading.running) {
42993
43115
  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" : ""}.`);
42994
43116
  } else if (totals.frames === 0) {
42995
43117
  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}`);
@@ -43010,9 +43132,20 @@ function profileAnswer(reading, limit) {
43010
43132
  functionsCounted: table.counted,
43011
43133
  omitted: table.omitted > 0 ? table.omitted : undefined,
43012
43134
  functions: table.functions,
43135
+ callers: reading.callersOf === undefined ? undefined : callersAnswer(totals, reading.callersOf),
43013
43136
  note: notes.length > 0 ? notes.join(" ") : undefined
43014
43137
  };
43015
43138
  }
43139
+ function callersAnswer(totals, asked) {
43140
+ const found = callersOf(totals, asked.name, asked.scripts());
43141
+ return {
43142
+ of: asked.name,
43143
+ rows: found.target,
43144
+ sites: found.sites,
43145
+ omitted: found.omitted > 0 ? found.omitted : undefined,
43146
+ note: found.sites.length === 0 ? `No script in the project names ${asked.name} in code, outside its declaration: a call made through a string, such as call("name"), is not found by name.` : found.target.length === 0 ? `${CALLERS_FOUND_BY_NAME} The profile counts no call of ${asked.name}, so these places are where it would be called from.` : CALLERS_FOUND_BY_NAME
43147
+ };
43148
+ }
43016
43149
  function foldedEntriesAnswer(log) {
43017
43150
  const problems = log.selectFolded({
43018
43151
  severity: "warning",
@@ -43586,7 +43719,7 @@ class GodotServer {
43586
43719
  return this.saying(answer, {
43587
43720
  update_available: {
43588
43721
  ...notice,
43589
- what_to_do: "Tell the user a newer gdharness is out, with what changed. Run the upgrade command " + "only with their agreement: a yes now, or a standing instruction of theirs, such as " + "their CLAUDE.md, to upgrade on every release. Without either, offer it and wait, since " + "it replaces the addons in their project. Afterwards the MCP server has to be " + "reconnected and the editor restarted with editor_launch restart."
43722
+ what_to_do: "Tell the user a newer gdharness is out, with what changed. Run the upgrade command " + "only with their agreement: a yes now, or a standing instruction of theirs, such as one " + "in their AGENTS.md or CLAUDE.md, to upgrade on every release. Without either, offer it " + "and wait, since it replaces the addons in their project. Afterwards the MCP server has " + "to be reconnected and the editor restarted with editor_launch restart."
43590
43723
  }
43591
43724
  });
43592
43725
  }
@@ -45822,6 +45955,7 @@ class GodotServer {
45822
45955
  pid: started.pid,
45823
45956
  arguments: cmdArgs,
45824
45957
  transcript: started.transcript,
45958
+ profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
45825
45959
  refreshedClasses: refreshed.value,
45826
45960
  ...scanWaitAnswer(scanned),
45827
45961
  savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
@@ -45961,6 +46095,7 @@ class GodotServer {
45961
46095
  const transcript = openTranscript(startedAt);
45962
46096
  closeSync2(transcript.fd);
45963
46097
  const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
46098
+ const which = playOf(playAnswer);
45964
46099
  const played = {
45965
46100
  pid: null,
45966
46101
  log,
@@ -45973,11 +46108,11 @@ class GodotServer {
45973
46108
  throughEditor: true,
45974
46109
  brokeOn: null,
45975
46110
  announcedBefore: alreadyPlaying,
45976
- seenPlaying: readBoolean(playAnswer, "playing") === true
46111
+ seenPlaying: readBoolean(playAnswer, "playing") === true,
46112
+ ...which === null ? {} : { play: which }
45977
46113
  };
45978
46114
  this.activeProcess = played;
45979
46115
  this.watchThePlayedConsole();
45980
- const which = playOf(playAnswer);
45981
46116
  writeEditorRunNote({
45982
46117
  projectPath,
45983
46118
  transcript: transcript.path,
@@ -45989,6 +46124,7 @@ class GodotServer {
45989
46124
  started: true,
45990
46125
  through: "editor",
45991
46126
  scene: scene === null ? "the main scene" : `res://${scene}`,
46127
+ profile: profileOnStart(profiled, "editor", runtimeAddonRegistered(projectPath)),
45992
46128
  refreshedClasses,
45993
46129
  ...scanWaitAnswer(scanned),
45994
46130
  debugPort: readNumber(playAnswer, "debugPort"),
@@ -46371,9 +46507,14 @@ class GodotServer {
46371
46507
  return;
46372
46508
  }
46373
46509
  const going = this.activeProcess;
46374
- const givesWay = going !== null && !stillRunning(going) && typeof going.endedHere === "string";
46510
+ const asked = going?.throughEditor === true && going.play !== undefined ? await this.editorPlayingState() : null;
46511
+ const superseded = going !== null && asked !== null && playedSince(going, asked.play);
46512
+ if (superseded && stillRunning(going)) {
46513
+ going.endedUnwatched = true;
46514
+ }
46515
+ const givesWay = going !== null && (superseded || !stillRunning(going) && typeof going.endedHere === "string");
46375
46516
  if (going !== null && !givesWay) {
46376
- if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
46517
+ if (going.throughEditor && going.consoleLost === true && (asked ?? await this.editorPlayingState())?.playing) {
46377
46518
  try {
46378
46519
  await this.dap().connect();
46379
46520
  going.log.append("stdout", `[gdharness] the debug adapter was reconnected here
@@ -46384,7 +46525,7 @@ class GodotServer {
46384
46525
  }
46385
46526
  return;
46386
46527
  }
46387
- const playing = await this.editorPlayingState();
46528
+ const playing = asked ?? await this.editorPlayingState();
46388
46529
  if (playing?.playing !== true) {
46389
46530
  return;
46390
46531
  }
@@ -46407,7 +46548,8 @@ class GodotServer {
46407
46548
  exitSignal: null,
46408
46549
  throughEditor: true,
46409
46550
  brokeOn: null,
46410
- pickedUpPlaying: true
46551
+ pickedUpPlaying: true,
46552
+ ...playing.play === null ? {} : { play: playing.play }
46411
46553
  };
46412
46554
  this.activeProcess = picked;
46413
46555
  this.watchThePlayedConsole();
@@ -46711,11 +46853,19 @@ class GodotServer {
46711
46853
  return this.createErrorResponse(this.nothingOfOursIsRunning());
46712
46854
  }
46713
46855
  const limit = readPositiveNumber(args, "limit") ?? PROFILE_FUNCTIONS;
46856
+ const callersName = readString(args, "callersOf");
46857
+ if (callersName !== undefined && !callersName.split(".").every(isIdentifier)) {
46858
+ return this.createErrorResponse(`callersOf takes a function's name, or Class.name for the functions of one class, and ${JSON.stringify(callersName)} is neither.`, ["editor_run profile names each function as it answers; pass one of those"]);
46859
+ }
46714
46860
  const going = await this.runStillGoing(run);
46715
- const registered = run.projectPath === null ? null : inspectProject(run.projectPath);
46716
- const runtimeAddon = registered !== null && (registered.runtimeAutoloadPath !== null || registered.runtimeLoaderAutoload !== null);
46861
+ const projectPath = run.projectPath;
46862
+ const fromProject = projectPath === null ? { runtimeAddon: false } : {
46863
+ runtimeAddon: runtimeAddonRegistered(projectPath),
46864
+ sourceOf: projectSourceOf(projectPath),
46865
+ ...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
46866
+ };
46717
46867
  if (run.throughEditor) {
46718
- return await this.profileOfThePlayedGame(going, runtimeAddon, limit);
46868
+ return await this.profileOfThePlayedGame(going, fromProject, limit);
46719
46869
  }
46720
46870
  const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
46721
46871
  const path = record !== null && record.pid === run.pid ? record.profile : undefined;
@@ -46734,10 +46884,10 @@ class GodotServer {
46734
46884
  running: going,
46735
46885
  totals,
46736
46886
  coveredMs: totals?.lastFrameAt == null ? 0 : totals.lastFrameAt - totals.startedAt,
46737
- runtimeAddon
46887
+ ...fromProject
46738
46888
  }, limit));
46739
46889
  }
46740
- async profileOfThePlayedGame(going, runtimeAddon, limit) {
46890
+ async profileOfThePlayedGame(going, fromProject, limit) {
46741
46891
  const read = await this.handleViaBridge("profile_read", {});
46742
46892
  if (read.isError === true) {
46743
46893
  return this.profilerRefusal(read);
@@ -46789,7 +46939,7 @@ class GodotServer {
46789
46939
  running: going,
46790
46940
  totals,
46791
46941
  coveredMs: readNonNegativeNumber(held, "coveredMs") ?? 0,
46792
- runtimeAddon
46942
+ ...fromProject
46793
46943
  }, limit));
46794
46944
  }
46795
46945
  async firstProfiledFrame() {
@@ -46848,11 +46998,13 @@ class GodotServer {
46848
46998
  if (announced !== undefined && editorSays === undefined) {
46849
46999
  return stillRunning(run);
46850
47000
  }
46851
- const said = editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays;
46852
- if (said === true) {
47001
+ const state = editorSays === undefined ? await this.editorPlayingState() : undefined;
47002
+ const said = state === undefined ? editorSays ?? null : state?.playing ?? null;
47003
+ const play = state?.play ?? null;
47004
+ if (said === true && !playedSince(run, play)) {
46853
47005
  run.seenPlaying = true;
46854
47006
  }
46855
- return runIsUp(run, said);
47007
+ return runIsUp(run, said, play);
46856
47008
  }
46857
47009
  async untilTheExitIsRecorded(run) {
46858
47010
  const until = Date.now() + EXIT_REPORTED_WITHIN_MS;
@@ -48058,7 +48210,7 @@ async function runGodotServer() {
48058
48210
  const server = new GodotServer;
48059
48211
  await server.run();
48060
48212
  }
48061
- 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, MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
48213
+ 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 "';
48062
48214
  var init_server3 = __esm(() => {
48063
48215
  init_mcp();
48064
48216
  init_stdio2();
@@ -48079,6 +48231,7 @@ var init_server3 = __esm(() => {
48079
48231
  init_engine_run();
48080
48232
  init_errors();
48081
48233
  init_game_log();
48234
+ init_gdscript_source();
48082
48235
  init_godot_bridge();
48083
48236
  init_godot_path();
48084
48237
  init_headless();
@@ -49189,7 +49342,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
49189
49342
  | What every label on a screen says | \`runtime_inspect tree\` with \`properties\` naming \`text\`, which reads it off every node that has one; \`includeProperties\` is every stored property of every node and ran to seventy-six thousand characters for one row |
49190
49343
  | Whether this engine has a method or property, and what it takes | \`editor_classes info\` with \`member\`, which answers that one member and which class declares it rather than the whole class |
49191
49344
  | Wait for a run to finish | \`editor_run wait\`, never a sleep or a shell loop on the pid: it answers as \`editor_output\` does, once the run is over |
49192
- | Find which GDScript makes a run slow | \`editor_run start\` with \`profile\` set, then \`editor_run profile\`: the engine's own script profiler, per function with calls and self and total milliseconds, by self time, never \`Time.get_ticks_usec()\` laps edited into the game. A scene that does its work in \`_ready\` and quits is covered whole when the runtime autoload is registered, and a game the editor is already playing is profiled from the moment it is asked |
49345
+ | Find which GDScript makes a run slow | \`editor_run start\` with \`profile\` set, then \`editor_run profile\`: the engine's own script profiler, per function with calls and self and total milliseconds, by self time, never \`Time.get_ticks_usec()\` laps edited into the game. A scene that does its work in \`_ready\` and quits is covered whole when the runtime autoload is registered, and a game the editor is already playing is profiled from the moment it is asked. \`callersOf\` names where the project's scripts call one function, each place with its function's numbers, since the engine records no callers |
49193
49346
  | End a bench and the workers it opened | \`editor_run stop\` with \`andChildren\`, never a process listing by scene name: it ends every process under the run's, to any depth, that is a game of the project, announced as one or the project's engine run with \`--path\` on it, names them under \`endedChildren\` with what identified each, and leaves and names any other |
49194
49347
  | End a game of the project that no server is holding | \`editor_run stop\` with its \`pid\` from \`editor_status\` \`runtimes\`, never a process kill by hand: it is ended only when it announced itself for the project and its command line is the project's engine run |
49195
49348
  | Play a game without touching the player's saves | \`editor_run start\` with \`savesIn\`, a directory for this run's \`user://\`: the editor plays with its own environment, so a run given one is started by the server, and the answer says under \`savesNote\` when the platform ignores it |
@@ -127,7 +127,10 @@ func stop_playing(_args: Dictionary) -> Dictionary:
127
127
  var was_playing: bool = EditorInterface.is_playing_scene()
128
128
  if was_playing:
129
129
  EditorInterface.stop_playing_scene()
130
- return {"ok": true, "wasPlaying": was_playing, "playing": EditorInterface.is_playing_scene()}
130
+ # Known over now rather than at the next frame's look: a play somebody else starts before that
131
+ # look would otherwise be read as this one still going, and given its number and its console.
132
+ _was_playing = EditorInterface.is_playing_scene()
133
+ return {"ok": true, "wasPlaying": was_playing, "playing": _was_playing}
131
134
 
132
135
 
133
136
  ## Restarts the editor, which is how a replaced addon is picked up.
package/build/index.js CHANGED
@@ -26523,6 +26523,45 @@ ${line.text}`;
26523
26523
  const { body } = parseBody(null, null, 0, 0, 0);
26524
26524
  return { className, body };
26525
26525
  }
26526
+ var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
26527
+ function indentOf(text) {
26528
+ return /^[ \t]*/.exec(text)?.[0].length ?? 0;
26529
+ }
26530
+ function enclosingFunction(source, line, code = codeOf(source)) {
26531
+ const lines = code.split(`
26532
+ `);
26533
+ const at = lines[line - 1];
26534
+ if (at === undefined) {
26535
+ return null;
26536
+ }
26537
+ const own = FUNC_LINE.exec(at);
26538
+ if (own?.[1] !== undefined) {
26539
+ return own[1];
26540
+ }
26541
+ let indent = indentOf(at);
26542
+ for (let index = line - 2;index >= 0 && indent > 0; index -= 1) {
26543
+ const text = lines[index] ?? "";
26544
+ if (text.trim() === "" || indentOf(text) >= indent) {
26545
+ continue;
26546
+ }
26547
+ indent = indentOf(text);
26548
+ const declared = FUNC_LINE.exec(text);
26549
+ if (declared?.[1] !== undefined) {
26550
+ return declared[1];
26551
+ }
26552
+ }
26553
+ return null;
26554
+ }
26555
+ function callSitesOf(source, name) {
26556
+ const regions = regionsOf(source);
26557
+ const code = codeOf(source, regions);
26558
+ const lines = new Lines(source);
26559
+ return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).map((one) => {
26560
+ const line = lines.positionOf(one.offset).line + 1;
26561
+ const after = code.slice(one.offset + name.length).trimStart();
26562
+ return { line, within: enclosingFunction(source, line, code), called: after.startsWith("(") };
26563
+ });
26564
+ }
26526
26565
  function collectEnumValues(text, from, into) {
26527
26566
  const close = text.indexOf("}");
26528
26567
  const inside = text.slice(1, close === -1 ? text.length : close);
@@ -33072,8 +33111,18 @@ function signatureParts(signature) {
33072
33111
  return { script: parts.slice(0, -2).join("::"), line: Number.isFinite(line) ? line : 0, function: name };
33073
33112
  }
33074
33113
  var ms = (seconds) => Math.round(seconds * 1e6) / 1000;
33075
- function profiledFunctions(totals, limit) {
33076
- const rows = Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
33114
+ function profiledFunctions(totals, limit, sourceOf = () => null) {
33115
+ const rows = rowsOf(totals);
33116
+ const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
33117
+ return {
33118
+ functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
33119
+ omitted: Math.max(0, rows.length - limit),
33120
+ scriptMs,
33121
+ counted: rows.length
33122
+ };
33123
+ }
33124
+ function rowsOf(totals) {
33125
+ return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
33077
33126
  const parts = signatureParts(signature);
33078
33127
  return {
33079
33128
  script: parts.script,
@@ -33084,13 +33133,36 @@ function profiledFunctions(totals, limit) {
33084
33133
  totalMs: ms(sum.totalSeconds)
33085
33134
  };
33086
33135
  }).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
33087
- const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
33088
- return {
33089
- functions: rows.slice(0, limit),
33090
- omitted: Math.max(0, rows.length - limit),
33091
- scriptMs,
33092
- counted: rows.length
33093
- };
33136
+ }
33137
+ function bareName(fn) {
33138
+ return fn.slice(fn.lastIndexOf(".") + 1);
33139
+ }
33140
+ function placedLambda(row, sourceOf) {
33141
+ if (!row.function.endsWith("(lambda)")) {
33142
+ return row;
33143
+ }
33144
+ const source = sourceOf(row.script);
33145
+ const within = source === null ? null : enclosingFunction(source, row.line);
33146
+ return within === null ? row : { ...row, within };
33147
+ }
33148
+ var CALL_SITES = 40;
33149
+ function callersOf(totals, name, scripts) {
33150
+ const rows = rowsOf(totals);
33151
+ const bare = bareName(name);
33152
+ const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
33153
+ const sites = [];
33154
+ for (const [script, source] of scripts) {
33155
+ for (const site of callSitesOf(source, bare)) {
33156
+ const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
33157
+ sites.push({
33158
+ script,
33159
+ ...site,
33160
+ ...within === undefined ? {} : { withinCalls: within.calls, withinSelfMs: within.selfMs, withinTotalMs: within.totalMs }
33161
+ });
33162
+ }
33163
+ }
33164
+ sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
33165
+ return { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
33094
33166
  }
33095
33167
 
33096
33168
  // src/setup.ts
@@ -34075,7 +34147,12 @@ var TOOL_SPECS = [
34075
34147
  profile: {
34076
34148
  type: "boolean",
34077
34149
  ops: ["start"],
34078
- description: "start: profile the run's GDScript with the engine's own script profiler from its start, for editor_run profile to answer which functions took the time. A run this server starts dials the process that keeps it as its debugger, on loopback, with breakpoints and error breaks skipped so it never stops to wait; a game the editor plays is profiled through the editor's debugger session. Needs an engine built with debugging, which the editor binary is and a release export template is not. Default false."
34150
+ description: "start: profile the run's GDScript with the engine's own script profiler from its start, for editor_run profile to answer which functions took the time. A run this server starts dials the process that keeps it as its debugger, on loopback, with breakpoints and error breaks skipped so it never stops to wait; a game the editor plays is profiled through the editor's debugger session. Needs an engine built with debugging, which the editor binary is and a release export template is not. The start answers profile.on and, under profile.totals, how the numbers will arrive. Default false."
34151
+ },
34152
+ callersOf: {
34153
+ type: "string",
34154
+ ops: ["profile"],
34155
+ description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too and a call made through a string is not."
34079
34156
  },
34080
34157
  limit: {
34081
34158
  type: "integer",
@@ -34111,7 +34188,7 @@ var TOOL_SPECS = [
34111
34188
  },
34112
34189
  wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
34113
34190
  profile: {
34114
- 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, 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. 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",
34191
+ 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",
34115
34192
  requires: []
34116
34193
  }
34117
34194
  },
@@ -34848,13 +34925,19 @@ function playOf(answer) {
34848
34925
  const editorPid = readNumber(answer, "editorPid");
34849
34926
  return play === undefined || editorPid === undefined ? null : { play, editorPid };
34850
34927
  }
34851
- function runIsUp(run, editorSays, now = Date.now()) {
34928
+ function playedSince(run, playing) {
34929
+ return run.throughEditor && run.play !== undefined && playing !== null && (playing.play !== run.play.play || playing.editorPid !== run.play.editorPid);
34930
+ }
34931
+ function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
34852
34932
  if (run?.throughEditor !== true) {
34853
34933
  return stillRunning(run);
34854
34934
  }
34855
34935
  if (typeof run.endedHere === "string") {
34856
34936
  return false;
34857
34937
  }
34938
+ if (playedSince(run, editorPlay)) {
34939
+ return false;
34940
+ }
34858
34941
  if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
34859
34942
  return false;
34860
34943
  }
@@ -35464,7 +35547,45 @@ function aboveTheRunner(entry) {
35464
35547
  };
35465
35548
  }
35466
35549
  var PROFILE_FUNCTIONS = 30;
35550
+ function runtimeAddonRegistered(projectPath) {
35551
+ const registered = inspectProject(projectPath);
35552
+ return registered.runtimeAutoloadPath !== null || registered.runtimeLoaderAutoload !== null;
35553
+ }
35554
+ function profileOnStart(profiled, through, runtimeAddon) {
35555
+ if (!profiled) {
35556
+ return;
35557
+ }
35558
+ const debuggerOf = through === "editor" ? "the editor's debugger session" : "the process that keeps the run";
35559
+ return {
35560
+ on: true,
35561
+ totals: `On from the game's start, through ${debuggerOf}. editor_run profile answers at any time: each frame is added as it ends, so a scene still inside a long _ready shows nothing until that returns, and the engine's own totals replace the sum when the game quits` + (runtimeAddon ? "." : ", which takes the runtime addon: without it the frames that arrived before the exit are what is kept.")
35562
+ };
35563
+ }
35467
35564
  var PROFILER_ON_MS = 3000;
35565
+ var 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.";
35566
+ function projectSourceOf(projectPath) {
35567
+ const read = new Map;
35568
+ return (script) => {
35569
+ if (!read.has(script)) {
35570
+ const inside = resolveWithinProject(projectPath, script);
35571
+ let source = null;
35572
+ if (inside.ok) {
35573
+ try {
35574
+ source = readFileSync21(inside.absolutePath, "utf8");
35575
+ } catch {}
35576
+ }
35577
+ read.set(script, source);
35578
+ }
35579
+ return read.get(script) ?? null;
35580
+ };
35581
+ }
35582
+ function projectScripts(projectPath) {
35583
+ const scripts = [];
35584
+ eachScript(projectPath, (script, source) => {
35585
+ scripts.push([script, source]);
35586
+ });
35587
+ return scripts;
35588
+ }
35468
35589
  function profileAnswer(reading, limit) {
35469
35590
  const { totals } = reading;
35470
35591
  if (totals === null) {
@@ -35476,10 +35597,12 @@ function profileAnswer(reading, limit) {
35476
35597
  note: reading.running ? "The game has not connected to the profiler yet: it dials its debugger once, as it starts, before any script runs. Ask again in a moment." : "The game never connected to the profiler, so nothing was measured. A game dials its debugger once, as it starts, and an engine built without debugging (a release export template) refuses to: profiling needs the editor binary or a debug template."
35477
35598
  };
35478
35599
  }
35479
- const table = profiledFunctions(totals, limit);
35600
+ const table = profiledFunctions(totals, limit, reading.sourceOf);
35480
35601
  const notes = [];
35481
35602
  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.";
35482
- if (totals.complete) {} else if (reading.running) {
35603
+ if (totals.complete) {} else if (reading.running && totals.frames === 0) {
35604
+ 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}`);
35605
+ } else if (reading.running) {
35483
35606
  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" : ""}.`);
35484
35607
  } else if (totals.frames === 0) {
35485
35608
  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}`);
@@ -35500,9 +35623,20 @@ function profileAnswer(reading, limit) {
35500
35623
  functionsCounted: table.counted,
35501
35624
  omitted: table.omitted > 0 ? table.omitted : undefined,
35502
35625
  functions: table.functions,
35626
+ callers: reading.callersOf === undefined ? undefined : callersAnswer(totals, reading.callersOf),
35503
35627
  note: notes.length > 0 ? notes.join(" ") : undefined
35504
35628
  };
35505
35629
  }
35630
+ function callersAnswer(totals, asked) {
35631
+ const found = callersOf(totals, asked.name, asked.scripts());
35632
+ return {
35633
+ of: asked.name,
35634
+ rows: found.target,
35635
+ sites: found.sites,
35636
+ omitted: found.omitted > 0 ? found.omitted : undefined,
35637
+ note: found.sites.length === 0 ? `No script in the project names ${asked.name} in code, outside its declaration: a call made through a string, such as call("name"), is not found by name.` : found.target.length === 0 ? `${CALLERS_FOUND_BY_NAME} The profile counts no call of ${asked.name}, so these places are where it would be called from.` : CALLERS_FOUND_BY_NAME
35638
+ };
35639
+ }
35506
35640
  var MOST_ENGINE_ENTRIES = 200;
35507
35641
  function foldedEntriesAnswer(log) {
35508
35642
  const problems = log.selectFolded({
@@ -36079,7 +36213,7 @@ class GodotServer {
36079
36213
  return this.saying(answer, {
36080
36214
  update_available: {
36081
36215
  ...notice,
36082
- what_to_do: "Tell the user a newer gdharness is out, with what changed. Run the upgrade command " + "only with their agreement: a yes now, or a standing instruction of theirs, such as " + "their CLAUDE.md, to upgrade on every release. Without either, offer it and wait, since " + "it replaces the addons in their project. Afterwards the MCP server has to be " + "reconnected and the editor restarted with editor_launch restart."
36216
+ what_to_do: "Tell the user a newer gdharness is out, with what changed. Run the upgrade command " + "only with their agreement: a yes now, or a standing instruction of theirs, such as one " + "in their AGENTS.md or CLAUDE.md, to upgrade on every release. Without either, offer it " + "and wait, since it replaces the addons in their project. Afterwards the MCP server has " + "to be reconnected and the editor restarted with editor_launch restart."
36083
36217
  }
36084
36218
  });
36085
36219
  }
@@ -38315,6 +38449,7 @@ class GodotServer {
38315
38449
  pid: started.pid,
38316
38450
  arguments: cmdArgs,
38317
38451
  transcript: started.transcript,
38452
+ profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
38318
38453
  refreshedClasses: refreshed.value,
38319
38454
  ...scanWaitAnswer(scanned),
38320
38455
  savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
@@ -38454,6 +38589,7 @@ class GodotServer {
38454
38589
  const transcript = openTranscript(startedAt);
38455
38590
  closeSync2(transcript.fd);
38456
38591
  const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
38592
+ const which = playOf(playAnswer);
38457
38593
  const played = {
38458
38594
  pid: null,
38459
38595
  log,
@@ -38466,11 +38602,11 @@ class GodotServer {
38466
38602
  throughEditor: true,
38467
38603
  brokeOn: null,
38468
38604
  announcedBefore: alreadyPlaying,
38469
- seenPlaying: readBoolean(playAnswer, "playing") === true
38605
+ seenPlaying: readBoolean(playAnswer, "playing") === true,
38606
+ ...which === null ? {} : { play: which }
38470
38607
  };
38471
38608
  this.activeProcess = played;
38472
38609
  this.watchThePlayedConsole();
38473
- const which = playOf(playAnswer);
38474
38610
  writeEditorRunNote({
38475
38611
  projectPath,
38476
38612
  transcript: transcript.path,
@@ -38482,6 +38618,7 @@ class GodotServer {
38482
38618
  started: true,
38483
38619
  through: "editor",
38484
38620
  scene: scene === null ? "the main scene" : `res://${scene}`,
38621
+ profile: profileOnStart(profiled, "editor", runtimeAddonRegistered(projectPath)),
38485
38622
  refreshedClasses,
38486
38623
  ...scanWaitAnswer(scanned),
38487
38624
  debugPort: readNumber(playAnswer, "debugPort"),
@@ -38864,9 +39001,14 @@ class GodotServer {
38864
39001
  return;
38865
39002
  }
38866
39003
  const going = this.activeProcess;
38867
- const givesWay = going !== null && !stillRunning(going) && typeof going.endedHere === "string";
39004
+ const asked = going?.throughEditor === true && going.play !== undefined ? await this.editorPlayingState() : null;
39005
+ const superseded = going !== null && asked !== null && playedSince(going, asked.play);
39006
+ if (superseded && stillRunning(going)) {
39007
+ going.endedUnwatched = true;
39008
+ }
39009
+ const givesWay = going !== null && (superseded || !stillRunning(going) && typeof going.endedHere === "string");
38868
39010
  if (going !== null && !givesWay) {
38869
- if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
39011
+ if (going.throughEditor && going.consoleLost === true && (asked ?? await this.editorPlayingState())?.playing) {
38870
39012
  try {
38871
39013
  await this.dap().connect();
38872
39014
  going.log.append("stdout", `[gdharness] the debug adapter was reconnected here
@@ -38877,7 +39019,7 @@ class GodotServer {
38877
39019
  }
38878
39020
  return;
38879
39021
  }
38880
- const playing = await this.editorPlayingState();
39022
+ const playing = asked ?? await this.editorPlayingState();
38881
39023
  if (playing?.playing !== true) {
38882
39024
  return;
38883
39025
  }
@@ -38900,7 +39042,8 @@ class GodotServer {
38900
39042
  exitSignal: null,
38901
39043
  throughEditor: true,
38902
39044
  brokeOn: null,
38903
- pickedUpPlaying: true
39045
+ pickedUpPlaying: true,
39046
+ ...playing.play === null ? {} : { play: playing.play }
38904
39047
  };
38905
39048
  this.activeProcess = picked;
38906
39049
  this.watchThePlayedConsole();
@@ -39204,11 +39347,19 @@ class GodotServer {
39204
39347
  return this.createErrorResponse(this.nothingOfOursIsRunning());
39205
39348
  }
39206
39349
  const limit = readPositiveNumber(args, "limit") ?? PROFILE_FUNCTIONS;
39350
+ const callersName = readString(args, "callersOf");
39351
+ if (callersName !== undefined && !callersName.split(".").every(isIdentifier)) {
39352
+ return this.createErrorResponse(`callersOf takes a function's name, or Class.name for the functions of one class, and ${JSON.stringify(callersName)} is neither.`, ["editor_run profile names each function as it answers; pass one of those"]);
39353
+ }
39207
39354
  const going = await this.runStillGoing(run);
39208
- const registered = run.projectPath === null ? null : inspectProject(run.projectPath);
39209
- const runtimeAddon = registered !== null && (registered.runtimeAutoloadPath !== null || registered.runtimeLoaderAutoload !== null);
39355
+ const projectPath = run.projectPath;
39356
+ const fromProject = projectPath === null ? { runtimeAddon: false } : {
39357
+ runtimeAddon: runtimeAddonRegistered(projectPath),
39358
+ sourceOf: projectSourceOf(projectPath),
39359
+ ...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
39360
+ };
39210
39361
  if (run.throughEditor) {
39211
- return await this.profileOfThePlayedGame(going, runtimeAddon, limit);
39362
+ return await this.profileOfThePlayedGame(going, fromProject, limit);
39212
39363
  }
39213
39364
  const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
39214
39365
  const path = record !== null && record.pid === run.pid ? record.profile : undefined;
@@ -39227,10 +39378,10 @@ class GodotServer {
39227
39378
  running: going,
39228
39379
  totals,
39229
39380
  coveredMs: totals?.lastFrameAt == null ? 0 : totals.lastFrameAt - totals.startedAt,
39230
- runtimeAddon
39381
+ ...fromProject
39231
39382
  }, limit));
39232
39383
  }
39233
- async profileOfThePlayedGame(going, runtimeAddon, limit) {
39384
+ async profileOfThePlayedGame(going, fromProject, limit) {
39234
39385
  const read = await this.handleViaBridge("profile_read", {});
39235
39386
  if (read.isError === true) {
39236
39387
  return this.profilerRefusal(read);
@@ -39282,7 +39433,7 @@ class GodotServer {
39282
39433
  running: going,
39283
39434
  totals,
39284
39435
  coveredMs: readNonNegativeNumber(held, "coveredMs") ?? 0,
39285
- runtimeAddon
39436
+ ...fromProject
39286
39437
  }, limit));
39287
39438
  }
39288
39439
  async firstProfiledFrame() {
@@ -39341,11 +39492,13 @@ class GodotServer {
39341
39492
  if (announced !== undefined && editorSays === undefined) {
39342
39493
  return stillRunning(run);
39343
39494
  }
39344
- const said = editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays;
39345
- if (said === true) {
39495
+ const state = editorSays === undefined ? await this.editorPlayingState() : undefined;
39496
+ const said = state === undefined ? editorSays ?? null : state?.playing ?? null;
39497
+ const play = state?.play ?? null;
39498
+ if (said === true && !playedSince(run, play)) {
39346
39499
  run.seenPlaying = true;
39347
39500
  }
39348
- return runIsUp(run, said);
39501
+ return runIsUp(run, said, play);
39349
39502
  }
39350
39503
  async untilTheExitIsRecorded(run) {
39351
39504
  const until = Date.now() + EXIT_REPORTED_WITHIN_MS;
package/build/keeper.js CHANGED
@@ -542,6 +542,60 @@ function lastTryOnExit() {
542
542
  import { readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
543
543
  import { createServer as createServer2 } from "node:net";
544
544
 
545
+ // src/gdscript-source.ts
546
+ var DECLARING = new Set(["var", "const", "func", "signal", "enum", "class", "for", "class_name"]);
547
+ var NAME = "[\\p{L}_][\\p{L}\\p{N}_]*";
548
+ var ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*";
549
+ var DECLARATION_LINE = new RegExp(`^(${ANNOTATIONS})(static\\s+)?(func|var|const|signal|enum|class)\\s+(${NAME})`, "u");
550
+ var CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
551
+ var EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
552
+ var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
553
+ var KEYWORDS = new Set([
554
+ "and",
555
+ "as",
556
+ "assert",
557
+ "await",
558
+ "break",
559
+ "breakpoint",
560
+ "class",
561
+ "class_name",
562
+ "const",
563
+ "continue",
564
+ "elif",
565
+ "else",
566
+ "enum",
567
+ "extends",
568
+ "false",
569
+ "for",
570
+ "func",
571
+ "if",
572
+ "in",
573
+ "is",
574
+ "match",
575
+ "namespace",
576
+ "not",
577
+ "null",
578
+ "or",
579
+ "pass",
580
+ "preload",
581
+ "return",
582
+ "self",
583
+ "signal",
584
+ "static",
585
+ "super",
586
+ "trait",
587
+ "true",
588
+ "var",
589
+ "void",
590
+ "when",
591
+ "while",
592
+ "yield",
593
+ "INF",
594
+ "NAN",
595
+ "PI",
596
+ "TAU"
597
+ ]);
598
+
545
599
  // src/godot-variant.ts
546
600
  var NIL = 0;
547
601
  var BOOL = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.57",
3
+ "version": "1.1.58",
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",