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

340 lines
9.4 KiB
Kotlin

package ru.dbotthepony.kstarbound.lua
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString
import org.classdump.luna.LuaObject
import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.LuaType
import org.classdump.luna.StateContext
import org.classdump.luna.Table
import org.classdump.luna.Variable
import org.classdump.luna.compiler.CompilerChunkLoader
import org.classdump.luna.compiler.CompilerSettings
import org.classdump.luna.env.RuntimeEnvironments
import org.classdump.luna.exec.CallPausedException
import org.classdump.luna.exec.Continuation
import org.classdump.luna.exec.DirectCallExecutor
import org.classdump.luna.impl.DefaultTable
import org.classdump.luna.impl.NonsuspendableFunctionException
import org.classdump.luna.lib.BasicLib
import org.classdump.luna.lib.CoroutineLib
import org.classdump.luna.lib.MathLib
import org.classdump.luna.lib.OsLib
import org.classdump.luna.lib.StringLib
import org.classdump.luna.lib.TableLib
import org.classdump.luna.lib.Utf8Lib
import org.classdump.luna.load.ChunkFactory
import org.classdump.luna.runtime.AbstractFunction1
import org.classdump.luna.runtime.Coroutine
import org.classdump.luna.runtime.ExecutionContext
import org.classdump.luna.runtime.LuaFunction
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.lua.bindings.provideRootBindings
import ru.dbotthepony.kstarbound.lua.bindings.provideUtilityBindings
import ru.dbotthepony.kstarbound.util.random.random
import java.util.concurrent.atomic.AtomicLong
class LuaEnvironment : StateContext {
private var nilMeta: Table? = null
private var booleanMeta: Table? = null
private var numberMeta: Table? = null
private var stringMeta: Table? = null
private var functionMeta: Table? = null
private var threadMeta: Table? = null
private var lightUserdataMeta: Table? = null
override fun getNilMetatable(): Table? {
return nilMeta
}
override fun getBooleanMetatable(): Table? {
return booleanMeta
}
override fun getNumberMetatable(): Table? {
return numberMeta
}
override fun getStringMetatable(): Table? {
return stringMeta
}
override fun getFunctionMetatable(): Table? {
return functionMeta
}
override fun getThreadMetatable(): Table? {
return threadMeta
}
override fun getLightUserdataMetatable(): Table? {
return lightUserdataMeta
}
override fun getMetatable(instance: Any?): Table? {
if (instance is LuaObject)
return instance.metatable
return when (val type = LuaType.typeOf(instance)!!) {
LuaType.NIL -> nilMeta
LuaType.BOOLEAN -> booleanMeta
LuaType.NUMBER -> numberMeta
LuaType.STRING -> stringMeta
LuaType.FUNCTION -> functionMeta
LuaType.USERDATA -> lightUserdataMeta
LuaType.THREAD -> threadMeta
else -> throw IllegalArgumentException("Illegal type: $type")
}
}
override fun setNilMetatable(table: Table?): Table? {
val old = nilMeta; nilMeta = table; return old
}
override fun setBooleanMetatable(table: Table?): Table? {
val old = booleanMeta; booleanMeta = table; return old
}
override fun setNumberMetatable(table: Table?): Table? {
val old = numberMeta; numberMeta = table; return old
}
override fun setStringMetatable(table: Table?): Table? {
val old = stringMeta; stringMeta = table; return old
}
override fun setFunctionMetatable(table: Table?): Table? {
val old = functionMeta; functionMeta = table; return old
}
override fun setThreadMetatable(table: Table?): Table? {
val old = threadMeta; threadMeta = table; return old
}
override fun setLightUserdataMetatable(table: Table?): Table? {
val old = lightUserdataMeta; lightUserdataMeta = table; return old
}
override fun setMetatable(instance: Any?, table: Table?): Table? {
if (instance is LuaObject)
return instance.setMetatable(table)
else
throw IllegalArgumentException("Can not set metatable of ${LuaType.typeOf(instance)}")
}
override fun newTable(): Table {
return DefaultTable()
}
override fun newTable(array: Int, hash: Int): Table {
return DefaultTable()
}
val globals: Table = newTable()
val executor: DirectCallExecutor = DirectCallExecutor.newExecutor()
var random = random()
init {
globals["_G"] = globals
globals["assert"] = BasicLib.assertFn()
globals["error"] = BasicLib.error()
globals["getmetatable"] = BasicLib.getmetatable()
globals["ipairs"] = BasicLib.ipairs()
globals["next"] = BasicLib.next()
globals["pairs"] = BasicLib.pairs()
globals["pcall"] = BasicLib.pcall()
globals["rawequal"] = BasicLib.rawequal()
globals["rawget"] = BasicLib.rawget()
globals["rawlen"] = BasicLib.rawlen()
globals["rawset"] = BasicLib.rawset()
globals["select"] = BasicLib.select()
globals["setmetatable"] = BasicLib.setmetatable()
globals["tostring"] = BasicLib.tostring()
globals["tonumber"] = BasicLib.tonumber()
globals["type"] = BasicLib.type()
globals["_VERSION"] = BasicLib._VERSION
globals["xpcall"] = BasicLib.xpcall()
globals["print"] = PrintFunction(globals)
// why not use _ENV anyway lol
globals["self"] = newTable()
CoroutineLib.installInto(this, globals)
TableLib.installInto(this, globals)
MathLib.installInto(this, globals)
StringLib.installInto(this, globals)
OsLib.installInto(this, globals, RuntimeEnvironments.system())
val math = globals["math"] as Table
math["random"] = luaFunction { origin: Number?, bound: Number? ->
if (origin == null && bound == null) {
returnBuffer.setTo(random.nextDouble())
} else if (bound == null) {
val origin = origin!!.toLong()
if (origin > 1L) {
returnBuffer.setTo(random.nextLong(1L, origin))
} else if (origin == 1L) {
returnBuffer.setTo(1L)
} else {
throw LuaRuntimeException("bad argument #1 to 'random' (interval is empty)")
}
} else {
val origin = origin!!.toLong()
val bound = bound.toLong()
if (bound > origin) {
returnBuffer.setTo(random.nextLong(origin, bound))
} else if (bound == origin) {
returnBuffer.setTo(origin)
} else {
throw LuaRuntimeException("bad argument #1 to 'random' (interval is empty)")
}
}
}
math["randomseed"] = luaFunction { seed: Number ->
random = random(seed.toLong())
}
// TODO: NYI, maybe polyfill?
Utf8Lib.installInto(this, globals)
provideRootBindings(this)
provideUtilityBindings(this)
}
private val scripts = ObjectArraySet<ChunkFactory>()
private var initCalled = false
private val loadedScripts = ObjectArraySet<String>()
val require = LuaRequire()
inner class LuaRequire : AbstractFunction1<ByteString>() {
override fun resume(context: ExecutionContext?, suspendedState: Any?) {
throw NonsuspendableFunctionException(this::class.java)
}
override fun invoke(context: ExecutionContext, arg1: ByteString) {
val name = arg1.decode()
if (loadedScripts.add(name)) {
val script = Starbound.loadScript(name)
executor.call(this@LuaEnvironment, script.newInstance(Variable(globals)))
}
}
}
init {
globals["require"] = require
}
fun attach(script: ChunkFactory) {
if (initCalled) {
executor.call(this@LuaEnvironment, script.newInstance(Variable(globals)))
} else {
scripts.add(script)
}
}
fun attach(scripts: Collection<AssetPath>) {
if (initCalled) {
for (name in scripts) {
if (loadedScripts.add(name.fullPath)) {
val script = Starbound.loadScript(name.fullPath)
executor.call(this@LuaEnvironment, script.newInstance(Variable(globals)))
}
}
} else {
for (script in scripts) {
if (loadedScripts.add(script.fullPath)) {
this.scripts.add(Starbound.loadScript(script.fullPath))
}
}
}
}
fun run(chunk: ChunkFactory): Array<out Any?> {
return executor.call(this, chunk.newInstance(Variable(globals)))
}
var errorState = false
private set
fun markErrored() {
errorState = true
}
fun call(fn: Any, vararg args: Any?) = executor.call(this, fn, *args)
fun init(callInit: Boolean = true): Boolean {
check(!initCalled) { "Already called init()" }
initCalled = true
for (script in scripts) {
try {
executor.call(this, script.newInstance(Variable(globals)))
} catch (err: Throwable) {
// errorState = true
LOGGER.error("Failed to attach script to environment", err)
scripts.clear()
return false
}
}
scripts.clear()
if (callInit) {
val init = globals["init"]
if (init is LuaFunction<*, *, *, *, *>) {
try {
executor.call(this, init)
} catch (err: Throwable) {
// errorState = true
LOGGER.error("Exception on init()", err)
return false
}
}
}
return true
}
fun invokeGlobal(name: String, vararg arguments: Any?): Array<Any?> {
if (errorState || !initCalled)
return arrayOf()
val load = globals[name]
if (load is LuaFunction<*, *, *, *, *>) {
return try {
executor.call(this, load, *arguments)
} catch (err: Throwable) {
// errorState = true
LOGGER.error("Exception while calling global $name", err)
arrayOf()
}
}
return arrayOf()
}
private val loader by lazy { CompilerChunkLoader.of(CompilerSettings.defaultNoAccountingSettings(), "sb_lua${COUNTER.getAndIncrement()}_") }
// leaks memory until LuaEnvironment goes out of scope. Too bad!
fun eval(chunk: String, name: String = "eval"): Array<Any?> {
return executor.call(this, loader.compileTextChunk(chunk, name).newInstance(Variable(globals)))
}
companion object {
private val LOGGER = LogManager.getLogger()
private val COUNTER = AtomicLong()
}
}