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 { allprojects {
tasks.withType<KotlinCompile> { tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "14" kotlinOptions.jvmTarget = "14"
kotlinOptions.languageVersion = "1.4"
kotlinOptions.freeCompilerArgs += "-progressive" kotlinOptions.freeCompilerArgs += "-progressive"
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.ExperimentalStdlibApi" kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.ExperimentalStdlibApi"
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.ExperimentalUnsignedTypes" 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" const val TAG_PACKAGE = "$BASE_PACKAGE.tag"
val MATERIAL_TYPE = ClassName(BLOCK_PACKAGE, "Material") val MATERIAL_TYPE = ClassName(BLOCK_PACKAGE, "Material")
val BLOCK_TYPE = ClassName(BLOCK_PACKAGE, "Block") val BLOCK_TYPE = ClassName(BLOCK_PACKAGE, "Block")
val ENTITY_TYPE = ClassName(ENTITY_PACKAGE, "Entity")
val ITEM_TYPE_TYPE = ClassName(ITEM_PACKAGE, "ItemType") val ITEM_TYPE_TYPE = ClassName(ITEM_PACKAGE, "ItemType")
val ENTITY_TYPE_TYPE = ClassName(ENTITY_PACKAGE, "EntityType") val ENTITY_TYPE_TYPE = ClassName(ENTITY_PACKAGE, "EntityType")
val TAG_TYPE = ClassName(TAG_PACKAGE, "Tag") val TAG_TYPE = ClassName(TAG_PACKAGE, "Tag")

View file

@ -73,33 +73,27 @@ class EntitiesGenerator(
val name = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, entity.get("name").toString())!! + val name = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, entity.get("name").toString())!! +
"Entity" "Entity"
val filePathRelativeToSourceRoot = "./${ENTITY_PACKAGE.replace(".", "/")}/$name.kt" if (name == "PaintingEntity") continue
if (sourcesDir.resolve(filePathRelativeToSourceRoot).exists()) continue
val type = TypeSpec.classBuilder(name) val path = "./${ENTITY_PACKAGE.replace(".", "/")}/$name.kt"
.superclass(ENTITY_TYPE) if (sourcesDir.resolve(path).exists()) continue
.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()
FileSpec.builder(ENTITY_PACKAGE, name) outputDir.resolve(path).writeText(
.addType(type) """
.build() package $ENTITY_PACKAGE
.writeTo(outputDir)
// open class AreaEffectCloudEntity : Entity() {
// final override val type: EntityType = Type
//
// companion object Type : AreaEffectCloudEntityType()
// }
""".trimIndent()
)
} }
} }
private fun generateEntityTypeList(entities: List<JsonAny>) { private fun generateEntityTypeList(entities: List<JsonAny>) {
val names = entities val names = entities.asSequence()
.map { it.get("name").toString() } .map { it.get("name").toString() }
.map { CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, it) + "Entity" } .map { CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, it) + "Entity" }
@ -107,7 +101,12 @@ class EntitiesGenerator(
"ENTITY_TYPES", "ENTITY_TYPES",
List::class.asTypeName().parameterizedBy(ENTITY_TYPE_TYPE) 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() .build()
FileSpec.builder(ENTITY_PACKAGE, "EntityTypes") FileSpec.builder(ENTITY_PACKAGE, "EntityTypes")

View file

@ -5,14 +5,26 @@
package space.uranos.entity package space.uranos.entity
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock 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.TickSynchronizationContainer
import space.uranos.util.WatchableSet
import space.uranos.util.memoized
import space.uranos.world.Chunk
import space.uranos.world.VoxelLocation
import space.uranos.world.World import space.uranos.world.World
import java.util.* 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() protected val container = TickSynchronizationContainer()
@Deprecated( @Deprecated(
@ -25,7 +37,8 @@ abstract class Entity internal constructor() {
container.tick() container.tick()
} }
open fun onTick() {} protected open fun onTick() {}
protected open fun onWorldSet() {}
/** /**
* The UUID of this entity. * The UUID of this entity.
@ -38,6 +51,25 @@ abstract class Entity internal constructor() {
open val uuid: UUID = UUID.randomUUID() open val uuid: UUID = UUID.randomUUID()
abstract val type: EntityType 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() private val worldMutex = Mutex()
var world: World? = null; protected set var world: World? = null; protected set
@ -53,6 +85,8 @@ abstract class Entity internal constructor() {
this.world = world this.world = world
world?.internalEntities?.add(this) 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. * An integer unique to this entity which will not be persisted, for example when the entity is serialized.
*/ */
@Suppress("LeakingThis") @Suppress("LeakingThis")
@Deprecated("This is an internal value that you usually should not use.", ReplaceWith("uuid"))
val numericID: Int = Uranos.registerEntity(this) 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 package space.uranos.entity
import space.uranos.Position 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 final override val type: EntityType = Type
override val chunkKey: Chunk.Key get() = Chunk.Key.from(position)
companion object Type : ItemEntityType() 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. * 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 package space.uranos.entity
import space.uranos.Position import space.uranos.Position
import space.uranos.Vector
import space.uranos.player.Player import space.uranos.player.Player
import space.uranos.world.World import space.uranos.world.World
@ -21,7 +20,6 @@ open class PlayerEntity(
override var headPitch: Float = 0f override var headPitch: Float = 0f
) : LivingEntity(position) { ) : LivingEntity(position) {
final override val type: EntityType = Type 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!!`. * 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. * 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 internalEntities: MutableSet<Entity> = Collections.newSetFromMap(WeakHashMap())
private val nextEntityID = AtomicInteger() val entities: Set<Entity> = internalEntities
private val nextEntityID = AtomicInteger(1)
/** /**
* Returns the UID for [entity]. * Returns the UID for [entity].
*/ */
internal fun registerEntity(entity: Entity): Int { internal fun registerEntity(entity: Entity): Int {
entities.add(entity) internalEntities.add(entity)
return nextEntityID.getAndIncrement() 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.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import space.uranos.Position
import space.uranos.Uranos import space.uranos.Uranos
import space.uranos.player.Player import space.uranos.player.Player
import kotlin.math.floor import kotlin.math.floor
@ -26,6 +27,11 @@ abstract class Chunk(
floor(location.x.toFloat() / LENGTH).toInt(), floor(location.x.toFloat() / LENGTH).toInt(),
floor(location.z.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)) 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.newSingleThreadDispatcher
import space.uranos.util.untilPossiblyLower import space.uranos.util.untilPossiblyLower
import java.util.* import java.util.*
import java.util.concurrent.CopyOnWriteArraySet import kotlin.collections.HashSet
/** /**
* A Minecraft world. * A Minecraft world.
@ -47,7 +47,7 @@ abstract class World(val uuid: UUID) {
/** /**
* All entities in this world. * All entities in this world.
*/ */
internal val internalEntities = CopyOnWriteArraySet<Entity>() internal val internalEntities = HashSet<Entity>()
val entities get() = internalEntities.toList() 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() { suspend fun destroy() {
// TODO: Move or kick players // TODO: Move or kick players
scope.cancel() 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, CompassTargetPacketCodec,
DeclareCommandsPacketCodec, DeclareCommandsPacketCodec,
DeclareRecipesPacketCodec, DeclareRecipesPacketCodec,
DestroyEntitiesPacketCodec,
DisconnectPacketCodec, DisconnectPacketCodec,
IncomingKeepAlivePacketCodec, IncomingKeepAlivePacketCodec,
IncomingPlayerPositionPacketCodec, IncomingPlayerPositionPacketCodec,

View file

@ -6,7 +6,6 @@
package space.uranos.net.packet.play package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import space.uranos.entity.LivingEntity
import space.uranos.net.MinecraftProtocolDataTypes.writeUUID import space.uranos.net.MinecraftProtocolDataTypes.writeUUID
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec 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.y * 8000).toInt().toShort().toInt())
dst.writeShort((velocity.z * 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 package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf 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.writeUUID
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec 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.y * 8000).toInt().toShort().toInt())
dst.writeShort((velocity.z * 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 io.netty.buffer.ByteBuf
import space.uranos.CardinalDirection import space.uranos.CardinalDirection
import space.uranos.PaintingMotive
import space.uranos.entity.PaintingEntity
import space.uranos.net.MinecraftProtocolDataTypes.writeUUID import space.uranos.net.MinecraftProtocolDataTypes.writeUUID
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.MinecraftProtocolDataTypes.writeVoxelLocation import space.uranos.net.MinecraftProtocolDataTypes.writeVoxelLocation
import space.uranos.net.packet.OutgoingPacketCodec import space.uranos.net.packet.OutgoingPacketCodec
import space.uranos.world.VoxelLocation
import kotlin.math.max
object SpawnPaintingPacketCodec : OutgoingPacketCodec<SpawnPaintingPacket>(0x03, SpawnPaintingPacket::class) { object SpawnPaintingPacketCodec : OutgoingPacketCodec<SpawnPaintingPacket>(0x03, SpawnPaintingPacket::class) {
override fun SpawnPaintingPacket.encode(dst: ByteBuf) { override fun SpawnPaintingPacket.encode(dst: ByteBuf) {
@ -29,19 +25,4 @@ object SpawnPaintingPacketCodec : OutgoingPacketCodec<SpawnPaintingPacket>(0x03,
CardinalDirection.EAST -> 3 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 kotlinx.coroutines.runBlocking
import space.uranos.command.Command import space.uranos.command.Command
import space.uranos.config.UranosConfig 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.UranosEventBus
import space.uranos.event.UranosEventHandlerPositionManager import space.uranos.event.UranosEventHandlerPositionManager
import space.uranos.logging.Logger import space.uranos.logging.Logger
import space.uranos.logging.UranosLoggingOutputProvider import space.uranos.logging.UranosLoggingOutputProvider
import space.uranos.net.UranosSocketServer import space.uranos.net.UranosSocketServer
import space.uranos.net.packet.play.DestroyEntitiesPacket
import space.uranos.net.packet.play.PlayerInfoPacket import space.uranos.net.packet.play.PlayerInfoPacket
import space.uranos.player.UranosPlayer import space.uranos.player.UranosPlayer
import space.uranos.plugin.UranosPluginManager import space.uranos.plugin.UranosPluginManager
import space.uranos.recipe.Recipe import space.uranos.recipe.Recipe
import space.uranos.server.Server import space.uranos.server.Server
import space.uranos.util.EncryptionUtils import space.uranos.util.EncryptionUtils
import space.uranos.util.createSpawnPacket
import space.uranos.util.msToTicks import space.uranos.util.msToTicks
import space.uranos.util.runInServerThread import space.uranos.util.runInServerThread
import space.uranos.world.BiomeRegistry import space.uranos.world.BiomeRegistry
@ -106,6 +110,7 @@ class UranosServer internal constructor() : Server() {
logger info "Listening on ${config.host}:${config.port}" logger info "Listening on ${config.host}:${config.port}"
scheduler.start() scheduler.start()
registerListeners()
startTicking() startTicking()
startPingSync() 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 { companion object {
val VERSION = UranosServer::class.java.`package`.implementationVersion ?: "development" val VERSION = UranosServer::class.java.`package`.implementationVersion ?: "development"
val VERSION_WITH_V = if (VERSION == "development") VERSION else "v$VERSION" 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.buffer.ByteBuf
import io.netty.util.ReferenceCountUtil import io.netty.util.ReferenceCountUtil
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.uranos.event.ifNotCancelled 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" } if (session.server.config.logging.shouldLog(packet)) session.logger.trace { "Packet received: $packet" }
session.server.eventBus.emit(PacketReceivedEvent(session, packet)).ifNotCancelled { 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
)
}
} }
} }

View file

@ -47,6 +47,8 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer
field = value field = value
} }
val earlyPlayer get() = (state as? State.WithPlayer)?.player
override var state: State = State.WaitingForHandshake override var state: State = State.WaitingForHandshake
override val currentProtocol: Protocol? override val currentProtocol: Protocol?
@ -112,7 +114,7 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer
lateinit var keepAliveDisconnectJob: Job lateinit var keepAliveDisconnectJob: Job
fun scheduleKeepAlivePacket(isFirst: Boolean = false) { fun scheduleKeepAlivePacket(isFirst: Boolean = false) {
scope.launch { scope.launch { // TODO: Fix random disconnects (maybe some response packets are skipped?)
if (!isFirst) { if (!isFirst) {
val timeSinceLastPacket = (System.currentTimeMillis() - lastKeepAlivePacketTimestamp).toInt() val timeSinceLastPacket = (System.currentTimeMillis() - lastKeepAlivePacketTimestamp).toInt()
delay(KEEP_ALIVE_PACKET_INTERVAL.toLong() - timeSinceLastPacket) delay(KEEP_ALIVE_PACKET_INTERVAL.toLong() - timeSinceLastPacket)

View file

@ -10,6 +10,6 @@ import space.uranos.net.UranosSession
object IncomingPlayerPositionPacketHandler : PacketReceivedEventHandler<IncomingPlayerPositionPacket>() { object IncomingPlayerPositionPacketHandler : PacketReceivedEventHandler<IncomingPlayerPositionPacket>() {
override suspend fun handle(session: UranosSession, packet: 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>() { object PlayerLocationPacketHandler : PacketReceivedEventHandler<PlayerLocationPacket>() {
override suspend fun handle(session: UranosSession, packet: 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( player.entity.position = player.entity.position.copy(
x = packet.location.x, x = packet.location.x,
y = packet.location.y, y = packet.location.y,

View file

@ -10,7 +10,7 @@ import space.uranos.net.UranosSession
object PlayerOrientationPacketHandler : PacketReceivedEventHandler<PlayerOrientationPacket>() { object PlayerOrientationPacketHandler : PacketReceivedEventHandler<PlayerOrientationPacket>() {
override suspend fun handle(session: UranosSession, packet: PlayerOrientationPacket) { override suspend fun handle(session: UranosSession, packet: PlayerOrientationPacket) {
val player = session.player!! session.earlyPlayer?.entity?.let { it.position = it.position.copy(yaw = packet.yaw, pitch = packet.pitch) }
player.entity.position = player.entity.position.copy(yaw = packet.yaw, pitch = packet.pitch) ?: error("Player not yet initialized")
} }
} }

View file

@ -6,6 +6,7 @@
package space.uranos.player package space.uranos.player
import space.uranos.Position import space.uranos.Position
import space.uranos.Uranos
import space.uranos.chat.TextComponent import space.uranos.chat.TextComponent
import space.uranos.entity.PlayerEntity import space.uranos.entity.PlayerEntity
import space.uranos.net.UranosSession 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.net.packet.play.SelectedHotbarSlotPacket
import space.uranos.util.TickSynchronizationContainer import space.uranos.util.TickSynchronizationContainer
import space.uranos.util.clampArgument import space.uranos.util.clampArgument
import space.uranos.util.createSpawnPacket
import space.uranos.world.Chunk import space.uranos.world.Chunk
import space.uranos.world.VoxelLocation import space.uranos.world.VoxelLocation
import space.uranos.world.World import space.uranos.world.World
@ -62,9 +64,11 @@ class UranosPlayer(
override val entity: PlayerEntity = PlayerEntity(position, this, headPitch) override val entity: PlayerEntity = PlayerEntity(position, this, headPitch)
suspend fun spawnInitially(world: World) { suspend fun spawnInitially(world: World) {
Uranos.entities.forEach { if (it.visibleToNewPlayers) it.viewers.add(this) }
entity.setWorld(world) entity.setWorld(world)
updateCurrentlyViewedChunks() updateCurrentlyViewedChunks()
sendChunksAndLight() sendChunksAndLight()
sendEntitiesInViewedChunks()
} }
/** /**
@ -89,4 +93,15 @@ class UranosPlayer(
chunks.forEach { session.send(ChunkLightDataPacket(it.key, it.getLightData(this))) } chunks.forEach { session.send(ChunkLightDataPacket(it.key, it.getLightData(this))) }
chunks.forEach { session.send(ChunkDataPacket(it.key, it.getData(this))) } chunks.forEach { session.send(ChunkDataPacket(it.key, it.getData(this))) }
} }
private suspend fun sendEntitiesInViewedChunks() {
val world = entity.safeWorld
val entities = world { world.entities.toList() }
entities
.asSequence()
.filter { it != entity && it.viewers.contains(this) }
.filter { entity -> currentlyViewedChunks.any { it.key == entity.chunkKey } }
.forEach { session.sendNextTick(it.createSpawnPacket()) }
}
} }