Merge branch '1.20' into 1.21.1
This commit is contained in:
commit
b702a4003e
|
|
@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
@Retention(RetentionPolicy.CLASS)
|
@Retention(RetentionPolicy.CLASS)
|
||||||
@Target(ElementType.TYPE)
|
@Target({ElementType.TYPE, ElementType.PACKAGE})
|
||||||
public @interface RequiresFeatureLevel {
|
public @interface RequiresFeatureLevel {
|
||||||
FeatureLevel value() default FeatureLevel.GA;
|
FeatureLevel value() default FeatureLevel.GA;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
@Retention(RetentionPolicy.CLASS)
|
@Retention(RetentionPolicy.CLASS)
|
||||||
@Target(ElementType.TYPE)
|
@Target({ElementType.TYPE, ElementType.PACKAGE})
|
||||||
public @interface RequiresMod {
|
public @interface RequiresMod {
|
||||||
String value() default "";
|
String value() default "";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,13 +86,9 @@ public abstract class LevelChunkMixin extends ChunkAccess {
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockEntity blockEntity = this.getBlockEntity(pos.immutable(), LevelChunk.EntityCreationType.IMMEDIATE);
|
BlockEntity blockEntity = this.getBlockEntity(pos.immutable(), LevelChunk.EntityCreationType.IMMEDIATE);
|
||||||
String blockName = state.getBlock().toString();
|
if (blockEntity != null && ModernFix.LOGGER.isDebugEnabled()) {
|
||||||
if (blockEntity != null) {
|
String blockName = state.getBlock().toString();
|
||||||
if (ModernFix.LOGGER.isDebugEnabled()) {
|
ModernFix.LOGGER.debug("Created missing block entity for {} at {}", blockName, pos.toShortString());
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,7 @@ public abstract class IngredientMixin implements ExtendedIngredient {
|
||||||
return stacks;
|
return stacks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
IngredientItemStacksSoftReference.clearReferences();
|
||||||
ItemStack[] result = computeItemsArray();
|
ItemStack[] result = computeItemsArray();
|
||||||
this.mfix$cachedItemStacks = new IngredientItemStacksSoftReference((Ingredient)(Object)this, result);
|
this.mfix$cachedItemStacks = new IngredientItemStacksSoftReference((Ingredient)(Object)this, result);
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -89,12 +89,58 @@ public class ModernFixEarlyConfig {
|
||||||
private final Set<String> mixinOptions = new ObjectOpenHashSet<>();
|
private final Set<String> mixinOptions = new ObjectOpenHashSet<>();
|
||||||
private final Map<String, String> mixinsMissingMods = new Object2ObjectOpenHashMap<>();
|
private final Map<String, String> mixinsMissingMods = new Object2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
|
private static class PackageMetadata {
|
||||||
|
String requiredModId;
|
||||||
|
FeatureLevel requiredLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<String, PackageMetadata> packageMetadataCache = new HashMap<>();
|
||||||
|
|
||||||
public static boolean isFabric = ModernFixEarlyConfig.class.getClassLoader().getResourceAsStream("modernfix-fabric.mixins.json") != null;
|
public static boolean isFabric = ModernFixEarlyConfig.class.getClassLoader().getResourceAsStream("modernfix-fabric.mixins.json") != null;
|
||||||
|
|
||||||
public Map<String, String> getPermanentlyDisabledMixins() {
|
public Map<String, String> getPermanentlyDisabledMixins() {
|
||||||
return mixinsMissingMods;
|
return mixinsMissingMods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> 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<AnnotationNode> 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() {
|
private void scanForAndBuildMixinOptions() {
|
||||||
List<String> configFiles = ImmutableList.of("modernfix-modernfix.mixins.json");
|
List<String> configFiles = ImmutableList.of("modernfix-modernfix.mixins.json");
|
||||||
List<String> mixinPaths = new ArrayList<>();
|
List<String> mixinPaths = new ArrayList<>();
|
||||||
|
|
@ -133,27 +179,41 @@ public class ModernFixEarlyConfig {
|
||||||
} else if(Objects.equals(annotation.desc, MIXIN_CLIENT_ONLY_DESC)) {
|
} else if(Objects.equals(annotation.desc, MIXIN_CLIENT_ONLY_DESC)) {
|
||||||
isClientOnly = true;
|
isClientOnly = true;
|
||||||
} else if(Objects.equals(annotation.desc, MIXIN_REQUIRES_MOD_DESC)) {
|
} else if(Objects.equals(annotation.desc, MIXIN_REQUIRES_MOD_DESC)) {
|
||||||
for(int i = 0; i < annotation.values.size(); i += 2) {
|
String modId = getAnnotationValue(annotation, "value");
|
||||||
if(annotation.values.get(i).equals("value")) {
|
if(modId != null) {
|
||||||
String modId = (String)annotation.values.get(i + 1);
|
requiredModPresent = modId.startsWith("!") ? !modPresent(modId.substring(1)) : modPresent(modId);
|
||||||
if(modId != null) {
|
requiredModId = modId;
|
||||||
requiredModPresent = modId.startsWith("!") ? !modPresent(modId.substring(1)) : modPresent(modId);
|
|
||||||
requiredModId = modId;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if(Objects.equals(annotation.desc, MIXIN_DEV_ONLY_DESC)) {
|
} else if(Objects.equals(annotation.desc, MIXIN_DEV_ONLY_DESC)) {
|
||||||
isDevOnly = true;
|
isDevOnly = true;
|
||||||
} else if(Objects.equals(annotation.desc, FEATURE_LEVEL_ANNOTATION_DESC)) {
|
} else if(Objects.equals(annotation.desc, FEATURE_LEVEL_ANNOTATION_DESC)) {
|
||||||
for(int i = 0; i < annotation.values.size(); i += 2) {
|
// ASM stores enum annotation values as String[]{typeDescriptor, constantName}
|
||||||
if(annotation.values.get(i).equals("value")) {
|
String[] enumVal = getAnnotationValue(annotation, "value");
|
||||||
// ASM stores enum annotation values as String[]{typeDescriptor, constantName}
|
requiredLevel = FeatureLevel.valueOf(enumVal[1]);
|
||||||
String[] enumVal = (String[]) annotation.values.get(i + 1);
|
}
|
||||||
requiredLevel = FeatureLevel.valueOf(enumVal[1]);
|
}
|
||||||
break;
|
// 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())) {
|
if(isMixin && (!isDevOnly || ModernFixPlatformHooks.INSTANCE.isDevEnv())) {
|
||||||
|
|
|
||||||
|
|
@ -11,28 +11,15 @@ public class IngredientItemStacksSoftReference extends SoftReference<ItemStack[]
|
||||||
private final Ingredient ingredient;
|
private final Ingredient ingredient;
|
||||||
|
|
||||||
private static final ReferenceQueue<ItemStack[]> QUEUE = new ReferenceQueue<>();
|
private static final ReferenceQueue<ItemStack[]> 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) {
|
public IngredientItemStacksSoftReference(Ingredient ingredient, ItemStack[] stacks) {
|
||||||
super(stacks, QUEUE);
|
super(stacks, QUEUE);
|
||||||
this.ingredient = ingredient;
|
this.ingredient = ingredient;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void clearReferences() {
|
public static void clearReferences() {
|
||||||
while (true) {
|
Reference<? extends ItemStack[]> ref;
|
||||||
Reference<? extends ItemStack[]> ref;
|
while ((ref = QUEUE.poll()) != null) {
|
||||||
try {
|
|
||||||
ref = QUEUE.remove();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ref instanceof IngredientItemStacksSoftReference ingRef && (Object)ingRef.ingredient instanceof ExtendedIngredient extIng) {
|
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.
|
// Null out the reference to the SoftReference object, to allow the SoftReference itself to be garbage collected.
|
||||||
extIng.mfix$clearReference();
|
extIng.mfix$clearReference();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package org.embeddedt.modernfix.resources;
|
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.resources.ResourceLocation;
|
||||||
import net.minecraft.server.packs.PackResources;
|
import net.minecraft.server.packs.PackResources;
|
||||||
import net.minecraft.server.packs.PackType;
|
import net.minecraft.server.packs.PackType;
|
||||||
|
|
@ -10,7 +13,9 @@ import java.io.InputStream;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.nio.channels.SeekableByteChannel;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.util.*;
|
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_EXTRA_LENGTH = 30;
|
||||||
private static final int CD_OFF_COMMENT_LENGTH = 32;
|
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
|
// DirNode
|
||||||
|
|
@ -54,14 +59,17 @@ public class ZipPackIndex {
|
||||||
|
|
||||||
static final class DirNode {
|
static final class DirNode {
|
||||||
Map<String, DirNode> childDirs;
|
Map<String, DirNode> childDirs;
|
||||||
int[] fileChildOffsets; // offsets into cdBuffer for each direct file child
|
IntList fileChildOffsets; // offsets into cdBuffer for each direct file child
|
||||||
|
|
||||||
DirNode() {
|
DirNode() {
|
||||||
childDirs = new HashMap<>();
|
childDirs = new Object2ObjectOpenHashMap<>();
|
||||||
fileChildOffsets = EMPTY_OFFSETS;
|
fileChildOffsets = EMPTY_OFFSETS;
|
||||||
}
|
}
|
||||||
|
|
||||||
void freeze() {
|
void freeze() {
|
||||||
|
if (fileChildOffsets instanceof IntArrayList arrayList) {
|
||||||
|
arrayList.trim();
|
||||||
|
}
|
||||||
childDirs = childDirs.isEmpty() ? Map.of() : Map.copyOf(childDirs);
|
childDirs = childDirs.isEmpty() ? Map.of() : Map.copyOf(childDirs);
|
||||||
for (DirNode child : childDirs.values()) {
|
for (DirNode child : childDirs.values()) {
|
||||||
child.freeze();
|
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. */
|
/** Central directory buffer (memory-mapped or heap-allocated fallback). May be null for empty/invalid zips. */
|
||||||
private final ByteBuffer cdBuffer;
|
private final ByteBuffer cdBuffer;
|
||||||
|
/** Top-level directories tracked by the index. */
|
||||||
|
private final Set<String> trackedTopLevelDirs;
|
||||||
/** Root of the directory tree, always non-null (may be empty but frozen). */
|
/** Root of the directory tree, always non-null (may be empty but frozen). */
|
||||||
private final DirNode root;
|
private final DirNode root;
|
||||||
|
|
||||||
|
|
@ -90,11 +100,24 @@ public class ZipPackIndex {
|
||||||
*/
|
*/
|
||||||
public ZipPackIndex(Path zipPath) throws IOException {
|
public ZipPackIndex(Path zipPath) throws IOException {
|
||||||
this.cdBuffer = readCentralDirectory(zipPath);
|
this.cdBuffer = readCentralDirectory(zipPath);
|
||||||
|
// Computed here (not statically) so that any loader-injected PackType values
|
||||||
|
// registered after class-load are included.
|
||||||
|
Set<String> packTypeDirs = new HashSet<>();
|
||||||
|
for (PackType type : PackType.values()) packTypeDirs.add(type.getDirectory());
|
||||||
|
this.trackedTopLevelDirs = Set.copyOf(packTypeDirs);
|
||||||
this.root = buildTree();
|
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 {
|
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();
|
long fileSize = channel.size();
|
||||||
if (fileSize < EOCD_SIZE) return null;
|
if (fileSize < EOCD_SIZE) return null;
|
||||||
|
|
||||||
|
|
@ -104,7 +127,8 @@ public class ZipPackIndex {
|
||||||
|
|
||||||
long tailStart = fileSize - tailSize;
|
long tailStart = fileSize - tailSize;
|
||||||
while (tail.hasRemaining()) {
|
while (tail.hasRemaining()) {
|
||||||
int n = channel.read(tail, tailStart + tail.position());
|
channel.position(tailStart + tail.position());
|
||||||
|
int n = channel.read(tail);
|
||||||
if (n < 0) {
|
if (n < 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -138,19 +162,22 @@ public class ZipPackIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try memory-mapping first; fall back to a heap copy if the OS refuses.
|
// Try memory-mapping first; fall back to a heap copy if the OS refuses.
|
||||||
try {
|
if (channel instanceof FileChannel fc) {
|
||||||
ByteBuffer buf = channel.map(FileChannel.MapMode.READ_ONLY, cdOffset, cdSize);
|
try {
|
||||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
ByteBuffer buf = fc.map(FileChannel.MapMode.READ_ONLY, cdOffset, cdSize);
|
||||||
return buf;
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
} catch (Exception ignored) {
|
return buf;
|
||||||
// mmap unavailable (e.g. some Linux mount flags, container restrictions);
|
} catch (Exception ignored) {
|
||||||
// read the central directory into a heap buffer instead.
|
// 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);
|
ByteBuffer buf = ByteBuffer.allocate((int) cdSize);
|
||||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
while (buf.hasRemaining()) {
|
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");
|
if (n < 0) throw new IOException("Truncated central directory during heap read");
|
||||||
}
|
}
|
||||||
buf.flip();
|
buf.flip();
|
||||||
|
|
@ -159,70 +186,79 @@ public class ZipPackIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
private DirNode buildTree() throws IOException {
|
private DirNode buildTree() throws IOException {
|
||||||
|
var cdBuffer = this.cdBuffer;
|
||||||
|
|
||||||
DirNode treeRoot = new DirNode();
|
DirNode treeRoot = new DirNode();
|
||||||
if (cdBuffer == null) {
|
if (cdBuffer == null) {
|
||||||
treeRoot.freeze();
|
treeRoot.freeze();
|
||||||
return treeRoot;
|
return treeRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Computed here (not statically) so that any loader-injected PackType values
|
|
||||||
// registered after class-load are included.
|
|
||||||
Set<String> packTypeDirs = new HashSet<>();
|
|
||||||
for (PackType type : PackType.values()) packTypeDirs.add(type.getDirectory());
|
|
||||||
|
|
||||||
// Accumulate file offsets per DirNode before compacting to int[]
|
|
||||||
IdentityHashMap<DirNode, List<Integer>> fileOffsets = new IdentityHashMap<>();
|
|
||||||
|
|
||||||
int pos = 0;
|
int pos = 0;
|
||||||
int limit = cdBuffer.limit();
|
int limit = cdBuffer.limit();
|
||||||
while (pos + CD_ENTRY_HEADER_SIZE <= limit) {
|
while (pos + CD_ENTRY_HEADER_SIZE <= limit) {
|
||||||
if (cdBuffer.getInt(pos) != CD_ENTRY_SIGNATURE) break;
|
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);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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();
|
treeRoot.freeze();
|
||||||
return treeRoot;
|
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+
|
// CD buffer reads — absolute-position gets are thread-safe on Java 13+
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
@ -246,6 +282,10 @@ public class ZipPackIndex {
|
||||||
// Public API
|
// Public API
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public Set<String> getTrackedTopLevelDirs() {
|
||||||
|
return this.trackedTopLevelDirs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all namespaces present under the given pack type directory.
|
* Returns all namespaces present under the given pack type directory.
|
||||||
*
|
*
|
||||||
|
|
@ -264,6 +304,28 @@ public class ZipPackIndex {
|
||||||
return result;
|
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
|
* Enumerate all resources under {@code type/namespace/path/} and deliver them
|
||||||
* to {@code output}.
|
* to {@code output}.
|
||||||
|
|
@ -310,8 +372,9 @@ public class ZipPackIndex {
|
||||||
ZipFile zipFile, String namespace,
|
ZipFile zipFile, String namespace,
|
||||||
PackResources.ResourceOutput output) {
|
PackResources.ResourceOutput output) {
|
||||||
// Emit direct file children of this node
|
// Emit direct file children of this node
|
||||||
for (int cdOffset : node.fileChildOffsets) {
|
var offsets = node.fileChildOffsets;
|
||||||
String basename = readBasename(cdOffset);
|
for (int i = 0; i < offsets.size(); i++) {
|
||||||
|
String basename = readBasename(offsets.getInt(i));
|
||||||
String rlPathFull = rlSubPath + basename;
|
String rlPathFull = rlSubPath + basename;
|
||||||
ResourceLocation rl = ResourceLocation.tryBuild(namespace, rlPathFull);
|
ResourceLocation rl = ResourceLocation.tryBuild(namespace, rlPathFull);
|
||||||
if (rl != null) {
|
if (rl != null) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user