XtGameKit/Doc/ROK-Bag-Spec.md

849 lines
47 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ROK 背包/道具系统业务规则1:1 迁移依据)
> 来源:`E:\Game\gmd\ROK` 服务端 Lua`ItemLogic.lua` / `Item.lua` proxy+ 客户端 C#`BagProxy.cs` / `BagMediator.cs` / `PlayerCmd.cs` / `ItemConfig.cs` / `ItemEnum.lua` / `Common.sproto` / `ErrorCode.cs`
> 用途:作为 Survivors 工程 `cn.etetet.bag` 包实现的业务依据,**不直接复制源代码**,只摊开业务规则与数据结构
> 调研时间2026-05-27
> 关键约定:表名加 `s_` 前缀(如 `s_Item`)的为服务端 ConfigEntity客户端对应 `ItemDefine`、`ItemPackageDefine` 等
---
## 1. 整体业务范围
| # | 子模块 | 一句话定位 |
|---|---|---|
| 1 | 通用道具增/删 | `ItemLogic:addItem / delItem / delItemById / checkItemEnough`,所有业务发奖/扣道具的唯一入口 |
| 2 | 道具分类 | 服务端 `Enum.ItemType`6 类HEAD 不入背包);客户端 `BagItemType`5 个 Tab |
| 3 | 堆叠规则 | 非装备同 `itemId` 合并到同一 `itemIndex`;装备每件独立 `itemIndex``overlay=1` |
| 4 | 道具使用 | `Item.lua:response.ItemUse`,按 `s_Item.itemFunction` 分支 if/elseif 处理(**无独立脚本表** |
| 5 | 资源类道具兑换 | `Item.lua:response.ItemChangeResource`,把"资源类道具"换成等额玩家货币(金币/木料/粮食/VIP/行动力) |
| 6 | 礼包/选包奖励 | `ItemLogic:getGroupPackage / giveReward / getItemPackage` + `s_ItemPackage` + `s_ItemRewardChoice` |
| 7 | 新道具/红点标记 | **服务端不存**,全部用 `PlayerPrefs` 在客户端持久化(按 `rid + itemIndex` |
| 8 | 头像框解锁 | `Enum.ItemType.HEAD` 类型道具**不进背包**,直接走 `RoleLogic:unlockRoleHead` |
| 9 | 资源货币 | 粮/木/石/金/宝石/行动力/VIP/远征币等**作为 Role 字段**存储,不进 Item 表(详见 §5.7 |
| 10 | 同步推送 | 唯一同步协议 `Item_ItemInfo`30101增量推 |
| 11 | 创角初始道具 | `s_Config.initialItemType + initialItemNum` 硬编码 8 个道具 |
| 12 | 资源阈值告警 | `s_GameWarning.num` 单道具数量超阈值时 `Common.sendResourceAlarm` 给运营 |
| 13 | 日志审计 | 每次增删都 `LogLogic:itemChange(rid, iggid, itemId, changeNum, oldNum, newNum, logType, logType2)` |
> ⚠️ ROK 工程中**没有 `ItemScript.lua` 实质实现**——同名文件只剩 3 个空函数。所有"道具使用脚本"逻辑都在 `Item.lua` proxy 的 `ItemUse` 函数里用 `if/elseif (sitem.itemFunction)` 巨型分支硬编码。
---
## 2. 道具分类 BagItemType
### 2.1 服务端 `Enum.ItemType``ItemEnum.lua`
| 名称 | 值 | 含义 | 是否入背包 | 备注 |
|---|---|---|---|---|
| `RESOURCE` | 1 | 资源(金/木/石/粮/宝石/VIP/行动力类道具袋) | ✅ | 通过 `Item_ItemChangeResource` 兑换玩家货币字段 |
| `SPEED` | 2 | 加速(建筑/训练/研究/治疗/通用 5 类) | ✅ | 在各功能界面用,背包 Tab2 |
| `GAIN` | 3 | 增益(采集加成/护盾/反侦察/部队扩编等) | ✅ | 大多对应 City BuffTab3 |
| `EQUIP` | 4 | 装备(武器/头盔/胸甲/手套/裤/鞋/饰品 + 装备材料 + 图纸) | ✅ | 每件独立 itemIndex |
| `OTHER` | 5 | 其他(改名卡、迁城卡、经验书、英雄雕像、钥匙等) | ✅ | 杂项 Tab |
| `HEAD` | 6 | 头像框 | ❌ | 进 `addItem` 后**直接走 `RoleLogic:unlockRoleHead`** 返回空 syncInfo |
**itemId 编码约定**:客户端 `BagProxy.GetItemTypeById(itemId) = (int)itemId / 100000000`,即 itemId 最高位决定 type。
- 1xxxxxxxx → RESOURCE`101010001`
- 2xxxxxxxx → SPEED (如 `201010010`
- 3xxxxxxxx → GAIN
- 4xxxxxxxx → EQUIP (如 `401010001` 材料、`401020001` 装备)
- 5xxxxxxxx → OTHER (如 `502030004` 改名卡、`502070001` 经验书)
### 2.2 客户端 `BagItemType``BagProxy.cs`
| 名称 | 值 | UI Tab | 与服务端关系 |
|---|---|---|---|
| `Resource` | 1 | 资源 | == ItemType.RESOURCE |
| `Speedup` | 2 | 加速 | == ItemType.SPEED |
| `Boost` | 3 | 增益 | == ItemType.GAIN |
| `Equipment` | 4 | 装备 | == ItemType.EQUIP |
| `Other` | 5 | 其他 | == ItemType.OTHER |
| `Icon` | 6 | 不显示 | == ItemType.HEAD实际客户端 BagPanel 只 5 个 Tab |
> `BagMediator` 实测**只显示 5 个 Tab**1-5HEAD 类型在 `for i = 1; i < 6` 循环里被略过。
### 2.3 `Enum.ItemSubType`46 个细分子类型,关键值)
`ItemEnum.lua` 中所有 subType 都是 5 位数:
| 段位 | 含义 | 示例 |
|---|---|---|
| `101xx` | RESOURCE 子类 | 10101=VIP, 10102=GOLD, 10103=STONE, 10104=WOOD, 10105=GRAIN |
| `201xx` | SPEED 子类 | 20101=全加速, 20102=建筑, 20103=训练, 20104=研究, 20105=治疗 |
| `301xx` | GAIN 子类 | 30101-30111 各种产出/Buff |
| `401xx` | 装备材料 | 40101 羽毛 / 40102 皮革 / 40103 铁石矿 / 40104 兽骨 / 40105 水晶 / 40106 丝绸 / 40107 乌木 |
| `402xx` | 装备本体 | 40201=武器/40202=头盔/40203=胸甲/40204=手套/40205=裤/40206=鞋/40207=饰品 |
| `403xx` | 装备图纸 | 40301-40307 对应 7 个部位 |
| `501xx-509xx` | OTHER 杂项 | 50101=经验书 / 50201=改名卡 / 50202=工人招募 / 50203=迁城 / 50204=钥匙 / 50205=文明更换 / 50206=天赋重置 / 50207=建筑升级 / 50208=行动力 / 50301-50304=升星材料 / 50305=技能材料 / 50901=英雄雕像 |
### 2.4 每类道具的堆叠/唯一性特征
| 类 | 堆叠 | maxStack | 唯一字段 |
|---|---|---|---|
| 装备 (`isEquipItem(subType) == true`,即 40201/40202/40203/40204/40205/40206/40207) | ❌ 每件独立 | 1强制 | `itemIndex` + `exclusive`(专属英雄)+ `heroId`(穿戴者) |
| 其他全部 | ✅ 无限叠 | **代码中无 maxStack 限制**`overlay` int 累加) | 只按 itemId 合并到同一 itemIndex |
> ⚠️ **没有 maxStack 限制**`addItem` 只在装备分支才循环创建多条;其他都是 `overlay = oldOverlay + itemNum` 直接累加。配置表 `ItemDefine` 也**没有 maxStack 字段**。
---
## 3. Item 数据模型
### 3.1 `.ItemInfo` sproto`Common.sproto` L308
| 字段 | 类型 | 含义 |
|---|---|---|
| `itemIndex` | integer | **背包槽位**,自增正整数,玩家维度唯一(`getFreeItemIndex` 找最小空位) |
| `uniqueIndex` | integer | 唯一索引(实测 Lua 中未使用,可能为后续扩展预留) |
| `itemId` | integer | 道具配置 ID关联 `s_Item.ID` |
| `overlay` | integer | 叠加数量(装备恒为 1 |
| `exclusive` | integer | 装备专属标志,`0` 不专属;非 0 时表示绑定英雄 ID影响装备分解和穿戴 |
| `heroId` | integer | 当前穿戴此装备的英雄 ID0 = 未穿戴) |
### 3.2 服务端存储(`MSM.d_item[rid]` snax service
`MSM.d_item[rid].req` 提供的操作:
- `Get(rid)` → 全部 items dict
- `Get(rid, itemIndex)` → 单条
- `Get(rid, itemIndex, fields)` → 部分字段
- `Set(rid, itemIndex, field, value)` → 改单字段
- `Add(rid, itemIndex, itemInfo)` → 新增
- `Delete(rid, itemIndex)` → 删除
`Enum.Item` 字段名常量:`itemIndex / itemId / overlay / exclusive / heroId`(即 ItemInfo 5 字段)
### 3.3 客户端缓存(`BagProxy.cs`
```
public Dictionary<Int64, ItemInfoEntity> Items; // itemIndex → 道具实体
private List<ItemInfoEntity> m_itemList; // 顺序列表
private Dictionary<Int64, int> m_itemNumMap; // itemId → 累计数量(懒计算,-1 表示需重算)
private Dictionary<long, ItemInfo> m_materialItemInfos; // 装备材料分组
private Dictionary<long, ItemInfo> m_equipItemInfos; // 装备本体分组
private Dictionary<int, Dictionary<long, long>> m_reddotRecord; // type → (itemIndex → 新增数量)
private Dictionary<int, long> m_reddotTotalDic; // type → 红点总数缓存
```
派生类:
- `ItemInfo` 基础:`ItemIndex / ItemID / ItemNum / ItemType / HeroID`
- `EquipItemInfo : ItemInfo` 额外:`Exclusive / Order / Group`(来自 `EquipDefine`
- `MaterialItem : ItemInfo` 额外:`MaterialDefine` + `MaterialType`Material / Equip / Drawing / DrawingMaterial
### 3.4 新道具标记存储(**纯客户端 PlayerPrefs**
Key 格式:`{rid}/itemIndex:{itemIndex}` → int
| 值 | 含义 |
|---|---|
| 不存在 | 还没在客户端被看到 |
| `1` | 新道具(红点亮) |
| `-1` | 旧道具(已被标记/查看过) |
| `0` | 已清除 |
特殊情况:装备 `heroId != 0`(已穿戴)→ 强制 `SetLocalItemToOld`,不亮红点。
---
## 4. 配置表清单
### 4.1 `ItemDefine``ItemConfig.cs`,对应服务端 `s_Item`
| 字段 | 类型 | 含义 |
|---|---|---|
| `ID` | int | 主键,道具 ID同 itemId |
| `l_nameID` | int | 道具名称语言包 ID |
| `l_tipsID` | int | tips 道具名称语言包 ID |
| `subType` | int | 子分组标签(`Enum.ItemSubType`5 位数) |
| `type` | int | 功能分组(`Enum.ItemType`1-6 |
| `l_typeDes` | int | 功能分组语言包 |
| `typeGroup` | int | 类型组装备页用1=Material 2=Equip 3=Drawing 4=DrawingMaterial |
| `itemIcon` | string | 道具图标资源路径 |
| `lv` | int | 使用等级要求(实测大部分==0 |
| `quality` | int | 品质 1-5白/绿/蓝/紫/橙,对应 `Enum.ItemQualityType` |
| `batchUse` | int | 是否可批量使用(`Enum.ItemBatchUse`0/1 |
| `l_buttonDes` | int | 按钮文字语言包(`<1` 表示无按钮) |
| `itemFunction` | int | **道具功能类型**,对应 `Enum.ItemFunctionType`(见 §4.2 |
| `data1` | int | 功能参数 1数值如 VIP 加多少点/兑换比例) |
| `data2` | int | 功能参数 2关联 ID`s_ItemPackage` group ID / `s_ItemRewardChoice` ID / cityBuff ID |
| `l_desID` | int | 道具描述语言包(`string.format(text, desData1, desData2)` |
| `desData1` | int | 描述参数 1 |
| `desData2` | int | 描述参数 2 |
| `l_topID` | int | 图标顶部信息语言包(`<1` 不显示) |
| `topData` | int | 顶部信息参数 |
| `get` | List\<int\> | 获得途径列表,关联 `s_ItemGet` |
| `shortcutPrice` | int | 快捷使用价格(钻石购买后直接使用,用于行动力/VIP 道具) |
| `shopPrice` | int | 商城道具价格 |
| `redDotPrompt` | int | 是否要红点提示0/1 |
| `rank` | int | 显示排序(背包内同 type 按 rank 升序rank 相同按 itemId 升序) |
> ⚠️ **没有 `maxStack` / `bind` / `tradeable` 字段**(与同名 IGG 老项目 `item.json` 形成对比,那个有 `MaxNum / CanTrade / IsDecompose`,但不是 ROK 用的)。
### 4.2 `Enum.ItemFunctionType` 枚举(**核心扩展点**
| 值 | 名称 | 服务端处理(`Item.lua:ItemUse` | 客户端处理(`BagMediator.BtnOperate` + `PlayerCmd` |
|---|---|---|---|
| 0 | NOT_USE | 直接返回 `ITEM_NOT_USE` 错误 | 无按钮 |
| 1 | OPEN_ITEMPACKAGE | 调用 `getItemPackage(data2)` 发奖(`data2` = s_ItemPackage group | 直接发请求 |
| 2 | CHOOSE_ITEMPACKAGE | 校验 `s_ItemRewardChoice[data2][id]`,按选择 ID 发对应 reward | 弹 `BagGiftOpenView`ShowType=1让玩家选 |
| 3 | RECYCLE | 同 OPEN_ITEMPACKAGE用 data2 兑换包) | 弹 `BagGiftOpenView`ShowType=2确认兑换 |
| 4 | CITY_BUFF | `RoleLogic:addCityBuff(data2)` 加 buff | `m_cityBuffProxy.SendUseItem(data2, 1, cb)` |
| 5 | VIP | `RoleLogic:addVip(data1 * itemNum)` | 走 VIP 满级校验后直接发 |
| 6 | ACTION_FORCE | `RoleLogic:addActionForce(data1 * itemNum)` | 直接发 |
| 10 | KINGDOM_MAP | 调 `DenseFogLogic:openNearDenseFog(pos)`,开雾失败则 `getItemPackage(data2)` 补偿 | 检测迷雾,否则弹 `OpenFogShow` 让玩家选位置;返回奖励则弹 `ItemCollection` |
| 12 | SUMMON_MONSTER | `MSM.MonsterSummonMgr.req.summonMonster(data2)` 召唤野怪 | 关闭背包,相机飞向召唤点,播放召唤特效 |
| 13 | SECONDE_QUEUE | 工人小屋:满建造队列则发 `data2` 补偿;否则 `BuildingLogic:unlockQueue(workQueueTime)` | 根据 `result.status` 分别提示成功/延长/补偿 |
| 14 | TRAIN_NUM | 预备部队增容:`itemAddTroopsCapacity = data1``itemAddTroopsCapacityCount += itemNum` | 若已有不同档位 `data1` 则二次确认 |
| 35 | LEAGUE_POINTS | 校验入盟后 `GuildLogic:addGuildCurrency(leaguePoints, data1*num)`,返回 `rewardInfo.leaguePoints` | 直接发 |
| 7 | (客户端独有)跳转界面 | — | `SystemOpen.IsCanOpenByUiId(data1)` + `OpenUI2(data1, data2)` |
| 17 | (客户端独有)暂未实现 | — | Tip 提示"暂未开放" |
| 30 | (客户端独有)通用迁城 | — | 弹 `MoveCity` 界面 |
| 31 | (客户端独有)随机迁城 | — | 二次确认后发 `Map_MoveCity{type=4}` |
> 关键约束:服务端枚举值与客户端的 `BtnOperate` 分支**必须一一对应**,新增 itemFunction 时两边都要改。
### 4.3 其他 Item 相关 Define
| 客户端类 | 服务端表 | 用途 |
|---|---|---|
| `ItemPackageDefine` | `s_ItemPackage` | 奖励组(按 randomGroup 分组随机,含 type/typeData/odds/number/civilization_limit/numberStep_lv/numberStep_increment/numberFloat_min/numberFloat_max |
| `ItemRewardChoiceDefine` | `s_ItemRewardChoice` | 选包奖励配置(按 data2 关联,每条 `{id, reward}` 表示一个选项对应的 itemPackage group |
| `ItemHeroDefine` | `s_Hero` 关联 | 英雄相关道具映射(英雄碎片 → 英雄 ID |
| `ItemGetDefine` | `s_ItemGet` | 获得途径配置(关联 `ItemDefine.get[]` |
| `ItemPackageShowDefine` | `s_ItemPackageShow` | 礼包展示配置 |
| `ItemPlayerHeadDefine` | `s_ItemPlayerHead` | 头像框配置 |
| `EquipDefine` | `s_Equip` | 装备本体配置(含 makeMaterial / makeMaterialNum / order / group |
| `EquipMaterialDefine` | `s_EquipMaterial` | 装备材料配置(含 mix / split / mixCostNum 用于合成分解) |
### 4.4 配置条目数
ROK 客户端使用 SQLite/Bin 序列化(`Assets/StreamingAssets/Config/Bin/Item.bin` ≈ 87KB**没有可读的 item.json**。`gmd/Unity/Assets/Bundles/Config/item.json` 是另一个老项目残留(字段为 `MaxNum / CanTrade / FunctionID` 等,与 ROK 完全不同结构),**不属于本次调研范围**。
实际条目数无法从源代码直接计数,但根据 itemId 编码5 个一级类 × 数百子分类)+ initialItemType 已出现 `502070003``201010013` 等量级,**估算总条目数在 200-500 范围**。
### 4.5 `ItemPackageType` 奖励类型枚举(`s_ItemPackage.type`
| 值 | 名称 | typeData 含义 |
|---|---|---|
| 0 | NONE | 空 |
| 100 | CURRENCY | `Enum.CurrencyType`food/wood/stone/gold/denar/actionForce/vip/expeditionCoin/individualPoints/leaguePoints/activityActivePoint |
| 200 | ITEM | `s_Item.ID`(普通道具) |
| 300 | SOLDIER | `s_Arms.ID` |
| 400 | HERO | `s_Hero.ID` |
| 500 | SUB_ITEM_TYPE | `Enum.ZeroEmptyType.SUB_ITEM_TYPE` 关联表 ID再从子表 Random 出具体 itemId |
| 600 | GUILD_GIFT | 联盟礼物类型 |
### 4.6 `CurrencyType``OtherEnum.lua` L101**不属于 Item是 Role 字段**
| 值 | 名称 | 含义 |
|---|---|---|
| 100 | food | 粮食 |
| 101 | wood | 木材 |
| 102 | stone | 石料 |
| 103 | gold | 金币 |
| 104 | denar | 宝石(钻石) |
| 105 | actionForce | 行动力 |
| 106 | individualPoints | 联盟个人积分 |
| 107 | leaguePoints | 联盟积分 |
| 108-111 | allianceFood/Wood/Stone/Gold | 联盟资源 |
| 112+ | vip / expeditionCoin / activityActivePoint | VIP 点 / 远征币 / 活动积分 |
---
## 5. 业务规则详细
### 5.1 添加道具 `ItemLogic:addItem(args)`
**入参** `args` 表:`{rid, itemId, itemNum=1, exclusive=nil, noSync=nil, eventType=nil, eventArg=nil}`
**流程**`ItemLogic.lua` L69-164
1.`s_Item:Get(itemId)`,不存在 → `LOG_ERROR` 返回 nil
2. **特殊分支**:若 `sitemInfo.type == HEAD`,调 `RoleLogic:unlockRoleHead(rid, itemId)` 解锁头像框后**直接返回 `{}`**,不进背包
3. **是否堆叠**
- **装备类**`isEquipItem(subType)` = subType 在 ARMS/HELMET/BREASTPLATE/GLOVES/PANTS/ACCESSORIES/SHOES 中)→ 跳过查找,必新增
- **其他**:遍历 `getItem(rid)` 查同 `itemId` 的现有 itemIndex
4. **堆叠分支**:找到现有同 itemId → `overlay = oldOverlay + itemNum``Set(rid, itemIndex, overlay, newNum)`
5. **新建分支**
- 非装备:`getFreeItemIndex(rid)` 取最小空 index新建一条 `{itemId, overlay=itemNum, rid, itemIndex, exclusive, heroId=0}``Add(rid, itemIndex, itemInfo)`
- 装备:**循环 itemNum 次**,每次 `getFreeItemIndex` + `overlay=1` 单独建条目(**装备永远不堆叠**
6. 同步:`syncItem(rid, nil, syncItemInfo, true)``Item_ItemInfo`
7. 日志:若 `eventType` 非空,调 `LogLogic:itemChange({rid, iggid, itemId, changeNum, oldNum, newNum, logType, logType2})`
8. 资源阈值告警:若 `s_GameWarning:Get(itemId, "num")` 存在且 `newNum > 阈值``Common.sendResourceAlarm(rid, itemId, newNum)`
**返回值**`syncItemInfo` dictitemIndex → 新建/更新后的 ItemInfo
**触发新道具标记**:服务端**不主动标**,客户端 `BagProxy.UpdateItemInfo` 收到推送时根据 `m_isFirstGetItemInfo == false`(即非首次登录) + `redDotPrompt == 1` + isNewItem 或 overlay 增加,则在 `m_reddotRecord[type][itemIndex]` 写入新增数量。
**协议**`Item_ItemInfo` (30101) 增量推送
**错误码**addItem 自身只 LOG_ERROR不返回错误码
---
### 5.2 移除道具
#### 5.2.1 按 itemIndex 移除:`ItemLogic:delItem(rid, itemIndex, itemNum, noSync, logType, logExtraType)`
**流程**`ItemLogic.lua` L167-198
1. `getItem(rid, itemIndex)` 取道具实体
2. **数量判定**
- `overlay <= itemNum` → 整条删除:`MSM.d_item[rid].req.Delete(rid, itemIndex)``newNum = 0`
- `overlay > itemNum` → 扣减:`Set(rid, itemIndex, overlay, overlay - itemNum)`
3. 同步推送 `{[Enum.Item.overlay] = newNum}`
4. 日志:`LogLogic:itemChange({rid, iggid, logType, logType2, itemId, changeNum, oldNum, newNum})`
**返回值**`syncItems` dict
> ⚠️ `delItem` **不校验** `itemNum > overlay`,传过量会直接清零(业务方需自己先 `checkItemEnough`)。
#### 5.2.2 按 itemId 移除:`ItemLogic:delItemById(rid, itemId, itemNum, noSync, logType, logExtraType)`
**流程**`ItemLogic.lua` L241-250遍历所有 items 找第一个匹配 itemId转发到 `delItem`
> ⚠️ 业务侧约束:因为非装备同 itemId 必合并在同一 itemIndex所以只有一条匹配**装备类不应该用 delItemById**(每个 itemIndex 独立)。
#### 5.2.3 数量检查:`ItemLogic:checkItemEnough(rid, itemId, itemNum)`
**返回**`(boolean, overlay)`
**特点**:因为非装备无限叠加 + 必合并,**只检查第一条匹配的 itemId.overlay 是否 >= itemNum**。
**装备移除前是否校验穿戴**`delItem` 本身**不校验** `heroId != 0`;具体校验由调用方在装备分解逻辑里处理(`Build_DecompositionEquipment` proxy handler 校验,本次未深入展开)。
**RPC**:无独立移除 RPC移除都从其他业务里调用如使用消耗、装备分解、资源兑换扣道具等
**错误码**delItem 不报错);上层有 `ITEM_NOT_ENOUGH(8000)` `ITEM_NOT_EXIST(8002)`
---
### 5.3 通用奖励/消耗结构
#### 5.3.1 数据结构
| 名称 | 结构 |
|---|---|
| 奖励项sproto | `.RewardItem { itemId, itemNum }` |
| 完整奖励信息 | `.RewardInfo { food, wood, stone, gold, denar, items:*RewardItem, soldiers:*SoldierInfo, groupId, actionForce, guildGifts, heros:*Heros, expeditionCoin, guildPoint, vip, leaguePoints, activityActivePoint }` |
| 消耗项sproto | `.Items { itemId, itemNum }`(用于商店购买/合成消耗) |
> 注意ROK **没有统一的 `CostInfo`**,消耗信息分散在各 RPC 的 request 里(如 `Shop_BuyShopItem.itemId/itemNum`)。
#### 5.3.2 批量接口
| 函数 | 用途 |
|---|---|
| `ItemLogic:giveReward(rid, rewards, groupId, noSync, noHeroShow, block, isBuyGift, packageNameId)` | **核心发奖入口**,遍历 RewardInfo 各字段调对应 `RoleLogic:add*` / `addItem` / `ArmyTrainLogic:addSoldiers` / `HeroLogic:addHero` |
| `ItemLogic:getItemPackage(rid, groupId, noAdd, noSync, noHeroShow, noMerge, block, isBuyGift, openNum, mergeHero, packageNameId)` | **打开礼包**:调 `getGroupPackage` 计算实际奖励,再调 `giveReward` 发放 |
| `ItemLogic:getGroupPackage(rid, groupId, noMerge, openNum, mergeHero)` | **奖励计算**:按 randomGroup 加权随机 + 等级增量 + 浮动比例,输出合并后的 reward 结构 |
| `ItemLogic:mergeReward(rawReward, addReward)` | **奖励合并**:把两个 RewardInfo 累加货币求和items/soldiers/heros 按 ID 合并) |
| `ItemLogic:checkItemEnough(rid, itemId, itemNum)` | 单道具数量校验 |
#### 5.3.3 事务性
**ROK 没有事务**`giveReward` 遍历各字段顺序发放,任一中途失败:
- 货币 `add*` 失败:仅 LOG_ERROR已发的不会回滚
- `addItem` 失败:同上
- **业务方需保证 reward 结构合法**(数量 > 0 / itemId 存在),失败属于配置错误
业务实现"先检查再扣除"模式:典型如 `Shop_BuyShopItem`
1. `checkItemEnough(rid, costItemId, costNum)` → false 返回 `ITEM_NOT_ENOUGH`
2. `delItemById(rid, costItemId, costNum)` 扣除
3. `addItem(rid, productItemId, productNum)` 发放
**约束**:检查/扣除/发放之间**没有锁**(单线程 Lua 协程模型actor 内串行执行天然安全;跨 actor 调用需 RPC 等待)
---
### 5.4 道具使用 `Item.lua:response.ItemUse(msg)`
#### 5.4.1 触发流程(客户端)
```
玩家点击道具 → BagMediator.RefreshItemDetail 显示按钮
↓ 玩家点"使用"
BagMediator.BtnOperate 按 itemFunction 分支:
├─ itemFunction == 2 → 弹 BagGiftOpenView选包用户选完调 Send(itemIndex, num, selectId)
├─ itemFunction == 3 → 弹 BagGiftOpenView兑换确认用户确认后 Send(itemIndex, num, 0)
├─ itemFunction == 4 → cityBuffProxy.SendUseItem(data2) → Send(itemIndex, 1, 0)
├─ itemFunction == 10 → 检测迷雾,否则弹 OpenFogShow 选坐标后发请求(含 pos
├─ itemFunction == 14 → 若已有不同档位预备部队 buff二次确认
├─ itemFunction == 31 → 二次确认后发 Map_MoveCity非 Item_ItemUse
└─ 其他 → Send(itemIndex, num, 0) → 发 Item_ItemUse.request
```
`Send` 实现:
```csharp
var sp = new Item_ItemUse.request {
itemIndex = itemIndex,
itemNum = itemNum,
id = id, // >0 时才赋值,给 CHOOSE_ITEMPACKAGE 选包用
};
AppFacade.GetInstance().SendSproto(sp);
```
#### 5.4.2 服务端校验流程
`Item.lua:response.ItemUse` 完整流程L82-221
1. **参数校验**`itemIndex` / `itemNum` 必填 → 否则 `ITEM_ARG_ERROR`
2. **道具存在**`getItem(rid, itemIndex)` → 空则 `ITEM_NOT_EXIST`
3. **可用性**`sitem.itemFunction == NOT_USE(0)``ITEM_NOT_USE`
4. **批量限制**`itemNum > 1 && sitem.batchUse == NO(0)``ITEM_NOT_BATCH_USE`
5. **业务前置**
- LEAGUE_POINTS未入盟 → `ITEM_NOT_JOIN_GUILD`
6. **数量足够**`itemInfo.overlay < itemNum``ITEM_NOT_ENOUGH`
7. **召唤怪物分支**SUMMON_MONSTER 时先 `MonsterSummonMgr.req.summonMonster` 校验成功,失败 → `ITEM_SOMMON_MONSTER_FAILED`
8. **选包校验**CHOOSE_ITEMPACKAGE 时 `s_ItemRewardChoice[data2][id]` 不存在 → `ITEM_PACKAGEID_NOT_EXIST`
9. **扣除道具**`delItem(rid, itemIndex, itemNum, nil, USE_BAG_ITEM_COST_ITEM)`
10. **任务进度**`TaskLogic:updateItemUseTaskSchedule(rid, nil, itemNum, sitem)`
11. **按 itemFunction 分发**
- OPEN_ITEMPACKAGE / RECYCLE → `rewardId = data2`
- CHOOSE_ITEMPACKAGE → `rewardId = s_ItemRewardChoice[data2][id].reward`
- CITY_BUFF → `addCityBuff(data2)` 直接返回
- VIP / ACTION_FORCE / LEAGUE_POINTS → 调 `add*` 直接返回
- KINGDOM_MAP → `openNearDenseFog(pos)`,开雾失败则 `getItemPackage(data2)` 补偿,含 rewardInfo 返回
- SECONDE_QUEUE → 满队列返补偿包,否则 `unlockQueue`
- TRAIN_NUM → 修改 `itemAddTroopsCapacity` + `Count`syncSelf 推送
12. **最终发奖**:若 `rewardId > 0``getItemPackage(rid, rewardId, openNum=itemNum)` 发放
13. **响应**`{itemId, itemNum, rewardInfo, objectIndex(召怪用), pos(召怪位置), status(队列状态)}`
#### 5.4.3 选择数量使用 / 选择目标使用
| 模式 | 实现 |
|---|---|
| **批量数量** | request.itemNum 字段;服务端 `batchUse == YES` 时支持;`getItemPackage` 内部 `openNum = itemNum` 表示开 N 次抽奖 |
| **选包 ID** | request.id 字段;只在 CHOOSE_ITEMPACKAGE 时传,对应 `s_ItemRewardChoice[data2][id].reward` 之 itemPackage group |
| **选择坐标** | request.pos 字段;只用于 KINGDOM_MAP开迷雾选位置 |
| **选择英雄** | **不支持**——经验书等给英雄加经验的道具走的是 `Hero_ExchangeHeroItem`602等独立 RPC**不走 ItemUse** |
#### 5.4.4 涉及 RPC
| RPC | ID | request | response |
|---|---|---|---|
| `Item_ItemUse` | 852 | itemIndex, itemNum, id(选包), pos(王国地图) | itemId, itemNum, rewardInfo, status, objectIndex, pos |
#### 5.4.5 错误码
| 错误码 | 值 | 含义 |
|---|---|---|
| `ITEM_NOT_ENOUGH` | 8000 | 道具不足 |
| `ITEM_ARG_ERROR` | 8001 | 参数错误 |
| `ITEM_NOT_EXIST` | 8002 | 道具不存在 |
| `ITEM_NOT_BATCH_USE` | 8004 | 不支持批量使用 |
| `ITEM_NOT_USE` | 8005 | 不可使用 |
| `ITEM_PACKAGEID_NOT_EXIST` | 8006 | 所选礼包组不存在 |
| `ITEM_KINGDOM_MAP_NO_POS` | 8007 | 王国地图未传坐标(实测代码中未使用,预留) |
| `ITEM_NOT_JOIN_GUILD` | 8008 | 未加入联盟 |
| `ITEM_SOMMON_MONSTER_FAILED` | 8009 | 召怪失败(周围乱军太多) |
---
### 5.5 道具"脚本"机制(**实际上没有独立脚本表**
#### 5.5.1 真实实现
`ItemScript.lua` 仅有 3 个空函数(`requireAllScript / useItem / dropItem`**没有任何注册机制**。所有"脚本"逻辑都是 `Item.lua:ItemUse` 函数内的 `if/elseif (sitem.itemFunction)` 巨型分支。
**没有的东西**
- ❌ 没有 itemId → Lua 函数的映射表
- ❌ 没有动态加载脚本
- ❌ 没有 `s_ItemScript` 配置表
**有的东西**
-`ItemDefine.itemFunction`int 枚举)一一对应硬编码 if 分支
-`ItemDefine.data1 / data2` 作为该分支的参数
#### 5.5.2 "脚本类型"清单(即 §4.2 ItemFunctionType 表)
按业务能力归类:
| 业务能力 | itemFunction | 实现位置 |
|---|---|---|
| 直接发奖励组 | 1 / 3 | `getItemPackage(data2)` |
| 选包奖励 | 2 | `s_ItemRewardChoice[data2][id].reward``getItemPackage` |
| 时效 Buff | 4 (CITY_BUFF) | `RoleLogic:addCityBuff(data2)` |
| 加 VIP / 行动力 | 5 / 6 | `RoleLogic:addVip / addActionForce` |
| 解锁迷雾 | 10 | `DenseFogLogic:openNearDenseFog(pos)` |
| 召唤野怪 | 12 | `MonsterSummonMgr.req.summonMonster(data2)` |
| 工人小屋扩展 | 13 | `BuildingLogic:unlockQueue` |
| 预备部队增容 | 14 | 修改 Role 字段 |
| 联盟积分 | 35 | `GuildLogic:addGuildCurrency(leaguePoints)` |
#### 5.5.3 选包奖励的客户端选择流程
```
点击使用 → BagMediator.BtnOperate 检测 itemFunction == 2
→ 弹 BagGiftOpenView (ShowType=1)
→ m_rewardGroupProxy.GetChoiceRewardDataByGroup(define.data2)
// 查 s_ItemRewardChoice[data2] 列表,每条 {id, reward(=itemPackage group)}
→ 用户点击某选项 → m_selectId = data.id
→ 点确定 → Item_ItemUse.request { itemIndex, itemNum, id = m_selectId }
→ 服务端按 id 查到具体 reward group → getItemPackage 发放
```
---
### 5.6 新道具标记 / 红点
#### 5.6.1 服务端/客户端分工
- **服务端****完全不维护新道具状态**,只负责增量推 `Item_ItemInfo`
- **客户端**:完全在 `BagProxy` 中维护:
- 持久化:`PlayerPrefs``{rid}/itemIndex:{itemIndex}` 存"新/旧/已清"三态
- 内存:`m_reddotRecord[type][itemIndex] = 新增数量` + `m_reddotTotalDic[type] = 红点总数`
#### 5.6.2 触发时机(`BagProxy.UpdateItemInfo` + `ReddotRecord`
1. **首次登录**`m_isFirstGetItemInfo == true`**全部不算新道具**(避免登录时所有道具都亮红点),并强制 `SetLocalItemToOld` 把所有 itemIndex 标为 -1
2. **非首次**:服务端推来一条 ItemInfo`ItemDefine.redDotPrompt == 1`,且:
- 新增 `itemIndex` → 红点+=overlay
-`itemIndex` overlay 增加 → 红点+=增量
-`itemIndex` overlay 减少 → 移除红点记录
3. **装备已穿戴**`heroId != 0`):直接 `SetLocalItemToOld`,不亮
#### 5.6.3 清除时机
| 函数 | 时机 |
|---|---|
| `ClearReddotRecordByIndex(index, itemId)` | 用户在 BagPanel 选中该道具看详情时(`RefreshItemDetail` 内调) |
| `ClearReddotRecordByType(type)` | 用户切换 Bag Tab 时(`SwitchMenu` 内对原 type 调) |
| `ClearAllReddotRecord()` | BagPanel 关闭时(`BagMediator.OnRemove` |
> ⚠️ 设计后果:**只要打开过背包并选了道具,红点就消**。不需要服务端记账。
#### 5.6.4 红点数量统计
`GetBagReddotTotal()` = sum over type `GetBagReddotNumByType(type)` = sum over reddotRecord[type][index]
---
### 5.7 资源类道具(金币 / 钻石 / 等)
#### 5.7.1 双轨制设计(**关键决策点**
ROK 采取**双轨制**
| 维度 | 玩家货币Role 字段) | 资源类道具Bag Item |
|---|---|---|
| 存储位置 | `Role.food / wood / stone / gold / denar / vip / actionForce / expeditionCoin / individualPoints / leaguePoints / activityActivePoint` | `s_Item` 配置 + 背包 Item 条目 |
| 操作接口 | `RoleLogic:addGold / addStone / addFood / addVip / addActionForce …` | `ItemLogic:addItem / delItem` |
| 同步协议 | `Role_RoleInfo` 推送 | `Item_ItemInfo` 推送 |
| 上限 | 单字段 int64配置表内有 individualPointsLimit/alliancePointsLimit 等业务上限 | `s_GameWarning.num` 仅做告警,**无硬上限** |
| 红点 | 无 | 有(按 type 统计) |
| 例子 | 玩家身上的"500 金币"是 Role.gold 字段 | 背包里"金币袋 x3每袋可换 1000 金币)"是 itemId 在 RESOURCE 类下的 Item |
#### 5.7.2 资源类道具如何转成货币
`Item_ItemChangeResource`851
```
request: { itemIndex, itemNum }
→ getItem(rid, itemIndex) 取 itemInfo
→ 校验 s_Item.type == RESOURCE 或 subType == ACTION_FORCE → 否则 ITEM_NOT_RESOURCE_ITEM
→ checkOverlay >= itemNum → 否则 ITEM_NOT_ENOUGH
→ delItem 扣除
→ 按 subType 分支调对应 RoleLogic:add*
- GOLD → addGold(data1 * itemNum)
- STONE → addStone
- WOOD → addWood
- GRAIN → addFood
- VIP → addVip
- ACTION_FORCE → addActionForce
→ updateItemUseTaskSchedule
response: { result, itemId, itemNum }
```
> ⚠️ **没有 DENAR钻石/ DIAMOND 这条分支**——钻石不能通过道具兑换得来(设计上钻石是付费货币)。
#### 5.7.3 资源大额变动审计
- 全部走 `LogLogic:itemChange` / `LogLogic:roleChange`(每个 RoleLogic:add* 内部都会调)
- 阈值告警:`s_GameWarning.num` 配置单 itemId 阈值 → `Common.sendResourceAlarm(rid, itemId, newNum)` 发给监控
---
### 5.8 初始资源发放
`ItemLogic:createRoleGiveItems(rid)`L49-66
```
读取 s_Config:Get("initialItemType") 和 ("initialItemNum")
两个 list 长度必须一致(不一致以小的为准)
循环调 addItem(rid, itemId, itemNum, eventType=GUILD_CREATE_ROLE_GAIN_ITEM)
```
**实际配置值**`Server/common/config/gen/Configs.data` L13376 `s_Config[0]`
```
initialItemType = { 502030004, 502010001, 502020001, 502040002, 201010010, 201010013, 502070001, 502070003 }
initialItemNum = { 8, 1, 1, 1, 30, 8, 10, 10 }
initialFood = 100000
initialWood = 100000
initialStone = 0
initialGold = 0
initialDiamond = 300 // 钻石denar
initialArmsType = { 10101 } // 士兵类型
initialArmsNum = { 1000 } // 士兵数
initialBuff = 30105 // 初始 City Buff治疗加速
```
- 道具初始有 8 种 × 各若干(共 69 个道具实例)
- 货币初始:粮 10W、木 10W、钻 300金/石/行动力默认 0
- 士兵初始10101 类型 1000 个
> 资源发放走 `RoleLogic:createRole`(或 `setRole` 直接赋值字段),不走 `addItem`。
---
## 6. 协议清单
### 6.1 RPC 协议(请求-响应)
| 协议名 | ID | 方向 | request | response | 用途 |
|---|---|---|---|---|---|
| `Item_ItemChangeResource` | 851 | C→S | `itemIndex, itemNum` | `result, itemId, itemNum` | 使用资源类道具兑换玩家货币(金/木/石/粮/VIP/行动力) |
| `Item_ItemUse` | 852 | C→S | `itemIndex, itemNum, id, pos` | `itemId, itemNum, rewardInfo, status, objectIndex, pos` | 使用任意可用道具(按 itemFunction 分发) |
### 6.2 推送协议(服务端 → 客户端)
| 协议名 | ID | 方向 | request | 用途 |
|---|---|---|---|---|
| `Item_ItemInfo` | 30101 | S→C | `itemInfo : *ItemInfo(itemIndex)` | 增量推送道具变更(新增/数量变更/删除),客户端按 `itemIndex` 合并到本地 `Items` dict |
### 6.3 间接相关协议(涉及道具但不属于 Item 模块)
| 协议 | 用途 |
|---|---|
| `Shop_BuyShopItem (901)` | 普通商店购买,含 itemId/itemNum |
| `Shop_BuyPostItem (902)` | 驿站道具 |
| `Shop_BuyVipStore (904)` | VIP 商店购买 |
| `Shop_BuyExpeditionStore (905)` | 远征商店购买 |
| `Build_ProduceMaterial / Build_MaterialDecomposition / Build_MaterialSynthesis / Build_MakeEquipment / Build_DecompositionEquipment / Build_CheckMakeEquip` | 装备/材料生产/合成/分解 |
| `Hero_ExchangeHeroItem (605)` | 英雄碎片兑换 |
| `Role_RoleInfo (30251)` | 含 RoleInfo货币字段 food/wood/stone/gold/denar 等) |
---
## 7. 错误码清单(`ITEM_` 前缀)
| 错误码 | 值 | 含义 |
|---|---|---|
| `ITEM_NOT_ENOUGH` | 8000 | 道具不足 |
| `ITEM_ARG_ERROR` | 8001 | 道具模块参数错误 |
| `ITEM_NOT_EXIST` | 8002 | 道具不存在 |
| `ITEM_NOT_RESOURCE_ITEM` | 8003 | 不是资源类型道具ChangeResource 用) |
| `ITEM_NOT_BATCH_USE` | 8004 | 道具不支持批量使用 |
| `ITEM_NOT_USE` | 8005 | 道具无法使用itemFunction=0 或前置不满足) |
| `ITEM_PACKAGEID_NOT_EXIST` | 8006 | 所选道具礼包组不存在 |
| `ITEM_KINGDOM_MAP_NO_POS` | 8007 | 使用王国地图未上传坐标(代码中预留,未实际触发) |
| `ITEM_NOT_JOIN_GUILD` | 8008 | 未加入任何联盟(用 LEAGUE_POINTS 道具时) |
| `ITEM_SOMMON_MONSTER_FAILED` | 8009 | 召唤怪物失败 |
### 7.1 关联模块道具相关错误码(散落在其他模块前缀)
| 错误码 | 值 | 含义 |
|---|---|---|
| `ROLE_NAME_ITEM_DENAR_NOT_ENOUGH` | 1046 | 改名道具+钻石都不足 |
| `ROLE_BUFF_ITEM_NOT_ENOUGH` | 1048 | buff 道具不足 |
| `ROLE_SPEED_ITEM_NOT_ENOUGH` | 1066 | 加速道具不足 |
| `MAP_MOVE_CITY_ITEM_NOT_ENOUGH` | 2030 | 迁城道具不足 |
| `HERO_SUMMON_ITEM_NOT_ENOUGH` | 3001 | 召唤统帅道具不足 |
| `HERO_LEVEL_ITEM_NOT_ENOUGH` | 3005 | 升级技能道具不足 |
| `HERO_EXCHANGE_ITEM_NOT_ENOUGH` | 3010 | 通用雕像不足 |
| `HERO_ITEM_ERROR` | 3015 | 升星材料类型错误 |
| `HERO_ITEM_TO_MUCH` | 3016 | 道具不足无法升星 |
| `BUILDING_ITEM_ERROR` | 6021 | 解锁建筑道具类型错误 |
| `BUILDING_SMITHY_ITEM_ERROR` | 6024 | 无法生产该道具 |
| `BUILDING_SMITHY_ITEM_NOT_ENOUGH` | 6027 | 该材料不足 |
| `BUILDING_EQUIP_ITEM_NO_ENOUGH` | 6030 | 装备道具不足 |
| `SHOP_ITEM_NOT_SELL` | 10000 | 商品没在商店售卖 |
| `SHOP_POST_ITEM_NOT_EXIST` | 10001 | 驿站道具不存在 |
| `SHOP_POST_ITEM_HAVE_BUY` | 10002 | 驿站道具已购买 |
| `SHOP_EXPEDITION_ITEM_COUNT_MAX` | 10007 | 远征商店购买超上限 |
| `GUILD_SHOP_ITEM_NOT_EXIST` | 12073 | 联盟商店商品不存在 |
| `GUILD_SHOP_ITEM_NOT_ENOUGH` | 12074 | 联盟商店商品不足 |
> ⚠️ **`BAG_` 前缀错误码不存在**——背包功能复用 `ITEM_` 前缀。
---
## 8. UI 流程(`BagMediator` / `BagGiftOpenMediator` / `UI_Win_UseItemMediator`
### 8.1 BagPanel 分页
- **5 个 Tab**:资源(1) / 加速(2) / 增益(3) / 装备(4) / 其他(5)
- Tab 切换:`OnMenuRes/Speed/Gain/Equip/Other``SwitchMenu(type)` → 切 Tab 时**自动清除当前 Tab 的红点记录**
- 每个 Tab 显示4 列 × N 行(`m_itemCol = 4``m_itemLineCount = ceil(count/4)`
- 数据源:`m_bagProxy.Items` 全部过滤 `overlay > 0`,按 `type = itemId / 100000000` 分组
- Tab 角标红点数:`m_pageReddotImgList[i]` + `GetBagReddotNumByType(i+1)`
### 8.2 排序规则
`DataSort` 在每个 Tab 数据列表上:
1.`ItemDefine.rank` 升序
2. rank 相同则按 itemId 升序
### 8.3 道具详情 Tooltip 显示内容
`RefreshItemDetail` 渲染:
- 品质背景图(`m_qualityDic[quality]`5 档)
- 道具图标(`itemDefine.itemIcon`
- 顶部小卡描述(仅 `l_topID >= 1` 时显示,格式 `format(text, topData)`
- 名称(`l_nameID`
- **装备**`typeGroup == 2`)→ 显示 `UI_Item_EquipAtt` 属性卡片
- **非装备** → 显示 `l_desID` 格式化文本 `format(text, desData1, desData2)`
- **可批量使用**`batchUse == 1`)→ 显示 `+/-` 数量调整 + 输入框 + Max 按钮
- **使用按钮**:仅 `l_buttonDes >= 1` 时显示(文字来自语言包)
### 8.4 使用流程的二次确认
| 情况 | 是否二次确认 |
|---|---|
| 普通礼包itemFunction=1 | 无确认,直接发请求 |
| 选包奖励itemFunction=2 | 弹 `BagGiftOpenView` 必选,本身就是确认界面 |
| 兑换确认itemFunction=3 | 弹 `BagGiftOpenView` 显示前后对比 |
| 王国地图itemFunction=10 | 选坐标本身就是确认 |
| 预备部队itemFunction=14 | 若已有不同档位 buff 弹 `Alert` 二次确认 |
| VIP 道具UseItemView | 计算 overflow超出时弹 `Alert` 二次确认 |
| 随机迁城itemFunction=31 | 强制弹 `Alert` 二次确认 |
| 其他 | 无 |
### 8.5 ItemUse 响应处理(`PlayerCmd.cs:Item_ItemUse.TagName`
1. 错误响应 → `ErrorCodeHelper.ShowErrorCodeTip`
2. 成功:`Tip.CreateTip("使用了 {道具名} x{数量}")`
3.`itemFunction` 分发后续 UI
- 10/13 → 弹 `s_itemCollection` 显示奖励
- 12 → 相机飞向召唤点 + 召唤特效
- 其他 → 若 `HasRewardInfo``s_rewardGetWin` 奖励展示
---
## 9. 与 Survivors 实现的对照点
### 9.1 模块对应表
| ROK 模块 | Survivors 对应(计划) | 说明 |
|---|---|---|
| `BagProxy.cs` + 客户端缓存 | `BagComponent`(挂玩家 Entity + `BagComponentSystem` | ET 风格的 Component/System业务逻辑放 System |
| `ItemLogic.lua:addItem/delItem` | `BagComponentSystem.AddItems / RemoveItems` | 统一入口,但 **Survivors 设计是"所有奖励都走 Item"**,比 ROK 更激进 |
| `Item.lua:ItemUse` 巨型 if/elseif | C# `Dictionary<int, IItemUseHandler>` 注册机制 | 比 Lua 硬编码更可扩展,便于单测 |
| `s_Item` + `ItemDefine` | Luban 配置 `ItemConfig` | 字段保留 quality/type/subType/itemFunction/data1/data2删掉 ROK 残留(`exclusive` 改名 `bindType` 等) |
| `Item_ItemInfo` 增量推送 | `M2C_ItemInfoSync`protobuf | 用 MemoryPack 序列化 |
| `Item_ItemUse` (852) | `C2M_UseItem` / `M2C_UseItemResponse` | RPC + 推送分离 |
| `BagPanel.prefab`5 Tab | `BagPanel.prefab`YIUI4 Tab资源/装备/道具/材料) | Survivors 设计简化成 4 Tab |
| `BagGiftOpenView`(选包) | `GiftOpenPanel` | 同流程 |
| 红点 PlayerPrefs | ET `RedDotComponent` + 持久化到 Mongo | 服务端记账,多端同步 |
| 创角初始道具 `initialItemType/Num` | `InitialResourceConfig`Luban | 列表配置,启动时遍历 AddItems |
| 错误码 8000-8009 | `ErrorCode.cs` 同段位 | 直接复用 8000-8009 |
### 9.2 关键差异决策
| 维度 | ROK | Survivors 设计 |
|---|---|---|
| **货币/道具是否合并** | **分离**(双轨制) | **合并**(所有可获得资源 = Item金币也是 itemId |
| **使用脚本机制** | 硬编码 if/elseifitemFunction 枚举) | C# `IItemUseHandler` 注册(每个 itemFunction 一个 handler 类) |
| **新道具红点** | 客户端 PlayerPrefs | 服务端 Mongo + 客户端缓存 |
| **配置表** | SQLite + 自研 Define | Luban 生成 |
| **协议** | sproto | protobuf3 + MemoryPack |
| **持久化** | 服务端 Redis + 落 SQL | MongoIDBComponent |
| **装备穿戴** | item.heroId 字段 | 同 |
| **奖励合并** | `mergeReward` Lua | C# 通用 RewardEntry 列表 |
### 9.3 设计陷阱(迁移时易踩)
1. **不要直接搬"双轨制"**Survivors 设计目标是统一 Item。金币/钻石如果也作为 Item则没有 `Item_ItemChangeResource` 这个 RPC相关 `ItemType.RESOURCE` 子类型大量道具变成"普通礼包道具"
2. **`exclusive` 字段**ROK 是装备专属英雄 IDSurvivors 设计中可能不需要装备绑定,可去除
3. **`uniqueIndex` 字段**ROK 中预留未用Survivors 直接删掉
4. **`maxStack` 缺失**:必须新增(防溢出 int 攻击)
5. **`itemFunction` 拆分**ROK 的 35 个值分散且历史包袱重Survivors 应只实现 P1-P3 核心 5-8 个
6. **`itemId` 编码约定**ROK 用 `itemId / 100000000` 划分 typeSurvivors 应改为 ItemConfig 显式 type 字段itemId 不再有语义)
---
## 10. 实现优先级建议
### P1 必须(核心基础设施,无 UI
| # | 任务 | 对应 ROK | 备注 |
|---|---|---|---|
| 1 | `ItemConfig` Luban 配置定义(含 type/subType/quality/itemFunction/data1/data2/icon/name/desc/maxStack | `s_Item` + `ItemDefine` | 删 exclusive/uniqueIndex加 maxStack |
| 2 | `BagComponent`(玩家身上挂) + `ItemUnit`(子 EntityitemIndex 主键) | `MSM.d_item[rid]` | Mongo 持久化 |
| 3 | `BagComponentSystem.AddItems / RemoveItems / HasItems / GetItemNum` | `addItem / delItem / checkItemEnough` | **统一发奖/扣资源入口**,所有业务都调它 |
| 4 | `M2C_ItemInfoSync` 增量推送 | `Item_ItemInfo` (30101) | 客户端 `BagComponentClient` 合并 |
| 5 | `RewardEntry { itemId, count }` 通用结构 | `.RewardItem` | 一切奖励/消耗都用它 |
| 6 | `InitialResourceConfig` + 创角发放 | `s_Config.initialItemType/Num` | Login 时调 |
| 7 | 错误码 8000-8009 | ErrorCode.cs | 直接复用 |
### P2 必须(道具使用 + 核心 itemFunction
| # | 任务 | 对应 ROK | 备注 |
|---|---|---|---|
| 1 | `C2M_UseItem` Handler | `Item.lua:ItemUse` | 校验/扣道具/调 handler/返回结果 |
| 2 | `IItemUseHandler` 注册机制 + `ItemUseHandlerComponent` | 巨型 if/elseif | C# 字典查找替代硬编码 |
| 3 | `ItemUseHandler_OpenPackage`itemFunction=1 | OPEN_ITEMPACKAGE | 调 `ItemPackageSystem.GetReward` |
| 4 | `ItemUseHandler_ChoosePackage`itemFunction=2 | CHOOSE_ITEMPACKAGE | 含 request.id 参数 |
| 5 | `ItemPackageConfig` + `ItemPackageSystem.GetReward(groupId, openNum)` | `s_ItemPackage` + `getItemPackage` | 加权随机 |
| 6 | `ItemRewardChoiceConfig` | `s_ItemRewardChoice` | 选包配置 |
| 7 | 操作日志 `LogLogic:itemChange` 等效 | `LogLogic` | jsonl 落盘 |
### P3 可推迟(业务扩展类 itemFunction
| # | 任务 | 备注 |
|---|---|---|
| 1 | itemFunction=5/6 VIP/行动力(**如果 Survivors 把它们作为 Item**,则这俩 handler 不需要,直接发 itemId | |
| 2 | itemFunction=4 CityBuff如果 Survivors 有 buff 系统才做) | |
| 3 | itemFunction=10 解锁迷雾Survivors 无大地图,**跳过** | |
| 4 | itemFunction=12 召唤怪物(**跳过** | |
| 5 | itemFunction=13/14 工人/部队SLG 专有,**跳过** | |
| 6 | itemFunction=30/31 迁城(**跳过** | |
| 7 | itemFunction=35 联盟积分(无联盟,**跳过** | |
### P4 UI 集中实现
| # | 任务 | 对应 ROK |
|---|---|---|
| 1 | `BagPanel`4 Tab资源/装备/道具/材料) | `BagView` |
| 2 | `BagItemUICommon` 通用道具显示组件 | `UI_Item_Bag` + `UI_Model_Item` |
| 3 | `BagItemTooltip` 道具详情浮层 | `RefreshItemDetail` 内的详情面板 |
| 4 | `GiftOpenPanel` 选包/兑换 UI | `BagGiftOpenView` |
| 5 | `RewardGetPanel` 奖励展示弹窗 | `s_rewardGetWin` |
| 6 | Tab 红点 | `m_pageReddotImgList` |
### P5 后续完善
| # | 任务 |
|---|---|
| 1 | 资源阈值告警(`GameWarning` 配置 + Alarm |
| 2 | 大额变动审计日志 |
| 3 | 装备分解/合成(如果做装备系统) |
| 4 | 装备图纸(如果做装备系统) |
---
## 附录 AROK 关键源代码位置索引
| 内容 | 路径 |
|---|---|
| 服务端 ItemLogicaddItem/delItem/giveReward/getItemPackage | `Server/server/game_server/logic/lualib/ItemLogic.lua` |
| 服务端 Item proxyItemUse/ItemChangeResource RPC handler | `Server/server/game_server/logic/service/proxy/Item.lua` |
| 服务端 Item 数据 service | `Server/server/game_server/logic/service/data/d_item.lua`snax 服务,本次未深入) |
| 道具枚举ItemType/SubType/FunctionType/PackageType/BatchUse | `Server/common/lualib/enum/ItemEnum.lua` |
| CurrencyType 枚举(货币不是 Item | `Server/common/lualib/enum/OtherEnum.lua` L101 |
| ItemScript**空壳**,无实质内容) | `Server/server/game_server/logic/lualib/itemscript/ItemScript.lua` |
| Common.sprotoItemInfo / RewardInfo / RewardItem | `Server/common/protocol/Common.sproto` L308 / L381 / L362 |
| Protocol.sprotoItem_ItemUse 852 / Item_ItemChangeResource 851 / Item_ItemInfo 30101 | `Server/common/protocol/Protocol.sproto` |
| 服务端 s_Config 初始道具配置initialItemType/Num | `Server/common/config/gen/Configs.data` L13376id=0 行) |
| 客户端 BagProxy增量同步/红点/装备/材料) | `Client/Assets/Scripts/Hotfix/MVC/Proxy/BagProxy.cs` |
| 客户端 BagMediator5 Tab + 使用按钮分支) | `Client/Assets/Scripts/Hotfix/MVC/View_Mediator/Bag/BagMediator.cs` |
| 客户端 BagGiftOpenMediator选包/兑换 UI | `Client/Assets/Scripts/Hotfix/MVC/View_Mediator/Bag/BagGiftOpenMediator.cs` |
| 客户端 UI_Win_UseItemMediator行动力/VIP 快捷使用) | `Client/Assets/Scripts/Hotfix/MVC/View_Mediator/UseItem/UI_Win_UseItemMediator.cs` |
| 客户端 PlayerCmd.csItem_ItemUse 响应处理) | `Client/Assets/Scripts/Hotfix/MVC/CMD/PlayerCmd.cs` L38-128 |
| 客户端 ItemDefine25 字段) | `Client/Assets/Scripts/Hotfix/Config/ItemConfig.cs` |
| 客户端 ItemPackageConfig / ItemRewardChoiceConfig 等 | `Client/Assets/Scripts/Hotfix/Config/Item*Config.cs`7 个) |
| 错误码 ITEM_ 前缀8000-8009 | `Client/Assets/Scripts/Hotfix/MVC/CMD/ErrorCode.cs` L255-264 |
| 客户端配置 Bin 文件 | `Client/Assets/StreamingAssets/Config/Bin/Item.bin`87 KB |
## 附录 B未深入调研的边角
- `s_Item.lua` 的 ConfigEntity 加载机制(只看了 stub没看 ConfigEntity 父类)
- `MSM.d_item[rid]` snax 服务实现细节
- `Build_*` 装备制造/合成 RPC 详细参数(涉及 EquipDefine + EquipMaterialDefine 联动)
- `Hero_ExchangeHeroItem` 等英雄相关道具用法(属于 Hero 模块)
- 钻石denar的付费充值入口RechargeLogic属于充值模块
- 联盟礼物 `guildGifts` 在奖励组中的具体发放机制(属于 Guild 模块)
- `LogLogic:itemChange` 的具体日志格式(输出到 log_server