gdharness 1.3.45 → 1.3.46

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.
@@ -503,7 +503,7 @@ static func can_read(holder: Variant, named: String) -> bool:
503
503
  var object: Object = holder
504
504
  if not method.is_empty():
505
505
  return call_refused(object, method, "", args.size()).is_empty()
506
- return _has_property(object, named)
506
+ return _has_property(object, named) or not constant_of(object, named).is_empty()
507
507
  return false
508
508
 
509
509
 
@@ -876,7 +876,7 @@ static func read_under(holder: Variant, named: String) -> Variant:
876
876
  if not method.is_empty():
877
877
  var args: Array = arguments_of(named)["args"]
878
878
  return object.callv(method, args)
879
- return object.get(named)
879
+ return constant_of(object, named)[0] if is_constant(object, named) else object.get(named)
880
880
 
881
881
 
882
882
  ## Where [param named] lands in a list of [param size], or -1 for a step that is not in it.
@@ -1030,3 +1030,30 @@ static func _has_property(holder: Object, named: String) -> bool:
1030
1030
  if str(entry["name"]) == named:
1031
1031
  return true
1032
1032
  return false
1033
+
1034
+
1035
+ ## The constant [param named] that [param holder]'s script or a script it extends declares, in a
1036
+ ## list of one, or an empty list. A constant is no property, so the property list never names it,
1037
+ ## and `get` answers null for one.
1038
+ static func constant_of(holder: Variant, named: String) -> Array:
1039
+ if not holder is Object:
1040
+ return []
1041
+ var object: Object = holder
1042
+ var attached: Variant = object.get_script()
1043
+ if not attached is Script:
1044
+ return []
1045
+ var script: Script = attached
1046
+ while script != null:
1047
+ var constants: Dictionary = script.get_script_constant_map()
1048
+ if constants.has(named):
1049
+ return [constants[named]]
1050
+ script = script.get_base_script()
1051
+ return []
1052
+
1053
+
1054
+ ## Whether [param named] reaches a constant on [param holder] rather than a property.
1055
+ static func is_constant(holder: Variant, named: String) -> bool:
1056
+ if not holder is Object:
1057
+ return false
1058
+ var object: Object = holder
1059
+ return not _has_property(object, named) and not constant_of(object, named).is_empty()
@@ -11,6 +11,11 @@ const LOOP_MODE_NAMES: Dictionary[int, String] = {
11
11
  Animation.LOOP_LINEAR: "linear",
12
12
  Animation.LOOP_PINGPONG: "pingpong",
13
13
  }
