市场注册表 Market.json

MarketRegistry —— 决定市场 GUI 里卖什么、收什么、怎么分类的全部数据源。这是本 mod 真正的「配置」主体。

基本信息

属性 值 来源
类 com.guigs44.farmingforengineers.registry.MarketRegistry(extends AbstractRegistry) registry/MarketRegistry.java:26
单例 MarketRegistry.INSTANCE registry/MarketRegistry.java:28
注册表名 "Market" registry/MarketRegistry.java:39
配置文件 <config>/FarmingForEngineers/Market.json AbstractRegistry.java:41 + FarmingForEngineers.java:68
加载时机 postInit 调 MarketRegistry.INSTANCE.load(configDir);/farmingforengineers reload 可热重载 FarmingForEngineers.java:104
写入时机 首次运行自动生成带缩进的 JSON;之后若解析器补过字段则回写 AbstractRegistry.java:42,79
客户端同步 打开 GUI 时由 ContainerMarket.detectAndSendChanges 发一次 MessageMarketList(封包判别字 0,S→C) container/ContainerMarket.java:120
购买确认 客户端点条目发 MessageMarketSelect(判别字 1,C→S),服务端 MarketRegistry.getEntryFor 回查 container/ContainerMarketClient.java:44

文件结构

首次运行生成的 Market.json 顶层只有 4 个键(MarketRegistry.create):

键 类型 作用
defaults object(string → boolean) 每个默认批次的开关;键名就是下面的批次名
defaults payment object(string → string) 覆盖某批次的默认货币
defaults blacklist string[] 从默认条目中删掉某些物品
custom entries object(item → cost) 追加/覆盖任意条目

AbstractRegistry 的默认 schema(options / custom.entries 数组)在这里不生效——MarketRegistry.hasCustomLoader() 返回 true,走自己的 load(JsonObject)。

加载顺序(顺序有语义)

MarketRegistry.load(registry/MarketRegistry.java:95)按固定四步执行:

步 动作 覆盖关系
1 读 defaults payment → 逐批次确定货币 缺失/空串 → 写入 getDefaultPayment() 的字符串形式并使用之;解析失败(未知物品)→ 静默回退到 getDefaultPayment()
2 读 defaults → 逐批次 registerDefaults 开关为 true 才注册;键缺失 → 写入 isEnabledByDefault() 并采用
3 读 defaults blacklist → tryRemoveEntry 只能删第 2 步刚注册的条目;删不掉会记一条 error
4 读 custom entries → loadMarketEntry 最后执行且优先级最高:先 tryRemoveEntry 掉同 output 的默认条目,再注册自定义条目

[!NOTE] 由此得出两条实用规则:想让默认批次彻底消失应用 defaults 开关(步骤 2)而不是 blacklist(步骤 3);而 custom entries 里写一个同 output 物品即等于覆盖默认价格。

条目语法

output 与 cost 共用同一个 ItemStack 字符串格式,正则(MarketRegistry.ITEMSTACK_PATTERN):

(?:([0-9]+)\*)?(?:([\w]+):)([\w]+)(?::([0-9]+))?(?:@(.+))?
段 含义 缺省
N* 数量 1
domain:name 必填,物品注册名 无缺省,缺则 logError("Invalid item %s, format mismatch") 并跳过
:meta metadata / 伤害值 0
@<nbt> JsonToNBT 解析的 NBT 挂在物品上 无

示例(custom entries 中的一对 key/value):

output cost 含义
minecraft:oak_sapling 2*minecraft:emerald 1 个橡树树苗换 2 个绿宝石
minecraft:sapling:5 1*forestry:seedCustom 5 号 metadata 树苗换 Forestry 种子
minecraft:spawn_egg 1*minecraft:emerald:8 带 EntityTag 的刷怪蛋可单独定价(meta 8 = 末影珍珠外观)

getEntryFor 匹配用 isItemEqual 加 areItemStackTagsEqual,所以带 NBT 的刷怪蛋与不带 NBT 的刷怪蛋是两条不同条目。

