gdharness 1.3.55 → 1.3.57
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.
|
@@ -89,7 +89,8 @@ var _unmade: Array[Dictionary] = []
|
|
|
89
89
|
##
|
|
90
90
|
## A write is refused while the editor holds unsaved changes to the scene: the file is reloaded
|
|
91
91
|
## into the editor after a write, which discards them without a prompt, and the editor saving its
|
|
92
|
-
## copy later would discard the write instead.
|
|
92
|
+
## copy later would discard the write instead. One is refused too while the scene names a file
|
|
93
|
+
## that is gone ([method missing_said]).
|
|
93
94
|
func open(scene_path: String, for_writing: bool) -> Dictionary:
|
|
94
95
|
path = scene_path
|
|
95
96
|
if not FileAccess.file_exists(path):
|
|
@@ -98,6 +99,9 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
|
|
|
98
99
|
var held_back: String = unsaved_in_editor(path)
|
|
99
100
|
if not held_back.is_empty():
|
|
100
101
|
return { "ok": false, "error": held_back }
|
|
102
|
+
var missing: String = missing_said(path, FileAccess.get_file_as_string(path))
|
|
103
|
+
if not missing.is_empty():
|
|
104
|
+
return { "ok": false, "error": missing }
|
|
101
105
|
var packed: PackedScene = (
|
|
102
106
|
ResourceLoader.load(path, "PackedScene", ResourceLoader.CACHE_MODE_IGNORE) as PackedScene
|
|
103
107
|
)
|
|
@@ -125,6 +129,92 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
|
|
|
125
129
|
return {}
|
|
126
130
|
|
|
127
131
|
|
|
132
|
+
## Why a write to the scene at [param scene_path], whose text is [param text], is refused, or "":
|
|
133
|
+
## an [ext_resource] naming no file the project has, by its uid or by its path. The engine loads the
|
|
134
|
+
## scene without it, so a save drops the entry and every value set from it, and the answer could
|
|
135
|
+
## only call those values empty.
|
|
136
|
+
static func missing_said(scene_path: String, text: String) -> String:
|
|
137
|
+
var gone: Dictionary = {}
|
|
138
|
+
var entry: RegEx = RegEx.create_from_string("(?m)^\\[ext_resource\\b[^\\n]*\\]")
|
|
139
|
+
var field: RegEx = RegEx.create_from_string('\\b(path|uid|id)="([^"]*)"')
|
|
140
|
+
for found: RegExMatch in entry.search_all(text):
|
|
141
|
+
var fields: Dictionary = {}
|
|
142
|
+
for one: RegExMatch in field.search_all(found.get_string()):
|
|
143
|
+
fields[one.get_string(1)] = one.get_string(2)
|
|
144
|
+
var uid: String = str(fields.get("uid", ""))
|
|
145
|
+
var by_uid: String = ""
|
|
146
|
+
if uid.begins_with("uid://") and ResourceUID.has_id(ResourceUID.text_to_id(uid)):
|
|
147
|
+
by_uid = ResourceUID.get_id_path(ResourceUID.text_to_id(uid))
|
|
148
|
+
var named: String = str(fields.get("path", ""))
|
|
149
|
+
if FileAccess.file_exists(by_uid) or FileAccess.file_exists(named):
|
|
150
|
+
continue
|
|
151
|
+
gone[str(fields.get("id", ""))] = named if not named.is_empty() else uid
|
|
152
|
+
if gone.is_empty():
|
|
153
|
+
return ""
|
|
154
|
+
var uses: PackedStringArray = []
|
|
155
|
+
var on: String = ""
|
|
156
|
+
var lines: PackedStringArray = text.split("\n")
|
|
157
|
+
var header: RegEx = RegEx.create_from_string('^\\[node name="([^"]*)"')
|
|
158
|
+
var parented: RegEx = RegEx.create_from_string('\\bparent="([^"]*)"')
|
|
159
|
+
for index: int in lines.size():
|
|
160
|
+
var line: String = lines[index]
|
|
161
|
+
if line.begins_with("["):
|
|
162
|
+
var node: RegExMatch = header.search(line)
|
|
163
|
+
var under: RegExMatch = parented.search(line)
|
|
164
|
+
if node == null:
|
|
165
|
+
on = line.get_slice(" ", 0).trim_prefix("[")
|
|
166
|
+
elif under == null or under.get_string(1) == ".":
|
|
167
|
+
on = node.get_string(1)
|
|
168
|
+
else:
|
|
169
|
+
on = under.get_string(1).path_join(node.get_string(1))
|
|
170
|
+
continue
|
|
171
|
+
for id: String in gone:
|
|
172
|
+
if line.contains('ExtResource("%s")' % id):
|
|
173
|
+
var said: String = "line %d on %s, %s" % [index + 1, on, line.strip_edges()]
|
|
174
|
+
var _used: bool = uses.append(said)
|
|
175
|
+
break
|
|
176
|
+
var paths: PackedStringArray = []
|
|
177
|
+
for id: String in gone:
|
|
178
|
+
var _named: bool = paths.append(str(gone[id]))
|
|
179
|
+
var one_file: bool = paths.size() == 1
|
|
180
|
+
return (
|
|
181
|
+
(
|
|
182
|
+
"Nothing was written: %s names %s, which %s not there, so the engine loads the scene "
|
|
183
|
+
+ "without %s, and a save would drop %s%s. Put the %s back at %s, or change the "
|
|
184
|
+
+ "[ext_resource] %s in the scene to %s that exists."
|
|
185
|
+
)
|
|
186
|
+
% [
|
|
187
|
+
scene_path,
|
|
188
|
+
", ".join(paths),
|
|
189
|
+
"is" if one_file else "are",
|
|
190
|
+
"it" if one_file else "them",
|
|
191
|
+
"it" if one_file else "them",
|
|
192
|
+
"" if uses.is_empty() else " and what is set from %s: %s" % [
|
|
193
|
+
"it" if one_file else "them", "; ".join(uses),
|
|
194
|
+
],
|
|
195
|
+
"file" if one_file else "files",
|
|
196
|
+
"that path" if one_file else "those paths",
|
|
197
|
+
"line" if one_file else "lines",
|
|
198
|
+
"one" if one_file else "ones",
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
## The connections the file holds, as it was opened, by their paths from [member root].
|
|
204
|
+
func held_connections() -> Array[Dictionary]:
|
|
205
|
+
var held: Array[Dictionary] = []
|
|
206
|
+
for index: int in _state.get_connection_count():
|
|
207
|
+
held.append(
|
|
208
|
+
{
|
|
209
|
+
"sourceNodePath": str(_state.get_connection_source(index)),
|
|
210
|
+
"signalName": str(_state.get_connection_signal(index)),
|
|
211
|
+
"targetNodePath": str(_state.get_connection_target(index)),
|
|
212
|
+
"methodName": str(_state.get_connection_method(index)),
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
return held
|
|
216
|
+
|
|
217
|
+
|
|
128
218
|
## The connections in [param state] the engine did not make on [member root]: Object.connect
|
|
129
219
|
## refuses a callable that binds or unbinds arguments unless its target has the method, so the scene
|
|
130
220
|
## loads without them, in the editor as well, and every save drops them.
|
|
@@ -392,6 +392,7 @@ func delete_node(args: Dictionary) -> Dictionary:
|
|
|
392
392
|
for under: Node in node.find_children("*", "", true, false):
|
|
393
393
|
if under.owner == scene.root:
|
|
394
394
|
went.append(scene.path_of(under))
|
|
395
|
+
var cut: Array[Dictionary] = _connections_with(scene, at)
|
|
395
396
|
node.get_parent().remove_child(node)
|
|
396
397
|
node.free()
|
|
397
398
|
var carried: GdhPaths = GdhPaths.new()
|
|
@@ -409,10 +410,25 @@ func delete_node(args: Dictionary) -> Dictionary:
|
|
|
409
410
|
var answer: Dictionary = { "ok": true, "deletedNodePath": at }
|
|
410
411
|
if not went.is_empty():
|
|
411
412
|
answer["deletedWithIt"] = went
|
|
413
|
+
if not cut.is_empty():
|
|
414
|
+
answer["connectionsRemoved"] = cut
|
|
412
415
|
answer.merge(GdhKin.follow(scene_path, at, "", carried))
|
|
413
416
|
return answer
|
|
414
417
|
|
|
415
418
|
|
|
419
|
+
## The connections [param scene]'s file holds from or to [param at] or a node under it, which go
|
|
420
|
+
## with the node; one an instanced scene makes inside itself is that scene's, and stays there.
|
|
421
|
+
static func _connections_with(scene: GdhSceneFile, at: String) -> Array[Dictionary]:
|
|
422
|
+
var cut: Array[Dictionary] = []
|
|
423
|
+
for held: Dictionary in scene.held_connections():
|
|
424
|
+
var ends: Array[String] = [str(held["sourceNodePath"]), str(held["targetNodePath"])]
|
|
425
|
+
for end: String in ends:
|
|
426
|
+
if end == at or end.begins_with(at + "/"):
|
|
427
|
+
cut.append(held)
|
|
428
|
+
break
|
|
429
|
+
return cut
|
|
430
|
+
|
|
431
|
+
|
|
416
432
|
func duplicate_node(args: Dictionary) -> Dictionary:
|
|
417
433
|
var project_path: String = str(args.get("projectPath", ""))
|
|
418
434
|
var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
|
|
@@ -405,7 +405,9 @@ func _add_inherited(answer: Dictionary, seen: Array[String]) -> void:
|
|
|
405
405
|
if above.is_empty():
|
|
406
406
|
break
|
|
407
407
|
inherits_from.append(base_path)
|
|
408
|
-
for list_name: String in [
|
|
408
|
+
for list_name: String in [
|
|
409
|
+
"signals", "variables", "functions", "constants", "enums", "inner_classes",
|
|
410
|
+
]:
|
|
409
411
|
var mine: Array[Dictionary] = answer[list_name]
|
|
410
412
|
var theirs: Array[Dictionary] = above[list_name]
|
|
411
413
|
for member: Dictionary in theirs:
|
package/build/keeper.js
CHANGED
|
@@ -6946,7 +6946,7 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
6946
6946
|
});
|
|
6947
6947
|
|
|
6948
6948
|
// src/keeper.ts
|
|
6949
|
-
import { closeSync as closeSync3, existsSync as
|
|
6949
|
+
import { closeSync as closeSync3, existsSync as existsSync3, mkdtempSync as mkdtempSync4, openSync as openSync4, readFileSync as readFileSync9, rmSync as rmSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
6950
6950
|
import { tmpdir as tmpdir8 } from "node:os";
|
|
6951
6951
|
import { join as join10 } from "node:path";
|
|
6952
6952
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
@@ -7435,7 +7435,6 @@ function throughHelper(how, command, args, files, assembly = helperAssembly()) {
|
|
|
7435
7435
|
// src/engine-slots.ts
|
|
7436
7436
|
import {
|
|
7437
7437
|
closeSync as closeSync2,
|
|
7438
|
-
existsSync as existsSync2,
|
|
7439
7438
|
mkdirSync,
|
|
7440
7439
|
openSync as openSync2,
|
|
7441
7440
|
readdirSync,
|
|
@@ -8834,15 +8833,29 @@ function holderOf(content) {
|
|
|
8834
8833
|
if (typeof parsed !== "object" || parsed === null) {
|
|
8835
8834
|
return null;
|
|
8836
8835
|
}
|
|
8837
|
-
const {
|
|
8838
|
-
|
|
8836
|
+
const { by, game, engine } = parsed;
|
|
8837
|
+
const named = namedProcess(parsed);
|
|
8838
|
+
if (named === null) {
|
|
8839
8839
|
return null;
|
|
8840
8840
|
}
|
|
8841
|
-
|
|
8841
|
+
const runs = namedProcess(engine);
|
|
8842
|
+
return {
|
|
8843
|
+
...named,
|
|
8844
|
+
...typeof by === "string" && by !== "" ? { by } : {},
|
|
8845
|
+
...game === true ? { game } : {},
|
|
8846
|
+
...runs === null ? {} : { engine: runs }
|
|
8847
|
+
};
|
|
8842
8848
|
} catch {
|
|
8843
8849
|
return null;
|
|
8844
8850
|
}
|
|
8845
8851
|
}
|
|
8852
|
+
function namedProcess(value) {
|
|
8853
|
+
if (typeof value !== "object" || value === null) {
|
|
8854
|
+
return null;
|
|
8855
|
+
}
|
|
8856
|
+
const { pid, started } = value;
|
|
8857
|
+
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && typeof started === "number" ? { pid, started } : null;
|
|
8858
|
+
}
|
|
8846
8859
|
async function ownStart() {
|
|
8847
8860
|
const said = (await startTimesOf([process4.pid])).get(process4.pid);
|
|
8848
8861
|
return said ?? Math.round(Date.now() - process4.uptime() * 1000);
|
|
@@ -8854,7 +8867,18 @@ function handSlot(lock, from, to) {
|
|
|
8854
8867
|
} catch {
|
|
8855
8868
|
return false;
|
|
8856
8869
|
}
|
|
8857
|
-
|
|
8870
|
+
const holder = content === null ? null : holderOf(content);
|
|
8871
|
+
return holder?.pid === from && writeWhole(lock, JSON.stringify(holder.game === true ? { ...to, game: true } : to));
|
|
8872
|
+
}
|
|
8873
|
+
function nameSlotEngine(lock, holder, engine) {
|
|
8874
|
+
let content;
|
|
8875
|
+
try {
|
|
8876
|
+
content = readWhole(lock);
|
|
8877
|
+
} catch {
|
|
8878
|
+
return false;
|
|
8879
|
+
}
|
|
8880
|
+
const named = content === null ? null : holderOf(content);
|
|
8881
|
+
return named?.pid === holder && writeWhole(lock, JSON.stringify({ ...named, engine }));
|
|
8858
8882
|
}
|
|
8859
8883
|
function giveBackSlot(lock, pid) {
|
|
8860
8884
|
try {
|
|
@@ -9314,7 +9338,7 @@ var IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
|
|
|
9314
9338
|
var hashes = new Map;
|
|
9315
9339
|
|
|
9316
9340
|
// src/scratch.ts
|
|
9317
|
-
import { existsSync as
|
|
9341
|
+
import { existsSync as existsSync2, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
|
|
9318
9342
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
9319
9343
|
import { join as join4 } from "node:path";
|
|
9320
9344
|
import process6 from "node:process";
|
|
@@ -9359,7 +9383,7 @@ function tryAgain(path, attempt) {
|
|
|
9359
9383
|
lastTryOnExit();
|
|
9360
9384
|
}
|
|
9361
9385
|
const after = AGAIN_AFTER_MS[attempt];
|
|
9362
|
-
if (after === undefined || !
|
|
9386
|
+
if (after === undefined || !existsSync2(path)) {
|
|
9363
9387
|
waiting.delete(path);
|
|
9364
9388
|
if (after === undefined) {
|
|
9365
9389
|
atExit.add(path);
|
|
@@ -10864,7 +10888,7 @@ async function startThroughHelper(spec, output, stopFile) {
|
|
|
10864
10888
|
exited.now = true;
|
|
10865
10889
|
});
|
|
10866
10890
|
for (let waited = 0;waited < DESKTOP_START_MS; waited += 50) {
|
|
10867
|
-
const pid =
|
|
10891
|
+
const pid = existsSync3(pidFile) ? Number(readFileSync9(pidFile, "utf8").trim()) : Number.NaN;
|
|
10868
10892
|
if (Number.isInteger(pid) && pid > 0) {
|
|
10869
10893
|
return { child: started.child, pid };
|
|
10870
10894
|
}
|
|
@@ -11062,10 +11086,12 @@ async function keep() {
|
|
|
11062
11086
|
});
|
|
11063
11087
|
const slot = run?.slot;
|
|
11064
11088
|
if (slot !== undefined && keeperStarted !== undefined) {
|
|
11089
|
+
const gameStarted = (await startTimesOf([pid])).get(pid);
|
|
11065
11090
|
handSlot(slot.lock, slot.from, {
|
|
11066
11091
|
pid: process.pid,
|
|
11067
11092
|
started: await keeperStarted,
|
|
11068
|
-
by: `${slot.by}, through the keeper process that runs it
|
|
11093
|
+
by: `${slot.by}, through the keeper process that runs it`,
|
|
11094
|
+
...gameStarted === undefined ? {} : { engine: { pid, started: gameStarted } }
|
|
11069
11095
|
});
|
|
11070
11096
|
}
|
|
11071
11097
|
let { code, signal } = await exited;
|
|
@@ -11101,6 +11127,9 @@ async function keep() {
|
|
|
11101
11127
|
restartedFrom: [...note.restartedFrom ?? [], before]
|
|
11102
11128
|
};
|
|
11103
11129
|
writeNoteIfItCan(note);
|
|
11130
|
+
if (slot !== undefined) {
|
|
11131
|
+
nameSlotEngine(slot.lock, process.pid, { pid: next.pid, started: next.startedAt });
|
|
11132
|
+
}
|
|
11104
11133
|
end = () => {
|
|
11105
11134
|
try {
|
|
11106
11135
|
process.kill(next.pid);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.57",
|
|
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",
|