gdharness 1.1.4 → 1.1.5

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
@@ -41827,6 +41827,7 @@ class GodotServer {
41827
41827
  const ghosts = blind.stillHeld ?? [];
41828
41828
  let waitedForEditorMs = 0;
41829
41829
  let first;
41830
+ let completedBefore;
41830
41831
  for (;; ) {
41831
41832
  const busySince = Date.now();
41832
41833
  let wasBusy = false;
@@ -41841,8 +41842,9 @@ class GodotServer {
41841
41842
  if (first.isError) {
41842
41843
  return first;
41843
41844
  }
41844
- const declined = asParams(JSON.parse(first.content[0]?.text ?? "{}"))["started"] === false;
41845
- if (!declined || Date.now() - started >= timeoutMs) {
41845
+ const firstAnswer = asParams(JSON.parse(first.content[0]?.text ?? "{}"));
41846
+ completedBefore = readNumber(firstAnswer, "scansCompletedBefore");
41847
+ if (firstAnswer["started"] !== false || Date.now() - started >= timeoutMs) {
41846
41848
  break;
41847
41849
  }
41848
41850
  await new Promise((settle) => setTimeout(settle, 100));
@@ -41853,7 +41855,9 @@ class GodotServer {
41853
41855
  await new Promise((settle) => setTimeout(settle, 100));
41854
41856
  const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
41855
41857
  const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
41856
- busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
41858
+ const completed = readNumber(status, "scansCompleted");
41859
+ const notYetOver = completedBefore !== undefined && completed !== undefined && completed <= completedBefore;
41860
+ busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending || notYetOver;
41857
41861
  if (busy) {
41858
41862
  idleSince = null;
41859
41863
  } else {
@@ -11,12 +11,14 @@ var _editor_plugin: EditorPlugin = null
11
11
  var _values: Serialisation = Serialisation.new()
12
12
  var _properties: PropertyValues = PropertyValues.new()
13
13
 
14
- ## How many times the editor has said its filesystem changed, which a scan run on the main thread
15
- ## does before `scan()` returns: counted so a rescan can tell a scan that ran there from one the
16
- ## editor declined.
17
- var _scans_finished: int = 0
18
-
19
- ## When the last scan finished, on the engine's clock, or -1 before the first. The editor writes
14
+ ## How many scans the editor has completed, counted off `sources_changed`, which it emits at the end
15
+ ## of every scan once the reimport the scan found is done: the one moment a scan is over. Neither
16
+ ## flag it offers says so. A threaded scan stops reporting itself when its thread finishes, and its
17
+ ## import runs a frame or more later when the thread is joined; an editor in the background took
18
+ ## long enough over that frame that a rescan answered before the import of 211 changed scenes began.
19
+ var _scans_completed: int = 0
20
+
21
+ ## When the last scan completed, on the engine's clock, or -1 before the first. The editor writes
20
22
  ## the class cache a frame after it stops reporting a scan, measured on 4.7.2, so a game started
21
23
  ## in between reads a file being rewritten; how long ago the scan ended is what tells a caller
22
24
  ## whether that write can still be coming.
@@ -26,14 +28,14 @@ var _scan_finished_at_msec: int = -1
26
28
  func set_editor_plugin(plugin: EditorPlugin) -> void:
27
29
  _editor_plugin = plugin
28
30
  var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
29
- if not filesystem.filesystem_changed.is_connected(_scan_finished):
30
- var watched: int = filesystem.filesystem_changed.connect(_scan_finished)
31
+ if not filesystem.sources_changed.is_connected(_scan_completed):
32
+ var watched: int = filesystem.sources_changed.connect(_scan_completed)
31
33
  if watched != OK:
32
- push_error("gdharness could not watch for finished scans: %d" % watched)
34
+ push_error("gdharness could not watch for completed scans: %d" % watched)
33
35
 
34
36
 
35
- func _scan_finished() -> void:
36
- _scans_finished += 1
37
+ func _scan_completed(_sources_exist: bool) -> void:
38
+ _scans_completed += 1
37
39
  _scan_finished_at_msec = Time.get_ticks_msec()
38
40
 
39
41
 
@@ -658,8 +660,8 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
658
660
  # conditions from its progress dialog. Declined and said, so the caller waits and asks again.
659
661
  var busy: bool = filesystem.is_scanning() or filesystem.is_importing()
660
662
  var started: bool = false
663
+ var completed_before: int = _scans_completed
661
664
  if not Read.as_bool(args.get("statusOnly", false)) and not busy:
662
- var finished_before: int = _scans_finished
663
665
  filesystem.scan()
664
666
  # Whether the scan ran, read off the editor rather than assumed. `scan()` returns without
665
667
  # a word while the thread of the scan before is still to be joined, which is a frame or
@@ -668,16 +670,19 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
668
670
  # downstream, a rescan after 698 files were rewritten answered finished in 1.5s and the
669
671
  # one changed file stayed on its old import. A threaded scan reports itself from inside
670
672
  # the call, and one run on this thread has finished and signalled before it returns.
671
- started = filesystem.is_scanning() or _scans_finished != finished_before
673
+ started = filesystem.is_scanning() or _scans_completed != completed_before
672
674
 
673
675
  # Importing is reported separately from scanning, and a class is not registered until
674
- # both are done, so a caller watching only one of them can look too early.
676
+ # both are done, so a caller watching only one of them can look too early. The two counts are
677
+ # what a caller waits on: the scan it asked for is over once the count has moved past the one
678
+ # from before it was asked.
675
679
  return {
676
680
  "ok": true,
677
681
  "started": started,
678
682
  "scanning": filesystem.is_scanning(),
679
683
  "importing": filesystem.is_importing(),
680
- "scansFinished": _scans_finished,
684
+ "scansCompletedBefore": completed_before,
685
+ "scansCompleted": _scans_completed,
681
686
  }
682
687
 
683
688
 
@@ -8,6 +8,11 @@ const IMPORTABLE_EXTENSIONS: Array[String] = [
8
8
  "png", "jpg", "jpeg", "webp", "svg", "wav", "mp3", "ogg", "ttf", "otf", "glb", "gltf", "fbx", "obj"
9
9
  ]
10
10
 
11
+ ## How many resources a scene's dependencies are followed through before the walk stops: a scene's
12
+ ## materials and what they use are a handful, and a project that references its way around a cycle
13
+ ## is not walked for ever.
14
+ const DEPENDENCY_WALK_LIMIT: int = 200
15
+
11
16
  var _log: Log
12
17
  var _files: FileWalk = FileWalk.new()
13
18
 
@@ -416,12 +421,18 @@ func _import_status_of(resource_path: String, import_file_path: String) -> Dicti
416
421
  # before the image was is built without it. Read off what the imported scene depends on, not off
417
422
  # file times: a texture a 3D scene uses is reimported compressed after the scene, so its outputs
418
423
  # are always the newer, and 33 correct scenes read as stale when judged that way.
424
+ #
425
+ # Followed through the resources the scene depends on, since a post-import script that swaps each
426
+ # material for a saved one leaves the scene depending on the material and only the material on
427
+ # the image: 211 correct scenes read as stale when only the scene's own list was read. And not
428
+ # held against a scene an import script shaped at all, since the script may replace a material
429
+ # with one using other images entirely, which is the scene it was meant to build.
419
430
  var without: Array[String] = []
420
431
  var images: Array[String] = _images_of(resource_path)
421
- if not images.is_empty():
422
- var depended: PackedStringArray = ResourceLoader.get_dependencies(resource_path)
432
+ if not images.is_empty() and not _shaped_by_a_script(import_file_path):
433
+ var depended: Dictionary[String, bool] = _depended_on(resource_path)
423
434
  for image: String in images:
424
- if FileAccess.file_exists(image + ".import") and not _names(depended, image):
435
+ if FileAccess.file_exists(image + ".import") and not depended.has(image):
425
436
  without.append(image)
426
437
  if not without.is_empty():
427
438
  status["status"] = "needs_reimport"
@@ -442,13 +453,38 @@ static func _recorded_source_md5(resource_path: String) -> String:
442
453
  return str(config.get_value("", "source_md5", ""))
443
454
 
444
455
 
445
- ## Whether [param dependencies], as [method ResourceLoader.get_dependencies] lists them, include
446
- ## [param path]; each entry may carry a uid and a type ahead of the path.
447
- static func _names(dependencies: PackedStringArray, path: String) -> bool:
448
- for entry: String in dependencies:
449
- if entry == path or entry.ends_with("::" + path):
450
- return true
451
- return false
456
+ ## The path of one entry [method ResourceLoader.get_dependencies] lists, past any uid and type.
457
+ static func _path_of(entry: String) -> String:
458
+ var at: int = entry.rfind("::")
459
+ return entry if at < 0 else entry.substr(at + 2)
460
+
461
+
462
+ ## Every path [param resource_path] depends on, through the resources it depends on in turn, as far
463
+ ## as [constant DEPENDENCY_WALK_LIMIT] resources; a set, keyed by path.
464
+ static func _depended_on(resource_path: String) -> Dictionary[String, bool]:
465
+ var found: Dictionary[String, bool] = {}
466
+ var pending: Array[String] = [resource_path]
467
+ var walked: int = 0
468
+ while not pending.is_empty() and walked < DEPENDENCY_WALK_LIMIT:
469
+ var next: String = pending.pop_back()
470
+ walked += 1
471
+ for entry: String in ResourceLoader.get_dependencies(next):
472
+ var path: String = _path_of(entry)
473
+ if found.has(path):
474
+ continue
475
+ found[path] = true
476
+ # Only what can hold further references: an image or a sound depends on nothing.
477
+ if path.get_extension().to_lower() in ["tres", "res", "tscn", "scn", "material"]:
478
+ pending.append(path)
479
+ return found
480
+
481
+
482
+ ## Whether the import of the file [param import_file_path] describes runs a post-import script.
483
+ static func _shaped_by_a_script(import_file_path: String) -> bool:
484
+ var config: ConfigFile = ConfigFile.new()
485
+ if config.load(import_file_path) != OK:
486
+ return false
487
+ return not str(config.get_value("params", "import_script/path", "")).is_empty()
452
488
 
453
489
 
454
490
  ## The files an import wrote, from its sidecar's [code]dest_files[/code]; empty when it lists none.
package/build/index.js CHANGED
@@ -34566,6 +34566,7 @@ class GodotServer {
34566
34566
  const ghosts = blind.stillHeld ?? [];
34567
34567
  let waitedForEditorMs = 0;
34568
34568
  let first;
34569
+ let completedBefore;
34569
34570
  for (;; ) {
34570
34571
  const busySince = Date.now();
34571
34572
  let wasBusy = false;
@@ -34580,8 +34581,9 @@ class GodotServer {
34580
34581
  if (first.isError) {
34581
34582
  return first;
34582
34583
  }
34583
- const declined = asParams(JSON.parse(first.content[0]?.text ?? "{}"))["started"] === false;
34584
- if (!declined || Date.now() - started >= timeoutMs) {
34584
+ const firstAnswer = asParams(JSON.parse(first.content[0]?.text ?? "{}"));
34585
+ completedBefore = readNumber(firstAnswer, "scansCompletedBefore");
34586
+ if (firstAnswer["started"] !== false || Date.now() - started >= timeoutMs) {
34585
34587
  break;
34586
34588
  }
34587
34589
  await new Promise((settle) => setTimeout(settle, 100));
@@ -34592,7 +34594,9 @@ class GodotServer {
34592
34594
  await new Promise((settle) => setTimeout(settle, 100));
34593
34595
  const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
34594
34596
  const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
34595
- busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
34597
+ const completed = readNumber(status, "scansCompleted");
34598
+ const notYetOver = completedBefore !== undefined && completed !== undefined && completed <= completedBefore;
34599
+ busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending || notYetOver;
34596
34600
  if (busy) {
34597
34601
  idleSince = null;
34598
34602
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
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",