refactor: save & load
This commit is contained in:
17
src/sim/__init__.py
Normal file
17
src/sim/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Simulator module
|
||||
"""
|
||||
# 延迟导入 Simulator 以避免循环导入
|
||||
# from .simulator import Simulator
|
||||
|
||||
def __getattr__(name):
|
||||
if name == "Simulator":
|
||||
from .simulator import Simulator
|
||||
return Simulator
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
# 导出常用的 save/load 函数,方便外部调用
|
||||
from .save.save_game import save_game, list_saves, get_save_info
|
||||
from .load.load_game import load_game, get_events_db_path, check_save_compatibility
|
||||
|
||||
__all__ = ["Simulator", "save_game", "list_saves", "get_save_info", "load_game", "get_events_db_path", "check_save_compatibility"]
|
||||
@@ -1,159 +0,0 @@
|
||||
"""
|
||||
读档功能模块
|
||||
"""
|
||||
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.relation import Relation
|
||||
from src.sim.simulator import Simulator
|
||||
from src.run.load_map import load_cultivation_world_map
|
||||
from src.utils.config import CONFIG
|
||||
|
||||
|
||||
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:
|
||||
# 读取存档文件
|
||||
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"])
|
||||
|
||||
# 计算事件数据库路径
|
||||
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,
|
||||
)
|
||||
|
||||
# 获取本局启用的宗门
|
||||
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
|
||||
|
||||
# 检查是否需要从 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)
|
||||
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}"
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
"""
|
||||
存档功能模块
|
||||
"""
|
||||
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
|
||||
from src.sim.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, str]:
|
||||
"""
|
||||
保存游戏状态到文件
|
||||
|
||||
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())
|
||||
|
||||
# 事件已实时写入 SQLite,不再保存到 JSON。
|
||||
# 记录事件数据库路径到元信息中(供参考)。
|
||||
events_db_path = get_events_db_path(save_path)
|
||||
meta["events_db"] = str(events_db_path.name)
|
||||
meta["event_count"] = world.event_manager.count()
|
||||
|
||||
# 保存模拟器数据
|
||||
simulator_data = {
|
||||
"awakening_rate": simulator.awakening_rate
|
||||
}
|
||||
|
||||
# 组装完整的存档数据(不含 events,事件在 SQLite 中)
|
||||
save_data = {
|
||||
"meta": meta,
|
||||
"world": world_data,
|
||||
"avatars": avatars_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}")
|
||||
print(f"事件数据库: {events_db_path} ({meta['event_count']} 条事件)")
|
||||
return True, save_path.name
|
||||
|
||||
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