交易与定价系统

基本信息

属性 值
类型 特殊机制
核心类 trade/TradeManager、trade/TradeDatabase、trade/Trade、trade/TradeGroup、util/Wallet
价格来源 JSON 文件(config/vendingmachine/tradeDatabase.json),不能通过 GUI 改价
货币 DreamCraft 的 14 种任务硬币(见下表)

价格从哪里来

交易数据不写在代码里,也不在游戏内编辑,而是从磁盘上的 JSON 读取:

项目 路径 / 值 源码
配置文件名 config/vendingmachine/tradeDatabase.json VMConfig.developer.trade_db_dir(默认 config/vendingmachine)
目录可改 是 Developer.trade_db_dir,@Config.RequiresMcRestart
备份 config/vendingmachine/backup/tradeDatabase.json SaveLoadHandler.writeDatabase() 每次写库前 CopyPaste
手动重载 /vending reload database SubCmdReload → reloadDatabase() + NetTradeDbSync.sendDatabase 广播
强制重写 Developer.force_rewrite_database(默认 false) 存档元数据数量变化时重写整库

一条交易的数据结构

Trade 的 NBT/JSON 字段(Trade.writeToNBT / readFromNBT):

字段 类型 含义
displayItem Compound GUI 上显示的图标;缺省时用 toItems[0]
fromCurrency List 需要支付的货币(CurrencyItem:type + value)
fromItems List 需要消耗的物品
nonConsumedItems List 需要放入但不消耗的物品(条件性材料)
toItems List 产出的物品

TradeGroup 把若干条 Trade 打包成一组「可重复交易组」,额外带:

字段 默认 含义
category unknown 分类,决定 GUI 标签页
cooldown -1 冷却秒数,-1 = 无冷却
maxTrades -1 终身可交易次数,-1 = 无限
requirementSet 空 ICondition 条件集合(由 ConditionParser 解析)

货币(CurrencyType,共 14 种)

全部是 DreamCraft 的任务硬币,物品前缀 + 面值后缀组合成注册名:

枚举 ID 物品前缀 纹理名
ADVENTURE adventure dreamcraft:CoinAdventure adventure
BEES bees dreamcraft:CoinBees bee
BLOOD blood dreamcraft:CoinBlood bm
CHEMIST chemist dreamcraft:CoinChemist chemist
COOK cook dreamcraft:CoinCook cook
DARK_WIZARD darkWizard dreamcraft:CoinDarkWizard wizard
FARMER farmer dreamcraft:CoinFarmer farmer
FLOWER flower dreamcraft:CoinFlower gardener
FORESTRY forestry dreamcraft:CoinForestry ranger
SMITH smith dreamcraft:CoinSmith builder
SPACE space dreamcraft:CoinSpace space
SURVIVOR survivor dreamcraft:CoinSurvivor survivor
TECHNICIAN technician dreamcraft:CoinTechnician technician
WITCH witch dreamcraft:CoinWitch witch

面值(CurrencyItem):

后缀 面值
IV 10000
III 1000
II 100
I 10
(无后缀) 1

识别方式:CurrencyItem.fromItemStack 拿物品注册名去 startsWith(itemPrefix) 匹配, 再用剩余后缀 mapSuffixToValue 换算面值 × 堆叠数。CurrencyItem.itemize() 反向把总面值拆成尽量少的大面额硬币堆 (单堆上限取 outputItem.getItemStackLimit())。

CurrencyType 构造函数显式拒绝 ID 为 ALL 的枚举值(IllegalArgumentException)。

钱包(WalletMode,共 2 种)

枚举 含义 存储
PERSONAL 个人钱包 玩家 UUID 对应数据,默认模式(VMConfig.gui.wallet_mode)
TEAM 团队钱包 gtnhlib TeamDataRegistry 里的团队数据
  • 团队模式需要玩家已加入团队;getWallet() 返回 null 时投币被静默丢弃
  • TeamSettings.soloTeam(默认 false)允许单人队伍
  • 存入方式:把硬币放进 8 个输入格会被拦截(makeInterceptingSlot 的 changeListener 检测到 CurrencyItem 就清空格子), 转而调 wallet.addCount(...) 并播放 vendingmachine:coin_insert;手持硬币 + 服务器端 depositHeldCoins 亦可
  • 取出方式:GUI 上的 3 个退币开关
    • 单币种退币(每个币种一个开关)
    • 全部退币(resetAllCount)
    • 退物品(把 8 个输入格的物品原样搬到 outputBuffer 再清空输入格)

交易执行流程

