gdharness 1.3.7 → 1.3.9

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 (38) hide show
  1. package/build/cli.js +999 -864
  2. package/build/godot/addons/gdharness_editor/file_reads.gd +13 -0
  3. package/build/godot/addons/gdharness_editor/reading.gd +61 -5
  4. package/build/godot/addons/gdharness_editor/scene_file.gd +41 -35
  5. package/build/godot/addons/gdharness_editor/serialisation.gd +8 -7
  6. package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -2
  7. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +244 -0
  8. package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
  9. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +105 -157
  10. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +25 -1
  11. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -1
  12. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +72 -3
  14. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +7 -1
  15. package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
  16. package/build/godot/addons/gdharness_runtime/reading.gd +61 -5
  17. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +6 -1
  19. package/build/godot/addons/gdharness_runtime/runtime_input.gd +74 -54
  20. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +38 -6
  21. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +2 -0
  22. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +26 -0
  23. package/build/godot/addons/gdharness_runtime/runtime_values.gd +105 -6
  24. package/build/godot/addons/gdharness_runtime/serialisation.gd +8 -7
  25. package/build/godot/operations/dependencies.gd +61 -10
  26. package/build/godot/operations/editor_tools.gd +8 -0
  27. package/build/godot/operations/file_reads.gd +13 -0
  28. package/build/godot/operations/gdscript_analysis.gd +76 -6
  29. package/build/godot/operations/gdscript_authoring.gd +5 -2
  30. package/build/godot/operations/godot_operations.gd +4 -0
  31. package/build/godot/operations/import_pipeline.gd +45 -1
  32. package/build/godot/operations/input_actions.gd +22 -7
  33. package/build/godot/operations/project_config.gd +31 -0
  34. package/build/godot/operations/project_diagnostics.gd +6 -5
  35. package/build/godot/operations/reading.gd +61 -5
  36. package/build/godot/operations/serialisation.gd +8 -7
  37. package/build/keeper.js +44 -13
  38. package/package.json +1 -1
@@ -24,6 +24,23 @@ extends RefCounted
24
24
 
25
25
  ## How many of a node's children a refusal for a path missing below it names.
26
26
  const NAMED_CHILDREN: int = 12
27
+ ## The types holding their floats as the engine's real numbers, 32-bit unless it was built for 64.
28
+ const SHAPES_OF_REALS: Array[int] = [
29
+ TYPE_VECTOR2,
30
+ TYPE_VECTOR3,
31
+ TYPE_VECTOR4,
32
+ TYPE_RECT2,
33
+ TYPE_PLANE,
34
+ TYPE_QUATERNION,
35
+ TYPE_AABB,
36
+ TYPE_BASIS,
37
+ TYPE_TRANSFORM2D,
38
+ TYPE_TRANSFORM3D,
39
+ TYPE_PROJECTION,
40
+ TYPE_PACKED_VECTOR2_ARRAY,
41
+ TYPE_PACKED_VECTOR3_ARRAY,
42
+ TYPE_PACKED_VECTOR4_ARRAY,
43
+ ]
27
44
 
28
45
 
29
46
  static func as_int(value: Variant, fallback: int = 0) -> int:
@@ -190,18 +207,26 @@ static func offered(said: String, word: String, names: Array) -> String:
190
207
 
191
208
 
192
209
  ## [param said], the refusal for [param property], which [param target] does not declare, with the
193
- ## names it declares nearest it when any are, as a misspelt one is usually one of them. Unlike
194
- ## [method offered], nothing is listed when none is near, since an object declares a hundred.
210
+ ## names it declares nearest it when any are, as a misspelt one is usually one of them, or else the
211
+ ## variables its script declares. The engine's own are not listed when none is near, since an object
212
+ ## has a hundred, while a script's are the few a caller most likely meant.
195
213
  static func near_property(said: String, target: Object, property: String) -> String:
196
214
  var headings: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
197
215
  var names: Array[String] = []
216
+ var scripted: Array[String] = []
198
217
  for declared: Dictionary in target.get_property_list():
199
- if not as_int(declared.get("usage", 0)) & headings:
218
+ var usage: int = as_int(declared.get("usage", 0))
219
+ if not usage & headings:
200
220
  names.append(str(declared.get("name", "")))
221
+ if usage & PROPERTY_USAGE_SCRIPT_VARIABLE and not usage & headings:
222
+ scripted.append(str(declared.get("name", "")))
201
223
  var close: PackedStringArray = nearest(property, PackedStringArray(names))
202
- if close.is_empty():
224
+ if not close.is_empty():
225
+ return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
226
+ if scripted.is_empty():
203
227
  return said
204
- return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
228
+ scripted.sort()
229
+ return "%s; its script declares %s" % [said, ", ".join(PackedStringArray(scripted))]
205
230
 
206
231
 
207
232
  ## The signals [param object] has that a caller can connect, its own class's first: the engine
