diff --git a/src/game/level.gd b/src/game/level.gd index e05f94d..243a138 100644 --- a/src/game/level.gd +++ b/src/game/level.gd @@ -9,6 +9,8 @@ const SAVE_PATH_FMT := "user://{0}.state.res" ## Human-readable name @export var pretty_name: String +@export var version := 0 + func get_save_path() -> String: return SAVE_PATH_FMT.format([id]) diff --git a/src/world/save_state.gd b/src/world/save_state.gd index 33e11e5..d3a209f 100644 --- a/src/world/save_state.gd +++ b/src/world/save_state.gd @@ -1,17 +1,18 @@ class_name SaveState extends Resource ## Serializable container for gameplay state. -const CURRENT_VERSION := 0 +const CURRENT_VERSION := 1 const PERSISTENT_GROUP := "Persistent" const SERIALIZE_METHOD := "serialize" const DESERIALIZE_METHOD := "deserialize" -@export var save_version := CURRENT_VERSION - -@export var level_path: String - @export var persistent_state: Dictionary[String, Dictionary] = {} +@export_group("Compatibility Data") +@export var save_version := CURRENT_VERSION +@export var level_path: String +@export var level_version := -1 + @export_group("WorldManager State") @export var grunk_tank_limit: int @export var mp3_player_unlocked: bool @@ -28,14 +29,34 @@ static func node_key(node: Node, world: World) -> String: func load_to_world(world: World) -> void: + # Check save compatibility + if save_version != SaveState.CURRENT_VERSION: + push_warning( + "Save state version ", + save_version, + " is incompatible with the current game version ", + SaveState.CURRENT_VERSION + ) + return + if level_path != world.current_level_scene.resource_path: push_warning( - "This save is for ", + "Save state for level ", level_path, - " but the loaded level is for ", + " is incompatible with the current level ", world.current_level_scene.resource_path ) + return + if level_version != world.current_level.version: + push_warning( + "Save state for level version ", + level_version, + " is incompatible with the current level version ", + world.current_level.version + ) + + # Deserialize world state world.manager.grunk_tank_limit = grunk_tank_limit world.manager.mp3_player_unlocked = mp3_player_unlocked world.manager.toothbrush_unlocked = toothbrush_unlocked @@ -44,9 +65,8 @@ func load_to_world(world: World) -> void: world.manager.grunk_vault = grunk_vault world.manager.alert_level = alert_level - var persistent := world.get_tree().get_nodes_in_group(PERSISTENT_GROUP) - - for node: Node in persistent: + # Deserialize persistent level nodes + for node: Node in world.get_tree().get_nodes_in_group(PERSISTENT_GROUP): var key := SaveState.node_key(node, world) if key in persistent_state: # Node is in our persistent state, so load it with data. @@ -59,8 +79,11 @@ func load_to_world(world: World) -> void: static func serialize(world: World) -> SaveState: var save := SaveState.new() + # Serialize compatibility data save.level_path = world.current_level_scene.resource_path + save.level_version = world.current_level.version + # Serialize world state save.grunk_tank_limit = world.manager.grunk_tank_limit save.mp3_player_unlocked = world.manager.mp3_player_unlocked save.toothbrush_unlocked = world.manager.toothbrush_unlocked @@ -69,11 +92,8 @@ static func serialize(world: World) -> SaveState: save.grunk_vault = world.manager.grunk_vault save.alert_level = world.manager.alert_level - # NOTE: I'm assuming that `persistent` will have the same order ever time the world is loaded. - # This may not be the case. If so, we need to find a different way to uniquely identify nodes. - var persistent := world.get_tree().get_nodes_in_group(PERSISTENT_GROUP) - - for node: Node in persistent: + # Serialize persistent level nodes + for node: Node in world.get_tree().get_nodes_in_group(PERSISTENT_GROUP): var key := SaveState.node_key(node, world) var data: Dictionary = Callable(node, SERIALIZE_METHOD).call() save.persistent_state[key] = data