From db13f39b30ca32deb62f516e8d6c434523f85ae8 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:53:51 -0400 Subject: [PATCH] Implement dynamic language loading --- .../ClientLanguageMixin.java | 57 +++++++++++++++++++ .../core/config/ModernFixEarlyConfig.java | 1 + .../dynamiclanguages/DynamicLanguageMap.java | 42 ++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java new file mode 100644 index 00000000..01595bdc --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java @@ -0,0 +1,57 @@ +package org.embeddedt.modernfix.common.mixin.perf.dynamic_languages; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.client.resources.language.ClientLanguage; +import net.minecraft.server.packs.resources.Resource; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.dynamiclanguages.DynamicLanguageMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; + +/** + * Modifies the language system to load/unload the contents of language entries based on GC pressure. + */ +@Mixin(ClientLanguage.class) +@ClientOnlyMixin +public class ClientLanguageMixin { + private static final ThreadLocal MFIX_MODIFY_APPEND_SEMANTICS = ThreadLocal.withInitial(() -> Boolean.FALSE); + + /** + * @author embeddedt + * @reason modify the semantics of appendFrom so that it's used to do a prepass + */ + @ModifyArg(method = "appendFrom", at = @At(value = "INVOKE", target = "Lnet/minecraft/locale/Language;loadFromJson(Ljava/io/InputStream;Ljava/util/function/BiConsumer;)V"), index = 1) + private static BiConsumer changeSemanticsOfConsumer(BiConsumer consumer, @Local(ordinal = 0, argsOnly = true) Map destinationMap, @Local(ordinal = 0) Resource resource) { + return MFIX_MODIFY_APPEND_SEMANTICS.get() ? ((k, v) -> destinationMap.put(k, resource)) : consumer; + } + + /** + * @author embeddedt + * @reason collect resources that own keys with a prepass + */ + @WrapOperation(method = "loadFrom", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/language/ClientLanguage;appendFrom(Ljava/lang/String;Ljava/util/List;Ljava/util/Map;)V")) + private static void trackEntrySource(String languageName, List resources, Map destinationMap, Operation original) { + MFIX_MODIFY_APPEND_SEMANTICS.set(true); + try { + original.call(languageName, resources, destinationMap); + } finally { + MFIX_MODIFY_APPEND_SEMANTICS.remove(); + } + } + + /** + * @author embeddedt + * @reason figure out which keys are dynamically loaded and which are injected by mixins + */ + @ModifyArg(method = "loadFrom", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/language/ClientLanguage;(Ljava/util/Map;Z)V"), index = 0) + private static Map modifyLanguageMap(Map storage) { + return DynamicLanguageMap.forStorage(Map.copyOf(storage)); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java index 6b8473e5..32519749 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -194,6 +194,7 @@ public class ModernFixEarlyConfig { .putConditionally(() -> !isFabric, "mixin.feature.registry_event_progress", false) // Beta (promote on next release) .put("mixin.perf.compact_entity_models", false) + .put("mixin.perf.dynamic_languages", false) .put("mixin.perf.faster_capabilities.bytecode_analysis", false) .put("mixin.perf.ingredient_item_deduplication", false) // END diff --git a/src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java b/src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java new file mode 100644 index 00000000..a3471627 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java @@ -0,0 +1,42 @@ +package org.embeddedt.modernfix.dynamiclanguages; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.Maps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.locale.Language; +import net.minecraft.server.packs.resources.Resource; +import org.embeddedt.modernfix.ModernFix; + +import java.io.IOException; +import java.util.Map; + +public class DynamicLanguageMap { + public static Map forStorage(Map storage) { + LoadingCache> languageFileContents = CacheBuilder.newBuilder() + .softValues() + .build(new CacheLoader<>() { + @Override + public Map load(Resource resource) throws Exception { + Map data = new Object2ObjectOpenHashMap<>(); + try (var stream = resource.open()) { + Language.loadFromJson(stream, data::put); + } catch (IOException e) { + ModernFix.LOGGER.error("Error loading language data from {}", resource.sourcePackId(), e); + } + return data; + } + }); + return Maps.asMap(storage.keySet(), k -> { + var value = storage.get(k); + if (value instanceof Resource r) { + return languageFileContents.getUnchecked(r).getOrDefault(k, ""); + } else if (value instanceof String s) { + return s; + } else { + return null; + } + }); + } +}