diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..a8bc72c1
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "ferricia"]
+ path = ferricia
+ url = https://github.com/AnvilloyDevStudio/TerraModulus-Ferricia-Engine
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 00000000..39152545
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 00000000..79ee123c
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index c2b27a04..e254cd93 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,10 +1,16 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/copyright.xml b/.idea/copyright/copyright.xml
new file mode 100644
index 00000000..a15ac165
--- /dev/null
+++ b/.idea/copyright/copyright.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 00000000..041ca056
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index bb449370..131e44d7 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 4b0bf0df..743f1bcc 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -4,5 +4,8 @@
-
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
index 713c8ba9..c81769a0 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,9 +2,8 @@
-
-
-
+
+
\ No newline at end of file
diff --git a/.idea/modules/src/client/TerraModulus.client.main.iml b/.idea/modules/src/client/TerraModulus.client.main.iml
deleted file mode 100644
index e2a94a0e..00000000
--- a/.idea/modules/src/client/TerraModulus.client.main.iml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules/src/client/TerraModulus.client.test.iml b/.idea/modules/src/client/TerraModulus.client.test.iml
new file mode 100644
index 00000000..4ac0bf1a
--- /dev/null
+++ b/.idea/modules/src/client/TerraModulus.client.test.iml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/src/common/TerraModulus.common.main.iml b/.idea/modules/src/common/TerraModulus.common.main.iml
deleted file mode 100644
index 33e4aae8..00000000
--- a/.idea/modules/src/common/TerraModulus.common.main.iml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules/src/common/TerraModulus.common.test.iml b/.idea/modules/src/common/TerraModulus.common.test.iml
new file mode 100644
index 00000000..5a56cf3d
--- /dev/null
+++ b/.idea/modules/src/common/TerraModulus.common.test.iml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/src/internal/client/TerraModulus.internal.client.main.iml b/.idea/modules/src/internal/client/TerraModulus.internal.client.main.iml
new file mode 100644
index 00000000..5c1a1cac
--- /dev/null
+++ b/.idea/modules/src/internal/client/TerraModulus.internal.client.main.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/src/kernel/client/TerraModulus.kernel.client.main.iml b/.idea/modules/src/kernel/client/TerraModulus.kernel.client.main.iml
new file mode 100644
index 00000000..15c47c5f
--- /dev/null
+++ b/.idea/modules/src/kernel/client/TerraModulus.kernel.client.main.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/src/server/TerraModulus.server.main.iml b/.idea/modules/src/server/TerraModulus.server.main.iml
deleted file mode 100644
index 57a9f4d3..00000000
--- a/.idea/modules/src/server/TerraModulus.server.main.iml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules/src/server/TerraModulus.server.test.iml b/.idea/modules/src/server/TerraModulus.server.test.iml
new file mode 100644
index 00000000..bc417dd5
--- /dev/null
+++ b/.idea/modules/src/server/TerraModulus.server.test.iml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 35eb1ddf..ba9deec3 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -2,5 +2,8 @@
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 46ab9473..79e43373 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,13 +1,18 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
- kotlin("jvm") version "2.1.0"
+ kotlin("jvm") version "2.1.20"
+ kotlin("plugin.serialization") version "2.1.20"
+ id("org.jetbrains.kotlinx.atomicfu") version "0.27.0"
application
}
+configure(listOf(project(":kernel:server"), project(":kernel:client"))) {
+ apply(plugin = "application")
+}
+
allprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
- apply(plugin = "application")
version = "0.1.0"
@@ -16,45 +21,118 @@ allprojects {
}
}
-subprojects {
- sourceSets.main {
- kotlin.srcDir("kotlin")
- resources.srcDir("resources")
- }
+configure(listOf(project(":kernel"), project(":internal"))) {
+ configure(listOf(project("common"), project("client"), project("server"))) {
+ sourceSets.main {
+ kotlin.srcDir("kotlin")
+ resources.srcDir("resources")
+ }
+
+ tasks.compileKotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
- tasks.compileKotlin {
- compilerOptions {
- jvmTarget.set(JvmTarget.JVM_21)
+ kotlin {
+ jvmToolchain(17)
}
}
- kotlin {
- jvmToolchain(21)
+ configure(listOf(project("client"), project("server"))) {
+ dependencies {
+ implementation(project("${parent!!.path}:common"))
+ }
}
}
-project(":common") {
- dependencies {
- implementation("org.jetbrains:annotations:26.0.2")
+project(":kernel") {
+ arrayOf("common", "client", "server").forEach {
+ project(it) {
+ dependencies {
+ implementation(project(":internal:$it"))
+ }
+ }
}
}
-project(":client") {
+project(":kernel:common") {
dependencies {
- implementation(project(":common"))
+ api("org.jetbrains:annotations:26.0.2")
+ api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
+ api("org.jetbrains.kotlinx:kotlinx-io-core:0.7.0")
+ api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
+ api("org.jetbrains.kotlinx:multik-core:0.2.3")
+ api("org.jetbrains.kotlinx:multik-default:0.2.3")
+ api(kotlin("reflect"))
+ api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
+ implementation("com.github.oshi:oshi-core:6.8.0")
+ api("com.google.errorprone:error_prone_annotations:2.38.0")
+ implementation("org.apache.logging.log4j:log4j-core:2.24.3")
+ implementation("org.apache.logging.log4j:log4j-api:2.24.3")
+ implementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.24.3")
+ implementation(platform("org.apache.logging.log4j:log4j-bom:2.24.3"))
+ annotationProcessor("org.apache.logging.log4j:log4j-core:2.24.3")
+ runtimeOnly("com.lmax:disruptor:4.0.0")
+ api("io.github.oshai:kotlin-logging-jvm:7.0.3")
+ implementation("net.sf.jopt-simple:jopt-simple:5.0.4")
}
+}
+
+project(":kernel:client").dependencies {
+ implementation("net.sf.jopt-simple:jopt-simple:5.0.4")
+}
+project(":kernel:server").dependencies {
+ implementation("net.sf.jopt-simple:jopt-simple:5.0.4")
+}
+
+project(":internal:common").dependencies {
+ implementation("net.java.dev.jna:jna:5.17.0")
+ implementation("net.java.dev.jna:jna-platform:5.17.0")
+}
+
+configure(listOf(project(":kernel:server"), project(":kernel:client"))) {
application {
- mainClass = "terramodulus.client.Main"
+ mainClass = "terramodulus.core.MainKt"
}
}
-project(":server") {
- dependencies {
- implementation(project(":common"))
+enum class Target {
+ CLIENT, SERVER;
+}
+
+/** Build Ferricia Engine with Cargo */
+fun Exec.configureCargoBuild(target: Target) {
+ workingDir = rootProject.file("ferricia")
+ commandLine("cargo", "build")
+ if (project.hasProperty("release")) args("--release") // use `-Prelease=true`
+ args("-F")
+ when (target) {
+ Target.CLIENT -> args("client")
+ Target.SERVER -> args("server")
}
+}
- application {
- mainClass = "terramodulus.server.Main"
+tasks.register("runClient") {
+ group = "application"
+ description = "Run client"
+ finalizedBy(":kernel:client:run")
+ configureCargoBuild(Target.CLIENT)
+}
+
+tasks.register("runServer") {
+ group = "application"
+ description = "Run server"
+ finalizedBy(":kernel:server:run")
+ configureCargoBuild(Target.SERVER)
+}
+
+configure(listOf(project(":kernel:server"), project(":kernel:client"))) {
+ tasks.named("run") {
+ jvmArgs("-Djava.library.path=${rootProject.file("ferricia/target/${
+ if (project.hasProperty("release")) "release" else "debug"
+ }").path}")
+ args("--screen-size", "800x500")
}
}
diff --git a/ferricia b/ferricia
new file mode 160000
index 00000000..f1c9c3a1
--- /dev/null
+++ b/ferricia
@@ -0,0 +1 @@
+Subproject commit f1c9c3a1dc12e65d25cc9f94069afd1f1d375edc
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 9ec2771e..48449515 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -11,7 +11,8 @@ plugins {
}
rootProject.name = "TerraModulus"
-include("common", "client", "server")
+include("internal", "kernel")
rootProject.children.forEach {
it.projectDir = File(settingsDir, "src/${it.name}")
+ include("${it.name}:common", "${it.name}:client", "${it.name}:server")
}
diff --git a/src/client/kotlin/terramodulus/client/Main.kt b/src/client/kotlin/terramodulus/client/Main.kt
deleted file mode 100644
index cf2c5455..00000000
--- a/src/client/kotlin/terramodulus/client/Main.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2025 Minicraft+ contributors
- * SPDX-License-Identifier: LGPL-3.0-only
- */
-
-package terramodulus.client
-
-class Main {
-
-}
diff --git a/src/internal/client/kotlin/terramodulus/engine/Canvas.kt b/src/internal/client/kotlin/terramodulus/engine/Canvas.kt
new file mode 100644
index 00000000..7b1d6a6d
--- /dev/null
+++ b/src/internal/client/kotlin/terramodulus/engine/Canvas.kt
@@ -0,0 +1,51 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.engine
+
+import terramodulus.engine.ferricia.Mui
+import terramodulus.engine.ferricia.Mui.clearCanvas
+import terramodulus.engine.ferricia.Mui.drawGuiGeo
+import terramodulus.engine.ferricia.Mui.drawGuiTex
+import terramodulus.engine.ferricia.Mui.dropCanvasHandle
+import terramodulus.engine.ferricia.Mui.geoShaders
+import terramodulus.engine.ferricia.Mui.getGLVersion
+import terramodulus.engine.ferricia.Mui.initCanvasHandle
+import terramodulus.engine.ferricia.Mui.loadImageToCanvas
+import terramodulus.engine.ferricia.Mui.setCanvasClearColor
+import terramodulus.engine.ferricia.Mui.texShaders
+import java.io.Closeable
+
+/**
+ * Manages the OpenGL viewport rendering as a "**canvas**"; managed by the GL context.
+ *
+ * This manages GL viewport in the SDL window and rendering in the viewport.
+ */
+class Canvas internal constructor(private val windowHandle: ULong) : Closeable {
+ private val canvasHandle = initCanvasHandle(windowHandle)
+ val glVersion = getGLVersion(windowHandle)
+
+ fun clear() = clearCanvas()
+
+ fun setClearColor(r: Float, g: Float, b: Float, a: Float) = setCanvasClearColor(r, g, b, a)
+
+ fun resizeGLViewport() = Mui.resizeGLViewport(windowHandle, canvasHandle)
+
+ fun loadImage(path: String) = loadImageToCanvas(canvasHandle, path)
+
+ fun loadGeoShaders(vsh: String, fsh: String) = geoShaders(vsh, fsh)
+
+ fun loadTexShaders(vsh: String, fsh: String) = texShaders(vsh, fsh)
+
+ fun renderGuiGeo(drawable: GeomDrawable, programHandle: ULong) =
+ drawGuiGeo(canvasHandle, drawable.handle, programHandle)
+
+ fun renderGuiTex(drawable: MeshDrawable, programHandle: ULong, textureHandle: UInt) =
+ drawGuiTex(canvasHandle, drawable.handle, programHandle, textureHandle)
+
+ override fun close() {
+ dropCanvasHandle(canvasHandle)
+ }
+}
diff --git a/src/internal/client/kotlin/terramodulus/engine/ColorFilter.kt b/src/internal/client/kotlin/terramodulus/engine/ColorFilter.kt
new file mode 100644
index 00000000..06b8bef7
--- /dev/null
+++ b/src/internal/client/kotlin/terramodulus/engine/ColorFilter.kt
@@ -0,0 +1,24 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.engine
+
+import terramodulus.engine.ferricia.Mui.editAlphaFilter
+import terramodulus.engine.ferricia.Mui.filterAlphaFilter
+
+@OptIn(ExperimentalUnsignedTypes::class)
+sealed class ColorFilter(handles: ULongArray) {
+ internal val handle: ULong = handles[0]
+ internal val wideHandle: ULong = handles[1]
+}
+
+@OptIn(ExperimentalUnsignedTypes::class)
+class AlphaFilter(alpha: Float) : ColorFilter(filterAlphaFilter(alpha)) {
+ var alpha = alpha
+ set(value) {
+ editAlphaFilter(handle, value)
+ field = value
+ }
+}
diff --git a/src/internal/client/kotlin/terramodulus/engine/Drawable.kt b/src/internal/client/kotlin/terramodulus/engine/Drawable.kt
new file mode 100644
index 00000000..870000dd
--- /dev/null
+++ b/src/internal/client/kotlin/terramodulus/engine/Drawable.kt
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.engine
+
+import terramodulus.engine.ferricia.Mui.addColorFilter
+import terramodulus.engine.ferricia.Mui.addModelTransform
+
+sealed class Drawable(internal val handle: ULong) {
+ fun add(model: ModelTransform) = addModelTransform(handle, model.wideHandle)
+
+ fun add(filter: ColorFilter) = addColorFilter(handle, filter.wideHandle)
+}
diff --git a/src/internal/client/kotlin/terramodulus/engine/GeomDrawable.kt b/src/internal/client/kotlin/terramodulus/engine/GeomDrawable.kt
new file mode 100644
index 00000000..b4da6865
--- /dev/null
+++ b/src/internal/client/kotlin/terramodulus/engine/GeomDrawable.kt
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.engine
+
+import terramodulus.engine.ferricia.Mui.newSimpleLineGeom
+import terramodulus.engine.ferricia.Mui.newSimpleRectGeom
+
+sealed class GeomDrawable(handle: ULong) : Drawable(handle) {
+}
+
+class SimpleLineGeom(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) :
+ GeomDrawable(newSimpleLineGeom(intArrayOf(x0, y0, x1, y1, r, g, b, a)))
+
+class SimpleRectGeom(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) :
+ GeomDrawable(newSimpleRectGeom(intArrayOf(x0, y0, x1, y1, r, g, b, a)))
diff --git a/src/internal/client/kotlin/terramodulus/engine/MeshDrawable.kt b/src/internal/client/kotlin/terramodulus/engine/MeshDrawable.kt
new file mode 100644
index 00000000..b60c82e4
--- /dev/null
+++ b/src/internal/client/kotlin/terramodulus/engine/MeshDrawable.kt
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.engine
+
+import terramodulus.engine.ferricia.Mui.newSpriteMesh
+
+sealed class MeshDrawable(handle: ULong) : Drawable(handle) {
+}
+
+class SpriteMesh(x0: Int, y0: Int, x1: Int, y1: Int) : MeshDrawable(newSpriteMesh(intArrayOf(x0, y0, x1, y1))) {
+
+}
diff --git a/src/internal/client/kotlin/terramodulus/engine/ModelTransform.kt b/src/internal/client/kotlin/terramodulus/engine/ModelTransform.kt
new file mode 100644
index 00000000..c7e5645b
--- /dev/null
+++ b/src/internal/client/kotlin/terramodulus/engine/ModelTransform.kt
@@ -0,0 +1,33 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.engine
+
+import terramodulus.engine.ferricia.Mui.modelFullScaling
+import terramodulus.engine.ferricia.Mui.modelSmartScaling
+
+@OptIn(ExperimentalUnsignedTypes::class)
+sealed class ModelTransform(handles: ULongArray) {
+ internal val handle: ULong = handles[0]
+ internal val wideHandle: ULong = handles[1]
+}
+
+@OptIn(ExperimentalUnsignedTypes::class)
+class SmartScaling private constructor(vararg args: Int) :
+ ModelTransform(modelSmartScaling(args)) {
+
+ companion object {
+ fun none(w: Int, h: Int) = SmartScaling(w, h, 0)
+
+ fun x(w: Int, h: Int, ww: Int, hh: Int) = SmartScaling(w, h, 1, ww, hh)
+
+ fun y(w: Int, h: Int, ww: Int, hh: Int) = SmartScaling(w, h, 2, ww, hh)
+
+ fun both(w: Int, h: Int, ww: Int, hh: Int) = SmartScaling(w, h, 3, ww, hh)
+ }
+}
+
+@OptIn(ExperimentalUnsignedTypes::class)
+class FullScaling(w: Int, h: Int) : ModelTransform(modelFullScaling(intArrayOf(w, h)))
diff --git a/src/internal/client/kotlin/terramodulus/engine/MuiEvent.kt b/src/internal/client/kotlin/terramodulus/engine/MuiEvent.kt
new file mode 100644
index 00000000..6f670c53
--- /dev/null
+++ b/src/internal/client/kotlin/terramodulus/engine/MuiEvent.kt
@@ -0,0 +1,76 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.engine
+
+sealed interface MuiEvent {
+ data class DisplayAdded(val displayHandle: Long) : MuiEvent
+ data class DisplayRemoved(val displayHandle: Long) : MuiEvent
+ data class DisplayMoved(val displayHandle: Long) : MuiEvent
+ data object WindowShown : MuiEvent
+ data object WindowHidden : MuiEvent
+ data object WindowExposed : MuiEvent
+ data class WindowMoved(val x: Int, val y: Int) : MuiEvent
+ data class WindowResized(val width: UInt, val height: UInt) : MuiEvent
+ data class WindowPixelSizeChanged(val width: UInt, val height: UInt) : MuiEvent
+ data object WindowMetalViewResized : MuiEvent
+ data object WindowMinimized : MuiEvent
+ data object WindowMaximized : MuiEvent
+ data object WindowRestored : MuiEvent
+ data object WindowMouseEnter : MuiEvent
+ data object WindowMouseLeave : MuiEvent
+ data object WindowFocusGained : MuiEvent
+ data object WindowFocusLost : MuiEvent
+ data object WindowCloseRequested : MuiEvent
+ data object WindowIccProfChanged : MuiEvent
+ data object WindowOccluded : MuiEvent
+ data object WindowEnterFullscreen : MuiEvent
+ data object WindowLeaveFullscreen : MuiEvent
+ data object WindowDestroyed : MuiEvent
+ data object WindowHdrStateChanged : MuiEvent
+ data class KeyboardKeyDown(val keyboardId: UInt, val key: UInt) : MuiEvent
+ data class KeyboardKeyUp(val keyboardId: UInt, val key: UInt) : MuiEvent
+ data class TextEditing(val text: String, val start: Int, val length: UInt) : MuiEvent
+ data class TextInput(val text: String) : MuiEvent
+ data object KeymapChanged : MuiEvent
+ data object KeyboardAdded : MuiEvent
+ data object KeyboardRemoved : MuiEvent
+ data object TextEditingCandidates : MuiEvent
+ data class MouseMotion(val mouseId: UInt, val x: Float, val y: Float) : MuiEvent
+ data class MouseButtonDown(val mouseId: UInt, val key: UByte) : MuiEvent
+ data class MouseButtonUp(val mouseId: UInt, val key: UByte) : MuiEvent
+ data class MouseWheel(val mouseId: UInt, val x: Float, val y: Float) : MuiEvent
+ data object MouseAdded : MuiEvent
+ data object MouseRemoved : MuiEvent
+ data class JoystickAxisMotion(val joystickId: UInt, val axis: UByte, val value: Short) : MuiEvent
+ data object JoystickBallMotion : MuiEvent
+ data class JoystickHatMotion(val joystickId: UInt, val hat: UByte, val value: UByte) : MuiEvent
+ data class JoystickButtonDown(val joystickId: UInt, val button: UByte) : MuiEvent
+ data class JoystickButtonUp(val joystickId: UInt, val button: UByte) : MuiEvent
+ data class JoystickAdded(val joystickId: UInt) : MuiEvent
+ data class JoystickRemoved(val joystickId: UInt) : MuiEvent
+ data object JoystickBatteryUpdated : MuiEvent
+ data class GamepadAxisMotion(val joystickId: UInt, val axis: UByte, val value: Short) : MuiEvent
+ data class GamepadButtonDown(val joystickId: UInt, val button: UByte) : MuiEvent
+ data class GamepadButtonUp(val joystickId: UInt, val button: UByte) : MuiEvent
+ data class GamepadAdded(val joystickId: UInt) : MuiEvent
+ data class GamepadRemoved(val joystickId: UInt) : MuiEvent
+ data class GamepadRemapped(val joystickId: UInt) : MuiEvent
+ data class GamepadTouchpadDown(val joystickId: UInt, val touchpad: Int, val finger: Int,
+ val x: Float, val y: Float, val pressure: Float) : MuiEvent
+ data class GamepadTouchpadMotion(val joystickId: UInt, val touchpad: Int, val finger: Int,
+ val x: Float, val y: Float, val pressure: Float) : MuiEvent
+ data class GamepadTouchpadUp(val joystickId: UInt, val touchpad: Int, val finger: Int,
+ val x: Float, val y: Float, val pressure: Float) : MuiEvent
+ data object GamepadSteamHandleUpdated : MuiEvent
+ data class DropFile(val filename: String) : MuiEvent
+ data class DropText(val text: String) : MuiEvent
+ data object DropBegin : MuiEvent
+ data object DropComplete : MuiEvent
+ data object DropPosition : MuiEvent
+ data object RenderTargetsReset : MuiEvent
+ data object RenderDeviceReset : MuiEvent
+ data object RenderDeviceLost : MuiEvent
+}
diff --git a/src/internal/client/kotlin/terramodulus/engine/Window.kt b/src/internal/client/kotlin/terramodulus/engine/Window.kt
new file mode 100644
index 00000000..969eb47b
--- /dev/null
+++ b/src/internal/client/kotlin/terramodulus/engine/Window.kt
@@ -0,0 +1,36 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.engine
+
+import terramodulus.engine.ferricia.Mui.dropSdlHandle
+import terramodulus.engine.ferricia.Mui.dropWindowHandle
+import terramodulus.engine.ferricia.Mui.initSdlHandle
+import terramodulus.engine.ferricia.Mui.initWindowHandle
+import terramodulus.engine.ferricia.Mui.resizeGLViewport
+import terramodulus.engine.ferricia.Mui.sdlPoll
+import terramodulus.engine.ferricia.Mui.showWindow
+import terramodulus.engine.ferricia.Mui.swapWindow
+import java.io.Closeable
+
+/**
+ * Manages the SDL window instance and the underlying GL context.
+ */
+class Window : Closeable {
+ private val sdlHandle = initSdlHandle()
+ private val windowHandle = initWindowHandle(sdlHandle)
+ val canvas = Canvas(windowHandle)
+
+ fun show() = showWindow(windowHandle)
+
+ fun swap() = swapWindow(windowHandle)
+
+ fun pollEvents() = sdlPoll(sdlHandle)
+
+ override fun close() {
+ dropWindowHandle(windowHandle)
+ dropSdlHandle(sdlHandle)
+ }
+}
diff --git a/src/internal/client/kotlin/terramodulus/engine/ferricia/Mui.kt b/src/internal/client/kotlin/terramodulus/engine/ferricia/Mui.kt
new file mode 100644
index 00000000..bae53762
--- /dev/null
+++ b/src/internal/client/kotlin/terramodulus/engine/ferricia/Mui.kt
@@ -0,0 +1,210 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.engine.ferricia
+
+import terramodulus.engine.MuiEvent
+
+@OptIn(ExperimentalUnsignedTypes::class)
+internal object Mui {
+ /**
+ * @return SDL handle pointer
+ */
+ @JvmName("initSdlHandle")
+ external fun initSdlHandle(): ULong
+
+ /**
+ * @param sdlHandle SDL handle pointer
+ */
+ @JvmName("dropSdlHandle")
+ external fun dropSdlHandle(sdlHandle: ULong)
+
+ /**
+ * @param sdlHandle SDL handle pointer
+ * @return window handle pointer
+ */
+ @JvmName("initWindowHandle")
+ external fun initWindowHandle(sdlHandle: ULong): ULong
+
+ /**
+ * @param windowHandle window handle pointer
+ */
+ @JvmName("dropWindowHandle")
+ external fun dropWindowHandle(windowHandle: ULong)
+
+ /**
+ * @param windowHandle window handle pointer
+ */
+ @JvmName("getGLVersion")
+ external fun getGLVersion(windowHandle: ULong): String
+
+ /**
+ * @param sdlHandle SDL handle pointer
+ * @return the list of all MUI events in this frame
+ */
+ @JvmName("sdlPoll")
+ external fun sdlPoll(sdlHandle: ULong): Array
+
+ /**
+ * @param windowHandle window handle pointer
+ */
+ @JvmName("resizeGLViewport")
+ external fun resizeGLViewport(windowHandle: ULong, canvasHandle: ULong)
+
+ /**
+ * @param windowHandle window handle pointer
+ */
+ @JvmName("showWindow")
+ external fun showWindow(windowHandle: ULong)
+
+ /**
+ * @param windowHandle window handle pointer
+ */
+ @JvmName("swapWindow")
+ external fun swapWindow(windowHandle: ULong)
+
+ /**
+ * @param windowHandle window handle pointer
+ * @return Canvas handle pointer
+ */
+ @JvmName("initCanvasHandle")
+ external fun initCanvasHandle(windowHandle: ULong): ULong
+
+ /**
+ * @param canvasHandle Canvas handle pointer
+ */
+ @JvmName("dropCanvasHandle")
+ external fun dropCanvasHandle(canvasHandle: ULong)
+
+ /**
+ * @param canvasHandle Canvas handle pointer
+ * @param path path to RGB image
+ * @return Texture ID
+ */
+ @JvmName("loadImageToCanvas")
+ external fun loadImageToCanvas(canvasHandle: ULong, path: String): UInt
+
+ @JvmName("clearCanvas")
+ external fun clearCanvas()
+
+ @JvmName("setCanvasClearColor")
+ external fun setCanvasClearColor(r: Float, g: Float, b: Float, a: Float)
+
+ /**
+ * @param vsh path to vector shader
+ * @param fsh path to fragment shader
+ * @return Geo Shader Program handle pointer
+ */
+ @JvmName("geoShaders")
+ external fun geoShaders(vsh: String, fsh: String): ULong
+
+ /**
+ * @param vsh path to vector shader
+ * @param fsh path to fragment shader
+ * @return Tex Shader Program handle pointer
+ */
+ @JvmName("texShaders")
+ external fun texShaders(vsh: String, fsh: String): ULong
+
+ /**
+ * @param data `[x0, y0, x1, y1, r, g, b, a]`
+ * @return SimpleLineGeom handle pointer
+ */
+ @JvmName("newSimpleLineGeom")
+ external fun newSimpleLineGeom(data: IntArray): ULong
+
+ /**
+ * @param data `[x0, y0, x1, y1, r, g, b, a]`
+ * @return SimpleRectGeom handle pointer
+ */
+ @JvmName("newSimpleRectGeom")
+ external fun newSimpleRectGeom(data: IntArray): ULong
+
+ /**
+ * @param data `[x0, y0, x1, y1]`
+ * @return SpriteMesh as DrawableSet handle pointer
+ */
+ @JvmName("newSpriteMesh")
+ external fun newSpriteMesh(data: IntArray): ULong
+
+ /**
+ * @param data `[w, h, param, w, h]`
+ * @return SmartScaling handle pointers
+ */
+ @JvmName("modelSmartScaling")
+ external fun modelSmartScaling(data: IntArray): ULongArray
+
+ /**
+ * @param data `[w, h]`
+ * @return FullScaling handle pointers
+ */
+ @JvmName("modelFullScaling")
+ external fun modelFullScaling(data: IntArray): ULongArray
+
+ /**
+ * @param data `[x, y]`
+ * @return SimpleTranslation handle pointers
+ */
+ @JvmName("modelSimpleTranslation")
+ external fun modelSimpleTranslation(data: FloatArray): ULongArray
+
+ /**
+ * @param data alpha
+ * @return AlphaFilter handle pointers
+ */
+ @JvmName("filterAlphaFilter")
+ external fun filterAlphaFilter(data: Float): ULongArray
+
+ /**
+ * @param filter AlphaFilter handle pointer
+ * @param data alpha
+ */
+ @JvmName("editAlphaFilter")
+ external fun editAlphaFilter(filter: ULong, data: Float)
+
+ /**
+ * @param drawableHandle DrawableSet handle pointer
+ * @param modelHandle Model Transform wide handle pointer
+ */
+ @JvmName("addModelTransform")
+ external fun addModelTransform(drawableHandle: ULong, modelHandle: ULong)
+
+ /**
+ * @param drawableHandle DrawableSet handle pointer
+ * @param modelHandle Model Transform wide handle pointer
+ */
+ @JvmName("removeModelTransform")
+ external fun removeModelTransform(drawableHandle: ULong, modelHandle: ULong)
+
+ /**
+ * @param drawableHandle DrawableSet handle pointer
+ * @param filterHandle Color Filter wide handle pointer
+ */
+ @JvmName("addColorFilter")
+ external fun addColorFilter(drawableHandle: ULong, filterHandle: ULong)
+
+ /**
+ * @param drawableHandle DrawableSet handle pointer
+ * @param filterHandle Color Filter wide handle pointer
+ */
+ @JvmName("removeColorFilter")
+ external fun removeColorFilter(drawableHandle: ULong, filterHandle: ULong)
+
+ /**
+ * @param canvasHandle Canvas handle pointer
+ * @param drawableHandle DrawableSet handle pointer
+ * @param programHandle Geo Shader Program handle pointer
+ */
+ @JvmName("drawGuiGeo")
+ external fun drawGuiGeo(canvasHandle: ULong, drawableHandle: ULong, programHandle: ULong)
+
+ /**
+ * @param canvasHandle Canvas handle pointer
+ * @param drawableHandle DrawableSet handle pointer
+ * @param programHandle Tex Shader Program handle pointer
+ */
+ @JvmName("drawGuiTex")
+ external fun drawGuiTex(canvasHandle: ULong, drawableHandle: ULong, programHandle: ULong, textureHandle: UInt)
+}
diff --git a/src/internal/client/kotlin/terramodulus/engine/ferricia/package-info.java b/src/internal/client/kotlin/terramodulus/engine/ferricia/package-info.java
new file mode 100644
index 00000000..74afb1ef
--- /dev/null
+++ b/src/internal/client/kotlin/terramodulus/engine/ferricia/package-info.java
@@ -0,0 +1,9 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+/**
+ * Contains direct bindings to the Ferricia Engine exposed JNI functions.
+ */
+package terramodulus.engine.ferricia;
diff --git a/src/internal/common/kotlin/terramodulus/engine/Core.kt b/src/internal/common/kotlin/terramodulus/engine/Core.kt
new file mode 100644
index 00000000..fbd31b6f
--- /dev/null
+++ b/src/internal/common/kotlin/terramodulus/engine/Core.kt
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.engine
+
+import terramodulus.engine.ferricia.Core
+import terramodulus.engine.ferricia.loadLibrary
+
+fun initEngine() {
+ loadLibrary()
+ Core.init()
+}
diff --git a/src/internal/common/kotlin/terramodulus/engine/ferricia/Core.kt b/src/internal/common/kotlin/terramodulus/engine/ferricia/Core.kt
new file mode 100644
index 00000000..62d5967e
--- /dev/null
+++ b/src/internal/common/kotlin/terramodulus/engine/ferricia/Core.kt
@@ -0,0 +1,26 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.engine.ferricia
+
+import terramodulus.internal.platform.Kernel32
+
+const val NULL: Long = 0;
+
+internal fun loadLibrary() {
+ if (Kernel32.INSTANCE != null) { // For Windows
+ Kernel32.INSTANCE.SetDllDirectoryW(System.getProperty("java.library.path")) // must use backslashes
+ }
+
+ System.loadLibrary("ferricia")
+}
+
+internal object Core {
+ /**
+ * Initializes the Engine handle.
+ */
+ @JvmName("init")
+ external fun init()
+}
diff --git a/src/internal/common/kotlin/terramodulus/internal/platform/Kernel32.kt b/src/internal/common/kotlin/terramodulus/internal/platform/Kernel32.kt
new file mode 100644
index 00000000..17a0e6dc
--- /dev/null
+++ b/src/internal/common/kotlin/terramodulus/internal/platform/Kernel32.kt
@@ -0,0 +1,24 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.internal.platform
+
+import com.sun.jna.Native
+import com.sun.jna.Platform
+import com.sun.jna.platform.win32.WinBase
+import com.sun.jna.win32.StdCallLibrary
+import com.sun.jna.win32.W32APIOptions
+
+@Suppress("FunctionName")
+interface Kernel32 : com.sun.jna.platform.win32.Kernel32, StdCallLibrary, WinBase {
+ // BOOL SetDllDirectoryW(LPCWSTR lpPathName);
+ fun SetDllDirectoryW(lpPathName: String?): Boolean
+
+ companion object {
+ val INSTANCE: Kernel32? =
+ if (Platform.isWindows()) Native.load("kernel32", Kernel32::class.java, W32APIOptions.DEFAULT_OPTIONS)
+ else null; // without relying on classloading
+ }
+}
diff --git a/src/kernel/client/kotlin/terramodulus/core/Main.kt b/src/kernel/client/kotlin/terramodulus/core/Main.kt
new file mode 100644
index 00000000..acfc5bc7
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/core/Main.kt
@@ -0,0 +1,122 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.core
+
+import joptsimple.OptionException
+import joptsimple.OptionParser
+import joptsimple.OptionSet
+import joptsimple.OptionSpec
+import joptsimple.ValueConversionException
+import joptsimple.ValueConverter
+import terramodulus.common.core.ApplicationArgumentParsingError
+import terramodulus.common.core.ApplicationInitializationFault
+import terramodulus.common.core.run
+import terramodulus.common.core.setupInit
+import terramodulus.mui.GuiManager
+import terramodulus.util.exception.CodeLogicFault
+import terramodulus.util.exception.triggerGlobalCrash
+import java.awt.Dimension
+import java.nio.file.InvalidPathException
+import java.nio.file.Path
+
+fun main(args: Array) {
+ setupInit()
+ try {
+ parseArgs(args)
+ } catch (e: ApplicationArgumentParsingError) {
+ triggerGlobalCrash(ApplicationInitializationFault(e))
+ }
+
+ run(TerraModulus())
+}
+
+/**
+ * Note that help message is not implemented here since it is not used and
+ * should not be accessible normally.
+ * Some features are then not used due to this, including but not limited to:
+ * - [ValueConverter.valuePattern]
+ * - [joptsimple.HelpFormatter]
+ * - [OptionParser.printHelpOn]
+ *
+ * @throws ApplicationArgumentParsingError
+ */
+fun parseArgs(args: Array) {
+ val parser = OptionParser()
+ val options = ArgumentOptions(parser)
+ @Suppress("NAME_SHADOWING")
+ val args = try {
+ Arguments(options, parser.parse(*args))
+ } catch (e: OptionException) {
+ throw ApplicationArgumentParsingError(e)
+ } catch (e: ClassCastException) {
+ triggerGlobalCrash(CodeLogicFault(e))
+ }
+}
+
+private object PathConverter : ValueConverter {
+ override fun convert(value: String): Path = try {
+ Path.of(value)
+ } catch (e: InvalidPathException) {
+ throw ValueConversionException("Invalid path", e)
+ }
+
+ override fun valueType(): Class = Path::class.java
+
+ override fun valuePattern(): String? = null
+}
+
+internal class ArgumentOptions(parser: OptionParser) {
+ val fullscreen: OptionSpec = parser.accepts("fullscreen")
+ val screenSize: OptionSpec = parser.accepts("screen-size")
+ .withRequiredArg()
+ .withValuesConvertedBy(object : ValueConverter {
+ private val REGEX = Regex("^([1-9][0-9]*)x([1-9][0-9]*)$")
+
+ override fun convert(value: String): Dimension {
+ val result = REGEX.find(value)
+ if (result == null) {
+ throw ValueConversionException("Invalid value (pattern unmatched): $value")
+ } else {
+ fun parseInt(value: String): Int {
+ try {
+ return value.toInt()
+ } catch (e: NumberFormatException) {
+ throw ValueConversionException("Invalid value: $value", e)
+ }
+ }
+
+ try {
+ return Dimension(parseInt(result.groups[1]!!.value), parseInt(result.groups[2]!!.value))
+ } catch (e: NullPointerException) {
+ triggerGlobalCrash(CodeLogicFault(e))
+ }
+ }
+ }
+
+ override fun valueType() = Dimension::class.java
+
+ override fun valuePattern() = null
+ })
+// val dataDir: OptionSpec = parser.accepts("data-dir")
+// .withRequiredArg()
+// .required()
+// .withValuesConvertedBy(PathConverter)
+// val resources: OptionSpec = parser.accepts("resources")
+// .withRequiredArg()
+// .required()
+// .withValuesConvertedBy(PathConverter)
+}
+
+/**
+ * @throws OptionException
+ * @throws ClassCastException regarded as code logic problems
+ */
+class Arguments internal constructor(options: ArgumentOptions, args: OptionSet) {
+ val fullscreen = args.has(options.fullscreen)
+ val screenSize: Dimension? = args.valueOf(options.screenSize)
+// val dataDir: Path = args.valueOf(options.dataDir)
+// val resources: Path = args.valueOf(options.resources)
+}
diff --git a/src/kernel/client/kotlin/terramodulus/core/TerraModulus.kt b/src/kernel/client/kotlin/terramodulus/core/TerraModulus.kt
new file mode 100644
index 00000000..1ed220b1
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/core/TerraModulus.kt
@@ -0,0 +1,30 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.core
+
+import terramodulus.common.core.AbstractTerraModulus
+import terramodulus.mui.GuiManager
+
+class TerraModulus internal constructor() : AbstractTerraModulus() {
+ private val guiManager = GuiManager()
+
+ override var tps: Int
+ get() = TODO("Not yet implemented")
+ set(value) {}
+
+ override fun run() {
+ guiManager.showWindow()
+ while (true) {
+ guiManager.updateCanvas()
+// guiManager.updateScreens()
+ Thread.sleep(1)
+ }
+ }
+
+ override fun close() {
+ TODO("Not yet implemented")
+ }
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/AuiManager.kt b/src/kernel/client/kotlin/terramodulus/mui/AuiManager.kt
new file mode 100644
index 00000000..3e3ae08d
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/AuiManager.kt
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui
+
+import terramodulus.mui.audio.AudioSystem
+
+/**
+ * Audio User Interface (AUI) Manager
+ */
+internal class AuiManager internal constructor() {
+ val audioSystem = AudioSystem()
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/GuiManager.kt b/src/kernel/client/kotlin/terramodulus/mui/GuiManager.kt
new file mode 100644
index 00000000..04f8ac3e
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/GuiManager.kt
@@ -0,0 +1,248 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui
+
+import terramodulus.engine.MuiEvent
+import terramodulus.engine.Window
+import terramodulus.mui.gfx.RenderSystem
+import terramodulus.mui.gms.ScreenManager
+import terramodulus.mui.input.InputSystem
+import terramodulus.util.logging.logger
+import java.io.Closeable
+import java.io.File
+
+private val logger = logger {}
+
+/**
+ * Graphical User Interface (GUI) Manager
+ */
+internal class GuiManager internal constructor() : Closeable {
+ private val window = Window()
+ val renderSystem = RenderSystem(window.canvas)
+ val inputSystem = InputSystem()
+ val screenManager = ScreenManager(renderSystem.handle)
+
+ internal fun showWindow() = window.show()
+
+// /**
+// * Screen updating, targeting as the same as *maximum FPS*,
+// * but the numbers of ticks are not supposed to be compensated when missed,
+// * so it is up to the callers to compensate missed activities.
+// */
+// internal fun updateScreens() {}
+
+ /**
+ * Canvas updating, per frame, maximally the *maximum FPS*.
+ * This includes input ticking and canvas rendering.
+ */
+ internal fun updateCanvas() {
+ window.pollEvents().forEach { event ->
+ when (event) {
+ is MuiEvent.DisplayAdded -> {
+ logger.debug { "Display added." }
+ }
+ is MuiEvent.DisplayMoved -> {
+ logger.debug { "Display moved." }
+ }
+ is MuiEvent.DisplayRemoved -> {
+ logger.debug { "Display removed." }
+ }
+ MuiEvent.DropBegin -> {
+ logger.debug { "Drop began." }
+ }
+ MuiEvent.DropComplete -> {
+ logger.debug { "Drop completed." }
+ }
+ is MuiEvent.DropFile -> {
+ logger.debug { "Dropped file \"${event.filename}\" on window." }
+ }
+ MuiEvent.DropPosition -> {
+ logger.debug { "Drop position." }
+ }
+ is MuiEvent.DropText -> {
+ logger.debug { "Dropped text \"${event.text}\" on window." }
+ }
+ is MuiEvent.GamepadAdded -> {
+ logger.debug { "Gamepad (id: ${event.joystickId}) added." }
+ }
+ is MuiEvent.GamepadAxisMotion -> {
+ logger.debug { "Gamepad (id: ${event.joystickId}) axis (${event.axis}) motion: ${event.value}" }
+ }
+ is MuiEvent.GamepadButtonDown -> {
+ logger.debug { "Gamepad (id: ${event.joystickId}) button (${event.button}) down." }
+ }
+ is MuiEvent.GamepadButtonUp -> {
+ logger.debug { "Gamepad (id: ${event.joystickId}) button (${event.button}) up." }
+ }
+ is MuiEvent.GamepadRemapped -> {
+ logger.debug { "Gamepad (id: ${event.joystickId}) remapped." }
+ }
+ is MuiEvent.GamepadRemoved -> {
+ logger.debug { "Gamepad (id: ${event.joystickId}) removed." }
+ }
+ MuiEvent.GamepadSteamHandleUpdated -> {
+ logger.debug { "Gamepad steam handle updated." }
+ }
+ is MuiEvent.GamepadTouchpadDown -> {
+ logger.debug { "Gamepad (id: ${event.joystickId}) touchpad (${event.touchpad}) down." }
+ }
+ is MuiEvent.GamepadTouchpadMotion -> {
+ logger.debug { "Gamepad (id: ${event.joystickId}) touchpad (${event.touchpad}) motion." }
+ }
+ is MuiEvent.GamepadTouchpadUp -> {
+ logger.debug { "Gamepad (id: ${event.joystickId}) touchpad (${event.touchpad}) up." }
+ }
+ is MuiEvent.JoystickAdded -> {
+ logger.debug { "Joystick (id: ${event.joystickId}) added." }
+ }
+ is MuiEvent.JoystickAxisMotion -> {
+ logger.debug { "Joystick (id: ${event.joystickId}) axis (${event.axis}) motion: ${event.value}" }
+ }
+ MuiEvent.JoystickBallMotion -> {
+ logger.debug { "Joystick ball motion." }
+ }
+ MuiEvent.JoystickBatteryUpdated -> {
+ logger.debug { "Joystick battery updated." }
+ }
+ is MuiEvent.JoystickButtonDown -> {
+ logger.debug { "Joystick (id: ${event.joystickId}) button (${event.button}) down." }
+ }
+ is MuiEvent.JoystickButtonUp -> {
+ logger.debug { "Joystick (id: ${event.joystickId}) button (${event.button}) up." }
+ }
+ is MuiEvent.JoystickHatMotion -> {
+ logger.debug { "Joystick (id: ${event.joystickId}) hat (${event.hat}) motion: ${event.value}." }
+ }
+ is MuiEvent.JoystickRemoved -> {
+ logger.debug { "Joystick (id: ${event.joystickId}) removed." }
+ }
+ MuiEvent.KeyboardAdded -> {
+ logger.debug { "Keyboard added." }
+ }
+ is MuiEvent.KeyboardKeyDown -> {
+ logger.debug { "Keyboard (id: ${event.keyboardId}) key `${event.key}` down." }
+ }
+ is MuiEvent.KeyboardKeyUp -> {
+ logger.debug { "Keyboard (id: ${event.keyboardId}) key `${event.key}` up." }
+ }
+ MuiEvent.KeyboardRemoved -> {
+ logger.debug { "Keyboard removed." }
+ }
+ MuiEvent.KeymapChanged -> {
+ logger.debug { "Keyboard keymap changed." }
+ }
+ MuiEvent.MouseAdded -> {
+ logger.debug { "Mouse added." }
+ }
+ is MuiEvent.MouseButtonDown -> {
+ logger.debug { "Mouse (id: ${event.mouseId}) key `${event.key}` down." }
+ }
+ is MuiEvent.MouseButtonUp -> {
+ logger.debug { "Mouse (id: ${event.mouseId}) key `${event.key}` up." }
+ }
+ is MuiEvent.MouseMotion -> {
+ logger.debug { "Mouse (id: ${event.mouseId}) motion (${event.x}, ${event.y})." }
+ }
+ MuiEvent.MouseRemoved -> {
+ logger.debug { "Mouse removed." }
+ }
+ is MuiEvent.MouseWheel -> {
+ logger.debug { "Mouse (id: ${event.mouseId}) wheel (${event.x}, ${event.y})." }
+ }
+ MuiEvent.RenderDeviceLost -> {
+ logger.debug { "Render device lost." }
+ }
+ MuiEvent.RenderDeviceReset -> {
+ logger.debug { "Render device reset." }
+ }
+ MuiEvent.RenderTargetsReset -> {
+ logger.debug { "Render targets reset." }
+ }
+ is MuiEvent.TextEditing -> {
+ logger.debug { "Text editing at ${event.start} with length ${event.length}." }
+ }
+ MuiEvent.TextEditingCandidates -> {
+ logger.debug { "Text editing candidates." }
+ }
+ is MuiEvent.TextInput -> {
+ logger.debug { "Text input." }
+ }
+ MuiEvent.WindowCloseRequested -> {
+ logger.debug { "Window close requested." }
+ }
+ MuiEvent.WindowDestroyed -> {
+ logger.debug { "Window destroyed." }
+ }
+ MuiEvent.WindowEnterFullscreen -> {
+ logger.debug { "Window entered fullscreen." }
+ }
+ MuiEvent.WindowExposed -> {
+ logger.debug { "Window exposed." }
+ }
+ MuiEvent.WindowFocusGained -> {
+ logger.debug { "Window focus gained." }
+ }
+ MuiEvent.WindowFocusLost -> {
+ logger.debug { "Window focus lost." }
+ }
+ MuiEvent.WindowHdrStateChanged -> {
+ logger.debug { "Window HDR state changed." }
+ }
+ MuiEvent.WindowHidden -> {
+ logger.debug { "Window hidden." }
+ }
+ MuiEvent.WindowIccProfChanged -> {
+ logger.debug { "Window ICC profile changed." }
+ }
+ MuiEvent.WindowLeaveFullscreen -> {
+ logger.debug { "Window left fullscreen." }
+ }
+ MuiEvent.WindowMaximized -> {
+ logger.debug { "Window maximized." }
+ }
+ MuiEvent.WindowMetalViewResized -> {
+ logger.debug { "Window metal view resized." }
+ }
+ MuiEvent.WindowMinimized -> {
+ logger.debug { "Window minimized." }
+ }
+ MuiEvent.WindowMouseEnter -> {
+ logger.debug { "Mouse entered window." }
+ }
+ MuiEvent.WindowMouseLeave -> {
+ logger.debug { "Mouse left window." }
+ }
+ is MuiEvent.WindowMoved -> {
+ logger.debug { "Window moved to (${event.x}, ${event.y})." }
+ }
+ MuiEvent.WindowOccluded -> {
+ logger.debug { "Window occluded." }
+ }
+ is MuiEvent.WindowPixelSizeChanged -> {
+ logger.debug { "Window pixel size changed to ${event.width}x${event.height}." }
+ window.canvas.resizeGLViewport()
+ logger.debug { "Window viewport resized." }
+ }
+ is MuiEvent.WindowResized -> {
+ logger.debug { "Window resized to ${event.width}x${event.height}." }
+ }
+ MuiEvent.WindowRestored -> {
+ logger.debug { "Window restored." }
+ }
+ MuiEvent.WindowShown -> {
+ logger.debug { "Window shown." }
+ }
+ }
+ }
+ window.canvas.clear()
+ screenManager.render(renderSystem)
+ window.swap()
+ }
+
+ override fun close() {
+ window.close()
+ }
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/audio/AudioSystem.kt b/src/kernel/client/kotlin/terramodulus/mui/audio/AudioSystem.kt
new file mode 100644
index 00000000..0c05c0f3
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/audio/AudioSystem.kt
@@ -0,0 +1,9 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.audio
+
+class AudioSystem internal constructor() {
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gfx/Anchor.kt b/src/kernel/client/kotlin/terramodulus/mui/gfx/Anchor.kt
new file mode 100644
index 00000000..6065fbd6
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gfx/Anchor.kt
@@ -0,0 +1,26 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gfx
+
+/*
+ * Every set of anchor position directions has different meanings in different context,
+ * They are kept separated for context and literal clarity and organization.
+ * Those should be used with care since they are not already interconvertible.
+ */
+
+/**
+ * Set of 4 anchor position directions
+ */
+enum class Anchor4 {
+ TopLeft, TopRight, BottomLeft, BottomRight;
+}
+
+/**
+ * Set of 5 anchor position directions
+ */
+enum class Anchor5 {
+ TopLeft, TopRight, BottomLeft, BottomRight, Center;
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gfx/ColorFilter.kt b/src/kernel/client/kotlin/terramodulus/mui/gfx/ColorFilter.kt
new file mode 100644
index 00000000..9ed99fb1
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gfx/ColorFilter.kt
@@ -0,0 +1,10 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gfx
+
+typealias ColorFilter = terramodulus.engine.ColorFilter
+
+typealias AlphaFilter = terramodulus.engine.AlphaFilter
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gfx/Dimension.kt b/src/kernel/client/kotlin/terramodulus/mui/gfx/Dimension.kt
new file mode 100644
index 00000000..df844d8d
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gfx/Dimension.kt
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gfx
+
+data class Dimension2I(val width: Int, val height: Int)
+
+data class Dimension2F(val width: Float, val height: Float)
+
+data class Dimension3I(val width: Int, val height: Int, val length: Int)
+
+data class Dimension3F(val width: Float, val height: Float, val length: Float)
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gfx/Direction.kt b/src/kernel/client/kotlin/terramodulus/mui/gfx/Direction.kt
new file mode 100644
index 00000000..867394e8
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gfx/Direction.kt
@@ -0,0 +1,75 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gfx
+
+/*
+ * Every set of directions has different meanings in different context,
+ * They are kept separated for context and literal clarity and organization.
+ * Those should be used with care since they are not already interconvertible.
+ */
+
+/**
+ * Set of 4 compass directions.
+ */
+enum class Direction4C {
+ North, South, West, East;
+}
+
+/**
+ * Set of 4 vertical directions.
+ */
+enum class Direction4V {
+ Left, Right, Up, Down;
+}
+
+/**
+ * Set of 4 horizontal directions.
+ */
+enum class Direction4H {
+ Front, Back, Left, Right;
+}
+
+/**
+ * Set of 4 axial directions.
+ */
+enum class Direction4A {
+ XPos, XNeg, YPos, YNeg;
+}
+
+/**
+ * Set of 8 compass directions.
+ */
+enum class Direction8C {
+ North, South, West, East, NorthWest, NorthEast, SouthWest, SouthEast;
+}
+
+/**
+ * Set of 8 vertical directions.
+ */
+enum class Direction8V {
+ Left, Right, Up, Down, UpLeft, UpRight, DownLeft, DownRight;
+}
+
+/**
+ * Set of 8 horizontal directions.
+ */
+enum class Direction8H {
+ Front, Back, Left, Right, FrontLeft, FrontRight, BackLeft, BackRight;
+}
+
+/**
+ * Set of 6 3D-relative directions.
+ */
+enum class Direction6R {
+ Up, Down, Left, Right, Front, Back;
+}
+
+/**
+ * Set of 6 3D-compass directions.
+ */
+enum class Direction6C {
+ North, South, West, East, Up, Down;
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gfx/GuiGeometry.kt b/src/kernel/client/kotlin/terramodulus/mui/gfx/GuiGeometry.kt
new file mode 100644
index 00000000..60413c2d
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gfx/GuiGeometry.kt
@@ -0,0 +1,24 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gfx
+
+import terramodulus.engine.GeomDrawable
+import terramodulus.engine.SimpleLineGeom
+import terramodulus.engine.SimpleRectGeom
+
+sealed class GuiGeometry(protected val geom: GeomDrawable) {
+ fun add(model: ModelTransform) = geom.add(model)
+
+ fun add(filter: ColorFilter) = geom.add(filter)
+
+ fun render(renderSystem: RenderSystem) = renderSystem.renderGuiGeo(geom)
+}
+
+class GuiLine(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) :
+ GuiGeometry(SimpleLineGeom(x0, y0, x1, y1, r, g, b, a))
+
+class GuiRect(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) :
+ GuiGeometry(SimpleRectGeom(x0, y0, x1, y1, r, g, b, a))
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gfx/GuiSprite.kt b/src/kernel/client/kotlin/terramodulus/mui/gfx/GuiSprite.kt
new file mode 100644
index 00000000..42935a81
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gfx/GuiSprite.kt
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gfx
+
+import terramodulus.engine.SpriteMesh
+
+class GuiSprite(private val rect: RectangleI, private val texture: UInt) {
+ private val mesh = SpriteMesh(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height)
+
+ fun add(model: ModelTransform) = mesh.add(model)
+
+ fun add(filter: ColorFilter) = mesh.add(filter)
+
+ fun render(renderSystem: RenderSystem) = renderSystem.renderGuiTex(mesh, texture)
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gfx/ManagedRect.kt b/src/kernel/client/kotlin/terramodulus/mui/gfx/ManagedRect.kt
new file mode 100644
index 00000000..5f251f8f
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gfx/ManagedRect.kt
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gfx
+
+import kotlin.properties.Delegates.observable
+
+class ManagedRect(rect: RectangleF) {
+ var rect: RectangleF by observable(rect) { _, _, newValue -> observers.forEach { it(newValue) } }
+
+ private val observers = LinkedHashSet<(RectangleF) -> Unit>()
+
+ fun observe(observer: (RectangleF) -> Unit) {
+ observers.add(observer)
+ }
+
+ fun unobserve(observer: (RectangleF) -> Unit) {
+ observers.remove(observer)
+ }
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gfx/ModelTransform.kt b/src/kernel/client/kotlin/terramodulus/mui/gfx/ModelTransform.kt
new file mode 100644
index 00000000..343cc5c6
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gfx/ModelTransform.kt
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gfx
+
+typealias ModelTransform = terramodulus.engine.ModelTransform
+
+typealias SmartScaling = terramodulus.engine.SmartScaling
+
+typealias FullScaling = terramodulus.engine.FullScaling
+
+fun FullScaling(rect: Dimension2I) = FullScaling(rect.width, rect.height)
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gfx/Rectangle.kt b/src/kernel/client/kotlin/terramodulus/mui/gfx/Rectangle.kt
new file mode 100644
index 00000000..0f38514a
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gfx/Rectangle.kt
@@ -0,0 +1,130 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gfx
+
+/**
+ * Rectangle in a coordinate system with (0, 0) on the bottom left.
+ * The anchor of the rectangle is the bottom-left corner.
+ */
+data class RectangleI(
+ val x: Int,
+ val y: Int,
+ val width: Int,
+ val height: Int
+) {
+ companion object {
+ fun withPoints(x0: Int, y0: Int, x1: Int, y1: Int): RectangleI {
+ val minX: Int;
+ val maxX: Int;
+ if (x0 < x1) {
+ minX = x0;
+ maxX = x1;
+ } else {
+ maxX = x0;
+ minX = x1;
+ }
+ val minY: Int;
+ val maxY: Int;
+ if (y0 < y1) {
+ minY = y0;
+ maxY = y1;
+ } else {
+ maxY = y0;
+ minY = y1;
+ }
+ return RectangleI(minX, maxX, minY, maxY)
+ }
+ }
+
+ val size get() = Dimension2I(width, height)
+
+ fun anchor(pos: Anchor5) = when (pos) {
+ Anchor5.TopLeft -> Vector2I(x, y + width)
+ Anchor5.TopRight -> Vector2I(x + width, y + height)
+ Anchor5.BottomLeft -> Vector2I(x, y)
+ Anchor5.BottomRight -> Vector2I(x + width, y)
+ Anchor5.Center -> Vector2I(x + width / 2, y + height / 2)
+ }
+
+ fun translateBy(pos: Vector2I) = RectangleI(x + pos.x, y + pos.y, width, height)
+
+ fun translateBy(x: Int, y: Int) = RectangleI(this.x + x, this.y + y, width, height)
+
+ fun translateByY(y: Int) = RectangleI(x, this.y + y, width, height)
+
+ fun translateByX(x: Int) = RectangleI(this.x + x, y, width, height)
+
+ fun translateToY(y: Int) = RectangleI(x, y, width, height)
+
+ fun translateToX(x: Int) = RectangleI(x, y, width, height)
+
+ fun translateTo(pos: Vector2I) = RectangleI(pos.x, pos.y, width, height)
+
+ fun translateTo(x: Int, y: Int) = RectangleI(x, y, width, height)
+
+ fun toFloat() = RectangleF(x.toFloat(), y.toFloat(), width.toFloat(), height.toFloat())
+}
+
+/**
+ * Rectangle in a coordinate system with (0, 0) on the bottom left.
+ * The anchor of the rectangle is the bottom-left corner.
+ */
+data class RectangleF(
+ val x: Float,
+ val y: Float,
+ val width: Float,
+ val height: Float
+) {
+ companion object {
+ fun withPoints(x0: Float, y0: Float, x1: Float, y1: Float): RectangleF {
+ val minX: Float;
+ val maxX: Float;
+ if (x0 < x1) {
+ minX = x0;
+ maxX = x1;
+ } else {
+ maxX = x0;
+ minX = x1;
+ }
+ val minY: Float;
+ val maxY: Float;
+ if (y0 < y1) {
+ minY = y0;
+ maxY = y1;
+ } else {
+ maxY = y0;
+ minY = y1;
+ }
+ return RectangleF(minX, maxX, minY, maxY)
+ }
+ }
+
+ val size get() = Dimension2F(width, height)
+
+ fun anchor(pos: Anchor5) = when (pos) {
+ Anchor5.TopLeft -> Vector2F(x, y + width)
+ Anchor5.TopRight -> Vector2F(x + width, y + height)
+ Anchor5.BottomLeft -> Vector2F(x, y)
+ Anchor5.BottomRight -> Vector2F(x + width, y)
+ Anchor5.Center -> Vector2F(x + width / 2, y + height / 2)
+ }
+
+ fun translateBy(pos: Vector2F) = RectangleF(x + pos.x, y + pos.y, width, height)
+
+ fun translateBy(x: Float, y: Float) = RectangleF(this.x + x, this.y + y, width, height)
+
+ fun translateByY(y: Float) = RectangleF(x, this.y + y, width, height)
+
+ fun translateByX(x: Float) = RectangleF(this.x + x, y, width, height)
+
+ fun translateToY(y: Float) = RectangleF(x, y, width, height)
+
+ fun translateToX(x: Float) = RectangleF(x, y, width, height)
+
+ fun translateTo(pos: Vector2F) = RectangleF(pos.x, pos.y, width, height)
+
+ fun translateTo(x: Float, y: Float) = RectangleF(x, y, width, height)
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gfx/RenderSystem.kt b/src/kernel/client/kotlin/terramodulus/mui/gfx/RenderSystem.kt
new file mode 100644
index 00000000..b7240322
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gfx/RenderSystem.kt
@@ -0,0 +1,50 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gfx
+
+import terramodulus.engine.Canvas
+import terramodulus.engine.GeomDrawable
+import terramodulus.engine.MeshDrawable
+import java.io.File
+
+private fun getPathOfResource(path: String): String {
+ return File(object {}.javaClass.getResource(path)!!.toURI()).absolutePath
+}
+
+class RenderSystem internal constructor(private val canvas: Canvas) {
+ val handle: Handle = HandleImpl()
+ private val texShaders = canvas.loadTexShaders(
+ getPathOfResource("/gms_tex.vsh"),
+ getPathOfResource("/gms_tex.fsh")
+ )
+ private val geoShaders = canvas.loadGeoShaders(
+ getPathOfResource("/gms_geo.vsh"),
+ getPathOfResource("/gms_geo.fsh")
+ )
+ val targetFps = 1000;
+
+ sealed interface Handle {
+ fun loadTexture(path: String): UInt
+
+ fun setBackgroundColor(red: Float, green: Float, blue: Float, alpha: Float)
+ }
+
+ private inner class HandleImpl : Handle {
+ override fun loadTexture(path: String) = canvas.loadImage(getPathOfResource(path))
+
+ override fun setBackgroundColor(red: Float, green: Float, blue: Float, alpha: Float) {
+ canvas.setClearColor(red, green, blue, alpha)
+ }
+ }
+
+ internal fun renderGuiTex(drawable: MeshDrawable, texture: UInt) = canvas.renderGuiTex(drawable, texShaders, texture)
+
+ internal fun renderGuiGeo(drawable: GeomDrawable) = canvas.renderGuiGeo(drawable, geoShaders)
+
+ internal fun render() {
+
+ }
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gfx/Vector.kt b/src/kernel/client/kotlin/terramodulus/mui/gfx/Vector.kt
new file mode 100644
index 00000000..7bb49977
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gfx/Vector.kt
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gfx
+
+data class Vector2I(val x: Int, val y: Int) {
+ companion object {
+ val ZERO = Vector2I(0, 0)
+ }
+
+ operator fun plus(other: Vector2I) = Vector2I(x + other.x, y + other.y)
+}
+
+data class Vector2D(val x: Double, val y: Double) {
+ companion object {
+ val ZERO = Vector2D(.0, .0)
+ }
+
+ operator fun plus(other: Vector2D) = Vector2D(x + other.x, y + other.y)
+}
+
+data class Vector2F(val x: Float, val y: Float) {
+ companion object {
+ val ZERO = Vector2F(0F, 0F)
+ }
+
+ operator fun plus(other: Vector2F) = Vector2F(x + other.x, y + other.y)
+}
+
+data class Vector3I(val x: Int, val y: Int, val z: Int) {
+ companion object {
+ val ZERO = Vector3I(0, 0, 0)
+ }
+
+ operator fun plus(other: Vector3I) = Vector3I(x + other.x, y + other.y, z + other.z)
+}
+
+data class Vector3D(val x: Double, val y: Double, val z: Double) {
+ companion object {
+ val ZERO = Vector3D(.0, .0, .0)
+ }
+
+ operator fun plus(other: Vector3D) = Vector3D(x + other.x, y + other.y, z + other.z)
+}
+
+data class Vector3F(val x: Float, val y: Float, val z: Float) {
+ companion object {
+ val ZERO = Vector3F(0F, 0F, 0F)
+ }
+
+ operator fun plus(other: Vector3F) = Vector3F(x + other.x, y + other.y, z + other.z)
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/AbstractPanel.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/AbstractPanel.kt
new file mode 100644
index 00000000..3582d919
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gms/AbstractPanel.kt
@@ -0,0 +1,9 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gms
+
+abstract class AbstractPanel : Component(), Container {
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/Component.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/Component.kt
new file mode 100644
index 00000000..acda77de
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gms/Component.kt
@@ -0,0 +1,41 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gms
+
+import terramodulus.mui.gfx.ManagedRect
+import terramodulus.mui.gfx.RenderSystem
+import terramodulus.mui.gms.event.ComponentEvent
+
+/**
+ * [Component] can only be contained by only one [Container].
+ *
+ * It is an undefined behavior when the `Component` is contained repeatedly
+ * or in different containers simultaneously.
+ */
+abstract class Component {
+ private val listeners = HashMap, LinkedHashSet<(ComponentEvent) -> Unit>>()
+
+ /**
+ * This should only be modified by [Layout] managers.
+ */
+ open lateinit var rect: ManagedRect
+ internal set
+
+ abstract fun render(renderSystem: RenderSystem)
+
+ fun addListener(e: Class, l: (T) -> Unit) {
+ @Suppress("UNCHECKED_CAST")
+ listeners.computeIfAbsent(e) { LinkedHashSet() }.add(l as (ComponentEvent) -> Unit)
+ }
+
+ fun removeListener(e: Class, l: (T) -> Unit) {
+ listeners[e]?.remove(l)
+ }
+
+ internal fun dispatchEvent(event: ComponentEvent) {
+ listeners[event.javaClass]?.forEach { it(event) }
+ }
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/Container.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/Container.kt
new file mode 100644
index 00000000..59788138
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gms/Container.kt
@@ -0,0 +1,12 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gms
+
+import terramodulus.mui.gfx.ManagedRect
+
+sealed interface Container {
+ val rect: ManagedRect
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/Layout.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/Layout.kt
new file mode 100644
index 00000000..6fd00483
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gms/Layout.kt
@@ -0,0 +1,120 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gms
+
+import terramodulus.mui.gfx.RectangleF
+import kotlin.reflect.KProperty
+
+/**
+ * [Layout] is always mutable.
+ *
+ * **Layout** is defined only when all its managed components all belong to the container
+ * associated with this layout manager *exclusively*.
+ */
+abstract class Layout(private val container: Container) {
+ companion object {
+ const val ALIGN_START = 0F;
+ const val ALIGN_CENTER = .5F;
+ const val ALIGN_END = 1F;
+ }
+
+ abstract val components: Iterable
+
+ private val containerObserver = ::layout.apply(container.rect::observe)
+
+ /**
+ * Updates the layout output using the current layout configurations
+ * by invoking [layout] internally.
+ *
+ * It is recommended to invoke this when this layout is being initialized
+ * or any layout configuration has been changed.
+ */
+ fun update() = layout(container.rect.rect)
+
+ /**
+ * Lays out the managed [components] by this [Layout] manager.
+ *
+ * Only the `rect`s of the managed `components` should be (re)assigned;
+ * no other state-changing operations should be done beside this.
+ * @param rect the rectangle of the container at this moment
+ */
+ protected abstract fun layout(rect: RectangleF)
+
+ /**
+ * Must be invoked when this [Layout] is no longer in use.
+ */
+ fun clear() {
+ container.rect.unobserve(containerObserver)
+ }
+
+ /**
+ * @param components must not be empty
+ */
+ protected class ComponentIterable(private vararg val components: KProperty) : Iterable {
+ override fun iterator(): Iterator = object : Iterator {
+ private var index = 0
+
+ private fun untilNotNull(): Boolean {
+ do {
+ if (components[index].getter.call() != null)
+ return true
+ else
+ index++
+ } while (index < components.size)
+ return false
+ }
+
+ override fun hasNext(): Boolean = untilNotNull()
+
+ override fun next(): Component = if (untilNotNull()) {
+ components[index].getter.call()!!
+ } else {
+ throw NoSuchElementException()
+ }
+ }
+ }
+
+ /**
+ * Internal list of the layout elements.
+ */
+ protected class ElementList private constructor(
+ private val components: MutableList, // order is defined here
+ private val elementMap: MutableMap, // elements are mapped here
+ ) : Iterable> {
+ companion object {
+ fun withComponentsDefault(default: () -> E, vararg components: Component): ElementList {
+ val map = hashMapOf()
+ components.forEach { map[it] = default() }
+ return ElementList(mutableListOf(*components), map)
+ }
+
+ fun withComponentsDefault(default: () -> E, components: Collection): ElementList {
+ val map = hashMapOf()
+ components.forEach { map[it] = default() }
+ return ElementList(ArrayList(components), map)
+ }
+
+ fun withElements(vararg elements: Pair): ElementList =
+ ElementList(elements.mapTo(ArrayList(elements.size)) { it.first }, hashMapOf(*elements))
+
+ fun withElements(elements: Map): ElementList =
+ ElementList(elements.mapTo(ArrayList(elements.size)) { it.key }, HashMap(elements))
+ }
+
+ val componentsView: Collection = components
+
+ override fun iterator(): Iterator> = object : Iterator> {
+ private val it = components.iterator()
+
+ override fun hasNext() = it.hasNext()
+
+ override fun next(): Pair {
+ val e = it.next()
+ return e to elementMap[e]!!
+ }
+ }
+ }
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/Menu.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/Menu.kt
new file mode 100644
index 00000000..b3ab96a2
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gms/Menu.kt
@@ -0,0 +1,67 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gms
+
+import terramodulus.mui.gms.event.MenuEvent
+import java.util.ArrayDeque
+
+abstract class Menu : Container {
+ private val listeners = HashMap, LinkedHashSet<(MenuEvent) -> Unit>>()
+ private val components = LinkedHashSet()
+ private val componentQueue = ArrayDeque()
+ val handle: Handle = HandleImpl()
+
+ private sealed interface ComponentOperation {
+ class Add(val component: () -> Component) : ComponentOperation
+
+ class Remove(val component: Component) : ComponentOperation
+ }
+
+ /**
+ * It is strongly suggested only using this function during initialization.
+ */
+ protected fun addComponent(component: Component) {
+ components.add(component)
+ }
+
+ /**
+ * It is strongly suggested only using this function during initialization.
+ */
+ protected fun removeComponent(component: Component) {
+ components.remove(component)
+ }
+
+ fun addListener(e: Class, l: (T) -> Unit) {
+ @Suppress("UNCHECKED_CAST")
+ listeners.computeIfAbsent(e) { LinkedHashSet() }.add(l as (MenuEvent) -> Unit)
+ }
+
+ fun removeListener(e: Class, l: (T) -> Unit) {
+ listeners[e]?.remove(l)
+ }
+
+ internal fun dispatchEvent(event: MenuEvent) {
+ listeners[event.javaClass]?.forEach { it(event) }
+ }
+
+ sealed interface Handle {
+ fun addComponent(component: () -> Component)
+
+ fun removeComponent(component: Component)
+ }
+
+ private inner class HandleImpl : Handle {
+ override fun addComponent(component: () -> Component) {
+ componentQueue.add(ComponentOperation.Add(component))
+ }
+
+ override fun removeComponent(component: Component) {
+ componentQueue.add(ComponentOperation.Remove(component))
+ }
+ }
+
+ abstract fun render()
+}
diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/Screen.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/Screen.kt
new file mode 100644
index 00000000..528c4976
--- /dev/null
+++ b/src/kernel/client/kotlin/terramodulus/mui/gms/Screen.kt
@@ -0,0 +1,136 @@
+/*
+ * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+package terramodulus.mui.gms
+
+import terramodulus.mui.gfx.RenderSystem
+import terramodulus.mui.gms.event.ScreenEvent
+import java.util.ArrayDeque
+
+abstract class Screen : Container {
+ private val listeners = HashMap, LinkedHashSet<(ScreenEvent) -> Unit>>()
+ private val menus = LinkedHashSet