API

配置与网络

玩家或服务器所有者无需改动代码就能修改的设置,以及在客户端与服务器之间传输你自己的数据。两者都可以在 CraftScript 和 Kotlin 中使用。

CraftScript 中的配置

一个独立的文件 settings.craft 声明各项设置及其默认值;模组的其他部分导入它们:

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") 不带值时,会在声明之后再次读取这些设置。
  • 第三个参数为 true 时,服务器会在客户端连接时把自己的值发送给它们。
  • 名称使用小写字母、数字、_ 或 -。一个模组可以有多个设置文件。

Kotlin 中的配置

每个模组一个类,包含可调整的值、默认值和说明:

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>/)会在你请求时创建。

谁能加入服务器

连接时,客户端和服务器会比对双方的模组。规则如下:

情况 结果
服务器只有插件或仅服务端的模组;客户端没有模组 可以加入
服务器有添加内容的模组;客户端没有模组 无法加入,并会看到所需模组的完整列表
客户端缺少一个添加内容的模组 无法加入:“Missing mods: rubies 1.2.0”
主版本不同(1.x 与 2.x) 无法加入,并显示双方的版本
次版本不同(1.2 与 1.3) 可以加入,服务器日志中会有警告
仅客户端的模组 始终允许

因此要谨慎地为模组定版本号:只有当不同版本的客户端和服务器无法再一起游玩时,才提升主版本号。

CraftScript 中的频道

频道在客户端与服务器之间传输数据:数字、字符串、布尔值、列表和映射。名称 "ping" 会变成 rubies:ping,因此不会与其他模组冲突。

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) 从客户端向服务器发送。
  • 数据包只会到达监听该频道的一端:每一端在连接时宣告自己的频道。所以模组要在两端分别为各自接收的内容调用 listen。
  • 模组重新加载或被禁用时,它的 listen 调用也会随之移除。

Kotlin 中的频道

频道在客户端与服务器之间发送某一类型的值。它在单人世界中和在服务器上的工作方式相同。

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