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 脚本篡改物品数据,可以通过以下方式实现:
- 使用 NPC 脚本发送邮件
- 立即修改邮件附件表
mail_attachments
中的物品数据 - 玩家接收邮件附件时,读取篡改后的物品数据到缓存,最终写入背包表
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;
}