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.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity 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.DamageNotificationPacket
import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket
import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket
@ -339,6 +340,12 @@ class ClientWorld(
client.activeConnection?.send(data) 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 { companion object {
val ring = listOf( val ring = listOf(
Vector2i(0, 0), 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 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() { override fun clear() {
nextIndex = min - 1 nextIndex = min - 1
map.clear() map.clear()

View File

@ -180,13 +180,12 @@ data class ItemDescriptor(
it.add("parameters", parameters.deepCopy()) it.add("parameters", parameters.deepCopy())
} }
fun toJson(): JsonObject? { fun toJson(): JsonElement {
if (Starbound.IS_STORE_JSON) { if (Starbound.IS_STORE_JSON) {
return VersionRegistry.make("Item", toJsonStruct()).toJson() return VersionRegistry.make("Item", toJsonStruct()).toJson()
} else { } else {
if (isEmpty) { if (isEmpty)
return null return JsonNull.INSTANCE
}
return toJsonStruct() return toJsonStruct()
} }

View File

@ -3,6 +3,10 @@ package ru.dbotthepony.kstarbound.item
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import it.unimi.dsi.fastutil.ints.IntArrayList 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.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
@ -11,6 +15,7 @@ interface IContainer {
var size: Int var size: Int
operator fun get(index: Int): ItemStack operator fun get(index: Int): ItemStack
operator fun set(index: Int, value: ItemStack) operator fun set(index: Int, value: ItemStack)
fun clear()
fun ageItems(by: Double): Boolean { fun ageItems(by: Double): Boolean {
var any = false 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 { 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() val copy = item.copy()
// first, try to put into not empty slots first var itr = slots.invoke()
for (i in 0 until size) {
val itemThere = this[i] while (itr.hasNext()) {
val slot = itr.nextInt()
if (slot !in 0 until size)
continue
val itemThere = this[slot]
if (itemThere.isStackable(copy)) { if (itemThere.isStackable(copy)) {
val newCount = (itemThere.size + copy.size).coerceAtMost(itemThere.maxStackSize) val newCount = (itemThere.size + copy.size).coerceAtMost(itemThere.maxStackSize)
@ -131,21 +152,29 @@ interface IContainer {
} }
} }
// then try to move into empty slots itr = slots.invoke()
for (i in 0 until size) {
val itemThere = this[i] while (itr.hasNext()) {
val slot = itr.nextInt()
if (slot !in 0 until size)
continue
val itemThere = this[slot]
if (itemThere.isEmpty) { if (itemThere.isEmpty) {
if (copy.size > copy.maxStackSize) { if (itemThere.isEmpty) {
if (!simulate) if (copy.size > copy.maxStackSize) {
this[i] = copy.copy(copy.maxStackSize) if (!simulate)
this[slot] = copy.copy(copy.maxStackSize)
copy.size -= copy.maxStackSize copy.size -= copy.maxStackSize
} else { } else {
if (!simulate) if (!simulate)
this[i] = copy this[slot] = copy
return ItemStack.EMPTY return ItemStack.EMPTY
}
} }
} }
} }
@ -153,7 +182,153 @@ interface IContainer {
return copy 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 { fun toJson(store: Boolean = false): JsonArray {
val result = JsonArray(size) 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) return ItemStack(entry, config, parameters.deepCopy(), size)
} }
fun toJson(): JsonObject? { fun toJson(): JsonElement {
if (isEmpty) if (isEmpty)
return null return JsonNull.INSTANCE
return createDescriptor().toJson() return createDescriptor().toJson()
} }

View File

@ -245,6 +245,8 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
} }
companion object { companion object {
const val SERVER_CONNECTION_ID = 0
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
private val warpActionCodec = StreamCodec.Pair(WarpAction.CODEC, WarpMode.CODEC).koptional() 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.DamageRequestPacket
import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket
import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket 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.EntityUpdateSetPacket
import ru.dbotthepony.kstarbound.network.packets.PingPacket import ru.dbotthepony.kstarbound.network.packets.PingPacket
import ru.dbotthepony.kstarbound.network.packets.PongPacket import ru.dbotthepony.kstarbound.network.packets.PongPacket
@ -483,8 +485,8 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.add(HitRequestPacket::read) LEGACY.add(HitRequestPacket::read)
LEGACY.add(DamageRequestPacket::read) LEGACY.add(DamageRequestPacket::read)
LEGACY.add(::DamageNotificationPacket) LEGACY.add(::DamageNotificationPacket)
LEGACY.skip("EntityMessage") LEGACY.add(::EntityMessagePacket)
LEGACY.skip("EntityMessageResponse") LEGACY.add(::EntityMessageResponsePacket)
LEGACY.add(::UpdateWorldPropertiesPacket) LEGACY.add(::UpdateWorldPropertiesPacket)
LEGACY.add(::StepUpdatePacket) 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.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.jsonArrayOf import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket
import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket
@ -304,7 +305,7 @@ class ServerWorld private constructor(
val broken = entity.damageTileEntity(occupySpaces, sourcePosition, actualDamage) val broken = entity.damageTileEntity(occupySpaces, sourcePosition, actualDamage)
if (source != null && broken) { 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, damagePositions.firstOrNull { p -> actualPositions.any { it.position == p } } ?: entity.tilePosition,
entity.type.jsonName, entity.type.jsonName,
(entity as? WorldObject)?.config?.key)) (entity as? WorldObject)?.config?.key))
@ -341,7 +342,7 @@ class ServerWorld private constructor(
topMost = topMost.coerceAtLeast(result) topMost = topMost.coerceAtLeast(result)
if (source != null && health?.isDead == true) { if (source != null && health?.isDead == true) {
source.receiveMessage("tileBroken", jsonArrayOf( source.dispatchMessage(Connection.SERVER_CONNECTION_ID, "tileBroken", jsonArrayOf(
pos, if (isBackground) "background" else "foreground", pos, if (isBackground) "background" else "foreground",
tile!!.tile(isBackground).material.id ?: tile.tile(isBackground).material.key, // TODO: explicit string identifiers support tile!!.tile(isBackground).material.id ?: tile.tile(isBackground).material.key, // TODO: explicit string identifiers support
tile.dungeonId, tile.dungeonId,
@ -644,6 +645,10 @@ class ServerWorld private constructor(
server.channels.connectionByID(data.destinationConnection)?.send(data) server.channels.connectionByID(data.destinationConnection)?.send(data)
} }
override fun remote(connectionID: Int): Connection? {
return server.channels.connectionByID(connectionID)
}
@JsonFactory @JsonFactory
data class MetadataJson( data class MetadataJson(
val playerStart: Vector2d, val playerStart: Vector2d,

View File

@ -1,5 +1,7 @@
package ru.dbotthepony.kstarbound.world 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.JsonElement
import com.google.gson.JsonNull import com.google.gson.JsonNull
import com.google.gson.JsonObject 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.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket
import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket
@ -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.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.*
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList 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.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate import java.util.function.Predicate
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import java.util.stream.Stream import java.util.stream.Stream
import kotlin.collections.ArrayList
import kotlin.math.roundToInt import kotlin.math.roundToInt
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val template: WorldTemplate) : ICellAccess { 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 networkHitRequest(data: HitRequestPacket)
protected abstract fun networkDamageRequest(data: DamageRequestPacket) 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 { companion object {
private val LOGGER = LogManager.getLogger() 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.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.koptional
import ru.dbotthepony.kommons.io.nullable import ru.dbotthepony.kommons.io.nullable
import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer 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.EntityDestroyPacket
import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket
import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket
import ru.dbotthepony.kstarbound.network.packets.EntityMessagePacket
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
import ru.dbotthepony.kstarbound.network.syncher.MasterElement import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup 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.castRay
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.function.Consumer import java.util.function.Consumer
import java.util.function.Predicate 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 var removalReason: RemovalReason? = null
private set 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) { open fun render(client: StarboundClient, layers: LayeredRenderer) {
} }

View File

@ -1,8 +1,12 @@
package ru.dbotthepony.kstarbound.world.entities.tile package ru.dbotthepony.kstarbound.world.entities.tile
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.ints.IntIterators
import it.unimi.dsi.fastutil.ints.IntLists
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import org.apache.logging.log4j.LogManager 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.IContainer
import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.math.Interpolator 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.NetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat 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, // Networking of this container to legacy clients is incredibly stupid,
// and networks entire state each time something has changed. // and networks entire state each time something has changed.
inner class Container(size: Int) : NetworkedElement(), IContainer { inner class Container(size: Int) : NetworkedElement(), IContainer {