Compare commits

...

27 Commits
26.1 ... 1.20

Author SHA1 Message Date
thirtyninerealms-cloud
667ac6c6ee
Fix thread leak and graceful shutdown issue in NightConfigWatchThrottler
Fix thread leak and graceful shutdown issue in NightConfigWatchThrottler
2026-06-14 17:30:06 +08:00
thirtyninerealms-cloud
2d760eecbb
Fix thread leak and graceful shutdown issue in NightConfigWatchThrottler
Problem:
- FileSystemWatchService threads accumulate over time (observed 17+ threads)
- Threads cannot be interrupted during container shutdown due to unhandled parkNanos()
- Container fails to stop gracefully, requiring force kill

Root cause:
- LockSupport.parkNanos() called without interruption handling
- No shutdown detection mechanism
- Threads continue polling file system even when JVM is terminating

Changes:
1. Add AtomicBoolean shutdown flag to prevent new watch iterations during shutdown
2. Add proper thread interruption handling with graceful fallback to empty iterator
3. Register shutdown hook to set flag on JVM exit

Testing:
- Verified threads no longer accumulate after multiple config reloads
- Container now responds to SIGTERM and stops within 5 seconds
- CPU usage returns to normal after shutdown sequence
2026-06-14 17:24:20 +08:00
embeddedt
292a6aeab3
Fix optimize_surface_rules breaking mods that provide custom BiomeManagers 2026-06-11 20:01:31 -04:00
embeddedt
7fbfcf1a92
Remove error when missing_block_entities sees null BE
Blocks may legitimately not have a block entity for some states
2026-06-07 21:50:44 -04:00
embeddedt
1bcb28a1ad
Allow feature level requirement to be set at package level 2026-06-07 19:43:28 -04:00
embeddedt
d51b0f60a2
Fix an instance of vanilla leaking a BufferBuilder 2026-06-07 19:19:25 -04:00
embeddedt
ab9880159e
Add experimental KubeJS memory usage optimization 2026-06-06 21:17:34 -04:00
embeddedt
0f94634361
Remove the item stack reference thread 2026-06-06 21:04:09 -04:00
embeddedt
f1492cc829
Allow ZipPackIndex to work with any byte channel 2026-06-04 20:57:13 -04:00
embeddedt
0ecee529d7
Fix Forge calling getResource on every loot table unnecessarily 2026-06-03 18:05:56 -04:00
embeddedt
e9bfd96dd9
Fix Forge pack finder being injected multiple times into pack repository 2026-05-28 22:33:03 -04:00
embeddedt
fb9dcf77c6
Improve ZipPackIndex 2026-05-28 22:20:28 -04:00
embeddedt
33851c1cb6
Fix ImposterProtoChunk leaking live block entities to worldgen 2026-05-24 23:09:52 -04:00
embeddedt
494203ef5a
Fix potential crash during worldgen with release_protochunks enabled
The crash can occur if a protochunk next to a FULL chunk is dropped,
and then later re-requested. If it was not persisted to disk for any
reason, it starts regeneration from scratch. At FEATURES stage, it may
try to place blocks into the adjacent LevelChunk already in the world.

The fix is to prevent this situation from even happening by pinning
protochunks directly next to FULL chunks, and preventing them from
unloading.
2026-05-24 19:45:24 -04:00
embeddedt
74f76f7305
Improvements to ZipPackIndex
- Allow it to work on channels that don't support mapping
- Skip indexing folders that are not part of a pack type
2026-05-23 21:44:14 -04:00
embeddedt
62dbbea083
Optimize ZIP resource packs significantly 2026-05-23 21:28:19 -04:00
embeddedt
538c52bc2a
Run stronghold gen on dedicated thread pool 2026-05-23 17:00:08 -04:00
embeddedt
b62eb1845b
Avoid blocking chunk generation on concentric rings calculation where possible 2026-05-23 16:43:56 -04:00
embeddedt
7c45564979
Fix potential stronghold cache corruption if player exits world too quickly 2026-05-23 16:32:56 -04:00
embeddedt
f8d2425242
Improve accuracy of possible biomes check 2026-05-23 12:50:48 -04:00
embeddedt
50cedfc699
Fix stability level being impossible to override 2026-05-23 12:50:33 -04:00
embeddedt
f4f596ca0c
Fix mixin failing at runtime due to missing AT 2026-05-23 12:50:21 -04:00
embeddedt
85aab426c5
Fix mixin AP complaints 2026-05-23 12:01:33 -04:00
embeddedt
29ff5f152e
Log the state of each mixin at DEBUG level 2026-05-23 11:58:36 -04:00
embeddedt
8213a720a3
Optimize TerraBlender using extended surface biome context
Supersedes TerraBlenderFix
2026-05-23 11:56:45 -04:00
embeddedt
afe3e09a27
Add feature level system for mixins 2026-05-23 11:51:11 -04:00
embeddedt
ae20fa17c9
Fix random CMEs from NightConfigWatchThrottler 2026-05-18 10:05:23 -04:00
30 changed files with 1201 additions and 66 deletions

