gdharness 1.1.1 → 1.1.2

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
@@ -15992,7 +15992,7 @@ var init_tool_definitions = __esm(() => {
15992
15992
  requires: ["projectPath"],
15993
15993
  operations: {
15994
15994
  status: {
15995
- summary: "which resources are outdated or failed, or one resource with resourcePath",
15995
+ summary: "which resources are outdated or failed, or one resource with resourcePath: a source changed since its import, an import whose output under .godot/imported is gone (missing_outputs), and a glTF scene imported before the images it uses (imported_before), each with its reason",
15996
15996
  requires: []
15997
15997
  },
15998
15998
  options: { summary: "the import options of one resource", requires: ["resourcePath"] },
@@ -380,12 +380,106 @@ func _import_status_of(resource_path: String, import_file_path: String) -> Dicti
380
380
  "source_exists": true
381
381
  }
382
382
 
383
- var source_modified: int = FileAccess.get_modified_time(resource_path)
384
- var import_modified: int = FileAccess.get_modified_time(import_file_path)
385
-
386
- return {
387
- "path": resource_path,
388
- "status": "needs_reimport" if source_modified > import_modified else "up_to_date",
389
- "import_file_exists": true,
390
- "source_exists": true
383
+ var status: Dictionary = {
384
+ "path": resource_path, "status": "up_to_date", "import_file_exists": true, "source_exists": true
391
385
  }
386
+ if FileAccess.get_modified_time(resource_path) > FileAccess.get_modified_time(import_file_path):
387
+ status["status"] = "needs_reimport"
388
+ status["reason"] = "the source changed after it was imported"
389
+ return status
390
+
391
+ # The sidecar alone does not say the import is there: deleting the outputs under
392
+ # .godot/imported is the ordinary way to force one, and a resource whose output is gone was
393
+ # answered as current, which is what a reimport is then skipped on.
394
+ var outputs: Array[String] = _outputs_of(import_file_path)
395
+ var missing: Array[String] = []
396
+ for output: String in outputs:
397
+ if not FileAccess.file_exists(output):
398
+ missing.append(output)
399
+ if not missing.is_empty():
400
+ status["status"] = "needs_reimport"
401
+ status["reason"] = "its imported output is not on disk"
402
+ status["missing_outputs"] = missing
403
+ return status
404
+
405
+ # A scene imported before the textures it uses were: it loads each image as it imports, so one
406
+ # imported ahead of them is built without them, and nothing about its own files says so. A second
407
+ # reimport started over one in progress did exactly that downstream, and 64 scenes that were
408
+ # untextured all read as up to date here.
409
+ var built_at: int = _oldest(outputs, import_file_path)
410
+ var before: Array[String] = []
411
+ for image: String in _images_of(resource_path):
412
+ if FileAccess.file_exists(image + ".import"):
413
+ if _newest(_outputs_of(image + ".import"), image + ".import") > built_at:
414
+ before.append(image)
415
+ if not before.is_empty():
416
+ status["status"] = "needs_reimport"
417
+ status["reason"] = "it was imported before the images it uses, so it was built without them"
418
+ status["imported_before"] = before
419
+ return status
420
+
421
+
422
+ ## The files an import wrote, from its sidecar's [code]dest_files[/code]; empty when it lists none.
423
+ static func _outputs_of(import_file_path: String) -> Array[String]:
424
+ var config: ConfigFile = ConfigFile.new()
425
+ var outputs: Array[String] = []
426
+ if config.load(import_file_path) != OK:
427
+ return outputs
428
+ var listed: Variant = config.get_value("deps", "dest_files", [])
429
+ if listed is Array:
430
+ for one: Variant in listed:
431
+ outputs.append(str(one))
432
+ return outputs
433
+
434
+
435
+ ## When the earliest of [param outputs] was written, or [param fallback] when none is on disk.
436
+ static func _oldest(outputs: Array[String], fallback: String) -> int:
437
+ var oldest: int = -1
438
+ for output: String in outputs:
439
+ if FileAccess.file_exists(output):
440
+ var at: int = FileAccess.get_modified_time(output)
441
+ oldest = at if oldest < 0 else mini(oldest, at)
442
+ return oldest if oldest >= 0 else FileAccess.get_modified_time(fallback)
443
+
444
+
445
+ ## When the latest of [param outputs] was written, or [param fallback] when none is on disk.
446
+ static func _newest(outputs: Array[String], fallback: String) -> int:
447
+ var newest: int = -1
448
+ for output: String in outputs:
449
+ if FileAccess.file_exists(output):
450
+ newest = maxi(newest, FileAccess.get_modified_time(output))
451
+ return newest if newest >= 0 else FileAccess.get_modified_time(fallback)
452
+
453
+
454
+ ## The image files a glTF scene refers to by path, resolved against the scene's own directory.
455
+ ## Embedded images and every other scene format answer nothing.
456
+ static func _images_of(scene_path: String) -> Array[String]:
457
+ var images: Array[String] = []
458
+ var text: String = ""
459
+ match scene_path.get_extension().to_lower():
460
+ "gltf":
461
+ text = FileAccess.get_file_as_string(scene_path)
462
+ "glb":
463
+ # A 12-byte header, then the first chunk: its length, its type, and for "JSON" the text.
464
+ var bytes: PackedByteArray = FileAccess.get_file_as_bytes(scene_path)
465
+ if bytes.size() >= 20 and bytes.decode_u32(16) == 0x4E4F534A:
466
+ var length: int = bytes.decode_u32(12)
467
+ text = bytes.slice(20, mini(20 + length, bytes.size())).get_string_from_utf8()
468
+ if text.is_empty():
469
+ return images
470
+ var parsed: Variant = JSON.parse_string(text)
471
+ if not parsed is Dictionary:
472
+ return images
473
+ var document: Dictionary = parsed
474
+ var listed: Variant = document.get("images", [])
475
+ if not listed is Array:
476
+ return images
477
+ for image: Variant in listed:
478
+ if not image is Dictionary:
479
+ continue
480
+ var entry: Dictionary = image
481
+ var uri: String = str(entry.get("uri", ""))
482
+ if uri.is_empty() or uri.begins_with("data:"):
483
+ continue
484
+ images.append(scene_path.get_base_dir().path_join(uri.uri_decode()).simplify_path())
485
+ return images
package/build/index.js CHANGED
@@ -30118,7 +30118,7 @@ var TOOL_SPECS = [
30118
30118
  requires: ["projectPath"],
30119
30119
  operations: {
30120
30120
  status: {
30121
- summary: "which resources are outdated or failed, or one resource with resourcePath",
30121
+ summary: "which resources are outdated or failed, or one resource with resourcePath: a source changed since its import, an import whose output under .godot/imported is gone (missing_outputs), and a glTF scene imported before the images it uses (imported_before), each with its reason",
30122
30122
  requires: []
30123
30123
  },
30124
30124
  options: { summary: "the import options of one resource", requires: ["resourcePath"] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
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",