diff --git a/annotations/src/main/java/org/embeddedt/modernfix/annotation/FeatureLevel.java b/annotations/src/main/java/org/embeddedt/modernfix/annotation/FeatureLevel.java new file mode 100644 index 00000000..06aa3098 --- /dev/null +++ b/annotations/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/annotations/src/main/java/org/embeddedt/modernfix/annotation/RequiresFeatureLevel.java b/annotations/src/main/java/org/embeddedt/modernfix/annotation/RequiresFeatureLevel.java new file mode 100644 index 00000000..7ebd7787 --- /dev/null +++ b/annotations/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) +public @interface RequiresFeatureLevel { + FeatureLevel value() default FeatureLevel.GA; +} diff --git a/build.gradle.kts b/build.gradle.kts index 1237d01d..ed8b0387 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -116,6 +116,7 @@ dependencies { compileOnly("curse.maven:cofhcore-69162:5374122") compileOnly("curse.maven:resourcefullib-570073:5659871") compileOnly("curse.maven:kubejs-238086:5853326") + compileOnly("curse.maven:terrablender-neoforge-940057:6054947") } tasks.named("jar") { diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NamespacedSurfaceRuleSourceMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NamespacedSurfaceRuleSourceMixin.java new file mode 100644 index 00000000..be95eb37 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NamespacedSurfaceRuleSourceMixin.java @@ -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 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 cir, + @Share("possibleNamespaces") LocalRef> possibleNamespacesRef) { + var possibleBiomes = ((ExtendedSurfaceContext)(Object)context).mfix$getPossibleBiomes(); + if (possibleBiomes == null) { + return; + } + Set 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> mfix$filterConsumer(Consumer> originalConsumer, + @Share("possibleNamespaces") LocalRef> possibleNamespacesRef) { + var possibleNamespaces = possibleNamespacesRef.get(); + if (possibleNamespaces == null) { + return originalConsumer; + } + return entry -> { + if(possibleNamespaces.contains(entry.getKey())) { + originalConsumer.accept(entry); + } + }; + } + + private static Set mfix$findNamespaces(Set> possibleBiomes) { + if (possibleBiomes.size() == 1) { + return Set.of(possibleBiomes.iterator().next().location().getNamespace()); + } else { + var namespaces = new ObjectArraySet(4); + for (var key : possibleBiomes) { + namespaces.add(key.location().getNamespace()); + } + return Set.copyOf(namespaces); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NoiseBasedChunkGeneratorMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NoiseBasedChunkGeneratorMixin.java index 4791d620..ff265251 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NoiseBasedChunkGeneratorMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/NoiseBasedChunkGeneratorMixin.java @@ -26,8 +26,15 @@ public class NoiseBasedChunkGeneratorMixin { @SuppressWarnings("unchecked") private static void mfix$accumulate(Set> chunkBiomes, LevelChunkSection section) { var palette = ((ExtendedPalettedContainer>)section.getBiomes()).mfix$getPalette(); - for (int i = 0; i < palette.getSize(); i++) { - chunkBiomes.add(palette.valueFor(i).unwrapKey().orElseThrow()); + if (palette.getSize() == 1) { + // No need to iterate the storage itself, as there can only be one value + chunkBiomes.add(palette.valueFor(0).unwrapKey().orElseThrow()); + } else { + // Use getAll() rather than raw palette iteration. PalettedContainer.recreate() seeds the new + // palette with Biomes.PLAINS (the initial default), leaving a stale palette entry even after + // fillBiomesFromNoise replaces all cells with real biomes. getAll() only visits entries that + // are actually referenced in the backing storage, so stale entries are correctly excluded. + section.getBiomes().getAll(holder -> chunkBiomes.add(holder.unwrapKey().orElseThrow())); } } diff --git a/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java b/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java index 05bf9147..e75b4cb6 100644 --- a/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java +++ b/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java @@ -3,6 +3,7 @@ package org.embeddedt.modernfix.core; import com.google.common.collect.ImmutableSet; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.embeddedt.modernfix.annotation.FeatureLevel; import org.embeddedt.modernfix.core.config.ModernFixEarlyConfig; import org.embeddedt.modernfix.core.config.Option; import org.embeddedt.modernfix.core.launchplugin.CoreLaunchPluginService; @@ -40,6 +41,11 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin { this.logger.info("Loaded configuration file for ModernFix {}: {} options available, {} override(s) found", ModernFixPlatformHooks.INSTANCE.getVersionString(), config.getOptionCount(), config.getOptionOverrideCount()); + if(ModernFixEarlyConfig.ACTIVE_FEATURE_LEVEL != FeatureLevel.GA) { + this.logger.warn("ModernFix stability level is set to {}. Features at this level may be unstable or cause crashes.", + ModernFixEarlyConfig.ACTIVE_FEATURE_LEVEL); + } + config.getOptionMap().values().forEach(option -> { if (option.isOverridden()) { String source = "[unknown]"; @@ -129,10 +135,17 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin { } String mixin = mixinClassName.substring(MIXIN_PACKAGE_ROOT.length()); - if(!instance.isOptionEnabled(mixin)) + if(!instance.isOptionEnabled(mixin)) { + this.logger.debug("Skipping mixin {}: disabled by configuration", mixin); return false; + } String disabledBecauseMod = instance.config.getPermanentlyDisabledMixins().get(mixin); - return disabledBecauseMod == null; + if(disabledBecauseMod != null) { + this.logger.debug("Skipping mixin {}: disabled for mod compat ({})", mixin, disabledBecauseMod); + return false; + } + this.logger.debug("Applying mixin {}", mixin); + return true; } public boolean isOptionEnabled(String mixin) { 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 72e16eca..f790107c 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.commons.lang3.SystemUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.annotation.FeatureLevel; import org.embeddedt.modernfix.annotation.IgnoreOutsideDev; +import org.embeddedt.modernfix.annotation.RequiresFeatureLevel; import org.embeddedt.modernfix.annotation.RequiresMod; import org.embeddedt.modernfix.core.ModernFixMixinPlugin; import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; @@ -65,6 +67,18 @@ public class ModernFixEarlyConfig { private static final String MIXIN_CLIENT_ONLY_DESC = Type.getDescriptor(ClientOnlyMixin.class); private static final String MIXIN_REQUIRES_MOD_DESC = Type.getDescriptor(RequiresMod.class); private static final String MIXIN_DEV_ONLY_DESC = Type.getDescriptor(IgnoreOutsideDev.class); + private static final String FEATURE_LEVEL_ANNOTATION_DESC = Type.getDescriptor(RequiresFeatureLevel.class); + + public static final FeatureLevel ACTIVE_FEATURE_LEVEL = resolveFeatureLevel(); + + private static FeatureLevel resolveFeatureLevel() { + String prop = System.getProperty("modernfix.stabilityLevel", "ga").toUpperCase(Locale.ROOT); + try { + return FeatureLevel.valueOf(prop); + } catch (IllegalArgumentException e) { + return FeatureLevel.GA; + } + } private static final Pattern PLATFORM_PREFIX = Pattern.compile("(neoforge|fabric|common)\\."); @@ -112,6 +126,7 @@ public class ModernFixEarlyConfig { return; boolean isMixin = false, isClientOnly = false, requiredModPresent = true, isDevOnly = false; String requiredModId = ""; + FeatureLevel requiredLevel = FeatureLevel.GA; for(AnnotationNode annotation : node.invisibleAnnotations) { if(Objects.equals(annotation.desc, MIXIN_DESC)) { isMixin = true; @@ -130,6 +145,15 @@ public class ModernFixEarlyConfig { } } else if(Objects.equals(annotation.desc, MIXIN_DEV_ONLY_DESC)) { isDevOnly = true; + } else if(Objects.equals(annotation.desc, FEATURE_LEVEL_ANNOTATION_DESC)) { + for(int i = 0; i < annotation.values.size(); i += 2) { + if(annotation.values.get(i).equals("value")) { + // ASM stores enum annotation values as String[]{typeDescriptor, constantName} + String[] enumVal = (String[]) annotation.values.get(i + 1); + requiredLevel = FeatureLevel.valueOf(enumVal[1]); + break; + } + } } } if(isMixin && (!isDevOnly || ModernFixPlatformHooks.INSTANCE.isDevEnv())) { @@ -138,6 +162,8 @@ public class ModernFixEarlyConfig { mixinsMissingMods.put(mixinClassName, requiredModId); else if(isClientOnly && !ModernFixPlatformHooks.INSTANCE.isClient()) mixinsMissingMods.put(mixinClassName, "[not client]"); + else if(!ACTIVE_FEATURE_LEVEL.isAtLeast(requiredLevel)) + mixinsMissingMods.put(mixinClassName, "[feature level: requires " + requiredLevel + "]"); String mixinCategoryName = "mixin." + mixinClassName.substring(0, mixinClassName.lastIndexOf('.')); mixinOptions.add(mixinCategoryName); } diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index 594c518d..5e05d496 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -5,6 +5,7 @@ public net.minecraft.client.renderer.block.model.multipart.MultiPart definition public net.minecraft.client.resources.model.ModelBakery$ModelBakerImpl public net.minecraft.client.resources.model.ModelBakery$ModelBakerImpl (Lnet/minecraft/client/resources/model/ModelBakery;Lnet/minecraft/client/resources/model/ModelBakery$TextureGetter;Lnet/minecraft/client/resources/model/ModelResourceLocation;)V public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRule +public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRule (Ljava/util/List;)V public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource (Ljava/util/List;)V public net.minecraft.world.level.levelgen.SurfaceRules$TestRuleSource