gdharness 1.3.39 → 1.3.41
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 +315 -112
- package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -6
- package/build/godot/addons/gdharness_editor/file_reads.gd +2 -1
- package/build/godot/addons/gdharness_editor/file_walk.gd +33 -1
- package/build/godot/addons/gdharness_editor/property_values.gd +6 -9
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +6 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +1 -1
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +11 -10
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +77 -22
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +5 -3
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +15 -8
- package/build/godot/operations/animation_reads.gd +7 -6
- package/build/godot/operations/dependencies.gd +5 -2
- package/build/godot/operations/file_reads.gd +2 -1
- package/build/godot/operations/file_walk.gd +33 -1
- package/build/godot/operations/gdscript_authoring.gd +10 -1
- package/build/godot/operations/import_pipeline.gd +1 -1
- package/build/godot/operations/plugins.gd +39 -1
- package/build/godot/operations/project_config.gd +11 -1
- package/build/godot/operations/property_values.gd +6 -9
- package/build/keeper.js +38 -5
- package/package.json +1 -1
|
@@ -125,15 +125,16 @@ static func no_animation(player: AnimationPlayer, called: String, animation_name
|
|
|
125
125
|
|
|
126
126
|
static func _summary(anim: Animation) -> Dictionary:
|
|
127
127
|
return {
|
|
128
|
-
"length": anim.length,
|
|
128
|
+
"length": held_seconds(anim.length),
|
|
129
129
|
"loopMode": LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
|
|
130
|
-
"step": anim.step,
|
|
130
|
+
"step": held_seconds(anim.step),
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
|
|
134
|
-
## [param seconds], a key's time, as the shortest decimal holding
|
|
135
|
-
##
|
|
136
|
-
|
|
134
|
+
## [param seconds], a key's time or an animation's length or step, as the shortest decimal holding
|
|
135
|
+
## the same 32-bit float. An animation keeps all three as 32-bit floats, so a key put at 0.7 read
|
|
136
|
+
## back as 0.699999988079071 and a step of 0.1 as 0.100000001490116.
|
|
137
|
+
static func held_seconds(seconds: float) -> float:
|
|
137
138
|
var held: float = PackedFloat32Array([seconds])[0]
|
|
138
139
|
for places: int in range(0, 10):
|
|
139
140
|
var shorter: float = String.num(held, places).to_float()
|
|
@@ -159,7 +160,7 @@ static func _track(anim: Animation, track: int, values: GdhSerialisation) -> Dic
|
|
|
159
160
|
var keyframes: Array[Dictionary] = []
|
|
160
161
|
var methods: Dictionary = {}
|
|
161
162
|
for key: int in anim.track_get_key_count(track):
|
|
162
|
-
var keyframe: Dictionary = {"time":
|
|
163
|
+
var keyframe: Dictionary = {"time": held_seconds(anim.track_get_key_time(track, key))}
|
|
163
164
|
if kind == Animation.TYPE_METHOD:
|
|
164
165
|
var method: String = str(anim.method_track_get_name(track, key))
|
|
165
166
|
methods[method] = true
|
|
@@ -11,6 +11,7 @@ extends RefCounted
|
|
|
11
11
|
## initialisers run, and a resource with a script is made with it.
|
|
12
12
|
|
|
13
13
|
const GdhRead = preload("reading.gd")
|
|
14
|
+
const GdhFileWalk = preload("file_walk.gd")
|
|
14
15
|
const GdhSame = preload("same_values.gd")
|
|
15
16
|
const GdhSerialisation = preload("serialisation.gd")
|
|
16
17
|
const GdhShapes = preload("value_shapes.gd")
|
|
@@ -487,7 +488,7 @@ static func _stored(type: String, given: Dictionary, include_defaults: bool) ->
|
|
|
487
488
|
static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
488
489
|
var file: String = res_path.get_slice("::", 0)
|
|
489
490
|
if not FileAccess.file_exists(file):
|
|
490
|
-
return {"ok": false, "error": "No resource at " + file}
|
|
491
|
+
return {"ok": false, "error": GdhFileWalk.new().with_nearest_files("No resource at " + file, file)}
|
|
491
492
|
var whole: Resource = ResourceLoader.load(file, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
492
493
|
if whole == null:
|
|
493
494
|
return {"ok": false, "error": "%s did not load as a resource." % file}
|
|
@@ -35,8 +35,24 @@ static func missing_reason(path: String) -> String:
|
|
|
35
35
|
|
|
36
36
|
## [param said], the refusal for [param path], a res:// path nothing is at, with the files of its
|
|
37
37
|
## extension whose names are nearest its own, or every one when there are few and none is near, so
|
|
38
|
-
## the caller has a path to send instead.
|
|
38
|
+
## the caller has a path to send instead. Files of the same name beside it come first, since a
|
|
39
|
+
## misspelt extension has no file of its own extension to be near.
|
|
39
40
|
func with_nearest_files(said: String, path: String) -> String:
|
|
41
|
+
# One the refusal names is the file it refuses, as a script given for a scene is. Word by word,
|
|
42
|
+
# since res://brain.gd is inside res://brain.gdd.
|
|
43
|
+
var named: Array[String] = []
|
|
44
|
+
for word: String in said.replace("\n", " ").replace(",", " ").replace(";", " ").split(" ", false):
|
|
45
|
+
named.append(word.rstrip(".:"))
|
|
46
|
+
var namesakes: Array[String] = []
|
|
47
|
+
for file: String in _namesakes(path):
|
|
48
|
+
if not file in named:
|
|
49
|
+
namesakes.append(file)
|
|
50
|
+
if not namesakes.is_empty():
|
|
51
|
+
var alone: bool = namesakes.size() == 1
|
|
52
|
+
return (
|
|
53
|
+
"%s. The %s of that name beside it %s %s."
|
|
54
|
+
% [said, "file" if alone else "files", "is" if alone else "are", ", ".join(namesakes)]
|
|
55
|
+
)
|
|
40
56
|
var extension: String = path.get_extension()
|
|
41
57
|
if extension.is_empty():
|
|
42
58
|
return said
|
|
@@ -65,6 +81,22 @@ func with_nearest_files(said: String, path: String) -> String:
|
|
|
65
81
|
return "%s. None of the project's %d .%s files is named like it." % [said, files.size(), extension]
|
|
66
82
|
|
|
67
83
|
|
|
84
|
+
# The files in [param path]'s folder named as it is with another extension, leaving out the import
|
|
85
|
+
# and uid files the engine keeps beside a file, which are named after the whole file name.
|
|
86
|
+
static func _namesakes(path: String) -> Array[String]:
|
|
87
|
+
var found: Array[String] = []
|
|
88
|
+
var folder: String = path.get_base_dir()
|
|
89
|
+
if not DirAccess.dir_exists_absolute(folder):
|
|
90
|
+
return found
|
|
91
|
+
var stem: String = path.get_file().get_basename()
|
|
92
|
+
var files: PackedStringArray = DirAccess.get_files_at(folder)
|
|
93
|
+
files.sort()
|
|
94
|
+
for file: String in files:
|
|
95
|
+
if file.get_basename() == stem and file != path.get_file():
|
|
96
|
+
found.append(folder.path_join(file))
|
|
97
|
+
return found
|
|
98
|
+
|
|
99
|
+
|
|
68
100
|
# A directory the engine steps over, marker file and all.
|
|
69
101
|
func is_stepped_over(path: String) -> bool:
|
|
70
102
|
return FileAccess.file_exists(path + ".gdignore")
|
|
@@ -383,19 +383,16 @@ func _readable(value: Variant, relative_to: Node) -> Variant:
|
|
|
383
383
|
return _values.serialize_value(value)
|
|
384
384
|
|
|
385
385
|
|
|
386
|
-
## What [param target] stores, by name, but its script and the properties in [param leave]
|
|
387
|
-
##
|
|
386
|
+
## What [param target] stores, by name, but its script and the properties in [param leave], in the
|
|
387
|
+
## names a read answers with: a Curve's points as point_0/position and the rest rather than the _data
|
|
388
|
+
## list every point write changes. A list or a map is copied, since a setter can change the one it
|
|
389
|
+
## holds in place.
|
|
388
390
|
func _stored(target: Object, leave: Dictionary) -> Dictionary:
|
|
389
391
|
var held: Dictionary = {}
|
|
390
|
-
for entry: Dictionary in
|
|
392
|
+
for entry: Dictionary in GdhShapes.read_properties(target):
|
|
391
393
|
var name: String = str(entry["name"])
|
|
392
394
|
var usage: int = GdhRead.as_int(entry["usage"])
|
|
393
|
-
if (
|
|
394
|
-
usage & PROPERTY_USAGE_STORAGE == 0
|
|
395
|
-
or usage & HEADINGS != 0
|
|
396
|
-
or name == "script"
|
|
397
|
-
or leave.has(name)
|
|
398
|
-
):
|
|
395
|
+
if usage & HEADINGS != 0 or name == "script" or leave.has(name):
|
|
399
396
|
continue
|
|
400
397
|
var value: Variant = target.get(name)
|
|
401
398
|
if value is Array:
|
|
@@ -320,7 +320,10 @@ func create_animation(args: Dictionary) -> Dictionary:
|
|
|
320
320
|
return "%s would not be in the scene when it loads." % animation_name
|
|
321
321
|
return ""
|
|
322
322
|
var answer: Dictionary = {
|
|
323
|
-
"ok": true,
|
|
323
|
+
"ok": true,
|
|
324
|
+
"animationName": animation_name,
|
|
325
|
+
"length": GdhAnimationReads.held_seconds(anim.length),
|
|
326
|
+
"loopMode": loop_mode_name
|
|
324
327
|
}
|
|
325
328
|
if library[2]:
|
|
326
329
|
answer["libraryMade"] = animation_name.get_slice("/", 0) if animation_name.contains("/") else ""
|
|
@@ -477,9 +480,9 @@ func set_animation(args: Dictionary) -> Dictionary:
|
|
|
477
480
|
var answer: Dictionary = {
|
|
478
481
|
"ok": true,
|
|
479
482
|
"animationName": animation_name,
|
|
480
|
-
"length": anim.length,
|
|
483
|
+
"length": GdhAnimationReads.held_seconds(anim.length),
|
|
481
484
|
"loopMode": GdhAnimationReads.LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
|
|
482
|
-
"step": anim.step,
|
|
485
|
+
"step": GdhAnimationReads.held_seconds(anim.step),
|
|
483
486
|
}
|
|
484
487
|
var past: String = _past_length(anim, animation_name)
|
|
485
488
|
if not past.is_empty():
|
|
@@ -444,7 +444,7 @@ static func _keys_at(anim: Animation, calling: Array, times: Array) -> Array:
|
|
|
444
444
|
if pair[1] >= 0 and key != pair[1]:
|
|
445
445
|
continue
|
|
446
446
|
var time: float = anim.track_get_key_time(index, key)
|
|
447
|
-
var _listed: bool = held.append(str(GdhAnimationReads.
|
|
447
|
+
var _listed: bool = held.append(str(GdhAnimationReads.held_seconds(time)))
|
|
448
448
|
for given: float in times:
|
|
449
449
|
if GdhAnimationReads.same_time(time, given):
|
|
450
450
|
at.append([index, key])
|
|
@@ -142,7 +142,6 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
|
|
|
142
142
|
|
|
143
143
|
if scene_path.is_empty():
|
|
144
144
|
return {"ok": false, "error": "Missing scenePath"}
|
|
145
|
-
var asked_type: String = root_node_type
|
|
146
145
|
var made: Dictionary = GdhSceneFile.made_as(
|
|
147
146
|
root_node_type,
|
|
148
147
|
"" if script_path.is_empty() else GdhSceneFile.res_path(project_path, script_path),
|
|
@@ -200,13 +199,20 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
|
|
|
200
199
|
if not folder.is_empty():
|
|
201
200
|
root.free()
|
|
202
201
|
return {"ok": false, "error": folder}
|
|
202
|
+
# Read before the write, which frees the root.
|
|
203
|
+
var root_class: Dictionary = GdhSceneFile.class_of(root)
|
|
203
204
|
var written: Dictionary = scene.write()
|
|
204
205
|
if not written.is_empty():
|
|
205
206
|
return written
|
|
206
207
|
|
|
207
208
|
var answer: Dictionary = {
|
|
208
|
-
"ok": true,
|
|
209
|
+
"ok": true,
|
|
210
|
+
"scenePath": scene_path,
|
|
211
|
+
"rootNodeType": root_class["nodeType"],
|
|
212
|
+
"rootNodeName": root_node_name
|
|
209
213
|
}
|
|
214
|
+
if root_class.has("scriptClass"):
|
|
215
|
+
answer["scriptClass"] = root_class["scriptClass"]
|
|
210
216
|
if not script_path.is_empty():
|
|
211
217
|
answer["script"] = script_path
|
|
212
218
|
if not had_folder:
|
|
@@ -241,7 +247,6 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
241
247
|
|
|
242
248
|
if node_name.is_empty():
|
|
243
249
|
return {"ok": false, "error": "Missing nodeName"}
|
|
244
|
-
var asked_type: String = node_type
|
|
245
250
|
var class_script: Script = null
|
|
246
251
|
if not node_type.is_empty() and not ClassDB.class_exists(node_type):
|
|
247
252
|
var declared: Dictionary = GdhSceneFile.script_class(node_type, "Node")
|
|
@@ -301,13 +306,9 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
301
306
|
|
|
302
307
|
var new_path: String = scene.path_of(new_node)
|
|
303
308
|
# Read before the write, which frees the scene it was added to.
|
|
304
|
-
var answer: Dictionary = {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
"nodeType": asked_type if class_script != null else new_node.get_class(),
|
|
308
|
-
"nodePath": new_path,
|
|
309
|
-
"index": new_node.get_index(),
|
|
310
|
-
}
|
|
309
|
+
var answer: Dictionary = {"ok": true, "nodeName": node_name}
|
|
310
|
+
answer.merge(GdhSceneFile.class_of(new_node))
|
|
311
|
+
answer.merge({"nodePath": new_path, "index": new_node.get_index()})
|
|
311
312
|
if not properties.is_empty():
|
|
312
313
|
var held: Dictionary = {}
|
|
313
314
|
for key: Variant in properties:
|
|
@@ -10,6 +10,9 @@ const GdhSerialisation = preload("../serialisation.gd")
|
|
|
10
10
|
|
|
11
11
|
## What every emission meets when a connection cannot call its method.
|
|
12
12
|
const FAILS: String = ", so every emission fails to call it."
|
|
13
|
+
## The engine's CONNECT_INHERITED, which GDScript is not given: on a connection an instanced or
|
|
14
|
+
## inherited scene makes, as a scene holding one loads it.
|
|
15
|
+
const CONNECT_INHERITED: int = 32
|
|
13
16
|
## The names Object.ConnectFlags gives each bit. The number alone left a reader guessing that 2 is
|
|
14
17
|
## the persistence every connection in a saved scene carries.
|
|
15
18
|
const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
|
|
@@ -18,9 +21,7 @@ const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
|
|
|
18
21
|
Object.CONNECT_ONE_SHOT: "one_shot",
|
|
19
22
|
Object.CONNECT_REFERENCE_COUNTED: "reference_counted",
|
|
20
23
|
Object.CONNECT_APPEND_SOURCE_OBJECT: "append_source_object",
|
|
21
|
-
|
|
22
|
-
# makes inside itself, as a scene holding one loads it.
|
|
23
|
-
32: "inherited",
|
|
24
|
+
CONNECT_INHERITED: "inherited",
|
|
24
25
|
}
|
|
25
26
|
## The flags a caller may give: the engine's own CONNECT_INHERITED marks a connection an instanced
|
|
26
27
|
## scene makes, and is never one a scene makes for itself.
|
|
@@ -295,7 +296,9 @@ static func _misfit(source: Node, signal_name: String, callable: Callable, flags
|
|
|
295
296
|
break
|
|
296
297
|
if flags & Object.CONNECT_APPEND_SOURCE_OBJECT:
|
|
297
298
|
passed.append({"name": "source", "type": TYPE_OBJECT, "class_name": source.get_class()})
|
|
298
|
-
|
|
299
|
+
var emitted: int = passed.size()
|
|
300
|
+
var unbound: int = callable.get_unbound_arguments_count()
|
|
301
|
+
passed = passed.slice(0, maxi(emitted - unbound, 0))
|
|
299
302
|
var from_signal: int = passed.size()
|
|
300
303
|
for bound: Variant in callable.get_bound_arguments():
|
|
301
304
|
var bound_class: String = ""
|
|
@@ -303,43 +306,58 @@ static func _misfit(source: Node, signal_name: String, callable: Callable, flags
|
|
|
303
306
|
var object: Object = bound
|
|
304
307
|
bound_class = object.get_class()
|
|
305
308
|
passed.append({"name": "bound", "type": typeof(bound), "class_name": bound_class})
|
|
309
|
+
var binds: int = passed.size() - from_signal
|
|
310
|
+
|
|
311
|
+
# What the method is called with, said in the parts that make it: an answer counting the bound
|
|
312
|
+
# arguments among the signal's said the signal passed more than it declares.
|
|
313
|
+
var called: String = "%s passes %s" % [signal_name, _count(emitted)]
|
|
314
|
+
if unbound > 0 or binds > 0:
|
|
315
|
+
var changed: Array[String] = []
|
|
316
|
+
if unbound > 0:
|
|
317
|
+
changed.append("unbinds %d" % unbound)
|
|
318
|
+
if binds > 0:
|
|
319
|
+
changed.append("binds %d" % binds)
|
|
320
|
+
called += (
|
|
321
|
+
" and the connection %s, so %s is called with %s, and"
|
|
322
|
+
% [" and ".join(PackedStringArray(changed)), method, _count(passed.size())]
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
called += ", and " + method
|
|
306
326
|
|
|
307
327
|
var parameters: Array = taken.get("args", [])
|
|
308
328
|
var defaults: Array = taken.get("default_args", [])
|
|
309
329
|
var needed: int = parameters.size() - defaults.size()
|
|
310
330
|
if passed.size() < needed:
|
|
311
|
-
return "%s
|
|
331
|
+
return "%s needs %d%s" % [called, needed, FAILS]
|
|
312
332
|
if passed.size() > parameters.size():
|
|
313
333
|
var most: String = "none" if parameters.is_empty() else "at most %d" % parameters.size()
|
|
314
334
|
# Said with the misfit, since unbinds is what a connection to a method wanting less is for, and
|
|
315
|
-
# only when the extra arguments are the signal's: a
|
|
335
|
+
# only when the extra arguments are the signal's: a scene's connection unbinds the arguments
|
|
336
|
+
# the signal passes before it adds the bound ones, so a bound one is never dropped.
|
|
316
337
|
var extra: int = passed.size() - parameters.size()
|
|
317
338
|
var remedy: String = (
|
|
318
339
|
(
|
|
319
|
-
"
|
|
320
|
-
% [
|
|
321
|
-
extra + callable.get_unbound_arguments_count(),
|
|
322
|
-
"argument" if extra == 1 else "%d arguments" % extra,
|
|
323
|
-
"is" if extra == 1 else "are",
|
|
324
|
-
]
|
|
340
|
+
" Connect it with unbinds %d to leave out the last %s the signal passes."
|
|
341
|
+
% [extra + unbound, "argument" if extra == 1 else "%d arguments" % extra]
|
|
325
342
|
)
|
|
326
343
|
if extra <= from_signal
|
|
327
344
|
else ""
|
|
328
345
|
)
|
|
329
|
-
return
|
|
330
|
-
"%s passes %s, and %s takes %s%s%s"
|
|
331
|
-
% [signal_name, _count(passed.size()), method, most, FAILS, remedy]
|
|
332
|
-
)
|
|
346
|
+
return "%s takes %s%s%s" % [called, most, FAILS, remedy]
|
|
333
347
|
for at: int in passed.size():
|
|
334
348
|
var given: Dictionary = passed[at]
|
|
335
349
|
var parameter: Dictionary = parameters[at]
|
|
336
350
|
if not _unrelated(given, parameter):
|
|
337
351
|
continue
|
|
352
|
+
var whose: String = (
|
|
353
|
+
"%s passes argument %d" % [signal_name, at + 1]
|
|
354
|
+
if at < from_signal
|
|
355
|
+
else "the connection binds argument %d" % (at + 1)
|
|
356
|
+
)
|
|
338
357
|
return (
|
|
339
|
-
"%s
|
|
358
|
+
"%s as %s, and %s takes it as %s: %s%s"
|
|
340
359
|
% [
|
|
341
|
-
|
|
342
|
-
at + 1,
|
|
360
|
+
whose,
|
|
343
361
|
_type_named(given),
|
|
344
362
|
method,
|
|
345
363
|
str(parameter.get("name", "")),
|
|
@@ -380,6 +398,25 @@ static func _type_named(declared: Dictionary) -> String:
|
|
|
380
398
|
return type_string(type)
|
|
381
399
|
|
|
382
400
|
|
|
401
|
+
## The scene that makes the connection [param source] holds with [param flags], as a path, or "" for
|
|
402
|
+
## one this scene makes. The engine marks such a connection inherited as the scene holding it loads:
|
|
403
|
+
## from an instance, the scene instanced, and from the root of an inherited scene, the one it
|
|
404
|
+
## inherits.
|
|
405
|
+
static func _made_in(scene: GdhSceneFile, source: Node, flags: int) -> String:
|
|
406
|
+
if not flags & CONNECT_INHERITED:
|
|
407
|
+
return ""
|
|
408
|
+
if source != scene.root and not source.scene_file_path.is_empty():
|
|
409
|
+
return source.scene_file_path
|
|
410
|
+
var holder: Node = source if source == scene.root else source.owner
|
|
411
|
+
while holder != null and holder != scene.root and holder.scene_file_path.is_empty():
|
|
412
|
+
holder = holder.owner
|
|
413
|
+
if holder != null and holder != scene.root:
|
|
414
|
+
return holder.scene_file_path
|
|
415
|
+
var packed: PackedScene = load(scene.path) as PackedScene
|
|
416
|
+
var base: SceneState = packed.get_state().get_base_scene_state() if packed != null else null
|
|
417
|
+
return base.get_path() if base != null else "a scene this one inherits"
|
|
418
|
+
|
|
419
|
+
|
|
383
420
|
static func _count(arguments: int) -> String:
|
|
384
421
|
if arguments == 0:
|
|
385
422
|
return "no arguments"
|
|
@@ -428,6 +465,21 @@ func disconnect_signal(args: Dictionary) -> Dictionary:
|
|
|
428
465
|
% [scene_path, said_from, signal_name, said_to, method_name]
|
|
429
466
|
)
|
|
430
467
|
)
|
|
468
|
+
var made_in: String = _made_in(scene, source, GdhRead.as_int(held["flags"]))
|
|
469
|
+
if not made_in.is_empty():
|
|
470
|
+
var named_from: String = str(source.name) if source == scene.root else from
|
|
471
|
+
var named_to: String = str(target.name) if target == scene.root else to
|
|
472
|
+
var made_by: String = (
|
|
473
|
+
"The connection from %s.%s to %s.%s is made by %s"
|
|
474
|
+
% [named_from, signal_name, named_to, method_name, made_in]
|
|
475
|
+
)
|
|
476
|
+
return scene.refuse(
|
|
477
|
+
(
|
|
478
|
+
made_by
|
|
479
|
+
+ ", and this scene saves nothing about it: disconnect it there, which changes every"
|
|
480
|
+
+ " scene using it."
|
|
481
|
+
)
|
|
482
|
+
)
|
|
431
483
|
var callable: Callable = held["callable"]
|
|
432
484
|
source.disconnect(signal_name, callable)
|
|
433
485
|
|
|
@@ -498,6 +550,9 @@ func list_connections(args: Dictionary) -> Dictionary:
|
|
|
498
550
|
if target_obj is Node:
|
|
499
551
|
target_path = str(root.get_path_to(target_obj as Node))
|
|
500
552
|
editable = editable and scene.foreign(target_obj as Node).is_empty()
|
|
553
|
+
# A connection an instanced scene makes on its own root sits on a node this scene owns,
|
|
554
|
+
# so the ends alone read it as one this scene could change.
|
|
555
|
+
var made_in: String = _made_in(scene, node, flags)
|
|
501
556
|
var listed: Dictionary = {
|
|
502
557
|
"sourceNodePath": path,
|
|
503
558
|
"signalName": signal_name,
|
|
@@ -505,11 +560,11 @@ func list_connections(args: Dictionary) -> Dictionary:
|
|
|
505
560
|
"methodName": str(callable.get_method()),
|
|
506
561
|
"flags": flags,
|
|
507
562
|
"flagNames": flag_names(flags),
|
|
508
|
-
|
|
509
|
-
# scene can change.
|
|
510
|
-
"editable": editable,
|
|
563
|
+
"editable": editable and made_in.is_empty(),
|
|
511
564
|
"methodFound": target_obj != null and target_obj.has_method(callable.get_method()),
|
|
512
565
|
}
|
|
566
|
+
if not made_in.is_empty():
|
|
567
|
+
listed["madeIn"] = made_in
|
|
513
568
|
var bound: Dictionary = {
|
|
514
569
|
"binds": callable.get_bound_arguments(),
|
|
515
570
|
"unbinds": callable.get_unbound_arguments_count(),
|
|
@@ -61,9 +61,11 @@ func _init(host: Node, values: GdhValues) -> void:
|
|
|
61
61
|
## pointer when the game is on one.
|
|
62
62
|
##
|
|
63
63
|
## For the root window the engine asks Windows where the pointer is, and GetCursorPos refuses a
|
|
64
|
-
## process on a desktop that is not in use:
|
|
65
|
-
## with access denied
|
|
66
|
-
##
|
|
64
|
+
## process on a desktop that is not in use: on one of gdharness's own it answers false and leaves the
|
|
65
|
+
## point unwritten, with access denied in the user's window station and with
|
|
66
|
+
## ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION in the one gdharness's desktop is in. The engine does
|
|
67
|
+
## not check, so a game driven there read get_mouse_position() as x 836852416 after a motion to
|
|
68
|
+
## 1310, 314. Nothing here can
|
|
67
69
|
## put a pointer on that desktop: Windows moves one only on the desktop in use, and making this one
|
|
68
70
|
## that would take the screen. What reads the injected event is right, tooltips included, since the
|
|
69
71
|
## engine places a tooltip from the motion that reached the control.
|
|
@@ -112,11 +112,15 @@ static func field_before(focused: Control) -> Dictionary:
|
|
|
112
112
|
## Whether [param focused], read as [param before], stopped being edited, as a [LineEdit] does when
|
|
113
113
|
## it is submitted: it keeps the focus and drops every key after, so a caller reading only the focus
|
|
114
114
|
## typed next into nothing.
|
|
115
|
-
|
|
115
|
+
##
|
|
116
|
+
## [param focused] is a Variant because the key can free it, as Escape closing the panel a field is
|
|
117
|
+
## on does, and a parameter typed Control refuses a freed object before the check here can run.
|
|
118
|
+
static func ended_editing(focused: Variant, before: Dictionary) -> bool:
|
|
116
119
|
var was: bool = before.get("editing", false)
|
|
117
120
|
if not was or not is_instance_valid(focused):
|
|
118
121
|
return false
|
|
119
|
-
var
|
|
122
|
+
var held: Object = focused
|
|
123
|
+
var field: LineEdit = held as LineEdit
|
|
120
124
|
return field != null and not field.is_editing()
|
|
121
125
|
|
|
122
126
|
|
|
@@ -124,15 +128,18 @@ static func ended_editing(focused: Control, before: Dictionary) -> bool:
|
|
|
124
128
|
## typed_into and holds where its text changed, and editing_ended where it stopped being edited. A
|
|
125
129
|
## field being edited takes a printable key as typing and keeps it from the game's _unhandled_input,
|
|
126
130
|
## so Space into a name field pressed no jump, and an answer naming only the focus did not say why.
|
|
127
|
-
|
|
131
|
+
## A Variant for the reason [method ended_editing] gives.
|
|
132
|
+
static func field_noted(said: Dictionary, focused: Variant, before: Dictionary) -> Dictionary:
|
|
128
133
|
if before.is_empty() or not is_instance_valid(focused):
|
|
129
134
|
return said
|
|
130
|
-
var
|
|
131
|
-
var
|
|
135
|
+
var held: Object = focused
|
|
136
|
+
var control: Control = held as Control
|
|
137
|
+
var field: LineEdit = control as LineEdit
|
|
138
|
+
var now: String = field.text if field != null else (control as TextEdit).text
|
|
132
139
|
if now != before["text"]:
|
|
133
|
-
said["typed_into"] = str(
|
|
134
|
-
said["holds"] = _what_it_holds(
|
|
135
|
-
if ended_editing(
|
|
140
|
+
said["typed_into"] = str(control.get_path())
|
|
141
|
+
said["holds"] = _what_it_holds(control)
|
|
142
|
+
if ended_editing(control, before):
|
|
136
143
|
said["editing_ended"] = true
|
|
137
144
|
return said
|
|
138
145
|
|
|
@@ -125,15 +125,16 @@ static func no_animation(player: AnimationPlayer, called: String, animation_name
|
|
|
125
125
|
|
|
126
126
|
static func _summary(anim: Animation) -> Dictionary:
|
|
127
127
|
return {
|
|
128
|
-
"length": anim.length,
|
|
128
|
+
"length": held_seconds(anim.length),
|
|
129
129
|
"loopMode": LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
|
|
130
|
-
"step": anim.step,
|
|
130
|
+
"step": held_seconds(anim.step),
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
|
|
134
|
-
## [param seconds], a key's time, as the shortest decimal holding
|
|
135
|
-
##
|
|
136
|
-
|
|
134
|
+
## [param seconds], a key's time or an animation's length or step, as the shortest decimal holding
|
|
135
|
+
## the same 32-bit float. An animation keeps all three as 32-bit floats, so a key put at 0.7 read
|
|
136
|
+
## back as 0.699999988079071 and a step of 0.1 as 0.100000001490116.
|
|
137
|
+
static func held_seconds(seconds: float) -> float:
|
|
137
138
|
var held: float = PackedFloat32Array([seconds])[0]
|
|
138
139
|
for places: int in range(0, 10):
|
|
139
140
|
var shorter: float = String.num(held, places).to_float()
|
|
@@ -159,7 +160,7 @@ static func _track(anim: Animation, track: int, values: GdhSerialisation) -> Dic
|
|
|
159
160
|
var keyframes: Array[Dictionary] = []
|
|
160
161
|
var methods: Dictionary = {}
|
|
161
162
|
for key: int in anim.track_get_key_count(track):
|
|
162
|
-
var keyframe: Dictionary = {"time":
|
|
163
|
+
var keyframe: Dictionary = {"time": held_seconds(anim.track_get_key_time(track, key))}
|
|
163
164
|
if kind == Animation.TYPE_METHOD:
|
|
164
165
|
var method: String = str(anim.method_track_get_name(track, key))
|
|
165
166
|
methods[method] = true
|
|
@@ -428,7 +428,10 @@ func _declared_class_name(path: String) -> String:
|
|
|
428
428
|
func _usage_files(kinds: Array) -> Array[String]:
|
|
429
429
|
var found: Array[String] = []
|
|
430
430
|
for kind: Variant in kinds:
|
|
431
|
-
|
|
431
|
+
# As project_search reads an extension: ".tscn" searched no file, and the answer said
|
|
432
|
+
# nothing used a script its scene attaches.
|
|
433
|
+
var named: String = str(kind).trim_prefix(".").to_lower()
|
|
434
|
+
match named:
|
|
432
435
|
"godot":
|
|
433
436
|
found.append("res://project.godot")
|
|
434
437
|
"cfg":
|
|
@@ -436,7 +439,7 @@ func _usage_files(kinds: Array) -> Array[String]:
|
|
|
436
439
|
if FileAccess.file_exists("res://export_presets.cfg"):
|
|
437
440
|
found.append("res://export_presets.cfg")
|
|
438
441
|
_:
|
|
439
|
-
found.append_array(_files.find_files("res://", "." +
|
|
442
|
+
found.append_array(_files.find_files("res://", "." + named))
|
|
440
443
|
return found
|
|
441
444
|
|
|
442
445
|
|
|
@@ -11,6 +11,7 @@ extends RefCounted
|
|
|
11
11
|
## initialisers run, and a resource with a script is made with it.
|
|
12
12
|
|
|
13
13
|
const GdhRead = preload("reading.gd")
|
|
14
|
+
const GdhFileWalk = preload("file_walk.gd")
|
|
14
15
|
const GdhSame = preload("same_values.gd")
|
|
15
16
|
const GdhSerialisation = preload("serialisation.gd")
|
|
16
17
|
const GdhShapes = preload("value_shapes.gd")
|
|
@@ -487,7 +488,7 @@ static func _stored(type: String, given: Dictionary, include_defaults: bool) ->
|
|
|
487
488
|
static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
488
489
|
var file: String = res_path.get_slice("::", 0)
|
|
489
490
|
if not FileAccess.file_exists(file):
|
|
490
|
-
return {"ok": false, "error": "No resource at " + file}
|
|
491
|
+
return {"ok": false, "error": GdhFileWalk.new().with_nearest_files("No resource at " + file, file)}
|
|
491
492
|
var whole: Resource = ResourceLoader.load(file, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
492
493
|
if whole == null:
|
|
493
494
|
return {"ok": false, "error": "%s did not load as a resource." % file}
|
|
@@ -35,8 +35,24 @@ static func missing_reason(path: String) -> String:
|
|
|
35
35
|
|
|
36
36
|
## [param said], the refusal for [param path], a res:// path nothing is at, with the files of its
|
|
37
37
|
## extension whose names are nearest its own, or every one when there are few and none is near, so
|
|
38
|
-
## the caller has a path to send instead.
|
|
38
|
+
## the caller has a path to send instead. Files of the same name beside it come first, since a
|
|
39
|
+
## misspelt extension has no file of its own extension to be near.
|
|
39
40
|
func with_nearest_files(said: String, path: String) -> String:
|
|
41
|
+
# One the refusal names is the file it refuses, as a script given for a scene is. Word by word,
|
|
42
|
+
# since res://brain.gd is inside res://brain.gdd.
|
|
43
|
+
var named: Array[String] = []
|
|
44
|
+
for word: String in said.replace("\n", " ").replace(",", " ").replace(";", " ").split(" ", false):
|
|
45
|
+
named.append(word.rstrip(".:"))
|
|
46
|
+
var namesakes: Array[String] = []
|
|
47
|
+
for file: String in _namesakes(path):
|
|
48
|
+
if not file in named:
|
|
49
|
+
namesakes.append(file)
|
|
50
|
+
if not namesakes.is_empty():
|
|
51
|
+
var alone: bool = namesakes.size() == 1
|
|
52
|
+
return (
|
|
53
|
+
"%s. The %s of that name beside it %s %s."
|
|
54
|
+
% [said, "file" if alone else "files", "is" if alone else "are", ", ".join(namesakes)]
|
|
55
|
+
)
|
|
40
56
|
var extension: String = path.get_extension()
|
|
41
57
|
if extension.is_empty():
|
|
42
58
|
return said
|
|
@@ -65,6 +81,22 @@ func with_nearest_files(said: String, path: String) -> String:
|
|
|
65
81
|
return "%s. None of the project's %d .%s files is named like it." % [said, files.size(), extension]
|
|
66
82
|
|
|
67
83
|
|
|
84
|
+
# The files in [param path]'s folder named as it is with another extension, leaving out the import
|
|
85
|
+
# and uid files the engine keeps beside a file, which are named after the whole file name.
|
|
86
|
+
static func _namesakes(path: String) -> Array[String]:
|
|
87
|
+
var found: Array[String] = []
|
|
88
|
+
var folder: String = path.get_base_dir()
|
|
89
|
+
if not DirAccess.dir_exists_absolute(folder):
|
|
90
|
+
return found
|
|
91
|
+
var stem: String = path.get_file().get_basename()
|
|
92
|
+
var files: PackedStringArray = DirAccess.get_files_at(folder)
|
|
93
|
+
files.sort()
|
|
94
|
+
for file: String in files:
|
|
95
|
+
if file.get_basename() == stem and file != path.get_file():
|
|
96
|
+
found.append(folder.path_join(file))
|
|
97
|
+
return found
|
|
98
|
+
|
|
99
|
+
|
|
68
100
|
# A directory the engine steps over, marker file and all.
|
|
69
101
|
func is_stepped_over(path: String) -> bool:
|
|
70
102
|
return FileAccess.file_exists(path + ".gdignore")
|
|
@@ -404,6 +404,14 @@ static func _modification_problems(modifications: Array, declared: Dictionary) -
|
|
|
404
404
|
)
|
|
405
405
|
if mod_type == "add_constant" and str(fields.get("defaultValue", "")).is_empty():
|
|
406
406
|
problems.append(which + " is a constant with no defaultValue, and a constant needs a value")
|
|
407
|
+
if (
|
|
408
|
+
not str(fields.get("exportHint", "")).is_empty()
|
|
409
|
+
and fields.has("isExport")
|
|
410
|
+
and not GdhRead.as_bool(fields["isExport"])
|
|
411
|
+
):
|
|
412
|
+
problems.append(
|
|
413
|
+
which + " has an exportHint and isExport false, and a hint is written only on an export"
|
|
414
|
+
)
|
|
407
415
|
if mod_name.is_empty():
|
|
408
416
|
problems.append(which + " has no name")
|
|
409
417
|
continue
|
|
@@ -527,8 +535,9 @@ func _add_variable(lines: Array[String], mod: Dictionary, placed: Array[Dictiona
|
|
|
527
535
|
var var_name: String = str(mod.get("name", ""))
|
|
528
536
|
var var_type: String = str(mod.get("varType", ""))
|
|
529
537
|
var default_value: String = str(mod.get("defaultValue", ""))
|
|
530
|
-
var is_export: bool = GdhRead.as_bool(mod.get("isExport", false))
|
|
531
538
|
var export_hint: String = str(mod.get("exportHint", ""))
|
|
539
|
+
# A hint can only mean an export, so a hint given alone writes one.
|
|
540
|
+
var is_export: bool = GdhRead.as_bool(mod.get("isExport", not export_hint.is_empty()))
|
|
532
541
|
var is_onready: bool = GdhRead.as_bool(mod.get("isOnready", false))
|
|
533
542
|
|
|
534
543
|
var var_line: String = ""
|
|
@@ -408,7 +408,7 @@ func _import_status_of(resource_path: String, import_file_path: String) -> Dicti
|
|
|
408
408
|
# Why [param resource_path] has no import file, with what to do about it where anything can be done.
|
|
409
409
|
func _no_sidecar(resource_path: String) -> String:
|
|
410
410
|
if not FileAccess.file_exists(resource_path):
|
|
411
|
-
return resource_path + " is not on disk"
|
|
411
|
+
return GdhFileWalk.new().with_nearest_files(resource_path + " is not on disk", resource_path)
|
|
412
412
|
var never: String = _never_imported(resource_path)
|
|
413
413
|
if not never.is_empty():
|
|
414
414
|
return resource_path + " has no import options: " + never
|