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 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: { type: "string", description: "For method tracks: the method to call." },
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 need the editor connected.",
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, such as "Button".'
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: { type: "number", ops: ["set_theme_font_size"], description: "set_theme_font_size: pixels." }
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: '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, found.item names the tab, list entry, tree row or menu bar title pressed (kind, index where its control numbers them, 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.',
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 bridge("create_animation");
46911
+ return await editorOrEngine("create_animation", "writes");
46905
46912
  case "add_track":
46906
- return await bridge("add_animation_track");
46913
+ return await editorOrEngine("add_animation_track", "writes");
46907
46914
  case "add_state":
46908
- return await bridge("add_animation_state");
46915
+ return await editorOrEngine("add_animation_state", "writes");
46909
46916
  default:
46910
- return await bridge("connect_animation_states");
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
- return [null, {"ok": false, "error": "%s not found at: %s" % [kind, node_path]}]
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 "The AnimationTree has no tree_root."
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
- var sent: Error = socket.send_text(JSON.stringify(message))
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
- var items: Array = value if value is Array else [value]
151
- for item: Variant in items:
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
- var items: Array = value if value is Array else [value]
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 [scene, null, scene.refuse("%s not found at: %s" % [kind, node_path])]
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 [scene, null, scene.refuse("%s not found at: %s" % [kind, node_path])]
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 = ResourceSaver.save(resource, str(file))
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
- EditorInterface.get_resource_filesystem().update_file(str(file))
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
- "ok": true,
725
- "themePath": theme_path,
726
- "controlType": control_type,
727
- "colorName": color_name,
728
- "color": {"r": stored.r, "g": stored.g, "b": stored.b, "a": stored.a}
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
- theme.set_font_size(font_size_name, control_type, Read.as_int(args.get("size", 0)))
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
- "ok": true,
758
- "themePath": theme_path,
759
- "controlType": control_type,
760
- "fontSizeName": font_size_name,
761
- "size": saved.get_font_size(font_size_name, control_type)
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
- owed.append_array((JSON.stringify(data) + "\n").to_utf8_buffer())
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
- var plural: String = "" if targets.size() == 1 else "s"
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 {"message": "is %s, which cannot become %s" % [JSON.stringify(serialize(item)), wanted]}
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
- return [null, {"ok": false, "error": "%s not found at: %s" % [kind, node_path]}]
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 "The AnimationTree has no tree_root."
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
- var written: bool = answer.store_string(JSON.stringify(payload))
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: { type: "string", description: "For method tracks: the method to call." },
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 need the editor connected.",
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, such as "Button".'
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: { type: "number", ops: ["set_theme_font_size"], description: "set_theme_font_size: pixels." }
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: '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, found.item names the tab, list entry, tree row or menu bar title pressed (kind, index where its control numbers them, 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.',
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 bridge("create_animation");
39648
+ return await editorOrEngine("create_animation", "writes");
39642
39649
  case "add_track":
39643
- return await bridge("add_animation_track");
39650
+ return await editorOrEngine("add_animation_track", "writes");
39644
39651
  case "add_state":
39645
- return await bridge("add_animation_state");
39652
+ return await editorOrEngine("add_animation_state", "writes");
39646
39653
  default:
39647
- return await bridge("connect_animation_states");
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.40",
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",