From 06fd4ae80d080cdb2b9227d086b9e159097afa8a Mon Sep 17 00:00:00 2001 From: qawow <3441561646@qq.com> Date: Tue, 11 Aug 2026 01:06:06 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20Minecraft=2026?= =?UTF-8?q?.1=EF=BC=88NeoForge=20/=20Forge=20/=20Fabric=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将支持范围扩展到 Minecraft 26.1 / 26.1.1 / 26.1.2,覆盖三个加载器。 NeoForge 26.1(FML 11)完全移除了 ModLauncher,因此新增 NeoForgeMod 作为 @Mod javafml 入口;同时用编译垫片(src/compat)让项目无需依赖 NeoForge 即可构建,垫片不进入最终 jar。 Forge 26.1 仍走 ModLauncher,但启动参数删除了 --fml.mcversion,唯一版本 来源变成 /forge_version.json。原代码用 Class.forName(name) 加载 FMLLoader 会触发静态初始化,初始化失败抛出的 Error 会逃过 catch (Exception)。改用 不初始化的加载方式,并捕获 LinkageError。 Minecraft 25w31a 起 pack_format 替换为 min_format/max_format,值可以是 [major, minor] 数组,原来的 Integer 字段存不下 26.1 的 [84,0],改为 JsonElement。 Fabric 侧无需改动,fabric-loader 0.19.3 上现有入口即可工作。 --- README.md | 8 +- build.gradle.kts | 28 +++- gradle.properties | 6 +- .../net/neoforged/api/distmarker/Dist.java | 7 + .../java/net/neoforged/fml/common/Mod.java | 22 +++ .../java/i18nupdatemod/I18nUpdateMod.java | 32 +++- .../core/ResourcePackConverter.java | 21 ++- .../i18nupdatemod/entity/GameMetaData.java | 11 +- .../modlauncher/ModLauncherService.java | 48 +++++- .../i18nupdatemod/neoforge/NeoForgeMod.java | 95 +++++++++++ .../resources/META-INF/neoforge.mods.toml | 12 ++ src/main/resources/i18nMetaData.json | 25 ++- .../i18nupdatemod/core/I18nConfigTest.java | 60 +++++++ .../core/ResourcePackConverterTest.java | 76 +++++++++ .../entity/GameMetaDataTest.java | 45 +++++ .../modlauncher/ModLauncherServiceTest.java | 74 ++++++++ .../neoforge/FmlApiShapeTest.java | 158 ++++++++++++++++++ .../neoforge/NeoForgeEntrypointTest.java | 106 ++++++++++++ 18 files changed, 806 insertions(+), 28 deletions(-) create mode 100644 src/compat/java/net/neoforged/api/distmarker/Dist.java create mode 100644 src/compat/java/net/neoforged/fml/common/Mod.java create mode 100644 src/main/java/i18nupdatemod/neoforge/NeoForgeMod.java create mode 100644 src/main/resources/META-INF/neoforge.mods.toml create mode 100644 src/test/java/i18nupdatemod/core/I18nConfigTest.java create mode 100644 src/test/java/i18nupdatemod/core/ResourcePackConverterTest.java create mode 100644 src/test/java/i18nupdatemod/entity/GameMetaDataTest.java create mode 100644 src/test/java/i18nupdatemod/modlauncher/ModLauncherServiceTest.java create mode 100644 src/test/java/i18nupdatemod/neoforge/FmlApiShapeTest.java create mode 100644 src/test/java/i18nupdatemod/neoforge/NeoForgeEntrypointTest.java diff --git a/README.md b/README.md index 9a8764b..d6ac438 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ ## 支持的版本 -- Minecraft:1.6.1~1.21.10 都支持 +- Minecraft:1.6.1~1.21.11、26.1~26.1.2 都支持 - Mod加载器:MinecraftForge、NeoForge、Fabric、Quilt 都支持 -- Java:8~21 都支持 +- Java:8~25 都支持 仅仅需要在mods文件夹中放置本Mod的jar文件即可,Mod本身与各主流Minecraft版本、Mod Loader、Java版本均兼容,Mod本身不需要进行任何版本隔离。 @@ -31,7 +31,7 @@ 为了尽可能实用,目前本Mod会根据游戏版本自动下载、合并、转换「简体中文资源包」。 -- 官方资源:1.10.2、1.12.2、1.16、1.18、1.19、1.20、1.21 +- 官方资源:1.10.2、1.12.2、1.16、1.18、1.19、1.20、1.21、26.1 - 合并转换:会合并加转换最近版本的一些资源包,尽可能做最大化的支持 - 特别说明:1.13开始将语言文件变化为json格式,所以不能将1.12.2的资源包用于1.13以上,反之同理 @@ -40,4 +40,4 @@ 请使用Java 8及以上的JDK构建。 ```shell gradle clean shadowJar -``` \ No newline at end of file +``` diff --git a/build.gradle.kts b/build.gradle.kts index 12fcfd9..d083be0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,16 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } +sourceSets { + named("main") { + java { + // The NeoForge entrypoint uses a compile-time annotation shim so this + // universal jar can still be built without a Java 25 NeoForge API jar. + srcDir("src/compat/java") + } + } +} + tasks.withType { options.encoding = "UTF-8" } @@ -34,6 +44,11 @@ tasks.shadowJar { exclude("LICENSE") } +tasks.withType { + // The real API is supplied by NeoForge at runtime. + exclude("net/neoforged/**") +} + repositories { mavenCentral() maven("https://libraries.minecraft.net/") @@ -48,7 +63,13 @@ configurations.configureEach { dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.3") + // This project sets isTransitive = false, so JUnit's own dependencies must be declared by hand. + // opentest4j carries TestAbortedException, which assumeTrue() throws. + testImplementation("org.junit.platform:junit-platform-commons:1.10.3") + testImplementation("org.opentest4j:opentest4j:1.3.0") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.3") + testRuntimeOnly("org.junit.platform:junit-platform-engine:1.10.3") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.3") implementation("net.runelite.archive-patcher:archive-patcher-applier:1.2") compileOnly("org.jetbrains:annotations:24.1.0") @@ -64,6 +85,11 @@ dependencies { tasks.test { useJUnitPlatform() + // Optional: point at a Maven-layout net/neoforged/fancymodloader/loader directory to check the + // reflective FMLLoader lookups against real jars. Those tests skip when it is not set. + (findProperty("fml.loader.libs") as String? ?: System.getenv("FML_LOADER_LIBS"))?.let { + systemProperty("fml.loader.libs", it) + } } tasks.processResources { @@ -103,4 +129,4 @@ curseforge { gameVersionStrings.addAll(curseForgeSpecialVersions) changelog = if (System.getenv("CHANGE_LOG") != null) System.getenv("CHANGE_LOG") else "No change log" } -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index 53d9848..e9b9d30 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.7.1 -minecraft=1.6.1,1.6.2,1.6.4,1.7.2,1.7.10,1.8,1.8.8,1.8.9,1.9,1.9.4,1.10,1.10.2,1.11,1.11.2,1.12,1.12.1,1.12.2,1.13.2,1.14,1.14.1,1.14.2,1.14.3,1.14.4,1.15,1.15.1,1.15.2,1.16,1.16.1,1.16.2,1.16.3,1.16.4,1.16.5,1.17,1.17.1,1.18,1.18.1,1.18.2,1.19,1.19.1,1.19.2,1.19.3,1.19.4,1.20,1.20.1,1.20.2,1.20.3,1.20.4,1.20.5,1.20.6,1.21,1.21.1,1.21.2,1.21.3,1.21.4,1.21.5,1.21.6,1.21.7,1.21.8,1.21.9,1.21.10,1.21.11 -curseforge=NeoForge,Forge,Fabric,Quilt,Client,Java 8,Java 9,Java 10,Java 11,Java 12,Java 13,Java 14,Java 15,Java 16,Java 17,Java 18 \ No newline at end of file +version=3.7.2 +minecraft=1.6.1,1.6.2,1.6.4,1.7.2,1.7.10,1.8,1.8.8,1.8.9,1.9,1.9.4,1.10,1.10.2,1.11,1.11.2,1.12,1.12.1,1.12.2,1.13.2,1.14,1.14.1,1.14.2,1.14.3,1.14.4,1.15,1.15.1,1.15.2,1.16,1.16.1,1.16.2,1.16.3,1.16.4,1.16.5,1.17,1.17.1,1.18,1.18.1,1.18.2,1.19,1.19.1,1.19.2,1.19.3,1.19.4,1.20,1.20.1,1.20.2,1.20.3,1.20.4,1.20.5,1.20.6,1.21,1.21.1,1.21.2,1.21.3,1.21.4,1.21.5,1.21.6,1.21.7,1.21.8,1.21.9,1.21.10,1.21.11,26.1,26.1.1,26.1.2 +curseforge=NeoForge,Forge,Fabric,Quilt,Client,Java 8,Java 9,Java 10,Java 11,Java 12,Java 13,Java 14,Java 15,Java 16,Java 17,Java 18,Java 19,Java 20,Java 21,Java 22,Java 23,Java 24,Java 25 diff --git a/src/compat/java/net/neoforged/api/distmarker/Dist.java b/src/compat/java/net/neoforged/api/distmarker/Dist.java new file mode 100644 index 0000000..1549fc2 --- /dev/null +++ b/src/compat/java/net/neoforged/api/distmarker/Dist.java @@ -0,0 +1,7 @@ +package net.neoforged.api.distmarker; + +/** Compile-time shim for the NeoForge distribution enum. */ +public enum Dist { + CLIENT, + DEDICATED_SERVER +} diff --git a/src/compat/java/net/neoforged/fml/common/Mod.java b/src/compat/java/net/neoforged/fml/common/Mod.java new file mode 100644 index 0000000..5c843bd --- /dev/null +++ b/src/compat/java/net/neoforged/fml/common/Mod.java @@ -0,0 +1,22 @@ +package net.neoforged.fml.common; + +import net.neoforged.api.distmarker.Dist; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Compile-time shim for the NeoForge annotation. The class is excluded from + * the produced JAR, so NeoForge supplies the runtime annotation implementation. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Mod { + String value(); + + Dist[] dist() default {Dist.CLIENT, Dist.DEDICATED_SERVER}; + + String[] depends() default {}; +} diff --git a/src/main/java/i18nupdatemod/I18nUpdateMod.java b/src/main/java/i18nupdatemod/I18nUpdateMod.java index 3566e57..6139897 100644 --- a/src/main/java/i18nupdatemod/I18nUpdateMod.java +++ b/src/main/java/i18nupdatemod/I18nUpdateMod.java @@ -20,6 +20,7 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -29,7 +30,21 @@ public class I18nUpdateMod { public static final Gson GSON = new Gson(); + /** + * Older NeoForge can reach {@link #init} through both the ModLauncher service and the + * {@code @Mod} entrypoint; only the first one should do the work. + */ + private static final AtomicBoolean INITIALIZED = new AtomicBoolean(); + + public static boolean isInitialized() { + return INITIALIZED.get(); + } + public static void init(Path minecraftPath, String minecraftVersion, String loader, @NotNull HashSet modDomainsSet) { + if (!INITIALIZED.compareAndSet(false, true)) { + Log.debug("Already initialized, skipping duplicate entrypoint"); + return; + } try (InputStream is = I18nUpdateMod.class.getResourceAsStream("/i18nMetaData.json")) { MOD_VERSION = GSON.fromJson(new InputStreamReader(is), JsonObject.class).get("version").getAsString(); } catch (Exception e) { @@ -37,6 +52,11 @@ public static void init(Path minecraftPath, String minecraftVersion, String load } modDomainsSet.remove("i18nupdatemod"); + int minecraftMajorVersion = getMinecraftMajorVersion(minecraftVersion); + if (minecraftMajorVersion >= 26) { + // The 26.1 pack supplies its CJK font in the vanilla resource domain. + modDomainsSet.add("minecraft"); + } Log.info(String.format("I18nUpdate Mod %s is loaded in %s with %s", MOD_VERSION, minecraftVersion, loader)); Log.debug(String.format("Minecraft path: %s", minecraftPath)); @@ -58,8 +78,6 @@ public static void init(Path minecraftPath, String minecraftVersion, String load FileUtil.setResourcePackDirPath(minecraftPath.resolve("resourcepacks")); - int minecraftMajorVersion = Integer.parseInt(minecraftVersion.split("\\.")[1]); - try { //Get asset GameAssetDetail assets = I18nConfig.getAssetDetail(minecraftVersion, loader); @@ -100,6 +118,14 @@ private static String getResourcePackDescription(List= 2 && "1".equals(parts[0])) { + return Integer.parseInt(parts[1]); + } + return Integer.parseInt(parts[0]); + } + public static String getLocalStoragePos(Path minecraftPath) { Path userHome = Paths.get(System.getProperty("user.home")); Path oldPath = userHome.resolve("." + MOD_ID); @@ -124,4 +150,4 @@ public static String getLocalStoragePos(Path minecraftPath) { ).findFirst().orElse(xdgDataHome); } -} \ No newline at end of file +} diff --git a/src/main/java/i18nupdatemod/core/ResourcePackConverter.java b/src/main/java/i18nupdatemod/core/ResourcePackConverter.java index ea511e3..6ed815b 100644 --- a/src/main/java/i18nupdatemod/core/ResourcePackConverter.java +++ b/src/main/java/i18nupdatemod/core/ResourcePackConverter.java @@ -2,6 +2,8 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; import i18nupdatemod.entity.GameMetaData; import i18nupdatemod.util.FileUtil; import i18nupdatemod.util.Log; @@ -44,8 +46,10 @@ public void convert(GameMetaData metaData, String description, HashSet m ZipEntry ze = e.nextElement(); String name = ze.getName(); String[] parts = name.split("/"); - // 正在筛选的是assets/modDomain/** && 当前的modDomain不需要 - if (parts.length >= 2 && !modDomainsSet.contains(parts[1])) { + // Keep only installed mod domains under assets/; top-level pack files + // and vanilla assets are retained. + if (parts.length >= 3 && "assets".equals(parts[0]) + && !modDomainsSet.contains(parts[1])) { continue; } @@ -80,10 +84,11 @@ public void convert(GameMetaData metaData, String description, HashSet m private byte[] convertPackMeta(InputStream is, GameMetaData metaData, String description) { PackMeta meta = GSON.fromJson(new InputStreamReader(is, StandardCharsets.UTF_8), PackMeta.class); - meta.pack.pack_format = metaData.useNewFormat() ? null : metaData.packFormat; + meta.pack.pack_format = metaData.useNewFormat() || metaData.packFormat == null + ? null : new JsonPrimitive(metaData.packFormat); meta.pack.min_format = metaData.useNewFormat() ? metaData.minFormat : null; meta.pack.max_format = metaData.useNewFormat() ? metaData.maxFormat : null; - meta.pack.description = description; + meta.pack.description = new JsonPrimitive(description); return GSON.toJson(meta).getBytes(StandardCharsets.UTF_8); } @@ -91,10 +96,10 @@ private static class PackMeta { Pack pack; private static class Pack { - Integer pack_format; // 改为 Integer,支持 null - Integer min_format; - Integer max_format; - String description; + JsonElement pack_format; + JsonElement min_format; + JsonElement max_format; + JsonElement description; } } } diff --git a/src/main/java/i18nupdatemod/entity/GameMetaData.java b/src/main/java/i18nupdatemod/entity/GameMetaData.java index b9f11e7..988ae90 100644 --- a/src/main/java/i18nupdatemod/entity/GameMetaData.java +++ b/src/main/java/i18nupdatemod/entity/GameMetaData.java @@ -1,11 +1,18 @@ package i18nupdatemod.entity; +import com.google.gson.JsonElement; + import java.util.List; public class GameMetaData { public String gameVersions; - public Integer packFormat, minFormat, maxFormat; + public Integer packFormat; + /** Supports both pre-26 integer formats and 26.1 structured formats. */ + public JsonElement minFormat, maxFormat; public List convertFrom; - public boolean useNewFormat() { return minFormat != null && maxFormat != null; } + public boolean useNewFormat() { + return minFormat != null && maxFormat != null + && !minFormat.isJsonNull() && !maxFormat.isJsonNull(); + } } diff --git a/src/main/java/i18nupdatemod/modlauncher/ModLauncherService.java b/src/main/java/i18nupdatemod/modlauncher/ModLauncherService.java index 33e0421..f28b3ad 100644 --- a/src/main/java/i18nupdatemod/modlauncher/ModLauncherService.java +++ b/src/main/java/i18nupdatemod/modlauncher/ModLauncherService.java @@ -19,7 +19,14 @@ import static i18nupdatemod.I18nUpdateMod.GSON; -//1.13-latest +/** + * Forge 1.13+ (including Minecraft 26.1) and NeoForge up to Minecraft 1.21.x. + *