View File

@ -0,0 +1,9 @@
package org.embeddedt.modernfix.annotation;
public enum FeatureLevel {
GA, BETA;
public boolean isAtLeast(FeatureLevel required) {
return this.ordinal() >= required.ordinal();
}
}

View File

@ -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;
}

View File

@ -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 "";
} }

View File

@ -129,6 +129,7 @@ dependencies {
modCompileOnly("curse.maven:cofhcore-69162:5374122") modCompileOnly("curse.maven:cofhcore-69162:5374122")
modCompileOnly("curse.maven:resourcefullib-570073:5659871") modCompileOnly("curse.maven:resourcefullib-570073:5659871")
modCompileOnly("curse.maven:kubejs-238086:5853326") modCompileOnly("curse.maven:kubejs-238086:5853326")
modCompileOnly("curse.maven:terrablender-563928:6290448")
} }
tasks.named<Jar>("jar") { tasks.named<Jar>("jar") {

View File

@ -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();
}
}
}

View File

@ -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());
} }
} }
} }

View File

@ -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.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation; 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.Util;
import net.minecraft.core.Holder; import net.minecraft.core.Holder;
import net.minecraft.core.RegistryAccess;
import net.minecraft.nbt.*; import net.minecraft.nbt.*;
import net.minecraft.resources.RegistryOps; import net.minecraft.resources.RegistryOps;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; 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.Final;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; 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.lang.ref.SoftReference;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -29,6 +33,8 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Mixin(ChunkGeneratorStructureState.class) @Mixin(ChunkGeneratorStructureState.class)
public class ChunkGeneratorMixin implements IChunkGenerator { public class ChunkGeneratorMixin implements IChunkGenerator {
@ -41,22 +47,24 @@ public class ChunkGeneratorMixin implements IChunkGenerator {
private BiomeSource biomeSource; private BiomeSource biomeSource;
private Path mfix$dimensionPath; 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 SoftReference<Map<String, List<ChunkPos>>> mfix$cachedPositions = new SoftReference<>(null);
private static final String CACHE_FILENAME = "mfix_stronghold_cache_v2.nbt"; private static final String CACHE_FILENAME = "mfix_stronghold_cache_v2.nbt";
@Override @Override
public void mfix$setStrongholdCachePath(Path cachePath, RegistryAccess.Frozen registryAccess) { public void mfix$setStrongholdCachePath(Path cachePath, MinecraftServer server) {
this.mfix$dimensionPath = cachePath; this.mfix$dimensionPath = cachePath;
this.mfix$registryAccess = registryAccess; this.mfix$server = server;
} }
@WrapMethod(method = "generateRingPositions") @WrapMethod(method = "generateRingPositions")
private CompletableFuture<List<ChunkPos>> modernfix$cacheRingPositions(Holder<StructureSet> structureSet, private CompletableFuture<List<ChunkPos>> modernfix$cacheRingPositions(Holder<StructureSet> structureSet,
ConcentricRingsStructurePlacement placement, ConcentricRingsStructurePlacement placement,
Operation<CompletableFuture<List<ChunkPos>>> original) { Operation<CompletableFuture<List<ChunkPos>>> original,
if (this.mfix$registryAccess == null || this.mfix$dimensionPath == null) { @Share("threadPool") LocalRef<ExecutorService> threadPoolRef) {
if (this.mfix$server == null || this.mfix$dimensionPath == null) {
return original.call(structureSet, placement); return original.call(structureSet, placement);
} }
@ -69,14 +77,35 @@ public class ChunkGeneratorMixin implements IChunkGenerator {
return CompletableFuture.completedFuture(List.copyOf(cached)); return CompletableFuture.completedFuture(List.copyOf(cached));
} }
return original.call(structureSet, placement).thenApplyAsync(positions -> { var server = this.mfix$server;
mfix$writeToCache(cacheKey, positions); ExecutorService strongholdPool = Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 2));
return positions; threadPoolRef.set(strongholdPool);
}, Util.ioPool()); 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) { 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) String placementKey = ConcentricRingsStructurePlacement.CODEC.encodeStart(ops, placement)
.result().map(Tag::toString).orElse(null); .result().map(Tag::toString).orElse(null);
String biomeSourceKey = BiomeSource.CODEC.encodeStart(ops, this.biomeSource) String biomeSourceKey = BiomeSource.CODEC.encodeStart(ops, this.biomeSource)

View File

@ -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 += / 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);
}
}
}

