Руководства
Kotlin и Java
Для больших модов. Тот же API, что и в CraftScript, со статическими типами, полноценной IDE и любой библиотекой Java, плюс несколько частей, которые пока есть только здесь. В этом руководстве используется Kotlin; Java работает так же, и она показана в конце.
Что можно сделать
| Область | В API | В CraftScript |
|---|---|---|
| Предметы, блоки, рецепты | ContentBuilder: item, block, shaped, shapeless, smelting |
Да |
| Существа | ContentBuilder.creature: тело, текстура, характеристики, Behaviour, TargetRule, добыча, естественное появление, приручение, разведение, езда, собственные поведения |
Да |
| Звуки и частицы | ContentBuilder.sound, ContentBuilder.particle |
Да |
| Генерация мира | ContentBuilder: ore, tree, patch, structure (пулы, plan/build), biome, dimension, feature, terrain, biomes, carver, tag. Смотри Генерация мира и Генерация кодом |
Всё, кроме feature, tag, salt и aboveBottom |
| Интерфейс | Form и player.showForm; server.bossBar; player.setSidebar |
Да |
| События | Crafting.events.on<E> { }, с приоритетом и отменой |
Да |
| Эффекты, атрибуты, экипировка | addEffect, attribute, setAttribute, equipment, setEquipment, EquipmentSlot |
Yes |
| Клавиши | ContentBuilder.key, PlayerKeyEvent |
Yes |
| Команды | Crafting.commands.command(…): подкоманды, типизированные аргументы, права, псевдонимы |
Да |
| Мир и игроки | World, Block, Entity, Player, Inventory, Server |
Да |
| Задачи | Crafting.scheduler: runLater, runRepeating, runAsync, runSync |
Без фоновой работы |
| Настройки в файлах | Типизированный Config в TOML, может синхронизироваться с клиентами |
Да (config) |
| Сеть | Crafting.network.channel(…), с собственными кодеками |
Да (listen, send) |
| Сохранённые данные | player.data["rubies.visits"] = n, data.getInt(…); также у сущностей, блоков и миров |
Да |
| Библиотеки Java | Любые | Нет |
Того, чего в API ещё нет, нет ни в одном языке. Не обходи это вызовами классов Minecraft: мод потеряет гарантию, что продолжит работать, когда игра изменится.
Создай проект
crafting new rubies --kind kotlin
cd rubies && gradle buildAPI пока нет в публичном репозитории Maven. Установи его в локальный Maven из исходников Crafting и объяви как compileOnly: Crafting предоставляет его моду во время работы.
cd crafting && ./gradlew :crafting-api:publishToMavenLocalplugins { kotlin("jvm") version "2.4.21" }
repositories { mavenCentral(); mavenLocal() }
dependencies {
compileOnly("dev.crafting:crafting-api:0.1.0")
}
kotlin { jvmToolchain(25) } // Minecraft 26.x работает на Java 25crafting new создаёт этот файл и проект, уже разделённый по ответственности: <Name>Mod.kt, который только связывает части, content/ModContent.kt, events/Welcome.kt и commands/GemCommand.kt.
Структура
Пакеты по темам, входной класс, который только связывает части, и по файлу на каждую часть. Руководство Как организовать мод объясняет почему; вот полный пример мода:
rubies/
├── build.gradle.kts
├── settings.gradle.kts
└── src/main/
├── kotlin/com/example/rubies/
│ ├── RubiesMod.kt entry point: only wires the parts
│ ├── RubiesConfig.kt player settings
│ ├── content/
│ │ ├── Items.kt
│ │ ├── Blocks.kt
│ │ ├── Creatures.kt
│ │ ├── Sounds.kt
│ │ ├── Particles.kt
│ │ └── Recipes.kt
│ ├── storage/
│ │ └── Visits.kt saved data: the keys, in one place
│ ├── events/
│ │ ├── MiningEvents.kt
│ │ ├── CombatEvents.kt
│ │ └── WelcomeEvents.kt
│ ├── ui/
│ │ └── ShopForm.kt forms: what the player sees
│ └── commands/
│ ├── RubyCommand.kt
│ └── ShopCommand.kt
└── resources/
├── crafting.mod.json
└── assets/rubies/
├── sounds/ruby/chime.ogg
└── textures/
├── item/ruby.png
├── entity/ruby_golem.png
└── particle/ruby_spark_0.png, ruby_spark_1.pngsrc/main/kotlin/com/example/rubies/
Входной класс сам ничего не делает: создаёт общее и вызывает каждую часть.
package com.example.rubies
class RubiesMod : CraftingMod {
override fun onLoad(ctx: ModContext) {
val config = Crafting.configs.load(RubiesConfig())
val content = ContentBuilder(ctx.metadata.id, Crafting.registries)
Items.register(content)
Blocks.register(content)
Creatures.register(content)
Sounds.register(content)
Particles.register(content)
Recipes.register(content)
MiningEvents.register(config)
CombatEvents.register(config)
WelcomeEvents.register()
RubyCommand.register()
ShopCommand.register()
}
}Манифест
{
"id": "rubies",
"name": "Rubies",
"version": "1.0.0",
"kind": "mod",
"side": "both",
"entrypoints": ["com.example.rubies.RubiesMod"],
"depends": { "crafting": ">=0.1.0" }
}| Поле | Что это |
|---|---|
id |
2–64 символа: строчные буквы, цифры, _ или -, первой идёт буква. Это также пространство имён для твоих предметов и блоков. |
kind |
mod или plugin (только сервер, перезагружаемый; смотри ниже). |
side |
both, client или server. Клиентский мод молча игнорируется на выделенном сервере. |
entrypoints |
Классы, реализующие CraftingMod. |
depends, optional, incompatible |
Id → диапазон версий: >=1.2 <2, ^1.4, ~1.4.2, 1.20.x, [1.0,2.0). Если что-то не так, Crafting сообщает обо всех проблемах сразу, с советом. |
loadAfter, loadBefore |
Порядок загрузки без жёсткой зависимости. |
Жизненный цикл
interface CraftingMod {
fun onLoad(ctx: ModContext)
fun onReady(ctx: ModContext) {}
fun onShutdown(ctx: ModContext) {}
}onLoad: регистрируй контент, события и команды.- Crafting создаёт блоки и предметы в игре. С этого момента регистрация контента выбрасывает
RegistryFrozenException. onReady: все моды загружены;Crafting.runtime.isModLoaded("other_mod")говорит, есть ли другой мод.onShutdown: при выходе.
Серверные плагины
Плагин — это CraftingMod с "kind": "plugin" и "side": "server"; crafting new chatguard --kind plugin создаёт входной класс, events/ChatFilter.kt и commands/HelloCommand.kt. Он не добавляет блоков и предметов, поэтому игроки с обычным Minecraft могут заходить на сервер, где есть только плагины. Его можно включать, отключать и перезагружать без перезапуска: onLoad и onReady выполняются при включении, onShutdown — при отключении или перезагрузке.
crafting new chatguard --kind pluginpackage com.example.chatguard
class ChatGuardPlugin : CraftingMod {
override fun onLoad(ctx: ModContext) {
ChatFilter.register()
Crafting.commands.command("guard") {
permission = "chatguard.admin"
executes { it.reply("ChatGuard активен") }
}
}
override fun onShutdown(ctx: ModContext) = ctx.logger.info("ChatGuard отключён")
}package com.example.chatguard.events
object ChatFilter {
private val blocked = listOf("spam", "scam")
fun register() {
Crafting.events.on<PlayerChatEvent> { e ->
if (blocked.any { it in e.message.lowercase() }) {
e.cancel()
e.player.send("&cПожалуйста, будь вежлив")
}
}
}
}Всё, что регистрирует плагин, привязано к нему. /crafting reload chatguard вызывает onShutdown, удаляет его слушателей, команды и задачи, заново читает jar и вызывает onLoad. В режиме разработки это происходит автоматически, когда ты заменяешь jar.
| Мод | Плагин | |
|---|---|---|
| Папка | mods/ |
plugins/ |
| Сторона | Клиент и/или сервер | Только сервер |
| Новые блоки и предметы | Да | Нет |
| Перезагрузка без перезапуска | Нет | Да |
| Могут зайти клиенты без модов | Нет, если он добавляет контент | Да |
Java
Тот же API, та же структура. Из Java точка входа — Crafting.INSTANCE, ты передаёшь все параметры, у которых есть значение по умолчанию, а слушатель возвращает Unit.INSTANCE:
package com.example.rubies;
public final class RubiesMod implements CraftingMod {
@Override
public void onLoad(ModContext ctx) {
ContentBuilder content = new ContentBuilder(ctx.getMetadata().getId(), Crafting.INSTANCE.getRegistries());
Items.register(content);
WelcomeEvents.register();
}
}package com.example.rubies.events;
public final class WelcomeEvents {
public static void register() {
Crafting.INSTANCE.getEvents().subscribe(PlayerJoinEvent.class, EventPriority.NORMAL, false, null, e -> {
e.getPlayer().send("&aWelcome, " + e.getPlayer().getName() + "!");
return Unit.INSTANCE;
});
}
}Старые способы
Они всё ещё работают, но помечены как устаревшие, и у них есть замена:
| Раньше | Сейчас |
|---|---|
class X : CraftingPlugin() с onEnable/onDisable |
CraftingMod с "kind": "plugin" |
Методы с @Subscribe и events.register(obj) |
events.on<E> { } (Java: events.subscribe(E.class, …)) |
@Mod("id") на классе |
Класс в entrypoints манифеста |
EntityDamageEvent.source |
damageType (id, например minecraft:fall) |
world.setBlockState(x, y, z, "…[open=true]") |
world.setBlock(x, y, z, type, mapOf("open" to true)) |
Правила
- Ничего не импортируй из
net.minecraft. Именно это позволяет моду продолжать работать, когда игра меняется. - Регистрируй контент только в
onLoad. - Задача
runAsyncне трогает мир; возвращайся в игровой поток черезrunSync.