From f9b339c0e487390dc49ea2ea50c9bb16b29c82b5 Mon Sep 17 00:00:00 2001
From: DBotThePony <dbotthepony@yandex.ru>
Date: Mon, 30 Dec 2024 12:03:14 +0700
Subject: [PATCH] More native Lua work

---
 .../actor/behavior/BehaviorNodeDefinition.kt  |   2 +
 .../defs/actor/behavior/NodeOutput.kt         |   2 +-
 .../defs/actor/behavior/NodeParameter.kt      |   2 +-
 .../dbotthepony/kstarbound/lua/Conversions.kt |  30 ++--
 .../ru/dbotthepony/kstarbound/lua/Errors.kt   |   2 +-
 .../kstarbound/lua/LuaSharedState.kt          |   4 +-
 .../dbotthepony/kstarbound/lua/LuaThread.kt   |  27 ++-
 .../kstarbound/lua/LuaUpdateComponent.kt      |   4 +-
 .../lua/bindings/MonsterBindings.kt           |   4 +-
 .../lua/bindings/UtilityBindings.kt           | 164 +++++++++---------
 .../lua/bindings/WorldEntityBindings.kt       |  13 +-
 .../lua/bindings/WorldObjectBindings.kt       |   4 +-
 .../kstarbound/lua/userdata/BehaviorState.kt  |  17 +-
 .../kstarbound/server/world/ServerChunk.kt    |   4 +-
 .../kstarbound/world/SystemWorld.kt           |  18 +-
 .../world/entities/MonsterEntity.kt           |   4 +-
 src/main/resources/scripts/behavior.lua       | 115 ++++++++----
 src/main/resources/scripts/global.lua         |  68 ++++++++
 src/main/resources/scripts/world.lua          |   2 +-
 .../dbotthepony/kstarbound/test/LuaTests.kt   |  13 +-
 20 files changed, 326 insertions(+), 173 deletions(-)

diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/BehaviorNodeDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/BehaviorNodeDefinition.kt
index 1493f436..54d6e1d2 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/BehaviorNodeDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/BehaviorNodeDefinition.kt
@@ -1,6 +1,7 @@
 package ru.dbotthepony.kstarbound.defs.actor.behavior
 
 import com.google.common.collect.ImmutableMap
+import ru.dbotthepony.kstarbound.defs.AssetPath
 import ru.dbotthepony.kstarbound.json.builder.JsonFactory
 
 /**
@@ -16,4 +17,5 @@ data class BehaviorNodeDefinition(
 	 */
 	val properties: ImmutableMap<String, NodeParameter> = ImmutableMap.of(),
 	val output: ImmutableMap<String, NodeOutput> = ImmutableMap.of(),
