Files
cultivation-world-simulator/src/sim/save/save_game.py
4thfever 0315dca6e6 Feat/hidden domain (#113)
Summary
新增秘境探索,属于多人活动,每N年触发一次
Closes #105
2026-01-31 20:43:42 +08:00

220 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
存档功能模块
主要功能:
- save_game: 保存游戏完整状态到JSON文件
- get_save_info: 读取存档的元信息(不加载完整数据)
- list_saves: 列出所有存档文件
存档内容:
- meta: 版本号、保存时间、游戏时间、事件数据库信息
- world: 游戏时间戳、本局启用的宗门列表
- avatars: 所有角色的完整状态通过AvatarSaveMixin.to_save_dict序列化
- events: 最近N条事件历史仅用于向后兼容迁移新事件存储在SQLite中
- simulator: 模拟器配置(如出生率)
存档格式:
- JSON明文易于调试+ SQLite事件数据库
- 存档位置assets/saves/ (配置在config.yml中)
- 事件数据库:{save_name}_events.db与JSON文件同目录
注意事项:
- 不支持跨版本兼容(版本号仅记录,不做检查)
- 地图本身不保存(因为地图是固定的,只保存宗门总部位置)
- relations在Avatar中已转换为id映射避免循环引用
- 事件实时写入SQLiteJSON中的events字段仅用于旧存档迁移
"""
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
from src.classes.language import language_manager
from src.sim.load.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, 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)
# 计算事件数据库路径。
events_db_path = get_events_db_path(save_path)
# 确保当前的 SQLite 数据库被复制到新存档的位置。
# 如果当前使用的是其他数据库文件,需要将其复制过来。
if hasattr(world.event_manager, "_storage") and world.event_manager._storage:
current_db_path = world.event_manager._storage._db_path
if current_db_path != events_db_path:
import shutil
# 确保源文件存在
if current_db_path.exists():
# 确保目标目录存在
events_db_path.parent.mkdir(parents=True, exist_ok=True)
# 复制数据库文件
shutil.copy2(current_db_path, events_db_path)
print(f"已复制事件数据库: {current_db_path} -> {events_db_path}")
else:
print(f"警告: 当前事件数据库不存在: {current_db_path}")
# 构建元信息
meta = {
"version": CONFIG.meta.version,
"save_time": datetime.now().isoformat(),
"game_time": f"{world.month_stamp.get_year()}{world.month_stamp.get_month().value}",
"language": str(language_manager),
# SQLite 事件数据库信息。
"events_db": str(events_db_path.name),
"event_count": world.event_manager.count(),
}
# 构建世界数据
# 收集有主洞府信息
from src.classes.region import CultivateRegion
cultivate_regions_hosts = {}
if hasattr(world.map, 'regions'):
for rid, region in world.map.regions.items():
if isinstance(region, CultivateRegion) and region.host_avatar:
cultivate_regions_hosts[str(rid)] = region.host_avatar.id
world_data = {
"month_stamp": int(world.month_stamp),
"start_year": world.start_year,
"existed_sect_ids": [sect.id for sect in existed_sects],
# 天地灵机
"current_phenomenon_id": world.current_phenomenon.id if world.current_phenomenon else None,
"phenomenon_start_year": world.phenomenon_start_year if hasattr(world, 'phenomenon_start_year') else 0,
"cultivate_regions_hosts": cultivate_regions_hosts,
# 出世物品流转
"circulation": world.circulation.to_save_dict(),
# 世界历史
"history": {
"text": world.history.text,
"modifications": world.history.modifications
},
}
# 保存所有Avatar第一阶段不含relations
# 需要保存活人和死者
avatars_data = []
for avatar in world.avatar_manager._iter_all_avatars():
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 = {
"awakening_rate": simulator.awakening_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