add save and load func

This commit is contained in:
bridge
2025-11-11 19:48:18 +08:00
parent 0cb7eacee7
commit 9b870475bf
21 changed files with 1348 additions and 32 deletions

17
src/sim/load/__init__.py Normal file
View File

@@ -0,0 +1,17 @@
"""读档功能模块
延迟导入以避免循环依赖
"""
def __getattr__(name):
"""延迟导入,避免在模块级别触发循环依赖"""
if name == "load_game":
from .load_game import load_game
return load_game
elif name == "check_save_compatibility":
from .load_game import check_save_compatibility
return check_save_compatibility
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = ["load_game", "check_save_compatibility"]

View File

@@ -0,0 +1,172 @@
"""
Avatar读档反序列化Mixin
将Avatar的反序列化逻辑从avatar.py分离出来。
读档策略:
- 两阶段加载先加载所有Avatarrelations留空再重建relations网络
- 引用对象通过id从全局字典获取如techniques_by_id
- treasure深拷贝后恢复devoured_souls
- 错误容错:缺失的引用对象会跳过而不是崩溃
"""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from src.classes.world import World
class AvatarLoadMixin:
"""Avatar读档反序列化Mixin
提供from_save_dict类方法从字典重建Avatar对象
"""
@classmethod
def from_save_dict(cls, data: dict, world: "World") -> "AvatarLoadMixin":
"""从字典重建Avatar用于读档
注意relations需要在所有Avatar加载完成后单独重建
Args:
data: 存档数据字典
world: 世界对象引用
Returns:
重建的Avatar对象relations为空需要外部第二阶段填充
"""
from src.classes.avatar import Gender
from src.classes.calendar import MonthStamp
from src.classes.cultivation import Realm, CultivationProgress
from src.classes.age import Age
from src.classes.hp_and_mp import HP, MP
from src.classes.technique import techniques_by_id
from src.classes.item import items_by_id
from src.classes.treasure import treasures_by_id
from src.classes.sect import sects_by_id
from src.classes.sect_ranks import SectRank
from src.classes.root import Root
from src.classes.alignment import Alignment
from src.classes.persona import personas_by_id
from src.classes.trait import traits_by_id
from src.classes.appearance import get_appearance_by_level
from src.classes.magic_stone import MagicStone
from src.classes.action_runtime import ActionPlan
# 重建基本对象
gender = Gender(data["gender"])
birth_month_stamp = MonthStamp(data["birth_month_stamp"])
# 重建修炼进度
cultivation_progress = CultivationProgress.from_dict(data["cultivation_progress"])
realm = cultivation_progress.realm
# 重建age
age = Age.from_dict(data["age"], realm)
# 创建Avatar不完整需要后续填充
avatar = cls(
world=world,
name=data["name"],
id=data["id"],
birth_month_stamp=birth_month_stamp,
age=age,
gender=gender,
cultivation_progress=cultivation_progress,
pos_x=data["pos_x"],
pos_y=data["pos_y"],
)
# 设置灵根
avatar.root = Root[data["root"]]
# 设置功法
technique_id = data.get("technique_id")
if technique_id is not None:
avatar.technique = techniques_by_id.get(technique_id)
# 设置HP/MP
avatar.hp = HP.from_dict(data["hp"])
avatar.mp = MP.from_dict(data["mp"])
# 设置物品与资源
avatar.magic_stone = MagicStone(data.get("magic_stone", 0))
# 重建items
items_dict = data.get("items", {})
avatar.items = {}
for item_id_str, quantity in items_dict.items():
item_id = int(item_id_str)
if item_id in items_by_id:
avatar.items[items_by_id[item_id]] = quantity
# 重建treasure深拷贝因为devoured_souls是实例特有的
treasure_id = data.get("treasure_id")
if treasure_id is not None and treasure_id in treasures_by_id:
import copy
avatar.treasure = copy.deepcopy(treasures_by_id[treasure_id])
avatar.treasure.devoured_souls = data.get("treasure_devoured_souls", 0)
# 重建spirit_animal
spirit_animal_data = data.get("spirit_animal")
if spirit_animal_data is not None:
from src.classes.spirit_animal import SpiritAnimal
spirit_realm = Realm[spirit_animal_data["realm"]]
avatar.spirit_animal = SpiritAnimal(
name=spirit_animal_data["name"],
realm=spirit_realm
)
# 设置社交与状态
sect_id = data.get("sect_id")
if sect_id is not None:
avatar.sect = sects_by_id.get(sect_id)
sect_rank_value = data.get("sect_rank")
if sect_rank_value is not None:
avatar.sect_rank = SectRank(sect_rank_value)
alignment_name = data.get("alignment")
if alignment_name is not None:
avatar.alignment = Alignment[alignment_name]
# 重建personas
persona_ids = data.get("persona_ids", [])
avatar.personas = [personas_by_id[pid] for pid in persona_ids if pid in personas_by_id]
# 重建trait
trait_id = data.get("trait_id")
if trait_id is not None and trait_id in traits_by_id:
avatar.trait = traits_by_id[trait_id]
# 设置外貌通过level获取完整的Appearance对象
avatar.appearance = get_appearance_by_level(data.get("appearance", 5))
# 设置行动与AI
avatar.thinking = data.get("thinking", "")
avatar.objective = data.get("objective", "")
avatar._action_cd_last_months = data.get("_action_cd_last_months", {})
# 重建planned_actions
planned_actions_data = data.get("planned_actions", [])
avatar.planned_actions = [ActionPlan.from_dict(plan_data) for plan_data in planned_actions_data]
# 重建current_action如果有
current_action_data = data.get("current_action")
if current_action_data is not None:
try:
action = avatar.create_action(current_action_data["action_name"])
from src.classes.action_runtime import ActionInstance
avatar.current_action = ActionInstance(
action=action,
params=current_action_data["params"],
status=current_action_data["status"]
)
except Exception:
# 如果动作无法重建,跳过(容错)
avatar.current_action = None
# relations需要在外部单独重建因为需要所有avatar都加载完成
avatar.relations = {}
return avatar

