From 5acb5115b91a086dc7640e1f9767475d1eef7c34 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:14:53 -0400 Subject: [PATCH 1/5] Add mixin audit to CI --- .github/workflows/gradle.yml | 2 ++ build.gradle.kts | 4 ++++ .../modernfix/common/mixin/core/BootstrapMixin.java | 8 ++++++++ 3 files changed, 14 insertions(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 9292434d..5b621513 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -44,6 +44,8 @@ jobs: run: ./scripts/tagcleaner.sh - name: Build ModernFix using Gradle run: ./gradlew build + - name: Run mixin audit + run: timeout 60 xvfb-run ./gradlew runAuditClient - name: Publish mod to CurseForge & Modrinth if: steps.check_branch.outputs.is_release == 'true' run: ./gradlew publishMods copyJarToBin diff --git a/build.gradle.kts b/build.gradle.kts index da20c349..e6dc06a9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,6 +38,10 @@ legacyForge { create("server") { server() } + create("auditClient") { + client() + jvmArguments.addAll("-Dmodernfix.auditAndExit=true", "-Djava.awt.headless=true") + } } mods { diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java index 1a108612..5009f274 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java @@ -8,6 +8,7 @@ import org.embeddedt.modernfix.forge.load.ModWorkManagerQueue; import org.embeddedt.modernfix.util.TimeFormatter; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -27,6 +28,13 @@ public class BootstrapMixin { LOGGER.info("ModernFix reached bootstrap stage ({} after launch)", TimeFormatter.formatNanos(ManagementFactory.getRuntimeMXBean().getUptime() * 1000L * 1000L)); ModWorkManagerQueue.replace(); ManifestCompactor.compactManifests(); + boolean auditAndExit = Boolean.getBoolean("modernfix.auditAndExit"); + if (auditAndExit || Boolean.getBoolean("modernfix.auditMixinsAtStart")) { + MixinEnvironment.getCurrentEnvironment().audit(); + if (auditAndExit) { + System.exit(0); + } + } } } From 438ceb1984d81964d6e7d6f8fbcec19259fc1d42 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:19:22 -0400 Subject: [PATCH 2/5] Move auditing to happen later in launch --- src/main/java/org/embeddedt/modernfix/ModernFix.java | 10 ++++++++++ .../modernfix/common/mixin/core/BootstrapMixin.java | 8 -------- .../embeddedt/modernfix/forge/init/ModernFixForge.java | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/ModernFix.java b/src/main/java/org/embeddedt/modernfix/ModernFix.java index 17e27d64..da1d2bee 100644 --- a/src/main/java/org/embeddedt/modernfix/ModernFix.java +++ b/src/main/java/org/embeddedt/modernfix/ModernFix.java @@ -12,6 +12,7 @@ import org.embeddedt.modernfix.core.ModernFixMixinPlugin; import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; import org.embeddedt.modernfix.resources.ReloadExecutor; import org.embeddedt.modernfix.util.ClassInfoManager; +import org.spongepowered.asm.mixin.MixinEnvironment; import java.lang.management.ManagementFactory; import java.util.concurrent.ExecutorService; @@ -45,6 +46,15 @@ public class ModernFix { return resourceReloadService; } + public static void runAuditIfRequested() { + boolean auditAndExit = Boolean.getBoolean("modernfix.auditAndExit"); + if (auditAndExit || Boolean.getBoolean("modernfix.auditMixinsAtStart")) { + MixinEnvironment.getCurrentEnvironment().audit(); + if (auditAndExit) { + System.exit(0); + } + } + } public ModernFix() { INSTANCE = this; diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java index 5009f274..1a108612 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java @@ -8,7 +8,6 @@ import org.embeddedt.modernfix.forge.load.ModWorkManagerQueue; import org.embeddedt.modernfix.util.TimeFormatter; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -28,13 +27,6 @@ public class BootstrapMixin { LOGGER.info("ModernFix reached bootstrap stage ({} after launch)", TimeFormatter.formatNanos(ManagementFactory.getRuntimeMXBean().getUptime() * 1000L * 1000L)); ModWorkManagerQueue.replace(); ManifestCompactor.compactManifests(); - boolean auditAndExit = Boolean.getBoolean("modernfix.auditAndExit"); - if (auditAndExit || Boolean.getBoolean("modernfix.auditMixinsAtStart")) { - MixinEnvironment.getCurrentEnvironment().audit(); - if (auditAndExit) { - System.exit(0); - } - } } } diff --git a/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java b/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java index 4030eca6..2ac9a768 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java +++ b/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java @@ -129,6 +129,7 @@ public class ModernFixForge { } ObjectHolderClearer.clearThrowables(); event.enqueueWork(ObjectHolderClearer::removeRedundantHolders); + event.enqueueWork(ModernFix::runAuditIfRequested); } @SubscribeEvent(priority = EventPriority.LOWEST) public void onServerDead(ServerStoppedEvent event) { From d749205427d714a4865155f03c16a48f8e564117 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:39:36 -0400 Subject: [PATCH 3/5] Adjust dynamic_languages for better mod compatibility --- .../ClientLanguageMixin.java | 44 +++++++++---------- .../dynamiclanguages/DynamicLanguageMap.java | 20 ++++++++- 2 files changed, 39 insertions(+), 25 deletions(-) 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 index 01595bdc..0d62a734 100644 --- 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 @@ -2,7 +2,8 @@ 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 com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; import net.minecraft.client.resources.language.ClientLanguage; import net.minecraft.server.packs.resources.Resource; import org.embeddedt.modernfix.annotation.ClientOnlyMixin; @@ -11,39 +12,33 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.ModifyArg; +import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.function.BiConsumer; +import java.util.Objects; /** * Modifies the language system to load/unload the contents of language entries based on GC pressure. */ -@Mixin(ClientLanguage.class) +@Mixin(value = ClientLanguage.class, priority = 2000) @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 + * @reason collect the list of all known language resources */ - @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(); + @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 collectResources(String languageName, List resources, + Map destinationMap, Operation original, + @Share("usedResources") LocalRef> usedResources) { + List collected = usedResources.get(); + if (collected == null) { + collected = new ArrayList<>(); + usedResources.set(collected); } + collected.addAll(resources); + original.call(languageName, resources, destinationMap); } /** @@ -51,7 +46,8 @@ public class ClientLanguageMixin { * @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)); + private static Map modifyLanguageMap(Map storage, @Share("usedResources") LocalRef> usedResources) { + List collected = Objects.requireNonNullElse(usedResources.get(), List.of()); + return DynamicLanguageMap.forVanillaData(storage, collected); } } diff --git a/src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java b/src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java index a3471627..9e6110dc 100644 --- a/src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java +++ b/src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java @@ -10,10 +10,28 @@ import net.minecraft.server.packs.resources.Resource; import org.embeddedt.modernfix.ModernFix; import java.io.IOException; +import java.util.HashMap; +import java.util.List; import java.util.Map; public class DynamicLanguageMap { - public static Map forStorage(Map storage) { + private static Map createStorage(Map rawLanguageContents, List languageResources) { + Map storage = new HashMap<>(rawLanguageContents); + for (var resource : languageResources) { + try (var stream = resource.open()) { + Language.loadFromJson(stream, (key, value) -> { + if (value != null && value.equals(storage.get(key))) { + storage.put(key, resource); + } + }); + } catch (Exception ignored) { + } + } + return Map.copyOf(storage); + } + + public static Map forVanillaData(Map rawLanguageContents, List languageResources) { + Map storage = createStorage(rawLanguageContents, languageResources); LoadingCache> languageFileContents = CacheBuilder.newBuilder() .softValues() .build(new CacheLoader<>() { From 85955ebf75749a96289ad42acdf2a330981df72c Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 12 Apr 2026 16:02:54 -0400 Subject: [PATCH 4/5] Ensure integrated server is ticked at least once before player connects Fixes #639 --- .../IntegratedServerMixin.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java index 57cd9f8b..a7090b1b 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java @@ -17,6 +17,8 @@ import org.embeddedt.modernfix.duck.suspend_integrated_server_during_load.IDefer import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.net.Proxy; import java.util.concurrent.atomic.AtomicBoolean; @@ -28,6 +30,7 @@ public abstract class IntegratedServerMixin extends MinecraftServer implements I @Shadow private boolean paused; + private int mfix$numTickServerCalls = 0; private final AtomicBoolean mfix$hasPrimaryClientJoined = new AtomicBoolean(false); public IntegratedServerMixin(Thread serverThread, LevelStorageSource.LevelStorageAccess storageSource, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer fixerUpper, Services services, ChunkProgressListenerFactory progressListenerFactory) { @@ -44,14 +47,26 @@ public abstract class IntegratedServerMixin extends MinecraftServer implements I return !mfix$hasPrimaryClientJoined.get() || original.call(instance); } + /** + * @author embeddedt + * @reason Keep our own tick count for the integrated server specifically, rather than relying on super + * to increment. + */ + @Inject(method = "tickServer", at = @At("HEAD")) + private void mfix$countTicks(CallbackInfo ci) { + this.mfix$numTickServerCalls++; + } + /** * @author embeddedt * @reason If waiting for a client connection to exist, we only need to tick the server connection, - * not the whole server as vanilla does. + * not the whole server as vanilla does. However, we must tick the whole server once to accommodate mods + * that rely on the first tick to initialize state as a side effect. Not doing this causes issues like + * #639. */ @WrapWithCondition(method = "tickServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;tickServer(Ljava/util/function/BooleanSupplier;)V", ordinal = 0)) private boolean preventRunningFullServerTick(MinecraftServer server, BooleanSupplier hasTimeLeft) { - if (this.paused && !mfix$hasPrimaryClientJoined.get()) { + if (this.mfix$numTickServerCalls >= 2 && this.paused && !mfix$hasPrimaryClientJoined.get()) { var conn = this.getConnection(); if (conn != null) { conn.tick(); From c64ca2e54b5ac89f6e6e2fef52caf37abb340174 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 12 Apr 2026 16:36:44 -0400 Subject: [PATCH 5/5] Fix potential crash with mods that inject custom surface building logic Fixes #638 --- .../mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java index aa26d345..c80b375e 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java @@ -26,7 +26,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.util.function.Function; -@Mixin(SurfaceSystem.class) +@Mixin(value = SurfaceSystem.class, priority = 2000) public class SurfaceSystemMixin { private static final ThreadLocal MFIX_LOOKUP_CACHE = ThreadLocal.withInitial(ChunkBiomeLookup::new); private static final ThreadLocal MFIX_BLOCK_COLUMN = new ThreadLocal<>(); @@ -43,7 +43,7 @@ public class SurfaceSystemMixin { return lookup; } - @Inject(method = "buildSurface", at = @At("RETURN")) + @Inject(method = "buildSurface", at = @At("TAIL")) private void finishAndDisposeLookups(RandomState randomState, BiomeManager biomeManager, Registry biomes, boolean p_224652_, WorldGenerationContext context, ChunkAccess chunk, NoiseChunk noiseChunk, SurfaceRules.RuleSource ruleSource, CallbackInfo ci) { MFIX_LOOKUP_CACHE.get().dispose(); var column = MFIX_BLOCK_COLUMN.get();