diff --git a/README.md b/README.md index 67c6e1b..65eb6e5 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,10 @@ Thus I've decided to use a custom event system that is similar to the one used i | `ui_focus_skip` | Focus checking on this element will be skipped | | `ui_focus_stop` | The focus will not be reset. Useful for keyboard | +### Saving and Loading + +In order for an entity to be saved, it has to implement the `_save` function returning a dictionary of the data that should be saved. +When loading, first the saved node gets instantiated, then either the `_load` function is called with the saved data, or when no `_load` function is implemented, the saved data directly applied to the node. ### Functions diff --git a/content/entities/light/light.gd b/content/entities/light/light.gd index 2641ead..c1bdeb2 100644 --- a/content/entities/light/light.gd +++ b/content/entities/light/light.gd @@ -55,4 +55,10 @@ func _on_click(event): attributes["brightness"] = int(brightness) HomeApi.set_state(entity_id, "on" if !state else "off", attributes) - set_state(!state, brightness) \ No newline at end of file + set_state(!state, brightness) + +func _save(): + return { + "transform": transform, + "entity_id": entity_id + } \ No newline at end of file diff --git a/content/entities/media_player/media_player.gd b/content/entities/media_player/media_player.gd index 1618340..ea1a707 100644 --- a/content/entities/media_player/media_player.gd +++ b/content/entities/media_player/media_player.gd @@ -86,3 +86,9 @@ func load_image(url: String): var texture = ImageTexture.create_from_image(image) logo.texture = texture logo.pixel_size = pixel_size + +func _save(): + return { + "transform": transform, + "entity_id": entity_id + } \ No newline at end of file diff --git a/content/entities/sensor/sensor.gd b/content/entities/sensor/sensor.gd index 5af77dc..78fd781 100644 --- a/content/entities/sensor/sensor.gd +++ b/content/entities/sensor/sensor.gd @@ -22,3 +22,9 @@ func set_text(stateInfo): text += " " + stateInfo["attributes"]["unit_of_measurement"] label.text = text + +func _save(): + return { + "transform": transform, + "entity_id": entity_id + } \ No newline at end of file diff --git a/content/entities/switch/switch.gd b/content/entities/switch/switch.gd index c2df949..1322d78 100644 --- a/content/entities/switch/switch.gd +++ b/content/entities/switch/switch.gd @@ -31,3 +31,9 @@ func _on_click(event): func _on_request_completed(): pass + +func _save(): + return { + "transform": transform, + "entity_id": entity_id + } \ No newline at end of file diff --git a/content/main.gd b/content/main.gd index 4f5bc04..855ca82 100644 --- a/content/main.gd +++ b/content/main.gd @@ -54,5 +54,4 @@ func vector_key_mapping(key_positive_x: int, key_negative_x: int, key_positive_y if vec: vec = vec.normalized() - return vec - + return vec \ No newline at end of file diff --git a/content/ui/menu/edit/edit_menu.gd b/content/ui/menu/edit/edit_menu.gd index 1dd855a..afcb926 100644 --- a/content/ui/menu/edit/edit_menu.gd +++ b/content/ui/menu/edit/edit_menu.gd @@ -154,7 +154,7 @@ func _on_entity_click(entity_name): return entity.set_position(global_position) - get_node("/root").add_child(entity) + get_node("/root/Main").add_child(entity) func clear_menu(): for child in devices_node.get_children(): diff --git a/content/ui/menu/room/room_menu.gd b/content/ui/menu/room/room_menu.gd index 063c74b..0ab7023 100644 --- a/content/ui/menu/room/room_menu.gd +++ b/content/ui/menu/room/room_menu.gd @@ -41,31 +41,32 @@ func _ready(): wall_mesh.visible = false ) - toggle_edit_button.on_button_up.connect(func(): - edit_enabled = false + toggle_edit_button.on_button_up.connect(_handle_button_up) - wall_corners.visible = false - wall_edges.visible = false - wall_mesh.mesh = generate_mesh() +func _handle_button_up(): + edit_enabled = false - if wall_mesh.mesh == null: - return - - var collisions = generate_collision(wall_mesh.mesh) + wall_corners.visible = false + wall_edges.visible = false + wall_mesh.mesh = generate_mesh() + + if wall_mesh.mesh == null: + return - for old_coll in wall_collisions.get_children(): - old_coll.queue_free() + var collisions = generate_collision(wall_mesh.mesh) - for collision in collisions: - var static_body = StaticBody3D.new() - static_body.set_collision_layer_value(4, true) - static_body.set_collision_layer_value(5, true) - static_body.collision_mask = 0 - static_body.add_child(collision) - wall_collisions.add_child(static_body) - - wall_mesh.visible = true - ) + for old_coll in wall_collisions.get_children(): + old_coll.queue_free() + + for collision in collisions: + var static_body = StaticBody3D.new() + static_body.set_collision_layer_value(4, true) + static_body.set_collision_layer_value(5, true) + static_body.collision_mask = 0 + static_body.add_child(collision) + wall_collisions.add_child(static_body) + + wall_mesh.visible = true func generate_mesh(): var corner_count = wall_corners.get_child_count() @@ -200,3 +201,17 @@ func corners_to_edge_transform(from_pos: Vector3, to_pos: Vector3) -> Transform3 var edge_transform = Transform3D(edge_basis, edge_position) return edge_transform + +func _save(): + return { + "corners": wall_corners.get_children().map(func(corner): return corner.position), + } + +func _load(data): + for corner in data["corners"]: + add_corner(corner) + + _handle_button_up() + + queue_free() + diff --git a/lib/globals/config_data.gd b/lib/globals/config_data.gd index 7d75ccc..95706cf 100644 --- a/lib/globals/config_data.gd +++ b/lib/globals/config_data.gd @@ -1,6 +1,8 @@ extends Node -var file_url: String = "user://config.json" +const VariantSerializer = preload("res://lib/utils/variant_serializer.gd") + +var file_url: String = "user://config.cfg" func save_config(data: Dictionary): var file := FileAccess.open(file_url, FileAccess.WRITE) @@ -8,7 +10,7 @@ func save_config(data: Dictionary): if file == null: return - var json_data := JSON.stringify(data) + var json_data := JSON.stringify(VariantSerializer.stringify_value(data)) file.store_string(json_data) func load_config(): @@ -18,6 +20,6 @@ func load_config(): return {} var json_data := file.get_as_text() - var data = JSON.parse_string(json_data) + var data = VariantSerializer.parse_value(JSON.parse_string(json_data)) return data \ No newline at end of file diff --git a/lib/globals/home_api.gd b/lib/globals/home_api.gd index 245c052..85fa799 100644 --- a/lib/globals/home_api.gd +++ b/lib/globals/home_api.gd @@ -47,6 +47,7 @@ func start_adapter(type: String, url: String, token: String): add_child(api) api.on_connect.connect(func(): + SaveSystem.load() on_connect.emit() ) @@ -92,3 +93,7 @@ func set_state(entity: String, state: String, attributes: Dictionary = {}): func watch_state(entity: String, callback: Callable): assert(has_connected(), "Not connected") return api.watch_state(entity, callback) + +func _notification(what): + if what == NOTIFICATION_WM_CLOSE_REQUEST || what == NOTIFICATION_WM_GO_BACK_REQUEST: + SaveSystem.save() \ No newline at end of file diff --git a/lib/globals/save_system.gd b/lib/globals/save_system.gd new file mode 100644 index 0000000..f9d4d0d --- /dev/null +++ b/lib/globals/save_system.gd @@ -0,0 +1,85 @@ +extends Node + +const VariantSerializer = preload("res://lib/utils/variant_serializer.gd") + +func save(): + if HomeApi.has_connected() == false: + return + + var filename = HomeApi.api.url.split("//")[1].replace("/api/websocket", "").replace(".", "_").replace(":", "_") + + var save_file = FileAccess.open("user://%s.save" % filename, FileAccess.WRITE) + + if save_file == null: + return + + var save_tree = _generate_save_tree(get_tree().root.get_node("Main")) + + var json_text = JSON.stringify(save_tree) + save_file.store_line(json_text) + +func load(): + if HomeApi.has_connected() == false: + return + + var filename = HomeApi.api.url.split("//")[1].replace("/api/websocket", "").replace(".", "_").replace(":", "_") + + var save_file = FileAccess.open("user://%s.save" % filename, FileAccess.READ) + + if save_file == null: + return + + var json_text = save_file.get_line() + var save_tree = JSON.parse_string(json_text) + + if save_tree is Array: + for tree in save_tree: + _build_save_tree(tree) + else: + _build_save_tree(save_tree) + + +func _generate_save_tree(node: Node): + var children = [] + + if node.has_method("_save") == false: + for child in node.get_children(): + var data = _generate_save_tree(child) + + if data is Array: + for child_data in data: + children.append(child_data) + else: + children.append(data) + return children + + + var save_tree = { + "data": VariantSerializer.stringify_value(node.call("_save")), + "parent": node.get_parent().get_path(), + "filename": node.get_scene_file_path() + } + + for child in node.get_children(): + var child_data = _generate_save_tree(child) + + if child_data is Array: + for data in child_data: + children.append(data) + else: + children.append(child_data) + + save_tree["children"] = children + + return save_tree + +func _build_save_tree(tree: Dictionary): + var new_object = load(tree["filename"]).instantiate() + + get_node(tree["parent"]).add_child(new_object) + + if new_object.has_method("_load"): + new_object.call("_load", VariantSerializer.parse_value(tree["data"])) + else: + for key in tree["data"].keys(): + new_object.set(key, VariantSerializer.parse_value(tree["data"][key])) diff --git a/lib/utils/variant_serializer.gd b/lib/utils/variant_serializer.gd new file mode 100644 index 0000000..e811246 --- /dev/null +++ b/lib/utils/variant_serializer.gd @@ -0,0 +1,79 @@ +extends Object + +static func stringify_value(value): + match typeof(value): + TYPE_DICTIONARY: + var new_dict = {} + for key in value.keys(): + new_dict[key] = stringify_value(value[key]) + return new_dict + TYPE_ARRAY: + return value.map(func(item): + return stringify_value(item) + ) + TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING: + return value + TYPE_VECTOR2: + return { + "x": value.x, + "y": value.y, + "_type": "Vector2" + } + TYPE_VECTOR3: + return { + "x": value.x, + "y": value.y, + "z": value.z, + "_type": "Vector3" + } + TYPE_TRANSFORM3D: + return { + "origin": stringify_value(value.origin), + "basis": stringify_value(value.basis), + "_type": "Transform3D" + } + TYPE_BASIS: + return { + "x": stringify_value(value.x), + "y": stringify_value(value.y), + "z": stringify_value(value.z), + "_type": "Basis" + } + TYPE_QUATERNION: + return { + "x": value.x, + "y": value.y, + "z": value.z, + "w": value.w, + "_type": "Quaternion" + } + _: + assert(false, "Unsupported type: %s" % typeof(value)) + +static func parse_value(value): + if typeof(value) == TYPE_ARRAY: + return value.map(func(item): + return parse_value(item) + ) + elif typeof(value) == TYPE_DICTIONARY: + if value.has("_type"): + match value["_type"]: + "Vector2": + return Vector2(value["x"], value["y"]) + "Vector3": + return Vector3(value["x"], value["y"], value["z"]) + "Transform3D": + return Transform3D(parse_value(value["basis"]), parse_value(value["origin"])) + "Basis": + return Basis(parse_value(value["x"]), parse_value(value["y"]), parse_value(value["z"])) + "Quaternion": + return Quaternion(value["x"], value["y"], value["z"], value["w"]) + _: + assert(false, "Unsupported type: %s" % value["_type"]) + else: + var new_dict = {} + for key in value.keys(): + new_dict[key] = parse_value(value[key]) + return new_dict + else: + return value diff --git a/project.godot b/project.godot index 9dbf93c..60f8748 100644 --- a/project.godot +++ b/project.godot @@ -23,6 +23,7 @@ Request="*res://lib/globals/request.gd" HomeApi="*res://lib/globals/home_api.gd" AudioPlayer="*res://lib/globals/audio_player.gd" EventSystem="*res://lib/globals/event_system.gd" +SaveSystem="*res://lib/globals/save_system.gd" [editor_plugins]