gdharness 1.3.35 → 1.3.37

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.
Files changed (31) hide show
  1. package/build/cli.js +288 -78
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +17 -1
  3. package/build/godot/addons/gdharness_editor/reading.gd +10 -2
  4. package/build/godot/addons/gdharness_editor/scene_file.gd +12 -6
  5. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +57 -0
  6. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +16 -0
  7. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +81 -4
  8. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -3
  9. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -2
  10. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +59 -0
  11. package/build/godot/addons/gdharness_editor/tools/script_paths.gd +148 -0
  12. package/build/godot/addons/gdharness_editor/tools/script_paths.gd.uid +1 -0
  13. package/build/godot/addons/gdharness_runtime/reading.gd +10 -2
  14. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +70 -0
  15. package/build/godot/addons/gdharness_runtime/runtime_input.gd +223 -248
  16. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +128 -5
  17. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +107 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +15 -1
  19. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +8 -0
  20. package/build/godot/addons/gdharness_runtime/runtime_words.gd +22 -0
  21. package/build/godot/operations/animation_reads.gd +17 -1
  22. package/build/godot/operations/classdb_queries.gd +16 -0
  23. package/build/godot/operations/dependencies.gd +46 -4
  24. package/build/godot/operations/editor_tools.gd +5 -5
  25. package/build/godot/operations/gdscript_authoring.gd +7 -3
  26. package/build/godot/operations/input_actions.gd +105 -0
  27. package/build/godot/operations/project_diagnostics.gd +4 -0
  28. package/build/godot/operations/project_validation.gd +15 -0
  29. package/build/godot/operations/reading.gd +10 -2
  30. package/build/keeper.js +5 -5
  31. package/package.json +1 -1
@@ -3,6 +3,17 @@ extends RefCounted
3
3
  const GdhRead = preload("reading.gd")
4
4
  const GdhLog = preload("logger.gd")
5
5
  const GdhProjectFile = preload("project_file.gd")
6
+ const GdhPatterns = preload("patterns.gd")
7
+ const GdhFileWalk = preload("file_walk.gd")
8
+ ## The calls that read an input action by name, Input's and an event's, up to the opening bracket.
9
+ const READS_AN_ACTION: String = (
10
+ "\\b(?:is_action(?:_just)?_(?:pressed|released)|is_action|get_action(?:_raw)?_strength"
11
+ + "|action_press|action_release|get_axis|get_vector)\\("
12
+ )
13
+ ## InputMap's call that makes an action at run time, which a script then reads as its own.
14
+ const ADDS_AN_ACTION: String = "\\badd_action\\("
15
+ ## An argument that is a name written in the call and nothing else, a String or a StringName.
16
+ const NAME_ARGUMENT: String = "^&?(?:\"([^\"\\\\]*)\"|'([^'\\\\]*)')$"
6
17
  ## The device an event matches every device with, InputMap's ALL_DEVICES, which GDScript is not given.
7
18
  const ALL_DEVICES: int = -1
