Guías

Organizar un mod

Un mod se escribe una vez y se mantiene durante años. Lo que más ayuda a mantenerlo no es el lenguaje: es dónde está cada cosa. Esta guía propone una estructura para CraftScript y otra para Kotlin, y explica por qué.

Cuatro reglas

  1. Una responsabilidad por archivo. Un archivo para los minerales, otro para las recetas, otro para cada comando. Si para describir un archivo necesitas la palabra “y”, probablemente son dos.
  2. Agrupa por tema, no por tamaño. Carpetas como content/, events/, commands/, ui/ y lib/. Quien abre el mod sabe dónde mirar sin leer el código.
  3. Separa los datos del comportamiento. Lo que existe (ítems, bloques, recetas) es una lista de propiedades. Lo que pasa (eventos, comandos) es código. Mezclarlos hace que cambiar un número obligue a leer lógica.
  4. Un punto de entrada que solo conecta. main.craft o la clase del mod dicen qué partes hay. No contienen reglas de juego.

Con esto, cambiar el equilibrio del mod es tocar content/recipes.craft; arreglar un comando es abrir commands/; y un error en un evento no está escondido entre doscientas líneas de bloques.

En CraftScript

Estructura recomendadaTexto
mods/rubies/
├── main.craft               el mod y la lista de sus partes
├── settings.craft           ajustes del dueño del servidor
├── content/                 datos: qué existe
│   ├── items.craft
│   ├── ores.craft
│   ├── world.craft
│   ├── dimensions.craft
│   ├── creatures.craft
│   ├── sounds.craft
│   ├── particles.craft
│   └── recipes.craft
├── events/                  comportamiento: qué pasa
│   ├── mining.craft
│   ├── hunting.craft
│   └── welcome.craft
├── ui/                      formularios: qué ve el jugador
│   └── shop.craft
├── commands/
│   ├── realm.craft
│   ├── rubies.craft
│   └── shop.craft
├── lib/                     lógica compartida
│   ├── rewards.craft
│   └── storage.craft        datos guardados: las claves, en un solo sitio
├── 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/

Solo declara el mod y enumera sus partes. Leyéndolo sabes qué hay y dónde.

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"

Cómo funcionan los imports

  • import "content/items" ejecuta ese archivo una sola vez. Los ítems, bloques, eventos y comandos que declara pasan a formar parte del mod, como si estuvieran en main.craft.
  • import { give_rubies } from "/lib/rewards" trae nombres concretos. Una ruta que empieza por / se lee desde la carpeta del mod, así funciona igual desde cualquier subcarpeta; sin /, es relativa al archivo que importa. La extensión .craft es opcional.
  • import "lib/rewards" as rewards trae el módulo entero con un nombre (rewards.give_rubies(p, 1)). Sin as, el nombre es el del archivo, así que dos archivos importados con el mismo nombre se pisan: usa nombres distintos o as.
  • Ninguna ruta puede salir de la carpeta del mod, y un import circular da un error que dice dónde.

Lógica compartida

Una función que usan dos archivos va a lib/. Si la lógica tiene estado propio (una economía, una cola de misiones), conviene una clase con sus métodos, también en lib/; ver CraftScript. Una variable que solo usa un archivo se queda en ese archivo, pero recuerda que se pierde al reiniciar: lo que deba durar va en los datos guardados.

Ajustes y datos guardados, cada uno en su archivo

Dos cosas que el resto del mod usa pero no debería conocer por dentro:

  • Los ajustes (settings.craft, en la raíz): un solo config(…) con todos los valores por defecto. Los demás archivos importan SETTINGS y leen lo que necesitan; ninguno escribe números de equilibrio a mano.
  • Los datos guardados (lib/storage.craft): las claves y unas funciones pequeñas, como visits(p) y add_visit(p). Quien cuenta visitas llama a add_visit(player) y no sabe que existe la clave rubies.visits. Si un día cambia, se cambia en un sitio.

No llames a ese archivo config.craft: al importarlo sin as, su nombre taparía la función config.

En Kotlin

Estructura recomendadaTexto
rubies/
├── build.gradle.kts
├── settings.gradle.kts
└── src/main/
    ├── kotlin/com/example/rubies/
    │   ├── RubiesMod.kt         entrada: solo conecta las partes
    │   ├── RubiesConfig.kt      ajustes del jugador
    │   ├── content/
    │   │   ├── Items.kt
    │   │   ├── Blocks.kt
    │   │   ├── Creatures.kt
    │   │   ├── Sounds.kt
    │   │   ├── Particles.kt
    │   │   └── Recipes.kt
    │   ├── storage/
    │   │   └── Visits.kt        datos guardados: las claves, en un solo sitio
    │   ├── events/
    │   │   ├── MiningEvents.kt
    │   │   ├── CombatEvents.kt
    │   │   └── WelcomeEvents.kt
    │   ├── ui/
    │   │   └── ShopForm.kt       formularios: qué ve el jugador
    │   └── 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/

La clase de entrada no hace nada por sí misma: crea lo compartido y llama a 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()
    }
}

Los import de cada archivo se omiten para que se lea mejor.

  • Paquetes por tema: content, events, commands. Un object por archivo con una función register.
  • La clase del mod solo conecta: crea lo que comparten varias partes (la configuración, el ContentBuilder) y se lo pasa. Ninguna parte va a buscar nada por su cuenta, así que cada una se entiende leyendo solo su archivo.
  • Los ids, como constantes en el objeto que los registra (Items.RUBY). Un error de escritura se convierte en un error de compilación.
  • Los datos guardados, en storage/: cada clave en un objeto con funciones como Visits.of(player) y Visits.add(player). Los ajustes, en su clase Config, que se pasa a quien los necesita.

En Java

La estructura es la misma, con clases en lugar de object. La API se usa a través de Crafting.INSTANCE, y los listeners devuelven 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;
        });
    }
}

Un ejemplo completo: Frostlands

Frostlands es un único mod de CraftScript organizado por partes, y usa casi todo lo que describe esta guía. Su main.craft solo lista las 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: un cristal, su mena y las recetas hechas con él.
  • content/gear: armadura, una hoja que congela, una varita que dispara un proyectil del mod con enfriamiento, y un efecto del mod.
  • content/blocks: un portal, una lámpara que se enciende con un clic o con redstone, un cofre de hielo y una plataforma de salto.
  • world/frostlands: una dimensión.
  • creatures/drake: una criatura con modelo propio y animaciones suaves; se puede domar, montar y volar con ella, y ruge con una tecla.
  • creatures/wisp: fuegos fatuos que disparan a los no muertos.
  • creatures/yak: yaks que se crían.
  • controls/keys: las teclas de impulso y de rugido.
  • events/welcome: un primer saludo.
  • commands/kit: un comando para probarlo todo.

Un archivo de criatura declara solo esa criatura. Este es el 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] }]
}

Mira Criaturas para cada una de esas propiedades.

Cuándo dividir

Señal Qué hacer
Un archivo pasa de unas 150 líneas Busca los temas que mezcla y sepáralos.
Copias la misma función en dos archivos Muévela a lib/ e impórtala.
Un número de equilibrio aparece en varios sitios Un ajuste en settings.craft (o en la clase Config en Kotlin), o una constante si no debe poder cambiarse.
Escribes la misma clave de datos en varios archivos Llévala a lib/storage.craft con dos funciones para leer y guardar.
Un evento hace tres cosas distintas Tres listeners del mismo evento, cada uno en el archivo de su tema.