工程参考/回收系统
回收系统把背包里的物品按表价换成元宝:客户端选物品 → 服务端逐格报价 → 玩家确认 → 在同一个背包事务里扣物品、加元宝、记当日额度。
查一件物品能回收多少 → ItemRecycle.txt;改每日上限 → ItemRecycleSettings.txt;判断某件物品为什么收不了 → 看下面的资格判定。
先分清三个「回收」
名字里都有「回收」,但货币、配置表和代码路径完全不同,先对号入座再改表。
| 系统 | 玩家看到的效果 | 数据来源 | 入口 |
|---|---|---|---|
| 物品回收(本页) | 背包物品按表价换成元宝 | ItemRecycle.txt、ItemRecycleSettings.txt | 客户端回收界面 |
| 玩家商店收购 | 把物品卖给店主换交子或金币 | RecycleItem.txt、RecycleItemClass.txt、RecycleItemType.txt | 玩家商店的收购菜单 |
| 物品箱回收次数 | 控制副本掉落箱允许被回收的次数 | 无配置表 | 副本脚本调 Lua 接口 |
RecycleItem.txt 那三张是商店收购白名单,和元宝回收没有任何关系;把物品加进那三张表不会让它变成可回收物品。
两张表
| 文件 | 结构 | 关键约束 | 当前样本 |
|---|---|---|---|
ItemRecycle.txt
|
2 列 INT:ItemID、YuanBao
|
ItemID 必须严格递增;价格 1 到 2000000000 | 214 条,2 到 25000 元宝 |
ItemRecycleSettings.txt
|
2 列 INT,恰好 1 行:ID、DailyLimit
|
ID 必须为 1;DailyLimit 1 到 2000000000 | 1 行,10000000 |
字段逐条解释见两张表的单独页面。两张表都放在 Public/Config/ 下,客户端不加载,价格与资格全部由服务端算完随包下发。
加载与生效
| 环节 | 实现 | 结果 |
|---|---|---|
| 打开文件 | GameTable.cpp 里 SetStrictIntegers(true) 后 OpenFromTXT
|
非整数直接解析失败 |
| 校验并载入 | ItemRecycle::Config().Load(recycleItems, recycleSettings)
|
任一约束不满足,返回 FALSE 并清空已读价格 |
| 失败后果 | TableInit::Init() 返回 FALSE,调用方是 Server.cpp 的 Assert(ret)
|
表错是起不了服,不是关掉功能 |
- 没有热重载入口:改完表要停服、替换实际
Public/Config/副本、重启 Server。 - 没有功能开关:
FeatureOpen.ini里没有回收项,表加载成功系统就可用。 - 没有 GM 指令:
GMList.ini里没有回收命令,验证只能走客户端界面。
一次回收的五个动作
| 动作 | 服务端做什么 | 可能的返回 |
|---|---|---|
| Open | 分配会话 context,把整包 240 个格子逐格描述(编号、数量、单价、不可回收原因)回给客户端 | Catalog、Busy、Processing |
| Quote | 按客户端选中的格子做快照与报价,算当日已用额度 | Quoted、Changed、DailyExceeded、BalanceExceeded、Invalid |
| Commit | 复核快照后原子结算 | Succeeded、Failed、Expired、Changed、Busy |
| Query | 查询某个请求的最终状态,用于断线或超时后确认 | 原状态、Processing、OutcomeUnknown |
| Close | 清掉 context 与报价 | 不返回结果 |
时序约束:
- 两次报价间隔小于 200 毫秒会被拒(Busy),防止连点刷价。
- 报价有效期 30000 毫秒;超时提交返回 Expired。
- 提交一旦匹配上报价,无论后续校验是否通过,这份报价都会被消费掉;重试必须重新报价。
- context 绑定打开时的场景;换场景后心跳会清空 context,旧报价全部作废。
资格判定
整次回收的门禁
下列任一不满足,打开回收界面只会拿到 Busy:
| 条件 | 说明 |
|---|---|
| 角色与场景 | 玩家对象、Player、所在场景都存在,且处于可执行逻辑的状态 |
| 没有进行中的回收 | 同一个角色同时只能有一个回收会话 |
| 元宝入口空闲 | 元宝票没有待发邮件,CurrencyBlocked 为假
|
| 不在切场景 | IsHaveChangeSceneFlag 为假
|
| 没有待领奖 | IsHaveAskPrizeFlag 为假
|
| 不在摆摊 | IsInStall 为假
|
| 交易盒空闲 | m_ExchangBox.m_Status 为 EXCHANGE_NONE
|
| 没有特定锁位 | m_BitLock.m_uLowFlags 的 0x800 位为 0
|
| 数据库不忙 | 不处于 DDZ 奖励结算的忙碌状态(HappyDDZRewardBusy 为假)
|
逐格判定
服务端对每个背包格按固定顺序判定,命中即停,这就是客户端提示「不可回收」的原因:
| 顺序 | 判定 | 返回原因 | 玩家看到的含义 |
|---|---|---|---|
| 1 | 物品类型是任务物品(ICLASS_TASKITEM)
|
Quest | 任务物品永不回收 |
| 2 | 物品已绑定 | Bound | 绑定物品不可回收 |
| 3 | 有密码锁或物品锁 | Locked | 先解锁 |
| 4 | 已加锁或正在交易 | Occupied | 物品被占用 |
| 5 | 表里查不到价格 | NotConfigured | 这件物品没配回收价 |
| 6 | 装备强化过或镶了宝石 | 保护位 | 需要玩家勾选确认后才报价 |
| 7 | 以上都不命中 | Eligible | 可以回收 |
价格查不到与价格为 0 是同一件事:服务端用 Config().Price() 取价,取不到就是 0,直接判 NotConfigured。但把价格写成 0 会让整表加载失败,所以要让某件物品不可回收只能删行。
价格、额度与上限
- 单格收益 = 表价 × 该格数量。按格计算,不跨格合并,也不看物品在其它格子的堆叠。
- 每日额度取
ItemRecycleSettings.txt的第二列,按角色记在任务数据第 180、181 槽:180 存日序号,181 存当日已得元宝。 - 日序号 = (Unix 秒 + 8 小时)÷ 86400,即东八区自然日。跨天不写回 0,而是「180 与今天不相等时,已用额度按 0 算」。
- 报价与提交都校验三件事:当日额度、元宝上限 2000000000、以及两者相加不溢出。
- 当前样本:额度 10000000(一千万)元宝每天,表内最高单价 25000 元宝,价格分布最集中的是 490 元宝 60 条、99 元宝 27 条、149 元宝 22 条。
额度是「每天最多换出多少元宝」,不是「每天能回收几次」。单价高的物品一两件就能打满,之后当天所有报价都会被判 DailyExceeded。
结算是一致性事务
- 报价时把每个选中格子的完整
_ITEM结构存进事务,不是只存编号和数量。 - 提交时重新逐格比对:GUID、物品编号、可回收原因、保护确认,以及整块
_ITEM的内存比较。任一不同判 Changed。 - 通过后走
CommitBagPlan,扣物品、SetYuanBao(..., YUANBAO_ITEM_RECYCLE, "物品回收")、写 180 与 181 三件事在同一个事务里;任一步失败整体回滚,元宝与额度一起还原。 - 成功后客户端收到「回收成功,获得N元宝。」的系统提示。
元宝流水里这笔记录的备注是「物品回收」,回滚是「物品回收回滚」,对账时按这两个备注筛。
报价里还记了配置版本(FNV-1a,含额度)和额度值,提交时再比一次,不一致判 Expired。当前没有热重载入口,同一进程内这两项不会变;这段比较是给将来加热重载或跨进程留的保险。
结果状态
| 状态 | 含义 | 客户端处理 |
|---|---|---|
| Catalog | 打开成功,回包带整包目录 | 渲染列表 |
| Quoted | 报价成功,回包带本次选中的格子与总收益 | 展示确认框 |
| Processing | 上一次请求还在结算 | 等待,可继续发 Query |
| Succeeded | 结算成功 | 刷新背包与元宝 |
| Failed | 背包事务提交失败,已整体回滚 | 提示失败,可重新报价 |
| Changed | 选中物品或角色状态与报价时不一致 | 重新报价 |
| Expired | 报价超过 30 秒;或提交时配置版本、额度与报价时不一致(当前没有热重载,正常流程只会因超时触发) | 重新报价 |
| Busy | 门禁不满足、请求过快或正在结算 | 稍后重试 |
| DailyExceeded | 本次收益超过当日剩余额度 | 提示今日额度已满 |
| BalanceExceeded | 元宝余额加上本次收益会超过上限 | 提示元宝已达上限 |
| Invalid | 事务结构校验失败 | 重新报价 |
| OutcomeUnknown | 找不到对应的请求(重登、请求号不匹配) | 重新打开界面 |
| Unavailable | 结果结构未初始化 | 正常流程不会收到 |
和其余系统的互斥
回收会话进行中时,服务端会拦掉所有可能动到「已预留格子」的包:使用物品、拆分、背包换位、存仓与取仓、交易加物品、丢弃、卖给商店、摆摊上架;元宝相关包整类拦掉。被拦时提示「物品回收正在结算,相关物品暂不能操作。」
另外,元宝票有邮件在处理时整次回收直接判 Busy,避免两个元宝入口同时改余额。
常见改法
让一件物品可回收
在 ItemRecycle.txt 追加一行,物品编号必须大于当前最后一条(50613006),价格填 1 到 2000000000。停服重启后生效。
改价
只改第二列,保持第一列顺序不动。服务端不读客户端,改完重启即可,客户端不需要更新。
改每日额度
改 ItemRecycleSettings.txt 的第二列,第一列必须保持 1,且文件只能有一行数据。
只清某个角色的额度
把该角色任务数据第 180 槽改成 0 或昨天、181 槽改成 0。服务端读到时发现日序号不等于今天,就把已用额度按 0 算。
停用整个系统
服务端没有开关,也没有「空表即关闭」的写法:Load() 要求物品表至少一条记录,清空同样会让启动失败。要停用只能改客户端脚本,让它不再调用打开回收界面的方法。
容易踩的坑
是「严格递增」,不是「唯一」
Load() 逐行比较当前 ItemID 必须大于上一行。乱序、重复、倒序都会让整表清空并导致启动失败。插入新条目只能追加到末尾,或者整段重排。
表头与类型是硬约束
两张表都必须恰好 2 列且两列都是 INT;SetStrictIntegers(true) 下任何空值、非整数都会让解析失败。不要加第三列备注,注释行也无法保留。
settings 只能有一行
Load() 要求 GetRecordsNum() == 1,且第一列必须是 1。多写一行、把 ID 写成 2 都会让服务端起不来。
额度按角色,不按账号
额度存在角色的任务数据里,换角色重新计算;同账号不同角色各自有额度。清额度也要按角色清。
只认背包
回收遍历的是 0 到 239 号背包格(数组长度 240)。装备栏、银行、临时背包里的物品不会出现在回收列表里,也不会被回收。
回收日志不是流水
结算只写 CacheLog 的调试日志,没有可重放的账本。道具找回、玩家申诉不能拿这份日志当依据,要查元宝流水里的「物品回收」备注。
材料价与单测基线绑定
20500、20501、20502 三个家族(精铁、棉布、秘银)的 1 到 3 级,样本价是 8、40、200,与 ItemRecycle::BuiltinPrice() 的基准值一致。改这几条价格,Common/tests/ItemRecycle 的基线断言会失败,需要同步测试。
相关页面
- ItemRecycle.txt:回收价目表,逐列说明与修改实例
- ItemRecycleSettings.txt:每日额度
- 配置文件索引:所有配置表按用途分组
- SetItemBoxRecycleTimes:物品箱回收次数,和本系统无关,别混用