diff --git a/ADDITIONS.md b/ADDITIONS.md index 6990d228..86584574 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -39,7 +39,7 @@ val color: TileColor = TileColor.DEFAULT ``` * `item` brush now can accept proper item descriptors (in json object tag), * Previous behavior remains unchanged (if specified as string, creates _randomized_ item, if as object, creates _exactly_ what have been specified) - * To stop randomizing as Tiled tileset brush, specify `"randomize"` as `false` + * To stop randomizing as Tiled tileset brush, specify `"dont_randomize"` as anything (e.g. as `""`) * `liquid` brush now can accept 'level' as second argument * Previous behavior is unchanged, `["liquid", "water", true]` will result into infinite water as before, but `["liquid", "water", 0.5, false]` will spawn half-filled water * In tiled, you already can do this using `"quantity"` property @@ -62,3 +62,22 @@ val color: TileColor = TileColor.DEFAULT * Used by object and plant anchoring code to determine valid placement * Used by world tile rendering code (render piece rule `Connects`) * And finally, used by `canPlaceMaterial` to determine whenever player can place blocks next to it (at least one such tile should be present for player to be able to place blocks next to it) + +--------------- + +### Scripting + +#### animator + + * Added `animator.targetRotationAngle(rotationGroup: string): double` + * Added `animator.hasRotationGroup(rotationGroup: string): boolean` + * Added `animator.rotationGroups(): List` (returns valid names for `rotateGroup`, `currentRotationAngle` and `targetRotationAngle`) + * Added `animator.transformationGroups(): List` + * Added `animator.particleEmitters(): List` + * Added `animator.hasParticleEmitter(emitter: string): boolean` + * Added `animator.lights(): List` + * Added `animator.hasLight(light: string): boolean` + * Added `animator.sounds(): List` + * Added `animator.effects(): List` + * Added `animator.hasEffect(effect: string): boolean` + * Added `animator.parts(): List` \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..b224da42 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ + +### Starbound engine recreation project + +Make sure to specify next settings as startup options to JVM: + +``` +-Dfile.encoding=UTF8 +``` + diff --git a/build.gradle.kts b/build.gradle.kts index 4212f965..73bbdad9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -83,7 +83,7 @@ dependencies { implementation("ru.dbotthepony.kommons:kommons-gson-linear-algebra:[$kommonsVersion,)") { setTransitive(false) } implementation("com.github.ben-manes.caffeine:caffeine:3.1.5") - implementation("org.classdump.luna:luna-all-shaded:0.4.1") + implementation(project(":luna")) implementation("io.netty:netty-transport:4.1.105.Final") } diff --git a/settings.gradle.kts b/settings.gradle.kts index e61f6e86..92412feb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,2 @@ rootProject.name = "KStarBound" - +include("luna") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 99ee7733..ab9e45be 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -7,6 +7,9 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import kotlinx.coroutines.asCoroutineDispatcher import org.apache.logging.log4j.LogManager +import org.classdump.luna.compiler.CompilerChunkLoader +import org.classdump.luna.compiler.CompilerSettings +import org.classdump.luna.load.ChunkFactory import ru.dbotthepony.kommons.gson.AABBTypeAdapter import ru.dbotthepony.kommons.gson.AABBiTypeAdapter import ru.dbotthepony.kommons.gson.EitherTypeAdapter @@ -68,6 +71,7 @@ import java.io.* import java.lang.ref.Cleaner import java.text.DateFormat import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executor import java.util.concurrent.ExecutorService import java.util.concurrent.ForkJoinPool @@ -150,7 +154,7 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca // Hrm. // val strings: Interner = Interner.newWeakInterner() // val strings: Interner = Interner { it } - @JvmField + @JvmField val STRINGS: Interner = interner(5) // immeasurably lazy and fragile solution, too bad! @@ -220,6 +224,27 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca } } + private val loader = CompilerChunkLoader.of(CompilerSettings.defaultNoAccountingSettings(), "sb_lua_") + private val scriptCache = ConcurrentHashMap() + + private fun loadScript0(path: String): ChunkFactory { + val find = locate(path) + + if (!find.exists) { + throw NoSuchElementException("Script $path does not exist") + } + + return loader.compileTextChunk(path, find.readToString()) + } + + fun loadScript(path: String): ChunkFactory { + return scriptCache.computeIfAbsent(path, ::loadScript0) + } + + fun compileScriptChunk(name: String, chunk: String): ChunkFactory { + return loader.compileTextChunk(name, chunk) + } + val gson: Gson = with(GsonBuilder()) { // serializeNulls() setDateFormat(DateFormat.LONG) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt index 8b16d9dc..aa382114 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt @@ -66,7 +66,7 @@ data class AnimatedPartsDefinition( @JsonFactory data class State( val properties: JsonObject = JsonObject(), - val frameProperties: JsonObject = JsonObject(), + val frameProperties: ImmutableMap = ImmutableMap.of(), ) } } \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrushType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrushType.kt index 879cc781..b2592e79 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrushType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrushType.kt @@ -239,7 +239,7 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable override fun readTiled(json: JsonObject): DungeonBrush? { if ("item" in json) { - return DungeonBrush.DropItem(ItemDescriptor(json["item"].asString, json.get("count", 1L), json.get("parameters") { JsonObject() }), json.get("randomize", true)) + return DungeonBrush.DropItem(ItemDescriptor(json["item"].asString, json.get("count", 1L), json.get("parameters") { JsonObject() }), "dont_randomize" !in json) } return null diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt index 0ed1286b..8f560961 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt @@ -15,12 +15,11 @@ import org.classdump.luna.ByteString import org.classdump.luna.LuaRuntimeException import org.classdump.luna.Table import org.classdump.luna.TableFactory -import ru.dbotthepony.kommons.gson.consumeNull +import org.classdump.luna.runtime.ExecutionContext import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.lua.StateMachine import ru.dbotthepony.kstarbound.lua.from -import ru.dbotthepony.kstarbound.lua.toJsonObject import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kommons.io.StreamCodec @@ -36,6 +35,8 @@ import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.writeJsonElement +import ru.dbotthepony.kstarbound.lua.indexNoYield +import ru.dbotthepony.kstarbound.lua.toJson import java.io.DataInputStream import java.io.DataOutputStream import java.util.function.Supplier @@ -80,17 +81,34 @@ fun ItemDescriptor(data: Table, stateMachine: StateMachine): Supplier() + + for ((_, v) in table) { + vertices.add(toVector2d(v)) + } + + return Poly(vertices) +} + +fun ExecutionContext.toVector2f(table: Any): Vector2f { + val x = indexNoYield(table, 1L) + val y = indexNoYield(table, 2L) + + if (x !is Number) throw ClassCastException("Expected table representing a vector, but value at [1] is not a number: $x") + if (y !is Number) throw ClassCastException("Expected table representing a vector, but value at [2] is not a number: $y") + + return Vector2f(x.toFloat(), y.toFloat()) +} + +fun ExecutionContext.toColor(table: Any): RGBAColor { + val x = indexNoYield(table, 1L) + val y = indexNoYield(table, 2L) + val z = indexNoYield(table, 3L) + val w = indexNoYield(table, 4L) ?: 255 + + if (x !is Number) throw ClassCastException("Expected table representing a Color, but value at [1] is not a number: $x") + if (y !is Number) throw ClassCastException("Expected table representing a Color, but value at [2] is not a number: $y") + if (z !is Number) throw ClassCastException("Expected table representing a Color, but value at [3] is not a number: $z") + if (w !is Number) throw ClassCastException("Expected table representing a Color, but value at [4] is not a number: $w") + + return RGBAColor(x.toInt(), y.toInt(), z.toInt(), w.toInt()) +} + +fun ExecutionContext.toAABB(table: Any): AABB { + val x = indexNoYield(table, 1L) + val y = indexNoYield(table, 2L) + val z = indexNoYield(table, 3L) + val w = indexNoYield(table, 4L) + + if (x !is Number) throw ClassCastException("Expected table representing a AABB, but value at [1] is not a number: $x") + if (y !is Number) throw ClassCastException("Expected table representing a AABB, but value at [2] is not a number: $y") + if (z !is Number) throw ClassCastException("Expected table representing a AABB, but value at [3] is not a number: $z") + if (w !is Number) throw ClassCastException("Expected table representing a AABB, but value at [4] is not a number: $w") + + return AABB(Vector2d(x.toDouble(), y.toDouble()), Vector2d(z.toDouble(), w.toDouble())) +} + +fun toJsonFromLua(value: Any?): JsonElement { + return when (value) { + null, is JsonNull -> JsonNull.INSTANCE + is String -> JsonPrimitive(value) + is ByteString -> JsonPrimitive(value.decode()) + is Number -> JsonPrimitive(value) + is Boolean -> InternedJsonElementAdapter.of(value) + is Table -> value.toJson() + else -> throw IllegalArgumentException("Unable to translate $value into json!") + } +} + +fun Table.toJson(forceObject: Boolean = false): JsonElement { + val arrayValues = Long2ObjectAVLTreeMap() + val hashValues = HashMap() + + val meta = metatable + var hint = LUA_HINT_NONE + + if (meta != null) { + val getHint = meta["__typehint"] + + if (getHint is Number) { + hint = getHint.toLong() + } + + val nils = meta["__nils"] + + if (nils is Table) { + // Nil entries just have a garbage integer as their value + for ((k, v) in nils) { + val ik = k.toLuaInteger() + + if (ik != null) { + arrayValues[ik] = JsonNull.INSTANCE + } else { + hashValues[k.toString()] = JsonNull.INSTANCE + } + } + } + } + + for ((k, v) in this) { + val ik = k.toLuaInteger() + + if (ik != null) { + arrayValues[ik] = toJsonFromLua(v) + } else { + hashValues[k.toString()] = toJsonFromLua(v) + } + } + + val interpretAsList = !forceObject && hashValues.isEmpty() && hint != LUA_HINT_OBJECT + + if (interpretAsList) { + val list = JsonArray() + + for ((k, v) in arrayValues) { + val ik = k.toInt() - 1 + + while (list.size() < ik) { + list.add(JsonNull.INSTANCE) + } + + list[ik] = v + } + + return list + } else { + for ((k, v) in arrayValues) { + hashValues[(k - 1L).toString()] = v + } + + return JsonObject().apply { + for ((k, v) in hashValues) { + this[k] = v + } + } + } +} + +fun TableFactory.from(value: JsonElement?): Any? { + when (value) { + is JsonPrimitive -> { + if (value.isNumber) { + return value.asDouble + } else if (value.isString) { + return ByteString.of(value.asString) + } else if (value.isBoolean) { + return value.asBoolean + } else { + throw RuntimeException("unreachable code") + } + } + + is JsonArray -> return from(value) + is JsonObject -> return from(value) + null, is JsonNull -> return null + else -> throw RuntimeException(value::class.qualifiedName) + } +} + +private data class JsonTable(val metatable: Table, val nils: Table, val data: Table) + +fun Any?.toLuaInteger(): Long? { + if (this == null) + return null + else if (this is Long) + return this + else if (this is Double) { + if (this % 1.0 == 0.0) { + return this.toLong() + } else { + return null + } + } else if (this is ByteString) { + val decoded = decode() + + if (decoded.contains('.')) + return null + + return decoded.toLongOrNull() + } else { + return null + } +} + +const val LUA_HINT_NONE = 0L +const val LUA_HINT_ARRAY = 1L +const val LUA_HINT_OBJECT = 2L + +private object JsonTableIndex : AbstractFunction3() { + override fun resume(context: ExecutionContext?, suspendedState: Any?) { + throw NonsuspendableFunctionException(this::class.java) + } + + override fun invoke(context: ExecutionContext, self: Table, key: Any, value: Any?) { + val meta = self.metatable + val nils = meta["__nils"] as Table + + // If we are setting an entry to nil, need to add a bogus integer entry + // to the __nils table, otherwise need to set the entry *in* the __nils + // table to nil and remove it. + + // TODO: __newindex is called only when assigning non-existing keys to values, + // TODO: as per Lua manual. + // TODO: Chucklefish weren't aware of this? + + if (value == null) { + nils[key] = 0L + } else { + nils[key] = null as Any? + } + + self[key] = value + } +} + +private fun TableFactory.createJsonTable(typeHint: Long, size: Int, hash: Int): JsonTable { + val metatable = newTable() + val nils = newTable() + val data = newTable(size, hash) + + metatable["__newindex"] = JsonTableIndex + metatable["__nils"] = nils + metatable["__typehint"] = typeHint + + data.metatable = metatable + return JsonTable(metatable, nils, data) +} + +fun TableFactory.from(value: JsonObject): Table { + val (_, nils, data) = createJsonTable(LUA_HINT_OBJECT, 0, value.size()) + + for ((k, v) in value.entrySet()) { + if (v.isJsonNull) { + nils[k] = 0L + } else { + data[k] = from(v) + } + } + + return data +} + +fun TableFactory.from(value: JsonArray): Table { + val (_, nils, data) = createJsonTable(LUA_HINT_ARRAY, 0, value.size()) + + for ((i, v) in value.withIndex()) { + if (v.isJsonNull) { + nils[i + 1L] = 0L + } else { + data[i + 1L] = from(v) + } + } + + return data +} + +fun TableFactory.createJsonObject(): Table { + return createJsonTable(LUA_HINT_OBJECT, 0, 0).data +} + +fun TableFactory.createJsonArray(): Table { + return createJsonTable(LUA_HINT_ARRAY, 0, 0).data +} + +fun TableFactory.from(value: IStruct2d): Table { + return newTable(2, 0).apply { + this[1L] = value.component1() + this[2L] = value.component2() + } +} + +fun TableFactory.from(value: Poly): Table { + return newTable(value.vertices.size, 0).apply { + value.vertices.withIndex().forEach { (i, v) -> this[i + 1L] = from(v) } + } +} + +fun TableFactory.fromCollection(value: Collection): Table { + val table = newTable(value.size, 0) + + for ((k, v) in value.withIndex()) { + table.rawset(Conversions.normaliseKey(k + 1), from(v)) + } + + return table +} + +fun TableFactory.from(value: IStruct2i): Table { + return newTable(2, 0).also { + it.rawset(1L, value.component1()) + it.rawset(2L, value.component2()) + } +} + +fun TableFactory.from(value: IStruct3i): Table { + return newTable(3, 0).also { + it.rawset(1L, value.component1()) + it.rawset(2L, value.component2()) + it.rawset(3L, value.component3()) + } +} + +fun TableFactory.from(value: IStruct4i): Table { + return newTable(3, 0).also { + it.rawset(1L, value.component1()) + it.rawset(2L, value.component2()) + it.rawset(3L, value.component3()) + it.rawset(4L, value.component4()) + } +} + +fun TableFactory.from(value: RGBAColor): Table { + return newTable(3, 0).also { + it.rawset(1L, value.redInt.toLong()) + it.rawset(2L, value.greenInt.toLong()) + it.rawset(3L, value.blueInt.toLong()) + it.rawset(4L, value.alphaInt.toLong()) + } +} + +fun TableFactory.from(value: Collection): Table { + return newTable(value.size, 0).also { + for ((i, v) in value.withIndex()) { + it.rawset(i + 1L, v) + } + } +} + +fun TableFactory.from(value: AABB): Table { + return newTable(3, 0).also { + it.rawset(1L, value.mins.x) + it.rawset(2L, value.mins.y) + it.rawset(3L, value.maxs.x) + it.rawset(4L, value.maxs.y) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt index f15382d6..8a625217 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt @@ -1,33 +1,38 @@ package ru.dbotthepony.kstarbound.lua -import com.google.gson.JsonArray import com.google.gson.JsonElement -import com.google.gson.JsonNull -import com.google.gson.JsonObject -import com.google.gson.JsonPrimitive -import it.unimi.dsi.fastutil.longs.Long2ObjectAVLTreeMap -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectIterators -import org.classdump.luna.ByteString -import org.classdump.luna.Conversions import org.classdump.luna.LuaRuntimeException import org.classdump.luna.Table -import org.classdump.luna.TableFactory import org.classdump.luna.impl.NonsuspendableFunctionException import org.classdump.luna.lib.ArgumentIterator -import org.classdump.luna.lib.BadArgumentException import org.classdump.luna.runtime.AbstractFunction0 import org.classdump.luna.runtime.AbstractFunction1 import org.classdump.luna.runtime.AbstractFunction2 import org.classdump.luna.runtime.AbstractFunction3 import org.classdump.luna.runtime.AbstractFunction4 import org.classdump.luna.runtime.AbstractFunctionAnyArg +import org.classdump.luna.runtime.Dispatch import org.classdump.luna.runtime.ExecutionContext import org.classdump.luna.runtime.LuaFunction -import ru.dbotthepony.kommons.util.IStruct2i -import ru.dbotthepony.kommons.util.IStruct3i -import ru.dbotthepony.kommons.util.IStruct4i -import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter +import org.classdump.luna.runtime.UnresolvedControlThrowable + +fun ExecutionContext.indexNoYield(table: Any, key: Any): Any? { + return try { + Dispatch.index(this, table, key) + returnBuffer.get0() + } catch (err: UnresolvedControlThrowable) { + throw RuntimeException("attempt to yield across C boundary", err) + } +} + +fun ExecutionContext.indexSetNoYield(table: Any, key: Any, value: Any?) { + try { + Dispatch.setindex(this, table, key, value) + } catch (err: UnresolvedControlThrowable) { + throw RuntimeException("attempt to yield across C boundary", err) + } +} fun ArgumentIterator.nextOptionalFloat(): Double? { if (hasNext()) { @@ -94,161 +99,6 @@ operator fun Table.iterator(): Iterator> { } } -fun Table.toJson(): JsonElement { - val arrayValues = Long2ObjectAVLTreeMap() - val hashValues = HashMap() - - for ((k, v) in this) { - if (k is Number) { - arrayValues[k.toLong()] = v - } else { - hashValues[k] = v - } - } - - if (hashValues.isEmpty()) { - return JsonArray(arrayValues.size).also { - for (v in arrayValues.values) { - if (v is ByteString) { - it.add(JsonPrimitive(v.decode())) - } else if (v is Number) { - it.add(JsonPrimitive(v)) - } else if (v is Boolean) { - it.add(InternedJsonElementAdapter.of(v)) - } else if (v is Table) { - it.add(v.toJson()) - } - } - } - } else { - return JsonObject().also { - for ((k, v) in arrayValues) { - if (v is ByteString) { - it.add(k.toString(), JsonPrimitive(v.decode())) - } else if (v is Number) { - it.add(k.toString(), JsonPrimitive(v)) - } else if (v is Boolean) { - it.add(k.toString(), InternedJsonElementAdapter.of(v)) - } else if (v is Table) { - it.add(k.toString(), v.toJson()) - } - } - - for ((k, v) in hashValues) { - if (v is ByteString) { - it.add(k.toString(), JsonPrimitive(v.decode())) - } else if (v is Number) { - it.add(k.toString(), JsonPrimitive(v)) - } else if (v is Boolean) { - it.add(k.toString(), InternedJsonElementAdapter.of(v)) - } else if (v is Table) { - it.add(k.toString(), v.toJson()) - } - } - } - } -} - -fun Table.toJsonObject(): JsonObject { - return JsonObject().also { - for ((k, v) in this) { - if (v is ByteString) { - it.add(k.toString(), JsonPrimitive(v.decode())) - } else if (v is Number) { - it.add(k.toString(), JsonPrimitive(v)) - } else if (v is Boolean) { - it.add(k.toString(), InternedJsonElementAdapter.of(v)) - } else if (v is Table) { - it.add(k.toString(), v.toJson()) - } - } - } -} - -fun TableFactory.from(value: JsonElement?): Any? { - when (value) { - is JsonPrimitive -> { - if (value.isNumber) { - return value.asDouble - } else if (value.isString) { - return ByteString.of(value.asString) - } else if (value.isBoolean) { - return value.asBoolean - } else { - throw RuntimeException("unreachable code") - } - } - - is JsonNull -> return null - is JsonArray -> return from(value) - is JsonObject -> return from(value) - null -> return null - else -> throw RuntimeException(value::class.qualifiedName) - } -} - -fun TableFactory.from(value: JsonObject): Table { - val table = newTable(0, value.size()) - - for ((k, v) in value.entrySet()) { - table.rawset(Conversions.normaliseKey(k), from(v)) - } - - return table -} - -fun TableFactory.from(value: JsonArray): Table { - val table = newTable(value.size(), 0) - - for ((k, v) in value.withIndex()) { - table.rawset(Conversions.normaliseKey(k + 1), from(v)) - } - - return table -} - -fun TableFactory.fromCollection(value: Collection): Table { - val table = newTable(value.size, 0) - - for ((k, v) in value.withIndex()) { - table.rawset(Conversions.normaliseKey(k + 1), from(v)) - } - - return table -} - -fun TableFactory.from(value: IStruct2i): Table { - return newTable(2, 0).also { - it.rawset(1L, value.component1()) - it.rawset(2L, value.component2()) - } -} - -fun TableFactory.from(value: IStruct3i): Table { - return newTable(3, 0).also { - it.rawset(1L, value.component1()) - it.rawset(2L, value.component2()) - it.rawset(3L, value.component3()) - } -} - -fun TableFactory.from(value: IStruct4i): Table { - return newTable(3, 0).also { - it.rawset(1L, value.component1()) - it.rawset(2L, value.component2()) - it.rawset(3L, value.component3()) - it.rawset(4L, value.component4()) - } -} - -fun TableFactory.from(value: Collection): Table { - return newTable(value.size, 0).also { - for ((i, v) in value.withIndex()) { - it.rawset(i + 1L, v) - } - } -} - @Deprecated("Function is a stub") fun luaStub(message: String = "not yet implemented"): LuaFunction { return object : LuaFunction() { @@ -286,42 +136,19 @@ fun luaStub(message: String = "not yet implemented"): LuaFunction Any?): LuaFunction { - return object : AbstractFunction1() { - override fun resume(context: ExecutionContext, suspendedState: Any) { - throw NonsuspendableFunctionException(this::class.java) - } - - override fun invoke(context: ExecutionContext, arg: Any?) { - if (arg !is ByteString) - throw BadArgumentException(1, name, "string expected, got ${if (arg == null) "nil" else arg::class.qualifiedName}") - - val result = callable.invoke(arg.decode()) - - if (result != null && result !== Unit) { - context.returnBuffer.setTo(result) - } - } - } -} - -fun luaFunctionN(name: String, callable: (ExecutionContext, ArgumentIterator) -> Any?): LuaFunction { +fun luaFunctionN(name: String, callable: ExecutionContext.(ArgumentIterator) -> Unit): LuaFunction { return object : AbstractFunctionAnyArg() { override fun resume(context: ExecutionContext, suspendedState: Any) { throw NonsuspendableFunctionException(this::class.java) } override fun invoke(context: ExecutionContext, args: Array) { - val result = callable.invoke(context, ArgumentIterator.of(context, name, args)) - - if (result != null && result !== Unit) { - context.returnBuffer.setTo(result) - } + callable.invoke(context, ArgumentIterator.of(context, name, args)) } } } -fun luaFunctionNS(name: String, callable: (ExecutionContext, ArgumentIterator) -> StateMachine): LuaFunction { +fun luaFunctionNS(name: String, callable: ExecutionContext.(ArgumentIterator) -> StateMachine): LuaFunction { return object : AbstractFunctionAnyArg() { override fun resume(context: ExecutionContext, suspendedState: Any) { (suspendedState as StateMachine).run(context) @@ -333,7 +160,19 @@ fun luaFunctionNS(name: String, callable: (ExecutionContext, ArgumentIterator) - } } -fun luaFunction(callable: (ExecutionContext) -> Unit): LuaFunction<*, *, *, *, *> { +fun luaFunctionArray(callable: ExecutionContext.(Array) -> Unit): LuaFunction { + return object : AbstractFunctionAnyArg() { + override fun resume(context: ExecutionContext, suspendedState: Any) { + throw NonsuspendableFunctionException(this::class.java) + } + + override fun invoke(context: ExecutionContext, args: Array) { + callable.invoke(context, args) + } + } +} + +fun luaFunction(callable: ExecutionContext.() -> Unit): LuaFunction<*, *, *, *, *> { return object : AbstractFunction0() { override fun resume(context: ExecutionContext, suspendedState: Any) { throw NonsuspendableFunctionException(this::class.java) @@ -345,7 +184,7 @@ fun luaFunction(callable: (ExecutionContext) -> Unit): LuaFunction<*, *, *, *, * } } -fun luaFunction(callable: (ExecutionContext, T) -> Unit): LuaFunction { +fun luaFunction(callable: ExecutionContext.(T) -> Unit): LuaFunction { return object : AbstractFunction1() { override fun resume(context: ExecutionContext, suspendedState: Any) { throw NonsuspendableFunctionException(this::class.java) @@ -357,7 +196,7 @@ fun luaFunction(callable: (ExecutionContext, T) -> Unit): LuaFunction luaFunction(callable: (ExecutionContext, T, T2) -> Unit): LuaFunction { +fun luaFunction(callable: ExecutionContext.(T, T2) -> Unit): LuaFunction { return object : AbstractFunction2() { override fun resume(context: ExecutionContext, suspendedState: Any) { throw NonsuspendableFunctionException(this::class.java) @@ -369,7 +208,7 @@ fun luaFunction(callable: (ExecutionContext, T, T2) -> Unit): LuaFunctio } } -fun luaFunction(callable: (ExecutionContext, T, T2, T3) -> Unit): LuaFunction { +fun luaFunction(callable: ExecutionContext.(T, T2, T3) -> Unit): LuaFunction { return object : AbstractFunction3() { override fun resume(context: ExecutionContext, suspendedState: Any) { throw NonsuspendableFunctionException(this::class.java) @@ -381,7 +220,7 @@ fun luaFunction(callable: (ExecutionContext, T, T2, T3) -> Unit): Lu } } -fun luaFunction(callable: (ExecutionContext, T, T2, T3, T4) -> Unit): LuaFunction { +fun luaFunction(callable: ExecutionContext.(T, T2, T3, T4) -> Unit): LuaFunction { return object : AbstractFunction4() { override fun resume(context: ExecutionContext, suspendedState: Any) { throw NonsuspendableFunctionException(this::class.java) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt new file mode 100644 index 00000000..3a5e14ae --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt @@ -0,0 +1,264 @@ +package ru.dbotthepony.kstarbound.lua + +import it.unimi.dsi.fastutil.objects.ObjectArraySet +import org.apache.logging.log4j.LogManager +import org.classdump.luna.ByteString +import org.classdump.luna.LuaObject +import org.classdump.luna.LuaType +import org.classdump.luna.StateContext +import org.classdump.luna.Table +import org.classdump.luna.Variable +import org.classdump.luna.env.RuntimeEnvironments +import org.classdump.luna.exec.DirectCallExecutor +import org.classdump.luna.impl.DefaultTable +import org.classdump.luna.impl.NonsuspendableFunctionException +import org.classdump.luna.lib.BasicLib +import org.classdump.luna.lib.CoroutineLib +import org.classdump.luna.lib.MathLib +import org.classdump.luna.lib.OsLib +import org.classdump.luna.lib.StringLib +import org.classdump.luna.lib.TableLib +import org.classdump.luna.lib.Utf8Lib +import org.classdump.luna.load.ChunkFactory +import org.classdump.luna.runtime.AbstractFunction1 +import org.classdump.luna.runtime.ExecutionContext +import org.classdump.luna.runtime.LuaFunction +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.lua.bindings.provideRootBindings +import ru.dbotthepony.kstarbound.lua.bindings.provideUtilityBindings + +class LuaEnvironment : StateContext { + private var nilMeta: Table? = null + private var booleanMeta: Table? = null + private var numberMeta: Table? = null + private var stringMeta: Table? = null + private var functionMeta: Table? = null + private var threadMeta: Table? = null + private var lightUserdataMeta: Table? = null + + override fun getNilMetatable(): Table? { + return nilMeta + } + + override fun getBooleanMetatable(): Table? { + return booleanMeta + } + + override fun getNumberMetatable(): Table? { + return numberMeta + } + + override fun getStringMetatable(): Table? { + return stringMeta + } + + override fun getFunctionMetatable(): Table? { + return functionMeta + } + + override fun getThreadMetatable(): Table? { + return threadMeta + } + + override fun getLightUserdataMetatable(): Table? { + return lightUserdataMeta + } + + override fun getMetatable(instance: Any?): Table? { + if (instance is LuaObject) + return instance.metatable + + return when (val type = LuaType.typeOf(instance)!!) { + LuaType.NIL -> nilMeta + LuaType.BOOLEAN -> booleanMeta + LuaType.NUMBER -> numberMeta + LuaType.STRING -> stringMeta + LuaType.FUNCTION -> functionMeta + LuaType.USERDATA -> lightUserdataMeta + LuaType.THREAD -> threadMeta + else -> throw IllegalArgumentException("Illegal type: $type") + } + } + + override fun setNilMetatable(table: Table?): Table? { + val old = nilMeta; nilMeta = table; return old + } + + override fun setBooleanMetatable(table: Table?): Table? { + val old = booleanMeta; booleanMeta = table; return old + } + + override fun setNumberMetatable(table: Table?): Table? { + val old = numberMeta; numberMeta = table; return old + } + + override fun setStringMetatable(table: Table?): Table? { + val old = stringMeta; stringMeta = table; return old + } + + override fun setFunctionMetatable(table: Table?): Table? { + val old = functionMeta; functionMeta = table; return old + } + + override fun setThreadMetatable(table: Table?): Table? { + val old = threadMeta; threadMeta = table; return old + } + + override fun setLightUserdataMetatable(table: Table?): Table? { + val old = lightUserdataMeta; lightUserdataMeta = table; return old + } + + override fun setMetatable(instance: Any?, table: Table?): Table? { + if (instance is LuaObject) + return instance.setMetatable(table) + else + throw IllegalArgumentException("Can not set metatable of ${LuaType.typeOf(instance)}") + } + + override fun newTable(): Table { + return DefaultTable() + } + + override fun newTable(array: Int, hash: Int): Table { + return DefaultTable() + } + + val globals: Table = newTable() + val executor: DirectCallExecutor = DirectCallExecutor.newExecutor() + + init { + globals["_G"] = globals + + globals["assert"] = BasicLib.assertFn() + globals["error"] = BasicLib.error() + globals["getmetatable"] = BasicLib.getmetatable() + globals["ipairs"] = BasicLib.ipairs() + globals["next"] = BasicLib.next() + globals["pairs"] = BasicLib.pairs() + globals["pcall"] = BasicLib.pcall() + + globals["rawequal"] = BasicLib.rawequal() + globals["rawget"] = BasicLib.rawget() + globals["rawlen"] = BasicLib.rawlen() + globals["rawset"] = BasicLib.rawset() + globals["select"] = BasicLib.select() + globals["setmetatable"] = BasicLib.setmetatable() + globals["tostring"] = BasicLib.tostring() + globals["tonumber"] = BasicLib.tonumber() + globals["type"] = BasicLib.type() + globals["_VERSION"] = BasicLib._VERSION + globals["xpcall"] = BasicLib.xpcall() + + globals["print"] = PrintFunction(globals) + + globals["require"] = LuaRequire() + + // why not use _ENV anyway lol + globals["self"] = newTable() + + CoroutineLib.installInto(this, globals) + TableLib.installInto(this, globals) + MathLib.installInto(this, globals) + StringLib.installInto(this, globals) + OsLib.installInto(this, globals, RuntimeEnvironments.system()) + + // TODO: NYI, maybe polyfill? + Utf8Lib.installInto(this, globals) + + provideRootBindings(this) + provideUtilityBindings(this) + } + + private val scripts = ObjectArraySet() + private var initCalled = false + private val loadedScripts = ObjectArraySet() + + inner class LuaRequire : AbstractFunction1() { + override fun resume(context: ExecutionContext?, suspendedState: Any?) { + throw NonsuspendableFunctionException(this::class.java) + } + + override fun invoke(context: ExecutionContext, arg1: ByteString) { + val name = arg1.decode() + + if (loadedScripts.add(name)) { + val script = Starbound.loadScript(name) + executor.call(this@LuaEnvironment, script.newInstance(Variable(globals))) + } + } + } + + fun attach(script: ChunkFactory) { + scripts.add(script) + } + + fun attach(scripts: Collection) { + for (script in scripts) { + attach(Starbound.loadScript(script.fullPath)) + } + } + + fun run(chunk: ChunkFactory): Array { + return executor.call(this, chunk.newInstance(Variable(globals))) + } + + var errorState = false + private set + + fun markErrored() { + errorState = true + } + + fun init(): Boolean { + check(!initCalled) { "Already called init()" } + initCalled = true + + for (script in scripts) { + try { + executor.call(this, script.newInstance(Variable(globals))) + } catch (err: Throwable) { + errorState = true + LOGGER.error("Failed to attach script to environment", err) + scripts.clear() + return false + } + } + + scripts.clear() + + val init = globals["init"] + + if (init is LuaFunction<*, *, *, *, *>) { + try { + executor.call(this, init) + } catch (err: Throwable) { + errorState = true + LOGGER.error("Exception on init()", err) + return false + } + } + + return true + } + + fun invokeGlobal(name: String) { + if (errorState) + return + + val load = globals[name] + + if (load is LuaFunction<*, *, *, *, *>) { + try { + executor.call(this, load) + } catch (err: Throwable) { + errorState = true + throw err + } + } + } + + companion object { + private val LOGGER = LogManager.getLogger() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaMessageHandler.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaMessageHandler.kt new file mode 100644 index 00000000..112aa2c8 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaMessageHandler.kt @@ -0,0 +1,73 @@ +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 + +class LuaMessageHandler(val lua: LuaEnvironment) { + private val handlers = HashMap>() + + init { + val table = lua.newTable() + + table["setHandler"] = luaFunction { name: ByteString, handler: LuaFunction<*, *, *, *, *>? -> + if (handler == null) { + handlers.remove(name.decode()) + } else { + handlers[name.decode()] = handler + } + } + + lua.globals["message"] = table + } + + fun handle(message: String, isLocal: Boolean, parameters: JsonArray): JsonElement { + if (lua.errorState) + return JsonNull.INSTANCE + + val handler = handlers[message] ?: return JsonNull.INSTANCE + + try { + val unpacked = arrayOfNulls(parameters.size() + 2) + + unpacked[0] = ByteString.of(message) + unpacked[1] = isLocal + + for ((i, v) in parameters.withIndex()) { + unpacked[i + 2] = lua.from(v) + } + + val result = lua.executor.call(lua, handler, *unpacked) + + if (result.isEmpty()) { + return JsonNull.INSTANCE + } else if (result.size == 1) { + return toJsonFromLua(result[0]) + } else { + val array = JsonArray() + + for (v in result) { + array.add(toJsonFromLua(v)) + } + + return array + } + } catch (err: CallPausedException) { + lua.markErrored() + LOGGER.error("Message handler for $message tried to yield", err) + return JsonNull.INSTANCE + } catch (err: Throwable) { + lua.markErrored() + LOGGER.error("Message handler for $message errored", err) + return JsonNull.INSTANCE + } + } + + companion object { + private val LOGGER = LogManager.getLogger() + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaRequire.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaRequire.kt deleted file mode 100644 index a3a73c39..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaRequire.kt +++ /dev/null @@ -1,30 +0,0 @@ -package ru.dbotthepony.kstarbound.lua - -import org.classdump.luna.ByteString -import org.classdump.luna.LuaRuntimeException -import org.classdump.luna.impl.NonsuspendableFunctionException -import org.classdump.luna.runtime.AbstractFunction1 -import org.classdump.luna.runtime.ExecutionContext -import ru.dbotthepony.kstarbound.Starbound - -class LuaRequire(private val state: NewLuaState) : AbstractFunction1() { - override fun resume(context: ExecutionContext?, suspendedState: Any?) { - throw NonsuspendableFunctionException(this::class.java) - } - - override fun invoke(context: ExecutionContext, arg1: ByteString) { - val name = arg1.decode() - - val file = Starbound.locate(name) - - if (!file.exists) { - throw LuaRuntimeException("File $name does not exist") - } - - if (!file.isFile) { - throw LuaRuntimeException("File $name is a directory") - } - - state.load(context, file.readToString(), file.computeFullPath()) - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt new file mode 100644 index 00000000..0b9b5391 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt @@ -0,0 +1,28 @@ +package ru.dbotthepony.kstarbound.lua + +import ru.dbotthepony.kstarbound.Starbound + +class LuaUpdateComponent(val lua: LuaEnvironment) { + var stepCount = 1 + private var steps = 0 + + init { + val script = lua.newTable() + lua.globals["script"] = script + + script["updateDt"] = luaFunction { + returnBuffer.setTo(stepCount * Starbound.TIMESTEP) + } + + script["setUpdateDelta"] = luaFunction { ticks: Int -> + stepCount = ticks + } + } + + fun update() { + if (steps++ >= stepCount) { + steps = 0 + lua.invokeGlobal("update") + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/NewLuaState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/NewLuaState.kt deleted file mode 100644 index 7a6087cd..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/NewLuaState.kt +++ /dev/null @@ -1,77 +0,0 @@ -package ru.dbotthepony.kstarbound.lua - -import org.classdump.luna.StateContext -import org.classdump.luna.Table -import org.classdump.luna.Variable -import org.classdump.luna.compiler.CompilerChunkLoader -import org.classdump.luna.compiler.CompilerSettings -import org.classdump.luna.env.RuntimeEnvironments -import org.classdump.luna.exec.DirectCallExecutor -import org.classdump.luna.impl.StateContexts -import org.classdump.luna.lib.BasicLib -import org.classdump.luna.lib.CoroutineLib -import org.classdump.luna.lib.MathLib -import org.classdump.luna.lib.OsLib -import org.classdump.luna.lib.StringLib -import org.classdump.luna.lib.TableLib -import org.classdump.luna.lib.Utf8Lib -import org.classdump.luna.runtime.Dispatch -import org.classdump.luna.runtime.ExecutionContext -import java.util.concurrent.atomic.AtomicInteger - -class NewLuaState { - val state: StateContext = StateContexts.newDefaultInstance() - val chunkLoader: CompilerChunkLoader = CompilerChunkLoader.of(CompilerSettings.defaultNoAccountingSettings(), "kstarbound_${COUNTER.getAndIncrement()}_") - val env: Table = state.newTable() - val executor: DirectCallExecutor = DirectCallExecutor.newExecutor() - - init { - env["_G"] = env - - env["assert"] = BasicLib.assertFn() - env["error"] = BasicLib.error() - env["getmetatable"] = BasicLib.getmetatable() - env["ipairs"] = BasicLib.ipairs() - env["next"] = BasicLib.next() - env["pairs"] = BasicLib.pairs() - env["pcall"] = BasicLib.pcall() - - env["rawequal"] = BasicLib.rawequal() - env["rawget"] = BasicLib.rawget() - env["rawlen"] = BasicLib.rawlen() - env["rawset"] = BasicLib.rawset() - env["select"] = BasicLib.select() - env["setmetatable"] = BasicLib.setmetatable() - env["tostring"] = BasicLib.tostring() - env["tonumber"] = BasicLib.tonumber() - env["type"] = BasicLib.type() - env["_VERSION"] = BasicLib._VERSION - env["xpcall"] = BasicLib.xpcall() - - env["print"] = PrintFunction(env) - - env["require"] = LuaRequire(this) - - CoroutineLib.installInto(state, env) - TableLib.installInto(state, env) - MathLib.installInto(state, env) - StringLib.installInto(state, env) - OsLib.installInto(state, env, RuntimeEnvironments.system()) - - // TODO: NYI, maybe polyfill? - Utf8Lib.installInto(state, env) - } - - fun load(code: String, name: String = "main chunk") { - val main = chunkLoader.loadTextChunk(Variable(env), name, code) - executor.call(state, main) - } - - fun load(context: ExecutionContext, code: String, name: String = "main chunk") { - Dispatch.call(context, chunkLoader.loadTextChunk(Variable(env), name, code)) - } - - companion object { - private val COUNTER = AtomicInteger() - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/AnimatorBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/AnimatorBindings.kt new file mode 100644 index 00000000..68e08ca1 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/AnimatorBindings.kt @@ -0,0 +1,260 @@ +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.kommons.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.toColor +import ru.dbotthepony.kstarbound.lua.toPoly +import ru.dbotthepony.kstarbound.lua.toVector2d +import ru.dbotthepony.kstarbound.lua.toVector2f +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 } + returnBuffer.setTo(keys) + } + + callbacks["hasTransformationGroup"] = luaFunction { group: ByteString -> + returnBuffer.setTo(self.hasTransformationGroup(group.decode())) + } + + callbacks["transformationGroups"] = luaFunction { + val groups = self.transformationGroups() + val keys = newTable(groups.size, 0) + groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v } + returnBuffer.setTo(keys) + } + + callbacks["translateTransformGroup"] = luaFunction { group: ByteString, translation: Table -> + self.translateTransformGroup(group.decode(), toVector2f(translation)) + } + + callbacks["rotateTransformGroup"] = luaFunction { group: ByteString, rotation: Double, center: Table? -> + self.rotateTransformGroup(group.decode(), rotation.toFloat(), if (center == null) Vector2f.ZERO else toVector2f(center)) + } + + callbacks["scaleTransformationGroup"] = luaFunction { group: ByteString, scale: Any, center: Table? -> + if (scale is Number) { + self.scaleTransformationGroup(group.decode(), Vector2f(scale.toFloat(), scale.toFloat()), if (center == null) Vector2f.ZERO else toVector2f(center)) + } else { + self.scaleTransformationGroup(group.decode(), toVector2f(scale), if (center == null) Vector2f.ZERO else toVector2f(center)) + } + } + + callbacks["transformTransformationGroup"] = luaFunctionArray { arguments: Array -> + val group = arguments[0] as ByteString + val r00 = arguments[1] as Number + val r01 = arguments[2] as Number + val r02 = arguments[3] as Number + val r10 = arguments[4] as Number + val r11 = arguments[5] as Number + val r12 = arguments[6] as Number + + self.transformTransformationGroup(group.decode(), r00.toFloat(), + r01.toFloat(), + r02.toFloat(), + r10.toFloat(), + r11.toFloat(), + r12.toFloat()) + } + + callbacks["resetTransformationGroup"] = luaFunction { group: ByteString -> + self.resetTransformationGroup(group.decode()) + } + + callbacks["particleEmitters"] = luaFunction { + val groups = self.particleEmitters() + val keys = newTable(groups.size, 0) + groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v } + returnBuffer.setTo(keys) + } + + callbacks["hasParticleEmitter"] = luaFunction { group: ByteString -> + returnBuffer.setTo(self.hasParticleEmitter(group.decode())) + } + + callbacks["setParticleEmitterActive"] = luaFunction { emitter: ByteString, state: Boolean -> + self.setParticleEmitterActive(emitter.decode(), state) + } + + callbacks["setParticleEmitterEmissionRate"] = luaFunction { emitter: ByteString, rate: Number -> + self.setParticleEmitterEmissionRate(emitter.decode(), rate.toDouble()) + } + + callbacks["setParticleEmitterBurstCount"] = luaFunction { emitter: ByteString, count: Number -> + self.setParticleEmitterBurstCount(emitter.decode(), count.toInt()) + } + + callbacks["setParticleEmitterOffsetRegion"] = luaFunction { emitter: ByteString, region: Table -> + self.setParticleEmitterOffsetRegion(emitter.decode(), toAABB(region)) + } + + callbacks["burstParticleEmitter"] = luaFunction { emitter: ByteString -> + self.burstParticleEmitter(emitter.decode()) + } + + callbacks["lights"] = luaFunction { + val groups = self.lights() + val keys = newTable(groups.size, 0) + groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v } + returnBuffer.setTo(keys) + } + + callbacks["hasLight"] = luaFunction { light: ByteString -> + returnBuffer.setTo(self.hasLight(light.decode())) + } + + callbacks["setLightActive"] = luaFunction { light: ByteString, state: Boolean -> + self.setLightActive(light.decode(), state) + } + + callbacks["setLightPosition"] = luaFunction { light: ByteString, position: Table -> + self.setLightPosition(light.decode(), toVector2d(position)) + } + + callbacks["setLightColor"] = luaFunction { light: ByteString, color: Table -> + self.setLightColor(light.decode(), toColor(color)) + } + + callbacks["setLightPointAngle"] = luaFunction { light: ByteString, angle: Number -> + self.setLightPointAngle(light.decode(), angle.toDouble()) + } + + callbacks["sounds"] = luaFunction { + val groups = self.sounds() + val keys = newTable(groups.size, 0) + groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v } + returnBuffer.setTo(keys) + } + + callbacks["hasSound"] = luaFunction { sound: ByteString -> + returnBuffer.setTo(self.hasSound(sound.decode())) + } + + callbacks["setSoundPool"] = luaFunction { sound: ByteString, sounds: Table -> + self.setSoundPool(sound.decode(), sounds.iterator().map { (it.value as ByteString).decode() }.toList()) + } + + callbacks["setSoundPosition"] = luaFunction { sound: ByteString, position: Table -> + self.setSoundPosition(sound.decode(), toVector2d(position)) + } + + callbacks["playSound"] = luaFunction { sound: ByteString, loops: Number? -> + self.playSound(sound.decode(), loops?.toInt() ?: 0) + } + + callbacks["setSoundVolume"] = luaFunction { sound: ByteString, volume: Number, rampTime: Number? -> + self.setSoundVolume(sound.decode(), volume.toDouble(), rampTime?.toDouble() ?: 0.0) + } + + callbacks["setSoundPitch"] = luaFunction { sound: ByteString, pitch: Number, rampTime: Number? -> + self.setSoundPitch(sound.decode(), pitch.toDouble(), rampTime?.toDouble() ?: 0.0) + } + + callbacks["stopAllSounds"] = luaFunction { sound: ByteString, rampTime: Number? -> + self.stopAllSounds(sound.decode(), rampTime?.toDouble() ?: 0.0) + } + + callbacks["effects"] = luaFunction { + val groups = self.effects() + val keys = newTable(groups.size, 0) + groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v } + returnBuffer.setTo(keys) + } + + callbacks["hasEffect"] = luaFunction { effect: ByteString -> + returnBuffer.setTo(self.hasEffect(effect.decode())) + } + + callbacks["setEffectActive"] = luaFunction { effect: ByteString, state: Boolean -> + self.setEffectActive(effect.decode(), state) + } + + callbacks["parts"] = luaFunction { + val groups = self.parts() + val keys = newTable(groups.size, 0) + groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v } + returnBuffer.setTo(keys) + } + + callbacks["partPoint"] = luaFunction { part: ByteString, property: ByteString -> + returnBuffer.setTo(self.partPoint(part.decode(), property.decode())?.let { from(it) }) + } + + callbacks["partPoly"] = luaFunction { part: ByteString, property: ByteString -> + returnBuffer.setTo(self.partPoly(part.decode(), property.decode())?.let { from(it) }) + } + + callbacks["partProperty"] = luaFunction { part: ByteString, property: ByteString -> + returnBuffer.setTo(from(self.partProperty(part.decode(), property.decode()))) + } + + callbacks["transformPoint"] = luaFunction { point: Table, part: ByteString -> + returnBuffer.setTo(from(toVector2d(point).times(self.partTransformation(part.decode())))) + } + + callbacks["transformPoly"] = luaFunction { poly: Table, part: ByteString -> + returnBuffer.setTo(from(toPoly(poly).transform(self.partTransformation(part.decode())))) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/AssetJsonFunction.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/AssetJsonFunction.kt similarity index 91% rename from src/main/kotlin/ru/dbotthepony/kstarbound/lua/AssetJsonFunction.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/AssetJsonFunction.kt index e7966962..f30bc682 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/AssetJsonFunction.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/AssetJsonFunction.kt @@ -1,4 +1,4 @@ -package ru.dbotthepony.kstarbound.lua +package ru.dbotthepony.kstarbound.lua.bindings import org.classdump.luna.ByteString import org.classdump.luna.LuaRuntimeException @@ -7,6 +7,7 @@ import org.classdump.luna.impl.NonsuspendableFunctionException import org.classdump.luna.runtime.AbstractFunction1 import org.classdump.luna.runtime.ExecutionContext import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.lua.from class AssetJsonFunction(private val tables: TableFactory) : AbstractFunction1() { override fun resume(context: ExecutionContext?, suspendedState: Any?) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt new file mode 100644 index 00000000..02b28fd6 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt @@ -0,0 +1,42 @@ +package ru.dbotthepony.kstarbound.lua.bindings + +import ru.dbotthepony.kommons.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.world.entities.AbstractEntity + +fun provideEntityBindings(self: AbstractEntity, lua: LuaEnvironment) { + val table = lua.newTable() + lua.globals["entity"] = table + + table["id"] = luaFunction { returnBuffer.setTo(self.entityID) } + table["position"] = luaFunction { returnBuffer.setTo(from(self.position)) } + table["entityType"] = luaFunction { returnBuffer.setTo(self.type.jsonName) } + table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get().orNull()) } + table["persistent"] = luaFunction { returnBuffer.setTo(self.isPersistent) } + + table["entityInSight"] = luaFunction { TODO() } + table["isValidTarget"] = luaFunction { TODO() } + + table["damageTeam"] = luaFunction { + val result = newTable() + + result["team"] = self.team.get().team + result["type"] = self.type.jsonName + + 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)) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt index e378278a..d40b2359 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.lua.bindings +import it.unimi.dsi.fastutil.longs.LongArrayList import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap import org.classdump.luna.ByteString import org.classdump.luna.LuaRuntimeException @@ -14,10 +15,15 @@ import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.image.Image import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor -import ru.dbotthepony.kstarbound.lua.AssetJsonFunction -import ru.dbotthepony.kstarbound.lua.NewLuaState +import ru.dbotthepony.kstarbound.lua.LUA_HINT_ARRAY +import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.lua.StateMachine +import ru.dbotthepony.kstarbound.lua.createJsonArray +import ru.dbotthepony.kstarbound.lua.createJsonObject import ru.dbotthepony.kstarbound.lua.from +import ru.dbotthepony.kstarbound.lua.get +import ru.dbotthepony.kstarbound.lua.indexNoYield +import ru.dbotthepony.kstarbound.lua.indexSetNoYield import ru.dbotthepony.kstarbound.lua.iterator import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.luaFunctionN @@ -26,12 +32,14 @@ import ru.dbotthepony.kstarbound.lua.luaStub import ru.dbotthepony.kstarbound.lua.nextOptionalFloat import ru.dbotthepony.kstarbound.lua.nextOptionalInteger import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.lua.toLuaInteger import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.isNotEmpty import kotlin.collections.random import kotlin.collections.set import kotlin.collections.withIndex +import kotlin.math.max private fun lookup(registry: Registry, key: Any?): Registry.Entry? { if (key is ByteString) { @@ -94,30 +102,30 @@ private fun nonEmptyRegion(context: ExecutionContext, name: ByteString) { } private fun registryDef(registry: Registry<*>): LuaFunction { - return luaFunction { context, name -> + return luaFunction { name -> val value = registry[name.decode()] ?: throw LuaRuntimeException("No such NPC type $name") - context.returnBuffer.setTo(context.from(value.json)) + returnBuffer.setTo(from(value.json)) } } private fun registryDef2(registry: Registry<*>): LuaFunction { - return luaFunction { context, name -> + return luaFunction { name -> val def = lookup(registry, name) if (def != null) { - context.returnBuffer.setTo(context.newTable(0, 2).also { + returnBuffer.setTo(newTable(0, 2).also { it["path"] = def.file?.computeFullPath() - it["config"] = context.from(def.json) + it["config"] = from(def.json) }) } else { - context.returnBuffer.setTo() + returnBuffer.setTo() } } } private fun registryDefExists(registry: Registry<*>): LuaFunction { - return luaFunction { context, name -> - context.returnBuffer.setTo(name.decode() in registry) + return luaFunction { name -> + returnBuffer.setTo(name.decode() in registry) } } @@ -278,11 +286,105 @@ private fun techConfig(context: ExecutionContext, arguments: ArgumentIterator) { context.returnBuffer.setTo(lookupStrict(Registries.techs, arguments.nextAny()).json) } -fun provideRootBindings(state: NewLuaState) { - val table = state.state.newTable() - state.env["root"] = table +private val jobject = luaFunction { returnBuffer.setTo(createJsonObject()) } +private val jarray = luaFunction { returnBuffer.setTo(createJsonArray()) } - table["assetJson"] = AssetJsonFunction(state.state) +private val jremove = luaFunction { self: Table, key: Any -> + val nils = self.metatable?.rawget("__nils") as? Table + + if (nils != null) { + nils[key] = 0L + } + + self[key] = null as Any? +} + +private val jsize = luaFunction { self: Table -> + var elemCount = 0L + var highestIndex = 0L + var hintList = false + + val meta = self.metatable + + if (meta != null) { + if (meta["__typehint"] == LUA_HINT_ARRAY) { + hintList = true + } + + val nils = meta["__nils"] + + if (nils is Table) { + for ((k, v) in nils) { + val ik = k.toLuaInteger() + + if (ik != null) { + highestIndex = max(ik, highestIndex) + } else { + hintList = false + } + } + } + } + + for ((k, v) in self) { + val ik = k.toLuaInteger() + + if (ik != null) { + highestIndex = max(ik, highestIndex) + } else { + hintList = false + } + + elemCount++ + } + + if (hintList) { + returnBuffer.setTo(highestIndex) + } else { + returnBuffer.setTo(elemCount) + } +} + +// why is this a thing? +private val jresize = luaFunction { self: Table, target: Long -> + val nils = self.metatable?.rawget("__nils") as? Table + + if (nils != null) { + val keysToRemove = ArrayList() + + for ((k, v) in nils) { + val ik = k.toLuaInteger() + + if (ik != null && ik > 0L && ik > target) + keysToRemove.add(k) + } + + for (k in keysToRemove) { + nils[k] = null as Any? + } + } + + val keysToRemove = ArrayList() + + for ((k, v) in self) { + val ik = k.toLuaInteger() + + if (ik != null && ik > 0L && ik > target) + keysToRemove.add(k) + } + + for (k in keysToRemove) { + self[k] = null as Any? + } + + indexSetNoYield(self, target, indexNoYield(self, target)) +} + +fun provideRootBindings(lua: LuaEnvironment) { + val table = lua.newTable() + lua.globals["root"] = table + + table["assetJson"] = AssetJsonFunction(lua) table["makeCurrentVersionedJson"] = luaStub("makeCurrentVersionedJson") table["loadVersionedJson"] = luaStub("loadVersionedJson") @@ -346,6 +448,9 @@ fun provideRootBindings(state: NewLuaState) { table["dungeonMetadata"] = luaStub("dungeonMetadata") table["behavior"] = luaStub("behavior") - state.env["jobject"] = luaFunction { executionContext -> executionContext.returnBuffer.setTo(executionContext.newTable()) } - state.env["jarray"] = luaFunction { executionContext -> executionContext.returnBuffer.setTo(executionContext.newTable()) } + lua.globals["jobject"] = jobject + lua.globals["jarray"] = jarray + lua.globals["jremove"] = jremove + lua.globals["jsize"] = jsize + lua.globals["jresize"] = jresize } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt new file mode 100644 index 00000000..d5062be8 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt @@ -0,0 +1,135 @@ +package ru.dbotthepony.kstarbound.lua.bindings + +import org.apache.logging.log4j.LogManager +import org.classdump.luna.ByteString +import org.classdump.luna.Table +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +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.luaFunctionArray +import ru.dbotthepony.kstarbound.lua.luaFunctionN +import ru.dbotthepony.kstarbound.lua.nextOptionalFloat +import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.lua.toJson +import ru.dbotthepony.kstarbound.lua.toVector2d +import ru.dbotthepony.kstarbound.lua.userdata.LuaPerlinNoise +import ru.dbotthepony.kstarbound.lua.userdata.LuaRandomGenerator +import ru.dbotthepony.kstarbound.math.Interpolator +import ru.dbotthepony.kstarbound.util.SBPattern +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise +import ru.dbotthepony.kstarbound.util.random.nextNormalDouble +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.util.random.staticRandom32 +import ru.dbotthepony.kstarbound.util.random.staticRandomDouble +import ru.dbotthepony.kstarbound.util.random.staticRandomLong +import ru.dbotthepony.kstarbound.util.toStarboundString +import java.util.* +import java.util.random.RandomGenerator + +private val LOGGER = LogManager.getLogger() + +private val logInfo = luaFunctionN("logInfo") { args -> + LOGGER.info(args.nextString().toString().format(*args.copyRemaining())) +} + +private val logWarn = luaFunctionN("logWarn") { args -> + LOGGER.warn(args.nextString().toString().format(*args.copyRemaining())) +} + +private val logError = luaFunctionN("logError") { args -> + LOGGER.error(args.nextString().toString().format(*args.copyRemaining())) +} + +private val interpolateSinEase = luaFunctionArray { args -> + if (args.size < 3) + throw IllegalArgumentException("Invalid amount of arguments to interpolateSinEase: ${args.size}") + + val offset = args[0] as? Number ?: throw IllegalArgumentException("Invalid 'offset' argument: ${args[0]}") + + if (args[1] is Number && args[2] is Number) { + returnBuffer.setTo(Interpolator.Sin.interpolate(offset.toDouble(), (args[1] as Number).toDouble(), (args[2] as Number).toDouble())) + } else { + // assume vectors + val a = toVector2d(args[1]!!) + val b = toVector2d(args[2]!!) + + val result = Vector2d( + Interpolator.Sin.interpolate(offset.toDouble(), a.x, b.x), + Interpolator.Sin.interpolate(offset.toDouble(), a.y, b.y), + ) + + returnBuffer.setTo(from(result)) + } +} + +private val replaceTags = luaFunction { string: ByteString, tags: Table -> + returnBuffer.setTo(SBPattern.of(string.toString()).resolveOrSkip({ tags[it]?.toString() })) +} + +private val makePerlinSource = luaFunction { settings: Table -> + returnBuffer.setTo(LuaPerlinNoise(AbstractPerlinNoise.of(Starbound.gson.fromJson(settings.toJson(), PerlinNoiseParameters::class.java)))) +} + +private val staticRandomI32 = luaFunctionArray { + returnBuffer.setTo(staticRandom32(*it)) +} + +private val staticRandomDouble = luaFunctionArray { + returnBuffer.setTo(staticRandomDouble(*it)) +} + +private val staticRandomDoubleRange = luaFunctionN("staticRandomDoubleRange") { + val min = it.nextFloat() + val max = it.nextFloat() + returnBuffer.setTo(staticRandomDouble(*it.copyRemaining()) * (max - min) + min) +} + +private val staticRandomI32Range = luaFunctionN("staticRandomI32Range") { + val min = it.nextInteger() + val max = it.nextInteger() + returnBuffer.setTo(staticRandomLong(min, max, *it.copyRemaining())) +} + +fun provideUtilityBindings( + lua: LuaEnvironment, + random: RandomGenerator = random() +) { + val table = lua.newTable() + lua.globals["sb"] = table + + table["makeUuid"] = luaFunction { + returnBuffer.setTo(UUID(random.nextLong(), random.nextLong()).toStarboundString()) + } + + table["logInfo"] = logInfo + table["logWarn"] = logWarn + table["logError"] = logError + + table["nrand"] = luaFunctionN("nrand") { args -> + val stdev = args.nextOptionalFloat() ?: 1.0 + val mean = args.nextOptionalFloat() ?: 0.0 + random.nextNormalDouble(stdev, mean) + } + + table["print"] = lua.globals["tostring"] + table["printJson"] = lua.globals["tostring"] + table["interpolateSinEase"] = interpolateSinEase + table["replaceTags"] = replaceTags + table["makeRandomSource"] = luaFunction { seed: Long? -> + returnBuffer.setTo(LuaRandomGenerator(random(seed ?: random.nextLong()))) + } + + table["makePerlinSource"] = makePerlinSource + + table["staticRandomI32"] = staticRandomI32 + table["staticRandomI64"] = staticRandomI32 + table["staticRandomDouble"] = staticRandomDouble + + table["staticRandomDoubleRange"] = staticRandomDoubleRange + table["staticRandomI32Range"] = staticRandomI32Range + table["staticRandomI64Range"] = staticRandomI32Range +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt new file mode 100644 index 00000000..084e981f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt @@ -0,0 +1,16 @@ +package ru.dbotthepony.kstarbound.lua.bindings + +import ru.dbotthepony.kstarbound.lua.LuaEnvironment +import ru.dbotthepony.kstarbound.lua.get +import ru.dbotthepony.kstarbound.lua.luaFunction +import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.world.World + +fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { + val callbacks = lua.newTable() + lua.globals["world"] = callbacks + + callbacks["flyingType"] = luaFunction { + returnBuffer.setTo(self.sky.flyingType.jsonName) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt new file mode 100644 index 00000000..50340291 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt @@ -0,0 +1,235 @@ +package ru.dbotthepony.kstarbound.lua.bindings + +import com.google.gson.JsonPrimitive +import org.classdump.luna.ByteString +import org.classdump.luna.Table +import ru.dbotthepony.kommons.util.KOptional +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.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.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.util.SBPattern +import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject + +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) + + 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) } + table["direction"] = luaFunction { returnBuffer.setTo(self.direction.luaValue) } + table["position"] = luaFunction { returnBuffer.setTo(from(self.tilePosition)) } + table["setInteractive"] = luaFunction { interactive: Boolean -> self.isInteractive = interactive } + table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get().orNull()) } + table["setUniqueId"] = luaFunction { id: ByteString? -> self.uniqueID.accept(KOptional.ofNullable(id?.decode())) } + table["boundBox"] = luaFunction { returnBuffer.setTo(from(self.metaBoundingBox)) } + + // original engine parity, it returns occupied spaces in local coordinates + table["spaces"] = luaFunction { returnBuffer.setTo(from(self.occupySpaces.map { from(it - self.tilePosition) })) } + + table["setProcessingDirectives"] = luaFunction { directives: ByteString -> self.animator.processingDirectives = directives.decode() } + table["setSoundEffectEnabled"] = luaFunction { state: Boolean -> self.soundEffectEnabled = state } + table["smash"] = luaFunction { smash: Boolean? -> self.callBreak(smash ?: false) } + 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?, config: Table -> + if (tags == null) { + if (line.isEmpty) { + returnBuffer.setTo(false) + } else { + self.addChatMessage(line.decode(), config.toJson()) + returnBuffer.setTo(true) + } + } else { + if (line.isEmpty) { + returnBuffer.setTo(false) + } else { + self.addChatMessage(SBPattern.of(line.decode()).resolveOrSkip({ tags[it]?.toString() }), config.toJson()) + 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) } + table["outputNodeCount"] = luaFunction { returnBuffer.setTo(self.outputNodes.size) } + + 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) as? WorldObject + + 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) as? WorldObject + + 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) { + v as Table + self.offeredQuests.add(Starbound.gson.fromJson(v.toJson(), QuestArcDescriptor::class.java)) + } + } + } + + 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.networkedMaterialSpaces.clear() + + 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.networkedMaterialSpaces.add(position to Registries.tiles.ref(material.decode())) + } + + self.markSpacesDirty() + } + + table["setDamageSources"] = luaFunction { sources: Table? -> + self.damageSources.clear() + + if (sources != null) { + for ((_, v) in sources) { + self.damageSources.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 } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPerlinNoise.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPerlinNoise.kt new file mode 100644 index 00000000..99eaca3b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPerlinNoise.kt @@ -0,0 +1,43 @@ +package ru.dbotthepony.kstarbound.lua.userdata + +import org.classdump.luna.Table +import org.classdump.luna.Userdata +import org.classdump.luna.impl.ImmutableTable +import ru.dbotthepony.kstarbound.lua.luaFunction +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise + +class LuaPerlinNoise(val noise: AbstractPerlinNoise) : Userdata() { + private var metatable: Table? = Companion.metatable + + override fun getMetatable(): Table? { + return metatable + } + + override fun setMetatable(mt: Table?): Table? { + val old = metatable + metatable = mt + return old + } + + override fun getUserValue(): AbstractPerlinNoise { + return noise + } + + override fun setUserValue(value: AbstractPerlinNoise?): AbstractPerlinNoise { + throw UnsupportedOperationException() + } + + companion object { + private val metatable = ImmutableTable.Builder() + .add("get", luaFunction { self: LuaPerlinNoise, x: Number, y: Number?, z: Number? -> + if (y != null && z != null) { + returnBuffer.setTo(self.noise[x.toDouble(), y.toDouble(), z.toDouble()]) + } else if (y != null) { + returnBuffer.setTo(self.noise[x.toDouble(), y.toDouble()]) + } else { + returnBuffer.setTo(self.noise[x.toDouble()]) + } + }) + .build() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaRandomGenerator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaRandomGenerator.kt new file mode 100644 index 00000000..b18de125 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaRandomGenerator.kt @@ -0,0 +1,104 @@ +package ru.dbotthepony.kstarbound.lua.userdata + +import org.classdump.luna.Table +import org.classdump.luna.Userdata +import org.classdump.luna.impl.ImmutableTable +import ru.dbotthepony.kstarbound.lua.luaFunction +import ru.dbotthepony.kstarbound.util.random.random +import java.util.random.RandomGenerator + +class LuaRandomGenerator(var random: RandomGenerator) : Userdata() { + private var metatable: Table? = Companion.metatable + + override fun getMetatable(): Table? { + return metatable + } + + override fun setMetatable(mt: Table?): Table? { + val old = metatable + metatable = mt + return old + } + + override fun getUserValue(): RandomGenerator { + return random + } + + override fun setUserValue(value: RandomGenerator?): RandomGenerator { + throw UnsupportedOperationException() + } + + fun randf(origin: Double?, bound: Double?): Double { + if (origin != null && bound != null) { + if (origin == bound) { + random.nextDouble() // to keep old behavior + return origin + } else { + return random.nextDouble() + } + } else { + return random.nextDouble() + } + } + + fun randomInt(arg1: Long, arg2: Long?): Long { + if (arg2 == null) { + if (arg1 == 0L) { + random.nextLong() // to keep old behavior + return 0L + } else if (arg1 < 0L) { + return random.nextLong(arg1, 0L) + } else { + return random.nextLong(0L, arg1) + } + } else { + if (arg2 == arg1) { + random.nextLong() // to keep old behavior + return arg1 + } else { + return random.nextLong(arg1, arg2) + } + } + } + + companion object { + private val metatable = ImmutableTable.Builder() + .add("init", luaFunction { self: LuaRandomGenerator, seed: Long? -> + self.random = random(seed ?: System.nanoTime()) + }) + .add("addEntropy", luaFunction { self: LuaRandomGenerator -> + throw UnsupportedOperationException("Adding entropy is not supported on new engine. If you have legitimate usecase for this, please let us know at issue tracker or discord") + }) + // TODO: Lua, by definition, has no unsigned numbers, + // and before 5.3, there were only doubles, longs (integers) were added + // in 5.3 + .add("randu32", luaFunction { self: LuaRandomGenerator -> + returnBuffer.setTo(self.random.nextLong(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong())) + }) + .add("randi32", luaFunction { self: LuaRandomGenerator -> + returnBuffer.setTo(self.random.nextLong(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong())) + }) + .add("randu64", luaFunction { self: LuaRandomGenerator -> + returnBuffer.setTo(self.random.nextLong()) + }) + .add("randi64", luaFunction { self: LuaRandomGenerator -> + returnBuffer.setTo(self.random.nextLong()) + }) + .add("randf", luaFunction { self: LuaRandomGenerator, origin: Double?, bound: Double? -> + returnBuffer.setTo(self.randf(origin, bound)) + }) + .add("randd", luaFunction { self: LuaRandomGenerator, origin: Double?, bound: Double? -> + returnBuffer.setTo(self.randf(origin, bound)) + }) + .add("randb", luaFunction { self: LuaRandomGenerator -> + returnBuffer.setTo(self.random.nextBoolean()) + }) + .add("randInt", luaFunction { self: LuaRandomGenerator, arg1: Long, arg2: Long? -> + returnBuffer.setTo(self.randomInt(arg1, arg2)) + }) + .add("randUInt", luaFunction { self: LuaRandomGenerator, arg1: Long, arg2: Long? -> + returnBuffer.setTo(self.randomInt(arg1, arg2)) + }) + .build() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index 80ec7a64..70847ff1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -511,9 +511,9 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn scope.launch { shipFlightEventLoop() } scope.launch { warpEventLoop() } - //if (server.channels.connections.size > 1) { - // enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!)) - //} + if (server.channels.connections.size > 1) { + enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!)) + } } }.exceptionally { LOGGER.error("Error while initializing shipworld for $this", it) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt index 9e5f8aec..ac4c2f0d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -1,7 +1,6 @@ package ru.dbotthepony.kstarbound.server.world import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet -import it.unimi.dsi.fastutil.objects.ObjectArrayList import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.future.await import kotlinx.coroutines.launch @@ -536,7 +535,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk` or as [defaultValue], + * depending on [replaceWithDefault]) + */ + fun resolveOrSkip(values: (String) -> String?, replaceWithDefault: Boolean = false, defaultValue: String = ""): String { + if (namesSet.isEmpty()) { + return raw + } else if (pieces.size == 1) { + val resolve = pieces[0].resolve(values, this::getParam) + ?: if (replaceWithDefault) { + return defaultValue + } else { + return raw + } + + return resolve + } + + val buffer = ArrayList(pieces.size) + + for (piece in pieces) { + var resolve = piece.resolve(values, this::getParam) + + if (resolve == null) { + if (replaceWithDefault) { + resolve = defaultValue + } else { + resolve = "<${piece.name!!}>" + } + } + + buffer.add(resolve) + } + + var count = 0 + for (piece in buffer) count += piece.length + val builder = StringBuilder(count) + for (piece in buffer) builder.append(piece) + return String(builder) + } + fun resolve(values: (String) -> String?): String? { if (namesSet.isEmpty()) { return raw @@ -146,7 +188,7 @@ class SBPattern private constructor( check(!(name != null && contents != null)) { "Both name and contents are not null" } } - fun resolve(map0: (String) -> String?, map1: (String) -> String?): String? { + inline fun resolve(map0: (String) -> String?, map1: (String) -> String?): String? { return contents ?: map0.invoke(name!!) ?: map1.invoke(name!!) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt index 0b22952f..f5d856d0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt @@ -55,7 +55,7 @@ private fun toBytes(accept: ByteConsumer, value: Float) { toBytes(accept, value.toBits()) } -fun staticRandom32(vararg values: Any): Int { +fun staticRandom32(vararg values: Any?): Int { val digest = XXHash32(2938728349.toInt()) for (value in values) { @@ -68,6 +68,7 @@ fun staticRandom32(vararg values: Any): Int { is Long -> toBytes(digest::update, value) is Double -> toBytes(digest::update, value) is Float -> toBytes(digest::update, value) + null -> {} // do nothing? else -> throw IllegalArgumentException("Can't hash value of type ${value::class.qualifiedName}") } } @@ -75,20 +76,25 @@ fun staticRandom32(vararg values: Any): Int { return digest.digestAsInt() } -fun staticRandomFloat(vararg values: Any): Float { +fun staticRandomFloat(vararg values: Any?): Float { return staticRandom32(*values).ushr(8) * 5.9604645E-8f } -fun staticRandomDouble(vararg values: Any): Double { +fun staticRandomDouble(vararg values: Any?): Double { return staticRandom64(*values).ushr(11) * 1.1102230246251565E-16 } -fun staticRandomInt(origin: Int, bound: Int, vararg values: Any): Int { +fun staticRandomInt(origin: Int, bound: Int, vararg values: Any?): Int { val rand = staticRandomDouble(*values) return origin + ((bound - origin) * rand).toInt() } -fun staticRandom64(vararg values: Any): Long { +fun staticRandomLong(origin: Long, bound: Long, vararg values: Any?): Long { + val rand = staticRandomDouble(*values) + return origin + ((bound - origin) * rand).toLong() +} + +fun staticRandom64(vararg values: Any?): Long { val digest = XXHash64(1997293021376312589L) for (value in values) { @@ -101,6 +107,7 @@ fun staticRandom64(vararg values: Any): Long { is Long -> toBytes(digest::update, value) is Double -> toBytes(digest::update, value) is Float -> toBytes(digest::update, value) + null -> {} // do nothing? else -> throw IllegalArgumentException("Can't hash value of type ${value::class.qualifiedName}") } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt index 492155ea..a0ec124a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt @@ -4,12 +4,12 @@ import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.json.builder.IStringSerializable -enum class Direction(val normal: Vector2d, override val jsonName: String) : IStringSerializable { - LEFT(Vector2d.NEGATIVE_X, "left") { +enum class Direction(val normal: Vector2d, override val jsonName: String, val luaValue: Long) : IStringSerializable { + LEFT(Vector2d.NEGATIVE_X, "left", -1L) { override val opposite: Direction get() = RIGHT }, - RIGHT(Vector2d.POSITIVE_X, "right") { + RIGHT(Vector2d.POSITIVE_X, "right", 1L) { override val opposite: Direction get() = LEFT }; diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileModification.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileModification.kt index 3793cd3a..3450c0d6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileModification.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileModification.kt @@ -110,7 +110,7 @@ sealed class TileModification { return false } - if (!allowEntityOverlap && world.entityIndex.any(rect, Predicate { it is DynamicEntity && it.movement.computeCollisionAABB().intersect(rect) })) { + if (!allowEntityOverlap && world.entityIndex.any(rect, Predicate { it.collisionArea.intersect(rect) })) { return false } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index eeb0c033..7a8ef7ed 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -5,7 +5,6 @@ import com.google.gson.JsonObject import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap -import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectArrayList import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.arrays.Object2DArray @@ -247,6 +246,8 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk = Predicate { true }): List { return entityIndex.query( AABBi(pos, pos + Vector2i.POSITIVE_XY).toDoubleAABB(), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt index c1467d78..f8f39a3d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -10,10 +10,12 @@ import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.world.ClientWorld +import ru.dbotthepony.kstarbound.defs.EntityDamageTeam import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.InteractAction import ru.dbotthepony.kstarbound.defs.InteractRequest import ru.dbotthepony.kstarbound.defs.JsonDriven +import ru.dbotthepony.kstarbound.defs.`object`.DamageTeam import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec import ru.dbotthepony.kstarbound.network.syncher.MasterElement import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup @@ -83,9 +85,11 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable frame) { + properties[key] = props.get(frame).deepCopy() + } + } + + // Each part can only have one state type x state match, so we are done. + break + } + } } private class RotationGroup { @@ -342,6 +422,15 @@ class Animator() { xShear = atan2(value[1, 0].toDouble(), value[0, 0].toDouble()) yShear = atan2(value[0, 1].toDouble(), value[1, 1].toDouble()) } + + fun reset() { + xTranslation = 0.0 + yTranslation = 0.0 + xScale = 1.0 + yScale = 1.0 + xShear = 0.0 + yShear = 0.0 + } } private class ParticleEmitter { @@ -376,7 +465,7 @@ class Animator() { private val elements = ArrayList() var processingDirectives by networkedString().also { elements.add(it) } - var zoom by networkedFloat().also { elements.add(it) } + var zoom by networkedFloat(1.0).also { elements.add(it) } var isFlipped by networkedBoolean().also { elements.add(it) } var flippedRelativeCenterLine by networkedFloat().also { elements.add(it) } var animationRate by networkedFloat(1.0).also { elements.add(it); it.interpolator = Interpolator.Linear } @@ -518,13 +607,7 @@ class Animator() { } fun setPartTag(partName: String, tagKey: String, tagValue: String) { - var tags = partTags[partName] - - if (tags == null) { - tags = NetworkedMap(InternedStringCodec, InternedStringCodec) - partTags[partName] = tags - } - + val tags = partTags[partName] ?: throw IllegalArgumentException("Unknown part $partName!") tags[tagKey] = tagValue } @@ -574,15 +657,280 @@ class Animator() { } fun setActiveState(type: String, state: String, alwaysStart: Boolean = false): Boolean { - val getType = stateTypes[type] ?: return false - val getState = getType.states[state] ?: return false + return stateTypes[type]?.set(state, alwaysStart) ?: return false + } - if (getType.activeState != getState || alwaysStart) { - getType.timer = 0.0 - return true + fun animationState(type: String): String { + val get = stateTypes[type] ?: throw NoSuchElementException("No such animation part $type") + return get.activeState?.name ?: "" // original engine parity............... + // it will return empty string if state type has no active state ( + } + + fun stateProperty(type: String, path: String): JsonElement { + val get = stateTypes[type] ?: throw NoSuchElementException("No such animation part $type") + return get.activeState?.properties?.get(path) ?: JsonNull.INSTANCE // очень и очень грустно + } + + fun stateProperty(type: String, path: JsonPath): JsonElement { + val get = stateTypes[type] ?: throw NoSuchElementException("No such animation part $type") + val props = get.activeState?.properties ?: return JsonNull.INSTANCE + return path.find(props) ?: JsonNull.INSTANCE // очень и очень грустно + } + + fun rotateGroup(group: String, targetAngle: Double, immediate: Boolean = false) { + val get = rotationGroups[group] ?: throw NoSuchElementException("No such rotation group $group") + get.targetAngle.accept(targetAngle) + + if (immediate) { + get.currentAngle = targetAngle + get.immediateEvent.trigger() } + } - return false + fun currentRotationAngle(group: String): Double { + return rotationGroups[group]?.currentAngle ?: throw NoSuchElementException("No such rotation group $group") + } + + fun targetRotationAngle(group: String): Double { + return rotationGroups[group]?.targetAngle?.get() ?: throw NoSuchElementException("No such rotation group $group") + } + + fun hasRotationGroup(group: String): Boolean { + return group in rotationGroups + } + + fun rotationGroups(): Collection { + return Collections.unmodifiableCollection(rotationGroups.keys) + } + + fun transformationGroups(): Collection { + return Collections.unmodifiableCollection(transformationGroups.keys) + } + + fun hasTransformationGroup(group: String): Boolean { + return group in transformationGroups + } + + fun translateTransformGroup(group: String, translation: Vector2f) { + val get = transformationGroups[group] ?: throw NoSuchElementException("No such transformation group $group") + get.setAffineTransform(Matrix3f.rowMajor(r02 = translation.x, r12 = translation.y).mul(get.affineTransform())) + } + + fun rotateTransformGroup(group: String, rotation: Float, center: Vector2f = Vector2f.ZERO) { + val get = transformationGroups[group] ?: throw NoSuchElementException("No such transformation group $group") + val sin = sin(rotation) + val cos = cos(rotation) + + val matrix = Matrix3f.rowMajor( + cos, -sin, center.x - cos * center.x + sin * center.y, + sin, cos, center.y - sin * center.y - cos * center.x + ) + + get.setAffineTransform(matrix.mul(get.affineTransform())) + } + + fun scaleTransformationGroup(group: String, scale: Vector2f, center: Vector2f = Vector2f.ZERO) { + val get = transformationGroups[group] ?: throw NoSuchElementException("No such transformation group $group") + + val matrix = Matrix3f.rowMajor( + scale.x, 0f, center.x - center.x * scale.x, + 0f, scale.y, center.y - center.y * scale.y + ) + + get.setAffineTransform(matrix.mul(get.affineTransform())) + } + + fun transformTransformationGroup( + group: String, + r00: Float, + r01: Float, + r02: Float, + r10: Float, + r11: Float, + r12: Float, + ) { + val get = transformationGroups[group] ?: throw NoSuchElementException("No such transformation group $group") + val matrix = Matrix3f.rowMajor(r00, r01, r02, r10, r11, r12) + get.setAffineTransform(matrix.mul(get.affineTransform())) + } + + fun resetTransformationGroup(group: String) { + val get = transformationGroups[group] ?: throw NoSuchElementException("No such transformation group $group") + get.reset() + } + + fun hasParticleEmitter(emitter: String): Boolean { + return emitter in particleEmitters + } + + fun particleEmitters(): Collection { + return Collections.unmodifiableCollection(particleEmitters.keys) + } + + fun setParticleEmitterActive(emitter: String, state: Boolean = true) { + val get = particleEmitters[emitter] ?: throw NoSuchElementException("No such particle emitter $emitter") + get.active = state + } + + fun setParticleEmitterEmissionRate(emitter: String, rate: Double) { + val get = particleEmitters[emitter] ?: throw NoSuchElementException("No such particle emitter $emitter") + get.emissionRate = rate + } + + fun setParticleEmitterBurstCount(emitter: String, count: Int) { + val get = particleEmitters[emitter] ?: throw NoSuchElementException("No such particle emitter $emitter") + require(count >= 0) { "Negative burst count: $count" } + get.burstCount = count + } + + fun setParticleEmitterOffsetRegion(emitter: String, region: AABB) { + val get = particleEmitters[emitter] ?: throw NoSuchElementException("No such particle emitter $emitter") + get.offsetRegion = KOptional(region) + } + + fun setParticleEmitterOffsetRegion(emitter: String) { + val get = particleEmitters[emitter] ?: throw NoSuchElementException("No such particle emitter $emitter") + get.offsetRegion = KOptional() + } + + fun burstParticleEmitter(emitter: String) { + val get = particleEmitters[emitter] ?: throw NoSuchElementException("No such particle emitter $emitter") + get.burstEvent.trigger() + } + + fun lights(): Collection { + return Collections.unmodifiableCollection(lights.keys) + } + + fun hasLight(light: String): Boolean { + return light in lights + } + + fun setLightActive(light: String, state: Boolean = true) { + val get = lights[light] ?: throw NoSuchElementException("No such light source $light") + get.active = state + } + + fun setLightPosition(light: String, position: Vector2d) { + val get = lights[light] ?: throw NoSuchElementException("No such light source $light") + get.xPosition = position.x + get.yPosition = position.y + } + + fun setLightPosition(light: String, x: Double, y: Double) { + val get = lights[light] ?: throw NoSuchElementException("No such light source $light") + get.xPosition = x + get.yPosition = y + } + + fun setLightColor(light: String, color: RGBAColor) { + val get = lights[light] ?: throw NoSuchElementException("No such light source $light") + get.color = color + } + + fun setLightPointAngle(light: String, angle: Double) { + val get = lights[light] ?: throw NoSuchElementException("No such light source $light") + get.pointAngle = angle + } + + fun sounds(): Collection { + return Collections.unmodifiableCollection(sounds.keys) + } + + fun hasSound(sound: String): Boolean { + return sound in sounds + } + + fun setSoundPool(sound: String, pool: Collection) { + val get = sounds[sound] ?: throw NoSuchElementException("No such sound source $sound") + + get.soundPool.clear() + get.soundPool.addAll(pool) + } + + fun setSoundPosition(sound: String, position: Vector2d) { + val get = sounds[sound] ?: throw NoSuchElementException("No such sound source $sound") + + get.xPosition = position.x + get.yPosition = position.y + } + + fun setSoundPosition(sound: String, x: Double, y: Double) { + val get = sounds[sound] ?: throw NoSuchElementException("No such sound source $sound") + + get.xPosition = x + get.yPosition = y + } + + fun playSound(sound: String, loops: Int = 0) { + val get = sounds[sound] ?: throw NoSuchElementException("No such sound source $sound") + require(loops >= 0) { "Negative amount of loops: $loops" } + get.loops = loops + get.signals.push(SoundSignal.PLAY) + } + + fun setSoundVolume(sound: String, volume: Double, rampTime: Double = 0.0) { + val get = sounds[sound] ?: throw NoSuchElementException("No such sound source $sound") + get.volumeTarget = volume + get.volumeRampTime = rampTime + } + + fun setSoundPitch(sound: String, pitch: Double, rampTime: Double = 0.0) { + val get = sounds[sound] ?: throw NoSuchElementException("No such sound source $sound") + get.pitchMultiplierTarget = pitch + get.pitchMultiplierRampTime = rampTime + } + + fun stopAllSounds(sound: String, rampTime: Double = 0.0) { + val get = sounds[sound] ?: throw NoSuchElementException("No such sound source $sound") + get.volumeRampTime = rampTime + get.signals.push(SoundSignal.STOP_ALL) + } + + fun effects(): Collection { + return Collections.unmodifiableCollection(effects.keys) + } + + fun hasEffect(effect: String): Boolean { + return effect in effects + } + + fun setEffectActive(effect: String, state: Boolean = true) { + val get = effects[effect] ?: throw NoSuchElementException("No such effect source $effect") + get.enabled.accept(state) + } + + fun parts(): Collection { + return Collections.unmodifiableCollection(parts.keys) + } + + fun partPoint(part: String, property: String): Vector2d? { + val get = parts[part] ?: throw NoSuchElementException("No such animation part $part") + get.freshen() + val lookup = get.properties[property] ?: return null + return vectors.fromJsonTree(lookup) + } + + fun partPoly(part: String, property: String): Poly? { + val get = parts[part] ?: throw NoSuchElementException("No such animation part $part") + get.freshen() + val lookup = get.properties[property] ?: return null + return polies.fromJsonTree(lookup) + } + + fun partProperty(part: String, property: String): JsonElement { + val get = parts[part] ?: throw NoSuchElementException("No such animation part $part") + get.freshen() + return get.properties[property] ?: JsonNull.INSTANCE + } + + fun partTransformation(part: String): Matrix3d { + val get = parts[part] ?: throw NoSuchElementException("No such animation part $part") + val result = Matrix3d.rowMajor() + + val active = get.activeState ?: return result + // TODO + return result } // TODO: Dynamic target @@ -618,6 +966,8 @@ class Animator() { } } + parts.values.forEach { it.activePartDirty = true } + for (rotationGroup in rotationGroups.values) { rotationGroup.tick(delta) } @@ -631,6 +981,14 @@ class Animator() { } } + fun finishAnimations() { + for (state in stateTypes.values) { + state.finishAnimations() + } + + parts.values.forEach { it.activePartDirty = true } + } + companion object { // lame fun load(path: String): Animator { @@ -649,5 +1007,7 @@ class Animator() { private val LOGGER = LogManager.getLogger() private val missing = Collections.synchronizedSet(ObjectOpenHashSet()) + private val vectors by lazy { Starbound.gson.getAdapter(Vector2d::class.java) } + private val polies by lazy { Starbound.gson.getAdapter(Poly::class.java) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt index 667c2f88..f306b0a2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt @@ -22,6 +22,9 @@ abstract class DynamicEntity(path: String) : AbstractEntity(path) { abstract val movement: MovementController + override val collisionArea: AABB + get() = movement.computeCollisionAABB() + override fun onNetworkUpdate() { super.onNetworkUpdate() movement.updateFixtures() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt index a1c00907..59eb6ba6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt @@ -82,7 +82,11 @@ class PlayerEntity() : HumanoidActorEntity("/") { private var xAimPosition by networkGroup.upstream.add(networkedFixedPoint(0.003125)) private var yAimPosition by networkGroup.upstream.add(networkedFixedPoint(0.003125).also { it.interpolator = Interpolator.Linear }) var humanoidData by networkGroup.upstream.add(networkedData(HumanoidData(), HumanoidData.CODEC, HumanoidData.LEGACY_CODEC)) - var teamState by networkGroup.upstream.add(networkedData(EntityDamageTeam(), EntityDamageTeam.CODEC, EntityDamageTeam.LEGACY_CODEC)) + + init { + networkGroup.upstream.add(team) + } + val landed = networkGroup.upstream.add(networkedEventCounter()) var chatMessage by networkGroup.upstream.add(networkedString()) val newChatMessage = networkGroup.upstream.add(networkedEventCounter()) @@ -106,7 +110,7 @@ class PlayerEntity() : HumanoidActorEntity("/") { override val aimPosition: Vector2d get() = Vector2d(xAimPosition, yAimPosition) - override val isApplicableForUnloading: Boolean + override val isPersistent: Boolean get() = false var uuid: UUID by Delegates.notNull() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt index c4d6e97f..4f54926f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt @@ -83,11 +83,11 @@ abstract class TileEntity(path: String) : AbstractEntity(path) { private val currentMaterialSpaces = HashSet>>() private val currentRoots = HashSet() - protected open fun markSpacesDirty() { + open fun markSpacesDirty() { needToUpdateSpaces = true } - protected open fun markRootsDirty() { + open fun markRootsDirty() { needToUpdateRoots = true } @@ -147,6 +147,8 @@ abstract class TileEntity(path: String) : AbstractEntity(path) { } protected fun updateMaterialSpaces(desired: Collection>>): Boolean { + check(world.isServer) { "Invalid realm" } + val toRemove = ArrayList>>() val toPlace = ArrayList>>() @@ -219,7 +221,7 @@ abstract class TileEntity(path: String) : AbstractEntity(path) { override fun tick(delta: Double) { super.tick(delta) - if (needToUpdateSpaces) { + if (world.isServer && needToUpdateSpaces) { updateMaterialSpacesNow() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt index f13b7e69..667215bb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt @@ -10,6 +10,8 @@ import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import com.google.gson.TypeAdapter import com.google.gson.reflect.TypeToken +import org.apache.logging.log4j.LogManager +import org.classdump.luna.Table import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Registries @@ -59,6 +61,17 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedJsonElement import ru.dbotthepony.kstarbound.network.syncher.networkedPointer import ru.dbotthepony.kstarbound.network.syncher.networkedString import ru.dbotthepony.kstarbound.json.writeJsonElement +import ru.dbotthepony.kstarbound.lua.LuaEnvironment +import ru.dbotthepony.kstarbound.lua.LuaMessageHandler +import ru.dbotthepony.kstarbound.lua.LuaUpdateComponent +import ru.dbotthepony.kstarbound.lua.bindings.provideAnimatorBindings +import ru.dbotthepony.kstarbound.lua.bindings.provideEntityBindings +import ru.dbotthepony.kstarbound.lua.bindings.provideWorldBindings +import ru.dbotthepony.kstarbound.lua.bindings.provideWorldObjectBindings +import ru.dbotthepony.kstarbound.lua.from +import ru.dbotthepony.kstarbound.lua.get +import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.lua.toJson import ru.dbotthepony.kstarbound.util.asStringOrNull import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.LightCalculator @@ -78,6 +91,8 @@ open class WorldObject(val config: Registry.Entry) : TileEntit isInteractive = data.get("interactive", false) tilePosition = data.get("tilePosition", vectors) + lua.globals["storage"] = lua.from(data.get("scriptStorage") { JsonObject() }) + uniqueID.accept(KOptional.ofNullable(data["uniqueId"]?.asStringOrNull)) loadParameters(data.get("parameters") { JsonObject() }) @@ -97,6 +112,12 @@ open class WorldObject(val config: Registry.Entry) : TileEntit into["orientationIndex"] = orientationIndex into["interactive"] = isInteractive + val scriptStorage = lua.globals["storage"] + + if (scriptStorage != null && scriptStorage is Table) { + into["scriptStorage"] = scriptStorage.toJson() + } + uniqueID.get().ifPresent { into["uniqueId"] = it } @@ -243,6 +264,14 @@ open class WorldObject(val config: Registry.Entry) : TileEntit networkGroup.upstream.add(animator.networkGroup) } + val lua = LuaEnvironment() + val luaUpdate = LuaUpdateComponent(lua) + val luaMessageHandler = LuaMessageHandler(lua) + + init { + lua.globals["storage"] = lua.newTable() + } + val unbreakable by LazyData { lookupProperty(JsonPath("unbreakable")) { JsonPrimitive(false) }.asBoolean } @@ -280,10 +309,20 @@ open class WorldObject(val config: Registry.Entry) : TileEntit // var color: TileColor by Property(JsonPath("color"), TileColor.DEFAULT) - var animationParts: ImmutableMap by Property(JsonPath("animationParts")) var imagePosition: Vector2i by Property(JsonPath("imagePosition"), Vector2i.ZERO) var animationPosition: Vector2i by Property(JsonPath("animationPosition"), Vector2i.ZERO) + // ???????? + val volumeBoundingBox: AABB get() { + val orientation = orientation + + if (orientation == null) { + return AABB(position, position + Vector2d.POSITIVE_XY) + } else { + return AABB(orientation.boundingBox.mins.toDoubleVector(), orientation.boundingBox.maxs.toDoubleVector() + Vector2d.POSITIVE_XY) + } + } + private val drawablesCache = LazyData { orientation?.drawables?.map { it.with(::getRenderParam) } ?: listOf() } @@ -336,6 +375,11 @@ open class WorldObject(val config: Registry.Entry) : TileEntit } } + override fun markSpacesDirty() { + super.markSpacesDirty() + materialSpaces0.invalidate() + } + override fun onJoinWorld(world: World<*, *>) { super.onJoinWorld(world) @@ -352,13 +396,32 @@ open class WorldObject(val config: Registry.Entry) : TileEntit networkedMaterialSpaces.addAll(orientation.materialSpaces) } - setImageKey("color", lookupProperty(JsonPath("color")) { JsonPrimitive("default") }.asString) + if (isRemote) { - for ((k, v) in lookupProperty(JsonPath("animationParts")) { JsonObject() }.asJsonObject.entrySet()) { - animator.setPartTag(k, "partImage", v.asString) + } else { + setImageKey("color", lookupProperty(JsonPath("color")) { JsonPrimitive("default") }.asString) + + for ((k, v) in lookupProperty(JsonPath("animationParts")) { JsonObject() }.asJsonObject.entrySet()) { + animator.setPartTag(k, "partImage", v.asString) + } + + updateMaterialSpacesNow() + + provideWorldBindings(world, lua) + provideWorldObjectBindings(this, lua) + provideEntityBindings(this, lua) + provideAnimatorBindings(animator, lua) + lua.attach(config.value.scripts) + lua.init() } - updateMaterialSpacesNow() + // as original code puts it: + // Don't animate the initial state when first spawned IF you're dumb, which by default + // you would be, and don't know how to use transition and static states properly. Someday + // I'll be brave and delete shit garbage entirely and we'll see what breaks. + if (lookupProperty(JsonPath("forceFinishAnimationsInInit")) { JsonPrimitive(true) }.asBoolean) { + animator.finishAnimations() + } } fun getRenderParam(key: String): String { @@ -400,6 +463,14 @@ open class WorldObject(val config: Registry.Entry) : TileEntit drawablesCache.invalidate() } + fun callBreak(smash: Boolean = false) { + + } + + fun addChatMessage(message: String, config: JsonElement, portrait: String? = null) { + + } + override fun tick(delta: Double) { super.tick(delta) @@ -420,6 +491,12 @@ open class WorldObject(val config: Registry.Entry) : TileEntit setImageKey("frame", frame.toString()) } } + + try { + luaUpdate.update() + } catch (err: Throwable) { + LogManager.getLogger().error("Error running update", err) + } } if (world.isServer && !unbreakable) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt index a85041d3..a3621f4a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt @@ -24,6 +24,7 @@ import ru.dbotthepony.kommons.io.readVector2f import ru.dbotthepony.kommons.io.writeCollection import ru.dbotthepony.kommons.io.writeStruct2d import ru.dbotthepony.kommons.io.writeStruct2f +import ru.dbotthepony.kommons.matrix.Matrix3d import ru.dbotthepony.kstarbound.json.listAdapter import ru.dbotthepony.kstarbound.math.Line2d import ru.dbotthepony.kstarbound.network.syncher.legacyCodec @@ -213,6 +214,10 @@ class Poly private constructor(val edges: ImmutableList, val vertices: I return Vector2d(min, max) } + fun transform(transformation: Matrix3d): Poly { + return Poly(vertices.map { it * transformation }) + } + fun windingNumber(point: IStruct2d): Int { val (x, y) = point