mcpbeat

Minecraft Multiloader

jahrome907/minecraft-multiloader

Build Minecraft mods targeting both NeoForge and Fabric simultaneously using the Architectury framework for Minecraft 1.21.x. Covers Architectury project structure (common/neoforge/fabric subprojects), ExpectPlatform annotation for platform-specific implementations, shared registry via Architectury's registration API, platform-specific entrypoints, architectury-loom Gradle plugin configuration, gradle.properties for both loaders, multi-jar publishing to Modrinth and CurseForge, and avoiding common pitfalls when sharing code. Use this skill when building a mod that must run on both NeoForge and Fabric with a single shared codebase.

5k tokens
context cost
the whole folder, loaded on every use
3
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
105
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/Jahrome907/minecraft-agent-skills --skill minecraft-multiloader

What comes with it

5 350 bytes besides the instruction
references/architectury-reference.md
scripts/check-version-sanity.sh

The instruction itself

23 sections, as written by the author

Minecraft Multiloader Skill (Architectury)

What Is Architectury?

Architectury is a framework that

lets you write one mod codebase that compiles to both NeoForge and Fabric JARs.

The common subproject has a shared API; platform subprojects implement

platform-specific behavior behind the @ExpectPlatform abstraction.

Routing Boundaries

  • Use when: one shared codebase must build and ship both NeoForge and Fabric artifacts.
  • Do not use when: the project is single-loader only (minecraft-modding for NeoForge/Fabric, not both).
  • Do not use when: the task is Paper/Bukkit plugin development (minecraft-plugin-dev).

| Component | Purpose |

|-----------|---------|

| architectury-loom | Gradle plugin — extends Fabric Loom for multiloader support |

| architectury-api | Runtime library — abstractions over both platforms |

| @ExpectPlatform | Annotation marking methods with platform-specific implementations |

| common/ | Shared code (no loader-specific APIs) |

| fabric/ | Fabric-specific code + entrypoint |

| neoforge/ | NeoForge-specific code + entrypoint |


Versions (1.21.x)

# gradle.properties (root)
mod_version=1.0.0
minecraft_version=1.21.11
enabled_platforms=fabric,neoforge

architectury_version=19.0.1
fabric_loader_version=0.19.3
fabric_api_version=0.141.4+1.21.11
neoforge_version=21.11.42

loom_version=1.17.11

Pin architectury_version, the Architectury plugin version, and loom_version

from the same released template line when scaffolding a new project. The values

above are for the stable 1.21.x toolchain story in this repo and avoid snapshot-only examples.

Bundled References And Helpers

  • Version alignment reference: references/architectury-reference.md
  • Sanity checker: ./scripts/check-version-sanity.sh --root <project>

Run the sanity checker after editing gradle.properties. It catches the most common

multiloader drift mistakes: snapshot toolchain pins, missing fabric / neoforge

platforms, and mismatched NeoForge vs Minecraft patch lines.


Root Project Layout

my-mod/
├── build.gradle           ← root build (shared config)
├── settings.gradle
├── gradle.properties
├── common/
│   ├── build.gradle
│   └── src/main/java/com/example/mymod/
│       ├── MyMod.java               ← shared init
│       ├── registry/
│       │   └── ModItems.java        ← shared registry declarations
│       └── platform/
│           └── PlatformHelper.java  ← @ExpectPlatform methods
├── fabric/
│   ├── build.gradle
│   └── src/main/
│       ├── java/com/example/mymod/fabric/
│       │   ├── MyModFabric.java          ← Fabric entrypoint
│       ├── java/com/example/mymod/platform/
│       │   └── PlatformHelperImpl.java   ← Fabric @ExpectPlatform implementation
│       └── resources/
│           ├── fabric.mod.json
│           └── assets/...
└── neoforge/
    ├── build.gradle
    └── src/main/
        ├── java/com/example/mymod/neoforge/
        │   ├── MyModNeoForge.java        ← NeoForge @Mod entry
        ├── java/com/example/mymod/platform/
        │   └── PlatformHelperImpl.java   ← NeoForge @ExpectPlatform implementation
        └── resources/
            ├── META-INF/neoforge.mods.toml
            └── assets/...

Root settings.gradle

pluginManagement {
    repositories {
        maven { url "https://maven.architectury.dev/" }
        maven { url "https://maven.fabricmc.net/" }
        maven { url "https://maven.neoforged.net/releases" }
        gradlePluginPortal()
    }
}

include "common"
include "fabric"
include "neoforge"

Root build.gradle

plugins {
    id "architectury-plugin" version "3.4" apply false
    id "dev.architectury.loom" version "${loom_version}" apply false
    id "com.github.johnrengelman.shadow" version "8.1.1" apply false
}

