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