From b765bcb51f4d335d639ce50543c6b11fde81fc9e Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:27:07 -0400 Subject: [PATCH 1/9] Improve compatibility with mods that inject into ModelBaker.bake Fixes #646 --- .../mixin/perf/dynamic_resources/ModelBakeryMixin.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelBakeryMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelBakeryMixin.java index f8a08e42..d3b3a5f9 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelBakeryMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelBakeryMixin.java @@ -374,7 +374,12 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { ModelBakery self = (ModelBakery) (Object) this; ModelBaker theBaker = self.new ModelBakerImpl(textureGetter, modelLocation); ((IExtendedModelBaker)theBaker).throwOnMissingModel(true); - synchronized(this) { m = theBaker.bake(modelLocation, state, theBaker.getModelTextureGetter()); } + synchronized(this) { + // We intentionally use the 2-arg overload for better mixin compatibility, because we use the baker's default + // texture getter anyway. + //noinspection deprecation + m = theBaker.bake(modelLocation, state); + } if(m != null) loadedBakedModels.put(key, m); return m; From 46dd5ecddda040f265462943b663c269fcb1c762 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:36:59 -0400 Subject: [PATCH 2/9] Comment on issues when fix is released Fixes #649 --- .github/workflows/gradle.yml | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 5b621513..037848bf 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -11,6 +11,8 @@ on: jobs: build: runs-on: ubuntu-22.04 + permissions: + issues: write concurrency: group: release-${{ github.ref }} cancel-in-progress: true @@ -52,6 +54,57 @@ jobs: env: CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + - name: Capture mod version + if: steps.check_branch.outputs.is_release == 'true' + run: echo "MOD_VERSION=$(./gradlew properties -q | grep '^version:' | awk '{print $2}')" >> $GITHUB_ENV + - name: Comment on fixed issues + if: steps.check_branch.outputs.is_release == 'true' + uses: actions/github-script@v7 + with: + script: | + const { execSync } = require('child_process'); + + const branch = context.ref.replace('refs/heads/', ''); + const { data: runs } = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'gradle.yml', + branch, + status: 'success', + per_page: 1 + }); + + const logArgs = runs.workflow_runs.length > 0 + ? `${runs.workflow_runs[0].head_sha}..${context.sha}` + : `-1 ${context.sha}`; + const log = execSync(`git log ${logArgs} --format=%s%n%b`, { encoding: 'utf8' }); + + const issueNumbers = new Set(); + const pattern = /(?:fix(?:es|ed)?|close[sd]?|resolve[sd]?)\s+#(\d+)/gi; + let match; + while ((match = pattern.exec(log)) !== null) { + issueNumbers.add(parseInt(match[1])); + } + + if (issueNumbers.size === 0) { + console.log('No fixed issues found in commits'); + return; + } + + const version = process.env.MOD_VERSION; + for (const issueNumber of issueNumbers) { + try { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `The fix for this issue has been released in ModernFix ${version}` + }); + console.log(`Commented on issue #${issueNumber}`); + } catch (e) { + console.log(`Could not comment on #${issueNumber}: ${e.message}`); + } + } - name: Upload Artifacts to GitHub uses: actions/upload-artifact@v4 with: From a40363c1fb2c247e14e1b7cba8d0811489ca4cb0 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:42:45 -0400 Subject: [PATCH 3/9] Improve issue comment workflow [skip ci] --- .github/workflows/gradle.yml | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 037848bf..b22cbe7f 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -56,7 +56,9 @@ jobs: MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} - name: Capture mod version if: steps.check_branch.outputs.is_release == 'true' - run: echo "MOD_VERSION=$(./gradlew properties -q | grep '^version:' | awk '{print $2}')" >> $GITHUB_ENV + run: | + echo "MOD_VERSION=$(./gradlew properties -q | grep '^version:' | awk '{print $2}')" >> $GITHUB_ENV + echo "MC_VERSION=$(grep '^minecraft_version=' gradle.properties | cut -d= -f2)" >> $GITHUB_ENV - name: Comment on fixed issues if: steps.check_branch.outputs.is_release == 'true' uses: actions/github-script@v7 @@ -91,16 +93,38 @@ jobs: return; } - const version = process.env.MOD_VERSION; + const MARKER = ''; + const modVersion = process.env.MOD_VERSION; + const mcVersion = process.env.MC_VERSION; + const newLine = `- ${modVersion} for Minecraft ${mcVersion}`; + for (const issueNumber of issueNumbers) { try { - await github.rest.issues.createComment({ + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, - body: `The fix for this issue has been released in ModernFix ${version}` + per_page: 100 }); - console.log(`Commented on issue #${issueNumber}`); + + const existing = comments.find(c => c.body.includes(MARKER)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: existing.body + `\n${newLine}` + }); + console.log(`Updated comment on issue #${issueNumber}`); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `${MARKER}\nThe fix for this issue has been released in the following versions of ModernFix:\n${newLine}` + }); + console.log(`Created comment on issue #${issueNumber}`); + } } catch (e) { console.log(`Could not comment on #${issueNumber}: ${e.message}`); } From 4e3ecf9b6d7ab3ebecbc0604db916cd4922689fc Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Mon, 27 Apr 2026 19:52:08 -0400 Subject: [PATCH 4/9] Disable `mixin.perf.release_protochunks` when Moonrise is present Fixes #652 --- .../embeddedt/modernfix/core/config/ModernFixEarlyConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3743d065..7bc8a9e6 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -241,7 +241,7 @@ public class ModernFixEarlyConfig { disableIfModPresent("mixin.bugfix.item_cache_flag", "lithium", "canary", "radium"); // DimThread makes changes to the server chunk manager (understandably), C2ME probably does the same disableIfModPresent("mixin.bugfix.chunk_deadlock", "c2me", "dimthread"); - disableIfModPresent("mixin.perf.release_protochunks", "c2me"); + disableIfModPresent("mixin.perf.release_protochunks", "c2me", "moonrise"); disableIfModPresent("mixin.launch.class_search_cache", "optifine"); disableIfModPresent("mixin.perf.faster_texture_stitching", "optifine"); disableIfModPresent("mixin.bugfix.entity_pose_stack", "optifine"); From c73cdc49a4e760524f229a0f00f9ea83c38f40d9 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Tue, 28 Apr 2026 18:46:09 -0400 Subject: [PATCH 5/9] Replace CapabilityProvider mixin with ASM transformer Works around this Mixin bug: https://github.com/FabricMC/Mixin/issues/146 Since CapabilityProvider is the parent of many commonly targeted classes like Level, ItemStack, etc., this breaks mods Fixes #650 --- .../CapabilityProviderMixin.java | 39 ------- .../modernfix/core/ModernFixMixinPlugin.java | 3 +- .../launchplugin/CoreLaunchPluginService.java | 73 +++++++++++++ .../CapabilityProviderTransformer.java | 102 ++++++++++++++++++ 4 files changed, 177 insertions(+), 40 deletions(-) delete mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/CapabilityProviderMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/core/launchplugin/CoreLaunchPluginService.java create mode 100644 src/main/java/org/embeddedt/modernfix/core/launchplugin/transformer/CapabilityProviderTransformer.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/CapabilityProviderMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/CapabilityProviderMixin.java deleted file mode 100644 index 6e68f94a..00000000 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/CapabilityProviderMixin.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.embeddedt.modernfix.common.mixin.perf.faster_capabilities; - -import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import net.minecraft.nbt.CompoundTag; -import net.minecraftforge.common.capabilities.CapabilityProvider; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; - -import java.util.Objects; - -@Mixin(value = CapabilityProvider.class, remap = false) -@SuppressWarnings("rawtypes") -public class CapabilityProviderMixin { - @Shadow - private boolean isLazy; - - @Shadow - private boolean initialized; - - @Shadow - private CompoundTag lazyData; - - /** - * @author embeddedt - * @reason avoid initializing capabilities in the case where we can reasonably expect the lazy data - * to deserialize to the same concrete caps - */ - @WrapMethod(method = "areCapsCompatible(Lnet/minecraftforge/common/capabilities/CapabilityProvider;)Z") - private boolean mfix$skipLazyInit(CapabilityProvider other, Operation original) { - if (this.isLazy && !this.initialized) { - var otherAcc = ((CapabilityProviderMixin)(Object)other); - if (otherAcc.isLazy && !otherAcc.initialized && otherAcc.getClass() == this.getClass() && Objects.equals(this.lazyData, otherAcc.lazyData)) { - return true; - } - } - return original.call(other); - } -} diff --git a/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java b/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java index c6a52cf1..05bf9147 100644 --- a/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java +++ b/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java @@ -5,6 +5,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.embeddedt.modernfix.core.config.ModernFixEarlyConfig; import org.embeddedt.modernfix.core.config.Option; +import org.embeddedt.modernfix.core.launchplugin.CoreLaunchPluginService; import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; import org.embeddedt.modernfix.world.ThreadDumper; import org.objectweb.asm.Opcodes; @@ -109,7 +110,7 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin { @Override public void onLoad(String mixinPackage) { - + CoreLaunchPluginService.install(); } @Override diff --git a/src/main/java/org/embeddedt/modernfix/core/launchplugin/CoreLaunchPluginService.java b/src/main/java/org/embeddedt/modernfix/core/launchplugin/CoreLaunchPluginService.java new file mode 100644 index 00000000..01db62fe --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/core/launchplugin/CoreLaunchPluginService.java @@ -0,0 +1,73 @@ +package org.embeddedt.modernfix.core.launchplugin; + +import cpw.mods.modlauncher.LaunchPluginHandler; +import cpw.mods.modlauncher.Launcher; +import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import org.embeddedt.modernfix.core.launchplugin.transformer.CapabilityProviderTransformer; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.Map; + +public class CoreLaunchPluginService implements ILaunchPluginService { + private static final Logger LOGGER = LoggerFactory.getLogger("ModernFixLaunchPlugin"); + + public static void install() { + try { + Field launchPluginsField = Launcher.class.getDeclaredField("launchPlugins"); + launchPluginsField.setAccessible(true); + LaunchPluginHandler launchPluginHandler = (LaunchPluginHandler) launchPluginsField.get(Launcher.INSTANCE); + Field pluginsField = LaunchPluginHandler.class.getDeclaredField("plugins"); + pluginsField.setAccessible(true); + Map plugins = (Map)pluginsField.get(launchPluginHandler); + var service = new CoreLaunchPluginService(); + try { + plugins.put(service.name(), service); + } catch (Exception e) { + var newMap = new LinkedHashMap<>(plugins); + newMap.put(service.name(), service); + pluginsField.set(launchPluginHandler, newMap); + } + } catch(Exception e) { + LOGGER.error("Error installing launch plugin service", e); + } + } + + @Override + public String name() { + return "modernfix"; + } + + private static final EnumSet GO = EnumSet.of(Phase.AFTER); + private static final EnumSet NOGO = EnumSet.noneOf(Phase.class); + + private static final Map TRANSFORMERS = Map.of( + "net.minecraftforge.common.capabilities.CapabilityProvider", new CapabilityProviderTransformer() + ); + + @Override + public EnumSet handlesClass(Type classType, boolean isEmpty) { + return !isEmpty && TRANSFORMERS.containsKey(classType.getClassName()) ? GO : NOGO; + } + + @Override + public int processClassWithFlags(Phase phase, ClassNode classNode, Type classType, String reason) { + if (classNode == null) { + return 0; + } + var transformer = TRANSFORMERS.get(classType.getClassName()); + if (transformer == null) { + return 0; + } + return transformer.transform(classNode); + } + + public interface Transformer { + int transform(ClassNode node); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/core/launchplugin/transformer/CapabilityProviderTransformer.java b/src/main/java/org/embeddedt/modernfix/core/launchplugin/transformer/CapabilityProviderTransformer.java new file mode 100644 index 00000000..d02da71a --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/core/launchplugin/transformer/CapabilityProviderTransformer.java @@ -0,0 +1,102 @@ +package org.embeddedt.modernfix.core.launchplugin.transformer; + +import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import org.embeddedt.modernfix.core.launchplugin.CoreLaunchPluginService; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.*; + +/** + * Injects an early-return into {@code CapabilityProvider#areCapsCompatible} that skips lazy + * initialization when both providers are lazy, uninitialized, of the same class, and carry equal + * {@code lazyData}. In that case the capabilities are trivially compatible and the full init can + * be avoided. + * @author embeddedt + */ +public class CapabilityProviderTransformer implements CoreLaunchPluginService.Transformer { + private static final String OWNER = "net/minecraftforge/common/capabilities/CapabilityProvider"; + private static final String COMPOUND_TAG = "net/minecraft/nbt/CompoundTag"; + private static final String HELPER_NAME = "mfix$skipLazyInit"; + private static final String HELPER_DESC = "(L" + OWNER + ";L" + OWNER + ";)Z"; + + @Override + public int transform(ClassNode node) { + String targetDesc = "(L" + OWNER + ";)Z"; + for (MethodNode method : node.methods) { + if (method.name.equals("areCapsCompatible") && method.desc.equals(targetDesc)) { + injectCall(method); + node.methods.add(buildHelper()); + break; + } + } + return ILaunchPluginService.ComputeFlags.COMPUTE_FRAMES; + } + + private MethodNode buildHelper() { + MethodNode mn = new MethodNode( + Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC, + HELPER_NAME, HELPER_DESC, null, null); + InsnList il = mn.instructions; + LabelNode returnFalse = new LabelNode(); + + // if (!self.isLazy) goto returnFalse + il.add(new VarInsnNode(Opcodes.ALOAD, 0)); + il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "isLazy", "Z")); + il.add(new JumpInsnNode(Opcodes.IFEQ, returnFalse)); + + // if (self.initialized) goto returnFalse + il.add(new VarInsnNode(Opcodes.ALOAD, 0)); + il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "initialized", "Z")); + il.add(new JumpInsnNode(Opcodes.IFNE, returnFalse)); + + // if (!other.isLazy) goto returnFalse + il.add(new VarInsnNode(Opcodes.ALOAD, 1)); + il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "isLazy", "Z")); + il.add(new JumpInsnNode(Opcodes.IFEQ, returnFalse)); + + // if (other.initialized) goto returnFalse + il.add(new VarInsnNode(Opcodes.ALOAD, 1)); + il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "initialized", "Z")); + il.add(new JumpInsnNode(Opcodes.IFNE, returnFalse)); + + // if (other.getClass() != self.getClass()) goto returnFalse + il.add(new VarInsnNode(Opcodes.ALOAD, 1)); + il.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false)); + il.add(new VarInsnNode(Opcodes.ALOAD, 0)); + il.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false)); + il.add(new JumpInsnNode(Opcodes.IF_ACMPNE, returnFalse)); + + // if (!Objects.equals(self.lazyData, other.lazyData)) goto returnFalse + il.add(new VarInsnNode(Opcodes.ALOAD, 0)); + il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "lazyData", "L" + COMPOUND_TAG + ";")); + il.add(new VarInsnNode(Opcodes.ALOAD, 1)); + il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "lazyData", "L" + COMPOUND_TAG + ";")); + il.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/util/Objects", "equals", "(Ljava/lang/Object;Ljava/lang/Object;)Z", false)); + il.add(new JumpInsnNode(Opcodes.IFEQ, returnFalse)); + + il.add(new InsnNode(Opcodes.ICONST_1)); + il.add(new InsnNode(Opcodes.IRETURN)); + + il.add(returnFalse); + il.add(new InsnNode(Opcodes.ICONST_0)); + il.add(new InsnNode(Opcodes.IRETURN)); + + mn.maxLocals = 2; + mn.maxStack = 2; + return mn; + } + + private void injectCall(MethodNode method) { + InsnList il = new InsnList(); + LabelNode skip = new LabelNode(); + + il.add(new VarInsnNode(Opcodes.ALOAD, 0)); + il.add(new VarInsnNode(Opcodes.ALOAD, 1)); + il.add(new MethodInsnNode(Opcodes.INVOKESTATIC, OWNER, HELPER_NAME, HELPER_DESC, false)); + il.add(new JumpInsnNode(Opcodes.IFEQ, skip)); + il.add(new InsnNode(Opcodes.ICONST_1)); + il.add(new InsnNode(Opcodes.IRETURN)); + il.add(skip); + + method.instructions.insert(il); + } +} From 1165d3bdd1dbf175efb7345a54335cc08efc1343 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:41:56 -0400 Subject: [PATCH 6/9] Fix Crash Assistant treating a mixin audit as a crash --- src/main/java/org/embeddedt/modernfix/ModernFix.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/embeddedt/modernfix/ModernFix.java b/src/main/java/org/embeddedt/modernfix/ModernFix.java index da1d2bee..57998497 100644 --- a/src/main/java/org/embeddedt/modernfix/ModernFix.java +++ b/src/main/java/org/embeddedt/modernfix/ModernFix.java @@ -2,6 +2,7 @@ package org.embeddedt.modernfix; import net.minecraft.SharedConstants; import net.minecraft.Util; +import net.minecraft.client.Minecraft; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ServerLevel; @@ -51,6 +52,8 @@ public class ModernFix { if (auditAndExit || Boolean.getBoolean("modernfix.auditMixinsAtStart")) { MixinEnvironment.getCurrentEnvironment().audit(); if (auditAndExit) { + // Prevents Crash Assistant from treating mixin audit as a crash + Minecraft.getInstance().stop(); System.exit(0); } } From 44113d253616f30c09f08e83dd68b1e8d6fdfb48 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Tue, 5 May 2026 19:41:28 -0400 Subject: [PATCH 7/9] Improve efficiency of surface rule optimizer when rules are complex --- .../SequenceRuleSourceMixin.java | 14 +++- .../world/gen/SurfaceRuleOptimizer.java | 70 +++++++++++++------ 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SequenceRuleSourceMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SequenceRuleSourceMixin.java index a599aa7b..428ed584 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SequenceRuleSourceMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SequenceRuleSourceMixin.java @@ -3,17 +3,25 @@ package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules; import net.minecraft.world.level.levelgen.SurfaceRules; import org.embeddedt.modernfix.world.gen.SurfaceRuleOptimizer; import org.spongepowered.asm.mixin.Mixin; +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.CallbackInfoReturnable; @Mixin(targets = {"net/minecraft/world/level/levelgen/SurfaceRules$SequenceRuleSource"}) public class SequenceRuleSourceMixin { + @Unique + private transient SurfaceRules.RuleSource mfix$optimizedSource; + @Inject(method = "apply(Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;)Lnet/minecraft/world/level/levelgen/SurfaceRules$SurfaceRule;", at = @At("HEAD"), cancellable = true) private void optimizeApply(SurfaceRules.Context context, CallbackInfoReturnable cir) { - var optimized = SurfaceRuleOptimizer.optimizeSequenceRule((SurfaceRules.SequenceRuleSource)(Object) this, context); - if (optimized != null) { - cir.setReturnValue(optimized); + var optimizedSource = mfix$optimizedSource; + if (optimizedSource == null) { + mfix$optimizedSource = optimizedSource = SurfaceRuleOptimizer.optimizeSequenceRuleSource((SurfaceRules.SequenceRuleSource)(Object) this); + } + // Must check for it not being ourselves, to avoid infinite reentrance + if (optimizedSource != (Object)this) { + cir.setReturnValue(optimizedSource.apply(context)); } } } diff --git a/src/main/java/org/embeddedt/modernfix/world/gen/SurfaceRuleOptimizer.java b/src/main/java/org/embeddedt/modernfix/world/gen/SurfaceRuleOptimizer.java index 42492fa1..40b13864 100644 --- a/src/main/java/org/embeddedt/modernfix/world/gen/SurfaceRuleOptimizer.java +++ b/src/main/java/org/embeddedt/modernfix/world/gen/SurfaceRuleOptimizer.java @@ -1,9 +1,11 @@ package org.embeddedt.modernfix.world.gen; +import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; import it.unimi.dsi.fastutil.objects.Reference2ObjectMaps; import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; import net.minecraft.core.Holder; import net.minecraft.resources.ResourceKey; +import net.minecraft.util.KeyDispatchDataCodec; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.levelgen.SurfaceRules; @@ -15,25 +17,33 @@ import java.util.Map; import java.util.Objects; public class SurfaceRuleOptimizer { - public static @Nullable SurfaceRules.SurfaceRule optimizeSequenceRule(SurfaceRules.SequenceRuleSource source, SurfaceRules.Context context) { + public static SurfaceRules.RuleSource optimizeSequenceRuleSource(SurfaceRules.SequenceRuleSource source) { // First pass: collect which biomes appear and count biome-gated branches - Reference2ObjectOpenHashMap, List> perBiomeSources = new Reference2ObjectOpenHashMap<>(); + Reference2ObjectOpenHashMap, List> perBiomeSources = null; int biomeGatedBranches = 0; - for (var innerSource : source.sequence()) { - if (innerSource instanceof SurfaceRules.TestRuleSource testRuleSource + var sequence = source.sequence(); + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < sequence.size(); i++) { + if (sequence.get(i) instanceof SurfaceRules.TestRuleSource testRuleSource && testRuleSource.ifTrue() instanceof SurfaceRules.BiomeConditionSource biomeConditionSource) { biomeGatedBranches++; + if (perBiomeSources == null) { + perBiomeSources = new Reference2ObjectOpenHashMap<>(); + } for (var biome : biomeConditionSource.biomes) { perBiomeSources.putIfAbsent(biome, new ArrayList<>()); } } } if (biomeGatedBranches < 3) { - return null; + // Just use the source as-is, not worth optimizing + return source; } // Second pass: build per-biome source lists preserving original interleaving order List noMatchSources = new ArrayList<>(); - for (var innerSource : source.sequence()) { + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < sequence.size(); i++) { + var innerSource = sequence.get(i); if (innerSource instanceof SurfaceRules.TestRuleSource testRuleSource && testRuleSource.ifTrue() instanceof SurfaceRules.BiomeConditionSource biomeConditionSource) { // Add the inner rule (condition stripped) only to the matching biomes' lists @@ -48,23 +58,41 @@ public class SurfaceRuleOptimizer { noMatchSources.add(innerSource); } } - // Compile all source lists into rule lists - Reference2ObjectOpenHashMap, List> compiledBiomeMatch = new Reference2ObjectOpenHashMap<>(perBiomeSources.size()); - Reference2ObjectMaps.fastForEach(perBiomeSources, entry -> { - List compiled = new ArrayList<>(entry.getValue().size()); - for (var src : entry.getValue()) { - compiled.add(src.apply(context)); - } - compiledBiomeMatch.put(entry.getKey(), List.copyOf(compiled)); - }); - List compiledNoMatch = new ArrayList<>(noMatchSources.size()); - for (var src : noMatchSources) { - compiledNoMatch.add(src.apply(context)); - } - return new OptimizedBiomeLookupSequenceRule(compiledBiomeMatch, List.copyOf(compiledNoMatch), context); + return new OptimizedBiomeLookupSequenceRule(perBiomeSources, List.copyOf(noMatchSources)); } public record OptimizedBiomeLookupSequenceRule( + Reference2ObjectMap, List> sourcesForBiomeMatch, + List sourcesForNoBiomeMatch + ) implements SurfaceRules.RuleSource { + @Override + public SurfaceRules.SurfaceRule apply(SurfaceRules.Context context) { + var sourcesForBiomeMatch = this.sourcesForBiomeMatch; + Reference2ObjectOpenHashMap, List> compiledBiomeMatch = + new Reference2ObjectOpenHashMap<>(sourcesForBiomeMatch.size()); + Reference2ObjectMaps.fastForEach(sourcesForBiomeMatch, entry -> { + SurfaceRules.SurfaceRule[] compiled = new SurfaceRules.SurfaceRule[entry.getValue().size()]; + var uncompiled = entry.getValue(); + for (int i = 0; i < uncompiled.size(); i++) { + compiled[i] = uncompiled.get(i).apply(context); + } + compiledBiomeMatch.put(entry.getKey(), List.of(compiled)); + }); + var sourcesForNoBiomeMatch = this.sourcesForNoBiomeMatch; + SurfaceRules.SurfaceRule[] compiledNoMatch = new SurfaceRules.SurfaceRule[sourcesForNoBiomeMatch.size()]; + for (int i = 0; i < sourcesForNoBiomeMatch.size(); i++) { + compiledNoMatch[i] = sourcesForNoBiomeMatch.get(i).apply(context); + } + return new CompiledOptimizedBiomeLookupRule(compiledBiomeMatch, List.of(compiledNoMatch), context); + } + + @Override + public KeyDispatchDataCodec codec() { + throw new UnsupportedOperationException("Do not try to serialize OptimizedBiomeLookupSequenceRule"); + } + } + + private record CompiledOptimizedBiomeLookupRule( Map, List> rulesForBiomeMatch, List rulesForNoBiomeMatch, SurfaceRules.Context context @@ -88,7 +116,7 @@ public class SurfaceRuleOptimizer { @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; - OptimizedBiomeLookupSequenceRule that = (OptimizedBiomeLookupSequenceRule) o; + CompiledOptimizedBiomeLookupRule that = (CompiledOptimizedBiomeLookupRule) o; return rulesForBiomeMatch.equals(that.rulesForBiomeMatch) && rulesForNoBiomeMatch.equals(that.rulesForNoBiomeMatch); } From 653a477060f4a26a9e7442c5bf583160eea09060 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Tue, 5 May 2026 20:23:06 -0400 Subject: [PATCH 8/9] Fix crash when mods use null attributes Fixes #658 --- .../attribute_supplier_dedup/AttributeSupplierMixin.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java index 5ffc3489..d4d0901b 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java @@ -1,5 +1,6 @@ package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.minecraft.world.entity.ai.attributes.Attribute; import net.minecraft.world.entity.ai.attributes.AttributeInstance; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; @@ -22,11 +23,11 @@ public class AttributeSupplierMixin { /** * @author embeddedt - * @reason Java 9's Map.of() implementation is significantly more compact than ImmutableMap, and we do not + * @reason more compact than ImmutableMap due to less wrapper objects, and we do not * care about insertion order in this context */ @Inject(method = "", at = @At("RETURN")) private void useCompactJavaMap(Map instances, CallbackInfo ci) { - this.instances = Map.copyOf(this.instances); + this.instances = new Object2ObjectOpenHashMap<>(this.instances); } } \ No newline at end of file From a73dd5ef6aceb651739865dd0ade25387cb73932 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Tue, 5 May 2026 20:27:55 -0400 Subject: [PATCH 9/9] Update bug report template --- .github/ISSUE_TEMPLATE/bug_report.yml | 82 +++++++++++++++++---------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 113b6525..3b2feb02 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -4,51 +4,75 @@ body: - type: markdown attributes: value: >- - **Note: This issue tracker is not intended for support requests!** If you need help with crashes or other issues, then - you should [ask on our Discord server](https://discord.gg/rN9Y7caguP) instead. Unless you are certain that you - have found a defect, and you are able to point to where the problem is, you should not open an issue. -

