前提:已经有 BagMgr(背包)、StoreMgr(仓库)、ConfigMgr、EventMgr、LogMgr、PlayerMgr;物品结构统一,item_uid 唯一。 支持:背包→仓库、仓库→背包;支持单个物品、支持可堆叠物品数量拆分;校验负重、仓库容量、物品锁、限时物品是否允许存仓。
1. Proto 协议(proto3)
ini
// 背包仓库互转请求
message C2S_BagStoreTransfer {
uint64 item_uid = 1; // 物品唯一uid
int32 transfer_type = 2; // 1:背包→仓库 2:仓库→背包
int32 transfer_num = 3; // 转移数量,堆叠物品生效;不可堆叠填1
}
// 返回
message S2C_BagStoreTransferRet {
int32 ret_code = 1; // 0成功,非0失败码
string msg = 2;
uint64 item_uid = 3;
int32 left_num = 4; // 原容器剩余数量
}
2. 错误码定义(放到常量文件)
ini
-- bag_store_const.lua
local const = {
RET_OK = 0,
RET_ITEM_NOT_EXIST = 1001,
RET_NOT_ENOUGH_NUM = 1002,
RET_STORE_FULL = 1003,
RET_BAG_FULL = 1004,
RET_OVER_WEIGHT = 1005,
RET_ITEM_LOCK = 1006,
RET_CANNOT_STORE = 1007, -- 物品配置标记:不可存入仓库
RET_PARAM_ERR = 1008,
}
return const
3. BagStoreTransfer 核心接口(写在 BagMgr 或者单独 bag_store_api.lua)
ini
local bag_store_const = require("bag_store_const")
local BagMgr = require("bag_mgr")
local StoreMgr = require("store_mgr")
local ConfigMgr = require("config_mgr")
local EventMgr = require("event_mgr")
local LogMgr = require("log_mgr")
local M = {}
-- @param player 玩家对象
-- @param item_uid 物品uid
-- @param transfer_type 1 bag->store 2 store->bag
-- @param transfer_num 转移数量
-- @return ret_code, msg, left_num
function M.BagStoreTransfer(player, item_uid, transfer_type, transfer_num)
local pid = player.pid
transfer_num = math.max(1, transfer_num or 1)
-- 参数校验
if not item_uid or (transfer_type ~= 1 and transfer_type ~= 2) then
return bag_store_const.RET_PARAM_ERR, "参数错误", 0
end
local src_container, dst_container
local src_name, dst_name
if transfer_type == 1 then
-- 背包 → 仓库
src_container = BagMgr.GetBagContainer(player)
dst_container = StoreMgr.GetStoreContainer(player)
src_name = "bag"
dst_name = "store"
else
-- 仓库 → 背包
src_container = StoreMgr.GetStoreContainer(player)
dst_container = BagMgr.GetBagContainer(player)
src_name = "store"
dst_name = "bag"
end
-- 源容器查找物品
local src_item = src_container:GetItemByUid(item_uid)
if not src_item then
return bag_store_const.RET_ITEM_NOT_EXIST, "物品不存在", 0
end
local item_cfg = ConfigMgr.GetItemCfg(src_item.item_id)
if not item_cfg then
return bag_store_const.RET_ITEM_NOT_EXIST, "物品配置不存在", src_item.num
end
-- 规则校验
-- 1. 物品上锁不能转移
if src_item.is_lock then
return bag_store_const.RET_ITEM_LOCK, "物品已锁定,无法转移", src_item.num
end
-- 2. 配置:是否允许存入仓库(只在背包->仓库校验)
if transfer_type == 1 and item_cfg.cannot_store == 1 then
return bag_store_const.RET_CANNOT_STORE, "该物品不能存入仓库", src_item.num
end
-- 3. 数量校验
if src_item.num < transfer_num then
return bag_store_const.RET_NOT_ENOUGH_NUM, "物品数量不足", src_item.num
end
-- 目标容器校验
local ok, err = dst_container:CheckCanAddItem(item_cfg, transfer_num)
if not ok then
if transfer_type == 1 then
return bag_store_const.RET_STORE_FULL, "仓库空间不足", src_item.num
else
if BagMgr.CheckAddWeight(player, item_cfg.weight * transfer_num) == false then
return bag_store_const.RET_OVER_WEIGHT, "负重不足", src_item.num
end
return bag_store_const.RET_BAG_FULL, "背包空间不足", src_item.num
end
end
-- === 开始扣源、加目标(原子操作,失败回滚)===
local old_src_num = src_item.num
local sub_ok, sub_left = src_container:SubItemNum(item_uid, transfer_num)
if not sub_ok then
return bag_store_const.RET_NOT_ENOUGH_NUM, "扣数量失败", old_src_num
end
local add_ok, new_item_uid = dst_container:AddItem(item_cfg.item_id, transfer_num, src_item)
if not add_ok then
-- 回滚:把数量加回去
src_container:AddItemNum(item_uid, transfer_num)
return bag_store_const.RET_BAG_FULL, "目标容器添加失败", sub_left
end
-- 流水日志
LogMgr:WriteFlowLog(pid, "bag_store_transfer", {
item_uid = item_uid,
item_id = src_item.item_id,
num = transfer_num,
transfer_type = transfer_type,
src = src_name,
dst = dst_name,
src_left = sub_left,
new_dst_uid = new_item_uid,
})
-- 事件广播:物品转移事件
EventMgr:Fire("EVT_BAG_STORE_TRANSFER", player, {
transfer_type = transfer_type,
item_uid = item_uid,
item_id = src_item.item_id,
transfer_num = transfer_num,
src_left = sub_left,
})
-- 同步容器给客户端
src_container:SyncToClient(player)
dst_container:SyncToClient(player)
return bag_store_const.RET_OK, "转移成功", sub_left
end
return M
4. 消息处理器(注册到网络)
ini
local BagStoreApi = require("bag_store_api")
local bag_store_const = require("bag_store_const")
function OnC2S_BagStoreTransfer(player, msg)
local ret_code, tips, left_num = BagStoreApi.BagStoreTransfer(
player,
msg.item_uid,
msg.transfer_type,
msg.transfer_num
)
player:SendMsg("S2C_BagStoreTransferRet", {
ret_code = ret_code,
msg = tips,
item_uid = msg.item_uid,
left_num = left_num,
})
end
-- 注册
RegisterProtoHandler("C2S_BagStoreTransfer", OnC2S_BagStoreTransfer)
5. 配套容器接口约定(你原有 Bag / Store 容器需要具备)
lua
-- 容器对象接口(背包、仓库统一抽象)
container:GetItemByUid(uid) -- 返回item or nil
container:SubItemNum(uid, num) -- 扣数量,返回 ok,left
container:AddItem(item_id, num, src_item) -- 添加物品,继承限时、属性等
container:AddItemNum(uid, num) -- 增加数量(回滚用)
container:CheckCanAddItem(cfg, num)-- 检查格子是否放得下
container:SyncToClient(player) -- 推送容器数据给客户端
6. 关键业务规则 & 坑点说明
- 原子性:先扣源,再加目标;加失败立刻回滚源数量,防止丢物品
- 继承属性:转移时,限时时间、强化、鉴定、附魔等属性跟随物品;堆叠物品拆分后,新副本继承过期时间
- 负重只校验【仓库→背包】 ;存入仓库不计背包负重
- 仓库上限只做格子数量限制,一般无负重(传奇常规设计)
- 特殊物品:任务物品、绑定物品、活动限时道具,在
cannot_store配置控制是否可存仓 - 上锁物品统一禁止双向转移
- 事件
EVT_BAG_STORE_TRANSFER可被成就、活跃、统计模块订阅
7. 客户端侧流程简要
- 玩家选中物品,点【存入仓库/取出仓库】,填数量
- 发送
C2S_BagStoreTransfer - 收到返回:成功则刷新背包、仓库UI;失败弹出tips