165
src/sim/load/load_game.py Normal file
View File

@@ -0,0 +1,165 @@
"""
读档功能模块
主要功能:
- load_game: 从JSON文件加载游戏完整状态
- check_save_compatibility: 检查存档版本兼容性(当前未实现严格检查)
加载流程(两阶段):
1. 第一阶段加载所有Avatar对象relations留空
- 通过AvatarLoadMixin.from_save_dict反序列化
- 配表对象Technique, Item等通过id从全局字典获取
2. 第二阶段重建Avatar之间的relations网络
- 必须在所有Avatar加载完成后才能建立引用关系
错误容错:
- 缺失的配表对象引用会被跳过如删除的Item
- 无法重建的动作会被置为None
- 不存在的Avatar引用会被忽略
注意事项:
- 读档后会重置前端UI状态头像图像、插值等
- 事件历史完整恢复(受限于保存时的数量)
- 地图从头重建(因为地图是固定的),但会恢复宗门总部位置
"""
import json
from pathlib import Path
from typing import Tuple, List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from src.classes.world import World
from src.sim.simulator import Simulator
from src.classes.sect import Sect
from src.classes.calendar import MonthStamp
from src.classes.event import Event
from src.classes.relation import Relation
from src.utils.config import CONFIG
def load_game(save_path: Optional[Path] = None) -> Tuple["World", "Simulator", List["Sect"]]:
"""
从文件加载游戏状态
Args:
save_path: 存档路径默认为saves/save.json
Returns:
(world, simulator, existed_sects)
Raises:
FileNotFoundError: 如果存档文件不存在
Exception: 如果加载失败
"""
# 确定加载路径
if save_path is None:
saves_dir = CONFIG.paths.saves
save_path = saves_dir / "save.json"
else:
save_path = Path(save_path)
if not save_path.exists():
raise FileNotFoundError(f"存档文件不存在: {save_path}")
try:
# 运行时导入,避免循环依赖
from src.classes.world import World
from src.classes.avatar import Avatar
from src.classes.sect import sects_by_id
from src.sim.simulator import Simulator
from src.run.create_map import create_cultivation_world_map, add_sect_headquarters
# 读取存档文件
with open(save_path, "r", encoding="utf-8") as f:
save_data = json.load(f)
# 读取元信息
meta = save_data.get("meta", {})
print(f"正在加载存档 (版本: {meta.get('version', 'unknown')}, "
f"游戏时间: {meta.get('game_time', 'unknown')})")
# 重建地图(地图本身不变,只需重建宗门总部位置)
game_map = create_cultivation_world_map()
# 读取世界数据
world_data = save_data.get("world", {})
month_stamp = MonthStamp(world_data["month_stamp"])
# 重建World对象
world = World(map=game_map, month_stamp=month_stamp)
# 获取本局启用的宗门
existed_sect_ids = world_data.get("existed_sect_ids", [])
existed_sects = [sects_by_id[sid] for sid in existed_sect_ids if sid in sects_by_id]
# 在地图上添加宗门总部
add_sect_headquarters(game_map, existed_sects)
# 第一阶段重建所有Avatar不含relations
avatars_data = save_data.get("avatars", [])
all_avatars = {}
for avatar_data in avatars_data:
avatar = Avatar.from_save_dict(avatar_data, world)
all_avatars[avatar.id] = avatar
# 第二阶段重建relations需要所有avatar都已加载
for avatar_data in avatars_data:
avatar_id = avatar_data["id"]
avatar = all_avatars[avatar_id]
relations_dict = avatar_data.get("relations", {})
for other_id, relation_value in relations_dict.items():
if other_id in all_avatars:
other_avatar = all_avatars[other_id]
relation = Relation(relation_value)
avatar.relations[other_avatar] = relation
# 将所有avatar添加到world
world.avatar_manager.avatars = all_avatars
# 重建事件历史
events_data = save_data.get("events", [])
for event_data in events_data:
event = Event.from_dict(event_data)
world.event_manager.add_event(event)
# 重建Simulator
simulator_data = save_data.get("simulator", {})
simulator = Simulator(world)
simulator.birth_rate = simulator_data.get("birth_rate", CONFIG.game.npc_birth_rate_per_month)
print(f"存档加载成功!共加载 {len(all_avatars)} 个角色,{len(events_data)} 条事件")
return world, simulator, existed_sects
except Exception as e:
print(f"加载游戏失败: {e}")
import traceback
traceback.print_exc()
raise
def check_save_compatibility(save_path: Path) -> Tuple[bool, str]:
"""
检查存档兼容性
Args:
save_path: 存档路径
Returns:
(是否兼容, 错误信息)
"""
try:
with open(save_path, "r", encoding="utf-8") as f:
save_data = json.load(f)
meta = save_data.get("meta", {})
save_version = meta.get("version", "unknown")
current_version = CONFIG.meta.version
# 当前不做版本兼容性检查,直接返回兼容
# 未来可以在这里添加版本比较逻辑
return True, ""
except Exception as e:
return False, f"无法读取存档文件: {e}"

