找回密码
 register
搜索
查看: 93|回复: 0

洱海月 交易中已经锁定的物品还能被丢弃 发现异常行为终止交易

[复制链接]
  • 打卡等级:本地老炮
  • 打卡总天数:533
  • 打卡月天数:22
  • 打卡总奖励:530
  • 最近打卡:2026-06-24 01:45:59
Waylee 发表于 2026-4-1 15:09 | 显示全部楼层 |阅读模式 | Google Chrome | Windows 10

马上注册,查看网站隐藏内容!!

您需要 登录 才可以下载或查看,没有账号?register

×

问题现象

已将物品放入交易盒后,客户端如果利用封包重放,继续发送丢弃该背包物品的请求,服务端原逻辑会直接把背包中的物品删除。

这会导致两边状态不一致:

  1. 交易盒中还保留着该物品的引用。
  2. 背包中的真实物品已经被删掉。
  3. 最终点击确认交易时,服务端才在结算阶段报错 "没有该道具"

更安全的处理方式应该是:

  1. 发现该物品正在交易中。
  2. 先清理交易状态。
  3. 再继续执行丢弃,避免交易盒残留脏数据。

根因分析

交易时,物品加入交易盒的核心逻辑在:

  • 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

所以实际上服务端只做了两件事:

  1. 交易盒保存了该物品对象引用。
  2. 背包中的物品被加了 lock 标记。

但是没有真正记录“这件物品正在交易”。

而丢弃入口 char_discard_item 又没有判断交易状态,最终导致重放丢弃包时,可以直接删除背包里的物品。


修改目标

本次修复目标不是等到交易确认时再兜底报错,而是把异常处理前移:

  1. 物品要能正确记录是否处于交易中。
  2. 丢弃入口收到请求时,优先判断该物品是不是交易物品。
  3. 如果是交易物品,先取消交易并清理交易盒。
  4. 清理完成后,再继续丢弃。

这样可以保证:

  1. 不会留下交易盒引用脏数据。
  2. 不会等到 char_exchange_ok_III 结算阶段才暴露异常。

修改文件

本次修改了两个文件:

  1. lualib/item.lua
  2. 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 这个函数做了什么

它做的事情很简单:

  1. 拿到自己的交易盒。
  2. 找到对方交易对象。
  3. 调用 my_exchange_box:reset() 清理自己的交易状态。
  4. 如果对方交易盒仍然指向自己,也一起 reset()
  5. 给双方发送 GCExchangeCancel

2.2 为什么不能只删背包物品

因为背包里的物品不是唯一状态来源,交易盒里也保存了该物品引用。

如果只删背包,不清理交易盒,那么:

  1. 交易盒里还是旧物品。
  2. 对方客户端仍认为交易物品存在。
  3. 最终结算时才报 "没有该道具"

因此必须先清交易状态,再处理丢弃。


第三步:改写 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

这里的顺序非常重要:

  1. 先判断是不是交易物品。
  2. 是的话先取消交易。
  3. 取消交易后重新从背包拿一次物品对象。

为什么要重新取一次 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

这也是为什么“取消交易”能够彻底修掉这个问题:

  1. 解锁物品。
  2. 清掉交易状态。
  3. 清空交易盒。
  4. 重置交易双方关系。

这样丢弃请求后续再执行时,数据已经恢复到一致状态。


第六步:修复后的完整行为

修复前:

  1. 物品加入交易盒。
  2. 背包还保留真实物品。
  3. 重放丢弃包时,背包物品被删掉。
  4. 交易盒引用未清理。
  5. 最终确认交易时报 "没有该道具"

修复后:

  1. 物品加入交易盒时,item:set_in_exchange(true) 真正生效。
  2. 收到丢弃请求时,先判断 item:is_in_exchange()
  3. 如果正在交易,立刻取消交易并重置交易盒。
  4. 重置后背包物品恢复到可操作状态。
  5. 再继续执行丢弃。
  6. 不会残留交易盒脏引用。

第七步:为什么不直接在 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

但这只是最后一道防线,不是最佳修复点。

原因有两个:

  1. 这个阶段才发现问题,客户端表现已经异常。
  2. 交易盒已经处于脏状态,修复成本更高。

所以更合理的做法是把防御前移到 char_discard_item


第八步:建议测试项

8.1 正常交易取消

步骤:

  1. 双方发起交易。
  2. 放入一件物品。
  3. 正常点击取消交易。

预期:

  1. 物品仍在背包。
  2. 物品不再处于交易状态。
  3. 物品锁定被解除。

8.2 交易中重放丢弃

步骤:

  1. 双方发起交易。
  2. 放入一件物品。
  3. 重放丢弃该背包格子的封包。

预期:

  1. 双方交易立即取消。
  2. 背包中的该物品被正常丢弃。
  3. 不再出现 "没有该道具"

8.3 多个交易物品

步骤:

  1. 交易盒里放入多个物品。
  2. 对其中一个做丢弃重放。

预期:

  1. 整个交易被取消。
  2. 所有交易中的物品都被重置回正常状态。
  3. 被丢弃的目标物品正常消失。

8.4 普通锁定物品

步骤:

  1. 选一个普通锁定物品。
  2. 发丢弃请求。

预期:

  1. 提示 "该道具已锁定,无法丢弃"
  2. 不会被删除。

8.5 不可丢弃物品

步骤:

  1. 选一个规则上禁止丢弃的物品。
  2. 发丢弃请求。

预期:

  1. 提示 "xxx不能丢弃"
  2. 物品不删除。

第九步:本次修改的实际代码位置

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

第十步:总结

这次修复的核心不是“禁止客户端发异常包”,而是“即便客户端发了异常包,服务端状态仍然保持一致”。

最终达到的效果是:

  1. 交易物品有了明确服务端状态。
  2. 丢弃请求优先识别交易状态。
  3. 交易中的物品在被丢弃前,会先打断交易并清理交易盒。
  4. 不再把异常留到交易结算阶段暴露。

这是一种更安全的服务端防御方式,适合处理封包重放、客户端绕流程操作这类问题。

您需要登录后才可以回帖 登录 | register

本版积分规则

QQ|雪舞知识库 ( 浙ICP备15015590号-1 | 萌ICP备20232229号|浙公网安备33048102000118号 )|天天打卡

GMT+8, 2026-6-24 05:31 , Processed in 0.066543 second(s), 25 queries .

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表