Перейти к основному содержимому

Кастомный блок

Предварительное требование: ванильные палитры блоков

Кастомные блоки опираются на ванильные палитры блоков (vanilla_palette_*.nbt) в папке bin/ каталога сервера: по одному файлу на каждую поддерживаемую версию протокола клиента, плюс варианты vanilla_palette_netease_*.nbt для клиентов NetEase. Nukkit-MOT скачивает их сам при запуске — вручную ничего класть не нужно.

При каждом запуске сервер проверяет папку bin/ и докачивает только отсутствующие или повреждённые файлы с официального зеркала https://bin-data.nkmot.com/ (источник — MemoriesOfTime/Bin_Data). Уже имеющиеся файлы повторно не скачиваются. При первом запуске (и первом запуске после крупного обновления игры) файлы действительно скачиваются, из-за чего старт занимает на несколько секунд дольше.

После первой загрузки структура каталога выглядит так, и трогать её больше не нужно:

NukkitServer/
├── bin/ ← обслуживается сервером
│ ├── vanilla_palette_1001.nbt
│ ├── vanilla_palette_netease_860.nbt
│ └── ...
├── plugins/
├── worlds/
└── Nukkit-MOT-SNAPSHOT.jar
Офлайн-серверы и фиксированный набор палитр

Если у машины нет доступа в интернет или нужно закрепить конкретную версию палитр, подготовьте файлы вручную и отключите загрузчик:

  1. Скачайте palettes.zip из репозитория Bin_Data — или только нужные файлы .nbt.
  2. Распакуйте .nbt прямо в bin/ (без вложенной папки).
  3. Отключите автоматическую загрузку в nukkit-mot.yml:
nukkit-mot.yml
custom-block-settings:
auto-download-vanilla-palette: false

Неудачная загрузка не фатальна: сервер всё равно запустится, выведет предупреждение со ссылкой на зеркало и напечатает эти же шаги для ручной установки. Но если палитра для какой-то версии протокола так и останется отсутствовать, клиенты на этой версии не смогут корректно отображать ваши кастомные блоки — в логе появится No vanilla palette found for <version>.. Полное описание опции — в разделе custom-block-settings.

Для создания кастомного блока необходимо реализовать две основные составляющие:

  1. Успешно зарегистрировать блок внутри плагина в Nukkit-MOT.
  2. Определить текстуры блока в пакете ресурсов, который отправляется клиенту.

Далее на примере кастомного блока камня я продемонстрирую шаги по созданию кастомного блока.

Регистрация блока в плагине​

Процесс регистрации описан следующей диаграммой последовательности:

Создание нового класса блока​

Внутри плагина создайте новый класс MyCustomStone, унаследованный от CustomBlock:

custom/block/MyCustomStone.java
package cn.nukkitmot.exampleplugin.custom.block;

import cn.nukkit.block.custom.container.CustomBlock;

public class MyCustomStone extends CustomBlock {
public static final String IDENTIFIER = "myplugin:custom_stone";
public static final int BLOCK_ID = 10001; // Must be >= 10000

public MyCustomStone() {
super(IDENTIFIER, BLOCK_ID);
}

@Override
public String getName() {
return "Custom Stone";
}

@Override
public double getHardness() {
return 1.5;
}

@Override
public double getResistance() {
return 6.0;
}

@Override
public int getToolType() {
return ItemTool.TYPE_PICKAXE;
}
}

Обзор ключевых классов​

CustomBlock vs CustomBlockMeta​

Nukkit-MOT предоставляет два базовых класса для кастомных блоков:

  • CustomBlock — для простых блоков без кастомных свойств (например, камень, земля)
  • CustomBlockMeta — для блоков с кастомными свойствами/состояниями (например, редстоун-лампа со состоянием вкл/выкл)

Регистрация блока​

Зарегистрируйте блок в методе onEnable вашего плагина:

ExamplePlugin.java
import cn.nukkit.block.custom.CustomBlockManager;
import cn.nukkit.block.custom.CustomBlockDefinition;
import cn.nukkit.math.Vector3f;
import cn.nukkitmot.exampleplugin.custom.block.MyCustomStone;

public class ExamplePlugin extends PluginBase {
@Override
public void onEnable() {
CustomBlockManager.get().registerCustomBlock(
MyCustomStone.IDENTIFIER,
MyCustomStone.BLOCK_ID,
CustomBlockDefinition.builder(new MyCustomStone())
.name("Custom Stone")
.texture("custom_stone")
.breakTime(1.5)
.collisionBox(new Vector3f(-8, 0, -8), new Vector3f(16, 16, 16))
.selectionBox(new Vector3f(-8, 0, -8), new Vector3f(16, 16, 16))
.creativeGroup("itemGroup.name.stoneBrick")
.creativeCategory(CreativeItemCategory.CONSTRUCTION)
.build(),
MyCustomStone::new
);
}
}
Критически важно: порядок загрузки плагина

Ваш плагин должен быть настроен с load: STARTUP в plugin.yml:

