Custom Mapping
Custom Mapping 系统允许 BetterBuildersWands 为特定方块指定自定义放置行为,用于处理 mod 方块的特殊放置逻辑。
用途
某些方块在 Block.getStackedBlock() 返回的 ItemStack 与实际放置的方块不同。例如:
- 青金石矿石的
getStackedBlock(0)返回青金石方块而非青金石矿石 - 红石矿石(点燃/未点燃)之间的映射
CustomMapping 的定位是:指定"看到某方块时,应该用某物品建造、放成某方块"。
数据结构
ICustomMapping 接口的 5 个查询方法:
| 方法 | 返回 | 用途 |
|---|---|---|
getLookBlock() |
Block |
玩家看到的源方块 |
getMeta() |
int |
源方块的 metadata |
getItems(world, player, point) |
ItemStack |
需要消耗的物品(可覆写以按 TileEntity 动态生成) |
getPlaceBlock() |
Block |
实际放下的方块 |
getPlaceMeta() |
int |
实际放下方块的 metadata |
shouldCopyTileNBT() |
boolean |
是否复制 TileEntity 的 NBT |
CustomMappingManager.getMapping(block, meta) 是线性扫描:
只要 getLookBlock() == block && getMeta() == meta 就返回该条,先注册者优先。
配置格式
minecraft:lapis_ore/0=>1*minecraft:lapis_ore/4=>minecraft:lapis_ore/0
解析为:源方块/元数据=>数量*源方块/元数据=>目标方块/元数据
即"看青金石矿石 → 需要 1 个青金石矿石(meta 4) → 放成青金石矿石(meta 0)"。
解析由 CustomMappingManager.parseMappingString 完成,要求整串完全匹配
下面这条正则(多一条少一条都会整条失效并记 error 日志):
final String REGEX = "^(\\w[^:]+:[^/]+)/(\\d+)=>(\\d)+\\*(\\w[^:]+:[^/]+)/(\\d+)=>(\\w[^:]+:[^/]+)/(\\d+)$";
| 组 | 含义 | 解析方式 |
|---|---|---|
| 1 | 源方块名 | Block.getBlockFromName |
| 2 | 源 meta | Integer.parseInt |
| 3 | 数量 | Integer.parseInt |
| 4 | 所需物品名 | GameRegistry.findItem(mod, name) —— 可以是任意物品,不限于方块 |
| 5 | 物品 meta | Integer.parseInt |
| 6 | 目标方块名 | Block.getBlockFromName |
| 7 | 目标 meta | Integer.parseInt |
多条映射用英文逗号分隔。任一 Block / Item 解析为 null 则整条丢弃。
内置映射(来自配置文件 forced_blocks)
Lapis Ore 映射
- 源:
minecraft:lapis_ore/0(元数据 0) - 需求:1 个
minecraft:lapis_ore/4 - 目标:
minecraft:lapis_ore/0 shouldCopyTileNBT:false
Redstone Ore 映射
- 源:
minecraft:lit_redstone_ore/0 - 需求:1 个
minecraft:redstone_ore/0 - 目标:
minecraft:lit_redstone_ore/0 shouldCopyTileNBT:false
⚠️ 这两条来自
ConfigValues.OVERRIDES_RECIPES_DEFAULT, 在preInit阶段由mappingManager.loadConfig(configValues.OVERRIDES_RECIPES)加载。 把forced_blocks设为空字符串会禁用它们(loadConfig直接 return)。
Mod 集成
ArchitectureCraft
ArchitectureCraftCustomMapping.java 继承 CustomMapping,注册 2 条:
| 源方块 | meta | shouldCopyTileNBT |
|---|---|---|
ArchitectureCraft:shape |
0 | true |
ArchitectureCraft:shapeSE |
15 | true |
getItems() 被覆写:从 TileShape 动态生成物品,
shapeTile.newItemStack(1) 并把 metadata 强制设为 getMeta():
public ItemStack getItems(IWorldShim world, IPlayerShim player, Point3d point) {
TileEntity tileEntity = world.getTile(point);
if (tileEntity instanceof TileShape) {
ItemStack itemStack = ((TileShape) tileEntity).newItemStack(1);
itemStack.setItemDamage(getMeta());
return itemStack;
}
return super.getItems(world, player, point);
}
只在 Loader.isModLoaded("ArchitectureCraft") 时调用 register()。
Forestry
ForestryMCCustomMapping.java 继承 CustomMapping,
shouldCopyTileNBT = true,在 PluginManager.Module.ARBORICULTURE.isEnabled() 时
批量注册原木 / 木板 / 台阶 / 楼梯 / 栅栏及其耐火变种:
| Forestry 方块组 | 注册的 meta |
|---|---|
logs |
0, 4, 8 |
logsFireproof |
0, 4, 8 |
planks |
0 |
planksFireproof |
0 |
slabs |
0, 8 |
slabsFireproof |
0, 8 |
slabsDouble |
0 |
slabsDoubleFireproof |
0 |
stairs |
0 ~ 7 |
stairsFireproof |
0 ~ 7 |
fences |
0 |
fencesFireproof |
0 |
getItems() 被覆写,从 TileWood 取出正确的树种/树种状态:
if (tileEntity instanceof TileWood) {
return TileWood.getPickBlock(getLookBlock(), world.getWorld(), point.x, point.y, point.z);
}
这正是 NBT 复制机制的意义:Forestry 原木的树种信息存在
TileWood里,shouldCopyTileNBT = true让 BFS 只复制完全相同 TileEntity 状态的方块, 放置后再把源 TileEntity 的 NBT 写过去。 详见 BFS 方块扩展算法 的过滤 9。
Ztones
Ztones.java 不是 CustomMapping 映射,而是一套独立的特例处理:
Ztones 的 ofanix 物品能让魔杖把圆石视为无限供应。
| 检查点 | 位置 | 效果 |
|---|---|---|
Ztones.isLoaded() |
BetterBuildersWandsMod.init 中 Loader.isModLoaded("Ztones") 时调用 init() |
设置 isLoaded = (OFANIX != null) |
| 上限绕过 | ItemBasicWand.onItemUse |
目标是圆石且背包有 ofanix → numBlocks = wand.getMaxBlocks(...),跳过 countItems |
| 扣取绕过 | BasicPlayerShim.useItem |
同样条件下直接返回全额,不扣任何材料 |
客户端预览(BlockEvents)也实现了同样的上限绕过,
所以看到 4096 格的线框是真实可放置的。
shouldCopyTileNBT 汇总
| 映射来源 | shouldCopyTileNBT |
影响 |
|---|---|---|
配置文件 forced_blocks |
false |
不复制 TileEntity;countItems 只按 item+meta 匹配,不比 NBT |
ArchitectureCraftCustomMapping |
true |
比对 + 复制 TileShape NBT |
ForestryMCCustomMapping |
true |
比对 + 复制 TileWood NBT |
| Backhand 副手临时构造 | false |
1:1 映射,不复制 NBT |
CustomMapping有 5 参构造(shouldCopyTileNBT默认false)和 6 参构造。 配置文件解析走 5 参版本,两个 mod 集成都显式传true。
缓存机制
StackedBlockManager 缓存 Block.getStackedBlock(int) 的调用结果,
减少重复的反射开销:
| 属性 | 值 |
|---|---|
| 缓存键 | `方块注册名 + " |
| 容器 | HashMap<String, ItemStack> |
| 反射方法名 | 优先 func_149644_j(1.7.10 的 MCP 混淆名),回退 createStackedBlock(1.8+) |
| 可见性 | setAccessible(true),并沿 block.getClass() 向上遍历父类查找覆写版本 |
| 生命周期 | 随 mod 实例存活,不清空 |
它只被
WandWorker.getEquivalentItemStack在canSilkHarvest为真时调用, 属于兜底推断路径。装了 Backhand 时副手优先,这条路径通常走不到。
死代码
shims/BlockShim.getPickBlock(...) 是一个基于 Block.getPickBlock 的封装,
但源码中没有任何调用点(ItemBasicWand.onItemUse 与 BlockEvents 中
对应的 getPickBlock 调用都被注释掉了)。模组改用
getStackedBlock / getItemDropped 两条推断路径,不走 getPickBlock。
相关条目
- BFS 方块扩展算法 -
shouldCopyTileNBT如何触发 NBT 比对 - Configuration -
forced_blocks的配置格式 - Backhand 集成 - 临时 1:1 映射
- 放置范围客户端预览 - 预览如何复用映射查询