gdharness 1.2.9 → 1.2.11

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
@@ -9358,8 +9358,31 @@ function callSitesOf(source, name, otherClasses = new Set, property = false) {
9358
9358
  const declared = bodyHolding(shape.body, at).declarations.find((one) => one.name === member && (one.kind === "var" || one.kind === "const"));
9359
9359
  return declared === undefined ? null : declaredClass(codeLines[lines.positionOf(declared.offset).line] ?? "", member);
9360
9360
  };
9361
+ const blockUnder = (declared) => {
9362
+ const indent = (codeLines[declared] ?? "").search(/\S|$/);
9363
+ let last = declared;
9364
+ for (let index = declared + 1;index < codeLines.length; index += 1) {
9365
+ const text = codeLines[index] ?? "";
9366
+ if (text.trim() === "") {
9367
+ continue;
9368
+ }
9369
+ if (text.search(/\S/) <= indent) {
9370
+ break;
9371
+ }
9372
+ last = index;
9373
+ }
9374
+ return [declared, last];
9375
+ };
9376
+ const accessorsIn = (body) => [
9377
+ ...body.declarations.filter((one) => one.kind === "var" && one.name === name).map((one) => blockUnder(lines.positionOf(one.offset).line)),
9378
+ ...body.inner.flatMap(accessorsIn)
9379
+ ];
9380
+ const accessors = property ? accessorsIn(shape.body) : [];
9361
9381
  return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).filter((one) => {
9362
9382
  const index = lines.positionOf(one.offset).line;
9383
+ if (!one.afterDot && accessors.some(([declared, last]) => index > declared && index <= last)) {
9384
+ return false;
9385
+ }
9363
9386
  if (one.afterDot) {
9364
9387
  const receiver = receiverBefore(code, one.offset);
9365
9388
  if (receiver === null || otherClasses.size === 0) {
@@ -9381,7 +9404,8 @@ function callSitesOf(source, name, otherClasses = new Set, property = false) {
9381
9404
  }).map((one) => {
9382
9405
  const line = lines.positionOf(one.offset).line + 1;
9383
9406
  const after = code.slice(one.offset + name.length).trimStart();
9384
- return { line, within: enclosingFunction(source, line, code), called: after.startsWith("(") };
9407
+ const within = enclosingFunction(source, line, code);
9408
+ return property ? { line, within, access: ASSIGNED.test(after) ? "write" : "read" } : { line, within, called: after.startsWith("(") };
9385
9409
  });
9386
9410
  }
9387
9411
  function collectEnumValues(text, from, into) {
@@ -9399,7 +9423,7 @@ function collectEnumValues(text, from, into) {
9399
9423
  function isIdentifier(name) {
9400
9424
  return new RegExp(`^${NAME}$`, "u").test(name) && !KEYWORDS.has(name);
9401
9425
  }
9402
- 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, DOTTED, MEMBER_KINDS, KEYWORDS;
9426
+ 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, ASSIGNED, DOTTED, MEMBER_KINDS, KEYWORDS;
9403
9427
  var init_gdscript_source = __esm(() => {
9404
9428
  IDENTIFIER_START = /[\p{L}_]/u;
9405
9429
  IDENTIFIER_PART = /[\p{L}\p{N}_]/u;
@@ -9411,6 +9435,7 @@ var init_gdscript_source = __esm(() => {
9411
9435
  FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
9412
9436
  SUPER_CALL = /(?<![\p{L}\p{N}_.])super\s*[.(]/u;
9413
9437
  LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
9438
+ ASSIGNED = /^(?:\*\*|<<|>>|[-+*/%&|^])?=(?!=)/;
9414
9439
  DOTTED = `${NAME}(?:\\.${NAME})*`;
9415
9440
  MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
9416
9441
  KEYWORDS = new Set([
@@ -18355,7 +18380,7 @@ var init_tool_definitions = __esm(() => {
18355
18380
  type: "integer",
18356
18381
  minimum: 0,
18357
18382
  ops: ["start"],
18358
- description: "start: how long to wait for the game to announce its runtime before answering. Default 5000, or half as long again as the last boot of this project took when that is more, up to 60000: the server notes how long each game took to announce, so a project that boots slowly is waited for from its second start on without being asked to. What it waits for is the first frame, so everything the game does before drawing one is inside it, including work the args just asked for: a flag that simulates six years of game time before anything is drawn makes the announcement that late, on a project that announces promptly without it. Raise it for such a run rather than reading runtime listening false as a fault. 0 does not wait at all, which is the one to pass for a scene that announces nothing by construction, such as a bench that prints and quits."
18383
+ description: "start: how long to wait for the game to announce its runtime before answering. Default 5000, or half as long again as the slowest of the last five boots of the same scene when that is more, up to 60000: the server notes how long each game took to announce, per scene and for the main scene when none is named, since a boot moves with the load on the machine, so a scene that boots slowly is waited for from its second start on without being asked to, and a harness scene that announces late sets nothing for the main scene. What it waits for is the first frame, so everything the game does before drawing one is inside it, including work the args just asked for: a flag that simulates six years of game time before anything is drawn makes the announcement that late, on a project that announces promptly without it. Raise it for such a run rather than reading runtime listening false as a fault. 0 does not wait at all, which is the one to pass for a scene that announces nothing by construction, such as a bench that prints and quits."
18359
18384
  },
18360
18385
  frames: {
18361
18386
  type: "integer",
@@ -18377,7 +18402,7 @@ var init_tool_definitions = __esm(() => {
18377
18402
  callersOf: {
18378
18403
  type: "string",
18379
18404
  ops: ["profile"],
18380
- 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, except, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed. A property's get or set block, which the profile names Class.@name_getter or Class.@name_setter, is asked about by that name or by the property's: the places are where the property is named, and the rows are both blocks'."
18405
+ 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, except, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed. A property's get or set block, which the profile names Class.@name_getter or Class.@name_setter, is asked about by that name or by the property's: the places are where the property is named, each saying instead of called whether it writes the property (access write), which runs the set block, or reads it (access read), which runs the get block, and the rows are both blocks'. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not places."
18381
18406
  },
18382
18407
  limit: {
18383
18408
  type: "integer",
@@ -18844,7 +18869,7 @@ var init_tool_definitions = __esm(() => {
18844
18869
  requires: [],
18845
18870
  operations: {
18846
18871
  click: {
18847
- summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room`,
18872
+ summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room`,
18848
18873
  requires: []
18849
18874
  },
18850
18875
  choose: {
@@ -34419,19 +34444,33 @@ import { dirname as dirname9, join as join14 } from "node:path";
34419
34444
  function bootNotePath(projectPath) {
34420
34445
  return join14(projectPath, ".godot", "gdharness-boot.json");
34421
34446
  }
34422
- function readBootNote(projectPath) {
34447
+ function readScenes(projectPath) {
34423
34448
  try {
34424
- const noted = readNumber(asParams(JSON.parse(readFileSync12(bootNotePath(projectPath), "utf8"))), "announcedAfterMs");
34425
- return noted !== undefined && Number.isFinite(noted) && noted > 0 ? noted : null;
34449
+ return asParams(asParams(JSON.parse(readFileSync12(bootNotePath(projectPath), "utf8")))["scenes"]);
34426
34450
  } catch {
34427
- return null;
34451
+ return {};
34428
34452
  }
34429
34453
  }
34430
- function writeBootNote(projectPath, announcedAfterMs) {
34454
+ function bootsOf(projectPath, scene) {
34455
+ const kept = asParams(readScenes(projectPath)[scene])["boots"];
34456
+ return Array.isArray(kept) ? kept.filter((one) => typeof one === "number" && Number.isFinite(one) && one > 0) : [];
34457
+ }
34458
+ function readBootNote(projectPath, scene) {
34459
+ const boots = bootsOf(projectPath, scene);
34460
+ return boots.length === 0 ? null : Math.max(...boots);
34461
+ }
34462
+ function writeBootNote(projectPath, scene, announcedAfterMs) {
34431
34463
  try {
34432
34464
  const path = bootNotePath(projectPath);
34433
34465
  mkdirSync6(dirname9(path), { recursive: true });
34434
- writeFileSync8(path, JSON.stringify({ announcedAfterMs: Math.round(announcedAfterMs), at: new Date().toISOString() }, null, 2), "utf8");
34466
+ const scenes = {
34467
+ ...readScenes(projectPath),
34468
+ [scene]: {
34469
+ boots: [...bootsOf(projectPath, scene), Math.round(announcedAfterMs)].slice(-BOOTS_KEPT),
34470
+ at: new Date().toISOString()
34471
+ }
34472
+ };
34473
+ writeFileSync8(path, JSON.stringify({ scenes }, null, 2), "utf8");
34435
34474
  } catch {}
34436
34475
  }
34437
34476
  function halfAsLongAgain(lastBootMs) {
@@ -34443,7 +34482,7 @@ function waitSizedTo(lastBootMs, usual = ANNOUNCE_BUDGET_MS) {
34443
34482
  }
34444
34483
  return Math.min(LONGEST_SIZED_WAIT_MS, Math.max(usual, halfAsLongAgain(lastBootMs)));
34445
34484
  }
34446
- var LONGEST_SIZED_WAIT_MS = 60000;
34485
+ var MAIN_SCENE = "", BOOTS_KEPT = 5, LONGEST_SIZED_WAIT_MS = 60000;
34447
34486
  var init_boot_note = __esm(() => {
34448
34487
  init_runtime_client();
34449
34488
  });
@@ -42695,7 +42734,8 @@ function classNamesOf(source) {
42695
42734
  function callersOf(totals, name, scripts) {
42696
42735
  const listed = [...scripts];
42697
42736
  const sources = new Map(listed);
42698
- const rows = rowsOf(totals, (script) => sources.get(script) ?? null);
42737
+ const perCallUs = perCallCostOf(totals);
42738
+ const rows = rowsOf(totals, (script) => sources.get(script) ?? null).map((row) => withOverhead(row, perCallUs));
42699
42739
  const asked = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : null;
42700
42740
  const bare = propertyOf(name) ?? bareName(name);
42701
42741
  const ownerOf = (fn) => fn.includes(".") ? fn.slice(0, fn.lastIndexOf(".")) : null;
@@ -42728,7 +42768,12 @@ function callersOf(totals, name, scripts) {
42728
42768
  }
42729
42769
  }
42730
42770
  sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
42731
- return { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
42771
+ return {
42772
+ target,
42773
+ sites: sites.slice(0, CALL_SITES),
42774
+ omitted: Math.max(0, sites.length - CALL_SITES),
42775
+ property
42776
+ };
42732
42777
  }
42733
42778
  var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler", CALL_SITES = 40;
42734
42779
  var init_script_profile = __esm(() => {
@@ -43108,7 +43153,7 @@ function runtimeVerdict(endpoint, after) {
43108
43153
  };
43109
43154
  }
43110
43155
  const state = after.playingSeen === false ? "the editor has not yet reported the game as playing, which a restarted editor does not until its scan is over" : "the game is still running";
43111
- const sized = after.sizedToMs === undefined ? "" : halfAsLongAgain(after.sizedToMs) > after.budgetMs ? `, the longest a start waits unasked though its last game took ${after.sizedToMs}ms to announce,` : `, half as long again as the ${after.sizedToMs}ms its last game took,`;
43156
+ const sized = after.sizedToMs === undefined ? "" : halfAsLongAgain(after.sizedToMs) > after.budgetMs ? `, the longest a start waits unasked though the slowest of its last boots took ${after.sizedToMs}ms to announce,` : `, half as long again as the ${after.sizedToMs}ms the slowest of its last boots took,`;
43112
43157
  return {
43113
43158
  listening: false,
43114
43159
  mayYetAnnounce: after.running,
@@ -43583,10 +43628,12 @@ function overheadRowsReturned(table, one) {
43583
43628
  if (shown === table.overheadDominated) {
43584
43629
  return one ? " Its row carries that share as overheadMs." : " Their rows carry that share as overheadMs.";
43585
43630
  }
43631
+ const theReturned = returned === 1 ? "the one row returned" : `the ${returned} rows returned`;
43586
43632
  if (shown === 0) {
43587
- return one ? ` Its row, which carries that share as overheadMs, is not among the ${returned} returned; limit ${table.overheadReach} returns it.` : ` Their rows, which carry that share as overheadMs, are none of the ${returned} returned; limit ${table.overheadReach} returns them all.`;
43633
+ return one ? ` Its row, which carries that share as overheadMs, comes after ${theReturned}; limit ${table.overheadReach} returns it.` : ` Their rows, which carry that share as overheadMs, come after ${theReturned}; limit ${table.overheadReach} returns them all.`;
43588
43634
  }
43589
- return ` Their rows carry that share as overheadMs, and ${shown} of them ${shown === 1 ? "is" : "are"} among the ${returned} returned; limit ${table.overheadReach} returns them all.`;
43635
+ const among = returned === 1 ? "the one row returned is one of them" : `${shown} of them ${shown === 1 ? "is" : "are"} among ${theReturned}`;
43636
+ return ` Their rows carry that share as overheadMs, and ${among}; limit ${table.overheadReach} returns them all.`;
43590
43637
  }
43591
43638
  function namedFew(names) {
43592
43639
  const shown = names.slice(0, 5);
@@ -43632,12 +43679,13 @@ function uncountedCallee(totals, name) {
43632
43679
  }
43633
43680
  function callersAnswer(totals, asked) {
43634
43681
  const found = callersOf(totals, asked.name, asked.scripts());
43682
+ const byName = found.target.length === 0 ? `${CALLERS_FOUND_BY_NAME} ${uncountedCallee(totals, asked.name)}` : CALLERS_FOUND_BY_NAME;
43635
43683
  return {
43636
43684
  of: asked.name,
43637
43685
  rows: found.target,
43638
43686
  sites: found.sites,
43639
43687
  omitted: found.omitted > 0 ? found.omitted : undefined,
43640
- 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} ${uncountedCallee(totals, asked.name)}` : CALLERS_FOUND_BY_NAME
43688
+ 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.property ? `${byName} ${PROPERTY_PLACES}` : byName
43641
43689
  };
43642
43690
  }
43643
43691
  function foldedEntriesAnswer(log) {
@@ -46448,7 +46496,7 @@ class GodotServer {
46448
46496
  platform: process.platform,
46449
46497
  variables: process.env
46450
46498
  });
46451
- const runtimeWaitMs = this.announceBudget(args, project.value.path);
46499
+ const runtimeWaitMs = this.announceBudget(args, project.value.path, sceneArgument);
46452
46500
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
46453
46501
  const silent = resolveSilent(args["silent"], process.env) && !headless;
46454
46502
  const profiled = readBoolean(args, "profile") === true;
@@ -46470,6 +46518,7 @@ class GodotServer {
46470
46518
  ]);
46471
46519
  }
46472
46520
  started.announcedBefore = alreadyPlaying;
46521
+ started.bootScene = runtimeWaitMs.scene;
46473
46522
  this.activeProcess = started;
46474
46523
  const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
46475
46524
  const endedForThis = endedToStartThis(ended);
@@ -46559,17 +46608,18 @@ class GodotServer {
46559
46608
  ...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
46560
46609
  });
46561
46610
  }
46562
- announceBudget(args, projectPath) {
46611
+ announceBudget(args, projectPath, scene) {
46612
+ const key = scene ?? MAIN_SCENE;
46563
46613
  const asked = readNonNegativeNumber(args, "runtimeWaitMs");
46564
46614
  if (asked !== undefined) {
46565
- return { ms: asked };
46615
+ return { ms: asked, scene: key };
46566
46616
  }
46567
- const lastBoot = readBootNote(projectPath);
46617
+ const lastBoot = readBootNote(projectPath, key);
46568
46618
  const ms = waitSizedTo(lastBoot);
46569
- return lastBoot !== null && ms > ANNOUNCE_BUDGET_MS ? { ms, sizedToMs: lastBoot } : { ms };
46619
+ return lastBoot !== null && ms > ANNOUNCE_BUDGET_MS ? { ms, sizedToMs: lastBoot, scene: key } : { ms, scene: key };
46570
46620
  }
46571
46621
  noteBootOf(run, endpoint) {
46572
- if (run.projectPath === null) {
46622
+ if (run.projectPath === null || run.bootScene === undefined) {
46573
46623
  return;
46574
46624
  }
46575
46625
  let announcedAt;
@@ -46581,7 +46631,7 @@ class GodotServer {
46581
46631
  const took = announcedAt - run.startedAt;
46582
46632
  this.logDebug(`boot of pid ${endpoint.pid} announced ${Math.round(took)}ms after the start of ${run.projectPath}`);
46583
46633
  if (took > 0 && took <= LONGEST_BOOT_NOTED_MS) {
46584
- writeBootNote(run.projectPath, took);
46634
+ writeBootNote(run.projectPath, run.bootScene, took);
46585
46635
  }
46586
46636
  }
46587
46637
  async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended, profiled) {
@@ -46640,7 +46690,8 @@ class GodotServer {
46640
46690
  brokeOn: null,
46641
46691
  announcedBefore: alreadyPlaying,
46642
46692
  seenPlaying: readBoolean(playAnswer, "playing") === true,
46643
- ...which === null ? {} : { play: which }
46693
+ ...which === null ? {} : { play: which },
46694
+ bootScene: runtimeWaitMs.scene
46644
46695
  };
46645
46696
  this.activeProcess = played;
46646
46697
  this.noteTheLatestRun({ projectPath, pid: null, startedAt });
@@ -46810,6 +46861,7 @@ class GodotServer {
46810
46861
  }
46811
46862
  running.endedHere = reason;
46812
46863
  const announced = this.announcedPidOf(running);
46864
+ await this.sendTheProfileFirst(running, announced);
46813
46865
  if (running.throughEditor) {
46814
46866
  const asked = await this.handleViaBridge("stop_playing", {});
46815
46867
  if (asked.isError === true) {
@@ -46880,6 +46932,25 @@ class GodotServer {
46880
46932
  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}.`);
46881
46933
  return gone ? "gone" : "lingering";
46882
46934
  }
46935
+ async sendTheProfileFirst(run, announced) {
46936
+ if (announced === undefined) {
46937
+ return;
46938
+ }
46939
+ const profiled = run.throughEditor ? await this.theEditorIsProfiling() : run.profile !== undefined;
46940
+ const endpoint = profiled ? runtimesAnnounced().running.find((one) => one.pid === announced) : undefined;
46941
+ if (endpoint === undefined) {
46942
+ return;
46943
+ }
46944
+ await runtimeRequest(endpoint, "send_profile", {}, PROFILE_SEND_MS);
46945
+ }
46946
+ async theEditorIsProfiling() {
46947
+ const read = await this.handleViaBridge("profile_read", {});
46948
+ if (read.isError === true) {
46949
+ return false;
46950
+ }
46951
+ const held = asParams(JSON.parse(read.content[0]?.text ?? "{}"));
46952
+ return readBoolean(held, "profiled") === true && readBoolean(held, "complete") !== true;
46953
+ }
46883
46954
  async endThroughTheKeeper(run, pid, keeper) {
46884
46955
  const asked = await askTheKeeperToStop(keeper, KEEPER_ANSWER_MS);
46885
46956
  if (typeof asked !== "string") {
@@ -48555,7 +48626,7 @@ class GodotServer {
48555
48626
  return null;
48556
48627
  }
48557
48628
  const which = run.throughEditor ? "the game the editor is playing for it" : `pid ${run.pid ?? "unknown"}`;
48558
- const comingUpUntil = run.startedAt + waitSizedTo(readBootNote(project)) + budgetMs;
48629
+ const comingUpUntil = run.startedAt + waitSizedTo(readBootNote(project, run.bootScene ?? MAIN_SCENE)) + budgetMs;
48559
48630
  const remaining = comingUpUntil - Date.now();
48560
48631
  if (remaining <= 0) {
48561
48632
  const seconds = Math.round((Date.now() - run.startedAt) / 1000);
@@ -48773,7 +48844,7 @@ async function runGodotServer() {
48773
48844
  const server = new GodotServer;
48774
48845
  await server.run();
48775
48846
  }
48776
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, 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, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
48847
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
48777
48848
  var init_server3 = __esm(() => {
48778
48849
  init_mcp();
48779
48850
  init_stdio2();
@@ -140,6 +140,7 @@ func _init() -> void:
140
140
  "wait_frames": _waits.wait_frames,
141
141
  "wait_signal": _waits.wait_signal,
142
142
  "wait_until": _waits.wait_until,
143
+ "send_profile": _send_profile_now,
143
144
  }
144
145
  _wait_for_the_profiler()
145
146
 
@@ -236,6 +237,16 @@ func _send_the_profile() -> void:
236
237
  )
237
238
 
238
239
 
240
+ ## The totals sent now, for a stop that is about to end the game without letting it quit: ended by
241
+ ## its process, a game takes the totals with it, and every stopped profile came back as the frames
242
+ ## summed. Nothing of the game's own runs, which quitting the tree would have done.
243
+ func _send_profile_now(_params: Dictionary) -> Dictionary:
244
+ if not (EngineDebugger.is_active() and EngineDebugger.is_profiling("servers")):
245
+ return {"sent": false}
246
+ _send_the_profile()
247
+ return {"sent": true, "confirmed": _profile_received}
248
+
249
+
239
250
  ## What the script profiler costs per call in this game, measured once each time it is switched on.
240
251
  ##
241
252
  ## The profiler times every call it counts, and that bookkeeping lands in the self time of the
@@ -517,6 +517,13 @@ func click(params: Dictionary) -> Dictionary:
517
517
  var on_rich_text: bool = rich != null
518
518
  if on_rich_text:
519
519
  var _listening: int = rich.meta_clicked.connect(heard)
520
+ # A toggle's state either side of the press, since landing says nothing of it: a game clock's
521
+ # switch clicked three times answered clicked each time, and a wait for the clock to move then
522
+ # ran out on a clock left stopped. A click the game undid reads the same before and after.
523
+ var toggle: BaseButton = control as BaseButton
524
+ var was_pressed: Variant = null
525
+ if toggle != null and toggle.toggle_mode:
526
+ was_pressed = toggle.button_pressed
520
527
  _press_button(viewport, _button(position, button, true, double))
521
528
  await _host.get_tree().process_frame
522
529
  _press_button(viewport, _button(position, button, false, false))
@@ -544,6 +551,11 @@ func click(params: Dictionary) -> Dictionary:
544
551
  }
545
552
  if found != null:
546
553
  answer["found"] = found
554
+ if was_pressed != null:
555
+ var now_pressed: Variant = null
556
+ if is_instance_valid(toggle):
557
+ now_pressed = toggle.button_pressed
558
+ answer["pressed"] = {"before": was_pressed, "after": now_pressed}
547
559
  # Whether a link was pressed apart from what it carries: a bare [url] reports an empty meta,
548
560
  # and given as the meta alone that press read exactly like one that reached no link.
549
561
  if on_rich_text:
package/build/index.js CHANGED
@@ -26036,19 +26036,35 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
26036
26036
  function bootNotePath(projectPath) {
26037
26037
  return join3(projectPath, ".godot", "gdharness-boot.json");
26038
26038
  }
26039
- function readBootNote(projectPath) {
26039
+ var MAIN_SCENE = "";
26040
+ function readScenes(projectPath) {
26040
26041
  try {
26041
- const noted = readNumber(asParams(JSON.parse(readFileSync4(bootNotePath(projectPath), "utf8"))), "announcedAfterMs");
26042
- return noted !== undefined && Number.isFinite(noted) && noted > 0 ? noted : null;
26042
+ return asParams(asParams(JSON.parse(readFileSync4(bootNotePath(projectPath), "utf8")))["scenes"]);
26043
26043
  } catch {
26044
- return null;
26044
+ return {};
26045
26045
  }
26046
26046
  }
26047
- function writeBootNote(projectPath, announcedAfterMs) {
26047
+ var BOOTS_KEPT = 5;
26048
+ function bootsOf(projectPath, scene) {
26049
+ const kept = asParams(readScenes(projectPath)[scene])["boots"];
26050
+ return Array.isArray(kept) ? kept.filter((one) => typeof one === "number" && Number.isFinite(one) && one > 0) : [];
26051
+ }
26052
+ function readBootNote(projectPath, scene) {
26053
+ const boots = bootsOf(projectPath, scene);
26054
+ return boots.length === 0 ? null : Math.max(...boots);
26055
+ }
26056
+ function writeBootNote(projectPath, scene, announcedAfterMs) {
26048
26057
  try {
26049
26058
  const path = bootNotePath(projectPath);
26050
26059
  mkdirSync2(dirname2(path), { recursive: true });
26051
- writeFileSync2(path, JSON.stringify({ announcedAfterMs: Math.round(announcedAfterMs), at: new Date().toISOString() }, null, 2), "utf8");
26060
+ const scenes = {
26061
+ ...readScenes(projectPath),
26062
+ [scene]: {
26063
+ boots: [...bootsOf(projectPath, scene), Math.round(announcedAfterMs)].slice(-BOOTS_KEPT),
26064
+ at: new Date().toISOString()
26065
+ }
26066
+ };
26067
+ writeFileSync2(path, JSON.stringify({ scenes }, null, 2), "utf8");
26052
26068
  } catch {}
26053
26069
  }
26054
26070
  var LONGEST_SIZED_WAIT_MS = 60000;
@@ -26613,6 +26629,7 @@ function bodyCallsSuper(code, line, lambda) {
26613
26629
  }
26614
26630
  return false;
26615
26631
  }
26632
+ var ASSIGNED = /^(?:\*\*|<<|>>|[-+*/%&|^])?=(?!=)/;
26616
26633
  function parametersOf(text, from) {
26617
26634
  const open = text.indexOf("(", from);
26618
26635
  if (open === -1) {
@@ -26731,8 +26748,31 @@ function callSitesOf(source, name, otherClasses = new Set, property = false) {
26731
26748
  const declared = bodyHolding(shape.body, at).declarations.find((one) => one.name === member && (one.kind === "var" || one.kind === "const"));
26732
26749
  return declared === undefined ? null : declaredClass(codeLines[lines.positionOf(declared.offset).line] ?? "", member);
26733
26750
  };
26751
+ const blockUnder = (declared) => {
26752
+ const indent = (codeLines[declared] ?? "").search(/\S|$/);
26753
+ let last = declared;
26754
+ for (let index = declared + 1;index < codeLines.length; index += 1) {
26755
+ const text = codeLines[index] ?? "";
26756
+ if (text.trim() === "") {
26757
+ continue;
26758
+ }
26759
+ if (text.search(/\S/) <= indent) {
26760
+ break;
26761
+ }
26762
+ last = index;
26763
+ }
26764
+ return [declared, last];
26765
+ };
26766
+ const accessorsIn = (body) => [
26767
+ ...body.declarations.filter((one) => one.kind === "var" && one.name === name).map((one) => blockUnder(lines.positionOf(one.offset).line)),
26768
+ ...body.inner.flatMap(accessorsIn)
26769
+ ];
26770
+ const accessors = property ? accessorsIn(shape.body) : [];
26734
26771
  return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).filter((one) => {
26735
26772
  const index = lines.positionOf(one.offset).line;
26773
+ if (!one.afterDot && accessors.some(([declared, last]) => index > declared && index <= last)) {
26774
+ return false;
26775
+ }
26736
26776
  if (one.afterDot) {
26737
26777
  const receiver = receiverBefore(code, one.offset);
26738
26778
  if (receiver === null || otherClasses.size === 0) {
@@ -26754,7 +26794,8 @@ function callSitesOf(source, name, otherClasses = new Set, property = false) {
26754
26794
  }).map((one) => {
26755
26795
  const line = lines.positionOf(one.offset).line + 1;
26756
26796
  const after = code.slice(one.offset + name.length).trimStart();
26757
- return { line, within: enclosingFunction(source, line, code), called: after.startsWith("(") };
26797
+ const within = enclosingFunction(source, line, code);
26798
+ return property ? { line, within, access: ASSIGNED.test(after) ? "write" : "read" } : { line, within, called: after.startsWith("(") };
26758
26799
  });
26759
26800
  }
26760
26801
  function collectEnumValues(text, from, into) {
@@ -33491,7 +33532,8 @@ var CALL_SITES = 40;
33491
33532
  function callersOf(totals, name, scripts) {
33492
33533
  const listed = [...scripts];
33493
33534
  const sources = new Map(listed);
33494
- const rows = rowsOf(totals, (script) => sources.get(script) ?? null);
33535
+ const perCallUs = perCallCostOf(totals);
33536
+ const rows = rowsOf(totals, (script) => sources.get(script) ?? null).map((row) => withOverhead(row, perCallUs));
33495
33537
  const asked = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : null;
33496
33538
  const bare = propertyOf(name) ?? bareName(name);
33497
33539
  const ownerOf = (fn) => fn.includes(".") ? fn.slice(0, fn.lastIndexOf(".")) : null;
@@ -33524,7 +33566,12 @@ function callersOf(totals, name, scripts) {
33524
33566
  }
33525
33567
  }
33526
33568
  sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
33527
- return { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
33569
+ return {
33570
+ target,
33571
+ sites: sites.slice(0, CALL_SITES),
33572
+ omitted: Math.max(0, sites.length - CALL_SITES),
33573
+ property
33574
+ };
33528
33575
  }
33529
33576
 
33530
33577
  // src/setup.ts
@@ -34492,7 +34539,7 @@ var TOOL_SPECS = [
34492
34539
  type: "integer",
34493
34540
  minimum: 0,
34494
34541
  ops: ["start"],
34495
- description: "start: how long to wait for the game to announce its runtime before answering. Default 5000, or half as long again as the last boot of this project took when that is more, up to 60000: the server notes how long each game took to announce, so a project that boots slowly is waited for from its second start on without being asked to. What it waits for is the first frame, so everything the game does before drawing one is inside it, including work the args just asked for: a flag that simulates six years of game time before anything is drawn makes the announcement that late, on a project that announces promptly without it. Raise it for such a run rather than reading runtime listening false as a fault. 0 does not wait at all, which is the one to pass for a scene that announces nothing by construction, such as a bench that prints and quits."
34542
+ description: "start: how long to wait for the game to announce its runtime before answering. Default 5000, or half as long again as the slowest of the last five boots of the same scene when that is more, up to 60000: the server notes how long each game took to announce, per scene and for the main scene when none is named, since a boot moves with the load on the machine, so a scene that boots slowly is waited for from its second start on without being asked to, and a harness scene that announces late sets nothing for the main scene. What it waits for is the first frame, so everything the game does before drawing one is inside it, including work the args just asked for: a flag that simulates six years of game time before anything is drawn makes the announcement that late, on a project that announces promptly without it. Raise it for such a run rather than reading runtime listening false as a fault. 0 does not wait at all, which is the one to pass for a scene that announces nothing by construction, such as a bench that prints and quits."
34496
34543
  },
34497
34544
  frames: {
34498
34545
  type: "integer",
@@ -34514,7 +34561,7 @@ var TOOL_SPECS = [
34514
34561
  callersOf: {
34515
34562
  type: "string",
34516
34563
  ops: ["profile"],
34517
- 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, except, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed. A property's get or set block, which the profile names Class.@name_getter or Class.@name_setter, is asked about by that name or by the property's: the places are where the property is named, and the rows are both blocks'."
34564
+ 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, except, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed. A property's get or set block, which the profile names Class.@name_getter or Class.@name_setter, is asked about by that name or by the property's: the places are where the property is named, each saying instead of called whether it writes the property (access write), which runs the set block, or reads it (access read), which runs the get block, and the rows are both blocks'. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not places."
34518
34565
  },
34519
34566
  limit: {
34520
34567
  type: "integer",
@@ -34981,7 +35028,7 @@ var TOOL_SPECS = [
34981
35028
  requires: [],
34982
35029
  operations: {
34983
35030
  click: {
34984
- summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room`,
35031
+ summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room`,
34985
35032
  requires: []
34986
35033
  },
34987
35034
  choose: {
@@ -35521,6 +35568,7 @@ function endedToStartThis(ended) {
35521
35568
  return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
35522
35569
  }
35523
35570
  var LONGEST_BOOT_NOTED_MS = 5 * 60000;
35571
+ var PROFILE_SEND_MS = 5000;
35524
35572
  var MOST_GROUPS = 20;
35525
35573
  function runtimeVerdict(endpoint, after) {
35526
35574
  if (endpoint !== null) {
@@ -35542,7 +35590,7 @@ function runtimeVerdict(endpoint, after) {
35542
35590
  };
35543
35591
  }
35544
35592
  const state = after.playingSeen === false ? "the editor has not yet reported the game as playing, which a restarted editor does not until its scan is over" : "the game is still running";
35545
- const sized = after.sizedToMs === undefined ? "" : halfAsLongAgain(after.sizedToMs) > after.budgetMs ? `, the longest a start waits unasked though its last game took ${after.sizedToMs}ms to announce,` : `, half as long again as the ${after.sizedToMs}ms its last game took,`;
35593
+ const sized = after.sizedToMs === undefined ? "" : halfAsLongAgain(after.sizedToMs) > after.budgetMs ? `, the longest a start waits unasked though the slowest of its last boots took ${after.sizedToMs}ms to announce,` : `, half as long again as the ${after.sizedToMs}ms the slowest of its last boots took,`;
35546
35594
  return {
35547
35595
  listening: false,
35548
35596
  mayYetAnnounce: after.running,
@@ -35992,6 +36040,7 @@ function playedCap(held) {
35992
36040
  totalCapped: readBoolean(held, "totalCapped") === true
35993
36041
  };
35994
36042
  }
36043
+ var PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.";
35995
36044
  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, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.";
35996
36045
  function projectSourceOf(projectPath) {
35997
36046
  const read = new Map;
@@ -36078,10 +36127,12 @@ function overheadRowsReturned(table, one) {
36078
36127
  if (shown === table.overheadDominated) {
36079
36128
  return one ? " Its row carries that share as overheadMs." : " Their rows carry that share as overheadMs.";
36080
36129
  }
36130
+ const theReturned = returned === 1 ? "the one row returned" : `the ${returned} rows returned`;
36081
36131
  if (shown === 0) {
36082
- return one ? ` Its row, which carries that share as overheadMs, is not among the ${returned} returned; limit ${table.overheadReach} returns it.` : ` Their rows, which carry that share as overheadMs, are none of the ${returned} returned; limit ${table.overheadReach} returns them all.`;
36132
+ return one ? ` Its row, which carries that share as overheadMs, comes after ${theReturned}; limit ${table.overheadReach} returns it.` : ` Their rows, which carry that share as overheadMs, come after ${theReturned}; limit ${table.overheadReach} returns them all.`;
36083
36133
  }
36084
- return ` Their rows carry that share as overheadMs, and ${shown} of them ${shown === 1 ? "is" : "are"} among the ${returned} returned; limit ${table.overheadReach} returns them all.`;
36134
+ const among = returned === 1 ? "the one row returned is one of them" : `${shown} of them ${shown === 1 ? "is" : "are"} among ${theReturned}`;
36135
+ return ` Their rows carry that share as overheadMs, and ${among}; limit ${table.overheadReach} returns them all.`;
36085
36136
  }
36086
36137
  function namedFew(names) {
36087
36138
  const shown = names.slice(0, 5);
@@ -36127,12 +36178,13 @@ function uncountedCallee(totals, name) {
36127
36178
  }
36128
36179
  function callersAnswer(totals, asked) {
36129
36180
  const found = callersOf(totals, asked.name, asked.scripts());
36181
+ const byName = found.target.length === 0 ? `${CALLERS_FOUND_BY_NAME} ${uncountedCallee(totals, asked.name)}` : CALLERS_FOUND_BY_NAME;
36130
36182
  return {
36131
36183
  of: asked.name,
36132
36184
  rows: found.target,
36133
36185
  sites: found.sites,
36134
36186
  omitted: found.omitted > 0 ? found.omitted : undefined,
36135
- 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} ${uncountedCallee(totals, asked.name)}` : CALLERS_FOUND_BY_NAME
36187
+ 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.property ? `${byName} ${PROPERTY_PLACES}` : byName
36136
36188
  };
36137
36189
  }
36138
36190
  var MOST_ENGINE_ENTRIES = 200;
@@ -38946,7 +38998,7 @@ class GodotServer {
38946
38998
  platform: process.platform,
38947
38999
  variables: process.env
38948
39000
  });
38949
- const runtimeWaitMs = this.announceBudget(args, project.value.path);
39001
+ const runtimeWaitMs = this.announceBudget(args, project.value.path, sceneArgument);
38950
39002
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
38951
39003
  const silent = resolveSilent(args["silent"], process.env) && !headless;
38952
39004
  const profiled = readBoolean(args, "profile") === true;
@@ -38968,6 +39020,7 @@ class GodotServer {
38968
39020
  ]);
38969
39021
  }
38970
39022
  started.announcedBefore = alreadyPlaying;
39023
+ started.bootScene = runtimeWaitMs.scene;
38971
39024
  this.activeProcess = started;
38972
39025
  const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
38973
39026
  const endedForThis = endedToStartThis(ended);
@@ -39057,17 +39110,18 @@ class GodotServer {
39057
39110
  ...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
39058
39111
  });
39059
39112
  }
39060
- announceBudget(args, projectPath) {
39113
+ announceBudget(args, projectPath, scene) {
39114
+ const key = scene ?? MAIN_SCENE;
39061
39115
  const asked = readNonNegativeNumber(args, "runtimeWaitMs");
39062
39116
  if (asked !== undefined) {
39063
- return { ms: asked };
39117
+ return { ms: asked, scene: key };
39064
39118
  }
39065
- const lastBoot = readBootNote(projectPath);
39119
+ const lastBoot = readBootNote(projectPath, key);
39066
39120
  const ms = waitSizedTo(lastBoot);
39067
- return lastBoot !== null && ms > ANNOUNCE_BUDGET_MS ? { ms, sizedToMs: lastBoot } : { ms };
39121
+ return lastBoot !== null && ms > ANNOUNCE_BUDGET_MS ? { ms, sizedToMs: lastBoot, scene: key } : { ms, scene: key };
39068
39122
  }
39069
39123
  noteBootOf(run, endpoint) {
39070
- if (run.projectPath === null) {
39124
+ if (run.projectPath === null || run.bootScene === undefined) {
39071
39125
  return;
39072
39126
  }
39073
39127
  let announcedAt;
@@ -39079,7 +39133,7 @@ class GodotServer {
39079
39133
  const took = announcedAt - run.startedAt;
39080
39134
  this.logDebug(`boot of pid ${endpoint.pid} announced ${Math.round(took)}ms after the start of ${run.projectPath}`);
39081
39135
  if (took > 0 && took <= LONGEST_BOOT_NOTED_MS) {
39082
- writeBootNote(run.projectPath, took);
39136
+ writeBootNote(run.projectPath, run.bootScene, took);
39083
39137
  }
39084
39138
  }
39085
39139
  async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended, profiled) {
@@ -39138,7 +39192,8 @@ class GodotServer {
39138
39192
  brokeOn: null,
39139
39193
  announcedBefore: alreadyPlaying,
39140
39194
  seenPlaying: readBoolean(playAnswer, "playing") === true,
39141
- ...which === null ? {} : { play: which }
39195
+ ...which === null ? {} : { play: which },
39196
+ bootScene: runtimeWaitMs.scene
39142
39197
  };
39143
39198
  this.activeProcess = played;
39144
39199
  this.noteTheLatestRun({ projectPath, pid: null, startedAt });
@@ -39308,6 +39363,7 @@ class GodotServer {
39308
39363
  }
39309
39364
  running.endedHere = reason;
39310
39365
  const announced = this.announcedPidOf(running);
39366
+ await this.sendTheProfileFirst(running, announced);
39311
39367
  if (running.throughEditor) {
39312
39368
  const asked = await this.handleViaBridge("stop_playing", {});
39313
39369
  if (asked.isError === true) {
@@ -39378,6 +39434,25 @@ class GodotServer {
39378
39434
  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}.`);
39379
39435
  return gone ? "gone" : "lingering";
39380
39436
  }
39437
+ async sendTheProfileFirst(run, announced) {
39438
+ if (announced === undefined) {
39439
+ return;
39440
+ }
39441
+ const profiled = run.throughEditor ? await this.theEditorIsProfiling() : run.profile !== undefined;
39442
+ const endpoint = profiled ? runtimesAnnounced().running.find((one) => one.pid === announced) : undefined;
39443
+ if (endpoint === undefined) {
39444
+ return;
39445
+ }
39446
+ await runtimeRequest(endpoint, "send_profile", {}, PROFILE_SEND_MS);
39447
+ }
39448
+ async theEditorIsProfiling() {
39449
+ const read = await this.handleViaBridge("profile_read", {});
39450
+ if (read.isError === true) {
39451
+ return false;
39452
+ }
39453
+ const held = asParams(JSON.parse(read.content[0]?.text ?? "{}"));
39454
+ return readBoolean(held, "profiled") === true && readBoolean(held, "complete") !== true;
39455
+ }
39381
39456
  async endThroughTheKeeper(run, pid, keeper) {
39382
39457
  const asked = await askTheKeeperToStop(keeper, KEEPER_ANSWER_MS);
39383
39458
  if (typeof asked !== "string") {
@@ -41053,7 +41128,7 @@ class GodotServer {
41053
41128
  return null;
41054
41129
  }
41055
41130
  const which = run.throughEditor ? "the game the editor is playing for it" : `pid ${run.pid ?? "unknown"}`;
41056
- const comingUpUntil = run.startedAt + waitSizedTo(readBootNote(project)) + budgetMs;
41131
+ const comingUpUntil = run.startedAt + waitSizedTo(readBootNote(project, run.bootScene ?? MAIN_SCENE)) + budgetMs;
41057
41132
  const remaining = comingUpUntil - Date.now();
41058
41133
  if (remaining <= 0) {
41059
41134
  const seconds = Math.round((Date.now() - run.startedAt) / 1000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.9",
3
+ "version": "1.2.11",
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",