gdharness 1.0.2 → 1.0.3

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
@@ -16155,7 +16155,7 @@ var init_tool_definitions = __esm(() => {
16155
16155
  property: {
16156
16156
  type: "string",
16157
16157
  ops: ["property", "find"],
16158
- description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step that is not there is named, and says what was there instead.'
16158
+ description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A step that is not there is named, and says what was there instead.'
16159
16159
  },
16160
16160
  depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
16161
16161
  includeProperties: {
@@ -16236,7 +16236,7 @@ var init_tool_definitions = __esm(() => {
16236
16236
  property: {
16237
16237
  type: "string",
16238
16238
  ops: ["set"],
16239
- description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
16239
+ description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
16240
16240
  },
16241
16241
  value: {
16242
16242
  ops: ["set"],
@@ -16246,7 +16246,7 @@ var init_tool_definitions = __esm(() => {
16246
16246
  method: {
16247
16247
  type: "string",
16248
16248
  ops: ["call"],
16249
- description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, and a list or a map is stepped into by index or key, "_game:run:roster:0:retire".'
16249
+ description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a method taking no arguments returned, "get_viewport():gui_get_focus_owner".'
16250
16250
  },
16251
16251
  args: {
16252
16252
  type: "array",
@@ -16306,7 +16306,7 @@ var init_tool_definitions = __esm(() => {
16306
16306
  pressed: {
16307
16307
  type: "boolean",
16308
16308
  ops: ["action", "key", "mouse_click"],
16309
- description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false."
16309
+ description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false; a wheel button down is a whole step, since a wheel is never held."
16310
16310
  },
16311
16311
  strength: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
16312
16312
  keycode: {
@@ -16384,7 +16384,7 @@ var init_tool_definitions = __esm(() => {
16384
16384
  requires: ["text"]
16385
16385
  },
16386
16386
  mouse_click: {
16387
- summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
16387
+ summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false. A wheel button is a whole step, press and release together as a mouse sends one, and the answer says released: a lone wheel press holds the viewport on the control that took it, so every click after it lands there",
16388
16388
  requires: ["x", "y"]
16389
16389
  },
16390
16390
  mouse_motion: {
@@ -16406,7 +16406,11 @@ var init_tool_definitions = __esm(() => {
16406
16406
  },
16407
16407
  nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
16408
16408
  signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
16409
- property: { type: "string", ops: ["until"], description: "until: the property name." },
16409
+ property: {
16410
+ type: "string",
16411
+ ops: ["until"],
16412
+ description: 'until: the property name, or a colon path through what the node holds, "_game:run:day", read the way runtime_inspect property reads one, a step written as a call included. Walked again on every frame, so a holder the game replaces while the wait is on is followed.'
16413
+ },
16410
16414
  value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
16411
16415
  says: {
16412
16416
  type: "string",
@@ -36462,18 +36466,59 @@ var init_lsp_client = __esm(() => {
36462
36466
  });
36463
36467
 
36464
36468
  // src/process-children.ts
36465
- import { execFile as execFile5 } from "node:child_process";
36469
+ import { execFile as execFile5, execFileSync, spawnSync as spawnSync2 } from "node:child_process";
36466
36470
  import { basename as basename2 } from "node:path";
36467
36471
  import { promisify as promisify5 } from "node:util";
36472
+ function startTimesOf(pids) {
36473
+ const began = new Map;
36474
+ if (pids.length === 0) {
36475
+ return began;
36476
+ }
36477
+ try {
36478
+ if (process.platform === "win32") {
36479
+ const filter = pids.map((pid) => `ProcessId=${pid}`).join(" OR ");
36480
+ const said = execFileSync("powershell", [
36481
+ "-NoProfile",
36482
+ "-NonInteractive",
36483
+ "-Command",
36484
+ `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`
36485
+ ], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, windowsHide: true });
36486
+ for (const line of said.split(`
36487
+ `)) {
36488
+ const found = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
36489
+ if (found?.[1] !== undefined && found[2] !== undefined) {
36490
+ began.set(Number(found[1]), Number(found[2]));
36491
+ }
36492
+ }
36493
+ return began;
36494
+ }
36495
+ const said = spawnSync2("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
36496
+ encoding: "utf8",
36497
+ timeout: ASK_TIMEOUT_MS,
36498
+ env: { ...process.env, LC_ALL: "C" }
36499
+ }).stdout;
36500
+ for (const line of said.split(`
36501
+ `)) {
36502
+ const found = /^\s*(\d+)\s+(.+)$/.exec(line);
36503
+ const at = Date.parse(found?.[2]?.trim() ?? "");
36504
+ if (found?.[1] !== undefined && !Number.isNaN(at)) {
36505
+ began.set(Number(found[1]), at);
36506
+ }
36507
+ }
36508
+ } catch {}
36509
+ return began;
36510
+ }
36468
36511
  async function processTree() {
36469
36512
  try {
36470
- const printed = process.platform === "win32" ? await askWindows() : await askPosix();
36471
- return parseProcessTable(printed);
36513
+ if (process.platform === "win32") {
36514
+ return parseProcessTable(await askWindows(), true);
36515
+ }
36516
+ return parseProcessTable(await askPosix());
36472
36517
  } catch {
36473
36518
  return;
36474
36519
  }
36475
36520
  }
36476
- function parseProcessTable(printed) {
36521
+ function parseProcessTable(printed, started = false) {
36477
36522
  const tree = new Map;
36478
36523
  for (const line of printed.split(/\r?\n/)) {
36479
36524
  const fields = /^\s*(\d+)\s+(\d+)(?:\s+(.*))?$/.exec(line);
@@ -36482,16 +36527,38 @@ function parseProcessTable(printed) {
36482
36527
  }
36483
36528
  const pid = Number(fields[1]);
36484
36529
  const parent = Number(fields[2]);
36485
- if (pid > 0) {
36486
- tree.set(pid, { parent, command: (fields[3] ?? "").trim() });
36530
+ const rest = (fields[3] ?? "").trim();
36531
+ if (pid <= 0) {
36532
+ continue;
36533
+ }
36534
+ if (!started) {
36535
+ tree.set(pid, { parent, command: rest });
36536
+ continue;
36537
+ }
36538
+ const timed = /^(\d+)(?:\s+(.*))?$/.exec(rest);
36539
+ if (timed === null) {
36540
+ continue;
36487
36541
  }
36542
+ const began = Number(timed[1]);
36543
+ tree.set(pid, { parent, command: (timed[2] ?? "").trim(), ...began > 0 ? { startedAt: began } : {} });
36488
36544
  }
36489
36545
  return tree;
36490
36546
  }
36547
+ function linked(tree, child, parent) {
36548
+ const from = tree.get(child);
36549
+ const to = tree.get(parent);
36550
+ if (from === undefined || from.parent !== parent) {
36551
+ return false;
36552
+ }
36553
+ if (from.startedAt === undefined || to?.startedAt === undefined) {
36554
+ return true;
36555
+ }
36556
+ return from.startedAt >= to.startedAt;
36557
+ }
36491
36558
  function childrenIn(tree, pid) {
36492
36559
  const children = [];
36493
- for (const [child, listed] of tree) {
36494
- if (listed.parent === pid) {
36560
+ for (const child of tree.keys()) {
36561
+ if (linked(tree, child, pid)) {
36495
36562
  children.push(child);
36496
36563
  }
36497
36564
  }
@@ -36519,13 +36586,15 @@ function descendantsIn(tree, pid) {
36519
36586
  function ancestorsIn(tree, pid, root) {
36520
36587
  const chain = [];
36521
36588
  const seen = new Set([pid]);
36589
+ let child = pid;
36522
36590
  let parent = tree.get(pid)?.parent;
36523
- while (parent !== undefined && !seen.has(parent)) {
36591
+ while (parent !== undefined && !seen.has(parent) && linked(tree, child, parent)) {
36524
36592
  chain.push(parent);
36525
36593
  if (parent === root) {
36526
36594
  break;
36527
36595
  }
36528
36596
  seen.add(parent);
36597
+ child = parent;
36529
36598
  parent = tree.get(parent)?.parent;
36530
36599
  }
36531
36600
  return chain;
@@ -36581,7 +36650,7 @@ async function askWindows() {
36581
36650
  "-NoProfile",
36582
36651
  "-NonInteractive",
36583
36652
  "-Command",
36584
- 'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $($_.CommandLine)" }'
36653
+ 'Get-CimInstance Win32_Process | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }'
36585
36654
  ], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
36586
36655
  return stdout;
36587
36656
  }
@@ -36813,7 +36882,7 @@ var init_restart_note = __esm(() => {
36813
36882
  import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync15, statSync as statSync3, unlinkSync } from "node:fs";
36814
36883
  import { createConnection as createConnection3 } from "node:net";
36815
36884
  import { tmpdir as tmpdir4 } from "node:os";
36816
- import { join as join17, resolve as resolve4 } from "node:path";
36885
+ import { dirname as dirname11, join as join17, resolve as resolve4 } from "node:path";
36817
36886
  import { setTimeout as delay2 } from "node:timers/promises";
36818
36887
  function runtimeDirectory(variables = process.env) {
36819
36888
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
@@ -36922,11 +36991,31 @@ function announcedAndGone(projectPath) {
36922
36991
  const wanted = resolve4(projectPath);
36923
36992
  return gone.filter((one) => one.noticedAt >= since && resolve4(one.project) === wanted);
36924
36993
  }
36994
+ function strangersAmong(candidates, now, startTimes = startTimesOf) {
36995
+ const toJudge = candidates.filter((one) => {
36996
+ if (now - one.writtenAt < JUDGED_AFTER_MS) {
36997
+ return false;
36998
+ }
36999
+ const known = verdicts.get(one.file);
37000
+ return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
37001
+ });
37002
+ if (toJudge.length > 0) {
37003
+ const began = startTimes(toJudge.map((one) => one.pid));
37004
+ for (const one of toJudge) {
37005
+ const at = began.get(one.pid);
37006
+ const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
37007
+ verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
37008
+ }
37009
+ }
37010
+ return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
37011
+ }
36925
37012
  function announcedIn(directory) {
36926
37013
  if (!existsSync13(directory)) {
36927
37014
  return [];
36928
37015
  }
36929
37016
  const found = [];
37017
+ const candidates = [];
37018
+ const entries = [];
36930
37019
  for (const entry of readdirSync5(directory)) {
36931
37020
  const report = ERROR_REPORT_PATTERN.exec(entry);
36932
37021
  if (report) {
@@ -36940,20 +37029,59 @@ function announcedIn(directory) {
36940
37029
  const file = join17(directory, entry);
36941
37030
  const pid = Number.parseInt(match[1] ?? "", 10);
36942
37031
  const alive = processAlive(pid);
36943
- const announced = alive ? parseAnnouncement(file, pid) : { kind: "rubbish" };
37032
+ entries.push({ file, pid, alive });
37033
+ if (alive) {
37034
+ try {
37035
+ candidates.push({ file, pid, writtenAt: statSync3(file).mtimeMs });
37036
+ } catch {}
37037
+ }
37038
+ }
37039
+ const strangers = strangersAmong(candidates, Date.now());
37040
+ for (const { file, pid, alive } of entries) {
37041
+ const announced = alive && !strangers.has(file) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
36944
37042
  if (announced.kind === "rubbish") {
36945
- if (!alive) {
37043
+ if (!alive || strangers.has(file)) {
36946
37044
  wentAway(file, pid);
36947
37045
  }
36948
37046
  try {
36949
37047
  unlinkSync(file);
36950
37048
  } catch {}
37049
+ verdicts.delete(file);
36951
37050
  continue;
36952
37051
  }
36953
37052
  found.push(announced);
36954
37053
  }
37054
+ const held = new Set(entries.map((one) => one.file));
37055
+ const here = join17(directory, ".");
37056
+ for (const file of verdicts.keys()) {
37057
+ if (dirname11(file) === here && !held.has(file)) {
37058
+ verdicts.delete(file);
37059
+ }
37060
+ }
36955
37061
  return found;
36956
37062
  }
37063
+ async function announcementEnded(pid, directories = runtimeDirectories()) {
37064
+ const files = directories.map((directory) => join17(directory, `runtime-${pid}.json`)).filter((file) => existsSync13(file));
37065
+ const first = files[0];
37066
+ if (first === undefined) {
37067
+ return;
37068
+ }
37069
+ const until = Date.now() + ENDED_WITHIN_MS;
37070
+ while (processAlive(pid)) {
37071
+ const left = until - Date.now();
37072
+ if (left <= 0) {
37073
+ return;
37074
+ }
37075
+ await delay2(Math.min(LOOK_EVERY_MS, left));
37076
+ }
37077
+ wentAway(first, pid);
37078
+ for (const file of files) {
37079
+ try {
37080
+ unlinkSync(file);
37081
+ } catch {}
37082
+ verdicts.delete(file);
37083
+ }
37084
+ }
36957
37085
  function sweepErrorReport(path, pid, now = Date.now()) {
36958
37086
  if (processAlive(pid)) {
36959
37087
  return;
@@ -37134,16 +37262,18 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
37134
37262
  });
37135
37263
  });
37136
37264
  }
37137
- var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
37265
+ var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
37138
37266
  var init_runtime_client = __esm(() => {
37267
+ init_process_children();
37139
37268
  ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
37140
37269
  ERROR_REPORT_PATTERN = /^runtime-(\d+)\.log$/;
37141
37270
  ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
37142
37271
  gone = [];
37272
+ verdicts = new Map;
37143
37273
  });
37144
37274
 
37145
37275
  // src/run-record.ts
37146
- import { execFileSync } from "node:child_process";
37276
+ import { execFileSync as execFileSync2 } from "node:child_process";
37147
37277
  import { mkdirSync as mkdirSync9, openSync, readdirSync as readdirSync6, readFileSync as readFileSync16, rmSync as rmSync9, statSync as statSync4, writeFileSync as writeFileSync11 } from "node:fs";
37148
37278
  import { basename as basename3, join as join18 } from "node:path";
37149
37279
  function runsDirectory() {
@@ -37269,7 +37399,7 @@ function runningAs(pid) {
37269
37399
  const raw = readFileSync16(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
37270
37400
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
37271
37401
  }
37272
- const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
37402
+ const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
37273
37403
  encoding: "utf8",
37274
37404
  timeout: 15000
37275
37405
  }).trim();
@@ -37281,20 +37411,14 @@ function runningAs(pid) {
37281
37411
  function listeningPid(port) {
37282
37412
  try {
37283
37413
  if (process.platform === "win32") {
37284
- const said = execFileSync("powershell", [
37285
- "-NoProfile",
37286
- "-NonInteractive",
37287
- "-Command",
37288
- `(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
37289
- ], { encoding: "utf8", timeout: 15000 }).trim();
37290
- return /^\d+$/.test(said) ? Number(said) : null;
37414
+ return listeningPidInNetstat(execFileSync2("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 }), port);
37291
37415
  }
37292
37416
  for (const [command, args, pattern] of [
37293
37417
  ["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
37294
37418
  ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
37295
37419
  ]) {
37296
37420
  try {
37297
- const said = execFileSync(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
37421
+ const said = execFileSync2(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
37298
37422
  const found = pattern.exec(said);
37299
37423
  if (found?.[1] !== undefined) {
37300
37424
  return Number(found[1]);
@@ -37306,9 +37430,22 @@ function listeningPid(port) {
37306
37430
  return null;
37307
37431
  }
37308
37432
  }
37433
+ function listeningPidInNetstat(printed, port) {
37434
+ for (const line of printed.split(/\r?\n/)) {
37435
+ const fields = /^\s*TCP\s+(\S+)\s+\S+\s+LISTENING\s+(\d+)\s*$/i.exec(line);
37436
+ if (fields === null) {
37437
+ continue;
37438
+ }
37439
+ const local = fields[1] ?? "";
37440
+ if (Number(local.slice(local.lastIndexOf(":") + 1)) === port) {
37441
+ return Number(fields[2]);
37442
+ }
37443
+ }
37444
+ return null;
37445
+ }
37309
37446
  function windowsCommandLine(pid) {
37310
37447
  try {
37311
- const answer = execFileSync("powershell", [
37448
+ const answer = execFileSync2("powershell", [
37312
37449
  "-NoProfile",
37313
37450
  "-NonInteractive",
37314
37451
  "-Command",
@@ -37326,7 +37463,7 @@ function startedAt(pid, windowsAnswer) {
37326
37463
  return Number.isNaN(when) ? null : when;
37327
37464
  }
37328
37465
  try {
37329
- const said = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
37466
+ const said = execFileSync2("ps", ["-p", String(pid), "-o", "lstart="], {
37330
37467
  encoding: "utf8",
37331
37468
  timeout: 15000
37332
37469
  }).trim();
@@ -37338,7 +37475,7 @@ function startedAt(pid, windowsAnswer) {
37338
37475
  }
37339
37476
  function windowsImage(pid) {
37340
37477
  try {
37341
- const csv = execFileSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
37478
+ const csv = execFileSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
37342
37479
  encoding: "utf8",
37343
37480
  timeout: 15000,
37344
37481
  windowsHide: true
@@ -37441,7 +37578,7 @@ import {
37441
37578
  readSync
37442
37579
  } from "node:fs";
37443
37580
  import { tmpdir as tmpdir5 } from "node:os";
37444
- import { basename as basename4, dirname as dirname11, join as join19, normalize as normalize3, relative as relative3 } from "node:path";
37581
+ import { basename as basename4, dirname as dirname12, join as join19, normalize as normalize3, relative as relative3 } from "node:path";
37445
37582
  import { setTimeout as delay3 } from "node:timers/promises";
37446
37583
  import { fileURLToPath as fileURLToPath4 } from "node:url";
37447
37584
  import { promisify as promisify7 } from "node:util";
@@ -38659,11 +38796,11 @@ class GodotServer {
38659
38796
  if (!engine.ok) {
38660
38797
  return engine.response;
38661
38798
  }
38662
- const folder = dirname11(output.absolutePath);
38799
+ const folder = dirname12(output.absolutePath);
38663
38800
  try {
38664
38801
  mkdirSync10(folder, { recursive: true });
38665
38802
  } catch (error) {
38666
- return this.createErrorResponse(`Cannot create ${dirname11(output.relativePath)} to export into: ${errorMessage(error)}`);
38803
+ return this.createErrorResponse(`Cannot create ${dirname12(output.relativePath)} to export into: ${errorMessage(error)}`);
38667
38804
  }
38668
38805
  const debug = readBoolean(args, "debug") ?? false;
38669
38806
  const exportLogs = mkdtempSync3(join19(tmpdir5(), "gdharness-export-"));
@@ -39117,11 +39254,14 @@ class GodotServer {
39117
39254
  return null;
39118
39255
  }
39119
39256
  try {
39120
- const answer = await Promise.race([
39121
- this.godotBridge.invokeTool("playing_status", {}),
39122
- delay3(PLAYING_STATUS_MS).then(() => null)
39123
- ]);
39257
+ const askedAt = Date.now();
39258
+ const asked = this.godotBridge.invokeTool("playing_status", {});
39259
+ const answer = await Promise.race([asked, delay3(PLAYING_STATUS_MS).then(() => null)]);
39124
39260
  if (answer === null) {
39261
+ this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
39262
+ asked.then(() => {
39263
+ this.logDebug(`playing_status answered after ${Date.now() - askedAt}ms`);
39264
+ }).catch(() => {});
39125
39265
  return null;
39126
39266
  }
39127
39267
  return {
@@ -39441,6 +39581,9 @@ class GodotServer {
39441
39581
  let asking = null;
39442
39582
  let editorAskedAt = 0;
39443
39583
  let editorSaysGoing = true;
39584
+ let editorAsks = 0;
39585
+ const waitBegan = Date.now();
39586
+ const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
39444
39587
  const endpoint = await announcedSince(projectPath, before, {
39445
39588
  budgetMs,
39446
39589
  accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
@@ -39454,6 +39597,7 @@ class GodotServer {
39454
39597
  }
39455
39598
  if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
39456
39599
  editorAskedAt = Date.now();
39600
+ editorAsks += 1;
39457
39601
  asking = this.runStillGoing(run).then((going) => {
39458
39602
  editorSaysGoing = going;
39459
39603
  }).finally(() => {
@@ -39463,6 +39607,7 @@ class GodotServer {
39463
39607
  return !editorSaysGoing;
39464
39608
  }
39465
39609
  });
39610
+ this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, editor asked ${editorAsks} times, last said ${lastSaid()}`);
39466
39611
  const going = this.currentRun();
39467
39612
  if (endpoint !== null && going !== null && going.announcedPid === undefined) {
39468
39613
  going.announcedPid = endpoint.pid;
@@ -40139,11 +40284,19 @@ class GodotServer {
40139
40284
  this.drainEditorOutput(stopped);
40140
40285
  this.drainTranscript(stopped);
40141
40286
  const wasRunning = await this.runStillGoing(stopped);
40142
- const endedPid = stopped.pid ?? this.announcedPidOf(stopped) ?? null;
40287
+ const announcedPid = this.announcedPidOf(stopped) ?? null;
40288
+ const endedPid = stopped.pid ?? announcedPid;
40143
40289
  const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
40144
40290
  this.logDebug("Stopping the running game");
40145
40291
  await this.endActiveGame("editor_run stop");
40146
40292
  const ended = children === null ? null : endChildrenAmong(children);
40293
+ if (wasRunning) {
40294
+ for (const pid of new Set([endedPid, announcedPid])) {
40295
+ if (pid !== null) {
40296
+ await announcementEnded(pid);
40297
+ }
40298
+ }
40299
+ }
40147
40300
  return this.jsonTextResponse({
40148
40301
  stopped: true,
40149
40302
  through: stopped.throughEditor ? "editor" : "gdharness",
@@ -40591,7 +40744,7 @@ var init_server3 = __esm(() => {
40591
40744
  init_tool_definitions();
40592
40745
  init_update_check();
40593
40746
  run7 = promisify7(execFile7);
40594
- __dirname2 = dirname11(fileURLToPath4(import.meta.url));
40747
+ __dirname2 = dirname12(fileURLToPath4(import.meta.url));
40595
40748
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
40596
40749
  VALUE_NAMES = {
40597
40750
  string: "a string",
@@ -42142,6 +42295,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
42142
42295
  | Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
42143
42296
  | What a property reads right now, on a node whose path you already hold | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
42144
42297
  | What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
42298
+ | Which control holds the focus, or anything only a method answers | the same path with a step written as a call, \`get_viewport():gui_get_focus_owner\`, which calls a method taking no arguments and walks into what it returned |
42145
42299
  | Press a button | \`runtime_input click\`, which says what was under the pointer |
42146
42300
  | Press the next thing after a click that changed screen | \`runtime_wait until\` with \`says\` and a word the new screen shows, then the click. A click sent straight after a screen change lands on a control the new screen is about to free, answers \`landed: true\`, and does nothing; a frame count is a number nobody can pick correctly across a cold start and a warm one, and waiting for the words answered in about 130ms where twenty frames had not been enough |
42147
42301
  | Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
@@ -352,8 +352,15 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
352
352
  # its drag from the press, which is where a real pointer would be.
353
353
  var _moved: Vector2 = _arrive(position)
354
354
  Input.parse_input_event(_button(position, button, pressed, double))
355
+ # A wheel step is a press and a release together, as a mouse sends one: there is no holding a
356
+ # wheel. A lone wheel press left the viewport's mouse focus on the control that took it, with
357
+ # the wheel's bit in the focus mask, and every click after it landed on that control rather
358
+ # than under the pointer, landing true and doing nothing, until a release was sent by hand.
359
+ var notch: bool = pressed and _mask_of(button) == 0
360
+ if notch:
361
+ Input.parse_input_event(_button(position, button, false, false))
355
362
 
356
- return {
363
+ var answer: Dictionary = {
357
364
  "type": "input_injected",
358
365
  "input_type": "mouse_click",
359
366
  "position": [position.x, position.y],
@@ -361,6 +368,9 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
361
368
  "pressed": pressed,
362
369
  "double": double
363
370
  }
371
+ if notch:
372
+ answer["released"] = true
373
+ return answer
364
374
 
365
375
 
366
376
  ## Moves the pointer to a position, with the movement the event carries taken from where the
@@ -895,4 +905,7 @@ func _resolve_mouse_button(raw: Variant) -> int:
895
905
  return MOUSE_BUTTON_WHEEL_DOWN
896
906
  _:
897
907
  return -1
898
- return Read.as_int(raw, -1)
908
+ # The engine's buttons run from 1 to 9: 0 is MOUSE_BUTTON_NONE, which no event carries, and a
909
+ # number past the two extra buttons names nothing a mask bit can be made for.
910
+ var number: int = Read.as_int(raw, -1)
911
+ return number if number >= MOUSE_BUTTON_LEFT and number <= MOUSE_BUTTON_XBUTTON2 else -1