From f82b48672e86c57ca46d7860409596e714db8219 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Mon, 16 Dec 2024 16:13:36 +0700 Subject: [PATCH] require, math.random and math.randomseed implementations --- .../dbotthepony/kstarbound/lua/LuaThread.kt | 135 +++++++++++++++--- src/main/resources/scripts/global.lua | 131 +++++++++++++++-- 2 files changed, 241 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt index 511b1363..2a21511f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt @@ -17,13 +17,18 @@ 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.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter +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") @@ -50,6 +55,8 @@ class LuaThread private constructor( 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) @@ -70,13 +77,60 @@ class LuaThread private constructor( storeGlobal("__print") + push { + val path = getString() + + try { + load(Starbound.readLuaScript(path).join(), "@$path") + 1 + } catch (err: Exception) { + LOGGER.error("Exception loading Lua script $path", err) + throw err + } + } + + storeGlobal("__require") + + push { + push(random.nextDouble()) + 1 + } + + storeGlobal("__random_double") + + push { + push(random.nextLong(getLong(), getLong())) + 1 + } + + storeGlobal("__random_long") + + push { + random = random(getLong()) + 0 + } + + storeGlobal("__random_seed") + load(globalScript, "@starbound.jar!/scripts/global.lua") call() } private var cleanable: Cleaner.Cleanable? = null + private var randomHolder: Delegate 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 { @@ -145,7 +199,7 @@ class LuaThread private constructor( CallContext.getCallContext(Type.POINTER, arrayOf(Type.POINTER, Type.ULONG_LONG, Type.POINTER), CallingConvention.DEFAULT, false) ) - this.throwLoadError(LuaJNR.INSTANCE.lua_load(this.pointer, closure.address, 0L, chunkName, "t")) + throwLoadError(LuaJNR.INSTANCE.lua_load(pointer, closure.address, 0L, chunkName, "t")) closure.dispose() } @@ -159,6 +213,66 @@ class LuaThread private constructor( return status } + private val attachedScripts = ArrayList() + 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.fullPath) to it.fullPath } + 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) { + if (initCalled) { + // minor hiccups during unpopulated script cache should be tolerable + load(Starbound.readLuaScript(script.fullPath).join(), "@" + script.fullPath) + call() + } else { + attachedScripts.add(script) + } + } + + fun attach(script: Collection) { + script.forEach { attach(it) } + } + fun getString(stackIndex: Int = -1, limit: Long = DEFAULT_STRING_LIMIT): String? { if (!this.isString(stackIndex)) return null @@ -263,20 +377,7 @@ class LuaThread private constructor( } fun typeAt(stackIndex: Int = -1): LuaType { - return when (val value = LuaJNR.INSTANCE.lua_type(this.pointer, stackIndex)) { - LUA_TNONE -> LuaType.NONE - LUA_TNIL -> LuaType.NIL - LUA_TBOOLEAN -> LuaType.BOOLEAN - LUA_TLIGHTUSERDATA -> LuaType.LIGHTUSERDATA - LUA_TNUMBER -> LuaType.NUMBER - LUA_TSTRING -> LuaType.STRING - LUA_TTABLE -> LuaType.TABLE - LUA_TFUNCTION -> LuaType.FUNCTION - LUA_TUSERDATA -> LuaType.USERDATA - LUA_TTHREAD -> LuaType.THREAD - LUA_NUMTYPES -> LuaType.UMTYPES - else -> throw RuntimeException("Invalid Lua type: $value") - } + return LuaType.valueOf(LuaJNR.INSTANCE.lua_type(this.pointer, stackIndex)) } /** @@ -621,8 +722,8 @@ class LuaThread private constructor( LuaJNR.INSTANCE.lua_setglobal(this.pointer, name) } - fun loadGlobal(name: String) { - LuaJNR.INSTANCE.lua_getglobal(this.pointer, name) + fun loadGlobal(name: String): LuaType { + return LuaType.valueOf(LuaJNR.INSTANCE.lua_getglobal(this.pointer, name)) } inner class ArgStack(val top: Int) { diff --git a/src/main/resources/scripts/global.lua b/src/main/resources/scripts/global.lua index e7e39d83..c63bcdaf 100644 --- a/src/main/resources/scripts/global.lua +++ b/src/main/resources/scripts/global.lua @@ -6,6 +6,15 @@ LUA_HINT_NONE = 0 LUA_HINT_ARRAY = 1 LUA_HINT_OBJECT = 2 +local rawset = rawset +local type = type +local pairs = pairs +local ipairs = ipairs +local setmetatable = setmetatable +local select = select +local math = math +local string = string + -- this replicates original engine code, but it shouldn't work in first place local function __newindex(self, key, value) local nils = getmetatable(self).__nils @@ -107,10 +116,12 @@ function jresize(self, target) if meta and meta.__nils then local indices = {} + local i = 1 for k, v in pairs(meta.__nils) do if type(k) == 'number' and k % 1.0 == 0.0 and k > target then - table.insert(indices, k) + indices[i] = k + i = i + 1 end end @@ -120,10 +131,12 @@ function jresize(self, target) end local indices = {} + local i = 1 for k, v in pairs(self) do if type(k) == 'number' and k % 1.0 == 0.0 and k > target then - table.insert(indices, k) + indices[i] = k + i = i + 1 end end @@ -132,14 +145,116 @@ function jresize(self, target) end end -local __print = __print +do + local __print = __print -function print(...) - local values = {...} + function print(...) + local values = {} - for i, v in ipairs(values) do - values[i] = tostring(v) + -- this is required for properly printing nils + for i = 1, select('#', ...) do + values[i] = tostring(select(i, ...)) + end + + __print(table.concat(values, '\t')) + end +end + +do + local __require = __require + local loadedScripts = {} + + function require(path, ...) + if type(path) ~= 'string' then + error('bad argument #1 to require: string expected, got ' .. type(path), 2) + end + + if string.sub(path, 1, 1) ~= '/' then + error('require: script path must be absolute: ' .. path) + end + + if loadedScripts[path] then return unpack(loadedScripts[path]) end + local fn = __require(path) + + if fn then + local result = {fn(...)} + loadedScripts[path] = result + return unpack(result) + else + print('Failed to require Lua script ' .. path) + loadedScripts[path] = {} + end + end +end + +do + local __random_double = __random_double + local __random_long = __random_long + local __random_seed = __random_seed + local floor = math.floor + + function math.random(a, b) + local tA = type(a) + local tB = type(b) + + if tA == 'nil' and tB == 'nil' then + return __random_double() + else + local low, high + + if tB == 'nil' then + if tA ~= 'number' then + error('bad argument #1 to math.random: integer expected, got ' .. tA, 2) + end + + low = 1 + high = tA + + if high % 1.0 ~= 0.0 then + error('bad argument #1 to math.random: integer expected, got double', 2) + end + else + if tA ~= 'number' then + error('bad argument #1 to math.random: number expected, got ' .. tA, 2) + end + + if tA ~= 'number' then + error('bad argument #2 to math.random: number expected, got ' .. tB, 2) + end + + low = tA + high = tB + + if low % 1.0 ~= 0.0 then + error('bad argument #1 to math.random: integer expected, got double', 2) + end + + if high % 1.0 ~= 0.0 then + error('bad argument #2 to math.random: integer expected, got double', 2) + end + end + + if low > high then + error('interval is empty (low: ' .. low .. ', high: ' .. high .. ')', 2) + elseif low == high then + return floor(low) + elseif high >= 9223372036854775807 || low <= -9223372036854775808 then + error('interval too large', 2) + else + return __random_long(floor(low), floor(high)) + end + end end - __print(table.concat(values, '\t')) + function math.randomseed(seed) + if type(seed) ~= 'number' then + error('bad argument #1 to math.randomseed: integer expected, got ' .. type(seed), 2) + end + + if seed % 1.0 ~= 0.0 then + error('bad argument #1 to math.randomseed: integer expected, got double', 2) + end + + __random_seed(floor(seed)) + end end