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

洱海月 玩家交易中物品珍兽多入口操作拦截说明

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

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

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

×

问题现象

玩家把物品或珍兽放进面对面交易框后,虽然交易框里已经显示了该对象,但服务端里原始对象其实还留在背包或珍兽栏里。

如果其他入口没有做“交易态”拦截,客户端仍然可以继续通过这些操作去改它:

  1. 使用道具。
  2. 穿戴装备、时装、宠装。
  3. 摘除宝石、转移宝石、洗暗器。
  4. 放进仓库、珍兽仓库。
  5. 出售给商店、上架交易行。
  6. 珍兽加点、放生、卸宠装。

最终会出现两类高危结果:

  1. 交易框里看到的是旧对象。
  2. 实际背包里的对象已经被别的入口改动了。

这会直接导致交易显示和最终结算不一致。


根因分析

1. 交易对象没有离开原容器

面对面交易加物、加宠时,本项目不是把对象复制一份,而是直接把原对象引用塞进交易盒:

my_item_container:set_item(empty_index, item)
item_operator:lock_item(prop_bag_container, ei.from_index)
item:set_in_exchange(true)

宠物也是同样的处理:

my_pet_container:set_item(empty_index, pet)
item_operator:lock_item(pet_bag_container, from_index)
pet:set_in_exchange(true)

所以:

  1. 交易盒看到的是同一个 Lua 对象。
  2. 原背包/珍兽栏里也还是这个对象。
  3. 只要别的入口还能操作它,交易数据就会被“带着一起改掉”。

2. 很多入口只校验“对象存在”,没有校验“对象正在交易”

例如旧逻辑里,不少函数都是直接拿到对象就继续往下走:

local item = obj_me:get_prop_bag_container():get_item(use.bagIndex)
if item then
    ...
end

或者:

local pet_detail = container:get_pet_by_guid(guid)
pet_detail:manual_attr(set)

这种写法会漏掉“该对象虽然存在,但已经处于交易中”这个状态。

3. 底层移动函数缺少锁定兜底

旧版 item_operator:move_item() / item_operator:exchange_item() 在很多场景下不会主动拒绝锁定对象:

  1. 上层如果漏拦。
  2. 客户端如果重放异常包。
  3. 跨容器移动如果直接走底层。

就还可能把交易中的对象挪走。

4. is_lock()is_in_exchange() 语义不能完全混用

这个项目里:

  1. is_in_exchange() 表示“当前是否正在交易”。
  2. is_lock() 既可能表示交易临时锁,也可能表示普通手动锁。

所以修复时要分两层:

  1. 上层业务提示优先看 is_in_exchange(),保证文案准确。
  2. 底层移动函数用 is_lock() 兜底,保证安全。

修改目标

这次修复的目标很明确:

  1. 所有会改动交易中物品/珍兽状态的高频入口,都要先拦交易态。
  2. 普通锁定态仍然要继续拦。
  3. 底层移动/交换函数额外加兜底。
  4. 文案上区分:
    • 交易态:交易中的道具/珍兽无法...
    • 普通锁定:该道具/珍兽已锁定,无法...

修改文件

本次主要修改 2 个文件:

  1. services/scene/scenecore.lua
  2. lualib/item_operator.lua

本次修复依赖项目里已经存在的两个状态函数:

  1. item:is_in_exchange()
  2. pet_detail:is_in_exchange()

第一步:在 scenecore.lua 里收口成统一判定函数

文件:

services/scene/scenecore.lua

新增统一工具函数:
***付费内容***

为什么要先收口

如果每个入口都自己写一遍:

if item:is_in_exchange() then ...
if item:is_lock() then ...

后面会有两个问题:

  1. 文案容易不一致。
  2. 新入口很容易漏改。

统一之后,后续只需要在入口顶部写:

if not self:check_item_operable(...) then
    return
end

或者:

if not self:check_pet_operable(...) then
    return
end

即可。


第二步:所有上层入口先刷新锁状态,再判交易态

对于包裹物品相关入口,统一先做:

obj_me:refresh_locked_item_unlock_state(false)

例如 char_use_item()

