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

  1. 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.
  2. Agrupe por tema, não por tamanho. Pastas como content/, events/, commands/, ui/ e lib/. Quem abre o mod sabe onde procurar sem ler o código.
  3. 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.
  4. Um ponto de entrada que só conecta as partes. main.craft ou 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

Estrutura recomendadaTexto
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/

Só declara o mod e lista suas partes. Lendo este arquivo, você sabe o que existe e onde.

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"

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 em main.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 rewards traz o módulo inteiro com um nome (rewards.give_rubies(p, 1)). Sem as, o nome é o nome do arquivo, então dois arquivos importados com o mesmo nome se sobrescrevem: use nomes diferentes ou as.
  • 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 único config(…) com todos os valores padrão. Os outros arquivos importam SETTINGS e 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, como visits(p) e add_visit(p). O código que conta visitas chama add_visit(player) e não sabe que a chave rubies.visits existe. 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

Estrutura recomendadaTexto
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/

A classe de entrada não faz nada sozinha: cria o que é compartilhado e chama cada parte.

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()
    }
}

As linhas de import de cada arquivo foram omitidas para facilitar a leitura.

  • Pacotes por tema: content, events, commands. Um object por arquivo com uma função register.
  • 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 como Visits.of(player) e Visits.add(player). Os ajustes ficam na classe Config, 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:

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;
        });
    }
}

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:

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: 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:

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] }]
}

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.