gdharness 1.0.8 → 1.0.10
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 +88 -14
- package/build/index.js +83 -9
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -201,7 +201,11 @@ function userDataIn(home, variables = process.env) {
|
|
|
201
201
|
function savesStayPut(platform = process.platform) {
|
|
202
202
|
return platform === "darwin";
|
|
203
203
|
}
|
|
204
|
-
|
|
204
|
+
function environmentFor(options, variables = process.env) {
|
|
205
|
+
const base = options.savesIn === undefined ? { ...variables } : userDataIn(options.savesIn, variables);
|
|
206
|
+
return { ...base, ...options.env, GDHARNESS_RUNTIME_DIR: options.runtimeDirectory };
|
|
207
|
+
}
|
|
208
|
+
var OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", RESERVED_VARIABLE_PREFIX = "GDHARNESS_", 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.";
|
|
205
209
|
|
|
206
210
|
// node_modules/yaml/dist/nodes/identity.js
|
|
207
211
|
var require_identity = __commonJS(function(exports) {
|
|
@@ -16012,6 +16016,16 @@ var init_tool_definitions = __esm(() => {
|
|
|
16012
16016
|
description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
|
|
16013
16017
|
},
|
|
16014
16018
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
16019
|
+
savesIn: {
|
|
16020
|
+
type: "string",
|
|
16021
|
+
ops: ["start", "check"],
|
|
16022
|
+
description: "An absolute directory for this run's user://, so a game that saves writes nowhere near the copy somebody plays: opening a save writes the one being left, and a game that autosaves writes on its own. Godot takes no flag for it, so this is the environment, and the run is started by this server rather than played by the editor, which cannot be given one. It does not have to exist: the engine makes it, and the Godot/app_userdata/<project> tree under it, measured on 4.7.2. On macOS the engine reads user:// off HOME and ignores it, also measured, and the answer says so under savesNote rather than leaving the caller to find out from the player's save list."
|
|
16023
|
+
},
|
|
16024
|
+
env: {
|
|
16025
|
+
type: "object",
|
|
16026
|
+
ops: ["start", "check"],
|
|
16027
|
+
description: "Environment variables for this run alone, as a map of strings, over the server's own environment. Like savesIn, a run carrying any is started by this server rather than played by the editor. Names beginning with GDHARNESS_ are refused: they are this server's contract with the addon in the game, and the runtime directory is set here whatever the rest of the environment says, so a game that moves TEMP still announces where this server looks."
|
|
16028
|
+
},
|
|
16015
16029
|
runtimeWaitMs: {
|
|
16016
16030
|
type: "number",
|
|
16017
16031
|
ops: ["start"],
|
|
@@ -31425,7 +31439,7 @@ async function askWindows() {
|
|
|
31425
31439
|
"-NoProfile",
|
|
31426
31440
|
"-NonInteractive",
|
|
31427
31441
|
"-Command",
|
|
31428
|
-
|
|
31442
|
+
`${POWERSHELL_UTF8}Get-CimInstance Win32_Process | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }`
|
|
31429
31443
|
], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
|
|
31430
31444
|
return stdout;
|
|
31431
31445
|
}
|
|
@@ -31436,7 +31450,7 @@ async function askPosix() {
|
|
|
31436
31450
|
});
|
|
31437
31451
|
return stdout;
|
|
31438
31452
|
}
|
|
31439
|
-
var run4, ASK_TIMEOUT_MS = 15000;
|
|
31453
|
+
var run4, POWERSHELL_UTF8 = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ", ASK_TIMEOUT_MS = 15000;
|
|
31440
31454
|
var init_process_children = __esm(() => {
|
|
31441
31455
|
run4 = promisify4(execFile4);
|
|
31442
31456
|
});
|
|
@@ -37518,7 +37532,7 @@ function windowsCommandLine(pid) {
|
|
|
37518
37532
|
"-NoProfile",
|
|
37519
37533
|
"-NonInteractive",
|
|
37520
37534
|
"-Command",
|
|
37521
|
-
`$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`
|
|
37535
|
+
`${POWERSHELL_UTF8}$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`
|
|
37522
37536
|
], { encoding: "utf8", timeout: 15000, windowsHide: true }).trim();
|
|
37523
37537
|
return answer === "" ? null : answer;
|
|
37524
37538
|
} catch {
|
|
@@ -37627,6 +37641,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
37627
37641
|
}
|
|
37628
37642
|
var AN_EDITOR, SAME_RUN_WINDOW_MS = 1e4;
|
|
37629
37643
|
var init_run_record = __esm(() => {
|
|
37644
|
+
init_process_children();
|
|
37630
37645
|
init_runtime_client();
|
|
37631
37646
|
AN_EDITOR = /(?:^|\s)(?:-e|--editor)(?:\s|$)/;
|
|
37632
37647
|
});
|
|
@@ -37645,10 +37660,11 @@ import {
|
|
|
37645
37660
|
readdirSync as readdirSync7,
|
|
37646
37661
|
readFileSync as readFileSync19,
|
|
37647
37662
|
readSync,
|
|
37663
|
+
rmdirSync,
|
|
37648
37664
|
statSync as statSync5
|
|
37649
37665
|
} from "node:fs";
|
|
37650
37666
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
37651
|
-
import { basename as basename4, dirname as dirname14, join as join21, normalize as normalize3, relative as relative3 } from "node:path";
|
|
37667
|
+
import { basename as basename4, dirname as dirname14, isAbsolute as isAbsolute3, join as join21, normalize as normalize3, relative as relative3 } from "node:path";
|
|
37652
37668
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
37653
37669
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
37654
37670
|
import { promisify as promisify7 } from "node:util";
|
|
@@ -37930,6 +37946,9 @@ function camelCased(params) {
|
|
|
37930
37946
|
}
|
|
37931
37947
|
return result;
|
|
37932
37948
|
}
|
|
37949
|
+
function needsItsOwnProcess(asked) {
|
|
37950
|
+
return asked.savesIn !== undefined || asked.env !== undefined;
|
|
37951
|
+
}
|
|
37933
37952
|
|
|
37934
37953
|
class GodotServer {
|
|
37935
37954
|
mcp;
|
|
@@ -38625,6 +38644,51 @@ class GodotServer {
|
|
|
38625
38644
|
}
|
|
38626
38645
|
return { ok: true, value: given };
|
|
38627
38646
|
}
|
|
38647
|
+
runEnvironment(args) {
|
|
38648
|
+
const savesIn = readNonEmptyString(args, "savesIn");
|
|
38649
|
+
if (savesIn !== undefined && !isAbsolute3(savesIn)) {
|
|
38650
|
+
return {
|
|
38651
|
+
ok: false,
|
|
38652
|
+
response: this.createErrorResponse(`savesIn must be an absolute path, not "${savesIn}".`, [
|
|
38653
|
+
"Name the directory user:// should be under, such as a temporary directory of this run's own"
|
|
38654
|
+
])
|
|
38655
|
+
};
|
|
38656
|
+
}
|
|
38657
|
+
if (args["env"] === undefined) {
|
|
38658
|
+
return { ok: true, value: savesIn === undefined ? {} : { savesIn } };
|
|
38659
|
+
}
|
|
38660
|
+
const given = readParams(args, "env");
|
|
38661
|
+
if (given === undefined) {
|
|
38662
|
+
return {
|
|
38663
|
+
ok: false,
|
|
38664
|
+
response: this.createErrorResponse("env must be a map of names to string values.", [
|
|
38665
|
+
'Pass the variables this run needs, such as {"MY_FLAG": "1"}'
|
|
38666
|
+
])
|
|
38667
|
+
};
|
|
38668
|
+
}
|
|
38669
|
+
const env = {};
|
|
38670
|
+
for (const [name, value] of Object.entries(given)) {
|
|
38671
|
+
if (typeof value !== "string") {
|
|
38672
|
+
return {
|
|
38673
|
+
ok: false,
|
|
38674
|
+
response: this.createErrorResponse(`env values must be strings, and ${name} is ${typeof value}.`, [
|
|
38675
|
+
'Pass every value as a string, such as {"WORKERS": "4"}'
|
|
38676
|
+
])
|
|
38677
|
+
};
|
|
38678
|
+
}
|
|
38679
|
+
if (name.toUpperCase().startsWith(RESERVED_VARIABLE_PREFIX)) {
|
|
38680
|
+
return {
|
|
38681
|
+
ok: false,
|
|
38682
|
+
response: this.createErrorResponse(`env may not set ${name}: names beginning with ${RESERVED_VARIABLE_PREFIX} are this server's own.`, [
|
|
38683
|
+
"GDHARNESS_RUNTIME_DIR is where the game announces itself and this server looks for it",
|
|
38684
|
+
"Name the directory for user:// with savesIn rather than by moving the environment yourself"
|
|
38685
|
+
])
|
|
38686
|
+
};
|
|
38687
|
+
}
|
|
38688
|
+
env[name] = value;
|
|
38689
|
+
}
|
|
38690
|
+
return { ok: true, value: { ...savesIn === undefined ? {} : { savesIn }, env } };
|
|
38691
|
+
}
|
|
38628
38692
|
async debuggedGame(intent) {
|
|
38629
38693
|
const game = this.activeProcess;
|
|
38630
38694
|
if (game !== null && game.exitCode !== null) {
|
|
@@ -39017,6 +39081,9 @@ class GodotServer {
|
|
|
39017
39081
|
reportProblem = errorMessage(error);
|
|
39018
39082
|
} finally {
|
|
39019
39083
|
discard(reportsDir, userData);
|
|
39084
|
+
try {
|
|
39085
|
+
rmdirSync(dirname14(reportsDir));
|
|
39086
|
+
} catch {}
|
|
39020
39087
|
}
|
|
39021
39088
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
39022
39089
|
const verdicts = {
|
|
@@ -39609,6 +39676,10 @@ class GodotServer {
|
|
|
39609
39676
|
if (!given.ok) {
|
|
39610
39677
|
return given.response;
|
|
39611
39678
|
}
|
|
39679
|
+
const asked = this.runEnvironment(args);
|
|
39680
|
+
if (!asked.ok) {
|
|
39681
|
+
return asked.response;
|
|
39682
|
+
}
|
|
39612
39683
|
if (!sceneToRun && !hasMainScene(project.value.file)) {
|
|
39613
39684
|
return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
|
|
39614
39685
|
"Pass scene to run one scene",
|
|
@@ -39625,7 +39696,7 @@ class GodotServer {
|
|
|
39625
39696
|
}
|
|
39626
39697
|
const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
|
|
39627
39698
|
if (op === "check") {
|
|
39628
|
-
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
|
|
39699
|
+
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, asked.value);
|
|
39629
39700
|
}
|
|
39630
39701
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
39631
39702
|
let ended = null;
|
|
@@ -39643,7 +39714,7 @@ class GodotServer {
|
|
|
39643
39714
|
});
|
|
39644
39715
|
const runtimeWaitMs = this.announceBudget(args, project.value.path);
|
|
39645
39716
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
39646
|
-
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0) {
|
|
39717
|
+
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value)) {
|
|
39647
39718
|
return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
|
|
39648
39719
|
}
|
|
39649
39720
|
const cmdArgs = runArguments({
|
|
@@ -39653,10 +39724,10 @@ class GodotServer {
|
|
|
39653
39724
|
userArgs: given.value
|
|
39654
39725
|
});
|
|
39655
39726
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
39656
|
-
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
|
|
39727
|
+
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined);
|
|
39657
39728
|
started.announcedBefore = alreadyPlaying;
|
|
39658
39729
|
this.activeProcess = started;
|
|
39659
|
-
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." : "";
|
|
39730
|
+
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." : "";
|
|
39660
39731
|
const endedForThis = endedToStartThis(ended);
|
|
39661
39732
|
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
39662
39733
|
const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
|
|
@@ -39669,6 +39740,7 @@ class GodotServer {
|
|
|
39669
39740
|
arguments: cmdArgs,
|
|
39670
39741
|
transcript: started.transcript,
|
|
39671
39742
|
refreshedClasses: refreshed.value,
|
|
39743
|
+
savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
39672
39744
|
endedPreviousRun: endedPreviousRun(ended),
|
|
39673
39745
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
39674
39746
|
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
@@ -40275,11 +40347,11 @@ class GodotServer {
|
|
|
40275
40347
|
currentRun() {
|
|
40276
40348
|
return this.activeProcess ?? this.adoptRecordedRun();
|
|
40277
40349
|
}
|
|
40278
|
-
async checkBoot(godotPath, projectPath, scene, args, userArgs) {
|
|
40350
|
+
async checkBoot(godotPath, projectPath, scene, args, userArgs, asked = {}) {
|
|
40279
40351
|
const frames = readPositiveNumber(args, "frames") ?? 3;
|
|
40280
40352
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
|
|
40281
40353
|
const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames, userArgs });
|
|
40282
|
-
const boot = this.spawnGame(godotPath, cmdArgs);
|
|
40354
|
+
const boot = this.spawnGame(godotPath, cmdArgs, needsItsOwnProcess(asked) ? environmentFor({ ...asked, runtimeDirectory: runtimeDirectory() }) : undefined);
|
|
40283
40355
|
const hung = await new Promise((resolve) => {
|
|
40284
40356
|
const timer = setTimeout(() => {
|
|
40285
40357
|
boot.process.kill();
|
|
@@ -40304,6 +40376,7 @@ class GodotServer {
|
|
|
40304
40376
|
frames,
|
|
40305
40377
|
errors,
|
|
40306
40378
|
warnings,
|
|
40379
|
+
savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
40307
40380
|
entries: forAnswer(boot.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
40308
40381
|
});
|
|
40309
40382
|
}
|
|
@@ -41638,7 +41711,7 @@ function displayPath(harness, platform = process3.platform) {
|
|
|
41638
41711
|
return `${harness.scope === "home" ? "~/" : ""}${file.replaceAll("\\", "/")}`;
|
|
41639
41712
|
}
|
|
41640
41713
|
var OURS = ["GODOT_PATH", "GDHARNESS_PROJECT"];
|
|
41641
|
-
function
|
|
41714
|
+
function environmentFor2(launch, held) {
|
|
41642
41715
|
const kept = {};
|
|
41643
41716
|
if (typeof held === "object" && held !== null && !Array.isArray(held)) {
|
|
41644
41717
|
for (const [name, value] of Object.entries(held)) {
|
|
@@ -41656,7 +41729,7 @@ function entryFor(shape, launch, held) {
|
|
|
41656
41729
|
...before,
|
|
41657
41730
|
type: "local",
|
|
41658
41731
|
command: [launch.command, ...launch.args],
|
|
41659
|
-
environment:
|
|
41732
|
+
environment: environmentFor2(launch, before["environment"]),
|
|
41660
41733
|
enabled: true
|
|
41661
41734
|
};
|
|
41662
41735
|
}
|
|
@@ -41665,7 +41738,7 @@ function entryFor(shape, launch, held) {
|
|
|
41665
41738
|
...shape === "typed" ? { type: "stdio" } : {},
|
|
41666
41739
|
command: launch.command,
|
|
41667
41740
|
args: [...launch.args],
|
|
41668
|
-
env:
|
|
41741
|
+
env: environmentFor2(launch, before["env"])
|
|
41669
41742
|
};
|
|
41670
41743
|
}
|
|
41671
41744
|
function merged(existing, harness, launch) {
|
|
@@ -42484,6 +42557,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
42484
42557
|
| Whether this engine has a method or property, and what it takes | \`editor_classes info\` with \`member\`, which answers that one member and which class declares it rather than the whole class |
|
|
42485
42558
|
| Wait for a run to finish | \`editor_run wait\`, never a sleep or a shell loop on the pid: it answers as \`editor_output\` does, once the run is over |
|
|
42486
42559
|
| End a bench and the workers it opened | \`editor_run stop\` with \`andChildren\`, never a process listing by scene name: it ends every process under the run's, to any depth, that is a game of the project, announced as one or the project's engine run with \`--path\` on it, names them under \`endedChildren\` with what identified each, and leaves and names any other |
|
|
42560
|
+
| Play a game without touching the player's saves | \`editor_run start\` with \`savesIn\`, a directory for this run's \`user://\`: the editor plays with its own environment, so a run given one is started by the server, and the answer says under \`savesNote\` when the platform ignores it |
|
|
42487
42561
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
42488
42562
|
|
|
42489
42563
|
These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
|
package/build/index.js
CHANGED
|
@@ -10181,10 +10181,11 @@ import {
|
|
|
10181
10181
|
readdirSync as readdirSync6,
|
|
10182
10182
|
readFileSync as readFileSync15,
|
|
10183
10183
|
readSync,
|
|
10184
|
+
rmdirSync,
|
|
10184
10185
|
statSync as statSync5
|
|
10185
10186
|
} from "node:fs";
|
|
10186
10187
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10187
|
-
import { basename as basename3, dirname as dirname9, join as join15, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10188
|
+
import { basename as basename3, dirname as dirname9, isAbsolute as isAbsolute3, join as join15, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10188
10189
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10189
10190
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10190
10191
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -24810,6 +24811,11 @@ function userDataIn(home, variables = process.env) {
|
|
|
24810
24811
|
function savesStayPut(platform = process.platform) {
|
|
24811
24812
|
return platform === "darwin";
|
|
24812
24813
|
}
|
|
24814
|
+
var RESERVED_VARIABLE_PREFIX = "GDHARNESS_";
|
|
24815
|
+
function environmentFor(options, variables = process.env) {
|
|
24816
|
+
const base = options.savesIn === undefined ? { ...variables } : userDataIn(options.savesIn, variables);
|
|
24817
|
+
return { ...base, ...options.env, GDHARNESS_RUNTIME_DIR: options.runtimeDirectory };
|
|
24818
|
+
}
|
|
24813
24819
|
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.";
|
|
24814
24820
|
|
|
24815
24821
|
// src/process-children.ts
|
|
@@ -24817,6 +24823,7 @@ import { execFile, execFileSync, spawnSync } from "node:child_process";
|
|
|
24817
24823
|
import { basename } from "node:path";
|
|
24818
24824
|
import { promisify } from "node:util";
|
|
24819
24825
|
var run = promisify(execFile);
|
|
24826
|
+
var POWERSHELL_UTF8 = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ";
|
|
24820
24827
|
function startTimesOf(pids) {
|
|
24821
24828
|
const began = new Map;
|
|
24822
24829
|
if (pids.length === 0) {
|
|
@@ -24999,7 +25006,7 @@ async function askWindows() {
|
|
|
24999
25006
|
"-NoProfile",
|
|
25000
25007
|
"-NonInteractive",
|
|
25001
25008
|
"-Command",
|
|
25002
|
-
|
|
25009
|
+
`${POWERSHELL_UTF8}Get-CimInstance Win32_Process | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }`
|
|
25003
25010
|
], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
|
|
25004
25011
|
return stdout;
|
|
25005
25012
|
}
|
|
@@ -29059,7 +29066,7 @@ function windowsCommandLine(pid) {
|
|
|
29059
29066
|
"-NoProfile",
|
|
29060
29067
|
"-NonInteractive",
|
|
29061
29068
|
"-Command",
|
|
29062
|
-
`$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`
|
|
29069
|
+
`${POWERSHELL_UTF8}$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`
|
|
29063
29070
|
], { encoding: "utf8", timeout: 15000, windowsHide: true }).trim();
|
|
29064
29071
|
return answer === "" ? null : answer;
|
|
29065
29072
|
} catch {
|
|
@@ -29817,6 +29824,16 @@ var TOOL_SPECS = [
|
|
|
29817
29824
|
description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
|
|
29818
29825
|
},
|
|
29819
29826
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
29827
|
+
savesIn: {
|
|
29828
|
+
type: "string",
|
|
29829
|
+
ops: ["start", "check"],
|
|
29830
|
+
description: "An absolute directory for this run's user://, so a game that saves writes nowhere near the copy somebody plays: opening a save writes the one being left, and a game that autosaves writes on its own. Godot takes no flag for it, so this is the environment, and the run is started by this server rather than played by the editor, which cannot be given one. It does not have to exist: the engine makes it, and the Godot/app_userdata/<project> tree under it, measured on 4.7.2. On macOS the engine reads user:// off HOME and ignores it, also measured, and the answer says so under savesNote rather than leaving the caller to find out from the player's save list."
|
|
29831
|
+
},
|
|
29832
|
+
env: {
|
|
29833
|
+
type: "object",
|
|
29834
|
+
ops: ["start", "check"],
|
|
29835
|
+
description: "Environment variables for this run alone, as a map of strings, over the server's own environment. Like savesIn, a run carrying any is started by this server rather than played by the editor. Names beginning with GDHARNESS_ are refused: they are this server's contract with the addon in the game, and the runtime directory is set here whatever the rest of the environment says, so a game that moves TEMP still announces where this server looks."
|
|
29836
|
+
},
|
|
29820
29837
|
runtimeWaitMs: {
|
|
29821
29838
|
type: "number",
|
|
29822
29839
|
ops: ["start"],
|
|
@@ -30667,6 +30684,9 @@ function camelCased(params) {
|
|
|
30667
30684
|
}
|
|
30668
30685
|
return result;
|
|
30669
30686
|
}
|
|
30687
|
+
function needsItsOwnProcess(asked) {
|
|
30688
|
+
return asked.savesIn !== undefined || asked.env !== undefined;
|
|
30689
|
+
}
|
|
30670
30690
|
|
|
30671
30691
|
class GodotServer {
|
|
30672
30692
|
mcp;
|
|
@@ -31362,6 +31382,51 @@ class GodotServer {
|
|
|
31362
31382
|
}
|
|
31363
31383
|
return { ok: true, value: given };
|
|
31364
31384
|
}
|
|
31385
|
+
runEnvironment(args) {
|
|
31386
|
+
const savesIn = readNonEmptyString(args, "savesIn");
|
|
31387
|
+
if (savesIn !== undefined && !isAbsolute3(savesIn)) {
|
|
31388
|
+
return {
|
|
31389
|
+
ok: false,
|
|
31390
|
+
response: this.createErrorResponse(`savesIn must be an absolute path, not "${savesIn}".`, [
|
|
31391
|
+
"Name the directory user:// should be under, such as a temporary directory of this run's own"
|
|
31392
|
+
])
|
|
31393
|
+
};
|
|
31394
|
+
}
|
|
31395
|
+
if (args["env"] === undefined) {
|
|
31396
|
+
return { ok: true, value: savesIn === undefined ? {} : { savesIn } };
|
|
31397
|
+
}
|
|
31398
|
+
const given = readParams(args, "env");
|
|
31399
|
+
if (given === undefined) {
|
|
31400
|
+
return {
|
|
31401
|
+
ok: false,
|
|
31402
|
+
response: this.createErrorResponse("env must be a map of names to string values.", [
|
|
31403
|
+
'Pass the variables this run needs, such as {"MY_FLAG": "1"}'
|
|
31404
|
+
])
|
|
31405
|
+
};
|
|
31406
|
+
}
|
|
31407
|
+
const env = {};
|
|
31408
|
+
for (const [name, value] of Object.entries(given)) {
|
|
31409
|
+
if (typeof value !== "string") {
|
|
31410
|
+
return {
|
|
31411
|
+
ok: false,
|
|
31412
|
+
response: this.createErrorResponse(`env values must be strings, and ${name} is ${typeof value}.`, [
|
|
31413
|
+
'Pass every value as a string, such as {"WORKERS": "4"}'
|
|
31414
|
+
])
|
|
31415
|
+
};
|
|
31416
|
+
}
|
|
31417
|
+
if (name.toUpperCase().startsWith(RESERVED_VARIABLE_PREFIX)) {
|
|
31418
|
+
return {
|
|
31419
|
+
ok: false,
|
|
31420
|
+
response: this.createErrorResponse(`env may not set ${name}: names beginning with ${RESERVED_VARIABLE_PREFIX} are this server's own.`, [
|
|
31421
|
+
"GDHARNESS_RUNTIME_DIR is where the game announces itself and this server looks for it",
|
|
31422
|
+
"Name the directory for user:// with savesIn rather than by moving the environment yourself"
|
|
31423
|
+
])
|
|
31424
|
+
};
|
|
31425
|
+
}
|
|
31426
|
+
env[name] = value;
|
|
31427
|
+
}
|
|
31428
|
+
return { ok: true, value: { ...savesIn === undefined ? {} : { savesIn }, env } };
|
|
31429
|
+
}
|
|
31365
31430
|
async debuggedGame(intent) {
|
|
31366
31431
|
const game = this.activeProcess;
|
|
31367
31432
|
if (game !== null && game.exitCode !== null) {
|
|
@@ -31754,6 +31819,9 @@ class GodotServer {
|
|
|
31754
31819
|
reportProblem = errorMessage(error);
|
|
31755
31820
|
} finally {
|
|
31756
31821
|
discard(reportsDir, userData);
|
|
31822
|
+
try {
|
|
31823
|
+
rmdirSync(dirname9(reportsDir));
|
|
31824
|
+
} catch {}
|
|
31757
31825
|
}
|
|
31758
31826
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
31759
31827
|
const verdicts = {
|
|
@@ -32346,6 +32414,10 @@ class GodotServer {
|
|
|
32346
32414
|
if (!given.ok) {
|
|
32347
32415
|
return given.response;
|
|
32348
32416
|
}
|
|
32417
|
+
const asked = this.runEnvironment(args);
|
|
32418
|
+
if (!asked.ok) {
|
|
32419
|
+
return asked.response;
|
|
32420
|
+
}
|
|
32349
32421
|
if (!sceneToRun && !hasMainScene(project.value.file)) {
|
|
32350
32422
|
return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
|
|
32351
32423
|
"Pass scene to run one scene",
|
|
@@ -32362,7 +32434,7 @@ class GodotServer {
|
|
|
32362
32434
|
}
|
|
32363
32435
|
const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
|
|
32364
32436
|
if (op === "check") {
|
|
32365
|
-
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
|
|
32437
|
+
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, asked.value);
|
|
32366
32438
|
}
|
|
32367
32439
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
32368
32440
|
let ended = null;
|
|
@@ -32380,7 +32452,7 @@ class GodotServer {
|
|
|
32380
32452
|
});
|
|
32381
32453
|
const runtimeWaitMs = this.announceBudget(args, project.value.path);
|
|
32382
32454
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
32383
|
-
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0) {
|
|
32455
|
+
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value)) {
|
|
32384
32456
|
return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
|
|
32385
32457
|
}
|
|
32386
32458
|
const cmdArgs = runArguments({
|
|
@@ -32390,10 +32462,10 @@ class GodotServer {
|
|
|
32390
32462
|
userArgs: given.value
|
|
32391
32463
|
});
|
|
32392
32464
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
32393
|
-
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
|
|
32465
|
+
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined);
|
|
32394
32466
|
started.announcedBefore = alreadyPlaying;
|
|
32395
32467
|
this.activeProcess = started;
|
|
32396
|
-
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." : "";
|
|
32468
|
+
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." : "";
|
|
32397
32469
|
const endedForThis = endedToStartThis(ended);
|
|
32398
32470
|
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
32399
32471
|
const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
|
|
@@ -32406,6 +32478,7 @@ class GodotServer {
|
|
|
32406
32478
|
arguments: cmdArgs,
|
|
32407
32479
|
transcript: started.transcript,
|
|
32408
32480
|
refreshedClasses: refreshed.value,
|
|
32481
|
+
savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
32409
32482
|
endedPreviousRun: endedPreviousRun(ended),
|
|
32410
32483
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
32411
32484
|
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
@@ -33012,11 +33085,11 @@ class GodotServer {
|
|
|
33012
33085
|
currentRun() {
|
|
33013
33086
|
return this.activeProcess ?? this.adoptRecordedRun();
|
|
33014
33087
|
}
|
|
33015
|
-
async checkBoot(godotPath, projectPath, scene, args, userArgs) {
|
|
33088
|
+
async checkBoot(godotPath, projectPath, scene, args, userArgs, asked = {}) {
|
|
33016
33089
|
const frames = readPositiveNumber(args, "frames") ?? 3;
|
|
33017
33090
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
|
|
33018
33091
|
const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames, userArgs });
|
|
33019
|
-
const boot = this.spawnGame(godotPath, cmdArgs);
|
|
33092
|
+
const boot = this.spawnGame(godotPath, cmdArgs, needsItsOwnProcess(asked) ? environmentFor({ ...asked, runtimeDirectory: runtimeDirectory() }) : undefined);
|
|
33020
33093
|
const hung = await new Promise((resolve) => {
|
|
33021
33094
|
const timer = setTimeout(() => {
|
|
33022
33095
|
boot.process.kill();
|
|
@@ -33041,6 +33114,7 @@ class GodotServer {
|
|
|
33041
33114
|
frames,
|
|
33042
33115
|
errors,
|
|
33043
33116
|
warnings,
|
|
33117
|
+
savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
33044
33118
|
entries: forAnswer(boot.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
33045
33119
|
});
|
|
33046
33120
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
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",
|