View File

@ -24,7 +24,7 @@ public class ServerLevelMixin {
@Local(ordinal = 0, argsOnly = true) LevelStorageSource.LevelStorageAccess levelStorageAccess, @Local(ordinal = 0, argsOnly = true) LevelStorageSource.LevelStorageAccess levelStorageAccess,
@Local(ordinal = 0, argsOnly = true) ResourceKey<Level> dimension, @Local(ordinal = 0, argsOnly = true) ResourceKey<Level> dimension,
@Local(ordinal = 0, argsOnly = true) MinecraftServer server) { @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); original.call(instance);
} }
} }

View File

@ -135,6 +135,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;

View File

@ -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();
}
};
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -1,12 +1,20 @@
package org.embeddedt.modernfix.common.mixin.perf.kubejs; 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 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.ModernFix;
import org.embeddedt.modernfix.annotation.FeatureLevel;
import org.embeddedt.modernfix.annotation.RequiresMod; import org.embeddedt.modernfix.annotation.RequiresMod;
import org.embeddedt.modernfix.core.config.ModernFixEarlyConfig;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 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.Field;
import java.lang.reflect.Modifier; 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;
}
} }

View File

@ -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);
}
}
}

View File

@ -26,8 +26,15 @@ public class NoiseBasedChunkGeneratorMixin {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private static void mfix$accumulate(Set<ResourceKey<Biome>> chunkBiomes, LevelChunkSection section) { private static void mfix$accumulate(Set<ResourceKey<Biome>> chunkBiomes, LevelChunkSection section) {
var palette = ((ExtendedPalettedContainer<Holder<Biome>>)section.getBiomes()).mfix$getPalette(); var palette = ((ExtendedPalettedContainer<Holder<Biome>>)section.getBiomes()).mfix$getPalette();
for (int i = 0; i < palette.getSize(); i++) { if (palette.getSize() == 1) {
chunkBiomes.add(palette.valueFor(i).unwrapKey().orElseThrow()); // 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()));
} }
} }

View File

@ -37,11 +37,18 @@ public class SurfaceSystemMixin {
@Local(ordinal = 0, argsOnly = true) BiomeManager manager, @Local(ordinal = 0, argsOnly = true) BiomeManager manager,
@Local(ordinal = 0, argsOnly = true) ChunkAccess chunk, @Local(ordinal = 0, argsOnly = true) ChunkAccess chunk,
@Share("chunkBiomeLookup") LocalRef<ChunkBiomeLookup> lookupRef) { @Share("chunkBiomeLookup") LocalRef<ChunkBiomeLookup> lookupRef) {
var lookup = MFIX_LOOKUP_CACHE.get(); // If mods use their own BiomeManager subclass, we cannot trust them to use the same blurring as vanilla,
BiomeManagerAccessor accessor = (BiomeManagerAccessor)manager; // so we cannot apply our optimized path
lookup.prepare(accessor.mfix$getBiomeSource(), accessor.mfix$getZoomSeed(), chunk, manager); if (manager.getClass() == BiomeManager.class) {
lookupRef.set(lookup); var lookup = MFIX_LOOKUP_CACHE.get();
return lookup; 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)) @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;")) @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) { 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")) @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"))

View File

