From 420d70f5fb501a5a1ac6cee2ca096c2b91a52a9a Mon Sep 17 00:00:00 2001 From: Coredex Date: Fri, 3 Jul 2026 12:46:03 +0530 Subject: [PATCH] 5.27.19 - commit.1 --- gradle.properties | 2 +- .../modernfix/annotation/FeatureLevel.java | 9 + .../annotation/RequiresFeatureLevel.java | 12 + .../modernfix/annotation/RequiresMod.java | 2 +- .../LevelChunkMixin.java | 9 +- .../ChunkGeneratorMixin.java | 50 ++- ...oncentricRingsStructurePlacementMixin.java | 83 +++++ .../cache_strongholds/ServerLevelMixin.java | 2 +- .../compact_entity_models/package-info.java | 5 + .../ClientLanguageMixin.java | 2 +- .../perf/dynamic_languages/package-info.java | 5 + .../faster_item_rendering/package-info.java | 5 + .../release_protochunks/ChunkHolderMixin.java | 5 +- .../release_protochunks/ChunkMapMixin.java | 5 +- .../ImposterProtoChunkMixin.java | 33 ++ .../resourcepacks/FilePackResourcesMixin.java | 89 +++++ .../SharedZipFileAccessAccessor.java | 18 + .../modernfix/core/ModernFixMixinPlugin.java | 36 +- .../modernfix/core/config/BuiltInOptions.java | 5 + .../core/config/ModernFixEarlyConfig.java | 209 +++++++++--- .../modernfix/core/config/Option.java | 73 ++-- .../modernfix/core/config/OptionType.java | 64 ++++ .../modernfix/duck/IChunkGenerator.java | 4 +- .../IClearableChunkHolder.java | 10 +- .../modernfix/resources/ZipPackIndex.java | 315 ++++++++++++++++++ .../modernfix/screen/OptionList.java | 27 +- .../resources/modernfix-common.mixins.json | 4 + src/main/resources/modernfix.accesswidener | 6 +- 28 files changed, 959 insertions(+), 130 deletions(-) create mode 100644 src/main/java/org/embeddedt/modernfix/annotation/FeatureLevel.java create mode 100644 src/main/java/org/embeddedt/modernfix/annotation/RequiresFeatureLevel.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ConcentricRingsStructurePlacementMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_entity_models/package-info.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/package-info.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_item_rendering/package-info.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ImposterProtoChunkMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/FilePackResourcesMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/SharedZipFileAccessAccessor.java create mode 100644 src/main/java/org/embeddedt/modernfix/core/config/BuiltInOptions.java create mode 100644 src/main/java/org/embeddedt/modernfix/core/config/OptionType.java create mode 100644 src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java diff --git a/gradle.properties b/gradle.properties index 966a66f19..7d800bf5f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2G # Mod properties mod_id=modernfix -version=5.27.18-build.1 +version=5.27.19-build.1 # Minecraft/Fabric minecraft_version=26.2 diff --git a/src/main/java/org/embeddedt/modernfix/annotation/FeatureLevel.java b/src/main/java/org/embeddedt/modernfix/annotation/FeatureLevel.java new file mode 100644 index 000000000..06aa3098f --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/annotation/FeatureLevel.java @@ -0,0 +1,9 @@ +package org.embeddedt.modernfix.annotation; + +public enum FeatureLevel { + GA, BETA; + + public boolean isAtLeast(FeatureLevel required) { + return this.ordinal() >= required.ordinal(); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/annotation/RequiresFeatureLevel.java b/src/main/java/org/embeddedt/modernfix/annotation/RequiresFeatureLevel.java new file mode 100644 index 000000000..3cc9ebb22 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/annotation/RequiresFeatureLevel.java @@ -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; +} diff --git a/src/main/java/org/embeddedt/modernfix/annotation/RequiresMod.java b/src/main/java/org/embeddedt/modernfix/annotation/RequiresMod.java index aa09f7c65..c3cf7a1c2 100644 --- a/src/main/java/org/embeddedt/modernfix/annotation/RequiresMod.java +++ b/src/main/java/org/embeddedt/modernfix/annotation/RequiresMod.java @@ -8,7 +8,7 @@ /** * Marks a mixin class as requiring a specific mod to be present (or absent with ! prefix). */ -@Target(ElementType.TYPE) +@Target({ElementType.TYPE, ElementType.PACKAGE}) @Retention(RetentionPolicy.RUNTIME) public @interface RequiresMod { String value(); diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/missing_block_entities/LevelChunkMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/missing_block_entities/LevelChunkMixin.java index cbf784aaa..c3613488f 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/missing_block_entities/LevelChunkMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/missing_block_entities/LevelChunkMixin.java @@ -85,12 +85,9 @@ private void makeBlockEntityIfNotExists(BlockState state, BlockPos.MutableBlockP } BlockEntity blockEntity = this.getBlockEntity(pos.immutable(), LevelChunk.EntityCreationType.IMMEDIATE); - String blockName = state.getBlock().toString(); - if (blockEntity != null) { - ModernFix.LOGGER.warn("Created missing block entity for {} at {}", blockName, pos.toShortString()); - } else { - ModernFix.LOGGER.error("Block entity is missing for {} at {}, but could not be created", blockName, pos.toShortString()); + if (blockEntity != null && ModernFix.LOGGER.isDebugEnabled()) { + String blockName = state.getBlock().toString(); + ModernFix.LOGGER.debug("Created missing block entity for {} at {}", blockName, pos.toShortString()); } } } - diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java index 0c1edbe53..b9076f573 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java @@ -2,10 +2,13 @@ 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.TracingExecutor; 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.util.Util; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.biome.BiomeSource; @@ -17,6 +20,8 @@ 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 +34,8 @@ 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 +48,23 @@ public class ChunkGeneratorMixin implements IChunkGenerator { private BiomeSource biomeSource; private Path mfix$dimensionPath; - private RegistryAccess.Frozen mfix$registryAccess; + private MinecraftServer mfix$server; private SoftReference>> 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> modernfix$cacheRingPositions(Holder structureSet, ConcentricRingsStructurePlacement placement, - Operation>> original) { - if (this.mfix$registryAccess == null || this.mfix$dimensionPath == null) { + Operation>> original, + @Share("threadPool") LocalRef threadPoolRef) { + if (this.mfix$server == null || this.mfix$dimensionPath == null) { return original.call(structureSet, placement); } @@ -69,14 +77,34 @@ 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(new TracingExecutor(strongholdPool)); + try { + return original.call(structureSet, placement).thenApplyAsync(positions -> { + 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/Util;backgroundExecutor()Lnet/minecraft/TracingExecutor;")) + private TracingExecutor useDedicatedService(@Share("threadPool") LocalRef threadPoolRef) { + return threadPoolRef.get(); } private String mfix$makeCacheKey(ConcentricRingsStructurePlacement placement) { - RegistryOps ops = RegistryOps.create(NbtOps.INSTANCE, this.mfix$registryAccess); + RegistryOps ops = RegistryOps.create(NbtOps.INSTANCE, this.mfix$server.registryAccess()); String placementKey = ConcentricRingsStructurePlacement.CODEC.codec().encodeStart(ops, placement) .result().map(Tag::toString).orElse(null); String biomeSourceKey = BiomeSource.CODEC.encodeStart(ops, this.biomeSource) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ConcentricRingsStructurePlacementMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ConcentricRingsStructurePlacementMixin.java new file mode 100644 index 000000000..55f691834 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ConcentricRingsStructurePlacementMixin.java @@ -0,0 +1,83 @@ +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; + + @Unique private static final int MFIX_MAX_BIOME_SNAP_SECTIONS_PER_AXIS = 7; + @Unique private static final double MFIX_MAX_ROUNDING_ERROR = Math.sqrt(2.0) * 0.5; + @Unique private static final double MFIX_MAX_BIOME_SNAP_ERROR = MFIX_MAX_BIOME_SNAP_SECTIONS_PER_AXIS * Math.sqrt(2.0); + @Unique private static final double MFIX_MAX_POSITION_ERROR = MFIX_MAX_ROUNDING_ERROR + MFIX_MAX_BIOME_SNAP_ERROR; + + @Unique private long mfix$innerRadiusSq; + @Unique private long mfix$outerRadiusSq; + + @Inject( + method = "(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; + + 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) { + this.mfix$outerRadiusSq = Long.MAX_VALUE; + return; + } + + int maxCircle = this.mfix$computeMaxCircleIndex(); + 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); + } + + @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. + */ + @Inject(method = "isPlacementChunk", at = @At("HEAD"), cancellable = true) + private void mfix$earlyRejectByRadius(ChunkGeneratorStructureState structureState, int x, int z, + CallbackInfoReturnable cir) { + long distSq = (long)x * x + (long)z * z; + if (distSq < this.mfix$innerRadiusSq || distSq > this.mfix$outerRadiusSq) { + cir.setReturnValue(false); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java index 8505303e8..dcd2b2f87 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java @@ -24,7 +24,7 @@ private void setCachePath(ChunkGeneratorStructureState instance, Operation @Local(ordinal = 0, argsOnly = true) LevelStorageSource.LevelStorageAccess levelStorageAccess, @Local(ordinal = 0, argsOnly = true) ResourceKey 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); } } diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_entity_models/package-info.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_entity_models/package-info.java new file mode 100644 index 000000000..fc0279f75 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_entity_models/package-info.java @@ -0,0 +1,5 @@ +@RequiresFeatureLevel(FeatureLevel.BETA) +package org.embeddedt.modernfix.common.mixin.perf.compact_entity_models; + +import org.embeddedt.modernfix.annotation.FeatureLevel; +import org.embeddedt.modernfix.annotation.RequiresFeatureLevel; diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java index e27eeda1d..0d62a7340 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java @@ -50,4 +50,4 @@ private static Map modifyLanguageMap(Map storage List collected = Objects.requireNonNullElse(usedResources.get(), List.of()); return DynamicLanguageMap.forVanillaData(storage, collected); } -} \ No newline at end of file +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/package-info.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/package-info.java new file mode 100644 index 000000000..041e734ae --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/package-info.java @@ -0,0 +1,5 @@ +@RequiresFeatureLevel(FeatureLevel.BETA) +package org.embeddedt.modernfix.common.mixin.perf.dynamic_languages; + +import org.embeddedt.modernfix.annotation.FeatureLevel; +import org.embeddedt.modernfix.annotation.RequiresFeatureLevel; diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_item_rendering/package-info.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_item_rendering/package-info.java new file mode 100644 index 000000000..1517e1a52 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_item_rendering/package-info.java @@ -0,0 +1,5 @@ +@RequiresFeatureLevel(FeatureLevel.BETA) +package org.embeddedt.modernfix.common.mixin.perf.faster_item_rendering; + +import org.embeddedt.modernfix.annotation.FeatureLevel; +import org.embeddedt.modernfix.annotation.RequiresFeatureLevel; diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java index ecfa534b4..7803e5dc0 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java @@ -3,7 +3,6 @@ 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.server.level.GenerationChunkHolder; import net.minecraft.world.level.ChunkPos; import org.embeddedt.modernfix.duck.release_protochunks.IClearableChunkHolder; @@ -64,7 +63,7 @@ private void markForSuspensionOnDemotion(ChunkMap chunkMap, Executor executor, C } 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); @@ -75,4 +74,4 @@ private void markForSuspensionOnDemotion(ChunkMap chunkMap, Executor executor, C }, map.mfix$getMainThreadExecutor()); } } -} \ No newline at end of file +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java index 694cbe2c5..017b8441c 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java @@ -5,7 +5,6 @@ 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 org.embeddedt.modernfix.duck.release_protochunks.IClearableChunkHolder; @@ -62,7 +61,7 @@ private void dropProtoChunks(BooleanSupplier hasMoreTime, CallbackInfo ci) { long pos = entry.getLongKey(); ChunkHolder holder = this.updatingChunkMap.get(pos); if (holder == null - || ChunkLevel.fullStatus(holder.getTicketLevel()).isOrAfter(FullChunkStatus.FULL) + || holder.getTicketLevel() < IClearableChunkHolder.LOWEST_DROPPABLE_TICKET_LEVEL || !ChunkLevel.isLoaded(holder.getTicketLevel()) ) { dropIterator.remove(); @@ -103,4 +102,4 @@ private void dropProtoChunks(BooleanSupplier hasMoreTime, CallbackInfo ci) { public Executor mfix$getMainThreadExecutor() { return this.mainThreadExecutor; } -} \ No newline at end of file +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ImposterProtoChunkMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ImposterProtoChunkMixin.java new file mode 100644 index 000000000..106db7b75 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ImposterProtoChunkMixin.java @@ -0,0 +1,33 @@ +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 Hide live BlockEntity instances from worldgen through ImposterProtoChunk wrappers. + */ + @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; + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/FilePackResourcesMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/FilePackResourcesMixin.java new file mode 100644 index 000000000..36c5838fe --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/FilePackResourcesMixin.java @@ -0,0 +1,89 @@ +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.IOException; +import java.util.Set; +import java.util.zip.ZipFile; + +@Mixin(FilePackResources.class) +@RequiresFeatureLevel(FeatureLevel.BETA) +public class FilePackResourcesMixin { + @Shadow + @Final + private FilePackResources.SharedZipFileAccess zipFileAccess; + + @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) { + var access = ((SharedZipFileAccessAccessor)this.zipFileAccess); + if (access.mfix$getOrCreateZipFile() == null) { + return null; + } + try { + mf$packIndex = index = new ZipPackIndex(access.mfix$getFile().toPath()); + } catch (IOException e) { + ModernFix.LOGGER.error("Failed to build zip index for {}", access.mfix$getFile(), 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> 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 = ((SharedZipFileAccessAccessor)this.zipFileAccess).mfix$getOrCreateZipFile(); + ZipPackIndex index = mf$getOrCreateIndex(); + if (index != null && zf != null) { + index.listResources(packType, namespace, path, zf, resourceOutput); + ci.cancel(); + } + } + + @Inject(method = "close", at = @At("HEAD")) + private void mf$invalidateIndex(CallbackInfo ci) { + mf$packIndex = null; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/SharedZipFileAccessAccessor.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/SharedZipFileAccessAccessor.java new file mode 100644 index 000000000..35d1d74f2 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/SharedZipFileAccessAccessor.java @@ -0,0 +1,18 @@ +package org.embeddedt.modernfix.common.mixin.perf.resourcepacks; + +import net.minecraft.server.packs.FilePackResources; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.io.File; +import java.util.zip.ZipFile; + +@Mixin(FilePackResources.SharedZipFileAccess.class) +public interface SharedZipFileAccessAccessor { + @Invoker("getOrCreateZipFile") + ZipFile mfix$getOrCreateZipFile(); + + @Accessor("file") + File mfix$getFile(); +} diff --git a/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java b/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java index c85753552..abe51f8f3 100644 --- a/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java +++ b/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java @@ -3,8 +3,11 @@ 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.BuiltInOptions; import org.embeddedt.modernfix.core.config.ModernFixEarlyConfig; import org.embeddedt.modernfix.core.config.Option; +import org.embeddedt.modernfix.core.config.OptionType; import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; import org.embeddedt.modernfix.world.ThreadDumper; import org.objectweb.asm.Opcodes; @@ -39,6 +42,11 @@ public ModernFixMixinPlugin() { this.logger.info("Loaded configuration file for ModernFix {}: {} options available, {} override(s) found", ModernFixPlatformHooks.INSTANCE.getVersionString(), config.getOptionCount(), config.getOptionOverrideCount()); + if(activeFeatureLevel() != FeatureLevel.GA) { + this.logger.warn("ModernFix stability level is set to {}. Features at this level may be unstable or cause crashes.", + activeFeatureLevel()); + } + config.getOptionMap().values().forEach(option -> { if (option.isOverridden()) { String source = "[unknown]"; @@ -51,7 +59,7 @@ public ModernFixMixinPlugin() { source = "mods [" + String.join(", ", option.getDefiningMods()) + "]"; } this.logger.warn("Option '{}' overriden (by {}) to '{}'", option.getName(), - source, option.isEnabled()); + source, option.getValue()); } }); @@ -128,14 +136,21 @@ public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { } 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) { - Option option = instance.config.getEffectiveOptionForMixin(mixin); + Option option = instance.config.getEffectiveOptionForMixin(mixin); if (option == null) { String msg = "No rules matched mixin '{}', treating as foreign and disabling!"; @@ -147,8 +162,17 @@ public boolean isOptionEnabled(String mixin) { return false; } - return option.isEnabled(); + return option.getType() == OptionType.BOOLEAN && option.asBoolean().getValue(); } + + public T getOptionValue(String optionName, Class type) { + return this.config.getOptionValue(optionName, type); + } + + public static FeatureLevel activeFeatureLevel() { + return instance.getOptionValue(BuiltInOptions.STABILITY_LEVEL, FeatureLevel.class); + } + @Override public void acceptTargets(Set myTargets, Set otherTargets) { @@ -300,4 +324,4 @@ private void applyBlockStateCacheScan(ClassNode targetClass) { } }); } -} \ No newline at end of file +} diff --git a/src/main/java/org/embeddedt/modernfix/core/config/BuiltInOptions.java b/src/main/java/org/embeddedt/modernfix/core/config/BuiltInOptions.java new file mode 100644 index 000000000..5b4fb3bb0 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/core/config/BuiltInOptions.java @@ -0,0 +1,5 @@ +package org.embeddedt.modernfix.core.config; + +public class BuiltInOptions { + public static final String STABILITY_LEVEL = "stability_level"; +} diff --git a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java index b89a988ee..3d93a2837 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -9,7 +9,9 @@ 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; @@ -35,8 +37,8 @@ public class ModernFixEarlyConfig { private static final Logger LOGGER = LogManager.getLogger("ModernFixConfig"); - private final Map options = new HashMap<>(); - private final Multimap optionsByCategory = HashMultimap.create(); + private final Map> options = new HashMap<>(); + private final Multimap> optionsByCategory = HashMultimap.create(); private static final boolean ALLOW_OVERRIDE_OVERRIDES = Boolean.getBoolean("modernfix.unsupported.allowOverriding"); @@ -65,6 +67,7 @@ private static boolean modPresent(String modId) { 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); private static final Pattern PLATFORM_PREFIX = Pattern.compile("(neoforge|fabric|common)\\."); @@ -74,6 +77,14 @@ public static String sanitize(String mixinClassName) { private final Set mixinOptions = new ObjectOpenHashSet<>(); private final Map mixinsMissingMods = new Object2ObjectOpenHashMap<>(); + private final Map mixinsRequiringLowerStability = new Object2ObjectOpenHashMap<>(); + + private static class PackageMetadata { + String requiredModId; + FeatureLevel requiredLevel; + } + + private final Map packageMetadataCache = new HashMap<>(); public static boolean isFabric = ModernFixEarlyConfig.class.getClassLoader().getResourceAsStream("modernfix-fabric.mixins.json") != null; @@ -81,6 +92,45 @@ public Map getPermanentlyDisabledMixins() { return mixinsMissingMods; } + @SuppressWarnings("unchecked") + private static T getAnnotationValue(AnnotationNode ann, String key) { + if (ann.values == null) return null; + for (int i = 0; i < ann.values.size(); i += 2) { + if (ann.values.get(i).equals(key)) return (T) ann.values.get(i + 1); + } + return null; + } + + private PackageMetadata loadPackageMetadata(String packageResourcePath) { + String classPath = packageResourcePath + "/package-info.class"; + try (InputStream stream = ModernFixEarlyConfig.class.getClassLoader().getResourceAsStream(classPath)) { + if (stream == null) return new PackageMetadata(); + ClassReader reader = new ClassReader(stream); + ClassNode node = new ClassNode(); + reader.accept(node, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG); + PackageMetadata meta = new PackageMetadata(); + List annotations = new ArrayList<>(); + if (node.invisibleAnnotations != null) annotations.addAll(node.invisibleAnnotations); + if (node.visibleAnnotations != null) annotations.addAll(node.visibleAnnotations); + for (AnnotationNode annotation : annotations) { + if (Objects.equals(annotation.desc, MIXIN_REQUIRES_MOD_DESC)) { + meta.requiredModId = getAnnotationValue(annotation, "value"); + } else if (Objects.equals(annotation.desc, FEATURE_LEVEL_ANNOTATION_DESC)) { + String[] enumVal = getAnnotationValue(annotation, "value"); + meta.requiredLevel = FeatureLevel.valueOf(enumVal[1]); + } + } + return meta; + } catch (IOException e) { + LOGGER.error("Error scanning package-info " + classPath, e); + return new PackageMetadata(); + } + } + + private PackageMetadata getOrLoadPackageMetadata(String packageResourcePath) { + return packageMetadataCache.computeIfAbsent(packageResourcePath, this::loadPackageMetadata); + } + private void scanForAndBuildMixinOptions() { List configFiles = ImmutableList.of( "modernfix-common.mixins.json", @@ -125,24 +175,46 @@ private void scanForAndBuildMixinOptions() { continue; boolean isMixin = false, isClientOnly = false, requiredModPresent = true, isDevOnly = false; String requiredModId = ""; + FeatureLevel requiredLevel = FeatureLevel.GA; for(AnnotationNode annotation : allAnnotations) { 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)) { + String[] enumVal = getAnnotationValue(annotation, "value"); + requiredLevel = FeatureLevel.valueOf(enumVal[1]); + } + } + 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())) { @@ -151,6 +223,9 @@ private void scanForAndBuildMixinOptions() { mixinsMissingMods.put(mixinClassName, requiredModId); else if(isClientOnly && !ModernFixPlatformHooks.INSTANCE.isClient()) mixinsMissingMods.put(mixinClassName, "[not client]"); + if (requiredLevel != FeatureLevel.GA) { + mixinsRequiringLowerStability.put(mixinClassName, requiredLevel); + } String mixinCategoryName = "mixin." + mixinClassName.substring(0, mixinClassName.lastIndexOf('.')); mixinOptions.add(mixinCategoryName); } @@ -189,17 +264,12 @@ public DefaultSettingMapBuilder put(String key, Boolean value) { .put("mixin.perf.deduplicate_location", false) .put("mixin.perf.dynamic_entity_renderers", false) .put("mixin.feature.integrated_server_watchdog", true) - .put("mixin.perf.faster_item_rendering", false) .put("mixin.feature.spam_thread_dump", false) .put("mixin.feature.remove_chat_signing", false) .put("mixin.feature.snapshot_easter_egg", true) .put("mixin.feature.spark_profile_launch", false) .put("mixin.feature.spark_profile_world_join", false) .put("mixin.devenv", isDevEnv) - // Beta (promote on next release) - .put("mixin.perf.compact_entity_models", false) - .put("mixin.perf.dynamic_languages", false) - // END .build(); private ModernFixEarlyConfig(File file) { @@ -209,16 +279,17 @@ private ModernFixEarlyConfig(File file) { mixinOptions.addAll(DEFAULT_SETTING_OVERRIDES.keySet()); for(String optionName : mixinOptions) { boolean defaultEnabled = DEFAULT_SETTING_OVERRIDES.getOrDefault(optionName, true); - Option option = new Option(optionName, defaultEnabled, false); + Option option = new Option<>(optionName, OptionType.BOOLEAN, defaultEnabled, false); this.options.putIfAbsent(optionName, option); this.optionsByCategory.put(OptionCategories.getCategoryForOption(optionName), option); } - for(Map.Entry entry : this.options.entrySet()) { + this.addBuiltInOptions(); + for(Map.Entry> entry : this.options.entrySet()) { int idx = entry.getKey().lastIndexOf('.'); if(idx <= 0) continue; String potentialParentKey = entry.getKey().substring(0, idx); - Option potentialParent = this.options.get(potentialParentKey); + Option potentialParent = this.options.get(potentialParentKey); if(potentialParent != null) { entry.getValue().setParent(potentialParent); } @@ -265,7 +336,7 @@ private void checkBlockstateCacheRebuilds() { ModernFixEarlyConfig.class.getClassLoader().getResource("/net/minecraft/world/level/Level.class"); if(deobfClass == null) { LOGGER.warn("We are in a non-Mojmap dev environment. Disabling blockstate cache patch"); - this.options.get("mixin.perf.reduce_blockstate_cache_rebuilds").addModOverride(false, "[not mojmap]"); + this.options.get("mixin.perf.reduce_blockstate_cache_rebuilds").asBoolean().addModOverride(false, "[not mojmap]"); } } catch(Throwable e) { e.printStackTrace(); @@ -274,10 +345,10 @@ private void checkBlockstateCacheRebuilds() { private void checkModelDataManager() { if(!isFabric && modPresent("rubidium") && !modPresent("embeddium")) { - Option option = this.options.get("mixin.bugfix.model_data_manager_cme"); + Option option = this.options.get("mixin.bugfix.model_data_manager_cme"); if(option != null) { LOGGER.warn("ModelDataManager bugfixes have been disabled to prevent broken rendering with Rubidium installed. Please migrate to Embeddium."); - option.addModOverride(false, "rubidium"); + option.asBoolean().addModOverride(false, "rubidium"); } } } @@ -285,13 +356,21 @@ private void checkModelDataManager() { private void disableIfModPresent(String configName, String... ids) { for(String id : ids) { if(!ModernFixPlatformHooks.INSTANCE.isEarlyLoadingNormally() || modPresent(id)) { - Option option = this.options.get(configName); + Option option = this.options.get(configName); if(option != null) - option.addModOverride(false, id); + option.asBoolean().addModOverride(false, id); } } } + private void addBuiltInOption(String name, OptionType type, T initialValue) { + this.options.putIfAbsent(name, new Option<>(name, type, initialValue, false)); + } + + private void addBuiltInOptions() { + this.addBuiltInOption(BuiltInOptions.STABILITY_LEVEL, OptionType.enumType(FeatureLevel.class), FeatureLevel.GA); + } + /** * Defines a Mixin rule which can be configured by users and other mods. * @throws IllegalStateException If a rule with that name already exists @@ -301,7 +380,7 @@ private void disableIfModPresent(String configName, String... ids) { private void addMixinRule(String mixin, boolean enabled) { String name = getMixinRuleName(mixin); - if (this.options.putIfAbsent(name, new Option(name, enabled, false)) != null) { + if (this.options.putIfAbsent(name, new Option<>(name, OptionType.BOOLEAN, enabled, false)) != null) { throw new IllegalStateException("Mixin rule already defined: " + mixin); } } @@ -311,9 +390,12 @@ private void readJVMProperties() { String value = System.getProperty("modernfix.config." + optionKey); if(value == null || value.length() == 0) continue; - boolean isEnabled = Boolean.valueOf(value); - ModernFixMixinPlugin.instance.logger.info("Configured {} to '{}' via JVM property.", optionKey, isEnabled); - this.options.get(optionKey).setEnabled(isEnabled, true); + try { + this.options.get(optionKey).setFromString(value, true); + ModernFixMixinPlugin.instance.logger.info("Configured {} to '{}' via JVM property.", optionKey, value); + } catch(RuntimeException e) { + ModernFixMixinPlugin.instance.logger.warn("Invalid value '{}' for JVM property '{}', ignoring", value, optionKey); + } } } @@ -351,27 +433,20 @@ private void readProperties(Properties props) { String key = (String) entry.getKey(); String value = (String) entry.getValue(); - Option option = this.options.get(key); + Option option = this.options.get(key); if (option == null) { LOGGER.warn("No configuration key exists with name '{}', ignoring", key); continue; } - boolean enabled; - - if (value.equalsIgnoreCase("true")) { - enabled = true; - } else if (value.equalsIgnoreCase("false")) { - enabled = false; - } else { - LOGGER.warn("Invalid value '{}' encountered for configuration key '{}', ignoring", value, key); - continue; - } - - if(ALLOW_OVERRIDE_OVERRIDES || !option.isModDefined()) - option.setEnabled(enabled, true); - else + if(ALLOW_OVERRIDE_OVERRIDES || !option.isModDefined()) { + try { + option.setFromString(value, true); + } catch(RuntimeException e) { + LOGGER.warn("Invalid value '{}' encountered for configuration key '{}', ignoring", value, key); + } + } else LOGGER.warn("Option '{}' already disabled by a mod. Ignoring user configuration", key); } } @@ -384,21 +459,21 @@ private void readProperties(Properties props) { * * @return Null if no options matched the given mixin name, otherwise the effective option for this Mixin */ - public Option getEffectiveOptionForMixin(String mixinClassName) { + public Option getEffectiveOptionForMixin(String mixinClassName) { int lastSplit = 0; int nextSplit; - Option rule = null; + Option rule = null; while ((nextSplit = mixinClassName.indexOf('.', lastSplit)) != -1) { String key = getMixinRuleName(mixinClassName.substring(0, nextSplit)); - Option candidate = this.options.get(key); + Option candidate = this.options.get(key); if (candidate != null) { rule = candidate; - if (!rule.isEnabled()) { + if (!rule.asBoolean().getValue()) { return rule; } } @@ -434,11 +509,24 @@ public static ModernFixEarlyConfig load(File file) { config.readGlobalProperties(); config.readJVMProperties(); + config.finalizeLoad(); } return config; } + /** + * Called after all properties have been read. + */ + public void finalizeLoad() { + var stabilityLevel = this.getOptionValue(BuiltInOptions.STABILITY_LEVEL, FeatureLevel.class); + for (var entry : mixinsRequiringLowerStability.entrySet()) { + if (!stabilityLevel.isAtLeast(entry.getValue())) { + mixinsMissingMods.put(entry.getKey(), "[feature level: requires " + entry.getValue() + "]"); + } + } + } + public void save() throws IOException { File dir = configFile.getParentFile(); @@ -462,6 +550,9 @@ public void save() throws IOException { writer.write("# mixin.perf.dynamic_resources=true\n"); writer.write("# Do not include the #. You may reset to defaults by deleting this file.\n"); writer.write("#\n"); + writer.write("# To enable features that are still in testing, add a line at the bottom setting the stability level:\n"); + writer.write("# stability_level=BETA\n"); + writer.write("#\n"); writer.write("# Available options:\n"); List keys = this.options.keySet().stream() .filter(key -> !key.equals("mixin.core")) @@ -469,14 +560,14 @@ public void save() throws IOException { .collect(Collectors.toList()); for(String line : keys) { if(!line.equals("mixin.core")) { - Option option = this.options.get(line); + Option option = this.options.get(line); String extraContext = ""; if(option != null) { if(!option.isUserDefined()) - extraContext = "=" + option.isEnabled() + " # " + (option.isModDefined() ? "(overridden for mod compat)" : "(default)"); + extraContext = "=" + option.getSerializedValue() + " # " + (option.isModDefined() ? "(overridden for mod compat)" : "(default)"); else { boolean defaultEnabled = DEFAULT_SETTING_OVERRIDES.getOrDefault(line, true); - extraContext = "=" + defaultEnabled + " # (default)"; + extraContext = "=" + (option.getType() == OptionType.BOOLEAN ? Boolean.toString(defaultEnabled) : option.getSerializedValue()) + " # (default)"; } } writer.write("# " + line + extraContext + "\n"); @@ -487,9 +578,9 @@ public void save() throws IOException { writer.write("# User overrides go here.\n"); for (String key : keys) { - Option option = this.options.get(key); + Option option = this.options.get(key); if(option.isUserDefined()) - writer.write(key + "=" + option.isEnabled() + "\n"); + writer.write(key + "=" + option.getSerializedValue() + "\n"); } } } @@ -509,11 +600,21 @@ public int getOptionOverrideCount() { .count(); } - public Map getOptionMap() { + public Map> getOptionMap() { return Collections.unmodifiableMap(this.options); } - public Multimap getOptionCategoryMap() { + public Multimap> getOptionCategoryMap() { return Multimaps.unmodifiableMultimap(this.optionsByCategory); } + + public T getOptionValue(String optionName, Class type) { + var option = this.options.get(optionName); + + if (option == null) { + throw new IllegalStateException("Attempting to read option '" + optionName + "' that is not registered!"); + } + + return option.asType(type).getValue(); + } } diff --git a/src/main/java/org/embeddedt/modernfix/core/config/Option.java b/src/main/java/org/embeddedt/modernfix/core/config/Option.java index 6eac94345..15f2da2fe 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/Option.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/Option.java @@ -3,33 +3,44 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.Objects; import java.util.Set; -public class Option { +public class Option { private final String name; + private final OptionType type; private Set modDefined = null; - private boolean enabled; + private T value; private boolean userDefined; - private Option parent = null; + private Option parent = null; - public Option(String name, boolean enabled, boolean userDefined) { + public Option(String name, OptionType type, T value, boolean userDefined) { this.name = name; - this.enabled = enabled; + this.type = type; + this.value = value; this.userDefined = userDefined; } - public void setEnabled(boolean enabled, boolean userDefined) { - if(this.enabled == enabled) + public OptionType getType() { + return this.type; + } + + public T getValue() { + return this.value; + } + + public void setValue(T value, boolean userDefined) { + if(Objects.equals(this.value, value)) return; - this.enabled = enabled; + this.value = value; this.userDefined = userDefined; } - public void addModOverride(boolean enabled, String modId) { - if(this.enabled == enabled) + public void addModOverride(T value, String modId) { + if(Objects.equals(this.value, value)) return; - this.enabled = enabled; + this.value = value; if (this.modDefined == null) { this.modDefined = new LinkedHashSet<>(); @@ -38,11 +49,31 @@ public void addModOverride(boolean enabled, String modId) { this.modDefined.add(modId); } - public void setParent(Option option) { + public String getSerializedValue() { + return this.type.serialize(this.value); + } + + public void setFromString(String value, boolean userDefined) { + setValue(this.type.parse(value), userDefined); + } + + @SuppressWarnings("unchecked") + public Option asType(Class type) { + if (this.type.type() != type) { + throw new IllegalStateException("Option '" + this.name + "' is not an option of type " + type.getName()); + } + return (Option) this; + } + + public Option asBoolean() { + return asType(Boolean.class); + } + + public void setParent(Option option) { this.parent = option; } - public Option getParent() { + public Option getParent() { return this.parent; } @@ -53,16 +84,12 @@ public int getDepth() { return this.parent.getDepth() + 1; } - public boolean isEnabled() { - return this.enabled; - } - /** * Checks if this option will effectively be disabled (regardless of its own status) * by the parent rule being disabled. */ public boolean isEffectivelyDisabledByParent() { - return this.parent != null && (!this.parent.enabled || this.parent.isEffectivelyDisabledByParent()); + return this.parent != null && (this.parent.type == OptionType.BOOLEAN && (!this.parent.asBoolean().getValue() || this.parent.isEffectivelyDisabledByParent())); } public boolean isOverridden() { @@ -88,15 +115,7 @@ public String getSelfName() { return this.name.substring(this.parent.getName().length() + 1); } - public void clearModsDefiningValue() { - this.modDefined = null; - } - - public void clearUserDefined() { - this.userDefined = false; - } - public Collection getDefiningMods() { return this.modDefined != null ? Collections.unmodifiableCollection(this.modDefined) : Collections.emptyList(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/embeddedt/modernfix/core/config/OptionType.java b/src/main/java/org/embeddedt/modernfix/core/config/OptionType.java new file mode 100644 index 000000000..e2770d7a1 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/core/config/OptionType.java @@ -0,0 +1,64 @@ +package org.embeddedt.modernfix.core.config; + +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; + +public abstract class OptionType { + public static final OptionType BOOLEAN = new OptionType() { + @Override + public Boolean parse(String s) { + if (s.equalsIgnoreCase("true")) { + return Boolean.TRUE; + } else if (s.equalsIgnoreCase("false")) { + return Boolean.FALSE; + } else { + throw new IllegalArgumentException(s); + } + } + + @Override + public String serialize(Boolean value) { + return value.toString(); + } + + @Override + public Class type() { + return Boolean.class; + } + }; + + private static final ConcurrentHashMap>, OptionType>> ENUM_TYPES = new ConcurrentHashMap<>(); + + private OptionType() { + } + + public abstract Class type(); + + public abstract T parse(String s) throws IllegalArgumentException; + + public abstract String serialize(T value); + + private static > OptionType createEnumType(Class enumClass) { + return new OptionType<>() { + @Override + public T parse(String s) throws IllegalArgumentException { + return Enum.valueOf(enumClass, s.toUpperCase(Locale.ROOT)); + } + + @Override + public String serialize(T value) { + return value.name(); + } + + @Override + public Class type() { + return enumClass; + } + }; + } + + @SuppressWarnings("unchecked") + public static > OptionType enumType(Class enumClass) { + return (OptionType)ENUM_TYPES.computeIfAbsent(enumClass, k -> createEnumType((Class)k)); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java b/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java index 3cf83acc8..312c5b441 100644 --- a/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java +++ b/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java @@ -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); } diff --git a/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java b/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java index 6c01069d4..7d933e3d8 100644 --- a/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java +++ b/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java @@ -1,5 +1,13 @@ package org.embeddedt.modernfix.duck.release_protochunks; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.FullChunkStatus; + public interface IClearableChunkHolder { + /** + * We don't want to drop FULL chunks, or chunks immediately surrounding FULL. So + 2 is the minimum we can drop. + */ + int LOWEST_DROPPABLE_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.FULL) + 2; + void mfix$resetProtoChunkFutures(); -} \ No newline at end of file +} diff --git a/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java b/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java new file mode 100644 index 000000000..a1c521b3e --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/resources/ZipPackIndex.java @@ -0,0 +1,315 @@ +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.Identifier; +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.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Indexes a zip central directory so namespace/resource listing can avoid full zip scans. + */ +public class ZipPackIndex { + 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(); + + static final class DirNode { + Map childDirs; + IntList fileChildOffsets; + + 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(); + } + } + } + + private final ByteBuffer cdBuffer; + private final Set trackedTopLevelDirs; + private final DirNode root; + + public ZipPackIndex(Path zipPath) throws IOException { + this.cdBuffer = readCentralDirectory(zipPath); + Set packTypeDirs = new HashSet<>(); + for (PackType type : PackType.values()) { + packTypeDirs.add(type.getDirectory()); + } + this.trackedTopLevelDirs = Set.copyOf(packTypeDirs); + this.root = buildTree(); + } + + private static SeekableByteChannel obtainChannel(Path filePath) throws IOException { + try { + return FileChannel.open(filePath, StandardOpenOption.READ); + } catch (Exception e) { + return Files.newByteChannel(filePath); + } + } + + private static ByteBuffer readCentralDirectory(Path filePath) throws IOException { + try (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(); + + 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"); + } + + 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) { + } + } + + 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 { + 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; + } + + 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); + if (next == null) { + current.childDirs.put(segment, next = new DirNode()); + } + current = next; + } + segStart = i + 1; + } + } + + if (!skipped && tracked && segStart < fileNameLen) { + if (current.fileChildOffsets == EMPTY_OFFSETS) { + current.fileChildOffsets = new IntArrayList(); + } + current.fileChildOffsets.add(pos); + } + + return recordLen; + } + + 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 Set getTrackedTopLevelDirs() { + return this.trackedTopLevelDirs; + } + + public Set getNamespaces(PackType type) { + DirNode typeNode = root.childDirs.get(type.getDirectory()); + if (typeNode == null) return Set.of(); + Set 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; + } + + 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; + + 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 = ""; + } + + String entryPrefix = type.getDirectory() + "/" + namespace + "/"; + collectResources(node, entryPrefix, rlSubPath, zipFile, namespace, output); + } + + private void collectResources(DirNode node, String entryPrefix, String rlSubPath, + ZipFile zipFile, String namespace, + PackResources.ResourceOutput output) { + var offsets = node.fileChildOffsets; + for (int i = 0; i < offsets.size(); i++) { + String basename = readBasename(offsets.getInt(i)); + String rlPathFull = rlSubPath + basename; + Identifier rl = Identifier.tryBuild(namespace, rlPathFull); + if (rl != null) { + ZipEntry entry = zipFile.getEntry(entryPrefix + rlPathFull); + if (entry != null) { + output.accept(rl, IoSupplier.create(zipFile, entry)); + } + } + } + for (Map.Entry child : node.childDirs.entrySet()) { + collectResources(child.getValue(), entryPrefix, + rlSubPath + child.getKey() + "/", zipFile, namespace, output); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/screen/OptionList.java b/src/main/java/org/embeddedt/modernfix/screen/OptionList.java index b41f644e4..e9776a984 100644 --- a/src/main/java/org/embeddedt/modernfix/screen/OptionList.java +++ b/src/main/java/org/embeddedt/modernfix/screen/OptionList.java @@ -22,6 +22,7 @@ import org.embeddedt.modernfix.core.ModernFixMixinPlugin; import org.embeddedt.modernfix.core.config.Option; import org.embeddedt.modernfix.core.config.OptionCategories; +import org.embeddedt.modernfix.core.config.OptionType; import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; import java.io.IOException; @@ -39,7 +40,7 @@ public class OptionList extends ContainerObjectSelectionList { private ModernFixConfigScreen mainScreen; - private static MutableComponent getOptionComponent(Option option) { + private static MutableComponent getOptionComponent(Option option) { String friendlyKey = "modernfix.option.name." + option.getName(); MutableComponent baseComponent = Component.literal(option.getSelfName()); if(Language.getInstance().has(friendlyKey)) @@ -56,9 +57,11 @@ public void updateOptionEntryStatuses() { } } - private final Set