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}"