问题现象
玩家面对面交易时,服务端在 char_exchange_sync_item_II() 的加物/加宠分支里,直接把背包对象塞进交易盒:
- 没有复用已有的“锁定态校验”。
- 物品没有校验
can_exchange()。
- 结果会出现两类问题:
- 已经被摆摊、商会或其他系统锁住的物品/珍兽,仍然可以加入玩家交易。
- 配置上禁止交易的物品,仍然可以加入玩家交易。
这个问题和摆摊入口形成了明显不一致。
根因分析
1. 摆摊入口其实已经有基线校验
文件:services/scene/scenecore.lua
摆摊加物时,原本就有两层判断:
if human_item_logic:is_item_can_exchange(obj, item_container, bag_index) then
local item = item_container:get_item(bag_index)
if item:is_bind() or item:is_ruler(define.ITEM_RULER_LIST.IRL_PICKBIND) then
obj:notify_tips("绑定道具不能交易")
return
end
if not item:can_exchange() then
obj:notify_tips(string.format("%s不能交易", item:get_name()))
return
end
...
end
摆摊加宠时,也会先校验锁定态:
if human_item_logic:is_pet_can_exchange(obj, pet_container, bag_index) then
...
end
2. 玩家交易入口缺少同样的校验
文件:services/scene/scenecore.lua
原始 char_exchange_sync_item_II() 加物逻辑里,只有“是否绑定”和“交易格是否有空位”的判断:
local item = human_item_logic:get_item(obj_me, ei.from_index)
if item then
for i = 0, define.EXCHANGE_BOX_SIZE - 1 do
local exchange_item = my_item_container:get_item(i)
if exchange_item and exchange_item:get_guid() == item:get_guid() then
return
end
end
if item:is_bind() or item:is_ruler(define.ITEM_RULER_LIST.IRL_PICKBIND) then
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
return
end
local empty_index = my_item_container:get_empty_item_index()
if empty_index ~= define.INVAILD_ID then
my_item_container:set_item(empty_index, item)
if my_item_container:get_item(empty_index) then
item_operator:lock_item(obj_me:get_prop_bag_container(), ei.from_index)
item:set_in_exchange(true)
...
end
end
end
加宠分支也一样,只校验了 from_index 是否存在,没有校验宠物当前是否已经被锁定:
local pet_bag_container = obj_me:get_pet_bag_container()
local from_index = pet_bag_container:get_index_by_pet_guid(ei.pet_guid)
if not from_index then
...
return
end
local empty_index = my_pet_container:get_empty_item_index()
if empty_index ~= define.INVAILD_ID then
local pet = pet_bag_container:get_item(from_index)
my_pet_container:set_item(empty_index, pet)
item_operator:lock_item(pet_bag_container, from_index)
pet:set_in_exchange(true)
...
end
所以玩家交易入口和摆摊入口的约束不一致,最终留下漏洞。
修改目标
本次修复只解决一个问题:
- 玩家交易加物时,必须校验“当前是否已被锁定”和“该物品是否允许交易”。
- 玩家交易加宠时,必须校验“当前是否已被锁定”。
- 交易入口拦截锁定物品时,提示语要准确,不要沿用摆摊提示。
修改文件
本次修复涉及 2 个文件:
lualib/human_item_logic.lua
services/scene/scenecore.lua
第一步:给通用校验函数增加“可选提示文案”
文件:lualib/human_item_logic.lua
原代码:
function human_item_logic:is_item_can_exchange(human, contaner, bag_index)
local item = contaner and contaner:get_item(bag_index)
if not item or item:is_empty() then
return false
end
if item:is_lock() then
human:notify_tips("该道具已在交易或摆摊中")
return false
end
return true
end
function human_item_logic:is_pet_can_exchange(human, contaner, bag_index)
local pet = contaner and contaner:get_item(bag_index)
if not pet then
return false
end
if pet.is_lock and pet:is_lock() then
human:notify_tips("该珍兽已在交易或摆摊中")
return false
end
return true
end
修改后:
function human_item_logic:is_item_can_exchange(human, contaner, bag_index, locked_tips)
local item = contaner and contaner:get_item(bag_index)
if not item or item:is_empty() then
return false
end
if item:is_lock() then
human:notify_tips(locked_tips or "该道具已在交易或摆摊中")
return false
end
return true
end
function human_item_logic:is_pet_can_exchange(human, contaner, bag_index, locked_tips)
local pet = contaner and contaner:get_item(bag_index)
if not pet then
return false
end
if pet.is_lock and pet:is_lock() then
human:notify_tips(locked_tips or "该珍兽已在交易或摆摊中")
return false
end
return true
end
为什么这么改
这样做有两个好处:
- 摆摊、商会等已有调用点不需要修改,默认提示语保持原样。
- 玩家交易入口可以传自己的提示语,例如:
"该道具已锁定,无法交易"
"该珍兽已锁定,无法交易"
这样不会把“摆摊文案”错误地显示在“玩家交易”里。
第二步:交易入口先刷新锁状态
文件:services/scene/scenecore.lua
在 char_exchange_sync_item_II() 入口最前面加入:
obj_me:refresh_locked_item_unlock_state(false)
修改后开头如下:
function scenecore:char_exchange_sync_item_II(who, ei)
local obj_me = self:get_obj_by_id(who)
if self:check_item_limit_exchange(who) then
obj_me:notify_tips("#{HJYK_201223_11}")
return
end
obj_me:refresh_locked_item_unlock_state(false)
local my_exchange_box = obj_me:get_exchange_box()
...
end
为什么要先刷新
因为项目里物品锁定既有“手动锁”,也有“延迟解锁”的状态。
先刷新一次,可以避免:
- 客户端已经看到物品解锁了。
- 服务端还拿着旧锁状态。
- 然后把本来能交易的物品误判成锁定。
第三步:修复加物分支
文件:services/scene/scenecore.lua
3.1 原始加物分支
if ei.opt == packet_def.CGExchangeSynchItemII.OPT.OPT_ADDITEM then
if ei.from_type == packet_def.CGExchangeSynchItemII.POS.POS_BAG then
local item = human_item_logic:get_item(obj_me, ei.from_index)
if item then
...
if item:is_bind() or item:is_ruler(define.ITEM_RULER_LIST.IRL_PICKBIND) then
...
return
end
local empty_index = my_item_container:get_empty_item_index()
if empty_index ~= define.INVAILD_ID then
my_item_container:set_item(empty_index, item)
if my_item_container:get_item(empty_index) then
item_operator:lock_item(obj_me:get_prop_bag_container(), ei.from_index)
item:set_in_exchange(true)
...
end
end
end
end
end
3.2 修改后的加物分支
if ei.opt == packet_def.CGExchangeSynchItemII.OPT.OPT_ADDITEM then
if ei.from_type == packet_def.CGExchangeSynchItemII.POS.POS_BAG then
local item = human_item_logic:get_item(obj_me, ei.from_index)
if item then
local prop_bag_container = obj_me:get_prop_bag_container()
if not human_item_logic:is_item_can_exchange(obj_me, prop_bag_container, ei.from_index, "该道具已锁定,无法交易") then
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
return
end
for i = 0, define.EXCHANGE_BOX_SIZE - 1 do
local exchange_item = my_item_container:get_item(i)
if exchange_item and exchange_item:get_guid() == item:get_guid() then
return
end
end
if item:is_bind() or item:is_ruler(define.ITEM_RULER_LIST.IRL_PICKBIND) then
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
return
end
if not item:can_exchange() then
obj_me:notify_tips(string.format("%s不能交易", item:get_name()))
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
return
end
local empty_index = my_item_container:get_empty_item_index()
if empty_index ~= define.INVAILD_ID then
my_item_container:set_item(empty_index, item)
if my_item_container:get_item(empty_index) then
item_operator:lock_item(prop_bag_container, ei.from_index)
item:set_in_exchange(true)
...
end
end
end
end
end
3.3 这里实际补了什么
补了两层防线:
第一层:锁定态校验
if not human_item_logic:is_item_can_exchange(obj_me, prop_bag_container, ei.from_index, "该道具已锁定,无法交易") then
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
return
end
这样就能拦住:
- 已被摆摊锁住的物品。
- 已被其他系统临时锁住的物品。
- 其他已经处于不可操作状态的物品。
第二层:物品交易规则校验
if not item:can_exchange() then
obj_me:notify_tips(string.format("%s不能交易", item:get_name()))
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
return
end
这一步专门拦:
- 配表上禁止交易的物品。
- 规则层不允许进入玩家交易的物品。
第四步:修复加宠分支
文件:services/scene/scenecore.lua
4.1 原始加宠分支
elseif ei.opt == packet_def.CGExchangeSynchItemII.OPT.OPT_ADDPET then
local pet_bag_container = obj_me:get_pet_bag_container()
local from_index = pet_bag_container:get_index_by_pet_guid(ei.pet_guid)
if not from_index then
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_NOT_ENOUGHT_EXROOM
self:send2client(obj_me, msg)
return
end
...
local empty_index = my_pet_container:get_empty_item_index()
if empty_index ~= define.INVAILD_ID then
local pet = pet_bag_container:get_item(from_index)
my_pet_container:set_item(empty_index, pet)
item_operator:lock_item(pet_bag_container, from_index)
pet:set_in_exchange(true)
...
end
end
4.2 修改后的加宠分支
elseif ei.opt == packet_def.CGExchangeSynchItemII.OPT.OPT_ADDPET then
local pet_bag_container = obj_me:get_pet_bag_container()
local from_index = pet_bag_container:get_index_by_pet_guid(ei.pet_guid)
if not from_index then
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_NOT_ENOUGHT_EXROOM
self:send2client(obj_me, msg)
return
end
for i = 0, define.EXCHANGE_BOX_SIZE - 1 do
local pet = my_pet_container:get_item(i)
if pet and pet:get_guid() == ei.pet_guid then
return
end
end
if not human_item_logic:is_pet_can_exchange(obj_me, pet_bag_container, from_index, "该珍兽已锁定,无法交易") then
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
return
end
local empty_index = my_pet_container:get_empty_item_index()
if empty_index ~= define.INVAILD_ID then
local pet = pet_bag_container:get_item(from_index)
my_pet_container:set_item(empty_index, pet)
item_operator:lock_item(pet_bag_container, from_index)
pet:set_in_exchange(true)
...
end
end
4.3 这里为什么只补锁定态
当前珍兽侧并没有和物品 item:can_exchange() 对应的统一规则函数。
所以本次第一阶段修复先把最危险的漏洞堵上:
- 已被锁定的珍兽,不能加入玩家交易。
- 提示语准确显示为“该珍兽已锁定,无法交易”。
如果后续项目里还有“珍兽交易规则表”,再在这里补一层显式规则校验即可。
第五步:为什么错误时还要发 GCExchangeError
锁定态拦截时,除了 notify_tips(),还额外回了:
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
这么做是为了保持交易协议层行为一致:
- 玩家能看到明确提示。
- 客户端交易界面能收到“本次操作非法/失败”的协议反馈。
只弹提示,不回协议,容易出现客户端界面状态停留在“已尝试加入”的半同步状态。
第六步:最终代码落点
lualib/human_item_logic.lua
新增可选提示参数:
function human_item_logic:is_item_can_exchange(human, contaner, bag_index, locked_tips)
...
end
function human_item_logic:is_pet_can_exchange(human, contaner, bag_index, locked_tips)
...
end
services/scene/scenecore.lua
交易入口最前面刷新锁状态:
obj_me:refresh_locked_item_unlock_state(false)
交易加物补锁定态和 can_exchange() 校验:
if not human_item_logic:is_item_can_exchange(obj_me, prop_bag_container, ei.from_index, "该道具已锁定,无法交易") then
...
return
end
if not item:can_exchange() then
...
return
end
交易加宠补锁定态校验:
if not human_item_logic:is_pet_can_exchange(obj_me, pet_bag_container, from_index, "该珍兽已锁定,无法交易") then
...
return
end
第七步:建议回归测试
7.1 锁定物品加入玩家交易
步骤:
- 准备一个已被其他系统锁住的物品。
- 发起玩家交易。
- 尝试把该物品加入交易。
预期:
- 服务端拒绝。
- 提示:
该道具已锁定,无法交易
- 客户端收到交易错误包。
7.2 禁交易物品加入玩家交易
步骤:
- 准备一个
can_exchange() == false 的物品。
- 发起玩家交易。
- 尝试加入交易。
预期:
- 服务端拒绝。
- 提示:
xxx不能交易
- 不会进入交易盒。
7.3 锁定珍兽加入玩家交易
步骤:
- 准备一个已被锁住的珍兽。
- 发起玩家交易。
- 尝试加入交易。
预期:
- 服务端拒绝。
- 提示:
该珍兽已锁定,无法交易
7.4 普通可交易物品加入交易
步骤:
- 准备普通未绑定、未锁定、允许交易的物品。
- 发起玩家交易。
- 正常加入交易盒。
预期:
- 服务端允许加入。
- 背包物品被锁定。
item:set_in_exchange(true) 生效。
7.5 摆摊原提示不变
步骤:
- 在摆摊入口放入锁定物品/珍兽。
预期:
- 仍然沿用原来的默认提示:
该道具已在交易或摆摊中
该珍兽已在交易或摆摊中
这说明“可选提示参数”没有误伤旧逻辑。