核心目标:堆叠同类、按规则排序、空位后置、锁定物品位置不动 ,不修改物品属性(强化、时效、绑定、唯一性) 前置前提:背包是数组结构
items[pos],pos 从 1~最大格子数;格子分【锁定格子】和【普通格子】
一、整体流程总览
markdown
输入:player玩家对象 → 取出背包数据
1. 分离锁定物品 和 待排序物品(锁定物品原位保留,不参与排序)
2. 对待排序物品做堆叠合并(满足堆叠条件才合并)
3. 对合并后的物品列表按策划排序规则排序
4. 重建背包格子:先放回锁定物品,再依次往空位填入排序后的物品
5. 落地存库 + 全量同步背包给客户端
6. 返回结果码
二、分步详细逻辑拆解
步骤1:遍历背包,分离锁定物品 & 可排序物品
遍历每一个背包格子(1 ~ 背包最大格子)
-
如果格子有物品,并且
item.is_lock == true:放入lock_map,key=格子位置pos,value=物品,这个格子后续不动锁定物品一般:穿戴中的装备、绑定锁、限时活动锁定道具、交易挂单物品
-
如果格子有物品、非锁定 :加入待排序列表
sort_list -
空格子直接跳过
判断分支:如果待排序列表是空,直接返回【无需整理】,提前return,节省性能。
步骤2:物品堆叠合并(最容易出bug的环节)
堆叠判定条件必须全部同时满足:
-
item_id 相同
-
bind(绑定标记)相同
-
expire_time(过期时间)相同
理由:绑定/非绑定不能堆一起;不同到期时间的时效道具不能堆一起
拿到物品配置表 ConfigMgr:GetItemCfg(item_id),读取该物品的 stack_max(最大堆叠数量)
-
不可堆叠物品(stack_max = 1):直接进入排序列表,跳过合并
-
可堆叠物品:
- 用拼接key
item_id|bind|expire_time做字典分组 - 同key分组内,往已有物品里面加数量,加到 stack_max 上限为止
- 填满之后剩下的数量生成新物品条目
- 用拼接key
示例: 物品A,最大堆叠99,已有60,新来50 → 原有变成99,剩余11,新建一条物品A(11)
步骤3:物品排序(策划可配优先级)
合并完成后,对 new_sort 数组执行 table.sort,排序优先级:
-
优先按
item_type物品类型升序(策划配置:装备→消耗→材料→其他) -
同类型下,按 item_id 升序(也可以配品质、等级,按需改)
这里是配置驱动,想改成【品质优先】只需要修改sort回调
步骤4:回填格子(重点!锁定物品位置优先保留)
-
先把背包 items 数组清空
-
把 lock_map 里面锁定物品,原样放回原来的 pos 位置
csslock_map[5] = 屠龙 → 背包第5格固定放屠龙,不会被挤走 -
从头遍历所有格子 1~max_grid,找到空位依次填入排序好的物品
- 遇到已经有锁定物品的格子:跳过
- 遇到空位:取排序列表下一个物品填入
- 排序列表填完,剩下所有格子保持为空
✅ 效果:锁定物品原地不动;所有可排序物品挤到锁定物品之后的空位,全部空格子集中到背包末尾
步骤5:数据持久化 + 客户端同步
- 把新的背包数据写入玩家存档(DbMgr落地)
- 调用
SyncBagAll(player),下发全量背包协议给客户端,刷新界面 - 写操作日志(GM调用写GM日志;玩家自助整理写普通操作日志)
三、关键业务约束(边界规则)
- ❌ 整理不会改变:物品uid、强化、幸运、鉴定属性、绑定、过期时间、数量
- ❌ 不参与排序:交易中、摆摊上架中的物品(上层加前置判断拦截)
- ❌ 唯一性物品(不可堆叠)不会合并
- ✅ 只移动格子位置,物品本身数据不变
- ✅ 背包最大格子上限不变,整理不会扩容
四、两种模式区分
- 玩家自助整理(C2S消息) :玩家点击按钮触发,权限无限制,只写普通操作日志
- GM指令整理 bag_sort:运营后台调用,增加角色存在校验、GM审计日志;可扩展高危参数强制忽略锁定
五、对比【仓库整理 StoreMgr:SortStore】
逻辑几乎完全复用这套算法,区别:
- 数据源换成个人仓库/行会仓库数据
- 同步对象:个人仓库只同步本人;行会仓库整理后同步给行会全部在线成员
六、常见坑清单(上线必查)
- 堆叠key没带上过期时间 → 不同时效道具合并,导致批量过期bug
- 锁定物品回填顺序错误 → 锁定物品被挤位置,玩家投诉
- 堆叠的时候超过 stack_max 上限
- 整理时玩家正在交易/摆摊,物品位置变动引发交易异常
- 只改内存数据,忘记持久化,重登变回原来顺序