API

設定とネットワーク

プレイヤーやサーバーの管理者がコードに触れずに変更できる設定と、クライアントとサーバー間でやり取りする独自のデータ。どちらも CraftScript からも Kotlin からも使えます。

CraftScript での設定

専用のファイル settings.craft で設定とそのデフォルト値を宣言し、Mod の他の部分はそれをインポートします。

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", { … }) は初回に config/rubies/settings.toml を作成し、現在の値を返します。各設定の型はデフォルト値から決まります。整数は整数のままです。
  • 値を渡さない config("settings") は、宣言済みの値をもう一度読み込みます。
  • 3 つ目の引数に true を渡すと、クライアントの接続時にサーバーが自分の値を送ります。
  • 名前には小文字、数字、_ または - を使います。1 つの Mod で複数の設定ファイルを持てます。

Kotlin での設定

Mod ごとに 1 つのクラスで、調整できる値、そのデフォルト値、説明を書きます。

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)     // 各部分は必要な設定を受け取る
}

Crafting は初回に 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
  • 型:int、long、double、bool、string、stringList。
  • コメントは再生成されますが、ユーザーが書いた値は保持されます。
  • 不正な値があっても何も壊れません。Crafting はログに警告を出し、デフォルト値を使います。
  • Crafting.configs.reload(config) でファイルを読み直し、save(config) で書き込みます。
  • synced = true にすると、Crafting を持つクライアントの接続時にサーバーが自分の値を送るので、全員が同じルールで遊べます。

独自のファイルには、ctx.configDir(config/<mod>/)と ctx.dataDir(data/<mod>/)が使えます。要求したときに作成されます。

サーバーに参加できる人

接続時に、クライアントとサーバーは互いの Mod を比較します。ルールは次のとおりです。

状況 結果
サーバーにはプラグインかサーバー側の Mod しかなく、クライアントには Mod がない 参加できる
サーバーにコンテンツを追加する Mod があり、クライアントには Mod がない 参加できない。必要な Mod の正確な一覧が表示されます
クライアントに、コンテンツを追加する Mod が足りない 参加できない:「Missing mods: rubies 1.2.0」
メジャーバージョンが異なる(1.x と 2.x) 参加できない。両方のバージョンが表示されます
マイナーバージョンが異なる(1.2 と 1.3) 参加できる。サーバーのログに警告が出ます
クライアント専用の Mod 常に許可

だからこそ、Mod のバージョンは慎重に付けましょう。メジャーバージョンを上げるのは、異なるバージョンのクライアントとサーバーが一緒に遊べなくなるときだけにします。

CraftScript でのチャンネル

チャンネルは、クライアントとサーバーの間でデータ(数値、文字列、真偽値、リスト、マップ)を運びます。名前 "ping" は rubies:ping になるので、他の Mod と衝突しません。

net/ping.craftCraftScript
// 各側は自分が受け取るものを受信する。パケットは受信している側にしか届かない。

// サーバー:参加したプレイヤーにあいさつする。
on join {
    send(player, "ping", { n: 1 })
}

// クライアント:返事をする。
listen("ping", fun(data, from) {
    send_to_server("pong", { n: data.n + 1 })
})

// サーバー:返事を読む(from は送ってきたプレイヤー)。
listen("pong", fun(data, from) {
    log("{from?.name} が {data.n} と返事しました")
})
  • listen(channel, fun(data, player) { … }) で受信します。player はサーバーでは送信者、クライアントでは null です。
  • send(player, channel, data) はサーバーからプレイヤーへ、send_to_server(channel, data) はクライアントからサーバーへ送ります。
  • パケットは、そのチャンネルを受信している側にしか届きません。各側は接続時に自分のチャンネルを通知します。そのため Mod は、それぞれが受け取るものについて両側で listen を呼びます。
  • Mod が再読み込みまたは無効化されると、その listen の呼び出しも一緒に消えます。

Kotlin でのチャンネル

チャンネルは、1 つの型の値をクライアントとサーバーの間で送ります。シングルプレイのワールドでもサーバーでも同じように動きます。

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:サーバーでは送信者、クライアントでは null
        }
    }

    fun announce(text: String) = channel.broadcast(text)         // サーバー → すべてのクライアント
    fun tell(player: Player, text: String) = channel.sendTo(player, text)
}
  • データは PacketWriter と PacketReader で、コンパクトなバイナリにエンコードされます。
  • Crafting は、Crafting のないクライアントに自分のパケットを決して送りません。送ると切断されてしまうからです。sendTo は、そのチャンネルを通知していないプレイヤーには何も送りません。Crafting.network.isChannelAvailable(player, id) で事前に確認できます。
  • 不正な形式のパケットはログに記録されて破棄され、サーバーを落とすことはありません。

独自の型

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