More Lua bindings

This commit is contained in:
DBotThePony 2024-05-02 17:22:50 +07:00
parent 151571e8d0
commit 782fb29dbf
Signed by: DBot
GPG Key ID: DCC23B5715498507
19 changed files with 659 additions and 532 deletions

View File

@ -129,6 +129,15 @@ val color: TileColor = TileColor.DEFAULT
* Added `world.objectLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>` * Added `world.objectLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>`
* Added `world.loungeableLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>` * Added `world.loungeableLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>`
* `world.entityCanDamage(source: EntityID, target: EntityID): Boolean` now properly accounts for case when `source == target` * `world.entityCanDamage(source: EntityID, target: EntityID): Boolean` now properly accounts for case when `source == target`
* `world.entityHandItem(id: EntityID, hand: String): String` now accepts `"secondary"` as `hand` argument (in addition to `"primary"`/`"alt"`)
* `world.containerConsume(id: EntityID, item: ItemDescriptor, exact: Boolean?): Boolean?` now accepts `exact` which forces exact match on item descriptor (default `false`)
* `world.containerStackItems(id: EntityID, items: ItemDescriptor): ItemDescriptor` now actually does what it says on tin, instead of being equal to `world.containerAddItems`
* **ONLY** for local entities, or when using native protocol (but why would you ever mutate containers over network in first place)
* Remote entities on legacy protocol will behave like `world.containerAddItems` has been called
* `world.containerItemApply(id: EntityID, items: ItemDescriptor, slot: Int): ItemDescriptor` is no longer equal to `world.containerSwapItemsNoCombine` and does what its docs say, but im not sure if it is ever makes sense
* Clarification - Original docs are not very clear, but what it does is it tries to put provided item into target slot _only_ if it contains item of same type (contains stackable). If slot is empty or item in slot can not be stacked with provided item, this function does nothing (and returns stack initially passed to the function)
* **ONLY** for local entities, or when using native protocol (but why would you ever mutate containers over network in first place)
* Remote entities on legacy protocol will try to simulate new behavior locally using item checks and remote `putItems` message
--------------- ---------------

View File

@ -0,0 +1,13 @@
package ru.dbotthepony.kstarbound.item
class ContainerIterator(private val container: IContainer) : Iterator<ItemStack> {
private var index = 0
override fun hasNext(): Boolean {
return index < container.size
}
override fun next(): ItemStack {
return container[index++]
}
}

View File

