KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt

1313 lines
36 KiB
Kotlin

package ru.dbotthepony.kstarbound.lua
import com.github.benmanes.caffeine.cache.Interner
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.kenai.jffi.CallContext
import com.kenai.jffi.CallingConvention
import com.kenai.jffi.Closure
import com.kenai.jffi.ClosureManager
import com.kenai.jffi.MemoryIO
import com.kenai.jffi.Type
import jnr.ffi.Pointer
import org.apache.logging.log4j.LogManager
import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.util.Delegate
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.lua.bindings.provideRootBindings
import ru.dbotthepony.kstarbound.lua.bindings.provideUtilityBindings
import ru.dbotthepony.kstarbound.util.random.random
import java.io.Closeable
import java.lang.ref.Cleaner
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.random.RandomGenerator
import kotlin.math.floor
import kotlin.properties.Delegates
import kotlin.system.exitProcess
@Suppress("unused")
class LuaThread private constructor(
private val pointer: Pointer,
val stringInterner: Interner<String> = Starbound.STRINGS
) : Closeable {
constructor(stringInterner: Interner<String> = Starbound.STRINGS) : this(LuaJNR.INSTANCE.luaL_newstate() ?: throw OutOfMemoryError("Unable to allocate new LuaState"), stringInterner) {
val pointer = this.pointer
val panic = ClosureManager.getInstance().newClosure(
{
LOGGER.fatal("Engine Error: LuaState at mem address $pointer has panicked!")
exitProcess(1)
},
CallContext.getCallContext(Type.SINT, arrayOf(Type.POINTER), CallingConvention.DEFAULT, false)
)
this.cleanable = Starbound.CLEANER.register(this) {
LuaJNR.INSTANCE.lua_close(pointer)
panic.dispose()
}
panic.setAutoRelease(false)
LuaJNR.INSTANCE.lua_atpanic(pointer, panic.address)
randomHolder = Delegate.Box(random())
LuaJNR.INSTANCE.luaopen_base(this.pointer)
this.storeGlobal("_G")
LuaJNR.INSTANCE.luaopen_table(this.pointer)
this.storeGlobal("table")
LuaJNR.INSTANCE.luaopen_coroutine(this.pointer)
this.storeGlobal("coroutine")
LuaJNR.INSTANCE.luaopen_string(this.pointer)
this.storeGlobal("string")
LuaJNR.INSTANCE.luaopen_math(this.pointer)
this.storeGlobal("math")
LuaJNR.INSTANCE.luaopen_utf8(this.pointer)
this.storeGlobal("utf8")
provideUtilityBindings(this)
provideRootBindings(this)
load(globalScript, "@starbound.jar!/scripts/global.lua")
call()
}
fun interface Fn {
fun invoke(args: ArgStack): Int
}
private var cleanable: Cleaner.Cleanable? = null
private var randomHolder: Delegate<RandomGenerator> by Delegates.notNull()
/**
* Responsible for generating random numbers using math.random
*
* Can be safely set to any other random number generator;
* math.randomseed sets this property to brand new generator with required seed
*/
var random: RandomGenerator
get() = randomHolder.get()
set(value) = randomHolder.accept(value)
private fun initializeFrom(other: LuaThread) {
randomHolder = other.randomHolder
}
fun newThread(): LuaThread {
val pointer = LuaJNR.INSTANCE.lua_newthread(pointer)
return LuaThread(pointer, stringInterner).also {
it.initializeFrom(this)
}
}
override fun close() {
this.cleanable?.clean()
}
val stackTop: Int get() {
val value = LuaJNR.INSTANCE.lua_gettop(this.pointer)
check(value >= 0) { "Invalid stack top $value" }
return value
}
/**
* Converts the acceptable index idx into an equivalent absolute index (that is, one that does not depend on the stack size).
*/
fun absStackIndex(index: Int): Int {
if (index >= 0)
return index
return LuaJNR.INSTANCE.lua_absindex(this.pointer, index)
}
private fun throwLoadError(code: Int) {
when (code) {
LUA_OK -> {}
LUA_ERRSYNTAX -> throw InvalidLuaSyntaxException(this.popString())
LUA_ERRMEM -> throw LuaMemoryAllocException()
// LUA_ERRGCMM -> throw LuaGCException()
else -> throw LuaException("Unknown Lua Loading error: $code")
}
}
fun load(code: String, chunkName: String = "main chunk") {
val bytes = code.toByteArray(charset = Charsets.UTF_8)
val buf = ByteBuffer.allocateDirect(bytes.size)
buf.order(ByteOrder.nativeOrder())
bytes.forEach(buf::put)
buf.position(0)
val closure = ClosureManager.getInstance().newClosure(
object : Closure {
override fun invoke(buffer: Closure.Buffer) {
val amountToRead = LuaJNR.RUNTIME.memoryManager.newPointer(buffer.getAddress(2))
if (buf.remaining() == 0) {
amountToRead.putLong(0L, 0L)
buffer.setAddressReturn(0L)
return
}
amountToRead.putLongLong(0L, buf.remaining().toLong())
val p = MemoryUtil.memAddress(buf)
buf.position(buf.remaining())
buffer.setAddressReturn(p)
}
},
CallContext.getCallContext(Type.POINTER, arrayOf(Type.POINTER, Type.ULONG_LONG, Type.POINTER), CallingConvention.DEFAULT, false)
)
throwLoadError(LuaJNR.INSTANCE.lua_load(pointer, closure.address, 0L, chunkName, "t"))
closure.dispose()
}
fun call(numArgs: Int = 0, numResults: Int = 0): Int {
val status = LuaJNR.INSTANCE.lua_pcallk(this.pointer, numArgs, numResults, 0, 0L, 0L)
if (status == LUA_ERRRUN) {
throw LuaRuntimeException(this.getString())
}
return status
}
/**
* Returns boolean indicating whenever function exists
*/
inline fun invokeGlobal(name: String, arguments: LuaThread.() -> Int): Boolean {
val top = stackTop
try {
val type = loadGlobal(name)
if (type != LuaType.FUNCTION)
return false
val numArguments = arguments(this)
check(numArguments >= 0) { "Invalid amount of arguments provided to Lua function" }
call(numArguments)
return true
} finally {
setTop(top)
}
}
/**
* Returns empty [KOptional] if function does not exist
*/
inline fun <T> invokeGlobal(name: String, numResults: Int, arguments: LuaThread.() -> Int, results: LuaThread.(firstValue: Int) -> T): KOptional<T> {
require(numResults > 0) { "Invalid amount of results: $numResults" }
val top = stackTop
try {
val type = loadGlobal(name)
if (type != LuaType.FUNCTION)
return KOptional()
val numArguments = arguments(this)
call(numArguments, numResults)
return KOptional(results(this, top + 1))
} finally {
setTop(top)
}
}
inline fun <T> eval(chunk: String, name: String = "eval", numResults: Int, arguments: LuaThread.() -> Int, results: LuaThread.(firstValue: Int) -> T): T {
require(numResults > 0) { "Invalid amount of results: $numResults" }
val top = stackTop
try {
val numArguments = arguments(this)
load(chunk, name)
call(numArguments, numResults)
return results(this, top + 1)
} finally {
setTop(top)
}
}
inline fun eval(chunk: String, name: String = "eval", arguments: LuaThread.() -> Int) {
val top = stackTop
try {
val numArguments = arguments(this)
load(chunk, name)
call(numArguments, 0)
} finally {
setTop(top)
}
}
fun eval(chunk: String, name: String = "eval") {
val top = stackTop
try {
load(chunk, name)
call()
} finally {
setTop(top)
}
}
private val attachedScripts = ArrayList<String>()
private var initCalled = false
fun initScripts(callInit: Boolean = true): Boolean {
check(!initCalled) { "Already initialized scripts!" }
initCalled = true
if (attachedScripts.isEmpty()) {
return true
}
val loadScripts = attachedScripts.map { Starbound.readLuaScript(it) to it }
attachedScripts.clear()
try {
// minor hiccups during unpopulated script cache should be tolerable
for ((chunk, path) in loadScripts) {
load(chunk.join(), "@$path")
call()
}
} catch (err: Exception) {
LOGGER.error("Failed to attach scripts to Lua environment", err)
return false
}
try {
if (callInit) {
val type = loadGlobal("init")
if (type == LuaType.FUNCTION) {
call()
} else if (type == LuaType.NIL || type == LuaType.NONE) {
pop()
} else {
pop()
throw LuaRuntimeException("init is not a function: $type")
}
}
} catch (err: Exception) {
LOGGER.error("Failed to call init() in Lua environment", err)
return false
}
return true
}
fun attach(script: AssetPath) {
attach(script.fullPath)
}
fun attach(script: String) {
if (initCalled) {
// minor hiccups during unpopulated script cache should be tolerable
load(Starbound.readLuaScript(script).join(), "@$script")
call()
} else {
attachedScripts.add(script)
}
}
fun attach(script: Collection<AssetPath>) {
script.forEach { attach(it) }
}
@JvmName("attachAsStrings")
fun attach(script: Collection<String>) {
script.forEach { attach(it) }
}
fun getString(stackIndex: Int = -1, limit: Long = DEFAULT_STRING_LIMIT): String? {
if (!this.isString(stackIndex))
return null
return getStringRaw(stackIndex, limit)
}
private fun getStringRaw(stackIndex: Int = -1, limit: Long = DEFAULT_STRING_LIMIT): String? {
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
val len = status[0]
stack.close()
if (len == 0L)
return ""
else if (len >= limit)
throw IllegalStateException("Unreasonably long Lua string: $len")
val readBytes = ByteArray(len.toInt())
p.get(0L, readBytes, 0, readBytes.size)
//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
fun isFunction(stackIndex: Int = -1): Boolean = this.typeAt(stackIndex) == LuaType.FUNCTION
fun isInteger(stackIndex: Int = -1): Boolean = LuaJNR.INSTANCE.lua_isinteger(this.pointer, this.absStackIndex(stackIndex)) > 0
fun isLightUserdata(stackIndex: Int = -1): Boolean = this.typeAt(stackIndex) == LuaType.LIGHTUSERDATA
fun isNil(stackIndex: Int = -1): Boolean = this.typeAt(stackIndex) == LuaType.NIL
fun isNone(stackIndex: Int = -1): Boolean = this.typeAt(stackIndex) == LuaType.NONE
fun isNoneOrNil(stackIndex: Int = -1): Boolean = this.typeAt(stackIndex).let { it == LuaType.NIL || it == LuaType.NONE }
fun isNumber(stackIndex: Int = -1): Boolean = LuaJNR.INSTANCE.lua_isnumber(this.pointer, this.absStackIndex(stackIndex)) > 0
fun isString(stackIndex: Int = -1): Boolean = LuaJNR.INSTANCE.lua_isstring(this.pointer, this.absStackIndex(stackIndex)) > 0
fun isTable(stackIndex: Int = -1): Boolean = this.typeAt(stackIndex) == LuaType.TABLE
fun isThread(stackIndex: Int = -1): Boolean = this.typeAt(stackIndex) == LuaType.THREAD
fun isUserdata(stackIndex: Int = -1): Boolean = LuaJNR.INSTANCE.lua_isuserdata(this.pointer, this.absStackIndex(stackIndex)) > 0
fun isBoolean(stackIndex: Int = -1): Boolean = this.typeAt(stackIndex) == LuaType.BOOLEAN
fun getBoolean(stackIndex: Int = -1): Boolean? {
if (!this.isBoolean(stackIndex))
return null
return LuaJNR.INSTANCE.lua_toboolean(this.pointer, stackIndex) > 0
}
private fun getBooleanRaw(stackIndex: Int = -1): Boolean {
return LuaJNR.INSTANCE.lua_toboolean(this.pointer, stackIndex) > 0
}
fun getLong(stackIndex: Int = -1): Long? {
if (!this.isNumber(stackIndex))
return null
val stack = MemoryStack.stackPush()
val status = stack.mallocInt(1)
val value = LuaJNR.INSTANCE.lua_tointegerx(this.pointer, stackIndex, MemoryUtil.memAddress(status))
val b = status[0] > 0
stack.close()
if (!b)
return null
return value
}
private fun getLongRaw(stackIndex: Int = -1): Long {
val stack = MemoryStack.stackPush()
val status = stack.mallocInt(1)
val value = LuaJNR.INSTANCE.lua_tointegerx(this.pointer, stackIndex, MemoryUtil.memAddress(status))
val b = status[0] > 0
stack.close()
if (!b) throw NumberFormatException("Lua was unable to parse Long present on stack at ${this.absStackIndex(stackIndex)} ($stackIndex)")
return value
}
fun getDouble(stackIndex: Int = -1): Double? {
if (!this.isNumber(stackIndex))
return null
val stack = MemoryStack.stackPush()
val status = stack.mallocInt(1)
val value = LuaJNR.INSTANCE.lua_tonumberx(this.pointer, stackIndex, MemoryUtil.memAddress(status))
val b = status[0] > 0
stack.close()
if (!b)
return null
return value
}
private fun getDoubleRaw(stackIndex: Int = -1): Double {
val stack = MemoryStack.stackPush()
val status = stack.mallocInt(1)
val value = LuaJNR.INSTANCE.lua_tonumberx(this.pointer, stackIndex, MemoryUtil.memAddress(status))
val b = status[0] > 0
stack.close()
if (!b) throw NumberFormatException("Lua was unable to parse Double present on stack at ${this.absStackIndex(stackIndex)} ($stackIndex)")
return value
}
fun typeAt(stackIndex: Int = -1): LuaType {
return LuaType.valueOf(LuaJNR.INSTANCE.lua_type(this.pointer, stackIndex))
}
/**
* 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 (val type = this.typeAt(abs)) {
LuaType.NONE -> null
LuaType.NIL -> JsonNull.INSTANCE
LuaType.BOOLEAN -> InternedJsonElementAdapter.of(this.getBooleanRaw(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 -> {
val values = HashMap<Any, JsonElement>()
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
val pairs = JsonObject()
this.push()
val top = this.stackTop
while (LuaJNR.INSTANCE.lua_next(this.pointer, abs) != 0) {
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)
}
LuaJNR.INSTANCE.lua_settop(this.pointer, top)
}
return pairs
}
fun iterateTable(stackIndex: Int = -1, keyVisitor: LuaThread.(stackIndex: Int) -> Unit, valueVisitor: LuaThread.(stackIndex: Int) -> Unit) {
val abs = this.absStackIndex(stackIndex)
if (!this.isTable(abs))
return
this.push()
val top = this.stackTop
try {
while (LuaJNR.INSTANCE.lua_next(this.pointer, abs) != 0) {
keyVisitor(this, abs + 1)
valueVisitor(this, abs + 2)
LuaJNR.INSTANCE.lua_settop(this.pointer, top)
}
} finally {
LuaJNR.INSTANCE.lua_settop(this.pointer, top - 1)
}
}
fun <T> readTableKeys(stackIndex: Int = -1, keyVisitor: LuaThread.(stackIndex: Int) -> T): MutableList<T>? {
val abs = this.absStackIndex(stackIndex)
if (!this.isTable(abs))
return null
val values = ArrayList<T>()
this.push()
val top = this.stackTop
try {
while (LuaJNR.INSTANCE.lua_next(this.pointer, abs) != 0) {
values.add(keyVisitor(this, abs + 1))
LuaJNR.INSTANCE.lua_settop(this.pointer, top)
}
} finally {
LuaJNR.INSTANCE.lua_settop(this.pointer, top - 1)
}
return values
}
fun <T> readTableValues(stackIndex: Int = -1, valueVisitor: LuaThread.(stackIndex: Int) -> T): MutableList<T>? {
val abs = this.absStackIndex(stackIndex)
if (!this.isTable(abs))
return null
val values = ArrayList<T>()
this.push()
val top = this.stackTop
try {
while (LuaJNR.INSTANCE.lua_next(this.pointer, abs) != 0) {
values.add(valueVisitor(this, abs + 2))
LuaJNR.INSTANCE.lua_settop(this.pointer, top)
}
} finally {
LuaJNR.INSTANCE.lua_settop(this.pointer, top - 1)
}
return values
}
fun <K, V> readTable(stackIndex: Int = -1, keyVisitor: LuaThread.(stackIndex: Int) -> K, valueVisitor: LuaThread.(stackIndex: Int) -> V): MutableList<Pair<K, V>>? {
val abs = this.absStackIndex(stackIndex)
if (!this.isTable(abs))
return null
val values = ArrayList<Pair<K, V>>()
this.push()
val top = this.stackTop
try {
while (LuaJNR.INSTANCE.lua_next(this.pointer, abs) != 0) {
values.add(keyVisitor(this, abs + 1) to valueVisitor(this, abs + 2))
LuaJNR.INSTANCE.lua_settop(this.pointer, top)
}
} finally {
LuaJNR.INSTANCE.lua_settop(this.pointer, top - 1)
}
return values
}
fun loadTableValue(stackIndex: Int = -2): LuaType {
return LuaType.valueOf(LuaJNR.INSTANCE.lua_gettable(this.pointer, stackIndex))
}
fun loadTableValue(name: String): LuaType {
push(name)
return loadTableValue()
}
fun popBoolean(): Boolean? {
try {
return this.getBoolean()
} finally {
this.pop()
}
}
fun popLong(): Long? {
try {
return this.getLong()
} finally {
this.pop()
}
}
fun popDouble(): Double? {
try {
return this.getDouble()
} finally {
this.pop()
}
}
fun popJson(): JsonElement? {
try {
return this.getJson()
} finally {
this.pop()
}
}
fun popTable(): JsonObject? {
try {
return this.getTable()
} finally {
this.pop()
}
}
fun popString(limit: Long = DEFAULT_STRING_LIMIT): String? {
try {
return this.getString(limit = limit)
} finally {
this.pop()
}
}
fun pop(amount: Int = 1): Int {
if (amount == 0) return 0
check(amount > 0) { "Invalid amount to pop: $amount" }
val old = this.stackTop
val new = (old - amount).coerceAtLeast(0)
LuaJNR.INSTANCE.lua_settop(this.pointer, new)
return old - new
}
fun storeGlobal(name: String) {
LuaJNR.INSTANCE.lua_setglobal(this.pointer, name)
}
fun loadGlobal(name: String): LuaType {
return LuaType.valueOf(LuaJNR.INSTANCE.lua_getglobal(this.pointer, name))
}
inner class ArgStack(val top: Int) {
val lua get() = this@LuaThread
var position = 1
fun peek(position: Int = this.position): LuaType {
if (position !in 1 .. top)
return LuaType.NONE
return this@LuaThread.typeAt(position)
}
fun hasNext(): Boolean {
return position <= top
}
fun nextString(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): String {
if (position !in 1 ..this.top)
throw IllegalArgumentException("bad argument #$position: string expected, got nil")
return this@LuaThread.getString(position, limit = limit)
?: throw IllegalArgumentException("bad argument #$position: string expected, got ${this@LuaThread.typeAt(position)}")
}
fun nextOptionalString(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): String? {
val type = this@LuaThread.typeAt(position)
if (type != LuaType.STRING && type != LuaType.NIL && type != LuaType.NONE)
throw IllegalArgumentException("bad argument #$position: string expected, got $type")
return this@LuaThread.getString(position, limit = limit)
}
fun nextLong(position: Int = this.position++): Long {
if (position !in 1 ..this.top)
throw IllegalArgumentException("bad argument #$position: number expected, got nil")
return this@LuaThread.getLong(position)
?: throw IllegalArgumentException("bad argument #$position: long expected, got ${this@LuaThread.typeAt(position)}")
}
fun nextJson(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@LuaThread.getJson(position, limit = limit)
return value ?: throw IllegalArgumentException("bad argument #$position: anything expected, got ${this@LuaThread.typeAt(position)}")
}
fun nextOptionalJson(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonElement? {
if (position !in 1 ..this.top)
return null
return this@LuaThread.getJson(position, limit = limit)
}
fun nextTable(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonObject {
if (position !in 1 ..this.top)
throw IllegalArgumentException("bad argument #$position: table expected, got nil")
val value = this@LuaThread.getTable(position, limit = limit)
return value ?: throw IllegalArgumentException("Lua code error: bad argument #$position: table expected, got ${this@LuaThread.typeAt(position)}")
}
fun nextAny(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")
return this@LuaThread.getJson(position, limit = limit)
}
fun nextDouble(position: Int = this.position++): Double {
if (position !in 1 ..this.top)
throw IllegalArgumentException("bad argument #$position: number expected, got nil")
return this@LuaThread.getDouble(position)
?: throw IllegalArgumentException("bad argument #$position: number expected, got ${this@LuaThread.typeAt(position)}")
}
fun nextOptionalDouble(position: Int = this.position++): Double? {
val type = this@LuaThread.typeAt(position)
if (type != LuaType.NUMBER && type != LuaType.NIL && type != LuaType.NONE)
throw IllegalArgumentException("bad argument #$position: double expected, got $type")
return this@LuaThread.getDouble(position)
}
fun nextOptionalLong(position: Int = this.position++): Long? {
val type = this@LuaThread.typeAt(position)
if (type != LuaType.NUMBER && type != LuaType.NIL && type != LuaType.NONE)
throw IllegalArgumentException("bad argument #$position: integer expected, got $type")
return this@LuaThread.getLong(position)
}
fun nextOptionalBoolean(position: Int = this.position++): Boolean? {
val type = this@LuaThread.typeAt(position)
if (type == LuaType.NIL || type == LuaType.NONE)
return null
else if (type == LuaType.BOOLEAN)
return this@LuaThread.getBoolean(position)
else
throw IllegalArgumentException("Lua code error: bad argument #$position: boolean expected, got $type")
}
fun nextBoolean(position: Int = this.position++): Boolean {
if (position !in 1 ..this.top)
throw IllegalArgumentException("bad argument #$position: boolean expected, got nil")
return this@LuaThread.getBoolean(position)
?: throw IllegalArgumentException("bad argument #$position: boolean expected, got ${this@LuaThread.typeAt(position)}")
}
}
fun push(function: Fn, performanceCritical: Boolean) {
LuaJNI.lua_pushcclosure(pointer.address()) lazy@{
val realLuaState: LuaThread
if (pointer.address() != it) {
realLuaState = LuaThread(LuaJNR.RUNTIME.memoryManager.newPointer(it), stringInterner = stringInterner)
realLuaState.initializeFrom(this)
} else {
realLuaState = this
}
val args = realLuaState.ArgStack(realLuaState.stackTop)
val rememberStack: ArrayList<String>?
if (performanceCritical) {
rememberStack = null
} else {
rememberStack = ArrayList(Exception().stackTraceToString().split('\n'))
rememberStack.removeAt(0) // java.lang. ...
// rememberStack.removeAt(0) // at ... push( ... )
}
try {
val value = function.invoke(args)
check(value >= 0) { "Internal JVM error: ${function::class.qualifiedName} returned incorrect number of arguments to be popped from stack by Lua" }
return@lazy value
} catch (err: Throwable) {
try {
if (performanceCritical) {
realLuaState.push(err.stackTraceToString())
return@lazy -1
} else {
rememberStack!!
val newStack = err.stackTraceToString().split('\n').toMutableList()
val rememberIterator = rememberStack.listIterator(rememberStack.size)
val iterator = newStack.listIterator(newStack.size)
var hit = false
while (rememberIterator.hasPrevious() && iterator.hasPrevious()) {
val a = rememberIterator.previous()
val b = iterator.previous()
if (a == b) {
hit = true
iterator.remove()
} else {
break
}
}
if (hit) {
newStack[newStack.size - 1] = "\t<...>"
}
realLuaState.push(newStack.joinToString("\n"))
return@lazy -1
}
} catch(err2: Throwable) {
realLuaState.push("JVM suffered an exception while handling earlier exception: ${err2.stackTraceToString()}; earlier: ${err.stackTraceToString()}")
return@lazy -1
}
}
}
}
fun push(function: Fn) = this.push(function, !RECORD_STACK_TRACES)
fun moveStackValuesOnto(other: LuaThread, amount: Int = 1) {
LuaJNR.INSTANCE.lua_xmove(pointer, other.pointer, amount)
}
fun push() {
LuaJNR.INSTANCE.lua_pushnil(this.pointer)
}
fun push(value: Long) {
LuaJNR.INSTANCE.lua_pushinteger(this.pointer, value)
}
fun push(value: Long?) {
if (value == null) {
LuaJNR.INSTANCE.lua_pushnil(pointer)
} else {
LuaJNR.INSTANCE.lua_pushinteger(this.pointer, value)
}
}
fun push(value: Double) {
LuaJNR.INSTANCE.lua_pushnumber(this.pointer, value)
}
fun push(value: Float) {
LuaJNR.INSTANCE.lua_pushnumber(this.pointer, value.toDouble())
}
fun push(value: Boolean) {
LuaJNR.INSTANCE.lua_pushboolean(this.pointer, if (value) 1 else 0)
}
fun push(value: Double?) {
if (value == null) {
LuaJNR.INSTANCE.lua_pushnil(pointer)
} else {
LuaJNR.INSTANCE.lua_pushnumber(this.pointer, value)
}
}
fun push(value: Float?) {
if (value == null) {
LuaJNR.INSTANCE.lua_pushnil(pointer)
} else {
LuaJNR.INSTANCE.lua_pushnumber(this.pointer, value.toDouble())
}
}
fun push(value: Boolean?) {
if (value == null) {
LuaJNR.INSTANCE.lua_pushnil(pointer)
} else {
LuaJNR.INSTANCE.lua_pushboolean(this.pointer, if (value) 1 else 0)
}
}
fun push(value: String) {
pushStringIntoThread(this, value)
}
fun copy(fromIndex: Int, toIndex: Int) {
LuaJNR.INSTANCE.lua_copy(pointer, fromIndex, toIndex)
}
fun dup() {
push()
copy(-2, -1)
}
fun dup(fromIndex: Int) {
push()
copy(fromIndex, -1)
}
fun setTop(topIndex: Int) {
LuaJNR.INSTANCE.lua_settop(pointer, topIndex)
}
fun storeRef(tableIndex: Int) {
LuaJNR.INSTANCE.luaL_ref(pointer, tableIndex)
}
fun pushTable(arraySize: Int = 0, hashSize: Int = 0) {
LuaJNR.INSTANCE.lua_createtable(pointer, arraySize, hashSize)
}
fun setTableValue(stackIndex: Int = -3) {
LuaJNR.INSTANCE.lua_settable(this.pointer, stackIndex)
}
fun setTableValue(key: String, value: Fn) {
this.push(key)
this.push(value)
this.setTableValue()
}
fun setTableValue(key: String, value: JsonElement?) {
this.push(key)
this.push(value)
this.setTableValue()
}
@Deprecated("Lua function is a stub")
fun setTableValueToStub(key: String) {
setTableValue(key) { throw NotImplementedError("NYI: $key") }
}
fun setTableValue(key: String, value: Int) {
this.push(key)
this.push(value.toLong())
this.setTableValue()
}
fun setTableValue(key: String, value: Long) {
this.push(key)
this.push(value)
this.setTableValue()
}
fun setTableValue(key: String, value: String?) {
value ?: return
this.push(key)
this.push(value)
this.setTableValue()
}
fun setTableValue(key: String, value: Float) {
this.push(key)
this.push(value)
this.setTableValue()
}
fun setTableValue(key: String, value: Double) {
this.push(key)
this.push(value)
this.setTableValue()
}
fun setTableValue(key: Int, value: JsonElement?) {
return setTableValue(key.toLong(), value)
}
fun setTableValue(key: Int, value: Int) {
return setTableValue(key.toLong(), value)
}
fun setTableValue(key: Int, value: Long) {
return setTableValue(key.toLong(), value)
}
fun setTableValue(key: Int, value: String) {
return setTableValue(key.toLong(), value)
}
fun setTableValue(key: Int, value: Float) {
return setTableValue(key.toLong(), value)
}
fun setTableValue(key: Int, value: Double) {
return setTableValue(key.toLong(), value)
}
fun setTableValue(key: Long, value: JsonElement?) {
this.push(key)
this.push(value)
this.setTableValue()
}
fun setTableValue(key: Long, value: Int) {
return setTableValue(key, value.toLong())
}
fun setTableValue(key: Long, value: Long) {
this.push(key)
this.push(value)
this.setTableValue()
}
fun setTableValue(key: Long, value: String) {
this.push(key)
this.push(value)
this.setTableValue()
}
fun setTableValue(key: Long, value: Float) {
return setTableValue(key, value.toDouble())
}
fun setTableValue(key: Long, value: Double) {
this.push(key)
this.push(value)
this.setTableValue()
}
fun push(value: JsonElement?) {
when (value) {
null, JsonNull.INSTANCE -> {
this.push()
}
is JsonPrimitive -> {
if (value.isNumber) {
val num = value.asNumber
when (num) {
is Int, is Long -> this.push(num.toLong())
else -> this.push(num.toDouble())
}
} else if (value.isString) {
this.push(value.asString)
} else if (value.isBoolean) {
this.push(value.asBoolean)
} else {
throw IllegalArgumentException(value.toString())
}
}
is JsonArray -> {
this.loadGlobal("jarray")
this.call(numResults = 1)
val index = this.stackTop
for ((i, v) in value.withIndex()) {
this.push(i + 1L)
this.push(v)
this.setTableValue(index)
}
}
is JsonObject -> {
this.loadGlobal("jobject")
this.call(numResults = 1)
val index = this.stackTop
for ((k, v) in value.entrySet()) {
this.push(k)
this.push(v)
this.setTableValue(index)
}
}
else -> {
throw IllegalArgumentException(value.toString())
}
}
}
companion object {
val LOGGER = LogManager.getLogger()
fun loadInternalScript(name: String): String {
return LuaThread::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<Long>()
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()
if (p == null) {
p = MemoryIO.getInstance().allocateMemory(DEFAULT_STRING_LIMIT, false)
if (p == 0L) {
throw OutOfMemoryError("Unable to allocate new string shared buffer")
}
sharedBuffers.set(p)
val p2 = p
Starbound.CLEANER.register(Thread.currentThread()) {
MemoryIO.getInstance().freeMemory(p2)
}
}
return p
}
fun pushStringIntoThread(lua: LuaThread, value: String) {
if (value.isEmpty()) {
LuaJNR.INSTANCE.lua_pushlstring(lua.pointer, lua.pointer.address(), 0)
return
}
val bytes = value.toByteArray(Charsets.UTF_8)
if (bytes.size < DEFAULT_STRING_LIMIT) {
MemoryIO.getInstance().putByteArray(sharedStringBufferPtr, bytes, 0, bytes.size)
LuaJNR.INSTANCE.lua_pushlstring(lua.pointer, sharedStringBufferPtr, bytes.size.toLong())
} else {
val mem = MemoryIO.getInstance()
val alloc = mem.allocateMemory(bytes.size.toLong(), false)
if (alloc == 0L)
throw OutOfMemoryError("Unable to allocate ${bytes.size} bytes on heap")
try {
mem.putByteArray(alloc, bytes, 0, bytes.size)
LuaJNR.INSTANCE.lua_pushlstring(lua.pointer, alloc, bytes.size.toLong())
} finally {
mem.freeMemory(alloc)
}
}
}
const val LUA_TNONE = -1
const val LUA_TNIL = 0
const val LUA_TBOOLEAN = 1
const val LUA_TLIGHTUSERDATA = 2
const val LUA_TNUMBER = 3
const val LUA_TSTRING = 4
const val LUA_TTABLE = 5
const val LUA_TFUNCTION = 6
const val LUA_TUSERDATA = 7
const val LUA_TTHREAD = 8
const val LUA_NUMTYPES = 9
const val CHUNK_READ_SIZE = 2L shl 10
const val DEFAULT_STRING_LIMIT = 2L shl 24
const val RECORD_STACK_TRACES = false
const val LUA_HINT_NONE = 0
const val LUA_HINT_ARRAY = 1
const val LUA_HINT_OBJECT = 2
}
}