Приветствую моддер, я не нашел в интернете подробного гайда по данной теме, поэтому эта статья - способ обобщить мой опыт и поделиться им с другими. В основном статья предназначена для начинающих, но будет не лишним и для тех кто хочет напомнить себе работу со структурами при моддинге. Позже я буду делать её более подробной, либо писать дополнения к этой статье.

  • Стек: Minecraft 1.20.1, Forge 47.0.+, Litematica, IntelliJ IDEA.

Подготовка

Прежде чем переходить к написанию кода начнем с подготовки самого строения. И для этого есть несколько методов:

  • Метод 1: Structure Block. Хорошо для начала.

    • Структурный блок подходит для небольших структур, всё из-за главной проблемы: Ограничения по размеру сохранения до 64x256x64 блоков, где первое число - это ширина (ось X), второе - высота (ось Y), и третье - глубина (ось Z).

  • **Метод 2: Litematica ** ( на котором мы остановимся). Что это? Специальный инструмент для строительства, редактирования, создания и экспорта схематик в формате .litematic или ванильном nbt.

    • Рабочий процесс: 1. Устанавливаем Litematica (и MaLiLib) как обычный мод. 2. Строим в креативном мире нашу мега-структуру. 3. С помощью инструментов Litematica выделяем ее и сохраняем в формате .litematic. 4. Ключевой шаг: Экспорт, необходимо зайти в меню сохраненных схем, выбрать нужную и в нижней панели выбрать ванильный формат (nbt).

    • Финальный шаг: Копируем полученный .nbt файл из папки chematics в корне самой игры, вставляем в ресурсы мода по пути src/main/resources/data/modid/structures/my_test_example.nbt.

Начало

Для того, чтобы наша структура работала и генерировалась в мире, нам понадобится подключить Json файлы, настроить (об этом потом) их содержимое к нашему nbt файлу и зарегистрировать в java класс - так происходит работа с привычным и в какой-то степени классическим Jigsaw-подходом.

  • Json файлы необходимые в этом случае: template_pool.json с одним элементом (single_pool_element), который указывает на наш файл .nbt и structure.json и structure_set.json Последние два файла содержат информацию о структуре (биом для генерации, частота генерации, уникальный номер, расположение к рельефу и т.д). Jigsaw отличный выбор для небольших структур, но есть и минусы, особенно с большими структурами. В моем случае minecraft даже не смог сгенерировать строение. Анализ проблемы: Этот метод метод был разработан не для размещения одного гигантского объекта, а для композиции структуры из множества мелких и средних частей. Например, деревня состоит из 10-20 небольших .nbt (дома, дороги, колодцы), и система эффективно справляется с их последовательным размещением. Когда Jigsaw-система решает разместить элемент из пула (например, через minecraft:single_pool_element), она загружает весь указанный .nbt файл в память, чтобы вставить его в мир. Отсутствие внятных ошибок: Если вы ошиблись, игра в 99% случаев просто промолчит. В логах не будет ошибки "файл не найден". Команда /locate будет говорить, что такой структуры не существует, и вы будете часами искать опечатку. Это крайне болезненный процесс отладки.

Решение: Программная генерация через свой класс Structure

Вместо того чтобы полагаться на JSON-конфигурацию для загрузки .nbt, мы напишем свой собственный Java-класс, который будет управлять процессом генерации. Это дает нам полный контроль.
Шаг 1: Создаем свой класс Structure

  • Создайте файл MyGiantStructure.java, который наследуется от net.minecraft.world.level.levelgen.structure.Structure.

public class MyGiantStructure extends Structure {

    // Кодек для сериализации/десериализации нашей структуры
    public static final Codec<MyGiantStructure> CODEC = simpleCodec(MyGiantStructure::new);

    public MyGiantStructure(StructureSettings settings) {
        super(settings);
    }

    // Это сердце нашего метода!
    @Override
    public Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
        // Логика поиска подходящего места для спавна.
        // Для простоты можно просто выбрать центр чанка.
        BlockPos centerOfChunk = context.chunkPos().getMiddleBlockPosition(0);

        // Возвращаем точку генерации
        return Optional.of(new GenerationStub(centerOfChunk, (builder) -> {
            // Здесь мы будем вызывать саму генерацию
            this.generate(builder, context);
        }));
    }
    
    // Метод, который будет размещать нашу структуру в мире
    private void generate(StructurePiecesBuilder builder, GenerationContext context) {
        BlockPos pos = builder.getCenter(); // Получаем позицию из GenerationStub
        ResourceLocation location = new ResourceLocation(MyMod.MODID, "my_giant_castle");
        
        // Добавляем "кусок" нашей структуры. Так как она одна, кусок будет один.
        builder.addPiece(new MyGiantStructurePiece(context.structureTemplateManager(), location, pos));
    }

    @Override
    public StructureType<?> type() {
        return ModStructureTypes.MY_GIANT_STRUCTURE.get(); // Ссылка на наш зарегистрированный тип
    }
}

