# Import接口详细说明 > **文档目标**:详细说明子游戏如何调用友乐游戏框架提供的服务接口,包括4个核心接口的使用方法和调用时机。 ## 📚 目录 1. [Import接口概述](#1-import接口概述) 2. [4个核心接口详解](#2-4个核心接口详解) 3. [调用时机和注意事项](#3-调用时机和注意事项) 4. [发包接口详解](#4-发包接口详解) 5. [实现模板和示例](#5-实现模板和示例) 6. [常见问题](#6-常见问题) --- ## 1. Import接口概述 ### 1.1 什么是Import接口 Import接口是**子游戏调用框架**的标准接口集合,定义在`import.js`文件中。子游戏通过这些接口来: - 验证玩家身份和权限 - 扣除房卡费用 - 保存游戏成绩 - 完成游戏任务 - 发送数据包给客户端 ``` ┌─────────────────┐ │ import.js │ │ (子游戏) │ └────────┬────────┘ │ 调用import接口 ↓ ┌─────────────────┐ │ 友乐框架 │ ← 框架提供的服务 │ (youle_app) │ └─────────────────┘ ``` ### 1.2 4个核心接口清单 | 序号 | 接口名称 | 调用时机 | 主要作用 | |-----|---------|---------|---------| | 1 | `check_player` | 每个RPC方法开始时 | 验证玩家身份和位置 ⭐⭐⭐⭐⭐ | | 2 | `deduct_roomcard` | 第一小局结算时 | 扣除房卡 ⭐⭐⭐⭐⭐ | | 3 | `save_grade` | 大局游戏结束时 | 保存成绩并释放房间 ⭐⭐⭐⭐⭐ | | 4 | `finish_gametask` | 完成游戏任务时 | 触发任务奖励(可选) ⭐⭐ | ### 1.3 实现方式 ```javascript // import.js文件结构 var cls_jinxianmahjong_import = { new: function() { var imp = {}; // 实现4个核心接口 imp.check_player = function(agentid, gameid, roomcode, seat, playerid, conmode, fromid) { // 调用框架验证服务 return mod_jinxianmahjong.app.youle_room.export.check_player(...); }; imp.deduct_roomcard = function(o_room) { // 调用框架扣卡服务 return mod_jinxianmahjong.app.youle_room.export.deduct_roomcard(o_room); }; imp.save_grade = function(o_room, o_gameinfo1, o_gameinfo2, freeroomflag) { // 调用框架成绩保存服务 mod_jinxianmahjong.app.youle_room.export.save_grade(...); }; imp.finish_gametask = function(agentid, o_player, taskid, finishamount) { // 调用框架任务完成服务 return mod_jinxianmahjong.app.youle_room.export.finish_gametask(...); }; return imp; } }; // 挂载到模块 mod_jinxianmahjong.import = cls_jinxianmahjong_import.new(); ``` --- ## 2. 4个核心接口详解 ### 2.1 check_player - 验证玩家身份 ⭐⭐⭐⭐⭐ #### 功能说明 **最重要的接口**,在每个RPC方法开始时必须调用。负责验证: 1. 玩家是否在指定房间 2. 玩家是否在指定座位 3. 连接是否有效 4. 房间状态是否正常 #### 接口定义 ```javascript imp.check_player = function(agentid, gameid, roomcode, seat, playerid, conmode, fromid) { // 返回房间对象或null return Object | null; } ``` #### 参数说明 | 参数 | 类型 | 说明 | 示例 | |-----|------|------|------| | `agentid` | String | 代理商ID | "agent001" | | `gameid` | String | 游戏ID | "jinxianmahjong" | | `roomcode` | Number | 房间号码(需parseInt转换) | 100001 | | `seat` | Number | 座位号(需parseInt转换) | 0 | | `playerid` | Number | 玩家ID(需parseInt转换) | 12345 | | `conmode` | String | 连接模式 | "tcp" | | `fromid` | String | 来源连接ID | "conn_123" | #### 返回值 **成功时**:返回房间对象(`o_room`) ```javascript { roomcode: 100001, roomtype: "1312111000", seatlist: [...], o_desk: {...}, // 游戏桌对象 method: { // 发包接口 sendpack_toall: function() {}, sendpack_toseat: function() {}, sendpack_toother: function() {} } } ``` **失败时**:返回`null` #### 验证内容 框架会检查以下内容: 1. ✅ 房间是否存在 2. ✅ 玩家是否在该房间 3. ✅ 玩家是否在指定座位 4. ✅ 连接ID是否匹配 5. ✅ 房间状态是否正常 #### 调用时机 **必须在每个RPC方法开始时调用**: ```javascript mod_jinxianmahjong.player_discard = function(pack) { // 1. 提取参数 var agentid = pack.data.agentid; var playerid = parseInt(pack.data.playerid); var gameid = pack.data.gameid; var roomcode = parseInt(pack.data.roomcode); var seat = parseInt(pack.data.seat); // 2. 验证玩家(必须!) var o_room = mod_jinxianmahjong.import.check_player( agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid ); // 3. 验证失败直接返回 if (!o_room) { console.error("[player_discard] 玩家验证失败"); return; // 框架会自动处理错误响应 } // 4. 验证成功,继续处理业务逻辑 var o_desk = o_room.o_desk; // ... }; ``` #### 为什么必须验证 **安全性**: - 防止玩家冒充他人 - 防止操作不属于自己的房间 - 防止座位错误导致的数据混乱 **稳定性**: - 确保房间存在且有效 - 确保连接状态正常 - 避免访问不存在的对象导致崩溃 #### 实现示例 ```javascript imp.check_player = function(agentid, gameid, roomcode, seat, playerid, conmode, fromid) { try { // 调用框架验证服务 return mod_jinxianmahjong.app.youle_room.export.check_player( agentid, gameid, roomcode, seat, playerid, conmode, fromid ); } catch (error) { console.error('[import.check_player] 玩家验证失败:', error); return null; } }; ``` --- ### 2.2 deduct_roomcard - 扣除房卡 ⭐⭐⭐⭐⭐ #### 功能说明 扣除创建房间所需的房卡费用。**非常重要**:必须在**第一小局结算时**调用,而不是开战时。 #### 接口定义 ```javascript imp.deduct_roomcard = function(o_room) { // 无返回值或返回扣卡结果 return void | Object; } ``` #### 参数说明 | 参数 | 类型 | 说明 | |-----|------|------| | `o_room` | Object | 房间对象 | #### 调用时机 ⚠️ **关键时机**:第一小局结算时调用 ``` 游戏开战(makewar) ↓ 第1局开始 ↓ 玩家游戏 ↓ 第1局结束(有胡牌或流局) ↓ 第1局结算 ← 在这里调用 deduct_roomcard() ⭐⭐⭐ ↓ 第2局开始 ↓ ... ``` **为什么不在开战时扣卡**: 1. 开战后可能立即解散(投票解散) 2. 只有真正开始游戏才扣卡,更公平 3. 避免误扣(网络问题等) #### 典型调用场景 ```javascript // 在第一小局结算函数中 function handleRoundEnd(o_room, winnerSeat, roundNumber) { // 计算本局得分 var scores = calculateScores(o_room, winnerSeat); // 更新玩家分数 updatePlayerScores(o_room, scores); // ⚠️ 关键:第一局结算时扣房卡 if (roundNumber === 1) { console.log("[handleRoundEnd] 第一局结算,扣除房卡"); mod_jinxianmahjong.import.deduct_roomcard(o_room); } // 判断是否继续游戏 if (shouldContinue(o_room)) { startNextRound(o_room); } else { endGame(o_room); } } ``` #### 扣卡规则 框架会根据之前`get_needroomcard`的返回值扣除相应房卡: | 房间类型 | 扣除房卡数 | 从谁扣除 | |---------|-----------|---------| | 8局房间 | 1张 | 房主(或AA制时每人) | | 16局房间 | 2张 | 房主(或AA制时每人) | | 24局房间 | 3张 | 房主(或AA制时每人) | #### 实现示例 ```javascript imp.deduct_roomcard = function(o_room) { try { console.log('[import.deduct_roomcard] 第一小局结算,开始扣除房卡:', { roomcode: o_room.roomcode || o_room.roomid, roomtype: o_room.roomtype }); // 调用框架房卡扣除服务 var result = mod_jinxianmahjong.app.youle_room.export.deduct_roomcard(o_room); console.log('[import.deduct_roomcard] 房卡扣除完成:', result); return result; } catch (error) { console.error('[import.deduct_roomcard] 扣除房卡失败:', error); throw error; } }; ``` --- ### 2.3 save_grade - 保存游戏成绩 ⭐⭐⭐⭐⭐ #### 功能说明 保存玩家的游戏成绩到数据库。**非常重要**: 1. 必须在**大局游戏结束**时调用 2. 调用后框架会**自动处理房间释放** 3. 这是游戏流程的**最后一步** #### 接口定义 ```javascript imp.save_grade = function(o_room, o_gameinfo1, o_gameinfo2, freeroomflag) { // 无返回值 return void; } ``` #### 参数说明 **o_room** - 房间对象 **o_gameinfo1** - 第一组玩家成绩信息 ```javascript { playerid: 12345, // 玩家ID nickname: "玩家1", // 昵称 avatar: "avatar1.jpg", // 头像 score: 150, // 总分数 winCount: 5, // 胜利局数 loseCount: 3, // 失败局数 // 其他统计信息 } ``` **o_gameinfo2** - 第二组玩家成绩信息(可选) - 对于2-4人游戏,可能需要多个玩家的成绩 - 可以传入数组或对象 **freeroomflag** - 是否为免费房间 - `true`: 免费房间(练习、活动等) - `false`: 正常房卡房间 #### 调用时机 ⚠️ **关键时机**:大局游戏结束时调用 ``` 游戏进行 ↓ 最后一局结束 ↓ 检查是否达到总局数 ↓ 大局结束,计算最终成绩 ↓ 调用 save_grade() ← 在这里 ⭐⭐⭐ ↓ 框架自动释放房间 ↓ 玩家返回大厅 ``` #### 重要注意事项 **1. 调用时机必须正确** - ✅ 所有局数完成后 - ✅ 最终成绩计算完毕后 - ✅ 所有结算数据准备好后 - ❌ 不要在中途调用 - ❌ 不要重复调用 **2. 框架自动释放房间** - 调用`save_grade`后,框架会自动处理房间解散 - 子游戏**不需要**再调用解散接口 - 房间对象会被清理,不能再使用 **3. 成绩数据完整性** - 确保所有玩家成绩都已计算 - 确保统计数据准确无误 - 成绩一旦保存无法修改 #### 典型调用场景 ```javascript // 大局结束处理函数 function handleGameEnd(o_room) { var gameState = o_room.o_desk.gameState; // 1. 检查是否达到总局数 if (gameState.currentRound < gameState.rulesConfig.gameRules.maxRounds) { console.log("[handleGameEnd] 游戏尚未结束"); return; } // 2. 计算最终成绩 var finalScores = calculateFinalScores(gameState); // 3. 准备成绩数据 var gameinfo1 = preparePlayerGradeInfo(gameState, 0); // 玩家1 var gameinfo2 = preparePlayerGradeInfo(gameState, 1); // 玩家2 // ... 可能有更多玩家 // 4. 调用save_grade保存成绩 console.log("[handleGameEnd] 大局结束,保存游戏成绩"); mod_jinxianmahjong.import.save_grade( o_room, gameinfo1, gameinfo2, false // 不是免费房间 ); // 5. ⚠️ 注意:调用save_grade后不要再操作o_room console.log("[handleGameEnd] 成绩已保存,房间将由框架自动释放"); } // 准备玩家成绩信息 function preparePlayerGradeInfo(gameState, seat) { var playerState = gameState.playersState[seat]; return { playerid: playerState.playerid, nickname: playerState.nickname, avatar: playerState.avatar, score: playerState.totalScore, winCount: playerState.statistics.winCount, loseCount: playerState.statistics.loseCount, zimoCount: playerState.statistics.zimoCount, dianpaoCount: playerState.statistics.dianpaoCount, gangCount: playerState.statistics.gangCount }; } ``` #### 实现示例 ```javascript imp.save_grade = function(o_room, o_gameinfo1, o_gameinfo2, freeroomflag) { try { console.log('[import.save_grade] 大局游戏结束,保存成绩:', { roomcode: o_room.roomcode || o_room.roomid, player1: o_gameinfo1 ? o_gameinfo1.playerid : null, player2: o_gameinfo2 ? o_gameinfo2.playerid : null, isFreeRoom: freeroomflag }); // 调用框架成绩保存服务 mod_jinxianmahjong.app.youle_room.export.save_grade( o_room, o_gameinfo1, o_gameinfo2, freeroomflag ); console.log('[import.save_grade] 游戏成绩保存完成'); } catch (error) { console.error('[import.save_grade] 保存游戏成绩失败:', error); throw error; } }; ``` --- ### 2.4 finish_gametask - 完成游戏任务 #### 功能说明 触发玩家完成游戏任务的奖励,如每日任务、成就系统等。这是一个**可选接口**,不是所有游戏都需要。 #### 接口定义 ```javascript imp.finish_gametask = function(agentid, o_player, taskid, finishamount) { // 返回任务完成结果 return Object | null; } ``` #### 参数说明 | 参数 | 类型 | 说明 | |-----|------|------| | `agentid` | String | 代理商ID | | `o_player` | Object | 玩家对象 | | `taskid` | String | 任务ID | | `finishamount` | Number | 完成数量(可选) | #### 调用时机 当玩家完成特定游戏任务时: - 完成一局游戏 - 达成特定成就(如自摸、杠牌等) - 完成每日任务 - 达到某个统计目标 #### 使用示例 ```javascript // 在游戏结算时检查任务 function checkAndCompleteTasks(o_room, playerSeat, roundResult) { var player = o_room.seatlist[playerSeat]; // 检查是否完成"玩10局游戏"任务 if (player.gamesPlayed === 10) { mod_jinxianmahjong.import.finish_gametask( o_room.agentid, player, "task_play_10_games", 1 ); } // 检查是否完成"自摸3次"任务 if (roundResult.isZimo && player.zimoCount === 3) { mod_jinxianmahjong.import.finish_gametask( o_room.agentid, player, "task_zimo_3_times", 1 ); } } ``` #### 实现示例 ```javascript imp.finish_gametask = function(agentid, o_player, taskid, finishamount) { try { return mod_jinxianmahjong.app.youle_room.export.finish_gametask( agentid, o_player, taskid, finishamount ); } catch (error) { console.error('[import.finish_gametask] 完成游戏任务失败:', error); return null; } }; ``` --- ## 3. 调用时机和注意事项 ### 3.1 接口调用时机总览 ``` 房间创建 ↓ 玩家加入 ↓ 游戏开战(makewar) ↓ ┌─────────────────────────────────────┐ │ 游戏进行中 │ │ │ │ 每个RPC请求: │ │ ├─ check_player() ← 必须调用 │ │ └─ 处理业务逻辑 │ │ │ │ 第1局结束: │ │ ├─ 结算 │ │ └─ deduct_roomcard() ← 扣房卡 │ │ │ │ 第2-N局: │ │ └─ 正常进行 │ │ │ │ 达到任务条件: │ │ └─ finish_gametask() ← 可选 │ └─────────────────────────────────────┘ ↓ 最后一局结束 ↓ 计算最终成绩 ↓ save_grade() ← 保存成绩,释放房间 ↓ 游戏结束 ``` ### 3.2 关键注意事项 #### check_player ✅ **必须做**: - 每个RPC方法开始时调用 - 验证失败立即return - 使用parseInt转换数值参数 ❌ **禁止做**: - 跳过验证直接处理 - 验证失败继续执行 - 使用字符串类型的数值参数 #### deduct_roomcard ✅ **必须做**: - 在第一小局结算时调用 - 只调用一次 - 确保游戏真正开始了 ❌ **禁止做**: - 在makewar时调用(太早) - 在第二局或后续局调用(太晚) - 重复调用(会重复扣卡) #### save_grade ✅ **必须做**: - 在大局结束时调用 - 确保所有成绩计算完毕 - 准备完整的成绩数据 - 调用后不再操作房间对象 ❌ **禁止做**: - 在中途调用(游戏未结束) - 重复调用(数据会混乱) - 调用后继续使用房间对象 - 成绩数据不完整或错误 --- ## 4. 发包接口详解 除了4个核心接口,房间对象还提供了发包接口用于向客户端发送消息。 ### 4.1 发包接口类型 通过`o_room.method`访问发包接口: ```javascript var o_room = mod_jinxianmahjong.import.check_player(...); // 1. 广播给所有玩家 o_room.method.sendpack_toall(msg); // 2. 发送给指定座位玩家 o_room.method.sendpack_toseat(msg, seat); // 3. 发送给除指定座位外的其他玩家 o_room.method.sendpack_toother(msg, seat); ``` ### 4.2 sendpack_toall - 广播给所有玩家 **用途**:所有玩家都需要知道的信息 **典型场景**: - 玩家出牌 - 玩家吃碰杠 - 玩家胡牌 - 游戏状态变化 **示例**: ```javascript // 玩家出牌,广播给所有人 var msg = { app: "youle", route: "jinxianmahjong", rpc: "player_discard_result", data: { seat: 0, card: "3m", remainingCards: 52 } }; o_room.method.sendpack_toall(msg); ``` ### 4.3 sendpack_toseat - 发送给指定玩家 **用途**:只有特定玩家需要知道的信息 **典型场景**: - 发送玩家手牌(只给该玩家) - 发送操作提示(只给当前操作玩家) - 发送错误消息(只给操作失败的玩家) **示例**: ```javascript // 发送手牌给玩家(私密信息) var msg = { app: "youle", route: "jinxianmahjong", rpc: "deal_cards", data: { seat: 0, handCards: ["1m", "2m", "3m", ...] // 只有该玩家能看到 } }; o_room.method.sendpack_toseat(msg, 0); ``` ### 4.4 sendpack_toother - 发送给其他玩家 **用途**:除指定玩家外的其他玩家需要知道的信息 **典型场景**: - 显示其他玩家的手牌数量(不显示具体牌) - 通知其他玩家某人的操作 - 隐藏操作者的私密信息 **示例**: ```javascript // 通知其他玩家:玩家0摸了一张牌(不显示具体什么牌) var msg = { app: "youle", route: "jinxianmahjong", rpc: "player_draw", data: { seat: 0, cardCount: 14 // 只显示数量,不显示具体牌 } }; o_room.method.sendpack_toother(msg, 0); ``` ### 4.5 发包选择指南 | 信息类型 | 使用接口 | 原因 | |---------|---------|------| | 玩家出牌 | `sendpack_toall` | 所有人都能看到 | | 玩家摸牌 | `sendpack_toseat` + `sendpack_toother` | 本人看到具体牌,他人只看数量 | | 吃碰杠操作 | `sendpack_toall` | 公开操作,所有人可见 | | 胡牌结算 | `sendpack_toall` | 结算信息所有人都要看 | | 操作错误提示 | `sendpack_toseat` | 只提示操作失败的玩家 | | 手牌信息 | `sendpack_toseat` | 私密信息,只给本人 | --- ## 5. 实现模板和示例 ### 5.1 完整的import.js模板 ```javascript /** * 进贤麻将Import接口实现 */ var cls_jinxianmahjong_import = { new: function() { var imp = {}; // ===== 1. 玩家验证接口 ===== imp.check_player = function(agentid, gameid, roomcode, seat, playerid, conmode, fromid) { try { return mod_jinxianmahjong.app.youle_room.export.check_player( agentid, gameid, roomcode, seat, playerid, conmode, fromid ); } catch (error) { console.error('[import.check_player] 玩家验证失败:', error); return null; } }; // ===== 2. 房卡扣除接口 ===== imp.deduct_roomcard = function(o_room) { try { console.log('[import.deduct_roomcard] 第一小局结算,开始扣除房卡'); var result = mod_jinxianmahjong.app.youle_room.export.deduct_roomcard(o_room); console.log('[import.deduct_roomcard] 房卡扣除完成:', result); return result; } catch (error) { console.error('[import.deduct_roomcard] 扣除房卡失败:', error); throw error; } }; // ===== 3. 成绩保存接口 ===== imp.save_grade = function(o_room, o_gameinfo1, o_gameinfo2, freeroomflag) { try { console.log('[import.save_grade] 大局游戏结束,保存成绩'); mod_jinxianmahjong.app.youle_room.export.save_grade( o_room, o_gameinfo1, o_gameinfo2, freeroomflag ); console.log('[import.save_grade] 游戏成绩保存完成'); } catch (error) { console.error('[import.save_grade] 保存游戏成绩失败:', error); throw error; } }; // ===== 4. 完成游戏任务接口 ===== imp.finish_gametask = function(agentid, o_player, taskid, finishamount) { try { return mod_jinxianmahjong.app.youle_room.export.finish_gametask( agentid, o_player, taskid, finishamount ); } catch (error) { console.error('[import.finish_gametask] 完成游戏任务失败:', error); return null; } }; return imp; } }; // 挂载到模块 mod_jinxianmahjong.import = cls_jinxianmahjong_import.new(); console.log("[import] ✅ 进贤麻将Import接口加载完成"); console.log("[import] ⚠️ 请严格控制deduct_roomcard和save_grade的调用时机!"); ``` ### 5.2 标准RPC方法模板(使用Import接口) ```javascript mod_jinxianmahjong.player_discard = function(pack) { // ========== 第1步:提取参数 ========== var agentid = pack.data.agentid; var playerid = parseInt(pack.data.playerid); var gameid = pack.data.gameid; var roomcode = parseInt(pack.data.roomcode); var seat = parseInt(pack.data.seat); var cardToDiscard = pack.data.card; // ========== 第2步:验证玩家(必须!)========== var o_room = mod_jinxianmahjong.import.check_player( agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid ); if (!o_room) { console.error("[player_discard] 玩家验证失败"); return; // 验证失败直接返回 } // ========== 第3步:获取游戏对象 ========== var o_desk = o_room.o_desk; var gameState = o_desk.gameState; // ========== 第4步:执行业务逻辑 ========== try { // 调用游戏服务处理出牌 var result = o_desk.gameService.playerDiscard(seat, cardToDiscard); if (!result.success) { // 操作失败,发送错误消息 var errorMsg = { app: "youle", route: "jinxianmahjong", rpc: "operation_error", data: { seat: seat, error: result.error } }; o_room.method.sendpack_toseat(errorMsg, seat); return; } // ========== 第5步:构造并发送响应包 ========== // 发给所有玩家:玩家X出了一张牌 var msgToAll = { app: "youle", route: "jinxianmahjong", rpc: "player_discard_result", data: { seat: seat, card: cardToDiscard, remainingCards: gameState.gameData.deck.length } }; o_room.method.sendpack_toall(msgToAll); // ========== 第6步:检查游戏状态 ========== // 检查是否有玩家可以吃碰杠胡 var operations = checkAvailableOperations(gameState, cardToDiscard); if (operations.length > 0) { // 发送操作提示给对应玩家 operations.forEach(function(op) { var opMsg = { app: "youle", route: "jinxianmahjong", rpc: "operation_prompt", data: { seat: op.seat, operations: op.availableOps } }; o_room.method.sendpack_toseat(opMsg, op.seat); }); } } catch (error) { console.error("[player_discard] 处理出牌失败:", error); } }; ``` ### 5.3 第一局结算示例(扣房卡) ```javascript function handleFirstRoundEnd(o_room, roundResult) { var gameState = o_room.o_desk.gameState; // 1. 计算本局得分 var scores = calculateRoundScores(gameState, roundResult); // 2. 更新玩家分数 updatePlayerScores(gameState, scores); // 3. ⚠️ 关键:第一局结算时扣房卡 console.log("[handleFirstRoundEnd] 第一局结算,扣除房卡"); try { mod_jinxianmahjong.import.deduct_roomcard(o_room); } catch (error) { console.error("[handleFirstRoundEnd] 扣除房卡失败:", error); } // 4. 发送结算结果给所有玩家 var resultMsg = { app: "youle", route: "jinxianmahjong", rpc: "round_end", data: { round: 1, scores: scores, winner: roundResult.winner, roomcardDeducted: true // 标记已扣房卡 } }; o_room.method.sendpack_toall(resultMsg); // 5. 准备下一局 prepareNextRound(o_room); } ``` ### 5.4 大局结束示例(保存成绩) ```javascript function handleGameEnd(o_room) { var gameState = o_room.o_desk.gameState; // 1. 确认游戏已完成所有局数 if (gameState.currentRound < gameState.totalRounds) { console.log("[handleGameEnd] 游戏尚未结束"); return; } // 2. 计算最终成绩排名 var finalRanking = calculateFinalRanking(gameState); // 3. 准备成绩数据 var gameinfoList = []; gameState.playersState.forEach(function(player) { if (player.playerid) { gameinfoList.push({ playerid: player.playerid, nickname: player.nickname, avatar: player.avatar, score: player.totalScore, rank: player.rank, winCount: player.statistics.winCount, loseCount: player.statistics.loseCount, zimoCount: player.statistics.zimoCount, dianpaoCount: player.statistics.dianpaoCount }); } }); // 4. 发送最终结算给所有玩家 var finalMsg = { app: "youle", route: "jinxianmahjong", rpc: "game_end", data: { ranking: finalRanking, totalRounds: gameState.totalRounds, timestamp: Date.now() } }; o_room.method.sendpack_toall(finalMsg); // 5. ⚠️ 关键:调用save_grade保存成绩 console.log("[handleGameEnd] 大局结束,保存游戏成绩"); try { mod_jinxianmahjong.import.save_grade( o_room, gameinfoList[0], // 第一个玩家 gameinfoList[1], // 第二个玩家(如果有更多,需要调整) false // 不是免费房间 ); } catch (error) { console.error("[handleGameEnd] 保存成绩失败:", error); } // 6. ⚠️ 注意:调用save_grade后不要再操作o_room console.log("[handleGameEnd] 成绩已保存,房间将由框架自动释放"); } ``` --- ## 6. 常见问题 ### Q1: 为什么check_player必须在每个RPC方法中调用? A: 原因有三个: 1. **安全性**:防止玩家冒充他人或操作不属于自己的房间 2. **稳定性**:确保房间和连接状态正常,避免访问无效对象 3. **规范性**:框架要求,验证失败时框架会自动处理错误响应 ### Q2: 为什么deduct_roomcard不在makewar时调用? A: 因为: 1. 开战后可能立即解散(玩家投票解散) 2. 只有真正开始游戏(第一局结束)才扣卡更公平 3. 避免网络问题导致误扣 4. 符合用户预期(玩了才扣费) ### Q3: save_grade调用后为什么不能再操作房间? A: 因为框架会在`save_grade`后自动释放房间资源: 1. 房间对象会被清理 2. 玩家连接会被断开 3. 内存会被释放 4. 继续操作会导致错误 ### Q4: 如何处理2-4人游戏的成绩保存? A: 根据实际玩家数量调整参数: ```javascript // 2人游戏 imp.save_grade(o_room, gameinfo1, gameinfo2, false); // 3-4人游戏:需要将多个玩家信息组合 var gameinfoArray = [player1Info, player2Info, player3Info, player4Info]; imp.save_grade(o_room, gameinfoArray[0], gameinfoArray, false); ``` ### Q5: 发包接口如何选择? A: 根据信息可见性选择: - **所有人都要知道**:`sendpack_toall`(如出牌、吃碰杠) - **只有特定玩家知道**:`sendpack_toseat`(如手牌、错误提示) - **除了操作者其他人知道**:`sendpack_toother`(如摸牌数量) ### Q6: parseInt为什么必须? A: 因为: 1. 网络传输的数据都是字符串 2. JavaScript的弱类型可能导致比较错误 3. 框架接口期望的是数值类型 4. 避免`"0" == 0`但`"0" === 0`为false的问题 ### Q7: 如何调试Import接口? A: 添加详细日志: ```javascript imp.deduct_roomcard = function(o_room) { console.log('[deduct_roomcard] 开始:', { roomcode: o_room.roomcode, currentRound: o_room.o_desk.gameState.currentRound }); try { var result = mod_jinxianmahjong.app.youle_room.export.deduct_roomcard(o_room); console.log('[deduct_roomcard] 成功:', result); return result; } catch (error) { console.error('[deduct_roomcard] 失败:', error); throw error; } }; ``` --- ## 7. 最佳实践 ### 7.1 参数提取和类型转换 ```javascript // ✅ 正确的参数提取方式 var agentid = pack.data.agentid; // 字符串,直接使用 var playerid = parseInt(pack.data.playerid); // 转换为数字 var gameid = pack.data.gameid; // 字符串,直接使用 var roomcode = parseInt(pack.data.roomcode); // 转换为数字 var seat = parseInt(pack.data.seat); // 转换为数字 // ❌ 错误的方式 var playerid = pack.data.playerid; // 可能是字符串"12345" var roomcode = pack.data.roomcode; // 可能是字符串"100001" ``` ### 7.2 错误处理 ```javascript // ✅ 完整的错误处理 var o_room = mod_jinxianmahjong.import.check_player(...); if (!o_room) { console.error("[RPC] 玩家验证失败"); return; // 验证失败直接返回 } try { // 业务逻辑 } catch (error) { console.error("[RPC] 处理失败:", error); // 发送错误消息给客户端 var errorMsg = { /* ... */ }; o_room.method.sendpack_toseat(errorMsg, seat); } ``` ### 7.3 调用时机检查 ```javascript // ✅ 第一局结算时扣房卡 if (gameState.currentRound === 1 && !gameState.roomcardDeducted) { mod_jinxianmahjong.import.deduct_roomcard(o_room); gameState.roomcardDeducted = true; // 标记已扣卡 } // ✅ 大局结束时保存成绩 if (gameState.currentRound >= gameState.totalRounds) { mod_jinxianmahjong.import.save_grade(...); } ``` --- ## 8. 下一步 阅读以下文档继续学习: - [03-RPC处理机制](./03-RPC处理机制.md) - 详细的RPC请求处理流程 - [04-游戏核心服务](../core/04-游戏核心服务.md) - 游戏逻辑实现 - [08-游戏流程概述](../architecture/08-游戏流程概述.md) - 完整游戏流程 --- **相关代码文件**: - `server/games2/jinxianmahjong/import.js` - `server/games2/jinxianmahjong/rpc/RpcHandler.js` - `docs/important/server/服务器子游戏开发要求.md`