feat: integrate room framework and isolated subgame bundles
Complete room UI and protocol integration, move game definitions and resources behind bundle entries, publish authoritative version XML, and document single-game builds. Include all current resource changes and experiment artifacts.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# Cocos 平台框架与子游戏开发文档
|
||||
|
||||
本目录是 Cocos 前端开发文档入口。说明按框架与子游戏分类,文档链接只指向本目录内部;源码文件名或工程路径只用于说明操作位置,不作为外部阅读依赖。
|
||||
|
||||
## 框架
|
||||
|
||||
- [框架总览与维护边界](framework/README.md)
|
||||
- [如何构建目标游戏](framework/build/README.md)
|
||||
- [单游戏构建步骤与产物验收](framework/build/single-game-build.md)
|
||||
- [如何接入子游戏](framework/integration/README.md)
|
||||
- [创建房间接入](framework/integration/create-room.md)
|
||||
- [当前架构与实施状态](framework/architecture/current-status.md)
|
||||
- [房间平台架构设计](framework/architecture/room-platform.md)
|
||||
- [SDK 接口说明](framework/sdk/contracts.md)
|
||||
- [原工程接口迁移说明](framework/sdk/legacy-api-migration.md)
|
||||
- [网络协议目录](framework/protocol/README.md)
|
||||
- [身份、版本与构建](framework/build/version-and-build.md)
|
||||
- [旧目录作用与迁移记录](framework/build/legacy-project-layout.md)
|
||||
|
||||
## 子游戏
|
||||
|
||||
- [子游戏专项资料目录](subgames/README.md):按具体游戏归档设计、协议与接入状态。
|
||||
- [二七王资料](subgames/erqiwang/README.md):设计、玩法协议和当前接入状态。
|
||||
|
||||
## 使用原则
|
||||
|
||||
服务器与原生接口零改动;子游戏接入不修改框架代码或资源;配置单一来源,下游不猜测。场景、prefab、anim、meta 通过编辑器维护。Layer612_Tips 只承载玩家业务提示,不显示程序错误。
|
||||
|
||||
本目录整合了原先分散的协议与二七王文档,迁移日期 2026-09-08。历史设计、原工程协议、当前实现能力必须区分;以各章节的实施状态说明为准,不能把设计中的接口等同于已上线能力。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 框架文档
|
||||
|
||||
返回[文档首页](../README.md)。
|
||||
|
||||
框架提供网络、配置解析、会话状态、公共房间流程、UI 能力与构建校验。具体游戏负责玩法编码、创建页面、牌局状态和玩法渲染。应用层负责注册与装配。
|
||||
|
||||
## 两条开发操作指南
|
||||
|
||||
- [如何构建](build/README.md):构建文档导航。
|
||||
- [单游戏构建操作](build/single-game-build.md):gameKey、Bundle 勾选、切换游戏、导出与产物检查。
|
||||
- [如何接入子游戏](integration/README.md):注册、目录约定、生命周期与资源绑定;包含[创建房间接入](integration/create-room.md)。
|
||||
|
||||
## 分类
|
||||
|
||||
- architecture:总体设计、房间生命周期、实施边界。
|
||||
- sdk:子游戏可用契约、旧接口迁移、资源释放约定。
|
||||
- protocol:传输、平台 agent/room RPC、数据结构、游戏消息桥接。
|
||||
- build:专门的构建指南目录,包含 XML 身份来源、预览、打包与旧脚手架说明。
|
||||
- integration:专门的子游戏接入指南目录,包含通用接入和创建房间流程。
|
||||
|
||||
## 入口
|
||||
|
||||
1. [当前架构与实施状态](architecture/current-status.md)
|
||||
2. [房间架构设计](architecture/room-platform.md)
|
||||
3. [SDK 契约](sdk/contracts.md)
|
||||
4. [协议目录](protocol/README.md)
|
||||
5. [单游戏构建](build/single-game-build.md)与[版本身份配置](build/version-and-build.md)
|
||||
|
||||
框架维护者增加公共能力时应使用注入的能力契约,不增加具体 gameKey/gameid 分支;子游戏开发者使用这些能力,不访问框架内部 Store、Runtime、WebSocket。
|
||||
@@ -0,0 +1,30 @@
|
||||
# 当前架构与实施状态
|
||||
|
||||
返回[框架目录](../README.md)。核对日期:2026-09-09。
|
||||
|
||||
## 目录职责
|
||||
|
||||
- 工程内 assets/framework:可复用的协议、状态、SDK、公共展示能力。
|
||||
- 工程内 assets/app:通用 Bundle 入口解析、游戏选择、XML 身份组装,不导入具体游戏。
|
||||
- 工程内 assets/games:当前实际的子游戏代码与资源。
|
||||
- 工程内 assets/scripts/platform-login:当前引擎 UI 适配和公共组件,包括 CreateRoomPage、GameComposition;虽然路径有历史名称,也不能为每款游戏增加具体实现分支。
|
||||
- 工程 extensions:构建扩展,不侵入运行时框架。
|
||||
|
||||
这里的目录名称用于定位开发职责,完整接入说明见[子游戏指南](../integration/README.md)。
|
||||
|
||||
## 已实现
|
||||
|
||||
各子游戏通过自己的 GameDefinition 和 GameEntry_<gameKey> 提供工厂、配置及组件类型,SubgameAssets 按 gameKey 动态加载入口及两份私有 prefab。公共承载场景仍在启动组件中绑定。平台接收注入的身份和入口,不内置具体游戏 ID。XML value 提供数值 versionCode,name 提供展示 version。
|
||||
|
||||
创建页面继承 CreateRoomPage,仅提交子游戏编码的 roomtype。平台保存和转发不透明 roomtype,具体游戏解释玩法。房间模块使用 room/messages/ui/scope 能力,结束时解除其资源和订阅。
|
||||
|
||||
构建扩展读取实际启动场景的游戏选择,验证 XML 与资源包,向 Web 根目录复制相同字节的 version.xml。
|
||||
|
||||
## 尚未完成
|
||||
|
||||
- gameKey 尚未自动同步构建 Bundle 勾选项;当前需在构建面板显式保留公共 resources、目标游戏及版本包。
|
||||
- 发布环境配置仍有历史 profiles.ts 来源,尚需迁移至应用发布配置;不能将每次改框架配置当作正式多游戏构建方案。
|
||||
- 二七王创建房间与房间入口已接通,完整牌局消息、结算、deskinfo 画面恢复未完成。
|
||||
- 房间音频所有权能力尚未接入。
|
||||
|
||||
[房间架构设计](room-platform.md)与[旧 API 迁移说明](../sdk/legacy-api-migration.md)保留详细设计;其中规划项需对照本页,不能承诺为现有功能。
|
||||
@@ -0,0 +1,391 @@
|
||||
# 房间平台与子游戏接入架构设计
|
||||
|
||||
版本:1.1 · 日期:2026-09-08 · 状态:平台核心已实施;验收与未支持边界见[实施记录](current-status.md)。
|
||||
|
||||
本文保留设计契约与目标示例;具体 API 名称、落地位置及兼容期以实施记录和代码为准。音频所有权、完整子游戏玩法与原生设备验收不因平台核心落地而自动视为完成。网络协议、原生接口与序列化资源约束继续以仓库规定及对应权威文档为准。
|
||||
|
||||
## 文档导航
|
||||
|
||||
- 源码审查、方案优缺点与原流程依据
|
||||
- [旧接口迁移与新接入契约](current-status.md)
|
||||
- [分阶段实施计划与验收](current-status.md)
|
||||
- 平台协议目录
|
||||
- [二七王当前玩法协议](../../subgames/erqiwang/protocol/packet_protocol.md)
|
||||
|
||||
## 1. 目标与非目标
|
||||
|
||||
### 1.1 目标
|
||||
|
||||
1. 原服务器零修改即可兼容,公开协议字段、route/rpc、数据类型与重连快照保持不变。
|
||||
2. 平台不依赖具体子游戏;子游戏不访问平台内部 Store、场景和网络实现。
|
||||
3. 纯逻辑无需启动 Cocos 即可测试;资源装配与真实交互必须另行在编辑器/构建中验证。
|
||||
4. 子游戏只开发规则、协议映射、游戏模型和展示,不重复实现大厅、聊天、投票、设置、原生桥。
|
||||
5. 房间生命周期、消息顺序、资源所有权、失败处理都具有明确契约。
|
||||
6. 对已证实的全量重建、重复头像资源等问题做增量优化,性能结论以基线测量为准。
|
||||
|
||||
### 1.2 非目标
|
||||
|
||||
- 不重新实现服务器,不添加前端方便但服务器不存在的字段或 requestId。
|
||||
- 不将原 Game_Modify/Utl 全量搬成同名 TypeScript 类。
|
||||
- 不在本阶段引入 ECS、Worker、全局响应流框架、反射容器或远程热插件系统。
|
||||
- 不以此文档承诺完成二七王叫分、出牌、计分、回放或结算画面。
|
||||
- 不自动修复已经用错误 roomtype 创建的服务器房间,不静默改写用户缓存。
|
||||
|
||||
### 1.3 “零耦合”的可验证定义
|
||||
|
||||
零具体游戏依赖:除应用组合根外,公共代码不得 import games/*。
|
||||
|
||||
零内部访问:游戏只能引用 sdk/contracts 和自身模块,不引用平台 Store、Router、PlatformStartup。
|
||||
|
||||
零引擎依赖的逻辑:房间应用层、状态与游戏规则/模型不 import cc,不读取 window、localStorage 或原生全局对象。
|
||||
|
||||
允许必要契约依赖:游戏和平台共同依赖小而稳定的 SDK。完全没有任何依赖既不可实现,也不是本方案目标。
|
||||
|
||||
## 2. 权威数据与兼容边界
|
||||
|
||||
### 2.1 平台数据
|
||||
|
||||
身份、连接/认证状态、房间公共元数据、成员、座位、在线/准备、公告、聊天、投票结果,以各自协议来源为准。平台负责公共协议字段的校验、归一化和状态提交。
|
||||
|
||||
归一化必须有实际协议依据、在入口单点执行。例如此前数字房号的兼容不能推广成“所有字段都 String/Number 一遍”。出站类型仍按对应 rpc 契约生成。
|
||||
|
||||
### 2.2 子游戏载荷
|
||||
|
||||
roomtype、deskinfo、deskwar、deskfree 及游戏 route 的数据,由游戏解释。平台只保存、传递、按协议判断载荷是否存在,不读取内部选项、手牌或分数。
|
||||
|
||||
拟议的通用可传输类型:
|
||||
|
||||
```ts
|
||||
type JsonValue = null | boolean | number | string
|
||||
| readonly JsonValue[]
|
||||
| { readonly [key: string]: JsonValue };
|
||||
|
||||
type Roomtype = JsonValue;
|
||||
```
|
||||
|
||||
运行时仅允许有限数字、无循环的 JSON 值,不允许函数、BigInt、引擎对象和 undefined。这是传输限制,不是玩法格式限制。每个外层协议若规定字段必需,则必须存在;不能把缺失字段改成 null。
|
||||
|
||||
“原样”指保持 JSON 数据结构、值、类型、数组顺序和未知字段;不要求保留 JSON 文本空格或对象键的序列化顺序。若协议某个字段本身是字符串编码,则字符串内容必须完整保留。
|
||||
|
||||
二七王创建编码仍由其编码器生成 11 位字符串;模板可以发送数组。平台不得为了接入方便转换任何一种格式。
|
||||
|
||||
### 2.3 游戏事实与公共展示
|
||||
|
||||
玩法包有时才包含下一局、参与状态和局内分数。因此平台不能要求所有公共展示变化只能来自平台 route,也不能允许游戏任意改 Store。
|
||||
|
||||
采用单向的 GameRoomProjection:游戏模型解析自己的协议后发布只读投影;公共展示选择器组合平台快照与游戏投影。
|
||||
|
||||
必须区分:
|
||||
|
||||
- 服务端公共 room stage 与游戏自己的 round phase,不用同一个字段存两者。
|
||||
- 账户余额与局内分数,不用 changeBean 写同一份值。
|
||||
- 公共准备状态与玩法参与/等待下一局状态,不把派生按钮显隐反写准备值。
|
||||
- 平台规则已禁止的动作与游戏附加限制:游戏可以收紧权限,不可扩大平台明确禁止的权限。
|
||||
|
||||
确有玩法包需要同步公共协议状态时,建立具名、带来源的映射,并在对应游戏适配器与协调器测试;禁止 patchRoom/setPlayerState 等任意写入接口。
|
||||
|
||||
## 3. 模块结构与依赖
|
||||
|
||||
以下为目标职责分布,不要求第一批任务一次搬完目录:
|
||||
|
||||
```text
|
||||
assets/
|
||||
app/ 唯一知道具体游戏的组合入口
|
||||
bootstrap/
|
||||
composition/
|
||||
framework/
|
||||
sdk/contracts/ 公共纯类型与稳定端口
|
||||
net/ 保留 WireClient、心跳、连接代际
|
||||
protocol/ 公共信封与协议适配
|
||||
platform/room/
|
||||
room-coordinator.ts 流程与事务编排
|
||||
room-scope.ts 会话作用域和清理
|
||||
room-mailbox.ts 有界业务消息等待队列
|
||||
room-entry.ts 不同进入来源的描述
|
||||
room-policy.ts 平台公共动作策略
|
||||
platform/stores/ 保留并渐进拆分公共状态
|
||||
presentation/ 无 Cocos 的展示模型与选择器
|
||||
adapters/ 网络、原生、存储等端口实现
|
||||
ui/room/ 公共 Cocos 面板与场景适配
|
||||
games/<game>/
|
||||
definition.ts 唯一游戏定义
|
||||
protocol/ 玩法解析、rpc 类型和入口映射
|
||||
rules/ roomtype、座位、描述等纯规则
|
||||
model/ 游戏状态和同步输入处理
|
||||
presentation/ 游戏展示模型
|
||||
cocos/ 创建页、房间 View、资源
|
||||
```
|
||||
|
||||
依赖关系:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
App[应用组合入口] --> Platform[平台房间应用层]
|
||||
App --> Game[具体游戏模块]
|
||||
App --> Cocos[Cocos 与设备适配]
|
||||
Platform --> SDK[共享契约]
|
||||
Game --> SDK
|
||||
Cocos --> SDK
|
||||
Platform --> Protocol[公共协议与状态]
|
||||
Game --> Rules[游戏规则与游戏模型]
|
||||
```
|
||||
|
||||
SDK 不能反向 import 平台实现。当前部分能力类型声明在 game-host-adapter.ts,迁移时应提取到契约目录,不能让 presentation 因一个类型依赖具体适配器。
|
||||
|
||||
## 4. 游戏定义与资源装配
|
||||
|
||||
### 4.1 唯一游戏定义
|
||||
|
||||
一个权威定义提供:本地 key、游戏协议 route、身份配置关联、规则、模块工厂、创建页/房间展示引用、游戏内容和可选能力声明。
|
||||
|
||||
身份中的 agentid/channelid/gameid 继续由配置来源提供。游戏定义引用该配置关联,不在多个文件复制实际渠道身份。
|
||||
|
||||
Cocos 组件可以引用生成的资源定义或唯一的装配资源,但不得与 TypeScript 定义分别维护两份 gameKey、route、prefab。实施时优先选择构建阶段验证明确静态引用的方式;不要运行时按名称扫描并猜测页面类型。
|
||||
|
||||
### 4.2 装配验证
|
||||
|
||||
- 每个创建 prefab 恰好一个创建页契约实现。
|
||||
- 每个房间 prefab 恰好一个房间展示契约实现。
|
||||
- 所有必需资源可解析,引用目标与声明的游戏一致。
|
||||
- 声明的协议 route 不与平台保留 route 冲突。
|
||||
- 不通过“随便创建再销毁一个游戏实例”验证声明;在真实创建时验证实例契约,避免工厂预检产生业务副作用。
|
||||
- 未使用的可选能力无需假实现;声明了必需能力却未绑定时装配失败。
|
||||
|
||||
最近二七王 prefab 中模板脚本残留的问题,必须由上述检查自动发现,而不是等创建 RPC 发出去才发现。
|
||||
|
||||
## 5. SDK 开发者接口
|
||||
|
||||
### 5.1 原则
|
||||
|
||||
SDK 面向开发者只暴露少量分组能力:room、messages、ui、audio、scope。内部实现可按职责拆分,不把每个旧函数变成独立服务。
|
||||
|
||||
禁止 getService(name)、任意 Store 写入、任意 app/route 发包、直接传平台节点。消息传输使用绑定当前游戏 route 的端口;通用房间命令由平台构建身份和房号。
|
||||
|
||||
### 5.2 核心形状
|
||||
|
||||
以下用于说明边界;完整字段按实施阶段测试补全,不是可直接粘贴的 SDK 实现:
|
||||
|
||||
```ts
|
||||
interface GameModel {
|
||||
// context 已绑定一个房间;不得缓存到跨房间全局。
|
||||
initialize(context: RoomContext): void;
|
||||
// 同步处理;不可返回等待动画结束的 Promise。
|
||||
handle(input: GameInput): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
interface RoomContext {
|
||||
readonly room: RoomReadPort;
|
||||
readonly commands: RoomCommandPort;
|
||||
readonly messages: GameMessagePort;
|
||||
readonly ui: GameUiPort;
|
||||
readonly audio: AudioPort;
|
||||
readonly scope: ScopePort;
|
||||
}
|
||||
|
||||
interface GameMessagePort {
|
||||
send(rpc: string, data: JsonValue): void;
|
||||
}
|
||||
```
|
||||
|
||||
GameMessagePort 的 void 表示传输操作本身,不表示服务器业务成功。实际回复仍按协议作为输入处理。游戏内部可以将 rpc 与 payload 建成强类型映射,公共平台不需要知道映射内容。
|
||||
|
||||
RoomReadPort 提供只读快照、按选择器订阅和座位映射。快照变更具有本地 revision;本地 revision 不加入网络包、不冒充服务器序列号。
|
||||
|
||||
### 5.3 输入事件保留来源
|
||||
|
||||
必须区分:
|
||||
|
||||
- created:成功创建结果,原 roomtype 和 infinite 等数据。
|
||||
- login-restored:登录恢复,携带原 deskinfo。
|
||||
- login-waiting:登录已有房间但无恢复快照。
|
||||
- joined-waiting:普通主动加入。
|
||||
- joined-snapshot:主动加入已有 deskinfo。
|
||||
- started:加入、准备、成员变化、self_makewar/makewar 引起的开战,携带 sourceRpc 和原整包。
|
||||
- player-ready/joined/left/online/offline/seat-changed:公共成员事件。
|
||||
- game-message:当前游戏 route 的原消息。
|
||||
- dissolution-settlement:服务端解散结果与原 deskfree,保留 freeNow/确认时机。
|
||||
|
||||
内部可用联合类型表示,不扩展服务器包。类型名称可以调整,但这些语义不能被一个 restore(unknown) 覆盖。
|
||||
|
||||
### 5.4 只读投影
|
||||
|
||||
游戏模型拥有 GameRoomProjection 并允许订阅;它只包含公共 HUD 真正需要的事实,不泄露完整手牌与内部模型。
|
||||
|
||||
第一阶段字段以模板和二七王实际需求为准:玩法阶段、自己的参与状态、按服务器座位的玩法分数、公共操作限制。投影是游戏状态的派生结果,不另设可任意写入的第二份状态。
|
||||
|
||||
RoomSnapshot revision 与 GameProjection revision 分别可追踪;在一次协议处理事务完成后向公共 UI 发布组合快照,避免 UI 看到同一消息的一半更新。
|
||||
|
||||
### 5.5 页面接口
|
||||
|
||||
创建页接收上次配置和 submit/cancel。编码由游戏完成,平台收到 roomtype 只封装与发送;未知或错误旧配置由游戏迁移策略处理,没有迁移策略就明确提示并允许用户显式重置。
|
||||
|
||||
房间 View 的职责只有挂载、根据模型绘制、将用户操作交给控制器、取消动画与卸载。它不读取 WebSocket、不直接恢复协议快照、不以节点可见性决定业务状态。
|
||||
|
||||
## 6. 生命周期与消息时序
|
||||
|
||||
### 6.1 分开建模
|
||||
|
||||
- Connection:连接中、已连接、重连中、停止。
|
||||
- Authentication:未登录、等待回复、已登录、拒绝/被踢。
|
||||
- RoomLifecycle:outside、entering、active、recovering、leaving、failed。
|
||||
- GamePhase:子游戏自己的轮次/行动阶段。
|
||||
- Dissolution/Settlement:投票和结果展示独立于 GamePhase。
|
||||
|
||||
这些维度存在明确约束,但不要混成一个巨大的状态枚举。
|
||||
|
||||
### 6.2 进入事务
|
||||
|
||||
1. 验证公共外层协议,保留原始载荷。
|
||||
2. 在游戏规则入口验证其拥有的配置并解析需要的座位约束;不在 UI 层补缺失数据。
|
||||
3. 完成公共状态构建的预检。已接受的新服务端房间不能在失败时偷偷恢复成“旧房间仍有效”。
|
||||
4. 使旧 scope 失效并取消旧任务,创建新的 generation,提交进入状态。
|
||||
5. 创建游戏模型、绑定能力,启动异步资源与界面挂载。
|
||||
6. 界面挂载就绪后,以正确来源发送进入输入;恢复使用原快照,不自行填快照字段。
|
||||
7. 标记可交互,按顺序处理进入后等待的业务消息。
|
||||
|
||||
失败策略:步骤 1–3 失败不创建半初始化 scope;步骤 4 后失败将新会话标为 failed 并完整释放,不伪装 active。是否重新登录恢复,由现有连接/恢复契约决定,不能对格式错误无休止自动重试。
|
||||
|
||||
### 6.3 异步就绪
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Net as 协议入口
|
||||
participant Room as 房间协调器
|
||||
participant View as Cocos 场景适配
|
||||
participant Game as 游戏模型
|
||||
Net->>Room: 接受创建/加入/登录结果
|
||||
Room->>Room: 预检并建立新 scope
|
||||
Room->>Game: initialize(context)
|
||||
Room->>View: mount(scope)
|
||||
Net->>Room: 后续房间业务消息
|
||||
Room->>Room: 暂存有界 FIFO
|
||||
View-->>Room: ready
|
||||
Room->>Game: 带来源的进入输入
|
||||
Room->>Game: 按序处理暂存消息
|
||||
```
|
||||
|
||||
model.initialize 只绑定上下文与初始模型,不要求 View 已可用。需要页面的操作通过能力端口等待就绪,且离房时可取消。禁止每种能力各自无界积压请求来弥补没有统一就绪契约。
|
||||
|
||||
旧 mainSceneLoaded 的通知时点与“可以访问节点”的内部就绪点分开。兼容映射保留原 create/login/join 的调用语义,不用新名字掩盖顺序变化。
|
||||
|
||||
### 6.4 队列规则
|
||||
|
||||
队列只保存当前已认证、当前 generation 的房间业务消息,FIFO;不改变原有登录屏障规则。换服尚未建立目标会话的消息按连接意图处理。
|
||||
|
||||
容量与最长等待时间由平台配置来源显式给出,先通过基线和压力测试确定;消费方不使用随意 fallback。溢出或超时导致明确诊断和恢复,不丢最旧的游戏包继续假装成功。
|
||||
|
||||
踢出、断线、用户取消等控制信号可立即使 scope 失效,不等待场景/动画。没有服务器序列号时,不按 rpc 名或 payload 相等对玩法事件去重。
|
||||
|
||||
### 6.5 准备和开战
|
||||
|
||||
准备请求发出只更新本地 pending,不提前更改权威 ready。回复提交 ready 后产生成员事件;如包中携带 deskwar,再提交相应阶段并触发 started,原整包保留。
|
||||
|
||||
同步处理状态不等待动画。动画队列属于 View,断线/恢复可以取消旧动画并直接渲染当前状态。开始按钮的条件来自统一策略,不由各按钮、摇一摇与子游戏分别实现。
|
||||
|
||||
### 6.6 解散与结算
|
||||
|
||||
投票倒计时不宣布结果。服务端拒绝结束本轮投票;普通通过等待结果确认后传递结算;freeNow 按协议立即传递。没有 deskfree 时按已核实的退出路径结束房间。
|
||||
|
||||
有 deskfree 时保留游戏与必要玩家上下文直到结算结束。明确区分 round-finished、room-finished 和 result-view-closed;不允许一个模糊 gameOver 同时决定分享、清空状态和跳场景。
|
||||
|
||||
### 6.7 退出与换服
|
||||
|
||||
requestExit 是请求,不是本地 leave。平台按当前模式和阶段决定退出/申请解散等合法动作,等待对应响应。
|
||||
|
||||
换服保留既定 connection intent 和重发信封;解散换服可能仍需保留结算 scope。这里不能用统一“换服就销毁游戏”替代原协议路径。
|
||||
|
||||
scope 失效是立即、幂等的;任何晚到回调只能释放它自己的资源,不能发包、提交状态或挂载节点。
|
||||
|
||||
## 7. 能力、错误与资源
|
||||
|
||||
### 7.1 输入与模态框
|
||||
|
||||
数字/文本/倍率输入返回明确的 confirmed/cancelled 结果。取消原因区分用户取消、房间结束、请求被替代;真实失败则拒绝并提供原因。
|
||||
|
||||
每种单例输入面板明确声明冲突策略。默认设计为:同一 scope 的重叠请求拒绝,调用方显式结束前一个后再开新请求;若现有业务必须替换,则旧请求必须得到 cancelled,不能丢失回调。
|
||||
|
||||
不会因为 Promise 方便就把没有业务回复的发包动作包装成“等待成功”。
|
||||
|
||||
### 7.2 Scope
|
||||
|
||||
拥有订阅、定时器、原生回调、资源租约、界面实例和未完成输入。释放顺序:停止接收/提交业务 → 取消异步输入与动画 → 释放游戏模型和 View → 释放能力资源与订阅。使用逆序登记的清理栈,异常逐项收集,不因一个 disposer 抛错而跳过其余清理。
|
||||
|
||||
共享头像/音频缓存不属于单个房间;房间只持有可释放租约。缓存有容量限制和明确逐出策略,不能以“缓存”名义永久持有所有资源。
|
||||
|
||||
### 7.3 错误分类
|
||||
|
||||
**Layer612_Tips 专用于玩家业务提示。** 游戏和平台的业务拒绝、输入校验、操作结果及可操作的超时提示可以使用;代码异常、堆栈、协议/配置校验失败、资源导入/加载错误、模块未接入与调试信息只能进入诊断日志,不得直接显示 error.message。禁止为了显示技术错误而改用踢出面板;踢出面板仅处理真实业务踢出。业务提示与诊断端口必须分开命名、分别注入。
|
||||
|
||||
- 协议/状态不一致:阻止当前事务,显式失败,记录来源和字段。
|
||||
- 业务拒绝:展示服务端约定信息、解除 pending,不终止正常连接。
|
||||
- 用户取消:正常结果,不上报 fatal。
|
||||
- 展示资源失败:局部能力按明确定义处理;没有 fallback 定义时报告资源错误,不能影响无关网络逻辑。
|
||||
- 清理失败:继续清理其余项,保留原始异常链。
|
||||
|
||||
诊断包含 direction、route/rpc、连接代际、房间 generation、生命周期阶段和原始 cause。完整包日志由 debugIO 控制;对外提交的错误报告与生产日志需要独立的敏感字段处理,不复用完整调试包。
|
||||
|
||||
## 8. 性能策略
|
||||
|
||||
优先处理已证实的结构性成本:
|
||||
|
||||
1. 聊天使用本地稳定消息 ID 维护行,追加不重建旧行。ID 不发送到服务器。
|
||||
2. 头像按 URL 去重加载、共享纹理租约;行复用时验证当前 URL/generation,避免旧图片写到新玩家。
|
||||
3. 选择器按最小相关状态通知;同帧合并的是绘制,不是业务消息。
|
||||
4. 投票保留按座位复用;小座位列表不引入不必要的虚拟化。
|
||||
5. 长聊天历史在测量后决定可变行高虚拟列表;上翻历史不被新消息强制滚底。
|
||||
6. 倒计时按来源建立截止时间并按差值绘制;后台恢复后重算,不累计定时器减法误差。
|
||||
7. 不在每个 UI 消费点递归 clone;入口隔离后结构共享。大型 deskinfo 的所有权策略应单独测量验证。
|
||||
|
||||
基线指标:进入/恢复至可交互耗时、主线程 p95、帧时间、长任务、节点数、纹理数、聊天追加开销、重复进退后的活跃订阅/计时器。
|
||||
|
||||
绝对耗时目标由目标设备测量确定。本版本不编造性能数字;结构性目标可以立即验收:一条消息不重建已有行,重复头像不重复创建纹理,100 次进入/退出后作用域资源数量回到基线。
|
||||
|
||||
## 9. 回放隔离
|
||||
|
||||
回放使用独立 ReplayContext、玩家列表、视角、时钟和消息源。可以复用游戏模型/展示,但不能共享在线 RoomStore、认证身份或真实网络发送能力。
|
||||
|
||||
未声明回放能力的游戏无需实现空回调。回放是后续独立阶段,本阶段只保证 SDK 不以全局状态阻止未来隔离。
|
||||
|
||||
## 10. 子游戏接入流程
|
||||
|
||||
1. 创建游戏定义并关联唯一配置身份与资源。
|
||||
2. 实现 roomtype 编码、读取、座位规则和纯描述。
|
||||
3. 将不同进入来源和游戏协议映射到自身模型输入。
|
||||
4. 创建页提交自己的配置;房间 View 订阅模型,操作走绑定能力。
|
||||
5. 声明需要的可选能力,不填写无意义的示例返回值。
|
||||
6. 运行接入测试和 prefab 唯一性检查,再在 Cocos 中验证真实按钮→实际包。
|
||||
|
||||
接入验收的核心是第二款游戏加入时不修改平台核心、公共 UI 或公共协议分发器。开发者不需要了解 PlatformStartup 内部字段,也不需要手动复制 agentid/playerid/roomcode。
|
||||
|
||||
## 11. 兼容与迁移策略
|
||||
|
||||
保留现有网络、状态缓存和会话租约,逐条迁移。每条消息在同一时刻只由一套业务处理器负责;严禁双跑旧新处理器发送重复命令或产生两份状态。
|
||||
|
||||
旧接口只在过渡适配层存在,并记录移除条件。新游戏不能新增 Utl 依赖。资源操作通过 Cocos MCP,主工作区已有修改不自动 stash、不覆盖、不批量恢复。
|
||||
|
||||
迁移检查从现有 framework/games 扩展到公共 scripts/ui,唯一豁免为明确的 app 组合根。CI 同时检查静态 import、契约测试和资源装配。
|
||||
|
||||
## 12. 风险与设计取舍
|
||||
|
||||
接口分层增加初期代码量,但减少后续游戏接入修改面;通过少量能力分组控制抽象数量。
|
||||
|
||||
原流程存在不合理顺序和混合状态,不能逐行复制。保留协议与业务效果,对改变的生命周期时点明确记录兼容映射及测试。
|
||||
|
||||
扩大 Roomtype 的平台承载类型需要检查全部消费方,防止 UI 或存储再次假设 length/index。未知格式不得在平台描述或转为字符串显示为玩法规则。
|
||||
|
||||
等待队列只解决界面加载窗口,不是事件日志系统。缺少服务器序列号时不承诺精确一次业务投递;重连依赖服务器权威快照及原协议屏障。
|
||||
|
||||
## 13. 完成标准
|
||||
|
||||
- 两种不同配置格式的游戏通过接入套件;新游戏无需修改平台核心。
|
||||
- 创建、加入、登录恢复、各开战入口的来源与载荷不丢失。
|
||||
- 加载中取消/踢出/换服、旧回调晚到、结算后释放均有测试。
|
||||
- 公共核心无 cc/window,游戏无平台内部依赖;装配检查发现重复页面。
|
||||
- 房间命令不由游戏手工拼公共身份;roomtype 不被平台解释或转换。
|
||||
- 聊天与头像增量策略通过资源计数/真实交互验证。
|
||||
- 文档明确未实现玩法,不以空函数或单元测试替代真实重连显示。
|
||||
|
||||
## 14. 待实施阶段收敛的参数
|
||||
|
||||
队列容量/等待时间、缓存容量、性能目标设备和绝对耗时阈值,由 P0/P3 基线确定并写入单一配置来源。第一阶段按平台核心与契约优先安排;二七王完整玩法和回放另立范围,不隐含在此次重构中。
|
||||
@@ -0,0 +1,20 @@
|
||||
# 构建指南
|
||||
|
||||
返回[框架目录](../README.md)。本目录专门说明如何选择子游戏、配置版本、预览和导出 Web 包。
|
||||
|
||||
## 推荐阅读顺序
|
||||
|
||||
1. [单游戏构建操作](single-game-build.md):二七王与模板的完整构建步骤、Bundle 勾选、切换游戏、配置复用、产物检查和排错。
|
||||
2. [版本配置与身份来源](version-and-build.md):XML 字段、网页与原生身份、版本包、构建扩展和发布环境限制。
|
||||
3. [旧构建目录与脚手架说明](legacy-project-layout.md):识别旧独立工程流程,避免使用错误的构建命令。
|
||||
4. 新接入游戏先完成[子游戏接入指南](../integration/README.md)和[创建房间接入](../integration/create-room.md)。
|
||||
|
||||
## 当前构建流程
|
||||
|
||||
保存启动场景中的 gameKey → 核对游戏入口、版本 XML 和 Bundle 元数据 → 构建面板仅保留公共包与目标游戏包 → 使用独立输出目录构建 → 检查代码、资源、XML → 通过 HTTP 运行导出网页。
|
||||
|
||||
本目录说明的是同一个 YouleNexus 工程内选择一款子游戏。切换现有游戏不修改框架代码、框架资源或引擎源码,也不依赖构建后删除其他游戏目录。
|
||||
|
||||
## 当前边界
|
||||
|
||||
已支持按 gameKey 动态加载游戏入口与私有 prefab,以及自动输出原生兼容 XML。构建时在 Bundle 列表保留公共 resources、目标游戏和目标版本包;当前还未自动同步 gameKey 与构建勾选项。正式发布环境注入仍待完善。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 旧独立子工程目录的作用与迁移
|
||||
|
||||
返回[构建指南](README.md)。当前操作见[单游戏构建](single-game-build.md)。旧目录核查与迁移日期:2026-09-08。
|
||||
|
||||
## 原来作用
|
||||
|
||||
顶层 cocoscreator_projects/games 是旧“每款游戏单独 Cocos 工程”脚手架预留的目录,与当前 YouleNexus/assets/games 完全不同。
|
||||
|
||||
旧 new-game 脚本会复制工程种子到该目录,创建 assets/game 代码骨架,并通过 assets/framework junction 共享宿主框架;setup-links 重建链接;build-game 将独立工程实体化到临时构建工作区,再调用 Cocos CLI。check-skin、check-cocos-version、bump-cocos 也保留对该旧目录的支持。
|
||||
|
||||
当前 Bundle 入口、运行时与 version.xml 构建扩展都使用 YouleNexus 内的子游戏,不依赖这个顶层目录。
|
||||
|
||||
## 实际内容与处理
|
||||
|
||||
删除前逐项核查:只有 .gitkeep 和三份二七王文档,无 package.json、assets、游戏代码、资源或 reparse/junction 链接。因此它实际上没有可构建的独立子工程。
|
||||
|
||||
三份文档已逐文件校验后迁入当前文档树:
|
||||
|
||||
- [创建房间接入](../../subgames/erqiwang/create-room-integration.md)
|
||||
- [玩法设计](../../subgames/erqiwang/design/design.md)
|
||||
- [玩法协议](../../subgames/erqiwang/protocol/packet_protocol.md)
|
||||
|
||||
迁移后删除 .gitkeep,再确认无遗留文件,删除空旧目录。真正使用的 YouleNexus/assets/games 未删除或移动。
|
||||
|
||||
## 剩余旧工具
|
||||
|
||||
本次没有删除旧脚手架脚本、模板或测试,也没有把它们冒充成当前架构的工具。旧 new-game 命令仍可能重新创建顶层目录,不能用于新接入;旧 build-game 也不是当前单工程选择构建入口。
|
||||
|
||||
当前操作以[单游戏构建](single-game-build.md)为准。是否整体移除旧脚手架及相关 npm 入口属于后续工具清理,不影响当前同工程构建。
|
||||
@@ -0,0 +1,153 @@
|
||||
# 单游戏构建操作
|
||||
|
||||
返回[构建目录](README.md)。关联文档:[版本配置](version-and-build.md)、[子游戏接入](../integration/README.md)。核对日期:2026-09-09,Cocos Creator 3.8.8。
|
||||
|
||||
## 1. 当前能力与边界
|
||||
|
||||
一个 YouleNexus 工程可以包含多个子游戏。公共 app/framework/scripts 不静态导入具体子游戏,启动时按 `gameKey` 加载目标游戏 Bundle 的入口,并加载该游戏的创建页面和房间 prefab。
|
||||
|
||||
构建时使用 Creator 自身的 Bundle 选择能力,使未选游戏的代码和资源不进入产物。不修改引擎源码,不先构建全部游戏再手动删除。
|
||||
|
||||
**目前需要配合完成两项选择:保存启动场景的 gameKey;在构建面板勾选对应的 Bundle。两者尚未自动同步。** 修改 gameKey 不会自动改变构建面板,导入构建配置也不会修改场景 gameKey。
|
||||
|
||||
当前真实验收范围是二七王 Web Mobile:产物无模板游戏实现及私有资源,导出网页进入登录页,二七王创建页与房间 prefab 可实例化,XML 版本正确。此结果不代表完整牌局、服务器重连或原生壳已重新验收。模板的同类步骤如下,但不能将独立实验工程的双向验证当作两款正式游戏均已完成导出运行验收。
|
||||
|
||||
## 2. 一次性资源准备
|
||||
|
||||
以下位置均以 YouleNexus 工程为基准。资源元数据、prefab 和场景使用编辑器配置;AI 操作走 Cocos MCP,不手改序列化文件。
|
||||
|
||||
公共 `assets/resources` 包名为 `resources`,当前优先级 8,包含 `dev-config` 等公共运行资源。它不是任何一款子游戏的私有目录,当前开发构建必须保留。
|
||||
|
||||
二七王的配置:
|
||||
|
||||
- 游戏目录:`assets/games/erqiwang`;包名 `game-erqiwang`;优先级 **1**。
|
||||
- 版本目录:`assets/games/erqiwang/resources`;包名 `version-erqiwang`;优先级 **2**。
|
||||
- 入口类名:`GameEntry_erqiwang`;static definition 提供本游戏定义。
|
||||
- 版本资源:`assets/games/erqiwang/resources/erqiwang/version.xml`。
|
||||
|
||||
模板的配置:
|
||||
|
||||
- 游戏目录:`assets/games/template`;包名 `game-Game_Surface_3`;优先级 **1**。
|
||||
- 版本目录:`assets/games/template/resources`;包名 `version-Game_Surface_3`;优先级 **2**。
|
||||
- 入口类名:`GameEntry_Game_Surface_3`。
|
||||
- 版本资源:`assets/games/template/resources/Game_Surface_3/version.xml`。
|
||||
|
||||
**版本包优先级必须高于游戏根包。** 两者相同时,嵌套 XML 可能被父包收走,产生“版本包存在,但包内没有 version”的错误。修正元数据归属,不增加另一条运行时加载路径。
|
||||
|
||||
新增或移动脚本后,确认编辑器完成导入,资源数据库能查到脚本且已生成 `.meta`。磁盘上存在 GameEntry.ts,并不等于构建已能包含它。
|
||||
|
||||
## 3. 构建二七王 Web Mobile
|
||||
|
||||
1. 打开 YouleNexus 的 `assets/scenes/PlatformStartup.scene`。
|
||||
2. 找到 `SubgameAssets`,设置 `gameKey = erqiwang`,保留公共 `roomScene = TemplateRoom.scene`,保存场景。不要再绑定 createRoom、room;这两个 prefab 由目标入口自动加载。
|
||||
3. 核对二七王 XML;版本字段与原生读取约定见[版本配置](version-and-build.md)。
|
||||
4. 在项目扩展管理中启用 **Youle Version Manifest**。
|
||||
5. 打开“项目 → 构建发布”,选择 **Web Mobile**,启动场景选择 **PlatformStartup**。
|
||||
6. 参与构建的场景包含 PlatformStartup、PlatformLoading、PlatformLogin、PlatformLobby、TemplateRoom。TemplateRoom 是公共空白承载场景,名称包含 Template 不代表打入模板玩法。
|
||||
7. 在 Bundles 取消全选,选择 `resources`、`game-erqiwang`、`version-erqiwang`。`main`、`internal` 为正常构建自动保留的公共包。排除 `game-Game_Surface_3`、`version-Game_Surface_3` 以及所有其他游戏的包。
|
||||
8. 当前工程采用 debug/local 配置,保留 Debug。取消 Debug 不会自动切换生产配置,详见[开发与发布环境](version-and-build.md#6-开发构建与正式发布的区别)。
|
||||
9. 使用新的独立输出目录,例如 `build/erqiwang-web-check`。在未验证跨游戏增量清理前,不把另一款游戏的旧产物目录直接用作发布目录。
|
||||
10. 执行构建。构建期间不要修改启动场景、XML 和相关 Bundle 元数据。
|
||||
11. 确认构建成功,且日志出现 `[youle-version-manifest] erqiwang: .../version.xml`。然后执行下文产物检查与 HTTP 运行验证。
|
||||
|
||||
无需更改 defaults.ts、公共注册表、框架 prefab 或引擎安装目录。
|
||||
|
||||
## 4. 切换到模板或新增游戏
|
||||
|
||||
构建模板时,把启动场景 `gameKey` 改为 **Game_Surface_3** 并保存;保留公共场景与 `resources`,选择 `game-Game_Surface_3` 和 `version-Game_Surface_3`,排除二七王两个包,使用新的模板输出目录。
|
||||
|
||||
模板目录名是 template,但 gameKey 是 Game_Surface_3。不要把目录名直接当作 gameKey;游戏入口名、Bundle 名和版本资源路径必须一致。
|
||||
|
||||
新游戏先按[接入指南](../integration/README.md)在自己的目录中提供定义、入口、prefab 和 XML。完成后,构建步骤相同:选择 `<gameKey>`,保留 `game-<gameKey>`、`version-<gameKey>` 及公共包,排除所有其他游戏包。构建选择属于应用配置,不是在框架代码中增加游戏分支。
|
||||
|
||||
## 5. 保存与复用构建配置
|
||||
|
||||
在构建面板配置好平台、场景、Bundle 和输出位置后,使用 **Export** 保存配置;以后用 **Import** 恢复。建议每款游戏保留独立的构建配置,使用时仍核对启动场景已保存的 gameKey。
|
||||
|
||||
下列仅展示二七王配置的 Bundle 部分,**不是完整可导入文件**;其余字段由当前 Creator 构建面板导出,不手工拼凑启动场景 UUID。
|
||||
|
||||
```json
|
||||
{
|
||||
"bundleConfigs": [
|
||||
{"root": "db://assets/resources", "name": "resources", "output": true},
|
||||
{"root": "db://assets/games/erqiwang", "name": "game-erqiwang", "output": true},
|
||||
{"root": "db://assets/games/erqiwang/resources", "name": "version-erqiwang", "output": true},
|
||||
{"root": "db://assets/games/template", "name": "game-Game_Surface_3", "output": false},
|
||||
{"root": "db://assets/games/template/resources", "name": "version-Game_Surface_3", "output": false}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`root` 使用 **db:// 资源目录路径,不使用 UUID**。构建工具应在提交构建任务时提供选择结果;当前版本 XML 扩展不负责修改 Bundle 勾选项,也不保证发现所有代码或资源泄漏。
|
||||
|
||||
当前 `npm run build-game` 对应旧的独立子工程实体化脚本,不是上述同工程 Bundle 构建的一键入口。参见[历史脚手架说明](legacy-project-layout.md)。
|
||||
|
||||
## 6. 检查实际产物
|
||||
|
||||
二七王构建的关键目录应为:
|
||||
|
||||
```text
|
||||
<输出目录>/
|
||||
index.html
|
||||
version.xml
|
||||
assets/
|
||||
internal/
|
||||
main/
|
||||
resources/
|
||||
game-erqiwang/
|
||||
version-erqiwang/
|
||||
src/
|
||||
settings.json
|
||||
```
|
||||
|
||||
还会有引擎代码、样式等公共文件。检查以下内容:
|
||||
|
||||
- `assets` 不存在 game-Game_Surface_3、version-Game_Surface_3 或其他未选游戏目录。
|
||||
- `src/settings.json` 的 assets.projectBundles 只列出当前公共包与二七王两个包;数组顺序不是验收条件。
|
||||
- 检查所有生成 JS,不能出现 createTemplateGame、TemplateCreateRoomView、TemplateRoomView、GameEntry_Game_Surface_3 等模板实现。仅删除 Bundle 文件夹不能清除已混入公共脚本的代码。
|
||||
- 版本包的资源路径包含 `erqiwang/version`,不能只检查版本包文件夹是否存在。
|
||||
- 输出根目录 version.xml 与源 XML 字节或 SHA256 一致。保持原生兼容文件名,不改成 gameKey.xml。
|
||||
|
||||
在 cocoscreator_projects 目录使用 PowerShell 检查某次输出,按实际目录修改第一行:
|
||||
|
||||
```powershell
|
||||
$webOutput = Resolve-Path 'YouleNexus/build/erqiwang-web-check'
|
||||
Get-ChildItem (Join-Path $webOutput 'assets') -Directory | Select-Object -ExpandProperty Name
|
||||
$settings = Get-Content (Join-Path $webOutput 'src/settings.json') -Raw | ConvertFrom-Json
|
||||
$settings.assets.projectBundles
|
||||
Get-FileHash 'YouleNexus/assets/games/erqiwang/resources/erqiwang/version.xml'
|
||||
Get-FileHash (Join-Path $webOutput 'version.xml')
|
||||
rg -n 'createTemplateGame|TemplateCreateRoomView|TemplateRoomView|GameEntry_Game_Surface_3' $webOutput -g '*.js'
|
||||
```
|
||||
|
||||
最后一条没有匹配时,rg 返回码为 1,表示未检出这些标记。标记扫描只是辅助检查,不代替依赖边界和实际运行验证。
|
||||
|
||||
## 7. 运行与回归验证
|
||||
|
||||
通过 HTTP 服务或 Creator 构建预览打开导出目录,不用 file:// 双击 index.html。查看浏览器 Network 和控制台:
|
||||
|
||||
1. 只请求公共包、game-erqiwang、version-erqiwang,没有未选游戏包请求或 404。
|
||||
2. 进入登录页,版本来自目标 XML,启动没有 terminal/fatal 错误。
|
||||
3. 成功登录后,验证创建页与房间视图是二七王;验证创建房间、服务器拒绝和重连等真实业务路径。这一步需要测试账号与运行中的服务器,本次解耦验证没有代替完整牌局回归。
|
||||
|
||||
源码检查从 cocoscreator_projects 运行:
|
||||
|
||||
```powershell
|
||||
node scripts/check-import-boundaries.mjs
|
||||
node scripts/check-room-assets.mjs
|
||||
node --experimental-transform-types --test framework-tests/architecture/subgame-entry.test.ts
|
||||
node --test scripts/test/version-manifest-build.test.mjs
|
||||
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
|
||||
```
|
||||
|
||||
类型和源码测试通过不等于产物通过;正式发布以本次实际生成的目录和运行结果为准。
|
||||
|
||||
## 8. 常见故障
|
||||
|
||||
- **Missing subgame entry**:核对 gameKey、入口 ccclass 名、游戏包勾选及 GameEntry.ts 的编辑器导入状态。不要添加模板回退入口。
|
||||
- **Load assets/game-.../index.js failed**:检查目标游戏包是否进入构建、部署路径是否完整;若只在编辑器场景进程发生,用浏览器预览或导出包确认,不能直接当成浏览器运行结果。
|
||||
- **Bundle version-... doesn't contain .../version**:检查嵌套包优先级,确认 XML 实际归属版本包,而非父游戏包。
|
||||
- **本地配置读取失败,dev-config 路径不能解析**:检查公共 resources 是否被排除,同时核对当前是否采用 debug/local 配置。
|
||||
- **旧游戏仍进入产物**:检查 Bundle 是否全选、公共代码是否静态导入游戏、公共 prefab/场景是否引用私有游戏脚本或资源,以及输出目录是否来自旧构建。
|
||||
- **切换 gameKey 后仍请求旧游戏**:保存真正用于构建的启动场景,重新构建;检查浏览器缓存或部署缓存,不能只改编辑器中未保存的 Inspector。
|
||||
- **输出缺少根 version.xml**:确认项目扩展启用并完成构建,查看具体扩展错误。扩展失败的产物不要通过手动补文件冒充成功。
|
||||
@@ -0,0 +1,95 @@
|
||||
# 版本配置与身份来源
|
||||
|
||||
返回[构建目录](README.md)。具体操作见[单游戏构建](single-game-build.md)。本文维护版本字段、身份来源及 XML 构建扩展的职责。
|
||||
|
||||
## 1. version.xml 是身份和版本唯一配置文件
|
||||
|
||||
源文件在各子游戏目录中:二七王为 `assets/games/erqiwang/resources/erqiwang/version.xml`,模板为 `assets/games/template/resources/Game_Surface_3/version.xml`。路径相对于 YouleNexus,本文不复制第二份配置。
|
||||
|
||||
结构保持原生既有格式:XML 声明、game 根节点,以及 agent/game/channel/version 子节点。
|
||||
|
||||
- agent@id:网页默认 agentid。
|
||||
- channel@id:网页默认 channelid。
|
||||
- 子节点 game@id:服务器 gameid。
|
||||
- version@value:数值 versionCode,发送到登录包的 version 字段。
|
||||
- version@name:字符串展示版本 version,登录页显示为 v 加该字符串。
|
||||
- agent/game/channel 的 name 属性保留供原生读取,必须显式提供。
|
||||
|
||||
网页和构建共用这一份源 XML。构建出的副本由工具产生,不手工维护,不反向当作源码编辑。
|
||||
|
||||
不要把展示字符串(例如 1.8)作为登录数值版本发送。通用协议部分早期类型描述与真实登录校验存在历史差异,数值约定及实包依据见[数据结构说明](../protocol/04-数据结构.md)。
|
||||
|
||||
## 2. 网页与原生身份来源
|
||||
|
||||
网页预览从选中游戏的 XML 生成启动身份。原生环境 agentid/channelid/marketid 继续通过既有 settings/uAgent_3 接口读取,缺失时显式报错;gameid 与版本仍来自 XML。
|
||||
|
||||
原 XML 没有 marketid。网页的唯一 marketid 配置在应用层 `build-identity.ts` 的 WEB_MARKET_ID,当前为 4。不要为此擅自改变原生 XML 格式。
|
||||
|
||||
现有启动解析器仍支持显式 URL agentid/channelid/marketid/version 调试覆盖;gameid 不接受 URL 覆盖。普通预览不传身份参数即可使用 XML。URL version 如使用,含义是数值协议版本,而不是展示版本。
|
||||
|
||||
当前 XML 保留当前开发注册身份和 versionCode=1;展示版本二七王为 1.8、模板为 1.1。正式部署要在这份源 XML 中核对真实发布身份与版本,不照搬旧工程过期注册值。
|
||||
|
||||
## 3. 版本资源包设置
|
||||
|
||||
每个游戏的 resources 文件夹在编辑器中配置 Asset Bundle:
|
||||
|
||||
版本 Bundle 优先级为 2,游戏根目录 `game-<gameKey>` Bundle 优先级为 1。不要使用相同优先级,否则嵌套 XML 可能归入父包,版本包变为空包。
|
||||
|
||||
- 二七王目录 `assets/games/erqiwang/resources`:bundleName 为 `version-erqiwang`。
|
||||
- 模板目录 `assets/games/template/resources`:bundleName 为 `version-Game_Surface_3`。
|
||||
|
||||
运行时加载 `version-<gameKey>`,再加载包内 `<gameKey>/version` TextAsset。嵌套的 resources 目录不会自动进入全局 resources 包,必须完成上述配置。文件扩展名仍是 .xml,加载路径不带扩展名。
|
||||
|
||||
game-config.ts 只定位资源,不保存第二份 gameid 或版本。包名、路径和已保存的场景 gameKey 不一致时应修正来源,不能通过运行时备用路径掩盖。
|
||||
|
||||
## 4. 构建操作入口
|
||||
|
||||
按[单游戏构建操作](single-game-build.md)选择 gameKey、场景和 Bundle,并校验实际输出。游戏包与版本包必须配对选择,同时保留公共 resources。
|
||||
|
||||
Web Mobile 已做真实构建验证;Web Desktop 注册同一 hook 并通过接口测试,但不把这一点当作所有平台都完成了真实验收。
|
||||
|
||||
## 5. 构建扩展做了什么
|
||||
|
||||
项目扩展名为 **Youle Version Manifest**,实现目录为 `extensions/youle-version-manifest`。新编辑器环境需要确认已启用。
|
||||
|
||||
扩展读取构建任务真正的 startScene,而不是猜测当前编辑器正在显示哪个场景。它查找场景中唯一且启用的 SubgameAssets,然后在各游戏目录中查找唯一 `resources/<gameKey>/version.xml`。
|
||||
|
||||
构建前验证 XML、版本资源包元数据并保留快照。构建后验证启动场景未变、所选版本 bundle 确实包含在产物、源 XML/场景/元数据未变,再把 XML 字节原样写到实际输出根目录。
|
||||
|
||||
无效 XML、重复来源、磁盘场景中的无效选择、缺失所选版本 bundle、构建中来源变化都会阻止构建。扩展只读取已保存场景,不能替用户保存或识别 Inspector 中尚未保存的游戏切换。
|
||||
|
||||
扩展不会自动勾选游戏包、自动排除其他游戏、检查版本包内所有资源路径或扫描所有生成代码。看到 XML 发布成功日志,仍需完成单游戏产物检查。失败产物不能当成可发布结果。
|
||||
|
||||
## 6. 开发构建与正式发布的区别
|
||||
|
||||
当前 `profiles.ts` 是 debug、本地配置、连接本机 127.0.0.1:3088。刚构建出的包不会因为 Cocos 取消 Debug 就自动切换正式服务器;本地配置加载器也不允许在非调试构建使用本地配置。工程的脚本 loose 设置保持 false,避免对标准集合遍历产生不正确的降级编译结果。
|
||||
|
||||
目前环境配置仍位于框架历史 profiles.ts,这是尚未完成的应用层配置迁移,不符合未来“发布时无需修改框架”的完整目标。不要把修改它当作每个子游戏接入的步骤。正式发布前,需要完成应用发布环境注入或作为独立框架任务迁移这处配置,并验证 remote/release 的真实来源。
|
||||
|
||||
## 7. 当前限制与后续构建方向
|
||||
|
||||
- gameKey 选择游戏入口和私有 prefab;roomScene 保持公共承载场景绑定。
|
||||
- XML 选择和根目录输出已经自动化。
|
||||
- 公共应用层已解除具体游戏静态导入。构建需显式保留公共 resources、目标游戏和目标版本 Bundle;gameKey 尚未自动同步构建面板勾选项。
|
||||
- 原有仓库 `scripts/build-game.mjs` 使用另外的子工程实体化流程,不是当前 YouleNexus 同工程游戏选择的一键入口;不要直接把它当成这里的构建命令。
|
||||
- 未来统一构建入口应在应用/工具层完成选择、资源装配、环境注入与产物裁剪,并验证导出包仅含目标游戏,不随游戏切换修改框架。
|
||||
|
||||
## 8. 验证命令与常见故障
|
||||
|
||||
从 cocoscreator_projects 目录运行:
|
||||
|
||||
```powershell
|
||||
node scripts/check-import-boundaries.mjs
|
||||
node scripts/check-room-assets.mjs
|
||||
node --test scripts/test/version-manifest-build.test.mjs
|
||||
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
|
||||
```
|
||||
|
||||
引擎运行验证脚本 `verify-version-xml-preview.mjs` 需通过 Cocos MCP 在 Game View 上下文执行,不能直接在普通 Node 中运行。它会尝试加载两款游戏,适用于包含两款游戏资源的开发预览,不适用于已排除其他游戏的单游戏发布包。单游戏产物按[导出运行验证](single-game-build.md#7-运行与回归验证)检查。
|
||||
|
||||
- `Missing subgame entry`:目标 Bundle 未注册 GameEntry_<gameKey>。
|
||||
- `Cannot load version-...`:版本目录未设为对应 bundle,或构建排除了它。
|
||||
- `Version resource does not match selected game`:game-config 中的路径与 gameKey 不一致。
|
||||
- 根目录没有 XML:确认构建扩展已启用、所选启动场景正确,并查看构建日志;不要手动复制文件后掩盖失败。
|
||||
- 切换游戏仍出现旧创建页面:检查本游戏 definition.resources 与 gameKey。
|
||||
- 登录包的版本错误:核对 XML value 及是否传了 URL version 调试参数,不把 XML name 发给服务器。
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# 子游戏静态依赖解除实施计划
|
||||
|
||||
目标:公共框架与应用启动层不导入具体子游戏。沿用已经通过独立构建与导出网页验证的 Bundle 动态加载能力,不修改引擎或服务器。
|
||||
|
||||
1. 为入口解析补充失败测试,覆盖未知入口、键不匹配、资源越界;新增公共代码不得静态引用 games 的边界检查。
|
||||
2. 将游戏定义、聊天和菜单策略放入各自目录。每个游戏通过 `GameEntry_<gameKey>` 导出定义;公共入口只依赖 SDK 契约。
|
||||
3. SubgameAssets 按 gameKey 加载 `game-<gameKey>`,完成入口解析、私有 prefab 加载后才允许启动平台。版本仍使用各自 `version-<gameKey>` Bundle,XML 保持唯一身份来源。
|
||||
4. 使用编辑器 API 将游戏根目录设为 Bundle,将模板创建房间 prefab 移到模板游戏目录;清除启动场景的私有 prefab 引用,保留公共房间场景。
|
||||
5. 更新静态资源检查、测试夹具与接入说明。测试所有既有行为及失败边界;编辑器导入后验证资源引用和真实 Web 构建运行。
|
||||
|
||||
边界:本次解除运行时依赖;不更改房间协议、玩法与原生接口。构建选择器的自动化单独围绕现有 gameKey 实现,不引入第二份身份配置。
|
||||
@@ -0,0 +1,141 @@
|
||||
# 子游戏接入与目录约定
|
||||
|
||||
返回[文档首页](../../README.md)。后续阅读:[创建房间](create-room.md)、[版本与构建](../build/version-and-build.md)。
|
||||
|
||||
## 1. gameKey、目录、gameid、route 分别是什么
|
||||
|
||||
`gameKey` 是子游戏入口和 Bundle 的选择键,负责选择游戏实现和版本资源。目录只是文件位置。`gameid` 是服务器游戏身份,由 XML 提供。`route` 是游戏入口的路由标识,必须遵循实际消息契约,不能因重命名目录而修改。
|
||||
|
||||
当前二七王:
|
||||
|
||||
- 注册键和 definition.key:`erqiwang`。
|
||||
- 源码目录:`assets/games/erqiwang`。
|
||||
- gameid:读取该游戏 XML 的 `game/game@id`,不是字符串 `erqiwang`。
|
||||
- 当前 definition.route:`erqiwang`;不要因此把平台创建房间请求的 route 改为 erqiwang,该请求仍是 `agent/create_room`。
|
||||
|
||||
当前模板:
|
||||
|
||||
- 注册键和 definition.key:`Game_Surface_3`。
|
||||
- 源码目录:`assets/games/template`。
|
||||
- 版本路径仍使用 `Game_Surface_3`,不是 template。
|
||||
|
||||
两者通过子游戏入口定义和 Bundle 名称建立关联,所以目录和 gameKey 目前允许不同。不能只把模板 gameKey 改成 template;现有注册项、资源路径与 bundle 名称不会自动跟随。
|
||||
|
||||
新游戏建议统一 `目录名 = gameKey = definition.key`。这是前端命名约定,不影响服务器 gameid 或协议路由。当前构建扩展允许 gameKey 使用英文字母、数字、下划线、连字符。
|
||||
|
||||
## 2. 建议目录
|
||||
|
||||
以下是新游戏 mygame 的组织示例,不表示该游戏已经存在:
|
||||
|
||||
```text
|
||||
YouleNexus/assets/
|
||||
app/composition/
|
||||
game-definitions.ts # 通用入口解析,不引用具体游戏
|
||||
SubgameAssets.ts # 动态加载目标游戏及版本
|
||||
games/mygame/
|
||||
game-config.ts # 定位版本 XML 和私有资源根
|
||||
game-definition.ts # 本游戏定义
|
||||
GameEntry.ts # 注册 GameEntry_mygame
|
||||
mygame-game.ts # GameBinding / GameEntry / GameModule
|
||||
mygame-room-rules.ts # 自己解释和生成 roomtype
|
||||
room-chat-config.ts # 自己的聊天配置
|
||||
MyGameCreateRoomView.ts
|
||||
MyGameCreateRoom.prefab
|
||||
MyGameRoomView.ts
|
||||
MyGameRoom.prefab
|
||||
resources/ # 编辑器设置为 version-mygame bundle
|
||||
mygame/version.xml # 身份和版本唯一来源
|
||||
```
|
||||
|
||||
框架中不添加 mygame 的游戏 ID、玩法开关、节点查找路径或特殊 RPC 分支。需要扩展公共契约时,应作为独立框架能力设计,而不是每接入一个游戏就改框架。
|
||||
|
||||
## 3. 配置与应用注册
|
||||
|
||||
游戏配置可参照 [erqiwang/game-config.ts](../sdk/contracts.md):
|
||||
|
||||
```ts
|
||||
export const gameConfig = Object.freeze({
|
||||
versionResource: 'mygame/version',
|
||||
assetRoot: 'games/mygame',
|
||||
});
|
||||
```
|
||||
|
||||
当前运行时要求 versionResource 精确等于 `<gameKey>/version`。XML 和资源包准备见[构建文档](../build/version-and-build.md)。
|
||||
|
||||
在自己的 `assets/games/mygame/game-definition.ts` 导入本游戏工厂、配置和聊天配置,导出只包含本游戏的 `GAME_DEFINITIONS`。公共应用层不添加注册项。需要提供的完整字段以 [GameDefinition](../sdk/contracts.md) 为准:
|
||||
|
||||
- `key`:与注册键及场景 gameKey 一致。
|
||||
- `route`:核对既有游戏路由,不自动从目录推导。
|
||||
- `config`:自己的版本资源定位配置。
|
||||
- `createGame(gameId)`:接收已解析的游戏身份,返回 GameBinding,不另写一份 gameid。
|
||||
- `roomMenu`:显式提供 mainSceneButton、vipInfinite 两个布尔策略。
|
||||
- `chat`:按 [RoomChatConfig](../sdk/contracts.md) 提供该游戏聊天配置。
|
||||
- `createPageClass`、`roomViewClass`:相应 Cocos 组件类名,注意 `@ccclass` 名称。
|
||||
- `resources.createRoom`、`resources.room`、`resources.roomScene`:资源路径描述,参照已有注册项。
|
||||
|
||||
**createRoom 和 room 按定义从目标游戏 Bundle 自动加载,不再绑定到启动场景。** 创建页面运行时是通过 `CreateRoomPage` 基类取组件;仅填写 createPageClass 不能替代继承该基类及在游戏 prefab 挂载组件。房间视图按 roomViewClass 取组件。
|
||||
|
||||
## 4. 实现房间入口和生命周期
|
||||
|
||||
参考 [erqiwang-game.ts](../sdk/contracts.md),但不要把其中尚未实现的玩法处理当成完整游戏模板。
|
||||
|
||||
`GameBinding` 提供 entry、mount(view)、restoredSnapshot、roomProjection。`GameEntry` 提供 key、gameId、route,以及:
|
||||
|
||||
- `resolveSeatCount(roomtype)`:解析真实服务器返回的本游戏 roomtype,返回座位数;输入非法时显式报错。平台不负责解释玩法字段。
|
||||
- `createModule()`:为房间创建独立的 GameModule,不在模块间共享上一房间的可变状态。
|
||||
|
||||
[GameModule](../sdk/contracts.md) 生命周期:
|
||||
|
||||
- `attach(host)`:接收平台能力并订阅状态。
|
||||
- `handlePlatformEvent(event)`:处理平台转交的房间事件。
|
||||
- `handleGameMessage(message)`:按本游戏协议处理玩法消息。
|
||||
- `restore(deskinfo)`:按本游戏快照契约恢复状态和视图。只保存原始数据不等于完成重连恢复。
|
||||
- `dispose()`:解除订阅、定时器与界面引用,结束后不继续发送或响应旧会话操作。
|
||||
|
||||
通过 `requireRoomCapabilities(host)` 使用 room/messages/ui/scope 能力,不从游戏中访问 PlatformRuntime、平台 Store、WebSocket 或另一款游戏的模块。
|
||||
|
||||
- room:快照、订阅、座位映射、准备、退出、解散。
|
||||
- messages:发送符合本游戏协议的消息。
|
||||
- ui:业务提示、具有所有权的 loading、数字输入及倍数选择;输入取消是正常结果,应处理取消分支。
|
||||
- scope:注册房间结束时的清理逻辑。
|
||||
|
||||
完整接口见 [game-capabilities.ts](../sdk/contracts.md)。当前未提供可用的房间音频所有权能力,不假设它存在。
|
||||
|
||||
## 5. 编辑器绑定与选中游戏
|
||||
|
||||
打开 `assets/scenes/PlatformStartup.scene`,在挂有 `SubgameAssets` 的节点配置:
|
||||
|
||||
1. `gameKey`:目标游戏键。
|
||||
2. 创建页面 prefab 在本游戏定义中声明,根节点挂自己的 CreateRoomPage 子类。
|
||||
3. 房间 prefab 在本游戏定义中声明,根节点挂匹配 roomViewClass 的组件。
|
||||
4. `roomScene`:用于承载房间的 SceneAsset。
|
||||
5. 保存场景;构建扩展读取磁盘中已保存的场景,而不是未保存的 Inspector 状态。
|
||||
|
||||
两款现有游戏当前都使用 `TemplateRoom.scene` 作为承载场景,场景名称不意味着二七王使用模板的玩法逻辑。现有二七王房间视图继承 [CommonRoomViewBase](../sdk/contracts.md),公共控制通过已有属性绑定。
|
||||
|
||||
## 6. 接入验收
|
||||
|
||||
- 注册键、definition.key、gameKey、版本资源路径与 bundle 名称一致。
|
||||
- 创建页面重复打开/关闭不重复注册监听;忙碌时不能重复提交。
|
||||
- 抓取实际 create_room 出包确认 roomtype,而不仅检查 UI 选项。
|
||||
- 验证成功、服务器拒绝、重连已有房间、退出后再进房间。
|
||||
- 游戏解析自己真实的 deskinfo;公共平台不转换子游戏快照格式。
|
||||
- 切换至另一款游戏并验证其身份、创建页面与版本 XML,避免留下另一款游戏的绑定。
|
||||
- 控制台无异常,业务提示不承载调试输出,房间释放后无旧订阅和迟到回调。
|
||||
|
||||
当前二七王 handleGameMessage 与解散结算仍有显式“尚未接入”分支,房间视图标识“玩法尚未接入”并隐藏准备按钮。这些是后续子游戏工作,不应宣称创建房间成功就已完成牌局迁移。
|
||||
|
||||
## 7. Bundle 入口
|
||||
|
||||
游戏根目录设置为 `game-<gameKey>` Bundle,优先级 1;版本子目录保持 `version-<gameKey>` Bundle,优先级 2。版本包必须比游戏包优先,保证 XML 属于版本包,不被父目录包提前收走。
|
||||
|
||||
```ts
|
||||
import {_decorator} from 'cc';
|
||||
import {GAME_DEFINITIONS} from './game-definition.ts';
|
||||
@_decorator.ccclass('GameEntry_mygame')
|
||||
export class MyGameEntry {
|
||||
static readonly definition = GAME_DEFINITIONS.mygame;
|
||||
}
|
||||
```
|
||||
|
||||
启动等待目标 Bundle 加载完成,通过公开类注册 API 读取入口定义,然后加载私有 prefab 和版本 XML。入口缺失、键不匹配、资源越界或加载失败均显式报错,不回退到其他游戏。新增游戏不修改 app/framework/scripts 或公共资源。
|
||||
@@ -0,0 +1,130 @@
|
||||
# 子游戏创建房间接入
|
||||
|
||||
返回[接入指南](README.md)。本章对应原工程的“创建页面选择玩法 → create_room → 进入房间”流程。
|
||||
|
||||
## 1. 平台与子游戏的职责
|
||||
|
||||
子游戏负责创建页面布局、选项状态、房卡展示、已有配置回显、roomtype 编码和校验。平台负责弹窗生命周期、忙碌状态、统一发包、身份与设备环境注入、响应分发和房间进入。
|
||||
|
||||
创建页面只调用 `context.submit(roomtype)`,不自行拼接 agentid/playerid/gameid/ip/location,不直接创建 WebSocket,不自行调用 create_room RPC。
|
||||
|
||||
平台将 roomtype 作为不透明配置保存和转发。它会验证可用的数据形态,但不会把所有游戏限制成字符串、数组或同一长度。当前模板使用数组,二七王新请求使用 11 位字符串。禁止对通用 roomtype 使用 String()/JSON.stringify() 进行协议类型转换。
|
||||
|
||||
## 2. 页面契约
|
||||
|
||||
接口定义:[create-room-page.ts](../sdk/contracts.md)。引擎组件基类:[CreateRoomPage.ts](../sdk/contracts.md)。
|
||||
|
||||
```ts
|
||||
interface CreateRoomPageContext {
|
||||
readonly previousRoomtype: unknown;
|
||||
submit(roomtype: Roomtype): void;
|
||||
cancel(): void;
|
||||
reportError(message: string): void;
|
||||
}
|
||||
|
||||
interface CreateRoomPagePort {
|
||||
open(context: CreateRoomPageContext): void;
|
||||
setBusy(busy: boolean): void;
|
||||
close(): void;
|
||||
}
|
||||
```
|
||||
|
||||
该代码用于说明现有契约;实际开发应直接 import SDK 类型,不复制一份接口到子游戏。
|
||||
|
||||
自己的组件应继承 CreateRoomPage,并挂在创建 prefab 的根节点。现有参考:
|
||||
|
||||
- [ErqiwangCreateRoomView.ts](../sdk/contracts.md)
|
||||
- [TemplateCreateRoomView.ts](../sdk/contracts.md)
|
||||
|
||||
### open(context)
|
||||
|
||||
1. 先清理前一次打开遗留的监听和 context。
|
||||
2. 验证必要的编辑器绑定完整,缺少控件时显式报错。
|
||||
3. 由本游戏规则对象解释 previousRoomtype,恢复 UI 选中状态。
|
||||
4. 绑定选项、提交和取消操作,保存解除监听的函数。
|
||||
5. 更新消费展示和按钮状态。
|
||||
|
||||
`previousRoomtype === null` 表示没有历史配置,子游戏规则来源可以在此定义自己的首次创建默认值。已有但非法的历史配置不应悄悄替换为默认值;应明确暴露或实现有协议依据的版本迁移。
|
||||
|
||||
当前平台从 localStorage 的 `tsgame_roomtype_<gameid><agentid>` 读取历史 JSON 的 data,交给页面。页面不必重新读取本地存储,也不要按另一套 key 再维护一份历史。
|
||||
|
||||
### setBusy(busy)
|
||||
|
||||
平台请求期间禁用提交和会改变 roomtype 的选项,避免双击重复创建。取消/关闭策略按业务处理;二七王当前保留关闭入口。忙碌状态由平台驱动,不能以固定延时假装请求结束。
|
||||
|
||||
### close() / onDestroy()
|
||||
|
||||
解除所有事件、清空 context,重复调用安全。组件 onDestroy 调用 close。异步回调应检查 context 或会话是否仍有效,关闭后不能继续 submit。
|
||||
|
||||
`context.reportError` 当前进入控制台诊断,不是给玩家看的提示接口。编码错误、缺少组件、堆栈等不得通过 Layer612_Tips 展示。
|
||||
|
||||
## 3. roomtype 放在独立规则对象中
|
||||
|
||||
建议将规则逻辑放在本游戏的 room-rules.ts,页面只负责从控件调用 select/build。规则对象应能在不启动 Cocos 的情况下测试:
|
||||
|
||||
- 无历史时的明确默认值。
|
||||
- 合法历史的解析和回显。
|
||||
- 非法值拒绝。
|
||||
- UI 选项到线协议字段的映射。
|
||||
- 房卡文案所需计算;最终扣卡仍以服务器为准。
|
||||
- 新请求编码,以及服务器回包/重连输入的解析。
|
||||
|
||||
**请求编码与历史解析不一定采用相同的严格程度。** 必须以真实协议和服务端行为为依据,不能为了让旧数据通过而在平台增加通用兜底。
|
||||
|
||||
## 4. 二七王的具体参考
|
||||
|
||||
[ErqiwangRoomRules](../sdk/contracts.md) 将 UI 顺序映射到服务器位序。UI 顺序与线协议顺序不同,不可直接拼接各 Toggle 的排列顺序。
|
||||
|
||||
当前创建页面需要在编辑器序列化:
|
||||
|
||||
- 3 个 ToggleContainer。
|
||||
- 8 个 Toggle,以及对应 8 个选项 Label。
|
||||
- 1 个提交 Button、2 个关闭/取消 Button。
|
||||
- 1 个费用 Label。
|
||||
|
||||
前三组选项是局数、扣卡方式、查牌设置;后两个独立选项是傍王和爬坡。准确的位定义见[二七王玩法协议](../../subgames/erqiwang/protocol/packet_protocol.md),本指南不另维护一份位索引常量。
|
||||
|
||||
当前 build() 新请求输出:**11 位字符串,前 5 位为 0/1,后 6 位为 0**。例如所有开关取 0 时是字符串 `"00000000000"`。此例不是平台默认配置,也不能转换成数字 0。
|
||||
|
||||
已有/回包配置解析当前允许至少 5 位、前 5 位为 0/1的字符串;新请求仍固定输出 11 位。二七王座位数由自己的 resolveSeatCount 返回 3,平台不内置该人数。
|
||||
|
||||
## 5. 完整提交与响应链
|
||||
|
||||
```text
|
||||
大厅打开创建弹窗
|
||||
→ 从 SubgameAssets.createRoom 实例化 prefab
|
||||
→ 根节点取得 CreateRoomPage 组件
|
||||
→ page.open(context),回显 previousRoomtype
|
||||
→ 点击创建:本游戏 rules.build()
|
||||
→ context.submit(roomtype)
|
||||
→ 平台请求管理与 PlatformRuntime.createRoom
|
||||
→ 既有 agent/create_room RPC
|
||||
→ 服务器响应
|
||||
→ RuntimeSession 解析公共响应
|
||||
→ 本游戏 resolveSeatCount(真实 roomtype)
|
||||
→ 平台提交房间状态,进入房间会话并挂载视图
|
||||
```
|
||||
|
||||
发包信封保持 `app: youle`、`route: agent`、`rpc: create_room`。data 中的身份字段、ip、location 由平台按既有契约提供,roomtype 原样采用子游戏生成值。通用协议入口见[agent 协议 create_room](../protocol/02-协议-agent路由.md)。该文档早期数组描述针对模板,不代表二七王也应发送数组。
|
||||
|
||||
发送成功不代表创建成功。必须等待服务器返回:拒绝响应走平台业务拒绝流程;成功响应才进入房间。页面不要在点击提交时直接切换到房间场景。
|
||||
|
||||
## 6. 房间视图与重连接续
|
||||
|
||||
创建 prefab、房间 prefab 是不同资源。除了创建页面,还要完成应用注册中的工厂和 roomViewClass,在本游戏定义中声明 room 路径,在 SubgameAssets 保留公共 roomScene 绑定。
|
||||
|
||||
房间状态/准备/退出通过公共能力接口;玩法消息通过本游戏 GameModule;登录发现已有房间时,deskinfo 必须由本游戏 restore 解释。只验证 create_room 正常,不足以证明重连已接通。
|
||||
|
||||
不要在创建页面 open 中建立房间模块、订阅房间消息或提前发送准备指令;该页面处于入房之前。
|
||||
|
||||
## 7. 推荐验证顺序
|
||||
|
||||
1. 纯规则测试:选项组合、历史恢复、边界值、非法输入。
|
||||
2. 实际 prefab:字段绑定完整,重复打开关闭无重复监听,费用显示正确。
|
||||
3. 实际按钮提交:截获 context.submit 参数,确认类型与完整 roomtype 内容。二七王可覆盖全部 32 种开关组合。
|
||||
4. 平台出包:核对最终 agent/create_room 的类型和字段,不能只看规则单测。
|
||||
5. 成功与拒绝:忙碌状态正确释放,拒绝不进入房间,连续点击不重复发包。
|
||||
6. 房间接续:创建、加入、退出重进、登录重连、已有牌局 deskinfo 恢复。
|
||||
7. 资源释放:弹窗关闭及场景销毁后无事件、计时器和迟到提交。
|
||||
|
||||
实际服务器操作会创建房间或改变玩家状态,应使用明确的测试账号;纯规则和 prefab 验证可以通过注入 submit 回调完成,不需要真实发包。
|
||||
@@ -0,0 +1,234 @@
|
||||
# 00 · 框架架构设计(子游戏框架总览)
|
||||
|
||||
> 本章说明 `Game_Surface_3` 的**整体架构与设计思想**。它不是某一款游戏,而是一套
|
||||
> **「子游戏框架」**:平台壳(Surface)把登录、大厅、房间、网络、UI、社交等**通用能力**全部做好,
|
||||
> 一款具体游戏(SubGame)只需在固定的接入点里实现**自己的对局逻辑**。
|
||||
>
|
||||
> 理解这套分层,是用 CocosCreator 重写前端的前提——新前端应复刻"平台通用层 + 子游戏对局层"的边界,
|
||||
> 这样才能与服务器无缝对接(服务器只认协议,不关心前端用什么引擎)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 框架定位
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ 一个可发布的游戏 │
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌───────────────────────────┐ │
|
||||
│ │ Surface 平台壳 (00_) │ │ SubGame 子游戏 (01_) │ │
|
||||
│ │ 登录/大厅/房间/网络/UI │◄─►│ 仅实现「对局逻辑」 │ │
|
||||
│ │ 社交/支付/战绩/排行... │钩子│ 发牌/出牌/结算/界面 │ │
|
||||
│ └─────────────────────────┘ └───────────────────────────┘ │
|
||||
│ ▲ │
|
||||
│ │ 引擎回调分发 (gamemain.js) │
|
||||
│ ┌──────────────┴──────────────────────────────────────────┐ │
|
||||
│ │ gameabc 自研精灵引擎 (gameabc.min.js) + Spine 骨骼动画 │ │
|
||||
│ │ Canvas 渲染 / 触摸 / 定时器 / 资源加载 / WebSocket │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **平台壳**和**子游戏**用同一套代码骨架;换游戏时只替换 `01_SubGame/` 与美术布局数据。
|
||||
- 平台壳通过**钩子(Hook)**反向调用子游戏;子游戏通过调用平台 API(`Net.Send_*`、`GameUI.*`、`Desk`、`C_Player`、`set_self`)使用平台能力。
|
||||
|
||||
---
|
||||
|
||||
## 2. 四层结构
|
||||
|
||||
| 层 | 载体 | 职责 |
|
||||
|----|------|------|
|
||||
| **引擎层** | `gameabc.min.js`、`spine-canvas.js`、`SpineMgr.js` | Canvas 精灵渲染、触摸/绘制/定时/资源/网络底层回调、Spine 骨骼动画 |
|
||||
| **桥接层** | `gamemain.js`(`gameabc_face`) | 把引擎回调统一分发给上层三个消费者(GameUI / Game_Modify / gameCombat) |
|
||||
| **平台层** | `js/00_Surface/*`(12 个文件) | 登录、大厅、房间、解散、网络协议、玩家数据、UI、聊天、支付、战绩、排行、敏感词等 |
|
||||
| **子游戏层** | `js/01_SubGame/*`(3 个文件) | 子游戏配置、对局逻辑、自定义输入处理(接入点) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 引擎层:自研精灵(Sprite)系统
|
||||
|
||||
渲染不是 DOM,也不是常规游戏引擎,而是一套基于**精灵编号(spid) + 标签(tag) + 组(group)**的自研系统。
|
||||
美术界面在编辑器里排版后导出为 `output/gameabc_data.min.js`(精灵布局数据),运行时引擎据此渲染。
|
||||
|
||||
### 3.1 核心 API(子游戏与平台都用它操作界面)
|
||||
|
||||
| API | 作用 |
|
||||
|-----|------|
|
||||
| `set_self(spid, attr, value, ...)` | 设置某精灵的属性 |
|
||||
| `get_self(spid, attr, ...)` | 读取某精灵属性 |
|
||||
| `set_group(groupid, attr, value, ...)` | 对一组精灵批量操作(常用于整页显隐) |
|
||||
| `play_ani(...)` | 播放属性动画(位移/缩放/帧动画) |
|
||||
| `set_clip(...)` | 设置裁剪区域(滚动列表用) |
|
||||
| `ifast_addtospritefromspritecopy(fSpid, srcSpid, x, y, tag)` | 以模板精灵为原型,动态复制出带 tag 的子精灵(实现动态列表,如战绩/排行行项) |
|
||||
| `ifast_dllpritefromspritecopy(fSpid, tag)` | 删除动态复制的子精灵 |
|
||||
| `ifast_check_add(spid, x, y)` | 命中检测,返回被点中的子精灵 tag(动态列表点击) |
|
||||
| `ifast_mydrawbmp(...)` | 在 `gamemydraw` 回调里自绘位图(如勾选框的对勾) |
|
||||
|
||||
### 3.2 常见 attr 属性码(由源码用法归纳)
|
||||
|
||||
| attr | 含义 |
|
||||
|------|------|
|
||||
| 7 | 文本内容(`set_self(spid,7,"文字")`) |
|
||||
| 18 / 19 | x 坐标 / y 坐标 |
|
||||
| 20 / 21 | 宽 / 高 |
|
||||
| 37 | 显示/隐藏(1 显示 0 隐藏) |
|
||||
| 43 | 按钮状态/帧索引(多态按钮切换外观) |
|
||||
| 1 | 资源/图片绑定(`set_self(268,1,资源号)`,预加载/换图) |
|
||||
|
||||
> 这是平台与子游戏共享的"界面操作语言"。CocosCreator 重写时,这一层用 Cocos 的 Node/Sprite/Label/Widget 取代,
|
||||
> **不需要复刻 spid 体系**——只要最终把同样的协议数据展示出来即可。
|
||||
|
||||
---
|
||||
|
||||
## 4. 桥接层:`gamemain.js`(引擎 → 上层 的总分发)
|
||||
|
||||
引擎对象 `gameabc_face` 的所有回调都在此**广播**给三个消费者,顺序固定为 `GameUI → Game_Modify → gameCombat`:
|
||||
|
||||
| 引擎回调 | 时机 | 分发去向 |
|
||||
|----------|------|----------|
|
||||
| `gamestart(gameid)` | 引擎就绪 | **`Logic.AppStart()`**(应用总入口) |
|
||||
| `mousedown / mousedown_nomove / mouseup / mousemove` | 触摸 | `GameUI.utl*` + `Game_Modify.utl*`/`mouseup` + `gameCombat.utl*` |
|
||||
| `gamemydrawbegin / gamemydraw` | 每精灵绘制前/绘制 | 同上三方(子游戏在此自绘) |
|
||||
| `gamebegindraw / gameenddraw` | 每帧开始/结束 | `GameUI` |
|
||||
| `ontimer` | 定时器 | `GameUI.utlontimer` |
|
||||
| `ani_doend` | 动画结束 | `GameUI` + `gameCombat` |
|
||||
| `onloadurl` | 图片/资源加载完成 | `GameUI.onloadurl` |
|
||||
| `onresize` | 屏幕尺寸变化 | (预留) |
|
||||
| `tcpconnected/tcpmessage/tcpdisconnected/tcperror` | 引擎自带 TCP | **空**(实际网络走 `00_minhttp.js` 的 WebSocket 封装,不用引擎 TCP) |
|
||||
|
||||
> 关键:**子游戏不直接向引擎注册回调**,而是由 `gamemain.js` 转发。新前端可保留这种"统一事件总线 → 平台/子游戏"的模式。
|
||||
|
||||
---
|
||||
|
||||
## 5. 平台层模块清单(`js/00_Surface/`)
|
||||
|
||||
| 文件 | 模块 | 职责 |
|
||||
|------|------|------|
|
||||
| `02_Const.js` | `ConstVal` / `AppList` / `RouteList` / `RpcList` | 全局常量、协议名、UI 布局常量 |
|
||||
| `04_Data.js` | `GameData` | 全局运行时状态(服务器地址、连接状态、各种缓存) |
|
||||
| `08_Utl_Output.js` | `Utl` | 工具函数、本地存储、退出房间、部分发包 |
|
||||
| `07_Desk.js` | `Desk` | **牌桌/房间状态机** + 几乎所有房间类接收处理 |
|
||||
| `05_Func.js` | `Func` | 通用功能(HTTP、语音录制、截图分享、创建房间渲染等) |
|
||||
| `10_Game.js` | `Game` | 定位等少量游戏级杂项 |
|
||||
| `11_GameUI.js` | `GameUI` | **平台所有界面**(大厅/房间/聊天/战绩入口/弹窗/列表),最大文件 |
|
||||
| `12_Logic.js` | `Logic` | **应用生命周期 + 连接管理 + 消息分发 + 重连 + 桥接** |
|
||||
| `00_minhttp.js` | `min_tcp` / `min_http` | WebSocket 与 HTTP 底层封装 |
|
||||
| `09_Net.js` | `Net` | **协议收发层**(`Send_*` 发包 / `Net.<rpc>` 收包路由到 Desk/Player) |
|
||||
| `06_Player.js` | `Player` / `C_Player` | 玩家数据结构与玩家相关接收处理 |
|
||||
| `03_Banwords.js` | `banwords` | 敏感词库(聊天过滤) |
|
||||
|
||||
> 网络协议、数据结构的字段细节见 **01–04 章**。
|
||||
|
||||
---
|
||||
|
||||
## 6. 子游戏层(`js/01_SubGame/`)—— 这是开发一款游戏唯一要写的部分
|
||||
|
||||
| 文件 | 模块 | 职责 |
|
||||
|------|------|------|
|
||||
| `00_SubGame_Config.js` | `Game_Config` | 子游戏配置:房间人数、聊天/语音气泡位置、分享、客服、声音、调试开关等 |
|
||||
| `01_SubGame_modify.js` | `Game_Modify` / `gameCombat` | **子游戏实现**:输入处理、创建房间界面、战绩界面、对局渲染 |
|
||||
| `02_SubGame_Input.js` | `Game_Modify`(接口桩) / `gameHallImport` | 平台会回调、子游戏需实现的**钩子接口默认空实现** |
|
||||
|
||||
### 6.1 两类接入点
|
||||
|
||||
**A. 业务回调钩子**(平台 → 子游戏;定义在 `02_SubGame_Input.js`,子游戏按需重写):
|
||||
|
||||
| 钩子 | 调用时机(平台侧) |
|
||||
|------|-------------------|
|
||||
| `Game_Modify._ReceiveData(msg)` | 收到**游戏内协议包**(route 非 platform/agent/room)|
|
||||
| `Game_Modify.StartWar(msg)` | 开局(收到 makewar)|
|
||||
| `Game_Modify.Reconnect(deskinfo)` | 登录/进房响应**含 `deskinfo`** 时触发(还原牌局;模板为空实现,由子游戏自定义)|
|
||||
| `Game_Modify.DeskInfo(deskinfo)` | 进房响应含 `deskinfo` 但未自动开战(`deskwar` 假)时,传入 `deskinfo` |
|
||||
| `Game_Modify.createRoom / onCreateRoom` | 创建房间成功 |
|
||||
| `Game_Modify.myJoinRoom / playerJoinRoom(seat) / playerLeaveRoom(seat)` | 进/离房 |
|
||||
| `Game_Modify.playerOffline/Online(seat)` / `playerphonestate` | 在线/电话状态 |
|
||||
| `Game_Modify.onReady(seat)` / `changeSeat(s1,s2)` / `onSurrender(msg)` / `Free(msg)` | 准备/换座/投降/解散 |
|
||||
| `Game_Modify.updateScene / closeGameScene / onEnterMainScene / onExitMainScene / stopAllSounds` | 场景/声音管理 |
|
||||
| `Game_Modify.getRoomInfo/getFullRoomInfo/getStarLimit/getMult/getVideoByRoomType/getRoomMode...` | 平台**向子游戏取**房间展示信息(返回值)|
|
||||
|
||||
**B. 引擎事件钩子**(引擎 → 子游戏;定义在 `01_SubGame_modify.js`):
|
||||
|
||||
| 钩子 | 用途 |
|
||||
|------|------|
|
||||
| `Game_Modify.utlmousedown / mouseup / utlmousemove / utlmousedown_nomove` | 子游戏自己的触摸交互 |
|
||||
| `Game_Modify.gamemydraw / utlgamemydrawbegin` | 子游戏自绘 |
|
||||
|
||||
### 6.2 子游戏能调用的平台能力
|
||||
|
||||
- **发包**:`Net.Send_*(data)`(平台层 RPC,见 02/03 章);游戏内自定义包用 `Net._SendData(app, route, rpc, data)`
|
||||
- **房间/玩家状态**:`Desk.*`(roomcode、PlayerList、stage…)、`C_Player.*`、`Desk.GetPlayerBySeat(seat)`
|
||||
- **界面**:`set_self/get_self/set_group/play_ani` + `GameUI.*`(弹窗、提示、聊天气泡等)
|
||||
- **座位换算**:`Logic.ChangeToStatus(myseat, targetseat)`(把服务器绝对座位转成"以我为视角"的相对位置)
|
||||
- **配置**:`Game_Config.*`
|
||||
|
||||
---
|
||||
|
||||
## 7. 应用生命周期
|
||||
|
||||
```
|
||||
引擎就绪 → gameabc_face.gamestart() (gamemain.js)
|
||||
→ Logic.AppStart() (12_Logic.js:313)
|
||||
├─ 读取 URL/app 参数:渠道(channelid)、代理(agentid)、市场(marketid)、启动模式(LaunchMode)
|
||||
├─ Logic.setGameServer() → 仅解析配置(Game_Config.Debugger.gameserver);配置服 update_json 回调 ServerUrl_Succ → GameData.Server = urlserver
|
||||
├─ Desk.Create() → 建空牌桌(PlayerList)
|
||||
├─ C_Player = new Player(-1)
|
||||
├─ 加载敏感词、音频、定位(cityjson IP)
|
||||
└─ 连接:Logic.firstConnect/Connect → new WebSocket(ws://Server)
|
||||
→ onopen → Net.Send_login(...)(有条件:isLogin 重连,或读到 wxinfo cookie;见 01 章)
|
||||
→ Desk.login(资产+房间恢复) (见 04 章)
|
||||
├─ 有 roomcode:恢复房间;响应含 deskinfo → Game_Modify.Reconnect(deskinfo)
|
||||
└─ 无 roomcode:进大厅 GameUI.JumpMenuScene()
|
||||
大厅 → 创建/加入房间 → 房间(准备/开局) → 对局(游戏内协议) → 结算(over_game) → 回房间/大厅
|
||||
期间断线 → onclose → Logic.TryConnect()(轮换服务器)→ 重连后重发 login 恢复状态
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 游戏内对局协议的通道(子游戏自定义)
|
||||
|
||||
平台层协议(01–04 章)是固定的;**对局协议由子游戏定义**,复用同一条 WebSocket 与同一套信封:
|
||||
|
||||
- **下行**:服务器发 `route` 非 platform/agent/room 的包 → `Game_Modify._ReceiveData(msg)` → 子游戏按 `msg.rpc` 自行分发
|
||||
- **上行**:`Net._SendData("youle", "<游戏route>", "<游戏rpc>", {agentid,gameid,playerid,roomcode,seat, ...对局字段})`
|
||||
- **结算**:`over_game`;**重连快照**:登录/进房响应里的 `deskinfo`(结构由子游戏定义)
|
||||
|
||||
> `Game_Surface_3` 是**模板工程**:`02_SubGame_Input.js` 里 `_ReceiveData/StartWar/Reconnect` 等为**空实现**
|
||||
> (即未含具体玩法),所以本工程取不到某款游戏的对局字段。需从真实子游戏工程或抓包补全(见 05 章)。
|
||||
|
||||
### 真实数据结构样例(战绩,平台层 `get_player_grade1` 响应)
|
||||
|
||||
`01_SubGame_modify.js` 的 `gameCombat` 揭示了一个真实嵌套结构,可作为对局数据风格参考:
|
||||
```
|
||||
data = {
|
||||
asetcount: 总局数,
|
||||
gradeinfo: [ // 每场对局
|
||||
{
|
||||
overtime: "结束时间",
|
||||
roomcode: "房号",
|
||||
idx: 翻页索引,
|
||||
gameinfo1: { // 可能是 JSON 字符串,需二次 parse
|
||||
roundsum: 小局数,
|
||||
playerlist: [ [昵称, 总分], ... ],
|
||||
round: [ [ [座位, 该局分], ... ], ... ] // 每小局每人得分
|
||||
}
|
||||
}, ...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 给 CocosCreator 重写的架构映射
|
||||
|
||||
| 原框架 | CocosCreator 对应 | 说明 |
|
||||
|--------|-------------------|------|
|
||||
| gameabc 精灵引擎 + spid/tag/group | Cocos 场景/Node/Prefab/Label/Sprite | **不复刻 spid 体系**,只还原界面与协议数据展示 |
|
||||
| `gamemain.js` 事件总线 | Cocos 输入事件 + 自建 EventBus | 统一把交互/帧更新派发到 UI 与对局模块 |
|
||||
| `00_Surface/*` 平台层 | 一个"平台 SDK"模块(登录/大厅/房间/网络) | **按 01–04 章协议 1:1 实现**,是与服务器对接的关键 |
|
||||
| `Net._SendData` + 双层解包 | Cocos 的 WebSocket 封装 | 严格保持信封 `{app,route,rpc,data}`、双层包装、心跳识别(01 章)|
|
||||
| `Game_Modify` 钩子 | 对局模块对外接口 | 平台模块在相应时机回调对局模块 |
|
||||
| `Desk` / `C_Player` | 房间状态/玩家数据模型 | 按 04 章字段建模 |
|
||||
| `01_SubGame/*` | 具体游戏对局场景 | 自由用 Cocos 实现,只需吃平台给的数据、按对局协议收发 |
|
||||
|
||||
**核心结论**:与服务器"完美适配"只取决于**平台层协议(01–04 章)+ 对局协议(05 章,需补全)**的字节级一致;
|
||||
渲染引擎、UI 实现方式可以完全替换。把"平台 SDK"和"对局逻辑"在新前端里分清边界,就复刻了这套子游戏框架的精髓。
|
||||
@@ -0,0 +1,263 @@
|
||||
# 01 · 传输层与架构
|
||||
|
||||
> 本篇是「框架模板(00_Surface 平台层)↔ 服务器」协议规范的**传输层**部分:连接建立、握手、心跳、断线重连、服务器切换、收发包封装与过滤。
|
||||
> 双向完整(C→S 与 S→C)。不含子游戏对局包(见 05 章)。
|
||||
> 所有 `文件:行` 引用均按当前源码核对。源码 bug 用 🐛 标注;需后端抓包确认的用 ⚠️待服务器确认 标注。
|
||||
|
||||
---
|
||||
|
||||
## 1. 整体架构
|
||||
|
||||
```
|
||||
┌─────────────┐ 配置请求(HTTP) ┌──────────────────┐
|
||||
│ 新前端 │ ─────────────────► │ 配置服务器 │ 返回 update_json (身份层级配置)
|
||||
│ (Cocos) │ ◄───────────────── │ (gameserver 地址) │
|
||||
└─────────────┘ └──────────────────┘
|
||||
│ ws://ip:port (解析服务器配置后)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 大厅服务器(agent) ←切换→ 房间服务器(room) │
|
||||
│ 登录/房间列表/排行/任务/支付 开局/聊天/解散/对局 │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 同一时刻只保持**一条** WebSocket 连接(`Net.ws_tcp`)。
|
||||
- 大厅与房间是**不同的服务器地址**,通过 `connect_roomserver` / `connect_agentserver` 指令切换:
|
||||
服务器下发新地址 → 客户端把 `GameData.Server` 替换为新地址 → **关闭当前连接**(`Net.ws_tcp.close()`)→ `onclose` 触发后用新地址重连 → 重新登录/握手。
|
||||
- 路由空间 `platform / agent / room`(`02_Const.js:8-10`,字面值已确认与变量名一致)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 传输方式
|
||||
|
||||
源码支持两种传输,由 `ConstVal.netType` 决定(`02_Const.js:654`,默认值 `0`):
|
||||
|
||||
| netType | 方式 | 说明 |
|
||||
|---------|------|------|
|
||||
| `0` | **WebSocket**(默认,正式使用) | `Net.ws_tcp.send(JSON.stringify(msg))` |
|
||||
| `1` | HTTP Ajax(备用/降级) | `Func.AjaxHttp2(...)`,POST JSON |
|
||||
|
||||
> 大厅模式(`ConstVal.isGameHall==true`)启动时会被强制改成 `ConstVal.netType=1`(`12_Logic.js:383`)。子游戏(本框架模板的目标场景)走 `netType=0 / WebSocket`,这是线上实际通道,**新前端按 WebSocket 实现即可**。
|
||||
> 收包路径在两种传输下**不同**,详见 3.3 节对比。
|
||||
|
||||
### WebSocket 连接地址
|
||||
|
||||
WebSocket 在 `min_tcp(config)`(`00_minhttp.js:256`)内创建:
|
||||
```js
|
||||
var ws = new WebSocket("ws://" + config.ipport); // 00_minhttp.js:258,例: ws://127.0.0.1:5414
|
||||
```
|
||||
- `config.ipport` 来自 `GameData.Server`(`12_Logic.js:856 / 900 / 941` 等连接函数把 `server` 赋给 `config.ipport`)。
|
||||
- `GameData.Server` 的来源(netType==0 分支):启动实际调用 `get_config`(`12_Logic.js:521 / 1330`),以 POST 空 body 读取配置,URL 为 `gameserver + "?" + min_timestamp()`;`getConfig_Succ` 对响应文本 JSON.parse 后保存到内存 `GameData.serverConfig.data`。这里的 `data` 是客户端包装,远程 JSON 根对象本身不带这一层。
|
||||
- `get_paravalue` 按全局 → agent → game → channel → market 查找;每层第一个身份宽松相等的条目生效,只有 truthy 参数覆盖继承值,缺少层级即返回继承值,对象整体覆盖。
|
||||
- `getConfig_Succ` 选择 truthy `game_server_tcp`,否则 `player_server_tcp`;`serverType != 0` 时在配置解析之后覆盖为本地地址。debug/release 均读取配置,`isDebugger` 不控制读取开关。
|
||||
- 旧 `ServerUrl_Succ`(`12_Logic.js:533`)中的 `data.urlserver` 属于未启用的旧请求路径,不能作为当前启动配置契约。整份远程配置不写磁盘或 localStorage。
|
||||
- ⚠️ 注意:`Logic.setGameServer()`(`12_Logic.js:1270`)只设置 `Game_Config.Debugger.gameserver`(配置文件 URL),**不**设置 `GameData.Server`;两者是两步、不可混淆。
|
||||
- 切换房间/大厅服时,`GameData.Server` 被替换为 `_msg.data.roomserver` / `_msg.data.agentserver`(`09_Net.js:509 / 526`)。
|
||||
- `GameData.Server` 可为**单个字符串,也可为数组**(多候选服务器)。为数组时按 `GameData.serverIndex` 轮询,详见第 7 节。
|
||||
|
||||
---
|
||||
|
||||
## 3. 消息信封(Envelope)
|
||||
|
||||
### 3.1 客户端 → 服务器(发送)
|
||||
|
||||
统一出口 `Net._SendData(_app, _route, _rpc, _data)`(`09_Net.js:12`)。组装的是**单层**信封:
|
||||
|
||||
```json
|
||||
{
|
||||
"app": "youle",
|
||||
"route": "agent", // platform | agent | room
|
||||
"rpc": "player_login", // RPC 名称
|
||||
"data": { ... } // 业务数据
|
||||
}
|
||||
```
|
||||
|
||||
- `app` 字段固定取 `AppList.app`,值为 `"youle"`(`02_Const.js:5`,已确认恒为 `"youle"`)。
|
||||
- `route` 取 `RouteList.platform/agent/room`(`02_Const.js:8-10`)。
|
||||
- netType==0:`Net.ws_tcp.send(JSON.stringify(_msg))`(`09_Net.js:20`)。**单层**,直接发信封。
|
||||
- netType==1:走 `Func.AjaxHttp2(GameData.Server, _msg, ...)`(`09_Net.js:24`),POST JSON。
|
||||
|
||||
### 3.2 服务器 → 客户端(接收)—— WebSocket 路径:**单层**(「外层 data」是浏览器 MessageEvent,非协议层)
|
||||
|
||||
> **⚠️ 重要更正(2026-06-28 真机联调确认)**:早期本节误记为「双层 `{data:<inner>}`」。实测+源码核对确认:**服务器实发的协议帧是单层** `{route, rpc, data}`(实测还带 `app` 字段)。所谓"外层 `data`"是**浏览器 WebSocket `MessageEvent` 对象**,不是服务器的协议层。
|
||||
|
||||
依据 `00_minhttp.js:266` 的 `min_tcp()`:`ws.onmessage = config.onmessage;`——把**浏览器原生 `MessageEvent`** 直接交给 `12_Logic.js:119` 的 `this.onmessage(_msg)`。所以 `_msg` 是 `MessageEvent`,`_msg.data`(= `MessageEvent.data`)才是服务器实发帧(源码注释原文:「`msg.data` 才是服务器发过来的业务数据」`00_minhttp.js:265`)。`12_Logic.js` 里 `data=_msg.data`(137) 与 `_msg=_msg.data`(224) 读的是**同一个 `MessageEvent.data`**,只剥掉这一层浏览器事件壳,没有第二层。
|
||||
|
||||
```
|
||||
MessageEvent.data(= 服务器实发帧,单层)= {
|
||||
"app": "youle", // 实测服务器回包带 app(与客户端发包同形)
|
||||
"route": "...",
|
||||
"rpc": "...",
|
||||
"data": { ... } // 真正的业务数据
|
||||
}
|
||||
// 握手包:MessageEvent.data 为原始串 "@toconcon...";心跳包:{ "com": "@serverheartbeat" }
|
||||
```
|
||||
|
||||
**新前端落地(YouleNexus)**:传输层(`CocosWebSocketTransport` / 联调用 `WsTransport`)已取 `ev.data`(= MessageEvent.data = 服务器单层帧)交给 `decodeFrame`,故 **`decodeFrame` 不得再剥一层 `.data`**——直接把 frame 当单层 `{route,rpc,data}` 解。(曾因沿用"双层"误解多剥一层,导致 `kick_server` 等业务包被误丢弃,真机联调暴露并已修正。)
|
||||
|
||||
完整解包顺序(含三道前置过滤,务必照此实现,否则会误处理旧连接包/错误回包/握手心跳):
|
||||
|
||||
1. **旧连接去重**:`if(GameData.TcpID != this.id) return;`(`12_Logic.js:122`)——每次重连 `GameData.TcpID++`,每个 WebSocket 实例闭包持有自己的 `this.id`,只有最新连接的包才被处理,旧连接残留包直接丢弃。
|
||||
2. 若网络状态关闭 `if(!GameData.netWorkSate) return;`(`12_Logic.js:127`)。
|
||||
3. `_msg`(= MessageEvent;若传输直接给字符串则 `JSON.parse`,`12_Logic.js:133-136`)→ 取 `data = _msg.data`(= 服务器实发帧,`12_Logic.js:137`)。
|
||||
4. **特殊错误包**:若 `data === "webserve-服务器未工作"` → 进入重发登录分支后 `return`(详见 4 节)。
|
||||
5. 若 `data` 是字符串:
|
||||
- 若 `data.substr(0,9) === "@toconcon"` → **握手包,直接 `return` 忽略**(`12_Logic.js:176-179`)。
|
||||
- 否则 `data = JSON.parse(data)`(`12_Logic.js:180`)→ 得到单层 `{route,rpc,data}`。
|
||||
6. **重置收包超时定时器**(仅 `!ConstVal.isGameHall` 时创建,见第 4 节)(`12_Logic.js:182-217`)。
|
||||
7. 若 `data.com === "@serverheartbeat"` → **心跳包,直接 `return` 忽略,不回包**(`12_Logic.js:218-223`)。
|
||||
8. 令 `_msg = _msg.data`(仍是同一个 `MessageEvent.data`),必要时再 `JSON.parse`(`12_Logic.js:224-229`)→ 得到 `{route, rpc, data}`。
|
||||
9. **错误回包忽略**:`if(_msg.rpc == "submit_error") return;`(`12_Logic.js:235`)。
|
||||
10. **登录态门控**(`isSendLoginState`):若 `GameData.isSendLoginState==true`(已发登录、等待 `player_login` 响应期间),则除 `player_login`(清门控)与 `kick_server` 外,**其它收包一律 `return` 丢弃**(`12_Logic.js:238-257`)。
|
||||
11. 按 `_msg.route` 分发(见第 5 节,`12_Logic.js:258`)。
|
||||
|
||||
> 注:源码对 `MessageEvent.data` 做了 `typeof == "string"` 判断后再 `JSON.parse`,是为兼容服务器有时发对象、有时发字符串(**同一层**的两种编码,非两层)。新前端「是字符串就 parse 一次」即可。
|
||||
|
||||
### 3.3 服务器 → 客户端 —— HTTP(netType==1) 路径:单层、无信封过滤
|
||||
|
||||
netType==1 收包走 `Func.AjaxHttp2` 的成功回调(`09_Net.js:24-44`),与 WebSocket 路径**完全不同**:
|
||||
|
||||
- 回调直接拿到 `_msg`(字符串则 `JSON.parse`),**单层**:直接读 `_msg.route` / `_msg.rpc` 分发,**没有外层 `data` 信封、没有 `@toconcon` 握手、没有 `@serverheartbeat` 心跳、没有 30s 超时、没有 TcpID/isSendLoginState 三道过滤**。
|
||||
- 分发逻辑同 5 节:`route ∈ {platform,agent,room}` → `Netrpc`,否则 → `Game_Modify._ReceiveData(_msg)`。
|
||||
- 失败回调里若是登录请求(`input_msg=="playerLogin"`)→ `GameUI.OpenTips("网络状况不好")`(`09_Net.js:38-43`)。
|
||||
|
||||
> 新前端只需实现 WebSocket(netType==0) 路径;此节列出仅供对照,说明降级通道的差异。
|
||||
|
||||
---
|
||||
|
||||
## 4. 握手与心跳
|
||||
|
||||
| 包 | 方向 | 触发判定 | 频率 | 客户端动作 |
|
||||
|----|------|---------|------|-----------|
|
||||
| 握手包 | 服务器→客户端 | 外层 `data` 为字符串且 `substr(0,9)=="@toconcon"` | 连接成功后首包 | 忽略 `return` |
|
||||
| 心跳包 | 服务器→客户端 | 解包后 `data.com === "@serverheartbeat"` | ~20s 一次 ⚠️待服务器确认 | 忽略,**不回包** |
|
||||
| 收包超时 | 客户端本地 | —— | 每次收包重置定时器 | 仅 `!isGameHall` 启用;超时见下方降级逻辑 |
|
||||
|
||||
关键常量:`ConstVal.Max.heartbeat = 30000`(`02_Const.js:144`),即 30 秒收包超时阈值。
|
||||
|
||||
> ⚠️待服务器确认:心跳"约 20s 一次"仅源码注释佐证(`12_Logic.js:219` 注释「20秒一次」);客户端侧唯一硬常量是收包超时 30000ms。实际心跳间隔需抓包确认。
|
||||
|
||||
### 4.1 收包超时定时器(仅子游戏模式)
|
||||
|
||||
- 仅当 `!ConstVal.isGameHall` 时才 `setTimeout(..., ConstVal.Max.heartbeat)` 创建(`12_Logic.js:185-217`);**大厅模式不启用**此定时器。
|
||||
- 每次收到任意包都先 `clearTimeout` 再重建(滑动窗口)。
|
||||
- 30s 内未再收包 → `GameUI.OpenTips("网络较慢!")` + `GameData.heartBeatStage=true`,并按当前连接 `readyState` 分两条路径(仅非 Debugger 模式):
|
||||
- `readyState == CLOSED`:`disType=true; ConstVal? NetType=1; Logic.TryConnect();`(直接进重连)(`12_Logic.js:190-195`)。
|
||||
- `readyState == OPEN`:`NetType=1; GameUI.StartLoad(); isClose=true; Net.ws_tcp.close();` 并启 `setInterval(..., 5000)` 反复 `close()` 直到真正断开(`12_Logic.js:196-214`)。
|
||||
- 下次正常收包时若 `heartBeatStage==true`,会 `GameUI.CloseTips()` 并复位(`12_Logic.js:129-132`)。
|
||||
|
||||
> **结论**:心跳是服务器单向 push,新前端**无需**主动发送任何心跳包,只需:① 收到 `@serverheartbeat` 时不当作业务包丢弃;② 子游戏模式下维护一个「收到任意包就重置」的 30s 超时定时器用于断线判断与降级。
|
||||
|
||||
### 4.2 特殊错误包 `webserve-服务器未工作`
|
||||
|
||||
外层 `data === "webserve-服务器未工作"` 时(`12_Logic.js:138`):
|
||||
- **前置条件** `if(get_self(225,37,0,0,0) == 0)`(`12_Logic.js:139`,某 UI 状态判定)成立时:`setTimeout(..., 10000)` 即 10s 后用 `C_Player` 现有信息重新组装 `data` 并 `Net.Send_login(data)`(`12_Logic.js:140-159`)。
|
||||
- 然后 `return`,不继续后续解包(`12_Logic.js:161`)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 路由分发逻辑
|
||||
|
||||
解包得到内层 `{route, rpc, data}` 后(WS 路径 `12_Logic.js:258`;HTTP 路径 `09_Net.js:31`;`_SendData` 内 netType==1 回调亦同):
|
||||
|
||||
```js
|
||||
if (route === "platform" || route === "agent" || route === "room") {
|
||||
if (typeof Net[rpc] === "function") Netrpc; // 平台层:调用 Net.<rpc>(内层msg)
|
||||
} else {
|
||||
Game_Modify._ReceiveData(msg); // 其它路由:交给子游戏
|
||||
}
|
||||
```
|
||||
|
||||
- **平台层**:`route ∈ {platform, agent, room}` → 调用 `Netrpc`,`Net.<rpc>` 再转 `Desk.<rpc>` / `C_Player.<rpc>` 处理。
|
||||
- **游戏内**:其它 `route` → `Game_Modify._ReceiveData(内层msg)`,由子游戏自行按 `rpc` 分发(见 05 章)。
|
||||
|
||||
> 在新前端中,等价于实现一个 `dispatch(inner)`:先判断 route 是否平台层,是则查表调用对应处理器,否则进入游戏内协议处理器。
|
||||
|
||||
---
|
||||
|
||||
## 6. 登录前的完整握手流程(netType==0)
|
||||
|
||||
```
|
||||
1. 启动 AppStart → HTTP 请求配置服务器(gameserver=Game_Config.Debugger.gameserver)
|
||||
→ get_config POST 空 body → getConfig_Succ JSON.parse → 内存 GameData.serverConfig.data
|
||||
→ get_paravalue 按身份层级读取 → game_server_tcp 或 player_server_tcp → 显式本地覆盖
|
||||
2. (netType==0 时) AppStart 启 GameData.TcpTimer = setInterval(..., 3*GameData.timer) = 30s 首连看门狗
|
||||
(12_Logic.js:485-496):超时则 GameUI.OpenTips("网络状况不好...") 并 Net.ws_tcp.close()
|
||||
首连成功(onopen)或拿到配置后会 clearTimeout 它。
|
||||
3. Logic.firstConnect(GameData.Server) → new game_websocket(TcpID) → min_tcp() → new WebSocket("ws://"+server)
|
||||
4. onopen 触发 (12_Logic.js:3)。是否发 player_login 取决于状态(见 6.1)。
|
||||
5. 服务器先回握手包 @toconcon (忽略)。
|
||||
6. 服务器回 player_login 响应 (route=agent, rpc=player_login) → Net.player_login → Desk.login 处理。
|
||||
(登录响应字段见 02 章)
|
||||
7. 进入大厅或恢复房间/对局。
|
||||
```
|
||||
|
||||
### 6.1 onopen 发送 player_login 是**有条件**的(修正:非无条件)
|
||||
|
||||
`onopen`(`12_Logic.js:3-118`)按 `GameData.ConnectType` 与登录状态分支:
|
||||
|
||||
- 若 `!GameData.ConnectType`(普通连接,非服务器切换)且 `NetType==0`:
|
||||
- **重连场景** `GameData.isLogin == true`:用 `C_Player` 现有信息(agentid/openid/gameid/nickname/avatar/sex/province/unionid/city/version/channelid/marketid,含 deviceLogin 时附 telphone)组装 `data` 并 `Net.Send_login(data)`(`12_Logic.js:34-52`)。
|
||||
- **首登场景** `GameData.isLogin == false`:读 `Utl.getCookie(Utl.Config.wxinfo)`;**cookie 为 null 时不发** `player_login`(`12_Logic.js:53-79`);非 null 时 `SetWxInfo` 后组装并 `Net.Send_login(data)`。
|
||||
- 若 `GameData.ConnectType`(服务器切换重连):按 `GameData.ConnectRpc` 重发 `connect_roomserver`/`connect_agentserver`,或在 `disType` 下补发登录(`12_Logic.js:82-117`)。
|
||||
|
||||
> 修正要点:文档不可写「onopen 立即/无条件发 player_login」。首登必须先有 wxinfo cookie。
|
||||
|
||||
### 6.2 Send_login 的 4s 守护超时(子游戏模式)
|
||||
|
||||
`Net.Send_login(_data)`(`09_Net.js:122-238`)在 `!ConstVal.isGameHall` 且 `sendLoginTimer==null` 时,设 `setTimeout(..., 4000)`(`09_Net.js:155-172`):4s 内未收到 `player_login` 响应则
|
||||
`ConnectType=false; disType=true; NetType=1; isClose=true; Net.ws_tcp.close();` 并启 `setInterval(..., 5000)` 反复 `close()` 直到断开(再由 onclose 走重连/降级)。
|
||||
发出登录时设 `GameData.isSendLoginState=true`(`09_Net.js:152`),由 3.2 第 10 步门控收包。
|
||||
|
||||
---
|
||||
|
||||
## 7. 断线重连与服务器切换
|
||||
|
||||
### 7.1 onclose(`12_Logic.js:278`)
|
||||
|
||||
`onclose` 按 `GameData.ConnectType` 与 `GameData.firstConnect` 分流:
|
||||
|
||||
- `GameData.urlFail` 为真直接 `return`(配置都没拿到,不重连)(`12_Logic.js:281`)。
|
||||
- **非切换** `!ConstVal? !GameData.ConnectType`:
|
||||
- 若 `!GameData.firstConnect`(已过首连):`NetType=1; Logic.TryConnect();`,按 `disType` 显示断线 UI(`12_Logic.js:297-306`)。
|
||||
- 若 `GameData.firstConnect`(仍在首连阶段):`GameData.tryTimes++`;候选数 `ttimes`(数组时取 `Server.length`,否则 3);`tryTimes % ttimes == 0` 时提示「网络状况不好...」;`Logic.Connect(GameData.Server)`(`12_Logic.js:307-317`)。
|
||||
- **切换** `GameData.ConnectType`:`!disType` → `Logic.Connect(GameData.Server)`,否则 `GameUI.StartLoad()`(`12_Logic.js:319-326`)。
|
||||
|
||||
### 7.2 重连间隔与重连函数
|
||||
|
||||
- `GameData.timer = 10000`(`04_Data.js:42`)= 重连定时器间隔(`Logic.Connect`/`Logic.TryConnect` 内 `setTimeout(..., GameData.timer)`,`12_Logic.js:910 / 1021`)。
|
||||
- `Logic.firstConnect(server)`(`12_Logic.js:826`):首连,不延时。
|
||||
- `Logic.Connect(server)`(`12_Logic.js:869`):`setTimeout timer` 后连接。
|
||||
- `Logic.TryConnect()`(`12_Logic.js:915`):`netType==1` 时直接 `return`(不重连);否则立即连一次 + 再挂 `setTimeout timer` 一次。
|
||||
|
||||
### 7.3 多候选服务器轮询(`GameData.Server` 为数组)
|
||||
|
||||
两套独立计数路径:
|
||||
|
||||
- **首连阶段(onclose, firstConnect)**:`GameData.tryTimes++`;候选数取 `Server.length`;提示节流用 `tryTimes % ttimes`(`12_Logic.js:308-316`)。`firstConnect`/`Connect` 内若 `isArray(server)` 则 `serverIndex` 取 0 或 `(serverIndex+1)%length` 切换(`12_Logic.js:833-836 / 876-880`)。
|
||||
- **已登录后断线(TryConnect)**:`GameData.tryReconnectTimes++`;`tryReconnectTimes % 3 == 0` 时归零并 `serverIndex = (serverIndex+1)%Server.length` 切下一个(`12_Logic.js:952-962 / 1001-1011`)。即**每失败约 3 次轮换一个候选地址**。
|
||||
- 数组模式下每次连接都把 `GameData.sendLoginTimes = -1` 复位(`12_Logic.js:834 / 877 / 938 / 989`)。
|
||||
|
||||
### 7.4 服务器切换指令(S→C 推送)
|
||||
|
||||
> connect_roomserver · route=room · S→C 推送;服务器要求客户端切到房间服;推送字段(S→C) `roomserver`(string)—新房间服 ip:port;客户端动作:`GameData.ConnectType=true; GameData.ConnectRpc=connect_roomserver; GameData.ConnectPack=data; GameData.Server=data.roomserver; Net.ws_tcp.close()`(onclose 后用新地址重连,重连 onopen 会重发 `Send_connect_roomserver(ConnectPack)`);接收 `09_Net.js:503-512`;备注 —
|
||||
|
||||
> connect_agentserver · route=agent · S→C 推送;服务器要求客户端切回大厅服;推送字段(S→C) `agentserver`(string)—新大厅服 ip:port、`opt`(string)—切换原因;客户端动作:若 `opt == other_break_room || opt == free_room` 则先 `GameUI.StartLoad()`;随后 `ConnectType=true; ConnectRpc=connect_agentserver; ConnectPack=data; GameData.Server=data.agentserver; Net.ws_tcp.close()`;接收 `09_Net.js:518-529`;备注 ⚠️待服务器确认 `data.opt` 完整取值域(源码仅见其与 `other_break_room` / `free_room` 比较)。
|
||||
|
||||
> 这两个指令客户端也可主动发起:`Net.Send_connect_roomserver` / `Net.Send_connect_agentserver`(`09_Net.js:500 / 515`,route 分别为 room/agent,rpc 同名)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 错误上报(可选实现,C→S)
|
||||
|
||||
> submit_error · route=agent · C→S 单向上报(无响应);上报客户端异常;请求字段(C→S) `packet`(string)—出错的包、`msg`(string)—错误堆栈、`playerid`/`agentid`/`gameid`;发送 `09_Net.js:55-80`(`Net.submit_error`,同 `msg` 去重,仅当 `errorMsg` 变化才发);备注:catch 中由 `12_Logic.js:265-274` 在 `isSubmitError` 开启时调用。
|
||||
|
||||
> submit_log · route=agent · C→S 单向上报;与 submit_error 同结构;发送 `09_Net.js:81-101`(`Net.submit_log`);备注 ⚠️待服务器确认:其内部 `rpc` 字段写死为 `"submit_error"`(`09_Net.js:85`),疑似复用——是否存在独立 `submit_log` 协议待后端确认。🐛 若服务端需区分两类上报,此处 rpc 名错误。
|
||||
|
||||
> 注意:客户端对收到的 `rpc=="submit_error"` 回包**直接 `return` 忽略**(`12_Logic.js:235`)。新前端对适配非必需,可选实现。
|
||||
|
||||
```json
|
||||
{ "app":"youle", "route":"agent", "rpc":"submit_error",
|
||||
"data": { "packet":"<出错的包>", "msg":"<错误堆栈>",
|
||||
"playerid":..., "agentid":..., "gameid":... } }
|
||||
```
|
||||
@@ -0,0 +1,411 @@
|
||||
# 02 · 平台层协议 — agent 路由(大厅服务器)
|
||||
|
||||
> 信封:`{ app:"youle", route:"agent", rpc:"<下列名称>", data:{...} }`
|
||||
> 本篇覆盖 **route=agent 的全部平台层数据包**(双向)。每个 rpc 给出 C→S 请求字段与 S→C 响应/推送字段。子游戏对局包(route=room)不在此篇。
|
||||
> 通用身份字段:`agentid`(代理ID)、`gameid`(游戏ID)、`playerid`(玩家ID)。多数请求由各调用点手工拼装,未必三者齐全,下文逐条标注实际字段。
|
||||
> 统一条目格式:
|
||||
> > `rpc名` · route=agent · <方向>
|
||||
> > 场景 / 请求字段(C→S) / 响应或推送字段(S→C) / 源码 发送·接收 / 备注(⚠️待后端确认,🐛源码 bug)
|
||||
|
||||
---
|
||||
|
||||
## 登录 / 账号
|
||||
|
||||
### `player_login` · route=agent · C→S 请求 + S→C 响应
|
||||
- 接收路由补充(2026-09-06 实包核对):C→S 仍为 `agent/player_login`;S→C 可以是 `agent`、`room` 或 `platform` 下的 `player_login`。原工程 `12_Logic.js:238-260` 按 rpc 解除登录等待,再把这三类平台路由交给 `Net[rpc]`。已实测短信验证码失败为 `room/player_login`,data 为 `{msg:"短信验证码不正确",state:-1,time:0}`;必须解除等待并交给登录失败处理,不能因响应 route 与发包 route 不同而忽略。
|
||||
- 场景:连接建立后 `onopen` 自动发送;断线重连、切服后重发(12_Logic.js:35、:61 构造,09_Net.js:122 `Send_login` 注入)。
|
||||
- 请求字段(C→S):
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| agentid | string/int | 代理ID |
|
||||
| gameid | string/int | 游戏ID |
|
||||
| openid | string | 微信 openid(游客可为空/特殊值) |
|
||||
| nickname | string | 昵称 |
|
||||
| avatar | string | 头像 URL |
|
||||
| sex | int | 性别 0未知/1男/2女 |
|
||||
| province | string | 省份 |
|
||||
| city | string | 城市 |
|
||||
| unionid | string/int | 微信开放平台 unionid |
|
||||
| version | string | 客户端版本号(GameData.versionCode) |
|
||||
| channelid | string/int | 渠道ID |
|
||||
| marketid | string/int | 市场ID |
|
||||
| ip | string | 客户端IP,`Send_login` 注入 `returnCitySN.ip`(09_Net.js:125,仅 returnCitySN 存在时) |
|
||||
| location | object | 定位对象,`Send_login` 注入 `C_Player.addr`(09_Net.js:132,可为 null) |
|
||||
| machineid | string | 机器标识 `Logic.getMachineId()`(09_Net.js:139) |
|
||||
| machineroom | string | 机房标识 `Utl.getRoomcode()`(09_Net.js:140) |
|
||||
| telphone | string | 绑定手机号,仅 `GameData.sysConfig.deviceLogin` 开启时随 `telphoneAuto:true` 一起携带(12_Logic.js:48-50) |
|
||||
| telphoneAuto | bool | 设备号自动登录标记,同上条件下=true |
|
||||
| playerid | int | 可选,本地缓存 playerid(`GameData.loginPlayerid` 开启时由 `Logic.readPlayerId()` 注入,09_Net.js:143-148,用于复用账号) |
|
||||
- 响应字段(S→C):`state`(int 0成功/非0失败) + 大量字段,分两组:
|
||||
- **A 组 账号资产**:`roomcard`、`bean`、`bank`(仓库星星)、`bankpower`、`bankpwd`、`charm`、`sign`、`tel`、`invitecode` 等(06_Player.js:88 `SetMyInfo` 读取,详见 [04-数据结构.md → 登录响应](04-数据结构.md#登录响应-deskloginplayer_login))。
|
||||
- **B 组 房间恢复**:在房时附带房间快照(roomcode/seat/players/deskinfo 等),由 `Desk.login` 处理,详见 doc04。
|
||||
- 源码:发送 `09_Net.js:236`(`Net.Send_login`→`Net._SendData`);接收 `09_Net.js:240`(`Net.player_login`→`Desk.login`)。
|
||||
- 备注:— (请求侧字段以本表为准;响应数据结构引用 doc04)
|
||||
|
||||
### `query_player2` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:仓库转账前查询目标玩家信息。
|
||||
- 请求字段(C→S):`agentid`、`playerid`(目标玩家ID)。
|
||||
- 响应字段(S→C):`avatar`(string)、`nickname`(string)、`playerid`(int);昵称/头像均空时提示"未找到对应玩家"。
|
||||
- 源码:发送 `09_Net.js:870`;接收 `09_Net.js:873`→`Desk.query_player2`(`07_Desk.js:1368`)。
|
||||
- 备注:—
|
||||
|
||||
### `binding_phone` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:绑定手机号。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`phonenum`、`smmcode`(短信验证码)。
|
||||
- 响应字段(S→C):`phonenum`(string,回写 `C_Player.tel`)。
|
||||
- 源码:发送 `09_Net.js:830`;接收 `09_Net.js:834`→`Desk.binding_phone`(`07_Desk.js:1337`)。
|
||||
- 备注:—
|
||||
|
||||
### `send_phone_checkcode` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:发送手机短信验证码。
|
||||
- 请求字段(C→S):`agentid`、`phonenum`(构造点未集中定位,至少含手机号)。
|
||||
- 响应字段(S→C):无业务字段(`Desk.send_phone_checkcode` 为空实现,07_Desk.js:1342)。
|
||||
- 源码:发送 `09_Net.js:838`;接收 `09_Net.js:842`。
|
||||
- 备注:⚠️ 请求字段以后端实现为准。
|
||||
|
||||
### `send_phone_code_wechat` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:微信渠道发送手机验证码(绑定手机流程的另一入口)。
|
||||
- 请求字段(C→S):`agentid`、`phonenum`(唯一调用点 11_GameUI.js:2620-2623 当前被注释,按注释代码为 agentid+phonenum)。
|
||||
- 响应字段(S→C):无业务字段(`Desk.send_phone_code_wechat` 为空实现,07_Desk.js:1345)。
|
||||
- 源码:RpcList 定义 `02_Const.js:81`;发送 `09_Net.js:846`(`Send_send_phone_code_wechat`);接收 `09_Net.js:850`→`Desk.send_phone_code_wechat`(`07_Desk.js:1345`)。
|
||||
- 备注:⚠️ 框架完整定义并接线,但**当前前端唯一调用点(11_GameUI.js:2623)被注释,实际不发送**;字段以后端实现为准。
|
||||
|
||||
### `setSign` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:设置个性签名。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`sign`。
|
||||
- 响应字段(S→C):`sign`(string,回显写入 `C_Player.sign`)。
|
||||
- 源码:发送 `09_Net.js:736`;接收 `09_Net.js:740`→`C_Player.setSign`(`06_Player.js:525`)。
|
||||
- 备注:—
|
||||
|
||||
---
|
||||
|
||||
## 房间创建 / 进入(大厅侧)
|
||||
|
||||
### `create_room` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:玩家创建房间。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`roomtype`(房间类型配置数组,见 doc04);`Send_create_room` 自动注入 `ip`=`C_Player.ip`、`location`=`C_Player.addr`(09_Net.js:106-107)。
|
||||
- 响应字段(S→C):`state`(0成功)、`roomcode`、`seat`、`roomtype`、`makewar`、`asetcount`、`shortcode`、`infinite`;失败 `showerror`/`error`。详见 [04 → 房间响应公共字段](04-数据结构.md#房间创建--进入响应公共字段)。
|
||||
- 源码:发送 `09_Net.js:104`;接收 `09_Net.js:111`→`Desk.create_room` + `Game_Modify.createRoom`。
|
||||
- 备注:—
|
||||
|
||||
### `self_join_room` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:输入房号 / 快速加入 / H5 唤起 / 进 VIP 配置房。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`roomcode`(房号);`Send_self_join_room` 自动注入 `location`=`C_Player.addr`、`ip`=`C_Player.ip`(09_Net.js:254-255);进 VIP 配置房入口额外带 `vipMatch:1`(12_Logic.js:2171);比赛进房入口可带 `match_id`。
|
||||
- 响应字段(S→C):`state`、`roomcode`、`seat`、`isowner`、`players[]`、`roomtype`、`makewar`、`asetcount`、`deskwar`、`deskinfo`(重连快照)。详见 [04 → 房间响应公共字段](04-数据结构.md#房间创建--进入响应公共字段)。
|
||||
- 源码:发送 `09_Net.js:251`;接收 `09_Net.js:259`→`Desk.self_join_room`。
|
||||
- 备注:—
|
||||
|
||||
### `quick_enter_share_room` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:快速进入分享/星星场房间。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`type`(房间类型)、`roomtype`(可选)。
|
||||
- 响应字段(S→C):走 self_join_room 进房流程(无独立 `Net.quick_enter_share_room` 接收函数,结果通过 self_join_room/show_message 等回包)。
|
||||
- 源码:发送 `09_Net.js:652`。
|
||||
- 备注:⚠️ 无对应接收处理函数,进房结果依赖其它推送。
|
||||
|
||||
### `advanced_roomlist` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:拉取 VIP/高级房间列表。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`gameid`。
|
||||
- 响应字段(S→C):房间列表对象,整包写入 `GameData.snrRoomList` 并渲染(07_Desk.js:1176)。
|
||||
- 源码:发送 `09_Net.js:660`;接收 `09_Net.js:665`→`Desk.advanced_roomlist`。
|
||||
- 备注:—
|
||||
|
||||
### `advanced_createroom` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:创建 VIP/高级房间。
|
||||
- 请求字段(C→S):`agentid`、`gameid`、`playerid`、`tea`(茶水费)、`infinite`(0/1无限局)、`roomtype`、`videoConfig`(可选)、`rebateLimit`(可选)、`rebateType`(可选)。
|
||||
- 响应字段(S→C):`tea`、`rebateLimit` 及房间配置(整包写入 `GameData.snrRoomList`,并回拉 advanced_roomlist,07_Desk.js:1180)。
|
||||
- 源码:发送 `09_Net.js:669`;接收 `09_Net.js:674`→`Desk.advanced_createroom`。
|
||||
- 备注:—
|
||||
|
||||
### `get_share_room` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:获取分享/星星场房间列表(仅非大厅环境发送)。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`gameid`。
|
||||
- 响应字段(S→C):房间数组(`Desk.get_share_room` / `Game_Modify.getShareRoom` 处理)。
|
||||
- 源码:发送 `09_Net.js:641`(`ConstVal.isGameHall` 为 true 时不发);接收 `09_Net.js:648`。
|
||||
- 备注:—
|
||||
|
||||
### `getInfoByShortCode` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:按短码批量查询房间信息(VIP 房列表)。
|
||||
- 请求字段(C→S):`agentid`、`gameid`、`shortcodeList`(短码列表)。
|
||||
- 响应字段(S→C):`roomInfo`(短号房间信息) → `GameUI.setVipRoomListData`(07_Desk.js:1284)。
|
||||
- 源码:发送 `09_Net.js:754`;接收 `09_Net.js:758`→`Desk.getInfoByShortCode`。
|
||||
- 备注:—
|
||||
|
||||
### `switchRoomList` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:开关房间在列表中的可见/可进入状态。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`isClose`(0开/1关)。
|
||||
- 响应字段(S→C):`state`(0成功)、`isClose`;失败 `error`(07_Desk.js:1267)。
|
||||
- 源码:发送 `09_Net.js:744`;接收 `09_Net.js:748`→`Desk.switchRoomList`。
|
||||
- 备注:—
|
||||
|
||||
---
|
||||
|
||||
## 战绩 / 排行 / 财富
|
||||
|
||||
### `get_player_grade1` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:拉取战绩(类型1)。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`type`(可选)、`direction`(可选,翻页)、`gradeidx`(可选,分页索引)。
|
||||
- 响应字段(S→C):战绩数据(`gameCombat.get_player_grade1` 渲染)。
|
||||
- 源码:发送 `09_Net.js:359`;接收 `09_Net.js:364`。
|
||||
- 备注:—
|
||||
|
||||
### `get_player_grade2` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:拉取战绩(类型2)。
|
||||
- 请求字段(C→S):透传调用方 `_data`,构造点未集中定位;至少含 `agentid`/`playerid`/`gameid`。
|
||||
- 响应字段(S→C):战绩数据(`gameCombat.get_player_grade2` 渲染)。
|
||||
- 源码:发送 `09_Net.js:372`;接收 `09_Net.js:377`。
|
||||
- 备注:⚠️ 请求字段构造点未定位,待核对。
|
||||
|
||||
### `get_treasurelist` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:财富榜。
|
||||
- 请求字段(C→S):`agentid`、`gameid`。
|
||||
- 响应字段(S→C):`list`(排行数组) → `Desk.get_treasurelist`。
|
||||
- 源码:发送 `09_Net.js:686`;接收 `09_Net.js:691`。
|
||||
- 备注:—
|
||||
|
||||
### `getShortCodeRankList` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:短号场排行榜。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`shortcode`。
|
||||
- 响应字段(S→C):成功为排行数据(整包写入 `GameData.vipRank.data`);失败 `error:true` + `message`(07_Desk.js:1311)。
|
||||
- 源码:发送 `09_Net.js:770`;接收 `09_Net.js:774`→`Desk.getShortCodeRankList`。
|
||||
- 备注:—
|
||||
|
||||
### `getVipRankList` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:VIP 排行榜。
|
||||
- 请求字段(C→S):`agentid`、`limit`(条数)。
|
||||
- 响应字段(S→C):`list`(VIP排行数组) → `GameData.rankList`(07_Desk.js:1328)。
|
||||
- 源码:发送 `09_Net.js:788`;接收 `09_Net.js:792`→`Desk.getVipRankList`。
|
||||
- 备注:—
|
||||
|
||||
---
|
||||
|
||||
## 任务系统
|
||||
|
||||
### `get_player_task` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:获取任务列表。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`gameid`。
|
||||
- 响应字段(S→C):`tasks`(任务数组) → `Desk.get_player_task`。
|
||||
- 数值兼容:实际回包的 `tasks[].award` 可为数字字符串;原 UI 对 `finish/total` 做算术、对 `state` 用宽松数值比较。新前端在任务列表入库时将这四个字段的数字/十进制数字字符串统一为 number;空值、非数字、非有限值仍报错,state 必须为整数。
|
||||
- 源码:发送 `09_Net.js:409`;接收 `09_Net.js:414`。
|
||||
- 备注:—
|
||||
|
||||
### `player_finish_task` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:上报任务完成(如分享成功触发,06_Player.js:414)。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`taskid`。
|
||||
- 响应字段(S→C):`state`(int)——`state==1` 且当前 `taskstate==0` 时把 `C_Player.taskstate` 置 1(06_Player.js:476)。
|
||||
- 源码:发送 `09_Net.js:421`;接收 `09_Net.js:425`→`C_Player.player_finish_task`(`06_Player.js:474`)。
|
||||
- 备注:原文档"响应含 `taskstate`"无源码依据,已删除——接收函数仅读 `_msg.data.state`。
|
||||
|
||||
### `get_task_award` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:领取任务奖励。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`taskid`。`11_GameUI.js` 的任务领取点击只构造这三个字段,`Net.Send_get_task_award` / `_SendData` 原样发送,不补 `gameid`(2026-09-06 源码复核修正)。
|
||||
- 响应字段(S→C):`taskid`(对应任务 state 置 2=已领取)、`taskstate`(写入 `C_Player.taskstate`)(06_Player.js:482)。
|
||||
- 源码:发送 `09_Net.js:429`;接收 `09_Net.js:435`→`C_Player.get_task_award`。
|
||||
- 备注:—
|
||||
|
||||
### `refresh_task_state` · route=agent · C→S 请求(发送侧 `Send_can_award`)
|
||||
- 场景:刷新任务可领取状态。
|
||||
- 请求字段(C→S):透传 `_data`(构造点未集中定位,至少含 `agentid`/`playerid`)。
|
||||
- 响应字段(S→C):服务器推送 `can_award`(见下条)。
|
||||
- 源码:发送 `09_Net.js:440`(`Net.Send_can_award`)。
|
||||
- 备注:⚠️ 发送函数名为 `Send_can_award`,但**实际发出的 rpc 是 `"refresh_task_state"`**(硬编码字面量,**不在 RpcList**)。文档以实际 rpc 名为准。
|
||||
|
||||
### `can_award` · route=agent · S→C 推送
|
||||
- 场景:服务器通知有任务可领取。
|
||||
- 请求字段:无(纯推送)。
|
||||
- 推送字段(S→C):任务可领取标记(载荷字段待后端确认)。
|
||||
- 源码:接收 `09_Net.js:444`(`Net.can_award`)→ 调 `C_Player.can_award(_msg)`(`09_Net.js:445`)。
|
||||
- 备注:🐛 `06_Player.js` **未定义** `Player.prototype.can_award`(全文无此方法)。推送一旦到达,`C_Player.can_award` 为 undefined,调用即抛 TypeError。当前为 bug,不可当正常协议使用。
|
||||
|
||||
---
|
||||
|
||||
## 支付 / 充值 / 资产
|
||||
|
||||
### `get_paylist` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:拉取支付项列表。
|
||||
- 请求字段(C→S):`agentid`。
|
||||
- 响应字段(S→C):`paylist`(支付项数组) → `GameData.payList`,并打开支付界面(09_Net.js:567)。
|
||||
- 源码:发送 `09_Net.js:562`;接收 `09_Net.js:567`。
|
||||
- 备注:—
|
||||
|
||||
### `pay_succ` · route=agent · C→S 请求(经 HTTP)
|
||||
- 场景:支付成功后通知服务器入账。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`channelid`、`productid`、`payid`、`amount`、`money`、`paytype`(05_Func.js:2240-2254 构造)。
|
||||
- 响应字段(S→C):无显式 WS 回包;资产变化通过 `update_bean`/`update_roomcard` 推送(充房卡场景前端还会本地 `UpdateRoomcard`,05_Func.js:2263)。
|
||||
- 源码:WS 发送函数 `Net.Send_pay_succ`(`09_Net.js:573`) **被注释**(05_Func.js:2249、:3199);实际改用 `Func.AjaxHttp` 以同样的 `{app,route:agent,rpc:pay_succ,data}` 信封走 **HTTP** 提交(05_Func.js:2250-2255、:3200-3205)。
|
||||
- 备注:⚠️ WebSocket 通道当前不发;该包以 HTTP POST 形式上行,rpc 名仍为 `pay_succ`,待后端确认接收端一致。
|
||||
|
||||
### `topup_card` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:充值卡兑换。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`cardno`(卡号)。
|
||||
- 响应字段(S→C):`Desk.topup_card` 为空实现(07_Desk.js:1365),资产变化经 update_bean/update_roomcard 推送。
|
||||
- 源码:发送 `09_Net.js:863`;接收 `09_Net.js:867`。
|
||||
- 备注:—
|
||||
|
||||
### `giveCoin` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:仓库面板向他人转账豆豆/金币。
|
||||
- 请求字段(C→S):`agentid`、`playerid`(转出)、`toPlayerid`(转入目标ID)、`gameid`、`count`(数量)、`password`(仓库密码)(11_GameUI.js:2132-2139)。
|
||||
- 响应字段(S→C):`state`(0成功)、`star2`(转出后**仓库星星**数 → `setWareHouseStarCOunt`,**非豆豆**);失败 `showerror`/`error`(07_Desk.js:1381)。
|
||||
- 源码:发送 `09_Net.js:876`;接收 `09_Net.js:879`→`Desk.giveCoin`。
|
||||
- 备注:响应 `star2` 为仓库星星数,注意区别于豆豆余额。
|
||||
|
||||
---
|
||||
|
||||
## 仓库 / 星星 / 魅力
|
||||
|
||||
### `set_bankpwd` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:设置仓库密码。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`unionid`、`password`。
|
||||
- 响应字段(S→C):`state`(0成功)、`password`;失败 `showerror`/`error`(07_Desk.js:1228)。
|
||||
- 源码:发送 `09_Net.js:697`;接收 `09_Net.js:701`→`Desk.set_bankpwd`。
|
||||
- 备注:—
|
||||
|
||||
### `change_star` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:仓库存/取(豆豆 ↔ 仓库星星)。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`mode`(0存入/1取出,源码 `safeInputType-1`)、`password`(仓库密码,字符串)、`count`(数量)(11_GameUI.js:2065-2069 调用点齐全携带此 5 字段)。
|
||||
- 响应字段(S→C):`state`(0成功)、`star1`(更新后豆豆余额 → `update_bean2`)、`star2`(更新后仓库星星数 → `setWareHouseStarCOunt`)、`msg`(可选提示)、`count`(可选,回填安全输入);失败 `showerror`/`error`(07_Desk.js:1238)。
|
||||
- 源码:发送 `09_Net.js:707`;接收 `09_Net.js:711`→`Desk.change_star`。
|
||||
- 备注:原审计疑虑"`mode`/`password` 未见"——经核对仓库存/取调用点(11_GameUI.js:2068-2069)**确含** `mode` 与 `password`,文档正确,疑虑解除。审计提到的"agentid/playerid/toPlayerid/gameid/count"实为相邻的 `giveCoin` 转账包(11_GameUI.js:2132-2139),并非本 rpc。
|
||||
|
||||
### `update_charm` · route=agent · S→C 推送
|
||||
- 场景:座位魅力值更新。
|
||||
- 请求字段:`Net.Send_update_charm`(`09_Net.js:727`) 已定义但**无任何调用点**;实际只作服务器→客户端推送。
|
||||
- 推送字段(S→C):`seatlist`:[ {`seat`(int), `charm`(number)} ](07_Desk.js:1260 遍历 setCharm)。
|
||||
- 源码:接收 `09_Net.js:731`→`Desk.update_charm`(`07_Desk.js:1260`)。
|
||||
- 备注:—
|
||||
|
||||
### `setAllCharm` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:批量设置总魅力。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`value`(11_GameUI.js:1269-1275)。
|
||||
- 响应字段(S→C):无显式业务字段,`Desk.setAllCharm` 仅本地存储并提示成功(07_Desk.js:1321)。
|
||||
- 源码:发送 `09_Net.js:779`;接收 `09_Net.js:783`→`Desk.setAllCharm`。
|
||||
- 备注:—
|
||||
|
||||
---
|
||||
|
||||
## 邀请码 / 绑定
|
||||
|
||||
### `binding_invitecode` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:绑定邀请码。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`invitecode`(11_GameUI.js:1367-1369)。
|
||||
- 响应字段(S→C):`state`(整数或整数型字符串,`0`/`"0"` 成功时写入 `invitecode`)、`invitecode`、`error`(提示文案)。原工程 `06_Player.js:270` 使用 `state == 0`;2026-09-06 联调确认服务器也返回字符串状态,前端在绑定响应入口归一化,不能用仅接受 number 的校验直接拒绝。
|
||||
- 源码:发送 `09_Net.js:587`;接收 `09_Net.js:592`→`C_Player.binding_invitecode`。
|
||||
- 备注:—
|
||||
|
||||
### `get_player_invitecode` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:获取自己的邀请码(打开绑定界面)。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`unionid`、`openid`(11_GameUI.js:1378-1382)。
|
||||
- 响应字段(S→C):`invitecode`(string) → `C_Player.setInvitecod` 并打开绑定界面(09_Net.js:607)。
|
||||
- 源码:发送 `09_Net.js:603`;接收 `09_Net.js:607`→`Net.get_player_invitecode`。
|
||||
- 备注:修正原文档——请求字段为 `agentid/playerid/unionid/openid`,**无 `gameid`**,新增 `unionid`/`openid`(11_GameUI.js:1378)。
|
||||
|
||||
---
|
||||
|
||||
## VIP 管理 / 黑白名单
|
||||
|
||||
### `optBanList` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:黑名单查看/添加/移除(多入口)。
|
||||
- 请求字段(C→S):随入口不同,公共字段 `agentid`、`playerid`、`type`:
|
||||
- `type=1` 查看黑名单列表(agentid/playerid/type,11_GameUI.js:2401-2405)
|
||||
- `type=3` 按 ID 添加(agentid/playerid/optId/type,11_GameUI.js:2376-2381)
|
||||
- `type=4` 按 ID 移除(agentid/playerid/optId/type,11_GameUI.js:2389-2394 及列表项删除 2453-2458)
|
||||
- `type=6` 一键全部添加(agentid/playerid/type,11_GameUI.js:1262-1267)
|
||||
- 另有带 `breakRoom`(0/1) 与 `gameid` 的 `type=3` 添加入口(agentid/gameid/playerid/optId/breakRoom/type,11_GameUI.js:2417-2428;breakRoom 由 `GameData.blackList.breakRoom` 决定,开关在 case 3260)
|
||||
- 服务器响应中亦见 `type=5`(另一种列表返回,07_Desk.js:1299)
|
||||
- 响应字段(S→C):`type`(1/3/4/5/6)、`banList`(黑名单数组)、`message`(可选提示)(07_Desk.js:1289)。
|
||||
- 源码:发送 `09_Net.js:762`;接收 `09_Net.js:766`→`Desk.optBanList`。
|
||||
- 备注:—
|
||||
|
||||
### `getPlayerWhiteList` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:获取白名单。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`gameid`。
|
||||
- 响应字段(S→C):`whiteList`(数组) → `GameData.whiteList.data`(07_Desk.js:1412)。
|
||||
- 源码:发送 `09_Net.js:882`;接收 `09_Net.js:886`→`Desk.getPlayerWhiteList`。
|
||||
- 备注:—
|
||||
|
||||
### `optWhiteList` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:白名单添加/修改/删除。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`shortcode`、`mode`、`userid`(目标ID):
|
||||
- `mode=1` 添加/修改,并带 `value`(设置的魅力值)
|
||||
- `mode=2` 删除(仅 `userid`)
|
||||
- 响应字段(S→C):`whiteList`(更新后数组)、`mode`(可选)、`message`(可选提示)(07_Desk.js:1398)。
|
||||
- 源码:发送 `09_Net.js:890`;接收 `09_Net.js:894`→`Desk.optWhiteList`。
|
||||
- 备注:—
|
||||
|
||||
### `setVipForbidSelect` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:VIP 房禁止玩家选桌开关。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`forbidSelect`(1开/0关,11_GameUI.js:2631-2639)。
|
||||
- 响应字段(S→C):`state`(0成功)、`forbidSelect`(1开/0关);失败 `error`(07_Desk.js:1348)。
|
||||
- 源码:发送 `09_Net.js:855`;接收 `09_Net.js:859`→`Desk.setVipForbidSelect`。
|
||||
- 备注:—
|
||||
|
||||
---
|
||||
|
||||
## 其它 agent 协议
|
||||
|
||||
### `submit_opinion` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:提交反馈/意见。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`content`。
|
||||
- 响应字段(S→C):`state`(0成功)(07_Desk.js:1100)。
|
||||
- 源码:发送 `09_Net.js:547`;接收 `09_Net.js:551`→`Desk.submit_opinion`。
|
||||
- 备注:—
|
||||
|
||||
### `submit_location` · route=agent · C→S 请求 + S→C 响应
|
||||
- 场景:提交定位信息(仅非大厅环境发送)。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`info`(定位对象)。
|
||||
- 响应字段(S→C):经 `Game.submit_location` 处理(09_Net.js:583)。
|
||||
- 源码:发送 `09_Net.js:577`(`ConstVal.isGameHall` 为 true 时不发);接收 `09_Net.js:583`。
|
||||
- 备注:—
|
||||
|
||||
### `submit_phoneinfo` · route=agent · C→S 请求(无响应处理)
|
||||
- 场景:提交手机/通讯录信息(仅非大厅环境发送)。
|
||||
- 请求字段(C→S):`agentid`、`playerid`、`info`{ `phoneInfo`(手机信息), `addrBook`(通讯录) }。
|
||||
- 响应字段(S→C):`Net.submit_phoneinfo` 接收函数为空实现(09_Net.js:722)。
|
||||
- 源码:发送 `09_Net.js:717`;接收 `09_Net.js:722`。
|
||||
- 备注:—
|
||||
|
||||
### `submit_error` / `submit_log` · route=agent · C→S 上行(硬编码包)
|
||||
- 场景:上报前端异常/日志。异常捕获时(12_Logic.js:270)、获取 HTML 失败(12_Logic.js:1990/2009/2014)、收到踢下线包(07_Desk.js:963)等处调用。
|
||||
- 请求字段(C→S):`packet`(出错数据包字符串)、`msg`(错误/堆栈信息)、`playerid`、`agentid`、`gameid`(09_Net.js:66-72)。
|
||||
- 响应字段(S→C):服务器若回 rpc `"submit_error"`,前端在分发处直接 `return` 忽略(12_Logic.js:235)。
|
||||
- 源码:发送 `Net.submit_error`(`09_Net.js:55`) / `Net.submit_log`(`09_Net.js:81`)。
|
||||
- 备注:均为**硬编码** `route:"agent", rpc:"submit_error"`,不经 `Send_*`/RpcList。注意 `Net.submit_log`(`09_Net.js:81`) 实际发出的 rpc 同样是 `"submit_error"`(与 submit_error 同包结构)。两者带去重:`submit_error` 对相同 `msg` 只发一次(09_Net.js:57-61),`submit_log` 不去重。
|
||||
|
||||
### `kick_server` · route=agent · C→S 请求 + S→C 推送
|
||||
- 场景:管理端踢出玩家 / 被踢下线弹窗。
|
||||
- 请求字段(C→S):`agentid` 等(构造点未集中定位)。
|
||||
- 推送字段(S→C):`msg`(踢出提示) → `GameUI.OpenKick`(07_Desk.js:1108)。
|
||||
- 源码:发送 `09_Net.js:555`;接收 `09_Net.js:558`→`Desk.kick_server`。
|
||||
- 备注:⚠️ 请求字段待补。
|
||||
|
||||
### `broadcast` · route=agent · C→S 请求 + S→C 推送
|
||||
- 场景:广播消息/滚动公告(主要为服务器推送)。
|
||||
- 请求字段(C→S):`agentid` 等(`Send_broadcast` 存在,09_Net.js:530)。
|
||||
- 推送字段(S→C):`msgtype`(0消息框/1滚动公告,可选,缺省 0)、`msgcontent`(内容)(07_Desk.js:1112)。
|
||||
- 源码:发送 `09_Net.js:530`;接收 `09_Net.js:534`→`Desk.broadcast`。
|
||||
- 备注:—
|
||||
|
||||
### `connect_agentserver` · route=agent · 双向(切服)
|
||||
- 场景:切换到大厅服务器。
|
||||
- 请求字段(C→S):`Send_connect_agentserver`(`09_Net.js:515`) 透传 `_data`(切服时使用)。
|
||||
- 推送字段(S→C):`agentserver`(新大厅服地址)、`opt`(切换原因,如 `other_break_room`/`free_room`)(09_Net.js:518)。
|
||||
- 源码:发送 `09_Net.js:515`;接收 `09_Net.js:518`→`Net.connect_agentserver`(关闭当前连接并重连新地址)。
|
||||
- 备注:—
|
||||
|
||||
### `playerBehavior` · route=agent(实际走 HTTP GET)
|
||||
- 场景:玩家行为埋点。
|
||||
- 请求字段:原 WS 路径(agentid/gameid/playerid/tag)**被注释**(09_Net.js:799-806),实际改走独立 HTTP GET 上报 `http://test3.1888day.com/api/gamedo/gamedo?agentid=&gameid=&playerid=&tag=`(09_Net.js:807-815)。
|
||||
- 响应字段(S→C):HTTP 回调 `playerBehavior_Succ`/`_Fail`(09_Net.js:818/822);WS 接收函数 `Net.playerBehavior`(`09_Net.js:826`) 当前无触发。
|
||||
- 源码:发送 `09_Net.js:797`(`Send_playerBehavior`)。
|
||||
- 备注:**非 WebSocket 协议**,RpcList 中虽有定义,实际不走 agent 路由。
|
||||
|
||||
---
|
||||
|
||||
## 仅接收的 agent 推送(无对应主动请求)
|
||||
|
||||
| rpc | 推送 data 字段 | 说明 | 接收源码 |
|
||||
|-----|---------------|------|---------|
|
||||
| `update_roomcard` | `roomcard`、`text`(可选) | 房卡变化(仅 `change` 未定义时更新,09_Net.js:383→06_Player.js:141) | 09_Net.js:383 |
|
||||
| `update_bean` | `bean`、`change`(可选)、`seat`(可选)、`type`(可选)、`text` | 豆豆变化(09_Net.js:598→Desk.update_bean,07_Desk.js:1201) | 09_Net.js:598 |
|
||||
| `can_award` | 任务可领取标记 | 🐛 接收即抛异常,见上文 任务系统 章 | 09_Net.js:444 |
|
||||
| `kick_offline` | `fromOther`(可选)、`gameid`(可选) | 被踢下线,弹 OpenKick;同时本地 `Net.submit_error` 上报"收到踢下线包"(07_Desk.js:945-963) | 09_Net.js:448 |
|
||||
| `show_message` | `msg`、`time` | 通用消息提示 → `GameUI.OpenTips`(07_Desk.js:1173) | 09_Net.js:656 |
|
||||
@@ -0,0 +1,306 @@
|
||||
# 03 · 平台层协议 — room 路由(房间服务器)
|
||||
|
||||
> 信封:`{ app:"youle", route:"room", rpc:"<下列名称>", data:{...} }`
|
||||
> 本篇覆盖 **route=room 的全部平台层数据包**(房间生命周期 / 解散投票 / 开局 / 房内社交 / 服务器切换)。不含子游戏对局内协议(见 05 章)。
|
||||
> 房间内操作通用请求字段(除特别说明外,C→S 请求恒含这四项):`agentid`、`gameid`、`playerid`、`roomcode`。
|
||||
> 「响应/推送」均指服务器返回内层 `data` 字段。座位号 `seat` 通常为 0 起整数;`Logic.ChangeToStatus(C_Player.seat, seat)` 把绝对座位转为以自己为基准的相对视角。
|
||||
>
|
||||
> **图例**:🐛 源码 bug;⚠️ 待后端确认;— 无特别说明。
|
||||
>
|
||||
> **条目格式**:
|
||||
> `rpc名` · route=room · <方向> — 场景;请求字段(C→S);响应/推送字段(S→C);源码 发送/接收;备注。
|
||||
>
|
||||
> **路由说明(交叉引用)**:发送(C→S)走 `RouteList.room`,但少数推送虽在 room 场景内消费,其上行请求实际走 `agent` 路由——下文逐条标注;这类项归档于 [02 · agent 路由](02-协议-agent路由.md),此处仅记录其在房内的接收语义。
|
||||
|
||||
---
|
||||
|
||||
## 房间生命周期
|
||||
|
||||
### self_break_room · route=room · C→S 请求 + S→C 响应
|
||||
- 场景:房主在**未开局**前主动解散房间。
|
||||
- 请求字段(C→S):仅通用四字段。
|
||||
- 响应字段(S→C):`roomcode`(可选,用于从本地"我的房间"列表移除)。处理:清空牌桌、回大厅。
|
||||
- 源码:发送 `09_Net.js:266`(`Send_self_break_room`)/接收 `09_Net.js:271` → `07_Desk.js:707`(`Desk.self_break_room`)。
|
||||
- 备注:—
|
||||
|
||||
### other_break_room · route=room · S→C 推送
|
||||
- 场景:他人(房主)解散房间,推送给房内其余玩家。
|
||||
- 请求字段(C→S):无(纯推送)。
|
||||
- 响应字段(S→C):无业务字段;触发 `Func.exitRoom()`、清桌、回大厅、提示"房主已解散房间!"。
|
||||
- 源码:接收 `09_Net.js:277` → `07_Desk.js:721`(`Desk.other_break_room`)。
|
||||
- 备注:常伴随 **connect_agentserver** 切服包(`data.opt == other_break_room`,见下文)。
|
||||
|
||||
### self_exit_room · route=room · C→S 请求 + S→C 响应
|
||||
- 场景:自己在**未开局**前退出房间。
|
||||
- 请求字段(C→S):仅通用四字段(发送处见 `08_Utl_Output.js:993`)。
|
||||
- 响应字段(S→C):`isowner`(可选,==1 且非无限局时把房间加回本地列表), `seat`(可选,传给 `Game_Modify.myExitRoom`), `roomcode`(可选)。
|
||||
- 源码:发送 `09_Net.js:289`(`Send_self_exit_room`)/接收 `09_Net.js:294` → `07_Desk.js:753`(`Desk.self_exit_room`)。
|
||||
- 备注:—
|
||||
|
||||
### other_exit_room · route=room · S→C 推送
|
||||
- 场景:其他玩家(未开局)退出房间。
|
||||
- 请求字段(C→S):无(纯推送)。
|
||||
- 响应字段(S→C):`seat`(离开者座位)。处理:清该座位、`playercnt--`;若 `seat==0` 且非无限局,提示房主已离开。
|
||||
- 源码:接收 `09_Net.js:301` → `07_Desk.js:785`(`Desk.other_exit_room`)。
|
||||
- 备注:—
|
||||
|
||||
### player_prepare · route=room · C→S 请求 + S→C 推送
|
||||
- 场景:玩家点击准备(`needprepare==1` 的房间)。
|
||||
- 请求字段(C→S):仅通用四字段。
|
||||
- 响应/推送字段(S→C):`seat`(准备者座位), `deskwar`(可选;为真表示满足开战条件 → `HideStartScene` + `Game_Modify.StartWar`)。推送给房内所有人。
|
||||
- 源码:发送 `09_Net.js:629`(`Send_player_prepare`)/接收 `09_Net.js:632` → `07_Desk.js:1126`(`Desk.player_prepare`)。
|
||||
- 备注:—
|
||||
|
||||
### change_room · route=room · C→S 请求 → S→C 响应(change_seat) · 跨座换桌
|
||||
- 场景:玩家请求换座 / 换桌。
|
||||
- 请求字段(C→S):仅通用四字段(**不带目标座位**,由服务器决定换到哪个空位)。发送处 `11_GameUI.js:1945`、`08_Utl_Output.js:1006`。
|
||||
- 响应字段(S→C):rpc 名改为 **`change_seat`**,`data`:`seat1`, `seat2`(两个互换的座位号)。处理:交换两座 Desk 信息,若自己在其中则 `C_Player.SetSeat` 更新。
|
||||
- 源码:发送 `09_Net.js:678`(`Send_change_room`)/接收 `09_Net.js:682`(`Net.change_seat`) → `07_Desk.js:178`(`Desk.change_seat`)。
|
||||
- 备注:上行 rpc=`change_room`,下行 rpc=`change_seat`,二者成对。
|
||||
|
||||
### share_room · route=room · C→S 请求 + S→C 响应
|
||||
- 场景:把房间分享到世界房列表。
|
||||
- 请求字段(C→S):通用四字段;可选 `roomlist`、`roomtype`、`shareType`(高级房分享时携带,见 `11_GameUI.js:1734`/`11_GameUI.js:1891`)。
|
||||
- 响应字段(S→C):无业务字段;提示"已成功分享至平台!"。
|
||||
- 源码:发送 `09_Net.js:635`(`Send_share_room`)/接收 `09_Net.js:638` → `07_Desk.js:1152`(`Desk.share_room`)。
|
||||
- 备注:—
|
||||
|
||||
---
|
||||
|
||||
## 进房推送(他人视角)
|
||||
|
||||
### other_join_room · route=room · S→C 推送
|
||||
- 场景:其他玩家加入当前房间。
|
||||
- 请求字段(C→S):无(纯推送;自己进房用 `self_join_room`,走 agent 路由,见 02 章)。
|
||||
- 响应/推送字段(S→C):`seat`(新玩家座位) + 该玩家完整对象(直接传给 `Player.SetDeskInfo`,见 [04 · Player 座位对象](04-数据结构.md#player-座位对象)):
|
||||
`playerid`, `nickname`, `avatar`, `sex`, `ip`, `onstate`, `bean`, `charm`, `sign` 等;另含 `needprepare`(可选), `deskwar`(可选;为真直接走 `Desk.makewar` 开战)。
|
||||
- 源码:接收 `09_Net.js:281` → `07_Desk.js:729`(`Desk.other_join_room`);字段写入见 `06_Player.js:263`(`Player.SetDeskInfo`)。
|
||||
- 备注:⚠️ `charm`/`sign` 是否每次必含、其余字段是否完整以 `Player.SetDeskInfo` 实际读取为准,详见 [04 章](04-数据结构.md#player-座位对象)。
|
||||
|
||||
### other_offline · route=room · S→C 推送
|
||||
- 场景:其他玩家离线。
|
||||
- 请求字段(C→S):无(纯推送)。
|
||||
- 响应字段(S→C):`seat`。处理:该座 `onstate=1`,刷新 UI。
|
||||
- 源码:接收 `09_Net.js:390` → `07_Desk.js:887`(`Desk.other_offline`)。
|
||||
- 备注:—
|
||||
|
||||
### other_online · route=room · S→C 推送
|
||||
- 场景:其他玩家重新上线。
|
||||
- 请求字段(C→S):无(纯推送)。
|
||||
- 响应字段(S→C):`seat`, `ip`。处理:该座 `onstate=0`、更新 ip。
|
||||
- 源码:接收 `09_Net.js:395` → `07_Desk.js:893`(`Desk.other_online`)。
|
||||
- 备注:—
|
||||
|
||||
---
|
||||
|
||||
## 解散投票(开局后)
|
||||
|
||||
> 流程:某玩家 apply → 全员收到 `other_apply_free_room`(带各座状态与倒计时)→
|
||||
> 各玩家 agree/refuse → 最终 `free_room` 广播结果。
|
||||
|
||||
### self_apply_free_room · route=room · C→S 请求 + S→C 响应
|
||||
- 场景:自己申请解散房间。
|
||||
- 请求字段(C→S):仅通用四字段。
|
||||
- 响应字段(S→C):`agreefree` { `state`:[各座同意状态数组], `countdown`:倒计时 }。
|
||||
- 源码:发送 `09_Net.js:308`/接收 `09_Net.js:313` → `07_Desk.js:804`(`Desk.self_apply_free_room`)。
|
||||
- 备注:—
|
||||
|
||||
### other_apply_free_room · route=room · S→C 推送
|
||||
- 场景:他人申请解散。
|
||||
- 请求字段(C→S):无(纯推送)。
|
||||
- 响应字段(S→C):`seat`(申请者), `agreefree` { `state`[], `countdown` }。
|
||||
- 源码:接收 `09_Net.js:319` → `07_Desk.js:817`(`Desk.other_apply_free_room`)。
|
||||
- 备注:—
|
||||
|
||||
### self_agree_free_room · route=room · C→S 请求 + S→C 响应
|
||||
- 场景:自己同意解散。
|
||||
- 请求字段(C→S):仅通用四字段。
|
||||
- 响应字段(S→C):无业务字段;本地把自己加入同意列表、刷新投票 UI。
|
||||
- 源码:发送 `09_Net.js:323`/接收 `09_Net.js:328` → `07_Desk.js:829`(`Desk.self_agree_free_room`)。
|
||||
- 备注:—
|
||||
|
||||
### other_agree_free_room · route=room · S→C 推送
|
||||
- 场景:他人同意解散。
|
||||
- 请求字段(C→S):无(纯推送)。
|
||||
- 响应字段(S→C):`seat`(同意者)。
|
||||
- 源码:接收 `09_Net.js:333` → `07_Desk.js:836`(`Desk.other_agree_free_room`)。
|
||||
- 备注:—
|
||||
|
||||
### self_refuse_free_room · route=room · C→S 请求 + S→C 响应
|
||||
- 场景:自己拒绝解散。
|
||||
- 请求字段(C→S):仅通用四字段。
|
||||
- 响应字段(S→C):无业务字段;清空同意列表、投票结果置不通过、弹"已拒绝"结果。
|
||||
- 源码:发送 `09_Net.js:338`/接收 `09_Net.js:343` → `07_Desk.js:842`(`Desk.self_refuse_free_room`)。
|
||||
- 备注:—
|
||||
|
||||
### other_refuse_free_room · route=room · S→C 推送
|
||||
- 场景:他人拒绝解散(任一人拒绝即否决本轮)。
|
||||
- 请求字段(C→S):无(纯推送)。
|
||||
- 响应字段(S→C):`seat`(拒绝者)。
|
||||
- 源码:接收 `09_Net.js:348` → `07_Desk.js:853`(`Desk.other_refuse_free_room`)。
|
||||
- 备注:—
|
||||
|
||||
### free_room · route=room · S→C 推送
|
||||
- 场景:解散投票最终结果。
|
||||
- 请求字段(C→S):无(纯推送)。
|
||||
- 响应字段(S→C):
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `freeNow` | true=立即解散;false=投票通过待确认 |
|
||||
| `deskfree` | 解散结算信息对象(可选) |
|
||||
| `roomcard` | 解散后房卡数(可选,写回 `C_Player.setRoomcard`) |
|
||||
| `tips` | 提示文本(`freeNow==true` 时) |
|
||||
| `time` | 提示显示时长(`freeNow==true` 时) |
|
||||
| `seats` | 投票通过的座位数组(`freeNow==false` 时传给 `OpenApplyResult`) |
|
||||
|
||||
- 源码:接收 `09_Net.js:353` → `07_Desk.js:864`(`Desk.free_room`)。
|
||||
- 备注:可伴随 **connect_agentserver** 切服包(`data.opt == free_room`,见下文)。
|
||||
|
||||
### beanroom_surrender · route=room · C→S 请求 + S→C 响应
|
||||
- 场景:豆豆房(金币房)投降。
|
||||
- 请求字段(C→S):通用四字段 + `count`(投降数量 = `GameData.surrendCount`,发送处 `11_GameUI.js:1245`)。
|
||||
- 响应字段(S→C):`state`(0=成功 → `Game_Modify.onSurrender(_msg)`); 失败时 `showerror`(==1 则弹) / `error`(错误文本)。
|
||||
- 源码:发送 `09_Net.js:613`(`Send_beanroom_surrender`)/接收 `09_Net.js:617`(`Net.beanroom_surrender`,逻辑直接在 Net 内处理)。
|
||||
- 备注:⚠️ 未见成对的 `other_xxx` 推送,是否向房内其他玩家广播他人投降待后端确认。
|
||||
|
||||
---
|
||||
|
||||
## 开局
|
||||
|
||||
### self_makewar · route=room · C→S 请求 → S→C 响应(self_makewar)
|
||||
- 场景:房主主动开局。
|
||||
- 请求字段(C→S):仅通用四字段。
|
||||
- 响应字段(S→C):rpc=`self_makewar`,无业务字段;触发 `Desk.self_makewar` → `Game_Modify.StartWar(_msg)`。
|
||||
- 源码:发送 `09_Net.js:470`(`Send_self_makewar`)/接收 `09_Net.js:475` → `07_Desk.js:987`(`Desk.self_makewar`)。
|
||||
- 备注:—
|
||||
|
||||
### other_makewar · route=room · S→C 推送
|
||||
- 场景:开局广播(房主开局或满员自动开局)给房内其他玩家。
|
||||
- 请求字段(C→S):无(纯推送)。
|
||||
- 响应字段(S→C):开局信息对象 → `Desk.makewar` → `Game_Modify.StartWar(msg)`。
|
||||
- 源码:接收 `09_Net.js:480`(`Net.other_makewar`) → `07_Desk.js:1020`(`Desk.makewar`)。
|
||||
- 备注:实际发牌等对局数据在此之后通过**子游戏内协议**下发(见 05 章)。
|
||||
|
||||
---
|
||||
|
||||
## 房间内社交
|
||||
|
||||
### send_text · route=room · C→S 请求 + S→C 推送 · 文字聊天
|
||||
- 场景:文字聊天 / 全服公告 / 预定义常用语。
|
||||
- 请求字段(C→S):通用四字段 + `text`(内容;点常用语时为 `Game_Config.Info.TextContent[spid-206]` 文本) + `type`(0=普通 / 1=全服公告;按是否勾选公告设 0/1) + `info`(可选;**有 info 时 `type` 改为 2**)。发送处 `11_GameUI.js:1170`(输入框)、`11_GameUI.js:2717`(常用语)。
|
||||
- 推送字段(S→C):`type`(0=普通 / 1=全服公告 / 2=机器人 / 3=预定义文字), `text`(内容;**type==3 时为 `Game_Config.Info.TextContent` 的 1 基索引**,客户端按 `(idx-1)%len` 取文本), `seat`(发送者座位,type≠1 时使用), `info`(type==2 时附加)。
|
||||
- 源码:发送 `09_Net.js:400`(`Send_send_text`)/接收 `09_Net.js:404` → `07_Desk.js:900`(`Desk.send_text`)。
|
||||
- 备注:百人场(`vipInfinite`)只处理 type==1 公告分支。
|
||||
|
||||
### receive_chat · route=room · S→C 推送(⚠️)
|
||||
- 场景:聊天推送的另一可能 rpc 名(与 `send_text` 成对)。
|
||||
- 请求字段(C→S):无。
|
||||
- 推送字段(S→C):未知(疑似同 `send_text` 推送结构)。
|
||||
- 源码:常量 `02_Const.js:38`(`RpcList.receive_chat`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**。
|
||||
- 备注:⚠️ `02_Const.js` 已定义但客户端无处理函数;服务器聊天推送究竟用 `send_text` 还是 `receive_chat` 待后端确认。
|
||||
|
||||
### send_voice · route=room · C→S 请求 + S→C 推送 · 语音
|
||||
- 场景:发送语音消息。
|
||||
- 请求字段(C→S):通用四字段 + `voiceurl`(已上传音频地址) + `time`(时长) + `info`(可选) + `type`(可选,有 info 时为 2)。发送处 `05_Func.js:1714`、`05_Func.js:2984`。
|
||||
> 语音需先上传得到 `voiceurl` 再随包发送,客户端不直接传音频二进制。
|
||||
- 推送字段(S→C):`type`(0=普通 / 2=机器人), `seat`(发送座位), `voiceurl`, `time`, `info`(type==2 时)。
|
||||
- 源码:发送 `09_Net.js:492`(`Send_send_voice`)/接收 `09_Net.js:495` → `07_Desk.js:1073`(`Desk.send_voice`)。
|
||||
- 备注:—
|
||||
|
||||
### play_voice · route=room · S→C 推送(⚠️)
|
||||
- 场景:与 `send_voice` 成对,疑为语音**播放**推送。
|
||||
- 请求字段(C→S):无(`send_voice` 上行,`play_voice` 疑为下行播放推送)。
|
||||
- 推送字段(S→C):未知。
|
||||
- 源码:常量 `02_Const.js:36` 与 `02_Const.js:54`(重复定义 `RpcList.play_voice`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**(注:`GameUI.play_voice` 是本地播放方法,非网络处理器)。
|
||||
- 备注:⚠️ `02_Const.js` 已定义但客户端无网络处理函数;服务器是否下发 `play_voice` 待后端确认。
|
||||
|
||||
### send_gift · route=room · C→S 请求 + S→C 推送 · 互动/送礼
|
||||
- 场景:向指定座位送互动礼物。
|
||||
- 请求字段(C→S):通用四字段 + `giftid`(=`spid_up - 255`,按钮精灵号算出,发送处 `11_GameUI.js:2725`) + `receiveseat`(=`GameData.InteractPlayer`) + `info`(可选) + `type`(有 info 时为 2)。
|
||||
- 推送字段(S→C):`type`(0=普通 / 2=机器人), `giftid`(客户端做 `(giftid-1)%4+1` 归一为 1~4 动画), `sendseat`(发送座位), `receiveseat`(接收座位), `info`(type==2 时)。
|
||||
- 源码:发送 `09_Net.js:484`(`Send_send_gift`)/接收 `09_Net.js:487` → `07_Desk.js:1060`(`Desk.send_gift`)。
|
||||
- 备注:—
|
||||
|
||||
### other_send_gift · route=room · S→C 推送(⚠️)
|
||||
- 场景:与 `send_gift` 成对,疑为他人送礼推送。
|
||||
- 请求字段(C→S):无。
|
||||
- 推送字段(S→C):未知(疑似同 `send_gift` 推送结构)。
|
||||
- 源码:常量 `02_Const.js:52`(`RpcList.other_send_gift`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**。
|
||||
- 备注:⚠️ `02_Const.js` 已定义但客户端无处理函数;服务器送礼推送用 `send_gift` 还是 `other_send_gift` 待后端确认。
|
||||
|
||||
### send_phiz · route=room · C→S 请求 + S→C 推送 · 表情
|
||||
- 场景:发送表情动画。
|
||||
- 请求字段(C→S):通用四字段 + `text`(=`up_id`,表情按钮序号 1~`ConstVal.Emotion.count`,发送处 `11_GameUI.js:815`) + `info`(可选) + `type`(有 info 时为 2)。
|
||||
- 推送字段(S→C):`type`(0=普通 / 2=机器人), `text`(表情 ID,客户端按 `(text-1)%ConstVal.Emotion.src_list.length+1` 归一), `seat`(发送座位), `info`(type==2 时)。
|
||||
- 源码:发送 `09_Net.js:539`(`Send_send_phiz`)/接收 `09_Net.js:543` → `07_Desk.js:1086`(`Desk.send_phiz`)。
|
||||
- 备注:—
|
||||
|
||||
### call_phone · route=room · C→S 请求 + S→C 推送 · 拨打电话
|
||||
- 场景:拨打/接听电话,置玩家 `onstate=2`(通话中)。
|
||||
- 请求字段(C→S):仅通用四字段(发送处 `06_Player.js:380` / `:388` / `:396`,对应接起/电话进来/去电三种触发)。
|
||||
- 推送字段(S→C):rpc=`call_phone`,`seat`(拨打者座位 → 该玩家 `onstate=2`)。
|
||||
- 源码:发送 `09_Net.js:452`(`Send_call_phone`)/接收 `09_Net.js:456` → `07_Desk.js:967`(`Desk.call_phone`)。
|
||||
- 备注:—
|
||||
|
||||
### other_callphone · route=room · S→C 推送(⚠️)
|
||||
- 场景:与 `call_phone` 成对,疑为他人拨打电话推送。
|
||||
- 请求字段(C→S):无。
|
||||
- 推送字段(S→C):未知(疑似含 `seat`)。
|
||||
- 源码:常量 `02_Const.js:45`(`RpcList.other_callphone`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**。
|
||||
- 备注:⚠️ `02_Const.js` 已定义但客户端无处理函数;服务器是否用 `other_callphone` 单独推送他人拨号待后端确认(当前客户端用 `call_phone` 推送统一处理本人与他人)。
|
||||
|
||||
### hangup_phone · route=room · C→S 请求 + S→C 推送 · 挂断电话
|
||||
- 场景:挂断电话,置玩家 `onstate=0`。
|
||||
- 请求字段(C→S):仅通用四字段(发送处 `06_Player.js:372`)。
|
||||
- 推送字段(S→C):rpc=`hangup_phone`,`seat`(挂断者座位 → 该玩家 `onstate=0`)。
|
||||
- 源码:发送 `09_Net.js:461`(`Send_hangup_phone`)/接收 `09_Net.js:465` → `07_Desk.js:977`(`Desk.hangup_phone`)。
|
||||
- 备注:—
|
||||
|
||||
### other_hangup · route=room · S→C 推送(⚠️)
|
||||
- 场景:与 `hangup_phone` 成对,疑为他人挂断电话推送。
|
||||
- 请求字段(C→S):无。
|
||||
- 推送字段(S→C):未知(疑似含 `seat`)。
|
||||
- 源码:常量 `02_Const.js:47`(`RpcList.other_hangup`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**。
|
||||
- 备注:⚠️ `02_Const.js` 已定义但客户端无处理函数;服务器是否用 `other_hangup` 单独推送待后端确认。
|
||||
|
||||
---
|
||||
|
||||
## 房内可收的其它推送(接收语义在 room 场景,发送走 agent 路由)
|
||||
|
||||
> 以下推送在房间内被消费,但其上行请求实际走 `agent` 路由,详细发送定义见 [02 · agent 路由](02-协议-agent路由.md);此处仅记录房内接收语义,避免遗漏。
|
||||
|
||||
### update_bean · S→C 推送(房内他人充值场景)
|
||||
- 场景:房间内**他人**充值豆豆/金币,刷新该座余额。
|
||||
- 推送字段(S→C):`seat`(充值者座位;无 `seat` 则为自己充值,走 `C_Player.update_bean`), `bean`(新余额), `type`(==6 时播放金币音效 `Logic.playCoinMp3()`);另含 `change`(存在时为大厅购买分支,不在房内座位场景)。
|
||||
- 源码:接收 `09_Net.js:598`(`Net.update_bean`) → `07_Desk.js:1201`(`Desk.update_bean`)。
|
||||
- 备注:与 [02 章的 update_bean](02-协议-agent路由.md) 为**同名推送**,此处侧重"房间内座位刷新"分支(`seat` 已定义时)。
|
||||
|
||||
### update_charm · S→C 推送(房内座位魅力更新)
|
||||
- 场景:批量更新房内座位魅力值。
|
||||
- 推送字段(S→C):`seatlist`:[ { `seat`, `charm` }, ... ],逐项写入对应座位 `Player.setCharm`。
|
||||
- 源码:接收 `09_Net.js:731`(`Net.update_charm`) → `07_Desk.js:1260`(`Desk.update_charm`)。
|
||||
- 备注:⚠️ 交叉引用——**发送走 agent 路由**(`09_Net.js:727` `Send_update_charm` → `RouteList.agent`),归档于 02 章;本篇仅记录房内接收。
|
||||
|
||||
### broadcast · S→C 推送(房内跑马灯 / 弹窗)
|
||||
- 场景:房内可收到的全局广播(跑马灯或顶部弹窗)。
|
||||
- 推送字段(S→C):`msgtype`(0=顶部即时弹窗 `ShowiMessage` / 1=跑马灯 `addBroadcast`), `msgcontent`(内容文本)。
|
||||
- 源码:接收 `09_Net.js:534`(`Net.broadcast`) → `07_Desk.js:1112`(`Desk.broadcast`)。
|
||||
- 备注:交叉引用——**发送走 agent 路由**(`09_Net.js:530` `Send_broadcast` → `RouteList.agent`),归档于 02 章;本篇仅记录房内接收。
|
||||
|
||||
---
|
||||
|
||||
## 服务器切换(room 侧)
|
||||
|
||||
### connect_roomserver · route=room · C→S 请求 + S→C 推送 · 切到房间服
|
||||
- 场景:从大厅服切换到房间服务器。
|
||||
- 请求字段(C→S):由 `Send_connect_roomserver` 发起(走 room 路由)。
|
||||
- 推送字段(S→C):`data.roomserver`(新房间服地址)。处理:置 `GameData.ConnectType=true`、`GameData.ConnectRpc=connect_roomserver`、`GameData.Server=data.roomserver`,关闭当前连接并用新地址重连。
|
||||
- 源码:发送 `09_Net.js:500`(`Send_connect_roomserver`)/接收 `09_Net.js:503`(`Net.connect_roomserver`)。
|
||||
- 备注:—
|
||||
|
||||
### connect_agentserver · S→C 推送(切回大厅服;上行走 agent 路由)
|
||||
- 场景:解散/退房后从房间服切回大厅服(agent)。
|
||||
- 推送字段(S→C):`data.opt`(切服原因,值为 `other_break_room` 或 `free_room`;命中其一时 `GameUI.StartLoad()`), `data.agentserver`(新大厅服地址)。处理:置 `GameData.ConnectRpc=connect_agentserver`、`GameData.Server=data.agentserver`,关连接重连。
|
||||
- 源码:接收 `09_Net.js:518`(`Net.connect_agentserver`);上行 `09_Net.js:515`(`Send_connect_agentserver` → `RouteList.agent`)。
|
||||
- 备注:交叉引用——**上行请求走 agent 路由**,归档于 02 章;本篇记录其作为 `other_break_room`/`free_room` 后续切服推送的语义(原文档"服务器切换"仅写了 `connect_roomserver`,此处补全 `connect_agentserver`)。
|
||||
@@ -0,0 +1,331 @@
|
||||
# 04 · 核心数据结构
|
||||
|
||||
> 这些结构是协议 `data` 的承载体,也是双向数据包共用的数据模型。新前端可据此定义自己的数据模型,字段名须与协议一致。
|
||||
>
|
||||
> 本篇负责**壳框架核心数据结构**:`C_Player`、`Player(seat)`、`Desk`、`player_login` 响应、`roomtype` 配置、`GameData` 相关字段。
|
||||
> **不含子游戏对局态结构**:`deskinfo` 内部结构由各子游戏定义,壳框架只把它当作"子游戏对局快照"原样透传给 `Game_Modify.Reconnect / DeskInfo`,用于断线重连(详见 05 章)。
|
||||
|
||||
---
|
||||
|
||||
## C_Player(本地玩家对象)
|
||||
|
||||
来源:`06_Player.js` `function Player(seat)`(`06_Player.js:1`)。
|
||||
全局唯一的本地玩家实例 `C_Player = new Player(-1)`(`12_Logic.js:480`),即初始 `seat = -1`。登录与各推送会写入这些字段。
|
||||
|
||||
逐字段与构造函数初值核对(`06_Player.js:2`–`33`):
|
||||
|
||||
| 字段 | 初值 | 类型 | 说明 |
|
||||
|------|------|------|------|
|
||||
| openid | "" | string | 微信 openid |
|
||||
| playerid | -1 | int | 玩家ID |
|
||||
| nickname | "" | string | 昵称 |
|
||||
| avatar | "" | string | 头像URL |
|
||||
| sex | 0 | int | 性别 0未知/1男/2女 |
|
||||
| ip | "" | string | IP 地址 |
|
||||
| province | "" | string | 省(微信) |
|
||||
| city | "" | string | 市(微信) |
|
||||
| roomcard | -1 | int | 房卡数量 |
|
||||
| taskstate | 0 | int | 任务状态 |
|
||||
| unionid | 0 | string \| number | 开放平台唯一标识。⚠️ 构造初值为 `0`(number),但 `SetWxInfo` 用服务器下发字符串赋值(`06_Player.js:81`),运行期实际为 string |
|
||||
| seat | seat(参数) | int | 座位号(C_Player 为 -1=大厅,≥0=房间内) |
|
||||
| score | 0 | int | 积分 |
|
||||
| state | -1 | int | 解散投票状态,见下方"state 语义重载"说明 |
|
||||
| status | 0 | int | 身份 0默认/1房主/2非房主 |
|
||||
| canexit | 1 | int | 是否可直接退出 1是/0否,见下方说明 |
|
||||
| onstate | 0 | int | 在线状态 0在线/1离线/2通话中 |
|
||||
| addr | null | object \| null | 定位信息(含 province/city/errorCode) |
|
||||
| invitecode | "" | string | 邀请码 |
|
||||
| isStart | false | bool | 能否点击按钮开始游戏 |
|
||||
| bean | 0 | int | 豆豆(游戏币 / 星星) |
|
||||
| initialBean | 0 | int | 进房时豆豆初始值 |
|
||||
| isprepare | 0 | int | 准备状态 0未/1已 |
|
||||
| advanced | 0 | int | 是否有高级选项 0无/1有 |
|
||||
| paycode | "" | string | 吱口令 |
|
||||
| wareHouseStarCount | 0 | int | 仓库库存星星数 |
|
||||
| bankpower | 0 | int | 是否有仓库权限 |
|
||||
| bankpwd | 0 | int | 是否已设仓库密码 |
|
||||
| charm | undefined | int \| undefined | 魅力值(构造时显式 `undefined`,`06_Player.js:31`) |
|
||||
| sign | "" | string | 签名 |
|
||||
| tel | "" | string | 绑定手机号 |
|
||||
|
||||
> **注意**:构造函数中 `offline` 字段被注释掉(`06_Player.js:17`),因此 `C_Player`(及 `new Player()` 出来的座位对象)初始**不带 `offline` 字段**;`offline` 仅在调用 `Init()` 后才被赋值(见下文 Player 节)。
|
||||
|
||||
### state 语义重载(重点订正)
|
||||
|
||||
`state` 默认值为 **-1**(非 0)。各取值含义以方法实际赋值为准(`06_Player.js`):
|
||||
|
||||
| 取值 | 含义 | 赋值来源 |
|
||||
|------|------|----------|
|
||||
| -1 | 默认 / 无投票状态 | 构造函数 `06_Player.js:15`、`BreakRoom()` `06_Player.js:312` |
|
||||
| 0 | 申请解散(发起方)/ 未同意 | `ApplyBreakRoom()` `06_Player.js:321` |
|
||||
| 1 | 同意解散 | `AgreeBreakRoom()` `06_Player.js:327` |
|
||||
| 2 | 拒绝解散 | `RefuseBreakRoom()` `06_Player.js:333` |
|
||||
|
||||
⚠️ **`0` 是语义重载值**,不能绝对化为"申请":
|
||||
- `ApplyBreakRoom()` 把 state 设为 `0`(申请解散);
|
||||
- 但 `self_refuse_free_room` / `other_refuse_free_room` 在拒绝后会把**所有玩家** `state` **重置为 0**(`07_Desk.js:849`、`07_Desk.js:860`),此处 `0` 是"重置/默认"语义;
|
||||
- `login` 重连恢复投票时,`agreefree[i]==0` 也把对应玩家 state 写成 `0`(`07_Desk.js:409`),表示"未同意"。
|
||||
|
||||
> 源码 `06_Player.js:15` 的行内注释把 `-1` 写成"申请解散",是**错误注释**;以上述四个方法的实际赋值为准。
|
||||
|
||||
### canexit 推断规则
|
||||
|
||||
`ChangeExit(v)` 设置(`06_Player.js:338`)。在 `login` / 进房 / 开战流程中:
|
||||
- `isbet == 0` → `ChangeExit(1)`(可退出);`isbet != 0` → `ChangeExit(0)`(不可退出)(`07_Desk.js:381`);
|
||||
- `infinite == 1`(无限局)→ 强制 `ChangeExit(1)`(始终可退出)(`07_Desk.js:386`)。
|
||||
|
||||
---
|
||||
|
||||
## Player(座位对象 / 房内其他玩家)
|
||||
|
||||
来源:`06_Player.js` `function Player(seat)`。`Desk.PlayerList[]` 中每个元素都是一个 `Player`,由 `SetDeskInfo(data, bTemp)` 从服务器数据填充。其字段表与 C_Player 完全相同(同一构造函数),差异仅在于哪些字段来自服务器、哪些本地维护。
|
||||
|
||||
### ① `SetDeskInfo(_data, bTemp)` 实际读取的服务器字段(权威,`06_Player.js:262`)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| playerid | int | 玩家ID |
|
||||
| nickname | string | 昵称 |
|
||||
| avatar | string | 头像URL |
|
||||
| sex | int | 性别 |
|
||||
| ip | string | IP |
|
||||
| onstate | int | 在线状态 0在线/1离线/2通话中 |
|
||||
| bean | int | 豆豆数 |
|
||||
| isprepare | int | 准备状态 0/1 |
|
||||
| paycode | string | 吱口令(`_data.paycode` 有才读,否则置 "",`06_Player.js:277`) |
|
||||
| charm | int | 魅力值 |
|
||||
| sign | string | 签名 |
|
||||
|
||||
**initialBean 的赋值依赖 `bTemp`(重点订正)**(`06_Player.js:271`):
|
||||
|
||||
```js
|
||||
if(!bTemp){ this.initialBean = _data.bean; } // 正常入座:用本局豆豆做初始值
|
||||
else { this.initialBean = _data.initialBean; } // 临时填充:保留原始 initialBean
|
||||
```
|
||||
|
||||
- `bTemp` 缺省(`undefined`/`false`):`initialBean = _data.bean`(如 `login`/`self_join_room`/`other_join_room` 填充,调用时**不传** bTemp)。
|
||||
- `bTemp == true`:`initialBean = _data.initialBean`,即**不**用 bean 覆盖。典型场景是 **换座** `change_seat`(`07_Desk.js:183`),两座玩家通过 `getDeskInfo()` 互换数据时以 `bTemp=true` 调用,保留各自原始初值。
|
||||
- 因此原文档"无条件把 bean 复制为 initialBean"的描述是**错误**的,须按上面分支理解。
|
||||
|
||||
> `other_join_room` 推送中,座位号在外层 `data.seat`,其余玩家字段与上表同级**平铺**在 `data` 上(`07_Desk.js:730`)。
|
||||
|
||||
### ② 客户端本地维护、不来自 `SetDeskInfo` 的字段
|
||||
|
||||
由其它包写入或本地推断,新前端按本地状态处理即可:
|
||||
|
||||
- `seat`:进房时分配(`SetSeat`)。
|
||||
- `status`:0默认/1房主/2非房主,由 `isowner` 或 `seat==0` 推断(`07_Desk.js:325`)。
|
||||
- `state`:解散投票 -1/0/1/2,由 apply/agree/refuse 包写入。
|
||||
- `canexit`:由 isbet/infinite 推断。
|
||||
- `offline`:0否/1离线。⚠️ **不在构造函数中**,仅 `Init(bTemp)` 会写入 `offline = 0`(`06_Player.js:53`);房内离线/上线则改 `onstate`(`07_Desk.js:889`、`07_Desk.js:895`),实际在线状态以 `onstate` 为准。
|
||||
- `tel`、`initialBean`(见上)、`isStart`、`roomcard`、`unionid`、`taskstate`、`advanced`、`bankpower`、`bankpwd`、`wareHouseStarCount`、`addr`、`invitecode`:座位对象一般用不到,仅 C_Player 维护。
|
||||
|
||||
---
|
||||
|
||||
## Desk(牌桌/房间对象)
|
||||
|
||||
来源:`07_Desk.js:3`–`29` 顶部定义。表示当前房间整体状态,全部字段如下(逐字段核对,共 25 项):
|
||||
|
||||
| 字段 | 初值 | 类型 | 说明 |
|
||||
|------|------|------|------|
|
||||
| PlayerList | [] | array\<Player\> | 座位玩家数组(`07_Desk.js:5`) |
|
||||
| roomcode | "" | string | 房号(`07_Desk.js:6`) |
|
||||
| stage | 0 | int | 牌桌阶段 0未开局/1已开局(`07_Desk.js:7`) |
|
||||
| state | 0 | int | 解散状态 0正常/1申请解散中(`07_Desk.js:8`) |
|
||||
| applyresult | -1 | int | 投票结果 -1无/0不通过/1通过(`07_Desk.js:9`) |
|
||||
| AgreeList | [] | array\<int\> | 同意解散的座位号列表(`07_Desk.js:10`) |
|
||||
| agreefree | [] | array\<int\> | 解散投票各座位 state 数组(来自 `agreefree.state`,`07_Desk.js:11`/`394`) |
|
||||
| roomtype | [] | array | 房间类型配置(透传,见下) |
|
||||
| warcnt | 0 | int | 开战条件(满足人数等,来自 `makewar`,`07_Desk.js:13`) |
|
||||
| playercnt | 0 | int | 当前玩家总数(`07_Desk.js:14`) |
|
||||
| count | 0 | int | 总局数(来自 `asetcount`,`07_Desk.js:15`) |
|
||||
| deskfree | null | object \| null | 解散结算快照(来自 `free_room` 的 `deskfree`,`07_Desk.js:16`/`868`) |
|
||||
| starCount | 0 | int | 投降扣除星星数(来自 `beanlimit`,见下方 ⚠️,`07_Desk.js:17`/`66`) |
|
||||
| roomMode | 0 | int | 0普通场/1星星场(`07_Desk.js:18`) |
|
||||
| needprepare | 0 | int | 是否需要准备(`07_Desk.js:19`) |
|
||||
| myInfo | null | Player \| null | 自己的信息(`07_Desk.js:20`) |
|
||||
| infinite | 0 | int | 是否无限局 0否/1是(`07_Desk.js:21`) |
|
||||
| isSystem | 1 | int | 是否系统房间,初值 1(`07_Desk.js:22`) |
|
||||
| shortcode | null | string \| null | 房间短号,初值 **null**(`07_Desk.js:23`/`93`) |
|
||||
| videoConfig | null | object \| null | 视频房选项(`07_Desk.js:24`) |
|
||||
| ownerNotice | null | string \| null | 短号房房主留言(`07_Desk.js:25`/`45`) |
|
||||
| videoDes | "" | string | 视频房描述(由 `setVideoConfig` 拼接,`07_Desk.js:26`/`108`) |
|
||||
| rebateNumber | 0 | int | 房间抽成数量(`07_Desk.js:27`) |
|
||||
| rebateMode | 0 | int | 抽成类型/方式(`07_Desk.js:28`) |
|
||||
| rebateType | 0 | int | 抽成对象——抽金币还是魅力值,⚠️ 数字取值见下(`07_Desk.js:29`) |
|
||||
|
||||
> 另有方法 `Desk.setMyInfo / setRoom / Create / Init / login / create_room / self_join_room / other_join_room` 等填充逻辑,详见对应推送章节。
|
||||
|
||||
---
|
||||
|
||||
## 登录响应(Desk.login / player_login)
|
||||
|
||||
`route=agent, rpc=player_login` 的内层 `data`。`Desk.login(_msg)` 解析(`07_Desk.js:212`)。
|
||||
`state==0` 为成功;字段分两组:A. 账号与资产(始终下发)、B. 房间恢复(在房 / 重连才下发)。
|
||||
|
||||
### A. 账号与资产(始终下发)
|
||||
|
||||
| 字段 | 类型 | 说明 | 出处 |
|
||||
|------|------|------|------|
|
||||
| state | int | 0成功,非0失败 | `07_Desk.js:214` |
|
||||
| playerid | int | 玩家ID(`SetMyInfo` 读取) | `07_Desk.js:268` |
|
||||
| score | int | 积分(缺省补 0,`07_Desk.js:243`) | |
|
||||
| bean | int | 豆豆 | `SetMyInfo` |
|
||||
| roomcard | int | 房卡 | `SetMyInfo` |
|
||||
| taskstate | int | 任务状态 | `SetMyInfo` |
|
||||
| ip | string | 玩家IP | `SetMyInfo` |
|
||||
| bankpower | int | 仓库权限 | `SetMyInfo` |
|
||||
| bank | int | 仓库库存星星数(→ wareHouseStarCount) | `06_Player.js:97` |
|
||||
| bankpwd | int | 是否已设仓库密码 | `06_Player.js:102` |
|
||||
| charm | int | 魅力值 | `SetMyInfo` |
|
||||
| sign | string | 签名 | `SetMyInfo` |
|
||||
| tel | string | 绑定手机号 | `06_Player.js:109`、`07_Desk.js:228` |
|
||||
| agentid | string | 代理ID(→ GameData.AgentId) | `07_Desk.js:276` |
|
||||
| channelid | string | 渠道ID(→ GameData.ChannelId) | `07_Desk.js:277` |
|
||||
| invitecode | string | 邀请码(可选,有才设) | `07_Desk.js:246` |
|
||||
| initCard | int | 初始房卡(可选,→ GameData.initCard) | `07_Desk.js:257` |
|
||||
| initBean | int | 初始豆豆(可选,→ GameData.initBean) | `07_Desk.js:263` |
|
||||
| advanced | int | 是否有高级选项(可选,缺省 0) | `07_Desk.js:271` |
|
||||
| **openid** | string | 微信 openid(仅 `deviceLogin` 分支读取) | `07_Desk.js:220` |
|
||||
| **nickname** | string | 昵称(deviceLogin 分支) | `07_Desk.js:222` |
|
||||
| **avatar** | string | 头像URL(deviceLogin 分支,作 `headimgurl`) | `07_Desk.js:221` |
|
||||
| **sex** | int | 性别(deviceLogin 分支) | `07_Desk.js:223` |
|
||||
| **city** | string | 城市(deviceLogin 分支) | `07_Desk.js:224` |
|
||||
| **province** | string | 省份(deviceLogin 分支) | `07_Desk.js:225` |
|
||||
| **unionid** | string | 开放平台ID(deviceLogin 分支) | `07_Desk.js:226` |
|
||||
|
||||
> `openid/nickname/avatar/sex/city/province/unionid/tel` 仅在 `GameData.sysConfig.deviceLogin` 为真时被读取并通过 `SetWxInfo` 写入 C_Player(`07_Desk.js:215`–`230`)。非设备登录时这些信息由微信授权链路另行获取。
|
||||
|
||||
> **✅ 真机联调实测确认(2026-06-28,YouleNexus + 本地测试服)**:成功登录回包(单层 `{app,route,rpc,data}`,见 [01 §3.2](01-传输层与架构.md))`data` 实测为:
|
||||
> ```json
|
||||
> {"state":0,"playerid":430511,"agentid":"…","channelid":"…","nickname":"…","avatar":"…","openid":"…","sex":0,"unionid":"…","roomcard":3,"bean":0,"score":0,"invitecode":null,"advanced":0,"taskstate":1,"ip":"127.0.0.1","bankpower":1,"bank":0,"sign":null,"tel":null,"initCard":"3","initBean":"0","bankpwd":0,
|
||||
> "agentname":"进贤","agentmode":2,"gameversion":41}
|
||||
> ```
|
||||
> 上表 A 组字段均得到印证。另含**文档此前未列**的三个字段(旧客户端 `Desk.login` 未读、属代理/版本信息):
|
||||
> - `agentname` string —— 代理商名称(实测"进贤")。
|
||||
> - `agentmode` int —— 代理模式(实测 2)。
|
||||
> - `gameversion` int —— 服务器侧游戏版本(实测 41)。
|
||||
>
|
||||
> 同时确认 **请求 `player_login.data.version` 字段须为数字 versionCode**(实测发 `10000` 通过;发字符串 `"1.1"` 被 `kick_server`「检查到新版本」拒绝),与源码 `data.version=GameData.versionCode` 一致。
|
||||
|
||||
### B. 房间恢复(在房 / 断线重连时才下发)
|
||||
|
||||
仅当 `_msg.data.roomcode` 存在时进入此分支(`07_Desk.js:282`),代表玩家原本就在房间内。
|
||||
|
||||
| 字段 | 类型 | 说明 | 出处 |
|
||||
|------|------|------|------|
|
||||
| roomcode | string / number | 房号;响应可为数字,见下方入口类型说明 | `07_Desk.js:282` |
|
||||
| roomtype | array | 房间类型配置(透传) | `07_Desk.js:284` |
|
||||
| asetcount | int | 总局数(→ Desk.count) | `07_Desk.js:323` |
|
||||
| isbattle | int | 0未开局/1已开局(→ Desk.stage) | `07_Desk.js:324` |
|
||||
| makewar | int | 开战条件(→ Desk.warcnt) | `07_Desk.js:321` |
|
||||
| seat | int | 自己的座位 | `07_Desk.js:320` |
|
||||
| isowner | int | 1房主/0非房主(→ C_Player.status) | `07_Desk.js:325` |
|
||||
| players | array | 房内玩家数组(按座位下标,元素为 SetDeskInfo 字段集,可含 null) | `07_Desk.js:338` |
|
||||
| roommode | int | 0普通/1星星场(→ Desk.roomMode) | `07_Desk.js:295` |
|
||||
| beanlimit | int | ⚠️ → `setStarCount`(投降扣除星星数),见下方说明 | `07_Desk.js:298` |
|
||||
| needprepare | int | 是否需准备 | `07_Desk.js:301` |
|
||||
| infinite | int | 0普通/1无限局 | `07_Desk.js:304` |
|
||||
| rebateNumber | int | 抽成数量 | `07_Desk.js:307` |
|
||||
| rebateMode | int | 抽成方式/类型 | `07_Desk.js:308` |
|
||||
| rebateType | int | 抽成对象(⚠️ 取值见下) | `07_Desk.js:309` |
|
||||
| sign | string | 签名(→ C_Player.setSign) | `07_Desk.js:310` |
|
||||
| ownerNotice | string | 短号房房主留言 | `07_Desk.js:311` |
|
||||
| videoConfig | object | 视频房配置 | `07_Desk.js:312` |
|
||||
| shortcode | string | VIP 房短号 | `07_Desk.js:313` |
|
||||
| match | object | 比赛信息(→ GameData.matchInfo) | `07_Desk.js:285` |
|
||||
| matchid | string | 比赛ID(→ GameData.matchId) | `07_Desk.js:290` |
|
||||
| agreefree | object | 解散投票信息 `{ state:int[], countdown }`,存在即表示恢复进行中的投票 | `07_Desk.js:389` |
|
||||
| isbet | int | 0可退出/1不可退出(→ ChangeExit) | `07_Desk.js:381` |
|
||||
| deskinfo | object | 子游戏对局快照,存在即触发重连(见下方 ⚠️) | `07_Desk.js:419` |
|
||||
|
||||
#### ⚠️ B 组存疑/订正点
|
||||
|
||||
- **deskinfo 与 isbattle 的关系**:`login()` 仅判断 `if(_msg.data.deskinfo)`(`07_Desk.js:419`)是否存在来决定是否调用 `Game_Modify.Reconnect`,**并不**判断 `isbattle`。原文档"isbattle=1 时用于重连"是推断;准确表述应为 **"当响应含 `deskinfo` 时触发子游戏重连(服务器通常仅在对局进行中才下发该字段)"**。
|
||||
- **beanlimit 语义**:源码为 `if(_msg.data.beanlimit){ Desk.setStarCount(_msg.data.beanlimit); }`(`07_Desk.js:298`),`setStarCount` 写入 `Desk.starCount`,而 `starCount` 注释为"投降扣除星星数量"(`07_Desk.js:17`)。因此 `beanlimit` 倾向于 **"星星场投降扣除数"** 而非泛指"豆豆限制",⚠️ 确切业务含义待后端确认。
|
||||
- **rebateType 取值**:`07_Desk.js:29` 仅注释"抽金币还是魅力值",源码**无明确数字定义**。原文档标注的"0金币/1魅力值"⚠️ **待后端确认**。
|
||||
- **deskwar 不在 login 中**:`login()` 函数体内**未读取** `deskwar`。`deskwar`(是否自动开战)由 `self_join_room`(`07_Desk.js:634`)、`other_join_room`(`07_Desk.js:747`)、`player_prepare`(`07_Desk.js:1137`)读取。因此 deskwar **不属于登录响应字段**,已移至下方"创建/进入响应"表(见 ⚠️ 标注)。
|
||||
|
||||
---
|
||||
|
||||
## 房间创建 / 进入响应公共字段
|
||||
|
||||
`create_room`(`07_Desk.js:454`)/ `self_join_room`(`07_Desk.js:529`)响应内层 `data`,字段集与登录 B 组高度一致:
|
||||
|
||||
| 字段 | 说明 | 备注 |
|
||||
|------|------|------|
|
||||
| state | 0成功,非0失败 | self_join_room 另有 99=房间不存在 |
|
||||
| roomcode | 房号 | string 或非负安全整数;见下方入口类型说明 |
|
||||
| seat | 自己座位 | |
|
||||
| isowner | 是否房主 0/1 | |
|
||||
| roomtype | 房间类型配置数组(透传) | |
|
||||
| makewar | 开战条件 | |
|
||||
| asetcount | 总局数 | |
|
||||
| players | 房内玩家数组(join 时有) | |
|
||||
| roommode / beanlimit / needprepare / infinite | 房间模式相关 | beanlimit 语义同上 ⚠️ |
|
||||
| rebateNumber / rebateMode / rebateType | 抽成相关 | rebateType 取值待确认 ⚠️ |
|
||||
| shortcode | 短号 | |
|
||||
| match / matchid | 比赛信息 | |
|
||||
| videoConfig | 视频房配置 | |
|
||||
| ownerNotice / ownerNote | 房主留言 / 备注(ownerNote 仅 self_join_room,`07_Desk.js:575`) | |
|
||||
| paycode | 吱口令(join 时可选,`07_Desk.js:584`) | |
|
||||
| **deskwar** | 是否自动开战(join 时可选,`07_Desk.js:634`)⚠️ 此字段属"进入"而非登录响应 | |
|
||||
| deskinfo | 子游戏对局快照(join 时可选,重连用,`07_Desk.js:647`) | |
|
||||
| showerror / error | 失败时:是否显示错误 / 错误文案 | |
|
||||
|
||||
---
|
||||
|
||||
### roomcode 入口类型说明
|
||||
|
||||
实际 `create_room` 响应可返回数字房号(2026-09-08 联调报文类型证据);旧前端直接接收该字段,服务端子游戏入口也按数值解析 roomcode。不能根据 Desk 初始值 `""` 推断所有响应都只返回字符串。
|
||||
|
||||
Cocos 在创建、加入及登录恢复的响应解析入口统一把数字房号转为内部字符串标识;字符串原样保留,包括前导零。数字必须为非负安全整数,非法类型显式报错,不补默认房号。原始响应 `raw` 不改写,roomtype 继续原样透传;此适配不要求服务器改动。
|
||||
|
||||
## roomtype(房间类型配置数组)⚠️ 子游戏自定义
|
||||
|
||||
`Desk.setRoom` 接收并直接保存到 `Desk.roomtype`(`07_Desk.js:166`),创建/进入时原样透传给 `Game_Modify.onCreateDesk` / `Game_Modify.setRoomDes`(`07_Desk.js:358`、`508`、`629`)。**壳框架本身不解析该数组的各位含义**。
|
||||
|
||||
示例(来自 `01_SubGame_modify.js` 创建房间,仅供参考):
|
||||
```js
|
||||
roomtype: [1, 4, 1, 2, 2, [1,1,[1,2000,10],null,null,1], [1,0,5]]
|
||||
```
|
||||
- 这是一个**嵌套数组**,各位含义(局数、人数、玩法选项、星星场配置、抽成配置等)由**具体子游戏**约定。
|
||||
- **服务器按这套数组解析房间规则**,新前端创建房间时必须发送与原子游戏**完全一致的 roomtype 结构**。
|
||||
- 上述示例数组各位精确含义属**子游戏范畴**,需对照目标子游戏的创建房间界面逐项核对,本壳框架未给出通用定义。
|
||||
|
||||
---
|
||||
|
||||
## GameData(全局数据,相关字段)
|
||||
|
||||
来源:`04_Data.js`(`var GameData = GameData || {}`,`04_Data.js:24`)。login / 进房流程写入的相关键:
|
||||
|
||||
| 字段 | 来源 | 说明 |
|
||||
|------|------|------|
|
||||
| AgentId | login `agentid`(`07_Desk.js:276`) | 代理ID |
|
||||
| ChannelId | login `channelid`(`07_Desk.js:277`) | 渠道ID |
|
||||
| initCard | login `initCard`(`07_Desk.js:258`) | 初始房卡 |
|
||||
| initBean | login `initBean`(`07_Desk.js:264`) | 初始豆豆 |
|
||||
| matchInfo | login/进房 `match`(`07_Desk.js:286`) | 比赛场信息 |
|
||||
| matchId | login/进房 `matchid`(`07_Desk.js:291`) | 比赛ID |
|
||||
| starName | 配置(`04_Data.js:189`,默认"星星") | 货币显示名 |
|
||||
| infoSeat | `04_Data.js:225`(默认 -1) | 信息面板焦点座位 |
|
||||
| sysConfig | `04_Data.js:440` | 子游戏系统配置(含 `deviceLogin` 等开关) |
|
||||
| isLogin / hallLogin / isReconnect / vipRoomJump | login 流程置位(`07_Desk.js:241` 等) | 登录/重连状态标记 |
|
||||
|
||||
> GameData 字段众多,此处仅列与本篇数据流(登录、房间恢复)直接相关者。完整 GameData 配置项见配置文档。
|
||||
|
||||
---
|
||||
|
||||
## 其它推送数据结构
|
||||
|
||||
| rpc | data 结构 | 出处 |
|
||||
|-----|-----------|------|
|
||||
| update_bean | `{ bean, change?, seat?, type?, text }` | `07_Desk.js:1201` |
|
||||
| update_roomcard | `{ roomcard, text, change? }`(无 change 时刷新本地) | `06_Player.js:142` |
|
||||
| update_charm | `{ seatlist:[ {seat, charm} ] }` | `07_Desk.js:1260` |
|
||||
| change_star | `{ state, star1, star2, msg?, count?, error? }` | `07_Desk.js:1238` |
|
||||
| broadcast | `{ msgtype?:0框/1滚动, msgcontent }` | `07_Desk.js:1112` |
|
||||
| show_message | `{ msg, time }` | `07_Desk.js:1173` |
|
||||
| kick_server | `{ msg }` | `07_Desk.js:1108` |
|
||||
| kick_offline | `{ fromOther?, gameid? }` | `07_Desk.js:945` |
|
||||
| free_room | `{ freeNow?, deskfree?, roomcard?, seats?, tips?, time? }` | `07_Desk.js:864` |
|
||||
@@ -0,0 +1,294 @@
|
||||
# 05 · 框架↔子游戏桥接 与 外部(H5/小程序)桥接
|
||||
|
||||
> 本篇只讲**框架侧机制**:route 分发判断、发对局包 API、对局包如何进入子游戏、deskinfo 重连、开战入口、外部 deeplink(H5/小程序)进房。
|
||||
> **不展开**具体子游戏的对局 rpc(发牌/出牌/下注/结算字段等由子游戏 `Game_Modify` 自定义实现,框架不感知)。
|
||||
|
||||
## 1. 平台层 vs 游戏内 的分界(route 分发)
|
||||
|
||||
WebSocket 主分发在 `12_Logic.js` 的 `onmessage` 内(约 `12_Logic.js:258-263`):
|
||||
|
||||
```js
|
||||
// 12_Logic.js onmessage 内(约 258-263)
|
||||
if (_msg.route == RouteList.platform || _msg.route == RouteList.agent || _msg.route == RouteList.room) {
|
||||
if (min_ExitsFunction(Net[_msg.rpc])) { // 存在性守卫:表里有该处理函数才调用
|
||||
Net_msg.rpc; // 平台层(02/03 章覆盖)
|
||||
}
|
||||
} else {
|
||||
Game_Modify._ReceiveData(_msg); // 游戏内对局协议 → 交子游戏
|
||||
}
|
||||
```
|
||||
|
||||
- `RouteList.platform = "platform"`、`RouteList.agent = "agent"`、`RouteList.room = "room"`(`02_Const.js:8-10`)。
|
||||
- `AppList.app = "youle"`(`02_Const.js:5`)。
|
||||
- **存在性守卫**:平台分支用 `min_ExitsFunction(Net[_msg.rpc])` 包裹,表里没有对应 `Net[rpc]` 处理函数时**静默丢弃**,不报错。新前端实现分发表时须复现这一点(未知 rpc 不应崩溃)。
|
||||
|
||||
> **关键**:游戏内对局包使用一个**非** `platform/agent/room` 的 `route`,落到 `else` 分支,
|
||||
> 由子游戏的 `Game_Modify._ReceiveData(_msg)` 接管,自行按 `_msg.rpc` 分发。框架对其字段一无所知。
|
||||
|
||||
### ⚠️ 另有一处同形分发,勿混淆
|
||||
|
||||
`09_Net.js:31-37` 里有一段**结构相同**的 route 分发,但它位于 `Net._SendData` 的 **HTTP/Ajax 成功回调内**,且**仅当 `ConstVal.netType != 0`(HTTP 模式)才会执行**:
|
||||
|
||||
```js
|
||||
// 09_Net.js Net._SendData 内(约 19-45)
|
||||
if (ConstVal.netType == 0) {
|
||||
Net.ws_tcp.send(JSON.stringify(_msg)); // 默认:WebSocket,走第 1 节的 onmessage 主分发
|
||||
} else {
|
||||
Func.AjaxHttp2(GameData.Server, _msg, function(_msg, state, input_msg){
|
||||
// —— HTTP 模式下的响应回调,内部才有那段 route 分发(约 31-37)——
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
默认 `netType == 0`(WebSocket),**不进**该分支。它是 HTTP 兜底模式的同步响应处理,**不是**与主分发并列的第二条主链路。**唯一权威的运行时分发是第 1 节的 `12_Logic.js:258-263`。**
|
||||
|
||||
## 2. 本工程状态:子游戏逻辑为空模板
|
||||
|
||||
`Game_Surface_3` 是**平台壳/模板工程**。`01_SubGame/02_SubGame_Input.js` 中 `Game_Modify.*` 全部是**空函数桩**:
|
||||
|
||||
```js
|
||||
// 01_SubGame/02_SubGame_Input.js(约 32-57)
|
||||
Game_Modify._ReceiveData = function(_msg){ } // 接收游戏内数据包(空)
|
||||
Game_Modify.StartWar = function(_msg){ } // 开战(空)
|
||||
Game_Modify.Reconnect = function(_deskinfo){ } // 重连恢复对局(空)
|
||||
Game_Modify.DeskInfo = function(_msg){ } // 未开战自己加入时的牌桌数据(空)
|
||||
```
|
||||
|
||||
**因此本工程内不存在某款具体游戏的对局字段定义。** 要拿到「发牌/出牌/结算」等精确字段,需从**目标子游戏工程**(其 `01_SubGame_modify.js` 有真实实现)提取,或对线上服务器**抓包**。本篇只描述上面这些钩子的**调用时机与实参**(框架契约)。
|
||||
|
||||
## 3. 游戏内发送通道(客户端 → 服务器)
|
||||
|
||||
游戏内操作同样走统一出口 `Net._SendData(_app, _route, _rpc, _data)`(`09_Net.js:12`):
|
||||
|
||||
```js
|
||||
Net._SendData("youle", "<game_route>", "<game_rpc>", {
|
||||
agentid: GameData.AgentId,
|
||||
gameid: GameData.GameId,
|
||||
playerid: C_Player.playerid,
|
||||
roomcode: Desk.roomcode,
|
||||
seat: C_Player.seat,
|
||||
// ... 该操作的业务字段(出牌/下注内容等,子游戏自定义)
|
||||
});
|
||||
```
|
||||
|
||||
- `_route` 用游戏约定值(非 agent/room/platform),服务器据此把包路由给对局逻辑。
|
||||
- 身份字段 `agentid/gameid/playerid/roomcode/seat` 是对局操作的通用前缀(约定俗成,非框架强制)。
|
||||
- 发送/接收的双层包装、心跳、握手规则与平台层**完全相同**(见 01 章)。
|
||||
- 框架不提供 `Net.Send_<game_rpc>` 之类的封装,子游戏直接调 `Net._SendData` 上行;**具体 rpc 名与字段不在框架职责内**。
|
||||
|
||||
## 4. 开战入口(框架侧)
|
||||
|
||||
框架共有三条进入 `Game_Modify.StartWar(_msg)` 的路径,**全部把整包 `_msg` 透传给子游戏**:
|
||||
|
||||
```
|
||||
入口 A · 房主主动开局
|
||||
房主点开始 → Net.Send_self_makewar(data) [route=room](09_Net.js:470-473)
|
||||
→ 服务器回 self_makewar → Net.self_makewar → Desk.self_makewar(_msg)
|
||||
→ Game_Modify.StartWar(_msg)(07_Desk.js:1018)
|
||||
|
||||
入口 B · 他人/自动开战广播
|
||||
服务器广播 other_makewar [route=room](09_Net.js:480-482)
|
||||
→ Desk.makewar(_msg) → Game_Modify.StartWar(_msg)(07_Desk.js:1057)
|
||||
|
||||
入口 C · 进房即已开战(断线/中途进房)
|
||||
Desk.self_join_room 中若 _msg.data.deskwar 为真:
|
||||
→ Desk.stage = 1 → Game_Modify.StartWar(_msg)(07_Desk.js:634-642)
|
||||
```
|
||||
|
||||
> 入口 C 是「进房自动开战」:玩家加入时这局已在打(`deskwar` 真),直接以整包 `_msg` 调 `StartWar`,无需再等 makewar 广播。
|
||||
|
||||
## 5. 进房分支与对局快照(self_join_room)
|
||||
|
||||
`Desk.self_join_room(_msg)`(`07_Desk.js:529` 起)是自己进房的总处理。`isbattle` 仅用于设置 `this.stage`(`07_Desk.js:324`),**不**决定重连。三种分支(`07_Desk.js:634-659`):
|
||||
|
||||
```js
|
||||
Game_Modify.myJoinRoom(_msg); // 总是先回调(07_Desk.js:632)
|
||||
|
||||
if (_msg.data.deskwar) { // ① 进房即已开战
|
||||
Desk.stage = 1;
|
||||
Game_Modify.StartWar(_msg); // 入口 C(07_Desk.js:642)
|
||||
} else {
|
||||
if (_msg.data.deskinfo) { // ② 未开战但有牌桌快照
|
||||
Desk.stage = 1;
|
||||
Game_Modify.DeskInfo(_msg.data.deskinfo); // 实参是 deskinfo,非整包(07_Desk.js:653)
|
||||
} else { // ③ 普通未开战
|
||||
Desk.stage = 0;
|
||||
GameUI.ShowStartScene();
|
||||
C_Player.ChangeExit(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `Game_Modify.DeskInfo` 收到的是 **`_msg.data.deskinfo`**(牌桌快照对象),**不是**整包 `_msg`。
|
||||
- `deskinfo` 内部结构由子游戏定义(应含当前轮次、各家手牌/明牌、出牌历史、分数等足以还原牌局的字段)。框架只负责把它原样递进去。
|
||||
|
||||
## 6. 重连恢复(deskinfo)
|
||||
|
||||
断线重连进房时的另一条恢复路径,在 `Desk.self_join_room` 进入主场景分支(`07_Desk.js:419-423`):
|
||||
|
||||
```js
|
||||
GameUI.mainSceneLoaded();
|
||||
if (_msg.data.deskinfo) { // 触发条件是 deskinfo 存在,不是 isbattle==1
|
||||
if (get_self(149,37,0,0,0) == 0 && !GameData.iscloseVideo) {
|
||||
Func.createRoom(); // 开视频相关
|
||||
}
|
||||
Game_Modify.Reconnect(_msg.data.deskinfo); // 实参是 deskinfo(07_Desk.js:423)
|
||||
}
|
||||
```
|
||||
|
||||
- **触发条件是 `_msg.data.deskinfo` 存在**,而非 `isbattle == 1`(旧文档此处有误)。
|
||||
- `Game_Modify.Reconnect` 的实参同样是 **`_msg.data.deskinfo`**,子游戏据此重建对局界面。
|
||||
|
||||
## 7. 结算与相关保留 rpc
|
||||
|
||||
- `RpcList.over_game = "over_game"`(`02_Const.js:29`):结算广播的保留 rpc 名。具体结算字段由子游戏在 `_ReceiveData` 中按自己的 route/rpc 处理,框架不解析。
|
||||
- `RpcList.agentserver_game = "agentserver_game"`(`02_Const.js:50`):对局数据在大厅(agent)服中转的保留 rpc 名。
|
||||
- 结算后资产更新走平台层既有推送(`update_bean` / `update_roomcard` 等,见 02/03 章),与对局协议分属两条链路。
|
||||
|
||||
## 8. 创建房间的双回调(create_room)
|
||||
|
||||
服务器一次 `create_room` 响应会**连续触发两个子游戏钩子**(`09_Net.js:111-119`):
|
||||
|
||||
```js
|
||||
Net.create_room = function(_msg){
|
||||
Desk.create_room(_msg);
|
||||
GameUI.EndLoad();
|
||||
Game_Modify.createRoom(_msg.data.roomtype, _msg.data.infinite); // 无条件调用
|
||||
if (Game_Modify.onCreateRoom) { // 带存在性守卫
|
||||
Game_Modify.onCreateRoom(_msg.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `createRoom(roomtype, infinite)`:**无条件**调用,传两个标量。
|
||||
- `onCreateRoom(data)`:**带 `if` 守卫**(子游戏可不实现),传整包 `data`。
|
||||
|
||||
## 9. 子游戏向框架暴露的钩子(接口清单)
|
||||
|
||||
新前端虽不用 JS 类,但**必须实现等价逻辑**——这些是框架流程的回调点(`01_SubGame/02_SubGame_Input.js`)。注意实参形态:
|
||||
|
||||
| 钩子 | 触发时机 | 实参 |
|
||||
|------|---------|------|
|
||||
| `_ReceiveData(msg)` | 收到游戏内(非平台层 route)数据包 | 整包 `_msg` |
|
||||
| `StartWar(msg)` | 开战(入口 A/B/C,见第 4 节) | 整包 `_msg` |
|
||||
| `Reconnect(deskinfo)` | 进房发现有 deskinfo,需恢复对局 | **`_msg.data.deskinfo`** |
|
||||
| `DeskInfo(deskinfo)` | 未开战自己加入但有牌桌快照 | **`_msg.data.deskinfo`** |
|
||||
| `createRoom(roomtype, infinite)` | 创建房间成功(无条件) | 两个标量 |
|
||||
| `onCreateRoom(data)` | 创建房间成功(带守卫,可选实现) | 整包 `data` |
|
||||
| `myJoinRoom(msg)` / `playerJoinRoom(seat)` / `playerLeaveRoom(seat)` | 进/离房 | — |
|
||||
| `myExitRoom(seat)` / `breakRoom()` | 自己退房 / 已开局退房 | — |
|
||||
| `playerOffline(seat)` / `playerOnline(seat)` | 玩家上下线 | — |
|
||||
| `playerphonestate(seat,type)` | 玩家电话状态 | — |
|
||||
| `onReady(seat)` | 玩家准备 | — |
|
||||
| `changeSeat(seat1,seat2)` | 换座 | — |
|
||||
| `onSurrender(msg)` | 投降回包 | — |
|
||||
| `Free(msg)` | 解散确认 | — |
|
||||
| `updateScene()` / `closeGameScene()` | 刷新/关闭游戏界面 | — |
|
||||
| `onEnterMainScene(roomtype)` / `onExitMainScene()` | 进/出主场景 | — |
|
||||
| `stopAllSounds()` | 关闭所有游戏声音 | — |
|
||||
|
||||
部分钩子需**返回**房间展示信息(创建/列表界面用):
|
||||
|
||||
| 钩子 | 返回 |
|
||||
|------|------|
|
||||
| `getRoomInfo(roomtype,type,tea)` | 房间描述(每行≤18字符) |
|
||||
| `getFullRoomInfo(roomtype)` | 完整房间描述(每行≤26字符) |
|
||||
| `getRoomTopDescAry(roomtype)` | 房间标题描述字符串数组 |
|
||||
| `getStarLimit(roomtype)` | 星星场准入下限 |
|
||||
| `getMult(roomtype,type)` | 星星场倍数 |
|
||||
| `getLeaveLimit(roomtype)` | 离场限制 |
|
||||
| `getVideoByRoomType(roomtype)` | 是否开视频 0/1 |
|
||||
| `getRoomMode(roomtype)` | 是否金币场 0/1 |
|
||||
|
||||
## 10. 外部桥接:H5 / 小程序唤起进房(deeplink)
|
||||
|
||||
**两套数据来源不同,勿混为一谈。**
|
||||
|
||||
### 10.1 H5:URL 参数 `gameData`
|
||||
|
||||
⚠️ 实际函数段约 `12_Logic.js:1900-1940`。
|
||||
|
||||
```js
|
||||
// 写入侧(生成 deeplink):Logic.setGameData(12_Logic.js:1900-1905)
|
||||
return encodeURI(JSON.stringify({ rpc: _rpc, data: _data }));
|
||||
|
||||
// 读取侧:Logic.getGameDataFromH5(12_Logic.js:1907-1918)
|
||||
var _data = fGetQuery("gameData");
|
||||
if (_data) {
|
||||
_data = decodeURI(_data);
|
||||
GameData.fromH5GameData = JSON.parse(_data); // payload 落到 fromH5GameData
|
||||
}
|
||||
```
|
||||
|
||||
payload 格式:
|
||||
|
||||
```json
|
||||
{ "rpc": "joinRoom", "data": { "roomcode": "<房号>" } }
|
||||
```
|
||||
|
||||
`h5RpcList.joinRoom = "joinRoom"`(`02_Const.js:112`)是目前唯一支持的桥接 rpc。
|
||||
|
||||
### 10.2 小程序:本地存储 `openminigamedata`(不是 URL 参数)
|
||||
|
||||
⚠️ 实际函数段约 `12_Logic.js:2406-2431`。**旧文档的 `miniProData` URL 参数在源码中根本不存在。** 小程序数据来自本地存储:
|
||||
|
||||
```js
|
||||
// Logic.getGameDataFromMiniPro(12_Logic.js:2406-2417)
|
||||
var _data = Utl.ReadData("openminigamedata"); // 读本地存储键 "openminigamedata"
|
||||
if (_data) {
|
||||
_data = decodeURI(_data);
|
||||
GameData.fromMiniProData = JSON.parse(_data); // payload 落到 fromMiniProData
|
||||
}
|
||||
```
|
||||
|
||||
(`openminigamedata` 会在进房等时机被清空,见 `07_Desk.js:283/456/531`。)
|
||||
|
||||
### 10.3 解析后并不直接发包,而是弹确认框
|
||||
|
||||
旧文档「解析后等价触发 `self_join_room`」是错的,且**与源码相反**——解析到 `joinRoom` 后是**弹确认框**,真正发包在用户点确认之后:
|
||||
|
||||
```js
|
||||
// H5:Logic.joinRoomFromH5(12_Logic.js:1919-1940)
|
||||
case h5RpcList.joinRoom:
|
||||
GameData.checkType = 5;
|
||||
GameUI.openCheck("是否进入房间?");
|
||||
//Net.Send_self_join_room(data); // ← 此处发包被注释,不在这里发
|
||||
|
||||
// 小程序:Logic.joinRoomFromMiniPro(12_Logic.js:2418-2431)
|
||||
case h5RpcList.joinRoom:
|
||||
GameData.checkType = 8;
|
||||
GameUI.openCheck("是否进入房间?");
|
||||
```
|
||||
|
||||
确认框点「确定」后在 `11_GameUI.js` 的 `checkType` 分发里真正发包:
|
||||
|
||||
```js
|
||||
// 11_GameUI.js openCheck 确认处理(约 1219 起的 switch(GameData.checkType))
|
||||
case 5: // H5 跳转进房确认(11_GameUI.js:1253-1261)
|
||||
data.roomcode = GameData.fromH5GameData.data.roomcode;
|
||||
Net.Send_self_join_room(data); // ← H5 真正发包点
|
||||
break;
|
||||
```
|
||||
|
||||
🐛 **小程序桥接存在 checkType 撞号 bug**:`joinRoomFromMiniPro` 设 `GameData.checkType = 8` 后弹确认框,但同一确认处理 switch 的 `case 8` 是「删除白名单玩家」`Net.Send_optWhiteList`(`11_GameUI.js:1278-1288`),**并非进房**。即小程序 deeplink 走确认框「确定」时不会发 `self_join_room`,反而触发白名单删除逻辑。(H5 的 `case 5` 正确。)新前端实现小程序唤起时应改用独立的进房 checkType,勿沿用 8。
|
||||
|
||||
## 11. gamemain.js:网络回调统一留空
|
||||
|
||||
`gamemain.js` 把引擎回调转发到框架;但 `tcpconnected` / `tcpmessage` / `tcpdisconnected` / `tcperror` / `httpmessage` **均为空体**(`gamemain.js:125-151`):
|
||||
|
||||
```js
|
||||
gameabc_face.tcpmessage = function(tcpid, data){ ; }; // 空
|
||||
gameabc_face.tcpdisconnected = function(tcpid){ }; // 空
|
||||
// ……其余网络回调同样为空
|
||||
```
|
||||
|
||||
> 对局包**统一从 `12_Logic.js` 的 `onmessage` → `_ReceiveData` 进入**,不走 gamemain 这些钩子。新前端无需在引擎层接网络。
|
||||
|
||||
## 12. 给 CocosCreator 新前端的落地清单(桥接部分)
|
||||
|
||||
1. **route 分发**:接收按 `route` 分流——`platform/agent/room` 查 `Net[rpc]` 表(带存在性守卫),否则进 `Game_Modify._ReceiveData(msg)`;唯一权威分发点对应 `12_Logic.js:258-263`。
|
||||
2. **发对局包**:复用统一出口 `Net._SendData("youle", <game_route>, <game_rpc>, data)`;具体 rpc/字段从目标子游戏工程或抓包补全。
|
||||
3. **开战入口**:实现 A(self_makewar) / B(other_makewar) / C(进房 deskwar 真) 三条都汇入 `StartWar(整包)`。
|
||||
4. **进房快照**:区分 `deskwar`(开战) / `deskinfo`(未开战有快照→`DeskInfo(deskinfo)`) / 普通三分支;重连恢复看 `data.deskinfo` 存在 → `Reconnect(deskinfo)`,**勿**用 `isbattle` 判断。
|
||||
5. **创建房间**:一个响应连发 `createRoom(roomtype,infinite)`(无条件) 与 `onCreateRoom(data)`(可选)。
|
||||
6. **外部桥接**:H5 读 `fGetQuery("gameData")` → `fromH5GameData`;小程序读本地存储 `openminigamedata` → `fromMiniProData`;payload 均为 `{rpc:"joinRoom",data:{roomcode}}`;解析后**弹确认框**,确认后才发 `self_join_room`。实现时给小程序用独立 checkType,避开源码的 case 8 撞号 bug。
|
||||
@@ -0,0 +1,237 @@
|
||||
# 06 · 子游戏开发模式(基于框架)与 CocosCreator 方案
|
||||
|
||||
> 本章讲**框架模板提供给子游戏的钩子契约 + 状态归属 + 子游戏开发模式**,并给出 CocosCreator 重写的两种设计方案。
|
||||
> **不涉及任何一款具体子游戏的对局算法/字段/专属模块**——凡涉及处统一标注"由各子游戏自定义"。
|
||||
> 模板工程以 `Game_Surface_3` 为基准,源码事实均以该工程为准。
|
||||
|
||||
---
|
||||
|
||||
## 1. 一个子游戏工程 = 模板壳 + 三处定制
|
||||
|
||||
对比模板 `Game_Surface_3`,真实子游戏的改动集中在固定几处,**平台层(`00_Surface/*`)几乎原样保留**:
|
||||
|
||||
| 部分 | 模板 | 子游戏 | 是否改动 |
|
||||
|------|------|--------|----------|
|
||||
| `js/00_Surface/*`(平台层) | 完整 | **几乎不动**(与服务器对接的协议层,保持兼容) | ❌ 基本不改 |
|
||||
| `js/01_SubGame/00_SubGame_Config.js` | 默认配置 | 改:人数/聊天气泡位/分享/声音/系统开关等 | ✅ 配置 |
|
||||
| `js/01_SubGame/01_SubGame_modify.js` | 含创建房间界面 + 战绩页,对局留空 | **填**:创建房间界面、战绩定制、对局界面相关 | ✅ 重写 |
|
||||
| `js/01_SubGame/02_SubGame_Input.js` | 全是空桩 | **填**:所有 `Game_Modify.*` 钩子的真实实现(含 `_ReceiveData`) | ✅ 重写 |
|
||||
| `js/gamemain.js` | 纯转发引擎回调,`tcpmessage` 等为空体 | **扩展**:对局精灵初始化、引擎定时器/动画回调驱动对局 | ✅ 定制 |
|
||||
| `js/*.js`(子游戏专属模块) | 无 | **新增**:玩法相关模块(牌型/出牌/理牌/回放等,**由各子游戏自定义**) | ✅ 新增 |
|
||||
| `js/class/*`(OOP 类库,可选) | 无 | **可选新增**:牌型/牌局/算法类库(**由各子游戏自定义**) | ✅ 新增 |
|
||||
| `output/*.min.js` | 模板界面 | **替换**:本游戏的精灵布局数据(编辑器导出) | ✅ 美术 |
|
||||
|
||||
> 一句话:**改服务器对接以上的那一层(`01_SubGame` 三件套 + `gamemain` + 专属模块 + 美术数据)就得到一款新游戏,平台层和协议不动**。这正是"子游戏框架"的价值,也是"服务器零改动"的根因。
|
||||
|
||||
### 加载顺序
|
||||
|
||||
```
|
||||
引擎(gameabc) + Spine
|
||||
→ 00_Surface/00..12 平台层(与模板一致,含 Desk/Net/GameUI/Logic 等)
|
||||
→ 01_SubGame/00,01,02 子游戏三件套
|
||||
→ gamemain.js 子游戏定制的引擎回调
|
||||
→ 专属模块 玩法相关模块(由各子游戏自定义)
|
||||
→ class/* 类库(可选,由各子游戏自定义)
|
||||
→ output/*.min.js 美术布局数据
|
||||
```
|
||||
后加载的 `01_SubGame/02_SubGame_Input.js` 用**真实实现覆盖**了平台默认的 `Game_Modify` 空桩。
|
||||
|
||||
---
|
||||
|
||||
## 2. 子游戏 ↔ 框架的契约(最重要)
|
||||
|
||||
### 2.1 框架 → 子游戏:回调钩子(平台在关键时机调用)
|
||||
|
||||
子游戏在 `02_SubGame_Input.js` 实现这些 `Game_Modify.*` 钩子,模板里它们全是空桩(或仅 `console.log`);平台层在固定时机调用它们,子游戏据此驱动对局界面与状态。下表的"平台调用点"以模板工程 `Game_Surface_3` 的源码为准(`文件:行号`),子游戏内部的具体实现"**由各子游戏自定义**"。
|
||||
|
||||
> 方向均为 **平台 → 子游戏调用**(框架在内部回调子游戏实现的钩子)。
|
||||
|
||||
| 钩子(签名) | 触发时机 / 平台调用点(文件:行) | 说明 |
|
||||
|------|------|------|
|
||||
| `_ReceiveData(_msg)` | 收到对局自定义包时(`12_Logic.js:263`、`09_Net.js:36`) | 对局包入口。子游戏内 `switch(_msg.rpc)` 自分发并刷新对局,rpc 集合由各子游戏自定义 |
|
||||
| `StartWar(_msg)` | 开战(`07_Desk.js:642 / 1018 / 1057 / 1141`) | 开局:初始化牌桌、起手发牌等 |
|
||||
| `Reconnect(_deskinfo)` | 进房响应**含 `deskinfo`** 时(`07_Desk.js:423`) | 断线重连还原对局快照。**触发条件是响应含 `deskinfo`**(见 §2.4);模板为空实现,还原逻辑由各子游戏自定义 |
|
||||
| `DeskInfo(_msg)` | 未开战状态下自己加入、携带牌桌数据时(`07_Desk.js:653`) | 同步未开战阶段的牌桌信息 |
|
||||
| `createRoom(_roomtype,_infinite)` | 收到创建房间回包(`09_Net.js:115`) | 重置对局变量、铺座位等 |
|
||||
| `onCreateRoom(_data)` | 创建房间成功后(`09_Net.js:117`,可选钩子) | 创建房间成功后的子游戏处理 |
|
||||
| `myJoinRoom(_msg)` | 自己进入房间(`07_Desk.js:632`) | 自己入座后的初始化 |
|
||||
| `playerJoinRoom(seat)` | 其他玩家加入(`07_Desk.js:736`) | 同步该座位玩家显示 |
|
||||
| `playerLeaveRoom(seat)` | 其他玩家离开(`07_Desk.js:801`) | 清理该座位显示 |
|
||||
| `myExitRoom(seat)` | 自己退出房间(`07_Desk.js:766`,可选钩子) | 自己离场处理 |
|
||||
| `breakRoom()` | 已开局情况下自己退出(`07_Desk.js:756`) | 开局中退出的清理 |
|
||||
| `playerOffline(seat)` | 玩家离线(`07_Desk.js:891`) | 标记离线态 |
|
||||
| `playerOnline(seat)` | 玩家上线(`07_Desk.js:898`) | 标记在线态 |
|
||||
| `playerphonestate(seat,type)` | 玩家电话状态(`07_Desk.js:974`/`984`,`type` 1=挂断、0=通话/来电) | 电话状态显示 |
|
||||
| `onReady(seat)` | 玩家准备(`07_Desk.js:1136`) | 同步准备态 |
|
||||
| `changeSeat(seat1,seat2)` | 收到换座包(`07_Desk.js:191`,可选钩子) | 同步换座后界面 |
|
||||
| `onSurrender(_msg)` | 收到投降回包(`09_Net.js:621`) | 投降结果处理 |
|
||||
| `Free(_msg)` | 投票解散同意后确认时(`07_Desk.js:873`、`11_GameUI.js:5026`) | 解散结算分支 |
|
||||
| `updateScene()` | 按本地状态重绘界面(`05_Func.js:1642 / 2922`,可选钩子) | 断线恢复 / 切前台时重绘整个对局界面 |
|
||||
| `closeGameScene()` | 关闭游戏界面(`07_Desk.js:427`) | 退出对局界面 |
|
||||
| `onEnterMainScene(roomtype)` | 进入游戏主场景(`11_GameUI.js:4795`) | 进入主场景 |
|
||||
| `onExitMainScene()` | 退出游戏主场景(`11_GameUI.js:3901`) | 退出主场景 |
|
||||
| `onMainMenuScene()` | 显示大厅界面(`11_GameUI.js:3904`,可选钩子) | 回到大厅 |
|
||||
| `onCreateDesk(roomtype)` | 进入游戏界面创建牌桌之前(`12_Logic.js:2125`) | 创建牌桌前的子游戏准备 |
|
||||
| `onGameConfig(_gameConfig)` | 获取到游戏配置时(`12_Logic.js:1825`,仅 game_config 有数据时调用) | 接收服务器下发的游戏配置 |
|
||||
| `onEnterVideo()` | 进入牌局回放时(`08_Utl_Output.js:593`) | 回放入口 |
|
||||
| `calResult(inputArr)` | 倍率结算面板确认(`11_GameUI.js:6556`,参数为倍率数组) | 结算计算回调 |
|
||||
| `onOpenHelp(spid)` | 打开帮助页面(`11_GameUI.js:4720`) | 帮助页处理 |
|
||||
| `onCheckInput(_result)` | 数字输入框确认(`11_GameUI.js:7986`/`7988`) | 数字输入回调 |
|
||||
| `onLocationInfo(_locationInfo)` | 成功获取定位信息(`05_Func.js:2063 / 3087`,可选钩子) | 定位信息回调 |
|
||||
| `onCloseVip()` | 关闭 vip 选项时(`11_GameUI.js:7436`,可选钩子) | 关闭 vip 处理 |
|
||||
| `getShareRoom(_msg)` | 收到星星场(分享房)信息时(`07_Desk.js:1160`,可选钩子) | 星星场房间处理 |
|
||||
| `stopAllSounds()` | 需要静音对局声音时(`05_Func.js:1658 / 2934`、`07_Desk.js:718 / 725 / 772`) | 关闭子游戏声音 |
|
||||
| `shakeEvent()` | 摇一摇事件(`05_Func.js:1958 / 3049`) | 模板示例里据 `GameData.shakeID` 走 `Net.Send_self_makewar()` 开战 |
|
||||
|
||||
**返回类钩子**(平台据返回值渲染大厅 / 房间列表,返回内容**由各子游戏自定义**):
|
||||
|
||||
| 钩子(签名) | 平台调用点(文件:行) | 返回值含义 |
|
||||
|------|------|------|
|
||||
| `getRoomInfo(roomtype,type,tea)` | `11_GameUI.js:6942` | 房间描述文本(`type` 1=系统房间、2=非系统房间) |
|
||||
| `getFullRoomInfo(roomtype)` | `11_GameUI.js:7479 / 9284 / 9337` | 房间全部信息描述文本 |
|
||||
| `getRoomTopDescAry(roomtype)` | `11_GameUI.js:8951` | 房间顶部一组描述(字符数组) |
|
||||
| `getStarLimit(roomtype)` | `11_GameUI.js:6874 / 7167 …` | 星星场准入下限 |
|
||||
| `getMult(roomtype,type)` | `11_GameUI.js:6823 / 6921 / 7168` | 星星场倍数 |
|
||||
| `getLeaveLimit(roomtype)` | `11_GameUI.js:6948` | 离场限制 |
|
||||
| `getVideoByRoomType(roomtype)` | 据 roomtype 决定是否开视频(模板内当前为注释状态) | 0=不开、1=开 |
|
||||
| `getRoomMode(roomtype)` | `11_GameUI.js:1886 / 6721 / 7183 …` | 是否金币场(1/0) |
|
||||
|
||||
> `roomtype` 的具体取值、房间描述文案、各返回值的格式均"**由各子游戏自定义**",本框架文档不展开。
|
||||
|
||||
### 2.2 子游戏 → 框架:可调用的 API(契约清单)
|
||||
|
||||
这是 CocosCreator 重写时**必须在"平台 SDK"侧提供的接口**。子游戏通过这些接口读取框架态、发包、复用平台 UI:
|
||||
|
||||
| 类别 | API | 作用 |
|
||||
|------|-----|------|
|
||||
| **座位/身份** | `Utl.getMySeat()` | 我的绝对座位 |
|
||||
| | `Logic.ChangeToStatus(mySeat, targetSeat)` | 绝对座位 → 以我为视角的相对位(UI 摆位关键) |
|
||||
| | `C_Player.playerid` / `GameData.AgentId` / `GameData.GameId` | 身份 |
|
||||
| **房间状态** | `Desk.roomcode` / `Desk.roomtype` / `Desk.GetPlayerBySeat(seat)` / `Desk.PlayerList` | 房间/座位玩家数据 |
|
||||
| | `Utl.getIsInfinite()` / `Utl.getIsDebugger()` | 房间属性/调试 |
|
||||
| | `Utl.setDeskStage(...)` | 设置牌桌阶段 |
|
||||
| **座位展示** | `Utl.setGrade(seat, value)` | 设置某座积分显示 |
|
||||
| | `Utl.setPlayerPrepare(seat, ...)` / `Utl.getPlayerReadyState(seat)` | 准备态读写 |
|
||||
| **发包(平台 RPC)** | `Net.Send_*(data)` | 平台 RPC(开局/聊天/解散/创建房间…见 02/03 章) |
|
||||
| | `Net.Send_self_makewar(data)` | 主动开战(摇一摇等触发) |
|
||||
| **发包(对局自定义)** | `Net._SendData(_app, _route, _rpc, _data)` | **对局自定义包**(出牌等);`_app/_route/_rpc` 取值由各子游戏自定义 |
|
||||
| **平台 UI** | `GameUI.*`(弹窗/按钮/提示等) | 复用平台通用界面 |
|
||||
| **渲染引擎** | `set_self/get_self/set_group/play_ani/set_clip/ifast_*` | 直接操作精灵(见 00 章) |
|
||||
| **配置/全局** | `Game_Config.*` / `GameData.*` | 配置与全局态 |
|
||||
|
||||
> 平台 RPC 的发包入口与字段见 02/03 章;对局自定义包统一走 `Net._SendData(_app,_route,_rpc,_data)`,其 `_app/_route/_rpc` 与 `_data` 结构"**由各子游戏自定义**"。
|
||||
|
||||
### 2.3 状态归属:框架态 vs 子游戏态
|
||||
|
||||
| | 框架维护 | 子游戏维护 |
|
||||
|---|---------|-----------|
|
||||
| 对象 | `Desk`、`C_Player`、`GameData` | 自建命名空间(对局状态对象,**由各子游戏自定义**) |
|
||||
| 内容 | 房间/座位/玩家公共信息、连接、资产 | 纯对局数据(牌、轮次、结算明细等) |
|
||||
| 来源 | 平台协议(01–04 章) | 对局协议(`_ReceiveData` 收到的 `_msg.data`) + `deskinfo`(重连快照) |
|
||||
|
||||
- **框架态**:由 `00_Surface/*` 平台层统一维护,跟随平台协议(登录/大厅/房间/解散/资产)更新,子游戏**只读不写**。
|
||||
- **子游戏态**:子游戏自建命名空间存放纯对局数据,来源是对局协议包与重连快照 `deskinfo`,结构由各子游戏自定义。
|
||||
|
||||
### 2.4 deskinfo 与断线重连机制(与 05 章一致)
|
||||
|
||||
- **触发条件**:进房响应里**携带 `deskinfo` 字段**时,平台在 `07_Desk.js:423` 调用 `Game_Modify.Reconnect(_msg.data.deskinfo)`。判定依据是"响应含 `deskinfo`"(`07_Desk.js:419` 的 `if(_msg.data.deskinfo)`),**不是** `isbattle==1` 之类的标志位。
|
||||
- **deskinfo 含义**:`deskinfo` 是子游戏对局态的**完整快照**,由服务器在开战时记录、在玩家重连时回发;其内部结构属对局协议,"**由各子游戏自定义**",本框架文档不展开。
|
||||
- **模板实现**:模板里 `Game_Modify.Reconnect` 是**空桩**(`02_SubGame_Input.js`)。"用 `deskinfo` 还原对局状态"的具体逻辑**由各子游戏实现**,模板为空实现。
|
||||
- **新前端约束**:CocosCreator 重写时,对局态必须能从同一份 `deskinfo` 完整还原,才能与旧服务器的重连流程兼容。
|
||||
|
||||
---
|
||||
|
||||
## 3. 对局驱动机制(子游戏的"心跳")
|
||||
|
||||
子游戏不轮询,而是靠**引擎回调**推进,全部经 `gamemain.js`(`gameabc_face.*`)转发到子游戏模块:
|
||||
|
||||
- **`ontimer(...)`**:子游戏用精灵定时器(`set_self(spid, 57, 间隔ms)`)开启,在此驱动动画 / 回合节奏。具体定时器 spid 与节奏"**由各子游戏自定义**"。
|
||||
- **`ani_doend(id,sx,count,allend)`**:动画结束回调,用于串联动画链。
|
||||
- **`gamemydraw / gamemydrawbegin`**:精灵自绘与裁剪(手牌滚动、勾选标记等)。
|
||||
- **`mousedown / mouseup / mousemove`**:对局交互(选牌/滑动/出牌);模板 `gamemain.js` 已把这些引擎回调转发给 `GameUI`、`Game_Modify`、`gameCombat` 三方。
|
||||
- **`gamestart`**:`Logic.AppStart()` 后,子游戏在此把对局精灵组初始化(通常先隐藏)。
|
||||
|
||||
> 网络回调(`tcpmessage / tcpconnected / tcpdisconnected` 等)在 `gamemain.js` 里**保持为空体**——对局包统一从平台的 `_ReceiveData` 进入,不走引擎 TCP。模板 `gamemain.js` 的桥接职责就是"把引擎回调转发给平台/子游戏模块",本身不含业务逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 4. 子游戏开发的通用模式(小结)
|
||||
|
||||
| 维度 | 框架提供 | 子游戏负责 |
|
||||
|------|----------|------------|
|
||||
| 平台协议层 `00_Surface/*` | 完整、稳定,不动 | 只读消费 |
|
||||
| 三件套 `01_SubGame/*` | 空桩契约(钩子签名固定) | 填实现(接入点统一) |
|
||||
| `gamemain.js` | 引擎回调桥接(转发,`tcp*` 空体) | 扩展对局精灵初始化与驱动 |
|
||||
| 对局 rpc(`_ReceiveData`) | 提供入口 | `switch(_msg.rpc)` 自分发,rpc 集合自定义 |
|
||||
| 重连 | 提供 `deskinfo` 快照与 `Reconnect` 钩子 | 实现"快照 → 对局态"的还原 |
|
||||
| 结算 | 提供 `Free` / 倍率面板等通用 UI | 据玩法走结算分支 |
|
||||
| 专属模块 / 类库 / 美术 | 无 | 全部自定义 |
|
||||
|
||||
**结论**:换一款游戏 = 换 `01_SubGame` 三件套 + `gamemain` 定制 + 专属模块 + 美术数据;**框架与协议是稳定底座**。具体玩法(牌型、出牌规则、roomtype 取值、deskinfo 结构、专属模块拆分)均"**由各子游戏自定义**",不在本框架文档范围内。
|
||||
|
||||
---
|
||||
|
||||
## 5. CocosCreator 重写方案(设计建议)
|
||||
|
||||
目标不变:**服务器零改动**。因此无论哪种方案,"平台 SDK + 对局协议"的字节级一致是硬约束(01–05 章),可自由替换的是渲染与工程组织。本节为设计建议,须与前述源码事实(钩子契约、状态归属、deskinfo 重连机制)保持一致。
|
||||
|
||||
### 方案 A:忠实复刻(迁移成本低、风险小)
|
||||
|
||||
把原框架的分层直接映射到 Cocos:
|
||||
|
||||
```
|
||||
NetworkLayer WebSocket 封装:信封{app,route,rpc,data}+双层解包+心跳(01章)
|
||||
PlatformSDK 复刻 00_Surface:登录/大厅/房间/解散/聊天/资产(02–04章协议 1:1)
|
||||
├─ Net Send_*/收包分发,含 _SendData(app,route,rpc,data) 对局自定义包通道
|
||||
├─ Desk 房间/座位状态(04章)
|
||||
├─ Player C_Player(04章)
|
||||
└─ GameUI 大厅/房间通用界面(Cocos 场景/Prefab 重画,不复刻 spid)
|
||||
SubGameModule 对局模块,暴露与 Game_Modify 等价的钩子接口:
|
||||
_ReceiveData(msg) / StartWar / Reconnect(deskinfo) / DeskInfo /
|
||||
myJoinRoom / playerJoinRoom / onReady / Free / getRoomInfo... (§2.1 全套)
|
||||
EventBus 取代 gamemain.js 的统一分发(输入/帧/定时 → UI 与对局)
|
||||
```
|
||||
- 优点:与原游戏行为高度一致,便于逐个对照验证、对接旧 `deskinfo`。
|
||||
- 适合:先快速上线、再逐步现代化。
|
||||
|
||||
### 方案 B:现代化重构(推荐,更高效)
|
||||
|
||||
保留协议层不变,对局层用 Cocos 的能力重做:
|
||||
|
||||
1. **协议层 TypeScript 化**:把 RpcList / 字段定义成 TS `interface` 与枚举,收发用泛型包裹,编译期校验字段。
|
||||
```ts
|
||||
interface Envelope<T> { app: string; route: string; rpc: string; data: T }
|
||||
```
|
||||
2. **平台 SDK 做成独立模块/npm 包**:`NetClient`(连接/心跳/解包) + `PlatformApi`(login/room/...) + `RoomStore`(响应式房间态)。多款游戏复用,等价于原 `00_Surface`。
|
||||
3. **对局用 Cocos 组件化**:场景=Scene,玩家位/手牌/牌桌=Prefab+组件;动画用 Cocos Animation/tween 取代引擎定时器与 `play_ani`;骨骼继续用 Spine(原框架也用 Spine)。
|
||||
4. **对局接入用接口而非全局函数**:定义 `IGameModule { onReceive(rpc,data); onReconnect(deskinfo); render(state) }`,平台 SDK 通过它回调对局;对局通过注入的 `sdk` 调 `sendAction()/getMySeat()/seatToView()`。这与原框架 `Game_Modify.*` 钩子 + 子游戏调 `Net/Desk/Utl` 的边界一一对应。
|
||||
5. **状态驱动渲染**:对局态集中在一个 store(对应原框架的子游戏命名空间),`render(state)` 纯函数刷新。**重连**:响应含 `deskinfo` 时 `onReconnect(deskinfo)` 把快照灌入 store——与原框架"响应含 deskinfo 即触发 Reconnect"的判定一致(§2.4)。
|
||||
6. **座位视角换算保留**:实现 `seatToView(mySeat,target)` 等价 `Logic.ChangeToStatus`,对局只关心"相对位"。
|
||||
|
||||
建议工程结构:
|
||||
```
|
||||
assets/
|
||||
scripts/
|
||||
net/ NetClient, Envelope, heartbeat, reconnect
|
||||
platform/ PlatformApi(02/03章), RoomStore/PlayerStore(04章), 通用UI
|
||||
game/<玩法>/ GameModule(实现 IGameModule), state, view 组件, prefab
|
||||
core/ EventBus, seatUtil, types(协议 TS 定义)
|
||||
```
|
||||
|
||||
### 两方案对比
|
||||
|
||||
| | 方案 A 复刻 | 方案 B 现代化 |
|
||||
|---|-----------|--------------|
|
||||
| 协议兼容 | ✅ | ✅(不动协议) |
|
||||
| 上手速度 | 快 | 中 |
|
||||
| 多游戏复用 | 一般 | 强(SDK 独立) |
|
||||
| 可维护性/类型安全 | 一般 | 强 |
|
||||
| 适合 | 首款快速验证 | 长期多游戏平台 |
|
||||
|
||||
> **共同底线**:把"平台 SDK"和"对局模块"边界划清(即 §2.1 钩子契约 + §2.2 API 清单 + §2.4 重连机制),协议字段严格对齐 01–05 章。这样新前端对服务器而言与旧前端**不可区分**,即达成"完美适配、服务器零改动"。
|
||||
|
||||
---
|
||||
|
||||
## 6. 待补:具体对局字段
|
||||
|
||||
本章聚焦框架使用模式与钩子契约,**未列任何对局包字段**。某款子游戏的对局 rpc 详细字段(`StartWar/Free/deskinfo` 等结构)、roomtype 取值、专属模块划分等,均属该子游戏范畴;可在确定首款游戏后,对其 `_ReceiveData` 各 case 与发包点做一次专项提取,补入 05 章。
|
||||
@@ -0,0 +1,833 @@
|
||||
# 平台模板 RPC 全量清单
|
||||
|
||||
> 核对日期:2026-09-05;基线:codex/local-platform-login @ 556c47a。本文是后续联调的索引与覆盖快照,不替代 01–05 章的字段契约,也不表示清单内接口已经验收。
|
||||
|
||||
## 范围与计数
|
||||
|
||||
- 共 **86 个唯一名称**:旧 RpcList 的 **82 个**(重复赋值按名称去重),加表外 submit_error、show_message、refresh_task_state,以及 h5RpcList.joinRoom。
|
||||
- 这是 Game_Surface_3 平台模板的全量:包含大厅、房间、纯推送、历史/保留名、HTTP 特例和外部桥接。具体子游戏自定义 route/rpc 没有在模板中穷举定义,留到子游戏阶段;本轮不选择或接入真实子游戏。
|
||||
- 86 是索引名称数,不是 86 个可直接调用的 WebSocket 接口。submit_log 不另计;握手 @toconcon、心跳 @serverheartbeat 和 webserve-服务器未工作 是传输控制数据,不是业务 RPC。
|
||||
- platform 是合法路由名称,但本次模板发送扫描未发现独占 route=platform 的命名业务请求;不复制 agent/room 名称来凑第三份清单。纯推送的路由归档不是当前本地服务器抓包证明。
|
||||
|
||||
## 接入与验收状态口径
|
||||
|
||||
- **L**:本地真实 Cocos 启动、加载、游客登录已验收;仅 player_login。依据[验收记录](../architecture/current-status.md)。
|
||||
- **R**:新 PlatformRuntime 的严格平台分发表已接入(共 11 个,含 L),仅表示源码处理链存在;除 L 外,当前批次没有真实 RPC 成功验收证据。
|
||||
- **N**:不在新 PlatformRuntime 平台分发表中;不代表旧 H5 或旧 PlatformSession 完全没有实现。协议常量存在、旧 room-handlers 存在,均不能算新 Runtime 已接入。
|
||||
- **X**:保留、HTTP 特例或本地桥接,不按普通 WS 平台命令判定。具体子游戏生命周期支持也不等于其某个对局 rpc 已实现。
|
||||
- 当前 LocalPlatformLogin 联调入口只允许发 agent/player_login;R 中的房间/切服条目仍受测试边界阻断。本文不放宽测试入口。
|
||||
- 条目中“旧发送/接收函数”只表示静态定义,不能推断调用点活跃或服务器确实响应。请求/响应摘要取自对应协议章节;未明确之处保留待核实,不补字段。
|
||||
|
||||
## 发现的差异与注意事项
|
||||
|
||||
1. refresh_task_state:当前旧源码引用未定义常量;02 章称硬编码字符串,已不符。新 routes.ts 也未收录。
|
||||
2. submit_error、show_message:旧路径确有硬编码发送/命名接收器,但不在旧 RpcList,也不在新 routes.ts;不能只扫常量表。
|
||||
3. submit_log:实际 rpc 仍为 submit_error;change_room 的响应为 change_seat;quick_enter_share_room 没有同名接收器。
|
||||
4. README 仍写 WS 双层包装,而 01 章已更正为单层 {app,route,rpc,data};MessageEvent.data 是浏览器事件壳。以 01 章与已验收传输实现为准。
|
||||
5. 02 章登录 version:string 与本地已验收 number 值有差异;openid/unionid 不得依据历史“游客可空”注释省略。
|
||||
6. 新 routes.ts 的注释分组不是权威 rpc→route 映射,例如 create_room/self_join_room/get_player_grade1/get_player_grade2 在 room 注释区域,但旧发送实际为 agent;本清单按协议与发送源码归档。
|
||||
7. can_award 的旧接收器调用未定义 C_Player.can_award;五个社交保留名没有旧 Net 接收器;不能按完整可用协议验收。
|
||||
8. update_roomcard/update_bean:05_Func.js 的原生 yPaytype 与 WVJB yPaytype 回调直接上行,02 章“仅接收、无主动请求”不完整;已在条目中补齐,不能划为只读接口。
|
||||
9. 本轮仅整理清单,未修改上述历史文档、业务代码、服务端或任何 Cocos 序列化资源。
|
||||
|
||||
## 配置来源与批次边界
|
||||
|
||||
- agentid/gameid/channelid/marketid/version 继续由现有配置来源经 runtime-config/local-startup 解析;各 RPC 只携带它所要求的字段,不能为统一外观给所有请求塞上整套身份。规范字段名为 channelid,不能写 chanelid。
|
||||
- openid/unionid 与游客资料由现有 visitor-account 来源读取缓存或生成;登录后的 playerid 来自服务器与 Store。查询目标 playerid 与当前登录 playerid 的语义要逐项区分。
|
||||
- roomcode/seat/shortcode/roomtype/deskinfo 等属于后续房间或子游戏范围;短信、原生数据、支付渠道等有各自前置条件,不能生成假值补齐。
|
||||
- 后续优先梳理非房间平台 RPC 的只读查询,再逐项处理写操作和推送;房间相关及子游戏最后进行。列入本清单不等于授权执行。
|
||||
|
||||
## 按功能快速浏览
|
||||
|
||||
- **大厅 · 登录 / 账号(6)**:`player_login`、`query_player2`、`binding_phone`、`send_phone_checkcode`、`send_phone_code_wechat`、`setSign`。
|
||||
- **大厅 · 房间创建 / 进入(大厅侧)(8)**:`create_room`、`self_join_room`、`quick_enter_share_room`、`advanced_roomlist`、`advanced_createroom`、`get_share_room`、`getInfoByShortCode`、`switchRoomList`。
|
||||
- **大厅 · 战绩 / 排行 / 财富(5)**:`get_player_grade1`、`get_player_grade2`、`get_treasurelist`、`getShortCodeRankList`、`getVipRankList`。
|
||||
- **大厅 · 任务系统(5)**:`get_player_task`、`player_finish_task`、`get_task_award`、`refresh_task_state`、`can_award`。
|
||||
- **大厅 · 支付 / 充值 / 资产(4)**:`get_paylist`、`pay_succ`、`topup_card`、`giveCoin`。
|
||||
- **大厅 · 仓库 / 星星 / 魅力(4)**:`set_bankpwd`、`change_star`、`update_charm`、`setAllCharm`。
|
||||
- **大厅 · 邀请码 / 绑定(2)**:`binding_invitecode`、`get_player_invitecode`。
|
||||
- **大厅 · VIP 管理 / 黑白名单(4)**:`optBanList`、`getPlayerWhiteList`、`optWhiteList`、`setVipForbidSelect`。
|
||||
- **大厅 · 其它 agent 协议(8)**:`submit_opinion`、`submit_location`、`submit_phoneinfo`、`submit_error`、`kick_server`、`broadcast`、`connect_agentserver`、`playerBehavior`。
|
||||
- **房间 · 房间生命周期(8)**:`self_break_room`、`other_break_room`、`self_exit_room`、`other_exit_room`、`player_prepare`、`change_room`、`share_room`、`change_seat`。
|
||||
- **房间 · 进房推送(他人视角)(3)**:`other_join_room`、`other_offline`、`other_online`。
|
||||
- **房间 · 解散投票(开局后)(8)**:`self_apply_free_room`、`other_apply_free_room`、`self_agree_free_room`、`other_agree_free_room`、`self_refuse_free_room`、`other_refuse_free_room`、`free_room`、`beanroom_surrender`。
|
||||
- **房间 · 开局(2)**:`self_makewar`、`other_makewar`。
|
||||
- **房间 · 房间内社交(6)**:`send_text`、`send_voice`、`send_gift`、`send_phiz`、`call_phone`、`hangup_phone`。
|
||||
- **保留名称 · 房间社交(5)**:`receive_chat`、`play_voice`、`other_send_gift`、`other_callphone`、`other_hangup`。
|
||||
- **大厅 · 资产更新与系统推送(4)**:`update_bean`、`update_roomcard`、`kick_offline`、`show_message`。
|
||||
- **房间 · 服务器切换(room 侧)(1)**:`connect_roomserver`。
|
||||
- **子游戏 · 框架保留名称(2)**:`over_game`、`agentserver_game`。
|
||||
- **外部桥接 · 非网络 RPC(1)**:`joinRoom`。
|
||||
|
||||
## 全量条目
|
||||
|
||||
### 大厅 · 登录 / 账号(6)
|
||||
|
||||
#### 01. player_login
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**L / R**;新 Rpc 常量:有。
|
||||
- 用途:连接建立后 `onopen` 自动发送;断线重连、切服后重发(12_Logic.js:35、:61 构造,09_Net.js:122 `Send_login` 注入)。
|
||||
- 请求:构建身份 agentid/gameid/channelid/marketid/version;账号 openid/unionid/nickname/avatar/sex/province/city;设备与环境 machineid/machineroom/location/ip;按来源契约携带设备登录和缓存 playerid 等可选字段。完整结构见登录契约与 02/04 章。
|
||||
- 响应/推送:state(0 成功);账号/资产字段,以及可能存在的房间恢复快照。完整结构见 04 章与当前 login-contract;本批只验收不在房的成功登录。
|
||||
- 注意:真实本地启动→加载→游客登录已验收;version=1(number)已被本地服务器接受。02 章 version:string、游客 openid 可空的描述不能作为本地联调输入依据;本地服务要求 openid 与 unionid。房间恢复不在已验收范围。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_login、player_login。
|
||||
|
||||
#### 02. query_player2
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:仓库转账前查询目标玩家信息。
|
||||
- 请求:`agentid`、`playerid`(目标玩家ID)。
|
||||
- 响应/推送:`avatar`(string)、`nickname`(string)、`playerid`(int);昵称/头像均空时提示"未找到对应玩家"。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_query_player2、query_player2。
|
||||
|
||||
#### 03. binding_phone
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:绑定手机号。
|
||||
- 请求:`agentid`、`playerid`、`phonenum`、`smmcode`(短信验证码)。
|
||||
- 响应/推送:`phonenum`(string,回写 `C_Player.tel`)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_binding_phone、binding_phone。
|
||||
|
||||
#### 04. send_phone_checkcode
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:发送手机短信验证码。
|
||||
- 请求:`agentid`、`phonenum`(构造点未集中定位,至少含手机号)。
|
||||
- 响应/推送:无业务字段(`Desk.send_phone_checkcode` 为空实现,07_Desk.js:1342)。
|
||||
- 注意:⚠️ 请求字段以后端实现为准。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_send_phone_checkcode、send_phone_checkcode。
|
||||
|
||||
#### 05. send_phone_code_wechat
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:微信渠道发送手机验证码(绑定手机流程的另一入口)。
|
||||
- 请求:`agentid`、`phonenum`(唯一调用点 11_GameUI.js:2620-2623 当前被注释,按注释代码为 agentid+phonenum)。
|
||||
- 响应/推送:无业务字段(`Desk.send_phone_code_wechat` 为空实现,07_Desk.js:1345)。
|
||||
- 注意:⚠️ 框架完整定义并接线,但**当前前端唯一调用点(11_GameUI.js:2623)被注释,实际不发送**;字段以后端实现为准。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_send_phone_code_wechat、send_phone_code_wechat。
|
||||
|
||||
#### 06. setSign
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:设置个性签名。
|
||||
- 请求:`agentid`、`playerid`、`sign`。
|
||||
- 响应/推送:`sign`(string,回显写入 `C_Player.sign`)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_setSign、setSign。
|
||||
|
||||
### 大厅 · 房间创建 / 进入(大厅侧)(8)
|
||||
|
||||
#### 07. create_room
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:玩家创建房间。
|
||||
- 请求:`agentid`、`playerid`、`gameid`、`roomtype`(房间类型配置数组,见 doc04);`Send_create_room` 自动注入 `ip`=`C_Player.ip`、`location`=`C_Player.addr`(09_Net.js:106-107)。
|
||||
- 响应/推送:`state`(0成功)、`roomcode`、`seat`、`roomtype`、`makewar`、`asetcount`、`shortcode`、`infinite`;失败 `showerror`/`error`。详见 [04 → 房间响应公共字段](04-数据结构.md#房间创建--进入响应公共字段)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_create_room、create_room。
|
||||
|
||||
#### 08. self_join_room
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**R**;新 Rpc 常量:有。
|
||||
- 用途:输入房号 / 快速加入 / H5 唤起 / 进 VIP 配置房。
|
||||
- 请求:`agentid`、`playerid`、`gameid`、`roomcode`(房号);`Send_self_join_room` 自动注入 `location`=`C_Player.addr`、`ip`=`C_Player.ip`(09_Net.js:254-255);进 VIP 配置房入口额外带 `vipMatch:1`(12_Logic.js:2171);比赛进房入口可带 `match_id`。
|
||||
- 响应/推送:`state`、`roomcode`、`seat`、`isowner`、`players[]`、`roomtype`、`makewar`、`asetcount`、`deskwar`、`deskinfo`(重连快照)。详见 [04 → 房间响应公共字段](04-数据结构.md#房间创建--进入响应公共字段)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_self_join_room、self_join_room。
|
||||
|
||||
#### 09. quick_enter_share_room
|
||||
|
||||
- 路由/通道:agent;方向:C→S;结果经其它 rpc 返回;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:快速进入分享/星星场房间。
|
||||
- 请求:`agentid`、`playerid`、`gameid`、`type`(房间类型)、`roomtype`(可选)。
|
||||
- 响应/推送:走 self_join_room 进房流程(无独立 `Net.quick_enter_share_room` 接收函数,结果通过 self_join_room/show_message 等回包)。
|
||||
- 注意:无 Net.quick_enter_share_room;关联 self_join_room/show_message,不能按同名请求响应配对。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_quick_enter_share_room。
|
||||
|
||||
#### 10. advanced_roomlist
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:拉取 VIP/高级房间列表。
|
||||
- 请求:`agentid`、`playerid`、`gameid`。
|
||||
- 响应/推送:房间列表对象,整包写入 `GameData.snrRoomList` 并渲染(07_Desk.js:1176)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_advanced_roomlist、advanced_roomlist。
|
||||
|
||||
#### 11. advanced_createroom
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:创建 VIP/高级房间。
|
||||
- 请求:`agentid`、`gameid`、`playerid`、`tea`(茶水费)、`infinite`(0/1无限局)、`roomtype`、`videoConfig`(可选)、`rebateLimit`(可选)、`rebateType`(可选)。
|
||||
- 响应/推送:`tea`、`rebateLimit` 及房间配置(整包写入 `GameData.snrRoomList`,并回拉 advanced_roomlist,07_Desk.js:1180)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_advanced_createroom、advanced_createroom。
|
||||
|
||||
#### 12. get_share_room
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:获取分享/星星场房间列表(仅非大厅环境发送)。
|
||||
- 请求:`agentid`、`playerid`、`gameid`。
|
||||
- 响应/推送:房间数组(`Desk.get_share_room` / `Game_Modify.getShareRoom` 处理)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_get_share_room、get_share_room。
|
||||
|
||||
#### 13. getInfoByShortCode
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:按短码批量查询房间信息(VIP 房列表)。
|
||||
- 请求:`agentid`、`gameid`、`shortcodeList`(短码列表)。
|
||||
- 响应/推送:`roomInfo`(短号房间信息) → `GameUI.setVipRoomListData`(07_Desk.js:1284)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_getInfoByShortCode、getInfoByShortCode。
|
||||
|
||||
#### 14. switchRoomList
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:开关房间在列表中的可见/可进入状态。
|
||||
- 请求:`agentid`、`playerid`、`gameid`、`isClose`(0开/1关)。
|
||||
- 响应/推送:`state`(0成功)、`isClose`;失败 `error`(07_Desk.js:1267)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_switchRoomList、switchRoomList。
|
||||
|
||||
### 大厅 · 战绩 / 排行 / 财富(5)
|
||||
|
||||
#### 15. get_player_grade1
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:拉取战绩(类型1)。
|
||||
- 请求:`agentid`、`playerid`、`gameid`、`type`(可选)、`direction`(可选,翻页)、`gradeidx`(可选,分页索引)。
|
||||
- 响应/推送:战绩数据(`gameCombat.get_player_grade1` 渲染)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_get_player_grade1、get_player_grade1。
|
||||
|
||||
#### 16. get_player_grade2
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:拉取战绩(类型2)。
|
||||
- 请求:透传调用方 `_data`,构造点未集中定位;至少含 `agentid`/`playerid`/`gameid`。
|
||||
- 响应/推送:战绩数据(`gameCombat.get_player_grade2` 渲染)。
|
||||
- 注意:⚠️ 请求字段构造点未定位,待核对。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_get_player_grade2、get_player_grade2。
|
||||
|
||||
#### 17. get_treasurelist
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:财富榜。
|
||||
- 请求:`agentid`、`gameid`。
|
||||
- 响应/推送:`list`(排行数组) → `Desk.get_treasurelist`。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_get_treasurelist、get_treasurelist。
|
||||
|
||||
#### 18. getShortCodeRankList
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:短号场排行榜。
|
||||
- 请求:`agentid`、`playerid`、`shortcode`。
|
||||
- 响应/推送:成功为排行数据(整包写入 `GameData.vipRank.data`);失败 `error:true` + `message`(07_Desk.js:1311)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_getShortCodeRankList、getShortCodeRankList。
|
||||
|
||||
#### 19. getVipRankList
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:VIP 排行榜。
|
||||
- 请求:`agentid`、`limit`(条数)。
|
||||
- 响应/推送:`list`(VIP排行数组) → `GameData.rankList`(07_Desk.js:1328)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_getVipRankList、getVipRankList。
|
||||
|
||||
### 大厅 · 任务系统(5)
|
||||
|
||||
#### 20. get_player_task
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:获取任务列表。
|
||||
- 请求:`agentid`、`playerid`、`gameid`。
|
||||
- 响应/推送:`tasks`(任务数组) → `Desk.get_player_task`。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_get_player_task、get_player_task。
|
||||
|
||||
#### 21. player_finish_task
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:上报任务完成(如分享成功触发,06_Player.js:414)。
|
||||
- 请求:`agentid`、`playerid`、`taskid`。
|
||||
- 响应/推送:`state`(int)——`state==1` 且当前 `taskstate==0` 时把 `C_Player.taskstate` 置 1(06_Player.js:476)。
|
||||
- 注意:原文档"响应含 `taskstate`"无源码依据,已删除——接收函数仅读 `_msg.data.state`。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_player_finish_task、player_finish_task。
|
||||
|
||||
#### 22. get_task_award
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:领取任务奖励。
|
||||
- 请求:`agentid`、`playerid`、`taskid`(任务领取点击与 Net 发送层均不补 gameid,见 02 文档源码复核说明)。
|
||||
- 响应/推送:`taskid`(对应任务 state 置 2=已领取)、`taskstate`(写入 `C_Player.taskstate`)(06_Player.js:482)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_get_task_award、get_task_award。
|
||||
|
||||
#### 23. refresh_task_state
|
||||
|
||||
- 路由/通道:agent;方向:C→S 意图;当前旧代码缺常量,不能认定可发送;状态:**N**;新 Rpc 常量:无。
|
||||
- 用途:刷新任务可领取状态。
|
||||
- 请求:透传 `_data`(构造点未集中定位,至少含 `agentid`/`playerid`)。
|
||||
- 响应/推送:服务器推送 `can_award`(见下条)。
|
||||
- 注意:本次核实:09_Net.js:440 用 RpcList.refresh_task_state,但 02_Const.js 无此定义;全模板搜索未找到赋值。未被外部注入时该值为 undefined,JSON.stringify 会省略 rpc。02 章“硬编码字符串”的记载与当前源码不符,待后续专项核对;本轮不修代码。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_can_award。
|
||||
|
||||
#### 24. can_award
|
||||
|
||||
- 路由/通道:agent;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:服务器通知有任务可领取。
|
||||
- 请求:无(纯推送)。
|
||||
- 响应/推送:任务可领取标记(载荷字段待后端确认)。
|
||||
- 注意:🐛 `06_Player.js` **未定义** `Player.prototype.can_award`(全文无此方法)。推送一旦到达,`C_Player.can_award` 为 undefined,调用即抛 TypeError。当前为 bug,不可当正常协议使用。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_can_award、can_award。
|
||||
|
||||
### 大厅 · 支付 / 充值 / 资产(4)
|
||||
|
||||
#### 25. get_paylist
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:拉取支付项列表。
|
||||
- 请求:`agentid`。
|
||||
- 响应/推送:`paylist`(支付项数组) → `GameData.payList`,并打开支付界面(09_Net.js:567)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_get_paylist、get_paylist。
|
||||
|
||||
#### 26. pay_succ
|
||||
|
||||
- 路由/通道:HTTP(保留 agent 信封);方向:C→HTTP;未见同名 Net 接收函数;状态:**X**;新 Rpc 常量:有。
|
||||
- 用途:支付成功后通知服务器入账。
|
||||
- 请求:`agentid`、`playerid`、`channelid`、`productid`、`payid`、`amount`、`money`、`paytype`(05_Func.js:2240-2254 构造)。
|
||||
- 响应/推送:无显式 WS 回包;资产变化通过 `update_bean`/`update_roomcard` 推送(充房卡场景前端还会本地 `UpdateRoomcard`,05_Func.js:2263)。
|
||||
- 注意:原工程支付流程使用 HTTP;存在 Send_pay_succ 定义,但调用点被注释。不得仅凭常量/发送函数判为活跃 WS 请求。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_pay_succ。
|
||||
|
||||
#### 27. topup_card
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:充值卡兑换。
|
||||
- 请求:`agentid`、`playerid`、`cardno`(卡号)。
|
||||
- 响应/推送:`Desk.topup_card` 为空实现(07_Desk.js:1365),资产变化经 update_bean/update_roomcard 推送。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_topup_card、topup_card。
|
||||
|
||||
#### 28. giveCoin
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:仓库面板向他人转账豆豆/金币。
|
||||
- 请求:`agentid`、`playerid`(转出)、`toPlayerid`(转入目标ID)、`gameid`、`count`(数量)、`password`(仓库密码)(11_GameUI.js:2132-2139)。
|
||||
- 响应/推送:`state`(0成功)、`star2`(转出后**仓库星星**数 → `setWareHouseStarCOunt`,**非豆豆**);失败 `showerror`/`error`(07_Desk.js:1381)。
|
||||
- 注意:响应 `star2` 为仓库星星数,注意区别于豆豆余额。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_giveCoin、giveCoin。
|
||||
|
||||
### 大厅 · 仓库 / 星星 / 魅力(4)
|
||||
|
||||
#### 29. set_bankpwd
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:设置仓库密码。
|
||||
- 请求:`agentid`、`playerid`、`unionid`、`password`。
|
||||
- 响应/推送:`state`(0成功)、`password`;失败 `showerror`/`error`(07_Desk.js:1228)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_set_bankpwd、set_bankpwd。
|
||||
|
||||
#### 30. change_star
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:仓库存/取(豆豆 ↔ 仓库星星)。
|
||||
- 请求:`agentid`、`playerid`、`mode`(0存入/1取出,源码 `safeInputType-1`)、`password`(仓库密码,字符串)、`count`(数量)(11_GameUI.js:2065-2069 调用点齐全携带此 5 字段)。
|
||||
- 响应/推送:`state`(0成功)、`star1`(更新后豆豆余额 → `update_bean2`)、`star2`(更新后仓库星星数 → `setWareHouseStarCOunt`)、`msg`(可选提示)、`count`(可选,回填安全输入);失败 `showerror`/`error`(07_Desk.js:1238)。
|
||||
- 注意:原审计疑虑"`mode`/`password` 未见"——经核对仓库存/取调用点(11_GameUI.js:2068-2069)**确含** `mode` 与 `password`,文档正确,疑虑解除。审计提到的"agentid/playerid/toPlayerid/gameid/count"实为相邻的 `giveCoin` 转账包(11_GameUI.js:2132-2139),并非本 rpc。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_change_star、change_star。
|
||||
|
||||
#### 31. update_charm
|
||||
|
||||
- 路由/通道:agent;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:座位魅力值更新。
|
||||
- 请求:`Net.Send_update_charm`(`09_Net.js:727`) 已定义但**无任何调用点**;实际只作服务器→客户端推送。
|
||||
- 响应/推送:`seatlist`:[ {`seat`(int), `charm`(number)} ](07_Desk.js:1260 遍历 setCharm)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_update_charm、update_charm。
|
||||
|
||||
#### 32. setAllCharm
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:批量设置总魅力。
|
||||
- 请求:`agentid`、`playerid`、`gameid`、`value`(11_GameUI.js:1269-1275)。
|
||||
- 响应/推送:无显式业务字段,`Desk.setAllCharm` 仅本地存储并提示成功(07_Desk.js:1321)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_setAllCharm、setAllCharm。
|
||||
|
||||
### 大厅 · 邀请码 / 绑定(2)
|
||||
|
||||
#### 33. binding_invitecode
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:绑定邀请码。
|
||||
- 请求:`agentid`、`playerid`、`invitecode`(11_GameUI.js:1367-1369)。
|
||||
- 响应/推送:`state`(0成功时写入 `invitecode`)、`invitecode`、`error`(提示文案)(06_Player.js:251)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_binding_invitecode、binding_invitecode。
|
||||
|
||||
#### 34. get_player_invitecode
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:获取自己的邀请码(打开绑定界面)。
|
||||
- 请求:`agentid`、`playerid`、`unionid`、`openid`(11_GameUI.js:1378-1382)。
|
||||
- 响应/推送:`invitecode`(string) → `C_Player.setInvitecod` 并打开绑定界面(09_Net.js:607)。
|
||||
- 注意:修正原文档——请求字段为 `agentid/playerid/unionid/openid`,**无 `gameid`**,新增 `unionid`/`openid`(11_GameUI.js:1378)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_get_player_invitecode、get_player_invitecode。
|
||||
|
||||
### 大厅 · VIP 管理 / 黑白名单(4)
|
||||
|
||||
#### 35. optBanList
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:黑名单查看/添加/移除(多入口)。
|
||||
- 请求:随入口不同,公共字段 `agentid`、`playerid`、`type`:
|
||||
- 响应/推送:`type`(1/3/4/5/6)、`banList`(黑名单数组)、`message`(可选提示)(07_Desk.js:1289)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_optBanList、optBanList。
|
||||
|
||||
#### 36. getPlayerWhiteList
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:获取白名单。
|
||||
- 请求:`agentid`、`playerid`、`gameid`。
|
||||
- 响应/推送:`whiteList`(数组) → `GameData.whiteList.data`(07_Desk.js:1412)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_getPlayerWhiteList、getPlayerWhiteList。
|
||||
|
||||
#### 37. optWhiteList
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:白名单添加/修改/删除。
|
||||
- 请求:`agentid`、`playerid`、`gameid`、`shortcode`、`mode`、`userid`(目标ID):
|
||||
- 响应/推送:`whiteList`(更新后数组)、`mode`(可选)、`message`(可选提示)(07_Desk.js:1398)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_optWhiteList、optWhiteList。
|
||||
|
||||
#### 38. setVipForbidSelect
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:VIP 房禁止玩家选桌开关。
|
||||
- 请求:`agentid`、`playerid`、`gameid`、`forbidSelect`(1开/0关,11_GameUI.js:2631-2639)。
|
||||
- 响应/推送:`state`(0成功)、`forbidSelect`(1开/0关);失败 `error`(07_Desk.js:1348)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_setVipForbidSelect、setVipForbidSelect。
|
||||
|
||||
### 大厅 · 其它 agent 协议(8)
|
||||
|
||||
#### 39. submit_opinion
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:提交反馈/意见。
|
||||
- 请求:`agentid`、`playerid`、`gameid`、`content`。
|
||||
- 响应/推送:`state`(0成功)(07_Desk.js:1100)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_submit_opinion、submit_opinion。
|
||||
|
||||
#### 40. submit_location
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:提交定位信息(仅非大厅环境发送)。
|
||||
- 请求:`agentid`、`playerid`、`info`(定位对象)。
|
||||
- 响应/推送:经 `Game.submit_location` 处理(09_Net.js:583)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_submit_location、submit_location。
|
||||
|
||||
#### 41. submit_phoneinfo
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求(无响应处理);状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:提交手机/通讯录信息(仅非大厅环境发送)。
|
||||
- 请求:`agentid`、`playerid`、`info`{ `phoneInfo`(手机信息), `addrBook`(通讯录) }。
|
||||
- 响应/推送:`Net.submit_phoneinfo` 接收函数为空实现(09_Net.js:722)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_submit_phoneinfo、submit_phoneinfo。
|
||||
|
||||
#### 42. submit_error
|
||||
|
||||
- 路由/通道:agent;方向:C→S;同名回包被旧接收器忽略;状态:**N**;新 Rpc 常量:无。
|
||||
- 用途:异常/日志上报(Net.submit_error 与 Net.submit_log 共用同一个实际 rpc)。
|
||||
- 请求:`packet`(出错数据包字符串)、`msg`(错误/堆栈信息)、`playerid`、`agentid`、`gameid`(09_Net.js:66-72)。
|
||||
- 响应/推送:服务器若回 rpc `"submit_error"`,前端在分发处直接 `return` 忽略(12_Logic.js:235)。
|
||||
- 注意:submit_log 是本地函数别名,不是第二个线上 rpc;不能重复计数。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:submit_error、submit_log。
|
||||
|
||||
#### 43. kick_server
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 推送;状态:**R**;新 Rpc 常量:有。
|
||||
- 用途:管理端踢出玩家 / 被踢下线弹窗。
|
||||
- 请求:`agentid` 等(构造点未集中定位)。
|
||||
- 响应/推送:`msg`(踢出提示) → `GameUI.OpenKick`(07_Desk.js:1108)。
|
||||
- 注意:⚠️ 请求字段待补。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_kick_server、kick_server。
|
||||
|
||||
#### 44. broadcast
|
||||
|
||||
- 路由/通道:agent;方向:C→S 请求 + S→C 推送;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:广播消息/滚动公告(主要为服务器推送)。
|
||||
- 请求:`agentid` 等(`Send_broadcast` 存在,09_Net.js:530)。
|
||||
- 响应/推送:`msgtype`(0消息框/1滚动公告,可选,缺省 0)、`msgcontent`(内容)(07_Desk.js:1112)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_broadcast、broadcast。
|
||||
|
||||
#### 45. connect_agentserver
|
||||
|
||||
- 路由/通道:agent;方向:双向(切服);状态:**R**;新 Rpc 常量:有。
|
||||
- 用途:切换到大厅服务器。
|
||||
- 请求:`Send_connect_agentserver`(`09_Net.js:515`) 透传 `_data`(切服时使用)。
|
||||
- 响应/推送:`agentserver`(新大厅服地址)、`opt`(切换原因,如 `other_break_room`/`free_room`)(09_Net.js:518)。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_connect_agentserver、connect_agentserver。
|
||||
|
||||
#### 46. playerBehavior
|
||||
|
||||
- 路由/通道:HTTP GET(历史 agent 名称);方向:C→HTTP;状态:**X**;新 Rpc 常量:有。
|
||||
- 用途:玩家行为埋点。
|
||||
- 请求:原 WS 路径(agentid/gameid/playerid/tag)**被注释**(09_Net.js:799-806),实际改走独立 HTTP GET 上报 `http://test3.1888day.com/api/gamedo/gamedo?agentid=&gameid=&playerid=&tag=`(09_Net.js:807-815)。
|
||||
- 响应/推送:HTTP 回调 `playerBehavior_Succ`/`_Fail`(09_Net.js:818/822);WS 接收函数 `Net.playerBehavior`(`09_Net.js:826`) 当前无触发。
|
||||
- 注意:当前调用走独立 HTTP 埋点地址,WS 段被注释;不归入本地 WebSocket 验收。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:Send_playerBehavior、playerBehavior。
|
||||
|
||||
### 房间 · 房间生命周期(8)
|
||||
|
||||
#### 47. self_break_room
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:房主在**未开局**前主动解散房间。
|
||||
- 请求:仅通用四字段。
|
||||
- 响应/推送:`roomcode`(可选,用于从本地"我的房间"列表移除)。处理:清空牌桌、回大厅。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_self_break_room、self_break_room。
|
||||
|
||||
#### 48. other_break_room
|
||||
|
||||
- 路由/通道:room;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:他人(房主)解散房间,推送给房内其余玩家。
|
||||
- 请求:无(纯推送)。
|
||||
- 响应/推送:无业务字段;触发 `Func.exitRoom()`、清桌、回大厅、提示"房主已解散房间!"。
|
||||
- 注意:常伴随 **connect_agentserver** 切服包(`data.opt == other_break_room`,见下文)。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:other_break_room。
|
||||
|
||||
#### 49. self_exit_room
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 响应;状态:**R**;新 Rpc 常量:有。
|
||||
- 用途:自己在**未开局**前退出房间。
|
||||
- 请求:仅通用四字段(发送处见 `08_Utl_Output.js:993`)。
|
||||
- 响应/推送:`isowner`(可选,==1 且非无限局时把房间加回本地列表), `seat`(可选,传给 `Game_Modify.myExitRoom`), `roomcode`(可选)。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_self_exit_room、self_exit_room。
|
||||
|
||||
#### 50. other_exit_room
|
||||
|
||||
- 路由/通道:room;方向:S→C 推送;状态:**R**;新 Rpc 常量:有。
|
||||
- 用途:其他玩家(未开局)退出房间。
|
||||
- 请求:无(纯推送)。
|
||||
- 响应/推送:`seat`(离开者座位)。处理:清该座位、`playercnt--`;若 `seat==0` 且非无限局,提示房主已离开。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:other_exit_room。
|
||||
|
||||
#### 51. player_prepare
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 推送;状态:**R**;新 Rpc 常量:有。
|
||||
- 用途:玩家点击准备(`needprepare==1` 的房间)。
|
||||
- 请求:仅通用四字段。
|
||||
- 响应/推送:`seat`(准备者座位), `deskwar`(可选;为真表示满足开战条件 → `HideStartScene` + `Game_Modify.StartWar`)。推送给房内所有人。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_player_prepare、player_prepare。
|
||||
|
||||
#### 52. change_room
|
||||
|
||||
- 路由/通道:room;方向:C→S;响应 rpc=change_seat;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:玩家请求换座 / 换桌。
|
||||
- 请求:仅通用四字段(**不带目标座位**,由服务器决定换到哪个空位)。发送处 `11_GameUI.js:1945`、`08_Utl_Output.js:1006`。
|
||||
- 响应/推送:rpc 名改为 **`change_seat`**,`data`:`seat1`, `seat2`(两个互换的座位号)。处理:交换两座 Desk 信息,若自己在其中则 `C_Player.SetSeat` 更新。
|
||||
- 注意:上行 rpc=`change_room`,下行 rpc=`change_seat`,二者成对。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_change_room。
|
||||
|
||||
#### 53. share_room
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:把房间分享到世界房列表。
|
||||
- 请求:通用四字段;可选 `roomlist`、`roomtype`、`shareType`(高级房分享时携带,见 `11_GameUI.js:1734`/`11_GameUI.js:1891`)。
|
||||
- 响应/推送:无业务字段;提示"已成功分享至平台!"。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_share_room、share_room。
|
||||
|
||||
#### 54. change_seat
|
||||
|
||||
- 路由/通道:room;方向:S→C(change_room 的响应);状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:服务器指定两座互换。
|
||||
- 请求:无同名上行;上行是 change_room。
|
||||
- 响应/推送:seat1、seat2。
|
||||
- 注意:独立列名,不能把 change_room 与 change_seat 合成一条而漏计。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:change_seat。
|
||||
|
||||
### 房间 · 进房推送(他人视角)(3)
|
||||
|
||||
#### 55. other_join_room
|
||||
|
||||
- 路由/通道:room;方向:S→C 推送;状态:**R**;新 Rpc 常量:有。
|
||||
- 用途:其他玩家加入当前房间。
|
||||
- 请求:无(纯推送;自己进房用 `self_join_room`,走 agent 路由,见 02 章)。
|
||||
- 响应/推送:`seat`(新玩家座位) + 该玩家完整对象(直接传给 `Player.SetDeskInfo`,见 [04 · Player 座位对象](04-数据结构.md#player-座位对象)):
|
||||
- 注意:⚠️ `charm`/`sign` 是否每次必含、其余字段是否完整以 `Player.SetDeskInfo` 实际读取为准,详见 [04 章](04-数据结构.md#player-座位对象)。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:other_join_room。
|
||||
|
||||
#### 56. other_offline
|
||||
|
||||
- 路由/通道:room;方向:S→C 推送;状态:**R**;新 Rpc 常量:有。
|
||||
- 用途:其他玩家离线。
|
||||
- 请求:无(纯推送)。
|
||||
- 响应/推送:`seat`。处理:该座 `onstate=1`,刷新 UI。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:other_offline。
|
||||
|
||||
#### 57. other_online
|
||||
|
||||
- 路由/通道:room;方向:S→C 推送;状态:**R**;新 Rpc 常量:有。
|
||||
- 用途:其他玩家重新上线。
|
||||
- 请求:无(纯推送)。
|
||||
- 响应/推送:`seat`, `ip`。处理:该座 `onstate=0`、更新 ip。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:other_online。
|
||||
|
||||
### 房间 · 解散投票(开局后)(8)
|
||||
|
||||
#### 58. self_apply_free_room
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:自己申请解散房间。
|
||||
- 请求:仅通用四字段。
|
||||
- 响应/推送:`agreefree` { `state`:[各座同意状态数组], `countdown`:倒计时 }。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_self_apply_free_room、self_apply_free_room。
|
||||
|
||||
#### 59. other_apply_free_room
|
||||
|
||||
- 路由/通道:room;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:他人申请解散。
|
||||
- 请求:无(纯推送)。
|
||||
- 响应/推送:`seat`(申请者), `agreefree` { `state`[], `countdown` }。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:other_apply_free_room。
|
||||
|
||||
#### 60. self_agree_free_room
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:自己同意解散。
|
||||
- 请求:仅通用四字段。
|
||||
- 响应/推送:无业务字段;本地把自己加入同意列表、刷新投票 UI。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_self_agree_free_room、self_agree_free_room。
|
||||
|
||||
#### 61. other_agree_free_room
|
||||
|
||||
- 路由/通道:room;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:他人同意解散。
|
||||
- 请求:无(纯推送)。
|
||||
- 响应/推送:`seat`(同意者)。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:other_agree_free_room。
|
||||
|
||||
#### 62. self_refuse_free_room
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:自己拒绝解散。
|
||||
- 请求:仅通用四字段。
|
||||
- 响应/推送:无业务字段;清空同意列表、投票结果置不通过、弹"已拒绝"结果。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_self_refuse_free_room、self_refuse_free_room。
|
||||
|
||||
#### 63. other_refuse_free_room
|
||||
|
||||
- 路由/通道:room;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:他人拒绝解散(任一人拒绝即否决本轮)。
|
||||
- 请求:无(纯推送)。
|
||||
- 响应/推送:`seat`(拒绝者)。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:other_refuse_free_room。
|
||||
|
||||
#### 64. free_room
|
||||
|
||||
- 路由/通道:room;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:解散投票最终结果。
|
||||
- 请求:无(纯推送)。
|
||||
- 响应/推送:freeNow;deskfree、roomcard、tips、time、seats 按具体分支读取,字段语义见 03 章。
|
||||
- 注意:可伴随 **connect_agentserver** 切服包(`data.opt == free_room`,见下文)。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:free_room。
|
||||
|
||||
#### 65. beanroom_surrender
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:豆豆房(金币房)投降。
|
||||
- 请求:通用四字段 + `count`(投降数量 = `GameData.surrendCount`,发送处 `11_GameUI.js:1245`)。
|
||||
- 响应/推送:`state`(0=成功 → `Game_Modify.onSurrender(_msg)`); 失败时 `showerror`(==1 则弹) / `error`(错误文本)。
|
||||
- 注意:⚠️ 未见成对的 `other_xxx` 推送,是否向房内其他玩家广播他人投降待后端确认。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_beanroom_surrender、beanroom_surrender。
|
||||
|
||||
### 房间 · 开局(2)
|
||||
|
||||
#### 66. self_makewar
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 → S→C 响应(self_makewar);状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:房主主动开局。
|
||||
- 请求:仅通用四字段。
|
||||
- 响应/推送:rpc=`self_makewar`,无业务字段;触发 `Desk.self_makewar` → `Game_Modify.StartWar(_msg)`。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_self_makewar、self_makewar。
|
||||
|
||||
#### 67. other_makewar
|
||||
|
||||
- 路由/通道:room;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:开局广播(房主开局或满员自动开局)给房内其他玩家。
|
||||
- 请求:无(纯推送)。
|
||||
- 响应/推送:开局信息对象 → `Desk.makewar` → `Game_Modify.StartWar(msg)`。
|
||||
- 注意:实际发牌等对局数据在此之后通过**子游戏内协议**下发(见 05 章)。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:other_makewar。
|
||||
|
||||
### 房间 · 房间内社交(6)
|
||||
|
||||
#### 68. send_text
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 推送 · 文字聊天;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:文字聊天 / 全服公告 / 预定义常用语。
|
||||
- 请求:通用四字段 + `text`(内容;点常用语时为 `Game_Config.Info.TextContent[spid-206]` 文本) + `type`(0=普通 / 1=全服公告;按是否勾选公告设 0/1) + `info`(可选;**有 info 时 `type` 改为 2**)。发送处 `11_GameUI.js:1170`(输入框)、`11_GameUI.js:2717`(常用语)。
|
||||
- 响应/推送:`type`(0=普通 / 1=全服公告 / 2=机器人 / 3=预定义文字), `text`(内容;**type==3 时为 `Game_Config.Info.TextContent` 的 1 基索引**,客户端按 `(idx-1)%len` 取文本), `seat`(发送者座位,type≠1 时使用), `info`(type==2 时附加)。
|
||||
- 注意:百人场(`vipInfinite`)只处理 type==1 公告分支。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_send_text、send_text。
|
||||
|
||||
#### 69. send_voice
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 推送 · 语音;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:发送语音消息。
|
||||
- 请求:通用四字段 + `voiceurl`(已上传音频地址) + `time`(时长) + `info`(可选) + `type`(可选,有 info 时为 2)。发送处 `05_Func.js:1714`、`05_Func.js:2984`。
|
||||
- 响应/推送:`type`(0=普通 / 2=机器人), `seat`(发送座位), `voiceurl`, `time`, `info`(type==2 时)。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_send_voice、send_voice。
|
||||
|
||||
#### 70. send_gift
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 推送 · 互动/送礼;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:向指定座位送互动礼物。
|
||||
- 请求:通用四字段 + `giftid`(=`spid_up - 255`,按钮精灵号算出,发送处 `11_GameUI.js:2725`) + `receiveseat`(=`GameData.InteractPlayer`) + `info`(可选) + `type`(有 info 时为 2)。
|
||||
- 响应/推送:`type`(0=普通 / 2=机器人), `giftid`(客户端做 `(giftid-1)%4+1` 归一为 1~4 动画), `sendseat`(发送座位), `receiveseat`(接收座位), `info`(type==2 时)。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_send_gift、send_gift。
|
||||
|
||||
#### 71. send_phiz
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 推送 · 表情;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:发送表情动画。
|
||||
- 请求:通用四字段 + `text`(=`up_id`,表情按钮序号 1~`ConstVal.Emotion.count`,发送处 `11_GameUI.js:815`) + `info`(可选) + `type`(有 info 时为 2)。
|
||||
- 响应/推送:`type`(0=普通 / 2=机器人), `text`(表情 ID,客户端按 `(text-1)%ConstVal.Emotion.src_list.length+1` 归一), `seat`(发送座位), `info`(type==2 时)。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_send_phiz、send_phiz。
|
||||
|
||||
#### 72. call_phone
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 推送 · 拨打电话;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:拨打/接听电话,置玩家 `onstate=2`(通话中)。
|
||||
- 请求:仅通用四字段(发送处 `06_Player.js:380` / `:388` / `:396`,对应接起/电话进来/去电三种触发)。
|
||||
- 响应/推送:rpc=`call_phone`,`seat`(拨打者座位 → 该玩家 `onstate=2`)。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_call_phone、call_phone。
|
||||
|
||||
#### 73. hangup_phone
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 推送 · 挂断电话;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:挂断电话,置玩家 `onstate=0`。
|
||||
- 请求:仅通用四字段(发送处 `06_Player.js:372`)。
|
||||
- 响应/推送:rpc=`hangup_phone`,`seat`(挂断者座位 → 该玩家 `onstate=0`)。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_hangup_phone、hangup_phone。
|
||||
|
||||
### 保留名称 · 房间社交(5)
|
||||
|
||||
#### 74. receive_chat
|
||||
|
||||
- 路由/通道:room(文档归类,线上未确认);方向:未确认(文档推测为下行);状态:**X**;新 Rpc 常量:有。
|
||||
- 用途:聊天推送的另一可能 rpc 名(与 `send_text` 成对)。
|
||||
- 请求:未发现同名发送链。
|
||||
- 响应/推送:未定义,不能套用其它 rpc 字段。
|
||||
- 注意:常量存在,旧 Net 无同名接收器。与实际已实现的 send_voice/send_text/call_phone/hangup_phone/send_gift 区分;不据此断言服务器绝不会发送。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。
|
||||
|
||||
#### 75. play_voice
|
||||
|
||||
- 路由/通道:room(文档归类,线上未确认);方向:未确认(文档推测为下行);状态:**X**;新 Rpc 常量:有。
|
||||
- 用途:与 `send_voice` 成对,疑为语音**播放**推送。
|
||||
- 请求:未发现同名发送链。
|
||||
- 响应/推送:未定义,不能套用其它 rpc 字段。
|
||||
- 注意:常量存在,旧 Net 无同名接收器。与实际已实现的 send_voice/send_text/call_phone/hangup_phone/send_gift 区分;不据此断言服务器绝不会发送。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。
|
||||
|
||||
#### 76. other_send_gift
|
||||
|
||||
- 路由/通道:room(文档归类,线上未确认);方向:未确认(文档推测为下行);状态:**X**;新 Rpc 常量:有。
|
||||
- 用途:与 `send_gift` 成对,疑为他人送礼推送。
|
||||
- 请求:未发现同名发送链。
|
||||
- 响应/推送:未定义,不能套用其它 rpc 字段。
|
||||
- 注意:常量存在,旧 Net 无同名接收器。与实际已实现的 send_voice/send_text/call_phone/hangup_phone/send_gift 区分;不据此断言服务器绝不会发送。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。
|
||||
|
||||
#### 77. other_callphone
|
||||
|
||||
- 路由/通道:room(文档归类,线上未确认);方向:未确认(文档推测为下行);状态:**X**;新 Rpc 常量:有。
|
||||
- 用途:与 `call_phone` 成对,疑为他人拨打电话推送。
|
||||
- 请求:未发现同名发送链。
|
||||
- 响应/推送:未定义,不能套用其它 rpc 字段。
|
||||
- 注意:常量存在,旧 Net 无同名接收器。与实际已实现的 send_voice/send_text/call_phone/hangup_phone/send_gift 区分;不据此断言服务器绝不会发送。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。
|
||||
|
||||
#### 78. other_hangup
|
||||
|
||||
- 路由/通道:room(文档归类,线上未确认);方向:未确认(文档推测为下行);状态:**X**;新 Rpc 常量:有。
|
||||
- 用途:与 `hangup_phone` 成对,疑为他人挂断电话推送。
|
||||
- 请求:未发现同名发送链。
|
||||
- 响应/推送:未定义,不能套用其它 rpc 字段。
|
||||
- 注意:常量存在,旧 Net 无同名接收器。与实际已实现的 send_voice/send_text/call_phone/hangup_phone/send_gift 区分;不据此断言服务器绝不会发送。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。
|
||||
|
||||
### 大厅 · 资产更新与系统推送(4)
|
||||
|
||||
#### 79. update_bean
|
||||
|
||||
- 路由/通道:上行 agent;下行按 agent 归档(实际推送路由待逐项抓包);方向:C→S + S→C;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:豆豆变化(本人或房内座位)
|
||||
- 请求:原生 yPaytype / WVJB yPaytype 回调直接发送 agentid、playerid、change:num、type:9;这是资产写入路径,不属于只读查询。
|
||||
- 响应/推送:bean、change/seat/type(按场景可选)、text;不同分支见原处理器。
|
||||
- 注意:02 章“无对应主动请求”与当前 05_Func.js 不符。存在发送代码不代表支付来源可信性、服务端行为或新框架已经验收。
|
||||
- 直接上行来源:原生 yPaytype、WVJB yPaytype。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:update_bean。
|
||||
|
||||
#### 80. update_roomcard
|
||||
|
||||
- 路由/通道:上行 agent;下行按 agent 归档(实际推送路由待逐项抓包);方向:C→S + S→C;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:房卡变化
|
||||
- 请求:原生 yPaytype / WVJB yPaytype 回调直接发送 agentid、playerid、change:num、type:9;这是资产写入路径,不属于只读查询。
|
||||
- 响应/推送:roomcard、text(可选);change 分支条件见原处理器。
|
||||
- 注意:02 章“无对应主动请求”与当前 05_Func.js 不符。存在发送代码不代表支付来源可信性、服务端行为或新框架已经验收。
|
||||
- 直接上行来源:原生 yPaytype、WVJB yPaytype。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:update_roomcard。
|
||||
|
||||
#### 81. kick_offline
|
||||
|
||||
- 路由/通道:agent(协议归档;实际推送路由待逐项抓包);方向:S→C;状态:**N**;新 Rpc 常量:有。
|
||||
- 用途:被踢下线
|
||||
- 请求:无对应主动请求。
|
||||
- 响应/推送:fromOther、gameid(可选);旧流程同时上报 submit_error。
|
||||
- 注意:仅有旧处理器不能证明新 Runtime 接入或真实服务已覆盖。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:kick_offline。
|
||||
|
||||
#### 82. show_message
|
||||
|
||||
- 路由/通道:agent(协议归档;实际推送路由待逐项抓包);方向:S→C;状态:**N**;新 Rpc 常量:无。
|
||||
- 用途:通用提示
|
||||
- 请求:无对应主动请求。
|
||||
- 响应/推送:msg、time。
|
||||
- 注意:仅有旧处理器不能证明新 Runtime 接入或真实服务已覆盖。
|
||||
- 依据:[02-协议-agent路由.md](02-协议-agent路由.md);旧函数:show_message。
|
||||
|
||||
### 房间 · 服务器切换(room 侧)(1)
|
||||
|
||||
#### 83. connect_roomserver
|
||||
|
||||
- 路由/通道:room;方向:C→S 请求 + S→C 推送 · 切到房间服;状态:**R**;新 Rpc 常量:有。
|
||||
- 用途:从大厅服切换到房间服务器。
|
||||
- 请求:由 `Send_connect_roomserver` 发起(走 room 路由)。
|
||||
- 响应/推送:`data.roomserver`(新房间服地址)。处理:置 `GameData.ConnectType=true`、`GameData.ConnectRpc=connect_roomserver`、`GameData.Server=data.roomserver`,关闭当前连接并用新地址重连。
|
||||
- 依据:[03-协议-room路由.md](03-协议-room路由.md);旧函数:Send_connect_roomserver、connect_roomserver。
|
||||
|
||||
### 子游戏 · 框架保留名称(2)
|
||||
|
||||
#### 84. over_game
|
||||
|
||||
- 路由/通道:由具体子游戏契约确定;方向:文档描述 S→C;实际由子游戏确认;状态:**X**;新 Rpc 常量:有。
|
||||
- 用途:结算广播保留名;具体载荷由子游戏定义。
|
||||
- 请求:模板未定义。
|
||||
- 响应/推送:模板未定义。
|
||||
- 注意:Game_Surface_3 的 Game_Modify 为模板,不能据名字推导具体子游戏协议。
|
||||
- 依据:[05-游戏内协议与桥接.md](05-游戏内协议与桥接.md);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。
|
||||
|
||||
#### 85. agentserver_game
|
||||
|
||||
- 路由/通道:由具体子游戏契约确定;方向:未定义;状态:**X**;新 Rpc 常量:有。
|
||||
- 用途:对局数据经大厅服中转的保留名。
|
||||
- 请求:模板未定义。
|
||||
- 响应/推送:模板未定义。
|
||||
- 注意:Game_Surface_3 的 Game_Modify 为模板,不能据名字推导具体子游戏协议。
|
||||
- 依据:[05-游戏内协议与桥接.md](05-游戏内协议与桥接.md);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。
|
||||
|
||||
### 外部桥接 · 非网络 RPC(1)
|
||||
|
||||
#### 86. joinRoom
|
||||
|
||||
- 路由/通道:H5/小程序本地桥接;方向:外部入口→前端确认流程;状态:**X**;新 Rpc 常量:有。
|
||||
- 用途:唤起进房确认;不是直接向服务器发送 joinRoom。
|
||||
- 请求:{rpc:"joinRoom",data:{roomcode}};H5 来自 URL gameData,小程序来自缓存 openminigamedata。
|
||||
- 响应/推送:确认后走 self_join_room;不产生 joinRoom 网络回包。
|
||||
- 注意:h5RpcList 唯一名称;旧小程序 checkType=8 与白名单删除撞号,不能沿用。WVJB handler 名不计为服务器 RPC。
|
||||
- 依据:[05-游戏内协议与桥接.md](05-游戏内协议与桥接.md);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。
|
||||
|
||||
## 覆盖核对方法
|
||||
|
||||
- 逐一取 02_Const.js 的 RpcList 字符串赋值去重;核对 09_Net.js 中 RpcList 引用、Net 命名函数以及硬编码 rpc;扫描全模板的直接 _SendData 调用与 rpc 赋值(包含 05_Func.js 支付桥接),补入 h5RpcList。
|
||||
- 与 02/03 章标题、02 章末尾推送表以及 05 章保留/桥接名称取并集;按实际名称一条一项,跨章节引用不重复计数。
|
||||
- 结果:82 个旧常量名称全部命中;补充 4 个名称后 86 条,无遗漏或重复;11 个现代严格分发名称全部命中。
|
||||
- 新框架状态依据 platform/runtime.ts、protocol/platform-handlers.ts、protocol/first-slice-routes.ts;旧 protocol/routes.ts 大常量表仅用于“名称存在”对照。
|
||||
- 本轮没有发送网络请求、触发 Cocos 编辑器操作或运行资源生成器;这份静态索引不新增真实验收结论。
|
||||
@@ -0,0 +1,20 @@
|
||||
# 平台网络协议
|
||||
|
||||
返回[框架目录](../README.md)。这些协议内容已整合在本目录,无需跳转外部文档。
|
||||
|
||||
1. [框架架构](00-框架架构设计.md)
|
||||
2. [传输层与架构](01-传输层与架构.md)
|
||||
3. [agent 路由](02-协议-agent路由.md)
|
||||
4. [room 路由](03-协议-room路由.md)
|
||||
5. [数据结构](04-数据结构.md)
|
||||
6. [游戏内协议与桥接](05-游戏内协议与桥接.md)
|
||||
7. [历史子游戏开发模式与 Cocos 方案](06-子游戏开发模式与Cocos方案.md)
|
||||
8. [RPC 清单](07-RPC全量清单.md)
|
||||
|
||||
## 阅读约束
|
||||
|
||||
协议保留既有服务器信封与字段;不得为了前端便利改变服务器。历史文档中的原工程类名用于追溯,不要求调用旧实现。
|
||||
|
||||
早期 agent 表将 version 标为 string,但实包登录字段必须是数值 versionCode,详见数据结构中的登录校验记录。早期 create_room 的数组 roomtype 描述针对模板;通用平台保存与转发目标游戏实际的配置形态,二七王按[二七王协议](../../subgames/erqiwang/protocol/packet_protocol.md)使用字符串。
|
||||
|
||||
设计文档中的未来接入方式不能替代[当前实施状态](../architecture/current-status.md)。
|
||||
@@ -0,0 +1,71 @@
|
||||
# 子游戏 SDK 契约
|
||||
|
||||
返回[框架目录](../README.md)。相关:[创建房间](../integration/create-room.md)、[房间设计](../architecture/room-platform.md)。
|
||||
|
||||
本页整理当前接口职责,类型名对应工程中的同名契约。具体游戏通过契约接入,不导入平台内部状态实现。
|
||||
|
||||
## GameDefinition:子游戏入口定义
|
||||
|
||||
必须提供 key、route、config.versionResource、config.assetRoot、createGame(gameId)、chat、roomMenu(mainSceneButton/vipInfinite)、createPageClass、roomViewClass,以及 resources 中的 createRoom/room/roomScene 路径描述。
|
||||
|
||||
key 与场景 gameKey 一致。route 遵循协议,gameId 来自 XML。versionResource 精确为 `<gameKey>/version`,assetRoot 是本游戏的 `games/<目录>`。createRoom/room 必须位于本游戏根目录内,加载器从目标 Bundle 自动加载;roomScene 是公共承载场景引用。定义由本游戏 `GameEntry_<gameKey>` 类的 static definition 暴露,公共框架不维护游戏列表。
|
||||
|
||||
## GameBinding 与 GameEntry
|
||||
|
||||
GameBinding 包含 entry、mount(view)、restoredSnapshot、roomProjection。entry 提供 key/gameId/route、resolveSeatCount(roomtype)、createModule()。
|
||||
|
||||
resolveSeatCount 解释服务器实际返回的本游戏配置;不要让平台猜人数。createModule 创建独立房间模块,禁止把上一房间可变状态复用于下一房间。
|
||||
|
||||
## GameModule 生命周期
|
||||
|
||||
```ts
|
||||
interface GameModule {
|
||||
attach(host: GameHost): void;
|
||||
handlePlatformEvent(event: PlatformToGameEvent): void;
|
||||
handleGameMessage(message: GameServerMessage): void;
|
||||
restore(deskinfo: unknown): void;
|
||||
dispose(): void;
|
||||
}
|
||||
```
|
||||
|
||||
这是核心生命周期摘录。可选 roomProjection 用于向公共房间展示提供游戏投影;没有投影时明确为空。restore 必须按本游戏快照协议恢复,不能将仅缓存对象当作恢复完成。
|
||||
|
||||
## RoomCapabilities
|
||||
|
||||
通过 requireRoomCapabilities(host) 取得能力;生产宿主缺少能力时显式报错,不退回另一套隐藏实现。
|
||||
|
||||
- room.seat:座位映射。
|
||||
- room.getSnapshot / subscribe:快照与订阅,subscribe 返回退订函数。
|
||||
- room.prepare / requestExit / applyDissolution:公共房间操作。
|
||||
- messages.send(rpc, data):本游戏消息发送,data 遵循 JSON 与真实协议。
|
||||
- ui.beginLoading():返回具有 close() 的所有权句柄。
|
||||
- ui.showBusinessTip(message,time):玩家业务提示,返回 close() 句柄;不能传入异常或调试字符串。
|
||||
- ui.requestNumber / requestDigitText / requestMultiplier:异步输入,区分 confirmed 与 cancelled。
|
||||
- scope.active:会话是否有效。
|
||||
- scope.defer(cleanup):会话结束清理;返回函数用于取消该清理注册,不会立即执行 cleanup。
|
||||
|
||||
输入取消原因包含 user、scope-ended、replaced。取消不是故障,游戏必须处理取消分支。释放后回调不得继续操作 UI 或发送旧房间消息。
|
||||
|
||||
## 创建页面
|
||||
|
||||
```ts
|
||||
interface CreateRoomPageContext {
|
||||
readonly previousRoomtype: unknown;
|
||||
submit(roomtype: Roomtype): void;
|
||||
cancel(): void;
|
||||
reportError(message: string): void;
|
||||
}
|
||||
interface CreateRoomPagePort {
|
||||
open(context: CreateRoomPageContext): void;
|
||||
setBusy(busy: boolean): void;
|
||||
close(): void;
|
||||
}
|
||||
```
|
||||
|
||||
Cocos 页面继承 CreateRoomPage 并挂在 prefab 根节点。previousRoomtype 为 null 时,子游戏来源可定义首次默认值;已有无效数据不能静默重置。reportError 当前用于诊断,不显示玩家提示。详细实现步骤见[创建房间接入](../integration/create-room.md)。
|
||||
|
||||
## 聊天、视图与释放
|
||||
|
||||
每款游戏提供自己的 RoomChatConfig,常用语和历史通过动态列表展示,不根据另一款游戏的固定数量写框架分支。
|
||||
|
||||
RoomPresentation.render 接收平台快照、公共操作与可选游戏投影;close 释放展示状态。房间视图匹配注册的 roomViewClass。组件、定时器、事件和订阅都要在页面关闭、房间结束或节点销毁后解除。
|
||||
@@ -0,0 +1,240 @@
|
||||
# 原接口迁移清单与子游戏接入契约
|
||||
|
||||
版本:1.1 · 2026-09-08 · 状态:已落地 API 与兼容边界见第 7 节及[实施记录](../architecture/current-status.md)。前文保留原接口迁移目标。
|
||||
|
||||
主文档:[房间平台架构设计](../architecture/current-status.md)。本清单基于原工程 Input/Output 文件及其调用点,区分“保留业务能力”与“保留旧调用方式”。默认不保留全局 Game_Modify、Utl、Desk、C_Player 接口。
|
||||
|
||||
## 1. 迁移规则
|
||||
|
||||
- 保留:协议语义、原始载荷、来源定义的顺序、玩家可观察的正确结果。
|
||||
- 改造:直接读写全局状态、精灵编号、跨层调用、手工拼公共身份、全局输入回调。
|
||||
- 分离:在线与回放、局内分数与账户资产、请求与确认、状态重绘与一次性业务副作用。
|
||||
- 不照搬:示例默认值、空钩子掩盖未接入、吞异常、以显示状态判断业务阶段。
|
||||
- 必需项缺失在装配时失败;可选项必须声明“未提供”的行为,不能通过任意 fallback 冒充支持。
|
||||
|
||||
## 2. Input:平台通知游戏
|
||||
|
||||
源码:02_SubGame_Input.js。
|
||||
|
||||
### 2.1 房间进入与游戏协议
|
||||
|
||||
`StartWar(_msg)` → started 输入。保留触发 sourceRpc 和原整包;包括 self_makewar、makewar、加入/准备附带 deskwar,不仅监听单个 rpc。
|
||||
|
||||
`_ReceiveData(_msg)` → game-message 输入。游戏自己的协议适配器校验 rpc/payload,平台只校验路由归属与传输外层。
|
||||
|
||||
`createRoom(roomtype,infinite)`、`onCreateRoom(data)` → created 成功输入的游戏适配。旧包装层可能在失败后仍调用钩子,必须记录并改为明确的结果/成功边界,禁止把失败视为建房成功。
|
||||
|
||||
`myJoinRoom(_msg)` → joined 输入及对应 waiting/snapshot/started 分支。不能因为有快照而省略主动加入语义。
|
||||
|
||||
`Reconnect(deskinfo)` → login-restored 输入;`ReconnectNoMakewar` 虽未在此文件声明,但在 Desk.login 调用,应纳入 login-waiting 契约。
|
||||
|
||||
`DeskInfo(deskinfo)` → joined-snapshot。与登录重连区分,游戏可以映射到共享内部还原函数,但平台不得强迫两者等同。
|
||||
|
||||
`onCreateDesk(roomtype)` → 游戏初始化/规则预检。座位规则可以先运行,任何需要 View 的工作移到挂载就绪后。
|
||||
|
||||
### 2.2 玩家与公共状态
|
||||
|
||||
`playerJoinRoom`、`playerLeaveRoom`、`playerOffline`、`playerOnline`、`onReady`、`changeSeat` → 类型明确的公共成员输入,始终使用服务器座位。
|
||||
|
||||
`myExitRoom`、`breakRoom` → 保留服务端确认的自身离开原因,在旧上下文仍可读取的收尾阶段通知,然后统一释放。游戏不能在通知中再次发送同一退出命令。
|
||||
|
||||
`playerphonestate` → 设备/玩家通话状态能力输入。旧参数 0/1 的实际语义以 Desk.call_phone/hangup_phone 为准,不照抄注释中的歧义文本。
|
||||
|
||||
### 2.3 视图生命周期
|
||||
|
||||
`onEnterMainScene` → View 挂载就绪后的明确通知;不通过读取精灵可见性判断。
|
||||
|
||||
`onExitMainScene`、`closeGameScene`、`stopAllSounds` → 由 scope 组织的停止/卸载/音频释放。音频使用房间或游戏专属 owner,不误停应用其他声音。
|
||||
|
||||
`updateScene` → 从现有游戏状态重建展示。不会重新解释创建响应、再次发命令或再次产生分享/活动结果。
|
||||
|
||||
`onMainMenuScene` → 应用/大厅导航生命周期,非每房间 GameModel 必需能力。
|
||||
|
||||
### 2.4 结算与活动
|
||||
|
||||
`Free` → dissolution-settlement,区分普通投票确认与 freeNow。deskfree 内容由游戏解析。
|
||||
|
||||
`onSurrender` → 对应来源的业务结果输入,游戏解释;公共投降入口只负责协议封装和展示流程。
|
||||
|
||||
`onEnterVideo` → 可选回放能力,绑定独立 ReplayContext。
|
||||
|
||||
`onCloseVip`、`getShareRoom` → 大厅/房间列表功能的可选扩展,不强迫纯房间游戏实现。
|
||||
|
||||
### 2.5 同步规则与描述
|
||||
|
||||
`getRoomInfo`、`getFullRoomInfo`、`getRoomTopDescAry` → GameRoomDescription。返回标题、说明项等结构化内容,UI 决定换行和字体,不让游戏按“18/26 个字符”进行硬布局。
|
||||
|
||||
`getRoomMode`、`getStarLimit`、`getLeaveLimit`、`getMult`、`getVideoByRoomType` → 游戏规则/策略查询;不修改 roomtype、不发包、不写 UI。仅保留项目实际支持的功能,未提供时有明确的能力边界。
|
||||
|
||||
创建前的规则预览与入房后的权威公共回包不能混用:例如 roommode 已由服务端明确给出时,不用本地规则覆盖。
|
||||
|
||||
`onGameConfig` → 游戏配置生命周期。配置来源唯一,更新时有版本/替换语义,游戏不得自行从另一个 URL 再读一套。
|
||||
|
||||
### 2.6 用户输入与设备
|
||||
|
||||
`calResult`、`onCheckInput` → 对应具体输入请求的结果,不再使用全局回调入口。
|
||||
|
||||
`shakeEvent` → 平台设备意图适配。平台提供的“摇动开始”与开始按钮调用同一个 start 命令;游戏自定义摇动行为可以声明独立能力,不能复制公共身份拼包代码。
|
||||
|
||||
`onLocationInfo` → Location 能力的结果/订阅。浏览器替身数据的缺省由浏览器适配器明确定义,游戏不猜地址或坐标。
|
||||
|
||||
`onOpenHelp(spid)` → 帮助内容/页面能力,不暴露原精灵编号。
|
||||
|
||||
### 2.7 gameHallImport
|
||||
|
||||
appStart、jumpMenuScene、gameStart、setGameList、clearGameinfo、getWebdata、isInstalled、up_imgurl、getphoto 属于宿主/大厅适配。应与房间游戏 SDK 分开,不要求每个子游戏定义一套全局对象。
|
||||
|
||||
isInstalled 等示例固定返回值不能成为新实现的真实能力证明。
|
||||
|
||||
## 3. Output:游戏使用平台能力
|
||||
|
||||
源码:08_Utl_Output.js。按职责归属迁移,不按原函数顺序重新堆成大工具类。
|
||||
|
||||
### 3.1 身份、玩家和房间查询
|
||||
|
||||
涉及:getMyInfo、getGameID、getAgentID、getPlayeridBySeat、getNicknameBySeat、getSexBySeat、getMyPlayerid、getMySex、getRoomcode、getMySeat、getMyOpenid、getPlayerInfoBySeat、getPlayerList、getPlayerCnt、getPlayerReadyState、getBeanBySeat、getShortCode、getIsInfinite、getInfMode。
|
||||
|
||||
归属:RoomReadPort 的只读快照、必要身份投影和具名选择器。occupiedCount 与 seatCount 分开,空位有明确表示;缺失玩家不能返回 -1 后让 UI 猜测。
|
||||
|
||||
getMyOpenid 不应作为普通玩法的默认必需字段;确有原业务用途时,通过最小身份能力提供,不把完整登录响应暴露给所有游戏。
|
||||
|
||||
changeToStatus → 统一 seat mapper。服务端座位和显示座位可使用不同类型别名,减少混用,转换只在边界进行。
|
||||
|
||||
getOnState/isMainScene → 分别读取状态投影和生命周期,不能读取 UI 数组或 Node.active 来决定业务。
|
||||
|
||||
### 3.2 网络与房间命令
|
||||
|
||||
sendData → 当前游戏 route 绑定的 GameMessagePort。app/route 不由子游戏每次传入,rpc/data 仍由游戏定义。
|
||||
|
||||
sendExitRoom/sendChangeRoom/sendText/enterShareRoom/getShareRoom → 公共具名命令。平台负责身份和房间参数,调用方只提供真正变化的业务参数。
|
||||
|
||||
Exit → 不暴露为可任意清空平台状态的方法。区分请求退出、已确认的本地会话结束、结算返回大厅,由协调器内部执行。
|
||||
|
||||
openMatchUrl、getMathInfo/getIsMathInfo、getAdvanced/getPlayerAdvanced、openSnrOption、getRebateRange → 对应比赛、VIP/房间策略能力。不要混入通用游戏传输端口;不支持的模式明确标记。
|
||||
|
||||
### 3.3 状态修改与展示
|
||||
|
||||
setGrade → 游戏状态中的局内分数,经 GameRoomProjection/HUD 展示模型更新,布局由 View 负责。
|
||||
|
||||
changeBean → 先追踪调用源。若是玩法结算的相对得分,应写游戏投影;若是权威资产更新,走对应协议来源。禁止 initialBean + delta 直接覆盖平台账户余额。
|
||||
|
||||
setPlayerPrepare/setDeskStage/changePlayerState → 拆分公共协议事实、游戏阶段/参与状态、公共动作限制。必要映射显式声明,禁止通用 setter 任意修改平台状态。
|
||||
|
||||
closeMainSceneButton/closeInvitation/closeCommunication/updatePlayerInfoUI → 公共展示模型的状态或具名 UI 操作。业务长期显隐通过模型表达,短暂关闭模态框通过所有者句柄执行,不直接查找平台节点。
|
||||
|
||||
getExitVisible/getChangeVisible → RoomPolicy 派生值。命令执行时仍重新验证当前 scope 与策略,不能只靠按钮隐藏防止错误操作。
|
||||
|
||||
### 3.4 UI、资源和声音
|
||||
|
||||
openTips/openTips2/closeTips → 有所有者的提示能力,返回可关闭句柄;一个游戏不能关闭其他来源的提示。
|
||||
|
||||
Layer612_Tips 的内容仅面向玩家业务。reportError/catch/Error.message 不得直接转接提示端口;配置、协议、资源及代码异常写诊断日志,业务提示通过单独的 notify/showBusinessTip 入口。不能把技术异常转移到 Kick 等其他玩家面板。
|
||||
|
||||
startLoad/endLoad → 任务绑定的 loading 句柄或计数租约;并发两个任务时,一个结束不能关闭另一个的加载提示。
|
||||
|
||||
openInputPanel/closeInputPanel/openTextInput → 有请求 ID(仅本地)、明确结果和取消语义的输入能力。
|
||||
|
||||
getMultipleResult → 纯计算与倍率偏好读取分开;展示倍率选择交给 UI 端口,禁止计算函数暗改 GameData.OrgArr 或存储。
|
||||
|
||||
playMusic/playSound/stopMusic/stopSound → AudioPort。声音标识来自游戏资源定义,播放策略遵循平台设置,句柄受 scope 管理。
|
||||
|
||||
getHeadimgSrc/openInfo/openInfo1 → 头像资源租约和玩家资料能力;不返回 116+seat 这类精灵 ID。
|
||||
|
||||
setFontColor/convertNumberToImg/getNameImgFrame_1/getNameImgFrame_2 → 展示适配层。Cocos 文本使用 Label/BitmapFont,具体业务不依赖字符替换 b/c/d 或图集帧号。
|
||||
|
||||
getRoomCardName/getstarName → 平台展示配置投影,保留单一来源,不在游戏里重复定义。
|
||||
|
||||
### 3.5 持久化与工具
|
||||
|
||||
SetStorage、Config.pre_* → 存储命名空间与键定义集中管理;账户/游戏作用域明确,不能以当前全局身份拼出不确定的键。
|
||||
|
||||
SaveData/ReadData/checkKey/RemoveItemByKey/ClearStorage、setCookie/getCookie/delCookie → 存储适配器。游戏只获得自身命名空间,不能清空应用全部登录/平台数据。
|
||||
|
||||
saveRoomtype/getRoomtype/delRoomtype → 游戏配置历史能力;保存与读取不转换 roomtype,版本迁移由游戏负责。
|
||||
|
||||
saveGradeInfo/readGradeInfo → 战绩/回放数据仓储,不与房间临时模型混放;来源、保留上限与账号隔离必须明确。
|
||||
|
||||
clone/removeItemFromArray → 普通内部工具或删除;不作为平台业务 SDK。新架构使用明确的数据所有权与不可变更新,避免原 clone 破坏数组类型。
|
||||
|
||||
### 3.6 结算、分享和回放
|
||||
|
||||
onGameFinished → 拆为已解析的结算事实、游戏结果展示和显式分享请求;渲染函数重复调用不会重复分享。
|
||||
|
||||
typeForActivity → 可选活动能力的一次性业务通知,绑定结算来源;不能在重绘中调用。
|
||||
|
||||
gameOver/mainScene → 由明确结束原因、结算状态与导航结果驱动。结束一局不等于结束房间。
|
||||
|
||||
openVideo/closeVideo → ReplaySession,独立状态、时钟、玩家视角,不修改在线房间数据。
|
||||
|
||||
### 3.7 设备与宿主环境
|
||||
|
||||
getLocation/getPhoneInfo/gameCopytext/getAppService/closeWindow → 设备/宿主能力;浏览器和原生各自实现相同契约。
|
||||
|
||||
getGameConfig/getVersionState/getH5Version/getIsDebugger/getShowShare → 配置与能力投影。新游戏优先判断声明的能力,而非散布平台版本号分支;调试配置不进入玩法规则。
|
||||
|
||||
getPayCodeBySeat、支付/VIP/分享等与房间非核心功能相连的读取 → 在迁移对应模式时核对实际字段来源,不能为满足接口数量提前返回假值。
|
||||
|
||||
## 4. 新接口的使用约定
|
||||
|
||||
### 4.1 查询
|
||||
|
||||
一次业务处理读取同一 revision 的快照。对象只读,任何修改都不通过 getter 返回对象完成。需要最新状态时再次读取;异步回调读取前确认 scope 仍有效。
|
||||
|
||||
### 4.2 命令
|
||||
|
||||
具名命令检查:scope 有效、登录有效、房间身份匹配、当前策略允许、是否已有同类 pending。发送成功不代表业务确认。
|
||||
|
||||
服务器没有关联 ID 时不得虚构关联。对于同房间互斥操作采用本地串行 pending,收到对应合法回复或超时后解除。重连恢复以服务器状态为准,不自动重发非幂等命令。
|
||||
|
||||
### 4.3 UI 请求
|
||||
|
||||
```ts
|
||||
type InputResult<T> =
|
||||
| { kind: 'confirmed'; value: T }
|
||||
| { kind: 'cancelled'; reason: 'user' | 'scope-ended' | 'replaced' };
|
||||
```
|
||||
|
||||
输入请求在离房时必须结束。数值模式返回 number,允许前导零的文本模式返回 string,不能在平台随意转换。类型签名可按不同请求模式精确约束。
|
||||
|
||||
### 4.4 投影
|
||||
|
||||
游戏模型是事实来源,投影只负责提取公共 HUD 所需字段。投影更新不能反向产生同一输入事件,避免状态→事件→状态循环。
|
||||
|
||||
平台准备/资产信息与投影若冲突,优先按各字段定义的唯一来源处理,并报告不一致,不采用“最后写入者胜出”。
|
||||
|
||||
### 4.5 一次性事件
|
||||
|
||||
结算活动、音效、分享等与状态渲染分开。重连直接重建状态,不默认重放历史音效、公告或分享。哪些效果需要重放由具体玩法契约明确规定。
|
||||
|
||||
## 5. 接入交付清单
|
||||
|
||||
- 一个权威 GameDefinition,明确必需和可选能力。
|
||||
- roomtype 编码/解析与规则测试,不要求平台理解格式。
|
||||
- 一个游戏模型以及进入来源/游戏 rpc 到模型的映射。
|
||||
- 创建页、房间 View 和资源定义;恰好一个页面契约实现。
|
||||
- SDK 回归夹具:创建、加入、重连有/无快照、开战、结算、取消、退出。
|
||||
- Cocos 实际创建按钮提交与 WebSocket 发包验证。
|
||||
- 已实现/未实现能力清单,不以空处理器通过检查。
|
||||
|
||||
只增加游戏目录和应用注册项即可接入。若仍必须修改平台菜单脚本、公共协议路由或提供很多固定返回值,说明边界设计尚未达标。
|
||||
|
||||
## 6. 过渡期
|
||||
|
||||
旧接口兼容层仅用于逐条迁移现有游戏,不作为新 SDK 的正式表面。每项兼容入口必须有目标能力、测试和移除条件。
|
||||
|
||||
不要求一次迁移所有历史宿主/VIP/回放功能,但必须明确未支持范围。任何尚未找到调用源或协议依据的接口不得凭名称实现。
|
||||
|
||||
## 7. 本轮实际落地位置
|
||||
|
||||
- StartWar:`PlatformToGameEvent.room.started`,保留完整 message/sourceRpc;线上 other_makewar 对应原 makewar 函数。原始游戏包由 `GameModule.handleGameMessage` 接收。
|
||||
- createRoom/onCreateRoom、myJoinRoom、Reconnect/DeskInfo:`room.entered.entry` 的来源分支与兼容 `restore`。开战优先于加入快照恢复。游戏自身牌桌显示仍未迁移。
|
||||
- 房间查询、座位映射:`requireRoomCapabilities(host).room.getSnapshot / subscribe / seat`。只读数据来自平台 Store。
|
||||
- 准备、退出、申请解散:同一 room 端口的 `prepare / requestExit / applyDissolution`,公共身份与投影权限由 PlatformCommands 处理。自定义游戏包使用 `messages.send`。
|
||||
- 数字、数字文本、倍率:`ui.requestNumber / requestDigitText / requestMultiplier`,返回 InputResult;本地取消、替换和 scope 结束均有结果。原回调式 Host 命令暂留兼容。
|
||||
- loading、玩家业务提示:`ui.beginLoading / showBusinessTip` 返回 `{close()}`,通过真实 FeedbackPanels 仲裁所有者。技术错误不得传入这些接口。
|
||||
- 资源生命周期:`scope.active / defer`;底层 RoomScope 复用既有 Host 租约失效机制。取消先撤销能力,再逐项释放资源。
|
||||
- 局内公共事实:`createGameRoomProjection` 的 owner 提交完整版本,GameModule 暴露 source;公共 render 第三个参数和命令权限读取同一来源。现有模板/二七王返回 null,未生成伪 phase 或局内分数。
|
||||
- 头像、聊天:RoomUi 使用共享租约;RoomChatPanel 使用本地 ID 增量行和同帧合并。协议不增加消息 ID。
|
||||
- 音频所有者接口:未提供,原生契约没有播放停止能力。原有 RoomVoiceAdapter 的语音功能保留;支付、分享、回放和其他未迁移 Output 接口继续按其原边界单独接入。
|
||||
|
||||
新游戏只需要自己的代码/资源、组合根注册,以及编辑器中对应的 SubgameAssets 资源配置。运行期不会预执行工厂来推断模块或资源;真实 open 时只创建一个模块并校验。
|
||||
@@ -0,0 +1,13 @@
|
||||
# 子游戏专项资料
|
||||
|
||||
返回[文档首页](../README.md)。本目录只保存各具体游戏的设计、协议和实施记录。
|
||||
|
||||
- [二七王](erqiwang/README.md)
|
||||
|
||||
通用流程统一放在框架文档中:
|
||||
|
||||
- [如何接入子游戏](../framework/integration/README.md)
|
||||
- [如何接入创建房间](../framework/integration/create-room.md)
|
||||
- [如何构建目标游戏](../framework/build/README.md)
|
||||
|
||||
新增具体游戏可建立自己的资料子目录;通用接入和构建规则不应复制到每款游戏中分别维护。
|
||||
@@ -0,0 +1,13 @@
|
||||
# 二七王
|
||||
|
||||
返回[子游戏指南](../../framework/integration/README.md)。
|
||||
|
||||
- [创建房间接入](create-room-integration.md)
|
||||
- [玩法设计](design/design.md)
|
||||
- [玩法与数据包协议](protocol/packet_protocol.md)
|
||||
- [通用创建页面接口](../../framework/integration/create-room.md)
|
||||
- [版本与构建](../../framework/build/version-and-build.md)
|
||||
|
||||
本目录资料由旧独立工程预留目录迁移。现有游戏代码仍在 YouleNexus 工程内的 assets/games/erqiwang。旧目录没有游戏源码或资源。
|
||||
|
||||
gameKey 为 erqiwang,版本资源包为 version-erqiwang。身份和版本读取本游戏 version.xml,不在页面重复配置。当前仅完成创建房间和房间入口;牌局、结算和快照画面恢复不能视为已经实现。
|
||||
@@ -0,0 +1,41 @@
|
||||
# 二七王创建房间接入
|
||||
|
||||
核对日期:2026-09-08。本篇记录创建房间阶段的实际实现,不代表二七王牌桌玩法已迁移完成。
|
||||
|
||||
## 资源与入口
|
||||
|
||||
工程为 `cocoscreator_projects/YouleNexus`。`PlatformStartup.scene` 的 `PlatformServices / SubgameAssets` 已配置:
|
||||
|
||||
- `gameKey = erqiwang`。
|
||||
- `createRoom`:`assets/games/erqiwang/ErqiwangCreateRoom.prefab`。
|
||||
- `room`:`assets/games/erqiwang/ErqiwangRoom.prefab`。
|
||||
- `roomScene`:沿用 `assets/scenes/TemplateRoom.scene` 的通用场景容器。
|
||||
|
||||
联调 gameid 按用户确认由本游戏 version.xml 提供 `J9r4022t80pxlb01zOK76DN1WD4QewFl`,身份仍由运行配置提供,不在创建页重复定义。
|
||||
|
||||
## 页面与规则
|
||||
|
||||
`ErqiwangCreateRoomView` 继承平台 `CreateRoomPage`,只通过上下文提交、取消和报告错误。平台统一处理登录身份、IP、定位、请求信封、等待、超时和回包。
|
||||
|
||||
Inspector 绑定三组 `ToggleContainer`,`allowSwitchOff=false`;八个 Toggle 依次为 6 局、12 局、房主扣卡、AA 每人扣卡、可查牌、不查牌、傍王、爬坡。最后两个 Toggle 没有 ToggleContainer,可以独立勾选或取消。费用由局数与扣卡方式联动计算。
|
||||
|
||||
规则模型 `erqiwang-room-rules.ts` 是前端选项与编码转换的唯一来源。界面顺序为局数、扣卡、查牌、傍王、爬坡;实际服务器编码顺序仍为局数、扣卡、傍王、爬坡、查牌。尾部六位填零,总长度十一位。默认 `00000000000`。协议依据见 [服务端协议 §0.5](protocol/packet_protocol.md#05-roomtype-房间选项位串),不能按界面顺序直接发包。
|
||||
|
||||
新请求生成固定 11 位、后六位为零的字符串;当前历史/回包解析接受至少 5 位且前五位为 0/1 的字符串,拒绝数组;历史来自平台成功回包保存的 `tsgame_roomtype_ + gameid + agentid`,格式仍为 JSON `{data: roomtype}`。没有历史时使用规则来源定义的默认选择,不静默修补非法历史。
|
||||
|
||||
## 平台契约与房间交接
|
||||
|
||||
公共 SDK 的不透明 `Roomtype` 用于创建页面、请求、成功响应、状态、GameEntry、GameHost 快照及事件。字符串保持原值,数组仍复制并冻结;平台不解释二七王位含义。VIP 房间配置同样保留字符串。
|
||||
|
||||
创建成功后,`createErqiwangGame` 校验配置并提供三个服务器座位,平台维护权威状态,临时房间页显示房号、玩家以及“玩法尚未接入”。退出使用本房间 GameHost。准备按钮暂不展示,避免进入尚未迁移的牌桌流程。玩法消息处理未实现,收到会显式报错;deskinfo 仅保留原值,尚无玩法恢复表现。
|
||||
|
||||
## 历史验证与限制
|
||||
|
||||
以下为原接入记录保留的验收结果,不表示本次文档迁移重新执行了这些测试。
|
||||
|
||||
- `framework-tests/presentation/erqiwang-room-entry.test.ts`:穷举 32 种选项组合,对照实际服务端 `class.config.js` 验证编码、恢复、费用和人数。
|
||||
- `framework-tests/platform/runtime.test.ts`:模拟登录、创建出站包、成功回包、三座位状态、房间显示与退出命令。
|
||||
- 相关回归测试 157 项通过;Cocos 工程与框架 TypeScript 检查、导入边界检查通过。
|
||||
- Game View 实际坐标点击验证 11 项:必选互斥、费用、独立开关、精确编码、busy、防重复、关闭清理及历史恢复。新 prefab 共 52 处 UUID 引用有效。
|
||||
|
||||
本次未修改服务器代码,未在真实服务器创建房间。本地模拟通过不等于实际服务端创建成功;后续联调从大厅“创建房间”入口进行,并检查出站 roomtype 类型与值。
|
||||
@@ -0,0 +1,784 @@
|
||||
# 二七王 玩法设计文档
|
||||
|
||||
> 本文档是二七王玩法的唯一权威规则说明。内容随讨论持续与规则设计者逐项确认、修订;文中标注"待确认"的地方是当前仍未有定论的开放问题,其余内容均已确认生效。
|
||||
|
||||
## 目录
|
||||
|
||||
1. [术语约定](#1-术语约定)
|
||||
2. [牌局构成](#2-牌局构成)
|
||||
3. [主牌顺序](#3-主牌顺序)
|
||||
4. [开局与坐庄流程](#4-开局与坐庄流程)
|
||||
5. [出牌规则](#5-出牌规则)
|
||||
- [5.1 跟牌基本原则](#51-跟牌基本原则)
|
||||
- [5.2 按牌型跟牌](#52-按牌型跟牌)
|
||||
- [5.3 拖拉机定义与相邻关系](#53-拖拉机定义与相邻关系)
|
||||
- [5.4 甩牌详细规则](#54-甩牌详细规则)
|
||||
6. [捡分与扣底](#6-捡分与扣底)
|
||||
- [6.1 分牌](#61-分牌)
|
||||
- [6.2 捡分规则](#62-捡分规则)
|
||||
- [6.3 扣底](#63-扣底)
|
||||
7. [结算:子数与升级](#7-结算子数与升级)
|
||||
- [7.0 四层关系总览 / 查表索引](#70-四层关系总览)
|
||||
- [7.1 常规算子:叫分 → 基础子数](#71-常规算子叫分-→-基础子数未勾选爬坡时的默认规则)
|
||||
- [7.2 常规算子的判定表](#72-捡分-vs-叫分庄家的过庄-小光-大光闲家的升级常规算子)
|
||||
- [7.3 爬坡](#73-爬坡叫分-→-基础子数与保小光分段可选规则开房时勾选爬坡生效)
|
||||
8. [算奖规则](#8-算奖规则)
|
||||
- [8.1 冲关](#81-冲关)
|
||||
- [8.2 亮牌规则](#82-亮牌规则)
|
||||
- [8.3 傍王(可选规则)](#83-傍王可选规则)
|
||||
- [8.4 算奖如何影响最终结算](#84-算奖如何影响最终结算)
|
||||
9. [牌局查看模式](#9-牌局查看模式)
|
||||
10. [房间设置选项](#10-房间设置选项)
|
||||
11. [牌局交互提示](#11-牌局交互提示)
|
||||
12. [完整牌局游玩流程](#12-完整牌局游玩流程)
|
||||
|
||||
---
|
||||
|
||||
## 1. 术语约定
|
||||
|
||||
| 术语 | 含义 |
|
||||
| --- | --- |
|
||||
| 主牌 / 副牌 | 庄家选定的花色为"主牌",其余三个花色为"副牌";主牌大于副牌 |
|
||||
| 固定主牌 | 不论选择哪个花色为主,大王、小王、以及所有花色的"2""7"恒为主牌 |
|
||||
| 正2 / 正7 | 选定花色的"2""7",大于其余花色的"2""7"(口语里也叫"主2""主7",本文档统一用"正2""正7")|
|
||||
| 副2 / 副7 | 非选定花色的"2""7" |
|
||||
| 底牌 | 发牌时**没有发给玩家**、扣在桌面的 8 张牌;庄家确认后可摸起查看,摸起后就不再扣在桌面,而是并入庄家手牌 |
|
||||
| 埋牌底牌 | 庄家"埋牌"时从手牌中选出重新扣下的 8 张牌;"底牌"与"埋牌底牌"是两批不同的牌,不通用、不互换 |
|
||||
| 埋牌 | 庄家看到底牌、摸起查看后,从手牌中选出 8 张重新扣下,作为"埋牌底牌" |
|
||||
| 捡分 | 出牌中赢下的分牌(5、10、K)计入闲家总得分的过程 |
|
||||
| 扣底 | 出完所有手牌后的最后一轮,若被闲家用主牌牌型压过庄家,则埋下的 8 张埋牌底牌也计入闲家捡分,并按牌型翻倍 |
|
||||
| 亮牌 | 庄家埋牌后、出牌前,若手中固定主牌达到门槛(总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6 任一),向两个闲家**亮出自己全部固定主牌的具体牌面**(见第 8.2 节)。不限叫分,仅可查牌模式 |
|
||||
| 余主公示 | 任一玩家主牌出空(报无主)后,为**全体三人**展示另外两家各自的**剩余主牌数与剩余主对数**(见第 9 节)。只给数量,不给具体牌面 |
|
||||
| 明牌 | 可查牌模式下,查看**另外两家**手中全部未出主牌的具体牌面(见第 9 节)。与"亮牌"同样给牌面,区别在:亮牌是**庄家公开自己的**,明牌是**去看别人的** |
|
||||
| 摸底 | 庄家坐定后把那 8 张**底牌**摸起查看并并入手牌(见第 4 节)。**非 70 分坐庄时只有庄家看得到这 8 张** |
|
||||
| 开底 | **70 分坐庄**时,在庄家摸底**之前**,先把这 8 张底牌向**所有玩家**翻开 3 秒,之后才由庄家摸入手牌(见第 4 节)。给的是具体牌面 |
|
||||
| 查底牌 | 对局中随时回看那 8 张底牌的功能(前端底栏按钮)。庄家全程可用;闲家仅在**开底**过的局(即 70 分坐庄)可用 |
|
||||
| 拖拉机 | 同一花色(含主牌)里点数相邻的连续对子,如 K K Q Q 为"两托"(两连对) |
|
||||
| 甩牌 | 首家出牌时,一次性打出多组主牌组合(单张 / 对子 / 拖拉机自由搭配);仅主牌可以甩牌,副牌禁止甩牌,只能分单张 / 对子 / 拖拉机依次打出(见第 5.4 节) |
|
||||
| 毙牌 | 跟牌时手中没有本轮花色(缺门),改用主牌打出、且牌面大过本轮目前所有人的牌(见第 5.1 / 5.2 节) |
|
||||
| 垫牌 | 跟牌时手中没有本轮花色(缺门)、且不用主牌毙牌,随意出一张其他副牌,不参与、不争夺本轮的出牌权(见第 5.1 节) |
|
||||
| 混合出牌 | 首家出副牌,你手中有该花色副牌但张数不够凑齐首家总张数,又没有(或不用)别的副牌补差额,只能用主牌补足缺口——打出的是"该花色副牌 + 主牌"的混合牌。这既不是垫牌(不是缺门、不是纯副牌),也不是毙牌(不是纯主牌牌型、压不过首家),服务端判定其牌面为 0,**与垫牌等效:不参与、不争夺本轮出牌权**(见第 5.1 / 5.2 节) |
|
||||
| 子 | 结算时用于计算输赢筹码的单位,与"倍数"是两个独立概念(见第 7 节) |
|
||||
| 冲关 | 局末对固定组合(三王/四王、六 2/七 2/八 2、六 7/七 7/八 7、连续对子链等)的额外奖励结算,**只计庄家**(见第 8.1 节)。旧称"常规算奖",现统一叫**冲关**;服务端字段名 `chongguan` 与此同源 |
|
||||
| 算奖 | **上位概念** = 冲关(8.1,只计庄家)+ 傍王(8.3,可选、庄闲都算)。两个分量求和后才是某玩家的总奖数 `N`,据此结算(见第 8.4 节)。**说"冲关"指的是庄家那一份,说"算奖"指的是合计** |
|
||||
| 傍王 | 可选房间规则:勾选后每张王额外算一奖,庄家、闲家都要算;这是在 8.1 冲关之外**额外叠加**的奖励,与冲关互不冲突(见第 8.3 节) |
|
||||
| 爬坡 | 可选房间规则:改变叫分对应的基础子数与保小光分数线,未勾选时按"常规算子"结算(见第 7 节) |
|
||||
|
||||
> **术语变更记录(2026-08-25)**:原「**暗牌**」(发牌留桌的 8 张)改称「**底牌**」;原「**底牌**」(庄家埋下的 8 张)改称「**埋牌底牌**」。
|
||||
>
|
||||
> 改名原因:前端界面上「底牌」按钮查看的正是发牌留桌那 8 张,旧命名与界面倒挂、极易写反。
|
||||
>
|
||||
> **本文已全文改用新术语。** 但 `docs/protocol/packet_protocol.md` 与服务端代码里的 `bottomcards` 字段名**尚未拆分**——两批牌目前仍共用这一个字段名,只靠「出现在哪个包」区分语义:`shangzhuang` / `ChooseMain` / `BuryCards` 里的是**底牌**,`maipai` / `PushCards` / 结算 `bottom` 分组里的是**埋牌底牌**。拆分计划见 `docs_dev/二七王-UI资源与精灵清单.md` §7.5 的 S-4。
|
||||
|
||||
> **易混术语对照(都带"亮/明"字,但方向与粒度各不相同,务必分清)**:
|
||||
>
|
||||
> | 术语 | 谁公开给谁 | 给的是什么 | 触发条件 | 协议字段 |
|
||||
> | --- | --- | --- | --- | --- |
|
||||
> | **亮牌** | 庄家 → 两个闲家 | **具体牌面**(庄家全部固定主牌) | 庄家埋牌后手牌达门槛(§8.2) | `liangpai` |
|
||||
> | **余主公示** | 全体 → 全体 | **只有数量**(剩余主牌数、主对数) | 任一玩家报无主(§9) | `seatlist[seat][4]` + `baozhu` |
|
||||
> | **明牌** | 他家 → 请求者 | **具体牌面**(他家全部未出主牌) | 可查牌 + 报无主,主动点击(§9) | `mingpai.others[].zhucards` |
|
||||
> | **开底** | 桌面 → 所有玩家 | **具体牌面**(8 张底牌) | 70 分坐庄,**摸底之前** 3 秒(§4) | `ancard3s` + `bottomcards` |
|
||||
>
|
||||
> 一句话记:**只有「余主公示」是统计类(给数字),「亮牌」「明牌」「开底」都给具体牌面**——区别在给谁的牌:亮牌给庄家自己的、明牌给他家的、开底给桌上那 8 张。
|
||||
> 另有一组「底」字族专管那 8 张底牌:**摸底**(庄家摸起看)、**开底**(70 分时全场看 3 秒)、**查底牌**(随时回看)、**扣底**(末轮翻开埋牌底牌计分)。
|
||||
> 另注:**「亮主」不是本文档的术语**——它是前端选主界面的标题文案(庄家选主牌花色那一步),与上表四项无关。
|
||||
|
||||
---
|
||||
|
||||
## 2. 牌局构成
|
||||
|
||||
两副扑克牌共 108 张,去掉两副牌中的"3"和"4"(2 种点数 × 4 花色 × 2 副 = 16 张),剩余 **92 张**:
|
||||
|
||||
- 点数 5、6、7、8、9、10、J、Q、K、A、2:共 11 种点数,每种 8 张(2 副 × 4 花色)
|
||||
- 大王、小王:各 2 张(每副各 1 对)
|
||||
|
||||
花色为黑桃 ♠、红桃 ♥、梅花 ♣、方块 ♦。
|
||||
|
||||
---
|
||||
|
||||
## 3. 主牌顺序
|
||||
|
||||
每局开局前庄家选择一个花色作为本局主牌,其余三个花色为副牌。
|
||||
|
||||
固定主牌(大王、小王、所有花色的 2 和 7)不论选择哪个花色为主,恒定属于主牌;选定花色里的其余普通牌也算主牌。
|
||||
|
||||
主牌从大到小排列:
|
||||
|
||||
| 顺序 | 牌 |
|
||||
| --- | --- |
|
||||
| 1 | 大王 |
|
||||
| 2 | 小王 |
|
||||
| 3 | 正 7(选定花色的 7) |
|
||||
| 4 | 副 7(其余花色的 7) |
|
||||
| 5 | 正 2(选定花色的 2) |
|
||||
| 6 | 副 2(其余花色的 2) |
|
||||
| 7 及以下 | 选定花色的其余普通牌:A、K、Q、J、10、9、8、6、5 |
|
||||
|
||||
副牌(非选定花色,且非 2、7、王)从大到小:A、K、Q、J、10、9、8、6、5。
|
||||
|
||||
> **关于"对子"的判定**:一副牌里同一张牌有两副(deck1 / deck2),"对子"指**同一张具体牌的两副**(如两张♦7、两张大王)。正 7、副 7、正 2、副 2 虽在上表中各占一个**大小等级**(用于排序与比大小时,同级的三张副 7 视为等大),但**成对只认同一花色同点数的两副**——即两张**不同花色**的副 7(如♠7 + ♥7)**不构成一对**,两张不同花色的副 2 同理。拖拉机的连对判定也以此为基础(每一节连对里的每个对子都须是同花色同点数两副)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 开局与坐庄流程
|
||||
|
||||
1. 每人摸 28 张牌,剩余 8 张扣在桌面作为"底牌"。
|
||||
2. 由暂定庄家开始,按逆时针方向依次叫分:
|
||||
- 叫分范围 5 ~ 70 分,步进 5 分。
|
||||
- 叫分数值代表"庄家承诺闲家捡分总数将严格低于该数值"(即闲家捡分只要达到或超过这个数值,庄家就算没达成承诺,见第 7 节);数值越低代表叫分者越有信心,因此后叫的人必须叫出比当前更低的分数才能压过前者,不能叫相同或更高的分数。
|
||||
- **暂定庄家必须叫分,不能"不叫"**:叫分从暂定庄家开始,他必须报出一个 5 ~ 70 之间的具体分数,不存在"暂定庄家弃权、三家都不叫"这种情况;起始叫分确定之后,后面的玩家才可以选择"叫出更低的分数"或"不叫"。
|
||||
- 若某玩家直接叫出 5 分,则无人能再压过,立即确定为庄家。
|
||||
- 若一家叫分后,另外两家都选择"不叫",则该叫分玩家成为本局庄家。
|
||||
3. **摸底**:庄家确定后把 8 张底牌摸入手牌(摸完后庄家共 36 张):
|
||||
- **非 70 分坐庄**:桌面 8 张底牌**只有庄家可见**,庄家查看后摸入手牌。
|
||||
- **70 分坐庄**(触发**开底**):在庄家**摸底**之前,先把这 8 张底牌**向所有玩家(含两个闲家)翻开 3 秒**,然后才由庄家把它们摸入手牌。
|
||||
4. **选主 / 投降(同一决策点,二选一,互斥)**:
|
||||
- **选主**:庄家在 4 个花色中选定一个作为本局主牌(见第 3 节)→ 进入下一步埋牌、随后出牌。
|
||||
- **投降**:仅当**叫分为 70 分**时,此决策点才额外提供"投降"选项;选择投降表示放弃本局,**直接结束、不再选主、不埋牌、不出牌**,按 7.1 节"70 分坐庄 · 投降"的固定结果结算(基础子数 1 个,庄家直接输;算奖仍照常,见 8.4 节)。选主与投降互斥——**选了花色就等于放弃投降、正常打牌;选了投降就不再选主**。
|
||||
- 叫分 < 70 分时没有投降选项,只能选主。
|
||||
- (**开底**——70 分坐庄的 8 张底牌向所有玩家翻开 3 秒,发生在上一步"庄家摸底之前",见步骤 3。)
|
||||
- **前端表现**:此阶段界面显示 4 个花色选主按钮,且**每个花色按钮上要显示该花色在庄家手中有多少对**(供庄家判断选哪门为主);70 分坐庄时并列再显示一个投降按钮。
|
||||
5. **埋牌**(仅"选主/打牌"路径):庄家从 36 张里选出 8 张牌重新扣下("埋牌"),作为"埋牌底牌",供最后判断"扣底"使用;埋牌后庄家保留 28 张。(**本局先选主、后埋牌**——庄家先定主牌花色,知道主副后再决定埋哪 8 张。)
|
||||
6. 坐庄轮换规则(决定下一局的"暂定庄家"):
|
||||
- 第一局的暂定庄家为 **0 号座位**的玩家(此前没有打过任何牌局、还不存在"上一局的庄家",故取固定座位)。
|
||||
- 若庄家本局获胜(庄赢),下一局暂定庄家仍为该玩家(连庄)。
|
||||
- 若庄家本局失败(闲家捡分达标 / 庄家投降),下一局暂定庄家变为该玩家的下家(按逆时针顺延)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 出牌规则
|
||||
|
||||
### 5.1 跟牌基本原则
|
||||
|
||||
- 每一轮由上一轮牌面最大的一方先出牌;**每一局(每一手牌)的第一轮固定由庄家先出**——此时还没有"上一轮"可以比较,只能由庄家开局。
|
||||
- 首家出什么花色,其余两家必须跟出同花色的牌;若手中没有该花色的牌或数量不够(缺门),可以任选以下一种方式补齐:
|
||||
- **毙牌**:改用主牌打出,且牌面要大过本轮目前所有人的牌,即可抢下本轮的出牌权;
|
||||
- **垫牌**:随意出一张其他副牌,不参与、不争夺本轮出牌权。
|
||||
- 首家若出主牌,其余两家必须跟出主牌,规则同上(此时缺门只能垫副牌,不存在再用主牌毙牌的情况)。
|
||||
|
||||
### 5.2 按牌型跟牌
|
||||
|
||||
跟牌的总原则:**在本轮首家花色内,尽量凑出与首家相同的牌型结构;能凑出的高规格部分必须优先打出,凑不齐的部分才用低规格牌补足。** 服务端按此层级精确计算每名跟牌者的"必出牌"与"可选牌",玩家无法跳过更高规格的强制部分。
|
||||
|
||||
| 首家牌型 | 跟牌要求 |
|
||||
| --- | --- |
|
||||
| 单张 | 跟同花色任意单张;同花色缺门则任意牌补齐 |
|
||||
| 对子 | 有同花色对子必须打出对子;没有对子则打两张同花色单牌;同花色牌不足两张时,现有的同花色牌必须打出、不足部分任意补齐 |
|
||||
| 拖拉机(N 连对) | 见下方「拖拉机跟牌的强制层级」 |
|
||||
|
||||
#### 拖拉机跟牌的强制层级(同花色牌足够时)
|
||||
|
||||
首家出 N 连对拖拉机,跟牌者手中该花色牌数量 ≥ 首家张数时,按以下优先级**从高到低**确定必出部分,高一级能凑出就必须先凑,不允许跳过:
|
||||
|
||||
1. **有同长(N 连对)的同花色拖拉机** → 必须打出该拖拉机;若手中有多组等长拖拉机,可任选其一打出。
|
||||
2. **没有 N 连对,但持有更短的同花色拖拉机(仅 3 连对及以上首家才会走到这一步)** → 必须先拆出手中能凑到的**最长**拖拉机作为强制部分,再用次长拖拉机 / 对子 / 单张继续补足到相同张数——即"能凑多长的拖拉机,就必须先凑多长",不允许放着更长的拖拉机不出而只出零散对子。
|
||||
3. **没有任何同花色拖拉机,但对子数量足够(对子数 × 2 ≥ 首家张数)** → 用现有对子凑够张数打出(多余的对子可自选保留)。
|
||||
4. **对子数量不够(对子数 × 2 < 首家张数)** → 手中现有的同花色对子**全部**必须打出,剩余缺口用同花色单张补齐。
|
||||
5. **完全没有同花色对子** → 用同花色单张随意补齐张数。
|
||||
|
||||
**同花色牌数量不足**首家张数时(手中还有该花色的牌,但张数不够——无论首家出的是单张、对子还是 N 连对拖拉机):手中该花色的牌**全部必须打出**(不论它们是否成对、是否成拖拉机),不足的缺口用**任意其他花色的牌**补齐——补齐的牌**不要求成对、不要求成拖拉机、也不要求是主牌**,随意垫即可。特别地,**即便首家出的是拖拉机,缺口部分也不强制补成对子或拖拉机**(服务端只校验总张数,不校验缺口部分的结构)。若缺口只能用主牌补(手中没有别的副牌可垫),打出的就是"该花色副牌 + 主牌"的**混合出牌**:它不是缺门、不是毙牌,牌面判为 0,**与垫牌等效,不争夺本轮出牌权**(见术语表「混合出牌」)。
|
||||
|
||||
若**完全缺门**(手中没有该花色的任何一张牌):整手都可任意出,可选择垫牌或用主牌毙牌(见下)。
|
||||
|
||||
毙牌的前提是**完全缺门**——手中**没有本轮首家花色的任何一张牌**。只要手里还剩该花色的牌(哪怕凑不成对子、凑不成拖拉机),都必须优先打出这些同花色牌(按上文 5.2 层级),**不允许留着该花色的牌改用主牌毙**。缺门后用主牌毙牌时,牌型与张数须与首家一一对应,才算"压过"、抢下出牌权:
|
||||
|
||||
- 若首家出副牌单张,其余玩家**完全缺门**该副牌花色,可以任意出牌(含主牌);但要压过首家,出的主牌也必须是单张(数量对应)。
|
||||
- 若首家出副牌对子,其余玩家**完全缺门**该副牌花色,要压过首家,出的主牌必须也是对子(不能用任意两张主牌顶替)。
|
||||
- 若首家出副牌拖拉机(N 连对),其余玩家**完全缺门**该副牌花色,要压过首家,出的主牌必须也是同样连对组数(N 连对)的主拖拉机,总张数完全对应;单张主牌或零散的主对子都不能顶替拖拉机。(注意:只要手里还有该副牌花色的牌,哪怕凑不成同长度拖拉机,也必须先出这些同花色牌,不能改用主牌毙。)
|
||||
|
||||
### 5.3 拖拉机定义与相邻关系
|
||||
|
||||
**拖拉机 = 在各自的大小序列里「位置相邻」的两个(或以上)对子**,如 K K Q Q 为两连对(两托),K K Q Q J J 为三连对(三托)。副牌与主牌各有自己的序列,相邻规则不同,分开说:
|
||||
|
||||
#### 副牌拖拉机(非主花色)
|
||||
|
||||
**必须同一花色**,点数在下面这条序列里相邻:
|
||||
|
||||
```
|
||||
A - K - Q - J - 10 - 9 - 8 - 6 - 5
|
||||
```
|
||||
|
||||
序列里没有 7 和 2(它们是固定主牌,见第 3 节),所以 **7 不与 6、8 相连,而 6 与 8 相连**(中间隔的 7 已被抽走);同理 4、3 不在牌堆里(见第 2 节),5 是副牌序列的最小一档。**跨花色不构成连对**——♥K 对 + ♣Q 对不是拖拉机。
|
||||
|
||||
#### 主牌拖拉机(主花色 + 固定主牌)
|
||||
|
||||
主牌只有**一条**完整的大小序列(即第 3 节的主牌顺序):
|
||||
|
||||
```
|
||||
大王 - 小王 - 正7 - 副7 - 正2 - 副2 - 主A - 主K - 主Q - 主J - 主10 - 主9 - 主8 - 主6 - 主5
|
||||
```
|
||||
|
||||
只要在这条序列里**位置相邻**的对子,就构成拖拉机,**不限花色**。因此「正7 对 + 副7 对」「副7 对 + 正2 对」「副2 对 + 主A 对」「大王对 + 小王对」都是合法的两连对,尽管两个对子的花色不同。序列末段(主A 到 主5)都是**选定花色**的普通牌,其中同样是 6 与 8 相连、7 不入此段。
|
||||
|
||||
> **同一档位的两个对子不相邻**:「副7」在序列里只占**一个**位置,所以 ♥7 对 + ♣7 对是**两个平级的对子、不是拖拉机**;副2 同理。这与第 3 节「成对只认同花色同点数的两副」是同一条原则的两面——先按同花色同点数成对,再看这些对子在序列里的位置是否相邻。
|
||||
|
||||
### 5.4 甩牌详细规则
|
||||
|
||||
#### 5.4.1 基础定义
|
||||
|
||||
**甩牌**:当你是本轮首家出牌人时,一次性打出多组主牌混合牌型(单张、对子、拖拉机自由组合),一轮打完多张主牌,无需分多轮依次打出。
|
||||
|
||||
两条硬性基础禁令:
|
||||
|
||||
1. **仅主牌允许甩牌,所有副牌完全禁止甩牌**:无论外面是否剩余该花色副牌,副牌只能分开单出、出对子、出拖拉机,不能一次性打包甩出。
|
||||
2. **只有本轮先手才能执行甩牌**:若他人先出牌,你只能跟牌、垫牌、用主牌毙牌,不具备甩牌资格。
|
||||
|
||||
#### 5.4.2 生效条件(最大性原则)与报无主
|
||||
|
||||
想要成功甩牌,必须同时满足:**剩余两名对手手中,不存在任何一张、一对、一组拖拉机能压制你甩出的整套主牌组合**。简单说,全场所有更大的主牌、更大的主对子、更长的主拖拉机必须全部在你手中,两家闲家没有任何牌型能盖过你的甩牌组合。
|
||||
|
||||
**合法性由服务端在甩牌提交时自动判定**:服务端掌握全部玩家的手牌,甩牌一旦提交,立即检查另外两名玩家手中是否"持有"(不要求对方真正打出)能压过这套组合任意一部分的主牌;只要有人持有,就直接判定为甩错(见 5.4.5),不需要、也不会等到对方实际出牌反制才判定。
|
||||
|
||||
"报无主"是给玩家参考的辅助信息,**不是合法性判定的依据**(合法性判定始终由服务端按全部手牌精确计算):对局中非最后一轮,任意玩家手里没有主牌了,必须口头报无主,方便其他玩家在决定要不要甩牌前,大致判断风险:
|
||||
|
||||
- **另外两家都报了无主**:全场只剩你自己持有主牌,甩牌必然合法,无需记牌。
|
||||
- **另外两家只有一家报了无主、还有一家没报**:那一家仍持有主牌,你若甩牌需要自行记牌估算风险;但即使判断失误,最终是否算甩错仍由服务端精确判定,不取决于你的记牌是否正确。
|
||||
|
||||
#### 5.4.3 甩牌可包含的牌型组合
|
||||
|
||||
甩牌仅限本局主牌(固定主牌 + 选定主花色的普通牌),内部可自由混搭任意数量、任意种类牌型:
|
||||
|
||||
- **纯单张**:大王、小王、正 7、副 7、正 2、副 2、主花色单牌混合;
|
||||
- **纯对子**:大王对 / 小王对(各自成对,大王与小王点数不同,不能混成一对)、正七对、副七对、正二对、主花色数字对子;
|
||||
- **主拖拉机**:大小王拖拉机、七七连对、二二连对、主花色数字连对;
|
||||
- **混合搭配**:单张 + 对子 + 多连拖拉机一同甩出,无组合数量限制。
|
||||
|
||||
#### 5.4.4 其余两家应对甩牌的强制跟牌规则
|
||||
|
||||
甩牌一旦被判定合法(见 5.4.2:服务端已确认两名闲家都不持有能盖压这套组合任何一部分的主牌),闲家就不可能再用主牌把它压下去——"能不能盖压"在甩牌提交的那一刻就已经由服务端裁定完毕,不会走到"闲家出牌盖压"这一步。
|
||||
|
||||
甩牌本身可能是单张、对子、多个不同长度拖拉机自由混搭的复杂组合(见 5.4.3)。闲家跟牌沿用 5.2「拖拉机跟牌的强制层级」同一原则(高规格能凑就必须先凑:拖拉机 → 对子 → 单张),只是这里要把整套甩牌**拆解成一个个独立的分量,逐个分量分别匹配**,而不是把整套甩牌当成一个笼统的整体来对待:
|
||||
|
||||
1. **每一组拖拉机分量**:闲家手中若有同等连对数的主拖拉机,必须优先拆出来跟这一组;没有同等连对数的拖拉机,退而求其次用同等张数的主对子顶替;主对子也不够,才能拆主单张顶替。甩牌里若有多组不同长度的拖拉机,逐组分别按此规则处理。
|
||||
2. **每一组单独的对子分量**:闲家手中若有主对子,必须优先拆出来跟这一组;没有主对子,才能拆主单张顶替。
|
||||
3. **每一组单独的单张分量**:用主单张顶替即可。
|
||||
4. **闲家手中的主牌拆解完仍不够覆盖甩牌剩余的分量**:不够的那部分只能随意垫副牌。
|
||||
5. **闲家完全没有主牌**:只能随意垫任意副牌,本轮无法压制你的甩牌,本轮出牌权归你。
|
||||
|
||||
无论怎么拆解跟牌,都不影响这套甩牌本身的合法性——合法性在提交时已经由服务端按 5.4.2 判定完毕,闲家的跟牌动作只是"凑够同等张数、同等结构"的手牌垫上去,不可能真正反超压制。
|
||||
|
||||
#### 5.4.5 甩错判定与惩罚规则
|
||||
|
||||
**甩错判定**:甩牌提交时,服务端按全部玩家的真实手牌自动检查——只要两名对手任意一人**持有**(不要求已经打出)能盖压你甩牌内任意一组牌型的主牌,本次甩牌立即判定为甩错,对方是否愿意打出来压制不影响判定结果。
|
||||
|
||||
**统一惩罚标准**:
|
||||
|
||||
- 本次甩牌全部收回手牌;
|
||||
- 本轮仅强制打出你甩出组合里最小的那一张牌;
|
||||
- 本轮失去再次甩牌、一次性多出牌的资格,剩余手牌只能分轮正常打出;
|
||||
- 线下娱乐、线上棋牌平台均统一执行该惩罚,不额外扣分。
|
||||
|
||||
甩错判罚后,本轮实际只算打出了那一张最小的单张主牌,其余两家按 5.2 节的"单张"跟牌规则正常应对即可,不再涉及甩牌相关规则。
|
||||
|
||||
---
|
||||
|
||||
## 6. 捡分与扣底
|
||||
|
||||
### 6.1 分牌
|
||||
|
||||
牌面 5 记 5 分,牌面 10 记 10 分,牌面 K 记 10 分,其余牌不计分。
|
||||
|
||||
### 6.2 捡分规则
|
||||
|
||||
每一轮出完牌后比较牌面大小:
|
||||
|
||||
- 若闲家最大,则本轮牌面上的分牌全部计入闲家捡分(两个闲家谁大不影响,都算闲家捡到)。
|
||||
- 若庄家最大,则本轮牌面上的分牌作废——不计入闲家捡分总数,也不属于庄家的个人分数(庄家本身没有"捡分"这个概念,庄家的输赢只看闲家捡分总数是否达到叫分,见第 7 节)。
|
||||
|
||||
### 6.3 扣底
|
||||
|
||||
所有手牌出完后,若最后一轮闲家**用主牌**(不论单张、对子、拖拉机)**牌面压过庄家**、赢下最后一轮,才视为"扣底"(若闲家赢了最后一轮但用的不是主牌,则不算扣底)。触发后,庄家埋下的 8 张埋牌底牌翻开,若有分则计入闲家捡分。
|
||||
|
||||
埋牌底牌的分要不要翻倍、翻几倍,取决于闲家扣底所用的那手牌(赢下最后一轮的牌)本身是什么牌型:
|
||||
|
||||
| 闲家扣底所用牌型 | 倍数 |
|
||||
| --- | --- |
|
||||
| 单张主牌 | 不翻倍,按原始分值计入 |
|
||||
| 主对子(一对主牌) | 2 倍 |
|
||||
| 两连对拖拉机 | 4 倍 |
|
||||
| 三连对拖拉机 | 6 倍 |
|
||||
| N 连对拖拉机 | 2 × N 倍,以此类推递增 |
|
||||
|
||||
若最后一轮闲家是用第 5.4 节的**甩牌**(单张 + 对子 + 拖拉机混合的组合)扣底,取这套组合里**最高规格牌型**对应的倍数计算,其余较低规格的部分不额外叠加。例如甩出"单张 + 一对 + 两连对拖拉机"并以此扣底,最高规格是两连对拖拉机,整手按 4 倍计算。
|
||||
|
||||
> **重要区分**:扣底翻的是**闲家的捡分得分(grade)**本身,属于"算分"阶段;这与第 7 节里"大光 ×3""小光 ×2""升 N 级 ×N"这些子数倍率是完全不同的两件事——那些乘的是**子数**,属于"算子"阶段。两者的先后顺序是:扣底先把埋牌底牌的分(可能翻倍)计入闲家的捡分总数,这个最终捡分总数再拿去跟庄家叫分比较,判定过庄 / 小光 / 大光 / 闲家赢(升级),最后才由判定结果决定子数乘几倍。
|
||||
|
||||
---
|
||||
|
||||
## 7. 结算:子数与升级
|
||||
|
||||
叫分与结算涉及两套并存的数值:**倍数**(用于计算"捡分得分" grade)与**子数**(用于计算最终筹码输赢),两者是独立概念,不能混用。
|
||||
|
||||
> **重要区分**:本节里提到的倍率(大光 ×3、小光 ×2、过庄 ×1、升 N 级 ×N),乘的都是**子数**(最终结算给玩家的筹码单位),不是对局过程中闲家捡到的分数(捡分得分)本身。捡分得分只用来判定庄家的过庄 / 小光 / 大光,或闲家的升级级数,一旦等级判定完成,实际相乘的对象是这一局最终要结算的子数。
|
||||
|
||||
> **算子模式的选择**:7.1 + 7.2 描述的"常规算子"是默认结算方式;开房时勾选"爬坡"后,才改用 7.3 的子数梯度和保小光分段;未勾选"爬坡"时,一律按常规算子(7.1 + 7.2)结算,两套规则不会同时生效。
|
||||
|
||||
### 7.0 四层关系总览
|
||||
|
||||
结算分四层,前三层是"算子"(本节),第四层是"算奖"(第 8 节),一层的输出是下一层的输入:
|
||||
|
||||
1. **叫分 → 基础子数**:叫分档位本身决定这一局的基础子数(见 7.1 / 7.3 的对照表),叫分越低(庄家承诺越苛刻),基础子数越高。
|
||||
2. **捡分 vs 叫分 → 判定结果**:局末闲家的捡分总数(含扣底,见 6.3 节)与庄家叫分作比较,只会落入互斥的两个分支之一:
|
||||
- **闲家没达到叫分**(庄家赢)→ 结果记在**庄家**名下,分三级:**过庄 / 小光 / 大光**。
|
||||
- **闲家达到或超过叫分**(闲家赢,庄家倒庄)→ 结果记在**闲家**名下,只有一个概念:**升级**(第几级)。
|
||||
- **大光 / 小光和升级是两套完全独立的命名体系,分别只属于庄家一侧和闲家一侧,不能混用或类比**:庄家赢的时候没有"升级",闲家赢的时候也没有"大光 / 小光"。
|
||||
3. **判定结果 → 子数倍率**:过庄 / 小光 / 大光 / 升级第几级,各自对应一个子数倍率,乘上第 1 层的基础子数,得到这一局的**子数结算结果 `X`**(前三层,即"算子",到此为止)。`X` 是庄家与**每一个**闲家之间"一对一"结算的基础金额:庄家赢(过庄 / 小光 / 大光)时,两个闲家各输给庄家 `X` 子;闲家赢(升级)时,庄家各输给两个闲家 `X` 子。
|
||||
4. **算奖 → 结算倍率**:第 8 节算出的"奖数"(8.1 冲关 + 8.3 傍王,各玩家分别求和)**是"持有奖数的这个人"从其余两名玩家那里各自多收一笔钱的独立结算线,作用对象是这个人本身,不是整场结算的统一系数**——某玩家 P 这一局总共有 `N_P` 奖,则**另外两名玩家(不分庄闲)都要各自额外付给 P:`X × N_P` 子**。三名玩家可能同时都持有各自的奖数,会产生最多 3 条互相独立的额外支付线,与第 1~3 层的基础输赢结算加总,才是每个人这一局的最终盈亏。算奖的计算过程本身不受叫分、过庄 / 小光 / 大光、升级影响(只看手牌结构),详见 8.3 节的完整举例。
|
||||
|
||||
> **例外**:70 分坐庄选择投降时不走第 1~3 层模型——直接固定基础子数 1 个、庄家输掉,不比较捡分、不判定过庄 / 小光 / 大光,见 7.1 末尾说明;但第 4 层算奖仍然照常叠加(投降是选主阶段的选择、未选主也未埋牌,按庄家"发牌 + 底牌"共 36 张、无主牌花色计算,见 8.4 节)。
|
||||
|
||||
**查表索引**:本节按叫分档位给出了完整判定表,位置如下:
|
||||
|
||||
| 叫分 | 常规算子(未勾选"爬坡") | 爬坡(勾选"爬坡") |
|
||||
| --- | --- | --- |
|
||||
| 70 分 | 7.2.1 末尾"70 分坐庄" | 与常规算子一致,见 7.3.3 末尾说明 |
|
||||
| 65 / 60 / 55 / 50 / 45 分 | 7.2.1 对应表格 | 与常规算子数值一致(45 分子数不同),见 7.3.3 末尾说明 |
|
||||
| 40 / 35 / 30 / 25 / 20 / 15 / 10 分 | 7.2.1"40 分及以下坐庄"通用规则 | 7.3.3 对应表格(子数、`Q` 值均与常规算子不同) |
|
||||
| 5 分 | 7.2.1"40 分及以下坐庄"内的"5 分坐庄没有小光"说明 | 7.3.3"5 分坐庄"(没有小光) |
|
||||
|
||||
### 7.1 常规算子:叫分 → 基础子数(未勾选"爬坡"时的默认规则)
|
||||
|
||||
叫分范围是 5 ~ 70 分(见第 4 节),常规算子下完整的"叫分 → 基础子数"对照:
|
||||
|
||||
| 叫分 | 65 分 | 60 分 | 55 分 | 50 分及以下(50 / 45 / 40 / 35 / 30 / 25 / 20 / 15 / 10 / 5) |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 基础子数 | 2 个子 | 3 个子 | 4 个子 | 统一 6 个子,不再随叫分继续增加 |
|
||||
|
||||
即叫分从 65 分往下每降 5 分子数依次是 2 → 3 → 4,但**到 50 分封顶**:50 分及以下所有档位(50、45、40、35、30、25、20、15、10、5)基础子数都固定是 6 个子,不会像爬坡那样继续往上加(爬坡的区别见 7.3)。
|
||||
|
||||
70 分叫庄是特例,基础子数不接着上面的阶梯往下走,而是在"投降"和"打牌"两条路径下分别取值:
|
||||
|
||||
- **选择投降**:基础子数按 **1 个**算,庄家直接输 1 个子(此时局面还没有真正出牌捡分,不经过 7.2 的大光 / 小光 / 升级判定,是一次性的固定结算)。
|
||||
- **选择打牌**:基础子数按 **2 个**算(与 65 分相同),之后**照样按 7.2 节的方法,用捡分总数判定大光 / 小光 / 过庄 / 升级**,不是不细分——完整判定表见 7.2.1。
|
||||
|
||||
70 分叫庄时,发牌时桌面留下的那 8 张底牌(见第 4 节;不是埋牌时埋下的那 8 张「埋牌底牌」)需要**在庄家把它们摸入手牌之前、向所有玩家亮出 3 秒**,之后庄家才摸入手中;投降和打牌两种情况都适用(因为亮牌发生在选主 / 投降决策之前)。
|
||||
|
||||
### 7.2 捡分 vs 叫分:庄家的过庄 / 小光 / 大光,闲家的升级(常规算子)
|
||||
|
||||
#### 7.2.0 判定方法(对任意叫分档位通用)
|
||||
|
||||
**大光的判定条件是固定的、不随叫分或算子模式变化**:闲家捡分 `grade == 0`(一分未捡到)就是大光,常规算子和爬坡完全一样。会随叫分变化的,是**小光 / 过庄**之间的分界线——常规算子下这条分界线**固定为 40 分**,不随叫分变化(这是与爬坡最核心的区别,爬坡的分界线 `Q` 会随叫分变化,见 7.3.2)。设叫分为 `call`,闲家捡分为 `grade`,按以下顺序依次判断(**必须先判断第 1 步,即是否达标,再判断大光 / 小光 / 过庄**,顺序不能颠倒):
|
||||
|
||||
1. **先判断是否达标**:若 `grade ≥ call`,闲家赢,判为**升级**——先升 1 级,此后每再多捡 40 分再升 1 级;子数倍率就是当前的级数本身:升 1 级 ×1、升 2 级 ×2、升 3 级 ×3……逐级 **+1**,不是逐级翻倍。
|
||||
2. **未达标(`grade < call`)时,再看捡分具体落在哪个区间**:
|
||||
- `grade == 0`(一分未捡到)→ **庄家:大光**,子数倍率 ×3。
|
||||
- `0 < grade < 40`(捡了一点,但没满 40 分)→ **庄家:小光**,子数倍率 ×2。
|
||||
- `40 ≤ grade < call`(捡满 40 分,但仍没达标)→ **庄家:过庄**,子数倍率 ×1(即基础子数,不翻倍)。
|
||||
|
||||
之所以要先判断"是否达标":当叫分本身 ≤ 40 时(比如叫 35 分坐庄),闲家只要捡到 35 分就已经达标升级了,根本不可能出现"捡到 40 分却还没达标"这种情况——"过庄"这一档这时候是空的,直接从"小光"跳到"升级"(完整例子见 7.2.1"40 分及以下坐庄")。
|
||||
|
||||
**大光 / 小光只描述庄家(闲家没达标时,庄家赢得有多彻底);升级只描述闲家(闲家达标之后,赢得有多彻底),两套命名不通用、不能互换。**
|
||||
|
||||
#### 7.2.1 常规算子完整判定表
|
||||
|
||||
按上面的方法,叫分 > 40 分的档位(65 / 60 / 55 / 50 / 45)"过庄"区间是存在的;叫分 ≤ 40 分的档位(40 / 35 / 30 / 25 / 20 / 15 / 10 / 5)"过庄"区间不存在,闲家捡分只要达到叫分就直接升级。
|
||||
|
||||
**65 分坐庄(基础 2 个子)**
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 6 个子 |
|
||||
| 1 ~ 39 分 | 庄家:小光 | ×2 | 4 个子 |
|
||||
| 40 ~ 64 分 | 庄家:过庄 | ×1 | 2 个子 |
|
||||
| 65 ~ 104 分 | 闲家:升 1 级 | ×1 | 2 个子 |
|
||||
| 105 ~ 144 分 | 闲家:升 2 级 | ×2 | 4 个子 |
|
||||
| 145 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +2 个子 |
|
||||
|
||||
**60 分坐庄(基础 3 个子)**
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 9 个子 |
|
||||
| 1 ~ 39 分 | 庄家:小光 | ×2 | 6 个子 |
|
||||
| 40 ~ 59 分 | 庄家:过庄 | ×1 | 3 个子 |
|
||||
| 60 ~ 99 分 | 闲家:升 1 级 | ×1 | 3 个子 |
|
||||
| 100 ~ 139 分 | 闲家:升 2 级 | ×2 | 6 个子 |
|
||||
| 140 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +3 个子 |
|
||||
|
||||
**55 分坐庄(基础 4 个子)**
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 12 个子 |
|
||||
| 1 ~ 39 分 | 庄家:小光 | ×2 | 8 个子 |
|
||||
| 40 ~ 54 分 | 庄家:过庄 | ×1 | 4 个子 |
|
||||
| 55 ~ 94 分 | 闲家:升 1 级 | ×1 | 4 个子 |
|
||||
| 95 ~ 134 分 | 闲家:升 2 级 | ×2 | 8 个子 |
|
||||
| 135 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +4 个子 |
|
||||
|
||||
**50 分坐庄(基础 6 个子)**
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 18 个子 |
|
||||
| 1 ~ 39 分 | 庄家:小光 | ×2 | 12 个子 |
|
||||
| 40 ~ 49 分 | 庄家:过庄 | ×1 | 6 个子 |
|
||||
| 50 ~ 89 分 | 闲家:升 1 级 | ×1 | 6 个子 |
|
||||
| 90 ~ 129 分 | 闲家:升 2 级 | ×2 | 12 个子 |
|
||||
| 130 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 |
|
||||
|
||||
**45 分坐庄(基础 6 个子——从这一档开始进入"50 分及以下统一 6 个子"的范围)**
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 18 个子 |
|
||||
| 1 ~ 39 分 | 庄家:小光 | ×2 | 12 个子 |
|
||||
| 40 ~ 44 分 | 庄家:过庄 | ×1 | 6 个子 |
|
||||
| 45 ~ 84 分 | 闲家:升 1 级 | ×1 | 6 个子 |
|
||||
| 85 ~ 124 分 | 闲家:升 2 级 | ×2 | 12 个子 |
|
||||
| 125 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 |
|
||||
|
||||
**40 分及以下坐庄(40 / 35 / 30 / 25 / 20 / 15 / 10 / 5,基础都是 6 个子;此时 `call ≤ 40`,没有"过庄"这一档)**
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 18 个子 |
|
||||
| 1 分 ~(叫分 − 1)分 | 庄家:小光 | ×2 | 12 个子 |
|
||||
| 达到叫分 | 闲家:升 1 级 | ×1 | 6 个子 |
|
||||
| 达到叫分 + 40 分 | 闲家:升 2 级 | ×2 | 12 个子 |
|
||||
| 此后每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 |
|
||||
|
||||
例如叫 10 分坐庄(基础 6 个子):0 分为大光(18 子);1 ~ 9 分为小光(12 子);捡到 10 分起升 1 级(6 子);捡到 50 分起升 2 级(12 子);之后每再多 40 分再升 1 级(每级再 +6 子)。
|
||||
|
||||
> **5 分坐庄没有"小光"**:分牌只有 5 分、10 分两种面值(见 6.1 节),闲家捡分永远是 5 的倍数。上面"1 分 ~(叫分 − 1)分为小光"这一行,在叫分为 5 分时对应区间是 `1 ~ 4 分`,里面不存在任何 5 的倍数,是空区间。所以叫 5 分坐庄时,闲家捡分只有 0(大光)和达到 5 分及以上(直接升 1 级,之后每再多 40 分再升 1 级)两种结果,没有小光这一档;这是"叫分 ≤ 40 分"这组档位里唯一的特例,其余档位(40/35/30/25/20/15/10)小光区间都至少包含一个 5 的倍数,正常存在。
|
||||
|
||||
**70 分坐庄**
|
||||
|
||||
- **投降**:基础子数按 1 个算,庄家直接输 1 个子,不经过大光 / 小光 / 过庄 / 升级判定(还没出牌捡分,直接放弃)。
|
||||
- **打牌**:基础子数按 2 个算,之后按 7.2.0 的方法正常判定,`Q` 同样固定为 40 分——判定表和 65 分坐庄完全一致,只是把"基础 2 个子"原样代入(数值恰好和 65 分相同):
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 6 个子 |
|
||||
| 1 ~ 39 分 | 庄家:小光 | ×2 | 4 个子 |
|
||||
| 40 ~ 69 分 | 庄家:过庄 | ×1 | 2 个子 |
|
||||
| 70 ~ 109 分 | 闲家:升 1 级 | ×1 | 2 个子 |
|
||||
| 110 ~ 149 分 | 闲家:升 2 级 | ×2 | 4 个子 |
|
||||
| 150 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +2 个子 |
|
||||
|
||||
### 7.3 爬坡:叫分 → 基础子数与保小光分段(可选规则,开房时勾选"爬坡"生效)
|
||||
|
||||
#### 7.3.1 基础子数:叫分 → 子数的完整对照
|
||||
|
||||
叫分从 65 分(2 个子)开始,每叫低 5 分加 1 个子,直到 50 分(6 个子)——这一段与常规算子的四档数值完全相同;50 分以下,继续按每叫低 5 分加 1 个子延伸,一直到叫分范围的下限 5 分:
|
||||
|
||||
| 叫分 | 65 | 60 | 55 | 50 | 45 | 40 | 35 | 30 | 25 | 20 | 15 | 10 | 5 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 基础子数 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|
||||
|
||||
#### 7.3.2 保小光分段:叫分 → 小光 / 过庄分界线(Q 值)
|
||||
|
||||
判定方法与 7.2.0 完全相同,只是把固定的 `Q = 40` 换成按叫分区间取值的 `Q`;**大光的判定条件不受 `Q` 影响,永远是 `grade == 0`**,常规算子和爬坡完全一样——`Q` 只决定"小光"和"过庄"之间的分界线:
|
||||
|
||||
| 叫分区间 | 小光 / 过庄的分界线(Q) |
|
||||
| --- | --- |
|
||||
| ≥ 45 分(含 65 / 60 / 55 / 50 / 45) | 40 分 |
|
||||
| 40 / 35 分 | 20 分 |
|
||||
| 30 / 25 分 | 15 分 |
|
||||
| 20 / 15 分 | 10 分 |
|
||||
| 10 分 | 5 分 |
|
||||
| 5 分 | 5 分(升级级距仍是 5,只是"小光"这一档因为区间为空而不存在,见下方说明) |
|
||||
|
||||
`≥ 45 分`这一档的 `Q = 40`,与 7.2 节常规算子沿用的分界线数值相同,只是子数改按 7.3.1 的公式取值(65/60/55/50 与常规算子数值一致,45 分是爬坡独有的新增档位)。
|
||||
|
||||
**5 分档没有"小光"**:分牌只有牌面 5(5 分)和牌面 10 / K(10 分)两种面值(见 6.1 节),闲家的捡分总数(`grade`)因此永远是 5 的倍数(0、5、10、15…),不可能出现 1 ~ 4 这样的中间值。当叫分是 5 分时,"小光"原本对应的区间是 `0 < grade < 5`,这个开区间里不存在任何 5 的倍数,也就是说这个区间**必然是空的**——闲家捡分要么是 0(大光),要么一旦捡到任何分牌(最少 5 分)就已经达到叫分、直接升级,中间不存在"捡到了一点、但没达标"的"小光"状态。所以 5 分档只有**大光**和**升级**两种结果,没有"小光"这一档;但 `Q` 值本身仍然是 5(与 10 分档一样),只是在"小光 / 过庄分界线"这个用途上区间恰好收窄为空,`Q = 5` 仍然继续作为"升级级距"生效(见 7.3.3 的 5 分坐庄判定表)。
|
||||
|
||||
**升级级距的通用规则**:不管哪个叫分档位,闲家赢了之后,"超过叫分多少分算升一级",用的分数差**就是该档位的 `Q` 值**——即上表里每一档的 `Q` 值,既是"小光 / 过庄"的分界线,也是"升级"的级距单位,两者共用同一个数字。例如叫分 40 分时 `Q = 20`:闲家捡分达到 40 分先升 1 级,之后每再多捡 20 分(60、80、100…)就再升一级。
|
||||
|
||||
#### 7.3.3 各分段的完整判定表(按 7.2.0 的方法代入对应 `call`、`Q`、基础子数)
|
||||
|
||||
**40 分坐庄(基础 8 个子,Q = 20)**
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 24 个子 |
|
||||
| 1 ~ 19 分 | 庄家:小光 | ×2 | 16 个子 |
|
||||
| 20 ~ 39 分 | 庄家:过庄 | ×1 | 8 个子 |
|
||||
| 40 ~ 59 分 | 闲家:升 1 级 | ×1 | 8 个子 |
|
||||
| 60 ~ 79 分 | 闲家:升 2 级 | ×2 | 16 个子 |
|
||||
| 80 分起,每再多 20 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +8 个子 |
|
||||
|
||||
**35 分坐庄(基础 9 个子,Q = 20)**
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 27 个子 |
|
||||
| 1 ~ 19 分 | 庄家:小光 | ×2 | 18 个子 |
|
||||
| 20 ~ 34 分 | 庄家:过庄 | ×1 | 9 个子 |
|
||||
| 35 ~ 54 分 | 闲家:升 1 级 | ×1 | 9 个子 |
|
||||
| 55 ~ 74 分 | 闲家:升 2 级 | ×2 | 18 个子 |
|
||||
| 75 分起,每再多 20 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +9 个子 |
|
||||
|
||||
**30 分坐庄(基础 10 个子,Q = 15)**
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 30 个子 |
|
||||
| 1 ~ 14 分 | 庄家:小光 | ×2 | 20 个子 |
|
||||
| 15 ~ 29 分 | 庄家:过庄 | ×1 | 10 个子 |
|
||||
| 30 ~ 44 分 | 闲家:升 1 级 | ×1 | 10 个子 |
|
||||
| 45 ~ 59 分 | 闲家:升 2 级 | ×2 | 20 个子 |
|
||||
| 60 分起,每再多 15 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +10 个子 |
|
||||
|
||||
**25 分坐庄(基础 11 个子,Q = 15)**
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 33 个子 |
|
||||
| 1 ~ 14 分 | 庄家:小光 | ×2 | 22 个子 |
|
||||
| 15 ~ 24 分 | 庄家:过庄 | ×1 | 11 个子 |
|
||||
| 25 ~ 39 分 | 闲家:升 1 级 | ×1 | 11 个子 |
|
||||
| 40 ~ 54 分 | 闲家:升 2 级 | ×2 | 22 个子 |
|
||||
| 55 分起,每再多 15 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +11 个子 |
|
||||
|
||||
**20 分坐庄(基础 12 个子,Q = 10)**
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 36 个子 |
|
||||
| 1 ~ 9 分 | 庄家:小光 | ×2 | 24 个子 |
|
||||
| 10 ~ 19 分 | 庄家:过庄 | ×1 | 12 个子 |
|
||||
| 20 ~ 29 分 | 闲家:升 1 级 | ×1 | 12 个子 |
|
||||
| 30 ~ 39 分 | 闲家:升 2 级 | ×2 | 24 个子 |
|
||||
| 40 分起,每再多 10 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +12 个子 |
|
||||
|
||||
**15 分坐庄(基础 13 个子,Q = 10)**
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 39 个子 |
|
||||
| 1 ~ 9 分 | 庄家:小光 | ×2 | 26 个子 |
|
||||
| 10 ~ 14 分 | 庄家:过庄 | ×1 | 13 个子 |
|
||||
| 15 ~ 24 分 | 闲家:升 1 级 | ×1 | 13 个子 |
|
||||
| 25 ~ 34 分 | 闲家:升 2 级 | ×2 | 26 个子 |
|
||||
| 35 分起,每再多 10 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +13 个子 |
|
||||
|
||||
**10 分坐庄(基础 14 个子,Q = 5——这一档同样没有"小光")**
|
||||
|
||||
分牌只有 5 分、10 分两种面值,捡分永远是 5 的倍数。`Q = 5` 时,"小光"对应的区间 `0 < grade < 5` 里不存在任何 5 的倍数,同样是空区间:闲家捡分不可能停在 1 ~ 4 分,只要捡到分就已经是 5 分(直接落入"过庄"区间)。
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 42 个子 |
|
||||
| 5 ~ 9 分 | 庄家:过庄(没有小光这一档) | ×1 | 14 个子 |
|
||||
| 10 ~ 14 分 | 闲家:升 1 级 | ×1 | 14 个子 |
|
||||
| 15 ~ 19 分 | 闲家:升 2 级 | ×2 | 28 个子 |
|
||||
| 20 分起,每再多 5 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +14 个子 |
|
||||
|
||||
**5 分坐庄(基础 15 个子,没有"小光",见 7.3.2 的说明)**
|
||||
|
||||
5 分是叫分范围的下限,捡分只能是 5 的倍数:一旦捡到任何分牌就已经是 5 分,等于直接达标(`grade ≥ call = 5`)。所以"小光"和"过庄"这两个区间都不存在,闲家捡分只有 0(大光)和达到 5 分及以上(直接升 1 级)两种结果。
|
||||
|
||||
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 分 | 庄家:大光 | ×3 | 45 个子 |
|
||||
| 5 ~ 9 分 | 闲家:升 1 级(没有小光、过庄这两档) | ×1 | 15 个子 |
|
||||
| 10 ~ 14 分 | 闲家:升 2 级 | ×2 | 30 个子 |
|
||||
| 15 分起,每再多 5 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +15 个子 |
|
||||
|
||||
`≥ 45` 分档(65 / 60 / 55 / 50 / 45)判定逻辑与 7.2.1 完全相同(Q = 40),只是 45 分档基础子数按 7.3.1 取 7 个子,其余数值套用 7.2.1 对应叫分的表即可,此处不重复列出。
|
||||
|
||||
70 分叫庄时与常规算子规则一致(见 7.2.1 末尾"70 分坐庄":投降固定基础子数 1 个,打牌基础子数 2 个并正常套用大光 / 小光 / 过庄 / 升级判定),不适用本节的保小光分段。
|
||||
|
||||
---
|
||||
|
||||
## 8. 算奖规则
|
||||
|
||||
算奖在**每个小局结算时**计算(与第 7 节的子数结算属于同一次小局结算,不是等大局所有局数打完才统一算,也不是在出牌过程中途算)。8.1 冲关只计入庄家;8.3 傍王是额外叠加的独立奖励,庄家、闲家都要算——两者的计入范围不同,注意区分。
|
||||
|
||||
**8.1 冲关只认庄家的身份,不看闲家的手牌是否达标**:即使某位闲家的手牌结构凑巧也满足 8.1 表里的条件(比如手里也有 3 张王),这份冲关也**不会**计入这位闲家——冲关从规则上就只针对庄家一人。闲家想要获得算奖,只能靠 8.3 傍王(且必须房间勾选了这条可选规则)。
|
||||
|
||||
**算奖依据的手牌快照时间点**:不管是 8.1(只看庄家)还是 8.3 傍王(庄闲都算),都按**静态初始手牌**计算——庄家是埋牌完成后的那一刻手牌(28 张),闲家是发牌完成后的那 28 张手牌;这个快照全程固定,不随之后的出牌、被吃、被打出而改变。**例外**:70 分投降局庄家未选主也未埋牌,其快照为"发牌 + 底牌"共 36 张(无主牌花色),见 8.4 节。
|
||||
|
||||
**算奖(8.1 / 8.3)与亮牌(8.2)是两个互不干涉的独立概念**:算奖是结算阶段"该给多少额外结算分"的规则;亮牌只是出牌开始前"要不要向对手公开一部分手牌"的展示规则。两者各自有自己的触发条件,只是恰好都以庄家埋牌后的手牌结构为依据,因此部分门槛数值相同——但达成亮牌的条件不代表一定触发算奖,反之亦然,具体对照见 8.2 末尾的说明。
|
||||
|
||||
### 8.1 冲关
|
||||
|
||||
> **旧称「常规算奖」**,现统一叫**冲关**(与服务端字段 `chongguan`、界面「冲关分」「冲关牌型」同口径)。
|
||||
> 它只是「算奖」的**其中一个分量**——另一个是 8.3 傍王;两者求和才是某玩家的总奖数 `N`(见 8.4)。
|
||||
|
||||
|
||||
| 组合 | 奖数 |
|
||||
| --- | --- |
|
||||
| 三个王 | 1 奖 |
|
||||
| 四个王 | 3 奖 |
|
||||
| 六个 7 | 1 奖 |
|
||||
| 七个 7 | 2 奖 |
|
||||
| 八个 7 | 3 奖 |
|
||||
| 六个 2 | 1 奖 |
|
||||
| 七个 2 | 2 奖 |
|
||||
| 八个 2 | 3 奖 |
|
||||
|
||||
三王 / 四王在拥有"正 7"及以后连续对子的情况下,每多一对连续对子再加 1 奖,连续顺序为:
|
||||
|
||||
```
|
||||
正7 - 副7 - 正2 - 副2 - A - K - Q - J - 10 - 9 - 8 - 6 - 5
|
||||
```
|
||||
|
||||
其中从 A 到 5(即 A、K、Q、J、10、9、8、6、5 这一段普通点数)都必须是选定花色的主牌,才能计入连续对子——非主花色的普通对子不算数,链条到这里就断了。
|
||||
|
||||
### 8.2 亮牌规则
|
||||
|
||||
**亮牌与算奖(8.1 / 8.3)是两个独立概念,互不干涉**:亮牌只是"要不要向对手公开一部分手牌"的展示规则,本身不产生任何奖数、不影响结算;算奖该怎么算、算多少,完全按 8.1 / 8.3 的规则来,不因为触发了亮牌而增加或减少。
|
||||
|
||||
**亮牌要求**(仅针对庄家):庄家**埋牌后**、**出牌开始前**,若手中剩余的**固定主牌**满足下表任一条件,就要向两个闲家**亮出自己全部固定主牌的具体牌面**。
|
||||
|
||||
| 触发条件(任一满足即亮牌) |
|
||||
| --- |
|
||||
| 固定主牌(双王 + 全部 2 + 全部 7)总数 ≥ 10 张 |
|
||||
| 王 ≥ 3 张 |
|
||||
| 7 ≥ 6 张 |
|
||||
| 2 ≥ 6 张 |
|
||||
|
||||
**亮出的内容是固定的一份**——**庄家手中全部固定主牌的具体牌面**(双王 + 全部 2 + 全部 7;**不含**主花色的普通牌 A/K/Q/J/10/9/8/6/5)。不论是被上表哪一条触发、还是同时满足多条,亮出的都是这同一份牌,不因触发条件不同而增减。
|
||||
|
||||
- **不限叫分**:任何叫分档位都适用,不是 70 分坐庄专有。
|
||||
- **仅可查牌模式**:房间勾选了"不查牌"时,闲家看不到亮牌(见第 9 节)。
|
||||
- **只亮庄家的**:闲家不亮牌。
|
||||
- 亮牌依据的是**埋牌后**的手牌,与 8.1 冲关用的是同一份静态快照(见第 8 节开头),全局固定、不随之后出牌缩水。
|
||||
|
||||
> **亮牌给的是牌面,不是统计**。这一点与「余主公示」(只给剩余主牌数与对子数,见第 9 节)不同;与「明牌」(查看**他家**全部未出主牌的牌面,见第 9 节)也不同——亮牌是**庄家主动公开自己的**,明牌是**闲家去看别人的**。
|
||||
|
||||
**亮牌门槛与 8.1 冲关起点的数值巧合,不代表两者是同一条规则**:王 ≥ 3 张 / 7 ≥ 6 张 / 2 ≥ 6 张这三个门槛,数值上恰好分别与 8.1 表里"三个王""六个 7""六个 2"的冲关起点相同,达到时会同时触发"亮牌"和"冲关"这两件独立的事——但它们是各自独立判定后凑巧同时发生,不是"亮牌导致冲关"或者"冲关导致亮牌"。这一点在"固定主牌 ≥ 10 张"这一档表现得最清楚:它只在亮牌这条规则里有定义、会触发亮牌,但 8.1 的冲关表里根本没有为这一档定义奖数,所以**不产生任何冲关奖**——如果亮牌和冲关是同一回事,这里就该矛盾;现在不矛盾,正说明两者互不干涉。
|
||||
|
||||
### 8.3 傍王(可选规则)
|
||||
|
||||
开房时勾选"傍王"后,每张王额外算一奖:这个奖是**庄家、闲家都要各自计算**(每个人按自己手里的王算,不限庄家)。
|
||||
|
||||
**傍王奖是在 8.1 冲关之外额外叠加的一份奖励,与冲关互不冲突、不互相替代**:庄家照常按 8.1 的规则算冲关(只计庄家),同时如果勾选了傍王,庄家、闲家还要**各自另外**再按自己手里的王数算一份傍王奖;两份奖励分开计算,最后相加。
|
||||
|
||||
### 8.4 算奖如何影响最终结算
|
||||
|
||||
**算奖是"持有奖数的那个人"从其余两名玩家那里各自多收一笔钱的独立结算线,不是把第 7 节算出的输赢结果整体重新乘一遍**:设第 7 节前三层算出的结算子数为 `X`(庄家与每个闲家之间"一对一"结算的基础金额,见 7.0 第 3 层),玩家 P 这一局总共积累了 `N_P` 奖(8.1 冲关 + 8.3 傍王,各自求和后相加;只有庄家可能有 8.1 的分量,8.3 傍王则庄闲都可能有):
|
||||
|
||||
- 若 `N_P > 0`,**另外两名玩家(不分是庄家还是闲家)都要各自额外付给 P:`X × N_P` 子**;
|
||||
- 若 `N_P = 0`,P 没有额外收入。
|
||||
|
||||
三名玩家可能同时都持有各自的奖数,这时会产生最多 3 条互相独立、方向不同的额外支付线,互不冲突、按各自的 `N` 值同时生效,最后与第 1~3 层的基础输赢结算加总,才是每个人这一局的最终盈亏。
|
||||
|
||||
**举例**(`X = 6` 个子,庄家为 A,闲家为 B、C):
|
||||
|
||||
- 若庄家 A 本局有 1 奖(`N_A = 1`):B、C 各自额外付给 A `6 × 1 = 6` 子。
|
||||
- 若闲家 B 同时勾选了"傍王"、本局有 2 奖(`N_B = 2`):A、C 各自额外付给 B `6 × 2 = 12` 子。
|
||||
- 这两条支付线同时成立、互不影响:A 从 B、C 各多收 6 子,B 从 A、C 各多收 12 子——包括"闲家 C 要付给闲家 B"这样的支付,即使 B、C 之间在第 1~3 层的基础输赢结算里并不直接结算(基础结算只发生在"庄家 vs 单个闲家"之间),算奖这一层是单独叠加在所有玩家两两之间的。
|
||||
|
||||
以下三点已确认:
|
||||
|
||||
- **庄家倒庄(闲家达标升级)时,8.1 冲关仍然生效**:冲关只看庄家埋牌后的手牌结构,与本局谁输谁赢无关。
|
||||
- **70 分投降时,仍叠加算奖倍率**:投降只是跳过了第 1~3 层的叫分 / 捡分判定(此时 `X = 1`)。投降是选主阶段的选择,庄家**未选主、未埋牌**,故算奖按庄家"发牌 + 底牌"共 36 张手牌计算——因没有选定主牌花色,**没有连对链**,只有三 / 四王、六~八个 7、六~八个 2 这些组合,以及(勾选傍王时)按王数的傍王奖计入;8.1(及勾选傍王时的 8.3)照常计算并叠加。
|
||||
- **算奖对象的身份限制会带进 `N` 的计算**:8.1 只算庄家、8.3 傍王勾选后才不分庄闲(见第 8 节开头说明),把这名玩家实际适用的分量求和,才是他自己的 `N`;"其余两人向他支付 `X × N`"这条支付规则本身不因身份而改变,但闲家的 `N` 从一开始就不可能包含 8.1 的分量。
|
||||
|
||||
---
|
||||
|
||||
## 9. 牌局查看模式
|
||||
|
||||
"查牌"指的是**回看那些不该随时可见的信息**:已经打过去的牌、他家手里还剩多少主牌、他家主牌的具体牌面。房间的"查牌模式"开关决定下列四项功能**整体**是否提供。
|
||||
|
||||
- **可查牌模式**(四项功能全部提供):
|
||||
1. **出牌历史**:当前牌局中,所有玩家已经打出的牌(含之前每一轮打出、已被收走的牌),任何时候都可以回看。
|
||||
2. **余主公示**:一旦有玩家的主牌全部打空(即 5.4.2 节所说的"报无主"状态;"主牌"指大王、小王、所有花色的 2、所有花色的 7,以及主花色的其余普通牌,见第 3 节),系统就为**全体三人**(含刚刚报无主的这位玩家自己)显示另外两家各自的主牌数量、以及这些主牌里有多少对子。**只给数量、不给具体牌面**——要看具体牌面是下一条的"明牌"。
|
||||
3. **明牌**:同时**为这三个人都**出现一个"明牌"按钮——判定依据是「**场上有人**报无主」,不是「**自己**报无主」,所以报无主的那位和另外两位一样都能点。点击后可以查看另外两家手中全部主牌的**具体牌面**(不只是数量 / 对子结构),再点一次取消查看、恢复隐藏;**不限次数,整个出牌阶段随时可查**。
|
||||
4. 庄家若触发了 8.2 节的"亮牌要求",两个闲家可以看到**庄家全部固定主牌的具体牌面**(见 8.2 节)。
|
||||
|
||||
- **不可查牌模式**:以上四项**一项都不提供**——
|
||||
- **不提供出牌历史**:之前各轮打出过什么牌一律不能回看,只能靠玩家自己记牌;
|
||||
- **不提供余主公示**:即使有玩家报无主,也不会显示任何人的主牌数量、对子结构,也没有"明牌"按钮;
|
||||
- 庄家即使触发了 8.2 节的亮牌条件,闲家也看不到那些牌(见 8.2 节)。
|
||||
|
||||
> **"查牌"与"当前这一轮桌面上的牌"是两回事**:本轮已经出过牌的玩家、打在桌面上的这几张牌,**两种模式下都必须对全体可见**——否则后出的人无从跟牌、也无从判断本轮谁最大。查牌模式管的是**已经收走的往轮牌**与**他家手牌信息**能不能回看,不影响当前这一轮的正常出牌展示(断线重连回到牌桌时同理:当前轮桌面上的牌照常恢复,往轮历史则按本节的模式开关决定给不给)。
|
||||
|
||||
---
|
||||
|
||||
## 10. 房间设置选项
|
||||
|
||||
创建页依次提供:**局数、扣卡方式、查牌模式、傍王、爬坡**。前三项各自为独立的单选必选组,每组必须且只能选一项;傍王与爬坡为两个独立可选开关,可以都不选、只选一个或同时勾选。
|
||||
|
||||
### 10.1 局数与牌局扣卡方式
|
||||
|
||||
局数:6 局 / 12 局,单选必选。扣卡方式:房主扣卡 / AA 制(每人扣卡),单选必选。两个选项组分别选择,费用按下表联动展示。
|
||||
|
||||
| 扣卡方式 | 6 局 | 12 局 |
|
||||
| --- | --- | --- |
|
||||
| AA 制(每人扣卡) | 每人 1 张 | 每人 2 张 |
|
||||
| 房主扣卡 | 2 张 | 4 张 |
|
||||
|
||||
### 10.2 查牌模式
|
||||
|
||||
可查牌 / 不查牌,单选必选(见第 9 节)。
|
||||
|
||||
### 10.3 附加规则
|
||||
|
||||
傍王、爬坡:分别为可选开关,允许不勾选;两者可以同时勾选(互不冲突),不属于互斥单选组。未勾选傍王时不计算傍王奖励;未勾选爬坡时使用常规算子。
|
||||
|
||||
### 10.4 创建参数与默认选择
|
||||
|
||||
默认选择为:6 局、房主扣卡、可查牌;傍王和爬坡均不勾选。单选必选表示始终保留一个有效选项,不要求用户额外点击已默认选中的选项。
|
||||
|
||||
`roomtype` 为 **11 位字符串**,前 5 位依次表示局数、扣卡方式、查牌模式、傍王、爬坡;后 6 位为预留,不显示为创建选项,也不赋予玩法含义。具体编码以 [协议 §0.5](../protocol/packet_protocol.md#05-roomtype-房间选项位串) 为唯一来源。预留位的固定填充值尚待确认。
|
||||
|
||||
---
|
||||
|
||||
## 11. 牌局交互提示
|
||||
|
||||
- 选主阶段:4 个花色选主按钮上各显示"该花色在庄家手中的对子数"(供庄家判断选哪门为主);70 分坐庄时并列再显示一个投降按钮——选主与投降是同一决策点的互斥选择(见第 4 节)。
|
||||
- 首家出主牌时可以甩牌,甩牌的生效条件、跟牌规则、甩错惩罚详见 5.4 节。
|
||||
- 牌局结束需要亮出埋牌底牌。
|
||||
- 闲家在出牌时需要有 3 个提示选项:
|
||||
1. **踩**:提示对家"我能大过庄家"。
|
||||
2. **没分**:提示对家"我手上没分了"。
|
||||
3. **有分**:提示对家"我手上有分"。
|
||||
- **倒计时只作展示,不触发任何自动操作**:叫分 / 选主 / 埋牌 / 出牌四个阶段都会给出一个倒计时秒数,用于界面提醒当前该谁操作。**倒计时归零后不做任何代打**——不自动叫分、不自动选主、不自动埋牌、不自动出牌,也不判负、不跳过该玩家;轮到谁而谁不操作,牌局就停在这一步一直等。
|
||||
|
||||
> **"无超时托管"是确认过的规则,不是待实现的缺口**:本局不设服务端代打/AI 托管,也不因超时改变任何对局状态。若玩家长时间不操作导致牌局停住,由玩家走房间的**解散**流程收场,按当前累计分结算(见 12.2)。**后续核对不要把"没有超时动作"当成缺陷去补"超时托管/自动出牌"。**
|
||||
|
||||
---
|
||||
|
||||
## 12. 完整牌局游玩流程
|
||||
|
||||
本节把前面各节的规则串成一局(一个"小局")从头到尾的完整流程,供整体理解与实现参照。这里只给出**步骤与先后顺序**,每一步的具体细则以其对应章节为准(避免与细则重复而产生分歧)。
|
||||
|
||||
### 12.1 一个小局的完整流程
|
||||
|
||||
1. **开局准备**:房间按第 10 节的设置(局数、扣卡方式、傍王、爬坡、查牌)建好后开战。第一局的暂定庄家为 0 号座位;之后每局按 4.6 的轮庄规则确定暂定庄家。
|
||||
|
||||
2. **发牌**(第 2 节):92 张牌洗匀,三家各摸 28 张,剩余 8 张扣在桌面作为"底牌"。
|
||||
|
||||
3. **叫分坐庄**(4.2):由暂定庄家起、按逆时针依次叫分。暂定庄家必叫(5~70 分、步进 5,不能"不叫");其后每家只能叫比当前更低的分,或选择"不叫";有人叫出 5 分即立即坐庄,或一家叫分后另外两家都"不叫"也即坐庄。叫分越低表示庄家对"压住闲家捡分"越有信心,对应的基础子数越高(第 7 节)。
|
||||
|
||||
4. **摸底**:庄家坐定后把 8 张底牌摸入手牌(摸完共 36 张)。非 70 分:这 8 张仅庄家可见。**70 分坐庄先触发「开底」:庄家摸底之前,先把这 8 张向所有玩家翻开 3 秒,再摸入手牌**(4.4 / 第 4 节)。
|
||||
|
||||
5. **选主 / 投降(同一决策点,二选一,互斥)**(第 4 节):
|
||||
- **选主**:庄家选定一门花色为本局主牌(大王、小王、所有花色的 2、7 恒为主牌)→ 进入下一步埋牌。前端此阶段每个花色按钮上要显示该花色在庄家手中的对子数;70 分坐庄时并列显示投降按钮。
|
||||
- **投降**:仅 70 分坐庄才有此选项;选投降即放弃本局,**不选主、不埋牌、不出牌**,直接按 7.1"70 分 · 投降"结算(算奖仍按下方 8 计,但用庄家 36 张、无主牌花色)。选花色就等于放弃投降、正常打牌。
|
||||
- (**开底**——70 分的 8 张底牌翻开 3 秒,发生在步骤 4"摸底之前"。)
|
||||
|
||||
6. **埋牌**(第 4 节,仅"选主/打牌"路径):庄家从 36 张里选 8 张重新扣下作为"埋牌底牌"(供最后"扣底"判断),保留 28 张。(**先选主、后埋牌**。)
|
||||
|
||||
7. **出牌对局**(第 5、6 节):
|
||||
- 第一轮固定由庄家先出;此后每轮由上一轮牌面最大的一方先出。
|
||||
- 首家出什么花色 / 牌型,其余两家按 5.1~5.3 跟牌(同花色不够时可用主牌"毙牌"抢权,或"垫牌"不争)。
|
||||
- 首家出主牌时可"甩牌"(5.4):一次性打出多组主牌组合;服务端按全场手牌判定"最大性",甩错则按 5.4.5 收回、只强制打出最小一张。副牌一律不能甩。
|
||||
- 每轮比大小定出胜者:闲家赢下的那一轮,牌面上的分牌(5→5 分、10→10 分、K→10 分)计入闲家捡分;庄家赢的轮分牌作废(6.1~6.2)。
|
||||
- 过程中:有玩家主牌出空需"报无主",可查牌模式下触发**余主公示**(为全体展示他家主牌数量 / 对子结构,第 9 节);庄家手牌达到 8.2 的门槛时触发**亮牌**。
|
||||
|
||||
8. **末轮扣底**(6.3):所有手牌出完后,若最后一轮由闲家**用主牌**赢下,则庄家埋的 8 张埋牌底牌翻开,其中的分按闲家赢牌的牌型翻倍(单张 ×1、主对 ×2、N 连对 ×2N)后计入闲家捡分。
|
||||
|
||||
9. **小局结算**(第 7、8 节,两部分同时结算):
|
||||
- **算子**:闲家总捡分(含扣底)与庄家叫分比较 → 判定庄家的**过庄 / 小光 / 大光**,或闲家的**升级第几级** → 结合叫分档的基础子数,得到庄家与每个闲家"一对一"的结算子数 `X`(勾选爬坡则改用 7.3 的梯度与分段)。
|
||||
- **算奖**:以静态初始手牌(正常局:庄家埋牌后 28 张、闲家发牌后 28 张;**70 分投降局**:庄家用发牌+底牌共 36 张、无主牌花色)计算奖数 `N` —— 冲关(8.1)**只计庄家**;勾选傍王(8.3)则庄闲每张王各算一奖。持有 `N` 奖的玩家从另外两人各多收 `X × N`(8.4)。
|
||||
- 以上两部分加总即各家本局盈亏;牌局结束需亮出埋牌底牌(第 11 节)。
|
||||
|
||||
10. **进入下一局**(4.6):庄家本局赢则连庄(暂定庄家仍是他);庄家输(闲家捡分达标 / 庄家投降)则暂定庄家顺延到其下家。回到第 2 步开新的一小局。
|
||||
|
||||
### 12.2 大局与结束
|
||||
|
||||
按房间设定的局数(6 局或 12 局,见第 10 节)打满全部小局后,进行大局结算(累计各家总分并记录战绩);若中途解散,则按当前累计分结算。
|
||||
|
||||
### 12.3 阶段速览
|
||||
|
||||
```
|
||||
建房(§10) → 发牌(§2) → 叫分坐庄(§4.2) → 摸底[70分先开底] → 选主(§3) → 埋牌(§4)
|
||||
→ [70分:投降/打牌决策(§4.4)] → 出牌对局(§5,§6.1-6.2) → 末轮扣底(§6.3)
|
||||
→ 小局结算=算子(§7)+算奖(§8) → 轮庄(§4.6) → 下一局 …… 打满局数 → 大局结算
|
||||
```
|
||||
|
||||
@@ -0,0 +1,717 @@
|
||||
# 二七王协议包列表
|
||||
|
||||
说明:`data` 均为发送方与接收方之间约定的 JSON 内容;成败判定只看推送包里的 `data.success`。
|
||||
|
||||
---
|
||||
|
||||
## 0. 通用约定(所有包适用,下文各表不再逐一重复)
|
||||
|
||||
### 0.0 术语:底牌 vs 埋牌底牌(先读这一条)
|
||||
|
||||
本项目于 2026-08-25 修订了这两个术语(`design.md` §1 已同步,本文亦已改用新称):
|
||||
|
||||
| 术语 | 指哪 8 张 | 旧称 |
|
||||
| --- | --- | --- |
|
||||
| **底牌** | 发牌时**没有发给玩家**、扣在桌面的 8 张 | ~~暗牌~~ |
|
||||
| **埋牌底牌** | 庄家**埋牌**时从手里扣下的 8 张 | ~~底牌~~ |
|
||||
|
||||
**两批牌的字段名已拆分**(2026-08-25,原先共用 `bottomcards`):
|
||||
|
||||
| 字段名 | 含义 | 出现在 |
|
||||
| --- | --- | --- |
|
||||
| **`bottomcards`** | **底牌** | `shangzhuang`、`deskinfo.ChooseMain`、`deskinfo.BuryCards` |
|
||||
| **`burycards`** | **埋牌底牌** | `maipai`、`deskinfo.PushCards` |
|
||||
| `bottom.cards` | **埋牌底牌** | 结算包的 `bottom` 分组(分组名已表明是抠底相关,字段未改名) |
|
||||
|
||||
同一个包里不会同时出现这两个字段。**下发面**:`burycards` 与 `bottomcards`(非 70 分时)都**只发给庄家**,闲家两者皆无——已由 `test/test_leak.js` 的泄露审计覆盖。
|
||||
|
||||
### 0.0b 四类「公开信息」术语(都带亮/明字,别读混)
|
||||
|
||||
| 术语 | 谁公开给谁 | 给的是什么 | 触发 | 本文相关字段 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **亮牌** | 庄家 → 两个闲家 | **具体牌面**(庄家全部固定主牌) | 庄家埋牌后固定主牌达门槛(design §8.2) | `liangpai.cards` |
|
||||
| **余主公示** | 全体 → 全体 | **只有数量**(剩余主牌数、主对数) | 任一玩家报无主(design §9) | `seatlist[seat][4]`、`baozhu` |
|
||||
| **明牌** | 他家 → 请求者 | **具体牌面**(他家全部未出主牌) | 可查牌 + **任一**玩家报无主 → **三家均可点、随时可查**(design §9) | `mingpai.others[].zhucards` |
|
||||
| **开底** | 桌面 → 所有玩家 | **具体牌面**(8 张底牌) | 70 分坐庄,**摸底之前** 3 秒(design §4) | `ancard3s` + `bottomcards` |
|
||||
|
||||
一句话记:**只有「余主公示」是统计类(给数字),「亮牌」「明牌」「开底」都给具体牌面**——区别在给谁的牌:亮牌给庄家自己的、明牌给他家的、开底给桌上那 8 张。
|
||||
|
||||
另有一组「底」字族专管发牌留桌的那 8 张:**摸底**(庄家摸起看,仅庄家)、**开底**(70 分时全场看 3 秒)、**查底牌**(对局中随时回看)、**扣底**(末轮翻开埋牌底牌计分)。
|
||||
(「亮主」不是术语,是前端选主界面的标题文案,与上表无关。)
|
||||
|
||||
|
||||
### 0.0c 算奖 = 冲关 + 傍王
|
||||
|
||||
| 术语 | 范围 | 相关字段 |
|
||||
| --- | --- | --- |
|
||||
| **冲关** | design §8.1,**只计庄家**。旧称"常规算奖",现统一叫冲关 | `chongguan`(奖数)、`cards`(冲关牌型) |
|
||||
| **傍王** | design §8.3,可选规则,勾选后**庄闲都算**,每张王 1 奖 | `wang`(王数)、`bangwang`(开关) |
|
||||
| **算奖** | **上位概念** = 冲关 + 傍王 | `naward`(总奖数 N)、`grade_aw`(算奖得分) |
|
||||
|
||||
说「**冲关**」指庄家那一份,说「**算奖**」指两者合计。界面上小局结算显示「冲关分」、按钮叫「冲关牌型」。
|
||||
|
||||
> ⚠️ `grade_aw` 是按合计后的 `N` 算出来的,**冲关与傍王的贡献事后无法拆分**。大局结算若要分别显示「冲关分」「傍王分」,须服务端在算钱时就分开代入公式——见前端清单 §7.5 **S-6**。
|
||||
|
||||
### 0.1 成败标志 `data.success`
|
||||
|
||||
**每一个「服务器 → 客户端」的包,`data` 都必带 `success`**(布尔):
|
||||
|
||||
- `success: true` —— 操作成功 / 正常推送。下文各包的字段表描述的都是这种情况,表里不再重复列 `success` 这一行。
|
||||
- `success: false` —— 操作失败,见 0.2。
|
||||
|
||||
前端一律 `if (!data.success)` 判成败,**不看 `status` / `code`,也不写 `status` 兼容兜底**。
|
||||
|
||||
### 0.2 失败回包
|
||||
|
||||
服务端受理请求时,任一校验不通过都会**回一个失败包**(此前是静默丢弃、前端只能干等倒计时):
|
||||
|
||||
- **rpc 与请求包同名**(如 `chupai` 失败回 `chupai`,而不是 `chupai1/2/3`);
|
||||
- **只回发给请求者**(`conmode`/`fromid` 取自请求包),其他两家收不到;
|
||||
- `data` 只含 `success: false` 与 `errcode`,无业务字段。
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| success | 布尔 | 恒为 `false` |
|
||||
| errcode | 整数 | 失败原因,见下表 |
|
||||
|
||||
`errcode` 取值(服务端 `youle_erqiwang.ERR`,`mod.js`):
|
||||
|
||||
| 值 | 名称 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| 1 | PLAYER | 玩家/房间/座位校验不通过(平台 `check_player` 返回 null) |
|
||||
| 2 | NODESK | 牌桌或牌局不存在 |
|
||||
| 3 | STEP | 当前阶段不允许该操作(如非出牌阶段发 `chupai`) |
|
||||
| 4 | SEAT | 位置不符,或还没轮到该玩家操作 |
|
||||
| 5 | PARAM | 参数非法:类型/范围/张数不对、牌不在手上、牌id 重复 |
|
||||
| 6 | RULE | 规则不允许:叫分未更低、出牌不合法、投降条件不满足、房间模式禁止等 |
|
||||
|
||||
> **牌id 列表的入参约束**(`cards` 字段,`maipai`/`chupai`):必须是**非空数组**,元素必须是 `0 ~ 107` 的**整数**牌id,且**互不重复**。字符串形式的数字(如 `"5"`)、小数、越界值、重复值一律按 `PARAM` 拒绝,服务端不做类型兜底转换。
|
||||
|
||||
> **例外:`tishi` 成功时不回执**。该包是闲家给对家的主观提示、不含任何对局状态,服务端只转发给对家(design §11),发送者收不到成功包;只有失败才回给发送者。
|
||||
|
||||
### 0.3 `countdown` 只是展示用的秒数
|
||||
|
||||
多个包带 `countdown`(叫分 / 选主 / 埋牌 / 出牌倒计时,取自 `class.desk.js` 的四个常量)。它**只供客户端显示提醒,服务端不据此做任何事**:
|
||||
|
||||
- 服务端**没有**对应的定时器,倒计时归零后**不会**自动叫分 / 选主 / 埋牌 / 出牌,也不判负、不跳过该玩家;
|
||||
- 轮到谁而谁不操作,牌局就停在该阶段一直等,`step` 与 `playproc` 都不变;
|
||||
- 这是 design §11 确认过的规则(无超时托管),不是未实现的功能。牌局因此停住时,由玩家走平台的**房间解散**流程收场(平台回调子游戏 `get_disbandRoom` → 解散结算,见 §14)。
|
||||
|
||||
因此**客户端不要在倒计时归零时做任何乐观的界面推进**(不要自行跳阶段、不要清控制权),一切仍以收到服务端推送为准。
|
||||
|
||||
### 0.4 断线重连包不在此约定内
|
||||
|
||||
`deskinfo` 由平台组装在 `pack.data.deskinfo` 下(见文末「断线重连」),`data.success` 由**平台**填写,子游戏不注入。
|
||||
|
||||
### 0.5 `roomtype` 房间选项位串
|
||||
|
||||
`roomtype` 是建房时由客户端生成、经平台建房流程存入 `o_room.roomtype` 的 **11 位字符串**。前 5 位依次为:**局数、扣卡方式、查牌模式、傍王、爬坡**;后 6 位预留。下表的位索引从 0 开始,与 `charAt` 一致,“前 5 位”即索引 0~4。
|
||||
|
||||
| 位索引 | 含义 | '0' | '1' | 创建页控件 | design |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 0 | 局数 | 6 局(默认) | 12 局 | 单选必选 | §10.1 |
|
||||
| 1 | 扣卡方式 | 房主扣卡(默认) | AA 每人扣卡 | 单选必选 | §10.1 |
|
||||
| 2 | 查牌模式 | 可查牌(默认) | 不查牌 | 单选必选 | §9 / §10.2 |
|
||||
| 3 | 傍王 | 关(默认) | 开 | 独立可选 | §8.3 / §10.3 |
|
||||
| 4 | 爬坡 | 常规算子(默认) | 爬坡 | 独立可选 | §7.3 / §10.3 |
|
||||
| 5~10 | 预留 | 尚未定义 | 尚未定义 | 无控件 | §10.4 |
|
||||
|
||||
前三组各自必须且只能选中一个选项;傍王与爬坡互不排斥,允许均不选、任选其一或同时选择。可选开关未勾选也必须编码为对应位的 '0',不能省略该位或缩短字符串。
|
||||
|
||||
默认选项的**前 5 位**为 `"00000"`,表示 6 局 / 房主扣卡 / 可查牌 / 无傍王 / 常规算子。例:前 5 位 `"10010"` 表示 12 局 / 房主扣卡 / 可查牌 / 傍王开 / 常规算子;`"00100"` 表示 6 局 / 房主扣卡 / 不查牌 / 无傍王 / 常规算子。这些仅是有效选项部分,**不是可直接发送的完整 roomtype**。
|
||||
|
||||
**待确认:后 6 位预留位的固定填充值。** 目前已确认总长度与预留位置,不能把未确认的全零填充写成已生效协议,也不能将预留位解释为额外规则。
|
||||
|
||||
本节为二七王位编码的唯一文档定义;玩法侧解析入口为 `class.config.js` 的 `parse()`,其他消费方使用解析结果。`multiple` / `bangwang` / `climb` / `baozhu` / `seatlist` / `liangpai` / `pushlist` 按相应选项取值或门控。
|
||||
|
||||
此次按规则设计者更正了长度和位序;尚未核验服务端实现是否已采用该定义。旧文档关于缺失、非字符串或过短值自动按 '0' 处理的描述,不构成前端补齐或静默容错的授权。前端应在编码入口生成完整合法参数,错误显式暴露,不改变服务器协议。
|
||||
|
||||
对应的房卡与局数(`export.get_asetcount` / `get_needroomcard` / `get_needroomcard_joinroom`,design §10.1):
|
||||
|
||||
| 扣卡方式(位1) | 局数(位0) | 房主开房扣 | 加入者扣 |
|
||||
| --- | --- | --- | --- |
|
||||
| 房主扣卡 `'0'` | 6 局 | 2 张 | 0 |
|
||||
| 房主扣卡 `'0'` | 12 局 | 4 张 | 0 |
|
||||
| AA 每人 `'1'` | 6 局 | 1 张 | 1 张 |
|
||||
| AA 每人 `'1'` | 12 局 | 2 张 | 2 张 |
|
||||
|
||||
### 0.6 「一手出牌」的顺序口径
|
||||
|
||||
同一份业务数据「某一手打出的牌」会出现在**三个下发位置**:
|
||||
|
||||
| 位置 | 字段 |
|
||||
|---|---|
|
||||
| 出牌推送 | `chupai1` / `chupai2` / `chupai3` 的 `data.cards` |
|
||||
| 本轮进行态 | `playproc.cards[座位]`(`chupai*` 与重连包 `PushCards` 都带)|
|
||||
| 重连出牌历史 | `PushCards.pushlist[轮次-1][座位]` |
|
||||
|
||||
**这三处的数组内容逐元素完全相等**——顺序统一为「按**本局主牌花色**从大到小」的**权威顺序**。
|
||||
服务端只在落牌那一刻归一化一次(`class.paiju.js` 的 `order_playcards`),随后:
|
||||
出牌推送读它、`playproc.cards` 存它、出牌历史 `playhistory` 归档它,下游一律**只读、不重排**。
|
||||
|
||||
因此前端**「增量回放」与「重连重建」得到的出牌历史必定一致**,两条路径可以复用同一份解析。
|
||||
|
||||
> ⚠️ **请求包里的 `cards` 顺序无意义**:那是玩家的点击顺序,同一手牌每次都可能不同。
|
||||
> 服务端不采信、不回显、也不据它判定牌型(server dev-guide 04 §8「前端不是数据源」)。
|
||||
> 一手单张时看不出差别;**一手多张(甩牌 / 对子 / 拖拉机)时才会显形**,尤其是同编码的一对牌
|
||||
> ——排序本身区分不了它们,只有「出牌当场归档」这一个写入处才能保证两条路径给出同一个数组。
|
||||
|
||||
---
|
||||
|
||||
## 1. 发牌(fapai)
|
||||
|
||||
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:fapai
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| asetidx | 整数 | 当前局数 |
|
||||
| asetcount | 整数 | 总局数 |
|
||||
| cards | 数组 | 自己得到的牌id列表 |
|
||||
| step | 整数 | **本局阶段**(发完牌恒为 `1` 叫分;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
|
||||
| seat | 整数 | **控制权**:当前等待叫分者的位置。与重连包 `CallRun.seat` 同源(`get_callgrade_seat()`)|
|
||||
| countdown | 整数 | 叫分倒计时 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 叫分或不叫(jiaofen,客户端→服务器)
|
||||
|
||||
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:jiaofen
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| agentid | 字符 | 代理id |
|
||||
| playerid | 整数 | 玩家id |
|
||||
| gameid | 字符 | 游戏id |
|
||||
| roomcode | 整数 | 房间号 |
|
||||
| seat | 整数 | 叫分者的位置序号 |
|
||||
| call | 整数 | 分数,0表示不叫 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 叫分或不叫(jiaofen,服务器→客户端)
|
||||
|
||||
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:jiaofen
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| seat | 整数 | 叫分者的位置序号 |
|
||||
| call | 整数 | 分数,0表示不叫 |
|
||||
| currcall | 整数 | 当前叫到的分数 |
|
||||
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位4):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
|
||||
| step | 整数 | **本局阶段**(叫分尚未结束,恒为 `1`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
|
||||
| nextseat | 整数 | **控制权**:下一个叫分者的位置序号,即本包之后「轮到谁」。与重连包 `CallRun.seat` 同源(`get_callgrade_seat()`)|
|
||||
| countdown | 整数 | 叫分倒计时 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 上庄(shangzhuang)
|
||||
|
||||
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:shangzhuang
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| seat | 整数 | 叫分者的位置序号 |
|
||||
| call | 整数 | 分数,0表示不叫 |
|
||||
| banker | 整数 | 庄家的位置序号 |
|
||||
| grade | 整数 | 庄家的叫分 |
|
||||
| step | 整数 | **本局阶段**(叫分结束、进入选主/投降,恒为 `2`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
|
||||
| nextseat | 整数 | **控制权**:本包之后「轮到谁」——恒为 `banker`,因为选主与投降都只由庄家做(服务端 `mod.xuanzhu`/`mod.touxiang` 的座位校验同样以 `banker` 为准)。<br>⚠️ **别和本包的 `seat` 混用**:`seat` 是**最后一个叫分者**,不是控制权。与重连包 `ChooseMain.seat` 同源同值 |
|
||||
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位4):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
|
||||
| bottomcards | 数组 | 8 张**底牌**(发牌时没发给玩家、扣在桌面的 8 张)。**庄家恒有**;**闲家仅当 70 分坐庄时才有**(供 3 秒亮牌用),非 70 分时闲家没有该属性(底牌只有庄家可见,design §4)。<br>**顺序在发牌结束时即冻结**(服务端 `paiju.bottomcards`,按「还没有主牌」的口径排一次):底牌是在**选主之前**翻给庄家看的,那时主牌花色尚不存在,故不按主牌花色排。本包与重连包 `ChooseMain.bottomcards` / `BuryCards.bottomcards` 三处**同序**,客户端存一次即可全程复用(含「查底牌」回看)|
|
||||
| ancard3s | 整数 | **开底**标志。仅 70 分坐庄时出现且为 `1`:表示庄家**摸底**之前,需将 `bottomcards` 这 8 张**底牌**向所有玩家翻开 3 秒(design §4/§7.1);非 70 分无此属性 |
|
||||
| cards | 数组 | 拿了底牌后手上的牌(36 张,含底牌),庄家才有此属性,闲家没有该属性 |
|
||||
| countdown | 整数 | 选主倒计时 |
|
||||
| touxiang | 整数 | 是否允许投降 0:不允许 1:允许(仅 70 分坐庄为 1)。投降与选主互斥、同为选主阶段(step2)的决策,见 touxiang 包与 design §4 |
|
||||
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):按此刻累计捡分实时算出的判定倍率,**带符号**,与结算包 `aset.upgrade` 同口径——`3`/`2`/`1` = 庄家 大光/小光/过庄,`-N` = 闲家升 N 级,`0` = 叫分未定。上庄时捡分恒为 0,故必为 `3`。三家同值(捡分本就公开)。**不含扣底**——扣底要到末轮才产生。<br>顶部「抓分」角标只关心倍数大小,取 `Math.abs` 即可;**符号供客户端的判定动画分辨该播哪个**(不能取绝对值,否则 3 大光与 -3 升3级会撞在一起)|
|
||||
|
||||
---
|
||||
|
||||
## 5. 投降(touxiang,客户端→服务器)
|
||||
|
||||
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:touxiang
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| agentid | 字符 | 代理id |
|
||||
| playerid | 整数 | 玩家id |
|
||||
| gameid | 字符 | 游戏id |
|
||||
| roomcode | 整数 | 房间号 |
|
||||
| seat | 整数 | 庄家的位置序号。投降是**选主阶段(step 2)与选主互斥**的选择:`mod.touxiang` 仅在 `step==2`、`banker==seat`、`call==70` 时受理;点投降即直接结算,不选主、不埋牌、不出牌(design §4) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 选主(xuanzhu,客户端→服务器)
|
||||
|
||||
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:xuanzhu
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| agentid | 字符 | 代理id |
|
||||
| playerid | 整数 | 玩家id |
|
||||
| gameid | 字符 | 游戏id |
|
||||
| roomcode | 整数 | 房间号 |
|
||||
| seat | 整数 | 选主者的位置序号 |
|
||||
| flower | 整数 | 花色 1方块 2梅花 3红心 4黑桃 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 选主(xuanzhu,服务器→客户端)
|
||||
|
||||
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:xuanzhu
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| banker | 整数 | 庄家的位置序号 |
|
||||
| flower | 整数 | 花色 1方块 2梅花 3红心 4黑桃 |
|
||||
| step | 整数 | **本局阶段**(选主完成、进入埋牌,恒为 `3`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
|
||||
| nextseat | 整数 | **控制权**:本包之后「轮到谁」——恒为 `banker`,埋牌只由庄家做(服务端 `mod.maipai` 的座位校验以 `banker` 为准)。与重连包 `BuryCards.seat` 同源同值 |
|
||||
| countdown | 整数 | 埋牌倒计时 |
|
||||
| cards | 数组 | 选主后自己手上的牌id列表 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 埋牌(maipai,客户端→服务器)
|
||||
|
||||
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:maipai
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| agentid | 字符 | 代理id |
|
||||
| playerid | 整数 | 玩家id |
|
||||
| gameid | 字符 | 游戏id |
|
||||
| roomcode | 整数 | 房间号 |
|
||||
| seat | 整数 | 埋牌者的位置序号 |
|
||||
| cards | 数组 | 埋牌的id列表 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 埋牌(maipai,服务器→客户端)
|
||||
|
||||
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:maipai
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| cards | 数组 | 埋牌后手上的牌,去掉了埋牌,庄家才有此属性,闲家没有该属性 |
|
||||
| burycards | 数组 | **埋牌底牌**(庄家埋下的 8 张),庄家才有此属性,闲家没有该属性。注意与 `shangzhuang.bottomcards`(**底牌**,发牌留桌 8 张)是两批不同的牌,见 §0.0。<br>**取服务端权威快照 `get_burycard()`,不是客户端请求包里 `cards` 的原序**:按本局主牌花色从大到小排好,与重连包 `PushCards.burycards` **同源同序** |
|
||||
| seatlist | 数组 | 三家座位牌况,**仅可查牌模式下发**(不查牌无此属性,design §9),结构与门控同 `chupai1/2/3.seatlist` 与重连包 `PushCards.seatlist`。<br>埋牌完成时它是刚初始化的**空表**(每家 `[[0,0],[0,0],[0,0],[0,0],[-1,-1]]`)——下发它是为了让「埋牌完成 → 庄家首出」这段窗口内,增量路径与重连路径拿到同一张表,客户端无需为这段窗口特判 |
|
||||
| playproc | json | **本轮进行态**(design §5.1),**恒有、三家同值**,结构与门控同 [`chupai1.playproc`](#11-第一个玩家出牌chupai1)/重连包 `PushCards.playproc`(同一个快照函数 `get_playproc()`)。`do_burycard` 内部已 `new_playround` 就地初始化好 round-1 的进行态,此包带的正是这份初值:`round=1`、`start`/`currseat` 均为即将首出的庄家、`cards` 全空、`shuai_demand` 为 `null`。<br>**可见性**:全部字段由桌面公开信息推出(此刻尚无一张牌打出),三家整体下发、不逐座位裁剪 |
|
||||
| step | 整数 | **本局阶段**(埋牌完成、进入出牌,恒为 `5`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
|
||||
| seat | 整数 | **控制权**:出牌者的位置序号(即将首出的庄家)。服务端取自权威的 `playproc.currseat`,与重连包 `PushCards.playproc.currseat` 同源同值 |
|
||||
| countdown | 整数 | 出牌倒计时 |
|
||||
| liangpai | json | **亮牌**(design §8.2)。**只有闲家、且可查牌模式、且庄家达门槛时才有**;不达标或不查牌则无此属性。结构:`{ cards: [牌id...] }`——**庄家手中全部固定主牌的具体牌面**,按本局主牌序从大到小排好。<br>**固定主牌 = 双王 + 全部花色的 2 + 全部花色的 7**,**不含**主花色的普通牌 A/K/Q/J/10/9/8/6/5。<br>**门槛**(任一满足即下发):固定主牌总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6。**不限叫分**。<br>亮出的是**固定的一份**,不随被哪条门槛触发而增减;也**不给数量统计**——数量前端自己数 `cards.length` 即可。<br>口径是庄家**埋牌后的静态快照**(排除已埋的 8 张,但包含之后已打出的牌),全局固定不随出牌缩水,故重连包里取值一致 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 出牌(chupai,客户端→服务器)
|
||||
|
||||
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:chupai
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| agentid | 字符 | 代理id |
|
||||
| playerid | 整数 | 玩家id |
|
||||
| gameid | 字符 | 游戏id |
|
||||
| roomcode | 整数 | 房间号 |
|
||||
| seat | 整数 | 出牌者的位置序号 |
|
||||
| cards | 数组 | 出牌的id列表 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 第一个玩家出牌(chupai1)
|
||||
|
||||
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai1
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| seat | 整数 | 出牌者的位置序号 |
|
||||
| cards | 数组 | 实际打出的牌id列表;**甩错时**(见 `shuaicuo`)为被强制打出的那一张最小主牌单张。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序——请求包里的 `cards` 顺序服务端不采信、不回显(见 [§0.6](#06-一手出牌的顺序口径))|
|
||||
| shuaicuo | 整数 | 甩错标志,仅甩错时出现且为 `1`(design §5.4.5:甩牌未通过最大性判定,整套甩牌收回,本轮只强制打出最小一张、失去本轮甩牌资格);正常出牌无此属性 |
|
||||
| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` |
|
||||
| count | 整数 | 出牌数量(甩错时为 1) |
|
||||
| flower | 整数 | 出牌花色 |
|
||||
| cardtype | 整数 | 出牌牌型:`>100` 单张(101 一张、102 两张…)、`>200` 对子(201 一对、202 两对…)、`>300` 拖拉机(302 两连对、303 三连对…),见 `class.pai.js` 顶部注释。**注意:cardtype 只能表达单一牌型**,而甩牌是单张/对子/拖拉机自由混搭(design §5.4.3),会被牌型推导压平成 1xx 或 2xx(例如「主K对 + 主5」得 103、「两连对 + 一散对」得 203)。**甩牌的真实结构请读 `shuai`,不要据 cardtype 反推** |
|
||||
| shuai | json | 甩牌分量构成,**仅本次出牌是合法甩牌时才有**(非甩牌、以及甩错退化为单张时都没有该属性)。结构 `{ tractors: [连对数...], pairs: 独立对子数, singles: 单张数 }`,例如「主K对 + 主5」为 `{tractors:[],pairs:1,singles:1}`。与服务端跟牌时逐分量强制匹配用的 `shuai_demand` 同源(design §5.4.3/§5.4.4)|
|
||||
| nextseat | 整数 | 下一个出牌者的位置序号 |
|
||||
| countdown | 整数 | 下一个出牌者的出牌倒计时 |
|
||||
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)|
|
||||
| cardsinhand | 数组 | 出牌者出牌后手上剩下的牌id列表,只有出牌者才有此属性 |
|
||||
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与结算包 `aset.upgrade` 同口径(`3`/`2`/`1` 庄家大光/小光/过庄,`-N` 闲家升 N 级,`0` 叫分未定)。同源同算法,差别仅在此处**不含扣底**。三家同值,随本包下发、不另开推送。符号是「谁赢」的区分,客户端判定动画靠它分辨 |
|
||||
| playproc | json | <a id="playproc-def"></a>**本轮进行态**(design §5.1)。**`chupai1/2/3` 三个包恒有,三家同值**;结构与重连包 [`PushCards.playproc`](#断线重连deskinfo) **完全一致**(服务端同一个快照函数 `get_playproc()`,前端增量回放与重连重建复用同一份解析)。<br>字段:`round` 第几轮、`start` 本轮首出位置、`currseat` 当前该谁出、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮暂时最大者、`maxcard` 其牌编码、`cards` 本轮三家各自出的牌(**定长 3,下标 = 座位序号**,未出的位置为 `null`)、`shuai_demand` 首家甩牌的分量需求 `{tractors:[连对数...],pairs,singles}`(非甩牌为 `null`)。<br>⚠️ **`chupai3` 带的是【下一轮】的进行态**(`round+1`、`cards` 全空、`currseat == nextseat == maxseat`):本轮第三家一出完,服务端就地开了新一轮。这与「此刻断线重连拿到的 `PushCards.playproc`」完全相同——本轮那三手牌客户端已由 `chupai1/2/3` 各自的 `seat`+`cards` 收到,收牌动画后即清台。**唯一例外**:`chupai3` 打完最后一张牌时本包会转成 `jiesuan`(见 §14),那种情况下**不带** `playproc`。<br>**可见性**:全部字段都由桌面公开信息推出(`cards` 就是已摊在桌上的牌,`shuai_demand` 与 `chupai1.shuai` 等价且甩出的牌本身已公开),故三家整体下发、不逐座位裁剪 |
|
||||
| mustcard | 数组 | **下一个出牌者本轮跟牌的必出牌**(design §5.2),供其客户端自动选中。**只发给 `nextseat` 那一家**,其余两家无此属性——它是该玩家自己手牌的子集,整表下发会泄露他家手牌结构。以下情形不下发:`nextseat` 是本轮首家、首家为**甩牌**(甩牌跟牌走逐分量匹配,见 design §5.4.4)、或算出的必出牌为空 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 第二个玩家出牌(chupai2)
|
||||
|
||||
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai2
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| seat | 整数 | 出牌者的位置序号 |
|
||||
| cards | 数组 | 出的牌id列表。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序(见 [§0.6](#06-一手出牌的顺序口径))|
|
||||
| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` |
|
||||
| nextseat | 整数 | 下一个出牌者的位置序号 |
|
||||
| countdown | 整数 | 出牌倒计时 |
|
||||
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)|
|
||||
| cardsinhand | 数组 | 出牌后出牌者手上剩下的牌id列表,只有出牌者才有此属性 |
|
||||
| playproc | json | **本轮进行态**,恒有、三家同值,结构见 [§11 `playproc`](#11-第一个玩家出牌chupai1)。此时 `cards` 已含首家与本家两手牌,`currseat` 指向第三家 |
|
||||
| mustcard | 数组 | **下一个出牌者本轮跟牌的必出牌**(design §5.2),供其客户端自动选中。**只发给 `nextseat` 那一家**,其余两家无此属性——它是该玩家自己手牌的子集,整表下发会泄露他家手牌结构。以下情形不下发:`nextseat` 是本轮首家、首家为**甩牌**、或算出的必出牌为空 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 第三个玩家出牌(chupai3)
|
||||
|
||||
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai3
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| seat | 整数 | 出牌者的位置序号 |
|
||||
| cards | 数组 | 出的牌id列表。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序(见 [§0.6](#06-一手出牌的顺序口径))|
|
||||
| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` |
|
||||
| nextseat | 整数 | 下一个出牌者的位置序号 |
|
||||
| countdown | 整数 | 出牌倒计时 |
|
||||
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)|
|
||||
| cardsinhand | 数组 | 出牌后出牌者手上剩下的牌id列表,只有出牌者才有此属性 |
|
||||
| maxseat | 整数 | 本轮出牌谁最大,只有本轮最后一个玩家出牌后才有此属性 |
|
||||
| grade | 整数 | 本轮闲家得分,只有本轮最后一个玩家出牌后且闲家有得分才有此属性 |
|
||||
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,口径同 `aset.upgrade`。同源同算法,差别仅在此处**不含扣底**。三家同值,随本包下发、不另开推送 |
|
||||
| playproc | json | **本轮进行态**,三家同值,结构见 [§11 `playproc`](#11-第一个玩家出牌chupai1)。⚠️ 本包带的是**下一轮**的进行态(`round+1`、`cards` 全空、`currseat == nextseat == maxseat`),与此刻重连拿到的 `PushCards.playproc` 一致。**牌局在本包打完(转为 `jiesuan`)时不带此属性** |
|
||||
|
||||
---
|
||||
|
||||
## 13.5 明牌(mingpai,客户端→服务器)
|
||||
|
||||
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:mingpai
|
||||
|
||||
查看另外两家手中全部主牌的具体牌面(design §9)。服务端仅在**可查牌模式、出牌阶段(step5)、且已有【任一】玩家报无主**(`have_baofu()`)时受理——注意判定的是「场上有人报无主」而非「请求者本人报无主」,报无主那位与另外两位同样有权查看(design §9.3);不满足时按 0.2 回 `mingpai` 失败包(不查牌 / 未报无主 → `RULE`,非出牌阶段 → `STEP`)。
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| agentid | 字符 | 代理id |
|
||||
| playerid | 整数 | 玩家id |
|
||||
| gameid | 字符 | 游戏id |
|
||||
| roomcode | 整数 | 房间号 |
|
||||
| seat | 整数 | 请求者的位置序号 |
|
||||
|
||||
## 13.6 明牌(mingpai,服务器→客户端)
|
||||
|
||||
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:mingpai (只回发给请求者)
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| seat | 整数 | 请求者的位置序号 |
|
||||
| others | 数组 | 另外两家各自未出的全部主牌,元素 `{ seat: 位置序号, zhucards: [主牌id列表] }` |
|
||||
|
||||
> "再点一次取消查看"是客户端的显示开关,无需再请求服务端。
|
||||
|
||||
## 13.7 出牌提示(tishi,客户端→服务器)
|
||||
|
||||
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:tishi
|
||||
|
||||
闲家在出牌阶段向对家(另一闲家)发"踩/没分/有分"提示(design §11)。服务端仅在**出牌阶段(step5)、且发起者为闲家**(`seat != banker`)、**tip 合法(1/2/3)** 时受理;**不校验提示真实性**(玩家可主观发送);庄家无对家,不受理。不满足时按 0.2 回 `tishi` 失败包给发送者(庄家发 → `RULE`,非出牌阶段 → `STEP`,tip 非法或缺失 → `PARAM`)。**成功时不给发送者回执**,只转发给对家。
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| agentid | 字符 | 代理id |
|
||||
| playerid | 整数 | 玩家id |
|
||||
| gameid | 字符 | 游戏id |
|
||||
| roomcode | 整数 | 房间号 |
|
||||
| seat | 整数 | 发出提示的闲家位置序号 |
|
||||
| tip | 整数 | 提示类型:1=踩(我能大过庄家)、2=没分(我手上没分了)、3=有分(我手上有分) |
|
||||
|
||||
## 13.8 出牌提示(tishi,服务器→客户端)
|
||||
|
||||
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:tishi (**只转发给对家**,即另一闲家 `3 - banker - seat`)
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| seat | 整数 | 发出提示的闲家位置序号 |
|
||||
| tip | 整数 | 提示类型:1=踩、2=没分、3=有分 |
|
||||
|
||||
---
|
||||
|
||||
## 14. 结算(jiesuan)
|
||||
|
||||
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:jiesuan
|
||||
|
||||
结算包由以下几个子结构拼装而成。三种结算来源的组成不同:
|
||||
|
||||
- **正常出牌结算**(`mod.chupai` 中最后一张牌出完,`get_paiju_account(0, ...)`):含 `chupai` + `bottom` + `aset`(末局再加 `account`)。
|
||||
- **投降结算**(`mod.touxiang`,`get_paiju_account(1, ...)`):只含 `aset`(末局再加 `account`),无 `chupai`、无 `bottom`。
|
||||
- **解散结算**(`export.get_disbandRoom`,`get_paiju_account(2, ...)`):只含 `aset` + `account`,无 `chupai`、无 `bottom`。**投递方式与上面两种不同,见 §14.1。**
|
||||
|
||||
上面这张表头(`route: erqiwang / rpc: jiesuan`)**只适用于前两种**——正常出牌结算与投降结算,由子游戏自己 `sendpack_toother` 广播,客户端在 `rpc == "jiesuan"` 的分支里按 `data.aset` 取值。
|
||||
|
||||
### 14.1 解散结算的投递方式(与前两种不同,客户端需单独处理)
|
||||
|
||||
解散**不是**由子游戏发包,而是平台在解散流程里回调 `youle_erqiwang.export.get_disbandRoom(o_room)`,把**返回值整个对象**塞进**房间路由**的解散包里下发(平台 `server_room/rpc.js`、`server_room/class.room.js`)。因此客户端收到的是:
|
||||
|
||||
```json
|
||||
{
|
||||
"app": "youle", "route": "room", "rpc": "free_room",
|
||||
"data": {
|
||||
"seats": [],
|
||||
"deskfree": {
|
||||
"rpc": "jiesuan",
|
||||
"data": { "success": true, "aset": {}, "account": [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
要点(照 §14 的表去 `data.aset` 取值会取空):
|
||||
|
||||
- **rpc 是平台的 `free_room`(route 也是 `room`),不是 `erqiwang/jiesuan`**;子游戏返回的 `rpc: "jiesuan"` 只是嵌在 `deskfree` 里的一个普通字段。
|
||||
- **多一层 `data` 包装**:真实取值路径是 `data.deskfree.data.aset` 与 `data.deskfree.data.account`。
|
||||
- 本包的成败标志有两层:外层 `data.success` 由**平台**填写(解散流程本身成功与否),子游戏结算自带的 `success` 在 `data.deskfree.data.success`。§0.1「每个包 `data` 必带 `success`」说的是子游戏自己发的包;本包外层归平台。
|
||||
- `aset` / `account` 的字段结构与下面 §14 的表完全一致(`aset.multiple = 0`、`upgrade = 0`、各家 `grade = 0`,即解散局不结算子数与算奖;`account` 恒有,见 design §12.2「按当前累计分结算」)。
|
||||
- 若解散发生在**开战后、首局牌发出前**的窗口内(本游戏首局由 `makewar` 延迟 1 秒创建),`get_disbandRoom` 返回 `null`,平台走「不带 `deskfree`」分支——客户端需容忍 `data.deskfree` 缺失。
|
||||
|
||||
**顶层字段(三种结算来源恒有)**
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| success | 布尔 | 恒 `true`(§0.1)|
|
||||
| step | 整数 | **本局阶段**,结算包恒为 `6`。三种结算来源(正常/投降/解散)统一由 `get_paiju_account` 给出——解散在此之前 `step` 可能还停在 1/2/3/5,由它落定为 6。客户端**不得按 rpc 名硬编码**,一律读此字段 |
|
||||
|
||||
**chupai(出牌包,仅正常出牌结算存在)**
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| seat | 整数 | 出牌者的位置序号,投降和解散无此属性 |
|
||||
| cards | 数组 | 出的牌id列表,投降和解散无此属性 |
|
||||
| maxseat | 整数 | 本轮出牌谁最大,投降和解散无此属性 |
|
||||
| grade | 整数 | 本轮闲家得分,投降和解散无此属性 |
|
||||
| gradecards | 数组 | 本轮闲家得分分牌列表,投降和解散无此属性。**注意:当前源码中赋值语句被注释(`class.paiju.js`/`mod.js` 中相关行均被注释掉),该字段实际永远不会出现在下发的包里,属于失效字段** |
|
||||
|
||||
**bottom(抠底包,仅正常出牌结算存在;`cards` 恒有,其余仅闲家抠底时有)**
|
||||
|
||||
> 本分组的 `cards` 是**埋牌底牌**(庄家埋下的 8 张),不是发牌留桌的底牌。
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| cards | 数组 | **埋牌底牌**(庄家埋下的 8 张),投降和解散无此属性 |
|
||||
| multiple | 整数 | 闲家抠底倍数,闲家没抠底无此属性,投降和解散无此属性 |
|
||||
| grade1 | 整数 | 埋牌底牌的分数,闲家没抠底无此属性,投降和解散无此属性 |
|
||||
| grade2 | 整数 | 闲家抠底得分,闲家没抠底无此属性,投降和解散无此属性 |
|
||||
|
||||
**aset(单局结算包)**
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| banker | 整数 | 庄家的位置序号,-1:无庄 |
|
||||
| call | 整数 | 庄家叫分,-1:无叫分 |
|
||||
| multiple | 整数 | 基础子数 `get_base_bycall(call, climb)`(design §7.1/§7.3.1):常规算子 65→2、60→3、55→4、50 及以下→6、70打牌→2;投降固定 1、解散 0;无叫分 0 |
|
||||
| flower | 整数 | 主牌花色,-1:无主牌花色 |
|
||||
| grade | 整数 | 闲家捡分(含抠底) |
|
||||
| upgrade | 整数 | 判定倍率(带符号,design §7.2.0):3大光 / 2小光 / 1过庄 / -N升N级(倒庄)/ -99投降 / 0解散或无判定 |
|
||||
| bangwang | 整数 | 本局是否启用傍王规则 0否 1是(roomtype 位3) |
|
||||
| climb | 整数 | 本局是否启用爬坡规则 0否 1是(roomtype 位4) |
|
||||
| seatlist | 数组 | 玩家列表,元素结构见下 |
|
||||
|
||||
`seatlist` 数组元素结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"cards": [], // 冲关牌型:参与冲关的牌(王 + 冲关组合牌)列表,供「冲关牌型」界面展示
|
||||
"chongguan": 0, // 冲关奖数(design §8.1,旧称"常规算奖";只有庄家才计入自己的 N)
|
||||
"wang": 0, // 手牌中的王数(傍王按此计奖)
|
||||
"naward": 0, // 该家总奖数 N =(庄家?冲关奖数:0)+(傍王?王数:0)
|
||||
"grade_aw": 0, // 算奖得分 = X×(2Ni−Nj−Nk),X 为每对子子数。= grade_cg + grade_bw
|
||||
"grade_cg": 0, // 其中的【冲关】分量(design §8.1,不含傍王)
|
||||
"grade_bw": 0, // 其中的【傍王】分量(design §8.3;未勾傍王时恒 0)
|
||||
"grade_jf": 0, // 捡分子数得分
|
||||
"grade": 0, // 本局总分 = grade_aw + grade_jf
|
||||
"score": 0 // 累计得分
|
||||
}
|
||||
```
|
||||
|
||||
> 结算数值模型(design §7~§8):每「庄–闲」对子的基础金额 `X = multiple × |upgrade|`(投降 X=1、解散 X=0)。捡分子数:庄赢时两闲家各付庄家 X、庄家收 2X;闲赢(升级)时庄家各付两闲家 X。算奖:持有 N 奖的玩家从另外两人各多收 `X×N`,三家两两独立叠加(含闲–闲),即 `grade_aw = X×(2Ni−Nj−Nk)`。
|
||||
>
|
||||
> **算奖的两个分量**(供大局结算分项展示):`N = N_冲关 + N_傍王`(冲关只计庄家、傍王勾选后庄闲都算)。
|
||||
> 服务端把两个分量**分别代入同一公式**各算一次,得到 `grade_cg` 与 `grade_bw`。
|
||||
> 公式对 `N` 是线性的,因此恒有 **`grade_cg + grade_bw == grade_aw`**,且两个分量各自零和。
|
||||
> 之所以必须在算钱时就拆,是因为两个 `N` 一旦相加就再也分不开——事后无法从 `grade_aw` 反推各自占比。
|
||||
|
||||
**account(大局结算包)**
|
||||
|
||||
> 仅当打到最后一局(`o_paiju.idx >= o_room.asetcount`)或房间中途解散(解散结算)时,`jiesuan` 包才会带上此分组;普通的中间局结算包没有 `account` 字段(`class.paiju.js` `get_paiju_account`)。
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| account | 数组 | 玩家列表(长度 3,下标 = 座位序号),元素结构见下 |
|
||||
|
||||
```json
|
||||
[
|
||||
{ "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 },
|
||||
{ "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 },
|
||||
{ "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 }
|
||||
]
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| score | 整数 | 累积总分(全部小局 `aset.seatlist[i].grade` 之和) |
|
||||
| grades | 数组 | 每局总分列表,按局序 |
|
||||
| grade_jf_total | 整数 | **基础分**累计:各局 `grade_jf`(捡分子数得分)之和 |
|
||||
| grade_cg_total | 整数 | **冲关分**累计:各局 `grade_cg` 之和(design §8.1,**不含**傍王) |
|
||||
| grade_bw_total | 整数 | **傍王分**累计:各局 `grade_bw` 之和(design §8.3;未勾傍王时恒 0) |
|
||||
|
||||
> 恒等式:`grade_jf_total + grade_cg_total + grade_bw_total == score`,供大局结算面板分项展示(基础分 / 冲关分 / 傍王分 / 总分)。
|
||||
>
|
||||
> ⚠️ **结构变更(2026-08-26)**:原为 `[[累积得分, [每局得分...]], ...]` 的二元数组,现改为**对象数组**并增加三项分解。数组下标语义不清,且前端尚未开工,此时改代价最小。
|
||||
|
||||
---
|
||||
|
||||
## 15. 准备(zhunbei,客户端→服务器)
|
||||
|
||||
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:zhunbei
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| agentid | 字符 | 代理id |
|
||||
| playerid | 整数 | 玩家id |
|
||||
| gameid | 字符 | 游戏id |
|
||||
| roomcode | 整数 | 房间号 |
|
||||
| seat | 整数 | 位置序号 |
|
||||
|
||||
---
|
||||
|
||||
## 16. 准备(zhunbei,服务器→客户端)
|
||||
|
||||
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:zhunbei
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| seat | 整数 | 位置序号 |
|
||||
|
||||
---
|
||||
|
||||
## 断线重连(deskinfo)
|
||||
|
||||
> 由平台在玩家进入房间/断线重连时回调 `youle_erqiwang.export.get_deskinfo(o_room, seat)` 生成并下发(服务器→客户端);下发的 app/route/rpc 由平台的进房/重连流程决定,不在本子游戏代码内固定。平台把它挂在 `pack.data.deskinfo` 下,**`data.success` 由平台填写、子游戏不注入**(见 0.4)。下列字段即该函数返回的 `deskinfo` 对象结构,按当前 `paiju.step` 只带对应阶段的分组。
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| count | 整数 | 总局数 |
|
||||
| idx | 整数 | 当前局数 |
|
||||
| PlayerInfo | 数组 | 三个玩家目前的总积分 `[0,0,0]` |
|
||||
| step | 整数 | 牌桌状态:1发完牌叫分 2选主/投降 3埋牌 5出牌 6结算(无独立投降阶段——投降在 step2 与选主互斥;埋牌后直接进入 step5 出牌) |
|
||||
| MyCards | 数组 | 自己手上的牌 |
|
||||
|
||||
**CallRun(不在叫分阶段无此属性)**
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| seat | 整数 | 当前叫分位置 |
|
||||
| countdown | 整数 | 叫分倒计时 |
|
||||
| nowcall | 整数 | 当前叫分 |
|
||||
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位4):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
|
||||
| call | 数组 | 三家叫分 `[null,0,65]`,null还未叫分,0不叫,>0叫了多少分 |
|
||||
|
||||
**ChooseMain(step 2 选主/投降阶段,不在此阶段无此属性)**
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| seat | 整数 | **控制权**:此刻轮到谁操作——恒为 `banker`(选主与投降都只由庄家做)。字段名与 `CallRun.seat` 一致:**各阶段分组里的 `seat` 统一表示「轮到谁」**。与增量推送 `shangzhuang.nextseat` 同源同值;客户端据此设控制权,**不要自己用 `banker` 反推**(那是把「选主=庄家」这条规则搬到前端)|
|
||||
| banker | 整数 | 庄家 |
|
||||
| call | 整数 | 叫分 |
|
||||
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位4):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
|
||||
| countdown | 整数 | 选主倒计时 |
|
||||
| bottomcards | 数组 | 8 张**底牌**(发牌留桌的 8 张),**只有庄家有此属性**(底牌仅庄家可见;70 分的 3 秒亮牌是上庄时的一次性事件,重连不重放)。顺序取发牌时冻结的快照,与 `shangzhuang.bottomcards`、`BuryCards.bottomcards` **完全同序**(见 §4)|
|
||||
| touxiang | 整数 | 是否允许投降 0:不允许 1:允许(仅 70 分坐庄为 1)。投降与选主互斥、同一决策点,见 touxiang 包与 design §4 |
|
||||
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `shangzhuang.curmultiple` 同源同值。选主阶段一张牌都还没出、捡分恒为 0,故**必为 `3`**(大光)。三家同值。有此字段,重连后顶部「抓分」角标才不会掉回 0 |
|
||||
|
||||
> 选主阶段前端需在每个花色按钮上显示"该花色在庄家手中的对子数"(design §4/§11)——庄家的完整手牌由 `MyCards` 提供(庄家为 36 张),对子数由前端据此计算,服务端不额外下发。
|
||||
|
||||
**BuryCards(step 3 埋牌阶段,不在此阶段无此属性)**
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| seat | 整数 | **控制权**:此刻轮到谁操作——恒为 `banker`(埋牌只由庄家做)。与增量推送 `xuanzhu.nextseat` 同源同值 |
|
||||
| banker | 整数 | 庄家 |
|
||||
| call | 整数 | 叫分 |
|
||||
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位4):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
|
||||
| flower | 整数 | 主牌花色 |
|
||||
| countdown | 整数 | 埋牌倒计时 |
|
||||
| bottomcards | 数组 | 8 张**底牌**(发牌留桌的 8 张),**只有庄家有此属性**(底牌仅庄家可见;70 分的 3 秒亮牌是上庄时的一次性事件,重连不重放)。顺序取发牌时冻结的快照,与 `shangzhuang.bottomcards`、`ChooseMain.bottomcards` **完全同序**——**不**按本局主牌花色重排(见 §4)|
|
||||
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `shangzhuang.curmultiple` 同源同值。埋牌阶段同样一张牌未出、捡分恒为 0,故**必为 `3`**(大光)。三家同值 |
|
||||
|
||||
> 埋牌阶段无投降(投降是 step2 与选主互斥的选择,选主后即不可再投降)。
|
||||
|
||||
**PushCards(step 5 出牌阶段,不在此阶段无此属性)**
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| banker | 整数 | 庄家 |
|
||||
| call | 整数 | 叫分 |
|
||||
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位4):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
|
||||
| flower | 整数 | 主牌花色 |
|
||||
| countdown | 整数 | 出牌倒计时 |
|
||||
| burycards | 数组 | **埋牌底牌**(庄家埋下的 8 张),只有庄家有此属性。注意与 `ChooseMain`/`BuryCards.bottomcards`(**底牌**)是两批不同的牌,见 §0.0。与 `maipai.burycards` **同源同序**(同一个 `get_burycard()`,按本局主牌花色从大到小)|
|
||||
| grade | 整数 | 当前的捡分分数 |
|
||||
| gradecards | 数组 | 当前的捡分分牌,只有闲家有此属性 |
|
||||
| playproc | json | **本轮进行态**,与 `chupai1/2/3.playproc` **同源同结构**(服务端同一个 `get_playproc()` 快照函数,见 [§11 `playproc`](#11-第一个玩家出牌chupai1)):`round` 第几轮、`start` 本轮首出位置、`currseat` 当前出牌位置、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮最大者位置、`maxcard` 最大牌编码、`cards` 本轮三家各自出的牌(**定长 3**,下标=位置序号,未出为 `null`)、`shuai_demand` 首家甩牌的分量需求 `{tractors:[连对数...],pairs,singles}`(非甩牌为 null,供跟牌逐分量强制匹配,design §5.4.4)。**两种查牌模式下恒有此属性**——`cards` 是当前这一轮桌面上的牌,不属于「查牌」,屏蔽了后出的人就无从跟牌(design §9 末尾)|
|
||||
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是,**恒有此属性**;与 `chupai1/2/3` 的 `baozhu` **同源同值同门控**(同一个 `have_baofu()` + 同一个查牌位)。它是**余主公示**与“明牌”按钮的开关,重连必须一并恢复,否则重连后按钮凭空消失;**不查牌模式恒为 0**(design §9)|
|
||||
| seatlist | 数组 | 三家座位牌况,与上文 `maipai` / `chupai1/2/3` 包的 `seatlist` 同名同结构(每个玩家一个 5 元素数组:4 个花色的 `[无该花色,无对]` + 报副 `[剩余主牌数,剩余主对数]`)。**仅可查牌模式下有此属性**(design §9)|
|
||||
| liangpai | json | **亮牌**,结构同 maipai 包的 `liangpai`(`{ cards: [牌id...] }`);**仅可查牌模式、且请求者为闲家、且庄家达标时有**(供闲家重连后仍能看到,design §8.2)|
|
||||
| pushlist | 数组 | 出牌历史 `[[[], [], []], [[], [], []], ...]`,外层下标=轮次(从第 1 轮起,含进行中的当前轮),内层**恒为 3 个数组**、按位置序号存该轮各家出的牌id。**每一手与当时那个 `chupai1/2/3` 包的 `cards` 逐元素完全相等**(同一份数据、同一个顺序口径,见 [§0.6](#06-一手出牌的顺序口径)),因此前端“增量回放”与“重连重建”得到的出牌历史必定一致。**仅可查牌模式下有此属性**——往轮打出、已被收走的牌属于「查牌」范畴,不查牌模式一律不下发(design §9)。注意当前这一轮桌面上的牌由 `playproc.cards` 恢复,**两种模式下都有**,否则后出的人无从跟牌 |
|
||||
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `chupai1/2/3` 同源同值,供重连后顶部「抓分」角标与判定动画状态立即正确 |
|
||||
| mustcard | 数组 | **本轮跟牌的必出牌**(design §5.2)。**仅当 `playproc.currseat == 请求者座位` 时才有**,且只算请求者自己的手牌;未轮到本家、本轮首家、甩牌局面、必出牌为空时均无此属性 |
|
||||
|
||||
**Balance(不在结算阶段无此属性)**
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| readystate | 数组 | 所有玩家的准备状态 |
|
||||
| aset | json | 单局结算包,同上面结算包中的 `aset`,只有当自己的准备状态为0时才有此属性 |
|
||||
| bottom | json | **抠底包**,结构同 §14 的 `bottom`,与该局 `jiesuan` 推送的 `data.bottom` **同源同值**(服务端在 `get_paiju_account` 末尾冻结的同一份快照)。**仅正常出牌结算才有**(投降/解散没有抠底这一步);同样受「自己的准备状态为 0」门控。<br>**为什么必须有**:结算面板还开着时断线重连/硬刷新,只恢复 `aset` 会让抠底明细整块空白——同一份数据两条路径给的不一样(server 红线「发全下发面」)。<br>**可见性**:埋牌底牌在本局结算时已随 `jiesuan` 广播给三家(design §11 结束亮底),此处不构成额外泄露 |
|
||||
| account | json | **大局结算包**,结构同 §14 的 `account`,与该局 `jiesuan` 推送的 `data.account` **同源同值**。**仅末局或中途解散才有**,与 `jiesuan` 的取舍完全一致(有就带、没有就不带)|
|
||||
|
||||
---
|
||||
|
||||
## 战绩(大局列表)gameinfo1
|
||||
|
||||
> 非客户端收发包:大局结束/解散时由 `class.desk.js` `get_desk_account` 组装,经 `import.save_grade(o_room, o_gameinfo1, o_gameinfo2, 1)` 传给平台战绩服务持久化(服务器→平台)。
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| roomcode | 整数 | 房号 |
|
||||
| asetcount | 整数 | 实际局数 |
|
||||
| createtime | 字符 | 开房时间 |
|
||||
| makewartime | 字符 | 开战时间 |
|
||||
| players | 数组 | 玩家列表,元素结构见下 |
|
||||
|
||||
`players` 数组元素结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"seat": 0, // 座位
|
||||
"playerid": 100001, // 玩家ID
|
||||
"name": "", // 昵称
|
||||
"avatar": "", // 头像
|
||||
"score": 0 // 成绩
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 战绩(大局)gameinfo2
|
||||
|
||||
> 非客户端收发包:与 gameinfo1 同批,由 `import.save_grade` 的第 3 个参数传给平台战绩服务(服务器→平台)。
|
||||
|
||||
牌局列表,数组元素结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"starttime": "", // 开始时间
|
||||
"endtime": "", // 结束时间
|
||||
"seatlist": [6, -6, 0], // 玩家成绩
|
||||
"callproc": [], // 叫分过程,详情见代码中的注释
|
||||
"banker": 0, // 庄
|
||||
"call": 0, // 叫分
|
||||
"flower": 0, // 主牌花色
|
||||
"result": 0, // 牌局结果
|
||||
"cards": [] // 发牌出牌情况,详情见代码中的注释
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user