Some initial Lua bindings (object, animator, root, sb)

This commit is contained in:
DBotThePony 2024-04-14 01:45:15 +07:00
parent ba05b27deb
commit 8ee60c0f4d
Signed by: DBot
GPG Key ID: DCC23B5715498507
39 changed files with 2376 additions and 386 deletions

View File

@ -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<string>` (returns valid names for `rotateGroup`, `currentRotationAngle` and `targetRotationAngle`)
* Added `animator.transformationGroups(): List<string>`
* Added `animator.particleEmitters(): List<string>`
* Added `animator.hasParticleEmitter(emitter: string): boolean`
* Added `animator.lights(): List<string>`
* Added `animator.hasLight(light: string): boolean`
* Added `animator.sounds(): List<string>`
* Added `animator.effects(): List<string>`
* Added `animator.hasEffect(effect: string): boolean`
* Added `animator.parts(): List<string>`

9
README.md Normal file
View File

@ -0,0 +1,9 @@
### Starbound engine recreation project
Make sure to specify next settings as startup options to JVM:
```
-Dfile.encoding=UTF8
```

View File

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

View File

@ -1,2 +1,2 @@
rootProject.name = "KStarBound"
include("luna")

View File

@ -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<String> = Interner.newWeakInterner()
// val strings: Interner<String> = Interner { it }
@JvmField
@JvmField
val STRINGS: Interner<String> = 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<String, ChunkFactory>()
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)

View File

@ -66,7 +66,7 @@ data class AnimatedPartsDefinition(
@JsonFactory
data class State(
val properties: JsonObject = JsonObject(),
val frameProperties: JsonObject = JsonObject(),
val frameProperties: ImmutableMap<String, JsonArray> = ImmutableMap.of(),
)
}
}

View File

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

View File

@ -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<ItemDescri
stateMachine.add {
val iname = name.get()
val icount = count.get().orElse(1L)
val iparameters = parameters.get().map { if (it is Table) it.toJsonObject() else throw LuaRuntimeException("Invalid item descriptor parameters ($it)") }.orElse { JsonObject() }
val iparameters = parameters.get().map { if (it is Table) it.toJson(true) else throw LuaRuntimeException("Invalid item descriptor parameters ($it)") }.orElse { JsonObject() }
if (iname !is ByteString) throw LuaRuntimeException("Invalid item descriptor name (${iname})")
if (icount !is Number) throw LuaRuntimeException("Invalid item descriptor count (${icount})")
result = KOptional(ItemDescriptor(iname.decode(), icount.toLong(), iparameters))
result = KOptional(ItemDescriptor(iname.decode(), icount.toLong(), iparameters as JsonObject))
}
return Supplier { result.value }
}
fun ExecutionContext.ItemDescriptor(data: Table): ItemDescriptor {
val name = indexNoYield(data, 1L) ?: indexNoYield(data, "name") ?: indexNoYield(data, "item")
val count = indexNoYield(data, 2L) ?: indexNoYield(data, "count") ?: 1L
val parameters = indexNoYield(data, 3L) ?: indexNoYield(data, "parameters") ?: indexNoYield(data, "data")
if (name !is ByteString) throw LuaRuntimeException("Invalid item descriptor name (${name})")
if (count !is Number) throw LuaRuntimeException("Invalid item descriptor count (${count})")
if (parameters == null) {
return ItemDescriptor(name.decode(), count.toLong())
} else if (parameters is Table) {
return ItemDescriptor(name.decode(), count.toLong(), parameters.toJson(true) as JsonObject)
} else {
throw LuaRuntimeException("Invalid item descriptor parameters ($parameters)")
}
}
fun ItemDescriptor(stream: DataInputStream): ItemDescriptor {
val name = stream.readInternedString()
val count = stream.readVarLong()

View File

@ -38,11 +38,11 @@ enum class SkyType {
}
}
enum class FlyingType {
NONE,
DISEMBARKING,
WARP,
ARRIVING;
enum class FlyingType(override val jsonName: String) : IStringSerializable {
NONE("none"),
DISEMBARKING("disembarking"),
WARP("warp"),
ARRIVING("arriving");
companion object {
val CODEC = StreamCodec.Enum(FlyingType::class.java)

View File

@ -0,0 +1,375 @@
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 org.classdump.luna.ByteString
import org.classdump.luna.Conversions
import org.classdump.luna.Table
import org.classdump.luna.TableFactory
import org.classdump.luna.impl.NonsuspendableFunctionException
import org.classdump.luna.runtime.AbstractFunction3
import org.classdump.luna.runtime.ExecutionContext
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.util.IStruct3i
import ru.dbotthepony.kommons.util.IStruct4i
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.world.physics.Poly
fun ExecutionContext.toVector2i(table: Any): Vector2i {
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 Vector2i(x.toInt(), y.toInt())
}
fun ExecutionContext.toVector2d(table: Any): Vector2d {
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 Vector2d(x.toDouble(), y.toDouble())
}
fun ExecutionContext.toPoly(table: Table): Poly {
val vertices = ArrayList<Vector2d>()
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<JsonElement>()
val hashValues = HashMap<String, JsonElement>()
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<Table, Any, Any?>() {
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<JsonElement?>): 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<Any>): 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)
}
}

View File

@ -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<Map.Entry<Any, Any>> {
}
}
fun Table.toJson(): JsonElement {
val arrayValues = Long2ObjectAVLTreeMap<Any>()
val hashValues = HashMap<Any, Any>()
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<JsonElement?>): 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<Any>): 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<Any?, Any?, Any?, Any?, Any?> {
return object : LuaFunction<Any?, Any?, Any?, Any?, Any?>() {
@ -286,42 +136,19 @@ fun luaStub(message: String = "not yet implemented"): LuaFunction<Any?, Any?, An
}
}
fun luaFunction0String(name: String, callable: (String) -> Any?): LuaFunction<Any?, *, *, *, *> {
return object : AbstractFunction1<Any?>() {
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<Any, Any, Any, Any, Any> {
fun luaFunctionN(name: String, callable: ExecutionContext.(ArgumentIterator) -> Unit): LuaFunction<Any, Any, Any, Any, Any> {
return object : AbstractFunctionAnyArg() {
override fun resume(context: ExecutionContext, suspendedState: Any) {
throw NonsuspendableFunctionException(this::class.java)
}
override fun invoke(context: ExecutionContext, args: Array<out Any>) {
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<Any, Any, Any, Any, Any> {
fun luaFunctionNS(name: String, callable: ExecutionContext.(ArgumentIterator) -> StateMachine): LuaFunction<Any, Any, Any, Any, Any> {
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<out Any?>) -> Unit): LuaFunction<Any, Any, Any, Any, Any> {
return object : AbstractFunctionAnyArg() {
override fun resume(context: ExecutionContext, suspendedState: Any) {
throw NonsuspendableFunctionException(this::class.java)
}
override fun invoke(context: ExecutionContext, args: Array<out Any?>) {
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 <T> luaFunction(callable: (ExecutionContext, T) -> Unit): LuaFunction<T, *, *, *, *> {
fun <T> luaFunction(callable: ExecutionContext.(T) -> Unit): LuaFunction<T, *, *, *, *> {
return object : AbstractFunction1<T>() {
override fun resume(context: ExecutionContext, suspendedState: Any) {
throw NonsuspendableFunctionException(this::class.java)
@ -357,7 +196,7 @@ fun <T> luaFunction(callable: (ExecutionContext, T) -> Unit): LuaFunction<T, *,
}
}
fun <T, T2> luaFunction(callable: (ExecutionContext, T, T2) -> Unit): LuaFunction<T, T2, *, *, *> {
fun <T, T2> luaFunction(callable: ExecutionContext.(T, T2) -> Unit): LuaFunction<T, T2, *, *, *> {
return object : AbstractFunction2<T, T2>() {
override fun resume(context: ExecutionContext, suspendedState: Any) {
throw NonsuspendableFunctionException(this::class.java)
@ -369,7 +208,7 @@ fun <T, T2> luaFunction(callable: (ExecutionContext, T, T2) -> Unit): LuaFunctio
}
}
fun <T, T2, T3> luaFunction(callable: (ExecutionContext, T, T2, T3) -> Unit): LuaFunction<T, T2, T3, *, *> {
fun <T, T2, T3> luaFunction(callable: ExecutionContext.(T, T2, T3) -> Unit): LuaFunction<T, T2, T3, *, *> {
return object : AbstractFunction3<T, T2, T3>() {
override fun resume(context: ExecutionContext, suspendedState: Any) {
throw NonsuspendableFunctionException(this::class.java)
@ -381,7 +220,7 @@ fun <T, T2, T3> luaFunction(callable: (ExecutionContext, T, T2, T3) -> Unit): Lu
}
}
fun <T, T2, T3, T4> luaFunction(callable: (ExecutionContext, T, T2, T3, T4) -> Unit): LuaFunction<T, T2, T3, T4, *> {
fun <T, T2, T3, T4> luaFunction(callable: ExecutionContext.(T, T2, T3, T4) -> Unit): LuaFunction<T, T2, T3, T4, *> {
return object : AbstractFunction4<T, T2, T3, T4>() {
override fun resume(context: ExecutionContext, suspendedState: Any) {
throw NonsuspendableFunctionException(this::class.java)

View File

@ -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<ChunkFactory>()
private var initCalled = false
private val loadedScripts = ObjectArraySet<String>()
inner class LuaRequire : AbstractFunction1<ByteString>() {
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<AssetPath>) {
for (script in scripts) {
attach(Starbound.loadScript(script.fullPath))
}
}
fun run(chunk: ChunkFactory): Array<out Any?> {
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()
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<out Any?> ->
val group = arguments[0] as ByteString
val r00 = arguments[1] as Number
val r01 = arguments[2] as Number
val r02 = arguments[3] as Number
val r10 = arguments[4] as Number
val r11 = arguments[5] as Number
val r12 = arguments[6] as Number
self.transformTransformationGroup(group.decode(), r00.toFloat(),
r01.toFloat(),
r02.toFloat(),
r10.toFloat(),
r11.toFloat(),
r12.toFloat())
}
callbacks["resetTransformationGroup"] = luaFunction { group: ByteString ->
self.resetTransformationGroup(group.decode())
}
callbacks["particleEmitters"] = luaFunction {
val groups = self.particleEmitters()
val keys = newTable(groups.size, 0)
groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v }
returnBuffer.setTo(keys)
}
callbacks["hasParticleEmitter"] = luaFunction { group: ByteString ->
returnBuffer.setTo(self.hasParticleEmitter(group.decode()))
}
callbacks["setParticleEmitterActive"] = luaFunction { emitter: ByteString, state: Boolean ->
self.setParticleEmitterActive(emitter.decode(), state)
}
callbacks["setParticleEmitterEmissionRate"] = luaFunction { emitter: ByteString, rate: Number ->
self.setParticleEmitterEmissionRate(emitter.decode(), rate.toDouble())
}
callbacks["setParticleEmitterBurstCount"] = luaFunction { emitter: ByteString, count: Number ->
self.setParticleEmitterBurstCount(emitter.decode(), count.toInt())
}
callbacks["setParticleEmitterOffsetRegion"] = luaFunction { emitter: ByteString, region: Table ->
self.setParticleEmitterOffsetRegion(emitter.decode(), toAABB(region))
}
callbacks["burstParticleEmitter"] = luaFunction { emitter: ByteString ->
self.burstParticleEmitter(emitter.decode())
}
callbacks["lights"] = luaFunction {
val groups = self.lights()
val keys = newTable(groups.size, 0)
groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v }
returnBuffer.setTo(keys)
}
callbacks["hasLight"] = luaFunction { light: ByteString ->
returnBuffer.setTo(self.hasLight(light.decode()))
}
callbacks["setLightActive"] = luaFunction { light: ByteString, state: Boolean ->
self.setLightActive(light.decode(), state)
}
callbacks["setLightPosition"] = luaFunction { light: ByteString, position: Table ->
self.setLightPosition(light.decode(), toVector2d(position))
}
callbacks["setLightColor"] = luaFunction { light: ByteString, color: Table ->
self.setLightColor(light.decode(), toColor(color))
}
callbacks["setLightPointAngle"] = luaFunction { light: ByteString, angle: Number ->
self.setLightPointAngle(light.decode(), angle.toDouble())
}
callbacks["sounds"] = luaFunction {
val groups = self.sounds()
val keys = newTable(groups.size, 0)
groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v }
returnBuffer.setTo(keys)
}
callbacks["hasSound"] = luaFunction { sound: ByteString ->
returnBuffer.setTo(self.hasSound(sound.decode()))
}
callbacks["setSoundPool"] = luaFunction { sound: ByteString, sounds: Table ->
self.setSoundPool(sound.decode(), sounds.iterator().map { (it.value as ByteString).decode() }.toList())
}
callbacks["setSoundPosition"] = luaFunction { sound: ByteString, position: Table ->
self.setSoundPosition(sound.decode(), toVector2d(position))
}
callbacks["playSound"] = luaFunction { sound: ByteString, loops: Number? ->
self.playSound(sound.decode(), loops?.toInt() ?: 0)
}
callbacks["setSoundVolume"] = luaFunction { sound: ByteString, volume: Number, rampTime: Number? ->
self.setSoundVolume(sound.decode(), volume.toDouble(), rampTime?.toDouble() ?: 0.0)
}
callbacks["setSoundPitch"] = luaFunction { sound: ByteString, pitch: Number, rampTime: Number? ->
self.setSoundPitch(sound.decode(), pitch.toDouble(), rampTime?.toDouble() ?: 0.0)
}
callbacks["stopAllSounds"] = luaFunction { sound: ByteString, rampTime: Number? ->
self.stopAllSounds(sound.decode(), rampTime?.toDouble() ?: 0.0)
}
callbacks["effects"] = luaFunction {
val groups = self.effects()
val keys = newTable(groups.size, 0)
groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v }
returnBuffer.setTo(keys)
}
callbacks["hasEffect"] = luaFunction { effect: ByteString ->
returnBuffer.setTo(self.hasEffect(effect.decode()))
}
callbacks["setEffectActive"] = luaFunction { effect: ByteString, state: Boolean ->
self.setEffectActive(effect.decode(), state)
}
callbacks["parts"] = luaFunction {
val groups = self.parts()
val keys = newTable(groups.size, 0)
groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v }
returnBuffer.setTo(keys)
}
callbacks["partPoint"] = luaFunction { part: ByteString, property: ByteString ->
returnBuffer.setTo(self.partPoint(part.decode(), property.decode())?.let { from(it) })
}
callbacks["partPoly"] = luaFunction { part: ByteString, property: ByteString ->
returnBuffer.setTo(self.partPoly(part.decode(), property.decode())?.let { from(it) })
}
callbacks["partProperty"] = luaFunction { part: ByteString, property: ByteString ->
returnBuffer.setTo(from(self.partProperty(part.decode(), property.decode())))
}
callbacks["transformPoint"] = luaFunction { point: Table, part: ByteString ->
returnBuffer.setTo(from(toVector2d(point).times(self.partTransformation(part.decode()))))
}
callbacks["transformPoly"] = luaFunction { poly: Table, part: ByteString ->
returnBuffer.setTo(from(toPoly(poly).transform(self.partTransformation(part.decode()))))
}
}

View File

@ -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<ByteString>() {
override fun resume(context: ExecutionContext?, suspendedState: Any?) {

View File

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

View File

@ -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 <T : Any> lookup(registry: Registry<T>, key: Any?): Registry.Entry<T>? {
if (key is ByteString) {
@ -94,30 +102,30 @@ private fun nonEmptyRegion(context: ExecutionContext, name: ByteString) {
}
private fun registryDef(registry: Registry<*>): LuaFunction<ByteString, *, *, *, *> {
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<Any?, *, *, *, *> {
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<ByteString, *, *, *, *> {
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<Any>()
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<Any>()
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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<ServerWorld, Server
val unloadable = world.entityIndex
.query(
aabbd,
filter = Predicate { it.isApplicableForUnloading && !it.isRemote && aabbd.isInside(it.position) },
filter = Predicate { it.isPersistent && !it.isRemote && aabbd.isInside(it.position) },
withEdges = false)
world.storage.saveCells(pos, copyCells())

View File

@ -57,6 +57,48 @@ class SBPattern private constructor(
return hash
}
/**
* Different from regular resolve that this always returns non-null string,
* even if we didn't replace all tags (not replaced tags appear as `<tags>` 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<String>(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!!)
}
}

View File

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

View File

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

View File

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

View File

@ -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<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
var ticks = 0L
private set
abstract val eventLoop: BlockableEventLoop
open fun broadcast(packet: IPacket) {
}
@ -312,8 +313,6 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
protected abstract fun chunkFactory(pos: ChunkPos): ChunkType
abstract val eventLoop: BlockableEventLoop
fun entitiesAtTile(pos: Vector2i, filter: Predicate<TileEntity> = Predicate { true }): List<TileEntity> {
return entityIndex.query(
AABBi(pos, pos + Vector2i.POSITIVE_XY).toDoubleAABB(),

View File

@ -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<Abstr
* Returning false will also stop entity from being saved to disk, and render entity orphaned
* when chunk containing it will get unloaded
*/
open val isApplicableForUnloading: Boolean
open val isPersistent: Boolean
get() = true
val team = networkedData(EntityDamageTeam(), EntityDamageTeam.CODEC, EntityDamageTeam.LEGACY_CODEC)
enum class RemovalReason(val removal: Boolean, val dying: Boolean, val remote: Boolean) {
UNLOADED(false, false, false), // Being saved to disk
REMOVED(true, false, false), // Got removed from world
@ -111,6 +115,9 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
*/
abstract val metaBoundingBox: AABB
open val collisionArea: AABB
get() = NEVER
open fun onNetworkUpdate() {
}
@ -189,5 +196,6 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
companion object {
private val LOGGER = LogManager.getLogger()
private val NEVER = AABB(Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY), Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY))
}
}

View File

@ -1,5 +1,7 @@
package ru.dbotthepony.kstarbound.world.entities
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
@ -9,15 +11,20 @@ import ru.dbotthepony.kommons.gson.getArray
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.io.IntValueCodec
import ru.dbotthepony.kommons.io.map
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.matrix.Matrix3d
import ru.dbotthepony.kommons.matrix.Matrix3f
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.animation.AnimatedPartsDefinition
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.animation.ParticleFactory
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.math.PeriodicFunction
@ -39,8 +46,10 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
import ru.dbotthepony.kstarbound.network.syncher.networkedString
import ru.dbotthepony.kstarbound.network.syncher.networkedUnsignedInt
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.*
import java.util.function.Consumer
import kotlin.NoSuchElementException
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
@ -183,6 +192,7 @@ class Animator() {
private var activeStateChanged = false
private var frameChanged = false
private var firstTime = true
var activeState: AnimatedPartsDefinition.StateType.State? = null
set(value) {
@ -216,9 +226,14 @@ class Animator() {
fun set(state: String, alwaysStart: Boolean): Boolean {
val getState = states[state] ?: return false
if (activeState != getState || alwaysStart) {
// allow to reset active state if calling for first time
// without this client and server will disagree what state is active
// if there default state present
if (activeState != getState || alwaysStart || firstTime) {
activeState = getState
timer = 0.0
startedEvent.trigger()
firstTime = true
return true
}
@ -227,8 +242,11 @@ class Animator() {
init {
if (default in states) {
noPropagate = true
activeState = states[default]!!
stateIndex.accept(activeState!!.index.toLong())
startedEvent.trigger()
noPropagate = false
}
stateIndex.addListener(Consumer {
@ -284,12 +302,74 @@ class Animator() {
}
}
}
fun finishAnimations() {
while (true) {
val active = activeState ?: break
if (active.mode == AnimatedPartsDefinition.AnimationMode.END) {
timer = active.cycle
} else if (active.mode == AnimatedPartsDefinition.AnimationMode.TRANSITION) {
activeState = states[active.transition]!! // validity of 'transition' is checked during json load
this.activeState = activeState
timer = 0.0
continue
}
break
}
}
}
private class Part(config: AnimatedPartsDefinition.Part) {
val partProperties = config.properties
private inner class Part(private val config: AnimatedPartsDefinition.Part) {
var activePartDirty = true
val partStates = config.partStates
var properties: JsonObject = config.properties.deepCopy()
private set
var activeState: AnimatedPartsDefinition.Part.State? = null
private set
fun freshen() {
if (!activePartDirty)
return
activePartDirty = false
// First reset all the active part information assuming that no state type
// x state match exists.
activeState = null
properties = config.properties.deepCopy()
// Then go through each of the state types and states and look for a part
// state match in order of priority.
// honestly, i have no idea what next code does
for ((stateTypeName, stateType) in stateTypes) {
// Skip disabled state types
if (!stateType.enabled)
continue
val partStateType = config.partStates[stateTypeName] ?: continue
val stateName = stateType.activeState?.name ?: continue
val partState = partStateType[stateName] ?: continue
activeState = partState
val frame = stateType.frame
// Then set the part state data, as well as any part state frame data if
// the current frame is within the list size.
mergeJson(properties, partState.properties)
for ((key, props) in partState.frameProperties) {
if (props.size() > 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<NetworkedElement>()
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<String> {
return Collections.unmodifiableCollection(rotationGroups.keys)
}
fun transformationGroups(): Collection<String> {
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<String> {
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<String> {
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<String> {
return Collections.unmodifiableCollection(sounds.keys)
}
fun hasSound(sound: String): Boolean {
return sound in sounds
}
fun setSoundPool(sound: String, pool: Collection<String>) {
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<String> {
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<String> {
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<String>())
private val vectors by lazy { Starbound.gson.getAdapter(Vector2d::class.java) }
private val polies by lazy { Starbound.gson.getAdapter(Poly::class.java) }
}
}

View File

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

View File

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

View File

@ -83,11 +83,11 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
private val currentMaterialSpaces = HashSet<Pair<Vector2i, Registry.Ref<TileDefinition>>>()
private val currentRoots = HashSet<Vector2i>()
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<Pair<Vector2i, Registry.Ref<TileDefinition>>>): Boolean {
check(world.isServer) { "Invalid realm" }
val toRemove = ArrayList<Pair<Vector2i, Registry.Ref<TileDefinition>>>()
val toPlace = ArrayList<Pair<Vector2i, Registry.Ref<TileDefinition>>>()
@ -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()
}
}

View File

@ -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<ObjectDefinition>) : 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<ObjectDefinition>) : 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<ObjectDefinition>) : 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<ObjectDefinition>) : TileEntit
//
var color: TileColor by Property(JsonPath("color"), TileColor.DEFAULT)
var animationParts: ImmutableMap<String, SpriteReference> 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<ObjectDefinition>) : 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<ObjectDefinition>) : 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<ObjectDefinition>) : 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<ObjectDefinition>) : TileEntit
setImageKey("frame", frame.toString())
}
}
try {
luaUpdate.update()
} catch (err: Throwable) {
LogManager.getLogger().error("Error running update", err)
}
}
if (world.isServer && !unbreakable) {

View File

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