references/common-patterns.md
# Common Minecraft Modding Patterns
Cross-platform patterns for blocks, items, entities, data generation, commands, recipes,
and more. Code examples use NeoForge syntax unless noted; adapt field/method names for Fabric.
This reference retains 1.21.11 examples alongside 26.x replacements. A heading
that names Minecraft 26.x uses the 26.1 API and official Mojang mappings;
verify any later 26.x API change before copying code into a pinned project.
Treat all other examples as 1.21.11 examples unless their exact API is
verified for the project. For 26.3 loot data, use entry `condition` / `modifier`
and predicate/function `type`; the older loot JSON below needs migration.
See the [26.3 migration primer](https://docs.neoforged.net/primer/docs/26.3/).
---
## Blocks
### Simple Full-Cube Block
Files needed:
1. Java class (if custom behavior) or `registerSimpleBlock()` call
2. `assets/<modid>/blockstates/<name>.json`
3. `assets/<modid>/models/block/<name>.json`
4. `assets/<modid>/items/<name>.json` (1.21.x item definition)
5. `assets/<modid>/models/item/<name>.json`
6. `assets/<modid>/textures/block/<name>.png`
7. `data/<modid>/loot_table/blocks/<name>.json`
8. `en_us.json` entry
`assets/mymod/blockstates/my_block.json`:
```json
{
"variants": {
"": { "model": "mymod:block/my_block" }
}
}
```
`assets/mymod/models/block/my_block.json`:
```json
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "mymod:block/my_block"
}
}
```
`assets/mymod/items/my_block.json`:
```json
{
"model": {
"type": "minecraft:model",
"model": "mymod:block/my_block"
}
}
```
`assets/mymod/models/item/my_block.json` is only needed when the block item needs
a model distinct from the block model. For that case:
```json
{
"parent": "mymod:block/my_block"
}
```
`data/mymod/loot_table/blocks/my_block.json`:
```json
{
"type": "minecraft:block",
"pools": [{
"rolls": 1,
"entries": [{
"type": "minecraft:item",
"name": "mymod:my_block"
}],
"conditions": [{
"condition": "minecraft:survives_explosion"
}]
}]
}
```
---
### Directional Block (faces a direction when placed)
Extend `DirectionalBlock`, define its codec, initialize the default `FACING`,
and set facing in `getStateForPlacement`. Register it through `registerBlock`
so its properties receive the block key. Use the
[1.21.11 block guide](https://docs.neoforged.net/docs/1.21.11/blocks/) for the
codec and registration signatures.
`assets/mymod/blockstates/my_directional_block.json`:
```json
{
"variants": {
"facing=north": { "model": "mymod:block/my_directional_block" },
"facing=south": { "model": "mymod:block/my_directional_block", "y": 180 },
"facing=west": { "model": "mymod:block/my_directional_block", "y": 270 },
"facing=east": { "model": "mymod:block/my_directional_block", "y": 90 },
"facing=up": { "model": "mymod:block/my_directional_block", "x": -90 },
"facing=down": { "model": "mymod:block/my_directional_block", "x": 90 }
}
}
```
---
### Slab Block
```java
public static final DeferredBlock<SlabBlock> MY_SLAB =
BLOCKS.registerBlock("my_slab", SlabBlock::new,
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.STONE_SLAB));
```
`assets/mymod/models/block/my_slab.json`:
```json
{
"parent": "minecraft:block/slab",
"textures": {
"bottom": "mymod:block/my_block",
"top": "mymod:block/my_block",
"side": "mymod:block/my_block"
}
}
```
`assets/mymod/models/block/my_slab_top.json`:
```json
{
"parent": "minecraft:block/slab_top",
"textures": {
"bottom": "mymod:block/my_block",
"top": "mymod:block/my_block",
"side": "mymod:block/my_block"
}
}
```
`assets/mymod/blockstates/my_slab.json`:
```json
{
"variants": {
"type=bottom": { "model": "mymod:block/my_slab" },
"type=top": { "model": "mymod:block/my_slab_top" },
"type=double": { "model": "mymod:block/my_block" }
}
}
```
---
### Stairs Block
```java
public static final DeferredBlock<StairBlock> MY_STAIRS =
BLOCKS.registerBlock("my_stairs",
props -> new StairBlock(ModBlocks.MY_BLOCK.get().defaultBlockState(), props),
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.STONE_STAIRS));
```
`assets/mymod/models/block/my_stairs.json`:
```json
{
"parent": "minecraft:block/stairs",
"textures": {
"bottom": "mymod:block/my_block",
"top": "mymod:block/my_block",
"side": "mymod:block/my_block"
}
}
```
Also create `my_stairs_inner.json` and `my_stairs_outer.json`, inheriting from
`minecraft:block/inner_stairs` and `minecraft:block/outer_stairs` respectively.
---
## Items
### Food Item
```java
// NeoForge
public static final DeferredItem<Item> MY_FOOD =
ITEMS.registerSimpleItem("my_food", props -> props
.food(new FoodProperties.Builder()
.nutrition(4)
.saturationModifier(0.3f)
.build(),
Consumable.builder()
.onConsume(new ApplyStatusEffectsConsumeEffect(
new MobEffectInstance(MobEffects.REGENERATION, 200, 1), 0.8f))
.build()));
```
Food effects belong to the consumable component, not `FoodProperties.Builder`.
See the [1.21.11 consumable API](https://docs.neoforged.net/docs/1.21.11/items/consumables/).
`assets/mymod/items/my_food.json`:
```json
{
"model": {
"type": "minecraft:model",
"model": "mymod:item/my_food"
}
}
```
`assets/mymod/models/item/my_food.json`:
```json
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "mymod:item/my_food"
}
}
```
### Tool Item (NeoForge 26.x)
`Tier` and `SwordItem.createAttributes` are obsolete here. Define a
`ToolMaterial`, then use the `Item.Properties` tool delegate during item
registration.
```java
public static final ToolMaterial MY_TOOL_MATERIAL = new ToolMaterial(
ModBlockTags.INCORRECT_FOR_MY_TOOL,
455, 5.0f, 1.5f, 22,
ModItemTags.REPAIRS_MY_TOOL
);
public static final DeferredItem<Item> MY_SWORD =
ITEMS.registerItem("my_sword", props ->
new Item(props.sword(MY_TOOL_MATERIAL, 3, -2.4f)));
```
### Armor Set (NeoForge 26.x)
`ArmorMaterial` is not a registry entry. Its equipment asset key identifies
the corresponding equipment definition, while `humanoidArmor` applies the
material to a normal `Item`.
```java
public static final ResourceKey<EquipmentAsset> MY_ARMOR_ASSET =
ResourceKey.create(EquipmentAssets.ROOT_ID,
Identifier.fromNamespaceAndPath(MyMod.MOD_ID, "my_material"));
public static final ArmorMaterial MY_ARMOR_MATERIAL = new ArmorMaterial(
15,
Map.of(
ArmorType.HELMET, 3,
ArmorType.CHESTPLATE, 8,
ArmorType.LEGGINGS, 6,
ArmorType.BOOTS, 3
),
5, SoundEvents.ARMOR_EQUIP_IRON, 0.0f, 0.0f,
ModItemTags.REPAIRS_MY_ARMOR, MY_ARMOR_ASSET
);
public static final DeferredItem<Item> MY_HELMET =
ITEMS.registerItem("my_helmet", props ->
new Item(props.humanoidArmor(MY_ARMOR_MATERIAL, ArmorType.HELMET)));
```
---
## Entity Types
```java
// ModEntityTypes.java (NeoForge 26.x)
public class ModEntityTypes {
public static final DeferredRegister.Entities ENTITY_TYPES =
DeferredRegister.createEntities(MyMod.MOD_ID);
public static final Supplier<EntityType<MyEntity>> MY_ENTITY =
ENTITY_TYPES.registerEntityType(
"my_entity", MyEntity::new, MobCategory.CREATURE,
builder -> builder
.sized(0.9f, 1.3f)
.clientTrackingRange(8)
.updateInterval(3)
);
}
```
For a concrete entity subclass, attributes, spawning, or renderer wiring, use
the current NeoForge entity guide for the project's exact version. Do not copy
the removed `EntityType.Builder#build(String)` overload into 26.x code.
---
## Commands (NeoForge 1.21.11, Mojang mappings)
```java
// NeoForge — register on GAME bus
@EventBusSubscriber(modid = MyMod.MOD_ID)
public class ModCommands {
@SubscribeEvent
public static void onRegisterCommands(RegisterCommandsEvent event) {
registerCommands(event.getDispatcher());
}
private static void registerCommands(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(
Commands.literal("mymod")
.then(Commands.literal("give")
.requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS))
.then(Commands.argument("player", EntityArgument.player())
.then(Commands.argument("count", IntegerArgumentType.integer(1, 64))
.executes(ctx -> executeGive(ctx,
EntityArgument.getPlayer(ctx, "player"),
IntegerArgumentType.getInteger(ctx, "count"))))))
);
}
private static int executeGive(CommandContext<CommandSourceStack> ctx,
ServerPlayer player, int count) throws CommandSyntaxException {
ItemStack stack = new ItemStack(ModItems.MY_ITEM.get(), count);
player.getInventory().add(stack);
ctx.getSource().sendSuccess(
() -> Component.translatable("commands.mymod.give.success",
count, player.getDisplayName()),
true);
return count;
}
}
```
Minecraft 1.21.11 replaced numeric permission calls with permission checks.
Keep Fabric's registration callback and mapping names separate. See the
[1.21.11 migration primer](https://docs.neoforged.net/primer/docs/1.21.11/).
---
## Recipes (Minecraft 26.x JSON)
### Shaped Crafting Recipe
`data/mymod/recipe/my_item.json`:
```json
{
"type": "minecraft:crafting_shaped",
"pattern": [
"SSS",
" I ",
" I "
],
"key": {
"S": "minecraft:stone",
"I": "minecraft:iron_ingot"
},
"result": {
"id": "mymod:my_item",
"count": 1
}
}
```
### Shapeless Recipe
```json
{
"type": "minecraft:crafting_shapeless",
"ingredients": [
"minecraft:diamond",
"minecraft:emerald"
],
"result": {
"id": "mymod:my_item",
"count": 2
}
}
```
### Smelting / Blasting / Smoking / Campfire
```json
{
"type": "minecraft:smelting",
"ingredient": "mymod:my_ore",
"result": { "id": "mymod:my_ingot" },
"experience": 0.7,
"cookingtime": 200
}
```
### Custom Recipe Type (NeoForge / Fabric)
Implement the target version's `Recipe<SingleRecipeInput>` contract, its
serializer, and recipe-type registration together. Follow the
[versioned recipe guide](https://docs.neoforged.net/docs/1.21.11/resources/server/recipes/)
for codecs, recipe input, and recipe-book display requirements.
---
## Tags
Tags group blocks/items for use in recipes and game logic.
Extend the `minecraft` namespace's mining tags for vanilla tool behavior;
`mymod:mineable/pickaxe` would be a separate tag that vanilla does not consult.
`data/minecraft/tags/block/mineable/pickaxe.json`:
```json
{
"replace": false,
"values": ["mymod:my_block"]
}
```
`data/minecraft/tags/block/needs_iron_tool.json`:
```json
{
"replace": false,
"values": ["mymod:my_block"]
}
```
`data/mymod/tags/item/my_material.json`:
```json
{
"replace": false,
"values": ["mymod:my_ingot", "mymod:my_nugget"]
}
```
---
## Data Generation (NeoForge 1.21.11 and 26.x)
Use the target version of the [resource guide](https://docs.neoforged.net/docs/1.21.11/resources/)
and the project's generated provider classes. `GatherDataEvent.Client` can
register both client and server providers; the current MDK uses `runClientData`.
A split setup uses `.Client` and `.Server` with their matching tasks.
- Use `ModelProvider` for blockstates, block models, and client item definitions.
- Register recipe generation through `RecipeProvider.Runner` and keep its
registry lookup and `RecipeOutput` wiring together.
- Use `event.createProvider(...)` and the project's registry provider setup.
Do not copy old `ExistingFileHelper` or `GatherDataEvent#includeClient` code.
- Inspect generated resource paths and schemas before packaging. Older provider
constructors and a successful static JSON check do not prove compatibility.
---
## Language File (en_us.json)
```json
{
"block.mymod.my_block": "My Block",
"item.mymod.my_item": "My Item",
"item.mymod.my_food": "My Food",
"entity.mymod.my_entity": "My Entity",
"container.mymod.my_container": "My Container",
"itemGroup.mymod.main_tab": "My Mod",
"commands.mymod.give.success": "Gave %s x%s My Item"
}
```
---
## Build and publishing
Keep the project's configured build tasks and publication destinations.
Fabric server Game Tests use the configured `build` integration; NeoForge uses
`runGameTestServer`. Run only tasks present in that project. Inspect each
loader's actual distributable before publishing; older Fabric projects may
require `remapJar`, while 26.x uses the non-remapping build.
references/fabric-api.md
# Fabric API Patterns (1.21.11)
Reference for Fabric-specific code patterns. Fabric is a lightweight, stable
modding platform. These examples target Minecraft 1.21.11, Yarn mappings, and Java 21.
Use the exact version's API documentation for 26.x or an earlier 1.21 release.
---
## Mod Entry Point
```java
// MyMod.java — registered as "main" entrypoint in fabric.mod.json
public class MyMod implements ModInitializer {
public static final String MOD_ID = "mymod";
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
@Override
public void onInitialize() {
// Runs on both client and server after mod loading
ModBlocks.register();
ModItems.register();
ModBlockEntities.register();
}
}
// MyModClient.java — registered as "client" entrypoint
@Environment(EnvType.CLIENT)
public class MyModClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
// Client-only setup: renderers, screens, keybinds
BlockEntityRendererFactories.register(ModBlockEntities.MY_BLOCK_ENTITY,
MyBlockEntityRenderer::new);
HandledScreens.register(ModMenuTypes.MY_MENU, MyScreen::new);
}
}
```
---
## fabric.mod.json
```json
{
"schemaVersion": 1,
"id": "mymod",
"version": "${version}",
"name": "My Mod",
"description": "A cool Minecraft mod.",
"authors": ["YourName"],
"contact": {
"sources": "https://github.com/yourname/mymod",
"issues": "https://github.com/yourname/mymod/issues"
},
"license": "MIT",
"icon": "assets/mymod/icon.png",
"environment": "*",
"entrypoints": {
"main": ["com.example.mymod.MyMod"],
"client": ["com.example.mymod.client.MyModClient"]
},
"mixins": ["mymod.mixins.json"],
"depends": {
"fabricloader": ">=0.19.3",
"fabric-api": ">=0.141.4+1.21.11",
"minecraft": "~1.21.11"
}
}
```
---
## Block & Item Registration
```java
// ModBlocks.java
public class ModBlocks {
public static final Identifier MY_BLOCK_ID = Identifier.of(MyMod.MOD_ID, "my_block");
public static final RegistryKey<Block> MY_BLOCK_KEY =
RegistryKey.of(RegistryKeys.BLOCK, MY_BLOCK_ID);
public static final Block MY_BLOCK = new Block(
AbstractBlock.Settings.create()
.registryKey(MY_BLOCK_KEY)
.mapColor(MapColor.STONE)
.strength(1.5f, 6.0f)
.sounds(BlockSoundGroup.STONE)
.requiresTool()
);
public static void register() {
Registry.register(Registries.BLOCK, MY_BLOCK_KEY, MY_BLOCK);
}
}
// ModItems.java
public class ModItems {
public static final Identifier MY_ITEM_ID = Identifier.of(MyMod.MOD_ID, "my_item");
public static final RegistryKey<Item> MY_ITEM_KEY =
RegistryKey.of(RegistryKeys.ITEM, MY_ITEM_ID);
public static final RegistryKey<Item> MY_BLOCK_ITEM_KEY =
RegistryKey.of(RegistryKeys.ITEM, ModBlocks.MY_BLOCK_ID);
public static final Item MY_ITEM = new Item(
new Item.Settings().registryKey(MY_ITEM_KEY).maxCount(16)
);
// BlockItem for a block
public static final Item MY_BLOCK_ITEM = new BlockItem(
ModBlocks.MY_BLOCK, new Item.Settings().registryKey(MY_BLOCK_ITEM_KEY)
);
public static void register() {
Registry.register(Registries.ITEM, MY_ITEM_KEY, MY_ITEM);
Registry.register(Registries.ITEM, MY_BLOCK_ITEM_KEY, MY_BLOCK_ITEM);
// Add to creative tab
ItemGroupEvents.modifyEntriesEvent(ItemGroups.BUILDING_BLOCKS)
.register(content -> content.add(MY_BLOCK_ITEM));
}
}
```
---
## Block Entity
```java
// MyBlockEntity.java
public class MyBlockEntity extends BlockEntity {
private int processingTicks;
public MyBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.MY_BLOCK_ENTITY, pos, state);
}
@Override
protected void writeData(WriteView view) {
super.writeData(view);
view.putInt("processing_ticks", processingTicks);
}
@Override
protected void readData(ReadView view) {
super.readData(view);
processingTicks = view.getInt("processing_ticks", 0);
}
}
// ModBlockEntities.java
public class ModBlockEntities {
public static final BlockEntityType<MyBlockEntity> MY_BLOCK_ENTITY =
FabricBlockEntityTypeBuilder.create(MyBlockEntity::new, ModBlocks.MY_BLOCK).build();
public static void register() {
Registry.register(Registries.BLOCK_ENTITY_TYPE,
Identifier.of(MyMod.MOD_ID, "my_block_entity"), MY_BLOCK_ENTITY);
}
}
```
In Yarn 1.21.11, block-entity persistence uses `ReadView` and `WriteView`.
For compound state such as inventories, use the view's codec/list helpers; do not
copy the earlier `readNbt` / `writeNbt` overloads into this version.
`FabricBlockEntityTypeBuilder` is from
`net.fabricmc.fabric.api.object.builder.v1.block.entity`; see the
[versioned block-entity guide](https://docs.fabricmc.net/1.21.11/develop/blocks/block-entities).
---
## Mixins
Mixins patch Minecraft classes without modifying them. Use sparingly — prefer Fabric API hooks.
```java
// mixin/MixinServerPlayer.java
@Mixin(ServerPlayerEntity.class)
public abstract class MixinServerPlayer {
// @Inject — add code at a specific point
@Inject(method = "tick", at = @At("HEAD"))
private void onTickHead(CallbackInfo ci) {
ServerPlayerEntity self = (ServerPlayerEntity)(Object) this;
// runs at the beginning of ServerPlayerEntity.tick()
}
// @Overwrite — replace a method entirely (avoid if possible; breaks compat)
// @ModifyVariable — modify a local variable
// @Redirect — redirect a method call within a method
}
```
Mixin config file (`mymod.mixins.json`):
```json
{
"required": true,
"minVersion": "0.8",
"package": "com.example.mymod.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": ["MixinServerPlayer"],
"client": [],
"server": [],
"injectors": {
"defaultRequire": 1
}
}
```
---
## Fabric Events
Fabric API provides stable event hooks. Prefer these over Mixins.
```java
// Register event callbacks in onInitialize()
ServerTickEvents.END_SERVER_TICK.register(server -> {
// runs at end of every server tick
});
ServerLifecycleEvents.SERVER_STARTED.register(server -> {
MyMod.LOGGER.info("Server started!");
});
UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> {
// return ActionResult.PASS to not consume the event
return ActionResult.PASS;
});
AttackEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> {
return ActionResult.PASS;
});
// Item use callback
UseItemCallback.EVENT.register((player, world, hand) -> {
return ActionResult.PASS;
});
```
---
## Networking (Fabric 1.21 — ServerPlayNetworking)
```java
// Define a payload record
public record MyPayload(int data) implements CustomPayload {
public static final CustomPayload.Id<MyPayload> ID =
new CustomPayload.Id<>(Identifier.of(MyMod.MOD_ID, "my_payload"));
public static final PacketCodec<RegistryByteBuf, MyPayload> CODEC =
PacketCodec.tuple(PacketCodecs.INTEGER, MyPayload::data, MyPayload::new);
@Override
public CustomPayload.Id<? extends CustomPayload> getId() { return ID; }
}
// Common initializer, before receivers: register each supported direction's codec
PayloadTypeRegistry.playC2S().register(MyPayload.ID, MyPayload.CODEC);
PayloadTypeRegistry.playS2C().register(MyPayload.ID, MyPayload.CODEC);
// Server-side: register receiver
ServerPlayNetworking.registerGlobalReceiver(MyPayload.ID,
(payload, context) -> {
context.server().execute(() -> {
ServerPlayerEntity player = context.player();
// handle payload
});
});
// Send from server to client
ServerPlayNetworking.send(serverPlayerEntity, new MyPayload(42));
// Client-side: register receiver
ClientPlayNetworking.registerGlobalReceiver(MyPayload.ID,
(payload, context) -> {
context.client().execute(() -> {
// handle on client
});
});
// Send from client to server
ClientPlayNetworking.send(new MyPayload(42));
```
Register a codec only for the directions the protocol actually uses, on both
physical sides. Keep client receiver registration in the client initializer.
Validate client-supplied targets, counts, and permissions on the server before
changing state. See the [Fabric networking guide](https://docs.fabricmc.net/1.21.11/develop/networking).
---
## Commands (Fabric)
```java
// Register commands via CommandRegistrationCallback
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
dispatcher.register(
CommandManager.literal("mycommand")
.requires(CommandManager.requirePermissionLevel(CommandManager.GAMEMASTERS_CHECK))
.then(CommandManager.argument("target", EntityArgumentType.player())
.executes(ctx -> {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "target");
ctx.getSource().sendFeedback(
() -> Text.literal("Hello, " + player.getName().getString()), false);
return 1;
}))
);
});
```
The [1.21.11 command API](https://maven.fabricmc.net/docs/yarn-1.21.11+build.6/net/minecraft/server/command/CommandManager.html)
uses permission checks; the older `source.hasPermissionLevel(2)` call is not
available on this version.
---
## Custom Screen / GUI
Use the [versioned container-menu guide](https://docs.fabricmc.net/1.21.11/develop/blocks/container-menus)
with the project's mappings. Its examples may use Mojang names; this reference
uses Yarn names, so translate the API names before copying.
- Keep `ScreenHandler` slot order identical on server and client.
- Implement `quickMove` with explicit ranges for container slots, player
inventory, and hotbar; return an empty stack when no transfer is possible.
- For block-bound menus, make `canUse` verify the block and player distance.
- Register `HandledScreen` through the client initializer and use that version's
draw API.
---
## gradle.properties (Fabric Loom template)
The examples in this reference use Yarn/Fabric-named classes such as
`Identifier`, `ServerPlayerEntity`, and `AbstractBlock.Settings`. Use Yarn
mappings for Fabric-only projects. Use official Mojang mappings only when a
shared project intentionally standardizes on Mojmap and the snippets have been
translated.
```properties
org.gradle.jvmargs=-Xmx2G
minecraft_version=1.21.11
loader_version=0.19.3
fabric_version=0.141.4+1.21.11
yarn_mappings=1.21.11+build.6
# Fabric-only examples in this file expect:
# mappings("net.fabricmc:yarn:$yarn_mappings:v2")
# Shared multiloader examples can use loom.officialMojangMappings() instead.
mod_version=1.0.0
maven_group=com.example
archives_base_name=mymod
```
---
## Useful Fabric Classes (1.21.x Quick Reference)
|Need|Class|
|---|---|
|Block settings|`AbstractBlock.Settings`|
|Item settings|`Item.Settings`|
|Identifier|`Identifier.of("mymod", "thing")`|
|Map colours|`MapColor.*`|
|Block sounds|`BlockSoundGroup.*`|
|Tool materials|`ToolMaterial.*`|
|Text/chat|`Text.literal("...")`, `Text.translatable("key")`|
|Block entity persistence|`ReadView`, `WriteView`|
|NBT values when an API explicitly requires them|`NbtCompound`, `NbtList`|
|Registries|`Registries.*`|
|Data pack registry|`RegistryKey.of(RegistryKeys.BIOME, id)`|
|Fabric events|`ServerTickEvents`, `UseBlockCallback`, etc.|
|Item group events|`ItemGroupEvents.modifyEntriesEvent(ItemGroups.*)`|
references/forge-1.20.1-api.md
# Forge 1.20.1 API Patterns
Use this reference only for Minecraft `1.20.1` projects using MinecraftForge
`47.4.x` and Java 17. Do not apply these snippets to NeoForge 1.21.x projects.
Forge 1.20.1 and NeoForge 1.21.x share some concepts, but the package names,
metadata file, event bus access, helper classes, and several data paths differ.
Sources: [Forge 1.20.1 downloads](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.20.1.html)
and [Forge 1.20.x documentation](https://docs.minecraftforge.net/en/1.20.x/).
## Project Detection
A Forge 1.20.1 project usually has these signatures:
```text
build.gradle: id 'net.minecraftforge.gradle' version '[6.0,6.2)'
gradle.properties: minecraft_version=1.20.1
gradle.properties: forge_version=47.4.28
src/main/resources/META-INF/mods.toml
```
Use Java 17 for Forge 1.20.1. The official MDK template sets:
```gradle
java.toolchain.languageVersion = JavaLanguageVersion.of(17)
```
## Gradle Properties
```properties
org.gradle.jvmargs=-Xmx3G
org.gradle.daemon=false
minecraft_version=1.20.1
minecraft_version_range=[1.20.1,1.21)
forge_version=47.4.28
forge_version_range=[47,)
loader_version_range=[47,)
mapping_channel=official
mapping_version=1.20.1
mod_id=mymod
mod_name=My Mod
mod_license=MIT
mod_version=1.0.0
mod_group_id=com.example.mymod
mod_authors=YourName
mod_description=A Forge 1.20.1 mod.
```
Use the current Forge 1.20.1 `47.4.x` patch from the Forge files page when
scaffolding a new project. Keep `minecraft_version` and the left side of the
Forge artifact coordinate paired as `1.20.1-${forge_version}`.
## build.gradle Essentials
```gradle
plugins {
id 'eclipse'
id 'idea'
id 'maven-publish'
id 'net.minecraftforge.gradle' version '[6.0,6.2)'
}
version = mod_version
group = mod_group_id
base {
archivesName = mod_id
}
java.toolchain.languageVersion = JavaLanguageVersion.of(17)
minecraft {
mappings channel: mapping_channel, version: mapping_version
copyIdeResources = true
runs {
configureEach {
workingDirectory project.file('run')
property 'forge.logging.markers', 'REGISTRIES'
property 'forge.logging.console.level', 'debug'
mods {
"${mod_id}" {
source sourceSets.main
}
}
}
client {
property 'forge.enabledGameTestNamespaces', mod_id
}
server {
property 'forge.enabledGameTestNamespaces', mod_id
args '--nogui'
}
data {
workingDirectory project.file('run-data')
args '--mod', mod_id, '--all', '--output', file('src/generated/resources/'),
'--existing', file('src/main/resources/')
}
}
}
sourceSets.main.resources { srcDir 'src/generated/resources' }
dependencies {
minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}"
}
```
## mods.toml
Forge 1.20.1 uses `src/main/resources/META-INF/mods.toml`, not
`neoforge.mods.toml`.
```toml
modLoader="javafml"
loaderVersion="${loader_version_range}"
license="${mod_license}"
[[mods]]
modId="${mod_id}"
version="${mod_version}"
displayName="${mod_name}"
authors="${mod_authors}"
description='''${mod_description}'''
[[dependencies.${mod_id}]]
modId="forge"
mandatory=true
versionRange="${forge_version_range}"
ordering="NONE"
side="BOTH"
[[dependencies.${mod_id}]]
modId="minecraft"
mandatory=true
versionRange="${minecraft_version_range}"
ordering="NONE"
side="BOTH"
```
## Entry Point And Event Buses
Forge 1.20.1 uses `net.minecraftforge.*` imports. The mod event bus comes from
`FMLJavaModLoadingContext`, and gameplay events use `MinecraftForge.EVENT_BUS`.
```java
@Mod(MyMod.MOD_ID)
public class MyMod {
public static final String MOD_ID = "mymod";
private static final Logger LOGGER = LogUtils.getLogger();
public MyMod(FMLJavaModLoadingContext context) {
IEventBus modEventBus = context.getModEventBus();
ModBlocks.BLOCKS.register(modEventBus);
ModItems.ITEMS.register(modEventBus);
ModCreativeTabs.CREATIVE_MODE_TABS.register(modEventBus);
modEventBus.addListener(this::commonSetup);
modEventBus.addListener(this::addCreative);
MinecraftForge.EVENT_BUS.register(this);
}
private void commonSetup(FMLCommonSetupEvent event) {
LOGGER.info("Common setup for {}", MOD_ID);
}
private void addCreative(BuildCreativeModeTabContentsEvent event) {
if (event.getTabKey() == CreativeModeTabs.BUILDING_BLOCKS) {
event.accept(ModItems.MY_BLOCK_ITEM);
}
}
@SubscribeEvent
public void onServerStarting(ServerStartingEvent event) {
LOGGER.info("Server starting");
}
}
```
## DeferredRegister
Forge 1.20.1 uses `ForgeRegistries` and `RegistryObject`.
```java
public final class ModBlocks {
public static final DeferredRegister<Block> BLOCKS =
DeferredRegister.create(ForgeRegistries.BLOCKS, MyMod.MOD_ID);
public static final RegistryObject<Block> MY_BLOCK = BLOCKS.register(
"my_block",
() -> new Block(BlockBehaviour.Properties.of()
.mapColor(MapColor.STONE)
.strength(1.5f, 6.0f)
.sound(SoundType.STONE)
.requiresCorrectToolForDrops())
);
private ModBlocks() {
}
}
```
```java
public final class ModItems {
public static final DeferredRegister<Item> ITEMS =
DeferredRegister.create(ForgeRegistries.ITEMS, MyMod.MOD_ID);
public static final RegistryObject<Item> MY_BLOCK_ITEM = ITEMS.register(
"my_block",
() -> new BlockItem(ModBlocks.MY_BLOCK.get(), new Item.Properties())
);
public static final RegistryObject<Item> MY_ITEM = ITEMS.register(
"my_item",
() -> new Item(new Item.Properties())
);
private ModItems() {
}
}
```
For vanilla registries not wrapped by `ForgeRegistries`, use the matching
`net.minecraft.core.registries.Registries` key with `DeferredRegister.create`.
Creative mode tabs in the official MDK use this pattern.
```java
public static final DeferredRegister<CreativeModeTab> CREATIVE_MODE_TABS =
DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MyMod.MOD_ID);
```
## ResourceLocation
Forge 1.20.1 uses the public constructors available in Minecraft 1.20.1.
Do not use NeoForge 1.21.x helpers such as `ResourceLocation.fromNamespaceAndPath`.
```java
ResourceLocation id = new ResourceLocation(MyMod.MOD_ID, "my_block");
```
## Networking
Forge 1.20.1 uses `SimpleChannel` with `NetworkRegistry.newSimpleChannel`.
Register packets with stable integer discriminators and always mark packets as
handled after scheduling work.
```java
public final class ModNetworking {
private static final String PROTOCOL_VERSION = "1";
public static final SimpleChannel CHANNEL = NetworkRegistry.newSimpleChannel(
new ResourceLocation(MyMod.MOD_ID, "main"),
() -> PROTOCOL_VERSION,
PROTOCOL_VERSION::equals,
PROTOCOL_VERSION::equals
);
public static void register() {
int id = 0;
CHANNEL.registerMessage(
id++,
SyncEnergyPacket.class,
SyncEnergyPacket::encode,
SyncEnergyPacket::decode,
SyncEnergyPacket::handle
);
}
private ModNetworking() {
}
}
```
```java
public record SyncEnergyPacket(BlockPos pos, int energy) {
public static void encode(SyncEnergyPacket packet, FriendlyByteBuf buffer) {
buffer.writeBlockPos(packet.pos());
buffer.writeInt(packet.energy());
}
public static SyncEnergyPacket decode(FriendlyByteBuf buffer) {
return new SyncEnergyPacket(buffer.readBlockPos(), buffer.readInt());
}
public static void handle(SyncEnergyPacket packet, Supplier<NetworkEvent.Context> context) {
NetworkEvent.Context ctx = context.get();
ctx.enqueueWork(() -> {
ServerPlayer sender = ctx.getSender();
if (sender == null || !sender.level().hasChunkAt(packet.pos())) {
return;
}
// Handle trusted server-side work here.
});
ctx.setPacketHandled(true);
}
}
```
Send packets through the channel and Forge `PacketDistributor` helpers. In Forge
`47.4.x`, `PacketDistributor<T>.with` takes the target object directly; for
`PLAYER`, pass the `ServerPlayer`, not a `Supplier<ServerPlayer>`.
```java
ModNetworking.CHANNEL.send(PacketDistributor.PLAYER.with(serverPlayer), packet);
ModNetworking.CHANNEL.sendToServer(packet);
```
## Data Generation And Paths
Run Forge 1.20.1 data generation with `./gradlew runData`. The generated output
is normally wired from `src/generated/resources` into `sourceSets.main.resources`.
Register data providers on the mod event bus; Forge fires `GatherDataEvent` there
when the data generator starts.
```java
public MyMod(FMLJavaModLoadingContext context) {
IEventBus modEventBus = context.getModEventBus();
modEventBus.addListener(ModDataGen::gatherData);
}
```
Use Forge 1.20.1 provider classes and constructors rather than NeoForge 1.21.x
examples. `ExistingFileHelper` comes from the event and validates referenced
assets such as textures and parent models.
```java
public final class ModDataGen {
public static void gatherData(GatherDataEvent event) {
DataGenerator generator = event.getGenerator();
PackOutput output = generator.getPackOutput();
ExistingFileHelper existingFileHelper = event.getExistingFileHelper();
CompletableFuture<HolderLookup.Provider> lookupProvider = event.getLookupProvider();
generator.addProvider(event.includeClient(), new ModBlockStateProvider(output, existingFileHelper));
generator.addProvider(event.includeClient(), new ModItemModelProvider(output, existingFileHelper));
generator.addProvider(event.includeServer(), new ModRecipeProvider(output));
ModBlockTagsProvider blockTags = new ModBlockTagsProvider(output, lookupProvider, existingFileHelper);
generator.addProvider(event.includeServer(), blockTags);
generator.addProvider(event.includeServer(), new ModItemTagsProvider(
output,
lookupProvider,
blockTags.contentsGetter(),
existingFileHelper
));
}
private ModDataGen() {
}
}
```
Common Forge 1.20.1 provider constructors look like this:
```java
public final class ModBlockStateProvider extends BlockStateProvider {
public ModBlockStateProvider(PackOutput output, ExistingFileHelper existingFileHelper) {
super(output, MyMod.MOD_ID, existingFileHelper);
}
@Override
protected void registerStatesAndModels() {
// Generate blockstates, block models, and simple block item models here.
}
}
```
```java
public final class ModItemModelProvider extends ItemModelProvider {
public ModItemModelProvider(PackOutput output, ExistingFileHelper existingFileHelper) {
super(output, MyMod.MOD_ID, existingFileHelper);
}
@Override
protected void registerModels() {
// Generate standalone item models here.
}
}
```
```java
public final class ModRecipeProvider extends RecipeProvider {
public ModRecipeProvider(PackOutput output) {
super(output);
}
@Override
protected void buildRecipes(Consumer<FinishedRecipe> consumer) {
// Generate recipes here.
}
}
```
```java
public final class ModBlockTagsProvider extends BlockTagsProvider {
public ModBlockTagsProvider(
PackOutput output,
CompletableFuture<HolderLookup.Provider> lookupProvider,
ExistingFileHelper existingFileHelper
) {
super(output, lookupProvider, MyMod.MOD_ID, existingFileHelper);
}
@Override
protected void addTags(HolderLookup.Provider provider) {
// Generate block tags here.
}
}
```
```java
public final class ModItemTagsProvider extends ItemTagsProvider {
public ModItemTagsProvider(
PackOutput output,
CompletableFuture<HolderLookup.Provider> lookupProvider,
CompletableFuture<TagsProvider.TagLookup<Block>> blockTags,
ExistingFileHelper existingFileHelper
) {
super(output, lookupProvider, blockTags, MyMod.MOD_ID, existingFileHelper);
}
@Override
protected void addTags(HolderLookup.Provider provider) {
// Generate item tags here.
}
}
```
Minecraft 1.20.1 uses the older server-data path names:
```text
data/<modid>/loot_tables/blocks/<block>.json
data/<modid>/tags/blocks/<tag>.json
data/<modid>/tags/items/<tag>.json
```
Do not rewrite these to the 1.21.x singular path names when editing a Forge
1.20.1 project. Conversely, do not copy these legacy paths into 1.21.x projects.
## Common Porting Mistakes
| Mistake | Forge 1.20.1 fix |
|---|---|
| Using `net.neoforged.*` imports | Use `net.minecraftforge.*` imports |
| Creating `META-INF/neoforge.mods.toml` | Use `META-INF/mods.toml` |
| Injecting `IEventBus` into the mod constructor | Accept `FMLJavaModLoadingContext context` and call `context.getModEventBus()` |
| Using `DeferredBlock` / `DeferredItem` | Use `RegistryObject<T>` |
| Using `BuiltInRegistries.BLOCK` with Forge deferred registers | Use `ForgeRegistries.BLOCKS` |
| Using `ResourceLocation.fromNamespaceAndPath` | Use `new ResourceLocation(namespace, path)` |
| Assuming Java 21 | Use Java 17 for Minecraft 1.20.1 |
references/neoforge-api.md
# NeoForge API Patterns (1.21.11)
Legacy NeoForge-specific code patterns for Minecraft 1.21.11 with Java 21.
Use the exact project's versioned documentation for current 26.x / Java 25 work.
---
## Mod Entry Point
```java
// MyMod.java
@Mod(MyMod.MOD_ID)
public class MyMod {
public static final String MOD_ID = "mymod";
public static final Logger LOGGER = LogUtils.getLogger();
public MyMod(IEventBus modEventBus) {
// Register deferred registers with the mod event bus
ModBlocks.BLOCKS.register(modEventBus);
ModItems.ITEMS.register(modEventBus);
ModBlockEntities.BLOCK_ENTITIES.register(modEventBus);
ModMenuTypes.MENUS.register(modEventBus);
ModEntityTypes.ENTITY_TYPES.register(modEventBus);
ModSounds.SOUNDS.register(modEventBus);
// Register mod event listeners
modEventBus.addListener(this::commonSetup);
modEventBus.addListener(this::addCreativeTabItems);
}
private void commonSetup(FMLCommonSetupEvent event) {
event.enqueueWork(() -> {
// Main-thread setup that cannot run in parallel goes here.
});
}
private void addCreativeTabItems(BuildCreativeModeTabContentsEvent event) {
if (event.getTabKey() == CreativeModeTabs.BUILDING_BLOCKS) {
event.accept(ModItems.MY_ITEM);
}
}
}
```
---
## neoforge.mods.toml (META-INF/neoforge.mods.toml)
> File renamed from `mods.toml` to `neoforge.mods.toml` in NeoForge 1.20.5+.
> Always use `neoforge.mods.toml` for 1.21.x projects.
```toml
modLoader="javafml"
loaderVersion="[1,)"
license="MIT"
[[mods]]
modId="mymod"
version="${file.jarVersion}"
displayName="My Mod"
description='''
A brief description of what my mod does.
'''
logoFile="mymod.png"
[[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"
```
---
## DeferredRegister Patterns
```java
// ModBlocks.java
public class ModBlocks {
public static final DeferredRegister.Blocks BLOCKS =
DeferredRegister.createBlocks(MyMod.MOD_ID);
// Simple full-cube block
public static final DeferredBlock<Block> MY_BLOCK =
BLOCKS.registerSimpleBlock("my_block", () ->
BlockBehaviour.Properties.of()
.mapColor(MapColor.STONE)
.instrument(NoteBlockInstrument.BASEDRUM)
.strength(1.5f, 6.0f)
.sound(SoundType.STONE)
.requiresCorrectToolForDrops());
// Custom block class
public static final DeferredBlock<MySpecialBlock> SPECIAL_BLOCK =
BLOCKS.registerBlock("special_block", MySpecialBlock::new,
() -> BlockBehaviour.Properties.of().strength(2.0f));
}
```
```java
// ModItems.java
public class ModItems {
public static final DeferredRegister.Items ITEMS =
DeferredRegister.createItems(MyMod.MOD_ID);
// BlockItem for a block
public static final DeferredItem<BlockItem> MY_BLOCK_ITEM =
ITEMS.registerSimpleBlockItem(ModBlocks.MY_BLOCK);
// Simple item
public static final DeferredItem<Item> MY_ITEM =
ITEMS.registerSimpleItem("my_item", props -> props.stacksTo(16));
// Custom item class
public static final DeferredItem<MyCustomItem> MY_CUSTOM_ITEM =
ITEMS.registerItem("my_custom_item", MyCustomItem::new);
}
```
For 26.x tool and armor APIs, use the explicitly versioned patterns in
`common-patterns.md`; this reference otherwise retains 1.21.x examples.
---
## Block Entity
```java
// MyBlockEntity.java
public class MyBlockEntity extends BlockEntity {
private int processingTicks;
public MyBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.MY_BLOCK_ENTITY.get(), pos, state);
}
@Override
protected void saveAdditional(ValueOutput output) {
super.saveAdditional(output);
output.putInt("processing_ticks", processingTicks);
}
@Override
public void loadAdditional(ValueInput input) {
super.loadAdditional(input);
processingTicks = input.getIntOr("processing_ticks", 0);
}
}
// ModBlockEntities.java
public class ModBlockEntities {
public static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITIES =
DeferredRegister.create(BuiltInRegistries.BLOCK_ENTITY_TYPE, MyMod.MOD_ID);
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<MyBlockEntity>>
MY_BLOCK_ENTITY = BLOCK_ENTITIES.register("my_block_entity",
() -> new BlockEntityType<>(
MyBlockEntity::new, false, ModBlocks.MY_BLOCK.get()));
}
```
---
## Event buses and data generation (1.21.11)
The injected mod bus handles registration and lifecycle events; the game bus
is `NeoForge.EVENT_BUS`. Use `addListener` on the appropriate bus or
`@EventBusSubscriber`, which selects the bus from the event type in 1.21.11.
The old `bus = Bus.MOD` / `Bus.GAME` annotation argument is obsolete here.
Client renderer handlers need `value = Dist.CLIENT` and a separate client
class so dedicated servers do not load rendering classes. See the
[versioned event guide](https://docs.neoforged.net/docs/1.21.11/concepts/events/).
For datagen, use `GatherDataEvent.Client` or `.Server` and
`event.createProvider(...)`. The current MDK registers both kinds of providers
from `.Client` and runs `runClientData`. Keep the registry lookup, model
provider, and recipe runner wiring from the
[1.21.11 resource guide](https://docs.neoforged.net/docs/1.21.11/resources/);
older `ExistingFileHelper` constructors and `includeClient()` flags do not
belong in this version.
## Menus and screens
Build from the target version's
[menu guide](https://docs.neoforged.net/docs/1.21.11/inventories/menus/). Keep server
slots, client slots, synchronized data, and `quickMoveStack` index ranges
consistent. A block menu's `stillValid` must check the block and player range.
Register its client screen separately; preserve the current rendering API
instead of copying an older `GuiGraphics.blit` overload.
---
## Capabilities (NeoForge 1.21.11)
NeoForge registers providers for concrete block entities, blocks, entity types, or
items on the mod event bus. Its capability lookup returns the implementation or
`null`. Do not use Forge's `AttachCapabilitiesEvent`, `ICapabilityProvider`, or
`LazyOptional` patterns in a NeoForge 1.21.11 project.
```java
@EventBusSubscriber(modid = MyMod.MOD_ID)
public final class ModCapabilities {
@SubscribeEvent
public static void registerCapabilities(RegisterCapabilitiesEvent event) {
event.registerBlockEntity(
Capabilities.Item.BLOCK,
ModBlockEntities.MY_BLOCK_ENTITY.get(),
(blockEntity, side) -> blockEntity.getItemHandler(side)
);
}
}
// Query from a level. A null result means this side has no item handler.
ResourceHandler<ItemResource> handler =
level.getCapability(Capabilities.Item.BLOCK, pos, Direction.NORTH);
if (handler != null) {
// Use handler.
}
```
For a custom capability, create a static `BlockCapability`, `EntityCapability`,
or `ItemCapability` and register its provider with the same event. See the
[NeoForge 1.21.11 capability guide](https://docs.neoforged.net/docs/1.21.11/inventories/capabilities/).
---
## Network payloads (1.21.11)
```java
// Register a payload (packet) type
public record MyPayload(int value) implements CustomPacketPayload {
public static final Type<MyPayload> TYPE =
new Type<>(Identifier.fromNamespaceAndPath(MyMod.MOD_ID, "my_payload"));
public static final StreamCodec<ByteBuf, MyPayload> STREAM_CODEC =
StreamCodec.composite(ByteBufCodecs.INT, MyPayload::value, MyPayload::new);
@Override
public Type<? extends CustomPacketPayload> type() { return TYPE; }
}
// Register on MOD bus
@SubscribeEvent
public static void registerPayloads(RegisterPayloadHandlersEvent event) {
PayloadRegistrar registrar = event.registrar("1");
registrar.playToClient(MyPayload.TYPE, MyPayload.STREAM_CODEC,
(payload, ctx) -> {
// handle on client — ctx.enqueueWork() for thread safety
ctx.enqueueWork(() -> handleOnClient(payload));
});
registrar.playToServer(MyPayload.TYPE, MyPayload.STREAM_CODEC,
(payload, ctx) -> ctx.enqueueWork(() -> handleOnServer(payload, ctx.player())));
}
// Send from server to client
PacketDistributor.sendToPlayer(serverPlayer, new MyPayload(42));
// Send from client to server
PacketDistributor.sendToServer(new MyPayload(42));
```
---
## Biome Modifier (World Gen Integration)
`data/mymod/neoforge/biome_modifier/add_spawn.json`:
```json
{
"type": "neoforge:add_spawns",
"biomes": "#minecraft:is_overworld",
"spawners": [
{
"type": "mymod:my_entity",
"weight": 10,
"minCount": 2,
"maxCount": 4
}
]
}
```
---
## gradle.properties (NeoForge MDK template)
```properties
org.gradle.jvmargs=-Xmx3G
org.gradle.daemon=false
minecraft_version=1.21.11
minecraft_version_range=[1.21.11,1.22)
neo_version=21.11.42
neo_version_range=[21.11,)
loader_version_range=[1,)
mod_id=mymod
mod_name=My Mod
mod_license=MIT
mod_version=1.0.0
mod_group_id=com.example.mymod
mod_authors=YourName
mod_description=A cool Minecraft mod.
## Dependencies (optional)
# patchouli_version=...
```
---
## Useful NeoForge Classes (1.21.x Quick Reference)
|Need|Class|
|---|---|
|Block properties|`BlockBehaviour.Properties`|
|Item properties|`Item.Properties`|
|Map colours|`MapColor.*`|
|Block sounds|`SoundType.*`|
|Tool materials|`ToolMaterial.*`|
|Rarity|`Rarity.*` (COMMON, UNCOMMON, RARE, EPIC)|
|Block tags|`BlockTags.*`|
|Item tags|`ItemTags.*`|
|Entity categories|`MobCategory.*`|
|Directions|`Direction.*`|
|Block positions|`BlockPos`, `BlockPos.MutableBlockPos`|
|Level access|`Level`, `ServerLevel`|
|Registry access|`BuiltInRegistries.*`, `Registries.*`|
scripts/check-build.sh
#!/usr/bin/env bash
# check-build.sh
# Verifies the Minecraft mod build environment and runs the project's build task.
# Run from the root of a Minecraft mod project.
set -euo pipefail
PASS="[PASS]"
FAIL="[FAIL]"
WARN="[WARN]"
check_one_of() {
local label="$1"
shift
for f in "$@"; do
if [[ -f "$f" ]]; then
echo "$PASS $f"
return 0
fi
done
echo "$WARN $label not found"
}
read_gradle_property() {
local key="$1"
if [[ -f "gradle.properties" ]]; then
sed -n "s/^${key}=//p" gradle.properties | head -1
fi
}
parse_java_major() {
local version="$1"
if [[ "$version" =~ ^1\.([0-9]+) ]]; then
echo "${BASH_REMATCH[1]}"
return 0
fi
if [[ "$version" =~ ^([0-9]+) ]]; then
echo "${BASH_REMATCH[1]}"
return 0
fi
return 1
}
version_at_least() {
local found="$1"
local required="$2"
[[ "$found" =~ ^[0-9]+$ ]] || return 1
[[ "$found" -ge "$required" ]]
}
echo "=== Minecraft Mod Build Environment Check ==="
echo ""
BUILD_FILES=(build.gradle build.gradle.kts settings.gradle settings.gradle.kts gradle.properties)
MINECRAFT_VERSION=$(read_gradle_property "minecraft_version")
# Platform detection is needed before the Java check because Forge 1.20.1 targets Java 17.
echo "Detecting mod platform..."
PLATFORM="unknown"
if grep -qr "architectury" "${BUILD_FILES[@]}" 2>/dev/null; then
PLATFORM="architectury"
elif grep -qr "net.neoforged" "${BUILD_FILES[@]}" 2>/dev/null; then
PLATFORM="neoforge"
elif grep -qr "net.minecraftforge" "${BUILD_FILES[@]}" 2>/dev/null; then
PLATFORM="forge"
elif grep -qr "fabric-loom\|fabricmc" "${BUILD_FILES[@]}" 2>/dev/null; then
PLATFORM="fabric"
fi
if [[ -n "$MINECRAFT_VERSION" ]]; then
echo "$PASS Platform: $PLATFORM (Minecraft $MINECRAFT_VERSION)"
else
echo "$PASS Platform: $PLATFORM"
fi
REQUIRED_JAVA=21
JAVA_REASON="Minecraft 1.20.5+ / 1.21.x requires Java 21"
if [[ "$MINECRAFT_VERSION" =~ ^([0-9]+)\.([0-9]+) ]] && (( BASH_REMATCH[1] >= 26 )); then
REQUIRED_JAVA=25
JAVA_REASON="Minecraft 26.x requires Java 25"
elif [[ "$PLATFORM" == "forge" && "$MINECRAFT_VERSION" == "1.20.1" ]]; then
REQUIRED_JAVA=17
JAVA_REASON="Forge 1.20.1 requires Java 17+ and should target Java 17"
fi
# Java version
echo ""
echo "Checking Java version..."
if ! command -v java &>/dev/null; then
echo "$FAIL java not found. Install JDK $REQUIRED_JAVA from https://adoptium.net/"
exit 1
fi
JAVA_VERSION=$(java -version 2>&1 | head -1 | sed -n 's/.*version "\([^"]*\)".*/\1/p')
JAVA_MAJOR=$(parse_java_major "$JAVA_VERSION" || true)
if [[ -z "$JAVA_MAJOR" ]]; then
echo "$FAIL Could not parse Java version from: $JAVA_VERSION"
exit 1
fi
if version_at_least "$JAVA_MAJOR" "$REQUIRED_JAVA"; then
echo "$PASS Java $JAVA_VERSION (JDK $REQUIRED_JAVA+ available)"
if [[ "$PLATFORM" == "forge" && "$MINECRAFT_VERSION" == "1.20.1" && "$JAVA_MAJOR" -gt 17 ]]; then
echo "$WARN Forge 1.20.1 projects should still compile with a Java 17 toolchain target"
fi
else
echo "$FAIL Java $JAVA_VERSION - $JAVA_REASON"
echo " Install from: https://adoptium.net/temurin/releases/?version=$REQUIRED_JAVA"
exit 1
fi
# Gradle wrapper
echo ""
echo "Checking Gradle wrapper..."
if [[ ! -f "gradlew" ]]; then
echo "$FAIL gradlew not found. Are you in the root of a Minecraft mod project?"
exit 1
fi
echo "$PASS gradlew found"
# Key files
echo ""
echo "Checking key mod files..."
case "$PLATFORM" in
neoforge)
for f in "src/main/resources/META-INF/neoforge.mods.toml" "gradle.properties"; do
[[ -f "$f" ]] && echo "$PASS $f" || echo "$WARN $f not found"
done
[[ -f "build.gradle" || -f "build.gradle.kts" ]] && echo "$PASS build script found" || echo "$WARN build script not found"
;;
forge)
for f in "src/main/resources/META-INF/mods.toml" "gradle.properties"; do
[[ -f "$f" ]] && echo "$PASS $f" || echo "$WARN $f not found"
done
if [[ "$MINECRAFT_VERSION" != "1.20.1" ]]; then
echo "$WARN Forge support in this skill is documented for Minecraft 1.20.1; verify other Forge versions upstream"
fi
[[ -f "build.gradle" || -f "build.gradle.kts" ]] && echo "$PASS build script found" || echo "$WARN build script not found"
;;
fabric)
for f in "src/main/resources/fabric.mod.json" "gradle.properties"; do
[[ -f "$f" ]] && echo "$PASS $f" || echo "$WARN $f not found"
done
[[ -f "build.gradle" || -f "build.gradle.kts" ]] && echo "$PASS build script found" || echo "$WARN build script not found"
;;
architectury)
check_one_of "common build script" "common/build.gradle" "common/build.gradle.kts"
check_one_of "fabric build script" "fabric/build.gradle" "fabric/build.gradle.kts"
check_one_of "neoforge build script" "neoforge/build.gradle" "neoforge/build.gradle.kts"
[[ -f "gradle.properties" ]] && echo "$PASS gradle.properties" || echo "$WARN gradle.properties not found"
;;
*)
echo "$WARN Unknown platform; skipping file checks"
;;
esac
# Run Gradle build
echo ""
echo "Running ./gradlew build..."
./gradlew build --console=plain
JAR_COUNT=$(find . -type f -path "*/build/libs/*.jar" ! -name "*-sources.jar" ! -name "*-dev.jar" 2>/dev/null | wc -l)
if [[ "$JAR_COUNT" -gt 0 ]]; then
echo ""
echo "$PASS Gradle build completed. Candidate output jar(s):"
find . -type f -path "*/build/libs/*.jar" ! -name "*-sources.jar" ! -name "*-dev.jar" | while read -r jar; do
echo " -> $jar"
done
else
echo ""
echo "$FAIL Build did not produce a jar. Check Gradle output above."
exit 1
fi
echo ""
echo "$WARN Candidate jars may predate this incremental build. Identify the intended distributable from Gradle output and run project-specific release validation before publishing."
echo "=== Build environment check complete ==="
SKILL.md
---
name: minecraft-modding
description: "Create, modify, debug, or migrate Minecraft mods for current NeoForge or Fabric 26.x, legacy 1.21.x, and Forge 1.20.1. Use for loader-based gameplay code and assets; use minecraft-multiloader when one codebase must target both modern loaders."
---
# Minecraft Modding Skill
## Overview
Supported platforms:
| Platform | MC Version | Java | Build System |
|---|---|---|---|
| **NeoForge** | 26.x current; 1.21.11 examples retained | Java 25 current; Java 21 on 1.21.x | Gradle + ModDevGradle |
| **Forge** | 1.20.1 legacy lane | Java 17 | Gradle + ForgeGradle 6 |
| **Fabric** | 26.x current; 1.21.11 examples retained | Java 25 current; Java 21 on 1.21.x | Gradle + Fabric Loom |
| **Architectury** (multiloader) | 26.x or 1.21.x | Match Minecraft | Gradle + Architectury Loom |
Always confirm the platform and Minecraft version from `gradle.properties` or `build.gradle`
before writing any mod-specific code.
Minecraft 26.1 introduced Java 25 and unobfuscated game executables. For 26.x
projects, start from the current loader generator or example mod and preserve
its build layout. Do not copy the 1.21.11 mapping, Loom plugin, remapping task,
or Java 21 snippets in this skill into a 26.x project. Fabric 26.x uses the
non-remapping Loom path and official names; NeoForge 26.x should start from the
current NeoForge generator. The detailed API references cover legacy 1.21.x
examples unless a section explicitly says 26.x.
### Routing Boundaries
- `Use when`: the task is Java/Kotlin mod code, registry/event work, networking, datagen wiring, and loader APIs.
- `Do not use when`: the task is command-only vanilla logic (`minecraft-commands-scripting`) or pure datapacks (`minecraft-datapack`).
- `Do not use when`: the task targets Paper/Bukkit plugins (`minecraft-plugin-dev`).
---
## 1. Identifying the Platform
```bash
# NeoForge project signature
grep -r "net.neoforged" gradle.properties build.gradle settings.gradle 2>/dev/null | head -5
# Forge 1.20.1 project signature
grep -r "net.minecraftforge" gradle.properties build.gradle settings.gradle 2>/dev/null | head -5
# Fabric project signature
grep -r "fabric" gradle.properties build.gradle settings.gradle 2>/dev/null | head -5
# Read mod ID and version
cat gradle.properties
```
Key files per platform:
- **NeoForge**: `src/main/resources/META-INF/neoforge.mods.toml`, annotated `@Mod` main class
- **Forge 1.20.1**: `src/main/resources/META-INF/mods.toml`, `net.minecraftforge:forge` dependency
- **Fabric**: `src/main/resources/fabric.mod.json`, class implementing `ModInitializer`
- **Architectury**: `common/`, `fabric/`, `neoforge/` subprojects
---
## 2. Build & Test Commands
```bash
# Build the mod jar
./gradlew build
# Run the Minecraft client to test
./gradlew runClient
# Run a dedicated server to test
./gradlew runServer
# NeoForge Game Tests (when the project configures this run)
./gradlew runGameTestServer
# Current NeoForge MDK data generation; Fabric/Forge use their configured task
./gradlew runClientData
# Remove this project's generated build outputs before a fresh rebuild
./gradlew clean
# Only if the project applies a dependency-updates plugin
./gradlew dependencyUpdates
```
`./gradlew build` runs the project's configured build tasks. Candidate mod jars are
usually under `build/libs/`, but task names and file names are project-specific.
Check the build result, then identify the intended
distributable before publishing it.
---
## 3. Project Layout (NeoForge)
```
src/
main/
java/<groupId>/<modid>/
MyMod.java ← @Mod entry point
block/
ModBlocks.java ← DeferredRegister.Blocks
MyCustomBlock.java
item/
ModItems.java ← DeferredRegister.Items
entity/
ModEntities.java ← DeferredRegister.Entities
menu/ ← custom GUI containers
recipe/
worldgen/
datagen/
ModDataGen.java ← GatherDataEvent handler
providers/
resources/
META-INF/
neoforge.mods.toml ← mod metadata (renamed from mods.toml in NeoForge 1.20.5+)
assets/<modid>/
blockstates/ ← JSON blockstate definitions
models/
block/ ← block model JSON
item/ ← item model JSON
items/ ← 1.21.4+ item-definition JSON
textures/
block/ ← 16×16 PNG textures
item/
lang/
en_us.json ← translation strings
data/<modid>/
recipe/ ← crafting recipe JSON (26.x)
loot_table/
blocks/ ← per-block loot table JSON
tags/
block/
item/
```
## 4. Project Layout (Forge 1.20.1)
Use this layout only when `minecraft_version=1.20.1` and the project depends on
`net.minecraftforge:forge`. Forge 1.20.1 is not NeoForge: keep `mods.toml`,
`net.minecraftforge.*` imports, Java 17, and ForgeGradle 6 patterns.
```
src/
main/
java/<groupId>/<modid>/
MyMod.java <- @Mod entry point
block/
ModBlocks.java <- DeferredRegister<Block>
item/
ModItems.java <- DeferredRegister<Item>
datagen/
ModDataGen.java <- GatherDataEvent handler
resources/
META-INF/
mods.toml <- Forge metadata
assets/<modid>/ <- client assets
data/<modid>/ <- server data using 1.20.1 paths
```
See `references/forge-1.20.1-api.md` before editing Forge 1.20.1 projects.
## 5. Project Layout (Fabric)
```
src/
main/
java/<groupId>/<modid>/
MyMod.java ← implements ModInitializer
client/
MyModClient.java ← implements ClientModInitializer
block/
item/
mixin/ ← Mixin classes
resources/
fabric.mod.json
assets/<modid>/ ← same as NeoForge
data/<modid>/ ← same as NeoForge
<modid>.mixins.json ← mixin configuration
```
---
## 6. Core Concepts Cheatsheet
### Sides
- **Physical client** – the game client JAR (has rendering code)
- **Physical server** – the dedicated server JAR (no rendering)
- **Logical client** – the client thread (handles rendering, input)
- **Logical server** – the server thread (handles world simulation)
- Keep rendering classes in a client-only initializer or listener. A logical-side
check alone does not prevent dedicated servers from loading client classes.
Follow the loader's [physical-side guidance](https://docs.neoforged.net/docs/1.21.11/concepts/sides/).
### Registries
Everything in Minecraft lives in a registry. Always register objects; never
construct them at field initializer time outside a registry call. Use the
mapping-appropriate registry constants for the loader you are editing:
| Type | NeoForge / Mojang mappings | Fabric / Yarn mappings |
|------|-----------------------------|-------------------------|
| Blocks | `BuiltInRegistries.BLOCK` | `Registries.BLOCK` |
| Items | `BuiltInRegistries.ITEM` | `Registries.ITEM` |
| Entity types | `BuiltInRegistries.ENTITY_TYPE` | `Registries.ENTITY_TYPE` |
| Block entity types | `BuiltInRegistries.BLOCK_ENTITY_TYPE` | `Registries.BLOCK_ENTITY_TYPE` |
| Menu / screen-handler types | `BuiltInRegistries.MENU` | `Registries.SCREEN_HANDLER` |
| Sound events | `BuiltInRegistries.SOUND_EVENT` | `Registries.SOUND_EVENT` |
| Biomes | `Registries.BIOME` registry keys | `RegistryKeys.BIOME` registry keys |
Do not copy older `Registry.BLOCK` / `Registry.ITEM` constants into 1.21.x code;
those names are stale for the examples in this skill.
### ResourceLocation / Identifier
Every registry entry needs a namespaced ID:
```java
// 26.x official names (NeoForge and Fabric)
Identifier id = Identifier.fromNamespaceAndPath("mymod", "my_block");
// Older Mojang mappings use ResourceLocation; keep the project's version.
// Legacy Fabric with Yarn mappings
Identifier id = Identifier.of("mymod", "my_block");
```
---
## 7. NeoForge Quick Patterns (26.x)
For 26.x, use the explicitly labelled 26.x sections in
`references/common-patterns.md` and select the project's exact version in the
[NeoForge documentation](https://docs.neoforged.net/docs/gettingstarted/).
`references/neoforge-api.md` contains legacy 1.21.x / Java 21 patterns only;
do not copy its dependency pins into a 26.x project.
```java
// Main mod class
@Mod(MyMod.MOD_ID)
public class MyMod {
public static final String MOD_ID = "mymod";
public MyMod(IEventBus modEventBus) {
ModBlocks.BLOCKS.register(modEventBus);
ModItems.ITEMS.register(modEventBus);
modEventBus.addListener(this::commonSetup);
}
private void commonSetup(FMLCommonSetupEvent event) {
// runs after all mods are registered
}
}
```
```java
// Block registration
public class ModBlocks {
public static final DeferredRegister.Blocks BLOCKS =
DeferredRegister.createBlocks(MyMod.MOD_ID);
public static final DeferredBlock<Block> MY_BLOCK =
BLOCKS.registerSimpleBlock("my_block", () ->
BlockBehaviour.Properties.of()
.mapColor(MapColor.STONE)
.strength(1.5f, 6.0f)
.sound(SoundType.STONE)
.requiresCorrectToolForDrops());
}
```
---
## 8. Forge 1.20.1 Quick Patterns
See full patterns in `references/forge-1.20.1-api.md`.
```java
// Main mod class
@Mod(MyMod.MOD_ID)
public class MyMod {
public static final String MOD_ID = "mymod";
public MyMod(FMLJavaModLoadingContext context) {
IEventBus modEventBus = context.getModEventBus();
ModBlocks.BLOCKS.register(modEventBus);
ModItems.ITEMS.register(modEventBus);
modEventBus.addListener(this::commonSetup);
MinecraftForge.EVENT_BUS.register(this);
}
private void commonSetup(FMLCommonSetupEvent event) {
// runs after registries are prepared
}
}
```
```java
// Block registration
public class ModBlocks {
public static final DeferredRegister<Block> BLOCKS =
DeferredRegister.create(ForgeRegistries.BLOCKS, MyMod.MOD_ID);
public static final RegistryObject<Block> MY_BLOCK =
BLOCKS.register("my_block", () -> new Block(
BlockBehaviour.Properties.of()
.mapColor(MapColor.STONE)
.strength(1.5f, 6.0f)
.sound(SoundType.STONE)
.requiresCorrectToolForDrops()));
}
```
---
## 9. Fabric Quick Patterns
Match the project's Minecraft version and mappings in the
[Fabric documentation](https://docs.fabricmc.net/develop/).
`references/fabric-api.md` contains legacy 1.21.x / Java 21 patterns only.
The explicitly labelled 26.x sections in `references/common-patterns.md`
use NeoForge syntax; adapt them against the exact Fabric API rather than
copying loader-specific classes or legacy dependency pins.
```java
// Main mod class
public class MyMod implements ModInitializer {
public static final String MOD_ID = "mymod";
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
@Override
public void onInitialize() {
ModBlocks.initialize();
ModItems.register();
}
}
```
```java
// Fabric 26.x with official Mojang mappings. Create the key before the object
// so its properties receive the required id during construction.
public final class ModBlocks {
public static final ResourceKey<Block> MY_BLOCK_KEY = ResourceKey.create(
Registries.BLOCK,
Identifier.fromNamespaceAndPath(MyMod.MOD_ID, "my_block")
);
public static final Block MY_BLOCK = register(
MY_BLOCK_KEY,
Block::new,
BlockBehaviour.Properties.of()
.mapColor(MapColor.STONE)
.strength(1.5f, 6.0f)
.sound(SoundType.STONE)
.requiresCorrectToolForDrops()
);
private static Block register(ResourceKey<Block> key,
Function<BlockBehaviour.Properties, Block> factory,
BlockBehaviour.Properties properties) {
Block block = factory.apply(properties.setId(key));
return Registry.register(BuiltInRegistries.BLOCK, key, block);
}
public static void initialize() {}
}
```
Call `ModBlocks.initialize()` from the Fabric initializer. The Yarn-named
1.21.11 examples remain in `references/fabric-api.md`; do not mix those names
with this 26.x pattern.
---
## 10. JSON Asset Templates
Always provide matching JSON assets for every registered block/item.
Codex should generate or update these files alongside Java code.
For Forge 1.20.1, check `references/forge-1.20.1-api.md` for legacy server-data
directory names before creating loot tables or tags.
See `references/common-patterns.md` for full JSON templates for:
- Blockstate JSON
- Block model JSON (cube, slab, stairs, fence, door, trapdoor, etc.)
- Item model JSON
- Loot table JSON
- Recipe JSON (crafting_shaped, crafting_shapeless, smelting, blasting, stonecutting)
- Language file (`en_us.json`) entries
- Tag JSON
---
## 11. Data Generation
Use the project's configured data providers. Current NeoForge uses
`GatherDataEvent.Client` / `.Server` and `event.createProvider(...)`; the MDK
normally generates both asset and data providers with `runClientData`. See the
[version-matched resource guide](https://docs.neoforged.net/docs/resources/)
before wiring providers. Keep Fabric and older projects on their own datagen
entrypoints and Gradle tasks, then inspect the generated files.
For Forge 1.20.1, use the mod-event-bus registration, `GatherDataEvent`
signature, provider classes, and legacy output paths from
`references/forge-1.20.1-api.md`.
---
## 12. Common Tasks Checklist
When adding a **new block**:
- [ ] `Block` subclass (or use vanilla Block with properties)
- [ ] Register in `ModBlocks.BLOCKS` / `Registries.BLOCK`
- [ ] Register `BlockItem` in `ModItems.ITEMS` / `Registries.ITEM`
- [ ] Blockstate JSON → `assets/<modid>/blockstates/<name>.json`
- [ ] Block model JSON → `assets/<modid>/models/block/<name>.json`
- [ ] Item model JSON → `assets/<modid>/models/item/<name>.json` (or inherits from block)
- [ ] 1.21.4+ item definition → `assets/<modid>/items/<name>.json`, pointing at the item or block model
- [ ] Texture PNG → `assets/<modid>/textures/block/<name>.png`
- [ ] Loot table JSON -> 1.21.x: `data/<modid>/loot_table/blocks/<name>.json`; Forge 1.20.1: `data/<modid>/loot_tables/blocks/<name>.json`
- [ ] Tags -> 1.21.x: `data/<modid>/tags/block/` and `tags/item/`; Forge 1.20.1: `data/<modid>/tags/blocks/` and `tags/items/`
- [ ] Language entry in `en_us.json`
- [ ] Vanilla mining and tool-tier tags when `requiresCorrectToolForDrops` is set
- [ ] Do not mix Forge 1.20.1 plural server-data paths with 1.21.x singular server-data paths
When adding a **new item**:
- [ ] `Item` subclass (or use `new Item(properties)`)
- [ ] Register in `ModItems` / `Registries.ITEM`
- [ ] Item model JSON
- [ ] 1.21.4+ item definition → `assets/<modid>/items/<name>.json`
- [ ] Texture PNG
- [ ] Language entry
- [ ] Creative tab registration (NeoForge/Forge: `BuildCreativeModeTabContentsEvent`; Fabric: `ItemGroupEvents`)
- [ ] Recipe JSON if craftable (`data/<modid>/recipe/` for 26.x; see the
version-specific recipe reference before using a 1.21.x project)
When adding a **new entity**:
- [ ] Entity class (extends appropriate base: `Mob`, `Animal`, `TamableAnimal`, etc.)
- [ ] `EntityType` registration
- [ ] Renderer and model classes loaded only on the physical client
- [ ] Register the renderer with `EntityRenderersEvent.RegisterRenderers` (NeoForge)
or `EntityRendererRegistry` (Fabric); register model layers separately
- [ ] Spawn egg item (optional)
- [ ] Spawn rules / biome modifier
---
## 13. References
- NeoForge API patterns and event system: `./references/neoforge-api.md`
- Forge 1.20.1 API patterns and MDK workflow: `./references/forge-1.20.1-api.md`
- Fabric API patterns and mixin guide: `./references/fabric-api.md`
- Blocks, items, recipes, commands, datagen: `./references/common-patterns.md`
- NeoForge official docs: https://docs.neoforged.net/
- Fabric developer docs: https://docs.fabricmc.net/develop/
- Architectury (multiloader): https://docs.architectury.dev/
- Minecraft Wiki (data formats): https://minecraft.wiki/w/Java_Edition_data_values