Archived
1
0
Fork 0

Add entity metadata packet and object entity spawning

This commit is contained in:
Moritz Ruth 2021-03-07 15:09:04 +01:00
parent c959478978
commit 0b1ba947a1
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
26 changed files with 288 additions and 93 deletions

5
.idea/misc.xml generated
View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<list size="1">
<item index="0" class="java.lang.String" itemvalue="com.squareup.moshi.FromJson" />
</list>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_14" default="true" project-jdk-name="14" project-jdk-type="JavaSDK" />
<component name="SuppressKotlinCodeStyleNotification">

View file

@ -3,12 +3,14 @@ package space.uranos.testplugin
import space.uranos.Uranos
import space.uranos.chat.ChatColor
import space.uranos.chat.TextComponent
import space.uranos.entity.MinecartEntity
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
@ -104,10 +106,22 @@ class TestPlugin : Plugin("Test", "1.0.0") {
}
}
// Not showing up yet because no metadata is sent
val entity = Uranos.create<MinecartEntity>()
val entity = Uranos.create<RideableMinecartEntity>()
entity.position = Position(0.0, 4.0, 0.0)
// entity.setWorld(world)
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

View file

@ -25,7 +25,7 @@ interface Entity {
val numericID: Int
/**
* Players that can see this entity.
* Players that can see this entity if it is in their view distance.
*/
val viewers: MutableSet<Player>
@ -38,6 +38,17 @@ interface Entity {
* If players should be added to [viewers] when they enter [world].
*/
var visibleToNewPlayers: Boolean
var glowing: Boolean // even experience orbs can glow
var invisible: Boolean
/**
* Whether this entity does not produce any sounds.
*/
var silent: Boolean
var ignoreGravity: Boolean
}
/**

View file

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

View file

@ -0,0 +1,5 @@
package space.uranos.entity
interface RideableMinecartEntity : MinecartEntity {
companion object Type : MinecartEntityType()
}

View file

@ -0,0 +1,34 @@
package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf
import space.uranos.net.MinecraftProtocolDataTypes.writeString
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec
object EntityMetadataPacketCodec : OutgoingPacketCodec<EntityMetadataPacket>(0x44, EntityMetadataPacket::class) {
override fun EntityMetadataPacket.encode(dst: ByteBuf) {
dst.writeVarInt(entityID)
for (entry in metadata) {
dst.writeByte(entry.index.toInt())
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 -> {
if (entry.value == null) dst.writeBoolean(false)
else {
dst.writeBoolean(true)
dst.writeString(entry.value!!.toJson())
}
}
is EntityMetadataPacket.MetadataEntry.Boolean -> dst.writeBoolean(entry.value)
}
}
dst.writeByte(0xff)
}
}

View file

@ -0,0 +1,14 @@
package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec
object EntityVelocityPacketCodec : OutgoingPacketCodec<EntityVelocityPacket>(0x46, EntityVelocityPacket::class) {
override fun EntityVelocityPacket.encode(dst: ByteBuf) {
dst.writeVarInt(entityID)
dst.writeShort(x.toInt())
dst.writeShort(y.toInt())
dst.writeShort(z.toInt())
}
}

View file

@ -14,10 +14,12 @@ object PlayProtocol : Protocol(
DestroyEntitiesPacketCodec,
DisconnectPacketCodec,
EntityHeadYawPacketCodec,
EntityMetadataPacketCodec,
EntityOrientationPacketCodec,
EntityRelativeMovePacketCodec,
EntityRelativeMoveWithOrientationPacketCodec,
EntityTeleportPacketCodec,
EntityVelocityPacketCodec,
IncomingKeepAlivePacketCodec,
IncomingPlayerPositionPacketCodec,
IncomingPluginMessagePacketCodec,

View file

@ -7,7 +7,7 @@ import space.uranos.util.numbers.floorMod
object PlayerOrientationPacketCodec :
IncomingPacketCodec<PlayerOrientationPacket>(0x14, PlayerOrientationPacket::class) {
override fun decode(msg: ByteBuf): PlayerOrientationPacket = PlayerOrientationPacket(
floorMod(msg.readFloat(), 360f),
floorMod(msg.readFloat(), 360f), // TODO: Ensure it is never 360 (should be 0 then)
msg.readFloat(),
msg.readBoolean()
)

View file

@ -20,8 +20,8 @@ object SpawnLivingEntityPacketCodec : OutgoingPacketCodec<SpawnLivingEntityPacke
// This is named "head pitch" on wiki.vg, but it is actually head yaw.
dst.writeByte(headYaw.mapToUByte(360f).toInt())
dst.writeShort((velocity.x * 8000).toInt().toShort().toInt())
dst.writeShort((velocity.y * 8000).toInt().toShort().toInt())
dst.writeShort((velocity.z * 8000).toInt().toShort().toInt())
dst.writeShort(velocityX.toInt())
dst.writeShort(velocityY.toInt())
dst.writeShort(velocityZ.toInt())
}
}

View file

@ -16,8 +16,8 @@ object SpawnObjectEntityPacketCodec : OutgoingPacketCodec<SpawnObjectEntityPacke
dst.writeByte(pitch.toInt())
dst.writeByte(yaw.toInt())
dst.writeInt(data)
dst.writeShort((velocity.x * 8000).toInt().toShort().toInt())
dst.writeShort((velocity.y * 8000).toInt().toShort().toInt())
dst.writeShort((velocity.z * 8000).toInt().toShort().toInt())
dst.writeShort(velocityX.toInt())
dst.writeShort(velocityY.toInt())
dst.writeShort(velocityZ.toInt())
}
}

View file

@ -0,0 +1,21 @@
package space.uranos.net.packet.play
import space.uranos.chat.ChatComponent
import space.uranos.net.packet.OutgoingPacket
data class EntityMetadataPacket(
val entityID: Int,
val metadata: Iterable<MetadataEntry<*>>
) : OutgoingPacket() {
sealed class MetadataEntry<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)
}
}

View file

@ -0,0 +1,10 @@
package space.uranos.net.packet.play
import space.uranos.net.packet.OutgoingPacket
data class EntityVelocityPacket(
val entityID: Int,
val x: Short,
val y: Short,
val z: Short
) : OutgoingPacket()

View file

@ -1,6 +1,5 @@
package space.uranos.net.packet.play
import space.uranos.Vector
import space.uranos.entity.EntityType
import space.uranos.entity.Position
import space.uranos.net.packet.OutgoingPacket
@ -8,6 +7,8 @@ import java.util.UUID
/**
* Sent to spawn **living** entities.
*
* Velocity is measured in 1/8000 blocks per tick.
*/
data class SpawnLivingEntityPacket(
val entityID: Int,
@ -17,8 +18,7 @@ data class SpawnLivingEntityPacket(
val yaw: Float,
val pitch: Float,
val headYaw: Float,
/**
* Velocity in blocks per tick
*/
val velocity: Vector
val velocityX: Short,
val velocityY: Short,
val velocityZ: Short
) : OutgoingPacket()

View file

@ -1,11 +1,12 @@
package space.uranos.net.packet.play
import space.uranos.Vector
import space.uranos.net.packet.OutgoingPacket
import java.util.UUID
/**
* Sent to spawn object entities.
*
* Velocity is measured in 1/8000 blocks per tick.
*/
data class SpawnObjectEntityPacket(
val entityID: Int,
@ -17,5 +18,7 @@ data class SpawnObjectEntityPacket(
val yaw: UByte,
val pitch: UByte,
val data: Int,
val velocity: Vector
val velocityX: Short,
val velocityY: Short,
val velocityZ: Short
) : OutgoingPacket()

View file

@ -8,37 +8,45 @@ import space.uranos.net.packet.play.SpawnPaintingPacket
import space.uranos.util.numbers.mapToUByte
fun Entity.createSpawnPacket(): OutgoingPacket = when (this) {
is LivingEntity -> if (this is HasMovableHead) SpawnLivingEntityPacket(
numericID,
uuid,
type,
position,
headYaw,
headPitch,
headYaw,
velocity
) else SpawnLivingEntityPacket(
numericID,
uuid,
type,
position,
if (this is YawRotatable) yaw else 0f,
if (this is PitchRotatable) pitch else 0f,
0f,
velocity
)
is ObjectEntity -> SpawnObjectEntityPacket(
numericID,
uuid,
type.numericID,
position.x,
position.y,
position.z,
if (this is YawRotatable) yaw.mapToUByte(360f) else 0u,
if (this is PitchRotatable) pitch.mapToUByte(360f) else 0u,
getDataValue(),
velocity
)
is LivingEntity -> {
val (velX, velY, velZ) = velocity.getAsVelocityPacketValues()
if (this is HasMovableHead) SpawnLivingEntityPacket(
numericID,
uuid,
type,
position,
headYaw,
headPitch,
headYaw,
velX, velY, velZ
) else SpawnLivingEntityPacket(
numericID,
uuid,
type,
position,
if (this is YawRotatable) yaw else 0f,
if (this is PitchRotatable) pitch else 0f,
0f,
velX, velY, velZ
)
}
is ObjectEntity -> {
val (velX, velY, velZ) = velocity.getAsVelocityPacketValues()
SpawnObjectEntityPacket(
numericID,
uuid,
type.numericID,
position.x,
position.y,
position.z,
if (this is YawRotatable) yaw.mapToUByte(360f) else 0u,
if (this is PitchRotatable) pitch.mapToUByte(360f) else 0u,
getDataValue(),
velX, velY, velZ
)
}
is PaintingEntity -> SpawnPaintingPacket(
numericID,
uuid,
@ -51,7 +59,7 @@ fun Entity.createSpawnPacket(): OutgoingPacket = when (this) {
fun ObjectEntity.getDataValue(): Int = when (this) {
is ItemEntity -> 1
is MinecartEntity -> 2
is RideableMinecartEntity -> 0
// TODO: Add remaining
else -> throw IllegalArgumentException("Unknown entity type")
}

View file

@ -0,0 +1,9 @@
package space.uranos.util
import space.uranos.Vector
fun Vector.getAsVelocityPacketValues() = Triple(
(x * 8000).toInt().toShort(),
(y * 8000).toInt().toShort(),
(z * 8000).toInt().toShort()
)

View file

@ -0,0 +1,16 @@
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

@ -5,8 +5,10 @@ import com.sksamuel.hoplite.ConfigLoader
import com.sksamuel.hoplite.ConfigSource
import kotlinx.coroutines.runBlocking
import space.uranos.config.UranosConfig
import space.uranos.entity.*
import space.uranos.entity.impl.*
import space.uranos.entity.Entity
import space.uranos.entity.EntityType
import space.uranos.entity.UranosEntity
import space.uranos.entity.impl.UranosPlayerEntity
import space.uranos.event.UranosEventBus
import space.uranos.event.UranosEventHandlerPositionManager
import space.uranos.logging.Logger
@ -79,20 +81,8 @@ class UranosServer internal constructor() : Server() {
private val internalEntities = HashSet<UranosEntity>()
override val entities: Set<Entity> = internalEntities
override fun <T : Entity> create(type: EntityType<T>): T {
val entity: UranosEntity = when (type) {
CowEntity -> UranosCowEntity(this)
BatEntity -> UranosBatEntity(this)
CreeperEntity -> UranosCreeperEntity(this)
MinecartEntity -> UranosMinecartEntity(this)
else -> throw IllegalArgumentException("Entities of this type cannot be created with this function")
}
internalEntities.add(entity)
@Suppress("UNCHECKED_CAST")
return entity as T
}
override fun <T : Entity> create(type: EntityType<T>): T =
createEntityInstance(this, type).also { internalEntities.add(it as UranosEntity) }
fun createPlayerEntity(player: UranosPlayer) =
UranosPlayerEntity(this, player).also { internalEntities.add(it) }

View file

@ -0,0 +1,11 @@
package space.uranos.entity
class EntityMetadataSynchronizer(val entity: UranosEntity) {
init {
// println(entity::class.allSuperclasses)
}
fun tick() {
}
}

View file

@ -23,8 +23,6 @@ import java.util.UUID
import java.util.WeakHashMap
sealed class UranosEntity(server: UranosServer) : Entity {
override fun belongsToChunk(key: Chunk.Key): Boolean = key == chunkKey
override val numericID: Int = server.claimEntityID()
override val uuid: UUID = UUID.randomUUID()
@ -65,19 +63,31 @@ sealed class UranosEntity(server: UranosServer) : Entity {
}
}
override fun toString(): String = "Entity($uuid)"
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 suspend fun tick()
abstract val chunkKey: Chunk.Key
protected val container = TickSynchronizationContainer()
protected fun sendSpawnAndDestroyPackets() {
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) } }
}
protected fun finishTick() {
@Suppress("LeakingThis")
private val metadataSynchronizer = EntityMetadataSynchronizer(this)
open suspend fun tick() {
container.tick()
metadataSynchronizer.tick()
sendSpawnAndDestroyPackets()
addedViewers.clear()
removedViewers.clear()
}
@ -106,8 +116,6 @@ abstract class UranosNotHasMovableHeadLivingEntity(server: UranosServer) : Urano
private var lastSentPitch: Float = 0f
final override suspend fun tick() {
container.tick()
val viewersWithoutAdded = viewers.subtract(addedViewers)
if (viewersWithoutAdded.isNotEmpty()) {
val packet = createMovementPacket()
@ -119,8 +127,7 @@ abstract class UranosNotHasMovableHeadLivingEntity(server: UranosServer) : Urano
if (this is PitchRotatable) lastSentPitch = pitch
if (this is YawRotatable) lastSentYaw = yaw
sendSpawnAndDestroyPackets()
finishTick()
super.tick()
}
private fun createMovementPacket(): OutgoingPacket? = createNotHasMovableHeadMovementPacket(position, lastSentPosition, lastSentYaw, lastSentPitch)
@ -138,8 +145,6 @@ abstract class UranosHasMovableHeadLivingEntity(server: UranosServer) : UranosLi
}
final override suspend fun tick() {
container.tick()
val viewersWithoutAdded = viewers.subtract(addedViewers)
if (viewersWithoutAdded.isNotEmpty()) {
val packets = createMovementPackets()
@ -152,8 +157,7 @@ abstract class UranosHasMovableHeadLivingEntity(server: UranosServer) : UranosLi
oldHeadPitch = headPitch
oldHeadYaw = headYaw
sendSpawnAndDestroyPackets()
finishTick()
super.tick()
}
private var oldPosition: Position = Position.ZERO
@ -216,8 +220,6 @@ abstract class UranosObjectEntity(server: UranosServer) : UranosEntity(server),
private var lastSentPitch: Float = 0f
final override suspend fun tick() {
container.tick()
val viewersWithoutAdded = viewers.subtract(addedViewers)
if (viewersWithoutAdded.isNotEmpty()) {
val packet = createMovementPacket()
@ -228,9 +230,9 @@ abstract class UranosObjectEntity(server: UranosServer) : UranosEntity(server),
if (this is PitchRotatable) lastSentPitch = pitch
if (this is YawRotatable) lastSentYaw = yaw
lastSentPosition = position
sendSpawnAndDestroyPackets()
finishTick()
super.tick()
}
private fun createMovementPacket(): OutgoingPacket? = createNotHasMovableHeadMovementPacket(position, lastSentPosition, lastSentYaw, lastSentPitch)
@ -247,8 +249,6 @@ class UranosPaintingEntity(
private var lastSentTopLeftLocation: VoxelLocation = topLeftLocation
override suspend fun tick() {
container.tick()
if (lastSentTopLeftLocation != topLeftLocation) {
val viewersWithoutAdded = viewers.subtract(addedViewers)
if (viewersWithoutAdded.isNotEmpty()) {
@ -269,8 +269,7 @@ class UranosPaintingEntity(
lastSentTopLeftLocation = topLeftLocation
sendSpawnAndDestroyPackets()
finishTick()
super.tick()
}
}

View file

@ -1,12 +1,10 @@
package space.uranos.entity.impl
package space.uranos.entity
import space.uranos.UranosServer
import space.uranos.entity.MinecartEntity
import space.uranos.entity.UranosObjectEntity
import space.uranos.util.numbers.validatePitch
import space.uranos.util.numbers.validateYaw
class UranosMinecartEntity(server: UranosServer) : UranosObjectEntity(server), MinecartEntity {
abstract class UranosMinecartEntity(server: UranosServer) : UranosObjectEntity(server), MinecartEntity {
override var yaw: Float = 0f
set(value) {
validateYaw(value); field = value
@ -16,4 +14,8 @@ class UranosMinecartEntity(server: UranosServer) : UranosObjectEntity(server), M
set(value) {
validatePitch(value); field = value
}
override val shakingPower: Int = 0
override val shakingDirection: Int = 1
override val shakingMultiplier: Float = 0f
}

View file

@ -0,0 +1,11 @@
package space.uranos.entity.impl
import space.uranos.UranosServer
import space.uranos.entity.RideableMinecartEntity
import space.uranos.entity.UranosMinecartEntity
import space.uranos.world.block.Block
class UranosRideableMinecartEntity(server: UranosServer) : UranosMinecartEntity(server), RideableMinecartEntity {
override val customBlock: Block? = null
override val blockOffset: Int = 6
}

View file

@ -68,6 +68,7 @@ class LoginAndJoinProcedure(val session: UranosSession) {
val result = AuthenticationHelper.authenticate(hashString, state.username)
session.sendNow(SetCompressionPacket(session.server.config.packetCompressionThreshold))
// TODO: Handle disconnect errors
session.enableCompressionCodec()
session.sendNow(LoginSuccessPacket(result.uuid, result.username))

View file

@ -18,6 +18,7 @@ class PacketsAdapter(val session: UranosSession) {
private val packetsForNextTick = ArrayList<OutgoingPacket>()
suspend fun tick() {
// TODO: Fix ConcurrentModificationException
packetsForNextTick.forEach { send(it) }
packetsForNextTick.clear()
}

View file

@ -125,6 +125,7 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer
if (!expected && currentProtocol != HandshakingProtocol && currentProtocol != StatusProtocol)
logger trace "The client disconnected unexpectedly"
// TODO: Remove the player entity and send PlayerInfo packet
packetsAdapter.stopProcessingIncomingPackets()
coroutineContext.cancel(DisconnectedCancellationException())
state = State.Disconnected