From 4be5cef3323d889d0ff2bcdb371f0d39a9758bb8 Mon Sep 17 00:00:00 2001 From: missinglove <0xwymx0@gmail.com> Date: Sat, 27 Jun 2026 22:35:23 +0800 Subject: [PATCH 01/12] Support extracting all languages --- README.md | 2 +- .../kotlin/de/snowii/extractor/Extractor.kt | 7 ++- .../extractors/non_registry/Translations.kt | 57 +++++++++++++++---- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 99cfb32..f78869e 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Extractor is a Fabric mod that extracts Minecraft data (blocks, items, entities, - [x] Chunk Status - [x] Game Event - [x] Game Rule -- [x] Translation (en_us) +- [x] Translations (all languages) - [x] Noise Parameters - [x] Particles - [x] Recipes diff --git a/src/main/kotlin/de/snowii/extractor/Extractor.kt b/src/main/kotlin/de/snowii/extractor/Extractor.kt index e56fd4c..b149303 100644 --- a/src/main/kotlin/de/snowii/extractor/Extractor.kt +++ b/src/main/kotlin/de/snowii/extractor/Extractor.kt @@ -24,6 +24,10 @@ class Extractor : ModInitializer { private val modID: String = "pumpkin_extractor" private val logger: Logger = LoggerFactory.getLogger(modID) + companion object { + const val OUTPUT_DIR: String = "pumpkin_extractor_output" + } + override fun onInitialize() { logger.info("Starting Pumpkin Extractor") @@ -172,7 +176,7 @@ class Extractor : ModInitializer { val outputDirectory: Path try { - outputDirectory = Files.createDirectories(Paths.get("pumpkin_extractor_output")) + outputDirectory = Files.createDirectories(Paths.get(OUTPUT_DIR)) } catch (e: IOException) { logger.info("Failed to create output directory.", e) return @@ -185,6 +189,7 @@ class Extractor : ModInitializer { for (ext in extractors) { try { val out = outputDirectory.resolve(ext.fileName()) + Files.createDirectories(out.parent) val fileWriter = FileWriter(out.toFile(), StandardCharsets.UTF_8) gson.toJson(ext.extract(server), fileWriter) fileWriter.close() diff --git a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt index 6c61051..b24136f 100644 --- a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt +++ b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt @@ -2,29 +2,66 @@ package de.snowii.extractor.extractors.non_registry import com.google.gson.Gson import com.google.gson.GsonBuilder +import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject import de.snowii.extractor.Extractor +import net.minecraft.resources.Identifier import net.minecraft.server.MinecraftServer +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.FileWriter import java.io.InputStreamReader import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Paths class Translations : Extractor.Extractor { private val gson: Gson = GsonBuilder().disableHtmlEscaping().create() + private val logger: Logger = LoggerFactory.getLogger("translations_extractor") override fun fileName(): String { - return "en_us.json" + return "translations.json" } override fun extract(server: MinecraftServer): JsonElement { - val inputStream = this.javaClass.getResourceAsStream("/assets/minecraft/lang/en_us.json") - ?: throw IllegalArgumentException("Could not find lang en_us.json") - - return inputStream.use { stream -> - gson.fromJson( - InputStreamReader(stream, StandardCharsets.UTF_8), - JsonObject::class.java - ) as JsonObject + val langDir = Paths.get(Extractor.OUTPUT_DIR, "lang") + Files.createDirectories(langDir) + + val resourceManager = server.resourceManager + val summary = JsonArray() + + // Scan for all language JSON files across all namespaces + val langResources = resourceManager.listResources("lang") { identifier: Identifier -> + identifier.path.endsWith(".json") + } + + for ((identifier, resource) in langResources) { + val langCode = identifier.path + .removePrefix("lang/") + .removeSuffix(".json") + + try { + val langFile = langDir.resolve("$langCode.json") + resource.open().use { stream -> + val translations = gson.fromJson( + InputStreamReader(stream, StandardCharsets.UTF_8), + JsonObject::class.java + ) as JsonObject + + FileWriter(langFile.toFile(), StandardCharsets.UTF_8).use { writer -> + gson.toJson(translations, writer) + } + } + summary.add(langCode) + logger.info("Wrote language: $langCode") + } catch (e: Exception) { + logger.warn("Failed to load language: $langCode", e) + } } + + logger.info("Extracted ${summary.size()} languages to ${langDir.toAbsolutePath()}") + + return summary } -} \ No newline at end of file +} From 415abd37f0344dfbd01dc0fe5bafe1f52cb26400 Mon Sep 17 00:00:00 2001 From: missinglove <0xwymx0@gmail.com> Date: Sat, 27 Jun 2026 22:39:55 +0800 Subject: [PATCH 02/12] actions --- .github/workflows/build.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1f9810c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,33 @@ +name: Build + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup JDK 25 + uses: actions/setup-java@v4 + with: + java-version: '25' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: ${{ github.event_name == 'pull_request' }} + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build + run: ./gradlew build From c8fb48fe9f74309fde4b771eade712b1b375d0c6 Mon Sep 17 00:00:00 2001 From: missinglove <0xwymx0@gmail.com> Date: Sat, 27 Jun 2026 22:49:42 +0800 Subject: [PATCH 03/12] actions --- .github/workflows/build.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1f9810c..168e0ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,3 +31,10 @@ jobs: - name: Build run: ./gradlew build + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: extractor-build + path: build/libs/*.jar + if-no-files-found: error From d3533f29498c58576d26b4846628288dcd3112f6 Mon Sep 17 00:00:00 2001 From: missinglove <0xwymx0@gmail.com> Date: Sat, 27 Jun 2026 23:26:03 +0800 Subject: [PATCH 04/12] fix --- .../kotlin/de/snowii/extractor/Extractor.kt | 30 +++++--- .../extractors/non_registry/Translations.kt | 74 +++++++++++++++---- 2 files changed, 79 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/de/snowii/extractor/Extractor.kt b/src/main/kotlin/de/snowii/extractor/Extractor.kt index b149303..1bdbe78 100644 --- a/src/main/kotlin/de/snowii/extractor/Extractor.kt +++ b/src/main/kotlin/de/snowii/extractor/Extractor.kt @@ -177,6 +177,7 @@ class Extractor : ModInitializer { val outputDirectory: Path try { outputDirectory = Files.createDirectories(Paths.get(OUTPUT_DIR)) + logger.info("Output directory: {}", outputDirectory.toAbsolutePath()) } catch (e: IOException) { logger.info("Failed to create output directory.", e) return @@ -185,21 +186,26 @@ class Extractor : ModInitializer { val gson = GsonBuilder().disableHtmlEscaping().create() ServerLifecycleEvents.SERVER_STARTED.register(ServerLifecycleEvents.ServerStarted { server: MinecraftServer -> - val timeInMillis = measureTimeMillis { - for (ext in extractors) { - try { - val out = outputDirectory.resolve(ext.fileName()) - Files.createDirectories(out.parent) - val fileWriter = FileWriter(out.toFile(), StandardCharsets.UTF_8) - gson.toJson(ext.extract(server), fileWriter) - fileWriter.close() - logger.info("Wrote " + out.toAbsolutePath()) - } catch (e: java.lang.Exception) { - logger.error(("Extractor for \"" + ext.fileName()) + "\" failed.", e) + logger.info("Server started — running extractors...") + try { + val timeInMillis = measureTimeMillis { + for (ext in extractors) { + try { + val out = outputDirectory.resolve(ext.fileName()) + Files.createDirectories(out.parent) + val fileWriter = FileWriter(out.toFile(), StandardCharsets.UTF_8) + gson.toJson(ext.extract(server), fileWriter) + fileWriter.close() + logger.info("Wrote " + out.toAbsolutePath()) + } catch (e: java.lang.Exception) { + logger.error(("Extractor for \"" + ext.fileName()) + "\" failed.", e) + } } } + logger.info("Done, took ${timeInMillis}ms") + } catch (e: Throwable) { + logger.error("Extraction failed with fatal error", e) } - logger.info("Done, took ${timeInMillis}ms") }) } diff --git a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt index b24136f..b0dddd0 100644 --- a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt +++ b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt @@ -6,15 +6,17 @@ import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject import de.snowii.extractor.Extractor -import net.minecraft.resources.Identifier import net.minecraft.server.MinecraftServer import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.io.File import java.io.FileWriter import java.io.InputStreamReader +import java.net.URL import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Paths +import java.util.jar.JarFile class Translations : Extractor.Extractor { private val gson: Gson = GsonBuilder().disableHtmlEscaping().create() @@ -28,27 +30,25 @@ class Translations : Extractor.Extractor { val langDir = Paths.get(Extractor.OUTPUT_DIR, "lang") Files.createDirectories(langDir) - val resourceManager = server.resourceManager val summary = JsonArray() + val langCodes = findLangCodesInClasspath() - // Scan for all language JSON files across all namespaces - val langResources = resourceManager.listResources("lang") { identifier: Identifier -> - identifier.path.endsWith(".json") - } - - for ((identifier, resource) in langResources) { - val langCode = identifier.path - .removePrefix("lang/") - .removeSuffix(".json") + for (langCode in langCodes) { + val resourcePath = "/assets/minecraft/lang/$langCode.json" + val inputStream = this.javaClass.getResourceAsStream(resourcePath) + if (inputStream == null) { + logger.warn("Could not find lang file: $resourcePath") + continue + } try { - val langFile = langDir.resolve("$langCode.json") - resource.open().use { stream -> + inputStream.use { stream -> val translations = gson.fromJson( InputStreamReader(stream, StandardCharsets.UTF_8), JsonObject::class.java ) as JsonObject + val langFile = langDir.resolve("${langCode}_java.json") FileWriter(langFile.toFile(), StandardCharsets.UTF_8).use { writer -> gson.toJson(translations, writer) } @@ -64,4 +64,52 @@ class Translations : Extractor.Extractor { return summary } + + /** + * Scans the classpath for all language JSON files under /assets/minecraft/lang/. + * Handles both JAR-packaged and file-system classpath layouts. + */ + private fun findLangCodesInClasspath(): List { + val codes = mutableListOf() + val langDirUrl: URL = this.javaClass.getResource("/assets/minecraft/lang/") + ?: return codes + + when (langDirUrl.protocol) { + "jar" -> { + // Running from a JAR (e.g. Minecraft client/server jar) + val jarUrlStr = langDirUrl.path.substringBefore("!") + val entryPrefix = langDirUrl.path.substringAfter("!").removePrefix("/") + val jarFile = JarFile(File(URL(jarUrlStr).toURI())) + jarFile.use { jar -> + val entries = jar.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + val name = entry.name + if (!entry.isDirectory + && name.startsWith(entryPrefix) + && name.endsWith(".json") + ) { + val filename = name.substringAfterLast("/") + codes.add(filename.removeSuffix(".json")) + } + } + } + } + "file" -> { + // Running from a file system (development environment) + val langDir = File(langDirUrl.toURI()) + langDir.listFiles()?.forEach { file -> + if (file.isFile && file.name.endsWith(".json")) { + codes.add(file.name.removeSuffix(".json")) + } + } + } + else -> { + logger.warn("Unsupported classpath protocol: ${langDirUrl.protocol}") + } + } + + logger.info("Found ${codes.size} language(s) in classpath") + return codes + } } From ad314def4b546e9170e8e791b025a53a1b486444 Mon Sep 17 00:00:00 2001 From: missinglove <0xwymx0@gmail.com> Date: Sat, 27 Jun 2026 23:33:50 +0800 Subject: [PATCH 05/12] fix --- .../extractors/non_registry/Translations.kt | 81 +++++++++++-------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt index b0dddd0..516aa38 100644 --- a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt +++ b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt @@ -12,11 +12,12 @@ import org.slf4j.LoggerFactory import java.io.File import java.io.FileWriter import java.io.InputStreamReader +import java.net.JarURLConnection import java.net.URL +import java.util.Enumeration import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Paths -import java.util.jar.JarFile class Translations : Extractor.Extractor { private val gson: Gson = GsonBuilder().disableHtmlEscaping().create() @@ -67,49 +68,61 @@ class Translations : Extractor.Extractor { /** * Scans the classpath for all language JSON files under /assets/minecraft/lang/. - * Handles both JAR-packaged and file-system classpath layouts. + * Uses getResources() to find ALL matching locations across the classpath, + * then scans each one via JarURLConnection (JAR) or File (filesystem). */ private fun findLangCodesInClasspath(): List { - val codes = mutableListOf() - val langDirUrl: URL = this.javaClass.getResource("/assets/minecraft/lang/") - ?: return codes + val codes = mutableSetOf() + val classLoader = this.javaClass.classLoader + val urls: Enumeration = classLoader.getResources("assets/minecraft/lang/") - when (langDirUrl.protocol) { - "jar" -> { - // Running from a JAR (e.g. Minecraft client/server jar) - val jarUrlStr = langDirUrl.path.substringBefore("!") - val entryPrefix = langDirUrl.path.substringAfter("!").removePrefix("/") - val jarFile = JarFile(File(URL(jarUrlStr).toURI())) - jarFile.use { jar -> - val entries = jar.entries() - while (entries.hasMoreElements()) { - val entry = entries.nextElement() - val name = entry.name - if (!entry.isDirectory - && name.startsWith(entryPrefix) - && name.endsWith(".json") - ) { - val filename = name.substringAfterLast("/") - codes.add(filename.removeSuffix(".json")) + while (urls.hasMoreElements()) { + val url = urls.nextElement() + logger.info("Scanning: $url (protocol=${url.protocol})") + + when (url.protocol) { + "jar" -> { + val conn = url.openConnection() + if (conn is JarURLConnection) { + val jarFile = conn.jarFile + val entryPrefix = conn.entryName ?: "" + logger.info(" JAR: ${jarFile.name} (prefix: $entryPrefix)") + + jarFile.use { jar -> + val entries = jar.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + val name = entry.name + if (!entry.isDirectory + && name.startsWith(entryPrefix) + && name.endsWith(".json") + ) { + val filename = name.substringAfterLast("/") + codes.add(filename.removeSuffix(".json")) + } + } } + } else { + logger.warn(" Not a JarURLConnection: ${conn.javaClass.name}") } } - } - "file" -> { - // Running from a file system (development environment) - val langDir = File(langDirUrl.toURI()) - langDir.listFiles()?.forEach { file -> - if (file.isFile && file.name.endsWith(".json")) { - codes.add(file.name.removeSuffix(".json")) + "file" -> { + val dir = File(url.toURI()) + logger.info(" Directory: ${dir.absolutePath}") + dir.listFiles()?.forEach { file -> + if (file.isFile && file.name.endsWith(".json")) { + codes.add(file.name.removeSuffix(".json")) + } } } - } - else -> { - logger.warn("Unsupported classpath protocol: ${langDirUrl.protocol}") + else -> { + logger.warn(" Unsupported protocol: ${url.protocol}") + } } } - logger.info("Found ${codes.size} language(s) in classpath") - return codes + val sorted = codes.sorted() + logger.info("Found ${sorted.size} language(s) in classpath") + return sorted } } From d4b5ab8850bd6171136fbf13e3d0ff5f0dd96b2e Mon Sep 17 00:00:00 2001 From: missinglove <0xwymx0@gmail.com> Date: Sun, 28 Jun 2026 05:44:41 +0800 Subject: [PATCH 06/12] fix --- .../extractors/non_registry/Translations.kt | 117 ++++++++---------- 1 file changed, 55 insertions(+), 62 deletions(-) diff --git a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt index 516aa38..9cbd834 100644 --- a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt +++ b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt @@ -6,17 +6,16 @@ import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject import de.snowii.extractor.Extractor +import net.minecraft.resources.Identifier import net.minecraft.server.MinecraftServer import org.slf4j.Logger import org.slf4j.LoggerFactory -import java.io.File import java.io.FileWriter +import java.io.InputStream import java.io.InputStreamReader -import java.net.JarURLConnection -import java.net.URL -import java.util.Enumeration import java.nio.charset.StandardCharsets import java.nio.file.Files +import java.nio.file.Path import java.nio.file.Paths class Translations : Extractor.Extractor { @@ -30,15 +29,14 @@ class Translations : Extractor.Extractor { override fun extract(server: MinecraftServer): JsonElement { val langDir = Paths.get(Extractor.OUTPUT_DIR, "lang") Files.createDirectories(langDir) - val summary = JsonArray() - val langCodes = findLangCodesInClasspath() + + val langCodes = findLangCodes() for (langCode in langCodes) { - val resourcePath = "/assets/minecraft/lang/$langCode.json" - val inputStream = this.javaClass.getResourceAsStream(resourcePath) + val inputStream = loadLangFile(langCode) if (inputStream == null) { - logger.warn("Could not find lang file: $resourcePath") + logger.warn("Could not find lang file for: $langCode") continue } @@ -62,67 +60,62 @@ class Translations : Extractor.Extractor { } logger.info("Extracted ${summary.size()} languages to ${langDir.toAbsolutePath()}") - return summary } /** - * Scans the classpath for all language JSON files under /assets/minecraft/lang/. - * Uses getResources() to find ALL matching locations across the classpath, - * then scans each one via JarURLConnection (JAR) or File (filesystem). + * Discovers all available language codes from the client-side LanguageManager. + * Falls back to classpath scanning on dedicated servers. */ - private fun findLangCodesInClasspath(): List { - val codes = mutableSetOf() - val classLoader = this.javaClass.classLoader - val urls: Enumeration = classLoader.getResources("assets/minecraft/lang/") - - while (urls.hasMoreElements()) { - val url = urls.nextElement() - logger.info("Scanning: $url (protocol=${url.protocol})") - - when (url.protocol) { - "jar" -> { - val conn = url.openConnection() - if (conn is JarURLConnection) { - val jarFile = conn.jarFile - val entryPrefix = conn.entryName ?: "" - logger.info(" JAR: ${jarFile.name} (prefix: $entryPrefix)") + @Suppress("UNCHECKED_CAST") + private fun findLangCodes(): List { + try { + val minecraftClass = Class.forName("net.minecraft.client.Minecraft") + val client = minecraftClass.getMethod("getInstance").invoke(null) ?: return classpathFallback() + val languageManager = minecraftClass.getMethod("getLanguageManager").invoke(client) + val languages = languageManager.javaClass.getMethod("getLanguages").invoke(languageManager) as Map + if (languages.isNotEmpty()) { + val codes = languages.keys.sorted() + logger.info("Found ${codes.size} language(s) via client LanguageManager") + return codes + } + } catch (e: Exception) { + logger.info("Client LanguageManager not available, scanning classpath...") + } + return classpathFallback() + } - jarFile.use { jar -> - val entries = jar.entries() - while (entries.hasMoreElements()) { - val entry = entries.nextElement() - val name = entry.name - if (!entry.isDirectory - && name.startsWith(entryPrefix) - && name.endsWith(".json") - ) { - val filename = name.substringAfterLast("/") - codes.add(filename.removeSuffix(".json")) - } - } - } - } else { - logger.warn(" Not a JarURLConnection: ${conn.javaClass.name}") - } - } - "file" -> { - val dir = File(url.toURI()) - logger.info(" Directory: ${dir.absolutePath}") - dir.listFiles()?.forEach { file -> - if (file.isFile && file.name.endsWith(".json")) { - codes.add(file.name.removeSuffix(".json")) - } - } - } - else -> { - logger.warn(" Unsupported protocol: ${url.protocol}") - } + /** + * Loads a language file from the client ResourceManager (which has access to the + * full Minecraft assets at runtime), falling back to classpath for dedicated servers. + */ + private fun loadLangFile(langCode: String): InputStream? { + // Try client resource manager first (has full assets at runtime) + try { + val minecraftClass = Class.forName("net.minecraft.client.Minecraft") + val client = minecraftClass.getMethod("getInstance").invoke(null) ?: return classpathLoad(langCode) + val resourceManager = minecraftClass.getMethod("getResourceManager").invoke(client) + val identifier = Identifier.fromNamespaceAndPath("minecraft", "lang/$langCode.json") + val resources = resourceManager.javaClass + .getMethod("getResourceStack", Identifier::class.java) + .invoke(resourceManager, identifier) as List<*> + if (resources.isNotEmpty()) { + val resource = resources.first()!! + return resource.javaClass.getMethod("open").invoke(resource) as InputStream } + } catch (e: Exception) { + logger.debug("Client resource load failed for $langCode, trying classpath...") } - val sorted = codes.sorted() - logger.info("Found ${sorted.size} language(s) in classpath") - return sorted + // Fallback: direct classpath loading (dedicated server or dev environment) + return classpathLoad(langCode) + } + + private fun classpathLoad(langCode: String): InputStream? { + return this.javaClass.getResourceAsStream("/assets/minecraft/lang/$langCode.json") + } + + private fun classpathFallback(): List { + return listOf("en_us") } } From 5697b3ecf8092bc67a29df2f52724a9e01065f95 Mon Sep 17 00:00:00 2001 From: missinglove <0xwymx0@gmail.com> Date: Sun, 28 Jun 2026 06:17:39 +0800 Subject: [PATCH 07/12] Optimization --- .../kotlin/de/snowii/extractor/Extractor.kt | 176 +++--------------- .../extractors/non_registry/Translations.kt | 116 +++++------- 2 files changed, 73 insertions(+), 219 deletions(-) diff --git a/src/main/kotlin/de/snowii/extractor/Extractor.kt b/src/main/kotlin/de/snowii/extractor/Extractor.kt index 1bdbe78..d672e9f 100644 --- a/src/main/kotlin/de/snowii/extractor/Extractor.kt +++ b/src/main/kotlin/de/snowii/extractor/Extractor.kt @@ -9,7 +9,6 @@ import de.snowii.extractor.extractors.structures.Structures import net.fabricmc.api.ModInitializer import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents import net.minecraft.server.MinecraftServer -import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.FileWriter import java.io.IOException @@ -21,157 +20,32 @@ import kotlin.system.measureTimeMillis class Extractor : ModInitializer { - private val modID: String = "pumpkin_extractor" - private val logger: Logger = LoggerFactory.getLogger(modID) + private val logger = LoggerFactory.getLogger("pumpkin_extractor") companion object { - const val OUTPUT_DIR: String = "pumpkin_extractor_output" + const val OUTPUT_DIR = "pumpkin_extractor_output" } override fun onInitialize() { logger.info("Starting Pumpkin Extractor") val extractors = arrayOf( - Dialog(), - DialogActionType(), - DialogBodyType(), - DialogType(), - InputControlType(), - Advancement(), - Effect(), - SpawnEgg(), - PotionBrewing(), - Potion(), - Sounds(), - Recipes(), - Biome(), - BiomeMixerTest(), - WorldEvent(), - Carver(), - Enchantments(), - ScoreboardDisplaySlot(), - Dimension(), - Particles(), - EntityAttributes(), - ChunkStatus(), - EntityStatuses(), - MessageType(), - SoundCategory(), - EntityPose(), - GameEvent(), - GameRules(), - SyncedRegistries(), - ChunkGenSetting(), - Packets(), - Screens(), - PlacedFeatures(), - ConfiguredFeatures(), - Tags(), - JukeboxSong(), - MetaDataType(), - TrackedData(), - NoiseParameters(), - Structures(), - StructureSet(), - Entities(), - Items(), - DataComponent(), - Blocks(), - MultiNoise(), - MultiNoise().Sample(), - ChunkGenSetting(), - Translations(), - DensityFunctions(), - DensityFunctions().Tests(), - DamageTypes(), - Fluids(), - Properties(), - ComposterIncreaseChance(), - FlowerPotTransformation(), - Fuels(), - RecipeRemainder(), - VillagerData(), - CustomStats(), - Stats(), - SlotRanges(), - /* ChunkDumpTests.NoiseDump( - "no_blend_no_beard_0_0.chunk", - 0, - 0, - 0, - arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_7_4.chunk", - 0, - 7, - 4, - arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_only_cell_cache_0_0.chunk", - 0, - 0, - 0, - ArrayList() - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_only_cell_cache_flat_cache_0_0.chunk", - 0, - 0, - 0, - arrayListOf("FlatCache") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_only_cell_cache_interpolated_0_0.chunk", - 0, - 0, - 0, - arrayListOf("Interpolated") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_only_cell_cache_once_cache_0_0.chunk", - 0, - 0, - 0, - arrayListOf("CacheOnce") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_-595_544.chunk", - 0, - -595, - 544, - arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_-119_183.chunk", - 0, - -119, - 183, - arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_13579_-6_11.chunk", - 13579, - -6, - 11, - arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_13579_-2_15.chunk", - 13579, - -2, - 15, - arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") - ), - BiomeDumpTests(), - BiomeDumpTests().MultiNoiseBiomeSourceTest(), - ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_0_0.chunk", 0, 0, 0), - ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_badlands_-595_544.chunk", 0, -595, 544), - ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_frozen_ocean_-119_183.chunk", 0, -119, 183), - ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_13579_-6_11.chunk", 13579, -6, 11), - ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_13579_-2_15.chunk", 13579, -2, 15), - ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_13579_-7_9.chunk", 13579, -7, 9)*/ + Dialog(), DialogActionType(), DialogBodyType(), DialogType(), + InputControlType(), Advancement(), Effect(), SpawnEgg(), + PotionBrewing(), Potion(), Sounds(), Recipes(), + Biome(), BiomeMixerTest(), WorldEvent(), Carver(), + Enchantments(), ScoreboardDisplaySlot(), Dimension(), Particles(), + EntityAttributes(), ChunkStatus(), EntityStatuses(), MessageType(), + SoundCategory(), EntityPose(), GameEvent(), GameRules(), + SyncedRegistries(), ChunkGenSetting(), Packets(), Screens(), + PlacedFeatures(), ConfiguredFeatures(), Tags(), JukeboxSong(), + MetaDataType(), TrackedData(), NoiseParameters(), Structures(), + StructureSet(), Entities(), Items(), DataComponent(), Blocks(), + MultiNoise(), MultiNoise().Sample(), Translations(), + DensityFunctions(), DensityFunctions().Tests(), DamageTypes(), + Fluids(), Properties(), ComposterIncreaseChance(), + FlowerPotTransformation(), Fuels(), RecipeRemainder(), + VillagerData(), CustomStats(), Stats(), SlotRanges(), ) val outputDirectory: Path @@ -179,7 +53,7 @@ class Extractor : ModInitializer { outputDirectory = Files.createDirectories(Paths.get(OUTPUT_DIR)) logger.info("Output directory: {}", outputDirectory.toAbsolutePath()) } catch (e: IOException) { - logger.info("Failed to create output directory.", e) + logger.error("Failed to create output directory.", e) return } @@ -193,12 +67,12 @@ class Extractor : ModInitializer { try { val out = outputDirectory.resolve(ext.fileName()) Files.createDirectories(out.parent) - val fileWriter = FileWriter(out.toFile(), StandardCharsets.UTF_8) - gson.toJson(ext.extract(server), fileWriter) - fileWriter.close() - logger.info("Wrote " + out.toAbsolutePath()) - } catch (e: java.lang.Exception) { - logger.error(("Extractor for \"" + ext.fileName()) + "\" failed.", e) + FileWriter(out.toFile(), StandardCharsets.UTF_8).use { writer -> + gson.toJson(ext.extract(server), writer) + } + logger.info("Wrote ${out.toAbsolutePath()}") + } catch (e: Exception) { + logger.error("Extractor \"${ext.fileName()}\" failed.", e) } } } diff --git a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt index 9cbd834..f25cd0d 100644 --- a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt +++ b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt @@ -1,6 +1,5 @@ package de.snowii.extractor.extractors.non_registry -import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.JsonArray import com.google.gson.JsonElement @@ -8,54 +7,47 @@ import com.google.gson.JsonObject import de.snowii.extractor.Extractor import net.minecraft.resources.Identifier import net.minecraft.server.MinecraftServer -import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.FileWriter import java.io.InputStream import java.io.InputStreamReader import java.nio.charset.StandardCharsets import java.nio.file.Files -import java.nio.file.Path import java.nio.file.Paths class Translations : Extractor.Extractor { - private val gson: Gson = GsonBuilder().disableHtmlEscaping().create() - private val logger: Logger = LoggerFactory.getLogger("translations_extractor") + private val gson = GsonBuilder().disableHtmlEscaping().create() + private val logger = LoggerFactory.getLogger("translations_extractor") - override fun fileName(): String { - return "translations.json" - } + /** Lazily reflect into net.minecraft.client.Minecraft to avoid compile-time dependency. */ + private val clientReflection by lazy { ClientReflection.resolve() } + + override fun fileName() = "translations.json" override fun extract(server: MinecraftServer): JsonElement { val langDir = Paths.get(Extractor.OUTPUT_DIR, "lang") Files.createDirectories(langDir) val summary = JsonArray() - val langCodes = findLangCodes() - - for (langCode in langCodes) { - val inputStream = loadLangFile(langCode) - if (inputStream == null) { + for (langCode in clientReflection?.languageCodes ?: listOf("en_us")) { + val stream = loadLangStream(langCode) ?: run { logger.warn("Could not find lang file for: $langCode") continue } - try { - inputStream.use { stream -> + stream.use { s -> val translations = gson.fromJson( - InputStreamReader(stream, StandardCharsets.UTF_8), + InputStreamReader(s, StandardCharsets.UTF_8), JsonObject::class.java ) as JsonObject - - val langFile = langDir.resolve("${langCode}_java.json") - FileWriter(langFile.toFile(), StandardCharsets.UTF_8).use { writer -> - gson.toJson(translations, writer) + FileWriter(langDir.resolve("${langCode}_java.json").toFile(), StandardCharsets.UTF_8).use { w -> + gson.toJson(translations, w) } } summary.add(langCode) logger.info("Wrote language: $langCode") } catch (e: Exception) { - logger.warn("Failed to load language: $langCode", e) + logger.warn("Failed to write language: $langCode", e) } } @@ -63,59 +55,47 @@ class Translations : Extractor.Extractor { return summary } - /** - * Discovers all available language codes from the client-side LanguageManager. - * Falls back to classpath scanning on dedicated servers. - */ - @Suppress("UNCHECKED_CAST") - private fun findLangCodes(): List { - try { - val minecraftClass = Class.forName("net.minecraft.client.Minecraft") - val client = minecraftClass.getMethod("getInstance").invoke(null) ?: return classpathFallback() - val languageManager = minecraftClass.getMethod("getLanguageManager").invoke(client) - val languages = languageManager.javaClass.getMethod("getLanguages").invoke(languageManager) as Map - if (languages.isNotEmpty()) { - val codes = languages.keys.sorted() - logger.info("Found ${codes.size} language(s) via client LanguageManager") - return codes - } - } catch (e: Exception) { - logger.info("Client LanguageManager not available, scanning classpath...") + private fun loadLangStream(langCode: String): InputStream? { + clientReflection?.let { ref -> + try { + val id = Identifier.fromNamespaceAndPath("minecraft", "lang/$langCode.json") + val resources = ref.getResourceStack(id) + if (resources.isNotEmpty()) { + return resources.first().open() as InputStream + } + } catch (_: Exception) { } } - return classpathFallback() + return this.javaClass.getResourceAsStream("/assets/minecraft/lang/$langCode.json") } /** - * Loads a language file from the client ResourceManager (which has access to the - * full Minecraft assets at runtime), falling back to classpath for dedicated servers. + * Cached reflective handle to the client Minecraft instance. + * Null when running on a dedicated server (no client available). */ - private fun loadLangFile(langCode: String): InputStream? { - // Try client resource manager first (has full assets at runtime) - try { - val minecraftClass = Class.forName("net.minecraft.client.Minecraft") - val client = minecraftClass.getMethod("getInstance").invoke(null) ?: return classpathLoad(langCode) - val resourceManager = minecraftClass.getMethod("getResourceManager").invoke(client) - val identifier = Identifier.fromNamespaceAndPath("minecraft", "lang/$langCode.json") - val resources = resourceManager.javaClass - .getMethod("getResourceStack", Identifier::class.java) - .invoke(resourceManager, identifier) as List<*> - if (resources.isNotEmpty()) { - val resource = resources.first()!! - return resource.javaClass.getMethod("open").invoke(resource) as InputStream - } - } catch (e: Exception) { - logger.debug("Client resource load failed for $langCode, trying classpath...") - } - - // Fallback: direct classpath loading (dedicated server or dev environment) - return classpathLoad(langCode) - } + private class ClientReflection private constructor( + private val getResourceStack: (Identifier) -> List<*>, + val languageCodes: List + ) { + companion object { + @Suppress("UNCHECKED_CAST") + fun resolve(): ClientReflection? { + return try { + val mc = Class.forName("net.minecraft.client.Minecraft") + val client = mc.getMethod("getInstance").invoke(null) ?: return null + val rm = mc.getMethod("getResourceManager").invoke(client) + val stackMethod = rm.javaClass.getMethod("getResourceStack", Identifier::class.java) - private fun classpathLoad(langCode: String): InputStream? { - return this.javaClass.getResourceAsStream("/assets/minecraft/lang/$langCode.json") - } + val lm = mc.getMethod("getLanguageManager").invoke(client) + val languages = lm.javaClass.getMethod("getLanguages").invoke(lm) as Map - private fun classpathFallback(): List { - return listOf("en_us") + ClientReflection( + getResourceStack = { id -> stackMethod.invoke(rm, id) as List<*> }, + languageCodes = languages.keys.sorted() + ) + } catch (_: Exception) { + null + } + } + } } } From a56c6c4120556d6f06ec6d84106366648e4f9f3e Mon Sep 17 00:00:00 2001 From: missinglove <0xwymx0@gmail.com> Date: Sun, 28 Jun 2026 06:24:38 +0800 Subject: [PATCH 08/12] Localization --- .../kotlin/de/snowii/extractor/Extractor.kt | 16 ++++----- src/main/kotlin/de/snowii/extractor/Lang.kt | 33 +++++++++++++++++++ .../extractors/non_registry/Translations.kt | 9 ++--- .../assets/extractor/lang/en_us.json | 18 ++++++++++ .../assets/extractor/lang/zh_cn.json | 18 ++++++++++ src/main/resources/fabric.mod.json | 4 +-- 6 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 src/main/kotlin/de/snowii/extractor/Lang.kt create mode 100644 src/main/resources/assets/extractor/lang/en_us.json create mode 100644 src/main/resources/assets/extractor/lang/zh_cn.json diff --git a/src/main/kotlin/de/snowii/extractor/Extractor.kt b/src/main/kotlin/de/snowii/extractor/Extractor.kt index d672e9f..c67903e 100644 --- a/src/main/kotlin/de/snowii/extractor/Extractor.kt +++ b/src/main/kotlin/de/snowii/extractor/Extractor.kt @@ -27,7 +27,7 @@ class Extractor : ModInitializer { } override fun onInitialize() { - logger.info("Starting Pumpkin Extractor") + logger.info(Lang.fmt("extractor.log.starting")) val extractors = arrayOf( Dialog(), DialogActionType(), DialogBodyType(), DialogType(), @@ -51,16 +51,16 @@ class Extractor : ModInitializer { val outputDirectory: Path try { outputDirectory = Files.createDirectories(Paths.get(OUTPUT_DIR)) - logger.info("Output directory: {}", outputDirectory.toAbsolutePath()) + logger.info(Lang.fmt("extractor.log.output_dir", outputDirectory.toAbsolutePath())) } catch (e: IOException) { - logger.error("Failed to create output directory.", e) + logger.error(Lang.fmt("extractor.log.output_dir_failed"), e) return } val gson = GsonBuilder().disableHtmlEscaping().create() ServerLifecycleEvents.SERVER_STARTED.register(ServerLifecycleEvents.ServerStarted { server: MinecraftServer -> - logger.info("Server started — running extractors...") + logger.info(Lang.fmt("extractor.log.server_started")) try { val timeInMillis = measureTimeMillis { for (ext in extractors) { @@ -70,15 +70,15 @@ class Extractor : ModInitializer { FileWriter(out.toFile(), StandardCharsets.UTF_8).use { writer -> gson.toJson(ext.extract(server), writer) } - logger.info("Wrote ${out.toAbsolutePath()}") + logger.info(Lang.fmt("extractor.log.wrote", out.toAbsolutePath())) } catch (e: Exception) { - logger.error("Extractor \"${ext.fileName()}\" failed.", e) + logger.error(Lang.fmt("extractor.log.extractor_failed", ext.fileName()), e) } } } - logger.info("Done, took ${timeInMillis}ms") + logger.info(Lang.fmt("extractor.log.done", timeInMillis)) } catch (e: Throwable) { - logger.error("Extraction failed with fatal error", e) + logger.error(Lang.fmt("extractor.log.fatal_error"), e) } }) } diff --git a/src/main/kotlin/de/snowii/extractor/Lang.kt b/src/main/kotlin/de/snowii/extractor/Lang.kt new file mode 100644 index 0000000..e415c8d --- /dev/null +++ b/src/main/kotlin/de/snowii/extractor/Lang.kt @@ -0,0 +1,33 @@ +package de.snowii.extractor + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import java.io.InputStreamReader + +/** + * Loads the mod's en_us.json language file and exposes [fmt] for formatted lookups. + * Log strings are kept in the lang file as the single source of truth; + * at runtime only the English fallback is resolved (logs are developer-facing). + */ +object Lang { + private val entries: Map by lazy { + val stream = Lang::class.java.classLoader + .getResourceAsStream("assets/extractor/lang/en_us.json") + if (stream == null) { + emptyMap() + } else { + stream.use { s -> + Gson().fromJson( + InputStreamReader(s, Charsets.UTF_8), + object : TypeToken>() {}.type + ) + } + } + } + + /** Look up key and format with classic String.format semantics. */ + fun fmt(key: String, vararg args: Any?): String { + val template = entries[key] ?: key + return if (args.isEmpty()) template else template.format(*args) + } +} diff --git a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt index f25cd0d..f3e7050 100644 --- a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt +++ b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt @@ -5,6 +5,7 @@ import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject import de.snowii.extractor.Extractor +import de.snowii.extractor.Lang import net.minecraft.resources.Identifier import net.minecraft.server.MinecraftServer import org.slf4j.LoggerFactory @@ -31,7 +32,7 @@ class Translations : Extractor.Extractor { for (langCode in clientReflection?.languageCodes ?: listOf("en_us")) { val stream = loadLangStream(langCode) ?: run { - logger.warn("Could not find lang file for: $langCode") + logger.warn(Lang.fmt("extractor.translations.not_found", langCode)) continue } try { @@ -45,13 +46,13 @@ class Translations : Extractor.Extractor { } } summary.add(langCode) - logger.info("Wrote language: $langCode") + logger.info(Lang.fmt("extractor.translations.wrote", langCode)) } catch (e: Exception) { - logger.warn("Failed to write language: $langCode", e) + logger.warn(Lang.fmt("extractor.translations.failed", langCode), e) } } - logger.info("Extracted ${summary.size()} languages to ${langDir.toAbsolutePath()}") + logger.info(Lang.fmt("extractor.translations.summary", summary.size(), langDir.toAbsolutePath())) return summary } diff --git a/src/main/resources/assets/extractor/lang/en_us.json b/src/main/resources/assets/extractor/lang/en_us.json new file mode 100644 index 0000000..193cb44 --- /dev/null +++ b/src/main/resources/assets/extractor/lang/en_us.json @@ -0,0 +1,18 @@ +{ + "mod.extractor.name": "Pumpkin Extractor", + "mod.extractor.description": "Extracts Minecraft data (blocks, items, entities, translations, etc.) into JSON files on server start.", + + "extractor.log.starting": "Starting Pumpkin Extractor", + "extractor.log.output_dir": "Output directory: %s", + "extractor.log.output_dir_failed": "Failed to create output directory.", + "extractor.log.server_started": "Server started — running extractors...", + "extractor.log.wrote": "Wrote %s", + "extractor.log.extractor_failed": "Extractor \"%s\" failed.", + "extractor.log.done": "Done, took %dms", + "extractor.log.fatal_error": "Extraction failed with fatal error", + + "extractor.translations.not_found": "Could not find lang file for: %s", + "extractor.translations.wrote": "Wrote language: %s", + "extractor.translations.failed": "Failed to write language: %s", + "extractor.translations.summary": "Extracted %d languages to %s" +} diff --git a/src/main/resources/assets/extractor/lang/zh_cn.json b/src/main/resources/assets/extractor/lang/zh_cn.json new file mode 100644 index 0000000..d7829ee --- /dev/null +++ b/src/main/resources/assets/extractor/lang/zh_cn.json @@ -0,0 +1,18 @@ +{ + "mod.extractor.name": "Pumpkin 数据提取器", + "mod.extractor.description": "在服务器启动时将 Minecraft 数据(方块、物品、实体、翻译等)提取为 JSON 文件。", + + "extractor.log.starting": "正在启动 Pumpkin Extractor", + "extractor.log.output_dir": "输出目录:%s", + "extractor.log.output_dir_failed": "无法创建输出目录。", + "extractor.log.server_started": "服务器已启动 — 开始执行提取器...", + "extractor.log.wrote": "已写入 %s", + "extractor.log.extractor_failed": "提取器 \"%s\" 失败。", + "extractor.log.done": "完成,耗时 %dms", + "extractor.log.fatal_error": "提取过程发生致命错误", + + "extractor.translations.not_found": "找不到语言文件:%s", + "extractor.translations.wrote": "已写入语言:%s", + "extractor.translations.failed": "写入语言失败:%s", + "extractor.translations.summary": "已将 %d 种语言提取到 %s" +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 6b3f31d..ea0e61a 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -2,8 +2,8 @@ "schemaVersion": 1, "id": "extractor", "version": "${version}", - "name": "pumpkin-extractor", - "description": "", + "name": "mod.extractor.name", + "description": "mod.extractor.description", "authors": [], "contact": {}, "license": "MIT", From 423af954fae6aa909978951d10a47015e5f7d86f Mon Sep 17 00:00:00 2001 From: missinglove <0xwymx0@gmail.com> Date: Sun, 28 Jun 2026 06:26:59 +0800 Subject: [PATCH 09/12] fix --- .../extractors/non_registry/Translations.kt | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt index f3e7050..944fa88 100644 --- a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt +++ b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt @@ -60,10 +60,7 @@ class Translations : Extractor.Extractor { clientReflection?.let { ref -> try { val id = Identifier.fromNamespaceAndPath("minecraft", "lang/$langCode.json") - val resources = ref.getResourceStack(id) - if (resources.isNotEmpty()) { - return resources.first().open() as InputStream - } + ref.openResource(id)?.let { return it } } catch (_: Exception) { } } return this.javaClass.getResourceAsStream("/assets/minecraft/lang/$langCode.json") @@ -74,11 +71,19 @@ class Translations : Extractor.Extractor { * Null when running on a dedicated server (no client available). */ private class ClientReflection private constructor( - private val getResourceStack: (Identifier) -> List<*>, + private val resourceManager: Any, + private val getResourceStackMethod: java.lang.reflect.Method, val languageCodes: List ) { + @Suppress("UNCHECKED_CAST") + fun openResource(identifier: Identifier): InputStream? { + val resources = getResourceStackMethod.invoke(resourceManager, identifier) as List<*> + if (resources.isEmpty()) return null + val resource = resources.first() ?: return null + return resource.javaClass.getMethod("open").invoke(resource) as InputStream + } + companion object { - @Suppress("UNCHECKED_CAST") fun resolve(): ClientReflection? { return try { val mc = Class.forName("net.minecraft.client.Minecraft") @@ -87,10 +92,12 @@ class Translations : Extractor.Extractor { val stackMethod = rm.javaClass.getMethod("getResourceStack", Identifier::class.java) val lm = mc.getMethod("getLanguageManager").invoke(client) + @Suppress("UNCHECKED_CAST") val languages = lm.javaClass.getMethod("getLanguages").invoke(lm) as Map ClientReflection( - getResourceStack = { id -> stackMethod.invoke(rm, id) as List<*> }, + resourceManager = rm, + getResourceStackMethod = stackMethod, languageCodes = languages.keys.sorted() ) } catch (_: Exception) { From 7e4e7cadbca0f409a20ffeda1f0bf1cc2fbf50ab Mon Sep 17 00:00:00 2001 From: missinglove <0xwymx0@gmail.com> Date: Sun, 28 Jun 2026 07:04:40 +0800 Subject: [PATCH 10/12] Partially revert changes --- .../kotlin/de/snowii/extractor/Extractor.kt | 156 ++++++++++++++++-- 1 file changed, 140 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/de/snowii/extractor/Extractor.kt b/src/main/kotlin/de/snowii/extractor/Extractor.kt index c67903e..d6cd1fd 100644 --- a/src/main/kotlin/de/snowii/extractor/Extractor.kt +++ b/src/main/kotlin/de/snowii/extractor/Extractor.kt @@ -30,22 +30,146 @@ class Extractor : ModInitializer { logger.info(Lang.fmt("extractor.log.starting")) val extractors = arrayOf( - Dialog(), DialogActionType(), DialogBodyType(), DialogType(), - InputControlType(), Advancement(), Effect(), SpawnEgg(), - PotionBrewing(), Potion(), Sounds(), Recipes(), - Biome(), BiomeMixerTest(), WorldEvent(), Carver(), - Enchantments(), ScoreboardDisplaySlot(), Dimension(), Particles(), - EntityAttributes(), ChunkStatus(), EntityStatuses(), MessageType(), - SoundCategory(), EntityPose(), GameEvent(), GameRules(), - SyncedRegistries(), ChunkGenSetting(), Packets(), Screens(), - PlacedFeatures(), ConfiguredFeatures(), Tags(), JukeboxSong(), - MetaDataType(), TrackedData(), NoiseParameters(), Structures(), - StructureSet(), Entities(), Items(), DataComponent(), Blocks(), - MultiNoise(), MultiNoise().Sample(), Translations(), - DensityFunctions(), DensityFunctions().Tests(), DamageTypes(), - Fluids(), Properties(), ComposterIncreaseChance(), - FlowerPotTransformation(), Fuels(), RecipeRemainder(), - VillagerData(), CustomStats(), Stats(), SlotRanges(), + Dialog(), + DialogActionType(), + DialogBodyType(), + DialogType(), + InputControlType(), + Advancement(), + Effect(), + SpawnEgg(), + PotionBrewing(), + Potion(), + Sounds(), + Recipes(), + Biome(), + BiomeMixerTest(), + WorldEvent(), + Carver(), + Enchantments(), + ScoreboardDisplaySlot(), + Dimension(), + Particles(), + EntityAttributes(), + ChunkStatus(), + EntityStatuses(), + MessageType(), + SoundCategory(), + EntityPose(), + GameEvent(), + GameRules(), + SyncedRegistries(), + ChunkGenSetting(), + Packets(), + Screens(), + PlacedFeatures(), + ConfiguredFeatures(), + Tags(), + JukeboxSong(), + MetaDataType(), + TrackedData(), + NoiseParameters(), + Structures(), + StructureSet(), + Entities(), + Items(), + DataComponent(), + Blocks(), + MultiNoise(), + MultiNoise().Sample(), + ChunkGenSetting(), + Translations(), + DensityFunctions(), + DensityFunctions().Tests(), + DamageTypes(), + Fluids(), + Properties(), + ComposterIncreaseChance(), + FlowerPotTransformation(), + Fuels(), + RecipeRemainder(), + VillagerData(), + CustomStats(), + Stats(), + SlotRanges(), + /* ChunkDumpTests.NoiseDump( + "no_blend_no_beard_0_0.chunk", + 0, + 0, + 0, + arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_7_4.chunk", + 0, + 7, + 4, + arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_only_cell_cache_0_0.chunk", + 0, + 0, + 0, + ArrayList() + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_only_cell_cache_flat_cache_0_0.chunk", + 0, + 0, + 0, + arrayListOf("FlatCache") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_only_cell_cache_interpolated_0_0.chunk", + 0, + 0, + 0, + arrayListOf("Interpolated") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_only_cell_cache_once_cache_0_0.chunk", + 0, + 0, + 0, + arrayListOf("CacheOnce") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_-595_544.chunk", + 0, + -595, + 544, + arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_-119_183.chunk", + 0, + -119, + 183, + arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_13579_-6_11.chunk", + 13579, + -6, + 11, + arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_13579_-2_15.chunk", + 13579, + -2, + 15, + arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") + ), + BiomeDumpTests(), + BiomeDumpTests().MultiNoiseBiomeSourceTest(), + ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_0_0.chunk", 0, 0, 0), + ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_badlands_-595_544.chunk", 0, -595, 544), + ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_frozen_ocean_-119_183.chunk", 0, -119, 183), + ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_13579_-6_11.chunk", 13579, -6, 11), + ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_13579_-2_15.chunk", 13579, -2, 15), + ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_13579_-7_9.chunk", 13579, -7, 9)*/ ) val outputDirectory: Path From 7980ecb7736b3e0e44e8fb0ddd832f83a2f6abe0 Mon Sep 17 00:00:00 2001 From: missinglove <0xwymx0@gmail.com> Date: Sun, 28 Jun 2026 07:11:10 +0800 Subject: [PATCH 11/12] Partially revert changes --- .../kotlin/de/snowii/extractor/Extractor.kt | 156 +++++++++--------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/src/main/kotlin/de/snowii/extractor/Extractor.kt b/src/main/kotlin/de/snowii/extractor/Extractor.kt index d6cd1fd..82998b5 100644 --- a/src/main/kotlin/de/snowii/extractor/Extractor.kt +++ b/src/main/kotlin/de/snowii/extractor/Extractor.kt @@ -92,84 +92,84 @@ class Extractor : ModInitializer { CustomStats(), Stats(), SlotRanges(), - /* ChunkDumpTests.NoiseDump( - "no_blend_no_beard_0_0.chunk", - 0, - 0, - 0, - arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_7_4.chunk", - 0, - 7, - 4, - arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_only_cell_cache_0_0.chunk", - 0, - 0, - 0, - ArrayList() - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_only_cell_cache_flat_cache_0_0.chunk", - 0, - 0, - 0, - arrayListOf("FlatCache") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_only_cell_cache_interpolated_0_0.chunk", - 0, - 0, - 0, - arrayListOf("Interpolated") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_only_cell_cache_once_cache_0_0.chunk", - 0, - 0, - 0, - arrayListOf("CacheOnce") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_-595_544.chunk", - 0, - -595, - 544, - arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_-119_183.chunk", - 0, - -119, - 183, - arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_13579_-6_11.chunk", - 13579, - -6, - 11, - arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") - ), - ChunkDumpTests.NoiseDump( - "no_blend_no_beard_13579_-2_15.chunk", - 13579, - -2, - 15, - arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") - ), - BiomeDumpTests(), - BiomeDumpTests().MultiNoiseBiomeSourceTest(), - ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_0_0.chunk", 0, 0, 0), - ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_badlands_-595_544.chunk", 0, -595, 544), - ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_frozen_ocean_-119_183.chunk", 0, -119, 183), - ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_13579_-6_11.chunk", 13579, -6, 11), - ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_13579_-2_15.chunk", 13579, -2, 15), - ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_13579_-7_9.chunk", 13579, -7, 9)*/ + /* ChunkDumpTests.NoiseDump( + "no_blend_no_beard_0_0.chunk", + 0, + 0, + 0, + arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_7_4.chunk", + 0, + 7, + 4, + arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_only_cell_cache_0_0.chunk", + 0, + 0, + 0, + ArrayList() + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_only_cell_cache_flat_cache_0_0.chunk", + 0, + 0, + 0, + arrayListOf("FlatCache") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_only_cell_cache_interpolated_0_0.chunk", + 0, + 0, + 0, + arrayListOf("Interpolated") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_only_cell_cache_once_cache_0_0.chunk", + 0, + 0, + 0, + arrayListOf("CacheOnce") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_-595_544.chunk", + 0, + -595, + 544, + arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_-119_183.chunk", + 0, + -119, + 183, + arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_13579_-6_11.chunk", + 13579, + -6, + 11, + arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") + ), + ChunkDumpTests.NoiseDump( + "no_blend_no_beard_13579_-2_15.chunk", + 13579, + -2, + 15, + arrayListOf("Interpolated", "CacheOnce", "FlatCache", "Cache2D") + ), + BiomeDumpTests(), + BiomeDumpTests().MultiNoiseBiomeSourceTest(), + ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_0_0.chunk", 0, 0, 0), + ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_badlands_-595_544.chunk", 0, -595, 544), + ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_frozen_ocean_-119_183.chunk", 0, -119, 183), + ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_13579_-6_11.chunk", 13579, -6, 11), + ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_13579_-2_15.chunk", 13579, -2, 15), + ChunkDumpTests.SurfaceDump("no_blend_no_beard_surface_13579_-7_9.chunk", 13579, -7, 9)*/ ) val outputDirectory: Path From e99ececa27f1dcb5bd60fb8e690fa643741fc228 Mon Sep 17 00:00:00 2001 From: missinglove <0xwymx0@gmail.com> Date: Thu, 2 Jul 2026 12:35:20 +0800 Subject: [PATCH 12/12] Make sure the output files are sorted. --- .../extractor/extractors/non_registry/Translations.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt index 944fa88..bd0aaba 100644 --- a/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt +++ b/src/main/kotlin/de/snowii/extractor/extractors/non_registry/Translations.kt @@ -37,12 +37,14 @@ class Translations : Extractor.Extractor { } try { stream.use { s -> - val translations = gson.fromJson( + val raw = gson.fromJson( InputStreamReader(s, StandardCharsets.UTF_8), JsonObject::class.java ) as JsonObject + val sorted = JsonObject() + raw.entrySet().sortedBy { it.key }.forEach { (k, v) -> sorted.add(k, v) } FileWriter(langDir.resolve("${langCode}_java.json").toFile(), StandardCharsets.UTF_8).use { w -> - gson.toJson(translations, w) + gson.toJson(sorted, w) } } summary.add(langCode)