diff --git a/docs/client/development-guide/04-网络对接与启动编排.md b/docs/client/development-guide/04-网络对接与启动编排.md index 77bb462..e35e2dc 100644 --- a/docs/client/development-guide/04-网络对接与启动编排.md +++ b/docs/client/development-guide/04-网络对接与启动编排.md @@ -33,6 +33,21 @@ RpcHelper.sendGameRpc(rpc, gameData, options); // route = "room"/子游戏路 > 平台字段缺失(如 `playerid` 为 0/空)应 fail-fast 暴露,不静默发出残缺包。 +### 请求包只带「意图」,不带结论 + +**请求包表达的是「我想做什么」,不是「结果是什么」。** 这是前端数据驱动架构([05 §6](./05-开发规范与红线.md))在发包侧的必然推论:前端不是数据源,凡由前端算出并回传的"结论",都等于把裁定权交给了不可信的客户端。 + +| 可以带(意图) | 禁止带(结论) | +|---|---| +| 操作类型(出牌/碰/杠/过/胡/叫分…) | 算好的得分、番数、结算金额 | +| 目标标识(牌的 `uniqueId`、`targetCard`、`choiceIndex`) | "我胡了 / 我听了 / 这步合法" 之类的判定结果 | +| 座位号(仅供服务端做一致性校验,**不作身份依据**) | 下一阶段是什么、下一个该谁、剩余时间 | +| 纯客户端偏好(音量、语言等非对局字段) | 手牌全量、他人信息等本应由服务端持有的状态 | + +- **服务端按自己的权威数据重算**,对请求里出现的结论字段一律**忽略**(服务端侧见 [04 §8](../../server/development-guide/04-开发规范与红线.md#8-操作请求合法性验证不可信客户端));协议设计阶段就**不应该定义**这类入参——定义了它,就是留了一个可被伪造的洞。 +- **前端 `shared/` 算出的结果不回传**:它只用于本地提示与预校验(见 05 §6.1、§8),发包时只发意图。 +- **违例信号**:发包方法里出现 `score`、`isWin`、`nextSeat`、`phase`、`result`、`handCards` 之类"由前端填的结论字段"。 + --- ## 2. 收包:统一分发 @@ -156,6 +171,7 @@ AI 托管:服务端 AI 决策 →┘ - **不得假设「本座刚点过」**:所需数据一律从**包内权威字段**读取,**禁止**依赖「点击时暂存的本地变量」。 - **先判 `data.success`**(§4),再据包字段渲染;无对应点击也能正确渲染。 +- **不得据本地推断补齐服务端未下发的状态**:包里没有的阶段/控制权/可用操作,不由前端算出来顶上——那是服务端漏发,修在服务端发包处(见 [05 §6.1](./05-开发规范与红线.md)、服务端 [03 §1.2](../../server/development-guide/03-数据收发与通信协议.md))。 ### 5.4 适用范围 @@ -235,6 +251,8 @@ AI 托管:服务端 AI 决策 →┘ | DO ✅ | DON'T ❌ | |------|---------| | 发包走语义化发包封装/`RpcHelper` | 业务里直接 `Utl.sendData` 手拼包 | +| 请求包只带意图(操作类型+目标标识) | 包里回传前端算出的分数/判定/阶段等结论字段 | +| 阶段/控制权/可用操作/倒计时只读包内权威字段 | 前端自建对局状态机、本地推导或用定时器自行推进阶段 | | 收包统一进收包分发器,一 rpc 一处理器 | 在 `Game_Modify.*` 或散点里直接处理推送 | | 受限入口只解包转交,业务在新架构 handler | 在受限文件堆业务逻辑 | | 成败只认 `data.success` | 用 `status`/`code` 判成败、写 status 兜底 | diff --git a/docs/client/development-guide/05-开发规范与红线.md b/docs/client/development-guide/05-开发规范与红线.md index 1c12d3e..92c67a6 100644 --- a/docs/client/development-guide/05-开发规范与红线.md +++ b/docs/client/development-guide/05-开发规范与红线.md @@ -70,9 +70,43 @@ --- -## 6. 数据权威、组件数据与表现延后 +## 6. 数据驱动架构:服务端状态的投影 + +**前端是服务端对局状态的一个投影(view),不是状态的第二个来源。** 阶段、轮次、控制权、可用操作、倒计时、分数、按钮可用性……一切对局态都由服务端唯一维护并随包下发,前端只做「**读包字段 → 写 `this.data` → 据 `this.data` 画界面**」这一条链路。这既是正确性要求(两端各推一套必然分叉),也是**反作弊**要求:**前端能自己推出来的东西,就是玩家能改的东西**。 - **服务端权威**:所有核心运算与胜负裁定以服务端为准、数据以服务端为权威;前端只做界面展示与玩家交互,本地数据仅为「渲染副本」,不做权威计算。 + +### 6.1 前端无对局状态机 + +- **状态机唯一在服务端**:阶段(`phase`)、当前控制权(轮到谁)、该座位可用操作(`availableActions`)、倒计时剩余、比分/结算——一律**读服务端下发的权威字段**渲染。 +- **禁止本地推导**:不得由「上一个包 + 本地规则」推出「现在该轮到谁 / 现在进入哪个阶段 / 现在该显示哪几个按钮」。缺字段是**服务端漏发**,修在服务端发包处(见服务端 [03 §1.2](../../server/development-guide/03-数据收发与通信协议.md)),**前端不推、不补、不猜**。 +- **禁止前端推进流程**:不得用本地定时器自动推进阶段、自行结算、超时后自判胜负或自动补发包。**超时/托管一律由服务端驱动**,前端只显示服务端下发的结果。 +- **倒计时的正确形态**:服务端下发锚点/剩余时长,前端可本地 tick 做插值显示;但**归零不代表状态改变**——超时的裁定与后续推进仍等服务端推送。 +- **`shared/` 预判不是状态**:前端跑 `shared/` 算出的胡牌/听牌/合法性只用于**提示与预校验**(如置灰不可点的牌),**不得**据其改写对局态、也**不得**用来替代服务端下发的 `availableActions`(见 §8)。 + +### 6.2 视图 = f(服务端快照) + +- `this.data` 是**服务端状态的镜像**,不是第二份真相;**不得存在「只活在前端、服务端不知道」的对局态**。 +- **判据(可直接用于自检与代码审查)**:任意时刻丢弃全部 `this.data`,仅用**最近一次服务端快照**(重连 `get_deskinfo` 或最近一次推送)重画,界面与交互状态必须**完全一致**。做不到 → 要么前端私存了对局态、要么服务端漏发了字段,二者必居其一,**都要修**。 +- 这与「重连即重画」是同一条路径(见 [04 §3.3](./04-网络对接与启动编排.md)):**重连之所以能只靠服务端快照还原,正因为前端从来没有过独占状态。** + +### 6.3 允许的本地 UI 态(白名单) + +只有**不影响对局裁定、服务端根本不关心**的纯表现态,才可以只存在于前端: + +| 允许只在前端 | 不允许(属对局态,必须来自服务端) | +|---|---| +| 选中/待出牌的高亮、拖拽位置 | 这张牌能不能出、出了之后轮到谁 | +| 按钮按下高亮/缩放、点击音效 | 按钮**该不该出现**、**能不能点** | +| 列表滚动位置、面板展开、设置开关 | 阶段、控制权、倒计时基准、分数、结算 | +| 动画进度、特效播放中标记 | 手牌/牌河/副露内容、亮牌信息、托管状态 | + +判别:**这个值若被玩家改成任意值,会不会影响对局结果、或让他看到/做到本不该的事?** 会 → 它是对局态,必须服务端权威。 + +> **与 [04 §5.6](./04-网络对接与启动编排.md#56-受控例外响应交互按钮乐观清除--服务端合法性验证) 受控例外的关系**:本玩家点击响应/掷骰交互按钮后的「乐观隐藏」仍然允许——它是抢先一步做了服务端稍后会确认的事,**不是前端私有状态**:按钮**该不该出现**依旧由服务端下发的 `availableActions` 决定,收包侧必须能独立得出同一结果(AI 托管无点击时也正确)。判据仍成立:丢弃 `this.data` 后按最近快照重画,该按钮的显隐与服务端一致。 + +### 6.4 组件数据与表现延后 + - **组件数据自持 + set-refresh**:组件(及其下每个「零件 UI」)的自有数据集中在 `this.data`,**不散落**各处;每个 UI 都有 `setXxx`(只写数据)/`refreshXxx`(只据数据画界面)成对方法,组件另有总 `refresh()` 据 `this.data` 重建整块界面(见 02)。 - **更新时机——发包只请求、收包才表现(悲观 UI / 输入-渲染解耦)**:用户点击**只发请求包**,**绝不**在点击时改动任何对局状态界面(提示显隐、按钮增删、当前控制权、倒计时启停、落牌、阶段/托管图标);这些表现一律在**收到服务端结果/推送包后**更新。点击回调只做「发包 +(可选)纯本地物理反馈(按下高亮/音效、防连点 `disable`)」。收包处理器**触发源无关**——数据从包字段读、不依赖「点击时暂存的本地变量」,故真人操作与 **AI 托管/他人广播共用同一更新路径、对前端透明**(无点击时也正确更新);把更新挂在「点击」而非「收包」会导致 AI 托管时界面卡死。详见 [04 §5](./04-网络对接与启动编排.md#5-输入渲染解耦发包只请求收包才表现)。**受控例外**:本玩家点击的**响应/掷骰交互按钮**(碰/杠/过/胡按钮、手动掷骰按钮)的**隐藏**允许乐观清除(兼作防连点+即时反馈),前提是**服务端合法性验证 + 收包侧兜底(AI 托管一致)**,见 [04 §5.6](./04-网络对接与启动编排.md#56-受控例外响应交互按钮乐观清除--服务端合法性验证) 与服务端 [04 §8](../../server/development-guide/04-开发规范与红线.md#8-操作请求合法性验证不可信客户端);其余提示/控制权/倒计时/落牌仍严格收包驱动。 - **收包节奏**:**先 `setXxx` 写数据 →(必要时 `refresh` 刷静态界面)→ 再播动画 → 动画回调里只刷新界面**;动画的开始/结束/出错等生命周期回调里**绝不设置核心数据**,动画期间**不改数据**。 @@ -125,6 +159,8 @@ | 渲染 | 只走 `SpriteManager`;ID 守范围、不编造;查返回值 | | 常量 | 精灵/群组/图层/图片/声音/Spine/坐标/动画/事件全进常量;UI 代码禁硬编码 id 与裸值 | | 组件 | 继承 `BaseComponent`;事件 `addEventListener`;`destroy` 清动态精灵/定时器 | +| 数据驱动 | 前端**无对局状态机**:阶段/控制权/可用操作/倒计时/分数只读包内权威字段,禁本地推导与本地推进流程;`this.data` 只是服务端快照的镜像,丢弃后仅凭最近快照重画须完全一致;只有白名单纯表现态可只存在于前端(§6.1–6.3) | +| 发包内容 | 请求包只带「意图」(操作类型+目标标识),**不带结论**(分数/判定结果/阶段指令);服务端按自己的权威数据重算(04 §1「请求包只带意图」) | | 数据 | 服务端权威(核心运算/裁定在服务端);组件自有数据集中 `this.data`;每个 UI/零件有 set/refresh 对、组件有总 `refresh` | | 更新时机 | 点击只发请求包,对局状态表现等**收包后**更新(悲观 UI);收包处理器触发源无关、数据从包字段读;AI 托管/他人广播共用同一更新路径。**受控例外**:响应/掷骰交互按钮隐藏可乐观清除(防连点+即时反馈),前提是服务端合法性验证+收包兜底(04 §5.6、服务端 04 §8) | | 节奏 | 先写数据后表现;动画回调只刷界面、**不写核心数据**;重画可随时据数据还原界面 | diff --git a/docs/client/development-guide/README.md b/docs/client/development-guide/README.md index d544fae..b7df6c8 100644 --- a/docs/client/development-guide/README.md +++ b/docs/client/development-guide/README.md @@ -23,7 +23,7 @@ | 02 | [02-渲染与UI组件体系.md](./02-渲染与UI组件体系.md) | 精灵 ID 体系、SpriteManager 分层、资源常量组织、BaseComponent 组件化、组件数据/set-refresh 范式、UIManager 场景、动态列表 | | 03 | [03-事件·动画·音频·Spine.md](./03-事件·动画·音频·Spine.md) | EventBus、AnimationManager+配置、AudioManager+音效资源、SpineMgr 全链路 | | 04 | [04-网络对接与启动编排.md](./04-网络对接与启动编排.md) | 发包链路(RpcHelper 注入平台字段)、收包统一分发、新旧架构对接边界、启动编排、处理器/管理器职责 | -| 05 | [05-开发规范与红线.md](./05-开发规范与红线.md) | 可编辑范围、ES5、框架中立、常量集中、组件生命周期、服务端权威/组件数据/表现延后、data.success、模块职责、测试 | +| 05 | [05-开发规范与红线.md](./05-开发规范与红线.md) | 可编辑范围、ES5、框架中立、常量集中、组件生命周期、**数据驱动架构(前端无状态机/视图=服务端快照投影/本地 UI 态白名单)**、组件数据与表现延后、data.success、模块职责、测试 | | 06 | [06-子游戏接入模式与Hooks外置.md](./06-子游戏接入模式与Hooks外置.md) | 内联模式 vs Hooks 外置模式、三契约文件可改性、退化为纯转发壳 + SubGameHooks 委托、subgame-entry 模板 | 建议第一次**从 01 顺序读到 05**;之后把 02–05 当手册随用随查。06 在接入新游戏或迁移到 Hooks 外置模式时选读。 @@ -62,6 +62,9 @@ BaseComponent / UIManager(ui,组件化与场景) ← gameabc-framework( - **精灵只走 SpriteManager**:UI 代码**禁止**直接调 `GameABCUtils` 或引擎原生 API;ID 必须落在规定范围(精灵 1001–3000、群组 ≥201、图层 101–200/弹窗 301–400,另有 501–600、701+ 备用段,框架保留 1–1000 及 3001+ 等交替段)。 - **常量集中、禁硬编码**:UI 代码**禁止任何硬编码 id / 裸值**——精灵 ID / 群组 ID / 图层 ID / 图片资源 ID / 声音 ID / Spine 资源 / 坐标尺寸 / 动画时长 / 事件名 一律定义在对应常量文件、只引用常量。 - **服务端权威、前端只展示**:核心运算与胜负裁定以服务端为准、数据以服务端为权威;前端只做界面展示与玩家交互,本地数据仅为渲染副本,不做权威计算。 +- **数据驱动、前端无对局状态机**:阶段、轮次/控制权、可用操作、倒计时、分数、按钮该不该出现,一律**读服务端下发的权威字段**渲染;**禁止**前端自建状态机、由本地规则推导「现在轮到谁/进入哪个阶段/显示哪些按钮」,**禁止**用本地定时器自行推进阶段或自判超时(超时与托管由服务端驱动,倒计时前端只做插值显示)。包里没有的状态是**服务端漏发**,修在服务端发包处,前端不推、不补、不猜(详见 05 §6.1)。 +- **视图 = f(服务端快照)**:`this.data` 只是服务端状态的镜像,**不得存在只活在前端、服务端不知道的对局态**。判据:任意时刻丢弃 `this.data`、仅凭最近一次服务端快照重画,界面必须完全一致——做不到即"前端私存了状态"或"服务端漏发了字段",都要修。只有不影响裁定的纯表现态(选中高亮、按下反馈、滚动位置、动画进度)可只存在于前端(详见 05 §6.2–6.3)。 +- **请求包只带「意图」**:发包只带「做什么 + 目标标识」(操作类型、牌 `uniqueId`、`choiceIndex`),**禁止**回传前端算出的结论(分数/番数、"我胡了"之类判定、结算结果、阶段推进指令);前端 `shared/` 的计算只用于本地提示与预校验,结果不回传(详见 04 §1)。**前端能自己推出来的东西,就是玩家能改的东西**——这是防作弊的根本。 - **组件走 BaseComponent**:UI 组件继承 `BaseComponent`,事件用 `this.addEventListener` 注册(`destroy()` 自动清理,防泄漏);动态精灵/定时器在 `onDestroy` 清理。 - **组件数据自持 + set-refresh**:组件自有数据集中 `this.data`;每个 UI/零件有 `setXxx`(只写数据)/`refreshXxx`(只据数据画)对,组件有总 `refresh()` 可据数据重建界面。 - **UI 组件专职自己的界面**:每个界面的数据与渲染只由其对应 UI 组件实现;别的模块要改/刷该界面一律**调该组件的公开接口**(`setXxx`/`refreshXxx`/语义方法),**禁止**在别处重复实现重叠或类似的界面数据/渲染逻辑。 diff --git a/docs/server/development-guide/03-数据收发与通信协议.md b/docs/server/development-guide/03-数据收发与通信协议.md index 809809a..d204a27 100644 --- a/docs/server/development-guide/03-数据收发与通信协议.md +++ b/docs/server/development-guide/03-数据收发与通信协议.md @@ -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` 判成败与兼容兜底。 diff --git a/docs/server/development-guide/04-开发规范与红线.md b/docs/server/development-guide/04-开发规范与红线.md index 049d380..4792637 100644 --- a/docs/server/development-guide/04-开发规范与红线.md +++ b/docs/server/development-guide/04-开发规范与红线.md @@ -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**。好处: @@ -204,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;定时器随房清理 | | 自动操作 | 复用真人链路,对前端透明 | diff --git a/docs/server/development-guide/README.md b/docs/server/development-guide/README.md index 208af44..29a41f9 100644 --- a/docs/server/development-guide/README.md +++ b/docs/server/development-guide/README.md @@ -21,8 +21,8 @@ | 00 | 本文 README | 这套文档是什么、怎么读、红线速查 | | 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 当手册随用随查。 @@ -85,6 +85,9 @@ o_room.method.sendpack_toseat 按座位取连接信息(conmode/fromid) → 下 - **房间隔离**:一切随对局变化的状态都挂 `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/软化断言/吞异常掩盖;**正式代码不得为兼容测试而加逻辑**。 ---