@ -6,12 +6,13 @@ import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.ints.IntIterable import it.unimi.dsi.fastutil.ints.IntIterable
import it.unimi.dsi.fastutil.ints.IntIterator import it.unimi.dsi.fastutil.ints.IntIterator
import it.unimi.dsi.fastutil.ints.IntIterators import it.unimi.dsi.fastutil.ints.IntIterators
import it.unimi.dsi.fastutil.ints.IntList
import it.unimi.dsi.fastutil.ints.IntLists import it.unimi.dsi.fastutil.ints.IntLists
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
interface IContainer { interface IContainer : Iterable<ItemStack> {
var size: Int var size: Int
operator fun get(index: Int): ItemStack operator fun get(index: Int): ItemStack
operator fun set(index: Int, value: ItemStack) operator fun set(index: Int, value: ItemStack)
@ -114,20 +115,41 @@ interface IContainer {
} }
} }
// returns not inserted items /**
fun add(item: ItemStack, simulate: Boolean = false): ItemStack { * not inserted items -> slots touched
*/
fun add(item: ItemStack, simulate: Boolean = false): Pair<ItemStack, IntList> {
return put({ IntIterators.fromTo(0, size) }, item, simulate) return put({ IntIterators.fromTo(0, size) }, item, simulate)
} }
// returns not inserted items /**
fun put(slots: IntIterable, item: ItemStack, simulate: Boolean = false): ItemStack { * not inserted items -> slots touched
*/
fun stackWithExisting(item: ItemStack, simulate: Boolean = false): Pair<ItemStack, IntList> {
val validSlots = IntArrayList(size)
for (slot in 0 until size) {
if (this[slot].isNotEmpty) {
validSlots.add(slot)
}
}
return put(validSlots, item, simulate)
}
/**
* not inserted items -> slots touched
*/
fun put(slots: IntIterable, item: ItemStack, simulate: Boolean = false): Pair<ItemStack, IntList> {
return put(slots::iterator, item, simulate) return put(slots::iterator, item, simulate)
} }
// returns not inserted items /**
fun put(slots: () -> IntIterator, item: ItemStack, simulate: Boolean = false): ItemStack { * not inserted items -> slots touched
*/
fun put(slots: () -> IntIterator, item: ItemStack, simulate: Boolean = false): Pair<ItemStack, IntList> {
val copy = item.copy() val copy = item.copy()
val touched = IntArrayList()
var itr = slots.invoke() var itr = slots.invoke()
while (itr.hasNext()) { while (itr.hasNext()) {
@ -139,6 +161,7 @@ interface IContainer {
val itemThere = this[slot] val itemThere = this[slot]
if (itemThere.isStackable(copy)) { if (itemThere.isStackable(copy)) {
touched.add(slot)
val newCount = (itemThere.size + copy.size).coerceAtMost(itemThere.maxStackSize) val newCount = (itemThere.size + copy.size).coerceAtMost(itemThere.maxStackSize)
val diff = newCount - itemThere.size val diff = newCount - itemThere.size
@ -148,7 +171,7 @@ interface IContainer {
itemThere.size += diff itemThere.size += diff
if (copy.isEmpty) if (copy.isEmpty)
return ItemStack.EMPTY return ItemStack.EMPTY to touched
} }
} }
@ -163,23 +186,23 @@ interface IContainer {
val itemThere = this[slot] val itemThere = this[slot]
if (itemThere.isEmpty) { if (itemThere.isEmpty) {
if (itemThere.isEmpty) { touched.add(slot)
if (copy.size > copy.maxStackSize) {
if (!simulate)
this[slot] = copy.copy(copy.maxStackSize)
copy.size -= copy.maxStackSize if (copy.size > copy.maxStackSize) {
} else { if (!simulate)
if (!simulate) this[slot] = copy.copy(copy.maxStackSize)
this[slot] = copy
return ItemStack.EMPTY copy.size -= copy.maxStackSize
} } else {
if (!simulate)
this[slot] = copy
return ItemStack.EMPTY to touched
} }
} }
} }
return copy return copy to touched
} }
fun take(slot: Int, amount: Long): ItemStack { fun take(slot: Int, amount: Long): ItemStack {
@ -233,20 +256,20 @@ interface IContainer {
} else if (item.isNotEmpty && existingItem.isEmpty) { } else if (item.isNotEmpty && existingItem.isEmpty) {
// place into slot // place into slot
// use put because item might be bigger than max stack size // use put because item might be bigger than max stack size
return put(IntLists.singleton(slot), item) return put(IntLists.singleton(slot), item).first
} else { } else {
// If something is there, try to stack with it first. If we can't stack, // If something is there, try to stack with it first. If we can't stack,
// then swap. // then swap.
if (tryCombine && existingItem.isStackable(item)) { if (tryCombine && existingItem.isStackable(item)) {
return put(IntLists.singleton(slot), item) return put(IntLists.singleton(slot), item).first
} else { } else {
this[slot] = ItemStack.EMPTY this[slot] = ItemStack.EMPTY
val slots = IntArrayList(IntIterators.fromTo(0, size)) val slots = IntArrayList(IntIterators.fromTo(0, size))
slots.removeInt(slot) slots.removeInt(slot)
slots.add(0, slot) slots.add(0, slot)
val remaining = put(slots, item) val remaining = put(slots, item).first
if (remaining.isNotEmpty && this[slot].isStackable(remaining)) { if (remaining.isNotEmpty && this[slot].isStackable(remaining)) {
// damn // damn
@ -258,6 +281,14 @@ interface IContainer {
} }
} }
fun combineAt(slot: Int, item: ItemStack): ItemStack {
if (this[slot].isEmpty || !this[slot].isStackable(item)) {
return item
} else {
return put(IntLists.singleton(slot), item).first
}
}
fun take(descriptor: ItemDescriptor, exactMatch: Boolean = false, simulate: Boolean = false): Boolean { fun take(descriptor: ItemDescriptor, exactMatch: Boolean = false, simulate: Boolean = false): Boolean {
var toTake = descriptor.count var toTake = descriptor.count
@ -387,7 +418,7 @@ interface IContainer {
val lost = ArrayList<ItemStack>() val lost = ArrayList<ItemStack>()
for (i in size until read.size) { for (i in size until read.size) {
val remaining = add(read[i]) val remaining = add(read[i]).first
if (remaining.isNotEmpty) { if (remaining.isNotEmpty) {
lost.add(remaining) lost.add(remaining)
@ -408,4 +439,8 @@ interface IContainer {
return emptyList() return emptyList()
} }
} }
override fun iterator(): Iterator<ItemStack> {
return ContainerIterator(this)
}
} }

View File

@ -41,6 +41,10 @@ object ItemRegistry {
val isNotEmpty: Boolean val isNotEmpty: Boolean
get() = !isEmpty get() = !isEmpty
// for Lua scripts
val nameOrNull: String?
get() = if (isEmpty) null else name
val directory = file?.computeDirectory() ?: "/" val directory = file?.computeDirectory() ?: "/"
} }

View File

@ -366,6 +366,14 @@ open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, para
return createDescriptor().toJson() return createDescriptor().toJson()
} }
fun toTable(allocator: TableFactory): Table? {
if (isEmpty) {
return null
}
return createDescriptor().toTable(allocator)
}
class Adapter(gson: Gson) : TypeAdapter<ItemStack>() { class Adapter(gson: Gson) : TypeAdapter<ItemStack>() {
override fun write(out: JsonWriter, value: ItemStack?) { override fun write(out: JsonWriter, value: ItemStack?) {
val json = value?.toJson() val json = value?.toJson()

View File

@ -156,6 +156,26 @@ fun TableFactory.tableOf(vararg values: Any?): Table {
return table return table
} }
fun TableFactory.tableOf(vararg values: Int): Table {
val table = newTable(values.size, 0)
for ((i, v) in values.withIndex()) {
table[i + 1L] = v.toLong()
}
return table
}
fun TableFactory.tableOf(vararg values: Long): Table {
val table = newTable(values.size, 0)
for ((i, v) in values.withIndex()) {
table[i + 1L] = v
}
return table
}
fun TableFactory.tableMapOf(vararg values: Pair<Any, Any?>): Table { fun TableFactory.tableMapOf(vararg values: Pair<Any, Any?>): Table {
val table = newTable(0, values.size) val table = newTable(0, values.size)

View File

@ -4,6 +4,7 @@ import org.classdump.luna.ByteString
import org.classdump.luna.LuaRuntimeException import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.Table import org.classdump.luna.Table
import org.classdump.luna.runtime.ExecutionContext import org.classdump.luna.runtime.ExecutionContext
import ru.dbotthepony.kommons.gson.JsonArrayCollector
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
@ -15,17 +16,23 @@ import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.indexNoYield import ru.dbotthepony.kstarbound.lua.indexNoYield
import ru.dbotthepony.kstarbound.lua.iterator import ru.dbotthepony.kstarbound.lua.iterator
import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.luaFunctionN
import ru.dbotthepony.kstarbound.lua.luaStub import ru.dbotthepony.kstarbound.lua.luaStub
import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableMapOf
import ru.dbotthepony.kstarbound.lua.tableOf import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toAABB import ru.dbotthepony.kstarbound.lua.toAABB
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toLine2d import ru.dbotthepony.kstarbound.lua.toLine2d
import ru.dbotthepony.kstarbound.lua.toPoly import ru.dbotthepony.kstarbound.lua.toPoly
import ru.dbotthepony.kstarbound.lua.toVector2d import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.toVector2i import ru.dbotthepony.kstarbound.lua.toVector2i
import ru.dbotthepony.kstarbound.lua.unpackAsArray import ru.dbotthepony.kstarbound.lua.unpackAsArray
import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.stream
import ru.dbotthepony.kstarbound.util.random.shuffle import ru.dbotthepony.kstarbound.util.random.shuffle
import ru.dbotthepony.kstarbound.util.valueOf import ru.dbotthepony.kstarbound.util.valueOf
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
@ -35,8 +42,11 @@ import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
import ru.dbotthepony.kstarbound.world.entities.HumanoidActorEntity import ru.dbotthepony.kstarbound.world.entities.HumanoidActorEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.api.InspectableEntity import ru.dbotthepony.kstarbound.world.entities.api.InspectableEntity
import ru.dbotthepony.kstarbound.world.entities.api.InteractiveEntity
import ru.dbotthepony.kstarbound.world.entities.api.LoungeableEntity
import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
import ru.dbotthepony.kstarbound.world.entities.tile.ContainerObject
import ru.dbotthepony.kstarbound.world.entities.tile.LoungeableObject import ru.dbotthepony.kstarbound.world.entities.tile.LoungeableObject
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
@ -356,4 +366,241 @@ fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEn
returnBuffer.setTo(tableOf(*entity.portrait(ActorEntity.PortraitMode.entries.valueOf(mode.decode())).map { from(it.toJson()) }.toTypedArray())) returnBuffer.setTo(tableOf(*entity.portrait(ActorEntity.PortraitMode.entries.valueOf(mode.decode())).map { from(it.toJson()) }.toTypedArray()))
} }
callbacks["entityHandItem"] = luaFunction { id: Number, hand: ByteString ->
val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()
when (val gethand = hand.decode().lowercase()) {
"primary" -> returnBuffer.setTo(entity.primaryHandItem.entry.nameOrNull)
"alt", "secondary" -> returnBuffer.setTo(entity.secondaryHandItem.entry.nameOrNull)
else -> throw LuaRuntimeException("Unknown tool hand $gethand")
}
}
callbacks["entityHandItemDescriptor"] = luaFunction { id: Number, hand: ByteString ->
val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()
when (val gethand = hand.decode().lowercase()) {
"primary" -> returnBuffer.setTo(entity.primaryHandItem.toTable(this))
"alt", "secondary" -> returnBuffer.setTo(entity.secondaryHandItem.toTable(this))
else -> throw LuaRuntimeException("Unknown tool hand $gethand")
}
}
callbacks["entityUniqueId"] = luaFunction { id: Number ->
returnBuffer.setTo(self.entities[id.toInt()]?.uniqueID?.get())
}
callbacks["getObjectParameter"] = luaFunction { id: Number, parameter: ByteString, defaultValue: Any? ->
// FIXME: this is stupid (defaultValue is ignored when we lookup parameter on non existing entity),
// but we must retain original behavior
val entity = self.entities[id.toInt()] as? WorldObject ?: return@luaFunction returnBuffer.setTo()
val result = entity.lookupProperty(parameter.decode())
if (result.isJsonNull) {
returnBuffer.setTo(defaultValue)
} else {
returnBuffer.setTo(from(result))
}
}
callbacks["getNpcScriptParameter"] = luaStub("getNpcScriptParameter")
callbacks["objectSpaces"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? WorldObject ?: return@luaFunction returnBuffer.setTo(tableOf())
returnBuffer.setTo(tableOf(*entity.occupySpaces.map { from(it - entity.tilePosition) }.toTypedArray()))
}
callbacks["farmableStage"] = luaStub("farmableStage")
callbacks["containerSize"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(entity.items.size)
}
callbacks["containerClose"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(false)
// FIXME: this doesn't get networked if called on client
// (AND this is the reason why in multiplayer player can't see chest/container open animations when
// other players open them)
entity.closeContainer()
returnBuffer.setTo(true)
}
callbacks["containerOpen"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(false)
// FIXME: this doesn't get networked if called on client
// (AND this is the reason why in multiplayer player can't see chest/container open animations when
// other players open them)
entity.openContainer()
returnBuffer.setTo(true)
}
callbacks["containerItems"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(tableOf(*entity.items.filter { it.isNotEmpty }.map { it.toTable(this) }.toTypedArray()))
}
callbacks["containerItemAt"] = luaFunction { id: Number, index: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
if (index.toInt() < entity.items.size)
returnBuffer.setTo(entity.items[index.toInt()].toTable(this))
}
callbacks["containerConsume"] = luaFunction { id: Number, desc: Any, exactMatch: Boolean? ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(entity.takeItem(ItemDescriptor(desc), exact = exactMatch ?: false).getNow(null))
}
callbacks["containerConsumeAt"] = luaFunction { id: Number, slot: Number, amount: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(entity.takeItemAt(slot.toInt(), amount.toLong()).getNow(null))
}
callbacks["containerAvailable"] = luaFunction { id: Number, desc: Any ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(entity.items.take(ItemDescriptor(desc), simulate = true))
}
// why we have containerItems + containerTakeAll, when we could have containerItems + containerClear?????????
callbacks["containerTakeAll"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(tableOf(*entity.clearContainer().getNow(listOf()).map { it.toTable(this) }.toTypedArray()))
}
callbacks["containerTakeAt"] = luaFunction { id: Number, slot: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
if (slot.toInt() < entity.items.size)
returnBuffer.setTo(entity.takeItemAt(slot.toInt()).getNow(null))
}
callbacks["containerTakeNumItemsAt"] = luaFunction { id: Number, slot: Number, amount: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
if (slot.toInt() < entity.items.size)
returnBuffer.setTo(entity.takeItemAt(slot.toInt(), amount.toLong()).getNow(null))
}
callbacks["containerItemsCanFit"] = luaFunction { id: Number, desc: Any ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
val item = ItemDescriptor(desc).build(random = lua.random)
returnBuffer.setTo(item.size - entity.items.add(item, simulate = true).first.size)
}
callbacks["containerItemsFitWhere"] = luaFunction { id: Number, desc: Any ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
val item = ItemDescriptor(desc).build(random = lua.random)
val (leftover, touched) = entity.items.add(item, simulate = true)
returnBuffer.setTo(tableMapOf(
"leftover" to leftover.size,
"slots" to tableOf(*touched.toIntArray())
))
}
callbacks["containerAddItems"] = luaFunction { id: Number, desc: Any ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
val build = ItemDescriptor(desc).build(random = lua.random)
returnBuffer.setTo(entity.addItems(build).getNow(build)?.toTable(this))
}
callbacks["containerStackItems"] = luaFunction { id: Number, desc: Any ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
val build = ItemDescriptor(desc).build(random = lua.random)
returnBuffer.setTo(entity.stackWithExisting(build).getNow(build)?.toTable(this))
}
callbacks["containerPutItemsAt"] = luaFunction { id: Number, desc: Any, slot: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
val build = ItemDescriptor(desc).build(random = lua.random)
returnBuffer.setTo(entity.putItems(slot.toInt(), build).getNow(build)?.toTable(this))
}
callbacks["containerSwapItems"] = luaFunction { id: Number, desc: Any, slot: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
val build = ItemDescriptor(desc).build(random = lua.random)
returnBuffer.setTo(entity.swapItems(slot.toInt(), build, tryCombine = true).getNow(build)?.toTable(this))
}
callbacks["containerSwapItemsNoCombine"] = luaFunction { id: Number, desc: Any, slot: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
val build = ItemDescriptor(desc).build(random = lua.random)
returnBuffer.setTo(entity.swapItems(slot.toInt(), build, tryCombine = false).getNow(build)?.toTable(this))
}
callbacks["containerItemApply"] = luaFunction { id: Number, desc: Any, slot: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
val build = ItemDescriptor(desc).build(random = lua.random)
returnBuffer.setTo(entity.combineItems(slot.toInt(), build).getNow(build)?.toTable(this))
}
callbacks["callScriptedEntity"] = luaFunctionN("callScriptedEntity") {
val id = it.nextInteger()
val function = it.nextString().decode()
val entity = self.entities[id.toInt()] ?: throw LuaRuntimeException("Entity with ID $id does not exist")
if (entity !is ScriptedEntity)
throw LuaRuntimeException("$entity is not scripted entity")
if (entity.isRemote)
throw LuaRuntimeException("$entity is not owned by this side")
returnBuffer.setToContentsOf(entity.callScript(function, *it.copyRemaining()))
}
callbacks["findUniqueEntity"] = luaStub("findUniqueEntity")
callbacks["sendEntityMessage"] = luaFunctionN("sendEntityMessage") {
val id = it.nextAny()
val func = it.nextString().decode()
if (id is Number) {
val entityID = id.toInt()
returnBuffer.setTo(LuaFuture(
future = self.dispatchEntityMessage(self.connectionID, entityID, func, it.copyRemaining()
.stream().map { toJsonFromLua(it) }.collect(JsonArrayCollector)).thenApply { from(it) },
isLocal = Connection.connectionForEntityID(entityID) == self.connectionID
))
} else {
id as ByteString
val entityID = id.decode()
val findAlreadyLoaded = self.entities.values.find { it.uniqueID.get() == entityID }
val isLocal = if (findAlreadyLoaded == null)
self.isServer
else
!findAlreadyLoaded.isRemote
returnBuffer.setTo(LuaFuture(
future = self.dispatchEntityMessage(self.connectionID, entityID, func, it.copyRemaining()
.stream().map { toJsonFromLua(it) }.collect(JsonArrayCollector)).thenApply { from(it) },
isLocal = isLocal
))
}
}
callbacks["loungeableOccupied"] = luaStub("loungeableOccupied")
callbacks["isMonster"] = luaStub("isMonster")
callbacks["monsterType"] = luaStub("monsterType")
callbacks["npcType"] = luaStub("npcType")
callbacks["stagehandType"] = luaStub("stagehandType")
callbacks["isNpc"] = luaStub("isNpc")
callbacks["isEntityInteractive"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()]
if (entity is InteractiveEntity)
returnBuffer.setTo(entity.isInteractive)
}
callbacks["entityMouthPosition"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] ?: return@luaFunction returnBuffer.setTo()
// original entine returns non nil only for "Chatty entity"
returnBuffer.setTo(from(entity.mouthPosition))
}
callbacks["entityTypeName"] = luaStub("entityTypeName")
} }

View File

@ -0,0 +1,76 @@
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 java.util.concurrent.CancellationException
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException
/**
* [isLocal] tells us whenever result() should block current thread if [future] is not yet fulfilled
* (to retain original behavior for "locally async" calls)
*
* god damn it.
*/
class LuaFuture(val future: CompletableFuture<out Any?>, val isLocal: Boolean) : Userdata<CompletableFuture<out Any?>>() {
override fun getMetatable(): Table {
return metadata
}
override fun setMetatable(mt: Table?): Table {
throw UnsupportedOperationException()
}
override fun getUserValue(): CompletableFuture<out Any?> {
return future
}
override fun setUserValue(value: CompletableFuture<out Any?>?): CompletableFuture<out Any?> {
throw UnsupportedOperationException()
}
companion object {
private val metadata = ImmutableTable.Builder()
.add("finished", luaFunction { self: LuaFuture ->
returnBuffer.setTo(self.future.isDone)
})
.add("succeeded", luaFunction { self: LuaFuture ->
returnBuffer.setTo(!self.future.isCompletedExceptionally)
})
.add("failed", luaFunction { self: LuaFuture ->
returnBuffer.setTo(self.future.isCompletedExceptionally)
})
.add("result", luaFunction { self: LuaFuture ->
try {
if (self.future.isCompletedExceptionally) {
returnBuffer.setTo()
} else if (self.isLocal) {
returnBuffer.setTo(self.future.join())
} else {
returnBuffer.setTo(self.future.getNow(null))
}
} catch (err: CompletionException) {
returnBuffer.setTo()
} catch (err: CancellationException) {
returnBuffer.setTo()
}
})
.add("error", luaFunction { self: LuaFuture ->
// this is slow, but we can't get Exception out of CompletableFuture without latter throwing former
try {
if (self.isLocal) {
returnBuffer.setTo(self.future.join())
} else {
returnBuffer.setTo(self.future.getNow(null))
}
} catch (err: CompletionException) {
returnBuffer.setTo(err.message ?: "internal error")
} catch (err: CancellationException) {
returnBuffer.setTo(err.message ?: "internal error")
}
})
.build()
}
}

View File

@ -45,24 +45,20 @@ class EntityMessagePacket(val entity: Either<Int, String>, val message: String,
} }
private fun handle(connection: Connection, world: World<*, *>) { private fun handle(connection: Connection, world: World<*, *>) {
val entity = if (entity.isLeft) { val future = if (entity.isLeft) {
world.entities[entity.left()] world.dispatchEntityMessage(connection.connectionID, entity.left(), message, arguments)
} else { } else {
world.entities.values.firstOrNull { it.uniqueID.get() == entity.right() } world.dispatchEntityMessage(connection.connectionID, entity.right(), message, arguments)
} }
if (entity == null) { future
connection.send(EntityMessageResponsePacket(Either.left("No such entity ${this@EntityMessagePacket.entity}"), id)) .thenAccept(Consumer {
} else { connection.send(EntityMessageResponsePacket(Either.right(it), id))
entity.dispatchMessage(connection.connectionID, message, arguments) })
.thenAccept(Consumer { .exceptionally(Function {
connection.send(EntityMessageResponsePacket(Either.right(it), id)) connection.send(EntityMessageResponsePacket(Either.left(it.message ?: "Internal server error"), id))
}) null
.exceptionally(Function { })
connection.send(EntityMessageResponsePacket(Either.left(it.message ?: "Internal server error"), id))
null
})
}
} }
override fun play(connection: ServerConnection) { override fun play(connection: ServerConnection) {

View File

@ -14,6 +14,7 @@ import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
@ -36,7 +37,7 @@ class EntityMessageResponsePacket(val response: Either<String, JsonElement>, val
if (message != null) { if (message != null) {
if (response.isLeft) { if (response.isLeft) {
message.completeExceptionally(AbstractEntity.MessageCallException(response.left())) message.completeExceptionally(World.MessageCallException(response.left()))
} else { } else {
message.complete(response.right()) message.complete(response.right())
} }
@ -50,7 +51,7 @@ class EntityMessageResponsePacket(val response: Either<String, JsonElement>, val
if (message != null) { if (message != null) {
if (response.isLeft) { if (response.isLeft) {
message.completeExceptionally(AbstractEntity.MessageCallException(response.left())) message.completeExceptionally(World.MessageCallException(response.left()))
} else { } else {
message.complete(response.right()) message.complete(response.right())
} }

View File

@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.EntityInteractResultPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.EntityInteractResultPacket
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.world.entities.api.InteractiveEntity
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.UUID import java.util.UUID
@ -28,14 +29,13 @@ class EntityInteractPacket(val request: InteractRequest, val id: UUID) : IServer
override fun play(connection: ServerConnection) { override fun play(connection: ServerConnection) {
if (request.target >= 0) { if (request.target >= 0) {
connection.enqueue { connection.enqueue {
connection.send(EntityInteractResultPacket(entities[request.target]?.interact(request) ?: InteractAction.NONE, id, request.source)) connection.send(EntityInteractResultPacket((entities[request.target] as? InteractiveEntity)?.interact(request) ?: InteractAction.NONE, id, request.source))
} }
} else { } else {
val other = connection.server.channels.connectionByID(Connection.connectionForEntityID(request.target)) ?: throw IllegalArgumentException("No such connection ID ${Connection.connectionForEntityID(request.target)} for EntityInteractPacket") val other = connection.server.channels.connectionByID(Connection.connectionForEntityID(request.target)) ?: throw IllegalArgumentException("No such connection ID ${Connection.connectionForEntityID(request.target)} for EntityInteractPacket")
if (other == connection) { if (other == connection)
throw IllegalStateException("Attempt to interact with own entity through server?") throw IllegalArgumentException("Attempt to interact with own entity through server?")
}
other.send(this) other.send(this)
} }

View File

@ -1,442 +0,0 @@
package ru.dbotthepony.kstarbound.util
import java.util.*
import java.util.concurrent.Callable
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Delayed
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
import java.util.concurrent.FutureTask
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.LockSupport
import java.util.function.Consumer
import kotlin.NoSuchElementException
import kotlin.collections.ArrayList
private fun <E : Comparable<E>> LinkedList<E>.enqueue(value: E) {
if (isEmpty()) {
add(value)
} else if (first >= value) {
addFirst(value)
} else if (last <= value) {
addLast(value)
} else {
val iterator = listIterator()
while (iterator.hasNext()) {
val i = iterator.next()
if (i >= value) {
iterator.previous()
iterator.add(value)
break
}
}
}
}
/**
* [ScheduledExecutorService] which act as a mailbox, [executeQueuedTasks] must be called from main thread.
*
* [submit], [execute], etc can be called on any thread. If any of enqueueing methods are called on the same thread
* as where [executeQueuedTasks] was called, executes provided lambda immediately and returns completed future.
*/
class MailboxExecutorService(@Volatile var thread: Thread = Thread.currentThread()) : ScheduledExecutorService {
private val futureQueue = ConcurrentLinkedQueue<FutureTask<*>>()
private val timers = LinkedList<Timer<*>>()
private val repeatableTimers = LinkedList<RepeatableTimer>()
@Volatile
private var isShutdown = false
@Volatile
private var isTerminated = false
private val timeOrigin = JVMClock()
var exceptionHandler: Consumer<Throwable>? = null
private inner class Timer<T>(task: Callable<T>, val executeAt: Long) : FutureTask<T>(task), ScheduledFuture<T> {
override fun compareTo(other: Delayed): Int {
return getDelay(TimeUnit.NANOSECONDS).compareTo(other.getDelay(TimeUnit.NANOSECONDS))
}
override fun getDelay(unit: TimeUnit): Long {
return unit.convert(executeAt, TimeUnit.NANOSECONDS) - timeOrigin.nanos
}
}
private data class CompletedFuture<T>(private val value: T) : Future<T> {
override fun cancel(mayInterruptIfRunning: Boolean): Boolean {
return false
}
override fun isCancelled(): Boolean {
return false
}
override fun isDone(): Boolean {
return true
}
override fun get(): T {
return value
}
override fun get(timeout: Long, unit: TimeUnit): T {
return value
}
companion object {
val VOID = CompletedFuture(Unit)
}
}
private inner class RepeatableTimer(
task: Runnable,
initialDelay: Long,
val period: Long,
val fixedDelay: Boolean,
): FutureTask<Unit>({ task.run() }), ScheduledFuture<Unit> {
var next = initialDelay
private set
public override fun runAndReset(): Boolean {
if (fixedDelay) {
next += period
return super.runAndReset()
} else {
try {
return super.runAndReset()
} finally {
next += period
}
}
}
override fun compareTo(other: Delayed): Int {
return getDelay(TimeUnit.NANOSECONDS).compareTo(other.getDelay(TimeUnit.NANOSECONDS))
}
override fun getDelay(unit: TimeUnit): Long {
return unit.convert(next, TimeUnit.NANOSECONDS) - timeOrigin.nanos
}
}
fun isSameThread(): Boolean {
return Thread.currentThread() === thread
}
fun executeQueuedTasks() {
thread = Thread.currentThread()
if (isShutdown) {
if (!isTerminated) {
isTerminated = true
futureQueue.forEach {
it.cancel(false)
}
futureQueue.clear()
timers.clear()
repeatableTimers.clear()
return
}
}
var next = futureQueue.poll()
while (next != null) {
if (isTerminated) return
next.run()
Thread.interrupted()
try {
next.get()
} catch (err: ExecutionException) {
exceptionHandler?.accept(err)
}
next = futureQueue.poll()
}
while (!timers.isEmpty()) {
if (isTerminated) return
val first = timers.first
if (first.isCancelled) {
timers.removeFirst()
} else if (first.executeAt <= timeOrigin.nanos) {
first.run()
Thread.interrupted()
try {
first.get()
} catch (err: ExecutionException) {
exceptionHandler?.accept(err)
}
timers.removeFirst()
} else {
break
}
}
if (repeatableTimers.isNotEmpty()) {
val executed = LinkedList<RepeatableTimer>()
while (repeatableTimers.isNotEmpty()) {
if (isTerminated) return
val first = repeatableTimers.first
if (first.isDone) {
repeatableTimers.removeFirst()
} else if (first.next <= timeOrigin.nanos) {
if (first.runAndReset()) {
executed.add(first)
}
repeatableTimers.removeFirst()
} else {
break
}
}
executed.forEach { repeatableTimers.enqueue(it) }
}
}
override fun execute(command: Runnable) {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
command.run()
} else {
futureQueue.add(FutureTask(command, Unit))
LockSupport.unpark(thread)
}
}
override fun shutdown() {
isShutdown = true
}
override fun shutdownNow(): List<Runnable> {
if (isTerminated) return listOf()
isShutdown = true
isTerminated = true
val result = ArrayList<Runnable>()
futureQueue.forEach {
it.cancel(false)
result.add(it)
}
futureQueue.clear()
timers.forEach { it.cancel(false) }
repeatableTimers.forEach { it.cancel(false) }
timers.clear()
repeatableTimers.clear()
return result
}
override fun isShutdown(): Boolean {
return isShutdown
}
override fun isTerminated(): Boolean {
return isTerminated
}
override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {
throw UnsupportedOperationException()
}
override fun <T : Any?> submit(task: Callable<T>): Future<T> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) return CompletedFuture(task.call())
return FutureTask(task).also { futureQueue.add(it); LockSupport.unpark(thread) }
}
override fun <T : Any?> submit(task: Runnable, result: T): Future<T> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) { task.run(); return CompletedFuture(result) }
return FutureTask { task.run(); result }.also { futureQueue.add(it); LockSupport.unpark(thread) }
}
override fun submit(task: Runnable): Future<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) { task.run(); return CompletedFuture.VOID }
return FutureTask { task.run() }.also { futureQueue.add(it); LockSupport.unpark(thread) }
}
override fun <T : Any?> invokeAll(tasks: Collection<Callable<T>>): List<Future<T>> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
return tasks.map { CompletedFuture(it.call()) }
} else {
return tasks.map { submit(it) }.onEach { it.get() }
}
}
override fun <T : Any?> invokeAll(
tasks: Collection<Callable<T>>,
timeout: Long,
unit: TimeUnit
): List<Future<T>> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
return tasks.map { CompletedFuture(it.call()) }
} else {
return tasks.map { submit(it) }.onEach { it.get(timeout, unit) }
}
}
override fun <T : Any?> invokeAny(tasks: Collection<Callable<T>>): T {
if (tasks.isEmpty())
throw NoSuchElementException("Provided task list is empty")
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
return tasks.first().call()
} else {
return submit(tasks.first()).get()
}
}
override fun <T : Any?> invokeAny(tasks: Collection<Callable<T>>, timeout: Long, unit: TimeUnit): T {
if (tasks.isEmpty())
throw NoSuchElementException("Provided task list is empty")
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
return tasks.first().call()
} else {
return submit(tasks.first()).get(timeout, unit)
}
}
fun <V> join(future: Future<V>): V {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (!isSameThread())
return future.get()
while (!future.isDone) {
executeQueuedTasks()
LockSupport.parkNanos(1_000_000L)
}
return future.get()
}
override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
val timer = Timer({ command.run() }, timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(delay, unit))
if (isSameThread() && delay <= 0L) {
timer.run()
Thread.interrupted()
} else if (isSameThread()) {
timers.enqueue(timer)
} else {
execute {
if (timer.isCancelled) {
// do nothing
} else if (timer.executeAt <= timeOrigin.nanos) {
timer.run()
Thread.interrupted()
} else {
timers.enqueue(timer)
}
}
}
return timer
}
override fun <V : Any?> schedule(callable: Callable<V>, delay: Long, unit: TimeUnit): ScheduledFuture<V> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
val timer = Timer(callable, timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(delay, unit))
if (isSameThread() && delay <= 0L) {
timer.run()
Thread.interrupted()
} else if (isSameThread()) {
timers.enqueue(timer)
} else {
execute {
if (timer.isCancelled) {
// do nothing
} else if (timer.executeAt <= timeOrigin.nanos) {
timer.run()
Thread.interrupted()
} else {
timers.enqueue(timer)
}
}
}
return timer
}
override fun scheduleAtFixedRate(
command: Runnable,
initialDelay: Long,
period: Long,
unit: TimeUnit
): ScheduledFuture<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
return RepeatableTimer(
command,
timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(initialDelay, unit),
TimeUnit.NANOSECONDS.convert(period, unit), true)
.also {
execute {
if (it.isCancelled) {
// do nothing
} else {
repeatableTimers.enqueue(it)
}
}
}
}
override fun scheduleWithFixedDelay(
command: Runnable,
initialDelay: Long,
delay: Long,
unit: TimeUnit
): ScheduledFuture<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
return RepeatableTimer(
command,
timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(initialDelay, unit),
TimeUnit.NANOSECONDS.convert(delay, unit), false)
.also {
execute {
if (it.isCancelled) {
// do nothing
} else {
repeatableTimers.enqueue(it)
}
}
}
}
}

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world
import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonNull import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
@ -14,6 +15,7 @@ import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.collect.filterNotNull import ru.dbotthepony.kommons.collect.filterNotNull
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
@ -32,6 +34,7 @@ import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket
import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket
import ru.dbotthepony.kstarbound.network.packets.EntityMessagePacket
import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket
import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.BlockableEventLoop
@ -582,6 +585,29 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
abstract fun remote(connectionID: Int): Connection? abstract fun remote(connectionID: Int): Connection?
// doesn't write stacktrace
class MessageCallException(message: String) : RuntimeException(message, null, true, false)
fun dispatchEntityMessage(sourceConnection: Int, entityID: Int, message: String, arguments: JsonArray): CompletableFuture<JsonElement> {
val connectionID = Connection.connectionForEntityID(entityID)
if (connectionID == this.connectionID) {
val entity = entities[entityID] ?: return CompletableFuture.failedFuture(MessageCallException("No such entity $entityID"))
return entity.tryHandleMessage(sourceConnection, message, arguments)
} else {
val connection = remote(connectionID) ?: return CompletableFuture.failedFuture(NoSuchElementException("Can't dispatch entity message, no such connection $connectionID"))
val future = CompletableFuture<JsonElement>()
val uuid = UUID(random.nextLong(), random.nextLong())
pendingEntityMessages.put(uuid, future)
connection.send(EntityMessagePacket(Either.left(entityID), message, arguments, uuid, sourceConnection))
return future
}
}
fun dispatchEntityMessage(sourceConnection: Int, entityID: String, message: String, arguments: JsonArray): CompletableFuture<JsonElement> {
TODO()
}
// this *could* have been divided into per-entity map and beheaded world's map // this *could* have been divided into per-entity map and beheaded world's map
// but we can't, because response packets contain only message UUID, and don't contain entity ID // but we can't, because response packets contain only message UUID, and don't contain entity ID
val pendingEntityMessages: Cache<UUID, CompletableFuture<JsonElement>> = Caffeine.newBuilder() val pendingEntityMessages: Cache<UUID, CompletableFuture<JsonElement>> = Caffeine.newBuilder()