[!WARNING] custom entries 的 __comment 写着 “Prefix with ! to blacklist instead.”,但 loadMarketEntry 只跳过 __comment 与 examplemod:example_item 两个键,没有任何 ! 前缀处理。该特性在当前 commit 未实现;想屏蔽默认条目只能用 defaults blacklist。

默认批次全表

registerDefaultHandler 被调用 10 次,覆盖 5 个 compat addon。EntryType 见下方枚举。

# 批次名(JSON 键) 来源 默认开 默认货币 条目数 归类 内容
1 Vanilla Seeds VanillaAddon 是 1*minecraft:emerald:0 3 SEEDS Items.wheat_seeds、Items.melon_seeds、Items.pumpkin_seeds
2 Vanilla Saplings VanillaAddon 是 1*minecraft:emerald:0 6 ※ SAPLINGS 遍历 BlockSapling.field_149882_a 全部 typeID
3 Bonemeal VanillaAddon 是 1*minecraft:emerald:0 1 OTHER Items.dye meta 15
4 Animal Eggs VanillaAddon 否 1*minecraft:emerald:0 9 OTHER Items.spawn_egg + NBT EntityTag.id,见下表
5 Pams Harvestcraft Seeds HarvestcraftAddon 是 1*minecraft:emerald:0 60 SEEDS 60 个作物名 + seeditem 后缀,见下表
6 Pams Harvestcraft Saplings HarvestcraftAddon 是 1*minecraft:emerald:0 36 SAPLINGS 36 个树名 + _sapling 后缀,见下表
7 Forestry Saplings ForestryAddon 是 2*minecraft:emerald:0 运行时 ※ SAPLINGS 遍历 TreeManager.treeRoot.getIndividualTemplates(),每棵 ITree 取 getMemberStack(tree, 0)
8 Natura Saplings NaturaAddon 是 1*minecraft:emerald:0 9 SAPLINGS natura:overworld_sapling meta 0-3、natura:overworld_sapling2 meta 0-3、natura:redwood_sapling meta 0
9 BiomesOPlenty Saplings BiomesOPlentyAddon 是 1*minecraft:emerald:0 23 SAPLINGS BiomesOPlenty:sapling_0/1/2 各 meta 0-7,跳过圣橡(见 #10)
10 BiomesOPlenty Sacred Oak Sapling BiomesOPlentyAddon 否 1*minecraft:emerald:8 1 SAPLINGS BiomesOPlenty:sapling_1 meta 7

※ 数量不由本仓库决定:#2 的循环上界是原版 BlockSapling.field_149882_a.length(1.7.10 为 6:oak / spruce / birch / jungle / acacia / dark_oak),#7 取决于玩家安装的 Forestry 树种数。两者的数组与类都不在本仓库内。

批次间的实质差异

对比 差异
#7 vs 全部其他批次 唯一的非绿宝石定价:2 个绿宝石;其余全是 1 个
#10 vs #9 货币用 meta 8(绿宝石物品的「末影珍珠」变体外观),且被 #9 显式 continue 跳过,拆成独立批次只为单独定价 + 单独开关
#4 vs #1/#2/#3 唯一默认关闭的原版批次(刷怪蛋可绕过刷怪笼刷生物),定价仍为 1 绿宝石
#5/#6 vs #1/#2 同为 1 绿宝石但条目数差 20 倍 / 6 倍;#5 拼 cropName + "seeditem",#6 拼 treeName + "_sapling"
#8 vs #9 #8 的 redwood_sapling 循环写死 i == 2 ? 1 : 4,即只有 1 个 metadata,另两个各 4 个;#9 一律 8 个

[!NOTE] #1/#2/#3/#4 内部所有条目共享同一个 defaultPayment ItemStack 实例引用(registerEntry 不做 copy()),四条 handler 各持有自己的一个实例。GuiMarket 读 entry.getCostItem().stackSize 只读不写,故无副作用;ContainerMarket.onItemBought 的 decrStackSize 作用于玩家输入槽的另一份 ItemStack,也不受影响。

#4 Animal Eggs 的 9 个 EntityTag.id

# EntityTag.id # EntityTag.id
1 Pig 6 Ocelot
2 Sheep 7 Rabbit
3 Cow 8 PolarBear
4 Chicken 9 Wolf
5 EntityHorse

NBT 结构为 root → EntityTag → {id: "<字符串>"}。源码在此处注明 Praise SideOnly:刷怪蛋 ItemStack 的 NBT 构造被放进 apply()(服务端执行)里。

#5 Pams Harvestcraft Seeds 全 60 个条目

物品 ID 形如 harvestcraft:<crop>seeditem。按源码数组顺序(缺失物品静默跳过,不报错):

blackberry、blueberry、candleberry、raspberry、strawberry、cactusfruit、asparagus、barley、oats、rye、corn、bambooshoot、cantaloupe、cucumber、wintersquash、zucchini、beet、onion、parsnip、peanut、radish、rutabaga、sweetpotato、turnip、rhubarb、celery、garlic、ginger、spiceleaf、tealeaf、coffeebean、mustardseeds、broccoli、cauliflower、leek、lettuce、scallion、artichoke、brusselsprout、cabbage、spinach、whitemushroom、bean、soybean、bellpepper、chilipepper、eggplant、okra、peas、tomato、cotton、pineapple、grape、kiwi、cranberry、rice、seaweed、curryleaf、sesameseeds、waterchestnut

#6 Pams Harvestcraft Saplings 全 36 个条目

物品 ID 形如 harvestcraft:<tree>_sapling:

apple、almond、apricot、avocado、banana、cashew、cherry、chestnut、coconut、date、dragonfruit、durian、fig、gooseberry、grapefruit、lemon、lime、mango、nutmeg、olive、orange、papaya、peach、pear、pecan、peppercorn、persimmon、pistachio、plum、pomegranate、starfruit、vanillabean、walnut、cinnamon、maple、paperbark

条目总数

情形 条目总数
仅原版 4 个批次(#1~#4) 3 + 6 + 1 + 9 = 19
+ HarvestCraft(#5/#6) 115
+ Natura(#8) 124
+ BiomesOPlenty(#9) 147
+ Forestry(#7,数量取决于安装的树种) 147 + N
+ 开启两个默认关闭批次(#4 #10 已在上面的 #4 里;此处指 #10) 148 + N

custom entries 里每追加一条即 +1;defaults blacklist 每命中一条即 -1。

EntryType 全枚举(3 值)

MarketEntry.EntryType,客户端筛选按钮与列表排序都基于 ordinal()。

ordinal 常量 tooltip 键 按钮贴图 U 触发条件
0 SEEDS gui.farmingforengineers:market.tooltip_seeds = Seeds 196 + 0 × 20 = 196 默认批次显式指定;自定义条目若 getUnlocalizedName() 含 seed
1 SAPLINGS ...tooltip_saplings = Saplings 216 默认批次显式指定;自定义条目若未本地化名含 sapling
2 OTHER ...tooltip_other = Other 236 兜底值

按钮贴图统一取自 farmingforengineers:textures/gui/market.png,getIconTextureY() 基值 14,hover 时 +20,非当前筛选态时 +40。

排序:ContainerMarketClient 用 Comparator.comparingInt(o -> o.getType().ordinal()) 稳定排序,故市场内顺序恒为「全部种子 → 全部树苗 → 全部其他」,同类别内保持注册顺序。

客户端筛选与搜索

ContainerMarketClient(extends ContainerMarket)新增 4 个字段:itemList / filteredItems / currentSearch / currentFilter / isDirty / scrollOffset。

机制 行为
搜索框 GuiMarket 在 (guiLeft + xSize - 78, guiTop - 5) 放 70 × 10 的 GuiTextField;逐键触发 container.search(text),按 getOutputItem().getDisplayName().toLowerCase().contains(query.toLowerCase()) 过滤,只匹配显示名,不匹配物品 ID
分类按钮 3 个 20 × 20 按钮(buttonId 1/2/3)位于 width/2 + 87、height/2 - 80 / -60 / -40;再点同一个按钮即取消筛选(setFilterType(null))
筛选逻辑 无搜索词且无分类 → 直接全量;否则搜索词与分类同时生效(AND)
清空搜索 在搜索框上中键(button == 1)点击清空
滚动 滚轮 / 拖动滚动条;VISIBLE_ROWS = 4 行 × 3 列 = 12 格可见;滚动条 7 × 77 px,颜色 0xFFAAAAAA
翻页偏移 populateMarketSlots 从 scrollOffset * 3 开始填 12 个 FakeSlotMarket,越界置 null

购买流程与槽位布局

ContainerMarket 共 50 个槽位(container/ContainerMarket.java:46):

slot index 数量 坐标 类型 内容
0 1 (23, 39) Slot 付款槽,绑定 marketInputBuffer(InventoryBasic,容量 1,canTakeStack = false)
1 1 (61, 39) SlotMarketBuy 出货槽,绑定 marketOutputBuffer(同类型,容量 1)
2 ~ 13 12 (102 + j*18, 11 + i*18) FakeSlotMarket 市场条目展示格,i 4 行 / j 3 列
14 ~ 40 27 (8 + j*18, 92 + i*18) Slot 玩家主物品栏 3 行
41 ~ 49 9 (8 + i*18, 150) Slot 玩家快捷栏

FakeSlot 的 putStack / decrStackSize / onSlotChanged 全部 no-op,isItemValid 与 canTakeStack 恒 false,getSlotStackLimit 为 64——纯粹是只读展示格,不能被 vanilla 的 Container.mergeItemStack 逻辑污染。

交易规则

规则 实现
选中条目 客户端 slotClick 命中 FakeSlotMarket → 本地设 selectedEntry 并发包;服务端 selectMarketEntry → getEntryFor
预览输出 onCraftMatrixChanged 把 selectedEntry.getOutputItem().copy() 写进 marketOutputBuffer 槽 0;未选中则写 null
能否取货 SlotMarketBuy.canTakeStack → container.isReadyToBuy():付款非空 且 已选中条目 且 payment.isItemEqual(costItem) 且 payment.stackSize >= costItem.stackSize
扣款 SlotMarketBuy.onPickupFromSlot → onItemBought() → marketInputBuffer.decrStackSize(0, cost.stackSize),再刷新输出
放错物品 func_94530_a(ItemStack, Slot) 被覆写为「slot.inventory != marketOutputBuffer」,即禁止 Shift 合并进出货槽;SlotMarketBuy.isItemValid 恒 false
关闭界面 onContainerClosed 把付款槽残留物 player.entityDropItem(stack, 1f) 丢在脚下
距离/权限 canInteractWith(EntityPlayer) 恒 return true,无距离与归属校验
自动付款预填 transferStackInSlot 对「未选中时按 Items.emerald、已选中时按 costItem」的判定做特例,把这类物品优先 mergeItemStack 到 slot 0

[!NOTE] 市场没有红石/按钮/自动化接口:SlotMarketBuy 只能靠玩家鼠标点击取出,没有任何 TileEntity 状态机、tick 逻辑或比较器输出。TileMarket 是空类(见 Market 方块)。整条交易链路纯粹是「玩家点格子 → 服务端发包确认 → 客户端扣款」。

错误与告警上报

AbstractRegistry 维护静态 List<String> registryErrors:

方法 记录到 registryErrors 写日志
logError 总是 logger.error
logWarning 仅当 ModConfig.showRegistryWarnings == true(见 FarmingForEngineers.cfg) logger.error
logUnknownItem / logUnknownFluid / logUnknownOre 总是 logger.error

错误在两个时机发给玩家:

  1. 玩家进服时 FarmingForEngineers.onPlayerLogin(@SubscribeEvent PlayerEvent.PlayerLoggedInEvent)逐条聊天栏输出
  2. /farmingforengineers reload 执行后立即输出(见 reload 指令)

refuseSave 标志在 JSON 语法错误、tryGetObject/tryGetArray 类型不符、tryParseInt 数字解析失败时置位,阻止把半成品回写覆盖用户的文件。

相关条目