Files
youlegames/codes/games/server/docs/guides/framework/02-Import接口说明.md
2026-02-04 23:47:45 +08:00

32 KiB
Raw Permalink Blame History

Import接口详细说明

文档目标详细说明子游戏如何调用友乐游戏框架提供的服务接口包括4个核心接口的使用方法和调用时机。

📚 目录

  1. Import接口概述
  2. 4个核心接口详解
  3. 调用时机和注意事项
  4. 发包接口详解
  5. 实现模板和示例
  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 实现方式

// 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. 房间状态是否正常

接口定义

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

验证内容

框架会检查以下内容:

  1. 房间是否存在
  2. 玩家是否在该房间
  3. 玩家是否在指定座位
  4. 连接ID是否匹配
  5. 房间状态是否正常

调用时机

必须在每个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局开始
    ↓
...

为什么不在开战时扣卡

  1. 开战后可能立即解散(投票解散)
  2. 只有真正开始游戏才扣卡,更公平
  3. 避免误扣(网络问题等)

典型调用场景

// 在第一小局结算函数中
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 - 保存游戏成绩

功能说明

保存玩家的游戏成绩到数据库。非常重要

  1. 必须在大局游戏结束时调用
  2. 调用后框架会自动处理房间释放
  3. 这是游戏流程的最后一步

接口定义

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: 原因有三个:

  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: 根据实际玩家数量调整参数:

// 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: 添加详细日志:

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. 下一步

阅读以下文档继续学习:


相关代码文件

  • server/games2/jinxianmahjong/import.js
  • server/games2/jinxianmahjong/rpc/RpcHandler.js
  • docs/important/server/服务器子游戏开发要求.md