Archived
1
0
Fork 0

Rework entity movement and spawning

This commit is contained in:
Moritz Ruth 2021-02-27 23:50:32 +01:00
parent dc156fcf34
commit a4947b5644
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
77 changed files with 644 additions and 376 deletions

3
.idea/misc.xml generated
View file

@ -2,4 +2,7 @@
<project version="4">
<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">
<option name="disableForAll" value="true" />
</component>
</project>

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Moritz Ruth and the Uranos contributors
Copyright (c) 2020-2021 Moritz Ruth and the Uranos contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -42,14 +42,18 @@ running the `Optimize Imports` action, you need to disable automatic wildcard im
### KDoc
1. If the name of the target is already sufficient for understanding what it does or what it's value represents, you
should **not** add a comment. If you want to provide additional information however, you should start the comment
with a short description nevertheless.
2. The name of the return type, property type or type of the enclosing class should **not** be wrapped in square
brackets.
1. If the name of the target is already sufficient for understanding what it does or what it's value represents, you should **not** add a comment. If you want
to provide additional information however, you should start the comment with a short description nevertheless.
2. The name of the return type, property type or type of the enclosing class should **not** be wrapped in square brackets.
3. If a comment only consists of the `@returns` block tag, the latter should be replaced with a sentence starting with
`Returns `.
### Code
### Packets
1. If a member function of a class creates an instance of another class which represents the same value, the functions name should be `as<name of the other class>`. If the new instance does not exactly represent the value of the original instance (for example because it is rounded), the name should be `to<name of the other class>`.
1. Outgoing packets should not use classes purely used as data containers such as Location. Enums are allowed.
### Other
1. If a member function of a class creates an instance of another class which represents the same value, the functions name should
be `as<name of the other class>`. If the new instance does not exactly represent the value of the original instance (for example because it is rounded), the
name should be `to<name of the other class>`.

View file

