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:
Zihao Xu
2026-01-07 23:21:55 -08:00
parent a6b8198c3f
commit 06d1bed987
4 changed files with 606 additions and 16 deletions

View File

@@ -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)

View File

@@ -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:

View File

@@ -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映射避免循环引用
- 事件实时写入SQLiteJSON中的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(),
}
# 构建世界数据