maxgame/addons/popochiu/migration/helpers/popochiu_migration_helper.gd
2025-07-17 01:49:18 -04:00

208 lines
7.4 KiB
GDScript

@tool
class_name PopochiuMigrationHelper
extends Node
## Helper class to assist migrating Popochiu Projects to newer versions
const MIGRATIONS_PATH = "res://addons/popochiu/migration/migrations/"
const MIGRATION_SECTION = "last_migration"
const MIGRATION_KEY = "version"
const POPOCHIU_PATH = "res://popochiu/"
static var old_settings_file := PopochiuResources.GAME_PATH.path_join("popochiu_settings.tres")
#region Public #####################################################################################
static func get_migrations_count() -> int:
return DirAccess.get_files_at(MIGRATIONS_PATH).size()
## Returns the game folder path. If this returns [member POPOCHIU_PATH], then the project is from
## Popochiu 1.x or Popochiu 2.0.0-AlphaX.
static func get_game_path() -> String:
if (
DirAccess.dir_exists_absolute(PopochiuResources.GAME_PATH)
and DirAccess.dir_exists_absolute(POPOCHIU_PATH)
):
return POPOCHIU_PATH
elif DirAccess.dir_exists_absolute(PopochiuResources.GAME_PATH):
return PopochiuResources.GAME_PATH
else: # Error cannot access the game folders
return ""
## Returns the user project migration version from the "res://game/popochiu_data.cfg" file.
## If the Popochiu Migration Version is greater than the user project migration version
## then a migration needs to be done.
## If -1 gets returned then an error has occurred.
static func get_user_migration_version() -> int:
# popochiu_data.cfg config file could not be loaded, return error
if PopochiuResources.get_data_cfg() == null:
PopochiuUtils.print_error("Can't load [code]popochiu_data.cfg[/code] file.")
return -1
if PopochiuResources.has_data_value(MIGRATION_SECTION, MIGRATION_KEY):
# Return the migration version in the popochiu_data.cfg file
return PopochiuResources.get_data_value(MIGRATION_SECTION, MIGRATION_KEY, 1)
else:
# Run Migration 1 and so on
return 0
## Returns [code]true[/code] if this is an empty project: no rooms, no characters, no inventory
## items, no dialogues, and no audio files.
static func is_empty_project() -> bool:
return (
get_game_path() == PopochiuResources.GAME_PATH
and PopochiuResources.get_section_keys("rooms").is_empty()
and PopochiuResources.get_section_keys("characters").is_empty()
and PopochiuResources.get_section_keys("inventory_items").is_empty()
and PopochiuResources.get_section_keys("dialogs").is_empty()
and PopochiuResources.get_section_keys("audio").is_empty()
)
## Returns [true] if the current Popochiu migration version is newer than the user's migration
## version, which means a migration is needed.
static func is_migration_needed() -> bool:
return get_migrations_count() > get_user_migration_version()
## Updates [code]res://game/popochiu_data.cfg[/code] migration version to [param version].
static func update_user_migration_version(new_version: int) -> void:
if PopochiuResources.set_data_value(MIGRATION_SECTION, MIGRATION_KEY, new_version) != OK:
PopochiuUtils.print_error(
"Couldn't update the Migration version from [b]%d[/b] to [b]%d[/b] in Data file." % [
get_migrations_count(), new_version
]
)
## Executes the [param steps] in [param migration] one by one and returns [code]true[/code] if
## all finished without failing. It stops execution if a step fails.
static func execute_migration_steps(migration: PopochiuMigration, steps: Array) -> bool:
if steps.is_empty():
PopochiuUtils.print_error(
"No steps to execute for Migration %d" % migration.get_version()
)
await PopochiuEditorHelper.frame_processed()
return false
var idx := 0
for step: Callable in steps:
# Update the migration step interface to show a loader
migration.start_step(idx)
# Run the actual step
var completion_type: PopochiuMigration.Completion = await step.call()
if completion_type in [
PopochiuMigration.Completion.DONE, PopochiuMigration.Completion.IGNORED
]:
# Update the interface, no more loader
await migration.step_finished(idx, completion_type)
else:
return false
idx += 1
return true
## Helper function to recursively scan the directory in [param path] and return an [Array] of
## absolute file paths with the specified extension.
static func get_absolute_file_paths_for_file_extensions(
path: String, file_extensions: Array[String], folders_to_ignore: Array[String] = []
) -> Array:
var file_paths := []
var dir: DirAccess = DirAccess.open(path)
if not dir.dir_exists(path):
return file_paths
dir.list_dir_begin()
var element_name = dir.get_next()
while not element_name.is_empty():
var file_path := path.path_join(element_name)
if dir.current_is_dir():
if element_name in folders_to_ignore:
element_name = dir.get_next()
continue
# Recurse into subdirectories
file_paths += get_absolute_file_paths_for_file_extensions(
file_path, file_extensions, folders_to_ignore
)
elif file_extensions.is_empty() or file_path.get_extension() in file_extensions:
# Add files with the specified extension to the [file_paths] array
file_paths.append(file_path)
element_name = dir.get_next()
dir.list_dir_end()
return file_paths
## Looks in the text of each file in [param file_paths] for coincidences of [param from], and
## replace them by [param to]. If any replacement was done, returns [code]true[/code].
static func replace_text_in_files(from: String, to: String, file_paths: Array) -> bool:
return PopochiuUtils.any_exhaustive(
file_paths,
func (file_path: String) -> bool:
if not FileAccess.file_exists(file_path):
return true
var file_read := FileAccess.open(file_path, FileAccess.READ)
var text := file_read.get_as_text()
file_read.close()
if not from in text:
return false
var file_write := FileAccess.open(file_path, FileAccess.WRITE)
text = text.replace(from, to)
file_write.store_string(text)
file_write.close()
return true
)
## Returns [true] if the game is checked as pixel-art based on the value in
## [code]popochiu/pixel/pixel_art_textures[/code] or in the old [code]popochiu_settings.tres[/code]
## file in versions prior to [i]2.0.0-beta3[/i].
static func is_pixel_art_game() -> bool:
var is_pixel_art := PopochiuConfig.is_pixel_art_textures()
if FileAccess.file_exists(old_settings_file):
var old_settings := load(old_settings_file)
if old_settings.get("is_pixel_art_game") != null:
is_pixel_art = old_settings.is_pixel_art_game
return is_pixel_art
## Checks if [param text] exists in the text of the file at [param file_path].
static func is_text_in_file(text: String, file_path: String) -> bool:
var file_read := FileAccess.open(file_path, FileAccess.READ)
var file_text := file_read.get_as_text()
file_read.close()
return text in file_text
## Replaces all text matches in all scripts of the game with the [param replacements].
## [param replacements] is an [Array] of [Dictionary] with the keys [code]from[/code] and
## [code]to[/code] to replace in the scripts. The [param folders_to_ignore] is an [Array] of
## folder names that should be ignored when searching for scripts.
static func replace_in_scripts(
replacements: Array[Dictionary], folders_to_ignore: Array[String] = []
) -> bool:
var scripts_paths := get_absolute_file_paths_for_file_extensions(
PopochiuResources.GAME_PATH, ["gd"], folders_to_ignore
)
var replaced_matches := 0
for dic: Dictionary in replacements:
replaced_matches += 1 if replace_text_in_files(dic.from, dic.to, scripts_paths) else 0
return replaced_matches > 0
#endregion