105 lines
3.5 KiB
Kotlin
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()
|
|
}
|
|
}
|