138
src/sim/load_game.py Normal file
View File

@@ -0,0 +1,138 @@
"""
读档功能模块
"""
import json
from pathlib import Path
from typing import Tuple, List, Optional
from src.classes.world import World
from src.classes.map import Map
from src.classes.calendar import MonthStamp
from src.classes.avatar import Avatar
from src.classes.event import Event
from src.classes.sect import sects_by_id, Sect
from src.classes.relation import Relation
from src.sim.simulator import Simulator
from src.run.create_map import create_cultivation_world_map, add_sect_headquarters
from src.utils.config import CONFIG
def load_game(save_path: Optional[Path] = None) -> Tuple[World, Simulator, List[Sect]]:
"""
从文件加载游戏状态
Args:
save_path: 存档路径默认为saves/save.json
Returns:
(world, simulator, existed_sects)
Raises:
FileNotFoundError: 如果存档文件不存在
Exception: 如果加载失败
"""
# 确定加载路径
if save_path is None:
saves_dir = CONFIG.paths.saves
save_path = saves_dir / "save.json"
else:
save_path = Path(save_path)
if not save_path.exists():
raise FileNotFoundError(f"存档文件不存在: {save_path}")
try:
# 读取存档文件
with open(save_path, "r", encoding="utf-8") as f:
save_data = json.load(f)
# 读取元信息
meta = save_data.get("meta", {})
print(f"正在加载存档 (版本: {meta.get('version', 'unknown')}, "
f"游戏时间: {meta.get('game_time', 'unknown')})")
# 重建地图(地图本身不变,只需重建宗门总部位置)
game_map = create_cultivation_world_map()
# 读取世界数据
world_data = save_data.get("world", {})
month_stamp = MonthStamp(world_data["month_stamp"])
# 重建World对象
world = World(map=game_map, month_stamp=month_stamp)
# 获取本局启用的宗门
existed_sect_ids = world_data.get("existed_sect_ids", [])
existed_sects = [sects_by_id[sid] for sid in existed_sect_ids if sid in sects_by_id]
# 在地图上添加宗门总部
add_sect_headquarters(game_map, existed_sects)
# 第一阶段重建所有Avatar不含relations
avatars_data = save_data.get("avatars", [])
all_avatars = {}
for avatar_data in avatars_data:
avatar = Avatar.from_save_dict(avatar_data, world)
all_avatars[avatar.id] = avatar
# 第二阶段重建relations需要所有avatar都已加载
for avatar_data in avatars_data:
avatar_id = avatar_data["id"]
avatar = all_avatars[avatar_id]
relations_dict = avatar_data.get("relations", {})
for other_id, relation_value in relations_dict.items():
if other_id in all_avatars:
other_avatar = all_avatars[other_id]
relation = Relation(relation_value)
avatar.relations[other_avatar] = relation
# 将所有avatar添加到world
world.avatar_manager.avatars = all_avatars
# 重建事件历史
events_data = save_data.get("events", [])
for event_data in events_data:
event = Event.from_dict(event_data)
world.event_manager.add_event(event)
# 重建Simulator
simulator_data = save_data.get("simulator", {})
simulator = Simulator(world)
simulator.birth_rate = simulator_data.get("birth_rate", CONFIG.game.npc_birth_rate_per_month)
print(f"存档加载成功!共加载 {len(all_avatars)} 个角色,{len(events_data)} 条事件")
return world, simulator, existed_sects
except Exception as e:
print(f"加载游戏失败: {e}")
import traceback
traceback.print_exc()
raise
def check_save_compatibility(save_path: Path) -> Tuple[bool, str]:
"""
检查存档兼容性
Args:
save_path: 存档路径
Returns:
(是否兼容, 错误信息)
"""
try:
with open(save_path, "r", encoding="utf-8") as f:
save_data = json.load(f)
meta = save_data.get("meta", {})
save_version = meta.get("version", "unknown")
current_version = CONFIG.meta.version
# 当前不做版本兼容性检查,直接返回兼容
# 未来可以在这里添加版本比较逻辑
return True, ""
except Exception as e:
return False, f"无法读取存档文件: {e}"

