Archived
1
0
Fork 0

Update everything to 1.16.4

this took me about 12 hours. it's 1 am
This commit is contained in:
Moritz Ruth 2020-12-21 01:05:28 +01:00
parent 4aef22eee7
commit ffe2349884
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
64 changed files with 640 additions and 420 deletions

View file

@ -55,6 +55,7 @@ tasks {
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", // TODO: Remove and use @OptIn instead
"-Xopt-in=kotlin.contracts.ExperimentalContracts", // TODO: Remove and use @OptIn instead
"-Xopt-in=kotlin.ExperimentalUnsignedTypes", // TODO: Remove and use @OptIn instead
"-Xopt-in=kotlin.RequiresOptIn",
"-progressive"
)
}

View file

@ -5,36 +5,6 @@ import space.blokk.net.Session
import space.blokk.player.Player
import space.blokk.server.Server
interface BlokkProvider {
val server: Server
private lateinit var serverInstance: Server
/**
* Whether the server is in development mode.
*
* For example, [EventBus][space.blokk.event.EventBus]es will log all emitted events and the time
* it took for all handlers to execute.
*/
val developmentMode: Boolean
}
@Suppress("DEPRECATION")
object Blokk : BlokkProvider {
@Deprecated("This value should only be set by the server on startup.")
lateinit var provider: BlokkProvider
override val server get() = provider.server
override val developmentMode: Boolean get() = provider.developmentMode
val scheduler: Scheduler get() = server.scheduler
/**
* Shorthand for [Blokk.server.sessions][Server.sessions].
*/
val sessions: EventTargetGroup<Session> get() = server.sessions
/**
* Shorthand for [Blokk.server.players][Server.players].
*/
val players: EventTargetGroup<Player> get() = server.players
}
object Blokk : Server by serverInstance

View file

@ -1,6 +1,17 @@
package space.blokk
import space.blokk.world.Dimension
// TODO: Remove this. Inline classes are experimental and this is not really useful
inline class NamespacedID(val value: String) {
val namespace get() = value.substringBefore(":")
val id get() = value.substringAfter(":")
val valid get() = namespace.matches(NAMESPACE_REGEX) && id.matches(ID_REGEX)
companion object {
val ID_REGEX = Regex("[0-9a-z._/-]")
val NAMESPACE_REGEX = Regex("[0-9a-z_-]")
}
}

View file

@ -0,0 +1,34 @@
package space.blokk
import java.util.concurrent.ConcurrentHashMap
open class Registry<T: RegistryItem> {
protected val internalItems = ConcurrentHashMap<NamespacedID, T>()
val items: Map<NamespacedID, T> = internalItems
/**
* Adds [item] to [items].
*
* @return false when there is already an item with this ID, even if it is [item]. This is because otherwise this
* function would need to be blocking or suspending.
*/
open fun register(item: T): Boolean = internalItems.putIfAbsent(item.id, item) == null
/**
* Removes [item] from [items].
*
* @return true when the item was contained in [items].
*/
open fun unregister(item: T): Boolean = unregister(item.id)
/**
* Removes the item with [id] from [items].
*
* @return true when the item was contained in [items].
*/
open fun unregister(id: NamespacedID): Boolean = internalItems.remove(id) != null
}
interface RegistryItem {
val id: NamespacedID
}

View file

