gdharness 1.1.24 → 1.1.26
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 +31 -6
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +67 -34
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd +39 -8
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +8 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +7 -3
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +27 -5
- package/build/index.js +31 -6
- package/build/keeper.js +3 -1
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -17556,7 +17556,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17556
17556
|
requires: ["projectPath"]
|
|
17557
17557
|
},
|
|
17558
17558
|
stop: {
|
|
17559
|
-
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. A game that started processes of its own keeps them unless andChildren says to end those too. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, and false with the number under stillGoing when what was told to end was still running ten seconds later",
|
|
17559
|
+
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. A game that started processes of its own keeps them unless andChildren says to end those too. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause",
|
|
17560
17560
|
requires: []
|
|
17561
17561
|
},
|
|
17562
17562
|
check: {
|
|
@@ -17874,7 +17874,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17874
17874
|
},
|
|
17875
17875
|
{
|
|
17876
17876
|
name: "runtime_input",
|
|
17877
|
-
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
|
|
17877
|
+
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: the pointer ops answer with pointer_note there, because get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position.",
|
|
17878
17878
|
parameters: {
|
|
17879
17879
|
projectPath: RUNNING_PROJECT_PATH,
|
|
17880
17880
|
pid: RUNNING_PID,
|
|
@@ -17914,7 +17914,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17914
17914
|
type: "string",
|
|
17915
17915
|
ops: ["text", "choose"],
|
|
17916
17916
|
blank: true,
|
|
17917
|
-
description: 'text: what to type. A newline is Enter and a tab is Tab, a space is a space, and "" with replace empties the field. choose: the item to take, by what it says as drawn, which in a game with translations is the translation; the key it holds is matched after that.'
|
|
17917
|
+
description: 'text: what to type. A newline is Enter and a tab is Tab, a space is a space, and "" with replace empties the field. choose: the item to take, by what it says as drawn, which in a game with translations is the translation; the key it holds is matched after that. An item whose whole words are these comes first; otherwise the words match as says does anywhere, a plain word as part of the item, a glob, or alternatives split at |, enabled items ahead of disabled ones, and several matching equally are refused with each listed.'
|
|
17918
17918
|
},
|
|
17919
17919
|
replace: {
|
|
17920
17920
|
type: "boolean",
|
|
@@ -39767,7 +39767,7 @@ function stopVerdict(stop) {
|
|
|
39767
39767
|
return {
|
|
39768
39768
|
stopped: false,
|
|
39769
39769
|
notSignalled: stop.endedPid,
|
|
39770
|
-
note: `Nothing was ended: pid ${stop.endedPid ?? "unknown"} no longer answers as the process this run was started as (its engine, its project and when it started), so it was not signalled. If that process is still the game, end it yourself, for instance with runtime_invoke calling get_tree().quit() when the game has the runtime addon; if it is not, it belongs to something else.`,
|
|
39770
|
+
note: `Nothing was ended: pid ${stop.endedPid ?? "unknown"} no longer answers as the process this run was started as (${stop.notEndedBecause ?? "its engine, its project and when it started"}), so it was not signalled. If that process is still the game, end it yourself, for instance with runtime_invoke calling get_tree().quit() when the game has the runtime addon; if it is not, it belongs to something else.`,
|
|
39771
39771
|
withChildren: false
|
|
39772
39772
|
};
|
|
39773
39773
|
}
|
|
@@ -39788,6 +39788,15 @@ function stopVerdict(stop) {
|
|
|
39788
39788
|
withChildren: true
|
|
39789
39789
|
};
|
|
39790
39790
|
}
|
|
39791
|
+
function previousRunHow(ended) {
|
|
39792
|
+
if (ended === null) {
|
|
39793
|
+
return {};
|
|
39794
|
+
}
|
|
39795
|
+
return {
|
|
39796
|
+
...ended.through === undefined || ended.ending === "refused" ? {} : { endedPreviousRunThrough: ended.through },
|
|
39797
|
+
...ended.ending === "refused" && ended.notEndedBecause !== undefined ? { previousRunLeftBecause: ended.notEndedBecause } : {}
|
|
39798
|
+
};
|
|
39799
|
+
}
|
|
39791
39800
|
function endedPreviousRun(ended) {
|
|
39792
39801
|
if (ended?.ending !== "gone") {
|
|
39793
39802
|
return;
|
|
@@ -42512,9 +42521,12 @@ class GodotServer {
|
|
|
42512
42521
|
if (before !== null && await this.runStillGoing(before)) {
|
|
42513
42522
|
this.logDebug("Ending the running game before starting another");
|
|
42514
42523
|
const endingFrom = Date.now();
|
|
42524
|
+
const ending = await this.endActiveGame("editor_run start, which ends the run that was going before it starts another", true);
|
|
42515
42525
|
ended = {
|
|
42516
42526
|
pid: before.pid ?? this.announcedPidOf(before) ?? null,
|
|
42517
|
-
ending
|
|
42527
|
+
ending,
|
|
42528
|
+
...before.endedThrough === undefined ? {} : { through: before.endedThrough },
|
|
42529
|
+
...before.notEndedBecause === undefined ? {} : { notEndedBecause: before.notEndedBecause }
|
|
42518
42530
|
};
|
|
42519
42531
|
this.logDebug(`The running game was ${ended.ending} after ${Date.now() - endingFrom}ms`);
|
|
42520
42532
|
}
|
|
@@ -42562,6 +42574,7 @@ class GodotServer {
|
|
|
42562
42574
|
savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
42563
42575
|
endedPreviousRun: endedPreviousRun(ended),
|
|
42564
42576
|
previousRunLeft: previousRunLeft(ended),
|
|
42577
|
+
...previousRunHow(ended),
|
|
42565
42578
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
42566
42579
|
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
42567
42580
|
});
|
|
@@ -42714,6 +42727,7 @@ class GodotServer {
|
|
|
42714
42727
|
debugPort: readNumber(playAnswer, "debugPort"),
|
|
42715
42728
|
endedPreviousRun: endedPreviousRun(ended),
|
|
42716
42729
|
previousRunLeft: previousRunLeft(ended),
|
|
42730
|
+
...previousRunHow(ended),
|
|
42717
42731
|
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
42718
42732
|
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
42719
42733
|
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
@@ -42868,6 +42882,7 @@ class GodotServer {
|
|
|
42868
42882
|
running.log.record("warning", `This run was not ended here: the editor could not be asked to stop it (${asked.content[0]?.text ?? "no reason given"}).`);
|
|
42869
42883
|
return "unreached";
|
|
42870
42884
|
}
|
|
42885
|
+
running.endedThrough = "editor";
|
|
42871
42886
|
if (announced === undefined) {
|
|
42872
42887
|
return await this.untilTheEditorStopsPlaying() ? "gone" : "lingering";
|
|
42873
42888
|
}
|
|
@@ -42896,6 +42911,7 @@ class GodotServer {
|
|
|
42896
42911
|
if (why !== null) {
|
|
42897
42912
|
clearTheRecord();
|
|
42898
42913
|
running.endedHere = null;
|
|
42914
|
+
running.notEndedBecause = why;
|
|
42899
42915
|
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the process the run was started as (${why}), so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
|
|
42900
42916
|
return "refused";
|
|
42901
42917
|
}
|
|
@@ -42920,6 +42936,9 @@ class GodotServer {
|
|
|
42920
42936
|
running.exitCode = null;
|
|
42921
42937
|
running.exitSignal = "SIGTERM";
|
|
42922
42938
|
}
|
|
42939
|
+
if (landed) {
|
|
42940
|
+
running.endedThrough = "signal";
|
|
42941
|
+
}
|
|
42923
42942
|
clearTheRecord();
|
|
42924
42943
|
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 ${STOP_WAIT_MS / 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}.`);
|
|
42925
42944
|
return gone ? "gone" : "lingering";
|
|
@@ -42930,6 +42949,9 @@ class GodotServer {
|
|
|
42930
42949
|
this.logDebug(`The keeper of pid ${pid} could not be asked to end it (${asked.unreached})`);
|
|
42931
42950
|
return null;
|
|
42932
42951
|
}
|
|
42952
|
+
if (asked === "signalled") {
|
|
42953
|
+
run.endedThrough = "keeper";
|
|
42954
|
+
}
|
|
42933
42955
|
const gone = await untilGone([pid]);
|
|
42934
42956
|
if (gone) {
|
|
42935
42957
|
await this.untilTheExitIsRecorded(run);
|
|
@@ -43655,11 +43677,13 @@ class GodotServer {
|
|
|
43655
43677
|
endedPid,
|
|
43656
43678
|
throughEditor: stopped.throughEditor,
|
|
43657
43679
|
exitSignal: stopped.exitSignal,
|
|
43658
|
-
clean: cleanVerdict(stopped)
|
|
43680
|
+
clean: cleanVerdict(stopped),
|
|
43681
|
+
...stopped.notEndedBecause === undefined ? {} : { notEndedBecause: stopped.notEndedBecause }
|
|
43659
43682
|
});
|
|
43660
43683
|
return this.jsonTextResponse({
|
|
43661
43684
|
stopped: verdict.stopped,
|
|
43662
43685
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
43686
|
+
endedThrough: wasRunning ? stopped.endedThrough : undefined,
|
|
43663
43687
|
endedPid: verdict.endedPid,
|
|
43664
43688
|
notSignalled: verdict.notSignalled,
|
|
43665
43689
|
stillGoing: verdict.stillGoing,
|
|
@@ -43724,6 +43748,7 @@ class GodotServer {
|
|
|
43724
43748
|
through: "gdharness",
|
|
43725
43749
|
endedPid: gone ? pid : undefined,
|
|
43726
43750
|
stillGoing: gone ? undefined : pid,
|
|
43751
|
+
...landed ? { endedThrough: "signal" } : {},
|
|
43727
43752
|
...childrenFields(ended),
|
|
43728
43753
|
exitedBeforeStop: !landed,
|
|
43729
43754
|
note: `${!gone ? `pid ${pid} was told to end and was still running ${STOP_WAIT_MS / 1000} seconds later, so it has not stopped: a game of this project that no server here was holding,` : landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" : " editor_status says when it has gone."} ${aboutTheChildren(ended, false)}`
|
|
@@ -21,6 +21,10 @@ const TO_SMALL: int = 32
|
|
|
21
21
|
## reason a control is out of reach. Only worth telling somebody when it is what they have.
|
|
22
22
|
const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
|
|
23
23
|
|
|
24
|
+
## Set, to the desktop's name, for a process gdharness started on a desktop of its own and for
|
|
25
|
+
## everything that process starts. Kept in step with `DESKTOP_VARIABLE` in src/desktop.ts.
|
|
26
|
+
const DESKTOP_VARIABLE: String = "GDHARNESS_DESKTOP"
|
|
27
|
+
|
|
24
28
|
var _host: Node
|
|
25
29
|
var _values: Values
|
|
26
30
|
|
|
@@ -45,6 +49,28 @@ func _init(host: Node, values: Values) -> void:
|
|
|
45
49
|
_values = values
|
|
46
50
|
|
|
47
51
|
|
|
52
|
+
## [param answer], saying what a desktop of gdharness's own does to the game's own reading of the
|
|
53
|
+
## pointer when the game is on one.
|
|
54
|
+
##
|
|
55
|
+
## For the root window the engine asks Windows where the pointer is, and GetCursorPos refuses a
|
|
56
|
+
## process on a desktop that is not in use: measured on one of gdharness's own, it answered false
|
|
57
|
+
## with access denied and left the point unwritten. The engine does not check, so a game driven
|
|
58
|
+
## there read get_mouse_position() as x 836852416 after a motion to 1310, 314. Nothing here can
|
|
59
|
+
## put a pointer on that desktop: Windows moves one only on the desktop in use, and making this one
|
|
60
|
+
## that would take the screen. What reads the injected event is right, tooltips included, since the
|
|
61
|
+
## engine places a tooltip from the motion that reached the control.
|
|
62
|
+
static func _pointer_noted(answer: Dictionary) -> Dictionary:
|
|
63
|
+
if OS.get_environment(DESKTOP_VARIABLE) != "":
|
|
64
|
+
answer["pointer_note"] = (
|
|
65
|
+
"This game is on gdharness's own desktop, where Windows refuses to say where the"
|
|
66
|
+
+ " pointer is, so get_mouse_position() on the root window, and get_global_mouse_position()"
|
|
67
|
+
+ " and get_local_mouse_position() on what is drawn there, read a meaningless position"
|
|
68
|
+
+ " rather than where this put it. What reads the event itself gets this position: the"
|
|
69
|
+
+ " control under the pointer, its hover and tooltip, and the game's input handlers."
|
|
70
|
+
)
|
|
71
|
+
return answer
|
|
72
|
+
|
|
73
|
+
|
|
48
74
|
## The movement a pointer arriving at [param position] carries, which is the distance from where
|
|
49
75
|
## the last injected event put it, and none at all for the first. The arrival is recorded, with how
|
|
50
76
|
## long it took: at least a frame, since two motions sent in one frame did not cross the screen in
|
|
@@ -238,15 +264,17 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
238
264
|
Input.parse_input_event(_button(position, button, false, false))
|
|
239
265
|
await _host.get_tree().process_frame
|
|
240
266
|
|
|
241
|
-
return
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
267
|
+
return _pointer_noted(
|
|
268
|
+
{
|
|
269
|
+
"type": "input_injected",
|
|
270
|
+
"input_type": "mouse_click",
|
|
271
|
+
"position": [position.x, position.y],
|
|
272
|
+
"button": button,
|
|
273
|
+
"pressed": held and not whole,
|
|
274
|
+
"whole": whole,
|
|
275
|
+
"double": double
|
|
276
|
+
}
|
|
277
|
+
)
|
|
250
278
|
|
|
251
279
|
|
|
252
280
|
## Moves the pointer to a position, with the movement the event carries taken from where the
|
|
@@ -275,12 +303,14 @@ func inject_mouse_motion(params: Dictionary) -> Dictionary:
|
|
|
275
303
|
|
|
276
304
|
Input.parse_input_event(_motion(position, relative))
|
|
277
305
|
|
|
278
|
-
return
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
306
|
+
return _pointer_noted(
|
|
307
|
+
{
|
|
308
|
+
"type": "input_injected",
|
|
309
|
+
"input_type": "mouse_motion",
|
|
310
|
+
"position": [position.x, position.y],
|
|
311
|
+
"relative": [relative.x, relative.y]
|
|
312
|
+
}
|
|
313
|
+
)
|
|
284
314
|
|
|
285
315
|
|
|
286
316
|
## Whether [param centre] is somewhere a click can reach it: inside the viewport, and inside every
|
|
@@ -289,11 +319,11 @@ static func _in_sight(control: Control, viewport: Viewport, centre: Vector2) ->
|
|
|
289
319
|
return viewport.get_visible_rect().has_point(centre) and _clipped_by(control, centre) == null
|
|
290
320
|
|
|
291
321
|
|
|
292
|
-
## The ScrollContainer between [param control] and
|
|
293
|
-
## or null when none does.
|
|
322
|
+
## The ScrollContainer between [param control] and its viewport that clips [param centre] away from
|
|
323
|
+
## it, or null when none does. Not past the viewport: one holding a dialog is in another space.
|
|
294
324
|
static func _clipped_by(control: Control, centre: Vector2) -> ScrollContainer:
|
|
295
325
|
var walking: Node = control.get_parent()
|
|
296
|
-
while walking != null:
|
|
326
|
+
while walking != null and not (walking is Viewport):
|
|
297
327
|
var holder: ScrollContainer = walking as ScrollContainer
|
|
298
328
|
# Carried into the same space the centre is in, which is the canvas rather than the
|
|
299
329
|
# container's own: the two are only the same while nothing above it is transformed.
|
|
@@ -312,11 +342,12 @@ static func _clipped_by(control: Control, centre: Vector2) -> ScrollContainer:
|
|
|
312
342
|
## next layout pass rather than inside the call, and the next one out works out where the control is
|
|
313
343
|
## from where it is drawn, so asked in the same frame it scrolled to where the control had been.
|
|
314
344
|
## Moved means a scroll value changed: a container asked to show what it already shows moves
|
|
315
|
-
## nothing, and answering true for it said the view had moved when it had not.
|
|
345
|
+
## nothing, and answering true for it said the view had moved when it had not. Only the containers in
|
|
346
|
+
## the control's own viewport: one holding a dialog scrolls the page behind it, not the dialog.
|
|
316
347
|
func _scroll_into_view(control: Control) -> bool:
|
|
317
348
|
var moved: bool = false
|
|
318
349
|
var walking: Variant = control.get_parent()
|
|
319
|
-
while walking != null:
|
|
350
|
+
while walking != null and not (walking is Viewport):
|
|
320
351
|
var step: Node = walking
|
|
321
352
|
var holder: ScrollContainer = step as ScrollContainer
|
|
322
353
|
if holder != null:
|
|
@@ -466,7 +497,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
466
497
|
}
|
|
467
498
|
if found != null:
|
|
468
499
|
answer["found"] = found
|
|
469
|
-
return answer
|
|
500
|
+
return _pointer_noted(answer)
|
|
470
501
|
|
|
471
502
|
|
|
472
503
|
## Why [param control] would take no click a player gave it, or "" when it would.
|
|
@@ -636,19 +667,21 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
|
|
|
636
667
|
_press_button(arriving, _button(position, button, false, false))
|
|
637
668
|
await _host.get_tree().process_frame
|
|
638
669
|
|
|
639
|
-
return
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
670
|
+
return _pointer_noted(
|
|
671
|
+
{
|
|
672
|
+
"type": "clicked",
|
|
673
|
+
"path": node_path,
|
|
674
|
+
"position": _values.serialize(position),
|
|
675
|
+
"button": button,
|
|
676
|
+
"double": double,
|
|
677
|
+
"hovered": hovered_path,
|
|
678
|
+
# The interface did not take it, so it reached the game's own input. As close to "it
|
|
679
|
+
# landed" as anything outside the game can get, and said in the same word the Control
|
|
680
|
+
# click says it in.
|
|
681
|
+
"landed": through["landed"],
|
|
682
|
+
"camera": found["camera"],
|
|
683
|
+
}
|
|
684
|
+
)
|
|
652
685
|
|
|
653
686
|
|
|
654
687
|
## Whether a pointer on its way out through [param out], the steps [method Screen.steps_out]
|
|
@@ -4,6 +4,7 @@ extends RefCounted
|
|
|
4
4
|
## where it is in the list.
|
|
5
5
|
|
|
6
6
|
const Read = preload("reading.gd")
|
|
7
|
+
const Says = preload("runtime_says.gd")
|
|
7
8
|
const Words = preload("runtime_words.gd")
|
|
8
9
|
|
|
9
10
|
|
|
@@ -20,20 +21,28 @@ static func menu_of(node: Node) -> PopupMenu:
|
|
|
20
21
|
return null
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
##
|
|
24
|
-
##
|
|
24
|
+
## The items asked for, best first: the one at `index`; else the one whose whole words are `text`,
|
|
25
|
+
## exactly and then case-insensitively; else every item `text` matches by the rules `says` has, a
|
|
26
|
+
## plain word as a contains, a glob, and alternatives split at `|`, the enabled ones when any are.
|
|
27
|
+
## Empty when nothing matches, and more than one when several match equally well, which the caller
|
|
28
|
+
## refuses rather than guesses between.
|
|
29
|
+
##
|
|
30
|
+
## By the same rules as `says` because a caller who has learnt them from a click, a find and a wait
|
|
31
|
+
## writes the same pattern here: "Leave*" was refused against a menu holding "Leave the hall", and
|
|
32
|
+
## the item had to be written out in full on a second call. The whole words still come first, so an
|
|
33
|
+
## item named in full is chosen over another whose words contain it.
|
|
25
34
|
##
|
|
26
35
|
## `text` is matched against the words the item shows before the ones it holds. In a game with
|
|
27
36
|
## translations they differ, and the shown ones are what a caller has read off the screen: an item
|
|
28
37
|
## held as `ACT_ENGAGE` and shown as "Hire" refused "Hire" and listed the keys. The held words
|
|
29
38
|
## still count after them, for a caller that has the key from the source.
|
|
30
|
-
static func
|
|
39
|
+
static func wanted_items(menu: PopupMenu, params: Dictionary) -> Array[int]:
|
|
31
40
|
if params.has("index"):
|
|
32
41
|
var asked: int = Read.as_int(params.get("index", -1), -1)
|
|
33
|
-
return asked if asked >= 0 and asked < menu.get_item_count() else
|
|
42
|
+
return _items([asked] if asked >= 0 and asked < menu.get_item_count() else [])
|
|
34
43
|
var wanted: String = str(params.get("text", ""))
|
|
35
44
|
if wanted.is_empty():
|
|
36
|
-
return
|
|
45
|
+
return _items([])
|
|
37
46
|
# The items before the separators, because a heading can carry the same words as an item under
|
|
38
47
|
# it, and finding the heading first refused the choice the caller meant. A separator is still
|
|
39
48
|
# looked for after, so a heading asked for by name is refused as one rather than as missing.
|
|
@@ -50,11 +59,33 @@ static func wanted_item(menu: PopupMenu, params: Dictionary) -> int:
|
|
|
50
59
|
for words: Array[String] in [shown, held]:
|
|
51
60
|
for at: int in words.size():
|
|
52
61
|
if words[at] == wanted:
|
|
53
|
-
return order[at]
|
|
62
|
+
return _items([order[at]])
|
|
54
63
|
for at: int in words.size():
|
|
55
64
|
if words[at].nocasecmp_to(wanted) == 0:
|
|
56
|
-
return order[at]
|
|
57
|
-
|
|
65
|
+
return _items([order[at]])
|
|
66
|
+
# Separators are left out here: a heading is refused when named in full, and a pattern that
|
|
67
|
+
# matched one beside the item it heads would tie the two.
|
|
68
|
+
for words: Array[String] in [shown, held]:
|
|
69
|
+
var matched: Array[int] = []
|
|
70
|
+
var enabled: Array[int] = []
|
|
71
|
+
for at: int in words.size():
|
|
72
|
+
var index: int = order[at]
|
|
73
|
+
if menu.is_item_separator(index) or not Says.matches(words[at], wanted):
|
|
74
|
+
continue
|
|
75
|
+
matched.append(index)
|
|
76
|
+
if not menu.is_item_disabled(index):
|
|
77
|
+
enabled.append(index)
|
|
78
|
+
if not matched.is_empty():
|
|
79
|
+
return enabled if not enabled.is_empty() else matched
|
|
80
|
+
return _items([])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## [param indices] as a list of item numbers: a literal list is untyped, and handed back where a
|
|
84
|
+
## typed one is declared it is refused when the call is made, not when the script is read.
|
|
85
|
+
static func _items(indices: Array) -> Array[int]:
|
|
86
|
+
var typed: Array[int] = []
|
|
87
|
+
typed.assign(indices)
|
|
88
|
+
return typed
|
|
58
89
|
|
|
59
90
|
|
|
60
91
|
## What the menu shows, for a refusal that names the choices rather than the miss: a titled
|
|
@@ -216,13 +216,21 @@ static func can_read(holder: Variant, named: String) -> bool:
|
|
|
216
216
|
## A step is text, and a map is keyed by whatever the game put in it: text, a StringName, or a
|
|
217
217
|
## number, which a map of units by id is. Keyed by 3, "3" is not a key of it, measured on 4.7.2, so a
|
|
218
218
|
## step that reads as a number is tried as one after the two spellings of text.
|
|
219
|
+
##
|
|
220
|
+
## A map typed by its key is asked only in that type. It checks every key it is asked about and
|
|
221
|
+
## reports an engine error for one of another type, so a step into a map typed by an enum read the
|
|
222
|
+
## right entry and left four errors in the game's log for each of the two walks, and the run was
|
|
223
|
+
## counted as unclean for a read.
|
|
219
224
|
static func _key_in(map: Dictionary, named: String) -> Array:
|
|
220
225
|
var spellings: Array = [named, StringName(named)]
|
|
221
226
|
if named.is_valid_int():
|
|
222
227
|
spellings.append(int(named))
|
|
223
228
|
if named.is_valid_float():
|
|
224
229
|
spellings.append(float(named))
|
|
230
|
+
var typed: int = map.get_typed_key_builtin() if map.is_typed_key() else TYPE_NIL
|
|
225
231
|
for key: Variant in spellings:
|
|
232
|
+
if typed != TYPE_NIL and typeof(key) != typed:
|
|
233
|
+
continue
|
|
226
234
|
if map.has(key):
|
|
227
235
|
return [key]
|
|
228
236
|
return []
|
|
@@ -229,7 +229,7 @@ static func _covered_note(covered: int, cover: Node) -> String:
|
|
|
229
229
|
## its words means.
|
|
230
230
|
static func _pressed_through(control: Control) -> Control:
|
|
231
231
|
var walk: Node = control
|
|
232
|
-
while walk != null:
|
|
232
|
+
while walk != null and not (walk is Viewport):
|
|
233
233
|
if walk is BaseButton:
|
|
234
234
|
var pressed: BaseButton = walk
|
|
235
235
|
return pressed
|
|
@@ -376,7 +376,7 @@ static func _where_it_lands(target: Control, point: Vector2) -> Vector2:
|
|
|
376
376
|
var half: Vector2 = (drawn * Rect2(Vector2.ZERO, target.size)).size * 0.5
|
|
377
377
|
var landed: Vector2 = point
|
|
378
378
|
var walk: Node = target.get_parent()
|
|
379
|
-
while walk != null:
|
|
379
|
+
while walk != null and not (walk is Viewport):
|
|
380
380
|
var holder: ScrollContainer = walk as ScrollContainer
|
|
381
381
|
if holder != null:
|
|
382
382
|
var shown: Rect2 = holder.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, holder.size)
|
|
@@ -405,9 +405,13 @@ static func _off_screen(target: Control) -> bool:
|
|
|
405
405
|
|
|
406
406
|
|
|
407
407
|
## Whether a container clipping what it holds cuts [param point] off from [param control].
|
|
408
|
+
##
|
|
409
|
+
## Only up to the viewport [param control] is in. A dialog is a window, and the containers the game
|
|
410
|
+
## added it under are in another space: judged against a clipping one of those, every button of a
|
|
411
|
+
## confirmation was refused as off the screen while it sat in plain view.
|
|
408
412
|
static func _clipped_away(control: Control, point: Vector2) -> bool:
|
|
409
413
|
var walk: Node = control.get_parent()
|
|
410
|
-
while walk != null:
|
|
414
|
+
while walk != null and not (walk is Viewport):
|
|
411
415
|
var holder: Control = walk as Control
|
|
412
416
|
if holder != null and holder.clip_contents and not _holds(holder, point):
|
|
413
417
|
return true
|
|
@@ -261,11 +261,24 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
261
261
|
var opened: bool = Menus.open_the_menu(node, menu)
|
|
262
262
|
await _host.get_tree().process_frame
|
|
263
263
|
|
|
264
|
-
var
|
|
264
|
+
var found: Array[int] = Menus.wanted_items(menu, params)
|
|
265
|
+
if found.size() > 1:
|
|
266
|
+
await _shut(menu, opened)
|
|
267
|
+
var tied: Array[String] = []
|
|
268
|
+
for at: int in found:
|
|
269
|
+
tied.append("%d: %s" % [at, Words.item_says(menu, at)])
|
|
270
|
+
return {
|
|
271
|
+
"type": "error",
|
|
272
|
+
"message":
|
|
273
|
+
(
|
|
274
|
+
"%s has %d items that say %s, so nothing was chosen: %s. Name one by its whole text or by index"
|
|
275
|
+
% [node_path, found.size(), JSON.stringify(str(params.get("text", ""))), ", ".join(tied)]
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
var index: int = found[0] if found.size() == 1 else -1
|
|
265
279
|
var refused: String = _not_a_choice(node_path, menu, index)
|
|
266
280
|
if not refused.is_empty():
|
|
267
|
-
|
|
268
|
-
menu.hide()
|
|
281
|
+
await _shut(menu, opened)
|
|
269
282
|
return {"type": "error", "message": refused}
|
|
270
283
|
|
|
271
284
|
# Read before the press, because the press can take the menu away: a game that rebuilds its
|
|
@@ -298,8 +311,7 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
298
311
|
menu.index_pressed.disconnect(hear)
|
|
299
312
|
|
|
300
313
|
if not fired.has(index):
|
|
301
|
-
|
|
302
|
-
menu.hide()
|
|
314
|
+
await _shut(menu, opened)
|
|
303
315
|
return {
|
|
304
316
|
"type": "error",
|
|
305
317
|
"message":
|
|
@@ -322,6 +334,16 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
322
334
|
return answer
|
|
323
335
|
|
|
324
336
|
|
|
337
|
+
## Shuts a menu this call opened, and lets the frame pass in which the engine finishes shutting it.
|
|
338
|
+
## A choice asked for in the frame a refusal shut the menu found it half shut, and the press it sent
|
|
339
|
+
## went nowhere: measured on 4.7.2, a MenuButton refused and at once asked again answered that the
|
|
340
|
+
## menu did not take the item.
|
|
341
|
+
func _shut(menu: PopupMenu, opened: bool) -> void:
|
|
342
|
+
if opened and is_instance_valid(menu) and menu.visible:
|
|
343
|
+
menu.hide()
|
|
344
|
+
await _host.get_tree().process_frame
|
|
345
|
+
|
|
346
|
+
|
|
325
347
|
## Why item [param index] of [param menu] is not something to choose, or "" when it is.
|
|
326
348
|
static func _not_a_choice(node_path: String, menu: PopupMenu, index: int) -> String:
|
|
327
349
|
if index < 0:
|
package/build/index.js
CHANGED
|
@@ -31540,7 +31540,7 @@ var TOOL_SPECS = [
|
|
|
31540
31540
|
requires: ["projectPath"]
|
|
31541
31541
|
},
|
|
31542
31542
|
stop: {
|
|
31543
|
-
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. A game that started processes of its own keeps them unless andChildren says to end those too. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, and false with the number under stillGoing when what was told to end was still running ten seconds later",
|
|
31543
|
+
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. A game that started processes of its own keeps them unless andChildren says to end those too. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause",
|
|
31544
31544
|
requires: []
|
|
31545
31545
|
},
|
|
31546
31546
|
check: {
|
|
@@ -31858,7 +31858,7 @@ var TOOL_SPECS = [
|
|
|
31858
31858
|
},
|
|
31859
31859
|
{
|
|
31860
31860
|
name: "runtime_input",
|
|
31861
|
-
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
|
|
31861
|
+
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: the pointer ops answer with pointer_note there, because get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position.",
|
|
31862
31862
|
parameters: {
|
|
31863
31863
|
projectPath: RUNNING_PROJECT_PATH,
|
|
31864
31864
|
pid: RUNNING_PID,
|
|
@@ -31898,7 +31898,7 @@ var TOOL_SPECS = [
|
|
|
31898
31898
|
type: "string",
|
|
31899
31899
|
ops: ["text", "choose"],
|
|
31900
31900
|
blank: true,
|
|
31901
|
-
description: 'text: what to type. A newline is Enter and a tab is Tab, a space is a space, and "" with replace empties the field. choose: the item to take, by what it says as drawn, which in a game with translations is the translation; the key it holds is matched after that.'
|
|
31901
|
+
description: 'text: what to type. A newline is Enter and a tab is Tab, a space is a space, and "" with replace empties the field. choose: the item to take, by what it says as drawn, which in a game with translations is the translation; the key it holds is matched after that. An item whose whole words are these comes first; otherwise the words match as says does anywhere, a plain word as part of the item, a glob, or alternatives split at |, enabled items ahead of disabled ones, and several matching equally are refused with each listed.'
|
|
31902
31902
|
},
|
|
31903
31903
|
replace: {
|
|
31904
31904
|
type: "boolean",
|
|
@@ -32287,7 +32287,7 @@ function stopVerdict(stop) {
|
|
|
32287
32287
|
return {
|
|
32288
32288
|
stopped: false,
|
|
32289
32289
|
notSignalled: stop.endedPid,
|
|
32290
|
-
note: `Nothing was ended: pid ${stop.endedPid ?? "unknown"} no longer answers as the process this run was started as (its engine, its project and when it started), so it was not signalled. If that process is still the game, end it yourself, for instance with runtime_invoke calling get_tree().quit() when the game has the runtime addon; if it is not, it belongs to something else.`,
|
|
32290
|
+
note: `Nothing was ended: pid ${stop.endedPid ?? "unknown"} no longer answers as the process this run was started as (${stop.notEndedBecause ?? "its engine, its project and when it started"}), so it was not signalled. If that process is still the game, end it yourself, for instance with runtime_invoke calling get_tree().quit() when the game has the runtime addon; if it is not, it belongs to something else.`,
|
|
32291
32291
|
withChildren: false
|
|
32292
32292
|
};
|
|
32293
32293
|
}
|
|
@@ -32308,6 +32308,15 @@ function stopVerdict(stop) {
|
|
|
32308
32308
|
withChildren: true
|
|
32309
32309
|
};
|
|
32310
32310
|
}
|
|
32311
|
+
function previousRunHow(ended) {
|
|
32312
|
+
if (ended === null) {
|
|
32313
|
+
return {};
|
|
32314
|
+
}
|
|
32315
|
+
return {
|
|
32316
|
+
...ended.through === undefined || ended.ending === "refused" ? {} : { endedPreviousRunThrough: ended.through },
|
|
32317
|
+
...ended.ending === "refused" && ended.notEndedBecause !== undefined ? { previousRunLeftBecause: ended.notEndedBecause } : {}
|
|
32318
|
+
};
|
|
32319
|
+
}
|
|
32311
32320
|
function endedPreviousRun(ended) {
|
|
32312
32321
|
if (ended?.ending !== "gone") {
|
|
32313
32322
|
return;
|
|
@@ -35092,9 +35101,12 @@ class GodotServer {
|
|
|
35092
35101
|
if (before !== null && await this.runStillGoing(before)) {
|
|
35093
35102
|
this.logDebug("Ending the running game before starting another");
|
|
35094
35103
|
const endingFrom = Date.now();
|
|
35104
|
+
const ending = await this.endActiveGame("editor_run start, which ends the run that was going before it starts another", true);
|
|
35095
35105
|
ended = {
|
|
35096
35106
|
pid: before.pid ?? this.announcedPidOf(before) ?? null,
|
|
35097
|
-
ending
|
|
35107
|
+
ending,
|
|
35108
|
+
...before.endedThrough === undefined ? {} : { through: before.endedThrough },
|
|
35109
|
+
...before.notEndedBecause === undefined ? {} : { notEndedBecause: before.notEndedBecause }
|
|
35098
35110
|
};
|
|
35099
35111
|
this.logDebug(`The running game was ${ended.ending} after ${Date.now() - endingFrom}ms`);
|
|
35100
35112
|
}
|
|
@@ -35142,6 +35154,7 @@ class GodotServer {
|
|
|
35142
35154
|
savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
35143
35155
|
endedPreviousRun: endedPreviousRun(ended),
|
|
35144
35156
|
previousRunLeft: previousRunLeft(ended),
|
|
35157
|
+
...previousRunHow(ended),
|
|
35145
35158
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
35146
35159
|
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
35147
35160
|
});
|
|
@@ -35294,6 +35307,7 @@ class GodotServer {
|
|
|
35294
35307
|
debugPort: readNumber(playAnswer, "debugPort"),
|
|
35295
35308
|
endedPreviousRun: endedPreviousRun(ended),
|
|
35296
35309
|
previousRunLeft: previousRunLeft(ended),
|
|
35310
|
+
...previousRunHow(ended),
|
|
35297
35311
|
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
35298
35312
|
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
35299
35313
|
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
@@ -35448,6 +35462,7 @@ class GodotServer {
|
|
|
35448
35462
|
running.log.record("warning", `This run was not ended here: the editor could not be asked to stop it (${asked.content[0]?.text ?? "no reason given"}).`);
|
|
35449
35463
|
return "unreached";
|
|
35450
35464
|
}
|
|
35465
|
+
running.endedThrough = "editor";
|
|
35451
35466
|
if (announced === undefined) {
|
|
35452
35467
|
return await this.untilTheEditorStopsPlaying() ? "gone" : "lingering";
|
|
35453
35468
|
}
|
|
@@ -35476,6 +35491,7 @@ class GodotServer {
|
|
|
35476
35491
|
if (why !== null) {
|
|
35477
35492
|
clearTheRecord();
|
|
35478
35493
|
running.endedHere = null;
|
|
35494
|
+
running.notEndedBecause = why;
|
|
35479
35495
|
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the process the run was started as (${why}), so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
|
|
35480
35496
|
return "refused";
|
|
35481
35497
|
}
|
|
@@ -35500,6 +35516,9 @@ class GodotServer {
|
|
|
35500
35516
|
running.exitCode = null;
|
|
35501
35517
|
running.exitSignal = "SIGTERM";
|
|
35502
35518
|
}
|
|
35519
|
+
if (landed) {
|
|
35520
|
+
running.endedThrough = "signal";
|
|
35521
|
+
}
|
|
35503
35522
|
clearTheRecord();
|
|
35504
35523
|
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 ${STOP_WAIT_MS / 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}.`);
|
|
35505
35524
|
return gone ? "gone" : "lingering";
|
|
@@ -35510,6 +35529,9 @@ class GodotServer {
|
|
|
35510
35529
|
this.logDebug(`The keeper of pid ${pid} could not be asked to end it (${asked.unreached})`);
|
|
35511
35530
|
return null;
|
|
35512
35531
|
}
|
|
35532
|
+
if (asked === "signalled") {
|
|
35533
|
+
run.endedThrough = "keeper";
|
|
35534
|
+
}
|
|
35513
35535
|
const gone = await untilGone([pid]);
|
|
35514
35536
|
if (gone) {
|
|
35515
35537
|
await this.untilTheExitIsRecorded(run);
|
|
@@ -36235,11 +36257,13 @@ class GodotServer {
|
|
|
36235
36257
|
endedPid,
|
|
36236
36258
|
throughEditor: stopped.throughEditor,
|
|
36237
36259
|
exitSignal: stopped.exitSignal,
|
|
36238
|
-
clean: cleanVerdict(stopped)
|
|
36260
|
+
clean: cleanVerdict(stopped),
|
|
36261
|
+
...stopped.notEndedBecause === undefined ? {} : { notEndedBecause: stopped.notEndedBecause }
|
|
36239
36262
|
});
|
|
36240
36263
|
return this.jsonTextResponse({
|
|
36241
36264
|
stopped: verdict.stopped,
|
|
36242
36265
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
36266
|
+
endedThrough: wasRunning ? stopped.endedThrough : undefined,
|
|
36243
36267
|
endedPid: verdict.endedPid,
|
|
36244
36268
|
notSignalled: verdict.notSignalled,
|
|
36245
36269
|
stillGoing: verdict.stillGoing,
|
|
@@ -36304,6 +36328,7 @@ class GodotServer {
|
|
|
36304
36328
|
through: "gdharness",
|
|
36305
36329
|
endedPid: gone ? pid : undefined,
|
|
36306
36330
|
stillGoing: gone ? undefined : pid,
|
|
36331
|
+
...landed ? { endedThrough: "signal" } : {},
|
|
36307
36332
|
...childrenFields(ended),
|
|
36308
36333
|
exitedBeforeStop: !landed,
|
|
36309
36334
|
note: `${!gone ? `pid ${pid} was told to end and was still running ${STOP_WAIT_MS / 1000} seconds later, so it has not stopped: a game of this project that no server here was holding,` : landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" : " editor_status says when it has gone."} ${aboutTheChildren(ended, false)}`
|
package/build/keeper.js
CHANGED
|
@@ -10,6 +10,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
10
10
|
// src/desktop.ts
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import { writeFileSync } from "node:fs";
|
|
13
|
+
var DESKTOP_VARIABLE = "GDHARNESS_DESKTOP";
|
|
13
14
|
var SOURCE = `using System;
|
|
14
15
|
using System.ComponentModel;
|
|
15
16
|
using System.IO;
|
|
@@ -565,7 +566,8 @@ async function startThroughHelper(spec, output, stopFile) {
|
|
|
565
566
|
error: errorFile,
|
|
566
567
|
stop: stopFile
|
|
567
568
|
});
|
|
568
|
-
const
|
|
569
|
+
const envChanges = spec.desktop === undefined ? spec.envChanges : { ...spec.envChanges, [DESKTOP_VARIABLE]: spec.desktop };
|
|
570
|
+
const started = await startDirectly({ ...spec, command: wrapper.command, args: wrapper.args, envChanges }, output, true, false, "ignore");
|
|
569
571
|
if ("error" in started) {
|
|
570
572
|
return started;
|
|
571
573
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.26",
|
|
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",
|