From 7420a7c7ab748f15a2119461b502bec184c37f60 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:08:41 -0500 Subject: [PATCH 1/5] Dispatch getCapability calls using specialized ASM loop per provider types Idea suggested by @eigenraven --- .../CapabilityDispatcherMixin.java | 42 +++ ...CapabilityProviderDispatcherGenerator.java | 259 ++++++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/CapabilityDispatcherMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/CapabilityDispatcherMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/CapabilityDispatcherMixin.java new file mode 100644 index 00000000..6a5bdfd0 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/CapabilityDispatcherMixin.java @@ -0,0 +1,42 @@ +package org.embeddedt.modernfix.common.mixin.perf.faster_capabilities; + +import net.minecraft.core.Direction; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.CapabilityDispatcher; +import net.minecraftforge.common.capabilities.CapabilityProvider; +import net.minecraftforge.common.capabilities.ICapabilityProvider; +import net.minecraftforge.common.util.LazyOptional; +import org.embeddedt.modernfix.forge.capability.CapabilityProviderDispatcherGenerator; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +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.callback.CallbackInfo; + +import java.util.List; +import java.util.Map; + +@Mixin(CapabilityDispatcher.class) +public class CapabilityDispatcherMixin { + @Shadow + @Final + private ICapabilityProvider[] caps; + private ICapabilityProvider mfix$turboDispatcher; + + @Inject(method = "(Ljava/util/Map;Ljava/util/List;Lnet/minecraftforge/common/capabilities/ICapabilityProvider;)V", at = @At("RETURN")) + private void createTurboDispatcher(Map list, List listeners, ICapabilityProvider parent, CallbackInfo ci) { + this.mfix$turboDispatcher = CapabilityProviderDispatcherGenerator.getOrGenerateDispatcher(this.caps); + } + + /** + * @author embeddedt + * @reason use ASM-generated dispatcher + */ + @Overwrite(remap = false) + public LazyOptional getCapability(Capability cap, @Nullable Direction side) { + return this.mfix$turboDispatcher.getCapability(cap, side); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java new file mode 100644 index 00000000..8a70b589 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java @@ -0,0 +1,259 @@ +package org.embeddedt.modernfix.forge.capability; + +import net.minecraftforge.common.capabilities.ICapabilityProvider; +import org.embeddedt.modernfix.ModernFix; +import org.objectweb.asm.*; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static org.objectweb.asm.Opcodes.*; + +/** + * Generates optimized hidden classes for ICapabilityProvider dispatch. + * Each generated class unrolls the capability provider array into final fields + * and performs direct dispatch instead of megamorphic virtual calls. + */ +public class CapabilityProviderDispatcherGenerator { + + private static final ConcurrentHashMap>, MethodHandle> cache = + new ConcurrentHashMap<>(); + + private static final AtomicInteger classCounter = new AtomicInteger(0); + private static final MethodHandles.Lookup lookup = MethodHandles.lookup(); + + // Type descriptors + private static final String ICAP_PROVIDER_DESC = "Lnet/minecraftforge/common/capabilities/ICapabilityProvider;"; + private static final String CAPABILITY_DESC = "Lnet/minecraftforge/common/capabilities/Capability;"; + private static final String LAZY_OPTIONAL_DESC = "Lnet/minecraftforge/common/util/LazyOptional;"; + private static final String DIRECTION_DESC = "Lnet/minecraft/core/Direction;"; + + /** + * Gets or generates a constructor MethodHandle for the given capability provider types. + * The constructor takes an array of ICapabilityProvider instances. + * + * @param providerTypes The types of capability providers in order + * @return A MethodHandle to construct the optimized dispatcher + */ + private static MethodHandle getOrGenerateConstructor(List> providerTypes) { + return cache.computeIfAbsent(providerTypes, CapabilityProviderDispatcherGenerator::generateClass); + } + + /** + * Convenience method that takes an array of providers and returns the constructor. + */ + private static MethodHandle getOrGenerateConstructor(ICapabilityProvider[] providers) { + List> types = Arrays.stream(providers) + .>map(ICapabilityProvider::getClass) + .toList(); + return getOrGenerateConstructor(types); + } + + public static ICapabilityProvider getOrGenerateDispatcher(ICapabilityProvider[] providers) { + var handle = getOrGenerateConstructor(providers); + try { + return (ICapabilityProvider)handle.invokeExact((Object)providers); + } catch (Throwable e) { + throw new RuntimeException("Error constructing dispatcher", e); + } + } + + private static MethodHandle generateClass(List> providerTypes) { + ModernFix.LOGGER.debug("Generating capability dispatcher for types: [{}]", providerTypes.stream().map(Class::getName).collect(Collectors.joining(", "))); + try { + String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + classCounter.incrementAndGet(); + byte[] classBytes = generateClassBytes(className, providerTypes); + + // Define the hidden class + MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClass( + classBytes, + true, + MethodHandles.Lookup.ClassOption.NESTMATE + ); + + // Return a MethodHandle to the constructor + // Constructor signature: (ICapabilityProvider[])V + // The constructor is adapted to take an Object and return an ICapabilityProvider to match + // the usage in getOrGenerateDispatcher + return hiddenLookup.findConstructor( + hiddenLookup.lookupClass(), + MethodType.methodType(void.class, ICapabilityProvider[].class) + ).asType(MethodType.methodType(ICapabilityProvider.class, Object.class)); + } catch (Exception e) { + throw new RuntimeException("Failed to generate capability dispatcher class", e); + } + } + + private static byte[] generateClassBytes(String className, List> providerTypes) { + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + + // Class declaration: implements ICapabilityProvider + cw.visit( + V17, + ACC_PUBLIC | ACC_FINAL | ACC_SUPER, + className.replace('.', '/'), + null, + "java/lang/Object", + new String[] { "net/minecraftforge/common/capabilities/ICapabilityProvider" } + ); + + // Generate final fields for each provider + for (int i = 0; i < providerTypes.size(); i++) { + cw.visitField( + ACC_PRIVATE | ACC_FINAL, + "provider" + i, + ICAP_PROVIDER_DESC, + null, + null + ).visitEnd(); + } + + // Generate constructor + generateConstructor(cw, className, providerTypes.size()); + + // Generate getCapability method with sided parameter + generateGetCapabilityMethod(cw, className, providerTypes.size()); + + cw.visitEnd(); + return cw.toByteArray(); + } + + private static void generateConstructor(ClassWriter cw, String className, int providerCount) { + Method constructor = Method.getMethod("void (net.minecraftforge.common.capabilities.ICapabilityProvider[])"); + GeneratorAdapter mg = new GeneratorAdapter(ACC_PUBLIC, constructor, null, null, cw); + + // Call super constructor + mg.loadThis(); + mg.invokeConstructor(Type.getType(Object.class), Method.getMethod("void ()")); + + // Unpack array into final fields + for (int i = 0; i < providerCount; i++) { + mg.loadThis(); // this + mg.loadArg(0); // array + mg.push(i); // index + mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC)); // array[i] + mg.putField( + Type.getObjectType(className.replace('.', '/')), + "provider" + i, + Type.getType(ICAP_PROVIDER_DESC) + ); + } + + mg.returnValue(); + mg.endMethod(); + } + + private static void generateGetCapabilityMethod(ClassWriter cw, String className, int providerCount) { + // Method: LazyOptional getCapability(Capability, Direction) + MethodVisitor mv = cw.visitMethod( + ACC_PUBLIC, + "getCapability", + "(" + CAPABILITY_DESC + DIRECTION_DESC + ")" + LAZY_OPTIONAL_DESC, + "(" + CAPABILITY_DESC.replace(";", ";") + DIRECTION_DESC + ")" + LAZY_OPTIONAL_DESC.replace(";", ";"), + null + ); + + mv.visitCode(); + + // Generate unrolled dispatch loop + // For each provider, call getCapability and check if present + Label endLabel = new Label(); + + for (int i = 0; i < providerCount; i++) { + Label nextLabel = new Label(); + + // LazyOptional result = this.providerN.getCapability(cap, side); + mv.visitVarInsn(ALOAD, 0); // this + mv.visitFieldInsn( + GETFIELD, + className.replace('.', '/'), + "provider" + i, + ICAP_PROVIDER_DESC + ); + mv.visitVarInsn(ALOAD, 1); // cap parameter + mv.visitVarInsn(ALOAD, 2); // side parameter + mv.visitMethodInsn( + INVOKEINTERFACE, + "net/minecraftforge/common/capabilities/ICapabilityProvider", + "getCapability", + "(" + CAPABILITY_DESC + DIRECTION_DESC + ")" + LAZY_OPTIONAL_DESC, + true + ); + + // Store result in local variable + mv.visitVarInsn(ASTORE, 3); + + // if (result == null) continue to next; + mv.visitVarInsn(ALOAD, 3); + mv.visitJumpInsn(IFNULL, nextLabel); + + // if (result.isPresent()) return result; + mv.visitVarInsn(ALOAD, 3); + mv.visitMethodInsn( + INVOKEVIRTUAL, + "net/minecraftforge/common/util/LazyOptional", + "isPresent", + "()Z", + false + ); + mv.visitJumpInsn(IFEQ, nextLabel); + + // return result + mv.visitVarInsn(ALOAD, 3); + mv.visitInsn(ARETURN); + + mv.visitLabel(nextLabel); + if (i < providerCount - 1) { + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + } + } + + // If no provider returned a capability, return empty + mv.visitLabel(endLabel); + mv.visitMethodInsn( + INVOKESTATIC, + "net/minecraftforge/common/util/LazyOptional", + "empty", + "()" + LAZY_OPTIONAL_DESC, + false + ); + mv.visitInsn(ARETURN); + + mv.visitMaxs(0, 0); // Computed by COMPUTE_MAXS + mv.visitEnd(); + } + + /** + * Creates an instance of the optimized dispatcher for the given providers. + */ + public static ICapabilityProvider createDispatcher(ICapabilityProvider[] providers) { + try { + MethodHandle constructor = getOrGenerateConstructor(providers); + return (ICapabilityProvider) constructor.invoke(providers); + } catch (Throwable e) { + throw new RuntimeException("Failed to create capability dispatcher instance", e); + } + } + + /** + * Clears the cache of generated classes. Use with caution. + */ + public static void clearCache() { + cache.clear(); + } + + /** + * Returns the number of cached dispatcher classes. + */ + public static int getCacheSize() { + return cache.size(); + } +} \ No newline at end of file From a30dd08cd1e4e4b03f5001533b31c944875e82c7 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:49:26 -0500 Subject: [PATCH 2/5] Optimize memory usage of ModFileScanData --- .../modernfix/forge/init/ModernFixForge.java | 2 + .../forge/load/ModFileScanDataCompactor.java | 90 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/forge/load/ModFileScanDataCompactor.java diff --git a/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java b/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java index d46a1cd8..dd2cd228 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java +++ b/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java @@ -28,6 +28,7 @@ import org.embeddedt.modernfix.core.ModernFixMixinPlugin; import org.embeddedt.modernfix.forge.ModernFixConfig; import org.embeddedt.modernfix.forge.config.ConfigFixer; import org.embeddedt.modernfix.forge.config.NightConfigFixer; +import org.embeddedt.modernfix.forge.load.ModFileScanDataCompactor; import org.embeddedt.modernfix.forge.packet.PacketHandler; import org.embeddedt.modernfix.forge.registry.ObjectHolderClearer; @@ -49,6 +50,7 @@ public class ModernFixForge { ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, ModernFixConfig.COMMON_CONFIG); PacketHandler.register(); ConfigFixer.replaceConfigHandlers(); + ModFileScanDataCompactor.compact(); } @SubscribeEvent diff --git a/src/main/java/org/embeddedt/modernfix/forge/load/ModFileScanDataCompactor.java b/src/main/java/org/embeddedt/modernfix/forge/load/ModFileScanDataCompactor.java new file mode 100644 index 00000000..92ee07e7 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/forge/load/ModFileScanDataCompactor.java @@ -0,0 +1,90 @@ +package org.embeddedt.modernfix.forge.load; + +import com.google.common.collect.ImmutableSet; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.forgespi.language.ModFileScanData; +import org.embeddedt.modernfix.ModernFix; +import org.objectweb.asm.Type; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ModFileScanDataCompactor { + private static final Field ANNOTATIONS_FIELD, CLASSES_FIELD; + private static final ObjectOpenHashSet TYPES = new ObjectOpenHashSet<>(); + + static { + ANNOTATIONS_FIELD = tryGetField("annotations"); + CLASSES_FIELD = tryGetField("classes"); + } + + private static Field tryGetField(String name) { + try { + var f = ModFileScanData.class.getDeclaredField(name); + f.setAccessible(true); + return f; + } catch (Exception e) { + ModernFix.LOGGER.error("Unable to access '{}' field on ModFileScanData", name, e); + return null; + } + } + + public static void compact() { + for (var file : ModList.get().getModFiles()) { + var scanResult = file.getFile().getScanResult(); + try { + compact(scanResult, file.getFile().getFileName()); + } catch (Throwable e) { + ModernFix.LOGGER.error("An error occured while compacting {}", file.getFile().getFileName(), e); + } + } + TYPES.clear(); + TYPES.trim(); + } + + private static void compact(ModFileScanData data, String fileName) { + ObjectOpenHashSet memberNames = new ObjectOpenHashSet<>(); + var annotationSet = data.getAnnotations().stream().filter(a -> { + // Filter out annotation classes that no one is likely to look for + String clzName = a.annotationType().getClassName(); + return !clzName.startsWith("kotlin.jvm.") + && !clzName.startsWith("scala.reflect.") + && !clzName.startsWith("org.spongepowered.asm.mixin.") + && !clzName.startsWith("com.llamalad7.mixinextras.") + && !clzName.contains("org.jetbrains.annotations.") + && !clzName.contains("javax.annotation.") + && !clzName.endsWith("kotlin.Metadata") + && !clzName.equals("net.minecraftforge.api.distmarker.OnlyIn"); + }).map(a -> new ModFileScanData.AnnotationData( + TYPES.addOrGet(a.annotationType()), + a.targetType(), + TYPES.addOrGet(a.clazz()), + memberNames.addOrGet(a.memberName()), + a.annotationData().entrySet().stream().collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> { + Object annValue = e.getValue(); + if (annValue instanceof List list) { + annValue = List.copyOf(list); + } + return annValue; + })) + )).collect(ImmutableSet.toImmutableSet()); + if (annotationSet.size() < data.getAnnotations().size()) { + ModernFix.LOGGER.debug("Removed {} unneeded annotations from file {}", + data.getAnnotations().size() - annotationSet.size(), fileName); + } + var classSet = data.getClasses().stream().map(c -> new ModFileScanData.ClassData( + TYPES.addOrGet(c.clazz()), + TYPES.addOrGet(c.parent()), + c.interfaces().stream().map(TYPES::addOrGet).collect(ImmutableSet.toImmutableSet()) + )).collect(ImmutableSet.toImmutableSet()); + try { + ANNOTATIONS_FIELD.set(data, annotationSet); + CLASSES_FIELD.set(data, classSet); + } catch (Exception e) { + ModernFix.LOGGER.error("Error replacing fields on ModFileScanData", e); + } + } +} From b2ed5b93410e65c8002f6a6239596d68c9d094c3 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 9 Jan 2026 21:05:06 -0500 Subject: [PATCH 3/5] Adjust mixin for CIT Reforged compatibility Related: https://github.com/embeddedt/ModernFix/issues/624 --- .../mixin/perf/dynamic_resources/ModelBakeryMixin.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 d42cde4b..c7719e3d 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 @@ -6,6 +6,7 @@ import com.google.common.cache.RemovalCause; import com.google.common.cache.RemovalNotification; import com.google.common.collect.ForwardingMap; import com.google.common.collect.ImmutableList; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import net.minecraft.client.Minecraft; import net.minecraft.client.color.block.BlockColors; import net.minecraft.client.renderer.block.model.BlockModel; @@ -119,10 +120,10 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { this.bakedTopLevelModels = new DynamicBakedModelProvider((ModelBakery)(Object)this, bakedCache); } - @ModifyArg(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/profiling/ProfilerFiller;popPush(Ljava/lang/String;)V", ordinal = 0), index = 0) - private String ignoreFutureModelLoads(String name) { + @ModifyExpressionValue(method = "", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, ordinal = 0, target = "Lnet/minecraft/client/resources/model/ModelBakery;STATIC_DEFINITIONS:Ljava/util/Map;")) + private Map> ignoreFutureModelLoads(Map> original) { this.ignoreModelLoad = true; - return name; + return original; } private void onModelRemoved(RemovalNotification notification) { From 4b18cc2cc65ffe53b5a361dbd3eeec3344892396 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 9 Jan 2026 21:05:49 -0500 Subject: [PATCH 4/5] Fix crash when user home/config folders are inaccessible --- .../core/config/ModernFixEarlyConfig.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 bec53e4a..99b12a07 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -317,16 +317,16 @@ public class ModernFixEarlyConfig { } private void readGlobalProperties() { - Path minecraftFolder; - if (SystemUtils.IS_OS_MAC) { - minecraftFolder = Paths.get(System.getProperty("user.home"), "Library", "Application Support", "minecraft"); - } else if (SystemUtils.IS_OS_WINDOWS) { - minecraftFolder = Paths.get(System.getenv("APPDATA"), ".minecraft"); - } else { - minecraftFolder = Paths.get(System.getProperty("user.home"), ".minecraft"); - } - Path globalPropsFile = minecraftFolder.resolve("global").resolve("modernfix-global-mixins.properties"); try { + Path minecraftFolder; + if (SystemUtils.IS_OS_MAC) { + minecraftFolder = Paths.get(System.getProperty("user.home"), "Library", "Application Support", "minecraft"); + } else if (SystemUtils.IS_OS_WINDOWS) { + minecraftFolder = Paths.get(System.getenv("APPDATA"), ".minecraft"); + } else { + minecraftFolder = Paths.get(System.getProperty("user.home"), ".minecraft"); + } + Path globalPropsFile = minecraftFolder.resolve("global").resolve("modernfix-global-mixins.properties"); if (Files.exists(globalPropsFile)) { Properties properties = new Properties(); try (var is = Files.newInputStream(globalPropsFile)) { From 00287612de29f0e207a2b59bbf85db2d2714b301 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:20:21 -0500 Subject: [PATCH 5/5] Reimplement publish task --- .github/workflows/release.yml | 2 +- build.gradle.kts | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f3848c9..dc478189 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: - name: Remove tags for release on other versions run: ./scripts/tagcleaner.sh - name: Build and publish mod to CurseForge & Modrinth - run: ./gradlew publishToModSites copyJarToBin + run: ./gradlew publishMods copyJarToBin env: CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} diff --git a/build.gradle.kts b/build.gradle.kts index 189c1e41..f0dc1af8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("net.neoforged.moddev.legacyforge") version("2.0.134") id("org.ajoberstar.grgit") version("5.2.0") id("com.palantir.git-version") version("1.0.0") + id("me.modmuss50.mod-publish-plugin") version("1.1.0") } val minecraft_version = rootProject.properties["minecraft_version"].toString() @@ -211,4 +212,28 @@ tasks.register("copyJarToBin") { tasks.named("build") { dependsOn("copyJarToBin", "copyJarNameConsistent") +} + +publishMods { + file.set(tasks.named(finalJarTask).get().outputs.files.singleFile) + changelog = "Please check the [GitHub wiki](https://github.com/embeddedt/ModernFix/wiki/Changelog) for major changes." + type = STABLE + + modLoaders.add("forge") + + curseforge { + projectId = "790626" + projectSlug = "modernfix" + accessToken = providers.environmentVariable("CURSEFORGE_TOKEN") + minecraftVersions.add(minecraft_version) + } + modrinth { + projectId = "modernfix" + accessToken = providers.environmentVariable("MODRINTH_TOKEN") + minecraftVersions.add(minecraft_version) + } +} + +tasks.named("publishMods") { + dependsOn(finalJarTask) } \ No newline at end of file