Archived
1
0
Fork 0

Register entities globally

This commit is contained in:
Moritz Ruth 2021-01-05 20:05:38 +01:00
parent 3fb9cc3298
commit cb50c7bb39
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
9 changed files with 79 additions and 56 deletions

View file

@ -12,13 +12,13 @@
- Players can have items (with metadata) in their inventory - Players can have items (with metadata) in their inventory
- Inventories - Inventories
- World modifications are sent to players - World modifications are sent to players
- Crafting
- PlayerInteract events are emitted when blocks are clicked - PlayerInteract events are emitted when blocks are clicked
- Command framework + permissions - Command framework + permissions
- Commands can be sent from the console - Commands can be sent from the console
- Players can be teleported between worlds - Players can be teleported between worlds
- Entity AI framework - Entity AI framework
- Scoreboards + Teams - Scoreboards + Teams
- Crafting
## Conventions ## Conventions

View file

@ -8,7 +8,6 @@ package space.uranos.testplugin.anvil
import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader import com.google.common.cache.CacheLoader
import com.google.common.cache.LoadingCache import com.google.common.cache.LoadingCache
import space.uranos.entity.Entity
import space.uranos.world.Chunk import space.uranos.world.Chunk
import space.uranos.world.Dimension import space.uranos.world.Dimension
import space.uranos.world.World import space.uranos.world.World
@ -19,7 +18,6 @@ class AnvilWorld(
override val isFlat: Boolean override val isFlat: Boolean
) : World(UUID.randomUUID()) { ) : World(UUID.randomUUID()) {
override val loadedChunks: Map<Chunk.Key, Chunk> get() = chunks.asMap().filter { (_, chunk) -> chunk.loaded } override val loadedChunks: Map<Chunk.Key, Chunk> get() = chunks.asMap().filter { (_, chunk) -> chunk.loaded }
override val entities = emptyList<Entity>()
private val chunks: LoadingCache<Chunk.Key, Chunk> = CacheBuilder.newBuilder() private val chunks: LoadingCache<Chunk.Key, Chunk> = CacheBuilder.newBuilder()
.build(object : CacheLoader<Chunk.Key, Chunk>() { .build(object : CacheLoader<Chunk.Key, Chunk>() {
@ -29,8 +27,4 @@ class AnvilWorld(
}) })
override fun getChunk(key: Chunk.Key): Chunk = chunks.get(key) override fun getChunk(key: Chunk.Key): Chunk = chunks.get(key)
override fun spawnEntity(entity: Entity) {
TODO("Not yet implemented")
}
} }

View file

@ -5,15 +5,13 @@
package space.uranos.entity package space.uranos.entity
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import space.uranos.Uranos import space.uranos.Uranos
import space.uranos.world.World
import java.util.* import java.util.*
abstract class Entity internal constructor() { 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. * The UUID of this entity.
* *
@ -25,4 +23,28 @@ abstract class Entity internal constructor() {
open val uuid: UUID = UUID.randomUUID() open val uuid: UUID = UUID.randomUUID()
abstract val type: EntityType 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)
} }

View file