architectury {
    minecraft = rootProject.minecraft_version
}

subprojects {
    apply plugin: "java"
    apply plugin: "architectury-plugin"

    group = "com.example.mymod"
    version = "${mod_version}+${minecraft_version}"
    archivesBaseName = "my-mod-${project.name}"

    repositories {
        maven { url "https://maven.architectury.dev/" }
        maven { url "https://mod-buildtools.pkg.github.com/TerraformersMC/" }
    }

    java {
        withSourcesJar()
        sourceCompatibility = JavaVersion.VERSION_21
        targetCompatibility = JavaVersion.VERSION_21
    }
}

common/build.gradle

plugins {
    id "dev.architectury.loom" apply true
}

architectury {
    common(rootProject.enabled_platforms.split(","))
}

loom {
    // common project uses mappings only
}

dependencies {
    minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
    mappings loom.officialMojangMappings()

    modImplementation "dev.architectury:architectury:${rootProject.architectury_version}"
}

fabric/build.gradle

plugins {
    id "com.github.johnrengelman.shadow"
    id "dev.architectury.loom" apply true
}

architectury {
    platformSetupLoomIde()
    fabric()
}

loom {
    accessWidenerPath = project(":common").loom.accessWidenerPath
}

configurations {
    common
    shadowCommon
    compileClasspath.extendsFrom common
    runtimeClasspath.extendsFrom common
    developmentFabric.extendsFrom common
}

dependencies {
    minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
    mappings loom.officialMojangMappings()

    modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}"
    modApi "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}"
    modApi "dev.architectury:architectury-fabric:${rootProject.architectury_version}"

    common(project(path: ":common", configuration: "namedElements")) { transitive false }
    shadowCommon(project(path: ":common", configuration: "transformProductionFabric")) { transitive false }
}

shadowJar {
    exclude "architectury.common.json"
    configurations = [project.configurations.shadowCommon]
    archiveClassifier = "dev-shadow"
}

remapJar {
    injectAccessWidener = true
    input.fileValue shadowJar.archiveFile.get().asFile
    dependsOn shadowJar
    archiveClassifier = ""
}

jar { archiveClassifier = "dev" }
sourcesJar { archiveClassifier = "dev-sources" }
components.java.withVariantsFromConfiguration(configurations.shadowRuntimeElements) { skip() }

neoforge/build.gradle

plugins {
    id "com.github.johnrengelman.shadow"
    id "dev.architectury.loom" apply true
}

architectury {
    platformSetupLoomIde()
    neoForge()
}

loom {
    accessWidenerPath = project(":common").loom.accessWidenerPath
}

configurations {
    common
    shadowCommon
    compileClasspath.extendsFrom common
    runtimeClasspath.extendsFrom common
    developmentNeoForge.extendsFrom common
}

dependencies {
    minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
    mappings loom.officialMojangMappings()

    neoForge "net.neoforged:neoforge:${rootProject.neoforge_version}"
    modApi "dev.architectury:architectury-neoforge:${rootProject.architectury_version}"

    common(project(path: ":common", configuration: "namedElements")) { transitive false }
    shadowCommon(project(path: ":common", configuration: "transformProductionNeoForge")) { transitive false }
}

shadowJar {
    exclude "architectury.common.json"
    configurations = [project.configurations.shadowCommon]
    archiveClassifier = "dev-shadow"
}

remapJar {
    input.fileValue shadowJar.archiveFile.get().asFile
    dependsOn shadowJar
    archiveClassifier = ""
}

jar { archiveClassifier = "dev" }

Shared Common Code

common/.../MyMod.java

package com.example.mymod;

import dev.architectury.registry.registries.DeferredRegister;
import dev.architectury.registry.registries.RegistrySupplier;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;

public class MyMod {
    public static final String MOD_ID = "mymod";

    // Architectury's DeferredRegister — works on both platforms
    public static final DeferredRegister<Item> ITEMS =
        DeferredRegister.create(MOD_ID, BuiltInRegistries.ITEM);

    public static final RegistrySupplier<Item> MY_ITEM =
        ITEMS.register("my_item", () -> new Item(new Item.Properties()));

    public static void init() {
        ITEMS.register(); // registers with both platforms
    }
}

@ExpectPlatform — platform-specific methods

Define the contract in common/:

package com.example.mymod.platform;

import dev.architectury.injectables.annotations.ExpectPlatform;
import net.minecraft.world.level.material.Fluid;

public class PlatformHelper {

