gdharness 1.3.68 → 1.3.70
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 +22 -6
- package/build/godot/addons/gdharness_editor/file_reads.gd +3 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +141 -1
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +82 -2
- package/build/godot/operations/file_reads.gd +3 -0
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -21188,7 +21188,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
21188
21188
|
name: "scene_tree",
|
|
21189
21189
|
title: "Scene tree",
|
|
21190
21190
|
effect: "read",
|
|
21191
|
-
description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. type is the engine class, and a node whose script declares a class_name names it under scriptClass, as scene_node get does. A node that instances another scene names that file under instanceOf, and the nodes listed under it come from that file rather than this one. Its editableChildren says whether this scene, or a scene it inherits, keeps values of its own for them (scene_node set editableChildren). The root of a scene inheriting another names the scene it inherits under inherits, and every node that scene or one further down makes names its file under inheritedFrom. This file holds only the values it changes on them. A node in any group answers groups, sorted, from this file and every scene it comes from. Needs no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without making its nodes, so no script of theirs runs (autoloads still start once it has answered, as in every headless call). For the nodes of a game that is running, runtime_inspect tree.",
|
|
21191
|
+
description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. type is the engine class, and a node whose script declares a class_name names it under scriptClass, as scene_node get does. A node that instances another scene names that file under instanceOf, and the nodes listed under it come from that file rather than this one. Its editableChildren says whether this scene, or a scene it inherits, keeps values of its own for them (scene_node set editableChildren). One saved as a placeholder answers placeholder true, with InstancePlaceholder as its type, as a game loads it (scene_node set placeholder). The root of a scene inheriting another names the scene it inherits under inherits, and every node that scene or one further down makes names its file under inheritedFrom. This file holds only the values it changes on them. A node in any group answers groups, sorted, from this file and every scene it comes from. Needs no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without making its nodes, so no script of theirs runs (autoloads still start once it has answered, as in every headless call). For the nodes of a game that is running, runtime_inspect tree.",
|
|
21192
21192
|
parameters: {
|
|
21193
21193
|
projectPath: PROJECT_PATH,
|
|
21194
21194
|
scenePath: SCENE_PATH,
|
|
@@ -21256,6 +21256,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
21256
21256
|
ops: ["set"],
|
|
21257
21257
|
description: "set: on an instance of another scene, whether its children are editable here, as the editor's Editable Children makes them. True lets scene_node set give the nodes inside that instance values of this scene's own, which no other instance shares. An instance nested inside another takes it once the one around it has editable children. False drops every value this scene keeps for them, and every node it added below one of them. The answer names the nodes whose values went under droppedValues (they stay, with the instance's own values) and the added nodes that went under droppedNodes. A node added straight under the instance stays. The answer says editableChildren as the file now holds it."
|
|
21258
21258
|
},
|
|
21259
|
+
placeholder: {
|
|
21260
|
+
type: "boolean",
|
|
21261
|
+
ops: ["add", "set"],
|
|
21262
|
+
description: "add, with instancePath, or set, on an instance this scene places: whether it is saved as the editor's Load as Placeholder saves it, as `instance_placeholder` in the file. A game then gets an InstancePlaceholder there and loads the scene itself, while the editor still shows it. A placeholder loads without its children, so it never has editableChildren too. Passing editableChildren false with placeholder true drops the children's saved values. The answer says placeholder as the file now holds it."
|
|
21263
|
+
},
|
|
21259
21264
|
addGroups: {
|
|
21260
21265
|
type: "array",
|
|
21261
21266
|
items: { type: "string" },
|
|
@@ -21310,12 +21315,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
21310
21315
|
},
|
|
21311
21316
|
get: {
|
|
21312
21317
|
effect: "read",
|
|
21313
|
-
summary: "read a node's class, path, properties and where its values come from, and a TileMapLayer's or TileMap's cells as set_tilemap_cells takes them, the first 1000 with cellCount for all. An instance of another scene also answers instanceOf
|
|
21318
|
+
summary: "read a node's class, path, properties and where its values come from, and a TileMapLayer's or TileMap's cells as set_tilemap_cells takes them, the first 1000 with cellCount for all. An instance of another scene also answers instanceOf, editableChildren and placeholder",
|
|
21314
21319
|
requires: ["nodePath"]
|
|
21315
21320
|
},
|
|
21316
21321
|
set: {
|
|
21317
21322
|
effect: "destructive",
|
|
21318
|
-
summary: "set properties on a node, make the children of an instance editable, or put it in groups or take it out of them, or any of them together",
|
|
21323
|
+
summary: "set properties on a node, make the children of an instance editable or the instance a placeholder, or put it in groups or take it out of them, or any of them together",
|
|
21319
21324
|
requires: ["nodePath"]
|
|
21320
21325
|
},
|
|
21321
21326
|
duplicate: {
|
|
@@ -21972,7 +21977,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
21972
21977
|
},
|
|
21973
21978
|
wait: {
|
|
21974
21979
|
effect: "read",
|
|
21975
|
-
summary: "wait for the run to end,
|
|
21980
|
+
summary: "wait for the run to end, to be held by its debugger at a breakpoint, a pause or a step, or to have crashed, then answer as editor_output does",
|
|
21976
21981
|
requires: []
|
|
21977
21982
|
},
|
|
21978
21983
|
profile: {
|
|
@@ -59398,6 +59403,8 @@ ${error.text}`;
|
|
|
59398
59403
|
const cancelled = callSignal();
|
|
59399
59404
|
let held = false;
|
|
59400
59405
|
let holdAsked = 0;
|
|
59406
|
+
let crashedAt;
|
|
59407
|
+
let crashed = false;
|
|
59401
59408
|
while (cancelled?.aborted !== true && await this.runStillGoing(run) && Date.now() < until) {
|
|
59402
59409
|
this.drainEditorOutput(run);
|
|
59403
59410
|
this.drainTranscript(run);
|
|
@@ -59407,13 +59414,19 @@ ${error.text}`;
|
|
|
59407
59414
|
held = true;
|
|
59408
59415
|
break;
|
|
59409
59416
|
}
|
|
59417
|
+
crashedAt ??= run.log.crashReport() === undefined ? undefined : Date.now();
|
|
59418
|
+
}
|
|
59419
|
+
if (crashedAt !== undefined && Date.now() - crashedAt >= CRASH_EXIT_GRACE_MS) {
|
|
59420
|
+
crashed = true;
|
|
59421
|
+
break;
|
|
59410
59422
|
}
|
|
59411
59423
|
await delay10(RUN_POLL_MS);
|
|
59412
59424
|
}
|
|
59413
59425
|
if (!await this.runStillGoing(run)) {
|
|
59414
59426
|
return await this.handleGetDebugOutput(args);
|
|
59415
59427
|
}
|
|
59416
|
-
|
|
59428
|
+
const after = Date.now() - began;
|
|
59429
|
+
return await this.handleGetDebugOutput(args, held ? { heldAfterMs: after } : crashed ? { crashedAfterMs: after } : { ranOutMs: budgetMs });
|
|
59417
59430
|
}
|
|
59418
59431
|
async heldByItsDebugger(run) {
|
|
59419
59432
|
if (!run.throughEditor) {
|
|
@@ -59611,6 +59624,9 @@ ${error.text}`;
|
|
|
59611
59624
|
if (waited !== undefined && "heldAfterMs" in waited) {
|
|
59612
59625
|
notes.push(`The wait answered after ${waited.heldAfterMs}ms because the game is held by its debugger, where heldAt says, and a held game does not end: debug_control continue lets it go, and editor_run wait then waits on.`);
|
|
59613
59626
|
}
|
|
59627
|
+
if (waited !== undefined && "crashedAfterMs" in waited) {
|
|
59628
|
+
notes.push(`The wait answered after ${waited.crashedAfterMs}ms because the game crashed and printed its crash report, under entries. Its process has not ended yet, since the system can hold a crashed process while it reports the crash, as Windows Error Reporting does. Nothing more will come from the game. editor_run stop ends it now.`);
|
|
59629
|
+
}
|
|
59614
59630
|
const restarts = run.restartedFrom ?? [];
|
|
59615
59631
|
if (restarts.length > 0) {
|
|
59616
59632
|
const chain = [...restarts.map((one) => one.pid), run.pid].join(" into ");
|
|
@@ -61552,7 +61568,7 @@ async function runGodotServer() {
|
|
|
61552
61568
|
const server = new GodotServer;
|
|
61553
61569
|
await server.run();
|
|
61554
61570
|
}
|
|
61555
|
-
var TYPE_ARGUMENTS, UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, HIDDEN_WITH_SOUND = "A hidden editor plays no sound, so silent: false is refused with it.", HIDDEN_WITH_SOUND_FIX = "Pass hidden: false to hear the editor, or leave silent out", SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, SLOW_ANSWER_MS = 1000, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_TIMEOUT_MS = 600000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, HOLD_ASKED_EVERY_MS = 1000, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
61571
|
+
var TYPE_ARGUMENTS, UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, HIDDEN_WITH_SOUND = "A hidden editor plays no sound, so silent: false is refused with it.", HIDDEN_WITH_SOUND_FIX = "Pass hidden: false to hear the editor, or leave silent out", SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, SLOW_ANSWER_MS = 1000, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_TIMEOUT_MS = 600000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, HOLD_ASKED_EVERY_MS = 1000, CRASH_EXIT_GRACE_MS = 1e4, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
61556
61572
|
var init_server = __esm(() => {
|
|
61557
61573
|
init_dist2();
|
|
61558
61574
|
init_stdio();
|
|
@@ -186,6 +186,7 @@ static func _lay(
|
|
|
186
186
|
elif state.is_node_instance_placeholder(index):
|
|
187
187
|
nodes[path] = _made("InstancePlaceholder")
|
|
188
188
|
nodes[path]["instanceOf"] = state.get_node_instance_placeholder(index)
|
|
189
|
+
nodes[path]["placeholder"] = true
|
|
189
190
|
elif str(state.get_node_type(index)).is_empty():
|
|
190
191
|
# An entry for a node of an instance that no longer has it, which the engine skips
|
|
191
192
|
# too.
|
|
@@ -367,6 +368,8 @@ static func _entry(
|
|
|
367
368
|
data["instanceOf"] = node["instanceOf"]
|
|
368
369
|
if node.has("editableChildren"):
|
|
369
370
|
data["editableChildren"] = node["editableChildren"]
|
|
371
|
+
if node.has("placeholder"):
|
|
372
|
+
data["placeholder"] = true
|
|
370
373
|
# An inherited scene's file holds only what it changes, so the read says what it inherits and
|
|
371
374
|
# which nodes come from there, as the editor greys them.
|
|
372
375
|
if node.has("inherits"):
|
|
@@ -79,6 +79,10 @@ var _copied_from: Dictionary = {}
|
|
|
79
79
|
var _reached: Dictionary = {}
|
|
80
80
|
## The file as it was opened, to compare its embedded resources with what the write saves.
|
|
81
81
|
var _text: String = ""
|
|
82
|
+
## The unique_id each placeholder put back as its instance had in the file, by the instance's object
|
|
83
|
+
## id. Scripts cannot set one, the engine gives the instance a new one as it packs it, and a scene
|
|
84
|
+
## inheriting this one finds a moved node by it.
|
|
85
|
+
var _held_ids: Dictionary = {}
|
|
82
86
|
## The engine properties a put back did not take on a node, as "id:name": the engine derives them
|
|
83
87
|
## from where the node is, as it keeps layout_mode at Container under a container, and stores the
|
|
84
88
|
## same from an editor.
|
|
@@ -124,6 +128,8 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
|
|
|
124
128
|
return { "ok": false, "error": "Failed to load %s as a scene" % path }
|
|
125
129
|
if root == null:
|
|
126
130
|
return { "ok": false, "error": "Failed to instantiate " + path }
|
|
131
|
+
if not Engine.is_editor_hint():
|
|
132
|
+
_unhold_placeholders()
|
|
127
133
|
_state = packed.get_state()
|
|
128
134
|
_base_paths = makers(_state)
|
|
129
135
|
if for_writing:
|
|
@@ -145,6 +151,133 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
|
|
|
145
151
|
return {}
|
|
146
152
|
|
|
147
153
|
|
|
154
|
+
## Puts back each placeholder this scene places as the instance itself, marked, as an editor opens
|
|
155
|
+
## it. An engine with no editor opens one as an InstancePlaceholder even in an edit state, and the
|
|
156
|
+
## scene then packs it as a node of that class with every value it holds, so a save lost the mark.
|
|
157
|
+
## The instance takes the placeholder's place, values, groups, connections and the node values
|
|
158
|
+
## naming it. One whose scene does not instance stays as it was.
|
|
159
|
+
func _unhold_placeholders() -> void:
|
|
160
|
+
var held: Array[InstancePlaceholder] = []
|
|
161
|
+
_placeholders_under(root, held)
|
|
162
|
+
var as_text: bool = path.ends_with(".tscn") and not held.is_empty()
|
|
163
|
+
var text: String = FileAccess.get_file_as_string(path) if as_text else ""
|
|
164
|
+
for one: InstancePlaceholder in held:
|
|
165
|
+
var packed: PackedScene = (
|
|
166
|
+
ResourceLoader.load(one.get_instance_path(), "PackedScene") as PackedScene
|
|
167
|
+
)
|
|
168
|
+
var instance: Node = (
|
|
169
|
+
packed.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE) if packed != null else null
|
|
170
|
+
)
|
|
171
|
+
if instance == null:
|
|
172
|
+
continue
|
|
173
|
+
instance.name = one.name
|
|
174
|
+
var values: Dictionary = one.get_stored_values()
|
|
175
|
+
for key: Variant in values:
|
|
176
|
+
instance.set(str(key), values[key])
|
|
177
|
+
var filed_id: String = _filed_id(text, _header_of(root.get_path_to(one)))
|
|
178
|
+
# replace_by moves the place and the groups.
|
|
179
|
+
one.replace_by(instance, true)
|
|
180
|
+
instance.owner = root
|
|
181
|
+
instance.scene_file_path = one.get_instance_path()
|
|
182
|
+
instance.set_scene_instance_load_placeholder(true)
|
|
183
|
+
if not filed_id.is_empty():
|
|
184
|
+
_held_ids[instance.get_instance_id()] = filed_id
|
|
185
|
+
_repointed(root, one, instance)
|
|
186
|
+
one.free()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
## How a text scene opens the entry of the node at [param at], a path from the root.
|
|
190
|
+
static func _header_of(at: NodePath) -> String:
|
|
191
|
+
var named: String = str(at.get_name(at.get_name_count() - 1))
|
|
192
|
+
var parent: String = "." if at.get_name_count() == 1 else str(at).get_base_dir()
|
|
193
|
+
return '[node name="%s" parent="%s"' % [named.c_escape(), parent.c_escape()]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
## The unique_id [param text] gives the node whose entry opens with [param header], or "".
|
|
197
|
+
static func _filed_id(text: String, header: String) -> String:
|
|
198
|
+
var id: RegEx = RegEx.create_from_string(" unique_id=(\\d+)")
|
|
199
|
+
for line: String in text.split("\n"):
|
|
200
|
+
if line.begins_with(header + " ") or line.begins_with(header + "]"):
|
|
201
|
+
var found: RegExMatch = id.search(line)
|
|
202
|
+
return "" if found == null else found.get_string(1)
|
|
203
|
+
return ""
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
## The unique_id to write back on each placeholder put back as its instance, by its entry's header,
|
|
207
|
+
## for those still in the scene.
|
|
208
|
+
func _ids_to_keep() -> Dictionary:
|
|
209
|
+
var keep: Dictionary = {}
|
|
210
|
+
for key: Variant in _held_ids:
|
|
211
|
+
var node: Node = instance_from_id(GdhRead.as_int(key)) as Node
|
|
212
|
+
if node != null and root.is_ancestor_of(node):
|
|
213
|
+
keep[_header_of(root.get_path_to(node))] = _held_ids[key]
|
|
214
|
+
return keep
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
## Saves [param packed] at [param file_path] as [method save_packed] does, with the unique_ids in
|
|
218
|
+
## [param keep].
|
|
219
|
+
static func _saved_keeping_ids(packed: PackedScene, file_path: String, keep: Dictionary) -> Error:
|
|
220
|
+
var saving: Error = GdhSceneUids.save_keeping_uid(packed, file_path)
|
|
221
|
+
return saving if saving != OK else _keep_ids(file_path, keep)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
## Writes back on the text scene at [param file_path] the unique_id of each entry in [param keep],
|
|
225
|
+
## by its header.
|
|
226
|
+
static func _keep_ids(file_path: String, keep: Dictionary) -> Error:
|
|
227
|
+
if keep.is_empty():
|
|
228
|
+
return OK
|
|
229
|
+
var lines: PackedStringArray = FileAccess.get_file_as_string(file_path).split("\n")
|
|
230
|
+
var id: RegEx = RegEx.create_from_string(" unique_id=\\d+")
|
|
231
|
+
var changed: bool = false
|
|
232
|
+
for index: int in lines.size():
|
|
233
|
+
for header: Variant in keep:
|
|
234
|
+
var opening: String = str(header)
|
|
235
|
+
if lines[index].begins_with(opening + " ") or lines[index].begins_with(opening + "]"):
|
|
236
|
+
lines[index] = id.sub(lines[index], " unique_id=" + str(keep[header]))
|
|
237
|
+
changed = true
|
|
238
|
+
if not changed:
|
|
239
|
+
return OK
|
|
240
|
+
var file: FileAccess = FileAccess.open(file_path, FileAccess.WRITE)
|
|
241
|
+
if file == null:
|
|
242
|
+
return FileAccess.get_open_error()
|
|
243
|
+
if not file.store_string("\n".join(lines)):
|
|
244
|
+
return ERR_FILE_CANT_WRITE
|
|
245
|
+
file.close()
|
|
246
|
+
return OK
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
## Every InstancePlaceholder below [param top] that this scene places, into [param into].
|
|
250
|
+
func _placeholders_under(top: Node, into: Array[InstancePlaceholder]) -> void:
|
|
251
|
+
for child: Node in top.get_children():
|
|
252
|
+
if child is InstancePlaceholder and child.owner == root:
|
|
253
|
+
into.append(child as InstancePlaceholder)
|
|
254
|
+
else:
|
|
255
|
+
_placeholders_under(child, into)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
## Points every value of a node of this scene that holds [param was], alone or in an array, at
|
|
259
|
+
## [param now].
|
|
260
|
+
func _repointed(top: Node, was: Node, now: Node) -> void:
|
|
261
|
+
if top.owner == root or top == root:
|
|
262
|
+
for declared: Dictionary in top.get_property_list():
|
|
263
|
+
if not GdhRead.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
264
|
+
continue
|
|
265
|
+
var named: String = str(declared["name"])
|
|
266
|
+
var value: Variant = top.get(named)
|
|
267
|
+
if value is Object and value == was:
|
|
268
|
+
top.set(named, now)
|
|
269
|
+
elif value is Array:
|
|
270
|
+
var listed: Array = value
|
|
271
|
+
if listed.has(was):
|
|
272
|
+
var swapped: Array = listed.duplicate()
|
|
273
|
+
for index: int in swapped.size():
|
|
274
|
+
if swapped[index] == was:
|
|
275
|
+
swapped[index] = now
|
|
276
|
+
top.set(named, swapped)
|
|
277
|
+
for child: Node in top.get_children():
|
|
278
|
+
_repointed(child, was, now)
|
|
279
|
+
|
|
280
|
+
|
|
148
281
|
## Why a write to the scene at [param scene_path], whose text is [param text], is refused, or "":
|
|
149
282
|
## an [ext_resource] naming no file the project has, by its uid or by its path. The engine loads the
|
|
150
283
|
## scene without it, so a save drops the entry and every value set from it, and the answer could
|
|
@@ -1013,6 +1146,7 @@ func _write(check: Callable) -> Dictionary:
|
|
|
1013
1146
|
if not missing.is_empty():
|
|
1014
1147
|
return refuse("Nothing was written: " + missing)
|
|
1015
1148
|
var also: Array[Dictionary] = _also_saved(packed.get_state())
|
|
1149
|
+
var keep: Dictionary = _ids_to_keep() if headless and path.ends_with(".tscn") else {}
|
|
1016
1150
|
close()
|
|
1017
1151
|
var previous: Variant = null
|
|
1018
1152
|
if FileAccess.file_exists(path):
|
|
@@ -1024,12 +1158,18 @@ func _write(check: Callable) -> Dictionary:
|
|
|
1024
1158
|
values.append_array(_stored_at(state, index).values())
|
|
1025
1159
|
var carried: Dictionary = GdhHeadlessCheck.embedded_in(values, {})
|
|
1026
1160
|
var written: Dictionary = save_packed(packed, path)
|
|
1161
|
+
if written.is_empty():
|
|
1162
|
+
var ids_kept: Error = _keep_ids(path, keep)
|
|
1163
|
+
if ids_kept != OK:
|
|
1164
|
+
written = {
|
|
1165
|
+
"ok": false, "error": "Failed to save %s: %s" % [path, error_string(ids_kept)],
|
|
1166
|
+
}
|
|
1027
1167
|
if not written.is_empty() or not headless or not path.ends_with(".tscn"):
|
|
1028
1168
|
if written.is_empty():
|
|
1029
1169
|
also_saved.append_array(also)
|
|
1030
1170
|
return written
|
|
1031
1171
|
var just_saved: GdhHeadlessCheck.GdhSaved = GdhHeadlessCheck.GdhSaved.new(
|
|
1032
|
-
path, _text, func() -> Error: return
|
|
1172
|
+
path, _text, func() -> Error: return _saved_keeping_ids(packed, path, keep)
|
|
1033
1173
|
)
|
|
1034
1174
|
var unkept: String = GdhHeadlessCheck.settle_saved(
|
|
1035
1175
|
just_saved,
|
|
@@ -263,9 +263,15 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
263
263
|
var parent_node_path: String = str(args.get("parentNodePath", "."))
|
|
264
264
|
var properties: Dictionary = _parse_properties_arg(args.get("properties", {}))
|
|
265
265
|
var adding: Array = GdhGroups.asked(args, "addGroups")
|
|
266
|
+
var placeholder: bool = GdhRead.as_bool(args.get("placeholder", false))
|
|
266
267
|
|
|
267
268
|
if node_name.is_empty():
|
|
268
269
|
return { "ok": false, "error": "Missing nodeName" }
|
|
270
|
+
if placeholder and instance_path.is_empty():
|
|
271
|
+
return {
|
|
272
|
+
"ok": false,
|
|
273
|
+
"error": "placeholder marks an instance of a scene, so add takes it with instancePath.",
|
|
274
|
+
}
|
|
269
275
|
var class_script: Script = null
|
|
270
276
|
if not node_type.is_empty() and not ClassDB.class_exists(node_type):
|
|
271
277
|
var declared: Dictionary = GdhSceneFile.script_class(node_type, "Node")
|
|
@@ -312,6 +318,8 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
312
318
|
parent.add_child(new_node)
|
|
313
319
|
GdhPlacing.place(new_node, place)
|
|
314
320
|
GdhPlacing.own(new_node, root)
|
|
321
|
+
if placeholder:
|
|
322
|
+
new_node.set_scene_instance_load_placeholder(true)
|
|
315
323
|
if class_script != null:
|
|
316
324
|
scene.asked(new_node, ["script"])
|
|
317
325
|
new_node.set_script(class_script)
|
|
@@ -357,6 +365,8 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
357
365
|
)
|
|
358
366
|
if unkept.is_empty() and saved.get_node_or_null(new_path) != null:
|
|
359
367
|
unkept = GdhGroups.unkept(saved.get_node(new_path), adding, [])
|
|
368
|
+
if unkept.is_empty() and placeholder:
|
|
369
|
+
unkept = _placeholder_unkept(saved, new_path, true)
|
|
360
370
|
return unkept
|
|
361
371
|
)
|
|
362
372
|
if not written.is_empty():
|
|
@@ -366,9 +376,25 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
366
376
|
_note_loads_as(answer, scene, new_path)
|
|
367
377
|
if packed != null:
|
|
368
378
|
answer["instanceOf"] = instance_path
|
|
379
|
+
answer["placeholder"] = placeholder
|
|
369
380
|
return answer
|
|
370
381
|
|
|
371
382
|
|
|
383
|
+
## Why the node at [param at] in [param saved], the scene as written, would not load with
|
|
384
|
+
## placeholder [param wanted], or "" when it would.
|
|
385
|
+
static func _placeholder_unkept(saved: Node, at: String, wanted: bool) -> String:
|
|
386
|
+
var node: Node = saved.get_node_or_null(at)
|
|
387
|
+
if node == null or _placeholder_of(node) == wanted:
|
|
388
|
+
return ""
|
|
389
|
+
return "%s would load with placeholder %s." % [at, not wanted]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
## Whether [param node] is an instance saved as a placeholder. An engine with no editor opens one as
|
|
393
|
+
## an InstancePlaceholder even in an edit state, and an editor opens the instance itself, marked.
|
|
394
|
+
static func _placeholder_of(node: Node) -> bool:
|
|
395
|
+
return node is InstancePlaceholder or node.get_scene_instance_load_placeholder()
|
|
396
|
+
|
|
397
|
+
|
|
372
398
|
func delete_node(args: Dictionary) -> Dictionary:
|
|
373
399
|
var project_path: String = str(args.get("projectPath", ""))
|
|
374
400
|
var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
|
|
@@ -1119,10 +1145,18 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
1119
1145
|
var editable: Variant = args.get("editableChildren")
|
|
1120
1146
|
var adding: Array = GdhGroups.asked(args, "addGroups")
|
|
1121
1147
|
var removing: Array = GdhGroups.asked(args, "removeGroups")
|
|
1122
|
-
|
|
1148
|
+
var placeholder: Variant = args.get("placeholder")
|
|
1149
|
+
if (
|
|
1150
|
+
properties.is_empty()
|
|
1151
|
+
and editable == null
|
|
1152
|
+
and placeholder == null
|
|
1153
|
+
and adding.is_empty()
|
|
1154
|
+
and removing.is_empty()
|
|
1155
|
+
):
|
|
1123
1156
|
return {
|
|
1124
1157
|
"ok": false,
|
|
1125
|
-
"error":
|
|
1158
|
+
"error":
|
|
1159
|
+
"set takes properties, editableChildren, placeholder, addGroups or removeGroups.",
|
|
1126
1160
|
}
|
|
1127
1161
|
|
|
1128
1162
|
var opened: Array = _open(scene_path, true)
|
|
@@ -1147,6 +1181,37 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
1147
1181
|
if not foreign.is_empty():
|
|
1148
1182
|
return scene.refuse(foreign)
|
|
1149
1183
|
|
|
1184
|
+
# A placeholder loads without its children, so the editor never keeps both settings on.
|
|
1185
|
+
var wants_placeholder: bool = placeholder != null and GdhRead.as_bool(placeholder)
|
|
1186
|
+
var wants_editable: bool = editable != null and GdhRead.as_bool(editable)
|
|
1187
|
+
if wants_placeholder and wants_editable:
|
|
1188
|
+
return scene.refuse(
|
|
1189
|
+
(
|
|
1190
|
+
"placeholder and editableChildren cannot both be true, "
|
|
1191
|
+
+ "since a placeholder loads without its children."
|
|
1192
|
+
)
|
|
1193
|
+
)
|
|
1194
|
+
if wants_placeholder and editable == null and scene.root.is_editable_instance(node):
|
|
1195
|
+
return scene.refuse(
|
|
1196
|
+
(
|
|
1197
|
+
(
|
|
1198
|
+
"%s has editable children, which a placeholder cannot keep. Pass "
|
|
1199
|
+
+ "editableChildren false with placeholder true to drop their saved values."
|
|
1200
|
+
)
|
|
1201
|
+
% scene.path_of(node)
|
|
1202
|
+
)
|
|
1203
|
+
)
|
|
1204
|
+
if wants_editable and placeholder == null and node.get_scene_instance_load_placeholder():
|
|
1205
|
+
return scene.refuse(
|
|
1206
|
+
(
|
|
1207
|
+
(
|
|
1208
|
+
"%s is a placeholder, which loads without its children. "
|
|
1209
|
+
+ "Pass placeholder false with editableChildren true."
|
|
1210
|
+
)
|
|
1211
|
+
% scene.path_of(node)
|
|
1212
|
+
)
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1150
1215
|
var dropped: Dictionary = {}
|
|
1151
1216
|
if editable != null:
|
|
1152
1217
|
var not_an_instance: String = GdhKin.set_editable(scene, node, GdhRead.as_bool(editable))
|
|
@@ -1154,6 +1219,16 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
1154
1219
|
return scene.refuse(not_an_instance)
|
|
1155
1220
|
if not GdhRead.as_bool(editable):
|
|
1156
1221
|
dropped = scene.held_under(scene.path_of(node))
|
|
1222
|
+
if placeholder != null:
|
|
1223
|
+
# The editor offers Load as Placeholder only on an instance this scene places itself.
|
|
1224
|
+
if node == scene.root or node.owner != scene.root or node.scene_file_path.is_empty():
|
|
1225
|
+
return scene.refuse(
|
|
1226
|
+
(
|
|
1227
|
+
"placeholder is a setting of an instance this scene places, and %s is not one."
|
|
1228
|
+
% scene.path_of(node)
|
|
1229
|
+
)
|
|
1230
|
+
)
|
|
1231
|
+
node.set_scene_instance_load_placeholder(GdhRead.as_bool(placeholder))
|
|
1157
1232
|
|
|
1158
1233
|
var old_path: String = scene.path_of(node)
|
|
1159
1234
|
scene.asked(node, properties.keys())
|
|
@@ -1219,6 +1294,8 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
1219
1294
|
"%s would load with editableChildren %s."
|
|
1220
1295
|
% [new_path, not GdhRead.as_bool(editable)]
|
|
1221
1296
|
)
|
|
1297
|
+
if unkept.is_empty() and placeholder != null:
|
|
1298
|
+
unkept = _placeholder_unkept(saved, new_path, GdhRead.as_bool(placeholder))
|
|
1222
1299
|
return unkept
|
|
1223
1300
|
)
|
|
1224
1301
|
if not written.is_empty():
|
|
@@ -1227,6 +1304,8 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
1227
1304
|
|
|
1228
1305
|
if editable != null:
|
|
1229
1306
|
answer["editableChildren"] = GdhRead.as_bool(editable)
|
|
1307
|
+
if placeholder != null:
|
|
1308
|
+
answer["placeholder"] = GdhRead.as_bool(placeholder)
|
|
1230
1309
|
if not dropped.is_empty():
|
|
1231
1310
|
answer["droppedValues"] = dropped["values"]
|
|
1232
1311
|
answer["droppedNodes"] = dropped["nodes"]
|
|
@@ -1404,6 +1483,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
|
|
|
1404
1483
|
if node != scene.root and node.owner != null and not node.scene_file_path.is_empty():
|
|
1405
1484
|
answer["instanceOf"] = node.scene_file_path
|
|
1406
1485
|
answer["editableChildren"] = scene.root.is_editable_instance(node)
|
|
1486
|
+
answer["placeholder"] = node.get_scene_instance_load_placeholder()
|
|
1407
1487
|
scene.close()
|
|
1408
1488
|
# Only an editor holds anything unsaved. A headless engine answering false would say it looked.
|
|
1409
1489
|
if Engine.is_editor_hint():
|
|
@@ -186,6 +186,7 @@ static func _lay(
|
|
|
186
186
|
elif state.is_node_instance_placeholder(index):
|
|
187
187
|
nodes[path] = _made("InstancePlaceholder")
|
|
188
188
|
nodes[path]["instanceOf"] = state.get_node_instance_placeholder(index)
|
|
189
|
+
nodes[path]["placeholder"] = true
|
|
189
190
|
elif str(state.get_node_type(index)).is_empty():
|
|
190
191
|
# An entry for a node of an instance that no longer has it, which the engine skips
|
|
191
192
|
# too.
|
|
@@ -367,6 +368,8 @@ static func _entry(
|
|
|
367
368
|
data["instanceOf"] = node["instanceOf"]
|
|
368
369
|
if node.has("editableChildren"):
|
|
369
370
|
data["editableChildren"] = node["editableChildren"]
|
|
371
|
+
if node.has("placeholder"):
|
|
372
|
+
data["placeholder"] = true
|
|
370
373
|
# An inherited scene's file holds only what it changes, so the read says what it inherits and
|
|
371
374
|
# which nodes come from there, as the editor greys them.
|
|
372
375
|
if node.has("inherits"):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.70",
|
|
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",
|