More grinding on entities and their networking

This commit is contained in:
DBotThePony 2024-02-05 03:17:30 +07:00
parent cae0a89ea2
commit 0eea0fa13f
Signed by: DBot
GPG Key ID: DCC23B5715498507
27 changed files with 615 additions and 418 deletions

View File

@ -70,10 +70,10 @@ fun main() {
val rand = Random() val rand = Random()
for (i in 0 until 0) { 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.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.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) 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)

View File

@ -962,7 +962,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
if (world != null) { if (world != null) {
font.render("Camera: ${camera.pos} ${settings.zoom}", y = 140f, scale = 0.25f) 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("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) drawPerformanceBasic(false)

View File

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

View File

@ -1,6 +1,8 @@
package ru.dbotthepony.kstarbound.client.network.packets package ru.dbotthepony.kstarbound.client.network.packets
import com.google.gson.JsonObject 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.client.network.ClientConnection
import ru.dbotthepony.kstarbound.json.readJsonObject import ru.dbotthepony.kstarbound.json.readJsonObject
import ru.dbotthepony.kstarbound.json.writeJsonObject import ru.dbotthepony.kstarbound.json.writeJsonObject
@ -8,11 +10,13 @@ import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.entities.WorldObject
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.UUID
class SpawnWorldObjectPacket(val data: JsonObject) : IClientPacket { class SpawnWorldObjectPacket(val uuid: UUID, val data: JsonObject) : IClientPacket {
constructor(stream: DataInputStream) : this(stream.readJsonObject()) constructor(stream: DataInputStream) : this(stream.readUUID(), stream.readJsonObject())
override fun write(stream: DataOutputStream) { override fun write(stream: DataOutputStream) {
stream.writeUUID(uuid)
stream.writeJsonObject(data) stream.writeJsonObject(data)
} }
@ -20,8 +24,8 @@ class SpawnWorldObjectPacket(val data: JsonObject) : IClientPacket {
connection.client.mailbox.execute { connection.client.mailbox.execute {
val world = connection.client.world ?: return@execute val world = connection.client.world ?: return@execute
val obj = WorldObject.fromJson(data) val obj = WorldObject.fromJson(data)
val chunk = world.chunkMap[world.geometry.chunkFromCell(obj.pos)] ?: return@execute obj.uuid = uuid
chunk.addObject(obj) obj.spawn(world)
} }
} }
} }

View File

@ -65,6 +65,10 @@ class ClientWorld(
return geometry.loopY || value in 0 .. renderRegionsY 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 RenderRegion(val x: Int, val y: Int) {
inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) { inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) {
val bakedMeshes = ArrayList<Pair<ConfiguredMesh<*>, RenderLayer.Point>>() val bakedMeshes = ArrayList<Pair<ConfiguredMesh<*>, RenderLayer.Point>>()
@ -87,7 +91,7 @@ class ClientWorld(
for (x in 0 until renderRegionWidth) { for (x in 0 until renderRegionWidth) {
for (y in 0 until renderRegionHeight) { for (y in 0 until renderRegionHeight) {
if (!inBounds(x, y)) continue if (!geometry.inBoundsCell(x, y)) continue
if (bakeTaskID != this.bakeTaskID) return@Supplier meshes if (bakeTaskID != this.bakeTaskID) return@Supplier meshes
val tile = view.getTile(x, y) val tile = view.getTile(x, y)
@ -241,9 +245,7 @@ class ClientWorld(
for (x in ix - paddingX .. ix + paddingX) { for (x in ix - paddingX .. ix + paddingX) {
for (y in iy - paddingY .. iy + paddingY) { 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) val index = renderRegionKey(ix, iy)
if (seen.add(index)) { if (seen.add(index)) {
lock.withLock { renderRegions[index]?.let(action)
renderRegions[index]?.let(action)
}
} }
} }
} else { } else {
val ix = pos.component1() / renderRegionWidth val ix = pos.component1() / renderRegionWidth
val iy = pos.component2() / renderRegionHeight 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) { 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) { ent.render(client, layers)
layers.add(RenderLayer.Overlay.point()) { ent.addLights(client.viewportLighting, client.viewportCellX, client.viewportCellY)
ent.render(client)
}
}
} }
} }

View File

@ -27,6 +27,10 @@ abstract class JsonDriven(val path: String) {
private val namedLazies = Object2ObjectOpenHashMap<String, ArrayList<LazyData<*>>>() private val namedLazies = Object2ObjectOpenHashMap<String, ArrayList<LazyData<*>>>()
protected val properties = JsonObject() protected val properties = JsonObject()
/**
* [JsonObject]s which define behavior of properties
*/
protected abstract fun defs(): Collection<JsonObject> protected abstract fun defs(): Collection<JsonObject>
protected open fun invalidate() { protected open fun invalidate() {

View File

@ -11,6 +11,7 @@ import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket 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.JoinWorldPacket
import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket
import ru.dbotthepony.kstarbound.network.packets.DisconnectPacket import ru.dbotthepony.kstarbound.network.packets.DisconnectPacket
@ -132,5 +133,6 @@ object PacketRegistry {
add(::TrackedPositionPacket) add(::TrackedPositionPacket)
add(::TrackedSizePacket) add(::TrackedSizePacket)
add(::SpawnWorldObjectPacket) add(::SpawnWorldObjectPacket)
add(::ForgetEntityPacket)
} }
} }

View File

@ -1,12 +1,15 @@
package ru.dbotthepony.kstarbound.server.network package ru.dbotthepony.kstarbound.server.network
import io.netty.channel.ChannelHandlerContext 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.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket 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.client.network.packets.SpawnWorldObjectPacket
import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide 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.StarboundServer
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.ChunkPos 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.* import java.util.*
class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type, UUID(0L, 0L)) { 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<ChunkPos, ServerWorld.ITicket>() private val tickets = Object2ObjectOpenHashMap<ChunkPos, ServerWorld.ITicket>()
private val sentChunks = ObjectOpenHashSet<ChunkPos>() private val pendingSend = ObjectLinkedOpenHashSet<ChunkPos>()
private var needsToRecomputeTrackedChunks = true 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() { fun onLeaveWorld() {
tickets.values.forEach { it.cancel() } tickets.values.forEach { it.cancel() }
tickets.clear() tickets.clear()
sentChunks.clear() pendingSend.clear()
} }
private fun recomputeTrackedChunks() { private fun recomputeTrackedChunks() {
@ -77,6 +99,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
for ((pos, ticket) in itr) { for ((pos, ticket) in itr) {
if (pos !in tracked) { if (pos !in tracked) {
send(ForgetChunkPacket(pos))
pendingSend.remove(pos)
ticket.cancel() ticket.cancel()
itr.remove() itr.remove()
} }
@ -84,7 +108,10 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
for (pos in tracked) { for (pos in tracked) {
if (pos !in tickets) { 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() recomputeTrackedChunks()
} }
for (pos in tickets.keys) { val itr = pendingSend.iterator()
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()
for (pos in itr) { for (pos in itr) {
if (pos !in tickets) { val chunk = world.chunkMap[pos] ?: continue
send(ForgetChunkPacket(pos)) send(ChunkCellsPacket(chunk))
itr.remove() itr.remove()
}
} }
} }

View File

@ -3,9 +3,9 @@ package ru.dbotthepony.kstarbound.server.world
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
interface IChunkSaver { interface IChunkSaver {
fun saveCells(pos: ChunkPos, data: Object2DArray<out AbstractCell>) fun saveCells(pos: ChunkPos, data: Object2DArray<out AbstractCell>)
fun saveObjects(pos: ChunkPos, data: Collection<WorldObject>) fun saveEntities(pos: ChunkPos, data: Collection<AbstractEntity>)
} }

View File

@ -5,19 +5,20 @@ import ru.dbotthepony.kommons.core.KOptional
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.entities.WorldObject
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
interface IChunkSource { interface IChunkSource {
fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>>
fun getEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<WorldObject>>> fun getEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>>
object Void : IChunkSource { object Void : IChunkSource {
override fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> { override fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> {
return CompletableFuture.completedFuture(KOptional.of(Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY))) return CompletableFuture.completedFuture(KOptional.of(Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY)))
} }
override fun getEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<WorldObject>>> { override fun getEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>> {
return CompletableFuture.completedFuture(KOptional.of(emptyList())) return CompletableFuture.completedFuture(KOptional.of(emptyList()))
} }
} }

View File

@ -12,6 +12,7 @@ import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.MutableCell import ru.dbotthepony.kstarbound.world.api.MutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.entities.WorldObject
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
@ -43,7 +44,7 @@ class LegacyChunkSource(val db: BTreeDB) : IChunkSource {
} }
} }
override fun getEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<WorldObject>>> { override fun getEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>> {
return CompletableFuture.supplyAsync { return CompletableFuture.supplyAsync {
val chunkX = pos.x val chunkX = pos.x
val chunkY = pos.y val chunkY = pos.y
@ -52,7 +53,7 @@ class LegacyChunkSource(val db: BTreeDB) : IChunkSource {
val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data), Inflater()))) val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data), Inflater())))
val i = reader.readVarInt() val i = reader.readVarInt()
val objects = ArrayList<WorldObject>() val objects = ArrayList<AbstractEntity>()
for (i2 in 0 until i) { for (i2 in 0 until i) {
val obj = VersionedJson(reader) val obj = VersionedJson(reader)

View File

@ -3,6 +3,8 @@ package ru.dbotthepony.kstarbound.server.world
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet 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.collect.chainOptionalFutures
import ru.dbotthepony.kommons.core.KOptional import ru.dbotthepony.kommons.core.KOptional
import ru.dbotthepony.kstarbound.Starbound 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.server.network.ServerConnection
import ru.dbotthepony.kstarbound.util.ExecutionSpinner import ru.dbotthepony.kstarbound.util.ExecutionSpinner
import ru.dbotthepony.kstarbound.world.ChunkPos 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.World
import ru.dbotthepony.kstarbound.world.WorldGeometry 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.Collections
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.RejectedExecutionException import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.LockSupport
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Consumer import java.util.function.Consumer
import java.util.function.Supplier import java.util.function.Supplier
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
@ -68,6 +77,7 @@ class ServerWorld(
val spinner = ExecutionSpinner(mailbox, ::spin, Starbound.TICK_TIME_ADVANCE_NANOS) val spinner = ExecutionSpinner(mailbox, ::spin, Starbound.TICK_TIME_ADVANCE_NANOS)
val thread = Thread(spinner, "Starbound Server World $seed") val thread = Thread(spinner, "Starbound Server World $seed")
val ticketListLock = ReentrantLock()
@Volatile @Volatile
var isClosed: Boolean = false var isClosed: Boolean = false
@ -114,10 +124,14 @@ class ServerWorld(
override val isRemote: Boolean override val isRemote: Boolean
get() = false get() = false
override fun thinkInner() { override fun isSameThread(): Boolean {
lock.withLock { return Thread.currentThread() === thread
internalPlayers.forEach { it.tick() } }
override fun thinkInner() {
internalPlayers.forEach { if (!isClosed) it.tick() }
ticketListLock.withLock {
ticketLists.removeIf { ticketLists.removeIf {
val valid = it.tick() val valid = it.tick()
@ -128,7 +142,15 @@ class ServerWorld(
val chunk = chunkMap[it.pos] val chunk = chunkMap[it.pos]
if (chunk != null) { if (chunk != null) {
val unloadable = chunk.entities.filter { it.isApplicableForUnloading }
saver?.saveCells(it.pos, chunk.copyCells()) saver?.saveCells(it.pos, chunk.copyCells())
saver?.saveEntities(it.pos, unloadable)
unloadable.forEach {
it.remove()
}
chunkMap.remove(it.pos) chunkMap.remove(it.pos)
} }
} }
@ -168,6 +190,11 @@ class ServerWorld(
val isCanceled: Boolean val isCanceled: Boolean
val pos: ChunkPos val pos: ChunkPos
val id: Int val id: Int
val chunk: ServerChunk?
fun addListener(listener: IChunkListener)
fun removeListener(listener: IChunkListener)
} }
interface ITimedTicket : ITicket, Comparable<ITimedTicket> { interface ITimedTicket : ITicket, Comparable<ITimedTicket> {
@ -181,7 +208,7 @@ class ServerWorld(
} }
} }
private inner class TicketList(val pos: ChunkPos) { private inner class TicketList(val pos: ChunkPos) : IChunkListener, IChunkMapListener<ServerChunk> {
constructor(pos: Long) : this(ChunkPos(pos)) constructor(pos: Long) : this(ChunkPos(pos))
private var first = true private var first = true
@ -189,6 +216,7 @@ class ServerWorld(
private val temporary = ObjectAVLTreeSet<TimedTicket>() private val temporary = ObjectAVLTreeSet<TimedTicket>()
private var ticks = 0 private var ticks = 0
private var nextTicketID = AtomicInteger() private var nextTicketID = AtomicInteger()
private var weAreResponsibleForLoadingTheChunk = false
val isValid: Boolean val isValid: Boolean
get() = temporary.isNotEmpty() || permanent.isNotEmpty() get() = temporary.isNotEmpty() || permanent.isNotEmpty()
@ -205,7 +233,34 @@ class ServerWorld(
return temporary.isNotEmpty() || permanent.isNotEmpty() 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 id: Int = nextTicketID.getAndIncrement()
final override val pos: ChunkPos final override val pos: ChunkPos
get() = this@TicketList.pos get() = this@TicketList.pos
@ -218,8 +273,11 @@ class ServerWorld(
if (geometry.x.inBoundsChunk(pos.x) && geometry.y.inBoundsChunk(pos.y)) { if (geometry.x.inBoundsChunk(pos.x) && geometry.y.inBoundsChunk(pos.y)) {
ticketLists.add(this@TicketList) ticketLists.add(this@TicketList)
chunkMap.addListener(this@TicketList)
if (chunkProviders.isNotEmpty() && chunkMap[pos] == null) {
weAreResponsibleForLoadingTheChunk = true
if (chunkProviders.isNotEmpty()) {
chainOptionalFutures(chunkProviders) chainOptionalFutures(chunkProviders)
{ if (!isValid) CompletableFuture.completedFuture(KOptional.empty()) else it.getTiles(pos) } { if (!isValid) CompletableFuture.completedFuture(KOptional.empty()) else it.getTiles(pos) }
.thenAccept(Consumer { tiles -> .thenAccept(Consumer { tiles ->
@ -234,7 +292,7 @@ class ServerWorld(
ents.ifPresent { ents.ifPresent {
for (obj in it) { for (obj in it) {
chunk.addObject(obj) obj.spawn(this@ServerWorld)
} }
} }
}, mailbox) }, mailbox)
@ -250,11 +308,40 @@ class ServerWorld(
lock.withLock { lock.withLock {
if (isCanceled) return if (isCanceled) return
isCanceled = true isCanceled = true
chunk?.entities?.forEach { e -> listeners.forEach { it.onEntityRemoved(e) } }
onCancel() onCancel()
} }
} }
protected abstract fun onCancel() protected abstract fun onCancel()
final override val chunk: ServerChunk?
get() = chunkMap[pos]
private val listeners = ReferenceLinkedOpenHashSet<IChunkListener>()
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() { inner class Ticket : AbstractTicket() {

View File

@ -11,8 +11,10 @@ import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess
import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
import ru.dbotthepony.kstarbound.world.entities.TileEntity
import java.util.concurrent.CompletableFuture
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
/** /**
@ -44,8 +46,9 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
var backgroundChangeset = 0 var backgroundChangeset = 0
private set private set
val entities = ReferenceOpenHashSet<Entity>() val entities = ReferenceOpenHashSet<AbstractEntity>()
val objects = ReferenceOpenHashSet<WorldObject>() val dynamicEntities = ReferenceOpenHashSet<DynamicEntity>()
val tileEntities = ReferenceOpenHashSet<TileEntity>()
protected val subscribers = ObjectArraySet<IChunkListener>() protected val subscribers = ObjectArraySet<IChunkListener>()
// local cells' tile access // local cells' tile access
@ -136,7 +139,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
changeset++ changeset++
cellChangeset++ cellChangeset++
subscribers.forEach { it.cellChanges(x, y, cell) } subscribers.forEach { it.onCellChanges(x, y, cell) }
} }
protected inline fun forEachNeighbour(block: (This) -> Unit) { protected inline fun forEachNeighbour(block: (This) -> Unit) {
@ -154,53 +157,69 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
return world.randomLongFor(x or pos.x shl CHUNK_SIZE_BITS, y or pos.y shl CHUNK_SIZE_BITS) return world.randomLongFor(x or pos.x shl CHUNK_SIZE_BITS, y or pos.y shl CHUNK_SIZE_BITS)
} }
fun addListener(subscriber: IChunkListener) { fun addListener(subscriber: IChunkListener): Boolean {
subscribers.add(subscriber) return subscribers.add(subscriber)
} }
fun removeListener(subscriber: IChunkListener) { fun removeListener(subscriber: IChunkListener): Boolean {
subscribers.remove(subscriber) return subscribers.remove(subscriber)
} }
fun addEntity(entity: Entity) { fun addEntity(entity: AbstractEntity) {
world.lock.withLock { world.lock.withLock {
if (!entities.add(entity)) { if (!entities.add(entity))
throw IllegalArgumentException("Already having having entity $entity") throw IllegalArgumentException("Already having having entity $entity")
}
if (entity is TileEntity)
tileEntities.add(entity)
if (entity is DynamicEntity)
dynamicEntities.add(entity)
changeset++ changeset++
subscribers.forEach { it.onEntityAdded(entity) } subscribers.forEach { it.onEntityAdded(entity) }
} }
} }
fun transferEntity(entity: Entity, otherChunk: Chunk<*, *>) { fun transferEntity(entity: AbstractEntity, otherChunk: Chunk<*, *>) {
world.lock.withLock { world.lock.withLock {
if (otherChunk == this) if (otherChunk == this)
throw IllegalArgumentException("what?") throw IllegalArgumentException("what?")
if (this::class.java != otherChunk::class.java) { if (world != otherChunk.world)
throw IllegalArgumentException("Incompatible types: $this !is $otherChunk") throw IllegalArgumentException("Chunks belong to different worlds: this: $this / other: $otherChunk")
}
if (!entities.add(entity)) {
throw IllegalArgumentException("Already containing $entity")
}
changeset++ 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) } otherChunk.subscribers.forEach { it.onEntityRemoved(entity) }
subscribers.forEach { it.onEntityAdded(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 { world.lock.withLock {
if (!entities.remove(entity)) { if (!entities.remove(entity))
throw IllegalArgumentException("Already not having entity $entity") throw IllegalArgumentException("Already not having entity $entity")
}
if (entity is TileEntity)
tileEntities.remove(entity)
if (entity is DynamicEntity)
dynamicEntities.remove(entity)
changeset++ changeset++
subscribers.forEach { it.onEntityRemoved(entity) } subscribers.forEach { it.onEntityRemoved(entity) }
@ -211,39 +230,8 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
return "${this::class.simpleName}(pos=$pos, entityCount=${entities.size}, world=$world)" return "${this::class.simpleName}(pos=$pos, entityCount=${entities.size}, world=$world)"
} }
fun addObject(obj: WorldObject) {
world.lock.withLock {
if (!objects.add(obj))
throw IllegalStateException("$this already has object $obj!")
if (!world.objects.add(obj))
throw IllegalStateException("World $world already has object $obj!")
obj.spawn(world)
subscribers.forEach { it.onObjectAdded(obj) }
}
}
fun removeObject(obj: WorldObject) {
world.lock.withLock {
if (!objects.remove(obj))
throw IllegalStateException("$this does not have object $obj!")
if (!world.objects.remove(obj))
throw IllegalStateException("World $world does not have object $obj!")
subscribers.forEach { it.onObjectRemoved(obj) }
}
}
open fun remove() { open fun remove() {
world.lock.withLock { world.lock.withLock {
for (obj in ObjectArrayList(objects)) {
if (!world.objects.remove(obj)) {
throw IllegalStateException("World $world does not have object $obj!")
}
}
for (ent in ObjectArrayList(entities)) { for (ent in ObjectArrayList(entities)) {
ent.chunk = null ent.chunk = null
} }

View File

@ -1,13 +1,18 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject
interface IChunkListener { fun interface IEntityAdditionListener {
fun onEntityAdded(entity: Entity) fun onEntityAdded(entity: AbstractEntity)
fun onEntityRemoved(entity: Entity)
fun onObjectAdded(obj: WorldObject)
fun onObjectRemoved(obj: WorldObject)
fun cellChanges(x: Int, y: Int, cell: ImmutableCell)
} }
fun interface IEntityRemovalListener {
fun onEntityRemoved(entity: AbstractEntity)
}
fun interface ICellChangeListener {
fun onCellChanges(x: Int, y: Int, cell: ImmutableCell)
}
interface IChunkListener : IEntityAdditionListener, IEntityRemovalListener, ICellChangeListener

View File

@ -1,9 +1,7 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.collect.filterNotNull import ru.dbotthepony.kommons.collect.filterNotNull
@ -17,8 +15,9 @@ import ru.dbotthepony.kstarbound.util.ParallelPerform
import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
import ru.dbotthepony.kstarbound.world.entities.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
@ -30,21 +29,11 @@ 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.concurrent.withLock
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>( abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(
val seed: Long, val seed: Long,
val geometry: WorldGeometry, val geometry: WorldGeometry,
) : ICellAccess, Closeable { ) : 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 background = TileView.Background(this)
val foreground = TileView.Foreground(this) val foreground = TileView.Foreground(this)
val mailbox = MailboxExecutorService() val mailbox = MailboxExecutorService()
@ -64,6 +53,11 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
return chunkMap.setCell(x, y, cell) return chunkMap.setCell(x, y, cell)
} }
interface IChunkMapListener<ChunkType : Chunk<*, *>> {
fun onChunkCreated(chunk: ChunkType) { }
fun onChunkRemoved(chunk: ChunkType) { }
}
abstract inner class ChunkMap : Iterable<ChunkType> { abstract inner class ChunkMap : Iterable<ChunkType> {
abstract operator fun get(x: Int, y: Int): ChunkType? abstract operator fun get(x: Int, y: Int): ChunkType?
abstract fun compute(x: Int, y: Int): ChunkType? abstract fun compute(x: Int, y: Int): ChunkType?
@ -77,15 +71,23 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
operator fun get(pos: ChunkPos) = get(pos.x, pos.y) operator fun get(pos: ChunkPos) = get(pos.x, pos.y)
protected val listeners = ReferenceOpenHashSet<IChunkMapListener<ChunkType>>()
fun addListener(listener: IChunkMapListener<ChunkType>) {
listeners.add(listener)
}
fun removeListener(listener: IChunkMapListener<ChunkType>) {
listeners.remove(listener)
}
protected fun create(x: Int, y: Int): ChunkType { protected fun create(x: Int, y: Int): ChunkType {
val pos = ChunkPos(x, y) val pos = ChunkPos(x, y)
val chunk = chunkFactory(pos) val chunk = chunkFactory(pos)
val orphanedInThisChunk = ArrayList<Entity>() val orphanedInThisChunk = ArrayList<AbstractEntity>()
for (ent in orphanedEntities) { for (ent in orphanedEntities) {
val (ex, ey) = ent.position if (ent.chunkPos == pos) {
if (geometry.x.chunkFromCell(ex) == x && geometry.y.chunkFromCell(ey) == y) {
orphanedInThisChunk.add(ent) orphanedInThisChunk.add(ent)
} }
} }
@ -94,6 +96,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
ent.chunk = chunk ent.chunk = chunk
} }
listeners.forEach { it.onChunkCreated(chunk) }
return chunk return chunk
} }
@ -120,11 +123,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
val index = ChunkPos.toLong(x, y) val index = ChunkPos.toLong(x, y)
val get = map[index] ?: create(x, y).also { map[index] = it }
val get = map[index] ?: lock.withLock {
map[index] ?: create(x, y).also { map[index] = it }
}
return get return get
} }
@ -137,16 +136,18 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val index = ChunkPos.toLong(cx, cy) val index = ChunkPos.toLong(cx, cy)
val get = map[index] ?: lock.withLock { val get = map[index] ?: create(cx, cy).also { map[index] = it }
map[index] ?: create(cx, cy).also { map[index] = it }
}
return get.setCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK, cell) return get.setCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK, cell)
} }
override fun remove(x: Int, y: Int) { override fun remove(x: Int, y: Int) {
lock.withLock { val index = ChunkPos.toLong(geometry.x.chunk(x), geometry.y.chunk(y))
map.remove(ChunkPos.toLong(geometry.x.chunk(x), geometry.y.chunk(y)))?.remove() val chunk = map.get(index)
if (chunk != null) {
chunk.remove()
listeners.forEach { it.onChunkRemoved(chunk) }
map.remove(index)
} }
} }
@ -168,7 +169,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
override fun compute(x: Int, y: Int): ChunkType? { override fun compute(x: Int, y: Int): ChunkType? {
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
return map[x, y] ?: lock.withLock { map[x, y] ?: create(x, y).also { existing.add(ChunkPos(x, y)); map[x, y] = it } } return map[x, y] ?: create(x, y).also { existing.add(ChunkPos(x, y)); map[x, y] = it }
} }
override fun getCell(x: Int, y: Int): AbstractCell { override fun getCell(x: Int, y: Int): AbstractCell {
@ -190,17 +191,17 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
return map[x, y] return map[x, y]
} }
@Suppress("NAME_SHADOWING")
override fun remove(x: Int, y: Int) { override fun remove(x: Int, y: Int) {
lock.withLock { val x = geometry.x.chunk(x)
val x = geometry.x.chunk(x) val y = geometry.y.chunk(y)
val y = geometry.y.chunk(y) val chunk = map[x, y]
val get = map[x, y]
if (get != null) { if (chunk != null) {
existing.remove(ChunkPos(x, y)) chunk.remove()
get.remove() listeners.forEach { it.onChunkRemoved(chunk) }
map[x, y] = null existing.remove(ChunkPos(x, y))
} map[x, y] = null
} }
} }
@ -229,40 +230,32 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
var gravity = Vector2d(0.0, -80.0) var gravity = Vector2d(0.0, -80.0)
abstract val isRemote: Boolean abstract val isRemote: Boolean
// used to synchronize read/writes to various world state stuff/memory structure // generic lock
val lock = ReentrantLock() val lock = ReentrantLock()
val orphanedEntities = ReferenceOpenHashSet<AbstractEntity>()
val entities = ReferenceOpenHashSet<AbstractEntity>()
val dynamicEntities = ReferenceOpenHashSet<DynamicEntity>()
val tileEntities = ReferenceOpenHashSet<TileEntity>()
abstract fun isSameThread(): Boolean
fun ensureSameThread() {
check(isSameThread()) { "Trying to access $this from ${Thread.currentThread()}" }
}
fun think() { fun think() {
try { try {
mailbox.executeQueuedTasks() 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() mailbox.executeQueuedTasks()
for (ent in entities) { entities.forEach { it.think() }
ent.thinkShared()
if (isRemote)
ent.thinkClient()
else
ent.thinkServer()
}
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
lock for (chunk in chunkMap) {
.withLock { ObjectArrayList(chunkMap.iterator()) } chunk.think()
.forEach { it.think() }
val objects = ObjectArrayList(objects)
for (ent in objects) {
ent.thinkShared()
if (isRemote)
ent.thinkClient()
else
ent.thinkServer()
} }
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
@ -274,10 +267,6 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
protected abstract fun thinkInner() protected abstract fun thinkInner()
val orphanedEntities = ReferenceOpenHashSet<Entity>()
val entities = ReferenceLinkedOpenHashSet<Entity>()
val objects = ReferenceLinkedOpenHashSet<WorldObject>()
protected abstract fun chunkFactory(pos: ChunkPos): ChunkType protected abstract fun chunkFactory(pos: ChunkPos): ChunkType
override fun close() { override fun close() {

View File

@ -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())) 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 { fun wrap(pos: ChunkPos): ChunkPos {
val x = this.x.chunk(pos.x) val x = this.x.chunk(pos.x)
val y = this.y.chunk(pos.y) val y = this.y.chunk(pos.y)

View File

@ -79,7 +79,7 @@ abstract class AbstractActorMovementController : AbstractMovementController() {
// this is set internally on each move step // this is set internally on each move step
final override var movementParameters: MovementParameters = MovementParameters.EMPTY final override var movementParameters: MovementParameters = MovementParameters.EMPTY
abstract var anchorEntity: Entity? abstract var anchorEntity: DynamicEntity?
var pathController: PathController? = null var pathController: PathController? = null
var groundMovementSustainTimer: GameTimer = GameTimer(0.0) var groundMovementSustainTimer: GameTimer = GameTimer(0.0)
@ -194,7 +194,7 @@ abstract class AbstractActorMovementController : AbstractMovementController() {
override fun move() { override fun move() {
// TODO: anchor entity // TODO: anchor entity
if (anchorEntity?.isRemoved == true) if (anchorEntity?.isSpawned != true)
anchorEntity = null anchorEntity = null
val anchorEntity = anchorEntity val anchorEntity = anchorEntity

View File

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

View File

@ -341,7 +341,7 @@ abstract class AbstractMovementController {
if (slopeCorrection) { if (slopeCorrection) {
// Starbound: First try separating with our ground sliding cheat. // 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 totalCorrection += separation.correction
checkBody += separation.correction checkBody += separation.correction
maxCollided = maxCollided.maxOf(separation.collisionType) 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 // KStarbound: if we got pushed into world geometry, then consider slide cheat didn't find a solution
if (separation.solutionFound) { 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 totalCorrection = Vector2d.ZERO
movingCollisionId = null movingCollisionId = null
for (i in 0 until Entity.SEPARATION_STEPS) { for (i in 0 until SEPARATION_STEPS) {
separation = collisionSeparate(checkBody, sorted, ignorePlatforms, maximumPlatformCorrection, false, Entity.SEPARATION_TOLERANCE) separation = collisionSeparate(checkBody, sorted, ignorePlatforms, maximumPlatformCorrection, false, SEPARATION_TOLERANCE)
totalCorrection += separation.correction totalCorrection += separation.correction
checkBody += separation.correction checkBody += separation.correction
maxCollided = maxCollided.maxOf(separation.collisionType) maxCollided = maxCollided.maxOf(separation.collisionType)
@ -388,8 +388,8 @@ abstract class AbstractMovementController {
checkBody = body checkBody = body
totalCorrection = -movement totalCorrection = -movement
for (i in 0 until Entity.SEPARATION_STEPS) { for (i in 0 until SEPARATION_STEPS) {
separation = collisionSeparate(checkBody, sorted, true, maximumPlatformCorrection, false, Entity.SEPARATION_TOLERANCE) separation = collisionSeparate(checkBody, sorted, true, maximumPlatformCorrection, false, SEPARATION_TOLERANCE)
totalCorrection += separation.correction totalCorrection += separation.correction
checkBody += separation.correction checkBody += separation.correction
maxCollided = maxCollided.maxOf(separation.collisionType) maxCollided = maxCollided.maxOf(separation.collisionType)
@ -408,7 +408,7 @@ abstract class AbstractMovementController {
movement = movement + totalCorrection, movement = movement + totalCorrection,
correction = totalCorrection, correction = totalCorrection,
isStuck = false, isStuck = false,
isOnGround = -totalCorrection.dot(determineGravity()) > Entity.SEPARATION_TOLERANCE, isOnGround = -totalCorrection.dot(determineGravity()) > SEPARATION_TOLERANCE,
movingCollisionId = movingCollisionId, movingCollisionId = movingCollisionId,
collisionType = maxCollided, collisionType = maxCollided,
// groundSlope = Vector2d.POSITIVE_Y, // groundSlope = Vector2d.POSITIVE_Y,
@ -489,4 +489,9 @@ abstract class AbstractMovementController {
return separation return separation
} }
companion object {
const val SEPARATION_STEPS = 3
const val SEPARATION_TOLERANCE = 0.001
}
} }

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import ru.dbotthepony.kstarbound.defs.player.ActorMovementModifiers
import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.World 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 val world: World<*, *> by entity::world
override var position: Vector2d by entity::position override var position: Vector2d by entity::position
override var actorMovementParameters: ActorMovementParameters = GlobalDefaults.actorMovementParameters override var actorMovementParameters: ActorMovementParameters = GlobalDefaults.actorMovementParameters
@ -63,5 +63,5 @@ class EntityActorMovementController(val entity: Entity) : AbstractActorMovementC
override val approachVelocityAngles: MutableList<ApproachVelocityAngleCommand> = ArrayList() override val approachVelocityAngles: MutableList<ApproachVelocityAngleCommand> = ArrayList()
override var movingDirection: Direction? = null override var movingDirection: Direction? = null
override var facingDirection: Direction? = null override var facingDirection: Direction? = null
override var anchorEntity: Entity? = null override var anchorEntity: DynamicEntity? = null
} }

View File

@ -5,7 +5,7 @@ import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.defs.MovementParameters import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.world.World 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 val world: World<*, *> by entity::world
override var position: Vector2d by entity::position override var position: Vector2d by entity::position
override var movementParameters: MovementParameters = GlobalDefaults.movementParameters override var movementParameters: MovementParameters = GlobalDefaults.movementParameters

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world.entities package ru.dbotthepony.kstarbound.world.entities
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.core.Either import ru.dbotthepony.kommons.core.Either
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2d 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.World
import ru.dbotthepony.kstarbound.world.physics.Poly 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 val movement = EntityMovementController(this)
override fun defs(): Collection<JsonObject> {
return emptyList()
}
init { init {
movement.movementParameters = movement.movementParameters.copy(collisionPoly = Either.left(Poly(AABB.rectangle(Vector2d.ZERO, 0.75, 0.75)))) movement.movementParameters = movement.movementParameters.copy(collisionPoly = Either.left(Poly(AABB.rectangle(Vector2d.ZERO, 0.75, 0.75))))
} }

View File

@ -1,12 +1,20 @@
package ru.dbotthepony.kstarbound.world.entities package ru.dbotthepony.kstarbound.world.entities
import com.google.gson.JsonObject
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
class PlayerEntity(world: World<*, *>) : Entity(world) { class PlayerEntity() : DynamicEntity("/") {
override val movement = EntityActorMovementController(this) override val movement = EntityActorMovementController(this)
override val isApplicableForUnloading: Boolean
get() = false
override fun defs(): Collection<JsonObject> {
return emptyList()
}
init { init {
movement.actorMovementParameters = movement.actorMovementParameters.merge( movement.actorMovementParameters = movement.actorMovementParameters.merge(
Starbound.gson.fromJson(""" Starbound.gson.fromJson("""

View File

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

View File

@ -11,6 +11,8 @@ import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound 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.client.world.ClientWorld
import ru.dbotthepony.kstarbound.defs.Drawable import ru.dbotthepony.kstarbound.defs.Drawable
import ru.dbotthepony.kstarbound.defs.JsonDriven 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.json.set
import ru.dbotthepony.kstarbound.world.Side import ru.dbotthepony.kstarbound.world.Side
import ru.dbotthepony.kstarbound.world.LightCalculator 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.World
import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.api.TileColor
import kotlin.properties.Delegates import kotlin.properties.Delegates
open class WorldObject( open class WorldObject(
val prototype: Registry.Entry<ObjectDefinition>, val prototype: Registry.Entry<ObjectDefinition>,
val pos: Vector2i, ) : TileEntity(prototype.file?.computeDirectory() ?: "/") {
) : JsonDriven(prototype.file?.computeDirectory() ?: "/") {
fun deserialize(data: JsonObject) { fun deserialize(data: JsonObject) {
direction = data.get("direction", directions) { Side.LEFT } direction = data.get("direction", directions) { Side.LEFT }
orientationIndex = data.get("orientationIndex", -1) orientationIndex = data.get("orientationIndex", -1)
@ -48,7 +50,7 @@ open class WorldObject(
fun serialize(): JsonObject { fun serialize(): JsonObject {
val into = JsonObject() val into = JsonObject()
into["name"] = prototype.key into["name"] = prototype.key
into["tilePosition"] = vectors.toJsonTree(pos) into["tilePosition"] = vectors.toJsonTree(position)
into["direction"] = directions.toJsonTree(direction) into["direction"] = directions.toJsonTree(direction)
into["orientationIndex"] = orientationIndex into["orientationIndex"] = orientationIndex
into["interactive"] = interactive into["interactive"] = interactive
@ -61,10 +63,6 @@ open class WorldObject(
return into return into
} }
val mailbox = MailboxExecutorService()
var world: World<*, *> by Delegates.notNull()
private set
// //
// internal runtime properties // internal runtime properties
// //
@ -83,11 +81,6 @@ open class WorldObject(
private var frameTimer = 0.0 private var frameTimer = 0.0
val flickerPeriod = prototype.value.flickerPeriod?.copy() val flickerPeriod = prototype.value.flickerPeriod?.copy()
var isRemoved = false
private set
var isSpawned = false
private set
// //
// top level properties // top level properties
// //
@ -139,33 +132,12 @@ open class WorldObject(
super.invalidate() super.invalidate()
} }
protected open fun innerSpawn() {} override fun thinkShared() {
protected open fun innerRemove() {} super.thinkShared()
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()
flickerPeriod?.update(Starbound.TICK_TIME_ADVANCE, world.random) flickerPeriod?.update(Starbound.TICK_TIME_ADVANCE, world.random)
} }
open fun thinkClient() { override fun thinkRemote() {
val orientation = orientation val orientation = orientation
if (orientation != null) { if (orientation != null) {
@ -174,10 +146,6 @@ open class WorldObject(
} }
} }
open fun thinkServer() {
}
val orientation: ObjectOrientation? get() { val orientation: ObjectOrientation? get() {
return orientations.getOrNull(orientationIndex) return orientations.getOrNull(orientationIndex)
} }
@ -193,7 +161,7 @@ open class WorldObject(
?: ImmutableMap.of() ?: ImmutableMap.of()
} }
fun addLights(lightCalculator: LightCalculator, xOffset: Int, yOffset: Int) { override fun addLights(lightCalculator: LightCalculator, xOffset: Int, yOffset: Int) {
var color = lightColors[color.lowercase] var color = lightColors[color.lowercase]
if (color != null) { if (color != null) {
@ -202,7 +170,16 @@ open class WorldObject(
color *= sample 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 { 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 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 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) result.deserialize(content)
return result return result
} }