Move remaining bindings to PUC Lua

This commit is contained in:
DBotThePony 2024-12-28 13:56:08 +07:00
parent 20b1a7b5e5
commit e1d1531e0a
Signed by: DBot
GPG Key ID: DCC23B5715498507
40 changed files with 3436 additions and 2354 deletions

View File

@ -155,6 +155,7 @@ In addition to `add`, `multiply`, `merge` and `override` new merge methods are a
* Added `animator.effects(): List<string>`
* Added `animator.hasEffect(effect: string): boolean`
* Added `animator.parts(): List<string>`
* 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<Vector2i>`, 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

View File

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

View File

@ -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<String, NodeParameter>, ImmutableList<AbstractBehaviorNode>) -> 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");
}

View File

@ -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<NodeOutput>() {
override fun write(out: JsonWriter, value: NodeOutput) {
out.beginObject()

View File

@ -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<NodeParameter>() {
override fun write(out: JsonWriter, value: NodeParameter) {
out.beginObject()

View File

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

View File

@ -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<Vector2i, AABB>? {
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()) })
}
}

View File

@ -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<String, LuaFunction<*, *, *, *, *>>()
class LuaMessageHandlerComponent(lua: LuaThread, val nameProvider: () -> String) {
private val handlers = HashMap<String, LuaHandle>()
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()
}
}

View File

@ -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<Any, JsonElement>()
@ -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 <T> 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)

View File

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

View File

@ -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<out Any?> ->
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<String>, 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()
}

View File

@ -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 <T : Any> setOfferedQuests(self: T, prop: (T) -> MutableList<QuestArcDescriptor>, 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 <T : Any> setTurnInQuests(self: T, prop: (T) -> MutableList<String>, 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
}

View File

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

View File

@ -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<Either<ImmutableMap<String, Registry.Ref<TreasurePoolDefinition>>, Registry.Ref<TreasurePoolDefinition>>>()
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()
}

View File

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

View File

@ -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<String, String>()
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())
}
}
lua.pop()
}

View File

@ -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<PersistentStatusEffect>()
private object CPersistentStatusEffectToken : TypeToken<ArrayList<PersistentStatusEffect>>()
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()
}

View File

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

View File

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

View File

@ -24,7 +24,7 @@ import ru.dbotthepony.kstarbound.world.tileAreaBrush
import java.util.concurrent.CompletableFuture
private fun damageTilesImpl(self: World<*, *>, args: LuaThread.ArgStack): CompletableFuture<TileDamageResult> {
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()

View File

@ -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<String, String>()
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()
}

View File

@ -1,4 +1,4 @@
package ru.dbotthepony.kstarbound.world.entities.behavior
package ru.dbotthepony.kstarbound.lua.userdata
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable

View File

@ -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<BehaviorState>() {
val blackboard get() = tree.blackboard
val lua get() = blackboard.lua
val functions = HashMap<String, LuaFunction<*, *, *, *, *>>()
private fun replaceBehaviorTag(parameter: NodeParameterValue, treeParameters: Map<String, NodeParameterValue>): 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, NodeParameterValue>): 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<String, NodeParameter>) {
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<String, NodeOutput>) {
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<String, NodeParameterValue>,
blackboard: LuaHandle,
scripts: MutableSet<String>,
functions: MutableSet<String>,
handles: MutableList<LuaHandle>
): 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<String>()
val scripts = HashSet<String>()
val mergedParams: BehaviorDefinition
val blackboard: LuaHandle
val handles = ArrayList<LuaHandle>()
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()
}

View File

@ -1,4 +1,4 @@
package ru.dbotthepony.kstarbound.world.entities.behavior
package ru.dbotthepony.kstarbound.lua.userdata
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable

View File

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

View File

@ -693,12 +693,12 @@ class Animator() {
return group in rotationGroups
}
fun rotationGroups(): Collection<String> {
return Collections.unmodifiableCollection(rotationGroups.keys)
fun rotationGroups(): Set<String> {
return Collections.unmodifiableSet(rotationGroups.keys)
}
fun transformationGroups(): Collection<String> {
return Collections.unmodifiableCollection(transformationGroups.keys)
fun transformationGroups(): Set<String> {
return Collections.unmodifiableSet(transformationGroups.keys)
}
fun hasTransformationGroup(group: String): Boolean {
@ -757,8 +757,8 @@ class Animator() {
return emitter in particleEmitters
}
fun particleEmitters(): Collection<String> {
return Collections.unmodifiableCollection(particleEmitters.keys)
fun particleEmitters(): Set<String> {
return Collections.unmodifiableSet(particleEmitters.keys)
}
fun setParticleEmitterActive(emitter: String, state: Boolean = true) {
@ -792,8 +792,8 @@ class Animator() {
get.burstEvent.trigger()
}
fun lights(): Collection<String> {
return Collections.unmodifiableCollection(lights.keys)
fun lights(): Set<String> {
return Collections.unmodifiableSet(lights.keys)
}
fun hasLight(light: String): Boolean {
@ -827,8 +827,8 @@ class Animator() {
get.pointAngle = angle
}
fun sounds(): Collection<String> {
return Collections.unmodifiableCollection(sounds.keys)
fun sounds(): Set<String> {
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<String> {
return Collections.unmodifiableCollection(effects.keys)
fun effects(): Set<String> {
return Collections.unmodifiableSet(effects.keys)
}
fun hasEffect(effect: String): Boolean {
@ -894,8 +894,12 @@ class Animator() {
get.enabled.accept(state)
}
fun parts(): Collection<String> {
return Collections.unmodifiableCollection(parts.keys)
fun parts(): Set<String> {
return Collections.unmodifiableSet(parts.keys)
}
fun hasPart(part: String): Boolean {
return part in parts
}
fun partPoint(part: String, property: String): Vector2d? {

View File

@ -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<String, NodeParameterValue>): 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, NodeParameterValue>): 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<String, NodeParameterValue>, 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()
}
}
}
}

View File

@ -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<String, NodeParameter>, val outputs: Map<String, NodeOutput>) : 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()
}
}

View File

@ -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<String, NodeParameterValue> = mapOf()) : AbstractBehaviorNode() {
val scripts = HashSet(data.scripts)
val functions = HashSet<String>()
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()
}
}

View File

@ -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<Blackboard>() {
private val board = EnumMap<NodeParameterType, HashMap<String, Any>>(NodeParameterType::class.java)
private val input = EnumMap<NodeParameterType, HashMap<String, ArrayList<Pair<String, Table>>>>(NodeParameterType::class.java)
private val parameters = HashMap<Any, Table>()
// key -> list of Lua tables and their indices
private val vectorNumberInput = HashMap<String, HashSet<Pair<Any, Table>>>()
private var ephemeral = HashSet<Pair<NodeParameterType, String>>()
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<String, NodeParameter>, 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<Pair<NodeParameterType, String>> {
val ephemeral = ephemeral
this.ephemeral = HashSet()
return ephemeral
}
fun clearEphemerals(ephemerals: Collection<Pair<NodeParameterType, String>>) {
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()
}
}
}

View File

@ -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<String, NodeParameter>, 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()
}
}

View File

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

View File

@ -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<String, NodeParameter>, val children: ImmutableList<AbstractBehaviorNode>) : 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
}
}

View File

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

View File

@ -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<AbstractBehaviorNode>, 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
}
}

View File

@ -226,6 +226,10 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : 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<ObjectDefinition>) : TileEntit
return mergedJson.value[path] ?: JsonNull.INSTANCE
}
fun lookupPropertyOrNull(path: String): JsonElement? {
return mergedJson.value[path]
}
init {
networkGroup.upstream.add(uniqueID)
}

View File

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

View File

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

View File

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

View File

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