API

Génération par code

Quand déclarer ne suffit pas, ton code construit le monde pendant la création des chunks : des features, des structures entières, la forme du relief, quel biome va où, les grottes et les blocs du sol.

Ce que tu peux écrire

Quoi Kotlin CraftScript
Features : des pointes, des plantes sur des murs, des blocs erratiques, tout ce qui se place dans un chunk feature("…") { place { } } Non
Structures construites par code, sur plusieurs chunks structure("…") { plan { }; build { } } Oui (plan, build)
Le relief d’une dimension terrain("minecraft:the_end") { } Oui (terrain)
Quel biome va où biomes("minecraft:the_end") { } Oui (biome_map)
Carvers : grottes et tunnels carver("…", chance) { } Oui (carver)
Le sol d’un biome, bloc par bloc biome("…") { surface { } } Oui (surface: fun(at))
Tags de blocs, d’objets et de biomes tag(TagKind.BIOME, "…", …) Non

Tout ce qui est sur cette page s’exécute pendant la création des chunks, sur plusieurs threads à la fois. Utilise les nombres aléatoires qu’on te donne (ou la graine du monde) plutôt que les tiens : la même graine fait alors le même monde. En Kotlin, garde sûr pour les threads tout ce que ton code partage entre les appels. En CraftScript, les membres de random ne sont pas disponibles ; tire la variété des coordonnées, de la graine ou du site de la structure.

Features

Une feature est quelque chose que ton code construit dans un chunk en cours de création, dans les biomes que tu listes. Crafting choisit les endroits, comme le fait le jeu pour ses propres features ; ta fonction place y construit et renvoie true quand elle a construit quelque chose. Les features s’écrivent en Kotlin.

content/CrystalSpikes.ktKotlin
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
            }
        }
    }
}
Option Valeurs Par défaut
biome(…) biomes où elle est placée, par id ou #tag ; au moins un —
count essais par chunk, une plage : 2..5 1..1
chance seulement dans un chunk sur tant 1
heights chaque essai à une hauteur aléatoire dans cette plage null : le fond du monde (ton code trouve sa place)
spread true : chaque essai dans une colonne aléatoire du chunk ; false : au coin du chunk, et ton code choisit à partir de là true
on FeatureOn.FLOOR ou CEILING : depuis sa hauteur, elle se déplace vers l’air à côté du sol ou du plafond le plus proche, jusqu’à 32 blocs ; sans aucun, elle n’est pas placée null
underground true : seulement sous la surface, au moins 2 blocs en dessous (dans les grottes) false
everyFloor true : sur chaque sol de chaque colonne choisie, la surface et le sol de chaque grotte ; count est le nombre de colonnes par sol false
tries avec everyFloor, combien de fois on parcourt les sols : à chaque fois count colonnes de plus sur chaque sol, si bien qu’une terre profonde est atteinte par plus d’essais 1..1
step quand elle est faite, parmi les étapes qui remplissent un chunk : LAKES, SURFACE, ORES, UNDERGROUND (ce qui décore les grottes), VEGETATION VEGETATION

Avec everyFloor, chance, heights et spread ne servent pas : les endroits sont les sols eux-mêmes.

content/GlowTufts.ktKotlin
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)
    }
}

Ce que voit une feature

place reçoit le monde en cours de création, vu depuis l’endroit où la feature est placée. Elle peut lire et modifier le chunk en cours de création et ceux qui l’entourent ; ce qui est posé en dehors est ignoré. En CraftScript (dans le build d’une structure), les mêmes membres s’écrivent en snake_case.

