API
Настройки и сеть
Настройки, которые игрок или владелец сервера может менять, не трогая код, и твои собственные данные между клиентом и сервером. И то и другое работает из CraftScript и из Kotlin.
Настройки в CraftScript
Отдельный файл, settings.craft, объявляет настройки со значениями по умолчанию; остальной мод их импортирует:
// Values the server owner can change in config/rubies/settings.toml
const SETTINGS = config("settings", {
bonus_chance: 0.25,
welcome_rubies: 3
})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
Один класс на мод с настраиваемыми значениями, их значениями по умолчанию и описанием:
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")
}override fun onLoad(ctx: ModContext) {
val config = Crafting.configs.load(RubiesConfig())
MiningEvents.register(config) // каждая часть получает нужные ей настройки
}Crafting в первый раз создаёт config/rubies.toml:
# 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, поэтому не пересекается с другими модами.
// Каждая сторона слушает то, что получает: пакет доходит только до той стороны, что слушает.
// Сервер: поздороваться с каждым зашедшим игроком.
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
Канал отправляет значения одного типа между клиентом и сервером. В одиночном мире он работает так же, как на сервере.
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)сообщает это заранее. - Повреждённый пакет записывается в журнал и отбрасывается; он никогда не роняет сервер.
Собственные типы
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)
}