8
19
  const MOUSE_NAMES: String = (
@@ -113,6 +124,100 @@ func _init(p_log: GdhLog) -> void:
113
124
  _log = p_log
114
125
 
115
126
 
127
+ ## The input actions the project's scripts read by a name written in the call that neither the
128
+ ## project nor the engine defines, each as {action, read_at} with the script and line of every read.
129
+ ## Godot answers such a read with false and an error on every frame, so the input never works and
130
+ ## nothing fails until the game runs. A name a script builds is not judged, and one a script adds
131
+ ## with InputMap.add_action is its own to define.
132
+ func undefined_in_scripts() -> Array[Dictionary]:
133
+ var read: Dictionary = {}
134
+ var added: Dictionary = {}
135
+ for script: String in GdhFileWalk.new().find_files("res://", ".gd"):
136
+ var number: int = 0
137
+ for line: String in FileAccess.get_file_as_string(script).split("\n"):
138
+ number += 1
139
+ var code: String = GdhPatterns.without_comment(line)
140
+ for adding: RegExMatch in GdhPatterns.compiled(ADDS_AN_ACTION).search_all(code):
141
+ if not _in_a_string(code, adding.get_start()):
142
+ for name: String in _names_given(code, adding.get_end()):
143
+ added[name] = true
144
+ for reading: RegExMatch in GdhPatterns.compiled(READS_AN_ACTION).search_all(code):
145
+ if _in_a_string(code, reading.get_start()):
146
+ continue
147
+ for name: String in _names_given(code, reading.get_end()):
148
+ var at: Array = read.get_or_add(name, [])
149
+ var place: String = "%s:%d" % [script, number]
150
+ if not place in at:
151
+ at.append(place)
152
+ var names: Array = read.keys()
153
+ names.sort()
154
+ var undefined: Array[Dictionary] = []
155
+ for name: String in names:
156
+ if ProjectSettings.has_setting("input/" + name) or added.has(name):
157
+ continue
158
+ undefined.append({"action": name, "read_at": read[name]})
159
+ return undefined
160
+
161
+
162
+ ## One line for [param entry], one of [method undefined_in_scripts].
163
+ static func undefined_line(entry: Dictionary) -> String:
164
+ var listed: Array = entry.get("read_at", [])
165
+ var at: PackedStringArray = PackedStringArray(listed)
166
+ return (
167
+ "%s is not an input action of this project, and %s %s it"
168
+ % [str(entry.get("action", "")), ", ".join(at), "reads" if at.size() == 1 else "read"]
169
+ )
170
+
171
+
172
+ ## The arguments of the call whose bracket opens before [param from] in [param code] that are each a
173
+ ## name written in the call, to its closing bracket or the end of the line.
174
+ static func _names_given(code: String, from: int) -> Array[String]:
175
+ var names: Array[String] = []
176
+ var depth: int = 0
177
+ var quote: String = ""
178
+ var argument: String = ""
179
+ for at: int in range(from, code.length()):
180
+ var ch: String = code[at]
181
+ if not quote.is_empty():
182
+ if ch == quote and code[at - 1] != "\\":
183
+ quote = ""
184
+ elif ch == '"' or ch == "'":
185
+ quote = ch
186
+ elif ch == "(" or ch == "[" or ch == "{":
187
+ depth += 1
188
+ elif ch == ")" or ch == "]" or ch == "}":
189
+ if depth == 0:
190
+ _take_name(argument, names)
191
+ return names
192
+ depth -= 1
193
+ elif ch == "," and depth == 0:
194
+ _take_name(argument, names)
195
+ argument = ""
196
+ continue
197
+ argument += ch
198
+ return names
199
+
200
+
201
+ ## Whether [param at] in [param code] is inside a string literal, where a call is text rather than one.
202
+ static func _in_a_string(code: String, at: int) -> bool:
203
+ var quote: String = ""
204
+ for index: int in range(at):
205
+ var ch: String = code[index]
206
+ if not quote.is_empty():
207
+ if ch == quote and code[index - 1] != "\\":
208
+ quote = ""
209
+ elif ch == '"' or ch == "'":
210
+ quote = ch
211
+ return not quote.is_empty()
212
+
213
+
214
+ static func _take_name(argument: String, names: Array[String]) -> void:
215
+ var named: RegExMatch = GdhPatterns.compiled(NAME_ARGUMENT).search(argument.strip_edges())
216
+ if named != null:
217
+ var single: String = named.get_string(2)
218
+ names.append(named.get_string(1) if single.is_empty() else single)
219
+
220
+
116
221
  # Add an input action to the InputMap
117
222
  func add_input_action(params: Dictionary) -> Dictionary:
118
223
  var action_name: String = str(params.get("action_name", ""))
@@ -3,6 +3,7 @@ extends RefCounted
3
3
  const GdhDependencies = preload("dependencies.gd")
4
4
  const GdhFileWalk = preload("file_walk.gd")
5
5
  const GdhImportPipeline = preload("import_pipeline.gd")
6
+ const GdhInputActions = preload("input_actions.gd")
6
7
  const GdhLog = preload("logger.gd")
7
8
  const GdhRead = preload("reading.gd")
8
9
  const GdhSceneConnections = preload("scene_connections.gd")
@@ -119,6 +120,9 @@ func get_project_health(params: Dictionary) -> Dictionary:
119
120
  for entry: Dictionary in GdhDependencies.new(_log).missing_references():
120
121
  details.append(GdhDependencies.missing_line(entry))
121
122
  deductions += 5
123
+ for entry: Dictionary in GdhInputActions.new(_log).undefined_in_scripts():
124
+ details.append(GdhInputActions.undefined_line(entry))
125
+ deductions += 5
122
126
  checks["references"] = {"name": "References", "passed": details.is_empty(), "details": details}
123
127
 
124
128
  # 4. Scenes Check
@@ -7,6 +7,7 @@ const GdhDependencies = preload("dependencies.gd")
7
7
  const GdhExportPresets = preload("export_presets.gd")
8
8
  const GdhFileWalk = preload("file_walk.gd")
9
9
  const GdhImportPipeline = preload("import_pipeline.gd")
10
+ const GdhInputActions = preload("input_actions.gd")
10
11
  const GdhLog = preload("logger.gd")
11
12
  const GdhRead = preload("reading.gd")
12
13
  const GdhSceneConnections = preload("scene_connections.gd")
@@ -219,6 +220,20 @@ func validate_project(params: Dictionary) -> Dictionary:
219
220
  )
