Archived
1
0
Fork 0

Move player implementations out of API project

This commit is contained in:
Moritz Ruth 2021-01-10 22:57:04 +01:00
parent 6c6b9c74f4
commit 792536ae7a
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
29 changed files with 471 additions and 355 deletions

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View file

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View file

@ -20,6 +20,12 @@
- Scoreboards + Teams - Scoreboards + Teams
- Crafting - 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 ## Conventions
### KDoc ### KDoc

View file

@ -16,7 +16,7 @@ const val TAG_PACKAGE = "$BASE_PACKAGE.tag"
val MATERIAL_TYPE = ClassName(BLOCK_PACKAGE, "Material") val MATERIAL_TYPE = ClassName(BLOCK_PACKAGE, "Material")
val BLOCK_TYPE = ClassName(BLOCK_PACKAGE, "Block") val BLOCK_TYPE = ClassName(BLOCK_PACKAGE, "Block")
val ITEM_TYPE_TYPE = ClassName(ITEM_PACKAGE, "ItemType") val ITEM_TYPE_TYPE = ClassName(ITEM_PACKAGE, "ItemType")
val ENTITY_TYPE = ClassName(ENTITY_PACKAGE, "Entity")
val ENTITY_TYPE_TYPE = ClassName(ENTITY_PACKAGE, "EntityType") val ENTITY_TYPE_TYPE = ClassName(ENTITY_PACKAGE, "EntityType")
val TAG_TYPE = ClassName(TAG_PACKAGE, "Tag") val TAG_TYPE = ClassName(TAG_PACKAGE, "Tag")
val TAG_TYPE_TYPE = TAG_TYPE.nestedClass("Type") val TAG_TYPE_TYPE = TAG_TYPE.nestedClass("Type")

View file

@ -11,6 +11,7 @@ import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import space.uranos.mdsp.JsonAny import space.uranos.mdsp.JsonAny
import java.io.File import java.io.File
import kotlin.reflect.KClass
class EntitiesGenerator( class EntitiesGenerator(
private val workingDir: File, private val workingDir: File,
@ -28,13 +29,28 @@ class EntitiesGenerator(
private fun generateEntityTypes(types: List<JsonAny>) { private fun generateEntityTypes(types: List<JsonAny>) {
for (entity in types) { for (entity in types) {
val name = val entityClassName =
CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, entity.get("name").toString())!! + "EntityType" 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) val type = TypeSpec.classBuilder(name)
.addModifiers(KModifier.ABSTRACT) .addModifiers(KModifier.ABSTRACT)
.primaryConstructor(FunSpec.constructorBuilder().addModifiers(KModifier.INTERNAL).build()) .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( .addProperty(
PropertySpec PropertySpec
.builder("numericID", Int::class, KModifier.OVERRIDE) .builder("numericID", Int::class, KModifier.OVERRIDE)
@ -78,17 +94,19 @@ class EntitiesGenerator(
val path = "./${ENTITY_PACKAGE.replace(".", "/")}/$name.kt" val path = "./${ENTITY_PACKAGE.replace(".", "/")}/$name.kt"
if (sourcesDir.resolve(path).exists()) continue if (sourcesDir.resolve(path).exists()) continue
outputDir.resolve(path).writeText( val type = TypeSpec.interfaceBuilder(name)
""" .addSuperinterface(ENTITY_TYPE)
package $ENTITY_PACKAGE .addType(
TypeSpec.companionObjectBuilder("Type")
// open class AreaEffectCloudEntity : Entity() { .superclass(ClassName(ENTITY_PACKAGE, name + "Type"))
// final override val type: EntityType = Type .build()
//
// companion object Type : AreaEffectCloudEntityType()
// }
""".trimIndent()
) )
.build()
FileSpec.builder(ENTITY_PACKAGE, name)
.addType(type)
.build()
.writeTo(outputDir)
} }
} }
@ -97,20 +115,32 @@ class EntitiesGenerator(
.map { it.get("name").toString() } .map { it.get("name").toString() }
.map { CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, it) + "Entity" } .map { CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, it) + "Entity" }
val property = PropertySpec.builder( val listProperty = PropertySpec.builder(
"ENTITY_TYPES", "ENTITY_TYPES",
List::class.asTypeName().parameterizedBy(ENTITY_TYPE_TYPE) List::class.asTypeName().parameterizedBy(ENTITY_TYPE_TYPE.parameterizedBy(STAR))
)
.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<T>"
)
.build()
) )
.initializer("listOf(\n${
names.joinToString(",\n") {
val path = "./${ENTITY_PACKAGE.replace(".", "/")}/$it.kt"
if (sourcesDir.resolve(path).exists()) it else "object : ${it}Type() {}"
}
}\n)")
.build() .build()
FileSpec.builder(ENTITY_PACKAGE, "EntityTypes") FileSpec.builder(ENTITY_PACKAGE, "EntityTypes")
.addProperty(property) .addProperty(typeProperty)
.addProperty(listProperty)
.build() .build()
.writeTo(outputDir) .writeTo(outputDir)
} }

