反射与日志助手
基本信息
| 属性 | 值 |
|---|---|
| 包 | modtweaker2.helpers |
| 文件数 | 6 个(829 行) |
| 核心类 | ReflectionHelper(183 行)、LogHelper(190 行) |
| 反射方式 | java.lang.reflect.Field#setAccessible + Class.forName |
功能
ModTweaker 需要读写大量第三方模组的私有静态注册表(配方表、战利品表、物质表……),而这些字段在 1.7.10 上大多没有公开 API。modtweaker2.helpers 下的 6 个类承担这层「越过可见性边界」的适配工作。
| 助手类 | 行数 | 作用 |
|---|---|---|
ReflectionHelper |
183 | 通用反射工具:取字段、取构造器、实例化、改私有静态值 |
LogHelper |
190 | 日志分级 + 把对象转成可直接粘进脚本的字符串 |
InputHelper |
191 | ItemStack / IItemStack / Object[] 之间的互转 |
StackHelper |
131 | 物品配方的增删与「是否同配方」判定 |
StringHelper |
75 | 字符串工具 |
ForgeHelper |
59 | 抓取 Forge 自身的私有表并造 SeedEntry |
ReflectionHelper
6 个反射入口,全部走 setAccessible(true):
| 方法 | 取什么 |
|---|---|
getObject(Object, String...) |
实例的私有字段,按字段名列表依次试,返回第一个命中的 |
getFinalObject(Object, String...) |
同上,但先抹掉 final 修饰符再取值 |
getStaticObject(Class, String...) |
类的私有静态字段 |
getStaticObject(String, String...) |
先 Class.forName 再转调上一重载 |
getConstructor(String, Class...) / getConstructor(Class, Class...) |
指定参数类型的构造器 |
getInstance(Constructor, Object...) |
用构造器造实例 |
setPrivateValue(Class, String, int) |
改私有 static int 字段 |
「字段名列表」的设计是为了跨 MCP 映射名容错。ForgeHelper 是典型例子——同一份数据同时给出开发名与混淆名(ForgeHelper.java:34-36):
translate = getFinalObject(
getStaticObject(StatCollector.class, "localizedName", "field_74839_a"),
"languageList",
"field_74816_c");
getFinalObject 的实现(ReflectionHelper.java:105-122)先取 Field.class 自身的 modifiers 字段,抹掉目标字段的 FINAL 位后再读——这是 1.7.10 / Java 8 上写 final 静态表的常规手法(modifiersField.setInt(result, result.getModifiers() & ~Modifier.FINAL),:113)。
ForgeHelper
静态初始化块一次性抓三张表(ForgeHelper.java:29-37):
| 字段 | 来源 |
|---|---|
seeds |
ForgeHooks.seedList |
loot |
ChestGenHooks.chestInfo |
translate |
StatCollector.localizedName → languageList |
拿到 seeds 之后,getSeedEntry(ItemStack, int)(ForgeHelper.java:41-52)用 ForgeHooks$SeedEntry 这个包私有内部类的构造器造条目,失败时显式抛 NullPointerException。这是 Ex Nihilo 的种子配方能被脚本改写的基础。
LogHelper
日志分级
logError / logWarning / logInfo / print 四个出口(LogHelper.java:52-70)都统一给消息加 [ModTweaker2] 前缀。logError 有一个带 Throwable 的重载(LogHelper.java:56-58),ReflectionHelper 的每个 catch 块都调用它——异常堆栈不会被丢弃。
对象 → 脚本字面量
getStackDescription(Object)(LogHelper.java:74-114)把任意 Java 对象转成能直接粘进 Zenscript 的字符串,是所有 /minetweaker dump 指令的公共出口:
| 输入 | 输出形式 |
|---|---|
ItemStack / IIngredient |
MCItemStack.toString() |
FluidStack |
<liquid:name> 或 <liquid:name> * 数量 |
Block |
视为 meta 0 的 ItemStack |
mekanism.api.gas.GasStack |
MCGasStack.toString(),需 TweakerPlugin.isLoaded("Mekanism") |
thaumcraft 的 AspectStack / AspectList |
要素列表,需 isLoaded("Thaumcraft") |
String |
若 OreDictionary.getOres() 非空则输出 <ore:name>,否则输出带引号原文 |
List / Object[] |
递归成 [a, b, c] |
null |
字符串 null |
这四处 TweakerPlugin.isLoaded 守卫(LogHelper.java:80、:82、:84)是防止在 Mekanism / Thaumcraft 缺席时对缺失类做 instanceof 而炸掉整条 dump 的关键。
getCraftingDescription(IRecipe)(LogHelper.java:159-165)分派到 4 个重载,覆盖 ShapelessOreRecipe / ShapedOreRecipe / ShapelessRecipes / ShapedRecipes,输出成方括号列表。有序配方用 ReflectionHelper.getObject(recipe, "width"/"height") 读尺寸(LogHelper.java:177-178)——注意这里读的正是混淆名,反射工具的字段名列表容错在此起了作用。
静默失败面
6 个 static {} 初始化块用完全空的 catch 吞掉初始化异常:
| 文件 | 行 |
|---|---|
modtweaker2/helpers/ForgeHelper.java |
37 |
modtweaker2/mods/thaumcraft/ThaumcraftHelper.java |
28 |
modtweaker2/mods/mariculture/MaricultureHelper.java |
28 |
modtweaker2/mods/factorization/FactorizationHelper.java |
29 |
modtweaker2/mods/metallurgy/MetallurgyHelper.java |
25 |
modtweaker2/mods/tconstruct/TConstructHelper.java |
51 |
全部是 } catch (Exception e) {} 形式,例如 ThaumcraftHelper 抓 ThaumcraftApi 的私有 warpMap(ThaumcraftHelper.java:25-28),FactorizationHelper 连续抓两个 TileEntity* 的 recipes 静态表(FactorizationHelper.java:24-29)。
后果可从代码直接读出:这些 static 字段若初始化失败会保持 null,且不留任何日志,直到某条脚本方法在空引用上抛 NPE 才暴露。TConstructHelper 一次抓 9 张表(TConstructHelper.java:38-51),是受影响面最大的一处。此处按代码原样记录,不据此断言缺陷。