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),但本类已提前拦掉。双保险。

已知问题 / 风险

  1. ⚠️ LoadControllerHelper 的静态块反射 FML 私有字段 "modController" / "packageOwners"(:26-27)—— 无 try/catch、无 null 检查,字段名变化会导致 ExceptionInInitializerError 且该类永久不可用。
  2. ⚠️ packageOwners 为 null 时 getOwningMod NPE(:40)—— 无防护。
  3. getOwningMod 只取 packageOwners.get(pkgName).get(0)(:41)—— 多 mod 声明同一包时归属任意。
  4. owningModForClass 是强引用 ConcurrentHashMap(:30)—— 类与 ModContainer 永久钉住,无失效机制;NONE 也被缓存。
  5. 匿名 NONE 的覆写集合是 FML 1.7.10 的接口快照(:50-90)—— FML 加带默认实现的新方法时会静默继承默认行为。
  6. NONE 的声明(:50)晚于 getOwningMod 的使用点(:45) —— Java 允许但易误读。
  7. LoadControllerHelper 177 行中约 87 行未读(:91-177,NONE 覆写集合的剩余部分 + 其它方法)—— 本条目最大缺口。
  8. getUpsideDownName 的 EntityLiving / EntityPlayer 分支顺序依赖类层次(:22、:26)—— 若 EntityPlayer 改继承 EntityLiving,玩家分支永不执行。
  9. getUpsideDownName 第 3 段无条件返回名字(:30),可能显示实体的默认名。
  10. hasEyePass 只有 3 个原版类(:18),第三方多眼实体不覆盖;参数用 Object 而非具体类型。
  11. stripFormattingCodes 的 isEmpty() 不 trim(:35),全空白串走原样返回路径。
  12. getUpsideDownName 不返回 null 而 stripFormattingCodes 接受 null(:23/:27 vs :34)—— 两方法契约不一致。
  13. helpers/ 是功能无关的两个类的杂项包 —— 不是子系统。若将来加第三个类,包名会继续失去语义。

相关条目