View File

@ -9,6 +9,7 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.nullable import ru.dbotthepony.kommons.io.nullable
import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
@ -34,7 +35,6 @@ import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.networkedData import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.EntityIndex import ru.dbotthepony.kstarbound.world.EntityIndex
import ru.dbotthepony.kstarbound.world.TileRayFilter import ru.dbotthepony.kstarbound.world.TileRayFilter
@ -42,8 +42,11 @@ import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.castRay import ru.dbotthepony.kstarbound.world.castRay
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.PriorityQueue
import java.util.UUID import java.util.UUID
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.function.Consumer import java.util.function.Consumer
import java.util.function.Predicate import java.util.function.Predicate
@ -81,9 +84,6 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
LOGGER.error("Error while executing queued task on $this", it) LOGGER.error("Error while executing queued task on $this", it)
} }
var mailbox = MailboxExecutorService().also { it.exceptionHandler = exceptionLogger }
private set
private var innerWorld: World<*, *>? = null private var innerWorld: World<*, *>? = null
val world: World<*, *> val world: World<*, *>
@ -201,9 +201,6 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
check(!world.entities.containsKey(entityID)) { "Duplicate entity ID: $entityID" } check(!world.entities.containsKey(entityID)) { "Duplicate entity ID: $entityID" }
if (mailbox.isShutdown)
mailbox = MailboxExecutorService().also { it.exceptionHandler = exceptionLogger }
innerWorld = world innerWorld = world
world.entities[entityID] = this world.entities[entityID] = this
world.entityList.add(this) world.entityList.add(this)
@ -227,7 +224,9 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
removalReason = reason removalReason = reason
mailbox.shutdownNow() scheduledTasks.forEach { it.cancel(false) }
scheduledTasks.clear()
check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" } check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" }
world.entityList.remove(this) world.entityList.remove(this)
@ -255,12 +254,24 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
} }
} }
open fun interact(request: InteractRequest): InteractAction { // for fast check on completed tasks
return InteractAction.NONE // This is necessary to cancel tasks when we are removed, so we don't reference ourselves
// in event loop after we have been removed
private val scheduledTasks = PriorityQueue<ScheduledFuture<*>>()
protected fun scheduleInTicks(ticks: Int, action: Runnable) {
scheduledTasks.add(world.eventLoop.schedule(action, ticks * Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS))
}
protected fun schedule(time: Long, unit: TimeUnit, action: Runnable) {
scheduledTasks.add(world.eventLoop.schedule(action, time, unit))
} }
var isRemote: Boolean = false var isRemote: Boolean = false
open val mouthPosition: Vector2d
get() = position
private fun isDamageAuthoritative(target: AbstractEntity): Boolean { private fun isDamageAuthoritative(target: AbstractEntity): Boolean {
// Damage manager is authoritative if either one of the entities is // Damage manager is authoritative if either one of the entities is
// masterOnly, OR the manager is server-side and both entities are // masterOnly, OR the manager is server-side and both entities are
@ -371,7 +382,9 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
open fun damagedOther(notification: DamageNotificationPacket) {} open fun damagedOther(notification: DamageNotificationPacket) {}
open fun tick(delta: Double) { open fun tick(delta: Double) {
mailbox.executeQueuedTasks() while (scheduledTasks.isNotEmpty() && scheduledTasks.peek().isDone) {
scheduledTasks.poll()
}
if (networkGroup.upstream.isInterpolating) { if (networkGroup.upstream.isInterpolating) {
networkGroup.upstream.tickInterpolation(delta) networkGroup.upstream.tickInterpolation(delta)
@ -467,30 +480,26 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
return null return null
} }
// doesn't write stacktrace fun tryHandleMessage(sourceConnection: Int, message: String, arguments: JsonArray): CompletableFuture<JsonElement> {
class MessageCallException(message: String) : RuntimeException(message, null, true, false) val response = try {
handleMessage(sourceConnection, message, arguments)
} catch (err: Throwable) {
return CompletableFuture.failedFuture(err)
}
if (response == null) {
return CompletableFuture.failedFuture(World.MessageCallException("Message '$message' was not handled"))
} else {
return CompletableFuture.completedFuture(response)
}
}
fun dispatchMessage(sourceConnection: Int, message: String, arguments: JsonArray): CompletableFuture<JsonElement> { fun dispatchMessage(sourceConnection: Int, message: String, arguments: JsonArray): CompletableFuture<JsonElement> {
if (isRemote) { if (!isRemote) {
val connection = world.remote(connectionID) ?: return CompletableFuture.failedFuture(NoSuchElementException("Can't dispatch entity message, no such connection $connectionID")) return tryHandleMessage(sourceConnection, message, arguments)
val future = CompletableFuture<JsonElement>()
val uuid = UUID(world.random.nextLong(), world.random.nextLong())
world.pendingEntityMessages.put(uuid, future)
connection.send(EntityMessagePacket(Either.left(entityID), message, arguments, uuid, sourceConnection))
return future
} else {
val response = try {
handleMessage(sourceConnection, message, arguments)
} catch (err: Throwable) {
return CompletableFuture.failedFuture(err)
}
if (response == null) {
return CompletableFuture.failedFuture(MessageCallException("Message '$message' was not handled"))
} else {
return CompletableFuture.completedFuture(response)
}
} }
return world.dispatchEntityMessage(sourceConnection, entityID, message, arguments)
} }
open fun render(client: StarboundClient, layers: LayeredRenderer) { open fun render(client: StarboundClient, layers: LayeredRenderer) {

View File

@ -0,0 +1,14 @@
package ru.dbotthepony.kstarbound.world.entities.api
import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest
// sigh
interface InteractiveEntity {
val isInteractive: Boolean
get() = false
fun interact(request: InteractRequest): InteractAction {
return InteractAction.NONE
}
}

View File

@ -0,0 +1,4 @@
package ru.dbotthepony.kstarbound.world.entities.api
interface LoungeableEntity {
}

View File

@ -5,7 +5,6 @@ import com.google.gson.JsonElement
import com.google.gson.JsonNull import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.ints.IntIterators
import it.unimi.dsi.fastutil.ints.IntLists import it.unimi.dsi.fastutil.ints.IntLists
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
@ -26,6 +25,8 @@ import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.item.IContainer import ru.dbotthepony.kstarbound.item.IContainer
import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.math.Interpolator import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
@ -38,10 +39,11 @@ import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.concurrent.CompletableFuture
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(config) { class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(config) {
var opened by networkedSignedInt().also { networkGroup.upstream.add(it) } var openFrameIndex by networkedSignedInt().also { networkGroup.upstream.add(it) }
var isCrafting by networkedBoolean().also { networkGroup.upstream.add(it) } var isCrafting by networkedBoolean().also { networkGroup.upstream.add(it) }
var craftingProgress by networkedFloat().also { networkGroup.upstream.add(it); it.interpolator = Interpolator.Linear } var craftingProgress by networkedFloat().also { networkGroup.upstream.add(it); it.interpolator = Interpolator.Linear }
val items = Container(lookupProperty("slotCount").asInt).also { networkGroup.upstream.add(it) } val items = Container(lookupProperty("slotCount").asInt).also { networkGroup.upstream.add(it) }
@ -65,6 +67,25 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
lookupProperty("itemAgeMultiplier") { JsonPrimitive(1.0) }.asDouble lookupProperty("itemAgeMultiplier") { JsonPrimitive(1.0) }.asDouble
}.also { parametersLazies.add(it) } }.also { parametersLazies.add(it) }
private var openCount = 0
fun openContainer() {
if (openCount++ == 0) {
openFrameIndex = lookupProperty("openFrameIndex") { JsonPrimitive(2) }.asInt
scheduleInTicks(lookupProperty("autoCloseCooldown").asInt) {
closeContainer()
}
}
}
fun closeContainer() {
if (--openCount <= 0) {
openCount = 0
openFrameIndex = 0
}
}
override fun tick(delta: Double) { override fun tick(delta: Double) {
super.tick(delta) super.tick(delta)
@ -106,7 +127,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
override fun deserialize(data: JsonObject) { override fun deserialize(data: JsonObject) {
super.deserialize(data) super.deserialize(data)
opened = data.get("opened", 0) openFrameIndex = data.get("opened", 0)
isCrafting = data.get("crafting", false) isCrafting = data.get("crafting", false)
craftingProgress = data.get("craftingProgress", 0.0) craftingProgress = data.get("craftingProgress", 0.0)
isInitialized = data.get("initialized", true) isInitialized = data.get("initialized", true)
@ -118,7 +139,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
// required by original engine // required by original engine
data["currentState"] = 0 data["currentState"] = 0
data["opened"] = opened data["opened"] = openFrameIndex
data["crafting"] = isCrafting data["crafting"] = isCrafting
data["craftingProgress"] = craftingProgress data["craftingProgress"] = craftingProgress
data["initialized"] = isInitialized data["initialized"] = isInitialized
@ -150,7 +171,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
LOGGER.error("Unknown treasure pool $get! Can't generate container contents at $tilePosition.") LOGGER.error("Unknown treasure pool $get! Can't generate container contents at $tilePosition.")
} else { } else {
for (item in treasurePool.value.evaluate(random, level)) { for (item in treasurePool.value.evaluate(random, level)) {
val leftover = items.add(item) val leftover = items.add(item).first
if (leftover.isNotEmpty) { if (leftover.isNotEmpty) {
LOGGER.warn("Tried to overfill container at $tilePosition") LOGGER.warn("Tried to overfill container at $tilePosition")
@ -189,7 +210,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
} }
override fun handleMessage(connection: Int, message: String, arguments: JsonArray): JsonElement? { override fun handleMessage(connection: Int, message: String, arguments: JsonArray): JsonElement? {
return when (message.lowercase()) { return when (message.lowercase()) { // because legacy protocol allows it
"startcrafting" -> { "startcrafting" -> {
startCrafting() startCrafting()
JsonNull.INSTANCE JsonNull.INSTANCE
@ -206,12 +227,14 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
} }
// returns not inserted items // returns not inserted items
"additems" -> items.add(ItemDescriptor(arguments[0]).build()).toJson() "additems" -> items.add(ItemDescriptor(arguments[0]).build()).first.toJson()
"putitems" -> items.put(IntLists.singleton(arguments[0].asInt), ItemDescriptor(arguments[1]).build()).toJson() "stackitems" -> items.stackWithExisting(ItemDescriptor(arguments[0]).build()).first.toJson()
"putitems" -> items.put(IntLists.singleton(arguments[0].asInt), ItemDescriptor(arguments[1]).build()).first.toJson()
"takeitems" -> items.take(arguments[0].asInt, arguments[1].asLong).toJson() "takeitems" -> items.take(arguments[0].asInt, arguments[1].asLong).toJson()
"swapitems" -> items.swap(arguments[0].asInt, ItemDescriptor(arguments[1]).build(), if (arguments.size() >= 3) arguments[2].asBoolean else true).toJson() "swapitems" -> items.swap(arguments[0].asInt, ItemDescriptor(arguments[1]).build(), if (arguments.size() >= 3) arguments[2].asBoolean else true).toJson()
"combineitems" -> items.combineAt(arguments[0].asInt, ItemDescriptor(arguments[1]).build()).toJson()
"applyaugment" -> TODO("applyaugment") "applyaugment" -> TODO("applyaugment")
"consumeitems" -> JsonPrimitive(items.take(ItemDescriptor(arguments[0]))) "consumeitems" -> JsonPrimitive(items.take(ItemDescriptor(arguments[0]), exactMatch = arguments.get(1, false)))
"consumeitemsat" -> JsonPrimitive(items.takeExact(arguments[0].asInt, arguments[1].asLong)) "consumeitemsat" -> JsonPrimitive(items.takeExact(arguments[0].asInt, arguments[1].asLong))
"clearcontainer" -> { "clearcontainer" -> {
val result = JsonArray() val result = JsonArray()
@ -230,6 +253,88 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
} }
} }
fun addItems(item: ItemStack): CompletableFuture<ItemStack> {
if (isRemote) {
return dispatchMessage(world.connectionID, "addItems", jsonArrayOf(item.toJson())).thenApply { ItemDescriptor(it).build() }
} else {
return CompletableFuture.completedFuture(items.add(item).first)
}
}
fun putItems(slot: Int, item: ItemStack): CompletableFuture<ItemStack> {
if (isRemote) {
return dispatchMessage(world.connectionID, "putItems", jsonArrayOf(item.toJson(), slot)).thenApply { ItemDescriptor(it).build() }
} else {
return CompletableFuture.completedFuture(items.put(IntLists.singleton(slot), item).first)
}
}
fun stackWithExisting(item: ItemStack): CompletableFuture<ItemStack> {
if (isRemote) {
if (world.remote(connectionID)!!.isLegacy) {
return dispatchMessage(world.connectionID, "addItems", jsonArrayOf(item.toJson())).thenApply { ItemDescriptor(it).build() }
} else {
return dispatchMessage(world.connectionID, "stackItems", jsonArrayOf(item.toJson())).thenApply { ItemDescriptor(it).build() }
}
} else {
return CompletableFuture.completedFuture(items.stackWithExisting(item).first)
}
}
fun swapItems(slot: Int, item: ItemStack, tryCombine: Boolean = true): CompletableFuture<ItemStack> {
if (isRemote) {
return dispatchMessage(world.connectionID, "swapItems", jsonArrayOf(slot, item.toJson(), tryCombine)).thenApply { ItemDescriptor(it).build() }
} else {
return CompletableFuture.completedFuture(items.swap(slot, item, tryCombine))
}
}
fun combineItems(slot: Int, item: ItemStack): CompletableFuture<ItemStack> {
if (isRemote) {
if (world.remote(connectionID)!!.isLegacy) {
if (items[slot].isEmpty || !items[slot].isStackable(item)) {
return CompletableFuture.completedFuture(item)
} else {
return dispatchMessage(world.connectionID, "putItems", jsonArrayOf(slot, item.toJson()))
.thenApply { ItemDescriptor(it).build() }
}
} else {
return dispatchMessage(world.connectionID, "combineItems", jsonArrayOf(slot, item.toJson()))
.thenApply { ItemDescriptor(it).build() }
}
} else {
return CompletableFuture.completedFuture(items.combineAt(slot, item))
}
}
fun takeItem(item: ItemDescriptor, exact: Boolean = false): CompletableFuture<Boolean> {
if (isRemote) {
return dispatchMessage(world.connectionID, "consumeItems", jsonArrayOf(item.toJson(), exact)).thenApply { it.asBoolean }
} else {
return CompletableFuture.completedFuture(items.take(item, exactMatch = exact))
}
}
fun takeItemAt(index: Int, amount: Long = Long.MAX_VALUE): CompletableFuture<Boolean> {
if (isRemote) {
return dispatchMessage(world.connectionID, "consumeItemsAt", jsonArrayOf(index, amount)).thenApply { it.asBoolean }
} else {
return CompletableFuture.completedFuture(items.takeExact(index, amount))
}
}
fun clearContainer(): CompletableFuture<List<ItemStack>> {
if (isRemote) {
return dispatchMessage(world.connectionID, "clearContainer", JsonArray()).thenApply {
(it as JsonArray).stream().map { ItemDescriptor(it).build() }.filter { it.isNotEmpty }.toList()
}
} else {
val future = CompletableFuture.completedFuture(items.filter { it.isNotEmpty })
items.clear()
return future
}
}
private fun startCrafting() { private fun startCrafting() {
} }

View File

@ -20,9 +20,10 @@ import ru.dbotthepony.kstarbound.util.coalesceNull
import ru.dbotthepony.kstarbound.util.valueOf import ru.dbotthepony.kstarbound.util.valueOf
import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.world.entities.api.LoungeableEntity
import java.lang.Math.toRadians import java.lang.Math.toRadians
class LoungeableObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(config) { class LoungeableObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(config), LoungeableEntity {
init { init {
isInteractive = true isInteractive = true
} }

View File

@ -90,6 +90,7 @@ import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.Animator import ru.dbotthepony.kstarbound.world.entities.Animator
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.api.InteractiveEntity
import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataOutputStream import java.io.DataOutputStream
@ -98,7 +99,7 @@ import java.util.HashMap
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import kotlin.math.min import kotlin.math.min
open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity(), ScriptedEntity { open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity(), ScriptedEntity, InteractiveEntity {
override fun deserialize(data: JsonObject) { override fun deserialize(data: JsonObject) {
super.deserialize(data) super.deserialize(data)
direction = data.get("direction", directions) { Direction.LEFT } direction = data.get("direction", directions) { Direction.LEFT }
@ -142,7 +143,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
data["parameters"] = JsonObject().also { data["parameters"] = JsonObject().also {
for ((k, v) in parameters) { for ((k, v) in parameters) {
it[k] = v.deepCopy() it[k] = v
} }
} }
} }
@ -203,7 +204,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
networkGroup.upstream.add(uniqueID) networkGroup.upstream.add(uniqueID)
} }
var isInteractive by networkedBoolean().also { networkGroup.upstream.add(it) } final override var isInteractive by networkedBoolean().also { networkGroup.upstream.add(it) }
val declaredMaterialSpaces = NetworkedList(materialSpacesCodec, materialSpacesCodecLegacy).also { networkGroup.upstream.add(it) } val declaredMaterialSpaces = NetworkedList(materialSpacesCodec, materialSpacesCodecLegacy).also { networkGroup.upstream.add(it) }
private val materialSpaces0 = ManualLazy { private val materialSpaces0 = ManualLazy {