市场注册表 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 内部所有条目共享同一个
defaultPaymentItemStack实例引用(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 |
错误在两个时机发给玩家:
- 玩家进服时
FarmingForEngineers.onPlayerLogin(@SubscribeEventPlayerEvent.PlayerLoggedInEvent)逐条聊天栏输出 /farmingforengineers reload执行后立即输出(见 reload 指令)
refuseSave 标志在 JSON 语法错误、tryGetObject/tryGetArray 类型不符、tryParseInt 数字解析失败时置位,阻止把半成品回写覆盖用户的文件。
相关条目
- Market 方块 - 打开本 GUI 的入口
- Merchant - 另一条打开本 GUI 的路径(右键商人)
- FarmingForEngineers.cfg - 控制告警是否上屏的唯一配置项
- reload 指令 - 热重载
Market.json