fix: integrate SQLite event storage into load/save system
- Fix load_game to use World.create_with_db() for SQLite event storage - Add get_events_db_path() to compute event database path from save path - Add JSON to SQLite migration for backward compatibility with old saves - Close old EventManager before loading new save to prevent connection leaks - Add events_db metadata to save file - Add comprehensive tests for database switching bug and save/load cycle
This commit is contained in:
@@ -1307,6 +1307,11 @@ def api_load_game(req: LoadGameRequest):
|
||||
if not target_path.exists():
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
# 关闭旧 World 的 EventManager,释放 SQLite 连接。
|
||||
old_world = game_instance.get("world")
|
||||
if old_world and hasattr(old_world, "event_manager"):
|
||||
old_world.event_manager.close()
|
||||
|
||||
# 加载
|
||||
new_world, new_sim, new_sects = load_game(target_path)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
主要功能:
|
||||
- load_game: 从JSON文件加载游戏完整状态
|
||||
- get_events_db_path: 根据存档路径计算事件数据库路径
|
||||
- check_save_compatibility: 检查存档版本兼容性(当前未实现严格检查)
|
||||
|
||||
加载流程(两阶段):
|
||||
@@ -17,9 +18,12 @@
|
||||
- 无法重建的动作会被置为None
|
||||
- 不存在的Avatar引用会被忽略
|
||||
|
||||
事件存储:
|
||||
- 事件存储在 SQLite 数据库中({save_name}_events.db)
|
||||
- 旧存档的 JSON 事件会自动迁移到 SQLite
|
||||
|
||||
注意事项:
|
||||
- 读档后会重置前端UI状态(头像图像、插值等)
|
||||
- 事件历史完整恢复(受限于保存时的数量)
|
||||
- 地图从头重建(因为地图是固定的),但会恢复宗门总部位置
|
||||
"""
|
||||
import json
|
||||
@@ -37,6 +41,15 @@ from src.classes.relation import Relation
|
||||
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"]]:
|
||||
"""
|
||||
从文件加载游戏状态
|
||||
@@ -85,8 +98,15 @@ def load_game(save_path: Optional[Path] = None) -> Tuple["World", "Simulator", L
|
||||
world_data = save_data.get("world", {})
|
||||
month_stamp = MonthStamp(world_data["month_stamp"])
|
||||
|
||||
# 重建World对象
|
||||
world = World(map=game_map, month_stamp=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,
|
||||
)
|
||||
|
||||
# 重建天地灵机
|
||||
from src.classes.celestial_phenomenon import celestial_phenomena_by_id
|
||||
@@ -153,18 +173,26 @@ def load_game(save_path: Optional[Path] = None) -> Tuple["World", "Simulator", L
|
||||
if t_name in techniques_by_name:
|
||||
sect.techniques.append(techniques_by_name[t_name])
|
||||
|
||||
# 重建事件历史
|
||||
# 检查是否需要从 JSON 迁移事件(向后兼容旧存档)。
|
||||
db_event_count = world.event_manager.count()
|
||||
events_data = save_data.get("events", [])
|
||||
for event_data in events_data:
|
||||
event = Event.from_dict(event_data)
|
||||
world.event_manager.add_event(event)
|
||||
|
||||
|
||||
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.birth_rate = simulator_data.get("birth_rate", CONFIG.game.npc_birth_rate_per_month)
|
||||
|
||||
print(f"存档加载成功!共加载 {len(all_avatars)} 个角色,{len(events_data)} 条事件")
|
||||
print(f"存档加载成功!共加载 {len(all_avatars)} 个角色")
|
||||
return world, simulator, existed_sects
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -7,20 +7,22 @@
|
||||
- list_saves: 列出所有存档文件
|
||||
|
||||
存档内容:
|
||||
- meta: 版本号、保存时间、游戏时间
|
||||
- meta: 版本号、保存时间、游戏时间、事件数据库信息
|
||||
- world: 游戏时间戳、本局启用的宗门列表
|
||||
- avatars: 所有角色的完整状态(通过AvatarSaveMixin.to_save_dict序列化)
|
||||
- events: 最近N条事件历史(N在config.yml中配置)
|
||||
- events: 最近N条事件历史(仅用于向后兼容迁移,新事件存储在SQLite中)
|
||||
- simulator: 模拟器配置(如出生率)
|
||||
|
||||
存档格式:JSON(明文,易于调试)
|
||||
存档位置:assets/saves/ (配置在config.yml中)
|
||||
存档格式:
|
||||
- JSON(明文,易于调试)+ SQLite事件数据库
|
||||
- 存档位置:assets/saves/ (配置在config.yml中)
|
||||
- 事件数据库:{save_name}_events.db(与JSON文件同目录)
|
||||
|
||||
注意事项:
|
||||
- 当前版本只支持单一存档槽位(save.json)
|
||||
- 不支持跨版本兼容(版本号仅记录,不做检查)
|
||||
- 地图本身不保存(因为地图是固定的,只保存宗门总部位置)
|
||||
- relations在Avatar中已转换为id映射,避免循环引用
|
||||
- 事件实时写入SQLite,JSON中的events字段仅用于旧存档迁移
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
@@ -33,6 +35,7 @@ if TYPE_CHECKING:
|
||||
from src.classes.sect import Sect
|
||||
|
||||
from src.utils.config import CONFIG
|
||||
from src.sim.load.load_game import get_events_db_path
|
||||
|
||||
|
||||
def save_game(
|
||||
@@ -72,11 +75,17 @@ def save_game(
|
||||
save_path = Path(save_path)
|
||||
save_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 计算事件数据库路径。
|
||||
events_db_path = get_events_db_path(save_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}月"
|
||||
"game_time": f"{world.month_stamp.get_year()}年{world.month_stamp.get_month().value}月",
|
||||
# SQLite 事件数据库信息。
|
||||
"events_db": str(events_db_path.name),
|
||||
"event_count": world.event_manager.count(),
|
||||
}
|
||||
|
||||
# 构建世界数据
|
||||
|
||||
Reference in New Issue
Block a user