32 KiB
Import接口详细说明
文档目标:详细说明子游戏如何调用友乐游戏框架提供的服务接口,包括4个核心接口的使用方法和调用时机。
📚 目录
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 实现方式
// 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方法开始时必须调用。负责验证:
- 玩家是否在指定房间
- 玩家是否在指定座位
- 连接是否有效
- 房间状态是否正常
接口定义
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)
{
roomcode: 100001,
roomtype: "1312111000",
seatlist: [...],
o_desk: {...}, // 游戏桌对象
method: { // 发包接口
sendpack_toall: function() {},
sendpack_toseat: function() {},
sendpack_toother: function() {}
}
}
失败时:返回null
验证内容
框架会检查以下内容:
- ✅ 房间是否存在
- ✅ 玩家是否在该房间
- ✅ 玩家是否在指定座位
- ✅ 连接ID是否匹配
- ✅ 房间状态是否正常
调用时机
必须在每个RPC方法开始时调用:
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;
// ...
};
为什么必须验证
安全性:
- 防止玩家冒充他人
- 防止操作不属于自己的房间
- 防止座位错误导致的数据混乱
稳定性:
- 确保房间存在且有效
- 确保连接状态正常
- 避免访问不存在的对象导致崩溃
实现示例
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 - 扣除房卡 ⭐⭐⭐⭐⭐
功能说明
扣除创建房间所需的房卡费用。非常重要:必须在第一小局结算时调用,而不是开战时。
接口定义
imp.deduct_roomcard = function(o_room) {
// 无返回值或返回扣卡结果
return void | Object;
}
参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
o_room |
Object | 房间对象 |
调用时机
⚠️ 关键时机:第一小局结算时调用
游戏开战(makewar)
↓
第1局开始
↓
玩家游戏
↓
第1局结束(有胡牌或流局)
↓
第1局结算 ← 在这里调用 deduct_roomcard() ⭐⭐⭐
↓
第2局开始
↓
...
为什么不在开战时扣卡:
- 开战后可能立即解散(投票解散)
- 只有真正开始游戏才扣卡,更公平
- 避免误扣(网络问题等)
典型调用场景
// 在第一小局结算函数中
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制时每人) |
实现示例
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 - 保存游戏成绩 ⭐⭐⭐⭐⭐
功能说明
保存玩家的游戏成绩到数据库。非常重要:
- 必须在大局游戏结束时调用
- 调用后框架会自动处理房间释放
- 这是游戏流程的最后一步
接口定义
imp.save_grade = function(o_room, o_gameinfo1, o_gameinfo2, freeroomflag) {
// 无返回值
return void;
}
参数说明
o_room - 房间对象
o_gameinfo1 - 第一组玩家成绩信息
{
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. 成绩数据完整性
- 确保所有玩家成绩都已计算
- 确保统计数据准确无误
- 成绩一旦保存无法修改
典型调用场景
// 大局结束处理函数
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
};
}
实现示例
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 - 完成游戏任务
功能说明
触发玩家完成游戏任务的奖励,如每日任务、成就系统等。这是一个可选接口,不是所有游戏都需要。
接口定义
imp.finish_gametask = function(agentid, o_player, taskid, finishamount) {
// 返回任务完成结果
return Object | null;
}
参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
agentid |
String | 代理商ID |
o_player |
Object | 玩家对象 |
taskid |
String | 任务ID |
finishamount |
Number | 完成数量(可选) |
调用时机
当玩家完成特定游戏任务时:
- 完成一局游戏
- 达成特定成就(如自摸、杠牌等)
- 完成每日任务
- 达到某个统计目标
使用示例
// 在游戏结算时检查任务
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
);
}
}
实现示例
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访问发包接口:
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 - 广播给所有玩家
用途:所有玩家都需要知道的信息
典型场景:
- 玩家出牌
- 玩家吃碰杠
- 玩家胡牌
- 游戏状态变化
示例:
// 玩家出牌,广播给所有人
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 - 发送给指定玩家
用途:只有特定玩家需要知道的信息
典型场景:
- 发送玩家手牌(只给该玩家)
- 发送操作提示(只给当前操作玩家)
- 发送错误消息(只给操作失败的玩家)
示例:
// 发送手牌给玩家(私密信息)
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 - 发送给其他玩家
用途:除指定玩家外的其他玩家需要知道的信息
典型场景:
- 显示其他玩家的手牌数量(不显示具体牌)
- 通知其他玩家某人的操作
- 隐藏操作者的私密信息
示例:
// 通知其他玩家:玩家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模板
/**
* 进贤麻将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接口)
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 第一局结算示例(扣房卡)
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 大局结束示例(保存成绩)
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: 原因有三个:
- 安全性:防止玩家冒充他人或操作不属于自己的房间
- 稳定性:确保房间和连接状态正常,避免访问无效对象
- 规范性:框架要求,验证失败时框架会自动处理错误响应
Q2: 为什么deduct_roomcard不在makewar时调用?
A: 因为:
- 开战后可能立即解散(玩家投票解散)
- 只有真正开始游戏(第一局结束)才扣卡更公平
- 避免网络问题导致误扣
- 符合用户预期(玩了才扣费)
Q3: save_grade调用后为什么不能再操作房间?
A: 因为框架会在save_grade后自动释放房间资源:
- 房间对象会被清理
- 玩家连接会被断开
- 内存会被释放
- 继续操作会导致错误
Q4: 如何处理2-4人游戏的成绩保存?
A: 根据实际玩家数量调整参数:
// 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: 因为:
- 网络传输的数据都是字符串
- JavaScript的弱类型可能导致比较错误
- 框架接口期望的是数值类型
- 避免
"0" == 0但"0" === 0为false的问题
Q7: 如何调试Import接口?
A: 添加详细日志:
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 参数提取和类型转换
// ✅ 正确的参数提取方式
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 错误处理
// ✅ 完整的错误处理
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 调用时机检查
// ✅ 第一局结算时扣房卡
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处理机制 - 详细的RPC请求处理流程
- 04-游戏核心服务 - 游戏逻辑实现
- 08-游戏流程概述 - 完整游戏流程
相关代码文件:
server/games2/jinxianmahjong/import.jsserver/games2/jinxianmahjong/rpc/RpcHandler.jsdocs/important/server/服务器子游戏开发要求.md