+ * Forge still boots through ModLauncher on 26.1 (bootstrap 2.1.8 + modlauncher 10.2.6), so this + * service remains the Forge entrypoint there. NeoForge's FML 11 dropped ModLauncher entirely, so on + * NeoForge 26.1+ this service is never loaded and {@link i18nupdatemod.neoforge.NeoForgeMod} takes + * over instead. + */ public class ModLauncherService implements ITransformationService { @Override public @NotNull String name() { @@ -39,7 +46,9 @@ public void initialize(IEnvironment environment) { Log.warning("Minecraft version not found"); return; } - I18nUpdateMod.init(minecraftPath.get(), minecraftVersion, "Forge", ModUtil.getModDomainsFromModsFolder(minecraftPath.get(), minecraftVersion, "Forge")); + String loader = isNeoForge() ? "NeoForge" : "Forge"; + I18nUpdateMod.init(minecraftPath.get(), minecraftVersion, loader, + ModUtil.getModDomainsFromModsFolder(minecraftPath.get(), minecraftVersion, loader)); } @Override @@ -67,20 +76,47 @@ private String getMinecraftVersion() { return args[i + 1]; } } - } catch (Exception e) { + } catch (Exception | LinkageError e) { Log.warning("Error getting minecraft version: %s", e); } - // MinecraftForge 1.20.3~ + // MinecraftForge 1.20.3~, and the only source on Forge 26.1 where --fml.mcversion is gone // 1.20.3: https://github.com/MinecraftForge/MinecraftForge/blob/1.20.x/fmlloader/src/main/java/net/minecraftforge/fml/loading/VersionInfo.java try { - Class clazz = Class.forName("net.minecraftforge.fml.loading.FMLLoader"); + // Resolved without running static initializers: only the class's resource root is needed, + // and initializing it can fail with an Error that would escape into the loader. + Class clazz = loadClass("net.minecraftforge.fml.loading.FMLLoader"); + if (clazz == null) { + return null; + } try (InputStream is = clazz.getResourceAsStream("/forge_version.json")) { + if (is == null) { + Log.warning("forge_version.json not found"); + return null; + } return GSON.fromJson(new InputStreamReader(is), JsonObject.class).get("mc").getAsString(); } - } catch (Exception e) { + } catch (Exception | LinkageError e) { Log.warning("Error getting minecraft version: %s", e); } return null; } + + private boolean isNeoForge() { + return loadClass("net.neoforged.fml.loading.FMLLoader") != null; + } + + /** + * @return the class, or null when it is absent or cannot be loaded + */ + private static Class loadClass(String name) { + try { + return Class.forName(name, false, ModLauncherService.class.getClassLoader()); + } catch (ClassNotFoundException ignored) { + return null; + } catch (LinkageError e) { + Log.warning("Error loading %s: %s", name, e); + return null; + } + } } diff --git a/src/main/java/i18nupdatemod/neoforge/NeoForgeMod.java b/src/main/java/i18nupdatemod/neoforge/NeoForgeMod.java new file mode 100644 index 0000000..184d734 --- /dev/null +++ b/src/main/java/i18nupdatemod/neoforge/NeoForgeMod.java @@ -0,0 +1,95 @@ +package i18nupdatemod.neoforge; + +import i18nupdatemod.I18nUpdateMod; +import i18nupdatemod.util.Log; +import i18nupdatemod.util.ModUtil; +import i18nupdatemod.util.Reflection; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.common.Mod; + +import java.nio.file.Path; + +/** + * NeoForge javafml entrypoint. + *