- Additionally, please make sure you have done the following: + **Need help?** Ask on [Discord](https://discord.gg/rN9Y7caguP) instead of opening an issue. - - **Have you ensured that all of your mods (including ModernFix) are up-to-date?** The latest version of ModernFix - can always be found [on Modrinth](https://modrinth.com/mod/modernfix). - - - **Have you used the [search tool](https://github.com/embeddedt/ModernFix/issues) to check whether your issue - has already been reported?** If it has been, then consider adding more information to the existing issue instead. - - - **Have you determined the minimum set of instructions to reproduce the issue?** If your problem only occurs - with other mods installed, then you should narrow down exactly which mods are causing the issue. Please do not - provide your entire list of mods to us and expect that we will be able to figure out the problem. + **Issues that do not meet the requirements below (or are otherwise impossible to address with the given info) will be closed without investigation.** + - type: checkboxes + id: confirmations + attributes: + label: Checklist + options: + - label: I am reporting a defect, not asking for help + required: true + - label: I have searched existing issues and this has not been reported + required: true + - label: I have reduced my mod list to the minimum required to reproduce this issue (see below) + required: true - type: textarea id: description attributes: label: Bug Description description: >- - Use this section to describe the issue you are experiencing in as much depth as possible. The description should - explain what behavior you were expecting, and why you believe the issue to be a bug. If the issue you are reporting - only occurs with specific mods installed, then provide the name and version of each mod. + Describe the issue in detail. Be sure to include what you expected to happen and what actually happened. + validations: + required: true + - type: textarea + id: minimal-mods + attributes: + label: Minimal Mod List + description: >- + List ONLY the mods required to reproduce this issue. Maintainers have debugging tools that help them + locate problems quickly, but these generally don't work well in modpacks or large mod sets. + A minimal list should typically contain fewer than 10 mods. - **Hint:** If you have any screenshots, videos, or other information that you feel is necessary to - explain the issue, you can attach them here. + Reports with large mod lists will likely be closed without investigation, unless the problem is very clear. + + If you don't know which mods are causing your problem, use binary search: + + 1. Remove half your mods + + 2. Test if the issue still occurs + + 3. If yes, remove half again. If no, restore the last removed half and repeat from step 1. + + 4. Repeat until only the necessary mods remain + placeholder: "- ModernFix 5.x.x\n- SomeMod 1.2.3" + validations: + required: true - type: textarea id: description-reproduction-steps attributes: label: Reproduction Steps description: >- - Provide as much information as possible on how to reproduce this bug. Make sure your instructions are as clear and - concise as possible, because other people will need to be able to follow your guide in order to re-create the issue. - - **Hint:** A common way to fill this section out is to write a step-by-step guide. + Provide clear steps to reproduce the bug. Each step should be a single concrete action. + + Maintainers are busy and need to be able to quickly replicate your problem. Your reproduction steps should be + clear enough for someone who is unfamiliar with your mods to follow in 5 minutes or less (not counting time + to launch the game). + + Providing vague steps is likely to result in the issue being closed. + + placeholder: "1. \n2. \n3. " validations: required: true - type: textarea - id: log-file + id: diagnostic-info attributes: - label: Log File + label: Diagnostic Info description: >- - **Hint:** You can usually find the log files within the folder `.minecraft/logs`. Most often, you will want the `latest.log` - file, since that file belongs to the last played session of the game. - placeholder: >- - Drag-and-drop the log file here. + Drag and drop `latest.log` from `.minecraft/logs/` for the session where the issue occurred. + Do not paste log text inline. Issues without a valid `latest.log` will be closed. + + If a crash occurred, also attach the relevant file from `.minecraft/crash-reports/`. validations: required: true