+	val script: AssetPath? = null,
 )
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeOutput.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeOutput.kt
index c253d8d0..4487a824 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeOutput.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeOutput.kt
@@ -15,7 +15,7 @@ import ru.dbotthepony.kstarbound.lua.userdata.NodeParameterType
 data class NodeOutput(val type: NodeParameterType, val key: String? = null, val ephemeral: Boolean = false) {
 	fun push(lua: LuaThread) {
 		lua.pushTable(hashSize = 3)
-		lua.setTableValue("type", type.ordinal)
+		lua.setTableValue("type", type.ordinal + 1)
 
 		if (key != null)
 			lua.setTableValue("key", key)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameter.kt
index 25b1eee1..01af9660 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameter.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameter.kt
@@ -20,7 +20,7 @@ data class NodeParameter(val type: NodeParameterType, val value: NodeParameterVa
 
 	fun push(lua: LuaThread) {
 		lua.pushTable(hashSize = 3)
-		lua.setTableValue("type", type.ordinal)
+		lua.setTableValue("type", type.ordinal + 1)
 
 		if (value.key != null) {
 			lua.setTableValue("key", value.key)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt
index 9b10e4da..d0360c98 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt
@@ -18,6 +18,7 @@ import ru.dbotthepony.kstarbound.math.Line2d
 import ru.dbotthepony.kstarbound.math.vector.Vector2d
 import ru.dbotthepony.kstarbound.math.vector.Vector2f
 import ru.dbotthepony.kstarbound.math.vector.Vector2i
+import ru.dbotthepony.kstarbound.util.floorToInt
 import ru.dbotthepony.kstarbound.world.physics.Poly
 
 // TODO: error reporting when argument was provided, but it is malformed
@@ -223,18 +224,20 @@ fun LuaThread.getVector2i(stackIndex: Int = -1): Vector2i? {
 	push(1)
 	loadTableValue(abs)
 
-	val x = getLong()
+	// FIXME: original engine parity, where it casts doubles into ints
+	//  while it seems okay, it can cause undesired side effects
+	val x = getDouble()
 	pop()
 	x ?: return null
 
 	push(2)
 	loadTableValue(abs)
 
-	val y = getLong()
+	val y = getDouble()
 	pop()
 	y ?: return null
 
-	return Vector2i(x.toInt(), y.toInt())
+	return Vector2i(x.floorToInt(), y.floorToInt())
 }
 
 fun LuaThread.ArgStack.nextVector2i(position: Int = this.position++): Vector2i {
@@ -262,31 +265,31 @@ fun LuaThread.getColor(stackIndex: Int = -1): RGBAColor? {
 	push(1)
 	loadTableValue(abs)
 
-	val x = getLong()
+	val x = getFloat()
 	pop()
 	x ?: return null
 
 	push(2)
 	loadTableValue(abs)
 
-	val y = getLong()
+	val y = getFloat()
 	pop()
 	y ?: return null
 
 	push(3)
 	loadTableValue(abs)
 
-	val z = getLong()
+	val z = getFloat()
 	pop()
 	z ?: return null
 
 	push(4)
 	loadTableValue(abs)
 
-	val w = getLong() ?: 255L
+	val w = getFloat() ?: 255f
 	pop()
 
-	return RGBAColor(x.toInt(), y.toInt(), z.toInt(), w.toInt())
+	return RGBAColor(x / 255f, y / 255f, z / 255f, w / 255f)
 }
 
 fun LuaThread.ArgStack.nextColor(position: Int = this.position++): RGBAColor {
@@ -365,32 +368,33 @@ fun LuaThread.getAABBi(stackIndex: Int = -1): AABBi? {
 	push(1)
 	loadTableValue(abs)
 
-	val x = getLong()
+	// FIXME: original engine parity
+	val x = getDouble()
 	pop()
 	x ?: return null
 
 	push(2)
 	loadTableValue(abs)
 
-	val y = getLong()
+	val y = getDouble()
 	pop()
 	y ?: return null
 
 	push(3)
 	loadTableValue(abs)
 
-	val z = getLong()
+	val z = getDouble()
 	pop()
 	z ?: return null
 
 	push(4)
 	loadTableValue(abs)
 
-	val w = getLong()
+	val w = getDouble()
 	pop()
 	w ?: return null
 
-	return AABBi(Vector2i(x.toInt(), y.toInt()), Vector2i(z.toInt(), w.toInt()))
+	return AABBi(Vector2i(x.floorToInt(), y.floorToInt()), Vector2i(z.floorToInt(), w.floorToInt()))
 }
 
 fun LuaThread.ArgStack.nextAABBi(position: Int = this.position++): AABBi {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Errors.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Errors.kt
index b37f7d78..54288ec2 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Errors.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Errors.kt
@@ -18,4 +18,4 @@ const val LUA_ERRERR = 5
 class InvalidLuaSyntaxException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause)
 class LuaMemoryAllocException(message: String? = null, cause: Throwable? = null) : Error(message, cause)
 class LuaException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause)
-class LuaRuntimeException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause)
+class LuaRuntimeException(message: String? = null, cause: Throwable? = null, writeStackTrace: Boolean = true) : RuntimeException(message, cause, true, writeStackTrace)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaSharedState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaSharedState.kt
index faaf7b06..e29f7f9f 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaSharedState.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaSharedState.kt
@@ -60,7 +60,7 @@ class LuaSharedState(val handlesThread: LuaThread, private val cleanable: Cleana
 		)
 
 		handlesThread.push {
-			//it.lua.push(it.nextObject<Throwable>().stackTraceToString())
+			//it.lua.push(it.nextObject<Throwable?>(-1)?.stackTraceToString() ?: it.nextObject<Any?>(-1).toString())
 			it.lua.push(it.nextObject<Any?>().toString())
 			1
 		}
@@ -79,7 +79,7 @@ class LuaSharedState(val handlesThread: LuaThread, private val cleanable: Cleana
 
 				if (obj is Throwable && obj !is LuaRuntimeException) {
 					it.lua.traceback(obj.toString(), 1)
-					val err = LuaRuntimeException(it.lua.getString(), cause = obj)
+					val err = LuaRuntimeException(it.lua.getString(), cause = obj, writeStackTrace = false)
 					it.lua.push(err)
 				}
 			}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt
index 7d4bc3f9..9e17ee37 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt
@@ -90,6 +90,10 @@ class LuaThread private constructor(
 		this.storeGlobal("math")
 		LuaJNR.INSTANCE.luaopen_utf8(this.pointer)
 		this.storeGlobal("utf8")
+		LuaJNR.INSTANCE.luaopen_debug(this.pointer)
+		this.storeGlobal("debug")
+		LuaJNR.INSTANCE.luaopen_os(this.pointer)
+		this.storeGlobal("os")
 
 		sharedState.initializeHandles(this)
 
@@ -819,8 +823,8 @@ class LuaThread private constructor(
 
 		try {
 			while (LuaJNR.INSTANCE.lua_next(this.pointer, abs) != 0) {
-				keyVisitor(this, abs + 1)
-				valueVisitor(this, abs + 2)
+				keyVisitor(this, top)
+				valueVisitor(this, top + 1)
 				LuaJNR.INSTANCE.lua_settop(this.pointer, top)
 			}
 		} finally {
@@ -843,7 +847,7 @@ class LuaThread private constructor(
 
 		try {
 			while (LuaJNR.INSTANCE.lua_next(this.pointer, abs) != 0) {
-				values.add(keyVisitor(this, abs + 1))
+				values.add(keyVisitor(this, top))
 				LuaJNR.INSTANCE.lua_settop(this.pointer, top)
 			}
 		} finally {
@@ -866,7 +870,7 @@ class LuaThread private constructor(
 
 		try {
 			while (LuaJNR.INSTANCE.lua_next(this.pointer, abs) != 0) {
-				values.add(valueVisitor(this, abs + 2))
+				values.add(valueVisitor(this, top + 1))
 				LuaJNR.INSTANCE.lua_settop(this.pointer, top)
 			}
 		} finally {
@@ -889,7 +893,7 @@ class LuaThread private constructor(
 
 		try {
 			while (LuaJNR.INSTANCE.lua_next(this.pointer, abs) != 0) {
-				values.add(keyVisitor(this, abs + 1) to valueVisitor(this, abs + 2))
+				values.add(keyVisitor(this, top) to valueVisitor(this, top + 1))
 				LuaJNR.INSTANCE.lua_settop(this.pointer, top)
 			}
 		} finally {
@@ -1233,7 +1237,7 @@ class LuaThread private constructor(
 			check(value >= 0) { "Internal JVM error: ${function::class.qualifiedName} returned incorrect number of arguments to be popped from stack by Lua" }
 			return value
 		} catch (err: Throwable) {
-			push(err)
+			realLuaState.push(err)
 			return -1
 		}
 	}
@@ -1436,6 +1440,17 @@ class LuaThread private constructor(
 		LuaJNR.INSTANCE.lua_copy(pointer, fromIndex, toIndex)
 	}
 
+	fun swap(indexA: Int, indexB: Int) {
+		if (indexA == indexB) return
+		val absA = if (indexA < 0) indexA - 1 else indexA
+		val absB = if (indexB < 0) indexB - 1 else indexB
+		push()
+		copy(absA, -1)
+		copy(absB, absA)
+		copy(-1, absB)
+		pop()
+	}
+
 	fun dup() {
 		push()
 		copy(-2, -1)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt
index c053fcdd..c07356f1 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt
@@ -41,13 +41,13 @@ class LuaUpdateComponent(val lua: LuaThread, val name: Any) {
 			lua.callConditional {
 				val type = loadGlobal("update")
 
-				if (type != lastType) {
+				/*if (type != lastType) {
 					lastType = type
 
 					if (type != LuaType.FUNCTION) {
 						LOGGER.warn("Lua environment for $name has $type as global 'update', script update wasn't called")
 					}
-				}
+				}*/
 
 				if (type == LuaType.FUNCTION) {
 					preRun()
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt
index e050dbe8..0da722cf 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt
@@ -100,12 +100,12 @@ private fun setDropPool(self: MonsterEntity, args: LuaThread.ArgStack): Int {
 
 private fun toAbsolutePosition(self: MonsterEntity, args: LuaThread.ArgStack): Int {
 	args.lua.push(self.movement.getAbsolutePosition(args.nextVector2d()))
-	return 0
+	return 1
 }
 
 private fun mouthPosition(self: MonsterEntity, args: LuaThread.ArgStack): Int {
 	args.lua.push(self.mouthPosition)
-	return 0
+	return 1
 }
 
 // This callback is registered here rather than in
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt
index 76a8f80d..330b57c4 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt
@@ -160,91 +160,89 @@ private fun printJson(args: LuaThread.ArgStack): Int {
 }
 
 fun provideUtilityBindings(lua: LuaThread) {
-	with(lua) {
-		push {
-			LuaThread.LOGGER.info(it.nextString())
-			0
-		}
-
-		storeGlobal("__print")
-
-		push {
-			LuaThread.LOGGER.warn(it.nextString())
-			0
-		}
-
-		storeGlobal("__print_warn")
-
-		push {
-			LuaThread.LOGGER.error(it.nextString())
-			0
-		}
-
-		storeGlobal("__print_error")
-
-		push {
-			LuaThread.LOGGER.fatal(it.nextString())
-			0
-		}
-
-		storeGlobal("__print_fatal")
-
-		push {
-			val path = it.nextString()
-
-			try {
-				load(Starbound.readLuaScript(path).join(), "@$path")
-				1
-			} catch (err: Exception) {
-				LuaThread.LOGGER.error("Exception loading Lua script $path", err)
-				throw err
-			}
-		}
-
-		storeGlobal("__require")
-
-		push {
-			push(random.nextDouble())
-			1
-		}
-
-		storeGlobal("__random_double")
-
-		push {
-			push(random.nextLong(it.nextLong(), it.nextLong()))
-			1
-		}
-
-		storeGlobal("__random_long")
-
-		push {
-			random = random(it.nextLong())
-			0
-		}
-
-		storeGlobal("__random_seed")
-
-		push {
-			push(it.lua.getNamedHandle(it.nextString()))
-			1
-		}
-
-		storeGlobal("gethandle")
-
-		push {
-			val find = it.lua.findNamedHandle(it.nextString())
-
-			if (find == null) {
-				0
-			} else {
-				push(find)
-				1
-			}
-		}
-
-		storeGlobal("findhandle")
+	lua.push {
+		LuaThread.LOGGER.info(it.nextString())
+		0
 	}
 
+	lua.storeGlobal("__print")
+
+	lua.push {
+		LuaThread.LOGGER.warn(it.nextString())
+		0
+	}
+
+	lua.storeGlobal("__print_warn")
+
+	lua.push {
+		LuaThread.LOGGER.error(it.nextString())
+		0
+	}
+
+	lua.storeGlobal("__print_error")
+
+	lua.push {
+		LuaThread.LOGGER.fatal(it.nextString())
+		0
+	}
+
+	lua.storeGlobal("__print_fatal")
+
+	lua.push {
+		val path = it.nextString()
+
+		try {
+			it.lua.load(Starbound.readLuaScript(path).join(), "@$path")
+			1
+		} catch (err: Exception) {
+			LuaThread.LOGGER.error("Exception loading Lua script $path", err)
+			throw err
+		}
+	}
+
+	lua.storeGlobal("__require")
+
+	lua.push {
+		it.lua.push(it.lua.random.nextDouble())
+		1
+	}
+
+	lua.storeGlobal("__random_double")
+
+	lua.push {
+		it.lua.push(it.lua.random.nextLong(it.nextLong(), it.nextLong()))
+		1
+	}
+
+	lua.storeGlobal("__random_long")
+
+	lua.push {
+		it.lua.random = random(it.nextLong())
+		0
+	}
+
+	lua.storeGlobal("__random_seed")
+
+	lua.push {
+		it.lua.push(it.lua.getNamedHandle(it.nextString()))
+		1
+	}
+
+	lua.storeGlobal("gethandle")
+
+	lua.push {
+		val find = it.lua.findNamedHandle(it.nextString())
+
+		if (find == null) {
+			0
+		} else {
+			it.lua.push(find)
+			1
+		}
+	}
+
+	lua.storeGlobal("findhandle")
+
 	lua.pushTable()
 	lua.dup()
 	lua.storeGlobal("sb")
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt
index f564374a..330c0280 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt
@@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.lua.bindings
 
 import com.google.gson.JsonArray
 import com.google.gson.JsonElement
+import com.google.gson.JsonObject
 import ru.dbotthepony.kommons.util.KOptional
 import ru.dbotthepony.kstarbound.Starbound
 import ru.dbotthepony.kstarbound.defs.EntityType
@@ -96,7 +97,17 @@ private data class CallScriptData(
 
 private fun LuaThread.ArgStack.getScriptData(): CallScriptData? {
 	if (peek() == LuaType.STRING) {
-		return CallScriptData(nextString(), nextJson().asJsonArray, nextJson())
+		val name = nextString()
+		val nextJson = nextJson()
+		val expected = nextJson()
+
+		if (nextJson is JsonObject && nextJson.size() == 0) {
+			return CallScriptData(name, JsonArray(), expected)
+		} else if (nextJson is JsonArray) {
+			return CallScriptData(name, nextJson, expected)
+		} else {
+			throw IllegalArgumentException("Invalid script arguments to use (expected to be an array): $nextJson")
+		}
 	} else {
 		skip(3)
 		return null
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt
index 2c7cd805..c5427184 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt
@@ -29,7 +29,7 @@ private fun name(self: WorldObject, args: LuaThread.ArgStack): Int {
 	return 1
 }
 
-private fun directions(self: WorldObject, args: LuaThread.ArgStack): Int {
+private fun direction(self: WorldObject, args: LuaThread.ArgStack): Int {
 	args.lua.push(self.direction.numericalValue)
 	return 1
 }
@@ -320,7 +320,7 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaThread) {
 	lua.storeGlobal("object")
 
 	lua.pushBinding(self, "name", ::name)
-	lua.pushBinding(self, "directions", ::directions)
+	lua.pushBinding(self, "direction", ::direction)
 	lua.pushBinding(self, "position", ::position)
 	lua.pushBinding(self, "setInteractive", ::setInteractive)
 	lua.pushBinding(self, "uniqueId", ::uniqueId)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorState.kt
index 42e266c4..b4c3518f 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorState.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorState.kt
@@ -29,7 +29,7 @@ private fun replaceBehaviorTag(parameter: NodeParameterValue, treeParameters: Ma
 	if (parameter.key != null)
 		str = parameter.key
 
-	// original engine does this, and i don't know why this make any sense
+	// FIXME: original engine does this, and i don't know why this make any sense
 	else if (parameter.value is JsonPrimitive && parameter.value.isString)
 		str = parameter.value.asString
 
@@ -144,7 +144,12 @@ private fun createNode(
 			functions.add(name)
 
 			val outputConfig = data.get("output") { JsonObject() }
-			val output = LinkedHashMap(Registries.behaviorNodes.getOrThrow(name).value.output)
+			val node = Registries.behaviorNodes.getOrThrow(name).value
+			val output = LinkedHashMap(node.output)
+
+			// original engine doesn't do this
+			if (node.script != null)
+				scripts.add(node.script.fullPath)
 
 			for ((k, v) in output.entries) {
 				val replaced = replaceOutputBehaviorTag(outputConfig[k]?.asString ?: v.key, treeParameters)
@@ -263,14 +268,12 @@ private fun createBehaviorTree(args: LuaThread.ArgStack): Int {
 	}
 
 	handles.add(blackboard)
-
 	args.lua.ensureExtraCapacity(40)
 
+	mergedParams.scripts.forEach { scripts.add(it.fullPath) }
 	val root = createNode(args.lua, mergedParams.root, mergedParams.mappedParameters, blackboard, scripts, functions, handles)
 	handles.add(root)
 
-	args.lua.loadGlobal("require")
-
 	scripts.forEach {
 		args.lua.call {
 			loadGlobal("require")
@@ -289,9 +292,11 @@ private fun createBehaviorTree(args: LuaThread.ArgStack): Int {
 		handle.push(this)
 		push("bake")
 		check(loadTableValue() == LuaType.FUNCTION) { "BehaviorTree.bake is not a Lua function" }
-		handle.push(this)
+		swap(-2, -1)
 	}
 
+	args.lua.push(handle)
+
 	handle.close()
 	handles.forEach { it.close() }
 	return 1
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt
index 1ebad4d8..63a2b7c0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt
@@ -51,6 +51,7 @@ import ru.dbotthepony.kstarbound.world.api.MutableTileState
 import ru.dbotthepony.kstarbound.world.api.TileColor
 import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
 import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
+import ru.dbotthepony.kstarbound.world.entities.NPCEntity
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.CopyOnWriteArrayList
 import java.util.concurrent.TimeUnit
@@ -232,7 +233,8 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
 
 				for (obj in world.storage.loadEntities(pos).await()) {
 					try {
-						obj.joinWorld(world)
+						if (obj !is NPCEntity)
+							obj.joinWorld(world)
 					} catch (err: Exception) {
 						LOGGER.error("Exception while spawning entity $obj in world", err)
 					}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt
index 48a31335..4d372f89 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt
@@ -122,18 +122,16 @@ abstract class SystemWorld(val location: Vector3i, val clock: JVMClock, val univ
 	}
 
 	fun compatCoordinateSeed(coordinate: UniversePos, seedMix: String): Long {
-		// original code is utterly broken here
-
-		// consider the following:
-		// auto satellite = coordinate.isSatelliteBody() ? coordinate.orbitNumber() : 0;
-		// auto planet = coordinate.isSatelliteBody() ? coordinate.parent().orbitNumber() : coordinate.isPlanetaryBody() && coordinate.orbitNumber() || 0;
-
-		// first obvious problem: coordinate.isPlanetaryBody() && coordinate.orbitNumber() || 0
-		// this "coalesces" planet orbit into either 0 or 1
-		// then, we have coordinate.parent().orbitNumber(), which is correct, but only if we are orbiting a satellite
+		// FIXME: original code is utterly broken here
+		//  consider the following:
+		//  auto satellite = coordinate.isSatelliteBody() ? coordinate.orbitNumber() : 0;
+		//  auto planet = coordinate.isSatelliteBody() ? coordinate.parent().orbitNumber() : coordinate.isPlanetaryBody() && coordinate.orbitNumber() || 0;
+		//  first obvious problem: coordinate.isPlanetaryBody() && coordinate.orbitNumber() || 0
+		//  this "coalesces" planet orbit into either 0 or 1
+		//  then, we have coordinate.parent().orbitNumber(), which is correct, but only if we are orbiting a satellite
 
 		// TODO: Use correct logic when there are no legacy clients in this system
-		// Correct logic properly randomizes starting planet orbits, and they feel much more natural
+		//  Correct logic properly randomizes starting planet orbits, and they feel much more natural
 
 		return staticRandom64(coordinate.location.x, coordinate.location.y, coordinate.location.z, if (coordinate.isPlanet) 1 else coordinate.planetOrbit, coordinate.satelliteOrbit, seedMix)
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt
index 5c0af90a..e1c555f9 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt
@@ -406,10 +406,10 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE
 				}
 			} else {
 				try {
-					/*luaUpdate.update(delta) {
+					luaUpdate.update(delta) {
 						luaMovement.clearControlsIfNeeded()
 						forceRegions.clear()
-					}*/
+					}
 				} catch (err: Exception) {
 					LOGGER.error("Exception while ticking $this", err)
 				}
diff --git a/src/main/resources/scripts/behavior.lua b/src/main/resources/scripts/behavior.lua
index 58af354d..4e9b4b60 100644
--- a/src/main/resources/scripts/behavior.lua
+++ b/src/main/resources/scripts/behavior.lua
@@ -61,7 +61,7 @@ local function blackboardSet(self, t, key, value)
 		local mappings = self.vectorNumberInput[key]
 
 		if mappings then
-			for _, pair in pairs(input) do
+			for _, pair in pairs(mappings) do
 				local index = pair[1]
 				local tab = pair[2]
 				tab[index] = value
@@ -107,12 +107,12 @@ function blackboardPrototype:parameters(parameters, nodeID)
 
 			if not typeInput then
 				typeInput = {}
-				self.input[i][pKey] = typeInput
+				self.input[t][pKey] = typeInput
 			end
 
 			table.insert(typeInput, {parameterName, tab})
 			tab[parameterName] = self.board[t][pKey]
-		elseif pValue then
+		elseif pValue ~= nil then
 			if t == 4 then -- vec2
 				-- dumb special case for allowing a vec2 of blackboard number keys
 				if type(pValue) ~= 'table' then
@@ -132,7 +132,7 @@ function blackboardPrototype:parameters(parameters, nodeID)
 						end
 
 						table.insert(typeInput, {i, vector})
-						vector[i] = self.board[5][key] -- number
+						vector[i] = self.board[5][vValue] -- number
 					else
 						vector[i] = vValue
 					end
@@ -142,8 +142,6 @@ function blackboardPrototype:parameters(parameters, nodeID)
 			else
 				tab[parameterName] = pValue
 			end
-		else
-			error(string.format('parameter %s of type %s for node %s has no key nor value', parameterName, parameter.type, nodeID))
 		end
 	end
 
@@ -183,7 +181,7 @@ function blackboardPrototype:clearEphemerals(ephemerals)
 	end
 end
 
-local function Blackboard()
+function Blackboard()
 	return setmetatable({}, blackboardPrototype):ctor()
 end
 
@@ -204,6 +202,19 @@ local function runAndReset(self, ...)
 	return status
 end
 
+local function reconstructTree(stack)
+	local top = #stack
+	if top == 0 then return '' end
+
+	local result = {'\nbehavior tree traceback:'}
+
+	for i = top, 1, -1 do
+		table.insert(result, string.format('%s%d. - %q', string.rep(' ', top - i + 1), top - i + 1, stack[i]))
+	end
+
+	return table.concat(result, '\n')
+end
+
 -- ActionNode
 
 local actionNode = {}
@@ -221,10 +232,10 @@ function actionNode:ctor(name, parameters, outputs)
 end
 
 function actionNode:bake()
-	self.callable = _G[self.name]
+	self.callable = _ENV[self.name]
 
-	if type(callable) ~= 'function' then
-		error('expected global ' .. self.name .. ' to be a function, but got ' .. type(callable))
+	if type(self.callable) ~= 'function' then
+		error('expected global ' .. self.name .. ' to be a function, but got ' .. type(self.callable))
 	end
 end
 
@@ -233,7 +244,8 @@ do
 	local resume = coroutine.resume
 	local status = coroutine.status
 
-	function actionNode:run(delta, blackboard)
+	function actionNode:run(delta, blackboard, stack)
+		--table.insert(stack, self.name)
 		self.calls = self.calls + 1
 		local status, nodeStatus, nodeExtra
 
@@ -246,11 +258,13 @@ do
 		end
 
 		if not status then
-			sb.logError('Behavior ActionNode %q failed: %s', self.name, nodeStatus)
+			sb.logError(debug.traceback(self.coroutine, string.format('Behavior ActionNode %q failed: %s%s', self.name, nodeStatus, reconstructTree(stack))))
+			--table.remove(stack)
 			return FAILURE
 		end
 
-		if result == nil then
+		if nodeStatus == nil then
+			--table.remove(stack)
 			return RUNNING
 		end
 
@@ -258,6 +272,8 @@ do
 			blackboard:setOutput(self, nodeExtra)
 		end
 
+		--table.remove(stack)
+
 		if nodeStatus then
 			return SUCCESS
 		else
@@ -297,10 +313,10 @@ function decoratorNode:ctor(name, parameters, child)
 end
 
 function decoratorNode:bake()
-	self.callable = _G[self.name]
+	self.callable = _ENV[self.name]
 
-	if type(callable) ~= 'function' then
-		error('expected global ' .. self.name .. ' to be a function, but got ' .. type(callable))
+	if type(self.callable) ~= 'function' then
+		error('expected global ' .. self.name .. ' to be a function, but got ' .. type(self.callable))
 	end
 
 	self.child:bake()
@@ -311,7 +327,8 @@ do
 	local resume = coroutine.resume
 	local coroutine_status = coroutine.status
 
-	function decoratorNode:run(delta, blackboard)
+	function decoratorNode:run(delta, blackboard, stack)
+		--table.insert(stack, self.name)
 		self.calls = self.calls + 1
 
 		if not self.coroutine then
@@ -320,7 +337,8 @@ do
 			local status, nodeStatus = resume(coroutine, parameters, blackboard, self.nodeID, delta)
 
 			if not status then
-				sb.logError('Behavior DecoratorNode %q failed: %s', self.name, nodeStatus)
+				sb.logError(debug.traceback(coroutine, string.format('Behavior DecoratorNode %q failed: %s%s', self.name, nodeStatus, reconstructTree(stack))))
+				--table.remove(stack)
 				return FAILURE
 			end
 
@@ -329,38 +347,44 @@ do
 
 				if s == 'dead' then
 					-- quite unexpected, but whatever
+					--table.remove(stack)
 					return SUCCESS
 				else
 					self.coroutine = coroutine
 				end
 			elseif nodeStatus then
+				--table.remove(stack)
 				return SUCCESS
 			else
+				--table.remove(stack)
 				return FAILURE
 			end
 		end
 
 		while true do
-			local childStatus = runAndReset(self.child, delta, blackboard)
+			local childStatus = runAndReset(self.child, delta, blackboard, stack)
 
 			if childStatus == RUNNING then
+				table.remove(stack)
 				return RUNNING
 			end
 
 			local status, nodeStatus = resume(self.coroutine, childStatus)
 
 			if not status then
-				sb.logError('Behavior DecoratorNode %q failed: %s', self.name, nodeStatus)
+				sb.logError(debug.traceback(coroutine, string.format('Behavior DecoratorNode %q failed: %s%s', self.name, nodeStatus, reconstructTree(stack))))
+				--table.remove(stack)
 				return FAILURE
 			end
 
 			if nodeStatus == nil then
 				-- another yield OR unexpected return?
 
-				local s = coroutine_status(coroutine)
+				local s = coroutine_status(self.coroutine)
 
 				if s == 'dead' then
 					self.coroutine = nil
+					--table.remove(stack)
 					return SUCCESS
 				end
 			else
@@ -368,8 +392,10 @@ do
 				self.coroutine = nil
 
 				if nodeStatus then
+					--table.remove(stack)
 					return SUCCESS
 				else
+					--table.remove(stack)
 					return FAILURE
 				end
 			end
@@ -407,20 +433,29 @@ function seqNode:ctor(children, isSelector)
 	return self
 end
 
-function seqNode:run(delta, blackboard)
+function seqNode:run(delta, blackboard, stack)
 	self.calls = self.calls + 1
 	local size = self.size
 	local isSelector = self.isSelector
 
+	--[[if isSelector then
+		table.insert(stack, 'SelectorNode')
+	else
+		table.insert(stack, 'SequenceNode')
+	end]]
+
 	while self.index <= size do
 		local child = self.children[self.index]
-		local status = runAndReset(child, delta, blackboard)
+		local status = runAndReset(child, delta, blackboard, stack)
 
 		if status == RUNNING then
+			--table.remove(stack)
 			return RUNNING
 		elseif isSelector and status == SUCCESS then
+			--table.remove(stack)
 			return SUCCESS
 		elseif not isSelector and status == FAILURE then
+			--table.remove(stack)
 			return FAILURE
 		end
 
@@ -464,13 +499,13 @@ parallelNode.__index = parallelNode
 function parallelNode:ctor(parameters, children)
 	self.children = children
 
-	if type(parameters.success) == 'number' then
+	if type(parameters.success) == 'number' and parameters.success >= 0 then
 		self.successLimit = parameters.success
 	else
 		self.successLimit = #children
 	end
 
-	if type(parameters.fail) == 'number' then
+	if type(parameters.fail) == 'number' and parameters.fail >= 0 then
 		self.failLimit = parameters.fail
 	else
 		self.failLimit = #children
@@ -483,15 +518,17 @@ function parallelNode:ctor(parameters, children)
 	return self
 end
 
-function parallelNode:run(delta, blackboard)
+function parallelNode:run(delta, blackboard, stack)
 	self.calls = self.calls + 1
 	local failed = 0
 	local succeeded = 0
 	local failLimit = self.failLimit
 	local successLimit = self.successLimit
 
+	--table.insert(stack, 'ParallelNode')
+
 	for _, node in ipairs(self.children) do
-		local status = runAndReset(node, delta, blackboard)
+		local status = runAndReset(node, delta, blackboard, stack)
 
 		if status == SUCCESS then
 			succeeded = succeeded + 1
@@ -502,16 +539,19 @@ function parallelNode:run(delta, blackboard)
 		if failed >= failLimit then
 			self.lastFailed = failed
 			self.lastSucceed = succeeded
+			--table.remove(stack)
 			return FAILURE
 		elseif succeeded >= successLimit then
 			self.lastFailed = failed
 			self.lastSucceed = succeeded
+			--table.remove(stack)
 			return SUCCESS
 		end
 	end
 
 	self.lastFailed = failed
 	self.lastSucceed = succeeded
+	--table.remove(stack)
 	return RUNNING
 end
 
@@ -550,11 +590,12 @@ function dynNode:ctor(children)
 	return self
 end
 
-function dynNode:run(delta, blackboard)
+function dynNode:run(delta, blackboard, stack)
 	self.calls = self.calls + 1
+	--table.insert(stack, 'DynamicNode')
 
 	for i, node in ipairs(self.children) do
-		local status = runAndReset(node, delta, blackboard)
+		local status = runAndReset(node, delta, blackboard, stack)
 
 		if stauts == FAILURE and self.index == i then
 			self.index = self.index + 1
@@ -564,10 +605,12 @@ function dynNode:run(delta, blackboard)
 		end
 
 		if status == SUCCESS or self.index > self.size then
+			--table.remove(stack)
 			return status
 		end
 	end
 
+	--table.remove(stack)
 	return RUNNING
 end
 
@@ -605,7 +648,7 @@ function randNode:ctor(children)
 	return self
 end
 
-function randNode:run(delta, blackboard)
+function randNode:run(delta, blackboard, stack)
 	self.calls = self.calls + 1
 
 	if self.index == -1 and self.size ~= 0 then
@@ -615,7 +658,10 @@ function randNode:run(delta, blackboard)
 	if self.index == -1 then
 		return FAILURE
 	else
-		return runAndReset(self.children[self.index], delta, blackboard)
+		--table.insert(stack, 'RandomNode')
+		local value = runAndReset(self.children[self.index], delta, blackboard, stack)
+		--table.remove(stack)
+		return value
 	end
 end
 
@@ -654,19 +700,20 @@ function statePrototype:ctor(blackboard, root)
 end
 
 function statePrototype:run(delta)
+	local stack = {}
 	local ephemerals = self._blackboard:takeEphemerals()
-	local status = runAndReset(self.root, delta, self._blackboard)
+	local status = runAndReset(self.root, delta, self._blackboard, stack)
 	self._blackboard:clearEphemerals(ephemerals)
 
 	return status
 end
 
 function statePrototype:clear()
-	self.tree:reset()
+	self.root:reset()
 end
 
 function statePrototype:bake()
-	self.tree:bake()
+	self.root:bake()
 end
 
 function statePrototype:blackboard()
diff --git a/src/main/resources/scripts/global.lua b/src/main/resources/scripts/global.lua
index a7a2be90..b410bed2 100644
--- a/src/main/resources/scripts/global.lua
+++ b/src/main/resources/scripts/global.lua
@@ -385,4 +385,72 @@ function mergeJson(base, with)
 	end
 end
 
+do
+	local line = ''
+
+	local function puts(f, ...)
+		line = line .. string.format(f, ...)
+	end
+
+	local function flush()
+		if line ~= '' then
+			sb.logInfo(line)
+			line = ''
+		end
+	end
+
+	local function printTable(input, level)
+		level = level or 0
+
+		if not next(input) then
+			puts('{ --[[ empty table ]] }')
+			if level == 0 then flush() end
+		else
+			local prefix = string.rep('    ', level + 1)
+
+			puts('{')
+			flush()
+
+			for k, v in pairs(input) do
+				if type(k) == 'string' then
+					puts('%s[%q] = ', prefix, k)
+				else
+					puts('%s[%s] = ', prefix, k)
+				end
+
+				printValue(v, level + 1)
+				puts(',')
+				flush()
+			end
+
+			puts('%s}', string.rep('    ', level))
+			if level == 0 then flush() end
+		end
+	end
+
+	function printValue(input, level)
+		level = level or 0
+
+		local t = type(input)
+
+		if t == 'nil' then
+			puts('%s', 'nil')
+			if level == 0 then flush() end
+		elseif t == 'number' then
+			puts('%f', input)
+			if level == 0 then flush() end
+		elseif t == 'string' then
+			puts('%q', tostring(input))
+			if level == 0 then flush() end
+		elseif t == 'boolean' then
+			puts('%s', tostring(input))
+			if level == 0 then flush() end
+		elseif t == 'table' then
+			printTable(input, level)
+		else
+			puts('unknown value type %q', t)
+			if level == 0 then flush() end
+		end
+	end
+end
 
diff --git a/src/main/resources/scripts/world.lua b/src/main/resources/scripts/world.lua
index ebf25d2c..0accd486 100644
--- a/src/main/resources/scripts/world.lua
+++ b/src/main/resources/scripts/world.lua
@@ -99,7 +99,7 @@ local function entityTypeNamesToIntegers(input, fullName)
 				error('invalid entity type ' .. tostring(v) .. ' for ' .. fullName .. ' in types table at index ' .. i, 3)
 			end
 
-			entityTypes[i] = lookup
+			input[i] = lookup
 		end
 
 		return input
diff --git a/src/test/kotlin/ru/dbotthepony/kstarbound/test/LuaTests.kt b/src/test/kotlin/ru/dbotthepony/kstarbound/test/LuaTests.kt
index 50a71ea6..a5fea6fc 100644
--- a/src/test/kotlin/ru/dbotthepony/kstarbound/test/LuaTests.kt
+++ b/src/test/kotlin/ru/dbotthepony/kstarbound/test/LuaTests.kt
@@ -13,19 +13,22 @@ object LuaTests {
 		val lua = LuaThread()
 
 		lua.push {
-			throw IllegalArgumentException("test!")
+			throw IllegalArgumentException("This is error message")
 		}
 
 		lua.storeGlobal("test")
 
-		val results = lua.call(5) {
+		lua.call {
 			lua.load("""
-				return 1, 4, 4.0, 4.1, {a = 71}
+				local function errornous()
+					test()
+				end
+				
+				local cor = coroutine.create(errornous)
+				print(coroutine.resume(cor))
 			""".trimIndent())
 		}
 
-		println(results)
-		println(results.last().toJson())
 		lua.close()
 	}
 }