Archived
1
0
Fork 0

Implement entity metadata synchronization

This commit is contained in:
Moritz Ruth 2021-04-07 14:12:13 +02:00
parent 0b1ba947a1
commit b51ed1c291
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
48 changed files with 459 additions and 179 deletions

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="LeakingThis" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

View file

@ -2,7 +2,7 @@
Uranos is a game server implementing the Minecraft protocol. That means you can use the official Minecraft client to join Uranos servers. Uranos is a game server implementing the Minecraft protocol. That means you can use the official Minecraft client to join Uranos servers.
Its goal is to be a modern alternative to Bukkit, SpigotMC and Paper. It is primarily intended to make creating custom games inside of Minecraft easier than it Its goal is to be a modern alternative to Bukkit, Spigot and Paper. It is primarily intended to make creating custom games inside of Minecraft easier than it
is possible with the existing alternatives, but it can also be used for something like a lobby/hub server. is possible with the existing alternatives, but it can also be used for something like a lobby/hub server.
The most important thing for Uranos is The most important thing for Uranos is
@ -10,7 +10,6 @@ The most important thing for Uranos is
## Milestones ## Milestones
- Players can see entities
- Players can see other players - Players can see other players
- Players can send and receive chat messages - Players can send and receive chat messages
- Players can see Titles - Players can see Titles
@ -24,6 +23,7 @@ The most important thing for Uranos is
- Command framework + permissions - Command framework + permissions
- Commands can be sent from the console - Commands can be sent from the console
- Players can be teleported between worlds - Players can be teleported between worlds
- All entities are implemented with all metadata
- Entity AI framework - Entity AI framework
- Scoreboards + Teams - Scoreboards + Teams
- Crafting - Crafting

View file

@ -49,8 +49,8 @@ class MinecraftDataSourcesPlugin : Plugin<Project> {
BlocksAndMaterialGenerator(workingDir, outputDir, sourcesDir).generate() BlocksAndMaterialGenerator(workingDir, outputDir, sourcesDir).generate()
TagsGenerator(workingDir, outputDir).generate() TagsGenerator(workingDir, outputDir).generate()
EntitiesGenerator(workingDir, outputDir, sourcesDir).generate() EntitiesGenerator(workingDir, outputDir, sourcesDir).generate()
FluidIDMapGenerator(workingDir, outputDir, registries).generate() FluidIDMapGenerator(outputDir, registries).generate()
ItemTypeEnumGenerator(workingDir, outputDir, registries).generate() ItemTypeEnumGenerator(outputDir, registries).generate()
} }
} }
} }

View file

