gdharness 1.3.3 → 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 +105 -74
- package/build/godot/addons/gdharness_editor/animation_reads.gd +3 -2
- 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/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 +195 -221
- 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 +97 -14
- package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
- package/build/godot/addons/gdharness_editor/tool_executor.gd +9 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +38 -35
- 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 +68 -2
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +94 -333
- 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 +3 -2
- package/build/godot/operations/declared_values.gd +157 -4
- package/build/godot/operations/dependencies.gd +14 -39
- package/build/godot/operations/editor_tools.gd +4 -3
- package/build/godot/operations/file_reads.gd +15 -3
- 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 +195 -221
- 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
|
@@ -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 = {
|
|
@@ -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
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends RefCounted
|
|
3
|
+
|
|
4
|
+
## Where a property a tool writes lives: on the object given, on a resource built into it, or on an
|
|
5
|
+
## element of one of its lists that the write has to make first.
|
|
6
|
+
|
|
7
|
+
const GdhRead = preload("reading.gd")
|
|
8
|
+
|
|
9
|
+
const HEADINGS: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## The entry [param target] declares for [param property], or {} when it declares none, headings
|
|
13
|
+
## left out.
|
|
14
|
+
static func declared_on(target: Object, property: String) -> Dictionary:
|
|
15
|
+
for declared: Dictionary in target.get_property_list():
|
|
16
|
+
if str(declared.get("name", "")) != property:
|
|
17
|
+
continue
|
|
18
|
+
if GdhRead.as_int(declared.get("usage", 0)) & HEADINGS:
|
|
19
|
+
continue
|
|
20
|
+
return declared
|
|
21
|
+
return {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Where [param key] is written: as [holder, name, through], the object holding the property, its
|
|
25
|
+
## name there, and the resources passed on the way. A key that is none of [param target]'s own may
|
|
26
|
+
## start with the name of one holding a resource built into it, then a slash and a key of that
|
|
27
|
+
## resource: "sources/0/0:0/0/probability" is the probability of tile 0:0 in the atlas source a
|
|
28
|
+
## TileSet keeps as sources/0. A colon path cannot reach it, since the tile's own name holds colons.
|
|
29
|
+
## A resource kept in a file of its own is not reached, nor, given the file [param home] that
|
|
30
|
+
## [param target] is saved in, one built into another file, since writing it here would leave that
|
|
31
|
+
## file as it was.
|
|
32
|
+
static func holder_of(target: Object, key: String, home: String = "") -> Array:
|
|
33
|
+
var through: Array[Resource] = []
|
|
34
|
+
var holder: Object = target
|
|
35
|
+
var name: String = key
|
|
36
|
+
while declared_on(holder, name).is_empty():
|
|
37
|
+
var reached: Resource = null
|
|
38
|
+
var at: int = name.find("/")
|
|
39
|
+
while at > 0:
|
|
40
|
+
var held: Resource = built_in_at(holder, name.left(at), home)
|
|
41
|
+
if held != null:
|
|
42
|
+
reached = held
|
|
43
|
+
break
|
|
44
|
+
at = name.find("/", at + 1)
|
|
45
|
+
if reached == null:
|
|
46
|
+
break
|
|
47
|
+
through.append(reached)
|
|
48
|
+
holder = reached
|
|
49
|
+
name = name.substr(at + 1)
|
|
50
|
+
return [holder, name, through]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## The resource [param target] holds under [param property] when it is built into a file, or null;
|
|
54
|
+
## given [param home], only one built into that file or into none yet.
|
|
55
|
+
static func built_in_at(target: Object, property: String, home: String = "") -> Resource:
|
|
56
|
+
var held: Resource = held_at(target, property)
|
|
57
|
+
if held == null or not held.is_built_in():
|
|
58
|
+
return null
|
|
59
|
+
var file: String = held.resource_path.get_slice("::", 0)
|
|
60
|
+
return held if home.is_empty() or file.is_empty() or file == home else null
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## The file [param target] is saved in: its scene's for a node, as the nodes of an instance are
|
|
64
|
+
## saved in the scene holding it, and its own for a resource; "" when it has none yet.
|
|
65
|
+
static func home_of(target: Object) -> String:
|
|
66
|
+
if target is Node:
|
|
67
|
+
var top: Node = target
|
|
68
|
+
while top.owner != null:
|
|
69
|
+
top = top.owner
|
|
70
|
+
return top.scene_file_path
|
|
71
|
+
if target is Resource:
|
|
72
|
+
var resource: Resource = target
|
|
73
|
+
return resource.resource_path.get_slice("::", 0)
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## The resource [param target] holds under [param property], built in or kept in a file, or null.
|
|
78
|
+
static func held_at(target: Object, property: String) -> Resource:
|
|
79
|
+
if declared_on(target, property).is_empty():
|
|
80
|
+
return null
|
|
81
|
+
var held: Variant = target.get(property)
|
|
82
|
+
return held if held is Resource else null
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## Makes the element of a list [param name] names on [param target] when it is the next one, as the
|
|
86
|
+
## inspector's Add Element does, and answers "" (also when [param name] is in no list), or why not,
|
|
87
|
+
## [param called] being what [param target] is called. The engine lists an element's properties
|
|
88
|
+
## only once the element exists, so a TileSet with no physics layer had no
|
|
89
|
+
## physics_layer_0/collision_layer to write. A list the engine declares as an array grows through
|
|
90
|
+
## the method named after its elements, add_physics_layer for physics_layer_, given the indices of
|
|
91
|
+
## the lists around it; a list counted by a property, as a tile's polygons are by polygons_count,
|
|
92
|
+
## grows by raising the count.
|
|
93
|
+
static func grow(target: Object, name: String, called: String) -> String:
|
|
94
|
+
var steps: PackedStringArray = name.split("/")
|
|
95
|
+
for depth: int in steps.size() - 1:
|
|
96
|
+
var step: String = steps[depth]
|
|
97
|
+
var cut: int = step.rfind("_")
|
|
98
|
+
if cut < 1 or not step.substr(cut + 1).is_valid_int():
|
|
99
|
+
continue
|
|
100
|
+
var index: int = step.substr(cut + 1).to_int()
|
|
101
|
+
var above: String = "/".join(steps.slice(0, depth))
|
|
102
|
+
var under: String = above + "/" if not above.is_empty() else ""
|
|
103
|
+
var prefix: String = under + step.left(cut + 1)
|
|
104
|
+
var element: String = step.left(cut)
|
|
105
|
+
var counted: String = under + element + "s_count"
|
|
106
|
+
var count: int = -1
|
|
107
|
+
var adding: Callable
|
|
108
|
+
if not declared_on(target, counted).is_empty():
|
|
109
|
+
count = GdhRead.as_int(target.get(counted))
|
|
110
|
+
adding = func() -> void: target.set(counted, count + 1)
|
|
111
|
+
elif _listed_array(target, prefix):
|
|
112
|
+
count = _elements(target, prefix)
|
|
113
|
+
var method: String = "add_" + element
|
|
114
|
+
var outer: Array = []
|
|
115
|
+
for enclosing: String in steps.slice(0, depth):
|
|
116
|
+
var at: int = enclosing.rfind("_")
|
|
117
|
+
if at > 0 and enclosing.substr(at + 1).is_valid_int():
|
|
118
|
+
outer.append(enclosing.substr(at + 1).to_int())
|
|
119
|
+
if target.has_method(method):
|
|
120
|
+
adding = func() -> void: target.callv(method, outer)
|
|
121
|
+
if count < 0 or index < count:
|
|
122
|
+
continue
|
|
123
|
+
var held: String = (
|
|
124
|
+
"%s has %d %s%s"
|
|
125
|
+
% [
|
|
126
|
+
above if not above.is_empty() else called,
|
|
127
|
+
count,
|
|
128
|
+
element.replace("_", " "),
|
|
129
|
+
"" if count == 1 else "s"
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
if index > count:
|
|
133
|
+
return (
|
|
134
|
+
"%s, and a new one goes after the last, so %s%d cannot be written yet: write %s%d first."
|
|
135
|
+
% [held, prefix, index, prefix, count]
|
|
136
|
+
)
|
|
137
|
+
if not adding.is_valid():
|
|
138
|
+
return "%s, and nothing makes another, so %s cannot be written." % [held, name]
|
|
139
|
+
adding.call()
|
|
140
|
+
return ""
|
|
141
|
+
return ""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
## Whether [param target] declares a list whose elements' properties start with [param prefix],
|
|
145
|
+
## as TileSet declares physics_layers for physics_layer_0/collision_layer.
|
|
146
|
+
static func _listed_array(target: Object, prefix: String) -> bool:
|
|
147
|
+
for declared: Dictionary in target.get_property_list():
|
|
148
|
+
var usage: int = GdhRead.as_int(declared.get("usage", 0))
|
|
149
|
+
if usage & PROPERTY_USAGE_ARRAY and str(declared.get("class_name", "")) == prefix:
|
|
150
|
+
return true
|
|
151
|
+
return false
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## How many elements of the list whose properties start with [param prefix] [param target] holds.
|
|
155
|
+
static func _elements(target: Object, prefix: String) -> int:
|
|
156
|
+
var count: int = 0
|
|
157
|
+
for declared: Dictionary in target.get_property_list():
|
|
158
|
+
var name: String = str(declared.get("name", ""))
|
|
159
|
+
if not name.begins_with(prefix):
|
|
160
|
+
continue
|
|
161
|
+
var index: String = name.substr(prefix.length()).get_slice("/", 0)
|
|
162
|
+
if index.is_valid_int():
|
|
163
|
+
count = maxi(count, index.to_int() + 1)
|
|
164
|
+
return count
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
## What [param target] is called in a refusal: the class_name its script declares, or else its
|
|
168
|
+
## engine class. A resource made as a project class is a Resource to the engine, and "Resource.kind"
|
|
169
|
+
## named a property no Resource has.
|
|
170
|
+
static func class_called(target: Object) -> String:
|
|
171
|
+
var attached: Variant = target.get_script()
|
|
172
|
+
if attached is Script:
|
|
173
|
+
var script: Script = attached
|
|
174
|
+
if not str(script.get_global_name()).is_empty():
|
|
175
|
+
return str(script.get_global_name())
|
|
176
|
+
return target.get_class()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
## The refusal for [param name], which [param target] does not declare, when it runs through a
|
|
180
|
+
## resource saved in a file other than [param home], the one [param target] is saved in; or "". The
|
|
181
|
+
## write belongs in that file, since this one would be saved and that one left as it was: through a
|
|
182
|
+
## TileMapLayer's tile_set kept in a file of its own, or through the mesh of an instance's editable
|
|
183
|
+
## child, which is built into the scene the instance comes from.
|
|
184
|
+
static func held_elsewhere(target: Object, name: String, home: String) -> String:
|
|
185
|
+
var at: int = name.find("/")
|
|
186
|
+
while at > 0:
|
|
187
|
+
var head: String = name.left(at)
|
|
188
|
+
var rest: String = name.substr(at + 1)
|
|
189
|
+
var held: Resource = held_at(target, head)
|
|
190
|
+
var file: String = "" if held == null else held.resource_path.get_slice("::", 0)
|
|
191
|
+
if held != null and not held.is_built_in():
|
|
192
|
+
return (
|
|
193
|
+
"%s.%s is %s, a file of its own, so %s is written there, with resource_edit modify on %s."
|
|
194
|
+
% [class_called(target), head, file, rest, file]
|
|
195
|
+
)
|
|
196
|
+
if held != null and not file.is_empty() and not home.is_empty() and file != home:
|
|
197
|
+
return _built_in_elsewhere(target, head, rest, held, file)
|
|
198
|
+
at = name.find("/", at + 1)
|
|
199
|
+
return ""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
## The refusal for writing [param rest] into [param held], which [param target] holds under
|
|
203
|
+
## [param head] and which is built into [param file], the scene an instance in this one comes from.
|
|
204
|
+
static func _built_in_elsewhere(
|
|
205
|
+
target: Object, head: String, rest: String, held: Resource, file: String
|
|
206
|
+
) -> String:
|
|
207
|
+
var node: String = ""
|
|
208
|
+
if target is Node:
|
|
209
|
+
var placed: Node = target
|
|
210
|
+
var from: Node = placed
|
|
211
|
+
while from != null and from.scene_file_path != file:
|
|
212
|
+
from = from.owner
|
|
213
|
+
if from != null:
|
|
214
|
+
node = " on %s" % from.get_path_to(placed)
|
|
215
|
+
var text: String = (
|
|
216
|
+
(
|
|
217
|
+
"%s.%s is built into %s, so %s is written there, with scene_node set%s in %s, which changes "
|
|
218
|
+
+ "it wherever that scene is used. To change it here alone, give %s a %s of its own: a "
|
|
219
|
+
+ 'value with "_type" set to its class and its properties beside it.'
|
|
220
|
+
)
|
|
221
|
+
% [class_called(target), head, file, rest, node, file, head, held.get_class()]
|
|
222
|
+
)
|
|
223
|
+
if target is GeometryInstance3D and (held is Material or rest.contains("material")):
|
|
224
|
+
text += " A material for this instance alone goes in material_override."
|
|
225
|
+
return text
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://mi7vnabydtlw
|