function scenecore:char_use_item(me, use)
    local obj_me = self.objs[me]
    if not obj_me then
        return
    end
    obj_me:refresh_locked_item_unlock_state(false)
    ...
    local item = obj_me:get_prop_bag_container():get_item(use.bagIndex)
    if item then
        if not self:check_item_operable(obj_me, item, "交易中的道具无法使用", "该道具已锁定,无法使用") then
            send_use_item_result(define.USEITEM_RESULT.USEITEM_CANNT_USE)
            return
        end
        ...
    end
end

为什么这一步要保留

因为项目里锁定还带有延迟解锁逻辑。

先刷新再判断,可以避免:

  1. 服务端仍拿着旧锁状态。
  2. 结果把本来已经恢复可操作的物品误判成锁定。

第三步:把物品入口统一补齐

3.1 使用道具

文件:

services/scene/scenecore.lua

函数:

char_use_item

修改后的关键代码:

local item = obj_me:get_prop_bag_container():get_item(use.bagIndex)
if item then
    if not self:check_item_operable(obj_me, item, "交易中的道具无法使用", "该道具已锁定,无法使用") then
        send_use_item_result(define.USEITEM_RESULT.USEITEM_CANNT_USE)
        return
    end
    ...
end

3.2 穿装备 / 穿时装

函数:

  1. char_bind_equip
  2. char_use_bag_fashion_equip

修改后的关键代码:

if not self:check_item_operable(obj_me, item, "交易中的道具无法装备", "该道具已锁定,无法装备") then
    return false
end

和:

if not self:check_item_operable(obj_me, item, "交易中的道具无法装备", "该道具已锁定,无法装备") then
    return
end

3.3 摘除宝石 / 摘除时装宝石

函数:

  1. char_remove_gem
  2. char_remove_dress_gem

修改后的关键代码:

local equip = obj_me:get_prop_bag_container():get_item(equip_index)
local mat = obj_me:get_prop_bag_container():get_item(mat_index)
if not self:check_item_operable(obj_me, equip, "交易中的道具无法摘除宝石", "该道具已锁定,无法摘除宝石") then
    return
end
if not self:check_item_operable(obj_me, mat, "交易中的道具无法进行此操作", "该道具已锁定,无法进行此操作") then
    return
end

这里为什么要同时校验 equipmat

  1. 被操作装备不能处于交易中。
  2. 消耗材料也不能处于交易中。

否则虽然主装备没动,但材料仍然可能被异常扣掉。

3.4 宝石转移

函数:

char_displace_gem

修改后的关键代码:

local from_item = human:get_prop_bag_container():get_item(request.from_bag_index)
local to_item = human:get_prop_bag_container():get_item(request.to_bag_index)
if not self:check_item_operable(human, from_item, "交易中的道具无法转移宝石", "该道具已锁定,无法转移宝石") then
    return
end
if not self:check_item_operable(human, to_item, "交易中的道具无法转移宝石", "该道具已锁定,无法转移宝石") then
    return
end

这个入口的特点是会同时改两件装备,所以源和目标都要判。

3.5 洗暗器

函数:

char_adjust_dark

修改后的关键代码:

local anqi = container:get_item(adjust.bag_index)
if not self:check_item_operable(obj, anqi, "交易中的道具无法洗炼", "该道具已锁定,无法洗炼") then
    return
end

第四步:把“移动/出售/上架”类入口补齐

4.1 存入仓库

函数:

char_bank_add_item

修改后的关键代码:

local from_item = prop_bag_container:get_item(from_index)
if not from_item then
    human:notify_tips("道具不存在")
    return
end
if not self:check_item_operable(human, from_item, "交易中的道具无法存入仓库", "该道具已锁定,无法存入仓库") then
    return
end

4.2 出售给商店

函数:

char_shop_sell

修改后的关键代码:

function scenecore:char_shop_sell(who, sell)
    local human = self:get_obj_by_id(who)
    if not human then
        return
    end
    human:refresh_locked_item_unlock_state(false)
    local item = human:get_prop_bag_container():get_item(sell.bag_index)
    if not self:check_item_operable(human, item, "交易中的道具无法出售", "该道具已锁定,无法出售") then
        return
    end
    human:shop_sell(sell.bag_index)
end

4.3 交易行上架前检查

函数:

auction_sell_before_check

修改后的关键代码:

if auction_sell.type == 1 then
    local pet = pet_bag_container:get_item(index)
    if pet then
        if not self:check_pet_operable(human, pet, "交易中的珍兽无法上架", "该珍兽已锁定,无法上架") then
            return
        end
        ...
    end
