gdharness 1.2.14 → 1.2.16
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 +393 -80
- package/build/godot/addons/gdharness_editor/property_values.gd +28 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +27 -8
- package/build/godot/addons/gdharness_editor/serialisation.gd +20 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +4 -1
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +172 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +60 -47
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +46 -4
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +13 -3
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +1 -17
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +29 -3
- package/build/godot/addons/gdharness_runtime/serialisation.gd +20 -0
- package/build/godot/operations/audio_buses.gd +35 -0
- package/build/godot/operations/classdb_queries.gd +31 -2
- package/build/godot/operations/file_walk.gd +9 -2
- package/build/godot/operations/gdscript_authoring.gd +4 -5
- package/build/godot/operations/godot_operations.gd +4 -0
- package/build/godot/operations/input_actions.gd +27 -0
- package/build/godot/operations/project_config.gd +8 -1
- package/build/godot/operations/project_diagnostics.gd +9 -5
- package/build/godot/operations/serialisation.gd +20 -0
- package/build/index.js +380 -66
- package/package.json +1 -1
|
@@ -17,6 +17,16 @@ extends RefCounted
|
|
|
17
17
|
const Read = preload("reading.gd")
|
|
18
18
|
const Serialisation = preload("serialisation.gd")
|
|
19
19
|
|
|
20
|
+
# The data types a Theme names its items by, as in "Button/colors/font_color", and what each holds.
|
|
21
|
+
const THEME_ITEMS: Dictionary[String, Array] = {
|
|
22
|
+
"colors": [TYPE_COLOR, ""],
|
|
23
|
+
"constants": [TYPE_INT, ""],
|
|
24
|
+
"font_sizes": [TYPE_INT, ""],
|
|
25
|
+
"fonts": [TYPE_OBJECT, "Font"],
|
|
26
|
+
"icons": [TYPE_OBJECT, "Texture2D"],
|
|
27
|
+
"styles": [TYPE_OBJECT, "StyleBox"],
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
# What each packed array holds, for the four whose elements are not types JSON has. A polygon is
|
|
21
31
|
# written as a list of pairs, and only the property knows that each pair is a Vector2 rather than a
|
|
22
32
|
# list of two numbers: without being told, every element was read as itself and the engine turned
|
|
@@ -394,9 +404,27 @@ func declaration(target: Object, property: String) -> Dictionary:
|
|
|
394
404
|
if Read.as_int(declared.get("usage", 0)) & HEADINGS:
|
|
395
405
|
continue
|
|
396
406
|
return declared
|
|
407
|
+
if target is Theme:
|
|
408
|
+
return _theme_item(property)
|
|
397
409
|
return {}
|
|
398
410
|
|
|
399
411
|
|
|
412
|
+
## The entry a Theme would declare for [param property] once it held that item, or {} when the name
|
|
413
|
+
## is not an item's. A Theme lists only the items it holds and takes any other through set, so an
|
|
414
|
+
## item a read of one theme answered with was refused when written into another that lacked it.
|
|
415
|
+
static func _theme_item(property: String) -> Dictionary:
|
|
416
|
+
var parts: PackedStringArray = property.split("/")
|
|
417
|
+
if parts.size() != 3 or parts[0].is_empty() or parts[2].is_empty() or not THEME_ITEMS.has(parts[1]):
|
|
418
|
+
return {}
|
|
419
|
+
var item: Array = THEME_ITEMS[parts[1]]
|
|
420
|
+
var kind: int = item[0]
|
|
421
|
+
var made: Dictionary = {"name": property, "type": kind, "usage": PROPERTY_USAGE_DEFAULT}
|
|
422
|
+
if kind == TYPE_OBJECT:
|
|
423
|
+
made["hint"] = PROPERTY_HINT_RESOURCE_TYPE
|
|
424
|
+
made["hint_string"] = item[1]
|
|
425
|
+
return made
|
|
426
|
+
|
|
427
|
+
|
|
400
428
|
## Whether [param held] is [param given], as a property read back after a set answers it.
|
|
401
429
|
##
|
|
402
430
|
## Close enough rather than equal for anything made of floats: a Vector2 property stores 32-bit
|
|
@@ -124,21 +124,40 @@ func fixed(node: Node) -> String:
|
|
|
124
124
|
return ""
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
## Why
|
|
128
|
-
##
|
|
129
|
-
##
|
|
130
|
-
func
|
|
131
|
-
|
|
127
|
+
## Why the folder a new file at [param file_path] goes in cannot be made, or "" once it is there. The
|
|
128
|
+
## engine makes no folder when it saves, so a create into one not yet made failed with no more than
|
|
129
|
+
## the engine's error, while script_edit create made its folder and went ahead.
|
|
130
|
+
static func make_folder_for(file_path: String) -> String:
|
|
131
|
+
var folder: String = file_path.get_base_dir()
|
|
132
|
+
if DirAccess.dir_exists_absolute(folder):
|
|
133
|
+
return ""
|
|
134
|
+
var made: Error = DirAccess.make_dir_recursive_absolute(folder)
|
|
135
|
+
return "" if made == OK else "Could not make the folder %s: %s" % [folder, error_string(made)]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
## Why no node can be called [param node_name], wherever it sits, or "".
|
|
139
|
+
static func unfit_name(node_name: String) -> String:
|
|
140
|
+
if node_name.is_empty():
|
|
132
141
|
return "A node needs a name."
|
|
133
|
-
var valid: String =
|
|
134
|
-
if valid !=
|
|
142
|
+
var valid: String = node_name.validate_node_name()
|
|
143
|
+
if valid != node_name:
|
|
135
144
|
return (
|
|
136
145
|
(
|
|
137
146
|
"%s is not a name a node can have; Godot would save it as %s. The characters "
|
|
138
147
|
+ '. : @ / " %% cannot be in a node name.'
|
|
139
148
|
)
|
|
140
|
-
% [
|
|
149
|
+
% [node_name, valid]
|
|
141
150
|
)
|
|
151
|
+
return ""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## Why [param child_name] cannot be given to a child of [param parent], or "". Godot renames a node
|
|
155
|
+
## whose name is taken or holds a character a path cannot, and the next call naming it would then
|
|
156
|
+
## reach another node.
|
|
157
|
+
func unusable_name(parent: Node, child_name: String, moving: Node = null) -> String:
|
|
158
|
+
var unfit: String = unfit_name(child_name)
|
|
159
|
+
if not unfit.is_empty():
|
|
160
|
+
return unfit
|
|
142
161
|
var taken: Node = parent.get_node_or_null(NodePath(child_name))
|
|
143
162
|
if taken != null and taken != moving and taken.get_parent() == parent:
|
|
144
163
|
return "%s already has a child named %s." % [path_of(parent), child_name]
|
|
@@ -540,6 +540,9 @@ func _serialize_colors(value: PackedColorArray) -> Array:
|
|
|
540
540
|
|
|
541
541
|
# A pathless Resource says so rather than inventing an empty path for the caller to load.
|
|
542
542
|
func _serialize_object(value: Object) -> Variant:
|
|
543
|
+
if value is InputEvent:
|
|
544
|
+
var event: InputEvent = value
|
|
545
|
+
return _describe_input_event(event)
|
|
543
546
|
if value is Resource:
|
|
544
547
|
var resource: Resource = value
|
|
545
548
|
if resource.resource_path.is_empty():
|
|
@@ -552,6 +555,23 @@ func _serialize_object(value: Object) -> Variant:
|
|
|
552
555
|
return _describe_object(value)
|
|
553
556
|
|
|
554
557
|
|
|
558
|
+
## An input event, by what it is bound to: the engine's own words for it and every field it stores
|
|
559
|
+
## away from its default. As a bare resource it was its class and nothing else, so an input action
|
|
560
|
+
## read back from project.godot could not say whether it was bound to Space or to a joypad button.
|
|
561
|
+
func _describe_input_event(event: InputEvent) -> Dictionary:
|
|
562
|
+
var described: Dictionary = {"class": event.get_class(), "_type": "Resource", "text": event.as_text()}
|
|
563
|
+
for property: Dictionary in event.get_property_list():
|
|
564
|
+
var usage: int = property.get("usage", 0)
|
|
565
|
+
var name: String = str(property.get("name", ""))
|
|
566
|
+
if not (usage & PROPERTY_USAGE_STORAGE) or name.begins_with("resource_") or name == "script":
|
|
567
|
+
continue
|
|
568
|
+
var held: Variant = event.get(name)
|
|
569
|
+
if held == ClassDB.class_get_property_default_value(event.get_class(), name):
|
|
570
|
+
continue
|
|
571
|
+
described[name] = serialize_value(held)
|
|
572
|
+
return described
|
|
573
|
+
|
|
574
|
+
|
|
555
575
|
## A plain object, said in enough detail to tell it from the one beside it.
|
|
556
576
|
##
|
|
557
577
|
## `{"class": "RefCounted"}` was all of it, so a roster of twelve people came back as the word
|
|
@@ -104,12 +104,12 @@ func _init_tools() -> void:
|
|
|
104
104
|
# Resource tools
|
|
105
105
|
"create_resource": [_resource_tools, "create_resource"],
|
|
106
106
|
"modify_resource": [_resource_tools, "modify_resource"],
|
|
107
|
+
"read_resource": [_resource_tools, "read_resource"],
|
|
107
108
|
"create_shader": [_resource_tools, "create_shader"],
|
|
108
109
|
"create_tileset": [_resource_tools, "create_tileset"],
|
|
109
110
|
"set_tilemap_cells": [_resource_tools, "set_tilemap_cells"],
|
|
110
111
|
"set_theme_color": [_resource_tools, "set_theme_color"],
|
|
111
112
|
"set_theme_font_size": [_resource_tools, "set_theme_font_size"],
|
|
112
|
-
# Animation tools
|
|
113
113
|
"play_scene": [_play_tools, "play_scene"],
|
|
114
114
|
"restart_editor": [_play_tools, "restart_editor"],
|
|
115
115
|
"quit_editor": [_play_tools, "quit_editor"],
|
|
@@ -118,6 +118,9 @@ func _init_tools() -> void:
|
|
|
118
118
|
# The script profiler on a game the editor plays
|
|
119
119
|
"profile_start": [_profile_tools, "profile_start"],
|
|
120
120
|
"profile_read": [_profile_tools, "profile_read"],
|
|
121
|
+
# Animation tools
|
|
122
|
+
"read_animations": [_animation_tools, "read_animations"],
|
|
123
|
+
"read_animation_states": [_animation_tools, "read_animation_states"],
|
|
121
124
|
"create_animation": [_animation_tools, "create_animation"],
|
|
122
125
|
"add_animation_track": [_animation_tools, "add_animation_track"],
|
|
123
126
|
"add_animation_state": [_animation_tools, "add_animation_state"],
|
|
@@ -6,9 +6,33 @@ extends Node
|
|
|
6
6
|
const PropertyValues = preload("../property_values.gd")
|
|
7
7
|
const Read = preload("../reading.gd")
|
|
8
8
|
const SceneFile = preload("../scene_file.gd")
|
|
9
|
+
const Serialisation = preload("../serialisation.gd")
|
|
10
|
+
|
|
11
|
+
const LOOP_MODE_NAMES: Dictionary[int, String] = {
|
|
12
|
+
Animation.LOOP_NONE: "none",
|
|
13
|
+
Animation.LOOP_LINEAR: "linear",
|
|
14
|
+
Animation.LOOP_PINGPONG: "pingpong",
|
|
15
|
+
}
|
|
16
|
+
const TRACK_TYPE_NAMES: Dictionary[int, String] = {
|
|
17
|
+
Animation.TYPE_VALUE: "property",
|
|
18
|
+
Animation.TYPE_POSITION_3D: "position_3d",
|
|
19
|
+
Animation.TYPE_ROTATION_3D: "rotation_3d",
|
|
20
|
+
Animation.TYPE_SCALE_3D: "scale_3d",
|
|
21
|
+
Animation.TYPE_BLEND_SHAPE: "blend_shape",
|
|
22
|
+
Animation.TYPE_METHOD: "method",
|
|
23
|
+
Animation.TYPE_BEZIER: "bezier",
|
|
24
|
+
Animation.TYPE_AUDIO: "audio",
|
|
25
|
+
Animation.TYPE_ANIMATION: "animation",
|
|
26
|
+
}
|
|
27
|
+
const SWITCH_MODE_NAMES: Dictionary[int, String] = {
|
|
28
|
+
AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE: "immediate",
|
|
29
|
+
AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC: "sync",
|
|
30
|
+
AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END: "at_end",
|
|
31
|
+
}
|
|
9
32
|
|
|
10
33
|
var _editor_plugin: EditorPlugin = null
|
|
11
34
|
var _properties: PropertyValues = PropertyValues.new()
|
|
35
|
+
var _values: Serialisation = Serialisation.new()
|
|
12
36
|
|
|
13
37
|
|
|
14
38
|
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
@@ -47,6 +71,19 @@ func _open_at(scene_path: String, node_path: String, kind: String) -> Array:
|
|
|
47
71
|
return [scene, node, {}]
|
|
48
72
|
|
|
49
73
|
|
|
74
|
+
## [param scene_path] opened for reading, as [method _open_at] answers. A node an instanced scene
|
|
75
|
+
## owns is read like any other, since nothing is written.
|
|
76
|
+
func _read_at(scene_path: String, node_path: String, kind: String) -> Array:
|
|
77
|
+
var scene: SceneFile = SceneFile.new()
|
|
78
|
+
var refused: Dictionary = scene.open(scene_path, false)
|
|
79
|
+
if not refused.is_empty():
|
|
80
|
+
return [scene, null, refused]
|
|
81
|
+
var node: Node = _find_node(scene.root, node_path)
|
|
82
|
+
if node == null or not node.is_class(kind):
|
|
83
|
+
return [scene, null, scene.refuse("%s not found at: %s" % [kind, node_path])]
|
|
84
|
+
return [scene, node, {}]
|
|
85
|
+
|
|
86
|
+
|
|
50
87
|
## Writes [param scene] once [param check] passes, then the files of [param homes], answering
|
|
51
88
|
## [param answer] or a refusal.
|
|
52
89
|
func _finish(scene: SceneFile, check: Callable, homes: Dictionary, answer: Dictionary) -> Dictionary:
|
|
@@ -181,6 +218,141 @@ func _animated(player: AnimationPlayer, track_path: String) -> Node:
|
|
|
181
218
|
return from.get_node_or_null(NodePath(track_path))
|
|
182
219
|
|
|
183
220
|
|
|
221
|
+
# =============================================================================
|
|
222
|
+
# read_animations, read_animation_states
|
|
223
|
+
# =============================================================================
|
|
224
|
+
## The animations of an AnimationPlayer, or one of them whole. A track comes back in the shape
|
|
225
|
+
## add_track takes, so one read here can be written to another animation as it stands.
|
|
226
|
+
func read_animations(args: Dictionary) -> Dictionary:
|
|
227
|
+
var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
|
|
228
|
+
var player_node_path: String = str(args.get("playerNodePath", "."))
|
|
229
|
+
var animation_name: String = str(args.get("animationName", ""))
|
|
230
|
+
if scene_path.strip_edges() == "res://":
|
|
231
|
+
return {"ok": false, "error": "Missing scenePath"}
|
|
232
|
+
|
|
233
|
+
var opened: Array = _read_at(scene_path, player_node_path, "AnimationPlayer")
|
|
234
|
+
var scene: SceneFile = opened[0]
|
|
235
|
+
if opened[1] == null:
|
|
236
|
+
return opened[2]
|
|
237
|
+
var player: AnimationPlayer = opened[1]
|
|
238
|
+
var answer: Dictionary = {"ok": true, "unsavedInEditor": SceneFile.unsaved(scene_path)}
|
|
239
|
+
if animation_name.is_empty():
|
|
240
|
+
var listed: Array[Dictionary] = []
|
|
241
|
+
for called: String in player.get_animation_list():
|
|
242
|
+
var listed_one: Animation = player.get_animation(StringName(called))
|
|
243
|
+
var summary: Dictionary = _summary(listed_one)
|
|
244
|
+
summary["animationName"] = called
|
|
245
|
+
summary["tracks"] = listed_one.get_track_count()
|
|
246
|
+
listed.append(summary)
|
|
247
|
+
answer["animations"] = listed
|
|
248
|
+
scene.close()
|
|
249
|
+
return answer
|
|
250
|
+
if not player.has_animation(StringName(animation_name)):
|
|
251
|
+
var names: PackedStringArray = player.get_animation_list()
|
|
252
|
+
return scene.refuse(
|
|
253
|
+
(
|
|
254
|
+
"%s has no animation %s. It has: %s."
|
|
255
|
+
% [player.name, animation_name, ", ".join(names) if not names.is_empty() else "none"]
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
var anim: Animation = player.get_animation(StringName(animation_name))
|
|
260
|
+
answer.merge(_summary(anim))
|
|
261
|
+
answer["animationName"] = animation_name
|
|
262
|
+
var tracks: Array[Dictionary] = []
|
|
263
|
+
for track: int in anim.get_track_count():
|
|
264
|
+
tracks.append(_track(anim, track))
|
|
265
|
+
answer["tracks"] = tracks
|
|
266
|
+
scene.close()
|
|
267
|
+
return answer
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
func _summary(anim: Animation) -> Dictionary:
|
|
271
|
+
return {
|
|
272
|
+
"length": anim.length,
|
|
273
|
+
"loopMode": LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
|
|
274
|
+
"step": anim.step,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
func _track(anim: Animation, track: int) -> Dictionary:
|
|
279
|
+
var kind: int = anim.track_get_type(track)
|
|
280
|
+
var path: NodePath = anim.track_get_path(track)
|
|
281
|
+
var shaped: Dictionary = {
|
|
282
|
+
"type": TRACK_TYPE_NAMES.get(kind, str(kind)), "nodePath": str(path.get_concatenated_names())
|
|
283
|
+
}
|
|
284
|
+
var property: String = str(path.get_concatenated_subnames())
|
|
285
|
+
if not property.is_empty():
|
|
286
|
+
shaped["property"] = property
|
|
287
|
+
var keyframes: Array[Dictionary] = []
|
|
288
|
+
var methods: Dictionary = {}
|
|
289
|
+
for key: int in anim.track_get_key_count(track):
|
|
290
|
+
var keyframe: Dictionary = {"time": anim.track_get_key_time(track, key)}
|
|
291
|
+
if kind == Animation.TYPE_METHOD:
|
|
292
|
+
var method: String = str(anim.method_track_get_name(track, key))
|
|
293
|
+
methods[method] = true
|
|
294
|
+
keyframe["method"] = method
|
|
295
|
+
keyframe["args"] = _values.serialize_value(anim.method_track_get_params(track, key))
|
|
296
|
+
else:
|
|
297
|
+
keyframe["value"] = _values.serialize_value(anim.track_get_key_value(track, key))
|
|
298
|
+
keyframes.append(keyframe)
|
|
299
|
+
# One method across every key is the track's, as add_track takes it. Keys calling different
|
|
300
|
+
# methods, which the editor can make and add_track cannot, keep their own.
|
|
301
|
+
if methods.size() == 1:
|
|
302
|
+
shaped["method"] = methods.keys()[0]
|
|
303
|
+
for keyframe: Dictionary in keyframes:
|
|
304
|
+
var _shared: bool = keyframe.erase("method")
|
|
305
|
+
shaped["keyframes"] = keyframes
|
|
306
|
+
return shaped
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
## The states and transitions of a state machine in an AnimationTree.
|
|
310
|
+
func read_animation_states(args: Dictionary) -> Dictionary:
|
|
311
|
+
var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
|
|
312
|
+
var anim_tree_path: String = str(args.get("animTreePath", ""))
|
|
313
|
+
var state_machine_path: String = str(args.get("stateMachinePath", ""))
|
|
314
|
+
if scene_path.strip_edges() == "res://":
|
|
315
|
+
return {"ok": false, "error": "Missing scenePath"}
|
|
316
|
+
if anim_tree_path.is_empty():
|
|
317
|
+
return {"ok": false, "error": "Missing animTreePath"}
|
|
318
|
+
|
|
319
|
+
var opened: Array = _read_at(scene_path, anim_tree_path, "AnimationTree")
|
|
320
|
+
var scene: SceneFile = opened[0]
|
|
321
|
+
if opened[1] == null:
|
|
322
|
+
return opened[2]
|
|
323
|
+
var anim_tree: AnimationTree = opened[1]
|
|
324
|
+
var sm: AnimationNodeStateMachine = _get_state_machine(anim_tree, state_machine_path)
|
|
325
|
+
if sm == null:
|
|
326
|
+
return scene.refuse("AnimationNodeStateMachine not found")
|
|
327
|
+
|
|
328
|
+
var states: Array[Dictionary] = []
|
|
329
|
+
for state: StringName in sm.get_node_list():
|
|
330
|
+
var node: AnimationNode = sm.get_node(state)
|
|
331
|
+
var entry: Dictionary = {"stateName": str(state), "type": node.get_class()}
|
|
332
|
+
if node is AnimationNodeAnimation:
|
|
333
|
+
var plays: AnimationNodeAnimation = node
|
|
334
|
+
entry["animationName"] = str(plays.animation)
|
|
335
|
+
states.append(entry)
|
|
336
|
+
var transitions: Array[Dictionary] = []
|
|
337
|
+
for index: int in sm.get_transition_count():
|
|
338
|
+
var transition: AnimationNodeStateMachineTransition = sm.get_transition(index)
|
|
339
|
+
var shaped: Dictionary = {
|
|
340
|
+
"from": str(sm.get_transition_from(index)),
|
|
341
|
+
"to": str(sm.get_transition_to(index)),
|
|
342
|
+
"transitionType": SWITCH_MODE_NAMES.get(transition.switch_mode, str(transition.switch_mode)),
|
|
343
|
+
}
|
|
344
|
+
if not str(transition.advance_condition).is_empty():
|
|
345
|
+
shaped["advanceCondition"] = str(transition.advance_condition)
|
|
346
|
+
transitions.append(shaped)
|
|
347
|
+
scene.close()
|
|
348
|
+
return {
|
|
349
|
+
"ok": true,
|
|
350
|
+
"states": states,
|
|
351
|
+
"transitions": transitions,
|
|
352
|
+
"unsavedInEditor": SceneFile.unsaved(scene_path),
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
|
|
184
356
|
# =============================================================================
|
|
185
357
|
# create_animation
|
|
186
358
|
# =============================================================================
|
|
@@ -6,51 +6,21 @@ extends Node
|
|
|
6
6
|
const PropertyValues = preload("../property_values.gd")
|
|
7
7
|
const Read = preload("../reading.gd")
|
|
8
8
|
const SceneFile = preload("../scene_file.gd")
|
|
9
|
+
const Serialisation = preload("../serialisation.gd")
|
|
9
10
|
|
|
10
|
-
#
|
|
11
|
-
# what ends up in the .gdshader can be read here. The %s is the shader type.
|
|
11
|
+
# The minimal shader of a type: its declaration and the one function the engine calls for it.
|
|
12
12
|
const SHADER_EMPTY: String = """shader_type %s;
|
|
13
13
|
|
|
14
|
-
void
|
|
14
|
+
void %s() {
|
|
15
15
|
}
|
|
16
16
|
"""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
void fragment() {
|
|
21
|
-
COLOR = vec4(1.0);
|
|
22
|
-
}
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
const SHADER_COLOR_SHIFT: String = """shader_type %s;
|
|
26
|
-
|
|
27
|
-
uniform vec4 color_shift : source_color = vec4(0.1, 0.0, 0.2, 0.0);
|
|
28
|
-
|
|
29
|
-
void fragment() {
|
|
30
|
-
vec4 base = texture(TEXTURE, UV);
|
|
31
|
-
COLOR = vec4(clamp(base.rgb + color_shift.rgb, 0.0, 1.0), base.a);
|
|
17
|
+
const SHADER_ENTRY: Dictionary[String, String] = {
|
|
18
|
+
"canvas_item": "fragment", "spatial": "fragment", "particles": "process", "sky": "sky", "fog": "fog"
|
|
32
19
|
}
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
const SHADER_OUTLINE: String = """shader_type %s;
|
|
36
|
-
|
|
37
|
-
uniform vec4 outline_color : source_color = vec4(0.0, 0.0, 0.0, 1.0);
|
|
38
|
-
uniform float outline_width : hint_range(0.0, 8.0) = 1.0;
|
|
39
|
-
|
|
40
|
-
void fragment() {
|
|
41
|
-
vec2 px = TEXTURE_PIXEL_SIZE * outline_width;
|
|
42
|
-
float a = texture(TEXTURE, UV).a;
|
|
43
|
-
float edge = max(
|
|
44
|
-
max(texture(TEXTURE, UV + vec2(px.x, 0.0)).a, texture(TEXTURE, UV - vec2(px.x, 0.0)).a),
|
|
45
|
-
max(texture(TEXTURE, UV + vec2(0.0, px.y)).a, texture(TEXTURE, UV - vec2(0.0, px.y)).a)
|
|
46
|
-
);
|
|
47
|
-
vec4 base = texture(TEXTURE, UV);
|
|
48
|
-
COLOR = mix(outline_color * edge, base, a);
|
|
49
|
-
}
|
|
50
|
-
"""
|
|
51
20
|
|
|
52
21
|
var _editor_plugin: EditorPlugin = null
|
|
53
22
|
var _properties: PropertyValues = PropertyValues.new()
|
|
23
|
+
var _values: Serialisation = Serialisation.new()
|
|
54
24
|
|
|
55
25
|
|
|
56
26
|
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
@@ -217,6 +187,9 @@ func create_resource(args: Dictionary) -> Dictionary:
|
|
|
217
187
|
if not refused.is_empty():
|
|
218
188
|
return {"ok": false, "error": refused}
|
|
219
189
|
|
|
190
|
+
var folder: String = SceneFile.make_folder_for(res_path)
|
|
191
|
+
if not folder.is_empty():
|
|
192
|
+
return {"ok": false, "error": folder}
|
|
220
193
|
var failed: String = _save_checked(resource, res_path, properties.keys(), null)
|
|
221
194
|
if not failed.is_empty():
|
|
222
195
|
return {"ok": false, "error": failed}
|
|
@@ -260,6 +233,50 @@ func modify_resource(args: Dictionary) -> Dictionary:
|
|
|
260
233
|
return {"ok": true, "resourcePath": res_path}
|
|
261
234
|
|
|
262
235
|
|
|
236
|
+
## What a resource file holds, in the form modify takes, so a value read here can be written back.
|
|
237
|
+
## Read from the file rather than the editor's cached copy, since the file is what the other ops
|
|
238
|
+
## write and what the project loads.
|
|
239
|
+
func read_resource(args: Dictionary) -> Dictionary:
|
|
240
|
+
var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
|
|
241
|
+
var include_defaults: bool = Read.as_bool(args.get("includeDefaults", false))
|
|
242
|
+
if res_path == "res://":
|
|
243
|
+
return {"ok": false, "error": "resourcePath is required"}
|
|
244
|
+
if not FileAccess.file_exists(res_path):
|
|
245
|
+
return {"ok": false, "error": "No resource at " + res_path}
|
|
246
|
+
var resource: Resource = ResourceLoader.load(res_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
247
|
+
if resource == null:
|
|
248
|
+
return {"ok": false, "error": "%s did not load as a resource." % res_path}
|
|
249
|
+
|
|
250
|
+
var script: Script = resource.get_script()
|
|
251
|
+
var scripted: Dictionary = {}
|
|
252
|
+
if script != null:
|
|
253
|
+
for declared: Dictionary in script.get_script_property_list():
|
|
254
|
+
scripted[str(declared.get("name", ""))] = true
|
|
255
|
+
var properties: Dictionary = {}
|
|
256
|
+
for p: Dictionary in resource.get_property_list():
|
|
257
|
+
var property: String = str(p.get("name", ""))
|
|
258
|
+
if property.is_empty() or property == "script":
|
|
259
|
+
continue
|
|
260
|
+
if not (Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
261
|
+
continue
|
|
262
|
+
var value: Variant = resource.get(property)
|
|
263
|
+
if not include_defaults:
|
|
264
|
+
var default: Variant = (
|
|
265
|
+
script.get_property_default_value(property)
|
|
266
|
+
if scripted.has(property)
|
|
267
|
+
else ClassDB.class_get_property_default_value(resource.get_class(), property)
|
|
268
|
+
)
|
|
269
|
+
if PropertyValues.same(value, default):
|
|
270
|
+
continue
|
|
271
|
+
properties[property] = _values.serialize_value(value)
|
|
272
|
+
var answer: Dictionary = {
|
|
273
|
+
"ok": true, "resourcePath": res_path, "resourceType": resource.get_class(), "properties": properties
|
|
274
|
+
}
|
|
275
|
+
if script != null:
|
|
276
|
+
answer["script"] = script.resource_path
|
|
277
|
+
return answer
|
|
278
|
+
|
|
279
|
+
|
|
263
280
|
func create_shader(args: Dictionary) -> Dictionary:
|
|
264
281
|
var shader_path: String = _ensure_res_path(str(args.get("shaderPath", "")))
|
|
265
282
|
var shader_type: String = str(args.get("shaderType", "canvas_item"))
|
|
@@ -270,19 +287,12 @@ func create_shader(args: Dictionary) -> Dictionary:
|
|
|
270
287
|
return {"ok": false, "error": taken}
|
|
271
288
|
|
|
272
289
|
var code: String = str(args.get("code", ""))
|
|
273
|
-
var template: String = str(args.get("template", ""))
|
|
274
|
-
|
|
275
290
|
if code == "":
|
|
276
|
-
|
|
277
|
-
"basic":
|
|
278
|
-
code = SHADER_BASIC % shader_type
|
|
279
|
-
"color_shift":
|
|
280
|
-
code = SHADER_COLOR_SHIFT % shader_type
|
|
281
|
-
"outline":
|
|
282
|
-
code = SHADER_OUTLINE % shader_type
|
|
283
|
-
_:
|
|
284
|
-
code = SHADER_EMPTY % shader_type
|
|
291
|
+
code = SHADER_EMPTY % [shader_type, SHADER_ENTRY.get(shader_type, "fragment")]
|
|
285
292
|
|
|
293
|
+
var folder: String = SceneFile.make_folder_for(shader_path)
|
|
294
|
+
if not folder.is_empty():
|
|
295
|
+
return {"ok": false, "error": folder}
|
|
286
296
|
var file: FileAccess = FileAccess.open(shader_path, FileAccess.WRITE)
|
|
287
297
|
if file == null:
|
|
288
298
|
return {
|
|
@@ -335,6 +345,9 @@ func create_tileset(args: Dictionary) -> Dictionary:
|
|
|
335
345
|
{"sourceId": added, "texture": atlas.texture.resource_path, "tiles": atlas.get_tiles_count()}
|
|
336
346
|
)
|
|
337
347
|
|
|
348
|
+
var folder: String = SceneFile.make_folder_for(tileset_path)
|
|
349
|
+
if not folder.is_empty():
|
|
350
|
+
return {"ok": false, "error": folder}
|
|
338
351
|
var saving: Error = ResourceSaver.save(tileset, tileset_path)
|
|
339
352
|
if saving != OK:
|
|
340
353
|
return {"ok": false, "error": "Failed to save %s: %s" % [tileset_path, error_string(saving)]}
|
|
@@ -8,6 +8,16 @@ const PropertyValues = preload("../property_values.gd")
|
|
|
8
8
|
const SceneFile = preload("../scene_file.gd")
|
|
9
9
|
const Serialisation = preload("../serialisation.gd")
|
|
10
10
|
|
|
11
|
+
## The names Object.ConnectFlags gives each bit. The number alone left a reader guessing that 2 is
|
|
12
|
+
## the persistence every connection in a saved scene carries.
|
|
13
|
+
const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
|
|
14
|
+
Object.CONNECT_DEFERRED: "deferred",
|
|
15
|
+
Object.CONNECT_PERSIST: "persist",
|
|
16
|
+
Object.CONNECT_ONE_SHOT: "one_shot",
|
|
17
|
+
Object.CONNECT_REFERENCE_COUNTED: "reference_counted",
|
|
18
|
+
Object.CONNECT_APPEND_SOURCE_OBJECT: "append_source_object",
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
var _editor_plugin: EditorPlugin = null
|
|
12
22
|
var _values: Serialisation = Serialisation.new()
|
|
13
23
|
var _properties: PropertyValues = PropertyValues.new()
|
|
@@ -17,6 +27,19 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
|
17
27
|
_editor_plugin = plugin
|
|
18
28
|
|
|
19
29
|
|
|
30
|
+
## Each bit set in [param flags] by name, and whatever bits are left unnamed as one number.
|
|
31
|
+
static func _flag_names(flags: int) -> Array[String]:
|
|
32
|
+
var names: Array[String] = []
|
|
33
|
+
var rest: int = flags
|
|
34
|
+
for bit: int in CONNECT_FLAG_NAMES:
|
|
35
|
+
if flags & bit:
|
|
36
|
+
names.append(CONNECT_FLAG_NAMES[bit])
|
|
37
|
+
rest &= ~bit
|
|
38
|
+
if rest != 0:
|
|
39
|
+
names.append(str(rest))
|
|
40
|
+
return names
|
|
41
|
+
|
|
42
|
+
|
|
20
43
|
func _ensure_res_path(path: String) -> String:
|
|
21
44
|
if not path.begins_with("res://"):
|
|
22
45
|
return "res://" + path
|
|
@@ -249,7 +272,8 @@ func create_scene(args: Dictionary) -> Dictionary:
|
|
|
249
272
|
var project_path: String = str(args.get("projectPath", ""))
|
|
250
273
|
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
251
274
|
var root_node_type: String = str(args.get("rootNodeType", "Node"))
|
|
252
|
-
var
|
|
275
|
+
var root_node_name: String = str(args.get("rootNodeName", root_node_type))
|
|
276
|
+
var script_path: String = str(args.get("script", ""))
|
|
253
277
|
|
|
254
278
|
if scene_path == "res://":
|
|
255
279
|
return {"ok": false, "error": "Missing scenePath"}
|
|
@@ -263,6 +287,9 @@ func create_scene(args: Dictionary) -> Dictionary:
|
|
|
263
287
|
}
|
|
264
288
|
if not ClassDB.can_instantiate(root_node_type):
|
|
265
289
|
return {"ok": false, "error": "%s cannot be instantiated." % root_node_type}
|
|
290
|
+
var unfit: String = SceneFile.unfit_name(root_node_name)
|
|
291
|
+
if not unfit.is_empty():
|
|
292
|
+
return {"ok": false, "error": unfit}
|
|
266
293
|
# Saving over a scene that is there replaced it, nodes and all, and answered as a new scene.
|
|
267
294
|
if FileAccess.file_exists(scene_path):
|
|
268
295
|
return {
|
|
@@ -278,7 +305,7 @@ func create_scene(args: Dictionary) -> Dictionary:
|
|
|
278
305
|
}
|
|
279
306
|
|
|
280
307
|
var root: Node = ClassDB.instantiate(root_node_type)
|
|
281
|
-
root.name =
|
|
308
|
+
root.name = root_node_name
|
|
282
309
|
|
|
283
310
|
if not script_path.is_empty():
|
|
284
311
|
var full_script_path: String = _to_scene_res_path(project_path, script_path)
|
|
@@ -296,6 +323,10 @@ func create_scene(args: Dictionary) -> Dictionary:
|
|
|
296
323
|
}
|
|
297
324
|
root.set_script(script)
|
|
298
325
|
|
|
326
|
+
var folder: String = SceneFile.make_folder_for(scene_path)
|
|
327
|
+
if not folder.is_empty():
|
|
328
|
+
root.free()
|
|
329
|
+
return {"ok": false, "error": folder}
|
|
299
330
|
var scene: SceneFile = SceneFile.new()
|
|
300
331
|
scene.root = root
|
|
301
332
|
scene.path = scene_path
|
|
@@ -303,7 +334,9 @@ func create_scene(args: Dictionary) -> Dictionary:
|
|
|
303
334
|
if not written.is_empty():
|
|
304
335
|
return written
|
|
305
336
|
|
|
306
|
-
return {
|
|
337
|
+
return {
|
|
338
|
+
"ok": true, "scenePath": scene_path, "rootNodeType": root_node_type, "rootNodeName": root_node_name
|
|
339
|
+
}
|
|
307
340
|
|
|
308
341
|
|
|
309
342
|
func list_scene_nodes(args: Dictionary) -> Dictionary:
|
|
@@ -597,12 +630,19 @@ func get_node_properties(args: Dictionary) -> Dictionary:
|
|
|
597
630
|
# names this scene's file sets are always answered.
|
|
598
631
|
var props: Dictionary = _properties_of(node, include_defaults, scene.set_here(node))
|
|
599
632
|
scene.close()
|
|
600
|
-
|
|
633
|
+
var answer: Dictionary = {
|
|
601
634
|
"ok": true,
|
|
602
635
|
"nodePath": node_path,
|
|
603
636
|
"properties": props,
|
|
604
637
|
"unsavedInEditor": SceneFile.unsaved(scene_path)
|
|
605
638
|
}
|
|
639
|
+
# An empty answer read as a node with nothing on it, rather than one with nothing changed.
|
|
640
|
+
if props.is_empty() and not include_defaults:
|
|
641
|
+
answer["note"] = (
|
|
642
|
+
"Every property of this node is at its default, so none is listed; "
|
|
643
|
+
+ "includeDefaults: true lists them all."
|
|
644
|
+
)
|
|
645
|
+
return answer
|
|
606
646
|
|
|
607
647
|
|
|
608
648
|
func save_scene(args: Dictionary) -> Dictionary:
|
|
@@ -739,6 +779,7 @@ func connect_signal(args: Dictionary) -> Dictionary:
|
|
|
739
779
|
"targetNodePath": to,
|
|
740
780
|
"methodName": method_name,
|
|
741
781
|
"flags": saved_flags[0],
|
|
782
|
+
"flagNames": _flag_names(Read.as_int(saved_flags[0])),
|
|
742
783
|
# Connected either way, as the editor's own dialog allows, since the method may be written
|
|
743
784
|
# next; a caller told nothing learnt of a misspelt one only when the signal fired.
|
|
744
785
|
"methodFound": method_found
|
|
@@ -894,6 +935,7 @@ func list_connections(args: Dictionary) -> Dictionary:
|
|
|
894
935
|
"targetNodePath": target_path,
|
|
895
936
|
"methodName": str(callable.get_method()),
|
|
896
937
|
"flags": flags,
|
|
938
|
+
"flagNames": _flag_names(flags),
|
|
897
939
|
# False for a connection an instanced scene makes inside itself, which only that
|
|
898
940
|
# scene can change.
|
|
899
941
|
"editable": editable
|
|
@@ -250,14 +250,24 @@ func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false
|
|
|
250
250
|
# texture keeps whatever was drawn last. A capture then comes back byte for byte the same
|
|
251
251
|
# every time, with a success payload, and a game running perfectly well reads as a game
|
|
252
252
|
# that has frozen. A frame nobody drew is not evidence of anything, so it is refused.
|
|
253
|
+
# Told apart, because the remedies are: a headless game told to restore its window had none.
|
|
254
|
+
if DisplayServer.get_name() == "headless":
|
|
255
|
+
return {
|
|
256
|
+
"type": "error",
|
|
257
|
+
"message":
|
|
258
|
+
(
|
|
259
|
+
"This game runs headless, so it has no window and draws nothing to capture. Start it "
|
|
260
|
+
+ "with a window (editor_run start with headless false) to capture it."
|
|
261
|
+
),
|
|
262
|
+
}
|
|
253
263
|
if not _host.get_tree().root.can_draw():
|
|
254
264
|
return {
|
|
255
265
|
"type": "error",
|
|
256
266
|
"message":
|
|
257
267
|
(
|
|
258
|
-
"Nothing is being drawn to the game's window
|
|
259
|
-
+ "
|
|
260
|
-
+ "
|
|
268
|
+
"Nothing is being drawn to the game's window because it is minimised. The texture "
|
|
269
|
+
+ "still holds the last frame that was drawn, so this and every capture after it "
|
|
270
|
+
+ "would be that frame. Restore the window and ask again."
|
|
261
271
|
),
|
|
262
272
|
}
|
|
263
273
|
|