가이드

모드 정리하기

모드는 한 번 작성하고 몇 년 동안 관리합니다. 관리에 가장 도움이 되는 것은 언어가 아니라, 모든 것이 어디에 있느냐입니다. 이 가이드는 CraftScript와 Kotlin 각각에 하나씩 구조를 제안하고 그 이유를 설명합니다.

네 가지 규칙

  1. 파일 하나에 책임 하나. 광석용 파일 하나, 조합법용 파일 하나, 명령어마다 파일 하나. 파일을 설명하는 데 “그리고”가 필요하다면, 아마 파일 두 개일 것입니다.
  2. 크기가 아니라 주제로 묶습니다. content/, events/, commands/, ui/, lib/ 같은 폴더. 모드를 연 사람은 코드를 읽지 않고도 어디를 봐야 할지 압니다.
  3. 데이터와 동작을 분리합니다. 존재하는 것(아이템, 블록, 조합법)은 속성 목록입니다. 일어나는 일(이벤트, 명령어)은 코드입니다. 둘을 섞으면 숫자 하나를 바꾸려 해도 로직을 읽어야 합니다.
  4. 연결만 하는 진입점. main.craft나 모드 클래스는 어떤 부분이 있는지만 말합니다. 게임 규칙은 담지 않습니다.

이렇게 하면 모드의 밸런스를 바꿀 때는 content/recipes.craft를 수정하고, 명령어를 고칠 때는 commands/를 열면 됩니다. 이벤트의 버그가 블록 이백 줄 사이에 숨어 있지도 않습니다.

CraftScript에서

권장 구조텍스트
mods/rubies/
├── main.craft               the mod and the list of its parts
├── settings.craft           settings for the server owner
├── content/                 data: what exists
│   ├── items.craft
│   ├── ores.craft
│   ├── world.craft
│   ├── dimensions.craft
│   ├── creatures.craft
│   ├── sounds.craft
│   ├── particles.craft
│   └── recipes.craft
├── events/                  behaviour: what happens
│   ├── mining.craft
│   ├── hunting.craft
│   └── welcome.craft
├── ui/                      forms: what the player sees
│   └── shop.craft
├── commands/
│   ├── realm.craft
│   ├── rubies.craft
│   └── shop.craft
├── lib/                     shared logic
│   ├── rewards.craft
│   └── storage.craft        saved data: the keys, in one place
├── sounds/
│   └── ruby/chime.ogg
├── structures/
│   └── ruby_shrine.nbt
└── textures/
    ├── item/ruby.png
    ├── block/ruby_ore.png, ruby_log.png, ruby_leaves.png, ruby_flower.png
    ├── entity/ruby_golem.png
    └── particle/ruby_spark_0.png, ruby_spark_1.png

mods/rubies/

모드를 선언하고 그 구성 요소를 나열하기만 합니다. 읽어 보면 무엇이 어디에 있는지 알 수 있습니다.

mods/rubies/main.craftCraftScript
mod "rubies" { name: "Rubies", version: "1.0.0", author: "you" }

// What exists
import "content/items"
import "content/ores"
import "content/world"
import "content/dimensions"
import "content/creatures"
import "content/sounds"
import "content/particles"
import "content/recipes"

// What happens
import "events/mining"
import "events/hunting"
import "events/welcome"
import "commands/realm"
import "commands/rubies"
import "commands/shop"

import의 작동 방식

  • import "content/items"는 그 파일을 한 번 실행합니다. 거기서 선언한 아이템, 블록, 이벤트, 명령어는 main.craft에 있는 것처럼 모드의 일부가 됩니다.
  • import { give_rubies } from "/lib/rewards"는 특정 이름을 가져옵니다. /로 시작하는 경로는 모드 폴더 기준이므로 어느 하위 폴더에서든 똑같이 작동합니다. /가 없으면 가져오는 파일 기준의 상대 경로입니다. .craft 확장자는 생략할 수 있습니다.
  • import "lib/rewards" as rewards는 모듈 전체를 하나의 이름으로 가져옵니다(rewards.give_rubies(p, 1)). as가 없으면 이름은 파일 이름이므로, 같은 이름의 파일 두 개를 가져오면 서로 덮어씁니다. 다른 이름을 쓰거나 as를 사용하세요.
  • 어떤 경로도 모드 폴더 밖으로 나갈 수 없으며, 순환 import는 위치를 알려 주는 오류를 냅니다.

