gdharness 1.1.1 → 1.1.3
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"] },
|
|
@@ -41842,13 +41842,23 @@ class GodotServer {
|
|
|
41842
41842
|
if (!declined || Date.now() - started >= timeoutMs) {
|
|
41843
41843
|
break;
|
|
41844
41844
|
}
|
|
41845
|
+
await new Promise((settle) => setTimeout(settle, 100));
|
|
41845
41846
|
}
|
|
41846
41847
|
let busy = true;
|
|
41847
|
-
|
|
41848
|
+
let idleSince = null;
|
|
41849
|
+
while (Date.now() - started < timeoutMs) {
|
|
41848
41850
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
41849
41851
|
const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
|
|
41850
41852
|
const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
|
|
41851
41853
|
busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
|
|
41854
|
+
if (busy) {
|
|
41855
|
+
idleSince = null;
|
|
41856
|
+
} else {
|
|
41857
|
+
idleSince ??= Date.now();
|
|
41858
|
+
if (Date.now() - idleSince >= SCAN_SETTLED_MS) {
|
|
41859
|
+
break;
|
|
41860
|
+
}
|
|
41861
|
+
}
|
|
41852
41862
|
}
|
|
41853
41863
|
if (!busy && projectPath !== "" && (atRisk.length > 0 || ghosts.length > 0 || blind.unchecked !== undefined)) {
|
|
41854
41864
|
const until = Date.now() + CACHE_WRITE_MS;
|
|
@@ -42318,7 +42328,7 @@ async function runGodotServer() {
|
|
|
42318
42328
|
const server = new GodotServer;
|
|
42319
42329
|
await server.run();
|
|
42320
42330
|
}
|
|
42321
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
42331
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
42322
42332
|
var init_server3 = __esm(() => {
|
|
42323
42333
|
init_mcp();
|
|
42324
42334
|
init_stdio2();
|
|
@@ -11,12 +11,10 @@ var _editor_plugin: EditorPlugin = null
|
|
|
11
11
|
var _values: Serialisation = Serialisation.new()
|
|
12
12
|
var _properties: PropertyValues = PropertyValues.new()
|
|
13
13
|
|
|
14
|
-
## How many
|
|
15
|
-
##
|
|
16
|
-
##
|
|
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
17
|
var _scans_finished: int = 0
|
|
18
|
-
var _scans_finished_at_request: int = 0
|
|
19
|
-
var _scan_pending: bool = false
|
|
20
18
|
|
|
21
19
|
## When the last scan finished, on the engine's clock, or -1 before the first. The editor writes
|
|
22
20
|
## the class cache a frame after it stops reporting a scan, measured on 4.7.2, so a game started
|
|
@@ -36,7 +34,6 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
|
36
34
|
|
|
37
35
|
func _scan_finished() -> void:
|
|
38
36
|
_scans_finished += 1
|
|
39
|
-
_scan_pending = false
|
|
40
37
|
_scan_finished_at_msec = Time.get_ticks_msec()
|
|
41
38
|
|
|
42
39
|
|
|
@@ -662,31 +659,24 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
|
|
|
662
659
|
var busy: bool = filesystem.is_scanning() or filesystem.is_importing()
|
|
663
660
|
var started: bool = false
|
|
664
661
|
if not Read.as_bool(args.get("statusOnly", false)) and not busy:
|
|
665
|
-
|
|
666
|
-
_scan_pending = true
|
|
662
|
+
var finished_before: int = _scans_finished
|
|
667
663
|
filesystem.scan()
|
|
668
|
-
|
|
664
|
+
# Whether the scan ran, read off the editor rather than assumed. `scan()` returns without
|
|
665
|
+
# a word while the thread of the scan before is still to be joined, which is a frame or
|
|
666
|
+
# more after that scan stops reporting itself, and that scan then signals as it is joined.
|
|
667
|
+
# Taken as started, the answer came from a scan begun before the files were written:
|
|
668
|
+
# downstream, a rescan after 698 files were rewritten answered finished in 1.5s and the
|
|
669
|
+
# one changed file stayed on its old import. A threaded scan reports itself from inside
|
|
670
|
+
# 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
|
|
669
672
|
|
|
670
673
|
# Importing is reported separately from scanning, and a class is not registered until
|
|
671
674
|
# both are done, so a caller watching only one of them can look too early.
|
|
672
|
-
#
|
|
673
|
-
# `pending` is the third state, and it is the one that was missing. A scan is queued rather
|
|
674
|
-
# than run, so for the first frames after asking, both flags are false and the editor has not
|
|
675
|
-
# started: a caller polling them reads "finished" off a scan that has not begun. Measured, a
|
|
676
|
-
# headless editor of 400 scripts takes 1619ms and answered honestly, while a downstream editor
|
|
677
|
-
# of 376 classes answered finished 296ms in and then wrote its own stale class list over the
|
|
678
|
-
# cache that had just been corrected. Whoever reads this is waiting for the scan to have
|
|
679
|
-
# landed, so "not yet" and "over" must not be spelled the same.
|
|
680
|
-
var scanning: bool = filesystem.is_scanning()
|
|
681
|
-
var importing: bool = filesystem.is_importing()
|
|
682
|
-
if scanning or importing or _scans_finished != _scans_finished_at_request:
|
|
683
|
-
_scan_pending = false
|
|
684
675
|
return {
|
|
685
676
|
"ok": true,
|
|
686
677
|
"started": started,
|
|
687
|
-
"scanning":
|
|
688
|
-
"importing":
|
|
689
|
-
"pending": _scan_pending,
|
|
678
|
+
"scanning": filesystem.is_scanning(),
|
|
679
|
+
"importing": filesystem.is_importing(),
|
|
690
680
|
"scansFinished": _scans_finished,
|
|
691
681
|
}
|
|
692
682
|
|
|
@@ -704,7 +694,6 @@ func scan_status(_args: Dictionary) -> Dictionary:
|
|
|
704
694
|
"ok": true,
|
|
705
695
|
"scanning": filesystem.is_scanning(),
|
|
706
696
|
"importing": filesystem.is_importing(),
|
|
707
|
-
"pending": _scan_pending,
|
|
708
697
|
"sinceScanFinishedMs":
|
|
709
698
|
-1 if _scan_finished_at_msec < 0 else Time.get_ticks_msec() - _scan_finished_at_msec,
|
|
710
699
|
}
|
|
@@ -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
|
|
384
|
-
|
|
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"] },
|
|
@@ -31475,6 +31475,7 @@ function scanWaitAnswer(scanned) {
|
|
|
31475
31475
|
}
|
|
31476
31476
|
var PLAYING_STATUS_MS = 1000;
|
|
31477
31477
|
var SCAN_START_MS = 2000;
|
|
31478
|
+
var SCAN_SETTLED_MS = 500;
|
|
31478
31479
|
var WAIT_FOR_RUN_MS = 600000;
|
|
31479
31480
|
var RUN_POLL_MS = 250;
|
|
31480
31481
|
var PATH_SOLUTIONS = [
|
|
@@ -34579,13 +34580,23 @@ class GodotServer {
|
|
|
34579
34580
|
if (!declined || Date.now() - started >= timeoutMs) {
|
|
34580
34581
|
break;
|
|
34581
34582
|
}
|
|
34583
|
+
await new Promise((settle) => setTimeout(settle, 100));
|
|
34582
34584
|
}
|
|
34583
34585
|
let busy = true;
|
|
34584
|
-
|
|
34586
|
+
let idleSince = null;
|
|
34587
|
+
while (Date.now() - started < timeoutMs) {
|
|
34585
34588
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
34586
34589
|
const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
|
|
34587
34590
|
const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
|
|
34588
34591
|
busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
|
|
34592
|
+
if (busy) {
|
|
34593
|
+
idleSince = null;
|
|
34594
|
+
} else {
|
|
34595
|
+
idleSince ??= Date.now();
|
|
34596
|
+
if (Date.now() - idleSince >= SCAN_SETTLED_MS) {
|
|
34597
|
+
break;
|
|
34598
|
+
}
|
|
34599
|
+
}
|
|
34589
34600
|
}
|
|
34590
34601
|
if (!busy && projectPath !== "" && (atRisk.length > 0 || ghosts.length > 0 || blind.unchecked !== undefined)) {
|
|
34591
34602
|
const until = Date.now() + CACHE_WRITE_MS;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
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",
|