"""存档读写。 存档是纯 JSON,方便人工检查,也方便外部工具/AI 直接读取分析。 写入采用"临时文件 + 原子替换",避免中途崩溃导致存档损坏。 """ from __future__ import annotations import json import os import time from typing import Any, Dict, Optional, Tuple from .models import GameState SAVE_VERSION = 1 def default_save_path() -> str: """默认存档位置:项目根目录下的 saves/save.json。""" root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) return os.path.join(root, "saves", "save.json") def save_state(state: GameState, path: Optional[str] = None) -> str: """把状态写入磁盘,返回实际写入路径。""" path = path or default_save_path() directory = os.path.dirname(os.path.abspath(path)) os.makedirs(directory, exist_ok=True) payload = { "version": SAVE_VERSION, "saved_at": time.strftime("%Y-%m-%d %H:%M:%S"), "state": state.to_dict(), } # 注意:to_dict() 里的派生字段在读取时会被忽略,这里只取原始字段 payload["state"] = _raw_state(state) tmp_path = path + ".tmp" with open(tmp_path, "w", encoding="utf-8") as handle: json.dump(payload, handle, ensure_ascii=False, indent=2) handle.flush() os.fsync(handle.fileno()) os.replace(tmp_path, path) return path def _raw_state(state: GameState) -> Dict[str, Any]: """导出可完整还原的原始字段(与 to_dict 的展示字段不同)。""" return { "day": state.day, "gold": state.gold, "level": state.level, "exp": state.exp, "ap": state.ap, "feed": state.feed, "plots": [ { "index": p.index, "crop": p.crop, "growth": p.growth, "moisture": p.moisture, "planted_day": p.planted_day, } for p in state.plots ], "animals": [ { "id": a.aid, "kind": a.kind, "fed": a.fed, "progress": a.progress, "starve": a.starve, "pending": a.pending, } for a in state.animals ], "market": state.market, "stats": { "harvested": state.total_harvested, "collected": state.total_collected, "earned": state.total_earned, "spent": state.total_spent, }, } def load_state(path: Optional[str] = None) -> Tuple[Optional[GameState], str]: """读取存档。 返回 ``(state, message)``;失败时 ``state`` 为 None 并给出原因, 调用方应当降级为"开新档"而不是崩溃。 """ path = path or default_save_path() if not os.path.exists(path): return None, f"未找到存档 {path}" try: with open(path, "r", encoding="utf-8") as handle: payload = json.load(handle) except (OSError, json.JSONDecodeError) as exc: return None, f"存档无法解析:{exc}" if not isinstance(payload, dict): return None, "存档格式错误:根节点不是对象" version = payload.get("version", 0) if version != SAVE_VERSION: return None, f"存档版本不兼容(文件 v{version},当前 v{SAVE_VERSION})" raw = payload.get("state") if not isinstance(raw, dict): return None, "存档缺少 state 字段" try: state = GameState.from_dict(raw) except (TypeError, ValueError, KeyError) as exc: return None, f"存档内容异常:{exc}" return state, f"已载入存档(第 {state.day} 天)" def delete_save(path: Optional[str] = None) -> bool: path = path or default_save_path() try: os.remove(path) return True except OSError: return False