diff --git a/blokk-api/build.gradle.kts b/blokk-api/build.gradle.kts index f124d23..19a5cf2 100644 --- a/blokk-api/build.gradle.kts +++ b/blokk-api/build.gradle.kts @@ -40,6 +40,10 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}") } +kotlin { + sourceSets["main"].kotlin.srcDir("src/main/generatedKotlin") +} + tasks { compileKotlin { kotlinOptions.jvmTarget = "1.8" diff --git a/blokk-api/src/main/kotlin/space/blokk/plugin/Plugin.kt b/blokk-api/src/main/kotlin/space/blokk/plugin/Plugin.kt index fdba348..ce2149c 100644 --- a/blokk-api/src/main/kotlin/space/blokk/plugin/Plugin.kt +++ b/blokk-api/src/main/kotlin/space/blokk/plugin/Plugin.kt @@ -1,6 +1,9 @@ package space.blokk.plugin interface Plugin { + fun onEnable() {} + fun onDisable() {} + companion object { fun getCalling(): Plugin? { // TODO: Return the last plugin in the stacktrace 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 7199f6f..e87c3ce 100644 --- a/blokk-api/src/main/kotlin/space/blokk/world/Chunk.kt +++ b/blokk-api/src/main/kotlin/space/blokk/world/Chunk.kt @@ -1,7 +1,7 @@ package space.blokk.world import kotlinx.coroutines.CoroutineScope -import space.blokk.world.block.Block +import space.blokk.world.block.BlockRef interface Chunk { /** @@ -9,6 +9,8 @@ interface Chunk { */ val key: Key + data class Key(val x: Int, val z: Int) + /** * [CoroutineScope] of this chunk. * @@ -16,24 +18,35 @@ interface Chunk { */ val scope: CoroutineScope - data class Key(val x: Int, val z: Int) - /** - * The [world][World] this chunk belongs to. + * The [World] this chunk belongs to. */ val world: World + // TODO: Allow more than 16 sections + /** + * The 16 [ChunkSection]s this chunk consists of. + */ val sections: Array /** - * Gets a block in this chunk. + * Whether this chunk must be loaded using [load] before it can be accessed. + * + * This value should not change for the lifetime of a chunk. + */ + val mustBeLoaded: Boolean + + /** + * Returns the [BlockRef] for the specified coordinates in this chunk. * * **The coordinates are relative to this chunk, not to the entire world.** */ - fun getBlock(x: Byte, y: Int, z: Byte): Block + fun getBlock(x: Byte, y: Int, z: Byte): BlockRef /** * Loads this chunk into memory. + * + * If the implementation does not need to load chunks, this method should do nothing. */ suspend fun load() 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 1abf0e4..1b7b94a 100644 --- a/blokk-api/src/main/kotlin/space/blokk/world/World.kt +++ b/blokk-api/src/main/kotlin/space/blokk/world/World.kt @@ -3,10 +3,13 @@ package space.blokk.world import kotlinx.coroutines.CoroutineScope import space.blokk.CoordinatePartOrder import space.blokk.entity.Entity -import space.blokk.world.block.Block +import space.blokk.world.block.BlockRef /** * 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.** */ abstract class World { /** @@ -38,6 +41,8 @@ abstract class World { /** * Gets the chunk key for the chunk containing the block at [location]. + * + * @public */ fun getChunkKeyAt(location: BlockLocation) = Chunk.Key(location.x / Chunk.WIDTH_AND_LENGTH, location.y / Chunk.WIDTH_AND_LENGTH) @@ -55,7 +60,7 @@ abstract class World { /** * @return The block at [location]. */ - abstract fun getBlock(location: BlockLocation): Block + abstract fun getBlock(location: BlockLocation): BlockRef /** * @param order The order of the arrays. @@ -66,7 +71,7 @@ abstract class World { firstCorner: BlockLocation, secondCorner: BlockLocation, order: CoordinatePartOrder = CoordinatePartOrder.DEFAULT - ): Array>> { + ): Array>> { val start = firstCorner.withLowestValues(secondCorner) val end = firstCorner.withHighestValues(secondCorner) @@ -85,7 +90,8 @@ abstract class World { 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/Block.kt b/blokk-api/src/main/kotlin/space/blokk/world/block/Block.kt index 2aae8ed..f716da2 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,27 +1,33 @@ package space.blokk.world.block -import space.blokk.world.BlockLocation -import space.blokk.world.Chunk -import space.blokk.world.World - /** - * Represents a block in a chunk of a world. - * - * There is always only one instance for every [location][BlockLocation] in a world. + * @param ref The [BlockRef] referencing this block. */ -interface Block { +abstract class Block internal constructor(val ref: BlockRef) { /** - * The [location][BlockLocation] of this block. + * The [Material] of this block. */ - val location: BlockLocation + abstract val material: Material /** - * The [World] containing this block. + * Whether [ref] still references this block. */ - val world: World + val destroyed: Boolean get() = ref.block === this /** - * The [Chunk] containing this block. + * Replaces this block with air. */ - val chunk: Chunk get() = world.getChunkAt(location) + fun replaceWithAir() { + checkNotDestroyed() + ref.place(Material.AIR) + } + + private fun checkNotDestroyed() { + if (destroyed) throw IllegalStateException("The block was destroyed.") + } + + /** + * Called when [ref] no longer references this block. + */ + internal abstract fun destroy() } diff --git a/blokk-api/src/main/kotlin/space/blokk/world/block/BlockContent.kt b/blokk-api/src/main/kotlin/space/blokk/world/block/BlockContent.kt deleted file mode 100644 index ef6b317..0000000 --- a/blokk-api/src/main/kotlin/space/blokk/world/block/BlockContent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package space.blokk.world.block - -interface BlockContent 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 new file mode 100644 index 0000000..6b0c8b6 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/world/block/BlockRef.kt @@ -0,0 +1,57 @@ +package space.blokk.world.block + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import space.blokk.world.BlockLocation +import space.blokk.world.Chunk +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. + * + * 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 + + private val placeMutex by lazy { Mutex() } + + /** + * Create a new [Block] and assign it to this ref (i.e. place it in the world). + */ + fun place(material: Material): Block = TODO() + + /** + * Create a new [Block] and assign it to this ref (i.e. place it in the world). + */ + suspend fun place(type: KClass): T = type.primaryConstructor!!.call(this).also { + placeMutex.withLock { + block.destroy() + block = it + } + } + + companion object { + fun of(world: World, location: BlockLocation) = world.getBlock(location) + } +} diff --git a/blokk-api/src/main/kotlin/space/blokk/world/block/BlockType.kt b/blokk-api/src/main/kotlin/space/blokk/world/block/BlockType.kt deleted file mode 100644 index 447fdb1..0000000 --- a/blokk-api/src/main/kotlin/space/blokk/world/block/BlockType.kt +++ /dev/null @@ -1,28 +0,0 @@ -package space.blokk.world.block - -import space.blokk.util.KClassToInstanceMap -import kotlin.reflect.KClass - -abstract class BlockType internal constructor( - val id: Int, - val name: String, - val hardness: Float, - val contentType: KClass, - val transparent: Boolean, - val emittedLight: Byte, - val filteredLight: Byte, - val collisionShape: Array>, - val maxStackSize: Byte -) { - private val extensions = KClassToInstanceMap() - - operator fun get(key: KClassToInstanceMap.Key): T = get(key.actualKey) - operator fun get(key: KClass): T = - extensions.getInstance(key) ?: throw ExtensionNotProvidedException() - - fun provideExtension(key: KClassToInstanceMap.Key, value: T) = extensions.putInstance(key, value) - - class ExtensionNotProvidedException : Exception("The specified extension is not provided for this BlockType") - - abstract class Extension -} diff --git a/blokk-api/src/main/kotlin/space/blokk/world/block/Stone.kt b/blokk-api/src/main/kotlin/space/blokk/world/block/Stone.kt deleted file mode 100644 index 8713c72..0000000 --- a/blokk-api/src/main/kotlin/space/blokk/world/block/Stone.kt +++ /dev/null @@ -1,4 +0,0 @@ -package space.blokk.world.block - -interface Stone : BlockContent { -} diff --git a/buildSrc/src/main/kotlin/space/blokk/mdsp/FilesGenerator.kt b/buildSrc/src/main/kotlin/space/blokk/mdsp/FilesGenerator.kt index 78ed86e..a2b4004 100644 --- a/buildSrc/src/main/kotlin/space/blokk/mdsp/FilesGenerator.kt +++ b/buildSrc/src/main/kotlin/space/blokk/mdsp/FilesGenerator.kt @@ -2,17 +2,20 @@ package space.blokk.mdsp import com.google.common.base.CaseFormat import com.jsoniter.JsonIterator -import com.jsoniter.any.Any import com.squareup.kotlinpoet.* import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import java.io.File typealias JsonAny = com.jsoniter.any.Any -class FilesGenerator(private val dataDir: File, private val outputDir: File) { +class FilesGenerator(private val dataDir: File, private val outputDir: File, private val sourcesDir: File) { companion object { const val API_PACKAGE = "space.blokk" const val BLOCK_PACKAGE = "space.blokk.world.block" + val MATERIAL_TYPE = ClassName(BLOCK_PACKAGE, "Material") + val BLOCK_TYPE = ClassName(BLOCK_PACKAGE, "Block") + val BLOCK_REF_TYPE = ClassName(BLOCK_PACKAGE, "BlockRef") + val K_CLASS_TYPE = ClassName("kotlin.reflect", "KClass") } fun generate() { @@ -24,10 +27,12 @@ class FilesGenerator(private val dataDir: File, private val outputDir: File) { val dataJson = dataDir.resolve("blocks.json").readText() val blocks = JsonIterator.deserialize(dataJson).asList() + + generateBlockStubs(blocks) generateMaterialEnum(blocks, collisionShapesData.get("blocks").asMap()) } - private fun generateCollisionShapeEnum(shapes: Map) { + private fun generateCollisionShapeEnum(shapes: Map) { val builder = TypeSpec.enumBuilder("CollisionShape") .primaryConstructor( FunSpec.constructorBuilder() @@ -38,6 +43,10 @@ class FilesGenerator(private val dataDir: File, private val outputDir: File) { ) .build() ) + .addKdoc( + "Explanation what the numbers mean: " + + "https://github.com/PrismarineJS/minecraft-data/blob/master/doc/blockCollisionShapes.md" + ) for (shape in shapes.toList().sortedBy { it.first.toInt() }) { builder.addEnumConstant(shape.first, TypeSpec.anonymousClassBuilder() @@ -55,21 +64,52 @@ class FilesGenerator(private val dataDir: File, private val outputDir: File) { .writeTo(outputDir) } - private fun generateMaterialEnum(blocks: List, collisionShapeKeyByBlockName: MutableMap) { + private fun generateBlockStubs(blocks: List) { + for (block in blocks) { + val lowerUnderscoreName = block.get("name").toString() + val upperCamelName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, lowerUnderscoreName) + val upperUnderscoreName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, lowerUnderscoreName) + val specificMaterialType = MATERIAL_TYPE.nestedClass(upperUnderscoreName) + + if (sourcesDir.resolve("./${BLOCK_PACKAGE.replace(".", "/")}/$upperCamelName.kt").exists()) + continue + + val type = TypeSpec.classBuilder(upperCamelName) + .addKdoc("A [$upperUnderscoreName][%T] block", specificMaterialType) + .addModifiers(KModifier.ABSTRACT) + .superclass(BLOCK_TYPE) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("ref", BLOCK_REF_TYPE) + .build() + ) + .addSuperclassConstructorParameter("ref") + .addProperty( + PropertySpec.builder("material", MATERIAL_TYPE, KModifier.OVERRIDE) + .initializer("%T", specificMaterialType) + .build() + ) + .build() + + FileSpec.builder(BLOCK_PACKAGE, upperCamelName) + .addComment("IF YOU CHANGE THIS FILE, MOVE IT FROM `generatedKotlin` TO `kotlin` OR IT WILL BE OVERWRITTEN.") + .addType(type) + .build() + .writeTo(outputDir) + } + } + + private fun generateMaterialEnum(blocks: List, collisionShapeKeyByBlockName: MutableMap) { val builder = TypeSpec.enumBuilder("Material") .primaryConstructor( FunSpec.constructorBuilder() .addParameter("id", Int::class) .addParameter("name", String::class) + .addParameter( + "blockType", + K_CLASS_TYPE.parameterizedBy(WildcardTypeName.producerOf(BLOCK_TYPE)) + ) .addParameter("hardness", Float::class) - // TODO: Uncomment when all block content interfaces exist -// .addParameter( -// "contentType", -// ClassName( -// "kotlin.reflect", -// "KClass" -// ).parameterizedBy(ClassName(BLOCK_PACKAGE, "BlockContent")) -// ) .addParameter("transparent", Boolean::class) .addParameter("emittedLight", Byte::class) .addParameter("filteredLight", Byte::class) @@ -83,10 +123,17 @@ class FilesGenerator(private val dataDir: File, private val outputDir: File) { val collisionShapeValue = collisionShapeKeyByBlockName[name]!! builder.addEnumConstant( - CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, block.get("name").toString()), + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, name), TypeSpec.anonymousClassBuilder() .addSuperclassConstructorParameter("%L", block.get("id").toInt()) .addSuperclassConstructorParameter("%S", name) + .addSuperclassConstructorParameter( + "%T::class", + ClassName( + BLOCK_PACKAGE, + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, name) + ) + ) .addSuperclassConstructorParameter("%Lf", block.get("hardness").toFloat()) .addSuperclassConstructorParameter("%L", block.get("transparent").toBoolean()) .addSuperclassConstructorParameter("%L", block.get("emitLight").toInt().toByte()) diff --git a/buildSrc/src/main/kotlin/space/blokk/mdsp/MinecraftDataSourcesPlugin.kt b/buildSrc/src/main/kotlin/space/blokk/mdsp/MinecraftDataSourcesPlugin.kt index 1fc7914..6ff3a96 100644 --- a/buildSrc/src/main/kotlin/space/blokk/mdsp/MinecraftDataSourcesPlugin.kt +++ b/buildSrc/src/main/kotlin/space/blokk/mdsp/MinecraftDataSourcesPlugin.kt @@ -23,7 +23,11 @@ class MinecraftDataSourcesPlugin : Plugin { project .project(":blokk-api") .projectDir - .resolve("src/main/generatedKotlin") + .resolve("src/main/generatedKotlin"), + project + .project(":blokk-api") + .projectDir + .resolve("src/main/kotlin") ).generate() } }