Membre Ce que c’est
x, y, z où elle est placée
random ses propres nombres aléatoires : la même graine fait la même feature
seed la graine du monde, pour un bruit identique sur tout le monde
minY, maxY les hauteurs du monde ; les blocs vont de minY jusqu’en dessous de maxY
block(x, y, z), state(x, y, z, name), isAir(x, y, z) le bloc à cet endroit, la valeur de l’un de ses états, s’il est de l’air
isIn(x, y, z, tag) si le bloc à cet endroit est dans un tag ("#minecraft:leaves") ou est ce bloc ("minecraft:end_stone")
canStay(x, y, z, block, states) si un bloc, avec ces états, pourrait tenir là : une plante sur son sol, une mousse sur un mur en tournant le dos au mur. states peut être omis
set(x, y, z, block, states) pose un bloc, avec des états comme mapOf("facing" to "north") ; false quand c’est en dehors de ce qu’on peut poser, ou que ce n’est pas un bloc ici
inside(x, z) si la colonne est de celles qu’elle peut lire et modifier
surface(x, z) le premier bloc au-dessus du plus haut qui n’est pas de l’air, tel qu’était le terrain avant les features
top(x, z) la même chose, maintenant, avec ce que les features ont posé depuis
flow(x, y, z) laisse l’eau (ou la lave, ou un bloc qui contient de l’eau) à cet endroit s’écouler une fois le chunk dans le monde. L’eau posée pendant la création d’un chunk reste immobile, et resterait suspendue à côté de l’air sans cela
structures(x, z) les structures (ids, comme minecraft:end_city) dont la zone englobe le chunk de cette colonne, pour tenir une feature à l’écart
placeFile(file, x, y, z, rotation, mirror, pivotX, pivotZ) place un fichier de structure ; voir Fichiers de structure depuis le code

Un bloc que ta feature nomme et qui n’existe pas (d’un mod non installé) est ignoré, avec un avertissement dans le journal ; set renvoie false pour lui.

Structures construites par code

Pour une structure qui n’est pas faite de fichiers, ou qui doit suivre le relief, ton code décide où elle est et la construit. Elle prend deux fonctions :

  • plan est interrogée, dans chaque chunk où le monde en placerait une (selon biomes, spacing et separation, comme pour n’importe quelle structure), pour savoir si elle y est, avant que les blocs du chunk soient faits. Elle renvoie son site : son milieu, jusqu’où elle s’étend et tout ce dont la construction a besoin ; ou rien, et il n’y en a pas là.
  • build construit la part du site dans chaque chunk qu’il couvre, un chunk à la fois. Chaque chunk reçoit le même site et les mêmes nombres aléatoires, si bien que chacun construit son morceau de la même structure. Le site est conservé avec le monde, donc les chunks créés après un redémarrage s’accordent toujours.
content/stone_ring.craftCraftScript
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/StoneRing.ktKotlin
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)
        }
    }
}

Le site (StructureSite en Kotlin, une table en CraftScript) :

Champ Ce que c’est Par défaut en CraftScript
x, y, z son milieu 0
radius sur combien de blocs autour du milieu il s’étend : les chunks qu’il couvre 8
min_y, max_y (minY, maxY) les hauteurs qu’il couvre y moins et plus radius
data ce dont la construction a besoin pour construire pareil dans chaque chunk : nombres, texte, vrai ou faux vide

En Kotlin, un site a aussi une seed tirée de sa position. Les nombres de data reviennent en décimaux après un redémarrage : lis-les avec (site.data["n"] as Number).toInt().

Ce que reçoit plan, avant qu’aucun bloc du chunk soit fait :

Membre Ce que c’est
chunkX, chunkZ le chunk pour lequel on l’interroge (bloc / 16)
seed, random la graine du monde, et les nombres aléatoires propres à cet endroit
height(x, z) la hauteur qu’aura la surface à cet endroit (le premier bloc au-dessus)
biome(x, y, z) le biome à cet endroit
column(x, z), minY les blocs qu’aura le terrain dans cette colonne, de minY jusqu’au sommet du monde, avant son sol, ses grottes et tout ce qui est construit (dans l’End : pierre de l’End et air)
fileSize(file) la taille d’un fichier de structure (x, y, z), ou null s’il n’y en a pas

build reçoit le même monde qu’une feature, mais elle ne peut poser que des blocs du chunk en cours de construction, et son random vient du site. Les autres options d’une structure qui s’appliquent ici sont biomes, spacing, separation et, en Kotlin, salt. Une structure de code a besoin de plan et de build. Trouve-la avec /locate structure, comme n’importe quelle autre.

