diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index cce665a..29c1699 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -22,6 +22,7 @@
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index efd1031..ea04d0a 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 35eb1dd..a12ee5a 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -2,5 +2,6 @@
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 7d3c6a2..f14cfea 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,14 +4,27 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -30,8 +43,14 @@
+
-
+
+
+
+
+
+
@@ -42,6 +61,8 @@
@@ -81,7 +102,8 @@
"RunOnceActivity.git.unshallow": "true",
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
"git-widget-placeholder": "master",
- "kotlin-language-version-configured": "true"
+ "kotlin-language-version-configured": "true",
+ "last_opened_file_path": "/home/william/Documents/GitHub/mc_terrain_java"
}
}
@@ -167,7 +189,15 @@
1759638464558
-
+
+
+ 1759731329848
+
+
+
+ 1759731329848
+
+
@@ -175,6 +205,7 @@
-
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 2d2def5..33fcf41 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -9,6 +9,11 @@ plugins {
}
val lwjglVersion = "3.3.6"
+repositories {
+ mavenCentral()
+ maven(url = "https://maven.seedfinding.com") // ✅ use url= keyword
+}
+
dependencies {
implementation(project(":utils"))
@@ -22,6 +27,13 @@ dependencies {
implementation("org.lwjgl:lwjgl-glfw:$lwjglVersion")
implementation("org.lwjgl:lwjgl-stb:$lwjglVersion")
+ implementation(project(":mc_terrain_java"))
+ implementation("com.seedfinding:mc_math:1.171.0")
+ implementation("com.seedfinding:mc_seed:1.171.1")
+ implementation("com.seedfinding:mc_core:1.192.1")
+ implementation("com.seedfinding:mc_noise:1.171.1")
+ implementation("com.seedfinding:mc_biome:1.171.1")
+
// Natives for all platforms
runtimeOnly("org.lwjgl:lwjgl:$lwjglVersion:natives-linux")
runtimeOnly("org.lwjgl:lwjgl:$lwjglVersion:natives-windows")
diff --git a/app/src/main/kotlin/App.kt b/app/src/main/kotlin/App.kt
index e317090..88fcde3 100644
--- a/app/src/main/kotlin/App.kt
+++ b/app/src/main/kotlin/App.kt
@@ -1,9 +1,7 @@
package dev.wbell.buildtopia.app
import dev.wbell.buildtopia.app.game.Game
-import org.joml.Matrix4f
import org.lwjgl.glfw.GLFW.*
-import org.lwjgl.opengl.GL11.*
import org.lwjgl.opengl.GL20.*
fun loadResource(path: String): String {
@@ -35,7 +33,7 @@ fun main() {
// Camera parameters
- var startTime = glfwGetTime()
+ val startTime = glfwGetTime()
var lastTime = startTime
//
//
diff --git a/app/src/main/kotlin/Resource_Pack_Loader/TextureAtlas/ClasspathResourceProvider/ClasspathResourceProvider.kt b/app/src/main/kotlin/Resource_Pack_Loader/TextureAtlas/ClasspathResourceProvider/ClasspathResourceProvider.kt
new file mode 100644
index 0000000..13734eb
--- /dev/null
+++ b/app/src/main/kotlin/Resource_Pack_Loader/TextureAtlas/ClasspathResourceProvider/ClasspathResourceProvider.kt
@@ -0,0 +1,49 @@
+package dev.wbell.buildtopia.app.Resource_Pack_Loader.TextureAtlas.ClasspathResourceProvider
+
+import dev.wbell.buildtopia.app.Resource_Pack_Loader.TextureAtlas.ResourceProvider
+import java.awt.image.BufferedImage
+import java.io.File
+import java.io.InputStream
+import java.util.jar.JarFile
+import javax.imageio.ImageIO
+
+class ClasspathResourceProvider(
+ private val basePath: String, // e.g. "/assets/Blocktopia/textures"
+ private val resourceOwner: Any // usually `this`
+) : ResourceProvider {
+
+ override fun listPngFiles(): List {
+ val uri = resourceOwner.javaClass.getResource(basePath)?.toURI() ?: return emptyList()
+
+ return when {
+ uri.scheme == "file" -> { // Running unpacked (IDE/dev mode)
+ println(File(uri).walkTopDown()
+ .filter { it.extension == "png" }
+ .map { it.relativeTo(File(uri)).path.replace("\\", "/") }
+ .toList())
+ File(uri).walkTopDown()
+ .filter { it.extension == "png" }
+ .map { it.relativeTo(File(uri)).path.replace("\\", "/") }
+ .toList()
+ }
+
+ uri.scheme == "jar" -> { // Running from packaged .jar
+ val jarPath = uri.schemeSpecificPart.substringBefore("!")
+ .removePrefix("file:")
+ val jar = JarFile(jarPath)
+ val entries = jar.entries().toList()
+ val prefix = basePath.removePrefix("/").trimEnd('/') + "/"
+ entries
+ .filter { it.name.startsWith(prefix) && it.name.endsWith(".png") }
+ .map { it.name.removePrefix(prefix) }
+ }
+
+ else -> emptyList()
+ }
+ }
+
+ override fun loadImage(path: String): BufferedImage? {
+ val stream: InputStream? = resourceOwner.javaClass.getResourceAsStream("$basePath/$path")
+ return stream?.use { ImageIO.read(it) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/Resource_Pack_Loader/TextureAtlas/FileSystemProvider/FileResourceProvider.kt b/app/src/main/kotlin/Resource_Pack_Loader/TextureAtlas/FileSystemProvider/FileResourceProvider.kt
new file mode 100644
index 0000000..6a351fd
--- /dev/null
+++ b/app/src/main/kotlin/Resource_Pack_Loader/TextureAtlas/FileSystemProvider/FileResourceProvider.kt
@@ -0,0 +1,19 @@
+package dev.wbell.buildtopia.app.Resource_Pack_Loader.TextureAtlas.FileSystemProvider
+
+import dev.wbell.buildtopia.app.Resource_Pack_Loader.TextureAtlas.ResourceProvider
+import java.awt.image.BufferedImage
+import java.io.File
+import javax.imageio.ImageIO
+
+class FileResourceProvider(private val rootPath: String) : ResourceProvider {
+ override fun listPngFiles(): List =
+ File(rootPath).walkTopDown()
+ .filter { it.extension == "png" }
+ .map { it.relativeTo(File(rootPath)).path.replace("\\", "/") }
+ .toList()
+
+ override fun loadImage(path: String): BufferedImage? {
+ val file = File("$rootPath/$path")
+ return if (file.exists()) ImageIO.read(file) else null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/Resource_Pack_Loader/TextureAtlas/TextureAtlas.kt b/app/src/main/kotlin/Resource_Pack_Loader/TextureAtlas/TextureAtlas.kt
new file mode 100644
index 0000000..7a28820
--- /dev/null
+++ b/app/src/main/kotlin/Resource_Pack_Loader/TextureAtlas/TextureAtlas.kt
@@ -0,0 +1,133 @@
+package dev.wbell.buildtopia.app.Resource_Pack_Loader.TextureAtlas
+
+import org.joml.Vector2f
+import org.lwjgl.BufferUtils
+import java.awt.image.BufferedImage
+import kotlin.math.ceil
+import kotlin.math.sqrt
+import org.lwjgl.opengl.GL11.*
+import java.awt.Image
+
+interface ResourceProvider {
+ fun listPngFiles(): List // e.g. "block/grass_block.png"
+ fun loadImage(path: String): BufferedImage?
+}
+
+class TextureAtlas(
+ private val resources: ResourceProvider,
+ private val tileSize: Int = 16
+) {
+ data class UV(val topLeft: Vector2f, val bottomRight: Vector2f)
+
+ val textureMap = mutableMapOf()
+ var textureId = 0
+ private set
+ private var atlasWidth = 0
+ private var atlasHeight = 0
+ private val missingTexture = createMissingTexture()
+
+ fun buildAtlas() {
+ val files = resources.listPngFiles()
+ val count = files.size
+ val padding = 1 // 1 pixel padding around each tile
+ val paddedTileSize = tileSize + 2 * padding
+ val gridSize = ceil(sqrt(count.toDouble() + 1)).toInt()
+ atlasWidth = gridSize * paddedTileSize
+ atlasHeight = gridSize * paddedTileSize
+
+ val atlasImage = BufferedImage(atlasWidth, atlasHeight, BufferedImage.TYPE_INT_ARGB)
+ val g = atlasImage.createGraphics()
+
+ var index = 0
+ for (path in files) {
+ val image = resources.loadImage(path) ?: missingTexture
+
+ // Scale to tile size
+ val scaled = image.getScaledInstance(tileSize, tileSize, Image.SCALE_SMOOTH)
+ val finalImage = BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB)
+ val g2 = finalImage.createGraphics()
+ g2.drawImage(scaled, 0, 0, null)
+ g2.dispose()
+
+ val gridX = index % gridSize
+ val gridY = index / gridSize
+ val x = gridX * paddedTileSize
+ val y = gridY * paddedTileSize
+
+ // Draw the main tile in the center of the padded area
+ g.drawImage(finalImage, x + padding, y + padding, null)
+
+ // Duplicate edges for padding (top, bottom, left, right)
+ // left/right columns
+ g.drawImage(finalImage.getSubimage(0, 0, 1, tileSize), x, y + padding, null) // left
+ g.drawImage(finalImage.getSubimage(tileSize - 1, 0, 1, tileSize), x + padding + tileSize, y + padding, null) // right
+ // top/bottom rows
+ g.drawImage(finalImage.getSubimage(0, 0, tileSize, 1), x + padding, y, null) // top
+ g.drawImage(finalImage.getSubimage(0, tileSize - 1, tileSize, 1), x + padding, y + padding + tileSize, null) // bottom
+ // corners
+ g.drawImage(finalImage.getSubimage(0, 0, 1, 1), x, y, null) // top-left
+ g.drawImage(finalImage.getSubimage(tileSize - 1, 0, 1, 1), x + padding + tileSize, y, null) // top-right
+ g.drawImage(finalImage.getSubimage(0, tileSize - 1, 1, 1), x, y + padding + tileSize, null) // bottom-left
+ g.drawImage(finalImage.getSubimage(tileSize - 1, tileSize - 1, 1, 1), x + padding + tileSize, y + padding + tileSize, null) // bottom-right
+
+ // Compute UVs only for inner tile area
+ val u1 = (x + padding).toFloat() / atlasWidth
+ val v1 = (y + padding).toFloat() / atlasHeight
+ val u2 = (x + padding + tileSize).toFloat() / atlasWidth
+ val v2 = (y + padding + tileSize).toFloat() / atlasHeight
+ textureMap[path] = UV(Vector2f(u1, v1), Vector2f(u2, v2))
+
+ index++
+ }
+
+ // Add missing texture the same way
+ val gridX = index % gridSize
+ val gridY = index / gridSize
+ val x = gridX * paddedTileSize
+ val y = gridY * paddedTileSize
+ g.drawImage(missingTexture, x + padding, y + padding, null)
+
+ val u1 = (x + padding).toFloat() / atlasWidth
+ val v1 = (y + padding).toFloat() / atlasHeight
+ val u2 = (x + padding + tileSize).toFloat() / atlasWidth
+ val v2 = (y + padding + tileSize).toFloat() / atlasHeight
+ textureMap["missing"] = UV(Vector2f(u1, v1), Vector2f(u2, v2))
+
+ g.dispose()
+ uploadToGL(atlasImage)
+ }
+
+ private fun createMissingTexture(): BufferedImage {
+ val img = BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB)
+ val magenta = 0xFFFF00FF.toInt()
+ val black = 0xFF000000.toInt()
+ for (y in 0 until tileSize)
+ for (x in 0 until tileSize)
+ img.setRGB(x, y, if (((x / (tileSize / 2)) + (y / (tileSize / 2))) % 2 == 0) magenta else black)
+ return img
+ }
+
+ private fun uploadToGL(image: BufferedImage) {
+ val pixels = IntArray(image.width * image.height)
+ image.getRGB(0, 0, image.width, image.height, pixels, 0, image.width)
+ val buffer = BufferUtils.createByteBuffer(image.width * image.height * 4)
+ for (y in 0 until image.height) {
+ for (x in 0 until image.width) {
+ val p = pixels[y * image.width + x]
+ buffer.put(((p shr 16) and 0xFF).toByte())
+ buffer.put(((p shr 8) and 0xFF).toByte())
+ buffer.put((p and 0xFF).toByte())
+ buffer.put(((p shr 24) and 0xFF).toByte())
+ }
+ }
+ buffer.flip()
+ textureId = glGenTextures()
+ glBindTexture(GL_TEXTURE_2D, textureId)
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image.width, image.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, buffer)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
+ glBindTexture(GL_TEXTURE_2D, 0)
+ }
+
+ fun getUV(path: String): UV = textureMap[path] ?: textureMap["missing"]!!//UV(Vector2f(0f,0f), Vector2f(1f,1f))//
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/game/Game.kt b/app/src/main/kotlin/game/Game.kt
index c42b9ff..98dac29 100644
--- a/app/src/main/kotlin/game/Game.kt
+++ b/app/src/main/kotlin/game/Game.kt
@@ -1,13 +1,16 @@
package dev.wbell.buildtopia.app.game
+import dev.wbell.buildtopia.app.Resource_Pack_Loader.TextureAtlas.ClasspathResourceProvider.ClasspathResourceProvider
+import dev.wbell.buildtopia.app.Resource_Pack_Loader.TextureAtlas.TextureAtlas
import dev.wbell.buildtopia.app.createShader
import dev.wbell.buildtopia.app.game.session.Session
import dev.wbell.buildtopia.app.game.session.world.World
+import dev.wbell.buildtopia.app.game.session.world.chunk.Block.BlockRegistry
+import dev.wbell.buildtopia.app.game.session.world.chunk.Block.BlockType
import dev.wbell.buildtopia.app.game.session.world.player.Player
import dev.wbell.buildtopia.app.loadResource
import org.joml.Matrix4f
import org.joml.Vector3d
-import org.lwjgl.BufferUtils
import org.lwjgl.glfw.GLFW.*
import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL20.glGetUniformLocation
@@ -15,46 +18,8 @@ import org.lwjgl.opengl.GL20.glUseProgram
import org.lwjgl.system.MemoryUtil.NULL
import org.lwjgl.opengl.GL13.*
import org.lwjgl.opengl.GL20.glUniformMatrix4fv
-import org.lwjgl.stb.STBImage
-import java.nio.IntBuffer
import kotlin.math.PI
-
-fun loadTexture(path: String): Int {
- val textureId = glGenTextures()
- glBindTexture(GL_TEXTURE_2D, textureId)
-
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
-
-
- // Load image from resources
- val stream = object {}.javaClass.getResourceAsStream(path)
- ?: throw RuntimeException("Failed to load texture: $path")
-
- // Read stream into a ByteBuffer
- val bytes = stream.readBytes()
- val buffer = BufferUtils.createByteBuffer(bytes.size)
- buffer.put(bytes)
- buffer.flip()
-
- val widthBuffer: IntBuffer = BufferUtils.createIntBuffer(1)
- val heightBuffer: IntBuffer = BufferUtils.createIntBuffer(1)
- val channelsBuffer: IntBuffer = BufferUtils.createIntBuffer(1)
-
- val image = STBImage.stbi_load_from_memory(buffer, widthBuffer, heightBuffer, channelsBuffer, 4)
- ?: throw RuntimeException("Failed to load texture: ${STBImage.stbi_failure_reason()}")
-
- val width = widthBuffer.get(0)
- val height = heightBuffer.get(0)
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image)
- STBImage.stbi_image_free(image)
-
- return textureId
-}
-
object Game {
val session = Session()
var width = 854
@@ -67,7 +32,7 @@ object Game {
var lastY: Double? = null
}
- var textureId = 0
+ val block_atlas = TextureAtlas(ClasspathResourceProvider("/assets/Blocktopia/textures/block", this))
var shaderProgram = 0
var modelLoc = 0
@@ -91,7 +56,12 @@ object Game {
}
fun init() {
- // Initialize GLFW
+ // Initialize GLF
+ BlockRegistry.register(BlockType("minecraft:air", "Air", "", transparent = true, solid = false))
+ println(BlockRegistry.getIndex("minecraft:air"))
+ BlockRegistry.register(BlockType("minecraft:stone", "Stone", "block/stone.png"))
+ BlockRegistry.register(BlockType("minecraft:dirt", "Dirt", "block/dirt.png", transparent = true))
+ BlockRegistry.register(BlockType("minecraft:glass_block", "Glass Block", "block/grass_block.png", transparent = true))
if (!glfwInit()) throw RuntimeException("Failed to initialize GLFW")
glfwWindowHint(GLFW_SAMPLES, 4)
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3)
@@ -109,8 +79,13 @@ object Game {
glCullFace(GL_BACK) // cull back faces
glFrontFace(GL_CCW) // counter-clockwise triangles are front-facing
glDepthFunc(GL_LESS)
+ glEnable(GL_BLEND)
+ glEnable(GL_SAMPLE_ALPHA_TO_COVERAGE);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
- textureId = loadTexture("/assets/Blocktopia/textures/block/diamond_ore.png")
+ block_atlas.buildAtlas()
// Simple shaders
val vertexShaderSrc = loadResource("/assets/Blocktopia/shaders/core/terrain.vsh")
@@ -123,7 +98,7 @@ object Game {
modelLoc = glGetUniformLocation(shaderProgram, "model")
viewLoc = glGetUniformLocation(shaderProgram, "view")
projLoc = glGetUniformLocation(shaderProgram, "projection")
- session.World = World(Player(Vector3d(0.0, 400.0, 0.0), 0f, 0f), session)
+ session.World = World(Player(Vector3d(0.0, 500.0, 0.0), 0f, 0f), session)
session.World!!.player.world = session.World
session.World!!.init()
diff --git a/app/src/main/kotlin/game/session/Server/InternalServer.kt b/app/src/main/kotlin/game/session/Server/InternalServer.kt
new file mode 100644
index 0000000..03fc5d4
--- /dev/null
+++ b/app/src/main/kotlin/game/session/Server/InternalServer.kt
@@ -0,0 +1,4 @@
+package dev.wbell.buildtopia.app.game.session.Server
+
+class InternalServer {
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/game/session/Server/Server.kt b/app/src/main/kotlin/game/session/Server/Server.kt
new file mode 100644
index 0000000..7db50bf
--- /dev/null
+++ b/app/src/main/kotlin/game/session/Server/Server.kt
@@ -0,0 +1,4 @@
+package dev.wbell.buildtopia.app.game.session.Server
+
+interface Server {
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/game/session/world/World.kt b/app/src/main/kotlin/game/session/world/World.kt
index 673e5be..f1592de 100644
--- a/app/src/main/kotlin/game/session/world/World.kt
+++ b/app/src/main/kotlin/game/session/world/World.kt
@@ -4,7 +4,7 @@ import OpenSimplex2S.OpenSimplex2S
import dev.wbell.buildtopia.app.game.Game
import dev.wbell.buildtopia.app.game.camera.Camera
import dev.wbell.buildtopia.app.game.session.Session
-import dev.wbell.buildtopia.app.game.session.world.chunk.Block.Block
+import dev.wbell.buildtopia.app.game.session.world.chunk.Block.BlockRegistry
import dev.wbell.buildtopia.app.game.session.world.chunk.BlockAccessor
import dev.wbell.buildtopia.app.game.session.world.chunk.Chunk
import dev.wbell.buildtopia.app.game.session.world.chunk.ChunkSection
@@ -16,14 +16,36 @@ import kotlinx.coroutines.*
import org.joml.Matrix4f
import org.joml.Vector2i
import org.joml.Vector3d
-import org.lwjgl.glfw.GLFW.*
+import org.lwjgl.glfw.GLFW.glfwGetTime
import org.lwjgl.opengl.GL20.*
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
-import java.util.concurrent.PriorityBlockingQueue
import kotlin.math.PI
+
+//fun main() {
+// val worldSeed: Long = 1L
+// val version = MCVersion.v1_16_5
+// val dimension = Dimension.OVERWORLD
+//
+// val biomeSource = BiomeSource.of(dimension, version, worldSeed)
+// val terrainGenerator = TerrainGenerator.of(biomeSource)
+// ?: throw IllegalStateException("Failed to create TerrainGenerator")
+//
+// val block: Optional? = terrainGenerator.getBlockAt(0, 0, 0)
+// val column = terrainGenerator.getColumnAt(0, 0)
+//
+// val surfaceGenHeight: Int = terrainGenerator.getFirstHeightInColumn(0, 0, TerrainGenerator.WORLD_SURFACE_WG)
+// val oceanGenHeight: Int = terrainGenerator.getFirstHeightInColumn(0, 0, TerrainGenerator.OCEAN_FLOOR_WG)
+// val surfaceBlockIn: Int = terrainGenerator.getHeightInGround(0, 0)
+// val surfaceBlockAbove: Int = terrainGenerator.getHeightOnGround(0, 0)
+//
+// println("Block at (0,0,0): $block")
+// println("Surface height: $surfaceGenHeight")
+// println("Ocean floor height: $oceanGenHeight")
+//}
+
fun floorDiv(a: Int, b: Int): Int {
// rounds down (towards -∞), unlike / which rounds toward 0
var div = a / b
@@ -50,11 +72,13 @@ class World(val player: Player, val session: Session) {
private var isActive = true
var lastTick = glfwGetTime()
var dayNightLoc = glGetUniformLocation(Game.shaderProgram, "dayNight")
- var dayNightTick = 0
+ var dayNightTick = 12000
+
+ val seed: Long = 1203223
- var seed: Long = 12345
fun init() {
+
startPhysics()
startChunkRenderer()
startChunkUpdateRenderer()
@@ -94,55 +118,56 @@ class World(val player: Player, val session: Session) {
chunksToRender.sortBy { (_, coords) -> (coords.x - centerChunk.x) * (coords.x - centerChunk.x) + (coords.y - centerChunk.y) * (coords.y - centerChunk.y) }
var i = 0
for ((key, coords) in chunksToRender) {
- var peak = -4* ChunkSection.SIZE
+ var peak = -4 * ChunkSection.SIZE
val blockData = Array(ChunkSection.LENGTH * ChunkSection.LENGTH * ChunkSection.LENGTH * 24) { i ->
val x = i % ChunkSection.LENGTH
val z = (i / ChunkSection.LENGTH) % ChunkSection.LENGTH
- val y = (i / (ChunkSection.LENGTH * ChunkSection.LENGTH)) - 4 * ChunkSection.LENGTH
+ val y = (i / (ChunkSection.LENGTH * ChunkSection.LENGTH)) - 0 * ChunkSection.LENGTH
- // world-space coordinates
+
+ // World coordinates
val worldX = coords.x * ChunkSection.LENGTH + x
val worldZ = coords.y * ChunkSection.LENGTH + z
val worldY = y
- // --- Biome / height noise ---
- val biomeFactor = OpenSimplex2S.noise2(seed + 1000, worldX * 0.01, worldZ * 0.01)
- val baseHeight = 64 + (biomeFactor * 8).toInt() // plains ~56-72
+ // ----- TERRAIN HEIGHT -----
+ val cont = OpenSimplex2S.noise2(seed + 1000, worldX * 0.0008, worldZ * 0.0008) // continents/ocean
+ val ridge = OpenSimplex2S.noise2(seed + 2000, worldX * 0.004, worldZ * 0.004) // mountains
+ val detail = OpenSimplex2S.noise2(seed + 3000, worldX * 0.02, worldZ * 0.02) // small variation
- // Hills / mountains
- val hillNoise = OpenSimplex2S.noise2(seed + 2000, worldX * 0.02, worldZ * 0.02)
- val hillHeight = (hillNoise * 16).toInt()
- val terrainHeight = baseHeight + hillHeight
+ // Blend noises together
+ val baseHeight = 64.0 + cont * 30.0
+ val ridgeHeight = ridge * ridge * 50.0 // squared for more contrast
+ val terrainHeight = (baseHeight + ridgeHeight + detail * 3.0).toInt()
- // --- Multi-octave 3D caves ---
+ // ----- CAVES -----
fun caveDensity(x: Double, y: Double, z: Double): Double {
- var density = 0.0
- var frequency = 0.02
- var amplitude = 1.0
- for (octave in 0 until 3) { // 3 octaves, can increase for more detail
- density += OpenSimplex2S.noise3_ImproveXY(seed + 3000 + octave * 1000, x * frequency, y * frequency, z * frequency) * amplitude
- frequency *= 2.0
- amplitude /= 2.0
+ var total = 0.0
+ var freq = 0.01
+ var amp = 1.0
+ for (octave in 0 until 4) {
+ total += OpenSimplex2S.noise3_ImproveXY(seed + 4000 + octave * 500, x * freq, y * freq, z * freq) * amp
+ freq *= 2.0
+ amp *= 0.5
}
- return density
+ return total
}
val density = caveDensity(worldX.toDouble(), worldY.toDouble(), worldZ.toDouble())
- // Optional: reduce cave openings near surface
- val heightFactor = ((terrainHeight - worldY).toDouble() / 30.0).coerceIn(0.0, 1.0)
- val isCave = density > 0.5 * heightFactor // adjust threshold for more/less caves
+ // Caves taper out near the surface
+ val heightFactor = ((terrainHeight - worldY).toDouble() / 40.0).coerceIn(0.0, 1.0)
+ val isCave = density > 0.6 * heightFactor
- // --- Set block ---
- val block: Block? = if (worldY <= terrainHeight && !isCave) {
+ // ----- BLOCK PLACEMENT -----
+ val block: Int = if (worldY <= terrainHeight && !isCave) {
when {
- worldY == terrainHeight -> Block()
- worldY >= terrainHeight - 3 -> Block()
- else -> Block()
+ worldY == terrainHeight -> BlockRegistry.getIndex("minecraft:grass_block")
+ worldY >= terrainHeight - 4 -> BlockRegistry.getIndex("minecraft:dirt")
+ else -> BlockRegistry.getIndex("minecraft:stone")
}
- } else null
-
- if (terrainHeight > peak) peak = terrainHeight
+ } else BlockRegistry.getIndex("minecraft:air")
+ if (block != 0 && y > peak) peak = y
block
}
chunks[key] = Chunk(
@@ -168,7 +193,7 @@ class World(val player: Player, val session: Session) {
for (chunk in chunks.values) {
if (chunk.toRenderMesh()) markChunkDirty(chunk)
}
- repeat(4) {
+ repeat(10) {
val next = chunkRenderQueue.poll() ?: return@repeat
val (chunk, _) = next
chunk.isQueuedForRender = false
@@ -190,7 +215,7 @@ class World(val player: Player, val session: Session) {
for (chunk in chunks.values) {
if (chunk.toUpdateMesh()) markChunkDirtyUpdate(chunk)
}
- repeat(4) {
+ repeat(10) {
val next = chunkUpdateRenderQueue.poll() ?: return@repeat
val (chunk, _) = next
chunk.isQueuedForRender = false
@@ -233,14 +258,14 @@ class World(val player: Player, val session: Session) {
val blocks = object : BlockAccessor {
- override operator fun get(x: Int, y: Int, z: Int): Block? {
+ override operator fun get(x: Int, y: Int, z: Int): Int {
return getChunk(floorDiv(x, ChunkSection.LENGTH), floorDiv(z, ChunkSection.LENGTH))?.blocks[floorMod(
x,
ChunkSection.LENGTH
- ), y, floorMod(z, ChunkSection.LENGTH)]
+ ), y, floorMod(z, ChunkSection.LENGTH)]?:0
}
- override operator fun set(x: Int, y: Int, z: Int, value: Block?) {
+ override operator fun set(x: Int, y: Int, z: Int, value: Int) {
getChunk(floorDiv(x, ChunkSection.LENGTH), floorDiv(z, ChunkSection.LENGTH))?.blocks[floorMod(
x,
ChunkSection.LENGTH
@@ -314,7 +339,7 @@ class World(val player: Player, val session: Session) {
(player.position.x / ChunkSection.LENGTH).toInt(),
(player.position.z / ChunkSection.LENGTH).toInt()
)
- val renderDistance = Settings.get(SettingKey.RENDERDISTANCE) ?: 32
+ val renderDistance = Settings[SettingKey.RENDERDISTANCE] ?: 32
// --- Unload distant chunks ---
val chunksToUnload = chunks.values.filter { chunk ->
@@ -333,7 +358,7 @@ class World(val player: Player, val session: Session) {
val chunkCoords = mutableListOf>()
for (dx in -renderDistance..renderDistance) {
for (dz in -renderDistance..renderDistance) {
- if (dx*dx + dz*dz > renderDistance*renderDistance) continue // circular radius
+ if (dx * dx + dz * dz > renderDistance * renderDistance) continue // circular radius
val chunkX = centerChunk.x + dx
val chunkZ = centerChunk.y + dz
val key = packChunkKey(chunkX, chunkZ)
@@ -345,7 +370,7 @@ class World(val player: Player, val session: Session) {
chunkCoords.sortBy { (_, coords) ->
val dx = coords.x - centerChunk.x
val dz = coords.y - centerChunk.y
- dx*dx + dz*dz
+ dx * dx + dz * dz
}
// --- Load/render chunks in order ---
diff --git a/app/src/main/kotlin/game/session/world/chunk/Block/Block.kt b/app/src/main/kotlin/game/session/world/chunk/Block/Block.kt
index 666ce7a..f10c89b 100644
--- a/app/src/main/kotlin/game/session/world/chunk/Block/Block.kt
+++ b/app/src/main/kotlin/game/session/world/chunk/Block/Block.kt
@@ -1,9 +1,33 @@
package dev.wbell.buildtopia.app.game.session.world.chunk.Block
+import dev.wbell.buildtopia.app.Resource_Pack_Loader.TextureAtlas.TextureAtlas
import dev.wbell.buildtopia.app.game.Game
import dev.wbell.buildtopia.app.game.session.world.chunk.ChunkSection
import org.joml.Vector2i
+data class BlockType(
+ val id: String,
+ val name: String,
+ val texture: String,
+ val transparent: Boolean = false,
+ val solid: Boolean = true,
+)
+
+object BlockRegistry {
+ private val blocksById = mutableMapOf()
+ private val idByIndex = mutableListOf() // optional compact storage
+
+ fun register( block: BlockType) {
+ if (block.id in blocksById) error("Block ID '${block.id}' already registered!")
+ blocksById[block.id] = block
+ idByIndex.add(block.id)
+ }
+
+ fun get(id: String): BlockType? = blocksById[id]
+ fun getByIndex(index: Int): BlockType? = blocksById[idByIndex.getOrNull(index)]
+ fun getIndex(id: String): Int = idByIndex.indexOf(id)
+}
+
fun getVertexLight(x: Int, y: Int, z: Int, chunk: Vector2i, offsets: List>): Pair {
var sunTotal = 0
var blockTotal = 0
@@ -25,7 +49,7 @@ fun getVertexLight(x: Int, y: Int, z: Int, chunk: Vector2i, offsets: List, x: Int, y: Int, z: Int, chunk: Vector2i, cubeScale: Float, face: String) {
+fun addFace(vertices: MutableList, x: Int, y: Int, z: Int, uv: TextureAtlas.UV, chunk: Vector2i, cubeScale: Float, face: String) {
val half = cubeScale / 2f
val lightR = 0f
@@ -51,13 +75,13 @@ fun addFace(vertices: MutableList, x: Int, y: Int, z: Int, chunk: Vector2
val (trR, trG) = getVertexLight(x, y, z, chunk, frontTopRight)
val (tlR, tlG) = getVertexLight(x, y, z, chunk, frontTopLeft)
vertices.addAll(listOf(
- x - half, y - half, z + half, 0f, 1f, blR, blG, // bottom-left
- x + half, y - half, z + half, 1f, 1f, brR, brG, // bottom-right
- x + half, y + half, z + half, 1f, 0f, trR, trG, // top-right
+ x - half, y - half, z + half, uv.topLeft.x, uv.bottomRight.y, blR, blG, // bottom-left
+ x + half, y - half, z + half, uv.bottomRight.x, uv.bottomRight.y, brR, brG, // bottom-right
+ x + half, y + half, z + half, uv.bottomRight.x, uv.topLeft.y, trR, trG, // top-right
- x + half, y + half, z + half, 1f, 0f, trR, trG,
- x - half, y + half, z + half, 0f, 0f, tlR, tlG, // top-left
- x - half, y - half, z + half, 0f, 1f, blR, blG,
+ x + half, y + half, z + half, uv.bottomRight.x, uv.topLeft.y, trR, trG,
+ x - half, y + half, z + half, uv.topLeft.x, uv.topLeft.y, tlR, tlG, // top-left
+ x - half, y - half, z + half, uv.topLeft.x, uv.bottomRight.y, blR, blG,
))}
// === BACK FACE ===
"back" -> {
@@ -80,13 +104,13 @@ fun addFace(vertices: MutableList, x: Int, y: Int, z: Int, chunk: Vector2
val (brR, brG) = getVertexLight(x, y, z, chunk, backBottomRight)
vertices.addAll(listOf(
- x - half, y - half, z - half, 1f, 1f, blR, blG,
- x - half, y + half, z - half, 1f, 0f, tlR, tlG,
- x + half, y + half, z - half, 0f, 0f, trR, trG,
+ x - half, y - half, z - half, uv.bottomRight.x, uv.bottomRight.y, blR, blG,
+ x - half, y + half, z - half, uv.bottomRight.x, uv.topLeft.y, tlR, tlG,
+ x + half, y + half, z - half, uv.topLeft.x, uv.topLeft.y, trR, trG,
- x + half, y + half, z - half, 0f, 0f, trR, trG,
- x + half, y - half, z - half, 0f, 1f, brR, brG,
- x - half, y - half, z - half, 1f, 1f, blR, blG,
+ x + half, y + half, z - half, uv.topLeft.x, uv.topLeft.y, trR, trG,
+ x + half, y - half, z - half, uv.topLeft.x, uv.bottomRight.y, brR, brG,
+ x - half, y - half, z - half, uv.bottomRight.x, uv.bottomRight.y, blR, blG,
))
}
@@ -111,13 +135,13 @@ fun addFace(vertices: MutableList, x: Int, y: Int, z: Int, chunk: Vector2
val (tbR, tbG) = getVertexLight(x, y, z, chunk, leftTopBack)
vertices.addAll(listOf(
- x - half, y - half, z - half, 0f, 1f, bbR, bbG,
- x - half, y - half, z + half, 1f, 1f, bfR, bfG,
- x - half, y + half, z + half, 1f, 0f, tfR, tfG,
+ x - half, y - half, z - half, uv.topLeft.x, uv.bottomRight.y, bbR, bbG,
+ x - half, y - half, z + half, uv.bottomRight.x, uv.bottomRight.y, bfR, bfG,
+ x - half, y + half, z + half, uv.bottomRight.x, uv.topLeft.y, tfR, tfG,
- x - half, y + half, z + half, 1f, 0f, tfR, tfG,
- x - half, y + half, z - half, 0f, 0f, tbR, tbG,
- x - half, y - half, z - half, 0f, 1f, bbR, bbG,
+ x - half, y + half, z + half, uv.bottomRight.x, uv.topLeft.y, tfR, tfG,
+ x - half, y + half, z - half, uv.topLeft.x, uv.topLeft.y, tbR, tbG,
+ x - half, y - half, z - half, uv.topLeft.x, uv.bottomRight.y, bbR, bbG,
))
}
@@ -142,13 +166,13 @@ fun addFace(vertices: MutableList, x: Int, y: Int, z: Int, chunk: Vector2
val (tbR, tbG) = getVertexLight(x, y, z, chunk, rightTopBack)
vertices.addAll(listOf(
- x + half, y - half, z - half, 1f, 1f, bbR, bbG,
- x + half, y + half, z + half, 0f, 0f, tfR, tfG,
- x + half, y - half, z + half, 0f, 1f, bfR, bfG,
+ x + half, y - half, z - half, uv.bottomRight.x, uv.bottomRight.y, bbR, bbG,
+ x + half, y + half, z + half, uv.topLeft.x, uv.topLeft.y, tfR, tfG,
+ x + half, y - half, z + half, uv.topLeft.x, uv.bottomRight.y, bfR, bfG,
- x + half, y + half, z + half, 0f, 0f, tfR, tfG,
- x + half, y - half, z - half, 1f, 1f, bbR, bbG,
- x + half, y + half, z - half, 1f, 0f, tbR, tbG,
+ x + half, y + half, z + half, uv.topLeft.x, uv.topLeft.y, tfR, tfG,
+ x + half, y - half, z - half, uv.bottomRight.x, uv.bottomRight.y, bbR, bbG,
+ x + half, y + half, z - half, uv.bottomRight.x, uv.topLeft.y, tbR, tbG,
))
}
@@ -173,13 +197,13 @@ fun addFace(vertices: MutableList, x: Int, y: Int, z: Int, chunk: Vector2
val (brR, brG) = getVertexLight(x, y, z, chunk, topBackRight)
vertices.addAll(listOf(
- x - half, y + half, z - half, 0f, 0f, blR, blG,
- x - half, y + half, z + half, 0f, 1f, flR, flG,
- x + half, y + half, z + half, 1f, 1f, frR, frG,
+ x - half, y + half, z - half, uv.topLeft.x, uv.topLeft.y, blR, blG,
+ x - half, y + half, z + half, uv.topLeft.x, uv.bottomRight.y, flR, flG,
+ x + half, y + half, z + half, uv.bottomRight.x, uv.bottomRight.y, frR, frG,
- x + half, y + half, z + half, 1f, 1f, frR, frG,
- x + half, y + half, z - half, 1f, 0f, brR, brG,
- x - half, y + half, z - half, 0f, 0f, blR, blG,
+ x + half, y + half, z + half, uv.bottomRight.x, uv.bottomRight.y, frR, frG,
+ x + half, y + half, z - half, uv.bottomRight.x, uv.topLeft.y, brR, brG,
+ x - half, y + half, z - half, uv.topLeft.x, uv.topLeft.y, blR, blG,
))
}
@@ -204,19 +228,15 @@ fun addFace(vertices: MutableList, x: Int, y: Int, z: Int, chunk: Vector2
val (brR, brG) = getVertexLight(x, y, z, chunk, bottomBackRight)
vertices.addAll(listOf(
- x - half, y - half, z - half, 1f, 0f, blR, blG,
- x + half, y - half, z + half, 0f, 1f, frR, frG,
- x - half, y - half, z + half, 1f, 1f, flR, flG,
+ x - half, y - half, z - half, uv.bottomRight.x, uv.topLeft.y, blR, blG,
+ x + half, y - half, z + half, uv.topLeft.x, uv.bottomRight.y, frR, frG,
+ x - half, y - half, z + half, uv.bottomRight.x, uv.bottomRight.y, flR, flG,
- x - half, y - half, z - half, 1f, 0f, blR, blG,
- x + half, y - half, z - half, 0f, 0f, brR, brG,
- x + half, y - half, z + half, 0f, 1f, frR, frG,
+ x - half, y - half, z - half, uv.bottomRight.x, uv.topLeft.y, blR, blG,
+ x + half, y - half, z - half, uv.topLeft.x, uv.topLeft.y, brR, brG,
+ x + half, y - half, z + half, uv.topLeft.x, uv.bottomRight.y, frR, frG,
))
}
}
}
-// @formatter:on
-
-
-class Block {
-}
\ No newline at end of file
+// @formatter:on
\ No newline at end of file
diff --git a/app/src/main/kotlin/game/session/world/chunk/Chunk.kt b/app/src/main/kotlin/game/session/world/chunk/Chunk.kt
index 6033488..b1c7d5b 100644
--- a/app/src/main/kotlin/game/session/world/chunk/Chunk.kt
+++ b/app/src/main/kotlin/game/session/world/chunk/Chunk.kt
@@ -2,7 +2,6 @@ package dev.wbell.buildtopia.app.game.session.world.chunk
import dev.wbell.buildtopia.app.game.Game
import dev.wbell.buildtopia.app.game.session.world.World
-import dev.wbell.buildtopia.app.game.session.world.chunk.Block.Block
import dev.wbell.buildtopia.app.game.session.world.chunk.Block.addFace
import dev.wbell.buildtopia.app.game.session.world.chunk.ChunkSection.Companion.LENGTH
import dev.wbell.buildtopia.app.game.session.world.floorDiv
@@ -24,8 +23,8 @@ import java.util.Collections
interface BlockAccessor {
- operator fun get(x: Int, y: Int, z: Int): Block?
- operator fun set(x: Int, y: Int, z: Int, value: Block?)
+ operator fun get(x: Int, y: Int, z: Int): Int
+ operator fun set(x: Int, y: Int, z: Int, value: Int)
}
interface LightAccessor {
@@ -33,7 +32,7 @@ interface LightAccessor {
operator fun set(x: Int, y: Int, z: Int, value: Int)
}
-class ChunkSection(val blockData: Array, val chunk: Chunk, val index: Int) {
+class ChunkSection(val blockData: Array, val chunk: Chunk, val index: Int) {
companion object {
const val LENGTH = 16
const val SIZE = LENGTH * LENGTH * LENGTH
@@ -58,7 +57,7 @@ class ChunkSection(val blockData: Array, val chunk: Chunk, val index: In
)
// Seed the queue with all blocks that have sunlight > 1
- var blocked = Array(LENGTH*LENGTH){1}
+ val blocked = Array(LENGTH*LENGTH){1}
var toBreak = false
for (y in LENGTH - 1 downTo 0) {
val worldY = y + index * LENGTH - chunk.ySectionsOffset * LENGTH
@@ -69,7 +68,7 @@ class ChunkSection(val blockData: Array, val chunk: Chunk, val index: In
val worldX = x + chunk.coords.x * LENGTH
val worldZ = z + chunk.coords.y * LENGTH
val light = sunLights[x, y, z]
- if (blocks[x,y,z] != null) {
+ if (blocks[x,y,z] != 0) {
blocked[posInBlocked] = 0
if (blocked.sum()==0) {
toBreak = true
@@ -102,7 +101,7 @@ class ChunkSection(val blockData: Array, val chunk: Chunk, val index: In
if (newLightLevel <= 0) continue
// skip if block is solid or light is already >= newLightLevel
- if (chunk.world.blocks[nx, ny, nz] != null) continue
+ if (chunk.world.blocks[nx, ny, nz] != 0) continue
if (chunk.world.sunLights[nx, ny, nz] >= newLightLevel) continue
// update light immediately and enqueue
@@ -339,13 +338,13 @@ class ChunkSection(val blockData: Array, val chunk: Chunk, val index: In
}
val blocks = object : BlockAccessor {
- override operator fun get(x: Int, y: Int, z: Int): Block? {
+ override operator fun get(x: Int, y: Int, z: Int): Int {
val index = x + z * LENGTH + y * LENGTH * LENGTH
- if ((x !in 0..= blockData.size) return
blockData[index] = value
@@ -455,7 +454,7 @@ class ChunkSection(val blockData: Array, val chunk: Chunk, val index: In
val vertexCount = vaoSize / 7 // <-- changed
// Bind texture
- glBindTexture(GL_TEXTURE_2D, Game.textureId)
+ glBindTexture(GL_TEXTURE_2D, Game.block_atlas.textureId)
glUniform1i(glGetUniformLocation(Game.shaderProgram, "texture1"), 0)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
@@ -485,59 +484,65 @@ class ChunkSection(val blockData: Array, val chunk: Chunk, val index: In
for (sectionY in 0..?,
+ var blockData: Array?,
val peak: Int
) {
var sections: Array = Array(numberOfSections) { sectionIndex ->
@@ -628,12 +633,12 @@ class Chunk(
}
val blocks = object : BlockAccessor {
- override operator fun get(x: Int, y: Int, z: Int): Block? {
- if (!loaded) return null
- return getChunkSection(y)?.blocks[x, floorMod(y, LENGTH), z]
+ override operator fun get(x: Int, y: Int, z: Int): Int {
+ if (!loaded) return 0
+ return getChunkSection(y)?.blocks[x, floorMod(y, LENGTH), z]?:0
}
- override operator fun set(x: Int, y: Int, z: Int, value: Block?) {
+ override operator fun set(x: Int, y: Int, z: Int, value: Int) {
if (!loaded) return
getChunkSection(y)?.blocks[x, floorMod(y, LENGTH), z] = value
}
diff --git a/app/src/main/kotlin/game/session/world/player/Player.kt b/app/src/main/kotlin/game/session/world/player/Player.kt
index 02a5d08..d4f5960 100644
--- a/app/src/main/kotlin/game/session/world/player/Player.kt
+++ b/app/src/main/kotlin/game/session/world/player/Player.kt
@@ -62,7 +62,7 @@ class Player(val position: Vector3d, var pitch: Float, var yaw: Float) {
for (y in minY..maxY) {
for (z in minZ..maxZ) {
val block = world!!.blocks[x, y, z]
- if (block != null) {
+ if (block != 0) {
boxes.add(
AABB(
x - 0.5, y.toDouble(), z - 0.5,
@@ -183,7 +183,12 @@ class Player(val position: Vector3d, var pitch: Float, var yaw: Float) {
}
fun getCamera(alpha: Double): Camera {
- val targetFovModifier = if (isSprinting) 1.1 else 1.0
+ var targetFovModifier = if (isSprinting) 1.1 else 1.0
+
+
+ if (glfwGetKey(Game.window!!, GLFW_KEY_C) == GLFW_PRESS) {
+ targetFovModifier = 0.2
+ }
// gradually move current toward target, just like Minecraft
fovModifierCurrent += (targetFovModifier - fovModifierCurrent) * 0.1
diff --git a/app/src/main/resources/assets/Blocktopia/shaders/core/terrain.fsh b/app/src/main/resources/assets/Blocktopia/shaders/core/terrain.fsh
index 8866850..95b5524 100644
--- a/app/src/main/resources/assets/Blocktopia/shaders/core/terrain.fsh
+++ b/app/src/main/resources/assets/Blocktopia/shaders/core/terrain.fsh
@@ -32,5 +32,6 @@ void main() {
vec3 factor = clamp(0.5 - light, 0.0, 1.0); // only affects darker areas
light = light + shadowLift * factor;
- FragColor = vec4(texColor.rgb * light, 1.0);
+ // Apply lighting to RGB but preserve alpha from texture
+ FragColor = vec4(texColor.rgb * light, texColor.a);
}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 85b4143..7db7ed0 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -22,5 +22,7 @@ plugins {
// Learn more about structuring projects with Gradle - https://docs.gradle.org/8.7/userguide/multi_project_builds.html
include(":app")
include(":utils")
+include("mc_terrain_java")
+project(":mc_terrain_java").projectDir = file("lib/mc_terrain_java")
rootProject.name = "Buildtopia"
\ No newline at end of file