Руководства

Как организовать мод

Мод пишут один раз, а поддерживают годами. Больше всего в поддержке помогает не язык, а то, где что лежит. Это руководство предлагает одну структуру для 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 "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.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. Один object на файл с функцией register.
  • Класс мода только связывает части: он создаёт то, что разделяют несколько частей (конфигурацию, 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 — это один мод на 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 (или в классе Config на Kotlin) либо константа, если менять его не должны.
Ты пишешь один и тот же ключ данных в нескольких файлах Перенеси его в lib/storage.craft с двумя функциями для чтения и сохранения.
Событие делает три разные вещи Три слушателя одного события, каждый в файле своей темы.