Fichiers de structure depuis le code

placeFile place un fichier de structure là où ton code le dit, tourné et en miroir : une tour issue de tes fichiers dans une structure de code, ou une ruine qu’une feature dépose dans une grotte. Le fichier est le structures/<chemin>.nbt du mod ("mymod:watchtower") ou l’un de ceux du jeu. Seule la part située dans ce que le monde peut modifier est placée, si bien qu’une structure qui couvre plusieurs chunks place son fichier depuis chacun d’eux. Les blocs de structure du fichier ne sont pas placés. Elle renvoie false s’il n’y a pas de tel fichier.

content/Watchtower.ktKotlin
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)
    }
}
Paramètre Valeurs Par défaut
file l’id du fichier —
x, y, z où va son coin, avant rotation —
rotation quarts de tour dans le sens horaire 0
mirror Mirror.NONE, LEFT_RIGHT (selon z) ou FRONT_BACK (selon x) NONE
pivotX, pivotZ le bloc du fichier autour duquel il tourne 0, 0

Le relief d’une dimension

Tu peux donner la forme du relief de l’une des dimensions du jeu (minecraft:overworld, minecraft:the_nether ou minecraft:the_end) : des îles flottantes, des collines douces, un monde de piliers. Ta forme dit à quel point chaque endroit est solide : au-dessus de 0, c’est de la terre, en dessous de 0, de l’air. Le relief est fait de la pierre habituelle de la dimension, et le sol de ses biomes se pose dessus.

content/hills.craftCraftScript
// 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 }
}
content/FloatingIslands.ktKotlin
// 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
    }
}
Membre Ce que c’est
column(x, z, ys, out) à quel point la colonne x, z est solide à chaque hauteur de ys, dans out dans le même ordre. En CraftScript, column(x, z, ys, world) renvoie la liste
hasLand(x, z) (has_land) si la colonne a de la terre. Dans l’End, cela décide entre les biomes de terre et ceux du vide
world.seed la graine du monde
world.minY, world.height les hauteurs où le relief est fait : à partir de minY, sur height blocs
world.biome(x, y, z) le biome à un bloc

Le jeu interroge ta forme aux coins de cellules larges de quelques blocs et fait une transition entre elles, elle n’a donc pas besoin d’être exacte bloc par bloc. En Kotlin, ta fonction est appelée une fois par monde avec sa graine, et renvoie la forme. Ta forme fait tout le relief : les grottes de bruit du jeu ne sont pas faites (ajoute un carver), et dans l’End l’île centrale est aussi à toi de la faire. Un seul mod peut façonner une dimension. Une forme qui échoue est signalée une fois dans le journal et ne laisse aucune terre là, sans planter.

Les dimensions à toi se déclarent sur la page Dimensions avec l’un des reliefs du jeu.

Quel biome va où

Par défaut, un biome prend une part (share) des emplacements de celui qu’il remplace (voir Génération du monde). Pour une carte à toi, ton code choisit le biome de chaque endroit. Pour l’instant, c’est pour l’End (minecraft:the_end).

content/end_map.craftCraftScript
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/EndMap.ktKotlin
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 fonction reçoit x, y, z en quarts (le jeu garde un biome par 4×4×4 blocs : un quart vaut bloc / 4) et land, le type de terre que fait le bruit du jeu à cet endroit, sous forme de biome : minecraft:end_highlands, end_midlands, end_barrens ou small_end_islands. Avec une forme à toi, end_highlands sur sa terre et small_end_islands en dehors. Tu reçois aussi land sur l’île centrale, que ta carte place elle-même : le jeu met minecraft:the_end à moins de 1024 blocs du milieu.

La réponse doit être un biome de l’End : celui du jeu, ou l’un des tiens déclaré avec replaces sur un biome de l’End. Toute autre réponse, ou une carte qui échoue, retombe sur le choix du jeu (signalé une fois dans le journal). Le world de la carte a la seed et tagged(tag), les biomes d’un tag de biome. Un seul mod peut choisir les biomes d’une dimension.

