API

Configuración y red

Ajustes que el jugador o el dueño del servidor pueden cambiar sin tocar el código, y datos propios entre el cliente y el servidor. Las dos funcionan desde CraftScript y desde Kotlin.

Configuración en CraftScript

Un archivo propio, settings.craft, declara los ajustes con sus valores por defecto; el resto del mod los 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", { … }) crea config/rubies/settings.toml la primera vez y devuelve los valores actuales. El tipo de cada ajuste sale de su valor por defecto: un número entero sigue siendo entero.
  • config("settings"), sin valores, vuelve a leerlos después de declararlos.
  • Un tercer argumento true hace que el servidor envíe sus valores a los clientes al conectarse.
  • El nombre usa minúsculas, dígitos, _ o -. Un mod puede tener varios archivos de ajustes.

Configuración en Kotlin

Una clase por mod con los valores ajustables, su valor por defecto y una explicación:

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)     // each part gets the config it needs
}

Crafting crea config/rubies.toml la primera 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.
  • Los comentarios se regeneran; los valores que escribió el usuario se conservan.
  • Un valor inválido no rompe nada: Crafting avisa en el log y usa el valor por defecto.
  • Crafting.configs.reload(config) vuelve a leer el archivo; save(config) lo escribe.
  • Con synced = true, el servidor envía sus valores a los clientes que tienen Crafting al conectarse, para que todos jueguen con las mismas reglas.

Para archivos propios, ctx.configDir (config/<mod>/) y ctx.dataDir (data/<mod>/) se crean cuando los pides.

Quién puede entrar a un servidor

Al conectarse, cliente y servidor comparan sus mods. Las reglas:

Situación Resultado
El servidor solo tiene plugins o mods de servidor; el cliente no tiene mods Entra
El servidor tiene mods que añaden contenido; el cliente no tiene mods No entra, y ve la lista exacta de mods que necesita
Al cliente le falta un mod que añade contenido No entra: “Missing mods: rubies 1.2.0”
Versión mayor distinta (1.x y 2.x) No entra, con las dos versiones
Versión menor distinta (1.2 y 1.3) Entra, con un aviso en el log del servidor
Mods solo de cliente Siempre permitidos

Por eso conviene versionar el mod con cuidado: sube la versión mayor solo cuando cliente y servidor de versiones distintas ya no puedan jugar juntos.

Canales en CraftScript

Un canal lleva datos entre cliente y servidor: números, textos, booleanos, listas y mapas. El nombre "ping" es rubies:ping, así que no choca con otros mods.

net/ping.craftCraftScript
// Every side listens to what it receives: a packet only reaches a side that listens.

// Server: say hello to each player that joins.
on join {
    send(player, "ping", { n: 1 })
}

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

// Server: read the answer (from is the player who sent it).
listen("pong", fun(data, from) {
    log("{from?.name} answered {data.n}")
})
  • listen(canal, fun(data, player) { … }) recibe; player es quien lo envió en el servidor, y null en el cliente.
  • send(player, canal, data) envía del servidor a un jugador; send_to_server(canal, data), del cliente al servidor.
  • Un paquete solo llega a un lado que escucha ese canal: cada lado anuncia sus canales al conectarse. Por eso el mod llama a listen en los dos lados para lo que cada uno recibe.
  • Al recargar o desactivar el mod, sus listen desaparecen con él.

Canales en Kotlin

Un canal envía valores de un tipo entre cliente y servidor. Funciona igual en un mundo de un jugador que en un 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: who sent it, on the server; null on the client
        }
    }

    fun announce(text: String) = channel.broadcast(text)         // server → every client
    fun tell(player: Player, text: String) = channel.sendTo(player, text)
}
  • Los datos se codifican en binario compacto con PacketWriter y PacketReader.
  • Crafting nunca envía sus paquetes a un cliente sin Crafting: lo desconectaría. sendTo no envía nada a un jugador que no anunció ese canal; Crafting.network.isChannelAvailable(player, id) lo dice de antemano.
  • Un paquete mal formado se registra y se descarta; nunca tumba el servidor.

Tipos propios

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