KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImagePartReader.kt

105 lines
3.5 KiB
Kotlin

package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.common.collect.ImmutableList
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.lwjgl.stb.STBImage
import org.lwjgl.system.MemoryUtil
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.image.Image
import java.lang.ref.Reference
class ImagePartReader(part: DungeonPart, val images: ImmutableList<Image>) : PartReader(part) {
override val size: Vector2i
get() = if (images.isEmpty()) Vector2i.ZERO else images.first().size
// ObjectArrayList doesn't check for concurrent modifications
private val layers = ObjectArrayList<Layer>()
private class Layer(val palette: Array<DungeonTile>, val data: VectorizedBitSet)
override fun bind(def: DungeonDefinition) {
check(def.tiles.isNotEmpty) { "Image parts require 'tiles' palette to be present in .dungeon definition" }
for ((i, image) in images.withIndex()) {
// go around image cache, since image will be loaded exactly once
// and then forgotten
val (bytes, width, height) = Image.readImageDirect(image.source)
val tileData = IntArray(width * height)
for (x in 0 until image.width) {
for (y in 0 until image.height) {
val offset = (x + y * image.width) * 4
// flip image as we go
tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or
bytes[offset + 1].toInt().and(0xFF).shl(8) or
bytes[offset + 2].toInt().and(0xFF).shl(16) or
bytes[offset + 3].toInt().and(0xFF).shl(24)
}
}
// determine unique tiles
val uniqueTiles = IntAVLTreeSet()
for (y in 0 until image.height) {
for (x in 0 until image.width) {
val color = tileData[x + y * image.width]
if (uniqueTiles.add(color)) {
if (part.dungeon.tiles[color] == null) {
val parse = RGBAColor.abgr(color)
throw IllegalStateException("Unknown tile inside ${image.path} at $x, $y with color [${parse.redInt}, ${parse.greenInt}, ${parse.blueInt}, ${parse.alphaInt}] (index $color)")
}
}
}
}
// construct palette
val indices = IntArrayList(uniqueTiles)
val palette = Array(indices.size) { part.dungeon.tiles[indices.getInt(it)]!! }
val data = VectorizedBitSet(32 - Integer.numberOfLeadingZeros(uniqueTiles.size), image.width, image.height)
// fill data
for (y in 0 until image.height) {
for (x in 0 until image.width) {
val index = indices.indexOf(tileData[x + y * image.width])
check(index != -1)
data[x, y] = index
}
}
layers.add(Layer(palette, data))
}
}
override fun <T> walkTiles(callback: TileCallback<T>): KOptional<T> {
for (layer in layers) {
// walk bottom-top first, this way we will place bottom objects/tiles before top ones
for (y in 0 until layer.data.height) {
for (x in 0 until layer.data.width) {
val get = callback(x, y, layer.palette[layer.data[x, y]])
if (get.isPresent) return get
}
}
}
return KOptional()
}
override fun <T> walkTilesAt(x: Int, y: Int, callback: TileCallback<T>): KOptional<T> {
for (layer in layers) {
if (x in 0 until layer.data.width && y in 0 until layer.data.height) {
val get = callback(x, y, layer.palette[layer.data[x, y]])
if (get.isPresent) return get
}
}
return KOptional()
}
}