API

Konfiguration und Netzwerk

Einstellungen, die der Spieler oder der Serverbetreiber ändern kann, ohne den Code anzufassen, und deine eigenen Daten zwischen Client und Server. Beides funktioniert aus CraftScript und aus Kotlin.

Konfiguration in CraftScript

Eine eigene Datei, settings.craft, deklariert die Einstellungen mit ihren Standardwerten, der Rest der Mod importiert sie:

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", { … }) erstellt beim ersten Mal config/rubies/settings.toml und gibt die aktuellen Werte zurück. Der Typ jeder Einstellung kommt von ihrem Standardwert: Eine Ganzzahl bleibt eine Ganzzahl.
  • config("settings") ohne Werte liest sie erneut, nachdem sie deklariert wurden.
  • Ein drittes Argument true sorgt dafür, dass der Server seine Werte an Clients schickt, wenn sie sich verbinden.
  • Der Name nutzt Kleinbuchstaben, Ziffern, _ oder -. Eine Mod kann mehrere Einstellungsdateien haben.

Konfiguration in Kotlin

Eine Klasse pro Mod mit den einstellbaren Werten, ihren Standardwerten und einer Beschreibung:

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)     // jeder Teil bekommt die Konfiguration, die er braucht
}

Crafting erstellt beim ersten Mal 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
  • Typen: int, long, double, bool, string, stringList.
  • Kommentare werden neu erzeugt, Werte, die der Nutzer geschrieben hat, bleiben erhalten.
  • Ein ungültiger Wert macht nichts kaputt: Crafting warnt im Log und nutzt den Standardwert.
  • Crafting.configs.reload(config) liest die Datei erneut, save(config) schreibt sie.
  • Mit synced = true schickt der Server seine Werte an Clients mit Crafting, wenn sie sich verbinden, damit alle nach denselben Regeln spielen.

Für deine eigenen Dateien werden ctx.configDir (config/<mod>/) und ctx.dataDir (data/<mod>/) erstellt, wenn du danach fragst.

Wer einem Server beitreten kann

Beim Verbinden vergleichen Client und Server ihre Mods. Die Regeln:

Situation Ergebnis
Der Server hat nur Plugins oder serverseitige Mods, der Client hat keine Mods Tritt bei
Der Server hat Mods, die Inhalte hinzufügen, der Client hat keine Mods Kann nicht beitreten und sieht die genaue Liste der Mods, die er braucht
Dem Client fehlt eine Mod, die Inhalte hinzufügt Kann nicht beitreten: „Missing mods: rubies 1.2.0“
Andere Hauptversion (1.x und 2.x) Kann nicht beitreten, mit beiden Versionen angezeigt
Andere Nebenversion (1.2 und 1.3) Tritt bei, mit einer Warnung im Server-Log
Reine Client-Mods Immer erlaubt

Deshalb solltest du deine Mod sorgfältig versionieren: Erhöhe die Hauptversion nur, wenn ein Client und ein Server mit verschiedenen Versionen nicht mehr zusammen spielen können.

Kanäle in CraftScript

Ein Kanal transportiert Daten zwischen Client und Server: Zahlen, Strings, Wahrheitswerte, Listen und Maps. Der Name "ping" wird zu rubies:ping, damit er nicht mit anderen Mods kollidiert.

net/ping.craftCraftScript
// Jede Seite hört auf das, was sie empfängt: Ein Paket erreicht nur eine Seite, die zuhört.

// Server: jeden Spieler begrüßen, der beitritt.
on join {
    send(player, "ping", { n: 1 })
}

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

// Server: die Antwort lesen (from ist der Spieler, der sie geschickt hat).
listen("pong", fun(data, from) {
    log("{from?.name} hat {data.n} geantwortet")
})
  • listen(channel, fun(data, player) { … }) empfängt. player ist auf dem Server der Absender und auf dem Client null.
  • send(player, channel, data) sendet vom Server an einen Spieler, send_to_server(channel, data) sendet vom Client an den Server.
  • Ein Paket erreicht nur eine Seite, die auf diesem Kanal hört: Jede Seite meldet beim Verbinden ihre Kanäle an. Deshalb ruft die Mod auf beiden Seiten listen für das auf, was jede empfängt.
  • Wird die Mod neu geladen oder deaktiviert, verschwinden ihre listen-Aufrufe mit ihr.

Kanäle in Kotlin

Ein Kanal sendet Werte eines Typs zwischen Client und Server. Er funktioniert in einer Einzelspielerwelt genauso wie auf einem Server.

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: wer es geschickt hat, auf dem Server; null auf dem Client
        }
    }

    fun announce(text: String) = channel.broadcast(text)         // Server → alle Clients
    fun tell(player: Player, text: String) = channel.sendTo(player, text)
}
  • Die Daten werden mit PacketWriter und PacketReader kompakt binär kodiert.
  • Crafting schickt seine Pakete nie an einen Client ohne Crafting: Das würde seine Verbindung trennen. sendTo sendet nichts an einen Spieler, der diesen Kanal nicht angemeldet hat. Crafting.network.isChannelAvailable(player, id) sagt es dir vorher.
  • Ein fehlerhaftes Paket wird geloggt und verworfen, es legt nie den Server lahm.

Eigene Typen

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