方块搬运器注册表(MovingTileRegistry)
决定"某个方块块该怎么被搬"的查表系统。由玩家可改的配置(
mover registry)、mod 内置的优先绑定(registerPreferredMover)、以及 mod 强制的锁定(registerMandatoryMover)三层叠加而成。
代码位置:src/main/scala/mrtjp/relocation/registry.scala。
核心设计:注册表本身就是搬运器
object MovingTileRegistry extends ITileMover —— 它实现了 ITileMover,扮演门面(facade):
override def canMove(w: World, x: Int, y: Int, z: Int) = {
val meta = w.getBlockMetadata(x, y, z)
w.getBlock(x, y, z) match {
case block: Block => getHandler(block, meta).canMove(w, x, y, z)
case _ => false
}
}
move 与 postMove 同构。getHandler 每次调用都重新读一次方块的 metadata——所以同一个 Block 的不同 meta 可以绑到不同搬运器。
六张表
| 字段 | 类型 | 作用 |
|---|---|---|
moverNameMap |
Map[String, ITileMover] |
名称 → 搬运器实例 |
moverDescMap |
Map[String, String] |
名称 → 描述(写进配置注释) |
blockMetaMap |
Map[(Block, Int), ITileMover] |
(方块, meta) → 搬运器;meta -1 表示"所有 meta" |
modMap |
Map[String, ITileMover] |
modid → 搬运器 |
defaultMover |
ITileMover |
兜底搬运器 |
preferredMovers / mandatoryMovers |
Seq[(String, String)] |
API 注册的软绑定 / 硬锁定 |
registerTileMover(name, desc, m) 是唯一的注入口,同时填 moverDescMap 与 moverNameMap。
查找优先级(四级回退)
private def getHandler(b: Block, m: Int) = {
blockMetaMap.getOrElse(
(b, m),
blockMetaMap.getOrElse(
(b, -1),
modMap.getOrElse(BlockLib.getModId(b), defaultMover)
)
)
}
| 优先级 | 键 | 匹配范围 |
|---|---|---|
| 1 | (b, m) |
精确到这个方块的这个 metadata |
| 2 | (b, -1) |
这个方块的所有 metadata |
| 3 | BlockLib.getModId(b) |
这个 mod 的所有方块 |
| 4 | defaultMover |
兜底 |
⚠️ defaultMover 初始为 null(var defaultMover: ITileMover = _)。若配置里没有 default 条目,getHandler 返回 null,canMove 会抛 NPE。默认配置恰好提供了 default -> saveload。
键的语法
四条正则(registry.scala:24-27):
val rKeyVal = raw"([^\s]+.+[^\s]+)\s*->\s*([^\s]+.+[^\s]+)".r
val rName = raw"([^\s]+.+[^\s]+)".r
val rNameMetaM = raw"([^\s]+.+[^\s]+)m(\d+)".r
val rMod = raw"mod:([^\s]+.+[^\s]+)".r
| 键 | 含义 | 落到哪张表 |
|---|---|---|
default |
默认搬运器 | defaultMover |
mod:<modID> |
该 mod 全部方块 | modMap(仅当 Loader.isModLoaded(mod)) |
<modID>:<blockname> |
该方块所有 meta | blockMetaMap((b, -1)) |
<modID>:<blockname>m<meta> |
该方块该 meta | blockMetaMap((b, meta)) |
fixName 补前缀:名字里没有 : 就自动加 minecraft:,所以 wooden_door -> saveload 与 minecraft:wooden_door -> saveload 等价。
⚠️ rNameMetaM 的 meta 部分是 (\d+),只接受非负整数。m-1 之类会落到 rName 分支被当作方块名(然后 Block.getBlockFromName 返回 null,blockMetaMap += (null, -1) -> h)。
setMover 的三条分支
def setMover(that: String, m: String) {
if (!moverNameMap.contains(m)) return
val h = moverNameMap(m)
that match {
case "default" => defaultMover = h
case rMod(mod) if Loader.isModLoaded(mod) => modMap += mod -> h
case _ => blockMetaMap += parseBlockMeta(that) -> h
}
}
- 右值(搬运器名)不存在时静默
return——配置里写错名字不报错、也不生效 mod:形式在 mod 未加载时静默跳过,并且会继续往下匹配:因为case rMod(mod) if Loader.isModLoaded(mod)的守卫为假,模式匹配会落到case _,把"mod:Foo"整个字符串当方块名去parseBlockMeta→fixName("mod:Foo")含:故原样 →Block.getBlockFromName("mod:Foo")→null→ 往blockMetaMap塞一条(null, -1)的垃圾条目。无害但会污染表。
三层叠加的装配顺序
def parseAndSetMovers(kv: Seq[String]) = {
var moverMap = ListMap(parseKV(kv): _*)
for ((k, v) <- preferredMovers)
if (!moverMap.contains(k)) moverMap += k -> v
for (pair <- mandatoryMovers) moverMap += pair
moverMap.foreach(h => setMover(h._1, h._2))
moverMap.map(p => p._1 + " -> " + p._2).toArray
}
用 ListMap 保持插入顺序。优先级链:
- 配置文件的条目先入
preferredMovers(软绑定)只补充配置里没有的键(if (!moverMap.contains(k)))——玩家配置永远压过 mod 软绑定- **
mandatoryMovers(硬锁定)**无条件+=覆盖——mod 硬锁定永远压过玩家配置
返回值是归一化后的 "key -> value" 字符串数组,被写回配置文件(见 配置文件)。因此配置文件每次加载都被重写成规范形式。
parseKV 对不满足 rKeyVal 的行抛 MatchError:
def parseKV(kv: Seq[String]) = kv.map {
case rKeyVal(k, v) => (k, v);
case s => throw new MatchError(s"Illegal [k -> v] pair: $s")
}
MatchError 是 scala.MatchError,不是 IllegalArgumentException——配置写坏会让 FML 初始化直接失败。
三个内置搬运器
在 RelocationProxy_server.preinit() 里注册,描述文本是配置 GUI 里的原文:
| 名称 | 类 | 配置描述原文 |
|---|---|---|
saveload |
SaveLoadTileMover |
Saves the tile and then reloads it in the next position. Reliable but CPU intensive. |
coordpush |
CoordPushTileMover |
Physically changes the location of tiles. Works if tiles do not cache their position. |
static |
StaticTileMover |
Setting this disables movement for the specified block. |
saveload —— 存盘再重载(默认)
override def move(w: World, x: Int, y: Int, z: Int, side: Int) {
val (b, meta, te) = getBlockInfo(w, x, y, z)
val pos = new BlockCoord(x, y, z).offset(side)
val tag = if (te != null) {
val tag = new NBTTagCompound
te.writeToNBT(tag)
tag.setInteger("x", pos.x); tag.setInteger("y", pos.y); tag.setInteger("z", pos.z)
te.onChunkUnload()
w.removeTileEntity(x, y, z)
tag
} else null
uncheckedSetBlock(w, x, y, z, Blocks.air, 0)
uncheckedSetBlock(w, pos.x, pos.y, pos.z, b, meta)
if (tag != null) {
TileEntity.createAndLoadEntity(tag) match {
case te: TileEntity => w.getChunkFromBlockCoords(pos.x, pos.z).addTileEntity(te)
case _ =>
}
}
}
流程:writeToNBT → 把 NBT 里的 x/y/z 改成目标坐标 → 调 onChunkUnload()(给方块实体一个"我要走了"的钩子)→ 移除原方块实体 → 写空气 → 写目标方块 → TileEntity.createAndLoadEntity(tag) 重新构造一个新实例并挂到目标区块。
优点:任何正确实现 NBT 序列化的方块实体都能搬。代价:CPU 开销大(每次移动都做完整 NBT 往返),且 onChunkUnload 可能被误当作区块卸载而做清理工作。
getBlockInfo(w,x,y,z) 来自 MrTJPCore 的 WorldLib._,返回 (Block, meta, TileEntity)。
coordpush —— 原地改坐标
override def move(w: World, x: Int, y: Int, z: Int, side: Int) {
val (b, meta, te) = getBlockInfo(w, x, y, z)
val pos = new BlockCoord(x, y, z).offset(side)
if (te != null) { te.invalidate(); uncheckedRemoveTileEntity(w, x, y, z) }
uncheckedSetBlock(w, x, y, z, Blocks.air, 0)
uncheckedSetBlock(w, pos.x, pos.y, pos.z, b, meta)
if (te != null) {
te.xCoord = pos.x; te.yCoord = pos.y; te.zCoord = pos.z
te.validate()
uncheckedSetTileEntity(w, pos.x, pos.y, pos.z, te)
}
}
同一个 TileEntity 实例,invalidate() → 改三个坐标字段 → validate()。快、无 NBT 开销,但方块实体若缓存了自己的坐标就彻底错位——所以它只绑定给已知不缓存坐标的 mod(见下表)。
uncheckedRemoveTileEntity / uncheckedSetTileEntity 是 MrTJPCore 的 WorldLib 方法。
static —— 钉死
override def canMove(w: World, x: Int, y: Int, z: Int) = false
override def move(...) {}
override def postMove(...) {}
canMove 恒 false,其余为空。它是唯一能主动否决移动的搬运器:把某方块绑到 static 上,它就不参与任何结构。注意——搬运器只在 运动模型 的 tryStartMove 之后才被查询,而 tryStartMove 只检查 canRunOverBlock(空气/软方块),不检查 canMove。所以 static 的 canMove=false 实际上不会阻止方块被纳入结构,只会让搬运阶段什么都不做(方块留在原地,结构错乱)。源码如此。
内置的 16 条 preferredMover 软绑定
RelocationProxy_server.preinit() 里注册(全部走 registerPreferredMover,即软绑定,玩家可在配置里改):
| 键 | 值 | 数量 |
|---|---|---|
default |
saveload |
1 |
mod:minecraft |
coordpush |
1 |
mod:Relocation |
coordpush |
1 |
mod:ComputerCraft / EnderStorage / ChickenChunks / Translocator |
coordpush |
4 |
mod:ProjRed|Compatibility |
coordpush |
1 |
mod:ProjRed|Core |
coordpush |
1 |
mod:ProjRed|Expansion |
coordpush |
1 |
mod:ProjRed|Exploration |
coordpush |
1 |
mod:ProjRed|Fabrication |
coordpush |
1 |
mod:ProjRed|Illumination |
coordpush |
1 |
mod:ProjRed|Integration |
coordpush |
1 |
mod:ProjRed|Transmission |
coordpush |
1 |
mod:ProjRed|Transportation |
coordpush |
1 |
ProjectRed 9 个子模块各自一条,注意是带 | 的完整子 modid(ProjRed|Core 等),不是通配。
⚠️ mod:Relocation 是一条失效条目
本 mod 的 modid 是 ForgeRelocation(RelocationMod.modID = "ForgeRelocation"),而软绑定写的是 mod:Relocation:
API.registerPreferredMover("mod:Relocation", "coordpush")
setMover 匹配 rMod 后要过 Loader.isModLoaded("Relocation") 守卫——FML 里不存在名为 Relocation 的 mod,守卫为假,该条目被完全丢弃。所以 ForgeRelocation 自己的方块(移动占位方块)走的是 default -> saveload(canMove 对 saveload 恒 true),而不是这条本意给它的 coordpush。
同时因为模式匹配落到 case _,"mod:Relocation" 会被当方块名塞进 blockMetaMap 的 (null, -1) 键(见上文 setMover 分析)。源码现状,未修正。
通路检查
tryStartMove 只用到一个方法:
def canRunOverBlock(w: World, x: Int, y: Int, z: Int) = {
if (w.blockExists(x, y, z))
w.isAirBlock(x, y, z) || WorldLib.isBlockSoft(w, x, y, z, w.getBlock(x, y, z))
else false
}
与 ITileMover.canMove 完全无关——方块能不能被移动到目标位置,判定依据是"目标格是不是空气或可被替换的软方块",而不是搬运器自己。同理,MCFrames 卡扣注册表 里 StickResolver_Impl 那行 MovingTileRegistry.canMove 检查是被注释掉的(源码注释:Dont ignore non-movables, have them halt movement.)。
相关条目
- 运动模型(结构与行的分解) ——
MovingTileRegistry.move/postMove的调用时机与顺序 - 配置文件 ——
mover registry的格式、moveLimit与归一化回写 - API 接口 ——
registerTileMover/registerPreferredMover/registerMandatoryMover三个注册方法的约束 - MCFrames 卡扣注册表 —— 结构解析,与搬运器正交