@ -5,15 +5,14 @@
package space.uranos.testplugin
import space.uranos.Position
import space.uranos.Uranos
import space.uranos.chat.ChatColor
import space.uranos.chat.TextComponent
import space.uranos.entity.CowEntity
import space.uranos.entity.MinecartEntity
import space.uranos.entity.Position
import space.uranos.net.ServerListInfo
import space.uranos.net.event.ServerListInfoRequestEvent
import space.uranos.net.event.SessionAfterLoginEvent
import space.uranos.net.packet.play.EntityHeadYawPacket
import space.uranos.player.GameMode
import space.uranos.plugin.Plugin
import space.uranos.testplugin.anvil.AnvilWorld
@ -23,11 +22,41 @@ import space.uranos.util.secondsToTicks
import space.uranos.world.*
import space.uranos.world.block.GreenWoolBlock
import space.uranos.world.block.RedWoolBlock
import kotlin.random.Random
import kotlin.random.nextUBytes
class TestPlugin : Plugin("Test", "1.0.0") {
fun enableOptifineFix() {
Uranos.biomeRegistry.register(
Biome(
"minecraft:swamp",
Biome.Precipitation.NONE,
RGBColor(12),
RGBColor(1),
RGBColor(1),
RGBColor(1),
Biome.MoodSound(0, 0.0, "minecraft:entity.pig.ambient", 0),
0f,
0f
)
)
Uranos.biomeRegistry.register(
Biome(
"minecraft:swamp_hills",
Biome.Precipitation.NONE,
RGBColor(12),
RGBColor(1),
RGBColor(1),
RGBColor(1),
Biome.MoodSound(0, 0.0, "minecraft:entity.pig.ambient", 0),
0f,
0f
)
)
}
override suspend fun onEnable() {
enableOptifineFix()
val dimension = Dimension(
"test:test",
true,
@ -64,10 +93,6 @@ class TestPlugin : Plugin("Test", "1.0.0") {
event.response = ServerListInfo("1.16.4", 754, TextComponent of "Test", 10, 0, emptyList())
}
val entity = Uranos.create<CowEntity>()
entity.position = Position(0.5, 10.0, 0.5, 0f, 0f)
entity.setWorld(world)
Uranos.eventBus.on<SessionAfterLoginEvent> { event ->
event.gameMode = GameMode.CREATIVE
event.canFly = true
@ -75,7 +100,6 @@ class TestPlugin : Plugin("Test", "1.0.0") {
event.initialWorldAndLocation = VoxelLocation
.of(0, 2, 0)
.atTopCenter()
.withRotation(0f, 0f)
.inside(world)
Uranos.scheduler.executeRepeating(secondsToTicks(1)) {
@ -85,20 +109,23 @@ class TestPlugin : Plugin("Test", "1.0.0") {
}
}
var x = 1.0
Uranos.scheduler.executeRepeating(20) {
x += 0.2
// Not showing up yet because no metadata is sent
val entity = Uranos.create<MinecartEntity>()
entity.position = Position(0.0, 4.0, 0.0)
entity.setWorld(world)
var x = 0f
var y = -90f
Uranos.scheduler.executeRepeating(1) {
runInServerThread {
Uranos.players.forEach {
it.session.send(
EntityHeadYawPacket(
entity.numericID,
Random.nextUBytes(1)[0]
)
)
}
entity.yaw = x
entity.pitch = y
}
if (x >= 10.0) x = 0.0
x += 5f
y += 5f
if (x == 360f) x = 0f
if (y == 90f) y = -90f
}
}
}

View file

@ -7,7 +7,7 @@ package space.uranos.testplugin.anvil
import com.google.common.cache.LoadingCache
import space.uranos.player.Player
import space.uranos.util.createWeakValuesLoadingCache
import space.uranos.util.collections.createWeakValuesLoadingCache
import space.uranos.world.*
import space.uranos.world.block.AirBlock

View file

@ -10,19 +10,3 @@ enum class CoordinatePart {
Y,
Z
}
data class CoordinatePartOrder(val first: CoordinatePart, val second: CoordinatePart, val third: CoordinatePart) {
init {
val values = arrayOf(first, second, third)
if (values.distinct().size != values.size)
throw IllegalArgumentException("first, second and third must have different values")
}
companion object {
val DEFAULT = CoordinatePartOrder(
CoordinatePart.X,
CoordinatePart.Y,
CoordinatePart.Z
)
}
}

View file

@ -1,52 +0,0 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos
import space.uranos.world.VoxelLocation
import space.uranos.world.World
import kotlin.math.roundToInt
/**
* A combination of x, y and z coordinates and an orientation (yaw and pitch).
*
* @param yaw The yaw rotation in degrees. Must be in `[0; 360[`.
* @param headPitch The pitch rotation of the head as a value between -90 (up) and 90 (down).
*/
data class Position(val x: Double, val y: Double, val z: Double, val yaw: Float, val headPitch: Float) {
init {
if (yaw >= 360) throw IllegalArgumentException("yaw must be lower than 360")
if (yaw < 0) throw IllegalArgumentException("yaw must not be negative")
}
/**
* Converts this Position to a VoxelLocation by converting [x], [y] and [z] to integers using [Double.toInt],
* in contrast to [roundToBlock] which uses [Double.roundToInt].
*/
fun toVoxelLocation(): VoxelLocation = VoxelLocation(x.toInt(), y.toInt().toUByte(), z.toInt())
/**
* Converts this Position to a VoxelLocation by converting [x], [y] and [z] to integers using [Double.roundToInt],
* in contrast to [toVoxelLocation] which uses [Double.toInt].
*/
fun roundToBlock(): VoxelLocation = VoxelLocation(x.roundToInt(), y.roundToInt().toUByte(), z.roundToInt())
fun toLocation(): Location = Location(x, y, z)
fun toVector() = Vector(x, y, z)
infix fun inside(world: World): Pair<World, Position> = world to this
val yawIn256Steps: UByte get() = ((yaw / 360) * 256).toInt().toUByte()
val headPitchIn256Steps: UByte get() = ((yaw / 360) * 256).toInt().toUByte()
operator fun get(part: CoordinatePart): Double = when (part) {
CoordinatePart.X -> x
CoordinatePart.Y -> y
CoordinatePart.Z -> z
}
companion object {
val ZERO = Position(0.0, 0.0, 0.0, 0f, 0f)
}
}

View file

@ -5,7 +5,8 @@
package space.uranos
import space.uranos.util.clamp
import space.uranos.entity.Position
import space.uranos.util.numbers.clamp
import space.uranos.world.VoxelLocation
import kotlin.math.abs
import kotlin.math.pow
@ -18,7 +19,7 @@ data class Vector(val x: Double, val y: Double, val z: Double) {
*/
fun toVoxelLocation() = VoxelLocation(x.toInt(), y.toInt().clamp(0..255).toUByte(), z.toInt())
fun roundToBlock() = VoxelLocation(x.roundToInt(), y.toInt().clamp(0..255).toUByte(), z.roundToInt())
fun asLocation() = Location(x, y, z)
fun asPosition() = Position(x, y, z)
operator fun plus(other: Vector) = Vector(x + other.x, y + other.y, z + other.z)
operator fun minus(other: Vector) = Vector(x - other.x, y - other.y, z - other.z)

View file

@ -40,7 +40,7 @@ interface Entity {
suspend fun setWorld(world: World?)
/**
* If players should be added to [viewers] when they join.
* If players should be added to [viewers] when they enter [world].
*/
var visibleToNewPlayers: Boolean
}

View file

@ -0,0 +1,18 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.entity
interface HasMovableHead : Mobile, Entity {
/**
* Must be `-90..90`.
*/
var headPitch: Float
/**
* Must be not negative and lower than 360.
*/
var headYaw: Float
}

View file

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

View file

@ -5,7 +5,6 @@
package space.uranos.entity
import space.uranos.Position
import space.uranos.Vector
interface Mobile {

View file

@ -18,8 +18,7 @@ interface PaintingEntity : Entity {
companion object Type : AreaEffectCloudEntityType()
}
val PaintingEntity.centerLocation
get() = topLeftLocation.copy(
x = max(0, motive.width / 2) + topLeftLocation.x,
z = motive.height / 2 + topLeftLocation.z
)
fun PaintingEntity.calculateCenterLocation() = topLeftLocation.copy(
x = max(0, motive.width / 2) + topLeftLocation.x,
z = motive.height / 2 + topLeftLocation.z
)

View file

@ -5,6 +5,6 @@
package space.uranos.entity
import space.uranos.UranosServer
class UranosCowEntity(server: UranosServer) : UranosLivingEntity(server), CowEntity
interface PitchRotatable {
var pitch: Float
}

View file

@ -8,7 +8,7 @@ package space.uranos.entity
import space.uranos.player.Player
import space.uranos.world.World
interface PlayerEntity : LivingEntity {
interface PlayerEntity : LivingEntity, HasMovableHead {
val player: Player
companion object Type : PlayerEntityType()

View file

@ -3,36 +3,40 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos
package space.uranos.entity
import space.uranos.CoordinatePart
import space.uranos.Vector
import space.uranos.world.VoxelLocation
import space.uranos.world.World
import kotlin.math.roundToInt
/**
* Represents a combination of x, y and z coordinates.
* A combination of x, y and z coordinates.
*/
data class Location(val x: Double, val y: Double, val z: Double) {
data class Position(val x: Double, val y: Double, val z: Double) {
/**
* Converts this Location to a VoxelLocation by converting [x], [y] and [z] to integers using [Double.toInt],
* Converts this Position to a VoxelLocation by converting [x], [y] and [z] to integers using [Double.toInt],
* in contrast to [roundToBlock] which uses [Double.roundToInt].
*/
fun toVoxelLocation(): VoxelLocation = VoxelLocation(x.toInt(), y.toInt().toUByte(), z.toInt())
/**
* Converts this Location to a VoxelLocation by converting [x], [y] and [z] to integers using [Double.roundToInt],
* Converts this Position to a VoxelLocation by converting [x], [y] and [z] to integers using [Double.roundToInt],
* in contrast to [toVoxelLocation] which uses [Double.toInt].
*/
fun roundToBlock(): VoxelLocation = VoxelLocation(x.roundToInt(), y.roundToInt().toUByte(), z.roundToInt())
fun withRotation(yaw: Float, pitch: Float) = Position(x, y, z, yaw, pitch)
fun asVector() = Vector(x, y, z)
infix fun inside(world: World): Pair<World, Location> = world to this
infix fun inside(world: World): Pair<World, Position> = world to this
operator fun get(part: CoordinatePart): Double = when (part) {
CoordinatePart.X -> x
CoordinatePart.Y -> y
CoordinatePart.Z -> z
}
companion object {
val ZERO = Position(0.0, 0.0, 0.0)
}
}

View file

@ -0,0 +1,10 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.entity
interface YawRotatable {
var yaw: Float
}

View file

@ -8,9 +8,9 @@ package space.uranos.net
import io.netty.buffer.ByteBuf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import space.uranos.Position
import space.uranos.Uranos
import space.uranos.chat.TextComponent
import space.uranos.entity.Position
import space.uranos.event.EventBusWrapper
import space.uranos.net.packet.OutgoingPacket
import space.uranos.net.packet.Protocol
@ -77,6 +77,7 @@ abstract class Session {
val gameMode: GameMode,
val world: World,
val position: Position,
val headYaw: Float,
val headPitch: Float,
val invulnerable: Boolean,
val reducedDebugInfo: Boolean,
@ -123,6 +124,8 @@ abstract class Session {
*/
abstract fun send(packet: OutgoingPacket)
fun send(packets: Iterable<OutgoingPacket>) = packets.forEach { this.send(it) }
/**
* Sends a plugin message packet.
*/

View file

@ -5,7 +5,7 @@
package space.uranos.net.event
import space.uranos.Position
import space.uranos.entity.Position
import space.uranos.event.Cancellable
import space.uranos.net.Session
import space.uranos.player.GameMode
@ -30,6 +30,7 @@ class SessionAfterLoginEvent(override val target: Session) : SessionEvent(), Can
*/
var initialWorldAndLocation: Pair<World, Position>? = null
var headYaw: Float = 0f
var headPitch: Float = 0f
var maxViewDistance: Int = 32

View file

@ -0,0 +1,10 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.player.event
import space.uranos.player.Player
class PlayerReadyEvent(player: Player) : PlayerEvent(player)

View file

@ -1,13 +0,0 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.util
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.job
import kotlin.coroutines.CoroutineContext
fun CoroutineContext.supervisorChild(name: String) = this + CoroutineName(name) + SupervisorJob(this.job)

View file

@ -10,8 +10,8 @@ import kotlinx.coroutines.launch
import java.lang.ref.WeakReference
import kotlin.reflect.KProperty
// If it is too resource-intensive to create a new object for every property, one instance could be used per TickSynchronizationContainer
class TickSynchronizationContainer {
// If it is too resource-intensive to create a new object for every property, one instance could be used per TickSynchronizationContainer
private val delegates = mutableSetOf<Delegate<*>>()
suspend fun tick() {

View file

@ -3,7 +3,7 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.util
package space.uranos.util.collections
import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader

View file

@ -3,7 +3,7 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.util
package space.uranos.util.collections
import kotlin.reflect.KClass

View file

@ -3,7 +3,7 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.util
package space.uranos.util.collections
import java.util.Spliterator
import java.util.function.Predicate

View file

@ -3,7 +3,7 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.util
package space.uranos.util.numbers
import kotlin.experimental.and
import kotlin.experimental.inv

View file

@ -3,10 +3,14 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.util
package space.uranos.util.numbers
fun Int.clamp(range: IntRange) = maxOf(minOf(range.first, range.last), minOf(maxOf(range.first, range.last), this))
fun clampArgument(name: String, range: IntRange, actualValue: Int) {
fun validateParameterIsInRange(name: String, range: IntRange, actualValue: Int) {
if (!range.contains(actualValue)) throw IllegalArgumentException("$name must be in $range")
}
fun validateParameterIsInRange(name: String, range: IntRange, actualValue: Float) {
if (range.first > actualValue || range.last < actualValue) throw IllegalArgumentException("$name must be in $range")
}

View file

@ -3,7 +3,7 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.util
package space.uranos.util.numbers
fun floorMod(dividend: Float, divisor: Float): Float {
if (divisor == 0f) throw ArithmeticException("divisor cannot be 0")

View file

@ -0,0 +1,8 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.util.numbers
fun Float.mapToUByte(divisor: Float) = ((this / divisor) * 256).toInt().toUByte()

View file

@ -3,7 +3,7 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.util
package space.uranos.util.numbers
infix fun Int.untilPossiblyLower(other: Int) =
IntProgression.fromClosedRange(this, other, if (this > other) -1 else 1)

View file

@ -0,0 +1,15 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.util.numbers
fun validateYaw(value: Float, name: String = "yaw") {
if (value >= 360f) throw IllegalArgumentException("$name must be lower than 360")
if (value < 0f) throw IllegalArgumentException("$name must not be negative")
}
fun validatePitch(value: Float, name: String = "pitch") {
validateParameterIsInRange(name, -90..90, value)
}

View file

@ -7,8 +7,8 @@ package space.uranos.world
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import space.uranos.Position
import space.uranos.Uranos
import space.uranos.entity.Position
import space.uranos.player.Player
import kotlin.math.floor

View file

@ -6,8 +6,8 @@
package space.uranos.world
import space.uranos.CoordinatePart
import space.uranos.Location
import space.uranos.Vector
import space.uranos.entity.Position
/**
* A location consisting of an x, y and z coordinate.
@ -19,7 +19,7 @@ data class VoxelLocation(val x: Int, val y: UByte, val z: Int) {
/**
* Converts this VoxelLocation to a Location.
*/
fun asLocation(): Location = Location(x.toDouble(), y.toDouble(), z.toDouble())
fun asPosition(): Position = Position(x.toDouble(), y.toDouble(), z.toDouble())
fun asVector(): Vector = Vector(x.toDouble(), y.toDouble(), z.toDouble())
@ -28,14 +28,14 @@ data class VoxelLocation(val x: Int, val y: UByte, val z: Int) {
*
* Example: `VoxelLocation(x = 1, y = 2, z = 3)` becomes `Location(x = 1.5, y = 2.5, z = 3.5)`.
*/
fun atCenter(): Location = Location(x.toDouble() + 0.5, y.toDouble() + 0.5, z.toDouble() + 0.5)
fun atCenter(): Position = Position(x.toDouble() + 0.5, y.toDouble() + 0.5, z.toDouble() + 0.5)
/**
* Converts this VoxelLocation to a Location **and then adds 0.5 to x and z, but not y**.
*
* Example: `VoxelLocation(x = 1, y = 2, z = 3)` becomes `Location(x = 1.5, y = 2, z = 3.5)`.
*/
fun atTopCenter(): Location = Location(x.toDouble() + 0.5, y.toDouble(), z.toDouble() + 0.5)
fun atTopCenter(): Position = Position(x.toDouble() + 0.5, y.toDouble(), z.toDouble() + 0.5)
/**
* Returns a VoxelLocation with the maximum x, y and z values of this and [other].
@ -54,6 +54,8 @@ data class VoxelLocation(val x: Int, val y: UByte, val z: Int) {
}
companion object {
val ZERO = VoxelLocation(0, 0u, 0)
fun of(x: Int, y: Int, z: Int): VoxelLocation {
if (y !in 0..255) throw IllegalArgumentException("y must be in 0..255")
return VoxelLocation(x, y.toUByte(), z)

View file

@ -10,7 +10,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext
import space.uranos.Vector
import space.uranos.entity.Entity
import space.uranos.util.untilPossiblyLower
import space.uranos.util.numbers.untilPossiblyLower
interface World {
val dispatcher: CoroutineDispatcher

View file

@ -1,6 +1,7 @@
package space.uranos.util
import org.junit.jupiter.api.Test
import space.uranos.util.numbers.floorMod
import strikt.api.expectThat
import strikt.api.expectThrows
import strikt.assertions.isEqualTo

View file

@ -12,7 +12,7 @@ import space.uranos.net.MinecraftProtocolDataTypes.writeNBT
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec
import space.uranos.util.generateHeightmap
import space.uranos.util.setBit
import space.uranos.util.numbers.setBit
import space.uranos.util.toCompactLongArray
import space.uranos.world.block.AirBlock
import space.uranos.world.block.CaveAirBlock

View file

@ -8,8 +8,8 @@ package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec
import space.uranos.util.checkBit
import space.uranos.util.setBit
import space.uranos.util.numbers.checkBit
import space.uranos.util.numbers.setBit
object ChunkLightDataPacketCodec : OutgoingPacketCodec<ChunkLightDataPacket>(0x23, ChunkLightDataPacket::class) {
private const val OUTSIDE_SECTIONS_MASK = 0b100000000000000001
@ -38,13 +38,13 @@ object ChunkLightDataPacketCodec : OutgoingPacketCodec<ChunkLightDataPacket>(0x2
dst.writeVarInt((emptyBlockLightMask shl 1) or OUTSIDE_SECTIONS_MASK)
for (sectionValues in data.skyLightValues) {
if (sectionValues === null) continue
if (sectionValues == null) continue
dst.writeVarInt(2048)
sectionValues.forEach { dst.writeByte(it.toInt()) }
}
for (sectionValues in data.blockLightValues) {
if (sectionValues === null) continue
if (sectionValues == null) continue
dst.writeVarInt(2048)
sectionValues.forEach { dst.writeByte(it.toInt()) }
}

View file

@ -13,7 +13,7 @@ import space.uranos.net.packet.IncomingPacketCodec
import space.uranos.player.ChatMode
import space.uranos.player.Hand
import space.uranos.player.SkinPartsConfiguration
import space.uranos.util.checkBit
import space.uranos.util.numbers.checkBit
object ClientSettingsPacketCodec : IncomingPacketCodec<ClientSettingsPacket>(0x05, ClientSettingsPacket::class) {
override fun decode(msg: ByteBuf): ClientSettingsPacket {

View file

@ -9,11 +9,11 @@ import io.netty.buffer.ByteBuf
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec
object EntityHeadPitchPacketCodec :
OutgoingPacketCodec<EntityHeadPitchPacket>(0x29, EntityHeadPitchPacket::class) {
override fun EntityHeadPitchPacket.encode(dst: ByteBuf) {
object EntityOrientationPacketCodec :
OutgoingPacketCodec<EntityOrientationPacket>(0x29, EntityOrientationPacket::class) {
override fun EntityOrientationPacket.encode(dst: ByteBuf) {
dst.writeVarInt(entityID)
dst.writeByte(0) // Should be yaw, but is actually ignored. Use EntityHeadYawPacket instead.
dst.writeByte(yaw.toInt())
dst.writeByte(pitch.toInt())
dst.writeBoolean(onGround)
}

View file

@ -17,7 +17,7 @@ object EntityRelativeMoveWithOrientationPacketCodec :
dst.writeShort(deltaY.toInt())
dst.writeShort(deltaZ.toInt())
dst.writeByte(yaw.toInt())
dst.writeByte(headPitch.toInt())
dst.writeByte(pitch.toInt())
dst.writeBoolean(onGround)
}
}

View file

@ -17,7 +17,7 @@ object EntityTeleportPacketCodec :
dst.writeDouble(y)
dst.writeDouble(z)
dst.writeByte(yaw.toInt())
dst.writeByte(headPitch.toInt())
dst.writeByte(pitch.toInt())
dst.writeBoolean(onGround)
}
}

View file

@ -6,9 +6,9 @@
package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf
import space.uranos.Position
import space.uranos.entity.Position
import space.uranos.net.packet.IncomingPacketCodec
import space.uranos.util.floorMod
import space.uranos.util.numbers.floorMod
object IncomingPlayerPositionPacketCodec :
IncomingPacketCodec<IncomingPlayerPositionPacket>(0x13, IncomingPlayerPositionPacket::class) {
@ -16,10 +16,10 @@ object IncomingPlayerPositionPacketCodec :
Position(
msg.readDouble(),
msg.readDouble(),
msg.readDouble(),
floorMod(msg.readFloat(), 360f),
msg.readFloat()
msg.readDouble()
),
floorMod(msg.readFloat(), 360f),
msg.readFloat(),
msg.readBoolean()
)
}

View file

@ -8,17 +8,17 @@ package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec
import space.uranos.util.bitmask
import space.uranos.util.numbers.bitmask
object OutgoingPlayerPositionPacketCodec :
OutgoingPacketCodec<OutgoingPlayerPositionPacket>(0x34, OutgoingPlayerPositionPacket::class) {
override fun OutgoingPlayerPositionPacket.encode(dst: ByteBuf) {
dst.writeDouble(position.x)
dst.writeDouble(position.y)
dst.writeDouble(position.z)
dst.writeFloat(position.yaw)
dst.writeFloat(position.headPitch)
dst.writeByte(bitmask(relativeX, relativeY, relativeZ, relativeYaw, relativePitch))
dst.writeDouble(x)
dst.writeDouble(y)
dst.writeDouble(z)
dst.writeFloat(yaw)
dst.writeFloat(pitch)
dst.writeByte(bitmask(relativeX, relativeY, relativeZ, relativeYaw, relativeHeadPitch))
dst.writeVarInt(0) // Teleport ID, I am not sure why this is needed
}
}

View file

@ -18,8 +18,8 @@ object PlayProtocol : Protocol(
DeclareRecipesPacketCodec,
DestroyEntitiesPacketCodec,
DisconnectPacketCodec,
EntityHeadPitchPacketCodec,
EntityHeadYawPacketCodec,
EntityOrientationPacketCodec,
EntityRelativeMovePacketCodec,
EntityRelativeMoveWithOrientationPacketCodec,
EntityTeleportPacketCodec,

View file

@ -7,7 +7,7 @@ package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf
import space.uranos.net.packet.OutgoingPacketCodec
import space.uranos.util.bitmask
import space.uranos.util.numbers.bitmask
object PlayerAbilitiesPacketCodec : OutgoingPacketCodec<PlayerAbilitiesPacket>(0x30, PlayerAbilitiesPacket::class) {
override fun PlayerAbilitiesPacket.encode(dst: ByteBuf) {

View file

@ -6,13 +6,13 @@
package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf
import space.uranos.Location
import space.uranos.entity.Position
import space.uranos.net.packet.IncomingPacketCodec
object PlayerLocationPacketCodec :
IncomingPacketCodec<PlayerLocationPacket>(0x12, PlayerLocationPacket::class) {
override fun decode(msg: ByteBuf): PlayerLocationPacket = PlayerLocationPacket(
Location(msg.readDouble(), msg.readDouble(), msg.readDouble()),
IncomingPacketCodec<PlayerPositionPacket>(0x12, PlayerPositionPacket::class) {
override fun decode(msg: ByteBuf): PlayerPositionPacket = PlayerPositionPacket(
Position(msg.readDouble(), msg.readDouble(), msg.readDouble()),
msg.readBoolean()
)
}

View file

@ -7,7 +7,7 @@ package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf
import space.uranos.net.packet.IncomingPacketCodec
import space.uranos.util.floorMod
import space.uranos.util.numbers.floorMod
object PlayerOrientationPacketCodec :
IncomingPacketCodec<PlayerOrientationPacket>(0x14, PlayerOrientationPacket::class) {

View file

@ -6,16 +6,15 @@
package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf
import space.uranos.Difficulty
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec
object SpawnExperienceOrbPacketCodec : OutgoingPacketCodec<SpawnExperienceOrbPacket>(0x01, SpawnExperienceOrbPacket::class) {
override fun SpawnExperienceOrbPacket.encode(dst: ByteBuf) {
dst.writeVarInt(entityID)
dst.writeDouble(location.x)
dst.writeDouble(location.y)
dst.writeDouble(location.z)
dst.writeDouble(x)
dst.writeDouble(y)
dst.writeDouble(z)
dst.writeShort(amount.toInt())
}
}

View file

@ -9,9 +9,9 @@ import io.netty.buffer.ByteBuf
import space.uranos.net.MinecraftProtocolDataTypes.writeUUID
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec
import space.uranos.util.numbers.mapToUByte
object SpawnLivingEntityPacketCodec : OutgoingPacketCodec<SpawnLivingEntityPacket>(0x02, SpawnLivingEntityPacket::class) {
@Suppress("DuplicatedCode")
override fun SpawnLivingEntityPacket.encode(dst: ByteBuf) {
dst.writeVarInt(entityID)
dst.writeUUID(uuid)
@ -19,9 +19,12 @@ object SpawnLivingEntityPacketCodec : OutgoingPacketCodec<SpawnLivingEntityPacke
dst.writeDouble(position.x)
dst.writeDouble(position.y)
dst.writeDouble(position.z)
dst.writeByte(position.yawIn256Steps.toInt())
dst.writeByte(position.headPitchIn256Steps.toInt())
dst.writeByte(0) // Head pitch; I do not know what this does
dst.writeByte(yaw.mapToUByte(360f).toInt())
dst.writeByte(pitch.mapToUByte(360f).toInt())
// 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())

View file

@ -11,16 +11,15 @@ import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec
object SpawnObjectEntityPacketCodec : OutgoingPacketCodec<SpawnObjectEntityPacket>(0x00, SpawnObjectEntityPacket::class) {
@Suppress("DuplicatedCode")
override fun SpawnObjectEntityPacket.encode(dst: ByteBuf) {
dst.writeVarInt(entityID)
dst.writeUUID(uuid)
dst.writeVarInt(type.numericID)
dst.writeDouble(position.x)
dst.writeDouble(position.y)
dst.writeDouble(position.z)
dst.writeByte(position.yawIn256Steps.toInt())
dst.writeByte(position.headPitchIn256Steps.toInt())
dst.writeVarInt(type)
dst.writeDouble(x)
dst.writeDouble(y)
dst.writeDouble(z)
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())

View file

@ -7,6 +7,7 @@ package space.uranos.net.packet.play
import space.uranos.net.packet.OutgoingPacket
// Name on wiki.vg: Entity Head Look
data class EntityHeadYawPacket(
val entityID: Int,
val yaw: UByte

View file

@ -7,8 +7,14 @@ package space.uranos.net.packet.play
import space.uranos.net.packet.OutgoingPacket
data class EntityHeadPitchPacket(
data class EntityOrientationPacket(
val entityID: Int,
/**
* Ignored for entities implementing [HasMovableHead][space.uranos.entity.HasMovableHead].
*
* [EntityHeadYawPacket][space.uranos.net.packet.play.EntityHeadYawPacket] must be used instead.
*/
val yaw: UByte,
val pitch: UByte,
val onGround: Boolean
) : OutgoingPacket()

View file

@ -7,6 +7,7 @@ package space.uranos.net.packet.play
import space.uranos.net.packet.OutgoingPacket
// Name on wiki.vg: Entity Position
data class EntityRelativeMovePacket(
val entityID: Int,
val deltaX: Short,

View file

@ -7,18 +7,22 @@ package space.uranos.net.packet.play
import space.uranos.net.packet.OutgoingPacket
// Name on wiki.vg: Entity Position and Rotation
data class EntityRelativeMoveWithOrientationPacket(
val entityID: Int,
val deltaX: Short,
val deltaY: Short,
val deltaZ: Short,
/**
* Absolute value.
* Ignored for entities implementing [HasMovableHead][space.uranos.entity.HasMovableHead].
*
* [EntityHeadYawPacket][space.uranos.net.packet.play.EntityHeadYawPacket] must be used instead.
*/
val yaw: UByte,
/**
* Absolute value.
*/
val headPitch: UByte,
val pitch: UByte,
val onGround: Boolean
) : OutgoingPacket()
) : OutgoingPacket() {
companion object {
fun convertToDeltaShort(delta: Double): Short = (delta * 32 * 128).toInt().toShort()
}
}

View file

@ -5,7 +5,6 @@
package space.uranos.net.packet.play
import space.uranos.Position
import space.uranos.net.packet.OutgoingPacket
data class EntityTeleportPacket(
@ -13,17 +12,12 @@ data class EntityTeleportPacket(
val x: Double,
val y: Double,
val z: Double,
/**
* Ignored for entities implementing [HasMovableHead][space.uranos.entity.HasMovableHead].
*
* [EntityHeadYawPacket][space.uranos.net.packet.play.EntityHeadYawPacket] must be used instead.
*/
val yaw: UByte,
val headPitch: UByte,
val pitch: UByte,
val onGround: Boolean
) : OutgoingPacket() {
constructor(entityID: Int, position: Position, onGround: Boolean) : this(
entityID,
position.x,
position.y,
position.z,
position.yawIn256Steps,
position.headPitchIn256Steps,
onGround
)
}
) : OutgoingPacket()

View file

@ -5,13 +5,16 @@
package space.uranos.net.packet.play
import space.uranos.Position
import space.uranos.entity.Position
import space.uranos.net.packet.IncomingPacket
// Name on wiki.vg: Player Position and Rotation (serverbound)
/**
* Combination of [PlayerLocationPacket] and [PlayerOrientationPacket].
* Combination of [PlayerPositionPacket] and [PlayerOrientationPacket].
*/
data class IncomingPlayerPositionPacket(
val position: Position,
val yaw: Float,
val pitch: Float,
val onGround: Boolean
) : IncomingPacket()

View file

@ -5,19 +5,23 @@
package space.uranos.net.packet.play
import space.uranos.Position
import space.uranos.net.packet.OutgoingPacket
import kotlin.random.Random
// Name on wiki.vg: Player Position and Look (clientbound)
/**
* Teleports the receiving player to the specified position.
*/
data class OutgoingPlayerPositionPacket(
val position: Position,
val x: Double,
val y: Double,
val z: Double,
val yaw: Float,
val pitch: Float,
val relativeX: Boolean = false,
val relativeY: Boolean = false,
val relativeZ: Boolean = false,
val relativeYaw: Boolean = false,
val relativePitch: Boolean = false,
val relativeHeadPitch: Boolean = false,
val teleportID: Int = Random.nextInt()
) : OutgoingPacket()

View file

@ -5,22 +5,14 @@
package space.uranos.net.packet.play
import space.uranos.Position
import space.uranos.net.packet.IncomingPacket
// Name on wiki.vg: Player Rotation
/**
* Sent by the client to update the player's orientation on the server.
*
* @see [Position]
*/
data class PlayerOrientationPacket(
/**
* Yaw in degrees.
*/
val yaw: Float,
/**
* Pitch in degrees.
*/
val pitch: Float,
val onGround: Boolean
) : IncomingPacket()

View file

@ -5,13 +5,14 @@
package space.uranos.net.packet.play
import space.uranos.Location
import space.uranos.entity.Position
import space.uranos.net.packet.IncomingPacket
// Name on wiki.vg: Player Position
/**
* Sent by the client to update the player's x, y and z coordinates on the server.
*/
data class PlayerLocationPacket(
val location: Location,
data class PlayerPositionPacket(
val position: Position,
val onGround: Boolean
) : IncomingPacket()

View file

@ -5,20 +5,15 @@
package space.uranos.net.packet.play
import space.uranos.Location
import space.uranos.Position
import space.uranos.Vector
import space.uranos.entity.EntityType
import space.uranos.net.packet.OutgoingPacket
import space.uranos.world.Chunk
import space.uranos.world.ChunkData
import java.util.*
/**
* Sent to spawn experience orbs.
*/
data class SpawnExperienceOrbPacket(
val entityID: Int,
val location: Location,
val x: Double,
val y: Double,
val z: Double,
val amount: Short
) : OutgoingPacket()

View file

@ -5,9 +5,9 @@
package space.uranos.net.packet.play
import space.uranos.Position
import space.uranos.Vector
import space.uranos.entity.EntityType
import space.uranos.entity.Position
import space.uranos.net.packet.OutgoingPacket
import java.util.UUID
@ -19,6 +19,9 @@ data class SpawnLivingEntityPacket(
val uuid: UUID,
val type: EntityType<*>,
val position: Position,
val yaw: Float,
val pitch: Float,
val headYaw: Float,
/**
* Velocity in blocks per tick
*/

View file

@ -5,9 +5,7 @@
package space.uranos.net.packet.play
import space.uranos.Position
import space.uranos.Vector
import space.uranos.entity.EntityType
import space.uranos.net.packet.OutgoingPacket
import java.util.UUID
@ -17,8 +15,12 @@ import java.util.UUID
data class SpawnObjectEntityPacket(
val entityID: Int,
val uuid: UUID,
val type: EntityType<*>,
val position: Position,
val type: Int,
val x: Double,
val y: Double,
val z: Double,
val yaw: UByte,
val pitch: UByte,
val data: Int,
val velocity: Vector
) : OutgoingPacket()

View file

@ -1,48 +0,0 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.util
import space.uranos.Position
import space.uranos.abs
import space.uranos.net.packet.OutgoingPacket
import space.uranos.net.packet.play.EntityHeadPitchPacket
import space.uranos.net.packet.play.EntityRelativeMovePacket
import space.uranos.net.packet.play.EntityRelativeMoveWithOrientationPacket
import space.uranos.net.packet.play.EntityTeleportPacket
fun createEntityMovementPacket(entityID: Int, oldPosition: Position, newPosition: Position): OutgoingPacket? {
val delta = abs(newPosition.toVector() - oldPosition.toVector())
val orientationChanged = oldPosition.yaw != newPosition.yaw || oldPosition.headPitch != newPosition.headPitch
val onGround = true // TODO: Find out what onGround does
return if (delta.x + delta.y + delta.z == 0.0) {
if (orientationChanged) EntityHeadPitchPacket(
entityID,
newPosition.headPitchIn256Steps,
onGround
) else null
} else if (delta.x > 8 || delta.y > 8 || delta.z > 8) {
EntityTeleportPacket(entityID, newPosition, onGround)
} else {
if (orientationChanged) EntityRelativeMoveWithOrientationPacket(
entityID,
getDeltaShort(delta.x),
getDeltaShort(delta.y),
getDeltaShort(delta.z),
newPosition.yawIn256Steps,
newPosition.headPitchIn256Steps,
onGround
) else EntityRelativeMovePacket(
entityID,
getDeltaShort(delta.x),
getDeltaShort(delta.y),
getDeltaShort(delta.z),
onGround
)
}
}
private fun getDeltaShort(delta: Double): Short = (delta * 32 * 128).toInt().toShort()

View file

@ -10,20 +10,37 @@ import space.uranos.net.packet.OutgoingPacket
import space.uranos.net.packet.play.SpawnLivingEntityPacket
import space.uranos.net.packet.play.SpawnObjectEntityPacket
import space.uranos.net.packet.play.SpawnPaintingPacket
import space.uranos.util.numbers.mapToUByte
fun Entity.createSpawnPacket(): OutgoingPacket = when (this) {
is LivingEntity -> SpawnLivingEntityPacket(
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,
position,
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
)
@ -31,7 +48,7 @@ fun Entity.createSpawnPacket(): OutgoingPacket = when (this) {
numericID,
uuid,
motive,
centerLocation,
calculateCenterLocation(),
facing
)
else -> throw IllegalArgumentException("Unknown entity type")
@ -39,6 +56,7 @@ fun Entity.createSpawnPacket(): OutgoingPacket = when (this) {
fun ObjectEntity.getDataValue(): Int = when (this) {
is ItemEntity -> 1
is MinecartEntity -> 2
// TODO: Add remaining
else -> throw IllegalArgumentException("Unknown entity type")
}

View file

@ -11,20 +11,17 @@ import com.sksamuel.hoplite.ConfigSource
import kotlinx.coroutines.runBlocking
import space.uranos.config.UranosConfig
import space.uranos.entity.*
import space.uranos.entity.event.ViewingChangedEvent
import space.uranos.event.EventHandlerPosition
import space.uranos.entity.impl.*
import space.uranos.event.UranosEventBus
import space.uranos.event.UranosEventHandlerPositionManager
import space.uranos.logging.Logger
import space.uranos.logging.UranosLoggingOutputProvider
import space.uranos.net.UranosSocketServer
import space.uranos.net.packet.play.DestroyEntitiesPacket
import space.uranos.net.packet.play.PlayerInfoPacket
import space.uranos.player.UranosPlayer
import space.uranos.plugin.UranosPluginManager
import space.uranos.server.Server
import space.uranos.util.EncryptionUtils
import space.uranos.util.createSpawnPacket
import space.uranos.util.msToTicks
import space.uranos.util.runInServerThread
import space.uranos.world.UranosWorldRegistry
@ -90,6 +87,9 @@ class UranosServer internal constructor() : Server() {
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")
}
@ -159,12 +159,7 @@ class UranosServer internal constructor() : Server() {
}
private fun registerListeners() {
eventBus.on<ViewingChangedEvent>(EventHandlerPosition.LAST) { event ->
if (event.target == event.player.entity) return@on
if (event.viewing) event.player.session.send(event.target.createSpawnPacket())
else event.player.session.send(DestroyEntitiesPacket(arrayOf(event.target.numericID)))
}
// Nothing
}
companion object {

View file

@ -10,11 +10,15 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import space.uranos.*
import space.uranos.entity.event.ViewingChangedEvent
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.TickSynchronizationContainer
import space.uranos.util.WatchableSet
import space.uranos.util.createEntityMovementPacket
import space.uranos.util.memoized
import space.uranos.util.*
import space.uranos.util.collections.WatchableSet
import space.uranos.util.numbers.mapToUByte
import space.uranos.util.numbers.validatePitch
import space.uranos.util.numbers.validateYaw
import space.uranos.world.Chunk
import space.uranos.world.VoxelLocation
import space.uranos.world.World
@ -24,38 +28,16 @@ import java.util.UUID
import java.util.WeakHashMap
sealed class UranosEntity(server: UranosServer) : Entity {
abstract val chunkKey: Chunk.Key
override fun belongsToChunk(key: Chunk.Key): Boolean = key == chunkKey
override val numericID: Int = server.claimEntityID()
override val uuid: UUID = UUID.randomUUID()
override val viewers: MutableSet<Player> = object : WatchableSet<Player>(Collections.newSetFromMap(WeakHashMap())) {
override fun beforeAdd(element: Player) {
if ((this@UranosEntity as? UranosPlayerEntity)?.let { it.player == element } == true)
throw IllegalArgumentException("A player cannot be a viewer of it's own entity")
}
override fun onAdd(element: Player) {
Uranos.scope.launch { Uranos.eventBus.emit(ViewingChangedEvent(this@UranosEntity, element, true)) }
}
override fun onRemove(element: Player) {
Uranos.scope.launch { Uranos.eventBus.emit(ViewingChangedEvent(this@UranosEntity, element, false)) }
}
}
/**
* If players should be added to [viewers] when they join.
*/
override var visibleToNewPlayers: Boolean = true
private val worldMutex = Mutex()
final override var world: World? = null; private set
protected val container = TickSynchronizationContainer()
override suspend fun setWorld(world: World?) {
if (world == null && this is PlayerEntity)
throw IllegalArgumentException("You cannot set the world of a PlayerEntity to null")
@ -69,41 +51,189 @@ sealed class UranosEntity(server: UranosServer) : Entity {
}
}
open suspend fun tick() {
container.tick()
protected val addedViewers = mutableSetOf<Player>()
protected val removedViewers = mutableSetOf<Player>()
override val viewers: MutableSet<Player> = object : WatchableSet<Player>(Collections.newSetFromMap(WeakHashMap())) {
override fun beforeAdd(element: Player) {
if ((this@UranosEntity as? UranosPlayerEntity)?.let { it.player == element } == true)
throw IllegalArgumentException("A player cannot be a viewer of it's own entity")
}
override fun onAdd(element: Player) {
removedViewers.remove(element)
addedViewers.add(element)
Uranos.scope.launch { Uranos.eventBus.emit(ViewingChangedEvent(this@UranosEntity, element, true)) }
}
override fun onRemove(element: Player) {
addedViewers.remove(element)
removedViewers.add(element)
Uranos.scope.launch { Uranos.eventBus.emit(ViewingChangedEvent(this@UranosEntity, element, false)) }
}
}
abstract suspend fun tick()
abstract val chunkKey: Chunk.Key
protected val container = TickSynchronizationContainer()
protected 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() {
addedViewers.clear()
removedViewers.clear()
}
}
abstract class UranosLivingEntity(server: UranosServer) : UranosEntity(server), LivingEntity {
sealed class UranosLivingEntity(server: UranosServer) : UranosEntity(server), LivingEntity {
override var velocity: Vector = Vector.ZERO
override var position: Position = Position.ZERO
override val chunkKey: Chunk.Key by memoized({ position }) { Chunk.Key.from(position) }
}
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
override var position: Position by container.ifChanged(Position.ZERO) { value ->
if (viewers.isNotEmpty()) createEntityMovementPacket(numericID, lastSentPosition, value)?.let {
for (viewer in viewers) {
viewer.session.send(it)
private var lastSentYaw: Float = 0f
private var lastSentPitch: Float = 0f
final override suspend fun tick() {
container.tick()
val viewersWithoutAdded = viewers.subtract(addedViewers)
if (viewersWithoutAdded.isNotEmpty()) {
val packet = createMovementPacket()
if (packet != null) {
viewersWithoutAdded.forEach { it.session.send(packet) }
}
}
lastSentPosition = value
if (this is PitchRotatable) lastSentPitch = pitch
if (this is YawRotatable) lastSentYaw = yaw
sendSpawnAndDestroyPackets()
finishTick()
}
override val chunkKey: Chunk.Key by memoized({ position }) { Chunk.Key.from(position) }
private fun createMovementPacket(): OutgoingPacket? = createNotHasMovableHeadMovementPacket(position, lastSentPosition, lastSentYaw, lastSentPitch)
}
abstract class UranosHasMovableHeadLivingEntity(server: UranosServer) : UranosLivingEntity(server), HasMovableHead {
override var headYaw: Float = 0f
set(value) {
validateYaw(value, "headYaw"); field = value
}
override var headPitch: Float = 0f
set(value) {
validatePitch(value, "headPitch"); field = value
}
final override suspend fun tick() {
container.tick()
val viewersWithoutAdded = viewers.subtract(addedViewers)
if (viewersWithoutAdded.isNotEmpty()) {
val packets = createMovementPackets()
if (packets.isNotEmpty()) {
viewersWithoutAdded.forEach { it.session.send(packets) }
}
}
oldPosition = position
oldHeadPitch = headPitch
oldHeadYaw = headYaw
sendSpawnAndDestroyPackets()
finishTick()
}
private var oldPosition: Position = Position.ZERO
private var oldHeadYaw: Float = 0f
private var oldHeadPitch: Float = 0f
private fun createMovementPackets(): ArrayList<OutgoingPacket> {
val packets = ArrayList<OutgoingPacket>(2)
val delta = position.asVector() - oldPosition.asVector()
val absoluteDelta = abs(delta)
val onGround = true
val oldHeadPitchUByte = oldHeadPitch.mapToUByte(90f)
val newHeadPitchUByte = headPitch.mapToUByte(90f)
val oldHeadYawUByte = oldHeadYaw.mapToUByte(360f)
val newHeadYawUByte = headYaw.mapToUByte(360f)
if (absoluteDelta.x + absoluteDelta.y + absoluteDelta.z == 0.0) {
if (oldHeadPitchUByte != newHeadPitchUByte) packets += EntityOrientationPacket(
numericID,
0u,
newHeadPitchUByte,
onGround
)
} else if (absoluteDelta.x > 8 || absoluteDelta.y > 8 || absoluteDelta.z > 8) {
packets += EntityTeleportPacket(numericID, position.x, position.y, position.z, 0u, newHeadPitchUByte, onGround)
} else {
packets += if (oldHeadPitchUByte != newHeadPitchUByte) EntityRelativeMoveWithOrientationPacket(
numericID,
EntityRelativeMoveWithOrientationPacket.convertToDeltaShort(delta.x),
EntityRelativeMoveWithOrientationPacket.convertToDeltaShort(delta.y),
EntityRelativeMoveWithOrientationPacket.convertToDeltaShort(delta.z),
0u,
newHeadPitchUByte,
onGround
) else EntityRelativeMovePacket(
numericID,
EntityRelativeMoveWithOrientationPacket.convertToDeltaShort(delta.x),
EntityRelativeMoveWithOrientationPacket.convertToDeltaShort(delta.y),
EntityRelativeMoveWithOrientationPacket.convertToDeltaShort(delta.z),
onGround
)
}
if (oldHeadYawUByte != newHeadYawUByte) packets += EntityHeadYawPacket(numericID, newHeadYawUByte)
return packets
}
}
abstract class UranosObjectEntity(server: UranosServer) : UranosEntity(server), ObjectEntity {
override var velocity: Vector = Vector.ZERO
private var lastSentPosition: Position = Position.ZERO
override var position: Position by container.ifChanged(Position.ZERO) { value ->
createEntityMovementPacket(numericID, lastSentPosition, value)?.let {
for (viewer in viewers) {
viewer.session.send(it)
}
}
}
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
final override suspend fun tick() {
container.tick()
val viewersWithoutAdded = viewers.subtract(addedViewers)
if (viewersWithoutAdded.isNotEmpty()) {
val packet = createMovementPacket()
if (packet != null) {
viewersWithoutAdded.forEach { it.session.send(packet) }
}
}
if (this is PitchRotatable) lastSentPitch = pitch
if (this is YawRotatable) lastSentYaw = yaw
sendSpawnAndDestroyPackets()
finishTick()
}
private fun createMovementPacket(): OutgoingPacket? = createNotHasMovableHeadMovementPacket(position, lastSentPosition, lastSentYaw, lastSentPitch)
}
class UranosPaintingEntity(
@ -113,4 +243,76 @@ class UranosPaintingEntity(
override val motive: PaintingMotive
) : UranosEntity(server), PaintingEntity {
override val chunkKey: Chunk.Key get() = Chunk.Key.from(topLeftLocation)
private var lastSentTopLeftLocation: VoxelLocation = topLeftLocation
override suspend fun tick() {
container.tick()
if (lastSentTopLeftLocation != topLeftLocation) {
val viewersWithoutAdded = viewers.subtract(addedViewers)
if (viewersWithoutAdded.isNotEmpty()) {
val centerLocation = calculateCenterLocation()
val packet = EntityTeleportPacket(
numericID,
centerLocation.x.toDouble(),
centerLocation.y.toDouble(),
centerLocation.z.toDouble(),
0u,
0u,
true
)
viewersWithoutAdded.forEach { it.session.send(packet) }
}
}
lastSentTopLeftLocation = topLeftLocation
sendSpawnAndDestroyPackets()
finishTick()
}
}
fun UranosEntity.createNotHasMovableHeadMovementPacket(
position: Position,
lastSentPosition: Position,
lastSentYaw: Float,
lastSentPitch: Float
): OutgoingPacket? {
val delta = position.asVector() - lastSentPosition.asVector()
val absoluteDelta = abs(delta)
val onGround = true
val oldPitchUByte = lastSentPitch.mapToUByte(90f)
val newPitchUByte = if (this is PitchRotatable) pitch.mapToUByte(90f) else 0u
val oldYawUByte = lastSentYaw.mapToUByte(360f)
val newYawUByte = if (this is YawRotatable) yaw.mapToUByte(360f) else 0u
return if (absoluteDelta.x + absoluteDelta.y + absoluteDelta.z == 0.0) {
if (oldPitchUByte != newPitchUByte || oldYawUByte != newYawUByte) EntityOrientationPacket(
numericID,
newYawUByte,
newPitchUByte,
onGround
) else null
} else if (absoluteDelta.x > 8 || absoluteDelta.y > 8 || absoluteDelta.z > 8) {
EntityTeleportPacket(numericID, position.x, position.y, position.z, newYawUByte, newPitchUByte, onGround)
} else {
if (oldPitchUByte != newPitchUByte || oldYawUByte != newYawUByte) EntityRelativeMoveWithOrientationPacket(
numericID,
EntityRelativeMoveWithOrientationPacket.convertToDeltaShort(delta.x),
EntityRelativeMoveWithOrientationPacket.convertToDeltaShort(delta.y),
EntityRelativeMoveWithOrientationPacket.convertToDeltaShort(delta.z),
newYawUByte,
newPitchUByte,
onGround
) else EntityRelativeMovePacket(
numericID,
EntityRelativeMoveWithOrientationPacket.convertToDeltaShort(delta.x),
EntityRelativeMoveWithOrientationPacket.convertToDeltaShort(delta.y),
EntityRelativeMoveWithOrientationPacket.convertToDeltaShort(delta.z),
onGround
)
}
}

View file

@ -0,0 +1,12 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.entity.impl
import space.uranos.UranosServer
import space.uranos.entity.BatEntity
import space.uranos.entity.UranosHasMovableHeadLivingEntity
class UranosBatEntity(server: UranosServer) : UranosHasMovableHeadLivingEntity(server), BatEntity

View file

@ -0,0 +1,12 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.entity.impl
import space.uranos.UranosServer
import space.uranos.entity.CowEntity
import space.uranos.entity.UranosHasMovableHeadLivingEntity
class UranosCowEntity(server: UranosServer) : UranosHasMovableHeadLivingEntity(server), CowEntity

View file

@ -0,0 +1,12 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.entity.impl
import space.uranos.UranosServer
import space.uranos.entity.CreeperEntity
import space.uranos.entity.UranosHasMovableHeadLivingEntity
class UranosCreeperEntity(server: UranosServer) : UranosHasMovableHeadLivingEntity(server), CreeperEntity

View file

@ -0,0 +1,24 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.entity.impl
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 {
override var yaw: Float = 0f
set(value) {
validateYaw(value); field = value
}
override var pitch: Float = 0f
set(value) {
validatePitch(value); field = value
}
}

View file

@ -3,15 +3,17 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.entity
package space.uranos.entity.impl
import space.uranos.UranosServer
import space.uranos.entity.PlayerEntity
import space.uranos.entity.UranosHasMovableHeadLivingEntity
import space.uranos.player.Player
import java.util.UUID
class UranosPlayerEntity(
server: UranosServer,
override val player: Player
) : UranosLivingEntity(server), PlayerEntity {
) : UranosHasMovableHeadLivingEntity(server), PlayerEntity {
override val uuid: UUID = player.uuid
}

View file

@ -17,6 +17,7 @@ import space.uranos.net.event.SessionAfterLoginEvent
import space.uranos.net.packet.login.*
import space.uranos.net.packet.play.*
import space.uranos.player.UranosPlayer
import space.uranos.player.event.PlayerReadyEvent
import space.uranos.tag.TagRegistry
import space.uranos.util.AuthenticationHelper
import space.uranos.util.EncryptionUtils
@ -134,6 +135,7 @@ class LoginAndJoinProcedure(val session: UranosSession) {
event.gameMode,
initialWorldAndLocation.first,
initialWorldAndLocation.second,
event.headYaw,
event.headPitch,
event.invulnerable,
event.reducedDebugInfo,
@ -160,8 +162,6 @@ class LoginAndJoinProcedure(val session: UranosSession) {
state.uuid,
state.gameMode,
settings,
state.position,
state.headPitch,
state.reducedDebugInfo,
state.fieldOfView,
state.canFly,
@ -182,7 +182,7 @@ class LoginAndJoinProcedure(val session: UranosSession) {
// session.send(DeclareCommandsPacket(session.server.commandRegistry.items.values))
// UnlockRecipes
session.sendNow(OutgoingPlayerPositionPacket(state.position))
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(
@ -209,6 +209,8 @@ class LoginAndJoinProcedure(val session: UranosSession) {
player.spawnInitially(state.world)
session.state = Session.State.Playing(player)
session.server.eventBus.emit(PlayerReadyEvent(session.player!!))
// WorldBorder
session.send(CompassTargetPacket(player.compassTarget))
}

View file

@ -10,6 +10,10 @@ import space.uranos.net.UranosSession
object IncomingPlayerPositionPacketHandler : PacketReceivedEventHandler<IncomingPlayerPositionPacket>() {
override suspend fun handle(session: UranosSession, packet: IncomingPlayerPositionPacket) {
session.earlyPlayer?.let { it.entity.position = packet.position } ?: error("Player not yet initialized")
session.earlyPlayer?.let {
it.entity.position = packet.position
it.entity.headYaw = packet.yaw
it.entity.headPitch = packet.pitch
} ?: error("Player not yet initialized")
}
}

View file

@ -15,6 +15,6 @@ object PlayProtocolHandler : ProtocolPacketReceivedEventHandler(
IncomingPlayerPositionPacket::class to IncomingPlayerPositionPacketHandler,
IncomingPluginMessagePacket::class to IncomingPluginMessagePacketHandler,
PlayerOrientationPacket::class to PlayerOrientationPacketHandler,
PlayerLocationPacket::class to PlayerLocationPacketHandler
PlayerPositionPacket::class to PlayerLocationPacketHandler
)
)

View file

@ -8,13 +8,13 @@ package space.uranos.net.packet.play
import space.uranos.net.PacketReceivedEventHandler
import space.uranos.net.UranosSession
object PlayerLocationPacketHandler : PacketReceivedEventHandler<PlayerLocationPacket>() {
override suspend fun handle(session: UranosSession, packet: PlayerLocationPacket) {
object PlayerLocationPacketHandler : PacketReceivedEventHandler<PlayerPositionPacket>() {
override suspend fun handle(session: UranosSession, packet: PlayerPositionPacket) {
val player = session.earlyPlayer ?: error("Player not yet initialized")
player.entity.position = player.entity.position.copy(
x = packet.location.x,
y = packet.location.y,
z = packet.location.z
x = packet.position.x,
y = packet.position.y,
z = packet.position.z
)
}
}

View file

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

View file

@ -5,7 +5,6 @@
package space.uranos.player
import space.uranos.Position
import space.uranos.chat.TextComponent
import space.uranos.entity.PlayerEntity
import space.uranos.entity.safeWorld
@ -15,7 +14,7 @@ import space.uranos.net.packet.play.ChunkLightDataPacket
import space.uranos.net.packet.play.PlayerInfoPacket
import space.uranos.net.packet.play.SelectedHotbarSlotPacket
import space.uranos.util.TickSynchronizationContainer
import space.uranos.util.clampArgument
import space.uranos.util.numbers.validateParameterIsInRange
import space.uranos.world.Chunk
import space.uranos.world.VoxelLocation
import space.uranos.world.World
@ -28,8 +27,6 @@ class UranosPlayer(
override val uuid: UUID,
override var gameMode: GameMode,
override var settings: Player.Settings,
position: Position,
headPitch: Float,
override var reducedDebugInfo: Boolean,
override var fieldOfView: Float,
override var canFly: Boolean,
@ -43,7 +40,7 @@ class UranosPlayer(
override var selectedHotbarSlot by container.ifChanged(
selectedHotbarSlot,
{ clampArgument("selectedHotbarSlot", 0..8, it) }) {
{ validateParameterIsInRange("selectedHotbarSlot", 0..8, it) }) {
session.sendNow(SelectedHotbarSlotPacket(it))
}
@ -60,12 +57,10 @@ class UranosPlayer(
override var currentlyViewedChunks = emptyList<Chunk>()
override val entity: PlayerEntity = session.server.createPlayerEntity(this).also {
it.position = position
}
override val entity: PlayerEntity = session.server.createPlayerEntity(this)
suspend fun spawnInitially(world: World) {
session.server.entities.forEach { if (it.visibleToNewPlayers && it != entity) it.viewers.add(this) }
session.server.entities.forEach { if (it.visibleToNewPlayers && it != entity && it.world === world) it.viewers.add(this) }
entity.setWorld(world)
updateCurrentlyViewedChunks()
sendChunksAndLight()