+ * Required from FML 11 (Minecraft 26.1) onwards, where ModLauncher was removed and + * {@link i18nupdatemod.modlauncher.ModLauncherService} is therefore never invoked. Older NeoForge + * still loads this class from the same universal jar, and there FMLLoader exposes static + * {@code getGamePath()}/{@code versionInfo()} instead of the current instance methods, so both + * shapes are resolved reflectively. + */ +@Mod(value = I18nUpdateMod.MOD_ID, dist = Dist.CLIENT) +public final class NeoForgeMod { + private static final String FML_LOADER = "net.neoforged.fml.loading.FMLLoader"; + + public NeoForgeMod() { + try { + if (I18nUpdateMod.isInitialized()) { + // ModLauncherService already did the work on an older NeoForge. + Log.debug("Already initialized, skipping NeoForge entrypoint"); + return; + } + + Path gameDir = getGameDir(); + if (gameDir == null) { + Log.warning("Minecraft path not found"); + return; + } + Log.setMinecraftLogFile(gameDir); + String mcVersion = getMcVersion(); + if (mcVersion == null) { + Log.warning("Minecraft version not found"); + return; + } + + I18nUpdateMod.init(gameDir, mcVersion, "NeoForge", + ModUtil.getModDomainsFromModsFolder(gameDir, mcVersion, "NeoForge")); + } catch (Exception e) { + Log.warning("Failed to initialize NeoForge entrypoint: " + e); + } + } + + /** + * @return game directory, or null if neither API shape is present + */ + private static Path getGameDir() { + // FML 11+ (Minecraft 26.1+) + try { + return (Path) Reflection.clazz(FML_LOADER) + .get("getCurrent()") + .get("getGameDir()") + .get(); + } catch (Exception ignored) { + } + // FML 1~4 (Minecraft 1.20.1~1.21.x) + try { + return (Path) Reflection.clazz(FML_LOADER).get("getGamePath()").get(); + } catch (Exception ignored) { + } + return null; + } + + /** + * @return Minecraft version, or null if neither API shape is present + */ + private static String getMcVersion() { + // FML 11+ (Minecraft 26.1+) + try { + return (String) Reflection.clazz(FML_LOADER) + .get("getCurrent()") + .get("getVersionInfo()") + .get("mcVersion()") + .get(); + } catch (Exception ignored) { + } + // FML 1~4 (Minecraft 1.20.1~1.21.x) + try { + return (String) Reflection.clazz(FML_LOADER) + .get("versionInfo()") + .get("mcVersion()") + .get(); + } catch (Exception ignored) { + } + return null; + } +} diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..ccb83b0 --- /dev/null +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,12 @@ +modLoader="javafml" +loaderVersion="[3,]" +license="AGPL-3.0-only" + +[[mods]] +modId="i18nupdatemod" +version="${version}" +displayName="I18nUpdateMod" +authors="CFPAOrg" +description=''' +Automatically downloads, updates, converts, and applies the CFPA Chinese language resource pack. +''' diff --git a/src/main/resources/i18nMetaData.json b/src/main/resources/i18nMetaData.json index 3c174bd..ca85a41 100644 --- a/src/main/resources/i18nMetaData.json +++ b/src/main/resources/i18nMetaData.json @@ -183,9 +183,32 @@ "1.20", "1.19" ] + }, + { + "gameVersions": "[26.1,26.1.2]", + "minFormat": [84, 0], + "maxFormat": [84, 0], + "convertFrom": [ + "26.1", + "1.21", + "1.20", + "1.19" + ] } ], "assets": [ + { + "targetVersion": "26.1", + "loader": "Forge", + "filename": "Minecraft-Mod-Language-Modpack-26-1.zip", + "md5Filename": "26.1.md5" + }, + { + "targetVersion": "26.1", + "loader": "Fabric", + "filename": "Minecraft-Mod-Language-Modpack-26-1-Fabric.zip", + "md5Filename": "26.1-fabric.md5" + }, { "targetVersion": "1.10.2", "loader": "Forge", @@ -253,4 +276,4 @@ "md5Filename": "1.21-fabric.md5" } ] -} \ No newline at end of file +} diff --git a/src/test/java/i18nupdatemod/core/I18nConfigTest.java b/src/test/java/i18nupdatemod/core/I18nConfigTest.java new file mode 100644 index 0000000..29bcc88 --- /dev/null +++ b/src/test/java/i18nupdatemod/core/I18nConfigTest.java @@ -0,0 +1,60 @@ +package i18nupdatemod.core; + +import i18nupdatemod.entity.GameMetaData; +import i18nupdatemod.entity.GameAssetDetail; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class I18nConfigTest { + @Test + void supportsEveryMinecraft261Patch() { + for (String version : new String[]{"26.1", "26.1.1", "26.1.2"}) { + GameMetaData metadata = I18nConfig.getPackFormat(version); + + assertEquals(84, metadata.minFormat.getAsJsonArray().get(0).getAsInt()); + assertEquals(0, metadata.minFormat.getAsJsonArray().get(1).getAsInt()); + assertEquals(metadata.minFormat, metadata.maxFormat); + assertEquals("26.1", metadata.convertFrom.get(0)); + } + } + + /** + * Forge keeps booting through ModLauncher on 26.1, so it reaches this code with loader="Forge" + * and must land on the Forge pack. NeoForge arrives via the javafml entrypoint instead, and + * Fabric must pick the separate Fabric pack. + */ + @Test + void picksTheLoaderSpecificPackFor261() { + for (String version : new String[]{"26.1", "26.1.1", "26.1.2"}) { + for (String loader : new String[]{"Forge", "NeoForge"}) { + assertEquals("Minecraft-Mod-Language-Modpack-26-1.zip", + firstDownload(version, loader).fileName, loader + " on " + version); + } + assertEquals("Minecraft-Mod-Language-Modpack-26-1-Fabric.zip", + firstDownload(version, "Fabric").fileName, "Fabric on " + version); + } + } + + /** + * 26.1 has no pack of its own for every fallback generation, so the chain must keep walking back + * to the 1.21/1.20/1.19 packs rather than stopping at the first miss. + */ + @Test + void keepsTheOlderPacksAsFallbacksOn261() { + List downloads = + I18nConfig.getAssetDetail("26.1.2", "Forge").downloads; + + assertEquals(4, downloads.size()); + assertEquals("26.1", downloads.get(0).targetVersion); + assertEquals("1.21", downloads.get(1).targetVersion); + assertEquals("1.20", downloads.get(2).targetVersion); + assertEquals("1.19", downloads.get(3).targetVersion); + } + + private static GameAssetDetail.AssetDownloadDetail firstDownload(String version, String loader) { + return I18nConfig.getAssetDetail(version, loader).downloads.get(0); + } +} diff --git a/src/test/java/i18nupdatemod/core/ResourcePackConverterTest.java b/src/test/java/i18nupdatemod/core/ResourcePackConverterTest.java new file mode 100644 index 0000000..cb4e522 --- /dev/null +++ b/src/test/java/i18nupdatemod/core/ResourcePackConverterTest.java @@ -0,0 +1,76 @@ +package i18nupdatemod.core; + +import com.google.gson.JsonParser; +import i18nupdatemod.entity.GameMetaData; +import i18nupdatemod.util.FileUtil; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ResourcePackConverterTest { + @TempDir + Path tempDir; + + @Test + void keepsVanillaAndInstalledModDomains() throws Exception { + FileUtil.setResourcePackDirPath(tempDir.resolve("resourcepacks")); + FileUtil.setTemporaryDirPath(tempDir.resolve("temporary")); + + Path sourceFile = tempDir.resolve("source.zip"); + try (OutputStream output = Files.newOutputStream(sourceFile); + ZipOutputStream zip = new ZipOutputStream(output, StandardCharsets.UTF_8)) { + writeEntry(zip, "pack.mcmeta", "{\"pack\":{\"pack_format\":75}}\n"); + writeEntry(zip, "assets/minecraft/font/default.json", "{}\n"); + writeEntry(zip, "assets/installed_mod/lang/zh_cn.json", "{}\n"); + writeEntry(zip, "assets/uninstalled_mod/lang/zh_cn.json", "{}\n"); + } + + ResourcePack source = new ResourcePack("source.zip"); + Files.copy(sourceFile, source.getTmpFilePath(), StandardCopyOption.REPLACE_EXISTING); + + GameMetaData metadata = new GameMetaData(); + metadata.minFormat = JsonParser.parseString("[84,0]"); + metadata.maxFormat = JsonParser.parseString("[84,0]"); + + ResourcePackConverter converter = new ResourcePackConverter( + Collections.singletonList(source), "converted.zip"); + converter.convert(metadata, "test pack", new HashSet<>(Arrays.asList("minecraft", "installed_mod"))); + + try (ZipFile result = new ZipFile(tempDir.resolve("resourcepacks/converted.zip").toFile(), StandardCharsets.UTF_8)) { + assertNotNull(result.getEntry("assets/minecraft/font/default.json")); + assertNotNull(result.getEntry("assets/installed_mod/lang/zh_cn.json")); + assertFalse(result.stream().anyMatch(entry -> + entry.getName().equals("assets/uninstalled_mod/lang/zh_cn.json"))); + + String packMeta = IOUtils.toString(result.getInputStream(result.getEntry("pack.mcmeta")), + StandardCharsets.UTF_8); + assertEquals(84, JsonParser.parseString(packMeta).getAsJsonObject() + .getAsJsonObject("pack").getAsJsonArray("min_format").get(0).getAsInt()); + assertEquals(84, JsonParser.parseString(packMeta).getAsJsonObject() + .getAsJsonObject("pack").getAsJsonArray("max_format").get(0).getAsInt()); + } + } + + private static void writeEntry(ZipOutputStream zip, String name, String content) throws Exception { + zip.putNextEntry(new ZipEntry(name)); + zip.write(content.getBytes(StandardCharsets.UTF_8)); + zip.closeEntry(); + } +} diff --git a/src/test/java/i18nupdatemod/entity/GameMetaDataTest.java b/src/test/java/i18nupdatemod/entity/GameMetaDataTest.java new file mode 100644 index 0000000..34b98e7 --- /dev/null +++ b/src/test/java/i18nupdatemod/entity/GameMetaDataTest.java @@ -0,0 +1,45 @@ +package i18nupdatemod.entity; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GameMetaDataTest { + private final Gson gson = new Gson(); + + @Test + void acceptsStructuredPackFormatsIntroducedBy261() { + // 26.1.2 client version.json: resource_major 84, resource_minor 0. + GameMetaData metadata = gson.fromJson( + "{\"minFormat\":[84,0],\"maxFormat\":[84,0]}", GameMetaData.class); + + assertTrue(metadata.useNewFormat()); + assertEquals(84, metadata.minFormat.getAsJsonArray().get(0).getAsInt()); + assertEquals(0, metadata.maxFormat.getAsJsonArray().get(1).getAsInt()); + } + + @Test + void acceptsLegacyNumericPackFormats() { + GameMetaData metadata = gson.fromJson( + "{\"packFormat\":75,\"minFormat\":69,\"maxFormat\":75}", GameMetaData.class); + + assertTrue(metadata.useNewFormat()); + assertEquals(69, metadata.minFormat.getAsInt()); + assertFalse(metadata.minFormat.isJsonArray()); + } + + @Test + void parsesBothMinecraftVersionSchemes() throws Exception { + Method method = Class.forName("i18nupdatemod.I18nUpdateMod") + .getDeclaredMethod("getMinecraftMajorVersion", String.class); + method.setAccessible(true); + + assertEquals(21, method.invoke(null, "1.21.11")); + assertEquals(26, method.invoke(null, "26.1.2")); + } +} diff --git a/src/test/java/i18nupdatemod/modlauncher/ModLauncherServiceTest.java b/src/test/java/i18nupdatemod/modlauncher/ModLauncherServiceTest.java new file mode 100644 index 0000000..a593e73 --- /dev/null +++ b/src/test/java/i18nupdatemod/modlauncher/ModLauncherServiceTest.java @@ -0,0 +1,74 @@ +package i18nupdatemod.modlauncher; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Forge still boots through ModLauncher on Minecraft 26.1, so this service stays the Forge + * entrypoint there. Two things changed on that version and are pinned here. + */ +class ModLauncherServiceTest { + /** + * Forge 26.1 launches with only {@code --launchTarget forge_client}; the {@code --fml.mcversion} + * argument this code reads first is gone, so the {@code /forge_version.json} fallback is now the + * only source of the version. It must degrade to null rather than throwing when neither is + * reachable, because {@code initialize} runs inside the loader. + */ + @Test + void reportsNoVersionOutsideForgeInsteadOfThrowing() throws Exception { + assertNull(invoke("getMinecraftVersion")); + } + + /** + * The loader label picks the Forge or NeoForge pack, so it must not report NeoForge when the + * NeoForge FMLLoader is absent. + */ + @Test + void doesNotClaimNeoForgeWhenItIsAbsent() throws Exception { + assertFalse((Boolean) invoke("isNeoForge")); + } + + /** + * FMLLoader is only needed as a resource anchor. Initializing it drags in the loader's own + * runtime, and on Forge 26.1 those classes are compiled for a newer Java than older launchers + * run, which raises an Error that {@code catch (Exception)} would not stop. Loading it + * non-initializing keeps such failures out of the launch path. + */ + @Test + void looksUpClassesWithoutRunningStaticInitializers() throws Exception { + Method loadClass = ModLauncherService.class.getDeclaredMethod("loadClass", String.class); + loadClass.setAccessible(true); + + assertNull(loadClass.invoke(null, "does.not.Exist")); + assertTrue(ExplodesOnInit.class == loadClass.invoke(null, ExplodesOnInit.class.getName()), + "must return the class without initializing it"); + // Held in a separate class, since reading a field of ExplodesOnInit would itself initialize it. + assertFalse(InitFlag.reached, "static initializer must not have run"); + } + + private static Object invoke(String name) throws Exception { + Method method = ModLauncherService.class.getDeclaredMethod(name); + method.setAccessible(true); + return method.invoke(new ModLauncherService()); + } + + /** Records initialization out of band, so observing it does not cause it. */ + static class InitFlag { + static boolean reached; + } + + /** Stands in for an FMLLoader whose static initializer fails. */ + static class ExplodesOnInit { + static { + InitFlag.reached = true; + if (Boolean.parseBoolean("true")) { + throw new RuntimeException("static initializer must not be reached"); + } + } + } +} diff --git a/src/test/java/i18nupdatemod/neoforge/FmlApiShapeTest.java b/src/test/java/i18nupdatemod/neoforge/FmlApiShapeTest.java new file mode 100644 index 0000000..a41e805 --- /dev/null +++ b/src/test/java/i18nupdatemod/neoforge/FmlApiShapeTest.java @@ -0,0 +1,158 @@ +package i18nupdatemod.neoforge; + +import org.junit.jupiter.api.Test; + +import java.io.DataInputStream; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Set; +import java.util.zip.ZipFile; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Checks the reflective FMLLoader lookups in {@link NeoForgeMod} against real FancyModLoader jars. + *

+ * The build has no NeoForge dependency (the API is compiled against a shim and excluded from the + * jar), so nothing else catches a renamed method. Skipped when the jars are not present. + */ +class FmlApiShapeTest { + /** + * Maven-layout directory holding {@code /loader-.jar}, e.g. the + * {@code net/neoforged/fancymodloader/loader} folder of any launcher's library cache. Set + * {@code -Dfml.loader.libs=...} or the {@code FML_LOADER_LIBS} environment variable to run these + * checks; without it there is nothing to verify against and both tests skip. + */ + private static final String LIB_PROPERTY = "fml.loader.libs"; + + private static Path libraryDir() { + String configured = System.getProperty(LIB_PROPERTY, System.getenv("FML_LOADER_LIBS")); + return configured == null || configured.isEmpty() ? null : Paths.get(configured); + } + + @Test + void fml11ExposesTheInstanceApiTheEntrypointUses() throws Exception { + Set methods = methodsOf("11.0.15"); + assumeTrue(!methods.isEmpty(), "set -D" + LIB_PROPERTY + " to a library dir containing FML 11"); + + assertTrue(methods.contains("getCurrent"), methods.toString()); + assertTrue(methods.contains("getGameDir"), methods.toString()); + assertTrue(methods.contains("getVersionInfo"), methods.toString()); + // The pre-26.1 statics are gone here, which is why the fallback exists. + assertTrue(!methods.contains("getGamePath")); + assertTrue(!methods.contains("versionInfo")); + } + + @Test + void fml4ExposesTheStaticApiTheFallbackUses() throws Exception { + Set methods = methodsOf("4.0.39"); + assumeTrue(!methods.isEmpty(), "set -D" + LIB_PROPERTY + " to a library dir containing FML 4"); + + assertTrue(methods.contains("getGamePath"), methods.toString()); + assertTrue(methods.contains("versionInfo"), methods.toString()); + // getCurrent() does not exist yet, so the primary lookup must be allowed to fail. + assertTrue(!methods.contains("getCurrent")); + } + + /** + * @return declared method names of FMLLoader in the given loader version, empty if absent + */ + private static Set methodsOf(String version) throws Exception { + Path lib = libraryDir(); + if (lib == null) { + return new HashSet<>(); + } + Path jar = lib.resolve(version).resolve("loader-" + version + ".jar"); + if (!Files.exists(jar)) { + return new HashSet<>(); + } + Set methods = new HashSet<>(); + try (ZipFile zf = new ZipFile(jar.toFile())) { + java.util.zip.ZipEntry entry = zf.getEntry("net/neoforged/fml/loading/FMLLoader.class"); + if (entry == null) { + return methods; + } + try (DataInputStream input = new DataInputStream(zf.getInputStream(entry))) { + methods.addAll(readMethodNames(input)); + } + } + return methods; + } + + /** + * Minimal class file reader. ASM 9.7 rejects the class file version FML 11 is built with, so the + * constant pool and method table are walked by hand. + * + * @see JVMS §4 + */ + private static Set readMethodNames(DataInputStream in) throws Exception { + in.readInt(); // magic + in.readShort(); // minor version + in.readShort(); // major version + + int constantPoolCount = in.readUnsignedShort(); + String[] utf8 = new String[constantPoolCount]; + for (int i = 1; i < constantPoolCount; i++) { + int tag = in.readUnsignedByte(); + switch (tag) { + case 1: // Utf8 + utf8[i] = in.readUTF(); + break; + case 7: // Class + case 8: // String + case 16: // MethodType + case 19: // Module + case 20: // Package + in.skipBytes(2); + break; + case 15: // MethodHandle + in.skipBytes(3); + break; + case 5: // Long + case 6: // Double + in.skipBytes(8); + i++; // these take two constant pool slots + break; + default: // Integer, Float, refs, InvokeDynamic, ... + in.skipBytes(4); + } + } + + in.readShort(); // access flags + in.readShort(); // this class + in.readShort(); // super class + in.skipBytes(in.readUnsignedShort() * 2); // interfaces + skipMembers(in); // fields + + Set methods = new HashSet<>(); + int methodCount = in.readUnsignedShort(); + for (int i = 0; i < methodCount; i++) { + in.readShort(); // access flags + methods.add(utf8[in.readUnsignedShort()]); + in.readShort(); // descriptor + skipAttributes(in); + } + return methods; + } + + private static void skipMembers(DataInputStream in) throws Exception { + int count = in.readUnsignedShort(); + for (int i = 0; i < count; i++) { + in.skipBytes(6); // access flags, name, descriptor + skipAttributes(in); + } + } + + private static void skipAttributes(DataInputStream in) throws Exception { + int count = in.readUnsignedShort(); + for (int i = 0; i < count; i++) { + in.readShort(); // name index + int length = in.readInt(); + in.skipBytes(length); + } + } +} diff --git a/src/test/java/i18nupdatemod/neoforge/NeoForgeEntrypointTest.java b/src/test/java/i18nupdatemod/neoforge/NeoForgeEntrypointTest.java new file mode 100644 index 0000000..fd151a8 --- /dev/null +++ b/src/test/java/i18nupdatemod/neoforge/NeoForgeEntrypointTest.java @@ -0,0 +1,106 @@ +package i18nupdatemod.neoforge; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class NeoForgeEntrypointTest { + @Test + void declaresTheClientOnlyNeoForgeEntrypoint() throws Exception { + AtomicBoolean foundModAnnotation = new AtomicBoolean(); + AtomicReference modId = new AtomicReference<>(); + AtomicReference dist = new AtomicReference<>(); + + try (InputStream input = NeoForgeMod.class.getResourceAsStream("/i18nupdatemod/neoforge/NeoForgeMod.class")) { + new ClassReader(input).accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (!"Lnet/neoforged/fml/common/Mod;".equals(descriptor)) { + return null; + } + foundModAnnotation.set(visible); + return new AnnotationVisitor(Opcodes.ASM9) { + @Override + public void visit(String name, Object value) { + if ("value".equals(name)) { + modId.set((String) value); + } + } + + @Override + public AnnotationVisitor visitArray(String name) { + if (!"dist".equals(name)) { + return null; + } + return new AnnotationVisitor(Opcodes.ASM9) { + @Override + public void visitEnum(String name, String descriptor, String value) { + dist.set(value); + } + }; + } + }; + } + }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + } + + assertTrue(foundModAnnotation.get()); + assertEquals("i18nupdatemod", modId.get()); + assertEquals("CLIENT", dist.get()); + } + + /** + * FML 11 (Minecraft 26.1+) exposes instance {@code getCurrent().getGameDir()/getVersionInfo()}, + * while FML 1~4 (Minecraft 1.20.1~1.21.x) exposes static {@code getGamePath()/versionInfo()}. + * The same universal jar loads on both, so the entrypoint must not bind either shape directly. + */ + @Test + void resolvesBothFmlApiShapesWithoutHardBinding() throws Exception { + Set fmlMethods = new HashSet<>(); + try (InputStream input = NeoForgeMod.class.getResourceAsStream("/i18nupdatemod/neoforge/NeoForgeMod.class")) { + new ClassReader(input).accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + return new MethodVisitor(Opcodes.ASM9) { + @Override + public void visitMethodInsn(int opcode, String owner, String name, + String descriptor, boolean isInterface) { + if (owner.startsWith("net/neoforged/fml/loading/")) { + fmlMethods.add(owner + "." + name); + } + } + + @Override + public void visitLdcInsn(Object value) { + if (value instanceof String) { + fmlMethods.add("LDC:" + value); + } + } + }; + } + }, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + } + + // No direct call may reach FMLLoader, or loading fails on whichever FML lacks that method. + assertTrue(fmlMethods.stream().noneMatch(it -> it.startsWith("net/neoforged/fml/loading/")), + "NeoForgeMod must not link FMLLoader directly, found: " + fmlMethods); + // Both API shapes must be attempted reflectively. + assertTrue(fmlMethods.contains("LDC:getCurrent()")); + assertTrue(fmlMethods.contains("LDC:getGamePath()")); + assertTrue(fmlMethods.contains("LDC:versionInfo()")); + assertTrue(fmlMethods.contains("LDC:getVersionInfo()")); + } +} From 8fe7db0c897ba0662cb18f93ae518d3416fe9a32 Mon Sep 17 00:00:00 2001 From: qawow <3441561646@qq.com> Date: Tue, 11 Aug 2026 12:19:35 +0800 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=E6=94=B6=E6=95=9B=E4=B8=BA=202?= =?UTF-8?q?6.1=20=E6=94=AF=E6=8C=81=E6=89=80=E9=9C=80=E7=9A=84=E6=9C=80?= =?UTF-8?q?=E5=B0=8F=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除三处与 26.1 支持无关、经实测为无行为差异的改动: - ResourcePackConverter 的域过滤条件还原为 main 的写法。在 26.1/1.21/ 1.20/1.19 四个真实资源包上逐条目比对,两种写法筛选结果完全一致, 属于纯重构。 - ModLauncherService 不再按 NeoForge 改写 loader 标签。metadata 中每个 版本的 Forge 与 NeoForge 条目都指向同一个包,该标签不影响下载选择; NeoForge 误判问题属于独立议题。 - 不再向域集合注入 minecraft。1.21/1.20/1.19 的包同样把 CJK 字体放在 assets/minecraft/ 下,该域本就被保留,注入会改变所有版本的既有行为。 随之移除因此变为死代码的 isNeoForge(),以及依赖外部 NeoForge jar、 默认始终跳过的 FmlApiShapeTest 及其构建配置。 改动后以真实的 4 个源包与 236 个 mod jar 复跑转换,产物与线上包逐条目 一致,pack.mcmeta 完全相同。 --- build.gradle.kts | 6 - .../java/i18nupdatemod/I18nUpdateMod.java | 7 +- .../core/ResourcePackConverter.java | 6 +- .../modlauncher/ModLauncherService.java | 8 +- .../modlauncher/ModLauncherServiceTest.java | 11 +- .../neoforge/FmlApiShapeTest.java | 158 ------------------ 6 files changed, 6 insertions(+), 190 deletions(-) delete mode 100644 src/test/java/i18nupdatemod/neoforge/FmlApiShapeTest.java diff --git a/build.gradle.kts b/build.gradle.kts index d083be0..b9b1b1f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -64,7 +64,6 @@ configurations.configureEach { dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.3") // This project sets isTransitive = false, so JUnit's own dependencies must be declared by hand. - // opentest4j carries TestAbortedException, which assumeTrue() throws. testImplementation("org.junit.platform:junit-platform-commons:1.10.3") testImplementation("org.opentest4j:opentest4j:1.3.0") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.3") @@ -85,11 +84,6 @@ dependencies { tasks.test { useJUnitPlatform() - // Optional: point at a Maven-layout net/neoforged/fancymodloader/loader directory to check the - // reflective FMLLoader lookups against real jars. Those tests skip when it is not set. - (findProperty("fml.loader.libs") as String? ?: System.getenv("FML_LOADER_LIBS"))?.let { - systemProperty("fml.loader.libs", it) - } } tasks.processResources { diff --git a/src/main/java/i18nupdatemod/I18nUpdateMod.java b/src/main/java/i18nupdatemod/I18nUpdateMod.java index 6139897..67d82c3 100644 --- a/src/main/java/i18nupdatemod/I18nUpdateMod.java +++ b/src/main/java/i18nupdatemod/I18nUpdateMod.java @@ -52,11 +52,6 @@ public static void init(Path minecraftPath, String minecraftVersion, String load } modDomainsSet.remove("i18nupdatemod"); - int minecraftMajorVersion = getMinecraftMajorVersion(minecraftVersion); - if (minecraftMajorVersion >= 26) { - // The 26.1 pack supplies its CJK font in the vanilla resource domain. - modDomainsSet.add("minecraft"); - } Log.info(String.format("I18nUpdate Mod %s is loaded in %s with %s", MOD_VERSION, minecraftVersion, loader)); Log.debug(String.format("Minecraft path: %s", minecraftPath)); @@ -78,6 +73,8 @@ public static void init(Path minecraftPath, String minecraftVersion, String load FileUtil.setResourcePackDirPath(minecraftPath.resolve("resourcepacks")); + int minecraftMajorVersion = getMinecraftMajorVersion(minecraftVersion); + try { //Get asset GameAssetDetail assets = I18nConfig.getAssetDetail(minecraftVersion, loader); diff --git a/src/main/java/i18nupdatemod/core/ResourcePackConverter.java b/src/main/java/i18nupdatemod/core/ResourcePackConverter.java index 6ed815b..ada12be 100644 --- a/src/main/java/i18nupdatemod/core/ResourcePackConverter.java +++ b/src/main/java/i18nupdatemod/core/ResourcePackConverter.java @@ -46,10 +46,8 @@ public void convert(GameMetaData metaData, String description, HashSet m ZipEntry ze = e.nextElement(); String name = ze.getName(); String[] parts = name.split("/"); - // Keep only installed mod domains under assets/; top-level pack files - // and vanilla assets are retained. - if (parts.length >= 3 && "assets".equals(parts[0]) - && !modDomainsSet.contains(parts[1])) { + // 正在筛选的是assets/modDomain/** && 当前的modDomain不需要 + if (parts.length >= 2 && !modDomainsSet.contains(parts[1])) { continue; } diff --git a/src/main/java/i18nupdatemod/modlauncher/ModLauncherService.java b/src/main/java/i18nupdatemod/modlauncher/ModLauncherService.java index f28b3ad..523c6a1 100644 --- a/src/main/java/i18nupdatemod/modlauncher/ModLauncherService.java +++ b/src/main/java/i18nupdatemod/modlauncher/ModLauncherService.java @@ -46,9 +46,7 @@ public void initialize(IEnvironment environment) { Log.warning("Minecraft version not found"); return; } - String loader = isNeoForge() ? "NeoForge" : "Forge"; - I18nUpdateMod.init(minecraftPath.get(), minecraftVersion, loader, - ModUtil.getModDomainsFromModsFolder(minecraftPath.get(), minecraftVersion, loader)); + I18nUpdateMod.init(minecraftPath.get(), minecraftVersion, "Forge", ModUtil.getModDomainsFromModsFolder(minecraftPath.get(), minecraftVersion, "Forge")); } @Override @@ -102,10 +100,6 @@ private String getMinecraftVersion() { return null; } - private boolean isNeoForge() { - return loadClass("net.neoforged.fml.loading.FMLLoader") != null; - } - /** * @return the class, or null when it is absent or cannot be loaded */ diff --git a/src/test/java/i18nupdatemod/modlauncher/ModLauncherServiceTest.java b/src/test/java/i18nupdatemod/modlauncher/ModLauncherServiceTest.java index a593e73..67a8ddd 100644 --- a/src/test/java/i18nupdatemod/modlauncher/ModLauncherServiceTest.java +++ b/src/test/java/i18nupdatemod/modlauncher/ModLauncherServiceTest.java @@ -10,7 +10,7 @@ /** * Forge still boots through ModLauncher on Minecraft 26.1, so this service stays the Forge - * entrypoint there. Two things changed on that version and are pinned here. + * entrypoint there. What changed on that version is pinned here. */ class ModLauncherServiceTest { /** @@ -24,15 +24,6 @@ void reportsNoVersionOutsideForgeInsteadOfThrowing() throws Exception { assertNull(invoke("getMinecraftVersion")); } - /** - * The loader label picks the Forge or NeoForge pack, so it must not report NeoForge when the - * NeoForge FMLLoader is absent. - */ - @Test - void doesNotClaimNeoForgeWhenItIsAbsent() throws Exception { - assertFalse((Boolean) invoke("isNeoForge")); - } - /** * FMLLoader is only needed as a resource anchor. Initializing it drags in the loader's own * runtime, and on Forge 26.1 those classes are compiled for a newer Java than older launchers diff --git a/src/test/java/i18nupdatemod/neoforge/FmlApiShapeTest.java b/src/test/java/i18nupdatemod/neoforge/FmlApiShapeTest.java deleted file mode 100644 index a41e805..0000000 --- a/src/test/java/i18nupdatemod/neoforge/FmlApiShapeTest.java +++ /dev/null @@ -1,158 +0,0 @@ -package i18nupdatemod.neoforge; - -import org.junit.jupiter.api.Test; - -import java.io.DataInputStream; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashSet; -import java.util.Set; -import java.util.zip.ZipFile; - -import static org.junit.jupiter.api.Assumptions.assumeTrue; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Checks the reflective FMLLoader lookups in {@link NeoForgeMod} against real FancyModLoader jars. - *

- * The build has no NeoForge dependency (the API is compiled against a shim and excluded from the - * jar), so nothing else catches a renamed method. Skipped when the jars are not present. - */ -class FmlApiShapeTest { - /** - * Maven-layout directory holding {@code /loader-.jar}, e.g. the - * {@code net/neoforged/fancymodloader/loader} folder of any launcher's library cache. Set - * {@code -Dfml.loader.libs=...} or the {@code FML_LOADER_LIBS} environment variable to run these - * checks; without it there is nothing to verify against and both tests skip. - */ - private static final String LIB_PROPERTY = "fml.loader.libs"; - - private static Path libraryDir() { - String configured = System.getProperty(LIB_PROPERTY, System.getenv("FML_LOADER_LIBS")); - return configured == null || configured.isEmpty() ? null : Paths.get(configured); - } - - @Test - void fml11ExposesTheInstanceApiTheEntrypointUses() throws Exception { - Set methods = methodsOf("11.0.15"); - assumeTrue(!methods.isEmpty(), "set -D" + LIB_PROPERTY + " to a library dir containing FML 11"); - - assertTrue(methods.contains("getCurrent"), methods.toString()); - assertTrue(methods.contains("getGameDir"), methods.toString()); - assertTrue(methods.contains("getVersionInfo"), methods.toString()); - // The pre-26.1 statics are gone here, which is why the fallback exists. - assertTrue(!methods.contains("getGamePath")); - assertTrue(!methods.contains("versionInfo")); - } - - @Test - void fml4ExposesTheStaticApiTheFallbackUses() throws Exception { - Set methods = methodsOf("4.0.39"); - assumeTrue(!methods.isEmpty(), "set -D" + LIB_PROPERTY + " to a library dir containing FML 4"); - - assertTrue(methods.contains("getGamePath"), methods.toString()); - assertTrue(methods.contains("versionInfo"), methods.toString()); - // getCurrent() does not exist yet, so the primary lookup must be allowed to fail. - assertTrue(!methods.contains("getCurrent")); - } - - /** - * @return declared method names of FMLLoader in the given loader version, empty if absent - */ - private static Set methodsOf(String version) throws Exception { - Path lib = libraryDir(); - if (lib == null) { - return new HashSet<>(); - } - Path jar = lib.resolve(version).resolve("loader-" + version + ".jar"); - if (!Files.exists(jar)) { - return new HashSet<>(); - } - Set methods = new HashSet<>(); - try (ZipFile zf = new ZipFile(jar.toFile())) { - java.util.zip.ZipEntry entry = zf.getEntry("net/neoforged/fml/loading/FMLLoader.class"); - if (entry == null) { - return methods; - } - try (DataInputStream input = new DataInputStream(zf.getInputStream(entry))) { - methods.addAll(readMethodNames(input)); - } - } - return methods; - } - - /** - * Minimal class file reader. ASM 9.7 rejects the class file version FML 11 is built with, so the - * constant pool and method table are walked by hand. - * - * @see JVMS §4 - */ - private static Set readMethodNames(DataInputStream in) throws Exception { - in.readInt(); // magic - in.readShort(); // minor version - in.readShort(); // major version - - int constantPoolCount = in.readUnsignedShort(); - String[] utf8 = new String[constantPoolCount]; - for (int i = 1; i < constantPoolCount; i++) { - int tag = in.readUnsignedByte(); - switch (tag) { - case 1: // Utf8 - utf8[i] = in.readUTF(); - break; - case 7: // Class - case 8: // String - case 16: // MethodType - case 19: // Module - case 20: // Package - in.skipBytes(2); - break; - case 15: // MethodHandle - in.skipBytes(3); - break; - case 5: // Long - case 6: // Double - in.skipBytes(8); - i++; // these take two constant pool slots - break; - default: // Integer, Float, refs, InvokeDynamic, ... - in.skipBytes(4); - } - } - - in.readShort(); // access flags - in.readShort(); // this class - in.readShort(); // super class - in.skipBytes(in.readUnsignedShort() * 2); // interfaces - skipMembers(in); // fields - - Set methods = new HashSet<>(); - int methodCount = in.readUnsignedShort(); - for (int i = 0; i < methodCount; i++) { - in.readShort(); // access flags - methods.add(utf8[in.readUnsignedShort()]); - in.readShort(); // descriptor - skipAttributes(in); - } - return methods; - } - - private static void skipMembers(DataInputStream in) throws Exception { - int count = in.readUnsignedShort(); - for (int i = 0; i < count; i++) { - in.skipBytes(6); // access flags, name, descriptor - skipAttributes(in); - } - } - - private static void skipAttributes(DataInputStream in) throws Exception { - int count = in.readUnsignedShort(); - for (int i = 0; i < count; i++) { - in.readShort(); // name index - int length = in.readInt(); - in.skipBytes(length); - } - } -}