220
221
  )
221
222
 
223
+ # An action read by a name the input map lacks answers false on every frame, so the input it was
224
+ # for never works, and the engine says so only while the game runs.
225
+ checks_performed.append("input_actions")
226
+ for entry: Dictionary in GdhInputActions.new(_log).undefined_in_scripts():
227
+ issues.append(
228
+ _finding(
229
+ "error",
230
+ "input_actions",
231
+ GdhInputActions.undefined_line(entry),
232
+ "Add it with project_settings add_input_action, or correct the name the script reads",
233
+ include_suggestions
234
+ )
235
+ )
236
+
222
237
  # A connection whose method or signal is not there loads without a word and fails only once the
223
238
  # game runs, on every emission or not at all; the references check sees files, not members.
224
239
  checks_performed.append("connections")
@@ -215,7 +215,8 @@ static func offered(said: String, word: String, names: Array) -> String:
215
215
  ## [param said], the refusal for [param property], which [param target] does not declare, with the
216
216
  ## names it declares nearest it when any are, as a misspelt one is usually one of them, or else the
217
217
  ## variables its script declares. The engine's own are not listed when none is near, since an object
218
- ## has a hundred, while a script's are the few a caller most likely meant.
218
+ ## has a hundred, while a script's are the few a caller most likely meant; an object with no script
219
+ ## is told where they are listed instead, since the refusal alone left nothing to try.
219
220
  static func near_property(said: String, target: Object, property: String) -> String:
220
221
  var headings: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
221
222
  var names: Array[String] = []
@@ -233,8 +234,15 @@ static func near_property(said: String, target: Object, property: String) -> Str
233
234
  var close: PackedStringArray = nearest(property, PackedStringArray(names))
234
235
  if not close.is_empty():
235
236
  return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
236
- if scripted.is_empty():
237
+ # With a script that declares nothing readable, as one that does not compile, the name is likelier
238
+ # the script's than the class's, so the class's list would point the wrong way.
239
+ if scripted.is_empty() and target.get_script() != null:
237
240
  return said
241
+ if scripted.is_empty():
242
+ return (
243
+ "%s; editor_classes info with className %s and includeInherited true lists the ones it has"
244
+ % [said, target.get_class()]
245
+ )
238
246
  scripted.sort()
239
247
  return "%s; its script declares %s" % [said, ", ".join(PackedStringArray(scripted))]
240
248
 
package/build/keeper.js CHANGED
@@ -9190,7 +9190,7 @@ class GameDebugger {
9190
9190
  case "stack": {
9191
9191
  const frames = await this.stack();
9192
9192
  if (frames === null) {
9193
- return this.notHeld();
9193
+ return this.notHeld("stack to read");
9194
9194
  }
9195
9195
  return {
9196
9196
  held: this.heldState(),
@@ -9218,9 +9218,9 @@ class GameDebugger {
9218
9218
  ...this.held.canContinue ? {} : { canContinue: false }
9219
9219
  };
9220
9220
  }
9221
- notHeld() {
9221
+ notHeld(what) {
9222
9222
  return {
9223
- refused: "The game is running, not held, so it has no stack to read: debug_control pause stops it where it is, and a breakpoint stops it on its line."
9223
+ refused: `The game is running, not held, so it has no ${what}: debug_control pause stops it where it is, and a breakpoint stops it on its line.`
9224
9224
  };
9225
9225
  }
9226
9226
  async stack() {
@@ -9384,7 +9384,7 @@ class GameDebugger {
9384
9384
  async variables(frame) {
9385
9385
  const held = this.held;
9386
9386
  if (held === null) {
9387
- return this.notHeld();
9387
+ return this.notHeld("frame whose variables could be read");
9388
9388
  }
9389
9389
  const frames = await this.stack();
9390
9390
  if (frames === null) {
@@ -9431,7 +9431,7 @@ class GameDebugger {
9431
9431
  async evaluate(frame, expression) {
9432
9432
  const held = this.held;
9433
9433
  if (held === null) {
9434
- return this.notHeld();
9434
+ return this.notHeld("frame to work an expression out in");
9435
9435
  }
9436
9436
  const read = await this.variables(frame);
9437
9437
  if (!Array.isArray(read["scopes"])) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.35",
3
+ "version": "1.3.37",
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",