gdharness 1.3.43 → 1.3.45

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.
@@ -56,6 +56,25 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
56
56
  # its first function would otherwise be judged against the default.
57
57
  _mark_virtuals(functions, extends_name, full_script_path)
58
58
 
59
+ # The engine loads a base named by its path and a constant handed to load or preload as surely
60
+ # as a path written in the call, and both were left out while project_dependencies named them: a
61
+ # script extending "frame.gd" did not depend on it, nor one calling load(TYPE_CASE).
62
+ for written: String in _based_on_paths(extends_name, own.inner_classes):
63
+ if not written in dependencies:
64
+ dependencies.append(written)
65
+ var held: Dictionary = {}
66
+ for constant: Dictionary in own.constants:
67
+ held[str(constant.get("name", ""))] = _unquoted(str(constant.get("value", "")))
68
+ for code: String in GdhDependencies.code_by_line(content):
69
+ for loading: RegExMatch in (
70
+ GdhPatterns
71
+ . compiled("(?<![\\w.])(?:preload|load)\\s*\\(\\s*([A-Za-z_]\\w*)\\s*\\)")
72
+ . search_all(code)
73
+ ):
74
+ var written: String = str(held.get(loading.get_string(1), ""))
75
+ if not written.is_empty() and not written in dependencies:
76
+ dependencies.append(written)
77
+
59
78
  # A script typed by its class_name is depended on as surely as one preloaded, and the answer
60
79
  # listed only the preloads: a `var stats: Stats` left stats.gd out, while project_dependencies
61
80
  # named it. The same reading as that tool's, so the two agree.
