跳到主要内容

回收系统

本文基于当前源码中的 RecycleRuleLoader + RecycleShopSession 行为整理,适用于最新 recycle/*.ymlshop/recycle*.yml 配置。

配置入口

  • 回收规则文件:plugins/Malkuth/recycle/*.yml
  • 回收商店文件:plugins/Malkuth/shop/*.yml
  • 商店模式:recyclerecycle_shop / recycle-shop 都会进入回收会话,但源码会把它们解析成两个不同模式值;其中 recycle_shop 仍要求配置 currency
  • 规则引用:商店 goods.<key>.id 对应 recycle/*.yml 顶层规则 ID

默认资源里包含 recycle/custom_fishing.yml,可直接作为 CustomFishing 回收的模板;recycle/example.yml 还包含 source_marked_item,展示 sourcenbt 匹配写法。

回收规则结构(recycle/*.yml)

每条规则的顶层结构如下:

字段必填说明
display图标显示,含 materialnamelore;缺省时自动使用默认值
check匹配条件,支持“单组 AND”或 check.or 多组 OR
rewards奖励列表,支持 material / currency / source / kether
limitation规则级限制,含 windowplayerglobalcustom_hourscron_expression
messages规则消息,含 successno_itemlimit_reachedcondition_failedremove_failedsuccess 可用 reward_* / recycled_*;在 shop/recycle*.yml 会话里,no_item 与自定义 limit_reached 还能读取商店上下文占位符;自定义 limit_reached 另可用 {limit_reason}
ifKether 条件,单次点击回收时会校验

当前回收链路不读取旧版 price 规则格式;请使用 rewards 定义奖励。

匹配逻辑

check 当前有两种写法:

  1. 单组 AND:check.<type> 直接并列
  2. 多组 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_lorelore 中包含指定文本
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 类型)指定捕获组编号,捕获结果可供奖励表达式使用
operatorenchant_level / durability / amount)比较运算符:==!=>>=<<=
path / key / expect / equals(仅 nbt / !nbt Map 写法)指定 NBT 路径与期望值
source / namespace / item / id(仅 source / !source Map 写法)指定物品源、可选命名空间与物品 ID

反向匹配

当前支持 ! 前缀的类型为:materialdisplay_namecontains_namecontains_loreregex_nameregex_loremodel_dataloreenchantenchant_levelunbreakableitem_flagsnbtsource / item_sourcecustom_fishing
durabilityamount 目前没有反向枚举,建议使用 operator 表达范围。

反向匹配示例:

"!contains_lore":
uncolor: true
match: 绑定

source / nbt 的实际写法

sourcenbt 都支持字符串写法和 Map 写法:

类型字符串写法Map 写法当前实现说明
sourcemythicmobs:legendary_boxitemsadder:weapons:legendary_bladecraftengine:weapon:legendary_blademythicmobssource + item,或 source + namespace + item会调用对应物品源的 getId(item);只写 source 时表示“来自这个物品源的任意物品”
nbtmalkuth_item_sourcemalkuth_item_source=mythicmobsCustomFishing.id=salmon_kingpath/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 "{{ &regex_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.ymlsource_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 是列表,逐条独立执行概率判定。

类型必填字段说明
materialmaterial, amount给予原版材料物品
currencyamount发放货币(当前实现通过 Vault 账户发放)
sourceitem, amount(可选)通过物品源发放,item 格式为 source:itemId;若物品源自身带命名空间,后半段可继续包含 :(如 mm:legendary_boxitemsadder:weapons:legendary_blade
kethercommands执行 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 "{{ &regex_lore }} * 1.2" }}'
变量说明
{{ &regex_name }}regex_name 检测类型捕获的内容
{{ &regex_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 / PERMANENT
  • currency_limit.global 会被解析,但当前 LimitManager 未执行该项校验
  • 单条规则点击回收时,messages.limit_reached 已生效;你可以用 {limit_reason} 显示实际拒绝原因
  • 一键回收 / 同类回收的成功与失败提示仍为会话内置文本,不走 rules.messages
  • 货币上限校验使用“预估值”(固定值直接取值,区间取平均值,表达式按 0 估算)
  • shop_player_* / shop_global_* / currency_* 占位符当前固定读取日统计;goods_player_* / goods_global_* 当前也固定读取规则的日统计,不会跟随 WEEKLY / MONTHLY / CUSTOM / CRON
  • target_inventory 对单条回收、一键回收、预览生效;malkuth recycle same 当前匹配阶段仍直接扫描玩家主背包,不会改用外部背包提供者
  • rollback.enabled 当前只会在“点击单条规则回收”时保存快照;一键回收与同类回收不会写入可撤回快照

回收商店配置(shop/recycle*.yml)

路径默认值当前状态说明
modenormal生效回收模式可使用 recycle;若使用 recycle_shop / recycle-shop,当前序列化器会继续要求 currency 节点
target_inventorynull生效回收目标背包:vanilla / legendwarehouse / soulringx;未知或不可用提供者会回退到原版背包
recycle.one_click_enabledtrue生效控制 malkuth recycle all
recycle.confirm_before_recycletrue仅解析当前会话未使用二次确认逻辑
recycle.preview_limit10仅解析当前预览逻辑未按该值截断输出;默认样例 shop/recycle_shop.yml 填写的是 8
recycle.filters.whitelist[]仅解析当前回收流程未执行白名单过滤;配置格式为 - material: MATERIAL_NAME
recycle.filters.blacklist[]仅解析当前回收流程未执行黑名单过滤;配置格式为 - material: MATERIAL_NAME
recycle.protect.lock_named_itemstrue生效保护有显示名物品不被回收
recycle.protect.lock_enchanted_itemstrue生效保护附魔物品不被回收
recycle.protect.lock_favorite_marked_itemstrue生效按 NBT 标记保护收藏物品
recycle.protect.favorite_tag_keysmalkuth.favorite,malkuth.locked,favorite,locked生效收藏标记键列表
recycle.rollback.enabledfalse生效开启后允许保存最近一次回收快照;当前仅单条规则回收会写入快照
recycle.rollback.expire_seconds30生效撤回有效时间(秒)
recycle.shop_level_limit.count_limitnull生效商店级次数限制(玩家/全局)
recycle.shop_level_limit.currency_limit.playernull生效商店级玩家货币限制
recycle.shop_level_limit.currency_limit.globalnull仅解析当前未执行全局货币限制校验
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 会额外注入下面这批商店上下文;successcondition_failedremove_failed 不会自动带上这些值。

占位符来源说明
{mode}serializer.mode.name当前商店模式名,通常是 RECYCLERECYCLE_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/*.ymldisplay,不是 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_amountpreview_currency_amounthas_currency / {balance} 当前直接使用 Kotlin 的数值字符串
  • {limit} / {limit_remaining} 只会比较商店级次数上限与规则级次数上限的剩余值,不会把 currency_limit 计入这两个占位符
  • 标题、普通图标、one_click_successpreview_headerpreview_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 开关,调试以命令切换为准。