马达方块(BlockMotor)

MCFrames 唯一的动力源:一个 6 面 × 4 姿态(24 组合)的方块,接受红石信号后沿自己的正面把整条相连的框架结构推一格,耗时 16 tick。

方块名 mcframes.motor,类 mrtjp.mcframes.motor.BlockMotor;方块实体 TileMotor。

基本属性

属性 值 来源
方块名 mcframes.motor motor.scala:28
基类 mrtjp.core.block.InstancedBlock motor.scala:28
材质 Material.iron motor.scala:28
硬度 setHardness(5f) motor.scala:29
抗爆 setResistance(10f) motor.scala:30
脚步声 Block.soundTypeMetal motor.scala:31
创造栏 CreativeTabs.tabTransport motor.scala:32
TileEntity addSingleTile(classOf[TileMotor]) motor.scala:33
isSideSolid true(六面均实心,与框架方块相反) motor.scala:35-41
renderAsNormalBlock true motor.scala:43

马达方块没有物品配方——GameRegistry.addRecipe 在整个 jar 里只出现一次,属于 框架方块。马达只能靠框架方块的配方产出路径之外的途径获得(或由其他 mod 提供)。

朝向:一个字节打包 6 位

TileMotor extends InstancedBlockTile with TTileOrient with IFrame。姿态全部存在 MrTJPCore 的 TTileOrient 里,是一个 Byte:

var orientation: Byte = 0
def side      = orientation >> 2     // 高 2 位:正面朝向(水平 0~3)
def rotation  = orientation & 0x3    // 低 2 位:绕正面轴的滚转(0~3)
def absoluteDir(absRot: Int) = Rotation.rotateSide(side, absRot)  // codechicken.lib.vec.Rotation
字段 含义
side 水平正面索引(0~3),即"推的方向"的反向
rotation 4 个滚转档位,决定贴图取哪张

setSide / setRotation 都会在值真正变化时回调 onOrientChanged(oldOrient)。

放置时的姿态

override def onBlockPlaced(side: Int, meta: Int, player: EntityPlayer, stack: ItemStack, hit: Vector3) {
  super.onBlockPlaced(side, meta, player, stack, hit)
  setSide(side ^ 1)
  setRotation(Rotation.getSidedRotation(player, side ^ 1))
}

side 参数是玩家点中的那个面,side ^ 1 是它的反向——也就是"背靠支撑、朝外推"的方向。所以马达放在地板上、点地板顶面时,马达自身的 side 字段指向它所依附的那一侧。Rotation.getSidedRotation(player, side ^ 1) 依据玩家视角决定初始滚转。

交互:右键切换

override def onBlockActivated(player: EntityPlayer, s: Int): Boolean = {
  if (super.onBlockActivated(player, s)) return true
  if (player.isSneaking) setRotation((rotation + 1) % 4)
  else setSide((side + 1) % 6)
  true
}
  • 潜行 + 右键:rotation 前进一档(0→1→2→3→0),切换贴图(用于换朝向的方块)
  • 普通右键:setSide((side + 1) % 6)——⚠️ 这里对 6 取模,但 side 实际是 orientation >> 2,只有 0~3 四个有效值。setSide 写入时是 (orientation & 0x3 | s << 2),传入 4 或 5 会把 orientation 推到 16 或 20(越出 1 字节无妨,但 side 变成 4/5,超出 Rotation.sideRotations 的 0~3 范围)。源码原文如此,未做范围保护。

注意 setSide 的实现是位或而非替换——orientation & 0x3 | s << 2 先清掉高 2 位再写入,所以传入 4 会得到 0b10_00 | 4 = 16。此处行为与设计意图不完全一致,属源码现状。

抓取规则(IFrame)

override def stickOut(w: World, x: Int, y: Int, z: Int, side: Int) = side == (this.side ^ 1)
override def stickIn(w: World, x: Int, y: Int, z: Int, side: Int)  = side != (this.side ^ 1)

与 框架方块 的"六面全 true"不同:马达只在正面 side ^ 1 伸出,其余 5 面 stickOut 为 false(不会主动抓别人);反过来 stickIn 在除正面外的 5 面为 true(可以被侧面的框架连成一体)。

驱动逻辑

update() 每 tick 执行(InstancedBlockTile 的更新钩子):

