API
配置与网络
玩家或服务器所有者无需改动代码就能修改的设置,以及在客户端与服务器之间传输你自己的数据。两者都可以在 CraftScript 和 Kotlin 中使用。
CraftScript 中的配置
一个独立的文件 settings.craft 声明各项设置及其默认值;模组的其他部分导入它们:
// 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")不带值时,会在声明之后再次读取这些设置。- 第三个参数为
true时,服务器会在客户端连接时把自己的值发送给它们。 - 名称使用小写字母、数字、
_或-。一个模组可以有多个设置文件。
Kotlin 中的配置
每个模组一个类,包含可调整的值、默认值和说明:
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>/)会在你请求时创建。
谁能加入服务器
连接时,客户端和服务器会比对双方的模组。规则如下:
| 情况 | 结果 |
|---|---|
| 服务器只有插件或仅服务端的模组;客户端没有模组 | 可以加入 |
| 服务器有添加内容的模组;客户端没有模组 | 无法加入,并会看到所需模组的完整列表 |
| 客户端缺少一个添加内容的模组 | 无法加入:“Missing mods: rubies 1.2.0” |
| 主版本不同(1.x 与 2.x) | 无法加入,并显示双方的版本 |
| 次版本不同(1.2 与 1.3) | 可以加入,服务器日志中会有警告 |
| 仅客户端的模组 | 始终允许 |
因此要谨慎地为模组定版本号:只有当不同版本的客户端和服务器无法再一起游玩时,才提升主版本号。
CraftScript 中的频道
频道在客户端与服务器之间传输数据:数字、字符串、布尔值、列表和映射。名称 "ping" 会变成 rubies:ping,因此不会与其他模组冲突。
// 每一端都监听自己接收的内容:数据包只会到达正在监听的一端。
// 服务器:向每个加入的玩家打招呼。
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 中的频道
频道在客户端与服务器之间发送某一类型的值。它在单人世界中和在服务器上的工作方式相同。
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)
}