@ -14,9 +14,6 @@ import space.uranos.world.World
import java.util.* import java.util.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
/**
* A **real** player.
*/
interface Player { interface Player {
val coroutineContext: CoroutineContext get() = session.coroutineContext val coroutineContext: CoroutineContext get() = session.coroutineContext
@ -107,7 +104,7 @@ interface Player {
var playerListName: TextComponent? var playerListName: TextComponent?
var compassTarget: VoxelLocation var compassTarget: VoxelLocation
val currentlyViewedChunks: List<Chunk> val currentlyViewedChunks: Collection<Chunk>
companion object { companion object {
const val DEFAULT_FIELD_OF_VIEW = 0.1f const val DEFAULT_FIELD_OF_VIEW = 0.1f

View file

@ -8,6 +8,7 @@ package space.uranos.server
import space.uranos.Registry import space.uranos.Registry
import space.uranos.Scheduler import space.uranos.Scheduler
import space.uranos.command.Command import space.uranos.command.Command
import space.uranos.entity.Entity
import space.uranos.event.EventBus import space.uranos.event.EventBus
import space.uranos.event.EventHandlerPositionManager import space.uranos.event.EventHandlerPositionManager
import space.uranos.logging.Logger import space.uranos.logging.Logger
@ -19,55 +20,66 @@ import space.uranos.recipe.Recipe
import space.uranos.world.BiomeRegistry import space.uranos.world.BiomeRegistry
import space.uranos.world.Dimension import space.uranos.world.Dimension
import java.io.File import java.io.File
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
interface Server { abstract class Server {
val eventBus: EventBus abstract val eventBus: EventBus
val eventHandlerPositions: EventHandlerPositionManager abstract val eventHandlerPositions: EventHandlerPositionManager
/** /**
* [CoroutineContext] confined to the server thread. * [CoroutineContext] confined to the server thread.
* *
* Is cancelled when the server is shutting down. * Is cancelled when the server is shutting down.
*/ */
val coroutineContext: CoroutineContext abstract val coroutineContext: CoroutineContext
/** /**
* All sessions connected to the server. * All sessions connected to the server.
*/ */
val sessions: Collection<Session> abstract val sessions: Collection<Session>
/** /**
* All players connected to the server. * All players connected to the server.
*/ */
val players: Collection<Player> abstract val players: Collection<Player>
val pluginManager: PluginManager abstract val pluginManager: PluginManager
val serverDirectory: File abstract val serverDirectory: File
val cacheDirectory: File get() = serverDirectory.resolve("cache") val cacheDirectory: File get() = serverDirectory.resolve("cache")
val dimensionRegistry: Registry<Dimension> abstract val dimensionRegistry: Registry<Dimension>
val recipeRegistry: Registry<Recipe> abstract val recipeRegistry: Registry<Recipe>
val commandRegistry: Registry<Command> abstract val commandRegistry: Registry<Command>
val biomeRegistry: BiomeRegistry abstract val biomeRegistry: BiomeRegistry
val loggingOutputProvider: LoggingOutputProvider abstract val loggingOutputProvider: LoggingOutputProvider
val scheduler: Scheduler abstract val scheduler: Scheduler
val developmentMode: Boolean abstract val developmentMode: Boolean
val minimumLogLevel: Logger.Level abstract val minimumLogLevel: Logger.Level
/**
* Returns an unused entity ID.
*
* **Should not be used by plugins.**
*/
fun claimEntityID(): Int
/** /**
* Initiates shutting down the server. * 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<Entity> = 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 { companion object {
const val TICKS_PER_SECOND = 20 const val TICKS_PER_SECOND = 20

View file

@ -6,3 +6,7 @@
package space.uranos.util package space.uranos.util
fun Int.clamp(range: IntRange) = maxOf(minOf(range.first, range.last), minOf(maxOf(range.first, range.last), this)) 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")
}

View file

@ -15,6 +15,7 @@ import space.uranos.util.newSingleThreadDispatcher
import space.uranos.util.supervisorChild import space.uranos.util.supervisorChild
import space.uranos.util.untilPossiblyNegative import space.uranos.util.untilPossiblyNegative
import java.util.* import java.util.*
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
/** /**
@ -45,9 +46,11 @@ abstract class World(val uuid: UUID) {
open val seed: Long? = null 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<Entity> internal val internalEntities = CopyOnWriteArraySet<Entity>()
val entities get() = internalEntities.toList()
abstract fun getChunk(key: Chunk.Key): Chunk abstract fun getChunk(key: Chunk.Key): Chunk
@ -66,7 +69,6 @@ abstract class World(val uuid: UUID) {
* *
* @param hollow Whether the cube is hollow * @param hollow Whether the cube is hollow
*/ */
@OptIn(ExperimentalStdlibApi::class)
fun getVoxelsInCube(cornerA: VoxelLocation, cornerB: VoxelLocation, hollow: Boolean = false): List<Voxel> { fun getVoxelsInCube(cornerA: VoxelLocation, cornerB: VoxelLocation, hollow: Boolean = false): List<Voxel> {
fun getList(a: Int, b: Int) = fun getList(a: Int, b: Int) =
if (hollow) listOf(a, b).distinct() if (hollow) listOf(a, b).distinct()
@ -95,11 +97,6 @@ abstract class World(val uuid: UUID) {
TODO() TODO()
} }
/**
* Spawns [entity][Entity] in this world.
*/
abstract fun spawnEntity(entity: Entity)
suspend fun destroy() = withContext(coroutineContext) { suspend fun destroy() = withContext(coroutineContext) {
// TODO: Move or kick players // TODO: Move or kick players
coroutineContext.cancel() coroutineContext.cancel()

View file

@ -28,13 +28,12 @@ import space.uranos.world.Dimension
import java.io.File import java.io.File
import java.security.KeyPair import java.security.KeyPair
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.system.exitProcess import kotlin.system.exitProcess
// TODO: Consider using DI because this improves testability // TODO: Consider using DI because this improves testability
class UranosServer internal constructor() : Server { class UranosServer internal constructor() : Server() {
val logger = Logger("Server") val logger = Logger("Server")
private val socketServer = UranosSocketServer(this) private val socketServer = UranosSocketServer(this)
@ -90,9 +89,6 @@ class UranosServer internal constructor() : Server {
override val eventBus = UranosEventBus(developmentMode) override val eventBus = UranosEventBus(developmentMode)
override val eventHandlerPositions = UranosEventHandlerPositionManager() override val eventHandlerPositions = UranosEventHandlerPositionManager()
private val nextEntityID = AtomicInteger()
override fun claimEntityID() = nextEntityID.incrementAndGet()
override fun shutdown() { override fun shutdown() {
runBlocking { runBlocking {
scheduler.shutdown() scheduler.shutdown()

View file

@ -10,6 +10,7 @@ import space.uranos.chat.TextComponent
import space.uranos.net.Session import space.uranos.net.Session
import space.uranos.net.packet.play.ChunkDataPacket import space.uranos.net.packet.play.ChunkDataPacket
import space.uranos.net.packet.play.ChunkLightDataPacket import space.uranos.net.packet.play.ChunkLightDataPacket
import space.uranos.util.clampArgument
import space.uranos.world.Chunk import space.uranos.world.Chunk
import space.uranos.world.VoxelLocation import space.uranos.world.VoxelLocation
import space.uranos.world.World import space.uranos.world.World
@ -35,8 +36,8 @@ class UranosPlayer(
) : Player { ) : Player {
override var selectedHotbarSlot = 0 override var selectedHotbarSlot = 0
set(value) { set(value) {
if (value in 0..8) field = value clampArgument("selectedHotbarSlot", 0..8, value)
else throw IllegalArgumentException("selectedHotbarSlot must be in 0..8") field = value
} }
init { init {
@ -44,7 +45,7 @@ class UranosPlayer(
} }
override var playerListName: TextComponent? = null override var playerListName: TextComponent? = null
override var currentlyViewedChunks: List<Chunk> = emptyList() override var currentlyViewedChunks = emptyList<Chunk>()
init { init {
updateCurrentlyViewedChunks() updateCurrentlyViewedChunks()