Шаг 2: Создаем класс "куска" структуры (StructurePiece).

  • Создайте MyGiantStructurePiece.java, который наследуется от TemplateStructurePiece. Этот класс будет отвечать за реальное размещение блоков из .nbt.

  • Логика здесь довольно стандартна, она просто загружает .nbt и размещает его.

public class MyGiantStructurePiece extends TemplateStructurePiece {
    public MyGiantStructurePiece(StructureTemplateManager manager, ResourceLocation location, BlockPos pos) {
        super(ModStructurePieceTypes.MY_GIANT_PIECE.get(), 0, manager, location, location.toString(), new StructurePlaceSettings(), pos);
    }

    // Конструктор для загрузки из NBT
    public MyGiantStructurePiece(ServerLevel level, CompoundTag tag) {
        super(ModStructurePieceTypes.MY_GIANT_PIECE.get(), tag, level.getServer().getStructureManager(), (location) -> new StructurePlaceSettings());
    }

    @Override
    protected void handleDataMarker(String function, BlockPos pos, ServerLevelAccessor level, RandomSource random, BoundingBox sbox) {
        // Можно оставить пустым, если у вас нет дата-блоков
    }
}

Шаг 3: Регистрация всего этого в Forge.

  • Покажите, как теперь выглядит регистрация StructureType (он ссылается на кодек нашего нового класса).

  • Добавьте регистрацию StructurePieceType.

// В классе ModStructureTypes.java
public static final RegistryObject<StructureType<MyGiantStructure>> MY_GIANT_STRUCTURE =
        STRUCTURE_TYPES.register("my_giant_structure", () -> () -> MyGiantStructure.CODEC);

// В отдельном классе ModStructurePieceTypes.java
public static final DeferredRegister<StructurePieceType> PIECE_TYPES = ...
public static final RegistryObject<StructurePieceType> MY_GIANT_PIECE = 
        PIECE_TYPES.register("my_giant_piece", () -> MyGiantStructurePiece::new);

Конфигурация в JSON

Теперь нам хватит двух файлов. template_pool.json - удаляем, он больше не нужен.
structure.json: Становится тривиальным. Его единственная задача — указать на наш зарегистрированный тип структуры и задать биомы/категорию спавна.

  "type": "mymod:my_giant_structure",
  "biomes": "#minecraft:is_overworld",
  "step": "surface_structures",
  "spawn_overrides": {}
}

structure_set.json: Остается таким же, он все еще отвечает за частоту и расстояние между структурами.

Финал

Основная настройка структуры подходит к концу, и чтобы подытожить и помочь я продемонстрирую как выглядят каркасы java файлов.
*MyGiantStructure.java. Этот класс решает, где разместить структуру, и инициирует процесс строительства.

// src/main/java/com/yourname/mymod/world/structure/MyGiantStructure.java

package com.yourname.mymod.world.structure;

import com.mojang.serialization.Codec;
import com.yourname.mymod.MyMod; // Замените на ваш главный класс мода
import com.yourname.mymod.world.ModStructureTypes; // Замените на ваш класс регистрации типов структур
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureType;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePiecesBuilder;

import java.util.Optional;

/**
 * Главный класс нашей структуры. Он выступает в роли "архитектора".
 * Его задача - найти подходящее место для генерации и дать команду на начало "строительства".
 */
public class MyGiantStructure extends Structure {

    /**
     * Codec - это механизм сериализации/десериализации от Mojang.
     * Он нужен, чтобы Minecraft мог сохранять и загружать информацию о нашей структуре.
     * simpleCodec использует конструктор класса для создания экземпляра.
     */
    public static final Codec<MyGiantStructure> CODEC = simpleCodec(MyGiantStructure::new);

    /**
     * Конструктор. Принимает настройки (биомы, шаг генерации и т.д.),
     * которые обычно загружаются из structure.json.
     */
    public MyGiantStructure(StructureSettings settings) {
        super(settings);
    }

    /**
     * Это сердце нашего класса. Метод вызывается для каждого чанка, чтобы определить,
     * можно ли здесь начать генерацию нашей структуры.
     * @return Optional.of(...) если место подходит, Optional.empty() если нет.
     */
    @Override
    public Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
        // Находим подходящую точку для старта. Для простоты возьмем центр чанка.
        // Вы можете добавить сюда сложную логику, например, проверку высоты ландшафта.
        BlockPos startPos = new BlockPos(context.chunkPos().getMinBlockX(), 90, context.chunkPos().getMinBlockZ());

