ガイド
Mod の整理
Mod は一度書いたら、何年もメンテナンスするものです。メンテナンスで一番役に立つのは言語ではなく、何がどこにあるかです。このガイドでは CraftScript と Kotlin それぞれの構成を提案し、その理由を説明します。
4 つのルール
- 1 つのファイルに 1 つの責務。鉱石に 1 ファイル、レシピに別の 1 ファイル、コマンドごとにさらに 1 ファイル。ファイルを説明するのに「と」が必要なら、おそらく 2 つのファイルにすべきです。
- 大きさではなくテーマでまとめる。
content/、events/、commands/、ui/、lib/のようなフォルダー。Mod を開いた人は、コードを読まなくてもどこを見ればよいかがわかります。 - データと振る舞いを分ける。存在するもの(アイテム、ブロック、レシピ)はプロパティの一覧です。起きること(イベント、コマンド)はコードです。混ぜてしまうと、数値を 1 つ変えるだけでロジックを読む羽目になります。
- つなぐだけのエントリーポイント。
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.pngmods/rubies/
Mod を宣言し、その構成要素を並べるだけ。読めば、何がどこにあるかがわかります。
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.pngsrc/main/kotlin/com/example/rubies/
エントリークラス自体は何もしません。共有するものを作り、各部分を呼び出します。
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 を返します。
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();
}
}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 はパートを並べるだけです:
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 outcontent/materials:クリスタル、その鉱石、それから作るレシピ。content/gear:防具、凍らせる刃、クールダウン付きで Mod の飛び道具を撃つ杖、Mod のエフェクト。content/blocks:ポータルのゲート、クリックまたはレッドストーンで点くランプ、氷のチェスト、ジャンプパッド。world/frostlands:ディメンション。creatures/drake:独自のモデルとなめらかなアニメーションを持つモブ。手なずけて、乗って、飛べて、キーで吠えます。creatures/wisp:アンデッドを撃つウィスプ。creatures/yak:繁殖するヤク。controls/keys:ダッシュと咆哮のキー。events/welcome:最初のあいさつ。commands/kit:すべてを試すためのコマンド。
モブのファイルには、そのモブの宣言だけを書きます。これがドレイクです:
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 つのリスナーを作り、それぞれのテーマのファイルに置きます。 |