Universe thread, more synchronization (to allow more pararllel code), initiate player movement test once again

This commit is contained in:
DBotThePony 2023-11-11 14:25:31 +07:00
parent 8af66ff359
commit 1526a1c127
Signed by: DBot
GPG Key ID: DCC23B5715498507
10 changed files with 143 additions and 61 deletions

View File

@ -13,6 +13,7 @@ import ru.dbotthepony.kstarbound.json.VersionedJson
import ru.dbotthepony.kstarbound.io.readVarInt import ru.dbotthepony.kstarbound.io.readVarInt
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.world.api.MutableCell import ru.dbotthepony.kstarbound.world.api.MutableCell
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.entities.WorldObject
import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2d
import java.io.BufferedInputStream import java.io.BufferedInputStream
@ -68,13 +69,20 @@ fun main() {
Starbound.terminateLoading = true Starbound.terminateLoading = true
} }
var ply: PlayerEntity? = null
Starbound.onInitialize { Starbound.onInitialize {
ply = PlayerEntity(client.world!!)
ply!!.position = Vector2d(225.0, 745.0)
ply!!.spawn()
//for (chunkX in 17 .. 18) { //for (chunkX in 17 .. 18) {
//for (chunkX in 14 .. 24) { //for (chunkX in 14 .. 24) {
for (chunkX in 0 .. 100) { for (chunkX in 0 .. 100) {
//for (chunkX in 0 .. 17) { //for (chunkX in 0 .. 17) {
// for (chunkY in 21 .. 21) { // for (chunkY in 21 .. 21) {
for (chunkY in 18 .. 24) { for (chunkY in 0 .. 24) {
val data = db.read(byteArrayOf(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())) val data = db.read(byteArrayOf(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte()))
val data2 = db.read(byteArrayOf(2, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())) val data2 = db.read(byteArrayOf(2, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte()))
@ -121,7 +129,7 @@ fun main() {
val rand = Random() val rand = Random()
for (i in 0 until 128) { for (i in 0 until 0) {
val item = ItemEntity(client.world!!, Registries.items.keys.values.random().value) val item = ItemEntity(client.world!!, Registries.items.keys.values.random().value)
item.position = Vector2d(225.0 - i, 785.0) item.position = Vector2d(225.0 - i, 785.0)
@ -177,23 +185,22 @@ fun main() {
} }
while (client.renderFrame()) { while (client.renderFrame()) {
Starbound.pollCallbacks() /*client.camera.pos += Vector2d(
//ent.think(client.frameRenderTime)
//client.camera.pos.x = ent.position.x.toFloat()
//client.camera.pos.y = ent.position.y.toFloat()
client.camera.pos += Vector2d(
(if (client.input.KEY_A_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_D_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0), (if (client.input.KEY_A_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_D_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0),
(if (client.input.KEY_W_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_S_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) (if (client.input.KEY_W_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_S_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0)
) )*/
//println(client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1) if (ply != null) {
client.camera.pos = ply!!.position
ply!!.velocity += Vector2d(
(if (client.input.KEY_A_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_D_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0),
(if (client.input.KEY_W_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_S_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0)
) * 8.0
}
if (client.input.KEY_ESCAPE_PRESSED) { if (client.input.KEY_ESCAPE_PRESSED) {
glfwSetWindowShouldClose(client.window, true) glfwSetWindowShouldClose(client.window, true)
} }
//ent.wantsToDuck = client.input.KEY_DOWN_DOWN
} }
} }

View File

@ -53,6 +53,7 @@ import ru.dbotthepony.kstarbound.util.JVMTimeSource
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.util.HashTableInterner import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.WriteOnce import ru.dbotthepony.kstarbound.util.WriteOnce
import ru.dbotthepony.kstarbound.util.filterNotNull import ru.dbotthepony.kstarbound.util.filterNotNull
import ru.dbotthepony.kstarbound.util.set import ru.dbotthepony.kstarbound.util.set
@ -63,6 +64,7 @@ import java.lang.ref.Cleaner
import java.text.DateFormat import java.text.DateFormat
import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask import java.util.concurrent.ForkJoinTask
import java.util.concurrent.locks.LockSupport
import java.util.function.BiConsumer import java.util.function.BiConsumer
import java.util.function.BinaryOperator import java.util.function.BinaryOperator
import java.util.function.Function import java.util.function.Function
@ -76,8 +78,16 @@ object Starbound : ISBFileLocator {
const val TICK_TIME_ADVANCE = 0.01666666666666664 const val TICK_TIME_ADVANCE = 0.01666666666666664
const val TICK_TIME_ADVANCE_NANOS = 16_666_666L const val TICK_TIME_ADVANCE_NANOS = 16_666_666L
val thread = Thread(::universeThread, "Starbound Universe")
val mailbox = MailboxExecutorService(thread)
init {
thread.isDaemon = true
thread.start()
}
val CLEANER: Cleaner = Cleaner.create { val CLEANER: Cleaner = Cleaner.create {
val t = Thread(it, "Starbound Global Cleaner Thread") val t = Thread(it, "Starbound Global Cleaner")
t.isDaemon = true t.isDaemon = true
t.priority = 2 t.priority = 2
t t
@ -264,6 +274,7 @@ object Starbound : ISBFileLocator {
} }
} }
@Volatile
var initializing = false var initializing = false
private set private set
var initialized = false var initialized = false
@ -452,9 +463,9 @@ object Starbound : ISBFileLocator {
} }
initializing = true initializing = true
Thread({ doInitialize(log, parallel) }, "Asset Loader").also {
it.isDaemon = true mailbox.submit {
it.start() doInitialize(log, parallel)
} }
} }
@ -466,13 +477,19 @@ object Starbound : ISBFileLocator {
} }
} }
fun pollCallbacks() { private fun universeThread() {
if (initialized && initCallbacks.isNotEmpty()) { while (true) {
for (callback in initCallbacks) { mailbox.executeQueuedTasks()
callback()
if (initialized && initCallbacks.isNotEmpty()) {
for (callback in initCallbacks) {
callback()
}
initCallbacks.clear()
} }
initCallbacks.clear() LockSupport.park()
} }
} }
} }

View File

@ -80,6 +80,7 @@ import java.util.concurrent.locks.LockSupport
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.IntConsumer import java.util.function.IntConsumer
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.concurrent.withLock
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -818,9 +819,11 @@ class StarboundClient : Closeable {
viewportLighting.clear() viewportLighting.clear()
val viewportLightingMem = viewportLightingMem val viewportLightingMem = viewportLightingMem
world.addLayers( world.lock.withLock {
layers = layers, world.addLayers(
size = viewportRectangle) layers = layers,
size = viewportRectangle)
}
if (viewportLightingMem != null && !fullbright) { if (viewportLightingMem != null && !fullbright) {
val spos = screenToWorld(mouseCoordinates) val spos = screenToWorld(mouseCoordinates)

View File

@ -29,6 +29,7 @@ import ru.dbotthepony.kvector.vector.Vector2f
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
import java.util.concurrent.Callable import java.util.concurrent.Callable
import java.util.concurrent.Future import java.util.concurrent.Future
import kotlin.concurrent.withLock
class ClientWorld( class ClientWorld(
val client: StarboundClient, val client: StarboundClient,
@ -204,11 +205,7 @@ class ClientWorld(
val renderRegions = Long2ObjectOpenHashMap<RenderRegion>() val renderRegions = Long2ObjectOpenHashMap<RenderRegion>()
fun renderRegionKey(x: Int, y: Int): Long { fun renderRegionKey(x: Int, y: Int): Long {
if (size == null) { return positiveModulo(x, renderRegionsX).toLong() shl 32 or positiveModulo(y, renderRegionsY).toLong()
return x.toLong() shl 32 or y.toLong()
} else {
return positiveModulo(x, renderRegionsX).toLong() shl 32 or positiveModulo(y, renderRegionsY).toLong()
}
} }
/** /**
@ -242,13 +239,18 @@ class ClientWorld(
val index = renderRegionKey(ix, iy) val index = renderRegionKey(ix, iy)
if (seen.add(index)) { if (seen.add(index)) {
renderRegions[index]?.let(action) lock.withLock {
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
renderRegions[renderRegionKey(ix, iy)]?.let(action)
lock.withLock {
renderRegions[renderRegionKey(ix, iy)]?.let(action)
}
} }
} }

View File

@ -25,10 +25,20 @@ class KOptional<T> private constructor(private val _value: T, val isPresent: Boo
throw NoSuchElementException("No value is present") throw NoSuchElementException("No value is present")
} }
inline fun ifPresent(block: (T) -> Unit) { inline fun ifPresent(block: (T) -> Unit): KOptional<T> {
if (isPresent) { if (isPresent) {
block.invoke(value) block.invoke(value)
} }
return this
}
inline fun ifNotPresent(block: () -> Unit): KOptional<T> {
if (!isPresent) {
block.invoke()
}
return this
} }
inline fun <R> map(block: (T) -> R): KOptional<R> { inline fun <R> map(block: (T) -> R): KOptional<R> {

View File

@ -142,7 +142,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
protected open fun onEntityRemoved(entity: Entity) { } protected open fun onEntityRemoved(entity: Entity) { }
fun addEntity(entity: Entity) { fun addEntity(entity: Entity) {
world.entityListsLock.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")
} }
@ -153,7 +153,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
} }
fun transferEntity(entity: Entity, otherChunk: Chunk<*, *>) { fun transferEntity(entity: Entity, otherChunk: Chunk<*, *>) {
world.entityListsLock.withLock { world.lock.withLock {
if (otherChunk == this) if (otherChunk == this)
throw IllegalArgumentException("what?") throw IllegalArgumentException("what?")
@ -176,7 +176,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
} }
fun removeEntity(entity: Entity) { fun removeEntity(entity: Entity) {
world.entityListsLock.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")
} }

View File

@ -1,7 +1,5 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import com.google.common.collect.ImmutableList
import it.unimi.dsi.fastutil.ints.IntList
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.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet
@ -16,19 +14,18 @@ import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.entities.WorldObject
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.getBlockPlatforms import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms
import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares
import ru.dbotthepony.kvector.api.IStruct2d import ru.dbotthepony.kvector.api.IStruct2d
import ru.dbotthepony.kvector.api.IStruct2i import ru.dbotthepony.kvector.api.IStruct2i
import ru.dbotthepony.kvector.arrays.Object2DArray import ru.dbotthepony.kvector.arrays.Object2DArray
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.util2d.AABBi
import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
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,
@ -117,7 +114,14 @@ 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 (!this@World.x.inBoundsChunk(x) || !this@World.y.inBoundsChunk(y)) return null if (!this@World.x.inBoundsChunk(x) || !this@World.y.inBoundsChunk(y)) return null
return map[ChunkPos.toLong(x, y)] ?: create(x, y).also { map[ChunkPos.toLong(x, y)] = it }
val index = ChunkPos.toLong(x, y)
val get = map[index] ?: lock.withLock {
map[index] ?: create(x, y).also { map[index] = it }
}
return get
} }
override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean { override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
@ -126,7 +130,14 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val iy = this@World.y.cell(y) val iy = this@World.y.cell(y)
val cx = this@World.x.chunkFromCell(ix) val cx = this@World.x.chunkFromCell(ix)
val cy = this@World.y.chunkFromCell(iy) val cy = this@World.y.chunkFromCell(iy)
return (map[ChunkPos.toLong(cx, cy)] ?: create(cx, cy).also { map[ChunkPos.toLong(cx, cy)] = it }).setCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK, cell)
val index = ChunkPos.toLong(cx, cy)
val get = map[index] ?: lock.withLock {
map[index] ?: create(cx, cy).also { map[index] = it }
}
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) {
@ -138,7 +149,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
private val map = Object2DArray.nulls<ChunkType>(divideUp(size.x, CHUNK_SIZE), divideUp(size.y, CHUNK_SIZE)) private val map = Object2DArray.nulls<ChunkType>(divideUp(size.x, CHUNK_SIZE), divideUp(size.y, CHUNK_SIZE))
private fun getRaw(x: Int, y: Int): ChunkType { private fun getRaw(x: Int, y: Int): ChunkType {
return map[x, y] ?: create(x, y).also { map[x, y] = it } return map[x, y] ?: lock.withLock { map[x, y] ?: create(x, y).also { map[x, y] = it } }
} }
override fun compute(x: Int, y: Int): ChunkType? { override fun compute(x: Int, y: Int): ChunkType? {
@ -176,13 +187,13 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
var gravity = Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION) var gravity = Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION)
abstract val isClient: Boolean abstract val isClient: Boolean
// used by world chunks to synchronize entity additions/transfer // used to synchronize read/writes to various world state stuff/memory structure
val entityListsLock = ReentrantLock() val lock = ReentrantLock()
fun think() { fun think() {
try { try {
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
val entities = ObjectArrayList(entities) val entities = lock.withLock { ObjectArrayList(entities) }
ForkJoinPool.commonPool().submit(ParallelPerform(entities.spliterator(), Entity::move)).join() ForkJoinPool.commonPool().submit(ParallelPerform(entities.spliterator(), Entity::move)).join()
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
@ -195,7 +206,8 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
ent.thinkServer() ent.thinkServer()
} }
val objects = ObjectArrayList(objects) mailbox.executeQueuedTasks()
val objects = lock.withLock { ObjectArrayList(objects) }
for (ent in objects) { for (ent in objects) {
ent.thinkShared() ent.thinkShared()

View File

@ -38,7 +38,7 @@ abstract class Entity(val world: World<*, *>) {
val oldChunk = field val oldChunk = field
field = value field = value
world.entityListsLock.withLock { world.lock.withLock {
if (oldChunk == null && value != null) { if (oldChunk == null && value != null) {
world.orphanedEntities.remove(this) world.orphanedEntities.remove(this)
value.addEntity(this) value.addEntity(this)
@ -107,11 +107,14 @@ abstract class Entity(val world: World<*, *>) {
throw IllegalStateException("Already spawned") throw IllegalStateException("Already spawned")
isSpawned = true isSpawned = true
world.entities.add(this)
chunk = world.chunkMap[world.chunkFromCell(position)]
if (chunk == null) { world.mailbox.execute {
world.orphanedEntities.add(this) world.entities.add(this)
chunk = world.chunkMap[world.chunkFromCell(position)]
if (chunk == null) {
world.orphanedEntities.add(this)
}
} }
} }
@ -123,8 +126,10 @@ abstract class Entity(val world: World<*, *>) {
mailbox.shutdownNow() mailbox.shutdownNow()
if (isSpawned) { if (isSpawned) {
world.entities.remove(this) world.mailbox.execute {
chunk?.removeEntity(this) world.entities.remove(this)
chunk?.removeEntity(this)
}
} }
} }
@ -163,7 +168,7 @@ abstract class Entity(val world: World<*, *>) {
return return
} }
val steps = roundTowardsPositiveInfinity(velocity.length / 30.0 / hitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().let { it.width.coerceAtLeast(it.height).coerceAtLeast(0.1) }) val steps = roundTowardsPositiveInfinity(velocity.length / 10.0 / hitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().let { it.width.coerceAtLeast(it.height).coerceAtLeast(0.1) })
val dt = Starbound.TICK_TIME_ADVANCE / steps val dt = Starbound.TICK_TIME_ADVANCE / steps
for (step in 0 until steps) { for (step in 0 until steps) {

View File

@ -0,0 +1,19 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.physics.Poly
class PlayerEntity(world: World<*, *>) : Entity(world) {
init {
movementParameters = GlobalDefaults.playerMovementParameters
GlobalDefaults.playerMovementParameters.standingPoly.ifPresent {
//hitboxes.add(it)
hitboxes.add(Starbound.gson.fromJson("""[ [0.5625, 1.9375], [1.0625, 1.4375], [1.0625, -2.5625], [0.5625, -3.0625], [-0.5625, -3.0625], [-1.0625, -2.5625], [-1.0625, 1.4375], [-0.5625, 1.9375] ]""", Poly::class.java))
}.ifNotPresent {
throw IllegalStateException("No player collision poly")
}
}
}

View File

@ -23,6 +23,7 @@ import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.RGBAColor
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
import kotlin.concurrent.withLock
open class WorldObject( open class WorldObject(
val world: World<*, *>, val world: World<*, *>,
@ -48,7 +49,7 @@ open class WorldObject(
} }
} }
val mailbox = MailboxExecutorService() val mailbox = MailboxExecutorService(world.mailbox.thread)
// //
// internal runtime properties // internal runtime properties
@ -129,16 +130,22 @@ open class WorldObject(
fun spawn() { fun spawn() {
if (isSpawned) return if (isSpawned) return
isSpawned = true isSpawned = true
world.objects.add(this)
innerSpawn() world.mailbox.execute {
invalidate() world.objects.add(this)
innerSpawn()
invalidate()
}
} }
open fun remove() { open fun remove() {
if (isRemoved || !isSpawned) return if (isRemoved || !isSpawned) return
isRemoved = true isRemoved = true
check(world.objects.remove(this))
innerRemove() world.mailbox.execute {
check(world.objects.remove(this))
innerRemove()
}
} }
open fun thinkShared() { open fun thinkShared() {