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}"
|
||||
|
||||
138
src/sim/load_game.py
Normal file
138
src/sim/load_game.py
Normal 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
20
src/sim/save/__init__.py
Normal 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"]
|
||||
|
||||
99
src/sim/save/avatar_save_mixin.py
Normal file
99
src/sim/save/avatar_save_mixin.py
Normal 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
171
src/sim/save/save_game.py
Normal 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
139
src/sim/save_game.py
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user