공유 로직

두 파일에서 쓰는 함수는 lib/에 둡니다. 로직에 자체 상태가 있다면(경제, 퀘스트 대기열) 메서드를 가진 클래스를 역시 lib/에 두세요. CraftScript를 참고하세요. 한 파일에서만 쓰는 변수는 그 파일에 두되, 재시작하면 사라진다는 점을 기억하세요. 유지되어야 하는 것은 저장 데이터에 넣습니다.

설정과 저장 데이터는 별도 파일로

모드의 나머지 부분이 사용하지만 내부를 알 필요는 없는 두 가지:

  • 설정(루트의 settings.craft): 모든 기본값을 담은 config(…) 하나. 다른 파일은 SETTINGS를 가져와 필요한 것을 읽으며, 어느 파일도 밸런스 숫자를 직접 적지 않습니다.
  • 저장 데이터(lib/storage.craft): 키와 visits(p), add_visit(p) 같은 작은 함수 몇 개. 방문을 세는 코드는 add_visit(player)를 호출할 뿐, rubies.visits 키가 있다는 것을 모릅니다. 언젠가 바뀌면 한 곳만 바꾸면 됩니다.

이 파일의 이름을 config.craft로 짓지 마세요. as 없이 가져오면 그 이름이 config 함수를 가립니다.

Kotlin에서

권장 구조텍스트
rubies/
├── build.gradle.kts
├── settings.gradle.kts
└── src/main/
    ├── kotlin/com/example/rubies/
    │   ├── RubiesMod.kt         entry point: only wires the parts
    │   ├── RubiesConfig.kt      player settings
    │   ├── content/
    │   │   ├── Items.kt
    │   │   ├── Blocks.kt
    │   │   ├── Creatures.kt
    │   │   ├── Sounds.kt
    │   │   ├── Particles.kt
    │   │   └── Recipes.kt
    │   ├── storage/
    │   │   └── Visits.kt        saved data: the keys, in one place
    │   ├── events/
    │   │   ├── MiningEvents.kt
    │   │   ├── CombatEvents.kt
    │   │   └── WelcomeEvents.kt
    │   ├── ui/
    │   │   └── ShopForm.kt       forms: what the player sees
    │   └── commands/
    │       ├── RubyCommand.kt
    │       └── ShopCommand.kt
    └── resources/
        ├── crafting.mod.json
        └── assets/rubies/
            ├── sounds/ruby/chime.ogg
            └── textures/
                ├── item/ruby.png
                ├── entity/ruby_golem.png
                └── particle/ruby_spark_0.png, ruby_spark_1.png

src/main/kotlin/com/example/rubies/

진입 클래스는 스스로 아무것도 하지 않습니다. 공유할 것을 만들고 각 부분을 호출합니다.

src/main/kotlin/com/example/rubies/RubiesMod.ktKotlin
package com.example.rubies

class RubiesMod : CraftingMod {
    override fun onLoad(ctx: ModContext) {
        val config = Crafting.configs.load(RubiesConfig())
        val content = ContentBuilder(ctx.metadata.id, Crafting.registries)

        Items.register(content)
        Blocks.register(content)
        Creatures.register(content)
        Sounds.register(content)
        Particles.register(content)
        Recipes.register(content)

        MiningEvents.register(config)
        CombatEvents.register(config)
        WelcomeEvents.register()
        RubyCommand.register()
        ShopCommand.register()
    }
}

읽기 쉽도록 각 파일의 import 줄은 생략했습니다.

  • 주제별 패키지: content, events, commands. 파일마다 register 함수를 가진 object 하나.
  • 모드 클래스는 연결만 합니다. 여러 부분이 공유하는 것(설정, ContentBuilder)을 만들어 각 부분에 전달합니다. 어떤 부분도 스스로 무언가를 찾아오지 않으므로, 각 부분은 그 파일만 읽어도 이해할 수 있습니다.
  • id를 상수로, 그것을 등록하는 object 안에(Items.RUBY). 오타는 컴파일 오류가 됩니다.
  • 저장 데이터는 storage/에: 각 키를 Visits.of(player), Visits.add(player) 같은 함수를 가진 object에 둡니다. 설정은 Config 클래스에 두고, 필요한 곳에 전달합니다.

Java에서