@@ -131,7 +150,7 @@ func _members(
131
150
  # Inside a string the raw line is counted, because a `#` in it is text and taking it for a
132
151
  # comment would cut off the quotes that close the string.
133
152
  if in_multiline_string:
134
- if _triple_quotes(raw) % 2 == 1:
153
+ if GdhPatterns.triple_quotes(raw) % 2 == 1:
135
154
  in_multiline_string = false
136
155
  continue
137
156
  # A `##` block documents what it sits directly above: the class when that is its class_name
@@ -165,7 +184,7 @@ func _members(
165
184
  # it opened on. Toggling on any count lost `const HELP := """Usage"""`, and everything after
166
185
  # it up to the next triple quote, with the state inverted from there to the end of the file.
167
186
  # The line that opens one is still read: it can be the declaration the string belongs to.
168
- if _triple_quotes(stripped) % 2 == 1:
187
+ if GdhPatterns.triple_quotes(stripped) % 2 == 1:
169
188
  in_multiline_string = true
170
189
 
171
190
  # Only a line at this body's own margin declares one of its members. Deeper, it is a
@@ -207,7 +226,7 @@ func _members(
207
226
  while i + 1 < lines.size():
208
227
  i += 1
209
228
  stripped += "\n" + lines[i]
210
- if _triple_quotes(lines[i]) % 2 == 1:
229
+ if GdhPatterns.triple_quotes(lines[i]) % 2 == 1:
211
230
  break
212
231
  in_multiline_string = false
213
232
  header = GdhPatterns.without_annotations(stripped)
@@ -821,13 +840,31 @@ func _collect_dependencies(line: String, into: Array[String]) -> void:
821
840
  into.append(dep)
822
841
 
823
842
 
824
- # How many triple quotes of either kind [param line] holds.
825
- static func _declares_variable(header: String) -> bool:
826
- return header.begins_with("var ") or header.begins_with("static var ")
843
+ ## The paths a script and its inner classes extend by, as written: [param extends_name] the
844
+ ## script's own extends, and [param inner_classes] its inner classes as answered.
845
+ static func _based_on_paths(extends_name: String, inner_classes: Array) -> Array[String]:
846
+ var paths: Array[String] = []
847
+ var written: String = _unquoted(extends_name)
848
+ if not written.is_empty():
849
+ paths.append(written)
850
+ for inner: Dictionary in inner_classes:
851
+ var deeper: Array = inner.get("inner_classes", [])
852
+ for path: String in _based_on_paths(str(inner.get("extends", "")), deeper):
853
+ if not path in paths:
854
+ paths.append(path)
855
+ return paths
856
+
857
+
858
+ ## The text of [param literal], a string literal as written; empty for anything else.
859
+ static func _unquoted(literal: String) -> String:
860
+ var text: String = literal.strip_edges()
861
+ if text.length() >= 2 and text[0] in ["\"", "'"] and text[-1] == text[0]:
862
+ return text.substr(1, text.length() - 2)
863
+ return ""
827
864
 
828
865
 
829
- static func _triple_quotes(line: String) -> int:
830
- return line.count('"""') + line.count("'''")
866
+ static func _declares_variable(header: String) -> bool:
867
+ return header.begins_with("var ") or header.begins_with("static var ")
831
868
 
832
869
 
833
870
  func _extract_dependencies(line: String) -> Array[String]:
@@ -108,3 +108,9 @@ static func without_comment(line: String) -> String:
108
108
  if ch == '"' or ch == "'":
109
109
  quote = ch
110
110
  return line.strip_edges()
111
+
112
+
113
+ ## How many triple quotes of either kind [param line] holds. An odd count opens or closes a string
114
+ ## that runs across lines.
115
+ static func triple_quotes(line: String) -> int:
116
+ return line.count('"""') + line.count("'''")
@@ -387,8 +387,7 @@ func add_autoload(params: Dictionary) -> Dictionary:
387
387
  var global: bool = GdhRead.as_bool(params.get("global", true), true)
388
388
 
389
389
  # There is no disabled autoload in Godot 4: every entry in the list is loaded under the root at
390
- # every start. `enabled: false` used to write the entry without its asterisk and answer that it
391
- # was disabled, and the autoload ran all the same.
390
+ # every start. An entry without its asterisk is one whose name is not global, and it still runs.
392
391
  if not GdhRead.as_bool(params.get("enabled", true), true):
393
392
  return (
394
393
  _log
@@ -433,12 +432,15 @@ func add_autoload(params: Dictionary) -> Dictionary:
433
432
  "action": "unchanged",
434
433
  "saved": false,
435
434
  }
435
+ # The editor's Add replaces an entry of the same name too; said here, since the replaced script
436
+ # is no longer loaded and nothing else in the answer names it.
437
+ var held: String = str(ProjectSettings.get_setting(setting)) if was_updated else ""
436
438
  ProjectSettings.set_setting(setting, entry)
437
439
  var err: Error = GdhProjectFile.save([setting])
438
440
  if err != OK:
439
441
  return _log.failure("Failed to save project.godot: " + error_string(err))
440
442
 
441
- return {
443
+ var answer: Dictionary = {
442
444
  "name": name,
443
445
  "path": path,
444
446
  "enabled": true,
@@ -446,6 +448,9 @@ func add_autoload(params: Dictionary) -> Dictionary:
446
448
  "action": "updated" if was_updated else "added",
447
449
  "saved": true,
448
450
  }
451
+ if was_updated:
452
+ answer["old_path"] = held.trim_prefix("*")
453
+ return answer
449
454
 
450
455
 
451
456
  ## Why [param name] cannot name an autoload, or empty when it can. The editor's Autoload tab refuses
@@ -196,9 +196,9 @@ func validate_project(params: Dictionary) -> Dictionary:
196
196
  var scripts_checked: int = 0
197
197
  var script_issues: Array[Dictionary] = []
198
198
 
199
- # A hundred scripts is enough to say whether the project is tidy without a large one
200
- # turning validation into a full read of its source tree.
201
- for script_path: String in script_files.slice(0, 100):
199
+ # Every script, as health reads them: cut at the first hundred in walk order, the count sat
200
+ # beside a compile check of all 803 scripts of a game and read as how many were compiled.
201
+ for script_path: String in script_files:
202
202
  scripts_checked += 1
203
203
  var file: FileAccess = FileAccess.open(script_path, FileAccess.READ)
204
204
  if file:
@@ -292,7 +292,6 @@ func validate_project(params: Dictionary) -> Dictionary:
292
292
  "warnings": warnings,
293
293
  "checks_performed": checks_performed,
294
294
  "scripts_checked": scripts_checked,
295
- # Said beside the count, so a project of four hundred scripts is not read as checked whole.
296
295
  "scripts_total": script_files.size(),
297
296
  "issue_count": issues.size(),
298
297
  "warning_count": warnings.size(),
@@ -312,16 +312,25 @@ static func _bare(word: String) -> String:
312
312
  return word.to_lower().replace("_", "").replace(" ", "")
313
313
 
314
314
 
315
- ## The signals [param object] has that a caller can connect, its own class's first: the engine
316
- ## lists them from Object down, which puts a Button's pressed after thirty a Control has, and names
317
- ## starting with an underscore are the engine's own.
315
+ ## The signals [param object] has that a caller can connect, its own class's first: its script's,
316
+ ## as declared, then the engine's from the class down to Object. The engine lists its own from
317
+ ## Object down, which puts a Button's pressed after thirty a Control has, and its script's ahead of
318
+ ## those, so reversing the whole list put a game's own two signals after twenty-four of the
319
+ ## engine's. Names starting with an underscore are the engine's own.
318
320
  static func signals_of(object: Object) -> Array:
321
+ var declared: Array = []
322
+ var script: Script = object.get_script()
323
+ if script != null:
324
+ for entry: Dictionary in script.get_script_signal_list():
325
+ var name: String = str(entry["name"])
326
+ if not name.begins_with("_") and not name in declared:
327
+ declared.append(name)
319
328
  var names: Array = []
320
329
  for entry: Dictionary in object.get_signal_list():
321
330
  var name: String = str(entry["name"])
322
- if not name.begins_with("_"):
331
+ if not name.begins_with("_") and not name in declared:
323
332
  names.push_front(name)
324
- return names
333
+ return declared + names
325
334
 
326
335
 
327
336
  ## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
@@ -62,7 +62,7 @@ static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
62
62
  var value: Variant = ProjectSettings.get_setting(name)
63
63
  var described: Dictionary = {
64
64
  "setting": name,
65
- "type": type_string(kind),
65
+ "type": type_named(kind, value),
66
66
  "value": values.serialize_value(value),
67
67
  }
68
68
  # As a single get says it, so a family read can be told apart into what the project sets and
@@ -81,6 +81,47 @@ static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
81
81
  return { "prefix": prefix, "count": found.size(), "settings": found }
82
82
 
83
83
 
84
+ ## The type [param kind] names, with what [param value] holds when it is a typed array or
85
+ ## dictionary, as project.godot writes it: a `Dictionary[String, bool]` was answered as Dictionary,
86
+ ## which hides what its keys and values must be.
87
+ static func type_named(kind: int, value: Variant) -> String:
88
+ if kind == TYPE_ARRAY and value is Array:
89
+ var list: Array = value
90
+ if list.is_typed():
91
+ var element: String = _held(
92
+ list.get_typed_builtin(), list.get_typed_class_name(), list.get_typed_script()
93
+ )
94
+ return "Array[%s]" % element
95
+ elif kind == TYPE_DICTIONARY and value is Dictionary:
96
+ var table: Dictionary = value
97
+ if table.is_typed():
98
+ var key: String = _held(
99
+ table.get_typed_key_builtin(),
100
+ table.get_typed_key_class_name(),
101
+ table.get_typed_key_script()
102
+ )
103
+ var held: String = _held(
104
+ table.get_typed_value_builtin(),
105
+ table.get_typed_value_class_name(),
106
+ table.get_typed_value_script()
107
+ )
108
+ return "Dictionary[%s, %s]" % [key, held]
109
+ return type_string(kind)
110
+
111
+
112
+ ## One side of a typed collection: a built-in type, a class, or a script by its class_name.
113
+ static func _held(builtin: int, named: StringName, script: Variant) -> String:
114
+ if builtin == TYPE_NIL:
115
+ return "Variant"
116
+ if builtin != TYPE_OBJECT:
117
+ return type_string(builtin)
118
+ if script is Script:
119
+ var declared: Script = script
120
+ if not declared.get_global_name().is_empty():
121
+ return declared.get_global_name()
122
+ return str(named)
123
+
124
+
84
125
  ## The setting [param setting_path] overrides for builds with one feature, as
85
126
  ## `application/run/max_fps.mobile` does `application/run/max_fps`, or "" for a plain setting.
86
127
  static func override_base(setting_path: String) -> String:
package/build/keeper.js CHANGED
@@ -6946,7 +6946,7 @@ var require_public_api = __commonJS(function(exports) {
6946
6946
  });
6947
6947
 
6948
6948
  // src/keeper.ts
6949
- import { closeSync as closeSync2, existsSync as existsSync3, mkdtempSync as mkdtempSync4, openSync as openSync3, readFileSync as readFileSync6, rmSync as rmSync6, writeFileSync as writeFileSync3 } from "node:fs";
6949
+ import { closeSync as closeSync2, existsSync as existsSync3, mkdtempSync as mkdtempSync4, openSync as openSync3, readFileSync as readFileSync7, rmSync as rmSync6, writeFileSync as writeFileSync3 } from "node:fs";
6950
6950
  import { tmpdir as tmpdir5 } from "node:os";
6951
6951
  import { join as join10 } from "node:path";
6952
6952
  import { setTimeout as delay3 } from "node:timers/promises";
@@ -7435,7 +7435,7 @@ function crashesReported() {
7435
7435
  }
7436
7436
 
7437
7437
  // src/game-debugger.ts
7438
- import { readFileSync as readFileSync4 } from "node:fs";
7438
+ import { readFileSync as readFileSync5 } from "node:fs";
7439
7439
  import { createServer } from "node:net";
7440
7440
  import { join as join6 } from "node:path";
7441
7441
 
@@ -7802,6 +7802,36 @@ var KEYWORDS = new Set([
7802
7802
  "TAU"
7803
7803
  ]);
7804
7804
 
7805
+ // src/whole-file.ts
7806
+ import { readFileSync as readFileSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
7807
+ var HELD_FILE_MS = 1000;
7808
+ var HELD_FILE_RETRY_MS = 10;
7809
+ var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
7810
+ function writeWhole(path, text) {
7811
+ const partial = `${path}.${process.pid}.partial`;
7812
+ try {
7813
+ writeFileSync2(partial, text, "utf8");
7814
+ } catch {
7815
+ return false;
7816
+ }
7817
+ const until = Date.now() + HELD_FILE_MS;
7818
+ for (;; ) {
7819
+ try {
7820
+ renameSync(partial, path);
7821
+ return true;
7822
+ } catch (error) {
7823
+ const code = error.code ?? "";
7824
+ if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
7825
+ try {
7826
+ rmSync2(partial, { force: true });
7827
+ } catch {}
7828
+ return false;
7829
+ }
7830
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
7831
+ }
7832
+ }
7833
+ }
7834
+
7805
7835
  // src/dictionary.ts
7806
7836
  function emptyRecord() {
7807
7837
  return Object.create(null);
@@ -8399,14 +8429,14 @@ var MEMORY_KEPT = 4 * 1024 ** 3;
8399
8429
  var MEMORY_PER_ENGINE = 1.5 * 1024 ** 3;
8400
8430
 
8401
8431
  // src/process-children.ts
8402
- import { readFileSync as readFileSync2 } from "node:fs";
8432
+ import { readFileSync as readFileSync3 } from "node:fs";
8403
8433
  import { basename as basename3 } from "node:path";
8404
8434
  async function linuxStartTimes(pids) {
8405
8435
  const began = new Map;
8406
8436
  const hertz = await ticksPerSecond();
8407
8437
  let booted;
8408
8438
  try {
8409
- booted = Number(/^btime\s+(\d+)$/m.exec(readFileSync2("/proc/stat", "utf8"))?.[1] ?? Number.NaN) * 1000;
8439
+ booted = Number(/^btime\s+(\d+)$/m.exec(readFileSync3("/proc/stat", "utf8"))?.[1] ?? Number.NaN) * 1000;
8410
8440
  } catch {
8411
8441
  return began;
8412
8442
  }
@@ -8416,7 +8446,7 @@ async function linuxStartTimes(pids) {
8416
8446
  for (const pid of pids) {
8417
8447
  let stat;
8418
8448
  try {
8419
- stat = readFileSync2(`/proc/${String(pid)}/stat`, "utf8");
8449
+ stat = readFileSync3(`/proc/${String(pid)}/stat`, "utf8");
8420
8450
  } catch {
8421
8451
  continue;
8422
8452
  }
@@ -8516,36 +8546,6 @@ function commandWords(command) {
8516
8546
  return words;
8517
8547
  }
8518
8548
 
8519
- // src/whole-file.ts
8520
- import { renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
8521
- var HELD_FILE_MS = 1000;
8522
- var HELD_FILE_RETRY_MS = 10;
8523
- var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
8524
- function writeWhole(path, text) {
8525
- const partial = `${path}.${process.pid}.partial`;
8526
- try {
8527
- writeFileSync2(partial, text, "utf8");
8528
- } catch {
8529
- return false;
8530
- }
8531
- const until = Date.now() + HELD_FILE_MS;
8532
- for (;; ) {
8533
- try {
8534
- renameSync(partial, path);
8535
- return true;
8536
- } catch (error) {
8537
- const code = error.code ?? "";
8538
- if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
8539
- try {
8540
- rmSync2(partial, { force: true });
8541
- } catch {}
8542
- return false;
8543
- }
8544
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
8545
- }
8546
- }
8547
- }
8548
-
8549
8549
  // src/game-log.ts
8550
8550
  var COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
8551
8551
  var ADMITTED = {
@@ -8847,7 +8847,7 @@ function parseIniLikeValue(value) {
8847
8847
  }
8848
8848
 
8849
8849
  // src/server-version.ts
8850
- import { readFileSync as readFileSync3 } from "node:fs";
8850
+ import { readFileSync as readFileSync4 } from "node:fs";
8851
8851
 
8852
8852
  // src/update-check.ts
8853
8853
  var CACHE_MS = 10 * 60 * 1000;
@@ -8858,7 +8858,7 @@ var MAX_RETRY_MS = 60 * 60 * 1000;
8858
8858
  var DEBUG_MODE = process.env["GDHARNESS_DEBUG"] === "1";
8859
8859
  var SERVER_VERSION = (() => {
8860
8860
  try {
8861
- const pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
8861
+ const pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8"));
8862
8862
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
8863
8863
  } catch {
8864
8864
  return "0.0.0";
@@ -8884,7 +8884,7 @@ var RUNTIME_AUTOLOAD2 = `${RUNTIME_ADDON}runtime_autoload.gd`;
8884
8884
  var frameEnd = null;
8885
8885
  function frameEndLine() {
8886
8886
  if (frameEnd === null) {
8887
- const lines = readFileSync4(join6(shippedAddonsDirectory(), "gdharness_runtime", "runtime_autoload.gd"), "utf8").split(`
8887
+ const lines = readFileSync5(join6(shippedAddonsDirectory(), "gdharness_runtime", "runtime_autoload.gd"), "utf8").split(`
8888
8888
  `);
8889
8889
  const declared = lines.findIndex((line) => line.startsWith("func _notification("));
8890
8890
  if (declared === -1) {
@@ -9642,7 +9642,7 @@ function readLaunched(printed) {
9642
9642
 
9643
9643
  // src/run-record.ts
9644
9644
  import { createHash as createHash2 } from "node:crypto";
9645
- import { mkdirSync, openSync as openSync2, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync5, statSync as statSync3 } from "node:fs";
9645
+ import { mkdirSync, openSync as openSync2, readdirSync as readdirSync2, readFileSync as readFileSync6, rmSync as rmSync5, statSync as statSync3 } from "node:fs";
9646
9646
  import { basename as basename4, join as join9, resolve as resolve4 } from "node:path";
9647
9647
 
9648
9648
  // src/runtime-client.ts
@@ -9738,7 +9738,7 @@ function recordRunEnded(projectPath, pid, ending) {
9738
9738
  function recordAt(path) {
9739
9739
  let parsed;
9740
9740
  try {
9741
- parsed = JSON.parse(readFileSync5(path, "utf8"));
9741
+ parsed = JSON.parse(readFileSync6(path, "utf8"));
9742
9742
  } catch {
9743
9743
  return null;
9744
9744
  }
@@ -10186,7 +10186,7 @@ async function startThroughHelper(spec, output, stopFile) {
10186
10186
  exited.now = true;
10187
10187
  });
10188
10188
  for (let waited = 0;waited < DESKTOP_START_MS; waited += 50) {
10189
- const pid = existsSync3(pidFile) ? Number(readFileSync6(pidFile, "utf8").trim()) : Number.NaN;
10189
+ const pid = existsSync3(pidFile) ? Number(readFileSync7(pidFile, "utf8").trim()) : Number.NaN;
10190
10190
  if (Number.isInteger(pid) && pid > 0) {
10191
10191
  return { child: started.child, pid };
10192
10192
  }
@@ -10217,7 +10217,7 @@ function profiledBy(path, projectPath) {
10217
10217
  };
10218
10218
  let projectGodot = null;
10219
10219
  try {
10220
- projectGodot = readFileSync6(join10(projectPath, "project.godot"), "utf8");
10220
+ projectGodot = readFileSync7(join10(projectPath, "project.godot"), "utf8");
10221
10221
  } catch {}
10222
10222
  const observer = new ProfileObserver((totals) => {
10223
10223
  const first = latest === null;
@@ -10456,7 +10456,7 @@ function restartSaid(notes, pid) {
10456
10456
  return null;
10457
10457
  }
10458
10458
  try {
10459
- const said = JSON.parse(readFileSync6(join10(notes, `exit-${String(pid)}.json`), "utf8"));
10459
+ const said = JSON.parse(readFileSync7(join10(notes, `exit-${String(pid)}.json`), "utf8"));
10460
10460
  return typeof said === "object" && said !== null && "restart" in said && typeof said.restart === "boolean" ? said.restart : null;
10461
10461
  } catch {
10462
10462
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.43",
3
+ "version": "1.3.45",
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",