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