diff --git a/src/main/java/ru/dbotthepony/kstarbound/lua/LuaJNR.java b/src/main/java/ru/dbotthepony/kstarbound/lua/LuaJNR.java index dcdda3f5..9c4dde82 100644 --- a/src/main/java/ru/dbotthepony/kstarbound/lua/LuaJNR.java +++ b/src/main/java/ru/dbotthepony/kstarbound/lua/LuaJNR.java @@ -49,10 +49,20 @@ public interface LuaJNR { public void luaopen_debug(@NotNull Pointer luaState); @Nullable - public Pointer lua_tolstring(@NotNull Pointer luaState, int index, @LongLong long size); + public Pointer lua_tolstring(@NotNull Pointer luaState, int index, @LongLong long statusCodeReturnPtr); public int lua_load(@NotNull Pointer luaState, @LongLong long reader, long userData, @NotNull String chunkName, @NotNull String mode); + /** + * Pops a table from the stack and sets it as the new metatable for the value at the given index. + */ + public void lua_setmetatable(@NotNull Pointer luaState, int stackIndex); + + /** + * Pushes onto the stack the field e from the metatable of the object at index obj and returns the type of the pushed value. If the object does not have a metatable, or if the metatable does not have this field, pushes nothing and returns LUA_TNIL. + */ + public int luaL_getmetafield(@NotNull Pointer luaState, int stackIndex, @LongLong long stringPtr); + public interface lua_CFunction extends Closure { int invoke(@NotNull Pointer luaState); diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt index fad09907..06a5bc9f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt @@ -454,3 +454,88 @@ fun TableFactory.tableFrom(collection: Collection): Table { return alloc } + +fun LuaState.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()) +} + +fun LuaState.ArgStack.getVector2i(position: Int = this.position++): Vector2i { + if (position !in 1 ..this.top) + throw IllegalArgumentException("Bad argument #$position: Vector2i expected, got nil") + + return lua.getVector2i(position) + ?: throw IllegalArgumentException("Lua code error: Bad argument #$position: Vector2i expected, got ${lua.typeAt(position)}") +} + +fun LuaState.push(value: IStruct4i) { + pushTable(arraySize = 4) + val table = stackTop + val (x, y, z, w) = value + + push(1) + push(x) + setTableValue(table) + + push(2) + push(y) + setTableValue(table) + + push(3) + push(z) + setTableValue(table) + + push(4) + push(w) + setTableValue(table) +} + +fun LuaState.push(value: IStruct3i) { + pushTable(arraySize = 3) + val table = stackTop + val (x, y, z) = value + + push(1) + push(x) + setTableValue(table) + + push(2) + push(y) + setTableValue(table) + + push(3) + push(z) + setTableValue(table) +} + +fun LuaState.push(value: IStruct2i) { + pushTable(arraySize = 2) + val table = stackTop + val (x, y) = value + + push(1) + push(x) + setTableValue(table) + + push(2) + push(y) + setTableValue(table) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt index 5388f7f3..4d78b4c9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt @@ -12,15 +12,11 @@ import com.kenai.jffi.Closure import com.kenai.jffi.ClosureManager import com.kenai.jffi.MemoryIO import com.kenai.jffi.Type -import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap import jnr.ffi.Pointer import org.apache.logging.log4j.LogManager import org.lwjgl.system.MemoryStack import org.lwjgl.system.MemoryUtil -import ru.dbotthepony.kommons.util.IStruct2i -import ru.dbotthepony.kommons.util.IStruct3i -import ru.dbotthepony.kommons.util.IStruct4i -import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter import ru.dbotthepony.kstarbound.math.vector.Vector2i @@ -29,6 +25,7 @@ import java.lang.ref.Cleaner import java.lang.ref.WeakReference import java.nio.ByteBuffer import java.nio.ByteOrder +import kotlin.math.floor import kotlin.system.exitProcess @Suppress("unused") @@ -64,6 +61,16 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter this.storeGlobal("math") LuaJNR.INSTANCE.luaopen_utf8(this.pointer) this.storeGlobal("utf8") + + push { + LOGGER.info(getString()) + 0 + } + + storeGlobal("__print") + + load(globalScript, "@starbound.jar!/scripts/global.lua") + call() } private val thread = Thread.currentThread() @@ -149,7 +156,7 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter } private fun getStringRaw(stackIndex: Int = -1, limit: Long = DEFAULT_STRING_LIMIT): String? { - check(limit <= Int.MAX_VALUE) { "Can't allocate string bigger than ${Int.MAX_VALUE} characters" } + require(limit <= Int.MAX_VALUE) { "Can't allocate string bigger than ${Int.MAX_VALUE} characters" } val stack = MemoryStack.stackPush() val status = stack.mallocLong(1) val p = LuaJNR.INSTANCE.lua_tolstring(this.pointer, this.absStackIndex(stackIndex), MemoryUtil.memAddress(status)) ?: return null @@ -163,7 +170,8 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter val readBytes = ByteArray(len.toInt()) p.get(0L, readBytes, 0, readBytes.size) - return this.stringInterner.intern(readBytes.toString(charset = Charsets.UTF_8)) + //return this.stringInterner.intern(readBytes.toString(charset = Charsets.UTF_8)) + return readBytes.toString(charset = Charsets.UTF_8) } fun isCFunction(stackIndex: Int = -1): Boolean = LuaJNR.INSTANCE.lua_iscfunction(this.pointer, this.absStackIndex(stackIndex)) > 0 @@ -260,44 +268,167 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter } } - fun getValue(stackIndex: Int = -1, limit: Long = DEFAULT_STRING_LIMIT): JsonElement? { + /** + * Loads value at specified stack's position as Json value according to next rules: + * * [LuaType.NONE] and invalid stack positions are loaded as literal `null` + * * [LuaType.NIL] is loaded as [JsonNull] + * * [LuaType.BOOLEAN], [LuaType.STRING] and [LuaType.NUMBER] are loaded as [JsonPrimitive] + * * [LuaType.TABLE] gets special treatment, if created by `jarray()` global Lua function AND it has only whole numeric indices, then it gets loaded as [JsonArray]. Otherwise (including being created by `jobject()`) it gets loaded as [JsonObject], non-string indices get cast to string. + * * Everything else generates [IllegalArgumentException] + */ + fun getJson(stackIndex: Int = -1, limit: Long = DEFAULT_STRING_LIMIT): JsonElement? { val abs = this.absStackIndex(stackIndex) if (abs == 0) return null - return when (this.typeAt(abs)) { + return when (val type = this.typeAt(abs)) { LuaType.NONE -> null - LuaType.NIL -> null // JsonNull.INSTANCE + LuaType.NIL -> JsonNull.INSTANCE LuaType.BOOLEAN -> InternedJsonElementAdapter.of(this.getBooleanRaw(abs)) - LuaType.LIGHTUSERDATA -> throw IllegalArgumentException("Can not get light userdata from Lua stack at $abs") LuaType.NUMBER -> JsonPrimitive(if (this.isInteger(abs)) this.getLongRaw(abs) else this.getDoubleRaw(abs)) LuaType.STRING -> JsonPrimitive(this.getStringRaw(abs, limit = limit)) - LuaType.TABLE -> this.getTableRaw(abs) - LuaType.FUNCTION -> throw IllegalArgumentException("Can not get function from Lua stack at $abs") - LuaType.USERDATA -> throw IllegalArgumentException("Can not get userdata from Lua stack at $abs") - LuaType.THREAD -> throw IllegalArgumentException("Can not get thread from Lua stack at $abs") - LuaType.UMTYPES -> throw IllegalArgumentException("Can not get umtypes from Lua stack at $abs") + + LuaType.TABLE -> { + val values = HashMap() + + val hintType = LuaJNR.INSTANCE.luaL_getmetafield(pointer, abs, __typehint) + var hint = LUA_HINT_NONE + var hasNonIntegerIndices = false + + if (hintType == LUA_TNUMBER) { + hint = getLongRaw().toInt() + pop() + + // if there is a valid hint, then try to look for __nils + val nilsType = LuaJNR.INSTANCE.luaL_getmetafield(pointer, abs, __nils) + + if (nilsType == LUA_TTABLE) { + // good. + push() + val top = this.stackTop + + while (LuaJNR.INSTANCE.lua_next(this.pointer, top - 1) != 0) { + val value = this.getJson(top + 1, limit = limit) + + if (value is JsonPrimitive) { + if (value.isString) { + values[value.asString] = JsonNull.INSTANCE + hasNonIntegerIndices = true + } else if (value.isNumber) { + var v = value.asNumber + + if (v is Long || v is Double && floor(v) == v) { + v = v.toLong() + } else { + hasNonIntegerIndices = true + } + + values[v] = JsonNull.INSTANCE + } + + } + + LuaJNR.INSTANCE.lua_settop(this.pointer, top) + } + + pop() + } else if (nilsType != LUA_TNIL) { + // what a shame. + pop() + } + } else if (hintType != LUA_TNIL) { + pop() + } + + if (hint != LUA_HINT_OBJECT && hint != LUA_HINT_ARRAY) { + hint = LUA_HINT_NONE + } + + push() + val top = this.stackTop + + while (LuaJNR.INSTANCE.lua_next(this.pointer, top - 1) != 0) { + val key = this.getJson(top, limit = limit) + val value = this.getJson(top + 1, limit = limit) + + if (key is JsonPrimitive && value != null) { + if (key.isString) { + values[key.asString] = value + hasNonIntegerIndices = true + } else if (key.isNumber) { + var v = key.asNumber + + if (v is Long || v is Double && floor(v) == v) { + v = v.toLong() + } else { + hasNonIntegerIndices = true + } + + values[v] = value + } + } + + LuaJNR.INSTANCE.lua_settop(this.pointer, top) + } + + val interpretAsList: Boolean + + when (hint) { + LUA_HINT_NONE -> interpretAsList = !hasNonIntegerIndices && values.isNotEmpty() + LUA_HINT_OBJECT -> interpretAsList = false + LUA_HINT_ARRAY -> interpretAsList = !hasNonIntegerIndices + else -> throw RuntimeException() + } + + if (interpretAsList) { + val list = JsonArray() + val sorted = LongArray(values.size) + var i = 0 + values.keys.forEach { sorted[i++] = it as Long } + sorted.sort() + + for (key in sorted) { + while (list.size() < key - 1) { + list.add(JsonNull.INSTANCE) + } + + list.add(values[key]) + } + + return list + } else { + val obj = JsonObject() + + for ((k, v) in values) { + obj[k.toString()] = v + } + + return obj + } + } + + //else -> throw IllegalArgumentException("Can not get $type from Lua stack at $abs") + else -> return null } } + /** + * Forcefully loads stack's value as key-value table + */ fun getTable(stackIndex: Int = -1, limit: Long = DEFAULT_STRING_LIMIT): JsonObject? { val abs = this.absStackIndex(stackIndex) if (!this.isTable(abs)) return null - return getTableRaw(abs, limit) - } - - private fun getTableRaw(abs: Int, limit: Long = DEFAULT_STRING_LIMIT): JsonObject { val pairs = JsonObject() this.push() val top = this.stackTop while (LuaJNR.INSTANCE.lua_next(this.pointer, abs) != 0) { - val key = this.getValue(abs + 1, limit = limit) - val value = this.getValue(abs + 2, limit = limit) + val key = this.getJson(abs + 1, limit = limit) + val value = this.getJson(abs + 2, limit = limit) if (key is JsonPrimitive && value != null) { pairs.add(this.stringInterner.intern(key.asString), value) @@ -309,70 +440,9 @@ 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] - * - * Не числовые индексы игнорируются - */ - fun getArray(stackIndex: Int = -1, limit: Long = DEFAULT_STRING_LIMIT): JsonArray? { - val abs = this.absStackIndex(stackIndex) - - if (!this.isTable(abs)) - return null - - val pairs = Int2ObjectAVLTreeMap() - this.push() - - while (LuaJNR.INSTANCE.lua_next(this.pointer, abs) != 0) { - val key = this.getValue(abs + 1, limit = limit) - val value = this.getValue(abs + 2, limit = limit) - - if (key is JsonPrimitive && key.isNumber && value != null) { - pairs.put(key.asInt, value) - } - - this.pop() - } - - val result = JsonArray() - - for ((index, value) in pairs) { - while (index > result.size()) { - result.add(JsonNull.INSTANCE) - } - - result.add(value) - } - - return result - } - fun getTableValue(stackIndex: Int = -2, limit: Long = DEFAULT_STRING_LIMIT): JsonElement? { this.loadTableValue(stackIndex) - return this.getValue(limit = limit) + return this.getJson(limit = limit) } fun loadTableValue(stackIndex: Int = -2, allowNothing: Boolean = false) { @@ -414,9 +484,9 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter } } - fun popValue(): JsonElement? { + fun popJson(): JsonElement? { try { - return this.getValue() + return this.getJson() } finally { this.pop() } @@ -460,14 +530,15 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter var position = 1 fun hasSomethingAt(position: Int): Boolean { - check(position in 1 ..this.top) { "JVM code error: Invalid argument position: $position" } + if (position !in 1 ..this.top) + return false + return this@LuaState.typeAt(position) != LuaType.NONE } fun hasSomethingAt(): Boolean { - if (hasSomethingAt(this.position + 1)) { + if (hasSomethingAt(this.position + 1)) return true - } this.position++ return false @@ -482,107 +553,87 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter } fun getString(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): String { - check(position in 1 ..this.top) { "JVM code error: Invalid argument position: $position" } - return this@LuaState.getString(position, limit = limit) - ?: throw IllegalArgumentException("Lua code error: Bad argument #$position: string expected, got ${this@LuaState.typeAt(position)}") - } - - fun getLong(position: Int = this.position++): Long { - check(position in 1 ..this.top) { "JVM code error: Invalid argument position: $position" } - return this@LuaState.getLong(position) - ?: throw IllegalArgumentException("Lua code error: Bad argument #$position: long expected, got ${this@LuaState.typeAt(position)}") - } - - fun getStringOrNil(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): String? { - check(position in 1 ..this.top) { "JVM code error: Invalid argument position: $position" } - - val type = this@LuaState.typeAt(position) - - if (type != LuaType.STRING && type != LuaType.NIL) - throw IllegalArgumentException("Lua code error: Bad argument #$position: string expected, got $type") + if (position !in 1 ..this.top) + throw IllegalArgumentException("Bad argument #$position: string expected, got nil") return this@LuaState.getString(position, limit = limit) + ?: throw IllegalArgumentException("Bad argument #$position: string expected, got ${this@LuaState.typeAt(position)}") } fun getStringOrNull(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): String? { - if (position > this.top) - return null + val type = this@LuaState.typeAt(position) - return this.getStringOrNil(position, limit = limit) + if (type != LuaType.STRING && type != LuaType.NIL && type != LuaType.NONE) + throw IllegalArgumentException("Bad argument #$position: string expected, got $type") + + return this@LuaState.getString(position, limit = limit) } - fun getValue(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonElement { - check(position in 1 ..this.top) { "JVM code error: Invalid argument position: $position" } - val value = this@LuaState.getValue(position, limit = limit) - return value ?: throw IllegalArgumentException("Lua code error: Bad argument #$position: anything expected, got ${this@LuaState.typeAt(position)}") + fun getLong(position: Int = this.position++): Long { + if (position !in 1 ..this.top) + throw IllegalArgumentException("Bad argument #$position: number expected, got nil") + + return this@LuaState.getLong(position) + ?: throw IllegalArgumentException("Bad argument #$position: long expected, got ${this@LuaState.typeAt(position)}") + } + + fun getJson(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonElement { + if (position !in 1 ..this.top) + throw IllegalArgumentException("Bad argument #$position: json expected, got nil") + + val value = this@LuaState.getJson(position, limit = limit) + return value ?: throw IllegalArgumentException("Bad argument #$position: anything expected, got ${this@LuaState.typeAt(position)}") } fun getTable(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonObject { - check(position in 1 ..this.top) { "JVM code error: Invalid argument position: $position" } + if (position !in 1 ..this.top) + throw IllegalArgumentException("Bad argument #$position: table expected, got nil") + val value = this@LuaState.getTable(position, limit = limit) return value ?: throw IllegalArgumentException("Lua code error: Bad argument #$position: table expected, got ${this@LuaState.typeAt(position)}") } - fun getArray(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonArray { - check(position in 1 ..this.top) { "JVM code error: Invalid argument position: $position" } - val value = this@LuaState.getArray(position, limit = limit) - return value ?: throw IllegalArgumentException("Lua code error: Bad argument #$position: table expected, got ${this@LuaState.typeAt(position)}") - } - fun getAnything(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonElement? { - check(position in 1..this.top) { "JVM code error: Invalid argument position: $position" } - return this@LuaState.getValue(position, limit = limit) - } + if (position !in 1 ..this.top) + throw IllegalArgumentException("Bad argument #$position: json expected, got nil") - fun getDoubleOrNil(position: Int = this.position++): Double? { - check(position in 1 ..this.top) { "JVM code error: Invalid argument position: $position" } - - val type = this@LuaState.typeAt(position) - - if (type != LuaType.NUMBER && type != LuaType.NIL) - throw IllegalArgumentException("Lua code error: Bad argument #$position: double expected, got $type") - - return this@LuaState.getDouble(position) + return this@LuaState.getJson(position, limit = limit) } fun getDouble(position: Int = this.position++): Double { - check(position in 1 ..this.top) { "JVM code error: Invalid argument position: $position" } + if (position !in 1 ..this.top) + throw IllegalArgumentException("Bad argument #$position: number expected, got nil") + return this@LuaState.getDouble(position) - ?: throw IllegalArgumentException("Lua code error: Bad argument #$position: double expected, got ${this@LuaState.typeAt(position)}") + ?: throw IllegalArgumentException("Bad argument #$position: number 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" } - + fun getDoubleOrNull(position: Int = this.position++): Double? { val type = this@LuaState.typeAt(position) - if (type != LuaType.BOOLEAN && type != LuaType.NIL) + if (type != LuaType.NUMBER && type != LuaType.NIL && type != LuaType.NONE) + throw IllegalArgumentException("Bad argument #$position: double expected, got $type") + + return this@LuaState.getDouble(position) + } + + fun getBooleanOrNull(position: Int = this.position++): Boolean? { + val type = this@LuaState.typeAt(position) + + if (type == LuaType.NIL || type == LuaType.NONE) + return null + else if (type == LuaType.BOOLEAN) + return this@LuaState.getBoolean(position) + else throw IllegalArgumentException("Lua code error: Bad argument #$position: boolean expected, got $type") - - return this@LuaState.getBoolean(position) } - fun getBool(position: Int = this.position++): Boolean { - check(position in 1 ..this.top) { "JVM code error: Invalid argument position: $position" } - return this@LuaState.getBoolean(position) - ?: throw IllegalArgumentException("Lua code error: Bad argument #$position: boolean expected, got ${this@LuaState.typeAt(position)}") - } + fun getBoolean(position: Int = this.position++): Boolean { + if (position !in 1 ..this.top) + throw IllegalArgumentException("Bad argument #$position: boolean expected, got nil") - fun getBoolOrNull(position: Int = this.position++): Boolean? { - if (position > this.top) return null - return this.getBoolOrNil(position) + return this@LuaState.getBoolean(position) + ?: throw IllegalArgumentException("Bad argument #$position: boolean expected, got ${this@LuaState.typeAt(position)}") } fun push() = this@LuaState.push() @@ -593,17 +644,8 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter fun push(value: Boolean) = this@LuaState.push(value) fun push(value: String?) = this@LuaState.push(value) fun push(value: JsonElement?) = this@LuaState.push(value) - fun push(value: Registry.Entry<*>?) = this@LuaState.push(value) - fun pushFull(value: Registry.Entry<*>?) = this@LuaState.pushFull(value) } - /** - * Создаёт новое замыкание на стороне Lua. [function], будучи переданным в Lua, - * создаст новый **GC Root**. - * - * Вышестоящий код ОБЯЗАН использовать [ArgStack] и его [ArgStack.lua] для доступа к [LuaState], так как - * при вызове замыкания из Lua текущий [LuaState] может отличаться от того, которому был передан [function]. - */ fun push(function: ArgStack.() -> Int, performanceCritical: Boolean) { val weak = WeakReference(this) val pointer = this.pointer @@ -674,63 +716,8 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter } } - /** - * Создаёт новое замыкание на стороне Lua. [function], будучи переданным в Lua, - * создаст новый **GC Root**, что может создать утечку память если будет создана циклическая зависимость, - * если быть неаккуратным. - * - * Пример: Parent -> Lua -> Closure -> Parent - * - * В примере выше Lua никогда не будет собран сборщиком мусора, тем самым никогда не будет (автоматически) вызван [close] - * - * Во избежание данной ситуации предоставлена функция [pushWeak] - * - * @see CClosure.invoke - */ fun push(function: ArgStack.() -> Int) = this.push(function, !RECORD_STACK_TRACES) - /** - * Создаёт новое замыкание на стороне Lua. [function], будучи переданным в Lua, - * создаст новый **GC Root**, что может создать утечку память если будет создана циклическая зависимость, - * если быть неаккуратным. - * - * В отличие от обычного [push] для замыканий, данный вариант создаёт [WeakReference] на [self], - * который, в свою очередь, может ссылаться обратно на данный [LuaState] через свои структуры. - * - * В силу того, что замыкание более не может создать циклическую ссылку на данный [LuaState] через [self], сборщик - * мусора сможет удалить [self], а затем удалить [LuaState]. - * - */ - fun pushWeak(self: T, function: T.(args: ArgStack) -> Int, performanceCritical: Boolean) { - val weakSelf = WeakReference(self) - - return push(performanceCritical = performanceCritical, function = lazy@{ - @Suppress("name_shadowing") - val self = weakSelf.get() - - if (self == null) { - this.lua.push("Referenced 'this' got reclaimed by JVM GC") - return@lazy -1 - } - - function.invoke(self, this) - }) - } - - /** - * Создаёт новое замыкание на стороне Lua. [function], будучи переданным в Lua, - * создаст новый **GC Root**, что может создать утечку память если будет создана циклическая зависимость, - * если быть неаккуратным. - * - * В отличие от обычного [push] для замыканий, данный вариант создаёт [WeakReference] на [self], - * который, в свою очередь, может ссылаться обратно на данный [LuaState] через свои структуры. - * - * В силу того, что замыкание более не может создать циклическую ссылку на данный [LuaState] через [self], сборщик - * мусора сможет удалить [self], а затем удалить [LuaState]. - * - */ - fun pushWeak(self: T, function: T.(args: ArgStack) -> Int) = this.pushWeak(self, function, !RECORD_STACK_TRACES) - fun push() { LuaJNR.INSTANCE.lua_pushnil(this.pointer) } @@ -755,80 +742,6 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter LuaJNR.INSTANCE.lua_pushboolean(this.pointer, if (value) 1 else 0) } - fun push(value: IStruct4i) { - pushTable(arraySize = 4) - val table = stackTop - val (x, y, z, w) = value - - push(1) - push(x) - setTableValue(table) - - push(2) - push(y) - setTableValue(table) - - push(3) - push(z) - setTableValue(table) - - push(4) - push(w) - setTableValue(table) - } - - fun push(value: IStruct3i) { - pushTable(arraySize = 3) - val table = stackTop - val (x, y, z) = value - - push(1) - push(x) - setTableValue(table) - - push(2) - push(y) - setTableValue(table) - - push(3) - push(z) - setTableValue(table) - } - - fun push(value: IStruct2i) { - pushTable(arraySize = 2) - val table = stackTop - val (x, y) = value - - push(1) - push(x) - setTableValue(table) - - push(2) - push(y) - setTableValue(table) - } - - fun pushStrings(strings: Iterable) { - val index = this.pushTable(arraySize = if (strings is Collection) strings.size else 0) - - for ((i, v) in strings.withIndex()) { - this.push(i + 1L) - this.push(v) - this.setTableValue(index) - } - } - - fun pushStrings(strings: Iterator) { - val index = this.pushTable() - - for ((i, v) in strings.withIndex()) { - this.push(i + 1L) - this.push(v) - this.setTableValue(index) - } - } - fun push(value: String?) { if (value == null) { this.push() @@ -837,7 +750,7 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter val bytes = value.toByteArray(Charsets.UTF_8) - if (bytes.size < 2 shl 16) { + if (bytes.size < DEFAULT_STRING_LIMIT) { MemoryIO.getInstance().putByteArray(sharedStringBufferPtr, bytes, 0, bytes.size) LuaJNR.INSTANCE.lua_pushlstring(this.pointer, sharedStringBufferPtr, bytes.size.toLong()) } else { @@ -865,20 +778,6 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter LuaJNR.INSTANCE.lua_settable(this.pointer, stackIndex) } - fun setTableFunction(key: String, self: T, value: T.(args: ArgStack) -> Int) { - val table = this.stackTop - this.push(key) - this.pushWeak(self, value) - this.setTableValue(table) - } - - fun setTableClosure(key: String, self: T, value: T.(args: ArgStack) -> Unit) { - val table = this.stackTop - this.push(key) - this.pushWeak(self) { value.invoke(this, it); 0 } - this.setTableValue(table) - } - fun setTableValue(key: String, value: JsonElement?) { val table = this.stackTop this.push(key) @@ -923,45 +822,27 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter } fun setTableValue(key: Int, value: JsonElement?) { - val table = this.stackTop - this.push(key) - this.push(value) - this.setTableValue(table) + return setTableValue(key.toLong(), value) } fun setTableValue(key: Int, value: Int) { - val table = this.stackTop - this.push(key) - this.push(value) - this.setTableValue(table) + return setTableValue(key.toLong(), value) } fun setTableValue(key: Int, value: Long) { - val table = this.stackTop - this.push(key) - this.push(value) - this.setTableValue(table) + return setTableValue(key.toLong(), value) } fun setTableValue(key: Int, value: String) { - val table = this.stackTop - this.push(key) - this.push(value) - this.setTableValue(table) + return setTableValue(key.toLong(), value) } fun setTableValue(key: Int, value: Float) { - val table = this.stackTop - this.push(key) - this.push(value) - this.setTableValue(table) + return setTableValue(key.toLong(), value) } fun setTableValue(key: Int, value: Double) { - val table = this.stackTop - this.push(key) - this.push(value) - this.setTableValue(table) + return setTableValue(key.toLong(), value) } fun setTableValue(key: Long, value: JsonElement?) { @@ -972,10 +853,7 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter } fun setTableValue(key: Long, value: Int) { - val table = this.stackTop - this.push(key) - this.push(value) - this.setTableValue(table) + return setTableValue(key, value.toLong()) } fun setTableValue(key: Long, value: Long) { @@ -993,10 +871,7 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter } fun setTableValue(key: Long, value: Float) { - val table = this.stackTop - this.push(key) - this.push(value) - this.setTableValue(table) + return setTableValue(key, value.toDouble()) } fun setTableValue(key: Long, value: Double) { @@ -1030,7 +905,9 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter } is JsonArray -> { - val index = this.pushTable(arraySize = value.size()) + this.loadGlobal("jarray") + this.call(numResults = 1) + val index = this.stackTop for ((i, v) in value.withIndex()) { this.push(i + 1L) @@ -1041,7 +918,10 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter } is JsonObject -> { - val index = this.pushTable(hashSize = value.size()) + this.loadGlobal("jobject") + this.call(numResults = 1) + + val index = this.stackTop for ((k, v) in value.entrySet()) { this.push(k) @@ -1057,28 +937,38 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter } } - fun push(value: Registry.Entry<*>?) { - if (value == null) - push() - else - push(value.json) - } - - fun pushFull(value: Registry.Entry<*>?) { - if (value == null) - push() - else { - pushTable(hashSize = 2) - setTableValue("path", value.file?.computeFullPath()) - setTableValue("config", value.json) - } - } - companion object { private val LOGGER = LogManager.getLogger() + fun loadInternalScript(name: String): String { + return LuaState::class.java.getResourceAsStream("/scripts/$name.lua")?.readAllBytes()?.toString(Charsets.UTF_8) ?: throw RuntimeException("/scripts/$name.lua is missing!") + } + + private val globalScript by lazy { loadInternalScript("global") } private val sharedBuffers = ThreadLocal() + private fun loadStringIntoBuffer(value: String): Long { + val bytes = value.toByteArray(Charsets.UTF_8) + + if (bytes.size < DEFAULT_STRING_LIMIT) { + MemoryIO.getInstance().putByteArray(sharedStringBufferPtr, bytes, 0, bytes.size) + return sharedStringBufferPtr + } else { + throw RuntimeException("Too long string: $bytes bytes") + } + } + + private fun makeNativeString(value: String): Long { + var bytes = value.toByteArray(Charsets.UTF_8) + bytes = bytes.copyOf(bytes.size + 1) + val p = MemoryIO.getInstance().allocateMemory(bytes.size.toLong(), false) + MemoryIO.getInstance().putByteArray(p, bytes, 0, bytes.size) + return p + } + + private val __nils = makeNativeString("__nils") + private val __typehint = makeNativeString("__typehint") + private val sharedStringBufferPtr: Long get() { var p: Long? = sharedBuffers.get() @@ -1119,5 +1009,9 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter const val DEFAULT_STRING_LIMIT = 2L shl 16 const val RECORD_STACK_TRACES = false + + const val LUA_HINT_NONE = 0 + const val LUA_HINT_ARRAY = 1 + const val LUA_HINT_OBJECT = 2 } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaType.kt index ac971293..9d9b145b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaType.kt @@ -1,5 +1,17 @@ package ru.dbotthepony.kstarbound.lua +import ru.dbotthepony.kstarbound.lua.LuaState.Companion.LUA_NUMTYPES +import ru.dbotthepony.kstarbound.lua.LuaState.Companion.LUA_TBOOLEAN +import ru.dbotthepony.kstarbound.lua.LuaState.Companion.LUA_TFUNCTION +import ru.dbotthepony.kstarbound.lua.LuaState.Companion.LUA_TLIGHTUSERDATA +import ru.dbotthepony.kstarbound.lua.LuaState.Companion.LUA_TNIL +import ru.dbotthepony.kstarbound.lua.LuaState.Companion.LUA_TNONE +import ru.dbotthepony.kstarbound.lua.LuaState.Companion.LUA_TNUMBER +import ru.dbotthepony.kstarbound.lua.LuaState.Companion.LUA_TSTRING +import ru.dbotthepony.kstarbound.lua.LuaState.Companion.LUA_TTABLE +import ru.dbotthepony.kstarbound.lua.LuaState.Companion.LUA_TTHREAD +import ru.dbotthepony.kstarbound.lua.LuaState.Companion.LUA_TUSERDATA + enum class LuaType { NONE, NIL, @@ -12,4 +24,23 @@ enum class LuaType { USERDATA, THREAD, UMTYPES; + + companion object { + fun valueOf(value: Int): LuaType { + return when (value) { + LUA_TNONE -> NONE + LUA_TNIL -> NIL + LUA_TBOOLEAN -> BOOLEAN + LUA_TLIGHTUSERDATA -> LIGHTUSERDATA + LUA_TNUMBER -> NUMBER + LUA_TSTRING -> STRING + LUA_TTABLE -> TABLE + LUA_TFUNCTION -> FUNCTION + LUA_TUSERDATA -> USERDATA + LUA_TTHREAD -> THREAD + LUA_NUMTYPES -> UMTYPES + else -> throw RuntimeException("Invalid Lua type: $value") + } + } + } } diff --git a/src/main/resources/scripts/config.lua b/src/main/resources/scripts/config.lua new file mode 100644 index 00000000..7b5f2a7e --- /dev/null +++ b/src/main/resources/scripts/config.lua @@ -0,0 +1,17 @@ + +config = {} +local config = config + +function config.getParameter(name, default) + if type(name) ~= 'string' then + error('config.getParameter: name must be a string, got ' .. type(name), 2) + end + + local get = config._get(name) + + if get == nil then + return default + else + return get + end +end diff --git a/src/main/resources/scripts/global.lua b/src/main/resources/scripts/global.lua new file mode 100644 index 00000000..c8fd7d5a --- /dev/null +++ b/src/main/resources/scripts/global.lua @@ -0,0 +1,56 @@ + +-- why not use _ENV anyway lol +self = self or {} + +local LUA_HINT_NONE = 0 +local LUA_HINT_ARRAY = 1 +local LUA_HINT_OBJECT = 2 + +-- this replicates original engine code, but it shouldn't work in first place +local function __newindex(self, key, value) + local nils = getmetatable(self).__nils + + -- If we are setting an entry to nil, need to add a bogus integer entry + -- to the __nils table, otherwise need to set the entry *in* the __nils + -- table to nil and remove it. + + -- TODO: __newindex is called only when assigning non-existing keys to values, + -- TODO: as per Lua manual. + -- TODO: Chucklefish weren't aware of this? + + if key == nil then + nils[key] = true + else + nils[key] = nil + end + + rawset(self, key, value) +end + +function jobject() + return setmetatable({}, { + __newindex = __newindex, + __nils = {}, + __typehint = LUA_HINT_OBJECT + }) +end + +function jarray() + return setmetatable({}, { + __newindex = __newindex, + __nils = {}, + __typehint = LUA_HINT_ARRAY + }) +end + +local __print = __print + +function print(...) + local values = {...} + + for i, v in ipairs(values) do + values[i] = tostring(v) + end + + __print(table.concat(values, '\t')) +end diff --git a/src/main/resources/scripts/message_handler.lua b/src/main/resources/scripts/message_handler.lua new file mode 100644 index 00000000..5b0a6d5a --- /dev/null +++ b/src/main/resources/scripts/message_handler.lua @@ -0,0 +1,27 @@ + +message = { + handlers = {} +} + +local message = message + +function message.setHandler(name, handler) + if type(name) ~= 'string' then + error('message.setHandler: Handler name must be a string, got ' .. type(name), 2) + end + + if type(handler) ~= 'function' then + error('message.setHandler: Handler itself must be a function, got ' .. type(handler), 2) + end + + message.subscribe(name) + message.handlers[name] = handler +end + +function message.call(name, ...) + local handler = message.handlers[name] + + if handler ~= nil then + return handler(...) + end +end