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)
}
⚠️ 两个要注意的点:
callback不校验(可传null)。tryStartMove存的是WeakReference(c);c为null时WeakReference(null).get返回null,后续callback match { case WeakReference(c) => ... }走case _分支,安全。- 返回值被丢弃——
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 映射或存档兼容处理 |
相关条目
- 方块搬运器注册表 ——
registerTileMover/registerPreferredMover/registerMandatoryMover的实际效果与查找优先级 - MCFrames 卡扣注册表 ——
registerFrameInteraction的落点与StickResolver实现 - 马达方块 ——
Relocator的官方参考用法 - 框架方块 ——
IFramePlacement的拦截点与raytraceFrame - 运动模型(结构与行的分解) ——
tryStartMove如何接收Relocator提交的坐标集 - ASM 核心 mod —— core mod 与 API 层共存于同一 jar 的工程学背景
- 网络协议 —— 移动状态如何同步到客户端