API

Настройки и сеть

Настройки, которые игрок или владелец сервера может менять, не трогая код, и твои собственные данные между клиентом и сервером. И то и другое работает из CraftScript и из Kotlin.

Настройки в CraftScript

Отдельный файл, settings.craft, объявляет настройки со значениями по умолчанию; остальной мод их импортирует:

settings.craftCraftScript
// Values the server owner can change in config/rubies/settings.toml
const SETTINGS = config("settings", {
    bonus_chance: 0.25,
    welcome_rubies: 3
})
events/mining.craftCraftScript
import { give_rubies } from "/lib/rewards"
import { SETTINGS } from "/settings"

on break(e) {
    let p = e.player
    if p == null { return }
    if e.block.type == "minecraft:diamond_ore" and chance(SETTINGS["bonus_chance"]) {
        give_rubies(p, 1)
        p.play_sound("rubies:ruby_chime")
        p.world.spawn_particle("rubies:ruby_spark", e.block.location, 12, 0.4, 0.02)
    }
}
  • config("settings", { … }) в первый раз создаёт config/rubies/settings.toml и возвращает текущие значения. Тип каждой настройки берётся из значения по умолчанию: целое остаётся целым.
  • config("settings") без значений снова читает их после того, как они объявлены.
  • Третий аргумент true заставляет сервер отправлять свои значения клиентам при подключении.
  • Имя состоит из строчных букв, цифр, _ или -. У мода может быть несколько файлов настроек.

Настройки в Kotlin

Один класс на мод с настраиваемыми значениями, их значениями по умолчанию и описанием:

RubiesConfig.ktKotlin
package com.example.rubies

class RubiesConfig : Config("rubies", synced = true) {
    var dropChance by double("drop_chance", 0.25, "Chance that diamond ore also drops a ruby")
    var armorReduction by double("armor_reduction", 0.2, "Damage reduction while holding a ruby")
}
RubiesMod.ktKotlin
override fun onLoad(ctx: ModContext) {
    val config = Crafting.configs.load(RubiesConfig())
    MiningEvents.register(config)     // каждая часть получает нужные ей настройки
}

Crafting в первый раз создаёт config/rubies.toml:

config/rubies.tomlTOML
# rubies — generated by Crafting. Edit freely; comments are regenerated.

# Chance that diamond ore also drops a ruby
# default: 0.25
drop_chance = 0.25

# Damage reduction while holding a ruby
# default: 0.2
armor_reduction = 0.2
  • Типы: int, long, double, bool, string, stringList.
  • Комментарии создаются заново; значения, которые записал пользователь, сохраняются.
  • Неверное значение ничего не ломает: Crafting предупреждает в журнале и использует значение по умолчанию.
  • Crafting.configs.reload(config) снова читает файл; save(config) записывает его.
  • С synced = true сервер при подключении отправляет свои значения клиентам с Crafting, чтобы все играли по одним правилам.

Для твоих собственных файлов ctx.configDir (config/<mod>/) и ctx.dataDir (data/<mod>/) создаются, когда ты их запрашиваешь.

Кто может зайти на сервер

При подключении клиент и сервер сравнивают свои моды. Правила:

Ситуация Результат
На сервере только плагины или серверные моды; у клиента нет модов Заходит
На сервере есть моды, добавляющие контент; у клиента нет модов Не может зайти и видит точный список нужных модов
У клиента не хватает мода, добавляющего контент Не может зайти: «Missing mods: rubies 1.2.0»
Разная мажорная версия (1.x и 2.x) Не может зайти, показываются обе версии
Разная минорная версия (1.2 и 1.3) Заходит, с предупреждением в журнале сервера
Только клиентские моды Всегда разрешено

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

Каналы в CraftScript

Канал переносит данные между клиентом и сервером: числа, строки, логические значения, списки и словари. Имя "ping" превращается в rubies:ping, поэтому не пересекается с другими модами.

net/ping.craftCraftScript
// Каждая сторона слушает то, что получает: пакет доходит только до той стороны, что слушает.

// Сервер: поздороваться с каждым зашедшим игроком.
on join {
    send(player, "ping", { n: 1 })
}

// Клиент: ответить.
listen("ping", fun(data, from) {
    send_to_server("pong", { n: data.n + 1 })
})

// Сервер: прочитать ответ (from — игрок, который его отправил).
listen("pong", fun(data, from) {
    log("{from?.name} ответил {data.n}")
})
  • listen(channel, fun(data, player) { … }) принимает; player — отправитель на сервере и null на клиенте.
  • send(player, channel, data) отправляет с сервера игроку; send_to_server(channel, data) отправляет с клиента на сервер.
  • Пакет доходит только до стороны, которая слушает этот канал: каждая сторона объявляет свои каналы при подключении. Поэтому мод вызывает listen на обеих сторонах для того, что каждая из них принимает.
  • Когда мод перезагружается или отключается, его вызовы listen уходят вместе с ним.

Каналы в Kotlin

Канал отправляет значения одного типа между клиентом и сервером. В одиночном мире он работает так же, как на сервере.

net/SparkleChannel.ktKotlin
package com.example.rubies.net

object SparkleChannel {
    private val channel = Crafting.network.channel(Identifier.of("rubies", "sparkle"), PacketCodec.STRING)

    fun register() {
        channel.onReceive { message, ctx ->
            // ctx.player: кто отправил, на сервере; null на клиенте
        }
    }

    fun announce(text: String) = channel.broadcast(text)         // сервер → все клиенты
    fun tell(player: Player, text: String) = channel.sendTo(player, text)
}
  • Данные кодируются в компактный двоичный формат через PacketWriter и PacketReader.
  • Crafting никогда не отправляет свои пакеты клиенту без Crafting: это бы его отключило. sendTo ничего не отправляет игроку, который не объявил этот канал; Crafting.network.isChannelAvailable(player, id) сообщает это заранее.
  • Повреждённый пакет записывается в журнал и отбрасывается; он никогда не роняет сервер.

Собственные типы

net/BountyChannel.ktKotlin
package com.example.rubies.net

data class Bounty(val target: String, val reward: Int)

object BountyChannel {
    private val CODEC = PacketCodec.codec<Bounty>(
        { b, out -> out.string(b.target).varInt(b.reward) },
        { input -> Bounty(input.string(), input.varInt()) },
    )
    val channel = Crafting.network.channel(Identifier.of("rubies", "bounty"), CODEC)
}