        // Возвращаем "заглушку" для генерации. Это "обещание" построить структуру.
        // Сама генерация происходит в лямбда-функции.
        return Optional.of(new GenerationStub(startPos, (piecesBuilder) -> {
            this.generatePieces(piecesBuilder, context, startPos);
        }));
    }

    /**
     * Этот метод создает и добавляет "куски" (pieces) нашей структуры в мир.
     * В нашем случае кусок всего один.
     */
    private void generatePieces(StructurePiecesBuilder piecesBuilder, GenerationContext context, BlockPos startPos) {
        // Указываем путь к нашему .nbt файлу.
        // Он должен лежать в `src/main/resources/data/mymod/structures/my_giant_castle.nbt`
        ResourceLocation location = new ResourceLocation(MyMod.MODID, "my_giant_castle");

        // Создаем и добавляем единственный "кусок" нашей структуры, передавая ему все необходимые данные.
        piecesBuilder.addPiece(new MyGiantStructurePiece(
                context.structureTemplateManager(),
                location,
                startPos
        ));
    }

    /**
     * Возвращает зарегистрированный тип этой структуры.
     * Это нужно, чтобы Minecraft мог идентифицировать её.
     */
    @Override
    public StructureType<?> type() {
        // ModStructureTypes.MY_GIANT_STRUCTURE - это RegistryObject, который вы создадите в отдельном классе.
        return ModStructureTypes.MY_GIANT_STRUCTURE.get();
    }
}

*MyGiantStructurePiece.java. Этот класс — "строительная бригада". Он берет конкретный .nbt файл и размещает его блоки в мире.

// src/main/java/com/yourname/mymod/world/structure/MyGiantStructurePiece.java

package com.yourname.mymod.world.structure;

import com.yourname.mymod.world.ModStructurePieceTypes; // Замените на ваш класс регистрации типов кусков
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ServerLevelAccessor;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.TemplateStructurePiece;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;

/**
 * Класс-"кусок" нашей структуры. Он отвечает за фактическое размещение блоков из .nbt файла.
 * Наследуется от TemplateStructurePiece, который содержит всю логику для работы с шаблонами.
 */
public class MyGiantStructurePiece extends TemplateStructurePiece {

    /**
     * Конструктор, который мы вызываем при первоначальной генерации мира.
     * @param templateManager Менеджер для загрузки .nbt файлов.
     * @param location Путь к нашему .nbt файлу.
     * @param pos Позиция, где будет размещена структура.
     */
    public MyGiantStructurePiece(StructureTemplateManager templateManager, ResourceLocation location, BlockPos pos) {
        // Вызываем конструктор родительского класса, передавая все необходимые параметры.
        super(
                ModStructurePieceTypes.MY_GIANT_PIECE.get(), // Зарегистрированный тип этого "куска".
                0, // genDepth, глубина генерации (для Jigsaw).
                templateManager,
                location, // ResourceLocation нашего .nbt
                location.toString(), // Имя шаблона, можно использовать то же самое.
                new StructurePlaceSettings(), // Настройки размещения (поворот, зеркалирование и т.д.).
                pos // Позиция.
        );
    }

    /**
     * Второй конструктор. Он необходим для загрузки структуры из сохраненного мира.
     * Minecraft не генерирует структуры заново при загрузке, а считывает их из NBT-данных чанка.
     * @param serverLevel Мир
     * @param tag NBT-данные этого "куска"
     */
    public MyGiantStructurePiece(net.minecraft.server.level.ServerLevel serverLevel, CompoundTag tag) {
        super(ModStructurePieceTypes.MY_GIANT_PIECE.get(), tag, serverLevel.getServer().getStructureManager(), (location) -> new StructurePlaceSettings());
    }


    /**
     * Этот метод вызывается для обработки специальных "блоков данных" (Data Structure Blocks) в вашем .nbt.
     * Они позволяют выполнять команды или спавнить сущностей.
     * Если вы их не используете, можно оставить этот метод пустым.
     */
    @Override
    protected void handleDataMarker(String function, BlockPos pos, ServerLevelAccessor level, RandomSource random, BoundingBox sbox) {
        // Пример:
        // if (function.equals("spawn_zombie")) {
        //     level.addFreshEntity(new Zombie(level.getLevel()));
        // }
    }
}

Не забудьте добавить "регистрацию" в главный файл mods.toml:

  • Если бы у вас был замок и, скажем, маленькая хижина (small_hut), ваш mods.toml выглядел бы так:

# ... (остальная часть файла)

[[features]]
type = "minecraft:structure"
id = "mymod:my_giant_castle"

[[features]]
type = "minecraft:structure"
id = "mymod:small_hut"

Без этого блока Forge при запуске просто не знает, что ему нужно заглянуть в папку data/mymod/worldgen/structure и поискать там файлы для загрузки. Он проигнорирует их.

Заключение

Путь от идеи до первого сгенерированного замка оказался куда длиннее и извилистее, чем я ожидал. Сначала я уперся в стену производительности стандартного Jigsaw-метода. Потом, написав, как мне казалось, идеальный код, я часами искал проблему, которая крылась не в сложной логике Java, а в одной-единственной забытой строчке в mods.toml.
Этот опыт научил меня двум вещам. Во-первых, для нестандартных задач нужны нестандартные подходы. Программная генерация через собственный класс Structure — это именно такой подход, дающий гибкость и производительность там, где пасуют стандартные конфигурации. Во-вторых, самый важный навык моддера — это не столько умение писать код, сколько умение его отлаживать и методично искать причину, когда что-то идет не так.
Надеюсь, мой опыт сэкономит вам несколько часов (или даже дней) отладки и покажет, что даже самые большие и сложные задачи решаемы, если подходить к ним системно.

Комментарии (0)