gdharness 1.3.2 → 1.3.4
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 +163 -85
- package/build/godot/addons/gdharness_editor/animation_reads.gd +14 -11
- package/build/godot/addons/gdharness_editor/declared_values.gd +157 -4
- package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
- package/build/godot/addons/gdharness_editor/file_walk.gd +134 -0
- package/build/godot/addons/gdharness_editor/file_walk.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
- package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
- package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +249 -227
- package/build/godot/addons/gdharness_editor/reading.gd +15 -0
- package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
- package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +100 -15
- package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
- package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +402 -55
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +88 -5
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +102 -337
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +10 -26
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +30 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
- package/build/godot/operations/animation_reads.gd +14 -11
- package/build/godot/operations/declared_values.gd +157 -4
- package/build/godot/operations/dependencies.gd +14 -39
- package/build/godot/operations/editor_tools.gd +7 -3
- package/build/godot/operations/file_reads.gd +15 -3
- package/build/godot/operations/file_walk.gd +1 -0
- package/build/godot/operations/gdscript_analysis.gd +229 -75
- package/build/godot/operations/gdscript_authoring.gd +142 -34
- package/build/godot/operations/import_pipeline.gd +17 -6
- package/build/godot/operations/logger.gd +7 -0
- package/build/godot/operations/property_paths.gd +225 -0
- package/build/godot/operations/property_values.gd +249 -227
- package/build/godot/operations/reading.gd +15 -0
- package/build/godot/operations/same_values.gd +177 -0
- package/build/godot/operations/serialisation.gd +75 -3
- package/package.json +1 -1
|
@@ -81,15 +81,7 @@ static func animations(player: AnimationPlayer, animation_name: String) -> Dicti
|
|
|
81
81
|
listed.append(summary)
|
|
82
82
|
return {"ok": true, "animations": listed}
|
|
83
83
|
if not player.has_animation(StringName(animation_name)):
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
"ok": false,
|
|
87
|
-
"error":
|
|
88
|
-
(
|
|
89
|
-
"%s has no animation %s. It has: %s."
|
|
90
|
-
% [player.name, animation_name, ", ".join(names) if not names.is_empty() else "none"]
|
|
91
|
-
)
|
|
92
|
-
}
|
|
84
|
+
return {"ok": false, "error": no_animation(player, str(player.name), animation_name)}
|
|
93
85
|
var anim: Animation = player.get_animation(StringName(animation_name))
|
|
94
86
|
var answer: Dictionary = _summary(anim)
|
|
95
87
|
answer["ok"] = true
|
|
@@ -102,6 +94,16 @@ static func animations(player: AnimationPlayer, animation_name: String) -> Dicti
|
|
|
102
94
|
return answer
|
|
103
95
|
|
|
104
96
|
|
|
97
|
+
## Why [param player], called [param called], cannot be asked for [param animation_name], with the
|
|
98
|
+
## animations it has.
|
|
99
|
+
static func no_animation(player: AnimationPlayer, called: String, animation_name: String) -> String:
|
|
100
|
+
var names: PackedStringArray = player.get_animation_list()
|
|
101
|
+
return (
|
|
102
|
+
"%s has no animation %s. It has: %s."
|
|
103
|
+
% [called, animation_name, ", ".join(names) if not names.is_empty() else "none"]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
105
107
|
static func _summary(anim: Animation) -> Dictionary:
|
|
106
108
|
return {
|
|
107
109
|
"length": anim.length,
|
|
@@ -162,8 +164,9 @@ static func state_machine(tree: AnimationTree, state_machine_path: String) -> An
|
|
|
162
164
|
static func no_machine(tree: AnimationTree, state_machine_path: String) -> String:
|
|
163
165
|
if tree == null or tree.tree_root == null:
|
|
164
166
|
return (
|
|
165
|
-
"The AnimationTree has no tree_root.
|
|
166
|
-
+ "AnimationNodeStateMachine
|
|
167
|
+
"The AnimationTree has no tree_root. Give it one with scene_node set: tree_root "
|
|
168
|
+
+ '{"_type": "AnimationNodeStateMachine"} builds one into the scene, and the res:// path '
|
|
169
|
+
+ "of one made with resource_edit create keeps it in a file of its own."
|
|
167
170
|
)
|
|
168
171
|
var current: AnimationNodeStateMachine = tree.tree_root as AnimationNodeStateMachine
|
|
169
172
|
if current == null:
|
|
@@ -58,12 +58,46 @@ static func enum_named(choices: Dictionary, word: String) -> Variant:
|
|
|
58
58
|
return null
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
## The values of the engine enum [param property] of [param target] holds, by the constants its
|
|
62
|
+
## class declares them as: blend_mode's Add is BLEND_MODE_ADD in a script, and the hint gives only
|
|
63
|
+
## the inspector's word. Empty for an enum a script declares.
|
|
64
|
+
##
|
|
65
|
+
## The enum is named by the setter's argument, as Node.ProcessMode, since the property list gives
|
|
66
|
+
## an engine enum property no class of its own.
|
|
67
|
+
static func engine_constants(target: Object, property: String) -> Dictionary:
|
|
68
|
+
var values: Dictionary = {}
|
|
69
|
+
var engine_class: String = target.get_class()
|
|
70
|
+
var setter: String = str(ClassDB.class_get_property_setter(engine_class, property))
|
|
71
|
+
if setter.is_empty():
|
|
72
|
+
return values
|
|
73
|
+
var named: String = ""
|
|
74
|
+
for method: Dictionary in ClassDB.class_get_method_list(engine_class):
|
|
75
|
+
var arguments: Array = method.get("args", [])
|
|
76
|
+
if str(method.get("name", "")) == setter and not arguments.is_empty():
|
|
77
|
+
var value_argument: Dictionary = arguments[-1]
|
|
78
|
+
named = str(value_argument.get("class_name", ""))
|
|
79
|
+
break
|
|
80
|
+
var holder: String = named.get_slice(".", 0)
|
|
81
|
+
if named.count(".") != 1 or not ClassDB.class_exists(holder):
|
|
82
|
+
return values
|
|
83
|
+
for constant: String in ClassDB.class_get_enum_constants(holder, named.get_slice(".", 1)):
|
|
84
|
+
values[constant] = ClassDB.class_get_integer_constant(holder, constant)
|
|
85
|
+
return values
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
static func enum_refusal(
|
|
89
|
+
holder: String, property: String, word: String, choices: Dictionary, constants: Dictionary = {}
|
|
90
|
+
) -> String:
|
|
91
|
+
var ways: String = "by name or by number"
|
|
92
|
+
if not constants.is_empty():
|
|
93
|
+
ways = "by name, by number or by the engine's constant, such as %s" % str(constants.keys()[0])
|
|
62
94
|
var said: String = (
|
|
63
|
-
"%s.%s is an enum, and %s is none of its values: send %s
|
|
64
|
-
% [holder, property, word, _listed(choices)]
|
|
95
|
+
"%s.%s is an enum, and %s is none of its values: send %s %s"
|
|
96
|
+
% [holder, property, word, _listed(choices), ways]
|
|
65
97
|
)
|
|
66
|
-
var
|
|
98
|
+
var named: PackedStringArray = PackedStringArray(choices.keys())
|
|
99
|
+
named.append_array(PackedStringArray(constants.keys()))
|
|
100
|
+
var close: PackedStringArray = GdhRead.nearest(word, named)
|
|
67
101
|
if close.is_empty():
|
|
68
102
|
return said + "."
|
|
69
103
|
return said + ". The nearest %s %s." % ["is" if close.size() == 1 else "are", ", ".join(close)]
|
|
@@ -76,6 +110,17 @@ static func outside(target: Object, declared: Dictionary, value: Variant, shown:
|
|
|
76
110
|
var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
77
111
|
var hint_string: String = str(declared.get("hint_string", ""))
|
|
78
112
|
var property: String = str(declared.get("name", ""))
|
|
113
|
+
if hint == PROPERTY_HINT_ENUM and (typeof(value) == TYPE_STRING or typeof(value) == TYPE_STRING_NAME):
|
|
114
|
+
var names: PackedStringArray = PackedStringArray(enum_values(hint_string).keys())
|
|
115
|
+
if names.is_empty() or names.has(str(value)):
|
|
116
|
+
return ""
|
|
117
|
+
return (
|
|
118
|
+
(
|
|
119
|
+
"%s is none of the names %s lists: %s, which the inspector holds it to; the engine "
|
|
120
|
+
+ "stores it as given."
|
|
121
|
+
)
|
|
122
|
+
% [shown, property, ", ".join(names)]
|
|
123
|
+
)
|
|
79
124
|
if typeof(value) != TYPE_INT and typeof(value) != TYPE_FLOAT:
|
|
80
125
|
return ""
|
|
81
126
|
var number: float = GdhRead.as_float(value)
|
|
@@ -114,3 +159,111 @@ static func _listed(choices: Dictionary) -> String:
|
|
|
114
159
|
## A range's end as the script wrote it: the engine's hint gives @export_range(0, 10) as 0.0 and 10.0.
|
|
115
160
|
static func _bound(end: float) -> String:
|
|
116
161
|
return str(int(end)) if end == floorf(end) and absf(end) < 1e15 else str(end)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
## Puts back on [param built] what its script set as it was attached and was not among
|
|
165
|
+
## [param wanted], to the defaults it declares, outside the editor. Attaching runs the script's _init
|
|
166
|
+
## there, which the editor never runs for a script that is not a tool, so a resource_name _init chose
|
|
167
|
+
## was saved as if asked for. A property whose default the engine cannot report is left as made.
|
|
168
|
+
static func unmade(built: Resource, wanted: Dictionary) -> void:
|
|
169
|
+
var attached: Variant = built.get_script()
|
|
170
|
+
if Engine.is_editor_hint() or not attached is Script:
|
|
171
|
+
return
|
|
172
|
+
var script: Script = attached
|
|
173
|
+
var asked: Dictionary = {}
|
|
174
|
+
for key: Variant in wanted:
|
|
175
|
+
asked[str(key).get_slice(":", 0)] = true
|
|
176
|
+
for entry: Dictionary in built.get_property_list():
|
|
177
|
+
var name: String = str(entry["name"])
|
|
178
|
+
var usage: int = GdhRead.as_int(entry["usage"])
|
|
179
|
+
if usage & PROPERTY_USAGE_STORAGE == 0 or name == "script" or asked.has(name):
|
|
180
|
+
continue
|
|
181
|
+
var declared: Variant = ClassDB.class_get_property_default_value(built.get_class(), name)
|
|
182
|
+
if usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
|
|
183
|
+
declared = script.get_property_default_value(name)
|
|
184
|
+
var held: Variant = built.get(name)
|
|
185
|
+
if declared != null and not (typeof(held) == typeof(declared) and held == declared):
|
|
186
|
+
built.set(name, declared)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
## The element types a typed Array or Dictionary property declares: [element] for an array,
|
|
190
|
+
## [key, value] for a dictionary, [] when nothing is declared.
|
|
191
|
+
##
|
|
192
|
+
## Read from the declaration because the value cannot say: a script's exported `Array[int]` sits
|
|
193
|
+
## behind a placeholder in the editor, and measured on 4.7.2 that placeholder stores [1.5, 2.0]
|
|
194
|
+
## as given, which the game then fails to load into the typed array.
|
|
195
|
+
static func elements_of(declared: Dictionary) -> Array:
|
|
196
|
+
var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
|
|
197
|
+
var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
198
|
+
var wanted: String = str(declared.get("hint_string", ""))
|
|
199
|
+
if wanted.is_empty():
|
|
200
|
+
return []
|
|
201
|
+
if hint == PROPERTY_HINT_TYPE_STRING:
|
|
202
|
+
var parts: PackedStringArray = wanted.split(";")
|
|
203
|
+
if type == TYPE_ARRAY and parts.size() == 1:
|
|
204
|
+
return [_type_in_hint(parts[0])]
|
|
205
|
+
if type == TYPE_DICTIONARY and parts.size() == 2:
|
|
206
|
+
return [_type_in_hint(parts[0]), _type_in_hint(parts[1])]
|
|
207
|
+
return []
|
|
208
|
+
if hint == PROPERTY_HINT_ARRAY_TYPE and type == TYPE_ARRAY:
|
|
209
|
+
return [_type_named(wanted)]
|
|
210
|
+
if hint == PROPERTY_HINT_DICTIONARY_TYPE and type == TYPE_DICTIONARY:
|
|
211
|
+
var parts: PackedStringArray = wanted.split(";")
|
|
212
|
+
if parts.size() == 2:
|
|
213
|
+
return [_type_named(parts[0]), _type_named(parts[1])]
|
|
214
|
+
return []
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
## The type at the front of one entry of a type-string hint, "2:" or "24/17:Texture2D".
|
|
218
|
+
static func _type_in_hint(entry: String) -> int:
|
|
219
|
+
var number: String = entry.get_slice(":", 0).get_slice("/", 0)
|
|
220
|
+
return number.to_int() if number.is_valid_int() else TYPE_NIL
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
## The Variant type [param name] names, or TYPE_OBJECT for a class.
|
|
224
|
+
static func _type_named(name: String) -> int:
|
|
225
|
+
var named: String = name.get_slice(":", 0).strip_edges()
|
|
226
|
+
for type: int in TYPE_MAX:
|
|
227
|
+
if type_string(type) == named:
|
|
228
|
+
return type
|
|
229
|
+
return TYPE_OBJECT if not named.is_empty() else TYPE_NIL
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
## The class the objects of a typed list or map hold, and whether they are nodes, as
|
|
233
|
+
## [class, nodes], read from entry [param at] of [param declared]'s hint: "24/17:Item" in a type
|
|
234
|
+
## string, "Item" in an array or dictionary type.
|
|
235
|
+
static func object_kind(declared: Dictionary, at: int) -> Array:
|
|
236
|
+
var entry: String = str(declared.get("hint_string", "")).get_slice(";", at)
|
|
237
|
+
if GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE)) != PROPERTY_HINT_TYPE_STRING:
|
|
238
|
+
return [entry.strip_edges(), false]
|
|
239
|
+
var nodes: bool = entry.get_slice(":", 0).get_slice("/", 1) == str(PROPERTY_HINT_NODE_TYPE)
|
|
240
|
+
return [entry.get_slice(":", 1), nodes]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
## The base class and script that type a list or map holding [param wanted], as [class, script],
|
|
244
|
+
## or [] when nothing in the project or the engine is called that.
|
|
245
|
+
static func object_typing(wanted: String) -> Array:
|
|
246
|
+
if ClassDB.class_exists(wanted):
|
|
247
|
+
return [StringName(wanted), null]
|
|
248
|
+
var script: Script = script_class(wanted)
|
|
249
|
+
return [] if script == null else [script.get_instance_base_type(), script]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
## The script declaring the class_name [param name], or null.
|
|
253
|
+
static func script_class(name: String) -> Script:
|
|
254
|
+
for entry: Dictionary in ProjectSettings.get_global_class_list():
|
|
255
|
+
if str(entry.get("class", "")) == name:
|
|
256
|
+
return load(str(entry.get("path", ""))) as Script
|
|
257
|
+
return null
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
## Whether [param value] is the class [param name] names, an engine class or a script's class_name.
|
|
261
|
+
static func is_a(value: Object, name: String) -> bool:
|
|
262
|
+
if value.is_class(name):
|
|
263
|
+
return true
|
|
264
|
+
var script: Script = value.get_script()
|
|
265
|
+
while script != null:
|
|
266
|
+
if str(script.get_global_name()) == name:
|
|
267
|
+
return true
|
|
268
|
+
script = script.get_base_script()
|
|
269
|
+
return false
|
|
@@ -10,8 +10,8 @@ extends RefCounted
|
|
|
10
10
|
## without making a node. Loading the file still does what loading does in the game: static
|
|
11
11
|
## initialisers run, and a resource with a script is made with it.
|
|
12
12
|
|
|
13
|
-
const GdhPropertyValues = preload("property_values.gd")
|
|
14
13
|
const GdhRead = preload("reading.gd")
|
|
14
|
+
const GdhSame = preload("same_values.gd")
|
|
15
15
|
const GdhSerialisation = preload("serialisation.gd")
|
|
16
16
|
|
|
17
17
|
|
|
@@ -188,6 +188,12 @@ static func _lay(
|
|
|
188
188
|
path,
|
|
189
189
|
state.get_node_index(index)
|
|
190
190
|
)
|
|
191
|
+
# Every file on the way puts the node in its own groups: the scene making it, and each scene
|
|
192
|
+
# instancing or inheriting that one.
|
|
193
|
+
var grouped: Array[String] = nodes[path]["groups"]
|
|
194
|
+
for group: StringName in state.get_node_groups(index):
|
|
195
|
+
if not grouped.has(str(group)):
|
|
196
|
+
grouped.append(str(group))
|
|
191
197
|
var held: Dictionary = nodes[path]["properties"]
|
|
192
198
|
for property: int in state.get_node_property_count(index):
|
|
193
199
|
held[str(state.get_node_property_name(index, property))] = state.get_node_property_value(
|
|
@@ -281,7 +287,8 @@ static func _by_id(nodes: Dictionary, owner: String, id: int) -> String:
|
|
|
281
287
|
|
|
282
288
|
static func _made(type: String) -> Dictionary:
|
|
283
289
|
var children: Array[String] = []
|
|
284
|
-
|
|
290
|
+
var groups: Array[String] = []
|
|
291
|
+
return {"name": "", "type": type, "properties": {}, "children": children, "groups": groups}
|
|
285
292
|
|
|
286
293
|
|
|
287
294
|
## Puts [param path] among [param parent]'s children, at [param at] when the file says where (a node
|
|
@@ -317,6 +324,11 @@ static func _entry(
|
|
|
317
324
|
data["instanceOf"] = node["instanceOf"]
|
|
318
325
|
if node.has("editableChildren"):
|
|
319
326
|
data["editableChildren"] = node["editableChildren"]
|
|
327
|
+
var groups: Array[String] = node["groups"]
|
|
328
|
+
if not groups.is_empty():
|
|
329
|
+
var sorted: Array[String] = groups.duplicate()
|
|
330
|
+
sorted.sort()
|
|
331
|
+
data["groups"] = sorted
|
|
320
332
|
if include_properties:
|
|
321
333
|
var given: Dictionary = node["properties"]
|
|
322
334
|
data["properties"] = _stored(str(node["type"]), given)
|
|
@@ -419,7 +431,7 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
419
431
|
if scripted.has(property)
|
|
420
432
|
else ClassDB.class_get_property_default_value(loaded.get_class(), property)
|
|
421
433
|
)
|
|
422
|
-
if
|
|
434
|
+
if GdhSame.same(value, default):
|
|
423
435
|
continue
|
|
424
436
|
properties[property] = serialiser.serialize_value(value)
|
|
425
437
|
var answer: Dictionary = {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends RefCounted
|
|
3
|
+
|
|
4
|
+
const GdhRead = preload("reading.gd")
|
|
5
|
+
|
|
6
|
+
# Two walks rather than one because they disagree about hidden entries on purpose: a suffix
|
|
7
|
+
# walk descends into every visible directory and matches whole names, while the extension walk
|
|
8
|
+
# skips anything beginning with a dot, including .import sidecars that would otherwise answer
|
|
9
|
+
# for the resource they belong to.
|
|
10
|
+
#
|
|
11
|
+
# Both stop at a `.gdignore`, because the engine does: nothing under one is imported, so what is
|
|
12
|
+
# in there is not a resource, not a dependency and not a global class. Walking in anyway hands
|
|
13
|
+
# every caller files the engine will never answer for.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Why [param path], a res:// or uid:// path a setting names, is not a file the engine can load, or
|
|
17
|
+
# empty when it is. A uid:// path resolves through the project's uid cache, which the import writes,
|
|
18
|
+
# so one that does not resolve is a file that has gone or a project not imported yet, and saying
|
|
19
|
+
# only that the file does not exist sent a caller looking for a scene that was there.
|
|
20
|
+
static func missing_reason(path: String) -> String:
|
|
21
|
+
if path.begins_with("uid://"):
|
|
22
|
+
var id: int = ResourceUID.text_to_id(path)
|
|
23
|
+
if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id):
|
|
24
|
+
return (
|
|
25
|
+
path
|
|
26
|
+
+ " does not resolve: no file in the project's uid cache has it, so the file has gone or the"
|
|
27
|
+
+ " project has not been imported yet (project_import refresh_uids imports it)"
|
|
28
|
+
)
|
|
29
|
+
var resolved: String = ResourceUID.get_id_path(id)
|
|
30
|
+
return (
|
|
31
|
+
"" if FileAccess.file_exists(resolved) else path + " names " + resolved + ", which does not exist"
|
|
32
|
+
)
|
|
33
|
+
return "" if FileAccess.file_exists(path) else path + " does not exist"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## [param said], the refusal for [param path], a res:// path nothing is at, with the files of its
|
|
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.
|
|
39
|
+
func with_nearest_files(said: String, path: String) -> String:
|
|
40
|
+
var extension: String = path.get_extension()
|
|
41
|
+
if extension.is_empty():
|
|
42
|
+
return said
|
|
43
|
+
var files: Array[String] = find_files_with_extensions("res://", [extension])
|
|
44
|
+
# The walk lists a directory in the order the filesystem keeps it, which differs between
|
|
45
|
+
# platforms, so the same project was answered with its files in another order on Linux.
|
|
46
|
+
files.sort()
|
|
47
|
+
if files.is_empty():
|
|
48
|
+
return "%s. The project holds no .%s file." % [said, extension]
|
|
49
|
+
var names: PackedStringArray = []
|
|
50
|
+
for file: String in files:
|
|
51
|
+
var _named: bool = names.append(file.get_file().get_basename())
|
|
52
|
+
var close: PackedStringArray = GdhRead.nearest(path.get_file().get_basename(), names)
|
|
53
|
+
var offered: PackedStringArray = []
|
|
54
|
+
for file: String in files:
|
|
55
|
+
if file.get_file().get_basename() in close:
|
|
56
|
+
var _offered: bool = offered.append(file)
|
|
57
|
+
if not offered.is_empty():
|
|
58
|
+
var one: bool = offered.size() == 1
|
|
59
|
+
return (
|
|
60
|
+
"%s. The nearest .%s %s %s."
|
|
61
|
+
% [said, extension, "file is" if one else "files are", ", ".join(offered)]
|
|
62
|
+
)
|
|
63
|
+
if files.size() <= GdhRead.NAMED_CHILDREN:
|
|
64
|
+
return "%s. The project's .%s files are %s." % [said, extension, ", ".join(files)]
|
|
65
|
+
return "%s. None of the project's %d .%s files is named like it." % [said, files.size(), extension]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# A directory the engine steps over, marker file and all.
|
|
69
|
+
func is_stepped_over(path: String) -> bool:
|
|
70
|
+
return FileAccess.file_exists(path + ".gdignore")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# An addon gdharness installed, told by the marker setup writes into it. The engine loads it, but
|
|
74
|
+
# nobody on the project wrote it: the health check counted its thirty-odd scripts as the project's,
|
|
75
|
+
# 39 beside the 5 project_info's own count gave, which the server's walks already step over.
|
|
76
|
+
func is_installed_by_gdharness(path: String) -> bool:
|
|
77
|
+
return FileAccess.file_exists(path + ".gdharness-version")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Files under `path` whose name ends with `extension`, which is passed with its dot.
|
|
81
|
+
func find_files(path: String, extension: String) -> Array[String]:
|
|
82
|
+
var files: Array[String] = []
|
|
83
|
+
if is_stepped_over(path) or is_installed_by_gdharness(path):
|
|
84
|
+
return files
|
|
85
|
+
|
|
86
|
+
var dir: DirAccess = DirAccess.open(path)
|
|
87
|
+
|
|
88
|
+
if dir:
|
|
89
|
+
if dir.list_dir_begin() != OK:
|
|
90
|
+
return files
|
|
91
|
+
var file_name: String = dir.get_next()
|
|
92
|
+
|
|
93
|
+
while file_name != "":
|
|
94
|
+
if dir.current_is_dir() and not file_name.begins_with("."):
|
|
95
|
+
files.append_array(find_files(path + file_name + "/", extension))
|
|
96
|
+
elif file_name.ends_with(extension):
|
|
97
|
+
files.append(path + file_name)
|
|
98
|
+
|
|
99
|
+
file_name = dir.get_next()
|
|
100
|
+
|
|
101
|
+
return files
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Files under `path` whose extension is in `extensions`, which are passed without their dot.
|
|
105
|
+
func find_files_with_extensions(path: String, extensions: Array) -> Array[String]:
|
|
106
|
+
var files: Array[String] = []
|
|
107
|
+
if is_stepped_over(path) or is_installed_by_gdharness(path):
|
|
108
|
+
return files
|
|
109
|
+
|
|
110
|
+
var dir: DirAccess = DirAccess.open(path)
|
|
111
|
+
|
|
112
|
+
if dir:
|
|
113
|
+
if dir.list_dir_begin() != OK:
|
|
114
|
+
return files
|
|
115
|
+
var file_name: String = dir.get_next()
|
|
116
|
+
|
|
117
|
+
while file_name != "":
|
|
118
|
+
if file_name.begins_with("."):
|
|
119
|
+
file_name = dir.get_next()
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
var full_path: String = path + file_name
|
|
123
|
+
if dir.current_is_dir():
|
|
124
|
+
files.append_array(find_files_with_extensions(full_path + "/", extensions))
|
|
125
|
+
else:
|
|
126
|
+
var ext: String = file_name.get_extension().to_lower()
|
|
127
|
+
if ext in extensions:
|
|
128
|
+
files.append(full_path)
|
|
129
|
+
|
|
130
|
+
file_name = dir.get_next()
|
|
131
|
+
|
|
132
|
+
dir.list_dir_end()
|
|
133
|
+
|
|
134
|
+
return files
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://c16785cs9xozr
|
|
@@ -11,8 +11,9 @@ extends RefCounted
|
|
|
11
11
|
## both, so a property a built-in setter derives (a Control's offsets from its size) is left alone
|
|
12
12
|
## unless a script that is not a tool is involved.
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const GdhPropertyPaths = preload("property_paths.gd")
|
|
15
15
|
const GdhRead = preload("reading.gd")
|
|
16
|
+
const GdhSame = preload("same_values.gd")
|
|
16
17
|
|
|
17
18
|
const REMEDY: String = (
|
|
18
19
|
"Open the project's editor (editor_launch with hidden: true) and ask again: the write then goes "
|
|
@@ -78,7 +79,7 @@ static func differing(object: Object, before: Dictionary, names: Dictionary) ->
|
|
|
78
79
|
if names.has(name):
|
|
79
80
|
continue
|
|
80
81
|
var was: Variant = before[name] if before.has(name) else default_of(object, name)
|
|
81
|
-
if not
|
|
82
|
+
if not GdhSame.same(now[name], was):
|
|
82
83
|
changed.append(name)
|
|
83
84
|
return changed
|
|
84
85
|
|
|
@@ -122,28 +123,46 @@ static func _initialised(script: Script, name: String, type: int) -> Variant:
|
|
|
122
123
|
## nothing a script did: the saver writes a property whose default the engine cannot report whatever
|
|
123
124
|
## it holds, the editor's save as well.
|
|
124
125
|
static func at_default(object: Object, name: String) -> bool:
|
|
125
|
-
return
|
|
126
|
+
return GdhSame.same(object.get(name), default_of(object, name))
|
|
126
127
|
|
|
127
128
|
|
|
128
|
-
##
|
|
129
|
-
##
|
|
129
|
+
## What [param property] writes in the embedded resource it reaches into on [param object], by its
|
|
130
|
+
## id in the file, as the set of names written in it: the hp of the resource built in as inner for
|
|
131
|
+
## [code]inner/hp[/code], or position for [code]inner/position:x[/code]. Only those names are spared
|
|
132
|
+
## from the check for what a script changed: the resource's _init still ran when it was loaded, and
|
|
133
|
+
## sparing the whole resource saved what that set.
|
|
130
134
|
static func reached(object: Object, property: String) -> Dictionary:
|
|
131
135
|
var ids: Dictionary = {}
|
|
132
|
-
var
|
|
133
|
-
var
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
ids[id] = true
|
|
136
|
+
var place: Array = GdhPropertyPaths.holder_of(object, property)
|
|
137
|
+
var through: Array[Resource] = place[2]
|
|
138
|
+
if through.is_empty():
|
|
139
|
+
return ids
|
|
140
|
+
var holder: Resource = through.back()
|
|
141
|
+
var name: String = place[1]
|
|
142
|
+
if GdhPropertyPaths.declared_on(holder, name).is_empty():
|
|
143
|
+
name = name.get_slice(":", 0)
|
|
144
|
+
var id: String = holder.get_scene_unique_id()
|
|
145
|
+
if not id.is_empty():
|
|
146
|
+
ids[id] = {name: true}
|
|
144
147
|
return ids
|
|
145
148
|
|
|
146
149
|
|
|
150
|
+
## Adds what [param more] spares to [param into], both as [method reached] answers them; true for an
|
|
151
|
+
## id spares the whole resource, as one a tool rebuilt.
|
|
152
|
+
static func spare(into: Dictionary, more: Dictionary) -> void:
|
|
153
|
+
for id: Variant in more:
|
|
154
|
+
var held: Variant = into.get(id)
|
|
155
|
+
var given: Variant = more[id]
|
|
156
|
+
if held is bool or given is bool:
|
|
157
|
+
into[id] = true
|
|
158
|
+
elif held is Dictionary:
|
|
159
|
+
var names: Dictionary = held
|
|
160
|
+
var added: Dictionary = given
|
|
161
|
+
names.merge(added)
|
|
162
|
+
else:
|
|
163
|
+
into[id] = given
|
|
164
|
+
|
|
165
|
+
|
|
147
166
|
## Whether [param object] holds, at any depth, an embedded resource with a script that is not a tool.
|
|
148
167
|
static func holds_scripted_embedded(object: Object, seen: Dictionary) -> bool:
|
|
149
168
|
for value: Variant in stored(object).values():
|
|
@@ -276,7 +295,7 @@ static func reads_the_same(was: Variant, now: Variant, default: Variant) -> bool
|
|
|
276
295
|
return false
|
|
277
296
|
else:
|
|
278
297
|
values.append(str_to_var(str(text)))
|
|
279
|
-
return
|
|
298
|
+
return GdhSame.same(values[0], values[1])
|
|
280
299
|
|
|
281
300
|
|
|
282
301
|
static func _main_entries(text: String) -> Dictionary:
|
|
@@ -315,9 +334,9 @@ static func scripted_sections(text: String) -> Dictionary:
|
|
|
315
334
|
|
|
316
335
|
## Each property an embedded resource of [param after] sets differently from [param before], as
|
|
317
336
|
## [its id, name, whether before sets it, the text before gives it, the text after gives it or
|
|
318
|
-
## null]. Only those with a script that
|
|
319
|
-
##
|
|
320
|
-
## [
|
|
337
|
+
## null]. Only those with a script that is not a tool are compared, since only a script runs
|
|
338
|
+
## differently here, and what [param reached_ids] says the tool wrote is left out, as
|
|
339
|
+
## [method reached] answers it.
|
|
321
340
|
static func embedded_changes(before: String, after: String, reached_ids: Dictionary) -> Array[Array]:
|
|
322
341
|
var changed: Array[Array] = []
|
|
323
342
|
if before.is_empty():
|
|
@@ -327,8 +346,10 @@ static func embedded_changes(before: String, after: String, reached_ids: Diction
|
|
|
327
346
|
for key: Variant in scripted_sections(before):
|
|
328
347
|
var section: String = str(key)
|
|
329
348
|
var id: String = section.trim_prefix("sub:")
|
|
330
|
-
|
|
349
|
+
var spared: Variant = reached_ids.get(id, {})
|
|
350
|
+
if not section.begins_with("sub:") or spared is bool or not new.has(section):
|
|
331
351
|
continue
|
|
352
|
+
var written: Dictionary = spared
|
|
332
353
|
var was_section: Dictionary = old[section]
|
|
333
354
|
var now_section: Dictionary = new[section]
|
|
334
355
|
var was: Dictionary = entries_of(was_section)
|
|
@@ -337,7 +358,7 @@ static func embedded_changes(before: String, after: String, reached_ids: Diction
|
|
|
337
358
|
all.merge(now)
|
|
338
359
|
for entry: Variant in all:
|
|
339
360
|
var name: String = str(entry)
|
|
340
|
-
if was.get(name) != now.get(name):
|
|
361
|
+
if was.get(name) != now.get(name) and not written.has(name):
|
|
341
362
|
changed.append([id, name, was.has(name), str(was.get(name, "")), now.get(name)])
|
|
342
363
|
return changed
|
|
343
364
|
|