找回密码
 register
搜索
查看: 102|回复: 0

洱海月 物品锁 / 二级密码服务端最终修改笔记

[复制链接]
  • 打卡等级:本地老炮
  • 打卡总天数:533
  • 打卡月天数:22
  • 打卡总奖励:530
  • 最近打卡:2026-06-24 01:45:59
Waylee 发表于 2026-3-31 13:22 | 显示全部楼层 |阅读模式 | Google Chrome | Windows 10

马上注册,查看网站隐藏内容!!

您需要 登录 才可以下载或查看,没有账号?register

×

物品锁 / 二级密码服务端最终修改笔记

这次把物品锁和二级密码这一套功能补完后,我按最终代码整理一份可直接复用的笔记。
目标很明确:客户端不改,只改 Lua 服务端,完全适配客户端现有逻辑。  

1. 最终效果

  1. 已设置二级密码时,点物品加锁/解锁前,必须先做一次二级密码认证。
  2. 未设置二级密码时,服务端返回“需要设置二级密码”,客户端走设置流程。
  3. PASSWDSETUP 只表示“是否已经设置过二级密码”,不再和“本次登录是否已经认证”混在一起。
  4. 解锁流程保留客户端原有表现,但服务端把解锁等待时间改成了 1 秒,基本无感。
  5. 发起解锁后,服务端会把物品当前锁状态和解锁进度下发给客户端,客户端可以显示解锁中的图标和剩余时间。
  6. 切场景后,ItemCoffer 能直接从背包摘要包恢复锁定图标,不需要再靠鼠标移上去触发刷新。
  7. 客户端代码不需要修改。

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

所以最终服务端有两个适配点:

  1. 单物品包里必须把 item.unknow_45item.unknow_44 维护好。
  2. 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] 的值我最终按下面这个约定发:

  • 0:未加锁
  • 1:已加锁
  • 2:解锁中

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

***付费内容***

这里必须把 DELETEPASSWDTIMEuint 正常写出去。

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. 客户端不改,所有适配都在服务端完成。
  2. 物品解锁不是“真正即时解锁”,而是 1 秒后自动解除。
  3. 加锁/解锁都依赖二级密码认证,不再区分“加锁要设置、解锁要认证”这种不一致逻辑。
  4. 只要已经设置过二级密码,重新登录后就必须重新认证一次,不能直接操作物品锁。
  5. 切场景后的图标恢复,靠背包摘要包,不靠悬浮刷新。

7. 回归清单

我后面再看这块时,直接按下面这份清单回归就够了:

  1. 角色未设置二级密码,打开物品锁后,客户端应进入“设置二级密码”流程。
  2. 角色已经设置二级密码,但本次登录尚未认证,点加锁/解锁都应提示先认证。
  3. 二级密码输入错误后,再次点加锁/解锁,仍然应提示认证,不能直接放行。
  4. 二级密码输入正确后,加锁应立即成功,物品立刻显示锁图标。
  5. 已锁物品点解锁后,应先显示“解锁中”图标,大约 1 秒后自动变成未锁定。
  6. 解锁中的物品再次请求解锁,应返回 UNLOCKING,客户端表现保持稳定。
  7. 切场景后重新打开 ItemCoffer,锁图标应直接正确显示,不需要鼠标移上去触发刷新。
  8. 背包全量拉取和按格子增量拉取都要正常,不能把锁摘要丢掉。

8. 一句话结论

这套方案本质上就是三件事:

  • 二级密码状态拆清楚
  • 物品锁状态写进单物品包和背包摘要包
  • 解锁保留客户端原流程,但把等待压到 1 秒
付费看帖
剩余 39% 内容需要支付 66.00 金币 后可完整阅读
支持付费阅读,激励作者创作更好的作品。
您需要登录后才可以回帖 登录 | register

本版积分规则

QQ|雪舞知识库 ( 浙ICP备15015590号-1 | 萌ICP备20232229号|浙公网安备33048102000118号 )|天天打卡

GMT+8, 2026-6-24 05:31 , Processed in 0.066404 second(s), 26 queries .

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表