From 66b3a647da7c88d0b51a3e50bcba65ea8ae71207 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 11:08:52 +0000 Subject: [PATCH] Ship pack.mcmeta on Forge and NeoForge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported on Forge 1.20.1: the game stops on the loading-error screen rather than starting. Forge turns every mod jar into a resource pack, and ResourcePackLoader.findPacks does this: var meta = Pack.readPackMetadata(info, supplier, version); ... if (pack == null) ModLoader.get().addWarning(new ModLoadingWarning(modinfo, ModLoadingStage.ERROR, "fml.modloading.brokenresources", file)); readPackMetadata returns null when pack.mcmeta is absent, and Forge raises that at ERROR, which is what halts loading. These jars have never carried one, so this affects every Forge and NeoForge jar the build has published, not only 1.20.1 — building a target only ever proved it compiled. pack_format comes from a table keyed by Minecraft version. The number decides a compatibility warning and nothing else, on a pack the player never sees, since mod packs are merged into the hidden "Mod resources" entry — so versions missing from the table fall back to the newest known value. Fabric needs none of this and is untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01S9bDcjZpEBbtCnt8daJigs --- forge/build.gradle | 26 ++++++++++++++++++++++++- forge/src/main/resources/pack.mcmeta | 6 ++++++ neoforge/build.gradle | 26 ++++++++++++++++++++++++- neoforge/src/main/resources/pack.mcmeta | 6 ++++++ 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 forge/src/main/resources/pack.mcmeta create mode 100644 neoforge/src/main/resources/pack.mcmeta diff --git a/forge/build.gradle b/forge/build.gradle index 69b65b9..9e80c6f 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -181,6 +181,29 @@ sourceSets.main.java { } } +// Forge turns every mod jar into a resource pack and reads pack.mcmeta out of +// it. A jar without one makes Pack.readPackMetadata return null, which Forge +// raises as ModLoadingStage.ERROR ("fml.modloading.brokenresources") and the +// game stops on the loading-error screen. The number itself only decides a +// compatibility warning on a pack the player never sees - these mod packs are +// merged into the hidden "Mod resources" entry - so an unlisted version +// falling back to the newest known value costs nothing. +def packFormats = [ + '1.14.4': 4, + '1.15' : 5, '1.15.1': 5, '1.15.2': 5, + '1.16' : 5, '1.16.1': 5, + '1.16.2': 6, '1.16.3': 6, '1.16.4': 6, '1.16.5': 6, + '1.17' : 7, '1.17.1': 7, + '1.18' : 8, '1.18.1': 8, '1.18.2': 8, + '1.19' : 9, '1.19.1': 9, '1.19.2': 9, + '1.19.3': 12, '1.19.4': 13, + '1.20' : 15, '1.20.1': 15, '1.20.2': 18, '1.20.3': 22, '1.20.4': 22, + '1.20.5': 32, '1.20.6': 32, + '1.21' : 34, '1.21.1': 34, '1.21.2': 42, '1.21.3': 42, '1.21.4': 46, + '1.21.5': 55, '1.21.6': 63, '1.21.7': 64, '1.21.8': 64, +] +def packFormat = packFormats[minecraftVersion] ?: packFormats.values().max() + processResources { def properties = [ version : version, @@ -195,9 +218,10 @@ processResources { minecraft_dependency : minecraftRange, loader_version_range : loaderRange, mixin_compat : "JAVA_${target.java}", + pack_format : packFormat, ] inputs.properties(properties) - filesMatching(['META-INF/mods.toml', '*.mixins.json']) { + filesMatching(['META-INF/mods.toml', '*.mixins.json', 'pack.mcmeta']) { expand(properties) } // Same reasoning as the source exclusion above: no bootstrap service diff --git a/forge/src/main/resources/pack.mcmeta b/forge/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..1744a58 --- /dev/null +++ b/forge/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "${mod_name}", + "pack_format": ${pack_format} + } +} diff --git a/neoforge/build.gradle b/neoforge/build.gradle index ea73128..0dd0025 100644 --- a/neoforge/build.gradle +++ b/neoforge/build.gradle @@ -120,6 +120,29 @@ dependencies { neoForge "net.neoforged:neoforge:${target.neoforge}" } +// Forge turns every mod jar into a resource pack and reads pack.mcmeta out of +// it. A jar without one makes Pack.readPackMetadata return null, which Forge +// raises as ModLoadingStage.ERROR ("fml.modloading.brokenresources") and the +// game stops on the loading-error screen. The number itself only decides a +// compatibility warning on a pack the player never sees - these mod packs are +// merged into the hidden "Mod resources" entry - so an unlisted version +// falling back to the newest known value costs nothing. +def packFormats = [ + '1.14.4': 4, + '1.15' : 5, '1.15.1': 5, '1.15.2': 5, + '1.16' : 5, '1.16.1': 5, + '1.16.2': 6, '1.16.3': 6, '1.16.4': 6, '1.16.5': 6, + '1.17' : 7, '1.17.1': 7, + '1.18' : 8, '1.18.1': 8, '1.18.2': 8, + '1.19' : 9, '1.19.1': 9, '1.19.2': 9, + '1.19.3': 12, '1.19.4': 13, + '1.20' : 15, '1.20.1': 15, '1.20.2': 18, '1.20.3': 22, '1.20.4': 22, + '1.20.5': 32, '1.20.6': 32, + '1.21' : 34, '1.21.1': 34, '1.21.2': 42, '1.21.3': 42, '1.21.4': 46, + '1.21.5': 55, '1.21.6': 63, '1.21.7': 64, '1.21.8': 64, +] +def packFormat = packFormats[minecraftVersion] ?: packFormats.values().max() + processResources { def properties = [ version : version, @@ -135,10 +158,11 @@ processResources { loader_version_range : languageRange, neoforge_dependency : neoforgeRange, mixin_compat : "JAVA_${target.java}", + pack_format : packFormat, ] inputs.properties(properties) inputs.property('modsTomlName', modsTomlName) - filesMatching(['META-INF/neoforge.mods.toml', '*.mixins.json']) { + filesMatching(['META-INF/neoforge.mods.toml', '*.mixins.json', 'pack.mcmeta']) { expand(properties) } filesMatching('META-INF/neoforge.mods.toml') { diff --git a/neoforge/src/main/resources/pack.mcmeta b/neoforge/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..1744a58 --- /dev/null +++ b/neoforge/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "${mod_name}", + "pack_format": ${pack_format} + } +}