Update everything to 1.16.4
this took me about 12 hours. it's 1 am
This commit is contained in:
parent
4aef22eee7
commit
ffe2349884
64 changed files with 640 additions and 420 deletions
|
@ -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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -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_-]")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
34
blokk-api/src/main/kotlin/space/blokk/Registry.kt
Normal file
34
blokk-api/src/main/kotlin/space/blokk/Registry.kt
Normal 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
|
||||||
|
}
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
|
|
@ -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()
|
73
blokk-api/src/main/kotlin/space/blokk/world/Biome.kt
Normal file
73
blokk-api/src/main/kotlin/space/blokk/world/Biome.kt
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
31
blokk-api/src/main/kotlin/space/blokk/world/BiomeRegistry.kt
Normal file
31
blokk-api/src/main/kotlin/space/blokk/world/BiomeRegistry.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
11
blokk-api/src/main/kotlin/space/blokk/world/Dimension.kt
Normal file
11
blokk-api/src/main/kotlin/space/blokk/world/Dimension.kt
Normal 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
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
package space.blokk.world
|
|
||||||
|
|
||||||
enum class WorldDimension(val id: Int) {
|
|
||||||
OVERWORLD(0),
|
|
||||||
NETHER(-1),
|
|
||||||
END(1)
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
package space.blokk.world
|
|
||||||
|
|
||||||
enum class WorldType {
|
|
||||||
DEFAULT,
|
|
||||||
FLAT,
|
|
||||||
LARGE_BIOMES,
|
|
||||||
AMPLIFIED,
|
|
||||||
CUSTOMIZED,
|
|
||||||
BUFFET
|
|
||||||
}
|
|
|
@ -1,5 +1,7 @@
|
||||||
package space.blokk.world.block
|
package space.blokk.world.block
|
||||||
|
|
||||||
|
import kotlin.annotation.Target
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @suppress
|
* @suppress
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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>()
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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`() {
|
||||||
|
|
|
@ -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() }
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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())
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()) }
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Reference in a new issue