VendingMachine
[!INFO] Git Commit:
4e6ec60| Updated: 2026-08-22
GTNH(GregTech 5 - New Horizons)1.7.10 分支的自动售货机模组,作者 CubeFury,
源码包 com.cubefury.vendingmachine(注意是 com.cubefury,不是 cubefury),modid vendingmachine。
在 自动售货机 上放一堆物品 + 钱,GUI 里就能像商店一样购买。
交易目录来自 config/vendingmachine/tradeDatabase.json,货币是 DreamCraft 的 14 种任务硬币。
可选安装 ME 售货机上行接口 让机器直接从 AE2 网络取料。
⚠️ 硬依赖 GregTech:
VendingMachine.init()中VendingMachineBlocks.registerBlocks()与VMItems.registerMultis()都在if (isGt5uNhLoaded)里,isGt5uNhLoaded由Loader.isModLoaded("gregtech_nh")决定。没有 gregtech_nh 时本 mod 的方块与物品一个都不注册, 只会留下/vending指令与配置文件。
硬性约束
MTEVendingMachine 的物品形态注册(VMItems.registerMultis())带 @Optional.Method(modid = "gregtech_nh"),
但调用点在运行时 if 判断里。GameRegistry.registerBlock / registerBlock 也全部经由 GregTech 的
GTGenericBlock 构造函数完成,因此本 mod 无法脱离 GregTech 独立运行。
方块列表
- 自动售货机外壳 — 本 mod 唯一的
Block子类,vendingmachine:casing,仅 metadata 0,硬度 5.0(铁块),扳手 / 等级 2 - ME 售货机上行接口 — GTNH MTE 2742,Tier 3,AE2 网格代理,潜行扳手右键切换六面连接
物品列表
- 占位物品 —
vendingmachine:placeholder,交易库引用已删除 mod 的物品时的内部占位符,无法正常获得
多方块结构列表
- 自动售货机 — 2×3×1 共 6 格,控制器 + 5 外壳,8 输入格 / 100 输出格
游戏设定列表
- /vending 指令 —
add/set/reset/reload,权限等级 4(OP 2 级) - 交易与定价系统 — 14 种货币、钱包、冷却、
maxTrades、10 种分类
枚举一览(skill §2.14 穷举)
SortMode(blocks/gui/SortMode.java)— 穷举,共 2 个值
| 枚举值 | lang key | 图标 | 配置默认值 |
|---|---|---|---|
SMART |
vendingmachine.gui.display_sort_smart |
GuiTextures.SORT_SMART |
✅ 默认 |
ALPHABET |
vendingmachine.gui.display_sort_alphabet |
GuiTextures.SORT_ALPHABET |
— |
排序逻辑实际写在 TradeMainPanel.formatTrades() 的 Comparator 里,两者差异如下:
| 规则 | SMART |
ALPHABET |
|---|---|---|
| 收藏优先 | ✅ | ✅ |
| 可交易状态分组 | ✅ getRank() 5 档 |
❌ |
| 冷却剩余时间倒序 | ✅ | ❌ |
| 物品 ID / damage | ✅ | ❌ |
| 交易组序号 | ✅ | ❌ |
| 最终排序 | 上述依次 | getDisplayName().compareTo(...) 纯字母序 |
getRank() 分档(SMART 专用,数字越小越靠前):
| rank | 条件 |
|---|---|
| 1 | 可交易且无冷却 |
| 2 | 可交易但在冷却中 |
| 3 | 不可交易且无冷却 |
| 4 | 不可交易且在冷却中 |
| 5 | 未启用(不满足 ICondition 条件) |
DisplayType(blocks/gui/DisplayType.java)— 穷举,共 2 个值
| 枚举值 | lang key | 图标 | 布局 | 配置默认值 |
|---|---|---|---|---|
TILE |
vendingmachine.gui.display_mode_tile |
GuiTextures.MODE_TILE |
每行 3 个(TILE_ITEMS_PER_ROW = 3),格 47×25 px |
✅ 默认 |
LIST |
vendingmachine.gui.display_mode_list |
GuiTextures.MODE_LIST |
每行 1 个(宽 153 × 高 14 px) | — |
两种布局的 300 个格子在客户端全部预先创建(displayedTradesTiles / displayedTradesList),
每 tick 用 setEnabledIf(display_type == displayType && display != null) 切换可见性。
WalletMode(blocks/gui/WalletMode.java)— 穷举,共 2 个值
| 枚举值 | lang key | 图标 | 配置默认值 |
|---|---|---|---|
PERSONAL |
vendingmachine.gui.display_wallet_personal |
GuiTextures.WALLET_PERSONAL |
✅ 默认 |
TEAM |
vendingmachine.gui.display_wallet_team |
GuiTextures.WALLET_TEAM |
— |
TEAM 模式下玩家没有团队时 TradeManager.getWallet() 返回 null,投币被静默丢弃。
CurrencyType(trade/CurrencyType.java)— 穷举,共 14 个值
全部为 DreamCraft 任务硬币。完整表格见 交易与定价系统。
TradeCategory(trade/TradeCategory.java)— 穷举,共 10 个值
FAVOURITES / ALL / COMPONENTS / RAW / FARMING / CHEMISTRY / MAGIC / BEES / MISC / UNKNOWN。
详见 交易与定价系统。
MusicTrack(blocks/gui/MusicTrack.java)— 穷举,共 2 个值
| 枚举值 | lang key | 音频 ResourceLocation | 配置默认值 |
|---|---|---|---|
NONE |
vendingmachine.gui.display_track_none |
null |
— |
LUNCH_BREAK |
vendingmachine.gui.display_track_lunch_break |
vendingmachine:track.lunch_break |
✅ 默认 |
配置(VMConfig.java,GTNHLib @Config,文件 vendingmachine.cfg)
| 分组 | 字段 | 类型 | 默认值 | 需重启 |
|---|---|---|---|---|
vendingMachineSettings |
gui_refresh_interval |
int | 20 |
需世界重启 |
vendingMachineSettings |
restock_notifications_enabled |
boolean | true |
需世界重启 |
vendingMachineSettings |
restock_notifications_interval |
int | 200 |
需世界重启 |
gui |
display_type |
DisplayType |
TILE |
否 |
gui |
sort_mode |
SortMode |
SMART |
否 |
gui |
wallet_mode |
WalletMode |
PERSONAL |
否 |
gui |
show_coins |
boolean | true |
否 |
gui |
update_coin_icon |
boolean | false |
否 |
team |
soloTeam |
boolean | false |
否 |
team |
maxTradeLimit |
int | -1 |
否 |
music |
current_track |
MusicTrack |
LUNCH_BREAK |
否 |
music |
music_volume |
float(范围 0~2) | 0.75f |
否 |
developer |
data_dir |
String | vendingmachine |
需 MC 重启 |
developer |
trade_db_dir |
String | config/vendingmachine |
需 MC 重启 |
developer |
force_rewrite_database |
boolean | false |
需世界重启 |
另有 @Config.Ignore 的 VMConfig.world_dir(File),由 SaveLoadHandler 在服务器启动时赋值,不写入配置文件。
team.maxTradeLimit = -1 表示「冷却期内允许的免费交易次数 = 当前在线队员数」,
详见 交易与定价系统。
GUI 说明
没有容器 GUI ID。MTEVendingMachine.forceUseMui2() 恒为 true,getGui() 直接 new MTEVendingMachineGui(this),
走 GregTech 的 ModularUI 2 面板系统(MTEMultiBlockBaseGui),本 mod 不含任何 Container / GuiContainer 子类,
所以不存在「GUI ID → Container」映射表。输出物品通过 fillPlayerInventoryWithDispensedItems() 直接塞玩家背包
(GUI 滑槽上 Shift + 左键)。
| 组件 | 源码 | 行为 |
|---|---|---|
| 分类标签 | VendingPageButton |
每个分类一个 PageButton,覆盖贴图 tradeCategories.get(index).getTexture();在 highlightedTabs 中时叠加 20×20 的 TAB_HIGHLIGHT;悬停时图标左右边距各增减 1px(ICON_MARGIN_LEFT = 7 / ICON_MARGIN_RIGHT = 5);点击时写 MTEVendingMachineGui.lastPage |
| 分页 | MUI2 PagedWidget |
宽 162 px;高度 2 + clamp(rootPanel.height - 176, 1, 144),show_coins = false 时再 + COIN_DISPLAY_HEIGHT(55)。每个分类一个 ListWidget(宽 161),交易格子全部预建 300 个 |
| 搜索栏 | SearchBar |
MUI2 BaseTextFieldWidget,占位文本 vendingmachine.gui.search。onUpdate 里文本变化即写静态字段 MTEVendingMachineGui.lastSearch、调 resetTradeDisplayScroll()、setForceRefresh()。搜索语法复用 NEI 的 SearchField.getFilter(...) + ItemFilter,匹配范围是「显示物品 + 所有产出 + 所有消耗」(TradeItemDisplay.satisfiesSearch)。搜索栏聚焦时吞掉所有 typedChar >= 32 且非 127 的按键 |
| 排序/筛选 | TradeMainPanel |
纯客户端(if (syncManager.isClient()))。按住 Shift 或 Ctrl 时停止重建列表,只更新已有格子的 enabled / hasCooldown / cooldownText / isFavourite 等状态 |
| 购买 / 收藏 | TradeItemDisplayWidget |
onMousePressed 要求 shiftHeld != ctrlHeld(必须且只能按住 Shift 或只按住 Ctrl,两个都不按或同时按都 Result.IGNORE)。Shift + 左键 → attemptPurchase 购买;Ctrl + 左键 → FavouritesTracker.toggleFavourites(...) 收藏/取消收藏 |
| 退币/退料 | ejectSingleCoin / ejectCoins / ejectItems |
三个 ToggleButton 同步值。ejectCoins 把钱包里全部币种 itemize() 后退料并 resetAllCount;单币种开关只退该币种;ejectItems 把 8 个输入格物品搬进 outputBuffer 再清空输入格 |
| 音量 | VolumeControlGui + volumeButton |
独立面板,值写 VMConfig.music.music_volume(0~2) |
| 音乐 | VMMusicManager |
打开 GUI 时 startVendingMachineMusic(true),onCloseAction 里 stopVendingMachineMusic() |
| 使用者占用 | NetResetVMUser |
TradeMainPanel.dispose() 时发包释放 currentUser |
下落物品动画(fallingitem 包,已实际接线)
FallingItemSlotFactory 在 MTEVendingMachineGui.addAllItemSlotsAsChildren()(第 588 行)被 new FallingItemSlotFactory(base.outputItems, 18 * 4)
实例化,并为 全部 100 个输出格各生成一个 TransformWidget,不是死代码。
| 参数 | 值 | 说明 |
|---|---|---|
FALL_ANIMATION_DURATION |
1000 ms | 单次下落动画时长 |
MAX_X_POS |
24 px | 横向散布范围 |
| 落距 | 18 * 4 = 72 px |
由 GUI 传入 |
| 插值曲线 | Interpolation.BOUNCE_OUT,bounds(0, 1) |
带回弹 |
| X 分布 | (int)((i * φ % 1.0) * 24),φ = (√5 - 1)/2 |
黄金比例共轭分配:每个新槽落在当前最大的空隙里,视觉上分布均匀但非线性 |
| 触发条件 | 槽位物品变化且 newItem != null && !onlyAmountChanged && !init |
纯数量变化不重播动画 |
配套的 ItemSlotWithDepth 给物品图标加深度偏移,Pos 是可变的动画值对象(copyOrImmutable() 返回快照供 MutableObjectAnimator 终点使用)。
输出格全部调用了 ModularSlot.accessibility(false, true) 并归入 outputSlotGroup 槽位组,
同时 background(IDrawable.EMPTY) + disableHoverBackground(),即不画默认槽位背景与悬停高亮。
音效
| 音效 | 触发 |
|---|---|
vendingmachine:coin_drop |
掉出的物品是货币(CurrencyItem.fromItemStack 非空) |
vendingmachine:item_drop |
掉出的物品是普通物品 |
vendingmachine:coin_insert |
投币成功(insertCoin / depositHeldCoins) |
附魔 / 耐久 / 配方
- 本 mod 不注册任何合成配方。
grep -rn "addRecipe\|addShapelessRecipe"在src/main/java下只命中 NEI 的局部变量名, 真正的GameRegistry调用只有一行registerItem(ItemPlaceholder.placeholder, "placeholder")。 售货机与上行接口的 GTNH 配方由 DreamCraft / 整合包提供,不在本仓库内。 - 不注册任何附魔,占位物品也未覆写
getItemEnchantability(走Item默认)。 - 外壳与两个机器均无耐久。
本 mod 没有的内容(grep 证据)
以下维度经全仓库 grep 确认完全没有实现,因此未建立对应目录。所有命令在
/Users/evlos/a/mirror/VendingMachine/src/main/java 下执行:
| 维度 | grep 模式 | 结果 |
|---|---|---|
| 热键 | grep -rn "KeyBinding|keyBinding|Lwjgl3Key|registerKeyBinding|InputEvent" . |
0 命中 — 无任何 KeyBinding |
| 附魔 | grep -rn "Enchantment" . |
0 命中 |
| Buff / Debuff | grep -rn "PotionEffect|MobEffect|registerPotion|applyPotionEffect" . |
0 命中 |
| 群系 | grep -rn "BiomeGen|BiomeProvider|registerBiome|getModifiableBiomes" . |
0 命中 |
| 世界生成 | grep -rn "IWorldGenerator|WorldGenerator|MapGenStructure|registerWorldGenerator" . |
0 命中 |
| 成就 | grep -rn "Achievement|addAchievement" . |
0 命中 |
| 实体 | grep -rn "extends Entity|registerEntity|EntityRegistry" . |
0 命中 — 无任何 Entity 子类 |
| 容器 GUI | grep -rn "extends Container|extends GuiContainer|ContainerGuiProvider|getContainer" . |
0 命中 — GUI 走 ModularUI 2,无 GUI ID |
| 投掷/药水类物品效果 | grep -rn "extends ItemProjectile|onEntityItemUpdate|onItemUseFirst|onRightClick" . |
0 命中 |
| 维度 | grep -rn "WorldProvider|registerDimension" . |
0 命中。仅 DimensionManager.getWorld(...) 出现在 4 个网络封包 handler 里,用于按维度 ID 取回 World 对象(NetResetVMUser / NetCoinDeposit / NetTradeRequestSync / PacketSender.sendToDimension),与「注册新维度」无关 |
补充说明:
- 没有多方块家族:全仓库
grep -rln "MTEMultiBlockBase"只命中 2 个文件 ——blocks/MTEVendingMachine.java(结构本体)和blocks/gui/MTEVendingMachineGui.java(其 GUI)。 唯一的IStructureDefinition就在MTEVendingMachine里,没有第二个多方块结构。 - 没有
structure/或biome/源码目录:ls -d */在src/main/java下只有com/一个目录。STRUCTURE_DEFINITION是玩家建造蓝图(survivalConstruct/construct),不是世界生成结构。 - 没有独立 TileEntity 类:
MTEVendingMachine/MTEVendingUplinkHatch是 GregTech 的IMetaTileEntity实现,不是net.minecraft.tileentity.TileEntity子类;瓦片数据存在 GTNH 的IGregTechTileEntity(NBT 键inputs/outputs/outputBuffer/pendingInject)。 - CfgElem(
gui/client/VMGuiClientConfig.java)只有 15 行,只是把VMConfig包成 GTNHLibSimpleGuiConfig的游戏内配置界面(super(parentScreen, MODID, NAME, true, VMConfig.class)), 自身不含任何配置字段;所有真实配置项都在VMConfig里。 - 无
EntityPlayer之外的权限检查:MTEVendingMachine.canUse()只判断「有没有别人在用」,MTEVendingUplinkHatch.isAccessAllowed()恒返回true;唯一的权限等级在/vending指令上(等级 4)。 VMGuiClientConfig构造时传入的categorized = true,对应VMConfig里每个分组 (vendingMachineSettings/gui/team/music/developer)各自的@Config.LangKey分类标题。