Class MinecraftPatchedProvider

java.lang.Object
dev.architectury.loom.forge.minecraft.MinecraftPatchedProvider
Direct Known Subclasses:
MinecraftLegacyPatchedProvider

public class MinecraftPatchedProvider extends Object
  • Field Details

    • project

      protected final org.gradle.api.Project project
    • logger

      protected final org.gradle.api.logging.Logger logger
    • minecraftProvider

      protected final MinecraftProvider minecraftProvider
    • type

      protected final MinecraftPatchedProvider.Type type
    • dirty

      protected boolean dirty
  • Constructor Details

  • Method Details

    • get

      public static MinecraftPatchedProvider get(org.gradle.api.Project project)
    • getGlobalCaches

      protected Path[] getGlobalCaches()
      Returns 本配置下必须可复用的共享产物列表,供 needsWork() 判定..

      列表只包含当前 jar 配置确实会生成的产物:server-only 不提供 client jar, client-extra.jar 既不生成也不进 classpath、不注册进 FORGE_EXTRA(见 providesClientJar()), 把它计入判定会让 needsWork() 恒为真、每次构建都白取一次跨进程锁。

      Returns:
      本配置下必须可复用的共享产物列表,供 needsWork() 判定
    • isReusableJar

      protected static boolean isReusableJar(Path jar)
      Returns 该共享产物是否可作为输入复用..

      口径与理由见 JarReusability.isReusable(Path):能作为 zip 打开且至少含一个条目。 实现只有那一份,其它共享缓存链(mapped minecraft jar、mcp.zip、SrgProvider 产物等)用的是同一判定; 本方法保留,是因为 legacy 子类仍以 isReusableJar 的名字调用它。

      Returns:
      该共享产物是否可作为输入复用
    • checkCache

      protected boolean checkCache() throws IOException
      锁内二次确认:判定现有产物能否复用,并返回本次是否需要整链重建.

      本方法刻意不再删除共享产物。这些 jar 位于跨 daemon 共享的 forge 缓存目录: 删除会让锁外只做存在性判定的读方拿到缺失文件,也会让其它工作树误判「需要重建」而互相触发重建。 取而代之的策略是:

      • 可复用的产物原样保留,缺失或损坏的产物由后续步骤按件补齐(needsWork() 同样基于可复用性判定);
      • 需要整链重建时由返回值表达,各步骤以「临时文件 + 原子落位」重新生成, 读方见到的要么是旧文件、要么是完整的新文件。
      Returns:
      是否需要整链重建
      Throws:
      IOException
    • provide

      public void provide() throws Exception
      Throws:
      Exception
    • provideProjectionBlocker

      protected @Nullable String provideProjectionBlocker()
      Returns 不能把三级 patched 产物的生产投影成执行期任务的原因;可以投影时为 null..

      与 MinecraftProvider.projectionBlocker() 同一风格:每一条都是一个「本形态的生产链不在本阶段 定义之内」或「配置期有确定的读者会按路径读这些 jar」的形态,命中即整段退回配置期路径 (那条路径与改造前逐字一致),并留下 lifecycle 留痕。

      • legacy Forge(1.8-1.16):MinecraftLegacyPatchedProvider.provide() 覆写了本方法, 走的是 FG2 工作目录下的一组产物,形态与 modern 不同。
      • disableObfuscation / unobfuscated Forge:pre-patch jar 走 createUnobfuscatedPrePatchJar(userdev 合并分支),且 remapJar 也在配置期生产 最终 jar 并读 at-patched jar——两处都不在本次迁移的形态之内。
      • mappings 迁移留在配置期:ForgeMigratedMappingConfiguration.manipulateMappings 在配置期生产迁移产物时,要按路径读 intermediate jar (见 getOrProduceMinecraftPatchedIntermediateJar())。投影后那件产物只在执行期落位, 配置期读者会拿到不存在的文件——因此该情形整批回退。迁移本身同样任务化之后,剩下的阻碍 只有「配置期有别的读者要真读迁移产物」,判据统一放在 ForgeMigratedMappingConfiguration.mappingsMigrationProjectionBlocker(net.fabricmc.loom.LoomGradleExtension)。
      Returns:
      不能把三级 patched 产物的生产投影成执行期任务的原因;可以投影时为 null
    • getPrePatchJarProducerTaskPath

      public @Nullable String getPrePatchJarProducerTaskPath()
      Returns pre-patch jar 的产出任务路径;未投影(配置期生产)时为 null..

      供「按裸路径声明该产物为输入」的消费方(genForgePatchedSources)建任务依赖: 它注册得比 provide() 更早,故只能把这次查询做成惰性 provider。

      Returns:
      pre-patch jar 的产出任务路径;未投影(配置期生产)时为 null
    • getOrProduceMinecraftPatchedIntermediateJar

      public Path getOrProduceMinecraftPatchedIntermediateJar()
      Returns 打补丁后的中间 Minecraft jar;产物不可复用时先按件补齐..

      该 jar 位于跨 daemon 共享的 forge 缓存目录:其它工作树的构建(尤其是仍在删除整组产物的 旧版本 loom)可能让它在被读取前一刻消失,直接读取会以 NoSuchFileException 打断整个构建。

      判定与补齐都基于「可复用」(存在、能作为 zip/jar 打开且至少含一个条目,见 isReusableJar(Path))而非「存在」:共享缓存里可能出现 22 字节空 zip 或截断文件, 只判存在性会把这类半成品原样交给调用方——它能以只读 jar 文件系统打开但读不出任何 class, 下游(FieldMappingsMigrator / MethodInheritanceMappingsMigrator)会把「读不出内容」当成 「没有内容」,从而把空结果写进映射缓存,形成静默错误映射。

      补齐走 produceIntermediateJarIfMissing():产物齐备时它是无锁快路径,不可复用时在 forge-patched 跨进程锁内只重建缺失件,并以原子方式落位。

      Returns:
      打补丁后的中间 Minecraft jar;产物不可复用时先按件补齐
    • produceIntermediateJarIfMissing

      protected void produceIntermediateJarIfMissing() throws Exception
      确保 getMinecraftPatchedIntermediateJar() 指向的产物已存在.

      默认实现调用 provide();legacy 链的产物在 remapJar(ServiceFactory) 阶段生成(provide() 只做判定),由子类覆盖补齐。

      任务路径下不能走 provide():那条路径只做接线、不写产物。本方法只被**配置期读者**调用 (getOrProduceMinecraftPatchedIntermediateJar 的调用方:mappings 迁移的配置期回退路径), 而那个读者与 provideProjectionBlocker() 用的是同一个判据——走到这里时任务路径本不该生效。 真走到这里(产物在判定之后被其它工作树删掉或写坏)时,provide() 只做接线, 随后 getOrProduceMinecraftPatchedIntermediateJar 的可复用性复检会以明确异常报出, 而不是把不存在的路径交给迁移器去读。

      Throws:
      Exception
    • remapJarProjectionBlocker

      protected @Nullable String remapJarProjectionBlocker()
      Returns 不能把最终 patched jar 的生产投影成执行期任务的原因;可以投影时为 null..

      与 MinecraftProvider.projectionBlocker() 同一风格:每一条都是一个「本形态的生产链不在本阶段 定义之内」的形态,命中即整段退回配置期路径(那条路径与改造前逐字一致),并留下 lifecycle 留痕。

      • legacy Forge(1.8-1.16):它的产物是一组 FG2 工作目录下的 jar (client/server/merged-patched.jar、forge.jar),且 remapJar 是与父类 不同的整链实现(见 MinecraftLegacyPatchedProvider.remapJar),形态与 modern 不同。
      • disableObfuscation / unobfuscated Forge:最终 jar 走 mergeUnobfuscatedPatchedJar(把 userdev jar 合进 AT 后的 jar)且映射树为空, 同样不在本次迁移的形态之内。
      Returns:
      不能把最终 patched jar 的生产投影成执行期任务的原因;可以投影时为 null
    • remapJar

      public void remapJar(ServiceFactory serviceFactory) throws Exception
      Throws:
      Exception
    • productionOptions

      public MinecraftPatchedProvider.ProductionOptions productionOptions(ServiceFactory serviceFactory)
      Returns 本 provider 当前状态对应的生产输入与平台开关..

      任务接线侧同样用它:任务声明的每一个输入都取自这里,因此两侧的输入逐项同源。 唯一例外是映射树——任务在执行期从它自己的 @Nested 映射配置取(见 GenerateForgePatchedJarTask), 与这里的 getMappingTree(ServiceFactory) 用的是同一个映射选项。

      Returns:
      本 provider 当前状态对应的生产输入与平台开关
    • runProduction

      public static void runProduction(MinecraftPatchedProvider.ProductionOptions options, @Nullable Path patchedJarOutput, @Nullable Path clientExtraOutput, Consumer<String> lifecycle) throws Exception
      把最终 patched jar(与 client-extra)生产到给定路径.

      配置期路径(remapJar(ServiceFactory))与执行期任务 (GenerateForgePatchedJarTask)共用这一处实现,因此两条路径的产物逐字节一致—— 同一次重映射、同一批输入,不存在「两套实现各自近似」的分叉。

      本方法不做任何「是否需要生产」的判定,也不落位:判定由调用方负责(配置期看 dirty, 任务侧看 needsFinalJarWork),落位一律由 AtomicFiles 的原子发布完成。

      静态:任务对象要能被配置缓存序列化,因此生产不能经由 provider 实例(见 MinecraftPatchedProvider.ProductionOptions)。

      Parameters:
      options - 生产输入与平台开关
      patchedJarOutput - 最终 patched jar 的落位路径;为 null 表示本次不产出它
      clientExtraOutput - client-extra 的落位路径;为 null 表示本次不产出它
      lifecycle - lifecycle 日志出口(配置期是项目 logger,执行期是任务 logger)
      Throws:
      Exception
    • produceFinalJarsInto

      public static void produceFinalJarsInto(MinecraftPatchedProvider.ProductionOptions options, Path patchedJarOutput, @Nullable Path clientExtraOutput, Consumer<String> lifecycle) throws Exception
      把最终 patched jar(与 client-extra)生产到**指定路径**,不触碰共享缓存路径、不取跨进程锁.

      供电线验证使用:它让「配置期实现」可以被独立调用一次作为对照,对照产物落在独立文件上, 因此被测产物的路径上只有被测任务一个写入者——不会出现「配置期先写一份、任务再覆盖」这类别名, 否则「两者一致」可以由「读到了同一份文件」伪造出来。

      Parameters:
      options - 生产输入(通常取自 productionOptions(ServiceFactory))
      patchedJarOutput - 最终 patched jar 的落位路径
      clientExtraOutput - client-extra 的落位路径;为 null 表示不产出它
      lifecycle - lifecycle 日志出口
      Throws:
      Exception
    • produceFinalJarsForTask

      public static void produceFinalJarsForTask(MinecraftPatchedProvider.ProductionOptions options, Path patchedJarOutput, @Nullable Path clientExtraOutput, Path lockRoot, String lockKey, boolean refreshDeps, Consumer<String> lifecycle) throws Exception
      执行期任务的生产入口:按需把最终 patched jar 与 client-extra 原子落位到任务声明的输出路径.

      「是否需要生产」的判据与配置期路径同源(见 needsFinalJarWork), 齐备时不写任何文件:于是任务路径与配置期路径在同一份共享缓存上「写了哪些文件」完全一致。 执行时机与跨进程并发保护分别由任务图与这里取的文件锁承担。

      Parameters:
      options - 生产输入与平台开关(任务从自己声明的输入构造)
      patchedJarOutput - 最终 patched jar 的落位路径(与配置期同一条路径)
      clientExtraOutput - client-extra 的落位路径;本配置不产出它时为 null
      lockRoot - 跨进程锁所在目录(与配置期 withPatchedLock 同一个目录)
      lockKey - 跨进程锁的 key(与配置期同一个 key)
      refreshDeps - 是否显式要求刷新(对应 --refresh-dependencies)
      lifecycle - lifecycle 日志出口
      Throws:
      Exception
    • withPatchedLock

      protected void withPatchedLock(Callable<Void> action)
      在跨进程锁保护下执行 patched jar 生产.

      产物位于跨 daemon 共享的 forge 缓存目录(不按项目隔离),且 client-extra.jar 在 各 Type 实例间共享同一路径;同一 MC+Forge 版本的多个并发构建会互相读写/删除同一批文件, 必须整段生产串行化。锁内判定均为幂等二次确认,等锁期间其它进程完成生产后直接复用。

    • withLock

      protected static void withLock(Path lockRoot, String lockKey, Callable<Void> action)
      在跨进程锁保护下执行 patched jar 生产.

      产物位于跨 daemon 共享的 forge 缓存目录(不按项目隔离),且 client-extra.jar 在 各 Type 实例间共享同一路径;同一 MC+Forge 版本的多个并发构建会互相读写/删除同一批文件, 必须整段生产串行化。锁内判定均为幂等二次确认,等锁期间其它进程完成生产后直接复用。

      静态:执行期任务同样要取这把锁,而任务不持有 provider。

    • publishAtomically

      public static void publishAtomically(Path target, MinecraftPatchedProvider.ThrowingProducer producer) throws IOException
      原子发布:在「临时文件 + 原子落位」保护下执行可能抛出任意受检异常的生产动作.

      补丁步骤会调用外部工具(binpatcher / installer tools 等),其受检异常不限于 IOException, 无法直接作为 AtomicFiles.IOConsumer;这里统一折叠为 UncheckedIOException 后向上抛, 以保证共享缓存里永远不会出现半截产物。

      Parameters:
      target - 最终落位路径(位于跨 daemon 共享的缓存目录)
      producer - 内容生产者,接收本次独占的临时文件路径
      Throws:
      IOException
    • createPrePatchJarMcpOptions

      public org.gradle.api.provider.Provider<McpExecutor.Options> createPrePatchJarMcpOptions(TempFiles tempFiles) throws IOException
      Returns 「跑 rename 步」的 MCP 执行器声明(配置期只接线、不执行)..

      与改造前那段内联代码的唯一区别是「不再顺带执行」:McpExecutorBuilder.build() 返回的是惰性 provider,步进逻辑选项、工具 jar 的坐标解析、以及 ConstantLogic 里 downloadClient 的 输出路径读取都推迟到它被取值时。因此本方法既能在配置期路径里调用,也能把返回值挂成任务的 @Nested 输入,由任务在执行期发起同一次调用。

      tempFiles 必须活到那次调用结束:MCP 执行链的步进缓存落在它管理的临时目录里。

      Returns:
      「跑 rename 步」的 MCP 执行器声明(配置期只接线、不执行)
      Throws:
      IOException
    • createPrePatchJarMcpOptions

      public org.gradle.api.provider.Provider<McpExecutor.Options> createPrePatchJarMcpOptions(Path mcpCache) throws IOException
      Returns 「跑 rename 步」的 MCP 执行器声明,工作目录由调用方给出..

      与 createPrePatchJarMcpOptions(TempFiles) 等价,区别只在于工作目录的来源:配置期路径用 TempFiles(本次调用结束即删),执行期任务用它自己的目录(见 GenerateForgePrePatchJarTask)。 两条路径的步进集合、初始配置与工具声明都取自同一处,因此不会分叉。

      Parameters:
      mcpCache - MCP 执行链的工作目录(由调用方负责创建与清理)
      Returns:
      「跑 rename 步」的 MCP 执行器声明,工作目录由调用方给出
      Throws:
      IOException
    • runMcpExecutor

      public static void runMcpExecutor(org.gradle.api.provider.Provider<McpExecutor.Options> options, Path output, ServiceFactory serviceFactory) throws IOException
      执行期求值:只认选项、本次落位路径与服务工厂,不读项目模型.

      把 pre-patch jar 的生产搬进任务之后,执行期这一侧就是本方法;它与配置期路径 共用 McpExecutor.execute() 这一处实现,因此两条路径的 MCP 步进集合、命令行与产物不会分叉。

      Parameters:
      options - 配置期接好的 MCP 执行器选项
      output - MCP 最后一步产物的落位路径(调用方负责原子落位)
      Throws:
      IOException
    • createNeoForgeInstallerTools

      public ForgeExternalToolService.Options createNeoForgeInstallerTools()
      Returns NeoForge installer-tools 的声明式选项(工具 classpath、主类与参数模板)..

      参数模板里的取值分两类:

      • 配置期就确定的纯值——--task、输入 jar 与 --neoform-data——就地定死在模板里, 于是这条链搬进任务后不需要在执行期回读项目模型;
      • 只有调用时才知道的路径——{mappings}(本次下载的 client 映射)与 {output}(本次调用的目标 jar)——留作占位符。

      参数顺序与改造前那串 settings.args(...) 逐条一致。

      与 createBinpatcherTool() 同理,供任务侧把同一条调用搬进执行期:把返回值挂成任务的 @Nested 输入即可。

      Returns:
      NeoForge installer-tools 的声明式选项(工具 classpath、主类与参数模板)
    • getMappingTree

      protected net.fabricmc.mappingio.tree.MemoryMappingTree getMappingTree(ServiceFactory serviceFactory)
      Returns 本工程的映射树..

      对子类开放:legacy Forge(1.8-1.16)的 forge.jar 修补需要按 srg→named 重映射, 与这里产出 minecraftPatchedAtJar 用的是同一棵映射树。

      Returns:
      本工程的映射树
    • getNamedForgeJar

      public @Nullable Path getNamedForgeJar()
      Returns 已重映射到 named 的 Forge jar. 仅 legacy Forge 具备该产物.

      legacy 的 Forge universal jar 按 srg 名发布,patchForge 会把它重映射成 named, 落位在 Forge 缓存工作目录的 forge.jar。该产物是 ForgeMemberNameFixer 建立「srg 名 → named 名」映射的 对照来源之一;现代 Forge 的 universal jar 本身就在 named 空间,没有这份产物。

      这里按路径定位而非依赖实例类型:MergedForgeMinecraftProvider.getPatchedProvider() 在部分时序下返回的是本基类而非 MinecraftLegacyPatchedProvider(实测 1.12.2 即如此), 若只靠子类覆写会拿到 null。产物不存在时返回 null,调用方据此跳过。

      Returns:
      已重映射到 named 的 Forge jar
    • isPatchedJarUpToDate

      protected boolean isPatchedJarUpToDate(Path jar) throws IOException
      Throws:
      IOException
    • accessTransformForge

      protected void accessTransformForge() throws IOException
      Throws:
      IOException
    • accessTransform

      protected void accessTransform(Path input, Path target) throws IOException
      对给定 jar 执行 Forge 的 access transform.

      现代 Forge 走 accessTransformForge()(中间产物路径);legacy Forge 的补丁 流程在 FG2 自己的工作目录下产出 jar(client/server/merged-patched.jar),必须显式指定 输入输出,否则会写错产物,令后续的 walkFileSystems / applyLoomPatchVersion 拿到陈旧文件。

      Throws:
      IOException
    • createAccessTransformerOptions

      public org.gradle.api.provider.Provider<AccessTransformerService.Options> createAccessTransformerOptions(TempFiles tempFiles)
      Returns AT 工具的声明式选项(工具 classpath、主类与待应用的 AT 文件)..

      配置期只接线、不求值:AccessTransformerService.createOptionsForLoaderAts(org.gradle.api.Project, dev.architectury.loom.util.TempFiles) 返回的是惰性 provider——依赖解析(DependencyDownloader 只建 detached configuration)与「把 AT 文件从 userdev jar 里抽出来」都推迟到 runAccessTransformer(org.gradle.api.provider.Provider<dev.architectury.loom.accesstransformer.AccessTransformerService.Options>, java.nio.file.Path, java.nio.file.Path, net.fabricmc.loom.util.service.ServiceFactory) 真正取值的那一刻。因此本方法既能在 配置期路径里调用,也能把返回值挂成任务的 @Nested 输入,由任务在执行期发起同一次调用。

      tempFiles 必须活到那次调用结束:AT 文件抽取到它管理的临时目录里。

      Returns:
      AT 工具的声明式选项(工具 classpath、主类与待应用的 AT 文件)
    • runAccessTransformer

      public static void runAccessTransformer(org.gradle.api.provider.Provider<AccessTransformerService.Options> options, Path input, Path target, ServiceFactory serviceFactory) throws IOException
      执行期求值:只认选项、本次调用的输入输出与服务工厂,不读项目模型.

      与 createAccessTransformerOptions(dev.architectury.loom.util.TempFiles) 配对,让配置期路径与「把 provide() 链搬进任务」 之后的路径共用同一份实现:AT 的实际参数仍只由 AccessTransformerService.execute(java.nio.file.Path, java.nio.file.Path) 构造一处, 因此两条路径跑出来的命令行不会分叉。

      Parameters:
      options - 配置期接好的 AT 选项
      input - 待执行 AT 的 jar
      target - AT 结果落位路径(本方法负责原子落位:先写私有副本,再 move 到 target)
      Throws:
      IOException
    • producePatchedIntermediateForTask

      public static void producePatchedIntermediateForTask(Path prePatchJar, Path output, Path patches, ForgeExternalToolService.Options binpatcherTool, boolean fixParameterAnnotations, Consumer<String> lifecycle, Consumer<String> info) throws Exception
      打补丁后的中间产物的生产实现(配置期路径与执行期任务共用).

      顺序与改造前逐字一致:binpatcher 打补丁 → 补出 pre-patch jar 里存在而补丁结果里缺失的类 → 删除 vignette 参数名 → (非 official 的 Forge 系)修补参数注解。

      静态:任务对象要能被配置缓存序列化,因此生产不能经由 provider 实例(见 MinecraftPatchedProvider.ProductionOptions)。

      output 是调用方给的落位路径(配置期与任务路径都是「本次独占的临时文件」), 本方法不做原子落位——落位由调用方负责。

      Parameters:
      prePatchJar - pre-patch jar,即 binpatcher 的 {clean} 与补出缺失类的来源
      output - 本次落位路径
      patches - 补丁 jar,即 binpatcher 的 {patch}
      binpatcherTool - binpatcher 的声明式选项
      fixParameterAnnotations - 是否修补参数注解
      lifecycle - lifecycle 日志出口(配置期是项目 logger,执行期是任务 logger)
      info - info 日志出口(同上)
      Throws:
      Exception
    • patchJars

      protected void patchJars(Path clean, Path output, Path patches) throws Exception
      对给定 jar 执行 Forge 的 binary patch.

      工具的 classpath、主类与参数模板都由 ForgeExternalToolService.Options 声明: classpath 是惰性的(配置期只接线,取值推迟到真正执行时),发起调用也只依赖 ServiceFactory。 因此本方法既能在配置期路径里调用,也能在把它连同调用点搬进任务之后于执行期调用—— 那条路径上不再有任何需要 Project 的同步求值。

      Throws:
      Exception
    • createBinpatcherTool

      public ForgeExternalToolService.Options createBinpatcherTool()
      Returns binpatcher 工具的声明式选项..

      配置期调用:只做接线。依赖坐标与参数模板都是从 userdev 配置读来的纯值,工具 classpath 是一个 惰性的 FileCollection(DependencyDownloader 只建 detached configuration, 不解析),主类由工具 jar 清单派生也是惰性的。因此这里既不解析依赖、也不发起进程。

      任务侧要用同一份声明时,把返回值挂成任务的 @Nested 输入即可。

      Returns:
      binpatcher 工具的声明式选项
    • runBinpatcher

      public static void runBinpatcher(ForgeExternalToolService.Options tool, Path clean, Path output, Path patches, ServiceFactory serviceFactory)
      执行 binpatcher:把参数模板里的 {clean}/{output}/{patch} 按本次调用的 路径展开后交给工具服务.

      静态:任务侧(执行期)不持有 provider,也要能用同一份实现发起这次调用,故只认选项与服务工厂。

      Parameters:
      tool - binpatcher 的声明式选项(见 createBinpatcherTool())
      clean - 待打补丁的 Minecraft jar
      output - 本次调用的目标 jar
      patches - 补丁 jar
      serviceFactory - 服务工厂
    • copyMissingClasses

      protected void copyMissingClasses(Path source, Path target) throws IOException
      Throws:
      IOException
    • createMcpExecutor

      public McpExecutorBuilder createMcpExecutor(Path cache)
    • getMinecraftIntermediateJar

      public Path getMinecraftIntermediateJar()
    • getMinecraftPatchedIntermediateJar

      public Path getMinecraftPatchedIntermediateJar()
    • getMinecraftPatchedIntermediateAtJar

      public Path getMinecraftPatchedIntermediateAtJar()
    • getMinecraftPatchedJar

      public Path getMinecraftPatchedJar()
    • isDirty

      public boolean isDirty()
      Checks whether the provider's state is dirty (regenerating jars).