@tool extends Node signal archive_loaded # Constants const CURRENT_VERSION = 6 const ARCHIVE_ID_MIN = 0 const ARCHIVE_ID_MAX = 99 const ARCHIVE_ID_DIGITS = 3 # Static paths static var user_root_dir := "user://data/" # must end with "/" static var archive_dir := "user://data/archives/" static var archive_prefix := "save" # Archive management var archive: AssembledArchive: set(val): archive = val if archive: GlobalConfigManager.print_global_info() print("use archive ", archive.resource_path) archive.event_stage["release_stage"] = GlobalConfig.RELEASE_STAGE print_rich("[color=brown] release_stage = %s[/color]" % GlobalConfig.RELEASE_STAGE) var archives_dict: Dictionary[int, AssembledArchive] = {} var archives_notes_dict: Dictionary[int, String] = {} var autosave_timer := Timer.new() # 供运行时缓存跨场景数据 var _runtime_global_dictionary: Dictionary[String, Variant] = {} func _ready() -> void: # 禁用默认退出行为,在 _notification 处理 NOTIFICATION_WM_CLOSE_REQUEST 时保存数据 get_tree().set_auto_accept_quit(false) process_mode = Node.PROCESS_MODE_ALWAYS if not _check_dirs_and_archives(): _handle_load_error("存档目录", "读写") return _setup_autosave_timer() # config should be loaded first load_config() # 在 debug or editor 模式下,直接保证有 archive if GlobalConfig.DEBUG or Engine.is_editor_hint(): _ensure_debug_archive() func _notification(what: int) -> void: # handle window close request if what == NOTIFICATION_WM_CLOSE_REQUEST: save_all() print("Saved all success before Quit") # 已保存所有数据 [ID:ui_saved_all] SceneManager.pop_notification("ui_saved_all") SceneManager.quit_game() func _setup_autosave_timer() -> void: autosave_timer.timeout.connect(_try_auto_save) autosave_timer.stop() add_child(autosave_timer) func _ensure_debug_archive() -> void: if archives_dict.is_empty(): create_and_use_new_archive(0) else: # debug 模式下默认使用 0 号存档 GlobalConfigManager.config.current_selected_archive_id = 0 func _on_archive_id_changed() -> void: var selected_id = GlobalConfigManager.config.current_selected_archive_id if selected_id < 0: return print("_on_archive_id_changed id=", selected_id) if not archives_dict.has(selected_id): print("新建存档 ", selected_id) create_and_use_new_archive(selected_id) # 已创建新存档 [ID:ui_new_archive] SceneManager.pop_notification("ui_new_archive") else: load_archive() func check_autosave_options() -> void: var config = GlobalConfigManager.config var should_enable_autosave = ( config.auto_save_enabled and archive and config.auto_save_seconds > 1 ) if should_enable_autosave: # reset left time autosave_timer.stop() autosave_timer.one_shot = false autosave_timer.wait_time = config.auto_save_seconds autosave_timer.start() else: autosave_timer.stop() if GlobalConfig.DEBUG: print( "check_autosave_option: ", config.auto_save_enabled, " wait_time=", autosave_timer.wait_time ) func _try_auto_save() -> void: if GlobalConfig.DEBUG: print("Auto save") if archive and GlobalConfigManager.config.auto_save_seconds > 1: save_all() # 自动保存成功 [ID:ui_auto_saved] SceneManager.pop_notification("ui_auto_saved") func _check_dirs_and_archives() -> bool: # Ensure directories exist _ensure_directory_exists(user_root_dir) _ensure_directory_exists(archive_dir) # Check if the archive directory is accessible var archive_dir_access = DirAccess.open(archive_dir) if not archive_dir_access: _handle_load_error("存档目录", "读取") return false # Load existing archives _load_existing_archives(archive_dir_access) return true func _ensure_directory_exists(dir_path: String) -> void: if not DirAccess.dir_exists_absolute(dir_path): DirAccess.make_dir_recursive_absolute(dir_path) print("Create directory:", dir_path) func _load_existing_archives(dir_access: DirAccess) -> void: var files = dir_access.get_files() files.sort() for file in files: if not _is_valid_archive_filename(file): continue var archive_info = _parse_archive_filename(file) if not archive_info: continue var id = archive_info.id var note = archive_info.note archives_notes_dict[id] = note if not archives_dict.has(id): var archive_resource = _load_archive_resource(archive_dir + file) if archive_resource: archives_dict[id] = archive_resource func _is_valid_archive_filename(filename: String) -> bool: return filename.begins_with(archive_prefix) and filename.ends_with(GlobalConfig.RES_FILE_FORMAT) func _parse_archive_filename(filename: String) -> Dictionary: # format: save012_xxxxx; save000 var basename = filename.get_basename() var id_and_note = basename.substr(archive_prefix.length()).strip_escapes().split("_", true, 1) var id_str = id_and_note[0] # 非三位数的 id 会被忽略 if id_str.length() != ARCHIVE_ID_DIGITS: return {} var id = int(id_str) # 读取范围是 0-99 if id < ARCHIVE_ID_MIN or id > ARCHIVE_ID_MAX: return {} var note_str = id_and_note[1] if id_and_note.size() >= 2 else (archive_prefix + id_str) return { "id": id, "note": note_str } func _load_archive_resource(path: String) -> AssembledArchive: var res = ResourceLoader.load( path, "AssembledArchive", ResourceLoader.CACHE_MODE_REPLACE_DEEP ) if is_instance_valid(res) and res.version >= CURRENT_VERSION: return res else: printerr("SKIP INVALID ARCHIVE! path=", path) return null # id = -1 means create a new archive, otherwise create an archive with the given id func create_and_use_new_archive(id := -1) -> void: _check_dirs_and_archives() if id < 0: # 如果 id 小于 0,找到一个新的 id,创建新存档 id = _find_next_available_id() _create_and_save_new_archive_resoure(id) else: # 如果 id 大于等于 0,创建指定 id 的存档 var archive_path = _get_archive_path(id) var take_over_path = FileAccess.file_exists(archive_path) _create_and_save_new_archive_resoure(id, take_over_path) # this will auto trigger signal and load the new archive GlobalConfigManager.config.current_selected_archive_id = id func _find_next_available_id() -> int: var id = 0 var archive_path = _get_archive_path(id) while FileAccess.file_exists(archive_path) and id <= ARCHIVE_ID_MAX: id += 1 archive_path = _get_archive_path(id) return id func _create_and_save_new_archive_resoure(id: int, take_over_path := false) -> void: var archive_path = _get_archive_path(id) archive = AssembledArchive.new() as Resource archive.version = CURRENT_VERSION if take_over_path: archive.take_over_path(archive_path) else: archive.resource_path = archive_path archive.archive_id = id archive.created_time = Time.get_datetime_string_from_system(false, true) ResourceSaver.save(archive, archive_path) archives_dict[id] = archive # 超过 999 个存档会出问题;不过这个游戏不会有这么多存档 func _get_archive_path(id: int) -> String: var id_str := str(id).pad_zeros(ARCHIVE_ID_DIGITS) return archive_dir + archive_prefix + id_str + GlobalConfig.RES_FILE_FORMAT func allow_resume(id := 1) -> bool: return archives_dict.has(id) func save_all() -> void: # save config var config = GlobalConfigManager.config if config: ResourceSaver.save(config) # save player state _save_player_state() # save archive if archive: ResourceSaver.save(archive) # reset autosave timer check_autosave_options() func _save_player_state() -> void: if not archive: return var player = SceneManager.get_player() as MainPlayer if player: archive.player_global_position_x = player.global_position.x archive.player_direction = player.facing_direction func load_config() -> void: if GlobalConfigManager.config: return var path = user_root_dir + "config" + GlobalConfig.RES_FILE_FORMAT if FileAccess.file_exists(path): var loaded_config = ResourceLoader.load(path) if is_instance_valid(loaded_config) and loaded_config.version >= CURRENT_VERSION: GlobalConfigManager.config = loaded_config else: printerr("SKIP INVALID CONFIG!") if not GlobalConfigManager.config: _create_default_config(path) GlobalConfigManager.config.resource_path = path if not Engine.is_editor_hint(): _connect_config_signals() func _create_default_config(path: String) -> void: var config = GlobalConfig.new() config.version = CURRENT_VERSION GlobalConfigManager.config = config ResourceSaver.save(config, path) func _connect_config_signals() -> void: var config = GlobalConfigManager.config config.current_selected_archive_id_changed.connect(_on_archive_id_changed) config.auto_save_seconds_changed.connect(check_autosave_options) config.auto_save_enabled_changed.connect(check_autosave_options) func load_archive() -> void: _check_dirs_and_archives() var selected_id = 0 if GlobalConfigManager.config: selected_id = GlobalConfigManager.config.current_selected_archive_id print("load_archive ", selected_id) if not archives_dict.has(selected_id): _handle_load_error(str(selected_id) + " 号存档", "查找") return archive = archives_dict[selected_id] # emit signal archive_loaded.emit() check_autosave_options() func _handle_load_error(target: String, action: String) -> void: var msg = str(target) + " " + str(action) + " failed. Permission Error." SceneManager.pop_notification(msg) printerr(msg) # TODO handle error func set_global_entry(property: StringName, value) -> void: if not archive: printerr("Archive is null, cannot set global entry") return archive.global_data_dict[property] = value func get_global_value(property: StringName, default = null) -> Variant: if not archive: printerr("Archive is null, cannot get global value") return default var val = archive.global_data_dict.get(property) return default if val == null else val func set_chapter_if_greater(c: int) -> void: if not archive: printerr("Archive is null, cannot set chapter") return # 1:序章;2-5:一~四章;6:结尾 const MIN_CHAPTER = 1 const MAX_CHAPTER = 6 if c < MIN_CHAPTER or c > MAX_CHAPTER: printerr("[ArchiveManager] set_chapter_if_greater: invalid chapter value: " + str(c)) return if EventManager.get_chapter_stage() >= c: return # 进入下一章 print("[ArchiveManager] set_chapter_if_greater: " + str(c)) EventManager.set_stage_if_greater("current_chapter_stage", c) func unlock_memory(id: int) -> void: if not archive: printerr("Archive is null, cannot unlock memory. id=", id) return if archive.mem_display_dict.get(id): print("memory already unlocked. id=", id) return archive.mem_display_dict[id] = true SceneManager.pop_notification("ui_notify_mem_update") # 供运行时缓存跨场景数据 func runtime_set(key: String, value: Variant) -> void: _runtime_global_dictionary[key] = value func runtime_get(key: String, default_value: Variant = null) -> Variant: return _runtime_global_dictionary.get(key, default_value) func runtime_has(key: String) -> bool: return _runtime_global_dictionary.has(key) func runtime_remove(key: String) -> void: _runtime_global_dictionary.erase(key)