Coremod 与 Mixin(Coremod / Mixin)

基本信息

属性 值
coreModClass core.CraftingTweaksCore
Coremod 全限定名 com.gtnewhorizons.craftingtweaks.core.CraftingTweaksCore
usesMixins true
usesMixinDebug true
mixinsPackage mixins.early
separateMixinSourceSet 空(Mixin 与主代码同在 src/main)
mixinPlugin 空(未使用自定义 IMixinConfigPlugin)
accessTransformersFile 空
forceEnableMixins false
containsMixinsAndOrCoreModOnly false
Mixin 数量 3 个(全在 mixins/early)

⚠️ 本 mod 没有 Access Transformer

gradle.properties 中 accessTransformersFile =(空值), CraftingTweaksCore.getAccessTransformerClass() 明确 return null, 且 src/main/resources/META-INF/ 目录根本不存在 (ls src/main/resources/META-INF → No such file or directory)。

权限提升完全靠 Mixin @Accessor / @Invoker 实现, Coremod 的 ASM transformer 列表也是 getASMTransformerClass() → null, 即没有任何字节码改写,唯一的 coremod 职责是「提前加载 Mixin」。

Coremod:CraftingTweaksCore

项 值
包路径 com.gtnewhorizons.craftingtweaks.core.CraftingTweaksCore
插件名 @IFMLLoadingPlugin.Name("CraftingTweaks-Core")
MC 版本 @IFMLLoadingPlugin.MCVersion("1.7.10")
实现接口 IFMLLoadingPlugin、IEarlyMixinLoader(gtnhmixins)

IFMLLoadingPlugin 各方法返回值:

方法 返回
getMixinConfig() "mixins.craftingtweaks.early.json"
getASMTransformerClass() null
getModContainerClass() null
getSetupClass() null
getAccessTransformerClass() null
injectData(Map<String, Object>) 空实现(no-op)

getMixins(Set<String> loadedCoreMods) 的返回值按侧分叉:

侧 返回的 Mixin 列表
客户端 minecraft.GuiScreenAccessor、minecraft.GuiContainerAccessor、minecraft.MixinGuiScreen_CraftingTweaks
服务端 Collections.emptyList()

即三个 Mixin 全部只在客户端应用,服务端完全不加载。

Mixin 清单

两个 Mixin 配置 JSON,package 字段只在 early 那个里声明:

配置文件 package refmap target minVersion
mixins.craftingtweaks.early.json com.gtnewhorizons.craftingtweaks.mixins.early mixins.craftingtweaks.refmap.json @env(DEFAULT) 0.8.5-GTNH
mixins.craftingtweaks.json 无 同上 @env(DEFAULT) 0.8.5-GTNH

mixins.craftingtweaks.json 不含 mixins / client / server 任何条目, 即空配置;它由 mixinsPackage 自动生成的注解处理器产出,实际生效的只有 early 配置(coremod 主动指定)。

两者 compatibilityLevel 均为 JAVA_8。

目标类 1:GuiScreenAccessor

@Mixin(GuiScreen.class)
public interface GuiScreenAccessor
成员 注解 目标 本 mod 内调用点
List<GuiButton> getButtonList() @Accessor 字段 buttonList(隐式推导) 0 处(无调用方)
void drawHoveringText(List<String> textLines, int x, int y) @Invoker(value = "func_146283_a") SRG func_146283_a ClientProxy.onDrawScreen 1 处

drawHoveringText 的 SRG 名在源码中以字符串形式显式给出(func_146283_a), 这是唯一一处直接书写 SRG 名的地方。

目标类 2:GuiContainerAccessor

@Mixin(GuiContainer.class)
public interface GuiContainerAccessor extends GuiScreenAccessor
成员 注解 目标字段/方法 本 mod 内调用点数
int getXSize() @Accessor xSize(隐式) 2(Gany’s、TFC 的 initGui)
int getYSize() @Accessor ySize(隐式) 0
int getGuiLeft() @Accessor guiLeft(隐式) 4(Gany’s/TFC 各 1,GuiTweakButton 构造 + drawButton)
int getGuiTop() @Accessor guiTop(隐式) 4(同上)
void setIgnoreMouseUp(boolean ignoreMouseUp) @Accessor(value = "field_146995_H") SRG field_146995_H 0
boolean getDragSplitting() @Accessor(value = "field_147007_t") SRG field_147007_t 0
void setDragSplitting(boolean dragSplitting) @Accessor(value = "field_147007_t") SRG field_147007_t 0
int getDragSplittingButton() @Accessor(value = "field_146988_G") SRG field_146988_G 16(压缩路径每次拖拽都读)
Slot callGetSlotAtPosition(int x, int y) @Invoker getSlotAtPosition(隐式) 2(onGuiClick + onDrawScreen)
void callHandleMouseClick(Slot slot, int slotId, int mouseButton, int clickType) @Invoker handleMouseClick(隐式) 0