14
+ const UPDATE_MODE_NAMES: Dictionary[int, String] = {
15
+ Animation.UPDATE_CONTINUOUS: "continuous",
16
+ Animation.UPDATE_DISCRETE: "discrete",
17
+ Animation.UPDATE_CAPTURE: "capture",
18
+ }
14
19
  const TRACK_TYPE_NAMES: Dictionary[int, String] = {
15
20
  Animation.TYPE_VALUE: "property",
16
21
  Animation.TYPE_POSITION_3D: "position_3d",
@@ -166,6 +171,9 @@ static func _track(anim: Animation, track: int, values: GdhSerialisation) -> Dic
166
171
  var property: String = str(path.get_concatenated_subnames())
167
172
  if not property.is_empty():
168
173
  shaped["property"] = property
174
+ if kind == Animation.TYPE_VALUE:
175
+ var mode: int = anim.value_track_get_update_mode(track)
176
+ shaped["update"] = UPDATE_MODE_NAMES.get(mode, str(mode))
169
177
  var keyframes: Array[Dictionary] = []
170
178
  var methods: Dictionary = {}
171
179
  for key: int in anim.track_get_key_count(track):
@@ -81,6 +81,8 @@ static func run(params: Dictionary) -> Dictionary:
81
81
  var answered: Dictionary = answer
82
82
  if not GdhSceneFile.also_saved.is_empty():
83
83
  answered["alsoSaved"] = GdhSceneFile.also_saved.duplicate(true)
84
+ if not GdhSceneFile.unmade_note.is_empty():
85
+ answered["unmadeConnections"] = GdhSceneFile.unmade_note
84
86
  if not GdhSceneUids.by_path.is_empty():
85
87
  answered["namedByPath"] = (
86
88
  (
@@ -96,6 +96,11 @@ static func without_annotations(line: String) -> String:
96
96
  # brace opens before the `#` and closes on a later line. Held by a case, since the argument for the
97
97
  # difference is not visible from the code and reads like an oversight beside its neighbours.
98
98
  static func without_comment(line: String) -> String:
99
+ var start: int = _comment_start(line)
100
+ return (line if start == -1 else line.substr(0, start)).strip_edges()
101
+
102
+
103
+ static func _comment_start(line: String) -> int:
99
104
  var quote: String = ""
100
105
  for i: int in range(line.length()):
101
106
  var ch: String = line[i]
@@ -104,10 +109,30 @@ static func without_comment(line: String) -> String:
104
109
  quote = ""
105
110
  continue
106
111
  if ch == "#":
107
- return line.substr(0, i).strip_edges()
112
+ return i
108
113
  if ch == '"' or ch == "'":
109
114
  quote = ch
110
- return line.strip_edges()
115
+ return -1
116
+
117
+
118
+ ## The code in front of each TODO or FIXME comment in [param content], one entry a comment.
119
+ ##
120
+ ## Read from the comments alone, as the editor highlights them, so `#TODO` counts and the same
121
+ ## words inside a string or a multi-line string do not.
122
+ static func noted_work(content: String) -> Array[String]:
123
+ var noted: Array[String] = []
124
+ var in_string: bool = false
125
+ for line: String in content.split("\n"):
126
+ if in_string:
127
+ in_string = triple_quotes(line) % 2 == 0
128
+ continue
129
+ var start: int = _comment_start(line)
130
+ var code: String = line if start == -1 else line.substr(0, start)
131
+ if triple_quotes(code) % 2 == 1:
132
+ in_string = true
133
+ elif start != -1 and compiled("^#+\\s*(?:TODO|FIXME)\\b").search(line.substr(start)):
134
+ noted.append(code.strip_edges())
135
+ return noted
111
136
 
112
137
 
113
138
  ## How many triple quotes of either kind [param line] holds. An odd count opens or closes a string
@@ -5,6 +5,7 @@ const GdhFileWalk = preload("file_walk.gd")
5
5
  const GdhImportPipeline = preload("import_pipeline.gd")
6
6
  const GdhInputActions = preload("input_actions.gd")
7
7
  const GdhLog = preload("logger.gd")
8
+ const GdhPatterns = preload("patterns.gd")
8
9
  const GdhRead = preload("reading.gd")
9
10
  const GdhSceneConnections = preload("scene_connections.gd")
10
11
  const GdhScriptCheck = preload("script_check.gd")
@@ -87,7 +88,7 @@ func get_project_health(params: Dictionary) -> Dictionary:
87
88
  var content: String = file.get_as_text()
88
89
  file.close()
89
90
 
90
- if "# TODO" in content or "# FIXME" in content:
91
+ if not GdhPatterns.noted_work(content).is_empty():
91
92
  todo_count += 1
92
93
 
93
94
  # Work noted for later, which no script fails on, so said rather than failed. A count of
@@ -8,6 +8,7 @@ const GdhFileWalk = preload("file_walk.gd")
8
8
  const GdhImportPipeline = preload("import_pipeline.gd")
9
9
  const GdhInputActions = preload("input_actions.gd")
10
10
  const GdhLog = preload("logger.gd")
11
+ const GdhPatterns = preload("patterns.gd")
11
12
  const GdhRead = preload("reading.gd")
12
13
  const GdhSceneConnections = preload("scene_connections.gd")
13
14
  const GdhScriptCheck = preload("script_check.gd")
@@ -205,11 +206,12 @@ func validate_project(params: Dictionary) -> Dictionary:
205
206
  var content: String = file.get_as_text()
206
207
  file.close()
207
208
 
208
- if "# TODO" in content or "# FIXME" in content:
209
+ var noted: Array[String] = GdhPatterns.noted_work(content)
210
+ if not noted.is_empty():
209
211
  script_issues.append(
210
212
  { "path": script_path, "issue": "Contains TODO/FIXME comments" }
211
213
  )
212
- if "pass # TODO" in content:
214
+ if "pass" in noted:
213
215
  script_issues.append(
214
216
  { "path": script_path, "issue": "Contains unimplemented functions" }
215
217
  )
package/build/keeper.js CHANGED
@@ -8895,6 +8895,7 @@ function frameEndLine() {
8895
8895
  return frameEnd;
8896
8896
  }
8897
8897
  var READ_MS = 5000;
8898
+ var EVALUATE_MS = 60000;
8898
8899
  var REPLIES = new Set([
8899
8900
  "debug_enter",
8900
8901
  "debug_exit",
@@ -9478,10 +9479,10 @@ class GameDebugger {
9478
9479
  }
9479
9480
  this.forget("evaluation_return");
9480
9481
  this.send("evaluate", [expression, frame], held.thread);
9481
- const answered = await this.next(GameDebugger.named("evaluation_return"), READ_MS);
9482
+ const answered = await this.next(GameDebugger.named("evaluation_return"), EVALUATE_MS);
9482
9483
  if (answered === null || !("data" in answered)) {
9483
9484
  return {
9484
- refused: `The game worked out nothing for ${expression} within ${READ_MS / 1000}s. Ask again.`
9485
+ refused: `The game worked out nothing for ${expression} within ${EVALUATE_MS / 1000}s. The first expression a game evaluates loads the script of every global class in the project first, static initialisers included, so one of those may still be running. Ask again.`
9485
9486
  };
9486
9487
  }
9487
9488
  const [, , type, value, hint] = answered.data;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.45",
3
+ "version": "1.3.46",
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",