Руководства
Как организовать мод
Мод пишут один раз, а поддерживают годами. Больше всего в поддержке помогает не язык, а то, где что лежит. Это руководство предлагает одну структуру для CraftScript и одну для Kotlin и объясняет почему.
Четыре правила
- Одна ответственность на файл. Один файл для руд, другой для рецептов, ещё по одному на каждую команду. Если для описания файла тебе нужно слово «и», скорее всего, это два файла.
- Группируй по теме, а не по размеру. Папки вроде
content/,events/,commands/,ui/иlib/. Любой, кто откроет мод, знает, где искать, не читая код. - Держи данные отдельно от поведения. То, что существует (предметы, блоки, рецепты), — это список свойств. То, что происходит (события, команды), — это код. Если их смешать, ради изменения числа придётся читать логику.
- Точка входа только связывает части.
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.pngmods/rubies/
Только объявляет мод и перечисляет его части. Прочитав его, ты знаешь, что есть и где.
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.import { give_rubies } from "/lib/rewards"подключает конкретные имена. Путь, начинающийся с/, отсчитывается от папки мода, поэтому одинаково работает из любой подпапки; без/он отсчитывается от импортирующего файла. Расширение.craftнеобязательно.import "lib/rewards" as rewardsподключает весь модуль под именем (rewards.give_rubies(p, 1)). Безasимя берётся из имени файла, поэтому два импортированных файла с одинаковым именем перезаписывают друг друга: используй разные имена илиas.- Ни один путь не может выйти за пределы папки мода, а циклический импорт даёт ошибку, которая говорит, где он.
Общая логика
Функция, которой пользуются два файла, живёт в 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.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. Одинobjectна файл с функциейregister. - Класс мода только связывает части: он создаёт то, что разделяют несколько частей (конфигурацию,
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 — это один мод на CraftScript, разбитый по частям, и он использует почти всё, что описано в этом руководстве. Его 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: броня, замораживающий клинок, жезл, стреляющий снарядом мода с перезарядкой, и эффект мода.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 строк | Найди темы, которые он смешивает, и раздели их. |
| Ты копируешь одну и ту же функцию в два файла | Перенеси её в lib/ и импортируй. |
| Число баланса встречается в нескольких местах | Настройка в settings.craft (или в классе Config на Kotlin) либо константа, если менять его не должны. |
| Ты пишешь один и тот же ключ данных в нескольких файлах | Перенеси его в lib/storage.craft с двумя функциями для чтения и сохранения. |
| Событие делает три разные вещи | Три слушателя одного события, каждый в файле своей темы. |