helpers 包(LoadControllerHelper / RendererLivingEntityHelper)
基本信息
| 属性 | 值 |
|---|---|
| 包 | com.gtnewhorizons.angelica.helpers |
| 本条目覆盖 | 2 个文件(目录共 2 个,即全覆盖) |
| 总行数 | 177 + 42 = 219 |
| 依赖 | FML(cpw.mods.fml.common 9 个类) |
helpers/ 是一个功能上完全无关的两个类组成的杂项包:
| # | 文件 | 行数 | 可见性 | 类别 |
|---|---|---|---|---|
| 1 | LoadControllerHelper.java |
177 | public class + @SideOnly(Side.CLIENT) |
FML 反射 |
| 2 | RendererLivingEntityHelper.java |
42 | public final |
实体渲染判定 |
⚠️ 两个类之间无任何关系(import 不相交)。helpers/ 是「不知道放哪」的归置包,不是子系统。本条目如实记录这一点,不强行赋予统一语义。
LoadControllerHelper:反射偷取 FML 的包归属表
177 行,public class,类上带 @SideOnly(Side.CLIENT)(:21)。
静态块反射取两个私有字段(:25-28)
static {
loadController = ObfuscationReflectionHelper.getPrivateValue(Loader.class, Loader.instance(), "modController"); // :26
packageOwners = ObfuscationReflectionHelper.getPrivateValue(LoadController.class, loadController, "packageOwners"); // :27
}
| 反射目标 | 私有字段 | 行 |
|---|---|---|
Loader.instance() |
"modController" |
:26 |
该 LoadController 实例 |
"packageOwners" |
:27 |
⚠️ ⚠️ 字段名 "modController" 与 "packageOwners" 是 FML 1.7.10 的私有字段名字符串,本机无 MCP 映射表可校验。若 FML 版本变化或字段改名,getPrivateValue 返回 null。
⚠️ ⚠️ 静态块无任何 null 检查(:25-28)—— packageOwners 为 null 时,getOwningMod(:40 packageOwners.containsKey(...))NPE。⚠️ 无 try/catch 包裹反射 —— getPrivateValue 在字段不存在时抛 ReflectionHelperUnableToAccessException(unchecked)→ 类初始化失败 → ExceptionInInitializerError → 该类永久不可用。
⚠️ 这是一个「用字符串反射 FML 私有实现」的强耦合。FML 1.7.10 是固定的(1.7.10 只能用那个 FML),所以实际安全;但这是明确的技术债,不是优雅设计。
ListMultimap<String, ModContainer>(:3 import Guava) —— packageOwners 是 Guava 的 multimap:一个包名可对应多个 ModContainer(理论上多个 mod 声明同一包)。
getOwningMod 的四段判定(:32-48)
public static ModContainer getOwningMod(Class<?> clz) { // :32
final ModContainer container = owningModForClass.computeIfAbsent(clz, c -> {
if (clz.getName().startsWith("net.minecraft.")) // :34
return Loader.instance().getMinecraftModContainer(); // :35
final int lastDot = clz.getName().lastIndexOf('.'); // :36
if (lastDot == -1) return NONE; // :37-38
final String pkgName = clz.getName().substring(0, lastDot); // :39
if (packageOwners.containsKey(pkgName)) return packageOwners.get(pkgName).get(0); // :40-41
else return NONE; // :42-43
});
if (container == NONE) return null; // :45-46
return container; // :47
}
| # | 情况 | 返回 | 行 |
|---|---|---|---|
| 1 | 类名以 "net.minecraft." 开头 |
Minecraft 的 ModContainer |
:34-35 |
| 2 | 类名无点(默认包) | NONE → null |
:37-38 |
| 3 | 包名在 packageOwners 里 |
get(0) —— 只取第一个 |
:40-41 |
| 4 | 包名不在表里 | NONE → null |
:42-43 |
⚠️ 第 3 段 packageOwners.get(pkgName).get(0) 只取第一个(:41)—— ListMultimap 的多个 owner 只认第一个。⚠️ 若两个 mod 声明同一包(如都写 com.example.render),归属是任意的(取决于 ListMultimap 的插入顺序)。这是真实的归属歧义。
⚠️ ⚠️ 缓存 computeIfAbsent 会缓存 NONE(:33、:30)—— 「这个类不属于任何 mod」的结果被永久缓存。⚠️ 若某个 mod 在运行期才注册包名(FML 一般不允许),首次查询得到的 null 会一直错。实际上 mod 加载早于游戏运行,所以这是安全的 —— 但缓存无失效机制。
⚠️ owningModForClass 是 ConcurrentHashMap<Class<?>, ModContainer>(:30)—— 强引用键值 —— 类与 ModContainer 永久被钉住。⚠️ 类卸载时条目不会清(与 WorkerWorldAccess 的弱引用 map、MipmapStrategies 的弱键不同)。影响:所有查过的类都常驻。实际 mod 集合有界,泄漏量有界。
⚠️ 缓存的是 NONE(:45 的 container == NONE 在 computeIfAbsent 之后) —— 即 NONE 被存进 map,而 NONE 是 private static final 的匿名实例。⚠️ NONE 的声明在 getOwningMod 之后(:50)—— Java 静态字段的初始化顺序要求「使用点在初始化点之前会看到 null」。⚠️ 但 computeIfAbsent 的 lambda 在方法被调用时才执行,此时 NONE 已初始化(类初始化已完成)。安全。
NONE:一个全返回 null 的假 ModContainer(:50-~120)
⚠️ ⚠️ NONE 是 new ModContainer() { ... } 的匿名类(:50),手写覆写了 9+ 个方法全部返回 null 或空实现:
| 方法 | 行 | 返回 |
|---|---|---|
getModId() |
:52-54 |
null |
getName() |
:56-58 |
null |
getVersion() |
:60-62 |
null |
getSource() |
:64-66 |
null |
getMetadata() |
:68-70 |
null |
bindMetadata(MetadataCollection) |
:72-74 |
空实现 |
setEnabledState(boolean) |
:76-78 |
空实现 |
getRequirements() |
:80-82 |
null |
getDependencies() |
:84-86 |
null |
getDependants() |
:88-90 |
null |
⚠️ ⚠️ 这个匿名 ModContainer 覆写集合是 FML 1.7.10 的接口快照。若 FML 给 ModContainer 加了新方法且它是 abstract 的,这里编译失败(好过静默错误)。但若新方法有默认实现,则不会编译失败而是继承默认行为 —— 不一致。
⚠️ 9 个方法里 7 个返回 null、2 个空实现 —— 调用方对 NONE 调任何 getter 都得到 null,但 getOwningMod 已在边界处(:45-46)把 NONE 转成 null,所以 NONE 永不逃出本类。这是正确的哨兵设计(区别于直接返回 null —— 缓存需要区分「没查过」与「查过且无主」)。
⚠️ 方法覆盖范围本条目只读到第 90 行(10 个方法),ModContainer 在 FML 1.7.10 中有更多方法(如 getModMetadata、getSortingOrder 等),剩余部分在 :91-177 本条目未读。⚠️ 177 行中约 87 行未读。
⚠️ ⚠️ NONE 声明为 private static final 但在 :30 的 map 与 :45 使用 —— 字段声明顺序(:50)晚于使用(:45 在方法体内)。Java 允许(方法体在类初始化后才执行),但阅读时容易误判。
RendererLivingEntityHelper:实体渲染的三个判定
42 行,public final,私有构造器(:15)。本条目完整读过。
三个静态方法
| 方法 | 行 | 语义 |
|---|---|---|
hasEyePass(Object renderer) |
:17-19 |
是否是「有独立眼睛渲染 pass」的渲染器 |
getUpsideDownName(EntityLivingBase) |
:21-31 |
「倒置渲染」时显示的名字 |
stripFormattingCodes(String) |
:33-41 |
剥离颜色格式码 |
hasEyePass:3 个类(:18)
return renderer instanceof RenderSpider || renderer instanceof RenderEnderman || renderer instanceof RenderDragon;
| 实体 | 1.7.10 渲染器 | 为什么需要独立 pass |
|---|---|---|
| 蜘蛛 | RenderSpider |
8 只眼睛,方位随实体朝向 |
| 末影人 | RenderEnderman |
眼睛纹理随状态变 |
| 末影龙 | RenderDragon |
多阶段眼部发光 |
⚠️ 参数是 Object 而非 RenderLivingEntity(:17)—— 刻意宽松,便于从 mixin 传任意对象。⚠️ 对 null 返回 false(instanceof 对 null 是 false)—— 安全。
⚠️ 只有 3 个类 —— 其它 mod 的多眼实体不在此列。⚠️ RenderSpider 的子类不匹配(instanceof 匹配子类,实际是匹配的 —— 纠正:子类会匹配)。⚠️ 准确说:子类匹配,兄弟类与代理类不匹配。
getUpsideDownName:三段判定(:21-31)
if (entity instanceof EntityLiving living) { // :22
return living.hasCustomNameTag() ? entity.getCommandSenderName() : ""; // :23
}
if (entity instanceof EntityPlayer player) { // :26
return player.getHideCape() ? "" : entity.getCommandSenderName(); // :27
}
return entity.getCommandSenderName(); // :30
| 段 | 条件 | 返回 | 行 |
|---|---|---|---|
| 1 | EntityLiving(含 EntityPlayer 的父类,但 EntityPlayer 不是 EntityLiving) |
有自定义名 → 名字;否则 "" |
:22-23 |
| 2 | EntityPlayer |
隐藏披风 → "";否则名字 |
:26-27 |
| 3 | 其它 EntityLivingBase |
无条件返回名字 | :30 |
⚠️ ⚠️ 第 1 段与第 2 段的顺序是必须的:1.7.10 的 EntityPlayer extends EntityLivingBase(不是 EntityLiving)—— ⚠️ EntityPlayerMP / EntityPlayerMP 都是 EntityLivingBase 的直接子类。⚠️ 若 EntityPlayer 改为继承 EntityLiving,第 1 段会先匹配,玩家分支永不执行。这是依赖类层次结构的隐式契约。
⚠️ 第 3 段「无条件返回名字」(:30)—— 对既非 EntityLiving 又非 EntityPlayer 的 EntityLivingBase(1.7.10 里主要是 EntityArmorStand 的渲染相关)总是显示名字,即使没有自定义名。⚠️ 会显示实体的默认名(如 “Armor Stand”)。这是否是预期行为,源码无注释。
⚠️ getCommandSenderName() 是 1.7.10 的 MCP 映射名(不是 SRG 名)—— 对应 SRG func_70023_ak / getEntityName 系。本条目用源码内可读名,未做 SRG 替换。
⚠️ 两处返回 ""(空串)而非 null(:23、:27)—— 调用方拿不到 null(好设计)。⚠️ 但 stripFormattingCodes 对 null 有处理(:34-36)—— 两处契约不一致:getUpsideDownName 不返回 null,stripFormattingCodes 接受 null。说明调用方可能来自别的来源。
stripFormattingCodes:双重早退(:33-41)
public static String stripFormattingCodes(String entityName) { // :33
if (entityName == null || entityName.isEmpty()) return ""; // :34-36
return entityName.indexOf(FORMATTING_CHAR) < 0 // :38
? entityName // :39
: EnumChatFormatting.getTextWithoutFormattingCodes(entityName); // :40
}
| 检查 | 作用 | 行 |
|---|---|---|
null || isEmpty() |
返回 "" |
:34-36 |
indexOf(FORMATTING_CHAR) < 0 |
无格式码 → 原样返回 | :38-39 |
| 否则 | EnumChatFormatting.getTextWithoutFormattingCodes |
:40 |
⚠️ indexOf 快路径是纯优化(避免 getTextWithoutFormattingCodes 分配)—— 无格式码时零分配。⚠️ FORMATTING_CHAR 从 com.gtnewhorizons.angelica.client.font.ColorCodeUtils 静态导入(:3)—— 即 1.7.10 的 §(\u00A7)。⚠️ 见 字体 条目。
⚠️ isEmpty() 是 Java 6+ 的 String 方法(String.isEmpty()),不是 StringUtils.isEmpty —— 全空白字符串 " " 不会被判空,会走到 :38-40 并原样返回。若调用方期望 trim 语义,这里不符合。
⚠️ getTextWithoutFormattingCodes 是 1.7.10 原版 API(EnumChatFormatting 的静态方法)—— 它也处理 null(返回 null),但本类已提前拦掉。双保险。
已知问题 / 风险
- ⚠️
LoadControllerHelper的静态块反射 FML 私有字段"modController"/"packageOwners"(:26-27)—— 无 try/catch、无 null 检查,字段名变化会导致ExceptionInInitializerError且该类永久不可用。 - ⚠️
packageOwners为 null 时getOwningModNPE(:40)—— 无防护。 getOwningMod只取packageOwners.get(pkgName).get(0)(:41)—— 多 mod 声明同一包时归属任意。owningModForClass是强引用ConcurrentHashMap(:30)—— 类与 ModContainer 永久钉住,无失效机制;NONE也被缓存。- 匿名
NONE的覆写集合是 FML 1.7.10 的接口快照(:50-90)—— FML 加带默认实现的新方法时会静默继承默认行为。 NONE的声明(:50)晚于getOwningMod的使用点(:45) —— Java 允许但易误读。LoadControllerHelper177 行中约 87 行未读(:91-177,NONE覆写集合的剩余部分 + 其它方法)—— 本条目最大缺口。getUpsideDownName的EntityLiving/EntityPlayer分支顺序依赖类层次(:22、:26)—— 若EntityPlayer改继承EntityLiving,玩家分支永不执行。getUpsideDownName第 3 段无条件返回名字(:30),可能显示实体的默认名。hasEyePass只有 3 个原版类(:18),第三方多眼实体不覆盖;参数用Object而非具体类型。stripFormattingCodes的isEmpty()不 trim(:35),全空白串走原样返回路径。getUpsideDownName不返回 null 而stripFormattingCodes接受 null(:23/:27vs:34)—— 两方法契约不一致。helpers/是功能无关的两个类的杂项包 —— 不是子系统。若将来加第三个类,包名会继续失去语义。
相关条目
- 字体 -
ColorCodeUtils.FORMATTING_CHAR的定义(client/font/) - Coremod 启动链 -
LoadControllerHelper的类初始化时机 - Mod ID 注册总表 -
getMinecraftModContainer的对照 - Angelica mixin 分组 -
hasEyePass/getUpsideDownName的注入点 - HUDCaching - 实体渲染的另一处辅助
- Tessellator / 线程世界访问 / 渲染队列 -
WorkerWorldAccess.modIdOf是「同类需求」的另一实现(用getSource()比对而非 FML 表)