服务器侧 MTEVendingMachine.processTradeOnServer:

  1. TradeManager.canExecuteTrade(playerId, tg) — 检查条件可见性、次数上限、冷却
  2. checkTrade(trade, playerId, walletMode, simulate=false) — 真正扣款
  3. 逐个 toItems 调 dispenseItemStacks 进入 outputBuffer
  4. TradeManager.executeTrade(playerId, tg) — 记历史、可能排通知
  5. sendTradeUpdate() 推送新列表

checkTrade 内部先把 inputItems 复制一份做模拟:

newInputs   = 8 个输入格的深拷贝
remainNC    = 从 newInputs 扣 nonConsumedItems(不消耗)
remainItems = 从 newInputs 扣 fromItems
remainCur   = 从 Wallet 扣 fromCurrency
  • 装了 ME 售货机上行接口:uplinkHatch.executeTrade(...) 走 AE2 网络抽取(Transaction 校验后 commit),8 个输入格可以不放东西
  • 没装上行接口:三个 remainder 必须全为空才算成功,材料与货币必须实打实塞进 8 个输入格 + 钱包
  • 物品匹配 MTEVendingMachine.matchItem(base, candidate, oreDict):
    • oreDict == null → isItemEqual 精确匹配
    • 指定 oreDict → 走 OreDictionary.getOreIDs 名称匹配;可损坏物品额外要求 metadata 相同

冷却与次数限制

TradeManager.canExecuteTrade 的判定:

条件 源码表达式 含义
条件可见 getAvailableTradeGroups(player).contains(tg) 交易组在玩家可见列表内(或属于 noConditionTrades)
次数未用尽 tg.maxTrades == -1 || tradeCount < tg.maxTrades maxTrades = -1 为无限
无冷却中 cooldownRemaining < 0 见下

cooldownRemaining 只在同时满足以下三条时才算「在冷却」:

  1. tg.cooldown != -1(该组配置了冷却)
  2. history.lastTrade != -1(历史上交易过)
  3. (now - lastTrade) / 1000 < tg.cooldown(距上次交易未超过冷却秒数)
  4. history.cooldownTradeCount >= getMaxTradesInCooldown(player)(团队人数配额已用满)

其中 getMaxTradesInCooldown:

if (VMConfig.team.maxTradeLimit < 1) return 在线队员数;
return Math.min(VMConfig.team.maxTradeLimit, 在线队员数);
  • maxTradeLimit 默认 -1 → 冷却期内允许的免费交易次数 = 当前在线队员数
  • 举例:3 人小队、maxTradeLimit = -1、冷却 60 秒 → 60 秒内前 3 次不进入冷却,第 4 次开始被拦
  • getMaxTradesInCooldown 在 player == null 或 TeamManager.getTeamByPlayer(player) 返回 null(即没有团队)时直接返回 0。 此时 cooldownTradeCount >= 0 恒成立,因此没有加入任何团队的玩家只要交易组配了冷却,第一次交易后立刻进入冷却 (TeamSettings.soloTeam 默认 false,不开单人队伍的话常见于此情况)
  • TradeHistory.executeTrade:cooldownOver 为真时把 cooldownTradeCount 重置为 1,否则 +1

分类(TradeCategory,共 10 种)

枚举 key 标签页
FAVOURITES favourites 收藏(由 FavouritesTracker 动态生成,存 config/vendingmachine/favourites/)
ALL all 全部
COMPONENTS components 组件
RAW raw 原材料
FARMING farming 农场
CHEMISTRY chemistry 化学
MAGIC magic 魔法
BEES bees 蜜蜂
MISC misc 杂项
UNKNOWN unknown 未知(TradeGroup.category 默认值,JSON 里写了无法识别的分类时回退)

标签页顺序固定为:FAVOURITES → ALL → 数据库里出现过的分类(TradeDatabase.getTradeCategories())。

交易历史持久化

数据 位置
玩家交易历史 <世界>/<data_dir>/tradeState/(dirTradeState,data_dir 默认 vendingmachine)
收藏 config/vendingmachine/favourites/
名称缓存 <世界>/<data_dir>/names.json
交易库 config/vendingmachine/tradeDatabase.json

第三方模组集成

模组 集成方式
Better Questing @Optional.Method(modid = "betterquesting"):BqAdapter、BqCondition、BqTradeGroup、PanelQBTrade;Trade.getTradeGui 用 BQ 面板替代默认显示
Applied Energistics 2 ME 售货机上行接口 直连 ME 网络抽取材料与货币
GregTech (gregtech_nh) 硬依赖:未加载时 自动售货机 与外壳完全不注册
NEI NeiRecipeHandler / NeiRecipeCache / NEIConfig,把售货机注册为配方催化剂(usage 0)
WAILA 显示结构是否完整(红/绿状态行);上行接口显示待回注缓存

相关条目