feat: Support for Escoria and Game migrations (#473)

Co-authored-by: Dennis Ploeger <develop@dieploegers.de>
This commit is contained in:
Dennis Ploeger
2021-12-01 15:00:19 +01:00
committed by GitHub
parent 4d38c0f770
commit b5d5217aa4
11 changed files with 311 additions and 3 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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"]

View File

@@ -0,0 +1,5 @@
extends ESCMigration
func migrate():
self._savegame.globals["test"] = "testb"

View File

@@ -0,0 +1,5 @@
extends ESCMigration
func migrate():
self._savegame.globals["test"] = "testc"

View File

@@ -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

View File

@@ -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 = "^(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\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

View File

@@ -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()

View File

@@ -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

View File

@@ -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]