API
設定とネットワーク
プレイヤーやサーバーの管理者がコードに触れずに変更できる設定と、クライアントとサーバー間でやり取りする独自のデータ。どちらも CraftScript からも Kotlin からも使えます。
CraftScript での設定
専用のファイル settings.craft で設定とそのデフォルト値を宣言し、Mod の他の部分はそれをインポートします。
// 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", { … })は初回にconfig/rubies/settings.tomlを作成し、現在の値を返します。各設定の型はデフォルト値から決まります。整数は整数のままです。- 値を渡さない
config("settings")は、宣言済みの値をもう一度読み込みます。 - 3 つ目の引数に
trueを渡すと、クライアントの接続時にサーバーが自分の値を送ります。 - 名前には小文字、数字、
_または-を使います。1 つの Mod で複数の設定ファイルを持てます。
Kotlin での設定
Mod ごとに 1 つのクラスで、調整できる値、そのデフォルト値、説明を書きます。
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) // 各部分は必要な設定を受け取る
}Crafting は初回に 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- 型:
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 と衝突しません。
// 各側は自分が受け取るものを受信する。パケットは受信している側にしか届かない。
// サーバー:参加したプレイヤーにあいさつする。
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 つの型の値をクライアントとサーバーの間で送ります。シングルプレイのワールドでもサーバーでも同じように動きます。
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)で事前に確認できます。 - 不正な形式のパケットはログに記録されて破棄され、サーバーを落とすことはありません。
独自の型
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)
}