Entity message handling

This commit is contained in:
DBotThePony 2024-04-29 13:50:59 +07:00
parent 0af6646212
commit 26c579f568
Signed by: DBot
GPG Key ID: DCC23B5715498507
13 changed files with 473 additions and 34 deletions

View File

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

View File

@ -45,6 +45,14 @@ class IdMap<T : Any>(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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Int, String>, 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)
}
}
}
}

View File

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

View File

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

View File

@ -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<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val template: WorldTemplate) : ICellAccess {
@ -573,6 +580,18 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
protected abstract fun networkHitRequest(data: HitRequestPacket)
protected abstract fun networkDamageRequest(data: DamageRequestPacket)
abstract fun remote(connectionID: Int): Connection?
// 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<UUID, CompletableFuture<JsonElement>> = Caffeine.newBuilder()
.maximumSize(32000L) // some unreachable value unless there is a malicious actor
.expireAfterWrite(1L, TimeUnit.MINUTES)
.executor(Starbound.EXECUTOR)
.scheduler(Starbound)
.removalListener<UUID, CompletableFuture<JsonElement>> { 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()

View File

@ -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<AbstractEntity> {
}
open fun receiveMessage(name: String, arguments: JsonArray): JsonElement? {
return null
}
var removalReason: RemovalReason? = null
private set
@ -459,6 +456,43 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
}
}
/**
* 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<JsonElement> {
if (isRemote) {
val connection = world.remote(connectionID) ?: return CompletableFuture.failedFuture(NoSuchElementException("Can't dispatch entity message, no such connection $connectionID"))
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)
}
}
}
open fun render(client: StarboundClient, layers: LayeredRenderer) {
}

View File

@ -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<ObjectDefinition>) : 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 {