All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jglrxavpok.hephaistos.mca.ChunkSection.kt Maven / Gradle / Ivy

There is a newer version: 2.6.1
Show newest version
package org.jglrxavpok.hephaistos.mca

import org.jglrxavpok.hephaistos.Options
import org.jglrxavpok.hephaistos.collections.ImmutableLongArray
import org.jglrxavpok.hephaistos.mca.AnvilException.Companion.missing
import org.jglrxavpok.hephaistos.mcdata.Biome
import org.jglrxavpok.hephaistos.nbt.NBT
import org.jglrxavpok.hephaistos.nbt.NBTCompound
import org.jglrxavpok.hephaistos.nbt.NBTLongArray
import org.jglrxavpok.hephaistos.nbt.NBTString
import kotlin.experimental.and
import kotlin.experimental.or
import kotlin.math.ceil

/**
 * 16x16x16 subchunk.
 */
class ChunkSection(val y: Byte) {

    /**
     * Palette used by this section, best not to touch if you don't know what you are doing
     */
    private var blockPalette: BlockPalette? = null
    val empty get()= blockPalette == null
    private val blockStates: Array = Array(16*16*16) { BlockState.AIR }
    var blockLights = ByteArray(0)
    var skyLights = ByteArray(0)

    /**
     * May not exist. If it does, it is 4*4*4 Strings of Biome IDs, for a 4x4x4 volume in the chunk (ie 1 string correspond
     * to the biome for a cube of 4x4x4 blocks).
     * Arranged by X, Z and then Y.
     */
    var biomes: Array? = null

    private val biomeArraySize get()= 4*4*4

    private var baseBiome = Biome.UnknownBiome

    /**
     * Constructs a ChunkSection from a TAG_Compound.
     * @throws AnvilException if the Compound in the argument is missing fields for loading
     */
    @Throws(AnvilException::class)
    @JvmOverloads
    constructor(nbt: NBTCompound, version: SupportedVersion = SupportedVersion.Latest): this(nbt.getByte("Y") ?: missing("Y")) {
        if(version < SupportedVersion.MC_1_17_0) {
            if(y !in 0..15)
                throw AnvilException("Invalid section Y: $y. Must be in 0..15 for pre-1.17 sections")
        }

        // empty palette can happen if the section exist but requires more than 8 bits to save the block IDs (in that case, the global ID is directly used)
        val blockPaletteNBT =
            when {
                version < SupportedVersion.MC_1_18_PRE_4 -> nbt.getList("Palette")
                else -> (nbt.getCompound("block_states") ?: missing("block_states")).getList("palette")
            }
        blockPalette = blockPaletteNBT?.let { BlockPalette(it) } // We consider that there are no blocks inside this section if the palette is null (because we cannot interpret IDs)

        if(Options.WarnWhenLoadingSectionWithNoPaletteButWithBlocks.active && blockPalette == null) {
            val hasBlockStates = if(version < SupportedVersion.MC_1_18_PRE_4) {
                nbt.containsKey("BlockStates")
            } else {
                (nbt.getCompound("block_states") ?: missing("block_states")).containsKey("data")
            }

            if(hasBlockStates) {
                System.err.println("[Hephaistos] Attempted to load a ChunkSection with no palette but with block states. Because Hephaistos cannot interpret global IDs, block states will be skipped")
            }
        }

        if(blockPalette != null) {
            val compactedBlockStates =
                when {
                    version < SupportedVersion.MC_1_18_PRE_4 -> nbt.getLongArray("BlockStates") ?: missing("BlockStates")

                    /* no data is seemingly allowed and represents a section which is full of the block at palette index 0
                    * + path has changed
                    * */
                    else -> (nbt.getCompound("block_states") ?: missing("block_states")).getLongArray("data") ?: ImmutableLongArray()
                }

            val sizeInBits = compactedBlockStates.size*64 / 4096
            val ids: IntArray
            when {
                version == SupportedVersion.MC_1_15 -> {
                    ids = decompress(compactedBlockStates, sizeInBits)
                    if(ids.size != 16*16*16) {
                        throw AnvilException("Invalid decompressed BlockStates length (${ids.size}). Must be 4096 (16x16x16)")
                    }
                }

                version >= SupportedVersion.MC_1_16 -> {
                    val expectedCompressedLength =
                        if(compactedBlockStates.size == 0) {
                            -1 /* force invalid value */
                        } else {
                            val intPerLong = 64 / sizeInBits
                            ceil(4096.0 / intPerLong).toInt()
                        }
                    var unpack = true
                    if(compactedBlockStates.size != expectedCompressedLength) {
                        if(version >= SupportedVersion.MC_1_18_PRE_4 && compactedBlockStates.size == 0) {
                            // palette only has a single element
                            unpack = false
                        } else {
                            throw AnvilException("Invalid compressed BlockStates length (${compactedBlockStates.size}). At $sizeInBits bit per value, expected $expectedCompressedLength bytes. Note that 0 length is not allowed with pre 1.18 formats.")
                        }
                    }

                    ids = if(unpack) {
                        unpack(compactedBlockStates, sizeInBits).sliceArray(0 until 4096)
                    } else {
                        IntArray(4096) { 0 }
                    }
                }

                else -> throw AnvilException("Unsupported version for compressed block states: $version")
            }

            for((index, id) in ids.withIndex()) {
                blockStates[index] = blockPalette!!.elements[id]
            }

            blockPalette!!.loadReferences(blockStates.asIterable())

            nbt.getByteArray("BlockLight")?.let {
                blockLights = ByteArray(it.size)
                it.copyInto(blockLights)
            }
            nbt.getByteArray("SkyLight")?.let {
                skyLights = ByteArray(it.size)
                it.copyInto(skyLights)
            }
        }

        if(version >= SupportedVersion.MC_1_18_PRE_4) {
            if("biomes" in nbt) {
                biomes = Array(biomeArraySize) { Biome.UnknownBiome }
                val biomesNBT = nbt.getCompound("biomes")!!
                val paletteNBT = biomesNBT.getList("palette") ?: missing("biomes.palette")
                val biomePalette = BiomePalette(paletteNBT)
                if("data" !in biomesNBT) {
                    if(biomePalette.elements.size > 0) {
                        baseBiome = biomePalette.elements[0]
                    }
                } else {
                    val compressedBiomes = biomesNBT.getLongArray("data")!!

                    val sizeInBits = compressedBiomes.size * 64 / biomeArraySize
                    val intPerLong = 64 / sizeInBits
                    val expectedCompressedLength = ceil(biomeArraySize.toDouble() / intPerLong).toInt()
                    if (compressedBiomes.size != expectedCompressedLength) {
                        throw AnvilException("Invalid compressed biomes length (${compressedBiomes.size}). At $sizeInBits bit per value, expected $expectedCompressedLength bytes")
                    }
                    val ids = unpack(compressedBiomes, sizeInBits).sliceArray(0 until biomeArraySize)
                    for ((index, id) in ids.withIndex()) {
                        biomes!![index] = biomePalette.elements[id]
                    }
                }
            }
        }
    }

