diff --git a/lophine-server/minecraft-patches/features/0013-I18n-support.patch b/lophine-server/minecraft-patches/features/0013-I18n-support.patch new file mode 100644 index 0000000..1fca81f --- /dev/null +++ b/lophine-server/minecraft-patches/features/0013-I18n-support.patch @@ -0,0 +1,80 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Thu, 12 Jun 2025 04:44:24 +0800 +Subject: [PATCH] I18n support + + +diff --git a/net/minecraft/locale/Language.java b/net/minecraft/locale/Language.java +index 7b9e2a1a208b46a69c16e6afd8b502259893574f..cdd26bbc6f6ff5510347049706e0f6f344fe79b1 100644 +--- a/net/minecraft/locale/Language.java ++++ b/net/minecraft/locale/Language.java +@@ -31,11 +31,27 @@ public abstract class Language { + public static final String DEFAULT = "en_us"; + private static volatile Language instance = loadDefault(); + ++ // Lophine start - I18n support + private static Language loadDefault() { ++ return load("/assets/minecraft/lang/" + DEFAULT + ".json"); ++ } ++ ++ public static void loadLocalLang(String lang) { ++ try { ++ inject(load(lang)); ++ } catch (Exception e) { ++ LOGGER.error("Failed to load language file for {}", me.earthme.lophine.config.modules.optimizations.LanguageConfig.lang, e); ++ inject(loadDefault()); ++ } ++ } ++ ++ // Lophine end - I18n support ++ ++ private static Language load(String lang) { // Lophine - I18n support + DeprecatedTranslationsInfo deprecatedTranslationsInfo = DeprecatedTranslationsInfo.loadFromDefaultResource(); + Map map = new HashMap<>(); + BiConsumer biConsumer = map::put; +- parseTranslations(biConsumer, "/assets/minecraft/lang/en_us.json"); ++ parseTranslations(biConsumer, lang); // Lophine - I18n support + deprecatedTranslationsInfo.applyToMap(map); + final Map map1 = Map.copyOf(map); + return new Language() { +@@ -65,13 +81,26 @@ public abstract class Language { + }; + } + ++ // Lophine start - I18n support + private static void parseTranslations(BiConsumer output, String languagePath) { + try (InputStream resourceAsStream = Language.class.getResourceAsStream(languagePath)) { + loadFromJson(resourceAsStream, output); +- } catch (JsonParseException | IOException var7) { +- LOGGER.error("Couldn't read strings from {}", languagePath, var7); ++ } catch (Exception e) { ++ try { ++ java.nio.file.Path filePath = java.nio.file.Paths.get(languagePath); ++ try (InputStream fileStream = java.nio.file.Files.newInputStream(filePath)) { ++ loadFromJson(fileStream, output); ++ } catch (java.nio.file.NoSuchFileException fileEx) { ++ LOGGER.error("Language file not found in both locations: {}", languagePath); ++ } catch (Exception fileEx) { ++ LOGGER.error("Failed to load from filesystem {}", filePath, fileEx); ++ } ++ } catch (Exception ep) { ++ LOGGER.error("Couldn't read strings from {}", languagePath, ep); ++ } + } + } ++ // Lophine end - I18n support + + public static void loadFromJson(InputStream stream, BiConsumer output) { + JsonObject jsonObject = GSON.fromJson(new InputStreamReader(stream, StandardCharsets.UTF_8), JsonObject.class); +diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java +index ba0cdba050cd3bf9231e15dd9543d34f37d9dae5..ce7d70afa524e5b8ea6f9de75d34c8f1eaa6ad72 100644 +--- a/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/net/minecraft/server/dedicated/DedicatedServer.java +@@ -172,6 +172,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess()); + // Paper end - initialize global and world-defaults configuration + me.earthme.lophine.config.LophineConfig.loadConfigFiles(); //Luminol - load config file // Lophine - load config file ++ me.earthme.lophine.utils.LocalLangUtil.init(); // Lophine - I18n support + if (false) this.server.spark.enableEarlyIfRequested(); // Paper - spark // Luminol - Force disable builtin spark + // Paper start - fix converting txt to json file; convert old users earlier after PlayerList creation but before file load/save + if (this.convertOldUsers()) { diff --git a/lophine-server/paper-patches/files/src/main/java/me/earthme/lophine/config/modules/optimizations/LanguageConfig.java.patch b/lophine-server/paper-patches/files/src/main/java/me/earthme/lophine/config/modules/optimizations/LanguageConfig.java.patch new file mode 100644 index 0000000..87dc8b2 --- /dev/null +++ b/lophine-server/paper-patches/files/src/main/java/me/earthme/lophine/config/modules/optimizations/LanguageConfig.java.patch @@ -0,0 +1,25 @@ +--- /dev/null ++++ b/src/main/java/me/earthme/lophine/config/modules/optimizations/LanguageConfig.java +@@ -1,0 +_,22 @@ ++package me.earthme.lophine.config.modules.optimizations; ++ ++import me.earthme.luminol.config.EnumConfigCategory; ++import me.earthme.luminol.config.IConfigModule; ++import me.earthme.luminol.config.flags.ConfigInfo; ++ ++public class LanguageConfig implements IConfigModule { ++ @ConfigInfo(baseName = "lang" ,comments = """ ++ Please use the key from https://minecraft.wiki/w/Language ++ Sample of format: en_us zh_cn zh_hk zh_tw""") ++ public static String lang = "en_us"; ++ ++ @Override ++ public EnumConfigCategory getCategory() { ++ return EnumConfigCategory.OPTIMIZATIONS; ++ } ++ ++ @Override ++ public String getBaseName() { ++ return "language"; ++ } ++} diff --git a/lophine-server/paper-patches/files/src/main/java/me/earthme/lophine/utils/LocalLangUtil.java.patch b/lophine-server/paper-patches/files/src/main/java/me/earthme/lophine/utils/LocalLangUtil.java.patch new file mode 100644 index 0000000..ee643db --- /dev/null +++ b/lophine-server/paper-patches/files/src/main/java/me/earthme/lophine/utils/LocalLangUtil.java.patch @@ -0,0 +1,159 @@ +--- /dev/null ++++ b/src/main/java/me/earthme/lophine/utils/LocalLangUtil.java +@@ -1,0 +_,156 @@ ++package me.earthme.lophine.utils; ++ ++import com.google.gson.JsonElement; ++import com.google.gson.JsonObject; ++import com.google.gson.JsonParser; ++ ++import java.io.ByteArrayInputStream; ++import java.io.IOException; ++import java.io.InputStream; ++import java.io.InputStreamReader; ++import java.net.HttpURLConnection; ++import java.net.URL; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.nio.file.Paths; ++import java.util.Objects; ++import java.util.concurrent.CompletableFuture; ++import java.util.logging.Logger; ++ ++import static me.earthme.lophine.config.modules.optimizations.LanguageConfig.lang; ++ ++public class LocalLangUtil { ++ static Logger logger = Logger.getLogger("LangLoader"); ++ static String VERSION = "1.21.5"; ++ static String basePath = "cache/lophine/" + VERSION + "/"; ++ ++ public static void init() { ++ if (Objects.equals(lang, "en_us")) return; ++ CompletableFuture.runAsync(() -> { ++ try { ++ downloadLangAndCheck(); ++ net.minecraft.locale.Language.loadLocalLang(basePath + "lang/" + lang + ".json"); ++ } catch (Exception e) { ++ logger.severe(() -> "Async initialization failed: " + e.getMessage()); ++ } ++ }); ++ } ++ ++ private static void downloadLangAndCheck() { ++ Path path = Paths.get(basePath + VERSION + ".json"); ++ JsonObject json; ++ if (Files.exists(path)) { ++ try { ++ json = loadJson(path.toString()); ++ } catch (Exception e) { ++ logger.warning("Failed to load local JSON: " + e.getMessage()); ++ json = download(); ++ } ++ } else { ++ json = download(); ++ } ++ ++ if (json == null) { ++ logger.warning("Failed to load language metadata"); ++ return; ++ } ++ ++ try { ++ JsonObject assetIndex = json.getAsJsonObject("assetIndex"); ++ String assetUrl = assetIndex.get("url").getAsString(); ++ byte[] assetData = fetchAndSave(assetUrl, basePath + "resource.json"); ++ ++ JsonObject assets = JsonParser.parseString(new String(assetData)).getAsJsonObject(); ++ JsonObject langEntry = assets.getAsJsonObject("objects") ++ .getAsJsonObject("minecraft/lang/" + lang + ".json"); ++ ++ String hash = langEntry.get("hash").getAsString(); ++ if (hash == null || hash.length() < 2) { ++ throw new IllegalArgumentException("Invalid hash value"); ++ } ++ ++ downloadLang(hash); ++ } catch (Exception e) { ++ logger.warning("Asset processing failed: " + e.getMessage()); ++ } ++ } ++ ++ private static void downloadLang(String hash) { ++ String url = "https://resources.download.minecraft.net/" ++ + hash.substring(0, 2) + "/" + hash; ++ ++ for (int i = 0; i < 3; i++) { ++ try { ++ fetchAndSave(url, basePath + "lang/" + lang + ".json"); ++ return; ++ } catch (Exception e) { ++ if (i == 2) logger.warning("Final attempt failed: " + e.getMessage()); ++ } ++ } ++ } ++ ++ private static JsonObject download() { ++ String versionManifestUrl = "https://launchermeta.mojang.com/mc/game/version_manifest.json"; ++ String targetVersionUrl = null; ++ ++ // Phase 1: Fetch version manifest ++ for (int i = 0; i < 3; i++) { ++ try (InputStreamReader reader = new InputStreamReader( ++ new ByteArrayInputStream(fetch(versionManifestUrl)))) { ++ JsonObject manifest = JsonParser.parseReader(reader).getAsJsonObject(); ++ for (JsonElement element : manifest.getAsJsonArray("versions")) { ++ JsonObject version = element.getAsJsonObject(); ++ if (VERSION.equals(version.get("id").getAsString())) { ++ targetVersionUrl = version.get("url").getAsString(); ++ break; ++ } ++ } ++ if (targetVersionUrl != null) break; ++ } catch (Exception e) { ++ logger.warning("Failed to fetch version manifest: " + e.getMessage()); ++ } ++ } ++ ++ if (targetVersionUrl == null) return null; ++ ++ // Phase 2: Fetch version metadata ++ for (int i = 0; i < 3; i++) { ++ try (InputStreamReader reader = new InputStreamReader( ++ new ByteArrayInputStream(fetchAndSave(targetVersionUrl, basePath + VERSION + ".json")))) { ++ return JsonParser.parseReader(reader).getAsJsonObject(); ++ } catch (Exception e) { ++ logger.warning("Failed to fetch version metadata: " + e.getMessage()); ++ } ++ } ++ return null; ++ } ++ ++ public static byte[] fetch(String urlString) throws IOException { ++ HttpURLConnection conn = (HttpURLConnection) new URL(urlString).openConnection(); ++ conn.setRequestMethod("GET"); ++ conn.setConnectTimeout(5000); ++ conn.setReadTimeout(10000); ++ ++ try (InputStream stream = conn.getInputStream()) { ++ if (conn.getResponseCode() != 200) { ++ throw new IOException("HTTP error: " + conn.getResponseCode()); ++ } ++ return stream.readAllBytes(); ++ } finally { ++ conn.disconnect(); ++ } ++ } ++ ++ public static byte[] fetchAndSave(String url, String savePath) throws IOException { ++ byte[] data = fetch(url); ++ Path outputPath = Paths.get(savePath); ++ Files.createDirectories(outputPath.getParent()); ++ Files.write(outputPath, data); ++ return data; ++ } ++ ++ public static JsonObject loadJson(String path) throws IOException { ++ byte[] data = Files.readAllBytes(Paths.get(path)); ++ return JsonParser.parseString(new String(data)).getAsJsonObject(); ++ } ++}