API
Генерація кодом
Коли самих оголошень замало, твій код будує світ у міру створення чанків: фічі, цілі структури, форму рельєфу, який біом де розташований, печери та блоки землі.
Що можна написати
| Що | Kotlin | CraftScript |
|---|---|---|
| Фічі: шипи, рослини на стінах, валуни, усе, що ставиться в чанку | feature("…") { place { } } |
Ні |
| Структури, збудовані кодом, на кілька чанків | structure("…") { plan { }; build { } } |
Так (plan, build) |
| Рельєф виміру | terrain("minecraft:the_end") { } |
Так (terrain) |
| Який біом де розташований | biomes("minecraft:the_end") { } |
Так (biome_map) |
| Карвери: печери й тунелі | carver("…", chance) { } |
Так (carver) |
| Земля біома, блок за блоком | biome("…") { surface { } } |
Так (surface: fun(at)) |
| Теги блоків, предметів і біомів | tag(TagKind.BIOME, "…", …) |
Ні |
Усе на цій сторінці виконується, поки створюються чанки, одразу в кількох потоках. Використовуй видані тобі випадкові числа (або сід світу), а не свої: тоді той самий сід створює той самий світ. У Kotlin стеж, щоб усе, що твій код ділить між викликами, було безпечним для потоків. У CraftScript члени random недоступні; різноманітність бери з координат, сіда чи ділянки структури.
Фічі
Фіча — це те, що твій код будує в чанку, який створюється, у перелічених тобою біомах. Crafting вибирає місця, як це робить гра для своїх фіч; твоя функція place будує там і повертає true, якщо щось побудувала. Фічі пишуться на 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
}
}
}
}| Параметр | Значення | За замовчуванням |
|---|---|---|
biome(…) |
біоми, де вона ставиться: id або #тег; принаймні один |
— |
count |
спроб на чанк, діапазон: 2..5 |
1..1 |
chance |
лише в одному з такої кількості чанків | 1 |
heights |
кожна спроба на випадковій висоті з цього діапазону | null: низ світу (місце знаходить твій код) |
spread |
true: кожна спроба у випадковому стовпці чанка; false: у куті чанка, а далі вибирає твій код |
true |
on |
FeatureOn.FLOOR або CEILING: з цієї висоти вона зсувається в повітря поруч із найближчою підлогою чи стелею, не далі ніж на 32 блоки; якщо їх немає, вона не ставиться |
null |
underground |
true: лише під поверхнею, принаймні на 2 блоки нижче за неї (в печерах) |
false |
everyFloor |
true: на кожній підлозі кожного вибраного стовпця, на поверхні й на підлозі кожної печери; count — стовпців на кожну підлогу |
false |
tries |
з everyFloor: скільки разів обходяться підлоги: щоразу на кожній підлозі ще count стовпців, тож глибока земля отримує більше спроб |
1..1 |
step |
коли вона створюється, серед етапів, що наповнюють чанк: LAKES, SURFACE, ORES, UNDERGROUND (те, що прикрашає печери), VEGETATION |
VEGETATION |
З everyFloor параметри chance, heights і spread не використовуються: місця — це самі підлоги.
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)
}
}Що бачить фіча
place отримує світ, що створюється, так, як він видимий із місця, де ставиться фіча. Вона може читати й змінювати чанк, що створюється, і чанки навколо нього; зміни поза ними пропускаються. У CraftScript (у build структури) ті самі члени пишуться в snake_case.
| Член | Що це |
|---|---|
x, y, z |
де вона ставиться |
random |
її власні випадкові числа: той самий сід створює ту саму фічу |
seed |
сід світу, для шуму, однакового в усьому світі |
minY, maxY |
висоти світу; блоки йдуть від minY угору до рівня нижче за maxY |
block(x, y, z), state(x, y, z, name), isAir(x, y, z) |
блок у цій точці, значення одного з його станів, чи це повітря |
isIn(x, y, z, tag) |
чи входить блок у цій точці в тег ("#minecraft:leaves") або це саме той блок ("minecraft:end_stone") |
canStay(x, y, z, block, states) |
чи зміг би блок із цими станами там утриматися: рослина на своїй землі, мох на стіні, спрямований від неї. states можна опустити |
set(x, y, z, block, states) |
ставить блок зі станами на кшталт mapOf("facing" to "north"); false, якщо це поза дозволеним або тут це не блок |
inside(x, z) |
чи можна в цьому стовпці читати й змінювати блоки |
surface(x, z) |
перший блок над найвищим не повітряним, якою була земля до фіч |
top(x, z) |
те саме, але зараз, з тим, що фічі поставили після |
flow(x, y, z) |
дозволяє воді (або лаві, або блоку з водою) там розтектися, коли чанк опиниться у світі. Вода, поставлена під час створення чанка, стоїть нерухомо й без цього висіла б поруч із повітрям |
structures(x, z) |
структури (id, наприклад minecraft:end_city), чия область охоплює чанк цього стовпця, щоб не пускати в них фічу |
placeFile(file, x, y, z, rotation, mirror, pivotX, pivotZ) |
ставить файл структури; див. Файли структур із коду |
Блок, названий у твоїй фічі, якого не існує (з не встановленого мода), пропускається з одним попередженням у журналі; set повертає для нього false.
Структури, збудовані кодом
Для структури, що не складається з файлів або має слідувати за рельєфом, твій код сам вирішує, де вона, і будує її. Потрібні дві функції:
planвикликається в кожному чанку, де світ поставив би структуру (заbiomes,spacingіseparation, як у будь-якої структури), до створення блоків чанка, і відповідає, чи є вона там. Вона повертає її ділянку: центр, як далеко вона сягає й усе, що потрібне для будівництва; або нічого, і тоді її там немає.buildбудує частину ділянки в кожному чанку, який вона покриває, по одному чанку за раз. Кожен чанк отримує ту саму ділянку й ті самі випадкові числа, тому кожен будує свою частину тієї самої структури. Ділянка зберігається разом зі світом, тож чанки, створені після перезапуску, усе одно збігаються.
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)
}
}
}Ділянка (StructureSite у Kotlin, мапа в CraftScript):
| Поле | Що це | За замовчуванням у CraftScript |
|---|---|---|
x, y, z |
її центр | 0 |
radius |
на скільки блоків навколо центру вона сягає: які чанки вона покриває | 8 |
min_y, max_y (minY, maxY) |
висоти, які вона покриває | y мінус і плюс radius |
data |
те, що потрібне будівництву, щоб збудувати те саме в кожному чанку: числа, текст, true або false | порожньо |
У Kotlin у ділянки є ще seed, отриманий з її позиції. Числа в data після перезапуску повертаються дробовими: читай їх через (site.data["n"] as Number).toInt().
Що отримує plan до створення будь-якого блока чанка:
| Член | Що це |
|---|---|
chunkX, chunkZ |
чанк, для якого питають (блок / 16) |
seed, random |
сід світу та власні випадкові числа цього місця |
height(x, z) |
висота, яку там матиме поверхня (перший блок над нею) |
biome(x, y, z) |
біом у цій точці |
column(x, z), minY |
блоки, які матиме земля в цьому стовпці, від minY угору до верху світу, до її покриття, печер і всього збудованого (в Енді: камінь Енду й повітря) |
fileSize(file) |
розмір файла структури (x, y, z) або null, якщо такого файла немає |
build отримує той самий світ, що й фіча, але може ставити блоки лише в чанку, що будується, а його random береться з ділянки. Решта параметрів структури, що тут застосовні, — це biomes, spacing, separation і, в Kotlin, salt. Структурі коду потрібні і plan, і build. Знайти її можна через /locate structure, як будь-яку іншу.
Файли структур із коду
placeFile ставить файл структури там, де скаже твій код, повернутим і відображеним: вежу з твоїх файлів усередині структури коду або руїну, яку фіча кладе в печеру. Файл — це structures/<path>.nbt мода ("mymod:watchtower") або один із файлів гри. Ставиться лише частина всередині того, що світ дозволяє змінювати, тому структура на кілька чанків ставить свій файл із кожного з них. Блоки структури у файлі не ставляться. Повертає false, якщо такого файла немає.
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)
}
}| Параметр | Значення | За замовчуванням |
|---|---|---|
file |
id файла | — |
x, y, z |
куди йде його кут, до повороту | — |
rotation |
чверті обороту за годинниковою стрілкою | 0 |
mirror |
Mirror.NONE, LEFT_RIGHT (уздовж z) або FRONT_BACK (уздовж x) |
NONE |
pivotX, pivotZ |
блок файла, навколо якого він повертається | 0, 0 |
Рельєф виміру
Ти можеш задати форму рельєфу одного з вимірів гри (minecraft:overworld, minecraft:the_nether або minecraft:the_end): плавучі острови, горбисті рівнини, світ колон. Твоя форма каже, наскільки щільне кожне місце: вище за 0 — суходіл, нижче за 0 — повітря. Суходіл складається зі звичайного каменю виміру, а земля його біомів лягає зверху.
// 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
}
}| Член | Що це |
|---|---|
column(x, z, ys, out) |
наскільки щільний стовпець x, z на кожній висоті з ys, у out в тому самому порядку. У CraftScript column(x, z, ys, world) повертає список |
hasLand(x, z) (has_land) |
чи є в стовпці бодай якийсь суходіл. В Енді це вирішує, які біоми там, як у суходолу чи як у порожнечі |
world.seed |
сід світу |
world.minY, world.height |
висоти, в яких створюється суходіл: від minY, height блоків угору |
world.biome(x, y, z) |
біом у блоці |
Гра питає твою форму в кутах комірок завширшки в кілька блоків і згладжує між ними, тож вона не мусить бути точною до блока. У Kotlin твоя функція викликається один раз на світ з його сідом і повертає форму. Твоя форма створює весь суходіл: печер із шуму гри не буде (додай карвер), а в Енді центральний острів теж робиш ти. Форму рельєфу виміру може задавати лише один мод. Форма, що падає з помилкою, один раз потрапляє в журнал і лишає там порожнечу без суходолу, а не збій.
Власні виміри оголошуються на сторінці Виміри з одним з ігрових рельєфів.
Який біом де розташований
За замовчуванням біом займає частку share місць того, який він замінює (див. Генерація світу). Для власної мапи біом кожного місця вибирає твій код. Поки що це лише для Енду (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)]
}
}
}Функція отримує x, y, z у квартах (гра зберігає один біом на 4×4×4 блоки: кварта — це блок / 4) і land, вид суходолу, який там створює шум гри, у вигляді біома: minecraft:end_highlands, end_midlands, end_barrens або small_end_islands. Зі своєю формою — end_highlands на її суходолі й small_end_islands поза ним. land ти отримуєш і на центральному острові, який твоя мапа розміщує сама: гра ставить minecraft:the_end у межах 1024 блоків від центру.
Відповіддю має бути біом Енду: ігровий або один із твоїх, оголошений із replaces, що вказує на біом Енду. Будь-яка інша відповідь або мапа, що падає з помилкою, повертається до вибору гри (один раз потрапляє в журнал). У world мапи є seed і tagged(tag), біоми в тегу біомів. Вибирати біоми виміру може лише один мод.
Карвери
Карвер видовбує печери в землі після її створення, до її покриття й фіч. Для кожного чанка, що створюється, гра обходить чанки навколо (по 8 в кожен бік); у кожному з них кожен карвер біому цього чанка спрацьовує з ймовірністю chance (0–1), а твоя функція позначає, що він видовбує в чанку, що створюється. Видовбані блоки стають повітрям (або водою під водоносним шаром), і земля створюється над ними наново. Біом використовує карвер, називаючи його в 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")
}| Член | Що це |
|---|---|
startX, startZ |
чанк, де починається ця печера (координати чанка: блок / 16) |
chunkX, chunkZ |
чанк, що створюється |
minY, maxY |
висоти, які можна видовбати, обидві включно |
seed, random |
сід світу та випадкові числа цього початку: вони видають те саме в кожному чанку, для якого питають цей початок, тож печера, що перетинає чанки, сходиться |
carve(x, y, z) |
видовбує цей блок, у світових координатах; блоки поза чанком, що створюється, пропускаються |
Печера може відходити від свого початку щонайбільше на 8 чанків. У CraftScript у chance за замовчуванням 1. Карвер, що падає з помилкою, нічого там не видовбує, і це один раз потрапляє в журнал.
Земля біома
surface покриває землю біома одним блоком. Для шарів, плям чи схилів дай їй функцію: поки створюється чанк, гра питає її для кожного блока землі біома згори вниз, який блок там поставити. Якщо відповіді немає, блок лишається виміру (його каменю).
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
}
}
}Член at |
Що це |
|---|---|
x, y, z |
блок, про який питають |
depthAbove (depth_above) |
блоків землі від відкритого повітря зверху до цього: 1 для верхнього блока землі чи підлоги печери |
depthBelow (depth_below) |
блоків землі від цього до відкритого повітря під ним: 1 для найнижчого над печерою чи порожнечею |
surfaceDepth (surface_depth) |
наскільки глибоко йдуть поверхневі шари цього стовпця, як їх варіює гра |
steep |
чи так круто йде схил, як там, де гори гри показують голий камінь |
noise(id, x, y, z) |
один із шумів світу за іменем ("minecraft:surface"), яким його робить сід |
Функція йде раніше за surface: де вона нічого не відповідає, блок із surface, якщо він теж заданий, усе одно покриває верх. Функція, що падає з помилкою, один раз потрапляє в журнал, а там, де вона падає, використовується земля виміру.
Теги
Теги об'єднують блоки, предмети чи біоми під одним ім'ям, щоб фіча могла перевіряти isIn(x, y, z, "#mymod:crystal_ground"), а список біомів міг містити "#mymod:glades". Мод додає до своїх тегів або до чужих, наприклад до ігрових:
// 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 — це BLOCK, ITEM або BIOME. Значення — це id або #теги; імена без простору імен належать модові. До одного тега можуть додавати кілька модів, а неіснуюче значення пропускається, а не викликає помилку.
Варто знати
- Лише нові чанки отримують те, що робить твій код, як і все на сторінці Генерація світу.
- Помилки не зупиняють світ. Код, що падає, залишає без того, що він створював у цьому місці, а помилка пишеться в журнал.
- Пиши швидко. Цей код виконується для кожного чанка, тисячі разів, поки гравці досліджують світ; уникай у ньому важкої роботи, а в CraftScript пам'ятай про ліміт кроків зі довідника.