问题现象
已将物品放入交易盒后,客户端如果利用封包重放,继续发送丢弃该背包物品的请求,服务端原逻辑会直接把背包中的物品删除。
这会导致两边状态不一致:
- 交易盒中还保留着该物品的引用。
- 背包中的真实物品已经被删掉。
- 最终点击确认交易时,服务端才在结算阶段报错
"没有该道具"。
更安全的处理方式应该是:
- 发现该物品正在交易中。
- 先清理交易状态。
- 再继续执行丢弃,避免交易盒残留脏数据。
根因分析
交易时,物品加入交易盒的核心逻辑在:
services/scene/scenecore.lua
char_exchange_sync_item_II
原有逻辑中,物品加入交易盒时会执行:
my_item_container:set_item(empty_index, item)
item_operator:lock_item(obj_me:get_prop_bag_container(), ei.from_index)
item:set_in_exchange(true)
但是 item:set_in_exchange(true) 对应的实现原来是空的:
function item:set_in_exchange()
end
所以实际上服务端只做了两件事:
- 交易盒保存了该物品对象引用。
- 背包中的物品被加了
lock 标记。
但是没有真正记录“这件物品正在交易”。
而丢弃入口 char_discard_item 又没有判断交易状态,最终导致重放丢弃包时,可以直接删除背包里的物品。
修改目标
本次修复目标不是等到交易确认时再兜底报错,而是把异常处理前移:
- 物品要能正确记录是否处于交易中。
- 丢弃入口收到请求时,优先判断该物品是不是交易物品。
- 如果是交易物品,先取消交易并清理交易盒。
- 清理完成后,再继续丢弃。
这样可以保证:
- 不会留下交易盒引用脏数据。
- 不会等到
char_exchange_ok_III 结算阶段才暴露异常。
修改文件
本次修改了两个文件:
lualib/item.lua
services/scene/scenecore.lua
第一步:给 item 增加真正的交易状态
文件:lualib/item.lua
1.1 在构造函数中初始化 in_exchange
原构造函数中没有这个字段:
function item:ctor()
self.guid = guid_cls.new()
self.item_index = -1
self.rule = 0
self.status = 0
self.unknow_4 = {
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0}
self.unknow_44 = 0
self.unknow_45 = 0
self.record_data = {}
self.equip_data = equip_data.new()
self.store_map_data = store_map_data.new()
self.material_data = material_data.new()
self.gem_data = gem_data.new()
self.pet_equip_data = pet_equip_data.new()
self.task_data = task_data.new()
end
修改后:
function item:ctor()
self.guid = guid_cls.new()
self.item_index = -1
self.rule = 0
self.status = 0
self.in_exchange = false
self.unknow_4 = {
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0}
self.unknow_44 = 0
self.unknow_45 = 0
self.record_data = {}
self.equip_data = equip_data.new()
self.store_map_data = store_map_data.new()
self.material_data = material_data.new()
self.gem_data = gem_data.new()
self.pet_equip_data = pet_equip_data.new()
self.task_data = task_data.new()
end
1.2 实现 is_in_exchange() 和 set_in_exchange()
原代码:
function item:set_in_exchange()
end
修改后:
function item:is_in_exchange()
return self.in_exchange == true
end
function item:set_in_exchange(in_exchange)
self.in_exchange = in_exchange and true or false
end
1.3 为什么要这么改
因为交易流程里本来就已经在调用:
item:set_in_exchange(true)
item:set_in_exchange(false)
只是之前这个函数为空,导致交易状态从未真正保存下来。
补上这个状态之后,后续丢弃逻辑就能识别“该物品是否正在交易”。
第二步:新增“因丢弃而取消交易”的辅助函数
文件:services/scene/scenecore.lua
新增函数:
function scenecore:cancel_exchange_by_item_discard(obj_me)
if not obj_me then
return
end
local my_exchange_box = obj_me:get_exchange_box()
if not my_exchange_box then
return
end
local msg = packet_def.GCExchangeCancel.new()
local obj_tar
local dest = my_exchange_box:get_dest()
if dest and dest ~= define.INVAILD_ID then
obj_tar = self:get_obj_by_id(dest)
end
my_exchange_box:reset()
self:send2client(obj_me, msg)
if obj_tar then
local tar_exchange_box = obj_tar:get_exchange_box()
if tar_exchange_box and tar_exchange_box:get_dest() == obj_me:get_obj_id() then
tar_exchange_box:reset()
self:send2client(obj_tar, msg)
end
end
end
2.1 这个函数做了什么
它做的事情很简单:
- 拿到自己的交易盒。
- 找到对方交易对象。
- 调用
my_exchange_box:reset() 清理自己的交易状态。
- 如果对方交易盒仍然指向自己,也一起
reset()。
- 给双方发送
GCExchangeCancel。
2.2 为什么不能只删背包物品
因为背包里的物品不是唯一状态来源,交易盒里也保存了该物品引用。
如果只删背包,不清理交易盒,那么:
- 交易盒里还是旧物品。
- 对方客户端仍认为交易物品存在。
- 最终结算时才报
"没有该道具"。
因此必须先清交易状态,再处理丢弃。
第三步:改写 char_discard_item
文件:services/scene/scenecore.lua
3.1 原逻辑
原逻辑只做了“是否允许丢弃”的判断,没有处理交易状态:
function scenecore:char_discard_item(who, discard)
local obj = self.objs[who]
if discard.opt == packet_def.CGDiscardItem.OPT.FromBag then
local container = obj:get_prop_bag_container()
local item = container:get_item(discard.bag_index)
if item then
local can_discard = item:can_discard()
if not can_discard then
obj:notify_tips(string.format("%s不能丢弃", item:get_name()))
return
end
local msg = packet_def.GCDiscardItemResult.new()
msg.item_index = item:get_index()
msg.opt = discard.opt
msg.bag_index = discard.bag_index
if item:is_ruler(define.ITEM_RULER_LIST.IRL_DISCARD) then
local logparam = { reason = "丢弃道具"}
local ret = human_item_logic:erase_item_by_bag_index(logparam, obj, discard.bag_index)
if ret then
end
msg.result = define.DISCARDITEM_RESULT.DISCARDITEM_SUCCESS
else
msg.result = define.DISCARDITEM_RESULT.DISCARDITEM_FAIL
end
self:send2client(obj, msg)
end
end
end
3.2 修改后逻辑
修改后:
function scenecore:char_discard_item(who, discard)
local obj = self.objs[who]
if not obj then
return
end
if discard.opt == packet_def.CGDiscardItem.OPT.FromBag then
obj:refresh_locked_item_unlock_state(false)
local container = obj:get_prop_bag_container()
local item = container:get_item(discard.bag_index)
if item then
if item.is_in_exchange and item:is_in_exchange() then
self:cancel_exchange_by_item_discard(obj)
item = container:get_item(discard.bag_index)
if not item then
return
end
elseif item:is_lock() then
obj:notify_tips("该道具已锁定,无法丢弃")
return
end
local can_discard = item:can_discard()
if not can_discard then
obj:notify_tips(string.format("%s不能丢弃", item:get_name()))
return
end
local msg = packet_def.GCDiscardItemResult.new()
msg.item_index = item:get_index()
msg.opt = discard.opt
msg.bag_index = discard.bag_index
if item:is_ruler(define.ITEM_RULER_LIST.IRL_DISCARD) then
local logparam = { reason = "丢弃道具"}
local ret = human_item_logic:erase_item_by_bag_index(logparam, obj, discard.bag_index)
if ret then
end
msg.result = define.DISCARDITEM_RESULT.DISCARDITEM_SUCCESS
else
msg.result = define.DISCARDITEM_RESULT.DISCARDITEM_FAIL
end
self:send2client(obj, msg)
end
end
end
第四步:重点逻辑解释
4.1 先判空
local obj = self.objs[who]
if not obj then
return
end
这一步是基础保护,避免场景对象不存在时继续执行。
4.2 先刷新锁状态
obj:refresh_locked_item_unlock_state(false)
这样可以避免把正常的“解锁倒计时物品”与“交易中的锁定物品”混淆。
4.3 优先判断交易状态
if item.is_in_exchange and item:is_in_exchange() then
self:cancel_exchange_by_item_discard(obj)
item = container:get_item(discard.bag_index)
if not item then
return
end
这里的顺序非常重要:
- 先判断是不是交易物品。
- 是的话先取消交易。
- 取消交易后重新从背包拿一次物品对象。
为什么要重新取一次 item?
因为 cancel_exchange_by_item_discard() 内部会触发交易盒 reset(),而 reset() 会执行:
item_operator:unlock_item(prop_bag, bag_index)
item:set_in_exchange(false)
也就是说,交易状态和锁状态都会被修改。
重新读取一次物品对象,可以保证后续使用的是最新状态。
4.4 普通锁定物品直接拒绝丢弃
elseif item:is_lock() then
obj:notify_tips("该道具已锁定,无法丢弃")
return
end
这一步避免了把“玩家自己正常锁定的物品”误删。
4.5 最后才走正常丢弃
local ret = human_item_logic:erase_item_by_bag_index(logparam, obj, discard.bag_index)
也就是说,丢弃动作本身没有变,变的是执行前的状态清理顺序。
第五步:交易状态是如何被清掉的
交易盒重置逻辑在 lualib/exchange_box.lua:
function exchange_box:reset()
if self.my_self then
local prop_bag = self.my_self:get_prop_bag_container()
local pet_bag = self.my_self:get_pet_bag_container()
for i = 0, self.item_container:get_size() - 1 do
local item = self.item_container:get_item(i)
if item then
local bag_index = prop_bag:get_index_by_guid(item:get_guid())
if bag_index then
item_operator:unlock_item(prop_bag, bag_index)
item:set_in_exchange(false)
end
end
local pet = self.pet_container:get_item(i)
if pet then
local bag_index = pet_bag:get_index_by_pet_guid(pet:get_guid())
if bag_index then
item_operator:unlock_item(pet_bag, bag_index)
pet:set_in_exchange(false)
end
end
end
end
self.serial = 1
self.money = 0
self.can_conform = false
self.dest = define.INVAILD_ID
self.status = self.STATUS.EXCHANGE_NONE
self.item_container:clean_up()
self.pet_container:clean_up()
self:unlock()
end
这也是为什么“取消交易”能够彻底修掉这个问题:
- 解锁物品。
- 清掉交易状态。
- 清空交易盒。
- 重置交易双方关系。
这样丢弃请求后续再执行时,数据已经恢复到一致状态。
第六步:修复后的完整行为
修复前:
- 物品加入交易盒。
- 背包还保留真实物品。
- 重放丢弃包时,背包物品被删掉。
- 交易盒引用未清理。
- 最终确认交易时报
"没有该道具"。
修复后:
- 物品加入交易盒时,
item:set_in_exchange(true) 真正生效。
- 收到丢弃请求时,先判断
item:is_in_exchange()。
- 如果正在交易,立刻取消交易并重置交易盒。
- 重置后背包物品恢复到可操作状态。
- 再继续执行丢弃。
- 不会残留交易盒脏引用。
第七步:为什么不直接在 char_exchange_ok_III 兜底
char_exchange_ok_III 里本来就有这类判断:
local bag_index = my_prop_bag_container:get_index_by_guid(item:get_guid())
if not bag_index then
obj_me:notify_tips("没有该道具。")
my_exchange_box:reset()
tar_exchange_box:reset()
return
end
但这只是最后一道防线,不是最佳修复点。
原因有两个:
- 这个阶段才发现问题,客户端表现已经异常。
- 交易盒已经处于脏状态,修复成本更高。
所以更合理的做法是把防御前移到 char_discard_item。
第八步:建议测试项
8.1 正常交易取消
步骤:
- 双方发起交易。
- 放入一件物品。
- 正常点击取消交易。
预期:
- 物品仍在背包。
- 物品不再处于交易状态。
- 物品锁定被解除。
8.2 交易中重放丢弃
步骤:
- 双方发起交易。
- 放入一件物品。
- 重放丢弃该背包格子的封包。
预期:
- 双方交易立即取消。
- 背包中的该物品被正常丢弃。
- 不再出现
"没有该道具"。
8.3 多个交易物品
步骤:
- 交易盒里放入多个物品。
- 对其中一个做丢弃重放。
预期:
- 整个交易被取消。
- 所有交易中的物品都被重置回正常状态。
- 被丢弃的目标物品正常消失。
8.4 普通锁定物品
步骤:
- 选一个普通锁定物品。
- 发丢弃请求。
预期:
- 提示
"该道具已锁定,无法丢弃"。
- 不会被删除。
8.5 不可丢弃物品
步骤:
- 选一个规则上禁止丢弃的物品。
- 发丢弃请求。
预期:
- 提示
"xxx不能丢弃"。
- 物品不删除。
第九步:本次修改的实际代码位置
item.lua
新增:
self.in_exchange = false
新增:
function item:is_in_exchange()
return self.in_exchange == true
end
function item:set_in_exchange(in_exchange)
self.in_exchange = in_exchange and true or false
end
scenecore.lua
新增:
function scenecore:cancel_exchange_by_item_discard(obj_me)
...
end
改造:
function scenecore:char_discard_item(who, discard)
...
end
第十步:总结
这次修复的核心不是“禁止客户端发异常包”,而是“即便客户端发了异常包,服务端状态仍然保持一致”。
最终达到的效果是:
- 交易物品有了明确服务端状态。
- 丢弃请求优先识别交易状态。
- 交易中的物品在被丢弃前,会先打断交易并清理交易盒。
- 不再把异常留到交易结算阶段暴露。
这是一种更安全的服务端防御方式,适合处理封包重放、客户端绕流程操作这类问题。