方块搬运器注册表(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 保持插入顺序。优先级链:

  1. 配置文件的条目先入
  2. preferredMovers(软绑定)只补充配置里没有的键(if (!moverMap.contains(k)))——玩家配置永远压过 mod 软绑定
  3. **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.)。

相关条目