Rename BlockRef to voxel and use annotations instead of BlockCodec.Builder
This commit is contained in:
parent
9f97b737c0
commit
dcbad8f5dc
12 changed files with 176 additions and 252 deletions
|
@ -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
|
||||
|
|
|
@ -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<ChunkSection>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
44
blokk-api/src/main/kotlin/space/blokk/world/Voxel.kt
Normal file
44
blokk-api/src/main/kotlin/space/blokk/world/Voxel.kt
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<Array<BlockRef>>> {
|
||||
): Array<Array<Array<Voxel>>> {
|
||||
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<BlockRef> {
|
||||
): Array<Voxel> {
|
||||
// https://www.reddit.com/r/VoxelGameDev/comments/2cttnt/how_to_create_a_sphere_out_of_voxels/
|
||||
TODO()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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.")
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<T : Block> private constructor(
|
||||
val blockClass: KClass<out T>,
|
||||
val id: Int,
|
||||
val firstStateID: Int,
|
||||
val states: List<Array<KPropertyValuePair<*>>>
|
||||
) {
|
||||
companion object {
|
||||
inline fun <reified T : Block> id(id: Int, firstStateID: Int) = id(T::class, id, firstStateID)
|
||||
fun <T : Block> id(blockClass: KClass<out T>, id: Int, firstStateID: Int) =
|
||||
Builder(blockClass, id, firstStateID)
|
||||
}
|
||||
|
||||
fun getStateID(block: T): Int {
|
||||
val values = mutableMapOf<KProperty<*>, 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<T : Block> internal constructor(
|
||||
private val blockClass: KClass<out T>,
|
||||
private val id: Int,
|
||||
private val firstStateID: Int
|
||||
) {
|
||||
private val attributes = mutableListOf<BlockAttribute<*>>()
|
||||
|
||||
@JvmName("booleanAttribute")
|
||||
fun attribute(property: KProperty<Boolean>) = this.apply {
|
||||
attributes.add(BlockAttribute.Boolean(property))
|
||||
}
|
||||
|
||||
@JvmName("enumAttribute")
|
||||
fun attribute(property: KProperty<Enum<*>>) = this.apply {
|
||||
attributes.add(BlockAttribute.Enum(property))
|
||||
}
|
||||
|
||||
@JvmName("intAttribute")
|
||||
fun attribute(property: KProperty<Int>, max: Int) = this.apply {
|
||||
attributes.add(BlockAttribute.Int(property, max))
|
||||
}
|
||||
|
||||
fun build() = Codec(blockClass, id, firstStateID, generateStates(attributes))
|
||||
|
||||
sealed class BlockAttribute<T : Any> {
|
||||
abstract val property: KProperty<T>
|
||||
|
||||
data class Boolean(override val property: KProperty<kotlin.Boolean>) : BlockAttribute<kotlin.Boolean>()
|
||||
data class Int(override val property: KProperty<kotlin.Int>, val max: kotlin.Int) :
|
||||
BlockAttribute<kotlin.Int>()
|
||||
|
||||
data class Enum(override val property: KProperty<kotlin.Enum<*>>) : BlockAttribute<kotlin.Enum<*>>()
|
||||
interface Meta<T : Block> {
|
||||
val blockClass: KClass<T>
|
||||
val codec: BlockCodec<T>
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun generateStates(properties: List<BlockAttribute<*>>): List<Array<KPropertyValuePair<*>>> {
|
||||
if (properties.isEmpty()) return emptyList()
|
||||
|
||||
val current = properties[0]
|
||||
|
||||
val allowedValues: Array<out Any> = 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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<T : Block> constructor(
|
||||
blockClass: KClass<T>,
|
||||
val id: Int,
|
||||
val firstStateID: Int
|
||||
) {
|
||||
val states: List<Array<KPropertyValuePair<*>>> =
|
||||
generateStates(blockClass.declaredMemberProperties.mapNotNull { property ->
|
||||
val annotation = property.findAnnotation<Attribute>() ?: return@mapNotNull null
|
||||
val type = property.returnType
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when {
|
||||
type == Int::class.starProjectedType -> BlockAttribute.Int(
|
||||
(property as KProperty1<Block, Int>),
|
||||
annotation.max
|
||||
)
|
||||
type == Boolean::class.starProjectedType -> BlockAttribute.Boolean(property as KProperty1<Block, Boolean>)
|
||||
type.isSubtypeOf(Enum::class.starProjectedType) -> BlockAttribute.Enum(property as KProperty1<Block, Enum<*>>)
|
||||
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<KProperty<*>, 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<T : Any> {
|
||||
abstract val property: KProperty1<Block, T>
|
||||
|
||||
data class Boolean(override val property: KProperty1<Block, kotlin.Boolean>) : BlockAttribute<kotlin.Boolean>()
|
||||
data class Int(override val property: KProperty1<Block, kotlin.Int>, val max: kotlin.Int) :
|
||||
BlockAttribute<kotlin.Int>()
|
||||
|
||||
data class Enum(override val property: KProperty1<Block, kotlin.Enum<*>>) : BlockAttribute<kotlin.Enum<*>>()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun generateStates(properties: List<BlockAttribute<*>>): List<Array<KPropertyValuePair<*>>> {
|
||||
if (properties.isEmpty()) return emptyList()
|
||||
|
||||
val current = properties[0]
|
||||
|
||||
val allowedValues: Array<out Any> = 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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<DaylightDetector>(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<DaylightDetector> by meta(1, 6158)
|
||||
}
|
||||
|
|
|
@ -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<Hopper>(328, 6192)
|
||||
.attribute(Hopper::enabled)
|
||||
.attribute(Hopper::facing)
|
||||
.build()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Reference in a new issue