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 1/9] Improve ZipPackIndex --- .../modernfix/resources/ZipPackIndex.java | 155 ++++++++++++------ 1 file changed, 102 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java b/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java index d0a4eb92..88b4dad2 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 @@ public class ZipPackIndex { /** 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 @@ public class ZipPackIndex { */ 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,70 +172,79 @@ public class ZipPackIndex { } 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; - - 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("/"); - 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); - } - } - - pos += recordLen; + pos += indexCdEntry(pos, limit, treeRoot, cdBuffer); } - // 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; } + /** + * 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"); + } + + 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; + } + } + + // 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); + } + + return recordLen; + } + // ------------------------------------------------------------------------- // CD buffer reads — absolute-position gets are thread-safe on Java 13+ // ------------------------------------------------------------------------- @@ -246,6 +268,10 @@ public class ZipPackIndex { // Public API // ------------------------------------------------------------------------- + public Set getTrackedTopLevelDirs() { + return this.trackedTopLevelDirs; + } + /** * Returns all namespaces present under the given pack type directory. * @@ -264,6 +290,28 @@ public class ZipPackIndex { 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 @@ public class ZipPackIndex { 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 2/9] 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 00000000..d78e8221 --- /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 3/9] 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 00000000..5d1340db --- /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 00000000..741c8458 --- /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 4/9] 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 88b4dad2..7b55e220 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.io.InputStream; 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 class ZipPackIndex { 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 @@ public class ZipPackIndex { 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 @@ public class ZipPackIndex { } // 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 5/9] 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 ac12f70d..0c920812 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 abstract class IngredientMixin implements ExtendedIngredient { 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 bfb0c94a..2c39a70e 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 6/9] 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 c6d39132..1125caa9 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 @@ public class RecipeEventJSMixin { } } } + + /** + * @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 7/9] 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 00000000..feb4a015 --- /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 8/9] 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 7ebd7787..3cc9ebb2 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.RetentionPolicy; 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 7f718bc0..b69ac89e 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.RetentionPolicy; 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 4f9f0086..9e827e56 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 class ModernFixEarlyConfig { 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 @@ public class ModernFixEarlyConfig { } 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 9/9] 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 b3aa94d5..47b29c22 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 @@ public abstract class LevelChunkMixin extends ChunkAccess { } 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()); } } }