refactor: 存档层从「一牧场一个 JSON」换成 SQLite(单库单表)
`saves/farm.db` 一张 farms 表,farm_id 主键,**一个牧场一行**。 接口签名不变(save_state / load_state / delete_save 只多了 farm_id 参数), 调用方只改了路径语义。 设计取舍:标量列 + 整份 payload。 * day/gold/level/ap/weather/forecast/net_worth 单独成列——外部工具或 AI 一条 SELECT 就能排榜、画曲线,不必先把每份存档读进内存; * 完整状态仍整份存 payload JSON:游戏数据隔三差五长新字段,规范化成 几十张表意味着每加字段写一次迁移,而存档只做整体读、整体写, 所以结构化列管可查询,完整性交给 JSON——加字段不用改表结构。 崩溃安全升级:写入包在事务里。原来的"临时文件 + os.replace"能挡进程 崩溃,挡不住写到一半掉电;SQLite 的日志能。读取失败照旧降级开新档。 向后兼容:SAVE_VERSION=2 / MIN_SAVE_VERSION=1 不变。老 JSON 存档由 migrate_json_saves() 在服务器启动时自动导入:同名已存在就跳过(宁可不 迁不覆盖),导入成功原文件改名 *.json.migrated 留底,先写库再改名 保证幂等;坏档一律跳过——迁移不许有能力阻止服务器启动。 异常边界:StorageError = sqlite3.Error,刻意不是 OSError 子类—— 调用方原来的 except OSError 全部跟着改,否则写盘失败会穿透出去。 性能实测:PRAGMA journal_mode=WAL 是持久属性,建库时设一次就够, 每次连接重设白花 2.2ms;save_state 约 6.9ms/次,大头是 synchronous=FULL 的 fsync——不省,存档的价值就是不丢档。bench 热路径无回归。 顺手修一个真 bug:connect() 建表失败时连接会泄漏,Windows 上句柄没关 就删不掉文件,而调用方正是靠"读失败→开新档"容错的。补 close。 测试:StorageTestCase 重写(往返/版本/损坏降级/多牧场隔离/迁移), test_server 断言 *.json 的用例改查库。513 项全过,ruff/pyright 全绿。 端到端:预置 v1/v2 老档起真服务器验 22 项——自动迁移、字段还原、 list_farms() 排榜、重启后进度分毫不差,全过。
This commit is contained in:
1 parent
6268283fc6
commit
bc3ae96a38
10 files changed
+563
-113
No files matched your search
@@ -324,7 +324,7 @@ SSE 长连接在两种协议下都照常工作。
|
||||
|
||||
## 7. 多人模型
|
||||
|
||||
* **一个牧场 = 一个玩家**:独立引擎、独立时钟、独立存档(`saves/<牧场名>.json`)。
|
||||
* **一个牧场 = 一个玩家**:独立引擎、独立时钟,存档在 `saves/farm.db`(SQLite,**一个牧场一行**)。
|
||||
* 牧场之间完全隔离(金币、土地、市场各自独立),但共享排行榜(`GET /api/farms`)。
|
||||
* 同牧场可以有多人/多程序同时操作,引擎内的 `RLock` 保证串行,不会出现"两个人同时种同一块地"这类竞态。
|
||||
* 所有牧场的时钟由**一个** `FarmClock` 线程统一驱动(而不是每个牧场一条线程)。
|
||||
@@ -351,7 +351,7 @@ uv run main.py play --url http://192.168.1.20:8765 --farm alice --token 找个
|
||||
| 日志 | `run/server.log`(`uv run main.py logs -n 50`) |
|
||||
| 停止 | `uv run main.py stop`(先走 HTTP 优雅停机,失败再发信号) |
|
||||
| 状态 | `uv run main.py status` |
|
||||
| 存档 | `saves/<牧场名>.json`(纯 JSON,可直接阅读分析) |
|
||||
| 存档 | `saves/farm.db`(SQLite;`sqlite3 saves/farm.db "SELECT farm_id,day,gold,net_worth FROM farms"` 可直接查) |
|
||||
| 接口文档 | <http://127.0.0.1:8765/docs> |
|
||||
|
||||
实现上不依赖 systemd / nohup / 任何第三方库:POSIX 用新会话、Windows 用 `DETACHED_PROCESS`,
|
||||
@@ -382,7 +382,7 @@ farm/
|
||||
config.py 数值与静态表(作物、牲畜、经济、时间)
|
||||
models.py Plot / Animal / GameState —— 只持有数据
|
||||
engine.py FarmEngine / Scheduler —— 纯游戏规则(不依赖网络与终端)
|
||||
storage.py 存档读写(临时文件 + 原子替换)
|
||||
storage.py 存档读写(SQLite,一牧场一行;老 JSON 档启动时自动导入)
|
||||
keys.py 跨平台按键读取
|
||||
net.py 回环请求绕过系统代理的小工具
|
||||
tui.py 终端界面(自适应宽度、双宽字符对齐,与引擎解耦)
|
||||
@@ -481,7 +481,9 @@ None 误用、属性不存在、参数类型不符。本项目的状态快照是
|
||||
* 推送节流 250ms,高频操作自动合并成一帧;每条推送带自增 `seq`,客户端据此丢弃乱序帧;
|
||||
* 引擎调用统一丢进线程池,不阻塞事件循环;
|
||||
* 单条连接/单个牧场出错不影响其它牧场;
|
||||
* 存档用"临时文件 + `os.replace`"原子替换,崩溃不会写坏存档;存档读取失败会降级为开新档而不是崩溃。
|
||||
* 存档是 SQLite(`saves/farm.db`,一牧场一行):写入包在事务里,掉电也不会写坏档;
|
||||
老版本 JSON 存档启动时自动导入(原文件改名 `*.json.migrated` 留底);
|
||||
存档读取失败会降级为开新档而不是崩溃。
|
||||
|
||||
**本地请求不走系统代理**(`farm/net.py`):
|
||||
|
||||
|
||||
Reference in new issue
Block a user