diff --git a/build.gradle.kts b/build.gradle.kts index 1c49252..2ef35c2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,8 @@ repositories { allprojects { tasks.withType { kotlinOptions.jvmTarget = "14" + kotlinOptions.languageVersion = "1.4" + kotlinOptions.freeCompilerArgs += "-progressive" kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.ExperimentalStdlibApi" kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.ExperimentalUnsignedTypes" diff --git a/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/Constants.kt b/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/Constants.kt index e7406b1..c823318 100644 --- a/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/Constants.kt +++ b/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/Constants.kt @@ -15,7 +15,6 @@ const val ITEM_PACKAGE = "$BASE_PACKAGE.item" const val TAG_PACKAGE = "$BASE_PACKAGE.tag" val MATERIAL_TYPE = ClassName(BLOCK_PACKAGE, "Material") val BLOCK_TYPE = ClassName(BLOCK_PACKAGE, "Block") -val ENTITY_TYPE = ClassName(ENTITY_PACKAGE, "Entity") val ITEM_TYPE_TYPE = ClassName(ITEM_PACKAGE, "ItemType") val ENTITY_TYPE_TYPE = ClassName(ENTITY_PACKAGE, "EntityType") val TAG_TYPE = ClassName(TAG_PACKAGE, "Tag") diff --git a/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/EntitiesGenerator.kt b/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/EntitiesGenerator.kt index 58816a4..5fe71f8 100644 --- a/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/EntitiesGenerator.kt +++ b/buildSrc/src/main/kotlin/space/uranos/mdsp/generator/EntitiesGenerator.kt @@ -73,33 +73,27 @@ class EntitiesGenerator( val name = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, entity.get("name").toString())!! + "Entity" - val filePathRelativeToSourceRoot = "./${ENTITY_PACKAGE.replace(".", "/")}/$name.kt" - if (sourcesDir.resolve(filePathRelativeToSourceRoot).exists()) continue + if (name == "PaintingEntity") continue - val type = TypeSpec.classBuilder(name) - .superclass(ENTITY_TYPE) - .addModifiers(KModifier.OPEN) - .addProperty( - PropertySpec.builder("type", ENTITY_TYPE_TYPE, KModifier.OVERRIDE, KModifier.FINAL) - .initializer("Type") - .build() - ) - .addType( - TypeSpec.companionObjectBuilder("Type") - .superclass(ClassName(ENTITY_PACKAGE, name + "Type")) - .build() - ) - .build() + val path = "./${ENTITY_PACKAGE.replace(".", "/")}/$name.kt" + if (sourcesDir.resolve(path).exists()) continue - FileSpec.builder(ENTITY_PACKAGE, name) - .addType(type) - .build() - .writeTo(outputDir) + outputDir.resolve(path).writeText( + """ + package $ENTITY_PACKAGE + + // open class AreaEffectCloudEntity : Entity() { + // final override val type: EntityType = Type + // + // companion object Type : AreaEffectCloudEntityType() + // } + """.trimIndent() + ) } } private fun generateEntityTypeList(entities: List) { - val names = entities + val names = entities.asSequence() .map { it.get("name").toString() } .map { CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, it) + "Entity" } @@ -107,7 +101,12 @@ class EntitiesGenerator( "ENTITY_TYPES", List::class.asTypeName().parameterizedBy(ENTITY_TYPE_TYPE) ) - .initializer("listOf(\n${names.joinToString(",\n")}\n)") + .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() FileSpec.builder(ENTITY_PACKAGE, "EntityTypes") diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/Entity.kt b/uranos-api/src/main/kotlin/space/uranos/entity/Entity.kt index 2f5ee7e..bacceae 100644 --- a/uranos-api/src/main/kotlin/space/uranos/entity/Entity.kt +++ b/uranos-api/src/main/kotlin/space/uranos/entity/Entity.kt @@ -5,14 +5,26 @@ package space.uranos.entity +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import space.uranos.Uranos +import space.uranos.* +import space.uranos.entity.event.ViewingChangedEvent +import space.uranos.event.EventBusWrapper +import space.uranos.player.Player import space.uranos.util.TickSynchronizationContainer +import space.uranos.util.WatchableSet +import space.uranos.util.memoized +import space.uranos.world.Chunk +import space.uranos.world.VoxelLocation import space.uranos.world.World import java.util.* +import kotlin.math.max -abstract class Entity internal constructor() { +private typealias Vector = space.uranos.Vector + +sealed class Entity { + internal var eventBusWrapper: EventBusWrapper<*>? = null protected val container = TickSynchronizationContainer() @Deprecated( @@ -25,7 +37,8 @@ abstract class Entity internal constructor() { container.tick() } - open fun onTick() {} + protected open fun onTick() {} + protected open fun onWorldSet() {} /** * The UUID of this entity. @@ -38,6 +51,25 @@ abstract class Entity internal constructor() { open val uuid: UUID = UUID.randomUUID() abstract val type: EntityType + abstract val chunkKey: Chunk.Key + + /** + * Players that can see this entity. + */ + val viewers: MutableSet = object : WatchableSet(Collections.newSetFromMap(WeakHashMap())) { + override fun onAdd(element: Player) { + Uranos.scope.launch { Uranos.eventBus.emit(ViewingChangedEvent(this@Entity, element, true)) } + } + + override fun onRemove(element: Player) { + Uranos.scope.launch { Uranos.eventBus.emit(ViewingChangedEvent(this@Entity, element, false)) } + } + } + + /** + * If players should be added to [viewers] when they join. + */ + var visibleToNewPlayers: Boolean = true private val worldMutex = Mutex() var world: World? = null; protected set @@ -53,6 +85,8 @@ abstract class Entity internal constructor() { this.world = world world?.internalEntities?.add(this) } + + onWorldSet() } /** @@ -64,6 +98,48 @@ abstract class Entity internal constructor() { * An integer unique to this entity which will not be persisted, for example when the entity is serialized. */ @Suppress("LeakingThis") - @Deprecated("This is an internal value that you usually should not use.", ReplaceWith("uuid")) val numericID: Int = Uranos.registerEntity(this) } + +abstract class LivingEntity(position: Position) : Entity(), Mobile { + abstract var headPitch: Float // TODO: This should probably be headYaw, but wiki.vg says headPitch. And it is only used in the SpawnLivingEntity packet + override var velocity: Vector = Vector.ZERO + + override var position: Position by container.ifChanged(position) { value -> + // TODO: Broadcast to players + } + + override val chunkKey: Chunk.Key by memoized({ position }) { Chunk.Key.from(position) } +} + +abstract class ObjectEntity(position: Position) : Entity(), Mobile { + override var position: Position by container.ifChanged(position) { value -> + // TODO: Broadcast to players + } + + override var velocity: Vector = Vector.ZERO +} + +open class PaintingEntity( + val topLeftLocation: VoxelLocation, + val direction: CardinalDirection, + val motive: PaintingMotive +) : Entity() { + override val type: EntityType = Type + override val chunkKey: Chunk.Key get() = Chunk.Key.from(topLeftLocation) + + val centerLocation = topLeftLocation.copy( + x = max(0, motive.width / 2) + topLeftLocation.x, + z = motive.height / 2 + topLeftLocation.z + ) + + companion object Type : PaintingEntityType() +} + +@Suppress("UNCHECKED_CAST") +fun T.events(): EventBusWrapper { + val wrapper = eventBusWrapper + + return if (wrapper == null) EventBusWrapper(this).also { eventBusWrapper = it } + else wrapper as EventBusWrapper +} diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/ItemEntity.kt b/uranos-api/src/main/kotlin/space/uranos/entity/ItemEntity.kt index e8d5a50..49c7657 100644 --- a/uranos-api/src/main/kotlin/space/uranos/entity/ItemEntity.kt +++ b/uranos-api/src/main/kotlin/space/uranos/entity/ItemEntity.kt @@ -6,9 +6,11 @@ package space.uranos.entity import space.uranos.Position +import space.uranos.world.Chunk -open class ItemEntity(override var position: Position) : ObjectEntity() { +open class ItemEntity(position: Position) : ObjectEntity(position) { final override val type: EntityType = Type + override val chunkKey: Chunk.Key get() = Chunk.Key.from(position) companion object Type : ItemEntityType() } diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/LivingEntity.kt b/uranos-api/src/main/kotlin/space/uranos/entity/LivingEntity.kt deleted file mode 100644 index 12d657c..0000000 --- a/uranos-api/src/main/kotlin/space/uranos/entity/LivingEntity.kt +++ /dev/null @@ -1,18 +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 - -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 - abstract override var velocity: Vector // TODO: Move the entity every tick - - override var position: Position by container.ifChanged(position) { value -> - // TODO: Broadcast to players - } -} diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/Mobile.kt b/uranos-api/src/main/kotlin/space/uranos/entity/Mobile.kt index 52ebe13..e7d5381 100644 --- a/uranos-api/src/main/kotlin/space/uranos/entity/Mobile.kt +++ b/uranos-api/src/main/kotlin/space/uranos/entity/Mobile.kt @@ -14,5 +14,5 @@ interface Mobile { /** * The velocity in blocks per tick. */ - var velocity: Vector + var velocity: Vector // TODO: Move the entity every tick } diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/ObjectEntity.kt b/uranos-api/src/main/kotlin/space/uranos/entity/ObjectEntity.kt deleted file mode 100644 index 211adc8..0000000 --- a/uranos-api/src/main/kotlin/space/uranos/entity/ObjectEntity.kt +++ /dev/null @@ -1,15 +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 - -abstract class ObjectEntity : Entity(), Mobile { - abstract override var position: Position - - final override var velocity: Vector = Vector.ZERO -} diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/PaintingEntity.kt b/uranos-api/src/main/kotlin/space/uranos/entity/PaintingEntity.kt deleted file mode 100644 index 73afd17..0000000 --- a/uranos-api/src/main/kotlin/space/uranos/entity/PaintingEntity.kt +++ /dev/null @@ -1,20 +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.CardinalDirection -import space.uranos.PaintingMotive -import space.uranos.world.VoxelLocation - -class PaintingEntity( - val topLeftLocation: VoxelLocation, - val direction: CardinalDirection, - val motive: PaintingMotive -) : Entity() { - override val type: EntityType = Type - - companion object Type : PaintingEntityType() -} diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/PlayerEntity.kt b/uranos-api/src/main/kotlin/space/uranos/entity/PlayerEntity.kt index a8b040c..5dc410f 100644 --- a/uranos-api/src/main/kotlin/space/uranos/entity/PlayerEntity.kt +++ b/uranos-api/src/main/kotlin/space/uranos/entity/PlayerEntity.kt @@ -6,7 +6,6 @@ package space.uranos.entity import space.uranos.Position -import space.uranos.Vector import space.uranos.player.Player import space.uranos.world.World @@ -21,7 +20,6 @@ open class PlayerEntity( override var headPitch: Float = 0f ) : LivingEntity(position) { final override val type: EntityType = Type - override var velocity: Vector = Vector.ZERO /** * Because [world] is never `null` for player entities, you can use this property instead of writing `world!!`. diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/event/EntityEvent.kt b/uranos-api/src/main/kotlin/space/uranos/entity/event/EntityEvent.kt new file mode 100644 index 0000000..a36f48b --- /dev/null +++ b/uranos-api/src/main/kotlin/space/uranos/entity/event/EntityEvent.kt @@ -0,0 +1,11 @@ +/* + * 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.event + +import space.uranos.entity.Entity +import space.uranos.event.TargetedEvent + +abstract class EntityEvent : TargetedEvent() diff --git a/uranos-api/src/main/kotlin/space/uranos/entity/event/ViewingChangedEvent.kt b/uranos-api/src/main/kotlin/space/uranos/entity/event/ViewingChangedEvent.kt new file mode 100644 index 0000000..866f8dd --- /dev/null +++ b/uranos-api/src/main/kotlin/space/uranos/entity/event/ViewingChangedEvent.kt @@ -0,0 +1,11 @@ +/* + * 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.event + +import space.uranos.entity.Entity +import space.uranos.player.Player + +data class ViewingChangedEvent(override val target: Entity, val player: Player, val viewing: Boolean) : EntityEvent() diff --git a/uranos-api/src/main/kotlin/space/uranos/server/Server.kt b/uranos-api/src/main/kotlin/space/uranos/server/Server.kt index a3606f1..2b00c8a 100644 --- a/uranos-api/src/main/kotlin/space/uranos/server/Server.kt +++ b/uranos-api/src/main/kotlin/space/uranos/server/Server.kt @@ -73,17 +73,17 @@ abstract class Server { /** * Set of all existing [Entity] instances. - * - * This is not public because the instances may not be fully initialized as they are added. */ - protected val entities: MutableSet = Collections.newSetFromMap(WeakHashMap()) - private val nextEntityID = AtomicInteger() + private val internalEntities: MutableSet = Collections.newSetFromMap(WeakHashMap()) + val entities: Set = internalEntities + + private val nextEntityID = AtomicInteger(1) /** * Returns the UID for [entity]. */ internal fun registerEntity(entity: Entity): Int { - entities.add(entity) + internalEntities.add(entity) return nextEntityID.getAndIncrement() } diff --git a/uranos-api/src/main/kotlin/space/uranos/util/MemoizedDelegate.kt b/uranos-api/src/main/kotlin/space/uranos/util/MemoizedDelegate.kt new file mode 100644 index 0000000..a026ce3 --- /dev/null +++ b/uranos-api/src/main/kotlin/space/uranos/util/MemoizedDelegate.kt @@ -0,0 +1,29 @@ +/* + * 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.util + +import kotlin.reflect.KProperty + +class MemoizedDelegate(private val dependingGetter: () -> Any?, private val initializer: () -> T) { + private object UNINITIALIZED + + private var lastDependingValue: Any? = null + private var value: Any? = UNINITIALIZED + + operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + val currentDependingValue = dependingGetter() + + if (value == UNINITIALIZED || (lastDependingValue != currentDependingValue)) { + value = initializer() + lastDependingValue = currentDependingValue + } + + @Suppress("UNCHECKED_CAST") + return value as T + } +} + +fun memoized(dependingGetter: () -> Any?, initializer: () -> T) = MemoizedDelegate(dependingGetter, initializer) diff --git a/uranos-api/src/main/kotlin/space/uranos/util/WatchableSet.kt b/uranos-api/src/main/kotlin/space/uranos/util/WatchableSet.kt new file mode 100644 index 0000000..62cad51 --- /dev/null +++ b/uranos-api/src/main/kotlin/space/uranos/util/WatchableSet.kt @@ -0,0 +1,73 @@ +/* + * 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.util + +import java.util.* +import java.util.function.Predicate +import java.util.stream.Stream + +abstract class WatchableSet(private val backingSet: MutableSet) : MutableSet by backingSet { + abstract fun onAdd(element: T) + abstract fun onRemove(element: T) + + override fun add(element: T): Boolean { + val added = backingSet.add(element) + if (added) onAdd(element) + return added + } + + override fun remove(element: T): Boolean { + val removed = backingSet.remove(element) + if (removed) onRemove(element) + return removed + } + + override fun addAll(elements: Collection): Boolean = elements.any { add(it) } + + override fun clear() { + backingSet.forEach { remove(it) } + } + + override fun removeAll(elements: Collection): Boolean = + if (elements.size > backingSet.size) backingSet.toSet().any { if (elements.contains(it)) remove(it) else false } + else elements.any { remove(it) } + + override fun retainAll(elements: Collection): Boolean = + if (elements.size > backingSet.size) backingSet.toSet().any { if (elements.contains(it)) remove(it) else false } + else elements.any { remove(it) } + + override fun iterator(): MutableIterator { + val iterator = backingSet.iterator() + var current: T? = null + + return object : MutableIterator { + override fun hasNext(): Boolean = iterator.hasNext() + override fun next(): T = iterator.next().also { current = it } + override fun remove() { + iterator.remove() + current?.let { onRemove(it) } + } + } + } + + override fun removeIf(filter: Predicate): Boolean { + val iterator = iterator() + var modified = false + + for (item in iterator) { + if (filter.test(item)) { + modified = true + iterator.remove() + } + } + + return modified + } + + override fun parallelStream(): Stream = throw UnsupportedOperationException("Not implemented for WatchableSet") + override fun stream(): Stream = throw UnsupportedOperationException("Not implemented for WatchableSet") + override fun spliterator(): Spliterator = throw UnsupportedOperationException("Not implemented for WatchableSet") +} diff --git a/uranos-api/src/main/kotlin/space/uranos/world/Chunk.kt b/uranos-api/src/main/kotlin/space/uranos/world/Chunk.kt index 92af9c2..34df482 100644 --- a/uranos-api/src/main/kotlin/space/uranos/world/Chunk.kt +++ b/uranos-api/src/main/kotlin/space/uranos/world/Chunk.kt @@ -7,6 +7,7 @@ package space.uranos.world import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob +import space.uranos.Position import space.uranos.Uranos import space.uranos.player.Player import kotlin.math.floor @@ -26,6 +27,11 @@ abstract class Chunk( floor(location.x.toFloat() / LENGTH).toInt(), floor(location.z.toFloat() / LENGTH).toInt() ) + + fun from(location: Position) = Key( + floor(location.x.toFloat() / LENGTH).toInt(), + floor(location.z.toFloat() / LENGTH).toInt() + ) } fun translateWorldToChunk(x: Int, z: Int) = Pair(Math.floorMod(x, LENGTH), Math.floorMod(z, LENGTH)) diff --git a/uranos-api/src/main/kotlin/space/uranos/world/World.kt b/uranos-api/src/main/kotlin/space/uranos/world/World.kt index 83f2748..68744c6 100644 --- a/uranos-api/src/main/kotlin/space/uranos/world/World.kt +++ b/uranos-api/src/main/kotlin/space/uranos/world/World.kt @@ -11,7 +11,7 @@ import space.uranos.entity.Entity import space.uranos.util.newSingleThreadDispatcher import space.uranos.util.untilPossiblyLower import java.util.* -import java.util.concurrent.CopyOnWriteArraySet +import kotlin.collections.HashSet /** * A Minecraft world. @@ -47,7 +47,7 @@ abstract class World(val uuid: UUID) { /** * All entities in this world. */ - internal val internalEntities = CopyOnWriteArraySet() + internal val internalEntities = HashSet() val entities get() = internalEntities.toList() @@ -102,6 +102,9 @@ abstract class World(val uuid: UUID) { } } + suspend inline operator fun invoke(noinline block: suspend CoroutineScope.() -> T): T = + withContext(dispatcher, block) + suspend fun destroy() { // TODO: Move or kick players scope.cancel() diff --git a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/DestroyEntitiesPacketCodec.kt b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/DestroyEntitiesPacketCodec.kt new file mode 100644 index 0000000..50e286d --- /dev/null +++ b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/DestroyEntitiesPacketCodec.kt @@ -0,0 +1,18 @@ +/* + * 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.net.packet.play + +import io.netty.buffer.ByteBuf +import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt +import space.uranos.net.packet.OutgoingPacketCodec + +object DestroyEntitiesPacketCodec : + OutgoingPacketCodec(0x36, DestroyEntitiesPacket::class) { + override fun DestroyEntitiesPacket.encode(dst: ByteBuf) { + dst.writeVarInt(entityIDs.size) + entityIDs.forEach { dst.writeVarInt(it) } + } +} diff --git a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/PlayProtocol.kt b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/PlayProtocol.kt index ede8bab..c6e23b1 100644 --- a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/PlayProtocol.kt +++ b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/PlayProtocol.kt @@ -16,6 +16,7 @@ object PlayProtocol : Protocol( CompassTargetPacketCodec, DeclareCommandsPacketCodec, DeclareRecipesPacketCodec, + DestroyEntitiesPacketCodec, DisconnectPacketCodec, IncomingKeepAlivePacketCodec, IncomingPlayerPositionPacketCodec, diff --git a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/SpawnLivingEntityPacketCodec.kt b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/SpawnLivingEntityPacketCodec.kt index 59a18c9..09df0aa 100644 --- a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/SpawnLivingEntityPacketCodec.kt +++ b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/SpawnLivingEntityPacketCodec.kt @@ -6,7 +6,6 @@ package space.uranos.net.packet.play import io.netty.buffer.ByteBuf -import space.uranos.entity.LivingEntity import space.uranos.net.MinecraftProtocolDataTypes.writeUUID import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt import space.uranos.net.packet.OutgoingPacketCodec @@ -27,14 +26,4 @@ object SpawnLivingEntityPacketCodec : OutgoingPacketCodec 1 - // TODO: Add remaining - else -> throw IllegalArgumentException("Unknown entity type") - } } diff --git a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/SpawnPaintingPacketCodec.kt b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/SpawnPaintingPacketCodec.kt index 3614a39..3145345 100644 --- a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/SpawnPaintingPacketCodec.kt +++ b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/SpawnPaintingPacketCodec.kt @@ -7,14 +7,10 @@ package space.uranos.net.packet.play import io.netty.buffer.ByteBuf import space.uranos.CardinalDirection -import space.uranos.PaintingMotive -import space.uranos.entity.PaintingEntity import space.uranos.net.MinecraftProtocolDataTypes.writeUUID import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt import space.uranos.net.MinecraftProtocolDataTypes.writeVoxelLocation import space.uranos.net.packet.OutgoingPacketCodec -import space.uranos.world.VoxelLocation -import kotlin.math.max object SpawnPaintingPacketCodec : OutgoingPacketCodec(0x03, SpawnPaintingPacket::class) { override fun SpawnPaintingPacket.encode(dst: ByteBuf) { @@ -29,19 +25,4 @@ object SpawnPaintingPacketCodec : OutgoingPacketCodec(0x03, CardinalDirection.EAST -> 3 }) } - - fun getPacketFromEntity(entity: PaintingEntity) = SpawnPaintingPacket( - @Suppress("DEPRECATION") - entity.numericID, - entity.uuid, - entity.motive, - getCenterLocation(entity.topLeftLocation, entity.motive), - entity.direction - ) - - private fun getCenterLocation(topLeftLocation: VoxelLocation, motive: PaintingMotive): VoxelLocation = - topLeftLocation.copy( - x = max(0, motive.width / 2) + topLeftLocation.x, - z = motive.height / 2 + topLeftLocation.z - ) } diff --git a/uranos-packet-codecs/src/main/kotlin/space/uranos/util/SpawnEntity.kt b/uranos-packet-codecs/src/main/kotlin/space/uranos/util/SpawnEntity.kt new file mode 100644 index 0000000..fdc4505 --- /dev/null +++ b/uranos-packet-codecs/src/main/kotlin/space/uranos/util/SpawnEntity.kt @@ -0,0 +1,44 @@ +/* + * 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.util + +import space.uranos.entity.* +import space.uranos.net.packet.OutgoingPacket +import space.uranos.net.packet.play.SpawnLivingEntityPacket +import space.uranos.net.packet.play.SpawnObjectEntityPacket +import space.uranos.net.packet.play.SpawnPaintingPacket + +fun Entity.createSpawnPacket(): OutgoingPacket = when (this) { + is LivingEntity -> SpawnLivingEntityPacket( + numericID, + uuid, + type, + position, + headPitch, + velocity + ) + is ObjectEntity -> SpawnObjectEntityPacket( + numericID, + uuid, + type, + position, + getDataValue(), + velocity + ) + is PaintingEntity -> SpawnPaintingPacket( + numericID, + uuid, + motive, + centerLocation, + direction + ) +} + +fun ObjectEntity.getDataValue(): Int = when (this) { + is ItemEntity -> 1 + // TODO: Add remaining + else -> throw IllegalArgumentException("Unknown entity type") +} diff --git a/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/DestroyEntitiesPacket.kt b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/DestroyEntitiesPacket.kt new file mode 100644 index 0000000..530ff50 --- /dev/null +++ b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/DestroyEntitiesPacket.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2020-2021 Moritz Ruth and Uranos contributors + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file + */ + +package space.uranos.net.packet.play + +import space.uranos.net.packet.Mergeable +import space.uranos.net.packet.OutgoingPacket + +class DestroyEntitiesPacket(val entityIDs: Array) : OutgoingPacket(), Mergeable { + override fun mergeWith(otherPacket: OutgoingPacket): OutgoingPacket? { + return (otherPacket as? DestroyEntitiesPacket)?.let { DestroyEntitiesPacket(it.entityIDs + otherPacket.entityIDs) } + } +} diff --git a/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt b/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt index d5da1dd..78bae50 100644 --- a/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt +++ b/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt @@ -11,17 +11,21 @@ import com.sksamuel.hoplite.ConfigSource import kotlinx.coroutines.runBlocking import space.uranos.command.Command import space.uranos.config.UranosConfig +import space.uranos.entity.event.ViewingChangedEvent +import space.uranos.event.EventHandlerPosition import space.uranos.event.UranosEventBus import space.uranos.event.UranosEventHandlerPositionManager import space.uranos.logging.Logger import space.uranos.logging.UranosLoggingOutputProvider import space.uranos.net.UranosSocketServer +import space.uranos.net.packet.play.DestroyEntitiesPacket import space.uranos.net.packet.play.PlayerInfoPacket import space.uranos.player.UranosPlayer import space.uranos.plugin.UranosPluginManager import space.uranos.recipe.Recipe import space.uranos.server.Server import space.uranos.util.EncryptionUtils +import space.uranos.util.createSpawnPacket import space.uranos.util.msToTicks import space.uranos.util.runInServerThread import space.uranos.world.BiomeRegistry @@ -106,6 +110,7 @@ class UranosServer internal constructor() : Server() { logger info "Listening on ${config.host}:${config.port}" scheduler.start() + registerListeners() startTicking() startPingSync() } @@ -135,6 +140,13 @@ class UranosServer internal constructor() : Server() { } } + private fun registerListeners() { + eventBus.on(EventHandlerPosition.LAST) { event -> + if (event.viewing) event.player.session.sendNextTick(event.target.createSpawnPacket()) + else event.player.session.sendNextTick(DestroyEntitiesPacket(arrayOf(event.target.numericID))) + } + } + companion object { val VERSION = UranosServer::class.java.`package`.implementationVersion ?: "development" val VERSION_WITH_V = if (VERSION == "development") VERSION else "v$VERSION" diff --git a/uranos-server/src/main/kotlin/space/uranos/net/PacketsAdapter.kt b/uranos-server/src/main/kotlin/space/uranos/net/PacketsAdapter.kt index 47ff18b..395d107 100644 --- a/uranos-server/src/main/kotlin/space/uranos/net/PacketsAdapter.kt +++ b/uranos-server/src/main/kotlin/space/uranos/net/PacketsAdapter.kt @@ -7,6 +7,7 @@ package space.uranos.net import io.netty.buffer.ByteBuf import io.netty.util.ReferenceCountUtil +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import space.uranos.event.ifNotCancelled @@ -39,7 +40,17 @@ class PacketsAdapter(val session: UranosSession) { if (session.server.config.logging.shouldLog(packet)) session.logger.trace { "Packet received: $packet" } session.server.eventBus.emit(PacketReceivedEvent(session, packet)).ifNotCancelled { - SessionPacketReceivedEventHandler.handle(session, packet) + try { + SessionPacketReceivedEventHandler.handle(session, packet) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + session.logger.error( + "An error occurred while handling a packet " + + "(${packet::class.simpleName!!.removeSuffix("Packet")})", + e + ) + } } } diff --git a/uranos-server/src/main/kotlin/space/uranos/net/UranosSession.kt b/uranos-server/src/main/kotlin/space/uranos/net/UranosSession.kt index 184e804..5e4ea44 100644 --- a/uranos-server/src/main/kotlin/space/uranos/net/UranosSession.kt +++ b/uranos-server/src/main/kotlin/space/uranos/net/UranosSession.kt @@ -47,6 +47,8 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer field = value } + val earlyPlayer get() = (state as? State.WithPlayer)?.player + override var state: State = State.WaitingForHandshake override val currentProtocol: Protocol? @@ -112,7 +114,7 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer lateinit var keepAliveDisconnectJob: Job fun scheduleKeepAlivePacket(isFirst: Boolean = false) { - scope.launch { + scope.launch { // TODO: Fix random disconnects (maybe some response packets are skipped?) if (!isFirst) { val timeSinceLastPacket = (System.currentTimeMillis() - lastKeepAlivePacketTimestamp).toInt() delay(KEEP_ALIVE_PACKET_INTERVAL.toLong() - timeSinceLastPacket) diff --git a/uranos-server/src/main/kotlin/space/uranos/net/packet/play/IncomingPlayerPositionPacketHandler.kt b/uranos-server/src/main/kotlin/space/uranos/net/packet/play/IncomingPlayerPositionPacketHandler.kt index efd39b6..fbcb44a 100644 --- a/uranos-server/src/main/kotlin/space/uranos/net/packet/play/IncomingPlayerPositionPacketHandler.kt +++ b/uranos-server/src/main/kotlin/space/uranos/net/packet/play/IncomingPlayerPositionPacketHandler.kt @@ -10,6 +10,6 @@ import space.uranos.net.UranosSession object IncomingPlayerPositionPacketHandler : PacketReceivedEventHandler() { override suspend fun handle(session: UranosSession, packet: IncomingPlayerPositionPacket) { - session.player!!.entity.position = packet.position + session.earlyPlayer?.let { it.entity.position = packet.position } ?: error("Player not yet initialized") } } diff --git a/uranos-server/src/main/kotlin/space/uranos/net/packet/play/PlayerLocationPacketHandler.kt b/uranos-server/src/main/kotlin/space/uranos/net/packet/play/PlayerLocationPacketHandler.kt index 7e97ad9..e99d59b 100644 --- a/uranos-server/src/main/kotlin/space/uranos/net/packet/play/PlayerLocationPacketHandler.kt +++ b/uranos-server/src/main/kotlin/space/uranos/net/packet/play/PlayerLocationPacketHandler.kt @@ -10,7 +10,7 @@ import space.uranos.net.UranosSession object PlayerLocationPacketHandler : PacketReceivedEventHandler() { override suspend fun handle(session: UranosSession, packet: PlayerLocationPacket) { - val player = session.player!! + val player = session.earlyPlayer ?: error("Player not yet initialized") player.entity.position = player.entity.position.copy( x = packet.location.x, y = packet.location.y, diff --git a/uranos-server/src/main/kotlin/space/uranos/net/packet/play/PlayerOrientationPacketHandler.kt b/uranos-server/src/main/kotlin/space/uranos/net/packet/play/PlayerOrientationPacketHandler.kt index e03fd87..652ea2f 100644 --- a/uranos-server/src/main/kotlin/space/uranos/net/packet/play/PlayerOrientationPacketHandler.kt +++ b/uranos-server/src/main/kotlin/space/uranos/net/packet/play/PlayerOrientationPacketHandler.kt @@ -10,7 +10,7 @@ import space.uranos.net.UranosSession object PlayerOrientationPacketHandler : PacketReceivedEventHandler() { override suspend fun handle(session: UranosSession, packet: PlayerOrientationPacket) { - val player = session.player!! - player.entity.position = player.entity.position.copy(yaw = packet.yaw, pitch = packet.pitch) + session.earlyPlayer?.entity?.let { it.position = it.position.copy(yaw = packet.yaw, pitch = packet.pitch) } + ?: error("Player not yet initialized") } } diff --git a/uranos-server/src/main/kotlin/space/uranos/player/UranosPlayer.kt b/uranos-server/src/main/kotlin/space/uranos/player/UranosPlayer.kt index ec8ffb2..4896615 100644 --- a/uranos-server/src/main/kotlin/space/uranos/player/UranosPlayer.kt +++ b/uranos-server/src/main/kotlin/space/uranos/player/UranosPlayer.kt @@ -6,6 +6,7 @@ package space.uranos.player import space.uranos.Position +import space.uranos.Uranos import space.uranos.chat.TextComponent import space.uranos.entity.PlayerEntity import space.uranos.net.UranosSession @@ -15,6 +16,7 @@ import space.uranos.net.packet.play.PlayerInfoPacket import space.uranos.net.packet.play.SelectedHotbarSlotPacket import space.uranos.util.TickSynchronizationContainer import space.uranos.util.clampArgument +import space.uranos.util.createSpawnPacket import space.uranos.world.Chunk import space.uranos.world.VoxelLocation import space.uranos.world.World @@ -62,9 +64,11 @@ class UranosPlayer( override val entity: PlayerEntity = PlayerEntity(position, this, headPitch) suspend fun spawnInitially(world: World) { + Uranos.entities.forEach { if (it.visibleToNewPlayers) it.viewers.add(this) } entity.setWorld(world) updateCurrentlyViewedChunks() sendChunksAndLight() + sendEntitiesInViewedChunks() } /** @@ -89,4 +93,15 @@ class UranosPlayer( chunks.forEach { session.send(ChunkLightDataPacket(it.key, it.getLightData(this))) } chunks.forEach { session.send(ChunkDataPacket(it.key, it.getData(this))) } } + + private suspend fun sendEntitiesInViewedChunks() { + val world = entity.safeWorld + val entities = world { world.entities.toList() } + + entities + .asSequence() + .filter { it != entity && it.viewers.contains(this) } + .filter { entity -> currentlyViewedChunks.any { it.key == entity.chunkKey } } + .forEach { session.sendNextTick(it.createSpawnPacket()) } + } }