Archived
1
0
Fork 0

Sync entities when joining or when viewers change

This commit is contained in:
Moritz Ruth 2021-01-10 14:40:03 +01:00
parent 1b98b19f16
commit 6c6b9c74f4
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
31 changed files with 371 additions and 145 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,5 +14,5 @@ interface Mobile {
/**
* The velocity in blocks per tick.
*/
var velocity: Vector
var velocity: Vector // TODO: Move the entity every tick
}

View file

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

View file

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

View file

@ -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!!`.

View file

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

View file

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

View file

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

View file

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

View 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")
}

View file

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

View file

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

View file

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

View file

@ -16,6 +16,7 @@ object PlayProtocol : Protocol(
CompassTargetPacketCodec,
DeclareCommandsPacketCodec,
DeclareRecipesPacketCodec,
DestroyEntitiesPacketCodec,
DisconnectPacketCodec,
IncomingKeepAlivePacketCodec,
IncomingPlayerPositionPacketCodec,

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,15 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.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) }
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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