add save and load func
This commit is contained in:
17
src/sim/load/__init__.py
Normal file
17
src/sim/load/__init__.py
Normal 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"]
|
||||
|
||||
172
src/sim/load/avatar_load_mixin.py
Normal file
172
src/sim/load/avatar_load_mixin.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Avatar读档反序列化Mixin
|
||||
|
||||
将Avatar的反序列化逻辑从avatar.py分离出来。
|
||||
|
||||
读档策略:
|
||||
- 两阶段加载:先加载所有Avatar(relations留空),再重建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
165
src/sim/load/load_game.py
Normal 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}"
|
||||
|
||||
Reference in New Issue
Block a user