    /**
     * Sets the block state in this section at the given position.
     *
     * Will create the palette if necessary, and will update it after (either created by the call or somewhen else)
     *
     * X,Y,Z must be inside this section (ie in a 16x16x16 cube)
     */
    operator fun set(x: Int, y: Int, z: Int, block: BlockState) {
        checkBounds(x, y, z)
        if(blockPalette == null) {
            blockPalette = BlockPalette() // initialize new palette
            blockPalette!!.elements += BlockState.AIR
            blockPalette!!.loadReferences(blockStates.asIterable()) // load as all air
            blockPalette!!.increaseReference(block)
            blockPalette!!.decreaseReference(BlockState.AIR)
            blockStates[index(x, y, z)] = block
        } else {
            val previous = this[x, y, z]
            blockPalette!!.increaseReference(block)
            blockPalette!!.decreaseReference(previous)
            blockStates[index(x, y, z)] = block
        }
    }

    private fun checkBounds(x: Int, y: Int, z: Int) {
        if(x !in 0..15) throw IllegalArgumentException("x ($x) is not in 0..15")
        if(y !in 0..15) throw IllegalArgumentException("y ($y) is not in 0..15")
        if(z !in 0..15) throw IllegalArgumentException("z ($z) is not in 0..15")
    }

    /**
     * Returns the block light at the given position
     *
     * If this section is empty, will throw AnvilException
     *
     * X,Y,Z must be in a 16x16x16 cube.
     *
     * @throws AnvilException if a get is attempted while this section is empty
     */
    @Throws(AnvilException::class)
    fun getBlockLight(x: Int, y: Int, z: Int): Byte {
        if(empty)
            throw AnvilException("Trying to access empty section!")
        checkBounds(x, y, z)
        val index = index(x,y,z)
        return if(index % 2 == 0) {
            blockLights[index/2] and 0x0F
        } else {
            ((blockLights[index/2].toInt() shr 4) and 0x0F).toByte()
        }
    }

    /**
     * Returns the sky light at the given position
     *
     * If this section is empty, will throw AnvilException
     *
     * X,Y,Z must be in a 16x16x16 cube.
     *
     * @throws AnvilException if a get is attempted while this section is empty
     */
    @Throws(AnvilException::class)
    fun getSkyLight(x: Int, y: Int, z: Int): Byte {
        if(empty)
            throw AnvilException("Trying to access empty section!")
        checkBounds(x, y, z)
        val index = index(x,y,z)
        return if(index % 2 == 0) {
            skyLights[index/2] and 0x0F
        } else {
            ((skyLights[index/2].toInt() shr 4) and 0x0F).toByte()
        }
    }

