API 接口

两个 mod 对外暴露 10 个 Java 接口 / 抽象类,供第三方 mod 注册自定义搬运器、注册"伪框架"、或直接发起一次方块移动。API 层全部是 Java,实现层全部是 Scala。

语言构成

本 jar 是 Scala + Java 混合工程,共 33 个源文件(20 个 .scala + 13 个 .java),注意 .java 文件也放在 src/main/scala/ 目录下(GTNH 约定构建把整个 src/main/scala 当一个源集编译)。分工是清晰的:

层 语言 文件数 内容
API 层 Java 13 全部在 mrtjp/relocation/api/(7 个)与 mrtjp/mcframes/api/(6 个),全是 interface / abstract class / 简单 POJO + package-info.java
实现层 Scala 20 全部在 mrtjp/relocation/(12)与 mrtjp/mcframes/(8),含全部 @Mod 入口、ASM 转换器、运动引擎、渲染

⚠️ 13 个 Java 文件里没有一个是 net.minecraft 的类——它们全是本 mod 自己的接口声明。这个 API 层被设计成纯 Java,是为了让其他 mod 用 soft dependency 引用时不必引入 Scala 库。

两个 @API 标记

// mrtjp/relocation/api/package-info.java
@API(owner = "ForgeRelocation", provides = "ForgeRelocation|API", apiVersion = Tags.VERSION)
package mrtjp.relocation.api;

mrtjp/relocation/api 有 @API 标记,声明 API 标识 ForgeRelocation|API。

MCFrames 的 @API 被刻意注释掉

mrtjp/mcframes/api/package-info.java 整段被注释,源码里留了一张 ASCII 图解释原因:

// You may wonder, "why is this commented out?"
// When FML sorts the modcontainers, it includes APIs. The API's dependency is always its owner but if an API is found
// embedded in a mod which isn't the owner, that mod is set as the API's dependant. In this case, the same jar contains
// both ForgeRelocation and  MCFrames, each with an API. So when FML determins which APIs are embedded where, it sees
// MCFrames|API embedded in ForgeRelocation and ForgeRelocation|API embedded in MCFrames. This results in the
// following circular load order:
//
//       MCFrames --------------> MCFrames|API <----+
//         ^   ^                        |           |
//         |   |                        |           |
//         |   +-------------------+    |           +- MrTJPCore
//         |                       |    |           |
//         |                       |    v           |
//       ForgeRelocation|API <--- ForgeRelocation <-+

根因是本 jar 里塞了两个 mod(见下),两个 API 互相"嵌入对方"造成循环依赖。源码处理方式是直接不发 @API 标记,让 FML 走普通 mod 排序。⚠️ 副作用:MCFramesAPI 不享受 API 版本隔离——它的类在 API 破坏性变更时无法与使用方解耦。

一个 jar、两个 @Mod

mcmod.info 声明两条 modList,src/ 下有两个 @Mod 注解:

项 ForgeRelocation MCFrames
@Mod 类 mrtjp.relocation.handler.RelocationMod(Scala object) mrtjp.mcframes.handler.MCFramesMod(Scala object)
modID ForgeRelocation MCFrames
modGroup(根包) mrtjp.relocation mrtjp.mcframes
useMetadata true true
modLanguage "scala" "scala"
acceptedMinecraftVersions "[1.7.10]" "[1.7.10]"
dependencies required-after:MrTJPCore required-after:MrTJPCore
guiFactory mrtjp.relocation.handler.GuiConfigFactory mrtjp.mcframes.handler.GuiConfigFactory
配置 config/ForgeRelocation.cfg config/MCFrames.cfg

⚠️ 两者 @Mod 注解里都没有 dependencies 指向对方——MCFrames 是 ForgeRelocation 的硬依赖之外的隐式依赖:MCFrames 的代码 import mrtjp.relocation.api.RelocationAPI 并在 TileMotor.update() 里调用 RelocationAPI.instance,但 FML 层面不声明。若 ForgeRelocation 缺失,MCFrames 会因 RelocationAPI.instance 恒 null 而 NPE。

⚠️ 两者都是硬依赖 MrTJPCore(required-after:MrTJPCore),且都没用 @SidedProxy:object RelocationProxy extends RelocationProxy_client / object MCFramesProxy extends MCFramesProxy_client 是硬编码继承客户端代理类,靠 @SideOnly(Side.CLIENT) 在 postinit 覆写上做剥离(服务端版 RelocationProxy_server.postinit 生效)。

两个 @Mod 都把 API 实例赋给静态字段(object 初始化时执行,早于 preInit):

object RelocationMod { RelocationAPI.instance = RelocationAPI_Impl; ... }
object MCFramesMod   { MCFramesAPI.instance = MCFramesAPI_Impl; ... }

RelocationAPI(Java 抽象类,5 个抽象方法)

public abstract class RelocationAPI {
    public static RelocationAPI instance;   // 装了就非 null;建议在 soft dependency 类里访问
    public abstract void registerTileMover(String name, String desc, ITileMover mover);
    public abstract void registerPreferredMover(String key, String value);
    public abstract void registerMandatoryMover(String key, String value);
    public abstract Relocator getRelocator();
    public abstract boolean isMoving(World world, int x, int y, int z);
}

javadoc 原文建议:“It is recommended that mods access this class within a soft dependency class.”

三个注册方法的时机约束

全部必须在 FML pre-initialization 期间调用,由 RelocationAPI_Impl 的断言强制:

object RelocationAPI_Impl extends RelocationAPI {
  var isPreInit = true
  override def registerTileMover(name: String, desc: String, handler: ITileMover) = {
    assert(isPreInit)
    MovingTileRegistry.registerTileMover(name, desc, handler)
  }
  override def registerPreferredMover(key: String, value: String) { assert(isPreInit); ... }
  override def registerMandatoryMover(key: String, value: String) { assert(isPreInit); ... }
  override def getRelocator = Relocator_Impl
  override def isMoving(world: World, x: Int, y: Int, z: Int) = MovementManager2.isMoving(world, x, y, z)
}

isPreInit 在 init 阶段被置 false:

@Mod.EventHandler def init(event: FMLInitializationEvent) {
  RelocationAPI_Impl.isPreInit = false
  RelocationConfig.loadConfig()
  RelocationProxy.init()
}

assert 是 Scala 的 Predef.assert(不是 JVM 的 -ea 断言),编译时若加 -Xdisable-assertions 才会被消除。源码未加该编译选项,因此默认会抛 AssertionError。

⚠️ 断言只在首次注册时校验,而 RelocationConfig.loadConfig()(在 init 里、即 isPreInit = false 之后)会读 preferredMovers / mandatoryMovers——在 init 之后调 registerPreferredMover 虽然会抛 AssertionError,但值已被 :+= 追加进 Seq(:+= 在抛错前就已求值?—— 实际 assert 在方法体第一行,故此时 preferredMovers 未被修改)。源码现状:init 之后注册是安全的失败。

三方法语义差异(preferredMover 软 / mandatoryMover 硬)见 方块搬运器注册表。

Relocator:栈式移动发起

public abstract class Relocator {
    public abstract void push();
    public abstract void pop();
    public abstract void setWorld(World world);
    public abstract void setDirection(int dir);     // ForgeDirection 索引 0~5
    public abstract void setSpeed(double speed);     // > 0 且 < 1,单位 米/tick
    public abstract void setCallback(IMovementCallback callback);
    public abstract void addBlock(int x, int y, int z);
    public abstract void addBlock(BlockPos bc);
    public abstract void addBlocks(Set<BlockPos> blocks);
    public abstract boolean execute();
}

javadoc 指向 [mrtjp.mcframes.TileMotor] for usage —— 官方的参考用法就是 马达方块 的 update()。

Relocator_Impl:双栈池

object Relocator_Impl extends Relocator {
  var mainStack = new MStack[RelocationRun]()
  var tempStack = new MStack[RelocationRun]()

  override def push() {
    val r = if (tempStack.isEmpty) new RelocationRun else tempStack.pop()
    mainStack.push(r)
  }
  override def pop() { assertState(); val r = mainStack.pop(); r.clear(); tempStack.push(r) }

两个栈做对象池:pop() 时把 RelocationRun 清理后压回 tempStack,下次 push() 复用而非 new。避免每次移动分配新对象。

RelocationRun 的字段:world、dir(初值 -1)、speed(初值 0.0)、callback、blocks: MSet[BlockCoord];clear() 全部复位。

逐项校验

方法 校验 异常消息
所有方法 mainStack 非空 Relocator stack is empty.
setWorld top.world == null World already set.
setDirection top.dir == -1 Direction already set.
setSpeed top.speed <= 0 Speed already set.
setCallback top.callback == null Callback already set.
execute 8 项(见下) 见下

execute() 的完整校验链:

override def execute() = {
  assertState()
  val top = mainStack.top
  if (top.world == null)   throw new IllegalStateException("World must be set before move.")
  if (top.world.isRemote)  throw new IllegalStateException("Movements cannot be executed client-side.")
  if (top.dir == -1)       throw new IllegalStateException("Direction must be set before move.")
  if (top.speed <= 0)      throw new IllegalStateException("Speed must be greater than 0.")
  if (top.speed >= 1)      throw new IllegalStateException("Speed must be less than 1.")
  if (top.blocks.isEmpty)  throw new IllegalStateException("No blocks queued for move.")
  MovementManager2.tryStartMove(top.world, top.blocks.toSet, top.dir, top.speed, top.callback)
}

⚠️ 两个要注意的点:

  1. callback 不校验(可传 null)。tryStartMove 存的是 WeakReference(c);c 为 null 时 WeakReference(null).get 返回 null,后续 callback match { case WeakReference(c) => ... } 走 case _ 分支,安全。
  2. 返回值被丢弃——Relocator_Impl.execute() 声明为无返回值 Unit(Scala),而 Java 侧 abstract boolean execute()。Scala 的 Unit 会编译成 void,与 Java 抽象方法的 boolean 不兼容。这意味着这段代码在 Scala/Java 混合编译下可能编译不过,或者 Scala 侧另有隐式转换。源码现状:javadoc 承诺 @return True if the movement was successfully started.,但 tryStartMove 的 Boolean 结果在实现里被直接丢弃,调用方拿不到"是否超过 moveLimit"的反馈。moveLimit 超限时 execute() 静默无效。

⚠️ mainStack 是 object 上的全局可变栈——不可重入、跨世界共享。若两个方块实体在同一次 tick 里各自 push()…execute()…pop(),正常配对没问题;但任何中途异常都会让 pop() 不执行,栈就此错位(TileMotor.update() 的 tryStartMove 之后没有 finally 保护 pop())。

ITileMover(Java 接口,3 个方法)

public interface ITileMover {
    boolean canMove(World w, int x, int y, int z);
    void move(World w, int x, int y, int z, int dir);
    void postMove(World w, int x, int y, int z);
}

javadoc 逐方法标注了调用侧与相位:

方法 何时被调 侧 语义
canMove 搬移前 仅服务端 决定是否可动;canMove=false → 不启动动画
move 动画结束后 客户端 + 服务端 真正搬方块与方块实体;对结构里每格顺序调用
postMove 所有格都搬完之后 客户端 + 服务端 让方块实体重新确认新位置 / 重新绑定区块

⚠️ javadoc 说 canMove 是搬移前的准入检查,但实际实现路径并不查询它——运动模型 的 tryStartMove 只调 canRunOverBlock(判目标格是否空气/软方块),MovingTileRegistry.canMove 在本 jar 内没有任何生产调用点(唯一提及处是 MCFrames 卡扣注册表 里那行被注释掉的代码)。写自定义搬运器时这是必须知道的事实。

三个内置实现(saveload / coordpush / static)的源码逐行分析见 方块搬运器注册表。

IMovementCallback 与 IMovementDescriptor

public interface IMovementCallback {
    void setDescriptor(IMovementDescriptor desc);
    void onMovementStarted();
    void onMovementFinished();
}

public interface IMovementDescriptor {
    boolean isMoving();
    double getProgress();   // 0 ~ 1
    int getSize();
}

IMovementCallback 的 javadoc 明确:这些方法只在服务端被调用,客户端收不到任何事件——因为回调是在移动执行时注册的,而移动只能服务端发起。

典型用法(javadoc 原文):“a motor may wish to know when the movement completes. In which case, the motor tile itself would implement this interface and pass itself into the Relocator.”

IMovementDescriptor 的 javadoc 提醒:所有方法的结果只在移动期间有效,其余时候未定义,用 isMoving() 先判。

MoveDesc:弱引用包装

class MoveDesc(b: WeakReference[BlockStruct]) extends IMovementDescriptor {
  def this(b: BlockStruct) = this(new WeakReference(b))
  override def isMoving   = b match { case WeakReference(b: BlockStruct) => !b.isFinished; case _ => false }
  override def getProgress = b match { case WeakReference(b: BlockStruct) => b.progress;   case _ => -1 }
  override def getSize     = b match { case WeakReference(b: BlockStruct) => b.allBlocks.size; case _ => 0 }
}

BlockStruct 是弱引用——BlockStruct 一旦被 GC 回收,getProgress 返回 -1、getSize 返回 0、isMoving 返回 false。javadoc 原文:“This descriptor is properly weak-referenced, keep or discard it at your own leisure.”

调用时机在 BlockStruct.onAdded(移动开始)与 endMove(移动结束):

def onAdded(w: World) { if (!w.isRemote) callback match {
  case WeakReference(c) => c.setDescriptor(new MoveDesc(this)); c.onMovementStarted(); case _ => } }
def endMove(w: World) { for (r <- rows) r.endMove(w); if (!w.isRemote) callback match {
  case WeakReference(c) => c.onMovementFinished(); case _ => } }

BlockPos:轻量坐标

public class BlockPos {
    public int x, y, z;
    public BlockPos(int x, int y, int z) { ... }
    public boolean equals(Object obj) { ... }
    public int hashCode() { return (this.x ^ this.z) * 31 + this.y; }
}

三个 public 可变字段(非 final)。javadoc 说明它的存在意义:“Lightweight block position object used to keep dependencies confined to the api package.” ——为了让 API 包不必暴露 CodeChickenLib 的 BlockCoord。实现侧用 BlockCoord,API 侧用 BlockPos,边界处靠 StickResolver_Impl 与 Relocator_Impl 里的 new BlockCoord(b.x, b.y, b.z) 转换。

⚠️ equals 未覆写 canEqual/类型精确检查(obj instanceof BlockPos 已足够),但字段可变却参与了 hashCode——放进 HashMap/HashSet 后改坐标会导致查找失败。StickResolver_Impl.getStructure 正是 result.map(b => new BlockPos(...)) 生成一批 BlockPos 放进 Set,但构造后不再修改。

MCFramesAPI(Java 抽象类,6 个抽象方法)

public abstract class MCFramesAPI {
    public static MCFramesAPI instance;
    public abstract void registerFramePlacement(IFramePlacement placement);
    public abstract void registerFrameInteraction(IFrameInteraction interaction);
    public abstract StickResolver getStickResolver();
    public abstract Block getFrameBlock();
    public abstract void renderFrame(double x, double y, double z, int mask);
    public abstract MovingObjectPosition raytraceFrame(double x, double y, double z, int mask, Vec3 start, Vec3 end);
}
方法 用途
registerFramePlacement 接管 框架方块 的放置行为(如并入 Multipart 结构)
registerFrameInteraction 给不实现 IFrame 的方块加上框架能力
getStickResolver 取结构解析器
getFrameBlock 取 BlockFrame 实例(供注册用途)
renderFrame 借用本 mod 的框架模型(“让你的方块看起来像框架”)
raytraceFrame 对框架模型求射线交

renderFrame / raytraceFrame 的 mask 语义:位为 1 表示不渲染该面(见 框架方块 的 FrameModelGen)。renderFrame 委托 RenderFrame.render(new Vector3(x, y, z), mask);raytraceFrame 委托 ModelRayTracer.raytraceModel(x, y, z, start, end, RenderFrame.getOrGenerateModel(mask))。

IFrame 与 IFrameInteraction

public interface IFrame {
    boolean stickOut(World w, int x, int y, int z, int side);   // 我能否在这个面抓住别人
    boolean stickIn(World w, int x, int y, int z, int side);    // 我能否在这个面被别人抓住
}
public interface IFrameInteraction extends IFrame {
    boolean canInteract(World w, int x, int y, int z);           // 我在这个位置是否生效
}

IFrame 只需在方块或方块实体上实现即可生效,javadoc 明说:“No other action besides implementation of this interface is needed for the block to function.” IFrameInteraction 是给无法修改类的方块用的替代方案。

⚠️ 硬性约定(两个方法的 javadoc 都重复写了一遍):stickOut / stickIn 必须在客户端与服务端返回相同结果。

IFramePlacement

public interface IFramePlacement {
    boolean onItemUse(ItemStack item, EntityPlayer player, World world, int x, int y, int z, int side, Vector3 hit);
}

在 框架方块 的 ItemBlockFrame.onItemUse 中被依次调用直到某个返回 true;返回 true 会阻止方块实际放置(改放一个放置音效)。

StickResolver

public abstract class StickResolver {
    public abstract Set<BlockPos> getStructure(World world, int x, int y, int z, BlockPos... exclusions);
}

javadoc 说明 exclusions 一般放"发起移动的那个马达方块"。实现见 MCFrames 卡扣注册表。

⚠️ 两处 javadoc 指向错误的 API 类

IFrameInteraction 与 IFramePlacement 的 javadoc 都写着 “This class must be registered in the RelocationAPI” / “This class can be registered through the RelocationAPI”:

/**
 * Class used instead of {@link IFrame} to add frame capabilities to a block without having the block or tile in that
 * location implement the IFrame interface. This class must be registered in the {@link RelocationAPI}.
 */
public interface IFrameInteraction extends IFrame {

这是错的——两者都应注册到 MCFramesAPI(registerFrameInteraction / registerFramePlacement)。RelocationAPI 根本没有接受它们的方法。实现侧证实了这点:

override def registerFrameInteraction(interaction: IFrameInteraction) { StickRegistry.interactionList :+= interaction }
override def registerFramePlacement(placement: IFramePlacement) { ItemBlockFrame.placements :+= placement }

源码的 javadoc 错误,未修正。

本 mod 没有的东西(grep 证据)

在 /Users/evlos/a/mirror/ForgeRelocation/ 的 src/ 全量检索:

断言 命令 结果
没有指令 grep -rn "ICommand|addChatCommand|CommandBase" src/ 0 命中
没有热键 grep -rn "KeyBinding" src/ 0 命中
没有实体注册 grep -rn "registerEntity|registerModEntity|EntityFX|spawnParticle" src/ 0 命中
没有附魔 / Buff / 群系生成 / 命令方块 / 世界生成 / 维度注册 grep -rnE "Enchantment|PotionEffect|BiomeGen|IBiomeProvider|CommandBlock|WorldGenerator|registerWorldGenerator|WorldProvider|registerDimension" src/ 2 命中,均为无关:<br>renders.scala:184-185 的 override def getBiomeGenForCoords —— MovingWorld 转发 IBlockAccess 接口方法的一行实现,不注册任何群系
没有能量 / 流体接口 grep -rnE "IEnergyHandler|IFluidHandler|FluidRegistry" src/ 0 命中
没有成就 grep -rn "Achievement" src/ 0 命中
没有矿词典 API 调用 grep -rn "OreDictionary" src/ 0 命中——唯一的矿词典用法是 ShapedOreRecipe 的 JC, "logWood" 字符串参数(见 框架方块)
没有 FML @Config / ForgeConfiguration grep -rnE "@Config|ForgeConfiguration|Configuration\.get" src/ 0 命中——配置全部走 MrTJPCore 的 ModConfig 基类(见 配置文件)
没有 addMapping / missingMapping grep -rn "addMapping|missingMapping" src/ 0 命中——无任何 ore dict 映射或存档兼容处理

相关条目