-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handle SimpleSpawner compatibility #290
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,71 @@ | ||
@tool | ||
extends Resource | ||
|
||
const BlockSerialization = preload("res://addons/block_code/serialization/block_serialization.gd") | ||
|
||
@export var name: StringName | ||
@export var name: StringName: | ||
set = _set_name | ||
@export var children: Array[BlockSerialization] | ||
@export var arguments: Dictionary # String, ValueBlockSerialization | ||
@export var arguments: Dictionary: # String, ValueBlockSerialization | ||
set = _set_arguments | ||
|
||
|
||
func _init(p_name: StringName = &"", p_children: Array[BlockSerialization] = [], p_arguments: Dictionary = {}): | ||
name = p_name | ||
children = p_children | ||
arguments = p_arguments | ||
|
||
|
||
# Block name and arguments backwards compatibility handling. | ||
const _renamed_blocks: Dictionary = { | ||
&"simplespawner_set_spawn_frequency": &"simplespawner_set_spawn_period", | ||
} | ||
|
||
|
||
func _set_name(value): | ||
var new_name = _renamed_blocks.get(value) | ||
if new_name: | ||
print("Migrating block %s to new name %s" % [value, new_name]) | ||
name = new_name | ||
if Engine.is_editor_hint(): | ||
EditorInterface.mark_scene_as_unsaved() | ||
else: | ||
name = value | ||
Comment on lines
+25
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm happy with just this as a place to start. Two thoughts for the distant future:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed, this only handles pure renames. If you changed the arguments or something like that, this wouldn't work. We do have a version number in the higher level BlockScriptSerialization, but it's not available here right now. @wjt poked at adding aliases for blocks in #284. I didn't look closely at his approach there, but it was my initial suggestion 😆 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately I actually did change the argument name in the "set |
||
|
||
|
||
const _renamed_arguments: Dictionary = { | ||
&"simplespawner_set_spawn_period": | ||
{ | ||
"new_frequency": "new_period", | ||
}, | ||
} | ||
|
||
|
||
func _set_arguments(value): | ||
if not value is Dictionary: | ||
return | ||
|
||
var renamed_args = _renamed_arguments.get(name) | ||
if not renamed_args: | ||
# Try with the new block name if it hasn't been migrated yet. | ||
var new_block_name = _renamed_blocks.get(name) | ||
if new_block_name: | ||
renamed_args = _renamed_arguments.get(new_block_name) | ||
|
||
if renamed_args: | ||
var changed: bool = false | ||
value = value.duplicate() | ||
for old_arg in renamed_args.keys(): | ||
if not old_arg in value: | ||
continue | ||
|
||
var new_arg = renamed_args[old_arg] | ||
print("Migrating block %s argument %s to new name %s" % [name, old_arg, new_arg]) | ||
value[new_arg] = value[old_arg] | ||
value.erase(old_arg) | ||
changed = true | ||
|
||
if changed and Engine.is_editor_hint(): | ||
EditorInterface.mark_scene_as_unsaved() | ||
|
||
arguments = value |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,62 @@ | ||
@tool | ||
extends Resource | ||
|
||
@export var name: StringName | ||
@export var arguments: Dictionary # String, ValueBlockSerialization | ||
@export var name: StringName: | ||
set = _set_name | ||
@export var arguments: Dictionary: # String, ValueBlockSerialization | ||
set = _set_arguments | ||
|
||
|
||
func _init(p_name: StringName = &"", p_arguments: Dictionary = {}): | ||
name = p_name | ||
arguments = p_arguments | ||
|
||
|
||
# Block name and arguments backwards compatibility handling. | ||
const _renamed_blocks: Dictionary = { | ||
&"simplespawner_get_spawn_frequency": &"simplespawner_get_spawn_period", | ||
} | ||
|
||
|
||
func _set_name(value): | ||
var new_name = _renamed_blocks.get(value) | ||
if new_name: | ||
print("Migrating block %s to new name %s" % [value, new_name]) | ||
name = new_name | ||
if Engine.is_editor_hint(): | ||
EditorInterface.mark_scene_as_unsaved() | ||
else: | ||
name = value | ||
|
||
|
||
const _renamed_arguments: Dictionary = {} | ||
|
||
|
||
func _set_arguments(value): | ||
if not value is Dictionary: | ||
return | ||
|
||
var renamed_args = _renamed_arguments.get(name) | ||
if not renamed_args: | ||
# Try with the new block name if it hasn't been migrated yet. | ||
var new_block_name = _renamed_blocks.get(name) | ||
if new_block_name: | ||
renamed_args = _renamed_arguments.get(new_block_name) | ||
|
||
if renamed_args: | ||
var changed: bool = false | ||
value = value.duplicate() | ||
for old_arg in renamed_args.keys(): | ||
if not old_arg in value: | ||
continue | ||
|
||
var new_arg = renamed_args[old_arg] | ||
print("Migrating block %s argument %s to new name %s" % [name, old_arg, new_arg]) | ||
value[new_arg] = value[old_arg] | ||
value.erase(old_arg) | ||
changed = true | ||
|
||
if changed and Engine.is_editor_hint(): | ||
EditorInterface.mark_scene_as_unsaved() | ||
|
||
arguments = value |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -168,3 +168,40 @@ static func setup_custom_blocks(): | |
block_list.append(block_definition) | ||
|
||
BlocksCatalog.add_custom_blocks(_class_name, block_list, [], {}) | ||
|
||
|
||
# Backwards compatibility handling | ||
func _get_property_list() -> Array[Dictionary]: | ||
return [ | ||
{ | ||
# spawn_frequency was renamed to spawn_period | ||
"name": "spawn_frequency", | ||
"class_name": &"", | ||
"type": TYPE_FLOAT, | ||
"hint": PROPERTY_HINT_NONE, | ||
"hint_string": "", | ||
"usage": PROPERTY_USAGE_NONE, | ||
}, | ||
] | ||
Comment on lines
+174
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooh, interesting! |
||
|
||
|
||
func _get(property: StringName) -> Variant: | ||
match property: | ||
"spawn_frequency": | ||
return spawn_period | ||
_: | ||
return null | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm somewhat disturbed that returning null means "handle the property normally" (ergo a property can't just be null), but, indeed. This is fun! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed, |
||
|
||
|
||
func _set(property: StringName, value: Variant) -> bool: | ||
match property: | ||
"spawn_frequency": | ||
print("Migrating SimpleSpawner spawn_frequency property to new name spawn_period") | ||
spawn_period = value | ||
_: | ||
return false | ||
|
||
# Any migrated properties need to be resaved. | ||
if Engine.is_editor_hint(): | ||
EditorInterface.mark_scene_as_unsaved() | ||
return true |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
[gd_scene load_steps=14 format=3 uid="uid://br3g0jiuidqmb"] | ||
|
||
[ext_resource type="Script" path="res://addons/block_code/simple_spawner/simple_spawner.gd" id="1_liuhg"] | ||
[ext_resource type="Script" path="res://addons/block_code/block_code_node/block_code.gd" id="2_xxmhe"] | ||
[ext_resource type="Script" path="res://addons/block_code/serialization/block_serialization_tree.gd" id="3_4xc0e"] | ||
[ext_resource type="Script" path="res://addons/block_code/serialization/block_serialization.gd" id="4_ao2cd"] | ||
[ext_resource type="Script" path="res://addons/block_code/serialization/value_block_serialization.gd" id="5_387nk"] | ||
[ext_resource type="Script" path="res://addons/block_code/serialization/block_script_serialization.gd" id="5_a3p2k"] | ||
[ext_resource type="Script" path="res://addons/block_code/code_generation/variable_definition.gd" id="6_1vau2"] | ||
|
||
[sub_resource type="Resource" id="Resource_kt8ln"] | ||
script = ExtResource("4_ao2cd") | ||
name = &"simplespawner_set_spawn_frequency" | ||
children = Array[ExtResource("4_ao2cd")]([]) | ||
arguments = { | ||
"new_frequency": 10.0 | ||
} | ||
|
||
[sub_resource type="Resource" id="Resource_4x50y"] | ||
script = ExtResource("5_387nk") | ||
name = &"simplespawner_get_spawn_frequency" | ||
arguments = {} | ||
|
||
[sub_resource type="Resource" id="Resource_ar5am"] | ||
script = ExtResource("4_ao2cd") | ||
name = &"print" | ||
children = Array[ExtResource("4_ao2cd")]([]) | ||
arguments = { | ||
"text": SubResource("Resource_4x50y") | ||
} | ||
|
||
[sub_resource type="Resource" id="Resource_yeqkk"] | ||
script = ExtResource("4_ao2cd") | ||
name = &"ready" | ||
children = Array[ExtResource("4_ao2cd")]([SubResource("Resource_kt8ln"), SubResource("Resource_ar5am")]) | ||
arguments = {} | ||
|
||
[sub_resource type="Resource" id="Resource_o6xkk"] | ||
script = ExtResource("3_4xc0e") | ||
root = SubResource("Resource_yeqkk") | ||
canvas_position = Vector2(54, 47) | ||
|
||
[sub_resource type="Resource" id="Resource_u6cb0"] | ||
script = ExtResource("5_a3p2k") | ||
script_inherits = "SimpleSpawner" | ||
block_serialization_trees = Array[ExtResource("3_4xc0e")]([SubResource("Resource_o6xkk")]) | ||
variables = Array[ExtResource("6_1vau2")]([]) | ||
generated_script = "extends SimpleSpawner | ||
|
||
|
||
func _ready(): | ||
do_set_spawn_frequency(10) | ||
print((spawn_frequency)) | ||
|
||
" | ||
version = 0 | ||
|
||
[node name="Root" type="Node2D"] | ||
|
||
[node name="SimpleSpawner" type="Node2D" parent="."] | ||
script = ExtResource("1_liuhg") | ||
spawn_frequency = 5.0 | ||
|
||
[node name="BlockCode" type="Node" parent="SimpleSpawner"] | ||
script = ExtResource("2_xxmhe") | ||
block_script = SubResource("Resource_u6cb0") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
extends GutTest | ||
## Tests for scene file backwards compatibility | ||
|
||
|
||
func get_scene_state_node_index(state: SceneState, name: String) -> int: | ||
for idx in state.get_node_count(): | ||
if state.get_node_name(idx) == name: | ||
return idx | ||
return -1 | ||
|
||
|
||
func get_scene_state_node_prop_index(state: SceneState, node_idx: int, name: String) -> int: | ||
for prop_idx in state.get_node_property_count(node_idx): | ||
if state.get_node_property_name(node_idx, prop_idx) == name: | ||
return prop_idx | ||
return -1 | ||
|
||
|
||
func _get_block_names_recursive(block: Resource, names: Array[String]): | ||
names.append(block.name) | ||
if "children" in block: | ||
for child in block.children: | ||
_get_block_names_recursive(child, names) | ||
for value in block.arguments.values(): | ||
if not value is Resource: | ||
continue | ||
if not value.script: | ||
continue | ||
if value.script.resource_path != "res://addons/block_code/serialization/value_block_serialization.gd": | ||
continue | ||
_get_block_names_recursive(value, names) | ||
|
||
|
||
func get_block_script_block_names(block_script: Resource) -> Array[String]: | ||
var names: Array[String] | ||
for tree in block_script.block_serialization_trees: | ||
_get_block_names_recursive(tree.root, names) | ||
return names | ||
|
||
|
||
func _get_block_argument_names_recursive(block: Resource, names: Array[String]): | ||
names.append_array(block.arguments.keys()) | ||
if "children" in block: | ||
for child in block.children: | ||
_get_block_argument_names_recursive(child, names) | ||
for value in block.arguments.values(): | ||
if not value is Resource: | ||
continue | ||
if not value.script: | ||
continue | ||
if value.script.resource_path != "res://addons/block_code/serialization/value_block_serialization.gd": | ||
continue | ||
_get_block_argument_names_recursive(value, names) | ||
|
||
|
||
func get_block_script_argument_names(block_script: Resource) -> Array[String]: | ||
var names: Array[String] | ||
for tree in block_script.block_serialization_trees: | ||
_get_block_argument_names_recursive(tree.root, names) | ||
return names | ||
|
||
|
||
func test_simple_spawner(): | ||
const old_block_names: Array[String] = [ | ||
"simplespawner_get_spawn_frequency", | ||
"simplespawner_set_spawn_frequency", | ||
] | ||
|
||
const new_block_names: Array[String] = [ | ||
"simplespawner_get_spawn_period", | ||
"simplespawner_set_spawn_period", | ||
] | ||
|
||
const old_argument_names: Array[String] = [ | ||
"new_frequency", | ||
] | ||
|
||
const new_argument_names: Array[String] = [ | ||
"new_period", | ||
] | ||
|
||
var scene: PackedScene = load("res://tests/data/simple_spawner_compat.tscn") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooh, it's marvellous to have an example of a test with a scene file! Thank you for writing tests :) |
||
assert_not_null(scene) | ||
assert_true(scene.can_instantiate(), "Scene should be instantiable") | ||
|
||
var scene_state := scene.get_state() | ||
var spawner_idx := get_scene_state_node_index(scene_state, "SimpleSpawner") | ||
assert_gt(spawner_idx, -1, "SimpleSpawner node could not be found") | ||
|
||
# The packed SimpleSpawner node should have a simple_frequency | ||
# property but no simple_period property. | ||
var frequency_idx := get_scene_state_node_prop_index(scene_state, spawner_idx, "spawn_frequency") | ||
var period_idx := get_scene_state_node_prop_index(scene_state, spawner_idx, "spawn_period") | ||
assert_gt(frequency_idx, -1, "Old SimpleSpawner node should have spawn_frequency property") | ||
assert_lt(period_idx, 0, "Old SimpleSpawner node should not have spawn_period property") | ||
|
||
var packed_frequency = scene_state.get_node_property_value(spawner_idx, frequency_idx) | ||
assert_typeof(packed_frequency, TYPE_FLOAT) | ||
assert_eq(packed_frequency, 5.0) | ||
|
||
var block_code_idx := get_scene_state_node_index(scene_state, "BlockCode") | ||
assert_gt(block_code_idx, -1, "BlockCode node could not be found") | ||
var block_script_idx := get_scene_state_node_prop_index(scene_state, block_code_idx, "block_script") | ||
assert_gt(block_script_idx, -1, "BlockCode node block_script could not be found") | ||
var packed_block_script = scene_state.get_node_property_value(block_code_idx, block_script_idx) | ||
assert_typeof(packed_block_script, TYPE_OBJECT) | ||
assert_eq("Resource", packed_block_script.get_class()) | ||
assert_eq(packed_block_script.script.resource_path, "res://addons/block_code/serialization/block_script_serialization.gd") | ||
|
||
# Unlike Nodes, Resources are created immediately when loading the | ||
# scene, so the block names and arguments should already be migrated. | ||
var packed_block_names := get_block_script_block_names(packed_block_script) | ||
for name in old_block_names: | ||
assert_does_not_have(packed_block_names, name, "Block script should not have old name %s" % name) | ||
for name in new_block_names: | ||
assert_has(packed_block_names, name, "Block script should have new name %s" % name) | ||
|
||
var packed_argument_names := get_block_script_argument_names(packed_block_script) | ||
for name in old_argument_names: | ||
assert_does_not_have(packed_argument_names, name, "Block script should not have old argument %s" % name) | ||
for name in new_argument_names: | ||
assert_has(packed_argument_names, name, "Block script should have new argument %s" % name) | ||
|
||
# Instantiate the scene and check the Node properties. | ||
var root := scene.instantiate() | ||
assert_not_null(root) | ||
autoqfree(root) | ||
|
||
var spawner: SimpleSpawner = root.get_node("SimpleSpawner") | ||
assert_eq(spawner.spawn_frequency, 5.0) | ||
assert_eq(spawner.spawn_period, 5.0) | ||
|
||
# Pack the scene and check that the old properties won't be saved. | ||
var err: Error = scene.pack(root) | ||
assert_eq(err, OK, "Packing scene should not cause an error") | ||
|
||
scene_state = scene.get_state() | ||
spawner_idx = get_scene_state_node_index(scene_state, "SimpleSpawner") | ||
assert_gt(spawner_idx, -1, "SimpleSpawner node could not be found") | ||
|
||
# The newly packed SimpleSpawner node should have a simple_period | ||
# property but no simple_frequency property. | ||
period_idx = get_scene_state_node_prop_index(scene_state, spawner_idx, "spawn_period") | ||
frequency_idx = get_scene_state_node_prop_index(scene_state, spawner_idx, "spawn_frequency") | ||
assert_gt(period_idx, -1, "New SimpleSpawner node should have spawn_period property") | ||
assert_lt(frequency_idx, 0, "New SimpleSpawner node should not have spawn_frequency property") | ||
|
||
var packed_period = scene_state.get_node_property_value(spawner_idx, period_idx) | ||
assert_typeof(packed_period, TYPE_FLOAT) | ||
assert_eq(packed_period, 5.0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess one could extend this so that the values are not just a string but a tuple of:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, something like that. An issue here is that we don't control the order that Godot sets the property in. So, name might not be migrated in this case. Here's an untested thing I came up with: