diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Constants.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Constants.kt index edf0f9fc..bf194b95 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Constants.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Constants.kt @@ -3,5 +3,6 @@ package ru.dbotthepony.kstarbound const val METRES_IN_STARBOUND_UNIT = 0.5 const val METRES_IN_STARBOUND_UNITf = 0.5f -const val PIXELS_IN_STARBOUND_UNIT = 8.0 -const val PIXELS_IN_STARBOUND_UNITf = 8.0f +const val PIXELS_IN_STARBOUND_UNITi = 8 +const val PIXELS_IN_STARBOUND_UNIT = PIXELS_IN_STARBOUND_UNITi.toDouble() +const val PIXELS_IN_STARBOUND_UNITf = PIXELS_IN_STARBOUND_UNITi.toFloat() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index c80371af..a5814847 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -375,7 +375,18 @@ class Starbound : ISBFileLocator { state.setTableFunction("imageSpaces", this) { args -> // List root.imageSpaces(String imagePath, Vec2F worldPosition, float spaceScan, bool flip) - TODO() + val values = imageData(args.getString()).worldSpaces(args.getVector2i(), args.getDouble(), args.getBool()) + + args.lua.pushTable(arraySize = values.size) + val table = args.lua.stackTop + + for ((i, value) in values.withIndex()) { + args.lua.push(i) + args.lua.push(value) + args.lua.setTableValue(table) + } + + 1 } state.setTableFunction("nonEmptyRegion", this) { args -> diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/ImageData.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/ImageData.kt index d0b94b14..8eb92520 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/ImageData.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/ImageData.kt @@ -2,7 +2,8 @@ package ru.dbotthepony.kstarbound.io import org.lwjgl.stb.STBImage import org.lwjgl.system.MemoryUtil.memAddress -import ru.dbotthepony.kvector.vector.ndouble.Vector2d +import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNIT +import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITi import ru.dbotthepony.kvector.vector.nint.Vector2i import ru.dbotthepony.kvector.vector.nint.Vector4i import java.lang.ref.Cleaner @@ -45,14 +46,65 @@ class ImageData(val data: ByteBuffer, val width: Int, val height: Int, val amoun } } - fun worldSpaces(pos: Vector2d, spaceScan: Double, flip: Boolean): List { - when (amountOfChannels) { - 3 -> TODO() - 4 -> TODO() - else -> throw IllegalStateException("Can not check world space taken by image with $amountOfChannels color channels") + operator fun get(x: Int, y: Int, flip: Boolean): Int { + require(x in 0 until width) { "X out of bounds: $x" } + require(y in 0 until height) { "Y out of bounds: $y" } + + if (flip) { + return this[width - x - 1, y] + } else { + return this[x, y] } } + fun isTransparent(x: Int, y: Int, flip: Boolean): Boolean { + if (x !in 0 until width) return true + if (y !in 0 until height) return true + if (amountOfChannels != 4) return false + return this[x, y, flip] and 0xFF != 0x0 + } + + fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): List { + if (amountOfChannels != 3 && amountOfChannels != 4) throw IllegalStateException("Can not check world space taken by image with $amountOfChannels color channels") + + val xDivL = pixelOffset.x % PIXELS_IN_STARBOUND_UNITi + val yDivB = pixelOffset.y % PIXELS_IN_STARBOUND_UNITi + + val xDivR = (pixelOffset.x + width) % PIXELS_IN_STARBOUND_UNITi + val yDivT = (pixelOffset.y + height) % PIXELS_IN_STARBOUND_UNITi + + val leftMostX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi - (if (xDivL != 0) 1 else 0) + val bottomMostY = pixelOffset.y / PIXELS_IN_STARBOUND_UNITi - (if (yDivB != 0) 1 else 0) + + val rightMostX = (pixelOffset.x + width) / PIXELS_IN_STARBOUND_UNITi + (if (xDivR != 0) 1 else 0) + val topMostY = (pixelOffset.y + height) / PIXELS_IN_STARBOUND_UNITi + (if (yDivT != 0) 1 else 0) + + val result = ArrayList() + + for (y in bottomMostY .. topMostY) { + for (x in leftMostX .. rightMostX) { + val left = x * PIXELS_IN_STARBOUND_UNITi + val bottom = y * PIXELS_IN_STARBOUND_UNITi + + var transparentPixels = 0 + + for (sX in 0 until PIXELS_IN_STARBOUND_UNITi) { + for (sY in 0 until PIXELS_IN_STARBOUND_UNITi) { + if (isTransparent(xDivL + sX + left, yDivB + sY + bottom, flip)) { + transparentPixels++ + } + } + } + + if (transparentPixels * FILL_RATIO >= spaceScan) { + result.add(Vector2i(x, y)) + } + } + } + + return result + } + val nonEmptyRegion by lazy { if (amountOfChannels == 4) { var x0 = 0 @@ -60,7 +112,7 @@ class ImageData(val data: ByteBuffer, val width: Int, val height: Int, val amoun search@for (y in 0 until height) { for (x in 0 until width) { - if (this[x, y] and 0x000000FF != 0x0) { + if (this[x, y] and 0xFF != 0x0) { x0 = x y0 = y break@search @@ -73,7 +125,7 @@ class ImageData(val data: ByteBuffer, val width: Int, val height: Int, val amoun search@for (y in height - 1 downTo y0) { for (x in width - 1 downTo x0) { - if (this[x, y] and 0x000000FF != 0x0) { + if (this[x, y] and 0xFF != 0x0) { x1 = x y1 = y break@search @@ -86,4 +138,8 @@ class ImageData(val data: ByteBuffer, val width: Int, val height: Int, val amoun Vector4i(0, 0, width, height) } + + companion object { + const val FILL_RATIO = 1 / (PIXELS_IN_STARBOUND_UNIT * PIXELS_IN_STARBOUND_UNIT) + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt index aca80c0f..61774069 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt @@ -311,6 +311,29 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter return pairs } + fun getVector2i(stackIndex: Int = -1): Vector2i? { + val abs = this.absStackIndex(stackIndex) + + if (!this.isTable(abs)) + return null + + push(1) + loadTableValue(abs) + + val x = getLong(abs + 1) + pop() + x ?: return null + + push(2) + loadTableValue(abs) + + val y = getLong(abs + 1) + pop() + y ?: return null + + return Vector2i(x.toInt(), y.toInt()) + } + /** * Пропуски заполняются [JsonNull.INSTANCE] * @@ -354,13 +377,13 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter return this.getValue(limit = limit) } - fun loadTableValue(stackIndex: Int = -2) { + fun loadTableValue(stackIndex: Int = -2, allowNothing: Boolean = false) { val abs = this.absStackIndex(stackIndex) if (!this.isTable(abs)) throw IllegalArgumentException("Attempt to index an ${this.typeAt(abs)} value") - if (LuaJNR.INSTANCE.lua_gettable(this.pointer, abs) == LUA_TNONE) + if (LuaJNR.INSTANCE.lua_gettable(this.pointer, abs) == LUA_TNONE && !allowNothing) throw IllegalStateException("loaded TNONE from Lua table") } @@ -508,6 +531,18 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter ?: throw IllegalArgumentException("Lua code error: Bad argument #$position: double expected, got ${this@LuaState.typeAt(position)}") } + fun getInt(position: Int = this.position++): Int { + check(position in 1 ..this.top) { "JVM code error: Invalid argument position: $position" } + return this@LuaState.getLong(position)?.toInt() + ?: throw IllegalArgumentException("Lua code error: Bad argument #$position: integer expected, got ${this@LuaState.typeAt(position)}") + } + + fun getVector2i(position: Int = this.position++): Vector2i { + check(position in 1 ..this.top) { "JVM code error: Invalid argument position: $position" } + return this@LuaState.getVector2i(position) + ?: throw IllegalArgumentException("Lua code error: Bad argument #$position: Vector2i expected, got ${this@LuaState.typeAt(position)}") + } + fun getBoolOrNil(position: Int = this.position++): Boolean? { check(position in 1 ..this.top) { "JVM code error: Invalid argument position: $position" }