物品锁 / 二级密码服务端最终修改笔记
这次把物品锁和二级密码这一套功能补完后,我按最终代码整理一份可直接复用的笔记。
目标很明确:客户端不改,只改 Lua 服务端,完全适配客户端现有逻辑。
1. 最终效果
- 已设置二级密码时,点物品加锁/解锁前,必须先做一次二级密码认证。
- 未设置二级密码时,服务端返回“需要设置二级密码”,客户端走设置流程。
PASSWDSETUP 只表示“是否已经设置过二级密码”,不再和“本次登录是否已经认证”混在一起。
- 解锁流程保留客户端原有表现,但服务端把解锁等待时间改成了
1 秒,基本无感。
- 发起解锁后,服务端会把物品当前锁状态和解锁进度下发给客户端,客户端可以显示解锁中的图标和剩余时间。
- 切场景后,
ItemCoffer 能直接从背包摘要包恢复锁定图标,不需要再靠鼠标移上去触发刷新。
- 客户端代码不需要修改。
2. 这次实际改到的文件
\home\ubuntu\Game2\msgagent_module\ma_func.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\services\game\packet.lua
3. 先记两个协议结论
3.1 二级密码弹“设置”还是“修改”,客户端只看有没有设置过
客户端现有逻辑是这样的:
function Packet_OpenDlgForErjimima()
local isSetMinorPwd = IsMinorPwdSetup();
if(tonumber(isSetMinorPwd) == 1) then
OpenChangeMinorPasswordDlg();
else
OpenSetMinorPasswordDlg();
end
end
所以服务端的 PASSWDSETUP 只能表达一件事:库里有没有二级密码。
不能把“当前登录已经认证过”也塞到这个状态里,不然客户端会误判,出现“明明设置过了却还让我再设置一次”的问题。
3.2 ItemCoffer 锁图标依赖两层数据
客户端现有 ItemCoffer 会读到下面两个状态:
- 单物品数据里的
bProtect / nUnlockElapsedTime
- 背包摘要包里的锁摘要位
单物品逻辑大概是这样:
function ItemCoffer_UpDateItem(szPacketName)
local i=1;
while i<=PACKAGE_BUTTONS_NUM do
local theAction,bLocked,bProtect,nUnlockElapsedTime = PlayerPackage:EnumItem(szPacketName, i-1);
if bLocked == 1 then
PACKAGE_BUTTONS[i]:Disable();
else
PACKAGE_BUTTONS[i]:Enable();
end
if( bProtect == 1 ) then
LOCK_ICON[i]:Show();
if( nUnlockElapsedTime == 0 ) then
LOCK_ICON[i]:SetProperty("Image","set:UIIcons image:Icon_Lock");
else
LOCK_ICON[i]:SetProperty("Image","set:CommonFrame6 image:NewLock");
end
end
i = i+1;
end
end
所以最终服务端有两个适配点:
- 单物品包里必须把
item.unknow_45 和 item.unknow_44 维护好。
GCMyBagList 里必须把每个格子的锁摘要状态带上,这样切场景后 ItemCoffer 才能立即恢复图标。
4. 最终实现思路
4.1 二级密码的“是否设置”与“本次会话是否已认证”彻底拆开
minor_password ~= nil 只表示已经设置过密码。
minor_password_is_unlock 只表示本次登录会话是否已通过认证。
- 设置密码成功后,会话认证态重置为未认证。
- 解锁密码输错时,会话认证态清掉。
- 只要角色已经设置过密码,那么物品加锁/解锁一律先要求认证。
4.2 物品解锁不做大改,只把等待缩到 1 秒
这次为了最小化修改,没有把流程改成“点解锁直接去锁”。
最终稳定版本仍然保留“解锁中 -> 自动解除”的客户端表现,只是把等待时间改成了 1 秒:
- 点解锁时,先记录
guid -> unlock_time
- 立即回一次物品包,客户端显示“解锁中”
- 角色心跳每
200ms 刷一次
- 到时间后自动把物品锁状态清掉,再回一次物品包
这样客户端表现完整,改动也小。
4.3 切场景后锁图标恢复,靠 GCMyBagList
切场景时,客户端会重新拉背包摘要。
如果服务端只在单物品包里带锁状态,而 GCMyBagList 不带,那么 ItemCoffer 初始是看不到锁图标的,只有鼠标移上去触发单物品刷新后才会恢复。
所以最终做法是:
ask_my_bag_list() 改成走 obj:get_item_bag_list()
get_item_bag_list() 先同步一次锁状态,再复制背包数据
CGAskMyBagList() 给每个格子补 pw_lock 摘要位
5. 源代码
源代码按文件分组记录。
5.1 msgagent_module/ma_func.lua
function ma_func:get_minor_password()
return self.my_data.minor_password
end
function ma_func:set_minor_password(minor_password)
self.my_data.minor_password = minor_password
self.minor_password_is_unlock = false
local updater = {}
updater["$set"] = {minor_password = minor_password}
skynet.call(".char_db", "lua", "update", { collection = "character", selector = {["attrib.guid"] = self.my_guid}, update = updater})
end
function ma_func:set_minor_password_is_unlock()
self.minor_password_is_unlock = true
end
function ma_func:clear_minor_password_is_unlock()
self.minor_password_is_unlock = false
end
function ma_func:is_minor_password_is_unlock()
if self.my_data.minor_password == nil then
return true
end
return self.minor_password_is_unlock
end
说明:
get_minor_password() 只看库里是否有密码。
is_minor_password_is_unlock() 负责判断本次会话是否已经通过认证。
- 未设置密码时默认返回
true,避免没有二级密码的角色被无意义拦截。
5.2 services/msgagent.lua
5.2.1 CGMinorPasswd
***付费内容***
这个函数里最关键的点有三个:
PASSWDSETUP 只回“有没有设置过密码”。
DELETEPASSWDTIME 正常回包,防止客户端流程断。
UNLOCKPASSWD 输错时清会话认证态,输对时只设置认证态,不动“是否已设置”。
5.2.2 CGAskMyBagList 需要补锁摘要
local function get_bag_list_pw_lock(item)
local goods_protect = tonumber(item and item.unknow_45) or 0
local unlock_elapsed = tonumber(item and item.unknow_44) or 0
if goods_protect == 0 then
return 0
end
return unlock_elapsed > 0 and 2 or 1
end
local function build_empty_item_guid()
return {
world = 0,
server = 0,
mask = 0,
series = 0,
}
end
local function build_bag_list_entry(pos, item)
local entry = {
index = math.max(0, math.floor(tonumber(pos) or 0)),
unknow = {0, 0, 0},
unknow_1 = {0, 0, 0},
guid = build_empty_item_guid(),
item_index = 0,
count = 0,
}
if not item then
return entry
end
entry.unknow_1[1] = get_bag_list_pw_lock(item)
entry.guid = item.guid or entry.guid
entry.item_index = math.max(0, math.floor(tonumber(item.item_index) or 0))
entry.count = math.max(0, math.floor(tonumber(item.count) or 0))
return entry
end
function request:CGAskMyBagList()
local item_list = skynet.call(my_scene, "lua", "ask_my_bag_list", my_obj_id)
local ret = packet_def.GCMyBagList.new()
local list = {}
local mode = math.floor(tonumber(self.unknow_1) or 0)
if mode == 1 then
local ask_count = math.max(0, math.floor(tonumber(self.size) or 0))
if ask_count > 100 then
ask_count = 100
end
ret.m_Mode = 1
ret.m_AskCount = ask_count
for i = 1, ask_count do
local bag_index = math.max(0, math.floor(tonumber(self.list[i]) or 0))
list[i] = build_bag_list_entry(bag_index, item_list[bag_index])
end
else
local bag_indexes = {}
for pos, item in pairs(item_list) do
if item then
table.insert(bag_indexes, math.max(0, math.floor(tonumber(pos) or 0)))
end
end
table.sort(bag_indexes)
for _, bag_index in ipairs(bag_indexes) do
table.insert(list, build_bag_list_entry(bag_index, item_list[bag_index]))
end
ret.m_Mode = 0
ret.m_AskCount = table.nums(list)
end
ret.list = list
Net:send(ret)
end
这里 unknow_1[1] 的值我最终按下面这个约定发:
5.2.3 CGAskLockObj 统一走二级密码门禁
function request:CGAskLockObj()
if ma_func:get_minor_password() == nil then
ma_func:send_operate_result_msg(define.OPERATE_RESULT.OR_NEED_SETMINORPASSWORD)
return
end
if not ma_func:is_minor_password_is_unlock() then
ma_func:send_operate_result_msg(define.OPERATE_RESULT.OR_NEED_UNLOCKMINORPASSWORD)
return
end
skynet.call(my_scene, "lua", "char_ask_lock_obj", my_obj_id, self)
end
这块是最终的统一入口:
- 没设置密码,直接要求设置。
- 已设置但没认证,直接要求先解锁二级密码。
- 已认证,才允许继续加锁/解锁。
5.3 services/scene/scenecore.lua
5.3.1 背包列表走人物对象统一处理
function scenecore:ask_my_bag_list(whos)
local obj = self.objs[whos]
if not obj then
return {}
end
return obj:get_item_bag_list()
end
5.3.2 最终物品锁/解锁逻辑
***付费内容***
这里的关键点:
- 加锁成功后立刻回物品信息。
- 解锁不是直接
set_lock(false),而是先记录解锁结束时间。
- 解锁发起时立即回一次物品信息,客户端就能进入“解锁中”状态。
5.4 services/scene/obj/human.lua
5.4.1 常量、初始化、心跳刷新
local ITEM_LOCK_UNLOCK_TIME_KEY = "item_lock_unlock_time"
local ITEM_LOCK_UNLOCK_DELAY_SECONDS = 1
local ITEM_LOCK_REFRESH_INTERVAL_MS = 200
local function make_item_lock_guid_key(guid)
if not guid then
return "0:0:0:0"
end
return string.format(
"%d:%d:%d:%d",
tonumber(guid.world) or 0,
tonumber(guid.server) or 0,
tonumber(guid.mask) or 0,
tonumber(guid.series) or 0
)
end
在 human:ctor(data) 里新增初始化:
self.item_unlock_refresh_tick = 0
在人物心跳逻辑里增加这段刷新代码:
if self:has_pending_item_unlock() then
if self.item_unlock_refresh_tick > delta_time then
self.item_unlock_refresh_tick = self.item_unlock_refresh_tick - delta_time
else
self.item_unlock_refresh_tick = ITEM_LOCK_REFRESH_INTERVAL_MS
self:refresh_locked_item_unlock_state(true)
end
else
self.item_unlock_refresh_tick = 0
end
说明:
- 解锁等待时间最终就是
1 秒。
- 有待解锁物品时,每
200ms 刷一次状态。
5.4.2 物品锁状态同步辅助函数
function human:get_item_unlock_delay_seconds()
return ITEM_LOCK_UNLOCK_DELAY_SECONDS
end
function human:get_item_unlock_time_map()
self.game_flag = self.game_flag or {}
local unlock_map = self.game_flag[ITEM_LOCK_UNLOCK_TIME_KEY]
if type(unlock_map) ~= "table" then
unlock_map = {}
self.game_flag[ITEM_LOCK_UNLOCK_TIME_KEY] = unlock_map
end
return unlock_map
end
function human:has_pending_item_unlock()
local unlock_map = self.game_flag and self.game_flag[ITEM_LOCK_UNLOCK_TIME_KEY]
return type(unlock_map) == "table" and next(unlock_map) ~= nil
end
function human:get_item_unlock_time_by_guid(guid)
local unlock_map = self:get_item_unlock_time_map()
local key = make_item_lock_guid_key(guid)
return math.max(0, math.floor(tonumber(unlock_map[key]) or 0))
end
function human:is_item_unlocking_by_guid(guid)
local unlock_time = self:get_item_unlock_time_by_guid(guid)
return unlock_time > os.time()
end
function human:set_item_unlock_time_by_guid(guid, unlock_time)
local unlock_map = self:get_item_unlock_time_map()
unlock_map[make_item_lock_guid_key(guid)] = math.max(0, math.floor(tonumber(unlock_time) or 0))
end
function human:clear_item_unlock_time_by_guid(guid)
local unlock_map = self:get_item_unlock_time_map()
unlock_map[make_item_lock_guid_key(guid)] = nil
end
function human:get_item_unlock_elapsed_seconds_by_guid(guid, now)
local unlock_time = self:get_item_unlock_time_by_guid(guid)
if unlock_time <= 0 then
return 0
end
local delay_seconds = self:get_item_unlock_delay_seconds()
now = now or os.time()
local remaining_seconds = math.max(0, unlock_time - now)
if remaining_seconds <= 0 then
return delay_seconds
end
return math.max(1, delay_seconds - remaining_seconds)
end
function human:sync_item_lock_packet_fields(item, now)
if not item or item:is_empty() then
return 0, 0
end
local goods_protect = item:is_lock() and 1 or 0
local unlock_elapsed = 0
if goods_protect == 1 then
unlock_elapsed = self:get_item_unlock_elapsed_seconds_by_guid(item:get_guid(), now)
end
item.unknow_44 = unlock_elapsed
item.unknow_45 = goods_protect
return unlock_elapsed, goods_protect
end
function human:copy_item_raw_data_with_lock_state(item, now)
if not item or item:is_empty() then
local raw_data = Item_cls.new():copy_raw_data()
raw_data.unknow_44 = 0
raw_data.unknow_45 = 0
return raw_data
end
self:sync_item_lock_packet_fields(item, now)
local raw_data = item:copy_raw_data()
raw_data.unknow_44 = item.unknow_44 or 0
raw_data.unknow_45 = item.unknow_45 or 0
return raw_data
end
function human:copy_item_container_raw_data_with_lock_state(container)
local raw_data = {}
if not container then
return raw_data
end
local now = os.time()
local item_data = container:get_item_data() or {}
for bag_index, item in pairs(item_data) do
if item and not item:is_empty() then
raw_data[bag_index] = self:copy_item_raw_data_with_lock_state(item, now)
end
end
return raw_data
end
这里就是服务端对客户端字段的最终适配:
item.unknow_45 = 是否加锁
item.unknow_44 = 已经过了多少秒
5.4.3 单物品回包统一带锁状态
function human:send_prop_bag_item_info(bag_index)
local scene = self:get_scene()
if not scene then
return
end
local item = self:get_prop_bag_container():get_item(bag_index)
local msg = packet_def.GCItemInfo.new()
msg.bagIndex = bag_index
msg.unknow_1 = item and 0 or 1
msg.item = self:copy_item_raw_data_with_lock_state(item)
msg.bag_type = define.BAG_TYPE.bag
scene:send2client(self, msg)
end
function human:send_bank_bag_item_info(bag_index)
local scene = self:get_scene()
if not scene then
return
end
local item = self:get_bank_bag_container():get_item(bag_index)
if not item then
return
end
local msg = packet_def.GCBankItemInfo.new()
msg.unknow_1 = bag_index
msg.unknow_2 = 0
msg.item = self:copy_item_raw_data_with_lock_state(item)
scene:send2client(self, msg)
end
5.4.4 解锁到期自动清锁
***付费内容***
5.4.5 背包摘要前先同步锁状态
***付费内容***
这一句很关键。
切场景后客户端拉背包摘要时,会先把最新锁状态同步进去。
5.5 services/game/packet.lua
5.5.1 GCMinorPasswd
***付费内容***
这里必须把 DELETEPASSWDTIME 的 uint 正常写出去。
5.5.2 GCMyBagList
packet.GCMyBagList = {
xy_id = packet.XYID_GC_MY_BAG_LIST,
new = function(o)
o = o or {}
setmetatable(o, {__index = packet.GCMyBagList})
o:ctor()
return o
end,
ctor = function(self)
self.m_Mode = 0
self.m_AskCount = 0
self.list = {}
end,
bis = function(self, buffer)
end,
bos = function(self)
local stream = bostream.new()
stream:writeuint(self.m_Mode)
stream:writeuchar(self.m_AskCount)
if self.m_AskCount > 0xF0 then self.m_AskCount = 0xF0 end
for i = 1, self.m_AskCount do
local item = self.list[i]
stream:writeuchar(item.index)
for j = 1, 0x3 do stream:writeuchar(item.unknow[j]) end
packet.ItemGUID.bos(item.guid, stream)
stream:writeuint(item.item_index)
stream:writeuchar(item.count)
for j = 1, 0x3 do stream:writeuchar(item.unknow_1[j]) end
end
return stream:get()
end
}
这个包本身结构不用大改,核心是服务端在 list[i].unknow_1[1] 里把锁摘要塞进去。
6. 我这次最终保留的行为约定
- 客户端不改,所有适配都在服务端完成。
- 物品解锁不是“真正即时解锁”,而是
1 秒后自动解除。
- 加锁/解锁都依赖二级密码认证,不再区分“加锁要设置、解锁要认证”这种不一致逻辑。
- 只要已经设置过二级密码,重新登录后就必须重新认证一次,不能直接操作物品锁。
- 切场景后的图标恢复,靠背包摘要包,不靠悬浮刷新。
7. 回归清单
我后面再看这块时,直接按下面这份清单回归就够了:
- 角色未设置二级密码,打开物品锁后,客户端应进入“设置二级密码”流程。
- 角色已经设置二级密码,但本次登录尚未认证,点加锁/解锁都应提示先认证。
- 二级密码输入错误后,再次点加锁/解锁,仍然应提示认证,不能直接放行。
- 二级密码输入正确后,加锁应立即成功,物品立刻显示锁图标。
- 已锁物品点解锁后,应先显示“解锁中”图标,大约
1 秒后自动变成未锁定。
- 解锁中的物品再次请求解锁,应返回
UNLOCKING,客户端表现保持稳定。
- 切场景后重新打开
ItemCoffer,锁图标应直接正确显示,不需要鼠标移上去触发刷新。
- 背包全量拉取和按格子增量拉取都要正常,不能把锁摘要丢掉。
8. 一句话结论
这套方案本质上就是三件事:
- 二级密码状态拆清楚
- 物品锁状态写进单物品包和背包摘要包
- 解锁保留客户端原流程,但把等待压到 1 秒