一、先说结论
这次要修的不是“某一个帮会任务脚本坏了”,而是整套老 Lua4.0 帮会城市任务脚本,迁到当前 Lua5.4 + skynet 服务端之后,副本兼容层没有补完整。
表面现象是:
- 接任务正常
- 点进入副本也会走逻辑
- 但人物就是进不去
- 有时还会报这类错误
attempt to compare number with boolean
./lualib/copyscenemanager_core.lua:127: assertion failed!
或者更隐蔽一点:
- 不报错
- 副本看起来也创建了
- 但是角色没有被正常传进去
这类问题最容易误判成:
- 客户端没发包
- 副本管理器有问题
- 某个单独任务脚本写坏了
但这次结合当前正在使用的 Game.exe、当前 Lua 服务端、以及老版本城市任务脚本的实际写法来看,根因非常明确:
不是客户端问题,必须只改服务端。
并且这次不是只改一个文件就完事,而是要补两层:
lualib/script_base.lua
services/scripts/event/city/*.lua
当前这一套 city 目录里,一共是 49 个独立 Lua 文件,不是再靠 module.lua 或聚合脚本兜底,所以兼容代码必须同步进这些单文件里。
二、问题现象
这次排查期间,出现过三种典型表现。
1. 布尔值和数字比较直接炸掉
attempt to compare number with boolean
比如:
if IsHaveMission(sceneId, selfId, missionId) > 0 then
老城市脚本默认 IsHaveMission() 返回的是 1/0。
但当前底层实际返回的是 true/false。
结果就是脚本里一旦写了 > 0、== 0、<= 0 这种判断,就会直接炸。
2. 副本创建时断在 Copyscenemanager
日志类似:
./lualib/copyscenemanager_core.lua:127: assertion failed!
这不是副本管理器本身坏了,而是旧脚本调用 LuaFnCreateCopyScene(sceneId) 的方式太老,传进去的配置字段不完整。
当前副本管理器至少要拿到这些关键字段:
conf.sn
conf.source
conf.params[1]
老脚本没补这些字段时,就会直接断言。
3. 副本创建成功,但人物不进去
这个最坑,因为它不一定报错。
根因是老脚本里大量使用的是旧版 NewWorld 调用方式:
NewWorld(sceneId, selfId, destSceneId, x, y, clientRes)
但当前底层实际接口是:
script_base:NewWorld(selfId, dest_scene_id, sn, x, y, client_res)
也就是说:
- 旧脚本传的是“源场景ID + 自己ID + 目标场景ID”
- 新底层要的是“自己ID + 目标场景ID + 副本SN”
如果不做兼容转换,就会出现:
- 副本场景可能已经分配出来了
- 角色却没有正确保存副本 SN
- 最终看起来像“点了没反应”
三、根因拆解
这次真正缺的,是旧帮会城市脚本到新服务端之间的四个兼容点。
1. IsHaveMission() 返回值类型不兼容
老脚本按 1/0 写逻辑。
当前底层返回布尔值。
所以所有任务判断都会不稳定。
2. LuaFnCreateCopyScene() 没补副本配置
旧脚本传参过于简化。
当前副本管理器需要 sn / source / params[1]。
这一步不补,副本创建链路就会断。
3. NewWorld() 旧签名没有做兼容封装
副本场景创建出来后,旧脚本还是拿旧接口传送。
当前底层没法自动猜出副本 sn,所以人物不会被正确送进去。
4. LuaFnIsCanDoScriptLogic() / LuaFnIsObjValid() 也有 1/0 兼容问题
很多副本脚本在 OnCopySceneReady、组队成员遍历、传送判断时,都写了这种条件:
if LuaFnIsCanDoScriptLogic(sceneId, leaderObjId) ~= 1 then
return
end
如果底层直接回布尔值,旧脚本会把正常对象也误判成异常状态,于是提前 return。
所以这次修复不是“加一个判断”那么简单,而是要把旧城市脚本依赖的运行时环境补完整。
四、第一处修改:修 script_base.lua 的 LuaFnCreateCopyScene
文件:
/home/ubuntu/Game2/lualib/script_base.lua
把 LuaFnCreateCopyScene 改成下面这样:
function script_base:LuaFnCreateCopyScene(config)
assert(type(config) == "table", type(config))
config.params = config.params or {}
if config.params[1] == nil then
config.params[1] = self.script_id
end
if config.sn == nil then
config.sn = self:LuaFnGenCopySceneSN()
end
config.source = skynet.self()
local dest_scene_id = skynet.call(".Copyscenemanager", "lua", "select", config)
return dest_scene_id
end
这段代码的作用很直接:
- 保证
config 必须是表
- 自动补
params
- 自动补
params[1] = self.script_id
- 自动补副本
sn
- 自动补
source = skynet.self()
这样老城市脚本只要把配置表拼出来,就能正常交给当前副本管理器。
这里要特别注意一件事:
不要再沿用那种只传一个 sceneId 然后让底层自己猜全部配置的思路。
当前这套服务端不是那个时代的运行模型了,副本配置必须明确补齐。
五、第二处修改:在 event/city/*.lua 里补 get_copy_scene_config
文件范围:
/home/ubuntu/Game2/services/scripts/event/city/*.lua
这次不是只改 ecity_0102eliminatethieves.lua 一个文件。
而是所有 city 目录下的独立脚本,都要带这个兼容方法。
以任意一个城市任务脚本为模板,比如:
/home/ubuntu/Game2/services/scripts/event/city/ecity_0102eliminatethieves.lua
加入或改成下面这样:
***付费内容***
这段代码解决的是“旧脚本能描述副本,但描述得不完整”的问题。
重点补了这些字段:
sn
params
params[1]
client_res
其中 client_res 很关键。
因为当前客户端切场景时,需要匹配正确的场景资源编号。
副本能创建出来,不代表客户端一定能正常接收和展示。
六、第三处修改:给旧版 NewWorld 调用方式做兼容
还是同一个范围:
/home/ubuntu/Game2/services/scripts/event/city/*.lua
在脚本环境表里,把 NewWorld 这段兼容进去:
CallScriptFunction = function(target_script_id, func_name, ...)
return current_script():call_city_script(target_script_id, func_name, ...)
end,
NewWorld = function(scene_id, self_id, dest_scene_id, x, y, client_res)
local script = current_script()
local target_scene_id = tonumber(dest_scene_id) or dest_scene_id
local sn
if type(target_scene_id) == "number" and target_scene_id >= define.COPY_SCENE_BEGIN then
sn = script:call_base("LuaFnGetCopySceneData_Sn", target_scene_id)
if sn == define.INVAILD_ID then
sn = nil
end
end
return script:call_base("NewWorld", self_id, target_scene_id, sn, x, y, client_res)
end,
LuaFnTryRecieveItem = function(...)
return current_script():call_base("TryRecieveItem", ...)
end,
这段兼容层解决的是“老脚本还在按老签名传送”的问题。
核心逻辑是:
- 先拿到老脚本传进来的
dest_scene_id
- 判断它是不是副本场景
- 如果是副本场景,就再通过
LuaFnGetCopySceneData_Sn 找到它对应的 sn
- 最后再按当前底层真正需要的签名调用:
script:call_base("NewWorld", self_id, target_scene_id, sn, x, y, client_res)
如果不做这层转换,很多帮会任务副本会表现成:
- 副本分配成功
- 任务逻辑继续跑
- 玩家却不进副本
七、第四处修改:把布尔返回值统一转回旧脚本习惯的 1/0
这一块同样在:
/home/ubuntu/Game2/services/scripts/event/city/*.lua
1. 先修 LuaFnIsCanDoScriptLogic 和 LuaFnIsObjValid
把这两段兼容进去:
LuaFnIsCanDoScriptLogic = function(...)
local result = current_script():call_base("LuaFnIsCanDoScriptLogic", ...)
return result and 1 or 0
end,
LuaFnIsObjValid = function(...)
local result = current_script():call_base("LuaFnIsObjValid", ...)
return result and 1 or 0
end,
这样旧脚本里这些判断才能恢复正常:
if LuaFnIsCanDoScriptLogic(sceneId, leaderObjId) ~= 1 then
return
end
if LuaFnIsObjValid(sceneId, mems[i]) == 1 then
...
end
2. 再修 IsHaveMission
在 setmetatable(env, { __index = function(_, key) ... end }) 这一层里,把 IsHaveMission 的结果统一转成 1/0:
local result = script_base[key](script, table.unpack(args))
if key == "IsHaveMission" then
return result and 1 or 0
end
return result
这一步非常关键。
因为老帮会任务脚本里到处都是这种写法:
if IsHaveMission(sceneId, selfId, missionId) > 0 then
if IsHaveMission(sceneId, selfId, missionId) == 0 then
if IsHaveMission(sceneId, selfId, missionId) <= 0 then
如果你只修副本创建,不修这里,就会继续出现:
attempt to compare number with boolean
八、为什么这次必须批量同步到全部 city 文件
很多人会想先只修一个,比如:
ecity_0102eliminatethieves.lua
这样做只能把“某一个任务”修通,后面换另一个帮会任务时还是会继续炸。
原因很简单:
- 当前
services/scripts/event/city 目录已经不是老式共享模块结构
- 现在是 49 个独立 Lua 文件 各自内嵌运行环境
- 兼容层不在公共
module.lua 里自动继承
所以正确做法不是“修一个任务文件试试”,而是把下面这些兼容段同步到全部城市任务脚本:
get_copy_scene_config
NewWorld 旧签名兼容
LuaFnIsCanDoScriptLogic 返回值兼容
LuaFnIsObjValid 返回值兼容
IsHaveMission 返回值兼容
如果你的正式服目录结构和这次一样,那么最终要上传的是:
/home/ubuntu/Game2/lualib/script_base.lua
/home/ubuntu/Game2/services/scripts/event/city/*.lua
也就是说:
script_base.lua 1 个文件
event/city 目录下 49 个 Lua 文件
九、建议的上传清单
这次修复如果你要往正式服发,按下面这个清单最稳:
必传
ubuntu/Game2/lualib/script_base.lua
ubuntu/Game2/services/scripts/event/city/*.lua
不需要上传
客户端任何文件
这次问题的根因在服务端兼容层,不在客户端。
所以博客里也建议明确写一句:
以当前 Game.exe 为准,禁止改客户端,只修服务端。
十、测试清单
这次修完以后,我建议至少做下面几组测试。
1. 基础进入副本测试
- 接一个帮会任务
- 点进入副本
- 确认角色是否真正切进副本地图
- 确认不是只创建了副本但人物留在原场景
2. 队伍进入测试
- 组队接任务
- 由队长发起进入副本
- 确认队员是否也能正常进入
- 确认不会卡在
LuaFnIsCanDoScriptLogic ~= 1
3. 任务状态判断测试
- 接任务后点击进入
- 放弃任务后再点进入
- 观察
IsHaveMission 判断是否正常
- 确认不再出现
attempt to compare number with boolean