# 01 · 服务端环境与框架基础 本篇建立**全局认知**:子游戏运行在什么环境里、平台框架由哪些对象构成、一个数据包如何被路由到你的代码、一个房间从生到死经历哪些阶段。读懂这一篇,后面的接入与协议才有坐标。 > 举例以麻将类房卡游戏为主,但本篇所有机制对任意子游戏一致。 --- ## 1. 运行环境:一套代码,两个运行时 子游戏代码**同时运行在两套环境**,写每一行都要同时考虑: | 运行时 | 场景 | 模块加载方式 | 是否有 `require` | |--------|------|--------------|------------------| | **Node.js** | 本地、单元测试 | `require()` | 有 | | **浏览器 / 友乐平台** | 线上部署 | 由 `mod.js` 用 `min_loadJsFile` 统一加载为**全局对象** | **没有** | 由此引出两条硬约束(详见 04): 1. **严格 ES5**:用 `var`/`function`,禁止 `let`/`const`/箭头函数/模板串/`class`/`Promise` 等;线上浏览器/友乐运行时不保证 ES6+。 2. **`require` 只能在文件开头的守卫块内**: ```js // ✅ 正确:集中在顶部守卫块;运行时按全局名引用 if (typeof require !== 'undefined') { var GameStateManager = require('./dataStructures/GameStateManager.js'); } // ... 之后直接用全局名 GameStateManager.xxx() ——浏览器由 mod.js 加载为同名全局 ``` 浏览器运行时没有 `require`,任何**无守卫的、函数体内的中途 `require`** 都会抛 `ReferenceError: require is not defined`,导致功能崩溃。跨模块统一**按全局名引用**:Node 由守卫块 `var X = require(...)` 得到,浏览器由 `mod.js` 加载的同名全局解析,二者靠共享全局作用域下的同名 `var` 统一。 ### 前后端是物理分离的两端 - 客户端跑在用户浏览器,服务端跑在 Node。两端**不是函数调用,而是 WebSocket/HTTP 收发 JSON 包**,全程异步。 - 前端代码不走 npm 构建,**前后端共享代码靠文件复制同步**(见 04 的 shared 同步流程)。 - **服务器权威**:凡房卡、胜负、积分、房间状态等关键数据/操作,一律以服务器为准,前端只负责显示,不能"前端说什么就是什么"。 --- ## 2. 平台代码 vs 子游戏代码 ``` server/ ← 平台框架(除 games2/<你的游戏>/ 外,全部禁改) ├── packet.js 总收发包入口、应用列表、最终 SendPack ├── applist.js 加载各应用(server / youle / update) ├── class/ 框架基础类 │ ├── class.app.js 应用基类 cls_app(按 route 路由到模块) │ ├── class.mod.js 模块基类 cls_mod(按 rpc 路由到方法) │ └── class.desk.js / ... 桌、牌等基础类 ├── youle/ 友乐应用(appname = "youle") │ ├── app.js 创建 youle_app │ └── server_room/ 房间模块 youle_room(routename = "room") │ ├── class.room.js 房间对象 o_room(含 seatlist、发包方法) │ ├── class.player.js 座位/玩家对象(含 conmode/fromid) │ ├── class.export.js 框架对外服务:check_player / deduct_roomcard / save_grade ... │ └── class.import.js 框架回调子游戏:makewar_deskwar / get_disbandRoom / player_* ... └── games2/ └── <你的游戏>/ ← 子游戏(唯一可编辑目录) ├── mod.js 模块入口:创建模块、加载文件、定义 RPC 方法 ├── export.js 子游戏对平台暴露的 8 个必需接口 ├── import.js 子游戏对平台服务的 4 个封装接口 └── ... 你的玩法业务代码 ``` **一句话**:平台提供"网络 + 房间 + 玩家 + 房卡 + 战绩"的地基,子游戏只往 `games2/<你的游戏>/` 里填"玩法"。两边的接缝就是 **export / import**(详见 02)。 --- ## 3. 核心对象模型 理解五个对象的层级关系,就理解了框架的数据骨架: ``` app (youle_app) 一个应用,appname = "youle" └── modlist[] 应用下挂的所有模块 ├── youle_room 平台房间模块,routename = "room" └── mod_<你的游戏> 你的子游戏模块,routename = "<你的游戏>" o_room 房间对象(平台创建,一桌一个) ├── roomcode / roomtype / asetcount / battlestate ... 平台基础数据 ├── seatlist[] 座位列表,长度=满桌人数 │ └── seatlist[seat] = o_player 座位上的玩家对象 │ ├── playerid / nickname / onstate │ └── conmode / fromid 连接方式与连接ID(发包要用) ├── method.sendpack_toseat(msg, seat) 平台发包方法 ├── method.sendpack_toother(msg, seat) └── o_desk ★ 子游戏的牌桌对象(由你在 makewar 里创建并挂上) ├── o_room 反向引用回房间 └── data.* ★ 你的对局状态全挂这里(房间隔离的落点) ``` 要点: - **`o_room` 由平台创建并持有**;`seatlist[seat]` 是玩家对象,发包所需的 `conmode`/`fromid` 就在它上面。 - **`o_desk` 由子游戏创建**(在 `makewar` 中),并与 `o_room` 建立双向引用:`o_room.o_desk = desk; desk.o_room = o_room;`。 - **对局状态一律挂在 `o_room.o_desk.data.*`**。这是"房间隔离"的物理落点:每桌一份,互不串扰(见 04)。 > 举例:麻将把游戏状态放在 `o_room.o_desk.data.roomAdapter.gameState`,把操作队列放在 `o_room.o_desk.data.operationQueue`。换成斗地主、跑得快也是同一套落点,只是 `data.*` 下的字段不同。 --- ## 4. 三层路由:一个包如何到达你的函数 平台把客户端发来的包,按 `app → route → rpc` 三级精确投递到子游戏的某个函数。 ``` { app: "youle", route: "<你的游戏>", rpc: "playCard", data: { ... } } ``` | 级 | 在哪 | 依据 | 动作 | |----|------|------|------| | 1 | `packet.js` `packet_face.ReceivePack` | `pack.app` | 在 `applist` 里找到 `appname == pack.app` 的应用,调 `app.ReceivePack(pack)` | | 2 | `class/class.app.js` `cls_app.ReceivePack` | `pack.route` | 在 `app.modlist` 里找到 `routename == pack.route` 的模块,调 `mod.DoPack(pack)` | | 3 | `class/class.mod.js` `cls_mod.DoPack` | `pack.rpc` | 若 `mod[pack.rpc]` 存在,调用 `mod[pack.rpc](pack)` | 证据: - 第 1 级:`server/packet.js` `ReceivePack` 按 `pack.app == applist[i].appname` 分发。 - 第 2 级:`server/class/class.app.js` `ReceivePack` 按 `pack.route == modlist[i].routename` 调 `DoPack`。 - 第 3 级:`server/class/class.mod.js` `DoPack` 按 `_msg.rpc` 调 `_obj_mod[_msg.rpc](_msg)`。 **对开发者的含义**:你在 `mod.js` 里写下 `mod_<游戏>.playCard = function(pack){...}`,前端只要发 `{route:"<你的游戏>", rpc:"playCard", ...}`,框架就会自动调到它——**无需自己写路由分发**,更不要在一个总入口里用 `switch(action)` 做二次路由(一个操作对应一个 RPC 方法)。 ### ⚠️ 关于 `DoPack` 的返回值(重要) `cls_app.ReceivePack` 在拿到 `DoPack` 的返回值后,确实会执行一次 `app.SendPack(repack)`。但这个回发: - 只回给**当前请求的那条连接**,且不经过"按座位定向"的处理; - 在浏览器/友乐链路下**不是子游戏向前端推送状态的可靠通道**。 因此本项目的铁律是:**子游戏 RPC handler 一律通过 `o_room.method.sendpack_toseat / sendpack_toother` 主动推送**来下发结果,**不依赖 `return`**。这条直接推导出 03 的"成败标志必须放在主动推送的 `data.success` 里"。 --- ## 5. 模块注册与全局暴露 子游戏模块用框架基类创建,并自动注册进应用: ```js // cls_mod.new(模块名, 路由名, 所属应用) var mod_<你的游戏> = mod_<你的游戏> || cls_mod.new("mod_<你的游戏>", "<你的游戏>", youle_app); ``` `cls_mod.new` 做了三件事:把模块 push 进 `app.modlist`、在 `app` 上挂 `app[模块名]=mod`、并通过 `OutputMod` 把模块**暴露为全局名**(`global[modname]`)。这套"双运行时全局暴露"是浏览器/友乐能按全局名找到模块的基础。 > 双运行时陷阱:凡"构造函数 + 单例实例"式模块,`module.exports` 之后必须**无条件**把全局名暴露出去(不要塞进 `else` 分支),否则线上浏览器拿不到该全局,报 `xxx is not a function`,而 Node 测试却测不出来。 `export.js` / `import.js` 在各自文件末尾把接口对象挂到 `mod.export` / `mod.import`,`mod.js` 只负责按顺序加载它们(见 02)。 --- ## 6. 房间生命周期 一个房间从创建到回收,平台与子游戏分工如下。**理解每个阶段"谁调用谁",是接入的关键**: ``` 创建房间 平台调 export.get_needroomcard / get_asetcount / get_needroomcard_joinroom → 决定房卡与总局数(此时还没有 o_desk,子游戏不需要知道谁坐在哪) 满桌/房主开战 平台调 export.makewar(o_room, o_game_config) → 子游戏创建 o_desk、初始化对局状态、返回开战数据包 → 平台把开战包下发所有客户端(对应前端 StartWar) → o_room.battlestate = 1 对局进行中 客户端发 RPC → mod[rpc](pack) → 子游戏处理 → 主动推送结果 断线重连 / 中途加入 平台调 export.get_deskinfo(o_room, seat) → 子游戏返回该座位「当前完整快照」(对应前端 Reconnect) 每小局结算 第一小局结算时:子游戏调 import.deduct_roomcard(o_room) 扣房卡(仅此一次) 大局(整场)结束 子游戏算完最终战绩后调 import.save_grade(o_room, ...) → 平台保存战绩并「自动释放房间」,子游戏无需再回收房间本身 解散房间 平台调 export.get_disbandRoom(o_room) 取解散数据包下发 玩家中途进出 平台调 export.player_enter / player_leave 通知子游戏处理 ``` **子游戏自己创建的东西,自己负责回收**:定时器、托管/决策状态、各类缓存等,必须能按房间定位,并在小局结束、解散、开新局时清理干净,禁止泄漏到下一局或别的房间(见 04 的房间隔离)。 --- ## 7. 小结 - 一套代码两个运行时 → 强制 ES5 + 守卫块 require。 - 对象层级:`app → mod`、`o_room → seatlist[seat](o_player) / o_desk(.data.*)`。 - 三层路由:`app(按app) → route(按route) → rpc(按rpc)`,框架自动投递,你只写 `mod.rpc 方法`。 - `DoPack` 的 `return` 不是可靠下发通道 → **一律主动 `sendpack_toseat` 推送**。 - 房间生命周期由 export/import 接缝串起:`makewar` 开局、`get_deskinfo` 重连、`deduct_roomcard` 首局扣卡、`save_grade` 终局保存并自动回收。 下一篇 [02-子游戏接入与开发流程](./02-子游戏接入与开发流程.md) 讲:这三个文件具体怎么写、8+4 个接口各做什么、一次操作的完整数据流。