diff --git a/ADDITIONS.md b/ADDITIONS.md index b6dcbf7e..2153faea 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -129,6 +129,15 @@ val color: TileColor = TileColor.DEFAULT * Added `world.objectLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List` * Added `world.loungeableLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List` * `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 --------------- diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ContainerIterator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ContainerIterator.kt new file mode 100644 index 00000000..ac06787b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ContainerIterator.kt @@ -0,0 +1,13 @@ +package ru.dbotthepony.kstarbound.item + +class ContainerIterator(private val container: IContainer) : Iterator { + private var index = 0 + + override fun hasNext(): Boolean { + return index < container.size + } + + override fun next(): ItemStack { + return container[index++] + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt index e79a59da..d9e8b3dc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt @@ -6,12 +6,13 @@ import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.ints.IntIterable import it.unimi.dsi.fastutil.ints.IntIterator import it.unimi.dsi.fastutil.ints.IntIterators +import it.unimi.dsi.fastutil.ints.IntList import it.unimi.dsi.fastutil.ints.IntLists import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import java.util.random.RandomGenerator -interface IContainer { +interface IContainer : Iterable { var size: Int operator fun get(index: Int): 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 { 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 { + 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 { 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 { val copy = item.copy() - + val touched = IntArrayList() var itr = slots.invoke() while (itr.hasNext()) { @@ -139,6 +161,7 @@ interface IContainer { val itemThere = this[slot] if (itemThere.isStackable(copy)) { + touched.add(slot) val newCount = (itemThere.size + copy.size).coerceAtMost(itemThere.maxStackSize) val diff = newCount - itemThere.size @@ -148,7 +171,7 @@ interface IContainer { itemThere.size += diff if (copy.isEmpty) - return ItemStack.EMPTY + return ItemStack.EMPTY to touched } } @@ -163,23 +186,23 @@ interface IContainer { val itemThere = this[slot] if (itemThere.isEmpty) { - if (itemThere.isEmpty) { - if (copy.size > copy.maxStackSize) { - if (!simulate) - this[slot] = copy.copy(copy.maxStackSize) + touched.add(slot) - copy.size -= copy.maxStackSize - } else { - if (!simulate) - this[slot] = copy + if (copy.size > copy.maxStackSize) { + if (!simulate) + this[slot] = copy.copy(copy.maxStackSize) - 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 { @@ -233,20 +256,20 @@ interface IContainer { } else if (item.isNotEmpty && existingItem.isEmpty) { // place into slot // 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 { // If something is there, try to stack with it first. If we can't stack, // then swap. if (tryCombine && existingItem.isStackable(item)) { - return put(IntLists.singleton(slot), item) + return put(IntLists.singleton(slot), item).first } else { this[slot] = ItemStack.EMPTY val slots = IntArrayList(IntIterators.fromTo(0, size)) slots.removeInt(slot) slots.add(0, slot) - val remaining = put(slots, item) + val remaining = put(slots, item).first if (remaining.isNotEmpty && this[slot].isStackable(remaining)) { // 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 { var toTake = descriptor.count @@ -387,7 +418,7 @@ interface IContainer { val lost = ArrayList() for (i in size until read.size) { - val remaining = add(read[i]) + val remaining = add(read[i]).first if (remaining.isNotEmpty) { lost.add(remaining) @@ -408,4 +439,8 @@ interface IContainer { return emptyList() } } + + override fun iterator(): Iterator { + return ContainerIterator(this) + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt index 11ef5548..5ddbf4ba 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt @@ -41,6 +41,10 @@ object ItemRegistry { val isNotEmpty: Boolean get() = !isEmpty + // for Lua scripts + val nameOrNull: String? + get() = if (isEmpty) null else name + val directory = file?.computeDirectory() ?: "/" } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt index 3f29c212..9bf22988 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt @@ -366,6 +366,14 @@ open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, para return createDescriptor().toJson() } + fun toTable(allocator: TableFactory): Table? { + if (isEmpty) { + return null + } + + return createDescriptor().toTable(allocator) + } + class Adapter(gson: Gson) : TypeAdapter() { override fun write(out: JsonWriter, value: ItemStack?) { val json = value?.toJson() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt index c5715008..98ef4b47 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt @@ -156,6 +156,26 @@ fun TableFactory.tableOf(vararg values: Any?): 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): Table { val table = newTable(0, values.size) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt index c8e4fc92..c1c93ad4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt @@ -4,6 +4,7 @@ import org.classdump.luna.ByteString import org.classdump.luna.LuaRuntimeException import org.classdump.luna.Table import org.classdump.luna.runtime.ExecutionContext +import ru.dbotthepony.kommons.gson.JsonArrayCollector import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.EntityType 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.iterator import ru.dbotthepony.kstarbound.lua.luaFunction +import ru.dbotthepony.kstarbound.lua.luaFunctionN import ru.dbotthepony.kstarbound.lua.luaStub import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.lua.tableMapOf import ru.dbotthepony.kstarbound.lua.tableOf import ru.dbotthepony.kstarbound.lua.toAABB +import ru.dbotthepony.kstarbound.lua.toJsonFromLua import ru.dbotthepony.kstarbound.lua.toLine2d import ru.dbotthepony.kstarbound.lua.toPoly import ru.dbotthepony.kstarbound.lua.toVector2d import ru.dbotthepony.kstarbound.lua.toVector2i import ru.dbotthepony.kstarbound.lua.unpackAsArray +import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture import ru.dbotthepony.kstarbound.math.AABB 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.valueOf 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.ItemDropEntity 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.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.WorldObject 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())) } + + 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") } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaFuture.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaFuture.kt new file mode 100644 index 00000000..5e9ed5a1 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaFuture.kt @@ -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, val isLocal: Boolean) : Userdata>() { + override fun getMetatable(): Table { + return metadata + } + + override fun setMetatable(mt: Table?): Table { + throw UnsupportedOperationException() + } + + override fun getUserValue(): CompletableFuture { + return future + } + + override fun setUserValue(value: CompletableFuture?): CompletableFuture { + 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() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessagePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessagePacket.kt index 53ab7778..48216d4e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessagePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessagePacket.kt @@ -45,24 +45,20 @@ class EntityMessagePacket(val entity: Either, val message: String, } private fun handle(connection: Connection, world: World<*, *>) { - val entity = if (entity.isLeft) { - world.entities[entity.left()] + val future = if (entity.isLeft) { + world.dispatchEntityMessage(connection.connectionID, entity.left(), message, arguments) } else { - world.entities.values.firstOrNull { it.uniqueID.get() == entity.right() } + world.dispatchEntityMessage(connection.connectionID, entity.right(), message, arguments) } - if (entity == null) { - connection.send(EntityMessageResponsePacket(Either.left("No such entity ${this@EntityMessagePacket.entity}"), id)) - } else { - entity.dispatchMessage(connection.connectionID, message, arguments) - .thenAccept(Consumer { - connection.send(EntityMessageResponsePacket(Either.right(it), id)) - }) - .exceptionally(Function { - connection.send(EntityMessageResponsePacket(Either.left(it.message ?: "Internal server error"), id)) - null - }) - } + future + .thenAccept(Consumer { + connection.send(EntityMessageResponsePacket(Either.right(it), id)) + }) + .exceptionally(Function { + connection.send(EntityMessageResponsePacket(Either.left(it.message ?: "Internal server error"), id)) + null + }) } override fun play(connection: ServerConnection) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessageResponsePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessageResponsePacket.kt index aaa56950..9a43c130 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessageResponsePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessageResponsePacket.kt @@ -14,6 +14,7 @@ import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.server.ServerConnection +import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import java.io.DataInputStream import java.io.DataOutputStream @@ -36,7 +37,7 @@ class EntityMessageResponsePacket(val response: Either, val if (message != null) { if (response.isLeft) { - message.completeExceptionally(AbstractEntity.MessageCallException(response.left())) + message.completeExceptionally(World.MessageCallException(response.left())) } else { message.complete(response.right()) } @@ -50,7 +51,7 @@ class EntityMessageResponsePacket(val response: Either, val if (message != null) { if (response.isLeft) { - message.completeExceptionally(AbstractEntity.MessageCallException(response.left())) + message.completeExceptionally(World.MessageCallException(response.left())) } else { message.complete(response.right()) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/EntityInteractPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/EntityInteractPacket.kt index 52478263..e6d1ff35 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/EntityInteractPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/EntityInteractPacket.kt @@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.EntityInteractResultPacket import ru.dbotthepony.kstarbound.server.ServerConnection +import ru.dbotthepony.kstarbound.world.entities.api.InteractiveEntity import java.io.DataInputStream import java.io.DataOutputStream import java.util.UUID @@ -28,14 +29,13 @@ class EntityInteractPacket(val request: InteractRequest, val id: UUID) : IServer override fun play(connection: ServerConnection) { if (request.target >= 0) { 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 { 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) { - throw IllegalStateException("Attempt to interact with own entity through server?") - } + if (other == connection) + throw IllegalArgumentException("Attempt to interact with own entity through server?") other.send(this) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt deleted file mode 100644 index 3a444f91..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt +++ /dev/null @@ -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 > LinkedList.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>() - - private val timers = LinkedList>() - private val repeatableTimers = LinkedList() - - @Volatile - private var isShutdown = false - @Volatile - private var isTerminated = false - - private val timeOrigin = JVMClock() - - var exceptionHandler: Consumer? = null - - private inner class Timer(task: Callable, val executeAt: Long) : FutureTask(task), ScheduledFuture { - 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(private val value: T) : Future { - 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({ task.run() }), ScheduledFuture { - 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() - - 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 { - if (isTerminated) return listOf() - isShutdown = true - isTerminated = true - - val result = ArrayList() - - 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 submit(task: Callable): Future { - 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 submit(task: Runnable, result: T): Future { - 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 invokeAll(tasks: Collection>): List> { - 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 invokeAll( - tasks: Collection>, - timeout: Long, - unit: TimeUnit - ): List> { - 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 invokeAny(tasks: Collection>): 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 invokeAny(tasks: Collection>, 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 join(future: Future): 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 schedule(callable: Callable, delay: Long, unit: TimeUnit): ScheduledFuture { - 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) - } - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 10cd5f5c..e17b3a6d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Caffeine +import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonNull 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.collect.filterNotNull import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2i 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.packets.DamageNotificationPacket 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.clientbound.SetPlayerStartPacket import ru.dbotthepony.kstarbound.util.BlockableEventLoop @@ -582,6 +585,29 @@ abstract class World, ChunkType : Chunk { + 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() + 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 { + TODO() + } + // 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 val pendingEntityMessages: Cache> = Caffeine.newBuilder() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt index 80631a95..779b0c1b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -9,6 +9,7 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.io.nullable import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.vector.Vector2d 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.networkedData import ru.dbotthepony.kstarbound.server.world.ServerWorld -import ru.dbotthepony.kstarbound.util.MailboxExecutorService import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.EntityIndex 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.physics.Poly import java.io.DataOutputStream +import java.util.PriorityQueue import java.util.UUID import java.util.concurrent.CompletableFuture +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit import java.util.function.Consumer import java.util.function.Predicate @@ -81,9 +84,6 @@ abstract class AbstractEntity : Comparable { LOGGER.error("Error while executing queued task on $this", it) } - var mailbox = MailboxExecutorService().also { it.exceptionHandler = exceptionLogger } - private set - private var innerWorld: World<*, *>? = null val world: World<*, *> @@ -201,9 +201,6 @@ abstract class AbstractEntity : Comparable { check(!world.entities.containsKey(entityID)) { "Duplicate entity ID: $entityID" } - if (mailbox.isShutdown) - mailbox = MailboxExecutorService().also { it.exceptionHandler = exceptionLogger } - innerWorld = world world.entities[entityID] = this world.entityList.add(this) @@ -227,7 +224,9 @@ abstract class AbstractEntity : Comparable { 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!" } world.entityList.remove(this) @@ -255,12 +254,24 @@ abstract class AbstractEntity : Comparable { } } - open fun interact(request: InteractRequest): InteractAction { - return InteractAction.NONE + // for fast check on completed tasks + // 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>() + + 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 + open val mouthPosition: Vector2d + get() = position + private fun isDamageAuthoritative(target: AbstractEntity): Boolean { // Damage manager is authoritative if either one of the entities is // masterOnly, OR the manager is server-side and both entities are @@ -371,7 +382,9 @@ abstract class AbstractEntity : Comparable { open fun damagedOther(notification: DamageNotificationPacket) {} open fun tick(delta: Double) { - mailbox.executeQueuedTasks() + while (scheduledTasks.isNotEmpty() && scheduledTasks.peek().isDone) { + scheduledTasks.poll() + } if (networkGroup.upstream.isInterpolating) { networkGroup.upstream.tickInterpolation(delta) @@ -467,30 +480,26 @@ abstract class AbstractEntity : Comparable { return null } - // doesn't write stacktrace - class MessageCallException(message: String) : RuntimeException(message, null, true, false) + fun tryHandleMessage(sourceConnection: Int, message: String, arguments: JsonArray): CompletableFuture { + 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 { - if (isRemote) { - val connection = world.remote(connectionID) ?: return CompletableFuture.failedFuture(NoSuchElementException("Can't dispatch entity message, no such connection $connectionID")) - val future = CompletableFuture() - 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) - } + if (!isRemote) { + return tryHandleMessage(sourceConnection, message, arguments) } + + return world.dispatchEntityMessage(sourceConnection, entityID, message, arguments) } open fun render(client: StarboundClient, layers: LayeredRenderer) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/InteractiveEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/InteractiveEntity.kt new file mode 100644 index 00000000..0dfc5dcc --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/InteractiveEntity.kt @@ -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 + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/LoungeableEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/LoungeableEntity.kt new file mode 100644 index 00000000..57140e96 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/LoungeableEntity.kt @@ -0,0 +1,4 @@ +package ru.dbotthepony.kstarbound.world.entities.api + +interface LoungeableEntity { +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt index 8a81035b..3e912870 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt @@ -5,7 +5,6 @@ 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.ints.IntIterators import it.unimi.dsi.fastutil.ints.IntLists import it.unimi.dsi.fastutil.io.FastByteArrayInputStream 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.item.IContainer 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.network.syncher.NetworkedElement 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 java.io.DataInputStream import java.io.DataOutputStream +import java.util.concurrent.CompletableFuture import java.util.random.RandomGenerator class ContainerObject(config: Registry.Entry) : 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 craftingProgress by networkedFloat().also { networkGroup.upstream.add(it); it.interpolator = Interpolator.Linear } val items = Container(lookupProperty("slotCount").asInt).also { networkGroup.upstream.add(it) } @@ -65,6 +67,25 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co lookupProperty("itemAgeMultiplier") { JsonPrimitive(1.0) }.asDouble }.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) { super.tick(delta) @@ -106,7 +127,7 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co override fun deserialize(data: JsonObject) { super.deserialize(data) - opened = data.get("opened", 0) + openFrameIndex = data.get("opened", 0) isCrafting = data.get("crafting", false) craftingProgress = data.get("craftingProgress", 0.0) isInitialized = data.get("initialized", true) @@ -118,7 +139,7 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co // required by original engine data["currentState"] = 0 - data["opened"] = opened + data["opened"] = openFrameIndex data["crafting"] = isCrafting data["craftingProgress"] = craftingProgress data["initialized"] = isInitialized @@ -150,7 +171,7 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co LOGGER.error("Unknown treasure pool $get! Can't generate container contents at $tilePosition.") } else { for (item in treasurePool.value.evaluate(random, level)) { - val leftover = items.add(item) + val leftover = items.add(item).first if (leftover.isNotEmpty) { LOGGER.warn("Tried to overfill container at $tilePosition") @@ -189,7 +210,7 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co } 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() JsonNull.INSTANCE @@ -206,12 +227,14 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co } // returns not inserted items - "additems" -> items.add(ItemDescriptor(arguments[0]).build()).toJson() - "putitems" -> items.put(IntLists.singleton(arguments[0].asInt), ItemDescriptor(arguments[1]).build()).toJson() + "additems" -> items.add(ItemDescriptor(arguments[0]).build()).first.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() "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") - "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)) "clearcontainer" -> { val result = JsonArray() @@ -230,6 +253,88 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co } } + fun addItems(item: ItemStack): CompletableFuture { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + if (isRemote) { + return dispatchMessage(world.connectionID, "consumeItemsAt", jsonArrayOf(index, amount)).thenApply { it.asBoolean } + } else { + return CompletableFuture.completedFuture(items.takeExact(index, amount)) + } + } + + fun clearContainer(): CompletableFuture> { + 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() { } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/LoungeableObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/LoungeableObject.kt index fba096dd..e0d0501e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/LoungeableObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/LoungeableObject.kt @@ -20,9 +20,10 @@ import ru.dbotthepony.kstarbound.util.coalesceNull import ru.dbotthepony.kstarbound.util.valueOf import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT +import ru.dbotthepony.kstarbound.world.entities.api.LoungeableEntity import java.lang.Math.toRadians -class LoungeableObject(config: Registry.Entry) : WorldObject(config) { +class LoungeableObject(config: Registry.Entry) : WorldObject(config), LoungeableEntity { init { isInteractive = true } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt index 73e70ceb..83196634 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt @@ -90,6 +90,7 @@ import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.Animator 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.physics.Poly import java.io.DataOutputStream @@ -98,7 +99,7 @@ import java.util.HashMap import java.util.random.RandomGenerator import kotlin.math.min -open class WorldObject(val config: Registry.Entry) : TileEntity(), ScriptedEntity { +open class WorldObject(val config: Registry.Entry) : TileEntity(), ScriptedEntity, InteractiveEntity { override fun deserialize(data: JsonObject) { super.deserialize(data) direction = data.get("direction", directions) { Direction.LEFT } @@ -142,7 +143,7 @@ open class WorldObject(val config: Registry.Entry) : TileEntit data["parameters"] = JsonObject().also { for ((k, v) in parameters) { - it[k] = v.deepCopy() + it[k] = v } } } @@ -203,7 +204,7 @@ open class WorldObject(val config: Registry.Entry) : TileEntit 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) } private val materialSpaces0 = ManualLazy {