Archived
1
0
Fork 0

Refactorings, function additions, contracts and documentation changes

This commit is contained in:
Moritz Ruth 2020-11-22 15:09:15 +01:00
parent 1476716c1d
commit 41344a8507
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
36 changed files with 169 additions and 183 deletions

View file

@ -1,3 +1 @@
# Blokk
## To Do
- Support packet compression

View file

@ -50,6 +50,7 @@ tasks {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.freeCompilerArgs = listOf(
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlin.contracts.ExperimentalContracts",
"-progressive"
)
}

View file

@ -28,11 +28,11 @@ abstract class AbstractLocation {
* Converts this [Location] to a [VoxelLocation] by converting [x], [y] and [z] to an integer using [Double.toInt],
* in contrast to [roundToBlock] which uses [Double.roundToInt].
*/
fun asBlockLocation(): VoxelLocation = VoxelLocation(x.toInt(), y.toInt(), z.toInt())
fun asVoxelLocation(): VoxelLocation = VoxelLocation(x.toInt(), y.toInt(), z.toInt())
/**
* Converts this [Location] to a [VoxelLocation] by rounding [x], [y] and [z] to an integer using
* [Double.roundToInt], in contrast to [asBlockLocation] which uses [Double.toInt].
* [Double.roundToInt], in contrast to [asVoxelLocation] which uses [Double.toInt].
*/
fun roundToBlock(): VoxelLocation = VoxelLocation(x.roundToInt(), y.roundToInt(), z.roundToInt())
@ -42,16 +42,20 @@ abstract class AbstractLocation {
abstract infix fun inside(world: World): AbstractWorldAndLocation<*>
}
// TODO: Remove the data modifier and implement these function manually
data class Location(override val x: Double, override val y: Double, override val z: Double) : AbstractLocation() {
/**
* @return A [Location.WithRotation] of this location, [yaw] and [pitch].
* Returns a LocationWithRotation composed of this location, [yaw] and [pitch].
*/
fun withRotation(yaw: Float, pitch: Float) = LocationWithRotation(x, y, z, yaw, pitch)
/**
* Returns a pair of [world] and this location.
*/
override fun inside(world: World) = WorldAndLocation(world, this)
}
// Sadly, we must extend AbstractLocation instead of Location because data classes are always final.
// TODO: Extend Location instead of AbstractLocation
data class LocationWithRotation(
override val x: Double,
override val y: Double,

View file

@ -2,7 +2,6 @@ package space.blokk.chat
import com.squareup.moshi.*
import space.blokk.Blokk
import kotlin.reflect.KClass
sealed class ChatComponent {
abstract val bold: Boolean
@ -11,29 +10,7 @@ sealed class ChatComponent {
abstract val strikethrough: Boolean
abstract val obfuscated: Boolean
abstract val color: Color?
/**
* Cannot be an empty list. Use null instead.
*/
abstract val extra: List<ChatComponent>?
init {
if (extra?.isEmpty() == true) throw IllegalArgumentException("extra cannot be an empty list. Use null instead.")
}
fun getExtraTypes(): List<KClass<out ChatComponent>> {
val types = mutableSetOf<KClass<out ChatComponent>>()
val extras = extra?.toMutableList() ?: return emptyList()
while (extras.isNotEmpty()) {
extras.toList().forEach {
types.add(it::class)
extras.remove(it)
it.extra?.let { extra -> extras.addAll(extra) }
}
}
return types.toList()
}
abstract val extra: List<ChatComponent>
enum class Color() {
BLACK,
@ -60,11 +37,13 @@ sealed class ChatComponent {
@FromJson
fun fromJson(value: String) = valueOf(value.toUpperCase())
}
// TODO: Support hex colors
}
object Adapter {
@FromJson
fun fromJson(reader: JsonReader): ChatComponent? {
fun fromJson(@Suppress("UNUSED_PARAMETER") reader: JsonReader): ChatComponent? {
throw UnsupportedOperationException("ChatComponent cannot be deserialized.")
}
@ -88,7 +67,7 @@ data class TextComponent(
override val strikethrough: Boolean = false,
override val obfuscated: Boolean = false,
override val color: Color? = null,
override val extra: List<ChatComponent>? = null
override val extra: List<ChatComponent> = emptyList()
) : ChatComponent() {
companion object {
/**

View file

@ -1,11 +1,11 @@
package space.blokk.chat
/**
* Legacy formatting codes. You should use [ChatComponent][space.blokk.chat.ChatComponent] whenever it's possible,
* but sometimes these codes are required, for example in
* [the name of a player in a server list sample][space.blokk.net.packet.status.ResponsePacket.Players.SampleEntry.name].
* You should use [ChatComponent][space.blokk.chat.ChatComponent] whenever it's possible, but sometimes these codes
* are required, for example in [the name of a player in a server list sample]
* [space.blokk.net.packet.status.ResponsePacket.Players.SampleEntry.name].
*/
enum class FormattingCode(private val char: Char) {
enum class LegacyFormattingCode(private val char: Char) {
BLACK('0'),
DARK_BLUE('1'),
DARK_GREEN('2'),
@ -30,5 +30,5 @@ enum class FormattingCode(private val char: Char) {
RESET('r');
override fun toString(): String = "§$char"
operator fun plus(value: FormattingCode) = toString() + value.toString()
operator fun plus(value: LegacyFormattingCode) = toString() + value.toString()
}

View file

@ -1,5 +1,8 @@
package space.blokk.entity
interface Entity {
import space.blokk.event.Event
import space.blokk.event.EventTarget
interface Entity : EventTarget<Event> {
// TODO
}

View file

@ -1,5 +1,8 @@
package space.blokk.event
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
interface Cancellable {
var isCancelled: Boolean
}
@ -7,9 +10,23 @@ interface Cancellable {
/**
* Only executes [fn] if [isCancelled][Cancellable.isCancelled] is true.
*/
inline fun <T : Cancellable, R> T.ifCancelled(fn: (T) -> R): R? = if (isCancelled) fn(this) else null
inline fun <T : Cancellable, R> T.ifCancelled(fn: (T) -> R): R? {
contract {
callsInPlace(fn, InvocationKind.AT_MOST_ONCE)
// returns(null) implies(isCancelled)
}
return if (isCancelled) fn(this) else null
}
/**
* Only executes [fn] if [isCancelled][Cancellable.isCancelled] is false.
*/
inline fun <T : Cancellable, R> T.ifNotCancelled(fn: (T) -> R): R? = if (!isCancelled) fn(this) else null
inline fun <T : Cancellable, R> T.ifNotCancelled(fn: (T) -> R): R? {
contract {
callsInPlace(fn, InvocationKind.AT_MOST_ONCE)
// returns(null) implies(isCancelled)
}
return if (!isCancelled) fn(this) else null
}

View file

@ -1,6 +1,6 @@
package space.blokk.event
// TODO: Develop new event ordering concept
// TODO: Develop a new event ordering concept
enum class EventPriority {
LOWEST,
LOW,

View file

@ -8,12 +8,12 @@ interface EventTarget<T : Event> {
/**
* Shorthand for [`eventBus.emit(event)`][EventBus.emit].
*/
suspend fun <SpecificT : T> emit(event: SpecificT): SpecificT = eventBus.emit(event)
suspend fun <E : T> emit(event: E): E = eventBus.emit(event)
/**
* Shorthand for [`eventBus.emitAsync(event)`][EventBus.emitAsync].
*/
fun <SpecificT : T> emitAsync(event: SpecificT): Deferred<SpecificT> = eventBus.emitAsync(event)
fun <E : T> emitAsync(event: E): Deferred<E> = eventBus.emitAsync(event)
/**
* Shorthand for [`eventBus.register(listener)`][EventBus.register].

View file

@ -1,7 +1,8 @@
package space.blokk.event
abstract class EventTargetGroup<T : EventTarget<*>> : Iterable<T> {
private val targets = mutableSetOf<T>()
// TODO: Allow using a different collection implementation (in order to allow concurrency)
private val targets: MutableCollection<T> = mutableSetOf()
private val listeners = mutableSetOf<Listener>()
/**

View file

@ -48,6 +48,6 @@ class Logger(val name: String) {
fun isGreaterOrEqualThan(level: Level) = ordinal >= level.ordinal
val isEnabled get() = isGreaterOrEqualThan(Blokk.server.minLogLevel)
val isEnabled get() = isGreaterOrEqualThan(Blokk.server.minimumLogLevel)
}
}

View file

@ -18,7 +18,7 @@ interface Session : EventTarget<SessionEvent> {
val address: InetAddress
/**
* The coroutine scope of this session. It is cancelled when the session disconnects.
* The coroutine scope of this session. It is cancelled when the session is disconnected.
*/
val scope: CoroutineScope
@ -34,15 +34,17 @@ interface Session : EventTarget<SessionEvent> {
val currentProtocol: Protocol?
/**
* 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] if it is one of [State.JoiningWorld] or
* [State.FinishJoining]
* 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
* [State.FinishJoining].
*/
val player: Player?
get() = (state as? State.Playing)?.player
/**
* The current [State] of this session.
* The current state of this session.
*/
var state: State
@ -79,9 +81,9 @@ interface Session : EventTarget<SessionEvent> {
/**
* Closes the connection with the client.
*
* If the current protocol is `LOGIN` or `PLAY`, [reason] is shown to the player, otherwise, it is discarded.
*
* @param loggableReason A short, loggable representation of [reason].
* @param reason The message shown to the player. Only used if [currentProtocol] is `LOGIN` or `PLAY`.
* @param loggableReason A short, loggable representation of [reason]. Not shown to the player, except
* in development mode.
*/
suspend fun disconnect(
reason: TextComponent = TextComponent.of("Disconnected."),

View file

@ -22,7 +22,7 @@ class PlayerInitializationEvent(session: Session, val settings: Player.Settings)
var gameMode: GameMode = GameMode.SURVIVAL
/**
* See [Player.selectedHotbarSlot]
* See [Player.selectedHotbarSlot].
*/
var selectedHotbarSlot: Byte = 0
set(value) {

View file

@ -1,6 +1,6 @@
package space.blokk.net.packet
abstract class Packet {
sealed class Packet {
override fun toString(): String = this::class.java.simpleName + "(no data)"
}

View file

@ -1,18 +1,19 @@
package space.blokk.net.packet.status
import com.squareup.moshi.JsonClass
import space.blokk.chat.FormattingCode
import space.blokk.chat.LegacyFormattingCode
import space.blokk.chat.TextComponent
import space.blokk.net.packet.OutgoingPacket
import java.util.*
// TODO: Move to blokk-packets
/**
* Sent by the server in response to a [RequestPacket].
* Sent by the server in response to a [RequestPacket][space.blokk.net.packet.status].
*
* @param versionName The name of the Minecraft version the server uses.
* @param protocolVersion The number of the [protocol version](https://wiki.vg/Protocol_version_numbers) used by the server.
* @param description The description of the server. Although this is a [TextComponent],
* [legacy Formatting codes][space.blokk.chat.FormattingCode] must be used.
* @param description The description of the server. Although this is a [TextComponent], [LegacyFormattingCode]s must be used.
* @param players The players shown when hovering over the player count.
* @param favicon The favicon of the server. This must be a base64 encoded 64x64 PNG image.
*/
@ -23,15 +24,12 @@ data class ResponsePacket(
val players: Players,
val favicon: String? = null
) : OutgoingPacket() {
init {
if (description.getExtraTypes().find { it != TextComponent::class } != null)
throw Exception("description may only contain instances of TextComponent")
}
// TODO: Write custom adapters for these classes
@JsonClass(generateAdapter = true)
data class Players(val max: Int, val online: Int, val sample: List<SampleEntry>) {
/**
* @param name The name of the player. You can use [FormattingCode](space.blokk.chat.FormattingCode)s
* @param name The name of the player. You can use [LegacyFormattingCode]s.
* @param id The UUID of the player as a string. You can use a random UUID as it doesn't get validated by the Minecraft client
*/
@JsonClass(generateAdapter = true)
@ -49,7 +47,7 @@ data class ResponsePacket(
10,
listOf(
Players.SampleEntry(
"${FormattingCode.AQUA}Gronkh",
"${LegacyFormattingCode.AQUA}Gronkh",
UUID.randomUUID().toString()
)
)

View file

@ -12,10 +12,13 @@ import java.util.*
* A **real** player.
*/
interface Player : EventTarget<PlayerEvent> {
val scope: CoroutineScope
/**
* Shorthand for [`session.scope`][Session.scope].
*/
val scope: CoroutineScope get() = session.scope
/**
* The [Session] of this player.
* The session of this player.
*/
val session: Session
@ -40,7 +43,7 @@ interface Player : EventTarget<PlayerEvent> {
val settings: Settings
/**
* The current location of the player.
* The current location of this player.
*/
var location: LocationWithRotation
@ -51,7 +54,7 @@ interface Player : EventTarget<PlayerEvent> {
/**
* The index of the hotbar slot which is currently selected.
* Must be between 0 and 8.
* Must be in `0..8`.
*/
var selectedHotbarSlot: Byte

View file

@ -2,7 +2,6 @@ package space.blokk.server
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.ReceiveChannel
import space.blokk.DifficultyOptions
import space.blokk.event.EventTarget
import space.blokk.event.EventTargetGroup
import space.blokk.logging.Logger
@ -16,11 +15,6 @@ import java.io.File
interface Server : EventTarget<ServerEvent> {
val scope: CoroutineScope
/**
* The [DifficultyOptions] displayed to clients.
*/
var displayedDifficultyOptions: DifficultyOptions
/**
* [EventTargetGroup] containing all sessions connected to the server.
*/
@ -34,7 +28,7 @@ interface Server : EventTarget<ServerEvent> {
val pluginManager: PluginManager
val serverDirectory: File
val minLogLevel: Logger.Level
val minimumLogLevel: Logger.Level
val loggingOutputProvider: LoggingOutputProvider
/**

View file

@ -2,8 +2,10 @@ package space.blokk.util
import kotlin.experimental.and
fun Byte.checkBit(bitIndex: Int): Boolean {
val flag = (0x1 shl bitIndex).toByte()
/**
* Returns true if the bit at [index] is 1.
*/
fun Byte.checkBit(index: Int): Boolean {
val flag = (0x1 shl index).toByte()
return (this and flag) == flag
}

View file

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

View file

@ -4,6 +4,11 @@ import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader
import com.google.common.cache.LoadingCache
/**
* Creates a LoadingCache with weak values.
*
* @param loader The function invoked every time a value is not yet in the cache.
*/
fun <K, V> createWeakValuesLoadingCache(loader: (key: K) -> V): LoadingCache<K, V> {
return CacheBuilder
.newBuilder()

View file

@ -1,5 +0,0 @@
package space.blokk.util
import kotlin.reflect.KClass
fun KClass<Enum<*>>.getValues(): Array<out Enum<*>> = java.enumConstants

View file

@ -5,7 +5,7 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson
import java.util.*
fun Moshi.toJson(value: Map<*, *>) = adapter(Map::class.java).toJson(value)
fun Moshi.toJson(value: Map<*, *>): String = adapter(Map::class.java).toJson(value)
object UUIDAdapter {
@ToJson

View file

@ -9,7 +9,7 @@ fun pluralize(singular: String, count: Int, plural: String = singular + "s") =
if (count == 1) singular else plural
/**
* Same as [pluralize], but prepends [count] + " ".
* Same as [pluralize], but prepends `count + " "`.
*/
fun pluralizeWithCount(singular: String, count: Int, plural: String = singular + "s") =
"$count ${pluralize(singular, count, plural)}"

View file

@ -1,5 +1,7 @@
package space.blokk.util
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import space.blokk.Blokk
import space.blokk.server.Server
@ -13,12 +15,17 @@ suspend fun delayTicks(ticks: Long) {
}
/**
* Suspends for one tick.
* Launches a new coroutine after a delay of 1 tick.
*
* @see [CoroutineScope.launch]
*/
suspend fun delayTick() = delayTicks(1)
fun CoroutineScope.launchNextTick(block: suspend () -> Unit) = launch {
delayTicks(1)
block()
}
/**
* Returns the amount of ticks which are equal to [seconds].
* Returns the amount of ticks (without decimal places) which are equal to [seconds].
*/
fun secondsToTicks(seconds: Double): Long = (seconds * Server.TICKS_PER_SECOND).toLong()

View file

@ -5,8 +5,15 @@ import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.ToJson
import java.util.*
fun getUUIDFromStringWithoutHyphens(string: String): UUID {
val chars = string.toMutableList()
/**
* Parses the UUID represented by this string and returns it.
*
* The difference to [UUID.fromString] is that this function also works on stringified UUIDs without hyphens.
*/
fun String.toUUID(): UUID {
if (this.contains("-")) return UUID.fromString(this)
val chars = this.toMutableList()
chars.add(20, '-')
chars.add(16, '-')
chars.add(12, '-')
@ -17,14 +24,14 @@ fun getUUIDFromStringWithoutHyphens(string: String): UUID {
fun UUID.toStringWithoutHyphens() = toString().replace("-", "")
@Retention(AnnotationRetention.RUNTIME)
@Retention
@JsonQualifier
annotation class StringWithoutHyphens
object UUIDWithoutHyphensAdapter {
@FromJson
@StringWithoutHyphens
fun fromJson(value: String) = getUUIDFromStringWithoutHyphens(value)
fun fromJson(value: String) = value.toUUID()
@ToJson
fun toJson(@StringWithoutHyphens value: UUID) = value.toStringWithoutHyphens()

View file

@ -1,17 +0,0 @@
package space.blokk.util
import kotlin.reflect.KProperty
class ClampDelegate(val min: Int, val max: Int, var value: Int) {
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
if (value > max) throw IllegalArgumentException("${property.name} must be less than or equal to $max")
if (value < min) throw IllegalArgumentException("${property.name} must be greater than or equal to $min")
this.value = value
}
}
inline fun clamp(min: Int, max: Int, initialValue: () -> Int) = ClampDelegate(min, max, initialValue())

View file

@ -11,13 +11,21 @@ abstract class Chunk(
*/
val world: World,
/**
* [Key] of this chunk.
* The [Key] of this chunk.
*/
val key: Key
) {
private val identifier = "Chunk(${world.uuid}/${key.x}-${key.z})"
data class Key(val x: Int, val z: Int)
data class Key(val x: Int, val z: Int) {
companion object {
/**
* @return The key for the chunk containing the voxel at [location].
*/
fun from(location: VoxelLocation) =
Key(location.x / WIDTH_AND_LENGTH, location.y / WIDTH_AND_LENGTH)
}
}
/**
* [CoroutineScope] of this chunk.
@ -26,7 +34,6 @@ abstract class Chunk(
*/
val scope: CoroutineScope by lazy { createUnconfinedSupervisorScope(identifier, world.scope.coroutineContext[Job]) }
// TODO: Allow more than 16 sections
/**
* The 16 [ChunkSection]s this chunk consists of.
*/
@ -37,7 +44,18 @@ abstract class Chunk(
*
* **The coordinates are relative to this chunk, not to the entire world.**
*/
abstract fun getBlock(x: Byte, y: Int, z: Byte): Voxel
abstract fun getVoxel(x: Byte, y: Byte, z: Byte): Voxel
/**
* Returns whether this chunk contains [location].
*/
operator fun contains(location: VoxelLocation) = Key.from(location) == key
/**
* Returns whether this chunk contains [voxel].
*/
// It does not just compare voxel.chunk with this because voxel.chunk may be expensive to create
fun contains(voxel: Voxel) = contains(voxel.location)
/**
* Loads this chunk into memory.

View file

@ -3,6 +3,7 @@ package space.blokk.world
import kotlinx.coroutines.CoroutineScope
import space.blokk.CoordinatePartOrder
import space.blokk.entity.Entity
import space.blokk.event.EventTargetGroup
import space.blokk.util.createUnconfinedSupervisorScope
import java.util.*
@ -11,7 +12,7 @@ import java.util.*
*/
abstract class World(val uuid: UUID) {
/**
* [CoroutineScope] of this world.
* The [CoroutineScope] of this world.
*
* Gets cancelled when the world is unloaded.
*/
@ -30,7 +31,7 @@ abstract class World(val uuid: UUID) {
/**
* All entities in this world. Use [spawnEntity] for spawning new ones.
*/
abstract val entities: List<Entity>
abstract val entities: EventTargetGroup.Public<Entity>
/**
* All currently loaded chunks of this world.
@ -38,24 +39,24 @@ abstract class World(val uuid: UUID) {
abstract val loadedChunks: Map<Chunk.Key, Chunk>
/**
* @return The chunk containing the block at [location].
* Returns the chunk containing the block at [location].
*/
abstract fun getChunkAt(location: VoxelLocation): Chunk
/**
* @return The chunk containing the block at [location] or null if this chunk is not loaded.
* Returns the chunk containing the voxel at [location] or null if this chunk is not loaded.
*/
fun getLoadedChunkAt(location: VoxelLocation): Chunk? = loadedChunks[getChunkKeyAt(location)]
fun getLoadedChunkAt(location: VoxelLocation): Chunk? = loadedChunks[Chunk.Key.from(location)]
/**
* @return The block at [location].
* Returns the block at [location].
*/
abstract fun getVoxelAt(location: VoxelLocation): Voxel
/**
* @param order The nesting order of the arrays.
* Returns all voxels in the cube with the corner points [firstCorner] and [secondCorner].
*
* @return All voxels in the cube with the corner points [firstCorner] and [secondCorner].
* @param order The nesting order of the arrays.
*/
fun getVoxelsInCube(
firstCorner: VoxelLocation,
@ -75,7 +76,7 @@ abstract class World(val uuid: UUID) {
}
/**
* @return All voxels in the sphere with the specified [center] and [radius].
* Returns all voxels in a sphere with the specified [center] and [radius].
*/
fun getBlocksInSphere(
center: VoxelLocation,
@ -89,12 +90,4 @@ abstract class World(val uuid: UUID) {
* Spawns an [entity][Entity] in this world.
*/
abstract fun spawnEntity(entity: Entity)
companion object {
/**
* Gets the chunk key for the chunk containing the block at [location].
*/
fun getChunkKeyAt(location: VoxelLocation) =
Chunk.Key(location.x / Chunk.WIDTH_AND_LENGTH, location.y / Chunk.WIDTH_AND_LENGTH)
}
}

View file

@ -1,5 +1,8 @@
package space.blokk.world.block
/**
* @suppress
*/
@Retention
@Target(AnnotationTarget.PROPERTY)
annotation class Attribute(

View file

@ -3,7 +3,9 @@ package space.blokk.world.block
import kotlin.reflect.KClass
/**
* Child classes should never have mutable properties.
* A block which can for example be placed in a [Voxel][space.blokk.world.Voxel].
*
* Blocks should always be **immutable**.
*/
abstract class Block internal constructor() {
/**
@ -17,7 +19,10 @@ abstract class Block internal constructor() {
}
companion object {
inline fun <reified T : Block> meta(id: Int, firstStateID: Int) = object : Meta<T> {
/**
* See [DaylightDetector] for an example on how this function should be used.
*/
internal inline fun <reified T : Block> meta(id: Int, firstStateID: Int) = object : Meta<T> {
override val blockClass: KClass<T> = T::class
override val codec: BlockCodec<T> = BlockCodec(blockClass, id, firstStateID)
}

View file

@ -10,7 +10,7 @@ import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.full.starProjectedType
import kotlin.reflect.jvm.jvmErasure
class BlockCodec<T : Block> constructor(
class BlockCodec<T : Block> internal constructor(
blockClass: KClass<T>,
val id: Int,
val firstStateID: Int

View file

@ -4,14 +4,14 @@ import org.junit.jupiter.api.Test
import strikt.api.expectThat
import strikt.assertions.isEqualTo
class FormattingCodeTest {
class LegacyFormattingCodeTest {
@Test
fun `correctly translates to a string`() {
expectThat("${FormattingCode.AQUA}").isEqualTo("§b")
expectThat("${LegacyFormattingCode.AQUA}").isEqualTo("§b")
}
@Test
fun `can be appended to another FormattingCode using the + operator`() {
expectThat(FormattingCode.AQUA + FormattingCode.BLACK).isEqualTo("§b§0")
expectThat(LegacyFormattingCode.AQUA + LegacyFormattingCode.BLACK).isEqualTo("§b§0")
}
}

View file

@ -1,39 +0,0 @@
package space.blokk.util
import org.junit.jupiter.api.Test
import strikt.api.expectThrows
class ValidationDelegatesTest {
private var clamped by clamp(MINIMUM_CLAMP_VALUE, MAXIMUM_CLAMP_VALUE) { 0 }
@Test
fun `clamp fails for too small values`() {
expectThrows<IllegalArgumentException> {
clamped = MINIMUM_CLAMP_VALUE - 1
clamped = MINIMUM_CLAMP_VALUE - 10000
Unit
}
}
@Test
fun `clamp fails for too great values`() {
expectThrows<IllegalArgumentException> {
clamped = MAXIMUM_CLAMP_VALUE + 1
clamped = MAXIMUM_CLAMP_VALUE + 10000
Unit
}
}
@Test
fun `clamp does not fail for values in the range`() {
clamped = MAXIMUM_CLAMP_VALUE
clamped = MAXIMUM_CLAMP_VALUE - 1
clamped = MINIMUM_CLAMP_VALUE
clamped = MINIMUM_CLAMP_VALUE + 1
}
companion object {
const val MINIMUM_CLAMP_VALUE = 0
const val MAXIMUM_CLAMP_VALUE = 10
}
}

View file

@ -26,7 +26,6 @@ class BlokkServer internal constructor() : Server {
override val scope = createUnconfinedSupervisorScope("Server")
override val eventBus = EventBus(ServerEvent::class, scope, logger)
override var displayedDifficultyOptions = DifficultyOptions(Difficulty.NORMAL, true)
override val sessions by socketServer::sessions
override val players = PlayerGroup()
override val pluginManager = BlokkPluginManager(this)
@ -63,7 +62,7 @@ class BlokkServer internal constructor() : Server {
)
.build().loadConfigOrThrow<BlokkConfig>()
override val minLogLevel = config.minLogLevel
override val minimumLogLevel = config.minLogLevel
override val loggingOutputProvider = BlokkLoggingOutputProvider
private val ticker = Ticker()

View file

@ -121,7 +121,7 @@ class BlokkSession(private val channel: Channel, val server: BlokkServer) : Sess
TextComponent.of(loggableReason)
)
reason.copy(extra = (reason.extra ?: emptyList()) + additional)
reason.copy(extra = reason.extra + additional)
} else reason
disconnectReason = loggableReason

View file

@ -1,8 +1,9 @@
package space.blokk.net
import io.netty.buffer.Unpooled
import space.blokk.Blokk
import space.blokk.BlokkServer
import space.blokk.Difficulty
import space.blokk.DifficultyOptions
import space.blokk.chat.TextComponent
import space.blokk.net.MinecraftProtocolDataTypes.writeString
import space.blokk.net.event.PlayerInitializationEvent
@ -15,7 +16,7 @@ import space.blokk.player.BlokkPlayer
import space.blokk.player.GameMode
import space.blokk.util.AuthenticationHelper
import space.blokk.util.EncryptionUtils
import space.blokk.world.World
import space.blokk.world.Chunk
import space.blokk.world.WorldDimension
import space.blokk.world.WorldType
import java.security.MessageDigest
@ -96,7 +97,8 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
Unpooled.buffer().writeString("Blokk ${BlokkServer.VERSION_WITH_V}")
)
session.send(ServerDifficultyPacket(Blokk.server.displayedDifficultyOptions))
// TODO: Use real data
session.send(ServerDifficultyPacket(DifficultyOptions(Difficulty.NORMAL, false)))
// TODO: Use real data
session.send(
@ -149,7 +151,7 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
// TODO: Send PlayerInfo packet
session.send(UpdateViewPositionPacket(World.getChunkKeyAt(session.player!!.location.asBlockLocation())))
session.send(UpdateViewPositionPacket(Chunk.Key.from(session.player!!.location.asVoxelLocation())))
}
}
}