extends Node class_name UpdateApplier ## Handles applying downloaded updates: backup, replace, rollback signal update_started signal update_progress(step: String, percentage: float) signal update_completed signal update_failed(error: String) signal rollback_completed const BACKUP_COUNT := 2 # Keep this many backup versions func apply_update(downloaded_pck_path: String, version: String) -> void: emit_signal("update_started") var game_dir := Config.GAME_DIRECTORY var pck_name := Config.get_game_pck() var target_pck := game_dir + pck_name var backup_dir := Config.BACKUP_DIRECTORY # Step 1: Ensure game directory exists emit_signal("update_progress", "Preparing directories...", 10.0) if not _ensure_directory(game_dir): emit_signal("update_failed", "Failed to create game directory") return if not _ensure_directory(backup_dir): emit_signal("update_failed", "Failed to create backup directory") return # Step 2: Backup existing PCK if it exists emit_signal("update_progress", "Backing up current version...", 30.0) if FileAccess.file_exists(target_pck): var backup_name := pck_name.get_basename() + "_backup_" + Time.get_datetime_string_from_system().replace(":", "-") + ".pck" var backup_path := backup_dir + backup_name if not _copy_file(target_pck, backup_path): emit_signal("update_failed", "Failed to backup current version") return # Clean up old backups _cleanup_old_backups(backup_dir, BACKUP_COUNT) # Step 3: Move downloaded file to game directory emit_signal("update_progress", "Installing update...", 60.0) # Delete old PCK first if FileAccess.file_exists(target_pck): var dir := DirAccess.open(game_dir) if dir: var err := dir.remove(pck_name) if err != OK: emit_signal("update_failed", "Failed to remove old game file") return # Copy new PCK if not _copy_file(downloaded_pck_path, target_pck): emit_signal("update_failed", "Failed to install update") return # Step 4: Copy executable if needed (first install) emit_signal("update_progress", "Finalizing...", 80.0) # Clean up temp file var temp_dir := DirAccess.open("user://temp/") if temp_dir: temp_dir.remove(downloaded_pck_path.get_file()) emit_signal("update_progress", "Update complete!", 100.0) emit_signal("update_completed") func _ensure_directory(path: String) -> bool: var dir := DirAccess.open("user://") if dir: var relative_path := path.replace("user://", "") if not dir.dir_exists(relative_path): return dir.make_dir_recursive(relative_path) == OK return true return false func _copy_file(from: String, to: String) -> bool: var source := FileAccess.open(from, FileAccess.READ) if not source: push_error("[UpdateApplier] Cannot open source file: " + from) return false var dest := FileAccess.open(to, FileAccess.WRITE) if not dest: source.close() push_error("[UpdateApplier] Cannot open destination file: " + to) return false # Copy in chunks for large files const CHUNK_SIZE := 1024 * 1024 # 1MB chunks while source.get_position() < source.get_length(): var chunk := source.get_buffer(CHUNK_SIZE) dest.store_buffer(chunk) source.close() dest.close() print("[UpdateApplier] Copied: ", from, " -> ", to) return true func _cleanup_old_backups(backup_dir: String, keep_count: int) -> void: var dir := DirAccess.open(backup_dir) if not dir: return var backups: Array[String] = [] dir.list_dir_begin() var file := dir.get_next() while file != "": if file.ends_with(".pck") and "backup" in file: backups.append(file) file = dir.get_next() dir.list_dir_end() # Sort by name (which includes timestamp) backups.sort() # Remove oldest backups if we have too many while backups.size() > keep_count: var old_backup = backups.pop_front() dir.remove(old_backup) print("[UpdateApplier] Removed old backup: ", old_backup) func rollback_to_backup() -> bool: ## Rollback to the most recent backup var backup_dir := Config.BACKUP_DIRECTORY var game_dir := Config.GAME_DIRECTORY var pck_name := Config.get_game_pck() var dir := DirAccess.open(backup_dir) if not dir: push_error("[UpdateApplier] Cannot access backup directory") return false # Find the most recent backup var backups: Array[String] = [] dir.list_dir_begin() var file := dir.get_next() while file != "": if file.ends_with(".pck") and "backup" in file: backups.append(file) file = dir.get_next() dir.list_dir_end() if backups.is_empty(): push_error("[UpdateApplier] No backups found") return false backups.sort() var latest_backup = backups.back() # Restore the backup if _copy_file(backup_dir + latest_backup, game_dir + pck_name): emit_signal("rollback_completed") print("[UpdateApplier] Rolled back to: ", latest_backup) return true return false