Compare commits

...

5 Commits

35 changed files with 1176 additions and 475 deletions

View File

@ -13,12 +13,7 @@ static int lua_jniFunc(lua_State *state) {
jint result = (*env)->CallIntMethod(env, *lua_JCClosure, callback, (long long) state);
if (result <= -1) {
const char* errMsg = lua_tostring(state, -1);
if (errMsg == NULL)
return luaL_error(state, "Internal JVM Error");
return luaL_error(state, "%s", errMsg);
return lua_error(state);
}
return result;

View File

@ -32,9 +32,9 @@ public interface LuaJNR {
@IgnoreError
public int lua_pcallk(@NotNull Pointer luaState, int numArgs, int numResults, int msgh, @LongLong long ctx, @LongLong long callback);
@IgnoreError
public int lua_callk(@NotNull Pointer luaState, int numArgs, int numResults, @LongLong long ctx, @LongLong long callback);
@IgnoreError
public long lua_atpanic(@NotNull Pointer luaState, @LongLong long fn);
@IgnoreError
public long luaL_traceback(@NotNull Pointer luaState, @NotNull Pointer forState, @NotNull String message, int level);
/**
* Creates a new thread, pushes it on the stack, and returns a pointer to a lua_State that represents this new thread. The new thread returned by this function shares with the original thread its global environment, but has an independent execution stack.

View File

@ -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,
)

View File

@ -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)

View File

@ -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)

View File

@ -274,7 +274,6 @@ data class ItemDescriptor(
push(parameters)
push(level)
push(seed)
5
}, { getJson() as JsonObject to getJson() as JsonObject }).get()
} finally {
lua.close()

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.json
import com.github.luben.zstd.ZstdDictDecompress
import com.github.luben.zstd.ZstdInputStreamNoFinalizer
import com.google.gson.JsonArray
import com.google.gson.JsonElement
@ -36,7 +37,7 @@ private enum class InflateType {
NONE
}
private fun <T> ByteArray.callRead(inflate: InflateType, callable: DataInputStream.() -> T): T {
private fun <T> ByteArray.callRead(inflate: InflateType, dictionary: ZstdDictDecompress? = null, callable: DataInputStream.() -> T): T {
val stream = FastByteArrayInputStream(this)
when (inflate) {
@ -53,12 +54,17 @@ private fun <T> ByteArray.callRead(inflate: InflateType, callable: DataInputStre
}
InflateType.ZSTD -> {
val data = DataInputStream(BufferedInputStream(ZstdInputStreamNoFinalizer(stream), 0x10000))
val f = ZstdInputStreamNoFinalizer(stream)
if (dictionary != null)
f.setDict(dictionary)
val data = DataInputStream(BufferedInputStream(f, 0x10000))
try {
return callable(data)
} finally {
data.close()
f.close()
}
}
@ -76,9 +82,9 @@ fun ByteArray.readJsonElementInflated(): JsonElement = callRead(InflateType.ZLIB
fun ByteArray.readJsonObjectInflated(): JsonObject = callRead(InflateType.ZLIB) { readJsonObject() }
fun ByteArray.readJsonArrayInflated(): JsonArray = callRead(InflateType.ZLIB) { readJsonArray() }
fun ByteArray.readJsonElementZstd(): JsonElement = callRead(InflateType.ZSTD) { readJsonElement() }
fun ByteArray.readJsonObjectZstd(): JsonObject = callRead(InflateType.ZSTD) { readJsonObject() }
fun ByteArray.readJsonArrayZstd(): JsonArray = callRead(InflateType.ZSTD) { readJsonArray() }
fun ByteArray.readJsonElementZstd(dictionary: ZstdDictDecompress? = null): JsonElement = callRead(InflateType.ZSTD, dictionary = dictionary) { readJsonElement() }
fun ByteArray.readJsonObjectZstd(dictionary: ZstdDictDecompress? = null): JsonObject = callRead(InflateType.ZSTD, dictionary = dictionary) { readJsonObject() }
fun ByteArray.readJsonArrayZstd(dictionary: ZstdDictDecompress? = null): JsonArray = callRead(InflateType.ZSTD, dictionary = dictionary) { readJsonArray() }
/**
* Позволяет читать двоичный JSON прямиком в [JsonElement]

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.json
import com.github.luben.zstd.ZstdDictCompress
import com.github.luben.zstd.ZstdOutputStreamNoFinalizer
import com.google.gson.JsonArray
import com.google.gson.JsonElement
@ -21,7 +22,7 @@ private enum class DeflateType {
NONE
}
private fun <T> T.callWrite(deflate: DeflateType, zstdCompressionLevel: Int = 6, callable: DataOutputStream.(T) -> Unit): ByteArray {
private fun <T> T.callWrite(deflate: DeflateType, zstdCompressionLevel: Int = 6, zstdDictionary: ZstdDictCompress? = null, callable: DataOutputStream.(T) -> Unit): ByteArray {
val stream = FastByteArrayOutputStream()
when (deflate) {
@ -38,6 +39,9 @@ private fun <T> T.callWrite(deflate: DeflateType, zstdCompressionLevel: Int = 6,
val s = ZstdOutputStreamNoFinalizer(stream)
s.setLevel(zstdCompressionLevel)
if (zstdDictionary != null)
s.setDict(zstdDictionary)
DataOutputStream(BufferedOutputStream(s, 0x10000)).use {
callable(it, this)
}
@ -57,9 +61,9 @@ fun JsonElement.writeJsonElementDeflated(): ByteArray = callWrite(DeflateType.ZL
fun JsonObject.writeJsonObjectDeflated(): ByteArray = callWrite(DeflateType.ZLIB) { writeJsonObject(it) }
fun JsonArray.writeJsonArrayDeflated(): ByteArray = callWrite(DeflateType.ZLIB) { writeJsonArray(it) }
fun JsonElement.writeJsonElementZstd(level: Int = 6): ByteArray = callWrite(DeflateType.ZSTD, zstdCompressionLevel = level) { writeJsonElement(it) }
fun JsonObject.writeJsonObjectZstd(level: Int = 6): ByteArray = callWrite(DeflateType.ZSTD, zstdCompressionLevel = level) { writeJsonObject(it) }
fun JsonArray.writeJsonArrayZstd(level: Int = 6): ByteArray = callWrite(DeflateType.ZSTD, zstdCompressionLevel = level) { writeJsonArray(it) }
fun JsonElement.writeJsonElementZstd(level: Int = 6, dictionary: ZstdDictCompress? = null): ByteArray = callWrite(DeflateType.ZSTD, zstdCompressionLevel = level, zstdDictionary = dictionary) { writeJsonElement(it) }
fun JsonObject.writeJsonObjectZstd(level: Int = 6, dictionary: ZstdDictCompress? = null): ByteArray = callWrite(DeflateType.ZSTD, zstdCompressionLevel = level, zstdDictionary = dictionary) { writeJsonObject(it) }
fun JsonArray.writeJsonArrayZstd(level: Int = 6, dictionary: ZstdDictCompress? = null): ByteArray = callWrite(DeflateType.ZSTD, zstdCompressionLevel = level, zstdDictionary = dictionary) { writeJsonArray(it) }
fun DataOutputStream.writeJsonElement(value: JsonElement) {
when (value) {

View File

@ -1,6 +1,10 @@
package ru.dbotthepony.kstarbound.lua
data class CommonHandleRegistry(
val future: LuaHandle,
val pathFinder: LuaHandle,
)
val future: LuaHandle = LuaHandle.Nil,
val pathFinder: LuaHandle = LuaHandle.Nil,
) {
companion object {
val EMPTY = CommonHandleRegistry()
}
}

View File

@ -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
@ -176,10 +177,10 @@ fun LuaThread.getVector2iOrAABB(stackIndex: Int = -1): Either<Vector2i, AABB>? {
fun LuaThread.ArgStack.nextVector2iOrAABB(position: Int = this.position++): Either<Vector2i, AABB> {
if (position !in 1 ..this.top)
throw IllegalArgumentException("bad argument #$position: Vector2d expected, got nil")
throw IllegalArgumentException("bad argument #$position: Vector2i expected, got nil")
return lua.getVector2iOrAABB(position)
?: throw IllegalArgumentException("bad argument #$position: Vector2d or AABB expected, got ${lua.typeAt(position)}")
?: throw IllegalArgumentException("bad argument #$position: Vector2i or AABB expected, got ${lua.typeAt(position)}")
}
fun LuaThread.ArgStack.nextOptionalVector2iOrAABB(position: Int = this.position++): Either<Vector2i, AABB>? {
@ -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 {

View File

@ -16,17 +16,6 @@ const val LUA_ERRMEM = 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) : RuntimeException(message, cause)
class LuaGCException(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)
fun throwPcallError(code: Int) {
when (code) {
LUA_OK -> {}
LUA_ERRRUN -> throw LuaException("Runtime Error")
LUA_ERRMEM -> throw LuaMemoryAllocException()
LUA_ERRERR -> throw LuaException("Exception inside Exception handler")
else -> throw LuaException("Unknown Lua Loading error: $code")
}
}
class LuaRuntimeException(message: String? = null, cause: Throwable? = null, writeStackTrace: Boolean = true) : RuntimeException(message, cause, true, writeStackTrace)

View File

@ -1,36 +1,169 @@
package ru.dbotthepony.kstarbound.lua
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonPrimitive
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import java.io.Closeable
import java.lang.ref.Cleaner.Cleanable
import java.lang.ref.WeakReference
class LuaHandle(private val parent: LuaSharedState, val handle: Int, val key: Any?) : Closeable {
private val cleanable: Cleanable
interface LuaHandle : Closeable {
fun push(into: LuaThread)
override fun close() {}
fun toJson(): JsonElement
val type: LuaType
var isValid = true
private set
init {
val parent = parent
val handle = handle
val key = key
cleanable = Starbound.CLEANER.register(this) {
parent.freeHandle(handle, key)
object Nil : LuaHandle {
override fun push(into: LuaThread) {
into.push()
}
override fun toString(): String {
return "LuaHandle.Nil"
}
override fun toJson(): JsonElement {
return JsonNull.INSTANCE
}
override val type: LuaType
get() = LuaType.NIL
}
fun push(into: LuaThread) {
check(isValid) { "Tried to use NULL handle!" }
parent.handlesThread.push()
parent.handlesThread.copy(handle, -1)
parent.handlesThread.moveStackValuesOnto(into)
object True : LuaHandle {
override fun push(into: LuaThread) {
into.push(true)
}
override fun toString(): String {
return "LuaHandle.True"
}
override fun toJson(): JsonElement {
return InternedJsonElementAdapter.TRUE
}
override val type: LuaType
get() = LuaType.BOOLEAN
}
override fun close() {
if (!isValid) return
cleanable.clean()
isValid = false
object False : LuaHandle {
override fun push(into: LuaThread) {
into.push(false)
}
override fun toString(): String {
return "LuaHandle.False"
}
override fun toJson(): JsonElement {
return InternedJsonElementAdapter.FALSE
}
override val type: LuaType
get() = LuaType.BOOLEAN
}
class LLong(val value: Long) : LuaHandle {
override fun push(into: LuaThread) {
into.push(value)
}
override fun equals(other: Any?): Boolean {
return other is LLong && other.value == value
}
override fun hashCode(): Int {
return value.hashCode()
}
override fun toString(): String {
return "LuaHandle.LLong[$value]"
}
override fun toJson(): JsonElement {
return JsonPrimitive(value)
}
override val type: LuaType
get() = LuaType.NUMBER
}
class LDouble(val value: Double) : LuaHandle {
override fun push(into: LuaThread) {
into.push(value)
}
override fun equals(other: Any?): Boolean {
return other is LDouble && other.value == value
}
override fun hashCode(): Int {
return value.hashCode()
}
override fun toString(): String {
return "LuaHandle.LDouble[$value]"
}
override fun toJson(): JsonElement {
return JsonPrimitive(value)
}
override val type: LuaType
get() = LuaType.NUMBER
}
class Regular(private val parent: LuaSharedState, val handle: Int, val key: Any?) : LuaHandle {
private val cleanable: Cleanable
var isValid = true
private set
override val type: LuaType by lazy(LazyThreadSafetyMode.NONE) {
check(isValid) { "Tried to use NULL handle!" }
parent.handlesThread.typeAt(handle)
}
init {
val parent = WeakReference(parent)
val handle = handle
val key = key
cleanable = Starbound.CLEANER.register(this) {
parent.get()?.freeHandle(handle, key)
}
}
override fun push(into: LuaThread) {
check(isValid) { "Tried to use NULL handle!" }
parent.handlesThread.push()
parent.handlesThread.copy(handle, -1)
parent.handlesThread.moveStackValuesOnto(into)
}
override fun close() {
if (!isValid) return
cleanable.clean()
isValid = false
}
override fun toJson(): JsonElement {
check(isValid) { "Tried to use NULL handle!" }
return parent.handlesThread.getJson(handle)!!
}
override fun toString(): String {
return "LuaHandle[$parent - $handle / $key]"
}
}
companion object {
fun boolean(of: Boolean): LuaHandle {
if (of) return True else return False
}
}
}

View File

@ -38,19 +38,16 @@ class LuaMessageHandlerComponent(val lua: LuaThread, val nameProvider: () -> Str
return handlers[name]
}
inline fun handle(message: String, isLocal: Boolean, arguments: LuaThread.() -> Int): JsonElement? {
fun handle(message: String, isLocal: Boolean, arguments: LuaThread.() -> Int): JsonElement? {
val handler = lookupHandler(message) ?: return null
val top = lua.stackTop
try {
lua.push(handler)
lua.push(isLocal)
val amountOfArguments = arguments(lua)
check(amountOfArguments >= 0) { "Invalid amount of arguments to pass to Lua handler: $amountOfArguments" }
lua.call(amountOfArguments + 1, 1)
return lua.getJson()
return lua.call(1, {
push(handler)
push(isLocal)
arguments(lua)
}, { getJson(it) })
} catch (err: Throwable) {
if (logPacer.consumeAndReturnDeadline() <= 0L)
LOGGER.error("${nameProvider.invoke()}: Exception while handling message '$message'", err)

View File

@ -30,11 +30,24 @@ class LuaSharedState(val handlesThread: LuaThread, private val cleanable: Cleana
check(isValid) { "Tried to use NULL LuaState!" }
}
var errorToStringFunction by Delegates.notNull<LuaHandle>()
private set
var errorTrapFunction by Delegates.notNull<LuaHandle>()
private set
override fun toString(): String {
return "LuaSharedState[$handlesThread]"
}
override fun close() {
if (!isValid) return
isValid = false
namedHandles.clear()
cleanable.clean()
errorToStringFunction = LuaHandle.Nil
errorTrapFunction = LuaHandle.Nil
commonHandles = CommonHandleRegistry.EMPTY
}
fun initializeHandles(mainThread: LuaThread) {
@ -45,6 +58,36 @@ class LuaSharedState(val handlesThread: LuaThread, private val cleanable: Cleana
future = future,
pathFinder = pathFinder,
)
handlesThread.push {
//it.lua.push(it.nextObject<Throwable?>(-1)?.stackTraceToString() ?: it.nextObject<Any?>(-1).toString())
it.lua.push(it.nextObject<Any?>().toString())
1
}
errorToStringFunction = allocateHandle(null)
handlesThread.push {
val peek = it.peek()
if (peek == LuaType.STRING) {
it.lua.traceback(it.lua.getString()!!, 1)
val err = LuaRuntimeException(it.lua.getString())
it.lua.push(err)
} else if (peek == LuaType.USERDATA) {
val obj = it.nextObject<Any>()
if (obj is Throwable && obj !is LuaRuntimeException) {
it.lua.traceback(obj.toString(), 1)
val err = LuaRuntimeException(it.lua.getString(), cause = obj, writeStackTrace = false)
it.lua.push(err)
}
}
1
}
errorTrapFunction = allocateHandle(null)
}
fun freeHandle(handle: Int, key: Any?) {
@ -79,10 +122,10 @@ class LuaSharedState(val handlesThread: LuaThread, private val cleanable: Cleana
if (freeHandles.isEmpty()) {
if (nextHandle % 10 == 0) {
handlesThread.ensureExtraCapacity(10)
handlesThread.ensureExtraCapacity(20)
}
return LuaHandle(this, ++nextHandle, name).also {
return LuaHandle.Regular(this, ++nextHandle, name).also {
if (name != null) namedHandles[name] = it
}
} else {
@ -92,7 +135,7 @@ class LuaSharedState(val handlesThread: LuaThread, private val cleanable: Cleana
handlesThread.copy(-1, handle)
handlesThread.pop()
return LuaHandle(this, handle, name).also {
return LuaHandle.Regular(this, handle, name).also {
if (name != null) namedHandles[name] = it
}
}

View File

@ -90,14 +90,19 @@ 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)
provideUtilityBindings(this)
provideRootBindings(this)
load(globalScript, "@/internal/global.lua")
call()
call {
load(globalScript, "@/internal/global.lua")
}
}
fun interface Fn {
@ -204,115 +209,219 @@ class LuaThread private constructor(
closure.dispose()
}
fun call(numArgs: Int = 0, numResults: Int = 0): Int {
sharedState.cleanup()
val status = LuaJNR.INSTANCE.lua_pcallk(this.pointer, numArgs, numResults, 0, 0L, 0L)
private fun pcall(numArgs: Int, numResults: Int, handler: Int) {
val status = LuaJNR.INSTANCE.lua_pcallk(this.pointer, numArgs, numResults, handler, 0L, 0L)
if (status == LUA_ERRRUN) {
throw LuaRuntimeException(this.getString())
}
val errObject = typeAt(-1)
return status
if (errObject == LuaType.STRING)
throw LuaRuntimeException(this.getString())
else if (errObject == LuaType.USERDATA)
throw getObject() as Throwable
else
throw RuntimeException("Lua raised an error, but it has invalid value as error: $errObject")
} else if (status == LUA_ERRMEM) {
throw LuaMemoryAllocException()
} else if (status == LUA_ERRERR) {
throw LuaException("Exception inside Exception handler")
}
}
/**
* Returns boolean indicating whenever function exists
*/
inline fun invokeGlobal(name: String, arguments: LuaThread.() -> Int): Boolean {
fun callConditional(block: LuaThread.() -> Boolean) {
sharedState.cleanup()
val top = stackTop
push(sharedState.errorTrapFunction)
try {
val type = loadGlobal(name)
if (type != LuaType.FUNCTION)
return false
if (block(this)) {
val newTop = stackTop
val numArguments = arguments(this)
check(numArguments >= 0) { "Invalid amount of arguments provided to Lua function" }
call(numArguments)
return true
if (newTop == top + 1) {
throw IllegalArgumentException("No function was pushed to stack")
}
pcall(newTop - top - 2, 0, top + 1)
}
} finally {
setTop(top)
LuaJNR.INSTANCE.lua_settop(pointer, top)
}
}
/**
* Returns boolean indicating whenever function exists
*/
fun invokeGlobal(name: String, numArguments: Int): Boolean {
fun call(block: LuaThread.() -> Unit) {
return callConditional { block(this); true }
}
fun <T> callConditional(numResults: Int, block: LuaThread.() -> Boolean, resultHandler: LuaThread.(firstStackIndex: Int) -> T): KOptional<T> {
require(numResults >= 0) { "Invalid amount of results to get from Lua call: $numResults" }
sharedState.cleanup()
val top = stackTop
push(sharedState.errorTrapFunction)
try {
val type = loadGlobal(name)
if (block(this)) {
val newTop = stackTop
if (type != LuaType.FUNCTION) {
pop(numArguments)
return false
if (newTop == top + 1) {
throw IllegalArgumentException("No function was pushed to stack")
}
pcall(newTop - top - 2, numResults, top + 1)
return KOptional(resultHandler(this, top + 2))
} else {
return KOptional()
}
} finally {
LuaJNR.INSTANCE.lua_settop(pointer, top)
}
}
fun <T> call(numResults: Int, block: LuaThread.() -> Unit, resultHandler: LuaThread.(firstStackIndex: Int) -> T): T {
return callConditional(numResults, { block(this); true }, resultHandler).value
}
fun callConditional(numResults: Int, block: LuaThread.() -> Boolean): List<LuaHandle>? {
require(numResults >= 0) { "Invalid amount of results to get from Lua call: $numResults" }
sharedState.cleanup()
val top = stackTop
push(sharedState.errorTrapFunction)
try {
if (!block(this))
return null
val newTop = stackTop
if (newTop == top + 1) {
throw IllegalArgumentException("No function was pushed to stack")
}
call(numArguments)
return true
pcall(newTop - top - 2, numResults, top + 1)
val handles = ArrayList<LuaHandle>()
for (i in 1 .. numResults) {
val stackIndex = top + 1 + i
when (typeAt(stackIndex)) {
LuaType.NONE, LuaType.NIL -> handles.add(LuaHandle.Nil)
LuaType.BOOLEAN -> handles.add(LuaHandle.boolean(getBooleanRaw(stackIndex)))
LuaType.NUMBER -> {
if (isInteger(stackIndex)) {
handles.add(LuaHandle.LLong(getLongRaw(stackIndex)))
} else {
handles.add(LuaHandle.LDouble(getDoubleRaw(stackIndex)))
}
}
else -> {
if (i != numResults) {
push()
copy(stackIndex, -1)
}
moveStackValuesOnto(sharedState.handlesThread)
handles.add(sharedState.allocateHandle(null))
}
}
}
return handles
} finally {
setTop(top)
LuaJNR.INSTANCE.lua_settop(pointer, top)
}
}
/**
* Returns empty [KOptional] if function does not exist
*/
inline fun <T> invokeGlobal(name: String, numResults: Int, arguments: LuaThread.() -> Int, results: LuaThread.(firstValue: Int) -> T): KOptional<T> {
require(numResults > 0) { "Invalid amount of results: $numResults" }
val top = stackTop
fun call(numResults: Int, block: LuaThread.() -> Unit): List<LuaHandle> {
return callConditional(numResults, { block(this); true })!!
}
try {
fun traceback(message: String, level: Int = 0) {
LuaJNR.INSTANCE.luaL_traceback(pointer, pointer, message, level)
}
fun invokeGlobal(name: String): Boolean {
var exists = false
callConditional {
val type = loadGlobal(name)
if (type != LuaType.FUNCTION)
return KOptional()
return@callConditional false
val numArguments = arguments(this)
call(numArguments, numResults)
return KOptional(results(this, top + 1))
} finally {
setTop(top)
exists = true
return@callConditional true
}
return exists
}
fun invokeGlobal(name: String, arguments: LuaThread.() -> Unit): Boolean {
var exists = false
callConditional {
val type = loadGlobal(name)
if (type != LuaType.FUNCTION)
return@callConditional false
arguments(this)
exists = true
return@callConditional true
}
return exists
}
fun invokeGlobal(name: String, numResults: Int, arguments: LuaThread.() -> Unit): List<LuaHandle>? {
return callConditional(numResults) {
val type = loadGlobal(name)
if (type != LuaType.FUNCTION)
return@callConditional false
arguments(this)
return@callConditional true
}
}
inline fun <T> eval(chunk: String, name: String = "eval", numResults: Int, arguments: LuaThread.() -> Int, results: LuaThread.(firstValue: Int) -> T): T {
require(numResults > 0) { "Invalid amount of results: $numResults" }
fun <T> invokeGlobal(name: String, numResults: Int, arguments: LuaThread.() -> Unit, resultHandler: LuaThread.(firstStackIndex: Int) -> T): KOptional<T> {
return callConditional(numResults, {
val type = loadGlobal(name)
if (type != LuaType.FUNCTION)
return@callConditional false
val top = stackTop
arguments(this)
return@callConditional true
}, resultHandler)
}
try {
val numArguments = arguments(this)
fun <T> eval(chunk: String, name: String = "eval", numResults: Int, arguments: LuaThread.() -> Unit, results: LuaThread.(firstValue: Int) -> T): T {
return call(numResults, {
load(chunk, name)
call(numArguments, numResults)
return results(this, top + 1)
} finally {
setTop(top)
arguments(this)
}, results)
}
fun eval(chunk: String, name: String = "eval", numResults: Int, arguments: LuaThread.() -> Unit): List<LuaHandle> {
return call(numResults) {
load(chunk, name)
arguments(this)
}
}
inline fun eval(chunk: String, name: String = "eval", arguments: LuaThread.() -> Int) {
val top = stackTop
try {
val numArguments = arguments(this)
fun eval(chunk: String, name: String = "eval", arguments: LuaThread.() -> Unit) {
return call {
load(chunk, name)
call(numArguments, 0)
} finally {
setTop(top)
arguments(this)
}
}
fun eval(chunk: String, name: String = "eval"): JsonElement {
val top = stackTop
try {
return call(1, {
load(chunk, name)
call(numResults = 1)
return getJson() ?: JsonNull.INSTANCE
} finally {
setTop(top)
}
}, { getJson(it)!! })
}
private val attachedScripts = ArrayList<String>()
@ -333,8 +442,9 @@ class LuaThread private constructor(
try {
// minor hiccups during unpopulated script cache should be tolerable
for ((chunk, path) in loadScripts) {
load(chunk.join(), "@$path")
call()
call {
load(chunk.join(), "@$path")
}
}
} catch (err: Exception) {
LOGGER.error("Failed to attach scripts to Lua environment", err)
@ -343,16 +453,18 @@ class LuaThread private constructor(
try {
if (callInit) {
val type = loadGlobal("init")
callConditional {
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")
if (type == LuaType.NIL || type == LuaType.NONE) {
return@callConditional false
} else if (type != LuaType.FUNCTION) {
throw LuaRuntimeException("init is not a function: $type")
}
true
}
}
} catch (err: Exception) {
LOGGER.error("Failed to call init() in Lua environment", err)
@ -371,8 +483,9 @@ class LuaThread private constructor(
sharedState.ensureValid()
if (initCalled) {
// minor hiccups during unpopulated script cache should be tolerable
load(Starbound.readLuaScript(script).join(), "@$script")
call()
call {
load(Starbound.readLuaScript(script).join(), "@$script")
}
} else {
attachedScripts.add(script)
}
@ -710,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 {
@ -734,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 {
@ -757,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 {
@ -780,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 {
@ -1106,7 +1219,7 @@ class LuaThread private constructor(
}
}
private fun closure(p: Long, function: Fn, performanceCritical: Boolean): Int {
private fun closure(p: Long, function: Fn): Int {
sharedState.cleanup()
val realLuaState: LuaThread
@ -1118,69 +1231,33 @@ class LuaThread private constructor(
}
val args = realLuaState.ArgStack(realLuaState.stackTop)
val rememberStack: ArrayList<String>?
if (performanceCritical) {
rememberStack = null
} else {
rememberStack = ArrayList(Exception().stackTraceToString().split('\n'))
rememberStack.removeAt(0) // java.lang. ...
// rememberStack.removeAt(0) // at ... push( ... )
}
try {
val value = function.invoke(args)
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) {
try {
if (performanceCritical) {
realLuaState.push(err.stackTraceToString())
return -1
} else {
rememberStack!!
val newStack = err.stackTraceToString().split('\n').toMutableList()
val rememberIterator = rememberStack.listIterator(rememberStack.size)
val iterator = newStack.listIterator(newStack.size)
var hit = false
while (rememberIterator.hasPrevious() && iterator.hasPrevious()) {
val a = rememberIterator.previous()
val b = iterator.previous()
if (a == b) {
hit = true
iterator.remove()
} else {
break
}
}
if (hit) {
newStack[newStack.size - 1] = "\t<...>"
}
realLuaState.push(newStack.joinToString("\n"))
return -1
}
} catch(err2: Throwable) {
realLuaState.push("JVM suffered an exception while handling earlier exception: ${err2.stackTraceToString()}; earlier: ${err.stackTraceToString()}")
return -1
}
realLuaState.push(err)
return -1
}
}
fun push(function: Fn, performanceCritical: Boolean) {
fun push(err: Throwable) {
pushTable()
push("__tostring")
push(sharedState.errorToStringFunction)
setTableValue()
pushObject(err)
}
fun push(function: Fn) {
sharedState.ensureValid()
LuaJNI.lua_pushcclosure(pointer.address()) {
closure(it, function, performanceCritical)
closure(it, function)
}
}
fun push(function: Fn) = this.push(function, !RECORD_STACK_TRACES)
fun <T> push(self: T, function: Binding<T>) {
push {
function.invoke(self, it)
@ -1363,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)
@ -1590,8 +1678,8 @@ class LuaThread private constructor(
}
is JsonArray -> {
this.loadGlobal("jarray")
this.call(numResults = 1)
val handle = call(1) { loadGlobal("jarray") }.first()
handle.push(this)
val index = this.stackTop
for ((i, v) in value.withIndex()) {
@ -1603,8 +1691,8 @@ class LuaThread private constructor(
}
is JsonObject -> {
this.loadGlobal("jobject")
this.call(numResults = 1)
val handle = call(1) { loadGlobal("jobject") }.first()
handle.push(this)
val index = this.stackTop
@ -1622,6 +1710,10 @@ class LuaThread private constructor(
}
}
override fun toString(): String {
return "LuaThread at ${pointer.address()}"
}
companion object {
val LOGGER = LogManager.getLogger()

View File

@ -38,23 +38,26 @@ class LuaUpdateComponent(val lua: LuaThread, val name: Any) {
if (steps >= stepCount) {
steps %= stepCount
val type = lua.loadGlobal("update")
lua.callConditional {
val type = loadGlobal("update")
if (type != lastType) {
lastType = type
/*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) {
LOGGER.warn("Lua environment for $name has $type as global 'update', script update wasn't called")
}
}*/
if (type == LuaType.FUNCTION) {
preRun()
push(stepCount * Starbound.TIMESTEP)
return@callConditional true
} else {
return@callConditional false
}
}
if (type == LuaType.FUNCTION) {
preRun()
lua.push(stepCount * Starbound.TIMESTEP)
lua.call(1)
} else {
lua.pop()
}
}
}

View File

@ -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

View File

@ -386,6 +386,7 @@ fun provideServerWorldBindings(self: ServerWorld, lua: LuaThread) {
lua.setTableValueToStub("setLayerEnvironmentBiome")
lua.setTableValueToStub("setPlanetType")
lua.load(script, "@/internal/server_world.lua")
lua.call()
lua.call {
load(script, "@/internal/server_world.lua")
}
}

View File

@ -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")

View File

@ -943,6 +943,7 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaThread) {
lua.pop()
lua.load(worldScript, "@/internal/world.lua")
lua.call()
lua.call {
load(worldScript, "@/internal/world.lua")
}
}

View File

@ -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

View File

@ -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)

View File

@ -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)
@ -154,14 +159,14 @@ private fun createNode(
}
}
check(lua.loadGlobal("ActionNode") == LuaType.FUNCTION) { "Global ActionNode is not a Lua function" }
lua.push(name)
push(lua, parameters)
push(lua, output)
lua.call(3, 1)
val handle = lua.createHandle()
val handle = lua.call(1) {
check(loadGlobal("ActionNode") == LuaType.FUNCTION) { "Global ActionNode is not a Lua function" }
push(name)
push(this, parameters)
push(this, output)
}[0]
handles.add(handle)
lua.pop()
return handle
}
@ -169,15 +174,14 @@ private fun createNode(
functions.add(name)
val sacrifice = createNode(lua, data["child"] as JsonObject, treeParameters, blackboard, scripts, functions, handles)
check(lua.loadGlobal("DecoratorNode") == LuaType.FUNCTION) { "Global DecoratorNode is not a Lua function" }
lua.push(name)
push(lua, parameters)
lua.push(sacrifice)
lua.call(3, 1)
val handle = lua.createHandle()
handles.add(handle)
lua.pop()
val handle = lua.call(1) {
check(loadGlobal("DecoratorNode") == LuaType.FUNCTION) { "Global DecoratorNode is not a Lua function" }
push(name)
push(this, parameters)
push(sacrifice)
}[0]
handles.add(handle)
return handle
}
@ -189,21 +193,20 @@ private fun createNode(
val factory = CompositeNodeType.entries.valueOf(name)
check(lua.loadGlobal(factory.fnName) == LuaType.FUNCTION) { "Global ${factory.fnName} is not a Lua function" }
push(lua, parameters)
val handle = lua.call(1) {
check(loadGlobal(factory.fnName) == LuaType.FUNCTION) { "Global ${factory.fnName} is not a Lua function" }
push(this, parameters)
lua.pushTable(children.size)
pushTable(children.size)
for ((i, child) in children.withIndex()) {
lua.push(i + 1L)
lua.push(child)
lua.setTableValue()
}
for ((i, child) in children.withIndex()) {
push(i + 1L)
push(child)
setTableValue()
}
}[0]
lua.call(2, 1)
val handle = lua.createHandle()
handles.add(handle)
lua.pop()
return handle
}
@ -212,11 +215,7 @@ private fun createNode(
}
private fun createBlackboard(lua: LuaThread): LuaHandle {
lua.loadGlobal("Blackboard")
lua.call(numResults = 1)
val handle = lua.createHandle()
lua.pop()
return handle
return lua.call(1) { lua.loadGlobal("Blackboard") }[0]
}
private fun createBehaviorTree(args: LuaThread.ArgStack): Int {
@ -269,19 +268,36 @@ 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)
check(args.lua.loadGlobal("BehaviorState") == LuaType.FUNCTION) { "Global BehaviorState is not a Lua function" }
scripts.forEach {
args.lua.call {
loadGlobal("require")
push(it)
}
}
args.lua.push(blackboard)
args.lua.push(root)
val handle = args.lua.call(1) {
check(loadGlobal("BehaviorState") == LuaType.FUNCTION) { "Global BehaviorState is not a Lua function" }
args.lua.call(2, 1)
push(blackboard)
push(root)
}[0]
args.lua.call {
handle.push(this)
push("bake")
check(loadTableValue() == LuaType.FUNCTION) { "BehaviorTree.bake is not a Lua function" }
swap(-2, -1)
}
args.lua.push(handle)
handle.close()
handles.forEach { it.close() }
return 1
}
@ -297,6 +313,7 @@ fun provideBehaviorBindings(lua: LuaThread) {
lua.pop()
lua.load(script, "@/internal/behavior.lua")
lua.call()
lua.call {
load(script, "@/internal/behavior.lua")
}
}

View File

@ -97,10 +97,12 @@ class LegacyWireProcessor(val world: ServerWorld) {
if (newState != node.state) {
try {
node.state = newState
entity.lua.pushTable(hashSize = 2)
entity.lua.setTableValue("node", i)
entity.lua.setTableValue("level", newState)
entity.lua.invokeGlobal("onInputNodeChange", 1)
entity.lua.invokeGlobal("onInputNodeChange") {
pushTable(hashSize = 2)
setTableValue("node", i)
setTableValue("level", newState)
}
} catch (err: Throwable) {
LOGGER.error("Exception while updating wire state of $entity at ${entity.tilePosition} (input node index $i)", err)
}

View File

@ -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)
}

View File

@ -1,9 +1,17 @@
package ru.dbotthepony.kstarbound.server.world
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.luben.zstd.Zstd
import com.github.luben.zstd.ZstdDictCompress
import com.github.luben.zstd.ZstdDictDecompress
import com.github.luben.zstd.ZstdException
import com.github.luben.zstd.ZstdInputStreamNoFinalizer
import com.github.luben.zstd.ZstdOutputStreamNoFinalizer
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import kotlinx.coroutines.CoroutineScope
@ -12,6 +20,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.JsonArrayCollector
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
@ -48,10 +57,12 @@ import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.Closeable
import java.io.File
import java.lang.ref.Cleaner.Cleanable
import java.nio.ByteBuffer
import java.sql.Connection
import java.sql.DriverManager
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.time.Duration
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
@ -112,9 +123,17 @@ class ServerUniverse(folder: File? = null) : Universe(), Closeable {
`z` INTEGER NOT NULL,
`parameters` BLOB NOT NULL,
`planets` BLOB NOT NULL,
`dictionary` INTEGER NOT NULL,
PRIMARY KEY(`x`, `y`, `z`)
)
""".trimIndent())
it.execute("""
CREATE TABLE IF NOT EXISTS `dictionary` (
`version` INTEGER NOT NULL PRIMARY KEY,
`data` BLOB NOT NULL
)
""".trimIndent())
}
database.autoCommit = false
@ -125,10 +144,13 @@ class ServerUniverse(folder: File? = null) : Universe(), Closeable {
private val scope = CoroutineScope(ScheduledCoroutineExecutor(carrier) + SupervisorJob())
private val selectChunk = database.prepareStatement("SELECT `systems`, `constellations` FROM `chunk` WHERE `x` = ? AND `y` = ?")
private val selectSystem = database.prepareStatement("SELECT `parameters`, `planets` FROM `system` WHERE `x` = ? AND `y` = ? AND `z` = ?")
private val selectSystem = database.prepareStatement("SELECT `parameters`, `planets`, `dictionary` FROM `system` WHERE `x` = ? AND `y` = ? AND `z` = ?")
private val selectDictionary = database.prepareStatement("SELECT `data` FROM `dictionary` WHERE `version` = ?")
private val selectSamples = database.prepareStatement("SELECT `x`, `y`, `z`, `parameters`, `planets` FROM `system` WHERE `dictionary` = ? ORDER BY RANDOM() LIMIT 2000")
private val insertChunk = database.prepareStatement("INSERT INTO `chunk` (`x`, `y`, `systems`, `constellations`) VALUES (?, ?, ?, ?)")
private val insertSystem = database.prepareStatement("INSERT INTO `system` (`x`, `y`, `z`, `parameters`, `planets`) VALUES (?, ?, ?, ?, ?)")
private val insertSystem = database.prepareStatement("REPLACE INTO `system` (`x`, `y`, `z`, `parameters`, `planets`, `dictionary`) VALUES (?, ?, ?, ?, ?, ?)")
private val insertDictionary = database.prepareStatement("INSERT INTO `dictionary` (`version`, `data`) VALUES (?, ?)")
private class SerializedChunk(val x: Int, val y: Int, val systems: ByteArray, val constellations: ByteArray) {
fun write(statement: PreparedStatement) {
@ -164,19 +186,16 @@ class ServerUniverse(folder: File? = null) : Universe(), Closeable {
}.collect(JsonArrayCollector).writeJsonArrayZstd(4)
)
}
fun write(statement: PreparedStatement) {
serialize().write(statement)
}
}
private class SerializedSystem(val x: Int, val y: Int, val z: Int, val parameters: ByteArray, val planets: ByteArray) {
private class SerializedSystem(val x: Int, val y: Int, val z: Int, val parameters: ByteArray, val planets: ByteArray, val dictionary: Int) {
fun write(statement: PreparedStatement) {
statement.setInt(1, x)
statement.setInt(2, y)
statement.setInt(3, z)
statement.setBytes(4, parameters)
statement.setBytes(5, planets)
statement.setInt(6, dictionary)
statement.execute()
}
@ -191,19 +210,16 @@ class ServerUniverse(folder: File? = null) : Universe(), Closeable {
}
}
fun serialize(): SerializedSystem {
fun serialize(dict: Dicts? = null): SerializedSystem {
return SerializedSystem(
x, y, z,
Starbound.gson.toJsonTree(parameters).writeJsonElementZstd(8),
Starbound.gson.toJsonTree(parameters).writeJsonElementZstd(6, dictionary = dict?.compress),
planets.entries.stream()
.map { jsonArrayOf(it.key.first, it.key.second, it.value) }
.collect(JsonArrayCollector).writeJsonArrayZstd(8)
.collect(JsonArrayCollector).writeJsonArrayZstd(6, dictionary = dict?.compress),
dict?.version ?: 0
)
}
fun write(statement: PreparedStatement) {
serialize().write(statement)
}
}
// first, chunks in process of loading/generating must not be evicted
@ -254,6 +270,239 @@ class ServerUniverse(folder: File? = null) : Universe(), Closeable {
.executor(Starbound.EXECUTOR)
.build<Vector3i, CompletableFuture<System?>>()
private class Dicts(val version: Int, bytesParameters: ByteArray) : Closeable {
private var compressL: Lazy<ZstdDictCompress>? = lazy(LazyThreadSafetyMode.NONE) { ZstdDictCompress(bytesParameters, 6) }
private var decompressL: Lazy<ZstdDictDecompress>? = lazy(LazyThreadSafetyMode.NONE) { ZstdDictDecompress(bytesParameters) }
val compress get() = compressL!!.value
val decompress get() = decompressL!!.value
override fun close() {
/*if (compressL?.isInitialized() == true) {
compressL!!.value.close()
compressL = null
}
if (decompressL?.isInitialized() == true) {
decompressL!!.value.close()
decompressL = null
}*/
// zstd dicts use finalizers
// while we could close the dicts explicitly, they might be still in use
// due to off-thread main thread compression being in-process
compressL = null
decompressL = null
}
}
private val dictionaryCache = Caffeine.newBuilder()
.maximumSize(32L)
.expireAfterAccess(Duration.ofMinutes(5))
.executor(Starbound.EXECUTOR)
.evictionListener<Int, KOptional<Dicts>> { _, value, _ -> value?.orNull()?.close() }
.build<Int, KOptional<Dicts>>()
private fun loadDictionary(id: Int): Dicts? {
return dictionaryCache.get(id) { v ->
selectDictionary.setInt(1, v)
selectDictionary.executeQuery().use {
if (it.next()) {
val bytes = decompress(it.getBytes(1), null)
return@get KOptional(Dicts(v, bytes))
} else {
return@get KOptional()
}
}
}.orNull()
}
private var latestDictionary = 0
private var latestDictionaryCapacity = 0
private var dictionary: Dicts? = null
init {
database.createStatement().use {
it.executeQuery("""
SELECT MAX(`version`) FROM `dictionary`
""".trimIndent()).use {
it.next()
// if dictionary table is empty, max() returns null, which gets treated as 0 by getInt
latestDictionary = it.getInt(1)
}
it.executeQuery("""
SELECT COUNT(*) FROM `dictionary` WHERE `version` = $latestDictionary
""".trimIndent()).use {
it.next()
latestDictionaryCapacity = it.getInt(1)
}
}
if (latestDictionary != 0) {
dictionary = loadDictionary(latestDictionary)!!
}
}
private fun decompress(input: ByteArray, dictionary: ZstdDictDecompress?): ByteArray {
val parts = ArrayList<ByteArray>()
val stream = ZstdInputStreamNoFinalizer(FastByteArrayInputStream(input))
if (dictionary != null)
stream.setDict(dictionary)
while (true) {
val alloc = ByteArray(1024 * 16)
val read = stream.read(alloc)
if (read <= 0) {
break
} else if (read == alloc.size) {
parts.add(alloc)
} else {
parts.add(alloc.copyOf(read))
}
}
stream.close()
val output = ByteArray(parts.sumOf { it.size })
var i = 0
for (part in parts) {
java.lang.System.arraycopy(part, 0, output, i, part.size)
i += part.size
}
return output
}
private fun recompress(input: ByteArray, dict: ZstdDictCompress): ByteArray {
val stream = FastByteArrayOutputStream()
ZstdOutputStreamNoFinalizer(stream, 6).use {
it.setDict(dict)
it.write(input)
}
return stream.array.copyOf(stream.length)
}
private fun recompress(input: ByteArray): ByteArray {
val stream = FastByteArrayOutputStream()
ZstdOutputStreamNoFinalizer(stream, 6).use {
it.write(input)
}
return stream.array.copyOf(stream.length)
}
private fun dictionaryMaintenance() {
val limit = if (latestDictionary == 0) 200 else 2000
if (latestDictionaryCapacity >= limit) {
LOGGER.info("Optimizing star map compression algorithm, star map might become unresponsive for a moment...")
class Tuple(val x: Int, val y: Int, val z: Int, parameters: ByteArray, planets: ByteArray) {
val parameters = Starbound.EXECUTOR.supplyAsync { decompress(parameters, dictionary?.decompress) }
val planets = Starbound.EXECUTOR.supplyAsync { decompress(planets, dictionary?.decompress) }
}
// current dictionary is old enough,
// create new one to adapt to possible data changes e.g. due to added or removed mods
// collect samples
val samples = ArrayList<Tuple>()
selectSamples.setInt(1, latestDictionary)
selectSamples.executeQuery().use {
while (it.next()) {
samples.add(Tuple(it.getInt(1), it.getInt(2), it.getInt(3), it.getBytes(4), it.getBytes(5)))
}
}
// wait for samples to be decompressed
val sampleBuffer = ByteBuffer.allocateDirect(samples.sumOf { it.parameters.join().size + it.planets.join().size })
for (sample in samples) {
sampleBuffer.put(sample.parameters.join())
sampleBuffer.put(sample.planets.join())
}
sampleBuffer.position(0)
// create dictionary
val buffer = ByteBuffer.allocateDirect(1024 * 1024 * 4) // up to 4 MiB dictionary (before compression, since dedicated zstd dictionaries are not compressed)
// 4 MiB seems to be sweet spot, dictionary isn't that big (smaller indices to reference inside dictionary),
// takes small amount of space, and training is done moderately fast
// Too big dictionaries cause over-fitting and generally *reduce* compression ratio,
// while too small dictionaries don't contain enough data to be effective
val status = Zstd.trainFromBufferDirect(
sampleBuffer,
IntArray(samples.size) { samples[it].parameters.join().size + samples[it].planets.join().size },
buffer,
false, 6
)
if (Zstd.isError(status)) {
throw ZstdException(status)
}
val copyBytes = ByteArray(status.toInt())
buffer.position(0)
buffer.get(copyBytes)
val dicts = Dicts(++latestDictionary, copyBytes)
dictionary = dicts
dictionaryCache.put(latestDictionary, KOptional(dicts))
insertDictionary.setInt(1, latestDictionary)
insertDictionary.setBytes(2, recompress(copyBytes))
insertDictionary.execute()
database.commit()
latestDictionaryCapacity = 0
LOGGER.info("Star map compression optimized")
if (latestDictionary == 1) {
LOGGER.info("Recompressing star map chunks with new dictionary...")
// previous data wasn't compressed by any dictionary, so let's recompress it with new dictionary
val recompressed = ArrayList<CompletableFuture<Triple<Tuple, ByteArray, ByteArray>>>()
for (tuple in samples) {
recompressed.add(
Starbound.EXECUTOR.supplyAsync {
Triple(tuple, recompress(tuple.parameters.join(), dicts.compress), recompress(tuple.planets.join(), dicts.compress))
}
)
}
try {
for ((tuple, parameters, planets) in recompressed.map { it.join() }) {
insertSystem.setInt(1, tuple.x)
insertSystem.setInt(2, tuple.y)
insertSystem.setInt(3, tuple.z)
insertSystem.setBytes(4, parameters)
insertSystem.setBytes(5, planets)
insertSystem.setInt(6, 1)
insertSystem.execute()
}
database.commit()
} catch (err: Throwable) {
database.rollback()
throw err
}
LOGGER.info("Recompressed star map chunks with new dictionary")
}
}
}
private fun loadSystem(pos: Vector3i): CompletableFuture<System>? {
selectSystem.setInt(1, pos.x)
selectSystem.setInt(2, pos.y)
@ -263,12 +512,21 @@ class ServerUniverse(folder: File? = null) : Universe(), Closeable {
if (it.next()) {
val parametersBytes = it.getBytes(1)
val planetsBytes = it.getBytes(2)
val dictionaryVersion = it.getInt(3)
val dict: ZstdDictDecompress?
if (dictionaryVersion == 0) {
dict = null
} else {
dict = loadDictionary(dictionaryVersion)?.decompress
}
// deserialize in off-thread since it involves big json structures
Starbound.EXECUTOR.supplyAsync {
val parameters: CelestialParameters = Starbound.gson.fromJson(parametersBytes.readJsonElementZstd())!!
val parameters: CelestialParameters = Starbound.gson.fromJson(parametersBytes.readJsonElementZstd(dictionary = dict))!!
val planets: Map<Pair<Int, Int>, CelestialParameters> = planetsBytes.readJsonArrayZstd().associate {
val planets: Map<Pair<Int, Int>, CelestialParameters> = planetsBytes.readJsonArrayZstd(dictionary = dict).associate {
it as JsonArray
(it[0].asInt to it[1].asInt) to Starbound.gson.fromJson(it[2])!!
}
@ -521,6 +779,8 @@ class ServerUniverse(folder: File? = null) : Universe(), Closeable {
val random = random(staticRandom64(chunkPos.x, chunkPos.y, "ChunkIndexMix"))
val region = chunkRegion(chunkPos)
val dict = dictionary
return CompletableFuture.supplyAsync(Supplier {
val constellationCandidates = ArrayList<Vector2i>()
val systemPositions = ArrayList<Vector3i>()
@ -534,7 +794,7 @@ class ServerUniverse(folder: File? = null) : Universe(), Closeable {
val system = generateSystem(random, pos) ?: continue
systemPositions.add(pos)
systems.add(CompletableFuture.supplyAsync(Supplier { system.serialize() }, Starbound.EXECUTOR))
systems.add(CompletableFuture.supplyAsync(Supplier { system.serialize(dict) }, Starbound.EXECUTOR))
systemCache.put(Vector3i(system.x, system.y, system.z), CompletableFuture.completedFuture(system))
if (
@ -554,6 +814,13 @@ class ServerUniverse(folder: File? = null) : Universe(), Closeable {
serialized.write(insertChunk)
systems.forEach { it.get().write(insertSystem) }
database.commit()
if (latestDictionary == (dict?.version ?: 0)) {
latestDictionaryCapacity += systems.size
// enqueue dictionary maintenance
carrier.execute(::dictionaryMaintenance)
}
chunk
}, carrier)
}, Starbound.EXECUTOR).thenCompose { it }
@ -738,4 +1005,8 @@ class ServerUniverse(folder: File? = null) : Universe(), Closeable {
}
override val region: AABBi = AABBi(Vector2i(baseInformation.xyCoordRange.x, baseInformation.xyCoordRange.x), Vector2i(baseInformation.xyCoordRange.y, baseInformation.xyCoordRange.y))
companion object {
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -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)
}

View File

@ -333,13 +333,13 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE
// TODO: hitDamageNotificationLimiter++ < Globals.npcs.hitDamageNotificationLimit maybe?
if (totalDamage > 0.0) {
lua.pushTable(hashSize = 4)
lua.setTableValue("sourceId", damage.request.sourceEntityId)
lua.setTableValue("damage", totalDamage)
lua.setTableValue("sourceDamage", damage.request.damage)
lua.setTableValue("sourceKind", damage.request.damageSourceKind)
lua.invokeGlobal("damage", 1)
lua.invokeGlobal("damage") {
pushTable(hashSize = 4)
setTableValue("sourceId", damage.request.sourceEntityId)
setTableValue("damage", totalDamage)
setTableValue("sourceDamage", damage.request.damage)
setTableValue("sourceKind", damage.request.damageSourceKind)
}
}
if (health <= 0.0) {
@ -350,7 +350,7 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE
}
private val shouldDie: Boolean get() {
val result = lua.invokeGlobal("shouldDie", 1, { 0 }, { getBoolean() == true }).orElse(false)
val result = lua.invokeGlobal("shouldDie", 1, {}, { getBoolean() == true }).orElse(false)
return result || health <= 0.0 //|| lua.errorState
}

View File

@ -332,13 +332,13 @@ class NPCEntity(val variant: NPCVariant) : HumanoidActorEntity() {
val totalDamage = notifications.sumOf { it.healthLost }
if (totalDamage > 0.0 && hitDamageNotificationLimiter++ < Globals.npcs.hitDamageNotificationLimit) {
lua.pushTable(hashSize = 4)
lua.setTableValue("sourceId", damage.request.sourceEntityId)
lua.setTableValue("damage", totalDamage)
lua.setTableValue("sourceDamage", damage.request.damage)
lua.setTableValue("sourceKind", damage.request.damageSourceKind)
lua.invokeGlobal("damage", 1)
lua.invokeGlobal("damage") {
pushTable(hashSize = 4)
setTableValue("sourceId", damage.request.sourceEntityId)
setTableValue("damage", totalDamage)
setTableValue("sourceDamage", damage.request.damage)
setTableValue("sourceKind", damage.request.damageSourceKind)
}
}
return notifications
@ -407,7 +407,7 @@ class NPCEntity(val variant: NPCVariant) : HumanoidActorEntity() {
remove(RemovalReason.DYING)
return
} else {
val shouldDie = lua.invokeGlobal("shouldDie", 1, { 0 }, { getBoolean() == true }).orElse(false)
val shouldDie = lua.invokeGlobal("shouldDie", 1, {}, { getBoolean() == true }).orElse(false)
if (shouldDie) {
remove(RemovalReason.DYING)
@ -425,7 +425,7 @@ class NPCEntity(val variant: NPCVariant) : HumanoidActorEntity() {
super.onRemove(world, reason)
if (isLocal)
lua.invokeGlobal("die", 0)
lua.invokeGlobal("die")
val dropPools by lazy { dropPools.stream().map { it.entry }.filterNotNull().toList() }

View File

@ -191,7 +191,6 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf
fun experienceDamage(damage: DamageData): List<DamageNotification> {
val results = lua.invokeGlobal("applyDamageRequest", 1, {
push(Starbound.gson.toJsonTree(damage))
1
}, { getJson() as? JsonArray }).flatMap { KOptional.ofNullable(it) }
if (results.isPresent) {
@ -632,7 +631,7 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf
fun remove() {
if (entity.isLocal)
lua.invokeGlobal("onExpire", 0)
lua.invokeGlobal("onExpire")
uniqueEffectMetadata.remove(metadataNetworkID)

View File

@ -306,13 +306,13 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
fun addConnection(connection: WireConnection) {
if (connection !in connectionsInternal) {
connectionsInternal.add(connection.copy())
lua.invokeGlobal("onNodeConnectionChange", 0)
lua.invokeGlobal("onNodeConnectionChange")
}
}
fun removeConnection(connection: WireConnection) {
if (connectionsInternal.remove(connection)) {
lua.invokeGlobal("onNodeConnectionChange", 0)
lua.invokeGlobal("onNodeConnectionChange")
}
}
@ -325,20 +325,20 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
val any = otherConnections?.getOrNull(it.index)?.connectionsInternal?.removeIf { it.entityLocation == tilePosition && it.index == index }
if (any == true) {
otherEntity!!.lua.invokeGlobal("onNodeConnectionChange", 0)
otherEntity!!.lua.invokeGlobal("onNodeConnectionChange")
}
any == true
}
if (any)
lua.invokeGlobal("onNodeConnectionChange", 0)
lua.invokeGlobal("onNodeConnectionChange")
}
}
fun removeConnectionsTo(pos: Vector2i) {
if (connectionsInternal.removeIf { it.entityLocation == pos }) {
lua.invokeGlobal("onNodeConnectionChange", 0)
lua.invokeGlobal("onNodeConnectionChange")
}
}
@ -599,8 +599,6 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
setTableValue("source", diff)
setTableValue("sourceId", request.source)
1
}, { getJson() ?: JsonNull.INSTANCE })
if (result.isPresent) {
@ -675,7 +673,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
// break connection if other entity got removed
if (connection.otherEntity?.removalReason?.removal == true) {
itr.remove()
lua.invokeGlobal("onNodeConnectionChange", 0)
lua.invokeGlobal("onNodeConnectionChange")
continue
}
@ -691,7 +689,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
// break connection if we point at invalid node
if (otherNode == null) {
itr.remove()
lua.invokeGlobal("onNodeConnectionChange", 0)
lua.invokeGlobal("onNodeConnectionChange")
} else {
newState = newState!! || otherNode.state
}
@ -706,10 +704,12 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
// otherwise, keep current node state
if (newState != null && node.state != newState) {
node.state = newState
lua.pushTable(hashSize = 2)
lua.setTableValue("node", i)
lua.setTableValue("level", newState)
lua.invokeGlobal("onInputNodeChange", 1)
lua.invokeGlobal("onInputNodeChange") {
pushTable(hashSize = 2)
setTableValue("node", i)
setTableValue("level", newState)
}
}
}
}
@ -781,9 +781,10 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
}
}
if (!isRemote && reason.dying) {
lua.push(health <= 0.0)
lua.invokeGlobal("die", 1)
if (isLocal && reason.dying) {
lua.invokeGlobal("die") {
push(health <= 0.0)
}
try {
if (doSmash) {

View File

@ -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,15 +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.root:bake()
end
function statePrototype:blackboard()

View File

@ -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

View File

@ -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

View File

@ -12,12 +12,22 @@ object LuaTests {
fun test() {
val lua = LuaThread()
lua.ensureExtraCapacity(1000)
lua.push {
throw IllegalArgumentException("This is error message")
}
lua.loadGlobal("collectgarbage")
lua.push("count")
lua.call(1, 1)
println(lua.popDouble()!! * 1024)
lua.storeGlobal("test")
lua.call {
lua.load("""
local function errornous()
test()
end
local cor = coroutine.create(errornous)
print(coroutine.resume(cor))
""".trimIndent())
}
lua.close()
}