Files
SimpleFarm/farm/storage.py
T
snowy-yang a7725f8812 chore: 工程化改造前的基线快照
在引入 uv 依赖管理、ruff 静态检查与格式化、pyright 类型检查之前,
先把当前可正常运行的状态固定下来,作为后续大规模自动改动的回滚点。

此时状态:97 项测试全绿,HTTP + WebSocket 多牧场服务器可用。
2026-09-18 21:12:40 +08:00

130 lines
3.8 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.
"""存档读写。
存档是纯 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