From c5dc4a465a2c5cf7ee73e12ad2da272b12471198 Mon Sep 17 00:00:00 2001
From: DBotThePony <dbotthepony@yandex.ru>
Date: Sat, 14 Dec 2024 20:12:09 +0700
Subject: [PATCH] Bring back most of native Lua code stuff

---
 .../ru/dbotthepony/kstarbound/lua/LuaJNR.java |  12 +-
 .../dbotthepony/kstarbound/lua/Conversions.kt |  85 +++
 .../ru/dbotthepony/kstarbound/lua/LuaState.kt | 624 ++++++++----------
 .../ru/dbotthepony/kstarbound/lua/LuaType.kt  |  31 +
 src/main/resources/scripts/config.lua         |  17 +
 src/main/resources/scripts/global.lua         |  56 ++
 .../resources/scripts/message_handler.lua     |  27 +
 7 files changed, 486 insertions(+), 366 deletions(-)
 create mode 100644 src/main/resources/scripts/config.lua
 create mode 100644 src/main/resources/scripts/global.lua
 create mode 100644 src/main/resources/scripts/message_handler.lua

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<Any?>): 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<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
 
-		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<JsonElement>()
-		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 <T : Any> 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 <T : Any> 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<String?>) {
-		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<String?>) {
-		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 <T : Any> 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 <T : Any> 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<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()
 
@@ -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