xiandie/manager/archive_manager/archive_manager.gd

391 lines
11 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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