Carvers

Un carver creuse des grottes dans le relief une fois fait, avant son sol et ses features. Pour chaque chunk en cours de création, le jeu parcourt les chunks autour (8 de chaque côté) ; dans chacun, chaque carver du biome de ce chunk démarre avec sa chance (0–1), et ta fonction marque ce qu’elle creuse dans le chunk en cours de création. Les blocs creusés deviennent de l’air (ou de l’eau sous un aquifère), et le sol est refait par-dessus. Un biome utilise un carver en le nommant dans carvers.

content/round_caves.craftCraftScript
// 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/RoundCaves.ktKotlin
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")
}
Membre Ce que c’est
startX, startZ le chunk où cette grotte commence (coordonnées de chunk : bloc / 16)
chunkX, chunkZ le chunk en cours de création
minY, maxY les hauteurs qu’elle peut creuser, bornes comprises
seed, random la graine du monde, et les nombres aléatoires de ce départ : ils tirent pareil dans chaque chunk pour lequel le départ est interrogé, si bien qu’une grotte qui traverse des chunks s’aligne
carve(x, y, z) creuse ce bloc, en coordonnées du monde ; les blocs en dehors du chunk en cours de création sont ignorés

Une grotte peut s’étendre au plus sur 8 chunks depuis son départ. En CraftScript, chance vaut 1 par défaut. Un carver qui échoue ne creuse rien là, signalé une fois dans le journal.

Le sol d’un biome

surface recouvre le sol d’un biome d’un seul bloc. Pour des couches, des plaques ou des pentes, donne-lui plutôt une fonction : pendant la création d’un chunk, le jeu lui demande, pour chaque bloc du sol du biome de haut en bas, quel bloc y va. Sans réponse, ce bloc est laissé à la dimension (sa pierre).

content/crystal_glade.craftCraftScript
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/CrystalGlade.ktKotlin
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
        }
    }
}
Membre de at Ce que c’est
x, y, z le bloc dont il s’agit
depthAbove (depth_above) blocs de sol depuis l’air libre au-dessus jusqu’à celui-ci : 1 pour le bloc du dessus du sol ou du sol d’une grotte
depthBelow (depth_below) blocs de sol depuis celui-ci jusqu’à l’air libre en dessous : 1 pour le plus bas au-dessus d’une grotte ou du vide
surfaceDepth (surface_depth) jusqu’où descendent les couches de surface de cette colonne, telles que le jeu les fait varier
steep si le sol est en pente autant que là où les montagnes du jeu montrent la pierre nue
noise(id, x, y, z) l’un des bruits du monde par son nom ("minecraft:surface"), tel que la graine le fait

La fonction passe avant surface : là où elle ne répond rien, un bloc surface donné aussi recouvre quand même le dessus. Une fonction qui échoue est signalée une fois, et le sol de la dimension est utilisé là où elle échoue.

Tags

Les tags regroupent des blocs, des objets ou des biomes sous un seul nom, si bien qu’une feature peut tester isIn(x, y, z, "#mymod:crystal_ground") et qu’une liste de biomes peut dire "#mymod:glades". Un mod ajoute à ses propres tags ou à ceux de n’importe qui, comme ceux du jeu :

content/Tags.ktKotlin
// 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 vaut BLOCK, ITEM ou BIOME. Les valeurs sont des ids ou des #tags ; les noms sans espace de noms sont ceux du mod. Plusieurs mods peuvent ajouter au même tag, et une valeur qui n’existe pas est ignorée, ce n’est pas une erreur.

Bon à savoir

  • Seulement les nouveaux chunks reçoivent ce que ton code fait, comme pour tout dans Génération du monde.
  • Les erreurs n’arrêtent pas le monde. Un code qui lève une exception laisse de côté ce qu’il était en train de faire là, et l’erreur est écrite dans le journal.
  • Reste rapide. Ce code s’exécute pour chaque chunk, des milliers de fois quand les joueurs explorent ; évite d’y faire un travail lourd, et en CraftScript garde en tête la limite d’étapes de la référence.