Посібники

Як організувати мод

Мод пишуть один раз, а підтримують роками. Найбільше в підтримці допомагає не мова, а те, де що лежить. Цей посібник пропонує одну структуру для 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 з двома функціями для читання й збереження.
Подія робить три різні речі Три слухачі тієї самої події, кожен у файлі своєї теми.