兼容门控

TaskNH 的每个可选依赖都用 Loader.isModLoaded 或类存在性检查隔离,没有装对应模组时相关代码完全不参与类加载。integration/ 包共 4 个类。

依赖声明

TaskNHMod 的 @Mod 注解:

dependencies = "required-after:gtnhlib@[0.11.51,);required-after:modularui2@[2.3.91,);"
    + "after:navigator;after:betterquesting;after:NotEnoughItems@[2.8.150,)"
模组 门控方式 门控内容
gtnhlib required-after:gtnhlib@[0.11.51,) 硬依赖 队伍系统(TeamManager)、网络层(NetworkChannel)、ColorResource
modularui2 required-after:modularui2@[2.3.91,) 硬依赖 整个 GUI 与主题系统
navigator after:navigator + Loader.isModLoaded 地图图层,见 Navigator 地图图层
betterquesting after:betterquesting + 类存在性 任务右键菜单,见下
NotEnoughItems after:NotEnoughItems@[2.8.150,) + ModularUI.Mods.NEI.isLoaded() 槽位配方查询、多方块预览
serverutilities 无注解声明,Loader.isModLoaded 权限等级,见 权限
blockrenderer6343 无注解声明,Loader.isModLoaded 多方块预览按钮

源码注释解释了 NEI 为什么用 after: 而不是 required-after::「FML only checks the version of an after: mod when it is installed, so NEI stays optional.」

gradle.properties 里 usesMixins = false、coreModClass =(空),mcmod.info 的 dependencies 与 requiredMods 均为空数组。

BetterQuesting 集成(2 个类)

integration/BQIntegration.java + integration/BetterQuestingIntegration.java。

双重门控:

  1. Loader.isModLoaded("betterquesting")
  2. Class.forName("betterquesting.api2.client.gui.context.IQuestContextMenuEntry", false, ...) —— 即 IQuestContextMenuEntry 类存在,注释标注该 API 是「BetterQuesting 3.8.72-GTNH 新增的」

用 Class.forName 的第二个参数 false(不初始化)。BQIntegration 的类注释强调:它绝不能直接引用任何 betterquesting 类,否则没装 BQ 时这个类本身就加载不了。检查失败只打一条 info 日志(缓存结果,不重复检查)。

ClientProxy.init() 里的门控位置也有讲究,注释写明:「Checked here rather than in the integration class, so that class never loads without the mods it uses.」

功能:在 BQ 的任务右键菜单注册一项 “Add to TaskNH”,点开后把该 BQ 任务转成 TaskNH 任务:

  • 标题取 NativeProps.NAME,图标取 NativeProps.ICON(toIconStack 复制一份并把 stackSize 置 1)
  • 遍历 quest.getTasks(),把每个 TaskRetrieval 类型的子任务的 requiredItems(BigItemStack)转成清单项,标题是 "<数量>x <物品名>",并设上 trackItem / trackItemCount,如果 required.hasOreDict() 就设 trackOre——注释说「一个要求标签的任务接受该标签下任何物品,所以清单项也这样」
  • 这是 trackOre 唯一的写入来源(见 任务数据模型)
  • 完成后走 TaskNHNetwork.sendEditToServer(task, new CreateTaskPacket(task))——即按普通新建任务的路径提交,客户端聊天回一条绿色的 “TaskNH: task … created.”

NEI 集成

integration/NEIRecipeIntegration.java,@SideOnly(Side.CLIENT)。类注释:「References NEI classes directly, so callers must check isAvailable() first」——所以调用方必须自己判 ModularUI.Mods.NEI.isLoaded()。

isAvailable() 就是 ModularUI.Mods.NEI.isLoaded()。showRecipes(ItemStack) 转发到 GuiCraftingRecipe.openRecipeGui("item", stack),返回 false 表示该物品没有配方。槽位左键行为见 主界面 GUI。

多方块预览按钮

integration/MultiblockTaskIntegration.java,@SideOnly(Side.CLIENT)。这是消费其他模组的多方块,不是自身提供多方块——见分类页「本 mod 没有多方块结构」一节。

门控在 ClientProxy.init(),两个条件都要满足:

ModularUI.Mods.NEI.isLoaded() && Loader.isModLoaded("blockrenderer6343")

实现:监听 BlockRenderer6343 在 NEI 中展示多方块预览时发出的 GuiRecipeButton.UpdateRecipeButtonsEvent.Post,if (!(ref.handler instanceof MultiblockHandler)) return;,然后在预览右上角(x = Math.min(166, event.width) - BUTTON_WIDTH, y = 0)加一个 + 按钮。事件优先级是 EventPriority.LOW,注释解释:BlockRenderer6343 会在默认优先级清空这个列表,若先加就会被抹掉。

按钮 ID 从 200 起(BUTTON_ID_START + ref.recipeIndex),注释说是为了避开 NEI 自己分配的配方按钮 id。

单层保护:isSingleLayer() 反射读 GuiMultiblockHandler 的 protected 字段 layerIndex(-1 表示全部)。预览的 Layer 控件若选中某一层,部件列表就只有那一层,会做出缺件、可能丢掉控制器的任务,因此直接拒绝并提示「Set Layer to All first」。findLayerIndex() 反射失败时返回 null,此时 isSingleLayer() 返回 false——注释:「A newer BlockRenderer6343 without the field keeps the button working rather than dead.」

生成任务的规则:

  • 标题取 handler.getFullRecipeName(),图标取 parts.get(0).item(NEI 把控制方排在最前,注释说明)
  • 每个部件生成一条清单项,标题 "<数量>x <物品名>",设 trackItem 与 trackItemCount
  • BRUtil.hatchFilter.test(stack) 命中的部件被跳过——注释:「Any tier of hatch fits its slot, so tracking the one the preview shows would miss the others.」即不把任何等级的输出仓做成追踪项
  • 「超过追踪上限的部件,完整数量仍保留在标题里」(clampTrackItemCount 夹取,标题用的是未夹取的 stack.stackSize)
  • 部件列表为空则直接返回
  • 同样走 sendEditToServer + CreateTaskPacket

提示文案:tasknh.chat.multiblock_created(成功)、tasknh.chat.multiblock_single_layer(单层被拒)、tooltip 里的 tasknh.gui.multiblock.single_layer。

见 Navigator 地图图层。门控是 ClientProxy.init() 里的 Loader.isModLoaded("navigator")。

ServerUtilities 集成

见 权限。未在 @Mod 注解里声明,纯靠 Loader.isModLoaded("serverutilities") 静态常量加一个桥接类 TaskNHPermissionsSU 隔离。