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:
// 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", { … })erstellt beim ersten Malconfig/rubies/settings.tomlund 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
truesorgt 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:
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) // jeder Teil bekommt die Konfiguration, die er braucht
}Crafting erstellt beim ersten Mal 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- 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 = trueschickt 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.
// 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.playerist auf dem Server der Absender und auf dem Clientnull.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
listenfü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.
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
PacketWriterundPacketReaderkompakt binär kodiert. - Crafting schickt seine Pakete nie an einen Client ohne Crafting: Das würde seine Verbindung trennen.
sendTosendet 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
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)
}