""" 存档功能模块 主要功能: - save_game: 保存游戏完整状态到JSON文件 - get_save_info: 读取存档的元信息(不加载完整数据) - list_saves: 列出所有存档文件 存档内容: - meta: 版本号、保存时间、游戏时间、事件数据库信息 - world: 游戏时间戳、本局启用的宗门列表 - avatars: 所有角色的完整状态(通过AvatarSaveMixin.to_save_dict序列化) - events: 最近N条事件历史(仅用于向后兼容迁移,新事件存储在SQLite中) - simulator: 模拟器配置(如出生率) 存档格式: - JSON(明文,易于调试)+ SQLite事件数据库 - 存档位置:assets/saves/ (配置在config.yml中) - 事件数据库:{save_name}_events.db(与JSON文件同目录) 注意事项: - 不支持跨版本兼容(版本号仅记录,不做检查) - 地图本身不保存(因为地图是固定的,只保存宗门总部位置) - relations在Avatar中已转换为id映射,避免循环引用 - 事件实时写入SQLite,JSON中的events字段仅用于旧存档迁移 """ 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 from src.classes.language import language_manager from src.sim.load.load_game import get_events_db_path 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) # 计算事件数据库路径。 events_db_path = get_events_db_path(save_path) # 确保当前的 SQLite 数据库被复制到新存档的位置。 # 如果当前使用的是其他数据库文件,需要将其复制过来。 if hasattr(world.event_manager, "_storage") and world.event_manager._storage: current_db_path = world.event_manager._storage._db_path if current_db_path != events_db_path: import shutil # 确保源文件存在 if current_db_path.exists(): # 确保目标目录存在 events_db_path.parent.mkdir(parents=True, exist_ok=True) # 复制数据库文件 shutil.copy2(current_db_path, events_db_path) print(f"已复制事件数据库: {current_db_path} -> {events_db_path}") else: print(f"警告: 当前事件数据库不存在: {current_db_path}") # 构建元信息 meta = { "version": CONFIG.meta.version, "save_time": datetime.now().isoformat(), "game_time": f"{world.month_stamp.get_year()}年{world.month_stamp.get_month().value}月", "language": str(language_manager), # SQLite 事件数据库信息。 "events_db": str(events_db_path.name), "event_count": world.event_manager.count(), } # 构建世界数据 # 收集有主洞府信息 from src.classes.region import CultivateRegion cultivate_regions_hosts = {} if hasattr(world.map, 'regions'): for rid, region in world.map.regions.items(): if isinstance(region, CultivateRegion) and region.host_avatar: cultivate_regions_hosts[str(rid)] = region.host_avatar.id world_data = { "month_stamp": int(world.month_stamp), "start_year": world.start_year, "existed_sect_ids": [sect.id for sect in existed_sects], # 天地灵机 "current_phenomenon_id": world.current_phenomenon.id if world.current_phenomenon else None, "phenomenon_start_year": world.phenomenon_start_year if hasattr(world, 'phenomenon_start_year') else 0, "cultivate_regions_hosts": cultivate_regions_hosts, # 出世物品流转 "circulation": world.circulation.to_save_dict(), # 世界历史 "history": { "text": world.history.text, "modifications": world.history.modifications }, } # 保存所有Avatar(第一阶段:不含relations) # 需要保存活人和死者 avatars_data = [] for avatar in world.avatar_manager._iter_all_avatars(): 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 = { "awakening_rate": simulator.awakening_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