API

Konfiguracja i sieć

Ustawienia, które gracz lub właściciel serwera może zmienić bez dotykania kodu, oraz twoje własne dane przesyłane między klientem a serwerem. Jedno i drugie działa z CraftScript i z Kotlina.

Konfiguracja w CraftScript

Osobny plik, settings.craft, deklaruje ustawienia z ich wartościami domyślnymi; reszta moda je importuje:

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", { … }) za pierwszym razem tworzy config/rubies/settings.toml i zwraca bieżące wartości. Typ każdego ustawienia wynika z jego wartości domyślnej: liczba całkowita pozostaje całkowitą.
  • config("settings"), bez wartości, czyta je ponownie po ich zadeklarowaniu.
  • Trzeci argument true sprawia, że serwer wysyła swoje wartości klientom, gdy się łączą.
  • Nazwa używa małych liter, cyfr, _ lub -. Mod może mieć kilka plików ustawień.

Konfiguracja w Kotlinie

Jedna klasa na mod z wartościami do zmiany, ich wartościami domyślnymi i opisem:

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)     // każda część dostaje potrzebną jej konfigurację
}

Crafting za pierwszym razem tworzy 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
  • Typy: int, long, double, bool, string, stringList.
  • Komentarze są generowane od nowa; wartości wpisane przez użytkownika zostają zachowane.
  • Nieprawidłowa wartość niczego nie psuje: Crafting ostrzega w dzienniku i używa wartości domyślnej.
  • Crafting.configs.reload(config) czyta plik ponownie; save(config) go zapisuje.
  • Z synced = true serwer wysyła swoje wartości klientom z Craftingiem, gdy się łączą, więc wszyscy grają według tych samych zasad.

Na twoje własne pliki ctx.configDir (config/<mod>/) i ctx.dataDir (data/<mod>/) są tworzone, gdy o nie poprosisz.

Kto może dołączyć do serwera

Przy połączeniu klient i serwer porównują swoje mody. Zasady:

Sytuacja Wynik
Serwer ma tylko wtyczki lub mody po stronie serwera; klient nie ma modów Dołącza
Serwer ma mody, które dodają zawartość; klient nie ma modów Nie może dołączyć i widzi dokładną listę potrzebnych modów
Klientowi brakuje moda, który dodaje zawartość Nie może dołączyć: „Brakujące mody: rubies 1.2.0”
Inna wersja główna (1.x i 2.x) Nie może dołączyć, z pokazaniem obu wersji
Inna wersja podrzędna (1.2 i 1.3) Dołącza, z ostrzeżeniem w dzienniku serwera
Mody tylko dla klienta Zawsze dozwolone

Dlatego wersjonuj swój mod starannie: zwiększaj wersję główną tylko wtedy, gdy klient i serwer w różnych wersjach nie mogą już grać razem.

Kanały w CraftScript

Kanał przenosi dane między klientem a serwerem: liczby, napisy, wartości logiczne, listy i mapy. Nazwa "ping" staje się rubies:ping, więc nie koliduje z innymi modami.

net/ping.craftCraftScript
// Każda strona nasłuchuje tego, co odbiera: pakiet dociera tylko do strony, która nasłuchuje.

// Serwer: przywitaj każdego gracza, który dołącza.
on join {
    send(player, "ping", { n: 1 })
}

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

// Serwer: odczytaj odpowiedź (from to gracz, który ją wysłał).
listen("pong", fun(data, from) {
    log("{from?.name} odpowiedział {data.n}")
})
  • listen(channel, fun(data, player) { … }) odbiera; player to nadawca na serwerze, a null na kliencie.
  • send(player, channel, data) wysyła z serwera do gracza; send_to_server(channel, data) wysyła z klienta do serwera.
  • Pakiet dociera tylko do strony, która nasłuchuje na danym kanale: każda strona ogłasza swoje kanały przy połączeniu. Dlatego mod wywołuje listen po obu stronach dla tego, co każda z nich odbiera.
  • Gdy mod zostaje przeładowany lub wyłączony, jego wywołania listen znikają razem z nim.

Kanały w Kotlinie

Kanał wysyła wartości jednego typu między klientem a serwerem. Działa tak samo w świecie jednoosobowym jak na serwerze.

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: nadawca na serwerze; null na kliencie
        }
    }

    fun announce(text: String) = channel.broadcast(text)         // serwer → każdy klient
    fun tell(player: Player, text: String) = channel.sendTo(player, text)
}
  • Dane są kodowane w zwartym formacie binarnym przez PacketWriter i PacketReader.
  • Crafting nigdy nie wysyła swoich pakietów do klienta bez Craftingu: to by go rozłączyło. sendTo nic nie wysyła graczowi, który nie ogłosił tego kanału; Crafting.network.isChannelAvailable(player, id) mówi ci to z góry.
  • Uszkodzony pakiet trafia do dziennika i zostaje odrzucony; nigdy nie wyłącza serwera.

Własne typy

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