View file

@ -18,10 +18,7 @@ import space.uranos.plugin.Plugin
import space.uranos.testplugin.anvil.AnvilWorld import space.uranos.testplugin.anvil.AnvilWorld
import space.uranos.util.RGBColor import space.uranos.util.RGBColor
import space.uranos.util.secondsToTicks import space.uranos.util.secondsToTicks
import space.uranos.world.Biome import space.uranos.world.*
import space.uranos.world.Chunk
import space.uranos.world.Dimension
import space.uranos.world.VoxelLocation
import space.uranos.world.block.GreenWoolBlock import space.uranos.world.block.GreenWoolBlock
import space.uranos.world.block.RedWoolBlock import space.uranos.world.block.RedWoolBlock
@ -49,20 +46,22 @@ class TestPlugin: Plugin("Test", "1.0.0") {
Uranos.dimensionRegistry.register(dimension) Uranos.dimensionRegistry.register(dimension)
Uranos.biomeRegistry.register(biome) 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 { 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() 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.getVoxelsInSphere(VoxelLocation.of(20, 50, 20), 40.0).forEach { it.block = RedWoolBlock() }
world.getChunk(Chunk.Key(0, 0)).setBiome(0, 0, 0, biome) world.getChunk(Chunk.Key(0, 0)).setBiome(0, 0, 0, biome)
Uranos.eventBus.on<ServerListInfoRequestEvent> { event -> Uranos.eventBus.on<ServerListInfoRequestEvent> { event ->
event.response = ServerListInfo("1.16.4", 754, TextComponent of "Test", 10, 0, emptyList()) 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<CowEntity>()
entity.position = Position(0.0, 10.0, 0.0, 0f, 0f)
entity.setWorld(world) entity.setWorld(world)
Uranos.eventBus.on<SessionAfterLoginEvent> { event -> Uranos.eventBus.on<SessionAfterLoginEvent> { event ->

View file

@ -8,6 +8,9 @@ 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 kotlinx.coroutines.CoroutineDispatcher
import space.uranos.entity.Entity
import space.uranos.util.newSingleThreadDispatcher
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
@ -15,8 +18,10 @@ import java.util.*
class AnvilWorld( class AnvilWorld(
override val dimension: Dimension, override val dimension: Dimension,
override val isFlat: Boolean override val isFlat: Boolean,
) : World(UUID.randomUUID()) { override val seed: Long?,
) : World {
override val dispatcher: CoroutineDispatcher = newSingleThreadDispatcher("AnvilWorld")
override val loadedChunks: Map<Chunk.Key, AnvilChunk> get() = chunks.asMap().filter { (_, chunk) -> chunk.loaded } override val loadedChunks: Map<Chunk.Key, AnvilChunk> get() = chunks.asMap().filter { (_, chunk) -> chunk.loaded }
private val chunks: LoadingCache<Chunk.Key, AnvilChunk> = CacheBuilder.newBuilder() private val chunks: LoadingCache<Chunk.Key, AnvilChunk> = CacheBuilder.newBuilder()
@ -27,4 +32,18 @@ class AnvilWorld(
}) })
override fun getChunk(key: Chunk.Key): AnvilChunk = chunks.get(key) override fun getChunk(key: Chunk.Key): AnvilChunk = chunks.get(key)
override val entities = mutableSetOf<Entity>()
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)
}
}
} }

