From 26c579f5683db99847a4706ea4b81f4490106c44 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Mon, 29 Apr 2024 13:50:59 +0700 Subject: [PATCH] Entity message handling --- .../kstarbound/client/world/ClientWorld.kt | 7 + .../dbotthepony/kstarbound/collect/IdMap.kt | 8 + .../kstarbound/defs/item/ItemDescriptor.kt | 7 +- .../dbotthepony/kstarbound/item/IContainer.kt | 207 ++++++++++++++++-- .../dbotthepony/kstarbound/item/ItemStack.kt | 4 +- .../kstarbound/network/Connection.kt | 2 + .../kstarbound/network/PacketRegistry.kt | 6 +- .../network/packets/EntityMessagePacket.kt | 83 +++++++ .../packets/EntityMessageResponsePacket.kt | 60 +++++ .../kstarbound/server/world/ServerWorld.kt | 9 +- .../ru/dbotthepony/kstarbound/world/World.kt | 19 ++ .../world/entities/AbstractEntity.kt | 46 +++- .../world/entities/tile/ContainerObject.kt | 49 ++++- 13 files changed, 473 insertions(+), 34 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessagePacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessageResponsePacket.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt index c1aed984..8a1e965b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -22,6 +22,7 @@ import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity +import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket @@ -339,6 +340,12 @@ class ClientWorld( client.activeConnection?.send(data) } + override fun remote(connectionID: Int): Connection? { + // connectionID might be of different client (and not of server's), + // but we always must send messages to server + return client.activeConnection + } + companion object { val ring = listOf( Vector2i(0, 0), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/collect/IdMap.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/collect/IdMap.kt index 0b354215..820cc0d8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/collect/IdMap.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/collect/IdMap.kt @@ -45,6 +45,14 @@ class IdMap(val min: Int = 0, val max: Int = Int.MAX_VALUE, private val return index } + override fun get(key: Int): T? { + if (map is Int2ObjectMap) { + return (map as Int2ObjectMap).get(key) + } else { + return map[key] + } + } + override fun clear() { nextIndex = min - 1 map.clear() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt index eb492f5d..1b7ef06d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt @@ -180,13 +180,12 @@ data class ItemDescriptor( it.add("parameters", parameters.deepCopy()) } - fun toJson(): JsonObject? { + fun toJson(): JsonElement { if (Starbound.IS_STORE_JSON) { return VersionRegistry.make("Item", toJsonStruct()).toJson() } else { - if (isEmpty) { - return null - } + if (isEmpty) + return JsonNull.INSTANCE return toJsonStruct() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt index a73dd3aa..e79a59da 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt @@ -3,6 +3,10 @@ package ru.dbotthepony.kstarbound.item import com.google.gson.JsonArray import com.google.gson.JsonElement 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.IntLists import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import java.util.random.RandomGenerator @@ -11,6 +15,7 @@ interface IContainer { var size: Int operator fun get(index: Int): ItemStack operator fun set(index: Int, value: ItemStack) + fun clear() fun ageItems(by: Double): Boolean { var any = false @@ -109,13 +114,29 @@ interface IContainer { } } - // puts item into container, returns remaining not put items + // returns not inserted items fun add(item: ItemStack, simulate: Boolean = false): ItemStack { + return put({ IntIterators.fromTo(0, size) }, item, simulate) + } + + // returns not inserted items + fun put(slots: IntIterable, item: ItemStack, simulate: Boolean = false): ItemStack { + return put(slots::iterator, item, simulate) + } + + // returns not inserted items + fun put(slots: () -> IntIterator, item: ItemStack, simulate: Boolean = false): ItemStack { val copy = item.copy() - // first, try to put into not empty slots first - for (i in 0 until size) { - val itemThere = this[i] + var itr = slots.invoke() + + while (itr.hasNext()) { + val slot = itr.nextInt() + + if (slot !in 0 until size) + continue + + val itemThere = this[slot] if (itemThere.isStackable(copy)) { val newCount = (itemThere.size + copy.size).coerceAtMost(itemThere.maxStackSize) @@ -131,21 +152,29 @@ interface IContainer { } } - // then try to move into empty slots - for (i in 0 until size) { - val itemThere = this[i] + itr = slots.invoke() + + while (itr.hasNext()) { + val slot = itr.nextInt() + + if (slot !in 0 until size) + continue + + val itemThere = this[slot] if (itemThere.isEmpty) { - if (copy.size > copy.maxStackSize) { - if (!simulate) - this[i] = copy.copy(copy.maxStackSize) + if (itemThere.isEmpty) { + if (copy.size > copy.maxStackSize) { + if (!simulate) + this[slot] = copy.copy(copy.maxStackSize) - copy.size -= copy.maxStackSize - } else { - if (!simulate) - this[i] = copy + copy.size -= copy.maxStackSize + } else { + if (!simulate) + this[slot] = copy - return ItemStack.EMPTY + return ItemStack.EMPTY + } } } } @@ -153,7 +182,153 @@ interface IContainer { return copy } - fun clear() + fun take(slot: Int, amount: Long): ItemStack { + if (slot !in 0 until size || amount <= 0L) + return ItemStack.EMPTY + + val item = this[slot] + + if (item.isEmpty) + return item + + if (item.size > amount) { + val copy = item.copy(amount) + item.shrink(amount) + return copy + } else { + this[slot] = ItemStack.EMPTY + return item + } + } + + fun takeExact(slot: Int, amount: Long): Boolean { + if (amount <= 0L) + return true + + if (slot !in 0 until size) + return false + + val item = this[slot] + + if (item.isEmpty) + return false + + if (item.size > amount) { + item.shrink(amount) + return true + } else { + return false + } + } + + fun swap(slot: Int, item: ItemStack, tryCombine: Boolean = true): ItemStack { + val existingItem = this[slot] + + if (item.isEmpty && existingItem.isEmpty) { + return ItemStack.EMPTY + } else if (item.isEmpty && existingItem.isNotEmpty) { + // If we are passed in nothing, simply return what's there, if anything. + this[slot] = ItemStack.EMPTY + return existingItem + } 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) + } 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) + } else { + this[slot] = ItemStack.EMPTY + val slots = IntArrayList(IntIterators.fromTo(0, size)) + slots.removeInt(slot) + slots.add(0, slot) + + val remaining = put(slots, item) + + if (remaining.isNotEmpty && this[slot].isStackable(remaining)) { + // damn + this[slot].grow(remaining.size) + } + + return existingItem + } + } + } + + fun take(descriptor: ItemDescriptor, exactMatch: Boolean = false, simulate: Boolean = false): Boolean { + var toTake = descriptor.count + + if (toTake <= 0L) + return true + + val extractSlots = IntArrayList() + + for (slot in 0 until size) { + val item = this[slot] + + if (item.matches(descriptor, exactMatch)) { + toTake -= item.size + if (!simulate) extractSlots.add(slot) + if (toTake <= 0L) break + } + } + + if (simulate || toTake > 0L) + return toTake <= 0L + + toTake = descriptor.count + + for (slot in extractSlots) { + if (this[slot].size <= toTake) { + toTake -= this[slot].size + this[slot] = ItemStack.EMPTY + } else { + this[slot].shrink(toTake) + break + } + } + + return true + } + + fun take(descriptor: ItemStack, exactMatch: Boolean = false, simulate: Boolean = false): Boolean { + var toTake = descriptor.size + + if (toTake <= 0L) + return true + + val extractSlots = IntArrayList() + + for (slot in 0 until size) { + val item = this[slot] + + if (item.matches(descriptor, exactMatch)) { + toTake -= item.size + if (!simulate) extractSlots.add(slot) + if (toTake <= 0L) break + } + } + + if (simulate || toTake > 0L) + return toTake <= 0L + + toTake = descriptor.size + + for (slot in extractSlots) { + if (this[slot].size <= toTake) { + toTake -= this[slot].size + this[slot] = ItemStack.EMPTY + } else { + this[slot].shrink(toTake) + break + } + } + + return true + } fun toJson(store: Boolean = false): JsonArray { val result = JsonArray(size) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt index 52e55192..3f29c212 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt @@ -359,9 +359,9 @@ open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, para return ItemStack(entry, config, parameters.deepCopy(), size) } - fun toJson(): JsonObject? { + fun toJson(): JsonElement { if (isEmpty) - return null + return JsonNull.INSTANCE return createDescriptor().toJson() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index 1f201087..fa69b415 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -245,6 +245,8 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : } companion object { + const val SERVER_CONNECTION_ID = 0 + private val LOGGER = LogManager.getLogger() private val warpActionCodec = StreamCodec.Pair(WarpAction.CODEC, WarpMode.CODEC).koptional() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index 8df368db..b9814528 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -24,6 +24,8 @@ import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket +import ru.dbotthepony.kstarbound.network.packets.EntityMessagePacket +import ru.dbotthepony.kstarbound.network.packets.EntityMessageResponsePacket import ru.dbotthepony.kstarbound.network.packets.EntityUpdateSetPacket import ru.dbotthepony.kstarbound.network.packets.PingPacket import ru.dbotthepony.kstarbound.network.packets.PongPacket @@ -483,8 +485,8 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.add(HitRequestPacket::read) LEGACY.add(DamageRequestPacket::read) LEGACY.add(::DamageNotificationPacket) - LEGACY.skip("EntityMessage") - LEGACY.skip("EntityMessageResponse") + LEGACY.add(::EntityMessagePacket) + LEGACY.add(::EntityMessageResponsePacket) LEGACY.add(::UpdateWorldPropertiesPacket) LEGACY.add(::StepUpdatePacket) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessagePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessagePacket.kt new file mode 100644 index 00000000..5178de79 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessagePacket.kt @@ -0,0 +1,83 @@ +package ru.dbotthepony.kstarbound.network.packets + +import com.google.gson.JsonArray +import ru.dbotthepony.kommons.io.readUUID +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeUUID +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.json.readJsonArray +import ru.dbotthepony.kstarbound.json.writeJsonArray +import ru.dbotthepony.kstarbound.network.Connection +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 java.io.DataInputStream +import java.io.DataOutputStream +import java.util.* +import java.util.function.Consumer +import java.util.function.Function + +class EntityMessagePacket(val entity: Either, val message: String, val arguments: JsonArray, val id: UUID, val sourceConnection: Int) : IClientPacket, IServerPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + if (stream.readBoolean()) Either.right(stream.readInternedString()) else Either.left(stream.readInt()), + stream.readInternedString(), + stream.readJsonArray(), + stream.readUUID(), + stream.readUnsignedShort() + ) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeBoolean(entity.isRight) + + if (entity.isLeft) { + stream.writeInt(entity.left()) + } else { + stream.writeBinaryString(entity.right()) + } + + stream.writeBinaryString(message) + stream.writeJsonArray(arguments) + stream.writeUUID(id) + stream.writeShort(sourceConnection) + } + + private fun handle(connection: Connection, world: World<*, *>) { + if (entity.isLeft) { + val entity = world.entities[entity.left()] + + 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 + }) + } + } else { + TODO("messages to unique entities") + } + } + + override fun play(connection: ServerConnection) { + connection.enqueue { + handle(connection, this) + } + } + + override fun play(connection: ClientConnection) { + connection.enqueue { + val world = world + + if (world != null) { + handle(connection, world) + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessageResponsePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessageResponsePacket.kt new file mode 100644 index 00000000..aaa56950 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessageResponsePacket.kt @@ -0,0 +1,60 @@ +package ru.dbotthepony.kstarbound.network.packets + +import com.google.gson.JsonElement +import ru.dbotthepony.kommons.io.readUUID +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeUUID +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.io.readEither +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.io.writeEither +import ru.dbotthepony.kstarbound.json.readJsonElement +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.entities.AbstractEntity +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.UUID + +class EntityMessageResponsePacket(val response: Either, val id: UUID) : IServerPacket, IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + stream.readEither(isLegacy, { readInternedString() }, { readJsonElement() }), + stream.readUUID()) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + // ah yes, Variant<> in EntityMessagePacket, but Either<> in EntityMessageResponsePacket + stream.writeEither(response, isLegacy, { writeBinaryString(it) }, { writeJsonElement(it) }) + stream.writeUUID(id) + } + + override fun play(connection: ServerConnection) { + connection.enqueue { + val message = pendingEntityMessages.asMap().remove(id) + + if (message != null) { + if (response.isLeft) { + message.completeExceptionally(AbstractEntity.MessageCallException(response.left())) + } else { + message.complete(response.right()) + } + } + } + } + + override fun play(connection: ClientConnection) { + connection.enqueue { + val message = world?.pendingEntityMessages?.asMap()?.remove(this@EntityMessageResponsePacket.id) + + if (message != null) { + if (response.isLeft) { + message.completeExceptionally(AbstractEntity.MessageCallException(response.left())) + } else { + message.complete(response.right()) + } + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index 36ce0e85..f645d95c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -31,6 +31,7 @@ import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.jsonArrayOf +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 @@ -304,7 +305,7 @@ class ServerWorld private constructor( val broken = entity.damageTileEntity(occupySpaces, sourcePosition, actualDamage) if (source != null && broken) { - source.receiveMessage("tileEntityBroken", jsonArrayOf( + source.dispatchMessage(Connection.SERVER_CONNECTION_ID, "tileEntityBroken", jsonArrayOf( damagePositions.firstOrNull { p -> actualPositions.any { it.position == p } } ?: entity.tilePosition, entity.type.jsonName, (entity as? WorldObject)?.config?.key)) @@ -341,7 +342,7 @@ class ServerWorld private constructor( topMost = topMost.coerceAtLeast(result) if (source != null && health?.isDead == true) { - source.receiveMessage("tileBroken", jsonArrayOf( + source.dispatchMessage(Connection.SERVER_CONNECTION_ID, "tileBroken", jsonArrayOf( pos, if (isBackground) "background" else "foreground", tile!!.tile(isBackground).material.id ?: tile.tile(isBackground).material.key, // TODO: explicit string identifiers support tile.dungeonId, @@ -644,6 +645,10 @@ class ServerWorld private constructor( server.channels.connectionByID(data.destinationConnection)?.send(data) } + override fun remote(connectionID: Int): Connection? { + return server.channels.connectionByID(connectionID) + } + @JsonFactory data class MetadataJson( val playerStart: Vector2d, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 6a84a57c..6c5a8e97 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -1,5 +1,7 @@ package ru.dbotthepony.kstarbound.world +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.Caffeine import com.google.gson.JsonElement import com.google.gson.JsonNull import com.google.gson.JsonObject @@ -26,6 +28,7 @@ import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.math.* +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 @@ -43,13 +46,17 @@ import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity import ru.dbotthepony.kstarbound.world.physics.CollisionPoly import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.Poly +import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock import java.util.function.Predicate import java.util.random.RandomGenerator import java.util.stream.Stream +import kotlin.collections.ArrayList import kotlin.math.roundToInt abstract class World, ChunkType : Chunk>(val template: WorldTemplate) : ICellAccess { @@ -573,6 +580,18 @@ abstract class World, ChunkType : Chunk> = Caffeine.newBuilder() + .maximumSize(32000L) // some unreachable value unless there is a malicious actor + .expireAfterWrite(1L, TimeUnit.MINUTES) + .executor(Starbound.EXECUTOR) + .scheduler(Starbound) + .removalListener> { key, value, cause -> if (!cause.wasEvicted()) value?.completeExceptionally(TimeoutException("Did not receive response from remote in time")) } + .build() + companion object { private val LOGGER = LogManager.getLogger() 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 c6b48e3f..80631a95 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -7,11 +7,9 @@ import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import it.unimi.dsi.fastutil.objects.ObjectArrayList import org.apache.logging.log4j.LogManager -import ru.dbotthepony.kommons.io.koptional import ru.dbotthepony.kommons.io.nullable import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.math.AABB -import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.render.LayeredRenderer @@ -30,6 +28,7 @@ import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket +import ru.dbotthepony.kstarbound.network.packets.EntityMessagePacket import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec import ru.dbotthepony.kstarbound.network.syncher.MasterElement import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup @@ -43,6 +42,8 @@ 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.UUID +import java.util.concurrent.CompletableFuture import java.util.function.Consumer import java.util.function.Predicate @@ -179,10 +180,6 @@ abstract class AbstractEntity : Comparable { } - open fun receiveMessage(name: String, arguments: JsonArray): JsonElement? { - return null - } - var removalReason: RemovalReason? = null private set @@ -459,6 +456,43 @@ abstract class AbstractEntity : Comparable { } } + /** + * Entities can send other entities potentially remote messages and get + * responses back from them, and should implement this to receive and respond + * to messages. If the message is NOT handled, should return Nothing, + * otherwise should return some Json value. + * This will only ever be called on master entities. + */ + protected open fun handleMessage(connection: Int, message: String, arguments: JsonArray): JsonElement? { + return null + } + + // doesn't write stacktrace + class MessageCallException(message: String) : RuntimeException(message, null, true, false) + + 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) + } + } + } + open fun render(client: StarboundClient, layers: LayeredRenderer) { } 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 8e163a25..0de42ecd 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 @@ -1,8 +1,12 @@ package ru.dbotthepony.kstarbound.world.entities.tile import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull import com.google.gson.JsonObject import com.google.gson.JsonPrimitive +import it.unimi.dsi.fastutil.ints.IntIterators +import it.unimi.dsi.fastutil.ints.IntLists import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import org.apache.logging.log4j.LogManager @@ -23,8 +27,6 @@ import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.item.IContainer import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.math.Interpolator -import ru.dbotthepony.kstarbound.math.vector.Vector2d -import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedFloat @@ -186,6 +188,49 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co } } + override fun handleMessage(connection: Int, message: String, arguments: JsonArray): JsonElement? { + return when (message.lowercase()) { + "startcrafting" -> startCrafting() + "stopcrafting" -> stopCrafting() + "burncontainercontents" -> burnContainerContents() + + // 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() + "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() + "applyaugment" -> TODO("applyaugment") + "consumeitems" -> JsonPrimitive(items.take(ItemDescriptor(arguments[0]))) + "consumeitemsat" -> JsonPrimitive(items.takeExact(arguments[0].asInt, arguments[1].asLong)) + "clearcontainer" -> { + val result = JsonArray() + + for (slot in 0 until items.size) { + if (items[slot].isNotEmpty) { + result.add(items[slot].toJson()) + } + } + + items.clear() + result + } + + else -> super.handleMessage(connection, message, arguments) + } + } + + private fun startCrafting(): JsonElement { + return JsonNull.INSTANCE + } + + private fun stopCrafting(): JsonElement { + return JsonNull.INSTANCE + } + + private fun burnContainerContents(): JsonElement { + return JsonNull.INSTANCE + } + // Networking of this container to legacy clients is incredibly stupid, // and networks entire state each time something has changed. inner class Container(size: Int) : NetworkedElement(), IContainer {