Sync entities when joining or when viewers change
This commit is contained in:
parent
1b98b19f16
commit
6c6b9c74f4
31 changed files with 371 additions and 145 deletions
|
@ -16,6 +16,8 @@ repositories {
|
|||
allprojects {
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions.jvmTarget = "14"
|
||||
kotlinOptions.languageVersion = "1.4"
|
||||
|
||||
kotlinOptions.freeCompilerArgs += "-progressive"
|
||||
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.ExperimentalStdlibApi"
|
||||
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.ExperimentalUnsignedTypes"
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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<JsonAny>) {
|
||||
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")
|
||||
|
|
|
@ -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<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
|
||||
|
@ -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 : Entity> T.events(): EventBusWrapper<T> {
|
||||
val wrapper = eventBusWrapper
|
||||
|
||||
return if (wrapper == null) EventBusWrapper<T>(this).also { eventBusWrapper = it }
|
||||
else wrapper as EventBusWrapper<T>
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -14,5 +14,5 @@ interface Mobile {
|
|||
/**
|
||||
* The velocity in blocks per tick.
|
||||
*/
|
||||
var velocity: Vector
|
||||
var velocity: Vector // TODO: Move the entity every tick
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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!!`.
|
||||
|
|
|
@ -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<Entity>()
|
|
@ -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()
|
|
@ -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<Entity> = Collections.newSetFromMap(WeakHashMap())
|
||||
private val nextEntityID = AtomicInteger()
|
||||
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 {
|
||||
entities.add(entity)
|
||||
internalEntities.add(entity)
|
||||
return nextEntityID.getAndIncrement()
|
||||
}
|
||||
|
||||
|
|
|
@ -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<T>(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 <T> memoized(dependingGetter: () -> Any?, initializer: () -> T) = MemoizedDelegate(dependingGetter, initializer)
|
73
uranos-api/src/main/kotlin/space/uranos/util/WatchableSet.kt
Normal file
73
uranos-api/src/main/kotlin/space/uranos/util/WatchableSet.kt
Normal file
|
@ -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<T>(private val backingSet: MutableSet<T>) : MutableSet<T> 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<T>): Boolean = elements.any { add(it) }
|
||||
|
||||
override fun clear() {
|
||||
backingSet.forEach { remove(it) }
|
||||
}
|
||||
|
||||
override fun removeAll(elements: Collection<T>): 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<T>): 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<T> {
|
||||
val iterator = backingSet.iterator()
|
||||
var current: T? = null
|
||||
|
||||
return object : MutableIterator<T> {
|
||||
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<in T>): 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<T> = throw UnsupportedOperationException("Not implemented for WatchableSet")
|
||||
override fun stream(): Stream<T> = throw UnsupportedOperationException("Not implemented for WatchableSet")
|
||||
override fun spliterator(): Spliterator<T> = throw UnsupportedOperationException("Not implemented for WatchableSet")
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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<Entity>()
|
||||
internal val internalEntities = HashSet<Entity>()
|
||||
|
||||
val entities get() = internalEntities.toList()
|
||||
|
||||
|
@ -102,6 +102,9 @@ abstract class World(val uuid: UUID) {
|
|||
}
|
||||
}
|
||||
|
||||
suspend inline operator fun <T> invoke(noinline block: suspend CoroutineScope.() -> T): T =
|
||||
withContext(dispatcher, block)
|
||||
|
||||
suspend fun destroy() {
|
||||
// TODO: Move or kick players
|
||||
scope.cancel()
|
||||
|
|
|
@ -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<DestroyEntitiesPacket>(0x36, DestroyEntitiesPacket::class) {
|
||||
override fun DestroyEntitiesPacket.encode(dst: ByteBuf) {
|
||||
dst.writeVarInt(entityIDs.size)
|
||||
entityIDs.forEach { dst.writeVarInt(it) }
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ object PlayProtocol : Protocol(
|
|||
CompassTargetPacketCodec,
|
||||
DeclareCommandsPacketCodec,
|
||||
DeclareRecipesPacketCodec,
|
||||
DestroyEntitiesPacketCodec,
|
||||
DisconnectPacketCodec,
|
||||
IncomingKeepAlivePacketCodec,
|
||||
IncomingPlayerPositionPacketCodec,
|
||||
|
|
|
@ -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<SpawnLivingEntityPacke
|
|||
dst.writeShort((velocity.y * 8000).toInt().toShort().toInt())
|
||||
dst.writeShort((velocity.z * 8000).toInt().toShort().toInt())
|
||||
}
|
||||
|
||||
fun getPacketFromEntity(entity: LivingEntity) = SpawnLivingEntityPacket(
|
||||
@Suppress("DEPRECATION")
|
||||
entity.numericID,
|
||||
entity.uuid,
|
||||
entity.type,
|
||||
entity.position,
|
||||
entity.headPitch,
|
||||
entity.velocity
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
package space.uranos.net.packet.play
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import space.uranos.entity.ItemEntity
|
||||
import space.uranos.entity.ObjectEntity
|
||||
import space.uranos.net.MinecraftProtocolDataTypes.writeUUID
|
||||
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
|
||||
import space.uranos.net.packet.OutgoingPacketCodec
|
||||
|
@ -28,20 +26,4 @@ object SpawnObjectEntityPacketCodec : OutgoingPacketCodec<SpawnObjectEntityPacke
|
|||
dst.writeShort((velocity.y * 8000).toInt().toShort().toInt())
|
||||
dst.writeShort((velocity.z * 8000).toInt().toShort().toInt())
|
||||
}
|
||||
|
||||
fun getPacketFromEntity(entity: ObjectEntity) = SpawnObjectEntityPacket(
|
||||
@Suppress("DEPRECATION")
|
||||
entity.numericID,
|
||||
entity.uuid,
|
||||
entity.type,
|
||||
entity.position,
|
||||
getDataForEntity(entity),
|
||||
entity.velocity
|
||||
)
|
||||
|
||||
fun getDataForEntity(entity: ObjectEntity): Int = when(entity) {
|
||||
is ItemEntity -> 1
|
||||
// TODO: Add remaining
|
||||
else -> throw IllegalArgumentException("Unknown entity type")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<SpawnPaintingPacket>(0x03, SpawnPaintingPacket::class) {
|
||||
override fun SpawnPaintingPacket.encode(dst: ByteBuf) {
|
||||
|
@ -29,19 +25,4 @@ object SpawnPaintingPacketCodec : OutgoingPacketCodec<SpawnPaintingPacket>(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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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<Int>) : OutgoingPacket(), Mergeable {
|
||||
override fun mergeWith(otherPacket: OutgoingPacket): OutgoingPacket? {
|
||||
return (otherPacket as? DestroyEntitiesPacket)?.let { DestroyEntitiesPacket(it.entityIDs + otherPacket.entityIDs) }
|
||||
}
|
||||
}
|
|
@ -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<ViewingChangedEvent>(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"
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -10,6 +10,6 @@ import space.uranos.net.UranosSession
|
|||
|
||||
object IncomingPlayerPositionPacketHandler : PacketReceivedEventHandler<IncomingPlayerPositionPacket>() {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import space.uranos.net.UranosSession
|
|||
|
||||
object PlayerLocationPacketHandler : PacketReceivedEventHandler<PlayerLocationPacket>() {
|
||||
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,
|
||||
|
|
|
@ -10,7 +10,7 @@ import space.uranos.net.UranosSession
|
|||
|
||||
object PlayerOrientationPacketHandler : PacketReceivedEventHandler<PlayerOrientationPacket>() {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) }
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue