API

Configuração e rede

Ajustes que o jogador ou o dono do servidor pode mudar sem mexer no código, e seus próprios dados entre cliente e servidor. Os dois funcionam em CraftScript e em Kotlin.

Configuração em CraftScript

Um arquivo próprio, settings.craft, declara os ajustes com seus valores padrão; o resto do mod os importa:

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", { … }) cria config/rubies/settings.toml na primeira vez e retorna os valores atuais. O tipo de cada ajuste vem do valor padrão: um inteiro continua inteiro.
  • config("settings"), sem valores, lê os ajustes de novo depois de declarados.
  • Um terceiro argumento true faz o servidor enviar seus valores aos clientes quando eles se conectam.
  • O nome usa letras minúsculas, dígitos, _ ou -. Um mod pode ter vários arquivos de ajustes.

Configuração em Kotlin

Uma classe por mod com os valores ajustáveis, seus padrões e uma descrição:

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)     // cada parte recebe a configuração de que precisa
}

O Crafting cria config/rubies.toml na primeira vez:

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
  • Tipos: int, long, double, bool, string, stringList.
  • Os comentários são regenerados; os valores que o usuário escreveu são mantidos.
  • Um valor inválido não quebra nada: o Crafting avisa no log e usa o valor padrão.
  • Crafting.configs.reload(config) lê o arquivo de novo; save(config) o grava.
  • Com synced = true, o servidor envia seus valores aos clientes que têm o Crafting quando eles se conectam, para que todos joguem com as mesmas regras.

Para seus próprios arquivos, ctx.configDir (config/<mod>/) e ctx.dataDir (data/<mod>/) são criados quando você os pede.

Quem pode entrar em um servidor

Ao conectar, cliente e servidor comparam seus mods. As regras:

Situação Resultado
O servidor só tem plugins ou mods de servidor; o cliente não tem mods Entra
O servidor tem mods que adicionam conteúdo; o cliente não tem mods Não entra, e vê a lista exata dos mods de que precisa
Falta ao cliente um mod que adiciona conteúdo Não entra: “Missing mods: rubies 1.2.0”
Versão principal diferente (1.x e 2.x) Não entra, com as duas versões exibidas
Versão secundária diferente (1.2 e 1.3) Entra, com um aviso no log do servidor
Mods só de cliente Sempre permitidos

Por isso, versione seu mod com cuidado: aumente a versão principal só quando um cliente e um servidor em versões diferentes não puderem mais jogar juntos.

Canais em CraftScript

Um canal leva dados entre cliente e servidor: números, strings, booleanos, listas e mapas. O nome "ping" vira rubies:ping, então não colide com outros mods.

net/ping.craftCraftScript
// Cada lado escuta o que recebe: um pacote só chega a um lado que escuta.

// Servidor: cumprimenta cada jogador que entra.
on join {
    send(player, "ping", { n: 1 })
}

// Cliente: responde.
listen("ping", fun(data, from) {
    send_to_server("pong", { n: data.n + 1 })
})

// Servidor: lê a resposta (from é o jogador que a enviou).
listen("pong", fun(data, from) {
    log("{from?.name} respondeu {data.n}")
})
  • listen(channel, fun(data, player) { … }) recebe; player é quem enviou, no servidor, e null no cliente.
  • send(player, channel, data) envia do servidor para um jogador; send_to_server(channel, data) envia do cliente para o servidor.
  • Um pacote só chega a um lado que escuta esse canal: cada lado anuncia seus canais ao conectar. Por isso o mod chama listen nos dois lados para o que cada um recebe.
  • Quando o mod é recarregado ou desativado, suas chamadas a listen vão embora com ele.

Canais em Kotlin

Um canal envia valores de um tipo entre cliente e servidor. Funciona igual em um mundo de um jogador e em um servidor.

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: quem enviou, no servidor; null no cliente
        }
    }

    fun announce(text: String) = channel.broadcast(text)         // servidor → todos os clientes
    fun tell(player: Player, text: String) = channel.sendTo(player, text)
}
  • Os dados são codificados em binário compacto com PacketWriter e PacketReader.
  • O Crafting nunca envia seus pacotes a um cliente sem Crafting: isso o desconectaria. sendTo não envia nada a um jogador que não anunciou esse canal; Crafting.network.isChannelAvailable(player, id) avisa você antes.
  • Um pacote malformado é registrado no log e descartado; ele nunca derruba o servidor.

Tipos personalizados

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