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=kotlinx.coroutines.ExperimentalCoroutinesApi", // TODO: Remove and use @OptIn instead
"-Xopt-in=kotlin.contracts.ExperimentalContracts", // 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.ExperimentalUnsignedTypes", // TODO: Remove and use @OptIn instead
"-Xopt-in=kotlin.RequiresOptIn",
"-progressive" "-progressive"
) )
} }

View file

@ -5,36 +5,6 @@ import space.blokk.net.Session
import space.blokk.player.Player import space.blokk.player.Player
import space.blokk.server.Server import space.blokk.server.Server
interface BlokkProvider { private lateinit var serverInstance: Server
val server: Server
/** object Blokk : Server by serverInstance
* 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
}

View file

@ -1,6 +1,17 @@
package space.blokk 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) { inline class NamespacedID(val value: String) {
val namespace get() = value.substringBefore(":") val namespace get() = value.substringBefore(":")
val id get() = value.substringAfter(":") 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.logging.Logger
import space.blokk.plugin.Plugin import space.blokk.plugin.Plugin
import space.blokk.util.pluralizeWithCount import space.blokk.util.pluralizeWithCount
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KFunction import kotlin.reflect.KFunction
import kotlin.reflect.full.* import kotlin.reflect.full.*
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
// TODO: Only create one event bus for everything and add helper method instead
class EventBus<EventT : Event>( class EventBus<EventT : Event>(
private val eventClass: KClass<EventT>, private val eventClass: KClass<EventT>,
private val logger: Logger, 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. * 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) { class Logger(val name: String, private val printThreadName: Boolean = true) {
fun log(level: Level, message: String, throwable: Throwable? = null) = 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) { fun error(msg: String, t: Throwable) {
if (Level.ERROR.isEnabled) { 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 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 space.blokk.world.WorldAndLocationWithRotation
import java.net.InetAddress import java.net.InetAddress
import java.util.* import java.util.*
import kotlin.coroutines.CoroutineContext
interface Session : EventTarget<SessionEvent> { interface Session : EventTarget<SessionEvent>, CoroutineScope {
/** /**
* The IP address of this session * The IP address of this session
*/ */
val address: InetAddress 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. * 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. * The player corresponding to this session.
* *
* This is null if [state] is not [State.Playing]. * 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]. * [State.FinishJoining].
*/ */
val player: Player? val player: Player?
@ -79,7 +80,7 @@ interface Session : EventTarget<SessionEvent> {
val player: Player 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 Playing(override val player: Player) : State(), WithPlayer
class Disconnected(val reason: String?) : State() 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.LocationWithRotation
import space.blokk.world.World import space.blokk.world.World
import java.util.* import java.util.*
import kotlin.coroutines.CoroutineContext
/** /**
* A **real** player. * A **real** player.
*/ */
interface Player : EventTarget<PlayerEvent> { interface Player : EventTarget<PlayerEvent>, CoroutineScope {
// TODO: Replace scopes with contexts override val coroutineContext: CoroutineContext get() = session.coroutineContext
/**
* Shorthand for [`session.scope`][Session.scope].
*/
val scope: CoroutineScope get() = session.scope
/** /**
* The session of this player. * The session of this player.

View file

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

View file

@ -1,7 +1,8 @@
package space.blokk.server package space.blokk.server
import kotlinx.coroutines.CoroutineScope import space.blokk.Registry
import space.blokk.Scheduler import space.blokk.Scheduler
import space.blokk.event.EventBus
import space.blokk.event.EventTarget import space.blokk.event.EventTarget
import space.blokk.event.EventTargetGroup import space.blokk.event.EventTargetGroup
import space.blokk.logging.Logger import space.blokk.logging.Logger
@ -11,31 +12,45 @@ import space.blokk.player.Player
import space.blokk.plugin.PluginManager import space.blokk.plugin.PluginManager
import space.blokk.recipe.Recipe import space.blokk.recipe.Recipe
import space.blokk.server.event.ServerEvent 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 java.io.File
import kotlin.coroutines.CoroutineContext
interface Server : EventTarget<ServerEvent> { 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> val sessions: EventTargetGroup<Session>
/** /**
* [EventTargetGroup] containing all players connected to the server. * All players connected to the server.
*/ */
val players: EventTargetGroup<Player> val players: EventTargetGroup<Player>
val pluginManager: PluginManager val pluginManager: PluginManager
val serverDirectory: File val serverDirectory: File
val minimumLogLevel: Logger.Level val dimensionRegistry: Registry<Dimension>
val recipeRegistry: Registry<Recipe>
val biomeRegistry: BiomeRegistry
val loggingOutputProvider: LoggingOutputProvider val loggingOutputProvider: LoggingOutputProvider
val recipes: Set<Recipe>
val scheduler: Scheduler val scheduler: Scheduler
val developmentMode: Boolean
val minimumLogLevel: Logger.Level
/** /**
* Initiates shutting down the server. * 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 // TODO: Replace lazy properties with functions called during startup
class Tag(val name: String, val type: Type, val rawValues: List<String>) { class Tag(val name: String, val type: Type, val rawValues: List<String>) {
val values: List<NamespacedID> by lazy { val values: List<NamespacedID> by lazy {
val tags = TagRegistry.tagsByNameByType.getValue(type)
rawValues.flatMap { rawValues.flatMap {
if (it.startsWith("#")) TagRegistry.tagsByName.getValue(it.removePrefix("#")).values if (it.startsWith("#")) tags.getValue(it.removePrefix("#")).values
else listOf(NamespacedID(it)) else listOf(NamespacedID(it))
} }
} }

View file

@ -1,6 +1,11 @@
package space.blokk.tag package space.blokk.tag
import space.blokk.NamespacedID
object TagRegistry { object TagRegistry {
val tags: List<Tag> = MINECRAFT_INTERNAL_TAGS.toList() 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 val mask = 1 shl index
return if (value) this or mask else this and mask.inv() 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 package space.blokk.world
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Job
import space.blokk.Blokk import space.blokk.Blokk
import space.blokk.player.Player import space.blokk.player.Player
import space.blokk.util.createUnconfinedSupervisorScope import kotlin.coroutines.CoroutineContext
import kotlin.math.abs
abstract class Chunk( abstract class Chunk(
val world: World, val world: World,
@ -21,7 +21,7 @@ abstract class Chunk(
Key(location.x / LENGTH, location.z / LENGTH) 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) = fun translateChunkToWorld(x: Int, z: Int) =
Pair(this.x * LENGTH + x, this.z * LENGTH + z) 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. * 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 package space.blokk.world
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import space.blokk.CoordinatePartOrder import space.blokk.CoordinatePartOrder
import space.blokk.entity.Entity import space.blokk.entity.Entity
import space.blokk.event.EventTargetGroup import space.blokk.event.EventTargetGroup
import space.blokk.util.createUnconfinedSupervisorScope import space.blokk.util.newSingleThreadDispatcher
import java.util.* import java.util.*
import kotlin.coroutines.CoroutineContext
/** /**
* A Minecraft world, sometimes also called level. * A Minecraft world.
*/ */
abstract class World(val uuid: UUID) { abstract class World(val uuid: UUID) {
/** private val identifier = "World($uuid)"
* The [CoroutineScope] of this world. private val threadExecutor = newSingleThreadDispatcher(identifier)
*
* It is cancelled when the world is unloaded.
*/
val scope: CoroutineScope by lazy { createUnconfinedSupervisorScope("World($uuid)") }
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 loadedChunks: Map<Chunk.Key, Chunk>
abstract val type: WorldType abstract val isFlat: Boolean
/** /**
* This can be any value. * This can be any value.
@ -53,30 +58,29 @@ abstract class World(val uuid: UUID) {
* *
* @param order The nesting order of the arrays. * @param order The nesting order of the arrays.
*/ */
fun getVoxelsInCube( @OptIn(ExperimentalStdlibApi::class)
firstCorner: VoxelLocation, fun getVoxelsInCube(firstCorner: VoxelLocation, secondCorner: VoxelLocation): List<Voxel> {
secondCorner: VoxelLocation,
order: CoordinatePartOrder = CoordinatePartOrder.DEFAULT
): Array<Array<Array<Voxel>>> {
val start = firstCorner.withLowestValues(secondCorner) val start = firstCorner.withLowestValues(secondCorner)
val end = firstCorner.withHighestValues(secondCorner) val end = firstCorner.withHighestValues(secondCorner)
return ((start[order.first])..(end[order.first])).map { x -> return buildList {
((start[order.second])..(end[order.second])).map { y -> for(x in start.x..end.x) {
((start[order.third])..(end[order.third])).map { z -> for(y in start.y..end.y) {
getVoxel(VoxelLocation(x, y.toUByte(), z)) for(z in start.z..end.z) {
}.toTypedArray() add(getVoxel(VoxelLocation(x, y.toUByte(), z)))
}.toTypedArray() }
}.toTypedArray() }
}
}
} }
/** /**
* Returns all voxels in a sphere with the specified [center] and [radius]. * Returns all voxels in a sphere with the specified [center] and [radius].
*/ */
fun getBlocksInSphere( fun getVoxelsInSphere(
center: VoxelLocation, center: VoxelLocation,
radius: Int radius: Int
): Array<Voxel> { ): List<Voxel> {
// https://www.reddit.com/r/VoxelGameDev/comments/2cttnt/how_to_create_a_sphere_out_of_voxels/ // https://www.reddit.com/r/VoxelGameDev/comments/2cttnt/how_to_create_a_sphere_out_of_voxels/
TODO() TODO()
} }
@ -85,4 +89,10 @@ abstract class World(val uuid: UUID) {
* Spawns an [entity][Entity] in this world. * Spawns an [entity][Entity] in this world.
*/ */
abstract fun spawnEntity(entity: Entity) 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 package space.blokk.world.block
import kotlin.annotation.Target
/** /**
* @suppress * @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") class IllegalAttributeTargetType : Exception("The type of the target property is not allowed for attributes")
fun getStateID(block: T): Int { fun getStateID(block: T): Int {
if (states.isEmpty()) return id if (states.isEmpty()) return firstStateID
val values = mutableMapOf<KProperty<*>, Any>() val values = mutableMapOf<KProperty<*>, Any>()

View file

@ -2,14 +2,6 @@ package space.blokk
import space.blokk.server.Server import space.blokk.server.Server
@Suppress("DEPRECATION") fun mockServerInstance() {
fun mockBlokkProvider() { // TODO
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
}
}
} }

View file

@ -6,7 +6,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import space.blokk.logging.Logger import space.blokk.logging.Logger
import space.blokk.mockBlokkProvider import space.blokk.mockServerInstance
import strikt.api.expectThat import strikt.api.expectThat
import strikt.api.expectThrows import strikt.api.expectThrows
import strikt.assertions.* import strikt.assertions.*
@ -18,10 +18,10 @@ private class SecondEvent : TestEvent()
class EventBusTest { class EventBusTest {
init { 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 @Test
fun `calls the handler exactly 1 time when the event is emitted 1 time`() { 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 inline fun <reified T> get(name: String) = data[name] as T
/** fun set(name: String, value: Byte) {
* 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")
map[name] = value 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) { fun write(destination: DataOutputStream, name: String? = null) {
if (name == null) NBTCompound.writeValue(destination, this) if (name == null) NBTCompound.writeValue(destination, this)
else NBTCompound.writeNamedTag(destination, name, 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.DataOutputStream
import java.io.IOException import java.io.IOException
object NBTList : NBTType<List<*>>(List::class, 9) { object NBTList : NBTType<Collection<*>>(Collection::class, 9) {
override fun toSNBT(value: List<*>, pretty: Boolean): String { override fun toSNBT(value: Collection<*>, pretty: Boolean): String {
val multiline = pretty && value.size > 1 val multiline = pretty && value.size > 1
val separator = "," + (if (multiline) "\n" else if (pretty) " " else "") val separator = "," + (if (multiline) "\n" else if (pretty) " " else "")
val items = value val items = value
.joinToString(separator) { getFor(it!!)!!.toSNBT(it, pretty) } .joinToString(separator) { of(it!!)!!.toSNBT(it, pretty) }
.run { .run {
if (multiline) prependIndent(" ") if (multiline) prependIndent(" ")
else this else this
@ -25,21 +25,21 @@ object NBTList : NBTType<List<*>>(List::class, 9) {
if (length == 0) return emptyList() if (length == 0) return emptyList()
val type = byID(typeID) val type = byID[typeID]
?: throw IOException("The NBT data contains an unknown type ID: $typeID") ?: throw IOException("The NBT data contains an unknown type ID: $typeID")
return (1..length).map { type.readValue(source) } return (1..length).map { type.readValue(source) }
} }
override fun writeValue(destination: DataOutputStream, value: List<*>) { override fun writeValue(destination: DataOutputStream, value: Collection<*>) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
value as List<Any> value as Collection<Any>
if (value.isEmpty()) { if (value.isEmpty()) {
destination.writeByte(END_TAG_ID.toInt()) destination.writeByte(END_TAG_ID.toInt())
destination.writeByte(0) destination.writeByte(0)
} else { } else {
val type = getFor(value[0]) val type = of(value.first())
?: throw IllegalArgumentException("The type of value cannot be represented as NBT") ?: throw IllegalArgumentException("The type of value cannot be represented as NBT")
destination.writeByte(type.id.toInt()) destination.writeByte(type.id.toInt())

View file

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

View file

@ -1,6 +1,9 @@
package space.blokk.net package space.blokk.net
import io.netty.buffer.ByteBuf 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.io.IOException
import java.util.* import java.util.*
import kotlin.experimental.and import kotlin.experimental.and
@ -122,4 +125,16 @@ object MinecraftProtocolDataTypes {
writeLong(value.leastSignificantBits) writeLong(value.leastSignificantBits)
return this 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 io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftProtocolDataTypes.writeString import space.blokk.net.MinecraftProtocolDataTypes.writeString
import space.blokk.net.MinecraftProtocolDataTypes.writeUUID
import space.blokk.net.packet.OutgoingPacketCodec import space.blokk.net.packet.OutgoingPacketCodec
object LoginSuccessPacketCodec : OutgoingPacketCodec<LoginSuccessPacket>(0x02, LoginSuccessPacket::class) { object LoginSuccessPacketCodec : OutgoingPacketCodec<LoginSuccessPacket>(0x02, LoginSuccessPacket::class) {
override fun LoginSuccessPacket.encode(dst: ByteBuf) { override fun LoginSuccessPacket.encode(dst: ByteBuf) {
dst.writeString(uuid.toString()) dst.writeUUID(uuid)
dst.writeString(username) dst.writeString(username)
} }
} }

View file

@ -6,6 +6,7 @@ import io.netty.buffer.Unpooled
import space.blokk.Blokk import space.blokk.Blokk
import space.blokk.nbt.NBT import space.blokk.nbt.NBT
import space.blokk.nbt.NBTCompound import space.blokk.nbt.NBTCompound
import space.blokk.net.MinecraftProtocolDataTypes.writeNBT
import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
import space.blokk.net.packet.OutgoingPacketCodec import space.blokk.net.packet.OutgoingPacketCodec
import space.blokk.util.generateHeightmap import space.blokk.util.generateHeightmap
@ -21,7 +22,7 @@ import java.util.zip.Deflater
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.log 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 airBlocks: Set<Material<*>> = setOf(Air, CaveAir)
private val nonSolidBlocks = Material.all.filter { it.collisionShape.isEmpty() } private val nonSolidBlocks = Material.all.filter { it.collisionShape.isEmpty() }
@ -54,13 +55,13 @@ object ChunkDataPacketCodec : OutgoingPacketCodec<ChunkDataPacket>(0x22, ChunkDa
)).toCompactLongArray(9) )).toCompactLongArray(9)
) )
DataOutputStream(ByteBufOutputStream(dst)).use { dst.writeNBT(heightmaps)
heightmaps.write(it, "")
it.flush()
}
// Biomes // 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 // Blocks
val dataBuf = Unpooled.buffer() // TODO: Set an initial capacity 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 import space.blokk.util.setBit
// TODO: Implement partial updates // TODO: Implement partial updates
object ChunkLightDataPacketCodec : OutgoingPacketCodec<ChunkLightDataPacket>(0x25, ChunkLightDataPacket::class) { object ChunkLightDataPacketCodec : OutgoingPacketCodec<ChunkLightDataPacket>(0x23, ChunkLightDataPacket::class) {
private val OUTSIDE_SECTIONS_MASK = 0b100000000000000001 private const val OUTSIDE_SECTIONS_MASK = 0b100000000000000001
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
override fun ChunkLightDataPacket.encode(dst: ByteBuf) { override fun ChunkLightDataPacket.encode(dst: ByteBuf) {
dst.writeVarInt(key.x) dst.writeVarInt(key.x)
dst.writeVarInt(key.z) dst.writeVarInt(key.z)
dst.writeBoolean(false) // Trust edges (?)
val emptySkyLightMask = data.skyLightValues val emptySkyLightMask = data.skyLightValues
.foldIndexed(0) { i, acc, current -> acc.setBit(i, current?.sum() == 0.toUInt()) } .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.MinecraftProtocolDataTypes.writeVarInt
import space.blokk.net.packet.OutgoingPacketCodec 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) { override fun DeclareRecipesPacket.encode(dst: ByteBuf) {
dst.writeVarInt(recipes.size) dst.writeVarInt(recipes.size)
for (recipe in recipes) { 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.MinecraftProtocolDataTypes.writeString
import space.blokk.net.packet.OutgoingPacketCodec 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) { override fun DisconnectPacket.encode(dst: ByteBuf) {
dst.writeString(reason.toJson()) dst.writeString(reason.toJson())
} }

View file

@ -1,32 +1,108 @@
package space.blokk.net.packet.play package space.blokk.net.packet.play
import io.netty.buffer.ByteBuf 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.writeString
import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
import space.blokk.net.packet.OutgoingPacketCodec 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) { override fun JoinGamePacket.encode(dst: ByteBuf) {
dst.writeInt(entityID) dst.writeInt(entityID)
dst.writeByte(gameMode.numericID.let { if (hardcore) it and 0x8 else it }) dst.writeBoolean(hardcore)
dst.writeInt(worldDimension.id) dst.writeByte(gameMode.numericID)
dst.writeLong(worldSeedHash) dst.writeByte(-1) // "Previous game mode"
dst.writeByte(0x00) // max players; not used anymore dst.writeVarInt(1)
dst.writeString("blokk:world")
dst.writeString( val dimensionsByID = Blokk.dimensionRegistry.items.values.mapIndexed { index, dimension ->
when (worldType) { dimension.id to buildNBT {
WorldType.DEFAULT -> "default" "natural" setAsByte !dimension.compassesSpinRandomly
WorldType.FLAT -> "flat" "ambient_light" set dimension.ambientLight
WorldType.LARGE_BIOMES -> "largeBiomes" "has_skylight" setAsByte dimension.hasSkylight
WorldType.AMPLIFIED -> "amplified"
WorldType.CUSTOMIZED -> "customized" // Not known what this does
WorldType.BUFFET -> "buffet" "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.writeVarInt(maxViewDistance)
dst.writeBoolean(reducedDebugInfo) dst.writeBoolean(reducedDebugInfo)
dst.writeBoolean(respawnScreenEnabled) 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 import space.blokk.net.packet.OutgoingPacketCodec
object OutgoingPluginMessagePacketCodec : object OutgoingPluginMessagePacketCodec :
OutgoingPacketCodec<OutgoingPluginMessagePacket>(0x19, OutgoingPluginMessagePacket::class) { OutgoingPacketCodec<OutgoingPluginMessagePacket>(0x17, OutgoingPluginMessagePacket::class) {
override fun OutgoingPluginMessagePacket.encode(dst: ByteBuf) { override fun OutgoingPluginMessagePacket.encode(dst: ByteBuf) {
dst.writeString(channel) dst.writeString(channel)
dst.writeBytes(data) dst.writeBytes(data)

View file

@ -2,16 +2,12 @@ package space.blokk.net.packet.play
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import space.blokk.net.packet.OutgoingPacketCodec 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) { override fun PlayerAbilitiesPacket.encode(dst: ByteBuf) {
var flags = 0 dst.writeByte(bitmask(invulnerable, flying, canFly, instantlyBreakBlocks))
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.writeFloat(flyingSpeed) dst.writeFloat(flyingSpeed)
dst.writeFloat(fieldOfView) dst.writeFloat(fieldOfView)
} }

View file

@ -8,7 +8,7 @@ import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
import space.blokk.net.packet.OutgoingPacketCodec import space.blokk.net.packet.OutgoingPacketCodec
import space.blokk.player.GameMode 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) { override fun PlayerInfoPacket.encode(dst: ByteBuf) {
val encoder = when (action) { val encoder = when (action) {
is PlayerInfoPacket.Action.AddPlayer -> ActionEncoder.AddPlayer is PlayerInfoPacket.Action.AddPlayer -> ActionEncoder.AddPlayer

View file

@ -3,27 +3,18 @@ package space.blokk.net.packet.play
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
import space.blokk.net.packet.OutgoingPacketCodec import space.blokk.net.packet.OutgoingPacketCodec
import space.blokk.util.bitmask
import space.blokk.util.setBit import space.blokk.util.setBit
object PlayerPositionAndLookPacketCodec : object PlayerPositionAndLookPacketCodec :
OutgoingPacketCodec<PlayerPositionAndLookPacket>(0x36, PlayerPositionAndLookPacket::class) { OutgoingPacketCodec<PlayerPositionAndLookPacket>(0x34, PlayerPositionAndLookPacket::class) {
override fun PlayerPositionAndLookPacket.encode(dst: ByteBuf) { override fun PlayerPositionAndLookPacket.encode(dst: ByteBuf) {
dst.writeDouble(locationWithRotation.x) dst.writeDouble(locationWithRotation.x)
dst.writeDouble(locationWithRotation.y) dst.writeDouble(locationWithRotation.y)
dst.writeDouble(locationWithRotation.z) dst.writeDouble(locationWithRotation.z)
dst.writeFloat(locationWithRotation.yaw) dst.writeFloat(locationWithRotation.yaw)
dst.writeFloat(locationWithRotation.pitch) dst.writeFloat(locationWithRotation.pitch)
dst.writeByte(bitmask(relativeX, relativeY, relativeZ, relativeYaw, relativePitch))
dst.writeByte(
0b00000000.toByte()
.setBit(0, relativeX)
.setBit(1, relativeY)
.setBit(2, relativeZ)
.setBit(3, relativeYaw)
.setBit(4, relativePitch)
.toInt()
)
dst.writeVarInt(0) // Teleport ID, I am not sure why this is needed 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.Difficulty
import space.blokk.net.packet.OutgoingPacketCodec 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) { override fun ServerDifficultyPacket.encode(dst: ByteBuf) {
dst.writeByte( dst.writeByte(
when (difficultySettings.difficulty) { when (difficultySettings.difficulty) {

View file

@ -4,8 +4,8 @@ import io.netty.buffer.ByteBuf
import space.blokk.net.packet.OutgoingPacketCodec import space.blokk.net.packet.OutgoingPacketCodec
object SetSelectedHotbarSlotPacketCodec : object SetSelectedHotbarSlotPacketCodec :
OutgoingPacketCodec<SetSelectedHotbarSlotPacket>(0x40, SetSelectedHotbarSlotPacket::class) { OutgoingPacketCodec<SetSelectedHotbarSlotPacket>(0x3F, SetSelectedHotbarSlotPacket::class) {
override fun SetSelectedHotbarSlotPacket.encode(dst: ByteBuf) { 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 import kotlin.time.toJavaDuration
object TagsPacketCodec : 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( private val ORDER = listOf(
Tag.Type.BLOCKS, Tag.Type.BLOCKS,
Tag.Type.ITEMS, Tag.Type.ITEMS,

View file

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

View file

@ -1,70 +1,39 @@
@file:Suppress("DuplicatedCode")
package space.blokk.util package space.blokk.util
/** fun IntArray.toCompactLongArray(bitsPerEntry: Int): LongArray {
* Taken from Minestom (https://git.io/JIFbN) val itemsPerLong = Long.SIZE_BITS / bitsPerEntry
* Original license: Apache License 2.0 val array = LongArray(size / itemsPerLong)
*
* Changes: Translated to Kotlin
*/
private val MAGIC = intArrayOf( var totalIndex = 0
-1, -1, 0, Int.MIN_VALUE, 0, 0, 1431655765, 1431655765, 0, Int.MIN_VALUE, for(longIndex in array.indices) {
0, 1, 858993459, 858993459, 0, 715827882, 715827882, 0, 613566756, 613566756, var long: Long = 0
0, Int.MIN_VALUE, 0, 2, 477218588, 477218588, 0, 429496729, 429496729, 0, for (index in 0 until itemsPerLong) {
390451572, 390451572, 0, 357913941, 357913941, 0, 330382099, 330382099, 0, 306783378, long = (get(totalIndex).toLong() shl (index * bitsPerEntry)) or long
306783378, 0, 286331153, 286331153, 0, Int.MIN_VALUE, 0, 3, 252645135, 252645135, totalIndex++
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")
} }
val cellIndex = (i * divideMul + divideAdd shr 32 shr divideShift).toInt() array[longIndex] = long
val bitIndex: Int = (i - cellIndex * valuesPerLong) * bitsPerEntry
data[cellIndex] =
data[cellIndex] and (maxEntryValue shl bitIndex).inv() or (value and maxEntryValue) shl bitIndex
} }
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 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 { fun ByteArray.toLong() = ByteBuffer.allocate(Long.SIZE_BYTES).also {
it.put(this) it.put(this)
it.flip() it.flip()
}.getLong() }.long
fun Long.toByteArray() = ByteBuffer.allocate(Long.SIZE_BYTES).also {
it.putLong(this)
}.array()

View file

@ -6,4 +6,4 @@ import space.blokk.recipe.Recipe
/** /**
* Sent by the server while the player is joining. * 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.net.packet.OutgoingPacket
import space.blokk.player.GameMode import space.blokk.player.GameMode
import space.blokk.world.WorldDimension import space.blokk.world.World
import space.blokk.world.WorldType
/** /**
* Sent by the server after the client logged in. * Sent by the server after the client logged in.
* *
* @param entityID ID of the player entity. * @param entityID ID of the player entity.
* @param gameMode Game mode of the player. * @param gameMode Game mode of the player.
* @param worldDimension Dimension of the world the player joins. * @param world The world in which the player spawns.
* @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 maxViewDistance Maximum view distance allowed by the server. * @param maxViewDistance Maximum view distance allowed by the server.
* @param reducedDebugInfo Whether the debug screen shows only reduced info. * @param reducedDebugInfo Whether the debug screen shows only reduced info.
* @param respawnScreenEnabled Whether the respawn screen is shown when the player dies. * @param respawnScreenEnabled Whether the respawn screen is shown when the player dies.
@ -21,9 +18,7 @@ data class JoinGamePacket(
val entityID: Int, val entityID: Int,
val gameMode: GameMode, val gameMode: GameMode,
val hardcore: Boolean, val hardcore: Boolean,
val worldDimension: WorldDimension, val world: World,
val worldSeedHash: Long,
val worldType: WorldType,
val maxViewDistance: Int, val maxViewDistance: Int,
val reducedDebugInfo: Boolean, val reducedDebugInfo: Boolean,
val respawnScreenEnabled: Boolean val respawnScreenEnabled: Boolean

View file

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

View file

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

View file

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

View file

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

View file

@ -9,6 +9,6 @@ class PacketMessageHandler(private val session: BlokkSession) :
SimpleChannelInboundHandler<IncomingPacketMessage<*>>() { SimpleChannelInboundHandler<IncomingPacketMessage<*>>() {
override fun channelRead0(ctx: ChannelHandlerContext, msg: IncomingPacketMessage<*>) { override fun channelRead0(ctx: ChannelHandlerContext, msg: IncomingPacketMessage<*>) {
session.logger.trace { "Packet received: ${msg.packet}" } 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.ChunkDataPacket
import space.blokk.net.packet.play.ChunkLightDataPacket import space.blokk.net.packet.play.ChunkLightDataPacket
import space.blokk.player.event.PlayerEvent import space.blokk.player.event.PlayerEvent
import space.blokk.util.createUnconfinedSupervisorScope
import space.blokk.world.* import space.blokk.world.*
import java.util.* import java.util.*
import kotlin.math.abs import kotlin.math.abs
@ -43,8 +42,7 @@ class BlokkPlayer(
private val identifier = "BlokkPlayer($name)" private val identifier = "BlokkPlayer($name)"
private val logger = Logger(identifier) private val logger = Logger(identifier)
override val scope by lazy { createUnconfinedSupervisorScope(identifier, session.scope.coroutineContext[Job]) } override val eventBus = EventBus(PlayerEvent::class, logger, coroutineContext)
override val eventBus = EventBus(PlayerEvent::class, logger, scope)
override var playerListName: TextComponent? = null override var playerListName: TextComponent? = null
override var rtt = -1 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} " + logger info "Successfully loaded ${plugins.size}/${files.size} " +
"${pluralize("plugin", plugins.size)}: " + "${pluralize("plugin", plugins.size)}: " +
plugins.joinToString { "${it.meta.name}@${it.meta.version}" } plugins.joinToString { "${it.meta.name} (${it.meta.version})" }
} }
sealed class LoadError { sealed class LoadError {

View file

@ -38,11 +38,11 @@ class DataDownloader(private val dir: File) {
companion object { companion object {
val FILES = mapOf( val FILES = mapOf(
"minecraft_server.jar" to "https://launcher.mojang.com/v1/objects/bb2b6b1aefcd70dfd1892149ac3a215f6c636b07/server.jar", "minecraft_server.jar" to "https://launcher.mojang.com/v1/objects/35139deedbd5182953cf1caa23835da59ca3d7cd/server.jar",
"blocks.json" to PRISMARINE_BASE_URL + "1.15.2/blocks.json", "blocks.json" to PRISMARINE_BASE_URL + "1.16.2/blocks.json",
"biomes.json" to PRISMARINE_BASE_URL + "1.15.2/biomes.json", "biomes.json" to PRISMARINE_BASE_URL + "1.16.2/biomes.json",
"entities.json" to PRISMARINE_BASE_URL + "1.15.2/entities.json", "entities.json" to PRISMARINE_BASE_URL + "1.16.2/entities.json",
"blockCollisionShapes.json" to PRISMARINE_BASE_URL + "1.15.2/blockCollisionShapes.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() BlocksAndMaterialGenerator(workingDir, outputDir, sourcesDir).generate()
TagsGenerator(workingDir, outputDir).generate() TagsGenerator(workingDir, outputDir).generate()
BiomesGenerator(workingDir, outputDir).generate()
EntitiesGenerator(workingDir, outputDir, sourcesDir).generate() EntitiesGenerator(workingDir, outputDir, sourcesDir).generate()
FluidIDMapGenerator(workingDir, outputDir, registries).generate() FluidIDMapGenerator(workingDir, outputDir, registries).generate()
ItemTypeEnumGenerator(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 package space.blokk.testplugin
import space.blokk.Blokk import space.blokk.Blokk
import space.blokk.NamespacedID
import space.blokk.event.EventHandler import space.blokk.event.EventHandler
import space.blokk.event.Listener import space.blokk.event.Listener
import space.blokk.net.event.SessionAfterLoginEvent import space.blokk.net.event.SessionAfterLoginEvent
import space.blokk.plugin.Plugin import space.blokk.plugin.Plugin
import space.blokk.world.* import space.blokk.world.*
import space.blokk.testplugin.anvil.AnvilWorld import space.blokk.testplugin.anvil.AnvilWorld
import space.blokk.world.block.Dirt
import space.blokk.world.block.GrassBlock 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") { class TestPlugin: Plugin("Test", "1.0.0") {
override fun onEnable() { override fun onEnable() {
val world = AnvilWorld(WorldDimension.OVERWORLD, WorldType.FLAT) val dimension = Dimension(
world.getVoxel(VoxelLocation(0, 2, 0)).block = GrassBlock() 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 { Blokk.sessions.registerListener(object : Listener {
@EventHandler @EventHandler

View file

@ -13,7 +13,7 @@ class AnvilChunk(world: AnvilWorld, key: Key) : Chunk(world, key) {
override fun getData(player: Player?): ChunkData { override fun getData(player: Player?): ChunkData {
return ChunkData( return ChunkData(
sections.map { section -> if (section.blocks.all { it == Air }) null else section.blocks }.toTypedArray(), 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 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 private val index = chunkY.toInt() * Chunk.AREA + chunkZ * Chunk.LENGTH + chunkX
override var block: Block override var block: Block

View file

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