交易与定价系统
基本信息
| 属性 | 值 |
|---|---|
| 类型 | 特殊机制 |
| 核心类 | 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:
TradeManager.canExecuteTrade(playerId, tg)— 检查条件可见性、次数上限、冷却checkTrade(trade, playerId, walletMode, simulate=false)— 真正扣款- 逐个
toItems调dispenseItemStacks进入outputBuffer TradeManager.executeTrade(playerId, tg)— 记历史、可能排通知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 只在同时满足以下三条时才算「在冷却」:
tg.cooldown != -1(该组配置了冷却)history.lastTrade != -1(历史上交易过)(now - lastTrade) / 1000 < tg.cooldown(距上次交易未超过冷却秒数)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 | 显示结构是否完整(红/绿状态行);上行接口显示待回注缓存 |
相关条目
- 自动售货机 - 消费与产出
- ME 售货机上行接口 - AE2 直连替代手动塞料
- /vending 指令 - 唯一能改余额的入口