148 lines
5.7 KiB
GDScript
148 lines
5.7 KiB
GDScript
# SaveLoadManager.gd
|
|
# Autoload Singleton
|
|
# 负责处理游戏的保存和加载流程。
|
|
# 与 GameState 交互以获取/恢复状态数据。
|
|
# 将游戏状态以 JSON 格式存储在 user:// 目录下。
|
|
|
|
extends Node
|
|
|
|
# --- 常量 ---
|
|
const SAVE_FILENAME_PREFIX = "save_"
|
|
const SAVE_FILENAME_EXTENSION = ".json"
|
|
|
|
|
|
# --- 核心方法 ---
|
|
|
|
# 保存当前游戏状态到指定的槽位
|
|
# slot_id: 整数,用于区分不同的存档文件 (例如 1, 2, 3)
|
|
# 返回: bool - 保存是否成功
|
|
func save_game(slot_id: int) -> bool:
|
|
print("SaveLoadManager: Attempting to save game to slot", slot_id)
|
|
|
|
# 1. 获取需要保存的数据
|
|
if not GameState:
|
|
printerr("SaveLoadManager: GameState Autoload not found!")
|
|
return false
|
|
|
|
var game_state_data: Dictionary = GameState.get_savable_state()
|
|
if game_state_data.is_empty() and OS.is_debug_build():
|
|
# 在调试模式下,如果保存的数据为空,可能是一个警告信号
|
|
print("SaveLoadManager: Warning - Game state data to save is empty.")
|
|
# 根据游戏逻辑,这里可能允许空状态保存,也可能返回 false
|
|
# return false
|
|
|
|
# 2. 准备完整的存档结构 (包含元数据)
|
|
var save_data = {
|
|
"metadata": {
|
|
# 使用 ISO 8601 标准格式记录时间,更通用
|
|
"save_time_iso": Time.get_datetime_string_from_system(true, true),
|
|
"game_version": ProjectSettings.get_setting("application/config/version", "unknown") # 获取项目版本
|
|
# 可以添加其他元数据,如游戏内日期、截图路径等
|
|
},
|
|
"game_state": game_state_data
|
|
}
|
|
|
|
# 3. 序列化为 JSON 字符串
|
|
# 使用制表符缩进,提高可读性,发布版可考虑移除缩进减小体积
|
|
var json_string = JSON.stringify(save_data, "\t")
|
|
if json_string.is_empty() and not save_data.is_empty():
|
|
printerr("SaveLoadManager: Failed to serialize game data to JSON.")
|
|
return false
|
|
|
|
# 4. 获取存档文件路径
|
|
var save_path = _get_save_path(slot_id)
|
|
if save_path.is_empty():
|
|
printerr("SaveLoadManager: Invalid slot_id provided:", slot_id)
|
|
return false
|
|
|
|
# 5. 写入文件
|
|
# 使用 FileAccess.open 进行文件操作
|
|
var file = FileAccess.open(save_path, FileAccess.WRITE)
|
|
if file == null:
|
|
# 获取文件操作错误信息
|
|
var error_code = FileAccess.get_open_error()
|
|
printerr("SaveLoadManager: Failed to open save file for writing at '", save_path, "'. Error code:", error_code)
|
|
return false
|
|
|
|
# 写入 JSON 字符串
|
|
file.store_string(json_string)
|
|
# file.close() # FileAccess 会在变量超出作用域时自动关闭,但显式关闭是好习惯
|
|
|
|
print("SaveLoadManager: Game successfully saved to '", save_path, "'")
|
|
return true
|
|
|
|
|
|
# 从指定的槽位加载游戏状态
|
|
# slot_id: 整数,要加载的存档文件槽位
|
|
# 返回: Variant - 成功时返回 game_state 字典,失败时返回 null
|
|
func load_game(slot_id: int) -> Variant: # <--- 修改返回类型为 Variant
|
|
print("SaveLoadManager: Attempting to load game data from slot", slot_id)
|
|
|
|
# 1. 获取存档文件路径 (不变)
|
|
var save_path = _get_save_path(slot_id)
|
|
if save_path.is_empty():
|
|
printerr("SaveLoadManager: Invalid slot_id provided:", slot_id)
|
|
return null # <--- 失败返回 null
|
|
|
|
# 2. 检查文件是否存在 (不变)
|
|
if not FileAccess.file_exists(save_path):
|
|
printerr("SaveLoadManager: Save file not found at '", save_path, "'")
|
|
return null # <--- 失败返回 null
|
|
|
|
# 3. 读取文件内容 (不变)
|
|
var file = FileAccess.open(save_path, FileAccess.READ)
|
|
if file == null:
|
|
var error_code = FileAccess.get_open_error()
|
|
printerr("SaveLoadManager: Failed to open save file for reading at '", save_path, "'. Error code:", error_code)
|
|
return null # <--- 失败返回 null
|
|
var json_string = file.get_as_text()
|
|
if json_string.is_empty():
|
|
printerr("SaveLoadManager: Save file is empty or failed to read content from '", save_path, "'")
|
|
return null # <--- 失败返回 null
|
|
|
|
# 4. 解析 JSON 字符串 (不变)
|
|
var parse_result = JSON.parse_string(json_string)
|
|
if parse_result == null:
|
|
printerr("SaveLoadManager: Failed to parse JSON from save file '", save_path, "'. File might be corrupted.")
|
|
return null # <--- 失败返回 null
|
|
if not parse_result is Dictionary:
|
|
printerr("SaveLoadManager: Parsed JSON data is not a Dictionary in '", save_path, "'. Invalid format.")
|
|
return null # <--- 失败返回 null
|
|
|
|
var loaded_data: Dictionary = parse_result
|
|
|
|
# 5. 验证存档结构并提取游戏状态 (不变)
|
|
if not loaded_data.has("game_state"):
|
|
printerr("SaveLoadManager: Save file '", save_path, "' is missing the 'game_state' key. Invalid format.")
|
|
return null # <--- 失败返回 null
|
|
var game_state_data = loaded_data["game_state"]
|
|
if not game_state_data is Dictionary:
|
|
printerr("SaveLoadManager: 'game_state' data in '", save_path, "' is not a Dictionary. Invalid format.")
|
|
return null # <--- 失败返回 null
|
|
|
|
print("SaveLoadManager: Game data successfully loaded from '", save_path, "'")
|
|
# 7. 返回提取到的 game_state 数据字典
|
|
return game_state_data # <--- 成功时返回字典
|
|
|
|
# 检查指定槽位的存档文件是否存在
|
|
func does_save_exist(slot_id: int) -> bool:
|
|
var save_path = _get_save_path(slot_id)
|
|
if save_path.is_empty():
|
|
return false
|
|
return FileAccess.file_exists(save_path)
|
|
|
|
|
|
# --- 辅助方法 ---
|
|
|
|
# 根据槽位 ID 构建完整的存档文件路径
|
|
# 返回: String - 完整的 user:// 路径,如果 slot_id 无效则返回空字符串
|
|
func _get_save_path(slot_id: int) -> String:
|
|
# 基础验证,确保 slot_id 是正整数
|
|
if slot_id <= 0:
|
|
printerr("SaveLoadManager: Invalid slot_id:", slot_id, ". Must be a positive integer.")
|
|
return ""
|
|
|
|
var filename = SAVE_FILENAME_PREFIX + str(slot_id) + SAVE_FILENAME_EXTENSION
|
|
# "user://" 是 Godot 指向用户数据目录的特殊路径
|
|
return "user://" + filename
|