From cb50c7bb39b999f42a75de63b579b15cf25f20aa Mon Sep 17 00:00:00 2001 From: Moritz Ruth Date: Tue, 5 Jan 2021 20:05:38 +0100 Subject: [PATCH] Register entities globally --- README.md | 2 +- .../uranos/testplugin/anvil/AnvilWorld.kt | 6 -- .../main/kotlin/space/uranos/entity/Entity.kt | 32 ++++++++-- .../main/kotlin/space/uranos/player/Player.kt | 5 +- .../main/kotlin/space/uranos/server/Server.kt | 60 +++++++++++-------- .../main/kotlin/space/uranos/util/Clamp.kt | 4 ++ .../main/kotlin/space/uranos/world/World.kt | 13 ++-- .../main/kotlin/space/uranos/UranosServer.kt | 6 +- .../space/uranos/player/UranosPlayer.kt | 7 ++- 9 files changed, 79 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index a6463e5..4945e5f 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ - Players can have items (with metadata) in their inventory - Inventories - World modifications are sent to players -- Crafting - PlayerInteract events are emitted when blocks are clicked - Command framework + permissions - Commands can be sent from the console - Players can be teleported between worlds - Entity AI framework - Scoreboards + Teams +- Crafting ## Conventions diff --git a/test-plugin/src/main/kotlin/space/uranos/testplugin/anvil/AnvilWorld.kt b/test-plugin/src/main/kotlin/space/uranos/testplugin/anvil/AnvilWorld.kt index 7a7c89f..a079b4a 100644 --- a/test-plugin/src/main/kotlin/space/uranos/testplugin/anvil/AnvilWorld.kt +++ b/test-plugin/src/main/kotlin/space/uranos/testplugin/anvil/AnvilWorld.kt @@ -8,7 +8,6 @@ package space.uranos.testplugin.anvil import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheLoader import com.google.common.cache.LoadingCache -import space.uranos.entity.Entity import space.uranos.world.Chunk import space.uranos.world.Dimension import space.uranos.world.World @@ -19,7 +18,6 @@ class AnvilWorld( override val isFlat: Boolean ) : World(UUID.randomUUID()) { override val loadedChunks: Map get() = chunks.asMap().filter { (_, chunk) -> chunk.loaded } - override val entities = emptyList() private val chunks: LoadingCache = CacheBuilder.newBuilder() .build(object : CacheLoader() { @@ -29,8 +27,4 @@ class AnvilWorld( }) override fun getChunk(key: Chunk.Key): Chunk = chunks.get(key) - - override fun spawnEntity(entity: Entity) { - TODO("Not yet implemented") - } } diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/Entity.kt b/uranos-api/src/main/kotlin/space/uranos/entity/Entity.kt index c67b95f..4d7bd17 100644 --- a/uranos-api/src/main/kotlin/space/uranos/entity/Entity.kt +++ b/uranos-api/src/main/kotlin/space/uranos/entity/Entity.kt @@ -5,15 +5,13 @@ package space.uranos.entity +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import space.uranos.Uranos +import space.uranos.world.World import java.util.* abstract class Entity internal constructor() { - /** - * An integer unique to this entity which will not be persisted, for example when the entity is serialized. - */ - val uid: Int = Uranos.claimEntityID() - /** * The UUID of this entity. * @@ -25,4 +23,28 @@ abstract class Entity internal constructor() { open val uuid: UUID = UUID.randomUUID() abstract val type: EntityType + + private val worldMutex = Mutex() + var world: World? = null; private set + + suspend fun setWorld(world: World?) { + if (world == this.world) return + + worldMutex.withLock { + this.world?.internalEntities?.remove(this) + this.world = world + world?.internalEntities?.add(this) + } + } + + /** + * Returns [world] if it is not null, otherwise throws [IllegalStateException]. + */ + fun getWorldOrFail() = world ?: throw IllegalStateException("This entity has not been spawned") + + /** + * An integer unique to this entity which will not be persisted, for example when the entity is serialized. + */ + @Suppress("LeakingThis") + val uid: Int = Uranos.registerEntity(this) } diff --git a/uranos-api/src/main/kotlin/space/uranos/player/Player.kt b/uranos-api/src/main/kotlin/space/uranos/player/Player.kt index da08df7..1206332 100644 --- a/uranos-api/src/main/kotlin/space/uranos/player/Player.kt +++ b/uranos-api/src/main/kotlin/space/uranos/player/Player.kt @@ -14,9 +14,6 @@ import space.uranos.world.World import java.util.* import kotlin.coroutines.CoroutineContext -/** - * A **real** player. - */ interface Player { val coroutineContext: CoroutineContext get() = session.coroutineContext @@ -107,7 +104,7 @@ interface Player { var playerListName: TextComponent? var compassTarget: VoxelLocation - val currentlyViewedChunks: List + val currentlyViewedChunks: Collection companion object { const val DEFAULT_FIELD_OF_VIEW = 0.1f diff --git a/uranos-api/src/main/kotlin/space/uranos/server/Server.kt b/uranos-api/src/main/kotlin/space/uranos/server/Server.kt index 1e0dc84..75e99a0 100644 --- a/uranos-api/src/main/kotlin/space/uranos/server/Server.kt +++ b/uranos-api/src/main/kotlin/space/uranos/server/Server.kt @@ -8,6 +8,7 @@ package space.uranos.server import space.uranos.Registry import space.uranos.Scheduler import space.uranos.command.Command +import space.uranos.entity.Entity import space.uranos.event.EventBus import space.uranos.event.EventHandlerPositionManager import space.uranos.logging.Logger @@ -19,55 +20,66 @@ import space.uranos.recipe.Recipe import space.uranos.world.BiomeRegistry import space.uranos.world.Dimension import java.io.File +import java.util.* +import java.util.concurrent.atomic.AtomicInteger import kotlin.coroutines.CoroutineContext -interface Server { - val eventBus: EventBus - val eventHandlerPositions: EventHandlerPositionManager +abstract class Server { + abstract val eventBus: EventBus + abstract val eventHandlerPositions: EventHandlerPositionManager /** * [CoroutineContext] confined to the server thread. * * Is cancelled when the server is shutting down. */ - val coroutineContext: CoroutineContext + abstract val coroutineContext: CoroutineContext /** * All sessions connected to the server. */ - val sessions: Collection + abstract val sessions: Collection /** * All players connected to the server. */ - val players: Collection + abstract val players: Collection - val pluginManager: PluginManager - val serverDirectory: File + abstract val pluginManager: PluginManager + abstract val serverDirectory: File val cacheDirectory: File get() = serverDirectory.resolve("cache") - val dimensionRegistry: Registry - val recipeRegistry: Registry - val commandRegistry: Registry - val biomeRegistry: BiomeRegistry + abstract val dimensionRegistry: Registry + abstract val recipeRegistry: Registry + abstract val commandRegistry: Registry + abstract val biomeRegistry: BiomeRegistry - val loggingOutputProvider: LoggingOutputProvider - val scheduler: Scheduler + abstract val loggingOutputProvider: LoggingOutputProvider + abstract val scheduler: Scheduler - val developmentMode: Boolean - val minimumLogLevel: Logger.Level - - /** - * Returns an unused entity ID. - * - * **Should not be used by plugins.** - */ - fun claimEntityID(): Int + abstract val developmentMode: Boolean + abstract val minimumLogLevel: Logger.Level /** * Initiates shutting down the server. */ - fun shutdown() + abstract fun shutdown() + + /** + * Set of all existing [Entity] instances. + * + * This is not public because the instances may not be fully initialized as they are added. + */ + protected val entities: MutableSet = Collections.newSetFromMap(WeakHashMap()) + private val nextEntityID = AtomicInteger() + + /** + * Returns the UID for [entity]. + */ + internal fun registerEntity(entity: Entity): Int { + entities.add(entity) + return nextEntityID.getAndIncrement() + } companion object { const val TICKS_PER_SECOND = 20 diff --git a/uranos-api/src/main/kotlin/space/uranos/util/Clamp.kt b/uranos-api/src/main/kotlin/space/uranos/util/Clamp.kt index cd171fd..ae451a1 100644 --- a/uranos-api/src/main/kotlin/space/uranos/util/Clamp.kt +++ b/uranos-api/src/main/kotlin/space/uranos/util/Clamp.kt @@ -6,3 +6,7 @@ package space.uranos.util fun Int.clamp(range: IntRange) = maxOf(minOf(range.first, range.last), minOf(maxOf(range.first, range.last), this)) + +fun clampArgument(name: String, range: IntRange, actualValue: Int) { + if (!range.contains(actualValue)) throw IllegalArgumentException("$name must be in $range") +} diff --git a/uranos-api/src/main/kotlin/space/uranos/world/World.kt b/uranos-api/src/main/kotlin/space/uranos/world/World.kt index 92ca27c..c3d1cce 100644 --- a/uranos-api/src/main/kotlin/space/uranos/world/World.kt +++ b/uranos-api/src/main/kotlin/space/uranos/world/World.kt @@ -15,6 +15,7 @@ import space.uranos.util.newSingleThreadDispatcher import space.uranos.util.supervisorChild import space.uranos.util.untilPossiblyNegative import java.util.* +import java.util.concurrent.CopyOnWriteArraySet import kotlin.coroutines.CoroutineContext /** @@ -45,9 +46,11 @@ abstract class World(val uuid: UUID) { open val seed: Long? = null /** - * All entities in this world. Use [spawnEntity] for spawning new ones. + * All entities in this world. */ - abstract val entities: Collection + internal val internalEntities = CopyOnWriteArraySet() + + val entities get() = internalEntities.toList() abstract fun getChunk(key: Chunk.Key): Chunk @@ -66,7 +69,6 @@ abstract class World(val uuid: UUID) { * * @param hollow Whether the cube is hollow */ - @OptIn(ExperimentalStdlibApi::class) fun getVoxelsInCube(cornerA: VoxelLocation, cornerB: VoxelLocation, hollow: Boolean = false): List { fun getList(a: Int, b: Int) = if (hollow) listOf(a, b).distinct() @@ -95,11 +97,6 @@ abstract class World(val uuid: UUID) { TODO() } - /** - * Spawns [entity][Entity] in this world. - */ - abstract fun spawnEntity(entity: Entity) - suspend fun destroy() = withContext(coroutineContext) { // TODO: Move or kick players coroutineContext.cancel() diff --git a/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt b/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt index fcb436e..44471ac 100644 --- a/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt +++ b/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt @@ -28,13 +28,12 @@ import space.uranos.world.Dimension import java.io.File import java.security.KeyPair import java.util.concurrent.Executors -import java.util.concurrent.atomic.AtomicInteger import kotlin.coroutines.CoroutineContext import kotlin.system.exitProcess // TODO: Consider using DI because this improves testability -class UranosServer internal constructor() : Server { +class UranosServer internal constructor() : Server() { val logger = Logger("Server") private val socketServer = UranosSocketServer(this) @@ -90,9 +89,6 @@ class UranosServer internal constructor() : Server { override val eventBus = UranosEventBus(developmentMode) override val eventHandlerPositions = UranosEventHandlerPositionManager() - private val nextEntityID = AtomicInteger() - override fun claimEntityID() = nextEntityID.incrementAndGet() - override fun shutdown() { runBlocking { scheduler.shutdown() diff --git a/uranos-server/src/main/kotlin/space/uranos/player/UranosPlayer.kt b/uranos-server/src/main/kotlin/space/uranos/player/UranosPlayer.kt index a248f17..35bc290 100644 --- a/uranos-server/src/main/kotlin/space/uranos/player/UranosPlayer.kt +++ b/uranos-server/src/main/kotlin/space/uranos/player/UranosPlayer.kt @@ -10,6 +10,7 @@ import space.uranos.chat.TextComponent import space.uranos.net.Session import space.uranos.net.packet.play.ChunkDataPacket import space.uranos.net.packet.play.ChunkLightDataPacket +import space.uranos.util.clampArgument import space.uranos.world.Chunk import space.uranos.world.VoxelLocation import space.uranos.world.World @@ -35,8 +36,8 @@ class UranosPlayer( ) : Player { override var selectedHotbarSlot = 0 set(value) { - if (value in 0..8) field = value - else throw IllegalArgumentException("selectedHotbarSlot must be in 0..8") + clampArgument("selectedHotbarSlot", 0..8, value) + field = value } init { @@ -44,7 +45,7 @@ class UranosPlayer( } override var playerListName: TextComponent? = null - override var currentlyViewedChunks: List = emptyList() + override var currentlyViewedChunks = emptyList() init { updateCurrentlyViewedChunks()