diff --git a/blokk-api/build.gradle.kts b/blokk-api/build.gradle.kts index 7118fe8..abecf8c 100644 --- a/blokk-api/build.gradle.kts +++ b/blokk-api/build.gradle.kts @@ -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}") diff --git a/blokk-api/src/main/kotlin/space/blokk/Location.kt b/blokk-api/src/main/kotlin/space/blokk/Location.kt index 7036425..774b9f5 100644 --- a/blokk-api/src/main/kotlin/space/blokk/Location.kt +++ b/blokk-api/src/main/kotlin/space/blokk/Location.kt @@ -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 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() + + 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() + } } diff --git a/blokk-api/src/main/kotlin/space/blokk/chat/ChatComponent.kt b/blokk-api/src/main/kotlin/space/blokk/chat/ChatComponent.kt index 366ad06..372f3a0 100644 --- a/blokk-api/src/main/kotlin/space/blokk/chat/ChatComponent.kt +++ b/blokk-api/src/main/kotlin/space/blokk/chat/ChatComponent.kt @@ -18,7 +18,6 @@ sealed class ChatComponent { abstract val extra: List? init { - @Suppress("LeakingThis") if (extra?.isEmpty() == true) throw IllegalArgumentException("extra cannot be an empty list. Use null instead.") } diff --git a/blokk-api/src/main/kotlin/space/blokk/net/event/PlayerInitializationEvent.kt b/blokk-api/src/main/kotlin/space/blokk/net/event/PlayerInitializationEvent.kt index 282c7d7..b869e38 100644 --- a/blokk-api/src/main/kotlin/space/blokk/net/event/PlayerInitializationEvent.kt +++ b/blokk-api/src/main/kotlin/space/blokk/net/event/PlayerInitializationEvent.kt @@ -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 diff --git a/blokk-api/src/main/kotlin/space/blokk/player/Player.kt b/blokk-api/src/main/kotlin/space/blokk/player/Player.kt index af229be..b8de844 100644 --- a/blokk-api/src/main/kotlin/space/blokk/player/Player.kt +++ b/blokk-api/src/main/kotlin/space/blokk/player/Player.kt @@ -38,7 +38,7 @@ interface Player : EventTarget { */ val settings: Settings - val location: Location.InWorld + val location: Location.WithRotation.InWorld /** * The index of the hotbar slot which is currently selected. diff --git a/blokk-api/src/main/kotlin/space/blokk/util/CreateWeakValuesLoadingCache.kt b/blokk-api/src/main/kotlin/space/blokk/util/CreateWeakValuesLoadingCache.kt new file mode 100644 index 0000000..6baa185 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/util/CreateWeakValuesLoadingCache.kt @@ -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 createWeakValuesLoadingCache(loader: (key: K) -> V): LoadingCache { + return CacheBuilder + .newBuilder() + .weakValues() + .build(object : CacheLoader() { + override fun load(key: K): V = loader(key) + }) +} diff --git a/blokk-api/src/main/kotlin/space/blokk/world/BlockLocation.kt b/blokk-api/src/main/kotlin/space/blokk/world/BlockLocation.kt index 8719dc1..1185586 100644 --- a/blokk-api/src/main/kotlin/space/blokk/world/BlockLocation.kt +++ b/blokk-api/src/main/kotlin/space/blokk/world/BlockLocation.kt @@ -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. */ diff --git a/blokk-api/src/main/kotlin/space/blokk/world/Chunk.kt b/blokk-api/src/main/kotlin/space/blokk/world/Chunk.kt index e87c3ce..48c4aa0 100644 --- a/blokk-api/src/main/kotlin/space/blokk/world/Chunk.kt +++ b/blokk-api/src/main/kotlin/space/blokk/world/Chunk.kt @@ -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 diff --git a/blokk-api/src/main/kotlin/space/blokk/world/World.kt b/blokk-api/src/main/kotlin/space/blokk/world/World.kt index 1b7b94a..356b516 100644 --- a/blokk-api/src/main/kotlin/space/blokk/world/World.kt +++ b/blokk-api/src/main/kotlin/space/blokk/world/World.kt @@ -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. diff --git a/blokk-api/src/main/kotlin/space/blokk/world/block/Block.kt b/blokk-api/src/main/kotlin/space/blokk/world/block/Block.kt index f716da2..ba6aceb 100644 --- a/blokk-api/src/main/kotlin/space/blokk/world/block/Block.kt +++ b/blokk-api/src/main/kotlin/space/blokk/world/block/Block.kt @@ -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() + } } diff --git a/blokk-api/src/main/kotlin/space/blokk/world/block/BlockRef.kt b/blokk-api/src/main/kotlin/space/blokk/world/block/BlockRef.kt index ecd60bf..3774001 100644 --- a/blokk-api/src/main/kotlin/space/blokk/world/block/BlockRef.kt +++ b/blokk-api/src/main/kotlin/space/blokk/world/block/BlockRef.kt @@ -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 place(type: KClass): 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 { diff --git a/blokk-packet-codecs/src/main/kotlin/space/blokk/net/packet/play/PlayerInfoPacketCodec.kt b/blokk-packet-codecs/src/main/kotlin/space/blokk/net/packet/play/PlayerInfoPacketCodec.kt new file mode 100644 index 0000000..98f81cb --- /dev/null +++ b/blokk-packet-codecs/src/main/kotlin/space/blokk/net/packet/play/PlayerInfoPacketCodec.kt @@ -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(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 + } + } + } + } +} diff --git a/blokk-packet-codecs/src/main/kotlin/space/blokk/net/packet/play/PlayerPositionAndLookPacketCodec.kt b/blokk-packet-codecs/src/main/kotlin/space/blokk/net/packet/play/PlayerPositionAndLookPacketCodec.kt new file mode 100644 index 0000000..3e0b543 --- /dev/null +++ b/blokk-packet-codecs/src/main/kotlin/space/blokk/net/packet/play/PlayerPositionAndLookPacketCodec.kt @@ -0,0 +1,24 @@ +package space.blokk.net.packet.play + +import io.netty.buffer.ByteBuf +import space.blokk.net.packet.OutgoingPacketCodec + +object PlayerPositionAndLookPacketCodec : + OutgoingPacketCodec(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) + } +} diff --git a/blokk-packets/src/main/kotlin/space/blokk/net/packet/play/PlayerInfoPacket.kt b/blokk-packets/src/main/kotlin/space/blokk/net/packet/play/PlayerInfoPacket.kt new file mode 100644 index 0000000..800577a --- /dev/null +++ b/blokk-packets/src/main/kotlin/space/blokk/net/packet/play/PlayerInfoPacket.kt @@ -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 { + abstract val entries: List + + interface IPlayerEntry { + val uuid: UUID + } + + data class AddPlayer(override val entries: List) : Action() { + data class PlayerEntry( + override val uuid: UUID, + val name: String, + val gameMode: GameMode, + val latency: Int, + val displayName: TextComponent?, + val properties: Map + ) : IPlayerEntry { + data class Property( + val value: String, + val signature: String? + ) + } + } + + data class UpdateGameMode(override val entries: List) : Action() { + data class PlayerEntry(override val uuid: UUID, val gameMode: GameMode) : IPlayerEntry + } + + data class UpdateLatency(override val entries: List) : Action() { + data class PlayerEntry(override val uuid: UUID, val latency: Int) : IPlayerEntry + } + + data class UpdateDisplayName(override val entries: List) : + Action() { + data class PlayerEntry(override val uuid: UUID, val displayName: TextComponent?) : IPlayerEntry + } + + data class RemovePlayer(override val entries: List) : Action() { + data class PlayerEntry(override val uuid: UUID) : IPlayerEntry + } + } +} diff --git a/blokk-packets/src/main/kotlin/space/blokk/net/packet/play/PlayerPositionAndLookPacket.kt b/blokk-packets/src/main/kotlin/space/blokk/net/packet/play/PlayerPositionAndLookPacket.kt new file mode 100644 index 0000000..8c3f518 --- /dev/null +++ b/blokk-packets/src/main/kotlin/space/blokk/net/packet/play/PlayerPositionAndLookPacket.kt @@ -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() diff --git a/blokk-server/src/main/kotlin/space/blokk/net/LoginAndJoinProcedure.kt b/blokk-server/src/main/kotlin/space/blokk/net/LoginAndJoinProcedure.kt index f973a0e..dd06eff 100644 --- a/blokk-server/src/main/kotlin/space/blokk/net/LoginAndJoinProcedure.kt +++ b/blokk-server/src/main/kotlin/space/blokk/net/LoginAndJoinProcedure.kt @@ -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)) } } } diff --git a/blokk-server/src/main/kotlin/space/blokk/player/BlokkPlayer.kt b/blokk-server/src/main/kotlin/space/blokk/player/BlokkPlayer.kt index e3cc809..ed25332 100644 --- a/blokk-server/src/main/kotlin/space/blokk/player/BlokkPlayer.kt +++ b/blokk-server/src/main/kotlin/space/blokk/player/BlokkPlayer.kt @@ -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]) - + Dispatchers.Unconfined - + CoroutineName(identifier) - ) + override val scope by lazy { + CoroutineScope( + SupervisorJob(session.scope.coroutineContext[Job]) + + Dispatchers.Unconfined + + CoroutineName(identifier) + ) + } override val eventBus = EventBus(PlayerEvent::class, scope, logger) diff --git a/build.gradle.kts b/build.gradle.kts index 3602848..925813c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } group = "space.blokk" -version = "0.0.1" +version = "0.0.1-SNAPSHOT" repositories { mavenCentral() diff --git a/buildSrc/src/main/kotlin/space/blokk/mdsp/FilesGenerator.kt b/buildSrc/src/main/kotlin/space/blokk/mdsp/FilesGenerator.kt index f7c075e..fa2c197 100644 --- a/buildSrc/src/main/kotlin/space/blokk/mdsp/FilesGenerator.kt +++ b/buildSrc/src/main/kotlin/space/blokk/mdsp/FilesGenerator.kt @@ -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()