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() + private val components = ArrayList() + private val componentQueue = ArrayDeque() + private val menuQueue = ArrayDeque() + val handle: Handle = HandleImpl() + + private sealed interface ComponentOperation { + fun apply(components: ArrayList) + + class Add(val component: () -> Component) : ComponentOperation { + override fun apply(components: ArrayList) { + components.add(component()) + } + } + + class Remove(val component: Component) : ComponentOperation { + override fun apply(components: ArrayList) { + components.remove(component) + } + } + } + + private sealed interface MenuOperation { + fun apply(menus: LinkedHashSet) + + class Add(val menu: () -> Menu) : MenuOperation { + override fun apply(menus: LinkedHashSet) { + menus.add(menu()) + } + } + + class Remove(val menu: Menu) : MenuOperation { + override fun apply(menus: LinkedHashSet) { + menus.remove(menu) + } + } + } + + /** + * 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) + } + + /** + * It is strongly suggested only using this function during initialization. + */ + protected fun addMenu(menu: Menu) { + menus.add(menu) + } + + /** + * It is strongly suggested only using this function during initialization. + */ + protected fun removeMenu(menu: Menu) { + menus.remove(menu) + } + + fun addListener(e: Class, l: (T) -> Unit) { + @Suppress("UNCHECKED_CAST") + listeners.computeIfAbsent(e) { LinkedHashSet() }.add(l as (ScreenEvent) -> Unit) + } + + fun removeListener(e: Class, l: (T) -> Unit) { + listeners[e]?.remove(l) + } + + internal fun dispatchEvent(event: ScreenEvent) { + listeners[event.javaClass]?.forEach { it(event) } + } + + sealed interface Handle { + fun addComponent(component: () -> Component) + + fun removeComponent(component: Component) + + fun addMenu(menu: () -> Menu) + + fun removeMenu(menu: Menu) + } + + 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)) + } + + override fun addMenu(menu: () -> Menu) { + menuQueue.add(MenuOperation.Add(menu)) + } + + override fun removeMenu(menu: Menu) { + menuQueue.add(MenuOperation.Remove(menu)) + } + } + + internal open fun update(renderSystem: RenderSystem, screenManager: ScreenManager) {}; + + internal fun render(renderSystem: RenderSystem, screenManager: ScreenManager) { + componentQueue.forEach { it.apply(components) } + componentQueue.clear() + menuQueue.forEach { it.apply(menus) } + menuQueue.clear() + components.forEach { it.render(renderSystem) } + update(renderSystem, screenManager) + } + + /** + * Cleans up and closes any used resource here. + */ + abstract fun exit() +} diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/ScreenManager.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/ScreenManager.kt new file mode 100644 index 00000000..eea9e18b --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/mui/gms/ScreenManager.kt @@ -0,0 +1,155 @@ +/* + * 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.impl.LaunchingScreen + +class ScreenManager internal constructor(private val renderSystemHandle: RenderSystem.Handle) { + /** + * FILO screen stack; the top-most screen instance is in the last. + */ + private val screens = ArrayDeque() + private val screenQueue = ArrayDeque() + val handle: Handle = HandleImpl() + + init { + screens.add(LaunchingScreen(renderSystemHandle)) + } + + private sealed interface ScreenOperation { + fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) + + /** + * Exits `n` times + * + * @throws IllegalArgumentException when `n` < 1 + * @throws IllegalStateException when `n` >= [screens] size during operation + */ + class Exit(val n: Int) : ScreenOperation { + init { + require(n < 0) { "`n` < 1" } + } + + override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { + if (n >= screens.size) { + throw IllegalStateException("`n` >= screens.size") + } + + for (i in 1..n) { + screens.removeLast().exit() + } + } + } + + /** + * Opens the `screen` + */ + class Open(val screen: (RenderSystem.Handle) -> Screen) : ScreenOperation { + override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { + screens.addLast(screen(handle)) + } + } + + /** + * Opens the `screen` before the `target` screen + */ + class OpenBefore(val screen: (RenderSystem.Handle) -> Screen, val target: Screen) : ScreenOperation { + override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { + screens.add(screens.lastIndexOf(target), screen(handle)) + } + } + + /** + * Exits until reaching the `screen` then remains on the `screen` + */ + class ExitTo(val screen: Screen) : ScreenOperation { + override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { + val it = screens.asReversed().listIterator() + while (it.hasNext()) { + val e = it.next() + if (e == screen) { + break + } else { + it.remove() + e.exit() + } + } + } + } + + /** + * Clears [screens] then opens the `screen` + */ + class Reset(val screen: (RenderSystem.Handle) -> Screen) : ScreenOperation { + override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { + screens.asReversed().forEach { it.exit() } + screens.clear() + screens.add(screen(handle)) + } + } + } + + sealed interface Handle { + /** + * @see ScreenOperation.Exit + */ + fun exit(n: Int) + + /** + * @see ScreenOperation.Open + */ + fun open(screen: (RenderSystem.Handle) -> Screen) + + /** + * It is not recommended to use this in general scenarios. + * @see ScreenOperation.OpenBefore + */ + fun openBefore(screen: (RenderSystem.Handle) -> Screen, target: Screen) + + /** + * @see ScreenOperation.ExitTo + */ + fun exitTo(screen: Screen) + + /** + * @see ScreenOperation.Reset + */ + fun reset(screen: (RenderSystem.Handle) -> Screen) + } + + private inner class HandleImpl : Handle { + override fun exit(n: Int) { + screenQueue.add(ScreenOperation.Exit(n)) + } + + override fun open(screen: (RenderSystem.Handle) -> Screen) { + screenQueue.add(ScreenOperation.Open(screen)) + } + + override fun openBefore(screen: (RenderSystem.Handle) -> Screen, target: Screen) { + screenQueue.add(ScreenOperation.OpenBefore(screen, target)) + } + + override fun exitTo(screen: Screen) { + screenQueue.add(ScreenOperation.ExitTo(screen)) + } + + override fun reset(screen: (RenderSystem.Handle) -> Screen) { + screenQueue.add(ScreenOperation.Reset(screen)) + } + } + +// internal fun update() { +// screens.forEach { it.update() } +// } + + internal fun render(renderSystem: RenderSystem) { + screenQueue.forEach { it.apply(renderSystemHandle, screens) } + screenQueue.clear() + screens.forEach { it.render(renderSystem, this) } + } +} diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/event/ComponentEvent.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/event/ComponentEvent.kt new file mode 100644 index 00000000..5a7ae4de --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/mui/gms/event/ComponentEvent.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.mui.gms.event + +sealed interface ComponentEvent { +} diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/event/MenuEvent.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/event/MenuEvent.kt new file mode 100644 index 00000000..d60a4595 --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/mui/gms/event/MenuEvent.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.mui.gms.event + +sealed interface MenuEvent { +} diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/event/ScreenEvent.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/event/ScreenEvent.kt new file mode 100644 index 00000000..b49731b9 --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/mui/gms/event/ScreenEvent.kt @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.mui.gms.event + +sealed interface ScreenEvent { + data object Open : ScreenEvent + data object Close : ScreenEvent +} diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/impl/BlankComponent.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/BlankComponent.kt new file mode 100644 index 00000000..a8d809c7 --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/BlankComponent.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.mui.gms.impl + +import terramodulus.mui.gfx.RenderSystem +import terramodulus.mui.gms.Component + +/** + * This can act as a placeholder [Component] in a [Layout][terramodulus.mui.gms.Layout]. + */ +class BlankComponent : Component() { + override fun render(renderSystem: RenderSystem) {} +} diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/impl/FlexibleBoxLayout.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/FlexibleBoxLayout.kt new file mode 100644 index 00000000..8c62f470 --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/FlexibleBoxLayout.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.mui.gms.impl + +import terramodulus.mui.gfx.RectangleF +import terramodulus.mui.gms.Component +import terramodulus.mui.gms.Container +import terramodulus.mui.gms.Layout + +class FlexibleBoxLayout(container: Container) : Layout(container) { + override val components: Iterable + get() = TODO("Not yet implemented") + + override fun layout(rect: RectangleF) { + TODO("Not yet implemented") + } +} diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/impl/GeomComponent.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/GeomComponent.kt new file mode 100644 index 00000000..ab4e38d1 --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/GeomComponent.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.mui.gms.impl + +import terramodulus.mui.gfx.GuiGeometry +import terramodulus.mui.gfx.RenderSystem +import terramodulus.mui.gms.Component + +class GeomComponent(val geom: GuiGeometry) : Component() { + override fun render(renderSystem: RenderSystem) { + geom.render(renderSystem) + } +} diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/impl/LaunchingScreen.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/LaunchingScreen.kt new file mode 100644 index 00000000..31b918cb --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/LaunchingScreen.kt @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.mui.gms.impl + +import terramodulus.mui.gfx.AlphaFilter +import terramodulus.mui.gfx.Dimension2I +import terramodulus.mui.gfx.FullScaling +import terramodulus.mui.gfx.GuiRect +import terramodulus.mui.gfx.RectangleI +import terramodulus.mui.gfx.RenderSystem +import terramodulus.mui.gfx.SmartScaling +import terramodulus.mui.gms.Screen +import terramodulus.mui.gms.ScreenManager + +private val REF_SIZE = Dimension2I(800, 480) + +private val BG_COLOR = floatArrayOf(.145F, .776F, 0.768F) + +private const val ANI_DURATION = .75F // in second + +private const val PAUSE_DURATION = 1 // in second + +internal class LaunchingScreen(renderSystemHandle: RenderSystem.Handle) : Screen() { + private var stage = 0 + private var last = System.currentTimeMillis() // timestamp in milliseconds + private var alphaFilter = AlphaFilter(0F) + + init { + GeomComponent(GuiRect(0, 0, 800, 480, 37, 198, 196, 255)).apply { + geom.add(alphaFilter) + geom.add(FullScaling(REF_SIZE)) + addComponent(this) + } + SpriteComponent(RectangleI(0, 0, 300, 300), renderSystemHandle.loadTexture("/studio_logo.png")).apply { + sprite.add(alphaFilter) + sprite.add(SmartScaling.both(REF_SIZE.width, REF_SIZE.height, 300, 300)) + addComponent(this) + } + } + + override fun update(renderSystem: RenderSystem, screenManager: ScreenManager) { + val current = System.currentTimeMillis() + val elapsed = (current - last) / 1000F // elapsed time for this stage + when (stage) { + 0 -> if (elapsed >= ANI_DURATION) { + stage = 1 + last = current + alphaFilter.alpha = 1F + } else { + alphaFilter.alpha = elapsed / ANI_DURATION + } + + 1 -> if (elapsed >= PAUSE_DURATION) { + stage = 2 + last = current + } + + 2 -> if (elapsed >= ANI_DURATION) { + stage = 3 + last = current + alphaFilter.alpha = 0F + } else { + alphaFilter.alpha = 1 - elapsed / ANI_DURATION + } + + 3 -> screenManager.handle.reset(::ResourceLoadingScreen) + } + } + + override fun exit() {} +} diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/impl/PositioningComponent.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/PositioningComponent.kt new file mode 100644 index 00000000..3cffc424 --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/PositioningComponent.kt @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.mui.gms.impl + +import terramodulus.mui.gfx.RenderSystem +import terramodulus.mui.gms.Component + +class PositioningComponent : Component() { + override fun render(renderSystem: RenderSystem) { + TODO("Not yet implemented") + } +} diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/impl/ResourceLoadingScreen.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/ResourceLoadingScreen.kt new file mode 100644 index 00000000..cbb87b89 --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/ResourceLoadingScreen.kt @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.mui.gms.impl + +import terramodulus.mui.gfx.AlphaFilter +import terramodulus.mui.gfx.Dimension2I +import terramodulus.mui.gfx.FullScaling +import terramodulus.mui.gfx.GuiRect +import terramodulus.mui.gfx.RectangleI +import terramodulus.mui.gfx.RenderSystem +import terramodulus.mui.gfx.SmartScaling +import terramodulus.mui.gms.Screen +import terramodulus.mui.gms.ScreenManager + +private val REF_SIZE = Dimension2I(800, 480) + +private val CONTENT_SIZE = Dimension2I(400, 200) + +private val BG_COLOR = floatArrayOf(.145F, .776F, 0.768F) + +private const val ANI_DURATION = 1F // in second + +class ResourceLoadingScreen(renderSystemHandle: RenderSystem.Handle) : Screen() { + private var stage = 0 + private var last = System.currentTimeMillis() // timestamp in milliseconds + private var alphaFilter = AlphaFilter(0F) + + init { + GeomComponent(GuiRect(0, 0, 800, 480, 37, 198, 196, 255)).apply { + geom.add(alphaFilter) + geom.add(FullScaling(REF_SIZE)) + addComponent(this) + } + val smartScaling = SmartScaling.both(REF_SIZE.width, REF_SIZE.height, CONTENT_SIZE.width, CONTENT_SIZE.height) + SpriteComponent(RectangleI(0, 100, 400, 100), renderSystemHandle.loadTexture("/game_logo.png")).apply { + sprite.add(alphaFilter) + sprite.add(smartScaling) + addComponent(this) + } + GeomComponent(GuiRect(0, 0, 400, 40, 240, 240, 240, 255)).apply { + geom.add(alphaFilter) + geom.add(smartScaling) + addComponent(this) + } + GeomComponent(GuiRect(5, 5, 390, 30, 37, 198, 196, 255)).apply { + geom.add(alphaFilter) + geom.add(smartScaling) + addComponent(this) + } + GeomComponent(GuiRect(8, 8, 384, 24, 240, 240, 240, 255)).apply { + geom.add(alphaFilter) + geom.add(smartScaling) + addComponent(this) + } + } + + override fun update(renderSystem: RenderSystem, screenManager: ScreenManager) { + val current = System.currentTimeMillis() + val elapsed = (current - last) / 1000F // elapsed time for this stage + when (stage) { + 0 -> if (elapsed >= ANI_DURATION) { + stage = 1 + last = current + alphaFilter.alpha = 1F + } else { + alphaFilter.alpha = elapsed / ANI_DURATION + } + + 1 -> { + + } + + 2 -> if (elapsed >= ANI_DURATION) { + stage = 3 + last = current + alphaFilter.alpha = 0F + } else { + alphaFilter.alpha = 1 - elapsed / ANI_DURATION + } + + 3 -> screenManager.handle.openBefore(::TitleScreen, this) + } + } + + override fun exit() { + + } +} diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/impl/SequenceLayout.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/SequenceLayout.kt new file mode 100644 index 00000000..7b0fb4fb --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/SequenceLayout.kt @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.mui.gms.impl + +import terramodulus.mui.gfx.RectangleF +import terramodulus.mui.gms.Component +import terramodulus.mui.gms.Container +import terramodulus.mui.gms.Layout + +/** + * Common implementation that is either [ColumnLayout] or [RowLayout]. + * + * This is an optimized special version of [FlexibleBoxLayout] without any expected + * multiple *sequences* of components in a single layout. + */ +sealed class SequenceLayout(container: Container, protected val elements: ElementList) : Layout(container) { + final override val components = elements.componentsView + + class Element { + companion object { + fun default() = Element() + } + } +} + +/** + * **Column** case of [SequenceLayout]. + */ +class ColumnLayout private constructor(container: Container, elements: ElementList) : + SequenceLayout(container, elements) { + companion object { + fun withComponents(vararg components: Component) = { it: Container -> + ColumnLayout(it, ElementList.withComponentsDefault(Element::default, *components)) + } + + fun withComponents(components: Collection) = { it: Container -> + ColumnLayout(it, ElementList.withComponentsDefault(Element::default, components)) + } + + fun withElements(vararg elements: Pair) = { it: Container -> + ColumnLayout(it, ElementList.withElements(*elements)) + } + + fun withElements(elements: Map) = { it: Container -> + ColumnLayout(it, ElementList.withElements(elements)) + } + } + + override fun layout(rect: RectangleF) { + elements.forEach { TODO() } + } +} + +/** + * **Row** case of [SequenceLayout]. + */ +class RowLayout private constructor(container: Container, elements: ElementList) : + SequenceLayout(container, elements) { + companion object { + fun withComponents(vararg components: Component) = { it: Container -> + RowLayout(it, ElementList.withComponentsDefault(Element::default, *components)) + } + + fun withComponents(components: Collection) = { it: Container -> + RowLayout(it, ElementList.withComponentsDefault(Element::default, components)) + } + + fun withElements(vararg elements: Pair) = { it: Container -> + RowLayout(it, ElementList.withElements(*elements)) + } + + fun withElements(elements: Map) = { it: Container -> + RowLayout(it, ElementList.withElements(elements)) + } + } + + override fun layout(rect: RectangleF) { + elements.forEach { TODO() } + } +} diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/impl/SpriteComponent.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/SpriteComponent.kt new file mode 100644 index 00000000..70c96c2e --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/SpriteComponent.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.mui.gms.impl + +import terramodulus.mui.gfx.GuiSprite +import terramodulus.mui.gfx.RenderSystem +import terramodulus.mui.gms.Component + +class SpriteComponent(val sprite: GuiSprite) : Component() { + override fun render(renderSystem: RenderSystem) { + sprite.render(renderSystem) + } +} diff --git a/src/kernel/client/kotlin/terramodulus/mui/gms/impl/TitleScreen.kt b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/TitleScreen.kt new file mode 100644 index 00000000..ad9a3dca --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/mui/gms/impl/TitleScreen.kt @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.mui.gms.impl + +import terramodulus.mui.gfx.RenderSystem +import terramodulus.mui.gms.Screen + +class TitleScreen(renderSystemHandle: RenderSystem.Handle) : Screen() { + override fun exit() {} +} diff --git a/src/kernel/client/kotlin/terramodulus/mui/input/InputSystem.kt b/src/kernel/client/kotlin/terramodulus/mui/input/InputSystem.kt new file mode 100644 index 00000000..fe1b1b39 --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/mui/input/InputSystem.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.mui.input + +class InputSystem internal constructor() { +} diff --git a/src/kernel/client/kotlin/terramodulus/settings/Preference.kt b/src/kernel/client/kotlin/terramodulus/settings/Preference.kt new file mode 100644 index 00000000..a4f00778 --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/settings/Preference.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.settings + +class Preference { +} diff --git a/src/kernel/client/kotlin/terramodulus/settings/Settings.kt b/src/kernel/client/kotlin/terramodulus/settings/Settings.kt new file mode 100644 index 00000000..3aff410f --- /dev/null +++ b/src/kernel/client/kotlin/terramodulus/settings/Settings.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.settings + +class Settings { +} diff --git a/src/kernel/client/resources/game_logo.png b/src/kernel/client/resources/game_logo.png new file mode 100644 index 00000000..e730454d Binary files /dev/null and b/src/kernel/client/resources/game_logo.png differ diff --git a/src/kernel/client/resources/gms_geo.fsh b/src/kernel/client/resources/gms_geo.fsh new file mode 100644 index 00000000..d697ce6b --- /dev/null +++ b/src/kernel/client/resources/gms_geo.fsh @@ -0,0 +1,9 @@ +#version 110 + +varying vec4 texColor; + +uniform mat4 filter; + +void main() { + gl_FragColor = filter * texColor; +} diff --git a/src/kernel/client/resources/gms_geo.vsh b/src/kernel/client/resources/gms_geo.vsh new file mode 100644 index 00000000..94d46f5b --- /dev/null +++ b/src/kernel/client/resources/gms_geo.vsh @@ -0,0 +1,14 @@ +#version 110 + +attribute vec2 pos; +attribute vec4 color; + +varying vec4 texColor; + +uniform mat4 model; +uniform mat4 projection; + +void main() { + gl_Position = projection * model * vec4(pos, 0.0, 1.0); + texColor = color; +} diff --git a/src/kernel/client/resources/gms_tex.fsh b/src/kernel/client/resources/gms_tex.fsh new file mode 100644 index 00000000..d561ac67 --- /dev/null +++ b/src/kernel/client/resources/gms_tex.fsh @@ -0,0 +1,10 @@ +#version 110 + +varying vec2 texCoord; + +uniform sampler2D tex; +uniform mat4 filter; + +void main() { + gl_FragColor = filter * texture2D(tex, texCoord); +} diff --git a/src/kernel/client/resources/gms_tex.vsh b/src/kernel/client/resources/gms_tex.vsh new file mode 100644 index 00000000..2881ea5a --- /dev/null +++ b/src/kernel/client/resources/gms_tex.vsh @@ -0,0 +1,14 @@ +#version 110 + +attribute vec2 pos; +attribute vec2 coord; + +varying vec2 texCoord; + +uniform mat4 model; +uniform mat4 projection; + +void main() { + gl_Position = projection * model * vec4(pos, 0.0, 1.0); + texCoord = coord; +} diff --git a/src/kernel/client/resources/studio_logo.png b/src/kernel/client/resources/studio_logo.png new file mode 100644 index 00000000..bee167f3 Binary files /dev/null and b/src/kernel/client/resources/studio_logo.png differ diff --git a/src/kernel/common/kotlin/terramodulus/common/core/AbstractTerraModulus.kt b/src/kernel/common/kotlin/terramodulus/common/core/AbstractTerraModulus.kt new file mode 100644 index 00000000..256b08a9 --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/common/core/AbstractTerraModulus.kt @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.common.core + +import java.io.Closeable + +/** + * Do not use/extend this outside TerraModulus game code. + * + * If this is Multiplatform, this could be `expect` with subclasses `actual`. + * + * @constructor Cannot be `internal` because `client` and `server` are other modules. + */ +abstract class AbstractTerraModulus : Closeable { + abstract var tps: Int + protected set + + abstract fun run() +} diff --git a/src/kernel/common/kotlin/terramodulus/common/core/Core.kt b/src/kernel/common/kotlin/terramodulus/common/core/Core.kt new file mode 100644 index 00000000..afad88aa --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/common/core/Core.kt @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.common.core + +import joptsimple.OptionException +import terramodulus.engine.initEngine +import terramodulus.util.exception.Error +import terramodulus.util.exception.Fault +import terramodulus.util.exception.UnhandledExceptionFault +import terramodulus.util.exception.triggerGlobalCrash +import terramodulus.util.logging.initLogging +import terramodulus.util.logging.logger +import java.io.Closeable +import java.io.File +import java.io.RandomAccessFile +import java.nio.channels.FileChannel +import java.nio.channels.FileLock +import java.nio.file.Files +import java.nio.file.Path + +private val logger = run { + initLogging() + logger {} +} + +/** + * This should only be used by `terramodulus.core.Main` in the beginning of `main` function. + */ +fun setupInit() { + Thread.setDefaultUncaughtExceptionHandler { _, e -> + triggerGlobalCrash(UnhandledExceptionFault.global(e)) + } + + logger.info {"System Properties:\n${ + System.getProperties().entries.joinToString(separator = "\n") { + "\t${it.key}=${it.value}" + } + }"} + + initEngine() +} + +fun run(game: AbstractTerraModulus) { + try { + game.run() + } catch (e: Throwable) { + triggerGlobalCrash(UnhandledExceptionFault.scoped("game.run()", e)) + } finally { + game.close() + } +} + +/** + * This should only be used by `terramodulus.core.Main`. + */ +class ApplicationInitializationFault(cause: Throwable) : + Fault("Failed to initialize application", cause) + +class ApplicationArgumentParsingError(cause: OptionException) : + Error("Failed to parse application arguments", cause) + +/** Checks whether this instance is the first process launched in the game data directory. */ +fun checkSingleInstance(dir: Path) { + if (dir.parent != null) Files.createDirectories(dir.parent) + val lockFile = File(dir.toFile(), "session.lock") + if (lockFile.createNewFile()) { + logger.info { "LOCK file is created." } + } else { + logger.info { "LOCK file exists." } + } + + SessionLockFile(RandomAccessFile(lockFile, "rw")) +} + +/** + * @throws java.io.IOException + */ +class SessionLockFile internal constructor(private val file: RandomAccessFile) : Closeable { + private val channel: FileChannel = file.channel + private val lock: FileLock = channel.lock() + + /** + * @throws java.io.IOException + */ + override fun close() { + lock.close() + channel.close() + file.close() + } +} diff --git a/src/kernel/common/kotlin/terramodulus/core/Core.kt b/src/kernel/common/kotlin/terramodulus/core/Core.kt new file mode 100644 index 00000000..46dcb456 --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/core/Core.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.core + +const val NAME = "TerraModulus" +const val VERSION = "0.1.0" // TODO placeholder diff --git a/src/kernel/common/kotlin/terramodulus/core/package-info.java b/src/kernel/common/kotlin/terramodulus/core/package-info.java new file mode 100644 index 00000000..923995bd --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/core/package-info.java @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +/** + * Critical components + */ +package terramodulus.core; diff --git a/src/kernel/common/kotlin/terramodulus/util/exception/CodeLogicFault.kt b/src/kernel/common/kotlin/terramodulus/util/exception/CodeLogicFault.kt new file mode 100644 index 00000000..3a1f7d25 --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/util/exception/CodeLogicFault.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.util.exception + +/** + * Code logic problems or bugs are regarded as defects in the software code and thus + * should be emitted as [Fault]s. This alerts that a made assumption or assertion was false. + * + * @param cause generally accepts [IllegalArgumentException], [IllegalStateException], + * [IndexOutOfBoundsException], [ArithmeticException], [NumberFormatException], + * [NullPointerException], [AssertionError], [ClassCastException], [NoSuchElementException], + * [ConcurrentModificationException], [ArrayStoreException], etc. + */ +class CodeLogicFault(cause: Throwable) : Fault("Illogical code", cause) diff --git a/src/kernel/common/kotlin/terramodulus/util/exception/Core.kt b/src/kernel/common/kotlin/terramodulus/util/exception/Core.kt new file mode 100644 index 00000000..99b1c628 --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/util/exception/Core.kt @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.util.exception + +import terramodulus.util.logging.logger + +private val logger = logger {} + +/** + * Records the recovered [exception] to logger. + * This logs the `exception` as a warning. + */ +fun recordException(exception: Exception) { + +} + +/** + * Pushes the recovered [exception] to application notification and records it to logger. + * This logs the `exception` as a warning. + */ +fun notifyAndRecordException(exception: Exception) { + // TODO exception notification + recordException(exception) +} + +/** + * Records the suppressed [error] to logger. + * This logs the `exception` as an error. + */ +fun recordError(error: Error) { + +} + +/** + * Pushes the suppressed [error] to application notification and records it to logger. + * This logs the `error` as an error. + */ +fun notifyAndRecordError(error: Error) { + // TODO exception notification + recordError(error) +} + +fun triggerSessionCrash() { + TODO() +} + +fun triggerGlobalCrash(error: Error): Nothing { + logger.error(error) {} + TODO() +} diff --git a/src/kernel/common/kotlin/terramodulus/util/exception/Error.kt b/src/kernel/common/kotlin/terramodulus/util/exception/Error.kt new file mode 100644 index 00000000..8539effb --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/util/exception/Error.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.util.exception + +abstract class Error(override val message: String, override val cause: Throwable?) : Exception(message, cause) { + constructor(message: String) : this(message, null) +} diff --git a/src/kernel/common/kotlin/terramodulus/util/exception/Exception.kt b/src/kernel/common/kotlin/terramodulus/util/exception/Exception.kt new file mode 100644 index 00000000..947c9fff --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/util/exception/Exception.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.util.exception + +abstract class Exception(override val message: String, override val cause: Throwable?) : Throwable(message, cause) { + constructor(message: String) : this(message, null) +} diff --git a/src/kernel/common/kotlin/terramodulus/util/exception/Failure.kt b/src/kernel/common/kotlin/terramodulus/util/exception/Failure.kt new file mode 100644 index 00000000..c918f712 --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/util/exception/Failure.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.util.exception + +abstract class Failure(override val message: String, override val cause: Throwable?) : Exception(message, cause) { + constructor(message: String) : this(message, null) +} diff --git a/src/kernel/common/kotlin/terramodulus/util/exception/Fault.kt b/src/kernel/common/kotlin/terramodulus/util/exception/Fault.kt new file mode 100644 index 00000000..a4f578cf --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/util/exception/Fault.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.util.exception + +abstract class Fault(override val message: String, override val cause: Throwable?) : Error(message, cause) { + constructor(message: String) : this(message, null) +} diff --git a/src/kernel/common/kotlin/terramodulus/util/exception/FerriciaEngineFault.kt b/src/kernel/common/kotlin/terramodulus/util/exception/FerriciaEngineFault.kt new file mode 100644 index 00000000..a649dcf0 --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/util/exception/FerriciaEngineFault.kt @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.util.exception + +class FerriciaEngineFault(message: String) : Fault(message) diff --git a/src/kernel/common/kotlin/terramodulus/util/exception/UnhandledExceptionFault.kt b/src/kernel/common/kotlin/terramodulus/util/exception/UnhandledExceptionFault.kt new file mode 100644 index 00000000..90fcfa76 --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/util/exception/UnhandledExceptionFault.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.util.exception + +class UnhandledExceptionFault private constructor(message: String, cause: Throwable) : Fault(message, cause) { + companion object { + fun global(cause: Throwable) = + UnhandledExceptionFault("Unknown unhandled exception", cause) + + fun scoped(scope: String, cause: Throwable) = + UnhandledExceptionFault("Unknown unhandled exception in $scope", cause) + } +} diff --git a/src/kernel/common/kotlin/terramodulus/util/exception/package-info.java b/src/kernel/common/kotlin/terramodulus/util/exception/package-info.java new file mode 100644 index 00000000..2299de90 --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/util/exception/package-info.java @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +/** + * This package refers to EFP 7 + * for implementation and standardization information. + */ +package terramodulus.util.exception; diff --git a/src/kernel/common/kotlin/terramodulus/util/logging/Core.kt b/src/kernel/common/kotlin/terramodulus/util/logging/Core.kt new file mode 100644 index 00000000..0adf7b81 --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/util/logging/Core.kt @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.util.logging + +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.Level as KLevel +import io.github.oshai.kotlinlogging.KotlinLogging +import org.apache.logging.log4j.Level +import org.apache.logging.log4j.status.StatusData +import org.apache.logging.log4j.status.StatusListener +import org.apache.logging.log4j.status.StatusLogger + +fun logger(func: () -> Unit): KLogger = KotlinLogging.logger(func) + +internal fun initLogging() { + System.setProperty("log4j2.debug", "true") + StatusLogger.getLogger().registerListener(object : StatusListener { + override fun close() {} + + override fun log(data: StatusData) { + logger.at(when (data.level) { + Level.FATAL -> KLevel.ERROR + Level.ERROR -> KLevel.ERROR + Level.WARN -> KLevel.WARN + Level.INFO -> KLevel.INFO + Level.DEBUG -> KLevel.DEBUG + Level.TRACE -> KLevel.TRACE + else -> throw IllegalArgumentException("Unsupported log level: ${data.level}") + }) { + cause = data.throwable + message = data.message.formattedMessage + } + } + + override fun getStatusLevel() = Level.ALL + }) +} + +private val logger = logger {} diff --git a/src/kernel/common/kotlin/terramodulus/util/logging/DelayedFileAppender.kt b/src/kernel/common/kotlin/terramodulus/util/logging/DelayedFileAppender.kt new file mode 100644 index 00000000..76a47b32 --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/util/logging/DelayedFileAppender.kt @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.util.logging + +import org.apache.logging.log4j.core.Appender +import org.apache.logging.log4j.core.Layout +import org.apache.logging.log4j.core.LogEvent +import org.apache.logging.log4j.core.appender.AbstractAppender +import org.apache.logging.log4j.core.appender.MemoryMappedFileAppender +import org.apache.logging.log4j.core.config.Configuration +import org.apache.logging.log4j.core.config.Node +import org.apache.logging.log4j.core.config.Property +import org.apache.logging.log4j.core.config.plugins.Plugin +import org.apache.logging.log4j.core.config.plugins.PluginAttribute +import org.apache.logging.log4j.core.config.plugins.PluginConfiguration +import org.apache.logging.log4j.core.config.plugins.PluginElement +import org.apache.logging.log4j.core.config.plugins.PluginFactory +import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required +import java.io.Serializable +import java.lang.management.ManagementFactory +import java.text.SimpleDateFormat +import java.util.Date +import java.util.concurrent.TimeUnit + +/** + * A wrapper over the underlying [MemoryMappedFileAppender] with the interoperation with single-instance checking. + * + * Note that this is not ready for generic public utility interface since some bridges are ignored + * due to the internal usage of this [Appender]. This includes features related to asynchronicity and filters. + */ +@Suppress("unused") +@Plugin(name = "DelayedFile", category = Node.CATEGORY, elementType = Appender.ELEMENT_TYPE, printObject = true) +internal class DelayedFileAppender private constructor( + name: String, + layout: Layout?, + config: Configuration, +) : AbstractAppender(name, null, layout, true, Property.EMPTY_ARRAY) { + private var session: Session = Session.Initial(DataFactory(name, layout, config)) + + init { + State.appender() + } + + /** + * With bridges of Life Cycle from + * [AbstractOutputStreamAppender][org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender] + */ + private sealed interface Session { + /** @see org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.start */ + fun start() + + /** @see org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.stop */ + fun stop(timeout: Long, timeUnit: TimeUnit?): Boolean + + /** @see org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.append */ + fun append(event: LogEvent) + + /** + * The initial state of the appender, which has not been initialized for the process. + * + * Life cycle is not applicable here, so NO-OP is applied for relevant functions. + * All [LogEvent] instances passed to this appender are stored temporary until the + * appender is initialized, and all stored events would be passed immediately afterward. + */ + class Initial(val data: DataFactory) : Session { + private val events: MutableList = ArrayList() + + fun buildFinal(): Final { + val timestamp = Date(ManagementFactory.getRuntimeMXBean().startTime) + val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS") + val final = Final(MemoryMappedFileAppender.Builder() + .setName(data.name) + .setLayout(data.layout) + .setConfiguration(data.config) + .setFileName("${format.format(timestamp)}.log") + .build()) + events.forEach(final::append) + return final + } + + override fun start() {} + + override fun stop(timeout: Long, timeUnit: TimeUnit?): Boolean = true + + override fun append(event: LogEvent) { + events.add(event) + } + } + + /** The initialized state of the appender, ready for use. */ + class Final(val fileAppender: MemoryMappedFileAppender) : Session { + override fun start() = fileAppender.start() + + override fun stop(timeout: Long, timeUnit: TimeUnit?): Boolean = fileAppender.stop(timeout, timeUnit) + + override fun append(event: LogEvent) = fileAppender.append(event) + } + } + + private class DataFactory( + val name: String, + val layout: Layout?, + val config: Configuration, + ) + + companion object { + @PluginFactory + @JvmStatic + fun createAppender( + @PluginConfiguration config: Configuration, + @PluginAttribute("name") @Required name: String, + @PluginElement("Layout") layout: Layout?, + ): Appender = DelayedFileAppender(name, layout, config) + } + + override fun start() = super.start() + + override fun stop(timeout: Long, timeUnit: TimeUnit?): Boolean = super.stop(timeout, timeUnit) + + override fun append(event: LogEvent) = session.append(event) +} diff --git a/src/kernel/common/kotlin/terramodulus/util/logging/State.kt b/src/kernel/common/kotlin/terramodulus/util/logging/State.kt new file mode 100644 index 00000000..cd0daed9 --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/util/logging/State.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.util.logging + +internal object State { + fun appender() { + logger {}.info { "Appender" } + } + + fun core() { + logger {}.info { "Core" } + } +} diff --git a/src/kernel/common/kotlin/terramodulus/world/WorldManager.kt b/src/kernel/common/kotlin/terramodulus/world/WorldManager.kt new file mode 100644 index 00000000..97e42c9a --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/world/WorldManager.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.world + +class WorldManager { +} diff --git a/src/kernel/common/kotlin/terramodulus/world/item/Items.kt b/src/kernel/common/kotlin/terramodulus/world/item/Items.kt new file mode 100644 index 00000000..2e87f1b6 --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/world/item/Items.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.world.item + +class Items { +} diff --git a/src/kernel/common/kotlin/terramodulus/world/tile/Tiles.kt b/src/kernel/common/kotlin/terramodulus/world/tile/Tiles.kt new file mode 100644 index 00000000..39aacee5 --- /dev/null +++ b/src/kernel/common/kotlin/terramodulus/world/tile/Tiles.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.world.tile + +class Tiles { +} diff --git a/src/kernel/common/resources/log4j2.xml b/src/kernel/common/resources/log4j2.xml new file mode 100644 index 00000000..8a0e4037 --- /dev/null +++ b/src/kernel/common/resources/log4j2.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/kernel/server/kotlin/terramodulus/core/Main.kt b/src/kernel/server/kotlin/terramodulus/core/Main.kt new file mode 100644 index 00000000..a14f1062 --- /dev/null +++ b/src/kernel/server/kotlin/terramodulus/core/Main.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.core + +import terramodulus.common.core.run +import terramodulus.common.core.setupInit + +fun main(args: Array) { + setupInit() + parseArgs(args) + println("java.library.path = ${System.getProperty("java.library.path")}") + run(TerraModulus()) +} + +fun parseArgs(args: Array) { + +} diff --git a/src/kernel/server/kotlin/terramodulus/core/TerraModulus.kt b/src/kernel/server/kotlin/terramodulus/core/TerraModulus.kt new file mode 100644 index 00000000..7cb78315 --- /dev/null +++ b/src/kernel/server/kotlin/terramodulus/core/TerraModulus.kt @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package terramodulus.core + +import terramodulus.common.core.AbstractTerraModulus + +class TerraModulus internal constructor() : AbstractTerraModulus() { + override var tps: Int + get() = TODO("Not yet implemented") + set(value) {} + + override fun run() { + TODO("Not yet implemented") + } + + override fun close() { + TODO("Not yet implemented") + } +}