More grinding on entities and their networking
This commit is contained in:
parent
cae0a89ea2
commit
0eea0fa13f
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>)
|
||||||
}
|
}
|
||||||
|
@ -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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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))))
|
||||||
}
|
}
|
||||||
|
@ -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("""
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user