Move player implementations out of API project
This commit is contained in:
parent
6c6b9c74f4
commit
792536ae7a
29 changed files with 471 additions and 355 deletions
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
|
</state>
|
||||||
|
</component>
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
}
|
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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()) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
|
@ -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
|
||||||
|
}
|
Reference in a new issue