API
Generación por código
Cuando declarar no basta, tu código construye el mundo a medida que se crean los chunks: features, estructuras enteras, la forma del terreno, qué bioma va dónde, cuevas y los bloques del suelo.
Qué puedes escribir
| Qué | Kotlin | CraftScript |
|---|---|---|
| Features: pinchos, plantas en paredes, rocas, cualquier cosa colocada en un chunk | feature("…") { place { } } |
No |
| Estructuras construidas por código, repartidas en varios chunks | structure("…") { plan { }; build { } } |
Sí (plan, build) |
| El terreno de una dimensión | terrain("minecraft:the_end") { } |
Sí (terrain) |
| Qué bioma va dónde | biomes("minecraft:the_end") { } |
Sí (biome_map) |
| Carvers: cuevas y túneles | carver("…", chance) { } |
Sí (carver) |
| El suelo de un bioma, bloque a bloque | biome("…") { surface { } } |
Sí (surface: fun(at)) |
| Etiquetas de bloques, ítems y biomas | tag(TagKind.BIOME, "…", …) |
No |
Todo lo de esta página se ejecuta mientras se crean los chunks, en varios hilos a la vez. Usa los números aleatorios que te dan (o la semilla del mundo) en lugar de los tuyos: así la misma semilla crea el mismo mundo. En Kotlin, mantén seguro para hilos todo lo que tu código comparta entre llamadas. En CraftScript, los miembros de random no están disponibles; saca la variedad de las coordenadas, de la semilla o del sitio de la estructura.
Features
Una feature es algo que tu código construye en un chunk que se está creando, en los biomas que indiques. Crafting elige los lugares, como hace el juego con sus propias features; tu función place construye allí y devuelve true cuando construyó algo. Las features se escriben en Kotlin.
object CrystalSpikes {
private val CRYSTAL = Identifier.parse("mymod:crystal")
fun register(content: ContentBuilder) {
content.feature("crystal_spike") {
biome("#minecraft:is_overworld")
count = 2..5
heights = -54..30
on = FeatureOn.FLOOR
underground = true
step = FeatureStep.UNDERGROUND
place { world ->
val (x, y, z) = Triple(world.x, world.y, world.z)
if (!world.isIn(x, y - 1, z, "#minecraft:base_stone_overworld")) return@place false
val height = 2 + world.random.nextInt(4)
for (dy in 0 until height) {
if (!world.isAir(x, y + dy, z)) break
world.set(x, y + dy, z, CRYSTAL)
}
true
}
}
}
}| Opción | Valores | Por defecto |
|---|---|---|
biome(…) |
biomas donde se coloca, por id o #etiqueta; al menos uno |
— |
count |
intentos por chunk, un rango: 2..5 |
1..1 |
chance |
solo en uno de cada tantos chunks | 1 |
heights |
cada intento a una altura al azar dentro de este rango | null: el fondo del mundo (tu código encuentra su lugar) |
spread |
true: cada intento en una columna al azar del chunk; false: en la esquina del chunk, y tu código elige desde ahí |
true |
on |
FeatureOn.FLOOR o CEILING: desde su altura, se mueve al aire junto al suelo o techo más cercano, hasta 32 bloques; si no hay ninguno, no se coloca |
null |
underground |
true: solo bajo la superficie, al menos 2 bloques por debajo (en cuevas) |
false |
everyFloor |
true: en cada suelo de cada columna elegida, la superficie y el suelo de cada cueva; count son las columnas por suelo |
false |
tries |
con everyFloor, cuántas veces se recorren los suelos: cada vez count columnas más en cada suelo, así un terreno profundo se alcanza con más intentos |
1..1 |
step |
cuándo se hace, entre los pasos que llenan un chunk: LAKES, SURFACE, ORES, UNDERGROUND (lo que decora las cuevas), VEGETATION |
VEGETATION |
Con everyFloor, chance, heights y spread no se usan: los lugares son los propios suelos.
content.feature("glow_tufts") {
biome("mymod:crystal_glade")
everyFloor = true
count = 4..8
tries = 1..3
place { world ->
val tuft = Identifier.parse("mymod:glow_tuft")
if (world.structures(world.x, world.z).isNotEmpty()) return@place false
world.canStay(world.x, world.y, world.z, tuft) && world.set(world.x, world.y, world.z, tuft)
}
}Lo que ve una feature
place recibe el mundo que se está creando, visto desde donde se coloca la feature. Puede leer y escribir el chunk que se está creando y los chunks de alrededor; lo que se escriba fuera de ellos se omite. En CraftScript (en el build de una estructura) los mismos miembros se escriben en snake_case.
| Miembro | Qué es |
|---|---|
x, y, z |
dónde se coloca |
random |
sus propios números aleatorios: la misma semilla crea la misma feature |
seed |
la semilla del mundo, para un ruido que es el mismo en todo el mundo |
minY, maxY |
las alturas del mundo; los bloques van desde minY hasta justo debajo de maxY |
block(x, y, z), state(x, y, z, name), isAir(x, y, z) |
el bloque que hay allí, el valor de uno de sus estados, si es aire |
isIn(x, y, z, tag) |
si el bloque que hay allí está en una etiqueta ("#minecraft:leaves") o es ese bloque ("minecraft:end_stone") |
canStay(x, y, z, block, states) |
si un bloque, con esos estados, podría quedarse allí: una planta sobre su suelo, un musgo en una pared mirando hacia fuera de ella. states se puede omitir |
set(x, y, z, block, states) |
coloca un bloque, con estados como mapOf("facing" to "north"); false cuando está fuera de lo que se puede escribir, o no es un bloque aquí |
inside(x, z) |
si la columna es una que puede leer y escribir |
surface(x, z) |
el primer bloque sobre el más alto que no es aire, tal como estaba el terreno antes de las features |
top(x, z) |
lo mismo, ahora, con lo que han colocado las features desde entonces |
flow(x, y, z) |
hace que el agua (o la lava, o un bloque que contiene agua) de allí fluya cuando el chunk esté en el mundo. El agua colocada mientras se crea un chunk se queda quieta, y sin esto quedaría colgando junto al aire |
structures(x, z) |
las estructuras (ids, como minecraft:end_city) cuya área abarca el chunk de esa columna, para mantener una feature fuera de ellas |
placeFile(file, x, y, z, rotation, mirror, pivotX, pivotZ) |
coloca un archivo de estructura; ver Archivos de estructura desde el código |
Un bloque que tu feature nombra y que no existe (de un mod que no está instalado) se omite, con un aviso en el log; set devuelve false para él.
Estructuras construidas por código
Para una estructura que no está hecha de archivos, o que tiene que seguir el terreno, tu código decide dónde está y la construye. Usa dos funciones:
planse consulta, en cada chunk donde el mundo colocaría una (segúnbiomes,spacingyseparation, como en cualquier estructura), si está allí, antes de que se creen los bloques del chunk. Devuelve su sitio: su centro, hasta dónde llega y lo que necesite la construcción; o nada, y allí no hay ninguna.buildconstruye la parte del sitio en cada chunk que cubre, un chunk cada vez. Cada chunk recibe el mismo sitio y los mismos números aleatorios, así que cada uno construye su trozo de la misma estructura. El sitio se guarda con el mundo, así que los chunks creados tras un reinicio siguen encajando.
structure "stone_ring" {
biomes: ["#minecraft:is_overworld"]
spacing: 32
separation: 12
plan: fun(p) {
let x = p.chunk_x * 16 + 8
let z = p.chunk_z * 16 + 8
let y = p.height(x, z)
return { x: x, y: y, z: z, radius: 10, min_y: y - 8, max_y: y + 12 }
}
build: fun(world, site) {
for i in 0..15 {
let a = i * pi() / 8
let x = site.x + round(cos(a) * 10)
let z = site.z + round(sin(a) * 10)
if world.inside(x, z) {
let ground = world.surface(x, z)
for dy in 0..2 { world.set(x, ground + dy, z, "minecraft:mossy_cobblestone") }
}
}
}
}content.structure("stone_ring") {
biome("#minecraft:is_overworld")
spacing = 32
separation = 12
plan { p ->
val x = p.chunkX * 16 + 8
val z = p.chunkZ * 16 + 8
val y = p.height(x, z)
StructureSite(x, y, z, radius = 10, minY = y - 8, maxY = y + 12,
data = mapOf("tall" to p.random.nextBoolean()))
}
build { world, site ->
val stone = Identifier.parse("minecraft:mossy_cobblestone")
val top = if (site.data["tall"] == true) 4 else 2
for (i in 0 until 16) {
val x = site.x + (cos(i * PI / 8) * 10).roundToInt()
val z = site.z + (sin(i * PI / 8) * 10).roundToInt()
if (!world.inside(x, z)) continue
val ground = world.surface(x, z)
for (dy in 0..top) world.set(x, ground + dy, z, stone)
}
}
}El sitio (StructureSite en Kotlin, un mapa en CraftScript):
| Campo | Qué es | Por defecto en CraftScript |
|---|---|---|
x, y, z |
su centro | 0 |
radius |
cuántos bloques alrededor del centro alcanza: los chunks que cubre | 8 |
min_y, max_y (minY, maxY) |
las alturas que cubre | y menos y más radius |
data |
lo que necesita la construcción para construir lo mismo en cada chunk: números, texto, verdadero o falso | vacío |
En Kotlin, un sitio también tiene una seed hecha a partir de su posición. Los números de data vuelven como decimales tras un reinicio: léelos con (site.data["n"] as Number).toInt().
Lo que recibe plan, antes de que se cree ningún bloque del chunk:
| Miembro | Qué es |
|---|---|
chunkX, chunkZ |
el chunk por el que se pregunta (bloque / 16) |
seed, random |
la semilla del mundo, y los números aleatorios propios de este lugar |
height(x, z) |
la altura que tendrá la superficie allí (el primer bloque sobre ella) |
biome(x, y, z) |
el bioma que hay allí |
column(x, z), minY |
los bloques que tendrá el terreno en esa columna, desde minY hasta lo más alto del mundo, antes de su suelo, sus cuevas y lo que se construya (en el End: piedra del End y aire) |
fileSize(file) |
el tamaño de un archivo de estructura (x, y, z), o null si no existe |
build recibe el mismo mundo que una feature, pero solo puede colocar bloques del chunk que se está construyendo, y su random sale del sitio. Las demás opciones de una estructura que se aplican aquí son biomes, spacing, separation y, en Kotlin, salt. Una estructura de código necesita tanto plan como build. Encuéntrala con /locate structure, como cualquier otra.
Archivos de estructura desde el código
placeFile coloca un archivo de estructura donde tu código diga, girado y reflejado: una torre de tus archivos dentro de una estructura de código, o unas ruinas que una feature deja en una cueva. El archivo es el structures/<ruta>.nbt del mod ("mymod:watchtower") o uno del juego. Solo se coloca la parte que queda dentro de lo que el mundo permite escribir, así que una estructura que cubre varios chunks coloca su archivo desde cada uno de ellos. Los bloques de estructura del archivo no se colocan. Devuelve false si no existe ese archivo.
val TOWER = Identifier.parse("mymod:watchtower")
content.structure("watchtower") {
biome("minecraft:plains")
spacing = 40
separation = 10
plan { p ->
val size = p.fileSize(TOWER) ?: return@plan null
val x = p.chunkX * 16 + 8
val z = p.chunkZ * 16 + 8
val y = p.height(x, z)
StructureSite(x, y, z, radius = maxOf(size.x, size.z), minY = y, maxY = y + size.y,
data = mapOf("turns" to p.random.nextInt(4), "half_x" to size.x / 2, "half_z" to size.z / 2))
}
build { world, site ->
val hx = (site.data["half_x"] as Number).toInt()
val hz = (site.data["half_z"] as Number).toInt()
// Its middle on the site, turned about its middle.
world.placeFile(TOWER, site.x - hx, site.y, site.z - hz,
rotation = (site.data["turns"] as Number).toInt(), pivotX = hx, pivotZ = hz)
}
}| Parámetro | Valores | Por defecto |
|---|---|---|
file |
el id del archivo | — |
x, y, z |
dónde va su esquina, antes de girarlo | — |
rotation |
cuartos de vuelta en sentido horario | 0 |
mirror |
Mirror.NONE, LEFT_RIGHT (a lo largo de z) o FRONT_BACK (a lo largo de x) |
NONE |
pivotX, pivotZ |
el bloque del archivo alrededor del cual gira | 0, 0 |
El terreno de una dimensión
Puedes dar la forma del terreno de una de las dimensiones del juego (minecraft:overworld, minecraft:the_nether o minecraft:the_end): islas flotantes, colinas suaves, un mundo de pilares. Tu forma dice cuán sólido es cada punto: por encima de 0 es tierra, por debajo de 0 es aire. El terreno se hace con la piedra habitual de la dimensión, y el suelo de sus biomas va encima.
// Rolling hills over the whole overworld.
terrain "minecraft:overworld" {
column: fun(x, z, ys, world) {
let ground = 72 + 14 * sin(x / 60) * cos(z / 60)
return ys.map(fun(y) { return (ground - y) / 8 })
}
has_land: fun(x, z, world) { return true }
}// Round, flattened islands in the End, one every 96 blocks.
content.terrain("minecraft:the_end") { world ->
val shift = kotlin.random.Random(world.seed).nextInt(96)
object : TerrainShape {
fun across(x: Int, z: Int): Double {
val dx = (x + shift).mod(96) - 48
val dz = (z + shift).mod(96) - 48
return sqrt((dx * dx + dz * dz).toDouble())
}
override fun column(x: Int, z: Int, ys: IntArray, out: DoubleArray) {
val flat = across(x, z)
for (i in ys.indices) {
val up = (ys[i] - 64) * 2.5
out[i] = (24 - sqrt(flat * flat + up * up)) / 24
}
}
override fun hasLand(x: Int, z: Int) = across(x, z) < 24
}
}| Miembro | Qué es |
|---|---|
column(x, z, ys, out) |
cuán sólida es la columna x, z a cada altura de ys, en out en el mismo orden. En CraftScript, column(x, z, ys, world) devuelve la lista |
hasLand(x, z) (has_land) |
si la columna tiene algo de tierra. En el End decide entre los biomas de tierra y los del vacío |
world.seed |
la semilla del mundo |
world.minY, world.height |
las alturas en que se hace el terreno: desde minY, height bloques hacia arriba |
world.biome(x, y, z) |
el bioma en un bloque |
El juego pregunta tu forma en las esquinas de celdas de unos pocos bloques de ancho y mezcla entre ellas, así que no necesita ser exacta bloque a bloque. En Kotlin tu función se llama una vez por mundo con su semilla, y devuelve la forma. Tu forma crea todo el terreno: las cuevas de ruido del propio juego no se crean (añade un carver), y en el End la isla central también te toca crearla a ti. Solo un mod puede dar forma a una dimensión. Una forma que falla se avisa una vez en el log y no deja tierra allí, sin cerrar el juego.
Las dimensiones propias se declaran en la página Dimensiones con uno de los terrenos del juego.
Qué bioma va dónde
Por defecto, un bioma se queda con una parte (share) de los lugares del que reemplaza (ver Generación de mundo). Para un mapa propio, tu código elige el bioma de cada punto. Por ahora esto es para el End (minecraft:the_end).
biome_map "minecraft:the_end" {
biome: fun(x, y, z, land, world) {
if x * x + z * z < 65536 { return "minecraft:the_end" } // the central island
if land == "minecraft:end_highlands" and (floor(x / 64) + floor(z / 64)) % 2 == 0 {
return "mymod:crystal_glade"
}
return land
}
}content.tag(TagKind.BIOME, "glades", "crystal_glade", "ember_glade")
content.biomes("minecraft:the_end") { world ->
val glades = world.tagged("mymod:glades")
val theEnd = Identifier.parse("minecraft:the_end")
BiomeMap { x, y, z, land ->
when {
x.toLong() * x + z.toLong() * z < 256L * 256 -> theEnd // within 1024 blocks
land.path != "end_highlands" || glades.isEmpty() -> land
else -> glades[Math.floorMod((x shr 6) * 31 + (z shr 6), glades.size)]
}
}
}La función recibe x, y, z en cuartos (el juego guarda un bioma por cada 4×4×4 bloques: un cuarto es bloque / 4) y land, el tipo de tierra que hace allí el ruido del juego, como un bioma: minecraft:end_highlands, end_midlands, end_barrens o small_end_islands. Con una forma propia, end_highlands sobre su tierra y small_end_islands fuera de ella. También recibes land en la isla central, que tu mapa coloca por sí mismo: el juego pone minecraft:the_end a menos de 1024 bloques del centro.
La respuesta debe ser un bioma del End: del juego, o uno tuyo declarado con replaces apuntando a un bioma del End. Cualquier otra respuesta, o un mapa que falla, vuelve a la elección del juego (se avisa una vez en el log). El world del mapa tiene la seed y tagged(tag), los biomas de una etiqueta de biomas. Solo un mod puede elegir los biomas de una dimensión.
Carvers
Un carver excava cuevas en el terreno una vez creado, antes de su suelo y de las features. Para cada chunk que se crea, el juego recorre los chunks de alrededor (8 a cada lado); en cada uno, cada carver del bioma de ese chunk empieza con su chance (0–1), y tu función marca lo que excava del chunk que se está creando. Los bloques excavados pasan a ser aire (o agua bajo un acuífero), y el suelo se vuelve a hacer sobre ellos. Un bioma usa un carver nombrándolo en carvers.
// A round room, 6 blocks across, in some chunks.
carver "round_caves" {
chance: 0.05
carve: fun(ctx) {
let cx = ctx.start_x * 16 + 8
let cz = ctx.start_z * 16 + 8
let cy = 20
for x in cx - 6..cx + 6 {
for y in cy - 6..cy + 6 {
for z in cz - 6..cz + 6 {
if (x - cx) * (x - cx) + (y - cy) * (y - cy) + (z - cz) * (z - cz) <= 36 { ctx.carve(x, y, z) }
}
}
}
}
}
biome "hollow_hills" {
replaces: "minecraft:meadow"
carvers: ["round_caves"]
}content.carver("round_caves", chance = 0.05) { ctx ->
val r = ctx.random
val cx = ctx.startX * 16 + r.nextInt(16)
val cz = ctx.startZ * 16 + r.nextInt(16)
val cy = r.nextInt(ctx.minY + 10, 40)
val radius = r.nextInt(4, 9)
for (x in cx - radius..cx + radius) for (y in cy - radius..cy + radius) for (z in cz - radius..cz + radius) {
val (dx, dy, dz) = Triple(x - cx, y - cy, z - cz)
if (dx * dx + dy * dy * 2 + dz * dz <= radius * radius) ctx.carve(x, y, z)
}
}
content.biome("hollow_hills") {
replaces = "minecraft:meadow"
carver("round_caves")
}| Miembro | Qué es |
|---|---|
startX, startZ |
el chunk donde empieza esta cueva (coordenadas de chunk: bloque / 16) |
chunkX, chunkZ |
el chunk que se está creando |
minY, maxY |
las alturas que puede excavar, ambas incluidas |
seed, random |
la semilla del mundo, y los números aleatorios de este comienzo: salen igual en cada chunk por el que se pregunta el comienzo, así que una cueva que cruza chunks encaja |
carve(x, y, z) |
excava ese bloque, en coordenadas del mundo; los bloques fuera del chunk que se está creando se omiten |
Una cueva puede llegar como mucho a 8 chunks de donde empieza. En CraftScript, chance vale 1 por defecto. Un carver que falla no excava nada allí, y se avisa una vez en el log.
El suelo de un bioma
surface cubre el suelo de un bioma con un bloque. Para capas, manchas o pendientes, dale una función en su lugar: mientras se crea un chunk, el juego se la pregunta, para cada bloque del suelo del bioma de arriba abajo, qué bloque va allí. Si no responde nada, ese bloque se deja a la dimensión (su piedra).
biome "crystal_glade" {
replaces: "minecraft:end_highlands"
share: 0.3
surface: fun(at) {
if at.depth_above == 1 { return "mymod:crystal_moss" }
if at.depth_above <= 3 { return "mymod:crystal_soil" }
return null
}
}content.biome("crystal_glade") {
replaces = "minecraft:end_highlands"
share = 0.3
surface { at ->
val patch = at.noise("minecraft:surface", at.x.toDouble(), 0.0, at.z.toDouble()) > 0.2
when {
at.depthAbove == 1 && patch -> Identifier.parse("mymod:glow_moss")
at.depthAbove == 1 -> Identifier.parse("mymod:crystal_moss")
at.depthAbove <= 3 -> Identifier.parse("mymod:crystal_soil")
else -> null
}
}
}Miembro de at |
Qué es |
|---|---|
x, y, z |
el bloque por el que se pregunta |
depthAbove (depth_above) |
bloques de suelo desde el aire libre de arriba hasta este: 1 para el bloque superior del suelo o del suelo de una cueva |
depthBelow (depth_below) |
bloques de suelo desde este hasta el aire libre de debajo: 1 para el más bajo sobre una cueva o el vacío |
surfaceDepth (surface_depth) |
cuán profundas llegan las capas de superficie de esta columna, según las varía el juego |
steep |
si el suelo tiene tanta pendiente como donde las montañas del juego muestran piedra desnuda |
noise(id, x, y, z) |
uno de los ruidos del mundo por su nombre ("minecraft:surface"), tal como lo hace la semilla |
La función va antes que surface: donde no responde nada, un bloque de surface también dado sigue cubriendo la parte superior. Una función que falla se avisa una vez, y donde falla se usa el suelo de la dimensión.
Etiquetas
Las etiquetas agrupan bloques, ítems o biomas bajo un solo nombre, para que una feature pueda comprobar isIn(x, y, z, "#mymod:crystal_ground") y una lista de biomas pueda decir "#mymod:glades". Un mod añade a sus propias etiquetas o a las de cualquiera, como las del juego:
// Blocks a crystal can grow on, yours and the game's.
content.tag(TagKind.BLOCK, "crystal_ground", "crystal_moss", "crystal_soil", "minecraft:end_stone")
// Biomes a feature or structure can name as "#mymod:glades".
content.tag(TagKind.BIOME, "glades", "crystal_glade", "ember_glade")
// Adding to the game's: a pickaxe mines it faster.
content.tag(TagKind.BLOCK, "minecraft:mineable/pickaxe", "crystal_soil")TagKind es BLOCK, ITEM o BIOME. Los valores son ids o #etiquetas; los nombres sin espacio de nombres son del mod. Varios mods pueden añadir a la misma etiqueta, y un valor que no existe se omite, no es un error.
Lo que conviene saber
- Solo los chunks nuevos reciben lo que crea tu código, como todo en Generación de mundo.
- Los errores no detienen el mundo. El código que lanza una excepción deja fuera lo que estaba creando allí, y el error se escribe en el log.
- Hazlo rápido. Este código se ejecuta en cada chunk, miles de veces mientras los jugadores exploran; evita el trabajo pesado en él, y en CraftScript ten presente el límite de pasos de la referencia.