diff --git a/gradle.properties b/gradle.properties index a17082037..e43478ece 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ accesstransformers_version=11.0.2 eventbus_version=8.0.5 bootstraplauncher_version=1.1.8 asm_version=9.7 -mixin_version=0.14.0+mixin.0.8.6 +mixin_version=0.16.4+mixin.0.8.7 terminalconsoleappender_version=1.3.0 nightconfig_version=3.8.0 jetbrains_annotations_version=26.0.2 diff --git a/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java b/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java index f5b962253..b121d666d 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java +++ b/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java @@ -83,7 +83,7 @@ public void addMixinConfigs() { final String modId = file.getModInfos().get(0).getModId(); for (ModFileParser.MixinConfig potential : file.getMixinConfigs()) { if (potential.requiredMods().stream().allMatch(id -> this.getModFileById(id) != null)) { - DeferredMixinConfigRegistration.addMixinConfig(potential.config(), modId); + DeferredMixinConfigRegistration.addMixinConfig(potential.config(), modId, potential.behaviorVersion()); } else { LOGGER.debug("Mixin config {} for mod {} not applied as required mods are missing", potential.config(), modId); } diff --git a/loader/src/main/java/net/neoforged/fml/loading/mixin/DeferredMixinConfigRegistration.java b/loader/src/main/java/net/neoforged/fml/loading/mixin/DeferredMixinConfigRegistration.java index 4c400e92c..536873be6 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/mixin/DeferredMixinConfigRegistration.java +++ b/loader/src/main/java/net/neoforged/fml/loading/mixin/DeferredMixinConfigRegistration.java @@ -8,6 +8,8 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,7 +24,7 @@ public class DeferredMixinConfigRegistration { private static boolean added = false; - record ConfigInfo(String fileName, @Nullable String modId) {} + record ConfigInfo(String fileName, @Nullable String modId, int behaviorVersion) {} private static final List mixinConfigs = new ArrayList<>(); @@ -39,11 +41,30 @@ public static void addMixinConfig(String config) { } public static void addMixinConfig(String config, @Nullable String modId) { + addMixinConfig(config, modId, null); + } + + @ApiStatus.Internal + public static void addMixinConfig(String config, @Nullable String modId, @Nullable ArtifactVersion behaviorVersion) { if (added) { throw new IllegalStateException("Too late to add mixin configs!"); } - mixinConfigs.add(new ConfigInfo(config, modId)); + mixinConfigs.add(new ConfigInfo(config, modId, calculateBehaviorVersion(behaviorVersion))); + } + + // Increment to break compatibility; during a BC window, this should be set to the latest version. This is _not_ set + // to COMPATIBILITY_LATEST, so that if mixin is bumped past a BC it does not break mods. + @ApiStatus.Internal + public static final int DEFAULT_BEHAVIOUR_VERSION = FabricUtil.COMPATIBILITY_0_14_0; + + private static int calculateBehaviorVersion(@Nullable ArtifactVersion behaviorVersion) { + if (behaviorVersion == null) { + return DEFAULT_BEHAVIOUR_VERSION; + } + return behaviorVersion.getMajorVersion() * (1000 * 1000) + + behaviorVersion.getMinorVersion() * 1000 + + behaviorVersion.getIncrementalVersion(); } static void registerConfigs() { @@ -59,6 +80,7 @@ static void registerConfigs() { LOG.warn("Config file {} was not registered!", cfg.fileName()); } else { config.decorate(FabricUtil.KEY_MOD_ID, cfg.modId()); + config.decorate(FabricUtil.KEY_COMPATIBILITY, cfg.behaviorVersion()); } }); mixinConfigs.clear(); diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileParser.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileParser.java index 6a25a85ce..eeff9eeb2 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileParser.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileParser.java @@ -10,19 +10,25 @@ import com.electronwill.nightconfig.core.file.FileConfig; import com.electronwill.nightconfig.toml.TomlFormat; import com.mojang.logging.LogUtils; +import java.lang.module.ModuleDescriptor; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Optional; import net.neoforged.fml.loading.LogMarkers; +import net.neoforged.fml.loading.mixin.DeferredMixinConfigRegistration; import net.neoforged.fml.loading.moddiscovery.readers.JarModsDotTomlModFileReader; import net.neoforged.neoforgespi.language.IConfigurable; import net.neoforged.neoforgespi.language.IModFileInfo; import net.neoforged.neoforgespi.locating.IModFile; import net.neoforged.neoforgespi.locating.InvalidModFileException; import net.neoforged.neoforgespi.locating.ModFileInfoParser; +import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; +import org.spongepowered.asm.mixin.FabricUtil; public class ModFileParser { private static final Logger LOGGER = LogUtils.getLogger(); @@ -63,10 +69,32 @@ private static UnmodifiableConfig copyConfig(ConcurrentConfig config) { /** * Represents a potential mixin configuration. * - * @param config The name of the mixin configuration. - * @param requiredMods The mod ids that are required for this mixin configuration to be loaded. If empty, will be loaded regardless. + * @param config The name of the mixin configuration. + * @param requiredMods The mod ids that are required for this mixin configuration to be loaded. If empty, will be loaded regardless. + * @param behaviorVersion The mixin version whose behavior this configuration requests; if unspecified, the default is provided by FML. */ - public record MixinConfig(String config, List requiredMods) {} + public record MixinConfig(String config, List requiredMods, @Nullable ArtifactVersion behaviorVersion) { + public MixinConfig(String config, List requiredMods) { + this(config, requiredMods, null); + } + } + + private static final ArtifactVersion HIGHEST_MIXIN_VERSION; + private static final ArtifactVersion LOWEST_MIXIN_VERSION; + + static { + HIGHEST_MIXIN_VERSION = new DefaultArtifactVersion(Optional.ofNullable(FabricUtil.class.getModule().getDescriptor()) + .flatMap(ModuleDescriptor::version).map(ModuleDescriptor.Version::toString) + .or(() -> Optional.ofNullable(FabricUtil.class.getPackage().getImplementationVersion())) + .orElseThrow(() -> new IllegalStateException("Cannot determine version of currently running mixin"))); + int defaultMixinVersion = DeferredMixinConfigRegistration.DEFAULT_BEHAVIOUR_VERSION; + int patch = defaultMixinVersion % 1000; + defaultMixinVersion /= 1000; + int minor = defaultMixinVersion % 1000; + defaultMixinVersion /= 1000; + int major = defaultMixinVersion; + LOWEST_MIXIN_VERSION = new DefaultArtifactVersion(major + "." + minor + "." + patch); + } protected static List getMixinConfigs(IModFileInfo modFileInfo) { try { @@ -78,7 +106,21 @@ protected static List getMixinConfigs(IModFileInfo modFileInfo) { var name = mixinsEntry.getConfigElement("config") .orElseThrow(() -> new InvalidModFileException("Missing \"config\" in [[mixins]] entry", modFileInfo)); var requiredModIds = mixinsEntry.>getConfigElement("requiredMods").orElse(List.of()); - potentialMixins.add(new MixinConfig(name, requiredModIds)); + var behaviorVersion = mixinsEntry.getConfigElement("behaviorVersion") + .map(DefaultArtifactVersion::new) + .orElse(null); + if (behaviorVersion != null) { + if (behaviorVersion.compareTo(HIGHEST_MIXIN_VERSION) > 0) { + throw new InvalidModFileException("Specified mixin behavior version " + behaviorVersion + + " is higher than the current mixin version " + HIGHEST_MIXIN_VERSION + "; this may be fixable by updating neoforge", + modFileInfo); + } else if (behaviorVersion.compareTo(LOWEST_MIXIN_VERSION) < 0) { + throw new InvalidModFileException("Specified mixin behavior version " + behaviorVersion + + " is lower than the minimum supported behavior version " + LOWEST_MIXIN_VERSION, + modFileInfo); + } + } + potentialMixins.add(new MixinConfig(name, requiredModIds, behaviorVersion)); } return potentialMixins;