崩溃点:
01/04/26 17:17:08.62 [:000000a8] lua call [1fa to :a8 : 2691 msgsz = 0] error : [31m./framework/lualib/skynet.lua:988: ./framework/lualib/skynet.lua:452: ./framework/lualib/skynet/queue.lua:20: ./framework/lualib/skynet.lua:720: call failed
stack traceback:
[C]: in function 'error'
./framework/lualib/skynet.lua:720: in upvalue 'yield_call'
./framework/lualib/skynet.lua:737: in function 'skynet.call'
./services/msgagent.lua:2028: in local 'f'
./lualib/net.lua:130: in function 'net.dispatch_message'
./services/msgagent.lua:2456: in local 'f'
./services/msgagent.lua:2633: in function <./services/msgagent.lua:2631>
[C]: in function 'xpcall'
./framework/lualib/skynet/queue.lua:34: in upvalue 'lock'
./services/msgagent.lua:2631: in upvalue 'f'
./framework/lualib/skynet.lua:402: in function <./framework/lualib/skynet.lua:374>
stack traceback:
[C]: in function 'assert'
./framework/lualib/skynet/queue.lua:20: in function <./framework/lualib/skynet/queue.lua:12>
(...tail calls...)
./services/msgagent.lua:2631: in upvalue 'f'
./framework/lualib/skynet.lua:402: in function <./framework/lualib/skynet.lua:374>
stack traceback:
[C]: in function 'assert'
./framework/lualib/skynet.lua:988: in function 'skynet.dispatch_message'[0m
01/04/26 17:17:08.63 [:000001fa] lua call [a8 to :1fa : 2691 msgsz = 78] error : [31m./framework/lualib/skynet.lua:988: ./framework/lualib/skynet.lua:452: ./services/scene/scenecore.lua:8847: attempt to index a nil value (local 'item')
stack traceback:
./services/scene/scenecore.lua:8847: in function 'scene.scenecore.char_exchange_sync_item_II'
(...tail calls...)
./services/scene/scene.lua:26: in upvalue 'realFun'
./lualib/profile.lua:56: in upvalue 'f'
./framework/lualib/skynet.lua:402: in function <./framework/lualib/skynet.lua:374>
stack traceback:
[C]: in function 'assert'
./framework/lualib/skynet.lua:988: in function 'skynet.dispatch_message'[0m
玩家交易移除物品空指针崩溃修复说明
问题现象
玩家交易过程中,服务端处理“从交易盒移除物品”的入口在:
services/scene/scenecore.lua
char_exchange_sync_item_II
OPT_REMOVEITEM
原逻辑里,如果客户端重放异常封包,或者传入一个已经为空的交易格,服务端会先对空对象调用:
item:get_guid()
这会直接触发空指针错误,严重时可能把场景逻辑打崩。
也就是说,这不是普通的“交易失败”,而是一个会把服务端执行流打断的崩溃点。
根因分析
原始代码顺序有问题
文件:services/scene/scenecore.lua
原始代码:
elseif ei.opt == packet_def.CGExchangeSynchItemII.OPT.OPT_REMOVEITEM then
if ei.to_type == packet_def.CGExchangeSynchItemII.POS.POS_BAG then
local item = my_item_container:get_item(ei.from_index)
local prop_bag_container = obj_me:get_prop_bag_container()
local bag_index = prop_bag_container:get_index_by_guid(item:get_guid())
if not bag_index then
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
return
end
if item then
my_item_container:erase_item(ei.from_index)
item_operator:unlock_item(prop_bag_container, bag_index)
item:set_in_exchange(false)
...
end
end
end
问题就在这一句:
local bag_index = prop_bag_container:get_index_by_guid(item:get_guid())
它发生在:
if item then
...
end
之前。
换句话说,代码还没确认 item 存在,就已经先使用 item:get_guid() 了。
当 my_item_container:get_item(ei.from_index) 返回 nil 时,就会直接报错。
触发方式
理论上至少有下面几种情况会命中:
- 客户端向一个空交易格发送
OPT_REMOVEITEM。
- 同一个移除封包被异常重放两次,第一次已经移除了物品,第二次格子为空。
- 交易状态已被其他逻辑清掉,但客户端又补发了旧的移除包。
这些情况本来都应该被服务端当作“非法操作”拒绝,而不是进入 Lua 运行时错误。
修复目标
这次修复只做一件事:
- 在读取
item:get_guid() 之前,先确认 item 不为空。
也就是说:
- 异常包应该被安全拒绝。
- 正常移除交易物品流程不能受影响。
- 修复必须尽量小,避免影响已有交易逻辑。
修改文件
本次只修改一个文件:
services/scene/scenecore.lua
修复方案
原始代码
local item = my_item_container:get_item(ei.from_index)
local prop_bag_container = obj_me:get_prop_bag_container()
local bag_index = prop_bag_container:get_index_by_guid(item:get_guid())
if not bag_index then
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
return
end
if item then
my_item_container:erase_item(ei.from_index)
item_operator:unlock_item(prop_bag_container, bag_index)
item:set_in_exchange(false)
...
end
修改后代码
local item = my_item_container:get_item(ei.from_index)
local prop_bag_container = obj_me:get_prop_bag_container()
if not item then
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
return
end
local bag_index = prop_bag_container:get_index_by_guid(item:get_guid())
if not bag_index then
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
return
end
my_item_container:erase_item(ei.from_index)
item_operator:unlock_item(prop_bag_container, bag_index)
item:set_in_exchange(false)
...
完整修复片段
建议直接对照这一段修改:
elseif ei.opt == packet_def.CGExchangeSynchItemII.OPT.OPT_REMOVEITEM then
if ei.to_type == packet_def.CGExchangeSynchItemII.POS.POS_BAG then
local item = my_item_container:get_item(ei.from_index)
local prop_bag_container = obj_me:get_prop_bag_container()
if not item then
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
return
end
local bag_index = prop_bag_container:get_index_by_guid(item:get_guid())
if not bag_index then
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
return
end
my_item_container:erase_item(ei.from_index)
item_operator:unlock_item(prop_bag_container, bag_index)
item:set_in_exchange(false)
if bag_index ~= define.INVAILD_ID then
local msg = packet_def.GCExchangeSynchII.new()
msg.is_my_self = 1
msg.opt = packet_def.CGExchangeSynchItemII.OPT.OPT_REMOVEITEM
msg.to_type = packet_def.CGExchangeSynchItemII.POS.POS_BAG
msg.to_index = bag_index
msg.from_index = ei.from_index
self:send2client(obj_me, msg)
msg = packet_def.GCExchangeSynchII.new()
msg.is_my_self = 0
msg.opt = packet_def.CGExchangeSynchItemII.OPT.OPT_REMOVEITEM
msg.from_index = ei.from_index
self:send2client(obj_tar, msg)
return
else
local msg = packet_def.GCExchangeError.new()
msg.error_code = define.EXCHANGE_ERR.ERR_ILLEGAL
self:send2client(obj_me, msg)
end
end
end
为什么这样修就够了
这个问题的根本原因不是交易状态机错了,而是访问顺序错了。
所以最小修复原则就是:
- 先判空。
- 再访问对象字段或方法。
这里不需要大改交易逻辑,也不需要改协议。
只要保证:
if not item then
return illegal
end
在 item:get_guid() 之前执行,就已经能把崩溃问题彻底挡住。
为什么返回 ERR_ILLEGAL
当交易格为空时,客户端发来的“移除该格物品”本身就是非法状态。
所以这里最合理的处理方式不是静默吞掉,而是回:
define.EXCHANGE_ERR.ERR_ILLEGAL
这样做的好处是:
- 客户端能收到明确失败反馈。
- 服务端不会继续执行异常逻辑。
- 不会把这种错误误判成“背包空间问题”或“交易盒满了”。
建议测试项
1. 正常移除交易物品
步骤:
- 发起交易。
- 放入一个正常物品。
- 从交易盒移除该物品。
预期:
- 正常移除成功。
- 物品回到背包。
lock 状态解除。
in_exchange 状态置回 false。
2. 对空交易格发送移除包
步骤:
- 发起交易,但不要往某个交易格放物品。
- 直接向该空格发送
OPT_REMOVEITEM。
预期:
- 服务端不崩溃。
- 返回
GCExchangeError。
- 错误码为
ERR_ILLEGAL。
3. 重放移除封包
步骤:
- 放入一个交易物品。
- 正常移除一次。
- 再重放同一个移除包。
预期:
- 第一次正常移除。
- 第二次被服务端安全拒绝。
- 不出现 Lua 空指针错误。
4. 交易取消后重放旧移除包
步骤:
- 放入物品。
- 取消交易。
- 重放之前的
OPT_REMOVEITEM 包。
预期:
- 服务端安全拒绝。
- 不崩溃。
- 不影响后续新的交易流程。
修复后效果
修复前:
- 交易格为空。
- 客户端发送
OPT_REMOVEITEM。
- 服务端执行
item:get_guid()。
- 直接触发空指针错误。
修复后:
- 交易格为空。
- 客户端发送
OPT_REMOVEITEM。
- 服务端先判空。
- 发现无物品,直接返回
ERR_ILLEGAL。
- 不再触发崩溃。
总结
这次修复的核心非常明确:
任何来自客户端的对象访问,都必须先做空值保护,再取字段。
在交易系统里,这一条尤其重要,因为:
- 客户端可能重放旧包。
- 客户端可能发顺序错乱的包。
- 服务端必须把这类异常流量当作“非法请求”处理,而不是让它进入运行时崩溃。
这次修改虽然很小,但它修掉的是一个真正的高危稳定性问题。