20
src/sim/save/__init__.py Normal file
View File

@@ -0,0 +1,20 @@
"""存档功能模块
延迟导入以避免循环依赖
"""
def __getattr__(name):
"""延迟导入,避免在模块级别触发循环依赖"""
if name == "save_game":
from .save_game import save_game
return save_game
elif name == "get_save_info":
from .save_game import get_save_info
return get_save_info
elif name == "list_saves":
from .save_game import list_saves
return list_saves
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = ["save_game", "get_save_info", "list_saves"]

View File

@@ -0,0 +1,99 @@
"""
Avatar存档序列化Mixin
将Avatar的序列化逻辑从avatar.py分离出来保持核心类的清晰性。
存档策略:
- 引用对象Technique, Item等保存id加载时从全局字典获取
- relations转换为dict[str, str]avatar_id -> relation_value
- items转换为dict[int, int]item_id -> quantity
- current_action保存动作类名和参数
- treasure需要深拷贝因为devoured_souls是实例特有的
"""
class AvatarSaveMixin:
"""Avatar存档序列化Mixin
提供to_save_dict方法将Avatar转换为可JSON序列化的字典
"""
def to_save_dict(self) -> dict:
"""转换为可序列化的字典(用于存档)
Returns:
包含Avatar完整状态的字典可直接JSON序列化
"""
# 序列化relations: dict[Avatar, Relation] -> dict[str, str]
relations_dict = {
other.id: relation.value
for other, relation in self.relations.items()
}
# 序列化items: dict[Item, int] -> dict[int, int]
items_dict = {
item.id: quantity
for item, quantity in self.items.items()
}
# 序列化current_action
current_action_dict = None
if self.current_action is not None:
current_action_dict = {
"action_name": self.current_action.action.__class__.__name__,
"params": self.current_action.params,
"status": self.current_action.status
}
# 序列化planned_actions
planned_actions_list = [plan.to_dict() for plan in self.planned_actions]
# 序列化spirit_animal
spirit_animal_dict = None
if self.spirit_animal is not None:
spirit_animal_dict = {
"name": self.spirit_animal.name,
"realm": self.spirit_animal.realm.name
}
return {
# 基础信息
"id": self.id,
"name": self.name,
"birth_month_stamp": int(self.birth_month_stamp),
"gender": self.gender.value,
"pos_x": self.pos_x,
"pos_y": self.pos_y,
# 修炼相关
"age": self.age.to_dict(),
"cultivation_progress": self.cultivation_progress.to_dict(),
"root": self.root.name,
"technique_id": self.technique.id if self.technique else None,
"hp": self.hp.to_dict(),
"mp": self.mp.to_dict(),
# 物品与资源
"magic_stone": self.magic_stone.value,
"items": items_dict,
"treasure_id": self.treasure.id if self.treasure else None,
"treasure_devoured_souls": self.treasure.devoured_souls if self.treasure else 0,
"spirit_animal": spirit_animal_dict,
# 社交与状态
"relations": relations_dict,
"sect_id": self.sect.id if self.sect else None,
"sect_rank": self.sect_rank.value if self.sect_rank else None,
"alignment": self.alignment.name if self.alignment else None,
"persona_ids": [p.id for p in self.personas] if self.personas else [],
"trait_id": self.trait.id if self.trait else None,
"appearance": self.appearance.level,
# 行动与AI
"current_action": current_action_dict,
"planned_actions": planned_actions_list,
"thinking": self.thinking,
"objective": self.objective,
"_action_cd_last_months": self._action_cd_last_months,
}

