diff --git a/ADDITIONS.md b/ADDITIONS.md index 07179fc9..cab8e348 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -155,6 +155,7 @@ In addition to `add`, `multiply`, `merge` and `override` new merge methods are a * Added `animator.effects(): List` * Added `animator.hasEffect(effect: string): boolean` * Added `animator.parts(): List` + * Added `animator.hasPart(part: String): boolean` ## mcontroller @@ -185,6 +186,10 @@ In addition to `add`, `multiply`, `merge` and `override` new merge methods are a * Added `status.minimumLiquidStatusEffectPercentage(): Double` * Added `status.setMinimumLiquidStatusEffectPercentage(value: Double)` +## objec + + * Added `object.worldSpaces(): List`, similar to `object.spaces()`, but returns coordinates in world-space grid instead of local coordinates + ## Path Finder In new engine, pathfinder returned by `world.platformerPathStart()`, if unable find path to goal inside `finder:explore()` due to budget constraints, launches diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index da253a25..353e198d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -116,6 +116,8 @@ object Starbound : BlockableEventLoop("Multiverse Thread"), Scheduler, ISBFileLo // compile flags. uuuugh const val DEDUP_CELL_STATES = true + // Debug option. Caffeine is almost 3x times slower than specialized interner, + // and saves only 200 mib on big modpack vs specialized's 300 mib const val USE_CAFFEINE_INTERNER = false const val USE_INTERNER = true diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/CompositeNodeType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/CompositeNodeType.kt index 9f2fd8eb..09b9f737 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/CompositeNodeType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/CompositeNodeType.kt @@ -1,19 +1,11 @@ package ru.dbotthepony.kstarbound.defs.actor.behavior -import com.google.common.collect.ImmutableList import ru.dbotthepony.kstarbound.json.builder.IStringSerializable -import ru.dbotthepony.kstarbound.world.entities.behavior.AbstractBehaviorNode -import ru.dbotthepony.kstarbound.world.entities.behavior.BehaviorTree -import ru.dbotthepony.kstarbound.world.entities.behavior.Blackboard -import ru.dbotthepony.kstarbound.world.entities.behavior.DynamicNode -import ru.dbotthepony.kstarbound.world.entities.behavior.ParallelNode -import ru.dbotthepony.kstarbound.world.entities.behavior.RandomizeNode -import ru.dbotthepony.kstarbound.world.entities.behavior.SequenceNode -enum class CompositeNodeType(override val jsonName: String, val factory: (Map, ImmutableList) -> AbstractBehaviorNode) : IStringSerializable { - SEQUENCE("Sequence", { _, c -> SequenceNode(c, isSelector = false) }), - SELECTOR("Selector", { _, c -> SequenceNode(c, isSelector = true) }), - PARALLEL("Parallel", { p, c -> ParallelNode(p, c) }), - DYNAMIC("Dynamic", { _, c -> DynamicNode(c) }), - RANDOMIZE("Randomize", { _, c -> RandomizeNode(c) }); +enum class CompositeNodeType(override val jsonName: String, val fnName: String) : IStringSerializable { + SEQUENCE("Sequence", "SequenceNode"), + SELECTOR("Selector", "SelectorNode"), + PARALLEL("Parallel", "ParallelNode"), + DYNAMIC("Dynamic", "DynamicNode"), + RANDOMIZE("Randomize", "RandomNode"); } 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 e47ce4b7..c253d8d0 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 @@ -6,12 +6,23 @@ import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kstarbound.json.popObject +import ru.dbotthepony.kstarbound.lua.LuaThread import ru.dbotthepony.kstarbound.util.asStringOrNull import ru.dbotthepony.kstarbound.util.valueOf -import ru.dbotthepony.kstarbound.world.entities.behavior.NodeParameterType +import ru.dbotthepony.kstarbound.lua.userdata.NodeParameterType @JsonAdapter(NodeOutput.Adapter::class) 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) + + if (key != null) + lua.setTableValue("key", key) + + lua.setTableValue("ephemeral", ephemeral) + } + class Adapter : TypeAdapter() { override fun write(out: JsonWriter, value: NodeOutput) { out.beginObject() 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 4a489615..25b1eee1 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 @@ -8,8 +8,9 @@ import com.google.gson.stream.JsonWriter import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kstarbound.json.popObject +import ru.dbotthepony.kstarbound.lua.LuaThread import ru.dbotthepony.kstarbound.util.valueOf -import ru.dbotthepony.kstarbound.world.entities.behavior.NodeParameterType +import ru.dbotthepony.kstarbound.lua.userdata.NodeParameterType @JsonAdapter(NodeParameter.Adapter::class) data class NodeParameter(val type: NodeParameterType, val value: NodeParameterValue) { @@ -17,6 +18,17 @@ data class NodeParameter(val type: NodeParameterType, val value: NodeParameterVa return "[$type as $value]" } + fun push(lua: LuaThread) { + lua.pushTable(hashSize = 3) + lua.setTableValue("type", type.ordinal) + + if (value.key != null) { + lua.setTableValue("key", value.key) + } else { + lua.setTableValue("value", value.value) + } + } + class Adapter : TypeAdapter() { override fun write(out: JsonWriter, value: NodeParameter) { out.beginObject() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameterValue.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameterValue.kt index 97f46b9c..9f66f402 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameterValue.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameterValue.kt @@ -17,6 +17,10 @@ import ru.dbotthepony.kstarbound.json.popObject */ @JsonAdapter(NodeParameterValue.Adapter::class) data class NodeParameterValue(val key: String?, val value: JsonElement?) { + init { + require(key != null || value != null) { "Both key and value are nulls" } + } + override fun toString(): String { if (key != null) return "key=$key" diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt index 8d5b440a..b42451a8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt @@ -26,6 +26,7 @@ import ru.dbotthepony.kommons.util.IStruct3i import ru.dbotthepony.kommons.util.IStruct4d import ru.dbotthepony.kommons.util.IStruct4f import ru.dbotthepony.kommons.util.IStruct4i +import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2f import ru.dbotthepony.kstarbound.math.vector.Vector2i @@ -557,6 +558,44 @@ fun LuaThread.ArgStack.nextOptionalVector2d(position: Int = this.position++): Ve return lua.getVector2d(position) } +fun LuaThread.getVector2f(stackIndex: Int = -1): Vector2f? { + val abs = this.absStackIndex(stackIndex) + + if (!this.isTable(abs)) + return null + + push(1) + loadTableValue(abs) + + val x = getFloat(abs + 1) + pop() + x ?: return null + + push(2) + loadTableValue(abs) + + val y = getFloat(abs + 1) + pop() + y ?: return null + + return Vector2f(x, y) +} + +fun LuaThread.ArgStack.nextVector2f(position: Int = this.position++): Vector2f { + if (position !in 1 ..this.top) + throw IllegalArgumentException("bad argument #$position: Vector2f expected, got nil") + + return lua.getVector2f(position) + ?: throw IllegalArgumentException("bad argument #$position: Vector2f expected, got ${lua.typeAt(position)}") +} + +fun LuaThread.ArgStack.nextOptionalVector2f(position: Int = this.position++): Vector2f? { + if (position !in 1 ..this.top) + return null + + return lua.getVector2f(position) +} + fun LuaThread.getVector2iOrAABB(stackIndex: Int = -1): Either? { val abs = this.absStackIndex(stackIndex) @@ -1062,3 +1101,28 @@ fun LuaThread.push(value: AABBi?) { push(w.toLong()) setTableValue(table) } + +fun LuaThread.push(value: Poly?) { + value ?: return push() + + pushTable(value.vertices.size) + + for ((i, vertex) in value.vertices.withIndex()) + setTableValue(i + 1L, vertex) +} + +fun LuaThread.setTableValue(key: String, value: Poly?) { push(key); push(value); setTableValue() } +fun LuaThread.setTableValue(key: Int, value: Poly?) { push(key.toLong()); push(value); setTableValue() } +fun LuaThread.setTableValue(key: Long, value: Poly?) { push(key); push(value); setTableValue() } + +fun LuaThread.push(value: Registry.Ref<*>, preferStringsIDs: Boolean = true) { + if (value.isPresent) { + if (preferStringsIDs || value.entry!!.id == null) { + push(value.entry!!.key) + } else { + push(value.entry!!.id!!.toLong()) + } + } else { + value.key.map({ push(it) }, { push(it.toLong()) }) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaMessageHandlerComponent.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaMessageHandlerComponent.kt index 82727167..0157539d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaMessageHandlerComponent.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaMessageHandlerComponent.kt @@ -1,61 +1,66 @@ package ru.dbotthepony.kstarbound.lua -import com.google.gson.JsonArray import com.google.gson.JsonElement -import com.google.gson.JsonNull import org.apache.logging.log4j.LogManager -import org.classdump.luna.ByteString -import org.classdump.luna.exec.CallPausedException -import org.classdump.luna.runtime.LuaFunction -import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.util.ActionPacer import ru.dbotthepony.kstarbound.util.sbIntern -import ru.dbotthepony.kstarbound.world.World -class LuaMessageHandlerComponent(val lua: LuaEnvironment, val nameProvider: () -> String) { - private val handlers = HashMap>() +class LuaMessageHandlerComponent(lua: LuaThread, val nameProvider: () -> String) { + private val handlers = HashMap() - init { - val table = lua.newTable() - lua.globals["message"] = table + private fun setHandler(args: LuaThread.ArgStack): Int { + val name = args.nextString() + val peek = args.peek() - table["setHandler"] = luaFunction { message: ByteString, handler: LuaFunction<*, *, *, *, *>? -> - if (handler == null) { - handlers.remove(message.decode()) - } else { - handlers[message.decode().sbIntern()] = handler - } + if (peek.isNothing) { + handlers.remove(name)?.close() + } else if (peek == LuaType.FUNCTION) { + args.lua.dup(2) + val handle = args.lua.createHandle() + handlers.put(name.sbIntern(), handle)?.close() } + + return 0 } - private val logPacer = ActionPacer(1, 5) + init { + lua.pushTable() + lua.dup() + lua.storeGlobal("message") + lua.setTableValue("setHandler", ::setHandler) + lua.pop() + } - fun handle(message: String, isLocal: Boolean, arguments: JsonArray): JsonElement? { - val handler = handlers[message] ?: return null + val logPacer = ActionPacer(1, 5) + + fun lookupHandler(name: String): LuaHandle? { + return handlers[name] + } + + inline fun handle(lua: LuaThread, message: String, isLocal: Boolean, arguments: LuaThread.() -> Int): JsonElement? { + val handler = lookupHandler(message) ?: return null + val top = lua.stackTop try { - val unpack = arguments.map { lua.from(it) }.toTypedArray() - val result = lua.executor.call(lua, handler, isLocal, *unpack) + lua.push(handler) + lua.push(isLocal) + val amountOfArguments = arguments(lua) + check(amountOfArguments >= 0) { "Invalid amount of arguments to pass to Lua handler: $amountOfArguments" } - if (result.isEmpty()) { - return null - } else { - return toJsonFromLua(result[0]) - } - } catch (err: CallPausedException) { - if (logPacer.consumeAndReturnDeadline() <= 0L) - LOGGER.error("${nameProvider.invoke()}: '$message' handler attempted to yield across C boundary", err) + lua.call(amountOfArguments + 1, 1) - throw World.MessageCallException("$message handler attempted to yield across C boundary") + return lua.getJson() } catch (err: Throwable) { if (logPacer.consumeAndReturnDeadline() <= 0L) LOGGER.error("${nameProvider.invoke()}: Exception while handling message '$message'", err) throw err + } finally { + lua.setTop(top) } } companion object { - private val LOGGER = LogManager.getLogger() + val LOGGER = LogManager.getLogger() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt index 1e0440ca..a7354c9c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt @@ -460,10 +460,22 @@ class LuaThread private constructor( val b = status[0] > 0 stack.close() - if (!b) + if (!b) return null + return value + } + + fun getFloat(stackIndex: Int = -1): Float? { + if (!this.isNumber(stackIndex)) return null - return value + val stack = MemoryStack.stackPush() + val status = stack.mallocInt(1) + val value = LuaJNR.INSTANCE.lua_tonumberx(this.pointer, stackIndex, MemoryUtil.memAddress(status)) + val b = status[0] > 0 + stack.close() + + if (!b) return null + return value.toFloat() } private fun getDoubleRaw(stackIndex: Int = -1): Double { @@ -499,7 +511,7 @@ class LuaThread private constructor( LuaType.NIL -> JsonNull.INSTANCE LuaType.BOOLEAN -> InternedJsonElementAdapter.of(this.getBooleanRaw(abs)) LuaType.NUMBER -> JsonPrimitive(if (this.isInteger(abs)) this.getLongRaw(abs) else this.getDoubleRaw(abs)) - LuaType.STRING -> JsonPrimitive(this.getStringRaw(abs, limit = limit)) + LuaType.STRING -> JsonPrimitive(stringInterner.intern(getStringRaw(abs, limit = limit))) LuaType.TABLE -> { val values = HashMap() @@ -826,10 +838,26 @@ class LuaThread private constructor( return LuaType.valueOf(LuaJNR.INSTANCE.lua_getglobal(this.pointer, name)) } + fun setGlobal(name: String, value: JsonElement?) { + push(value) + storeGlobal(name) + } + + fun getGlobal(name: String): JsonElement? { + loadGlobal(name) + return getJson() + } + inner class ArgStack(val top: Int) { val lua get() = this@LuaThread var position = 1 + val isEmpty: Boolean + get() = top == 0 + + val isNotEmpty: Boolean + get() = top != 0 + init { if (top >= 10) { this@LuaThread.ensureExtraCapacity(10) @@ -849,7 +877,7 @@ class LuaThread private constructor( return peek } - fun hasNext(): Boolean { + val hasNext: Boolean get() { return position <= top } @@ -951,7 +979,7 @@ class LuaThread private constructor( fun nextJson(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonElement { if (position !in 1 ..this.top) - throw IllegalArgumentException("bad argument #$position: json expected, got nil") + throw IllegalArgumentException("bad argument #$position: json expected, got nothing") val value = this@LuaThread.getJson(position, limit = limit) return value ?: throw IllegalArgumentException("bad argument #$position: anything expected, got ${this@LuaThread.typeAt(position)}") @@ -988,6 +1016,14 @@ class LuaThread private constructor( ?: throw IllegalArgumentException("bad argument #$position: number expected, got ${this@LuaThread.typeAt(position)}") } + fun nextFloat(position: Int = this.position++): Float { + if (position !in 1 ..this.top) + throw IllegalArgumentException("bad argument #$position: number expected, got nil") + + return this@LuaThread.getFloat(position) + ?: throw IllegalArgumentException("bad argument #$position: number expected, got ${this@LuaThread.typeAt(position)}") + } + fun nextOptionalDouble(position: Int = this.position++): Double? { val type = this@LuaThread.typeAt(position) @@ -997,6 +1033,15 @@ class LuaThread private constructor( return this@LuaThread.getDouble(position) } + fun nextOptionalFloat(position: Int = this.position++): Float? { + val type = this@LuaThread.typeAt(position) + + if (type != LuaType.NUMBER && type != LuaType.NIL && type != LuaType.NONE) + throw IllegalArgumentException("bad argument #$position: double expected, got $type") + + return this@LuaThread.getFloat(position) + } + fun nextOptionalLong(position: Int = this.position++): Long? { val type = this@LuaThread.typeAt(position) @@ -1136,6 +1181,12 @@ class LuaThread private constructor( setTableValue() } + fun pushBinding(self: T, name: String, function: (T) -> Unit) { + push(name) + push { function.invoke(self); 0 } + setTableValue() + } + fun ensureExtraCapacity(maxSize: Int): Boolean { return LuaJNR.INSTANCE.lua_checkstack(pointer, maxSize) } @@ -1376,6 +1427,40 @@ class LuaThread private constructor( return setTableValue(key.toLong(), value) } + fun setTableValue(key: String, value: Boolean?) { + value ?: return + this.push(key) + this.push(value) + this.setTableValue() + } + + fun setTableValue(key: String, value: Boolean) { + this.push(key) + this.push(value) + this.setTableValue() + } + + fun setTableValue(key: Int, value: Boolean?) { + return setTableValue(key.toLong(), value) + } + + fun setTableValue(key: Int, value: Boolean) { + return setTableValue(key.toLong(), value) + } + + fun setTableValue(key: Long, value: Boolean?) { + value ?: return + this.push(key) + this.push(value) + this.setTableValue() + } + + fun setTableValue(key: Long, value: Boolean) { + this.push(key) + this.push(value) + this.setTableValue() + } + fun setTableValue(key: Long, value: JsonElement?) { this.push(key) this.push(value) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt index 1c26cda6..0ebd0935 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt @@ -1,22 +1,32 @@ package ru.dbotthepony.kstarbound.lua +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.Starbound -class LuaUpdateComponent(val lua: LuaEnvironment) { +class LuaUpdateComponent(val lua: LuaThread, val name: Any) { var stepCount = 1.0 private var steps = 0.0 + private var lastType = LuaType.FUNCTION + + private fun updateDt(args: LuaThread.ArgStack): Int { + args.lua.push(stepCount * Starbound.TIMESTEP) + return 1 + } + + private fun setUpdateDelta(args: LuaThread.ArgStack): Int { + stepCount = args.nextDouble() + return 0 + } init { - val script = lua.newTable() - lua.globals["script"] = script + lua.pushTable() + lua.dup() + lua.storeGlobal("script") - script["updateDt"] = luaFunction { - returnBuffer.setTo(stepCount * Starbound.TIMESTEP) - } + lua.setTableValue("updateDt", ::updateDt) + lua.setTableValue("setUpdateDelta", ::setUpdateDelta) - script["setUpdateDelta"] = luaFunction { ticks: Number -> - stepCount = ticks.toDouble() - } + lua.pop() } fun update(delta: Double, preRun: () -> Unit) { @@ -27,20 +37,32 @@ class LuaUpdateComponent(val lua: LuaEnvironment) { if (steps >= stepCount) { steps %= stepCount - preRun() - lua.invokeGlobal("update", stepCount * Starbound.TIMESTEP) + + val type = lua.loadGlobal("update") + + 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() + lua.push(stepCount * Starbound.TIMESTEP) + lua.call(1) + } else { + lua.pop() + } } } - fun update(delta: Double, vararg arguments: Any?) { - if (stepCount == 0.0) - return + fun update(delta: Double) { + return update(delta) {} + } - steps += delta / Starbound.TIMESTEP - - if (steps >= stepCount) { - steps %= stepCount - lua.invokeGlobal("update", stepCount * Starbound.TIMESTEP, *arguments) - } + companion object { + private val LOGGER = LogManager.getLogger() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/AnimatorBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/AnimatorBindings.kt index ce7d2a92..6f495c1c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/AnimatorBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/AnimatorBindings.kt @@ -1,261 +1,318 @@ package ru.dbotthepony.kstarbound.lua.bindings -import org.classdump.luna.ByteString -import org.classdump.luna.Table -import ru.dbotthepony.kommons.collect.map -import ru.dbotthepony.kommons.collect.toList +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.lua.LuaType +import ru.dbotthepony.kstarbound.lua.nextAABB +import ru.dbotthepony.kstarbound.lua.nextColor +import ru.dbotthepony.kstarbound.lua.nextOptionalVector2f +import ru.dbotthepony.kstarbound.lua.nextPoly +import ru.dbotthepony.kstarbound.lua.nextVector2d +import ru.dbotthepony.kstarbound.lua.nextVector2f +import ru.dbotthepony.kstarbound.lua.push import ru.dbotthepony.kstarbound.math.vector.Vector2f -import ru.dbotthepony.kstarbound.lua.LuaEnvironment -import ru.dbotthepony.kstarbound.lua.from -import ru.dbotthepony.kstarbound.lua.get -import ru.dbotthepony.kstarbound.lua.iterator -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.luaFunctionArray -import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.toAABB -import ru.dbotthepony.kstarbound.lua.toByteString -import ru.dbotthepony.kstarbound.lua.toColor -import ru.dbotthepony.kstarbound.lua.toPoly -import ru.dbotthepony.kstarbound.lua.toVector2d -import ru.dbotthepony.kstarbound.lua.toVector2f +import ru.dbotthepony.kstarbound.util.sbIntern import ru.dbotthepony.kstarbound.world.entities.Animator -fun provideAnimatorBindings(self: Animator, lua: LuaEnvironment) { - val callbacks = lua.newTable() - lua.globals["animator"] = callbacks - - callbacks["setAnimationState"] = luaFunction { type: ByteString, state: ByteString, alwaysStart: Boolean? -> - returnBuffer.setTo(self.setActiveState(type.decode(), state.decode(), alwaysStart ?: false)) - } - - callbacks["animationState"] = luaFunction { type: ByteString -> - returnBuffer.setTo(self.animationState(type.decode())) - } - - callbacks["animationStateProperty"] = luaFunction { type: ByteString, key: ByteString -> - returnBuffer.setTo(self.stateProperty(type.decode(), key.decode())) - } - - callbacks["setGlobalTag"] = luaFunction { key: ByteString, value: ByteString -> - self.setGlobalTag(key.decode(), value.decode()) - } - - callbacks["setPartTag"] = luaFunction { part: ByteString, key: ByteString, value: ByteString -> - self.setPartTag(part.decode(), key.decode(), value.decode()) - } - - callbacks["setFlipped"] = luaFunction { isFlipped: Boolean, centerLine: Double? -> - self.isFlipped = isFlipped - self.flippedRelativeCenterLine = centerLine ?: 0.0 - } - - callbacks["setAnimationRate"] = luaFunction { rate: Double -> - self.animationRate = rate - } - - callbacks["rotateGroup"] = luaFunction { group: ByteString, rotation: Number, immediate: Boolean? -> - self.rotateGroup(group.decode(), rotation.toDouble(), immediate ?: false) - } - - callbacks["currentRotationAngle"] = luaFunction { group: ByteString -> - returnBuffer.setTo(self.currentRotationAngle(group.decode())) - } - - callbacks["targetRotationAngle"] = luaFunction { group: ByteString -> - returnBuffer.setTo(self.targetRotationAngle(group.decode())) - } - - callbacks["hasRotationGroup"] = luaFunction { group: ByteString -> - returnBuffer.setTo(self.hasRotationGroup(group.decode())) - } - - callbacks["rotationGroups"] = luaFunction { - val groups = self.rotationGroups() - val keys = newTable(groups.size, 0) - groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v.toByteString() } - returnBuffer.setTo(keys) - } - - callbacks["hasTransformationGroup"] = luaFunction { group: ByteString -> - returnBuffer.setTo(self.hasTransformationGroup(group.decode())) - } - - callbacks["transformationGroups"] = luaFunction { - val groups = self.transformationGroups() - val keys = newTable(groups.size, 0) - groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v } - returnBuffer.setTo(keys) - } - - callbacks["translateTransformGroup"] = luaFunction { group: ByteString, translation: Table -> - self.translateTransformGroup(group.decode(), toVector2f(translation)) - } - - callbacks["rotateTransformGroup"] = luaFunction { group: ByteString, rotation: Double, center: Table? -> - self.rotateTransformGroup(group.decode(), rotation.toFloat(), if (center == null) Vector2f.ZERO else toVector2f(center)) - } - - callbacks["scaleTransformationGroup"] = luaFunction { group: ByteString, scale: Any, center: Table? -> - if (scale is Number) { - self.scaleTransformationGroup(group.decode(), Vector2f(scale.toFloat(), scale.toFloat()), if (center == null) Vector2f.ZERO else toVector2f(center)) - } else { - self.scaleTransformationGroup(group.decode(), toVector2f(scale), if (center == null) Vector2f.ZERO else toVector2f(center)) - } - } - - callbacks["transformTransformationGroup"] = luaFunctionArray { arguments: Array -> - val group = arguments[0] as ByteString - val r00 = arguments[1] as Number - val r01 = arguments[2] as Number - val r02 = arguments[3] as Number - val r10 = arguments[4] as Number - val r11 = arguments[5] as Number - val r12 = arguments[6] as Number - - self.transformTransformationGroup(group.decode(), r00.toFloat(), - r01.toFloat(), - r02.toFloat(), - r10.toFloat(), - r11.toFloat(), - r12.toFloat()) - } - - callbacks["resetTransformationGroup"] = luaFunction { group: ByteString -> - self.resetTransformationGroup(group.decode()) - } - - callbacks["particleEmitters"] = luaFunction { - val groups = self.particleEmitters() - val keys = newTable(groups.size, 0) - groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v } - returnBuffer.setTo(keys) - } - - callbacks["hasParticleEmitter"] = luaFunction { group: ByteString -> - returnBuffer.setTo(self.hasParticleEmitter(group.decode())) - } - - callbacks["setParticleEmitterActive"] = luaFunction { emitter: ByteString, state: Boolean -> - self.setParticleEmitterActive(emitter.decode(), state) - } - - callbacks["setParticleEmitterEmissionRate"] = luaFunction { emitter: ByteString, rate: Number -> - self.setParticleEmitterEmissionRate(emitter.decode(), rate.toDouble()) - } - - callbacks["setParticleEmitterBurstCount"] = luaFunction { emitter: ByteString, count: Number -> - self.setParticleEmitterBurstCount(emitter.decode(), count.toInt()) - } - - callbacks["setParticleEmitterOffsetRegion"] = luaFunction { emitter: ByteString, region: Table -> - self.setParticleEmitterOffsetRegion(emitter.decode(), toAABB(region)) - } - - callbacks["burstParticleEmitter"] = luaFunction { emitter: ByteString -> - self.burstParticleEmitter(emitter.decode()) - } - - callbacks["lights"] = luaFunction { - val groups = self.lights() - val keys = newTable(groups.size, 0) - groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v } - returnBuffer.setTo(keys) - } - - callbacks["hasLight"] = luaFunction { light: ByteString -> - returnBuffer.setTo(self.hasLight(light.decode())) - } - - callbacks["setLightActive"] = luaFunction { light: ByteString, state: Boolean -> - self.setLightActive(light.decode(), state) - } - - callbacks["setLightPosition"] = luaFunction { light: ByteString, position: Table -> - self.setLightPosition(light.decode(), toVector2d(position)) - } - - callbacks["setLightColor"] = luaFunction { light: ByteString, color: Table -> - self.setLightColor(light.decode(), toColor(color)) - } - - callbacks["setLightPointAngle"] = luaFunction { light: ByteString, angle: Number -> - self.setLightPointAngle(light.decode(), angle.toDouble()) - } - - callbacks["sounds"] = luaFunction { - val groups = self.sounds() - val keys = newTable(groups.size, 0) - groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v } - returnBuffer.setTo(keys) - } - - callbacks["hasSound"] = luaFunction { sound: ByteString -> - returnBuffer.setTo(self.hasSound(sound.decode())) - } - - callbacks["setSoundPool"] = luaFunction { sound: ByteString, sounds: Table -> - self.setSoundPool(sound.decode(), sounds.iterator().map { (it.value as ByteString).decode() }.toList()) - } - - callbacks["setSoundPosition"] = luaFunction { sound: ByteString, position: Table -> - self.setSoundPosition(sound.decode(), toVector2d(position)) - } - - callbacks["playSound"] = luaFunction { sound: ByteString, loops: Number? -> - self.playSound(sound.decode(), loops?.toInt() ?: 0) - } - - callbacks["setSoundVolume"] = luaFunction { sound: ByteString, volume: Number, rampTime: Number? -> - self.setSoundVolume(sound.decode(), volume.toDouble(), rampTime?.toDouble() ?: 0.0) - } - - callbacks["setSoundPitch"] = luaFunction { sound: ByteString, pitch: Number, rampTime: Number? -> - self.setSoundPitch(sound.decode(), pitch.toDouble(), rampTime?.toDouble() ?: 0.0) - } - - callbacks["stopAllSounds"] = luaFunction { sound: ByteString, rampTime: Number? -> - self.stopAllSounds(sound.decode(), rampTime?.toDouble() ?: 0.0) - } - - callbacks["effects"] = luaFunction { - val groups = self.effects() - val keys = newTable(groups.size, 0) - groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v } - returnBuffer.setTo(keys) - } - - callbacks["hasEffect"] = luaFunction { effect: ByteString -> - returnBuffer.setTo(self.hasEffect(effect.decode())) - } - - callbacks["setEffectActive"] = luaFunction { effect: ByteString, state: Boolean -> - self.setEffectActive(effect.decode(), state) - } - - callbacks["parts"] = luaFunction { - val groups = self.parts() - val keys = newTable(groups.size, 0) - groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v } - returnBuffer.setTo(keys) - } - - callbacks["partPoint"] = luaFunction { part: ByteString, property: ByteString -> - returnBuffer.setTo(self.partPoint(part.decode(), property.decode())?.let { from(it) }) - } - - callbacks["partPoly"] = luaFunction { part: ByteString, property: ByteString -> - returnBuffer.setTo(self.partPoly(part.decode(), property.decode())?.let { from(it) }) - } - - callbacks["partProperty"] = luaFunction { part: ByteString, property: ByteString -> - returnBuffer.setTo(from(self.partProperty(part.decode(), property.decode()))) - } - - callbacks["transformPoint"] = luaFunction { point: Table, part: ByteString -> - returnBuffer.setTo(from(toVector2d(point).times(self.partTransformation(part.decode())))) - } - - callbacks["transformPoly"] = luaFunction { poly: Table, part: ByteString -> - returnBuffer.setTo(from(toPoly(poly).transform(self.partTransformation(part.decode())))) - } +private fun setAnimationState(self: Animator, args: LuaThread.ArgStack): Int { + val type = args.nextString() + val state = args.nextString() + val alwaysStart = args.nextOptionalBoolean() == true + self.setActiveState(type, state, alwaysStart) + return 0 +} + +private fun animationState(self: Animator, args: LuaThread.ArgStack): Int { + val type = args.nextString() + args.lua.push(self.animationState(type)) + return 1 +} + +private fun animationStateProperty(self: Animator, args: LuaThread.ArgStack): Int { + val key = args.nextString() + val value = args.nextString() + args.lua.push(self.stateProperty(key, value)) + return 1 +} + +private fun setGlobalTag(self: Animator, args: LuaThread.ArgStack): Int { + val key = args.nextString().sbIntern() + val value = args.nextString().sbIntern() + self.setGlobalTag(key, value) + return 0 +} + +private fun setPartTag(self: Animator, args: LuaThread.ArgStack): Int { + val part = args.nextString().sbIntern() + val key = args.nextString().sbIntern() + val value = args.nextString().sbIntern() + self.setPartTag(part, key, value) + return 0 +} + +private fun setFlipped(self: Animator, args: LuaThread.ArgStack): Int { + self.isFlipped = args.nextBoolean() + self.flippedRelativeCenterLine = args.nextOptionalDouble() ?: 0.0 + return 0 +} + +private fun setAnimationRate(self: Animator, args: LuaThread.ArgStack): Int { + self.animationRate = args.nextDouble() + return 0 +} + +private fun rotateGroup(self: Animator, args: LuaThread.ArgStack): Int { + val group = args.nextString() + val rotation = args.nextDouble() + val immediate = args.nextOptionalBoolean() == true + self.rotateGroup(group, rotation, immediate) + return 0 +} + +private fun currentRotationAngle(self: Animator, args: LuaThread.ArgStack): Int { + val group = args.nextString() + args.lua.push(self.currentRotationAngle(group)) + return 1 +} + +private fun targetRotationAngle(self: Animator, args: LuaThread.ArgStack): Int { + args.lua.push(self.targetRotationAngle(args.nextString())) + return 1 +} + +private fun has(self: Animator, getter: (Animator, String) -> Boolean, args: LuaThread.ArgStack): Int { + args.lua.push(getter(self, args.nextString())) + return 1 +} + +private fun pushNames(self: Animator, getter: (Animator) -> Collection, args: LuaThread.ArgStack): Int { + val values = getter(self) + args.lua.pushTable(values.size) + + for ((i, v) in values.withIndex()) + args.lua.setTableValue(i + 1L, v) + + return 1 +} + +private fun translateTransformGroup(self: Animator, args: LuaThread.ArgStack): Int { + val group = args.nextString() + val translation = args.nextVector2f() + self.translateTransformGroup(group, translation) + return 0 +} + +private fun rotateTransformGroup(self: Animator, args: LuaThread.ArgStack): Int { + val group = args.nextString() + val rotation = args.nextFloat() + val center = args.nextOptionalVector2f() ?: Vector2f.ZERO + self.rotateTransformGroup(group, rotation, center) + return 0 +} + +private fun scaleTransformationGroup(self: Animator, args: LuaThread.ArgStack): Int { + val group = args.nextString() + + if (args.peek() == LuaType.NUMBER) { + val value = args.nextFloat() + self.scaleTransformationGroup(group, Vector2f(value, value), args.nextOptionalVector2f() ?: Vector2f.ZERO) + } else { + self.scaleTransformationGroup(group, args.nextVector2f(), args.nextOptionalVector2f() ?: Vector2f.ZERO) + } + + return 0 +} + +private fun transformTransformationGroup(self: Animator, args: LuaThread.ArgStack): Int { + val group = args.nextString() + val r00 = args.nextFloat() + val r01 = args.nextFloat() + val r02 = args.nextFloat() + val r10 = args.nextFloat() + val r11 = args.nextFloat() + val r12 = args.nextFloat() + + self.transformTransformationGroup( + group, + r00, + r01, + r02, + r10, + r11, + r12) + + return 0 +} + +private fun resetTransformationGroup(self: Animator, args: LuaThread.ArgStack): Int { + val group = args.nextString() + self.resetTransformationGroup(group) + return 0 +} + +private fun setParticleEmitterActive(self: Animator, args: LuaThread.ArgStack): Int { + self.setParticleEmitterActive(args.nextString(), args.nextBoolean()) + return 0 +} + +private fun setParticleEmitterEmissionRate(self: Animator, args: LuaThread.ArgStack): Int { + self.setParticleEmitterEmissionRate(args.nextString(), args.nextDouble()) + return 0 +} + +private fun setParticleEmitterBurstCount(self: Animator, args: LuaThread.ArgStack): Int { + self.setParticleEmitterBurstCount(args.nextString(), args.nextInt()) + return 0 +} + +private fun setParticleEmitterOffsetRegion(self: Animator, args: LuaThread.ArgStack): Int { + self.setParticleEmitterOffsetRegion(args.nextString(), args.nextAABB()) + return 0 +} + +private fun burstParticleEmitter(self: Animator, args: LuaThread.ArgStack): Int { + self.burstParticleEmitter(args.nextString()) + return 0 +} + +private fun setLightActive(self: Animator, args: LuaThread.ArgStack): Int { + self.setLightActive(args.nextString(), args.nextBoolean()) + return 0 +} + +private fun setLightPosition(self: Animator, args: LuaThread.ArgStack): Int { + self.setLightPosition(args.nextString(), args.nextVector2d()) + return 0 +} + +private fun setLightColor(self: Animator, args: LuaThread.ArgStack): Int { + self.setLightColor(args.nextString(), args.nextColor()) + return 0 +} + +private fun setLightPointAngle(self: Animator, args: LuaThread.ArgStack): Int { + self.setLightPointAngle(args.nextString(), args.nextDouble()) + return 0 +} + +private fun setSoundPool(self: Animator, args: LuaThread.ArgStack): Int { + val pool = args.nextString() + val sounds = args.readTableValues { getString(it)?.sbIntern() ?: throw IllegalArgumentException("invalid values in sound pool table") } + self.setSoundPool(pool, sounds) + return 0 +} + +private fun setSoundPosition(self: Animator, args: LuaThread.ArgStack): Int { + self.setSoundPosition(args.nextString(), args.nextVector2d()) + return 0 +} + +private fun playSound(self: Animator, args: LuaThread.ArgStack): Int { + self.playSound(args.nextString(), args.nextOptionalInt() ?: 0) + return 0 +} + +private fun setSoundVolume(self: Animator, args: LuaThread.ArgStack): Int { + self.setSoundVolume(args.nextString(), args.nextDouble(), args.nextOptionalDouble() ?: 0.0) + return 0 +} + +private fun setSoundPitch(self: Animator, args: LuaThread.ArgStack): Int { + self.setSoundPitch(args.nextString(), args.nextDouble(), args.nextOptionalDouble() ?: 0.0) + return 0 +} + +private fun stopAllSounds(self: Animator, args: LuaThread.ArgStack): Int { + self.stopAllSounds(args.nextString(), args.nextOptionalDouble() ?: 0.0) + return 0 +} + +private fun setEffectActive(self: Animator, args: LuaThread.ArgStack): Int { + self.setEffectActive(args.nextString(), args.nextBoolean()) + return 0 +} + +private fun partPoint(self: Animator, args: LuaThread.ArgStack): Int { + args.lua.push(self.partPoint(args.nextString(), args.nextString())) + return 1 +} + +private fun partPoly(self: Animator, args: LuaThread.ArgStack): Int { + args.lua.push(self.partPoly(args.nextString(), args.nextString())) + return 1 +} + +private fun partProperty(self: Animator, args: LuaThread.ArgStack): Int { + args.lua.push(self.partProperty(args.nextString(), args.nextString())) + return 1 +} + +private fun transformPoint(self: Animator, args: LuaThread.ArgStack): Int { + args.lua.push(args.nextVector2d() * self.partTransformation(args.nextString())) + return 1 +} + +private fun transformPoly(self: Animator, args: LuaThread.ArgStack): Int { + args.lua.push(args.nextPoly().transform(self.partTransformation(args.nextString()))) + return 1 +} + +fun provideAnimatorBindings(self: Animator, lua: LuaThread) { + lua.pushTable() + lua.dup() + lua.storeGlobal("animator") + + lua.pushBinding(self, "setAnimationState", ::setAnimationState) + lua.pushBinding(self, "animationState", ::animationState) + lua.pushBinding(self, "animationStateProperty", ::animationStateProperty) + lua.pushBinding(self, "setGlobalTag", ::setGlobalTag) + lua.pushBinding(self, "setPartTag", ::setPartTag) + lua.pushBinding(self, "setFlipped", ::setFlipped) + lua.pushBinding(self, "setAnimationRate", ::setAnimationRate) + lua.pushBinding(self, "rotateGroup", ::rotateGroup) + lua.pushBinding(self, "currentRotationAngle", ::currentRotationAngle) + lua.pushBinding(self, "targetRotationAngle", ::targetRotationAngle) + lua.pushBinding(self, "translateTransformGroup", ::translateTransformGroup) + lua.pushBinding(self, "rotateTransformGroup", ::rotateTransformGroup) + lua.pushBinding(self, "scaleTransformationGroup", ::scaleTransformationGroup) + lua.pushBinding(self, "transformTransformationGroup", ::transformTransformationGroup) + lua.pushBinding(self, "resetTransformationGroup", ::resetTransformationGroup) + lua.pushBinding(self, "setParticleEmitterActive", ::setParticleEmitterActive) + lua.pushBinding(self, "setParticleEmitterEmissionRate", ::setParticleEmitterEmissionRate) + lua.pushBinding(self, "setParticleEmitterBurstCount", ::setParticleEmitterBurstCount) + lua.pushBinding(self, "setParticleEmitterOffsetRegion", ::setParticleEmitterOffsetRegion) + lua.pushBinding(self, "burstParticleEmitter", ::burstParticleEmitter) + lua.pushBinding(self, "setLightActive", ::setLightActive) + lua.pushBinding(self, "setLightPosition", ::setLightPosition) + lua.pushBinding(self, "setLightColor", ::setLightColor) + lua.pushBinding(self, "setLightPointAngle", ::setLightPointAngle) + lua.pushBinding(self, "setSoundPool", ::setSoundPool) + lua.pushBinding(self, "setSoundPosition", ::setSoundPosition) + lua.pushBinding(self, "playSound", ::playSound) + lua.pushBinding(self, "setSoundVolume", ::setSoundVolume) + lua.pushBinding(self, "setSoundPitch", ::setSoundPitch) + lua.pushBinding(self, "stopAllSounds", ::stopAllSounds) + lua.pushBinding(self, "setEffectActive", ::setEffectActive) + lua.pushBinding(self, "partPoint", ::partPoint) + lua.pushBinding(self, "partPoly", ::partPoly) + lua.pushBinding(self, "partProperty", ::partProperty) + lua.pushBinding(self, "transformPoint", ::transformPoint) + lua.pushBinding(self, "transformPoly", ::transformPoly) + + lua.pushBinding(self, Animator::hasRotationGroup, "hasRotationGroup", ::has) + lua.pushBinding(self, Animator::hasTransformationGroup, "hasTransformationGroup", ::has) + lua.pushBinding(self, Animator::hasParticleEmitter, "hasParticleEmitter", ::has) + lua.pushBinding(self, Animator::hasLight, "hasLight", ::has) + lua.pushBinding(self, Animator::hasSound, "hasSound", ::has) + lua.pushBinding(self, Animator::hasEffect, "hasEffect", ::has) + lua.pushBinding(self, Animator::hasPart, "hasPart", ::has) + + lua.pushBinding(self, Animator::rotationGroups, "rotationGroups", ::pushNames) + lua.pushBinding(self, Animator::transformationGroups, "transformationGroups", ::pushNames) + lua.pushBinding(self, Animator::particleEmitters, "particleEmitters", ::pushNames) + lua.pushBinding(self, Animator::lights, "lights", ::pushNames) + lua.pushBinding(self, Animator::sounds, "sounds", ::pushNames) + lua.pushBinding(self, Animator::effects, "effects", ::pushNames) + lua.pushBinding(self, Animator::parts, "parts", ::pushNames) + + lua.pop() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/CommonBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/CommonBindings.kt new file mode 100644 index 00000000..198200b4 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/CommonBindings.kt @@ -0,0 +1,41 @@ +package ru.dbotthepony.kstarbound.lua.bindings + +import com.google.common.collect.ImmutableList +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor +import ru.dbotthepony.kstarbound.defs.quest.QuestDescriptor +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.lua.LuaType +import ru.dbotthepony.kstarbound.util.sbIntern + +fun setOfferedQuests(self: T, prop: (T) -> MutableList, args: LuaThread.ArgStack): Int { + val offeredQuests = prop(self) + offeredQuests.clear() + + if (args.hasNext) { + offeredQuests.addAll(args.readTableValues { + val peek = typeAt(it) + + if (peek == LuaType.TABLE) { + Starbound.gson.fromJson(getJson(it)!!, QuestArcDescriptor::class.java) + } else if (peek == LuaType.STRING) { + QuestArcDescriptor(ImmutableList.of(QuestDescriptor(getString(it)!!.sbIntern()))) + } else { + throw IllegalArgumentException("Invalid value in provided quests table: $peek") + } + }) + } + + return 0 +} + +fun setTurnInQuests(self: T, prop: (T) -> MutableList, args: LuaThread.ArgStack): Int { + val turnInQuests = prop(self) + turnInQuests.clear() + + if (args.hasNext) { + turnInQuests.addAll(args.readTableValues { getString(it)?.sbIntern() ?: throw IllegalArgumentException("Turn-in quests table contains non-string values") }) + } + + return 0 +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt index 0784f860..47bd1240 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt @@ -1,13 +1,9 @@ package ru.dbotthepony.kstarbound.lua.bindings -import ru.dbotthepony.kstarbound.math.vector.Vector2d -import ru.dbotthepony.kstarbound.lua.LuaEnvironment -import ru.dbotthepony.kstarbound.lua.from -import ru.dbotthepony.kstarbound.lua.get -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.toByteString +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.lua.push import ru.dbotthepony.kstarbound.math.Line2d +import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.ActorEntity import ru.dbotthepony.kstarbound.world.entities.MonsterEntity @@ -15,7 +11,84 @@ import ru.dbotthepony.kstarbound.world.entities.NPCEntity import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject -fun provideEntityBindings(self: AbstractEntity, lua: LuaEnvironment) { +private fun id(self: AbstractEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.entityID.toLong()) + return 1 +} + +private fun position(self: AbstractEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.position) + return 1 +} + +private fun entityType(self: AbstractEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.type.jsonName) + return 1 +} + +private fun uniqueId(self: AbstractEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.uniqueID.get()) + return 1 +} + +private fun persistent(self: AbstractEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.isPersistent) + return 1 +} + +private fun entityInSight(self: AbstractEntity, args: LuaThread.ArgStack): Int { + val target = self.world.entities[args.nextInt()] + + if (target == null) { + args.lua.push(false) + } else { + args.lua.push(self.world.chunkMap.collide(Line2d(self.position, target.position)) { it.type.isSolidCollision } == null) + } + + return 1 +} + +private fun isValidTarget(self: AbstractEntity, args: LuaThread.ArgStack): Int { + val target = self.world.entities[args.nextInt()] + + if (target == null) { + args.lua.push(false) + } else { + if (!self.team.get().canDamage(target.team.get(), self == target)) // original engine always passes `false` to `isSelfDamage` + args.lua.push(false) + else if (target is MonsterEntity) + args.lua.push(target.isAggressive) + else + args.lua.push(target is PlayerEntity) + + // TODO: NPC handling here + } + + return 1 +} + +private fun damageTeam(self: AbstractEntity, args: LuaThread.ArgStack): Int { + args.lua.pushTable(hashSize = 2) + args.lua.setTableValue("team", self.team.get().team) + args.lua.setTableValue("type", self.team.get().type.jsonName) + return 1 +} + +private fun distanceToEntity(self: AbstractEntity, args: LuaThread.ArgStack): Int { + val target = self.world.entities[args.nextInt()] + + if (target != null) { + args.lua.push(self.world.geometry.diff(target.position, self.position)) + } else { + args.lua.push(Vector2d.ZERO) + } + + return 1 +} + +fun provideEntityBindings(self: AbstractEntity, lua: LuaThread) { + provideWorldBindings(self.world, lua) + if (self is WorldObject) provideWorldObjectBindings(self, lua) @@ -28,52 +101,19 @@ fun provideEntityBindings(self: AbstractEntity, lua: LuaEnvironment) { if (self is NPCEntity) provideNPCBindings(self, lua) - //provideWorldBindings(self.world, lua) + lua.pushTable() + lua.dup() + lua.storeGlobal("entity") - val table = lua.newTable() - lua.globals["entity"] = table + lua.pushBinding(self, "id", ::id) + lua.pushBinding(self, "position", ::position) + lua.pushBinding(self, "entityType", ::entityType) + lua.pushBinding(self, "uniqueId", ::uniqueId) + lua.pushBinding(self, "persistent", ::persistent) + lua.pushBinding(self, "entityInSight", ::entityInSight) + lua.pushBinding(self, "isValidTarget", ::isValidTarget) + lua.pushBinding(self, "damageTeam", ::damageTeam) + lua.pushBinding(self, "distanceToEntity", ::distanceToEntity) - table["id"] = luaFunction { returnBuffer.setTo(self.entityID.toLong()) } - table["position"] = luaFunction { returnBuffer.setTo(from(self.position)) } - table["entityType"] = luaFunction { returnBuffer.setTo(self.type.jsonName.toByteString()) } - table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get().toByteString()) } - table["persistent"] = luaFunction { returnBuffer.setTo(self.isPersistent) } - - table["entityInSight"] = luaFunction { target: Number -> - val entity = self.world.entities[target.toInt()] ?: return@luaFunction returnBuffer.setTo(false) - returnBuffer.setTo(self.world.chunkMap.collide(Line2d(self.position, entity.position)) { it.type.isSolidCollision } == null) - } - - table["isValidTarget"] = luaFunction { target: Number -> - val entity = self.world.entities[target.toInt()] ?: return@luaFunction returnBuffer.setTo(false) - - if (!self.team.get().canDamage(entity.team.get(), self == entity)) // original engine always passes `false` to `isSelfDamage` - return@luaFunction returnBuffer.setTo(false) - - if (entity is MonsterEntity) - return@luaFunction returnBuffer.setTo(entity.isAggressive) - - // TODO: NPC handling here - - returnBuffer.setTo(entity is PlayerEntity) - } - - table["damageTeam"] = luaFunction { - val result = newTable() - - result["team"] = self.team.get().team.toLong() - result["type"] = self.type.jsonName.toByteString() - - returnBuffer.setTo(result) - } - - table["distanceToEntity"] = luaFunction { entity: Number -> - val find = self.world.entities[entity.toInt()] - - if (find != null) { - returnBuffer.setTo(from(self.world.geometry.diff(find.position, self.position))) - } else { - returnBuffer.setTo(from(Vector2d.ZERO)) - } - } + lua.pop() } 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 d15cd009..e050dbe8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt @@ -1,186 +1,227 @@ package ru.dbotthepony.kstarbound.lua.bindings +import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableSet -import org.classdump.luna.ByteString -import org.classdump.luna.Table -import ru.dbotthepony.kommons.collect.collect -import ru.dbotthepony.kommons.collect.map +import com.google.gson.JsonNull +import com.google.gson.reflect.TypeToken +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.DamageSource import ru.dbotthepony.kstarbound.defs.EntityDamageTeam import ru.dbotthepony.kstarbound.defs.PhysicsForceRegion +import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.fromJsonFast -import ru.dbotthepony.kstarbound.lua.LuaEnvironment -import ru.dbotthepony.kstarbound.lua.from -import ru.dbotthepony.kstarbound.lua.get -import ru.dbotthepony.kstarbound.lua.iterator -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.stream -import ru.dbotthepony.kstarbound.lua.toJson -import ru.dbotthepony.kstarbound.lua.toJsonFromLua -import ru.dbotthepony.kstarbound.lua.toVector2d +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.lua.nextVector2d +import ru.dbotthepony.kstarbound.lua.push import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.sbIntern import ru.dbotthepony.kstarbound.util.valueOf import ru.dbotthepony.kstarbound.world.entities.ActorEntity import ru.dbotthepony.kstarbound.world.entities.MonsterEntity -fun provideMonsterBindings(self: MonsterEntity, lua: LuaEnvironment) { - val callbacks = lua.newTable() - lua.globals["monster"] = callbacks +private object DropPoolToken : TypeToken>, Registry.Ref>>() - callbacks["type"] = luaFunction { - returnBuffer.setTo(self.variant.type) - } - - callbacks["seed"] = luaFunction { - // what the fuck. - returnBuffer.setTo(self.variant.seed.toString()) - } - - callbacks["seedNumber"] = luaFunction { - returnBuffer.setTo(self.variant.seed) - } - - callbacks["uniqueParameters"] = luaFunction { - returnBuffer.setTo(from(self.variant.uniqueParameters)) - } - - callbacks["level"] = luaFunction { - // TODO: this makes half sense - returnBuffer.setTo(self.monsterLevel ?: 0.0) - } - - callbacks["setDamageOnTouch"] = luaFunction { damage: Boolean -> - self.damageOnTouch = damage - } - - callbacks["setDamageSources"] = luaFunction { sources: Table? -> - self.customDamageSources.clear() - - if (sources != null) { - for ((_, v) in sources) { - self.customDamageSources.add(Starbound.gson.fromJson((v as Table).toJson(), DamageSource::class.java)) - } - } - } - - callbacks["setDamageParts"] = luaFunction { parts: Table? -> - if (parts == null) { - self.animationDamageParts.clear() - } else { - val strings = parts.stream().map { (_, v) -> v.toString() }.collect(ImmutableSet.toImmutableSet()) - self.animationDamageParts.removeIf { it !in strings } - - for (v in strings) { - if (v !in self.animationDamageParts) { - self.animationDamageParts.add(v) - } - } - } - } - - callbacks["setAggressive"] = luaFunction { isAggressive: Boolean -> - self.isAggressive = isAggressive - } - - callbacks["setActiveSkillName"] = luaFunction { name: ByteString -> - self.activeSkillName = name.decode().sbIntern() - } - - callbacks["setDropPool"] = luaFunction { pool: Any -> - self.dropPool = Starbound.gson.fromJsonFast(toJsonFromLua(pool)) - } - - callbacks["toAbsolutePosition"] = luaFunction { position: Table -> - returnBuffer.setTo(from(self.movement.getAbsolutePosition(toVector2d(position)))) - } - - callbacks["mouthPosition"] = luaFunction { - returnBuffer.setTo(from(self.mouthPosition)) - } - - // This callback is registered here rather than in - // makeActorMovementControllerCallbacks - // because it requires access to world - callbacks["flyTo"] = luaFunction { position: Table -> - self.movement.controlFly = self.world.geometry.diff(toVector2d(position), self.movement.position) - } - - callbacks["setDeathParticleBurst"] = luaFunction { value: ByteString? -> - self.deathParticlesBurst = value?.decode()?.sbIntern() ?: "" - } - - callbacks["setDeathSound"] = luaFunction { value: ByteString? -> - self.deathSound = value?.decode()?.sbIntern() ?: "" - } - - callbacks["setPhysicsForces"] = luaFunction { forces: Table? -> - if (forces == null) { - self.forceRegions.clear() - } else { - self.forceRegions.clear() - - for ((_, v) in forces) { - self.forceRegions.add(Starbound.gson.fromJsonFast(toJsonFromLua(v), PhysicsForceRegion::class.java)) - } - } - } - - callbacks["setName"] = luaFunction { name: ByteString? -> - self.networkName = name?.decode() - } - - callbacks["setDisplayNametag"] = luaFunction { shouldDisplay: Boolean -> - self.displayNameTag = shouldDisplay - } - - callbacks["say"] = luaFunction { line: ByteString, tags: Table? -> - var actualLine = line.decode() - - if (tags != null) { - actualLine = SBPattern.of(actualLine).resolveOrSkip({ tags[it]?.toString() }) - } - - if (actualLine.isNotBlank()) { - self.addChatMessage(actualLine.sbIntern()) - } - - returnBuffer.setTo(actualLine.isNotBlank()) - } - - callbacks["sayPortrait"] = luaFunction { line: ByteString, portrait: ByteString, tags: Table? -> - var actualLine = line.decode() - - if (tags != null) { - actualLine = SBPattern.of(actualLine).resolveOrSkip({ tags[it]?.toString() }) - } - - if (actualLine.isNotBlank()) { - self.addChatMessage(actualLine.sbIntern(), portrait.decode().sbIntern()) - } - - returnBuffer.setTo(actualLine.isNotBlank()) - } - - callbacks["setDamageTeam"] = luaFunction { team: Any -> - self.team.accept(Starbound.gson.fromJsonFast(toJsonFromLua(team), EntityDamageTeam::class.java)) - } - - callbacks["setUniqueId"] = luaFunction { name: ByteString? -> - self.uniqueID.accept(name?.decode()?.sbIntern()) - } - - callbacks["setDamageBar"] = luaFunction { type: ByteString -> - self.damageBarType = ActorEntity.DamageBarType.entries.valueOf(type.decode()) - } - - callbacks["setInteractive"] = luaFunction { isInteractive: Boolean -> - self.isInteractive = isInteractive - } - - callbacks["setAnimationParameter"] = luaFunction { name: ByteString, value: Any? -> - self.scriptedAnimationParameters[name.decode().sbIntern()] = toJsonFromLua(value) - } +private fun type(self: MonsterEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.variant.type) + return 1 +} + +private fun seed(self: MonsterEntity, args: LuaThread.ArgStack): Int { + // FIXME: what the fuck. + args.lua.push(self.variant.seed.toString()) + return 1 +} + +private fun seedNumber(self: MonsterEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.variant.seed) + return 1 +} + +private fun uniqueParameters(self: MonsterEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.variant.uniqueParameters) + return 1 +} + +private fun level(self: MonsterEntity, args: LuaThread.ArgStack): Int { + // FIXME: this makes half sense + args.lua.push(self.monsterLevel ?: 0.0) + return 1 +} + +private fun setDamageOnTouch(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.damageOnTouch = args.nextBoolean() + return 0 +} + +private fun setDamageSources(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.customDamageSources.clear() + + if (args.isNotEmpty) { + val sources = args.readTableValues { Starbound.gson.fromJsonFast(getJson(it)!!, DamageSource::class.java) } + self.customDamageSources.addAll(sources) + } + + return 0 +} + +private fun setDamageParts(self: MonsterEntity, args: LuaThread.ArgStack): Int { + if (args.isEmpty) { + self.animationDamageParts.clear() + } else { + val strings = ImmutableSet.copyOf(args.readTableValues { getString(it) ?: throw IllegalArgumentException("Invalid values in damage parts table") }) + self.animationDamageParts.removeIf { it !in strings } + + for (v in strings) { + if (v !in self.animationDamageParts) { + self.animationDamageParts.add(v) + } + } + } + + return 0 +} + +private fun setAggressive(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.isAggressive = args.nextBoolean() + return 0 +} + +private fun setActiveSkillName(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.activeSkillName = args.nextString().sbIntern() + return 0 +} + +private fun setDropPool(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.dropPool = Starbound.gson.fromJsonFast(args.nextOptionalJson() ?: JsonNull.INSTANCE, DropPoolToken) + return 0 +} + +private fun toAbsolutePosition(self: MonsterEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.movement.getAbsolutePosition(args.nextVector2d())) + return 0 +} + +private fun mouthPosition(self: MonsterEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.mouthPosition) + return 0 +} + +// This callback is registered here rather than in +// makeActorMovementControllerCallbacks +// because it requires access to world +private fun flyTo(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.movement.controlFly = self.world.geometry.diff(args.nextVector2d(), self.movement.position) + return 0 +} + +private fun setDeathParticleBurst(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.deathParticlesBurst = args.nextOptionalString()?.sbIntern() ?: "" + return 0 +} + +private fun setDeathSound(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.deathSound = args.nextOptionalString()?.sbIntern() ?: "" + return 0 +} + +private fun setPhysicsForces(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.forceRegions.clear() + + if (args.isNotEmpty) { + val forces = args.readTableValues { Starbound.gson.fromJsonFast(getJson(it)!!, PhysicsForceRegion::class.java) } + self.forceRegions.addAll(forces) + } + + return 0 +} + +private fun setName(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.networkName = args.nextOptionalString()?.sbIntern() ?: "" + return 0 +} + +private fun setDisplayNametag(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.displayNameTag = args.nextBoolean() + return 0 +} + +private fun say(self: MonsterEntity, hasPortrait: Boolean, args: LuaThread.ArgStack): Int { + var line = args.nextString() + val portrait = if (hasPortrait) args.nextString() else null + + if (args.hasNext) { + val tags = args.readTable(keyVisitor = { getString(it) ?: throw IllegalArgumentException("Pattern tag replacement table contains non-string keys") }, valueVisitor = { getString(it) ?: throw IllegalArgumentException("Pattern tag replacement table contains non-string values") }) + line = SBPattern.of(line).resolveOrSkip({ t -> tags.firstOrNull { it.first == t }?.second }) + } + + if (line.isNotBlank()) { + self.addChatMessage(line.sbIntern(), portrait = portrait) + args.lua.push(true) + } else { + args.lua.push(false) + } + + return 1 +} + +private fun setDamageTeam(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.team.accept(Starbound.gson.fromJsonFast(args.nextJson(), EntityDamageTeam::class.java)) + return 0 +} + +private fun setUniqueId(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.uniqueID.accept(args.nextOptionalString()?.sbIntern()) + return 0 +} + +private fun setDamageBar(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.damageBarType = ActorEntity.DamageBarType.entries.valueOf(args.nextString()) + return 0 +} + +private fun setInteractive(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.isInteractive = args.nextBoolean() + return 0 +} + +private fun setAnimationParameter(self: MonsterEntity, args: LuaThread.ArgStack): Int { + self.scriptedAnimationParameters[args.nextString().sbIntern()] = args.nextJson() + return 0 +} + +fun provideMonsterBindings(self: MonsterEntity, lua: LuaThread) { + lua.pushTable() + lua.dup() + lua.storeGlobal("monster") + + lua.pushBinding(self, "type", ::type) + lua.pushBinding(self, "seed", ::seed) + lua.pushBinding(self, "seedNumber", ::seedNumber) + lua.pushBinding(self, "uniqueParameters", ::uniqueParameters) + lua.pushBinding(self, "level", ::level) + lua.pushBinding(self, "setDamageOnTouch", ::setDamageOnTouch) + lua.pushBinding(self, "setDamageSources", ::setDamageSources) + lua.pushBinding(self, "setDamageParts", ::setDamageParts) + lua.pushBinding(self, "setAggressive", ::setAggressive) + lua.pushBinding(self, "setActiveSkillName", ::setActiveSkillName) + lua.pushBinding(self, "setDropPool", ::setDropPool) + lua.pushBinding(self, "toAbsolutePosition", ::toAbsolutePosition) + lua.pushBinding(self, "mouthPosition", ::mouthPosition) + lua.pushBinding(self, "flyTo", ::flyTo) + lua.pushBinding(self, "setDeathParticleBurst", ::setDeathParticleBurst) + lua.pushBinding(self, "setDeathSound", ::setDeathSound) + lua.pushBinding(self, "setPhysicsForces", ::setPhysicsForces) + lua.pushBinding(self, "setName", ::setName) + lua.pushBinding(self, "setDisplayNametag", ::setDisplayNametag) + lua.pushBinding(self, false, "say", ::say) + lua.pushBinding(self, true, "sayPortrait", ::say) + lua.pushBinding(self, "setDamageTeam", ::setDamageTeam) + lua.pushBinding(self, "setUniqueId", ::setUniqueId) + lua.pushBinding(self, "setDamageBar", ::setDamageBar) + lua.pushBinding(self, "setInteractive", ::setInteractive) + lua.pushBinding(self, "setAnimationParameter", ::setAnimationParameter) + + lua.pop() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MovementControllerBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MovementControllerBindings.kt index f4fb5e1b..e000d5dd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MovementControllerBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MovementControllerBindings.kt @@ -1,20 +1,14 @@ package ru.dbotthepony.kstarbound.lua.bindings -import org.classdump.luna.Table +import com.google.gson.JsonNull import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.defs.actor.ActorMovementModifiers import ru.dbotthepony.kstarbound.defs.ActorMovementParameters +import ru.dbotthepony.kstarbound.defs.actor.ActorMovementModifiers import ru.dbotthepony.kstarbound.fromJsonFast -import ru.dbotthepony.kstarbound.lua.LuaEnvironment -import ru.dbotthepony.kstarbound.lua.from -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.tableFrom -import ru.dbotthepony.kstarbound.lua.tableOf -import ru.dbotthepony.kstarbound.lua.toByteString -import ru.dbotthepony.kstarbound.lua.toJson -import ru.dbotthepony.kstarbound.lua.toJsonFromLua -import ru.dbotthepony.kstarbound.lua.toVector2d +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.lua.nextVector2d +import ru.dbotthepony.kstarbound.lua.push +import ru.dbotthepony.kstarbound.lua.setTableValue import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.entities.ActorMovementController @@ -23,230 +17,369 @@ import ru.dbotthepony.kstarbound.world.physics.Poly import kotlin.math.PI class MovementControllerBindings(val self: ActorMovementController) { - fun init(lua: LuaEnvironment) { - val callbacks = lua.newTable() - lua.globals["mcontroller"] = callbacks + private fun mass(args: LuaThread.ArgStack): Int { + args.lua.push(self.mass) + return 1 + } - // pass-through - callbacks["mass"] = luaFunction { - returnBuffer.setTo(self.mass) + private fun localBoundBox(args: LuaThread.ArgStack): Int { + args.lua.push(self.computeLocalCollisionAABB()) + return 1 + } + + // TODO + private fun boundBox(args: LuaThread.ArgStack): Int { + args.lua.push(self.computeLocalCollisionAABB()) + return 1 + } + + private fun collisionBoundBox(args: LuaThread.ArgStack): Int { + args.lua.push(self.computeGlobalCollisionAABB()) + return 1 + } + + private fun collisionPoly(args: LuaThread.ArgStack): Int { + args.lua.push(self.movementParameters.collisionPoly?.map({ it }, { it.firstOrNull() }) ?: Poly.EMPTY) + return 1 + } + + private fun collisionPolies(args: LuaThread.ArgStack): Int { + val result = self.movementParameters.collisionPoly?.map({ listOf(it) }, { it }) ?: listOf(Poly.EMPTY) + args.lua.pushTable(result.size) + + for ((i, v) in result.withIndex()) + args.lua.setTableValue(i + 1L, v) + + return 1 + } + + private fun collisionBody(args: LuaThread.ArgStack): Int { + args.lua.push(self.computeGlobalHitboxes().firstOrNull() ?: Poly.EMPTY) + return 1 + } + + + private fun collisionBodies(args: LuaThread.ArgStack): Int { + val result = self.computeGlobalHitboxes() + args.lua.pushTable(result.size) + + for ((i, v) in result.withIndex()) + args.lua.setTableValue(i + 1L, v) + + return 1 + } + + private fun position(args: LuaThread.ArgStack): Int { args.lua.push(self.position); return 1 } + private fun xPosition(args: LuaThread.ArgStack): Int { args.lua.push(self.xPosition); return 1 } + private fun yPosition(args: LuaThread.ArgStack): Int { args.lua.push(self.yPosition); return 1 } + private fun velocity(args: LuaThread.ArgStack): Int { args.lua.push(self.velocity); return 1 } + private fun xVelocity(args: LuaThread.ArgStack): Int { args.lua.push(self.xVelocity); return 1 } + private fun yVelocity(args: LuaThread.ArgStack): Int { args.lua.push(self.yVelocity); return 1 } + private fun rotation(args: LuaThread.ArgStack): Int { args.lua.push(self.rotation); return 1 } + private fun isColliding(args: LuaThread.ArgStack): Int { args.lua.push(self.isColliding); return 1 } + private fun isNullColliding(args: LuaThread.ArgStack): Int { args.lua.push(self.isCollidingWithNull); return 1 } + private fun isCollisionStuck(args: LuaThread.ArgStack): Int { args.lua.push(self.isCollisionStuck); return 1 } + private fun stickingDirection(args: LuaThread.ArgStack): Int { args.lua.push(self.stickingDirection); return 1 } + private fun liquidPercentage(args: LuaThread.ArgStack): Int { args.lua.push(self.liquidPercentage); return 1 } + private fun liquidId(args: LuaThread.ArgStack): Int { args.lua.push(self.liquid?.id?.toLong() ?: 0L); return 1 } + private fun liquidName(args: LuaThread.ArgStack): Int { args.lua.push(self.liquid?.key); return 1 } + private fun onGround(args: LuaThread.ArgStack): Int { args.lua.push(self.isOnGround); return 1 } + private fun zeroG(args: LuaThread.ArgStack): Int { args.lua.push(self.isZeroGravity); return 1 } + + private fun atWorldLimit(args: LuaThread.ArgStack): Int { + args.lua.push(self.isAtWorldLimit(args.nextBoolean())) + return 1 + } + + private fun setAnchorState(args: LuaThread.ArgStack): Int { + self.anchorNetworkState = AnchorNetworkState(args.nextInt(), args.nextInt()) + return 0 + } + + private fun resetAnchorState(args: LuaThread.ArgStack): Int { + self.anchorNetworkState = null + return 0 + } + + private fun anchorState(args: LuaThread.ArgStack): Int { + val anchorState = self.anchorNetworkState + + if (anchorState != null) { + args.lua.push(anchorState.entityID.toLong()) + args.lua.push(anchorState.positionIndex.toLong()) + return 2 + } else { + return 0 + } + } + + private fun setPosition(args: LuaThread.ArgStack): Int { + resetPathMove = true + self.position = args.nextVector2d() + return 0 + } + + private fun translate(args: LuaThread.ArgStack): Int { + resetPathMove = true + self.position += args.nextVector2d() + return 0 + } + + private fun setXPosition(args: LuaThread.ArgStack): Int { + resetPathMove = true + self.xPosition = args.nextDouble() + return 0 + } + + private fun setYPosition(args: LuaThread.ArgStack): Int { + resetPathMove = true + self.yPosition = args.nextDouble() + return 0 + } + + private fun setVelocity(args: LuaThread.ArgStack): Int { + resetPathMove = true + self.velocity = args.nextVector2d() + return 0 + } + + private fun setXVelocity(args: LuaThread.ArgStack): Int { + resetPathMove = true + self.xVelocity = args.nextDouble() + return 0 + } + + private fun setYVelocity(args: LuaThread.ArgStack): Int { + resetPathMove = true + self.yVelocity = args.nextDouble() + return 0 + } + + private fun addMomentum(args: LuaThread.ArgStack): Int { + resetPathMove = true + + if (self.mass != 0.0) // let's not collapse into black hole + self.velocity += args.nextVector2d() / self.mass + + return 0 + } + + private fun setRotation(args: LuaThread.ArgStack): Int { + resetPathMove = true + self.rotation = args.nextDouble() + return 0 + } + + private fun baseParameters(args: LuaThread.ArgStack): Int { args.lua.push(Starbound.gson.toJsonTree(self.actorMovementParameters)); return 1 } + private fun walking(args: LuaThread.ArgStack): Int { args.lua.push(self.isWalking); return 1 } + private fun running(args: LuaThread.ArgStack): Int { args.lua.push(self.isRunning); return 1 } + private fun movingDirection(args: LuaThread.ArgStack): Int { args.lua.push(self.movingDirection.numericalValue); return 1 } + private fun facingDirection(args: LuaThread.ArgStack): Int { args.lua.push(self.facingDirection.numericalValue); return 1 } + private fun crouching(args: LuaThread.ArgStack): Int { args.lua.push(self.isCrouching); return 1 } + private fun flying(args: LuaThread.ArgStack): Int { args.lua.push(self.isFlying); return 1 } + private fun falling(args: LuaThread.ArgStack): Int { args.lua.push(self.isFalling); return 1 } + private fun canJump(args: LuaThread.ArgStack): Int { args.lua.push(self.canJump); return 1 } + private fun jumping(args: LuaThread.ArgStack): Int { args.lua.push(self.isJumping); return 1 } + private fun groundMovement(args: LuaThread.ArgStack): Int { args.lua.push(self.isGroundMovement); return 1 } + private fun liquidMovement(args: LuaThread.ArgStack): Int { args.lua.push(self.isLiquidMovement); return 1 } + + // controls, stored locally, reset automatically or are persistent + private fun controlRotation(args: LuaThread.ArgStack): Int { + controlRotationRate += args.nextDouble() + return 0 + } + + private fun controlAcceleration(args: LuaThread.ArgStack): Int { + controlAcceleration += args.nextVector2d() + return 0 + } + + private fun controlForce(args: LuaThread.ArgStack): Int { + controlForce += args.nextVector2d() + return 0 + } + + private fun controlApproachVelocity(args: LuaThread.ArgStack): Int { + controlApproachVelocity = ActorMovementController.ApproachVelocityCommand(args.nextVector2d(), args.nextDouble()) + return 0 + } + + private fun controlApproachVelocityAlongAngle(args: LuaThread.ArgStack): Int { + controlApproachVelocityAlongAngle = ActorMovementController.ApproachVelocityAngleCommand(args.nextDouble(), args.nextDouble(), args.nextDouble(), args.nextBoolean()) + return 0 + } + + private fun controlApproachXVelocity(args: LuaThread.ArgStack): Int { + controlApproachVelocityAlongAngle = ActorMovementController.ApproachVelocityAngleCommand(0.0, args.nextDouble(), args.nextDouble(), false) + return 0 + } + + private fun controlApproachYVelocity(args: LuaThread.ArgStack): Int { + controlApproachVelocityAlongAngle = ActorMovementController.ApproachVelocityAngleCommand(PI / 2.0, args.nextDouble(), args.nextDouble(), false) + return 0 + } + + private fun controlParameters(args: LuaThread.ArgStack): Int { + controlParameters = controlParameters.merge(Starbound.gson.fromJsonFast(args.nextJson(), ActorMovementParameters::class.java)) + return 0 + } + + private fun controlModifiers(args: LuaThread.ArgStack): Int { + controlModifiers = controlModifiers.merge(Starbound.gson.fromJsonFast(args.nextJson(), ActorMovementModifiers::class.java)) + return 0 + } + + private fun controlMove(args: LuaThread.ArgStack): Int { + // why? + val new = Direction.valueOf(args.nextDouble()) + val shouldRun = args.nextOptionalBoolean() + + if (new != null) { + controlMove = new + controlShouldRun = shouldRun ?: false } - callbacks["localBoundBox"] = luaFunction { - returnBuffer.setTo(from(self.computeLocalCollisionAABB())) - } + return 0 + } - callbacks["boundBox"] = luaFunction { - returnBuffer.setTo(from(self.computeLocalCollisionAABB())) - } + private fun controlFace(args: LuaThread.ArgStack): Int { + // why? + val new = Direction.valueOf(args.nextDouble()) - callbacks["collisionBoundBox"] = luaFunction { - returnBuffer.setTo(from(self.computeGlobalCollisionAABB())) - } + if (new != null) + controlFace = new - callbacks["collisionPoly"] = luaFunction { - returnBuffer.setTo(from(self.movementParameters.collisionPoly?.map({ it }, { it.firstOrNull() }) ?: Poly.EMPTY)) - } + return 0 + } - callbacks["collisionPolies"] = luaFunction { - returnBuffer.setTo(tableFrom((self.movementParameters.collisionPoly?.map({ listOf(it) }, { it }) ?: listOf(Poly.EMPTY)).map { from(it) })) - } + private fun controlDown(args: LuaThread.ArgStack): Int { controlDown = true; return 0 } + private fun controlCrouch(args: LuaThread.ArgStack): Int { controlCrouch = true; return 0 } + private fun controlHoldJump(args: LuaThread.ArgStack): Int { controlHoldJump = true; return 0 } + private fun controlJump(args: LuaThread.ArgStack): Int { controlJump = args.nextBoolean(); return 0 } + private fun controlFly(args: LuaThread.ArgStack): Int { controlFly = args.nextVector2d(); return 0 } - callbacks["collisionBody"] = luaFunction { - returnBuffer.setTo(from(self.computeGlobalHitboxes().firstOrNull() ?: Poly.EMPTY)) - } + private fun controlPathMove(args: LuaThread.ArgStack): Int { + val position = args.nextVector2d() - callbacks["collisionBodies"] = luaFunction { - returnBuffer.setTo(tableFrom(self.computeGlobalHitboxes().map { from(it) })) - } + if (pathMoveResult?.first == position) { + val pathMoveResult = this.pathMoveResult!! + this.pathMoveResult = null + args.lua.push(pathMoveResult.second) + } else { + pathMoveResult = null - callbacks["position"] = luaFunction { returnBuffer.setTo(tableOf(self.xPosition, self.yPosition)) } - callbacks["xPosition"] = luaFunction { returnBuffer.setTo(self.xPosition) } - callbacks["yPosition"] = luaFunction { returnBuffer.setTo(self.yPosition) } - callbacks["velocity"] = luaFunction { returnBuffer.setTo(tableOf(self.xVelocity, self.yVelocity)) } - callbacks["xVelocity"] = luaFunction { returnBuffer.setTo(self.xVelocity) } - callbacks["yVelocity"] = luaFunction { returnBuffer.setTo(self.yVelocity) } - callbacks["rotation"] = luaFunction { returnBuffer.setTo(self.rotation) } - callbacks["isColliding"] = luaFunction { returnBuffer.setTo(self.isColliding) } - callbacks["isNullColliding"] = luaFunction { returnBuffer.setTo(self.isCollidingWithNull) } - callbacks["isCollisionStuck"] = luaFunction { returnBuffer.setTo(self.isCollisionStuck) } - callbacks["stickingDirection"] = luaFunction { returnBuffer.setTo(self.stickingDirection) } - callbacks["liquidPercentage"] = luaFunction { returnBuffer.setTo(self.liquidPercentage) } - callbacks["liquidId"] = luaFunction { returnBuffer.setTo(self.liquid?.id ?: 0) } - callbacks["liquidName"] = luaFunction { returnBuffer.setTo(self.liquid?.key.toByteString()) } - callbacks["onGround"] = luaFunction { returnBuffer.setTo(self.isOnGround) } - callbacks["zeroG"] = luaFunction { returnBuffer.setTo(self.isZeroGravity) } + val run = args.nextOptionalBoolean() == true + val parameters = args.nextOptionalJson() - callbacks["atWorldLimit"] = luaFunction { bottomOnly: Boolean -> - returnBuffer.setTo(self.isAtWorldLimit(bottomOnly)) - } + val result = self.pathMove(position, run, Starbound.gson.fromJsonFast(parameters ?: JsonNull.INSTANCE)) - callbacks["setAnchorState"] = luaFunction { anchor: Number, index: Number -> - self.anchorNetworkState = AnchorNetworkState(anchor.toInt(), index.toInt()) - } - - callbacks["resetAnchorState"] = luaFunction { - self.anchorNetworkState = null - } - - callbacks["anchorState"] = luaFunction { - val anchorState = self.anchorNetworkState - - if (anchorState != null) { - returnBuffer.setTo(anchorState.entityID, anchorState.positionIndex) + if (result == null) { + controlPathMove = position to false } + + args.lua.push(result?.second) } - callbacks["setPosition"] = luaFunction { value: Table -> - resetPathMove = true - self.position = toVector2d(value) - } + return 1 + } - callbacks["setXPosition"] = luaFunction { value: Number -> - resetPathMove = true - self.xPosition = value.toDouble() - } + private fun pathfinding(args: LuaThread.ArgStack): Int { + args.lua.push(self.pathController.isPathfinding) + return 1 + } - callbacks["setYPosition"] = luaFunction { value: Number -> - resetPathMove = true - self.yPosition = value.toDouble() - } + private fun autoClearControls(args: LuaThread.ArgStack): Int { + args.lua.push(autoClearControls) + return 1 + } - callbacks["translate"] = luaFunction { value: Table -> - resetPathMove = true - self.position += toVector2d(value) - } + private fun setAutoClearControls(args: LuaThread.ArgStack): Int { + autoClearControls = args.nextBoolean() + return 0 + } - callbacks["setVelocity"] = luaFunction { value: Table -> - resetPathMove = true - self.velocity = toVector2d(value) - } + private fun clearControls(args: LuaThread.ArgStack): Int { + clearControls() + return 0 + } - callbacks["setXVelocity"] = luaFunction { value: Number -> - resetPathMove = true - self.xVelocity = value.toDouble() - } + fun init(lua: LuaThread) { + lua.pushTable() + lua.dup() + lua.storeGlobal("mcontroller") - callbacks["setYVelocity"] = luaFunction { value: Number -> - resetPathMove = true - self.yVelocity = value.toDouble() - } + lua.setTableValue("mass", ::mass) + lua.setTableValue("localBoundBox", ::localBoundBox) + lua.setTableValue("boundBox", ::boundBox) + lua.setTableValue("collisionBoundBox", ::collisionBoundBox) + lua.setTableValue("collisionPoly", ::collisionPoly) + lua.setTableValue("collisionPolies", ::collisionPolies) + lua.setTableValue("collisionBody", ::collisionBody) + lua.setTableValue("collisionBodies", ::collisionBodies) + lua.setTableValue("position", ::position) + lua.setTableValue("xPosition", ::xPosition) + lua.setTableValue("yPosition", ::yPosition) + lua.setTableValue("velocity", ::velocity) + lua.setTableValue("xVelocity", ::xVelocity) + lua.setTableValue("yVelocity", ::yVelocity) + lua.setTableValue("rotation", ::rotation) + lua.setTableValue("isColliding", ::isColliding) + lua.setTableValue("isNullColliding", ::isNullColliding) + lua.setTableValue("isCollisionStuck", ::isCollisionStuck) + lua.setTableValue("stickingDirection", ::stickingDirection) + lua.setTableValue("liquidPercentage", ::liquidPercentage) + lua.setTableValue("liquidId", ::liquidId) + lua.setTableValue("liquidName", ::liquidName) + lua.setTableValue("onGround", ::onGround) + lua.setTableValue("zeroG", ::zeroG) + lua.setTableValue("atWorldLimit", ::atWorldLimit) + lua.setTableValue("setAnchorState", ::setAnchorState) + lua.setTableValue("resetAnchorState", ::resetAnchorState) + lua.setTableValue("anchorState", ::anchorState) + lua.setTableValue("setPosition", ::setPosition) + lua.setTableValue("setXPosition", ::setXPosition) + lua.setTableValue("setYPosition", ::setYPosition) + lua.setTableValue("translate", ::translate) + lua.setTableValue("setVelocity", ::setVelocity) + lua.setTableValue("setXVelocity", ::setXVelocity) + lua.setTableValue("setYVelocity", ::setYVelocity) + lua.setTableValue("addMomentum", ::addMomentum) + lua.setTableValue("setRotation", ::setRotation) + lua.setTableValue("baseParameters", ::baseParameters) + lua.setTableValue("walking", ::walking) + lua.setTableValue("running", ::running) + lua.setTableValue("movingDirection", ::movingDirection) + lua.setTableValue("facingDirection", ::facingDirection) + lua.setTableValue("crouching", ::crouching) + lua.setTableValue("flying", ::flying) + lua.setTableValue("falling", ::falling) + lua.setTableValue("canJump", ::canJump) + lua.setTableValue("jumping", ::jumping) + lua.setTableValue("groundMovement", ::groundMovement) + lua.setTableValue("liquidMovement", ::liquidMovement) + lua.setTableValue("controlRotation", ::controlRotation) + lua.setTableValue("controlAcceleration", ::controlAcceleration) + lua.setTableValue("controlForce", ::controlForce) + lua.setTableValue("controlApproachVelocity", ::controlApproachVelocity) + lua.setTableValue("controlApproachVelocityAlongAngle", ::controlApproachVelocityAlongAngle) + lua.setTableValue("controlApproachXVelocity", ::controlApproachXVelocity) + lua.setTableValue("controlApproachYVelocity", ::controlApproachYVelocity) + lua.setTableValue("controlParameters", ::controlParameters) + lua.setTableValue("controlModifiers", ::controlModifiers) + lua.setTableValue("controlMove", ::controlMove) + lua.setTableValue("controlFace", ::controlFace) + lua.setTableValue("controlDown", ::controlDown) + lua.setTableValue("controlCrouch", ::controlCrouch) + lua.setTableValue("controlHoldJump", ::controlHoldJump) + lua.setTableValue("controlJump", ::controlJump) + lua.setTableValue("controlFly", ::controlFly) + lua.setTableValue("controlPathMove", ::controlPathMove) + lua.setTableValue("pathfinding", ::pathfinding) + lua.setTableValue("autoClearControls", ::autoClearControls) + lua.setTableValue("setAutoClearControls", ::setAutoClearControls) + lua.setTableValue("clearControls", ::clearControls) - callbacks["addMomentum"] = luaFunction { value: Table -> - resetPathMove = true - if (self.mass != 0.0) // let's not collapse into black hole - self.velocity += toVector2d(value) / self.mass - } - - callbacks["setRotation"] = luaFunction { value: Number -> - resetPathMove = true - self.rotation = value.toDouble() - } - - callbacks["baseParameters"] = luaFunction { returnBuffer.setTo(from(Starbound.gson.toJsonTree(self.actorMovementParameters))) } - callbacks["walking"] = luaFunction { returnBuffer.setTo(self.isWalking) } - callbacks["running"] = luaFunction { returnBuffer.setTo(self.isRunning) } - callbacks["movingDirection"] = luaFunction { returnBuffer.setTo(self.movingDirection.numericalValue) } - callbacks["facingDirection"] = luaFunction { returnBuffer.setTo(self.facingDirection.numericalValue) } - callbacks["crouching"] = luaFunction { returnBuffer.setTo(self.isCrouching) } - callbacks["flying"] = luaFunction { returnBuffer.setTo(self.isFlying) } - callbacks["falling"] = luaFunction { returnBuffer.setTo(self.isFalling) } - callbacks["canJump"] = luaFunction { returnBuffer.setTo(self.canJump) } - callbacks["jumping"] = luaFunction { returnBuffer.setTo(self.isJumping) } - callbacks["groundMovement"] = luaFunction { returnBuffer.setTo(self.isGroundMovement) } - callbacks["liquidMovement"] = luaFunction { returnBuffer.setTo(self.isLiquidMovement) } - - // controls, stored locally, reset automatically or are persistent - callbacks["controlRotation"] = luaFunction { value: Number -> - controlRotationRate += value.toDouble() - } - - callbacks["controlAcceleration"] = luaFunction { value: Table -> - controlAcceleration += toVector2d(value) - } - - callbacks["controlForce"] = luaFunction { value: Table -> - controlForce += toVector2d(value) - } - - callbacks["controlApproachVelocity"] = luaFunction { value: Table, rate: Number -> - controlApproachVelocity = ActorMovementController.ApproachVelocityCommand(toVector2d(value), rate.toDouble()) - } - - callbacks["controlApproachVelocityAlongAngle"] = luaFunction { angle: Number, targetVelocity: Number, maxControlForce: Number, positiveOnly: Boolean -> - controlApproachVelocityAlongAngle = ActorMovementController.ApproachVelocityAngleCommand(angle.toDouble(), targetVelocity.toDouble(), maxControlForce.toDouble(), positiveOnly) - } - - callbacks["controlApproachXVelocity"] = luaFunction { targetVelocity: Number, maxControlForce: Number -> - controlApproachVelocityAlongAngle = ActorMovementController.ApproachVelocityAngleCommand(0.0, targetVelocity.toDouble(), maxControlForce.toDouble(), false) - } - - callbacks["controlApproachYVelocity"] = luaFunction { targetVelocity: Number, maxControlForce: Number -> - controlApproachVelocityAlongAngle = ActorMovementController.ApproachVelocityAngleCommand(PI / 2.0, targetVelocity.toDouble(), maxControlForce.toDouble(), false) - } - - callbacks["controlParameters"] = luaFunction { data: Table -> - controlParameters = controlParameters.merge(Starbound.gson.fromJsonFast(data.toJson(true), ActorMovementParameters::class.java)) - } - - callbacks["controlModifiers"] = luaFunction { data: Table -> - controlModifiers = controlModifiers.merge(Starbound.gson.fromJsonFast(data.toJson(true), ActorMovementModifiers::class.java)) - } - - callbacks["controlMove"] = luaFunction { direction: Number, shouldRun: Boolean? -> - // why? - val new = Direction.valueOf(direction) - - if (new != null) { - controlMove = new - controlShouldRun = shouldRun ?: false - } - } - - callbacks["controlFace"] = luaFunction { direction: Number -> - // why? - val new = Direction.valueOf(direction) - - if (new != null) - controlFace = new - } - - callbacks["controlDown"] = luaFunction { controlDown = true } - callbacks["controlCrouch"] = luaFunction { controlCrouch = true } - callbacks["controlJump"] = luaFunction { should: Boolean -> controlJump = should } - callbacks["controlHoldJump"] = luaFunction { controlHoldJump = true } - callbacks["controlFly"] = luaFunction { value: Table -> controlFly = toVector2d(value) } - - callbacks["controlPathMove"] = luaFunction { position: Table, run: Boolean?, parameters: Table? -> - if (pathMoveResult?.first == toVector2d(position)) { - val pathMoveResult = pathMoveResult!! - this@MovementControllerBindings.pathMoveResult = null - returnBuffer.setTo(pathMoveResult.second) - } else { - pathMoveResult = null - val result = self.pathMove(toVector2d(position), run == true, Starbound.gson.fromJsonFast(toJsonFromLua(parameters))) - - if (result == null) { - controlPathMove = toVector2d(position) to (run == true) - } - - returnBuffer.setTo(result?.second) - } - } - - callbacks["pathfinding"] = luaFunction { - returnBuffer.setTo(self.pathController.isPathfinding) - } - - callbacks["autoClearControls"] = luaFunction { - returnBuffer.setTo(autoClearControls) - } - - callbacks["setAutoClearControls"] = luaFunction { should: Boolean -> - autoClearControls = should - } - - callbacks["clearControls"] = luaFunction { clearControls() } + lua.pop() } private var autoClearControls = true diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/NPCBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/NPCBindings.kt index d00015b2..da2aafa4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/NPCBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/NPCBindings.kt @@ -1,222 +1,364 @@ package ru.dbotthepony.kstarbound.lua.bindings -import org.classdump.luna.ByteString -import org.classdump.luna.Table +import com.google.gson.JsonNull +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.EntityDamageTeam import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor -import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor import ru.dbotthepony.kstarbound.fromJsonFast -import ru.dbotthepony.kstarbound.lua.LuaEnvironment -import ru.dbotthepony.kstarbound.lua.from -import ru.dbotthepony.kstarbound.lua.get -import ru.dbotthepony.kstarbound.lua.iterator -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.tableOf -import ru.dbotthepony.kstarbound.lua.toByteString -import ru.dbotthepony.kstarbound.lua.toJsonFromLua -import ru.dbotthepony.kstarbound.lua.toVector2d +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.lua.LuaType +import ru.dbotthepony.kstarbound.lua.nextVector2d +import ru.dbotthepony.kstarbound.lua.push import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.sbIntern import ru.dbotthepony.kstarbound.util.valueOfOrNull import ru.dbotthepony.kstarbound.world.entities.AnchorNetworkState import ru.dbotthepony.kstarbound.world.entities.HumanoidActorEntity +import ru.dbotthepony.kstarbound.world.entities.LoungeAnchorState import ru.dbotthepony.kstarbound.world.entities.NPCEntity import ru.dbotthepony.kstarbound.world.entities.api.LoungeableEntity +import kotlin.collections.contains +import kotlin.collections.indices +import kotlin.collections.isNotEmpty +import kotlin.collections.set +import kotlin.collections.withIndex -fun provideNPCBindings(self: NPCEntity, lua: LuaEnvironment) { - val callbacks = lua.newTable() - lua.globals["npc"] = callbacks +private fun toAbsolutePosition(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.toAbsolutePosition(args.nextVector2d())) + return 1 +} - callbacks["toAbsolutePosition"] = luaFunction { pos: Table -> - returnBuffer.setTo(from(self.toAbsolutePosition(toVector2d(pos)))) +private fun species(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.variant.species.key) + return 1 +} + +private fun gender(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.variant.humanoidIdentity.gender.jsonName) + return 1 +} + +private fun humanoidIdentity(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(Starbound.gson.toJsonTree(self.variant.humanoidIdentity)) + return 1 +} + +private fun npcType(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.variant.typeName) + return 1 +} + +private fun seed(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.variant.seed) + return 1 +} + +private fun level(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.variant.level) + return 1 +} + +private fun dropPools(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.pushTable(self.dropPools.size) + + for ((i, entry) in self.dropPools.withIndex()) { + args.lua.setTableValue(i + 1L, entry.key.left()) } - callbacks["species"] = luaFunction { returnBuffer.setTo(self.variant.species.key.toByteString()) } - callbacks["gender"] = luaFunction { returnBuffer.setTo(self.variant.humanoidIdentity.gender.jsonName.toByteString()) } - callbacks["humanoidIdentity"] = luaFunction { returnBuffer.setTo(from(Starbound.gson.toJsonTree(self.variant.humanoidIdentity))) } - callbacks["npcType"] = luaFunction { returnBuffer.setTo(self.variant.typeName.toByteString()) } - callbacks["seed"] = luaFunction { returnBuffer.setTo(self.variant.seed) } - callbacks["level"] = luaFunction { returnBuffer.setTo(self.variant.level) } - callbacks["dropPools"] = luaFunction { returnBuffer.setTo(tableOf(*self.dropPools.map { it.key.left().toByteString() }.toTypedArray())) } + return 1 +} - callbacks["setDropPools"] = luaFunction { dropPools: Table? -> - self.dropPools.clear() +private fun setDropPools(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.dropPools.clear() - if (dropPools != null) { - for ((_, pool) in dropPools) { - self.dropPools.add(Registries.treasurePools.ref(pool.toString())) - } + if (args.hasNext) { + val values = args.readTableValues { Registries.treasurePools.ref(getString(it) ?: throw IllegalArgumentException("Drop pools table contains non-string values")) } + self.dropPools.addAll(values) + } + + return 0 +} + +private fun energy(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.energy) + return 1 +} + +private fun maxEnergy(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.maxEnergy) + return 1 +} + +private fun say(self: NPCEntity, hasPortrait: Boolean, args: LuaThread.ArgStack): Int { + val line = args.nextString() + val portrait = if (hasPortrait) args.nextString() else "" + val tags = Object2ObjectArrayMap() + val tagsType = args.peek() + + if (tagsType == LuaType.TABLE) { + val pairs = args.readTable( + keyVisitor = { getString(it) ?: throw IllegalArgumentException("Tag replacement table contains non-strings as keys") }, + valueVisitor = { getString(it) ?: throw IllegalArgumentException("Tag replacement table contains non-strings as keys") }, + ) + + for ((k, v) in pairs) { + tags[k] = v } + } else if (!tagsType.isNothing) { + throw IllegalArgumentException("bad argument #2 to object.say: table expected, got $tagsType") + } else { + args.skip() } - // lol why - callbacks["energy"] = luaFunction { returnBuffer.setTo(self.energy) } - callbacks["maxEnergy"] = luaFunction { returnBuffer.setTo(self.maxEnergy) } + val sayConfig = args.nextOptionalJson() ?: JsonNull.INSTANCE - callbacks["say"] = luaFunction { line: ByteString, tags: Table?, config: Any? -> - val actualLine = if (tags != null) { - SBPattern.of(line.decode()).resolveOrSkip({ tags[it]?.toString() }) + if (line.isBlank()) { + args.lua.push(false) + } else if (tags.isEmpty()) { + self.addChatMessage(line, sayConfig, portrait = portrait) + args.lua.push(true) + } else { + self.addChatMessage(SBPattern.of(line).resolveOrSkip(tags::get), sayConfig, portrait = portrait) + args.lua.push(true) + } + + return 1 +} + +private fun emote(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.addEmote(args.nextString()) + return 0 +} + +private fun dance(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.setDance(args.nextString()) + return 0 +} + +private fun setInteractive(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.isInteractive = args.nextBoolean() + return 0 +} + +private fun setLounging(self: NPCEntity, args: LuaThread.ArgStack): Int { + val loungeable = args.nextInt() + val anchorIndex = args.nextOptionalInt() ?: 0 + + val entity = self.world.entities[loungeable] as? LoungeableEntity + + if (entity == null || anchorIndex !in entity.anchors.indices || entity.entitiesLoungingIn(anchorIndex).isNotEmpty()) { + args.lua.push(false) + } else { + self.movement.anchorNetworkState = AnchorNetworkState(loungeable, anchorIndex) + args.lua.push(true) + } + + return 1 +} + +private fun resetLounging(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.movement.anchorNetworkState = null + return 0 +} + +private fun isLounging(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.movement.anchorState is LoungeAnchorState) + return 1 +} + +private fun loungingIn(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.movement.anchorNetworkState?.entityID?.toLong()) + return 1 +} + +private fun setItemSlot(self: NPCEntity, args: LuaThread.ArgStack): Int { + val slot = args.nextString() + val descriptor = ItemDescriptor(args) + args.lua.push(self.setItem(slot, descriptor)) + return 1 +} + +private fun getItemSlot(self: NPCEntity, args: LuaThread.ArgStack): Int { + val slot = args.nextString() + val slotType = HumanoidActorEntity.ItemSlot.entries.valueOfOrNull(slot.lowercase()) + + if (slotType == null) { + if (slot in self.variant.items) { + self.variant.items[slot]!!.store(args.lua) } else { - line.decode() + return 0 } - - val isNotEmpty = actualLine.isNotEmpty() - - if (isNotEmpty) - self.addChatMessage(actualLine, toJsonFromLua(config)) - - returnBuffer.setTo(isNotEmpty) + } else { + self.getItem(slotType).createDescriptor().store(args.lua) } - callbacks["sayPortrait"] = luaFunction { line: ByteString, portrait: ByteString, tags: Table?, config: Any? -> - val actualLine = if (tags != null) { - SBPattern.of(line.decode()).resolveOrSkip({ tags[it]?.toString() }) - } else { - line.decode() - } + return 1 +} - val isNotEmpty = actualLine.isNotEmpty() +private fun disableWornArmor(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.disableWornArmor = args.nextBoolean() + return 0 +} - if (isNotEmpty) - self.addChatMessage(actualLine, toJsonFromLua(config), portrait.decode()) +private fun getDisableWornArmor(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.disableWornArmor) + return 1 +} - returnBuffer.setTo(isNotEmpty) - } +private fun setShifting(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.isShifting = args.nextBoolean() + return 0 +} - callbacks["emote"] = luaFunction { emote: ByteString -> - self.addEmote(emote.decode()) - } +private fun isShifting(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.isShifting) + return 1 +} - callbacks["dance"] = luaFunction { dance: ByteString -> - self.setDance(dance.decode()) - } +private fun getDamageOnTouch(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.damageOnTouch) + return 1 +} - callbacks["setInteractive"] = luaFunction { isInteractive: Boolean -> - self.isInteractive = isInteractive - } +private fun setDamageOnTouch(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.damageOnTouch = args.nextBoolean() + return 0 +} - callbacks["setLounging"] = luaFunction { loungeable: Number, oAnchorIndex: Number? -> - val anchorIndex = oAnchorIndex?.toInt() ?: 0 - val entity = self.world.entities[loungeable.toInt()] as? LoungeableEntity +private fun aimPosition(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.aimPosition) + return 1 +} - if (entity == null || anchorIndex !in 0 until entity.anchors.size || entity.entitiesLoungingIn(anchorIndex).isNotEmpty()) { - returnBuffer.setTo(false) - } else { - self.movement.anchorNetworkState = AnchorNetworkState(loungeable.toInt(), anchorIndex) - returnBuffer.setTo(true) - } - } +private fun setAimPosition(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.aimPosition = self.world.geometry.diff(args.nextVector2d(), self.position) + return 0 +} - callbacks["resetLounging"] = luaFunction { - self.movement.anchorNetworkState = null - } +private fun getDeathParticleBurst(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.deathParticleBurst) + return 1 +} - callbacks["isLounging"] = luaFunction { - returnBuffer.setTo(self.movement.anchorNetworkState != null) - } +private fun setDeathParticleBurst(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.deathParticleBurst = args.nextOptionalString()?.sbIntern() + return 0 +} - callbacks["loungingIn"] = luaFunction { - returnBuffer.setTo(self.movement.anchorNetworkState?.entityID) - } +private fun getStatusText(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.statusText) + return 1 +} - callbacks["setOfferedQuests"] = luaFunction { values: Table? -> - self.offeredQuests.clear() +private fun setStatusText(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.statusText = args.nextOptionalString()?.sbIntern() + return 0 +} - if (values != null) { - for ((_, quest) in values) { - self.offeredQuests.add(Starbound.gson.fromJsonFast(toJsonFromLua(quest), QuestArcDescriptor::class.java)) - } - } - } +private fun getDisplayNametag(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.displayNametag) + return 1 +} - callbacks["setTurnInQuests"] = luaFunction { values: Table? -> - self.turnInQuests.clear() +private fun setDisplayNametag(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.displayNametag = args.nextBoolean() + return 0 +} - if (values != null) { - for ((_, value) in values) { - self.turnInQuests.add(value.toString()) - } - } - } +private fun getKeepAlive(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.keepAlive) + return 1 +} - callbacks["setItemSlot"] = luaFunction { slot: ByteString, descriptor: Any -> - returnBuffer.setTo(self.setItem(slot.decode(), ItemDescriptor(descriptor))) - } +private fun setKeepAlive(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.keepAlive = args.nextBoolean() + return 0 +} - callbacks["getItemSlot"] = luaFunction { slot: ByteString -> - val decoded = slot.decode() - val slotType = HumanoidActorEntity.ItemSlot.entries.valueOfOrNull(decoded.lowercase()) +private fun getAggressive(self: NPCEntity, args: LuaThread.ArgStack): Int { + args.lua.push(self.isAggressive) + return 1 +} - if (slotType == null) { - if (decoded in self.variant.items) { - returnBuffer.setTo(self.variant.items[decoded]!!.toTable(this)) - } else { - returnBuffer.setTo() - } - } else { - returnBuffer.setTo(self.getItem(slotType).toTable(this)) - } - } +private fun setAggressive(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.isAggressive = args.nextBoolean() + return 0 +} - callbacks["disableWornArmor"] = luaFunction { disable: Boolean -> - self.disableWornArmor = disable - } +private fun setPersistent(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.isPersistent = args.nextBoolean() + return 0 +} - callbacks["beginPrimaryFire"] = luaFunction { self.beginPrimaryFire() } - callbacks["beginAltFire"] = luaFunction { self.beginSecondaryFire() } - callbacks["beginSecondaryFire"] = luaFunction { self.beginSecondaryFire() } - callbacks["endPrimaryFire"] = luaFunction { self.endPrimaryFire() } - callbacks["endAltFire"] = luaFunction { self.endSecondaryFire() } - callbacks["endSecondaryFire"] = luaFunction { self.endSecondaryFire() } +private fun setDamageTeam(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.team.accept(Starbound.gson.fromJsonFast(args.nextJson(), EntityDamageTeam::class.java)) + return 0 +} - callbacks["setShifting"] = luaFunction { value: Boolean -> - self.isShifting = value - } +private fun setUniqueId(self: NPCEntity, args: LuaThread.ArgStack): Int { + self.uniqueID.accept(args.nextOptionalString()?.sbIntern()) + return 0 +} - callbacks["setDamageOnTouch"] = luaFunction { value: Boolean -> - self.damageOnTouch = value - } +fun provideNPCBindings(self: NPCEntity, lua: LuaThread) { + lua.pushTable() + lua.dup() + lua.storeGlobal("npc") - callbacks["aimPosition"] = luaFunction { - returnBuffer.setTo(from(self.aimPosition)) - } + lua.pushBinding(self, "toAbsolutePosition", ::toAbsolutePosition) + lua.pushBinding(self, "species", ::species) + lua.pushBinding(self, "gender", ::gender) + lua.pushBinding(self, "humanoidIdentity", ::humanoidIdentity) + lua.pushBinding(self, "npcType", ::npcType) + lua.pushBinding(self, "seed", ::seed) + lua.pushBinding(self, "level", ::level) + lua.pushBinding(self, "dropPools", ::dropPools) + lua.pushBinding(self, "setDropPools", ::setDropPools) + lua.pushBinding(self, "energy", ::energy) + lua.pushBinding(self, "maxEnergy", ::maxEnergy) + lua.pushBinding(self, false, "say", ::say) + lua.pushBinding(self, true, "sayPortrait", ::say) + lua.pushBinding(self, "emote", ::emote) + lua.pushBinding(self, "dance", ::dance) + lua.pushBinding(self, "setInteractive", ::setInteractive) + lua.pushBinding(self, "setLounging", ::setLounging) + lua.pushBinding(self, "resetLounging", ::resetLounging) + lua.pushBinding(self, "isLounging", ::isLounging) + lua.pushBinding(self, "loungingIn", ::loungingIn) + lua.pushBinding(self, NPCEntity::offeredQuests, "setOfferedQuests", ::setOfferedQuests) + lua.pushBinding(self, NPCEntity::turnInQuests, "setTurnInQuests", ::setTurnInQuests) + lua.pushBinding(self, "setItemSlot", ::setItemSlot) + lua.pushBinding(self, "getItemSlot", ::getItemSlot) + lua.pushBinding(self, "disableWornArmor", ::disableWornArmor) + lua.pushBinding(self, "getDisableWornArmor", ::getDisableWornArmor) + lua.pushBinding(self, "setShifting", ::setShifting) + lua.pushBinding(self, "isShifting", ::isShifting) + lua.pushBinding(self, "setDamageOnTouch", ::setDamageOnTouch) + lua.pushBinding(self, "getDamageOnTouch", ::getDamageOnTouch) + lua.pushBinding(self, "aimPosition", ::aimPosition) + lua.pushBinding(self, "setAimPosition", ::setAimPosition) + lua.pushBinding(self, "getDeathParticleBurst", ::getDeathParticleBurst) + lua.pushBinding(self, "setDeathParticleBurst", ::setDeathParticleBurst) + lua.pushBinding(self, "getStatusText", ::getStatusText) + lua.pushBinding(self, "setStatusText", ::setStatusText) + lua.pushBinding(self, "getDisplayNametag", ::getDisplayNametag) + lua.pushBinding(self, "setDisplayNametag", ::setDisplayNametag) + lua.pushBinding(self, "getKeepAlive", ::getKeepAlive) + lua.pushBinding(self, "setKeepAlive", ::setKeepAlive) + lua.pushBinding(self, "getAggressive", ::getAggressive) + lua.pushBinding(self, "setAggressive", ::setAggressive) + lua.pushBinding(self, "setPersistent", ::setPersistent) + lua.pushBinding(self, "setDamageTeam", ::setDamageTeam) + lua.pushBinding(self, "setUniqueId", ::setUniqueId) - callbacks["setAimPosition"] = luaFunction { pos: Table -> - self.aimPosition = self.world.geometry.diff(toVector2d(pos), self.position) - } + lua.pushBinding(self, "beginPrimaryFire", NPCEntity::beginPrimaryFire) + lua.pushBinding(self, "beginAltFire", NPCEntity::beginSecondaryFire) + lua.pushBinding(self, "beginSecondaryFire", NPCEntity::beginSecondaryFire) + lua.pushBinding(self, "endPrimaryFire", NPCEntity::endPrimaryFire) + lua.pushBinding(self, "endAltFire", NPCEntity::endSecondaryFire) + lua.pushBinding(self, "endSecondaryFire", NPCEntity::endSecondaryFire) - callbacks["setDeathParticleBurst"] = luaFunction { value: ByteString? -> - self.deathParticleBurst = value?.decode()?.sbIntern() - } - - callbacks["setStatusText"] = luaFunction { value: ByteString? -> - self.statusText = value?.decode()?.sbIntern() - } - - callbacks["setDisplayNametag"] = luaFunction { value: Boolean -> - self.displayNametag = value - } - - callbacks["setPersistent"] = luaFunction { value: Boolean -> - self.isPersistent = value - } - - callbacks["setKeepAlive"] = luaFunction { value: Boolean -> - self.keepAlive = value - } - - callbacks["setAggressive"] = luaFunction { value: Boolean -> - self.isAggressive = value - } - - callbacks["setDamageTeam"] = luaFunction { value: Table -> - self.team.accept(Starbound.gson.fromJsonFast(toJsonFromLua(value), EntityDamageTeam::class.java)) - } - - callbacks["setUniqueId"] = luaFunction { value: ByteString? -> - self.uniqueID.accept(value?.decode()?.sbIntern()) - } -} \ No newline at end of file + lua.pop() +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt index 95542a0a..baf8a22d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt @@ -1,249 +1,397 @@ package ru.dbotthepony.kstarbound.lua.bindings -import com.google.gson.JsonObject +import com.google.gson.JsonNull import com.google.gson.reflect.TypeToken -import org.classdump.luna.ByteString -import org.classdump.luna.LuaRuntimeException -import org.classdump.luna.Table -import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.DamageData import ru.dbotthepony.kstarbound.defs.EphemeralStatusEffect import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect import ru.dbotthepony.kstarbound.fromJsonFast -import ru.dbotthepony.kstarbound.lua.LuaEnvironment -import ru.dbotthepony.kstarbound.lua.from -import ru.dbotthepony.kstarbound.lua.iterator -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.tableOf -import ru.dbotthepony.kstarbound.lua.toByteString -import ru.dbotthepony.kstarbound.lua.toJsonFromLua +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.lua.push import ru.dbotthepony.kstarbound.util.sbIntern import ru.dbotthepony.kstarbound.world.entities.StatusController private object PersistentStatusEffectToken : TypeToken() private object CPersistentStatusEffectToken : TypeToken>() -fun provideStatusControllerBindings(self: StatusController, lua: LuaEnvironment) { - val callbacks = lua.newTable() - lua.globals["status"] = callbacks +private fun statusProperty(self: StatusController, args: LuaThread.ArgStack): Int { + val get = self.getProperty(args.nextString()) - callbacks["statusProperty"] = luaFunction { key: ByteString, ifMissing: Any? -> - val get = self.getProperty(key.decode()) - - if (get == null) { - returnBuffer.setTo(ifMissing) - } else { - returnBuffer.setTo(from(get)) + if (get == null) { + if (args.top == 1) { + args.lua.push() + } else if (args.top > 3) { + args.lua.dup(2) } + } else { + args.lua.push(get) } - callbacks["setStatusProperty"] = luaFunction { key: ByteString, value: Any? -> - self.setProperty(key.decode().sbIntern(), toJsonFromLua(value)) - } - - callbacks["stat"] = luaFunction { name: ByteString -> - returnBuffer.setTo(self.effectiveStats[name.decode()]?.effectiveModifiedValue ?: 0.0) - } - - callbacks["statPositive"] = luaFunction { name: ByteString -> - returnBuffer.setTo(self.statPositive(name.decode())) - } - - callbacks["resourceNames"] = luaFunction { - returnBuffer.setTo(tableOf(self.resources.keys.toTypedArray())) - } - - callbacks["isResource"] = luaFunction { name: ByteString -> - returnBuffer.setTo(name.decode() in self.resources.keys) - } - - callbacks["resource"] = luaFunction { name: ByteString -> - val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(0.0) - returnBuffer.setTo(resource.value) - } - - callbacks["resourcePositive"] = luaFunction { name: ByteString -> - val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(false) - returnBuffer.setTo(resource.value > 0.0) - } - - callbacks["setResource"] = luaFunction { name: ByteString, value: Number -> - val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") - resource.value = value.toDouble() - } - - callbacks["modifyResource"] = luaFunction { name: ByteString, value: Number -> - val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") - resource.value += value.toDouble() - } - - callbacks["giveResource"] = luaFunction { name: ByteString, value: Number -> - // while other functions throw an exception if resource does not exist - // this one returns 0 - // Consistency is my first, second, and last name - val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(0.0) - returnBuffer.setTo(resource.give(value.toDouble())) - } - - callbacks["consumeResource"] = luaFunction { name: ByteString, value: Number -> - // while other functions throw an exception if resource does not exist - // this one returns 0 - // Consistency is my first, second, and last name - val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(false) - returnBuffer.setTo(resource.consume(value.toDouble())) - } - - callbacks["overConsumeResource"] = luaFunction { name: ByteString, value: Number -> - // while other functions throw an exception if resource does not exist - // this one returns 0 - // Consistency is my first, second, and last name - val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(false) - returnBuffer.setTo(resource.consume(value.toDouble(), allowOverdraw = true)) - } - - callbacks["resourceLocked"] = luaFunction { name: ByteString -> - val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") - returnBuffer.setTo(resource.isLocked) - } - - callbacks["setResourceLocked"] = luaFunction { name: ByteString, isLocked: Boolean -> - val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") - resource.isLocked = isLocked - } - - callbacks["resetResource"] = luaFunction { name: ByteString, isLocked: Boolean -> - val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") - resource.reset() - } - - callbacks["resetAllResources"] = luaFunction { name: ByteString, isLocked: Boolean -> - self.resetResources() - } - - callbacks["resourceMax"] = luaFunction { name: ByteString -> - val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") - returnBuffer.setTo(resource.maxValue) - } - - callbacks["resourcePercentage"] = luaFunction { name: ByteString -> - val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") - returnBuffer.setTo(resource.percentage) - } - - callbacks["setResourcePercentage"] = luaFunction { name: ByteString, value: Number -> - val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") - resource.setAsPercentage(value.toDouble()) - } - - callbacks["modifyResourcePercentage"] = luaFunction { name: ByteString, value: Number -> - val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") - resource.modifyPercentage(value.toDouble()) - } - - callbacks["getPersistentEffects"] = luaFunction { category: ByteString -> - returnBuffer.setTo(tableOf(self.getPersistentEffects(category.decode()).map { it.map({ from(Starbound.gson.toJsonTree(it)) }, { it }) })) - } - - callbacks["addPersistentEffect"] = luaFunction { category: ByteString, effect: Any -> - self.addPersistentEffect(category.decode().sbIntern(), Starbound.gson.fromJsonFast(toJsonFromLua(effect), PersistentStatusEffectToken)) - } - - callbacks["addPersistentEffects"] = luaFunction { category: ByteString, effect: Any -> - self.addPersistentEffects(category.decode().sbIntern(), Starbound.gson.fromJsonFast(toJsonFromLua(effect), CPersistentStatusEffectToken)) - } - - callbacks["setPersistentEffects"] = luaFunction { category: ByteString, effect: Any -> - self.setPersistentEffects(category.decode().sbIntern(), Starbound.gson.fromJsonFast(toJsonFromLua(effect), CPersistentStatusEffectToken)) - } - - callbacks["clearPersistentEffects"] = luaFunction { category: ByteString -> - self.removePersistentEffects(category.decode()) - } - - callbacks["clearAllPersistentEffects"] = luaFunction { - self.removeAllPersistentEffects() - } - - callbacks["addEphemeralEffect"] = luaFunction { name: ByteString, duration: Number?, source: Number? -> - self.addEphemeralEffect(EphemeralStatusEffect(Registries.statusEffects.ref(name.decode().sbIntern()), duration?.toDouble()), source?.toInt()) - } - - callbacks["addEphemeralEffects"] = luaFunction { effects: Table, source: Number? -> - for ((_, effect) in effects) { - self.addEphemeralEffect(Starbound.gson.fromJsonFast(toJsonFromLua(effect), EphemeralStatusEffect::class.java), source?.toInt()) - } - } - - callbacks["removeEphemeralEffect"] = luaFunction { name: ByteString -> - self.removeEphemeralEffect(name.decode()) - } - - callbacks["clearEphemeralEffects"] = luaFunction { - self.removeEphemeralEffects() - } - - callbacks["damageTakenSince"] = luaFunction { since: Number? -> - val (list, newSince) = self.recentDamageReceived(since?.toLong() ?: 0L) - returnBuffer.setTo(tableOf(*list.map { from(Starbound.gson.toJsonTree(it)) }.toTypedArray()), newSince) - } - - callbacks["inflictedHitsSince"] = luaFunction { since: Number? -> - val (list, newSince) = self.recentHitsDealt(since?.toLong() ?: 0L) - - returnBuffer.setTo(tableOf(*list.map { p -> - from(Starbound.gson.toJsonTree(p.second).also { it as JsonObject; it["targetEntityId"] = p.first }) - }.toTypedArray()), newSince) - } - - callbacks["inflictedDamageSince"] = luaFunction { since: Number? -> - val (list, newSince) = self.recentDamageDealt(since?.toLong() ?: 0L) - returnBuffer.setTo(tableOf(*list.map { from(Starbound.gson.toJsonTree(it)) }.toTypedArray()), newSince) - } - - callbacks["activeUniqueStatusEffectSummary"] = luaFunction { - returnBuffer.setTo(tableOf(*self.activeUniqueStatusEffectSummary().map { tableOf(it.first.key, it.second) }.toTypedArray())) - } - - callbacks["uniqueStatusEffectActive"] = luaFunction { name: ByteString -> - returnBuffer.setTo(self.uniqueStatusEffectActive(name.decode())) - } - - callbacks["primaryDirectives"] = luaFunction { - returnBuffer.setTo(self.primaryDirectives.toByteString()) - } - - callbacks["setPrimaryDirectives"] = luaFunction { directives: ByteString? -> - self.primaryDirectives = directives?.decode()?.sbIntern() ?: "" - } - - callbacks["applySelfDamageRequest"] = luaFunction { damage: Table -> - self.inflictSelfDamage(Starbound.gson.fromJsonFast(toJsonFromLua(damage), DamageData::class.java)) - } - - callbacks["appliesEnvironmentStatusEffects"] = luaFunction { - returnBuffer.setTo(self.appliesEnvironmentStatusEffects) - } - - callbacks["appliesWeatherStatusEffects"] = luaFunction { - returnBuffer.setTo(self.appliesWeatherStatusEffects) - } - - callbacks["minimumLiquidStatusEffectPercentage"] = luaFunction { - returnBuffer.setTo(self.minimumLiquidStatusEffectPercentage) - } - - callbacks["setAppliesEnvironmentStatusEffects"] = luaFunction { should: Boolean -> - self.appliesEnvironmentStatusEffects = should - } - - callbacks["setAppliesWeatherStatusEffects"] = luaFunction { should: Boolean -> - self.appliesWeatherStatusEffects = should - } - - callbacks["setMinimumLiquidStatusEffectPercentage"] = luaFunction { value: Number -> - self.minimumLiquidStatusEffectPercentage = value.toDouble() - } + return 1 +} + +private fun setStatusProperty(self: StatusController, args: LuaThread.ArgStack): Int { + self.setProperty(args.nextString().sbIntern(), args.nextOptionalJson() ?: JsonNull.INSTANCE) + return 0 +} + +private fun stat(self: StatusController, args: LuaThread.ArgStack): Int { + args.lua.push(self.effectiveStats[args.nextString()]?.effectiveModifiedValue ?: 0.0) + return 1 +} + +private fun statPositive(self: StatusController, args: LuaThread.ArgStack): Int { + args.lua.push(self.statPositive(args.nextString())) + return 1 +} + +private fun resourceNames(self: StatusController, args: LuaThread.ArgStack): Int { + args.lua.pushTable(self.resources.size) + + for ((i, value) in self.resources.keys.withIndex()) + args.lua.setTableValue(i + 1, value) + + return 1 +} + +private fun isResource(self: StatusController, args: LuaThread.ArgStack): Int { + args.lua.push(args.nextString() in self.resources) + return 1 +} + +private fun resource(self: StatusController, args: LuaThread.ArgStack): Int { + val resource = self.resources[args.nextString()] ?: return 0 + args.lua.push(resource.value) + return 1 +} + +private fun resourcePositive(self: StatusController, args: LuaThread.ArgStack): Int { + val resource = self.resources[args.nextString()] + + if (resource == null) + args.lua.push(false) + else + args.lua.push(resource.isPositive) + + return 1 +} + +private fun setResource(self: StatusController, args: LuaThread.ArgStack): Int { + val name = args.nextString() + val resource = self.resources[name] ?: throw NoSuchElementException("No such resource $name") + resource.value = args.nextDouble() + return 0 +} + +private fun modifyResource(self: StatusController, args: LuaThread.ArgStack): Int { + val name = args.nextString() + val resource = self.resources[name] ?: throw NoSuchElementException("No such resource $name") + resource.value += args.nextDouble() + return 0 +} + +private fun giveResource(self: StatusController, args: LuaThread.ArgStack): Int { + val name = args.nextString() + val resource = self.resources[name] + val amount = args.nextDouble() + + if (resource == null) { + args.lua.push(0.0) + } else { + args.lua.push(resource.give(amount)) + } + + return 1 +} + +private fun consumeResource(self: StatusController, overdraw: Boolean, args: LuaThread.ArgStack): Int { + val name = args.nextString() + val resource = self.resources[name] + val amount = args.nextDouble() + + if (resource == null) { + args.lua.push(false) + } else { + args.lua.push(resource.consume(amount, overdraw)) + } + + return 1 +} + +private fun resourceLocked(self: StatusController, args: LuaThread.ArgStack): Int { + val name = args.nextString() + val resource = self.resources[name] ?: throw NoSuchElementException("No such resource $name") + args.lua.push(resource.isLocked) + return 1 +} + +private fun setResourceLocked(self: StatusController, args: LuaThread.ArgStack): Int { + val name = args.nextString() + val resource = self.resources[name] ?: throw NoSuchElementException("No such resource $name") + resource.isLocked = args.nextBoolean() + return 0 +} + +private fun resetResource(self: StatusController, args: LuaThread.ArgStack): Int { + val name = args.nextString() + val resource = self.resources[name] ?: throw NoSuchElementException("No such resource $name") + resource.reset() + return 0 +} + +private fun resetAllResources(self: StatusController, args: LuaThread.ArgStack): Int { + self.resetResources() + return 0 +} + +private fun resourceMax(self: StatusController, args: LuaThread.ArgStack): Int { + val name = args.nextString() + val resource = self.resources[name] ?: throw NoSuchElementException("No such resource $name") + args.lua.push(resource.maxValue) + return 1 +} + +private fun resourcePercentage(self: StatusController, args: LuaThread.ArgStack): Int { + val name = args.nextString() + val resource = self.resources[name] ?: throw NoSuchElementException("No such resource $name") + args.lua.push(resource.percentage) + return 1 +} + +private fun setResourcePercentage(self: StatusController, args: LuaThread.ArgStack): Int { + val name = args.nextString() + val resource = self.resources[name] ?: throw NoSuchElementException("No such resource $name") + resource.setAsPercentage(args.nextDouble()) + return 0 +} + +private fun modifyResourcePercentage(self: StatusController, args: LuaThread.ArgStack): Int { + val name = args.nextString() + val resource = self.resources[name] ?: throw NoSuchElementException("No such resource $name") + resource.modifyPercentage(args.nextDouble()) + return 0 +} + +private fun getPersistentEffects(self: StatusController, args: LuaThread.ArgStack): Int { + val effects = self.getPersistentEffects(args.nextString()) + args.lua.pushTable(effects.size) + + for ((i, effect) in effects.withIndex()) { + args.lua.push(i + 1L) + effect.map({ args.lua.push(Starbound.gson.toJsonTree(it)) }, { args.lua.push(it) }) + args.lua.setTableValue() + } + + return 1 +} + +private fun addPersistentEffect(self: StatusController, args: LuaThread.ArgStack): Int { + val category = args.nextString().sbIntern() + val effect = Starbound.gson.fromJsonFast(args.nextJson(), PersistentStatusEffectToken) + self.addPersistentEffect(category, effect) + return 0 +} + +private fun addPersistentEffects(self: StatusController, args: LuaThread.ArgStack): Int { + val category = args.nextString().sbIntern() + val effects = Starbound.gson.fromJsonFast(args.nextJson(), CPersistentStatusEffectToken) + self.addPersistentEffects(category, effects) + return 0 +} + +private fun setPersistentEffects(self: StatusController, args: LuaThread.ArgStack): Int { + val category = args.nextString().sbIntern() + val effects = Starbound.gson.fromJsonFast(args.nextJson(), CPersistentStatusEffectToken) + self.setPersistentEffects(category, effects) + return 0 +} + +private fun clearPersistentEffects(self: StatusController, args: LuaThread.ArgStack): Int { + val category = args.nextString() + self.removePersistentEffects(category) + return 0 +} + +private fun clearAllPersistentEffects(self: StatusController, args: LuaThread.ArgStack): Int { + self.removeAllPersistentEffects() + return 0 +} + +private fun addEphemeralEffect(self: StatusController, args: LuaThread.ArgStack): Int { + val name = args.nextString() + val duration = args.nextOptionalDouble() + val sourceEntity = args.nextOptionalInt() + + self.addEphemeralEffect(EphemeralStatusEffect(Registries.statusEffects.ref(name), duration), sourceEntity) + return 0 +} + +private fun addEphemeralEffects(self: StatusController, args: LuaThread.ArgStack): Int { + val effects = args.readTableValues { Starbound.gson.fromJsonFast(getJson() ?: throw IllegalArgumentException("invalid values in effects table"), EphemeralStatusEffect::class.java) } + val sourceEntity = args.nextOptionalInt() + + for (effect in effects) { + self.addEphemeralEffect(effect, sourceEntity) + } + + return 0 +} + +private fun removeEphemeralEffect(self: StatusController, args: LuaThread.ArgStack): Int { + self.removeEphemeralEffect(args.nextString()) + return 0 +} + +private fun removeEphemeralEffects(self: StatusController, args: LuaThread.ArgStack): Int { + self.removeEphemeralEffects() + return 0 +} + +private fun damageTakenSince(self: StatusController, args: LuaThread.ArgStack): Int { + val since = args.nextOptionalLong() ?: 0L + val (list, newSince) = self.recentDamageReceived(since) + + args.lua.pushTable(list.size) + + for ((i, v) in list.withIndex()) { + args.lua.setTableValue(i + 1L, Starbound.gson.toJsonTree(v)) + } + + args.lua.push(newSince) + return 2 +} + +private fun inflictedHitsSince(self: StatusController, args: LuaThread.ArgStack): Int { + val since = args.nextOptionalLong() ?: 0L + val (list, newSince) = self.recentHitsDealt(since) + + args.lua.pushTable(list.size) + + for ((i, v) in list.withIndex()) { + val (target, data) = v + + args.lua.push(i + 1L) + + args.lua.push(Starbound.gson.toJsonTree(data)) + args.lua.setTableValue("targetEntityId", target) + + args.lua.setTableValue() + } + + args.lua.push(newSince) + return 2 +} + +private fun inflictedDamageSince(self: StatusController, args: LuaThread.ArgStack): Int { + val since = args.nextOptionalLong() ?: 0L + val (list, newSince) = self.recentHitsDealt(since) + + args.lua.pushTable(list.size) + + for ((i, v) in list.withIndex()) { + args.lua.setTableValue(i + 1L, Starbound.gson.toJsonTree(v)) + } + + args.lua.push(newSince) + return 2 +} + +private fun activeUniqueStatusEffectSummary(self: StatusController, args: LuaThread.ArgStack): Int { + val effects = self.activeUniqueStatusEffectSummary() + + args.lua.pushTable(effects.size) + + for ((i, data) in effects.withIndex()) { + args.lua.push(i + 1L) + args.lua.pushTable(2) + args.lua.setTableValue(1L, data.first.key) + args.lua.setTableValue(2L, data.second) + args.lua.setTableValue() + } + + return 1 +} + +private fun uniqueStatusEffectActive(self: StatusController, args: LuaThread.ArgStack): Int { + args.lua.push(self.uniqueStatusEffectActive(args.nextString())) + return 1 +} + +private fun primaryDirectives(self: StatusController, args: LuaThread.ArgStack): Int { + args.lua.push(self.primaryDirectives) + return 1 +} + +private fun setPrimaryDirectives(self: StatusController, args: LuaThread.ArgStack): Int { + self.primaryDirectives = args.nextOptionalString()?.sbIntern() ?: "" + return 0 +} + +private fun applySelfDamageRequest(self: StatusController, args: LuaThread.ArgStack): Int { + self.inflictSelfDamage(Starbound.gson.fromJsonFast(args.nextJson(), DamageData::class.java)) + return 1 +} + +private fun appliesEnvironmentStatusEffects(self: StatusController, args: LuaThread.ArgStack): Int { args.lua.push(self.appliesEnvironmentStatusEffects); return 1 } +private fun appliesWeatherStatusEffects(self: StatusController, args: LuaThread.ArgStack): Int { args.lua.push(self.appliesWeatherStatusEffects); return 1 } +private fun minimumLiquidStatusEffectPercentage(self: StatusController, args: LuaThread.ArgStack): Int { args.lua.push(self.minimumLiquidStatusEffectPercentage); return 1 } + +private fun setAppliesEnvironmentStatusEffects(self: StatusController, args: LuaThread.ArgStack): Int { self.appliesEnvironmentStatusEffects = args.nextBoolean(); return 0 } +private fun setAppliesWeatherStatusEffects(self: StatusController, args: LuaThread.ArgStack): Int { self.appliesWeatherStatusEffects = args.nextBoolean(); return 0 } +private fun setMinimumLiquidStatusEffectPercentage(self: StatusController, args: LuaThread.ArgStack): Int { self.minimumLiquidStatusEffectPercentage = args.nextDouble(); return 0 } + +fun provideStatusControllerBindings(self: StatusController, lua: LuaThread) { + lua.pushTable() + lua.dup() + lua.storeGlobal("status") + + lua.pushBinding(self, "statusProperty", ::statusProperty) + lua.pushBinding(self, "setStatusProperty", ::setStatusProperty) + lua.pushBinding(self, "stat", ::stat) + lua.pushBinding(self, "statPositive", ::statPositive) + lua.pushBinding(self, "resourceNames", ::resourceNames) + lua.pushBinding(self, "isResource", ::isResource) + lua.pushBinding(self, "resource", ::resource) + lua.pushBinding(self, "resourcePositive", ::resourcePositive) + lua.pushBinding(self, "setResource", ::setResource) + lua.pushBinding(self, "modifyResource", ::modifyResource) + lua.pushBinding(self, "giveResource", ::giveResource) + lua.pushBinding(self, false, "consumeResource", ::consumeResource) + lua.pushBinding(self, true, "overConsumeResource", ::consumeResource) + lua.pushBinding(self, "resourceLocked", ::resourceLocked) + lua.pushBinding(self, "setResourceLocked", ::setResourceLocked) + lua.pushBinding(self, "resetResource", ::resetResource) + lua.pushBinding(self, "resetAllResources", ::resetAllResources) + lua.pushBinding(self, "resourceMax", ::resourceMax) + lua.pushBinding(self, "resourcePercentage", ::resourcePercentage) + lua.pushBinding(self, "setResourcePercentage", ::setResourcePercentage) + lua.pushBinding(self, "modifyResourcePercentage", ::modifyResourcePercentage) + lua.pushBinding(self, "getPersistentEffects", ::getPersistentEffects) + lua.pushBinding(self, "addPersistentEffect", ::addPersistentEffect) + lua.pushBinding(self, "addPersistentEffects", ::addPersistentEffects) + lua.pushBinding(self, "setPersistentEffects", ::setPersistentEffects) + lua.pushBinding(self, "clearPersistentEffects", ::clearPersistentEffects) + lua.pushBinding(self, "clearAllPersistentEffects", ::clearAllPersistentEffects) + lua.pushBinding(self, "addEphemeralEffect", ::addEphemeralEffect) + lua.pushBinding(self, "addEphemeralEffects", ::addEphemeralEffects) + lua.pushBinding(self, "removeEphemeralEffect", ::removeEphemeralEffect) + lua.pushBinding(self, "removeEphemeralEffects", ::removeEphemeralEffects) + lua.pushBinding(self, "damageTakenSince", ::damageTakenSince) + lua.pushBinding(self, "inflictedHitsSince", ::inflictedHitsSince) + lua.pushBinding(self, "inflictedDamageSince", ::inflictedDamageSince) + lua.pushBinding(self, "activeUniqueStatusEffectSummary", ::activeUniqueStatusEffectSummary) + lua.pushBinding(self, "uniqueStatusEffectActive", ::uniqueStatusEffectActive) + lua.pushBinding(self, "primaryDirectives", ::primaryDirectives) + lua.pushBinding(self, "setPrimaryDirectives", ::setPrimaryDirectives) + lua.pushBinding(self, "applySelfDamageRequest", ::applySelfDamageRequest) + lua.pushBinding(self, "appliesEnvironmentStatusEffects", ::appliesEnvironmentStatusEffects) + lua.pushBinding(self, "appliesWeatherStatusEffects", ::appliesWeatherStatusEffects) + lua.pushBinding(self, "minimumLiquidStatusEffectPercentage", ::minimumLiquidStatusEffectPercentage) + lua.pushBinding(self, "setAppliesEnvironmentStatusEffects", ::setAppliesEnvironmentStatusEffects) + lua.pushBinding(self, "setAppliesWeatherStatusEffects", ::setAppliesWeatherStatusEffects) + lua.pushBinding(self, "setMinimumLiquidStatusEffectPercentage", ::setMinimumLiquidStatusEffectPercentage) + + lua.pop() } 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 36bfbffb..76a8f80d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt @@ -4,19 +4,13 @@ import com.google.gson.JsonElement import com.google.gson.JsonNull import com.google.gson.internal.bind.TypeAdapters import com.google.gson.stream.JsonWriter -import org.classdump.luna.ByteString -import org.classdump.luna.runtime.ExecutionContext import ru.dbotthepony.kommons.util.XXHash32 import ru.dbotthepony.kommons.util.XXHash64 import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters import ru.dbotthepony.kstarbound.json.JsonPath -import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.lua.LuaThread import ru.dbotthepony.kstarbound.lua.LuaType -import ru.dbotthepony.kstarbound.lua.from -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.userdata.LuaPerlinNoise import ru.dbotthepony.kstarbound.lua.userdata.LuaRandom import ru.dbotthepony.kstarbound.util.SBPattern @@ -47,7 +41,7 @@ private fun replaceTags(args: LuaThread.ArgStack): Int { private fun hash32(args: LuaThread.ArgStack): Int { val digest = XXHash32(2938728349.toInt()) - while (args.hasNext()) { + while (args.hasNext) { when (args.peek()) { LuaType.BOOLEAN -> digest.update(if (args.nextBoolean()) 1 else 0) LuaType.NUMBER -> toBytes(digest::update, args.nextDouble()) @@ -62,7 +56,7 @@ private fun hash32(args: LuaThread.ArgStack): Int { private fun hash64(args: LuaThread.ArgStack): Long { val digest = XXHash64(1997293021376312589L) - while (args.hasNext()) { + while (args.hasNext) { when (args.peek()) { LuaType.BOOLEAN -> digest.update(if (args.nextBoolean()) 1 else 0) LuaType.NUMBER -> toBytes(digest::update, args.nextDouble()) @@ -329,17 +323,34 @@ fun provideUtilityBindings(lua: LuaThread) { lua.pop() } -fun provideConfigBindings(lua: LuaEnvironment, lookup: ExecutionContext.(path: JsonPath, ifMissing: Any?) -> Any?) { - val config = lua.newTable() - lua.globals["config"] = config - config["getParameter"] = createConfigBinding(lookup) +private typealias ConfigLookup = (path: JsonPath) -> JsonElement? + +private fun lookupConfigValue(self: ConfigLookup, args: LuaThread.ArgStack): Int { + val parameter = args.nextString() + val lookup = self(JsonPath.query(parameter)) + + if (lookup == null) { + // TODO: while this is considerably faster, it does not correspond to original engine behavior where "default value" is always copied (lua -> json -> lua) + if (args.top == 1) { + args.lua.push() + } else if (args.top > 2) { + args.lua.dup(2) + } + } else { + args.lua.push(lookup) + } + + return 1 } -fun createConfigBinding(lookup: ExecutionContext.(path: JsonPath, ifMissing: Any?) -> Any?) = luaFunction { name: ByteString, default: Any? -> - val get = lookup(this, JsonPath.query(name.decode()), default) - - if (get is JsonElement) - returnBuffer.setTo(from(get)) - else - returnBuffer.setTo(get) +fun provideConfigBinding(lua: LuaThread, lookup: ConfigLookup) { + lua.pushTable() + lua.dup() + lua.storeGlobal("config") + lua.pushBinding(lookup, "getParameter", ::lookupConfigValue) + lua.pop() +} + +fun createConfigBinding(lua: LuaThread, lookup: ConfigLookup) { + lua.push { lookupConfigValue(lookup, it) } } 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 1911fb12..f564374a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt @@ -522,10 +522,10 @@ private fun getObjectParameter(self: World<*, *>, args: LuaThread.ArgStack): Int // FIXME: this is stupid (defaultValue is ignored when we lookup parameter on non existing entity), // but we must retain original behavior val entity = self.entities[id] as? WorldObject ?: return 0 - val result = entity.lookupProperty(parameter) + val result = entity.lookupPropertyOrNull(parameter) - if (result.isJsonNull) { - // TODO: while this is faster, it does not correspond to original engine behavior where "default value" is always copied (lua -> json -> lua) + if (result == null) { + // TODO: while this is considerably faster, it does not correspond to original engine behavior where "default value" is always copied (lua -> json -> lua) if (args.top == 2) { args.lua.push() } else if (args.top != 3) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt index 56e75fd8..a93df626 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt @@ -24,7 +24,7 @@ import ru.dbotthepony.kstarbound.world.tileAreaBrush import java.util.concurrent.CompletableFuture private fun damageTilesImpl(self: World<*, *>, args: LuaThread.ArgStack): CompletableFuture { - val positions = args.readTableValues { getVector2i() ?: throw IllegalArgumentException("Positions table contains invalid positions") } + val positions = args.readTableValues { getVector2i(it) ?: throw IllegalArgumentException("Positions table contains invalid positions") } val isBackground = args.nextBoolean() val sourcePosition = args.nextVector2d() 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 dae75a4b..2c7cd805 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt @@ -1,255 +1,363 @@ package ru.dbotthepony.kstarbound.lua.bindings -import com.google.common.collect.ImmutableList import com.google.gson.JsonNull import com.google.gson.JsonPrimitive -import org.classdump.luna.ByteString -import org.classdump.luna.LuaRuntimeException -import org.classdump.luna.Table +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.DamageSource -import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor -import ru.dbotthepony.kstarbound.defs.quest.QuestDescriptor +import ru.dbotthepony.kstarbound.fromJsonFast import ru.dbotthepony.kstarbound.json.JsonPath -import ru.dbotthepony.kstarbound.lua.LuaEnvironment -import ru.dbotthepony.kstarbound.lua.from -import ru.dbotthepony.kstarbound.lua.get -import ru.dbotthepony.kstarbound.lua.indexNoYield -import ru.dbotthepony.kstarbound.lua.iterator -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.tableOf -import ru.dbotthepony.kstarbound.lua.toByteString -import ru.dbotthepony.kstarbound.lua.toColor -import ru.dbotthepony.kstarbound.lua.toJson -import ru.dbotthepony.kstarbound.lua.toJsonFromLua -import ru.dbotthepony.kstarbound.lua.toVector2d -import ru.dbotthepony.kstarbound.lua.toVector2i +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.lua.LuaType +import ru.dbotthepony.kstarbound.lua.getVector2i +import ru.dbotthepony.kstarbound.lua.nextColor +import ru.dbotthepony.kstarbound.lua.nextVector2d +import ru.dbotthepony.kstarbound.lua.push +import ru.dbotthepony.kstarbound.lua.setTableValue import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.sbIntern import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject +import kotlin.collections.forEach +import kotlin.collections.isNotEmpty +import kotlin.collections.set +import kotlin.collections.withIndex -fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) { - val config = lua.newTable() - lua.globals["config"] = config - - config["getParameter"] = luaFunction { name: ByteString, default: Any? -> - val path = JsonPath.query(name.decode()) - val find = self.lookupProperty(path) { JsonNull.INSTANCE } - - if (find.isJsonNull) { - returnBuffer.setTo(default) - } else { - returnBuffer.setTo(from(find)) - } - } - - val table = lua.newTable() - lua.globals["object"] = table - - table["name"] = luaFunction { returnBuffer.setTo(self.config.key.toByteString()) } - table["direction"] = luaFunction { returnBuffer.setTo(self.direction.numericalValue) } - table["position"] = luaFunction { returnBuffer.setTo(from(self.tilePosition)) } - table["setInteractive"] = luaFunction { interactive: Boolean -> self.isInteractive = interactive } - table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get().toByteString()) } - table["setUniqueId"] = luaFunction { id: ByteString? -> self.uniqueID.accept(id?.decode()?.sbIntern()) } - table["boundBox"] = luaFunction { returnBuffer.setTo(from(self.metaBoundingBox)) } - - // original engine parity, it returns occupied spaces in local coordinates - table["spaces"] = luaFunction { returnBuffer.setTo(tableOf(*self.occupySpaces.map { from(it - self.tilePosition) }.toTypedArray())) } - - table["setProcessingDirectives"] = luaFunction { directives: ByteString -> self.animator.processingDirectives = directives.decode() } - table["setSoundEffectEnabled"] = luaFunction { state: Boolean -> self.soundEffectEnabled = state } - - table["smash"] = luaFunction { smash: Boolean? -> - if (smash == true) { - self.health = 0.0 - } - - self.remove(AbstractEntity.RemovalReason.DYING) - } - - table["level"] = luaFunction { returnBuffer.setTo(self.lookupProperty(JsonPath("level")) { JsonPrimitive(self.world.template.threatLevel) }.asDouble) } - table["toAbsolutePosition"] = luaFunction { pos: Table -> returnBuffer.setTo(from(toVector2d(pos) + self.position)) } - - table["say"] = luaFunction { line: ByteString, tags: Table?, sayConfig: Table? -> - if (tags == null) { - if (line.isEmpty) { - returnBuffer.setTo(false) - } else { - self.addChatMessage(line.decode(), sayConfig?.toJson() ?: JsonNull.INSTANCE) - returnBuffer.setTo(true) - } - } else { - if (line.isEmpty) { - returnBuffer.setTo(false) - } else { - self.addChatMessage(SBPattern.of(line.decode()).resolveOrSkip({ tags[it]?.toString() }), sayConfig?.toJson() ?: JsonNull.INSTANCE) - returnBuffer.setTo(true) - } - } - } - - table["sayPortrait"] = luaFunction { line: ByteString, portrait: ByteString, tags: Table?, config: Table -> - if (tags == null) { - if (line.isEmpty) { - returnBuffer.setTo(false) - } else { - self.addChatMessage(line.decode(), config.toJson(), portrait.decode()) - returnBuffer.setTo(true) - } - } else { - if (line.isEmpty) { - returnBuffer.setTo(false) - } else { - self.addChatMessage(SBPattern.of(line.decode()).resolveOrSkip({ tags[it]?.toString() }), config.toJson(), portrait.decode()) - returnBuffer.setTo(true) - } - } - } - - table["isTouching"] = luaFunction { entity: Number -> - val find = self.world.entities[entity] - - if (find != null) { - returnBuffer.setTo(find.collisionArea.intersect(self.volumeBoundingBox)) - } else { - returnBuffer.setTo(false) - } - } - - table["setLightColor"] = luaFunction { color: Table -> - self.lightSourceColor = toColor(color) - } - - table["getLightColor"] = luaFunction { - returnBuffer.setTo(from(self.lightSourceColor)) - } - - table["inputNodeCount"] = luaFunction { returnBuffer.setTo(self.inputNodes.size.toLong()) } - table["outputNodeCount"] = luaFunction { returnBuffer.setTo(self.outputNodes.size.toLong()) } - - table["getInputNodePosition"] = luaFunction { index: Long -> - returnBuffer.setTo(from(self.inputNodes[index.toInt()].position)) - } - - table["getOutputNodePosition"] = luaFunction { index: Long -> - returnBuffer.setTo(from(self.outputNodes[index.toInt()].position)) - } - - table["getInputNodeLevel"] = luaFunction { index: Long -> - returnBuffer.setTo(self.inputNodes[index.toInt()].state) - } - - table["getOutputNodeLevel"] = luaFunction { index: Long -> - returnBuffer.setTo(self.outputNodes[index.toInt()].state) - } - - table["isInputNodeConnected"] = luaFunction { index: Long -> - returnBuffer.setTo(self.inputNodes[index.toInt()].connections.isNotEmpty()) - } - - table["isOutputNodeConnected"] = luaFunction { index: Long -> - returnBuffer.setTo(self.outputNodes[index.toInt()].connections.isNotEmpty()) - } - - table["getInputNodeIds"] = luaFunction { index: Long -> - val results = newTable() - - for (connection in self.inputNodes[index.toInt()].connections) { - val entity = self.world.entityIndex.tileEntityAt(connection.entityLocation, WorldObject::class) - - if (entity != null) { - results[entity.entityID] = connection.index - } - } - - returnBuffer.setTo(results) - } - - table["getOutputNodeIds"] = luaFunction { index: Long -> - val results = newTable() - - for (connection in self.outputNodes[index.toInt()].connections) { - val entity = self.world.entityIndex.tileEntityAt(connection.entityLocation, WorldObject::class) - - if (entity != null) { - results[entity.entityID] = connection.index - } - } - - returnBuffer.setTo(results) - } - - table["setOutputNodeLevel"] = luaFunction { index: Long, state: Boolean -> - self.outputNodes[index.toInt()].state = state - } - - table["setAllOutputNodes"] = luaFunction { state: Boolean -> - self.outputNodes.forEach { it.state = state } - } - - table["setOfferedQuests"] = luaFunction { quests: Table? -> - self.offeredQuests.clear() - - if (quests != null) { - for ((_, v) in quests) { - if (v is Table) { - self.offeredQuests.add(Starbound.gson.fromJson(v.toJson(), QuestArcDescriptor::class.java)) - } else if (v is ByteString) { - self.offeredQuests.add(QuestArcDescriptor(ImmutableList.of(QuestDescriptor(v.decode())))) - } else { - throw LuaRuntimeException("Unknown quest arc descriptor type: $v") - } - } - } - } - - table["setTurnInQuests"] = luaFunction { quests: Table? -> - self.turnInQuests.clear() - - if (quests != null) { - for ((_, v) in quests) { - self.turnInQuests.add((v as ByteString).decode()) - } - } - } - - table["setConfigParameter"] = luaFunction { key: ByteString, value: Any? -> - self.parameters[key.decode()] = toJsonFromLua(value) - } - - table["setAnimationParameter"] = luaFunction { key: ByteString, value: Any? -> - self.scriptedAnimationParameters[key.decode()] = toJsonFromLua(value) - } - - table["setMaterialSpaces"] = luaFunction { spaces: Table? -> - self.declaredMaterialSpaces.clear() - - if (spaces != null) { - for ((i, pair) in spaces) { - pair as Table - - val position = toVector2i(indexNoYield(pair, 1L) ?: throw NullPointerException("invalid space at $i")) - val material = indexNoYield(pair, 2L) as? ByteString ?: throw NullPointerException("invalid space at $i") - - self.declaredMaterialSpaces.add(position to Registries.tiles.ref(material.decode())) - } - } - } - - table["setDamageSources"] = luaFunction { sources: Table? -> - self.customDamageSources.clear() - - if (sources != null) { - for ((_, v) in sources) { - self.customDamageSources.add(Starbound.gson.fromJson((v as Table).toJson(), DamageSource::class.java)) - } - } - } - - table["health"] = luaFunction { returnBuffer.setTo(self.health) } - table["setHealth"] = luaFunction { health: Double -> self.health = health } +private fun name(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.config.key) + return 1 +} + +private fun directions(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.direction.numericalValue) + return 1 +} + +private fun position(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.tilePosition) + return 1 +} + +private fun setInteractive(self: WorldObject, args: LuaThread.ArgStack): Int { + self.isInteractive = args.nextBoolean() + return 0 +} + +private fun uniqueId(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.uniqueID.get()) + return 1 +} + +private fun setUniqueId(self: WorldObject, args: LuaThread.ArgStack): Int { + self.uniqueID.accept(args.nextOptionalString()?.sbIntern()) + return 0 +} + +private fun boundBox(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.metaBoundingBox) + return 1 +} + +// original engine parity, it returns occupied spaces in local coordinates +private fun spaces(self: WorldObject, local: Boolean, args: LuaThread.ArgStack): Int { + val spaces = self.occupySpaces + val spos = self.tilePosition + + args.lua.pushTable(spaces.size) + + if (local) { + for ((i, pos) in spaces.withIndex()) { + args.lua.setTableValue(i + 1, pos - spos) + } + } else { + for ((i, pos) in spaces.withIndex()) { + args.lua.setTableValue(i + 1, pos) + } + } + + return 1 +} + +private fun setProcessingDirectives(self: WorldObject, args: LuaThread.ArgStack): Int { + self.animator.processingDirectives = args.nextString().sbIntern() + return 0 +} + +private fun setSoundEffectEnabled(self: WorldObject, args: LuaThread.ArgStack): Int { + self.soundEffectEnabled = args.nextBoolean() + return 0 +} + +private fun smash(self: WorldObject, args: LuaThread.ArgStack): Int { + if (args.nextOptionalBoolean() == true) { + self.health = 0.0 + } + + self.remove(AbstractEntity.RemovalReason.DYING) + return 0 +} + +private fun level(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.lookupProperty(JsonPath("level")) { JsonPrimitive(self.world.template.threatLevel) }.asDouble) + return 1 +} + +private fun toAbsolutePosition(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(args.nextVector2d() + self.position) + return 1 +} + +private fun say(self: WorldObject, hasPortrait: Boolean, args: LuaThread.ArgStack): Int { + val line = args.nextString() + val portrait = if (hasPortrait) args.nextString() else null + val tags = Object2ObjectArrayMap() + val tagsType = args.peek() + + if (tagsType == LuaType.TABLE) { + val pairs = args.readTable( + keyVisitor = { getString(it) ?: throw IllegalArgumentException("Tag replacement table contains non-strings as keys") }, + valueVisitor = { getString(it) ?: throw IllegalArgumentException("Tag replacement table contains non-strings as keys") }, + ) + + for ((k, v) in pairs) { + tags[k] = v + } + } else if (!tagsType.isNothing) { + throw IllegalArgumentException("bad argument #2 to object.say: table expected, got $tagsType") + } else { + args.skip() + } + + val sayConfig = args.nextOptionalJson() ?: JsonNull.INSTANCE + + if (line.isBlank()) { + args.lua.push(false) + } else if (tags.isEmpty()) { + self.addChatMessage(line, sayConfig, portrait = portrait) + args.lua.push(true) + } else { + self.addChatMessage(SBPattern.of(line).resolveOrSkip(tags::get), sayConfig, portrait = portrait) + args.lua.push(true) + } + + return 1 +} + +private fun isTouching(self: WorldObject, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.world.entities[id] + + if (entity == null) { + args.lua.push(false) + } else { + args.lua.push(entity.collisionArea.intersect(self.volumeBoundingBox)) + } + + return 1 +} + +private fun setLightColor(self: WorldObject, args: LuaThread.ArgStack): Int { + self.lightSourceColor = args.nextColor() + return 0 +} + +private fun getLightColor(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.lightSourceColor) + return 1 +} + +private fun inputNodeCount(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.inputNodes.size.toLong()) + return 1 +} + +private fun outputNodeCount(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.outputNodes.size.toLong()) + return 1 +} + +private fun getInputNodePosition(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.inputNodes[args.nextInt()].position) + return 1 +} + +private fun getOutputNodePosition(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.outputNodes[args.nextInt()].position) + return 1 +} + +private fun getInputNodeLevel(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.inputNodes[args.nextInt()].state) + return 1 +} + +private fun getOutputNodeLevel(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.outputNodes[args.nextInt()].state) + return 1 +} + +private fun isInputNodeConnected(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.inputNodes[args.nextInt()].connections.isNotEmpty()) + return 1 +} + +private fun isOutputNodeConnected(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.outputNodes[args.nextInt()].connections.isNotEmpty()) + return 1 +} + +private fun getInputNodeIds(self: WorldObject, args: LuaThread.ArgStack): Int { + val connections = self.inputNodes[args.nextInt()].connections + + args.lua.pushTable(connections.size) + + for (connection in connections) { + val entity = self.world.entityIndex.tileEntityAt(connection.entityLocation, WorldObject::class) + + if (entity != null) { + args.lua.setTableValue(entity.entityID, connection.index) + } + } + + return 1 +} + +private fun getOutputNodeIds(self: WorldObject, args: LuaThread.ArgStack): Int { + val connections = self.outputNodes[args.nextInt()].connections + + args.lua.pushTable(connections.size) + + for (connection in connections) { + val entity = self.world.entityIndex.tileEntityAt(connection.entityLocation, WorldObject::class) + + if (entity != null) { + args.lua.setTableValue(entity.entityID, connection.index) + } + } + + return 1 +} + +private fun setOutputNodeLevel(self: WorldObject, args: LuaThread.ArgStack): Int { + val index = args.nextInt() + val state = args.nextBoolean() + self.outputNodes[index].state = state + return 0 +} + +private fun setAllOutputNodes(self: WorldObject, args: LuaThread.ArgStack): Int { + val state = args.nextBoolean() + self.outputNodes.forEach { it.state = state } + return 0 +} + +private fun setConfigParameter(self: WorldObject, args: LuaThread.ArgStack): Int { + val key = args.nextString().sbIntern() + val value = args.nextJson() + self.parameters[key] = value + return 0 +} + +private fun setAnimationParameter(self: WorldObject, args: LuaThread.ArgStack): Int { + val key = args.nextString().sbIntern() + val value = args.nextJson() + self.scriptedAnimationParameters[key] = value + return 0 +} + +private fun setMaterialSpaces(self: WorldObject, args: LuaThread.ArgStack): Int { + self.declaredMaterialSpaces.clear() + + if (args.hasNext) { + val pairs = args.readTableValues { tI -> + push(1L) + val vType = loadTableValue(tI) + val position = getVector2i() ?: throw IllegalArgumentException("invalid space pair, first value is not a vector: $vType") + + push(2L) + val mType = loadTableValue(tI) + val material = getString() ?: throw IllegalArgumentException("invalid space pair, second value is not a string: $mType") + + position to Registries.tiles.ref(material) + } + + self.declaredMaterialSpaces.addAll(pairs) + } + + return 0 +} + +private fun setDamageSources(self: WorldObject, args: LuaThread.ArgStack): Int { + self.customDamageSources.clear() + + if (args.hasNext) { + val sources = args.readTableValues { + Starbound.gson.fromJsonFast(getJson(it)!!, DamageSource::class.java) + } + + self.customDamageSources.addAll(sources) + } + + return 0 +} + +private fun health(self: WorldObject, args: LuaThread.ArgStack): Int { + args.lua.push(self.health) + return 1 +} + +private fun setHealth(self: WorldObject, args: LuaThread.ArgStack): Int { + self.health = args.nextDouble() + return 0 +} + +fun provideWorldObjectBindings(self: WorldObject, lua: LuaThread) { + provideConfigBinding(lua, self::lookupPropertyOrNull) + + lua.pushTable() + lua.dup() + lua.storeGlobal("object") + + lua.pushBinding(self, "name", ::name) + lua.pushBinding(self, "directions", ::directions) + lua.pushBinding(self, "position", ::position) + lua.pushBinding(self, "setInteractive", ::setInteractive) + lua.pushBinding(self, "uniqueId", ::uniqueId) + lua.pushBinding(self, "setUniqueId", ::setUniqueId) + lua.pushBinding(self, "boundBox", ::boundBox) + lua.pushBinding(self, true, "spaces", ::spaces) + lua.pushBinding(self, false, "worldSpaces", ::spaces) + lua.pushBinding(self, "setProcessingDirectives", ::setProcessingDirectives) + lua.pushBinding(self, "setSoundEffectEnabled", ::setSoundEffectEnabled) + lua.pushBinding(self, "smash", ::smash) + lua.pushBinding(self, "level", ::level) + lua.pushBinding(self, "toAbsolutePosition", ::toAbsolutePosition) + lua.pushBinding(self, false, "say", ::say) + lua.pushBinding(self, true, "sayPortrait", ::say) + lua.pushBinding(self, "isTouching", ::isTouching) + lua.pushBinding(self, "setLightColor", ::setLightColor) + lua.pushBinding(self, "getLightColor", ::getLightColor) + lua.pushBinding(self, "inputNodeCount", ::inputNodeCount) + lua.pushBinding(self, "outputNodeCount", ::outputNodeCount) + lua.pushBinding(self, "getInputNodePosition", ::getInputNodePosition) + lua.pushBinding(self, "getOutputNodePosition", ::getOutputNodePosition) + lua.pushBinding(self, "getInputNodeLevel", ::getInputNodeLevel) + lua.pushBinding(self, "getOutputNodeLevel", ::getOutputNodeLevel) + lua.pushBinding(self, "isInputNodeConnected", ::isInputNodeConnected) + lua.pushBinding(self, "isOutputNodeConnected", ::isOutputNodeConnected) + lua.pushBinding(self, "getInputNodeIds", ::getInputNodeIds) + lua.pushBinding(self, "getOutputNodeIds", ::getOutputNodeIds) + lua.pushBinding(self, "setOutputNodeLevel", ::setOutputNodeLevel) + lua.pushBinding(self, "setAllOutputNodes", ::setAllOutputNodes) + lua.pushBinding(self, WorldObject::offeredQuests, "setOfferedQuests", ::setOfferedQuests) + lua.pushBinding(self, WorldObject::turnInQuests, "setTurnInQuests", ::setTurnInQuests) + lua.pushBinding(self, "setConfigParameter", ::setConfigParameter) + lua.pushBinding(self, "setAnimationParameter", ::setAnimationParameter) + lua.pushBinding(self, "setMaterialSpaces", ::setMaterialSpaces) + lua.pushBinding(self, "setDamageSources", ::setDamageSources) + lua.pushBinding(self, "health", ::health) + lua.pushBinding(self, "setHealth", ::setHealth) + + lua.pop() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/BehaviorNodeType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorNodeType.kt similarity index 80% rename from src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/BehaviorNodeType.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorNodeType.kt index 60b44120..d024d06e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/BehaviorNodeType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorNodeType.kt @@ -1,4 +1,4 @@ -package ru.dbotthepony.kstarbound.world.entities.behavior +package ru.dbotthepony.kstarbound.lua.userdata import ru.dbotthepony.kstarbound.json.builder.IStringSerializable 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 5cf51ce8..1b04311f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorState.kt @@ -1,126 +1,303 @@ package ru.dbotthepony.kstarbound.lua.userdata +import com.google.common.collect.ImmutableList +import com.google.gson.JsonArray import com.google.gson.JsonNull import com.google.gson.JsonObject -import org.classdump.luna.ByteString -import org.classdump.luna.LuaRuntimeException -import org.classdump.luna.Table -import org.classdump.luna.Userdata -import org.classdump.luna.impl.ImmutableTable -import org.classdump.luna.runtime.LuaFunction +import com.google.gson.JsonPrimitive +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.gson.stream import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorDefinition +import ru.dbotthepony.kstarbound.defs.actor.behavior.CompositeNodeType +import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeOutput +import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameter +import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameterValue import ru.dbotthepony.kstarbound.fromJsonFast import ru.dbotthepony.kstarbound.json.mergeJson -import ru.dbotthepony.kstarbound.lua.LuaEnvironment -import ru.dbotthepony.kstarbound.lua.get -import ru.dbotthepony.kstarbound.lua.iterator -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.tableMapOf -import ru.dbotthepony.kstarbound.lua.toJson +import ru.dbotthepony.kstarbound.lua.LuaHandle +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.lua.LuaType import ru.dbotthepony.kstarbound.lua.toJsonFromLua -import ru.dbotthepony.kstarbound.world.entities.behavior.AbstractBehaviorNode -import ru.dbotthepony.kstarbound.world.entities.behavior.BehaviorTree -import ru.dbotthepony.kstarbound.world.entities.behavior.Blackboard -import java.util.concurrent.atomic.AtomicLong +import ru.dbotthepony.kstarbound.util.valueOf -class BehaviorState(val tree: BehaviorTree) : Userdata() { - val blackboard get() = tree.blackboard - val lua get() = blackboard.lua - val functions = HashMap>() +private fun replaceBehaviorTag(parameter: NodeParameterValue, treeParameters: Map): NodeParameterValue { + var str: String? = null - init { - lua.attach(tree.scripts) + if (parameter.key != null) + str = parameter.key - for (name in tree.functions) { - val get = lua.globals[name] + // 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 - if (get == null) { - throw LuaRuntimeException("No such function for behavior: $name") - } else if (get !is LuaFunction<*, *, *, *, *>) { - throw LuaRuntimeException("Not a Lua function for behavior: $name") + if (!str.isNullOrEmpty()) { + if (str.first() == '<' && str.last() == '>') { + val treeKey = str.substring(1, str.length - 1) + val param = treeParameters[treeKey] + + if (param != null) { + return param } else { - this.functions[name] = get + throw NoSuchElementException("No parameter specified for tag '$treeKey'") } } } - fun run(delta: Double): AbstractBehaviorNode.Status { - val ephemerals = blackboard.takeEphemerals() - val status = tree.runAndReset(delta, this) - blackboard.clearEphemerals(ephemerals) - return status - } + return parameter +} - override fun getMetatable(): Table { - return Companion.metatable - } +private fun replaceOutputBehaviorTag(output: String?, treeParameters: Map): String? { + if (output == null) { + return null + } else if (output.first() == '<' && output.last() == '>') { + val replacement = treeParameters[output.substring(1, output.length - 1)] ?: throw NoSuchElementException("No parameter specified for tag '$output'") - override fun setMetatable(mt: Table?): Table { - throw UnsupportedOperationException() - } - - override fun getUserValue(): BehaviorState { - return this - } - - override fun setUserValue(value: BehaviorState?): BehaviorState? { - throw UnsupportedOperationException() - } - - companion object { - // required for Lua code - // in original engine, passes directly 32/64 bit pointer of *Node struct - val NODE_GARBAGE_INDEX = AtomicLong() - - private fun __index(): Table { - return metatable - } - - private val metatable = ImmutableTable.Builder() - .add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) }) - .add("run", luaFunction { self: BehaviorState, delta: Number -> - returnBuffer.setTo(self.run(delta.toDouble())) - }) - .add("clear", luaFunction { self: BehaviorState -> - self.tree.reset() - }) - .add("blackboard", luaFunction { self: BehaviorState -> - returnBuffer.setTo(self.blackboard) - }) - .build() - - fun provideBindings(lua: LuaEnvironment) { - lua.globals["behavior"] = lua.tableMapOf( - "behavior" to luaFunction { config: Any, parameters: Table, _: Any?, blackboard: Blackboard? -> - val tree: BehaviorTree - - if (config is ByteString) { - val base = Registries.behavior.getOrThrow(config.decode()) - - if (!parameters.iterator().hasNext()) { - tree = BehaviorTree(blackboard ?: Blackboard(lua), base.value) - } else { - tree = BehaviorTree(blackboard ?: Blackboard(lua), Starbound.gson.fromJsonFast(base.json.deepCopy().also { - it as JsonObject - it["parameters"] = mergeJson(it["parameters"] ?: JsonNull.INSTANCE, toJsonFromLua(parameters)) - }, BehaviorDefinition::class.java)) - } - } else { - val cast = (config as Table).toJson(true) as JsonObject - - if (parameters.iterator().hasNext()) - cast["parameters"] = mergeJson(cast["parameters"] ?: JsonNull.INSTANCE, toJsonFromLua(parameters)) - - tree = BehaviorTree(blackboard ?: Blackboard(lua), Starbound.gson.fromJsonFast(cast, BehaviorDefinition::class.java)) - } - - returnBuffer.setTo(BehaviorState(tree)) - } - ) - } + if (replacement.key != null) + return replacement.key + else if (replacement.value is JsonPrimitive && replacement.value.isString) + return replacement.value.asString + else + return null + } else { + return output } } + +@JvmName("pushInputs") +private fun push(lua: LuaThread, parameters: Map) { + lua.pushTable(hashSize = parameters.size) + + for ((k, v) in parameters) { + lua.push(k) + v.push(lua) + lua.setTableValue() + } +} + +@JvmName("pushOutputs") +private fun push(lua: LuaThread, parameters: Map) { + lua.pushTable(hashSize = parameters.size) + + for ((k, v) in parameters) { + lua.push(k) + v.push(lua) + lua.setTableValue() + } +} + +private fun createNode( + lua: LuaThread, + data: JsonObject, + treeParameters: Map, + blackboard: LuaHandle, + scripts: MutableSet, + functions: MutableSet, + handles: MutableList +): LuaHandle { + val type = BehaviorNodeType.entries.valueOf(data["type"].asString) + val name = data["name"].asString + val parameterConfig = data.get("parameters") { JsonObject() } + + if (type == BehaviorNodeType.MODULE) { + val base = Registries.behavior.getOrThrow(name).value + base.scripts.forEach { scripts.add(it.fullPath) } + + // merge in module parameters to a copy of the treeParameters to propagate + // tree parameters into the subtree, but allow modules to override + val mergedParameters = LinkedHashMap(base.mappedParameters) + mergedParameters.putAll(treeParameters) + + for ((k, v) in parameterConfig.entrySet()) { + mergedParameters[k] = replaceBehaviorTag( + Starbound.gson.fromJsonFast(v, NodeParameterValue::class.java), + treeParameters + ) + } + + return createNode(lua, base.root, mergedParameters, blackboard, scripts, functions, handles) + } + + val parameters = LinkedHashMap(Registries.behaviorNodes.getOrThrow(name).value.properties) + + for ((k, v) in parameters.entries) { + if (k in parameterConfig) { + parameters[k] = v.copy(value = replaceBehaviorTag( + Starbound.gson.fromJsonFast( + parameterConfig[k], + NodeParameterValue::class.java + ), treeParameters) + ) + } else { + val replaced = replaceBehaviorTag(v.value, treeParameters) + + if (replaced != v.value) { + parameters[k] = v.copy(value = replaced) + } + } + } + + when (type) { + BehaviorNodeType.ACTION -> { + functions.add(name) + + val outputConfig = data.get("output") { JsonObject() } + val output = LinkedHashMap(Registries.behaviorNodes.getOrThrow(name).value.output) + + for ((k, v) in output.entries) { + val replaced = replaceOutputBehaviorTag(outputConfig[k]?.asString ?: v.key, treeParameters) + + if (replaced != v.key) { + output[k] = v.copy(key = replaced) + } + } + + 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() + handles.add(handle) + lua.pop() + return handle + } + + BehaviorNodeType.DECORATOR -> { + 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() + + return handle + } + + BehaviorNodeType.COMPOSITE -> { + val children = data.get("children") { JsonArray() } + .stream() + .map { createNode(lua, it as JsonObject, treeParameters, blackboard, scripts, functions, handles) } + .collect(ImmutableList.toImmutableList()) + + val factory = CompositeNodeType.entries.valueOf(name) + + check(lua.loadGlobal(factory.fnName) == LuaType.FUNCTION) { "Global ${factory.fnName} is not a Lua function" } + push(lua, parameters) + + lua.pushTable(children.size) + + for ((i, child) in children.withIndex()) { + lua.push(i + 1L) + lua.push(child) + lua.setTableValue() + } + + lua.call(2, 1) + val handle = lua.createHandle() + handles.add(handle) + lua.pop() + return handle + } + + BehaviorNodeType.MODULE -> throw RuntimeException() + } +} + +private fun createBlackboard(lua: LuaThread): LuaHandle { + lua.loadGlobal("Blackboard") + lua.call(numResults = 1) + val handle = lua.createHandle() + lua.pop() + return handle +} + +private fun createBehaviorTree(args: LuaThread.ArgStack): Int { + val functions = HashSet() + val scripts = HashSet() + val mergedParams: BehaviorDefinition + val blackboard: LuaHandle + val handles = ArrayList() + + if (args.peek() == LuaType.STRING) { + val base = Registries.behavior.getOrThrow(args.nextString()) + val parameters = args.nextJson() as JsonObject + args.skip() + + if (args.peek().isNothing) { + blackboard = createBlackboard(args.lua) + } else { + // assume proper blackboard was given + args.lua.dup(args.position++) + blackboard = args.lua.createHandle() + args.lua.pop() + } + + if (parameters.size() == 0) { + mergedParams = base.value + } else { + mergedParams = Starbound.gson.fromJsonFast(base.json.deepCopy().also { + it as JsonObject + it["parameters"] = mergeJson(it["parameters"] ?: JsonNull.INSTANCE, toJsonFromLua(parameters)) + }, BehaviorDefinition::class.java) + } + } else { + val config = args.nextJson() as JsonObject + val parameters = args.nextJson() as JsonObject + args.skip() + + if (args.peek().isNothing) { + blackboard = createBlackboard(args.lua) + } else { + // assume proper blackboard was given + args.lua.dup(args.position++) + blackboard = args.lua.createHandle() + args.lua.pop() + } + + if (parameters.size() != 0) + config["parameters"] = mergeJson(config["parameters"] ?: JsonNull.INSTANCE, parameters) + + mergedParams = Starbound.gson.fromJsonFast(config, BehaviorDefinition::class.java) + } + + handles.add(blackboard) + + args.lua.ensureExtraCapacity(40) + + 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" } + + args.lua.push(blackboard) + args.lua.push(root) + + args.lua.call(2, 1) + + handles.forEach { it.close() } + return 1 +} + +private val script by lazy { LuaThread.loadInternalScript("behavior") } + +fun provideBehaviorBindings(lua: LuaThread) { + lua.pushTable() + lua.dup() + lua.storeGlobal("behavior") + + lua.setTableValue("behavior", ::createBehaviorTree) + + lua.pop() + + lua.load(script, "@/internal/behavior.lua") + lua.call() +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/NodeParameterType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/NodeParameterType.kt similarity index 87% rename from src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/NodeParameterType.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/NodeParameterType.kt index cb147b4a..f01125a2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/NodeParameterType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/NodeParameterType.kt @@ -1,4 +1,4 @@ -package ru.dbotthepony.kstarbound.world.entities.behavior +package ru.dbotthepony.kstarbound.lua.userdata import ru.dbotthepony.kstarbound.json.builder.IStringSerializable diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt index b7965b04..97ae5ee3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt @@ -37,5 +37,25 @@ enum class Direction(val normal: Vector2d, override val jsonName: String, val nu return RIGHT } } + + fun valueOf(value: Long): Direction? { + if (value == 0L) { + return null + } else if (value < 0L) { + return LEFT + } else { + return RIGHT + } + } + + fun valueOf(value: Double): Direction? { + if (value == 0.0) { + return null + } else if (value < 0.0) { + return LEFT + } else { + return RIGHT + } + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt index 48be6fc1..09e2753b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt @@ -693,12 +693,12 @@ class Animator() { return group in rotationGroups } - fun rotationGroups(): Collection { - return Collections.unmodifiableCollection(rotationGroups.keys) + fun rotationGroups(): Set { + return Collections.unmodifiableSet(rotationGroups.keys) } - fun transformationGroups(): Collection { - return Collections.unmodifiableCollection(transformationGroups.keys) + fun transformationGroups(): Set { + return Collections.unmodifiableSet(transformationGroups.keys) } fun hasTransformationGroup(group: String): Boolean { @@ -757,8 +757,8 @@ class Animator() { return emitter in particleEmitters } - fun particleEmitters(): Collection { - return Collections.unmodifiableCollection(particleEmitters.keys) + fun particleEmitters(): Set { + return Collections.unmodifiableSet(particleEmitters.keys) } fun setParticleEmitterActive(emitter: String, state: Boolean = true) { @@ -792,8 +792,8 @@ class Animator() { get.burstEvent.trigger() } - fun lights(): Collection { - return Collections.unmodifiableCollection(lights.keys) + fun lights(): Set { + return Collections.unmodifiableSet(lights.keys) } fun hasLight(light: String): Boolean { @@ -827,8 +827,8 @@ class Animator() { get.pointAngle = angle } - fun sounds(): Collection { - return Collections.unmodifiableCollection(sounds.keys) + fun sounds(): Set { + return Collections.unmodifiableSet(sounds.keys) } fun hasSound(sound: String): Boolean { @@ -881,8 +881,8 @@ class Animator() { get.signals.push(SoundSignal.STOP_ALL) } - fun effects(): Collection { - return Collections.unmodifiableCollection(effects.keys) + fun effects(): Set { + return Collections.unmodifiableSet(effects.keys) } fun hasEffect(effect: String): Boolean { @@ -894,8 +894,12 @@ class Animator() { get.enabled.accept(state) } - fun parts(): Collection { - return Collections.unmodifiableCollection(parts.keys) + fun parts(): Set { + return Collections.unmodifiableSet(parts.keys) + } + + fun hasPart(part: String): Boolean { + return part in parts } fun partPoint(part: String, property: String): Vector2d? { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/AbstractBehaviorNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/AbstractBehaviorNode.kt deleted file mode 100644 index c8562cc2..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/AbstractBehaviorNode.kt +++ /dev/null @@ -1,156 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities.behavior - -import com.google.common.collect.ImmutableList -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import com.google.gson.JsonPrimitive -import ru.dbotthepony.kommons.gson.contains -import ru.dbotthepony.kommons.gson.get -import ru.dbotthepony.kommons.gson.stream -import ru.dbotthepony.kstarbound.Registries -import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.defs.actor.behavior.CompositeNodeType -import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameterValue -import ru.dbotthepony.kstarbound.fromJsonFast -import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState -import ru.dbotthepony.kstarbound.util.valueOf - -abstract class AbstractBehaviorNode { - enum class Status(val asBoolean: Boolean?) { - INVALID(null), - SUCCESS(true), - FAILURE(false), - RUNNING(null) - } - - var calls = 0 - protected set - - abstract fun run(delta: Double, state: BehaviorState): Status - abstract fun reset() - - fun runAndReset(delta: Double, state: BehaviorState): Status { - val status = run(delta, state) - - if (status != Status.RUNNING) - reset() - - return status - } - - companion object { - fun replaceBehaviorTag(parameter: NodeParameterValue, treeParameters: Map): NodeParameterValue { - var str: String? = null - - if (parameter.key != null) - str = parameter.key - - // 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 - - if (!str.isNullOrEmpty()) { - if (str.first() == '<' && str.last() == '>') { - val treeKey = str.substring(1, str.length - 1) - val param = treeParameters[treeKey] - - if (param != null) { - return param - } else { - throw NoSuchElementException("No parameter specified for tag '$treeKey'") - } - } - } - - return parameter - } - - fun replaceOutputBehaviorTag(output: String?, treeParameters: Map): String? { - if (output == null) { - return null - } else if (output.first() == '<' && output.last() == '>') { - val replacement = treeParameters[output.substring(1, output.length - 1)] ?: throw NoSuchElementException("No parameter specified for tag '$output'") - - if (replacement.key != null) - return replacement.key - else if (replacement.value is JsonPrimitive && replacement.value.isString) - return replacement.value.asString - else - return null - } else { - return output - } - } - - fun create(data: JsonObject, treeParameters: Map, tree: BehaviorTree): AbstractBehaviorNode { - val type = BehaviorNodeType.entries.valueOf(data["type"].asString) - val name = data["name"].asString - val parameterConfig = data.get("parameters") { JsonObject() } - - if (type == BehaviorNodeType.MODULE) { - // merge in module parameters to a copy of the treeParameters to propagate - // tree parameters into the sub-tree, but allow modules to override - val moduleParameters = LinkedHashMap(treeParameters) - - for ((k, v) in parameterConfig.entrySet()) { - moduleParameters[k] = replaceBehaviorTag(Starbound.gson.fromJsonFast(v, NodeParameterValue::class.java), treeParameters) - } - - val module = BehaviorTree(tree.blackboard, Registries.behavior.getOrThrow(name).value, moduleParameters) - tree.scripts.addAll(module.scripts) - tree.functions.addAll(module.functions) - return module.root - } - - val parameters = LinkedHashMap(Registries.behaviorNodes.getOrThrow(name).value.properties) - - for ((k, v) in parameters.entries) { - if (k in parameterConfig) { - parameters[k] = v.copy(value = replaceBehaviorTag(Starbound.gson.fromJsonFast(parameterConfig[k], NodeParameterValue::class.java), treeParameters)) - } else { - val replaced = replaceBehaviorTag(v.value, treeParameters) - - if (replaced != v.value) { - parameters[k] = v.copy(value = replaced) - } - } - } - - when (type) { - BehaviorNodeType.ACTION -> { - tree.functions.add(name) - - val outputConfig = data.get("output") { JsonObject() } - val output = LinkedHashMap(Registries.behaviorNodes.getOrThrow(name).value.output) - - for ((k, v) in output.entries) { - val replaced = replaceOutputBehaviorTag(outputConfig[k]?.asString ?: v.key, treeParameters) - - if (replaced != v.key) { - output[k] = v.copy(key = replaced) - } - } - - return ActionNode(name, parameters, output) - } - - BehaviorNodeType.DECORATOR -> { - tree.functions.add(name) - val sacrifice = create(data["child"] as JsonObject, treeParameters, tree) - return DecoratorNode(name, parameters, sacrifice) - } - - BehaviorNodeType.COMPOSITE -> { - val children = data.get("children") { JsonArray() } - .stream() - .map { create(it as JsonObject, treeParameters, tree) } - .collect(ImmutableList.toImmutableList()) - - return CompositeNodeType.entries.valueOf(name).factory(parameters, children) - } - - BehaviorNodeType.MODULE -> throw RuntimeException() - } - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ActionNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ActionNode.kt deleted file mode 100644 index 0d1510c7..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ActionNode.kt +++ /dev/null @@ -1,77 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities.behavior - -import org.apache.logging.log4j.LogManager -import org.classdump.luna.Table -import org.classdump.luna.exec.CallPausedException -import org.classdump.luna.lib.CoroutineLib -import org.classdump.luna.runtime.Coroutine -import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeOutput -import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameter -import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState - -class ActionNode(val name: String, val parameters: Map, val outputs: Map) : AbstractBehaviorNode() { - private var coroutine: Coroutine? = null - private val nodeID = BehaviorState.NODE_GARBAGE_INDEX.getAndIncrement() - - override fun run(delta: Double, state: BehaviorState): Status { - calls++ - var coroutine = coroutine - var firstTime = false - - if (coroutine == null) { - firstTime = true - - val fn = state.functions[name] ?: throw RuntimeException("How? $name") - - coroutine = state.lua.call(CoroutineLib.create(), fn)[0] as Coroutine - } - - try { - val result = if (firstTime) { - val parameters = state.blackboard.parameters(parameters, this) - state.lua.call(CoroutineLib.resume(), coroutine, parameters, state.blackboard, nodeID, delta) - } else { - state.lua.call(CoroutineLib.resume(), coroutine, delta) - } - - val coroutineStatus = result[0] as Boolean - - if (!coroutineStatus) { - LOGGER.warn("Behavior ActionNode '$name' failed: ${result[1]}") - return Status.FAILURE - } - - if (result.size == 1) { - return Status.RUNNING - } - - val nodeStatus = result.getOrNull(1) as? Boolean ?: false - val nodeExtra = result.getOrNull(2) as? Table - - if (nodeExtra != null) { - state.blackboard.setOutput(this, nodeExtra) - } - - if (!nodeStatus) { - return Status.FAILURE - } else { - return Status.SUCCESS - } - } catch (err: CallPausedException) { - LOGGER.error("Behavior ActionNode '$name' called blocking code, which initiated pause. This is not supported.") - return Status.FAILURE - } - } - - override fun reset() { - coroutine = null - } - - override fun toString(): String { - return "ActionNode[$calls / $name, parameters=$parameters, outputs=$outputs, coroutine=$coroutine]" - } - - companion object { - private val LOGGER = LogManager.getLogger() - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/BehaviorTree.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/BehaviorTree.kt deleted file mode 100644 index 970aaba4..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/BehaviorTree.kt +++ /dev/null @@ -1,31 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities.behavior - -import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorDefinition -import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameterValue -import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState - -class BehaviorTree(val blackboard: Blackboard, val data: BehaviorDefinition, overrides: Map = mapOf()) : AbstractBehaviorNode() { - val scripts = HashSet(data.scripts) - val functions = HashSet() - - val root: AbstractBehaviorNode - - init { - if (overrides.isEmpty()) { - root = create(data.root, data.mappedParameters, this) - } else { - val parameters = LinkedHashMap(data.mappedParameters) - parameters.putAll(overrides) - root = create(data.root, parameters, this) - } - } - - override fun run(delta: Double, state: BehaviorState): Status { - return root.run(delta, state) - } - - override fun reset() { - root.reset() - } -} - diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/Blackboard.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/Blackboard.kt deleted file mode 100644 index 0a861eee..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/Blackboard.kt +++ /dev/null @@ -1,194 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities.behavior - -import com.google.gson.JsonArray -import com.google.gson.JsonElement -import com.google.gson.JsonNull -import com.google.gson.JsonPrimitive -import org.classdump.luna.ByteString -import org.classdump.luna.Table -import org.classdump.luna.Userdata -import org.classdump.luna.impl.ImmutableTable -import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameter -import ru.dbotthepony.kstarbound.lua.LuaEnvironment -import ru.dbotthepony.kstarbound.lua.from -import ru.dbotthepony.kstarbound.lua.get -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.util.valueOf -import java.util.EnumMap -import java.util.HashSet -import kotlin.collections.HashMap - -/** - * Blackboard (Greenboard, if you will) for tracking inputs and outputs of function nodes in behavior tree. - * - * While this is obviously flawed, and inputs/outputs should _generally_ be stored inside nodes themselves - * (since we create/clone behavior tree each time we make AI actor), this class is exposed to Lua scripts, - * so we must keep this class - * - * On side note, this architecture also employs global inputs/outputs of functions, hence you can't make - * an actual functional tree out of behavior nodes (because variable flow is global, and not local, so you - * can't have, per say, function "calculate sin out of x" because both input "x" and "sin result" are - * defined as global variables). - * Bravo, Chucklefish. - */ -class Blackboard(val lua: LuaEnvironment) : Userdata() { - private val board = EnumMap>(NodeParameterType::class.java) - private val input = EnumMap>>>(NodeParameterType::class.java) - private val parameters = HashMap() - // key -> list of Lua tables and their indices - private val vectorNumberInput = HashMap>>() - private var ephemeral = HashSet>() - - init { - for (v in NodeParameterType.entries) { - board[v] = HashMap() - input[v] = HashMap() - } - } - - operator fun set(type: NodeParameterType, key: String, value: Any?) { - if (value == null) { - board[type]!!.remove(key) - } else { - board[type]!![key] = value - } - - val input = input[type]!![key] - - if (input != null) { - for ((index, table) in input) { - table[index] = value - } - } - - // dumb special case for setting number outputs to vec2 inputs - if (type == NodeParameterType.NUMBER) { - val mappings = vectorNumberInput[key] - - if (mappings != null) { - for ((index, table) in mappings) { - table[index] = value - } - } - } - } - - operator fun get(type: NodeParameterType, key: String): Any? { - return board[type]!![key] - } - - fun parameters(parameters: Map, nodeID: Any): Table { - val get = this.parameters[nodeID] - - if (get != null) - return get - - val table = lua.newTable() - - for ((name, parameter) in parameters.entries) { - if (parameter.value.key != null) { - val typeInput = input[parameter.type]!!.computeIfAbsent(parameter.value.key) { ArrayList() } - typeInput.add(name to table) - table[name] = this[parameter.type, parameter.value.key] - } else { - val value = parameter.value.value ?: JsonNull.INSTANCE - - if (value.isJsonNull) - continue - - // dumb special case for allowing a vec2 of blackboard number keys - if (parameter.type == NodeParameterType.VEC2) { - if (value !is JsonArray) - throw IllegalArgumentException("Expected '$name' to be VEC2 array, got $value") - - val vector = lua.newTable(value.size(), 0) - - for ((i, element) in value.withIndex()) { - if (element is JsonPrimitive && element.isString) { - vectorNumberInput.computeIfAbsent(element.asString) { HashSet() }.add(i + 1L to vector) - vector[i + 1L] = this[NodeParameterType.NUMBER, element.asString] - } else { - vector[i + 1L] = lua.from(element) - } - } - - table[name] = vector - } else { - table[name] = lua.from(value) - } - } - } - - this.parameters[nodeID] = table - return table - } - - fun setOutput(node: ActionNode, output: Table) { - for ((tableKey, out) in node.outputs) { - if (out.key != null) { - this[out.type, out.key] = output[tableKey] - - if (out.ephemeral) { - ephemeral.add(out.type to out.key) - } - } - } - } - - fun takeEphemerals(): Set> { - val ephemeral = ephemeral - this.ephemeral = HashSet() - return ephemeral - } - - fun clearEphemerals(ephemerals: Collection>) { - for ((type, key) in ephemerals) { - this[type, key] = null - } - } - - override fun getMetatable(): Table { - return Companion.metatable - } - - override fun setMetatable(mt: Table?): Table { - throw UnsupportedOperationException() - } - - override fun getUserValue(): Blackboard { - return this - } - - override fun setUserValue(value: Blackboard?): Blackboard { - throw UnsupportedOperationException() - } - - companion object { - private val metatable: ImmutableTable - private fun __index() = metatable - - init { - val builder = ImmutableTable.Builder() - .add("get", luaFunction { self: Blackboard, type: ByteString, key: ByteString -> - returnBuffer.setTo(self[NodeParameterType.entries.valueOf(type.decode()), key.decode()]) - }) - .add("set", luaFunction { self: Blackboard, type: ByteString, key: ByteString, value: Any? -> - self[NodeParameterType.entries.valueOf(type.decode()), key.decode()] = value - }) - - for (type in NodeParameterType.entries) { - builder.add("get${type.funcName}", luaFunction { self: Blackboard, key: ByteString -> - returnBuffer.setTo(self[type, key.decode()]) - }) - - builder.add("set${type.funcName}", luaFunction { self: Blackboard, key: ByteString, value: Any? -> - self[type, key.decode()] = value - }) - } - - builder.add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) }) - metatable = builder.build() - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DecoratorNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DecoratorNode.kt deleted file mode 100644 index 0942115a..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DecoratorNode.kt +++ /dev/null @@ -1,110 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities.behavior - -import org.apache.logging.log4j.LogManager -import org.classdump.luna.ByteString -import org.classdump.luna.Table -import org.classdump.luna.exec.CallPausedException -import org.classdump.luna.lib.CoroutineLib -import org.classdump.luna.runtime.Coroutine -import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameter -import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState -import java.util.concurrent.atomic.AtomicLong - -class DecoratorNode(val name: String, val parameters: Map, val child: AbstractBehaviorNode) : AbstractBehaviorNode() { - private var coroutine: Coroutine? = null - private val nodeID = BehaviorState.NODE_GARBAGE_INDEX.getAndIncrement() - - override fun run(delta: Double, state: BehaviorState): Status { - calls++ - var coroutine = coroutine - - if (coroutine == null) { - val parameters = state.blackboard.parameters(parameters, this) - val fn = state.functions[name] ?: throw RuntimeException("How? $name") - - try { - coroutine = state.lua.call(CoroutineLib.create(), fn)[0] as Coroutine - val result = state.lua.call(CoroutineLib.resume(), coroutine, parameters, state.blackboard, nodeID) - val status = result[0] as Boolean - - if (!status) { - LOGGER.warn("Behavior DecoratorNode '$name' failed: ${result[1]}") - return Status.FAILURE - } - - if (result.size == 1) { - val coroutineStatus = state.lua.call(CoroutineLib.status(), coroutine)[0] as String - - if (coroutineStatus == "dead") { - // quite unexpected, but whatever - return Status.SUCCESS - } else { - this.coroutine = coroutine - } - } else { - val nodeStatus = result.getOrNull(1) as? Boolean ?: false - // val nodeExtra = result.getOrNull(3) as? Table - return if (nodeStatus) Status.SUCCESS else Status.FAILURE - } - } catch (err: CallPausedException) { - LOGGER.error("Behavior DecoratorNode '$name' called blocking code, which initiated pause. This is not supported.") - return Status.FAILURE - } - } - - // decorator runs its child on yield and is resumed with the child's status on success or failure - var status = Status.RUNNING - - while (status == Status.RUNNING) { - val childStatus = child.runAndReset(delta, state) - - if (childStatus == Status.SUCCESS || childStatus == Status.FAILURE) { - try { - val result = state.lua.call(CoroutineLib.resume(), coroutine, childStatus.asBoolean) - val execStatus = result[0] as Boolean - - if (!execStatus && result.size >= 2) { - LOGGER.warn("Behavior DecoratorNode '$name' failed: ${result[1]}") - } - - if (result.size == 1) { - // another yield OR unexpected return? - val coroutineStatus = state.lua.call(CoroutineLib.status(), coroutine)[0] as String - - if (coroutineStatus == "dead") { - this.coroutine = null - status = Status.SUCCESS - } else - status = Status.RUNNING - } else { - // yield or return with status - val nodeStatus = result.getOrNull(1) as? Boolean ?: false - // val nodeExtra = result.getOrNull(3) as? Table - - status = if (nodeStatus) Status.SUCCESS else Status.FAILURE - this.coroutine = null - } - } catch (err: CallPausedException) { - LOGGER.error("Behavior DecoratorNode '$name' called blocking code on children return, which initiated pause. This is not supported.") - status = Status.FAILURE - } - } else { - return Status.RUNNING - } - } - - return status - } - - override fun reset() { - coroutine = null - } - - override fun toString(): String { - return "DecoratorNode[$calls / $name, coroutine=$coroutine, parameters=$parameters, children=$child]" - } - - companion object { - private val LOGGER = LogManager.getLogger() - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DynamicNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DynamicNode.kt deleted file mode 100644 index 7e7e8b2d..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DynamicNode.kt +++ /dev/null @@ -1,38 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities.behavior - -import com.google.common.collect.ImmutableList -import org.classdump.luna.runtime.ExecutionContext -import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState - -class DynamicNode(val children: ImmutableList) : AbstractBehaviorNode() { - private var index = 0 - - override fun run(delta: Double, state: BehaviorState): Status { - calls++ - for ((i, node) in children.withIndex()) { - val status = node.runAndReset(delta, state) - - if (status == Status.FAILURE && index == i) - index++ - else if (i < index && (status == Status.SUCCESS || status == Status.FAILURE)) { - node.reset() - index = i - } - - if (status == Status.SUCCESS || index >= children.size) { - return status - } - } - - return Status.RUNNING - } - - override fun toString(): String { - return "DynamicNode[$calls / ${children.size}, index=$index, current=${children.getOrNull(index)}]" - } - - override fun reset() { - children.forEach { it.reset() } - index = 0 - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ParallelNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ParallelNode.kt deleted file mode 100644 index b82f6ef9..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ParallelNode.kt +++ /dev/null @@ -1,70 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities.behavior - -import com.google.common.collect.ImmutableList -import com.google.gson.JsonPrimitive -import org.classdump.luna.runtime.ExecutionContext -import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameter -import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState - -class ParallelNode(parameters: Map, val children: ImmutableList) : AbstractBehaviorNode() { - val successLimit: Int - val failLimit: Int - - init { - val value = (parameters["success"]?.value?.value as? JsonPrimitive)?.asInt ?: -1 - - if (value == -1) { - successLimit = children.size - } else { - successLimit = value - } - } - - init { - val value = (parameters["fail"]?.value?.value as? JsonPrimitive)?.asInt ?: -1 - - if (value == -1) { - failLimit = children.size - } else { - failLimit = value - } - } - - private var lastFailed = -1 - private var lastSucceeded = -1 - - override fun run(delta: Double, state: BehaviorState): Status { - calls++ - var failed = 0 - var succeeded = 0 - - for (node in children) { - val status = node.runAndReset(delta, state) - - if (status == Status.SUCCESS) - succeeded++ - else if (status == Status.FAILURE) - failed++ - - if (succeeded >= successLimit || failed >= failLimit) { - lastFailed = failed - lastSucceeded = succeeded - return if (succeeded >= successLimit) Status.SUCCESS else Status.FAILURE - } - } - - lastFailed = failed - lastSucceeded = succeeded - return Status.RUNNING - } - - override fun toString(): String { - return "ParallelNode[$calls / children=${children.size}, successLimit=$successLimit, failLimit=$failLimit, lastFailed=$lastFailed, lastSucceeded=$lastSucceeded]" - } - - override fun reset() { - children.forEach { it.reset() } - lastSucceeded = -1 - lastFailed = -1 - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/RandomizeNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/RandomizeNode.kt deleted file mode 100644 index f0b1a511..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/RandomizeNode.kt +++ /dev/null @@ -1,33 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities.behavior - -import com.google.common.collect.ImmutableList -import org.classdump.luna.runtime.ExecutionContext -import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState - -class RandomizeNode(val children: ImmutableList) : AbstractBehaviorNode() { - var index = -1 - - override fun run(delta: Double, state: BehaviorState): Status { - calls++ - - if (index == -1 && children.isNotEmpty()) - index = state.lua.random.nextInt(children.size) - - if (index == -1) - return Status.FAILURE - - return children[index].runAndReset(delta, state) - } - - override fun toString(): String { - if (index == -1) - return "RandomizeNode[$calls / ${children.size}, not chosen]" - else - return "RandomizeNode[$calls / ${children.size}, ${children[index]}]" - } - - override fun reset() { - children.forEach { it.reset() } - index = -1 - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/SequenceNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/SequenceNode.kt deleted file mode 100644 index e4ef48ac..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/SequenceNode.kt +++ /dev/null @@ -1,37 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities.behavior - -import com.google.common.collect.ImmutableList -import org.classdump.luna.runtime.ExecutionContext -import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState - -class SequenceNode(val children: ImmutableList, val isSelector: Boolean) : AbstractBehaviorNode() { - private var index = 0 - - override fun run(delta: Double, state: BehaviorState): Status { - calls++ - - while (index < children.size) { - val child = children[index] - val status = child.runAndReset(delta, state) - - if ((isSelector && status == Status.SUCCESS || !isSelector && status == Status.FAILURE) || status == Status.RUNNING) - return status - - index++ - } - - return if (isSelector) Status.FAILURE else Status.SUCCESS - } - - override fun toString(): String { - if (isSelector) - return "SelectorNode[$calls / ${children.size}, index=$index, current=${children.getOrNull(index)}]" - else - return "SequenceNode[$calls / ${children.size}, index=$index, current=${children.getOrNull(index)}]" - } - - override fun reset() { - children.forEach { it.reset() } - index = 0 - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt index 575e4aab..dc1c3124 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt @@ -226,6 +226,10 @@ open class WorldObject(val config: Registry.Entry) : TileEntit return path.get(mergedJson.value, orElse) } + fun lookupPropertyOrNull(path: JsonPath): JsonElement? { + return path.find(mergedJson.value) + } + fun lookupProperty(path: String, orElse: () -> JsonElement): JsonElement { return mergedJson.value[path] ?: orElse.invoke() } @@ -234,6 +238,10 @@ open class WorldObject(val config: Registry.Entry) : TileEntit return mergedJson.value[path] ?: JsonNull.INSTANCE } + fun lookupPropertyOrNull(path: String): JsonElement? { + return mergedJson.value[path] + } + init { networkGroup.upstream.add(uniqueID) } diff --git a/src/main/resources/scripts/behavior.lua b/src/main/resources/scripts/behavior.lua new file mode 100644 index 00000000..38b7376a --- /dev/null +++ b/src/main/resources/scripts/behavior.lua @@ -0,0 +1,674 @@ + +-- TODO: Incomplete. + +-- Blackboard +local blackboardPrototype = {} +blackboardPrototype.__index = blackboardPrototype + +local nodeParametersTypes = { + {'json', 'Json'}, + {'entity', 'Entity'}, + {'position', 'Position'}, + {'vec2', 'Vec2'}, + {'number', 'Number'}, + {'bool', 'Bool'}, + {'list', 'List'}, + {'table', 'Table'}, + {'string', 'String'}, +} + +local mappedParameterTypes = {} + +for i, data in ipairs(nodeParametersTypes) do + mappedParameterTypes[i] = i + mappedParameterTypes[data[1]] = i +end + +function blackboardPrototype:ctor() + self.board = {} + self.input = {} + self._parameters = {} + self.vectorNumberInput = {} + self.ephemeral = {} + + for i, data in ipairs(nodeParametersTypes) do + self.board[data[1]] = {} + self.input[data[1]] = {} + self.ephemeral[i] = {} + + self.board[i] = self.board[data[1]] + self.input[i] = self.input[data[1]] + end + + return self +end + +local function blackboardSet(self, t, key, value) + self.board[t][key] = value + + local input = self.input[t][key] + + if input then + for _, pair in ipairs(input) do + local index = pair[1] + local tab = pair[2] + tab[index] = value + end + end + + -- dumb special case for setting number outputs to vec2 inputs + if t == 5 then + local mappings = self.vectorNumberInput[key] + + if mappings then + for _, pair in pairs(input) do + local index = pair[1] + local tab = pair[2] + tab[index] = value + end + end + end +end + +function blackboardPrototype:set(t, key, value) + blackboardSet(self, mappedParameterTypes[t], key, value) +end + +function blackboardPrototype:setRaw(t, key, value) + blackboardSet(self, t, key, value) +end + +function blackboardPrototype:get(t, key) + return self.board[t][key] +end + +for i, data in ipairs(nodeParametersTypes) do + blackboardPrototype['get' .. data[2]] = function(self, key) + return self.board[i][key] + end + + blackboardPrototype['set' .. data[2]] = function(self, key, value) + blackboardSet(self, i, key, value) + end +end + +function blackboardPrototype:parameters(parameters, nodeID) + local tab = self._parameters[nodeID] + if tab then return tab end + tab = {} + + for parameterName, parameter in pairs(parameters) do + local t = assert(mappedParameterTypes[parameter.type]) + local pKey = parameter.key + local pValue = parameter.value + + if pKey then + local typeInput = self.input[t][pKey] + + if not typeInput then + typeInput = {} + self.input[i][pKey] = typeInput + end + + table.insert(typeInput, {parameterName, tab}) + tab[parameterName] = self.board[t][pKey] + elseif pValue then + if t == 4 then -- vec2 + -- dumb special case for allowing a vec2 of blackboard number keys + if type(pValue) ~= 'table' then + error(string.format('parameter %s of type %s for node %s has has non-table value: %s', parameterName, parameter.type, nodeID, type(pValue))) + end + + local vector = {} + + for i, vValue in ipairs(pValue) do + if type(vValue) == 'string' then + -- vector part with symbolic reference + local typeInput = self.vectorNumberInput[vValue] + + if not typeInput then + typeInput = {} + self.vectorNumberInput[vValue] = typeInput + end + + table.insert(typeInput, {i, vector}) + vector[i] = self.board[5][key] -- number + else + vector[i] = vValue + end + end + + tab[parameterName] = vector + 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 + + self._parameters[nodeID] = tab + return tab +end + +function blackboardPrototype:setOutput(node, output) + for key, out in pairs(node.outputs) do + if out.key then + local t = out.type + blackboardSet(self, t, out.key, output[key]) + + if out.ephemeral then + self.ephemeral[t][out.key] = true + end + end + end +end + +function blackboardPrototype:takeEphemerals() + local value = self.ephemeral + self.ephemeral = {} + + for i, _ in ipairs(nodeParametersTypes) do + self.ephemeral[i] = {} + end + + return value +end + +function blackboardPrototype:clearEphemerals(ephemerals) + for i, keys in ipairs(ephemerals) do + for key in pairs(keys) do + blackboardSet(self, i, key, nil) + end + end +end + +local function Blackboard() + return setmetatable({}, blackboardPrototype):ctor() +end + +-- //// Blackboard + +local SUCCESS = true +local FAILURE = false +local RUNNING = nil +local nextNodeID = 0 + +local function runAndReset(self, ...) + local status = self:run(...) + + if status ~= RUNNING then + self:reset() + end + + return status +end + +-- ActionNode + +local actionNode = {} +actionNode.__index = actionNode + +function actionNode:ctor(name, parameters, outputs) + self.name = name + self.parameters = parameters + self.outputs = outputs + self.calls = 0 + self.nodeID = nextNodeID + nextNodeID = nextNodeID + 1 + + return self +end + +function actionNode:bake() + self.callable = _G[self.name] + + if type(callable) ~= 'function' then + error('expected global ' .. self.name .. ' to be a function, but got ' .. type(callable)) + end +end + +do + local create = coroutine.create + local resume = coroutine.resume + local status = coroutine.status + + function actionNode:run(delta, blackboard) + self.calls = self.calls + 1 + local status, nodeStatus, nodeExtra + + if not self.coroutine then + self.coroutine = create(self.callable) + local parameters = blackboard:parameters(self.parameters, self) + status, nodeStatus, nodeExtra = resume(self.coroutine, parameters, blackboard, self.nodeID, delta) + else + status, nodeStatus, nodeExtra = resume(self.coroutine, delta) + end + + if not status then + sb.logError('Behavior ActionNode %q failed: %s', self.name, nodeStatus) + return FAILURE + end + + if result == nil then + return RUNNING + end + + if nodeExtra ~= nil then + blackboard:setOutput(self, nodeExtra) + end + + if nodeStatus then + return SUCCESS + else + return FAILURE + end + end +end + +function actionNode:reset() + self.coroutine = nil +end + +function actionNode:__tostring() + return string.format('ActionNode[%q / %d / %s]', self.name, self.calls, tostring(self.coroutine or 'none')) +end + +function ActionNode(...) + return setmetatable({}, actionNode):ctor(...) +end + +-- //// ActionNode + +-- DecoratorNode + +local decoratorNode = {} +decoratorNode.__index = decoratorNode + +function decoratorNode:ctor(name, parameters, child) + self.name = name + self.parameters = parameters + self.child = child + self.calls = 0 + self.nodeID = nextNodeID + nextNodeID = nextNodeID + 1 + + return self +end + +function decoratorNode:bake() + self.callable = _G[self.name] + + if type(callable) ~= 'function' then + error('expected global ' .. self.name .. ' to be a function, but got ' .. type(callable)) + end + + self.child:bake() +end + +do + local create = coroutine.create + local resume = coroutine.resume + local coroutine_status = coroutine.status + + function decoratorNode:run(delta, blackboard) + self.calls = self.calls + 1 + + if not self.coroutine then + local parameters = blackboard:parameters(self.parameters, self) + local coroutine = create(self.callable) + local status, nodeStatus = resume(coroutine, parameters, blackboard, self.nodeID, delta) + + if not status then + sb.logError('Behavior DecoratorNode %q failed: %s', self.name, nodeStatus) + return FAILURE + end + + if nodeStatus == nil then + local s = coroutine_status(coroutine) + + if s == 'dead' then + -- quite unexpected, but whatever + return SUCCESS + else + self.coroutine = coroutine + end + elseif nodeStatus then + return SUCCESS + else + return FAILURE + end + end + + while true do + local childStatus = runAndReset(self.child, delta, blackboard) + + if childStatus == RUNNING then + return RUNNING + end + + local status, nodeStatus = resume(self.coroutine, childStatus) + + if not status then + sb.logError('Behavior DecoratorNode %q failed: %s', self.name, nodeStatus) + return FAILURE + end + + if nodeStatus == nil then + -- another yield OR unexpected return? + + local s = coroutine_status(coroutine) + + if s == 'dead' then + self.coroutine = nil + return SUCCESS + end + else + -- yield or return with status + self.coroutine = nil + + if nodeStatus then + return SUCCESS + else + return FAILURE + end + end + end + end +end + +function decoratorNode:reset() + self.coroutine = nil + self.child:reset() +end + +function actionNode:__tostring() + return string.format('DecoratorNode[%q / %d / %s; %s]', self.name, self.calls, tostring(self.coroutine or 'none'), tostring(self.child)) +end + +function DecoratorNode(...) + return setmetatable({}, decoratorNode):ctor(...) +end + +-- //// DecoratorNode + +-- Composite nodes + +local seqNode = {} +seqNode.__index = seqNode + +function seqNode:ctor(children, isSelector) + self.children = children + self.isSelector = isSelector + self.calls = 0 + self.index = 1 + self.size = #children + + return self +end + +function seqNode:run(delta, blackboard) + self.calls = self.calls + 1 + local size = self.size + local isSelector = self.isSelector +` + while self.index <= size do + local child = self.children[self.index] + local status = runAndReset(child, delta, blackboard) + + if status == RUNNING then + return RUNNING + elseif isSelector and status == SUCCESS then + return SUCCESS + elseif not isSelector and status == FAILURE then + return FAILURE + end + + self.index = self.index + 1 + end +end + +function seqNode:reset() + self.index = 1 + + for _, child in ipairs(self.children) do + child:reset() + end +end + +function seqNode:bake() + for _, child in ipairs(self.children) do + child:bake() + end +end + +function seqNode:__tostring() + if self.isSelector then + return string.format('SelectorNode[%d / %d, index=%d, current=%s]', self.calls, self.size, self.index, tostring(self.children[self.index])) + else + return string.format('SequenceNode[%d / %d, index=%d, current=%s]', self.calls, self.size, self.index, tostring(self.children[self.index])) + end +end + +function SequenceNode(p, c) + return setmetatable({}, seqNode):ctor(c, false) +end + +function SelectorNode(p, c) + return setmetatable({}, seqNode):ctor(c, true) +end + +local parallelNode = {} +parallelNode.__index = parallelNode + +function parallelNode:ctor(parameters, children) + self.children = children + + if type(parameters.success) == 'number' then + self.successLimit = parameters.success + else + self.successLimit = #children + end + + if type(parameters.fail) == 'number' then + self.failLimit = parameters.fail + else + self.failLimit = #children + end + + self.lastFailed = -1 + self.lastSucceed = -1 + self.calls = 0 + + return self +end + +function parallelNode:run(delta, blackboard) + self.calls = self.calls + 1 + local failed = 0 + local succeeded = 0 + local failLimit = self.failLimit + local successLimit = self.successLimit + + for _, node in ipairs(self.children) do + local status = runAndReset(node, delta, blackboard) + + if status == SUCCESS then + succeeded = succeeded + 1 + elseif status == FAILURE then + failed = failed + 1 + end + + if failed >= failLimit then + self.lastFailed = failed + self.lastSucceed = succeeded + return FAILURE + elseif succeeded >= successLimit then + self.lastFailed = failed + self.lastSucceed = succeeded + return SUCCESS + end + end + + self.lastFailed = failed + self.lastSucceed = succeeded + return RUNNING +end + +function parallelNode:bake() + for _, child in ipairs(self.children) do + child:bake() + end +end + +function parallelNode:reset() + self.lastFailed = -1 + self.lastSucceed = -1 + + for _, child in ipairs(self.children) do + child:reset() + end +end + +function parallelNode:__tostring() + return string.format('ParallelNode[%d / %d, limits=%d / %d, last=%d / %d]', self.calls, #self.children, self.successLimit, self.failLimit, self.lastSucceed, self.lastFailed) +end + +function ParallelNode(p, c) + return setmetatable({}, parallelNode):ctor(p, c) +end + +local dynNode = {} +dynNode.__index = dynNode + +function dynNode:ctor(children) + self.children = children + self.calls = 0 + self.index = 1 + self.size = #children + + return self +end + +function dynNode:run(delta, blackboard) + self.calls = self.calls + 1 + + for i, node in ipairs(self.children) do + local status = runAndReset(node, delta, blackboard) + + if stauts == FAILURE and self.index == i then + self.index = self.index + 1 + elseif status ~= RUNNING and i < self.index then + node:reset() + self.index = i + end + + if status == SUCCESS or self.index > self.size then + return status + end + end + + return RUNNING +end + +function dynNode:bake() + for _, child in ipairs(self.children) do + child:bake() + end +end + +function dynNode:reset() + self.index = 1 + + for _, child in ipairs(self.children) do + child:reset() + end +end + +function dynNode:__tostring() + return string.format('DynamicNode[%d / %d, index=%d, current=%s]', self.calls, self.size, self.index, tostring(self.children[self.index])) +end + +function DynamicNode(p, c) + return setmetatable({}, dynNode):ctor(c) +end + +local randNode = {} +randNode.__index = randNode + +function randNode:ctor(children) + self.children = children + self.index = -1 + self.calls = 0 + self.size = #children + + return self +end + +function randNode:run(delta, blackboard) + self.calls = self.calls + 1 + + if self.index == -1 and self.size ~= 0 then + self.index = math.random(1, self.size) + end + + if self.index == -1 then + return FAILURE + else + return runAndReset(self.children[self.index], delta, blackboard) + end +end + +function randNode:bake() + for _, child in ipairs(self.children) do + child:bake() + end +end + +function randNode:reset() + self.index = -1 + + for _, child in ipairs(self.children) do + child:reset() + end +end + +function randNode:__tostring() + return string.format('RandomNode[%d / %d, index=%d, current=%s]', self.calls, self.size, self.index, tostring(self.children[self.index])) +end + +function RandomNode(p, c) + return setmetatable({}, randNode):ctor(c) +end + +-- //// Composite nodes + +local statePrototype = {} +statePrototype.__index = statePrototype + +function statePrototype:ctor(blackboard, root) + self.root = root + self._blackboard = blackboard + + return self +end + +function statePrototype:run(delta) + local ephemerals = self._blackboard:takeEphemerals() + local status = runAndReset(self.root, delta, self._blackboard) + self._blackboard:clearEphemerals(ephemerals) + + return status +end + +function statePrototype:clear() + self.tree:reset() +end + +function statePrototype:blackboard() + return self._blackboard +end + +function BehaviorState(...) + return setmetatable({}, statePrototype):ctor(...) +end diff --git a/src/main/resources/scripts/config.lua b/src/main/resources/scripts/config.lua deleted file mode 100644 index 7b5f2a7e..00000000 --- a/src/main/resources/scripts/config.lua +++ /dev/null @@ -1,17 +0,0 @@ - -config = {} -local config = config - -function config.getParameter(name, default) - if type(name) ~= 'string' then - error('config.getParameter: name must be a string, got ' .. type(name), 2) - end - - local get = config._get(name) - - if get == nil then - return default - else - return get - end -end diff --git a/src/main/resources/scripts/global.lua b/src/main/resources/scripts/global.lua index c43aa0c9..b29d4897 100644 --- a/src/main/resources/scripts/global.lua +++ b/src/main/resources/scripts/global.lua @@ -18,7 +18,7 @@ local format = string.format function checkarg(value, index, expected, fnName, overrideExpected) if type(value) ~= expected then - error(string.format('bad argument #%d to %s: %s expected, got %s', index, fnName, overrideExpected or expected, type(value)), 3) + error(format('bad argument #%d to %s: %s expected, got %s', index, fnName, overrideExpected or expected, type(value)), 3) end end @@ -286,22 +286,6 @@ do end end -do - local min = math.min - local max = math.max - - function math.clamp(value, _min, _max) - if _min > _max then - error(format('interval is empty: %d <= %d', _min, _max), 2) - end - - return max(min(value, _min), _max) - end -end - -function math.lerp(t, a, b) - return t * (b - a) + a -end do local lerp = math.lerp @@ -336,3 +320,85 @@ end -- why is this even a thing. sb.print = tostring + +-- KStarbound global Lua functions and extensions +do + local min = math.min + local max = math.max + + function math.clamp(value, _min, _max) + if _min > _max then + error(format('interval is empty: %d <= %d', _min, _max), 2) + end + + return max(min(value, _min), _max) + end +end + +function math.lerp(t, a, b) + return t * (b - a) + a +end + +function table.copy(source) + local copy = {} + + for k, v in pairs(source) do + if type(v) == 'table' then + copy[k] = table.copy(v) + else + copy[k] = v + end + end + + return copy +end + +function jsonType(value) + if type(value) ~= 'table' then + return LUA_HINT_NONE + else + local meta = getmetatable(value) + + if not meta then + return LUA_HINT_NONE + else + return meta.__typehint or LUA_HINT_NONE + end + end +end + +function mergeJson(base, with) + local bType = jsonType(base) + local wType = jsonType(with) + + if bType == LUA_HINT_OBJECT and wType == LUA_HINT_OBJECT or bType == LUA_HINT_NONE and wType == LUA_HINT_NONE and type(base) == 'table' and type(with) == 'table' then + -- merge as json objects as long as they are both "json objects" or plain tables + for k, v in pairs(with) do + base[k] = mergeJson(base[k], v) + end + + return base + elseif wType == nil then + return base + else + return with + end +end + +string.__index = string + +do + local sub = string.sub + + function string:__index(key) + if type(key) == 'number' then + return sub(self, key, key) + else + return string[key] + end + end +end + +setmetatable('', string) + + diff --git a/src/main/resources/scripts/message_handler.lua b/src/main/resources/scripts/message_handler.lua deleted file mode 100644 index 5b0a6d5a..00000000 --- a/src/main/resources/scripts/message_handler.lua +++ /dev/null @@ -1,27 +0,0 @@ - -message = { - handlers = {} -} - -local message = message - -function message.setHandler(name, handler) - if type(name) ~= 'string' then - error('message.setHandler: Handler name must be a string, got ' .. type(name), 2) - end - - if type(handler) ~= 'function' then - error('message.setHandler: Handler itself must be a function, got ' .. type(handler), 2) - end - - message.subscribe(name) - message.handlers[name] = handler -end - -function message.call(name, ...) - local handler = message.handlers[name] - - if handler ~= nil then - return handler(...) - end -end