Archived
1
0
Fork 0

Rename BlockRef to voxel and use annotations instead of BlockCodec.Builder

This commit is contained in:
Moritz Ruth 2020-11-17 17:21:44 +01:00
parent 9f97b737c0
commit dcbad8f5dc
12 changed files with 176 additions and 252 deletions

View file

@ -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

View file

@ -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.

View 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)
}
}

View file

@ -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()
}

View file

@ -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
)

View file

@ -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<T : Block> {
val blockClass: KClass<T>
val codec: BlockCodec<T>
}
/**
* 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<*>>()
}
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) }
}
}
}
companion object {
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

@ -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) }
}
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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)
}