Merge 1.20 into 1.21.1

This commit is contained in:
embeddedt 2026-05-05 20:54:44 -04:00
commit 51f273fae4
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
8 changed files with 297 additions and 57 deletions

View File

@ -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.
<br><br>
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

View File

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

View File

@ -1,5 +1,6 @@
package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.core.Holder;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
@ -23,11 +24,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 = "<init>", at = @At("RETURN"))
private void useCompactJavaMap(Map<Holder<Attribute>, AttributeInstance> instances, CallbackInfo ci) {
this.instances = Map.copyOf(this.instances);
this.instances = new Object2ObjectOpenHashMap<>(this.instances);
}
}
}

View File

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

View File

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

View File

@ -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<String, ILaunchPluginService> plugins = (Map<String, ILaunchPluginService>)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<Phase> GO = EnumSet.of(Phase.AFTER);
private static final EnumSet<Phase> NOGO = EnumSet.noneOf(Phase.class);
private static final Map<String, Transformer> TRANSFORMERS = Map.of(
"net.minecraftforge.common.capabilities.CapabilityProvider", new CapabilityProviderTransformer()
);
@Override
public EnumSet<Phase> 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);
}
}

View File

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

View File

@ -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<ResourceKey<Biome>, List<SurfaceRules.RuleSource>> perBiomeSources = new Reference2ObjectOpenHashMap<>();
Reference2ObjectOpenHashMap<ResourceKey<Biome>, List<SurfaceRules.RuleSource>> 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<SurfaceRules.RuleSource> 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<ResourceKey<Biome>, List<SurfaceRules.SurfaceRule>> compiledBiomeMatch = new Reference2ObjectOpenHashMap<>(perBiomeSources.size());
Reference2ObjectMaps.fastForEach(perBiomeSources, entry -> {
List<SurfaceRules.SurfaceRule> compiled = new ArrayList<>(entry.getValue().size());
for (var src : entry.getValue()) {
compiled.add(src.apply(context));
}
compiledBiomeMatch.put(entry.getKey(), List.copyOf(compiled));
});
List<SurfaceRules.SurfaceRule> 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<ResourceKey<Biome>, List<SurfaceRules.RuleSource>> sourcesForBiomeMatch,
List<SurfaceRules.RuleSource> sourcesForNoBiomeMatch
) implements SurfaceRules.RuleSource {
@Override
public SurfaceRules.SurfaceRule apply(SurfaceRules.Context context) {
var sourcesForBiomeMatch = this.sourcesForBiomeMatch;
Reference2ObjectOpenHashMap<ResourceKey<Biome>, List<SurfaceRules.SurfaceRule>> 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<? extends SurfaceRules.RuleSource> codec() {
throw new UnsupportedOperationException("Do not try to serialize OptimizedBiomeLookupSequenceRule");
}
}
private record CompiledOptimizedBiomeLookupRule(
Map<ResourceKey<Biome>, List<SurfaceRules.SurfaceRule>> rulesForBiomeMatch,
List<SurfaceRules.SurfaceRule> 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);
}