Add more packets and change some world related files
This commit is contained in:
parent
785ee432cc
commit
2de7338a88
19 changed files with 231 additions and 47 deletions
|
@ -1,7 +1,6 @@
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm")
|
kotlin("jvm")
|
||||||
kotlin("kapt")
|
kotlin("kapt")
|
||||||
id("com.github.johnrengelman.shadow") version "6.1.0"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = rootProject.group
|
group = rootProject.group
|
||||||
|
@ -31,6 +30,9 @@ dependencies {
|
||||||
// Netty
|
// Netty
|
||||||
api("io.netty:netty-buffer:${nettyVersion}")
|
api("io.netty:netty-buffer:${nettyVersion}")
|
||||||
|
|
||||||
|
// Other
|
||||||
|
api("com.google.guava:guava:30.0-jre")
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
testImplementation("io.strikt:strikt-core:${striktVersion}")
|
testImplementation("io.strikt:strikt-core:${striktVersion}")
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
|
testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
|
||||||
|
|
|
@ -4,7 +4,11 @@ import space.blokk.world.BlockLocation
|
||||||
import space.blokk.world.World
|
import space.blokk.world.World
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
data class Location(val x: Double, val y: Double, val z: Double) {
|
abstract class AbstractLocation {
|
||||||
|
abstract val x: Double
|
||||||
|
abstract val y: Double
|
||||||
|
abstract val z: Double
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts this [Location] to a [BlockLocation] by converting [x], [y] and [z] to an integer using [Double.toInt],
|
* Converts this [Location] to a [BlockLocation] by converting [x], [y] and [z] to an integer using [Double.toInt],
|
||||||
* in contrast to [roundToBlock] which uses [Double.roundToInt].
|
* in contrast to [roundToBlock] which uses [Double.roundToInt].
|
||||||
|
@ -17,5 +21,36 @@ data class Location(val x: Double, val y: Double, val z: Double) {
|
||||||
*/
|
*/
|
||||||
fun roundToBlock(): BlockLocation = BlockLocation(x.roundToInt(), y.roundToInt(), z.roundToInt())
|
fun roundToBlock(): BlockLocation = BlockLocation(x.roundToInt(), y.roundToInt(), z.roundToInt())
|
||||||
|
|
||||||
data class InWorld(val world: World, val location: Location)
|
/**
|
||||||
|
* @return A [Pair] of this location and [world].
|
||||||
|
*/
|
||||||
|
abstract infix fun inside(world: World): InWorld<*>
|
||||||
|
|
||||||
|
abstract class InWorld<T : AbstractLocation> internal constructor() {
|
||||||
|
abstract val world: World
|
||||||
|
abstract val location: T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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].
|
||||||
|
*/
|
||||||
|
fun withRotation(yaw: Float, pitch: Float) = WithRotation(x, y, z, yaw, pitch)
|
||||||
|
|
||||||
|
override fun inside(world: World) = InWorld(world, this)
|
||||||
|
data class InWorld(override val world: World, override val location: Location) :
|
||||||
|
AbstractLocation.InWorld<Location>()
|
||||||
|
|
||||||
|
data class WithRotation(
|
||||||
|
override val x: Double,
|
||||||
|
override val y: Double,
|
||||||
|
override val z: Double,
|
||||||
|
val yaw: Float,
|
||||||
|
val pitch: Float
|
||||||
|
) : AbstractLocation() {
|
||||||
|
override fun inside(world: World) = InWorld(world, this)
|
||||||
|
data class InWorld(override val world: World, override val location: WithRotation) :
|
||||||
|
AbstractLocation.InWorld<WithRotation>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ sealed class ChatComponent {
|
||||||
abstract val extra: List<ChatComponent>?
|
abstract val extra: List<ChatComponent>?
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@Suppress("LeakingThis")
|
|
||||||
if (extra?.isEmpty() == true) throw IllegalArgumentException("extra cannot be an empty list. Use null instead.")
|
if (extra?.isEmpty() == true) throw IllegalArgumentException("extra cannot be an empty list. Use null instead.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ class PlayerInitializationEvent(session: Session, val settings: Player.Settings)
|
||||||
/**
|
/**
|
||||||
* The location where the player will spawn. If this is null after all handlers ran, the player will disconnect.
|
* The location where the player will spawn. If this is null after all handlers ran, the player will disconnect.
|
||||||
*/
|
*/
|
||||||
var spawnLocation: Location.InWorld? = null
|
var spawnLocation: Location.WithRotation.InWorld? = null
|
||||||
|
|
||||||
var gameMode: GameMode = GameMode.SURVIVAL
|
var gameMode: GameMode = GameMode.SURVIVAL
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ interface Player : EventTarget<PlayerEvent> {
|
||||||
*/
|
*/
|
||||||
val settings: Settings
|
val settings: Settings
|
||||||
|
|
||||||
val location: Location.InWorld
|
val location: Location.WithRotation.InWorld
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The index of the hotbar slot which is currently selected.
|
* The index of the hotbar slot which is currently selected.
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package space.blokk.util
|
||||||
|
|
||||||
|
import com.google.common.cache.CacheBuilder
|
||||||
|
import com.google.common.cache.CacheLoader
|
||||||
|
import com.google.common.cache.LoadingCache
|
||||||
|
|
||||||
|
fun <K, V> createWeakValuesLoadingCache(loader: (key: K) -> V): LoadingCache<K, V> {
|
||||||
|
return CacheBuilder
|
||||||
|
.newBuilder()
|
||||||
|
.weakValues()
|
||||||
|
.build(object : CacheLoader<K, V>() {
|
||||||
|
override fun load(key: K): V = loader(key)
|
||||||
|
})
|
||||||
|
}
|
|
@ -17,6 +17,14 @@ data class BlockLocation(val x: Int, val y: Int, val z: Int) {
|
||||||
*/
|
*/
|
||||||
fun getCenterLocation(): Location = Location(x.toDouble() + 0.5, y.toDouble() + 0.5, z.toDouble() + 0.5)
|
fun getCenterLocation(): Location = Location(x.toDouble() + 0.5, y.toDouble() + 0.5, z.toDouble() + 0.5)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this [BlockLocation] to a [Location] by converting [x], [y] and [z] to a double using [Int.toDouble]
|
||||||
|
* and then adding 0.5 to x and z, but not y.
|
||||||
|
*
|
||||||
|
* Example: `BlockLocation(x = 1, y = 2, z = 3)` becomes `Location(x = 1.5, y = 2, z = 3.5)`.
|
||||||
|
*/
|
||||||
|
fun getTopCenterLocation(): Location = Location(x.toDouble() + 0.5, y.toDouble(), z.toDouble() + 0.5)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return A new [BlockLocation] with the maximum x, y and z values of a and b.
|
* @return A new [BlockLocation] with the maximum x, y and z values of a and b.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -48,7 +48,7 @@ interface Chunk {
|
||||||
*
|
*
|
||||||
* If the implementation does not need to load chunks, this method should do nothing.
|
* If the implementation does not need to load chunks, this method should do nothing.
|
||||||
*/
|
*/
|
||||||
suspend fun load()
|
suspend fun load() {}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val WIDTH_AND_LENGTH: Byte = 16
|
const val WIDTH_AND_LENGTH: Byte = 16
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
package space.blokk.world
|
package space.blokk.world
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineName
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import space.blokk.CoordinatePartOrder
|
import space.blokk.CoordinatePartOrder
|
||||||
import space.blokk.entity.Entity
|
import space.blokk.entity.Entity
|
||||||
import space.blokk.world.block.BlockRef
|
import space.blokk.world.block.BlockRef
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Minecraft world, sometimes also called level.
|
* A Minecraft world, sometimes also called level.
|
||||||
|
@ -11,13 +15,19 @@ import space.blokk.world.block.BlockRef
|
||||||
* **Note: Although the methods in this class are called `getBlock`, `getBlocksInSphere` and so on, they actually
|
* **Note: Although the methods in this class are called `getBlock`, `getBlocksInSphere` and so on, they actually
|
||||||
* return [BlockRef]s.**
|
* return [BlockRef]s.**
|
||||||
*/
|
*/
|
||||||
abstract class World {
|
abstract class World(val uuid: UUID) {
|
||||||
/**
|
/**
|
||||||
* [CoroutineScope] of this world.
|
* [CoroutineScope] of this world.
|
||||||
*
|
*
|
||||||
* Gets cancelled when the world is unloaded.
|
* Gets cancelled when the world is unloaded.
|
||||||
*/
|
*/
|
||||||
abstract val scope: CoroutineScope
|
val scope: CoroutineScope by lazy {
|
||||||
|
CoroutineScope(
|
||||||
|
SupervisorJob()
|
||||||
|
+ Dispatchers.Unconfined
|
||||||
|
+ CoroutineName("World($uuid)")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [dimension][WorldDimension] of this world.
|
* The [dimension][WorldDimension] of this world.
|
||||||
|
|
|
@ -14,20 +14,16 @@ abstract class Block internal constructor(val ref: BlockRef) {
|
||||||
*/
|
*/
|
||||||
val destroyed: Boolean get() = ref.block === this
|
val destroyed: Boolean get() = ref.block === this
|
||||||
|
|
||||||
/**
|
protected fun checkNotDestroyed() {
|
||||||
* Replaces this block with air.
|
|
||||||
*/
|
|
||||||
fun replaceWithAir() {
|
|
||||||
checkNotDestroyed()
|
|
||||||
ref.place(Material.AIR)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkNotDestroyed() {
|
|
||||||
if (destroyed) throw IllegalStateException("The block was destroyed.")
|
if (destroyed) throw IllegalStateException("The block was destroyed.")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when [ref] no longer references this block.
|
* Called when [ref] no longer references this block.
|
||||||
*/
|
*/
|
||||||
internal abstract fun destroy()
|
protected open fun onDestroy() {}
|
||||||
|
|
||||||
|
internal fun destroy() {
|
||||||
|
onDestroy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
package space.blokk.world.block
|
package space.blokk.world.block
|
||||||
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import space.blokk.world.BlockLocation
|
import space.blokk.world.BlockLocation
|
||||||
import space.blokk.world.Chunk
|
import space.blokk.world.Chunk
|
||||||
import space.blokk.world.World
|
import space.blokk.world.World
|
||||||
import kotlin.reflect.KClass
|
|
||||||
import kotlin.reflect.full.primaryConstructor
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reference to a block in a chunk of a world.
|
* A reference to a block in a chunk of a world.
|
||||||
|
@ -34,21 +30,18 @@ abstract class BlockRef {
|
||||||
*/
|
*/
|
||||||
abstract var block: Block; protected set
|
abstract var block: Block; protected set
|
||||||
|
|
||||||
private val placeMutex by lazy { Mutex() }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new [Block] and assign it to this ref (i.e. place it in the world).
|
* Create a new [Block] and assign it to this ref (i.e. place it in the world).
|
||||||
*/
|
*/
|
||||||
abstract fun place(material: Material): Block
|
abstract suspend fun place(material: Material)
|
||||||
|
|
||||||
/**
|
// There should always be only one [BlockRef] instance for every location in a world.
|
||||||
* Create a new [Block] and assign it to this ref (i.e. place it in the world).
|
override fun equals(other: Any?): Boolean = this === other
|
||||||
*/
|
|
||||||
suspend fun <T : Block> place(type: KClass<T>): T = type.primaryConstructor!!.call(this).also {
|
override fun hashCode(): Int {
|
||||||
placeMutex.withLock {
|
var result = location.hashCode()
|
||||||
block.destroy()
|
result = 31 * result + world.hashCode()
|
||||||
block = it
|
return result
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package space.blokk.net.packet.play
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf
|
||||||
|
import space.blokk.net.MinecraftProtocolDataTypes.writeString
|
||||||
|
import space.blokk.net.packet.OutgoingPacketCodec
|
||||||
|
|
||||||
|
object PlayerInfoPacketCodec : OutgoingPacketCodec<PlayerInfoPacket>(0x32, PlayerInfoPacket::class) {
|
||||||
|
override fun PlayerInfoPacket.encode(dst: ByteBuf) {
|
||||||
|
when (val a = action) {
|
||||||
|
is PlayerInfoPacket.Action.AddPlayer -> {
|
||||||
|
dst.writeInt(0)
|
||||||
|
dst.writeInt(a.entries.size)
|
||||||
|
|
||||||
|
for (entry in a.entries) {
|
||||||
|
dst.writeString(entry.name)
|
||||||
|
dst.writeInt(entry.properties.size)
|
||||||
|
|
||||||
|
for ((name, property) in entry.properties) {
|
||||||
|
dst.writeString(name)
|
||||||
|
dst.writeString(property.value)
|
||||||
|
|
||||||
|
val hasSignature = property.signature != null
|
||||||
|
dst.writeBoolean(hasSignature)
|
||||||
|
if (hasSignature) dst.writeString(property.signature!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unfinished
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package space.blokk.net.packet.play
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf
|
||||||
|
import space.blokk.net.packet.OutgoingPacketCodec
|
||||||
|
|
||||||
|
object PlayerPositionAndLookPacketCodec :
|
||||||
|
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)
|
||||||
|
|
||||||
|
var flags = 0x00
|
||||||
|
if (relativeX) flags = flags and 0x01
|
||||||
|
if (relativeY) flags = flags and 0x02
|
||||||
|
if (relativeZ) flags = flags and 0x04
|
||||||
|
if (relativeYaw) flags = flags and 0x08
|
||||||
|
if (relativePitch) flags = flags and 0x10
|
||||||
|
|
||||||
|
dst.writeByte(flags)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package space.blokk.net.packet.play
|
||||||
|
|
||||||
|
import space.blokk.chat.TextComponent
|
||||||
|
import space.blokk.net.packet.OutgoingPacket
|
||||||
|
import space.blokk.player.GameMode
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Informs the client about other players.
|
||||||
|
* If the real game mode of a player differs from the one in this packet, weird things can happen.
|
||||||
|
*/
|
||||||
|
data class PlayerInfoPacket(val action: Action<*>) : OutgoingPacket() {
|
||||||
|
sealed class Action<T : Action.IPlayerEntry> {
|
||||||
|
abstract val entries: List<T>
|
||||||
|
|
||||||
|
interface IPlayerEntry {
|
||||||
|
val uuid: UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AddPlayer(override val entries: List<PlayerEntry>) : Action<AddPlayer.PlayerEntry>() {
|
||||||
|
data class PlayerEntry(
|
||||||
|
override val uuid: UUID,
|
||||||
|
val name: String,
|
||||||
|
val gameMode: GameMode,
|
||||||
|
val latency: Int,
|
||||||
|
val displayName: TextComponent?,
|
||||||
|
val properties: Map<String, Property>
|
||||||
|
) : IPlayerEntry {
|
||||||
|
data class Property(
|
||||||
|
val value: String,
|
||||||
|
val signature: String?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UpdateGameMode(override val entries: List<PlayerEntry>) : Action<UpdateGameMode.PlayerEntry>() {
|
||||||
|
data class PlayerEntry(override val uuid: UUID, val gameMode: GameMode) : IPlayerEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UpdateLatency(override val entries: List<PlayerEntry>) : Action<UpdateLatency.PlayerEntry>() {
|
||||||
|
data class PlayerEntry(override val uuid: UUID, val latency: Int) : IPlayerEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UpdateDisplayName(override val entries: List<PlayerEntry>) :
|
||||||
|
Action<UpdateDisplayName.PlayerEntry>() {
|
||||||
|
data class PlayerEntry(override val uuid: UUID, val displayName: TextComponent?) : IPlayerEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
data class RemovePlayer(override val entries: List<PlayerEntry>) : Action<RemovePlayer.PlayerEntry>() {
|
||||||
|
data class PlayerEntry(override val uuid: UUID) : IPlayerEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package space.blokk.net.packet.play
|
||||||
|
|
||||||
|
import space.blokk.Location
|
||||||
|
import space.blokk.net.packet.OutgoingPacket
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teleports the receiving player to the specified location.
|
||||||
|
*/
|
||||||
|
data class PlayerPositionAndLookPacket(
|
||||||
|
val locationWithRotation: Location.WithRotation,
|
||||||
|
val relativeX: Boolean = false,
|
||||||
|
val relativeY: Boolean = false,
|
||||||
|
val relativeZ: Boolean = false,
|
||||||
|
val relativeYaw: Boolean = false,
|
||||||
|
val relativePitch: Boolean = false,
|
||||||
|
val teleportID: Int = Random.nextInt()
|
||||||
|
) : OutgoingPacket()
|
|
@ -143,6 +143,8 @@ class LoginAndJoinProcedure(val session: BlokkSession) {
|
||||||
// TODO: Send Declare Recipes packet
|
// TODO: Send Declare Recipes packet
|
||||||
// TODO: Send Tags packet
|
// TODO: Send Tags packet
|
||||||
// TODO: Send Entity Status packet with OP permission level
|
// TODO: Send Entity Status packet with OP permission level
|
||||||
|
|
||||||
|
session.send(PlayerPositionAndLookPacket(spawnLocation.location))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package space.blokk.player
|
package space.blokk.player
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineName
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import space.blokk.Location
|
import space.blokk.Location
|
||||||
import space.blokk.event.EventBus
|
import space.blokk.event.EventBus
|
||||||
import space.blokk.logging.Logger
|
import space.blokk.logging.Logger
|
||||||
|
@ -17,17 +14,19 @@ class BlokkPlayer(
|
||||||
override val uuid: UUID,
|
override val uuid: UUID,
|
||||||
override var gameMode: GameMode,
|
override var gameMode: GameMode,
|
||||||
override var settings: Player.Settings,
|
override var settings: Player.Settings,
|
||||||
override val location: Location.InWorld,
|
override val location: Location.WithRotation.InWorld,
|
||||||
selectedHotbarSlot: Byte
|
selectedHotbarSlot: Byte
|
||||||
) : Player {
|
) : Player {
|
||||||
private val identifier = "BlokkPlayer($username)"
|
private val identifier = "BlokkPlayer($username)"
|
||||||
private val logger = Logger(identifier)
|
private val logger = Logger(identifier)
|
||||||
|
|
||||||
override val scope = CoroutineScope(
|
override val scope by lazy {
|
||||||
Job(session.scope.coroutineContext[Job])
|
CoroutineScope(
|
||||||
|
SupervisorJob(session.scope.coroutineContext[Job])
|
||||||
+ Dispatchers.Unconfined
|
+ Dispatchers.Unconfined
|
||||||
+ CoroutineName(identifier)
|
+ CoroutineName(identifier)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override val eventBus = EventBus(PlayerEvent::class, scope, logger)
|
override val eventBus = EventBus(PlayerEvent::class, scope, logger)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "space.blokk"
|
group = "space.blokk"
|
||||||
version = "0.0.1"
|
version = "0.0.1-SNAPSHOT"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|
|
@ -76,8 +76,7 @@ class FilesGenerator(private val dataDir: File, private val outputDir: File, pri
|
||||||
continue
|
continue
|
||||||
|
|
||||||
val type = TypeSpec.classBuilder(upperCamelName)
|
val type = TypeSpec.classBuilder(upperCamelName)
|
||||||
.addKdoc("A [$upperUnderscoreName][%T] block", specificMaterialType)
|
.addKdoc("A block of type [$upperUnderscoreName][%T]", specificMaterialType)
|
||||||
.addModifiers(KModifier.ABSTRACT)
|
|
||||||
.superclass(BLOCK_TYPE)
|
.superclass(BLOCK_TYPE)
|
||||||
.primaryConstructor(
|
.primaryConstructor(
|
||||||
FunSpec.constructorBuilder()
|
FunSpec.constructorBuilder()
|
||||||
|
|
Reference in a new issue