override def update() {
  if (!world.isRemote && world.getBlockPowerInput(x, y, z) > 0) {
    val pos = position.offset(side ^ 1)
    if (world.isAirBlock(pos.x, pos.y, pos.z)) return

    if (!RelocationAPI.instance.isMoving(world, pos.x, pos.y, pos.z) &&
        !RelocationAPI.instance.isMoving(world, x, y, z)) {
      val blocks = MCFramesAPI.instance.getStickResolver
        .getStructure(world, pos.x, pos.y, pos.z, new BlockPos(x, y, z))

      val r = RelocationAPI.instance.getRelocator
      r.push()
      r.setWorld(world)
      r.setDirection(getMoveDir)
      r.setSpeed(1 / 16d)
      r.addBlocks(blocks)
      r.execute()
      r.pop()
    }
  }
}

逐条拆解:

条件 作用
!world.isRemote 仅服务端执行;客户端不重复发起
world.getBlockPowerInput(x,y,z) > 0 需要红石信号(getBlockPowerInput 在无信号时返回 0)
目标格是空气则 return 前面没东西就不推
目标格与自身都不在移动中 避免移动途中重复触发、避免自锁
getStructure(world, pos..., new BlockPos(x,y,z)) 解析整条框架结构,把马达自己作为排除项传入
setSpeed(1 / 16d) 速度 0.0625 格/tick → 单格位移恰好 16 tick
push() … pop() 栈式 API,见 API 接口

马达不消耗能量——红石信号是唯一输入,信号持续多久就每 tick 尝试推一次(但被 isMoving 挡住,结构动完才可能再推)。

getMoveDir = absoluteDir((rotation + 2) % 4):以 side 为基准,滚转 rotation 再加 180°。absoluteDir 委托给 CodeChickenLib 的 Rotation.rotateSide(side, absRot)(该库不在本仓库内)。

姿态同步:独立于 ForgeRelocation 的第二套通道

TileMotor 的姿态不走 ForgeRelocation 的网络层,而走 MrTJPCore InstancedBlockTile 实现的 ICustomPacketTile:

override def readDesc(in: MCDataInput) { super.readDesc(in); orientation = in.readByte() }
override def writeDesc(out: MCDataOutput) { super.writeDesc(out); out.writeByte(orientation) }

override def read(in: MCDataInput, key: Int) = key match {
  case 2 => orientation = in.readByte(); markRender()
  case _ => super.read(in, key)
}

def sendOrientUpdate() {
  if (!world.isRemote) writeStream(2).writeByte(orientation).sendToChunk()
}
  • NBT 键:tag.setByte("orient", orientation) / tag.getByte("orient")
  • 描述符:MrTJPCore 基类内容之后追加 1 字节 orientation
  • 增量包:key = 2,只带 1 字节;发送范围是 sendToChunk()(同区块内全部玩家)
  • onOrientChanged 自动触发 sendOrientUpdate(),因此每一次右键改姿态都会立刻同步

这与结构运动同步的两类包(PacketCustom(channel, 1) / 2)是完全独立的通道,见 网络协议。

渲染:TCubeMapRender + 5 张贴图

TileRenderRegistry.setRenderer(MCFramesMod.blockMotor, 0, RenderMotor)。RenderMotor extends TCubeMapRender,用 getData(w,x,y,z) 取出 (side, rotation, MultiIconTransformation) 三元组:

override def registerIcons(reg: IIconRegister) {
  bottom = reg.registerIcon("mcframes:motor/bottom")
  top    = reg.registerIcon("mcframes:motor/top")
  side   = reg.registerIcon("mcframes:motor/side")
  sidew  = reg.registerIcon("mcframes:motor/sidew")
  sidee  = reg.registerIcon("mcframes:motor/sidee")
}
资源路径 用途
assets/mcframes/textures/blocks/motor/bottom.png 底面
assets/mcframes/textures/blocks/motor/top.png 顶面
assets/mcframes/textures/blocks/motor/side.png 侧面(side == 2 与 side == 3 共用)
assets/mcframes/textures/blocks/motor/sidew.png 侧面(side == 4,西向)
assets/mcframes/textures/blocks/motor/sidee.png 侧面(side == 5,东向)

getIcon 的分派:0 → bottom、1 → top、2 → side、3 → side、4 → sidew、5 → sidee——即 6 个面里 2/3 共用一张,共 5 张贴图。getInvData 固定返回 (0, 0, ...),即物品栏里总是显示默认姿态。

语言

tile.mcframes.motor.name=Debug Frame Motor
tile.mcframes.motor|0.name=Debug Frame Motor

⚠️ 显示名是 “Debug Frame Motor”(调试用马达),两条键分别是"无 metadata 形式"与 meta = 0 形式,文本相同。原文未汉化,且 src/main/resources 下没有 zh_CN.lang。

相关条目