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)
}