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() private var initCalled = false private val loadedScripts = ObjectArraySet() val require = LuaRequire() inner class LuaRequire : AbstractFunction1() { 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) { 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 { 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 { 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 { return executor.call(this, loader.compileTextChunk(chunk, name).newInstance(Variable(globals))) } companion object { private val LOGGER = LogManager.getLogger() private val COUNTER = AtomicLong() } }