回收系统
本文基于当前源码中的 RecycleRuleLoader + RecycleShopSession 行为整理,适用于最新 recycle/*.yml 与 shop/recycle*.yml 配置。
配置入口
- 回收规则文件:
plugins/Malkuth/recycle/*.yml - 回收商店文件:
plugins/Malkuth/shop/*.yml - 商店模式:
recycle与recycle_shop/recycle-shop都会进入回收会话,但源码会把它们解析成两个不同模式值;其中recycle_shop仍要求配置currency - 规则引用:商店
goods.<key>.id对应recycle/*.yml顶层规则 ID
默认资源里包含 recycle/custom_fishing.yml,可直接作为 CustomFishing 回收的模板;recycle/example.yml 还包含 source_marked_item,展示 source 与 nbt 匹配写法。
回收规则结构(recycle/*.yml)
每条规则的顶层结构如下:
| 字段 | 必填 | 说明 |
|---|---|---|
display | 否 | 图标显示,含 material、name、lore;缺省时自动使用默认值 |
check | 是 | 匹配条件,支持“单组 AND”或 check.or 多组 OR |
rewards | 是 | 奖励列表,支持 material / currency / source / kether |
limitation | 否 | 规则级限制,含 window、player、global、custom_hours、cron_expression |
messages | 否 | 规则消息,含 success、no_item、limit_reached、condition_failed、remove_failed;success 可用 reward_* / recycled_*;在 shop/recycle*.yml 会话里,no_item 与自定义 limit_reached 还能读取商店上下文占位符;自定义 limit_reached 另可用 {limit_reason} |
if | 否 | Kether 条件,单次点击回收时会校验 |
当前回收链路不读取旧版
price规则格式;请使用rewards定义奖励。
匹配逻辑
check 当前有两种写法:
- 单组 AND:
check.<type>直接并列 - 多组 OR:
check.or为列表,每个元素是一组 AND 条件
check:
# 单组 AND
material: DIAMOND_SWORD
"!contains_lore":
uncolor: true
match: 绑定
check:
# 组1 OR 组2
or:
- material: DIAMOND_SWORD
- contains_name:
uncolor: true
match: 传说
model_data:
match: 10001
旧式
check: - material: ...列表写法不属于当前RecycleRuleSerializer的解析结构。
检测类型
Malkuth 回收匹配支持以下检测类型:
基础检测类型(8 种)
| 类型 | 说明 |
|---|---|
material | 匹配物品材质(如 DIAMOND_SWORD) |
display_name | 精确匹配物品显示名称(需完整名称一致) |
contains_name | 物品名称包含指定文本 |
regex_name | 正则匹配物品名称,支持 group 捕获 |
lore | 单行 lore 精确匹配(需完全一致) |
contains_lore | lore 中包含指定文本 |
regex_lore | 正则匹配 lore(逐行匹配,支持 group 捕获) |
model_data | 匹配 CustomModelData 数值 |
高级检测类型(9 种)
| 类型 | 说明 |
|---|---|
enchant | 匹配附魔类型(如 SHARPNESS) |
enchant_level | 匹配附魔等级,match 需包含附魔名(如 SHARPNESS:5)并配合 operator |
durability | 比较“当前耐久值”(maxDurability - damage),建议配合 operator |
amount | 比较物品堆叠数量,建议配合 operator |
unbreakable | 匹配 isUnbreakable 属性(当前实现不读取 match 值) |
item_flags | 匹配物品标志(如 HIDE_ENCHANTS) |
nbt | 匹配物品 NBT;支持只判断路径存在,也支持 path=value 精确比较 |
source / item_source | 匹配物品源识别结果;支持只匹配来源,也支持匹配完整 source:itemId |
custom_fishing | 匹配 CustomFishing 物品;match 为空表示匹配所有 CF 物品,非空时按 CF 物品 ID 精确匹配 |
通用参数
检测类型常见参数如下;不同检测类型只会读取其中相关字段:
| 参数 | 说明 |
|---|---|
match | 匹配内容(文本、正则或数值);普通 Map 写法可用 value 作为别名 |
uncolor | 设为 true 时去除颜色代码后再匹配;主要对名称和 lore 类检测有意义 |
group | (仅 regex 类型)指定捕获组编号,捕获结果可供奖励表达式使用 |
operator | (enchant_level / durability / amount)比较运算符:==、!=、>、>=、<、<= |
path / key / expect / equals | (仅 nbt / !nbt Map 写法)指定 NBT 路径与期望值 |
source / namespace / item / id | (仅 source / !source Map 写法)指定物品源、可选命名空间与物品 ID |
反向匹配
当前支持 ! 前缀的类型为:material、display_name、contains_name、contains_lore、regex_name、regex_lore、model_data、lore、enchant、enchant_level、unbreakable、item_flags、nbt、source / item_source、custom_fishing。
durability 与 amount 目前没有反向枚举,建议使用 operator 表达范围。
反向匹配示例:
"!contains_lore":
uncolor: true
match: 绑定
source / nbt 的实际写法
source 与 nbt 都支持字符串写法和 Map 写法:
| 类型 | 字符串写法 | Map 写法 | 当前实现说明 |
|---|---|---|---|
source | mythicmobs:legendary_box、itemsadder:weapons:legendary_blade、craftengine:weapon:legendary_blade、mythicmobs | source + item,或 source + namespace + item | 会调用对应物品源的 getId(item);只写 source 时表示“来自这个物品源的任意物品” |
nbt | malkuth_item_source、malkuth_item_source=mythicmobs、CustomFishing.id=salmon_king | path/key + expect/equals | 会先尝试读取顶层 key,再用深层路径读取;比较时统一按字符串比较 |
当前实现还支持两个实用别名:
item_source等价于source!item_source等价于!source
- 想匹配“任何被该物品源识别的物品”时,写
source: mythicmobs即可。 - 想匹配 Malkuth 构建后写入的标记,可直接检查
nbt: 'malkuth_item_source=<source>'或nbt: 'malkuth_item_id=<id>'。 - 想先确认手里物品能识别成什么 ID,可先执行
/malkuth source <物品源名>。
各检测类型示例
1. material + OR
钻石装备回收:
display:
material: DIAMOND_SWORD
name: '&b钻石装备回收'
check:
or:
- material: DIAMOND_SWORD
- material: DIAMOND_CHESTPLATE
- material: DIAMOND_LEGGINGS
- material: DIAMOND_BOOTS
- material: DIAMOND_HELMET
rewards:
- type: currency
amount: 100
2. contains_name + 反向匹配
传说物品回收:
check:
contains_name:
uncolor: true
match: 传说
"!contains_lore":
uncolor: true
match: 绑定
rewards:
- type: currency
amount: 300
3. regex_name 捕获变量
等级装备回收:
check:
regex_name:
uncolor: true
match: 'Lv\\.(\\d+)'
group: 1
rewards:
- type: currency
amount: 'inline {{ math "{{ ®ex_name }} * 10 + 50" }}'
4. enchant_level
高级锋利武器回收:
check:
material: DIAMOND_SWORD
enchant_level:
match: 'SHARPNESS:5'
operator: '>='
rewards:
- type: currency
amount: 500
5. durability
破损工具回收:
check:
material: IRON_PICKAXE
durability:
value: 50
operator: '<'
rewards:
- type: material
material: IRON_INGOT
amount: 1-3
unbreakable实际判定基于物品isUnbreakable属性,match字段当前不参与比较。
6. custom_fishing
CustomFishing 回收:
check:
custom_fishing: '' # 为空时匹配所有 CF 物品
rewards:
- type: currency
amount: 'inline {{ if check &cf_price > 0 then math mul [ &cf_price 0.8 ] else 50 }}'
custom_fishing 场景常用上下文变量:
&cf_item_id:CustomFishing 物品 ID&cf_price:CustomFishing 物品价格(读取失败时为0)
7. source / item_source
下面节选自默认 recycle/example.yml 的 source_marked_item:
source_marked_item:
display:
material: PAPER
name: '&e物品源标记回收'
lore:
- '&7匹配由 Malkuth 或物品源 API 可识别的物品'
- '&7source 格式: <source>:[namespace]:<itemId>'
- '&7source 会直接调用配置物品源的 getId 判断'
check:
source: 'mythicmobs:legendary_box'
rewards:
- type: currency
amount: 500
chance: 1.0
source 命中后,当前回收上下文会额外带上:
{{ &source }}:实际命中的物品源名{{ &source_id }}:实际命中的完整物品源 ID
8. nbt
同一个默认示例里还演示了 nbt 匹配:
check:
# nbt map 写法(取消注释后生效)
# nbt:
# path: malkuth_item_source
# expect: mythicmobs
nbt: 'malkuth_item_source=mythicmobs'
nbt 命中后,当前回收上下文会额外带上:
{{ &nbt_path }}:实际读取的 NBT 路径{{ &nbt }}:读取到的 NBT 字符串值
9. !nbt / !source 的边界
!nbt: some.path:只有“该 NBT 不存在”时才会命中。!nbt: some.path=value:NBT 不存在或值不等于value时命中。!source: mythicmobs:只要当前物品不能被mythicmobs物品源识别,就会命中。!source: mythicmobs:legendary_box:识别结果不是这个完整 ID 时命中。
奖励系统(rewards)
每条规则的 rewards 是列表,逐条独立执行概率判定。
| 类型 | 必填字段 | 说明 |
|---|---|---|
material | material, amount | 给予原版材料物品 |
currency | amount | 发放货币(当前实现通过 Vault 账户发放) |
source | item, amount(可选) | 通过物品源发放,item 格式为 source:itemId;若物品源自身带命名空间,后半段可继续包含 :(如 mm:legendary_box、itemsadder:weapons:legendary_blade) |
kether | commands | 执行 Kether 命令列表 |
amount 支持:
- 固定值:
100 - 区间:
2-4 - 表达式:
inline ... - Map 区间:
min/max
概率字段:
rewards:
- type: currency
amount: 100
chance: 0.5
表达式变量
当匹配阶段产生捕获变量时,可在 amount: 'inline ...' 中使用:
rewards:
- type: currency
amount: 'inline {{ math "{{ ®ex_lore }} * 1.2" }}'
| 变量 | 说明 |
|---|---|
{{ ®ex_name }} | regex_name 检测类型捕获的内容 |
{{ ®ex_lore }} | regex_lore 检测类型捕获的内容 |
{{ &enchant_level }} | enchant_level 检测时的实际附魔等级 |
{{ &durability }} | 当前剩余耐久;buildContext() 会提供该值,durability 检测命中时也会写入同名变量 |
{{ &max_durability }} | durability 检测命中时写入的最大耐久值 |
{{ &amount }} | amount 检测时的物品堆叠数量 |
{{ &enchant_count }} | 当前物品的附魔数量 |
{{ &nbt_path }} | nbt / !nbt 命中时记录的 NBT 路径 |
{{ &nbt }} | nbt 命中时读取到的 NBT 字符串值 |
{{ &source }} | source 命中时记录的物品源名 |
{{ &source_id }} | source 命中时记录的完整物品源 ID |
{{ &cf_item_id }} | CustomFishing 物品 ID(仅 CF 物品) |
{{ &cf_price }} | CustomFishing 物品价格(仅 CF 物品;读取失败时为 0.0) |
{{ &is_custom_fishing }} | 是否为 CustomFishing 物品 |
限制系统
回收链路使用独立统计系统(RecycleStatsDao / RecycleStatsManager / LimitManager),不走购买链路的 TradeLimitDao。
规则级限制(recycle/*.yml)
limitation:
window: DAILY # DAILY/WEEKLY/MONTHLY/PERMANENT/CUSTOM/CRON
player: 10
global: 100
custom_hours: 6
cron_expression: '0 0 0 ? * MON'
商店级限制(shop/recycle*.yml)
recycle:
shop_level_limit:
count_limit:
player:
enabled: true
type: DAILY
max_count: 200
global:
enabled: true
type: DAILY
max_count: 5000
currency_limit:
player:
enabled: true
type: DAILY
max_amount: 100000.0
当前实现注意点
CUSTOM/CRON会被解析,但当前回收统计窗口仅实际处理DAILY/WEEKLY/MONTHLY/PERMANENTcurrency_limit.global会被解析,但当前LimitManager未执行该项校验- 单条规则点击回收时,
messages.limit_reached已生效;你可以用{limit_reason}显示实际拒绝原因 - 一键回收 / 同类回收的成功与失败提示仍为会话内置文本,不走
rules.messages - 货币上限校验使用“预估值”(固定值直接取值,区间取平均值,表达式按 0 估算)
shop_player_*/shop_global_*/currency_*占位符当前固定读取日统计;goods_player_*/goods_global_*当前也固定读取规则的日统计,不会跟随WEEKLY/MONTHLY/CUSTOM/CRONtarget_inventory对单条回收、一键回收、预览生效;malkuth recycle same当前匹配阶段仍直接扫描玩家主背包,不会改用外部背包提供者rollback.enabled当前只会在“点击单条规则回收”时保存快照;一键回收与同类回收不会写入可撤回快照
回收商店配置(shop/recycle*.yml)
| 路径 | 默认值 | 当前状态 | 说明 |
|---|---|---|---|
mode | normal | 生效 | 回收模式可使用 recycle;若使用 recycle_shop / recycle-shop,当前序列化器会继续要求 currency 节点 |
target_inventory | null | 生效 | 回收目标背包:vanilla / legendwarehouse / soulringx;未知或不可用提供者会回退到原版背包 |
recycle.one_click_enabled | true | 生效 | 控制 malkuth recycle all |
recycle.confirm_before_recycle | true | 仅解析 | 当前会话未使用二次确认逻辑 |
recycle.preview_limit | 10 | 仅解析 | 当前预览逻辑未按该值截断输出;默认样例 shop/recycle_shop.yml 填写的是 8 |
recycle.filters.whitelist | [] | 仅解析 | 当前回收流程未执行白名单过滤;配置格式为 - material: MATERIAL_NAME |
recycle.filters.blacklist | [] | 仅解析 | 当前回收流程未执行黑名单过滤;配置格式为 - material: MATERIAL_NAME |
recycle.protect.lock_named_items | true | 生效 | 保护有显示名物品不被回收 |
recycle.protect.lock_enchanted_items | true | 生效 | 保护附魔物品不被回收 |
recycle.protect.lock_favorite_marked_items | true | 生效 | 按 NBT 标记保护收藏物品 |
recycle.protect.favorite_tag_keys | malkuth.favorite,malkuth.locked,favorite,locked | 生效 | 收藏标记键列表 |
recycle.rollback.enabled | false | 生效 | 开启后允许保存最近一次回收快照;当前仅单条规则回收会写入快照 |
recycle.rollback.expire_seconds | 30 | 生效 | 撤回有效时间(秒) |
recycle.shop_level_limit.count_limit | null | 生效 | 商店级次数限制(玩家/全局) |
recycle.shop_level_limit.currency_limit.player | null | 生效 | 商店级玩家货币限制 |
recycle.shop_level_limit.currency_limit.global | null | 仅解析 | 当前未执行全局货币限制校验 |
recycle.messages.* | - | 生效 | 覆盖 shop 级回收提示;各键与附加占位符见下方说明 |
shop 级消息节点与附加占位符
| 路径 | 说明 | 额外可用占位符 |
|---|---|---|
recycle.messages.rule_not_found | 单条规则不存在时提示 | {rule_id} |
recycle.messages.one_click_disabled | 一键回收被关闭 | 无 |
recycle.messages.one_click_success | 一键回收成功 | {recycled_count}、{reward_item_count}、{reward_item_total_amount}、{reward_currency_amount}、{reward_item_1_name} / {reward_item_1_amount} / {reward_item_1_material}… |
recycle.messages.one_click_no_item | 一键回收未命中任何物品 | 无 |
recycle.messages.same_type_empty_hand | 同类回收时主手为空 | 无 |
recycle.messages.same_type_unsupported | 主手物品没有匹配规则 | {recycled_item_name}、{recycled_item_material} |
recycle.messages.preview_empty | 预览时没有任何可回收物品 | 无 |
recycle.messages.preview_header | 预览标题行 | {preview_rule_count}、{preview_total_count}、{preview_currency_amount} |
recycle.messages.preview_line | 预览单条规则行 | {preview_rule_id}、{preview_rule_name}、{preview_count} |
recycle.messages.preview_summary | 预览汇总行 | {preview_rule_count}、{preview_total_count}、{preview_currency_amount} |
recycle.messages.rollback_success / rollback_no_snapshot / rollback_expired | 撤回结果提示 | 无 |
单条规则消息的回收商店上下文
点击单条规则时,只有 messages.no_item 与自定义 messages.limit_reached 会额外注入下面这批商店上下文;success、condition_failed、remove_failed 不会自动带上这些值。
| 占位符 | 来源 | 说明 |
|---|---|---|
{mode} | serializer.mode.name | 当前商店模式名,通常是 RECYCLE 或 RECYCLE_SHOP |
{title} | serializer.title | 商店标题配置原文;这里不会再额外展开 {page} / {max-page} |
{shop} / {shop_id} | shopId | 当前商店 ID |
{display_name} / {display-name} | rule.display.name | 当前规则显示名 |
{has_currency} / {balance} | serializer.currency?.parseCurrency(player) | 玩家当前商店货币余额 |
{limit} / {limit_remaining} | resolveRecycleLimitRemaining(ruleId) | 商店/规则次数上限里的最小剩余额度,不包含货币上限 |
{rule_id} / {rule_name} | rule.id / rule.display.name | 当前规则 ID 与显示名 |
{goods} / {goods_id} / {goods-id} | shop.goods key | 能映射到 shop.goods 时为商品 key;未命中时回退为当前规则 ID |
{price} / {amount} | ShopGoods.getEffectivePrice() / getEffectiveGoodsAmount() | 能映射到 shop.goods 时为当前商品实际价格与数量;未命中时回退为 0.0 / 1 |
图标动作与运行行为
可用图标动作:
| 动作 | 行为 |
|---|---|
malkuth recycle all | 一键回收全部匹配物品 |
malkuth recycle same | 按主手物品匹配规则,批量回收同类 |
malkuth recycle preview | 输出可回收预览 |
malkuth recycle rollback | 撤回最近一次回收快照 |
malkuth refresh | 刷新页面 |
page next / page pre | 翻页 |
当前行为说明:
- 点击单个规则回收会检查该规则
if - 一键回收 / 同类回收 / 预览当前不会检查规则
if - 预览当前也不会执行限额校验,只统计“匹配且未受保护”的物品
- 回收 UI 商品图标来自
recycle/*.yml的display,不是shop.goods.*.display shop.goods.*.price在回收会话中不会影响回收奖励
回收占位符
以下占位符分两层:基础限额占位符由 RecyclePlaceholderManager 提供;不同回收流程还会额外注入成功、预览或异常占位符。
基础限额占位符
{shop_player_current}/{shop_player_max}/{shop_player_remaining}{shop_global_current}/{shop_global_max}/{shop_global_remaining}{currency_current}/{currency_max}/{currency_remaining}{goods_player_current}/{goods_player_max}/{goods_player_remaining}{goods_global_current}/{goods_global_max}/{goods_global_remaining}
额外流程占位符
- 单条规则成功 / 一键回收成功:
{recycled_count}、{recycled_item_name}、{recycled_item_material}、{reward_item_count}、{reward_item_total_amount}、{reward_currency_amount}、{reward_item_1_name}/{reward_item_1_amount}/{reward_item_1_material}… - 回收商店中点击单条规则时,
messages.no_item/ 自定义messages.limit_reached还会额外注入{mode}、{title}、{shop}/{shop_id}、{display_name}/{display-name}、{has_currency}/{balance}、{limit}/{limit_remaining}、{rule_id}/{rule_name}、以及{goods}/{goods_id}/{goods-id}/{price}/{amount} - 预览标题 / 汇总:
{preview_rule_count}、{preview_total_count}、{preview_currency_amount} - 预览单行:
{preview_rule_id}、{preview_rule_name}、{preview_count} - 规则不存在:
{rule_id} - 自定义
messages.limit_reached:{limit_reason}
当前实现注意点
- 未配置上限时,
*_max与*_remaining会显示为无限 currency_current/currency_max/currency_remaining会格式化为两位小数;reward_currency_amount、preview_currency_amount、has_currency/{balance}当前直接使用 Kotlin 的数值字符串{limit}/{limit_remaining}只会比较商店级次数上限与规则级次数上限的剩余值,不会把currency_limit计入这两个占位符- 标题、普通图标、
one_click_success、preview_header、preview_summary这类“没有 ruleId 上下文”的文本里,只会读取基础限额占位符;{goods}/{price}/{amount}这批回收商店会话占位符不会注入进去 - 规则图标名称/描述与
preview_line会带上 ruleId 上下文,因此goods_player_*/goods_global_*系列仅在这些位置才有实际规则维度含义;但{goods}/{price}/{amount}仍只在单条规则的messages.no_item/ 自定义messages.limit_reached中注入
完整配置参考
# recycle/example.yml
示例_钻石装备:
display:
material: DIAMOND_SWORD
name: '&b钻石装备回收'
lore:
- '&7回收任意钻石工具/护甲'
- '&7获得随机奖励'
check:
or:
- material: DIAMOND_SWORD
- material: DIAMOND_PICKAXE
- material: DIAMOND_AXE
- material: DIAMOND_SHOVEL
- material: DIAMOND_HOE
rewards:
- type: material
material: DIAMOND
amount: 2-4
chance: 1.0
- type: currency
amount: 100
chance: 1.0
limitation:
window: DAILY
player: 10
global: 100
messages:
success: '&a回收成功!共回收 {recycled_count} 个物品,获得物品种类 {reward_item_count} 个,物品总数 {reward_item_total_amount} 个,货币 {reward_currency_amount}'
no_item: '&c未找到可回收的钻石物品。'
limit_reached: '&c今日回收次数已达上限。'
condition_failed: '&c不满足回收条件。'
remove_failed: '&c物品移除失败。'
if: 'perm malkuth.recycle.diamond'
# shop/recycle_shop.yml
mode: RECYCLE
enable: true
currency:
type: vault
title: '&2回收商店 &7{page}/{max-page}'
display: '&2回收商店'
target_inventory: vanilla
recycle:
one_click_enabled: true
confirm_before_recycle: true
preview_limit: 8
filters:
whitelist: []
blacklist:
- material: DIAMOND_SWORD
protect:
lock_named_items: true
lock_enchanted_items: true
lock_favorite_marked_items: true
favorite_tag_keys:
- malkuth.favorite
- malkuth.locked
- favorite
- locked
rollback:
enabled: true
expire_seconds: 30
messages:
rule_not_found: '&c回收规则不存在: {rule_id}'
one_click_disabled: '&c一键回收功能未启用'
one_click_success: '&a成功回收 {recycled_count} 个物品!获得物品种类 {reward_item_count} 个,物品总数 {reward_item_total_amount} 个,货币 {reward_currency_amount}'
one_click_no_item: '&c没有可回收的物品'
same_type_empty_hand: '&c请手持要回收的物品'
same_type_unsupported: '&c该物品无法回收'
preview_empty: '&c没有可回收的物品'
preview_header: '&a=== 回收预览 ==='
preview_line: '&7{preview_rule_name}: &f{preview_count} 个'
preview_summary: '&7共 {preview_total_count} 个物品,预计获得货币: &f{preview_currency_amount}'
rollback_success: '&a成功撤回上次回收'
rollback_no_snapshot: '&c没有可撤回的回收记录'
rollback_expired: '&c撤回时间已过期'
layout:
- '#########'
- ' ggggggg '
- ' ggggggg '
- ' ggggggg '
- ' ggggggg '
- 'P#R#S#V#N'
goods:
diamond:
id: 钻石装备回收
price: '{price}'
display:
material: 'diamond chestplate'
name: '&b钻石装备回收'
lore:
- '&7回收匹配规则的钻石装备'
- '&7单价由规则计算'
icons:
'R':
material: 'hopper'
name: '&a一键回收'
lore:
- '&7回收背包中所有匹配规则的物品'
actions:
left:
- 'malkuth recycle all'
调试与重载
- 重载规则:
/malkuth reload(会重新执行RecycleRuleLoader.loadRules()) - 调试日志:
/malkuth debug(切换DebugLogger.enabled)
当前源码没有 config.yml 的独立 debug 开关,调试以命令切换为准。