Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
667ac6c6ee | ||
|
|
2d760eecbb | ||
|
|
292a6aeab3 | ||
|
|
7fbfcf1a92 | ||
|
|
1bcb28a1ad | ||
|
|
d51b0f60a2 | ||
|
|
ab9880159e | ||
|
|
0f94634361 | ||
|
|
f1492cc829 | ||
|
|
0ecee529d7 | ||
|
|
e9bfd96dd9 | ||
|
|
fb9dcf77c6 | ||
|
|
33851c1cb6 | ||
|
|
494203ef5a | ||
|
|
74f76f7305 | ||
|
|
62dbbea083 | ||
|
|
538c52bc2a | ||
|
|
b62eb1845b | ||
|
|
7c45564979 | ||
|
|
f8d2425242 | ||
|
|
50cedfc699 | ||
|
|
f4f596ca0c | ||
|
|
85aab426c5 | ||
|
|
29ff5f152e | ||
|
|
8213a720a3 | ||
|
|
afe3e09a27 | ||
|
|
ae20fa17c9 |
|
|
@ -0,0 +1,9 @@
|
|||
package org.embeddedt.modernfix.annotation;
|
||||
|
||||
public enum FeatureLevel {
|
||||
GA, BETA;
|
||||
|
||||
public boolean isAtLeast(FeatureLevel required) {
|
||||
return this.ordinal() >= required.ordinal();
|
||||
}
|
||||
}
|
||||
|
|
@ -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, ElementType.PACKAGE})
|
||||
public @interface RequiresFeatureLevel {
|
||||
FeatureLevel value() default FeatureLevel.GA;
|
||||
}
|
||||
|
|
@ -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 "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>("jar") {
|
||||
|
|
|
|||
|
|
@ -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<RenderType, BufferBuilder> mapBuilders, RenderType renderType, CallbackInfo ci) {
|
||||
if (mapBuilders.containsKey(renderType)) {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ package org.embeddedt.modernfix.common.mixin.perf.cache_strongholds;
|
|||
|
||||
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.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;
|
||||
|
|
@ -17,6 +19,8 @@ import org.embeddedt.modernfix.duck.IChunkGenerator;
|
|||
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.HashMap;
|
|||
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 {
|
||||
|
|
@ -41,22 +47,24 @@ public class ChunkGeneratorMixin implements IChunkGenerator {
|
|||
private BiomeSource biomeSource;
|
||||
|
||||
private Path mfix$dimensionPath;
|
||||
private RegistryAccess.Frozen mfix$registryAccess;
|
||||
private MinecraftServer mfix$server;
|
||||
|
||||
private SoftReference<Map<String, List<ChunkPos>>> 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<List<ChunkPos>> modernfix$cacheRingPositions(Holder<StructureSet> structureSet,
|
||||
ConcentricRingsStructurePlacement placement,
|
||||
Operation<CompletableFuture<List<ChunkPos>>> original) {
|
||||
if (this.mfix$registryAccess == null || this.mfix$dimensionPath == null) {
|
||||
ConcentricRingsStructurePlacement placement,
|
||||
Operation<CompletableFuture<List<ChunkPos>>> original,
|
||||
@Share("threadPool") LocalRef<ExecutorService> threadPoolRef) {
|
||||
if (this.mfix$server == null || this.mfix$dimensionPath == null) {
|
||||
return original.call(structureSet, placement);
|
||||
}
|
||||
|
||||
|
|
@ -69,14 +77,35 @@ public class ChunkGeneratorMixin implements IChunkGenerator {
|
|||
return CompletableFuture.completedFuture(List.copyOf(cached));
|
||||
}
|
||||
|
||||
return original.call(structureSet, placement).thenApplyAsync(positions -> {
|
||||
mfix$writeToCache(cacheKey, positions);
|
||||
return positions;
|
||||
}, Util.ioPool());
|
||||
var server = this.mfix$server;
|
||||
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<ExecutorService> threadPoolRef) {
|
||||
return threadPoolRef.get();
|
||||
}
|
||||
|
||||
private String mfix$makeCacheKey(ConcentricRingsStructurePlacement placement) {
|
||||
RegistryOps<Tag> ops = RegistryOps.create(NbtOps.INSTANCE, this.mfix$registryAccess);
|
||||
RegistryOps<Tag> 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)
|
||||
|
|
|
|||
|
|
@ -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 = "<init>(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<Boolean> cir) {
|
||||
long distSq = (long)x * x + (long)z * z;
|
||||
if (distSq < this.mfix$innerRadiusSq || distSq > this.mfix$outerRadiusSq) {
|
||||
cir.setReturnValue(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ public class ServerLevelMixin {
|
|||
@Local(ordinal = 0, argsOnly = true) LevelStorageSource.LevelStorageAccess levelStorageAccess,
|
||||
@Local(ordinal = 0, argsOnly = true) ResourceKey<Level> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<ResourceLocation, JsonElement, ResourceManager, Optional<LootTable>> 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ResourceLocation, JsonElement> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ResourceLocation, JsonElement> 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<Recipe<?>> cir) {
|
||||
if (!ModernFixEarlyConfig.ACTIVE_FEATURE_LEVEL.isAtLeast(FeatureLevel.BETA)) {
|
||||
return;
|
||||
}
|
||||
r.json = null;
|
||||
r.originalJson = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, SurfaceRules.RuleSource> 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, remap = false)
|
||||
private void modernfix$fastApply(SurfaceRules.Context context, CallbackInfoReturnable<SurfaceRules.SurfaceRule> cir,
|
||||
@Share("possibleNamespaces") LocalRef<Set<String>> possibleNamespacesRef) {
|
||||
var possibleBiomes = ((ExtendedSurfaceContext)(Object)context).mfix$getPossibleBiomes();
|
||||
if (possibleBiomes == null) {
|
||||
return;
|
||||
}
|
||||
Set<String> 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"), remap = false)
|
||||
private Consumer<Map.Entry<String, SurfaceRules.RuleSource>> mfix$filterConsumer(Consumer<Map.Entry<String, SurfaceRules.RuleSource>> originalConsumer,
|
||||
@Share("possibleNamespaces") LocalRef<Set<String>> possibleNamespacesRef) {
|
||||
var possibleNamespaces = possibleNamespacesRef.get();
|
||||
if (possibleNamespaces == null) {
|
||||
return originalConsumer;
|
||||
}
|
||||
return entry -> {
|
||||
if(possibleNamespaces.contains(entry.getKey())) {
|
||||
originalConsumer.accept(entry);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Set<String> mfix$findNamespaces(Set<ResourceKey<Biome>> possibleBiomes) {
|
||||
if (possibleBiomes.size() == 1) {
|
||||
return Set.of(possibleBiomes.iterator().next().location().getNamespace());
|
||||
} else {
|
||||
var namespaces = new ObjectArraySet<String>(4);
|
||||
for (var key : possibleBiomes) {
|
||||
namespaces.add(key.location().getNamespace());
|
||||
}
|
||||
return Set.copyOf(namespaces);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,8 +26,15 @@ public class NoiseBasedChunkGeneratorMixin {
|
|||
@SuppressWarnings("unchecked")
|
||||
private static void mfix$accumulate(Set<ResourceKey<Biome>> chunkBiomes, LevelChunkSection section) {
|
||||
var palette = ((ExtendedPalettedContainer<Holder<Biome>>)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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,11 +37,18 @@ public class SurfaceSystemMixin {
|
|||
@Local(ordinal = 0, argsOnly = true) BiomeManager manager,
|
||||
@Local(ordinal = 0, argsOnly = true) ChunkAccess chunk,
|
||||
@Share("chunkBiomeLookup") LocalRef<ChunkBiomeLookup> 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 @@ public class SurfaceSystemMixin {
|
|||
|
||||
@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<Biome> useFasterLookup(BiomeManager instance, BlockPos pos, @Share("chunkBiomeLookup") LocalRef<ChunkBiomeLookup> 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;<init>(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"))
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import com.mojang.datafixers.util.Either;
|
|||
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 @@ public class ChunkHolderMixin implements IClearableChunkHolder {
|
|||
}
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
|||
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 @@ public abstract class ChunkMapMixin implements ISuspendedHolderTrackingChunkMap
|
|||
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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
* <p></p>
|
||||
* 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.
|
||||
* <p></p>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Set<String>> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<PackRepository> 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<Map<IModFile, ? extends PathPackResources>, ? extends RepositorySource> packFinder) {
|
||||
return MFIX$INJECTED_REPOSITORIES.add(resourcePacks);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package org.embeddedt.modernfix.core;
|
|||
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 class ModernFixMixinPlugin implements IMixinConfigPlugin {
|
|||
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]";
|
||||
|
|
@ -129,10 +135,17 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ import org.apache.commons.lang3.SystemUtils;
|
|||
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 @@ public class ModernFixEarlyConfig {
|
|||
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").toUpperCase(Locale.ROOT);
|
||||
try {
|
||||
return FeatureLevel.valueOf(prop);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return FeatureLevel.GA;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Pattern PLATFORM_PREFIX = Pattern.compile("(forge|fabric|common)\\.");
|
||||
|
||||
|
|
@ -75,12 +89,58 @@ public class ModernFixEarlyConfig {
|
|||
private final Set<String> mixinOptions = new ObjectOpenHashSet<>();
|
||||
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 Map<String, String> getPermanentlyDisabledMixins() {
|
||||
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() {
|
||||
List<String> configFiles = ImmutableList.of("modernfix-modernfix.mixins.json");
|
||||
List<String> mixinPaths = new ArrayList<>();
|
||||
|
|
@ -112,24 +172,48 @@ public class ModernFixEarlyConfig {
|
|||
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;
|
||||
} 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)) {
|
||||
// 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())) {
|
||||
|
|
@ -138,6 +222,8 @@ public class ModernFixEarlyConfig {
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -18,11 +18,21 @@ import java.util.concurrent.locks.LockSupport;
|
|||
*/
|
||||
public class NightConfigWatchThrottler {
|
||||
private static final long DELAY = TimeUnit.MILLISECONDS.toNanos(1000);
|
||||
|
||||
|
||||
// FIXED: Add shutdown hook to clean up watcher threads
|
||||
private static void addShutdownHook() {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
isShuttingDown.set(true);
|
||||
}, "ModernFix-ShutdownHook"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static void throttle() {
|
||||
// FIXED: Register shutdown hook for clean cleanup
|
||||
addShutdownHook();
|
||||
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 +54,32 @@ public class NightConfigWatchThrottler {
|
|||
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) {
|
||||
// FIXED: Check for shutdown state to prevent new watches from being created
|
||||
if (isShuttingDown.get()) {
|
||||
return java.util.Collections.emptyIterator();
|
||||
}
|
||||
LockSupport.parkNanos(DELAY);
|
||||
// FIXED: Properly handle thread interruption to allow graceful container shutdown
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
return java.util.Collections.emptyIterator();
|
||||
}
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,28 +11,15 @@ public class IngredientItemStacksSoftReference extends SoftReference<ItemStack[]
|
|||
private final Ingredient ingredient;
|
||||
|
||||
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) {
|
||||
super(stacks, QUEUE);
|
||||
this.ingredient = ingredient;
|
||||
}
|
||||
|
||||
private static void clearReferences() {
|
||||
while (true) {
|
||||
Reference<? extends ItemStack[]> ref;
|
||||
try {
|
||||
ref = QUEUE.remove();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
public static void clearReferences() {
|
||||
Reference<? extends ItemStack[]> 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();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,393 @@
|
|||
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;
|
||||
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.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.*;
|
||||
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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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 IntList EMPTY_OFFSETS = IntList.of();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DirNode
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
static final class DirNode {
|
||||
Map<String, DirNode> childDirs;
|
||||
IntList fileChildOffsets; // offsets into cdBuffer for each direct file child
|
||||
|
||||
DirNode() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Fields
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** 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<String> trackedTopLevelDirs;
|
||||
/** 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 parsed
|
||||
*/
|
||||
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<String> 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 (SeekableByteChannel channel = obtainChannel(filePath)) {
|
||||
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()) {
|
||||
channel.position(tailStart + tail.position());
|
||||
int n = channel.read(tail);
|
||||
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 memory-mapping first; fall back to a heap copy if the OS refuses.
|
||||
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()) {
|
||||
channel.position(cdOffset + buf.position());
|
||||
int n = channel.read(buf);
|
||||
if (n < 0) throw new IOException("Truncated central directory during heap read");
|
||||
}
|
||||
buf.flip();
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
private DirNode buildTree() throws IOException {
|
||||
var cdBuffer = this.cdBuffer;
|
||||
|
||||
DirNode treeRoot = new DirNode();
|
||||
if (cdBuffer == null) {
|
||||
treeRoot.freeze();
|
||||
return treeRoot;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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+
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public Set<String> getTrackedTopLevelDirs() {
|
||||
return this.trackedTopLevelDirs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all namespaces present under the given pack type directory.
|
||||
*
|
||||
* <p>Equivalent to {@code FilePackResources.getNamespaces(type)} but reads from
|
||||
* the pre-built tree rather than scanning all zip entries.
|
||||
*/
|
||||
public Set<String> getNamespaces(PackType type) {
|
||||
DirNode typeNode = root.childDirs.get(type.getDirectory());
|
||||
if (typeNode == null) return Set.of();
|
||||
Set<String> result = new HashSet<>();
|
||||
for (String ns : typeNode.childDirs.keySet()) {
|
||||
if (ns.equals(ns.toLowerCase(Locale.ROOT))) {
|
||||
result.add(ns);
|
||||
}
|
||||
}
|
||||
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}.
|
||||
*
|
||||
* <p>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
|
||||
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) {
|
||||
ZipEntry entry = zipFile.getEntry(entryPrefix + rlPathFull);
|
||||
if (entry != null) {
|
||||
output.accept(rl, IoSupplier.create(zipFile, entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Recurse into subdirectories
|
||||
for (Map.Entry<String, DirNode> child : node.childDirs.entrySet()) {
|
||||
collectResources(child.getValue(), entryPrefix,
|
||||
rlSubPath + child.getKey() + "/", zipFile, namespace, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -97,6 +97,9 @@ public class ChunkBiomeLookup implements Function<BlockPos, Holder<Biome>> {
|
|||
}
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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 <init>(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 <init>(Ljava/util/List;)V
|
||||
public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource
|
||||
public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource <init>(Ljava/util/List;)V
|
||||
public net.minecraft.world.level.levelgen.SurfaceRules$TestRuleSource
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user