From dcbad8f5dcffa879f06b2a5e6f130e217b6931b6 Mon Sep 17 00:00:00 2001 From: Moritz Ruth Date: Tue, 17 Nov 2020 17:21:44 +0100 Subject: [PATCH] Rename BlockRef to voxel and use annotations instead of BlockCodec.Builder --- blokk-api/build.gradle.kts | 3 + .../main/kotlin/space/blokk/world/Chunk.kt | 5 +- .../main/kotlin/space/blokk/world/Voxel.kt | 44 +++++++ .../main/kotlin/space/blokk/world/World.kt | 9 +- .../space/blokk/world/block/Attribute.kt | 11 ++ .../kotlin/space/blokk/world/block/Block.kt | 114 ++---------------- .../space/blokk/world/block/BlockCodec.kt | 81 +++++++++++++ .../space/blokk/world/block/BlockRef.kt | 51 -------- .../blokk/world/block/DaylightDetector.kt | 22 ++-- .../kotlin/space/blokk/world/block/Hopper.kt | 28 ----- .../space/blokk/world/block/BlockCodecTest.kt | 20 +-- .../kotlin/space/blokk/mdsp/FilesGenerator.kt | 40 ++---- 12 files changed, 176 insertions(+), 252 deletions(-) create mode 100644 blokk-api/src/main/kotlin/space/blokk/world/Voxel.kt create mode 100644 blokk-api/src/main/kotlin/space/blokk/world/block/Attribute.kt create mode 100644 blokk-api/src/main/kotlin/space/blokk/world/block/BlockCodec.kt delete mode 100644 blokk-api/src/main/kotlin/space/blokk/world/block/BlockRef.kt delete mode 100644 blokk-api/src/main/kotlin/space/blokk/world/block/Hopper.kt diff --git a/blokk-api/build.gradle.kts b/blokk-api/build.gradle.kts index 87f7379..e6b68b4 100644 --- a/blokk-api/build.gradle.kts +++ b/blokk-api/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { kotlin("jvm") kotlin("kapt") @@ -60,3 +62,4 @@ tasks { useJUnitPlatform() } } +val compileKotlin: KotlinCompile by tasks diff --git a/blokk-api/src/main/kotlin/space/blokk/world/Chunk.kt b/blokk-api/src/main/kotlin/space/blokk/world/Chunk.kt index 619948a..d87dede 100644 --- a/blokk-api/src/main/kotlin/space/blokk/world/Chunk.kt +++ b/blokk-api/src/main/kotlin/space/blokk/world/Chunk.kt @@ -2,7 +2,6 @@ package space.blokk.world import kotlinx.coroutines.* import space.blokk.world.Chunk.Key -import space.blokk.world.block.BlockRef abstract class Chunk( /** @@ -35,11 +34,11 @@ abstract class Chunk( abstract val sections: Array /** - * Returns the [BlockRef] for the specified coordinates in this chunk. + * Returns the [Voxel] for the specified coordinates in this chunk. * * **The coordinates are relative to this chunk, not to the entire world.** */ - abstract fun getBlock(x: Byte, y: Int, z: Byte): BlockRef + abstract fun getBlock(x: Byte, y: Int, z: Byte): Voxel /** * Loads this chunk into memory. diff --git a/blokk-api/src/main/kotlin/space/blokk/world/Voxel.kt b/blokk-api/src/main/kotlin/space/blokk/world/Voxel.kt new file mode 100644 index 0000000..a1a05ce --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/world/Voxel.kt @@ -0,0 +1,44 @@ +package space.blokk.world + +import space.blokk.world.block.Block + +/** + * A voxel in a chunk of a world. + * + * There is always only one instance for every [location][BlockLocation] in a world. + */ +abstract class Voxel { + /** + * The [location][BlockLocation] of this voxel. + */ + abstract val location: BlockLocation + + /** + * The [World] containing this voxel. + */ + abstract val world: World + + /** + * The [Chunk] containing this voxel. + */ + val chunk: Chunk get() = world.getChunkAt(location) + + /** + * The current content ([Block]) of this voxel. + */ + abstract var block: Block + + /** + * Shorthand for [chunk.loaded][Chunk.loaded]. + */ + val loaded: Boolean get() = chunk.loaded + + /** + * Shorthand for [`chunk.load()`][Chunk.load]. + */ + suspend fun load() = chunk.load() + + companion object { + fun of(world: World, location: BlockLocation) = world.getBlock(location) + } +} diff --git a/blokk-api/src/main/kotlin/space/blokk/world/World.kt b/blokk-api/src/main/kotlin/space/blokk/world/World.kt index f837fbb..173463d 100644 --- a/blokk-api/src/main/kotlin/space/blokk/world/World.kt +++ b/blokk-api/src/main/kotlin/space/blokk/world/World.kt @@ -6,14 +6,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import space.blokk.CoordinatePartOrder import space.blokk.entity.Entity -import space.blokk.world.block.BlockRef import java.util.* /** * A Minecraft world, sometimes also called level. * * **Note: Although the methods in this class are called `getBlock`, `getBlocksInSphere` and so on, they actually - * return [BlockRef]s.** + * return [Voxel]s.** */ abstract class World(val uuid: UUID) { /** @@ -62,7 +61,7 @@ abstract class World(val uuid: UUID) { /** * @return The block at [location]. */ - abstract fun getBlock(location: BlockLocation): BlockRef + abstract fun getBlock(location: BlockLocation): Voxel /** * @param order The order of the arrays. @@ -73,7 +72,7 @@ abstract class World(val uuid: UUID) { firstCorner: BlockLocation, secondCorner: BlockLocation, order: CoordinatePartOrder = CoordinatePartOrder.DEFAULT - ): Array>> { + ): Array>> { val start = firstCorner.withLowestValues(secondCorner) val end = firstCorner.withHighestValues(secondCorner) @@ -92,7 +91,7 @@ abstract class World(val uuid: UUID) { fun getBlocksInSphere( center: BlockLocation, radius: Int - ): Array { + ): Array { // https://www.reddit.com/r/VoxelGameDev/comments/2cttnt/how_to_create_a_sphere_out_of_voxels/ TODO() } diff --git a/blokk-api/src/main/kotlin/space/blokk/world/block/Attribute.kt b/blokk-api/src/main/kotlin/space/blokk/world/block/Attribute.kt new file mode 100644 index 0000000..5807de9 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/world/block/Attribute.kt @@ -0,0 +1,11 @@ +package space.blokk.world.block + +@Retention +@Target(AnnotationTarget.PROPERTY) +annotation class Attribute( + /** + * The maximum allowed value of this attribute. + * Silently ignored for properties whose type is not [Int]. + */ + val max: Int = 0 +) diff --git a/blokk-api/src/main/kotlin/space/blokk/world/block/Block.kt b/blokk-api/src/main/kotlin/space/blokk/world/block/Block.kt index 2257d00..1fe03e0 100644 --- a/blokk-api/src/main/kotlin/space/blokk/world/block/Block.kt +++ b/blokk-api/src/main/kotlin/space/blokk/world/block/Block.kt @@ -1,121 +1,25 @@ package space.blokk.world.block -import space.blokk.util.KPropertyValuePair import kotlin.reflect.KClass -import kotlin.reflect.KProperty -import kotlin.reflect.jvm.jvmErasure /** - * @param ref The [BlockRef] referencing this block. + * Child classes should never have mutable properties. */ -abstract class Block internal constructor(val ref: BlockRef) { +abstract class Block internal constructor() { /** * The [Material] of this block. */ val material: Material get() = Material.byClass.getValue(this::class) - /** - * Whether [ref] still references this block. - */ - val destroyed: Boolean get() = ref.block === this - - protected fun throwIfDestroyed() { - if (destroyed) throw IllegalStateException("The block was destroyed.") + interface Meta { + val blockClass: KClass + val codec: BlockCodec } - /** - * Called when [ref] no longer references this block. - */ - protected open fun onDestroy() {} - - internal fun destroy() { - onDestroy() - } - - abstract class Companion { - abstract val codec: Codec<*> - } - - class Codec private constructor( - val blockClass: KClass, - val id: Int, - val firstStateID: Int, - val states: List>> - ) { - companion object { - inline fun id(id: Int, firstStateID: Int) = id(T::class, id, firstStateID) - fun id(blockClass: KClass, id: Int, firstStateID: Int) = - Builder(blockClass, id, firstStateID) - } - - fun getStateID(block: T): Int { - val values = mutableMapOf, Any>() - - val index = states.indexOfFirst { propertyValuePairs -> - propertyValuePairs.all { - values.getOrPut(it.property) { it.property.call(block)!! } == it.value - } - } - - // If this happens, we did something wrong with adding the attributes or with clamping integer attributes - if (index == -1) throw Error("The state of block (${block.material}) has no ID") - - return firstStateID + index - } - - class Builder internal constructor( - private val blockClass: KClass, - private val id: Int, - private val firstStateID: Int - ) { - private val attributes = mutableListOf>() - - @JvmName("booleanAttribute") - fun attribute(property: KProperty) = this.apply { - attributes.add(BlockAttribute.Boolean(property)) - } - - @JvmName("enumAttribute") - fun attribute(property: KProperty>) = this.apply { - attributes.add(BlockAttribute.Enum(property)) - } - - @JvmName("intAttribute") - fun attribute(property: KProperty, max: Int) = this.apply { - attributes.add(BlockAttribute.Int(property, max)) - } - - fun build() = Codec(blockClass, id, firstStateID, generateStates(attributes)) - - sealed class BlockAttribute { - abstract val property: KProperty - - data class Boolean(override val property: KProperty) : BlockAttribute() - data class Int(override val property: KProperty, val max: kotlin.Int) : - BlockAttribute() - - data class Enum(override val property: KProperty>) : BlockAttribute>() - } - - companion object { - fun generateStates(properties: List>): List>> { - if (properties.isEmpty()) return emptyList() - - val current = properties[0] - - val allowedValues: Array = when (current) { - is BlockAttribute.Boolean -> arrayOf(true, false) - is BlockAttribute.Int -> (0..current.max).toList().toTypedArray() - is BlockAttribute.Enum -> current.property.returnType.jvmErasure.java.enumConstants - } - - val rest = generateStates(properties.drop(1)) - return allowedValues.flatMap { value -> - if (rest.isEmpty()) listOf(arrayOf(KPropertyValuePair(current.property, value))) - else rest.map { arrayOf(KPropertyValuePair(current.property, value), *it) } - } - } - } + companion object { + inline fun meta(id: Int, firstStateID: Int) = object : Meta { + override val blockClass: KClass = T::class + override val codec: BlockCodec = BlockCodec(blockClass, id, firstStateID) } } } diff --git a/blokk-api/src/main/kotlin/space/blokk/world/block/BlockCodec.kt b/blokk-api/src/main/kotlin/space/blokk/world/block/BlockCodec.kt new file mode 100644 index 0000000..5cae8b3 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/world/block/BlockCodec.kt @@ -0,0 +1,81 @@ +package space.blokk.world.block + +import space.blokk.util.KPropertyValuePair +import kotlin.reflect.KClass +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.isSubtypeOf +import kotlin.reflect.full.starProjectedType +import kotlin.reflect.jvm.jvmErasure + +class BlockCodec constructor( + blockClass: KClass, + val id: Int, + val firstStateID: Int +) { + val states: List>> = + generateStates(blockClass.declaredMemberProperties.mapNotNull { property -> + val annotation = property.findAnnotation() ?: return@mapNotNull null + val type = property.returnType + + @Suppress("UNCHECKED_CAST") + when { + type == Int::class.starProjectedType -> BlockAttribute.Int( + (property as KProperty1), + annotation.max + ) + type == Boolean::class.starProjectedType -> BlockAttribute.Boolean(property as KProperty1) + type.isSubtypeOf(Enum::class.starProjectedType) -> BlockAttribute.Enum(property as KProperty1>) + else -> throw IllegalAttributeTargetType() + } + }) + + class IllegalAttributeTargetType : Exception("The type of the target property is not allowed for attributes") + + fun getStateID(block: T): Int { + val values = mutableMapOf, Any>() + + val index = states.indexOfFirst { propertyValuePairs -> + propertyValuePairs.all { + values.getOrPut(it.property) { it.property.call(block)!! } == it.value + } + } + + // If this happens, we did something wrong with adding the attributes or with clamping integer attributes + if (index == -1) throw Error("The state of block (${block.material}) has no ID") + + return firstStateID + index + } + + private sealed class BlockAttribute { + abstract val property: KProperty1 + + data class Boolean(override val property: KProperty1) : BlockAttribute() + data class Int(override val property: KProperty1, val max: kotlin.Int) : + BlockAttribute() + + data class Enum(override val property: KProperty1>) : BlockAttribute>() + } + + companion object { + private fun generateStates(properties: List>): List>> { + if (properties.isEmpty()) return emptyList() + + val current = properties[0] + + val allowedValues: Array = when (current) { + is BlockAttribute.Boolean -> arrayOf(true, false) + is BlockAttribute.Int -> (0..current.max).toList().toTypedArray() + is BlockAttribute.Enum -> current.property.returnType.jvmErasure.java.enumConstants + } + + val rest = generateStates(properties.drop(1)) + return allowedValues.flatMap { value -> + if (rest.isEmpty()) listOf(arrayOf(KPropertyValuePair(current.property, value))) + else rest.map { arrayOf(KPropertyValuePair(current.property, value), *it) } + } + } + } +} diff --git a/blokk-api/src/main/kotlin/space/blokk/world/block/BlockRef.kt b/blokk-api/src/main/kotlin/space/blokk/world/block/BlockRef.kt deleted file mode 100644 index d3ec548..0000000 --- a/blokk-api/src/main/kotlin/space/blokk/world/block/BlockRef.kt +++ /dev/null @@ -1,51 +0,0 @@ -package space.blokk.world.block - -import space.blokk.world.BlockLocation -import space.blokk.world.Chunk -import space.blokk.world.World - -/** - * A reference to a block in a chunk of a world. - * - * There is always only one instance for every [location][BlockLocation] in a world. - */ -abstract class BlockRef { - /** - * The [location][BlockLocation] of this ref. - */ - abstract val location: BlockLocation - - /** - * The [World] containing this ref. - */ - abstract val world: World - - /** - * The [Chunk] containing this ref. - */ - val chunk: Chunk get() = world.getChunkAt(location) - - /** - * The current [Block] of this ref. - */ - abstract var block: Block; protected set - - /** - * Whether the chunk containing [block] is loaded. - */ - val loaded: Boolean get() = chunk.loaded - - /** - * Shorthand for [`chunk.load()`][Chunk.load]. - */ - suspend fun load() = chunk.load() - - /** - * Create a new [Block] of type [material] and assign it to this ref (i.e. place it in the world). - */ - abstract suspend fun place(material: Material) - - companion object { - fun of(world: World, location: BlockLocation) = world.getBlock(location) - } -} diff --git a/blokk-api/src/main/kotlin/space/blokk/world/block/DaylightDetector.kt b/blokk-api/src/main/kotlin/space/blokk/world/block/DaylightDetector.kt index 575fe74..bfdbda3 100644 --- a/blokk-api/src/main/kotlin/space/blokk/world/block/DaylightDetector.kt +++ b/blokk-api/src/main/kotlin/space/blokk/world/block/DaylightDetector.kt @@ -1,21 +1,13 @@ package space.blokk.world.block -import space.blokk.util.clamp - /** * Material: [DAYLIGHT_DETECTOR][Material.DAYLIGHT_DETECTOR] */ -class DaylightDetector( - ref: BlockRef -) : Block(ref) { - var inverted: Boolean = false - var power by clamp(0, 15) { 0 } - - companion object : Block.Companion() { - override val codec = Codec - .id(325, 6158) - .attribute(DaylightDetector::inverted) - .attribute(DaylightDetector::power, 15) - .build() - } +data class DaylightDetector( + @Attribute + val inverted: Boolean = false, + @Attribute(15) + val power: Int +) : Block() { + companion object : Meta by meta(1, 6158) } diff --git a/blokk-api/src/main/kotlin/space/blokk/world/block/Hopper.kt b/blokk-api/src/main/kotlin/space/blokk/world/block/Hopper.kt deleted file mode 100644 index 6a8d392..0000000 --- a/blokk-api/src/main/kotlin/space/blokk/world/block/Hopper.kt +++ /dev/null @@ -1,28 +0,0 @@ -package space.blokk.world.block - -/** - * Material: [HOPPER][Material.HOPPER] - */ -class Hopper( - ref: BlockRef -) : Block(ref) { - var enabled: Boolean = true - - var facing: Facing = Facing.DOWN - - enum class Facing { - DOWN, - NORTH, - SOUTH, - WEST, - EAST - } - - companion object : Block.Companion() { - override val codec = Codec - .id(328, 6192) - .attribute(Hopper::enabled) - .attribute(Hopper::facing) - .build() - } -} diff --git a/blokk-api/src/test/kotlin/space/blokk/world/block/BlockCodecTest.kt b/blokk-api/src/test/kotlin/space/blokk/world/block/BlockCodecTest.kt index 465a61e..5dab1d3 100644 --- a/blokk-api/src/test/kotlin/space/blokk/world/block/BlockCodecTest.kt +++ b/blokk-api/src/test/kotlin/space/blokk/world/block/BlockCodecTest.kt @@ -1,33 +1,17 @@ package space.blokk.world.block import org.junit.jupiter.api.Test -import space.blokk.world.BlockLocation -import space.blokk.world.World import strikt.api.expectThat import strikt.assertions.isEqualTo class BlockCodecTest { - private val ref = object : BlockRef() { - override val location: BlockLocation get() = error("") - override val world: World get() = error("") - override var block: Block = DaylightDetector(this) - - override suspend fun place(material: Material) = error("") - } - @Test fun `getStateID returns 6158 for DaylightDetector(inverted=true, power=0)`() { - val daylightDetector = ref.block as DaylightDetector - daylightDetector.inverted = true - daylightDetector.power = 0 - expectThat(DaylightDetector.codec.getStateID(daylightDetector)).isEqualTo(6158) + expectThat(DaylightDetector.codec.getStateID(DaylightDetector(true, 0))).isEqualTo(6158) } @Test fun `getStateID returns 6189 for DaylightDetector(inverted=false, power=15)`() { - val daylightDetector = ref.block as DaylightDetector - daylightDetector.inverted = false - daylightDetector.power = 15 - expectThat(DaylightDetector.codec.getStateID(daylightDetector)).isEqualTo(6189) + expectThat(DaylightDetector.codec.getStateID(DaylightDetector(false, 15))).isEqualTo(6189) } } diff --git a/buildSrc/src/main/kotlin/space/blokk/mdsp/FilesGenerator.kt b/buildSrc/src/main/kotlin/space/blokk/mdsp/FilesGenerator.kt index 70ed9d3..16e933a 100644 --- a/buildSrc/src/main/kotlin/space/blokk/mdsp/FilesGenerator.kt +++ b/buildSrc/src/main/kotlin/space/blokk/mdsp/FilesGenerator.kt @@ -82,34 +82,22 @@ class FilesGenerator(private val dataDir: File, private val outputDir: File, pri if (sourcesDir.resolve(filePathRelativeToSourceRoot).exists()) continue val type = TypeSpec.classBuilder(upperCamelName) + .apply { + if (block.get("states").asList().isNotEmpty()) { + addModifiers(KModifier.DATA) + primaryConstructor( + FunSpec.constructorBuilder().addParameter("PLACEHOLDER", Unit::class).build() + ) + addProperty(PropertySpec.builder("PLACEHOLDER", Unit::class).initializer("PLACEHOLDER").build()) + } + } .addKdoc("Material: [$upperUnderscoreName][%T]", specificMaterialType) .superclass(BLOCK_TYPE) - .primaryConstructor( - FunSpec.constructorBuilder() - .addParameter("ref", BLOCK_REF_TYPE) - .build() - ) - .addSuperclassConstructorParameter("ref") .addType( TypeSpec.companionObjectBuilder() - .superclass(BLOCK_TYPE.nestedClass("Companion")) - .addProperty( - PropertySpec.builder( - "codec", - ClassName(IMPORT_FOR_REMOVAL_PACKAGE_NAME, PROPERTY_TYPE_FOR_REMOVAL_NAME), - KModifier.OVERRIDE - ) - .initializer( - """ - %T - .id<${upperCamelName}>(${block.get("id").toInt()}, ${ - block.get("minStateId").toInt() - }) - // .attribute(${upperCamelName}::PROPERTY) - .build() - """.trimIndent(), BLOCK_CODEC_TYPE - ) - .build() + .addSuperinterface( + BLOCK_TYPE.nestedClass("Meta").parameterizedBy(ClassName(BLOCK_PACKAGE, upperCamelName)), + CodeBlock.of("meta(${block.get("id").toInt()}, ${block.get("minStateId").toInt()})") ) .build() ) @@ -120,9 +108,7 @@ class FilesGenerator(private val dataDir: File, private val outputDir: File, pri .addType(type) .build() .toString() - .replaceFirst("\n\nimport $IMPORT_FOR_REMOVAL_PACKAGE_NAME.$PROPERTY_TYPE_FOR_REMOVAL_NAME", "") - .replaceFirst(": $PROPERTY_TYPE_FOR_REMOVAL_NAME", "") - .replaceFirst("Block.Codec", "Codec") + .replaceFirst("Block.Meta", "Meta") outputDir.resolve(filePathRelativeToSourceRoot).writeText(fileContent) }