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