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/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()); } } } 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 0b619714..d33f88ea 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 @@ -154,6 +154,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/core/config/ModernFixEarlyConfig.java b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java index f790107c..a88d3f17 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())) { diff --git a/src/main/java/org/embeddedt/modernfix/neoforge/recipe/IngredientItemStacksSoftReference.java b/src/main/java/org/embeddedt/modernfix/neoforge/recipe/IngredientItemStacksSoftReference.java index ab118a96..ff4977ed 100644 --- a/src/main/java/org/embeddedt/modernfix/neoforge/recipe/IngredientItemStacksSoftReference.java +++ b/src/main/java/org/embeddedt/modernfix/neoforge/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 && (Object)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(); diff --git a/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java b/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java index d0a4eb92..7b55e220 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; @@ -10,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.*; @@ -46,7 +51,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 +59,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 +83,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,11 +100,24 @@ 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(); } + 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; @@ -104,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; } @@ -138,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(); @@ -159,70 +186,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 +282,10 @@ public class ZipPackIndex { // Public API // ------------------------------------------------------------------------- + public Set getTrackedTopLevelDirs() { + return this.trackedTopLevelDirs; + } + /** * Returns all namespaces present under the given pack type directory. * @@ -264,6 +304,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 +372,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) {