Add more packets and change some world related files
This commit is contained in:
parent
785ee432cc
commit
2de7338a88
19 changed files with 231 additions and 47 deletions
|
@ -1,7 +1,6 @@
|
|||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("kapt")
|
||||
id("com.github.johnrengelman.shadow") version "6.1.0"
|
||||
}
|
||||
|
||||
group = rootProject.group
|
||||
|
@ -31,6 +30,9 @@ dependencies {
|
|||
// Netty
|
||||
api("io.netty:netty-buffer:${nettyVersion}")
|
||||
|
||||
// Other
|
||||
api("com.google.guava:guava:30.0-jre")
|
||||
|
||||
// Testing
|
||||
testImplementation("io.strikt:strikt-core:${striktVersion}")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
|
||||
|
|
|
@ -4,7 +4,11 @@ import space.blokk.world.BlockLocation
|
|||
import space.blokk.world.World
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
data class Location(val x: Double, val y: Double, val z: Double) {
|
||||
abstract class AbstractLocation {
|
||||
abstract val x: Double
|
||||
abstract val y: Double
|
||||
abstract val z: Double
|
||||
|
||||
/**
|
||||
* Converts this [Location] to a [BlockLocation] by converting [x], [y] and [z] to an integer using [Double.toInt],
|
||||
* in contrast to [roundToBlock] which uses [Double.roundToInt].
|
||||
|
@ -17,5 +21,36 @@ data class Location(val x: Double, val y: Double, val z: Double) {
|
|||
*/
|
||||
fun roundToBlock(): BlockLocation = BlockLocation(x.roundToInt(), y.roundToInt(), z.roundToInt())
|
||||
|
||||
data class InWorld(val world: World, val location: Location)
|
||||
/**
|
||||
* @return A [Pair] of this location and [world].
|
||||
*/
|
||||
abstract infix fun inside(world: World): InWorld<*>
|
||||
|
||||
abstract class InWorld<T : AbstractLocation> internal constructor() {
|
||||
abstract val world: World
|
||||
abstract val location: T
|
||||
}
|
||||
}
|
||||
|
||||
data class Location(override val x: Double, override val y: Double, override val z: Double) : AbstractLocation() {
|
||||
/**
|
||||
* @return A [Location.WithRotation] of this location, [yaw] and [pitch].
|
||||
*/
|
||||
fun withRotation(yaw: Float, pitch: Float) = WithRotation(x, y, z, yaw, pitch)
|
||||
|
||||
override fun inside(world: World) = InWorld(world, this)
|
||||
data class InWorld(override val world: World, override val location: Location) :
|
||||
AbstractLocation.InWorld<Location>()
|
||||
|
||||
data class WithRotation(
|
||||
override val x: Double,
|
||||
override val y: Double,
|
||||
override val z: Double,
|
||||
val yaw: Float,
|
||||
val pitch: Float
|
||||
) : AbstractLocation() {
|
||||
override fun inside(world: World) = InWorld(world, this)
|
||||
data class InWorld(override val world: World, override val location: WithRotation) :
|
||||
AbstractLocation.InWorld<WithRotation>()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ sealed class ChatComponent {
|
|||
abstract val extra: List<ChatComponent>?
|
||||
|
||||
init {
|
||||
@Suppress("LeakingThis")
|
||||
if (extra?.isEmpty() == true) throw IllegalArgumentException("extra cannot be an empty list. Use null instead.")
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ class PlayerInitializationEvent(session: Session, val settings: Player.Settings)
|
|||
/**
|
||||
* The location where the player will spawn. If this is null after all handlers ran, the player will disconnect.
|
||||
*/
|
||||
var spawnLocation: Location.InWorld? = null
|
||||
var spawnLocation: Location.WithRotation.InWorld? = null
|
||||
|
||||
var gameMode: GameMode = GameMode.SURVIVAL
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ interface Player : EventTarget<PlayerEvent> {
|
|||
*/
|
||||
val settings: Settings
|
||||
|
||||
val location: Location.InWorld
|
||||
val location: Location.WithRotation.InWorld
|
||||
|
||||
/**
|
||||
* The index of the hotbar slot which is currently selected.
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package space.blokk.util
|
||||
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import com.google.common.cache.CacheLoader
|
||||
import com.google.common.cache.LoadingCache
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
|
@ -17,6 +17,14 @@ data class BlockLocation(val x: Int, val y: Int, val z: Int) {
|
|||
*/
|
||||
fun getCenterLocation(): Location = Location(x.toDouble() + 0.5, y.toDouble() + 0.5, z.toDouble() + 0.5)
|
||||
|
||||
/**
|
||||
* Converts this [BlockLocation] to a [Location] by converting [x], [y] and [z] to a double using [Int.toDouble]
|
||||
* and then adding 0.5 to x and z, but not y.
|
||||
*
|
||||
* Example: `BlockLocation(x = 1, y = 2, z = 3)` becomes `Location(x = 1.5, y = 2, z = 3.5)`.
|
||||
*/
|
||||
fun getTopCenterLocation(): Location = Location(x.toDouble() + 0.5, y.toDouble(), z.toDouble() + 0.5)
|
||||
|
||||
/**
|
||||
* @return A new [BlockLocation] with the maximum x, y and z values of a and b.
|
||||
*/
|
||||
|
|
|
@ -48,7 +48,7 @@ interface Chunk {
|
|||
*
|
||||
* If the implementation does not need to load chunks, this method should do nothing.
|
||||
*/
|
||||
suspend fun load()
|
||||
suspend fun load() {}
|
||||
|
||||
companion object {
|
||||
const val WIDTH_AND_LENGTH: Byte = 16
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
package space.blokk.world
|
||||
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import space.blokk.CoordinatePartOrder
|
||||
import space.blokk.entity.Entity
|
||||
import space.blokk.world.block.BlockRef
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A Minecraft world, sometimes also called level.
|
||||
|
@ -11,13 +15,19 @@ import space.blokk.world.block.BlockRef
|
|||
* **Note: Although the methods in this class are called `getBlock`, `getBlocksInSphere` and so on, they actually
|
||||
* return [BlockRef]s.**
|
||||
*/
|
||||
abstract class World {
|
||||
abstract class World(val uuid: UUID) {
|
||||
/**
|
||||
* [CoroutineScope] of this world.
|
||||
*
|
||||
* Gets cancelled when the world is unloaded.
|
||||
*/
|
||||
abstract val scope: CoroutineScope
|
||||
val scope: CoroutineScope by lazy {
|
||||
CoroutineScope(
|
||||
SupervisorJob()
|
||||
+ Dispatchers.Unconfined
|
||||
+ CoroutineName("World($uuid)")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The [dimension][WorldDimension] of this world.
|
||||
|
|
|
@ -14,20 +14,16 @@ abstract class Block internal constructor(val ref: BlockRef) {
|
|||
*/
|
||||
val destroyed: Boolean get() = ref.block === this
|
||||
|
||||
/**
|
||||
* Replaces this block with air.
|
||||
*/
|
||||
fun replaceWithAir() {
|
||||
checkNotDestroyed()
|
||||
ref.place(Material.AIR)
|
||||
}
|
||||
|
||||
private fun checkNotDestroyed() {
|
||||
protected fun checkNotDestroyed() {
|
||||
if (destroyed) throw IllegalStateException("The block was destroyed.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when [ref] no longer references this block.
|
||||
*/
|
||||
internal abstract fun destroy()
|
||||
protected open fun onDestroy() {}
|
||||
|
||||
internal fun destroy() {
|
||||
onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
package space.blokk.world.block
|
||||
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import space.blokk.world.BlockLocation
|
||||
import space.blokk.world.Chunk
|
||||
import space.blokk.world.World
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
|
||||
/**
|
||||
* A reference to a block in a chunk of a world.
|
||||
|
@ -34,21 +30,18 @@ abstract class BlockRef {
|
|||
*/
|
||||
abstract var block: Block; protected set
|
||||
|
||||
private val placeMutex by lazy { Mutex() }
|
||||
|
||||
/**
|
||||
* Create a new [Block] and assign it to this ref (i.e. place it in the world).
|
||||
*/
|
||||
abstract fun place(material: Material): Block
|
||||
abstract suspend fun place(material: Material)
|
||||
|
||||
/**
|
||||
* Create a new [Block] and assign it to this ref (i.e. place it in the world).
|
||||
*/
|
||||
suspend fun <T : Block> place(type: KClass<T>): T = type.primaryConstructor!!.call(this).also {
|
||||
placeMutex.withLock {
|
||||
block.destroy()
|
||||
block = it
|
||||
}
|
||||
// There should always be only one [BlockRef] instance for every location in a world.
|
||||
override fun equals(other: Any?): Boolean = this === other
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = location.hashCode()
|
||||
result = 31 * result + world.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package space.blokk.net.packet.play
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import space.blokk.net.MinecraftProtocolDataTypes.writeString
|
||||
import space.blokk.net.packet.OutgoingPacketCodec
|
||||
|
||||
object PlayerInfoPacketCodec : OutgoingPacketCodec<PlayerInfoPacket>(0x32, PlayerInfoPacket::class) {
|
||||
override fun PlayerInfoPacket.encode(dst: ByteBuf) {
|
||||
when (val a = action) {
|
||||
is PlayerInfoPacket.Action.AddPlayer -> {
|
||||
dst.writeInt(0)
|
||||
dst.writeInt(a.entries.size)
|
||||
|
||||
for (entry in a.entries) {
|
||||
dst.writeString(entry.name)
|
||||
dst.writeInt(entry.properties.size)
|
||||
|
||||
for ((name, property) in entry.properties) {
|
||||
dst.writeString(name)
|
||||
dst.writeString(property.value)
|
||||
|
||||
val hasSignature = property.signature != null
|
||||
dst.writeBoolean(hasSignature)
|
||||
if (hasSignature) dst.writeString(property.signature!!)
|
||||
}
|
||||
|
||||
// Unfinished
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package space.blokk.net.packet.play
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import space.blokk.net.packet.OutgoingPacketCodec
|
||||
|
||||
object PlayerPositionAndLookPacketCodec :
|
||||
OutgoingPacketCodec<PlayerPositionAndLookPacket>(0x34, PlayerPositionAndLookPacket::class) {
|
||||
override fun PlayerPositionAndLookPacket.encode(dst: ByteBuf) {
|
||||
dst.writeDouble(locationWithRotation.x)
|
||||
dst.writeDouble(locationWithRotation.y)
|
||||
dst.writeDouble(locationWithRotation.z)
|
||||
dst.writeFloat(locationWithRotation.yaw)
|
||||
dst.writeFloat(locationWithRotation.pitch)
|
||||
|
||||
var flags = 0x00
|
||||
if (relativeX) flags = flags and 0x01
|
||||
if (relativeY) flags = flags and 0x02
|
||||
if (relativeZ) flags = flags and 0x04
|
||||
if (relativeYaw) flags = flags and 0x08
|
||||
if (relativePitch) flags = flags and 0x10
|
||||
|
||||
dst.writeByte(flags)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package space.blokk.net.packet.play
|
||||
|
||||
import space.blokk.chat.TextComponent
|
||||
import space.blokk.net.packet.OutgoingPacket
|
||||
import space.blokk.player.GameMode
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Informs the client about other players.
|
||||
* If the real game mode of a player differs from the one in this packet, weird things can happen.
|
||||
*/
|
||||
data class PlayerInfoPacket(val action: Action<*>) : OutgoingPacket() {
|
||||
sealed class Action<T : Action.IPlayerEntry> {
|
||||
abstract val entries: List<T>
|
||||
|
||||
interface IPlayerEntry {
|
||||
val uuid: UUID
|
||||
}
|
||||
|
||||
data class AddPlayer(override val entries: List<PlayerEntry>) : Action<AddPlayer.PlayerEntry>() {
|
||||
data class PlayerEntry(
|
||||
override val uuid: UUID,
|
||||
val name: String,
|
||||
val gameMode: GameMode,
|
||||
val latency: Int,
|
||||
val displayName: TextComponent?,
|
||||
val properties: Map<String, Property>
|
||||
) : IPlayerEntry {
|
||||
data class Property(
|
||||
val value: String,
|
||||
val signature: String?
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class UpdateGameMode(override val entries: List<PlayerEntry>) : Action<UpdateGameMode.PlayerEntry>() {
|
||||
data class PlayerEntry(override val uuid: UUID, val gameMode: GameMode) : IPlayerEntry
|
||||
}
|
||||
|
||||
data class UpdateLatency(override val entries: List<PlayerEntry>) : Action<UpdateLatency.PlayerEntry>() {
|
||||
data class PlayerEntry(override val uuid: UUID, val latency: Int) : IPlayerEntry
|
||||
}
|
||||
|
||||
data class UpdateDisplayName(override val entries: List<PlayerEntry>) :
|
||||
Action<UpdateDisplayName.PlayerEntry>() {
|
||||
data class PlayerEntry(override val uuid: UUID, val displayName: TextComponent?) : IPlayerEntry
|
||||
}
|
||||
|
||||
data class RemovePlayer(override val entries: List<PlayerEntry>) : Action<RemovePlayer.PlayerEntry>() {
|
||||
data class PlayerEntry(override val uuid: UUID) : IPlayerEntry
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package space.blokk.net.packet.play
|
||||
|
||||
import space.blokk.Location
|
||||
import space.blokk.net.packet.OutgoingPacket
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Teleports the receiving player to the specified location.
|
||||
*/
|
||||
data class PlayerPositionAndLookPacket(
|
||||
val locationWithRotation: Location.WithRotation,
|
||||
val relativeX: Boolean = false,
|
||||
val relativeY: Boolean = false,
|
||||
val relativeZ: Boolean = false,
|
||||
val relativeYaw: Boolean = false,
|
||||
val relativePitch: Boolean = false,
|
||||
val teleportID: Int = Random.nextInt()
|
||||
) : OutgoingPacket()
|
|
@ -143,6 +143,8 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
|
|||
// TODO: Send Declare Recipes packet
|
||||
// TODO: Send Tags packet
|
||||
// TODO: Send Entity Status packet with OP permission level
|
||||
|
||||
session.send(PlayerPositionAndLookPacket(spawnLocation.location))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
package space.blokk.player
|
||||
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.*
|
||||
import space.blokk.Location
|
||||
import space.blokk.event.EventBus
|
||||
import space.blokk.logging.Logger
|
||||
|
@ -17,17 +14,19 @@ class BlokkPlayer(
|
|||
override val uuid: UUID,
|
||||
override var gameMode: GameMode,
|
||||
override var settings: Player.Settings,
|
||||
override val location: Location.InWorld,
|
||||
override val location: Location.WithRotation.InWorld,
|
||||
selectedHotbarSlot: Byte
|
||||
) : Player {
|
||||
private val identifier = "BlokkPlayer($username)"
|
||||
private val logger = Logger(identifier)
|
||||
|
||||
override val scope = CoroutineScope(
|
||||
Job(session.scope.coroutineContext[Job])
|
||||
override val scope by lazy {
|
||||
CoroutineScope(
|
||||
SupervisorJob(session.scope.coroutineContext[Job])
|
||||
+ Dispatchers.Unconfined
|
||||
+ CoroutineName(identifier)
|
||||
)
|
||||
}
|
||||
|
||||
override val eventBus = EventBus(PlayerEvent::class, scope, logger)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ plugins {
|
|||
}
|
||||
|
||||
group = "space.blokk"
|
||||
version = "0.0.1"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
|
|
@ -76,8 +76,7 @@ class FilesGenerator(private val dataDir: File, private val outputDir: File, pri
|
|||
continue
|
||||
|
||||
val type = TypeSpec.classBuilder(upperCamelName)
|
||||
.addKdoc("A [$upperUnderscoreName][%T] block", specificMaterialType)
|
||||
.addModifiers(KModifier.ABSTRACT)
|
||||
.addKdoc("A block of type [$upperUnderscoreName][%T]", specificMaterialType)
|
||||
.superclass(BLOCK_TYPE)
|
||||
.primaryConstructor(
|
||||
FunSpec.constructorBuilder()
|
||||
|
|
Reference in a new issue