API

Configuration et réseau

Des réglages que le joueur ou le propriétaire du serveur peuvent changer sans toucher au code, et tes propres données entre client et serveur. Les deux fonctionnent depuis CraftScript et depuis Kotlin.

Configuration en CraftScript

Un fichier à part, settings.craft, déclare les réglages avec leurs valeurs par défaut ; le reste du mod les importe :

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", { … }) crée config/rubies/settings.toml la première fois et renvoie les valeurs actuelles. Le type de chaque réglage vient de sa valeur par défaut : un entier reste un entier.
  • config("settings"), sans valeurs, les relit une fois qu’elles sont déclarées.
  • Un troisième argument true fait envoyer par le serveur ses valeurs aux clients quand ils se connectent.
  • Le nom utilise des lettres minuscules, des chiffres, _ ou -. Un mod peut avoir plusieurs fichiers de réglages.

Configuration en Kotlin

Une classe par mod avec les valeurs ajustables, leurs valeurs par défaut et une description :

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)     // chaque partie reçoit la config dont elle a besoin
}

Crafting crée config/rubies.toml la première fois :

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
  • Types : int, long, double, bool, string, stringList.
  • Les commentaires sont régénérés ; les valeurs écrites par l’utilisateur sont conservées.
  • Une valeur invalide ne casse rien : Crafting prévient dans le journal et utilise la valeur par défaut.
  • Crafting.configs.reload(config) relit le fichier ; save(config) l’écrit.
  • Avec synced = true, le serveur envoie ses valeurs aux clients qui ont Crafting quand ils se connectent, pour que tout le monde joue avec les mêmes règles.

Pour tes propres fichiers, ctx.configDir (config/<mod>/) et ctx.dataDir (data/<mod>/) sont créés quand tu les demandes.

Qui peut rejoindre un serveur

À la connexion, le client et le serveur comparent leurs mods. Les règles :

Situation Résultat
Le serveur n’a que des plugins ou des mods côté serveur ; le client n’a pas de mods Se connecte
Le serveur a des mods qui ajoutent du contenu ; le client n’a pas de mods Ne peut pas se connecter, et voit la liste exacte des mods nécessaires
Il manque au client un mod qui ajoute du contenu Ne peut pas se connecter : « Missing mods: rubies 1.2.0 »
Version majeure différente (1.x et 2.x) Ne peut pas se connecter, avec les deux versions affichées
Version mineure différente (1.2 et 1.3) Se connecte, avec un avertissement dans le journal du serveur
Mods uniquement côté client Toujours autorisés

C’est pour ça qu’il faut versionner ton mod avec soin : n’augmente la version majeure que lorsqu’un client et un serveur sur des versions différentes ne peuvent plus jouer ensemble.

Canaux en CraftScript

Un canal transporte des données entre client et serveur : nombres, chaînes, booléens, listes et maps. Le nom "ping" devient rubies:ping, il n’entre donc pas en conflit avec d’autres mods.

net/ping.craftCraftScript
// Chaque côté écoute ce qu’il reçoit : un paquet n’arrive qu’à un côté qui écoute.

// Serveur : dit bonjour à chaque joueur qui se connecte.
on join {
    send(player, "ping", { n: 1 })
}

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

// Serveur : lit la réponse (from est le joueur qui l’a envoyée).
listen("pong", fun(data, from) {
    log("{from?.name} a répondu {data.n}")
})
  • listen(channel, fun(data, player) { … }) reçoit ; player est l’expéditeur côté serveur, et null côté client.
  • send(player, channel, data) envoie du serveur vers un joueur ; send_to_server(channel, data) envoie du client vers le serveur.
  • Un paquet n’arrive qu’à un côté qui écoute ce canal : chaque côté annonce ses canaux à la connexion. C’est pour ça que le mod appelle listen des deux côtés pour ce que chacun reçoit.
  • Quand le mod est rechargé ou désactivé, ses appels à listen disparaissent avec lui.

Canaux en Kotlin

Un canal envoie des valeurs d’un type donné entre client et serveur. Il fonctionne de la même façon dans un monde solo que sur un serveur.

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 : qui l’a envoyé, côté serveur ; null côté client
        }
    }

    fun announce(text: String) = channel.broadcast(text)         // serveur → tous les clients
    fun tell(player: Player, text: String) = channel.sendTo(player, text)
}
  • Les données sont encodées en binaire compact avec PacketWriter et PacketReader.
  • Crafting n’envoie jamais ses paquets à un client sans Crafting : cela le déconnecterait. sendTo n’envoie rien à un joueur qui n’a pas annoncé ce canal ; Crafting.network.isChannelAvailable(player, id) te le dit à l’avance.
  • Un paquet mal formé est consigné et jeté ; il ne fait jamais tomber le serveur.

Types personnalisés

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