Guias
Organizando um mod
Você escreve um mod uma vez e o mantém por anos. O que mais ajuda na manutenção não é a linguagem: é onde cada coisa fica. Este guia sugere uma estrutura para CraftScript e outra para Kotlin, e explica por quê.
Quatro regras
- Uma responsabilidade por arquivo. Um arquivo para os minérios, outro para as receitas, outro para cada comando. Se você precisa da palavra “e” para descrever um arquivo, provavelmente são dois arquivos.
- Agrupe por tema, não por tamanho. Pastas como
content/,events/,commands/,ui/elib/. Quem abre o mod sabe onde procurar sem ler o código. - Mantenha os dados separados do comportamento. O que existe (itens, blocos, receitas) é uma lista de propriedades. O que acontece (eventos, comandos) é código. Misturar os dois faz com que mudar um número obrigue você a ler lógica.
- Um ponto de entrada que só conecta as partes.
main.craftou a classe do mod dizem quais partes existem. Não contêm regras do jogo.
Com isso, rebalancear o mod é editar content/recipes.craft; corrigir um comando é abrir commands/; e um bug em um evento não fica escondido entre duzentas linhas de blocos.
Em 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/
Só declara o mod e lista suas partes. Lendo este arquivo, você sabe o que existe e onde.
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"Como os imports funcionam
import "content/items"executa esse arquivo uma vez. Os itens, blocos, eventos e comandos que ele declara passam a fazer parte do mod, como se estivessem emmain.craft.import { give_rubies } from "/lib/rewards"traz nomes específicos. Um caminho que começa com/é lido a partir da pasta do mod, então funciona igual em qualquer subpasta; sem/, é relativo ao arquivo que importa. A extensão.crafté opcional.import "lib/rewards" as rewardstraz o módulo inteiro com um nome (rewards.give_rubies(p, 1)). Semas, o nome é o nome do arquivo, então dois arquivos importados com o mesmo nome se sobrescrevem: use nomes diferentes ouas.- Nenhum caminho pode sair da pasta do mod, e um import circular dá um erro que diz onde.
Lógica compartilhada
Uma função usada por dois arquivos vai em lib/. Se a lógica tem estado próprio (uma economia, uma fila de missões), use uma classe com seus métodos, também em lib/; veja CraftScript. Uma variável usada por um só arquivo fica nesse arquivo, mas lembre que ela se perde ao reiniciar: o que precisa durar vai nos dados salvos.
Ajustes e dados salvos em arquivos próprios
Duas coisas que o resto do mod usa, mas cujos detalhes internos não deveria conhecer:
- Ajustes (
settings.craft, na raiz): um únicoconfig(…)com todos os valores padrão. Os outros arquivos importamSETTINGSe leem o que precisam; nenhum deles escreve números de balanceamento direto no código. - Dados salvos (
lib/storage.craft): as chaves e algumas funções pequenas, comovisits(p)eadd_visit(p). O código que conta visitas chamaadd_visit(player)e não sabe que a chaverubies.visitsexiste. Se um dia ela mudar, você muda em um só lugar.
Não chame esse arquivo de config.craft: importado sem as, o nome dele esconderia a função config.
Em 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/
A classe de entrada não faz nada sozinha: cria o que é compartilhado e chama cada parte.
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()
}
}As linhas de import de cada arquivo foram omitidas para facilitar a leitura.
- Pacotes por tema:
content,events,commands. Umobjectpor arquivo com uma funçãoregister. - A classe do mod só conecta as partes: cria o que várias partes compartilham (a configuração, o
ContentBuilder) e o repassa a elas. Nenhuma parte busca nada por conta própria, então você entende cada uma lendo só o arquivo dela. - Ids como constantes no object que os registra (
Items.RUBY). Um erro de digitação vira um erro de compilação. - Dados salvos em
storage/: cada chave em um object com funções comoVisits.of(player)eVisits.add(player). Os ajustes ficam na classeConfig, que é repassada a quem precisar deles.
Em Java
A estrutura é a mesma, com classes em vez de object. Você usa a API por meio de Crafting.INSTANCE, e os listeners retornam 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;
});
}
}Um exemplo completo: Frostlands
Frostlands é um único mod em CraftScript organizado por partes, e usa quase tudo o que este guia descreve. O seu main.craft só lista as partes:
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: um cristal, o seu minério e as receitas feitas com ele.content/gear: armadura, uma lâmina congelante, uma varinha que dispara um projétil do mod com tempo de recarga e um efeito do mod.content/blocks: um portal, uma lâmpada acesa por um clique ou por redstone, um baú de gelo e uma plataforma de salto.world/frostlands: uma dimensão.creatures/drake: uma criatura com modelo próprio e animações suaves; dá para domesticá-la, montá-la e voar com ela, e ela ruge ao apertar uma tecla.creatures/wisp: fogos-fátuos que atiram nos mortos-vivos.creatures/yak: iaques que se reproduzem.controls/keys: as teclas de investida e de rugido.events/welcome: uma primeira saudação.commands/kit: um comando para testar tudo.
Um arquivo de criatura declara apenas essa criatura. Este é o drake:
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] }]
}Veja Criaturas para cada uma dessas propriedades.
Quando dividir
| Sinal | O que fazer |
|---|---|
| Um arquivo passa de umas 150 linhas | Encontre os temas que ele mistura e separe-os. |
| Você copia a mesma função em dois arquivos | Mova-a para lib/ e importe-a. |
| Um número de balanceamento aparece em vários lugares | Um ajuste em settings.craft (ou na classe Config em Kotlin), ou uma constante se não deve ser alterável. |
| Você escreve a mesma chave de dados em vários arquivos | Mova-a para lib/storage.craft com duas funções para ler e salvar. |
| Um evento faz três coisas diferentes | Três listeners para o mesmo evento, cada um no arquivo do seu tema. |