删除hook,框架改动
This commit is contained in:
@@ -55,6 +55,28 @@
|
||||
|
||||
> 一句话:**服务端权威不仅是"服务端算",也是"服务端把界面要用的核心数据发全"——前端只据包渲染,不推算、不兜底。**
|
||||
|
||||
### 1.3 发全 ≠ 发多:按可见性下发(防作弊)
|
||||
|
||||
§1.2 要求"该看到的发全",本节是它的另一半:**不该看到的一律不发**。前端是不可信环境——**下发即泄露**:包到了客户端就能被抓包/改内存看到,"前端拿到但不渲染"等于零防护。
|
||||
|
||||
- **按座位裁剪可见面**:他人手牌、牌堆剩余序列与顺序、未公开的判定结果(他人是否听牌/能否胡、暗牌内容、未到揭示时机的底牌与亮牌明细)——**对不该看到的座位不放进 `data`**,用差异化下发(`sendtype:1` + `seatlist[]` 逐座位组包,见 §3)实现。
|
||||
- **只发"公开面"的统计量**:他人的信息若界面确实要显示,只发**已公开的派生量**(如剩余张数、已亮出的花色/数量),不发原始牌面。
|
||||
- **不发未来**:尚未发生或尚未公开的权威结果(下一张要摸的牌、预先算好的胜负、待揭晓的底牌)不提前下发,哪怕前端"只是缓存"。
|
||||
- **判别**:问一句 **"这个字段落到一个改过的客户端手里,玩家会不会因此获得优势?"** 会 → 不发(或只发到该看到的座位)。
|
||||
- **违例信号**:`deepCopy(baseData)` 后**没有**逐座位裁剪敏感字段就全员广播;把全量 `handCards`/`cardPool` 塞进公共包;重连快照 `get_deskinfo` 直接回整张桌的内部状态。
|
||||
|
||||
> §1.2 与 §1.3 合起来才是完整的下发面:**该看到的一个不少(否则前端缺数据),不该看到的一个不多(否则等于送作弊入口)。**
|
||||
|
||||
### 1.4 阶段与状态机唯一在服务端,并随包下发
|
||||
|
||||
前端**不持有对局状态机**(前端侧规范见 [客户端 05 §6](../../client/development-guide/05-开发规范与红线.md)):阶段、轮到谁、该座位可用操作、倒计时基准,前端一概不推导。因此这些字段**必须由服务端唯一维护、并在每个相关下发包里明确给出**:
|
||||
|
||||
- **阶段/状态标志**(当前 `phase`、是否在响应窗口、是否已结束等)——不让前端按"收到了什么包"去反推阶段。
|
||||
- **控制权**(下一个该谁操作,如 `nextControlSeat`/`currentPlayer`)——显式字段,不让前端按座位顺序自己算。
|
||||
- **该座位可用操作**(`availableActions[seat]`)——按座位下发;它同时是服务端准入白名单(见 [04 §8](./04-开发规范与红线.md#8-操作请求合法性验证不可信客户端)),**同一份权威数据既驱动前端按钮显示、又校验请求合法性**,天然不会两边判得不一致。
|
||||
- **倒计时锚点与时长**——前端只做本地插值显示,**超时的裁定与后续推进仍在服务端**,不接受前端上报"我超时了"。
|
||||
- **状态变更必须有包**:任何阶段推进都要有一个下发包承载;**没有包的状态变化 = 前端不可能正确显示**,只能靠前端猜——那正是本节要杜绝的。
|
||||
|
||||
---
|
||||
|
||||
## 2. 收包处理的固定步骤(在 handler 里)
|
||||
@@ -124,7 +146,7 @@ for (var seat = 0; seat < o_room.seatlist.length; seat++) {
|
||||
}
|
||||
```
|
||||
|
||||
> 敏感信息(手牌、暗牌、未公开的判定)**只发给该看到的人**,这是服务器权威的一部分。
|
||||
> 敏感信息(手牌、暗牌、未公开的判定)**只发给该看到的人**,这是服务器权威的一部分。裁剪清单与判别标准见 [§1.3 按可见性下发](#13-发全--发多按可见性下发防作弊)——**下发即泄露,前端"拿到但不渲染"不算防护**。
|
||||
|
||||
---
|
||||
|
||||
@@ -246,6 +268,8 @@ if (!data.success) { /* 失败处理 */ return; }
|
||||
|
||||
- 包结构恒为 `{app, route, rpc, data}`;收包先 `check_player`,失败静默 `return`。
|
||||
- **下发包必须发全前端界面所需的核心数据**(§1.2):前端以服务端为权威、不自算,漏发修在服务端发包处、前端不补洞。
|
||||
- **不该看到的一律不发**(§1.3):下发即泄露,按座位裁剪可见面;"该看到的一个不少、不该看到的一个不多"。
|
||||
- **阶段/控制权/可用操作/倒计时锚点由服务端唯一维护并显式下发**(§1.4):前端不持有状态机、不反推阶段;状态变更必须有包承载。
|
||||
- 三种发包方式按"谁该看到什么"选;差异化广播逐座位定制,敏感信息只发本人。
|
||||
- **主动推送是唯一可靠下发通道**,`return` 不算。
|
||||
- **成败只认 `data.success`**,推送必自带 `success`,禁止 `status`/`code` 判成败与兼容兜底。
|
||||
|
||||
@@ -59,6 +59,7 @@ function foo() { GameStateManager.doSomething(); }
|
||||
- **下游不修补**:下游不得为"让流程跑起来"而补写/重算上游本该提供的数据;发现下游在修补,应把责任前移到权威来源。
|
||||
- **边界**:只有展示层、纯 UI 兼容、非关键日志字段,才可在明确边界内用安全默认值。
|
||||
- **下发面要发全**:因前端以服务端为权威、不自算权威结果,服务端**每个下发包必须携带前端界面所需的全部核心数据**(界面要显示、或前端 set-refresh 要用的字段都发全);漏发 = 前端缺数据,**修在服务端发包处、前端不补洞**(详见 [03 §1.2](./03-数据收发与通信协议.md))。
|
||||
- **前端不是数据源**:客户端只提供"意图"(想做什么、对哪个目标),**永远不是任何业务数据的权威来源**。凡进入服务端状态的值——分数、牌面、阶段、控制权、结算——只能由服务端自己算出或从 `o_desk.data.*` 读出,**绝不采信包体里前端算好的同名字段**(见 §8)。对应地,阶段/控制权/`availableActions`/倒计时锚点由服务端唯一维护并显式下发,前端不推导(见 [03 §1.4](./03-数据收发与通信协议.md)、前端 [05 §6](../../client/development-guide/05-开发规范与红线.md))。
|
||||
|
||||
> 审查信号:看到 `|| []`、`|| 0`、`|| ''`、三元默认、双源字段,先判断它是不是**权威字段**(影响发牌/庄家/手牌/规则/结算/重连/状态流转)。是 → 改成权威读取;只是展示/日志 → 才可保留默认。
|
||||
|
||||
@@ -117,6 +118,15 @@ function foo() { GameStateManager.doSomething(); }
|
||||
4. **幂等 / 去重 / 时序(连点的根治)**:不是限流,而是**状态机的单调推进**——一个操作被处理后立即推进状态并更新 `availableActions`,使**重复包因落在新 `availableActions` 之外而天然非法**;响应窗口对同座位重复响应去重(如 `addPlayerResponse` 记录后再收同座位响应即忽略);对已结束窗口/轮次的迟到包丢弃。
|
||||
5. **参数 / 数据合法性**:牌 `uniqueId` 是否真在该玩家手里、`targetCard`/`fromSeat`/`choiceIndex` 是否与权威牌局一致 —— 全按**服务端权威数据**校验,不信客户端传的牌面。
|
||||
|
||||
### 只接受「意图」入参:客户端回传的结论一律忽略
|
||||
|
||||
第 5 层的前提是**入参里根本不该出现结论**。请求包只表达"我想做什么"(操作类型 + 目标标识),**不表达"结果是什么"**(前端侧规范见 [客户端 04 §1「请求包只带意图」](../../client/development-guide/04-网络对接与启动编排.md)):
|
||||
|
||||
- **协议层不定义结论入参**:`score`/`isWin`/`multiplier`/`nextSeat`/`phase`/`result`/`handCards` 之类字段**不出现在请求包定义里**——定义了它,就是留了一个可被伪造的洞。
|
||||
- **收到也忽略**:即便非正规客户端硬塞这些字段,handler **一律不读、不落地**,全部按服务端权威数据重算。审查信号:handler 里出现 `pack.data.score`、`pack.data.isWin`、`pack.data.phase` 这类读取。
|
||||
- **包内 `seat` 只作一致性校验**:身份由连接反查(第 1 层),包内座位不等即拒,**不作身份依据**。
|
||||
- **前端 `shared/` 的计算结果不是输入**:前端跑 `shared/` 只为提示与预校验,其结论不回传、服务端也不采信;服务端自己跑同一份 `shared/` 得出权威结果(见 §9)。
|
||||
|
||||
### 设计形态:统一裁决网关,默认拒绝(fail-closed)
|
||||
|
||||
把五层收敛成**一道所有操作共用的准入网关**(而非每个 handler 各写一遍散点 if)。网关默认 **deny**,仅当请求显式**命中该座位 `availableActions`** 且通过座位/阶段/参数校验才 **allow**。好处:
|
||||
@@ -143,19 +153,10 @@ function foo() { GameStateManager.doSomething(); }
|
||||
|
||||
### `shared/` 是什么:子游戏自己的游戏逻辑,与平台无关
|
||||
|
||||
- **`shared/` 属于子游戏,不是平台代码**:它存放**本玩法自身**、且**前后端都真正会用到**的那部分纯逻辑/常量/数据结构。平台**既不提供也不感知** `shared/`——它完全落在子游戏可编辑范围内,是你玩法逻辑的一部分。
|
||||
- **为什么要"共享"**:**同一段逻辑需要在两处运行且必须算出完全一致的结果**——服务端(Node)做**权威裁定**,前端(浏览器)确有场景**在本地即时预判/预校验**同一段逻辑。若两端各写一份,必随规则演化而分叉、算出不一致结果。因此把这类"两端都用、且必须逐字一致"的纯逻辑收进 `shared/`,两端跑**逐字相同**的代码。
|
||||
- **`shared/` 属于子游戏,不是平台代码**:它存放**本玩法自身**的规则、算法、常量、数据结构(本项目如胡牌检测 `WinDetectionFactory`、精牌 `JingAlgorithm`、计分 `ScoreCalculation`、牌型/比精,以及 `constants/` 下各类常量)。平台**既不提供也不感知** `shared/`——它完全落在子游戏可编辑范围内,是你玩法逻辑的一部分。
|
||||
- **为什么要"共享"**:**同一份玩法逻辑需要在两处运行**——服务端(Node)做**权威裁定**,前端(浏览器)做**即时表现/预校验**。若两端各写一份,必随规则演化而分叉、算出不一致结果。因此把这类"前后端必须完全一致"的纯逻辑收进 `shared/`,两端跑**逐字相同**的代码。
|
||||
- **它与"数据权威"的关系**:`shared/` 是**逻辑同源**(两端同一套算法),不改变**数据权威**(结果仍以服务端为准,见 §4);前端算出的只是表现/预判,最终以服务端 `shared/` 算的为准。
|
||||
|
||||
### 准入门槛:慎重——默认留服务端,别把 shared 当玩法逻辑的垃圾桶
|
||||
|
||||
**进 `shared/` 的唯一理由是"前后端都真正会用到这段逻辑",不是"它是玩法逻辑"。** 判定务必慎重:
|
||||
|
||||
- **前端只做展示、不做核心运算**(服务器权威,裁定与结果均由服务端下发,见 §4),叠加**悲观 UI**(可操作项/提示/落子结果由服务端推送、前端不预测,见前端 04 §5)——因此**前端实际会重算的玩法逻辑很少**。
|
||||
- 绝大多数**计算类逻辑**(发牌、洗牌、胡牌/听牌裁定、计分、AI 决策、随机数等)前端**根本不重算**:结果由服务端算好推给前端展示即可。它们应**只留在服务端、不进 `shared/`**。
|
||||
- 只有当某段逻辑**确有明确的前端使用场景**——例如需要本地即时预判/高亮可操作项,或纯展示所需的**规则常量、牌型/花色映射、文案枚举**——才把**那一部分**提升进 `shared/`;提升的是"前端确需的最小子集",不是把整套算法一并搬过去。
|
||||
- **默认放服务端**:拿不准某文件前端到底用不用,就先留在服务端子游戏目录下,等出现真实的前端使用点再提升进 `shared/`。宁可后补,也不要预防性地把一堆前端用不到的文件塞进 `shared/`(徒增只读副本体积、误导读者以为前端在重算)。
|
||||
|
||||
### 同步流程:服务端权威源 → 前端只读副本
|
||||
|
||||
前后端各持一份 `shared/`,**必须**经同步流程单向更新,禁止直接编辑前端副本:
|
||||
@@ -169,20 +170,13 @@ function foo() { GameStateManager.doSomething(); }
|
||||
2. 改完运行根目录的同步脚本(如 `sync-shared.ps1`)同步到前端。
|
||||
3. **禁止直接编辑前端 `codes/shared/`**(同步脚本会覆盖)。
|
||||
|
||||
> 判别一段逻辑该不该进 `shared/`,连问两关:**①前端到底用不用它?**(前端只做展示、悲观 UI 下多数计算不重算——不用 → 只留服务端)**②两端是否必须算出完全一致的结果?**(是 → 才有共享价值)。两关都过(前端确有本地预判/展示用途,且必须与服务端逐字一致,如某些规则常量、牌型映射、本地可高亮的预校验)→ 放 `shared/`;**只要有一关不过**——服务端流程编排、只有服务端裁定的计算(发牌/胡牌/计分/AI)、只是前端表现——都**各自放自己那侧,不进 `shared/`**。
|
||||
> 判别一段逻辑该不该进 `shared/`:**它是不是"前后端必须算出完全相同结果"的玩法逻辑**?是(胡牌/听牌/比精/牌型/计分/规则常量)→ 放 `shared/`;只是服务端流程编排或只是前端表现 → 各自放自己那侧,不进 `shared/`。
|
||||
|
||||
---
|
||||
|
||||
## 10. 硬编码常量准则
|
||||
|
||||
**需提取为常量**(满足任一):会随规则变化、跨模块复用、裸值无法自解释。典型:分数数值、状态/类型字符串标识、配置阈值、跨模块共享的 key 名。
|
||||
|
||||
**魔法字符串尤其要慎重、默认常量化**:出现在**判等/分支/作为 key/跨模块传递**的字符串字面量(状态名、类型/枚举值、rpc 名、字段名、事件名等)几乎都属"魔法字符串"——**同一个值被硬编码在多处**时,拼写漂移、改名遗漏、无法被搜索/校验,是隐性 bug 的高发点。这类一律**提取为单一来源的常量**、各处只引用;同一语义禁止在多处重复写裸串。
|
||||
|
||||
**常量定义放哪里,同样要慎重(沿用 §9 的 shared 准入门槛)**:常量并非一律进 `shared/constants/`。判据仍是"**前后端是否都真正用到**"——
|
||||
- **仅服务端用到**的常量(服务端内部状态机名、只在服务端分支的类型标识、服务端流程 key 等)→ 放在**服务端子游戏目录内**的常量文件,**不进 `shared/`**。
|
||||
- **前后端都会用到、且必须取值完全一致**的常量(前端展示/预判也要引用的规则常量、牌型/花色映射、双方约定的枚举值等)→ 才归入 `shared/constants/` 下对应文件(分数类、规则配置类、类型枚举类)。
|
||||
- 默认放服务端一侧,只有出现真实的前端引用点,才把**那一部分**常量提升进 `shared/`。
|
||||
**需提取为常量**(满足任一):会随规则变化、跨模块复用、裸值无法自解释。典型:分数数值、状态/类型字符串标识、配置阈值、跨模块共享的 key 名。常量按语义归入 `shared/constants/` 下对应文件(分数类、规则配置类、类型枚举类)。
|
||||
|
||||
**不必提取**(避免过度设计):单函数内一次性临时值(循环初值 `0`、空数组 `[]`)、框架约定固定串(`require` 路径)、自解释布尔开关、纯展示标点文字。
|
||||
|
||||
@@ -220,6 +214,9 @@ function foo() { GameStateManager.doSomething(); }
|
||||
| 语言 | 纯 ES5;`require` 仅在顶部守卫块 |
|
||||
| 成败 | 只认 `data.success`,推送必自带,禁 `status` 兜底 |
|
||||
| 数据 | 权威唯一、缺失报错/返回 null、禁兜底、下游不修补;下发包发全前端界面所需核心数据 |
|
||||
| 前端不是数据源 | 请求包只接受「意图」(操作类型+目标标识),协议不定义 `score`/`isWin`/`phase` 等结论入参,收到也忽略并按权威数据重算;包内 `seat` 只作一致性校验 |
|
||||
| 下发可见性 | 不该看到的不发(他人手牌/牌堆序列/未公开判定/未来结果),按座位差异化裁剪;"下发即泄露",前端不渲染不算防护(03 §1.3) |
|
||||
| 状态机归属 | 阶段/控制权/`availableActions`/倒计时锚点由服务端唯一维护并显式下发,前端不推导;状态变更必须有包承载(03 §1.4) |
|
||||
| 职责 | 一职能一模块,调用不重造 |
|
||||
| 隔离 | 状态挂 `o_desk.data.*`,禁全局;`房间+seat` 作 key;定时器随房清理 |
|
||||
| 自动操作 | 复用真人链路,对前端透明 |
|
||||
|
||||
@@ -18,74 +18,22 @@
|
||||
|
||||
| 篇 | 文档 | 解决什么问题 |
|
||||
|----|------|--------------|
|
||||
| 00 | 本文 README | 这套文档是什么、怎么读、红线速查 |
|
||||
| 00 | 本文 README | 这套文档是什么、怎么读 |
|
||||
| — | [红线速查.md](./红线速查.md) | **一页纸运作模型 + 命名约定 + 红线清单**(由 `CLAUDE.md` 常驻加载,改代码前先对照) |
|
||||
| 01 | [01-服务端环境与框架基础.md](./01-服务端环境与框架基础.md) | 平台与子游戏的关系、核心对象模型、三层路由、房间生命周期 |
|
||||
| 02 | [02-子游戏接入与开发流程.md](./02-子游戏接入与开发流程.md) | 三文件架构、export/import 接口、makewar/重连、一次操作的完整数据流 |
|
||||
| 03 | [03-数据收发与通信协议.md](./03-数据收发与通信协议.md) | 包结构、收发包方式、主动推送、成败标志协议、前后端对接点 |
|
||||
| 04 | [04-开发规范与红线.md](./04-开发规范与红线.md) | 可编辑范围、ES5/require、数据权威、模块职责、房间隔离、测试纪律 |
|
||||
| 03 | [03-数据收发与通信协议.md](./03-数据收发与通信协议.md) | 包结构、收发包方式、**下发面发全 / 按可见性裁剪 / 状态机唯一在服务端**、主动推送、成败标志协议、前后端对接点 |
|
||||
| 04 | [04-开发规范与红线.md](./04-开发规范与红线.md) | 可编辑范围、ES5/require、数据权威(含**前端不是数据源**)、模块职责、房间隔离、请求合法性验证、测试纪律 |
|
||||
|
||||
建议第一次**从 01 顺序读到 04**;之后把 03/04 当手册随用随查。
|
||||
|
||||
---
|
||||
|
||||
## 一页纸:核心运作模型
|
||||
## 一页纸与红线
|
||||
|
||||
```
|
||||
客户端数据包 { app, route, rpc, data }
|
||||
│
|
||||
▼
|
||||
packet_face.ReceivePack 按 pack.app 找到「应用」
|
||||
│
|
||||
▼
|
||||
app.ReceivePack 按 pack.route 找到「模块」(子游戏)
|
||||
│
|
||||
▼
|
||||
mod.DoPack 按 pack.rpc 调到「方法」 mod[rpc](pack)
|
||||
│
|
||||
▼
|
||||
子游戏 RPC handler 校验玩家 → 取对局状态 → 执行业务 → 主动推送
|
||||
│
|
||||
▼
|
||||
o_room.method.sendpack_toseat 按座位取连接信息(conmode/fromid) → 下发客户端
|
||||
```
|
||||
**运作模型、命名约定与红线清单已独立成篇:[红线速查.md](./红线速查.md)。**
|
||||
|
||||
- **平台框架**负责:网络收发、应用/模块/房间/玩家对象、路由、房卡、战绩、房间生命周期。
|
||||
- **子游戏**负责:玩法规则、对局状态、每个操作的处理与广播。两者通过 **export / import** 两组接口对接。
|
||||
|
||||
> 上图是**入站半程**(前端 → 服务端 handler)。完整的往返链路(含服务端 `sendpack_toseat` 推回 → 前端 `Game_Modify._ReceiveData` 按 `rpc` 分发)见 [03 §6「端到端收发全链路」](./03-数据收发与通信协议.md#6-端到端收发全链路前后端对照),前端侧细节见 [前端 04 网络对接与启动编排](../../client/development-guide/04-网络对接与启动编排.md)。
|
||||
|
||||
---
|
||||
|
||||
## 命名约定:哪些是框架契约,哪些只是示例
|
||||
|
||||
本套文档的代码示例里出现的**具体名字,绝大多数是举例,并非强制标准**。请区分两类:
|
||||
|
||||
- **框架契约(必须照用)**——由平台代码决定,改了就对接不上:
|
||||
- 数据包四字段 `{ app, route, rpc, data }`,其中 `app` 固定为 `"youle"`;
|
||||
- 平台真实回调的 export 钩子名、你调用的 import 接口名(如 `makewar`、`get_deskinfo`、`check_player`、`deduct_roomcard`、`save_grade`);
|
||||
- 成败字段 `data.success`;
|
||||
- 平台 API 名(`cls_mod.new`、`min_loadJsFile`、`o_room.method.sendpack_toseat` / `sendpack_toother` 等)。
|
||||
- **示例命名(可自定)**——本项目的举例,你的子游戏可按自己的风格命名:
|
||||
- 业务 `rpc` 名(如 `playCard`、`declareHu`,只需**前后端约定一致**即可);
|
||||
- handler / 类 / 文件 / 变量 / 分层目录名(如 `RpcHandler.handlePlayCard`、`OperationManager`、`ResponseBuilder`、`BroadcastManager`、`RoomAdapter`、`GameController`、`game/`、`rpc/`、`rules/` 等)。
|
||||
|
||||
> 一句话:**平台接缝上的名字是契约、要照用;接缝之内你自己代码里的名字都是示例、可自定。** 后续 01–04 的代码块同样遵循这条约定,不再逐处重复声明。
|
||||
|
||||
---
|
||||
|
||||
## 红线速查(详见 04)
|
||||
|
||||
> 以下每一条都有过真实事故或返工,**改代码前先对照**。
|
||||
|
||||
- **可编辑范围**:服务端只能改子游戏目录 `server/<游戏容器目录>/<你的游戏>/`(容器目录名由接入方自定,**非框架强制**,换任何不与框架冲突的目录皆可),`server/` 其余皆平台代码,禁改。接入新游戏还需在 `server/youle/app.js` 里加一行 `min_loadJsFile` 加载其 `mod.js`(唯一必须触碰的平台文件,属游戏注册接入点)。
|
||||
- **双运行时**:代码同时跑在 Node 与浏览器/友乐平台。必须严格 **ES5**;`require` 只能写在文件开头 `if (typeof require !== 'undefined') {}` 守卫块内,**禁止函数体内/中途 require**。
|
||||
- **成败标志唯一是 `data.success`**:前端只认 `if (!data.success)`;**主动推送的 `data` 必须自带 `success`**(mod 的 `return` 值不可靠下发前端);禁止用 `status`/`code` 判成败、禁止 `status` 兼容兜底。
|
||||
- **数据权威**:同一业务数据只有一个权威来源,下游只读不重算;权威字段缺失要**显式报错/返回 null**,禁止 `|| 0`、`|| []`、`|| ''` 兜底掩盖。
|
||||
- **模块职责边界**:一个职能只在一个模块实现,其他模块**调用而非重造**。
|
||||
- **房间隔离**:一切随对局变化的状态都挂 `o_room.o_desk.data.*`;**禁止模块级单例/全局变量存对局态**;缓存/定时器/决策表不得只用 `seat` 作 key,必须 `房间 + seat`;结束/解散/开新局时按房间清理全部定时器与残留状态。
|
||||
- **服务端自动操作复用真人链路**:服务端代替玩家做的操作(如 AI 托管)必须复用「真人操作」的同一套处理入口与广播链路,对前端透明,不为它单开一套下发分支。
|
||||
- **服务器权威 + 发全下发面**:房卡、胜负、积分、状态等关键数据与操作一律以服务器为准、前端只作显示不自算;因此**服务端每个下发包必须携带前端界面所需的全部核心数据**,漏发修在服务端发包处、前端不补(详见 03 §1.2 / 04 §4)。
|
||||
- **测试纪律**:测试失败先用证据裁定「业务缺陷 vs 测试脚本缺陷」,禁止 skip/软化断言/吞异常掩盖;**正式代码不得为兼容测试而加逻辑**。
|
||||
该文件由 `CLAUDE.md` 通过 `@import` 常驻每次会话上下文,是红线的权威源;本 README 只负责导航,不重复抄写红线(避免两处不同步)。红线的完整细节见 [04-开发规范与红线.md](./04-开发规范与红线.md)。
|
||||
|
||||
---
|
||||
|
||||
@@ -94,5 +42,3 @@ o_room.method.sendpack_toseat 按座位取连接信息(conmode/fromid) → 下
|
||||
- 本套文档是平台级收发包/子游戏开发规范的**落地版**:结合本仓库真实代码,给出可直接照做的接入步骤与红线,并对其中已被本项目实践修正的部分(如成败标志由 `status` 收敛为 `success`)以本套为准。
|
||||
- 各子游戏内部的架构细节(模块划分、算法)仍以各自 `<游戏容器目录>/<游戏>/docs/` 为准。
|
||||
- **平台无关的通用工程与架构规范**(分层、可扩展模式、配置化、数据权威、反模式与审查清单)见 [工程与架构通则](../../games/engineering/):本套讲"平台怎么接、红线是什么",工程通则讲"该怎么设计、怎么长久演进",互补阅读。
|
||||
</content>
|
||||
</invoke>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# 服务端 · 一页纸运作模型与红线速查
|
||||
|
||||
> 本文是服务端开发**必须常驻在手边**的那一页:运作模型 + 命名约定 + 红线清单。
|
||||
> 它由 `CLAUDE.md` 通过 `@import` 常驻每次会话上下文;**红线以本文为权威**。
|
||||
> 文档导航、阅读顺序、各篇主题见 [README](./README.md);红线的完整细节见 [04-开发规范与红线.md](./04-开发规范与红线.md)。
|
||||
|
||||
---
|
||||
|
||||
## 一页纸:核心运作模型
|
||||
|
||||
```
|
||||
客户端数据包 { app, route, rpc, data }
|
||||
│
|
||||
▼
|
||||
packet_face.ReceivePack 按 pack.app 找到「应用」
|
||||
│
|
||||
▼
|
||||
app.ReceivePack 按 pack.route 找到「模块」(子游戏)
|
||||
│
|
||||
▼
|
||||
mod.DoPack 按 pack.rpc 调到「方法」 mod[rpc](pack)
|
||||
│
|
||||
▼
|
||||
子游戏 RPC handler 校验玩家 → 取对局状态 → 执行业务 → 主动推送
|
||||
│
|
||||
▼
|
||||
o_room.method.sendpack_toseat 按座位取连接信息(conmode/fromid) → 下发客户端
|
||||
```
|
||||
|
||||
- **平台框架**负责:网络收发、应用/模块/房间/玩家对象、路由、房卡、战绩、房间生命周期。
|
||||
- **子游戏**负责:玩法规则、对局状态、每个操作的处理与广播。两者通过 **export / import** 两组接口对接。
|
||||
|
||||
> 上图是**入站半程**(前端 → 服务端 handler)。完整的往返链路(含服务端 `sendpack_toseat` 推回 → 前端 `Game_Modify._ReceiveData` 按 `rpc` 分发)见 [03 §6「端到端收发全链路」](./03-数据收发与通信协议.md#6-端到端收发全链路前后端对照),前端侧细节见 [前端 04 网络对接与启动编排](../../client/development-guide/04-网络对接与启动编排.md)。
|
||||
|
||||
---
|
||||
|
||||
## 命名约定:哪些是框架契约,哪些只是示例
|
||||
|
||||
本套文档的代码示例里出现的**具体名字,绝大多数是举例,并非强制标准**。请区分两类:
|
||||
|
||||
- **框架契约(必须照用)**——由平台代码决定,改了就对接不上:
|
||||
- 数据包四字段 `{ app, route, rpc, data }`,其中 `app` 固定为 `"youle"`;
|
||||
- 平台真实回调的 export 钩子名、你调用的 import 接口名(如 `makewar`、`get_deskinfo`、`check_player`、`deduct_roomcard`、`save_grade`);
|
||||
- 成败字段 `data.success`;
|
||||
- 平台 API 名(`cls_mod.new`、`min_loadJsFile`、`o_room.method.sendpack_toseat` / `sendpack_toother` 等)。
|
||||
- **示例命名(可自定)**——本项目的举例,你的子游戏可按自己的风格命名:
|
||||
- 业务 `rpc` 名(如 `playCard`、`declareHu`,只需**前后端约定一致**即可);
|
||||
- handler / 类 / 文件 / 变量 / 分层目录名(如 `RpcHandler.handlePlayCard`、`OperationManager`、`ResponseBuilder`、`BroadcastManager`、`RoomAdapter`、`GameController`、`game/`、`rpc/`、`rules/` 等)。
|
||||
|
||||
> 一句话:**平台接缝上的名字是契约、要照用;接缝之内你自己代码里的名字都是示例、可自定。** 01–04 的代码块同样遵循这条约定,不再逐处重复声明。
|
||||
|
||||
---
|
||||
|
||||
## 红线速查(详见 04)
|
||||
|
||||
> 以下每一条都有过真实事故或返工,**改代码前先对照**。
|
||||
|
||||
- **可编辑范围**:服务端只能改子游戏目录 `server/<游戏容器目录>/<你的游戏>/`(容器目录名由接入方自定,**非框架强制**,换任何不与框架冲突的目录皆可),`server/` 其余皆平台代码,禁改。接入新游戏还需在 `server/youle/app.js` 里加一行 `min_loadJsFile` 加载其 `mod.js`(唯一必须触碰的平台文件,属游戏注册接入点)。
|
||||
- **双运行时**:代码同时跑在 Node 与浏览器/友乐平台。必须严格 **ES5**;`require` 只能写在文件开头 `if (typeof require !== 'undefined') {}` 守卫块内,**禁止函数体内/中途 require**。
|
||||
- **成败标志唯一是 `data.success`**:前端只认 `if (!data.success)`;**主动推送的 `data` 必须自带 `success`**(mod 的 `return` 值不可靠下发前端);禁止用 `status`/`code` 判成败、禁止 `status` 兼容兜底。
|
||||
- **数据权威**:同一业务数据只有一个权威来源,下游只读不重算;权威字段缺失要**显式报错/返回 null**,禁止 `|| 0`、`|| []`、`|| ''` 兜底掩盖。
|
||||
- **模块职责边界**:一个职能只在一个模块实现,其他模块**调用而非重造**。
|
||||
- **房间隔离**:一切随对局变化的状态都挂 `o_room.o_desk.data.*`;**禁止模块级单例/全局变量存对局态**;缓存/定时器/决策表不得只用 `seat` 作 key,必须 `房间 + seat`;结束/解散/开新局时按房间清理全部定时器与残留状态。
|
||||
- **服务端自动操作复用真人链路**:服务端代替玩家做的操作(如 AI 托管)必须复用「真人操作」的同一套处理入口与广播链路,对前端透明,不为它单开一套下发分支。
|
||||
- **服务器权威 + 发全下发面**:房卡、胜负、积分、状态等关键数据与操作一律以服务器为准、前端只作显示不自算;因此**服务端每个下发包必须携带前端界面所需的全部核心数据**,漏发修在服务端发包处、前端不补(详见 03 §1.2 / 04 §4)。
|
||||
- **状态机唯一在服务端**:前端是**数据驱动**的、不持有对局状态机(见前端 05 §6),所以阶段、控制权(轮到谁)、该座位 `availableActions`、倒计时锚点**必须由服务端唯一维护并在相关下发包里显式给出**;**任何状态变更都要有包承载**——没有包的状态变化等于逼前端去猜。超时裁定在服务端,不接受前端上报"我超时了"(详见 03 §1.4)。
|
||||
- **前端不是数据源,只收「意图」**:请求包只接受「操作类型 + 目标标识」;协议**不定义** `score`/`isWin`/`phase`/`nextSeat`/`handCards` 之类**结论入参**,即便客户端硬塞也一律**不读、不落地**,全部按服务端权威数据重算;包内 `seat` 只作一致性校验、身份由连接反查(详见 04 §8)。
|
||||
- **发全 ≠ 发多,按可见性下发**:他人手牌、牌堆剩余序列、未公开判定、尚未揭晓的结果**不发给不该看到的座位**(差异化下发逐座位裁剪)。**下发即泄露**——包到了客户端就能被抓包看到,"前端拿到但不渲染"不算防护(详见 03 §1.3)。
|
||||
- **测试纪律**:测试失败先用证据裁定「业务缺陷 vs 测试脚本缺陷」,禁止 skip/软化断言/吞异常掩盖;**正式代码不得为兼容测试而加逻辑**。
|
||||
Reference in New Issue
Block a user