style: 引入 ruff,474 项告警清零
配置见 pyproject.toml:
- line-length=100:贴着现有代码定。若用默认的 88 会重排 122 行,
噪音淹没真正有价值的修复。
- 启用 E/W/F/I/UP/B/C4/SIM/RET/PERF/RUF;**不**启用 PTH。因为本项目的路径
多是"字符串身份"——要塞进 subprocess 命令行、写进 JSON 存档、打印给用户,
全换 pathlib 只是包一层再拆一层,换来 70 处纯风格改动。
- 忽略 RUF001/002/003:中文全角标点是正确写法,不是"歧义字符"。
- 忽略 PERF203:剩下两处都是"逐项容错"语义(逐个牧场存档要立刻上报失败、
收获全部要跳过失败地块)。把 try 提到循环外会毁掉"一项失败不影响其它项"
这个行为,是错的优化。
自动修复 358 项:Dict/List/Optional 换成 PEP 585/604 写法、导入排序、
失效的 noqa、空占位 f-string 等。
手工修复的(这些是真问题,不是风格偏好):
- B904 x9:except 中 raise 补异常链。刻意区分——网络/IO 失败用 from exc 保留
根因;参数解析失败用 from None,因为底层 ValueError 对玩家只是噪音。
- RUF012 x2:StateView 的 plots/animals 原本是**类级共享可变列表**,
改成在 __init__ 里初始化(apply({}) 会提前返回,所以必须显式摆好)。
- F841:TUI 里一个赋值后再没用过的死变量。
- SIM105 x11:try/except/pass 统一改成 contextlib.suppress。
- SIM115:日志句柄改用 with。Popen 已把 fd 复制给子进程,父进程可安全关闭。
- E501 x7:拆超长行;main.py 那处顺手提取变量,避免超长嵌套表达式。
- E741:把 l/r 改成 left_line/right_line。
- 其余:RUF046 / PERF401 / RET504 / RUF005 / UP031 / B007。
ruff format 重排 24 个文件。
验证:97 项测试全绿。
This commit is contained in:
1 parent
c7c8a6308a
commit
fcd4cb4649
25 files changed
+1204
-794
No files matched your search
+154
-111
@@ -15,10 +15,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
||||
@@ -40,7 +42,7 @@ class ServerConfig:
|
||||
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 8765
|
||||
token: Optional[str] = None
|
||||
token: str | None = None
|
||||
tick_seconds: float = config.DEFAULT_TICK_SECONDS
|
||||
save_dir: str = "saves"
|
||||
max_sessions: int = 64
|
||||
@@ -48,7 +50,7 @@ class ServerConfig:
|
||||
autosave_interval: float = 60.0
|
||||
allow_remote_control: bool = True
|
||||
world_name: str = "SimpleFarm 公共农场"
|
||||
extra: Dict[str, Any] = field(default_factory=dict)
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class ServerHandle:
|
||||
@@ -59,8 +61,8 @@ class ServerHandle:
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._start_callbacks: List[Callable[[], None]] = []
|
||||
self._stop_callbacks: List[Callable[[], None]] = []
|
||||
self._start_callbacks: list[Callable[[], None]] = []
|
||||
self._stop_callbacks: list[Callable[[], None]] = []
|
||||
self.stopping = False
|
||||
|
||||
def on_start(self, callback: Callable[[], None]) -> None:
|
||||
@@ -71,25 +73,23 @@ class ServerHandle:
|
||||
|
||||
def notify_start(self) -> None:
|
||||
for callback in list(self._start_callbacks):
|
||||
try:
|
||||
# 单个启动钩子失败不该拖垮整个服务
|
||||
with contextlib.suppress(Exception):
|
||||
callback()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def request_stop(self) -> bool:
|
||||
if self.stopping:
|
||||
return False
|
||||
self.stopping = True
|
||||
for callback in list(self._stop_callbacks):
|
||||
try:
|
||||
# 停机要尽力而为:一个钩子抛异常不能拦住其它钩子
|
||||
with contextlib.suppress(Exception):
|
||||
callback()
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- 工具
|
||||
def brief(session: FarmSession, hub: ConnectionHub) -> Dict[str, Any]:
|
||||
def brief(session: FarmSession, hub: ConnectionHub) -> dict[str, Any]:
|
||||
"""操作后回给调用者的精简状态:够做下一步决策,又不至于太吵。"""
|
||||
snap = session.snapshot()
|
||||
return {
|
||||
@@ -125,7 +125,7 @@ def rules_text() -> str:
|
||||
f"1. 时间:每个牧场每 {config.DEFAULT_TICK_SECONDS:.0f} 秒自动推进一天(可配置)。"
|
||||
f"每天开始时行动点恢复为 {config.START_AP} 点,作物生长、牲畜产出、市场价格都在此刻结算。\n"
|
||||
f"2. 行动点:播种/浇水/喂食各消耗 1 点,收获与收取产出免费。行动点用完只能等第二天"
|
||||
"(POST /api/actions {\"action\":\"sleep\"} 可主动推进一天,有 2 秒冷却)。\n"
|
||||
'(POST /api/actions {"action":"sleep"} 可主动推进一天,有 2 秒冷却)。\n'
|
||||
f"3. 水分:作物每天消耗 1 点水分(上限 {config.WATER_CAPACITY}),水分为 0 时停止生长。"
|
||||
f"播种时自带 {config.PLANT_INITIAL_MOISTURE} 点水分。\n"
|
||||
f"4. 收获:成熟后 harvest 获得金币与经验,有 {config.LUCKY_CHANCE:.0%} 概率丰收翻倍。\n"
|
||||
@@ -139,17 +139,17 @@ def rules_text() -> str:
|
||||
"排行榜见 GET /api/world。\n"
|
||||
"\n推荐决策循环(AI 最短路径):\n"
|
||||
" GET /api/actions?farm=me → 拿到当前所有**合法**动作(含开销与理由)\n"
|
||||
" POST /api/actions → {\"action\":\"<上一步里的 action>\","
|
||||
"\"params\":{...}} 直接执行\n"
|
||||
' POST /api/actions → {"action":"<上一步里的 action>",'
|
||||
'"params":{...}} 直接执行\n'
|
||||
" 响应里的 state 就是执行后的最新状态,无需再查一次。\n"
|
||||
"\n实时通道:ws://host/ws?farm=me ,服务器在状态变化时推送 state 消息;"
|
||||
"也可以直接在 WS 上发送 {\"type\":\"action\",\"action\":\"plant\","
|
||||
"\"params\":{\"plot\":1,\"crop\":\"wheat\"},\"id\":1} 并等待 result 回执。\n"
|
||||
'也可以直接在 WS 上发送 {"type":"action","action":"plant",'
|
||||
'"params":{"plot":1,"crop":"wheat"},"id":1} 并等待 result 回执。\n'
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- 应用
|
||||
def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
def create_app(cfg: ServerConfig | None = None) -> FastAPI:
|
||||
cfg = cfg or ServerConfig()
|
||||
manager = SessionManager(
|
||||
save_dir=cfg.save_dir,
|
||||
@@ -206,12 +206,12 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------ 鉴权与取牧场
|
||||
def authorized(supplied: Optional[str]) -> bool:
|
||||
def authorized(supplied: str | None) -> bool:
|
||||
if not cfg.token:
|
||||
return True
|
||||
return supplied == cfg.token
|
||||
|
||||
def token_of(request: Request) -> Optional[str]:
|
||||
def token_of(request: Request) -> str | None:
|
||||
return request.headers.get("x-api-token") or request.query_params.get("token")
|
||||
|
||||
def guard(request: Request) -> None:
|
||||
@@ -227,19 +227,19 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
raw = (request.query_params.get("full") or "").lower()
|
||||
return raw in ("1", "true", "yes", "on")
|
||||
|
||||
async def body_of(request: Request) -> Dict[str, Any]:
|
||||
async def body_of(request: Request) -> dict[str, Any]:
|
||||
raw = await request.body()
|
||||
if not raw:
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(raw.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise GameError("bad_json", f"请求体不是合法 JSON:{exc}")
|
||||
raise GameError("bad_json", f"请求体不是合法 JSON:{exc}") from exc
|
||||
if not isinstance(data, dict):
|
||||
raise GameError("bad_json", "请求体必须是 JSON 对象")
|
||||
return data
|
||||
|
||||
def execute_action(session: FarmSession, name: Any, params: Any) -> Dict[str, Any]:
|
||||
def execute_action(session: FarmSession, name: Any, params: Any) -> dict[str, Any]:
|
||||
"""执行一个动作,返回引擎/会话级的结果字典。
|
||||
|
||||
``save`` 与 ``reset`` 必须由**牧场会话**处理而不是引擎:
|
||||
@@ -250,21 +250,24 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
if key == "save":
|
||||
path = session.save()
|
||||
session.engine.add_log(f"已存档到 {path}", "info")
|
||||
return {"ok": True, "action": "save", "path": path,
|
||||
"message": f"已存档到 {path}"}
|
||||
return {"ok": True, "action": "save", "path": path, "message": f"已存档到 {path}"}
|
||||
if key == "reset":
|
||||
session.reset()
|
||||
return {"ok": True, "action": "reset",
|
||||
"message": f"牧场「{session.farm_id}」已重置,重新开始经营"}
|
||||
return {
|
||||
"ok": True,
|
||||
"action": "reset",
|
||||
"message": f"牧场「{session.farm_id}」已重置,重新开始经营",
|
||||
}
|
||||
return actions_mod.run_action(session.engine, name, params)
|
||||
|
||||
def run_and_wrap(session: FarmSession, name: Any, params: Any,
|
||||
full: bool = False) -> Dict[str, Any]:
|
||||
def run_and_wrap(
|
||||
session: FarmSession, name: Any, params: Any, full: bool = False
|
||||
) -> dict[str, Any]:
|
||||
"""执行动作并拼装统一响应。HTTP 与 WS 共用。"""
|
||||
result = execute_action(session, name, params)
|
||||
counters["actions"] += 1
|
||||
session.touch(count_action=True)
|
||||
payload: Dict[str, Any] = dict(result)
|
||||
payload: dict[str, Any] = dict(result)
|
||||
payload["ok"] = True
|
||||
payload["farm"] = session.farm_id
|
||||
payload["seq"] = next_seq()
|
||||
@@ -285,7 +288,7 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
|
||||
# ================================================================ 元信息
|
||||
@app.get("/api/health", tags=["元信息"], summary="健康检查(无需鉴权)")
|
||||
async def health() -> Dict[str, Any]:
|
||||
async def health() -> dict[str, Any]:
|
||||
return {
|
||||
"ok": True,
|
||||
"service": SERVER_NAME,
|
||||
@@ -298,7 +301,7 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
}
|
||||
|
||||
@app.get("/api/help", tags=["元信息"], summary="玩法规则与接口说明(给 AI 的说明书)")
|
||||
async def help_page(request: Request) -> Dict[str, Any]:
|
||||
async def help_page(request: Request) -> dict[str, Any]:
|
||||
guard(request)
|
||||
return {
|
||||
"ok": True,
|
||||
@@ -313,14 +316,16 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
"catalog": "GET /api/catalog?farm=NAME",
|
||||
"actions": "GET /api/actions?farm=NAME",
|
||||
"log": "GET /api/log?farm=NAME&limit=20&since=0",
|
||||
"execute": "POST /api/actions {\"action\":\"plant\",\"params\":{\"plot\":1,\"crop\":\"wheat\"}}",
|
||||
"farms": "GET /api/farms | POST /api/farms {\"farm\":\"NAME\"}",
|
||||
"execute": (
|
||||
'POST /api/actions {"action":"plant","params":{"plot":1,"crop":"wheat"}}'
|
||||
),
|
||||
"farms": 'GET /api/farms | POST /api/farms {"farm":"NAME"}',
|
||||
"realtime": "WS /ws?farm=NAME",
|
||||
},
|
||||
}
|
||||
|
||||
@app.get("/api/world", tags=["世界"], summary="世界概览:排行榜 + 在线连接 + 时钟")
|
||||
async def world(request: Request) -> Dict[str, Any]:
|
||||
async def world(request: Request) -> dict[str, Any]:
|
||||
guard(request)
|
||||
return {
|
||||
"ok": True,
|
||||
@@ -336,13 +341,13 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
|
||||
# ================================================================ 牧场
|
||||
@app.get("/api/farms", tags=["世界"], summary="牧场排行榜")
|
||||
async def list_farms(request: Request) -> Dict[str, Any]:
|
||||
async def list_farms(request: Request) -> dict[str, Any]:
|
||||
guard(request)
|
||||
rows = manager.leaderboard(hub.online_map())
|
||||
return {"ok": True, "count": len(rows), "farms": rows}
|
||||
|
||||
@app.post("/api/farms", tags=["世界"], summary="创建或进入一个牧场")
|
||||
async def create_farm(request: Request) -> Dict[str, Any]:
|
||||
async def create_farm(request: Request) -> dict[str, Any]:
|
||||
guard(request)
|
||||
body = await body_of(request)
|
||||
farm = validate_farm_id(body.get("farm") or body.get("name"))
|
||||
@@ -354,7 +359,7 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
try:
|
||||
tick_value = float(tick)
|
||||
except (TypeError, ValueError):
|
||||
raise GameError("bad_param", "tick 必须是数字")
|
||||
raise GameError("bad_param", "tick 必须是数字") from None
|
||||
session.engine.tick_seconds = max(
|
||||
config.MIN_TICK_SECONDS, min(config.MAX_TICK_SECONDS, tick_value)
|
||||
)
|
||||
@@ -370,7 +375,7 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
|
||||
# ================================================================ 单牧场只读
|
||||
@app.get("/api/state", tags=["牧场"], summary="完整状态快照")
|
||||
async def get_state(request: Request) -> Dict[str, Any]:
|
||||
async def get_state(request: Request) -> dict[str, Any]:
|
||||
guard(request)
|
||||
session = session_of(request)
|
||||
return {
|
||||
@@ -381,13 +386,13 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
}
|
||||
|
||||
@app.get("/api/catalog", tags=["牧场"], summary="作物/牲畜目录与实时价格")
|
||||
async def get_catalog(request: Request) -> Dict[str, Any]:
|
||||
async def get_catalog(request: Request) -> dict[str, Any]:
|
||||
guard(request)
|
||||
session = session_of(request)
|
||||
return {"ok": True, "farm": session.farm_id, "catalog": session.engine.catalog()}
|
||||
|
||||
@app.get("/api/actions", tags=["牧场"], summary="当前所有合法动作(AI 推荐入口)")
|
||||
async def get_actions(request: Request) -> Dict[str, Any]:
|
||||
async def get_actions(request: Request) -> dict[str, Any]:
|
||||
guard(request)
|
||||
session = session_of(request)
|
||||
items = session.engine.available_actions()
|
||||
@@ -400,11 +405,11 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
}
|
||||
|
||||
@app.get("/api/log", tags=["牧场"], summary="最近事件")
|
||||
async def get_log(request: Request) -> Dict[str, Any]:
|
||||
async def get_log(request: Request) -> dict[str, Any]:
|
||||
guard(request)
|
||||
session = session_of(request)
|
||||
limit = _int(request.query_params.get("limit"), 20, 1, 200)
|
||||
since = _int(request.query_params.get("since"), 0, 0, 10 ** 9)
|
||||
since = _int(request.query_params.get("since"), 0, 0, 10**9)
|
||||
entries = session.engine.recent_log(limit=limit, since=since)
|
||||
return {
|
||||
"ok": True,
|
||||
@@ -416,7 +421,7 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
|
||||
# ================================================================ 写操作
|
||||
@app.post("/api/actions", tags=["动作"], summary="通用动作入口(推荐 AI 使用)")
|
||||
async def post_action(request: Request) -> Dict[str, Any]:
|
||||
async def post_action(request: Request) -> dict[str, Any]:
|
||||
guard(request)
|
||||
body = await body_of(request)
|
||||
session = session_of(request)
|
||||
@@ -428,7 +433,7 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
return run_and_wrap(session, name, params, full_requested(request))
|
||||
|
||||
@app.post("/api/batch", tags=["动作"], summary="批量执行动作(一个请求干多件事)")
|
||||
async def post_batch(request: Request) -> Dict[str, Any]:
|
||||
async def post_batch(request: Request) -> dict[str, Any]:
|
||||
"""给 AI 的省流入口:一次提交多个动作,按顺序执行。
|
||||
|
||||
每个动作独立成败,失败的只记错误不中断后续动作。
|
||||
@@ -441,7 +446,7 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
raise GameError("bad_param", "actions 必须是至少含一个元素的数组")
|
||||
if len(items) > 50:
|
||||
raise GameError("too_many", "单次批量最多 50 个动作")
|
||||
results: List[Dict[str, Any]] = []
|
||||
results: list[dict[str, Any]] = []
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
results.append({"ok": False, "error": "bad_param", "message": "每个动作必须是对象"})
|
||||
@@ -455,7 +460,7 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
counters["errors"] += 1
|
||||
results.append({**exc.to_dict(), "action": item.get("action")})
|
||||
ok_count = sum(1 for r in results if r.get("ok"))
|
||||
payload: Dict[str, Any] = {
|
||||
payload: dict[str, Any] = {
|
||||
"ok": True,
|
||||
"farm": session.farm_id,
|
||||
"done": ok_count,
|
||||
@@ -468,7 +473,7 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
return payload
|
||||
|
||||
def make_action_route(action_name: str) -> Callable[[Request], Any]:
|
||||
async def route(request: Request) -> Dict[str, Any]:
|
||||
async def route(request: Request) -> dict[str, Any]:
|
||||
guard(request)
|
||||
body = await body_of(request)
|
||||
session = session_of(request)
|
||||
@@ -489,19 +494,21 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
|
||||
# ================================================================ 管理
|
||||
@app.post("/api/admin/save", tags=["管理"], summary="把所有牧场写入存档")
|
||||
async def admin_save(request: Request) -> Dict[str, Any]:
|
||||
async def admin_save(request: Request) -> dict[str, Any]:
|
||||
guard(request)
|
||||
saved: List[str] = []
|
||||
saved: list[str] = []
|
||||
for session in manager.all():
|
||||
try:
|
||||
session.save()
|
||||
saved.append(session.farm_id)
|
||||
except OSError as exc:
|
||||
raise GameError("save_failed", f"{session.farm_id} 存档失败:{exc}", status=500)
|
||||
raise GameError(
|
||||
"save_failed", f"{session.farm_id} 存档失败:{exc}", status=500
|
||||
) from exc
|
||||
return {"ok": True, "message": f"已保存 {len(saved)} 个牧场", "farms": saved}
|
||||
|
||||
@app.post("/api/admin/shutdown", tags=["管理"], summary="优雅停机(守护进程 stop 使用)")
|
||||
async def admin_shutdown(request: Request) -> Dict[str, Any]:
|
||||
async def admin_shutdown(request: Request) -> dict[str, Any]:
|
||||
guard(request)
|
||||
if not cfg.allow_remote_control:
|
||||
raise GameError("forbidden", "服务器未开放远程停机", status=403)
|
||||
@@ -535,33 +542,37 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
session = manager.get(farm, create=True)
|
||||
except GameError as exc:
|
||||
# 例如牧场数量达到上限:说清楚原因再关,别让客户端以为是网络问题
|
||||
await websocket.send_json({"type": "error", "error": exc.code,
|
||||
"message": exc.message})
|
||||
await websocket.send_json({"type": "error", "error": exc.code, "message": exc.message})
|
||||
await websocket.close(code=1013, reason=exc.message[:100])
|
||||
return
|
||||
hub.register(client)
|
||||
session.touch()
|
||||
|
||||
await websocket.send_json({
|
||||
"type": "welcome",
|
||||
"client_id": client.cid,
|
||||
"farm": farm,
|
||||
"server": {
|
||||
"name": SERVER_NAME,
|
||||
"version": API_VERSION,
|
||||
"world": cfg.world_name,
|
||||
"tick_seconds": session.engine.tick_seconds,
|
||||
},
|
||||
"state": session.snapshot(),
|
||||
"catalog": session.engine.catalog(),
|
||||
"actions": session.engine.available_actions(),
|
||||
"log": session.engine.recent_log(limit=12),
|
||||
"online": hub.online(farm),
|
||||
})
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "welcome",
|
||||
"client_id": client.cid,
|
||||
"farm": farm,
|
||||
"server": {
|
||||
"name": SERVER_NAME,
|
||||
"version": API_VERSION,
|
||||
"world": cfg.world_name,
|
||||
"tick_seconds": session.engine.tick_seconds,
|
||||
},
|
||||
"state": session.snapshot(),
|
||||
"catalog": session.engine.catalog(),
|
||||
"actions": session.engine.available_actions(),
|
||||
"log": session.engine.recent_log(limit=12),
|
||||
"online": hub.online(farm),
|
||||
}
|
||||
)
|
||||
hub.broadcast(
|
||||
farm,
|
||||
{"type": "notice", "farm": farm,
|
||||
"text": f"{client.name or client.cid} 来到了牧场(在线 {hub.online(farm)})"},
|
||||
{
|
||||
"type": "notice",
|
||||
"farm": farm,
|
||||
"text": f"{client.name or client.cid} 来到了牧场(在线 {hub.online(farm)})",
|
||||
},
|
||||
exclude=client,
|
||||
)
|
||||
|
||||
@@ -585,12 +596,14 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
await websocket.send_json({"type": "error", "error": "bad_json",
|
||||
"message": f"不是合法 JSON:{exc}"})
|
||||
await websocket.send_json(
|
||||
{"type": "error", "error": "bad_json", "message": f"不是合法 JSON:{exc}"}
|
||||
)
|
||||
return
|
||||
if not isinstance(data, dict):
|
||||
await websocket.send_json({"type": "error", "error": "bad_json",
|
||||
"message": "消息必须是 JSON 对象"})
|
||||
await websocket.send_json(
|
||||
{"type": "error", "error": "bad_json", "message": "消息必须是 JSON 对象"}
|
||||
)
|
||||
return
|
||||
|
||||
kind = str(data.get("type") or "").lower()
|
||||
@@ -601,55 +614,79 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
return
|
||||
|
||||
if kind in ("sync", "refresh", "state"):
|
||||
await websocket.send_json({
|
||||
"type": "sync",
|
||||
"id": rid,
|
||||
"seq": next_seq(),
|
||||
"farm": session.farm_id,
|
||||
"state": session.snapshot(),
|
||||
"actions": session.engine.available_actions(),
|
||||
"log": session.engine.recent_log(limit=20),
|
||||
})
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "sync",
|
||||
"id": rid,
|
||||
"seq": next_seq(),
|
||||
"farm": session.farm_id,
|
||||
"state": session.snapshot(),
|
||||
"actions": session.engine.available_actions(),
|
||||
"log": session.engine.recent_log(limit=20),
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
if kind == "help":
|
||||
await websocket.send_json({"type": "help", "id": rid, "rules": rules_text(),
|
||||
"actions": actions_mod.describe_actions()})
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "help",
|
||||
"id": rid,
|
||||
"rules": rules_text(),
|
||||
"actions": actions_mod.describe_actions(),
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
if kind == "action":
|
||||
try:
|
||||
# 引擎是同步的,丢到线程池里跑,别堵住事件循环
|
||||
result = await asyncio.to_thread(
|
||||
execute_action, session, data.get("action"),
|
||||
execute_action,
|
||||
session,
|
||||
data.get("action"),
|
||||
data.get("params") or {},
|
||||
)
|
||||
except GameError as exc:
|
||||
counters["errors"] += 1
|
||||
await websocket.send_json({"type": "result", "id": rid, "ok": False,
|
||||
"error": exc.code, "message": exc.message,
|
||||
"farm": session.farm_id})
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "result",
|
||||
"id": rid,
|
||||
"ok": False,
|
||||
"error": exc.code,
|
||||
"message": exc.message,
|
||||
"farm": session.farm_id,
|
||||
}
|
||||
)
|
||||
return
|
||||
counters["actions"] += 1
|
||||
session.touch(count_action=True)
|
||||
await websocket.send_json({
|
||||
"type": "result",
|
||||
"id": rid,
|
||||
"ok": True,
|
||||
"farm": session.farm_id,
|
||||
"message": result.get("message", "完成"),
|
||||
"result": result,
|
||||
"state": brief(session, hub),
|
||||
# 回执里带上完整快照:客户端可以立刻刷新画面,不必等下一次推送
|
||||
"snapshot": session.snapshot(),
|
||||
"log": session.engine.recent_log(limit=3),
|
||||
"seq": next_seq(),
|
||||
})
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "result",
|
||||
"id": rid,
|
||||
"ok": True,
|
||||
"farm": session.farm_id,
|
||||
"message": result.get("message", "完成"),
|
||||
"result": result,
|
||||
"state": brief(session, hub),
|
||||
# 回执里带上完整快照:客户端可以立刻刷新画面,不必等下一次推送
|
||||
"snapshot": session.snapshot(),
|
||||
"log": session.engine.recent_log(limit=3),
|
||||
"seq": next_seq(),
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
await websocket.send_json({"type": "error", "error": "unknown_message",
|
||||
"message": f"不认识的消息类型 {kind!r}",
|
||||
"hint": "可用:action / ping / sync / help"})
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "error",
|
||||
"error": "unknown_message",
|
||||
"message": f"不认识的消息类型 {kind!r}",
|
||||
"hint": "可用:action / ping / sync / help",
|
||||
}
|
||||
)
|
||||
|
||||
writer_task = asyncio.create_task(writer())
|
||||
reader_task = asyncio.create_task(reader())
|
||||
@@ -666,20 +703,26 @@ def create_app(cfg: Optional[ServerConfig] = None) -> FastAPI:
|
||||
session.touch()
|
||||
hub.broadcast(
|
||||
session.farm_id,
|
||||
{"type": "notice", "farm": session.farm_id,
|
||||
"text": f"{client.name or client.cid} 离开了牧场(在线 {hub.online(session.farm_id)})"},
|
||||
{
|
||||
"type": "notice",
|
||||
"farm": session.farm_id,
|
||||
"text": (
|
||||
f"{client.name or client.cid} 离开了牧场"
|
||||
f"(在线 {hub.online(session.farm_id)})"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def _int(raw: Optional[str], default: int, low: int, high: int) -> int:
|
||||
def _int(raw: str | None, default: int, low: int, high: int) -> int:
|
||||
if raw is None or raw == "":
|
||||
return default
|
||||
try:
|
||||
value = int(raw)
|
||||
except (TypeError, ValueError):
|
||||
raise GameError("bad_param", f"参数必须是整数,收到 {raw!r}")
|
||||
raise GameError("bad_param", f"参数必须是整数,收到 {raw!r}") from None
|
||||
return max(low, min(high, value))
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user