gdharness 1.0.13 → 1.0.14
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 +57 -7
- package/build/index.js +119 -72
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -31671,8 +31671,7 @@ function announcedAndGone(projectPath) {
|
|
|
31671
31671
|
return [];
|
|
31672
31672
|
}
|
|
31673
31673
|
const since = Date.now() - GONE_WINDOW_MS;
|
|
31674
|
-
|
|
31675
|
-
return gone.filter((one) => one.noticedAt >= since && resolve3(one.project) === wanted);
|
|
31674
|
+
return gone.filter((one) => one.noticedAt >= since && isSameDirectory(one.project, projectPath));
|
|
31676
31675
|
}
|
|
31677
31676
|
function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
31678
31677
|
const toJudge = candidates.filter((one) => {
|
|
@@ -31778,11 +31777,10 @@ function sweepErrorReport(path, pid, now = Date.now()) {
|
|
|
31778
31777
|
} catch {}
|
|
31779
31778
|
}
|
|
31780
31779
|
async function announcedSince(projectPath, before, waiting = {}) {
|
|
31781
|
-
const wanted = resolve3(projectPath);
|
|
31782
31780
|
const directories = waiting.directories ?? runtimeDirectories();
|
|
31783
31781
|
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
31784
31782
|
for (;; ) {
|
|
31785
|
-
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) &&
|
|
31783
|
+
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && isSameDirectory(endpoint.project.path, projectPath) && (waiting.accept?.(endpoint) ?? true));
|
|
31786
31784
|
if (fresh !== undefined) {
|
|
31787
31785
|
return fresh;
|
|
31788
31786
|
}
|
|
@@ -31803,7 +31801,7 @@ function theOneProject(endpoints) {
|
|
|
31803
31801
|
if (endpoints.some((endpoint) => endpoint.project.path === "")) {
|
|
31804
31802
|
return;
|
|
31805
31803
|
}
|
|
31806
|
-
const paths = new Set(endpoints.map((endpoint) => resolve3(endpoint.project.path)));
|
|
31804
|
+
const paths = new Set(endpoints.map((endpoint) => realPathOr(resolve3(endpoint.project.path))));
|
|
31807
31805
|
const only = [...paths][0];
|
|
31808
31806
|
return paths.size === 1 ? only : undefined;
|
|
31809
31807
|
}
|
|
@@ -31832,7 +31830,7 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
|
31832
31830
|
if (pid !== undefined) {
|
|
31833
31831
|
const named = endpoints.find((endpoint) => endpoint.pid === pid);
|
|
31834
31832
|
if (named !== undefined) {
|
|
31835
|
-
if (projectPath !== undefined &&
|
|
31833
|
+
if (projectPath !== undefined && !isSameDirectory(named.project.path, projectPath)) {
|
|
31836
31834
|
return {
|
|
31837
31835
|
problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve3(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
|
|
31838
31836
|
};
|
|
@@ -31875,7 +31873,7 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
|
31875
31873
|
}
|
|
31876
31874
|
if (projectPath !== undefined) {
|
|
31877
31875
|
const wanted = resolve3(projectPath);
|
|
31878
|
-
const matching = endpoints.filter((endpoint) =>
|
|
31876
|
+
const matching = endpoints.filter((endpoint) => isSameDirectory(endpoint.project.path, wanted));
|
|
31879
31877
|
if (matching.length === 0) {
|
|
31880
31878
|
return {
|
|
31881
31879
|
problem: `No running game is from ${wanted}. Running: ${endpoints.map(describe2).join("; ")}.`
|
|
@@ -31986,6 +31984,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
31986
31984
|
}
|
|
31987
31985
|
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;
|
|
31988
31986
|
var init_runtime_client = __esm(() => {
|
|
31987
|
+
init_paths();
|
|
31989
31988
|
init_process_children();
|
|
31990
31989
|
ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
|
|
31991
31990
|
ERROR_REPORT_PATTERN = /^runtime-(\d+)\.log$/;
|
|
@@ -41289,8 +41288,59 @@ class GodotServer {
|
|
|
41289
41288
|
}
|
|
41290
41289
|
return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
|
|
41291
41290
|
}
|
|
41291
|
+
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
41292
|
+
const run = this.currentRun();
|
|
41293
|
+
const project = projectPath ?? run?.projectPath ?? null;
|
|
41294
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync15(join22(project, RUNTIME_AUTOLOAD2.path)) || !await this.runStillGoing(run)) {
|
|
41295
|
+
return null;
|
|
41296
|
+
}
|
|
41297
|
+
const already = runtimesAnnounced().running;
|
|
41298
|
+
const announcedSinceTheStart = (one) => {
|
|
41299
|
+
if (run.announcedBefore !== undefined) {
|
|
41300
|
+
return !run.announcedBefore.has(one.pid);
|
|
41301
|
+
}
|
|
41302
|
+
try {
|
|
41303
|
+
return statSync8(one.file).mtimeMs >= run.startedAt;
|
|
41304
|
+
} catch {
|
|
41305
|
+
return false;
|
|
41306
|
+
}
|
|
41307
|
+
};
|
|
41308
|
+
if (already.some((one) => announcedSinceTheStart(one) && this.isTheGameOf(run, one, project))) {
|
|
41309
|
+
return null;
|
|
41310
|
+
}
|
|
41311
|
+
const which = run.throughEditor ? "the game the editor is playing for it" : `pid ${run.pid ?? "unknown"}`;
|
|
41312
|
+
const comingUpUntil = run.startedAt + waitSizedTo(readBootNote(project)) + budgetMs;
|
|
41313
|
+
const remaining = comingUpUntil - Date.now();
|
|
41314
|
+
if (remaining <= 0) {
|
|
41315
|
+
const seconds = Math.round((Date.now() - run.startedAt) / 1000);
|
|
41316
|
+
return `The game this server started (${which}) has been running for ${seconds}s and has not announced its runtime, so there is nothing to talk to. A game that keeps the runtime off, as a project's own loader can for some scenes, never announces; editor_output says what it has printed.`;
|
|
41317
|
+
}
|
|
41318
|
+
const endpoint = await announcedSince(project, new Set(already.map((one) => one.pid)), {
|
|
41319
|
+
budgetMs: Math.min(budgetMs, remaining),
|
|
41320
|
+
accept: (one) => this.isTheGameOf(this.currentRun(), one, project),
|
|
41321
|
+
giveUp: () => this.currentRun() !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
|
|
41322
|
+
});
|
|
41323
|
+
if (endpoint !== null) {
|
|
41324
|
+
const tiedMeanwhile = () => run.announcedPid !== undefined;
|
|
41325
|
+
if (!tiedMeanwhile()) {
|
|
41326
|
+
this.tie(run, endpoint);
|
|
41327
|
+
}
|
|
41328
|
+
return null;
|
|
41329
|
+
}
|
|
41330
|
+
if (this.currentRun() !== run || !await this.runStillGoing(run)) {
|
|
41331
|
+
return null;
|
|
41332
|
+
}
|
|
41333
|
+
return `The game this server started (${which}) is still coming up and has not announced its runtime after a wait of ${Math.min(budgetMs, remaining)}ms, so there is nothing to talk to yet. It is still running: ask again in a moment rather than starting another.`;
|
|
41334
|
+
}
|
|
41292
41335
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
41293
41336
|
const { op: _op, projectPath, pid, ...params } = asParams(args);
|
|
41337
|
+
const waited = typeof pid === "number" ? null : await this.ownRunOnItsWay(typeof projectPath === "string" ? projectPath : this.ownProject, timeoutMs);
|
|
41338
|
+
if (waited !== null) {
|
|
41339
|
+
return this.createErrorResponse(waited, [
|
|
41340
|
+
"editor_status lists it under runtimes once it has announced",
|
|
41341
|
+
"editor_output says what it has printed so far"
|
|
41342
|
+
]);
|
|
41343
|
+
}
|
|
41294
41344
|
const announced = runtimesAnnounced();
|
|
41295
41345
|
const wanted = typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined;
|
|
41296
41346
|
const own = typeof pid === "number" ? undefined : this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
|
package/build/index.js
CHANGED
|
@@ -24770,7 +24770,7 @@ import { dirname as dirname2, join as join3 } from "node:path";
|
|
|
24770
24770
|
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync, unlinkSync } from "node:fs";
|
|
24771
24771
|
import { createConnection } from "node:net";
|
|
24772
24772
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
24773
|
-
import { dirname, join as join2, resolve } from "node:path";
|
|
24773
|
+
import { dirname, join as join2, resolve as resolve2 } from "node:path";
|
|
24774
24774
|
import { setTimeout as delay } from "node:timers/promises";
|
|
24775
24775
|
|
|
24776
24776
|
// src/launch.ts
|
|
@@ -24837,6 +24837,65 @@ function environmentFor(options, variables = process.env) {
|
|
|
24837
24837
|
}
|
|
24838
24838
|
var SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
|
|
24839
24839
|
|
|
24840
|
+
// src/paths.ts
|
|
24841
|
+
import { realpathSync } from "node:fs";
|
|
24842
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
24843
|
+
var RESOURCE_SCHEME = "res://";
|
|
24844
|
+
var OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
24845
|
+
function realPathOr(path) {
|
|
24846
|
+
try {
|
|
24847
|
+
return realpathSync.native(path);
|
|
24848
|
+
} catch {
|
|
24849
|
+
return path;
|
|
24850
|
+
}
|
|
24851
|
+
}
|
|
24852
|
+
function isSameDirectory(onePath, otherPath) {
|
|
24853
|
+
return relative(realPathOr(resolve(onePath)), realPathOr(resolve(otherPath))) === "";
|
|
24854
|
+
}
|
|
24855
|
+
function isWithinRoot(rootPath, candidatePath) {
|
|
24856
|
+
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
24857
|
+
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
24858
|
+
}
|
|
24859
|
+
function resolveWithinProject(projectPath, candidatePath) {
|
|
24860
|
+
const trimmed = candidatePath.trim();
|
|
24861
|
+
if (trimmed === "") {
|
|
24862
|
+
return { ok: false, reason: "Path is empty." };
|
|
24863
|
+
}
|
|
24864
|
+
if (trimmed.includes("\x00")) {
|
|
24865
|
+
return { ok: false, reason: "Path contains a null byte." };
|
|
24866
|
+
}
|
|
24867
|
+
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
24868
|
+
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
24869
|
+
if (scheme) {
|
|
24870
|
+
return {
|
|
24871
|
+
ok: false,
|
|
24872
|
+
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
|
|
24873
|
+
};
|
|
24874
|
+
}
|
|
24875
|
+
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
24876
|
+
return {
|
|
24877
|
+
ok: false,
|
|
24878
|
+
reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
|
|
24879
|
+
};
|
|
24880
|
+
}
|
|
24881
|
+
if (withoutScheme.includes(":")) {
|
|
24882
|
+
return {
|
|
24883
|
+
ok: false,
|
|
24884
|
+
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
24885
|
+
};
|
|
24886
|
+
}
|
|
24887
|
+
const root = resolve(projectPath);
|
|
24888
|
+
const absolutePath = resolve(root, withoutScheme);
|
|
24889
|
+
if (!isWithinRoot(root, absolutePath)) {
|
|
24890
|
+
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
24891
|
+
}
|
|
24892
|
+
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
24893
|
+
if (relativePath === "") {
|
|
24894
|
+
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
24895
|
+
}
|
|
24896
|
+
return { ok: true, absolutePath, relativePath };
|
|
24897
|
+
}
|
|
24898
|
+
|
|
24840
24899
|
// src/process-children.ts
|
|
24841
24900
|
import { execFile, execFileSync, spawnSync } from "node:child_process";
|
|
24842
24901
|
import { basename } from "node:path";
|
|
@@ -25110,7 +25169,7 @@ function runtimeDirectories(variables = process.env) {
|
|
|
25110
25169
|
for (const base of fallbacks) {
|
|
25111
25170
|
candidates.push(join2(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
25112
25171
|
}
|
|
25113
|
-
return [...new Set(candidates.map((path) =>
|
|
25172
|
+
return [...new Set(candidates.map((path) => resolve2(path)))];
|
|
25114
25173
|
}
|
|
25115
25174
|
var ERROR_REPORT_PATTERN = /^runtime-(\d+)\.log$/;
|
|
25116
25175
|
function errorReportOf(pid, directories = runtimeDirectories()) {
|
|
@@ -25205,8 +25264,7 @@ function announcedAndGone(projectPath) {
|
|
|
25205
25264
|
return [];
|
|
25206
25265
|
}
|
|
25207
25266
|
const since = Date.now() - GONE_WINDOW_MS;
|
|
25208
|
-
|
|
25209
|
-
return gone.filter((one) => one.noticedAt >= since && resolve(one.project) === wanted);
|
|
25267
|
+
return gone.filter((one) => one.noticedAt >= since && isSameDirectory(one.project, projectPath));
|
|
25210
25268
|
}
|
|
25211
25269
|
var STARTED_AFTER_ANNOUNCING_MS = 2000;
|
|
25212
25270
|
var CONFIRMED_FOR_MS = 60000;
|
|
@@ -25319,11 +25377,10 @@ function sweepErrorReport(path, pid, now = Date.now()) {
|
|
|
25319
25377
|
var LOOK_EVERY_MS = 50;
|
|
25320
25378
|
var ANNOUNCE_BUDGET_MS = 5000;
|
|
25321
25379
|
async function announcedSince(projectPath, before, waiting = {}) {
|
|
25322
|
-
const wanted = resolve(projectPath);
|
|
25323
25380
|
const directories = waiting.directories ?? runtimeDirectories();
|
|
25324
25381
|
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
25325
25382
|
for (;; ) {
|
|
25326
|
-
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) &&
|
|
25383
|
+
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && isSameDirectory(endpoint.project.path, projectPath) && (waiting.accept?.(endpoint) ?? true));
|
|
25327
25384
|
if (fresh !== undefined) {
|
|
25328
25385
|
return fresh;
|
|
25329
25386
|
}
|
|
@@ -25344,7 +25401,7 @@ function theOneProject(endpoints) {
|
|
|
25344
25401
|
if (endpoints.some((endpoint) => endpoint.project.path === "")) {
|
|
25345
25402
|
return;
|
|
25346
25403
|
}
|
|
25347
|
-
const paths = new Set(endpoints.map((endpoint) =>
|
|
25404
|
+
const paths = new Set(endpoints.map((endpoint) => realPathOr(resolve2(endpoint.project.path))));
|
|
25348
25405
|
const only = [...paths][0];
|
|
25349
25406
|
return paths.size === 1 ? only : undefined;
|
|
25350
25407
|
}
|
|
@@ -25373,9 +25430,9 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
|
25373
25430
|
if (pid !== undefined) {
|
|
25374
25431
|
const named = endpoints.find((endpoint) => endpoint.pid === pid);
|
|
25375
25432
|
if (named !== undefined) {
|
|
25376
|
-
if (projectPath !== undefined &&
|
|
25433
|
+
if (projectPath !== undefined && !isSameDirectory(named.project.path, projectPath)) {
|
|
25377
25434
|
return {
|
|
25378
|
-
problem: `The game with pid ${pid} is running ${named.project.path}, not ${
|
|
25435
|
+
problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve2(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
|
|
25379
25436
|
};
|
|
25380
25437
|
}
|
|
25381
25438
|
return { endpoint: named };
|
|
@@ -25415,8 +25472,8 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
|
25415
25472
|
};
|
|
25416
25473
|
}
|
|
25417
25474
|
if (projectPath !== undefined) {
|
|
25418
|
-
const wanted =
|
|
25419
|
-
const matching = endpoints.filter((endpoint) =>
|
|
25475
|
+
const wanted = resolve2(projectPath);
|
|
25476
|
+
const matching = endpoints.filter((endpoint) => isSameDirectory(endpoint.project.path, wanted));
|
|
25420
25477
|
if (matching.length === 0) {
|
|
25421
25478
|
return {
|
|
25422
25479
|
problem: `No running game is from ${wanted}. Running: ${endpoints.map(describe2).join("; ")}.`
|
|
@@ -25560,67 +25617,6 @@ function waitSizedTo(lastBootMs, usual = ANNOUNCE_BUDGET_MS) {
|
|
|
25560
25617
|
// src/breakpoint-note.ts
|
|
25561
25618
|
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
25562
25619
|
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, relative as relative2 } from "node:path";
|
|
25563
|
-
|
|
25564
|
-
// src/paths.ts
|
|
25565
|
-
import { realpathSync } from "node:fs";
|
|
25566
|
-
import { isAbsolute, relative, resolve as resolve2, sep } from "node:path";
|
|
25567
|
-
var RESOURCE_SCHEME = "res://";
|
|
25568
|
-
var OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
25569
|
-
function realPathOr(path) {
|
|
25570
|
-
try {
|
|
25571
|
-
return realpathSync.native(path);
|
|
25572
|
-
} catch {
|
|
25573
|
-
return path;
|
|
25574
|
-
}
|
|
25575
|
-
}
|
|
25576
|
-
function isSameDirectory(onePath, otherPath) {
|
|
25577
|
-
return relative(realPathOr(resolve2(onePath)), realPathOr(resolve2(otherPath))) === "";
|
|
25578
|
-
}
|
|
25579
|
-
function isWithinRoot(rootPath, candidatePath) {
|
|
25580
|
-
const step = relative(resolve2(rootPath), resolve2(candidatePath));
|
|
25581
|
-
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
25582
|
-
}
|
|
25583
|
-
function resolveWithinProject(projectPath, candidatePath) {
|
|
25584
|
-
const trimmed = candidatePath.trim();
|
|
25585
|
-
if (trimmed === "") {
|
|
25586
|
-
return { ok: false, reason: "Path is empty." };
|
|
25587
|
-
}
|
|
25588
|
-
if (trimmed.includes("\x00")) {
|
|
25589
|
-
return { ok: false, reason: "Path contains a null byte." };
|
|
25590
|
-
}
|
|
25591
|
-
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
25592
|
-
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
25593
|
-
if (scheme) {
|
|
25594
|
-
return {
|
|
25595
|
-
ok: false,
|
|
25596
|
-
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
|
|
25597
|
-
};
|
|
25598
|
-
}
|
|
25599
|
-
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
25600
|
-
return {
|
|
25601
|
-
ok: false,
|
|
25602
|
-
reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
|
|
25603
|
-
};
|
|
25604
|
-
}
|
|
25605
|
-
if (withoutScheme.includes(":")) {
|
|
25606
|
-
return {
|
|
25607
|
-
ok: false,
|
|
25608
|
-
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
25609
|
-
};
|
|
25610
|
-
}
|
|
25611
|
-
const root = resolve2(projectPath);
|
|
25612
|
-
const absolutePath = resolve2(root, withoutScheme);
|
|
25613
|
-
if (!isWithinRoot(root, absolutePath)) {
|
|
25614
|
-
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
25615
|
-
}
|
|
25616
|
-
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
25617
|
-
if (relativePath === "") {
|
|
25618
|
-
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
25619
|
-
}
|
|
25620
|
-
return { ok: true, absolutePath, relativePath };
|
|
25621
|
-
}
|
|
25622
|
-
|
|
25623
|
-
// src/breakpoint-note.ts
|
|
25624
25620
|
function breakpointNotePath(projectPath) {
|
|
25625
25621
|
return join4(projectPath, ".godot", "gdharness-breakpoints.json");
|
|
25626
25622
|
}
|
|
@@ -34030,8 +34026,59 @@ class GodotServer {
|
|
|
34030
34026
|
}
|
|
34031
34027
|
return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
|
|
34032
34028
|
}
|
|
34029
|
+
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
34030
|
+
const run = this.currentRun();
|
|
34031
|
+
const project = projectPath ?? run?.projectPath ?? null;
|
|
34032
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync10(join16(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
34033
|
+
return null;
|
|
34034
|
+
}
|
|
34035
|
+
const already = runtimesAnnounced().running;
|
|
34036
|
+
const announcedSinceTheStart = (one) => {
|
|
34037
|
+
if (run.announcedBefore !== undefined) {
|
|
34038
|
+
return !run.announcedBefore.has(one.pid);
|
|
34039
|
+
}
|
|
34040
|
+
try {
|
|
34041
|
+
return statSync7(one.file).mtimeMs >= run.startedAt;
|
|
34042
|
+
} catch {
|
|
34043
|
+
return false;
|
|
34044
|
+
}
|
|
34045
|
+
};
|
|
34046
|
+
if (already.some((one) => announcedSinceTheStart(one) && this.isTheGameOf(run, one, project))) {
|
|
34047
|
+
return null;
|
|
34048
|
+
}
|
|
34049
|
+
const which = run.throughEditor ? "the game the editor is playing for it" : `pid ${run.pid ?? "unknown"}`;
|
|
34050
|
+
const comingUpUntil = run.startedAt + waitSizedTo(readBootNote(project)) + budgetMs;
|
|
34051
|
+
const remaining = comingUpUntil - Date.now();
|
|
34052
|
+
if (remaining <= 0) {
|
|
34053
|
+
const seconds = Math.round((Date.now() - run.startedAt) / 1000);
|
|
34054
|
+
return `The game this server started (${which}) has been running for ${seconds}s and has not announced its runtime, so there is nothing to talk to. A game that keeps the runtime off, as a project's own loader can for some scenes, never announces; editor_output says what it has printed.`;
|
|
34055
|
+
}
|
|
34056
|
+
const endpoint = await announcedSince(project, new Set(already.map((one) => one.pid)), {
|
|
34057
|
+
budgetMs: Math.min(budgetMs, remaining),
|
|
34058
|
+
accept: (one) => this.isTheGameOf(this.currentRun(), one, project),
|
|
34059
|
+
giveUp: () => this.currentRun() !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
|
|
34060
|
+
});
|
|
34061
|
+
if (endpoint !== null) {
|
|
34062
|
+
const tiedMeanwhile = () => run.announcedPid !== undefined;
|
|
34063
|
+
if (!tiedMeanwhile()) {
|
|
34064
|
+
this.tie(run, endpoint);
|
|
34065
|
+
}
|
|
34066
|
+
return null;
|
|
34067
|
+
}
|
|
34068
|
+
if (this.currentRun() !== run || !await this.runStillGoing(run)) {
|
|
34069
|
+
return null;
|
|
34070
|
+
}
|
|
34071
|
+
return `The game this server started (${which}) is still coming up and has not announced its runtime after a wait of ${Math.min(budgetMs, remaining)}ms, so there is nothing to talk to yet. It is still running: ask again in a moment rather than starting another.`;
|
|
34072
|
+
}
|
|
34033
34073
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
34034
34074
|
const { op: _op, projectPath, pid, ...params } = asParams(args);
|
|
34075
|
+
const waited = typeof pid === "number" ? null : await this.ownRunOnItsWay(typeof projectPath === "string" ? projectPath : this.ownProject, timeoutMs);
|
|
34076
|
+
if (waited !== null) {
|
|
34077
|
+
return this.createErrorResponse(waited, [
|
|
34078
|
+
"editor_status lists it under runtimes once it has announced",
|
|
34079
|
+
"editor_output says what it has printed so far"
|
|
34080
|
+
]);
|
|
34081
|
+
}
|
|
34035
34082
|
const announced = runtimesAnnounced();
|
|
34036
34083
|
const wanted = typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined;
|
|
34037
34084
|
const own = typeof pid === "number" ? undefined : this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
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",
|