@@ -219,3 +244,34 @@ static func signals_of(object: Object) -> Array:
219
244
  ## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
220
245
  static func with_article(word: String) -> String:
221
246
  return ("an " if word.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + word
247
+
248
+
249
+ ## [param value], which the engine holds as a 32-bit float, as the shortest decimal that is still
250
+ ## that float. JSON carries a double, and the double a 32-bit 0.2 widens to is 0.200000002980232:
251
+ ## the same number, but not the one written or the one the file shows, so a caller comparing what it
252
+ ## set against what it read saw them differ. The shorter one, sent back, stores the same 32 bits. A
253
+ ## plain float is left alone by the serialiser, since nothing says whether the engine holds it in
254
+ ## 32 bits or 64.
255
+ static func single(value: float) -> float:
256
+ if not is_finite(value) or value == 0.0:
257
+ return value
258
+ var held: float = PackedFloat32Array([value])[0]
259
+ var magnitude: int = floori(log(absf(held)) / log(10.0))
260
+ for digits: int in range(1, 10):
261
+ var shift: int = digits - 1 - magnitude
262
+ var tried: float = (
263
+ roundf(held * pow(10.0, shift)) / pow(10.0, shift)
264
+ if shift >= 0
265
+ else roundf(held / pow(10.0, -shift)) * pow(10.0, -shift)
266
+ )
267
+ if PackedFloat32Array([tried])[0] == held:
268
+ return tried
269
+ return held
270
+
271
+
272
+ ## Whether a value of [param type] holds its floats in 32 bits: a colour always, and the vectors and
273
+ ## the shapes built of them unless the engine was built for 64.
274
+ static func holds_singles(type: int) -> bool:
275
+ if type in [TYPE_COLOR, TYPE_PACKED_COLOR_ARRAY, TYPE_PACKED_FLOAT32_ARRAY]:
276
+ return true
277
+ return not OS.has_feature("double") and type in SHAPES_OF_REALS
@@ -142,28 +142,29 @@ func serialize_value(value: Variant) -> Variant:
142
142
  return value
143
143
  var carried: Variant = call(serialiser, value)
144
144
  # The fixed shapes carry their components as bare floats, so an infinite one in a Vector2 is
145
- # tagged here; containers reach their members through this function already.
146
- return carried if typeof(value) in [TYPE_ARRAY, TYPE_DICTIONARY] else _finite(carried)
145
+ # tagged here, and a 32-bit one shortened; containers reach their members through this already.
146
+ var type: int = typeof(value)
147
+ return carried if type in [TYPE_ARRAY, TYPE_DICTIONARY] else _finite(carried, GdhRead.holds_singles(type))
147
148
 
148
149
 
149
150
  # [param carried] with every non-finite float in it tagged. JSON has no spelling for infinity or
150
151
  # NaN: the engine writes 1e99999 and null, and the server, reading 1e99999 as Infinity, writes it
151
152
  # back out as null, so a property holding INF was answered as holding nothing.
152
- func _finite(carried: Variant) -> Variant:
153
+ func _finite(carried: Variant, single: bool = false) -> Variant:
153
154
  if carried is float:
154
155
  var number: float = carried
155
- return _serialize_float(number)
156
+ return _serialize_float(GdhRead.single(number) if single else number)
156
157
  if carried is Dictionary:
157
158
  var fields: Dictionary = carried
158
159
  var tagged: Dictionary = {}
159
160
  for key: Variant in fields:
160
- tagged[key] = _finite(fields[key])
161
+ tagged[key] = _finite(fields[key], single)
161
162
  return tagged
162
163
  if carried is Array:
163
164
  var items: Array = carried
164
165
  var tagged_items: Array = []
165
166
  for item: Variant in items:
166
- tagged_items.append(_finite(item))
167
+ tagged_items.append(_finite(item, single))
167
168
  return tagged_items
168
169
  return carried
169
170
 
@@ -189,7 +190,7 @@ func _serialize_floats(value: Variant) -> Variant:
189
190
  for each: float in numbers:
190
191
  items.append(_serialize_float(each))
191
192
  return items
192
- return value
193
+ return numbers if value is PackedFloat32Array else value
193
194
 
194
195
 
195
196
  # Rebuilds a Godot value from the shape serialize_value gave it.
package/build/keeper.js CHANGED
@@ -814,11 +814,13 @@ class GameDebugger {
814
814
  server;
815
815
  port;
816
816
  breakpoints;
817
+ breakpointLines;
817
818
  observers;
818
819
  constructor(server, port, breakpoints, observers) {
819
820
  this.server = server;
820
821
  this.port = port;
821
822
  this.breakpoints = breakpoints;
823
+ this.breakpointLines = new Set(breakpoints);
822
824
  this.observers = observers;
823
825
  }
824
826
  static async open(breakpoints, observers = []) {
@@ -1010,6 +1012,11 @@ class GameDebugger {
1010
1012
  }
1011
1013
  switch (request.op) {
1012
1014
  case "breakpoint":
1015
+ if (request.set) {
1016
+ this.breakpointLines.add(`${request.script}:${String(request.line)}`);
1017
+ } else {
1018
+ this.breakpointLines.delete(`${request.script}:${String(request.line)}`);
1019
+ }
1013
1020
  this.send("breakpoint", [request.script, request.line, request.set], this.held?.thread ?? MAIN_THREAD);
1014
1021
  return { sent: true };
1015
1022
  case "pause":
@@ -1087,18 +1094,12 @@ class GameDebugger {
1087
1094
  };
1088
1095
  }
1089
1096
  }
1090
- let frames = await this.stack();
1091
- const stepping = Date.now();
1092
- while (frames !== null && this.held !== null && (frames[0]?.script.startsWith(RUNTIME_ADDON) ?? false) && Date.now() - stepping < ADDON_STEP_MS) {
1093
- this.forget("debug_enter");
1094
- this.send("step", [], this.held.thread);
1095
- if (await this.next(GameDebugger.named("debug_enter"), STEP_MS) === null) {
1096
- return {
1097
- paused: false,
1098
- note: `The game stopped in gdharness's runtime addon and was stepped on to reach its own script, and did not stop again within ${STEP_MS / 1000}s. It stops at the next line of script it runs; debug_state stack reads it then.`
1099
- };
1100
- }
1101
- frames = await this.stack();
1097
+ const frames = await this.pastTheAddon();
1098
+ if (frames === "lost") {
1099
+ return {
1100
+ paused: false,
1101
+ note: `The game stopped in gdharness's runtime addon and was stepped on to reach its own script, and did not stop again within ${STEP_MS / 1000}s. It stops at the next line of script it runs; debug_state stack reads it then.`
1102
+ };
1102
1103
  }
1103
1104
  this.stoppedBy("Paused");
1104
1105
  const stack = frames ?? [];
@@ -1111,6 +1112,19 @@ class GameDebugger {
1111
1112
  } : {}
1112
1113
  };
1113
1114
  }
1115
+ async pastTheAddon() {
1116
+ let frames = await this.stack();
1117
+ const stepping = Date.now();
1118
+ while (frames !== null && this.held !== null && (frames[0]?.script.startsWith(RUNTIME_ADDON) ?? false) && Date.now() - stepping < ADDON_STEP_MS) {
1119
+ this.forget("debug_enter");
1120
+ this.send("step", [], this.held.thread);
1121
+ if (await this.next(GameDebugger.named("debug_enter"), STEP_MS) === null) {
1122
+ return "lost";
1123
+ }
1124
+ frames = await this.stack();
1125
+ }
1126
+ return frames;
1127
+ }
1114
1128
  stoppedBy(cause) {
1115
1129
  if (this.held !== null) {
1116
1130
  this.held = { ...this.held, reason: cause };
@@ -1139,8 +1153,24 @@ class GameDebugger {
1139
1153
  note: `The game had not stopped again ${STEP_MS / 1000}s after the step: the step is still running, as one out of a function that never returns does, or it went on without reaching another line. debug_state stack answers once it stops, and debug_control pause stops it wherever it is.`
1140
1154
  };
1141
1155
  }
1156
+ const frames = await this.pastTheAddon();
1157
+ if (frames === "lost") {
1158
+ return {
1159
+ stepped: true,
1160
+ heldAgain: false,
1161
+ note: `The step stopped in gdharness's runtime addon and was stepped on to reach the game's own script, and did not stop again within ${STEP_MS / 1000}s. It stops at the next line of script the game runs; debug_state stack reads it then.`
1162
+ };
1163
+ }
1142
1164
  this.stoppedBy("Stepped");
1143
- return { stepped: true, heldAgain: true, held: this.heldState(), stack: await this.stack() ?? [] };
1165
+ const stack = frames ?? [];
1166
+ const top = stack[0];
1167
+ const onBreakpoint = top !== undefined && this.breakpointLines.has(`${top.script}:${String(top.line)}`);
1168
+ return {
1169
+ stepped: true,
1170
+ heldAgain: true,
1171
+ held: { ...this.heldState(), ...onBreakpoint ? { onBreakpoint: true } : {} },
1172
+ stack
1173
+ };
1144
1174
  }
1145
1175
  async variables(frame) {
1146
1176
  const held = this.held;
@@ -1668,6 +1698,7 @@ function emptyRecord() {
1668
1698
  // src/project-scan.ts
1669
1699
  var SKIPPED = new Set(["node_modules"]);
1670
1700
  var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
1701
+ var SIDECAR_EXTENSIONS = new Set(["import", "uid"]);
1671
1702
  var ASSET_EXTENSIONS = new Set([
1672
1703
  "png",
1673
1704
  "jpg",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
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",