Документация

Kuroko 1.1.0-dev

JVM-язык для Minecraft-плагинов: аннотации вместо бойлерплейта, auto-import Paper API, null-safety и понятные ошибки. Всё ниже проверено на тест-корпусе компилятора.

новичок

Установка

Kuroko компилирует в JVM-байткод, поэтому единственное требование — установленная Java.

Требования

  • JDK 17 или новее (рекомендуется 21) — проверьте командой java -version
  • Gradle 8.x — wrapper поставляется вместе с проектами, отдельная установка не нужна

Создание проекта

терминал
kuroko init MyPlugin
cd MyPlugin

Команда создаст каркас: build.gradle.kts с плагином io.sylnora.kuroko, конфиг kuroko.toml и стартовый шаблон src/main/kuroko/MyPlugin.kr.

Сборка и тестовый сервер

терминал
./gradlew kurokoBuild   # .jar в build/libs/
./gradlew kurokoRun     # Paper в run/ с вашим плагином

Готовый .jar появляется в build/libs/. Задача kurokoRun скачает Paper, положит плагин в plugins/ и запустит сервер.

Пока kurokoc распространяется как jar из монорепозитория (фаза 5 закрывает fat-jar), команды pkr-установки работают в локальном режиме.
новичок

Первый плагин за 5 минут

Сделаем плагин, который приветствует игрока при входе и отвечает на команду /hello. Полный код — 14 значимых строк.

1. Пишем код

Откройте src/main/kuroko/HelloPlugin.kr и замените содержимое:

HelloPlugin.krkuroko

2. Собираем и запускаем

терминал
./gradlew kurokoBuild
./gradlew kurokoRun

Зайдите на сервер и проверьте: при входе появляется золотое приветствие, команда /hello отвечает зелёным сообщением.

3. Что произошло под капотом

  • @Plugin → класс наследует JavaPlugin, plugin.yml сгенерирован из аннотации
  • @On → класс зарегистрирован как Listener, метод получил @EventHandler
  • @Command → создан CommandExecutor, команда прописана в plugin.yml
  • msg("&6…") → цветовые коды переведены автоматически
  • Player, PlayerJoinEvent → резолвнуты через auto-import, ни одного use
Тот же функционал на Java занимает ~48–55 строк плюс отдельный plugin.yml. На Kotlin — около 30 строк. Полное сравнение — в разделе Kuroko против Java.
новичок

Структура проекта

MyPlugin/tree
MyPlugin/
├── build.gradle.kts        ← плагин io.sylnora.kuroko
├── kuroko.toml             ← конфиг языка и MC-платформы
├── settings.gradle.kts
└── src/
    ├── main/
    │   ├── kuroko/
    │   │   └── MyPlugin.kr
    │   └── resources/
    │       └── config.yml  ← опционально
    └── test/
        └── kuroko/
            └── MyPluginTest.kr
└── run/                    ← тестовый сервер (gitignored)

Каждая команда компилятора знает про эту структуру: kurokoc compile src/ собирает все .kr-файлы, kurokoc check src/ проверяет без генерации, kurokoc run Main.kr исполняет скрипт.

новичок

Переменные и типы

val — неизменяемая переменная, var — изменяемая. Тип выводится автоматически; явно указывайте его, когда нужно.

variables.krkuroko
val name = "Steve"          // String, выводится сам
var health = 20             // Int
var ratio: Float = 0.5      // явный тип

say("Игрок: $name, HP: $health")     // строковые шаблоны
say("Урон: ${health * 2}")

Числа с единицами измерения

Специально для Minecraft: единицы — часть языка, компилятор переводит их в тики и не даёт перепутать дистанцию со скоростью.

units.krkuroko
val speed   = 0.3 blocks_per_tick
val delay   = 100 ms
val reload  = 5 seconds
val range   = 10 blocks
ТипЛитералДля чего
Int20счётчики, количество
Double0.5координаты, урон в здоровье
Booleantrue / falseфлаги
String"текст"сообщения, имена, шаблоны $x
T?null nullable-значения — см. null-safety
новичок

Функции

Однострочные функции — без фигурных скобок и return. Умолчания параметров работают как в Kotlin. Точка входа скрипта — fun main(), класс-обёртка не нужна.

functions.krkuroko
fun main() {
    say(square(4))            // 16
    say(greet("Steve"))       // Привет, Steve!
    spawn(world)              // умолчания y = 64.0
}

fun square(x: Int) = x * x
fun greet(name: String) = "Привет, $name!"

fun spawn(world: World, x: Double = 0.0, y: Double = 64.0, z: Double = 0.0) {
    // ...
}

Расширяющие функции

Добавляйте методы к чужим классам — например, к Player из Paper API.

extensions.krkuroko
fun Player.heal(amount: Int) {
    health = min(health + amount, 20)
}

// вызов как обычный метод:
player.heal(10)

Лямбды

lambdas.krkuroko
val doubled = listOf(1, 2, 3).map { it * 2 }
val evens   = listOf(1, 2, 3, 4).filter { it % 2 == 0 }

every(1 second) onlyIf { players.size > 0 } {
    players.forEach { p -> p.msg("&7Тик") }
}
новичок

Классы

Классы выглядят как в Kotlin: первичный конструктор в заголовке, свойства — val/var. data class даёт equals, hashCode, toString и copy бесплатно. object — синглтон вместо static.

classes.krkuroko
class Player(val name: String, var health: Int = 20) {

    fun isAlive() = health > 0

    fun takeDamage(amount: Int) {
        health -= amount
        if (!isAlive()) say("$name погиб!")
    }
}

data class Item(val id: String, val name: String, val damage: Int)

object Config {
    val version = "1.0.0"
    val debug = false
}

class Boss(name: String) : Player(name, health = 200) {
    override fun takeDamage(amount: Int) {
        super.takeDamage(amount / 2)   // боссы получают половину урона
    }
}
Нет new — просто Item("sword", "Меч", 7). Нет public/private на каждом методе — умолчание public. Нет ;.
новичок

Управление потоком

if, цикл for … in по коллекциям и диапазонам, while и when вместо switch. when работает и как выражение.

flow.krkuroko
// диапазоны
for i in 1..10 {
    say(i)
}

// коллекции
for item in list {
    say(item)
}

// пока
while health > 0 {
    health--
}

// when — как switch, но умнее
when material {
    DIAMOND  -> say("Алмаз!")
    GOLD     -> say("Золото!")
    is Sword -> say("Меч!")
    else     -> say("Что-то другое")
}

// when как выражение
val price = when material {
    DIAMOND -> 100
    GOLD    -> 50
    else    -> 1
}
новичок

Коллекции

Списки, множества и словари создаются литералами — без listOf() и mapOf().

collections.krkuroko
val list = [1, 2, 3, 4, 5]        // список
val map  = {"key": "value", "hp": 20}   // словарь
val set  = {1, 2, 3}              // фигурные без пар — множество

// функциональный стиль
val names = players.map { it.name }.filter { it.startsWith("A") }

val top = Players.where { kills > 10 }.orderBy { kills }.limit(10).find()
ОперацияПримерЧто делает
maplist.map { it * 2 }преобразует каждый элемент
filterlist.filter { it > 2 }оставляет подходящие
forEachlist.forEach { say(it) }обходит с действием
where / orderBy / limittable-запросыцепочки запросов к таблицам
средний

Null-safety

Тип без ? не может быть null — это проверяется компилятором с кодом KR001. Сервисные вызовы — через ?., значение по умолчанию — через ?: (элвис).

null-safety.krkuroko
var player: Player? = null     // может отсутствовать

player?.sendMessage("Привет")  // безопасный вызов
val name = player?.name ?: "Неизвестный"   // элвис

// в командах это избавляет от проверок:
val world = server.getWorld(home.world)
    ?: return player.msg("&cМир не найден!")
Компилятор ловит присвоение null non-nullable типу ещё на сборке: KR001 с точной строкой и подсказкой. Это главная защита от NPE на живом сервере.
новичок

Плагин и жизненный цикл

Плагин — класс с аннотацией @Plugin, наследующий KPlugin. Блоки on load / on unload превращаются в onEnable / onDisable.

plugin.krkuroko
@Plugin(
    name = "HomePlugin",
    version = "1.0",
    description = "Система домов для игроков",
    authors = ["Sylnora"]
)
class HomePlugin : KPlugin() {

    on load {
        Homes.createTable()
        log("HomePlugin загружен. Дома готовы.")
    }

    on unload {
        log("HomePlugin выгружен.")
    }
}

Из аннотации компилятор собирает plugin.yml: name, version, main, api-version, authors и все команды с правами. Манифест не может разъехаться с кодом — он генерируется из него.

новичок

Команды

Команда — аннотированная функция. Права, кулдаун и usage — соседние аннотации. Аргументы приходят массивом, отправитель типизируется параметром.

commands.krkuroko
@Command("sethome", description = "Установить точку дома", usage = "/sethome [название]")
@Permission("homeplugin.sethome")
@Cooldown(3 seconds, message = "&cПодожди ещё {remaining} сек!")
fun cmdSetHome(player: Player, args: Array) {
    val name = args.getOrElse(0) { "home" }
    val loc = player.location

    Homes.set(player = player.uniqueId, name = name, world = loc.world.name,
              x = loc.x, y = loc.y, z = loc.z)

    player.msg("&aДом '&e$name&a' установлен!")
}

@Command("home", description = "Телепортироваться домой")
@Permission("homeplugin.home")
@Cooldown(5 seconds)
fun cmdHome(player: Player, args: Array) {
    val name = args.getOrElse(0) { "home" }
    val home = Homes.find(player.uniqueId, name)
        ?: return player.msg("&cДом '&e$name&c' не найден!")
    player.tp(Location(server.getWorld(home.world), home.x, home.y, home.z))
    player.msg("&aТелепорт к дому '&e$name&a'!")
}
  • @Permission — проверка права и запись в plugin.yml permissions
  • @Cooldown — пер-игроковый кулдаун, плейсхолдер {remaining} в сообщении
  • player.msg() — отправка сообщения с авто-переводом &-цветов
новичок

События

Событие — функция с @On, тип события выводится из сигнатуры. Регистрация слушателя генерируется автоматически.

events.krkuroko
@On
fun onJoin(event: PlayerJoinEvent) {
    event.player.msg("&6Добро пожаловать, &e${event.player.name}&6!")
    event.joinMessage = null          // убрать системное сообщение
}

@On
fun onBreak(event: BlockBreakEvent) {
    if (event.block.type == DIAMOND_ORE) {
        log("${event.player.name} добыл алмаз")
    }
}

Поддерживаются стандартные события Paper: PlayerJoinEvent, PlayerQuitEvent, PlayerMoveEvent, BlockBreakEvent, BlockPlaceEvent, EntityDamageEvent, InventoryClickEvent и остальные — через auto-import.

средний

Задачи и async

Три конструкции закрывают планировщик Bukkit: every — периодическая задача, after — отложенная, async — вне главного потока. Единицы времени пересчитываются в тики компилятором.

tasks.krkuroko
every(20 ticks) {              // каждую секунду
    players.forEach { p ->
        if (p.health < 5) p.msg("&cОпасно! Мало HP!")
    }
}

after(5 seconds) {             // через 5 секунд
    log("Прошло 5 секунд")
}

async {                        // не блокируем главный поток
    val data = fetchFromDatabase()
    player.msg("Загружено: $data")
}

// с условием
every(1 second) onlyIf { players.size > 0 } {
    log("кто-то онлайн")
}
Sync-задачи выполняются в главном потоке сервера, async — в пуле. Доступ к миру из async-блока требует возврата в sync — как и в чистом Bukkit.
средний

Таблицы и GUI

table {} — декларативная база данных

Таблица объявляется прямо в плагине. Компилятор создаёт SQLite-схему и DAO-методы. Ни одной строки SQL.

tables.krkuroko
table Homes {
    player: UUID primary
    name: String primary
    world: String
    x: Double; y: Double; z: Double
}

on load { Homes.createTable() }

// CRUD без SQL:
Homes.set(player.uniqueId, "home", loc)
val home = Homes.find(player.uniqueId, "home")
val list = Homes.where { player == player.uniqueId }.find()
Homes.delete(player.uniqueId, "home")

Из этого объявления генерируется DDL:

сгенерированный DDLsqlite

GUI — меню инвентаря DSL-ом

