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=kotlin.contracts.ExperimentalContracts", // TODO: Remove and use @OptIn instead
|
||||
"-Xopt-in=kotlin.ExperimentalUnsignedTypes", // TODO: Remove and use @OptIn instead
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-progressive"
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,36 +5,6 @@ import space.blokk.net.Session
|
|||
import space.blokk.player.Player
|
||||
import space.blokk.server.Server
|
||||
|
||||
interface BlokkProvider {
|
||||
val server: Server
|
||||
private lateinit var serverInstance: Server
|
||||
|
||||
/**
|
||||
* Whether the server is in development mode.
|
||||
*
|
||||
* For example, [EventBus][space.blokk.event.EventBus]es will log all emitted events and the time
|
||||
* it took for all handlers to execute.
|
||||
*/
|
||||
val developmentMode: Boolean
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
object Blokk : BlokkProvider {
|
||||
@Deprecated("This value should only be set by the server on startup.")
|
||||
lateinit var provider: BlokkProvider
|
||||
|
||||
override val server get() = provider.server
|
||||
override val developmentMode: Boolean get() = provider.developmentMode
|
||||
|
||||
val scheduler: Scheduler get() = server.scheduler
|
||||
|
||||
/**
|
||||
* Shorthand for [Blokk.server.sessions][Server.sessions].
|
||||
*/
|
||||
val sessions: EventTargetGroup<Session> get() = server.sessions
|
||||
|
||||
/**
|
||||
* Shorthand for [Blokk.server.players][Server.players].
|
||||
*/
|
||||
val players: EventTargetGroup<Player> get() = server.players
|
||||
|
||||
}
|
||||
object Blokk : Server by serverInstance
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
package space.blokk
|
||||
|
||||
import space.blokk.world.Dimension
|
||||
|
||||
// TODO: Remove this. Inline classes are experimental and this is not really useful
|
||||
inline class NamespacedID(val value: String) {
|
||||
val namespace get() = value.substringBefore(":")
|
||||
val id get() = value.substringAfter(":")
|
||||
val valid get() = namespace.matches(NAMESPACE_REGEX) && id.matches(ID_REGEX)
|
||||
|
||||
companion object {
|
||||
val ID_REGEX = Regex("[0-9a-z._/-]")
|
||||
val NAMESPACE_REGEX = Regex("[0-9a-z_-]")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
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.plugin.Plugin
|
||||
import space.blokk.util.pluralizeWithCount
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.full.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
// TODO: Only create one event bus for everything and add helper method instead
|
||||
class EventBus<EventT : Event>(
|
||||
private val eventClass: KClass<EventT>,
|
||||
private val logger: Logger,
|
||||
private val scope: CoroutineScope = Blokk.server.scope
|
||||
coroutineContext: CoroutineContext = Blokk.coroutineContext
|
||||
) {
|
||||
private val scope = CoroutineScope(coroutineContext)
|
||||
|
||||
/**
|
||||
* All event handlers, sorted by their priority and the order in which they were registered.
|
||||
*/
|
||||
|
|
|
@ -4,7 +4,7 @@ import space.blokk.Blokk
|
|||
|
||||
class Logger(val name: String, private val printThreadName: Boolean = true) {
|
||||
fun log(level: Level, message: String, throwable: Throwable? = null) =
|
||||
Blokk.server.loggingOutputProvider.log(printThreadName, name, level, message, throwable)
|
||||
Blokk.loggingOutputProvider.log(printThreadName, name, level, message, throwable)
|
||||
|
||||
fun error(msg: String, t: Throwable) {
|
||||
if (Level.ERROR.isEnabled) {
|
||||
|
@ -48,6 +48,6 @@ class Logger(val name: String, private val printThreadName: Boolean = true) {
|
|||
|
||||
fun isGreaterOrEqualThan(level: Level) = ordinal >= level.ordinal
|
||||
|
||||
val isEnabled get() = isGreaterOrEqualThan(Blokk.server.minimumLogLevel)
|
||||
val isEnabled get() = isGreaterOrEqualThan(Blokk.minimumLogLevel)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,17 +12,18 @@ import space.blokk.player.Player
|
|||
import space.blokk.world.WorldAndLocationWithRotation
|
||||
import java.net.InetAddress
|
||||
import java.util.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
interface Session : EventTarget<SessionEvent> {
|
||||
interface Session : EventTarget<SessionEvent>, CoroutineScope {
|
||||
/**
|
||||
* The IP address of this session
|
||||
*/
|
||||
val address: InetAddress
|
||||
|
||||
/**
|
||||
* The coroutine scope of this session. It is cancelled when the session is disconnected.
|
||||
* Unconfined [CoroutineContext] which is cancelled when the session is disconnected.
|
||||
*/
|
||||
val scope: CoroutineScope
|
||||
override val coroutineContext: CoroutineContext
|
||||
|
||||
/**
|
||||
* The brand name the client optionally sent during the login procedure.
|
||||
|
@ -39,7 +40,7 @@ interface Session : EventTarget<SessionEvent> {
|
|||
* The player corresponding to this session.
|
||||
*
|
||||
* This is null if [state] is not [State.Playing].
|
||||
* If you need the player instance earlier, you can use [state].player if it is one of [State.JoiningWorld] or
|
||||
* If you need the player instance earlier, you can use [state].player if it is one of [State.Joining] or
|
||||
* [State.FinishJoining].
|
||||
*/
|
||||
val player: Player?
|
||||
|
@ -79,7 +80,7 @@ interface Session : EventTarget<SessionEvent> {
|
|||
val player: Player
|
||||
}
|
||||
|
||||
class JoiningWorld(override val player: Player) : State(), WithPlayer
|
||||
class Joining(override val player: Player) : State(), WithPlayer
|
||||
class Playing(override val player: Player) : State(), WithPlayer
|
||||
|
||||
class Disconnected(val reason: String?) : State()
|
||||
|
|
|
@ -9,16 +9,13 @@ import space.blokk.world.Chunk
|
|||
import space.blokk.world.LocationWithRotation
|
||||
import space.blokk.world.World
|
||||
import java.util.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* A **real** player.
|
||||
*/
|
||||
interface Player : EventTarget<PlayerEvent> {
|
||||
// TODO: Replace scopes with contexts
|
||||
/**
|
||||
* Shorthand for [`session.scope`][Session.scope].
|
||||
*/
|
||||
val scope: CoroutineScope get() = session.scope
|
||||
interface Player : EventTarget<PlayerEvent>, CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext get() = session.coroutineContext
|
||||
|
||||
/**
|
||||
* The session of this player.
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package space.blokk.recipe
|
||||
|
||||
sealed class Recipe {
|
||||
import space.blokk.RegistryItem
|
||||
|
||||
sealed class Recipe: RegistryItem {
|
||||
abstract val group: String
|
||||
// TODO
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package space.blokk.server
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import space.blokk.Registry
|
||||
import space.blokk.Scheduler
|
||||
import space.blokk.event.EventBus
|
||||
import space.blokk.event.EventTarget
|
||||
import space.blokk.event.EventTargetGroup
|
||||
import space.blokk.logging.Logger
|
||||
|
@ -11,31 +12,45 @@ import space.blokk.player.Player
|
|||
import space.blokk.plugin.PluginManager
|
||||
import space.blokk.recipe.Recipe
|
||||
import space.blokk.server.event.ServerEvent
|
||||
import space.blokk.world.Biome
|
||||
import space.blokk.world.BiomeRegistry
|
||||
import space.blokk.world.Dimension
|
||||
import java.io.File
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
interface Server : EventTarget<ServerEvent> {
|
||||
val scope: CoroutineScope
|
||||
override val eventBus: EventBus<ServerEvent>
|
||||
|
||||
/**
|
||||
* [EventTargetGroup] containing all sessions connected to the server.
|
||||
* [CoroutineContext] confined to the server thread.
|
||||
*
|
||||
* Is cancelled when the server is shutting down.
|
||||
*/
|
||||
val coroutineContext: CoroutineContext
|
||||
|
||||
/**
|
||||
* All sessions connected to the server.
|
||||
*/
|
||||
val sessions: EventTargetGroup<Session>
|
||||
|
||||
/**
|
||||
* [EventTargetGroup] containing all players connected to the server.
|
||||
* All players connected to the server.
|
||||
*/
|
||||
val players: EventTargetGroup<Player>
|
||||
|
||||
val pluginManager: PluginManager
|
||||
val serverDirectory: File
|
||||
|
||||
val minimumLogLevel: Logger.Level
|
||||
val dimensionRegistry: Registry<Dimension>
|
||||
val recipeRegistry: Registry<Recipe>
|
||||
val biomeRegistry: BiomeRegistry
|
||||
|
||||
val loggingOutputProvider: LoggingOutputProvider
|
||||
|
||||
val recipes: Set<Recipe>
|
||||
|
||||
val scheduler: Scheduler
|
||||
|
||||
val developmentMode: Boolean
|
||||
val minimumLogLevel: Logger.Level
|
||||
|
||||
/**
|
||||
* Initiates shutting down the server.
|
||||
*/
|
||||
|
|
|
@ -9,8 +9,10 @@ import space.blokk.world.block.Material
|
|||
// TODO: Replace lazy properties with functions called during startup
|
||||
class Tag(val name: String, val type: Type, val rawValues: List<String>) {
|
||||
val values: List<NamespacedID> by lazy {
|
||||
val tags = TagRegistry.tagsByNameByType.getValue(type)
|
||||
|
||||
rawValues.flatMap {
|
||||
if (it.startsWith("#")) TagRegistry.tagsByName.getValue(it.removePrefix("#")).values
|
||||
if (it.startsWith("#")) tags.getValue(it.removePrefix("#")).values
|
||||
else listOf(NamespacedID(it))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
package space.blokk.tag
|
||||
|
||||
import space.blokk.NamespacedID
|
||||
|
||||
object TagRegistry {
|
||||
val tags: List<Tag> = MINECRAFT_INTERNAL_TAGS.toList()
|
||||
val tagsByName: Map<String, Tag> = tags.map { it.name to it }.toMap()
|
||||
val tagsByType: Map<Tag.Type, List<Tag>> = tags.groupBy { it.type }
|
||||
|
||||
val tagsByNameByType: Map<Tag.Type, Map<String, Tag>> =
|
||||
tagsByType.mapValues { (_, tags) -> tags.map { it.name to it }.toMap() }
|
||||
}
|
||||
|
|
|
@ -30,3 +30,9 @@ fun Int.setBit(index: Int, value: Boolean): Int {
|
|||
val mask = 1 shl index
|
||||
return if (value) this or mask else this and mask.inv()
|
||||
}
|
||||
|
||||
fun bitmask(vararg values: Boolean): Int {
|
||||
var mask = 0
|
||||
values.forEachIndexed { index, value -> mask = mask.setBit(index, value) }
|
||||
return mask
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.*
|
||||
import space.blokk.Blokk
|
||||
import space.blokk.player.Player
|
||||
import space.blokk.util.createUnconfinedSupervisorScope
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.math.abs
|
||||
|
||||
abstract class Chunk(
|
||||
val world: World,
|
||||
|
@ -21,7 +21,7 @@ abstract class Chunk(
|
|||
Key(location.x / LENGTH, location.z / LENGTH)
|
||||
}
|
||||
|
||||
fun translateWorldToChunk(x: Int, z: Int) = Pair(x % LENGTH, z % LENGTH)
|
||||
fun translateWorldToChunk(x: Int, z: Int) = Pair(abs(x - (this.x * LENGTH)), abs(z - (this.z * LENGTH)))
|
||||
fun translateChunkToWorld(x: Int, z: Int) =
|
||||
Pair(this.x * LENGTH + x, this.z * LENGTH + z)
|
||||
|
||||
|
@ -33,11 +33,12 @@ abstract class Chunk(
|
|||
}
|
||||
|
||||
/**
|
||||
* The [CoroutineScope] of this chunk.
|
||||
* [CoroutineContext] confined to the world thread.
|
||||
*
|
||||
* It is cancelled when the chunk is unloaded.
|
||||
* Is cancelled when the chunk is unloaded.
|
||||
*/
|
||||
val scope: CoroutineScope by lazy { createUnconfinedSupervisorScope(identifier, world.scope.coroutineContext[Job]) }
|
||||
val coroutineContext: CoroutineContext =
|
||||
world.coroutineContext + CoroutineName(identifier) + SupervisorJob(world.coroutineContext[Job])
|
||||
|
||||
/**
|
||||
* A list of all players who have locked this chunk.
|
||||
|
|
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
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.*
|
||||
import space.blokk.CoordinatePartOrder
|
||||
import space.blokk.entity.Entity
|
||||
import space.blokk.event.EventTargetGroup
|
||||
import space.blokk.util.createUnconfinedSupervisorScope
|
||||
import space.blokk.util.newSingleThreadDispatcher
|
||||
import java.util.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* A Minecraft world, sometimes also called level.
|
||||
* A Minecraft world.
|
||||
*/
|
||||
abstract class World(val uuid: UUID) {
|
||||
/**
|
||||
* The [CoroutineScope] of this world.
|
||||
*
|
||||
* It is cancelled when the world is unloaded.
|
||||
*/
|
||||
val scope: CoroutineScope by lazy { createUnconfinedSupervisorScope("World($uuid)") }
|
||||
private val identifier = "World($uuid)"
|
||||
private val threadExecutor = newSingleThreadDispatcher(identifier)
|
||||
|
||||
abstract val dimension: WorldDimension
|
||||
/**
|
||||
* [CoroutineContext] confined to the world thread.
|
||||
*
|
||||
* Is cancelled when the world is unloaded.
|
||||
*/
|
||||
val coroutineContext: CoroutineContext =
|
||||
CoroutineName(identifier) + SupervisorJob() + threadExecutor
|
||||
|
||||
abstract val dimension: Dimension
|
||||
abstract val loadedChunks: Map<Chunk.Key, Chunk>
|
||||
abstract val type: WorldType
|
||||
abstract val isFlat: Boolean
|
||||
|
||||
/**
|
||||
* This can be any value.
|
||||
|
@ -53,30 +58,29 @@ abstract class World(val uuid: UUID) {
|
|||
*
|
||||
* @param order The nesting order of the arrays.
|
||||
*/
|
||||
fun getVoxelsInCube(
|
||||
firstCorner: VoxelLocation,
|
||||
secondCorner: VoxelLocation,
|
||||
order: CoordinatePartOrder = CoordinatePartOrder.DEFAULT
|
||||
): Array<Array<Array<Voxel>>> {
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun getVoxelsInCube(firstCorner: VoxelLocation, secondCorner: VoxelLocation): List<Voxel> {
|
||||
val start = firstCorner.withLowestValues(secondCorner)
|
||||
val end = firstCorner.withHighestValues(secondCorner)
|
||||
|
||||
return ((start[order.first])..(end[order.first])).map { x ->
|
||||
((start[order.second])..(end[order.second])).map { y ->
|
||||
((start[order.third])..(end[order.third])).map { z ->
|
||||
getVoxel(VoxelLocation(x, y.toUByte(), z))
|
||||
}.toTypedArray()
|
||||
}.toTypedArray()
|
||||
}.toTypedArray()
|
||||
return buildList {
|
||||
for(x in start.x..end.x) {
|
||||
for(y in start.y..end.y) {
|
||||
for(z in start.z..end.z) {
|
||||
add(getVoxel(VoxelLocation(x, y.toUByte(), z)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all voxels in a sphere with the specified [center] and [radius].
|
||||
*/
|
||||
fun getBlocksInSphere(
|
||||
fun getVoxelsInSphere(
|
||||
center: VoxelLocation,
|
||||
radius: Int
|
||||
): Array<Voxel> {
|
||||
): List<Voxel> {
|
||||
// https://www.reddit.com/r/VoxelGameDev/comments/2cttnt/how_to_create_a_sphere_out_of_voxels/
|
||||
TODO()
|
||||
}
|
||||
|
@ -85,4 +89,10 @@ abstract class World(val uuid: UUID) {
|
|||
* Spawns an [entity][Entity] in this world.
|
||||
*/
|
||||
abstract fun spawnEntity(entity: Entity)
|
||||
|
||||
fun unload() {
|
||||
coroutineContext.cancel()
|
||||
// TODO: Unload chunks
|
||||
threadExecutor.close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
import kotlin.annotation.Target
|
||||
|
||||
/**
|
||||
* @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")
|
||||
|
||||
fun getStateID(block: T): Int {
|
||||
if (states.isEmpty()) return id
|
||||
if (states.isEmpty()) return firstStateID
|
||||
|
||||
val values = mutableMapOf<KProperty<*>, Any>()
|
||||
|
||||
|
|
|
@ -2,14 +2,6 @@ package space.blokk
|
|||
|
||||
import space.blokk.server.Server
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun mockBlokkProvider() {
|
||||
try {
|
||||
Blokk.provider
|
||||
} catch (e: UninitializedPropertyAccessException) {
|
||||
Blokk.provider = object : BlokkProvider {
|
||||
override val server: Server get() = throw UnsupportedOperationException("Not allowed in tests")
|
||||
override val developmentMode: Boolean = false
|
||||
}
|
||||
}
|
||||
fun mockServerInstance() {
|
||||
// TODO
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Test
|
||||
import space.blokk.logging.Logger
|
||||
import space.blokk.mockBlokkProvider
|
||||
import space.blokk.mockServerInstance
|
||||
import strikt.api.expectThat
|
||||
import strikt.api.expectThrows
|
||||
import strikt.assertions.*
|
||||
|
@ -18,10 +18,10 @@ private class SecondEvent : TestEvent()
|
|||
|
||||
class EventBusTest {
|
||||
init {
|
||||
mockBlokkProvider()
|
||||
mockServerInstance()
|
||||
}
|
||||
|
||||
private val eventBus = EventBus(TestEvent::class, Logger("logger"), CoroutineScope(Dispatchers.Default))
|
||||
private val eventBus = EventBus(TestEvent::class, Logger("logger"), Dispatchers.Default)
|
||||
|
||||
@Test
|
||||
fun `calls the handler exactly 1 time when the event is emitted 1 time`() {
|
||||
|
|
|
@ -19,19 +19,56 @@ class NBT internal constructor(private val map: MutableMap<String, Any>) {
|
|||
*/
|
||||
inline fun <reified T> get(name: String) = data[name] as T
|
||||
|
||||
/**
|
||||
* Sets the value for [name].
|
||||
*
|
||||
* The official NBT specification does not define any constraints for the [name],
|
||||
* but this implementation forbids line breaks
|
||||
*/
|
||||
fun set(name: String, value: Any) {
|
||||
if (name.contains('\n')) throw IllegalArgumentException("name may not contain line breaks")
|
||||
|
||||
NBTType.getFor(value) ?: throw IllegalArgumentException("The type of value cannot be represented as NBT")
|
||||
fun set(name: String, value: Byte) {
|
||||
map[name] = value
|
||||
}
|
||||
|
||||
fun set(name: String, value: Short) {
|
||||
map[name] = value
|
||||
}
|
||||
|
||||
fun set(name: String, value: Int) {
|
||||
map[name] = value
|
||||
}
|
||||
|
||||
fun set(name: String, value: Long) {
|
||||
map[name] = value
|
||||
}
|
||||
|
||||
fun set(name: String, value: Float) {
|
||||
map[name] = value
|
||||
}
|
||||
|
||||
fun set(name: String, value: Double) {
|
||||
map[name] = value
|
||||
}
|
||||
|
||||
fun set(name: String, value: ByteArray) {
|
||||
map[name] = value
|
||||
}
|
||||
|
||||
fun set(name: String, value: String) {
|
||||
map[name] = value
|
||||
}
|
||||
|
||||
fun set(name: String, value: Collection<*>) {
|
||||
map[name] = value
|
||||
}
|
||||
|
||||
fun set(name: String, value: NBT) {
|
||||
map[name] = value
|
||||
}
|
||||
|
||||
fun set(name: String, value: LongArray) {
|
||||
map[name] = value
|
||||
}
|
||||
|
||||
fun setAsByte(name: String, value: Boolean) {
|
||||
map[name] = if(value) 1.toByte() else 0.toByte()
|
||||
}
|
||||
|
||||
inline fun set(name: String, nested: NBTBuilderContext.() -> Unit) = set(name, buildNBT(nested))
|
||||
|
||||
fun write(destination: DataOutputStream, name: String? = null) {
|
||||
if (name == null) NBTCompound.writeValue(destination, this)
|
||||
else NBTCompound.writeNamedTag(destination, name, this)
|
||||
|
@ -46,3 +83,5 @@ class NBT internal constructor(private val map: MutableMap<String, Any>) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun buildNBT(init: NBTBuilderContext.() -> Unit) = NBT().also { NBTBuilderContext(it).init() }
|
||||
|
|
|
@ -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.IOException
|
||||
|
||||
object NBTList : NBTType<List<*>>(List::class, 9) {
|
||||
override fun toSNBT(value: List<*>, pretty: Boolean): String {
|
||||
object NBTList : NBTType<Collection<*>>(Collection::class, 9) {
|
||||
override fun toSNBT(value: Collection<*>, pretty: Boolean): String {
|
||||
val multiline = pretty && value.size > 1
|
||||
val separator = "," + (if (multiline) "\n" else if (pretty) " " else "")
|
||||
|
||||
val items = value
|
||||
.joinToString(separator) { getFor(it!!)!!.toSNBT(it, pretty) }
|
||||
.joinToString(separator) { of(it!!)!!.toSNBT(it, pretty) }
|
||||
.run {
|
||||
if (multiline) prependIndent(" ")
|
||||
else this
|
||||
|
@ -25,21 +25,21 @@ object NBTList : NBTType<List<*>>(List::class, 9) {
|
|||
|
||||
if (length == 0) return emptyList()
|
||||
|
||||
val type = byID(typeID)
|
||||
val type = byID[typeID]
|
||||
?: throw IOException("The NBT data contains an unknown type ID: $typeID")
|
||||
|
||||
return (1..length).map { type.readValue(source) }
|
||||
}
|
||||
|
||||
override fun writeValue(destination: DataOutputStream, value: List<*>) {
|
||||
override fun writeValue(destination: DataOutputStream, value: Collection<*>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
value as List<Any>
|
||||
value as Collection<Any>
|
||||
|
||||
if (value.isEmpty()) {
|
||||
destination.writeByte(END_TAG_ID.toInt())
|
||||
destination.writeByte(0)
|
||||
} else {
|
||||
val type = getFor(value[0])
|
||||
val type = of(value.first())
|
||||
?: throw IllegalArgumentException("The type of value cannot be represented as NBT")
|
||||
|
||||
destination.writeByte(type.id.toInt())
|
||||
|
|
|
@ -26,7 +26,7 @@ abstract class NBTType<T : Any> internal constructor(val typeClass: KClass<*>, v
|
|||
companion object {
|
||||
const val END_TAG_ID: Byte = 0
|
||||
|
||||
val ALL by lazy {
|
||||
val all by lazy {
|
||||
setOf<NBTType<*>>(
|
||||
NBTByte,
|
||||
NBTShort,
|
||||
|
@ -42,12 +42,10 @@ abstract class NBTType<T : Any> internal constructor(val typeClass: KClass<*>, v
|
|||
)
|
||||
}
|
||||
|
||||
private val BY_ID: Map<Byte, NBTType<*>> by lazy { ALL.map { it.id to it }.toMap() }
|
||||
|
||||
fun byID(id: Byte): NBTType<*>? = BY_ID[id]
|
||||
val byID: Map<Byte, NBTType<*>> by lazy { all.map { it.id to it }.toMap() }
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Any> getFor(value: T): NBTType<T>? = ALL.find { it.typeClass.isInstance(value) } as NBTType<T>?
|
||||
fun <T : Any> of(value: T): NBTType<T>? = all.find { it.typeClass.isInstance(value) } as NBTType<T>?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,7 +117,7 @@ object NBTCompound : NBTType<NBT>(NBT::class, 10) {
|
|||
val separator = ",${stringIf(pretty, "\n")}"
|
||||
|
||||
val entries = value.data.entries.joinToString(separator) { (name, v) ->
|
||||
"\"$name\"" + ":" + stringIf(pretty) + getFor(v)!!.toSNBT(v, pretty)
|
||||
"\"$name\"" + ":" + stringIf(pretty) + of(v)!!.toSNBT(v, pretty)
|
||||
}.run {
|
||||
if (pretty) prependIndent(" ")
|
||||
else this
|
||||
|
@ -135,7 +133,7 @@ object NBTCompound : NBTType<NBT>(NBT::class, 10) {
|
|||
val typeID = source.readByte()
|
||||
if (typeID == END_TAG_ID) break
|
||||
|
||||
val type = byID(typeID) ?: throw IOException("The NBT data contains an unknown type ID: $typeID")
|
||||
val type = byID[typeID] ?: throw IOException("The NBT data contains an unknown type ID: $typeID")
|
||||
val name = source.readUTF()
|
||||
data[name] = type.readValue(source)
|
||||
}
|
||||
|
@ -150,7 +148,7 @@ object NBTCompound : NBTType<NBT>(NBT::class, 10) {
|
|||
val typeID = source.readByte()
|
||||
if (typeID == END_TAG_ID) throw IOException("TAG_END is not allowed on the top level")
|
||||
|
||||
val type = byID(typeID) ?: throw IOException("The NBT data contains an unknown type ID: $typeID")
|
||||
val type = byID[typeID] ?: throw IOException("The NBT data contains an unknown type ID: $typeID")
|
||||
val name = source.readUTF()
|
||||
data[name] = type.readValue(source)
|
||||
}
|
||||
|
@ -160,7 +158,7 @@ object NBTCompound : NBTType<NBT>(NBT::class, 10) {
|
|||
|
||||
override fun writeValue(destination: DataOutputStream, value: NBT) {
|
||||
value.data.forEach { (name, v) ->
|
||||
getFor(v)!!.writeNamedTag(destination, name, v)
|
||||
of(v)!!.writeNamedTag(destination, name, v)
|
||||
}
|
||||
|
||||
destination.writeByte(END_TAG_ID.toInt())
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package space.blokk.net
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.ByteBufOutputStream
|
||||
import space.blokk.nbt.NBT
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import kotlin.experimental.and
|
||||
|
@ -122,4 +125,16 @@ object MinecraftProtocolDataTypes {
|
|||
writeLong(value.leastSignificantBits)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a compound NBT tag with an empty name.
|
||||
*/
|
||||
fun ByteBuf.writeNBT(value: NBT): ByteBuf {
|
||||
DataOutputStream(ByteBufOutputStream(this)).use {
|
||||
value.write(it, "")
|
||||
it.flush()
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,12 @@ package space.blokk.net.packet.login
|
|||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import space.blokk.net.MinecraftProtocolDataTypes.writeString
|
||||
import space.blokk.net.MinecraftProtocolDataTypes.writeUUID
|
||||
import space.blokk.net.packet.OutgoingPacketCodec
|
||||
|
||||
object LoginSuccessPacketCodec : OutgoingPacketCodec<LoginSuccessPacket>(0x02, LoginSuccessPacket::class) {
|
||||
override fun LoginSuccessPacket.encode(dst: ByteBuf) {
|
||||
dst.writeString(uuid.toString())
|
||||
dst.writeUUID(uuid)
|
||||
dst.writeString(username)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import io.netty.buffer.Unpooled
|
|||
import space.blokk.Blokk
|
||||
import space.blokk.nbt.NBT
|
||||
import space.blokk.nbt.NBTCompound
|
||||
import space.blokk.net.MinecraftProtocolDataTypes.writeNBT
|
||||
import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
|
||||
import space.blokk.net.packet.OutgoingPacketCodec
|
||||
import space.blokk.util.generateHeightmap
|
||||
|
@ -21,7 +22,7 @@ import java.util.zip.Deflater
|
|||
import kotlin.math.ceil
|
||||
import kotlin.math.log
|
||||
|
||||
object ChunkDataPacketCodec : OutgoingPacketCodec<ChunkDataPacket>(0x22, ChunkDataPacket::class) {
|
||||
object ChunkDataPacketCodec : OutgoingPacketCodec<ChunkDataPacket>(0x20, ChunkDataPacket::class) {
|
||||
private val airBlocks: Set<Material<*>> = setOf(Air, CaveAir)
|
||||
private val nonSolidBlocks = Material.all.filter { it.collisionShape.isEmpty() }
|
||||
|
||||
|
@ -54,13 +55,13 @@ object ChunkDataPacketCodec : OutgoingPacketCodec<ChunkDataPacket>(0x22, ChunkDa
|
|||
)).toCompactLongArray(9)
|
||||
)
|
||||
|
||||
DataOutputStream(ByteBufOutputStream(dst)).use {
|
||||
heightmaps.write(it, "")
|
||||
it.flush()
|
||||
}
|
||||
dst.writeNBT(heightmaps)
|
||||
|
||||
// Biomes
|
||||
data.biomes.forEach { dst.writeInt(it.numericID) }
|
||||
dst.writeVarInt(data.biomes.size)
|
||||
data.biomes.forEach {
|
||||
dst.writeVarInt(it.numericID ?: throw IllegalStateException("A biome in the chunk was not registered"))
|
||||
}
|
||||
|
||||
// Blocks
|
||||
val dataBuf = Unpooled.buffer() // TODO: Set an initial capacity
|
||||
|
|
|
@ -7,14 +7,16 @@ import space.blokk.util.checkBit
|
|||
import space.blokk.util.setBit
|
||||
|
||||
// TODO: Implement partial updates
|
||||
object ChunkLightDataPacketCodec : OutgoingPacketCodec<ChunkLightDataPacket>(0x25, ChunkLightDataPacket::class) {
|
||||
private val OUTSIDE_SECTIONS_MASK = 0b100000000000000001
|
||||
object ChunkLightDataPacketCodec : OutgoingPacketCodec<ChunkLightDataPacket>(0x23, ChunkLightDataPacket::class) {
|
||||
private const val OUTSIDE_SECTIONS_MASK = 0b100000000000000001
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
override fun ChunkLightDataPacket.encode(dst: ByteBuf) {
|
||||
dst.writeVarInt(key.x)
|
||||
dst.writeVarInt(key.z)
|
||||
|
||||
dst.writeBoolean(false) // Trust edges (?)
|
||||
|
||||
val emptySkyLightMask = data.skyLightValues
|
||||
.foldIndexed(0) { i, acc, current -> acc.setBit(i, current?.sum() == 0.toUInt()) }
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import io.netty.buffer.ByteBuf
|
|||
import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
|
||||
import space.blokk.net.packet.OutgoingPacketCodec
|
||||
|
||||
object DeclareRecipesPacketCodec : OutgoingPacketCodec<DeclareRecipesPacket>(0x5B, DeclareRecipesPacket::class) {
|
||||
object DeclareRecipesPacketCodec : OutgoingPacketCodec<DeclareRecipesPacket>(0x5A, DeclareRecipesPacket::class) {
|
||||
override fun DeclareRecipesPacket.encode(dst: ByteBuf) {
|
||||
dst.writeVarInt(recipes.size)
|
||||
for (recipe in recipes) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import io.netty.buffer.ByteBuf
|
|||
import space.blokk.net.MinecraftProtocolDataTypes.writeString
|
||||
import space.blokk.net.packet.OutgoingPacketCodec
|
||||
|
||||
object DisconnectPacketCodec : OutgoingPacketCodec<DisconnectPacket>(0x1B, DisconnectPacket::class) {
|
||||
object DisconnectPacketCodec : OutgoingPacketCodec<DisconnectPacket>(0x19, DisconnectPacket::class) {
|
||||
override fun DisconnectPacket.encode(dst: ByteBuf) {
|
||||
dst.writeString(reason.toJson())
|
||||
}
|
||||
|
|
|
@ -1,32 +1,108 @@
|
|||
package space.blokk.net.packet.play
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import space.blokk.Blokk
|
||||
import space.blokk.nbt.buildNBT
|
||||
import space.blokk.net.MinecraftProtocolDataTypes.writeNBT
|
||||
import space.blokk.net.MinecraftProtocolDataTypes.writeString
|
||||
import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
|
||||
import space.blokk.net.packet.OutgoingPacketCodec
|
||||
import space.blokk.world.WorldType
|
||||
import space.blokk.util.sha256
|
||||
import space.blokk.util.toByteArray
|
||||
import space.blokk.util.toLong
|
||||
import kotlin.random.Random
|
||||
|
||||
object JoinGamePacketCodec : OutgoingPacketCodec<JoinGamePacket>(0x26, JoinGamePacket::class) {
|
||||
object JoinGamePacketCodec : OutgoingPacketCodec<JoinGamePacket>(0x24, JoinGamePacket::class) {
|
||||
override fun JoinGamePacket.encode(dst: ByteBuf) {
|
||||
dst.writeInt(entityID)
|
||||
dst.writeByte(gameMode.numericID.let { if (hardcore) it and 0x8 else it })
|
||||
dst.writeInt(worldDimension.id)
|
||||
dst.writeLong(worldSeedHash)
|
||||
dst.writeByte(0x00) // max players; not used anymore
|
||||
dst.writeBoolean(hardcore)
|
||||
dst.writeByte(gameMode.numericID)
|
||||
dst.writeByte(-1) // "Previous game mode"
|
||||
dst.writeVarInt(1)
|
||||
dst.writeString("blokk:world")
|
||||
|
||||
dst.writeString(
|
||||
when (worldType) {
|
||||
WorldType.DEFAULT -> "default"
|
||||
WorldType.FLAT -> "flat"
|
||||
WorldType.LARGE_BIOMES -> "largeBiomes"
|
||||
WorldType.AMPLIFIED -> "amplified"
|
||||
WorldType.CUSTOMIZED -> "customized"
|
||||
WorldType.BUFFET -> "buffet"
|
||||
val dimensionsByID = Blokk.dimensionRegistry.items.values.mapIndexed { index, dimension ->
|
||||
dimension.id to buildNBT {
|
||||
"natural" setAsByte !dimension.compassesSpinRandomly
|
||||
"ambient_light" set dimension.ambientLight
|
||||
"has_skylight" setAsByte dimension.hasSkylight
|
||||
|
||||
// Not known what this does
|
||||
"effects" set "minecraft:overworld"
|
||||
|
||||
// These values do not actually change something client-sided
|
||||
"ultrawarm" setAsByte false
|
||||
"has_ceiling" setAsByte false
|
||||
"has_raids" setAsByte true
|
||||
"logical_height" set 255
|
||||
"coordinate_scale" set 1.toFloat()
|
||||
"bed_works" setAsByte true
|
||||
"fixed_light" setAsByte false
|
||||
"infiniburn" set ""
|
||||
"respawn_anchor_works" setAsByte false
|
||||
"piglin_safe" setAsByte false
|
||||
}
|
||||
)
|
||||
}.toMap()
|
||||
|
||||
// TODO: Cache this
|
||||
val dimensions = buildNBT {
|
||||
"minecraft:dimension_type" {
|
||||
"type" set "minecraft:dimension_type"
|
||||
"value" set dimensionsByID.entries.mapIndexed { index, (id, dimension) ->
|
||||
buildNBT {
|
||||
"name" set id.value
|
||||
"id" set index
|
||||
"element" set dimension
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"minecraft:worldgen/biome" {
|
||||
"type" set "minecraft:worldgen/biome"
|
||||
"value" set Blokk.biomeRegistry.items.values.mapIndexed { index, biome ->
|
||||
buildNBT {
|
||||
"name" set biome.id.value
|
||||
"id" set index
|
||||
"element" {
|
||||
"precipitation" set biome.precipitation.name.toLowerCase()
|
||||
"temperature" set biome.temperature
|
||||
"downfall" set biome.downfall
|
||||
"effects" {
|
||||
"sky_color" set biome.skyColor.rgb
|
||||
"water_fog_color" set biome.waterFogColor.rgb
|
||||
"fog_color" set biome.fogColor.rgb
|
||||
"water_color" set biome.waterColor.rgb
|
||||
"mood_sound" {
|
||||
"tick_delay" set biome.moodSound.delay
|
||||
"offset" set biome.moodSound.offset
|
||||
"sound" set biome.moodSound.sound.value
|
||||
"block_search_extend" set biome.moodSound.maxDistance
|
||||
}
|
||||
}
|
||||
|
||||
// These values do not actually change something client-sided
|
||||
"depth" set 0f
|
||||
"scale" set 0f
|
||||
"category" set "plains"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dst.writeNBT(dimensions)
|
||||
dst.writeNBT(dimensionsByID.getValue(world.dimension.id))
|
||||
|
||||
dst.writeString("blokk:world")
|
||||
dst.writeLong(sha256((world.seed ?: 0).toByteArray()).sliceArray(0..7).toLong())
|
||||
|
||||
dst.writeVarInt(0) // max players; not used anymore
|
||||
|
||||
dst.writeVarInt(maxViewDistance)
|
||||
dst.writeBoolean(reducedDebugInfo)
|
||||
dst.writeBoolean(respawnScreenEnabled)
|
||||
|
||||
dst.writeBoolean(false) // Is debug world
|
||||
dst.writeBoolean(world.isFlat)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import space.blokk.net.MinecraftProtocolDataTypes.writeString
|
|||
import space.blokk.net.packet.OutgoingPacketCodec
|
||||
|
||||
object OutgoingPluginMessagePacketCodec :
|
||||
OutgoingPacketCodec<OutgoingPluginMessagePacket>(0x19, OutgoingPluginMessagePacket::class) {
|
||||
OutgoingPacketCodec<OutgoingPluginMessagePacket>(0x17, OutgoingPluginMessagePacket::class) {
|
||||
override fun OutgoingPluginMessagePacket.encode(dst: ByteBuf) {
|
||||
dst.writeString(channel)
|
||||
dst.writeBytes(data)
|
||||
|
|
|
@ -2,16 +2,12 @@ package space.blokk.net.packet.play
|
|||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import space.blokk.net.packet.OutgoingPacketCodec
|
||||
import space.blokk.util.bitmask
|
||||
import space.blokk.util.setBit
|
||||
|
||||
object PlayerAbilitiesPacketCodec : OutgoingPacketCodec<PlayerAbilitiesPacket>(0x32, PlayerAbilitiesPacket::class) {
|
||||
object PlayerAbilitiesPacketCodec : OutgoingPacketCodec<PlayerAbilitiesPacket>(0x30, PlayerAbilitiesPacket::class) {
|
||||
override fun PlayerAbilitiesPacket.encode(dst: ByteBuf) {
|
||||
var flags = 0
|
||||
if (invulnerable) flags = flags and 0x01
|
||||
if (flying) flags = flags and 0x02
|
||||
if (canFly) flags = flags and 0x04
|
||||
if (instantlyBreakBlocks) flags = flags and 0x08
|
||||
|
||||
dst.writeByte(flags)
|
||||
dst.writeByte(bitmask(invulnerable, flying, canFly, instantlyBreakBlocks))
|
||||
dst.writeFloat(flyingSpeed)
|
||||
dst.writeFloat(fieldOfView)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
|
|||
import space.blokk.net.packet.OutgoingPacketCodec
|
||||
import space.blokk.player.GameMode
|
||||
|
||||
object PlayerInfoPacketCodec : OutgoingPacketCodec<PlayerInfoPacket>(0x34, PlayerInfoPacket::class) {
|
||||
object PlayerInfoPacketCodec : OutgoingPacketCodec<PlayerInfoPacket>(0x32, PlayerInfoPacket::class) {
|
||||
override fun PlayerInfoPacket.encode(dst: ByteBuf) {
|
||||
val encoder = when (action) {
|
||||
is PlayerInfoPacket.Action.AddPlayer -> ActionEncoder.AddPlayer
|
||||
|
|
|
@ -3,27 +3,18 @@ package space.blokk.net.packet.play
|
|||
import io.netty.buffer.ByteBuf
|
||||
import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
|
||||
import space.blokk.net.packet.OutgoingPacketCodec
|
||||
import space.blokk.util.bitmask
|
||||
import space.blokk.util.setBit
|
||||
|
||||
object PlayerPositionAndLookPacketCodec :
|
||||
OutgoingPacketCodec<PlayerPositionAndLookPacket>(0x36, PlayerPositionAndLookPacket::class) {
|
||||
OutgoingPacketCodec<PlayerPositionAndLookPacket>(0x34, PlayerPositionAndLookPacket::class) {
|
||||
override fun PlayerPositionAndLookPacket.encode(dst: ByteBuf) {
|
||||
dst.writeDouble(locationWithRotation.x)
|
||||
dst.writeDouble(locationWithRotation.y)
|
||||
dst.writeDouble(locationWithRotation.z)
|
||||
dst.writeFloat(locationWithRotation.yaw)
|
||||
dst.writeFloat(locationWithRotation.pitch)
|
||||
|
||||
dst.writeByte(
|
||||
0b00000000.toByte()
|
||||
.setBit(0, relativeX)
|
||||
.setBit(1, relativeY)
|
||||
.setBit(2, relativeZ)
|
||||
.setBit(3, relativeYaw)
|
||||
.setBit(4, relativePitch)
|
||||
.toInt()
|
||||
)
|
||||
|
||||
dst.writeByte(bitmask(relativeX, relativeY, relativeZ, relativeYaw, relativePitch))
|
||||
dst.writeVarInt(0) // Teleport ID, I am not sure why this is needed
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import io.netty.buffer.ByteBuf
|
|||
import space.blokk.Difficulty
|
||||
import space.blokk.net.packet.OutgoingPacketCodec
|
||||
|
||||
object ServerDifficultyPacketCodec : OutgoingPacketCodec<ServerDifficultyPacket>(0x0E, ServerDifficultyPacket::class) {
|
||||
object ServerDifficultyPacketCodec : OutgoingPacketCodec<ServerDifficultyPacket>(0x0D, ServerDifficultyPacket::class) {
|
||||
override fun ServerDifficultyPacket.encode(dst: ByteBuf) {
|
||||
dst.writeByte(
|
||||
when (difficultySettings.difficulty) {
|
||||
|
|
|
@ -4,8 +4,8 @@ import io.netty.buffer.ByteBuf
|
|||
import space.blokk.net.packet.OutgoingPacketCodec
|
||||
|
||||
object SetSelectedHotbarSlotPacketCodec :
|
||||
OutgoingPacketCodec<SetSelectedHotbarSlotPacket>(0x40, SetSelectedHotbarSlotPacket::class) {
|
||||
OutgoingPacketCodec<SetSelectedHotbarSlotPacket>(0x3F, SetSelectedHotbarSlotPacket::class) {
|
||||
override fun SetSelectedHotbarSlotPacket.encode(dst: ByteBuf) {
|
||||
dst.writeByte(index.toInt())
|
||||
dst.writeByte(index)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import kotlin.time.Duration
|
|||
import kotlin.time.toJavaDuration
|
||||
|
||||
object TagsPacketCodec :
|
||||
OutgoingPacketCodec<TagsPacket>(0x5C, TagsPacket::class, CacheOptions(3, Duration.INFINITE.toJavaDuration())) {
|
||||
OutgoingPacketCodec<TagsPacket>(0x5B, TagsPacket::class, CacheOptions(3, Duration.INFINITE.toJavaDuration())) {
|
||||
private val ORDER = listOf(
|
||||
Tag.Type.BLOCKS,
|
||||
Tag.Type.ITEMS,
|
||||
|
|
|
@ -5,7 +5,7 @@ import space.blokk.net.MinecraftProtocolDataTypes.writeVarInt
|
|||
import space.blokk.net.packet.OutgoingPacketCodec
|
||||
|
||||
object UpdateViewPositionPacketCodec :
|
||||
OutgoingPacketCodec<UpdateViewPositionPacket>(0x41, UpdateViewPositionPacket::class) {
|
||||
OutgoingPacketCodec<UpdateViewPositionPacket>(0x40, UpdateViewPositionPacket::class) {
|
||||
override fun UpdateViewPositionPacket.encode(dst: ByteBuf) {
|
||||
dst.writeVarInt(chunkKey.x)
|
||||
dst.writeVarInt(chunkKey.z)
|
||||
|
|
|
@ -1,70 +1,39 @@
|
|||
@file:Suppress("DuplicatedCode")
|
||||
|
||||
package space.blokk.util
|
||||
|
||||
/**
|
||||
* Taken from Minestom (https://git.io/JIFbN)
|
||||
* Original license: Apache License 2.0
|
||||
*
|
||||
* Changes: Translated to Kotlin
|
||||
*/
|
||||
fun IntArray.toCompactLongArray(bitsPerEntry: Int): LongArray {
|
||||
val itemsPerLong = Long.SIZE_BITS / bitsPerEntry
|
||||
val array = LongArray(size / itemsPerLong)
|
||||
|
||||
private val MAGIC = intArrayOf(
|
||||
-1, -1, 0, Int.MIN_VALUE, 0, 0, 1431655765, 1431655765, 0, Int.MIN_VALUE,
|
||||
0, 1, 858993459, 858993459, 0, 715827882, 715827882, 0, 613566756, 613566756,
|
||||
0, Int.MIN_VALUE, 0, 2, 477218588, 477218588, 0, 429496729, 429496729, 0,
|
||||
390451572, 390451572, 0, 357913941, 357913941, 0, 330382099, 330382099, 0, 306783378,
|
||||
306783378, 0, 286331153, 286331153, 0, Int.MIN_VALUE, 0, 3, 252645135, 252645135,
|
||||
0, 238609294, 238609294, 0, 226050910, 226050910, 0, 214748364, 214748364, 0,
|
||||
204522252, 204522252, 0, 195225786, 195225786, 0, 186737708, 186737708, 0, 178956970,
|
||||
178956970, 0, 171798691, 171798691, 0, 165191049, 165191049, 0, 159072862, 159072862,
|
||||
0, 153391689, 153391689, 0, 148102320, 148102320, 0, 143165576, 143165576, 0,
|
||||
138547332, 138547332, 0, Int.MIN_VALUE, 0, 4, 130150524, 130150524, 0, 126322567,
|
||||
126322567, 0, 122713351, 122713351, 0, 119304647, 119304647, 0, 116080197, 116080197,
|
||||
0, 113025455, 113025455, 0, 110127366, 110127366, 0, 107374182, 107374182, 0,
|
||||
104755299, 104755299, 0, 102261126, 102261126, 0, 99882960, 99882960, 0, 97612893,
|
||||
97612893, 0, 95443717, 95443717, 0, 93368854, 93368854, 0, 91382282, 91382282,
|
||||
0, 89478485, 89478485, 0, 87652393, 87652393, 0, 85899345, 85899345, 0,
|
||||
84215045, 84215045, 0, 82595524, 82595524, 0, 81037118, 81037118, 0, 79536431,
|
||||
79536431, 0, 78090314, 78090314, 0, 76695844, 76695844, 0, 75350303, 75350303,
|
||||
0, 74051160, 74051160, 0, 72796055, 72796055, 0, 71582788, 71582788, 0,
|
||||
70409299, 70409299, 0, 69273666, 69273666, 0, 68174084, 68174084, 0, Int.MIN_VALUE,
|
||||
0, 5
|
||||
)
|
||||
|
||||
fun IntArray.toCompactLongArray(bitsPerEntry: Int): LongArray =
|
||||
toCompactLongArray(indices, this::get, size, bitsPerEntry)
|
||||
|
||||
fun ByteArray.toCompactLongArray(bitsPerEntry: Int): LongArray =
|
||||
toCompactLongArray(indices, this::get, size, bitsPerEntry)
|
||||
|
||||
private fun toCompactLongArray(
|
||||
indices: IntRange,
|
||||
getValue: (index: Int) -> Any,
|
||||
size: Int,
|
||||
bitsPerEntry: Int
|
||||
): LongArray {
|
||||
val maxEntryValue = (1L shl bitsPerEntry) - 1
|
||||
val valuesPerLong = (64 / bitsPerEntry)
|
||||
val magicIndex = 3 * (valuesPerLong - 1)
|
||||
val divideMul = Integer.toUnsignedLong(MAGIC[magicIndex])
|
||||
val divideAdd = Integer.toUnsignedLong(MAGIC[magicIndex + 1])
|
||||
val divideShift = MAGIC[magicIndex + 2]
|
||||
val longArraySize: Int = (size + valuesPerLong - 1) / valuesPerLong
|
||||
|
||||
val data = LongArray(longArraySize)
|
||||
|
||||
for (i in indices) {
|
||||
val value = when (val v = getValue(i)) {
|
||||
is Int -> v.toLong()
|
||||
is Byte -> v.toLong()
|
||||
else -> error("value must be a byte or an int")
|
||||
var totalIndex = 0
|
||||
for(longIndex in array.indices) {
|
||||
var long: Long = 0
|
||||
for (index in 0 until itemsPerLong) {
|
||||
long = (get(totalIndex).toLong() shl (index * bitsPerEntry)) or long
|
||||
totalIndex++
|
||||
}
|
||||
|
||||
val cellIndex = (i * divideMul + divideAdd shr 32 shr divideShift).toInt()
|
||||
val bitIndex: Int = (i - cellIndex * valuesPerLong) * bitsPerEntry
|
||||
|
||||
data[cellIndex] =
|
||||
data[cellIndex] and (maxEntryValue shl bitIndex).inv() or (value and maxEntryValue) shl bitIndex
|
||||
array[longIndex] = long
|
||||
}
|
||||
|
||||
return data
|
||||
return array
|
||||
}
|
||||
|
||||
fun ByteArray.toCompactLongArray(bitsPerEntry: Int): LongArray {
|
||||
val itemsPerLong = Long.SIZE_BITS / bitsPerEntry
|
||||
val array = LongArray(size / itemsPerLong)
|
||||
|
||||
var totalIndex = 0
|
||||
for(longIndex in array.indices) {
|
||||
var long: Long = 0
|
||||
for (index in 0 until itemsPerLong) {
|
||||
long = (get(totalIndex).toLong() shl (index * bitsPerEntry)) or long
|
||||
totalIndex++
|
||||
}
|
||||
|
||||
array[longIndex] = long
|
||||
}
|
||||
|
||||
return array
|
||||
}
|
||||
|
|
|
@ -2,12 +2,11 @@ package space.blokk.util
|
|||
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
fun Long.toByteArray(): ByteArray = ByteBuffer.allocate(Long.SIZE_BYTES).also {
|
||||
it.putLong(this)
|
||||
}.array()
|
||||
|
||||
fun ByteArray.toLong() = ByteBuffer.allocate(Long.SIZE_BYTES).also {
|
||||
it.put(this)
|
||||
it.flip()
|
||||
}.getLong()
|
||||
|
||||
fun Long.toByteArray() = ByteBuffer.allocate(Long.SIZE_BYTES).also {
|
||||
it.putLong(this)
|
||||
}.array()
|
||||
}.long
|
|
@ -6,4 +6,4 @@ import space.blokk.recipe.Recipe
|
|||
/**
|
||||
* Sent by the server while the player is joining.
|
||||
*/
|
||||
data class DeclareRecipesPacket(val recipes: Set<Recipe>) : OutgoingPacket()
|
||||
data class DeclareRecipesPacket(val recipes: Collection<Recipe>) : OutgoingPacket()
|
||||
|
|
|
@ -2,17 +2,14 @@ package space.blokk.net.packet.play
|
|||
|
||||
import space.blokk.net.packet.OutgoingPacket
|
||||
import space.blokk.player.GameMode
|
||||
import space.blokk.world.WorldDimension
|
||||
import space.blokk.world.WorldType
|
||||
import space.blokk.world.World
|
||||
|
||||
/**
|
||||
* Sent by the server after the client logged in.
|
||||
*
|
||||
* @param entityID ID of the player entity.
|
||||
* @param gameMode Game mode of the player.
|
||||
* @param worldDimension Dimension of the world the player joins.
|
||||
* @param worldSeedHash First 8 bytes of the SHA-256 hash of the world's seed.
|
||||
* @param worldType Type of the world the player joins.
|
||||
* @param world The world in which the player spawns.
|
||||
* @param maxViewDistance Maximum view distance allowed by the server.
|
||||
* @param reducedDebugInfo Whether the debug screen shows only reduced info.
|
||||
* @param respawnScreenEnabled Whether the respawn screen is shown when the player dies.
|
||||
|
@ -21,9 +18,7 @@ data class JoinGamePacket(
|
|||
val entityID: Int,
|
||||
val gameMode: GameMode,
|
||||
val hardcore: Boolean,
|
||||
val worldDimension: WorldDimension,
|
||||
val worldSeedHash: Long,
|
||||
val worldType: WorldType,
|
||||
val world: World,
|
||||
val maxViewDistance: Int,
|
||||
val reducedDebugInfo: Boolean,
|
||||
val respawnScreenEnabled: Boolean
|
||||
|
|
|
@ -3,8 +3,7 @@ package space.blokk
|
|||
import com.sksamuel.hoplite.ConfigFilePropertySource
|
||||
import com.sksamuel.hoplite.ConfigLoader
|
||||
import com.sksamuel.hoplite.ConfigSource
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.*
|
||||
import space.blokk.config.BlokkConfig
|
||||
import space.blokk.event.EventBus
|
||||
import space.blokk.event.EventTargetGroup
|
||||
|
@ -17,25 +16,19 @@ import space.blokk.recipe.Recipe
|
|||
import space.blokk.server.Server
|
||||
import space.blokk.server.event.ServerEvent
|
||||
import space.blokk.util.EncryptionUtils
|
||||
import space.blokk.util.createUnconfinedSupervisorScope
|
||||
import space.blokk.world.Biome
|
||||
import space.blokk.world.BiomeRegistry
|
||||
import space.blokk.world.Dimension
|
||||
import java.io.File
|
||||
import java.security.KeyPair
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class BlokkServer internal constructor() : Server {
|
||||
val logger = Logger("Server")
|
||||
private val socketServer = BlokkSocketServer(this)
|
||||
|
||||
override val scope = createUnconfinedSupervisorScope("Server")
|
||||
override val eventBus = EventBus(ServerEvent::class, logger, scope)
|
||||
override val sessions by socketServer::sessions
|
||||
override val players = EventTargetGroup.Mutable<Player>(true)
|
||||
override val pluginManager = BlokkPluginManager(this)
|
||||
override val loggingOutputProvider = BlokkLoggingOutputProvider
|
||||
override val scheduler = BlokkScheduler()
|
||||
override val recipes: Set<Recipe> = CopyOnWriteArraySet()
|
||||
|
||||
val keyPair: KeyPair =
|
||||
try {
|
||||
EncryptionUtils.generateKeyPair()
|
||||
|
@ -45,12 +38,28 @@ class BlokkServer internal constructor() : Server {
|
|||
|
||||
val x509EncodedPublicKey: ByteArray = EncryptionUtils.generateX509Key(keyPair.public).encoded
|
||||
|
||||
override val serverDirectory: File = run {
|
||||
var dir = File(BlokkServer::class.java.protectionDomain.codeSource.location.toURI())
|
||||
if (VERSION == "development") dir = dir.resolve("../../../../../serverData").normalize().also { it.mkdirs() }
|
||||
dir
|
||||
override val coroutineContext: CoroutineContext =
|
||||
CoroutineName("Server") + Executors.newSingleThreadExecutor().asCoroutineDispatcher() + SupervisorJob()
|
||||
|
||||
override val eventBus = EventBus(ServerEvent::class, logger, coroutineContext)
|
||||
|
||||
override val sessions by socketServer::sessions
|
||||
override val players = EventTargetGroup.Mutable<Player>(true)
|
||||
|
||||
override val pluginManager = BlokkPluginManager(this)
|
||||
override val serverDirectory: File = File(BlokkServer::class.java.protectionDomain.codeSource.location.toURI()).let {
|
||||
it.mkdirs()
|
||||
if (VERSION == "development") it.resolve("../../../../../serverData").normalize()
|
||||
else it
|
||||
}
|
||||
|
||||
override val recipeRegistry = Registry<Recipe>()
|
||||
override val dimensionRegistry = Registry<Dimension>()
|
||||
override val biomeRegistry = BiomeRegistry()
|
||||
|
||||
override val loggingOutputProvider = BlokkLoggingOutputProvider
|
||||
override val scheduler = BlokkScheduler()
|
||||
|
||||
val config = ConfigLoader.Builder()
|
||||
.addPropertySource(
|
||||
ConfigFilePropertySource(
|
||||
|
@ -66,13 +75,14 @@ class BlokkServer internal constructor() : Server {
|
|||
.build().loadConfigOrThrow<BlokkConfig>()
|
||||
|
||||
override val minimumLogLevel = config.minLogLevel
|
||||
override val developmentMode: Boolean = config.developmentMode
|
||||
|
||||
init {
|
||||
@Suppress("DEPRECATION")
|
||||
Blokk.provider = object : BlokkProvider {
|
||||
override val server = this@BlokkServer
|
||||
override val developmentMode: Boolean = config.developmentMode
|
||||
}
|
||||
val clazz = Class.forName("space.blokk.BlokkKt")
|
||||
val field = clazz.getDeclaredField("serverInstance")
|
||||
field.isAccessible = true
|
||||
field.set(null, this)
|
||||
field.isAccessible = false
|
||||
}
|
||||
|
||||
private fun failInitialization(t: Throwable): Nothing {
|
||||
|
@ -94,8 +104,6 @@ class BlokkServer internal constructor() : Server {
|
|||
}
|
||||
|
||||
override fun shutdown() {
|
||||
scope.cancel("Shutdown")
|
||||
|
||||
runBlocking {
|
||||
scheduler.shutdown()
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import space.blokk.Blokk
|
|||
|
||||
class LogbackAppender : AppenderBase<ILoggingEvent>() {
|
||||
override fun append(event: ILoggingEvent) {
|
||||
Blokk.server.loggingOutputProvider.log(
|
||||
Blokk.loggingOutputProvider.log(
|
||||
true,
|
||||
event.loggerName,
|
||||
when (event.level) {
|
||||
|
|
|
@ -2,8 +2,7 @@ package space.blokk.net
|
|||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.channel.Channel
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import space.blokk.BlokkServer
|
||||
import space.blokk.chat.ChatColor
|
||||
import space.blokk.chat.ChatComponent
|
||||
|
@ -23,10 +22,10 @@ import space.blokk.net.packet.play.PlayProtocol
|
|||
import space.blokk.net.packet.status.StatusProtocol
|
||||
import space.blokk.server.event.SessionInitializedEvent
|
||||
import space.blokk.util.awaitSuspending
|
||||
import space.blokk.util.createUnconfinedSupervisorScope
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import javax.crypto.SecretKey
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class BlokkSession(private val channel: Channel, val server: BlokkServer) : Session {
|
||||
override val address: InetAddress = (channel.remoteAddress() as InetSocketAddress).address
|
||||
|
@ -47,14 +46,16 @@ class BlokkSession(private val channel: Channel, val server: BlokkServer) : Sess
|
|||
|
||||
is State.LoginSucceeded,
|
||||
is State.WaitingForClientSettings,
|
||||
is State.JoiningWorld,
|
||||
is State.Joining,
|
||||
is State.Playing -> PlayProtocol
|
||||
|
||||
is State.Disconnected -> null
|
||||
}
|
||||
|
||||
override val scope = createUnconfinedSupervisorScope(identifier)
|
||||
override val eventBus = EventBus(SessionEvent::class, logger, scope)
|
||||
override val coroutineContext: CoroutineContext =
|
||||
CoroutineName(identifier) + Dispatchers.Unconfined + SupervisorJob()
|
||||
|
||||
override val eventBus = EventBus(SessionEvent::class, logger, coroutineContext)
|
||||
override var brand: String? = null; internal set
|
||||
|
||||
private var disconnectReason: String? = null
|
||||
|
@ -70,7 +71,7 @@ class BlokkSession(private val channel: Channel, val server: BlokkServer) : Sess
|
|||
})
|
||||
}
|
||||
|
||||
fun onConnect() = scope.launch {
|
||||
fun onConnect() = launch {
|
||||
logger trace "Connected"
|
||||
if (server.eventBus.emitAsync(SessionInitializedEvent(this@BlokkSession)).isCancelled) channel.close()
|
||||
else server.sessions.add(this@BlokkSession)
|
||||
|
@ -86,7 +87,7 @@ class BlokkSession(private val channel: Channel, val server: BlokkServer) : Sess
|
|||
else message
|
||||
}
|
||||
|
||||
scope.cancel(DisconnectedCancellationException(reason))
|
||||
cancel(DisconnectedCancellationException(reason))
|
||||
state = State.Disconnected(reason)
|
||||
|
||||
server.sessions.remove(this)
|
||||
|
|
|
@ -2,8 +2,6 @@ package space.blokk.net
|
|||
|
||||
import io.netty.buffer.Unpooled
|
||||
import space.blokk.BlokkServer
|
||||
import space.blokk.Difficulty
|
||||
import space.blokk.DifficultySettings
|
||||
import space.blokk.chat.TextComponent
|
||||
import space.blokk.event.ifCancelled
|
||||
import space.blokk.net.MinecraftProtocolDataTypes.writeString
|
||||
|
@ -12,7 +10,6 @@ import space.blokk.net.event.SessionAfterLoginEvent
|
|||
import space.blokk.net.packet.login.*
|
||||
import space.blokk.net.packet.play.*
|
||||
import space.blokk.player.BlokkPlayer
|
||||
import space.blokk.player.GameMode
|
||||
import space.blokk.tag.TagRegistry
|
||||
import space.blokk.util.*
|
||||
import space.blokk.world.Chunk
|
||||
|
@ -20,7 +17,6 @@ import java.security.MessageDigest
|
|||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.random.Random
|
||||
|
||||
class LoginAndJoinProcedure(val session: BlokkSession) {
|
||||
private val tagsPacket by lazy { TagsPacket(TagRegistry.tags) }
|
||||
|
@ -90,17 +86,13 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
|
|||
session.disconnect(loggableReason = "No spawn location set")
|
||||
}
|
||||
else -> {
|
||||
val seedAsBytes = (initialWorldAndLocation.world.seed ?: Random.nextLong()).toByteArray()
|
||||
|
||||
// TODO: Spawn the player entity
|
||||
session.send(
|
||||
JoinGamePacket(
|
||||
0,
|
||||
event.gameMode,
|
||||
event.hardcoreHearts,
|
||||
initialWorldAndLocation.world.dimension,
|
||||
sha256(seedAsBytes).sliceArray(0..7).toLong(),
|
||||
initialWorldAndLocation.world.type,
|
||||
initialWorldAndLocation.world,
|
||||
event.maxViewDistance,
|
||||
event.reducedDebugInfo,
|
||||
event.respawnScreenEnabled
|
||||
|
@ -111,22 +103,22 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
|
|||
"minecraft:brand",
|
||||
Unpooled.buffer().writeString("Blokk ${BlokkServer.VERSION_WITH_V}")
|
||||
)
|
||||
//
|
||||
// // As this is only visual, there is no way of changing it aside from intercepting the packet.
|
||||
// session.send(ServerDifficultyPacket(DifficultySettings(Difficulty.NORMAL, false)))
|
||||
|
||||
// As this is only visual, there is no way of changing this aside from intercepting the packet.
|
||||
session.send(ServerDifficultyPacket(DifficultySettings(Difficulty.NORMAL, false)))
|
||||
|
||||
session.send(
|
||||
PlayerAbilitiesPacket(
|
||||
event.invulnerable,
|
||||
event.flying,
|
||||
event.canFly,
|
||||
// TODO: Consider allowing to modify this value
|
||||
event.gameMode == GameMode.CREATIVE,
|
||||
// TODO: Find out how this relates to the entity property named `generic.flying_speed`
|
||||
event.flyingSpeed,
|
||||
event.fieldOfView
|
||||
)
|
||||
)
|
||||
// session.send(
|
||||
// PlayerAbilitiesPacket(
|
||||
// event.invulnerable,
|
||||
// event.flying,
|
||||
// event.canFly,
|
||||
// // TODO: Consider allowing to modify this value
|
||||
// event.gameMode == GameMode.CREATIVE,
|
||||
// // TODO: Find out how this relates to the entity property named `generic.flying_speed`
|
||||
// event.flyingSpeed,
|
||||
// event.fieldOfView
|
||||
// )
|
||||
// )
|
||||
|
||||
session.state = Session.State.WaitingForClientSettings(
|
||||
state.username,
|
||||
|
@ -171,11 +163,11 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
|
|||
state.selectedHotbarSlot
|
||||
)
|
||||
|
||||
session.state = Session.State.JoiningWorld(player)
|
||||
session.state = Session.State.Joining(player)
|
||||
|
||||
session.send(SetSelectedHotbarSlotPacket(state.selectedHotbarSlot))
|
||||
|
||||
session.send(DeclareRecipesPacket(session.server.recipes))
|
||||
session.send(DeclareRecipesPacket(session.server.recipeRegistry.items.values))
|
||||
session.send(tagsPacket)
|
||||
|
||||
// DeclareCommands
|
||||
|
@ -183,7 +175,7 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
|
|||
|
||||
session.send(PlayerPositionAndLookPacket(state.initialWorldAndLocation.location))
|
||||
|
||||
session.send(PlayerInfoPacket(PlayerInfoPacket.Action.AddPlayer(session.server.players.map { it ->
|
||||
session.send(PlayerInfoPacket(PlayerInfoPacket.Action.AddPlayer((session.server.players + player).map {
|
||||
it.uuid to PlayerInfoPacket.Action.AddPlayer.Data(
|
||||
it.name,
|
||||
it.gameMode,
|
||||
|
@ -193,13 +185,21 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
|
|||
)
|
||||
}.toMap())))
|
||||
|
||||
session.send(
|
||||
PlayerInfoPacket(
|
||||
PlayerInfoPacket.Action.UpdateLatency(session.server.players.map { it.uuid to it.rtt }.toMap())
|
||||
)
|
||||
)
|
||||
|
||||
session.send(UpdateViewPositionPacket(Chunk.Key.from(player.location.asVoxelLocation())))
|
||||
|
||||
player.sendChunksAndLight()
|
||||
|
||||
// TODO: Send WorldBorder packet
|
||||
// WorldBorder
|
||||
// TODO: Send SpawnPosition packet
|
||||
// TODO: Send PlayerPositionAndLook packet (again)
|
||||
|
||||
session.send(PlayerPositionAndLookPacket(state.initialWorldAndLocation.location))
|
||||
|
||||
// TODO: Wait for ClientStatus(action=0) packet
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,6 @@ class PacketMessageHandler(private val session: BlokkSession) :
|
|||
SimpleChannelInboundHandler<IncomingPacketMessage<*>>() {
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: IncomingPacketMessage<*>) {
|
||||
session.logger.trace { "Packet received: ${msg.packet}" }
|
||||
session.scope.launch { session.eventBus.emit(PacketReceivedEvent(session, msg.packet)) }
|
||||
session.launch { session.eventBus.emit(PacketReceivedEvent(session, msg.packet)) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import space.blokk.net.Session
|
|||
import space.blokk.net.packet.play.ChunkDataPacket
|
||||
import space.blokk.net.packet.play.ChunkLightDataPacket
|
||||
import space.blokk.player.event.PlayerEvent
|
||||
import space.blokk.util.createUnconfinedSupervisorScope
|
||||
import space.blokk.world.*
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
@ -43,8 +42,7 @@ class BlokkPlayer(
|
|||
|
||||
private val identifier = "BlokkPlayer($name)"
|
||||
private val logger = Logger(identifier)
|
||||
override val scope by lazy { createUnconfinedSupervisorScope(identifier, session.scope.coroutineContext[Job]) }
|
||||
override val eventBus = EventBus(PlayerEvent::class, logger, scope)
|
||||
override val eventBus = EventBus(PlayerEvent::class, logger, coroutineContext)
|
||||
|
||||
override var playerListName: TextComponent? = null
|
||||
override var rtt = -1
|
||||
|
|
|
@ -72,7 +72,7 @@ class BlokkPluginManager(private val server: BlokkServer) : PluginManager {
|
|||
|
||||
logger info "Successfully loaded ${plugins.size}/${files.size} " +
|
||||
"${pluralize("plugin", plugins.size)}: " +
|
||||
plugins.joinToString { "${it.meta.name}@${it.meta.version}" }
|
||||
plugins.joinToString { "${it.meta.name} (${it.meta.version})" }
|
||||
}
|
||||
|
||||
sealed class LoadError {
|
||||
|
|
|
@ -38,11 +38,11 @@ class DataDownloader(private val dir: File) {
|
|||
|
||||
companion object {
|
||||
val FILES = mapOf(
|
||||
"minecraft_server.jar" to "https://launcher.mojang.com/v1/objects/bb2b6b1aefcd70dfd1892149ac3a215f6c636b07/server.jar",
|
||||
"blocks.json" to PRISMARINE_BASE_URL + "1.15.2/blocks.json",
|
||||
"biomes.json" to PRISMARINE_BASE_URL + "1.15.2/biomes.json",
|
||||
"entities.json" to PRISMARINE_BASE_URL + "1.15.2/entities.json",
|
||||
"blockCollisionShapes.json" to PRISMARINE_BASE_URL + "1.15.2/blockCollisionShapes.json"
|
||||
"minecraft_server.jar" to "https://launcher.mojang.com/v1/objects/35139deedbd5182953cf1caa23835da59ca3d7cd/server.jar",
|
||||
"blocks.json" to PRISMARINE_BASE_URL + "1.16.2/blocks.json",
|
||||
"biomes.json" to PRISMARINE_BASE_URL + "1.16.2/biomes.json",
|
||||
"entities.json" to PRISMARINE_BASE_URL + "1.16.2/entities.json",
|
||||
"blockCollisionShapes.json" to PRISMARINE_BASE_URL + "1.16.1/blockCollisionShapes.json"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ class MinecraftDataSourcesPlugin : Plugin<Project> {
|
|||
|
||||
BlocksAndMaterialGenerator(workingDir, outputDir, sourcesDir).generate()
|
||||
TagsGenerator(workingDir, outputDir).generate()
|
||||
BiomesGenerator(workingDir, outputDir).generate()
|
||||
EntitiesGenerator(workingDir, outputDir, sourcesDir).generate()
|
||||
FluidIDMapGenerator(workingDir, outputDir, registries).generate()
|
||||
ItemTypeEnumGenerator(workingDir, outputDir, registries).generate()
|
||||
|
|
|
@ -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
|
||||
|
||||
import space.blokk.Blokk
|
||||
import space.blokk.NamespacedID
|
||||
import space.blokk.event.EventHandler
|
||||
import space.blokk.event.Listener
|
||||
import space.blokk.net.event.SessionAfterLoginEvent
|
||||
import space.blokk.plugin.Plugin
|
||||
import space.blokk.world.*
|
||||
import space.blokk.testplugin.anvil.AnvilWorld
|
||||
import space.blokk.world.block.Dirt
|
||||
import space.blokk.world.block.GrassBlock
|
||||
import space.blokk.world.block.GreenWool
|
||||
import space.blokk.world.block.SnowBlock
|
||||
|
||||
class TestPlugin: Plugin("Test", "1.0.0") {
|
||||
override fun onEnable() {
|
||||
val world = AnvilWorld(WorldDimension.OVERWORLD, WorldType.FLAT)
|
||||
world.getVoxel(VoxelLocation(0, 2, 0)).block = GrassBlock()
|
||||
val dimension = Dimension(
|
||||
NamespacedID("test:test"),
|
||||
true,
|
||||
0.0f,
|
||||
true
|
||||
)
|
||||
|
||||
Blokk.dimensionRegistry.register(dimension)
|
||||
|
||||
val world = AnvilWorld(dimension, true)
|
||||
world.getVoxelsInCube(VoxelLocation(100, 0, 100), VoxelLocation(-100, 0, -100)).forEach {
|
||||
it.block = GreenWool()
|
||||
}
|
||||
|
||||
world.getVoxel(VoxelLocation(0, 10, 0)).block = Dirt()
|
||||
world.getVoxel(VoxelLocation(10, 4, 22)).block = GrassBlock()
|
||||
world.getVoxel(VoxelLocation(40, 3, -32)).block = SnowBlock()
|
||||
|
||||
Blokk.sessions.registerListener(object : Listener {
|
||||
@EventHandler
|
||||
|
|
|
@ -13,7 +13,7 @@ class AnvilChunk(world: AnvilWorld, key: Key) : Chunk(world, key) {
|
|||
override fun getData(player: Player?): ChunkData {
|
||||
return ChunkData(
|
||||
sections.map { section -> if (section.blocks.all { it == Air }) null else section.blocks }.toTypedArray(),
|
||||
Array(ChunkData.BIOME_AREAS_IN_CHUNK) { Biome.THE_VOID }
|
||||
Array(ChunkData.BIOME_AREAS_IN_CHUNK) { Biome.PLAINS }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ class AnvilVoxel(
|
|||
chunkZ = z
|
||||
}
|
||||
|
||||
private val sectionIndex = location.y.toInt() / Chunk.LENGTH
|
||||
private val sectionIndex = chunkY.toInt() / Chunk.SECTION_HEIGHT
|
||||
private val index = chunkY.toInt() * Chunk.AREA + chunkZ * Chunk.LENGTH + chunkX
|
||||
|
||||
override var block: Block
|
||||
|
|
|
@ -7,13 +7,12 @@ import space.blokk.entity.Entity
|
|||
import space.blokk.event.EventTargetGroup
|
||||
import space.blokk.world.Chunk
|
||||
import space.blokk.world.World
|
||||
import space.blokk.world.WorldDimension
|
||||
import space.blokk.world.WorldType
|
||||
import space.blokk.world.Dimension
|
||||
import java.util.*
|
||||
|
||||
class AnvilWorld(
|
||||
override val dimension: WorldDimension,
|
||||
override val type: WorldType
|
||||
override val dimension: Dimension,
|
||||
override val isFlat: Boolean
|
||||
) : World(UUID.randomUUID()) {
|
||||
override val loadedChunks: Map<Chunk.Key, Chunk> get() = chunks.asMap().filter { (_, chunk) -> chunk.loaded }
|
||||
override val entities: EventTargetGroup.Mutable<Entity> = EventTargetGroup.Mutable(false)
|
||||
|
|
Reference in a new issue