    /**
     * Sets the sky light in this section at the given position.
     *
     * X,Y,Z must be inside this section (ie in a 16x16x16 cube)
     */
    fun setSkyLight(x: Int, y: Int, z: Int, light: Byte) {
        checkBounds(x, y, z)
        fillInIfEmpty()
        if(skyLights.isEmpty()) {
            skyLights = ByteArray(2048)
        }
        val index = index(x,y,z)
        if(index % 2 == 0) {
            skyLights[index/2] = (skyLights[index/2] and 0xF0.toByte()) or (light and 0x0F)
        } else {
            skyLights[index/2] = (skyLights[index/2] and 0x0F.toByte()) or ((light.toInt() shl 4) and 0x0F).toByte()
        }
    }

    /**
     * Sets the sky light in this section at the given position.
     *
     * X,Y,Z must be inside this section (ie in a 16x16x16 cube)
     */
    fun setBlockLight(x: Int, y: Int, z: Int, light: Byte) {
        checkBounds(x, y, z)
        fillInIfEmpty()
        if(blockLights.isEmpty()) {
            blockLights = ByteArray(2048)
        }
        val index = index(x,y,z)
        if(index % 2 == 0) {
            blockLights[index/2] = (blockLights[index/2] and 0xF0.toByte()) or (light and 0x0F)
        } else {
            blockLights[index/2] = (blockLights[index/2] and 0x0F.toByte()) or ((light.toInt() shl 4) and 0x0F).toByte()
        }
    }

    private fun fillInIfEmpty() {
        if(empty) {
            blockPalette = BlockPalette() // initialize new palette
            blockPalette!!.elements += BlockState.AIR
            blockPalette!!.loadReferences(blockStates.asIterable()) // load as all air
        }
    }

    /**
     * Returns the block state at the given position
     *
     * If this section is empty, will throw AnvilException
     *
     * X,Y,Z must be in a 16x16x16 cube.
     *
     * @throws AnvilException if a get is attempted while this section is empty
     */
    @Throws(AnvilException::class)
    operator fun get(x: Int, y: Int, z: Int): BlockState {
        checkBounds(x, y, z)
        if(empty)
            throw AnvilException("Trying to access empty section!")
        return blockStates[index(x,y,z)]
    }

    /**
     * Returns the biome stored inside this section at the given position
     * Be aware that biome data may not be present inside this column, in that case, this method returns UnknownBiome
     */
    fun getBiome(x: Int, y: Int, z: Int): String {
        if(biomes == null) {
            return baseBiome
        }
        val index = x/4+(z/4)*4+(y/4)*16
        return biomes!![index]
    }

    /**
     * Sets the biome stored inside this section at the given position
     * If biome data did not exist before calling this method, the biome array is created then filled with UnknownBiome
     */
    fun setBiome(x: Int, y: Int, z: Int, biomeID: String) {
        if(biomes == null) {
            biomes = Array(biomeArraySize) { Biome.UnknownBiome }
        }
        biomes?.set(x/4+(z/4)*4+(y/4)*16, biomeID)
    }

    /**
     * Returs true iif this section has biome data
     */
    fun hasBiomeData() = biomes != null

    private fun index(x: Int, y: Int, z: Int) = y*16*16+z*16+x

    /**
     * Converts this ChunkSection into its NBT representation
     */
    @JvmOverloads
    fun toNBT(version: SupportedVersion = SupportedVersion.Latest): NBTCompound = NBT.Kompound {
        this["Y"] = NBT.Byte(y)
        this["BlockLight"] = NBT.ByteArray(*blockLights)
        this["SkyLight"] = NBT.ByteArray(*skyLights)
        if(!empty) {
            if(version < SupportedVersion.MC_1_18_PRE_4) {
                this["Palette"] = blockPalette!!.toNBT()
                this["BlockStates"] = NBT.LongArray(blockPalette!!.compactIDs(blockStates, version))
            } else {
                this["block_states"] = NBT.Kompound {
                    this["palette"] = blockPalette!!.toNBT()
                    this["data"] = NBT.LongArray(blockPalette!!.compactIDs(blockStates, version))
                }


                if(biomes != null) {
                    val biomePalette = BiomePalette()
                    this["biomes"] = NBT.Kompound {
                        this["palette"] = biomePalette!!.toNBT()
                        this["data"] = NBT.LongArray(biomePalette!!.compactIDs(biomes!!, version))
                    }
                }
            }
        }
    }


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy