Archived
1
0
Fork 0

Add more packets and change some world related files

This commit is contained in:
Moritz Ruth 2020-11-03 19:55:24 +01:00
parent 785ee432cc
commit 2de7338a88
19 changed files with 231 additions and 47 deletions

View file

@ -1,7 +1,6 @@
plugins { plugins {
kotlin("jvm") kotlin("jvm")
kotlin("kapt") kotlin("kapt")
id("com.github.johnrengelman.shadow") version "6.1.0"
} }
group = rootProject.group group = rootProject.group
@ -31,6 +30,9 @@ dependencies {
// Netty // Netty
api("io.netty:netty-buffer:${nettyVersion}") api("io.netty:netty-buffer:${nettyVersion}")
// Other
api("com.google.guava:guava:30.0-jre")
// Testing // Testing
testImplementation("io.strikt:strikt-core:${striktVersion}") testImplementation("io.strikt:strikt-core:${striktVersion}")
testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}") testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")

View file

@ -4,7 +4,11 @@ import space.blokk.world.BlockLocation
import space.blokk.world.World import space.blokk.world.World
import kotlin.math.roundToInt 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], * 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]. * 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()) 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>()
}
} }

View file

@ -18,7 +18,6 @@ sealed class ChatComponent {
abstract val extra: List<ChatComponent>? abstract val extra: List<ChatComponent>?
init { init {
@Suppress("LeakingThis")
if (extra?.isEmpty() == true) throw IllegalArgumentException("extra cannot be an empty list. Use null instead.") if (extra?.isEmpty() == true) throw IllegalArgumentException("extra cannot be an empty list. Use null instead.")
} }

View file

@ -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. * 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 var gameMode: GameMode = GameMode.SURVIVAL

View file

@ -38,7 +38,7 @@ interface Player : EventTarget<PlayerEvent> {
*/ */
val settings: Settings val settings: Settings
val location: Location.InWorld val location: Location.WithRotation.InWorld
/** /**
* The index of the hotbar slot which is currently selected. * The index of the hotbar slot which is currently selected.

View file

@ -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)
})
}

View file

@ -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) 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. * @return A new [BlockLocation] with the maximum x, y and z values of a and b.
*/ */

View file

@ -48,7 +48,7 @@ interface Chunk {
* *
* If the implementation does not need to load chunks, this method should do nothing. * If the implementation does not need to load chunks, this method should do nothing.
*/ */
suspend fun load() suspend fun load() {}
companion object { companion object {
const val WIDTH_AND_LENGTH: Byte = 16 const val WIDTH_AND_LENGTH: Byte = 16

View file

@ -1,9 +1,13 @@
package space.blokk.world package space.blokk.world
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import space.blokk.CoordinatePartOrder import space.blokk.CoordinatePartOrder
import space.blokk.entity.Entity import space.blokk.entity.Entity
import space.blokk.world.block.BlockRef import space.blokk.world.block.BlockRef
import java.util.*
/** /**
* A Minecraft world, sometimes also called level. * 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 * **Note: Although the methods in this class are called `getBlock`, `getBlocksInSphere` and so on, they actually
* return [BlockRef]s.** * return [BlockRef]s.**
*/ */
abstract class World { abstract class World(val uuid: UUID) {
/** /**
* [CoroutineScope] of this world. * [CoroutineScope] of this world.
* *
* Gets cancelled when the world is unloaded. * 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. * The [dimension][WorldDimension] of this world.

View file

@ -14,20 +14,16 @@ abstract class Block internal constructor(val ref: BlockRef) {
*/ */
val destroyed: Boolean get() = ref.block === this val destroyed: Boolean get() = ref.block === this
/** protected fun checkNotDestroyed() {
* Replaces this block with air.
*/
fun replaceWithAir() {
checkNotDestroyed()
ref.place(Material.AIR)
}
private fun checkNotDestroyed() {
if (destroyed) throw IllegalStateException("The block was destroyed.") if (destroyed) throw IllegalStateException("The block was destroyed.")
} }
/** /**
* Called when [ref] no longer references this block. * Called when [ref] no longer references this block.
*/ */
internal abstract fun destroy() protected open fun onDestroy() {}
internal fun destroy() {
onDestroy()
}
} }

View file

@ -1,12 +1,8 @@
package space.blokk.world.block package space.blokk.world.block
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import space.blokk.world.BlockLocation import space.blokk.world.BlockLocation
import space.blokk.world.Chunk import space.blokk.world.Chunk
import space.blokk.world.World 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. * A reference to a block in a chunk of a world.
@ -34,21 +30,18 @@ abstract class BlockRef {
*/ */
abstract var block: Block; protected set 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). * 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)
/** // There should always be only one [BlockRef] instance for every location in a world.
* Create a new [Block] and assign it to this ref (i.e. place it in the world). override fun equals(other: Any?): Boolean = this === other
*/
suspend fun <T : Block> place(type: KClass<T>): T = type.primaryConstructor!!.call(this).also { override fun hashCode(): Int {
placeMutex.withLock { var result = location.hashCode()
block.destroy() result = 31 * result + world.hashCode()
block = it return result
}
} }
companion object { companion object {

View file

@ -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
}
}
}
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -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()

View file

@ -143,6 +143,8 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
// TODO: Send Declare Recipes packet // TODO: Send Declare Recipes packet
// TODO: Send Tags packet // TODO: Send Tags packet
// TODO: Send Entity Status packet with OP permission level // TODO: Send Entity Status packet with OP permission level
session.send(PlayerPositionAndLookPacket(spawnLocation.location))
} }
} }
} }

View file

@ -1,9 +1,6 @@
package space.blokk.player package space.blokk.player
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import space.blokk.Location import space.blokk.Location
import space.blokk.event.EventBus import space.blokk.event.EventBus
import space.blokk.logging.Logger import space.blokk.logging.Logger
@ -17,17 +14,19 @@ class BlokkPlayer(
override val uuid: UUID, override val uuid: UUID,
override var gameMode: GameMode, override var gameMode: GameMode,
override var settings: Player.Settings, override var settings: Player.Settings,
override val location: Location.InWorld, override val location: Location.WithRotation.InWorld,
selectedHotbarSlot: Byte selectedHotbarSlot: Byte
) : Player { ) : Player {
private val identifier = "BlokkPlayer($username)" private val identifier = "BlokkPlayer($username)"
private val logger = Logger(identifier) private val logger = Logger(identifier)
override val scope = CoroutineScope( override val scope by lazy {
Job(session.scope.coroutineContext[Job]) CoroutineScope(
SupervisorJob(session.scope.coroutineContext[Job])
+ Dispatchers.Unconfined + Dispatchers.Unconfined
+ CoroutineName(identifier) + CoroutineName(identifier)
) )
}
override val eventBus = EventBus(PlayerEvent::class, scope, logger) override val eventBus = EventBus(PlayerEvent::class, scope, logger)

View file

@ -5,7 +5,7 @@ plugins {
} }
group = "space.blokk" group = "space.blokk"
version = "0.0.1" version = "0.0.1-SNAPSHOT"
repositories { repositories {
mavenCentral() mavenCentral()

View file

@ -76,8 +76,7 @@ class FilesGenerator(private val dataDir: File, private val outputDir: File, pri
continue continue
val type = TypeSpec.classBuilder(upperCamelName) val type = TypeSpec.classBuilder(upperCamelName)
.addKdoc("A [$upperUnderscoreName][%T] block", specificMaterialType) .addKdoc("A block of type [$upperUnderscoreName][%T]", specificMaterialType)
.addModifiers(KModifier.ABSTRACT)
.superclass(BLOCK_TYPE) .superclass(BLOCK_TYPE)
.primaryConstructor( .primaryConstructor(
FunSpec.constructorBuilder() FunSpec.constructorBuilder()