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
- 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.
- Agrupa por tema, no por tamaño. Carpetas como
content/,events/,commands/,ui/ylib/. Quien abre el mod sabe dónde mirar sin leer el código. - 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.
- Un punto de entrada que solo conecta.
main.crafto 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
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.pngmods/rubies/
Solo declara el mod y enumera sus partes. Leyéndolo sabes qué hay y dónde.
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 enmain.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.craftes opcional.import "lib/rewards" as rewardstrae el módulo entero con un nombre (rewards.give_rubies(p, 1)). Sinas, el nombre es el del archivo, así que dos archivos importados con el mismo nombre se pisan: usa nombres distintos oas.- 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 soloconfig(…)con todos los valores por defecto. Los demás archivos importanSETTINGSy 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, comovisits(p)yadd_visit(p). Quien cuenta visitas llama aadd_visit(player)y no sabe que existe la claverubies.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
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.pngsrc/main/kotlin/com/example/rubies/
La clase de entrada no hace nada por sí misma: crea lo compartido y llama a 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()
}
}Los import de cada archivo se omiten para que se lea mejor.
- Paquetes por tema:
content,events,commands. Unobjectpor archivo con una funciónregister. - 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 comoVisits.of(player)yVisits.add(player). Los ajustes, en su claseConfig, 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:
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;
});
}
}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:
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: 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:
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. |