elseif auction_sell.type == 2 then
    local item = prop_bag_container:get_item(index)
    if item then
        if not self:check_item_operable(human, item, "交易中的道具无法上架", "该道具已锁定,无法上架") then
            return
        end
        ...
    end
end

4.4 交易行真正移除背包对象时再次校验

函数:

auction_sell_after_remove

修改后的关键代码:

local pet = pet_bag_container:get_item(index)
if not self:check_pet_operable(human, pet, "交易中的珍兽无法上架", "该珍兽已锁定,无法上架") then
    return
end
pet_bag_container:erase_item(index)

和:

local item = prop_bag_container:get_item(index)
if not self:check_item_operable(human, item, "交易中的道具无法上架", "该道具已锁定,无法上架") then
    return
end
prop_bag_container:erase_item(index)

为什么交易行要前后两层都判

因为交易行是跨服务流程:

  1. before_check
  2. 再由外层逻辑真正执行 after_remove

中间如果状态变了,只在前面判一次不够稳。


第五步:把珍兽入口补齐

5.1 珍兽进仓 / 珍兽栏与仓库互换

函数:

char_pet_bank_add_pet

修改后的关键代码:

local pet = pet_bag_container:get_pet_by_guid(bap.pet_guid_from)
if not self:check_pet_operable(human, pet, "交易中的珍兽无法存入仓库", "该珍兽已锁定,无法进行此操作") then
    return
end

以及:

local pet_from = pet_bag_container:get_item(from)
if not self:check_pet_operable(human, pet_from, "交易中的珍兽无法与仓库互换", "该珍兽已锁定,无法进行此操作") then
    return
end

5.2 珍兽加点

函数:

char_set_pet_attrib

修改后的关键代码:

local pet_detail = container:get_pet_by_guid(guid)
if not self:check_pet_operable(obj, pet_detail, "交易中的珍兽无法加点", "该珍兽已锁定,无法加点") then
    return
end
pet_detail:manual_attr(set)

5.3 放生珍兽

函数:

char_manipulate_pet

只在 MANIPULATE_FREEPET 分支补拦截:

local pet = obj:get_pet_bag_container():get_pet_by_guid(guid)
if not self:check_pet_operable(obj, pet, "交易中的珍兽无法放生", "该珍兽已锁定,无法放生") then
    return
end
local result = obj:free_pet_to_nature(logparam, guid)

5.4 穿宠装

函数:

char_use_pet_equip

修改后的关键代码:

if not self:check_item_operable(obj, pet_equip, "交易中的道具无法装备到珍兽身上", "该道具已锁定,无法操作") then
    return
end

if not self:check_pet_operable(obj, pet_detail, "交易中的珍兽无法进行此操作", "该珍兽已锁定,无法操作") then
    return
end

这里同时要判:

  1. 宠装道具本身。
  2. 目标珍兽。

5.5 卸宠装

函数:

char_un_pet_equip

修改后的关键代码:

local pet_detail = pet_container:get_pet_by_guid(guid)
if not self:check_pet_operable(obj, pet_detail, "交易中的珍兽无法卸下装备", "该珍兽已锁定,无法卸下装备") then
    return
end

第六步:在 item_operator.lua 里加底层兜底

这一步是本次修复里非常重要的一层“保险丝”。

文件:

lualib/item_operator.lua

6.1 move_item() 拦锁定源对象

修改后的关键代码:

function item_operator:move_item(source_container,source_index,dest_container,dest_index,flag)
    local item_1 = source_container:get_item(source_index)
    if not item_1 then
        return define.ITEM_OPERATOR_ERROR.ITEMOE_SOUROPERATOR_EMPTY
    end
    if item_1.is_lock and item_1:is_lock() then
        return define.ITEM_OPERATOR_ERROR.ITEMOE_SOUROPERATOR_LOCK
    end
    ...
end

6.2 exchange_item() 同时拦源和目标

修改后的关键代码:

function item_operator:exchange_item(source_container, source_index, dest_container, dest_index, checkfalse)
    local source = source_container:get_item(source_index)
    if not checkfalse then
        if not source then
            return define.ITEM_OPERATOR_ERROR.ITEMOE_SOUROPERATOR_EMPTY
        end
    end
    if source and source.is_lock and source:is_lock() then
        return define.ITEM_OPERATOR_ERROR.ITEMOE_SOUROPERATOR_LOCK
    end
    ...
    local dest = dest_container:get_item(dest_index)
    if dest and dest.is_lock and dest:is_lock() then
        return define.ITEM_OPERATOR_ERROR.ITEMOE_DESTOPERATOR_LOCK
    end
    ...