@ -6,16 +6,20 @@ import space.blokk.Blokk
import space.blokk.logging.Logger
import space.blokk.plugin.Plugin
import space.blokk.util.pluralizeWithCount
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.full.*
import kotlin.system.measureTimeMillis
// TODO: Only create one event bus for everything and add helper method instead
class EventBus<EventT : Event>(
private val eventClass: KClass<EventT>,
private val logger: Logger,
private val scope: CoroutineScope = Blokk.server.scope
coroutineContext: CoroutineContext = Blokk.coroutineContext
) {
private val scope = CoroutineScope(coroutineContext)
/**
* All event handlers, sorted by their priority and the order in which they were registered.
*/

View file

@ -4,7 +4,7 @@ import space.blokk.Blokk
class Logger(val name: String, private val printThreadName: Boolean = true) {
fun log(level: Level, message: String, throwable: Throwable? = null) =
Blokk.server.loggingOutputProvider.log(printThreadName, name, level, message, throwable)
Blokk.loggingOutputProvider.log(printThreadName, name, level, message, throwable)
fun error(msg: String, t: Throwable) {
if (Level.ERROR.isEnabled) {
@ -48,6 +48,6 @@ class Logger(val name: String, private val printThreadName: Boolean = true) {
fun isGreaterOrEqualThan(level: Level) = ordinal >= level.ordinal
val isEnabled get() = isGreaterOrEqualThan(Blokk.server.minimumLogLevel)
val isEnabled get() = isGreaterOrEqualThan(Blokk.minimumLogLevel)
}
}

View file

@ -12,17 +12,18 @@ import space.blokk.player.Player
import space.blokk.world.WorldAndLocationWithRotation
import java.net.InetAddress
import java.util.*
import kotlin.coroutines.CoroutineContext
interface Session : EventTarget<SessionEvent> {
interface Session : EventTarget<SessionEvent>, CoroutineScope {
/**
* The IP address of this session
*/
val address: InetAddress
/**
* The coroutine scope of this session. It is cancelled when the session is disconnected.
* Unconfined [CoroutineContext] which is cancelled when the session is disconnected.
*/
val scope: CoroutineScope
override val coroutineContext: CoroutineContext
/**
* The brand name the client optionally sent during the login procedure.
@ -39,7 +40,7 @@ interface Session : EventTarget<SessionEvent> {
* The player corresponding to this session.
*
* This is null if [state] is not [State.Playing].
* If you need the player instance earlier, you can use [state].player if it is one of [State.JoiningWorld] or
* If you need the player instance earlier, you can use [state].player if it is one of [State.Joining] or
* [State.FinishJoining].
*/
val player: Player?
@ -79,7 +80,7 @@ interface Session : EventTarget<SessionEvent> {
val player: Player
}
class JoiningWorld(override val player: Player) : State(), WithPlayer
class Joining(override val player: Player) : State(), WithPlayer
class Playing(override val player: Player) : State(), WithPlayer
class Disconnected(val reason: String?) : State()

View file

@ -9,16 +9,13 @@ import space.blokk.world.Chunk
import space.blokk.world.LocationWithRotation
import space.blokk.world.World
import java.util.*
import kotlin.coroutines.CoroutineContext
/**
* A **real** player.
*/
interface Player : EventTarget<PlayerEvent> {
// TODO: Replace scopes with contexts
/**
* Shorthand for [`session.scope`][Session.scope].
*/
val scope: CoroutineScope get() = session.scope
interface Player : EventTarget<PlayerEvent>, CoroutineScope {
override val coroutineContext: CoroutineContext get() = session.coroutineContext
/**
* The session of this player.

View file

@ -1,6 +1,8 @@
package space.blokk.recipe
sealed class Recipe {
import space.blokk.RegistryItem
sealed class Recipe: RegistryItem {
abstract val group: String
// TODO
}

View file

@ -1,7 +1,8 @@
package space.blokk.server
import kotlinx.coroutines.CoroutineScope
import space.blokk.Registry
import space.blokk.Scheduler
import space.blokk.event.EventBus
import space.blokk.event.EventTarget
import space.blokk.event.EventTargetGroup
import space.blokk.logging.Logger
@ -11,31 +12,45 @@ import space.blokk.player.Player
import space.blokk.plugin.PluginManager
import space.blokk.recipe.Recipe
import space.blokk.server.event.ServerEvent
import space.blokk.world.Biome
import space.blokk.world.BiomeRegistry
import space.blokk.world.Dimension
import java.io.File
import kotlin.coroutines.CoroutineContext
interface Server : EventTarget<ServerEvent> {
val scope: CoroutineScope
override val eventBus: EventBus<ServerEvent>
/**
* [EventTargetGroup] containing all sessions connected to the server.
* [CoroutineContext] confined to the server thread.
*
* Is cancelled when the server is shutting down.
*/
val coroutineContext: CoroutineContext
/**
* All sessions connected to the server.
*/
val sessions: EventTargetGroup<Session>
/**
* [EventTargetGroup] containing all players connected to the server.
* All players connected to the server.
*/
val players: EventTargetGroup<Player>
val pluginManager: PluginManager
val serverDirectory: File
val minimumLogLevel: Logger.Level
val dimensionRegistry: Registry<Dimension>
val recipeRegistry: Registry<Recipe>
val biomeRegistry: BiomeRegistry
val loggingOutputProvider: LoggingOutputProvider
val recipes: Set<Recipe>
val scheduler: Scheduler
val developmentMode: Boolean
val minimumLogLevel: Logger.Level
/**
* Initiates shutting down the server.
*/

View file

@ -9,8 +9,10 @@ import space.blokk.world.block.Material
// TODO: Replace lazy properties with functions called during startup
class Tag(val name: String, val type: Type, val rawValues: List<String>) {
val values: List<NamespacedID> by lazy {
val tags = TagRegistry.tagsByNameByType.getValue(type)
rawValues.flatMap {
if (it.startsWith("#")) TagRegistry.tagsByName.getValue(it.removePrefix("#")).values
if (it.startsWith("#")) tags.getValue(it.removePrefix("#")).values
else listOf(NamespacedID(it))
}
}

View file

@ -1,6 +1,11 @@
package space.blokk.tag
import space.blokk.NamespacedID
object TagRegistry {
val tags: List<Tag> = MINECRAFT_INTERNAL_TAGS.toList()
val tagsByName: Map<String, Tag> = tags.map { it.name to it }.toMap()
val tagsByType: Map<Tag.Type, List<Tag>> = tags.groupBy { it.type }
val tagsByNameByType: Map<Tag.Type, Map<String, Tag>> =
tagsByType.mapValues { (_, tags) -> tags.map { it.name to it }.toMap() }
}

View file

@ -30,3 +30,9 @@ fun Int.setBit(index: Int, value: Boolean): Int {
val mask = 1 shl index
return if (value) this or mask else this and mask.inv()
}
fun bitmask(vararg values: Boolean): Int {
var mask = 0
values.forEachIndexed { index, value -> mask = mask.setBit(index, value) }
return mask
}

View file

@ -1,12 +0,0 @@
package space.blokk.util
import kotlinx.coroutines.*
/**
* Returns a new [unconfined][Dispatchers.Unconfined] coroutine scope with a [SupervisorJob].
*
* @param name The [name][CoroutineName] of the coroutine scope.
* @param parentJob The parent of the [SupervisorJob].
*/
fun createUnconfinedSupervisorScope(name: String, parentJob: Job? = null) =
CoroutineScope(CoroutineName(name) + SupervisorJob(parentJob) + Dispatchers.Unconfined)

View file

@ -0,0 +1,9 @@
package space.blokk.util
import com.google.common.util.concurrent.ThreadFactoryBuilder
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
fun newSingleThreadDispatcher(name: String) =
Executors.newSingleThreadExecutor { r -> Thread(r, name) }.asCoroutineDispatcher()

View file

@ -0,0 +1,73 @@
package space.blokk.world
import space.blokk.NamespacedID
import space.blokk.RegistryItem
import java.awt.Color
data class Biome(
override val id: NamespacedID,
val precipitation: Precipitation,
val skyColor: Color, // TODO: Maybe replace with an own color class
val waterFogColor: Color,
val fogColor: Color,
val waterColor: Color,
val moodSound: MoodSound,
/**
* Has an effect on grass and foliage color
*/
val temperature: Float,
/**
* Has an effect on grass and foliage color
*/
val downfall: Float
): RegistryItem {
/**
* The numeric ID of this biome. Is set when this biome is registered in the biome registry.
*/
var numericID: Int? = null; internal set
enum class Precipitation {
NONE,
RAIN,
SNOW
}
data class MoodSound(
/**
* The amount of ticks after which the sound plays.
*/
val delay: Int, // TODO: Confirm this
val offset: Double, // TODO: Find out what this does
val sound: NamespacedID,
/**
* Determines the cubic range of possible positions to play the mood sound.
* The player is at the center of the cubic range, and the edge length is `2 * maxDistance + 1`.
*/
val maxDistance: Int
)
companion object {
/**
* The vanilla "plains" biome.
*
* This is automatically registered in the biome registry because otherwise the client crashes.
* It cannot be unregistered.
*/
val PLAINS = Biome(
NamespacedID("minecraft:plains"),
Precipitation.RAIN,
Color(7907327),
Color(329011),
Color(12638463),
Color(4159204),
MoodSound(
6000,
2.0,
NamespacedID("minecraft:ambient.cave"), // TODO: Create constants for vanilla sounds
8
),
0.8f,
0.4f
)
}
}

View file

@ -0,0 +1,31 @@
package space.blokk.world
import space.blokk.NamespacedID
import space.blokk.Registry
import java.lang.IllegalArgumentException
import java.util.concurrent.atomic.AtomicInteger
class BiomeRegistry: Registry<Biome>() {
private val nextID = AtomicInteger(0)
override fun register(item: Biome): Boolean {
item.numericID = nextID.getAndIncrement()
return super.register(item)
}
init {
register(Biome.PLAINS)
}
override fun unregister(id: NamespacedID): Boolean {
if (id == Biome.PLAINS.id) throw IllegalArgumentException("The plains biome cannot be removed")
val biome = internalItems.remove(id)
return if (biome == null) false
else {
biome.numericID = null
true
}
}
}

View file

@ -1,10 +1,10 @@
package space.blokk.world
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.*
import space.blokk.Blokk
import space.blokk.player.Player
import space.blokk.util.createUnconfinedSupervisorScope
import kotlin.coroutines.CoroutineContext
import kotlin.math.abs
abstract class Chunk(
val world: World,
@ -21,7 +21,7 @@ abstract class Chunk(
Key(location.x / LENGTH, location.z / LENGTH)
}
fun translateWorldToChunk(x: Int, z: Int) = Pair(x % LENGTH, z % LENGTH)
fun translateWorldToChunk(x: Int, z: Int) = Pair(abs(x - (this.x * LENGTH)), abs(z - (this.z * LENGTH)))
fun translateChunkToWorld(x: Int, z: Int) =
Pair(this.x * LENGTH + x, this.z * LENGTH + z)
@ -33,11 +33,12 @@ abstract class Chunk(
}
/**
* The [CoroutineScope] of this chunk.
* [CoroutineContext] confined to the world thread.
*
* It is cancelled when the chunk is unloaded.
* Is cancelled when the chunk is unloaded.
*/
val scope: CoroutineScope by lazy { createUnconfinedSupervisorScope(identifier, world.scope.coroutineContext[Job]) }
val coroutineContext: CoroutineContext =
world.coroutineContext + CoroutineName(identifier) + SupervisorJob(world.coroutineContext[Job])
/**
* A list of all players who have locked this chunk.

View file

@ -0,0 +1,11 @@
package space.blokk.world
import space.blokk.NamespacedID
import space.blokk.RegistryItem
data class Dimension(
override val id: NamespacedID,
val compassesSpinRandomly: Boolean,
val ambientLight: Float,
val hasSkylight: Boolean
): RegistryItem

View file

@ -1,26 +1,31 @@
package space.blokk.world
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.*
import space.blokk.CoordinatePartOrder
import space.blokk.entity.Entity
import space.blokk.event.EventTargetGroup
import space.blokk.util.createUnconfinedSupervisorScope
import space.blokk.util.newSingleThreadDispatcher
import java.util.*
import kotlin.coroutines.CoroutineContext
/**
* A Minecraft world, sometimes also called level.
* A Minecraft world.
*/
abstract class World(val uuid: UUID) {
/**
* The [CoroutineScope] of this world.
*
* It is cancelled when the world is unloaded.
*/
val scope: CoroutineScope by lazy { createUnconfinedSupervisorScope("World($uuid)") }
private val identifier = "World($uuid)"
private val threadExecutor = newSingleThreadDispatcher(identifier)
abstract val dimension: WorldDimension
/**
* [CoroutineContext] confined to the world thread.
*
* Is cancelled when the world is unloaded.
*/
val coroutineContext: CoroutineContext =
CoroutineName(identifier) + SupervisorJob() + threadExecutor
abstract val dimension: Dimension
abstract val loadedChunks: Map<Chunk.Key, Chunk>
abstract val type: WorldType
abstract val isFlat: Boolean
/**
* This can be any value.
@ -53,30 +58,29 @@ abstract class World(val uuid: UUID) {
*
* @param order The nesting order of the arrays.
*/
fun getVoxelsInCube(
firstCorner: VoxelLocation,
secondCorner: VoxelLocation,
order: CoordinatePartOrder = CoordinatePartOrder.DEFAULT
): Array<Array<Array<Voxel>>> {
@OptIn(ExperimentalStdlibApi::class)
fun getVoxelsInCube(firstCorner: VoxelLocation, secondCorner: VoxelLocation): List<Voxel> {
val start = firstCorner.withLowestValues(secondCorner)
val end = firstCorner.withHighestValues(secondCorner)
return ((start[order.first])..(end[order.first])).map { x ->
((start[order.second])..(end[order.second])).map { y ->
((start[order.third])..(end[order.third])).map { z ->
getVoxel(VoxelLocation(x, y.toUByte(), z))
}.toTypedArray()
}.toTypedArray()
}.toTypedArray()
return buildList {
for(x in start.x..end.x) {
for(y in start.y..end.y) {
for(z in start.z..end.z) {
add(getVoxel(VoxelLocation(x, y.toUByte(), z)))
}
}
}
}
}
/**
* Returns all voxels in a sphere with the specified [center] and [radius].
*/
fun getBlocksInSphere(
fun getVoxelsInSphere(
center: VoxelLocation,
radius: Int
): Array<Voxel> {
): List<Voxel> {
// https://www.reddit.com/r/VoxelGameDev/comments/2cttnt/how_to_create_a_sphere_out_of_voxels/
TODO()
}
@ -85,4 +89,10 @@ abstract class World(val uuid: UUID) {
* Spawns an [entity][Entity] in this world.
*/
abstract fun spawnEntity(entity: Entity)
fun unload() {
coroutineContext.cancel()
// TODO: Unload chunks
threadExecutor.close()
}
}

View file

@ -1,7 +0,0 @@
package space.blokk.world
enum class WorldDimension(val id: Int) {
OVERWORLD(0),
NETHER(-1),
END(1)
}

View file

@ -1,10 +0,0 @@
package space.blokk.world
enum class WorldType {
DEFAULT,
FLAT,
LARGE_BIOMES,
AMPLIFIED,
CUSTOMIZED,
BUFFET
}

View file

@ -1,5 +1,7 @@
package space.blokk.world.block
import kotlin.annotation.Target
/**
* @suppress
*/

View file

@ -37,7 +37,7 @@ class BlockCodec<T : Block> internal constructor(
class IllegalAttributeTargetType : Exception("The type of the target property is not allowed for attributes")
fun getStateID(block: T): Int {
if (states.isEmpty()) return id
if (states.isEmpty()) return firstStateID
val values = mutableMapOf<KProperty<*>, Any>()

View file

@ -2,14 +2,6 @@ package space.blokk
import space.blokk.server.Server
@Suppress("DEPRECATION")
fun mockBlokkProvider() {
try {
Blokk.provider
} catch (e: UninitializedPropertyAccessException) {
Blokk.provider = object : BlokkProvider {
override val server: Server get() = throw UnsupportedOperationException("Not allowed in tests")
override val developmentMode: Boolean = false
}
}
fun mockServerInstance() {
// TODO
}

View file

@ -6,7 +6,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import space.blokk.logging.Logger
import space.blokk.mockBlokkProvider
import space.blokk.mockServerInstance
import strikt.api.expectThat
import strikt.api.expectThrows
import strikt.assertions.*
@ -18,10 +18,10 @@ private class SecondEvent : TestEvent()
class EventBusTest {
init {
mockBlokkProvider()
mockServerInstance()
}
private val eventBus = EventBus(TestEvent::class, Logger("logger"), CoroutineScope(Dispatchers.Default))
private val eventBus = EventBus(TestEvent::class, Logger("logger"), Dispatchers.Default)
@Test
fun `calls the handler exactly 1 time when the event is emitted 1 time`() {

View file

@ -19,19 +19,56 @@ class NBT internal constructor(private val map: MutableMap<String, Any>) {
*/
inline fun <reified T> get(name: String) = data[name] as T
/**
* Sets the value for [name].
*
* The official NBT specification does not define any constraints for the [name],
* but this implementation forbids line breaks
*/
fun set(name: String, value: Any) {
if (name.contains('\n')) throw IllegalArgumentException("name may not contain line breaks")
NBTType.getFor(value) ?: throw IllegalArgumentException("The type of value cannot be represented as NBT")
fun set(name: String, value: Byte) {
map[name] = value
}
fun set(name: String, value: Short) {
map[name] = value
}
fun set(name: String, value: Int) {
map[name] = value
}
fun set(name: String, value: Long) {
map[name] = value
}
fun set(name: String, value: Float) {
map[name] = value
}
fun set(name: String, value: Double) {
map[name] = value
}
fun set(name: String, value: ByteArray) {
map[name] = value
}
fun set(name: String, value: String) {
map[name] = value
}
fun set(name: String, value: Collection<*>) {
map[name] = value
}
fun set(name: String, value: NBT) {
map[name] = value
}
fun set(name: String, value: LongArray) {
map[name] = value
}
fun setAsByte(name: String, value: Boolean) {
map[name] = if(value) 1.toByte() else 0.toByte()
}
inline fun set(name: String, nested: NBTBuilderContext.() -> Unit) = set(name, buildNBT(nested))
fun write(destination: DataOutputStream, name: String? = null) {
if (name == null) NBTCompound.writeValue(destination, this)
else NBTCompound.writeNamedTag(destination, name, this)
@ -46,3 +83,5 @@ class NBT internal constructor(private val map: MutableMap<String, Any>) {
}
}
}
inline fun buildNBT(init: NBTBuilderContext.() -> Unit) = NBT().also { NBTBuilderContext(it).init() }

View file

@ -0,0 +1,17 @@
package space.blokk.nbt
class NBTBuilderContext(val nbt: NBT) {
inline operator fun String.invoke(init: NBTBuilderContext.() -> Unit) = nbt.set(this, buildNBT(init))
infix fun String.set(value: Byte) = nbt.set(this, value)
infix fun String.set(value: Short) = nbt.set(this, value)
infix fun String.set(value: Int) = nbt.set(this, value)
infix fun String.set(value: Long) = nbt.set(this, value)
infix fun String.set(value: Float) = nbt.set(this, value)
infix fun String.set(value: Double) = nbt.set(this, value)
infix fun String.set(value: ByteArray) = nbt.set(this, value)
infix fun String.set(value: String) = nbt.set(this, value)
infix fun String.set(value: Collection<*>) = nbt.set(this, value)
infix fun String.set(value: NBT) = nbt.set(this, value)
infix fun String.set(value: LongArray) = nbt.set(this, value)
infix fun String.setAsByte(value: Boolean) = nbt.setAsByte(this, value)
}

View file

@ -4,13 +4,13 @@ import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.IOException
object NBTList : NBTType<List<*>>(List::class, 9) {
override fun toSNBT(value: List<*>, pretty: Boolean): String {
object NBTList : NBTType<Collection<*>>(Collection::class, 9) {
override fun toSNBT(value: Collection<*>, pretty: Boolean): String {
val multiline = pretty && value.size > 1
val separator = "," + (if (multiline) "\n" else if (pretty) " " else "")
val items = value
.joinToString(separator) { getFor(it!!)!!.toSNBT(it, pretty) }
.joinToString(separator) { of(it!!)!!.toSNBT(it, pretty) }
.run {
if (multiline) prependIndent(" ")
else this
@ -25,21 +25,21 @@ object NBTList : NBTType<List<*>>(List::class, 9) {
if (length == 0) return emptyList()
val type = byID(typeID)
val type = byID[typeID]
?: throw IOException("The NBT data contains an unknown type ID: $typeID")
return (1..length).map { type.readValue(source) }
}
override fun writeValue(destination: DataOutputStream, value: List<*>) {
override fun writeValue(destination: DataOutputStream, value: Collection<*>) {
@Suppress("UNCHECKED_CAST")
value as List<Any>
value as Collection<Any>
if (value.isEmpty()) {
destination.writeByte(END_TAG_ID.toInt())
destination.writeByte(0)
} else {
val type = getFor(value[0])
val type = of(value.first())
?: throw IllegalArgumentException("The type of value cannot be represented as NBT")
destination.writeByte(type.id.toInt())

View file

@ -26,7 +26,7 @@ abstract class NBTType<T : Any> internal constructor(val typeClass: KClass<*>, v
companion object {
const val END_TAG_ID: Byte = 0
val ALL by lazy {
val all by lazy {
setOf<NBTType<*>>(
NBTByte,
NBTShort,
@ -42,12 +42,10 @@ abstract class NBTType<T : Any> internal constructor(val typeClass: KClass<*>, v
)
}
private val BY_ID: Map<Byte, NBTType<*>> by lazy { ALL.map { it.id to it }.toMap() }
fun byID(id: Byte): NBTType<*>? = BY_ID[id]
val byID: Map<Byte, NBTType<*>> by lazy { all.map { it.id to it }.toMap() }
@Suppress("UNCHECKED_CAST")
fun <T : Any> getFor(value: T): NBTType<T>? = ALL.find { it.typeClass.isInstance(value) } as NBTType<T>?
fun <T : Any> of(value: T): NBTType<T>? = all.find { it.typeClass.isInstance(value) } as NBTType<T>?
}
}
@ -119,7 +117,7 @@ object NBTCompound : NBTType<NBT>(NBT::class, 10) {
val separator = ",${stringIf(pretty, "\n")}"
val entries = value.data.entries.joinToString(separator) { (name, v) ->
"\"$name\"" + ":" + stringIf(pretty) + getFor(v)!!.toSNBT(v, pretty)
"\"$name\"" + ":" + stringIf(pretty) + of(v)!!.toSNBT(v, pretty)
}.run {
if (pretty) prependIndent(" ")
else this
@ -135,7 +133,7 @@ object NBTCompound : NBTType<NBT>(NBT::class, 10) {
val typeID = source.readByte()
if (typeID == END_TAG_ID) break
val type = byID(typeID) ?: throw IOException("The NBT data contains an unknown type ID: $typeID")
val type = byID[typeID] ?: throw IOException("The NBT data contains an unknown type ID: $typeID")
val name = source.readUTF()
data[name] = type.readValue(source)
}
@ -150,7 +148,7 @@ object NBTCompound : NBTType<NBT>(NBT::class, 10) {
val typeID = source.readByte()
if (typeID == END_TAG_ID) throw IOException("TAG_END is not allowed on the top level")
val type = byID(typeID) ?: throw IOException("The NBT data contains an unknown type ID: $typeID")
val type = byID[typeID] ?: throw IOException("The NBT data contains an unknown type ID: $typeID")
val name = source.readUTF()
data[name] = type.readValue(source)
}
@ -160,7 +158,7 @@ object NBTCompound : NBTType<NBT>(NBT::class, 10) {
override fun writeValue(destination: DataOutputStream, value: NBT) {
value.data.forEach { (name, v) ->
getFor(v)!!.writeNamedTag(destination, name, v)
of(v)!!.writeNamedTag(destination, name, v)
}
destination.writeByte(END_TAG_ID.toInt())

View file

@ -1,6 +1,9 @@
package space.blokk.net
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufOutputStream
import space.blokk.nbt.NBT
import java.io.DataOutputStream
import java.io.IOException
import java.util.*
import kotlin.experimental.and
@ -122,4 +125,16 @@ object MinecraftProtocolDataTypes {
writeLong(value.leastSignificantBits)
return this
}
/**
* Writes a compound NBT tag with an empty name.
*/
fun ByteBuf.writeNBT(value: NBT): ByteBuf {
DataOutputStream(ByteBufOutputStream(this)).use {
value.write(it, "")
it.flush()
}
return this
}
}

View file

@ -2,11 +2,12 @@ package space.blokk.net.packet.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftProtocolDataTypes.writeString
import space.blokk.net.MinecraftProtocolDataTypes.writeUUID
import space.blokk.net.packet.OutgoingPacketCodec
object LoginSuccessPacketCodec : OutgoingPacketCodec<LoginSuccessPacket>(0x02, LoginSuccessPacket::class) {
override fun LoginSuccessPacket.encode(dst: ByteBuf) {
dst.writeString(uuid.toString())
dst.writeUUID(uuid)
dst.writeString(username)
}
}

View file

@ -6,6 +6,7 @@ import io.netty.buffer.Unpooled
import space.blokk.Blokk
import space.blokk.nbt.NBT
import space.blokk.nbt.NBTCompound
import space.blokk.net.MinecraftProtocolDataTypes.writeNBT
import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
import space.blokk.net.packet.OutgoingPacketCodec
import space.blokk.util.generateHeightmap
@ -21,7 +22,7 @@ import java.util.zip.Deflater
import kotlin.math.ceil
import kotlin.math.log
object ChunkDataPacketCodec : OutgoingPacketCodec<ChunkDataPacket>(0x22, ChunkDataPacket::class) {
object ChunkDataPacketCodec : OutgoingPacketCodec<ChunkDataPacket>(0x20, ChunkDataPacket::class) {
private val airBlocks: Set<Material<*>> = setOf(Air, CaveAir)
private val nonSolidBlocks = Material.all.filter { it.collisionShape.isEmpty() }
@ -54,13 +55,13 @@ object ChunkDataPacketCodec : OutgoingPacketCodec<ChunkDataPacket>(0x22, ChunkDa
)).toCompactLongArray(9)
)
DataOutputStream(ByteBufOutputStream(dst)).use {
heightmaps.write(it, "")
it.flush()
}
dst.writeNBT(heightmaps)
// Biomes
data.biomes.forEach { dst.writeInt(it.numericID) }
dst.writeVarInt(data.biomes.size)
data.biomes.forEach {
dst.writeVarInt(it.numericID ?: throw IllegalStateException("A biome in the chunk was not registered"))
}
// Blocks
val dataBuf = Unpooled.buffer() // TODO: Set an initial capacity

View file

@ -7,14 +7,16 @@ import space.blokk.util.checkBit
import space.blokk.util.setBit
// TODO: Implement partial updates
object ChunkLightDataPacketCodec : OutgoingPacketCodec<ChunkLightDataPacket>(0x25, ChunkLightDataPacket::class) {
private val OUTSIDE_SECTIONS_MASK = 0b100000000000000001
object ChunkLightDataPacketCodec : OutgoingPacketCodec<ChunkLightDataPacket>(0x23, ChunkLightDataPacket::class) {
private const val OUTSIDE_SECTIONS_MASK = 0b100000000000000001
@OptIn(ExperimentalUnsignedTypes::class)
override fun ChunkLightDataPacket.encode(dst: ByteBuf) {
dst.writeVarInt(key.x)
dst.writeVarInt(key.z)
dst.writeBoolean(false) // Trust edges (?)
val emptySkyLightMask = data.skyLightValues
.foldIndexed(0) { i, acc, current -> acc.setBit(i, current?.sum() == 0.toUInt()) }

View file

@ -4,7 +4,7 @@ import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
import space.blokk.net.packet.OutgoingPacketCodec
object DeclareRecipesPacketCodec : OutgoingPacketCodec<DeclareRecipesPacket>(0x5B, DeclareRecipesPacket::class) {
object DeclareRecipesPacketCodec : OutgoingPacketCodec<DeclareRecipesPacket>(0x5A, DeclareRecipesPacket::class) {
override fun DeclareRecipesPacket.encode(dst: ByteBuf) {
dst.writeVarInt(recipes.size)
for (recipe in recipes) {

View file

@ -4,7 +4,7 @@ import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftProtocolDataTypes.writeString
import space.blokk.net.packet.OutgoingPacketCodec
object DisconnectPacketCodec : OutgoingPacketCodec<DisconnectPacket>(0x1B, DisconnectPacket::class) {
object DisconnectPacketCodec : OutgoingPacketCodec<DisconnectPacket>(0x19, DisconnectPacket::class) {
override fun DisconnectPacket.encode(dst: ByteBuf) {
dst.writeString(reason.toJson())
}

View file

@ -1,32 +1,108 @@
package space.blokk.net.packet.play
import io.netty.buffer.ByteBuf
import space.blokk.Blokk
import space.blokk.nbt.buildNBT
import space.blokk.net.MinecraftProtocolDataTypes.writeNBT
import space.blokk.net.MinecraftProtocolDataTypes.writeString
import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
import space.blokk.net.packet.OutgoingPacketCodec
import space.blokk.world.WorldType
import space.blokk.util.sha256
import space.blokk.util.toByteArray
import space.blokk.util.toLong
import kotlin.random.Random
object JoinGamePacketCodec : OutgoingPacketCodec<JoinGamePacket>(0x26, JoinGamePacket::class) {
object JoinGamePacketCodec : OutgoingPacketCodec<JoinGamePacket>(0x24, JoinGamePacket::class) {
override fun JoinGamePacket.encode(dst: ByteBuf) {
dst.writeInt(entityID)
dst.writeByte(gameMode.numericID.let { if (hardcore) it and 0x8 else it })
dst.writeInt(worldDimension.id)
dst.writeLong(worldSeedHash)
dst.writeByte(0x00) // max players; not used anymore
dst.writeBoolean(hardcore)
dst.writeByte(gameMode.numericID)
dst.writeByte(-1) // "Previous game mode"
dst.writeVarInt(1)
dst.writeString("blokk:world")
dst.writeString(
when (worldType) {
WorldType.DEFAULT -> "default"
WorldType.FLAT -> "flat"
WorldType.LARGE_BIOMES -> "largeBiomes"
WorldType.AMPLIFIED -> "amplified"
WorldType.CUSTOMIZED -> "customized"
WorldType.BUFFET -> "buffet"
val dimensionsByID = Blokk.dimensionRegistry.items.values.mapIndexed { index, dimension ->
dimension.id to buildNBT {
"natural" setAsByte !dimension.compassesSpinRandomly
"ambient_light" set dimension.ambientLight
"has_skylight" setAsByte dimension.hasSkylight
// Not known what this does
"effects" set "minecraft:overworld"
// These values do not actually change something client-sided
"ultrawarm" setAsByte false
"has_ceiling" setAsByte false
"has_raids" setAsByte true
"logical_height" set 255
"coordinate_scale" set 1.toFloat()
"bed_works" setAsByte true
"fixed_light" setAsByte false
"infiniburn" set ""
"respawn_anchor_works" setAsByte false
"piglin_safe" setAsByte false
}
)
}.toMap()
// TODO: Cache this
val dimensions = buildNBT {
"minecraft:dimension_type" {
"type" set "minecraft:dimension_type"
"value" set dimensionsByID.entries.mapIndexed { index, (id, dimension) ->
buildNBT {
"name" set id.value
"id" set index
"element" set dimension
}
}
}
"minecraft:worldgen/biome" {
"type" set "minecraft:worldgen/biome"
"value" set Blokk.biomeRegistry.items.values.mapIndexed { index, biome ->
buildNBT {
"name" set biome.id.value
"id" set index
"element" {
"precipitation" set biome.precipitation.name.toLowerCase()
"temperature" set biome.temperature
"downfall" set biome.downfall
"effects" {
"sky_color" set biome.skyColor.rgb
"water_fog_color" set biome.waterFogColor.rgb
"fog_color" set biome.fogColor.rgb
"water_color" set biome.waterColor.rgb
"mood_sound" {
"tick_delay" set biome.moodSound.delay
"offset" set biome.moodSound.offset
"sound" set biome.moodSound.sound.value
"block_search_extend" set biome.moodSound.maxDistance
}
}
// These values do not actually change something client-sided
"depth" set 0f
"scale" set 0f
"category" set "plains"
}
}
}
}
}
dst.writeNBT(dimensions)
dst.writeNBT(dimensionsByID.getValue(world.dimension.id))
dst.writeString("blokk:world")
dst.writeLong(sha256((world.seed ?: 0).toByteArray()).sliceArray(0..7).toLong())
dst.writeVarInt(0) // max players; not used anymore
dst.writeVarInt(maxViewDistance)
dst.writeBoolean(reducedDebugInfo)
dst.writeBoolean(respawnScreenEnabled)
dst.writeBoolean(false) // Is debug world
dst.writeBoolean(world.isFlat)
}
}

View file

@ -5,7 +5,7 @@ import space.blokk.net.MinecraftProtocolDataTypes.writeString
import space.blokk.net.packet.OutgoingPacketCodec
object OutgoingPluginMessagePacketCodec :
OutgoingPacketCodec<OutgoingPluginMessagePacket>(0x19, OutgoingPluginMessagePacket::class) {
OutgoingPacketCodec<OutgoingPluginMessagePacket>(0x17, OutgoingPluginMessagePacket::class) {
override fun OutgoingPluginMessagePacket.encode(dst: ByteBuf) {
dst.writeString(channel)
dst.writeBytes(data)

View file

@ -2,16 +2,12 @@ package space.blokk.net.packet.play
import io.netty.buffer.ByteBuf
import space.blokk.net.packet.OutgoingPacketCodec
import space.blokk.util.bitmask
import space.blokk.util.setBit
object PlayerAbilitiesPacketCodec : OutgoingPacketCodec<PlayerAbilitiesPacket>(0x32, PlayerAbilitiesPacket::class) {
object PlayerAbilitiesPacketCodec : OutgoingPacketCodec<PlayerAbilitiesPacket>(0x30, PlayerAbilitiesPacket::class) {
override fun PlayerAbilitiesPacket.encode(dst: ByteBuf) {
var flags = 0
if (invulnerable) flags = flags and 0x01
if (flying) flags = flags and 0x02
if (canFly) flags = flags and 0x04
if (instantlyBreakBlocks) flags = flags and 0x08
dst.writeByte(flags)
dst.writeByte(bitmask(invulnerable, flying, canFly, instantlyBreakBlocks))
dst.writeFloat(flyingSpeed)
dst.writeFloat(fieldOfView)
}

View file

@ -8,7 +8,7 @@ import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
import space.blokk.net.packet.OutgoingPacketCodec
import space.blokk.player.GameMode
object PlayerInfoPacketCodec : OutgoingPacketCodec<PlayerInfoPacket>(0x34, PlayerInfoPacket::class) {
object PlayerInfoPacketCodec : OutgoingPacketCodec<PlayerInfoPacket>(0x32, PlayerInfoPacket::class) {
override fun PlayerInfoPacket.encode(dst: ByteBuf) {
val encoder = when (action) {
is PlayerInfoPacket.Action.AddPlayer -> ActionEncoder.AddPlayer

View file

@ -3,27 +3,18 @@ package space.blokk.net.packet.play
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
import space.blokk.net.packet.OutgoingPacketCodec
import space.blokk.util.bitmask
import space.blokk.util.setBit
object PlayerPositionAndLookPacketCodec :
OutgoingPacketCodec<PlayerPositionAndLookPacket>(0x36, PlayerPositionAndLookPacket::class) {
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)
dst.writeByte(
0b00000000.toByte()
.setBit(0, relativeX)
.setBit(1, relativeY)
.setBit(2, relativeZ)
.setBit(3, relativeYaw)
.setBit(4, relativePitch)
.toInt()
)
dst.writeByte(bitmask(relativeX, relativeY, relativeZ, relativeYaw, relativePitch))
dst.writeVarInt(0) // Teleport ID, I am not sure why this is needed
}
}

View file

@ -4,7 +4,7 @@ import io.netty.buffer.ByteBuf
import space.blokk.Difficulty
import space.blokk.net.packet.OutgoingPacketCodec
object ServerDifficultyPacketCodec : OutgoingPacketCodec<ServerDifficultyPacket>(0x0E, ServerDifficultyPacket::class) {
object ServerDifficultyPacketCodec : OutgoingPacketCodec<ServerDifficultyPacket>(0x0D, ServerDifficultyPacket::class) {
override fun ServerDifficultyPacket.encode(dst: ByteBuf) {
dst.writeByte(
when (difficultySettings.difficulty) {

View file

@ -4,8 +4,8 @@ import io.netty.buffer.ByteBuf
import space.blokk.net.packet.OutgoingPacketCodec
object SetSelectedHotbarSlotPacketCodec :
OutgoingPacketCodec<SetSelectedHotbarSlotPacket>(0x40, SetSelectedHotbarSlotPacket::class) {
OutgoingPacketCodec<SetSelectedHotbarSlotPacket>(0x3F, SetSelectedHotbarSlotPacket::class) {
override fun SetSelectedHotbarSlotPacket.encode(dst: ByteBuf) {
dst.writeByte(index.toInt())
dst.writeByte(index)
}
}

View file

@ -9,7 +9,7 @@ import kotlin.time.Duration
import kotlin.time.toJavaDuration
object TagsPacketCodec :
OutgoingPacketCodec<TagsPacket>(0x5C, TagsPacket::class, CacheOptions(3, Duration.INFINITE.toJavaDuration())) {
OutgoingPacketCodec<TagsPacket>(0x5B, TagsPacket::class, CacheOptions(3, Duration.INFINITE.toJavaDuration())) {
private val ORDER = listOf(
Tag.Type.BLOCKS,
Tag.Type.ITEMS,

View file

@ -5,7 +5,7 @@ import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
import space.blokk.net.packet.OutgoingPacketCodec
object UpdateViewPositionPacketCodec :
OutgoingPacketCodec<UpdateViewPositionPacket>(0x41, UpdateViewPositionPacket::class) {
OutgoingPacketCodec<UpdateViewPositionPacket>(0x40, UpdateViewPositionPacket::class) {
override fun UpdateViewPositionPacket.encode(dst: ByteBuf) {
dst.writeVarInt(chunkKey.x)
dst.writeVarInt(chunkKey.z)

View file

@ -1,70 +1,39 @@
@file:Suppress("DuplicatedCode")
package space.blokk.util
/**
* Taken from Minestom (https://git.io/JIFbN)
* Original license: Apache License 2.0
*
* Changes: Translated to Kotlin
*/
fun IntArray.toCompactLongArray(bitsPerEntry: Int): LongArray {
val itemsPerLong = Long.SIZE_BITS / bitsPerEntry
val array = LongArray(size / itemsPerLong)
private val MAGIC = intArrayOf(
-1, -1, 0, Int.MIN_VALUE, 0, 0, 1431655765, 1431655765, 0, Int.MIN_VALUE,
0, 1, 858993459, 858993459, 0, 715827882, 715827882, 0, 613566756, 613566756,
0, Int.MIN_VALUE, 0, 2, 477218588, 477218588, 0, 429496729, 429496729, 0,
390451572, 390451572, 0, 357913941, 357913941, 0, 330382099, 330382099, 0, 306783378,
306783378, 0, 286331153, 286331153, 0, Int.MIN_VALUE, 0, 3, 252645135, 252645135,
0, 238609294, 238609294, 0, 226050910, 226050910, 0, 214748364, 214748364, 0,
204522252, 204522252, 0, 195225786, 195225786, 0, 186737708, 186737708, 0, 178956970,
178956970, 0, 171798691, 171798691, 0, 165191049, 165191049, 0, 159072862, 159072862,
0, 153391689, 153391689, 0, 148102320, 148102320, 0, 143165576, 143165576, 0,
138547332, 138547332, 0, Int.MIN_VALUE, 0, 4, 130150524, 130150524, 0, 126322567,
126322567, 0, 122713351, 122713351, 0, 119304647, 119304647, 0, 116080197, 116080197,
0, 113025455, 113025455, 0, 110127366, 110127366, 0, 107374182, 107374182, 0,
104755299, 104755299, 0, 102261126, 102261126, 0, 99882960, 99882960, 0, 97612893,
97612893, 0, 95443717, 95443717, 0, 93368854, 93368854, 0, 91382282, 91382282,
0, 89478485, 89478485, 0, 87652393, 87652393, 0, 85899345, 85899345, 0,
84215045, 84215045, 0, 82595524, 82595524, 0, 81037118, 81037118, 0, 79536431,
79536431, 0, 78090314, 78090314, 0, 76695844, 76695844, 0, 75350303, 75350303,
0, 74051160, 74051160, 0, 72796055, 72796055, 0, 71582788, 71582788, 0,
70409299, 70409299, 0, 69273666, 69273666, 0, 68174084, 68174084, 0, Int.MIN_VALUE,
0, 5
)
fun IntArray.toCompactLongArray(bitsPerEntry: Int): LongArray =
toCompactLongArray(indices, this::get, size, bitsPerEntry)
fun ByteArray.toCompactLongArray(bitsPerEntry: Int): LongArray =
toCompactLongArray(indices, this::get, size, bitsPerEntry)
private fun toCompactLongArray(
indices: IntRange,
getValue: (index: Int) -> Any,
size: Int,
bitsPerEntry: Int
): LongArray {
val maxEntryValue = (1L shl bitsPerEntry) - 1
val valuesPerLong = (64 / bitsPerEntry)
val magicIndex = 3 * (valuesPerLong - 1)
val divideMul = Integer.toUnsignedLong(MAGIC[magicIndex])
val divideAdd = Integer.toUnsignedLong(MAGIC[magicIndex + 1])
val divideShift = MAGIC[magicIndex + 2]
val longArraySize: Int = (size + valuesPerLong - 1) / valuesPerLong
val data = LongArray(longArraySize)
for (i in indices) {
val value = when (val v = getValue(i)) {
is Int -> v.toLong()
is Byte -> v.toLong()
else -> error("value must be a byte or an int")
var totalIndex = 0
for(longIndex in array.indices) {
var long: Long = 0
for (index in 0 until itemsPerLong) {
long = (get(totalIndex).toLong() shl (index * bitsPerEntry)) or long
totalIndex++
}
val cellIndex = (i * divideMul + divideAdd shr 32 shr divideShift).toInt()
val bitIndex: Int = (i - cellIndex * valuesPerLong) * bitsPerEntry
data[cellIndex] =
data[cellIndex] and (maxEntryValue shl bitIndex).inv() or (value and maxEntryValue) shl bitIndex
array[longIndex] = long
}
return data
return array
}
fun ByteArray.toCompactLongArray(bitsPerEntry: Int): LongArray {
val itemsPerLong = Long.SIZE_BITS / bitsPerEntry
val array = LongArray(size / itemsPerLong)
var totalIndex = 0
for(longIndex in array.indices) {
var long: Long = 0
for (index in 0 until itemsPerLong) {
long = (get(totalIndex).toLong() shl (index * bitsPerEntry)) or long
totalIndex++
}
array[longIndex] = long
}
return array
}

View file

@ -2,12 +2,11 @@ package space.blokk.util
import java.nio.ByteBuffer
@Suppress("UsePropertyAccessSyntax")
fun Long.toByteArray(): ByteArray = ByteBuffer.allocate(Long.SIZE_BYTES).also {
it.putLong(this)
}.array()
fun ByteArray.toLong() = ByteBuffer.allocate(Long.SIZE_BYTES).also {
it.put(this)
it.flip()
}.getLong()
fun Long.toByteArray() = ByteBuffer.allocate(Long.SIZE_BYTES).also {
it.putLong(this)
}.array()
}.long

View file

@ -6,4 +6,4 @@ import space.blokk.recipe.Recipe
/**
* Sent by the server while the player is joining.
*/
data class DeclareRecipesPacket(val recipes: Set<Recipe>) : OutgoingPacket()
data class DeclareRecipesPacket(val recipes: Collection<Recipe>) : OutgoingPacket()

View file

@ -2,17 +2,14 @@ package space.blokk.net.packet.play
import space.blokk.net.packet.OutgoingPacket
import space.blokk.player.GameMode
import space.blokk.world.WorldDimension
import space.blokk.world.WorldType
import space.blokk.world.World
/**
* Sent by the server after the client logged in.
*
* @param entityID ID of the player entity.
* @param gameMode Game mode of the player.
* @param worldDimension Dimension of the world the player joins.
* @param worldSeedHash First 8 bytes of the SHA-256 hash of the world's seed.
* @param worldType Type of the world the player joins.
* @param world The world in which the player spawns.
* @param maxViewDistance Maximum view distance allowed by the server.
* @param reducedDebugInfo Whether the debug screen shows only reduced info.
* @param respawnScreenEnabled Whether the respawn screen is shown when the player dies.
@ -21,9 +18,7 @@ data class JoinGamePacket(
val entityID: Int,
val gameMode: GameMode,
val hardcore: Boolean,
val worldDimension: WorldDimension,
val worldSeedHash: Long,
val worldType: WorldType,
val world: World,
val maxViewDistance: Int,
val reducedDebugInfo: Boolean,
val respawnScreenEnabled: Boolean

View file

@ -3,8 +3,7 @@ package space.blokk
import com.sksamuel.hoplite.ConfigFilePropertySource
import com.sksamuel.hoplite.ConfigLoader
import com.sksamuel.hoplite.ConfigSource
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.*
import space.blokk.config.BlokkConfig
import space.blokk.event.EventBus
import space.blokk.event.EventTargetGroup
@ -17,25 +16,19 @@ import space.blokk.recipe.Recipe
import space.blokk.server.Server
import space.blokk.server.event.ServerEvent
import space.blokk.util.EncryptionUtils
import space.blokk.util.createUnconfinedSupervisorScope
import space.blokk.world.Biome
import space.blokk.world.BiomeRegistry
import space.blokk.world.Dimension
import java.io.File
import java.security.KeyPair
import java.util.concurrent.CopyOnWriteArraySet
import java.util.concurrent.Executors
import kotlin.coroutines.CoroutineContext
import kotlin.system.exitProcess
class BlokkServer internal constructor() : Server {
val logger = Logger("Server")
private val socketServer = BlokkSocketServer(this)
override val scope = createUnconfinedSupervisorScope("Server")
override val eventBus = EventBus(ServerEvent::class, logger, scope)
override val sessions by socketServer::sessions
override val players = EventTargetGroup.Mutable<Player>(true)
override val pluginManager = BlokkPluginManager(this)
override val loggingOutputProvider = BlokkLoggingOutputProvider
override val scheduler = BlokkScheduler()
override val recipes: Set<Recipe> = CopyOnWriteArraySet()
val keyPair: KeyPair =
try {
EncryptionUtils.generateKeyPair()
@ -45,12 +38,28 @@ class BlokkServer internal constructor() : Server {
val x509EncodedPublicKey: ByteArray = EncryptionUtils.generateX509Key(keyPair.public).encoded
override val serverDirectory: File = run {
var dir = File(BlokkServer::class.java.protectionDomain.codeSource.location.toURI())
if (VERSION == "development") dir = dir.resolve("../../../../../serverData").normalize().also { it.mkdirs() }
dir
override val coroutineContext: CoroutineContext =
CoroutineName("Server") + Executors.newSingleThreadExecutor().asCoroutineDispatcher() + SupervisorJob()
override val eventBus = EventBus(ServerEvent::class, logger, coroutineContext)
override val sessions by socketServer::sessions
override val players = EventTargetGroup.Mutable<Player>(true)
override val pluginManager = BlokkPluginManager(this)
override val serverDirectory: File = File(BlokkServer::class.java.protectionDomain.codeSource.location.toURI()).let {
it.mkdirs()
if (VERSION == "development") it.resolve("../../../../../serverData").normalize()
else it
}
override val recipeRegistry = Registry<Recipe>()
override val dimensionRegistry = Registry<Dimension>()
override val biomeRegistry = BiomeRegistry()
override val loggingOutputProvider = BlokkLoggingOutputProvider
override val scheduler = BlokkScheduler()
val config = ConfigLoader.Builder()
.addPropertySource(
ConfigFilePropertySource(
@ -66,13 +75,14 @@ class BlokkServer internal constructor() : Server {
.build().loadConfigOrThrow<BlokkConfig>()
override val minimumLogLevel = config.minLogLevel
override val developmentMode: Boolean = config.developmentMode
init {
@Suppress("DEPRECATION")
Blokk.provider = object : BlokkProvider {
override val server = this@BlokkServer
override val developmentMode: Boolean = config.developmentMode
}
val clazz = Class.forName("space.blokk.BlokkKt")
val field = clazz.getDeclaredField("serverInstance")
field.isAccessible = true
field.set(null, this)
field.isAccessible = false
}
private fun failInitialization(t: Throwable): Nothing {
@ -94,8 +104,6 @@ class BlokkServer internal constructor() : Server {
}
override fun shutdown() {
scope.cancel("Shutdown")
runBlocking {
scheduler.shutdown()
}

View file

@ -7,7 +7,7 @@ import space.blokk.Blokk
class LogbackAppender : AppenderBase<ILoggingEvent>() {
override fun append(event: ILoggingEvent) {
Blokk.server.loggingOutputProvider.log(
Blokk.loggingOutputProvider.log(
true,
event.loggerName,
when (event.level) {

View file

@ -2,8 +2,7 @@ package space.blokk.net
import io.netty.buffer.ByteBuf
import io.netty.channel.Channel
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import space.blokk.BlokkServer
import space.blokk.chat.ChatColor
import space.blokk.chat.ChatComponent
@ -23,10 +22,10 @@ import space.blokk.net.packet.play.PlayProtocol
import space.blokk.net.packet.status.StatusProtocol
import space.blokk.server.event.SessionInitializedEvent
import space.blokk.util.awaitSuspending
import space.blokk.util.createUnconfinedSupervisorScope
import java.net.InetAddress
import java.net.InetSocketAddress
import javax.crypto.SecretKey
import kotlin.coroutines.CoroutineContext
class BlokkSession(private val channel: Channel, val server: BlokkServer) : Session {
override val address: InetAddress = (channel.remoteAddress() as InetSocketAddress).address
@ -47,14 +46,16 @@ class BlokkSession(private val channel: Channel, val server: BlokkServer) : Sess
is State.LoginSucceeded,
is State.WaitingForClientSettings,
is State.JoiningWorld,
is State.Joining,
is State.Playing -> PlayProtocol
is State.Disconnected -> null
}
override val scope = createUnconfinedSupervisorScope(identifier)
override val eventBus = EventBus(SessionEvent::class, logger, scope)
override val coroutineContext: CoroutineContext =
CoroutineName(identifier) + Dispatchers.Unconfined + SupervisorJob()
override val eventBus = EventBus(SessionEvent::class, logger, coroutineContext)
override var brand: String? = null; internal set
private var disconnectReason: String? = null
@ -70,7 +71,7 @@ class BlokkSession(private val channel: Channel, val server: BlokkServer) : Sess
})
}
fun onConnect() = scope.launch {
fun onConnect() = launch {
logger trace "Connected"
if (server.eventBus.emitAsync(SessionInitializedEvent(this@BlokkSession)).isCancelled) channel.close()
else server.sessions.add(this@BlokkSession)
@ -86,7 +87,7 @@ class BlokkSession(private val channel: Channel, val server: BlokkServer) : Sess
else message
}
scope.cancel(DisconnectedCancellationException(reason))
cancel(DisconnectedCancellationException(reason))
state = State.Disconnected(reason)
server.sessions.remove(this)

View file

@ -2,8 +2,6 @@ package space.blokk.net
import io.netty.buffer.Unpooled
import space.blokk.BlokkServer
import space.blokk.Difficulty
import space.blokk.DifficultySettings
import space.blokk.chat.TextComponent
import space.blokk.event.ifCancelled
import space.blokk.net.MinecraftProtocolDataTypes.writeString
@ -12,7 +10,6 @@ import space.blokk.net.event.SessionAfterLoginEvent
import space.blokk.net.packet.login.*
import space.blokk.net.packet.play.*
import space.blokk.player.BlokkPlayer
import space.blokk.player.GameMode
import space.blokk.tag.TagRegistry
import space.blokk.util.*
import space.blokk.world.Chunk
@ -20,7 +17,6 @@ import java.security.MessageDigest
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import kotlin.random.Random
class LoginAndJoinProcedure(val session: BlokkSession) {
private val tagsPacket by lazy { TagsPacket(TagRegistry.tags) }
@ -90,17 +86,13 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
session.disconnect(loggableReason = "No spawn location set")
}
else -> {
val seedAsBytes = (initialWorldAndLocation.world.seed ?: Random.nextLong()).toByteArray()
// TODO: Spawn the player entity
session.send(
JoinGamePacket(
0,
event.gameMode,
event.hardcoreHearts,
initialWorldAndLocation.world.dimension,
sha256(seedAsBytes).sliceArray(0..7).toLong(),
initialWorldAndLocation.world.type,
initialWorldAndLocation.world,
event.maxViewDistance,
event.reducedDebugInfo,
event.respawnScreenEnabled
@ -111,22 +103,22 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
"minecraft:brand",
Unpooled.buffer().writeString("Blokk ${BlokkServer.VERSION_WITH_V}")
)
//
// // As this is only visual, there is no way of changing it aside from intercepting the packet.
// session.send(ServerDifficultyPacket(DifficultySettings(Difficulty.NORMAL, false)))
// As this is only visual, there is no way of changing this aside from intercepting the packet.
session.send(ServerDifficultyPacket(DifficultySettings(Difficulty.NORMAL, false)))
session.send(
PlayerAbilitiesPacket(
event.invulnerable,
event.flying,
event.canFly,
// TODO: Consider allowing to modify this value
event.gameMode == GameMode.CREATIVE,
// TODO: Find out how this relates to the entity property named `generic.flying_speed`
event.flyingSpeed,
event.fieldOfView
)
)
// session.send(
// PlayerAbilitiesPacket(
// event.invulnerable,
// event.flying,
// event.canFly,
// // TODO: Consider allowing to modify this value
// event.gameMode == GameMode.CREATIVE,
// // TODO: Find out how this relates to the entity property named `generic.flying_speed`
// event.flyingSpeed,
// event.fieldOfView
// )
// )
session.state = Session.State.WaitingForClientSettings(
state.username,
@ -171,11 +163,11 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
state.selectedHotbarSlot
)
session.state = Session.State.JoiningWorld(player)
session.state = Session.State.Joining(player)
session.send(SetSelectedHotbarSlotPacket(state.selectedHotbarSlot))
session.send(DeclareRecipesPacket(session.server.recipes))
session.send(DeclareRecipesPacket(session.server.recipeRegistry.items.values))
session.send(tagsPacket)
// DeclareCommands
@ -183,7 +175,7 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
session.send(PlayerPositionAndLookPacket(state.initialWorldAndLocation.location))
session.send(PlayerInfoPacket(PlayerInfoPacket.Action.AddPlayer(session.server.players.map { it ->
session.send(PlayerInfoPacket(PlayerInfoPacket.Action.AddPlayer((session.server.players + player).map {
it.uuid to PlayerInfoPacket.Action.AddPlayer.Data(
it.name,
it.gameMode,
@ -193,13 +185,21 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
)
}.toMap())))
session.send(
PlayerInfoPacket(
PlayerInfoPacket.Action.UpdateLatency(session.server.players.map { it.uuid to it.rtt }.toMap())
)
)
session.send(UpdateViewPositionPacket(Chunk.Key.from(player.location.asVoxelLocation())))
player.sendChunksAndLight()
// TODO: Send WorldBorder packet
// WorldBorder
// TODO: Send SpawnPosition packet
// TODO: Send PlayerPositionAndLook packet (again)
session.send(PlayerPositionAndLookPacket(state.initialWorldAndLocation.location))
// TODO: Wait for ClientStatus(action=0) packet
}
}

View file

@ -9,6 +9,6 @@ class PacketMessageHandler(private val session: BlokkSession) :
SimpleChannelInboundHandler<IncomingPacketMessage<*>>() {
override fun channelRead0(ctx: ChannelHandlerContext, msg: IncomingPacketMessage<*>) {
session.logger.trace { "Packet received: ${msg.packet}" }
session.scope.launch { session.eventBus.emit(PacketReceivedEvent(session, msg.packet)) }
session.launch { session.eventBus.emit(PacketReceivedEvent(session, msg.packet)) }
}
}

View file

@ -10,7 +10,6 @@ import space.blokk.net.Session
import space.blokk.net.packet.play.ChunkDataPacket
import space.blokk.net.packet.play.ChunkLightDataPacket
import space.blokk.player.event.PlayerEvent
import space.blokk.util.createUnconfinedSupervisorScope
import space.blokk.world.*
import java.util.*
import kotlin.math.abs
@ -43,8 +42,7 @@ class BlokkPlayer(
private val identifier = "BlokkPlayer($name)"
private val logger = Logger(identifier)
override val scope by lazy { createUnconfinedSupervisorScope(identifier, session.scope.coroutineContext[Job]) }
override val eventBus = EventBus(PlayerEvent::class, logger, scope)
override val eventBus = EventBus(PlayerEvent::class, logger, coroutineContext)
override var playerListName: TextComponent? = null
override var rtt = -1

View file

@ -72,7 +72,7 @@ class BlokkPluginManager(private val server: BlokkServer) : PluginManager {
logger info "Successfully loaded ${plugins.size}/${files.size} " +
"${pluralize("plugin", plugins.size)}: " +
plugins.joinToString { "${it.meta.name}@${it.meta.version}" }
plugins.joinToString { "${it.meta.name} (${it.meta.version})" }
}
sealed class LoadError {

View file

@ -38,11 +38,11 @@ class DataDownloader(private val dir: File) {
companion object {
val FILES = mapOf(
"minecraft_server.jar" to "https://launcher.mojang.com/v1/objects/bb2b6b1aefcd70dfd1892149ac3a215f6c636b07/server.jar",
"blocks.json" to PRISMARINE_BASE_URL + "1.15.2/blocks.json",
"biomes.json" to PRISMARINE_BASE_URL + "1.15.2/biomes.json",
"entities.json" to PRISMARINE_BASE_URL + "1.15.2/entities.json",
"blockCollisionShapes.json" to PRISMARINE_BASE_URL + "1.15.2/blockCollisionShapes.json"
"minecraft_server.jar" to "https://launcher.mojang.com/v1/objects/35139deedbd5182953cf1caa23835da59ca3d7cd/server.jar",
"blocks.json" to PRISMARINE_BASE_URL + "1.16.2/blocks.json",
"biomes.json" to PRISMARINE_BASE_URL + "1.16.2/biomes.json",
"entities.json" to PRISMARINE_BASE_URL + "1.16.2/entities.json",
"blockCollisionShapes.json" to PRISMARINE_BASE_URL + "1.16.1/blockCollisionShapes.json"
)
}
}

View file

@ -48,7 +48,6 @@ class MinecraftDataSourcesPlugin : Plugin<Project> {
BlocksAndMaterialGenerator(workingDir, outputDir, sourcesDir).generate()
TagsGenerator(workingDir, outputDir).generate()
BiomesGenerator(workingDir, outputDir).generate()
EntitiesGenerator(workingDir, outputDir, sourcesDir).generate()
FluidIDMapGenerator(workingDir, outputDir, registries).generate()
ItemTypeEnumGenerator(workingDir, outputDir, registries).generate()

View file

@ -1,51 +0,0 @@
package space.blokk.mdsp.generator
import com.google.common.base.CaseFormat
import com.jsoniter.JsonIterator
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.TypeSpec
import space.blokk.mdsp.util.ConstructorPropertiesHelper
import java.io.File
class BiomesGenerator(private val workingDir: File, private val outputDir: File) {
fun generate() {
val cph = ConstructorPropertiesHelper()
val enumSpec = TypeSpec.enumBuilder("Biome")
.primaryConstructor(
FunSpec.constructorBuilder()
.addParameter(cph.create("numericID", Int::class))
.addParameter(cph.create("id", NAMESPACED_ID_TYPE))
.build()
)
.addProperties(cph.getProperties())
val dataJson = workingDir.resolve("biomes.json").readText()
val biomes = JsonIterator.deserialize(dataJson).asList()
for (biome in biomes) {
val numericID = biome.get("id").toInt()
val id = biome.get("name").toString()
val name = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, id)
enumSpec.addEnumConstant(
name, TypeSpec.anonymousClassBuilder()
.addSuperclassConstructorParameter("%L", numericID)
.addSuperclassConstructorParameter(
"%T(%S)",
NAMESPACED_ID_TYPE,
"minecraft:$id"
)
.build()
)
}
FileSpec.builder("space.blokk.world", "Biome")
.addType(enumSpec.build())
.build()
.writeTo(outputDir)
}
}

View file

@ -1,18 +1,37 @@
package space.blokk.testplugin
import space.blokk.Blokk
import space.blokk.NamespacedID
import space.blokk.event.EventHandler
import space.blokk.event.Listener
import space.blokk.net.event.SessionAfterLoginEvent
import space.blokk.plugin.Plugin
import space.blokk.world.*
import space.blokk.testplugin.anvil.AnvilWorld
import space.blokk.world.block.Dirt
import space.blokk.world.block.GrassBlock
import space.blokk.world.block.GreenWool
import space.blokk.world.block.SnowBlock
class TestPlugin: Plugin("Test", "1.0.0") {
override fun onEnable() {
val world = AnvilWorld(WorldDimension.OVERWORLD, WorldType.FLAT)
world.getVoxel(VoxelLocation(0, 2, 0)).block = GrassBlock()
val dimension = Dimension(
NamespacedID("test:test"),
true,
0.0f,
true
)
Blokk.dimensionRegistry.register(dimension)
val world = AnvilWorld(dimension, true)
world.getVoxelsInCube(VoxelLocation(100, 0, 100), VoxelLocation(-100, 0, -100)).forEach {
it.block = GreenWool()
}
world.getVoxel(VoxelLocation(0, 10, 0)).block = Dirt()
world.getVoxel(VoxelLocation(10, 4, 22)).block = GrassBlock()
world.getVoxel(VoxelLocation(40, 3, -32)).block = SnowBlock()
Blokk.sessions.registerListener(object : Listener {
@EventHandler

View file

@ -13,7 +13,7 @@ class AnvilChunk(world: AnvilWorld, key: Key) : Chunk(world, key) {
override fun getData(player: Player?): ChunkData {
return ChunkData(
sections.map { section -> if (section.blocks.all { it == Air }) null else section.blocks }.toTypedArray(),
Array(ChunkData.BIOME_AREAS_IN_CHUNK) { Biome.THE_VOID }
Array(ChunkData.BIOME_AREAS_IN_CHUNK) { Biome.PLAINS }
)
}

View file

@ -20,7 +20,7 @@ class AnvilVoxel(
chunkZ = z
}
private val sectionIndex = location.y.toInt() / Chunk.LENGTH
private val sectionIndex = chunkY.toInt() / Chunk.SECTION_HEIGHT
private val index = chunkY.toInt() * Chunk.AREA + chunkZ * Chunk.LENGTH + chunkX
override var block: Block

View file

@ -7,13 +7,12 @@ import space.blokk.entity.Entity
import space.blokk.event.EventTargetGroup
import space.blokk.world.Chunk
import space.blokk.world.World
import space.blokk.world.WorldDimension
import space.blokk.world.WorldType
import space.blokk.world.Dimension
import java.util.*
class AnvilWorld(
override val dimension: WorldDimension,
override val type: WorldType
override val dimension: Dimension,
override val isFlat: Boolean
) : World(UUID.randomUUID()) {
override val loadedChunks: Map<Chunk.Key, Chunk> get() = chunks.asMap().filter { (_, chunk) -> chunk.loaded }
override val entities: EventTargetGroup.Mutable<Entity> = EventTargetGroup.Mutable(false)