구조는 같고, object 대신 클래스를 씁니다. API는 Crafting.INSTANCE를 통해 사용하며, 리스너는 Unit.INSTANCE를 반환합니다.

src/main/java/com/example/rubies/RubiesMod.javaJava
package com.example.rubies;

public final class RubiesMod implements CraftingMod {
    @Override
    public void onLoad(ModContext ctx) {
        ContentBuilder content = new ContentBuilder(ctx.getMetadata().getId(), Crafting.INSTANCE.getRegistries());
        Items.register(content);
        WelcomeEvents.register();
    }
}
src/main/java/com/example/rubies/events/WelcomeEvents.javaJava
package com.example.rubies.events;

public final class WelcomeEvents {
    public static void register() {
        Crafting.INSTANCE.getEvents().subscribe(PlayerJoinEvent.class, EventPriority.NORMAL, false, null, e -> {
            e.getPlayer().send("&aWelcome, " + e.getPlayer().getName() + "!");
            return Unit.INSTANCE;
        });
    }
}

완성된 예제: Frostlands

Frostlands는 부분별로 정리한 하나의 CraftScript 모드로, 이 가이드에서 설명하는 대부분을 사용합니다. main.craft는 부분들만 나열합니다:

mods/frostlands/main.craftCraftScript
mod "frostlands" { name: "Frostlands", version: "1.0.0" }

// One file per part of the mod. Each part only declares what it is about.
import "content/materials"   // frost crystals: their ore, the recipes made from them
import "content/gear"        // armor, blade, wand, the frozen effect and the frost bolt
import "content/blocks"      // the gate, a lamp, an ice chest, a frost pad
import "world/frostlands"    // the frozen dimension
import "creatures/drake"     // tame, ride and fly a frost drake
import "creatures/wisp"      // wisps that shoot frost bolts at the undead
import "creatures/yak"       // yaks to breed
import "controls/keys"       // dash and roar keys
import "events/welcome"      // a first greeting
import "commands/kit"        // /frostkit for trying it all out
  • content/materials: 수정, 그 광석, 그것으로 만드는 조합법.
  • content/gear: 방어구, 얼리는 칼날, 쿨다운과 함께 모드의 투사체를 쏘는 지팡이, 모드의 효과.
  • content/blocks: 포털 문, 클릭이나 레드스톤으로 켜지는 램프, 얼음 상자, 점프 패드.
  • world/frostlands: 차원.
  • creatures/drake: 자체 모델과 부드러운 애니메이션을 가진 몹. 길들이고, 타고, 날 수 있으며, 키를 누르면 울부짖습니다.
  • creatures/wisp: 언데드를 쏘는 위습.
  • creatures/yak: 번식하는 야크.
  • controls/keys: 돌진과 울부짖음 키.
  • events/welcome: 첫 인사.
  • commands/kit: 모든 것을 시험해 보는 명령어.

몹 파일에는 그 몹의 선언만 들어갑니다. 이것이 드레이크입니다:

mods/frostlands/creatures/drake.craftCraftScript
creature "drake" {
    body: "minecraft:bee"
    model: "entity/drake"
    texture: "entity/drake"
    size: [1.4, 1.1]
    health: 30
    speed: 0.4
    tame_item: "frostlands:frost_crystal"
    rideable: true
    behaviour: [{ follow_owner: 1.4, distance: 10 }, { fly_around: 1 }, { look_at_players: 8 }, "look_around"]
    spawns: [{ biomes: ["minecraft:ice_spikes"], weight: 3, group: [1, 1] }]
}

각 속성은 몹을 참고하세요.

나눌 때

신호 할 일
파일이 150줄 정도를 넘어갑니다 섞여 있는 주제를 찾아 분리합니다.
같은 함수를 두 파일에 복사합니다 lib/로 옮기고 가져옵니다.
밸런스 숫자가 여러 곳에 나옵니다 settings.craft(Kotlin에서는 Config 클래스)의 설정으로, 바뀌면 안 되는 값이라면 상수로 만듭니다.
같은 데이터 키를 여러 파일에 적습니다 읽기와 저장용 함수 두 개와 함께 lib/storage.craft로 옮깁니다.
이벤트 하나가 서로 다른 세 가지 일을 합니다 같은 이벤트에 리스너 세 개를, 각각 주제에 맞는 파일에 둡니다.