⚠️ 三个 SRG 字段名(field_146995_H、field_147007_t、field_146988_G) 在本 mod 源码中只出现在 Accessor 声明处,没有任何调用方。 压缩流程用的是 field_146988_G(getDragSplittingButton)这一条 —— 它被 ClientProvider.compress 的拖拽分配调用了 16 次(ClientProvider.java 第 385/387/389/397/399/401/403/405/407/418/422/426/428/430/432/434 行; 第 457 行是私有辅助方法 getDragSplittingButton(int, int) 的定义, 不计入调用点); 另外两个字段的 getter/setter 是预留但未使用的声明。

同理 getYSize() 与 callHandleMouseClick(...) 也是零调用方的声明。

⚠️ GuiContainerAccessor 继承 GuiScreenAccessor, 所以 drawHoveringText 这条 @Invoker 在 GuiContainer 上也可用 —— ClientProxy.onDrawScreen 正是靠 event.gui instanceof GuiScreenAccessor 的 instanceof 判定(而非 GuiContainerAccessor)来决定能否画 tooltip。

目标类 3:MixinGuiScreen_CraftingTweaks

@Mixin(GuiScreen.class)
public class MixinGuiScreen_CraftingTweaks {
    @Inject(
        method = "handleMouseInput",
        at = @At(value = "INVOKE_ASSIGN",
                 target = "Lorg/lwjgl/input/Mouse;getEventButton()I",
                 remap = false),
        cancellable = true)
    private void craftingtweaks$handleMouseInput(CallbackInfo ci,
        @Local(ordinal = 0) int mouseX,
        @Local(ordinal = 1) int mouseY,
        @Local(ordinal = 2) int btn) { ... }
}
项 值
目标方法 GuiScreen.handleMouseInput(隐式 SRG 名,未写字符串)
注入点 @At(value = "INVOKE_ASSIGN", target = "Lorg/lwjgl/input/Mouse;getEventButton()I", remap = false)
remap = false 因为目标是 LWJGL 的 Mouse,不是 Minecraft 方法
捕获的局部变量 通过 MixinExtras @Local(ordinal 0/1/2 = mouseX / mouseY / btn)
回调体 if (CraftingTweaks.onGuiClick(mouseX, mouseY, btn)) ci.cancel();

这是唯一会取消原版行为的注入:当 onGuiClick 返回 true 时, handleMouseInput 被中断,从而阻断后续的鼠标点击分发。

注入点选在 INVOKE_ASSIGN(把 Mouse.getEventButton() 结果赋给局部变量处) 而非 HEAD,目的是确保三个 @Local 能按 ordinal 正确取到值。

@Local 来自 com.llamalad7.mixinextras.sugar.Local, 说明构建期依赖 MixinExtras。

GuiClickEvent 链路

CraftingTweaks.onGuiClick(mouseX, mouseY, btn):

  1. 仅当 Mouse.getEventButtonState() 为真(鼠标按键刚按下)才继续
  2. 组装 GuiClickEvent(currentScreen, mouseX, mouseY, button)
  3. MinecraftForge.EVENT_BUS.post(event)
  4. 返回 event.isCanceled()

GuiClickEvent 是 @Cancelable 的 cpw.mods.fml.common.eventhandler.Event, 三个字段全 public final:gui(GuiScreen)、mouseX、mouseY、button。

消费方只有 ClientProxy.onGuiClick(@SubscribeEvent(priority = EventPriority.LOWEST)), 用于 W 键正向点击投料,见 合成网格操作。

NEI 热键屏蔽

NEIHotkeyCheck implements HotkeyCheck:

public boolean allowHotkeys() {
    return LayoutManager.getInputFocused() == null;
}

通过 FMLPostInitializationEvent.buildSoftDependProxy("NotEnoughItems", "...") 软依赖加载 —— NEI 未安装时 hotkeyCheck == null,判定短路放行。 加载的类直接 import codechicken.nei.LayoutManager,故 NEI 是软依赖而非硬依赖。

ClientProxy.onClientTick 开头:

if ((hotkeyCheck != null && !hotkeyCheck.allowHotkeys())) return;

NEI 物品搜索框聚焦时,全部 6 个热键一并失效。

相关条目