gdharness 1.2.40 → 1.2.41
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 +19 -12
- package/build/godot/addons/gdharness_editor/animation_reads.gd +11 -2
- package/build/godot/addons/gdharness_editor/bridge_client.gd +2 -1
- package/build/godot/addons/gdharness_editor/headless_check.gd +22 -11
- package/build/godot/addons/gdharness_editor/property_values.gd +1 -1
- package/build/godot/addons/gdharness_editor/reading.gd +15 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +9 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +39 -5
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +68 -15
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +12 -12
- package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +3 -1
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +11 -6
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +3 -1
- package/build/godot/operations/animation_reads.gd +11 -2
- package/build/godot/operations/editor_tools.gd +9 -0
- package/build/godot/operations/godot_operations.gd +2 -1
- package/build/godot/operations/property_values.gd +1 -1
- package/build/godot/operations/reading.gd +15 -0
- package/build/index.js +19 -12
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -11925,7 +11925,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
11925
11925
|
type: { type: "string", enum: ["property", "method"] },
|
|
11926
11926
|
nodePath: { type: "string", description: "Target node, relative to the AnimationPlayer root." },
|
|
11927
11927
|
property: { type: "string", description: "For property tracks: the property to animate." },
|
|
11928
|
-
method: {
|
|
11928
|
+
method: {
|
|
11929
|
+
type: "string",
|
|
11930
|
+
description: "For method tracks: the method to call. The answer's methodFound says whether the target node has it; a method it lacks is still added, since it may be written next, and until it exists each call the track makes fails."
|
|
11931
|
+
},
|
|
11929
11932
|
keyframes: {
|
|
11930
11933
|
type: "array",
|
|
11931
11934
|
items: {
|
|
@@ -12507,7 +12510,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12507
12510
|
{
|
|
12508
12511
|
name: "scene_signal",
|
|
12509
12512
|
title: "Scene signals",
|
|
12510
|
-
description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connect of a pair already connected says alreadyConnected, with flagsBefore when it changed the flags. ${WITHOUT_AN_EDITOR}`,
|
|
12513
|
+
description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connect of a pair already connected says alreadyConnected, with flagsBefore when it changed the flags. A connect to a method the target does not have is still written, as the editor's own dialog allows, since the method may be written next; methodFound false says so, and until the method exists the signal fails each time it is emitted. list marks editable false on a connection an instanced scene makes inside itself, which only that scene can change. ${WITHOUT_AN_EDITOR}`,
|
|
12511
12514
|
parameters: {
|
|
12512
12515
|
projectPath: PROJECT_PATH,
|
|
12513
12516
|
scenePath: SCENE_PATH,
|
|
@@ -12552,7 +12555,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12552
12555
|
{
|
|
12553
12556
|
name: "scene_animation",
|
|
12554
12557
|
title: "Animations",
|
|
12555
|
-
description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. read and read_states need 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 running the scene's scripts. The ops that write
|
|
12558
|
+
description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. read and read_states need 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 running the scene's scripts. The ops that write go through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.",
|
|
12556
12559
|
parameters: {
|
|
12557
12560
|
projectPath: PROJECT_PATH,
|
|
12558
12561
|
scenePath: SCENE_PATH,
|
|
@@ -12564,7 +12567,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12564
12567
|
animationName: {
|
|
12565
12568
|
type: "string",
|
|
12566
12569
|
ops: ["read", "create", "add_track", "add_state"],
|
|
12567
|
-
description: "The animation in the player. read: the one to answer with whole; without it, read lists them. add_state: the one that state plays."
|
|
12570
|
+
description: "The animation in the player. read: the one to answer with whole, its tracks a list; without it, read lists them, each with the number of its tracks under tracks. add_state: the one that state plays."
|
|
12568
12571
|
},
|
|
12569
12572
|
length: { type: "number", ops: ["create"], description: "create: seconds. Default 1." },
|
|
12570
12573
|
loopMode: {
|
|
@@ -12819,7 +12822,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12819
12822
|
controlType: {
|
|
12820
12823
|
type: "string",
|
|
12821
12824
|
ops: ["set_theme_color", "set_theme_font_size"],
|
|
12822
|
-
description: 'Theme ops: the Control class
|
|
12825
|
+
description: 'Theme ops: the theme type, a Control or Window class such as "Button", or the name of a type variation, which can be any name. A name that is no such class is set as a variation and answered with a note saying so.'
|
|
12823
12826
|
},
|
|
12824
12827
|
colorName: {
|
|
12825
12828
|
type: "string",
|
|
@@ -12832,7 +12835,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
12832
12835
|
ops: ["set_theme_font_size"],
|
|
12833
12836
|
description: 'set_theme_font_size: such as "font_size".'
|
|
12834
12837
|
},
|
|
12835
|
-
size: {
|
|
12838
|
+
size: {
|
|
12839
|
+
type: "number",
|
|
12840
|
+
ops: ["set_theme_font_size"],
|
|
12841
|
+
description: "set_theme_font_size: pixels, a whole number of 1 or more."
|
|
12842
|
+
}
|
|
12836
12843
|
},
|
|
12837
12844
|
requires: ["projectPath", "resourcePath"],
|
|
12838
12845
|
operations: {
|
|
@@ -13037,7 +13044,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13037
13044
|
requires: []
|
|
13038
13045
|
}
|
|
13039
13046
|
},
|
|
13040
|
-
answer: "start: scene is the scene the game started in, the project's main scene when none was given. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, 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. 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: close (the game was asked to close and exited, so exitCode is its own), 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. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
|
|
13047
|
+
answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, 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. 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: close (the game was asked to close and exited, so exitCode is its own), 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. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
|
|
13041
13048
|
defaultOperation: "start"
|
|
13042
13049
|
},
|
|
13043
13050
|
{
|
|
@@ -13425,7 +13432,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13425
13432
|
name: "runtime_input",
|
|
13426
13433
|
title: "Send input to the game",
|
|
13427
13434
|
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, mouse motion or drag. All of it works headless, where the viewport is the project's window size 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: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: 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. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
|
|
13428
|
-
answer:
|
|
13435
|
+
answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again, for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.`,
|
|
13429
13436
|
parameters: {
|
|
13430
13437
|
projectPath: RUNNING_PROJECT_PATH,
|
|
13431
13438
|
pid: RUNNING_PID,
|
|
@@ -46901,13 +46908,13 @@ class GodotServer {
|
|
|
46901
46908
|
stateMachinePath: readString(args, "stateMachinePath") ?? ""
|
|
46902
46909
|
}), async () => await bridge("read_animation_states"));
|
|
46903
46910
|
case "create":
|
|
46904
|
-
return await
|
|
46911
|
+
return await editorOrEngine("create_animation", "writes");
|
|
46905
46912
|
case "add_track":
|
|
46906
|
-
return await
|
|
46913
|
+
return await editorOrEngine("add_animation_track", "writes");
|
|
46907
46914
|
case "add_state":
|
|
46908
|
-
return await
|
|
46915
|
+
return await editorOrEngine("add_animation_state", "writes");
|
|
46909
46916
|
default:
|
|
46910
|
-
return await
|
|
46917
|
+
return await editorOrEngine("connect_animation_states", "writes");
|
|
46911
46918
|
}
|
|
46912
46919
|
case "script_edit":
|
|
46913
46920
|
return await this.handleRenameSymbol(args);
|
|
@@ -5,6 +5,7 @@ extends RefCounted
|
|
|
5
5
|
## stores, which runs no script of the scene's, so a read answers the same either way.
|
|
6
6
|
|
|
7
7
|
const FileReads = preload("file_reads.gd")
|
|
8
|
+
const Read = preload("reading.gd")
|
|
8
9
|
const Serialisation = preload("serialisation.gd")
|
|
9
10
|
|
|
10
11
|
const LOOP_MODE_NAMES: Dictionary[int, String] = {
|
|
@@ -60,7 +61,12 @@ static func _bare(scene_path: String, node_path: String, kind: String) -> Array:
|
|
|
60
61
|
return [null, read]
|
|
61
62
|
var type: String = str(read.get("type", ""))
|
|
62
63
|
if not ClassDB.class_exists(type) or not ClassDB.is_parent_class(type, kind):
|
|
63
|
-
|
|
64
|
+
var said: String = "%s not found at: %s" % [kind, node_path]
|
|
65
|
+
if read.is_empty() and FileReads.scene_node(scene_path, node_path.get_slice("/", 0)).is_empty():
|
|
66
|
+
said = Read.from_the_root(
|
|
67
|
+
said, node_path, str(FileReads.scene_node(scene_path, ".").get("name", ""))
|
|
68
|
+
)
|
|
69
|
+
return [null, {"ok": false, "error": said}]
|
|
64
70
|
var bare: Node = ClassDB.instantiate(type)
|
|
65
71
|
bare.name = str(read["name"])
|
|
66
72
|
var given: Dictionary = read["properties"]
|
|
@@ -164,7 +170,10 @@ static func state_machine(tree: AnimationTree, state_machine_path: String) -> An
|
|
|
164
170
|
## the path that failed, and what is there instead.
|
|
165
171
|
static func no_machine(tree: AnimationTree, state_machine_path: String) -> String:
|
|
166
172
|
if tree == null or tree.tree_root == null:
|
|
167
|
-
return
|
|
173
|
+
return (
|
|
174
|
+
"The AnimationTree has no tree_root. Make one with resource_edit create of an "
|
|
175
|
+
+ "AnimationNodeStateMachine, then give its res:// path to tree_root with scene_node set."
|
|
176
|
+
)
|
|
168
177
|
var current: AnimationNodeStateMachine = tree.tree_root as AnimationNodeStateMachine
|
|
169
178
|
if current == null:
|
|
170
179
|
return (
|
|
@@ -544,7 +544,8 @@ func send_tool_result(request_id: String, success: bool, result: Variant = null,
|
|
|
544
544
|
|
|
545
545
|
func _send_message(message: Dictionary) -> void:
|
|
546
546
|
if socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
|
|
547
|
-
|
|
547
|
+
# Keys in the order they were put in, which for a map read is the order the project keeps.
|
|
548
|
+
var sent: Error = socket.send_text(JSON.stringify(message, "", false))
|
|
548
549
|
if sent != OK:
|
|
549
550
|
push_error("gdharness: could not send to the server: " + error_string(sent))
|
|
550
551
|
|
|
@@ -147,12 +147,8 @@ static func reached(object: Object, property: String) -> Dictionary:
|
|
|
147
147
|
## Whether [param object] holds, at any depth, an embedded resource with a script that is not a tool.
|
|
148
148
|
static func holds_scripted_embedded(object: Object, seen: Dictionary) -> bool:
|
|
149
149
|
for value: Variant in stored(object).values():
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if not item is Resource or seen.has(item):
|
|
153
|
-
continue
|
|
154
|
-
var resource: Resource = item
|
|
155
|
-
if not resource.is_built_in():
|
|
150
|
+
for resource: Resource in resources_in(value):
|
|
151
|
+
if seen.has(resource) or not resource.is_built_in():
|
|
156
152
|
continue
|
|
157
153
|
seen[resource] = true
|
|
158
154
|
var script: Script = resource.get_script()
|
|
@@ -163,6 +159,25 @@ static func holds_scripted_embedded(object: Object, seen: Dictionary) -> bool:
|
|
|
163
159
|
return false
|
|
164
160
|
|
|
165
161
|
|
|
162
|
+
## The resources [param value] is or holds through arrays and dictionaries at any depth, keys
|
|
163
|
+
## included: a file embeds a resource kept as a map's value, the way an AnimationLibrary keeps its
|
|
164
|
+
## animations, as it embeds one held directly.
|
|
165
|
+
static func resources_in(value: Variant) -> Array[Resource]:
|
|
166
|
+
var found: Array[Resource] = []
|
|
167
|
+
if value is Resource:
|
|
168
|
+
found.append(value)
|
|
169
|
+
elif value is Array:
|
|
170
|
+
var items: Array = value
|
|
171
|
+
for item: Variant in items:
|
|
172
|
+
found.append_array(resources_in(item))
|
|
173
|
+
elif value is Dictionary:
|
|
174
|
+
var entries_held: Dictionary = value
|
|
175
|
+
for key: Variant in entries_held:
|
|
176
|
+
found.append_array(resources_in(key))
|
|
177
|
+
found.append_array(resources_in(entries_held[key]))
|
|
178
|
+
return found
|
|
179
|
+
|
|
180
|
+
|
|
166
181
|
## Puts the file at [param file_path] back as [param previous], or removes it when there was none,
|
|
167
182
|
## answering "" or a sentence saying that failed too.
|
|
168
183
|
static func put_back(file_path: String, previous: Variant) -> String:
|
|
@@ -370,11 +385,7 @@ static func settle_saved(
|
|
|
370
385
|
## Every embedded resource [param values] reach, at any depth, by its id in the file.
|
|
371
386
|
static func embedded_in(values: Array, found: Dictionary) -> Dictionary:
|
|
372
387
|
for value: Variant in values:
|
|
373
|
-
|
|
374
|
-
for item: Variant in items:
|
|
375
|
-
if not item is Resource:
|
|
376
|
-
continue
|
|
377
|
-
var resource: Resource = item
|
|
388
|
+
for resource: Resource in resources_in(value):
|
|
378
389
|
var id: String = resource.get_scene_unique_id()
|
|
379
390
|
if not resource.is_built_in() or id.is_empty() or found.has(id):
|
|
380
391
|
continue
|
|
@@ -590,7 +590,7 @@ static func _listed(type: int) -> bool:
|
|
|
590
590
|
|
|
591
591
|
## [param value] as a caller would write it, for a refusal to quote.
|
|
592
592
|
func shown(value: Variant) -> String:
|
|
593
|
-
return JSON.stringify(_values.serialize_value(value))
|
|
593
|
+
return JSON.stringify(_values.serialize_value(value), "", false)
|
|
594
594
|
|
|
595
595
|
|
|
596
596
|
## A dictionary carrying its own type name, as the serialiser writes it.
|
|
@@ -99,3 +99,18 @@ static func json_or_null(text: String) -> Variant:
|
|
|
99
99
|
if json.parse(text) != OK:
|
|
100
100
|
return null
|
|
101
101
|
return json.data
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
## [param said], the refusal for [param node_path] found nowhere in a scene whose root is called
|
|
105
|
+
## [param root_name], with how paths are read when the path starts with that name and the root has
|
|
106
|
+
## no child called that. Paths go from the root, so its own name there names a child of it, and a
|
|
107
|
+
## caller who read the root as Main in a tree wrote Main/Body and was told only that nothing is there.
|
|
108
|
+
static func from_the_root(said: String, node_path: String, root_name: String) -> String:
|
|
109
|
+
var first: String = node_path.get_slice("/", 0)
|
|
110
|
+
if root_name.is_empty() or first != root_name:
|
|
111
|
+
return said
|
|
112
|
+
var rest: String = node_path.substr(first.length() + 1)
|
|
113
|
+
var how: String = 'Paths go from the root, %s, whose own path is "."' % root_name
|
|
114
|
+
if not rest.is_empty():
|
|
115
|
+
how += ', so this one is "%s"' % rest
|
|
116
|
+
return "%s%s %s." % [said, "" if said.ends_with(".") else ".", how]
|
|
@@ -30,6 +30,7 @@ const HeadlessCheck = preload("headless_check.gd")
|
|
|
30
30
|
## How many times what scripts changed is put back before a write is refused for it.
|
|
31
31
|
const PUTTING_BACK_PASSES: int = 4
|
|
32
32
|
const PropertyValues = preload("property_values.gd")
|
|
33
|
+
const Read = preload("reading.gd")
|
|
33
34
|
|
|
34
35
|
## Whether a scan asked for through [method scan_later] is still to start, which a caller waiting
|
|
35
36
|
## for the editor's files to settle has to wait through as well.
|
|
@@ -150,6 +151,14 @@ func refuse(error: String) -> Dictionary:
|
|
|
150
151
|
return {"ok": false, "error": error}
|
|
151
152
|
|
|
152
153
|
|
|
154
|
+
## [param said], the refusal for [param node_path] found nowhere in this scene, with how a path is
|
|
155
|
+
## read when it starts with the root's own name.
|
|
156
|
+
func not_found(said: String, node_path: String) -> String:
|
|
157
|
+
if root == null or root.has_node(NodePath(node_path.get_slice("/", 0))):
|
|
158
|
+
return said
|
|
159
|
+
return Read.from_the_root(said, node_path, str(root.name))
|
|
160
|
+
|
|
161
|
+
|
|
153
162
|
func close() -> void:
|
|
154
163
|
if root != null:
|
|
155
164
|
root.free()
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
@tool
|
|
2
2
|
extends Node
|
|
3
3
|
|
|
4
|
-
## Animations and animation trees, edited in the open editor
|
|
4
|
+
## Animations and animation trees, edited in the open editor or, for a project no editor serves, by a
|
|
5
|
+
## headless engine.
|
|
5
6
|
|
|
6
7
|
const AnimationReads = preload("../animation_reads.gd")
|
|
8
|
+
const HeadlessCheck = preload("../headless_check.gd")
|
|
7
9
|
const PropertyValues = preload("../property_values.gd")
|
|
8
10
|
const Read = preload("../reading.gd")
|
|
9
11
|
const SceneFile = preload("../scene_file.gd")
|
|
@@ -46,7 +48,9 @@ func _open_at(scene_path: String, node_path: String, kind: String) -> Array:
|
|
|
46
48
|
return [scene, null, refused]
|
|
47
49
|
var node: Node = _find_node(scene.root, node_path)
|
|
48
50
|
if node == null or not node.is_class(kind):
|
|
49
|
-
return [
|
|
51
|
+
return [
|
|
52
|
+
scene, null, scene.refuse(scene.not_found("%s not found at: %s" % [kind, node_path], node_path))
|
|
53
|
+
]
|
|
50
54
|
var foreign: String = scene.foreign(node)
|
|
51
55
|
if not foreign.is_empty():
|
|
52
56
|
return [scene, null, scene.refuse(foreign)]
|
|
@@ -62,25 +66,55 @@ func _read_at(scene_path: String, node_path: String, kind: String) -> Array:
|
|
|
62
66
|
return [scene, null, refused]
|
|
63
67
|
var node: Node = _find_node(scene.root, node_path)
|
|
64
68
|
if node == null or not node.is_class(kind):
|
|
65
|
-
return [
|
|
69
|
+
return [
|
|
70
|
+
scene, null, scene.refuse(scene.not_found("%s not found at: %s" % [kind, node_path], node_path))
|
|
71
|
+
]
|
|
66
72
|
return [scene, node, {}]
|
|
67
73
|
|
|
68
74
|
|
|
69
75
|
## Writes [param scene] once [param check] passes, then the files of [param homes], answering
|
|
70
76
|
## [param answer] or a refusal.
|
|
71
77
|
func _finish(scene: SceneFile, check: Callable, homes: Dictionary, answer: Dictionary) -> Dictionary:
|
|
78
|
+
if not Engine.is_editor_hint():
|
|
79
|
+
for file: Variant in homes:
|
|
80
|
+
var home: Resource = homes[file]
|
|
81
|
+
if _scripted(home):
|
|
82
|
+
return (
|
|
83
|
+
scene
|
|
84
|
+
. refuse(
|
|
85
|
+
(
|
|
86
|
+
(
|
|
87
|
+
(
|
|
88
|
+
"Nothing was written: %s holds a resource with a script that is not a tool, and what "
|
|
89
|
+
+ "that script changed when a headless engine loaded it cannot be told apart from the "
|
|
90
|
+
+ "edit. "
|
|
91
|
+
)
|
|
92
|
+
% file
|
|
93
|
+
)
|
|
94
|
+
+ HeadlessCheck.REMEDY
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
)
|
|
72
98
|
var written: Dictionary = scene.write(check)
|
|
73
99
|
if not written.is_empty():
|
|
74
100
|
return written
|
|
75
101
|
for file: Variant in homes:
|
|
76
102
|
var resource: Resource = homes[file]
|
|
77
|
-
var saving: Error =
|
|
103
|
+
var saving: Error = SceneFile.save_keeping_uid(resource, str(file))
|
|
78
104
|
if saving != OK:
|
|
79
105
|
return {"ok": false, "error": "Failed to save %s: %s" % [file, error_string(saving)]}
|
|
80
|
-
|
|
106
|
+
if Engine.is_editor_hint():
|
|
107
|
+
EditorInterface.get_resource_filesystem().update_file(str(file))
|
|
81
108
|
return answer
|
|
82
109
|
|
|
83
110
|
|
|
111
|
+
## Whether [param resource], or one it embeds, has a script that is not a tool, which a headless
|
|
112
|
+
## engine ran as it loaded the file.
|
|
113
|
+
func _scripted(resource: Resource) -> bool:
|
|
114
|
+
var script: Script = resource.get_script()
|
|
115
|
+
return (script != null and not script.is_tool()) or HeadlessCheck.holds_scripted_embedded(resource, {})
|
|
116
|
+
|
|
117
|
+
|
|
84
118
|
func _parse_json_maybe(value: Variant) -> Variant:
|
|
85
119
|
if typeof(value) != TYPE_STRING:
|
|
86
120
|
return value
|
|
@@ -399,6 +399,19 @@ func _atlas(entry: Variant, index: int) -> Array:
|
|
|
399
399
|
if texture_path.split("/").has(".."):
|
|
400
400
|
return [false, "sources[%d].texture leaves the project: %s" % [index, texture_path]]
|
|
401
401
|
if not ResourceLoader.exists(texture_path):
|
|
402
|
+
# An image the engine has not imported loads as nothing, where "no texture" would send a caller
|
|
403
|
+
# looking for a file that is there.
|
|
404
|
+
if FileAccess.file_exists(texture_path) and not FileAccess.file_exists(texture_path + ".import"):
|
|
405
|
+
return [
|
|
406
|
+
false,
|
|
407
|
+
(
|
|
408
|
+
(
|
|
409
|
+
"sources[%d].texture: %s has not been imported, so the engine has no texture for it "
|
|
410
|
+
+ "yet. project_import reimport with resourcePath %s imports it."
|
|
411
|
+
)
|
|
412
|
+
% [index, texture_path, texture_path]
|
|
413
|
+
)
|
|
414
|
+
]
|
|
402
415
|
return [false, "sources[%d].texture: no texture at %s" % [index, texture_path]]
|
|
403
416
|
var texture: Texture2D = load(texture_path) as Texture2D
|
|
404
417
|
if texture == null:
|
|
@@ -720,13 +733,15 @@ func set_theme_color(args: Dictionary) -> Dictionary:
|
|
|
720
733
|
return {"ok": false, "error": "The theme saved without %s/%s" % [control_type, color_name]}
|
|
721
734
|
|
|
722
735
|
var stored: Color = saved.get_color(color_name, control_type)
|
|
723
|
-
return
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
736
|
+
return _noting_variation(
|
|
737
|
+
{
|
|
738
|
+
"ok": true,
|
|
739
|
+
"themePath": theme_path,
|
|
740
|
+
"controlType": control_type,
|
|
741
|
+
"colorName": color_name,
|
|
742
|
+
"color": {"r": stored.r, "g": stored.g, "b": stored.b, "a": stored.a}
|
|
743
|
+
}
|
|
744
|
+
)
|
|
730
745
|
|
|
731
746
|
|
|
732
747
|
func set_theme_font_size(args: Dictionary) -> Dictionary:
|
|
@@ -743,7 +758,23 @@ func set_theme_font_size(args: Dictionary) -> Dictionary:
|
|
|
743
758
|
if theme == null:
|
|
744
759
|
return {"ok": false, "error": "No Theme at %s. Create one there first." % theme_path}
|
|
745
760
|
|
|
746
|
-
|
|
761
|
+
# Godot reads a size of 0 or less as unset and answers the theme's default, so -3 would be read
|
|
762
|
+
# back as 16.
|
|
763
|
+
var pixels: float = Read.as_float(args.get("size"), 0.0)
|
|
764
|
+
if not is_finite(pixels) or pixels < 1 or pixels != floorf(pixels):
|
|
765
|
+
var shown: String = str(int(pixels)) if pixels == floorf(pixels) else str(pixels)
|
|
766
|
+
return {
|
|
767
|
+
"ok": false,
|
|
768
|
+
"error":
|
|
769
|
+
(
|
|
770
|
+
(
|
|
771
|
+
"size must be a whole number of pixels, 1 or more, not %s: Godot reads 0 or less as no size "
|
|
772
|
+
% shown
|
|
773
|
+
)
|
|
774
|
+
+ "and uses the theme's default instead."
|
|
775
|
+
)
|
|
776
|
+
}
|
|
777
|
+
theme.set_font_size(font_size_name, control_type, int(pixels))
|
|
747
778
|
|
|
748
779
|
var unsaved: String = _save_theme(theme, theme_path, "%s/font_sizes/%s" % [control_type, font_size_name])
|
|
749
780
|
if not unsaved.is_empty():
|
|
@@ -753,10 +784,32 @@ func set_theme_font_size(args: Dictionary) -> Dictionary:
|
|
|
753
784
|
if saved == null or not saved.has_font_size(font_size_name, control_type):
|
|
754
785
|
return {"ok": false, "error": "The theme saved without %s/%s" % [control_type, font_size_name]}
|
|
755
786
|
|
|
756
|
-
return
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
787
|
+
return _noting_variation(
|
|
788
|
+
{
|
|
789
|
+
"ok": true,
|
|
790
|
+
"themePath": theme_path,
|
|
791
|
+
"controlType": control_type,
|
|
792
|
+
"fontSizeName": font_size_name,
|
|
793
|
+
"size": saved.get_font_size(font_size_name, control_type)
|
|
794
|
+
}
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
## [param answer] with a note when its controlType names no class a theme styles. Any name is a
|
|
799
|
+
## theme type, so one that is no class is a type variation, which styles only a control whose
|
|
800
|
+
## theme_type_variation names it, and a misspelt "Buton" would otherwise read as Button set.
|
|
801
|
+
func _noting_variation(answer: Dictionary) -> Dictionary:
|
|
802
|
+
var type: String = str(answer.get("controlType", ""))
|
|
803
|
+
if (
|
|
804
|
+
ClassDB.class_exists(type)
|
|
805
|
+
and (ClassDB.is_parent_class(type, "Control") or ClassDB.is_parent_class(type, "Window"))
|
|
806
|
+
):
|
|
807
|
+
return answer
|
|
808
|
+
answer["note"] = (
|
|
809
|
+
(
|
|
810
|
+
"%s is not a Control or Window class, so this is a type variation: it styles only a control "
|
|
811
|
+
+ "whose theme_type_variation is %s."
|
|
812
|
+
)
|
|
813
|
+
% [type, type]
|
|
814
|
+
)
|
|
815
|
+
return answer
|
|
@@ -111,7 +111,7 @@ func _kept(scene: SceneFile, saved: Node, node: Node, names: Array) -> String:
|
|
|
111
111
|
"%s.%s would load from the file as %s: the scene does not keep it, because the property "
|
|
112
112
|
+ "is not one a scene stores or the node belongs to another scene."
|
|
113
113
|
)
|
|
114
|
-
% [at, property, JSON.stringify(_values.serialize_value(loaded))]
|
|
114
|
+
% [at, property, JSON.stringify(_values.serialize_value(loaded), "", false)]
|
|
115
115
|
)
|
|
116
116
|
return ""
|
|
117
117
|
|
|
@@ -312,7 +312,7 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
312
312
|
var root: Node = scene.root
|
|
313
313
|
var parent: Node = _find_node(root, parent_node_path)
|
|
314
314
|
if not parent:
|
|
315
|
-
return scene.refuse("Parent node not found: " + parent_node_path)
|
|
315
|
+
return scene.refuse(scene.not_found("Parent node not found: " + parent_node_path, parent_node_path))
|
|
316
316
|
var foreign: String = scene.foreign(parent)
|
|
317
317
|
if not foreign.is_empty():
|
|
318
318
|
return scene.refuse(foreign)
|
|
@@ -387,7 +387,7 @@ func delete_node(args: Dictionary) -> Dictionary:
|
|
|
387
387
|
|
|
388
388
|
var node: Node = _find_node(scene.root, node_path)
|
|
389
389
|
if not node:
|
|
390
|
-
return scene.refuse("Node not found: " + node_path)
|
|
390
|
+
return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
|
|
391
391
|
var fixed: String = scene.fixed(node)
|
|
392
392
|
if not fixed.is_empty():
|
|
393
393
|
return scene.refuse(fixed)
|
|
@@ -427,7 +427,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
|
|
|
427
427
|
var root: Node = scene.root
|
|
428
428
|
var source: Node = _find_node(root, node_path)
|
|
429
429
|
if not source:
|
|
430
|
-
return scene.refuse("Node not found: " + node_path)
|
|
430
|
+
return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
|
|
431
431
|
if source == root:
|
|
432
432
|
return scene.refuse("The root of %s cannot be copied into its own scene." % scene_path)
|
|
433
433
|
var foreign: String = scene.foreign(source)
|
|
@@ -438,7 +438,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
|
|
|
438
438
|
if not parent_path.is_empty():
|
|
439
439
|
target_parent = _find_node(root, parent_path)
|
|
440
440
|
if not target_parent:
|
|
441
|
-
return scene.refuse("Parent not found: " + parent_path)
|
|
441
|
+
return scene.refuse(scene.not_found("Parent not found: " + parent_path, parent_path))
|
|
442
442
|
foreign = scene.foreign(target_parent)
|
|
443
443
|
if not foreign.is_empty():
|
|
444
444
|
return scene.refuse(foreign)
|
|
@@ -491,9 +491,9 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
491
491
|
var node: Node = _find_node(root, node_path)
|
|
492
492
|
var new_parent: Node = _find_node(root, new_parent_path)
|
|
493
493
|
if not node:
|
|
494
|
-
return scene.refuse("Node not found: " + node_path)
|
|
494
|
+
return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
|
|
495
495
|
if not new_parent:
|
|
496
|
-
return scene.refuse("New parent not found: " + new_parent_path)
|
|
496
|
+
return scene.refuse(scene.not_found("New parent not found: " + new_parent_path, new_parent_path))
|
|
497
497
|
var fixed: String = scene.fixed(node)
|
|
498
498
|
if not fixed.is_empty():
|
|
499
499
|
return scene.refuse(fixed)
|
|
@@ -562,7 +562,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
562
562
|
|
|
563
563
|
var node: Node = _find_node(scene.root, node_path)
|
|
564
564
|
if not node:
|
|
565
|
-
return scene.refuse("Node not found: " + node_path)
|
|
565
|
+
return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
|
|
566
566
|
var foreign: String = scene.foreign(node)
|
|
567
567
|
if not foreign.is_empty():
|
|
568
568
|
return scene.refuse(foreign)
|
|
@@ -606,7 +606,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
|
|
|
606
606
|
|
|
607
607
|
var node: Node = _find_node(scene.root, node_path)
|
|
608
608
|
if not node:
|
|
609
|
-
return scene.refuse("Node not found: " + node_path)
|
|
609
|
+
return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
|
|
610
610
|
|
|
611
611
|
# Against the default the node would have in no scene: its script's for what the script
|
|
612
612
|
# declares, the class's for the rest. Compared with a bare instance of the class, every exported
|
|
@@ -792,9 +792,9 @@ func _connection_ends(
|
|
|
792
792
|
var source: Node = _find_node(scene.root, source_node_path)
|
|
793
793
|
var target: Node = _find_node(scene.root, target_node_path)
|
|
794
794
|
if not source:
|
|
795
|
-
return [null, "Source node not found: " + source_node_path]
|
|
795
|
+
return [null, scene.not_found("Source node not found: " + source_node_path, source_node_path)]
|
|
796
796
|
if not target:
|
|
797
|
-
return [null, "Target node not found: " + target_node_path]
|
|
797
|
+
return [null, scene.not_found("Target node not found: " + target_node_path, target_node_path)]
|
|
798
798
|
# pack drops a connection with either end inside an instanced scene, and the answer said it was
|
|
799
799
|
# made or removed.
|
|
800
800
|
for end: Node in [source, target]:
|
|
@@ -897,7 +897,7 @@ func list_connections(args: Dictionary) -> Dictionary:
|
|
|
897
897
|
if not filter_path.is_empty():
|
|
898
898
|
filtered = _find_node(root, filter_path)
|
|
899
899
|
if filtered == null:
|
|
900
|
-
return scene.refuse("Node not found: " + filter_path)
|
|
900
|
+
return scene.refuse(scene.not_found("Node not found: " + filter_path, filter_path))
|
|
901
901
|
var nodes: Array = []
|
|
902
902
|
_collect_nodes_recursive(root, ".", nodes)
|
|
903
903
|
|
|
@@ -99,3 +99,18 @@ static func json_or_null(text: String) -> Variant:
|
|
|
99
99
|
if json.parse(text) != OK:
|
|
100
100
|
return null
|
|
101
101
|
return json.data
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
## [param said], the refusal for [param node_path] found nowhere in a scene whose root is called
|
|
105
|
+
## [param root_name], with how paths are read when the path starts with that name and the root has
|
|
106
|
+
## no child called that. Paths go from the root, so its own name there names a child of it, and a
|
|
107
|
+
## caller who read the root as Main in a tree wrote Main/Body and was told only that nothing is there.
|
|
108
|
+
static func from_the_root(said: String, node_path: String, root_name: String) -> String:
|
|
109
|
+
var first: String = node_path.get_slice("/", 0)
|
|
110
|
+
if root_name.is_empty() or first != root_name:
|
|
111
|
+
return said
|
|
112
|
+
var rest: String = node_path.substr(first.length() + 1)
|
|
113
|
+
var how: String = 'Paths go from the root, %s, whose own path is "."' % root_name
|
|
114
|
+
if not rest.is_empty():
|
|
115
|
+
how += ', so this one is "%s"' % rest
|
|
116
|
+
return "%s%s %s." % [said, "" if said.ends_with(".") else ".", how]
|
|
@@ -660,7 +660,9 @@ static func _is_empty(owed: Variant) -> bool:
|
|
|
660
660
|
## taken the lot: see [member _outgoing].
|
|
661
661
|
func _send_response(client: StreamPeerTCP, data: Dictionary) -> void:
|
|
662
662
|
var owed: PackedByteArray = _outgoing.get(client, PackedByteArray())
|
|
663
|
-
|
|
663
|
+
# Keys left in the order they were put in: sorted, a map read came back in another order than
|
|
664
|
+
# its keys() and the game iterate it in.
|
|
665
|
+
owed.append_array((JSON.stringify(data, "", false) + "\n").to_utf8_buffer())
|
|
664
666
|
_outgoing[client] = owed
|
|
665
667
|
|
|
666
668
|
|
|
@@ -129,8 +129,8 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
129
129
|
% [
|
|
130
130
|
reached["called"],
|
|
131
131
|
named,
|
|
132
|
-
JSON.stringify(_values.serialize(given)),
|
|
133
|
-
JSON.stringify(_values.serialize(now)),
|
|
132
|
+
JSON.stringify(_values.serialize(given), "", false),
|
|
133
|
+
JSON.stringify(_values.serialize(now), "", false),
|
|
134
134
|
why,
|
|
135
135
|
]
|
|
136
136
|
)
|
|
@@ -195,14 +195,19 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
195
195
|
}
|
|
196
196
|
var index: int = 0 if which == null else Read.as_int(which, -1)
|
|
197
197
|
if index < 0 or index >= targets.size():
|
|
198
|
-
|
|
198
|
+
# A number arrives from JSON as a float, and 5 was quoted back as 5.0.
|
|
199
|
+
var given: String = str(index) if index >= 0 else str(which)
|
|
200
|
+
var among: String = (
|
|
201
|
+
'the only match on screen%s saying "%s" is index 0' % [under, wanted]
|
|
202
|
+
if targets.size() == 1
|
|
203
|
+
else (
|
|
204
|
+
'the %d matches on screen%s saying "%s" are 0 to %d'
|
|
205
|
+
% [targets.size(), under, wanted, targets.size() - 1]
|
|
206
|
+
)
|
|
207
|
+
)
|
|
199
208
|
return {
|
|
200
209
|
"type": "error",
|
|
201
|
-
"message":
|
|
202
|
-
(
|
|
203
|
-
'index %s is not one of the %d control%s on screen%s saying "%s": %s'
|
|
204
|
-
% [str(which), targets.size(), plural, under, wanted, _candidates(targets, lines)]
|
|
205
|
-
)
|
|
210
|
+
"message": "index %s picks nothing: %s: %s" % [given, among, _candidates(targets, lines)]
|
|
206
211
|
}
|
|
207
212
|
# A match saying the words as part of more, while another control says exactly them, is a stand-in
|
|
208
213
|
# for that one, not the control meant: a tab reading "Out" sat in a collapsed drawer and a dispatch
|
|
@@ -477,5 +477,7 @@ func _element(item: Variant, builtin: int, declared: String, script: Script, res
|
|
|
477
477
|
var wanted: String = type_string(builtin)
|
|
478
478
|
if builtin == TYPE_OBJECT:
|
|
479
479
|
wanted = declared if script == null else script_named(script)
|
|
480
|
-
return {
|
|
480
|
+
return {
|
|
481
|
+
"message": "is %s, which cannot become %s" % [JSON.stringify(serialize(item), "", false), wanted]
|
|
482
|
+
}
|
|
481
483
|
return {"value": fitted_item}
|
|
@@ -5,6 +5,7 @@ extends RefCounted
|
|
|
5
5
|
## stores, which runs no script of the scene's, so a read answers the same either way.
|
|
6
6
|
|
|
7
7
|
const FileReads = preload("file_reads.gd")
|
|
8
|
+
const Read = preload("reading.gd")
|
|
8
9
|
const Serialisation = preload("serialisation.gd")
|
|
9
10
|
|
|
10
11
|
const LOOP_MODE_NAMES: Dictionary[int, String] = {
|
|
@@ -60,7 +61,12 @@ static func _bare(scene_path: String, node_path: String, kind: String) -> Array:
|
|
|
60
61
|
return [null, read]
|
|
61
62
|
var type: String = str(read.get("type", ""))
|
|
62
63
|
if not ClassDB.class_exists(type) or not ClassDB.is_parent_class(type, kind):
|
|
63
|
-
|
|
64
|
+
var said: String = "%s not found at: %s" % [kind, node_path]
|
|
65
|
+
if read.is_empty() and FileReads.scene_node(scene_path, node_path.get_slice("/", 0)).is_empty():
|
|
66
|
+
said = Read.from_the_root(
|
|
67
|
+
said, node_path, str(FileReads.scene_node(scene_path, ".").get("name", ""))
|
|
68
|
+
)
|
|
69
|
+
return [null, {"ok": false, "error": said}]
|
|
64
70
|
var bare: Node = ClassDB.instantiate(type)
|
|
65
71
|
bare.name = str(read["name"])
|
|
66
72
|
var given: Dictionary = read["properties"]
|
|
@@ -164,7 +170,10 @@ static func state_machine(tree: AnimationTree, state_machine_path: String) -> An
|
|
|
164
170
|
## the path that failed, and what is there instead.
|
|
165
171
|
static func no_machine(tree: AnimationTree, state_machine_path: String) -> String:
|
|
166
172
|
if tree == null or tree.tree_root == null:
|
|
167
|
-
return
|
|
173
|
+
return (
|
|
174
|
+
"The AnimationTree has no tree_root. Make one with resource_edit create of an "
|
|
175
|
+
+ "AnimationNodeStateMachine, then give its res:// path to tree_root with scene_node set."
|
|
176
|
+
)
|
|
168
177
|
var current: AnimationNodeStateMachine = tree.tree_root as AnimationNodeStateMachine
|
|
169
178
|
if current == null:
|
|
170
179
|
return (
|
|
@@ -7,6 +7,7 @@ extends RefCounted
|
|
|
7
7
|
## made here passes the same checks and answers in the same shape. What the engine does that an
|
|
8
8
|
## editor does not, running every script, is put back in scene_file.gd and headless_check.gd.
|
|
9
9
|
|
|
10
|
+
const AnimationTools = preload("../addons/gdharness_editor/tools/animation_tools.gd")
|
|
10
11
|
const ResourceTools = preload("../addons/gdharness_editor/tools/resource_tools.gd")
|
|
11
12
|
const SceneTools = preload("../addons/gdharness_editor/tools/scene_tools.gd")
|
|
12
13
|
|
|
@@ -32,6 +33,12 @@ const RESOURCE_COMMANDS: Array[String] = [
|
|
|
32
33
|
"set_theme_color",
|
|
33
34
|
"set_theme_font_size",
|
|
34
35
|
]
|
|
36
|
+
const ANIMATION_COMMANDS: Array[String] = [
|
|
37
|
+
"create_animation",
|
|
38
|
+
"add_animation_track",
|
|
39
|
+
"add_animation_state",
|
|
40
|
+
"connect_animation_states",
|
|
41
|
+
]
|
|
35
42
|
|
|
36
43
|
|
|
37
44
|
## Runs the tool [code]command[/code] names with [code]arguments[/code], answering what it answers.
|
|
@@ -46,6 +53,8 @@ static func run(params: Dictionary) -> Dictionary:
|
|
|
46
53
|
tools = SceneTools.new()
|
|
47
54
|
elif command in RESOURCE_COMMANDS:
|
|
48
55
|
tools = ResourceTools.new()
|
|
56
|
+
elif command in ANIMATION_COMMANDS:
|
|
57
|
+
tools = AnimationTools.new()
|
|
49
58
|
else:
|
|
50
59
|
return {"ok": false, "error": "No editor tool runs headless as " + command}
|
|
51
60
|
var answer: Variant = tools.call(command, arguments)
|
|
@@ -129,7 +129,8 @@ func _answer(operation: String, params: Dictionary, answer_path: String) -> void
|
|
|
129
129
|
)
|
|
130
130
|
quit(1)
|
|
131
131
|
return
|
|
132
|
-
|
|
132
|
+
# Keys in the order they were put in, which for a map read is the order the resource keeps.
|
|
133
|
+
var written: bool = answer.store_string(JSON.stringify(payload, "", false))
|
|
133
134
|
answer.close()
|
|
134
135
|
if not written:
|
|
135
136
|
_log.error("Could not write the answer to " + answer_path)
|
|
@@ -590,7 +590,7 @@ static func _listed(type: int) -> bool:
|
|
|
590
590
|
|
|
591
591
|
## [param value] as a caller would write it, for a refusal to quote.
|
|
592
592
|
func shown(value: Variant) -> String:
|
|
593
|
-
return JSON.stringify(_values.serialize_value(value))
|
|
593
|
+
return JSON.stringify(_values.serialize_value(value), "", false)
|
|
594
594
|
|
|
595
595
|
|
|
596
596
|
## A dictionary carrying its own type name, as the serialiser writes it.
|
|
@@ -99,3 +99,18 @@ static func json_or_null(text: String) -> Variant:
|
|
|
99
99
|
if json.parse(text) != OK:
|
|
100
100
|
return null
|
|
101
101
|
return json.data
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
## [param said], the refusal for [param node_path] found nowhere in a scene whose root is called
|
|
105
|
+
## [param root_name], with how paths are read when the path starts with that name and the root has
|
|
106
|
+
## no child called that. Paths go from the root, so its own name there names a child of it, and a
|
|
107
|
+
## caller who read the root as Main in a tree wrote Main/Body and was told only that nothing is there.
|
|
108
|
+
static func from_the_root(said: String, node_path: String, root_name: String) -> String:
|
|
109
|
+
var first: String = node_path.get_slice("/", 0)
|
|
110
|
+
if root_name.is_empty() or first != root_name:
|
|
111
|
+
return said
|
|
112
|
+
var rest: String = node_path.substr(first.length() + 1)
|
|
113
|
+
var how: String = 'Paths go from the root, %s, whose own path is "."' % root_name
|
|
114
|
+
if not rest.is_empty():
|
|
115
|
+
how += ', so this one is "%s"' % rest
|
|
116
|
+
return "%s%s %s." % [said, "" if said.ends_with(".") else ".", how]
|
package/build/index.js
CHANGED
|
@@ -35528,7 +35528,10 @@ var ANIMATION_TRACK = {
|
|
|
35528
35528
|
type: { type: "string", enum: ["property", "method"] },
|
|
35529
35529
|
nodePath: { type: "string", description: "Target node, relative to the AnimationPlayer root." },
|
|
35530
35530
|
property: { type: "string", description: "For property tracks: the property to animate." },
|
|
35531
|
-
method: {
|
|
35531
|
+
method: {
|
|
35532
|
+
type: "string",
|
|
35533
|
+
description: "For method tracks: the method to call. The answer's methodFound says whether the target node has it; a method it lacks is still added, since it may be written next, and until it exists each call the track makes fails."
|
|
35534
|
+
},
|
|
35532
35535
|
keyframes: {
|
|
35533
35536
|
type: "array",
|
|
35534
35537
|
items: {
|
|
@@ -36110,7 +36113,7 @@ var TOOL_SPECS = [
|
|
|
36110
36113
|
{
|
|
36111
36114
|
name: "scene_signal",
|
|
36112
36115
|
title: "Scene signals",
|
|
36113
|
-
description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connect of a pair already connected says alreadyConnected, with flagsBefore when it changed the flags. ${WITHOUT_AN_EDITOR}`,
|
|
36116
|
+
description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connect of a pair already connected says alreadyConnected, with flagsBefore when it changed the flags. A connect to a method the target does not have is still written, as the editor's own dialog allows, since the method may be written next; methodFound false says so, and until the method exists the signal fails each time it is emitted. list marks editable false on a connection an instanced scene makes inside itself, which only that scene can change. ${WITHOUT_AN_EDITOR}`,
|
|
36114
36117
|
parameters: {
|
|
36115
36118
|
projectPath: PROJECT_PATH2,
|
|
36116
36119
|
scenePath: SCENE_PATH,
|
|
@@ -36155,7 +36158,7 @@ var TOOL_SPECS = [
|
|
|
36155
36158
|
{
|
|
36156
36159
|
name: "scene_animation",
|
|
36157
36160
|
title: "Animations",
|
|
36158
|
-
description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. read and read_states need 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 running the scene's scripts. The ops that write
|
|
36161
|
+
description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. read and read_states need 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 running the scene's scripts. The ops that write go through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.",
|
|
36159
36162
|
parameters: {
|
|
36160
36163
|
projectPath: PROJECT_PATH2,
|
|
36161
36164
|
scenePath: SCENE_PATH,
|
|
@@ -36167,7 +36170,7 @@ var TOOL_SPECS = [
|
|
|
36167
36170
|
animationName: {
|
|
36168
36171
|
type: "string",
|
|
36169
36172
|
ops: ["read", "create", "add_track", "add_state"],
|
|
36170
|
-
description: "The animation in the player. read: the one to answer with whole; without it, read lists them. add_state: the one that state plays."
|
|
36173
|
+
description: "The animation in the player. read: the one to answer with whole, its tracks a list; without it, read lists them, each with the number of its tracks under tracks. add_state: the one that state plays."
|
|
36171
36174
|
},
|
|
36172
36175
|
length: { type: "number", ops: ["create"], description: "create: seconds. Default 1." },
|
|
36173
36176
|
loopMode: {
|
|
@@ -36422,7 +36425,7 @@ var TOOL_SPECS = [
|
|
|
36422
36425
|
controlType: {
|
|
36423
36426
|
type: "string",
|
|
36424
36427
|
ops: ["set_theme_color", "set_theme_font_size"],
|
|
36425
|
-
description: 'Theme ops: the Control class
|
|
36428
|
+
description: 'Theme ops: the theme type, a Control or Window class such as "Button", or the name of a type variation, which can be any name. A name that is no such class is set as a variation and answered with a note saying so.'
|
|
36426
36429
|
},
|
|
36427
36430
|
colorName: {
|
|
36428
36431
|
type: "string",
|
|
@@ -36435,7 +36438,11 @@ var TOOL_SPECS = [
|
|
|
36435
36438
|
ops: ["set_theme_font_size"],
|
|
36436
36439
|
description: 'set_theme_font_size: such as "font_size".'
|
|
36437
36440
|
},
|
|
36438
|
-
size: {
|
|
36441
|
+
size: {
|
|
36442
|
+
type: "number",
|
|
36443
|
+
ops: ["set_theme_font_size"],
|
|
36444
|
+
description: "set_theme_font_size: pixels, a whole number of 1 or more."
|
|
36445
|
+
}
|
|
36439
36446
|
},
|
|
36440
36447
|
requires: ["projectPath", "resourcePath"],
|
|
36441
36448
|
operations: {
|
|
@@ -36640,7 +36647,7 @@ var TOOL_SPECS = [
|
|
|
36640
36647
|
requires: []
|
|
36641
36648
|
}
|
|
36642
36649
|
},
|
|
36643
|
-
answer: "start: scene is the scene the game started in, the project's main scene when none was given. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, 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. 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: close (the game was asked to close and exited, so exitCode is its own), 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. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
|
|
36650
|
+
answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, 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. 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: close (the game was asked to close and exited, so exitCode is its own), 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. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
|
|
36644
36651
|
defaultOperation: "start"
|
|
36645
36652
|
},
|
|
36646
36653
|
{
|
|
@@ -37028,7 +37035,7 @@ var TOOL_SPECS = [
|
|
|
37028
37035
|
name: "runtime_input",
|
|
37029
37036
|
title: "Send input to the game",
|
|
37030
37037
|
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, mouse motion or drag. All of it works headless, where the viewport is the project's window size 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: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: 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. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
|
|
37031
|
-
answer:
|
|
37038
|
+
answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again, for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.`,
|
|
37032
37039
|
parameters: {
|
|
37033
37040
|
projectPath: RUNNING_PROJECT_PATH,
|
|
37034
37041
|
pid: RUNNING_PID,
|
|
@@ -39638,13 +39645,13 @@ class GodotServer {
|
|
|
39638
39645
|
stateMachinePath: readString(args, "stateMachinePath") ?? ""
|
|
39639
39646
|
}), async () => await bridge("read_animation_states"));
|
|
39640
39647
|
case "create":
|
|
39641
|
-
return await
|
|
39648
|
+
return await editorOrEngine("create_animation", "writes");
|
|
39642
39649
|
case "add_track":
|
|
39643
|
-
return await
|
|
39650
|
+
return await editorOrEngine("add_animation_track", "writes");
|
|
39644
39651
|
case "add_state":
|
|
39645
|
-
return await
|
|
39652
|
+
return await editorOrEngine("add_animation_state", "writes");
|
|
39646
39653
|
default:
|
|
39647
|
-
return await
|
|
39654
|
+
return await editorOrEngine("connect_animation_states", "writes");
|
|
39648
39655
|
}
|
|
39649
39656
|
case "script_edit":
|
|
39650
39657
|
return await this.handleRenameSymbol(args);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.41",
|
|
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",
|