plugin.yml
name: MyPlugin
main: cn.example.MyPlugin
version: 1.0.0
load: STARTUP

Это связано с тем, что кастомные блоки должны быть зарегистрированы до вызова CustomBlockManager.closeRegistry(). Последовательность запуска сервера:

  1. enablePlugins(STARTUP) → вызывает onEnable() для плагинов STARTUP
  2. CustomBlockManager.closeRegistry() → закрывает регистрацию, генерирует палитру
  3. enablePlugins(POSTWORLD) → вызывает onEnable() для плагинов POSTWORLD

Если ваш плагин использует значение по умолчанию load: POSTWORLD, к моменту вызова onEnable() реестр блоков уже будет закрыт, и регистрация завершится с ошибкой.

Другие замечания
  1. Идентификаторы кастомных блоков должны быть >= 10000 (CustomBlockManager.LOWEST_CUSTOM_BLOCK_ID)
  2. Идентификатор должен соответствовать формату namespace:block_name

Методы билдера CustomBlockDefinition​

Источник: cn.nukkit.block.custom.CustomBlockDefinition:

Основные свойства​

МетодОписание
name(String name)Задаёт отображаемое имя
texture(String texture)Задаёт идентификатор текстуры
breakTime(double second)Задаёт время добычи в секундах

Коллизия и бокс выделения​

// Full block collision
.collisionBox(new Vector3f(-8, 0, -8), new Vector3f(16, 16, 16))
.selectionBox(new Vector3f(-8, 0, -8), new Vector3f(16, 16, 16))

// Half slab collision
.collisionBox(new Vector3f(-8, 0, -8), new Vector3f(16, 8, 16))

Начало координат находится в центре нижней грани блока. Размер указывается в единицах 1/16 (пикселях).

Инвентарь творческого режима​

.creativeCategory(CreativeItemCategory.CONSTRUCTION)
.creativeGroup("itemGroup.name.stoneBrick")
// Or use combined method:
.creativeGroupAndCategory(CreativeItemGroup.STONE_BRICK, CreativeItemCategory.CONSTRUCTION)

Геометрическая модель​

// Use geometry identifier
.geometry("geometry.custom_model")

// Or use Geometry object with bone visibility
.geometry(new Geometry("geometry.custom_model")
.boneVisibility("bone1", true)
.boneVisibility("bone2", "query.block_property('myplugin:active') == 1"))

Трансформация​

import cn.nukkit.block.custom.container.data.Transformation;
import cn.nukkit.math.Vector3;

// Rotation must be multiples of 90 degrees
.transformation(new Transformation(
new Vector3(0, 0, 0), // translation
new Vector3(1, 1, 1), // scale
new Vector3(0, 90, 0) // rotation (90 degree increments)
))

// Simple rotation
.rotation(new Vector3f(0, 90, 0))

Материалы (текстуры для нескольких граней)​

import cn.nukkit.block.custom.container.data.Materials;

.materials(Materials.builder()
.up(Materials.RenderMethod.OPAQUE, "grass_top")
.down(Materials.RenderMethod.OPAQUE, "dirt")
.any(Materials.RenderMethod.OPAQUE, "grass_side")
.build())

Варианты RenderMethod:

  • OPAQUE — полностью непрозрачный (по умолчанию)
  • BLEND — прозрачность со смешиванием (как у стекла)
  • ALPHA_TEST — двоичная прозрачность (как у листвы)

Блоки со свойствами​

Для блоков, которым нужно хранить состояние (например, лампа, которая может быть включена или выключена), используйте CustomBlockMeta с BlockProperties.

Создание блока со свойствами​

custom/block/MyLamp.java
package cn.nukkitmot.exampleplugin.custom.block;

import cn.nukkit.block.custom.container.CustomBlockMeta;
import cn.nukkit.block.custom.properties.BlockProperties;
import cn.nukkit.block.custom.properties.BooleanBlockProperty;

public class MyLamp extends CustomBlockMeta {
public static final String IDENTIFIER = "myplugin:my_lamp";
public static final int BLOCK_ID = 10002;

// Define the property
public static final BooleanBlockProperty LIT =
new BooleanBlockProperty("myplugin:lit", true);

public static final BlockProperties PROPERTIES =
new BlockProperties(LIT);

public MyLamp() {
this(0);
}

public MyLamp(int meta) {
super(IDENTIFIER, BLOCK_ID, PROPERTIES, meta);
}

// Getter and setter for the property
public boolean isLit() {
return getBooleanValue(LIT.getName());
}

public void setLit(boolean lit) {
setBooleanValue(LIT.getName(), lit);
}

@Override
public int getLightLevel() {
return isLit() ? 15 : 0;
}
}

Регистрация блока со свойствами​

