加载中...

「NPC 技巧」利用邮件触发客户端缓存同步数据库


RO 双端的数据读写机制

在 rAthena 中,数据库的读写操作有特定的时间节点:

对于客户端:

  • 读取数据库固定是人物登录时的第一个动作
  • 写入数据库固定是人物登出时的最后一个动作
  • conf/map_athena.conf 中的 autosave_time: 可以控制客户端缓存数据与数据库同步的定时间隔

对于服务端:

  • NPC 脚本的 query_sql: 没有缓存,总是实时读写数据库
  • NPC 脚本的 query_sql_async: 纯粹开子线程异步读写数据库,效果和 query_sql 一样

由于上述机制,像背包表 inventory 和仓库表 storage 等数据,在玩家游戏过程中只是实时写入到客户端缓存,不会直接同步到数据库。

GF 每次维护的时候,GM 总是要求玩家主动提前下线,避免回档,究其原因就是让玩家通过登出行为触发缓存同步到数据库。

故而,当 NPC 脚本在玩家在线期间、直接修改了玩家的背包表 inventory 或仓库表 storage 时,若不同时修改客户端的缓存数据,那么当玩家登出时,缓存数据会覆写数据库,导致数据库的变更无效。

因此,大部分情况下,不建议 NPC 脚本直接与数据库交互:

  • 数据库读取可能会得到过期数据
  • 数据库写入的数据最终会被客户端缓存数据覆盖

在服务端触发客户端缓存同步

然而,某些 NPC 脚本命令,可以实时触发客户端实时同步缓存到数据库。

其中最简单的命令就是内部邮件 mail,它有两个触发同步的时机:

  • 发送邮件时,会实时写入邮件表 mail 和附件表 mail_attachments
  • 收取邮件附件时,会实时读取附件表 mail_attachments 到缓存,然后写入背包表 inventory

因此,如果想在 NPC 脚本篡改物品数据,可以通过以下方式实现:

  1. 使用 NPC 脚本发送邮件
  2. 立即修改邮件附件表 mail_attachments 中的物品数据
  3. 玩家接收邮件附件时,读取篡改后的物品数据到缓存,最终写入背包表 inventory

当 rAthena 没有提供接口修改物品某些参数时,可以使用这种方法直接修改数据库。

例如,在早期的 rAthena 版本中,不存在 NPC 命令 getitem4,此时便可利用 mail 机制把包含 “评价等级” enchantgrade 的道具送入玩家背包。

示例代码

// 通过邮件发送装备给当前玩家
//============================================================ 
// - param: .@equip_id 装备 ID
// - param: .@email_sender$ 邮件发送人名称(后台无校验,任意即可)
// - param: .@email_revcer 邮件接收角色 ID(默认当前玩家)
// - param: .@email_title$ 邮件标题
// - param: .@email_body$ 邮件正文
// - param: .@refine_lv 精炼等级,范围 [0, 20]
// - param: .@grade_lv 评价等级,范围 [0, 4]
// - param: .@card_id_0 卡槽 0 的卡片 ID
// - param: .@card_id_1 卡槽 1 的卡片 ID
// - param: .@card_id_2 卡槽 2 的卡片 ID
// - param: .@card_id_3 卡槽 3 的卡片 ID
// - param: .@roa_idx_0 词条 0 的索引值
// - param: .@roa_val_0 词条 0 的效果值
// - param: .@roa_idx_1 词条 1 的索引值
// - param: .@roa_val_1 词条 1 的效果值
// - param: .@roa_idx_2 词条 2 的索引值
// - param: .@roa_val_2 词条 2 的效果值
// - param: .@roa_idx_3 词条 3 的索引值
// - param: .@roa_val_3 词条 3 的效果值
// - param: .@roa_idx_4 词条 4 的索引值
// - param: .@roa_val_4 词条 4 的效果值
// - return: .@is_ok 是否成功
function    script    F_SEND_EQUIP_TO_EMAIL    {
    .@equip_id = getarg(0);
    .@email_sender$ = getarg(1, "System");
    .@email_revcer = getarg(2, callfunc("F_CHAR_ID"));
    .@email_title$ = getarg(3, "系统邮件");
    .@email_body$ = getarg(4, "");
    .@refine_lv = getarg(5, 0);
    .@grade_lv = getarg(6, 0);
    .@card_id_0 = getarg(7, 0);
    .@card_id_1 = getarg(8, 0);
    .@card_id_2 = getarg(9, 0);
    .@card_id_3 = getarg(10, 0);
    .@roa_idx_0 = getarg(11, 0);
    .@roa_val_0 = getarg(12, 0);
    .@roa_idx_1 = getarg(13, 0);
    .@roa_val_1 = getarg(14, 0);
    .@roa_idx_2 = getarg(15, 0);
    .@roa_val_2 = getarg(16, 0);
    .@roa_idx_3 = getarg(17, 0);
    .@roa_val_3 = getarg(18, 0);
    .@roa_idx_4 = getarg(19, 0);
    .@roa_val_4 = getarg(20, 0);

    .@is_ok = false;
    .@refine_lv = callfunc("F_IN_RANGE", 0, .@refine_lv, 20);
    .@grade_lv = callfunc("F_IN_RANGE", 0, .@grade_lv, 4);

    // 发送邮件(含有附件装备 .@equip_id)
    .@zeny = 0;
    setarray .@mail_item[0], .@equip_id;     // 附件物品清单
    setarray .@mail_amount[0], 1;            // 附件物品数量
    mail .@email_revcer, .@email_sender$, .@email_title$, .@email_body$, .@zeny, .@mail_item, .@mail_amount;

    .@interval = 200;    // ms
    .@cnt = 0;
    .@MAX_RETRY = 50;
    while (.@cnt < .@MAX_RETRY) {
        sleep2 .@interval;    // 等待邮件数据入库
        .@cnt += 1;

        // 获取最后一封邮件的 ID(含有附件装备 .@equip_id)
        .@sql$ = "SELECT a.`id`, b.`index` FROM `mail` AS a, `mail_attachments` AS b WHERE " + 
                    " a.`send_id` = 0 AND " + 
                    " a.`dest_id` = " + .@email_revcer + " AND " + 
                    " a.`id` = b.`id` AND " + 
                    " b.`nameid` = " + .@equip_id + 
                    " ORDER BY a.`id` DESC LIMIT 1";
        .@line_num = query_sql(.@sql$, .@ids, .@indexs);
        if (.@line_num > 0) {
            break;
        }
    }
    if (.@cnt >= .@MAX_RETRY) {
        return .@is_ok;
    }

    // 篡改附件的装备数据
    .@sql$ = "UPDATE `mail_attachments` SET " + 
                "  `refine` = " + .@refine_lv + 
                ", `enchantgrade` = " + .@grade_lv + 
                ", `card0` = " + .@card_id_0 + ", `card1` = " + .@card_id_1 + 
                ", `card2` = " + .@card_id_2 + ", `card3` = " + .@card_id_3 + 
                ", `option_id0` = " + .@roa_idx_0 + ", `option_val0` = " + .@roa_val_0 + 
                ", `option_id1` = " + .@roa_idx_1 + ", `option_val1` = " + .@roa_val_1 + 
                ", `option_id2` = " + .@roa_idx_2 + ", `option_val2` = " + .@roa_val_2 + 
                ", `option_id3` = " + .@roa_idx_3 + ", `option_val3` = " + .@roa_val_3 + 
                ", `option_id4` = " + .@roa_idx_4 + ", `option_val4` = " + .@roa_val_4 + 
            " WHERE " + 
                "`id` = " + .@ids[0] + " AND " + 
                "`index` = " + .@indexs[0]
            ;
    .@rst = query_sql(.@sql$);
    .@is_ok = (.@rst == 0 ? true : false);
    return .@is_ok;
}

备用视频源:youtube

文章作者: EXP
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 EXP !
 上一篇
如何为英语视频添加双语字幕 如何为英语视频添加双语字幕
本文展示了从无到有为英语视频制作中英双语字幕的全过程,包括英文字幕的识别、字幕翻译、以及最终的双语字幕的生成 ...
2024-07-04
下一篇 
「NPC 技巧」随机打乱数组 —— 如何数组传参? 「NPC 技巧」随机打乱数组 —— 如何数组传参?
在开发 rAthena 的 NPC 脚本的时候,我们有时希望随机打乱一个数组,在其他高级语言中,这是一件很容易的事情,但在 NPC 脚本中却一波三折 ...
2024-07-02
  目录