View file

@ -5,10 +5,8 @@
package space.uranos package space.uranos
import java.util.concurrent.ConcurrentHashMap
open class Registry<T: RegistryItem> { open class Registry<T: RegistryItem> {
protected val internalItems = ConcurrentHashMap<String, T>() protected val internalItems = HashMap<String, T>()
val items: Map<String, T> = internalItems val items: Map<String, T> = internalItems
/** /**

View file

@ -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()
}

View file

@ -5,41 +5,15 @@
package space.uranos.entity package space.uranos.entity
import kotlinx.coroutines.launch import com.google.common.cache.CacheBuilder
import kotlinx.coroutines.sync.Mutex import com.google.common.cache.CacheLoader
import kotlinx.coroutines.sync.withLock
import space.uranos.*
import space.uranos.entity.event.ViewingChangedEvent
import space.uranos.event.EventBusWrapper import space.uranos.event.EventBusWrapper
import space.uranos.player.Player 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.Chunk
import space.uranos.world.VoxelLocation
import space.uranos.world.World import space.uranos.world.World
import java.util.* import java.util.UUID
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() {}
interface Entity {
/** /**
* The UUID of this entity. * The UUID of this entity.
* *
@ -48,98 +22,40 @@ sealed class Entity {
* *
* Otherwise, it is usually randomly generated. * Otherwise, it is usually randomly generated.
*/ */
open val uuid: UUID = UUID.randomUUID() val uuid: UUID
abstract val type: EntityType
abstract val chunkKey: Chunk.Key
/**
* Players that can see this entity.
*/
val viewers: MutableSet<Player> = object : WatchableSet<Player>(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")
/** /**
* An integer unique to this entity which will not be persisted, for example when the entity is serialized. * An integer unique to this entity which will not be persisted, for example when the entity is serialized.
*/ */
@Suppress("LeakingThis") val numericID: Int
val numericID: Int = Uranos.registerEntity(this)
/**
* Players that can see this entity.
*/
val viewers: MutableSet<Player>
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 * Returns [Entity.world] if it is not null, otherwise throws [IllegalStateException].
override var velocity: Vector = Vector.ZERO */
fun Entity.getWorldOrFail() = world ?: throw IllegalStateException("This entity has not been spawned")
override var position: Position by container.ifChanged(position) { value -> private val eventBusWrapperCache = CacheBuilder.newBuilder()
// TODO: Broadcast to players .weakKeys()
} .build(object : CacheLoader<Entity, EventBusWrapper<Entity>>() {
override fun load(key: Entity): EventBusWrapper<Entity> = EventBusWrapper(key)
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()
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : Entity> T.events(): EventBusWrapper<T> { val <T : Entity> T.events
val wrapper = eventBusWrapper get() = eventBusWrapperCache.get(this) as EventBusWrapper<T>
return if (wrapper == null) EventBusWrapper<T>(this).also { eventBusWrapper = it }
else wrapper as EventBusWrapper<T>
}

View file

@ -7,7 +7,8 @@ package space.uranos.entity
import kotlin.reflect.KClass import kotlin.reflect.KClass
interface EntityType { interface EntityType<T : Entity> {
val interfaceType: KClass<T>
val numericID: Int val numericID: Int
val id: String val id: String
val width: Float val width: Float
@ -17,7 +18,16 @@ interface EntityType {
/** /**
* All entity types, sorted by their numeric ID in ascending order. * All entity types, sorted by their numeric ID in ascending order.
*/ */
val all = ENTITY_TYPES val all: Collection<EntityType<*>> = ENTITY_TYPES
val byID = ENTITY_TYPES.map { it.id to it }.toMap()
val byID: Map<String, EntityType<*>> = all.map { it.id to it }.toMap()
fun byID(id: String) = byID[id]
private val byInterfaceTypeMap: Map<KClass<out Entity>, EntityType<*>> =
ENTITY_TYPES.map { it.interfaceType to it }.toMap()
@Suppress("UNCHECKED_CAST")
fun <T : Entity> byInterfaceType(interfaceType: KClass<T>): EntityType<T> =
byInterfaceTypeMap[interfaceType] as EntityType<T>
} }
} }

View file

@ -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()
}

View file

@ -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
}

View file

@ -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 * 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 {
}

View file

@ -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
)

View file

@ -5,26 +5,16 @@
package space.uranos.entity package space.uranos.entity
import space.uranos.Position
import space.uranos.player.Player import space.uranos.player.Player
import space.uranos.world.World import space.uranos.world.World
open class PlayerEntity( interface PlayerEntity : LivingEntity {
position: Position, val player: Player
/**
* 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
/**
* 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()

View file

@ -12,6 +12,7 @@ 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.entity.Entity
import space.uranos.entity.EntityType
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
@ -23,9 +24,8 @@ import space.uranos.recipe.Recipe
import space.uranos.util.newSingleThreadDispatcher import space.uranos.util.newSingleThreadDispatcher
import space.uranos.world.BiomeRegistry import space.uranos.world.BiomeRegistry
import space.uranos.world.Dimension import space.uranos.world.Dimension
import space.uranos.world.WorldRegistry
import java.io.File import java.io.File
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
abstract class Server { abstract class Server {
abstract val eventBus: EventBus abstract val eventBus: EventBus
@ -55,10 +55,11 @@ abstract class Server {
abstract val serverDirectory: File abstract val serverDirectory: File
val cacheDirectory: File get() = serverDirectory.resolve("cache") val cacheDirectory: File get() = serverDirectory.resolve("cache")
abstract val dimensionRegistry: Registry<Dimension> abstract val worldRegistry: WorldRegistry
abstract val recipeRegistry: Registry<Recipe> val dimensionRegistry: Registry<Dimension> = Registry()
abstract val commandRegistry: Registry<Command> val recipeRegistry: Registry<Recipe> = Registry()
abstract val biomeRegistry: BiomeRegistry val commandRegistry: Registry<Command> = Registry()
val biomeRegistry: BiomeRegistry = BiomeRegistry()
abstract val loggingOutputProvider: LoggingOutputProvider abstract val loggingOutputProvider: LoggingOutputProvider
abstract val scheduler: Scheduler abstract val scheduler: Scheduler
@ -66,27 +67,24 @@ abstract class Server {
abstract val developmentMode: Boolean abstract val developmentMode: Boolean
abstract val minimumLogLevel: Logger.Level abstract val minimumLogLevel: Logger.Level
/**
* Set of all existing [Entity] instances.
*/
abstract val entities: Set<Entity>
/**
* Create a new entity of [type].
*
* @throws IllegalArgumentException When [type] is [PlayerEntity][space.uranos.entity.PlayerEntity].
*/
abstract fun <T : Entity> create(type: EntityType<T>): T
inline fun <reified T : Entity> create(): T = create(EntityType.byInterfaceType(T::class))
/** /**
* Initiates shutting down the server. * Initiates shutting down the server.
*/ */
abstract fun shutdown() abstract fun shutdown()
/**
* Set of all existing [Entity] instances.
*/
private val internalEntities: MutableSet<Entity> = Collections.newSetFromMap(WeakHashMap())
val entities: Set<Entity> = 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 { companion object {
const val TICKS_PER_SECOND = 20 const val TICKS_PER_SECOND = 20
} }

View file

@ -16,7 +16,7 @@ abstract class Chunk(
val world: World, val world: World,
val key: Key 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) { data class Key(val x: Int, val z: Int) {
companion object { companion object {

View file

@ -5,35 +5,26 @@
package space.uranos.world 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.Vector
import space.uranos.entity.Entity import space.uranos.entity.Entity
import space.uranos.util.newSingleThreadDispatcher
import space.uranos.util.untilPossiblyLower import space.uranos.util.untilPossiblyLower
import java.util.*
import kotlin.collections.HashSet
/** interface World {
* A Minecraft world. val dispatcher: CoroutineDispatcher
*/ val dimension: Dimension
abstract class World(val uuid: UUID) { val isFlat: Boolean
private val identifier = "World($uuid)" val entities: Collection<Entity>
val loadedChunks: Map<Chunk.Key, Chunk>
private val internalDispatcher = newSingleThreadDispatcher(identifier) interface Internals {
val world: World
/** fun addEntity(entity: Entity)
* A coroutine dispatcher that is confined to the server thread. fun removeEntity(entity: Entity)
*/ }
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<Chunk.Key, Chunk>
abstract val isFlat: Boolean
/** /**
* This can be any value. * This can be any value.
@ -42,33 +33,27 @@ abstract class World(val uuid: UUID) {
* (and it seems not to use it in any way). * (and it seems not to use it in any way).
* A random value will be used if this is null. * A random value will be used if this is null.
*/ */
open val seed: Long? = null val seed: Long?
/** fun getChunk(key: Chunk.Key): Chunk
* All entities in this world. }
*/
internal val internalEntities = HashSet<Entity>()
val entities get() = internalEntities.toList()
abstract fun getChunk(key: Chunk.Key): Chunk
/** /**
* Returns the chunk containing the voxel at [location]. * Returns the chunk containing the voxel at [location].
*/ */
fun getChunk(location: VoxelLocation): Chunk = getChunk(Chunk.Key.from(location)) fun World.getChunk(location: VoxelLocation): Chunk = getChunk(Chunk.Key.from(location))
/** /**
* Returns the voxel at [location]. * Returns the voxel at [location].
*/ */
fun getVoxel(location: VoxelLocation): Voxel = getChunk(location).getVoxel(location) fun World.getVoxel(location: VoxelLocation): Voxel = getChunk(location).getVoxel(location)
/** /**
* Returns all voxels in the cuboid with the corner points [cornerA] and [cornerB]. * Returns all voxels in the cuboid with the corner points [cornerA] and [cornerB].
* *
* @param hollow Whether the cuboid is hollow * @param hollow Whether the cuboid is hollow
*/ */
fun getVoxelsInCuboid(cornerA: VoxelLocation, cornerB: VoxelLocation, hollow: Boolean = false): List<Voxel> { fun World.getVoxelsInCuboid(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()
else (a untilPossiblyLower b).toList() else (a untilPossiblyLower b).toList()
@ -84,10 +69,11 @@ abstract class World(val uuid: UUID) {
} }
} }
/** /**
* Returns all voxels in a sphere with the specified [center] and [radius]. * Returns all voxels in a sphere with the specified [center] and [radius].
*/ */
fun getVoxelsInSphere( fun World.getVoxelsInSphere(
center: VoxelLocation, center: VoxelLocation,
radius: Double, radius: Double,
wallWidth: Int = Int.MAX_VALUE wallWidth: Int = Int.MAX_VALUE
@ -102,17 +88,5 @@ abstract class World(val uuid: UUID) {
} }
} }
suspend inline operator fun <T> invoke(noinline block: suspend CoroutineScope.() -> T): T = suspend inline fun <T> runIn(world: World, noinline block: suspend CoroutineScope.() -> T): T =
withContext(dispatcher, block) withContext(world.dispatcher, block)
suspend fun destroy() {
// TODO: Move or kick players
scope.cancel()
coroutineScope {
loadedChunks.values.forEach { launch { (it as? Chunk.Unloadable)?.unload() } }
}
internalDispatcher.close()
}
}

View file

@ -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<World, World.Internals>()
val items: Collection<World> get() = internalItems.keys
fun register(internals: World.Internals) {
internalItems[internals.world] = internals
}
}

View file

@ -33,8 +33,9 @@ fun Entity.createSpawnPacket(): OutgoingPacket = when (this) {
uuid, uuid,
motive, motive,
centerLocation, centerLocation,
direction facing
) )
else -> throw IllegalArgumentException("Unknown entity type")
} }
fun ObjectEntity.getDataValue(): Int = when (this) { fun ObjectEntity.getDataValue(): Int = when (this) {

View file

@ -9,9 +9,7 @@ import space.uranos.Position
import space.uranos.Vector import space.uranos.Vector
import space.uranos.entity.EntityType import space.uranos.entity.EntityType
import space.uranos.net.packet.OutgoingPacket import space.uranos.net.packet.OutgoingPacket
import space.uranos.world.Chunk import java.util.UUID
import space.uranos.world.ChunkData
import java.util.*
/** /**
* Sent to spawn **living** entities. * Sent to spawn **living** entities.
@ -19,7 +17,7 @@ import java.util.*
data class SpawnLivingEntityPacket( data class SpawnLivingEntityPacket(
val entityID: Int, val entityID: Int,
val uuid: UUID, val uuid: UUID,
val type: EntityType, val type: EntityType<*>,
val position: Position, val position: Position,
val headPitch: Float, val headPitch: Float,
/** /**

View file

@ -9,9 +9,7 @@ import space.uranos.Position
import space.uranos.Vector import space.uranos.Vector
import space.uranos.entity.EntityType import space.uranos.entity.EntityType
import space.uranos.net.packet.OutgoingPacket import space.uranos.net.packet.OutgoingPacket
import space.uranos.world.Chunk import java.util.UUID
import space.uranos.world.ChunkData
import java.util.*
/** /**
* Sent to spawn object entities. * Sent to spawn object entities.
@ -19,7 +17,7 @@ import java.util.*
data class SpawnObjectEntityPacket( data class SpawnObjectEntityPacket(
val entityID: Int, val entityID: Int,
val uuid: UUID, val uuid: UUID,
val type: EntityType, val type: EntityType<*>,
val position: Position, val position: Position,
val data: Int, val data: Int,
val velocity: Vector val velocity: Vector

View file

@ -9,8 +9,8 @@ import com.sksamuel.hoplite.ConfigFilePropertySource
import com.sksamuel.hoplite.ConfigLoader import com.sksamuel.hoplite.ConfigLoader
import com.sksamuel.hoplite.ConfigSource import com.sksamuel.hoplite.ConfigSource
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import space.uranos.command.Command
import space.uranos.config.UranosConfig import space.uranos.config.UranosConfig
import space.uranos.entity.*
import space.uranos.entity.event.ViewingChangedEvent import space.uranos.entity.event.ViewingChangedEvent
import space.uranos.event.EventHandlerPosition import space.uranos.event.EventHandlerPosition
import space.uranos.event.UranosEventBus 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.net.packet.play.PlayerInfoPacket
import space.uranos.player.UranosPlayer import space.uranos.player.UranosPlayer
import space.uranos.plugin.UranosPluginManager import space.uranos.plugin.UranosPluginManager
import space.uranos.recipe.Recipe
import space.uranos.server.Server import space.uranos.server.Server
import space.uranos.util.EncryptionUtils import space.uranos.util.EncryptionUtils
import space.uranos.util.createSpawnPacket import space.uranos.util.createSpawnPacket
import space.uranos.util.msToTicks import space.uranos.util.msToTicks
import space.uranos.util.runInServerThread import space.uranos.util.runInServerThread
import space.uranos.world.BiomeRegistry import space.uranos.world.UranosWorldRegistry
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.atomic.AtomicInteger
import kotlin.system.exitProcess import kotlin.system.exitProcess
// TODO: Consider using DI because this improves testability // TODO: Consider using DI because this improves testability
@ -60,11 +59,6 @@ class UranosServer internal constructor() : Server() {
else it else it
} }
override val dimensionRegistry = Registry<Dimension>()
override val recipeRegistry = Registry<Recipe>()
override val commandRegistry = Registry<Command>()
override val biomeRegistry = BiomeRegistry()
override val loggingOutputProvider = UranosLoggingOutputProvider override val loggingOutputProvider = UranosLoggingOutputProvider
override val scheduler = UranosScheduler() override val scheduler = UranosScheduler()
@ -88,12 +82,35 @@ class UranosServer internal constructor() : Server() {
override val eventBus = UranosEventBus(config.logging.events) override val eventBus = UranosEventBus(config.logging.events)
override val eventHandlerPositions = UranosEventHandlerPositionManager() override val eventHandlerPositions = UranosEventHandlerPositionManager()
override val worldRegistry = UranosWorldRegistry()
private val internalEntities = HashSet<UranosEntity>()
override val entities: Set<Entity> = internalEntities
override fun <T : Entity> create(type: EntityType<T>): 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() { override fun shutdown() {
runBlocking { runBlocking {
scheduler.shutdown() scheduler.shutdown()
} }
} }
private val nextEntityID = AtomicInteger(1)
fun claimEntityID() = nextEntityID.getAndIncrement()
private fun failInitialization(t: Throwable): Nothing { private fun failInitialization(t: Throwable): Nothing {
logger.error("Server initialization failed:", t) logger.error("Server initialization failed:", t)
exitProcess(1) exitProcess(1)
@ -119,12 +136,7 @@ class UranosServer internal constructor() : Server() {
scheduler.executeRepeating(1, 0) { scheduler.executeRepeating(1, 0) {
runInServerThread { runInServerThread {
players.forEach { it.container.tick() } players.forEach { it.container.tick() }
internalEntities.forEach { it.tick() }
entities.forEach {
@Suppress("DEPRECATION_ERROR")
it.tick()
}
sessions.forEach { it.packetsAdapter.tick() } sessions.forEach { it.packetsAdapter.tick() }
} }
} }
@ -142,6 +154,8 @@ class UranosServer internal constructor() : Server() {
private fun registerListeners() { private fun registerListeners() {
eventBus.on<ViewingChangedEvent>(EventHandlerPosition.LAST) { event -> eventBus.on<ViewingChangedEvent>(EventHandlerPosition.LAST) { event ->
if (event.target == event.player.entity) return@on
if (event.viewing) event.player.session.sendNextTick(event.target.createSpawnPacket()) if (event.viewing) event.player.session.sendNextTick(event.target.createSpawnPacket())
else event.player.session.sendNextTick(DestroyEntitiesPacket(arrayOf(event.target.numericID))) else event.player.session.sendNextTick(DestroyEntitiesPacket(arrayOf(event.target.numericID)))
} }

View file

@ -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

View file

@ -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<Player> = object : WatchableSet<Player>(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)
}

View file

@ -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
}

View file

@ -6,9 +6,9 @@
package space.uranos.player package space.uranos.player
import space.uranos.Position import space.uranos.Position
import space.uranos.Uranos
import space.uranos.chat.TextComponent import space.uranos.chat.TextComponent
import space.uranos.entity.PlayerEntity import space.uranos.entity.PlayerEntity
import space.uranos.entity.safeWorld
import space.uranos.net.UranosSession import space.uranos.net.UranosSession
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
@ -16,11 +16,10 @@ import space.uranos.net.packet.play.PlayerInfoPacket
import space.uranos.net.packet.play.SelectedHotbarSlotPacket import space.uranos.net.packet.play.SelectedHotbarSlotPacket
import space.uranos.util.TickSynchronizationContainer import space.uranos.util.TickSynchronizationContainer
import space.uranos.util.clampArgument import space.uranos.util.clampArgument
import space.uranos.util.createSpawnPacket
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
import java.util.* import java.util.UUID
import kotlin.math.abs import kotlin.math.abs
class UranosPlayer( class UranosPlayer(
@ -61,14 +60,16 @@ class UranosPlayer(
override var currentlyViewedChunks = emptyList<Chunk>() override var currentlyViewedChunks = emptyList<Chunk>()
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) { 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) entity.setWorld(world)
updateCurrentlyViewedChunks() updateCurrentlyViewedChunks()
sendChunksAndLight() sendChunksAndLight()
sendEntitiesInViewedChunks()
} }
/** /**
@ -93,15 +94,4 @@ class UranosPlayer(
chunks.forEach { session.send(ChunkLightDataPacket(it.key, it.getLightData(this))) } chunks.forEach { session.send(ChunkLightDataPacket(it.key, it.getLightData(this))) }
chunks.forEach { session.send(ChunkDataPacket(it.key, it.getData(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()) }
}
} }

View file

@ -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")

View file

@ -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
}