ExamplePlugin.java
CustomBlockManager.get().registerCustomBlock(
MyLamp.IDENTIFIER,
MyLamp.BLOCK_ID,
MyLamp.PROPERTIES,
CustomBlockDefinition.builder(new MyLamp())
.name("My Lamp")
.texture("my_lamp_off")
.breakTime(0.3)
// Use permutations for different states
.permutation(new Permutation(
Materials.builder().any(Materials.RenderMethod.OPAQUE, "my_lamp_on").build(),
"query.block_property('myplugin:lit') == 1"
))
.build(),
MyLamp::new // Factory that accepts meta parameter
);

Типы свойств​

BooleanBlockProperty​

// Simple on/off state
BooleanBlockProperty powered = new BooleanBlockProperty("myplugin:powered", true);

IntBlockProperty​

// Range of values (e.g., water level 0-7)
IntBlockProperty level = new IntBlockProperty("myplugin:level", true, 7, 0);

// With custom bit size
IntBlockProperty rotation = new IntBlockProperty("myplugin:rotation", true, 15, 0, 4);

EnumBlockProperty​

// Using string array
EnumBlockProperty<String> woodType = new EnumBlockProperty<>(
"myplugin:wood_type",
true,
new String[]{"oak", "spruce", "birch", "jungle"}
);

// Using Java enum
public enum FacingDirection { NORTH, SOUTH, EAST, WEST }
EnumBlockProperty<FacingDirection> facing = new EnumBlockProperty<>(
"myplugin:facing",
true,
FacingDirection.class
);

Перестановки (условный рендеринг)​

Перестановки позволяют использовать разный рендеринг в зависимости от свойств блока:

import cn.nukkit.block.custom.container.data.Permutation;

CustomBlockDefinition.builder(new MyBlock())
// Default state
.texture("my_block_default")
// When property is true, use different texture
.permutation(new Permutation(
Materials.builder().any(Materials.RenderMethod.OPAQUE, "my_block_active").build(),
"query.block_property('myplugin:active') == 1"
))
// Multiple permutations
.permutations(
new Permutation(geometry1, "query.block_property('myplugin:state') == 0"),
new Permutation(geometry2, "query.block_property('myplugin:state') == 1"),
new Permutation(geometry3, "query.block_property('myplugin:state') == 2")
)
.build()

Создание пакета ресурсов​

Структура каталогов пакета ресурсов​

📁Resource Pack
📄manifest.json
🖼️pack_icon.png
📁textures
📄terrain_texture.json
📁blocks
🖼️custom_stone.png
🖼️my_lamp_off.png
🖼️my_lamp_on.png

terrain_texture.json​

RP/textures/terrain_texture.json
{
"resource_pack_name": "myplugin",
"texture_name": "atlas.terrain",
"padding": 8,
"num_mip_levels": 4,
"texture_data": {
"custom_stone": {
"textures": "textures/blocks/custom_stone"
},
"my_lamp_off": {
"textures": "textures/blocks/my_lamp_off"
},
"my_lamp_on": {
"textures": "textures/blocks/my_lamp_on"
}
}
}
Текстуры блоков и предметов
  • Текстуры блоков используют terrain_texture.json и папку textures/blocks/
  • Текстуры предметов используют item_texture.json и папку textures/items/

Кастомные геометрические модели​

Если используется кастомная геометрия, добавьте файл модели:

📁Resource Pack
📁models
📁blocks
📄custom_model.geo.json
RP/models/blocks/custom_model.geo.json
{
"format_version": "1.16.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.custom_model",
"texture_width": 16,
"texture_height": 16
},
"bones": [
{
"name": "root",
"pivot": [0, 0, 0],
"cubes": [
{
"origin": [-8, 0, -8],
"size": [16, 16, 16],
"uv": [0, 0]
}
]
}
]
}
]
}

Дальнейшее изучение​

Встроенные пакеты ресурсов в плагине​

Как и в случае с кастомными предметами, вы можете встроить пакеты ресурсов в свой плагин. Создайте папку assets/resource_pack внутри каталога resources.

Показать структуру каталога resources

📁resources
📄plugin.yml
📁assets
📁resource_pack
📄manifest.json
🖼️pack_icon.png
📁textures
📄terrain_texture.json
📁blocks
🖼️custom_stone.png

Блок верстака​

Создайте кастомный верстак со своей собственной сеткой рецептов:

import cn.nukkit.block.custom.container.data.CraftingTable;

CustomBlockDefinition.builder(new MyCraftingTable())
.craftingTable(new CraftingTable(
"My Crafting Table", // Table name
Arrays.asList("crafting_table", "my_table") // Tags for recipes
))
.build()

Теги блока​

Добавьте теги к своему блоку для совместимости с ванильными механиками:

CustomBlockDefinition.builder(new MyBlock())
.blockTags("stone", "minecraft:mineable/pickaxe")
.build()

Кастомная конфигурация NBT​

Для расширенных настроек, не охватываемых методами билдера:

CustomBlockDefinition.builder(new MyBlock())
.texture("my_texture")
.customBuild(nbt -> {
// Add custom NBT data
nbt.getCompound("components")
.putCompound("minecraft:light_emission", new CompoundTag()
.putInt("emission", 15));
})