diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md index 4945e5f..f3b0ea3 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,12 @@ - Scoreboards + Teams - Crafting +## Development + +Because IntelliJ sometimes removes needed imports for types which have the same name as types in `java.util` when +running the `Optimize Imports` action, you need to disable automatic wildcard imports for this package +(Editor / Code Style / Kotlin / Imports / Packages to Use Import With '*'). + ## Conventions ### KDoc diff --git a/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/Constants.kt b/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/Constants.kt index c823318..e38940a 100644 --- a/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/Constants.kt +++ b/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/Constants.kt @@ -16,7 +16,7 @@ const val TAG_PACKAGE = "$BASE_PACKAGE.tag" val MATERIAL_TYPE = ClassName(BLOCK_PACKAGE, "Material") val BLOCK_TYPE = ClassName(BLOCK_PACKAGE, "Block") val ITEM_TYPE_TYPE = ClassName(ITEM_PACKAGE, "ItemType") +val ENTITY_TYPE = ClassName(ENTITY_PACKAGE, "Entity") val ENTITY_TYPE_TYPE = ClassName(ENTITY_PACKAGE, "EntityType") val TAG_TYPE = ClassName(TAG_PACKAGE, "Tag") val TAG_TYPE_TYPE = TAG_TYPE.nestedClass("Type") - diff --git a/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/EntitiesGenerator.kt b/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/EntitiesGenerator.kt index 5fe71f8..ac33bcd 100644 --- a/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/EntitiesGenerator.kt +++ b/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/EntitiesGenerator.kt @@ -11,6 +11,7 @@ import com.squareup.kotlinpoet.* import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import space.uranos.mdsp.JsonAny import java.io.File +import kotlin.reflect.KClass class EntitiesGenerator( private val workingDir: File, @@ -28,13 +29,28 @@ class EntitiesGenerator( private fun generateEntityTypes(types: List) { for (entity in types) { - val name = - CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, entity.get("name").toString())!! + "EntityType" + val entityClassName = + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, entity.get("name").toString())!! + + "Entity" + + val entityClass = ClassName(ENTITY_PACKAGE, entityClassName) + + val name = entityClassName + "Type" val type = TypeSpec.classBuilder(name) .addModifiers(KModifier.ABSTRACT) .primaryConstructor(FunSpec.constructorBuilder().addModifiers(KModifier.INTERNAL).build()) - .addSuperinterface(ENTITY_TYPE_TYPE) + .addSuperinterface(ENTITY_TYPE_TYPE.parameterizedBy(entityClass)) + .addProperty( + PropertySpec + .builder( + "interfaceType", + KClass::class.asTypeName().parameterizedBy(entityClass), + KModifier.OVERRIDE + ) + .initializer("%T::class", entityClass) + .build() + ) .addProperty( PropertySpec .builder("numericID", Int::class, KModifier.OVERRIDE) @@ -78,17 +94,19 @@ class EntitiesGenerator( val path = "./${ENTITY_PACKAGE.replace(".", "/")}/$name.kt" if (sourcesDir.resolve(path).exists()) continue - outputDir.resolve(path).writeText( - """ - package $ENTITY_PACKAGE - - // open class AreaEffectCloudEntity : Entity() { - // final override val type: EntityType = Type - // - // companion object Type : AreaEffectCloudEntityType() - // } - """.trimIndent() - ) + val type = TypeSpec.interfaceBuilder(name) + .addSuperinterface(ENTITY_TYPE) + .addType( + TypeSpec.companionObjectBuilder("Type") + .superclass(ClassName(ENTITY_PACKAGE, name + "Type")) + .build() + ) + .build() + + FileSpec.builder(ENTITY_PACKAGE, name) + .addType(type) + .build() + .writeTo(outputDir) } } @@ -97,20 +115,32 @@ class EntitiesGenerator( .map { it.get("name").toString() } .map { CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, it) + "Entity" } - val property = PropertySpec.builder( + val listProperty = PropertySpec.builder( "ENTITY_TYPES", - List::class.asTypeName().parameterizedBy(ENTITY_TYPE_TYPE) + List::class.asTypeName().parameterizedBy(ENTITY_TYPE_TYPE.parameterizedBy(STAR)) ) - .initializer("listOf(\n${ - names.joinToString(",\n") { - val path = "./${ENTITY_PACKAGE.replace(".", "/")}/$it.kt" - if (sourcesDir.resolve(path).exists()) it else "object : ${it}Type() {}" - } - }\n)") + .initializer("listOf(\n${names.joinToString(",\n")}\n)") + .build() + + val typeVariableName = TypeVariableName("T", ENTITY_TYPE) + + val typeProperty = PropertySpec.builder("type", ENTITY_TYPE_TYPE.parameterizedBy(typeVariableName)) + .addTypeVariable(typeVariableName) + .receiver(typeVariableName) + .addAnnotation(AnnotationSpec.builder(Suppress::class).addMember(""""UNCHECKED_CAST"""").build()) + .getter( + FunSpec.getterBuilder() + .addStatement( + "return when(this) {\n${names.joinToString("\n") { "is $it -> $it" }.prependIndent(" ")}\n" + + " else -> error(\"Unknown entity type\")\n} as EntityType" + ) + .build() + ) .build() FileSpec.builder(ENTITY_PACKAGE, "EntityTypes") - .addProperty(property) + .addProperty(typeProperty) + .addProperty(listProperty) .build() .writeTo(outputDir) } diff --git a/test-plugin/src/main/kotlin/space/uranos/testplugin/TestPlugin.kt b/test-plugin/src/main/kotlin/space/uranos/testplugin/TestPlugin.kt index 2d1ea46..78aa0f7 100644 --- a/test-plugin/src/main/kotlin/space/uranos/testplugin/TestPlugin.kt +++ b/test-plugin/src/main/kotlin/space/uranos/testplugin/TestPlugin.kt @@ -18,10 +18,7 @@ import space.uranos.plugin.Plugin import space.uranos.testplugin.anvil.AnvilWorld import space.uranos.util.RGBColor import space.uranos.util.secondsToTicks -import space.uranos.world.Biome -import space.uranos.world.Chunk -import space.uranos.world.Dimension -import space.uranos.world.VoxelLocation +import space.uranos.world.* import space.uranos.world.block.GreenWoolBlock import space.uranos.world.block.RedWoolBlock @@ -49,20 +46,22 @@ class TestPlugin: Plugin("Test", "1.0.0") { Uranos.dimensionRegistry.register(dimension) Uranos.biomeRegistry.register(biome) - val world = AnvilWorld(dimension, true) + val world = AnvilWorld(dimension, true, null) + Uranos.worldRegistry.register(world.internals) + world.getVoxelsInCuboid(VoxelLocation.of(16, 0, 16), VoxelLocation.of(-16, 0, -16)).forEach { it.block = if (it.location.x % 16 == 0 || it.location.z % 16 == 0) GreenWoolBlock() else RedWoolBlock() } world.getVoxelsInSphere(VoxelLocation.of(20, 50, 20), 40.0).forEach { it.block = RedWoolBlock() } - world.getChunk(Chunk.Key(0, 0)).setBiome(0, 0, 0, biome) Uranos.eventBus.on { event -> event.response = ServerListInfo("1.16.4", 754, TextComponent of "Test", 10, 0, emptyList()) } - val entity = CowEntity(Position(0.0, 10.0, 0.0, 0f, 0f), 0f) + val entity = Uranos.create() + entity.position = Position(0.0, 10.0, 0.0, 0f, 0f) entity.setWorld(world) Uranos.eventBus.on { event -> 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 18d967f..f3493fc 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,6 +8,9 @@ package space.uranos.testplugin.anvil import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheLoader import com.google.common.cache.LoadingCache +import kotlinx.coroutines.CoroutineDispatcher +import space.uranos.entity.Entity +import space.uranos.util.newSingleThreadDispatcher import space.uranos.world.Chunk import space.uranos.world.Dimension import space.uranos.world.World @@ -15,8 +18,10 @@ import java.util.* class AnvilWorld( override val dimension: Dimension, - override val isFlat: Boolean -) : World(UUID.randomUUID()) { + override val isFlat: Boolean, + override val seed: Long?, +) : World { + override val dispatcher: CoroutineDispatcher = newSingleThreadDispatcher("AnvilWorld") override val loadedChunks: Map get() = chunks.asMap().filter { (_, chunk) -> chunk.loaded } private val chunks: LoadingCache = CacheBuilder.newBuilder() @@ -27,4 +32,18 @@ class AnvilWorld( }) override fun getChunk(key: Chunk.Key): AnvilChunk = chunks.get(key) + + override val entities = mutableSetOf() + + val internals = object : World.Internals { + override val world: World = this@AnvilWorld + + override fun addEntity(entity: Entity) { + entities.add(entity) + } + + override fun removeEntity(entity: Entity) { + entities.remove(entity) + } + } } diff --git a/uranos-api/src/main/kotlin/space/uranos/Registry.kt b/uranos-api/src/main/kotlin/space/uranos/Registry.kt index 7adcac4..8bc57bb 100644 --- a/uranos-api/src/main/kotlin/space/uranos/Registry.kt +++ b/uranos-api/src/main/kotlin/space/uranos/Registry.kt @@ -5,10 +5,8 @@ package space.uranos -import java.util.concurrent.ConcurrentHashMap - open class Registry { - protected val internalItems = ConcurrentHashMap() + protected val internalItems = HashMap() val items: Map = internalItems /** diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/CowEntity.kt b/uranos-api/src/main/kotlin/space/uranos/entity/CowEntity.kt deleted file mode 100644 index 62a089e..0000000 --- a/uranos-api/src/main/kotlin/space/uranos/entity/CowEntity.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2020-2021 Moritz Ruth and Uranos contributors - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file - */ - -package space.uranos.entity - -import space.uranos.Position -import space.uranos.Vector - -open class CowEntity(position: Position, override var headPitch: Float) : LivingEntity(position) { - final override val type: EntityType = Type - override var velocity: Vector = Vector.ZERO - - companion object Type : CowEntityType() -} 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 bacceae..b5c53ca 100644 --- a/uranos-api/src/main/kotlin/space/uranos/entity/Entity.kt +++ b/uranos-api/src/main/kotlin/space/uranos/entity/Entity.kt @@ -5,41 +5,15 @@ package space.uranos.entity -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import space.uranos.* -import space.uranos.entity.event.ViewingChangedEvent +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader import space.uranos.event.EventBusWrapper import space.uranos.player.Player -import space.uranos.util.TickSynchronizationContainer -import space.uranos.util.WatchableSet -import space.uranos.util.memoized import space.uranos.world.Chunk -import space.uranos.world.VoxelLocation import space.uranos.world.World -import java.util.* -import kotlin.math.max - -private typealias Vector = space.uranos.Vector - -sealed class Entity { - internal var eventBusWrapper: EventBusWrapper<*>? = null - protected val container = TickSynchronizationContainer() - - @Deprecated( - "This function should only be called by the server.", - ReplaceWith(""), - DeprecationLevel.ERROR - ) - suspend fun tick() { - onTick() - container.tick() - } - - protected open fun onTick() {} - protected open fun onWorldSet() {} +import java.util.UUID +interface Entity { /** * The UUID of this entity. * @@ -48,98 +22,40 @@ sealed class Entity { * * Otherwise, it is usually randomly generated. */ - open val uuid: UUID = UUID.randomUUID() - - abstract val type: EntityType - abstract val chunkKey: Chunk.Key - - /** - * Players that can see this entity. - */ - val viewers: MutableSet = object : WatchableSet(Collections.newSetFromMap(WeakHashMap())) { - override fun onAdd(element: Player) { - Uranos.scope.launch { Uranos.eventBus.emit(ViewingChangedEvent(this@Entity, element, true)) } - } - - override fun onRemove(element: Player) { - Uranos.scope.launch { Uranos.eventBus.emit(ViewingChangedEvent(this@Entity, element, false)) } - } - } - - /** - * If players should be added to [viewers] when they join. - */ - var visibleToNewPlayers: Boolean = true - - private val worldMutex = Mutex() - var world: World? = null; protected set - - suspend fun setWorld(world: World?) { - if (world == null && this is PlayerEntity) - throw IllegalArgumentException("You cannot set the world of a PlayerEntity to null") - - if (world == this.world) return - - worldMutex.withLock { - this.world?.internalEntities?.remove(this) - this.world = world - world?.internalEntities?.add(this) - } - - onWorldSet() - } - - /** - * Returns [world] if it is not null, otherwise throws [IllegalStateException]. - */ - fun getWorldOrFail() = world ?: throw IllegalStateException("This entity has not been spawned") + val uuid: UUID /** * An integer unique to this entity which will not be persisted, for example when the entity is serialized. */ - @Suppress("LeakingThis") - val numericID: Int = Uranos.registerEntity(this) + val numericID: Int + + /** + * Players that can see this entity. + */ + val viewers: MutableSet + + fun belongsToChunk(key: Chunk.Key): Boolean + + val world: World? + suspend fun setWorld(world: World?) + + /** + * If players should be added to [viewers] when they join. + */ + var visibleToNewPlayers: Boolean } -abstract class LivingEntity(position: Position) : Entity(), Mobile { - abstract var headPitch: Float // TODO: This should probably be headYaw, but wiki.vg says headPitch. And it is only used in the SpawnLivingEntity packet - override var velocity: Vector = Vector.ZERO +/** + * Returns [Entity.world] if it is not null, otherwise throws [IllegalStateException]. + */ +fun Entity.getWorldOrFail() = world ?: throw IllegalStateException("This entity has not been spawned") - override var position: Position by container.ifChanged(position) { value -> - // TODO: Broadcast to players - } - - override val chunkKey: Chunk.Key by memoized({ position }) { Chunk.Key.from(position) } -} - -abstract class ObjectEntity(position: Position) : Entity(), Mobile { - override var position: Position by container.ifChanged(position) { value -> - // TODO: Broadcast to players - } - - override var velocity: Vector = Vector.ZERO -} - -open class PaintingEntity( - val topLeftLocation: VoxelLocation, - val direction: CardinalDirection, - val motive: PaintingMotive -) : Entity() { - override val type: EntityType = Type - override val chunkKey: Chunk.Key get() = Chunk.Key.from(topLeftLocation) - - val centerLocation = topLeftLocation.copy( - x = max(0, motive.width / 2) + topLeftLocation.x, - z = motive.height / 2 + topLeftLocation.z - ) - - companion object Type : PaintingEntityType() -} +private val eventBusWrapperCache = CacheBuilder.newBuilder() + .weakKeys() + .build(object : CacheLoader>() { + override fun load(key: Entity): EventBusWrapper = EventBusWrapper(key) + }) @Suppress("UNCHECKED_CAST") -fun T.events(): EventBusWrapper { - val wrapper = eventBusWrapper - - return if (wrapper == null) EventBusWrapper(this).also { eventBusWrapper = it } - else wrapper as EventBusWrapper -} +val T.events + get() = eventBusWrapperCache.get(this) as EventBusWrapper diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/EntityType.kt b/uranos-api/src/main/kotlin/space/uranos/entity/EntityType.kt index 2069b2e..4e6cb76 100644 --- a/uranos-api/src/main/kotlin/space/uranos/entity/EntityType.kt +++ b/uranos-api/src/main/kotlin/space/uranos/entity/EntityType.kt @@ -7,7 +7,8 @@ package space.uranos.entity import kotlin.reflect.KClass -interface EntityType { +interface EntityType { + val interfaceType: KClass val numericID: Int val id: String val width: Float @@ -17,7 +18,16 @@ interface EntityType { /** * All entity types, sorted by their numeric ID in ascending order. */ - val all = ENTITY_TYPES - val byID = ENTITY_TYPES.map { it.id to it }.toMap() + val all: Collection> = ENTITY_TYPES + + val byID: Map> = all.map { it.id to it }.toMap() + fun byID(id: String) = byID[id] + + private val byInterfaceTypeMap: Map, EntityType<*>> = + ENTITY_TYPES.map { it.interfaceType to it }.toMap() + + @Suppress("UNCHECKED_CAST") + fun byInterfaceType(interfaceType: KClass): EntityType = + byInterfaceTypeMap[interfaceType] as EntityType } } diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/ItemEntity.kt b/uranos-api/src/main/kotlin/space/uranos/entity/ItemEntity.kt deleted file mode 100644 index 49c7657..0000000 --- a/uranos-api/src/main/kotlin/space/uranos/entity/ItemEntity.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2020-2021 Moritz Ruth and Uranos contributors - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file - */ - -package space.uranos.entity - -import space.uranos.Position -import space.uranos.world.Chunk - -open class ItemEntity(position: Position) : ObjectEntity(position) { - final override val type: EntityType = Type - override val chunkKey: Chunk.Key get() = Chunk.Key.from(position) - - companion object Type : ItemEntityType() -} diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/LivingEntity.kt b/uranos-api/src/main/kotlin/space/uranos/entity/LivingEntity.kt new file mode 100644 index 0000000..c952a6e --- /dev/null +++ b/uranos-api/src/main/kotlin/space/uranos/entity/LivingEntity.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2020-2021 Moritz Ruth and Uranos contributors + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file + */ + +package space.uranos.entity + +interface LivingEntity : Entity, Mobile { + // TODO: This should probably be headYaw, but wiki.vg says headPitch. + // And it is only used in the SpawnLivingEntity packet + var headPitch: Float +} diff --git a/uranos-api/src/main/kotlin/space/uranos/CalledFromWrongThread.kt b/uranos-api/src/main/kotlin/space/uranos/entity/ObjectEntity.kt similarity index 66% rename from uranos-api/src/main/kotlin/space/uranos/CalledFromWrongThread.kt rename to uranos-api/src/main/kotlin/space/uranos/entity/ObjectEntity.kt index 80498dd..7a7f274 100644 --- a/uranos-api/src/main/kotlin/space/uranos/CalledFromWrongThread.kt +++ b/uranos-api/src/main/kotlin/space/uranos/entity/ObjectEntity.kt @@ -3,6 +3,8 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file */ -package space.uranos +package space.uranos.entity -class CalledFromWrongThread(message: String) : Exception(message) +interface ObjectEntity : Entity, Mobile { + +} diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/PaintingEntity.kt b/uranos-api/src/main/kotlin/space/uranos/entity/PaintingEntity.kt new file mode 100644 index 0000000..529686c --- /dev/null +++ b/uranos-api/src/main/kotlin/space/uranos/entity/PaintingEntity.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2020-2021 Moritz Ruth and Uranos contributors + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file + */ + +package space.uranos.entity + +import space.uranos.CardinalDirection +import space.uranos.PaintingMotive +import space.uranos.world.VoxelLocation +import kotlin.math.max + +interface PaintingEntity : Entity { + val topLeftLocation: VoxelLocation + val facing: CardinalDirection + val motive: PaintingMotive + + companion object Type : AreaEffectCloudEntityType() +} + +val PaintingEntity.centerLocation + get() = topLeftLocation.copy( + x = max(0, motive.width / 2) + topLeftLocation.x, + z = motive.height / 2 + topLeftLocation.z + ) diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/PlayerEntity.kt b/uranos-api/src/main/kotlin/space/uranos/entity/PlayerEntity.kt index 5dc410f..ccb5e5b 100644 --- a/uranos-api/src/main/kotlin/space/uranos/entity/PlayerEntity.kt +++ b/uranos-api/src/main/kotlin/space/uranos/entity/PlayerEntity.kt @@ -5,26 +5,16 @@ package space.uranos.entity -import space.uranos.Position import space.uranos.player.Player import space.uranos.world.World -open class PlayerEntity( - position: Position, - /** - * The player to which this entity belongs. - * - * **Not every player entity belongs to a player.** - */ - open val player: Player? = null, - override var headPitch: Float = 0f -) : LivingEntity(position) { - final override val type: EntityType = Type +interface PlayerEntity : LivingEntity { + val player: Player - /** - * Because [world] is never `null` for player entities, you can use this property instead of writing `world!!`. - */ - val safeWorld: World get() = world!! - - companion object Type : PlayerEntityType() + companion object Type : PlayerEntityType() } + +/** + * As player entities are always in a world, you can use this property instead of [getWorldOrFail]. + */ +val PlayerEntity.safeWorld: World get() = getWorldOrFail() 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 2b00c8a..302df07 100644 --- a/uranos-api/src/main/kotlin/space/uranos/server/Server.kt +++ b/uranos-api/src/main/kotlin/space/uranos/server/Server.kt @@ -12,6 +12,7 @@ import space.uranos.Registry import space.uranos.Scheduler import space.uranos.command.Command import space.uranos.entity.Entity +import space.uranos.entity.EntityType import space.uranos.event.EventBus import space.uranos.event.EventHandlerPositionManager import space.uranos.logging.Logger @@ -23,9 +24,8 @@ import space.uranos.recipe.Recipe import space.uranos.util.newSingleThreadDispatcher import space.uranos.world.BiomeRegistry import space.uranos.world.Dimension +import space.uranos.world.WorldRegistry import java.io.File -import java.util.* -import java.util.concurrent.atomic.AtomicInteger abstract class Server { abstract val eventBus: EventBus @@ -55,10 +55,11 @@ abstract class Server { abstract val serverDirectory: File val cacheDirectory: File get() = serverDirectory.resolve("cache") - abstract val dimensionRegistry: Registry - abstract val recipeRegistry: Registry - abstract val commandRegistry: Registry - abstract val biomeRegistry: BiomeRegistry + abstract val worldRegistry: WorldRegistry + val dimensionRegistry: Registry = Registry() + val recipeRegistry: Registry = Registry() + val commandRegistry: Registry = Registry() + val biomeRegistry: BiomeRegistry = BiomeRegistry() abstract val loggingOutputProvider: LoggingOutputProvider abstract val scheduler: Scheduler @@ -66,27 +67,24 @@ abstract class Server { abstract val developmentMode: Boolean abstract val minimumLogLevel: Logger.Level + /** + * Set of all existing [Entity] instances. + */ + abstract val entities: Set + + /** + * Create a new entity of [type]. + * + * @throws IllegalArgumentException When [type] is [PlayerEntity][space.uranos.entity.PlayerEntity]. + */ + abstract fun create(type: EntityType): T + inline fun create(): T = create(EntityType.byInterfaceType(T::class)) + /** * Initiates shutting down the server. */ abstract fun shutdown() - /** - * Set of all existing [Entity] instances. - */ - private val internalEntities: MutableSet = Collections.newSetFromMap(WeakHashMap()) - val entities: Set = internalEntities - - private val nextEntityID = AtomicInteger(1) - - /** - * Returns the UID for [entity]. - */ - internal fun registerEntity(entity: Entity): Int { - internalEntities.add(entity) - return nextEntityID.getAndIncrement() - } - companion object { const val TICKS_PER_SECOND = 20 } diff --git a/uranos-api/src/main/kotlin/space/uranos/world/Chunk.kt b/uranos-api/src/main/kotlin/space/uranos/world/Chunk.kt index 34df482..8254db7 100644 --- a/uranos-api/src/main/kotlin/space/uranos/world/Chunk.kt +++ b/uranos-api/src/main/kotlin/space/uranos/world/Chunk.kt @@ -16,7 +16,7 @@ abstract class Chunk( val world: World, val key: Key ) { - private val identifier = "Chunk(${world.uuid}/${key.x}-${key.z})" + private val identifier = "Chunk(${key.x}-${key.z})" data class Key(val x: Int, val z: Int) { companion object { 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 68744c6..5dfe6e0 100644 --- a/uranos-api/src/main/kotlin/space/uranos/world/World.kt +++ b/uranos-api/src/main/kotlin/space/uranos/world/World.kt @@ -5,35 +5,26 @@ package space.uranos.world -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.withContext import space.uranos.Vector import space.uranos.entity.Entity -import space.uranos.util.newSingleThreadDispatcher import space.uranos.util.untilPossiblyLower -import java.util.* -import kotlin.collections.HashSet -/** - * A Minecraft world. - */ -abstract class World(val uuid: UUID) { - private val identifier = "World($uuid)" +interface World { + val dispatcher: CoroutineDispatcher + val dimension: Dimension + val isFlat: Boolean + val entities: Collection + val loadedChunks: Map - private val internalDispatcher = newSingleThreadDispatcher(identifier) + interface Internals { + val world: World - /** - * A coroutine dispatcher that is confined to the server thread. - */ - val dispatcher: CoroutineDispatcher = internalDispatcher - - /** - * A coroutine scope confined to [dispatcher] that is cancelled when the world is destroyed. - */ - val scope = CoroutineScope(SupervisorJob() + dispatcher) - - abstract val dimension: Dimension - abstract val loadedChunks: Map - abstract val isFlat: Boolean + fun addEntity(entity: Entity) + fun removeEntity(entity: Entity) + } /** * This can be any value. @@ -42,77 +33,60 @@ abstract class World(val uuid: UUID) { * (and it seems not to use it in any way). * A random value will be used if this is null. */ - open val seed: Long? = null + val seed: Long? - /** - * All entities in this world. - */ - internal val internalEntities = HashSet() + fun getChunk(key: Chunk.Key): Chunk +} - val entities get() = internalEntities.toList() +/** + * Returns the chunk containing the voxel at [location]. + */ +fun World.getChunk(location: VoxelLocation): Chunk = getChunk(Chunk.Key.from(location)) - abstract fun getChunk(key: Chunk.Key): Chunk +/** + * Returns the voxel at [location]. + */ +fun World.getVoxel(location: VoxelLocation): Voxel = getChunk(location).getVoxel(location) - /** - * Returns the chunk containing the voxel at [location]. - */ - fun getChunk(location: VoxelLocation): Chunk = getChunk(Chunk.Key.from(location)) +/** + * Returns all voxels in the cuboid with the corner points [cornerA] and [cornerB]. + * + * @param hollow Whether the cuboid is hollow + */ +fun World.getVoxelsInCuboid(cornerA: VoxelLocation, cornerB: VoxelLocation, hollow: Boolean = false): List { + fun getList(a: Int, b: Int) = + if (hollow) listOf(a, b).distinct() + else (a untilPossiblyLower b).toList() - /** - * Returns the voxel at [location]. - */ - fun getVoxel(location: VoxelLocation): Voxel = getChunk(location).getVoxel(location) - - /** - * Returns all voxels in the cuboid with the corner points [cornerA] and [cornerB]. - * - * @param hollow Whether the cuboid is hollow - */ - fun getVoxelsInCuboid(cornerA: VoxelLocation, cornerB: VoxelLocation, hollow: Boolean = false): List { - fun getList(a: Int, b: Int) = - if (hollow) listOf(a, b).distinct() - else (a untilPossiblyLower b).toList() - - return buildList { - for (x in getList(cornerA.x, cornerB.x)) { - for (y in getList(cornerA.y.toInt(), cornerB.y.toInt())) { - for (z in getList(cornerA.z, cornerB.z)) { - add(getVoxel(VoxelLocation(x, y.toUByte(), z))) - } + return buildList { + for (x in getList(cornerA.x, cornerB.x)) { + for (y in getList(cornerA.y.toInt(), cornerB.y.toInt())) { + for (z in getList(cornerA.z, cornerB.z)) { + add(getVoxel(VoxelLocation(x, y.toUByte(), z))) } } } } +} - /** - * Returns all voxels in a sphere with the specified [center] and [radius]. - */ - fun getVoxelsInSphere( - center: VoxelLocation, - radius: Double, - wallWidth: Int = Int.MAX_VALUE - ): List { - val centerVector = center.asVector() - return getVoxelsInCuboid( - centerVector.plus(radius, radius, radius).toVoxelLocation(), - centerVector.minus(radius, radius, radius).toVoxelLocation() - ).filter { - val distance = Vector.distanceBetween(it.location.asVector(), centerVector) - distance < radius && distance > radius - 0.5 - wallWidth - } - } - suspend inline operator fun invoke(noinline block: suspend CoroutineScope.() -> T): T = - withContext(dispatcher, block) - - suspend fun destroy() { - // TODO: Move or kick players - scope.cancel() - - coroutineScope { - loadedChunks.values.forEach { launch { (it as? Chunk.Unloadable)?.unload() } } - } - - internalDispatcher.close() +/** + * Returns all voxels in a sphere with the specified [center] and [radius]. + */ +fun World.getVoxelsInSphere( + center: VoxelLocation, + radius: Double, + wallWidth: Int = Int.MAX_VALUE +): List { + val centerVector = center.asVector() + return getVoxelsInCuboid( + centerVector.plus(radius, radius, radius).toVoxelLocation(), + centerVector.minus(radius, radius, radius).toVoxelLocation() + ).filter { + val distance = Vector.distanceBetween(it.location.asVector(), centerVector) + distance < radius && distance > radius - 0.5 - wallWidth } } + +suspend inline fun runIn(world: World, noinline block: suspend CoroutineScope.() -> T): T = + withContext(world.dispatcher, block) diff --git a/uranos-api/src/main/kotlin/space/uranos/world/WorldRegistry.kt b/uranos-api/src/main/kotlin/space/uranos/world/WorldRegistry.kt new file mode 100644 index 0000000..9cad049 --- /dev/null +++ b/uranos-api/src/main/kotlin/space/uranos/world/WorldRegistry.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2020-2021 Moritz Ruth and Uranos contributors + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file + */ + +package space.uranos.world + +open class WorldRegistry { + protected val internalItems = HashMap() + val items: Collection get() = internalItems.keys + + fun register(internals: World.Internals) { + internalItems[internals.world] = internals + } +} diff --git a/uranos-packet-codecs/src/main/kotlin/space/uranos/util/SpawnEntity.kt b/uranos-packet-codecs/src/main/kotlin/space/uranos/util/SpawnEntity.kt index fdc4505..f54e72a 100644 --- a/uranos-packet-codecs/src/main/kotlin/space/uranos/util/SpawnEntity.kt +++ b/uranos-packet-codecs/src/main/kotlin/space/uranos/util/SpawnEntity.kt @@ -33,8 +33,9 @@ fun Entity.createSpawnPacket(): OutgoingPacket = when (this) { uuid, motive, centerLocation, - direction + facing ) + else -> throw IllegalArgumentException("Unknown entity type") } fun ObjectEntity.getDataValue(): Int = when (this) { diff --git a/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/SpawnLivingEntityPacket.kt b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/SpawnLivingEntityPacket.kt index 6d135d2..cc6c690 100644 --- a/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/SpawnLivingEntityPacket.kt +++ b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/SpawnLivingEntityPacket.kt @@ -9,9 +9,7 @@ import space.uranos.Position import space.uranos.Vector import space.uranos.entity.EntityType import space.uranos.net.packet.OutgoingPacket -import space.uranos.world.Chunk -import space.uranos.world.ChunkData -import java.util.* +import java.util.UUID /** * Sent to spawn **living** entities. @@ -19,7 +17,7 @@ import java.util.* data class SpawnLivingEntityPacket( val entityID: Int, val uuid: UUID, - val type: EntityType, + val type: EntityType<*>, val position: Position, val headPitch: Float, /** diff --git a/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/SpawnObjectEntityPacket.kt b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/SpawnObjectEntityPacket.kt index 9296490..a299e39 100644 --- a/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/SpawnObjectEntityPacket.kt +++ b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/SpawnObjectEntityPacket.kt @@ -9,9 +9,7 @@ import space.uranos.Position import space.uranos.Vector import space.uranos.entity.EntityType import space.uranos.net.packet.OutgoingPacket -import space.uranos.world.Chunk -import space.uranos.world.ChunkData -import java.util.* +import java.util.UUID /** * Sent to spawn object entities. @@ -19,7 +17,7 @@ import java.util.* data class SpawnObjectEntityPacket( val entityID: Int, val uuid: UUID, - val type: EntityType, + val type: EntityType<*>, val position: Position, val data: Int, val velocity: Vector diff --git a/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt b/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt index 78bae50..0530105 100644 --- a/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt +++ b/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt @@ -9,8 +9,8 @@ import com.sksamuel.hoplite.ConfigFilePropertySource import com.sksamuel.hoplite.ConfigLoader import com.sksamuel.hoplite.ConfigSource import kotlinx.coroutines.runBlocking -import space.uranos.command.Command import space.uranos.config.UranosConfig +import space.uranos.entity.* import space.uranos.entity.event.ViewingChangedEvent import space.uranos.event.EventHandlerPosition import space.uranos.event.UranosEventBus @@ -22,16 +22,15 @@ import space.uranos.net.packet.play.DestroyEntitiesPacket import space.uranos.net.packet.play.PlayerInfoPacket import space.uranos.player.UranosPlayer import space.uranos.plugin.UranosPluginManager -import space.uranos.recipe.Recipe import space.uranos.server.Server import space.uranos.util.EncryptionUtils import space.uranos.util.createSpawnPacket import space.uranos.util.msToTicks import space.uranos.util.runInServerThread -import space.uranos.world.BiomeRegistry -import space.uranos.world.Dimension +import space.uranos.world.UranosWorldRegistry import java.io.File import java.security.KeyPair +import java.util.concurrent.atomic.AtomicInteger import kotlin.system.exitProcess // TODO: Consider using DI because this improves testability @@ -60,11 +59,6 @@ class UranosServer internal constructor() : Server() { else it } - override val dimensionRegistry = Registry() - override val recipeRegistry = Registry() - override val commandRegistry = Registry() - override val biomeRegistry = BiomeRegistry() - override val loggingOutputProvider = UranosLoggingOutputProvider override val scheduler = UranosScheduler() @@ -88,12 +82,35 @@ class UranosServer internal constructor() : Server() { override val eventBus = UranosEventBus(config.logging.events) override val eventHandlerPositions = UranosEventHandlerPositionManager() + override val worldRegistry = UranosWorldRegistry() + + private val internalEntities = HashSet() + override val entities: Set = internalEntities + + override fun create(type: EntityType): T { + val entity: UranosEntity = when (type) { + CowEntity -> UranosCowEntity(this) + else -> throw IllegalArgumentException("Entities of this type cannot be created with this function") + } + + internalEntities.add(entity) + + @Suppress("UNCHECKED_CAST") + return entity as T + } + + fun createPlayerEntity(player: UranosPlayer) = + UranosPlayerEntity(this, player).also { internalEntities.add(it) } + override fun shutdown() { runBlocking { scheduler.shutdown() } } + private val nextEntityID = AtomicInteger(1) + fun claimEntityID() = nextEntityID.getAndIncrement() + private fun failInitialization(t: Throwable): Nothing { logger.error("Server initialization failed:", t) exitProcess(1) @@ -119,12 +136,7 @@ class UranosServer internal constructor() : Server() { scheduler.executeRepeating(1, 0) { runInServerThread { players.forEach { it.container.tick() } - - entities.forEach { - @Suppress("DEPRECATION_ERROR") - it.tick() - } - + internalEntities.forEach { it.tick() } sessions.forEach { it.packetsAdapter.tick() } } } @@ -142,6 +154,8 @@ class UranosServer internal constructor() : Server() { private fun registerListeners() { eventBus.on(EventHandlerPosition.LAST) { event -> + if (event.target == event.player.entity) return@on + if (event.viewing) event.player.session.sendNextTick(event.target.createSpawnPacket()) else event.player.session.sendNextTick(DestroyEntitiesPacket(arrayOf(event.target.numericID))) } diff --git a/uranos-server/src/main/kotlin/space/uranos/entity/UranosCowEntity.kt b/uranos-server/src/main/kotlin/space/uranos/entity/UranosCowEntity.kt new file mode 100644 index 0000000..ed57538 --- /dev/null +++ b/uranos-server/src/main/kotlin/space/uranos/entity/UranosCowEntity.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2020-2021 Moritz Ruth and Uranos contributors + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file + */ + +package space.uranos.entity + +import space.uranos.UranosServer + +class UranosCowEntity(server: UranosServer) : UranosLivingEntity(server), CowEntity diff --git a/uranos-server/src/main/kotlin/space/uranos/entity/UranosEntity.kt b/uranos-server/src/main/kotlin/space/uranos/entity/UranosEntity.kt new file mode 100644 index 0000000..03d3a6a --- /dev/null +++ b/uranos-server/src/main/kotlin/space/uranos/entity/UranosEntity.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2020-2021 Moritz Ruth and Uranos contributors + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file + */ + +package space.uranos.entity + +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import space.uranos.* +import space.uranos.entity.event.ViewingChangedEvent +import space.uranos.player.Player +import space.uranos.util.TickSynchronizationContainer +import space.uranos.util.WatchableSet +import space.uranos.util.memoized +import space.uranos.world.Chunk +import space.uranos.world.VoxelLocation +import space.uranos.world.World +import space.uranos.world.getInternalsIfRegistered +import java.util.Collections +import java.util.UUID +import java.util.WeakHashMap + +sealed class UranosEntity(server: UranosServer) : Entity { + abstract val chunkKey: Chunk.Key + + override fun belongsToChunk(key: Chunk.Key): Boolean = key == chunkKey + + override val numericID: Int = server.claimEntityID() + override val uuid: UUID = UUID.randomUUID() + + override val viewers: MutableSet = object : WatchableSet(Collections.newSetFromMap(WeakHashMap())) { + override fun onAdd(element: Player) { + Uranos.scope.launch { Uranos.eventBus.emit(ViewingChangedEvent(this@UranosEntity, element, true)) } + } + + override fun onRemove(element: Player) { + Uranos.scope.launch { Uranos.eventBus.emit(ViewingChangedEvent(this@UranosEntity, element, false)) } + } + } + + /** + * If players should be added to [viewers] when they join. + */ + override var visibleToNewPlayers: Boolean = true + + private val worldMutex = Mutex() + final override var world: World? = null; private set + + protected val container = TickSynchronizationContainer() + + override suspend fun setWorld(world: World?) { + if (world == null && this is PlayerEntity) + throw IllegalArgumentException("You cannot set the world of a PlayerEntity to null") + + if (world == this.world) return + + worldMutex.withLock { + this.world?.getInternalsIfRegistered()?.removeEntity(this) + this.world = world + world?.getInternalsIfRegistered()?.addEntity(this) + } + } + + suspend fun tick() { + container.tick() + } +} + +abstract class UranosLivingEntity(server: UranosServer) : UranosEntity(server), LivingEntity { + override var velocity: Vector = Vector.ZERO + override var headPitch: Float = 0f + + override var position: Position by container.ifChanged(Position.ZERO) { value -> + // TODO: Broadcast to players + } + + override val chunkKey: Chunk.Key by memoized({ position }) { Chunk.Key.from(position) } +} + +abstract class UranosObjectEntity(server: UranosServer) : UranosEntity(server), ObjectEntity { + override var velocity: Vector = Vector.ZERO + + override var position: Position by container.ifChanged(Position.ZERO) { value -> + // TODO: Broadcast to players + } + + override val chunkKey: Chunk.Key by memoized({ position }) { Chunk.Key.from(position) } +} + +class UranosPaintingEntity( + server: UranosServer, + override val topLeftLocation: VoxelLocation, + override val facing: CardinalDirection, + override val motive: PaintingMotive +) : UranosEntity(server), PaintingEntity { + override val chunkKey: Chunk.Key get() = Chunk.Key.from(topLeftLocation) +} diff --git a/uranos-server/src/main/kotlin/space/uranos/entity/UranosPlayerEntity.kt b/uranos-server/src/main/kotlin/space/uranos/entity/UranosPlayerEntity.kt new file mode 100644 index 0000000..cecaa40 --- /dev/null +++ b/uranos-server/src/main/kotlin/space/uranos/entity/UranosPlayerEntity.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2020-2021 Moritz Ruth and Uranos contributors + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file + */ + +package space.uranos.entity + +import space.uranos.UranosServer +import space.uranos.player.Player +import java.util.UUID + +class UranosPlayerEntity( + server: UranosServer, + override val player: Player +) : UranosLivingEntity(server), PlayerEntity { + override val uuid: UUID = player.uuid +} 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 4896615..732c6d1 100644 --- a/uranos-server/src/main/kotlin/space/uranos/player/UranosPlayer.kt +++ b/uranos-server/src/main/kotlin/space/uranos/player/UranosPlayer.kt @@ -6,9 +6,9 @@ package space.uranos.player import space.uranos.Position -import space.uranos.Uranos import space.uranos.chat.TextComponent import space.uranos.entity.PlayerEntity +import space.uranos.entity.safeWorld import space.uranos.net.UranosSession import space.uranos.net.packet.play.ChunkDataPacket import space.uranos.net.packet.play.ChunkLightDataPacket @@ -16,11 +16,10 @@ import space.uranos.net.packet.play.PlayerInfoPacket import space.uranos.net.packet.play.SelectedHotbarSlotPacket import space.uranos.util.TickSynchronizationContainer import space.uranos.util.clampArgument -import space.uranos.util.createSpawnPacket import space.uranos.world.Chunk import space.uranos.world.VoxelLocation import space.uranos.world.World -import java.util.* +import java.util.UUID import kotlin.math.abs class UranosPlayer( @@ -61,14 +60,16 @@ class UranosPlayer( override var currentlyViewedChunks = emptyList() - override val entity: PlayerEntity = PlayerEntity(position, this, headPitch) + override val entity: PlayerEntity = session.server.createPlayerEntity(this).also { + it.position = position + it.headPitch = headPitch + } suspend fun spawnInitially(world: World) { - Uranos.entities.forEach { if (it.visibleToNewPlayers) it.viewers.add(this) } + session.server.entities.forEach { if (it.visibleToNewPlayers) it.viewers.add(this) } entity.setWorld(world) updateCurrentlyViewedChunks() sendChunksAndLight() - sendEntitiesInViewedChunks() } /** @@ -93,15 +94,4 @@ class UranosPlayer( chunks.forEach { session.send(ChunkLightDataPacket(it.key, it.getLightData(this))) } chunks.forEach { session.send(ChunkDataPacket(it.key, it.getData(this))) } } - - private suspend fun sendEntitiesInViewedChunks() { - val world = entity.safeWorld - val entities = world { world.entities.toList() } - - entities - .asSequence() - .filter { it != entity && it.viewers.contains(this) } - .filter { entity -> currentlyViewedChunks.any { it.key == entity.chunkKey } } - .forEach { session.sendNextTick(it.createSpawnPacket()) } - } } diff --git a/uranos-server/src/main/kotlin/space/uranos/world/GetInternalsIfRegistered.kt b/uranos-server/src/main/kotlin/space/uranos/world/GetInternalsIfRegistered.kt new file mode 100644 index 0000000..651774c --- /dev/null +++ b/uranos-server/src/main/kotlin/space/uranos/world/GetInternalsIfRegistered.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2020-2021 Moritz Ruth and Uranos contributors + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file + */ + +package space.uranos.world + +import space.uranos.Uranos +import space.uranos.UranosServer + +fun World.getInternalsIfRegistered(): World.Internals = + (Uranos as UranosServer).worldRegistry.internals[this] ?: throw IllegalStateException("World not registered") diff --git a/uranos-server/src/main/kotlin/space/uranos/world/UranosWorldRegistry.kt b/uranos-server/src/main/kotlin/space/uranos/world/UranosWorldRegistry.kt new file mode 100644 index 0000000..bc63c88 --- /dev/null +++ b/uranos-server/src/main/kotlin/space/uranos/world/UranosWorldRegistry.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2020-2021 Moritz Ruth and Uranos contributors + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file + */ + +package space.uranos.world + +class UranosWorldRegistry : WorldRegistry() { + val internals = internalItems +}