@ -7,7 +7,6 @@ import space.uranos.mdsp.JsonAny
import java.io.File import java.io.File
class FluidIDMapGenerator( class FluidIDMapGenerator(
private val workingDir: File,
private val outputDir: File, private val outputDir: File,
private val registries: JsonAny private val registries: JsonAny
) { ) {

View file

@ -8,7 +8,6 @@ import space.uranos.mdsp.util.ConstructorPropertiesHelper
import java.io.File import java.io.File
class ItemTypeEnumGenerator( class ItemTypeEnumGenerator(
private val workingDir: File,
private val outputDir: File, private val outputDir: File,
private val registries: JsonAny private val registries: JsonAny
) { ) {

View file

@ -3,25 +3,22 @@ package space.uranos.testplugin
import space.uranos.Uranos import space.uranos.Uranos
import space.uranos.chat.ChatColor import space.uranos.chat.ChatColor
import space.uranos.chat.TextComponent import space.uranos.chat.TextComponent
import space.uranos.entity.CowEntity
import space.uranos.entity.Position import space.uranos.entity.Position
import space.uranos.entity.RideableMinecartEntity
import space.uranos.net.ServerListInfo import space.uranos.net.ServerListInfo
import space.uranos.net.event.ServerListInfoRequestEvent import space.uranos.net.event.ServerListInfoRequestEvent
import space.uranos.net.event.SessionAfterLoginEvent import space.uranos.net.event.SessionAfterLoginEvent
import space.uranos.net.packet.play.EntityMetadataPacket
import space.uranos.player.GameMode import space.uranos.player.GameMode
import space.uranos.player.event.PlayerReadyEvent
import space.uranos.plugin.Plugin import space.uranos.plugin.Plugin
import space.uranos.testplugin.anvil.AnvilWorld import space.uranos.testplugin.anvil.AnvilWorld
import space.uranos.util.RGBColor import space.uranos.util.RGBColor
import space.uranos.util.runInServerThread
import space.uranos.util.secondsToTicks import space.uranos.util.secondsToTicks
import space.uranos.world.* import space.uranos.world.*
import space.uranos.world.block.GreenWoolBlock import space.uranos.world.block.GreenWoolBlock
import space.uranos.world.block.RedWoolBlock import space.uranos.world.block.RedWoolBlock
class TestPlugin : Plugin("Test", "1.0.0") { class TestPlugin : Plugin("Test", "1.0.0") {
fun enableOptifineFix() { private fun enableOptifineFix() {
Uranos.biomeRegistry.register( Uranos.biomeRegistry.register(
Biome( Biome(
"minecraft:swamp", "minecraft:swamp",
@ -106,35 +103,12 @@ class TestPlugin : Plugin("Test", "1.0.0") {
} }
} }
val entity = Uranos.create<RideableMinecartEntity>() val entity = Uranos.create<CowEntity>()
entity.position = Position(0.0, 4.0, 0.0) entity.position = Position(0.0, 4.0, 0.0)
entity.setWorld(world) entity.setWorld(world)
Uranos.eventBus.on<PlayerReadyEvent> { Uranos.scheduler.executeRepeating(20) {
it.player.session.send(EntityMetadataPacket(entity.numericID, listOf( entity.isBaby = !entity.isBaby
EntityMetadataPacket.MetadataEntry.Byte(0u, 0x00),
EntityMetadataPacket.MetadataEntry.Int(1u, 0x00),
EntityMetadataPacket.MetadataEntry.OptChat(2u, null),
EntityMetadataPacket.MetadataEntry.Boolean(3u, false),
EntityMetadataPacket.MetadataEntry.Boolean(4u, false),
EntityMetadataPacket.MetadataEntry.Boolean(5u, false),
EntityMetadataPacket.MetadataEntry.Int(6u, 0),
EntityMetadataPacket.MetadataEntry.Boolean(12u, true)
)))
}
var x = 0f
var y = -90f
Uranos.scheduler.executeRepeating(1) {
runInServerThread {
entity.yaw = x
entity.pitch = y
}
x += 5f
y += 5f
if (x == 360f) x = 0f
if (y == 90f) y = -90f
} }
} }
} }

View file

@ -9,7 +9,6 @@ import space.uranos.util.newSingleThreadDispatcher
import space.uranos.world.Chunk import space.uranos.world.Chunk
import space.uranos.world.Dimension import space.uranos.world.Dimension
import space.uranos.world.World import space.uranos.world.World
import java.util.*
class AnvilWorld( class AnvilWorld(
override val dimension: Dimension, override val dimension: Dimension,

View file

@ -1,10 +1,8 @@
package space.uranos package space.uranos
enum class CardinalDirection(private val direction: Direction) { enum class CardinalDirection(val direction: Direction) {
NORTH(Direction.NORTH), NORTH(Direction.NORTH),
EAST(Direction.EAST), EAST(Direction.EAST),
SOUTH(Direction.SOUTH), SOUTH(Direction.SOUTH),
WEST(Direction.WEST); WEST(Direction.WEST);
fun asDirection() = direction
} }

View file

@ -47,7 +47,7 @@ sealed class ChatColor(val stringRepresentation: String) {
PINK, PINK,
YELLOW, YELLOW,
WHITE WHITE
).map { it.stringRepresentation to it }.toMap() ).associateBy { it.stringRepresentation }
} }
fun fromString(value: String): ChatColor = fun fromString(value: String): ChatColor =

View file

@ -0,0 +1,5 @@
package space.uranos.entity
interface Ageable: Entity {
var isBaby: Boolean
}

View file

@ -0,0 +1,7 @@
package space.uranos.entity
interface BatEntity : LivingEntity, HasMovableHead {
companion object Type : BatEntityType()
var hanging: Boolean
}

View file

@ -0,0 +1,5 @@
package space.uranos.entity
interface CowEntity : LivingEntity, Ageable {
companion object Type : CowEntityType()
}

View file

@ -1,9 +1,8 @@
package space.uranos.entity package space.uranos.entity
import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader
import space.uranos.event.EventBusWrapper import space.uranos.event.EventBusWrapper
import space.uranos.player.Player import space.uranos.player.Player
import space.uranos.util.collections.createWeakKeysLoadingCache
import space.uranos.world.Chunk import space.uranos.world.Chunk
import space.uranos.world.World import space.uranos.world.World
import java.util.UUID import java.util.UUID
@ -39,16 +38,9 @@ interface Entity {
*/ */
var visibleToNewPlayers: Boolean var visibleToNewPlayers: Boolean
var glowing: Boolean // even experience orbs can glow var glowing: Boolean // It seems like every entity can glow
var invisible: Boolean var invisible: Boolean
/**
* Whether this entity does not produce any sounds.
*/
var silent: Boolean
var ignoreGravity: Boolean
} }
/** /**
@ -56,11 +48,7 @@ interface Entity {
*/ */
fun Entity.getWorldOrFail() = world ?: throw IllegalStateException("This entity has not been spawned") fun Entity.getWorldOrFail() = world ?: throw IllegalStateException("This entity has not been spawned")
private val eventBusWrapperCache = CacheBuilder.newBuilder() private val eventBusWrapperCache = createWeakKeysLoadingCache<Entity, EventBusWrapper<Entity>> { EventBusWrapper(it) }
.weakKeys()
.build(object : CacheLoader<Entity, EventBusWrapper<Entity>>() {
override fun load(key: Entity): EventBusWrapper<Entity> = EventBusWrapper(key)
})
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val <T : Entity> T.events val <T : Entity> T.events

View file

@ -15,11 +15,11 @@ interface EntityType<T : Entity> {
*/ */
val all: Collection<EntityType<*>> = ENTITY_TYPES val all: Collection<EntityType<*>> = ENTITY_TYPES
val byID: Map<String, EntityType<*>> = all.map { it.id to it }.toMap() val byID: Map<String, EntityType<*>> = all.associateBy { it.id }
fun byID(id: String) = byID[id] fun byID(id: String) = byID[id]
private val byInterfaceTypeMap: Map<KClass<out Entity>, EntityType<*>> = private val byInterfaceTypeMap: Map<KClass<out Entity>, EntityType<*>> =
ENTITY_TYPES.map { it.interfaceType to it }.toMap() ENTITY_TYPES.associateBy { it.interfaceType }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : Entity> byInterfaceType(interfaceType: KClass<T>): EntityType<T> = fun <T : Entity> byInterfaceType(interfaceType: KClass<T>): EntityType<T> =

View file

@ -1,4 +1,5 @@
package space.uranos.entity package space.uranos.entity
interface LivingEntity : Entity, Mobile { interface LivingEntity : Entity, Mobile {
// potion effects
} }

View file

@ -6,22 +6,22 @@ interface MinecartEntity: ObjectEntity, YawRotatable, PitchRotatable {
/** /**
* Default is `0`. * Default is `0`.
*/ */
val shakingPower: Int var shakingPower: Int
/** /**
* Default is `1`. * Default is `1`.
*/ */
val shakingDirection: Int var shakingDirection: Int
/** /**
* Default is `0.0`. * Default is `0.0`.
*/ */
val shakingMultiplier: Float var shakingMultiplier: Float
val customBlock: Block? var customBlock: Block?
/** /**
* The Y offset of the block inside the minecart, measured in 16ths of a block. * The Y offset of the block inside the minecart, measured in 16ths of a block.
*/ */
val blockOffset: Int var blockOffset: Int
} }

View file

@ -1,5 +1,3 @@
package space.uranos.entity package space.uranos.entity
interface ObjectEntity : Entity, Mobile { interface ObjectEntity : Entity, Mobile
}

View file

@ -10,7 +10,7 @@ interface PaintingEntity : Entity {
val facing: CardinalDirection val facing: CardinalDirection
val motive: PaintingMotive val motive: PaintingMotive
companion object Type : AreaEffectCloudEntityType() companion object Type : PaintingEntityType()
} }
fun PaintingEntity.calculateCenterLocation() = topLeftLocation.copy( fun PaintingEntity.calculateCenterLocation() = topLeftLocation.copy(

View file

@ -0,0 +1,11 @@
package space.uranos.entity.metadata
enum class Pose {
STANDING,
FLYING,
LYING,
SWIMMING,
SPIN_ATTACKING,
SNEAKING,
DYING
}

View file

@ -0,0 +1,3 @@
package space.uranos.entity.metadata
data class ThreeAxisRotation(val x: Float, val y: Float, val z: Float)

View file

@ -4,8 +4,8 @@ import kotlin.reflect.KClass
abstract class Protocol constructor(val name: String, vararg codecs: PacketCodec<*>) { abstract class Protocol constructor(val name: String, vararg codecs: PacketCodec<*>) {
val codecs = codecs.toSet() val codecs = codecs.toSet()
private val codecsByPacketType = codecs.map { it.dataType to it }.toMap() private val codecsByPacketType = codecs.associateBy { it.dataType }
val incomingPacketCodecsByID = codecs.filterIsInstance<IncomingPacketCodec<*>>().map { it.id to it }.toMap() val incomingPacketCodecsByID = codecs.filterIsInstance<IncomingPacketCodec<*>>().associateBy { it.id }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : Packet> getCodecByType(type: KClass<T>): PacketCodec<T> = codecsByPacketType[type] as PacketCodec<T>? fun <T : Packet> getCodecByType(type: KClass<T>): PacketCodec<T> = codecsByPacketType[type] as PacketCodec<T>?

View file

@ -5,5 +5,5 @@ object TagRegistry {
val tagsByType: Map<Tag.Type, List<Tag>> = tags.groupBy { it.type } val tagsByType: Map<Tag.Type, List<Tag>> = tags.groupBy { it.type }
val tagsByNameByType: Map<Tag.Type, Map<String, Tag>> = val tagsByNameByType: Map<Tag.Type, Map<String, Tag>> =
tagsByType.mapValues { (_, tags) -> tags.map { it.name to it }.toMap() } tagsByType.mapValues { (_, tags) -> tags.associateBy { it.name } }
} }

View file

@ -20,7 +20,7 @@ class TickSynchronizationContainer {
onChange: ((value: T) -> Unit)? = null, onChange: ((value: T) -> Unit)? = null,
onTick: suspend (value: T) -> Unit onTick: suspend (value: T) -> Unit
): Delegate<T> = ): Delegate<T> =
Delegate(initialValue, onChange, { value, changed -> if (changed) onTick(value) }).also { delegates.add(it) } Delegate(initialValue, onChange) { value, changed -> if (changed) onTick(value) }.also { delegates.add(it) }
operator fun <T> invoke( operator fun <T> invoke(
initialValue: T, initialValue: T,

View file

@ -0,0 +1,27 @@
package space.uranos.util.collections
import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader
import com.google.common.cache.LoadingCache
/**
* Creates a loading cache with weak **keys**.
*
* @param create The function invoked to create a new value.
*/
fun <K, V> createWeakKeysLoadingCache(create: (key: K) -> V): LoadingCache<K, V> {
return CacheBuilder.newBuilder().weakKeys().build(object : CacheLoader<K, V>() {
override fun load(key: K): V = create(key)
})
}
/**
* Creates a loading cache with weak **values**.
*
* @param create The function invoked to create a new value.
*/
fun <K, V> createWeakValuesLoadingCache(create: (key: K) -> V): LoadingCache<K, V> {
return CacheBuilder.newBuilder().weakValues().build(object : CacheLoader<K, V>() {
override fun load(key: K): V = create(key)
})
}

View file

@ -1,19 +0,0 @@
package space.uranos.util.collections
import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader
import com.google.common.cache.LoadingCache
/**
* Creates a LoadingCache with weak values.
*
* @param loader The function invoked every time a value is not yet in the cache.
*/
fun <K, V> createWeakValuesLoadingCache(loader: (key: K) -> V): LoadingCache<K, V> {
return CacheBuilder
.newBuilder()
.weakValues()
.build(object : CacheLoader<K, V>() {
override fun load(key: K): V = loader(key)
})
}

View file

@ -35,3 +35,10 @@ fun bitmask(vararg values: Boolean): Int {
values.forEachIndexed { index, value -> mask = mask.setBit(index, value) } values.forEachIndexed { index, value -> mask = mask.setBit(index, value) }
return mask return mask
} }
@JvmName("bitmaskFromArray")
fun bitmask(values: BooleanArray): Int {
var mask = 0
values.forEachIndexed { index, value -> mask = mask.setBit(index, value) }
return mask
}

View file

@ -11,8 +11,6 @@ abstract class Chunk(
val world: World, val world: World,
val key: Key val key: Key
) { ) {
private val identifier = "Chunk(${key.x}-${key.z})"
data class Key(val x: Int, val z: Int) { data class Key(val x: Int, val z: Int) {
companion object { companion object {
/** /**

View file

@ -10,6 +10,8 @@ import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.full.starProjectedType import kotlin.reflect.full.starProjectedType
import kotlin.reflect.jvm.jvmErasure import kotlin.reflect.jvm.jvmErasure
// TODO: Move this class and Attribute to uranos-packet-codecs
class BlockCodec<T : Block> internal constructor( class BlockCodec<T : Block> internal constructor(
blockClass: KClass<T>, blockClass: KClass<T>,
val id: Int, val id: Int,

View file

@ -17,9 +17,9 @@ interface Material<T : Block> {
* All materials, sorted by their numeric ID in ascending order. * All materials, sorted by their numeric ID in ascending order.
*/ */
val all = GENERATED_BLOCKS val all = GENERATED_BLOCKS
val byID = GENERATED_BLOCKS.map { it.id to it }.toMap() val byID = GENERATED_BLOCKS.associateBy { it.id }
private val byClass = GENERATED_BLOCKS.map { it.blockClass to it }.toMap() private val byClass = GENERATED_BLOCKS.associateBy { it.blockClass }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : Block> byClass(blockClass: KClass<T>): Material<T>? = byClass[blockClass] as Material<T>? fun <T : Block> byClass(blockClass: KClass<T>): Material<T>? = byClass[blockClass] as Material<T>?

View file

@ -42,7 +42,7 @@ abstract class NBTType<T : Any> internal constructor(val typeClass: KClass<*>, v
) )
} }
val byID: Map<Byte, NBTType<*>> by lazy { all.map { it.id to it }.toMap() } val byID: Map<Byte, NBTType<*>> by lazy { all.associateBy { it.id } }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : Any> of(value: T): NBTType<T>? = all.find { it.typeClass.isInstance(value) } as NBTType<T>? fun <T : Any> of(value: T): NBTType<T>? = all.find { it.typeClass.isInstance(value) } as NBTType<T>?

View file

@ -14,18 +14,18 @@ object EntityMetadataPacketCodec : OutgoingPacketCodec<EntityMetadataPacket>(0x4
dst.writeVarInt(entry.typeID) dst.writeVarInt(entry.typeID)
when(entry) { when(entry) {
is EntityMetadataPacket.MetadataEntry.Byte -> dst.writeByte(entry.value.toInt()) is EntityMetadataPacket.Entry.Byte -> dst.writeByte(entry.value.toInt())
is EntityMetadataPacket.MetadataEntry.Int -> dst.writeVarInt(entry.value) is EntityMetadataPacket.Entry.Int -> dst.writeVarInt(entry.value)
is EntityMetadataPacket.MetadataEntry.Float -> dst.writeFloat(entry.value) is EntityMetadataPacket.Entry.Float -> dst.writeFloat(entry.value)
is EntityMetadataPacket.MetadataEntry.String -> dst.writeString(entry.value) is EntityMetadataPacket.Entry.String -> dst.writeString(entry.value)
is EntityMetadataPacket.MetadataEntry.OptChat -> { is EntityMetadataPacket.Entry.OptionalChatComponent -> {
if (entry.value == null) dst.writeBoolean(false) if (entry.value == null) dst.writeBoolean(false)
else { else {
dst.writeBoolean(true) dst.writeBoolean(true)
dst.writeString(entry.value!!.toJson()) dst.writeString(entry.value!!.toJson())
} }
} }
is EntityMetadataPacket.MetadataEntry.Boolean -> dst.writeBoolean(entry.value) is EntityMetadataPacket.Entry.Boolean -> dst.writeBoolean(entry.value)
} }
} }

View file

@ -20,7 +20,10 @@ object JoinGamePacketCodec : OutgoingPacketCodec<JoinGamePacket>(0x24, JoinGameP
dst.writeVarInt(1) dst.writeVarInt(1)
dst.writeString("uranos:world") dst.writeString("uranos:world")
val dimensionsByID = Uranos.dimensionRegistry.items.values.map { dimension -> val dimensionsByID = // Not known what this does
// These values do not actually change something client-sided
Uranos.dimensionRegistry.items.values.associate { dimension ->
dimension.id to buildNBT { dimension.id to buildNBT {
"natural" setAsByte !dimension.compassesSpinRandomly "natural" setAsByte !dimension.compassesSpinRandomly
"ambient_light" set dimension.ambientLight "ambient_light" set dimension.ambientLight
@ -41,7 +44,7 @@ object JoinGamePacketCodec : OutgoingPacketCodec<JoinGamePacket>(0x24, JoinGameP
"respawn_anchor_works" setAsByte false "respawn_anchor_works" setAsByte false
"piglin_safe" setAsByte false "piglin_safe" setAsByte false
} }
}.toMap() }
// TODO: Cache this // TODO: Cache this
val dimensions = buildNBT { val dimensions = buildNBT {

View file

@ -1,21 +1,37 @@
package space.uranos.net.packet.play package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf
import space.uranos.chat.ChatComponent import space.uranos.chat.ChatComponent
import space.uranos.entity.metadata.ThreeAxisRotation
import space.uranos.net.packet.OutgoingPacket import space.uranos.net.packet.OutgoingPacket
import java.util.UUID
data class EntityMetadataPacket( data class EntityMetadataPacket(
val entityID: Int, val entityID: Int,
val metadata: Iterable<MetadataEntry<*>> val metadata: Collection<Entry<*>>
) : OutgoingPacket() { ) : OutgoingPacket() {
sealed class MetadataEntry<T>(val typeID: kotlin.Int) { sealed class Entry<T>(val typeID: kotlin.Int) {
abstract val index: UByte abstract val index: UByte
abstract val value: T abstract val value: T
data class Byte(override val index: UByte, override val value: kotlin.Byte) : MetadataEntry<kotlin.Byte>(0) data class Byte(override val index: UByte, override val value: kotlin.Byte) : Entry<kotlin.Byte>(0)
data class Int(override val index: UByte, override val value: kotlin.Int) : MetadataEntry<kotlin.Int>(1) data class Int(override val index: UByte, override val value: kotlin.Int) : Entry<kotlin.Int>(1)
data class Float(override val index: UByte, override val value: kotlin.Float) : MetadataEntry<kotlin.Float>(2) data class Float(override val index: UByte, override val value: kotlin.Float) : Entry<kotlin.Float>(2)
data class String(override val index: UByte, override val value: kotlin.String) : MetadataEntry<kotlin.String>(3) data class String(override val index: UByte, override val value: kotlin.String) : Entry<kotlin.String>(3)
data class OptChat(override val index: UByte, override val value: ChatComponent?) : MetadataEntry<ChatComponent?>(5) data class Chat(override val index: UByte, override val value: ChatComponent) : Entry<ChatComponent>(4)
data class Boolean(override val index: UByte, override val value: kotlin.Boolean) : MetadataEntry<kotlin.Boolean>(7) data class OptionalChatComponent(override val index: UByte, override val value: ChatComponent?) : Entry<ChatComponent?>(5)
// data class ItemStack(override val index: UByte, override val value: ItemStack) : Entry<ItemStack>(6)
data class Boolean(override val index: UByte, override val value: kotlin.Boolean) : Entry<kotlin.Boolean>(7)
data class Rotation(override val index: UByte, override val value: ThreeAxisRotation) : Entry<ThreeAxisRotation>(8)
data class Position(override val index: UByte, override val value: space.uranos.entity.Position) : Entry<space.uranos.entity.Position>(9)
data class OptionalPosition(override val index: UByte, override val value: space.uranos.entity.Position?) : Entry<space.uranos.entity.Position?>(10)
data class Direction(override val index: UByte, override val value: space.uranos.Direction) : Entry<space.uranos.Direction>(11)
data class OptionalUUID(override val index: UByte, override val value: UUID?) : Entry<UUID?>(12)
data class OptionalBlockID(override val index: UByte, override val value: kotlin.Int) : Entry<kotlin.Int>(13)
data class NBT(override val index: UByte, override val value: ByteBuf) : Entry<ByteBuf>(14)
// data class Particle(override val index: UByte, override val value: ?) : Entry<?>(15)
// data class VillagerData(override val index: UByte, override val value: ?) : Entry<?>(16)
data class OptionalInt(override val index: UByte, override val value: kotlin.Int?) : Entry<kotlin.Int?>(17)
data class Pose(override val index: UByte, override val value: space.uranos.entity.metadata.Pose) : Entry<space.uranos.entity.metadata.Pose>(18)
} }
} }

View file

@ -60,7 +60,7 @@ data class PlayerInfoPacket(val action: Action<*>) : OutgoingPacket(), Mergeable
data class UpdateDisplayName(override val entries: Map<UUID, TextComponent?>) : Action<TextComponent?>() data class UpdateDisplayName(override val entries: Map<UUID, TextComponent?>) : Action<TextComponent?>()
data class RemovePlayer(override val entries: Map<UUID, Unit>) : Action<Unit>() { data class RemovePlayer(override val entries: Map<UUID, Unit>) : Action<Unit>() {
constructor(entries: List<UUID>) : this(entries.map { it to Unit }.toMap()) constructor(entries: List<UUID>) : this(entries.associateWith {})
} }
} }

View file

@ -3,14 +3,12 @@ package space.uranos
import space.uranos.entity.* import space.uranos.entity.*
import space.uranos.entity.impl.UranosBatEntity import space.uranos.entity.impl.UranosBatEntity
import space.uranos.entity.impl.UranosCowEntity import space.uranos.entity.impl.UranosCowEntity
import space.uranos.entity.impl.UranosCreeperEntity
import space.uranos.entity.impl.UranosRideableMinecartEntity import space.uranos.entity.impl.UranosRideableMinecartEntity
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : Entity> createEntityInstance(server: UranosServer, type: EntityType<T>): T = when (type) { fun <T : Entity> createEntityInstance(server: UranosServer, type: EntityType<T>): T = when (type) {
CowEntity -> UranosCowEntity(server) CowEntity -> UranosCowEntity(server)
BatEntity -> UranosBatEntity(server) BatEntity -> UranosBatEntity(server)
CreeperEntity -> UranosCreeperEntity(server)
RideableMinecartEntity -> UranosRideableMinecartEntity(server) RideableMinecartEntity -> UranosRideableMinecartEntity(server)
else -> throw IllegalArgumentException("Entities of this type cannot be created with this function") else -> throw IllegalArgumentException("Entities of this type cannot be created with this function")
} as T } as T

View file

@ -131,7 +131,7 @@ class UranosServer internal constructor() : Server() {
// Not in UranosSession.tick because this simplifies reusing the packet and the ping is not a critical information // Not in UranosSession.tick because this simplifies reusing the packet and the ping is not a critical information
scheduler.executeRepeating(msToTicks(config.pingUpdateInterval.toMillis()), 0) { scheduler.executeRepeating(msToTicks(config.pingUpdateInterval.toMillis()), 0) {
val packet = PlayerInfoPacket( val packet = PlayerInfoPacket(
PlayerInfoPacket.Action.UpdateLatency(players.map { it.uuid to it.session.ping }.toMap()) PlayerInfoPacket.Action.UpdateLatency(players.associate { it.uuid to it.session.ping })
) )
players.forEach { players.forEach {

View file

@ -1,11 +1,58 @@
package space.uranos.entity package space.uranos.entity
class EntityMetadataSynchronizer(val entity: UranosEntity) { import space.uranos.entity.metadata.EntityMetadataFieldsTable
init { import space.uranos.net.packet.play.EntityMetadataPacket
// println(entity::class.allSuperclasses) import space.uranos.player.Player
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
class EntityMetadataSynchronizer(val entity: Entity, private val table: EntityMetadataFieldsTable) {
private var oldValues = mutableMapOf<KProperty<*>, Any?>()
private fun createFullPacket() = EntityMetadataPacket(entity.numericID, table.fields.map { it.value.createMetadataPacketEntry(it.key, entity) })
// Does two things so that only one iteration is needed.
private fun createEntryIfChangedAndUpdateOldValue(index: UByte, field: EntityMetadataFieldsTable.Field): EntityMetadataPacket.Entry<*>? {
var changed = false
@Suppress("UNCHECKED_CAST")
(field.dependingProperties as Array<KProperty1<Entity, *>>).forEach {
val value = it.get(entity)
if (!changed) changed = value != oldValues[it]
oldValues[it] = value
} }
fun tick() { return if (changed) field.createMetadataPacketEntry(index, entity) else null
}
fun tick(newViewers: Collection<Player>) {
if (entity.viewers.isEmpty()) return
if (oldValues.isEmpty()) {
// First invocation with at least one viewer
createFullPacket().let { packet -> entity.viewers.forEach { it.session.send(packet) } }
for (field in table.fields.values) {
@Suppress("UNCHECKED_CAST")
(field.dependingProperties as Array<KProperty1<Entity, *>>).forEach {
oldValues[it] = it.get(entity)
}
}
} else {
if (newViewers.isNotEmpty()) createFullPacket().let { packet -> newViewers.forEach { it.session.send(packet) } }
val entries = table.fields.mapNotNull { createEntryIfChangedAndUpdateOldValue(it.key, it.value) }
if (entries.isNotEmpty()) {
val packet = EntityMetadataPacket(entity.numericID, entries)
for (viewer in entity.viewers) {
if (!newViewers.contains(viewer)) viewer.session.send(packet)
}
}
}
} }
} }

View file

@ -9,8 +9,9 @@ import space.uranos.entity.impl.UranosPlayerEntity
import space.uranos.net.packet.OutgoingPacket import space.uranos.net.packet.OutgoingPacket
import space.uranos.net.packet.play.* import space.uranos.net.packet.play.*
import space.uranos.player.Player import space.uranos.player.Player
import space.uranos.util.*
import space.uranos.util.collections.WatchableSet import space.uranos.util.collections.WatchableSet
import space.uranos.util.createSpawnPacket
import space.uranos.util.memoized
import space.uranos.util.numbers.mapToUByte import space.uranos.util.numbers.mapToUByte
import space.uranos.util.numbers.validatePitch import space.uranos.util.numbers.validatePitch
import space.uranos.util.numbers.validateYaw import space.uranos.util.numbers.validateYaw
@ -64,30 +65,23 @@ sealed class UranosEntity(server: UranosServer) : Entity {
} }
override var glowing: Boolean = false override var glowing: Boolean = false
override var ignoreGravity: Boolean = false
override var invisible: Boolean = false override var invisible: Boolean = false
override var silent: Boolean = false
override fun toString(): String = "Entity($uuid)" override fun toString(): String = "Entity($uuid)"
override fun belongsToChunk(key: Chunk.Key): Boolean = key == chunkKey override fun belongsToChunk(key: Chunk.Key): Boolean = key == chunkKey
abstract val chunkKey: Chunk.Key abstract val chunkKey: Chunk.Key
abstract val metadataSynchronizer: EntityMetadataSynchronizer?
protected val container = TickSynchronizationContainer()
private fun sendSpawnAndDestroyPackets() { private fun sendSpawnAndDestroyPackets() {
if (addedViewers.isNotEmpty()) createSpawnPacket().let { packet -> addedViewers.forEach { it.session.send(packet) } } if (addedViewers.isNotEmpty()) createSpawnPacket().let { packet -> addedViewers.forEach { it.session.send(packet) } }
if (removedViewers.isNotEmpty()) DestroyEntitiesPacket(arrayOf(numericID)).let { packet -> removedViewers.forEach { it.session.send(packet) } } if (removedViewers.isNotEmpty()) DestroyEntitiesPacket(arrayOf(numericID)).let { packet -> removedViewers.forEach { it.session.send(packet) } }
} }
@Suppress("LeakingThis")
private val metadataSynchronizer = EntityMetadataSynchronizer(this)
open suspend fun tick() { open suspend fun tick() {
container.tick()
metadataSynchronizer.tick()
sendSpawnAndDestroyPackets() sendSpawnAndDestroyPackets()
metadataSynchronizer?.tick(addedViewers)
addedViewers.clear() addedViewers.clear()
removedViewers.clear() removedViewers.clear()
} }
@ -107,14 +101,12 @@ sealed class UranosLivingEntity(server: UranosServer) : UranosEntity(server), Li
abstract class UranosNotHasMovableHeadLivingEntity(server: UranosServer) : UranosLivingEntity(server) { abstract class UranosNotHasMovableHeadLivingEntity(server: UranosServer) : UranosLivingEntity(server) {
override var velocity: Vector = Vector.ZERO override var velocity: Vector = Vector.ZERO
override var position: Position = Position.ZERO
override val chunkKey: Chunk.Key by memoized({ position }) { Chunk.Key.from(position) }
private var lastSentPosition: Position = Position.ZERO private var lastSentPosition: Position = Position.ZERO
private var lastSentYaw: Float = 0f private var lastSentYaw: Float = 0f
private var lastSentPitch: Float = 0f private var lastSentPitch: Float = 0f
@Suppress("DuplicatedCode")
final override suspend fun tick() { final override suspend fun tick() {
val viewersWithoutAdded = viewers.subtract(addedViewers) val viewersWithoutAdded = viewers.subtract(addedViewers)
if (viewersWithoutAdded.isNotEmpty()) { if (viewersWithoutAdded.isNotEmpty()) {
@ -212,13 +204,13 @@ abstract class UranosHasMovableHeadLivingEntity(server: UranosServer) : UranosLi
abstract class UranosObjectEntity(server: UranosServer) : UranosEntity(server), ObjectEntity { abstract class UranosObjectEntity(server: UranosServer) : UranosEntity(server), ObjectEntity {
override var velocity: Vector = Vector.ZERO override var velocity: Vector = Vector.ZERO
override var position: Position = Position.ZERO override var position: Position = Position.ZERO
override val chunkKey: Chunk.Key by memoized({ position }) { Chunk.Key.from(position) } override val chunkKey: Chunk.Key by memoized({ position }) { Chunk.Key.from(position) }
private var lastSentPosition: Position = Position.ZERO private var lastSentPosition: Position = Position.ZERO
private var lastSentYaw: Float = 0f private var lastSentYaw: Float = 0f
private var lastSentPitch: Float = 0f private var lastSentPitch: Float = 0f
@Suppress("DuplicatedCode")
final override suspend fun tick() { final override suspend fun tick() {
val viewersWithoutAdded = viewers.subtract(addedViewers) val viewersWithoutAdded = viewers.subtract(addedViewers)
if (viewersWithoutAdded.isNotEmpty()) { if (viewersWithoutAdded.isNotEmpty()) {
@ -246,6 +238,8 @@ class UranosPaintingEntity(
) : UranosEntity(server), PaintingEntity { ) : UranosEntity(server), PaintingEntity {
override val chunkKey: Chunk.Key get() = Chunk.Key.from(topLeftLocation) override val chunkKey: Chunk.Key get() = Chunk.Key.from(topLeftLocation)
override val metadataSynchronizer: EntityMetadataSynchronizer? = null
private var lastSentTopLeftLocation: VoxelLocation = topLeftLocation private var lastSentTopLeftLocation: VoxelLocation = topLeftLocation
override suspend fun tick() { override suspend fun tick() {
@ -268,7 +262,6 @@ class UranosPaintingEntity(
} }
lastSentTopLeftLocation = topLeftLocation lastSentTopLeftLocation = topLeftLocation
super.tick() super.tick()
} }
} }

View file

@ -15,7 +15,7 @@ abstract class UranosMinecartEntity(server: UranosServer) : UranosObjectEntity(s
validatePitch(value); field = value validatePitch(value); field = value
} }
override val shakingPower: Int = 0 override var shakingPower: Int = 0
override val shakingDirection: Int = 1 override var shakingDirection: Int = 1
override val shakingMultiplier: Float = 0f override var shakingMultiplier: Float = 0f
} }

View file

@ -2,6 +2,20 @@ package space.uranos.entity.impl
import space.uranos.UranosServer import space.uranos.UranosServer
import space.uranos.entity.BatEntity import space.uranos.entity.BatEntity
import space.uranos.entity.EntityMetadataSynchronizer
import space.uranos.entity.UranosHasMovableHeadLivingEntity import space.uranos.entity.UranosHasMovableHeadLivingEntity
import space.uranos.entity.metadata.EntityMetadataFieldsTable
class UranosBatEntity(server: UranosServer) : UranosHasMovableHeadLivingEntity(server), BatEntity class UranosBatEntity(server: UranosServer) : UranosHasMovableHeadLivingEntity(server), BatEntity {
override var hanging: Boolean = false
override val metadataSynchronizer: EntityMetadataSynchronizer = EntityMetadataSynchronizer(this, entityMetadataFieldsTable)
companion object {
val entityMetadataFieldsTable = EntityMetadataFieldsTable.DEFAULT.extend {
bits(15u) {
+ BatEntity::hanging
}
}
}
}

View file

@ -1,7 +1,20 @@
package space.uranos.entity.impl package space.uranos.entity.impl
import space.uranos.UranosServer import space.uranos.UranosServer
import space.uranos.entity.Ageable
import space.uranos.entity.CowEntity import space.uranos.entity.CowEntity
import space.uranos.entity.EntityMetadataSynchronizer
import space.uranos.entity.UranosHasMovableHeadLivingEntity import space.uranos.entity.UranosHasMovableHeadLivingEntity
import space.uranos.entity.metadata.EntityMetadataFieldsTable
class UranosCowEntity(server: UranosServer) : UranosHasMovableHeadLivingEntity(server), CowEntity class UranosCowEntity(server: UranosServer) : UranosHasMovableHeadLivingEntity(server), CowEntity {
override var isBaby = false
override val metadataSynchronizer: EntityMetadataSynchronizer = EntityMetadataSynchronizer(this, entityMetadataFieldsTable)
companion object {
val entityMetadataFieldsTable = EntityMetadataFieldsTable.DEFAULT.extend {
required(15u, Ageable::isBaby)
}
}
}

View file

@ -1,7 +0,0 @@
package space.uranos.entity.impl
import space.uranos.UranosServer
import space.uranos.entity.CreeperEntity
import space.uranos.entity.UranosHasMovableHeadLivingEntity
class UranosCreeperEntity(server: UranosServer) : UranosHasMovableHeadLivingEntity(server), CreeperEntity

View file

@ -1,8 +1,10 @@
package space.uranos.entity.impl package space.uranos.entity.impl
import space.uranos.UranosServer import space.uranos.UranosServer
import space.uranos.entity.EntityMetadataSynchronizer
import space.uranos.entity.PlayerEntity import space.uranos.entity.PlayerEntity
import space.uranos.entity.UranosHasMovableHeadLivingEntity import space.uranos.entity.UranosHasMovableHeadLivingEntity
import space.uranos.entity.metadata.EntityMetadataFieldsTable
import space.uranos.player.Player import space.uranos.player.Player
import java.util.UUID import java.util.UUID
@ -11,4 +13,12 @@ class UranosPlayerEntity(
override val player: Player override val player: Player
) : UranosHasMovableHeadLivingEntity(server), PlayerEntity { ) : UranosHasMovableHeadLivingEntity(server), PlayerEntity {
override val uuid: UUID = player.uuid override val uuid: UUID = player.uuid
override val metadataSynchronizer: EntityMetadataSynchronizer = EntityMetadataSynchronizer(this, entityMetadataFieldsTable)
companion object {
val entityMetadataFieldsTable = EntityMetadataFieldsTable.DEFAULT.extend {
// 16 and 17 are sent externally because they depend on player.settings
}
}
} }

View file

@ -1,11 +1,38 @@
package space.uranos.entity.impl package space.uranos.entity.impl
import space.uranos.UranosServer import space.uranos.UranosServer
import space.uranos.entity.EntityMetadataSynchronizer
import space.uranos.entity.MinecartEntity
import space.uranos.entity.RideableMinecartEntity import space.uranos.entity.RideableMinecartEntity
import space.uranos.entity.UranosMinecartEntity import space.uranos.entity.UranosMinecartEntity
import space.uranos.entity.metadata.EntityMetadataFieldsTable
import space.uranos.net.packet.play.EntityMetadataPacket
import space.uranos.world.block.Block import space.uranos.world.block.Block
import space.uranos.world.block.material
class UranosRideableMinecartEntity(server: UranosServer) : UranosMinecartEntity(server), RideableMinecartEntity { class UranosRideableMinecartEntity(server: UranosServer) : UranosMinecartEntity(server), RideableMinecartEntity {
override val customBlock: Block? = null override var customBlock: Block? = null
override val blockOffset: Int = 6 override var blockOffset: Int = 6
override val metadataSynchronizer: EntityMetadataSynchronizer = EntityMetadataSynchronizer(this, entityMetadataFieldsTable)
companion object {
val entityMetadataFieldsTable = EntityMetadataFieldsTable.DEFAULT.extend {
required(7u, MinecartEntity::shakingPower)
required(8u, MinecartEntity::shakingDirection)
required(9u, MinecartEntity::shakingMultiplier)
computed(10u, arrayOf(MinecartEntity::customBlock)) { index, entity ->
val block = (entity as UranosRideableMinecartEntity).customBlock
EntityMetadataPacket.Entry.Int(index, block?.material()?.codec?.getStateID(block) ?: 0)
}
required(11u, MinecartEntity::blockOffset)
computed(12u, arrayOf(MinecartEntity::customBlock)) { index, entity ->
val block = (entity as UranosRideableMinecartEntity).customBlock
EntityMetadataPacket.Entry.Boolean(index, block != null)
}
}
}
} }

View file

@ -0,0 +1,155 @@
package space.uranos.entity.metadata
import space.uranos.chat.ChatComponent
import space.uranos.entity.Entity
import space.uranos.net.packet.play.EntityMetadataPacket
import space.uranos.util.numbers.bitmask
import kotlin.reflect.KProperty1
class EntityMetadataFieldsTable private constructor(val fields: Map<UByte, Field>) {
sealed class Field(val dependingProperties: Array<KProperty1<out Entity, *>>) {
abstract fun createMetadataPacketEntry(index: UByte, entity: Entity): EntityMetadataPacket.Entry<*>
class StaticBoolean(val value: Boolean) : Field(emptyArray()) {
override fun createMetadataPacketEntry(index: UByte, entity: Entity) = EntityMetadataPacket.Entry.Boolean(index, value)
}
class StaticInt(val value: Int) : Field(emptyArray()) {
override fun createMetadataPacketEntry(index: UByte, entity: Entity) = EntityMetadataPacket.Entry.Int(index, value)
}
abstract class Simple<T>(property: KProperty1<out Entity, T>) : Field(arrayOf(property)) {
@Suppress("UNCHECKED_CAST")
val property: KProperty1<Entity, T>
get() = dependingProperties[0] as KProperty1<Entity, T>
}
abstract class Required<T : Any>(property: KProperty1<out Entity, T>) : Simple<T>(property)
abstract class Optional<T : Any>(property: KProperty1<out Entity, T?>) : Simple<T?>(property)
class RequiredInt(property: KProperty1<out Entity, Int>) : Required<Int>(property) {
override fun createMetadataPacketEntry(index: UByte, entity: Entity) = EntityMetadataPacket.Entry.Int(index, property.get(entity))
}
class RequiredBoolean(property: KProperty1<out Entity, Boolean>) : Required<Boolean>(property) {
override fun createMetadataPacketEntry(index: UByte, entity: Entity) = EntityMetadataPacket.Entry.Boolean(index, property.get(entity))
}
class RequiredFloat(property: KProperty1<out Entity, Float>) : Required<Float>(property) {
override fun createMetadataPacketEntry(index: UByte, entity: Entity) = EntityMetadataPacket.Entry.Float(index, property.get(entity))
}
class OptionalChatComponent(property: KProperty1<out Entity, ChatComponent?>) : Optional<ChatComponent>(property) {
override fun createMetadataPacketEntry(index: UByte, entity: Entity) = EntityMetadataPacket.Entry.OptionalChatComponent(index, property.get(entity))
}
class Bits(properties: Array<KProperty1<out Entity, Boolean>>, private val static: Array<Boolean?>) : Field(arrayOf(*properties)) {
override fun createMetadataPacketEntry(index: UByte, entity: Entity): EntityMetadataPacket.Entry.Byte {
var nonStaticIndex = 0
return EntityMetadataPacket.Entry.Byte(index, bitmask(BooleanArray(static.size) { i ->
val staticValue = static[i]
if (staticValue == null) {
nonStaticIndex++
@Suppress("UNCHECKED_CAST")
(dependingProperties as Array<KProperty1<Entity, *>>)[nonStaticIndex - 1].get(entity) as Boolean
} else staticValue
}).toByte())
}
}
class Computed(dependingProperties: Array<KProperty1<out Entity, *>>, val getEntry: (index: UByte, entity: Entity) -> EntityMetadataPacket.Entry<*>) :
Field(dependingProperties) {
override fun createMetadataPacketEntry(index: UByte, entity: Entity) = getEntry(index, entity)
}
}
fun extend(init: BuilderContext.() -> Unit) =
EntityMetadataFieldsTable(fields.plus(buildMap { BuilderContext(this).init() }))
@BuilderMarker
@Suppress("INAPPLICABLE_JVM_NAME")
class BuilderContext(private val fields: MutableMap<UByte, Field>) {
@JvmName("requiredInt")
fun required(index: UByte, property: KProperty1<out Entity, Int>) {
fields[index] = Field.RequiredInt(property)
}
@JvmName("requiredBoolean")
fun required(index: UByte, property: KProperty1<out Entity, Boolean>) {
fields[index] = Field.RequiredBoolean(property)
}
@JvmName("requiredFloat")
fun required(index: UByte, property: KProperty1<out Entity, Float>) {
fields[index] = Field.RequiredFloat(property)
}
@JvmName("optionalChatComponent")
fun optional(index: UByte, property: KProperty1<out Entity, ChatComponent?>) {
fields[index] = Field.OptionalChatComponent(property)
}
fun static(index: UByte, value: Boolean) {
fields[index] = Field.StaticBoolean(value)
}
fun static(index: UByte, value: Int) {
fields[index] = Field.StaticInt(value)
}
fun bits(index: UByte, init: BitsContext.() -> Unit) {
val properties = mutableListOf<KProperty1<out Entity, Boolean>>()
val static = mutableListOf<Boolean?>()
BitsContext(properties, static).init()
fields[index] = Field.Bits(properties.toTypedArray(), static.toTypedArray())
}
@BuilderMarker
class BitsContext(private val properties: MutableList<KProperty1<out Entity, Boolean>>, private val static: MutableList<Boolean?>) {
operator fun KProperty1<out Entity, Boolean>.unaryPlus() {
properties.add(this)
static.add(null)
}
operator fun Boolean.unaryPlus() {
static.add(this)
}
}
fun computed(
index: UByte,
dependingProperties: Array<KProperty1<out Entity, *>>,
getEntry: (index: UByte, entity: Entity) -> EntityMetadataPacket.Entry<*>
) {
fields[index] = Field.Computed(dependingProperties, getEntry)
}
}
@DslMarker
private annotation class BuilderMarker
companion object {
val DEFAULT = EntityMetadataFieldsTable(emptyMap()).extend {
bits(0u) {
+false // on fire
+false // crouching
+false // unused
+false // sprinting
+false // swimming
+Entity::invisible
+Entity::glowing
+false // flying with an elytra
}
static(1u, 0) // ticks until drowning
static(2u, false) // custom name
static(3u, false) // custom name visible
static(4u, false) // silent
static(5u, false) // ignores gravity
static(6u, 0) // pose
}
}
}

View file

@ -180,7 +180,8 @@ class LoginAndJoinProcedure(val session: UranosSession) {
session.sendNow(OutgoingPlayerPositionPacket(state.position.x, state.position.y, state.position.z, state.headYaw, state.headPitch)) session.sendNow(OutgoingPlayerPositionPacket(state.position.x, state.position.y, state.position.z, state.headYaw, state.headPitch))
session.sendNow(PlayerInfoPacket(PlayerInfoPacket.Action.AddPlayer((session.server.players + player).map { session.sendNow(PlayerInfoPacket(PlayerInfoPacket.Action.AddPlayer(
(session.server.players + player).associate {
it.uuid to PlayerInfoPacket.Action.AddPlayer.Data( it.uuid to PlayerInfoPacket.Action.AddPlayer.Data(
it.name, it.name,
it.gameMode, it.gameMode,
@ -188,12 +189,13 @@ class LoginAndJoinProcedure(val session: UranosSession) {
it.playerListName, it.playerListName,
emptyMap() // TODO: Load skin emptyMap() // TODO: Load skin
) )
}.toMap()))) }
)))
session.sendNow( session.sendNow(
PlayerInfoPacket( PlayerInfoPacket(
PlayerInfoPacket.Action.UpdateLatency( PlayerInfoPacket.Action.UpdateLatency(
session.server.players.map { it.uuid to it.session.ping }.toMap() session.server.players.associate { it.uuid to it.session.ping }
) )
) )
) )

View file

@ -51,6 +51,7 @@ class PacketsAdapter(val session: UranosSession) {
} }
fun sendNextTick(packet: OutgoingPacket) { fun sendNextTick(packet: OutgoingPacket) {
// TODO: Allow disabling merging in the config
if (packet is Mergeable) { if (packet is Mergeable) {
for (i in packetsForNextTick.indices.reversed()) { for (i in packetsForNextTick.indices.reversed()) {
val merged = packet.mergeWith(packetsForNextTick[i]) val merged = packet.mergeWith(packetsForNextTick[i])

View file

@ -1,13 +1,13 @@
package space.uranos.net package space.uranos.net
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import kotlinx.coroutines.* import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import space.uranos.Uranos import space.uranos.Uranos
import space.uranos.UranosServer import space.uranos.UranosServer
import space.uranos.chat.ChatColor import space.uranos.chat.ChatColor
import space.uranos.chat.ChatComponent import space.uranos.chat.ChatComponent
import space.uranos.chat.TextComponent import space.uranos.chat.TextComponent
import space.uranos.event.*
import space.uranos.logging.Logger import space.uranos.logging.Logger
import space.uranos.net.packet.OutgoingPacket import space.uranos.net.packet.OutgoingPacket
import space.uranos.net.packet.Protocol import space.uranos.net.packet.Protocol