end

为什么底层这里用 is_lock() 而不是只用 is_in_exchange()

因为这里的目标不是“给玩家准确提示”,而是“绝不允许锁定对象继续被移动”。

到底层时:

  1. 交易锁要拦。
  2. 其他普通锁定也应该拦。
  3. 这里优先保证安全性。

所以:

  1. 上层业务入口负责给准确提示。
  2. 底层移动函数负责兜底阻断。

两层职责不同,不冲突。


第七步:以后新增入口时,直接套这个模板

7.1 包裹物品模板

obj:refresh_locked_item_unlock_state(false)
local item = obj:get_prop_bag_container():get_item(bag_index)
if not self:check_item_operable(obj, item, "交易中的道具无法XXX", "该道具已锁定,无法XXX") then
    return
end

7.2 珍兽模板

local pet = obj:get_pet_bag_container():get_pet_by_guid(guid)
if not self:check_pet_operable(obj, pet, "交易中的珍兽无法XXX", "该珍兽已锁定,无法XXX") then
    return
end

7.3 如果一个入口会同时动多个对象

比如:

  1. 一个主装备 + 一个材料。
  2. 一个源装备 + 一个目标装备。
  3. 一个宠装 + 一个珍兽。

那就每个对象都单独判一次,不要只判主对象。


第八步:建议回归测试

8.1 交易中使用道具

步骤:

  1. 把道具放入交易框。
  2. char_use_item

预期:

  1. 服务端拒绝。
  2. 提示:交易中的道具无法使用

8.2 交易中穿装备 / 穿时装

步骤:

  1. 把装备放入交易框。
  2. 发穿戴协议。

预期:

  1. 服务端拒绝。
  2. 提示:交易中的道具无法装备

8.3 交易中摘除宝石 / 转移宝石

步骤:

  1. 把装备放入交易框。
  2. 尝试摘除或转移宝石。

预期:

  1. 服务端拒绝。
  2. 装备属性不变。
  3. 材料不会异常扣除。

8.4 交易中存仓 / 上架 / 商店出售

步骤:

  1. 把道具放入交易框。
  2. 分别尝试存仓、交易行上架、卖商店。

预期:

  1. 全部被拦。
  2. 不会把对象从背包里真正移走。

8.5 交易中珍兽加点 / 放生 / 进仓

步骤:

  1. 把珍兽放入交易框。
  2. 尝试加点、放生、放入珍兽仓库。

预期:

  1. 全部被拦。
  2. 珍兽数据不变。

8.6 交易中宠装穿脱

步骤:

  1. 把宠装道具放入交易框,尝试给珍兽穿。
  2. 把珍兽放入交易框,尝试卸宠装。

预期:

  1. 两边都被正确拒绝。
  2. 不会出现“交易框里是旧数据,实际珍兽已换装”的情况。

8.7 普通锁定验证

步骤:

  1. 不进入交易,只给道具或珍兽加普通锁。
  2. 尝试相同入口。

预期:

  1. 提示走“该道具/珍兽已锁定,无法...”。
  2. 不会误显示成“交易中的...”。

8.8 正常非交易流程验证

步骤:

  1. 用未交易、未锁定的普通对象走这些入口。

预期:

  1. 原功能保持不变。

总结

这次修复不是只补某一个按钮,而是把“交易态对象禁止被其他入口继续改动”这件事,补成了一套完整的结构:

  1. scenecore.lua 统一封装交易态/锁定态判定函数。
  2. 高风险业务入口全部先拦截。
  3. item_operator.lua 在底层继续兜底。

以后如果项目里再加新的“背包物品修改入口”或“珍兽修改入口”,直接复用:

  1. check_item_operable
  2. check_pet_operable

就不会再把同类漏洞放出来了。

付费看帖
剩余 8% 内容需要支付 66.00 金币 后可完整阅读
支持付费阅读,激励作者创作更好的作品。
您需要登录后才可以回帖 登录 | register

本版积分规则

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

GMT+8, 2026-6-24 07:04 , Processed in 0.065921 second(s), 25 queries .

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

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