问题现象
玩家把物品或珍兽放进面对面交易框后,虽然交易框里已经显示了该对象,但服务端里原始对象其实还留在背包或珍兽栏里。
如果其他入口没有做“交易态”拦截,客户端仍然可以继续通过这些操作去改它:
- 使用道具。
- 穿戴装备、时装、宠装。
- 摘除宝石、转移宝石、洗暗器。
- 放进仓库、珍兽仓库。
- 出售给商店、上架交易行。
- 珍兽加点、放生、卸宠装。
最终会出现两类高危结果:
- 交易框里看到的是旧对象。
- 实际背包里的对象已经被别的入口改动了。
这会直接导致交易显示和最终结算不一致。
根因分析
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)
所以:
- 交易盒看到的是同一个 Lua 对象。
- 原背包/珍兽栏里也还是这个对象。
- 只要别的入口还能操作它,交易数据就会被“带着一起改掉”。
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() 在很多场景下不会主动拒绝锁定对象:
- 上层如果漏拦。
- 客户端如果重放异常包。
- 跨容器移动如果直接走底层。
就还可能把交易中的对象挪走。
4. is_lock() 和 is_in_exchange() 语义不能完全混用
这个项目里:
is_in_exchange() 表示“当前是否正在交易”。
is_lock() 既可能表示交易临时锁,也可能表示普通手动锁。
所以修复时要分两层:
- 上层业务提示优先看
is_in_exchange(),保证文案准确。
- 底层移动函数用
is_lock() 兜底,保证安全。
修改目标
这次修复的目标很明确:
- 所有会改动交易中物品/珍兽状态的高频入口,都要先拦交易态。
- 普通锁定态仍然要继续拦。
- 底层移动/交换函数额外加兜底。
- 文案上区分:
- 交易态:
交易中的道具/珍兽无法...
- 普通锁定:
该道具/珍兽已锁定,无法...
修改文件
本次主要修改 2 个文件:
services/scene/scenecore.lua
lualib/item_operator.lua
本次修复依赖项目里已经存在的两个状态函数:
item:is_in_exchange()
pet_detail:is_in_exchange()
第一步:在 scenecore.lua 里收口成统一判定函数
文件:
services/scene/scenecore.lua
新增统一工具函数:
***付费内容***
为什么要先收口
如果每个入口都自己写一遍:
if item:is_in_exchange() then ...
if item:is_lock() then ...
后面会有两个问题:
- 文案容易不一致。
- 新入口很容易漏改。
统一之后,后续只需要在入口顶部写:
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
为什么这一步要保留
因为项目里锁定还带有延迟解锁逻辑。
先刷新再判断,可以避免:
- 服务端仍拿着旧锁状态。
- 结果把本来已经恢复可操作的物品误判成锁定。
第三步:把物品入口统一补齐
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 穿装备 / 穿时装
函数:
char_bind_equip
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 摘除宝石 / 摘除时装宝石
函数:
char_remove_gem
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
这里为什么要同时校验 equip 和 mat:
- 被操作装备不能处于交易中。
- 消耗材料也不能处于交易中。
否则虽然主装备没动,但材料仍然可能被异常扣掉。
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)
为什么交易行要前后两层都判
因为交易行是跨服务流程:
- 先
before_check。
- 再由外层逻辑真正执行
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
这里同时要判:
- 宠装道具本身。
- 目标珍兽。
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()
因为这里的目标不是“给玩家准确提示”,而是“绝不允许锁定对象继续被移动”。
到底层时:
- 交易锁要拦。
- 其他普通锁定也应该拦。
- 这里优先保证安全性。
所以:
- 上层业务入口负责给准确提示。
- 底层移动函数负责兜底阻断。
两层职责不同,不冲突。
第七步:以后新增入口时,直接套这个模板
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 如果一个入口会同时动多个对象
比如:
- 一个主装备 + 一个材料。
- 一个源装备 + 一个目标装备。
- 一个宠装 + 一个珍兽。
那就每个对象都单独判一次,不要只判主对象。
第八步:建议回归测试
8.1 交易中使用道具
步骤:
- 把道具放入交易框。
- 发
char_use_item。
预期:
- 服务端拒绝。
- 提示:
交易中的道具无法使用
8.2 交易中穿装备 / 穿时装
步骤:
- 把装备放入交易框。
- 发穿戴协议。
预期:
- 服务端拒绝。
- 提示:
交易中的道具无法装备
8.3 交易中摘除宝石 / 转移宝石
步骤:
- 把装备放入交易框。
- 尝试摘除或转移宝石。
预期:
- 服务端拒绝。
- 装备属性不变。
- 材料不会异常扣除。
8.4 交易中存仓 / 上架 / 商店出售
步骤:
- 把道具放入交易框。
- 分别尝试存仓、交易行上架、卖商店。
预期:
- 全部被拦。
- 不会把对象从背包里真正移走。
8.5 交易中珍兽加点 / 放生 / 进仓
步骤:
- 把珍兽放入交易框。
- 尝试加点、放生、放入珍兽仓库。
预期:
- 全部被拦。
- 珍兽数据不变。
8.6 交易中宠装穿脱
步骤:
- 把宠装道具放入交易框,尝试给珍兽穿。
- 把珍兽放入交易框,尝试卸宠装。
预期:
- 两边都被正确拒绝。
- 不会出现“交易框里是旧数据,实际珍兽已换装”的情况。
8.7 普通锁定验证
步骤:
- 不进入交易,只给道具或珍兽加普通锁。
- 尝试相同入口。
预期:
- 提示走“该道具/珍兽已锁定,无法...”。
- 不会误显示成“交易中的...”。
8.8 正常非交易流程验证
步骤:
- 用未交易、未锁定的普通对象走这些入口。
预期:
- 原功能保持不变。
总结
这次修复不是只补某一个按钮,而是把“交易态对象禁止被其他入口继续改动”这件事,补成了一套完整的结构:
scenecore.lua 统一封装交易态/锁定态判定函数。
- 高风险业务入口全部先拦截。
item_operator.lua 在底层继续兜底。
以后如果项目里再加新的“背包物品修改入口”或“珍兽修改入口”,直接复用:
check_item_operable
check_pet_operable
就不会再把同类漏洞放出来了。