Как создавать мод для Minecraft: выбор версии, Forge/Fabric, структура проекта, примеры и публикация
1. Введение
Мод Minecraft — это дополнение, которое расширяет игру на уровне кода: добавляет новые блоки, предметы, сущности, механики, интерфейсы, сетевое взаимодействие и интеграции с другими модами.
Важно различать три подхода, потому что они дают разный “потолок” возможностей:
-
Resourcepack: текстуры, модели, звуки, интерфейсные ресурсы. Код не меняет.
-
Datapack: рецепты, лут-таблицы, теги, функции, простая логика (в рамках возможностей датапака). Код не требует.
-
Полноценный мод: Java-код + ресурсы. Максимальная гибкость, но выше требования к подготовке и поддержке.
2. Что выбрать: datapack/resourcepack или мод
2.1. Когда достаточно datapack
Datapack подходит, если нужно:
-
добавить/изменить рецепты крафта;
-
настроить дроп (loot tables);
-
добавить/изменить теги (tags);
-
добавить достижения (advancements);
-
настроить генерацию в мире в рамках поддерживаемых механизмов.
Плюсы:
-
не нужен загрузчик модов;
-
проще обновлять между версиями (в среднем);
-
ниже порог входа.
Минусы:
-
ограничения по логике и взаимодействию;
-
сложнее сделать “новую механику” (GUI, сеть, сложное поведение).
2.2. Когда нужен полноценный мод
Мод нужен, если требуется:
-
новые блоки/предметы с кастомным поведением;
-
новые мобы/сущности;
-
новые интерфейсы, контейнеры, меню;
-
новая механика мира, машины, энергия, сложные системы;
-
сетевые пакеты и мультиплеерная логика.
Плюсы:
-
максимум возможностей;
-
можно делать масштабные системы и интеграции.
Минусы:
-
потребуется Java и понимание архитектуры;
-
обновления Minecraft часто ломают API и требуют сопровождения;
-
выше риск конфликтов и багов без тестирования.
3. Выбор версии Minecraft и модлоадера
3.1. Почему версия важна
Мод “привязан” к версии игры и к конкретному лоадеру. Если выбрать версию без экосистемы или слишком новую/редкую, мод будет сложнее тестировать, а пользователям — собирать модпак.
Практическая рекомендация:
-
выбирайте версию, под которую уже существуют нужные вам зависимости (библиотеки, API, базовые моды);
-
фиксируйте версию на старте проекта и обновляйтесь только по плану.
3.2. Forge / NeoForge
Forge-ветка (и NeoForge как ответвление экосистемы) обычно выбирается, когда:
-
нужны “тяжелые” моды и большая экосистема;
-
нужны готовые решения и привычные паттерны (регистры, события);
-
планируется совместимость с большим набором Forge-модов.
Плюсы:
-
широкая экосистема;
-
зрелые практики для крупных модов;
-
много примеров архитектурных решений.
Минусы:
-
сложнее старт для новичка;
-
некоторые версии требуют больше внимания к конфигурации и зависимости.
3.3. Fabric (и Quilt)
Fabric часто выбирают, когда:
-
нужен относительно легкий стек;
-
важна скорость обновлений и минимализм;
-
вы готовы работать с более “низкоуровневым” подходом (включая mixin-практики, если требуется).
Плюсы:
-
проще “тонко” вмешиваться в игру;
-
часто легче стартовый проект;
-
хорошие варианты для небольших модов и оптимизаций.
Минусы:
-
для некоторых задач придется больше собирать “вручную”;
-
совместимость и “готовые модули” зависят от конкретной экосистемы.
4. Подготовка окружения разработки
4.1. Java (JDK)
Minecraft использует Java. Для моддинга нужны:
-
JDK (не JRE);
-
корректная версия Java под целевую версию Minecraft.
Ориентиры по версиям Java (в типовых ветках Minecraft):
-
старые ветки (например, 1.16.x) часто работают на Java 8;
-
современные ветки (1.18+ и далее) обычно требуют Java 17;
-
отдельные будущие версии могут поднять требование выше — это нужно проверять на момент выбора версии.
4.2. IDE
На практике используют:
-
IntelliJ IDEA (часто удобнее для Gradle, анализа кода и рефакторинга),
-
Eclipse (тоже возможно, но обычно требует больше ручной настройки).
4.3. Gradle и структура проекта
Почти все современные шаблоны модов собираются Gradle’ом:
-
зависимости лоадера;
-
запуск dev-клиента/сервера;
-
сборка итогового JAR.
Типовые проблемы:
-
неправильная версия JDK в Gradle;
-
сломанные кэши Gradle;
-
конфликт версий библиотек.
5. Создание проекта мода: общая структура
Независимо от лоадера, структура обычно похожа:
-
src/main/java— исходники Java -
src/main/resources— ресурсы мода:-
assets//— клиентские ресурсы (текстуры, модели, lang и т. д.) -
data//— дата-ресурсы (рецепты, лут, теги, worldgen и т. д.)
-
5.1. Базовые идентификаторы
-
modid: короткий идентификатор (строчные латинские буквы, цифры, подчёркивание). Не меняется после релиза.
-
name: отображаемое имя мода.
-
version: версия мода (желательно семантически:
1.0.0,1.0.1и т. п.).
6. Архитектура мода и регистрация (углубление)
Примечание по версиям: API Forge/Fabric меняется между ветками Minecraft, поэтому примеры ориентированы на “современные” версии (логика и структура верны, но названия отдельных классов/ивентов могут отличаться на конкретной связке).
6.1. Минимальная структура проекта
Общая структура (подходит и для Forge, и для Fabric)
examplemod/
build.gradle
settings.gradle
gradle.properties
src/main/java/com/example/examplemod/
ExampleMod.java
ModItems.java
ModBlocks.java
ModCreativeTabs.java (опционально)
network/
ModNetworking.java
ExampleC2SPacket.java
block/
ExampleBlock.java (опционально)
src/main/resources/
assets/examplemod/
lang/ru_ru.json
models/item/example_ingot.json
models/block/example_block.json
models/item/example_block.json
blockstates/example_block.json
textures/item/example_ingot.png
textures/block/example_block.png
data/examplemod/
recipes/example_block.json
loot_tables/blocks/example_block.json
tags/items/example_ingots.json
6.2. Forge: регистрация через DeferredRegister (правильная “база”)
6.2.1. Main-класс мода (Forge)
package com.example.examplemod;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
@Mod(ExampleMod.MODID)
public class ExampleMod {
public static final String MODID = "examplemod";
public ExampleMod() {
IEventBus modBus = FMLJavaModLoadingContext.get().getModEventBus();
ModItems.register(modBus);
ModBlocks.register(modBus);
ModCreativeTabs.register(modBus); // опционально
// Сеть обычно инициализируют в common setup или статически (зависит от версии)
ModNetworking.init();
}
}
6.2.2. Предметы (Forge)
package com.example.examplemod;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.Rarity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.Level;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
import java.util.List;
public final class ModItems {
private ModItems() {}
public static final DeferredRegister ITEMS =
DeferredRegister.create(ForgeRegistries.ITEMS, ExampleMod.MODID);
// Простой предмет
public static final RegistryObject EXAMPLE_INGOT =
ITEMS.register("example_ingot", () -> new Item(new Item.Properties()));
// Предмет с поведением и tooltip
public static final RegistryObject DEBUG_WAND =
ITEMS.register("debug_wand", () -> new Item(new Item.Properties().stacksTo(1).rarity(Rarity.UNCOMMON)) {
@Override
public void appendHoverText(ItemStack stack, Level level, List tooltip, TooltipFlag flag) {
tooltip.add(Component.literal("Тестовый предмет для отладки действий."));
}
});
public static void register(IEventBus bus) {
ITEMS.register(bus);
}
}
6.2.3. Блоки + BlockItem (Forge — практичный паттерн)
Критически важно: блок и его “предмет-представление” (BlockItem) — разные реестры.
package com.example.examplemod;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.material.MapColor;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
import java.util.function.Supplier;
public final class ModBlocks {
private ModBlocks() {}
public static final DeferredRegister BLOCKS =
DeferredRegister.create(ForgeRegistries.BLOCKS, ExampleMod.MODID);
public static final RegistryObject EXAMPLE_BLOCK =
BLOCKS.register("example_block", () -> new Block(
BlockBehaviour.Properties.of()
.mapColor(MapColor.STONE)
.strength(2.0f, 6.0f)
.sound(SoundType.STONE)
));
// Утилита: регистрируем блок и BlockItem “в одном месте”
private static RegistryObject registerBlock(
String name,
Supplier blockSupplier
) {
RegistryObject block = BLOCKS.register(name, blockSupplier);
ModItems.ITEMS.register(name, () -> new BlockItem(block.get(), new Item.Properties()));
return block;
}
// Альтернатива: используйте registerBlock(...) вместо ручной пары
public static final RegistryObject EXAMPLE_BLOCK_2 =
registerBlock("example_block_2", () -> new Block(BlockBehaviour.Properties.of().strength(1.5f)));
public static void register(IEventBus bus) {
BLOCKS.register(bus);
}
}
6.3. Fabric: регистрация через Registry (база)
6.3.1. Main-класс (Fabric)
package com.example.examplemod;
import net.fabricmc.api.ModInitializer;
public class ExampleMod implements ModInitializer {
public static final String MODID = "examplemod";
@Override
public void onInitialize() {
ModItems.register();
ModBlocks.register();
ModNetworking.registerC2S(); // серверные пакеты
}
}
6.3.2. Предметы (Fabric)
package com.example.examplemod;
import net.minecraft.item.Item;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.util.Identifier;
public final class ModItems {
private ModItems() {}
public static final Item EXAMPLE_INGOT = new Item(new Item.Settings());
public static void register() {
Registry.register(Registries.ITEM, new Identifier(ExampleMod.MODID, "example_ingot"), EXAMPLE_INGOT);
}
}
6.3.3. Блоки + BlockItem (Fabric)
package com.example.examplemod;
import net.minecraft.block.Block;
import net.minecraft.block.AbstractBlock;
import net.minecraft.block.Blocks;
import net.minecraft.item.BlockItem;
import net.minecraft.item.Item;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.util.Identifier;
public final class ModBlocks {
private ModBlocks() {}
public static final Block EXAMPLE_BLOCK = new Block(
AbstractBlock.Settings.copy(Blocks.STONE).strength(2.0f, 6.0f)
);
public static void register() {
Registry.register(Registries.BLOCK, new Identifier(ExampleMod.MODID, "example_block"), EXAMPLE_BLOCK);
Registry.register(Registries.ITEM, new Identifier(ExampleMod.MODID, "example_block"),
new BlockItem(EXAMPLE_BLOCK, new Item.Settings()));
}
}
7. Подход Fabric: скелет проекта и регистрация
7.1. Точка входа (Fabric)
package com.example.examplemod;
import net.fabricmc.api.ModInitializer;
public class ExampleMod implements ModInitializer {
public static final String MODID = "examplemod";
@Override
public void onInitialize() {
ModItems.register();
ModBlocks.register();
}
}
7.2. Регистрация предметов (Fabric)
package com.example.examplemod;
import net.minecraft.item.Item;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.util.Identifier;
public final class ModItems {
private ModItems() {}
public static final Item EXAMPLE_INGOT = new Item(new Item.Settings());
public static void register() {
Registry.register(Registries.ITEM, new Identifier(ExampleMod.MODID, "example_ingot"), EXAMPLE_INGOT);
}
}
7.3. Регистрация блока и BlockItem (Fabric)
package com.example.examplemod;
import net.minecraft.block.Block;
import net.minecraft.block.AbstractBlock;
import net.minecraft.block.Blocks;
import net.minecraft.item.BlockItem;
import net.minecraft.item.Item;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.util.Identifier;
public final class ModBlocks {
private ModBlocks() {}
public static final Block EXAMPLE_BLOCK = new Block(
AbstractBlock.Settings.copy(Blocks.STONE).strength(2.0f, 6.0f)
);
public static void register() {
Registry.register(Registries.BLOCK, new Identifier(ExampleMod.MODID, "example_block"), EXAMPLE_BLOCK);
Registry.register(Registries.ITEM, new Identifier(ExampleMod.MODID, "example_block"),
new BlockItem(EXAMPLE_BLOCK, new Item.Settings()));
}
}
8. Практика 1: предмет глубже (поведение, взаимодействие, кулдаун, NBT)
8.1. Forge: предмет, который при ПКМ пишет в чат и ставит кулдаун
package com.example.examplemod.item;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
public class PingItem extends Item {
public PingItem(Properties props) {
super(props);
}
@Override
public InteractionResultHolder use(Level level, Player player, InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand);
if (!level.isClientSide) {
player.sendSystemMessage(Component.literal("PingItem: сервер получил действие."));
player.getCooldowns().addCooldown(this, 40); // 2 секунды при 20 tps
}
return InteractionResultHolder.sidedSuccess(stack, level.isClientSide);
}
}
Регистрация (Forge):
// внутри ModItems
public static final RegistryObject PING_ITEM =
ITEMS.register("ping_item", () -> new PingItem(new Item.Properties().stacksTo(1)));
8.2. Fabric: предмет с NBT-счётчиком кликов
package com.example.examplemod.item;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.text.Text;
import net.minecraft.util.Hand;
import net.minecraft.util.TypedActionResult;
import net.minecraft.world.World;
import net.minecraft.entity.player.PlayerEntity;
public class CounterItem extends Item {
private static final String KEY = "clicks";
public CounterItem(Settings settings) {
super(settings);
}
@Override
public TypedActionResult use(World world, PlayerEntity user, Hand hand) {
ItemStack stack = user.getStackInHand(hand);
if (!world.isClient) {
NbtCompound nbt = stack.getOrCreateNbt();
int clicks = nbt.getInt(KEY) + 1;
nbt.putInt(KEY, clicks);
user.sendMessage(Text.literal("Кликов: " + clicks), false);
}
return TypedActionResult.success(stack, world.isClient);
}
}
Регистрация (Fabric):
public static final Item COUNTER_ITEM = new CounterItem(new Item.Settings().maxCount(1));
// Registry.register(..., "counter_item", COUNTER_ITEM);
9. Практика: блок глубже (взаимодействие и серверная логика)
9.1. Forge: блок, который при ПКМ меняет состояние (BooleanProperty)
package com.example.examplemod.block;
import net.minecraft.core.BlockPos;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.network.chat.Component;
public class ToggleBlock extends Block {
public static final BooleanProperty ENABLED = BooleanProperty.create("enabled");
public ToggleBlock(Properties props) {
super(props);
this.registerDefaultState(this.stateDefinition.any().setValue(ENABLED, false));
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder builder) {
builder.add(ENABLED);
}
@Override
public InteractionResult use(BlockState state, Level level, BlockPos pos,
Player player, InteractionHand hand, BlockHitResult hit) {
if (!level.isClientSide) {
boolean current = state.getValue(ENABLED);
level.setBlock(pos, state.setValue(ENABLED, !current), 3);
player.sendSystemMessage(Component.literal("ToggleBlock: enabled=" + (!current)));
}
return InteractionResult.sidedSuccess(level.isClientSide);
}
}
Регистрация:
public static final RegistryObject TOGGLE_BLOCK =
ModBlocks.BLOCKS.register("toggle_block", () -> new ToggleBlock(
BlockBehaviour.Properties.of().strength(2.0f)
));
// BlockItem регистрируйте в Items, как в паттерне registerBlock(...)
Ресурсы для blockstate с variants по enabled:
// assets/examplemod/blockstates/toggle_block.json
{
"variants": {
"enabled=false": { "model": "examplemod:block/toggle_block_off" },
"enabled=true": { "model": "examplemod:block/toggle_block_on" }
}
}
9.2. Fabric: блок с onUse и проверкой server-side
package com.example.examplemod.block;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.text.Text;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand;
import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.world.World;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.math.BlockPos;
public class PingBlock extends Block {
public PingBlock(Settings settings) {
super(settings);
}
@Override
public ActionResult onUse(BlockState state, World world, BlockPos pos,
PlayerEntity player, Hand hand, BlockHitResult hit) {
if (!world.isClient) {
player.sendMessage(Text.literal("PingBlock: сервер обработал onUse."), false);
}
return ActionResult.SUCCESS;
}
}
10. Практика: рецепты, лут, теги (плюс DataGen)
10.1. “Ручные” JSON (универсально)
Рецепт shaped
{
"type": "minecraft:crafting_shaped",
"pattern": [
"II",
"II"
],
"key": {
"I": { "item": "examplemod:example_ingot" }
},
"result": {
"item": "examplemod:example_block",
"count": 1
}
}
Лут блока (дроп самого себя)
{
"type": "minecraft:block",
"pools": [
{
"rolls": 1,
"entries": [
{ "type": "minecraft:item", "name": "examplemod:example_block" }
],
"conditions": [
{ "condition": "minecraft:survives_explosion" }
]
}
]
}
Тег предметов (собственный)
{
"replace": false,
"values": [
"examplemod:example_ingot"
]
}
10.2. Forge DataGen: рецепты (пример провайдера)
Преимущество DataGen: меньше ручной рутины и меньше ошибок путей.
package com.example.examplemod.datagen;
import com.example.examplemod.ModItems;
import com.example.examplemod.ModBlocks;
import net.minecraft.data.PackOutput;
import net.minecraft.data.recipes.RecipeOutput;
import net.minecraft.data.recipes.ShapedRecipeBuilder;
import net.minecraft.data.recipes.RecipeProvider;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.ItemLike;
public class ModRecipeProvider extends RecipeProvider {
public ModRecipeProvider(PackOutput output) {
super(output);
}
@Override
protected void buildRecipes(RecipeOutput output) {
shaped2x2(output, ModBlocks.EXAMPLE_BLOCK.get(), ModItems.EXAMPLE_INGOT.get());
}
private void shaped2x2(RecipeOutput out, ItemLike result, ItemLike ingredient) {
ShapedRecipeBuilder.shaped(result, 1)
.pattern("II")
.pattern("II")
.define('I', ingredient)
.unlockedBy("has_ingredient", has(ingredient))
.save(out);
}
}
10.3. Fabric: генерация data обычно решается отдельным модулем datagen
В Fabric распространён подход “отдельный entrypoint для datagen”. Концептуально это похоже: провайдеры рецептов/лутов/тегов генерируют JSON в src/main/generated, который затем подключается как ресурс. Реализация зависит от конкретного шаблона проекта.
11. Практика 4: генерация руды или структуры (концептуально)
Worldgen сильно менялся между ветками Minecraft. Поэтому корректный путь такой:
-
определить целевую версию;
-
использовать её модель worldgen (placed features / configured features и т. п.);
-
вынести параметры (частота, высоты, биомы) в data-ресурсы, где это возможно;
-
тестировать на отдельном мире с фиксированным seed.
Плюсы worldgen через data-ресурсы:
-
часть параметров можно менять без перекомпиляции;
-
проще балансировать.
Минусы:
-
легко “сломать” генерацию неправильными диапазонами или конфликтующими правилами;
-
для сложных структур часто потребуется код и аккуратная интеграция с биомами/шумами.
12. Практика 5: простая сущность (общий подход)
Создание моба включает:
-
регистрацию типа сущности;
-
атрибуты (здоровье, скорость и т. п.);
-
поведение (AI goals);
-
клиентский рендер и модель (для визуала).
Плюсы:
-
расширение геймплея и контента;
-
можно строить новые механики (дроп, взаимодействия, квесты).
Минусы:
-
повышается риск проблем с производительностью и сетевой синхронизацией;
-
требует чёткого разделения client/server, иначе мод будет падать на выделенном сервере.
13. GUI и взаимодействие с игроком
GUI в модах — это почти всегда:
-
серверная логика контейнера/меню (проверки, инвентарь, синхронизация),
-
клиентский экран (отрисовка, ввод),
-
сетевые сообщения, если требуется интерактивность сверх стандартных операций.
Плюсы:
-
можно делать “станки”, интерфейсы, терминалы, конфиги в игре.
Минусы:
-
типовая ошибка — выполнять серверную логику на клиенте;
-
типовая ошибка — забыть синхронизацию данных, из-за чего возникают дюпы/рассинхрон.
14. Сеть и мультиплеер: пакеты C2S и правило “сервер решает”
Ниже минимальный практический пример: клиент нажимает “действие”, отправляет пакет на сервер, сервер выполняет логику и отвечает (или просто подтверждает).
14.1. Forge: SimpleChannel + C2S пакет
14.1.1. Инициализация канала
package com.example.examplemod.network;
import com.example.examplemod.ExampleMod;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.network.NetworkRegistry;
import net.minecraftforge.network.simple.SimpleChannel;
public final class ModNetworking {
private ModNetworking() {}
private static final String PROTOCOL = "1";
public static final SimpleChannel CHANNEL = NetworkRegistry.newSimpleChannel(
new ResourceLocation(ExampleMod.MODID, "main"),
() -> PROTOCOL,
PROTOCOL::equals,
PROTOCOL::equals
);
private static int id = 0;
public static void init() {
CHANNEL.registerMessage(id++, ExampleC2SPacket.class,
ExampleC2SPacket::encode,
ExampleC2SPacket::decode,
ExampleC2SPacket::handle
);
}
}
14.1.2. Пакет C2S
package com.example.examplemod.network;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.network.chat.Component;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
public class ExampleC2SPacket {
private final int value;
public ExampleC2SPacket(int value) {
this.value = value;
}
public static void encode(ExampleC2SPacket msg, FriendlyByteBuf buf) {
buf.writeInt(msg.value);
}
public static ExampleC2SPacket decode(FriendlyByteBuf buf) {
return new ExampleC2SPacket(buf.readInt());
}
public static void handle(ExampleC2SPacket msg, Supplier ctxSupplier) {
NetworkEvent.Context ctx = ctxSupplier.get();
ctx.enqueueWork(() -> {
ServerPlayer player = ctx.getSender();
if (player == null) return;
// Серверная логика
player.sendSystemMessage(Component.literal("C2S пакет получен. value=" + msg.value));
});
ctx.setPacketHandled(true);
}
}
14.1.3. Отправка пакета с клиента
Например, из предмета при ПКМ:
import com.example.examplemod.network.ModNetworking;
import com.example.examplemod.network.ExampleC2SPacket;
// ...
if (level.isClientSide) {
ModNetworking.CHANNEL.sendToServer(new ExampleC2SPacket(123));
}
Плюсы:
-
корректная мультиплеерная модель;
-
легко расширять на дополнительные действия.
Минусы:
-
требуется дисциплина версий протокола;
-
нельзя выполнять “мировую” логику на клиенте.
14.2. Fabric: ServerPlayNetworking (C2S)
14.2.1. Идентификатор и регистрация при инициализации
package com.example.examplemod.network;
import com.example.examplemod.ExampleMod;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.text.Text;
public final class ModNetworking {
private ModNetworking() {}
public static final Identifier EXAMPLE_C2S = new Identifier(ExampleMod.MODID, "example_c2s");
public static void registerC2S() {
ServerPlayNetworking.registerGlobalReceiver(EXAMPLE_C2S, (server, player, handler, buf, responseSender) -> {
int value = buf.readInt();
server.execute(() -> {
player.sendMessage(Text.literal("C2S пакет получен. value=" + value), false);
// Серверная логика здесь
});
});
}
}
14.2.2. Отправка пакета с клиента
import com.example.examplemod.network.ModNetworking;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.util.Identifier;
import io.netty.buffer.Unpooled;
PacketByteBuf buf = new PacketByteBuf(Unpooled.buffer());
buf.writeInt(123);
ClientPlayNetworking.send(ModNetworking.EXAMPLE_C2S, buf);
15. Отладка и типовые ошибки (углубление): что ломается чаще всего и как чинить
15.1. “Фиолетово-чёрная” текстура / отсутствует модель
Почти всегда причина в одном из пунктов:
-
неправильный путь в
assets//textures/...; -
в JSON модель ссылается на
examplemod:item/..., а modid другой; -
имя файла не совпадает (регистр, подчёркивания).
Минимальная проверка:
-
сверить
modidв коде и в ресурсах; -
открыть JSON и убедиться, что строка текстуры соответствует пути.
15.2. Краш при старте из-за регистрации
Типовые причины:
-
один и тот же id зарегистрирован дважды;
-
обращение к
RegistryObject.get()слишком рано (Forge); -
клиентские классы загружаются на сервере (Screen/Renderer в common).
Практика (Forge):
-
держать регистрации строго через DeferredRegister;
-
не дергать
.get()в статических инициализациях, где порядок не гарантирован.
15.3. “Работает в одиночке, падает на dedicated server”
Причина: на сервере нет клиентских классов. Если common-код импортирует их — сервер падает при загрузке.
Как избегать:
-
рендеры, экраны, клиентские обработчики — только в client-модуле/клиентской инициализации;
-
серверные действия — строго на сервере;
-
сеть — мост между клиентом и сервером.
15.4. Быстрый диагностический минимум
-
читать stacktrace сверху вниз до первого класса вашего мода;
-
проверять
Caused by:— там обычно причина; -
отделять “первичный” NPE/IllegalState от последствий.
16. Сборка, упаковка и публикация
16.1. Сборка
После разработки:
-
собираете JAR через Gradle;
-
проверяете на “чистой” сборке (без dev-среды);
-
тестируете минимум в двух режимах: singleplayer и dedicated server (если мод не чисто клиентский).
16.2. Версионирование и совместимость
Рекомендуемая дисциплина:
-
mod version:major.minor.patch; -
фиксация совместимости: версия Minecraft и лоадера;
-
список изменений (changelog) на каждую версию.
16.3. Публикация
До публикации стоит подготовить:
-
краткое описание и список функций;
-
требования (версия Minecraft, лоадер, зависимости);
-
инструкции по установке;
-
список известных проблем и ограничений;
-
лицензию (особенно если планируется open-source).
Плюсы такой подготовки:
-
меньше вопросов от пользователей;
-
быстрее воспроизводятся баги.
Минусы:
-
требует времени и дисциплины релизов.
17. Чек-лист перед релизом
-
Сборка проходит без ошибок в чистой среде.
-
Нет отсутствующих ресурсов (модели/текстуры/локализация).
-
Тест одиночной игры: новые предметы/блоки работают, рецепты и дроп корректны.
-
Тест сервера (если мод не клиентский): старт без краша, взаимодействия корректны.
-
Нет client-only импорта в common-коде.
-
Нет конфликтов id и повторной регистрации.
-
Настроены теги/рецепты/лут так, чтобы мод был устойчив к отсутствию “соседних” модов.
-
Документация: требования, установка, версии.
18. FAQ
Можно ли сделать мод без Java
Полноценный мод — нет, требуется код (обычно Java). Но часть задач закрывается datapack/resourcepack без программирования.
Что проще: Forge или Fabric
Для “первого мода” часто проще тот стек, для которого у вас лучше шаблон и меньше зависимостей. В среднем:
-
Forge проще “по шаблонам” и типовым решениям в крупных модах,
-
Fabric проще “по минимализму”, но иногда требует больше понимания низкого уровня.
Почему мод ломается после обновления Minecraft
Потому что меняются внутренние классы, регистрационные механики, структуры data-ресурсов и API лоадеров. Нужно закладывать время на сопровождение.
Какой минимальный мод стоит сделать первым
Практичный “первый мод”:
-
1 предмет,
-
1 блок,
-
1 рецепт,
-
корректные модели/текстуры/lang,
-
проверка на клиенте и сервере (если мод не client-only).