    @ExpectPlatform
    public static boolean isModLoaded(String modId) {
        // This body is replaced at compile time by the platform implementation
        throw new AssertionError("ExpectPlatform implementation not found");
    }

    @ExpectPlatform
    public static boolean isClient() {
        throw new AssertionError();
    }
}

Keep each platform implementation in the same Java package as the common

@ExpectPlatform class. Only the source set changes between common/,

fabric/, and neoforge/.

Implement in fabric/.../platform/PlatformHelperImpl.java:

package com.example.mymod.platform;

import net.fabricmc.loader.api.FabricLoader;

// Class name must match: <common class name>Impl
public class PlatformHelperImpl {

    public static boolean isModLoaded(String modId) {
        return FabricLoader.getInstance().isModLoaded(modId);
    }

    public static boolean isClient() {
        return FabricLoader.getInstance().getEnvironmentType() ==
            net.fabricmc.api.EnvType.CLIENT;
    }
}

Implement in neoforge/.../platform/PlatformHelperImpl.java:

package com.example.mymod.platform;

import net.neoforged.fml.ModList;
import net.neoforged.fml.loading.FMLEnvironment;

public class PlatformHelperImpl {

    public static boolean isModLoaded(String modId) {
        return ModList.get().isLoaded(modId);
    }

    public static boolean isClient() {
        return FMLEnvironment.dist.isClient();
    }
}

Fabric Entrypoint

fabric/.../MyModFabric.java

package com.example.mymod.fabric;

import com.example.mymod.MyMod;
import net.fabricmc.api.ModInitializer;

public class MyModFabric implements ModInitializer {
    @Override
    public void onInitialize() {
        MyMod.init();
    }
}

fabric/.../resources/fabric.mod.json

{
  "schemaVersion": 1,
  "id": "mymod",
  "version": "${version}",
  "name": "My Mod",
  "description": "A multiloader example mod",
  "license": "MIT",
  "environment": "*",
  "entrypoints": {
    "main": ["com.example.mymod.fabric.MyModFabric"]
  },
  "depends": {
    "fabricloader": ">=0.19.3",
    "fabric-api": ">=0.141.4+1.21.11",
    "architectury": ">=19.0.1",
    "minecraft": "~1.21.11"
  }
}

NeoForge Entrypoint

neoforge/.../MyModNeoForge.java

package com.example.mymod.neoforge;

import com.example.mymod.MyMod;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;

@Mod(MyMod.MOD_ID)
public class MyModNeoForge {
    public MyModNeoForge(IEventBus modEventBus) {
        MyMod.init();
    }
}

neoforge/.../resources/META-INF/neoforge.mods.toml

modLoader = "javafml"
loaderVersion = "[1,)"
license = "MIT"

[[mods]]
modId = "mymod"
version = "${file.jarVersion}"
displayName = "My Mod"
description = "A multiloader example mod"

[[dependencies.mymod]]
modId = "neoforge"
type = "required"
versionRange = "[21.11,)"
ordering = "NONE"
side = "BOTH"

[[dependencies.mymod]]
modId = "minecraft"
type = "required"
versionRange = "[1.21.11,1.22)"
ordering = "NONE"
side = "BOTH"

Build Commands

# Build both JARs simultaneously
./gradlew build

# Outputs:
#   fabric/build/libs/my-mod-fabric-1.0.0+1.21.11.jar
#   neoforge/build/libs/my-mod-neoforge-1.0.0+1.21.11.jar

# Run in dev environment
./gradlew :fabric:runClient
./gradlew :neoforge:runClient
./gradlew :neoforge:runServer

# Datagen (if applicable)
./gradlew :neoforge:runData

Common Pitfalls

| Pitfall | Solution |

|---------|----------|

| Using net.neoforged.* / net.fabricmc.* in common/ | Only use vanilla MC and Architectury APIs in common |

| Direct field access on DeferredRegister (NeoForge style) in common | Use Architectury's DeferredRegister |

| Forgetting @ExpectPlatform throws AssertionError at runtime | Both fabric/ and neoforge/ must have matching same-package *Impl classes |

| Assets duplicated in fabric/ and neoforge/ | Keep assets in common/src/main/resources/assets/ |

| Mixins in common — not supported on NeoForge | Put Mixins in the platform subprojects only |

| Accessing world/registry on mod init thread | Use mod bus events for setup; never access world on init |


References

  • Architectury API GitHub: https://github.com/architectury/architectury-api
  • Architectury Loom: https://github.com/architectury/architectury-loom
  • Architectury templates: https://github.com/architectury/architectury-templates
  • Architectury docs: https://docs.architectury.dev/

How to use it

Copy the folder

Take jahrome907/minecraft-multiloader from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.