API
Generowanie kodem
Gdy deklarowanie nie wystarcza, twój kod buduje świat podczas tworzenia chunków: featury, całe struktury, kształt lądu, to, który biom leży gdzie, jaskinie i bloki gruntu.
Co możesz napisać
| Co | Kotlin | CraftScript |
|---|---|---|
| Featury: kolce, rośliny na ścianach, głazy, cokolwiek stawianego w chunku | feature("…") { place { } } |
Nie |
| Struktury budowane kodem, na kilku chunkach | structure("…") { plan { }; build { } } |
Tak (plan, build) |
| Ląd wymiaru | terrain("minecraft:the_end") { } |
Tak (terrain) |
| Który biom leży gdzie | biomes("minecraft:the_end") { } |
Tak (biome_map) |
| Carvery: jaskinie i tunele | carver("…", chance) { } |
Tak (carver) |
| Grunt biomu, blok po bloku | biome("…") { surface { } } |
Tak (surface: fun(at)) |
| Tagi bloków, przedmiotów i biomów | tag(TagKind.BIOME, "…", …) |
Nie |
Wszystko na tej stronie działa podczas tworzenia chunków, na kilku wątkach naraz. Używaj liczb losowych, które dostajesz (albo ziarna świata), zamiast własnych: to samo ziarno tworzy wtedy ten sam świat. W Kotlinie to, co twój kod współdzieli między wywołaniami, musi być bezpieczne dla wątków. W CraftScript składowe random nie są dostępne; różnorodność bierz ze współrzędnych, ziarna lub miejsca struktury.
Featury
Feature to coś, co twój kod buduje w tworzonym chunku, w wymienionych przez ciebie biomach. Crafting wybiera miejsca, tak jak gra dla własnych featurów; twoja funkcja place buduje tam i zwraca true, gdy coś zbudowała. Featury pisze się w Kotlinie.
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
}
}
}
}| Opcja | Wartości | Domyślnie |
|---|---|---|
biome(…) |
biomy, w których jest stawiany, przez id lub #tag; co najmniej jeden |
— |
count |
prób na chunk, zakres: 2..5 |
1..1 |
chance |
tylko w jednym na tyle chunków | 1 |
heights |
każda próba na losowej wysokości z tego zakresu | null: dno świata (twój kod sam znajduje miejsce) |
spread |
true: każda próba w losowej kolumnie chunku; false: w rogu chunku, a twój kod wybiera stamtąd |
true |
on |
FeatureOn.FLOOR lub CEILING: z jego wysokości przesuwa się do powietrza obok najbliższej podłogi lub sufitu, do 32 bloków dalej; gdy żadnej nie ma, nie jest stawiany |
null |
underground |
true: tylko pod powierzchnią, co najmniej 2 bloki pod nią (w jaskiniach) |
false |
everyFloor |
true: na każdej podłodze każdej wybranej kolumny, na powierzchni i na podłodze każdej jaskini; count to kolumny na podłogę |
false |
tries |
z everyFloor, ile razy przechodzi się przez podłogi: za każdym razem count kolumn więcej na każdej podłodze, więc głęboki ląd osiąga się większą liczbą prób |
1..1 |
step |
kiedy jest tworzony, wśród kroków wypełniających chunk: LAKES, SURFACE, ORES, UNDERGROUND (to, co zdobi jaskinie), VEGETATION |
VEGETATION |
Z everyFloor opcje chance, heights i spread nie są używane: miejscami są same podłogi.
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)
}
}Co widzi feature
place dostaje tworzony świat, widziany z miejsca, w którym feature jest stawiany. Może czytać i ustawiać tworzony chunk oraz chunki wokół niego; ustawianie poza nimi jest pomijane. W CraftScript (w build struktury) te same składowe zapisuje się w snake_case.
| Składowa | Czym jest |
|---|---|
x, y, z |
gdzie jest stawiany |
random |
jego własne liczby losowe: to samo ziarno tworzy ten sam feature |
seed |
ziarno świata, dla szumu takiego samego w całym świecie |
minY, maxY |
wysokości świata; bloki sięgają od minY w górę do poniżej maxY |
block(x, y, z), state(x, y, z, name), isAir(x, y, z) |
blok w tym miejscu, wartość jednego z jego stanów, czy jest powietrzem |
isIn(x, y, z, tag) |
czy blok w tym miejscu należy do tagu ("#minecraft:leaves") albo jest tym blokiem ("minecraft:end_stone") |
canStay(x, y, z, block, states) |
czy blok z tymi stanami mógłby tam stać: roślina na swoim gruncie, mech na ścianie zwrócony od niej. states można pominąć |
set(x, y, z, block, states) |
ustawia blok, ze stanami takimi jak mapOf("facing" to "north"); false, gdy jest poza tym, co można ustawiać, albo nie jest tu blokiem |
inside(x, z) |
czy kolumna jest taką, którą wolno czytać i ustawiać |
surface(x, z) |
pierwszy blok nad najwyższym, który nie jest powietrzem, takim, jaki był ląd przed featurami |
top(x, z) |
to samo, teraz, z tym, co od tamtej pory ustawiły featury |
flow(x, y, z) |
pozwala wodzie (lub lawie, albo blokowi z wodą) w tym miejscu płynąć, gdy chunk trafi do świata. Woda ustawiona podczas tworzenia chunku stoi nieruchomo i bez tego wisiałaby obok powietrza |
structures(x, z) |
struktury (id, jak minecraft:end_city), których obszar obejmuje chunk tej kolumny, żeby trzymać feature z dala od nich |
placeFile(file, x, y, z, rotation, mirror, pivotX, pivotZ) |
stawia plik struktury; zobacz Pliki struktur z kodu |
Blok, który twój feature nazywa, a który nie istnieje (z moda, który nie jest zainstalowany), jest pomijany, z jednym ostrzeżeniem w logu; set zwraca dla niego false.
Struktury budowane kodem
Dla struktury, która nie składa się z plików albo która musi podążać za lądem, to twój kod decyduje, gdzie jest, i ją buduje. Potrzebuje dwóch funkcji:
planjest pytana, w każdym chunku, w którym świat postawiłby strukturę (wedługbiomes,spacingiseparation, jak dla każdej struktury), czy tam jest, zanim powstaną bloki chunku. Zwraca jej miejsce (site): jej środek, jak daleko sięga i wszystko, czego potrzebuje budowa; albo nic, i wtedy jej tam nie ma.buildbuduje część miejsca w każdym chunku, który ono obejmuje, po jednym chunku naraz. Każdy chunk dostaje to samo miejsce i te same liczby losowe, więc każdy buduje swój kawałek tej samej struktury. Miejsce jest zapisywane ze światem, więc chunki tworzone po restarcie nadal do niego pasują.
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)
}
}
}Miejsce (StructureSite w Kotlinie, mapa w CraftScript):
| Pole | Czym jest | Domyślnie w CraftScript |
|---|---|---|
x, y, z |
jego środek | 0 |
radius |
ile bloków wokół środka sięga: chunki, które obejmuje | 8 |
min_y, max_y (minY, maxY) |
wysokości, które obejmuje | y minus i plus radius |
data |
to, czego potrzebuje budowa, żeby w każdym chunku zbudować to samo: liczby, tekst, prawda lub fałsz | puste |
W Kotlinie miejsce ma też seed utworzone z jego położenia. Liczby w data wracają po restarcie jako liczby dziesiętne: odczytuj je przez (site.data["n"] as Number).toInt().
Co dostaje plan, zanim powstanie jakikolwiek blok chunku:
| Składowa | Czym jest |
|---|---|
chunkX, chunkZ |
chunk, o który jest pytana (blok / 16) |
seed, random |
ziarno świata i liczby losowe tego miejsca |
height(x, z) |
wysokość, jaką będzie tam miała powierzchnia (pierwszy blok nad nią) |
biome(x, y, z) |
biom w tym miejscu |
column(x, z), minY |
bloki, które ląd będzie miał w tej kolumnie, od minY w górę do szczytu świata, przed jego gruntem, jaskiniami i czymkolwiek zbudowanym (w Endzie: kamień endu i powietrze) |
fileSize(file) |
rozmiar pliku struktury (x, y, z) albo null, jeśli go nie ma |
build dostaje ten sam świat co feature, ale może ustawiać tylko bloki budowanego chunku, a jego random pochodzi z miejsca. Pozostałe opcje struktury, które tu obowiązują, to biomes, spacing, separation i, w Kotlinie, salt. Struktura z kodu potrzebuje zarówno plan, jak i build. Znajdziesz ją przez /locate structure, jak każdą inną.
Pliki struktur z kodu
placeFile stawia plik struktury tam, gdzie każe twój kod, obrócony i odbity: wieżę z twoich plików wewnątrz struktury z kodu albo ruinę, którą feature zostawia w jaskini. Plik to structures/<ścieżka>.nbt moda ("mymod:watchtower") albo jeden z plików gry. Stawiana jest tylko ta część, która mieści się w tym, co świat może ustawiać, więc struktura obejmująca kilka chunków stawia swój plik z każdego z nich. Bloki strukturalne w pliku nie są stawiane. Zwraca false, jeśli takiego pliku nie ma.
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)
}
}| Parametr | Wartości | Domyślnie |
|---|---|---|
file |
id pliku | — |
x, y, z |
gdzie trafia jego róg, przed obrotem | — |
rotation |
ćwierćobroty zgodnie z ruchem wskazówek zegara | 0 |
mirror |
Mirror.NONE, LEFT_RIGHT (wzdłuż z) lub FRONT_BACK (wzdłuż x) |
NONE |
pivotX, pivotZ |
blok pliku, wokół którego się obraca | 0, 0 |
Ląd wymiaru
Możesz nadać kształt lądowi jednego z wymiarów gry (minecraft:overworld, minecraft:the_nether lub minecraft:the_end): latające wyspy, faliste wzgórza, świat filarów. Twój kształt mówi, jak lity jest każdy punkt: powyżej 0 to ląd, poniżej 0 to powietrze. Ląd jest zrobiony ze zwykłego kamienia wymiaru, a grunt jego biomów leży na wierzchu.
// 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
}
}| Składowa | Czym jest |
|---|---|
column(x, z, ys, out) |
jak lita jest kolumna x, z na każdej wysokości z ys, do out w tej samej kolejności. W CraftScript column(x, z, ys, world) zwraca listę |
hasLand(x, z) (has_land) |
czy kolumna ma jakikolwiek ląd. W Endzie decyduje między biomami lądu a biomami pustki |
world.seed |
ziarno świata |
world.minY, world.height |
wysokości, na których tworzony jest ląd: od minY, height bloków w górę |
world.biome(x, y, z) |
biom w danym bloku |
Gra pyta o twój kształt w rogach komórek szerokich na kilka bloków i płynnie je łączy, więc nie musi być dokładny co do bloku. W Kotlinie twoja funkcja jest wywoływana raz na świat z jego ziarnem i zwraca kształt. Twój kształt tworzy cały ląd: własne jaskinie szumowe gry nie powstają (dodaj carver), a w Endzie centralną wyspę też tworzysz ty. Wymiar może kształtować tylko jeden mod. Kształt, który zawiedzie, jest zgłaszany raz w logu i nie zostawia tam lądu, zamiast powodować awarię.
Własne wymiary deklaruje się na stronie Wymiary z jednym z terenów gry.
Który biom leży gdzie
Domyślnie biom zajmuje część (share) miejsc biomu, który zastępuje (zobacz Generowanie świata). Dla własnej mapy twój kod wybiera biom każdego punktu. Na razie dotyczy to Endu (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)]
}
}
}Funkcja dostaje x, y, z w kwartach (gra trzyma jeden biom na 4×4×4 bloków: kwart to blok / 4) oraz land, rodzaj lądu, który tam tworzy szum gry, jako biom: minecraft:end_highlands, end_midlands, end_barrens lub small_end_islands. Przy własnym kształcie jest to end_highlands na jego lądzie i small_end_islands poza nim. Dostajesz też land na centralnej wyspie, którą twoja mapa stawia sama: gra umieszcza minecraft:the_end w promieniu 1024 bloków od środka.
Odpowiedź musi być biomem Endu: z gry albo jednym z twoich, zadeklarowanym z replaces ustawionym na biom Endu. Każda inna odpowiedź, albo mapa, która zawiedzie, wraca do wyboru gry (zgłaszane raz w logu). world mapy ma seed i tagged(tag), czyli biomy z tagu biomów. Biomy wymiaru może wybierać tylko jeden mod.
Carvery
Carver (wyrzynacz) drąży jaskinie w lądzie po jego utworzeniu, przed jego gruntem i featurami. Dla każdego tworzonego chunku gra przechodzi przez chunki wokół niego (po 8 w każdą stronę); w każdym z nich każdy carver biomu tego chunku startuje ze swoją szansą chance (0–1), a twoja funkcja zaznacza, co drąży w tworzonym chunku. Wydrążone bloki stają się powietrzem (albo wodą pod warstwą wodonośną), a grunt jest tworzony na nowo nad nimi. Biom używa carvera, nazywając go w 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")
}| Składowa | Czym jest |
|---|---|
startX, startZ |
chunk, w którym zaczyna się ta jaskinia (współrzędne chunków: blok / 16) |
chunkX, chunkZ |
tworzony chunk |
minY, maxY |
wysokości, które wolno drążyć, oba włącznie |
seed, random |
ziarno świata i liczby losowe tego startu: losują to samo w każdym chunku, o który start jest pytany, więc jaskinia przecinająca chunki się schodzi |
carve(x, y, z) |
drąży ten blok, we współrzędnych świata; bloki poza tworzonym chunkiem są pomijane |
Jaskinia może sięgać najwyżej 8 chunków od miejsca startu. W CraftScript chance domyślnie wynosi 1. Carver, który zawiedzie, nic tam nie drąży, co jest zgłaszane raz w logu.
Grunt biomu
surface pokrywa grunt biomu jednym blokiem. Dla warstw, plam lub zboczy podaj zamiast tego funkcję: podczas tworzenia chunku gra pyta ją, dla każdego bloku gruntu biomu od góry do dołu, jaki blok tam trafia. Brak odpowiedzi zostawia ten blok wymiarowi (jego kamieniowi).
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
}
}
}Składowa at |
Czym jest |
|---|---|
x, y, z |
blok, o który się pyta |
depthAbove (depth_above) |
bloków gruntu od otwartego powietrza nad nim w dół do tego: 1 dla górnego bloku gruntu albo podłogi jaskini |
depthBelow (depth_below) |
bloków gruntu od tego w dół do otwartego powietrza pod nim: 1 dla najniższego nad jaskinią lub pustką |
surfaceDepth (surface_depth) |
jak głęboko sięgają warstwy powierzchniowe tej kolumny, tak jak gra je zmienia |
steep |
czy grunt opada tak stromo jak tam, gdzie góry z gry pokazują gołą skałę |
noise(id, x, y, z) |
jeden z szumów świata po nazwie ("minecraft:surface"), taki, jaki tworzy ziarno |
Funkcja ma pierwszeństwo przed surface: tam, gdzie nic nie odpowiada, podany też blok surface nadal pokrywa wierzch. Funkcja, która zawiedzie, jest zgłaszana raz, a tam, gdzie zawodzi, używany jest grunt wymiaru.
Tagi
Tagi grupują bloki, przedmioty lub biomy pod jedną nazwą, więc feature może sprawdzić isIn(x, y, z, "#mymod:crystal_ground"), a lista biomów może zawierać "#mymod:glades". Mod dodaje do własnych tagów albo do cudzych, takich jak te z gry:
// 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 to BLOCK, ITEM lub BIOME. Wartości to id lub #tagi; nazwy bez przestrzeni nazw należą do moda. Kilka modów może dodawać do tego samego tagu, a wartość, która nie istnieje, jest pomijana, a nie jest błędem.
Warto wiedzieć
- Tylko nowe chunki dostają to, co tworzy twój kod, tak jak wszystko w Generowaniu świata.
- Błędy nie zatrzymują świata. Kod, który rzuci wyjątek, pomija to, co tam tworzył, a błąd trafia do logu.
- Pisz szybko. Ten kod działa dla każdego chunku, tysiące razy, gdy gracze eksplorują; unikaj w nim ciężkiej pracy, a w CraftScript pamiętaj o limicie kroków z opisu języka.