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.
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.
The most important thing for Uranos is
@ -10,7 +10,6 @@ The most important thing for Uranos is
## Milestones
- Players can see entities
- Players can see other players
- Players can send and receive chat messages
- Players can see Titles
@ -24,6 +23,7 @@ The most important thing for Uranos is
- Command framework + permissions
- Commands can be sent from the console
- Players can be teleported between worlds
- All entities are implemented with all metadata
- Entity AI framework
- Scoreboards + Teams
- Crafting

View file

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

View file

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

View file

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

View file

@ -3,25 +3,22 @@ package space.uranos.testplugin
import space.uranos.Uranos
import space.uranos.chat.ChatColor
import space.uranos.chat.TextComponent
import space.uranos.entity.CowEntity
import space.uranos.entity.Position
import space.uranos.entity.RideableMinecartEntity
import space.uranos.net.ServerListInfo
import space.uranos.net.event.ServerListInfoRequestEvent
import space.uranos.net.event.SessionAfterLoginEvent
import space.uranos.net.packet.play.EntityMetadataPacket
import space.uranos.player.GameMode
import space.uranos.player.event.PlayerReadyEvent
import space.uranos.plugin.Plugin
import space.uranos.testplugin.anvil.AnvilWorld
import space.uranos.util.RGBColor
import space.uranos.util.runInServerThread
import space.uranos.util.secondsToTicks
import space.uranos.world.*
import space.uranos.world.block.GreenWoolBlock
import space.uranos.world.block.RedWoolBlock
class TestPlugin : Plugin("Test", "1.0.0") {
fun enableOptifineFix() {
private fun enableOptifineFix() {
Uranos.biomeRegistry.register(
Biome(
"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.setWorld(world)
Uranos.eventBus.on<PlayerReadyEvent> {
it.player.session.send(EntityMetadataPacket(entity.numericID, listOf(
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
Uranos.scheduler.executeRepeating(20) {
entity.isBaby = !entity.isBaby
}
}
}

View file

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

View file

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

View file

@ -47,7 +47,7 @@ sealed class ChatColor(val stringRepresentation: String) {
PINK,
YELLOW,
WHITE
).map { it.stringRepresentation to it }.toMap()
).associateBy { it.stringRepresentation }
}
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
import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader
import space.uranos.event.EventBusWrapper
import space.uranos.player.Player
import space.uranos.util.collections.createWeakKeysLoadingCache
import space.uranos.world.Chunk
import space.uranos.world.World
import java.util.UUID
@ -39,16 +38,9 @@ interface Entity {
*/
var visibleToNewPlayers: Boolean
var glowing: Boolean // even experience orbs can glow
var glowing: Boolean // It seems like every entity can glow
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")
private val eventBusWrapperCache = CacheBuilder.newBuilder()
.weakKeys()
.build(object : CacheLoader<Entity, EventBusWrapper<Entity>>() {
override fun load(key: Entity): EventBusWrapper<Entity> = EventBusWrapper(key)
})
private val eventBusWrapperCache = createWeakKeysLoadingCache<Entity, EventBusWrapper<Entity>> { EventBusWrapper(it) }
@Suppress("UNCHECKED_CAST")
val <T : Entity> T.events

View file

@ -15,11 +15,11 @@ interface EntityType<T : Entity> {
*/
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]
private val byInterfaceTypeMap: Map<KClass<out Entity>, EntityType<*>> =
ENTITY_TYPES.map { it.interfaceType to it }.toMap()
ENTITY_TYPES.associateBy { it.interfaceType }
@Suppress("UNCHECKED_CAST")
fun <T : Entity> byInterfaceType(interfaceType: KClass<T>): EntityType<T> =

View file

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

View file

@ -6,22 +6,22 @@ interface MinecartEntity: ObjectEntity, YawRotatable, PitchRotatable {
/**
* Default is `0`.
*/
val shakingPower: Int
var shakingPower: Int
/**
* Default is `1`.
*/
val shakingDirection: Int
var shakingDirection: Int
/**
* 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.
*/
val blockOffset: Int
var blockOffset: Int
}

View file

@ -1,5 +1,3 @@
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 motive: PaintingMotive
companion object Type : AreaEffectCloudEntityType()
companion object Type : PaintingEntityType()
}
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<*>) {
val codecs = codecs.toSet()
private val codecsByPacketType = codecs.map { it.dataType to it }.toMap()
val incomingPacketCodecsByID = codecs.filterIsInstance<IncomingPacketCodec<*>>().map { it.id to it }.toMap()
private val codecsByPacketType = codecs.associateBy { it.dataType }
val incomingPacketCodecsByID = codecs.filterIsInstance<IncomingPacketCodec<*>>().associateBy { it.id }
@Suppress("UNCHECKED_CAST")
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 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,
onTick: suspend (value: T) -> Unit
): 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(
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) }
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 key: Key
) {
private val identifier = "Chunk(${key.x}-${key.z})"
data class Key(val x: Int, val z: Int) {
companion object {
/**

View file

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

View file

@ -17,9 +17,9 @@ interface Material<T : Block> {
* All materials, sorted by their numeric ID in ascending order.
*/
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")
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")
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)
when(entry) {
is EntityMetadataPacket.MetadataEntry.Byte -> dst.writeByte(entry.value.toInt())
is EntityMetadataPacket.MetadataEntry.Int -> dst.writeVarInt(entry.value)
is EntityMetadataPacket.MetadataEntry.Float -> dst.writeFloat(entry.value)
is EntityMetadataPacket.MetadataEntry.String -> dst.writeString(entry.value)
is EntityMetadataPacket.MetadataEntry.OptChat -> {
is EntityMetadataPacket.Entry.Byte -> dst.writeByte(entry.value.toInt())
is EntityMetadataPacket.Entry.Int -> dst.writeVarInt(entry.value)
is EntityMetadataPacket.Entry.Float -> dst.writeFloat(entry.value)
is EntityMetadataPacket.Entry.String -> dst.writeString(entry.value)
is EntityMetadataPacket.Entry.OptionalChatComponent -> {
if (entry.value == null) dst.writeBoolean(false)
else {
dst.writeBoolean(true)
dst.writeString(entry.value!!.toJson())
}
}
is EntityMetadataPacket.MetadataEntry.Boolean -> dst.writeBoolean(entry.value)
is EntityMetadataPacket.Entry.Boolean -> dst.writeBoolean(entry.value)
}
}

View file

@ -20,28 +20,31 @@ object JoinGamePacketCodec : OutgoingPacketCodec<JoinGamePacket>(0x24, JoinGameP
dst.writeVarInt(1)
dst.writeString("uranos:world")
val dimensionsByID = Uranos.dimensionRegistry.items.values.map { dimension ->
dimension.id to buildNBT {
"natural" setAsByte !dimension.compassesSpinRandomly
"ambient_light" set dimension.ambientLight
"has_skylight" setAsByte dimension.hasSkylight
val dimensionsByID = // Not known what this does
// Not known what this does
"effects" set "minecraft:overworld"
// These values do not actually change something client-sided
Uranos.dimensionRegistry.items.values.associate { dimension ->
dimension.id to buildNBT {
"natural" setAsByte !dimension.compassesSpinRandomly
"ambient_light" set dimension.ambientLight
"has_skylight" setAsByte dimension.hasSkylight
// These values do not actually change something client-sided
"ultrawarm" setAsByte false
"has_ceiling" setAsByte false
"has_raids" setAsByte true
"logical_height" set 255
"coordinate_scale" set 1.toFloat()
"bed_works" setAsByte true
"fixed_light" setAsByte false
"infiniburn" set ""
"respawn_anchor_works" setAsByte false
"piglin_safe" setAsByte false
// Not known what this does
"effects" set "minecraft:overworld"
// These values do not actually change something client-sided
"ultrawarm" setAsByte false
"has_ceiling" setAsByte false
"has_raids" setAsByte true
"logical_height" set 255
"coordinate_scale" set 1.toFloat()
"bed_works" setAsByte true
"fixed_light" setAsByte false
"infiniburn" set ""
"respawn_anchor_works" setAsByte false
"piglin_safe" setAsByte false
}
}
}.toMap()
// TODO: Cache this
val dimensions = buildNBT {

View file

@ -1,21 +1,37 @@
package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf
import space.uranos.chat.ChatComponent
import space.uranos.entity.metadata.ThreeAxisRotation
import space.uranos.net.packet.OutgoingPacket
import java.util.UUID
data class EntityMetadataPacket(
val entityID: Int,
val metadata: Iterable<MetadataEntry<*>>
val metadata: Collection<Entry<*>>
) : OutgoingPacket() {
sealed class MetadataEntry<T>(val typeID: kotlin.Int) {
sealed class Entry<T>(val typeID: kotlin.Int) {
abstract val index: UByte
abstract val value: T
data class Byte(override val index: UByte, override val value: kotlin.Byte) : MetadataEntry<kotlin.Byte>(0)
data class Int(override val index: UByte, override val value: kotlin.Int) : MetadataEntry<kotlin.Int>(1)
data class Float(override val index: UByte, override val value: kotlin.Float) : MetadataEntry<kotlin.Float>(2)
data class String(override val index: UByte, override val value: kotlin.String) : MetadataEntry<kotlin.String>(3)
data class OptChat(override val index: UByte, override val value: ChatComponent?) : MetadataEntry<ChatComponent?>(5)
data class Boolean(override val index: UByte, override val value: kotlin.Boolean) : MetadataEntry<kotlin.Boolean>(7)
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) : Entry<kotlin.Int>(1)
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) : Entry<kotlin.String>(3)
data class Chat(override val index: UByte, override val value: ChatComponent) : Entry<ChatComponent>(4)
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 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.impl.UranosBatEntity
import space.uranos.entity.impl.UranosCowEntity
import space.uranos.entity.impl.UranosCreeperEntity
import space.uranos.entity.impl.UranosRideableMinecartEntity
@Suppress("UNCHECKED_CAST")
fun <T : Entity> createEntityInstance(server: UranosServer, type: EntityType<T>): T = when (type) {
CowEntity -> UranosCowEntity(server)
BatEntity -> UranosBatEntity(server)
CreeperEntity -> UranosCreeperEntity(server)
RideableMinecartEntity -> UranosRideableMinecartEntity(server)
else -> throw IllegalArgumentException("Entities of this type cannot be created with this function")
} 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
scheduler.executeRepeating(msToTicks(config.pingUpdateInterval.toMillis()), 0) {
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 {

View file

@ -1,11 +1,58 @@
package space.uranos.entity
class EntityMetadataSynchronizer(val entity: UranosEntity) {
init {
// println(entity::class.allSuperclasses)
import space.uranos.entity.metadata.EntityMetadataFieldsTable
import space.uranos.net.packet.play.EntityMetadataPacket
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
}
return if (changed) field.createMetadataPacketEntry(index, entity) else null
}
fun tick() {
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.play.*
import space.uranos.player.Player
import space.uranos.util.*
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.validatePitch
import space.uranos.util.numbers.validateYaw
@ -64,30 +65,23 @@ sealed class UranosEntity(server: UranosServer) : Entity {
}
override var glowing: Boolean = false
override var ignoreGravity: Boolean = false
override var invisible: Boolean = false
override var silent: Boolean = false
override fun toString(): String = "Entity($uuid)"
override fun belongsToChunk(key: Chunk.Key): Boolean = key == chunkKey
abstract val chunkKey: Chunk.Key
protected val container = TickSynchronizationContainer()
abstract val metadataSynchronizer: EntityMetadataSynchronizer?
private fun sendSpawnAndDestroyPackets() {
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) } }
}
@Suppress("LeakingThis")
private val metadataSynchronizer = EntityMetadataSynchronizer(this)
open suspend fun tick() {
container.tick()
metadataSynchronizer.tick()
sendSpawnAndDestroyPackets()
metadataSynchronizer?.tick(addedViewers)
addedViewers.clear()
removedViewers.clear()
}
@ -107,14 +101,12 @@ sealed class UranosLivingEntity(server: UranosServer) : UranosEntity(server), Li
abstract class UranosNotHasMovableHeadLivingEntity(server: UranosServer) : UranosLivingEntity(server) {
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 lastSentYaw: Float = 0f
private var lastSentPitch: Float = 0f
@Suppress("DuplicatedCode")
final override suspend fun tick() {
val viewersWithoutAdded = viewers.subtract(addedViewers)
if (viewersWithoutAdded.isNotEmpty()) {
@ -212,13 +204,13 @@ abstract class UranosHasMovableHeadLivingEntity(server: UranosServer) : UranosLi
abstract class UranosObjectEntity(server: UranosServer) : UranosEntity(server), ObjectEntity {
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 lastSentYaw: Float = 0f
private var lastSentPitch: Float = 0f
@Suppress("DuplicatedCode")
final override suspend fun tick() {
val viewersWithoutAdded = viewers.subtract(addedViewers)
if (viewersWithoutAdded.isNotEmpty()) {
@ -246,6 +238,8 @@ class UranosPaintingEntity(
) : UranosEntity(server), PaintingEntity {
override val chunkKey: Chunk.Key get() = Chunk.Key.from(topLeftLocation)
override val metadataSynchronizer: EntityMetadataSynchronizer? = null
private var lastSentTopLeftLocation: VoxelLocation = topLeftLocation
override suspend fun tick() {
@ -268,7 +262,6 @@ class UranosPaintingEntity(
}
lastSentTopLeftLocation = topLeftLocation
super.tick()
}
}

View file

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

View file

@ -2,6 +2,20 @@ package space.uranos.entity.impl
import space.uranos.UranosServer
import space.uranos.entity.BatEntity
import space.uranos.entity.EntityMetadataSynchronizer
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
import space.uranos.UranosServer
import space.uranos.entity.Ageable
import space.uranos.entity.CowEntity
import space.uranos.entity.EntityMetadataSynchronizer
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
import space.uranos.UranosServer
import space.uranos.entity.EntityMetadataSynchronizer
import space.uranos.entity.PlayerEntity
import space.uranos.entity.UranosHasMovableHeadLivingEntity
import space.uranos.entity.metadata.EntityMetadataFieldsTable
import space.uranos.player.Player
import java.util.UUID
@ -11,4 +13,12 @@ class UranosPlayerEntity(
override val player: Player
) : UranosHasMovableHeadLivingEntity(server), PlayerEntity {
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
import space.uranos.UranosServer
import space.uranos.entity.EntityMetadataSynchronizer
import space.uranos.entity.MinecartEntity
import space.uranos.entity.RideableMinecartEntity
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.material
class UranosRideableMinecartEntity(server: UranosServer) : UranosMinecartEntity(server), RideableMinecartEntity {
override val customBlock: Block? = null
override val blockOffset: Int = 6
override var customBlock: Block? = null
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,20 +180,22 @@ class LoginAndJoinProcedure(val session: UranosSession) {
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 {
it.uuid to PlayerInfoPacket.Action.AddPlayer.Data(
it.name,
it.gameMode,
it.session.ping,
it.playerListName,
emptyMap() // TODO: Load skin
)
}.toMap())))
session.sendNow(PlayerInfoPacket(PlayerInfoPacket.Action.AddPlayer(
(session.server.players + player).associate {
it.uuid to PlayerInfoPacket.Action.AddPlayer.Data(
it.name,
it.gameMode,
it.session.ping,
it.playerListName,
emptyMap() // TODO: Load skin
)
}
)))
session.sendNow(
PlayerInfoPacket(
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) {
// TODO: Allow disabling merging in the config
if (packet is Mergeable) {
for (i in packetsForNextTick.indices.reversed()) {
val merged = packet.mergeWith(packetsForNextTick[i])

View file

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