ガイド

Mod の整理

Mod は一度書いたら、何年もメンテナンスするものです。メンテナンスで一番役に立つのは言語ではなく、何がどこにあるかです。このガイドでは CraftScript と Kotlin それぞれの構成を提案し、その理由を説明します。

4 つのルール

  1. 1 つのファイルに 1 つの責務。鉱石に 1 ファイル、レシピに別の 1 ファイル、コマンドごとにさらに 1 ファイル。ファイルを説明するのに「と」が必要なら、おそらく 2 つのファイルにすべきです。
  2. 大きさではなくテーマでまとめる。content/、events/、commands/、ui/、lib/ のようなフォルダー。Mod を開いた人は、コードを読まなくてもどこを見ればよいかがわかります。
  3. データと振る舞いを分ける。存在するもの(アイテム、ブロック、レシピ)はプロパティの一覧です。起きること(イベント、コマンド)はコードです。混ぜてしまうと、数値を 1 つ変えるだけでロジックを読む羽目になります。
  4. つなぐだけのエントリーポイント。main.craft や Mod のクラスは、どんな部分があるかを書くだけです。ゲームのルールは含みません。

こうしておけば、Mod のバランス調整は content/recipes.craft の編集で済み、コマンドの修正は commands/ を開けば済み、イベントのバグが 200 行のブロック定義の中に埋もれることもありません。

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/

Mod を宣言し、その構成要素を並べるだけ。読めば、何がどこにあるかがわかります。

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 "content/items" はそのファイルを一度実行します。そこで宣言されたアイテム、ブロック、イベント、コマンドは、main.craft にあるかのように Mod の一部になります。
  • import { give_rubies } from "/lib/rewards" は特定の名前を取り込みます。/ で始まるパスは Mod のフォルダーから読まれるので、どのサブフォルダーからでも同じように動きます。/ がなければ、インポートするファイルからの相対パスです。.craft 拡張子は省略できます。
  • import "lib/rewards" as rewards はモジュール全体を名前付きで取り込みます(rewards.give_rubies(p, 1))。as がなければ名前はファイル名になるので、同じ名前の 2 つのファイルをインポートすると互いに上書きされます。別の名前にするか、as を使ってください。
  • どのパスも Mod のフォルダーの外には出られません。循環インポートは、その場所を示すエラーになります。

共有するロジック

2 つのファイルで使う関数は lib/ に置きます。ロジックが独自の状態(経済、クエストのキューなど)を持つなら、メソッドを持つクラスを使い、これも lib/ に置きます。CraftScript を参照してください。1 つのファイルだけで使う変数はそのファイルに置きますが、再起動すると失われることを忘れずに。残す必要があるものはセーブデータに入れます。

設定とセーブデータは専用のファイルに

Mod の他の部分が使うけれど、中身を知るべきではないものが 2 つあります。

  • 設定(ルートの settings.craft):すべてのデフォルト値を持つ 1 つの config(…)。他のファイルは SETTINGS をインポートして必要なものを読みます。バランスの数値を直接書き込むファイルはありません。
  • セーブデータ(lib/storage.craft):キーと、visits(p) や add_visit(p) のような小さな関数。訪問を数えるコードは add_visit(player) を呼ぶだけで、rubies.visits というキーがあることを知りません。いつか変わっても、変更は 1 か所で済みます。

このファイルを 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。1 ファイルに 1 つの object と register 関数。
  • Mod のクラスはつなぐだけ:複数の部分で共有するもの(設定、ContentBuilder)を作り、それらに渡します。どの部分も自分で何かを取りに行かないので、そのファイルを読むだけで各部分を理解できます。
  • id は定数に。登録するオブジェクトに置きます(Items.RUBY)。タイプミスはコンパイルエラーになります。
  • セーブデータは storage/ に:各キーを、Visits.of(player) や Visits.add(player) のような関数を持つオブジェクトに。設定は 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 は、パートごとに整理された 1 つの CraftScript Mod で、このガイドで説明していることの大半を使っています。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:防具、凍らせる刃、クールダウン付きで Mod の飛び道具を撃つ杖、Mod のエフェクト。
  • 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 行ほどを超えた 混ざっているテーマを見つけて分けます。
同じ関数を 2 つのファイルにコピーしている lib/ に移してインポートします。
バランスの数値が複数の場所にある settings.craft(Kotlin では Config クラス)の設定にするか、変更できるべきでないなら定数にします。
同じデータキーを複数のファイルに書いている 読み込みと保存の 2 つの関数とともに lib/storage.craft に移します。
1 つのイベントが 3 つの異なることをしている 同じイベントに 3 つのリスナーを作り、それぞれのテーマのファイルに置きます。