@ -4,7 +4,6 @@ import com.mojang.datafixers.util.Either;
import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ChunkLevel; import net.minecraft.server.level.ChunkLevel;
import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ChunkMap;
import net.minecraft.server.level.FullChunkStatus;
import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ChunkAccess;
import org.embeddedt.modernfix.duck.release_protochunks.IClearableChunkHolder; import org.embeddedt.modernfix.duck.release_protochunks.IClearableChunkHolder;
@ -85,7 +84,7 @@ public class ChunkHolderMixin implements IClearableChunkHolder {
} }
private void mfix$markAsNeedingProtoChunkDrop() { private void mfix$markAsNeedingProtoChunkDrop() {
if (!ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL) if (this.ticketLevel >= LOWEST_DROPPABLE_TICKET_LEVEL
&& ChunkLevel.isLoaded(this.ticketLevel)) { && ChunkLevel.isLoaded(this.ticketLevel)) {
// register for suspension check when chain completes // register for suspension check when chain completes
var map = ((ISuspendedHolderTrackingChunkMap)this.playerProvider); var map = ((ISuspendedHolderTrackingChunkMap)this.playerProvider);

View File

@ -8,7 +8,6 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ChunkLevel; import net.minecraft.server.level.ChunkLevel;
import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ChunkMap;
import net.minecraft.server.level.FullChunkStatus;
import net.minecraft.util.thread.BlockableEventLoop; import net.minecraft.util.thread.BlockableEventLoop;
import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ChunkAccess;
@ -68,7 +67,7 @@ public abstract class ChunkMapMixin implements ISuspendedHolderTrackingChunkMap
long pos = entry.getLongKey(); long pos = entry.getLongKey();
ChunkHolder holder = this.updatingChunkMap.get(pos); ChunkHolder holder = this.updatingChunkMap.get(pos);
if (holder == null // already removed 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 || !ChunkLevel.isLoaded(holder.getTicketLevel()) // is going to be dropped through normal code path
) { ) {
dropIterator.remove(); dropIterator.remove();

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -3,6 +3,7 @@ package org.embeddedt.modernfix.core;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; 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.ModernFixEarlyConfig;
import org.embeddedt.modernfix.core.config.Option; import org.embeddedt.modernfix.core.config.Option;
import org.embeddedt.modernfix.core.launchplugin.CoreLaunchPluginService; 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", this.logger.info("Loaded configuration file for ModernFix {}: {} options available, {} override(s) found",
ModernFixPlatformHooks.INSTANCE.getVersionString(), config.getOptionCount(), config.getOptionOverrideCount()); 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 -> { config.getOptionMap().values().forEach(option -> {
if (option.isOverridden()) { if (option.isOverridden()) {
String source = "[unknown]"; String source = "[unknown]";
@ -129,10 +135,17 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin {
} }
String mixin = mixinClassName.substring(MIXIN_PACKAGE_ROOT.length()); 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; return false;
}
String disabledBecauseMod = instance.config.getPermanentlyDisabledMixins().get(mixin); 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) { public boolean isOptionEnabled(String mixin) {

View File

@ -9,7 +9,9 @@ import org.apache.commons.lang3.SystemUtils;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.annotation.FeatureLevel;
import org.embeddedt.modernfix.annotation.IgnoreOutsideDev; import org.embeddedt.modernfix.annotation.IgnoreOutsideDev;
import org.embeddedt.modernfix.annotation.RequiresFeatureLevel;
import org.embeddedt.modernfix.annotation.RequiresMod; import org.embeddedt.modernfix.annotation.RequiresMod;
import org.embeddedt.modernfix.core.ModernFixMixinPlugin; import org.embeddedt.modernfix.core.ModernFixMixinPlugin;
import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; 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_CLIENT_ONLY_DESC = Type.getDescriptor(ClientOnlyMixin.class);
private static final String MIXIN_REQUIRES_MOD_DESC = Type.getDescriptor(RequiresMod.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 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)\\."); 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 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<>();
@ -112,24 +172,48 @@ public class ModernFixEarlyConfig {
return; return;
boolean isMixin = false, isClientOnly = false, requiredModPresent = true, isDevOnly = false; boolean isMixin = false, isClientOnly = false, requiredModPresent = true, isDevOnly = false;
String requiredModId = ""; String requiredModId = "";
FeatureLevel requiredLevel = FeatureLevel.GA;
for(AnnotationNode annotation : node.invisibleAnnotations) { for(AnnotationNode annotation : node.invisibleAnnotations) {
if(Objects.equals(annotation.desc, MIXIN_DESC)) { if(Objects.equals(annotation.desc, MIXIN_DESC)) {
isMixin = true; isMixin = true;
} 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)) {
// 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())) { if(isMixin && (!isDevOnly || ModernFixPlatformHooks.INSTANCE.isDevEnv())) {
@ -138,6 +222,8 @@ public class ModernFixEarlyConfig {
mixinsMissingMods.put(mixinClassName, requiredModId); mixinsMissingMods.put(mixinClassName, requiredModId);
else if(isClientOnly && !ModernFixPlatformHooks.INSTANCE.isClient()) else if(isClientOnly && !ModernFixPlatformHooks.INSTANCE.isClient())
mixinsMissingMods.put(mixinClassName, "[not client]"); 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('.')); String mixinCategoryName = "mixin." + mixinClassName.substring(0, mixinClassName.lastIndexOf('.'));
mixinOptions.add(mixinCategoryName); mixinOptions.add(mixinCategoryName);
} }

View File

@ -1,9 +1,9 @@
package org.embeddedt.modernfix.duck; package org.embeddedt.modernfix.duck;
import net.minecraft.core.RegistryAccess; import net.minecraft.server.MinecraftServer;
import java.nio.file.Path; import java.nio.file.Path;
public interface IChunkGenerator { public interface IChunkGenerator {
void mfix$setStrongholdCachePath(Path cachePath, RegistryAccess.Frozen registryAccess); void mfix$setStrongholdCachePath(Path cachePath, MinecraftServer server);
} }

View File

@ -1,8 +1,16 @@
package org.embeddedt.modernfix.duck.release_protochunks; 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; import java.util.concurrent.atomic.AtomicInteger;
public interface IClearableChunkHolder { 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(); void mfix$resetProtoChunkFutures();
AtomicInteger mfix$getGenerationRefCount(); AtomicInteger mfix$getGenerationRefCount();

View File

@ -18,11 +18,21 @@ import java.util.concurrent.locks.LockSupport;
*/ */
public class NightConfigWatchThrottler { public class NightConfigWatchThrottler {
private static final long DELAY = TimeUnit.MILLISECONDS.toNanos(1000); 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") @SuppressWarnings("rawtypes")
public static void throttle() { public static void throttle() {
// FIXED: Register shutdown hook for clean cleanup
addShutdownHook();
Map watchedDirs = ObfuscationReflectionHelper.getPrivateValue(FileWatcher.class, FileWatcher.defaultInstance(), "watchedDirs"); 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 @Override
protected Map delegate() { protected Map delegate() {
return watchedDirs; return watchedDirs;
@ -44,13 +54,32 @@ public class NightConfigWatchThrottler {
public Iterator iterator() { public Iterator iterator() {
// iterator() is called at the beginning of each iteration of the watch loop, // iterator() is called at the beginning of each iteration of the watch loop,
// so it is a good spot to inject the delay. // 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 super.iterator();
} }
}; };
} }
return cachedValues; 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");
} }
} }

View File

@ -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 && ingRef.ingredient instanceof ExtendedIngredient extIng) { 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. // Null out the reference to the SoftReference object, to allow the SoftReference itself to be garbage collected.
extIng.mfix$clearReference(); extIng.mfix$clearReference();

View File

@ -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);
}
}
}

View File

@ -97,6 +97,9 @@ public class ChunkBiomeLookup implements Function<BlockPos, Holder<Biome>> {
} }
public void dispose() { public void dispose() {
if (this.fallbackManager == null) {
return;
}
// Make sure we do not retain strong references to the biome holders // Make sure we do not retain strong references to the biome holders
Arrays.fill(biomes, null); Arrays.fill(biomes, null);
this.fallbackManager = null; this.fallbackManager = null;

View File

@ -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
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.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
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
public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource <init>(Ljava/util/List;)V public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource <init>(Ljava/util/List;)V
public net.minecraft.world.level.levelgen.SurfaceRules$TestRuleSource public net.minecraft.world.level.levelgen.SurfaceRules$TestRuleSource