"""多牧场会话管理——服务器版的"世界"。 一个牧场 = 一个玩家 = 一份独立存档 + 一个独立时钟。 牧场之间互不干扰(各自的作物、金币、市场),但它们共享同一个服务器进程, 因此可以互相看到排行榜、串门看别人的农场。 并发模型 -------- * 每个牧场有一个 :class:`FarmEngine`,引擎内部自带 ``RLock``; * 同一牧场的多个请求由这把锁串行化(不会出现"两个人同时种同一块地"); * 不同牧场之间完全并行,互不阻塞; * 所有牧场的时钟由**一个** :class:`FarmClock` 线程统一驱动, 而不是每个牧场一个线程——牧场多了也不会有一堆线程在空转。 """ from __future__ import annotations import os import re import threading import time from typing import Any, Dict, List, Optional, Tuple from .. import config from ..engine import FarmEngine, GameError from ..storage import load_state, save_state DEFAULT_FARM = "default" MAX_FARM_ID = 24 # 牧场 ID 会变成存档文件名,所以只允许"安全且可读"的字符 _FARM_ID_RE = re.compile(r"^[A-Za-z0-9_\-\u4e00-\u9fff]{1,%d}$" % MAX_FARM_ID) def validate_farm_id(raw: Any, default: str = DEFAULT_FARM) -> str: """校验牧场 ID,返回规范化后的值。""" if raw is None or raw == "": return default if isinstance(raw, (int, float)): raw = str(raw) if not isinstance(raw, str): raise GameError("bad_farm", "牧场 ID 必须是字符串") fid = raw.strip() if not _FARM_ID_RE.match(fid): raise GameError( "bad_farm", f"牧场 ID {fid!r} 不合法:只允许 1~{MAX_FARM_ID} 个汉字、字母、数字、下划线或短横线", ) return fid def validate_owner(raw: Any) -> str: if raw is None: return "" if not isinstance(raw, (str, int, float)): raise GameError("bad_param", "owner 必须是字符串") text = str(raw).strip() if len(text) > 32: raise GameError("bad_param", "owner 最长 32 个字符") return text class FarmSession: """一个独立的农场玩家。""" def __init__(self, farm_id: str, save_dir: str, tick_seconds: float, owner: str = "") -> None: self.farm_id = farm_id self.owner = validate_owner(owner) or farm_id self.save_dir = save_dir self.path = os.path.join(save_dir, f"{farm_id}.json") self._lock = threading.RLock() self.engine = FarmEngine(tick_seconds=tick_seconds) # 由 FarmClock 维护,Publisher 读取;用普通属性即可,都是原子读写 self.dirty = True self.published_day = 0 self.last_events: List[str] = [] self.created_at = time.time() self.last_active = time.time() self.saved_at = 0.0 self.actions_taken = 0 self.load_message = "" self.engine.subscribe(self._on_change) self.load() # ------------------------------------------------------------ 生命周期 def _on_change(self) -> None: """引擎的任何状态变化都会走到这里(可能来自任意线程)。""" self.dirty = True def load(self) -> str: state, message = load_state(self.path) if state is not None: self.engine.state = state self.load_message = message self.engine.add_log(f"服务器载入存档:第 {state.day} 天继续经营", "info") else: self.engine.add_log(f"新的牧场「{self.farm_id}」开张,欢迎光临!", "info") self.load_message = f"{message},已开新档" self.published_day = self.engine.state.day self.dirty = True return self.load_message def save(self) -> str: with self._lock: path = save_state(self.engine.state, self.path) self.saved_at = time.time() return path def autosave(self) -> bool: """只在有变化时落盘,返回是否真的写了。""" if not self.dirty or self.save_dir == "": return False if time.time() - self.saved_at < 5: return False try: self.save() return True except OSError: return False def shutdown(self) -> None: try: self.save() except OSError: pass def touch(self, count_action: bool = False) -> None: self.last_active = time.time() if count_action: self.actions_taken += 1 self.dirty = True def advance_day(self, reason: str = "时间流逝") -> List[str]: with self._lock: events = self.engine.advance_day(reason=reason) self.last_events = events self.dirty = True return events def reset(self) -> None: with self._lock: self.engine.reset() self.touch(count_action=True) # ------------------------------------------------------------ 查询 def snapshot(self) -> Dict[str, Any]: data = self.engine.snapshot() data["farm"] = self.farm_id data["owner"] = self.owner data["tick_seconds"] = self.engine.tick_seconds return data def summary(self, online: int = 0) -> Dict[str, Any]: """给小而全的一行摘要:排行榜、`/api/world`、命令行 `status` 都用它。""" snap = self.engine.snapshot() return { "farm": self.farm_id, "owner": self.owner, "day": snap["day"], "gold": snap["gold"], "level": snap["level"], "net_worth": snap["net_worth"], "plots": f"{snap['plot_count']}/{snap['max_plots']}", "animals": snap["animal_count"], "goal_index": snap["goal_index"], "paused": snap["paused"], "online": online, "actions": self.actions_taken, "idle_seconds": round(time.time() - self.last_active, 1), } def state_message(self, seq: int = 0, note: str = "") -> Dict[str, Any]: from .hub import build_state_message return build_state_message(self, seq, note) class SessionManager: """所有牧场的登记处。""" def __init__(self, save_dir: str, tick_seconds: float = config.DEFAULT_TICK_SECONDS, max_sessions: int = 64, idle_timeout: float = 1800.0) -> None: self.save_dir = save_dir self.tick_seconds = tick_seconds self.max_sessions = max_sessions self.idle_timeout = idle_timeout self._lock = threading.RLock() self._sessions: Dict[str, FarmSession] = {} os.makedirs(save_dir, exist_ok=True) # ------------------------------------------------------------ 取用 def get(self, farm_id: str, create: bool = True, owner: str = "") -> FarmSession: farm_id = validate_farm_id(farm_id) with self._lock: session = self._sessions.get(farm_id) if session is not None: return session if not create: raise GameError("no_such_farm", f"牧场「{farm_id}」不存在", status=404) if len(self._sessions) >= self.max_sessions: raise GameError( "too_many_farms", f"服务器牧场数量已达上限({self.max_sessions})," "请稍后再试或联系管理员扩容", status=503, ) session = FarmSession(farm_id, self.save_dir, self.tick_seconds, owner) self._sessions[farm_id] = session return session def find(self, farm_id: str) -> Optional[FarmSession]: with self._lock: return self._sessions.get(validate_farm_id(farm_id)) def all(self) -> List[FarmSession]: with self._lock: return list(self._sessions.values()) def ids(self) -> List[str]: with self._lock: return sorted(self._sessions) def count(self) -> int: with self._lock: return len(self._sessions) # ------------------------------------------------------------ 世界视图 def leaderboard(self, online_map: Optional[Dict[str, int]] = None) -> List[Dict[str, Any]]: online_map = online_map or {} rows = [s.summary(online_map.get(s.farm_id, 0)) for s in self.all()] rows.sort(key=lambda row: (-row["net_worth"], -row["day"], row["farm"])) for rank, row in enumerate(rows, start=1): row["rank"] = rank return rows # ------------------------------------------------------------ 维护 def autosave_all(self) -> int: saved = 0 for session in self.all(): if session.autosave(): saved += 1 return saved def reap(self, online_map: Optional[Dict[str, int]] = None) -> List[str]: """回收长时间无人访问的牧场:先落盘,再从内存里移除。""" online_map = online_map or {} now = time.time() dropped: List[str] = [] for session in self.all(): if online_map.get(session.farm_id, 0) > 0: continue if session.engine.paused: continue # 暂停是人为操作,说明玩家还会回来 if now - session.last_active < self.idle_timeout: continue try: session.shutdown() except OSError: continue # 存不下就别丢,宁可占着内存 with self._lock: self._sessions.pop(session.farm_id, None) dropped.append(session.farm_id) return dropped def shutdown(self) -> None: for session in self.all(): session.shutdown() class FarmClock(threading.Thread): """统一驱动所有牧场的游戏时间。""" def __init__(self, manager: SessionManager, accuracy: float = 0.2) -> None: super().__init__(name="farm-clock", daemon=True) self.manager = manager self.accuracy = accuracy self._stop = threading.Event() self.ticks = 0 def stop(self) -> None: self._stop.set() def run(self) -> None: while not self._stop.wait(self.accuracy): now = time.time() for session in self.manager.all(): engine = session.engine try: if engine.paused: # 暂停时把下一次推进顺延,恢复后仍有完整的一个 tick engine.next_tick_at = now + engine.tick_seconds continue deadline = engine.next_tick_at if deadline is None: engine.next_tick_at = now + engine.tick_seconds continue if now >= deadline: session.advance_day("时间流逝") engine.next_tick_at = now + engine.tick_seconds self.ticks += 1 except Exception: # 单个牧场出错不影响其它牧场的时间 engine.next_tick_at = now + engine.tick_seconds def stats(self) -> Tuple[int, float]: return self.ticks, self.accuracy