diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index bea1e1c8..18fffdec 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -70,10 +70,10 @@ fun main() { val rand = Random() for (i in 0 until 0) { - val item = ItemEntity(world, Registries.items.keys.values.random().value) + val item = ItemEntity(Registries.items.keys.values.random().value) item.position = Vector2d(225.0 - i, 785.0) - item.spawn() + item.spawn(world) item.movement.velocity = Vector2d(rand.nextDouble() * 32.0 - 16.0, rand.nextDouble() * 32.0 - 16.0) item.mailbox.scheduleAtFixedRate({ item.movement.velocity += Vector2d(rand.nextDouble() * 32.0 - 16.0, rand.nextDouble() * 32.0 - 16.0) }, 1000 + rand.nextLong(-100, 100), 1000 + rand.nextLong(-100, 100), TimeUnit.MILLISECONDS) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index c738e6c5..d4a612f1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -962,7 +962,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { if (world != null) { font.render("Camera: ${camera.pos} ${settings.zoom}", y = 140f, scale = 0.25f) font.render("Cursor: $mouseCoordinates -> ${screenToWorld(mouseCoordinates)}", y = 160f, scale = 0.25f) - font.render("World chunk: ${world.chunkFromCell(camera.pos)}", y = 180f, scale = 0.25f) + font.render("World chunk: ${world.geometry.chunkFromCell(camera.pos)}", y = 180f, scale = 0.25f) } drawPerformanceBasic(false) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetEntityPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetEntityPacket.kt new file mode 100644 index 00000000..7a28598d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetEntityPacket.kt @@ -0,0 +1,25 @@ +package ru.dbotthepony.kstarbound.client.network.packets + +import ru.dbotthepony.kommons.io.readUUID +import ru.dbotthepony.kommons.io.writeUUID +import ru.dbotthepony.kstarbound.client.network.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.UUID + +class ForgetEntityPacket(val uuid: UUID) : IClientPacket { + constructor(buff: DataInputStream) : this(buff.readUUID()) + + override fun write(stream: DataOutputStream) { + stream.writeUUID(uuid) + } + + override fun play(connection: ClientConnection) { + val world = connection.client.world ?: return + + world.mailbox.execute { + world.entities.firstOrNull { it.uuid == uuid }?.remove() + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt index ef553dae..56323da0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt @@ -1,6 +1,8 @@ package ru.dbotthepony.kstarbound.client.network.packets import com.google.gson.JsonObject +import ru.dbotthepony.kommons.io.readUUID +import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kstarbound.client.network.ClientConnection import ru.dbotthepony.kstarbound.json.readJsonObject import ru.dbotthepony.kstarbound.json.writeJsonObject @@ -8,11 +10,13 @@ import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.world.entities.WorldObject import java.io.DataInputStream import java.io.DataOutputStream +import java.util.UUID -class SpawnWorldObjectPacket(val data: JsonObject) : IClientPacket { - constructor(stream: DataInputStream) : this(stream.readJsonObject()) +class SpawnWorldObjectPacket(val uuid: UUID, val data: JsonObject) : IClientPacket { + constructor(stream: DataInputStream) : this(stream.readUUID(), stream.readJsonObject()) override fun write(stream: DataOutputStream) { + stream.writeUUID(uuid) stream.writeJsonObject(data) } @@ -20,8 +24,8 @@ class SpawnWorldObjectPacket(val data: JsonObject) : IClientPacket { connection.client.mailbox.execute { val world = connection.client.world ?: return@execute val obj = WorldObject.fromJson(data) - val chunk = world.chunkMap[world.geometry.chunkFromCell(obj.pos)] ?: return@execute - chunk.addObject(obj) + obj.uuid = uuid + obj.spawn(world) } } } 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 d133a5c6..92ee702c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -65,6 +65,10 @@ class ClientWorld( return geometry.loopY || value in 0 .. renderRegionsY } + override fun isSameThread(): Boolean { + return client.isSameThread() + } + inner class RenderRegion(val x: Int, val y: Int) { inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) { val bakedMeshes = ArrayList, RenderLayer.Point>>() @@ -87,7 +91,7 @@ class ClientWorld( for (x in 0 until renderRegionWidth) { for (y in 0 until renderRegionHeight) { - if (!inBounds(x, y)) continue + if (!geometry.inBoundsCell(x, y)) continue if (bakeTaskID != this.bakeTaskID) return@Supplier meshes val tile = view.getTile(x, y) @@ -241,9 +245,7 @@ class ClientWorld( for (x in ix - paddingX .. ix + paddingX) { for (y in iy - paddingY .. iy + paddingY) { - lock.withLock { - renderRegions[renderRegionKey(x, y)]?.let(action) - } + renderRegions[renderRegionKey(x, y)]?.let(action) } } } @@ -264,18 +266,14 @@ class ClientWorld( val index = renderRegionKey(ix, iy) if (seen.add(index)) { - lock.withLock { - renderRegions[index]?.let(action) - } + renderRegions[index]?.let(action) } } } else { val ix = pos.component1() / renderRegionWidth val iy = pos.component2() / renderRegionHeight - lock.withLock { - renderRegions[renderRegionKey(ix, iy)]?.let(action) - } + renderRegions[renderRegionKey(ix, iy)]?.let(action) } } @@ -304,25 +302,9 @@ class ClientWorld( } } - for (obj in objects) { - if (obj.pos.x in client.viewportCellX .. client.viewportCellX + client.viewportCellWidth && obj.pos.y in client.viewportCellY .. client.viewportCellY + client.viewportCellHeight) { - val layer = layers.getLayer(obj.orientation?.renderLayer ?: continue) - - obj.drawables.forEach { - val (x, y) = obj.imagePosition - it.render(client, layer, obj.pos.x.toFloat() + x / PIXELS_IN_STARBOUND_UNITf, obj.pos.y.toFloat() + y / PIXELS_IN_STARBOUND_UNITf) - } - - obj.addLights(client.viewportLighting, client.viewportCellX, client.viewportCellY) - } - } - for (ent in entities) { - if (ent.position.x.toInt() in client.viewportCellX .. client.viewportCellX + client.viewportCellWidth && ent.position.y.toInt() in client.viewportCellY .. client.viewportCellY + client.viewportCellHeight) { - layers.add(RenderLayer.Overlay.point()) { - ent.render(client) - } - } + ent.render(client, layers) + ent.addLights(client.viewportLighting, client.viewportCellX, client.viewportCellY) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt index eec8d3b3..9b51541e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt @@ -27,6 +27,10 @@ abstract class JsonDriven(val path: String) { private val namedLazies = Object2ObjectOpenHashMap>>() protected val properties = JsonObject() + + /** + * [JsonObject]s which define behavior of properties + */ protected abstract fun defs(): Collection protected open fun invalidate() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index 94a1a7f0..81e93f4b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -11,6 +11,7 @@ import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket +import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket import ru.dbotthepony.kstarbound.network.packets.DisconnectPacket @@ -132,5 +133,6 @@ object PacketRegistry { add(::TrackedPositionPacket) add(::TrackedSizePacket) add(::SpawnWorldObjectPacket) + add(::ForgetEntityPacket) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerConnection.kt index 543d2a8b..46580c9e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerConnection.kt @@ -1,12 +1,15 @@ package ru.dbotthepony.kstarbound.server.network import io.netty.channel.ChannelHandlerContext +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket +import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.ConnectionSide @@ -16,6 +19,10 @@ import ru.dbotthepony.kstarbound.network.packets.HelloPacket import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.IChunkListener +import ru.dbotthepony.kstarbound.world.api.ImmutableCell +import ru.dbotthepony.kstarbound.world.entities.AbstractEntity +import ru.dbotthepony.kstarbound.world.entities.WorldObject import java.util.* class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type, UUID(0L, 0L)) { @@ -49,14 +56,29 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } private val tickets = Object2ObjectOpenHashMap() - private val sentChunks = ObjectOpenHashSet() + private val pendingSend = ObjectLinkedOpenHashSet() private var needsToRecomputeTrackedChunks = true + private inner class ChunkListener(val pos: ChunkPos) : IChunkListener { + override fun onEntityAdded(entity: AbstractEntity) { + if (entity is WorldObject) + send(SpawnWorldObjectPacket(entity.uuid, entity.serialize())) + } + + override fun onEntityRemoved(entity: AbstractEntity) { + send(ForgetEntityPacket(entity.uuid)) + } + + override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) { + pendingSend.add(pos) + } + } + fun onLeaveWorld() { tickets.values.forEach { it.cancel() } tickets.clear() - sentChunks.clear() + pendingSend.clear() } private fun recomputeTrackedChunks() { @@ -77,6 +99,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn for ((pos, ticket) in itr) { if (pos !in tracked) { + send(ForgetChunkPacket(pos)) + pendingSend.remove(pos) ticket.cancel() itr.remove() } @@ -84,7 +108,10 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn for (pos in tracked) { if (pos !in tickets) { - tickets[pos] = world.permanentChunkTicket(pos) + val ticket = world.permanentChunkTicket(pos) + tickets[pos] = ticket + ticket.addListener(ChunkListener(pos)) + pendingSend.add(pos) } } } @@ -101,29 +128,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn recomputeTrackedChunks() } - for (pos in tickets.keys) { - val chunk = world.chunkMap[pos] ?: continue - - if (pos !in sentChunks) { - send(ChunkCellsPacket(chunk)) - - chunk.objects.forEach { - send(SpawnWorldObjectPacket(it.serialize())) - } - - sentChunks.add(pos) - } - - - } - - val itr = sentChunks.iterator() + val itr = pendingSend.iterator() for (pos in itr) { - if (pos !in tickets) { - send(ForgetChunkPacket(pos)) - itr.remove() - } + val chunk = world.chunkMap[pos] ?: continue + send(ChunkCellsPacket(chunk)) + itr.remove() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSaver.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSaver.kt index 088e4183..43913cbe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSaver.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSaver.kt @@ -3,9 +3,9 @@ package ru.dbotthepony.kstarbound.server.world import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.api.AbstractCell -import ru.dbotthepony.kstarbound.world.entities.WorldObject +import ru.dbotthepony.kstarbound.world.entities.AbstractEntity interface IChunkSaver { fun saveCells(pos: ChunkPos, data: Object2DArray) - fun saveObjects(pos: ChunkPos, data: Collection) + fun saveEntities(pos: ChunkPos, data: Collection) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt index e07ce2d7..7c53d59a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt @@ -5,19 +5,20 @@ import ru.dbotthepony.kommons.core.KOptional import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.api.AbstractCell +import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.WorldObject import java.util.concurrent.CompletableFuture interface IChunkSource { fun getTiles(pos: ChunkPos): CompletableFuture>> - fun getEntities(pos: ChunkPos): CompletableFuture>> + fun getEntities(pos: ChunkPos): CompletableFuture>> object Void : IChunkSource { override fun getTiles(pos: ChunkPos): CompletableFuture>> { return CompletableFuture.completedFuture(KOptional.of(Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY))) } - override fun getEntities(pos: ChunkPos): CompletableFuture>> { + override fun getEntities(pos: ChunkPos): CompletableFuture>> { return CompletableFuture.completedFuture(KOptional.of(emptyList())) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt index e6f0e447..8e26ae8a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt @@ -12,6 +12,7 @@ import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.MutableCell +import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.WorldObject import java.io.BufferedInputStream import java.io.ByteArrayInputStream @@ -43,7 +44,7 @@ class LegacyChunkSource(val db: BTreeDB) : IChunkSource { } } - override fun getEntities(pos: ChunkPos): CompletableFuture>> { + override fun getEntities(pos: ChunkPos): CompletableFuture>> { return CompletableFuture.supplyAsync { val chunkX = pos.x val chunkY = pos.y @@ -52,7 +53,7 @@ class LegacyChunkSource(val db: BTreeDB) : IChunkSource { val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data), Inflater()))) val i = reader.readVarInt() - val objects = ArrayList() + val objects = ArrayList() for (i2 in 0 until i) { val obj = VersionedJson(reader) 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 40db2330..e71f43ac 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -3,6 +3,8 @@ package ru.dbotthepony.kstarbound.server.world import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet +import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet import ru.dbotthepony.kommons.collect.chainOptionalFutures import ru.dbotthepony.kommons.core.KOptional import ru.dbotthepony.kstarbound.Starbound @@ -11,13 +13,20 @@ import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.network.ServerConnection import ru.dbotthepony.kstarbound.util.ExecutionSpinner import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.ICellChangeListener +import ru.dbotthepony.kstarbound.world.IChunkListener +import ru.dbotthepony.kstarbound.world.IEntityAdditionListener +import ru.dbotthepony.kstarbound.world.IEntityRemovalListener import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.WorldGeometry +import ru.dbotthepony.kstarbound.world.api.ImmutableCell +import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import java.util.Collections import java.util.concurrent.CompletableFuture import java.util.concurrent.RejectedExecutionException import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.LockSupport +import java.util.concurrent.locks.ReentrantLock import java.util.function.Consumer import java.util.function.Supplier import kotlin.concurrent.withLock @@ -68,6 +77,7 @@ class ServerWorld( val spinner = ExecutionSpinner(mailbox, ::spin, Starbound.TICK_TIME_ADVANCE_NANOS) val thread = Thread(spinner, "Starbound Server World $seed") + val ticketListLock = ReentrantLock() @Volatile var isClosed: Boolean = false @@ -114,10 +124,14 @@ class ServerWorld( override val isRemote: Boolean get() = false - override fun thinkInner() { - lock.withLock { - internalPlayers.forEach { it.tick() } + override fun isSameThread(): Boolean { + return Thread.currentThread() === thread + } + override fun thinkInner() { + internalPlayers.forEach { if (!isClosed) it.tick() } + + ticketListLock.withLock { ticketLists.removeIf { val valid = it.tick() @@ -128,7 +142,15 @@ class ServerWorld( val chunk = chunkMap[it.pos] if (chunk != null) { + val unloadable = chunk.entities.filter { it.isApplicableForUnloading } + saver?.saveCells(it.pos, chunk.copyCells()) + saver?.saveEntities(it.pos, unloadable) + + unloadable.forEach { + it.remove() + } + chunkMap.remove(it.pos) } } @@ -168,6 +190,11 @@ class ServerWorld( val isCanceled: Boolean val pos: ChunkPos val id: Int + + val chunk: ServerChunk? + + fun addListener(listener: IChunkListener) + fun removeListener(listener: IChunkListener) } interface ITimedTicket : ITicket, Comparable { @@ -181,7 +208,7 @@ class ServerWorld( } } - private inner class TicketList(val pos: ChunkPos) { + private inner class TicketList(val pos: ChunkPos) : IChunkListener, IChunkMapListener { constructor(pos: Long) : this(ChunkPos(pos)) private var first = true @@ -189,6 +216,7 @@ class ServerWorld( private val temporary = ObjectAVLTreeSet() private var ticks = 0 private var nextTicketID = AtomicInteger() + private var weAreResponsibleForLoadingTheChunk = false val isValid: Boolean get() = temporary.isNotEmpty() || permanent.isNotEmpty() @@ -205,7 +233,34 @@ class ServerWorld( return temporary.isNotEmpty() || permanent.isNotEmpty() } - abstract inner class AbstractTicket : ITicket { + override fun onEntityAdded(entity: AbstractEntity) { + permanent.forEach { it.onEntityAdded(entity) } + temporary.forEach { it.onEntityAdded(entity) } + } + + override fun onEntityRemoved(entity: AbstractEntity) { + permanent.forEach { it.onEntityRemoved(entity) } + temporary.forEach { it.onEntityRemoved(entity) } + } + + override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) { + permanent.forEach { it.onCellChanges(x, y, cell) } + temporary.forEach { it.onCellChanges(x, y, cell) } + } + + override fun onChunkCreated(chunk: ServerChunk) { + if (chunk.pos == pos) { + chunk.addListener(this) + } + } + + override fun onChunkRemoved(chunk: ServerChunk) { + if (chunk.pos == pos) { + chunk.removeListener(this) + } + } + + abstract inner class AbstractTicket : ITicket, IChunkListener { final override val id: Int = nextTicketID.getAndIncrement() final override val pos: ChunkPos get() = this@TicketList.pos @@ -218,8 +273,11 @@ class ServerWorld( if (geometry.x.inBoundsChunk(pos.x) && geometry.y.inBoundsChunk(pos.y)) { ticketLists.add(this@TicketList) + chunkMap.addListener(this@TicketList) + + if (chunkProviders.isNotEmpty() && chunkMap[pos] == null) { + weAreResponsibleForLoadingTheChunk = true - if (chunkProviders.isNotEmpty()) { chainOptionalFutures(chunkProviders) { if (!isValid) CompletableFuture.completedFuture(KOptional.empty()) else it.getTiles(pos) } .thenAccept(Consumer { tiles -> @@ -234,7 +292,7 @@ class ServerWorld( ents.ifPresent { for (obj in it) { - chunk.addObject(obj) + obj.spawn(this@ServerWorld) } } }, mailbox) @@ -250,11 +308,40 @@ class ServerWorld( lock.withLock { if (isCanceled) return isCanceled = true + chunk?.entities?.forEach { e -> listeners.forEach { it.onEntityRemoved(e) } } onCancel() } } protected abstract fun onCancel() + final override val chunk: ServerChunk? + get() = chunkMap[pos] + + private val listeners = ReferenceLinkedOpenHashSet() + + final override fun addListener(listener: IChunkListener) { + if (isCanceled) return + listeners.add(listener) + chunk?.entities?.forEach { listener.onEntityAdded(it) } + } + + final override fun removeListener(listener: IChunkListener) { + if (listeners.remove(listener)) { + chunk?.entities?.forEach { listener.onEntityRemoved(it) } + } + } + + final override fun onEntityAdded(entity: AbstractEntity) { + listeners.forEach { it.onEntityAdded(entity) } + } + + final override fun onEntityRemoved(entity: AbstractEntity) { + listeners.forEach { it.onEntityRemoved(entity) } + } + + final override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) { + listeners.forEach { it.onCellChanges(x, y, cell) } + } } inner class Ticket : AbstractTicket() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index ab69517e..3149bd90 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -11,8 +11,10 @@ import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess import ru.dbotthepony.kstarbound.world.api.TileView -import ru.dbotthepony.kstarbound.world.entities.Entity -import ru.dbotthepony.kstarbound.world.entities.WorldObject +import ru.dbotthepony.kstarbound.world.entities.AbstractEntity +import ru.dbotthepony.kstarbound.world.entities.DynamicEntity +import ru.dbotthepony.kstarbound.world.entities.TileEntity +import java.util.concurrent.CompletableFuture import kotlin.concurrent.withLock /** @@ -44,8 +46,9 @@ abstract class Chunk, This : Chunk() - val objects = ReferenceOpenHashSet() + val entities = ReferenceOpenHashSet() + val dynamicEntities = ReferenceOpenHashSet() + val tileEntities = ReferenceOpenHashSet() protected val subscribers = ObjectArraySet() // local cells' tile access @@ -136,7 +139,7 @@ abstract class Chunk, This : Chunk Unit) { @@ -154,53 +157,69 @@ abstract class Chunk, This : Chunk) { + fun transferEntity(entity: AbstractEntity, otherChunk: Chunk<*, *>) { world.lock.withLock { if (otherChunk == this) throw IllegalArgumentException("what?") - if (this::class.java != otherChunk::class.java) { - throw IllegalArgumentException("Incompatible types: $this !is $otherChunk") - } - - if (!entities.add(entity)) { - throw IllegalArgumentException("Already containing $entity") - } + if (world != otherChunk.world) + throw IllegalArgumentException("Chunks belong to different worlds: this: $this / other: $otherChunk") changeset++ + otherChunk.changeset++ + + entities.add(entity) + otherChunk.entities.remove(entity) + + if (entity is TileEntity) { + tileEntities.add(entity) + otherChunk.tileEntities.remove(entity) + } + + if (entity is DynamicEntity) { + dynamicEntities.add(entity) + otherChunk.dynamicEntities.remove(entity) + } + otherChunk.subscribers.forEach { it.onEntityRemoved(entity) } subscribers.forEach { it.onEntityAdded(entity) } - - if (!otherChunk.entities.remove(entity)) { - throw IllegalStateException("Unable to remove $entity from $otherChunk after transfer") - } } } - fun removeEntity(entity: Entity) { + fun removeEntity(entity: AbstractEntity) { world.lock.withLock { - if (!entities.remove(entity)) { + if (!entities.remove(entity)) throw IllegalArgumentException("Already not having entity $entity") - } + + if (entity is TileEntity) + tileEntities.remove(entity) + + if (entity is DynamicEntity) + dynamicEntities.remove(entity) changeset++ subscribers.forEach { it.onEntityRemoved(entity) } @@ -211,39 +230,8 @@ abstract class Chunk, This : Chunk, ChunkType : Chunk>( val seed: Long, val geometry: WorldGeometry, ) : ICellAccess, Closeable { - // whenever provided cell position is within actual world borders, ignoring wrapping - fun inBounds(x: Int, y: Int) = geometry.x.inBoundsCell(x) && geometry.y.inBoundsCell(y) - fun inBounds(value: IStruct2i) = geometry.x.inBoundsCell(value.component1()) && geometry.y.inBoundsCell(value.component2()) - - fun chunkFromCell(x: Int, y: Int) = ChunkPos(geometry.x.chunkFromCell(x), geometry.y.chunkFromCell(y)) - fun chunkFromCell(x: Double, y: Double) = ChunkPos(geometry.x.chunkFromCell(x.toInt()), geometry.y.chunkFromCell(y.toInt())) - fun chunkFromCell(value: IStruct2i) = chunkFromCell(value.component1(), value.component2()) - fun chunkFromCell(value: IStruct2d) = chunkFromCell(value.component1(), value.component2()) - val background = TileView.Background(this) val foreground = TileView.Foreground(this) val mailbox = MailboxExecutorService() @@ -64,6 +53,11 @@ abstract class World, ChunkType : Chunk> { + fun onChunkCreated(chunk: ChunkType) { } + fun onChunkRemoved(chunk: ChunkType) { } + } + abstract inner class ChunkMap : Iterable { abstract operator fun get(x: Int, y: Int): ChunkType? abstract fun compute(x: Int, y: Int): ChunkType? @@ -77,15 +71,23 @@ abstract class World, ChunkType : Chunk>() + + fun addListener(listener: IChunkMapListener) { + listeners.add(listener) + } + + fun removeListener(listener: IChunkMapListener) { + listeners.remove(listener) + } + protected fun create(x: Int, y: Int): ChunkType { val pos = ChunkPos(x, y) val chunk = chunkFactory(pos) - val orphanedInThisChunk = ArrayList() + val orphanedInThisChunk = ArrayList() for (ent in orphanedEntities) { - val (ex, ey) = ent.position - - if (geometry.x.chunkFromCell(ex) == x && geometry.y.chunkFromCell(ey) == y) { + if (ent.chunkPos == pos) { orphanedInThisChunk.add(ent) } } @@ -94,6 +96,7 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk() + val entities = ReferenceOpenHashSet() + val dynamicEntities = ReferenceOpenHashSet() + val tileEntities = ReferenceOpenHashSet() + + abstract fun isSameThread(): Boolean + + fun ensureSameThread() { + check(isSameThread()) { "Trying to access $this from ${Thread.currentThread()}" } + } + fun think() { try { mailbox.executeQueuedTasks() - val entities = ObjectArrayList(entities) - ForkJoinPool.commonPool().submit(ParallelPerform(entities.spliterator(), { it.movement.move() })).join() + + ForkJoinPool.commonPool().submit(ParallelPerform(dynamicEntities.spliterator(), { it.movement.move() })).join() mailbox.executeQueuedTasks() - for (ent in entities) { - ent.thinkShared() - - if (isRemote) - ent.thinkClient() - else - ent.thinkServer() - } - + entities.forEach { it.think() } mailbox.executeQueuedTasks() - lock - .withLock { ObjectArrayList(chunkMap.iterator()) } - .forEach { it.think() } - - val objects = ObjectArrayList(objects) - - for (ent in objects) { - ent.thinkShared() - - if (isRemote) - ent.thinkClient() - else - ent.thinkServer() + for (chunk in chunkMap) { + chunk.think() } mailbox.executeQueuedTasks() @@ -274,10 +267,6 @@ abstract class World, ChunkType : Chunk() - val entities = ReferenceLinkedOpenHashSet() - val objects = ReferenceLinkedOpenHashSet() - protected abstract fun chunkFactory(pos: ChunkPos): ChunkType override fun close() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt index ab2fb30c..72deb0f3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt @@ -42,6 +42,14 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean, val loopY: Bool return ChunkPos(x.chunkFromCell(pos.component1()), y.chunkFromCell(pos.component2())) } + fun inBoundsCell(pos: IStruct2i): Boolean { + return x.inBoundsCell(pos.component1()) && y.inBoundsCell(pos.component2()) + } + + fun inBoundsCell(x: Int, y: Int): Boolean { + return this.x.inBoundsCell(x) && this.y.inBoundsCell(y) + } + fun wrap(pos: ChunkPos): ChunkPos { val x = this.x.chunk(pos.x) val y = this.y.chunk(pos.y) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractActorMovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractActorMovementController.kt index 04424464..4fc39545 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractActorMovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractActorMovementController.kt @@ -79,7 +79,7 @@ abstract class AbstractActorMovementController : AbstractMovementController() { // this is set internally on each move step final override var movementParameters: MovementParameters = MovementParameters.EMPTY - abstract var anchorEntity: Entity? + abstract var anchorEntity: DynamicEntity? var pathController: PathController? = null var groundMovementSustainTimer: GameTimer = GameTimer(0.0) @@ -194,7 +194,7 @@ abstract class AbstractActorMovementController : AbstractMovementController() { override fun move() { // TODO: anchor entity - if (anchorEntity?.isRemoved == true) + if (anchorEntity?.isSpawned != true) anchorEntity = null val anchorEntity = anchorEntity diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt new file mode 100644 index 00000000..0dcb4861 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -0,0 +1,132 @@ +package ru.dbotthepony.kstarbound.world.entities + +import ru.dbotthepony.kommons.util.MailboxExecutorService +import ru.dbotthepony.kstarbound.client.StarboundClient +import ru.dbotthepony.kstarbound.client.render.LayeredRenderer +import ru.dbotthepony.kstarbound.defs.JsonDriven +import ru.dbotthepony.kstarbound.world.Chunk +import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.LightCalculator +import ru.dbotthepony.kstarbound.world.World +import java.util.UUID +import kotlin.concurrent.withLock + +abstract class AbstractEntity(path: String) : JsonDriven(path) { + /** + * The chunk this entity resides in + */ + var chunk: Chunk<*, *>? = null + set(value) { + if (innerWorld == null) { + throw IllegalStateException("Trying to set chunk this entity belong to before spawning in world") + } else if (value != null && innerWorld != value.world) { + throw IllegalArgumentException("$this belongs to $innerWorld, $value belongs to ${value.world}") + } else if (value == field) { + return + } + + val oldChunk = field + field = value + + world.lock.withLock { + if (oldChunk == null && value != null) { + world.orphanedEntities.remove(this) + value.addEntity(this) + } else if (oldChunk != null && value == null) { + world.orphanedEntities.add(this) + oldChunk.removeEntity(this) + } else if (oldChunk != null && value != null) { + value.transferEntity(this, oldChunk) + } + } + } + + var uuid: UUID = UUID.randomUUID() + abstract val chunkPos: ChunkPos + + var mailbox = MailboxExecutorService() + private set + + private var innerWorld: World<*, *>? = null + + val world: World<*, *> + get() = innerWorld ?: throw IllegalStateException("Not in world") + + val isSpawned: Boolean + get() = innerWorld != null + + /** + * Whenever this entity should be removed when chunk containing it is being unloaded + * + * Returning false will also stop entity from being saved to disk, and render entity orphaned + * when chunk containing it will get unloaded + */ + open val isApplicableForUnloading: Boolean + get() = true + + protected open fun onSpawn(world: World<*, *>) { } + protected open fun onRemove(world: World<*, *>) { } + + /** + * MUST be called by [World] itself + */ + fun spawn(world: World<*, *>) { + if (innerWorld != null) + throw IllegalStateException("Already spawned (in world $innerWorld)") + + world.ensureSameThread() + + if (mailbox.isShutdown) + mailbox = MailboxExecutorService() + + innerWorld = world + world.entities.add(this) + world.orphanedEntities.add(this) + onSpawn(world) + } + + fun remove() { + val world = innerWorld ?: throw IllegalStateException("Not in world") + world.ensureSameThread() + + mailbox.shutdownNow() + chunk = null + world.entities.remove(this) + world.orphanedEntities.remove(this) + onRemove(world) + innerWorld = null + } + + open val isRemote: Boolean + get() = innerWorld?.isRemote ?: false + + fun think() { + thinkShared() + + if (isRemote) { + thinkRemote() + } else { + thinkLocal() + } + } + + protected open fun thinkShared() { + mailbox.executeQueuedTasks() + } + + protected open fun thinkRemote() { + + } + + protected open fun thinkLocal() { + + } + + open fun render(client: StarboundClient, layers: LayeredRenderer) { + + } + + open fun addLights(lightCalculator: LightCalculator, xOffset: Int, yOffset: Int) { + + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractMovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractMovementController.kt index 316af504..5257b667 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractMovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractMovementController.kt @@ -341,7 +341,7 @@ abstract class AbstractMovementController { if (slopeCorrection) { // Starbound: First try separating with our ground sliding cheat. - separation = collisionSeparate(checkBody, sorted, ignorePlatforms, maximumPlatformCorrection, true, Entity.SEPARATION_TOLERANCE) + separation = collisionSeparate(checkBody, sorted, ignorePlatforms, maximumPlatformCorrection, true, SEPARATION_TOLERANCE) totalCorrection += separation.correction checkBody += separation.correction maxCollided = maxCollided.maxOf(separation.collisionType) @@ -360,7 +360,7 @@ abstract class AbstractMovementController { // KStarbound: if we got pushed into world geometry, then consider slide cheat didn't find a solution if (separation.solutionFound) { - separation.solutionFound = staticBodies.all { it.poly.intersect(checkBody).let { it == null || it.penetration.absoluteValue <= Entity.SEPARATION_TOLERANCE } } + separation.solutionFound = staticBodies.all { it.poly.intersect(checkBody).let { it == null || it.penetration.absoluteValue <= SEPARATION_TOLERANCE } } } } @@ -369,8 +369,8 @@ abstract class AbstractMovementController { totalCorrection = Vector2d.ZERO movingCollisionId = null - for (i in 0 until Entity.SEPARATION_STEPS) { - separation = collisionSeparate(checkBody, sorted, ignorePlatforms, maximumPlatformCorrection, false, Entity.SEPARATION_TOLERANCE) + for (i in 0 until SEPARATION_STEPS) { + separation = collisionSeparate(checkBody, sorted, ignorePlatforms, maximumPlatformCorrection, false, SEPARATION_TOLERANCE) totalCorrection += separation.correction checkBody += separation.correction maxCollided = maxCollided.maxOf(separation.collisionType) @@ -388,8 +388,8 @@ abstract class AbstractMovementController { checkBody = body totalCorrection = -movement - for (i in 0 until Entity.SEPARATION_STEPS) { - separation = collisionSeparate(checkBody, sorted, true, maximumPlatformCorrection, false, Entity.SEPARATION_TOLERANCE) + for (i in 0 until SEPARATION_STEPS) { + separation = collisionSeparate(checkBody, sorted, true, maximumPlatformCorrection, false, SEPARATION_TOLERANCE) totalCorrection += separation.correction checkBody += separation.correction maxCollided = maxCollided.maxOf(separation.collisionType) @@ -408,7 +408,7 @@ abstract class AbstractMovementController { movement = movement + totalCorrection, correction = totalCorrection, isStuck = false, - isOnGround = -totalCorrection.dot(determineGravity()) > Entity.SEPARATION_TOLERANCE, + isOnGround = -totalCorrection.dot(determineGravity()) > SEPARATION_TOLERANCE, movingCollisionId = movingCollisionId, collisionType = maxCollided, // groundSlope = Vector2d.POSITIVE_Y, @@ -489,4 +489,9 @@ abstract class AbstractMovementController { return separation } + + companion object { + const val SEPARATION_STEPS = 3 + const val SEPARATION_TOLERANCE = 0.001 + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt new file mode 100644 index 00000000..1a813710 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt @@ -0,0 +1,71 @@ +package ru.dbotthepony.kstarbound.world.entities + +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.util.AABB +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.client.StarboundClient +import ru.dbotthepony.kstarbound.client.render.LayeredRenderer +import ru.dbotthepony.kstarbound.client.render.RenderLayer +import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.World + +/** + * Entities with dynamics (Player, Drops, Projectiles, NPCs, etc) + */ +abstract class DynamicEntity(path: String) : AbstractEntity(path) { + private var forceChunkRepos = false + + var position = Vector2d() + set(value) { + val old = field + + if (isSpawned) { + field = world.geometry.wrap(value) + + val oldChunkPos = world.geometry.chunkFromCell(old) + val newChunkPos = world.geometry.chunkFromCell(field) + + chunkPos = newChunkPos + + if (oldChunkPos != newChunkPos || forceChunkRepos) { + chunk = world.chunkMap[newChunkPos] + forceChunkRepos = false + } + } else { + field = value + } + } + + abstract val movement: AbstractMovementController + final override var chunkPos: ChunkPos = ChunkPos.ZERO + private set + + override fun onSpawn(world: World<*, *>) { + world.dynamicEntities.add(this) + forceChunkRepos = true + position = position + } + + override fun onRemove(world: World<*, *>) { + world.dynamicEntities.remove(this) + } + + override fun render(client: StarboundClient, layers: LayeredRenderer) { + layers.add(RenderLayer.Overlay.point()) { + val hitboxes = movement.localHitboxes.toList() + if (hitboxes.isEmpty()) return@add + + hitboxes.forEach { it.render(client) } + + world.queryCollisions( + hitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().enlarge(2.0, 2.0) + ).filter(movement::shouldCollideWithBody).forEach { it.poly.render(client, BLOCK_COLLISION_COLOR) } + } + } + + companion object { + val BLOCK_COLLISION_COLOR = RGBAColor(65, 179, 217) + const val SEPARATION_STEPS = 3 + const val SEPARATION_TOLERANCE = 0.001 + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt deleted file mode 100644 index 675fe658..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt +++ /dev/null @@ -1,155 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities - -import ru.dbotthepony.kommons.math.RGBAColor -import ru.dbotthepony.kommons.util.AABB -import ru.dbotthepony.kommons.util.MailboxExecutorService -import ru.dbotthepony.kommons.vector.Vector2d -import ru.dbotthepony.kstarbound.client.StarboundClient -import ru.dbotthepony.kstarbound.world.Chunk -import ru.dbotthepony.kstarbound.world.World -import kotlin.concurrent.withLock - -abstract class Entity(val world: World<*, *>) { - var chunk: Chunk<*, *>? = null - set(value) { - if (!isSpawned) { - throw IllegalStateException("Trying to set chunk this entity belong to before spawning in world") - } else if (isRemoved) { - throw IllegalStateException("This entity was removed") - } else if (value == field) { - return - } - - val chunkPos = world.chunkFromCell(position) - - if (value != null && chunkPos != value.pos) { - throw IllegalStateException("Set proper position before setting chunk this Entity belongs to (expected chunk $chunkPos, got chunk ${value.pos})") - } - - val oldChunk = field - field = value - - world.lock.withLock { - if (oldChunk == null && value != null) { - world.orphanedEntities.remove(this) - value.addEntity(this) - } else if (oldChunk != null && value == null) { - world.orphanedEntities.add(this) - oldChunk.removeEntity(this) - } else if (oldChunk != null && value != null) { - value.transferEntity(this, oldChunk) - } - } - } - - var position = Vector2d() - set(value) { - if (field == value) - return - - val old = field - field = Vector2d(world.geometry.x.cell(value.x), world.geometry.y.cell(value.y)) - - if (isSpawned && !isRemoved) { - val oldChunkPos = world.chunkFromCell(old) - val newChunkPos = world.chunkFromCell(field) - - if (oldChunkPos != newChunkPos) { - chunk = world.chunkMap[newChunkPos] - } - } - } - - abstract val movement: AbstractMovementController - - val mailbox = MailboxExecutorService(world.mailbox.thread) - - /** - * true - whitelist, false - blacklist - */ - protected var collisionFilterMode = false - - /** - * Whenever is this entity spawned in world ([spawn] called). - * Doesn't mean entity still exists in world, check it with [isRemoved] - */ - var isSpawned = false - private set - - /** - * Whenever is this entity was removed from world ([remove] called). - */ - var isRemoved = false - private set - - open fun spawn() { - if (isSpawned) - throw IllegalStateException("Already spawned") - - isSpawned = true - - world.mailbox.execute { - world.entities.add(this) - chunk = world.chunkMap[world.chunkFromCell(position)] - - if (chunk == null) { - world.orphanedEntities.add(this) - } - } - } - - open fun remove() { - if (isRemoved) - throw IllegalStateException("Already removed") - - isRemoved = true - mailbox.shutdownNow() - - if (isSpawned) { - world.mailbox.execute { - world.entities.remove(this) - chunk?.removeEntity(this) - } - } - } - - /** - * this function is executed sequentially - */ - open fun thinkShared() { - mailbox.executeQueuedTasks() - } - - open fun thinkClient() { - - } - - open fun thinkServer() { - - } - - open fun render(client: StarboundClient = StarboundClient.current()) { - val hitboxes = movement.localHitboxes.toList() - if (hitboxes.isEmpty()) return - - hitboxes.forEach { it.render(client) } - - world.queryCollisions( - hitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().enlarge(2.0, 2.0) - ).filter(movement::shouldCollideWithBody).forEach { it.poly.render(client, BLOCK_COLLISION_COLOR) } - } - - open var maxHealth = 0.0 - open var health = 0.0 - - open fun hurt(amount: Double): Boolean { - return false - } - - companion object { - const val PHYSICS_TICKS_UNTIL_SLEEP = 16 - val BLOCK_COLLISION_COLOR = RGBAColor(65, 179, 217) - const val SEPARATION_STEPS = 3 - const val SEPARATION_TOLERANCE = 0.001 - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EntityActorMovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EntityActorMovementController.kt index 38921534..1a94ea66 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EntityActorMovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EntityActorMovementController.kt @@ -7,7 +7,7 @@ import ru.dbotthepony.kstarbound.defs.player.ActorMovementModifiers import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.World -class EntityActorMovementController(val entity: Entity) : AbstractActorMovementController() { +class EntityActorMovementController(val entity: DynamicEntity) : AbstractActorMovementController() { override val world: World<*, *> by entity::world override var position: Vector2d by entity::position override var actorMovementParameters: ActorMovementParameters = GlobalDefaults.actorMovementParameters @@ -63,5 +63,5 @@ class EntityActorMovementController(val entity: Entity) : AbstractActorMovementC override val approachVelocityAngles: MutableList = ArrayList() override var movingDirection: Direction? = null override var facingDirection: Direction? = null - override var anchorEntity: Entity? = null + override var anchorEntity: DynamicEntity? = null } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EntityMovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EntityMovementController.kt index 113000a7..49a33479 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EntityMovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EntityMovementController.kt @@ -5,7 +5,7 @@ import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.defs.MovementParameters import ru.dbotthepony.kstarbound.world.World -class EntityMovementController(val entity: Entity) : AbstractMovementController() { +class EntityMovementController(val entity: DynamicEntity) : AbstractMovementController() { override val world: World<*, *> by entity::world override var position: Vector2d by entity::position override var movementParameters: MovementParameters = GlobalDefaults.movementParameters diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt index 57be5fb9..c6579341 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.world.entities +import com.google.gson.JsonObject import ru.dbotthepony.kommons.core.Either import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.vector.Vector2d @@ -7,9 +8,13 @@ import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.physics.Poly -class ItemEntity(world: World<*, *>, val def: IItemDefinition) : Entity(world) { +class ItemEntity(val def: IItemDefinition) : DynamicEntity("/") { override val movement = EntityMovementController(this) + override fun defs(): Collection { + return emptyList() + } + init { movement.movementParameters = movement.movementParameters.copy(collisionPoly = Either.left(Poly(AABB.rectangle(Vector2d.ZERO, 0.75, 0.75)))) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt index 3c74ff30..565122dd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt @@ -1,12 +1,20 @@ package ru.dbotthepony.kstarbound.world.entities +import com.google.gson.JsonObject import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.world.World -class PlayerEntity(world: World<*, *>) : Entity(world) { +class PlayerEntity() : DynamicEntity("/") { override val movement = EntityActorMovementController(this) + override val isApplicableForUnloading: Boolean + get() = false + + override fun defs(): Collection { + return emptyList() + } + init { movement.actorMovementParameters = movement.actorMovementParameters.merge( Starbound.gson.fromJson(""" diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/TileEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/TileEntity.kt new file mode 100644 index 00000000..1c77c2b6 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/TileEntity.kt @@ -0,0 +1,47 @@ +package ru.dbotthepony.kstarbound.world.entities + +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.World + +/** + * (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid + */ +abstract class TileEntity(path: String) : AbstractEntity(path) { + private var forceChunkRepos = false + + var position = Vector2i() + set(value) { + val old = field + + if (isSpawned) { + field = world.geometry.wrap(value) + + val oldChunkPos = world.geometry.chunkFromCell(old) + val newChunkPos = world.geometry.chunkFromCell(field) + + chunkPos = newChunkPos + + if (oldChunkPos != newChunkPos || forceChunkRepos) { + chunk = world.chunkMap[newChunkPos] + forceChunkRepos = false + } + } else { + field = value + } + } + + final override var chunkPos: ChunkPos = ChunkPos.ZERO + private set + + override fun onSpawn(world: World<*, *>) { + world.tileEntities.add(this) + forceChunkRepos = true + position = position + } + + override fun onRemove(world: World<*, *>) { + world.tileEntities.remove(this) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt index 4ea7893d..51ad84a1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt @@ -11,6 +11,8 @@ import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.client.StarboundClient +import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.world.ClientWorld import ru.dbotthepony.kstarbound.defs.Drawable import ru.dbotthepony.kstarbound.defs.JsonDriven @@ -22,14 +24,14 @@ import ru.dbotthepony.kstarbound.json.get import ru.dbotthepony.kstarbound.json.set import ru.dbotthepony.kstarbound.world.Side import ru.dbotthepony.kstarbound.world.LightCalculator +import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.api.TileColor import kotlin.properties.Delegates open class WorldObject( val prototype: Registry.Entry, - val pos: Vector2i, -) : JsonDriven(prototype.file?.computeDirectory() ?: "/") { +) : TileEntity(prototype.file?.computeDirectory() ?: "/") { fun deserialize(data: JsonObject) { direction = data.get("direction", directions) { Side.LEFT } orientationIndex = data.get("orientationIndex", -1) @@ -48,7 +50,7 @@ open class WorldObject( fun serialize(): JsonObject { val into = JsonObject() into["name"] = prototype.key - into["tilePosition"] = vectors.toJsonTree(pos) + into["tilePosition"] = vectors.toJsonTree(position) into["direction"] = directions.toJsonTree(direction) into["orientationIndex"] = orientationIndex into["interactive"] = interactive @@ -61,10 +63,6 @@ open class WorldObject( return into } - val mailbox = MailboxExecutorService() - var world: World<*, *> by Delegates.notNull() - private set - // // internal runtime properties // @@ -83,11 +81,6 @@ open class WorldObject( private var frameTimer = 0.0 val flickerPeriod = prototype.value.flickerPeriod?.copy() - var isRemoved = false - private set - var isSpawned = false - private set - // // top level properties // @@ -139,33 +132,12 @@ open class WorldObject( super.invalidate() } - protected open fun innerSpawn() {} - protected open fun innerRemove() {} - - fun spawn(world: World<*, *>) { - check(!isSpawned) { "Already spawned in ${this.world}!" } - this.world = world - isSpawned = true - innerSpawn() - invalidate() - } - - fun remove() { - if (isRemoved || !isSpawned) return - isRemoved = true - - world.mailbox.execute { - check(world.objects.remove(this)) - innerRemove() - } - } - - open fun thinkShared() { - mailbox.executeQueuedTasks() + override fun thinkShared() { + super.thinkShared() flickerPeriod?.update(Starbound.TICK_TIME_ADVANCE, world.random) } - open fun thinkClient() { + override fun thinkRemote() { val orientation = orientation if (orientation != null) { @@ -174,10 +146,6 @@ open class WorldObject( } } - open fun thinkServer() { - - } - val orientation: ObjectOrientation? get() { return orientations.getOrNull(orientationIndex) } @@ -193,7 +161,7 @@ open class WorldObject( ?: ImmutableMap.of() } - fun addLights(lightCalculator: LightCalculator, xOffset: Int, yOffset: Int) { + override fun addLights(lightCalculator: LightCalculator, xOffset: Int, yOffset: Int) { var color = lightColors[color.lowercase] if (color != null) { @@ -202,7 +170,16 @@ open class WorldObject( color *= sample } - lightCalculator.addPointLight(pos.x - xOffset, pos.y - yOffset, color) + lightCalculator.addPointLight(position.x - xOffset, position.y - yOffset, color) + } + } + + override fun render(client: StarboundClient, layers: LayeredRenderer) { + val layer = layers.getLayer(orientation?.renderLayer ?: return) + + drawables.forEach { + val (x, y) = imagePosition + it.render(client, layer, position.x.toFloat() + x / PIXELS_IN_STARBOUND_UNITf, position.y.toFloat() + y / PIXELS_IN_STARBOUND_UNITf) } } @@ -216,7 +193,8 @@ open class WorldObject( fun fromJson(content: JsonObject): WorldObject { val prototype = Registries.worldObjects[content["name"]?.asString ?: throw IllegalArgumentException("Missing object name")] ?: throw IllegalArgumentException("No such object defined for '${content["name"]}'") val pos = content.get("tilePosition", vectors) { throw IllegalArgumentException("No tilePosition was present in saved data") } - val result = WorldObject(prototype, pos) + val result = WorldObject(prototype) + result.position = pos result.deserialize(content) return result }