""" 读档功能模块 主要功能: - load_game: 从JSON文件加载游戏完整状态 - get_events_db_path: 根据存档路径计算事件数据库路径 - check_save_compatibility: 检查存档版本兼容性(当前未实现严格检查) 加载流程(两阶段): 1. 第一阶段:加载所有Avatar对象(relations留空) - 通过AvatarLoadMixin.from_save_dict反序列化 - 配表对象(Technique, Material等)通过id从全局字典获取 2. 第二阶段:重建Avatar之间的relations网络 - 必须在所有Avatar加载完成后才能建立引用关系 错误容错: - 缺失的配表对象引用会被跳过(如删除的Item) - 无法重建的动作会被置为None - 不存在的Avatar引用会被忽略 事件存储: - 事件存储在 SQLite 数据库中({save_name}_events.db) - 旧存档的 JSON 事件会自动迁移到 SQLite 注意事项: - 读档后会重置前端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 apply_history_modifications(world, modifications): """ 回放历史修改记录,恢复世界状态 """ if not modifications: return print(f"正在回放历史差分 ({len(modifications)} 个分类)...") # 导入需要修改的对象容器 from src.classes.sect import sects_by_id, sects_by_name from src.classes.technique import techniques_by_id, techniques_by_name from src.classes.item_registry import ItemRegistry # 1. 宗门修改 sects_mod = modifications.get("sects", {}) for sid_str, changes in sects_mod.items(): try: sid = int(sid_str) sect = sects_by_id.get(sid) if sect: old_name = sect.name # 应用修改 if "name" in changes: sect.name = changes["name"] if "desc" in changes: sect.desc = changes["desc"] # 同步索引 if sect.name != old_name: if old_name in sects_by_name: del sects_by_name[old_name] sects_by_name[sect.name] = sect except Exception: pass # 2. 区域修改 regions_mod = modifications.get("regions", {}) for rid_str, changes in regions_mod.items(): try: rid = int(rid_str) region = world.map.regions.get(rid) if region: if "name" in changes: region.name = changes["name"] if "desc" in changes: region.desc = changes["desc"] except Exception: pass # 3. 功法修改 techniques_mod = modifications.get("techniques", {}) for tid_str, changes in techniques_mod.items(): try: tid = int(tid_str) tech = techniques_by_id.get(tid) if tech: old_name = tech.name if "name" in changes: tech.name = changes["name"] if "desc" in changes: tech.desc = changes["desc"] if tech.name != old_name: if old_name in techniques_by_name: del techniques_by_name[old_name] techniques_by_name[tech.name] = tech except Exception: pass # 4. 武器修改 (通过 ItemRegistry) weapons_mod = modifications.get("weapons", {}) from src.classes.weapon import weapons_by_name for iid_str, changes in weapons_mod.items(): try: iid = int(iid_str) item = ItemRegistry.get(iid) if item: old_name = item.name if "name" in changes: item.name = changes["name"] if "desc" in changes: item.desc = changes["desc"] if item.name != old_name: if old_name in weapons_by_name: del weapons_by_name[old_name] weapons_by_name[item.name] = item except Exception: pass # 5. 辅助装备修改 (通过 ItemRegistry) aux_mod = modifications.get("auxiliaries", {}) from src.classes.auxiliary import auxiliaries_by_name for iid_str, changes in aux_mod.items(): try: iid = int(iid_str) item = ItemRegistry.get(iid) if item: old_name = item.name if "name" in changes: item.name = changes["name"] if "desc" in changes: item.desc = changes["desc"] if item.name != old_name: if old_name in auxiliaries_by_name: del auxiliaries_by_name[old_name] auxiliaries_by_name[item.name] = item except Exception: pass print("历史差分回放完成。") def get_events_db_path(save_path: Path) -> Path: """ 根据存档路径计算事件数据库路径。 例如:save_20260105_1423.json -> save_20260105_1423_events.db """ return save_path.with_suffix("").with_name(save_path.stem + "_events.db") 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.load_map import load_cultivation_world_map # 读取存档文件 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 = load_cultivation_world_map() # 读取世界数据 world_data = save_data.get("world", {}) month_stamp = MonthStamp(world_data["month_stamp"]) start_year = world_data.get("start_year", 100) # 计算事件数据库路径。 events_db_path = get_events_db_path(save_path) # 重建World对象(使用 SQLite 事件存储)。 world = World.create_with_db( map=game_map, month_stamp=month_stamp, events_db_path=events_db_path, start_year=start_year, ) # 恢复世界历史 history_data = world_data.get("history", {}) world.history.text = history_data.get("text", "") world.history.modifications = history_data.get("modifications", {}) # 恢复并回放历史修改记录(关键修复:在加载角色前还原规则) if world.history.modifications: apply_history_modifications(world, world.history.modifications) # 重建天地灵机 from src.classes.celestial_phenomenon import celestial_phenomena_by_id phenomenon_id = world_data.get("current_phenomenon_id") if phenomenon_id is not None and phenomenon_id in celestial_phenomena_by_id: world.current_phenomenon = celestial_phenomena_by_id[phenomenon_id] world.phenomenon_start_year = world_data.get("phenomenon_start_year", 0) # 恢复出世物品流转 circulation_data = world_data.get("circulation", {}) world.circulation.load_from_dict(circulation_data) # 获取本局启用的宗门 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] # 第一阶段:重建所有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 # 恢复洞府主人关系 cultivate_regions_hosts = world_data.get("cultivate_regions_hosts", {}) from src.classes.region import CultivateRegion for rid_str, avatar_id in cultivate_regions_hosts.items(): rid = int(rid_str) if rid in game_map.regions: region = game_map.regions[rid] if isinstance(region, CultivateRegion) and avatar_id in all_avatars: region.host_avatar = all_avatars[avatar_id] # 重建宗门成员关系与功法列表 from src.classes.technique import techniques_by_name # 1. 重建成员 for avatar in all_avatars.values(): if avatar.sect: # 存档中 avatar.sect 已经被 Avatar.from_save_dict 恢复为 Sect 对象引用 # 但 Sect.members 是空的(因为 Sect 是重新加载配置生成的) avatar.sect.add_member(avatar) # 2. 重建功法对象列表(兼容旧存档) for sect in existed_sects: if not sect.techniques and sect.technique_names: sect.techniques = [] for t_name in sect.technique_names: if t_name in techniques_by_name: sect.techniques.append(techniques_by_name[t_name]) # 检查是否需要从 JSON 迁移事件(向后兼容旧存档)。 db_event_count = world.event_manager.count() events_data = save_data.get("events", []) if db_event_count == 0 and len(events_data) > 0: # SQLite 数据库是空的,但 JSON 中有事件,执行迁移。 print(f"正在从 JSON 迁移 {len(events_data)} 条事件到 SQLite...") for event_data in events_data: event = Event.from_dict(event_data) world.event_manager.add_event(event) print("事件迁移完成") else: print(f"已从 SQLite 加载 {db_event_count} 条事件") # 重建Simulator simulator_data = save_data.get("simulator", {}) simulator = Simulator(world) # 兼容旧存档 "birth_rate" simulator.awakening_rate = simulator_data.get("awakening_rate", simulator_data.get("birth_rate", CONFIG.game.npc_awakening_rate_per_month)) print(f"存档加载成功!共加载 {len(all_avatars)} 个角色") 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}"