gui.krkuroko
@Command("menu")
fun cmdMenu(player: Player) {
    val gui = GUI("&6Меню", rows = 3) {
        slot(1, 1) {
            item = DIAMOND
            name = "&bАлмаз"
            onClick { p ->
                p.giveItem(DIAMOND, 1)
                p.msg("&aПолучен алмаз!")
            }
        }
        slot(2, 2) {
            item = BARRIER
            name = "&cЗакрыть"
            onClick { it.closeInventory() }
        }
    }
    player.openInventory(gui)
}

config {} и smartItem

misc.krkuroko
config("settings") {
    "max_players" default 100
    "prefix" default "&6[MyPlugin] "
    "debug" default false
}
val maxPlayers = config["max_players"].int

val sword = smartItem(DIAMOND_SWORD) {
    name = "&6Экскалибур"
    lore = ["&7Легендарный меч", "&8Урон: &c+50"]
    undroppable = true
    onRightClick { p -> p.msg("&bМеч дрожит...") }
}
новичок

Каталог ошибок

Каждая ошибка компилятора имеет код KR-класса, сообщение на русском и подсказку. kurokoc explain KR001 печатает полное объяснение.

КодЗначениеКак чинить
KR001 Null присваивается non-nullable типу Объяви тип с ? (var hp: Int?) или присвой значение; для строк — toIntOrNull() ?: 0
KR002 Имя не найдено (unresolved reference) Тип не в auto-import и не объявлен локально — проверь опечатку или добавь use
KR003 Нет точки входа Файл скрипта без fun main() и без @Plugin-класса
KRS01/02 Синтаксис: непарная скобка, незакрытая строка Компилятор укажет строку открытой скобки — проверь баланс

Пример сообщения

BuggyPlugin.kr:6 · KR001 · ошибка Нельзя присвоить null типу Int (переменная «hp»). Объяви Int? или убери null.

Плохо:

плохоkuroko
var health: Int = null

Хорошо:

хорошоkuroko
var health: Int? = null     // значение может отсутствовать
var health2: Int = 20       // или просто присвой число
продвинутый

Kuroko против Java

Замеры по каноническим реализациям HomePlugin в монорепозитории: examples/home-plugin/, методика — в LOC_COMPARISON.md.

ЯзыкФайлВсего строкЗначимыхИмпортов
JavaHomePlugin.java~210~17515
KotlinHomePlugin.kt~160~13010
KurokoHomePlugin.kr~85~550

Что именно исчезает

  • Java: отдельные CommandExecutor-классы, ручные проверки прав в каждой команде, многословный YAML-код, явные registerEvents / setExecutor, plugin.yml отдельным файлом
  • Kotlin: ergonomics лучше, но всё то же самое остаётся
  • Kuroko: команда + права + кулдаун = одна аннотированная функция; хранилище = table {}; жизненный цикл = on load / on unload; ноль импортов Paper API

HelloPlugin в цифрах: Java ~48–55 строк, Kotlin ~28–35, Kuroko — 14. plugin.yml у Java и Kotlin пишется вручную, у Kuroko генерируется.

Сокращение строк не значит «медленнее»: Kuroko выдаёт обычный JVM-байткод без рефлексии в рантайме — на сервере он неотличим от Java-плагина.
средний

kuroko.toml

Конфиг проекта объявляет платформу Minecraft — от неё зависит auto-import и цели сборки.

kuroko.tomltoml
[language]
version = "1.0"
target-jvm = 17

[minecraft]
api = "paper"          # paper / spigot / purpur / fabric / forge / neoforge
version = "1.21.4"
auto-import = true

[compiler]
warnings-as-errors = false
language-errors = "ru"   # язык сообщений: ru / en
incremental = true
optimize = true

[runtime]
debug = false

Флаги компилятора

терминал
kurokoc compile src/        # компиляция всех .kr
kurokoc check src/          # только проверка
kurokoc run Main.kr         # запуск скрипта
kurokoc explain KR001       # объяснение ошибки
kurokoc build               # полная сборка через Gradle

Платформы

ПлатформаСтатус
Paper 1.21.xосновная
Purpur / Leafтот же байткод, что и Paper
Spigot 1.16+best effort
Foliaпланировщик отличается — проверки в рантайме
Fabric / Forge / NeoForgeадаптер — после Beta

Текущий статус: 1.1.0-dev, design-stable developer preview. Политика совместимости 1.x — только аддитивные изменения. Полная дорожная карта — на главной.