diff --git a/addons/escoria-core/game/core-scripts/esc/_test/test_esc_compiler.gd b/addons/escoria-core/_test/test_esc_compiler.gd similarity index 100% rename from addons/escoria-core/game/core-scripts/esc/_test/test_esc_compiler.gd rename to addons/escoria-core/_test/test_esc_compiler.gd diff --git a/addons/escoria-core/game/core-scripts/esc/_test/test_esc_compiler.tscn b/addons/escoria-core/_test/test_esc_compiler.tscn similarity index 93% rename from addons/escoria-core/game/core-scripts/esc/_test/test_esc_compiler.tscn rename to addons/escoria-core/_test/test_esc_compiler.tscn index dda5bef1..3022be8a 100644 --- a/addons/escoria-core/game/core-scripts/esc/_test/test_esc_compiler.tscn +++ b/addons/escoria-core/_test/test_esc_compiler.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=2] -[ext_resource path="res://addons/escoria-core/game/core-scripts/esc/_test/test_esc_compiler.gd" type="Script" id=1] +[ext_resource path="res://addons/escoria-core/_test/test_esc_compiler.gd" type="Script" id=1] [node name="Testsuite" type="Control"] anchor_right = 1.0 diff --git a/addons/escoria-core/_test/test_migrations.gd b/addons/escoria-core/_test/test_migrations.gd new file mode 100644 index 00000000..c1514f7b --- /dev/null +++ b/addons/escoria-core/_test/test_migrations.gd @@ -0,0 +1,20 @@ +extends Control + + + +func _on_CheckESCMigrationManager_pressed() -> bool: + var savegame: ESCSaveGame = ESCSaveGame.new() + + savegame.globals["test"] = "testa" + + var migration_manager: ESCMigrationManager = ESCMigrationManager.new() + savegame = migration_manager.migrate( + savegame, + "1.0.0", + "2.0.0", + "res://addons/escoria-core/_test/testversions" + ) + + assert(savegame.globals["test"] == "testc") + + return true diff --git a/addons/escoria-core/_test/test_migrations.tscn b/addons/escoria-core/_test/test_migrations.tscn new file mode 100644 index 00000000..bd8eadcf --- /dev/null +++ b/addons/escoria-core/_test/test_migrations.tscn @@ -0,0 +1,25 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/escoria-core/_test/test_migrations.gd" type="Script" id=1] + +[node name="Control" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="CheckESCMigrationManager" type="CheckButton" parent="VBoxContainer"] +margin_right = 1280.0 +margin_bottom = 40.0 +text = "Check ESCMigrationManager" + +[connection signal="pressed" from="VBoxContainer/CheckESCMigrationManager" to="." method="_on_CheckESCMigrationManager_pressed"] diff --git a/addons/escoria-core/_test/testversions/1.0.1.gd b/addons/escoria-core/_test/testversions/1.0.1.gd new file mode 100644 index 00000000..a1cf8b8c --- /dev/null +++ b/addons/escoria-core/_test/testversions/1.0.1.gd @@ -0,0 +1,5 @@ +extends ESCMigration + +func migrate(): + self._savegame.globals["test"] = "testb" + diff --git a/addons/escoria-core/_test/testversions/1.1.0.gd b/addons/escoria-core/_test/testversions/1.1.0.gd new file mode 100644 index 00000000..78404903 --- /dev/null +++ b/addons/escoria-core/_test/testversions/1.1.0.gd @@ -0,0 +1,5 @@ +extends ESCMigration + +func migrate(): + self._savegame.globals["test"] = "testc" + diff --git a/addons/escoria-core/game/core-scripts/migrations/esc_migration.gd b/addons/escoria-core/game/core-scripts/migrations/esc_migration.gd new file mode 100644 index 00000000..8ac0a9d0 --- /dev/null +++ b/addons/escoria-core/game/core-scripts/migrations/esc_migration.gd @@ -0,0 +1,27 @@ +# Base class for all migration version scripts. Extending scripts should be +# named like the version they migrate the savegame to. (e.g. 1.0.0.gd, 1.0.1.gd) +extends Object +class_name ESCMigration + + +var _savegame: ESCSaveGame + + +# Set the savegame +# +# #### Parameters +# - savegame: Savegame to modify +func set_savegame(savegame: ESCSaveGame): + _savegame = savegame + + +# Get the savegame +# **Returns** Savegame +func get_savegame(): + return _savegame + + +# Override this function in the version script with +# the things that need to be applied to the savegame +func migrate(): + pass diff --git a/addons/escoria-core/game/core-scripts/migrations/esc_migration_manager.gd b/addons/escoria-core/game/core-scripts/migrations/esc_migration_manager.gd new file mode 100644 index 00000000..ca3642b6 --- /dev/null +++ b/addons/escoria-core/game/core-scripts/migrations/esc_migration_manager.gd @@ -0,0 +1,172 @@ +# Class that handles migrations between different game or escoria versions +extends Object +class_name ESCMigrationManager + + +# Regular expression that matches a simple semver version string +const VERSION_REGEX = "^(?\\d+)\\.(?\\d+)\\.(?\\d+)$" + + +# Compiled regex +var version_regex: RegEx + + +func _init() -> void: + version_regex = RegEx.new() + version_regex.compile(VERSION_REGEX) + + +# Migrates the specified savegame from a specified version to another version +# based on a directory of migration scripts. +# +# The migration manager searches for scripts from after the given version up +# to the target version in this directory, loads them and applies the version. +# +# Each migration will return a modified version of the given savegame +func migrate( + savegame: ESCSaveGame, + from: String, + to: String, + versions_directory: String +) -> ESCSaveGame: + escoria.logger.info("Migrating from version %s to %s" % [ + from, + to + ]) + + var from_info = version_regex.search(from) + var to_info = version_regex.search(to) + + var wrong_version: bool = false + if from_info.get_string("major") > to_info.get_string("major"): + wrong_version = true + elif from_info.get_string("major") == to_info.get_string("major") and\ + from_info.get_string("minor") > to_info.get_string("minor"): + wrong_version = true + elif from_info.get_string("major") == to_info.get_string("major") and\ + from_info.get_string("minor") == to_info.get_string("minor") and\ + from_info.get_string("patch") > to_info.get_string("patch"): + wrong_version = true + + if wrong_version: + escoria.logger.report_errors( + "esc_migration_manager:migrate", + [ + "Can not migrate savegame from version %s to version %s" % [ + from, + to + ] + ] + ) + + var versions = _find_versions(versions_directory, from, to) + versions.sort_custom(self, "_compare_version") + if versions[0].get_file().get_basename() == from: + versions.pop_front() + + for version in versions: + var migration_script = load(version).new() + if not migration_script is ESCMigration: + escoria.logger.report_errors( + "esc_migration_manager:migrate", + [ + "File %s is not a valid migration script" % version + ] + ) + escoria.logger.debug("Migrating using %s" % version) + (migration_script as ESCMigration).set_savegame(savegame) + (migration_script as ESCMigration).migrate() + savegame = (migration_script as ESCMigration).get_savegame() + + return savegame + + +# Find all fitting version scripts between the given versions in a directory +# and all its subdirectories +# +# #### Parameters +# - directory: Directory to search in +# - from: Start version to check +# - to: End version to check +# **Returns** A list of version scripts +func _find_versions(directory: String, from: String, to: String) -> Array: + escoria.logger.trace("Searching directory %s" % directory) + var versions = [] + var dir = Directory.new() + dir.open(directory) + dir.list_dir_begin(true, true) + var file_name = dir.get_next() + while file_name != "": + var version = file_name.get_basename() + var regex_result = version_regex.search(version) + if dir.current_is_dir(): + versions += _find_versions( + directory.plus_file(file_name), + from, + to + ) + elif regex_result and _version_between(version, from, to): + escoria.logger.trace("Found fitting migration script %s" % version) + versions.append( + directory.plus_file(file_name) + ) + file_name = dir.get_next() + return versions + + +# Check, whether the given version is >= from and <= to +# +# #### Parameters +# - version: Version to check +# - from: Start version +# - to: End version +# **Returns** Whether the version matches +func _version_between(version: String, from: String, to: String) -> bool: + var version_info = version_regex.search(version) + var from_info = version_regex.search(from) + var to_info = version_regex.search(to) + + if from_info.get_string("major") < version_info.get_string("major") and \ + version_info.get_string("major") < to_info.get_string("major"): + return true + elif from_info.get_string("major") == version_info.get_string("major") and \ + from_info.get_string("minor") < version_info.get_string("minor"): + return true + elif from_info.get_string("major") == version_info.get_string("major") and \ + from_info.get_string("minor") == \ + version_info.get_string("minor") and \ + from_info.get_string("patch") < version_info.get_string("patch"): + return true + elif to_info.get_string("major") == version_info.get_string("major") and \ + to_info.get_string("minor") > version_info.get_string("minor"): + return true + elif to_info.get_string("major") == version_info.get_string("major") and \ + to_info.get_string("minor") == version_info.get_string("minor") and\ + to_info.get_string("patch") > version_info.get_string("patch"): + return true + + return false + + +# Compare to version strings +# +# #### Parameters +# - version_a: First version to compare +# - version_b: Second version to compare +# **Returns** true when version_b should be sorted after version_a +func _compare_version(version_a: String, version_b: String) -> bool: + var a_info = version_regex.search(version_a.get_file().get_basename()) + var b_info = version_regex.search(version_b.get_file().get_basename()) + + if a_info.get_string("major") < b_info.get_string("major"): + return true + elif a_info.get_string("major") == b_info.get_string("major") and \ + a_info.get_string("minor") < b_info.get_string("minor"): + return true + elif a_info.get_string("major") == b_info.get_string("major") and \ + a_info.get_string("minor") == b_info.get_string("minor") and \ + a_info.get_string("patch") < b_info.get_string("patch"): + return true + + return false + diff --git a/addons/escoria-core/game/core-scripts/save_data/esc_save_manager.gd b/addons/escoria-core/game/core-scripts/save_data/esc_save_manager.gd index 9da2a5a3..bd070aa0 100644 --- a/addons/escoria-core/game/core-scripts/save_data/esc_save_manager.gd +++ b/addons/escoria-core/game/core-scripts/save_data/esc_save_manager.gd @@ -174,7 +174,39 @@ func load_game(id: int): "esc_save_manager.gd:load_game()", ["Loading savegame %s" % str(id)]) - var save_game: Resource = ResourceLoader.load(save_file_path) + var save_game: ESCSaveGame = ResourceLoader.load(save_file_path) + + var plugin_config = ConfigFile.new() + plugin_config.load("res://addons/escoria-core/plugin.cfg") + var escoria_version = plugin_config.get_value("plugin", "version") + + # Migrate savegame through escoria versions + + if escoria_version != save_game.escoria_version: + var migration_manager: ESCMigrationManager = ESCMigrationManager.new() + save_game = migration_manager.migrate( + save_game, + save_game.escoria_version, + escoria_version, + "res://addons/escoria-core/game/core-scripts/migrations/versions" + ) + + # Migrate savegame through game versions + + if ProjectSettings.get_setting("escoria/main/game_version") != \ + save_game.game_version and \ + ProjectSettings.get_setting( + "escoria/main/game_migration_path" + ) != "": + var migration_manager: ESCMigrationManager = ESCMigrationManager.new() + save_game = migration_manager.migrate( + save_game, + save_game.game_version, + ProjectSettings.get_setting("escoria/main/game_version"), + ProjectSettings.get_setting( + "escoria/main/game_migration_path" + ) + ) escoria.event_manager.interrupt_running_event() diff --git a/addons/escoria-core/plugin.gd b/addons/escoria-core/plugin.gd index 297e42dd..20425918 100644 --- a/addons/escoria-core/plugin.gd +++ b/addons/escoria-core/plugin.gd @@ -177,6 +177,15 @@ func set_escoria_main_settings(): "hint": PROPERTY_HINT_DIR } ) + + escoria.register_setting( + "escoria/main/game_migration_path", + "", + { + "type": TYPE_STRING, + "hint": PROPERTY_HINT_DIR + } + ) # Prepare the settings in the Escoria debug category diff --git a/project.godot b/project.godot index fb1e9df2..5db31713 100644 --- a/project.godot +++ b/project.godot @@ -264,6 +264,16 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://addons/escoria-core/game/core-scripts/log/esc_logger.gd" }, { +"base": "Object", +"class": "ESCMigration", +"language": "GDScript", +"path": "res://addons/escoria-core/game/core-scripts/migrations/esc_migration.gd" +}, { +"base": "Object", +"class": "ESCMigrationManager", +"language": "GDScript", +"path": "res://addons/escoria-core/game/core-scripts/migrations/esc_migration_manager.gd" +}, { "base": "Node", "class": "ESCMovable", "language": "GDScript", @@ -596,6 +606,8 @@ _global_script_class_icons={ "ESCItem": "res://addons/escoria-core/design/esc_item.svg", "ESCLocation": "res://addons/escoria-core/design/esc_location.svg", "ESCLogger": "", +"ESCMigration": "", +"ESCMigrationManager": "", "ESCMovable": "", "ESCMusicPlayer": "", "ESCObject": "", @@ -702,7 +714,7 @@ sound/sfx_volume=1 sound/speech_volume=1 sound/master_volume=1 main/command_directories=[ "res://addons/escoria-core/game/core-scripts/esc/commands" ] -debug/log_level="DEBUG" +debug/log_level="TRACE" platform/skip_cache=false platform/skip_cache.mobile=true ui/items_autoregister_path="res://game/items/inventory" @@ -732,6 +744,7 @@ debug/crash_message="We're sorry, but the game crashed. Please send us the follo %s" ui/default_dialog_scene="res://addons/escoria-core/ui_library/dialogs/floating_dialog_player.tscn" esc/command_paths=[ "res://addons/escoria-core/game/core-scripts/esc/commands" ] +main/game_migration_path="" [input]