From ae20fa17c9e747211193820d0a327434f105349d Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Mon, 18 May 2026 10:05:23 -0400 Subject: [PATCH 01/34] Fix random CMEs from NightConfigWatchThrottler --- .../config/NightConfigWatchThrottler.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/forge/config/NightConfigWatchThrottler.java b/src/main/java/org/embeddedt/modernfix/forge/config/NightConfigWatchThrottler.java index a0283bd2e..b75bfad10 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/config/NightConfigWatchThrottler.java +++ b/src/main/java/org/embeddedt/modernfix/forge/config/NightConfigWatchThrottler.java @@ -22,7 +22,8 @@ public class NightConfigWatchThrottler { @SuppressWarnings("rawtypes") public static void throttle() { Map watchedDirs = ObfuscationReflectionHelper.getPrivateValue(FileWatcher.class, FileWatcher.defaultInstance(), "watchedDirs"); - ObfuscationReflectionHelper.setPrivateValue(FileWatcher.class, FileWatcher.defaultInstance(), new ForwardingMap() { + Thread launchThread = Thread.currentThread(); + Map watchedDirsWrapper = new ForwardingMap() { @Override protected Map delegate() { return watchedDirs; @@ -44,13 +45,24 @@ protected Collection delegate() { public Iterator iterator() { // iterator() is called at the beginning of each iteration of the watch loop, // so it is a good spot to inject the delay. - LockSupport.parkNanos(DELAY); + if (Thread.currentThread() != launchThread) { + LockSupport.parkNanos(DELAY); + } return super.iterator(); } }; } return cachedValues; } - }, "watchedDirs"); + }; + // Force all classes related to the iterator to be loaded ahead of time. This is necessary to prevent + // a ConcurrentModificationException from being thrown inside ModLauncher when the NightConfig file + // watcher thread loads forwarding collection classes while the main thread is still mutating the + // launch plugin map. + //noinspection StatementWithEmptyBody + for (var ignored : watchedDirsWrapper.values()) { + + } + ObfuscationReflectionHelper.setPrivateValue(FileWatcher.class, FileWatcher.defaultInstance(), watchedDirsWrapper, "watchedDirs"); } } From afe3e09a27df65553e2feda427abfa04074ae09a Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 23 May 2026 11:51:11 -0400 Subject: [PATCH 02/34] Add feature level system for mixins --- .../modernfix/annotation/FeatureLevel.java | 9 +++++++ .../annotation/RequiresFeatureLevel.java | 12 +++++++++ .../modernfix/core/ModernFixMixinPlugin.java | 6 +++++ .../core/config/ModernFixEarlyConfig.java | 26 +++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 annotations/src/main/java/org/embeddedt/modernfix/annotation/FeatureLevel.java create mode 100644 annotations/src/main/java/org/embeddedt/modernfix/annotation/RequiresFeatureLevel.java diff --git a/annotations/src/main/java/org/embeddedt/modernfix/annotation/FeatureLevel.java b/annotations/src/main/java/org/embeddedt/modernfix/annotation/FeatureLevel.java new file mode 100644 index 000000000..06aa3098f --- /dev/null +++ b/annotations/src/main/java/org/embeddedt/modernfix/annotation/FeatureLevel.java @@ -0,0 +1,9 @@ +package org.embeddedt.modernfix.annotation; + +public enum FeatureLevel { + GA, BETA; + + public boolean isAtLeast(FeatureLevel required) { + return this.ordinal() >= required.ordinal(); + } +} diff --git a/annotations/src/main/java/org/embeddedt/modernfix/annotation/RequiresFeatureLevel.java b/annotations/src/main/java/org/embeddedt/modernfix/annotation/RequiresFeatureLevel.java new file mode 100644 index 000000000..7ebd7787c --- /dev/null +++ b/annotations/src/main/java/org/embeddedt/modernfix/annotation/RequiresFeatureLevel.java @@ -0,0 +1,12 @@ +package org.embeddedt.modernfix.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface RequiresFeatureLevel { + FeatureLevel value() default FeatureLevel.GA; +} diff --git a/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java b/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java index 05bf9147b..aeda65edd 100644 --- a/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java +++ b/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableSet; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.embeddedt.modernfix.annotation.FeatureLevel; import org.embeddedt.modernfix.core.config.ModernFixEarlyConfig; import org.embeddedt.modernfix.core.config.Option; import org.embeddedt.modernfix.core.launchplugin.CoreLaunchPluginService; @@ -40,6 +41,11 @@ public ModernFixMixinPlugin() { this.logger.info("Loaded configuration file for ModernFix {}: {} options available, {} override(s) found", ModernFixPlatformHooks.INSTANCE.getVersionString(), config.getOptionCount(), config.getOptionOverrideCount()); + if(ModernFixEarlyConfig.ACTIVE_FEATURE_LEVEL != FeatureLevel.GA) { + this.logger.warn("ModernFix stability level is set to {}. Features at this level may be unstable or cause crashes.", + ModernFixEarlyConfig.ACTIVE_FEATURE_LEVEL); + } + config.getOptionMap().values().forEach(option -> { if (option.isOverridden()) { String source = "[unknown]"; 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 bebe4c6bb..6e67bd7f5 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -9,7 +9,9 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.annotation.FeatureLevel; import org.embeddedt.modernfix.annotation.IgnoreOutsideDev; +import org.embeddedt.modernfix.annotation.RequiresFeatureLevel; import org.embeddedt.modernfix.annotation.RequiresMod; import org.embeddedt.modernfix.core.ModernFixMixinPlugin; import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; @@ -65,6 +67,18 @@ private static boolean modPresent(String modId) { private static final String MIXIN_CLIENT_ONLY_DESC = Type.getDescriptor(ClientOnlyMixin.class); private static final String MIXIN_REQUIRES_MOD_DESC = Type.getDescriptor(RequiresMod.class); private static final String MIXIN_DEV_ONLY_DESC = Type.getDescriptor(IgnoreOutsideDev.class); + private static final String FEATURE_LEVEL_ANNOTATION_DESC = Type.getDescriptor(RequiresFeatureLevel.class); + + public static final FeatureLevel ACTIVE_FEATURE_LEVEL = resolveFeatureLevel(); + + private static FeatureLevel resolveFeatureLevel() { + String prop = System.getProperty("modernfix.stabilityLevel", "ga").toLowerCase(Locale.ROOT); + try { + return FeatureLevel.valueOf(prop); + } catch (IllegalArgumentException e) { + return FeatureLevel.GA; + } + } private static final Pattern PLATFORM_PREFIX = Pattern.compile("(forge|fabric|common)\\."); @@ -112,6 +126,7 @@ private void scanForAndBuildMixinOptions() { return; boolean isMixin = false, isClientOnly = false, requiredModPresent = true, isDevOnly = false; String requiredModId = ""; + FeatureLevel requiredLevel = FeatureLevel.GA; for(AnnotationNode annotation : node.invisibleAnnotations) { if(Objects.equals(annotation.desc, MIXIN_DESC)) { isMixin = true; @@ -130,6 +145,15 @@ private void scanForAndBuildMixinOptions() { } } else if(Objects.equals(annotation.desc, MIXIN_DEV_ONLY_DESC)) { isDevOnly = true; + } else if(Objects.equals(annotation.desc, FEATURE_LEVEL_ANNOTATION_DESC)) { + for(int i = 0; i < annotation.values.size(); i += 2) { + if(annotation.values.get(i).equals("value")) { + // ASM stores enum annotation values as String[]{typeDescriptor, constantName} + String[] enumVal = (String[]) annotation.values.get(i + 1); + requiredLevel = FeatureLevel.valueOf(enumVal[1]); + break; + } + } } } if(isMixin && (!isDevOnly || ModernFixPlatformHooks.INSTANCE.isDevEnv())) { @@ -138,6 +162,8 @@ private void scanForAndBuildMixinOptions() { mixinsMissingMods.put(mixinClassName, requiredModId); else if(isClientOnly && !ModernFixPlatformHooks.INSTANCE.isClient()) mixinsMissingMods.put(mixinClassName, "[not client]"); + else if(!ACTIVE_FEATURE_LEVEL.isAtLeast(requiredLevel)) + mixinsMissingMods.put(mixinClassName, "[feature level: requires " + requiredLevel + "]"); String mixinCategoryName = "mixin." + mixinClassName.substring(0, mixinClassName.lastIndexOf('.')); mixinOptions.add(mixinCategoryName); } From 8213a720a3da109a5882989ddbd838088cff4821 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 23 May 2026 11:56:45 -0400 Subject: [PATCH 03/34] Optimize TerraBlender using extended surface biome context Supersedes TerraBlenderFix --- build.gradle.kts | 1 + .../NamespacedSurfaceRuleSourceMixin.java | 97 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NamespacedSurfaceRuleSourceMixin.java diff --git a/build.gradle.kts b/build.gradle.kts index e6dc06a95..82c336aa5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -129,6 +129,7 @@ dependencies { modCompileOnly("curse.maven:cofhcore-69162:5374122") modCompileOnly("curse.maven:resourcefullib-570073:5659871") modCompileOnly("curse.maven:kubejs-238086:5853326") + modCompileOnly("curse.maven:terrablender-563928:6290448") } tasks.named("jar") { diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NamespacedSurfaceRuleSourceMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NamespacedSurfaceRuleSourceMixin.java new file mode 100644 index 000000000..223a9cdb3 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NamespacedSurfaceRuleSourceMixin.java @@ -0,0 +1,97 @@ +package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules; + +import com.google.common.collect.ImmutableList; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import it.unimi.dsi.fastutil.objects.ObjectArraySet; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.levelgen.SurfaceRules; +import org.embeddedt.modernfix.annotation.FeatureLevel; +import org.embeddedt.modernfix.annotation.RequiresFeatureLevel; +import org.embeddedt.modernfix.annotation.RequiresMod; +import org.embeddedt.modernfix.world.gen.ExtendedSurfaceContext; +import org.spongepowered.asm.mixin.Final; +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.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import terrablender.worldgen.surface.NamespacedSurfaceRuleSource; + +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +@Mixin(NamespacedSurfaceRuleSource.class) +@RequiresMod("terrablender") +@RequiresFeatureLevel(FeatureLevel.BETA) +public class NamespacedSurfaceRuleSourceMixin { + @Shadow + @Final + private Map sources; + + @Shadow + @Final + private SurfaceRules.RuleSource base; + + /** + * @author embeddedt + * @reason Avoid doing an expensive biome lookup per block in cases where we can prove all biomes will be from a + * single namespace. This achieves much of the benefit of TerraBlenderFix without the compatibility issues. + */ + @Inject(method = "apply(Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;)Lnet/minecraft/world/level/levelgen/SurfaceRules$SurfaceRule;", at = @At("HEAD"), cancellable = true) + private void modernfix$fastApply(SurfaceRules.Context context, CallbackInfoReturnable cir, + @Share("possibleNamespaces") LocalRef> possibleNamespacesRef) { + var possibleBiomes = ((ExtendedSurfaceContext)(Object)context).mfix$getPossibleBiomes(); + if (possibleBiomes == null) { + return; + } + Set namespaces = mfix$findNamespaces(possibleBiomes); + possibleNamespacesRef.set(namespaces); + if (namespaces.size() != 1) { + return; + } + String singleNamespace = namespaces.iterator().next(); + // In a single namespace scenario, we can bypass the biome lookup and directly construct a sequence rule + SurfaceRules.RuleSource namespacedSource = this.sources.get(singleNamespace); + if (namespacedSource == null) { + // Sequence rule wrapper not required + cir.setReturnValue(this.base.apply(context)); + } else { + cir.setReturnValue(new SurfaceRules.SequenceRule(ImmutableList.of(namespacedSource.apply(context), this.base.apply(context)))); + } + } + + /** + * @author embeddedt + * @reason Even if we have to fall back to the namespaced source, avoid compiling surface rules for namespaces that + * will never be hit in the given chunk. + */ + @ModifyArg(method = "apply(Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;)Lnet/minecraft/world/level/levelgen/SurfaceRules$SurfaceRule;", at = @At(value = "INVOKE", target = "Ljava/util/Set;forEach(Ljava/util/function/Consumer;)V")) + private Consumer> mfix$filterConsumer(Consumer> originalConsumer, + @Share("possibleNamespaces") LocalRef> possibleNamespacesRef) { + var possibleNamespaces = possibleNamespacesRef.get(); + if (possibleNamespaces == null) { + return originalConsumer; + } + return entry -> { + if(possibleNamespaces.contains(entry.getKey())) { + originalConsumer.accept(entry); + } + }; + } + + private static Set mfix$findNamespaces(Set> possibleBiomes) { + if (possibleBiomes.size() == 1) { + return Set.of(possibleBiomes.iterator().next().location().getNamespace()); + } else { + var namespaces = new ObjectArraySet(4); + for (var key : possibleBiomes) { + namespaces.add(key.location().getNamespace()); + } + return Set.copyOf(namespaces); + } + } +} From 29ff5f152e1ede9905cd2a4b7cd1ab728f810690 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 23 May 2026 11:58:36 -0400 Subject: [PATCH 04/34] Log the state of each mixin at DEBUG level --- .../modernfix/core/ModernFixMixinPlugin.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java b/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java index aeda65edd..e75b4cb68 100644 --- a/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java +++ b/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java @@ -135,10 +135,17 @@ public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { } String mixin = mixinClassName.substring(MIXIN_PACKAGE_ROOT.length()); - if(!instance.isOptionEnabled(mixin)) + if(!instance.isOptionEnabled(mixin)) { + this.logger.debug("Skipping mixin {}: disabled by configuration", mixin); return false; + } String disabledBecauseMod = instance.config.getPermanentlyDisabledMixins().get(mixin); - return disabledBecauseMod == null; + if(disabledBecauseMod != null) { + this.logger.debug("Skipping mixin {}: disabled for mod compat ({})", mixin, disabledBecauseMod); + return false; + } + this.logger.debug("Applying mixin {}", mixin); + return true; } public boolean isOptionEnabled(String mixin) { From 85aab426c52baad57f55691f3724c5fd3edf8562 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 23 May 2026 12:01:33 -0400 Subject: [PATCH 05/34] Fix mixin AP complaints --- .../NamespacedSurfaceRuleSourceMixin.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NamespacedSurfaceRuleSourceMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NamespacedSurfaceRuleSourceMixin.java index 223a9cdb3..be95eb37b 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NamespacedSurfaceRuleSourceMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NamespacedSurfaceRuleSourceMixin.java @@ -41,7 +41,7 @@ public class NamespacedSurfaceRuleSourceMixin { * @reason Avoid doing an expensive biome lookup per block in cases where we can prove all biomes will be from a * single namespace. This achieves much of the benefit of TerraBlenderFix without the compatibility issues. */ - @Inject(method = "apply(Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;)Lnet/minecraft/world/level/levelgen/SurfaceRules$SurfaceRule;", at = @At("HEAD"), cancellable = true) + @Inject(method = "apply(Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;)Lnet/minecraft/world/level/levelgen/SurfaceRules$SurfaceRule;", at = @At("HEAD"), cancellable = true, remap = false) private void modernfix$fastApply(SurfaceRules.Context context, CallbackInfoReturnable cir, @Share("possibleNamespaces") LocalRef> possibleNamespacesRef) { var possibleBiomes = ((ExtendedSurfaceContext)(Object)context).mfix$getPossibleBiomes(); @@ -69,7 +69,7 @@ public class NamespacedSurfaceRuleSourceMixin { * @reason Even if we have to fall back to the namespaced source, avoid compiling surface rules for namespaces that * will never be hit in the given chunk. */ - @ModifyArg(method = "apply(Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;)Lnet/minecraft/world/level/levelgen/SurfaceRules$SurfaceRule;", at = @At(value = "INVOKE", target = "Ljava/util/Set;forEach(Ljava/util/function/Consumer;)V")) + @ModifyArg(method = "apply(Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;)Lnet/minecraft/world/level/levelgen/SurfaceRules$SurfaceRule;", at = @At(value = "INVOKE", target = "Ljava/util/Set;forEach(Ljava/util/function/Consumer;)V"), remap = false) private Consumer> mfix$filterConsumer(Consumer> originalConsumer, @Share("possibleNamespaces") LocalRef> possibleNamespacesRef) { var possibleNamespaces = possibleNamespacesRef.get(); From f4f596ca0c37d775a4277a27b5e838562323d1ef Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 23 May 2026 12:50:21 -0400 Subject: [PATCH 06/34] Fix mixin failing at runtime due to missing AT --- src/main/resources/META-INF/accesstransformer.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index c7980896e..e25fffd68 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -5,6 +5,7 @@ public net.minecraft.client.renderer.block.model.multipart.MultiPart f_111962_ public net.minecraft.client.resources.model.ModelBakery$ModelBakerImpl public net.minecraft.client.resources.model.ModelBakery$ModelBakerImpl (Lnet/minecraft/client/resources/model/ModelBakery;Ljava/util/function/BiFunction;Lnet/minecraft/resources/ResourceLocation;)V public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRule +public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRule (Ljava/util/List;)V public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource (Ljava/util/List;)V public net.minecraft.world.level.levelgen.SurfaceRules$TestRuleSource From 50cedfc6993d7ad69e86f46494f61cd637dd6f04 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 23 May 2026 12:50:33 -0400 Subject: [PATCH 07/34] Fix stability level being impossible to override --- .../embeddedt/modernfix/core/config/ModernFixEarlyConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6e67bd7f5..4f9f00860 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -72,7 +72,7 @@ private static boolean modPresent(String modId) { public static final FeatureLevel ACTIVE_FEATURE_LEVEL = resolveFeatureLevel(); private static FeatureLevel resolveFeatureLevel() { - String prop = System.getProperty("modernfix.stabilityLevel", "ga").toLowerCase(Locale.ROOT); + String prop = System.getProperty("modernfix.stabilityLevel", "ga").toUpperCase(Locale.ROOT); try { return FeatureLevel.valueOf(prop); } catch (IllegalArgumentException e) { From f8d2425242e7865651133ac5d0e2d7cc257a1499 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 23 May 2026 12:50:48 -0400 Subject: [PATCH 08/34] Improve accuracy of possible biomes check --- .../NoiseBasedChunkGeneratorMixin.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NoiseBasedChunkGeneratorMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NoiseBasedChunkGeneratorMixin.java index 4791d620b..ff265251e 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NoiseBasedChunkGeneratorMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NoiseBasedChunkGeneratorMixin.java @@ -26,8 +26,15 @@ public class NoiseBasedChunkGeneratorMixin { @SuppressWarnings("unchecked") private static void mfix$accumulate(Set> chunkBiomes, LevelChunkSection section) { var palette = ((ExtendedPalettedContainer>)section.getBiomes()).mfix$getPalette(); - for (int i = 0; i < palette.getSize(); i++) { - chunkBiomes.add(palette.valueFor(i).unwrapKey().orElseThrow()); + if (palette.getSize() == 1) { + // No need to iterate the storage itself, as there can only be one value + chunkBiomes.add(palette.valueFor(0).unwrapKey().orElseThrow()); + } else { + // Use getAll() rather than raw palette iteration. PalettedContainer.recreate() seeds the new + // palette with Biomes.PLAINS (the initial default), leaving a stale palette entry even after + // fillBiomesFromNoise replaces all cells with real biomes. getAll() only visits entries that + // are actually referenced in the backing storage, so stale entries are correctly excluded. + section.getBiomes().getAll(holder -> chunkBiomes.add(holder.unwrapKey().orElseThrow())); } } From 7c455649794911289c838f366d89773cffaff2f1 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 23 May 2026 16:32:56 -0400 Subject: [PATCH 09/34] Fix potential stronghold cache corruption if player exits world too quickly --- .../ChunkGeneratorMixin.java | 19 ++++++++++++------- .../cache_strongholds/ServerLevelMixin.java | 2 +- .../modernfix/duck/IChunkGenerator.java | 4 ++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java index 65d5b3838..0b865124b 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java @@ -4,9 +4,9 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import net.minecraft.Util; import net.minecraft.core.Holder; -import net.minecraft.core.RegistryAccess; import net.minecraft.nbt.*; import net.minecraft.resources.RegistryOps; +import net.minecraft.server.MinecraftServer; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; @@ -41,22 +41,23 @@ public class ChunkGeneratorMixin implements IChunkGenerator { private BiomeSource biomeSource; private Path mfix$dimensionPath; - private RegistryAccess.Frozen mfix$registryAccess; + private MinecraftServer mfix$server; + private SoftReference>> mfix$cachedPositions = new SoftReference<>(null); private static final String CACHE_FILENAME = "mfix_stronghold_cache_v2.nbt"; @Override - public void mfix$setStrongholdCachePath(Path cachePath, RegistryAccess.Frozen registryAccess) { + public void mfix$setStrongholdCachePath(Path cachePath, MinecraftServer server) { this.mfix$dimensionPath = cachePath; - this.mfix$registryAccess = registryAccess; + this.mfix$server = server; } @WrapMethod(method = "generateRingPositions") private CompletableFuture> modernfix$cacheRingPositions(Holder structureSet, ConcentricRingsStructurePlacement placement, Operation>> original) { - if (this.mfix$registryAccess == null || this.mfix$dimensionPath == null) { + if (this.mfix$server == null || this.mfix$dimensionPath == null) { return original.call(structureSet, placement); } @@ -69,14 +70,18 @@ public class ChunkGeneratorMixin implements IChunkGenerator { return CompletableFuture.completedFuture(List.copyOf(cached)); } + var server = this.mfix$server; return original.call(structureSet, placement).thenApplyAsync(positions -> { - mfix$writeToCache(cacheKey, positions); + // Skip write if server exited before we finished + if (server.isRunning()) { + mfix$writeToCache(cacheKey, positions); + } return positions; }, Util.ioPool()); } private String mfix$makeCacheKey(ConcentricRingsStructurePlacement placement) { - RegistryOps ops = RegistryOps.create(NbtOps.INSTANCE, this.mfix$registryAccess); + RegistryOps ops = RegistryOps.create(NbtOps.INSTANCE, this.mfix$server.registryAccess()); String placementKey = ConcentricRingsStructurePlacement.CODEC.encodeStart(ops, placement) .result().map(Tag::toString).orElse(null); String biomeSourceKey = BiomeSource.CODEC.encodeStart(ops, this.biomeSource) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java index 8505303e8..dcd2b2f87 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java @@ -24,7 +24,7 @@ private void setCachePath(ChunkGeneratorStructureState instance, Operation @Local(ordinal = 0, argsOnly = true) LevelStorageSource.LevelStorageAccess levelStorageAccess, @Local(ordinal = 0, argsOnly = true) ResourceKey dimension, @Local(ordinal = 0, argsOnly = true) MinecraftServer server) { - ((IChunkGenerator)instance).mfix$setStrongholdCachePath(levelStorageAccess.getDimensionPath(dimension), server.registryAccess()); + ((IChunkGenerator)instance).mfix$setStrongholdCachePath(levelStorageAccess.getDimensionPath(dimension), server); original.call(instance); } } diff --git a/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java b/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java index 3cf83acc8..312c5b441 100644 --- a/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java +++ b/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java @@ -1,9 +1,9 @@ package org.embeddedt.modernfix.duck; -import net.minecraft.core.RegistryAccess; +import net.minecraft.server.MinecraftServer; import java.nio.file.Path; public interface IChunkGenerator { - void mfix$setStrongholdCachePath(Path cachePath, RegistryAccess.Frozen registryAccess); + void mfix$setStrongholdCachePath(Path cachePath, MinecraftServer server); } From b62eb1845b978200d3f51494d08c9fb3f10c4854 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 23 May 2026 16:43:56 -0400 Subject: [PATCH 10/34] Avoid blocking chunk generation on concentric rings calculation where possible --- ...oncentricRingsStructurePlacementMixin.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ConcentricRingsStructurePlacementMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ConcentricRingsStructurePlacementMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ConcentricRingsStructurePlacementMixin.java new file mode 100644 index 000000000..79aa608f8 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ConcentricRingsStructurePlacementMixin.java @@ -0,0 +1,123 @@ +package org.embeddedt.modernfix.common.mixin.perf.cache_strongholds; + +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; +import org.embeddedt.modernfix.annotation.FeatureLevel; +import org.embeddedt.modernfix.annotation.RequiresFeatureLevel; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ConcentricRingsStructurePlacement.class) +@RequiresFeatureLevel(FeatureLevel.BETA) +public class ConcentricRingsStructurePlacementMixin { + + @Shadow @Final private int distance; + @Shadow @Final private int spread; + @Shadow @Final private int count; + + /** + * Maximum per-axis section displacement from the initial ring chunk after biome snapping. + * + * Vanilla calls findBiomeHorizontal with radius=112 blocks. In quart space this is ±28, + * and converting the selected quart back to section coordinates yields at most ±7 chunks + * per axis from the original (initialX, initialZ). + */ + @Unique private static final int MFIX_MAX_BIOME_SNAP_SECTIONS_PER_AXIS = 7; + /** + * Worst-case Euclidean error introduced by rounding: + * initialX/Z = round(cos(angle) * dist), round(sin(angle) * dist). + */ + @Unique private static final double MFIX_MAX_ROUNDING_ERROR = Math.sqrt(2.0) * 0.5; + /** + * Worst-case Euclidean biome-snap displacement when each axis can move by at most 7 chunks. + */ + @Unique private static final double MFIX_MAX_BIOME_SNAP_ERROR = MFIX_MAX_BIOME_SNAP_SECTIONS_PER_AXIS * Math.sqrt(2.0); + /** + * Total conservative positional slack (rounding + biome snap) applied to radial bounds. + */ + @Unique private static final double MFIX_MAX_POSITION_ERROR = MFIX_MAX_ROUNDING_ERROR + MFIX_MAX_BIOME_SNAP_ERROR; + + /** Squared chunk-distance below which no ring position can ever land. */ + @Unique private long mfix$innerRadiusSq; + /** Squared chunk-distance above which no ring position can ever land. */ + @Unique private long mfix$outerRadiusSq; + + /** + * Precomputes conservative radial bounds for vanilla's ring placement distance: + * {@code dist = 4*i + i*i1*6 + noise}, where {@code i=distance} and {@code i1=circle}. + * + * - Inner bound uses the minimum possible base term ({@code i1=0} => {@code 4*i}). + * - Outer bound uses the maximum reachable {@code i1} for this ({@code spread,count}) pair. + * + * Both bounds are expanded by {@link #MFIX_MAX_POSITION_ERROR} so we never reject a valid + * chunk produced by rounding and biome snapping. + */ + @Inject( + method = "(Lnet/minecraft/core/Vec3i;Lnet/minecraft/world/level/levelgen/structure/placement/StructurePlacement$FrequencyReductionMethod;FILjava/util/Optional;IIILnet/minecraft/core/HolderSet;)V", + at = @At("RETURN") + ) + private void mfix$computeRadiusBounds(CallbackInfo ci) { + double maxNoise = this.distance * 1.25; // (nextDouble() - 0.5) * (distance * 2.5) + + // min(dist): 4*i + i*0*6 - maxNoise + double minDist = 4.0 * this.distance - maxNoise; + double safeInnerRadius = minDist - MFIX_MAX_POSITION_ERROR; + this.mfix$innerRadiusSq = (long)Math.max(0.0, Math.floor(safeInnerRadius * safeInnerRadius)); + + if (this.spread == 0) { + // Vanilla behavior becomes non-finite here (angle += 2π / 0), so keep only inner rejection. + this.mfix$outerRadiusSq = Long.MAX_VALUE; + return; + } + + int maxCircle = this.mfix$computeMaxCircleIndex(); + // max(dist): 4*i + i*maxCircle*6 + maxNoise + double maxDist = 4.0 * this.distance + (double)this.distance * maxCircle * 6.0 + maxNoise; + double safeOuterRadius = maxDist + MFIX_MAX_POSITION_ERROR; + this.mfix$outerRadiusSq = (long)Math.ceil(safeOuterRadius * safeOuterRadius); + } + + /** + * Computes the highest ring index ({@code circle}) that vanilla can reach for this placement. + * + * This mirrors the spread/total update logic in + * {@link net.minecraft.world.level.chunk.ChunkGeneratorStructureState#generateRingPositions}, + * but only tracks deterministic loop state (no RNG). + */ + @Unique + private int mfix$computeMaxCircleIndex() { + int ringSpread = this.spread; + int total = 0; + int circle = 0; + + while (total + ringSpread < this.count) { + total += ringSpread; + circle++; + ringSpread += 2 * ringSpread / (circle + 1); + ringSpread = Math.min(ringSpread, this.count - total); + } + + return circle; + } + + /** + * @author embeddedt, GPT-5.3-Codex + * @reason Avoid calling getRingPositionsFor() when we know the current chunk lies outside the region where + * concentric placement can even happen. This is particularly helpful when creating new worlds, because we can + * avoid blocking on the slow noise computations within the spawn region around (0, 0). + */ + @Inject(method = "isPlacementChunk", at = @At("HEAD"), cancellable = true) + private void mfix$earlyRejectByRadius(ChunkGeneratorStructureState structureState, int x, int z, + CallbackInfoReturnable cir) { + long distSq = (long)x * x + (long)z * z; + if (distSq < this.mfix$innerRadiusSq || distSq > this.mfix$outerRadiusSq) { + cir.setReturnValue(false); + } + } +} From 538c52bc2a0011406bf4a0e1a89e2daf20c3faa2 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 23 May 2026 17:00:08 -0400 Subject: [PATCH 11/34] Run stronghold gen on dedicated thread pool --- .../ChunkGeneratorMixin.java | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java index 0b865124b..de35aa17a 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java @@ -2,6 +2,8 @@ import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; import net.minecraft.Util; import net.minecraft.core.Holder; import net.minecraft.nbt.*; @@ -17,6 +19,8 @@ import org.spongepowered.asm.mixin.Final; 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.Redirect; import java.lang.ref.SoftReference; import java.nio.charset.StandardCharsets; @@ -29,6 +33,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; @Mixin(ChunkGeneratorStructureState.class) public class ChunkGeneratorMixin implements IChunkGenerator { @@ -55,8 +61,9 @@ public class ChunkGeneratorMixin implements IChunkGenerator { @WrapMethod(method = "generateRingPositions") private CompletableFuture> modernfix$cacheRingPositions(Holder structureSet, - ConcentricRingsStructurePlacement placement, - Operation>> original) { + ConcentricRingsStructurePlacement placement, + Operation>> original, + @Share("threadPool") LocalRef threadPoolRef) { if (this.mfix$server == null || this.mfix$dimensionPath == null) { return original.call(structureSet, placement); } @@ -71,13 +78,30 @@ public class ChunkGeneratorMixin implements IChunkGenerator { } var server = this.mfix$server; - return original.call(structureSet, placement).thenApplyAsync(positions -> { - // Skip write if server exited before we finished - if (server.isRunning()) { - mfix$writeToCache(cacheKey, positions); - } - return positions; - }, Util.ioPool()); + ExecutorService strongholdPool = Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 2)); + threadPoolRef.set(strongholdPool); + try { + return original.call(structureSet, placement).thenApplyAsync(positions -> { + // Skip write if server exited before we finished + if (server.isRunning()) { + mfix$writeToCache(cacheKey, positions); + } + return positions; + }, Util.ioPool()); + } finally { + strongholdPool.shutdown(); + } + } + + /** + * @author embeddedt + * @reason Ring position calculation is often not required for initial chunk generation, but the tasks still occupy + * CPU time on the main worker pool and prevent higher priority work from progressing. To fix this we use a + * dedicated pool. + */ + @Redirect(method = "generateRingPositions", at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;backgroundExecutor()Ljava/util/concurrent/ExecutorService;")) + private ExecutorService useDedicatedService(@Share("threadPool") LocalRef threadPoolRef) { + return threadPoolRef.get(); } private String mfix$makeCacheKey(ConcentricRingsStructurePlacement placement) { From 62dbbea083d52adfb4ac194aa290ed46e310abb8 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 23 May 2026 21:23:26 -0400 Subject: [PATCH 12/34] Optimize ZIP resource packs significantly --- .../resourcepacks/FilePackResourcesMixin.java | 95 ++++++ .../modernfix/resources/ZipPackIndex.java | 311 ++++++++++++++++++ 2 files changed, 406 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/FilePackResourcesMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/FilePackResourcesMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/FilePackResourcesMixin.java new file mode 100644 index 000000000..2153f6dfe --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/FilePackResourcesMixin.java @@ -0,0 +1,95 @@ +package org.embeddedt.modernfix.common.mixin.perf.resourcepacks; + +import net.minecraft.server.packs.FilePackResources; +import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.PackType; +import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.annotation.FeatureLevel; +import org.embeddedt.modernfix.annotation.RequiresFeatureLevel; +import org.embeddedt.modernfix.resources.ZipPackIndex; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.io.File; +import java.io.IOException; +import java.util.Set; +import java.util.zip.ZipFile; + +@Mixin(FilePackResources.class) +@RequiresFeatureLevel(FeatureLevel.BETA) +public class FilePackResourcesMixin { + @Final + @Shadow private File file; + + @Shadow @Nullable private ZipFile getOrCreateZipFile() { return null; } + + @Unique + @Nullable + private volatile ZipPackIndex mf$packIndex; + + @Unique + @Nullable + private ZipPackIndex mf$getOrCreateIndex() { + var index = mf$packIndex; + if (index == null) { + synchronized (this) { + index = mf$packIndex; + if (index == null) { + // Ensure the ZipFile is open first; if it fails, getOrCreateZipFile returns null. + if (getOrCreateZipFile() == null) { + return null; + } + try { + mf$packIndex = index = new ZipPackIndex(file.toPath()); + } catch (IOException e) { + ModernFix.LOGGER.error("Failed to build zip index for {}", file, e); + } + } + } + } + return index; + } + + /** + * @author embeddedt + * @reason use the index instead of scanning the whole zip + */ + @Inject(method = "getNamespaces", at = @At("HEAD"), cancellable = true) + private void mf$getNamespaces(PackType type, CallbackInfoReturnable> cir) { + ZipPackIndex index = mf$getOrCreateIndex(); + if (index != null) { + cir.setReturnValue(index.getNamespaces(type)); + } + } + + /** + * @author embeddedt + * @reason use the index instead of scanning the whole zip + */ + @Inject(method = "listResources", at = @At("HEAD"), cancellable = true) + private void mf$listResources(PackType packType, String namespace, String path, + PackResources.ResourceOutput resourceOutput, CallbackInfo ci) { + ZipFile zf = getOrCreateZipFile(); + ZipPackIndex index = mf$getOrCreateIndex(); + if (index != null && zf != null) { + index.listResources(packType, namespace, path, zf, resourceOutput); + ci.cancel(); + } + } + + /** + * Drop the index when the pack is closed so it can be rebuilt cleanly if the + * pack is ever re-opened. + */ + @Inject(method = "close", at = @At("HEAD")) + private void mf$invalidateIndex(CallbackInfo ci) { + mf$packIndex = null; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java b/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java new file mode 100644 index 000000000..813d2d4fe --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java @@ -0,0 +1,311 @@ +package org.embeddedt.modernfix.resources; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.IoSupplier; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * An index over a zip file's central directory that allows efficient namespace listing + * and resource enumeration without iterating all entries on every call. + * + *

The index is built once at construction time by memory-mapping the zip's central + * directory and parsing it into a {@link DirNode} tree. All subsequent queries run in + * O(depth + k) time where k is the number of matching results. + * + *

The caller is responsible for opening and closing the {@link ZipFile}; this class + * only holds a read-only view of the zip's metadata via a mmap'd buffer. + */ +public class ZipPackIndex { + + // ------------------------------------------------------------------------- + // Zip structural constants (identical to EfficientZipFileSystem in blacksmith) + // ------------------------------------------------------------------------- + + private static final int EOCD_SIGNATURE = 0x06054b50; + private static final int EOCD_SIZE = 22; + private static final int EOCD_OFF_CD_SIZE = 12; + private static final int EOCD_OFF_CD_OFFSET = 16; + private static final int EOCD_MAX_COMMENT_LENGTH = 65535; + + private static final int CD_ENTRY_SIGNATURE = 0x02014b50; + private static final int CD_ENTRY_HEADER_SIZE = 46; + private static final int CD_OFF_FILENAME_LENGTH = 28; + private static final int CD_OFF_EXTRA_LENGTH = 30; + private static final int CD_OFF_COMMENT_LENGTH = 32; + + private static final int[] EMPTY_OFFSETS = new int[0]; + + // ------------------------------------------------------------------------- + // DirNode + // ------------------------------------------------------------------------- + + static final class DirNode { + Map childDirs; + int[] fileChildOffsets; // offsets into cdBuffer for each direct file child + + DirNode() { + childDirs = new HashMap<>(); + fileChildOffsets = EMPTY_OFFSETS; + } + + void freeze() { + childDirs = childDirs.isEmpty() ? Map.of() : Map.copyOf(childDirs); + for (DirNode child : childDirs.values()) { + child.freeze(); + } + } + } + + // ------------------------------------------------------------------------- + // Fields + // ------------------------------------------------------------------------- + + /** Memory-mapped central directory. May be null for empty/invalid zips. */ + private final MappedByteBuffer cdBuffer; + /** Root of the directory tree, always non-null (may be empty but frozen). */ + private final DirNode root; + + // ------------------------------------------------------------------------- + // Construction + // ------------------------------------------------------------------------- + + /** + * Build an index from the zip at the given path. Does not open a {@link ZipFile} + * and does not keep a reference to one; the caller owns all {@link ZipFile} lifecycle. + * + * @throws IOException if the file cannot be read or its central directory cannot be mapped + */ + public ZipPackIndex(Path zipPath) throws IOException { + this.cdBuffer = mmapCentralDirectory(zipPath); + this.root = buildTree(); + } + + private static MappedByteBuffer mmapCentralDirectory(Path filePath) throws IOException { + try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ)) { + long fileSize = channel.size(); + if (fileSize < EOCD_SIZE) return null; + + int tailSize = (int) Math.min(fileSize, (long) EOCD_SIZE + EOCD_MAX_COMMENT_LENGTH); + ByteBuffer tail = ByteBuffer.allocate(tailSize); + tail.order(ByteOrder.LITTLE_ENDIAN); + + long tailStart = fileSize - tailSize; + while (tail.hasRemaining()) { + int n = channel.read(tail, tailStart + tail.position()); + if (n < 0) { + break; + } + } + if (tail.hasRemaining()) { + throw new IOException("Failed to read ZIP tail"); + } + tail.flip(); + + // Scan backwards for the EOCD signature and validate comment length. + int eocdPos = -1; + for (int i = tailSize - EOCD_SIZE; i >= 0; i--) { + if (tail.getInt(i) == EOCD_SIGNATURE) { + int commentLen = Short.toUnsignedInt(tail.getShort(i + 20)); + if (i + EOCD_SIZE + commentLen == tailSize) { + eocdPos = i; + break; + } + } + } + if (eocdPos < 0) return null; + + long cdSize = Integer.toUnsignedLong(tail.getInt(eocdPos + EOCD_OFF_CD_SIZE)); + long cdOffset = Integer.toUnsignedLong(tail.getInt(eocdPos + EOCD_OFF_CD_OFFSET)); + if (cdSize == 0) return null; + if (cdSize == 0xFFFFFFFFL || cdOffset == 0xFFFFFFFFL) { + throw new IOException("ZIP64 not supported by ZipPackIndex"); + } + if (cdOffset > fileSize - cdSize) { + throw new IOException("Invalid central directory range"); + } + + try { + MappedByteBuffer buf = channel.map(FileChannel.MapMode.READ_ONLY, cdOffset, cdSize); + buf.order(ByteOrder.LITTLE_ENDIAN); + return buf; + } catch (RuntimeException e) { + throw new IOException("Failed to map central directory", e); + } + } + } + + private DirNode buildTree() throws IOException { + DirNode treeRoot = new DirNode(); + if (cdBuffer == null) { + treeRoot.freeze(); + return treeRoot; + } + + // Accumulate file offsets per DirNode before compacting to int[] + IdentityHashMap> fileOffsets = new IdentityHashMap<>(); + + int pos = 0; + int limit = cdBuffer.limit(); + while (pos + CD_ENTRY_HEADER_SIZE <= limit) { + if (cdBuffer.getInt(pos) != CD_ENTRY_SIGNATURE) break; + + int fileNameLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_FILENAME_LENGTH)); + int extraLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_EXTRA_LENGTH)); + int commentLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_COMMENT_LENGTH)); + int recordLen = CD_ENTRY_HEADER_SIZE + fileNameLen + extraLen + commentLen; + if (pos + recordLen > limit) { + throw new IOException("Truncated central directory"); + } + + byte[] nameBytes = new byte[fileNameLen]; + cdBuffer.get(pos + CD_ENTRY_HEADER_SIZE, nameBytes); + String name = new String(nameBytes, StandardCharsets.UTF_8); + + boolean isDirectory = name.endsWith("/"); + if (isDirectory) name = name.substring(0, name.length() - 1); + + if (!name.isEmpty()) { + String[] parts = name.split("/"); + DirNode current = treeRoot; + int dirDepth = isDirectory ? parts.length : parts.length - 1; + for (int i = 0; i < dirDepth; i++) { + current = current.childDirs.computeIfAbsent(parts[i], k -> new DirNode()); + } + if (!isDirectory) { + fileOffsets.computeIfAbsent(current, k -> new ArrayList<>()).add(pos); + } + } + + pos += recordLen; + } + + // Compact to int[] arrays + fileOffsets.forEach((node, offsets) -> { + int[] arr = new int[offsets.size()]; + for (int i = 0; i < arr.length; i++) arr[i] = offsets.get(i); + node.fileChildOffsets = arr; + }); + + treeRoot.freeze(); + return treeRoot; + } + + // ------------------------------------------------------------------------- + // CD buffer reads — absolute-position gets are thread-safe on Java 13+ + // ------------------------------------------------------------------------- + + /** + * Extract the basename (the portion after the last '/') of the entry whose + * central-directory record starts at {@code cdOffset}. + */ + String readBasename(int cdOffset) { + int nameLen = Short.toUnsignedInt(cdBuffer.getShort(cdOffset + CD_OFF_FILENAME_LENGTH)); + byte[] nameBytes = new byte[nameLen]; + cdBuffer.get(cdOffset + CD_ENTRY_HEADER_SIZE, nameBytes); + int lastSlash = -1; + for (int i = nameBytes.length - 1; i >= 0; i--) { + if (nameBytes[i] == '/') { lastSlash = i; break; } + } + return new String(nameBytes, lastSlash + 1, nameLen - lastSlash - 1, StandardCharsets.UTF_8); + } + + // ------------------------------------------------------------------------- + // Public API + // ------------------------------------------------------------------------- + + /** + * Returns all namespaces present under the given pack type directory. + * + *

Equivalent to {@code FilePackResources.getNamespaces(type)} but reads from + * the pre-built tree rather than scanning all zip entries. + */ + public Set getNamespaces(PackType type) { + DirNode typeNode = root.childDirs.get(type.getDirectory()); + if (typeNode == null) return Set.of(); + Set result = new HashSet<>(); + for (String ns : typeNode.childDirs.keySet()) { + if (ns.equals(ns.toLowerCase(Locale.ROOT))) { + result.add(ns); + } + } + return result; + } + + /** + * Enumerate all resources under {@code type/namespace/path/} and deliver them + * to {@code output}. + * + *

Equivalent to {@code FilePackResources.listResources(type, namespace, path, output)} + * but uses the pre-built tree for O(k) traversal instead of a full zip scan. + * + * @param zipFile the open zip file, used only to supply {@link InputStream}s on demand; + * the caller retains ownership of its lifecycle + */ + public void listResources(PackType type, String namespace, String path, + ZipFile zipFile, PackResources.ResourceOutput output) { + DirNode node = root.childDirs.get(type.getDirectory()); + if (node == null) return; + node = node.childDirs.get(namespace); + if (node == null) return; + + // Walk to the requested sub-path + String rlSubPath; + if (!path.isEmpty()) { + for (String segment : path.split("/")) { + if (segment.isEmpty()) continue; + node = node.childDirs.get(segment); + if (node == null) return; + } + rlSubPath = path + "/"; + } else { + rlSubPath = ""; + } + + // entryPrefix = the part of the zip entry name before the ResourceLocation path + String entryPrefix = type.getDirectory() + "/" + namespace + "/"; + collectResources(node, entryPrefix, rlSubPath, zipFile, namespace, output); + } + + /** + * Recursively walk {@code node}, reconstructing zip entry names as we go and + * emitting each file to {@code output}. + * + * @param entryPrefix the constant prefix before the RL path, e.g. {@code "assets/minecraft/"} + * @param rlSubPath the RL-relative path accumulated so far, e.g. {@code "textures/block/"} + */ + private void collectResources(DirNode node, String entryPrefix, String rlSubPath, + ZipFile zipFile, String namespace, + PackResources.ResourceOutput output) { + // Emit direct file children of this node + for (int cdOffset : node.fileChildOffsets) { + String basename = readBasename(cdOffset); + String rlPathFull = rlSubPath + basename; + ResourceLocation rl = ResourceLocation.tryBuild(namespace, rlPathFull); + if (rl != null) { + ZipEntry entry = zipFile.getEntry(entryPrefix + rlPathFull); + if (entry != null) { + output.accept(rl, IoSupplier.create(zipFile, entry)); + } + } + } + // Recurse into subdirectories + for (Map.Entry child : node.childDirs.entrySet()) { + collectResources(child.getValue(), entryPrefix, + rlSubPath + child.getKey() + "/", zipFile, namespace, output); + } + } +} From 74f76f7305fee265858eac4d29ba4aae6322207e Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 23 May 2026 21:44:14 -0400 Subject: [PATCH 13/34] Improvements to ZipPackIndex - Allow it to work on channels that don't support mapping - Skip indexing folders that are not part of a pack type --- .../modernfix/resources/ZipPackIndex.java | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java b/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java index 813d2d4fe..d0a4eb924 100644 --- a/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java +++ b/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java @@ -9,7 +9,6 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -74,8 +73,8 @@ void freeze() { // Fields // ------------------------------------------------------------------------- - /** Memory-mapped central directory. May be null for empty/invalid zips. */ - private final MappedByteBuffer cdBuffer; + /** Central directory buffer (memory-mapped or heap-allocated fallback). May be null for empty/invalid zips. */ + private final ByteBuffer cdBuffer; /** Root of the directory tree, always non-null (may be empty but frozen). */ private final DirNode root; @@ -87,14 +86,14 @@ void freeze() { * Build an index from the zip at the given path. Does not open a {@link ZipFile} * and does not keep a reference to one; the caller owns all {@link ZipFile} lifecycle. * - * @throws IOException if the file cannot be read or its central directory cannot be mapped + * @throws IOException if the file cannot be read or its central directory cannot be parsed */ public ZipPackIndex(Path zipPath) throws IOException { - this.cdBuffer = mmapCentralDirectory(zipPath); + this.cdBuffer = readCentralDirectory(zipPath); this.root = buildTree(); } - private static MappedByteBuffer mmapCentralDirectory(Path filePath) throws IOException { + private static ByteBuffer readCentralDirectory(Path filePath) throws IOException { try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ)) { long fileSize = channel.size(); if (fileSize < EOCD_SIZE) return null; @@ -138,13 +137,24 @@ private static MappedByteBuffer mmapCentralDirectory(Path filePath) throws IOExc throw new IOException("Invalid central directory range"); } + // Try memory-mapping first; fall back to a heap copy if the OS refuses. try { - MappedByteBuffer buf = channel.map(FileChannel.MapMode.READ_ONLY, cdOffset, cdSize); + ByteBuffer buf = channel.map(FileChannel.MapMode.READ_ONLY, cdOffset, cdSize); buf.order(ByteOrder.LITTLE_ENDIAN); return buf; - } catch (RuntimeException e) { - throw new IOException("Failed to map central directory", e); + } catch (Exception ignored) { + // mmap unavailable (e.g. some Linux mount flags, container restrictions); + // read the central directory into a heap buffer instead. } + + ByteBuffer buf = ByteBuffer.allocate((int) cdSize); + buf.order(ByteOrder.LITTLE_ENDIAN); + while (buf.hasRemaining()) { + int n = channel.read(buf, cdOffset + buf.position()); + if (n < 0) throw new IOException("Truncated central directory during heap read"); + } + buf.flip(); + return buf; } } @@ -155,6 +165,11 @@ private DirNode buildTree() throws IOException { return treeRoot; } + // Computed here (not statically) so that any loader-injected PackType values + // registered after class-load are included. + Set packTypeDirs = new HashSet<>(); + for (PackType type : PackType.values()) packTypeDirs.add(type.getDirectory()); + // Accumulate file offsets per DirNode before compacting to int[] IdentityHashMap> fileOffsets = new IdentityHashMap<>(); @@ -180,6 +195,10 @@ private DirNode buildTree() throws IOException { if (!name.isEmpty()) { String[] parts = name.split("/"); + if (!packTypeDirs.contains(parts[0])) { + pos += recordLen; + continue; + } DirNode current = treeRoot; int dirDepth = isDirectory ? parts.length : parts.length - 1; for (int i = 0; i < dirDepth; i++) { From 494203ef5af2669230bee9129d97ba4ee4e57a87 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 24 May 2026 19:45:24 -0400 Subject: [PATCH 14/34] Fix potential crash during worldgen with release_protochunks enabled The crash can occur if a protochunk next to a FULL chunk is dropped, and then later re-requested. If it was not persisted to disk for any reason, it starts regeneration from scratch. At FEATURES stage, it may try to place blocks into the adjacent LevelChunk already in the world. The fix is to prevent this situation from even happening by pinning protochunks directly next to FULL chunks, and preventing them from unloading. --- .../mixin/perf/release_protochunks/ChunkHolderMixin.java | 3 +-- .../mixin/perf/release_protochunks/ChunkMapMixin.java | 3 +-- .../duck/release_protochunks/IClearableChunkHolder.java | 8 ++++++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java index b66ff4dc4..56cc5efb6 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java @@ -4,7 +4,6 @@ import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkLevel; import net.minecraft.server.level.ChunkMap; -import net.minecraft.server.level.FullChunkStatus; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.chunk.ChunkAccess; import org.embeddedt.modernfix.duck.release_protochunks.IClearableChunkHolder; @@ -85,7 +84,7 @@ private void markForSuspensionOnDemotion(ChunkMap chunkMap, Executor executor, C } private void mfix$markAsNeedingProtoChunkDrop() { - if (!ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL) + if (this.ticketLevel >= LOWEST_DROPPABLE_TICKET_LEVEL && ChunkLevel.isLoaded(this.ticketLevel)) { // register for suspension check when chain completes var map = ((ISuspendedHolderTrackingChunkMap)this.playerProvider); diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java index 916f73259..2ec129831 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java @@ -8,7 +8,6 @@ import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkLevel; import net.minecraft.server.level.ChunkMap; -import net.minecraft.server.level.FullChunkStatus; import net.minecraft.util.thread.BlockableEventLoop; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.chunk.ChunkAccess; @@ -68,7 +67,7 @@ private void dropProtoChunks(BooleanSupplier hasMoreTime, CallbackInfo ci) { long pos = entry.getLongKey(); ChunkHolder holder = this.updatingChunkMap.get(pos); if (holder == null // already removed - || ChunkLevel.fullStatus(holder.getTicketLevel()).isOrAfter(FullChunkStatus.FULL) // promoted to FULL + || holder.getTicketLevel() < IClearableChunkHolder.LOWEST_DROPPABLE_TICKET_LEVEL // promoted to FULL or adjacent to FULL chunk || !ChunkLevel.isLoaded(holder.getTicketLevel()) // is going to be dropped through normal code path ) { dropIterator.remove(); diff --git a/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java b/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java index b6910cf6d..5ba3c06ee 100644 --- a/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java +++ b/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java @@ -1,8 +1,16 @@ package org.embeddedt.modernfix.duck.release_protochunks; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.FullChunkStatus; + import java.util.concurrent.atomic.AtomicInteger; public interface IClearableChunkHolder { + /** + * We don't want to drop FULL chunks, or chunks immediately surrouding FULL. So + 2 is the minimum we can drop. + */ + int LOWEST_DROPPABLE_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.FULL) + 2; + void mfix$resetProtoChunkFutures(); AtomicInteger mfix$getGenerationRefCount(); From 33851c1cb640ed17ae94ad25d38635606f55a5c2 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 24 May 2026 23:08:07 -0400 Subject: [PATCH 15/34] Fix ImposterProtoChunk leaking live block entities to worldgen --- .../ImposterProtoChunkMixin.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ImposterProtoChunkMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ImposterProtoChunkMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ImposterProtoChunkMixin.java new file mode 100644 index 000000000..e97d27d37 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ImposterProtoChunkMixin.java @@ -0,0 +1,46 @@ +package org.embeddedt.modernfix.common.mixin.perf.release_protochunks; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import org.embeddedt.modernfix.ModernFix; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ImposterProtoChunk.class) +public class ImposterProtoChunkMixin { + @Shadow + @Final + private boolean allowWrites; + + /** + * @author embeddedt + * @reason This is a workaround for a very complicated and subtle vanilla issue. Vanilla uses ImposterProtoChunk as + * a way of exposing fully generated chunks to other chunks that are still working on earlier generation stages. + * The problem is that these fully generated chunks may be in two different states: promoted to FULL and already + * visible to the level (with real BlockEntity objects), or in a loaded but not yet promoted state, where the postload + * hook has not yet run to convert the NBT-serialized block entities from the disk into real BlockEntity objects. + * The former state is the problematic one. If such a chunk is exposed to worldgen, features/structures may try + * to interact with the block entity (e.g. by calling setChanged on it). This has the potential to deadlock. + *

+ * The solution we use here is to simply hide the existence of any "real" BE that has a level attached from worldgen. + * This is consistent with what other code would observe if the fully generated chunk were to be saved to disk + * and then reloaded (ending up in the latter state), so it should not break well-behaved mods. + *

+ * This problem occurs rather often with `mixin.perf.release_protochunks` enabled, because it significantly increases + * the chance of a promoted LevelChunk wrapped in ImposterProtoChunk being used for world generation. + */ + @ModifyExpressionValue(method = "getBlockEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunk;getBlockEntity(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/entity/BlockEntity;")) + private BlockEntity avoidLeakingLiveBE(BlockEntity original, @Local(ordinal = 0, argsOnly = true) BlockPos pos) { + if (!this.allowWrites && original != null && original.getLevel() != null) { + ModernFix.LOGGER.debug("Blocked accessing the main level BlockEntity at {} from the ImposterProtoChunk wrapper, as this is unsafe during worldgen.", pos, new Exception("Stacktrace")); + return null; + } else { + return original; + } + } +} From fb9dcf77c69315b9831a7de58565c453da4c6bb4 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 28 May 2026 22:20:28 -0400 Subject: [PATCH 16/34] Improve ZipPackIndex --- .../modernfix/resources/ZipPackIndex.java | 147 ++++++++++++------ 1 file changed, 98 insertions(+), 49 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java b/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java index d0a4eb924..88b4dad2d 100644 --- a/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java +++ b/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java @@ -1,5 +1,8 @@ package org.embeddedt.modernfix.resources; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.PackResources; import net.minecraft.server.packs.PackType; @@ -46,7 +49,7 @@ public class ZipPackIndex { private static final int CD_OFF_EXTRA_LENGTH = 30; private static final int CD_OFF_COMMENT_LENGTH = 32; - private static final int[] EMPTY_OFFSETS = new int[0]; + private static final IntList EMPTY_OFFSETS = IntList.of(); // ------------------------------------------------------------------------- // DirNode @@ -54,14 +57,17 @@ public class ZipPackIndex { static final class DirNode { Map childDirs; - int[] fileChildOffsets; // offsets into cdBuffer for each direct file child + IntList fileChildOffsets; // offsets into cdBuffer for each direct file child DirNode() { - childDirs = new HashMap<>(); + childDirs = new Object2ObjectOpenHashMap<>(); fileChildOffsets = EMPTY_OFFSETS; } void freeze() { + if (fileChildOffsets instanceof IntArrayList arrayList) { + arrayList.trim(); + } childDirs = childDirs.isEmpty() ? Map.of() : Map.copyOf(childDirs); for (DirNode child : childDirs.values()) { child.freeze(); @@ -75,6 +81,8 @@ void freeze() { /** Central directory buffer (memory-mapped or heap-allocated fallback). May be null for empty/invalid zips. */ private final ByteBuffer cdBuffer; + /** Top-level directories tracked by the index. */ + private final Set trackedTopLevelDirs; /** Root of the directory tree, always non-null (may be empty but frozen). */ private final DirNode root; @@ -90,6 +98,11 @@ void freeze() { */ public ZipPackIndex(Path zipPath) throws IOException { this.cdBuffer = readCentralDirectory(zipPath); + // Computed here (not statically) so that any loader-injected PackType values + // registered after class-load are included. + Set packTypeDirs = new HashSet<>(); + for (PackType type : PackType.values()) packTypeDirs.add(type.getDirectory()); + this.trackedTopLevelDirs = Set.copyOf(packTypeDirs); this.root = buildTree(); } @@ -159,68 +172,77 @@ private static ByteBuffer readCentralDirectory(Path filePath) throws IOException } private DirNode buildTree() throws IOException { + var cdBuffer = this.cdBuffer; + DirNode treeRoot = new DirNode(); if (cdBuffer == null) { treeRoot.freeze(); return treeRoot; } - // Computed here (not statically) so that any loader-injected PackType values - // registered after class-load are included. - Set packTypeDirs = new HashSet<>(); - for (PackType type : PackType.values()) packTypeDirs.add(type.getDirectory()); - - // Accumulate file offsets per DirNode before compacting to int[] - IdentityHashMap> fileOffsets = new IdentityHashMap<>(); - int pos = 0; int limit = cdBuffer.limit(); while (pos + CD_ENTRY_HEADER_SIZE <= limit) { if (cdBuffer.getInt(pos) != CD_ENTRY_SIGNATURE) break; + pos += indexCdEntry(pos, limit, treeRoot, cdBuffer); + } - int fileNameLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_FILENAME_LENGTH)); - int extraLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_EXTRA_LENGTH)); - int commentLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_COMMENT_LENGTH)); - int recordLen = CD_ENTRY_HEADER_SIZE + fileNameLen + extraLen + commentLen; - if (pos + recordLen > limit) { - throw new IOException("Truncated central directory"); - } - - byte[] nameBytes = new byte[fileNameLen]; - cdBuffer.get(pos + CD_ENTRY_HEADER_SIZE, nameBytes); - String name = new String(nameBytes, StandardCharsets.UTF_8); + treeRoot.freeze(); + return treeRoot; + } - boolean isDirectory = name.endsWith("/"); - if (isDirectory) name = name.substring(0, name.length() - 1); + /** + * Parses the CD entry at {@code pos}, inserts it into the tree, and returns the + * number of bytes to advance {@code pos} (i.e. the full record length). + */ + private int indexCdEntry(int pos, int limit, + DirNode treeRoot, + ByteBuffer cdBuffer) throws IOException { + int fileNameLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_FILENAME_LENGTH)); + int extraLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_EXTRA_LENGTH)); + int commentLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_COMMENT_LENGTH)); + int recordLen = CD_ENTRY_HEADER_SIZE + fileNameLen + extraLen + commentLen; + if (pos + recordLen > limit) { + throw new IOException("Truncated central directory"); + } - if (!name.isEmpty()) { - String[] parts = name.split("/"); - if (!packTypeDirs.contains(parts[0])) { - pos += recordLen; - continue; - } - DirNode current = treeRoot; - int dirDepth = isDirectory ? parts.length : parts.length - 1; - for (int i = 0; i < dirDepth; i++) { - current = current.childDirs.computeIfAbsent(parts[i], k -> new DirNode()); - } - if (!isDirectory) { - fileOffsets.computeIfAbsent(current, k -> new ArrayList<>()).add(pos); + byte[] nameBytes = new byte[fileNameLen]; + cdBuffer.get(pos + CD_ENTRY_HEADER_SIZE, nameBytes); + + DirNode current = treeRoot; + boolean tracked = false; + boolean skipped = false; + int segStart = 0; + + for (int i = 0; i < fileNameLen; i++) { + if (nameBytes[i] == '/') { + int segLen = i - segStart; + if (segLen > 0) { + String segment = new String(nameBytes, segStart, segLen, StandardCharsets.UTF_8); + if (!tracked) { + if (!trackedTopLevelDirs.contains(segment)) { skipped = true; break; } + tracked = true; + } + DirNode next = current.childDirs.get(segment); + //noinspection Java8MapApi + if (next == null) { + current.childDirs.put(segment, next = new DirNode()); + } + current = next; } + segStart = i + 1; } - - pos += recordLen; } - // Compact to int[] arrays - fileOffsets.forEach((node, offsets) -> { - int[] arr = new int[offsets.size()]; - for (int i = 0; i < arr.length; i++) arr[i] = offsets.get(i); - node.fileChildOffsets = arr; - }); + // A remaining non-empty segment after the last '/' is a file basename. + if (!skipped && tracked && segStart < fileNameLen) { + if (current.fileChildOffsets == EMPTY_OFFSETS) { + current.fileChildOffsets = new IntArrayList(); + } + current.fileChildOffsets.add(pos); + } - treeRoot.freeze(); - return treeRoot; + return recordLen; } // ------------------------------------------------------------------------- @@ -246,6 +268,10 @@ String readBasename(int cdOffset) { // Public API // ------------------------------------------------------------------------- + public Set getTrackedTopLevelDirs() { + return this.trackedTopLevelDirs; + } + /** * Returns all namespaces present under the given pack type directory. * @@ -264,6 +290,28 @@ public Set getNamespaces(PackType type) { return result; } + public boolean hasResource(String... paths) { + var node = this.root; + for (int i = 0; i < paths.length - 1; i++) { + var path = paths[i]; + if (path.isEmpty()) { + continue; + } + node = node.childDirs.get(path); + if (node == null) { + return false; + } + } + String basename = paths[paths.length - 1]; + var offsets = node.fileChildOffsets; + for (int i = 0; i < offsets.size(); i++) { + if (basename.equals(readBasename(offsets.getInt(i)))) { + return true; + } + } + return false; + } + /** * Enumerate all resources under {@code type/namespace/path/} and deliver them * to {@code output}. @@ -310,8 +358,9 @@ private void collectResources(DirNode node, String entryPrefix, String rlSubPath ZipFile zipFile, String namespace, PackResources.ResourceOutput output) { // Emit direct file children of this node - for (int cdOffset : node.fileChildOffsets) { - String basename = readBasename(cdOffset); + var offsets = node.fileChildOffsets; + for (int i = 0; i < offsets.size(); i++) { + String basename = readBasename(offsets.getInt(i)); String rlPathFull = rlSubPath + basename; ResourceLocation rl = ResourceLocation.tryBuild(namespace, rlPathFull); if (rl != null) { From e9bfd96dd9b997a5a9c468d9e164b4d144cfec12 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 28 May 2026 22:33:03 -0400 Subject: [PATCH 17/34] Fix Forge pack finder being injected multiple times into pack repository --- .../resourcepacks/MinecraftServerMixin.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/MinecraftServerMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/MinecraftServerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/MinecraftServerMixin.java new file mode 100644 index 000000000..d78e8221a --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/MinecraftServerMixin.java @@ -0,0 +1,30 @@ +package org.embeddedt.modernfix.common.mixin.perf.resourcepacks; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.repository.RepositorySource; +import net.minecraftforge.forgespi.locating.IModFile; +import net.minecraftforge.resource.PathPackResources; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.function.Function; + +@Mixin(MinecraftServer.class) +public class MinecraftServerMixin { + private static final Set MFIX$INJECTED_REPOSITORIES = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>())); + + /** + * @author embeddedt + * @reason we do not want to inject the Forge pack finder more than once to any given repository + */ + @WrapWithCondition(method = "configurePackRepository", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/resource/ResourcePackLoader;loadResourcePacks(Lnet/minecraft/server/packs/repository/PackRepository;Ljava/util/function/Function;)V")) + private static boolean skipInjectIfAlreadyInjected(PackRepository resourcePacks, Function, ? extends RepositorySource> packFinder) { + return MFIX$INJECTED_REPOSITORIES.add(resourcePacks); + } +} From 0ecee529d7cfb1313c504591609d4ada57efa21a Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:05:56 -0400 Subject: [PATCH 18/34] Fix Forge calling getResource on every loot table unnecessarily --- .../faster_loot_loading/ForgeHooksMixin.java | 57 +++++++++++++++++++ .../LootDataManagerMixin.java | 41 +++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_loot_loading/ForgeHooksMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_loot_loading/LootDataManagerMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_loot_loading/ForgeHooksMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_loot_loading/ForgeHooksMixin.java new file mode 100644 index 000000000..5d1340dbb --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_loot_loading/ForgeHooksMixin.java @@ -0,0 +1,57 @@ +package org.embeddedt.modernfix.common.mixin.perf.faster_loot_loading; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.world.level.storage.loot.LootTable; +import net.minecraftforge.common.ForgeHooks; +import org.apache.commons.lang3.function.TriFunction; +import org.apache.logging.log4j.Logger; +import org.embeddedt.modernfix.annotation.FeatureLevel; +import org.embeddedt.modernfix.annotation.RequiresFeatureLevel; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Optional; + +import static net.minecraftforge.common.ForgeHooks.loadLootTable; + +@Mixin(value = ForgeHooks.class, remap = false) +@RequiresFeatureLevel(FeatureLevel.BETA) +public class ForgeHooksMixin { + @Shadow + @Final + private static Logger LOGGER; + + private static boolean mfix$isVanillaTable(JsonElement data) { + if (!(data instanceof JsonObject obj)) { + return false; + } + var vanillaMarker = obj.getAsJsonPrimitive("mfix$isVanillaTable"); + if (vanillaMarker == null) { + return false; + } + return vanillaMarker.getAsBoolean(); + } + + /** + * @author embeddedt + * @reason avoid getResource() call per loot table by using injected marker + */ + @Overwrite + public static TriFunction> getLootTableDeserializer(Gson gson, String directory) { + return (location, data, resourceManager) -> { + try { + boolean custom = !mfix$isVanillaTable(data); + return Optional.ofNullable(loadLootTable(gson, location, data, custom)); + } catch (Exception exception) { + LOGGER.error("Couldn't parse element {}:{}", directory, location, exception); + return Optional.empty(); + } + }; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_loot_loading/LootDataManagerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_loot_loading/LootDataManagerMixin.java new file mode 100644 index 000000000..741c84582 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_loot_loading/LootDataManagerMixin.java @@ -0,0 +1,41 @@ +package org.embeddedt.modernfix.common.mixin.perf.faster_loot_loading; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.resources.FileToIdConverter; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.world.level.storage.loot.LootDataManager; +import net.minecraft.world.level.storage.loot.LootDataType; +import org.embeddedt.modernfix.annotation.FeatureLevel; +import org.embeddedt.modernfix.annotation.RequiresFeatureLevel; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Map; + +@Mixin(LootDataManager.class) +@RequiresFeatureLevel(FeatureLevel.BETA) +public class LootDataManagerMixin { + /** + * @author embeddedt + * @reason inject a marker for vanilla loot tables into the JSON so that we can retrieve it from the deserializer + */ + @Inject(method = "lambda$scheduleElementParse$5", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/packs/resources/SimpleJsonResourceReloadListener;scanDirectory(Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/lang/String;Lcom/google/gson/Gson;Ljava/util/Map;)V", shift = At.Shift.AFTER)) + private static void mfix$scanAndCapture(ResourceManager resourceManager, LootDataType lootDataType, Map map, CallbackInfo ci, + @Local(ordinal = 1) Map lootTables) { + FileToIdConverter converter = FileToIdConverter.json(lootDataType.directory()); + var lootTableResourceMap = converter.listMatchingResources(resourceManager); + for (var entry : lootTableResourceMap.entrySet()) { + if (lootTables.get(converter.fileToId(entry.getKey())) instanceof JsonObject obj) { + var resource = entry.getValue(); + if (resource != null && !resource.isBuiltin()) { + obj.addProperty("mfix$isVanillaTable", true); + } + } + } + } +} From f1492cc829b7172da10fa55b14cf14ec35f23c47 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 4 Jun 2026 20:57:13 -0400 Subject: [PATCH 19/34] Allow ZipPackIndex to work with any byte channel --- .../modernfix/resources/ZipPackIndex.java | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java b/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java index 88b4dad2d..7b55e2206 100644 --- a/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java +++ b/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java @@ -13,7 +13,9 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; +import java.nio.channels.SeekableByteChannel; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.*; @@ -106,8 +108,16 @@ public ZipPackIndex(Path zipPath) throws IOException { this.root = buildTree(); } + private static SeekableByteChannel obtainChannel(Path filePath) throws IOException { + try { + return FileChannel.open(filePath, StandardOpenOption.READ); + } catch (Exception e) { + return Files.newByteChannel(filePath); + } + } + private static ByteBuffer readCentralDirectory(Path filePath) throws IOException { - try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ)) { + try (SeekableByteChannel channel = obtainChannel(filePath)) { long fileSize = channel.size(); if (fileSize < EOCD_SIZE) return null; @@ -117,7 +127,8 @@ private static ByteBuffer readCentralDirectory(Path filePath) throws IOException long tailStart = fileSize - tailSize; while (tail.hasRemaining()) { - int n = channel.read(tail, tailStart + tail.position()); + channel.position(tailStart + tail.position()); + int n = channel.read(tail); if (n < 0) { break; } @@ -151,19 +162,22 @@ private static ByteBuffer readCentralDirectory(Path filePath) throws IOException } // Try memory-mapping first; fall back to a heap copy if the OS refuses. - try { - ByteBuffer buf = channel.map(FileChannel.MapMode.READ_ONLY, cdOffset, cdSize); - buf.order(ByteOrder.LITTLE_ENDIAN); - return buf; - } catch (Exception ignored) { - // mmap unavailable (e.g. some Linux mount flags, container restrictions); - // read the central directory into a heap buffer instead. + if (channel instanceof FileChannel fc) { + try { + ByteBuffer buf = fc.map(FileChannel.MapMode.READ_ONLY, cdOffset, cdSize); + buf.order(ByteOrder.LITTLE_ENDIAN); + return buf; + } catch (Exception ignored) { + // mmap unavailable (e.g. some Linux mount flags, container restrictions); + // read the central directory into a heap buffer instead. + } } ByteBuffer buf = ByteBuffer.allocate((int) cdSize); buf.order(ByteOrder.LITTLE_ENDIAN); while (buf.hasRemaining()) { - int n = channel.read(buf, cdOffset + buf.position()); + channel.position(cdOffset + buf.position()); + int n = channel.read(buf); if (n < 0) throw new IOException("Truncated central directory during heap read"); } buf.flip(); From 0f946343610227ae187ebd6f5a6cc3f293aa2592 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 6 Jun 2026 21:04:09 -0400 Subject: [PATCH 20/34] Remove the item stack reference thread --- .../faster_ingredients/IngredientMixin.java | 1 + .../IngredientItemStacksSoftReference.java | 19 +++---------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_ingredients/IngredientMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_ingredients/IngredientMixin.java index ac12f70d1..0c920812b 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_ingredients/IngredientMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_ingredients/IngredientMixin.java @@ -135,6 +135,7 @@ public ItemStack[] getItems() { return stacks; } } + IngredientItemStacksSoftReference.clearReferences(); ItemStack[] result = computeItemsArray(); this.mfix$cachedItemStacks = new IngredientItemStacksSoftReference((Ingredient)(Object)this, result); return result; diff --git a/src/main/java/org/embeddedt/modernfix/forge/recipe/IngredientItemStacksSoftReference.java b/src/main/java/org/embeddedt/modernfix/forge/recipe/IngredientItemStacksSoftReference.java index bfb0c94aa..2c39a70ec 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/recipe/IngredientItemStacksSoftReference.java +++ b/src/main/java/org/embeddedt/modernfix/forge/recipe/IngredientItemStacksSoftReference.java @@ -11,28 +11,15 @@ public class IngredientItemStacksSoftReference extends SoftReference QUEUE = new ReferenceQueue<>(); - private static final Thread DISCARD_THREAD = new Thread(IngredientItemStacksSoftReference::clearReferences, "Ingredient reference clearing thread"); - - static { - DISCARD_THREAD.setPriority(Thread.NORM_PRIORITY + 2); - DISCARD_THREAD.setDaemon(true); - DISCARD_THREAD.start(); - } public IngredientItemStacksSoftReference(Ingredient ingredient, ItemStack[] stacks) { super(stacks, QUEUE); this.ingredient = ingredient; } - private static void clearReferences() { - while (true) { - Reference ref; - try { - ref = QUEUE.remove(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } + public static void clearReferences() { + Reference ref; + while ((ref = QUEUE.poll()) != null) { if (ref instanceof IngredientItemStacksSoftReference ingRef && ingRef.ingredient instanceof ExtendedIngredient extIng) { // Null out the reference to the SoftReference object, to allow the SoftReference itself to be garbage collected. extIng.mfix$clearReference(); From ab9880159e750108dfca557c1018d0d8e57b2acc Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 6 Jun 2026 21:08:44 -0400 Subject: [PATCH 21/34] Add experimental KubeJS memory usage optimization --- .../mixin/perf/kubejs/RecipeEventJSMixin.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/kubejs/RecipeEventJSMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/kubejs/RecipeEventJSMixin.java index c6d391328..1125caa90 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/kubejs/RecipeEventJSMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/kubejs/RecipeEventJSMixin.java @@ -1,12 +1,20 @@ package org.embeddedt.modernfix.common.mixin.perf.kubejs; +import com.google.gson.JsonElement; +import dev.latvian.mods.kubejs.recipe.RecipeJS; import dev.latvian.mods.kubejs.recipe.RecipesEventJS; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeManager; import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.annotation.FeatureLevel; import org.embeddedt.modernfix.annotation.RequiresMod; +import org.embeddedt.modernfix.core.config.ModernFixEarlyConfig; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -49,4 +57,30 @@ private void clearRecipeLists(CallbackInfo ci) { } } } + + /** + * @author embeddedt + * @reason once datapackRecipeMap is iterated, it is never referenced again, so clear it to avoid retaining + * references to the JSON objects + */ + @Inject(method = "post", at = @At(value = "NEW", target = "()Ljava/util/concurrent/ConcurrentLinkedQueue;", ordinal = 0), remap = false) + private void modernfix$clearDatapackRecipeMap(RecipeManager recipeManager, Map datapackRecipeMap, CallbackInfo ci) { + if (ModernFixEarlyConfig.ACTIVE_FEATURE_LEVEL.isAtLeast(FeatureLevel.BETA)) { + datapackRecipeMap.clear(); + } + } + + /** + * @author embeddedt + * @reason As we start materializing the final recipe objects, null out the JSON references so we avoid having + * to keep both in memory at the same time + */ + @Inject(method = "createRecipe", at = @At("RETURN"), remap = false) + private void modernfix$clearJson(RecipeJS r, CallbackInfoReturnable> cir) { + if (!ModernFixEarlyConfig.ACTIVE_FEATURE_LEVEL.isAtLeast(FeatureLevel.BETA)) { + return; + } + r.json = null; + r.originalJson = null; + } } From d51b0f60a23b167b6ee8459073c706ab8b20a6fe Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:19:25 -0400 Subject: [PATCH 22/34] Fix an instance of vanilla leaking a BufferBuilder --- .../RenderBuffersMixin.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/buffer_builder_leak/RenderBuffersMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/buffer_builder_leak/RenderBuffersMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/buffer_builder_leak/RenderBuffersMixin.java new file mode 100644 index 000000000..feb4a015f --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/buffer_builder_leak/RenderBuffersMixin.java @@ -0,0 +1,27 @@ +package org.embeddedt.modernfix.common.mixin.bugfix.buffer_builder_leak; + +import com.mojang.blaze3d.vertex.BufferBuilder; +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; +import net.minecraft.client.renderer.RenderBuffers; +import net.minecraft.client.renderer.RenderType; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(RenderBuffers.class) +@ClientOnlyMixin +public class RenderBuffersMixin { + /** + * @author embeddedt + * @reason put() may be called for multiple instances of the same render type (e.g. signSheet and hangingSignSheet + * in 1.20.1). This leaks the previous BufferBuilder if one is already in the map. + */ + @Inject(method = "put", at = @At("HEAD"), cancellable = true) + private static void mfix$preventBufferLeak(Object2ObjectLinkedOpenHashMap mapBuilders, RenderType renderType, CallbackInfo ci) { + if (mapBuilders.containsKey(renderType)) { + ci.cancel(); + } + } +} From 1bcb28a1ad3071946b09df6e9963f163b28d9cf2 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:43:28 -0400 Subject: [PATCH 23/34] Allow feature level requirement to be set at package level --- .../annotation/RequiresFeatureLevel.java | 2 +- .../modernfix/annotation/RequiresMod.java | 2 +- .../core/config/ModernFixEarlyConfig.java | 90 +++++++++++++++---- 3 files changed, 77 insertions(+), 17 deletions(-) diff --git a/annotations/src/main/java/org/embeddedt/modernfix/annotation/RequiresFeatureLevel.java b/annotations/src/main/java/org/embeddedt/modernfix/annotation/RequiresFeatureLevel.java index 7ebd7787c..3cc9ebb22 100644 --- a/annotations/src/main/java/org/embeddedt/modernfix/annotation/RequiresFeatureLevel.java +++ b/annotations/src/main/java/org/embeddedt/modernfix/annotation/RequiresFeatureLevel.java @@ -6,7 +6,7 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.CLASS) -@Target(ElementType.TYPE) +@Target({ElementType.TYPE, ElementType.PACKAGE}) public @interface RequiresFeatureLevel { FeatureLevel value() default FeatureLevel.GA; } diff --git a/annotations/src/main/java/org/embeddedt/modernfix/annotation/RequiresMod.java b/annotations/src/main/java/org/embeddedt/modernfix/annotation/RequiresMod.java index 7f718bc0b..b69ac89e1 100644 --- a/annotations/src/main/java/org/embeddedt/modernfix/annotation/RequiresMod.java +++ b/annotations/src/main/java/org/embeddedt/modernfix/annotation/RequiresMod.java @@ -6,7 +6,7 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.CLASS) -@Target(ElementType.TYPE) +@Target({ElementType.TYPE, ElementType.PACKAGE}) public @interface RequiresMod { String value() default ""; } 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 4f9f00860..9e827e56a 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -89,12 +89,58 @@ public static String sanitize(String mixinClassName) { private final Set mixinOptions = new ObjectOpenHashSet<>(); private final Map mixinsMissingMods = new Object2ObjectOpenHashMap<>(); + private static class PackageMetadata { + String requiredModId; + FeatureLevel requiredLevel; + } + + private final Map packageMetadataCache = new HashMap<>(); + public static boolean isFabric = ModernFixEarlyConfig.class.getClassLoader().getResourceAsStream("modernfix-fabric.mixins.json") != null; public Map getPermanentlyDisabledMixins() { return mixinsMissingMods; } + @SuppressWarnings("unchecked") + private static T getAnnotationValue(AnnotationNode ann, String key) { + if (ann.values == null) return null; + for (int i = 0; i < ann.values.size(); i += 2) { + if (ann.values.get(i).equals(key)) return (T) ann.values.get(i + 1); + } + return null; + } + + private PackageMetadata loadPackageMetadata(String packageResourcePath) { + String classPath = packageResourcePath + "/package-info.class"; + try (InputStream stream = ModernFixEarlyConfig.class.getClassLoader().getResourceAsStream(classPath)) { + if (stream == null) return new PackageMetadata(); + ClassReader reader = new ClassReader(stream); + ClassNode node = new ClassNode(); + reader.accept(node, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG); + PackageMetadata meta = new PackageMetadata(); + List annotations = new ArrayList<>(); + if (node.invisibleAnnotations != null) annotations.addAll(node.invisibleAnnotations); + if (node.visibleAnnotations != null) annotations.addAll(node.visibleAnnotations); + for (AnnotationNode annotation : annotations) { + if (Objects.equals(annotation.desc, MIXIN_REQUIRES_MOD_DESC)) { + meta.requiredModId = getAnnotationValue(annotation, "value"); + } else if (Objects.equals(annotation.desc, FEATURE_LEVEL_ANNOTATION_DESC)) { + String[] enumVal = getAnnotationValue(annotation, "value"); + meta.requiredLevel = FeatureLevel.valueOf(enumVal[1]); + } + } + return meta; + } catch (IOException e) { + LOGGER.error("Error scanning package-info " + classPath, e); + return new PackageMetadata(); + } + } + + private PackageMetadata getOrLoadPackageMetadata(String packageResourcePath) { + return packageMetadataCache.computeIfAbsent(packageResourcePath, this::loadPackageMetadata); + } + private void scanForAndBuildMixinOptions() { List configFiles = ImmutableList.of("modernfix-modernfix.mixins.json"); List mixinPaths = new ArrayList<>(); @@ -133,27 +179,41 @@ private void scanForAndBuildMixinOptions() { } else if(Objects.equals(annotation.desc, MIXIN_CLIENT_ONLY_DESC)) { isClientOnly = true; } else if(Objects.equals(annotation.desc, MIXIN_REQUIRES_MOD_DESC)) { - for(int i = 0; i < annotation.values.size(); i += 2) { - if(annotation.values.get(i).equals("value")) { - String modId = (String)annotation.values.get(i + 1); - if(modId != null) { - requiredModPresent = modId.startsWith("!") ? !modPresent(modId.substring(1)) : modPresent(modId); - requiredModId = modId; - } - break; - } + String modId = getAnnotationValue(annotation, "value"); + if(modId != null) { + requiredModPresent = modId.startsWith("!") ? !modPresent(modId.substring(1)) : modPresent(modId); + requiredModId = modId; } } else if(Objects.equals(annotation.desc, MIXIN_DEV_ONLY_DESC)) { isDevOnly = true; } else if(Objects.equals(annotation.desc, FEATURE_LEVEL_ANNOTATION_DESC)) { - for(int i = 0; i < annotation.values.size(); i += 2) { - if(annotation.values.get(i).equals("value")) { - // ASM stores enum annotation values as String[]{typeDescriptor, constantName} - String[] enumVal = (String[]) annotation.values.get(i + 1); - requiredLevel = FeatureLevel.valueOf(enumVal[1]); - break; + // ASM stores enum annotation values as String[]{typeDescriptor, constantName} + String[] enumVal = getAnnotationValue(annotation, "value"); + requiredLevel = FeatureLevel.valueOf(enumVal[1]); + } + } + // Merge constraints from ancestor package-info files (up to the mixin root) + String classPackagePath = mixinPath.substring(0, mixinPath.lastIndexOf('/')); + int mixinRootEnd = classPackagePath.indexOf("/mixin"); + if (mixinRootEnd >= 0) { + String mixinRoot = classPackagePath.substring(0, mixinRootEnd + "/mixin".length()); + String walkPkg = mixinRoot; + while (walkPkg.length() < classPackagePath.length()) { + int nextSlash = classPackagePath.indexOf('/', walkPkg.length() + 1); + walkPkg = (nextSlash == -1) ? classPackagePath : classPackagePath.substring(0, nextSlash); + PackageMetadata pkgMeta = getOrLoadPackageMetadata(walkPkg); + if (requiredModPresent && pkgMeta.requiredModId != null) { + boolean present = pkgMeta.requiredModId.startsWith("!") + ? !modPresent(pkgMeta.requiredModId.substring(1)) + : modPresent(pkgMeta.requiredModId); + if (!present) { + requiredModPresent = false; + requiredModId = pkgMeta.requiredModId; } } + if (pkgMeta.requiredLevel != null && pkgMeta.requiredLevel.ordinal() > requiredLevel.ordinal()) { + requiredLevel = pkgMeta.requiredLevel; + } } } if(isMixin && (!isDevOnly || ModernFixPlatformHooks.INSTANCE.isDevEnv())) { From 7fbfcf1a9267ba368bdd6e941a51d7190d4a8912 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:50:01 -0400 Subject: [PATCH 24/34] Remove error when missing_block_entities sees null BE Blocks may legitimately not have a block entity for some states --- .../bugfix/missing_block_entities/LevelChunkMixin.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/missing_block_entities/LevelChunkMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/missing_block_entities/LevelChunkMixin.java index b3aa94d50..47b29c224 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/missing_block_entities/LevelChunkMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/missing_block_entities/LevelChunkMixin.java @@ -86,13 +86,9 @@ private void makeBlockEntityIfNotExists(BlockState state, BlockPos.MutableBlockP } BlockEntity blockEntity = this.getBlockEntity(pos.immutable(), LevelChunk.EntityCreationType.IMMEDIATE); - String blockName = state.getBlock().toString(); - if (blockEntity != null) { - if (ModernFix.LOGGER.isDebugEnabled()) { - ModernFix.LOGGER.debug("Created missing block entity for {} at {}", blockName, pos.toShortString()); - } - } else { - ModernFix.LOGGER.error("Block entity is missing for {} at {}, but could not be created", blockName, pos.toShortString()); + if (blockEntity != null && ModernFix.LOGGER.isDebugEnabled()) { + String blockName = state.getBlock().toString(); + ModernFix.LOGGER.debug("Created missing block entity for {} at {}", blockName, pos.toShortString()); } } } From 292a6aeab380fc3a572ce6ea0f24b40b36ce3862 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 11 Jun 2026 20:01:31 -0400 Subject: [PATCH 25/34] Fix optimize_surface_rules breaking mods that provide custom BiomeManagers --- .../SurfaceSystemMixin.java | 24 ++++++++++++++----- .../modernfix/world/gen/ChunkBiomeLookup.java | 3 +++ 2 files changed, 21 insertions(+), 6 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 9c48fe9a3..9a3a8e64e 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 @@ -37,11 +37,18 @@ private Function> useFasterLookup(Function lookupRef) { - var lookup = MFIX_LOOKUP_CACHE.get(); - BiomeManagerAccessor accessor = (BiomeManagerAccessor)manager; - lookup.prepare(accessor.mfix$getBiomeSource(), accessor.mfix$getZoomSeed(), chunk, manager); - lookupRef.set(lookup); - return lookup; + // If mods use their own BiomeManager subclass, we cannot trust them to use the same blurring as vanilla, + // so we cannot apply our optimized path + if (manager.getClass() == BiomeManager.class) { + var lookup = MFIX_LOOKUP_CACHE.get(); + BiomeManagerAccessor accessor = (BiomeManagerAccessor)manager; + lookup.prepare(accessor.mfix$getBiomeSource(), accessor.mfix$getZoomSeed(), chunk, manager); + lookupRef.set(lookup); + return lookup; + } else { + lookupRef.set(null); + return biomeGetter; + } } @Inject(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/SurfaceRules$RuleSource;apply(Ljava/lang/Object;)Ljava/lang/Object;", ordinal = 0)) @@ -60,7 +67,12 @@ private void finishAndDisposeLookups(RandomState randomState, BiomeManager biome @Redirect(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/biome/BiomeManager;getBiome(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/core/Holder;")) private Holder useFasterLookup(BiomeManager instance, BlockPos pos, @Share("chunkBiomeLookup") LocalRef lookupRef) { - return lookupRef.get().apply(pos); + var lookup = lookupRef.get(); + if (lookup != null) { + return lookup.apply(pos); + } else { + return instance.getBiome(pos); + } } @Inject(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;(Lnet/minecraft/world/level/levelgen/SurfaceSystem;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/world/level/levelgen/NoiseChunk;Ljava/util/function/Function;Lnet/minecraft/core/Registry;Lnet/minecraft/world/level/levelgen/WorldGenerationContext;)V")) diff --git a/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java b/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java index 03c9b6bbc..f96b43099 100644 --- a/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java +++ b/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java @@ -97,6 +97,9 @@ public void prepare(BiomeManager.NoiseBiomeSource source, long biomeZoomSeed, Ch } public void dispose() { + if (this.fallbackManager == null) { + return; + } // Make sure we do not retain strong references to the biome holders Arrays.fill(biomes, null); this.fallbackManager = null; From 1174f86165d738a13bd7b30dae4f52ea54724b2e Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Mon, 15 Jun 2026 18:58:48 -0400 Subject: [PATCH 26/34] Fix blast_search_trees not working as expected with custom creative search tabs The fix is two parts: - The JEI search tree now filters results to ensure they belong to the expected tab - A fallback is added for modded search tabs whose items don't properly appear in JEI. In this case, the optimization is disabled on the problematic tab. This fallback is not applied to the main search tab, even though that is technically more correct, as it would defeat the purpose of the optimization. Fixes #669 --- .../blast_search_trees/MinecraftMixin.java | 9 +--- .../platform/ModernFixPlatformHooks.java | 3 +- .../forge/ModernFixPlatformHooksImpl.java | 23 ++++++--- .../modernfix/searchtree/DummySearchTree.java | 3 +- .../searchtree/JEIBackedSearchTree.java | 43 +++++++++++++--- .../searchtree/JEIRuntimeCapturer.java | 49 +++++++++++++++++++ .../SearchTreeProviderRegistry.java | 4 +- 7 files changed, 112 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/blast_search_trees/MinecraftMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/blast_search_trees/MinecraftMixin.java index 5a9deb538..d92d60408 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/blast_search_trees/MinecraftMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/blast_search_trees/MinecraftMixin.java @@ -3,7 +3,6 @@ import net.minecraft.client.KeyMapping; import net.minecraft.client.Minecraft; import net.minecraft.client.searchtree.SearchRegistry; -import net.minecraft.world.item.ItemStack; import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; @@ -33,12 +32,8 @@ private void replaceSearchTrees(CallbackInfo ci) { if(provider == null) return; ModernFix.LOGGER.info("Replacing search trees with '{}' provider", provider.getName()); - SearchRegistry.TreeBuilderSupplier nameSupplier = list -> provider.getSearchTree(false); - SearchRegistry.TreeBuilderSupplier tagSupplier = list -> provider.getSearchTree(true); - this.searchRegistry.register(SearchRegistry.CREATIVE_NAMES, nameSupplier); - this.searchRegistry.register(SearchRegistry.CREATIVE_TAGS, tagSupplier); - this.searchRegistry.register(SearchRegistry.RECIPE_COLLECTIONS, list -> new RecipeBookSearchTree(provider.getSearchTree(false), list)); - ModernFixPlatformHooks.INSTANCE.registerCreativeSearchTrees(this.searchRegistry, nameSupplier, tagSupplier, this::populateSearchTree); + this.searchRegistry.register(SearchRegistry.RECIPE_COLLECTIONS, list -> new RecipeBookSearchTree(provider.getSearchTree(false, null), list)); + ModernFixPlatformHooks.INSTANCE.registerCreativeSearchTrees(this.searchRegistry, provider, this::populateSearchTree); // grab components for all key mappings in order to prevent them from being loaded off-thread later // this populates the LazyLoadedValues // we also need to suppress GLFW errors to prevent crashes if a key is missing diff --git a/src/main/java/org/embeddedt/modernfix/platform/ModernFixPlatformHooks.java b/src/main/java/org/embeddedt/modernfix/platform/ModernFixPlatformHooks.java index 0a8058141..badb702fd 100644 --- a/src/main/java/org/embeddedt/modernfix/platform/ModernFixPlatformHooks.java +++ b/src/main/java/org/embeddedt/modernfix/platform/ModernFixPlatformHooks.java @@ -7,6 +7,7 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.item.ItemStack; +import org.embeddedt.modernfix.searchtree.SearchTreeProviderRegistry; import org.objectweb.asm.tree.ClassNode; import java.nio.file.Path; @@ -47,7 +48,7 @@ public interface ModernFixPlatformHooks { void onLaunchComplete(); - void registerCreativeSearchTrees(SearchRegistry registry, SearchRegistry.TreeBuilderSupplier nameSupplier, SearchRegistry.TreeBuilderSupplier tagSupplier, BiConsumer, List> populator); + void registerCreativeSearchTrees(SearchRegistry registry, SearchTreeProviderRegistry.Provider provider, BiConsumer, List> populator); String getPlatformName(); } diff --git a/src/main/java/org/embeddedt/modernfix/platform/forge/ModernFixPlatformHooksImpl.java b/src/main/java/org/embeddedt/modernfix/platform/forge/ModernFixPlatformHooksImpl.java index dbf8edf9f..799ca6cb1 100644 --- a/src/main/java/org/embeddedt/modernfix/platform/forge/ModernFixPlatformHooksImpl.java +++ b/src/main/java/org/embeddedt/modernfix/platform/forge/ModernFixPlatformHooksImpl.java @@ -8,9 +8,11 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.CreativeModeTabs; import net.minecraft.world.item.ItemStack; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.client.CreativeModeTabSearchRegistry; +import net.minecraftforge.common.CreativeModeTabRegistry; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.fml.ModLoader; @@ -30,6 +32,7 @@ import org.embeddedt.modernfix.forge.init.ModernFixForge; import org.embeddedt.modernfix.forge.packet.PacketHandler; import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; +import org.embeddedt.modernfix.searchtree.SearchTreeProviderRegistry; import org.embeddedt.modernfix.spark.SparkLaunchProfiler; import org.embeddedt.modernfix.util.CommonModUtil; import org.embeddedt.modernfix.util.DummyList; @@ -39,6 +42,7 @@ import java.lang.reflect.Field; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -153,12 +157,19 @@ public Multimap getCustomModOptions() { return modOptions; } - public void registerCreativeSearchTrees(SearchRegistry registry, SearchRegistry.TreeBuilderSupplier nameSupplier, SearchRegistry.TreeBuilderSupplier tagSupplier, BiConsumer, List> populator) { - for (SearchRegistry.Key nameKey : CreativeModeTabSearchRegistry.getNameSearchKeys().values()) { - registry.register(nameKey, nameSupplier); - } - for (SearchRegistry.Key tagKey : CreativeModeTabSearchRegistry.getTagSearchKeys().values()) { - registry.register(tagKey, tagSupplier); + public void registerCreativeSearchTrees(SearchRegistry registry, SearchTreeProviderRegistry.Provider provider, BiConsumer, List> populator) { + List tabs = new ArrayList<>(); + tabs.add(CreativeModeTabs.searchTab()); + tabs.addAll(CreativeModeTabRegistry.getSortedCreativeModeTabs()); + for (var tab : tabs) { + var nameKey = CreativeModeTabSearchRegistry.getNameSearchKey(tab); + if (nameKey != null) { + registry.register(nameKey, c -> provider.getSearchTree(false, tab)); + } + var tagKey = CreativeModeTabSearchRegistry.getTagSearchKey(tab); + if (tagKey != null) { + registry.register(tagKey, c -> provider.getSearchTree(true, tab)); + } } Map> tagSearchKeys = CreativeModeTabSearchRegistry.getTagSearchKeys(); CreativeModeTabSearchRegistry.getNameSearchKeys().forEach((tab, nameSearchKey) -> { diff --git a/src/main/java/org/embeddedt/modernfix/searchtree/DummySearchTree.java b/src/main/java/org/embeddedt/modernfix/searchtree/DummySearchTree.java index a430faaf4..a8dbcbb02 100644 --- a/src/main/java/org/embeddedt/modernfix/searchtree/DummySearchTree.java +++ b/src/main/java/org/embeddedt/modernfix/searchtree/DummySearchTree.java @@ -1,6 +1,7 @@ package org.embeddedt.modernfix.searchtree; import net.minecraft.client.searchtree.RefreshableSearchTree; +import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.ItemStack; import java.util.Collections; @@ -27,7 +28,7 @@ public List search(String pSearchText) { static final SearchTreeProviderRegistry.Provider PROVIDER = new SearchTreeProviderRegistry.Provider() { @Override - public RefreshableSearchTree getSearchTree(boolean tag) { + public RefreshableSearchTree getSearchTree(boolean tag, CreativeModeTab backingTab) { return new DummySearchTree<>(); } diff --git a/src/main/java/org/embeddedt/modernfix/searchtree/JEIBackedSearchTree.java b/src/main/java/org/embeddedt/modernfix/searchtree/JEIBackedSearchTree.java index 92ced347b..3fef8ace0 100644 --- a/src/main/java/org/embeddedt/modernfix/searchtree/JEIBackedSearchTree.java +++ b/src/main/java/org/embeddedt/modernfix/searchtree/JEIBackedSearchTree.java @@ -4,10 +4,15 @@ import mezz.jei.gui.ingredients.IngredientFilter; import mezz.jei.gui.ingredients.IngredientFilterApi; import mezz.jei.library.runtime.JeiRuntime; +import net.minecraft.ChatFormatting; +import net.minecraft.client.searchtree.PlainTextSearchTree; import net.minecraft.client.searchtree.RefreshableSearchTree; +import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; +import org.jetbrains.annotations.Nullable; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -16,6 +21,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; @@ -26,6 +32,9 @@ public class JEIBackedSearchTree extends DummySearchTree { private final boolean filteringByTag; private String lastSearchText = ""; private final List listCache = new ArrayList<>(); + private final @Nullable CreativeModeTab filterTab; + + private PlainTextSearchTree fallbackSearchTree; private static final Field filterField; private static final MethodHandle getIngredientListUncached; @@ -47,13 +56,15 @@ public class JEIBackedSearchTree extends DummySearchTree { filterField = f; } - public JEIBackedSearchTree(boolean filteringByTag) { + public JEIBackedSearchTree(boolean filteringByTag, @Nullable CreativeModeTab tab) { this.filteringByTag = filteringByTag; + this.filterTab = tab; } + @Override public List search(String pSearchText) { Optional runtime = JEIRuntimeCapturer.runtime(); - if(runtime.isPresent()) { + if (runtime.isPresent() && (filterTab == null || JEIRuntimeCapturer.getRepresentedTabs().contains(filterTab))) { IngredientFilterApi iFilterApi = (IngredientFilterApi)runtime.get().getIngredientFilter(); IngredientFilter filter; try { @@ -63,6 +74,9 @@ public List search(String pSearchText) { return Collections.emptyList(); } return this.searchJEI(filter, pSearchText); + } else if (filterTab != null) { + /* Construct a search tree for that particular tab */ + return this.searchFallback(pSearchText); } else { /* Use the default, dummy implementation */ return super.search(pSearchText); @@ -80,9 +94,13 @@ private List searchJEI(IngredientFilter filter, String pSearchText) { ModernFix.LOGGER.error("Error searching", e); ingredients = Stream.empty(); } + var filteredSet = filterTab != null ? filterTab.getSearchTabDisplayItems() : null; ingredients.toList().forEach(ingredient -> { - if(ingredient.getIngredient() instanceof ItemStack) { - listCache.add((ItemStack)ingredient.getIngredient()); + if(ingredient.getIngredient() instanceof ItemStack stack) { + // Only show the item if it would appear in the corresponding creative tab + if (filteredSet == null || filteredSet.contains(stack)) { + listCache.add(stack); + } } }); lastSearchText = pSearchText; @@ -90,10 +108,23 @@ private List searchJEI(IngredientFilter filter, String pSearchText) { return listCache; } + private List searchFallback(String pSearchText) { + Objects.requireNonNull(filterTab); + + if (fallbackSearchTree == null) { + fallbackSearchTree = PlainTextSearchTree.create(new ArrayList<>(filterTab.getSearchTabDisplayItems()), stack -> + stack.getTooltipLines(null, TooltipFlag.Default.NORMAL.asCreative()).stream() + .map(c -> ChatFormatting.stripFormatting(c.getString()).trim()) + .filter(s -> !s.isEmpty())); + } + + return fallbackSearchTree.search(pSearchText); + } + public static final SearchTreeProviderRegistry.Provider PROVIDER = new SearchTreeProviderRegistry.Provider() { @Override - public RefreshableSearchTree getSearchTree(boolean tag) { - return new JEIBackedSearchTree(tag); + public RefreshableSearchTree getSearchTree(boolean tag, @Nullable CreativeModeTab tab) { + return new JEIBackedSearchTree(tag, tab); } @Override diff --git a/src/main/java/org/embeddedt/modernfix/searchtree/JEIRuntimeCapturer.java b/src/main/java/org/embeddedt/modernfix/searchtree/JEIRuntimeCapturer.java index 47f09d66e..3457ce2e7 100644 --- a/src/main/java/org/embeddedt/modernfix/searchtree/JEIRuntimeCapturer.java +++ b/src/main/java/org/embeddedt/modernfix/searchtree/JEIRuntimeCapturer.java @@ -1,22 +1,68 @@ package org.embeddedt.modernfix.searchtree; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; import mezz.jei.api.IModPlugin; import mezz.jei.api.JeiPlugin; import mezz.jei.api.runtime.IJeiRuntime; import mezz.jei.library.runtime.JeiRuntime; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.CreativeModeTabs; import org.embeddedt.modernfix.ModernFix; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; @JeiPlugin public class JEIRuntimeCapturer implements IModPlugin { private static JeiRuntime runtimeHandle = null; + private static Set representedTabs = null; + public static Optional runtime() { return Optional.ofNullable(runtimeHandle); } + /** + * Return the set of all tabs which currently appear to be represented in JEI. + */ + public static Set getRepresentedTabs() { + if (representedTabs == null) { + Reference2IntOpenHashMap countsByTab = new Reference2IntOpenHashMap<>(); + + var allTabs = CreativeModeTabs.allTabs().stream().filter(t -> t.getType() != CreativeModeTab.Type.SEARCH).toList(); + + for (var stack : runtimeHandle.getIngredientManager().getAllItemStacks()) { + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < allTabs.size(); i++) { + var tab = allTabs.get(i); + if (tab.getSearchTabDisplayItems().contains(stack)) { + countsByTab.addTo(tab, 1); + } + } + } + + Set tabs = new HashSet<>(); + + // Always allow search tab to be shown + tabs.add(CreativeModeTabs.searchTab()); + + for (var tab : allTabs) { + int expectedDisplayItems = tab.getSearchTabDisplayItems().size(); + int actualItems = countsByTab.getOrDefault(tab, 0); + // Allow JEI to be used as the backing store if it has at least 25% of the items + if (actualItems >= (expectedDisplayItems / 4)) { + tabs.add(tab); + } + } + + representedTabs = Set.copyOf(tabs); + } + + return representedTabs; + } + @Override public ResourceLocation getPluginUid() { return new ResourceLocation(ModernFix.MODID, "capturer"); @@ -26,10 +72,13 @@ public ResourceLocation getPluginUid() { public void onRuntimeAvailable(IJeiRuntime jeiRuntime) { if (jeiRuntime instanceof JeiRuntime) runtimeHandle = (JeiRuntime)jeiRuntime; + + } @Override public void onRuntimeUnavailable() { runtimeHandle = null; + representedTabs = null; } } diff --git a/src/main/java/org/embeddedt/modernfix/searchtree/SearchTreeProviderRegistry.java b/src/main/java/org/embeddedt/modernfix/searchtree/SearchTreeProviderRegistry.java index abd4f8ebf..f63db75c4 100644 --- a/src/main/java/org/embeddedt/modernfix/searchtree/SearchTreeProviderRegistry.java +++ b/src/main/java/org/embeddedt/modernfix/searchtree/SearchTreeProviderRegistry.java @@ -1,8 +1,10 @@ package org.embeddedt.modernfix.searchtree; import net.minecraft.client.searchtree.RefreshableSearchTree; +import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.ItemStack; import org.embeddedt.modernfix.core.ModernFixMixinPlugin; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; @@ -27,7 +29,7 @@ public static synchronized void register(Provider p) { } public interface Provider { - RefreshableSearchTree getSearchTree(boolean tag); + RefreshableSearchTree getSearchTree(boolean tag, @Nullable CreativeModeTab backingTab); boolean canUse(); String getName(); } From 3ce04c440802d81ed7af83919cd3918892730296 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:15:53 -0400 Subject: [PATCH 27/34] Improve emulation of genuinely missing models Fixes #670 --- .../dynamic_resources/ModelBakeryMixin.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelBakeryMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelBakeryMixin.java index 26c554c34..35eac7be2 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelBakeryMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelBakeryMixin.java @@ -6,6 +6,7 @@ import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import net.minecraft.client.color.block.BlockColors; +import net.minecraft.client.renderer.block.model.BlockModel; import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.resources.model.BlockModelRotation; import net.minecraft.client.resources.model.BlockStateModelLoader; @@ -13,6 +14,7 @@ import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.client.resources.model.UnbakedModel; import net.minecraft.core.DefaultedRegistry; +import net.minecraft.resources.FileToIdConverter; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.profiling.ProfilerFiller; import org.embeddedt.modernfix.ModernFix; @@ -79,6 +81,12 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { @Shadow protected abstract void registerModelAndLoadDependencies(ModelResourceLocation modelLocation, UnbakedModel model); + @Shadow + @Final + private Map modelResources; + @Shadow + @Final + public static FileToIdConverter MODEL_LISTER; private final Map mfix$emulatedBakedRegistry = new DynamicOverridableMap<>(ModelResourceLocation.class, this::loadBakedModelDynamic); @Override @@ -98,6 +106,11 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { if(location.variant().equals("inventory")) { this.loadItemModelAndDependencies(location.id()); } else if (location.variant().equals("fabric_resource") || location.variant().equals("standalone")) { + // Check that the model file will actually exist. Quark's tiny potato (and possibly other mods) + // rely on this to decide when to fall back to a default model. + if (!this.modelResources.containsKey(MODEL_LISTER.idToFile(location.id()))) { + return null; + } UnbakedModel unbakedModel = this.getModel(location.id()); this.registerModelAndLoadDependencies(location, unbakedModel); } else { @@ -135,6 +148,10 @@ private BakedModel loadBakedModelDynamic(ModelResourceLocation location) { model = bakedTopLevelModels.get(location); if(model == null) { UnbakedModel prototype = mfix$loadUnbakedModelDynamic(location); + if (prototype == null) { + // model legitimately should not exist + return null; + } if(prototype == missingModel) { model = bakedMissingModel; } else { From 38824e624f82d784a5f17aaeff098ac9811444b0 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 25 Jun 2026 21:15:31 -0400 Subject: [PATCH 28/34] Make generated capability providers more memory and JIT compilation friendly We now use LOOKUPSWITCH over capability identity hashes rather than an immutable map. This technique is inspired by how the JDK used to compile string switches: https://mail.openjdk.org/pipermail/amber-spec-observers/2018-April/000568.html As a nice side effect, this allows us to get rid of the map per provider. The map is essentially "inlined" into the bytecode. --- ...CapabilityProviderDispatcherGenerator.java | 190 +++++++++--------- 1 file changed, 97 insertions(+), 93 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java index db87e92a4..4fca8165e 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java @@ -22,11 +22,13 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -47,8 +49,8 @@ sealed interface ProviderDispatch { record Guarded(int providerIndex, String fieldDesc, CapabilityRef capability) implements ProviderDispatch {} /** Provider capabilities are unknown - dispatch unconditionally. */ record Unguarded(int providerIndex, String fieldDesc) implements ProviderDispatch {} - /** Multiple guarded dispatches collapsed into a Map lookup. */ - record Hash(int mapIndex, List entries) implements ProviderDispatch {} + /** Multiple guarded dispatches collapsed into an identity-hash switch. */ + record Hash(List entries) implements ProviderDispatch {} } /** @@ -62,6 +64,7 @@ record Hash(int mapIndex, List entries) implements ProviderDispatch {} * Sentinel used in generated guards to skip empty results via reference equality, * avoiding a method call to {@code isPresent()}. */ + @SuppressWarnings("unused") public static final LazyOptional EMPTY = LazyOptional.empty(); private static final ConcurrentHashMap>, MethodHandle> cache = @@ -75,8 +78,6 @@ record Hash(int mapIndex, List entries) implements ProviderDispatch {} private static final String CAPABILITY_DESC = "Lnet/minecraftforge/common/capabilities/Capability;"; private static final String LAZY_OPTIONAL_DESC = "Lnet/minecraftforge/common/util/LazyOptional;"; private static final String DIRECTION_DESC = "Lnet/minecraft/core/Direction;"; - private static final String MAP_DESC = "Ljava/util/Map;"; - private static final String MAP_SIGNATURE = "Ljava/util/Map;Lnet/minecraftforge/common/capabilities/ICapabilityProvider;>;"; private static final String LOOKUP_DESC = "Ljava/lang/invoke/MethodHandles$Lookup;"; /** @@ -130,6 +131,13 @@ private static MethodHandle generateClass(List capRefIndices = collectCapabilityRefs(dispatches); List> capValues = resolveCapabilityValues(capRefIndices); + // Resolve each capability's identity hash now, from the singleton instances that will + // also be present at runtime. These become the LOOKUPSWITCH keys in the generated dispatch. + Map capRefHashes = new HashMap<>(); + for (Map.Entry entry : capRefIndices.entrySet()) { + capRefHashes.put(entry.getKey(), System.identityHashCode(capValues.get(entry.getValue()))); + } + ModernFix.LOGGER.debug("Generating capability dispatcher #{} for types: [{}]", () -> generatedClassId, () -> { StringBuilder sb = new StringBuilder(); for (int i = 0; i < providerTypes.size(); i++) { @@ -139,7 +147,7 @@ private static MethodHandle generateClass(List