171
src/sim/save/save_game.py Normal file
View File

@@ -0,0 +1,171 @@
"""
存档功能模块
主要功能:
- save_game: 保存游戏完整状态到JSON文件
- get_save_info: 读取存档的元信息(不加载完整数据)
- list_saves: 列出所有存档文件
存档内容:
- meta: 版本号、保存时间、游戏时间
- world: 游戏时间戳、本局启用的宗门列表
- avatars: 所有角色的完整状态通过AvatarSaveMixin.to_save_dict序列化
- events: 最近N条事件历史N在config.yml中配置
- simulator: 模拟器配置(如出生率)
存档格式JSON明文易于调试
存档位置assets/saves/ (配置在config.yml中)
注意事项:
- 当前版本只支持单一存档槽位save.json
- 不支持跨版本兼容(版本号仅记录,不做检查)
- 地图本身不保存(因为地图是固定的,只保存宗门总部位置)
- relations在Avatar中已转换为id映射避免循环引用
"""
import json
from pathlib import Path
from datetime import datetime
from typing import List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from src.classes.world import World
from src.sim.simulator import Simulator
from src.classes.sect import Sect
from src.utils.config import CONFIG
def save_game(
world: "World",
simulator: "Simulator",
existed_sects: List["Sect"],
save_path: Optional[Path] = None
) -> tuple[bool, Optional[str]]:
"""
保存游戏状态到文件
Args:
world: 世界对象
simulator: 模拟器对象
existed_sects: 本局启用的宗门列表
save_path: 保存路径默认为saves/时间戳_游戏时间.json
Returns:
(保存是否成功, 保存的文件名)
"""
try:
# 确定保存路径
if save_path is None:
saves_dir = CONFIG.paths.saves
saves_dir.mkdir(parents=True, exist_ok=True)
# 生成友好的文件名20251111_193000_Y100M1.json
now = datetime.now()
time_str = now.strftime("%Y%m%d_%H%M%S")
year = world.month_stamp.get_year()
month = world.month_stamp.get_month().value
game_time_str = f"Y{year}M{month}"
filename = f"{time_str}_{game_time_str}.json"
save_path = saves_dir / filename
else:
save_path = Path(save_path)
save_path.parent.mkdir(parents=True, exist_ok=True)
# 构建元信息
meta = {
"version": CONFIG.meta.version,
"save_time": datetime.now().isoformat(),
"game_time": f"{world.month_stamp.get_year()}{world.month_stamp.get_month().value}"
}
# 构建世界数据
world_data = {
"month_stamp": int(world.month_stamp),
"existed_sect_ids": [sect.id for sect in existed_sects]
}
# 保存所有Avatar第一阶段不含relations
avatars_data = []
for avatar in world.avatar_manager.avatars.values():
avatars_data.append(avatar.to_save_dict())
# 保存事件历史(限制数量)
max_events = CONFIG.save.max_events_to_save
events_data = []
recent_events = world.event_manager.get_recent_events(limit=max_events)
for event in recent_events:
events_data.append(event.to_dict())
# 保存模拟器数据
simulator_data = {
"birth_rate": simulator.birth_rate
}
# 组装完整的存档数据
save_data = {
"meta": meta,
"world": world_data,
"avatars": avatars_data,
"events": events_data,
"simulator": simulator_data
}
# 写入文件
with open(save_path, "w", encoding="utf-8") as f:
json.dump(save_data, f, ensure_ascii=False, indent=2)
print(f"游戏已保存到: {save_path}")
return True, save_path.name
except Exception as e:
print(f"保存游戏失败: {e}")
import traceback
traceback.print_exc()
return False, None
def get_save_info(save_path: Path) -> Optional[dict]:
"""
读取存档文件的元信息(不加载完整数据)
Args:
save_path: 存档路径
Returns:
存档元信息字典如果读取失败返回None
"""
try:
with open(save_path, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("meta", {})
except Exception:
return None
def list_saves(saves_dir: Optional[Path] = None) -> List[tuple[Path, dict]]:
"""
列出所有存档文件及其元信息
Args:
saves_dir: 存档目录默认为config中的saves目录
Returns:
[(存档路径, 元信息字典), ...]
"""
if saves_dir is None:
saves_dir = CONFIG.paths.saves
if not saves_dir.exists():
return []
saves = []
for save_file in saves_dir.glob("*.json"):
info = get_save_info(save_file)
if info is not None:
saves.append((save_file, info))
# 按保存时间倒序排列
saves.sort(key=lambda x: x[1].get("save_time", ""), reverse=True)
return saves

139
src/sim/save_game.py Normal file
View File

@@ -0,0 +1,139 @@
"""
存档功能模块
"""
import json
from pathlib import Path
from datetime import datetime
from typing import List, Optional
from src.classes.world import World
from src.sim.simulator import Simulator
from src.classes.sect import Sect
from src.utils.config import CONFIG
def save_game(
world: World,
simulator: Simulator,
existed_sects: List[Sect],
save_path: Optional[Path] = None
) -> bool:
"""
保存游戏状态到文件
Args:
world: 世界对象
simulator: 模拟器对象
existed_sects: 本局启用的宗门列表
save_path: 保存路径默认为saves/save.json
Returns:
保存是否成功
"""
try:
# 确定保存路径
if save_path is None:
saves_dir = CONFIG.paths.saves
saves_dir.mkdir(parents=True, exist_ok=True)
save_path = saves_dir / "save.json"
else:
save_path = Path(save_path)
save_path.parent.mkdir(parents=True, exist_ok=True)
# 构建元信息
meta = {
"version": CONFIG.meta.version,
"save_time": datetime.now().isoformat(),
"game_time": f"{world.month_stamp.get_year()}{world.month_stamp.get_month().value}"
}
# 构建世界数据
world_data = {
"month_stamp": int(world.month_stamp),
"existed_sect_ids": [sect.id for sect in existed_sects]
}
# 保存所有Avatar第一阶段不含relations
avatars_data = []
for avatar in world.avatar_manager.avatars.values():
avatars_data.append(avatar.to_save_dict())
# 保存事件历史(限制数量)
max_events = CONFIG.save.max_events_to_save
events_data = []
recent_events = world.event_manager.get_recent_events(limit=max_events)
for event in recent_events:
events_data.append(event.to_dict())
# 保存模拟器数据
simulator_data = {
"birth_rate": simulator.birth_rate
}
# 组装完整的存档数据
save_data = {
"meta": meta,
"world": world_data,
"avatars": avatars_data,
"events": events_data,
"simulator": simulator_data
}
# 写入文件
with open(save_path, "w", encoding="utf-8") as f:
json.dump(save_data, f, ensure_ascii=False, indent=2)
print(f"游戏已保存到: {save_path}")
return True
except Exception as e:
print(f"保存游戏失败: {e}")
import traceback
traceback.print_exc()
return False
def get_save_info(save_path: Path) -> Optional[dict]:
"""
读取存档文件的元信息(不加载完整数据)
Args:
save_path: 存档路径
Returns:
存档元信息字典如果读取失败返回None
"""
try:
with open(save_path, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("meta", {})
except Exception:
return None
def list_saves(saves_dir: Optional[Path] = None) -> List[tuple[Path, dict]]:
"""
列出所有存档文件及其元信息
Args:
saves_dir: 存档目录默认为config中的saves目录
Returns:
[(存档路径, 元信息字典), ...]
"""
if saves_dir is None:
saves_dir = CONFIG.paths.saves
if not saves_dir.exists():
return []
saves = []
for save_file in saves_dir.glob("*.json"):
info = get_save_info(save_file)
if info is not None:
saves.append((save_file, info))
# 按保存时间倒序排列
saves.sort(key=lambda x: x[1].get("save_time", ""), reverse=True)
return saves