一、先说结论
这次我要修的是一个很典型的“界面能打开,但真正提交链路断掉”的老问题。
问题现象有两个阶段:
- 第一阶段:
帮贡牌兑换 界面能正常打开,但输入合法帮贡后,服务端永远提示“您输入的帮贡数量大于您拥有的帮贡数量”。
- 第二阶段:前面的提示修掉之后,二次确认界面点“确定”又变成完全没反应。
这类问题最容易误判成“客户端没发包”或者“服务端缺整套功能”。
但这次结合当前 Game.exe、现网 Lua 服务端,以及老 Server 的 IDA 结果看下来,结论非常明确:
- 不是客户端坏了。
- 也不是
pc_guilddirector.lua 自己的兑换逻辑没写。
- 真正断掉的是“当前客户端实际发包路径”和“Lua 服务端实际接包路径”之间的兼容层。
并且这个问题只能改服务端,不需要也不应该改客户端。
二、为什么我说不要改客户端
先看当前客户端真实代码。
文件:
\Interface\BanggongExchange\BanggongExchange.lua
function BanggongExchange_OK_Clicked()
local str = BanggongExchange_Moral_Value:GetText()
local strNumber = 0
if str == nil or str == "" then
return
end
strNumber = tonumber(str)
if strNumber > Guild:GetGuildContri() then
PushDebugMessage("#{BGCH_8829_03}")
return
end
--帮贡牌兑换最大不能超过200点
if strNumber > g_ExchangeMaxBangGong then
PushDebugMessage("#{BGCH_8922_25}")
return
end
--帮贡牌兑换最小不能低于10点
if strNumber < g_ExchangeMinBangGong then
PushDebugMessage("#{BGCH_8922_26}")
return
end
-- Clear_XSCRIPT();
-- Set_XSCRIPT_Function_Name("BanggongExchange");
-- Set_XSCRIPT_ScriptID(805009);
-- Set_XSCRIPT_Parameter(0,strNumber);
-- Set_XSCRIPT_ParamCount(1);
-- Send_XSCRIPT();
Guild:ExchangeBangGong(strNumber,g_clientNpcId)
BanggongExchange_Close()
end
这里最关键的一点就是最后这句:
Guild:ExchangeBangGong(strNumber,g_clientNpcId)
也就是说,当前客户端已经不再直接走注释掉的旧 XSCRIPT 代码了。
帮会捐钱也是同样的写法。
文件:
\Interface\ConfraternityJuanxian\ConfraternityJuanxian.lua
-- Clear_XSCRIPT();
-- Set_XSCRIPT_Function_Name("PutGuildMoney");
-- Set_XSCRIPT_ScriptID(805012);
-- Set_XSCRIPT_Parameter(0,strNumber);
-- Set_XSCRIPT_ParamCount(1);
-- Send_XSCRIPT();
Guild:PutGuildMoney(strNumber,g_clientNpcId)
所以这次修复的思路一定要立住:
- 一切以当前
Game.exe 和当前客户端脚本为准。
- 不要去改客户端 Lua。
- 只把服务端缺失的兼容层补完整。
三、问题真正卡在了哪三层
这次实际卡了三层。
1. CGEventRequest 兼容字段没补
帮会相关 UI 在某些流程里还是会经过 CGEventRequest 风格包装。
而现有 Lua 版 packet.lua 只解析了老字段名,没有补业务层常用别名,导致场景层拿参数时很容易拿错。
2. 二次确认实际走的是 CGExecuteScript
这一步是最容易踩坑的地方。
前面“能弹二次确认框”并不代表真正的提交也走同一条链。
当前客户端的二次确认,实际是 CGExecuteScript,而不是 CGEventRequest。
Lua 服务端如果还沿用旧的 char_excute_Script 生硬直转,或者函数名里还带 \0 结尾,最后就会出现:
- 白名单过不去
- 函数名匹配不上
- 或者脚本参数进错位置
表面现象就是“点了确定没反应”。
3. 二级密码解锁状态没有同步到场景对象
这个是最后的真拦路虎。
pc_guilddirector.lua 和 city0_building5.lua 在执行兑换、捐献时,都会过:
self:IsPilferLockFlag(selfId)
但原来的 Lua 版服务端,场景里的 human 对象并没有真正维护“当前是否已解锁二级密码”这个状态。
结果就是玩家明明在网关侧已经解锁了,脚本层依然会把他当成未解锁,直接提前返回。
于是就出现了最迷惑的现象:
- 客户端点确定了
- 服务端也收到请求了
- 但脚本里提前
return
- 玩家看到的结果就是“没有任何反应”
四、修复目标
我这次修复遵循 4 个原则:
- 不修改客户端。
- 一切以当前
Game.exe 的行为为准。
- 只补服务端兼容层,不重做整套帮会系统。
- 顺手把“静默失败”改成“有提示失败”,方便后续排错。
五、涉及到的文件
本次真正和 帮贡牌兑换 修复直接相关的文件如下:
\home\ubuntu\Game2\services\game\packet.lua
\home\ubuntu\Game2\services\msgagent.lua
\home\ubuntu\Game2\services\scene\scenecore.lua
\home\ubuntu\Game2\services\scene\obj\human.lua
\home\ubuntu\Game2\lualib\script_base.lua
为了避免修完二级密码后影响其他旧功能,我还顺手修了三处历史反向判断:
\home\ubuntu\Game2\services\scripts\event\equip\wuhun.lua
\home\ubuntu\Game2\services\scripts\event\loulangucheng\eloulan_darkup.lua
\home\ubuntu\Game2\services\scripts\obj\luoyang\oluoyang_zhouran.lua
六、第一步:补 packet.lua 的协议兼容
文件:
\home\ubuntu\Game2\services\game\packet.lua
1. 修改 packet.CGExecuteScript
把 bis 改成下面这样,重点是把结尾的 \0 去掉:
***付费内容***
如果这一步不做,很容易出现:
- 白名单明明配了函数名,但匹配不上
BanggongExchange\0 和 BanggongExchange 被当成两个名字
2. 修改 packet.CGEventRequest
把 bis 改成下面这样,重点是补四个别名字段:
***付费内容***
这一步的意义很简单:
- 老代码习惯用
m_objID/index/arg/unknow
- 新兼容逻辑更适合统一用
script_id/ex_index/npc_obj_id/issue_script_id
别名补上之后,后面的场景层就可以不用反复猜字段。
七、第二步:在场景层补帮会 UI 的包装桥
文件:
\home\ubuntu\Game2\services\scene\scenecore.lua
这一段是本次修复里最关键的第一刀。
我建议直接把下面这整段加进去。
***付费内容***
然后把 char_event_request 改成下面这样:
***付费内容***
这一步修完之后,前面“永远提示帮贡不足”的问题通常就能消掉。
因为服务端终于能从包装过的事件参数里,把真正的兑换数量提出来了。
八、第三步:把 CGExecuteScript 路由改对
文件:
\home\ubuntu\Game2\services\msgagent.lua
1. 先加辅助函数
把下面这段加进去:
local function check_cg_execute_script_allow(script_id, func_name)
local configs = configenginer:get_config("allowable_script_func")
if not configs then
return false, "config_not_loaded"
end
local script_funcs = configs[script_id]
if not script_funcs then
return false, "script_id_not_allowed"
end
if script_funcs[func_name] ~= true then
return false, "func_name_not_allowed"
end
return true
end
local function log_cg_execute_script_reject(script_id, func_name, reason)
local my_data = ma_func:get_my_data()
local guid = my_data and my_data.attrib and my_data.attrib.guid or "nil"
local name = my_data and my_data.attrib and my_data.attrib.name or "nil"
skynet.loge("CGExecuteScript blocked:",
"guid =", guid,
";name =", name,
";script_id =", script_id,
";func_name =", tostring(func_name),
";reason =", reason)
end
local function notify_cg_execute_script_reject(reason)
if reason == "invalid_packet" then
return
end
ma_func:notify_tips("该功能当前暂未开放,请联系管理员处理")
end
local function normalize_cg_execute_script_func_name(func_name)
if type(func_name) ~= "string" then
return func_name
end
local zero_pos = string.find(func_name, "\0", 1, true)
if zero_pos then
func_name = string.sub(func_name, 1, zero_pos - 1)
end
return func_name
end
2. 用新的 request:CGExecuteScript() 覆盖旧逻辑
把原来的 request:CGExecuteScript() 换成下面这段:
function request:CGExecuteScript()
local script_id = self.m_ScriptID
local func_name = normalize_cg_execute_script_func_name(self.m_szFunName)
self.m_szFunName = func_name
if not script_id or not func_name or func_name == "" then
log_cg_execute_script_reject(script_id, func_name, "invalid_packet")
notify_cg_execute_script_reject("invalid_packet")
return
end
local allow, reason = check_cg_execute_script_allow(script_id, func_name)
if not allow then
log_cg_execute_script_reject(script_id, func_name, reason)
notify_cg_execute_script_reject(reason)
return
end
skynet.call(my_scene, "lua", "execute_client_script", my_obj_id, script_id, func_name, table.unpack(self.m_aParam))
end
这里我特意不再走老的:
char_excute_Script
原因很简单:
- 这次我们要在场景层对
BanggongExchange 和 PutGuildMoney 做参数纠正。
- 还要对缺脚本、缺函数做兜底提示。
- 所以必须单独走一个新的
execute_client_script 入口。
九、第四步:给场景层增加 execute_client_script
文件:
\home\ubuntu\Game2\services\scene\scenecore.lua
把下面这两个函数加进去:
***付费内容***
这段代码我建议原样抄。
它解决了三件事:
- 二次确认真正进到了正确脚本。
who 和 amount 的参数顺序被重新拉正。
- 后续如果客户端发到了一个服务端未开放脚本,也不会再表现成“没反应”,而是明确提示玩家。
十、第五步:把二级密码解锁状态同步到场景对象
到这一步为止,很多人会以为已经修好了。
但实际上,帮贡牌兑换 真正提交时还要经过物品锁校验。
如果你不补这一层,就会出现:
- 日志里已经能看到
execute_client_script
- 玩家也点了确定
- 但脚本就是不往下走
1. 给 human.lua 增加场景态字段
文件:
\home\ubuntu\Game2\services\scene\obj\human.lua
在初始化位置补一个字段:
self.minor_password_is_unlock = false
然后新增 getter / setter:
function human:set_minor_password_is_unlock(is_unlock)
self.minor_password_is_unlock = is_unlock == true
end
function human:is_minor_password_is_unlock()
return self.minor_password_is_unlock == true
end
2. 改写 script_base:IsPilferLockFlag
文件:
\home\ubuntu\Game2\lualib\script_base.lua
直接改成下面这样:
function script_base:IsPilferLockFlag(selfId)
local human = self.scene:get_obj_by_id(selfId)
if not human or human:get_obj_type() ~= "human" then
return false
end
if human:is_minor_password_is_unlock() then
return true
end
human:send_operate_result_msg(define.OPERATE_RESULT.OR_NEED_UNLOCKMINORPASSWORD)
return false
end
这一步就是整次修复里最关键的最后一刀。
因为 pc_guilddirector.lua 本身就是这样写的:
function pc_guilddirector:BanggongExchange(selfId, nvalue)
local haveBangGong = self:CityGetAttr(selfId, ScriptGlobal.GUILD_CONTRIB_POINT)
if not self:IsPilferLockFlag(selfId) then
return
end
if nvalue > haveBangGong then
self:NotifyFailTips(selfId, "#{BGCH_8829_03}")
return
end
city0_building5.lua 也是同样道理:
function city0_building5:PutGuildMoney(selfId, money)
...
if not self:IsPilferLockFlag(selfId) then return end
...
end
也就是说,只要 IsPilferLockFlag() 判断错了,兑换和捐献都会在最前面被拦死。
3. 在 msgagent.lua 里补状态同步
文件:
\home\ubuntu\Game2\services\msgagent.lua
先加一个同步函数:
local function sync_minor_password_unlock_state_to_scene()
if my_scene == nil or my_obj_id == nil then
return
end
local ok, err = pcall(
skynet.call,
my_scene,
"lua",
"set_minor_password_unlock_state",
my_obj_id,
ma_func:is_minor_password_is_unlock()
)
if not ok then
skynet.loge(
"sync_minor_password_unlock_state_to_scene failed:",
"scene =",
my_scene,
";obj_id =",
my_obj_id,
";err =",
err
)
end
end
然后在以下三个时机调用它:
1. 玩家进入场景后
ma_func:on_enter_scene(my_scene, my_obj_id, sceneid)
sync_minor_password_unlock_state_to_scene()
2. 设置二级密码成功后
minor_password = { password = self.password }
ma_func:set_minor_password(minor_password)
sync_minor_password_unlock_state_to_scene()
msg.m_Type = define.MINORPASSWD_RETURN_TYPE.MRETT_SETPASSWDSUCC
3. 解锁密码成功或失败时
if self.password ~= minor_password.password then
ma_func:clear_minor_password_is_unlock()
sync_minor_password_unlock_state_to_scene()
msg.m_Type = define.MINORPASSWD_RETURN_TYPE.MRETT_ERR_UNLOCKPASSWDFAIL
else
ma_func:set_minor_password_is_unlock()
sync_minor_password_unlock_state_to_scene()
msg.m_Type = define.MINORPASSWD_RETURN_TYPE.MRETT_UNLOCKPASSWDSUCC
end
这样一来:
- 网关侧知道玩家是否已解锁
- 场景里的
human 也知道
- 脚本层
IsPilferLockFlag() 才能判断正确
十一、第六步:修一下这三个历史反向判断
这一步不是专门为 帮贡牌兑换 写的,但既然我们把 IsPilferLockFlag() 语义修正了,就必须顺手把几个历史错误调用改过来。
正确写法应该是:
if not self:IsPilferLockFlag(selfId) then return end
我这次顺手改了这三处:
1. services/scripts/event/equip/wuhun.lua
function wuhun:KfsCompoud(selfId, nKfsMain, nKfsCom)
if not self:IsPilferLockFlag(selfId) then return end
...
end
2. services/scripts/event/loulangucheng/eloulan_darkup.lua
function eloulan_darkup:Anqi2Shenzhen(selfId, nPos, nMaterial)
if not self:IsPilferLockFlag(selfId) then return end
...
end
3. services/scripts/obj/luoyang/oluoyang_zhouran.lua
function oluoyang_zhouran:CheckBuyLuoyang(selfId, targetId)
if not self:IsPilferLockFlag(selfId) then return 0 end
...
end
如果你只修 帮贡牌兑换,这三处不是强依赖。
但如果你已经把 IsPilferLockFlag() 改正确了,这三个地方不改,后面早晚会炸出新的“解锁后反而不能操作”的老坑。
十二、为什么老 Server 只能参考思路,不能直接照搬
我这次还用 IDA 看了老 Server。
老 C++ 版里,CGExecuteScriptHandler::Execute 的核心思路是:
- 先过
AllowableScriptFunc
- 再按参数个数进
LuaInterface::ExeScript_*
这只能说明一件事:
但它不能直接拿来抄,因为我们现在跑的是 Lua 化后的 skynet 服务端,接包点、场景对象、脚本引擎接口都已经变了。
所以我的做法是:
- 用老
Server 确认“行为方向”
- 真正落地代码时,只改现网 Lua 服务端
这才是最稳的修法。
十三、修完后的验证方法
我建议按下面顺序验证:
1. 帮贡牌兑换
- 登录一个已经加入帮会的角色。
- 找到帮会总管,打开
帮贡牌兑换。
- 输入一个合法数值,例如
10。
- 点确认。
- 如果角色有二级密码,先完成解锁。
- 再次点击确认。
正常结果:
- 不再提示“您输入的帮贡数量大于您拥有的帮贡数量”
- 二次确认后真正执行兑换
2. 看日志
如果修好了,场景日志里通常能看到类似:
scenecore:execute_client_script banggong exchange who = 16 amount = 10
如果这里能打出来,说明:
CGExecuteScript 路由已经通了
- 参数位置也对了
3. 顺手测一下帮会捐钱
因为 PutGuildMoney 和 BanggongExchange 这次走的是同一套兼容思路,所以最好顺手也测一下。
十四、如果你只想最快抄代码,最少要改哪些地方
如果你只想最小闭环,我建议至少改下面这些:
必改
services/game/packet.lua
CGExecuteScript.bis
CGEventRequest.bis
services/scene/scenecore.lua
- 帮会 UI wrapper 兼容桥
execute_client_script
set_minor_password_unlock_state
services/msgagent.lua
request:CGExecuteScript
sync_minor_password_unlock_state_to_scene
- 登录进场景后同步
- 设置/解锁二级密码后同步
services/scene/obj/human.lua
minor_password_is_unlock 字段
- getter / setter
lualib/script_base.lua
建议顺手改
services/scripts/event/equip/wuhun.lua
services/scripts/event/loulangucheng/eloulan_darkup.lua
services/scripts/obj/luoyang/oluoyang_zhouran.lua
十五、最后总结
这次 帮贡牌兑换 的问题,本质上不是一个“单点脚本报错”,而是一个典型的多层兼容链断裂:
- 客户端 UI 已经换成了新的调用方式。
- Lua 服务端还在按旧入口理解它。
- 真正提交时又被二级密码场景态拦住了。
所以如果只补其中一层,就会出现那种很烦人的半修状态:
- 不是永远报错
- 就是弹确认后没反应
- 看起来像修好了,其实还差最后一脚
我这次这套改法的核心优点有三个:
- 不改客户端。
- 一切以当前
Game.exe 行为为准。
- 修完之后不仅
帮贡牌兑换 能用,后续同类 CGExecuteScript 问题也更容易排查。