# RPC处理机制详解 > **文档目标**:详细说明进贤麻将的RPC请求处理流程,包括RpcHandler、OperationEnumerator、AIRpcHandler的功能和使用方法。 ## 📚 目录 1. [RPC机制概述](#1-rpc机制概述) 2. [RpcHandler - RPC请求处理器](#2-rpchandler---rpc请求处理器) 3. [OperationEnumerator - 操作列举器](#3-operationenumerator---操作列举器) 4. [AIRpcHandler - AI玩家处理器](#4-airpchandler---ai玩家处理器) 5. [RPC处理标准流程](#5-rpc处理标准流程) 6. [数据包构建规范](#6-数据包构建规范) 7. [实现示例](#7-实现示例) --- ## 1. RPC机制概述 ### 1.1 什么是RPC **RPC (Remote Procedure Call)** 是远程过程调用,允许客户端通过网络调用服务端的方法。 在友乐游戏框架中: ``` 客户端(浏览器) 服务端(Node.js) │ │ │ WebSocket/HTTP │ │ ────────────────────────> │ │ {app, route, rpc, data} │ │ │ │ packet.js │ ↓ │ youle_app │ ↓ │ mod_jinxianmahjong │ ↓ │ mod_jinxianmahjong.player_discard(pack) │ ↓ │ 处理业务逻辑 │ ↓ │ <────────────────────────│ │ 响应数据包 │ ``` ### 1.2 三层路由机制 ``` 【第1层】packet.js - 网络层 ↓ 根据app字段路由 【第2层】youle_app - 应用层 ↓ 根据route字段路由 【第3层】mod_jinxianmahjong - 模块层 ↓ 根据rpc字段调用 【执行】RPC方法执行 ``` **示例数据包**: ```javascript { "app": "youle", // 应用标识 → 路由到youle_app "route": "jinxianmahjong", // 模块标识 → 路由到mod_jinxianmahjong "rpc": "player_discard", // 方法名 → 调用mod_jinxianmahjong.player_discard() "data": { // 业务数据 "agentid": "agent001", "playerid": 12345, "gameid": "jinxianmahjong", "roomcode": 100001, "seat": 0, "cardUniqueId": 45 } } ``` ### 1.3 进贤麻将RPC架构 ``` ┌────────────────────────────────────────┐ │ mod.js (RPC定义) │ │ - 定义所有RPC方法 │ │ - mod_jinxianmahjong.player_discard │ │ - mod_jinxianmahjong.player_peng │ │ - mod_jinxianmahjong.player_gang │ │ - ... │ └───────────────┬────────────────────────┘ │ 调用 ┌───────────────▼────────────────────────┐ │ RpcHandler.js │ │ - 处理RPC请求 │ │ - 参数提取和验证 │ │ - 调用业戏控制器 │ │ - 构建响应数据包 │ └───────────────┬────────────────────────┘ │ 委托 ┌───────────────▼────────────────────────┐ │ GameController / OperationManager │ │ - 游戏逻辑控制 │ │ - 状态管理 │ │ - 操作验证 │ └───────────────┬────────────────────────┘ │ 发包 ┌───────────────▼────────────────────────┐ │ o_room.method.sendpack_* │ │ - sendpack_toall() │ │ - sendpack_toseat() │ │ - sendpack_toother() │ └────────────────────────────────────────┘ ``` --- ## 2. RpcHandler - RPC请求处理器 ### 2.1 RpcHandler的职责 RpcHandler是进贤麻将的**核心RPC处理模块**,负责: 1. **接收和解析RPC请求**:提取参数、验证格式 2. **玩家身份验证**:调用`check_player`验证玩家 3. **委托业务逻辑**:调用GameController或OperationManager 4. **构建响应数据**:按"一包多信息"原则构建响应 5. **发送响应**:调用发包接口推送给客户端 6. **错误处理**:统一的错误处理和日志记录 ### 2.2 主要RPC方法 进贤麻将实现的核心RPC方法: | RPC方法 | 对应操作 | 说明 | |--------|---------|------| | `handlePlayCard` | 出牌 | 玩家打出一张手牌 | | `handleDeclarePeng` | 碰牌 | 玩家碰别人打出的牌 | | `handleDeclareGang` | 杠牌 | 玩家杠牌(明杠/暗杠/加杠) | | `handleDeclareHu` | 胡牌 | 玩家胡牌(自摸/点炮) | | `handlePass` | 过牌 | 玩家放弃吃碰杠胡机会 | | `handleReady` | 准备 | 玩家准备开始游戏 | | `getGameState` | 获取状态 | 获取当前游戏状态 | ### 2.3 RPC方法标准结构 每个RPC方法遵循统一的处理流程: ```javascript handlePlayCard: function(pack) { try { // ===== 第1步:提取参数 ===== var agentid = pack.data.agentid; var playerid = parseInt(pack.data.playerid); var gameid = pack.data.gameid; var roomcode = pack.data.roomcode; var seat = parseInt(pack.data.seat); var cardUniqueId = parseInt(pack.data.cardUniqueId); console.log('[RpcHandler.playCard] 开始处理出牌请求:', { playerid, seat, cardUniqueId }); // ===== 第2步:验证玩家 ===== var o_room = mod_jinxianmahjong.import.check_player( agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid ); if (!o_room) { console.error('[RpcHandler.playCard] 玩家验证失败'); return { success: false, error: "玩家验证失败" }; } // ===== 第3步:获取游戏对象 ===== var o_desk = o_room.o_desk; if (!o_desk) { console.error('[RpcHandler.playCard] 游戏桌不存在'); this.sendErrorResponse(o_room, seat, 500, "游戏桌不存在"); return { success: false, error: "游戏桌不存在" }; } // ===== 第4步:记录收包(调试) ===== if (o_desk.debug && typeof o_desk.debug.save_receivepack === 'function') { o_desk.debug.save_receivepack(pack, seat, playerid); } // ===== 第5步:委托业务逻辑 ===== var operationRequest = { operation: "discard_card", playerSeat: seat, uniqueId: cardUniqueId, requestId: this._generateRequestId(), timestamp: Date.now() }; var operationResult = OperationManager.handleOperation(o_room, operationRequest); // ===== 第6步:处理结果 ===== if (operationResult.success) { // 构建响应数据包 var responseData = this._buildPlayCardResponse(o_room, seat, cardUniqueId, operationResult); // 发送响应 this._sendResponse(o_room, seat, responseData); return { success: true }; } else { // 操作失败,发送错误 this.sendErrorResponse(o_room, seat, 400, operationResult.error); return { success: false, error: operationResult.error }; } } catch (error) { console.error('[RpcHandler.playCard] 处理失败:', error); this.sendErrorResponse(o_room, seat, 500, "服务器内部错误"); return { success: false, error: error.message }; } } ``` ### 2.4 "一包多信息"原则 RpcHandler遵循友乐平台的**"一包多信息"设计原则**: **原则说明**: - 单个响应包包含**完整的状态更新信息** - 减少网络请求次数 - 确保客户端状态同步 **示例**:出牌响应包包含 ```javascript { status: 200, seat: 0, // 出牌玩家 discardedCard: {...}, // 打出的牌 gameState: { // 游戏状态更新 currentPlayer: 1, remainingCards: 52, phase: "playing" }, playerActions: { // 其他玩家可执行操作 1: { availableActions: ["peng", "gang", "hu"], pengResult: {...}, // 碰牌详情 gangResult: {...}, // 杠牌详情 huResult: {...}, // 胡牌详情 timeout: 10000 }, 2: { availableActions: ["hu"], huResult: {...}, timeout: 10000 } }, autoDrawCard: null, // 是否自动摸牌 waitingForResponse: true // 是否等待响应 } ``` ### 2.5 分层推送机制 RpcHandler实现**分层推送**,为不同玩家定制不同的信息: ```javascript // 1. 给操作玩家:包含完整信息 var dataForPlayer = { status: 200, seat: 0, myHandCards: [...], // 我的手牌(完整) discardedCard: {...}, gameState: {...} }; o_room.method.sendpack_toseat(dataForPlayer, 0); // 2. 给其他玩家:隐藏私密信息 var dataForOthers = { status: 200, seat: 0, handCardCount: 13, // 只显示数量,不显示具体牌 discardedCard: {...}, gameState: {...} }; o_room.method.sendpack_toother(dataForOthers, 0); ``` --- ## 3. OperationEnumerator - 操作列举器 ### 3.1 OperationEnumerator的职责 OperationEnumerator负责**生成玩家可执行的所有操作选项**: 1. **枚举所有可能操作**:出牌、吃、碰、杠、胡、过 2. **计算操作参数**:每个操作需要的牌、组合等 3. **生成choiceIndex**:为每个操作分配索引 4. **验证操作合法性**:确保操作符合规则 ### 3.2 操作类型 ```javascript var operationTypes = { discard: [], // 出牌操作 chi: [], // 吃牌操作 peng: [], // 碰牌操作 gang: [], // 杠牌操作(明杠、暗杠、加杠) hu: [], // 胡牌操作 pass: [] // 过牌操作 }; ``` ### 3.3 核心方法 #### generateAvailableOperations 生成完整的可执行操作列表: ```javascript /** * 为指定玩家生成完整的可执行操作列表 * @param {Object} gameState - 当前游戏状态 * @param {number} seat - 玩家座位号 * @param {Object} context - 上下文信息 * @returns {Object} 完整的操作列表 */ generateAvailableOperations(gameState, seat, context = {}) { var operations = { discard: [], chi: [], peng: [], gang: [], hu: [], pass: [] }; try { // 1. 生成出牌操作 operations.discard = this.generateDiscardActions(gameState, seat); // 2. 如果有刚出的牌,检查吃碰杠胡操作 if (context.lastDiscardCard) { operations.chi = this.generateChiActions(gameState, seat, context.lastDiscardCard); operations.peng = this.generatePengActions(gameState, seat, context.lastDiscardCard); operations.gang = this.generateMingGangActions(gameState, seat, context.lastDiscardCard); operations.hu = this.generateHuActions(gameState, seat, context.lastDiscardCard, "discard"); } // 3. 检查自摸杠牌和胡牌 if (context.justDrawCard) { operations.gang = operations.gang.concat(this.generateAnGangActions(gameState, seat)); operations.gang = operations.gang.concat(this.generateJiaGangActions(gameState, seat)); operations.hu = operations.hu.concat(this.generateHuActions(gameState, seat, context.justDrawCard, "draw")); } // 4. 生成过牌操作 if (this.hasNonDiscardOperations(operations)) { operations.pass = this.generatePassAction(); } } catch (error) { console.error('[OperationEnumerator] 生成操作列表时出错:', error); } return operations; } ``` #### generateDiscardActions 生成出牌操作: ```javascript generateDiscardActions(gameState, seat) { if (!gameState.playerHands || !gameState.playerHands[seat]) { return []; } var handCards = gameState.playerHands[seat]; var allowedCards = []; // 获取所有手牌的uniqueId for (var i = 0; i < handCards.length; i++) { if (handCards[i] && handCards[i].uniqueId) { allowedCards.push(handCards[i].uniqueId); } } return [{ choiceIndex: 0, operationType: "discard", allowedCards: allowedCards, description: "出牌操作" }]; } ``` #### generatePengActions 生成碰牌操作: ```javascript generatePengActions(gameState, seat, sourceCard, fromSeat) { var pengActions = []; if (!gameState.playerHands || !gameState.playerHands[seat] || !sourceCard) { return pengActions; } var handCards = gameState.playerHands[seat]; var sourceCode = sourceCard.code || sourceCard; // 检查手牌中是否有至少2张相同的牌 var sameCards = this.findSameCards(handCards, sourceCode, 2); if (sameCards.length >= 2) { pengActions.push({ choiceIndex: 0, operationType: "peng", sourceCard: { uniqueId: sourceCard.uniqueId, code: sourceCode, fromSeat: fromSeat }, requiredCards: sameCards.slice(0, 2), description: "碰" + this.getCardName(sourceCode) }); } return pengActions; } ``` ### 3.4 choiceIndex机制 **choiceIndex**是操作选择的索引,用于客户端选择和服务端执行: ```javascript // 服务端生成操作列表 var operations = OperationEnumerator.generateAvailableOperations(gameState, seat, context); // 示例输出 { peng: [ { choiceIndex: 0, operationType: "peng", ... }, // 碰1万 ], gang: [ { choiceIndex: 0, operationType: "gang", gangType: "angang", ... }, // 暗杠2万 { choiceIndex: 1, operationType: "gang", gangType: "jiagang", ... } // 加杠3万 ], hu: [ { choiceIndex: 0, operationType: "hu", huType: "zimo", ... } ] } // 客户端选择操作,发送choiceIndex // 例如:选择加杠操作(choiceIndex=1) var request = { operation: "gang", choiceIndex: 1, // 选择第2个杠牌操作 // ... }; // 服务端根据choiceIndex执行对应操作 var selectedOperation = operations.gang[choiceIndex]; ``` --- ## 4. AIRpcHandler - AI玩家处理器 ### 4.1 AIRpcHandler的职责 AIRpcHandler处理**AI玩家的自动操作**: 1. **AI决策**:根据游戏状态做出决策 2. **自动操作**:自动出牌、吃碰杠胡 3. **延时模拟**:模拟人类玩家的思考时间 4. **策略选择**:根据难度选择不同策略 ### 4.2 AI决策流程 ```javascript // AI玩家轮到操作 AIRpcHandler.handleAITurn(o_room, aiSeat) { // 1. 获取可执行操作 var operations = OperationEnumerator.generateAvailableOperations( gameState, aiSeat, context ); // 2. AI决策 var decision = AIStrategy.makeDecision(gameState, aiSeat, operations); // 3. 延时模拟思考 setTimeout(function() { // 4. 执行AI操作 if (decision.operation === "discard") { AIRpcHandler.executeAIDiscard(o_room, aiSeat, decision.cardUniqueId); } else if (decision.operation === "peng") { AIRpcHandler.executeAIPeng(o_room, aiSeat, decision); } // ... }, decision.thinkingTime); } ``` ### 4.3 AI策略 ```javascript // 简单策略示例 AIStrategy.makeDecision = function(gameState, seat, operations) { // 优先级:胡 > 杠 > 碰 > 吃 > 出牌 if (operations.hu.length > 0) { return { operation: "hu", choiceIndex: 0 }; } if (operations.gang.length > 0) { return { operation: "gang", choiceIndex: 0 }; } if (operations.peng.length > 0) { return { operation: "peng", choiceIndex: 0 }; } // 默认出牌:出最不需要的牌 var cardToDiscard = this.selectCardToDiscard(gameState, seat); return { operation: "discard", cardUniqueId: cardToDiscard.uniqueId }; }; ``` --- ## 5. RPC处理标准流程 ### 5.1 完整RPC处理流程图 ``` 客户端发送请求 ↓ 【1】packet.js接收 ↓ 【2】youle_app路由 ↓ 【3】mod_jinxianmahjong路由 ↓ 【4】RPC方法(如player_discard) ↓ 【5】RpcHandler.handlePlayCard ├─ 提取参数 ├─ check_player验证 ├─ 记录收包 └─ 委托OperationManager ↓ 【6】OperationManager.handleOperation ├─ 验证操作合法性 ├─ 执行游戏逻辑 ├─ 更新游戏状态 └─ 返回结果 ↓ 【7】RpcHandler构建响应 ├─ 基础响应数据 ├─ 检查其他玩家操作机会 ├─ 添加操作提示 └─ 构建完整响应包 ↓ 【8】发送响应 ├─ sendpack_toseat(给操作玩家) ├─ sendpack_toother(给其他玩家) └─ sendpack_toall(广播给所有人) ↓ 客户端接收响应 ├─ 更新本地状态 ├─ 播放动画 └─ 显示界面 ``` ### 5.2 标准RPC方法实现模板 ```javascript // 在mod.js中定义RPC方法 mod_jinxianmahjong.player_discard = function(pack) { return RpcHandler.handlePlayCard(pack); }; mod_jinxianmahjong.player_peng = function(pack) { return RpcHandler.handleDeclarePeng(pack); }; mod_jinxianmahjong.player_gang = function(pack) { return RpcHandler.handleDeclareGang(pack); }; mod_jinxianmahjong.player_hu = function(pack) { return RpcHandler.handleDeclareHu(pack); }; mod_jinxianmahjong.player_pass = function(pack) { return RpcHandler.handlePass(pack); }; ``` --- ## 6. 数据包构建规范 ### 6.1 响应包标准结构 ```javascript { status: 200, // HTTP状态码风格 message: "操作成功", // 消息说明 data: { // 业务数据 // 具体业务数据 }, timestamp: 1234567890 // 时间戳 } ``` ### 6.2 错误响应结构 ```javascript { status: 400, // 错误状态码 error: "操作失败", // 错误消息 code: "INVALID_OPERATION", // 错误码 details: "详细错误信息", // 详细说明 timestamp: 1234567890 } ``` ### 6.3 状态码规范 | 状态码 | 含义 | 使用场景 | |-------|------|---------| | 200 | 成功 | 操作执行成功 | | 400 | 请求错误 | 参数错误、操作不合法 | | 401 | 未授权 | 玩家验证失败 | | 403 | 禁止操作 | 不是当前玩家的回合 | | 404 | 未找到 | 房间或玩家不存在 | | 500 | 服务器错误 | 内部错误 | --- ## 7. 实现示例 ### 7.1 完整的出牌RPC实现 ```javascript // mod.js - 定义RPC方法 mod_jinxianmahjong.player_discard = function(pack) { return RpcHandler.handlePlayCard(pack); }; // RpcHandler.js - 实现处理逻辑 RpcHandler.handlePlayCard = function(pack) { try { // 1. 提取和验证参数 var params = this._extractParams(pack); if (!params.valid) { return { success: false, error: params.error }; } // 2. 验证玩家 var o_room = mod_jinxianmahjong.import.check_player( params.agentid, params.gameid, params.roomcode, params.seat, params.playerid, pack.conmode, pack.fromid ); if (!o_room) { return { success: false, error: "玩家验证失败" }; } // 3. 执行出牌操作 var result = OperationManager.handleOperation(o_room, { operation: "discard_card", playerSeat: params.seat, uniqueId: params.cardUniqueId }); if (!result.success) { this.sendErrorResponse(o_room, params.seat, 400, result.error); return result; } // 4. 构建响应数据 var responseData = this._buildPlayCardResponse(o_room, params.seat, result); // 5. 发送响应 this._sendLayeredResponse(o_room, params.seat, responseData); return { success: true }; } catch (error) { console.error('[RpcHandler.playCard] 错误:', error); return { success: false, error: error.message }; } }; // 辅助方法:构建响应数据 RpcHandler._buildPlayCardResponse = function(o_room, seat, result) { var gameState = o_room.o_desk.gameState; return { status: 200, seat: seat, discardedCard: this._serializeCard(result.discardedCard), gameState: { phase: gameState.phase, currentPlayer: gameState.currentPlayer, remainingCards: gameState.gameData.deck.length }, playerActions: this._buildPlayerActions(o_room, result), waitingForResponse: result.hasResponse, timestamp: Date.now() }; }; // 辅助方法:分层发送响应 RpcHandler._sendLayeredResponse = function(o_room, seat, responseData) { // 给操作玩家:包含完整信息 var dataForPlayer = Object.assign({}, responseData, { myHandCards: this._serializeHandCards(o_room, seat) }); o_room.method.sendpack_toseat(dataForPlayer, seat); // 给其他玩家:隐藏私密信息 var dataForOthers = Object.assign({}, responseData, { handCardCount: this._getHandCardCount(o_room, seat) }); o_room.method.sendpack_toother(dataForOthers, seat); }; ``` ### 7.2 操作枚举示例 ```javascript // 使用OperationEnumerator var operations = OperationEnumerator.generateAvailableOperations( gameState, seat, { lastDiscardCard: { uniqueId: 45, code: 13 }, // 刚出的3万 fromSeat: 1 } ); // 输出示例 { discard: [], chi: [], peng: [ { choiceIndex: 0, operationType: "peng", sourceCard: { uniqueId: 45, code: 13, fromSeat: 1 }, requiredCards: [ { uniqueId: 12, code: 13 }, { uniqueId: 78, code: 13 } ], description: "碰3万" } ], gang: [], hu: [ { choiceIndex: 0, operationType: "hu", huType: "dianpao", winCards: [...], score: 8, description: "胡牌 - 8分" } ], pass: [ { choiceIndex: 0, operationType: "pass", description: "过" } ] } ``` --- ## 8. 最佳实践 ### 8.1 参数提取 ```javascript // ✅ 统一的参数提取方法 _extractParams: function(pack) { try { return { valid: true, agentid: pack.data.agentid, playerid: parseInt(pack.data.playerid), gameid: pack.data.gameid, roomcode: pack.data.roomcode, seat: parseInt(pack.data.seat), cardUniqueId: parseInt(pack.data.cardUniqueId) }; } catch (error) { return { valid: false, error: "参数解析失败: " + error.message }; } } ``` ### 8.2 错误处理 ```javascript // ✅ 统一的错误响应 sendErrorResponse: function(o_room, seat, statusCode, message) { var errorMsg = { app: "youle", route: "jinxianmahjong", rpc: "error", data: { status: statusCode, error: message, timestamp: Date.now() } }; o_room.method.sendpack_toseat(errorMsg, seat); } ``` ### 8.3 日志记录 ```javascript // ✅ 详细的日志记录 console.log('[RpcHandler.playCard] 开始处理:', { playerid: params.playerid, seat: params.seat, cardUniqueId: params.cardUniqueId }); console.log('[RpcHandler.playCard] 操作结果:', { success: result.success, error: result.error }); ``` --- ## 9. 常见问题 ### Q1: RpcHandler和OperationManager的区别? A: - **RpcHandler**:处理RPC请求,负责参数提取、验证、响应构建 - **OperationManager**:执行游戏逻辑,负责状态管理、规则验证 ### Q2: 为什么需要OperationEnumerator? A: 因为需要提前告诉客户端有哪些操作可以执行,客户端根据choiceIndex选择操作。 ### Q3: choiceIndex有什么用? A: choiceIndex是操作选择的索引: - 服务端生成所有可能操作并分配索引 - 客户端选择后发送choiceIndex - 服务端根据索引执行对应操作 ### Q4: 如何实现分层推送? A: 使用不同的发包接口: - `sendpack_toseat`:发给操作玩家(完整信息) - `sendpack_toother`:发给其他玩家(隐藏私密信息) ### Q5: AI玩家如何处理? A: AI玩家通过AIRpcHandler自动处理: - 监听游戏状态变化 - 自动调用AI决策 - 模拟延时后执行操作 --- ## 10. 下一步 阅读以下文档继续学习: - [04-游戏核心服务](../core/04-游戏核心服务.md) - GameController和OperationManager详解 - [05-共享代码模块](../core/05-共享代码模块.md) - 核心算法实现 - [08-游戏流程概述](../architecture/08-游戏流程概述.md) - 完整游戏流程 --- **相关代码文件**: - `server/games2/jinxianmahjong/mod.js` - RPC方法定义 - `server/games2/jinxianmahjong/rpc/RpcHandler.js` - RPC处理器 - `server/games2/jinxianmahjong/rpc/OperationEnumerator.js` - 操作列举器 - `server/games2/jinxianmahjong/rpc/AIRpcHandler.js` - AI处理器