diff --git a/build.gradle.kts b/build.gradle.kts index eb8e169..c255cba 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,7 @@ plugins { - // this is necessary to avoid the plugins to be loaded multiple times - // in each subproject's classloader - alias(libs.plugins.kotlinMultiplatform) apply false - alias(libs.plugins.kotlinSerialization) apply false - // alias(libs.plugins.kotlinKtor) apply false alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.kotlinSerialization) apply false alias(libs.plugins.composeCompiler) apply false + alias(libs.plugins.kotlinJvm) apply false + alias(libs.plugins.kotlinMultiplatform) apply false } \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 78f9ff2..2dc5255 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -29,16 +29,15 @@ kotlin { api(libs.koin.core) implementation(libs.koin.compose) implementation(libs.koin.compose.viewmodel) - // network. + // common + implementation(projects.shared) + // network implementation(libs.kotlinx.serialization.json) implementation(libs.ktor.client.core) implementation(libs.ktor.client.cio) implementation(libs.ktor.client.websockets) - implementation(libs.ktor.server.core) - implementation(libs.ktor.server.netty) - implementation(libs.ktor.server.websockets) // shell - implementation(libs.lordcodes.turttle) + implementation(libs.turtle) } commonTest.dependencies { diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index ce157a1..2d1506b 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -136,7 +136,6 @@ host port Se connecter à la table - Héberger la table Se déconnecter État de connexion : %1$s Connecté diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt index 95cb46d..c3deaea 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt @@ -1,15 +1,17 @@ package com.pixelized.desktop.lwa.repository.network +//import com.pixelized.desktop.lwa.repository.network.helper.server +//import io.ktor.server.engine.EmbeddedServer +//import io.ktor.server.netty.NettyApplicationEngine import com.pixelized.desktop.lwa.repository.network.helper.client import com.pixelized.desktop.lwa.repository.network.helper.connectWebSocket -import com.pixelized.desktop.lwa.repository.network.helper.server -import com.pixelized.desktop.lwa.repository.network.protocol.Message -import com.pixelized.desktop.lwa.repository.network.protocol.MessageContent +import com.pixelized.desktop.lwa.utils.extention.decodeFromFrame +import com.pixelized.desktop.lwa.utils.extention.encodeToFrame +import com.pixelized.server.lwa.SERVER_PORT +import com.pixelized.server.lwa.protocol.Message +import com.pixelized.server.lwa.protocol.MessageContent import io.ktor.client.HttpClient -import io.ktor.server.engine.EmbeddedServer -import io.ktor.server.netty.NettyApplicationEngine import io.ktor.websocket.Frame -import io.ktor.websocket.readText import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -21,20 +23,17 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json -import kotlinx.serialization.json.encodeToJsonElement -typealias Server = EmbeddedServer typealias Client = HttpClient class NetworkRepository { companion object { - const val DEFAULT_PORT = 16030 + const val DEFAULT_PORT = SERVER_PORT const val DEFAULT_HOST = "pixelized.freeboxos.fr" } private val scope = CoroutineScope(Dispatchers.IO) private var networkJob: Job? = null - private var server: Server? = null private var client: Client? = null private val outgoingMessageBuffer = MutableSharedFlow() @@ -47,65 +46,14 @@ class NetworkRepository { private val _status = MutableStateFlow(Status.DISCONNECTED) val status: StateFlow get() = _status - private val _type = MutableStateFlow(Type.NONE) - val type: StateFlow get() = _type - fun onPlayerNameChange(player: String) { _player.value = player } - fun host( - port: Int, - ) { - _type.value = Type.SERVER - _status.value = Status.CONNECTED - - server = server(port = port) { - println("Server launched") - - val job = launch { - // send local message to the clients - outgoingMessageBuffer.collect { message -> - send(Json.encodeToFrame(message = message)) - } - } - runCatching { - // watching for clients incoming message - incoming.consumeEach { frame -> - if (frame is Frame.Text) { - val message = Json.decodeFromFrame(frame = frame) - incomingMessageBuffer.emit(message) - // broadcast to clients the message - outgoingMessageBuffer.emit(message) - } - } - }.onFailure { exception -> - // TODO - println("WebSocket exception: ${exception.localizedMessage}") - }.also { - job.cancel() - } - } - - networkJob?.cancel() - networkJob = scope.launch { - try { - server?.start(wait = true) - } catch (exception: Exception) { - // TODO - println("WebSocket exception: ${exception.localizedMessage}") - } finally { - println("Server close") - _type.value = Type.NONE - _status.value = Status.DISCONNECTED - } - } - } - fun connect( host: String, port: Int, - onConnect: (Type) -> Unit = { }, + onConnect: () -> Unit = { }, onFailure: (Exception) -> Unit = { }, onClose: () -> Unit = { }, ) { @@ -115,9 +63,8 @@ class NetworkRepository { networkJob = scope.launch { try { client?.connectWebSocket(host = host, port = port) { - _type.value = Type.CLIENT _status.value = Status.CONNECTED - onConnect(Type.CLIENT) + onConnect() val job = launch { // send message to the server @@ -140,7 +87,6 @@ class NetworkRepository { } catch (exception: Exception) { onFailure(exception) } finally { - _type.value = Type.NONE _status.value = Status.DISCONNECTED onClose() } @@ -150,7 +96,6 @@ class NetworkRepository { fun disconnect() { networkJob?.cancel() scope.launch { - server?.stop() client?.close() } } @@ -165,10 +110,6 @@ class NetworkRepository { ) // emit the message into the outgoing buffer outgoingMessageBuffer.emit(message) - // emit the message into the incoming buffer IF we are the server - if (type.value == Type.SERVER) { - incomingMessageBuffer.emit(message) - } } } @@ -176,20 +117,4 @@ class NetworkRepository { CONNECTED, DISCONNECTED } - - enum class Type { - CLIENT, - SERVER, - NONE, - } -} - -private fun Json.decodeFromFrame(frame: Frame.Text): Message { - val json = frame.readText() - return decodeFromString(json) -} - -private fun Json.encodeToFrame(message: Message): Frame { - val json = encodeToJsonElement(message) - return Frame.Text(text = json.toString()) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/helper/Server.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/helper/Server.kt deleted file mode 100644 index ee202be..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/helper/Server.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.pixelized.desktop.lwa.repository.network.helper - -import io.ktor.server.application.install -import io.ktor.server.engine.EmbeddedServer -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.netty.NettyApplicationEngine -import io.ktor.server.routing.routing -import io.ktor.server.websocket.DefaultWebSocketServerSession -import io.ktor.server.websocket.WebSockets -import io.ktor.server.websocket.pingPeriod -import io.ktor.server.websocket.timeout -import io.ktor.server.websocket.webSocket -import kotlin.time.Duration.Companion.seconds - -// https://ktor.io/docs/server-websockets.html#handle-multiple-session -fun server( - port: Int, - handler: suspend DefaultWebSocketServerSession.() -> Unit -): EmbeddedServer { - return embeddedServer( - factory = Netty, - port = port, - module = { - install(WebSockets) { - pingPeriod = 15.seconds - timeout = 15.seconds - maxFrameSize = Long.MAX_VALUE - masking = false - } - routing { - webSocket( - path = "/ws", - handler = handler, - ) - } - }, - ) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/roll_history/RollHistoryRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/roll_history/RollHistoryRepository.kt index 1c045bb..b6b8a90 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/roll_history/RollHistoryRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/roll_history/RollHistoryRepository.kt @@ -1,8 +1,8 @@ package com.pixelized.desktop.lwa.repository.roll_history import com.pixelized.desktop.lwa.repository.network.NetworkRepository -import com.pixelized.desktop.lwa.repository.network.protocol.Message -import com.pixelized.desktop.lwa.repository.network.protocol.RollMessage +import com.pixelized.server.lwa.protocol.Message +import com.pixelized.server.lwa.protocol.RollMessage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharedFlow diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkFactory.kt index dbfbc2a..e9ec5e3 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkFactory.kt @@ -1,14 +1,12 @@ package com.pixelized.desktop.lwa.screen.network import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status -import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Type class NetworkFactory { fun convertToUio( player: String, status: Status, - type: Type, host: String, port: Int, ): NetworkPageUio { @@ -18,7 +16,7 @@ class NetworkFactory { port = "$port", enableFields = status == Status.DISCONNECTED, enableActions = status == Status.DISCONNECTED && player.isNotBlank() && host.isNotBlank() && port > 0, - enableCancel = type != Type.NONE && status == Status.CONNECTED + enableCancel = status == Status.CONNECTED ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkPage.kt index 27bb1e9..0b5c07f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkPage.kt @@ -48,11 +48,9 @@ import lwacharactersheet.composeapp.generated.resources.network__player_name__la import lwacharactersheet.composeapp.generated.resources.network__port__label import lwacharactersheet.composeapp.generated.resources.network__socket__connect_action import lwacharactersheet.composeapp.generated.resources.network__socket__disconnect_action -import lwacharactersheet.composeapp.generated.resources.network__socket__host_action import lwacharactersheet.composeapp.generated.resources.network__title import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel -import org.koin.core.annotation.KoinExperimentalAPI @Stable data class NetworkPageUio( @@ -64,7 +62,6 @@ data class NetworkPageUio( val enableCancel: Boolean, ) -@OptIn(KoinExperimentalAPI::class) @Composable fun NetworkPage( viewModel: NetworkViewModel = koinViewModel(), @@ -91,7 +88,6 @@ fun NetworkPage( onHostChange = viewModel::onHostChange, onPortChange = viewModel::onPortChange, onConnect = viewModel::connect, - onHost = viewModel::host, onDisconnect = viewModel::disconnect, ) } @@ -133,7 +129,6 @@ private fun NetworkContent( onHostChange: (String) -> Unit, onPortChange: (String) -> Unit, onConnect: () -> Unit, - onHost: () -> Unit, onDisconnect: () -> Unit, ) { Scaffold( @@ -207,13 +202,6 @@ private fun NetworkContent( Text(text = stringResource(Res.string.network__socket__connect_action)) } - TextButton( - enabled = player.value.enableActions, - onClick = onHost, - ) { - Text(text = stringResource(Res.string.network__socket__host_action)) - } - TextButton( enabled = player.value.enableCancel, onClick = onDisconnect, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkViewModel.kt index f753541..e747022 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkViewModel.kt @@ -20,7 +20,6 @@ class NetworkViewModel( private val repository: NetworkRepository, private val factory: NetworkFactory ) : ViewModel() { - private val host = mutableStateOf(NetworkRepository.DEFAULT_HOST) private val port = mutableStateOf(NetworkRepository.DEFAULT_PORT) @@ -41,13 +40,11 @@ class NetworkViewModel( get() { val player = repository.player.collectAsState() val status = repository.status.collectAsState() - val type = repository.type.collectAsState() return remember { derivedStateOf { factory.convertToUio( player = player.value, status = status.value, - type = type.value, host = host.value, port = port.value, ) @@ -67,10 +64,6 @@ class NetworkViewModel( this.host.value = host } - fun host() { - repository.host(port = port.value) - } - fun connect() { controller.show() _isLoading.value = true diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/rollhistory/RollHistoryViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/rollhistory/RollHistoryViewModel.kt index 0171c61..202164f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/rollhistory/RollHistoryViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/rollhistory/RollHistoryViewModel.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.pixelized.desktop.lwa.repository.network.protocol.RollMessage +import com.pixelized.server.lwa.protocol.RollMessage import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository import kotlinx.coroutines.launch diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/JsonExt.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/JsonExt.kt new file mode 100644 index 0000000..4c40882 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/JsonExt.kt @@ -0,0 +1,17 @@ +package com.pixelized.desktop.lwa.utils.extention + +import com.pixelized.server.lwa.protocol.Message +import io.ktor.websocket.Frame +import io.ktor.websocket.readText +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToJsonElement + +fun Json.decodeFromFrame(frame: Frame.Text): Message { + val json = frame.readText() + return decodeFromString(json) +} + +fun Json.encodeToFrame(message: Message): Frame { + val json = encodeToJsonElement(message) + return Frame.Text(text = json.toString()) +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index ddad6aa..1097b62 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,7 @@ kotlin.code.style=official kotlin.daemon.jvmargs=-Xmx2048M #Gradle -org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 \ No newline at end of file +org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 + +#Ktor +io.ktor.development=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 55ad362..b76ae76 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,14 +5,19 @@ kotlinx-json = "1.7.3" compose-multiplatform = "1.7.0" androidx-lifecycle = "2.8.3" androidx-navigation = "2.8.0-alpha10" -ktor_version = "3.0.0" +ktor = "3.0.1" koin = "4.0.0" +turtle = "0.5.0" +logback = "1.5.11" + [plugins] composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +ktor = { id = "io.ktor.plugin", version.ref = "ktor" } [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -29,11 +34,12 @@ koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } -ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor_version" } -ktor-client-cio = { group = 'io.ktor', name = "ktor-client-cio", version.ref = "ktor_version" } -ktor-client-websockets = { group = 'io.ktor', name = "ktor-client-websockets", version.ref = "ktor_version" } -ktor-server-core = { group = 'io.ktor', name = "ktor-server-core", version.ref = "ktor_version" } -ktor-server-netty = { group = 'io.ktor', name = "ktor-server-netty", version.ref = "ktor_version" } -ktor-server-websockets = { group = 'io.ktor', name = "ktor-server-websockets", version.ref = "ktor_version" } +ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } +ktor-client-cio = { group = 'io.ktor', name = "ktor-client-cio", version.ref = "ktor" } +ktor-client-websockets = { group = 'io.ktor', name = "ktor-client-websockets", version.ref = "ktor" } +ktor-server-core = { group = 'io.ktor', name = "ktor-server-core", version.ref = "ktor" } +ktor-server-netty = { group = 'io.ktor', name = "ktor-server-netty", version.ref = "ktor" } +ktor-server-websockets = { group = 'io.ktor', name = "ktor-server-websockets", version.ref = "ktor" } -lordcodes-turttle = { group="com.lordcodes.turtle", name="turtle", version="0.5.0"} \ No newline at end of file +turtle = { group = "com.lordcodes.turtle", name = "turtle", version.ref = "turtle" } +logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 494178b..0dc6d93 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -1,44 +1,23 @@ plugins { - alias(libs.plugins.androidApplication) - alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.ktor) + alias(libs.plugins.kotlinSerialization) + application } -android { - namespace = "com.pixelized.server" - compileSdk = 34 +group = "com.pixelized.server.lwa" +version = "1.0.0" - defaultConfig { - applicationId = "com.pixelized.server" - minSdk = 24 - targetSdk = 34 - versionCode = 1 - versionName = "1.0" - - testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = "1.8" - } +application { + mainClass.set("com.pixelized.server.lwa.ApplicationKt") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=${extra["io.ktor.development"] ?: "false"}") } dependencies { - - implementation(libs.appcompat.v7) - testImplementation(libs.junit) - androidTestImplementation(libs.runner) - androidTestImplementation(libs.espresso.core) + implementation(projects.shared) + implementation(libs.kotlinx.serialization.json) + implementation(libs.logback) + implementation(libs.ktor.server.core) + implementation(libs.ktor.server.netty) + implementation(libs.ktor.server.websockets) } \ No newline at end of file diff --git a/server/proguard-rules.pro b/server/proguard-rules.pro deleted file mode 100644 index 481bb43..0000000 --- a/server/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/server/src/androidTest/java/com/pixelized/server/ExampleInstrumentedTest.kt b/server/src/androidTest/java/com/pixelized/server/ExampleInstrumentedTest.kt deleted file mode 100644 index 6b7d46f..0000000 --- a/server/src/androidTest/java/com/pixelized/server/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.pixelized.server - -import android.support.test.InstrumentationRegistry -import android.support.test.runner.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.pixelized.server", appContext.packageName) - } -} \ No newline at end of file diff --git a/server/src/main/AndroidManifest.xml b/server/src/main/AndroidManifest.xml deleted file mode 100644 index a2157ec..0000000 --- a/server/src/main/AndroidManifest.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/Application.kt b/server/src/main/kotlin/com/pixelized/server/lwa/Application.kt new file mode 100644 index 0000000..70e6e5e --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/Application.kt @@ -0,0 +1,7 @@ +package com.pixelized.server.lwa + +import com.pixelized.server.lwa.server.LocalServer + +fun main() { + LocalServer().create().start() +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt b/server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt new file mode 100644 index 0000000..a3b7a7c --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt @@ -0,0 +1,17 @@ +package com.pixelized.server.lwa.extention + +import com.pixelized.server.lwa.protocol.Message +import io.ktor.websocket.Frame +import io.ktor.websocket.readText +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToJsonElement + +fun Json.decodeFromFrame(frame: Frame.Text): Message { + val json = frame.readText() + return decodeFromString(json) +} + +fun Json.encodeToFrame(message: Message): Frame { + val json = encodeToJsonElement(message) + return Frame.Text(text = json.toString()) +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt new file mode 100644 index 0000000..16e39cf --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt @@ -0,0 +1,101 @@ +package com.pixelized.server.lwa.server + +import com.pixelized.server.lwa.SERVER_PORT +import com.pixelized.server.lwa.extention.decodeFromFrame +import io.ktor.server.application.install +import io.ktor.server.engine.EmbeddedServer +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.netty.NettyApplicationEngine +import io.ktor.server.routing.routing +import io.ktor.server.websocket.DefaultWebSocketServerSession +import io.ktor.server.websocket.WebSockets +import io.ktor.server.websocket.pingPeriod +import io.ktor.server.websocket.timeout +import io.ktor.server.websocket.webSocket +import io.ktor.websocket.Frame +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlin.time.Duration.Companion.seconds + +// https://ktor.io/docs/server-websockets.html#handle-multiple-session +typealias Server = EmbeddedServer + +class LocalServer { + private var server: Server? = null + private val scope = CoroutineScope(Dispatchers.IO) + private var networkJob: Job? = null + private val outgoingMessageBuffer = MutableSharedFlow() + + fun create(): LocalServer { + server = build { + println("Server launched") + + val job = launch { + // send local message to the clients + outgoingMessageBuffer.collect { frame -> + send(frame) + } + } + runCatching { + // watching for clients incoming message + incoming.consumeEach { frame -> + if (frame is Frame.Text) { + val message = Json.decodeFromFrame(frame = frame) + println(message) + // broadcast to clients the message + outgoingMessageBuffer.emit(frame) + } + } + }.onFailure { exception -> + println("WebSocket exception: ${exception.localizedMessage}") + }.also { + job.cancel() + } + } + return this + } + + fun start() { + runBlocking { + try { + server?.start(wait = true) + } catch (exception: Exception) { + // TODO + println("WebSocket exception: ${exception.localizedMessage}") + } finally { + println("Server close") + } + } + } + + private fun build( + port: Int = SERVER_PORT, + handler: suspend DefaultWebSocketServerSession.() -> Unit + ): EmbeddedServer { + return embeddedServer( + factory = Netty, + port = port, + module = { + install(WebSockets) { + pingPeriod = 15.seconds + timeout = 15.seconds + maxFrameSize = Long.MAX_VALUE + masking = false + } + routing { + webSocket( + path = "/ws", + handler = handler, + ) + } + }, + ) + } +} \ No newline at end of file diff --git a/server/src/main/res/drawable/ic_launcher_background.xml b/server/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/server/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/server/src/main/res/drawable/ic_launcher_foreground.xml b/server/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/server/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/server/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/server/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 6f3b755..0000000 --- a/server/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/server/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/server/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 6f3b755..0000000 --- a/server/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/server/src/main/res/mipmap-hdpi/ic_launcher.webp b/server/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/server/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/server/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/server/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/server/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/server/src/main/res/mipmap-mdpi/ic_launcher.webp b/server/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/server/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/server/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/server/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/server/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/server/src/main/res/mipmap-xhdpi/ic_launcher.webp b/server/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/server/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/server/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/server/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/server/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/server/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/server/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/server/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/server/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/server/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/server/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/server/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/server/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/server/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/server/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/server/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/server/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/server/src/main/res/values-night/themes.xml b/server/src/main/res/values-night/themes.xml deleted file mode 100644 index b112a1c..0000000 --- a/server/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/server/src/main/res/values/colors.xml b/server/src/main/res/values/colors.xml deleted file mode 100644 index f8c6127..0000000 --- a/server/src/main/res/values/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - \ No newline at end of file diff --git a/server/src/main/res/values/strings.xml b/server/src/main/res/values/strings.xml deleted file mode 100644 index 8d8a20c..0000000 --- a/server/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - server - \ No newline at end of file diff --git a/server/src/main/res/values/themes.xml b/server/src/main/res/values/themes.xml deleted file mode 100644 index 17c56b5..0000000 --- a/server/src/main/res/values/themes.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml new file mode 100644 index 0000000..3e11d78 --- /dev/null +++ b/server/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/server/src/test/java/com/pixelized/server/ExampleUnitTest.kt b/server/src/test/java/com/pixelized/server/ExampleUnitTest.kt deleted file mode 100644 index d95fbd9..0000000 --- a/server/src/test/java/com/pixelized/server/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.pixelized.server - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index ec1776f..ecb7e05 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,4 +28,6 @@ dependencyResolutionManagement { } } -include(":composeApp") \ No newline at end of file +include(":composeApp") +include(":server") +include(":shared") \ No newline at end of file diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts new file mode 100644 index 0000000..7028ceb --- /dev/null +++ b/shared/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) +} + +kotlin { + jvm() + + sourceSets { + commonMain.dependencies { + implementation(libs.kotlinx.serialization.json) + } + } +} + diff --git a/shared/src/commonMain/kotlin/com/pixelized/server/lwa/Constants.kt b/shared/src/commonMain/kotlin/com/pixelized/server/lwa/Constants.kt new file mode 100644 index 0000000..f87f00b --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/server/lwa/Constants.kt @@ -0,0 +1,3 @@ +package com.pixelized.server.lwa + +const val SERVER_PORT = 16030 \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/protocol/Message.kt b/shared/src/commonMain/kotlin/com/pixelized/server/lwa/protocol/Message.kt similarity index 68% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/protocol/Message.kt rename to shared/src/commonMain/kotlin/com/pixelized/server/lwa/protocol/Message.kt index 20904a0..cfe94c9 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/protocol/Message.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/server/lwa/protocol/Message.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.repository.network.protocol +package com.pixelized.server.lwa.protocol import kotlinx.serialization.Serializable diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/protocol/MessageContent.kt b/shared/src/commonMain/kotlin/com/pixelized/server/lwa/protocol/MessageContent.kt similarity index 58% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/protocol/MessageContent.kt rename to shared/src/commonMain/kotlin/com/pixelized/server/lwa/protocol/MessageContent.kt index bcfbb30..6c59cb9 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/protocol/MessageContent.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/server/lwa/protocol/MessageContent.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.repository.network.protocol +package com.pixelized.server.lwa.protocol import kotlinx.serialization.Serializable diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/protocol/RollMessage.kt b/shared/src/commonMain/kotlin/com/pixelized/server/lwa/protocol/RollMessage.kt similarity index 79% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/protocol/RollMessage.kt rename to shared/src/commonMain/kotlin/com/pixelized/server/lwa/protocol/RollMessage.kt index fddf0a8..55a76f4 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/protocol/RollMessage.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/server/lwa/protocol/RollMessage.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.repository.network.protocol +package com.pixelized.server.lwa.protocol import kotlinx.serialization.Serializable