Refactorings, function additions, contracts and documentation changes
This commit is contained in:
parent
1476716c1d
commit
41344a8507
36 changed files with 169 additions and 183 deletions
|
@ -1,3 +1 @@
|
|||
# Blokk
|
||||
## To Do
|
||||
- Support packet compression
|
||||
|
|
|
@ -50,6 +50,7 @@ tasks {
|
|||
kotlinOptions.jvmTarget = "1.8"
|
||||
kotlinOptions.freeCompilerArgs = listOf(
|
||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xopt-in=kotlin.contracts.ExperimentalContracts",
|
||||
"-progressive"
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
/**
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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>()
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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."),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
package space.blokk.util
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
fun KClass<Enum<*>>.getValues(): Array<out Enum<*>> = java.enumConstants
|
|
@ -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
|
|
@ -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)}"
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package space.blokk.world.block
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
@Retention
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
annotation class Attribute(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue