gdharness 1.3.43 → 1.3.44
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 +321 -234
- package/build/godot/addons/gdharness_editor/reading.gd +14 -5
- package/build/godot/addons/gdharness_editor/scene_file.gd +18 -1
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +31 -12
- package/build/godot/addons/gdharness_runtime/reading.gd +14 -5
- package/build/godot/operations/dependencies.gd +51 -16
- package/build/godot/operations/editor_tools.gd +5 -4
- package/build/godot/operations/project_config.gd +8 -3
- package/build/godot/operations/reading.gd +14 -5
- package/build/keeper.js +43 -43
- package/package.json +1 -1
|
@@ -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:
|
|
316
|
-
##
|
|
317
|
-
##
|
|
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".
|
|
@@ -526,10 +526,27 @@ func unusable_name(parent: Node, child_name: String, moving: Node = null) -> Str
|
|
|
526
526
|
if taken != null and taken != moving and taken.get_parent() == parent:
|
|
527
527
|
# The root by its name, since its path is ".", and ". already has a child" read as a typo.
|
|
528
528
|
var holder: String = "The root, %s," % parent.name if parent == root else path_of(parent)
|
|
529
|
-
return
|
|
529
|
+
return (
|
|
530
|
+
"%s already has a child named %s; %s is free there."
|
|
531
|
+
% [holder, child_name, _free_name(parent, child_name)]
|
|
532
|
+
)
|
|
530
533
|
return ""
|
|
531
534
|
|
|
532
535
|
|
|
536
|
+
## The first name after [param taken] that no child of [param parent] has, numbered as Godot numbers
|
|
537
|
+
## a duplicate: Crate2 after Crate, Crate4 after Crate3 when that is taken too. The refusal alone
|
|
538
|
+
## left a caller guessing at a name that might be taken as well.
|
|
539
|
+
static func _free_name(parent: Node, taken: String) -> String:
|
|
540
|
+
var digits: int = 0
|
|
541
|
+
while digits < taken.length() and taken[taken.length() - 1 - digits] in "0123456789":
|
|
542
|
+
digits += 1
|
|
543
|
+
var base: String = taken.left(taken.length() - digits)
|
|
544
|
+
var at: int = 2 if digits == 0 else taken.right(digits).to_int() + 1
|
|
545
|
+
while parent.has_node(NodePath(base + str(at))):
|
|
546
|
+
at += 1
|
|
547
|
+
return base + str(at)
|
|
548
|
+
|
|
549
|
+
|
|
533
550
|
## The properties this scene's own file sets on [param node], each with the value the file gives it.
|
|
534
551
|
func set_here(node: Node) -> Dictionary:
|
|
535
552
|
return set_at(path_of(node))
|
|
@@ -51,7 +51,7 @@ func reload_script(args: Dictionary) -> Dictionary:
|
|
|
51
51
|
var asked: String = str(args.get("scriptPath", ""))
|
|
52
52
|
if asked.is_empty():
|
|
53
53
|
return { "ok": false, "error": "scriptPath is required" }
|
|
54
|
-
var path: String = _spelled_as_on_disk(asked)
|
|
54
|
+
var path: String = _spelled_as_on_disk(asked, {})
|
|
55
55
|
if not ResourceLoader.exists(path):
|
|
56
56
|
return { "ok": false, "error": "No script at " + path }
|
|
57
57
|
|
|
@@ -151,8 +151,9 @@ func scripts_behind(args: Dictionary) -> Dictionary:
|
|
|
151
151
|
unsaved = EditorInterface.get_script_editor().get_unsaved_files()
|
|
152
152
|
var behind: Array[String] = []
|
|
153
153
|
var edited: Array[String] = []
|
|
154
|
+
var listings: Dictionary = {}
|
|
154
155
|
for entry: Variant in asked:
|
|
155
|
-
var path: String = _spelled_as_on_disk(str(entry))
|
|
156
|
+
var path: String = _spelled_as_on_disk(str(entry), listings)
|
|
156
157
|
var script: GDScript = ResourceLoader.get_cached_ref(path) as GDScript
|
|
157
158
|
if script == null:
|
|
158
159
|
continue
|
|
@@ -196,28 +197,46 @@ func _reread_open_tabs(path: String, answer: Dictionary) -> void:
|
|
|
196
197
|
## `res://core/rules.gd`, but the editor holds its copy under the spelling on disk, and a load by
|
|
197
198
|
## another spelling misses that copy and compiles a new one: reloading that answers with a fresh
|
|
198
199
|
## compile's members and leaves the copy the editor holds as it was.
|
|
199
|
-
|
|
200
|
+
##
|
|
201
|
+
## [param listings] keeps each directory's entries for the length of one call that spells many
|
|
202
|
+
## paths. Listed again for every path, a check of the 416 classes of one project listed a folder of
|
|
203
|
+
## eight hundred files twice per class, and held an editor's main thread for close to a minute on a
|
|
204
|
+
## CI runner, with every request behind it waiting.
|
|
205
|
+
static func _spelled_as_on_disk(path: String, listings: Dictionary) -> String:
|
|
200
206
|
var local: String = ProjectSettings.localize_path(path)
|
|
201
207
|
if not local.begins_with("res://"):
|
|
202
208
|
return local
|
|
203
209
|
var spelled: String = "res://"
|
|
204
210
|
for step: String in local.trim_prefix("res://").split("/", false):
|
|
205
|
-
|
|
206
|
-
|
|
211
|
+
if not listings.has(spelled):
|
|
212
|
+
listings[spelled] = _entries_by_spelling(spelled)
|
|
213
|
+
var entries: Variant = listings[spelled]
|
|
214
|
+
if entries == null:
|
|
207
215
|
return local
|
|
208
|
-
var
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
found = entry
|
|
212
|
-
break
|
|
213
|
-
if found.is_empty() and entry.nocasecmp_to(step) == 0:
|
|
214
|
-
found = entry
|
|
216
|
+
var exact: Dictionary = entries[0]
|
|
217
|
+
var folded: Dictionary = entries[1]
|
|
218
|
+
var found: String = step if exact.has(step) else str(folded.get(step.to_lower(), ""))
|
|
215
219
|
if found.is_empty():
|
|
216
220
|
return local
|
|
217
221
|
spelled = spelled.path_join(found)
|
|
218
222
|
return spelled
|
|
219
223
|
|
|
220
224
|
|
|
225
|
+
## The entries of [param directory] by their own spelling and by their spelling in lower case, the
|
|
226
|
+
## first of those that fold together winning, or null for a directory that cannot be opened.
|
|
227
|
+
static func _entries_by_spelling(directory: String) -> Variant:
|
|
228
|
+
var listing: DirAccess = DirAccess.open(directory)
|
|
229
|
+
if listing == null:
|
|
230
|
+
return null
|
|
231
|
+
var exact: Dictionary = {}
|
|
232
|
+
var folded: Dictionary = {}
|
|
233
|
+
for entry: String in listing.get_directories() + listing.get_files():
|
|
234
|
+
exact[entry] = true
|
|
235
|
+
if not folded.has(entry.to_lower()):
|
|
236
|
+
folded[entry.to_lower()] = entry
|
|
237
|
+
return [exact, folded]
|
|
238
|
+
|
|
239
|
+
|
|
221
240
|
func _method_names(script: GDScript) -> Array[String]:
|
|
222
241
|
var names: Array[String] = []
|
|
223
242
|
for entry: Dictionary in script.get_script_method_list():
|
|
@@ -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:
|
|
316
|
-
##
|
|
317
|
-
##
|
|
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".
|
|
@@ -36,6 +36,8 @@ var _files: GdhFileWalk = GdhFileWalk.new()
|
|
|
36
36
|
## script again made the whole-project answer quadratic in the number of scripts.
|
|
37
37
|
var _classes_declared: Dictionary = {}
|
|
38
38
|
var _classes_read: bool = false
|
|
39
|
+
var _uids_declared: Dictionary = {}
|
|
40
|
+
var _uids_read: bool = false
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
func _init(p_log: GdhLog) -> void:
|
|
@@ -709,7 +711,7 @@ func _depend(
|
|
|
709
711
|
|
|
710
712
|
## Every autoload the project registers as a global name, with its script. One that is not global
|
|
711
713
|
## is no name code can use.
|
|
712
|
-
|
|
714
|
+
func autoloads_declared() -> Dictionary:
|
|
713
715
|
var autoloads: Dictionary = {}
|
|
714
716
|
for property: Dictionary in ProjectSettings.get_property_list():
|
|
715
717
|
var setting: String = str(property.get("name", ""))
|
|
@@ -762,27 +764,63 @@ static func classes_used_in(content: String, classes: Dictionary) -> Array[Strin
|
|
|
762
764
|
return used
|
|
763
765
|
|
|
764
766
|
|
|
765
|
-
# [param written] as the res:// path the engine would load from [param from]: a uid:// by
|
|
766
|
-
#
|
|
767
|
-
#
|
|
768
|
-
|
|
769
|
-
static func _resolved(written: String, from: String) -> String:
|
|
767
|
+
# [param written] as the res:// path the engine would load from [param from]: a uid:// by the file
|
|
768
|
+
# it names, and a relative path against the referring file's directory. Both were passed over as not
|
|
769
|
+
# being res:// paths, so a script's `preload("sibling.gd")` was no dependency at all.
|
|
770
|
+
func _resolved(written: String, from: String) -> String:
|
|
770
771
|
if written.begins_with("res://"):
|
|
771
772
|
return written
|
|
772
773
|
if written.begins_with("uid://"):
|
|
773
|
-
var
|
|
774
|
-
return
|
|
774
|
+
var found: String = _uid_path(written)
|
|
775
|
+
return written if found.is_empty() else found
|
|
775
776
|
if written.contains("://"):
|
|
776
777
|
return written
|
|
777
778
|
return from.get_base_dir().path_join(written).simplify_path()
|
|
778
779
|
|
|
779
780
|
|
|
781
|
+
# The file the uid:// [param text] names, or empty when nothing does: the engine's uid cache first,
|
|
782
|
+
# then the uids the project's files declare. Only an import writes the cache, so on a fresh clone
|
|
783
|
+
# every uid was answered as naming no file, and a scene moved outside the editor was missing under
|
|
784
|
+
# its old path, although the first import loads it by its uid. A cached path with no file there
|
|
785
|
+
# gives way to a file declaring the uid, and is the answer when none does.
|
|
786
|
+
func _uid_path(text: String) -> String:
|
|
787
|
+
var id: int = ResourceUID.text_to_id(text)
|
|
788
|
+
if id == ResourceUID.INVALID_ID:
|
|
789
|
+
return ""
|
|
790
|
+
var cached: String = ResourceUID.get_id_path(id) if ResourceUID.has_id(id) else ""
|
|
791
|
+
if FileAccess.file_exists(cached):
|
|
792
|
+
return cached
|
|
793
|
+
return str(_files_by_uid().get(id, cached))
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
# Every file of the project that declares a uid, by its uid's id: in a .uid sidecar, an import
|
|
797
|
+
# sidecar or the header of a text resource, as GdhResourceFiles reads them. Read once for the call,
|
|
798
|
+
# the first time the cache does not know a uid.
|
|
799
|
+
func _files_by_uid() -> Dictionary:
|
|
800
|
+
if _uids_read:
|
|
801
|
+
return _uids_declared
|
|
802
|
+
_uids_read = true
|
|
803
|
+
var declaring: Dictionary = {}
|
|
804
|
+
for file: String in _files.find_files_with_extensions(
|
|
805
|
+
"res://", ["uid", "import"] + GdhResourceFiles.TEXT_RESOURCE_KINDS
|
|
806
|
+
):
|
|
807
|
+
var sidecar: bool = file.get_extension() in ["uid", "import"]
|
|
808
|
+
declaring[file.get_basename() if sidecar else file] = true
|
|
809
|
+
for file: String in declaring:
|
|
810
|
+
if not FileAccess.file_exists(file):
|
|
811
|
+
continue
|
|
812
|
+
var id: int = ResourceUID.text_to_id(str(GdhResourceFiles.uid_of(file).get("uid", "")))
|
|
813
|
+
if id != ResourceUID.INVALID_ID:
|
|
814
|
+
_uids_declared[id] = file
|
|
815
|
+
return _uids_declared
|
|
816
|
+
|
|
817
|
+
|
|
780
818
|
# The ext_resource paths in [param content] whose uid leads elsewhere, each with the file the uid
|
|
781
819
|
# names. The engine loads an ext_resource by its uid whenever the uid is known, whatever the path
|
|
782
820
|
# says, so a file moved outside the editor still loads, and the walk called it missing; every
|
|
783
821
|
# pattern reads the old path, the bare res:// one included, so the paths are mapped before any of
|
|
784
822
|
# them runs.
|
|
785
|
-
|
|
823
|
+
func _moved_by_uid(content: String) -> Dictionary:
|
|
786
824
|
var moved: Dictionary = {}
|
|
787
825
|
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
|
|
788
826
|
var found: String = _loaded_by_uid(line.get_string())
|
|
@@ -795,7 +833,7 @@ static func _moved_by_uid(content: String) -> Dictionary:
|
|
|
795
833
|
# The ext_resource paths in [param content], read from [param from], whose uid names no file the
|
|
796
834
|
# project has, each with that uid. The engine warns that the uid is invalid and loads the path, and
|
|
797
835
|
# the walk followed the path saying nothing, so an export was the first to mention it.
|
|
798
|
-
|
|
836
|
+
func _unknown_uids(content: String, from: String) -> Dictionary:
|
|
799
837
|
var unknown: Dictionary = {}
|
|
800
838
|
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
|
|
801
839
|
var uid: String = _unknown_uid_of(line.get_string())
|
|
@@ -806,21 +844,18 @@ static func _unknown_uids(content: String, from: String) -> Dictionary:
|
|
|
806
844
|
|
|
807
845
|
|
|
808
846
|
# The uid an ext_resource [param entry] carries when it names no file the project has, or empty.
|
|
809
|
-
|
|
847
|
+
func _unknown_uid_of(entry: String) -> String:
|
|
810
848
|
var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
|
|
811
849
|
return "" if uid == null or not _loaded_by_uid(entry).is_empty() else uid.get_string(1)
|
|
812
850
|
|
|
813
851
|
|
|
814
852
|
# The file an ext_resource [param entry] is loaded from by its uid, or empty when it carries none
|
|
815
853
|
# the project knows, and the engine falls back to the path.
|
|
816
|
-
|
|
854
|
+
func _loaded_by_uid(entry: String) -> String:
|
|
817
855
|
var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
|
|
818
856
|
if uid == null:
|
|
819
857
|
return ""
|
|
820
|
-
var
|
|
821
|
-
if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id):
|
|
822
|
-
return ""
|
|
823
|
-
var found: String = ResourceUID.get_id_path(id)
|
|
858
|
+
var found: String = _uid_path(uid.get_string(1))
|
|
824
859
|
return found if FileAccess.file_exists(found) else ""
|
|
825
860
|
|
|
826
861
|
|
|
@@ -85,15 +85,16 @@ static func run(params: Dictionary) -> Dictionary:
|
|
|
85
85
|
answered["namedByPath"] = (
|
|
86
86
|
(
|
|
87
87
|
"What this saved names %s by path rather than by uid, which loads the same. A "
|
|
88
|
-
+ "
|
|
89
|
-
+ "
|
|
90
|
-
+ "
|
|
91
|
-
+ "
|
|
88
|
+
+ "reference by uid is resolved through the project's uid cache, which holds "
|
|
89
|
+
+ "none for %s yet, as with a file made since the project was last scanned, so "
|
|
90
|
+
+ "naming %s by uid now would warn on every load. project_import refresh_uids "
|
|
91
|
+
+ "scans the project, and writes after it name %s by uid."
|
|
92
92
|
)
|
|
93
93
|
% [
|
|
94
94
|
", ".join(GdhSceneUids.by_path),
|
|
95
95
|
"it" if GdhSceneUids.by_path.size() == 1 else "them",
|
|
96
96
|
"it" if GdhSceneUids.by_path.size() == 1 else "them",
|
|
97
|
+
"it" if GdhSceneUids.by_path.size() == 1 else "them",
|
|
97
98
|
]
|
|
98
99
|
)
|
|
99
100
|
return answered
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
@@ -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:
|
|
316
|
-
##
|
|
317
|
-
##
|
|
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".
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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.
|
|
3
|
+
"version": "1.3.44",
|
|
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",
|