Add server & shared module and remove the serveur from the client app.

This commit is contained in:
Thomas Andres Gomez 2024-11-29 18:19:54 +01:00
parent fa87f05be6
commit 3419afbe59
47 changed files with 233 additions and 530 deletions

View file

@ -1,9 +1,7 @@
plugins { 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.composeMultiplatform) apply false
alias(libs.plugins.kotlinSerialization) apply false
alias(libs.plugins.composeCompiler) apply false alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinJvm) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
} }

View file

@ -29,16 +29,15 @@ kotlin {
api(libs.koin.core) api(libs.koin.core)
implementation(libs.koin.compose) implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel) implementation(libs.koin.compose.viewmodel)
// network. // common
implementation(projects.shared)
// network
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio) implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.websockets) implementation(libs.ktor.client.websockets)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.websockets)
// shell // shell
implementation(libs.lordcodes.turttle) implementation(libs.turtle)
} }
commonTest.dependencies { commonTest.dependencies {

View file

@ -136,7 +136,6 @@
<string name="network__host__label">host</string> <string name="network__host__label">host</string>
<string name="network__port__label">port</string> <string name="network__port__label">port</string>
<string name="network__socket__connect_action">Se connecter à la table</string> <string name="network__socket__connect_action">Se connecter à la table</string>
<string name="network__socket__host_action">Héberger la table</string>
<string name="network__socket__disconnect_action">Se déconnecter</string> <string name="network__socket__disconnect_action">Se déconnecter</string>
<string name="network__socket__status_state">État de connexion : %1$s</string> <string name="network__socket__status_state">État de connexion : %1$s</string>
<string name="network__socket__status_connected">Connecté</string> <string name="network__socket__status_connected">Connecté</string>

View file

@ -1,15 +1,17 @@
package com.pixelized.desktop.lwa.repository.network 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.client
import com.pixelized.desktop.lwa.repository.network.helper.connectWebSocket import com.pixelized.desktop.lwa.repository.network.helper.connectWebSocket
import com.pixelized.desktop.lwa.repository.network.helper.server import com.pixelized.desktop.lwa.utils.extention.decodeFromFrame
import com.pixelized.desktop.lwa.repository.network.protocol.Message import com.pixelized.desktop.lwa.utils.extention.encodeToFrame
import com.pixelized.desktop.lwa.repository.network.protocol.MessageContent 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.client.HttpClient
import io.ktor.server.engine.EmbeddedServer
import io.ktor.server.netty.NettyApplicationEngine
import io.ktor.websocket.Frame import io.ktor.websocket.Frame
import io.ktor.websocket.readText
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -21,20 +23,17 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToJsonElement
typealias Server = EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration>
typealias Client = HttpClient typealias Client = HttpClient
class NetworkRepository { class NetworkRepository {
companion object { companion object {
const val DEFAULT_PORT = 16030 const val DEFAULT_PORT = SERVER_PORT
const val DEFAULT_HOST = "pixelized.freeboxos.fr" const val DEFAULT_HOST = "pixelized.freeboxos.fr"
} }
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
private var networkJob: Job? = null private var networkJob: Job? = null
private var server: Server? = null
private var client: Client? = null private var client: Client? = null
private val outgoingMessageBuffer = MutableSharedFlow<Message>() private val outgoingMessageBuffer = MutableSharedFlow<Message>()
@ -47,65 +46,14 @@ class NetworkRepository {
private val _status = MutableStateFlow(Status.DISCONNECTED) private val _status = MutableStateFlow(Status.DISCONNECTED)
val status: StateFlow<Status> get() = _status val status: StateFlow<Status> get() = _status
private val _type = MutableStateFlow(Type.NONE)
val type: StateFlow<Type> get() = _type
fun onPlayerNameChange(player: String) { fun onPlayerNameChange(player: String) {
_player.value = player _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( fun connect(
host: String, host: String,
port: Int, port: Int,
onConnect: (Type) -> Unit = { }, onConnect: () -> Unit = { },
onFailure: (Exception) -> Unit = { }, onFailure: (Exception) -> Unit = { },
onClose: () -> Unit = { }, onClose: () -> Unit = { },
) { ) {
@ -115,9 +63,8 @@ class NetworkRepository {
networkJob = scope.launch { networkJob = scope.launch {
try { try {
client?.connectWebSocket(host = host, port = port) { client?.connectWebSocket(host = host, port = port) {
_type.value = Type.CLIENT
_status.value = Status.CONNECTED _status.value = Status.CONNECTED
onConnect(Type.CLIENT) onConnect()
val job = launch { val job = launch {
// send message to the server // send message to the server
@ -140,7 +87,6 @@ class NetworkRepository {
} catch (exception: Exception) { } catch (exception: Exception) {
onFailure(exception) onFailure(exception)
} finally { } finally {
_type.value = Type.NONE
_status.value = Status.DISCONNECTED _status.value = Status.DISCONNECTED
onClose() onClose()
} }
@ -150,7 +96,6 @@ class NetworkRepository {
fun disconnect() { fun disconnect() {
networkJob?.cancel() networkJob?.cancel()
scope.launch { scope.launch {
server?.stop()
client?.close() client?.close()
} }
} }
@ -165,10 +110,6 @@ class NetworkRepository {
) )
// emit the message into the outgoing buffer // emit the message into the outgoing buffer
outgoingMessageBuffer.emit(message) 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, CONNECTED,
DISCONNECTED DISCONNECTED
} }
enum class Type {
CLIENT,
SERVER,
NONE,
}
}
private fun Json.decodeFromFrame(frame: Frame.Text): Message {
val json = frame.readText()
return decodeFromString<Message>(json)
}
private fun Json.encodeToFrame(message: Message): Frame {
val json = encodeToJsonElement(message)
return Frame.Text(text = json.toString())
} }

View file

@ -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<NettyApplicationEngine, NettyApplicationEngine.Configuration> {
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,
)
}
},
)
}

View file

@ -1,8 +1,8 @@
package com.pixelized.desktop.lwa.repository.roll_history package com.pixelized.desktop.lwa.repository.roll_history
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.network.protocol.Message import com.pixelized.server.lwa.protocol.Message
import com.pixelized.desktop.lwa.repository.network.protocol.RollMessage import com.pixelized.server.lwa.protocol.RollMessage
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow

View file

@ -1,14 +1,12 @@
package com.pixelized.desktop.lwa.screen.network package com.pixelized.desktop.lwa.screen.network
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Type
class NetworkFactory { class NetworkFactory {
fun convertToUio( fun convertToUio(
player: String, player: String,
status: Status, status: Status,
type: Type,
host: String, host: String,
port: Int, port: Int,
): NetworkPageUio { ): NetworkPageUio {
@ -18,7 +16,7 @@ class NetworkFactory {
port = "$port", port = "$port",
enableFields = status == Status.DISCONNECTED, enableFields = status == Status.DISCONNECTED,
enableActions = status == Status.DISCONNECTED && player.isNotBlank() && host.isNotBlank() && port > 0, enableActions = status == Status.DISCONNECTED && player.isNotBlank() && host.isNotBlank() && port > 0,
enableCancel = type != Type.NONE && status == Status.CONNECTED enableCancel = status == Status.CONNECTED
) )
} }
} }

View file

@ -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__port__label
import lwacharactersheet.composeapp.generated.resources.network__socket__connect_action 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__disconnect_action
import lwacharactersheet.composeapp.generated.resources.network__socket__host_action
import lwacharactersheet.composeapp.generated.resources.network__title import lwacharactersheet.composeapp.generated.resources.network__title
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.annotation.KoinExperimentalAPI
@Stable @Stable
data class NetworkPageUio( data class NetworkPageUio(
@ -64,7 +62,6 @@ data class NetworkPageUio(
val enableCancel: Boolean, val enableCancel: Boolean,
) )
@OptIn(KoinExperimentalAPI::class)
@Composable @Composable
fun NetworkPage( fun NetworkPage(
viewModel: NetworkViewModel = koinViewModel(), viewModel: NetworkViewModel = koinViewModel(),
@ -91,7 +88,6 @@ fun NetworkPage(
onHostChange = viewModel::onHostChange, onHostChange = viewModel::onHostChange,
onPortChange = viewModel::onPortChange, onPortChange = viewModel::onPortChange,
onConnect = viewModel::connect, onConnect = viewModel::connect,
onHost = viewModel::host,
onDisconnect = viewModel::disconnect, onDisconnect = viewModel::disconnect,
) )
} }
@ -133,7 +129,6 @@ private fun NetworkContent(
onHostChange: (String) -> Unit, onHostChange: (String) -> Unit,
onPortChange: (String) -> Unit, onPortChange: (String) -> Unit,
onConnect: () -> Unit, onConnect: () -> Unit,
onHost: () -> Unit,
onDisconnect: () -> Unit, onDisconnect: () -> Unit,
) { ) {
Scaffold( Scaffold(
@ -207,13 +202,6 @@ private fun NetworkContent(
Text(text = stringResource(Res.string.network__socket__connect_action)) 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( TextButton(
enabled = player.value.enableCancel, enabled = player.value.enableCancel,
onClick = onDisconnect, onClick = onDisconnect,

View file

@ -20,7 +20,6 @@ class NetworkViewModel(
private val repository: NetworkRepository, private val repository: NetworkRepository,
private val factory: NetworkFactory private val factory: NetworkFactory
) : ViewModel() { ) : ViewModel() {
private val host = mutableStateOf(NetworkRepository.DEFAULT_HOST) private val host = mutableStateOf(NetworkRepository.DEFAULT_HOST)
private val port = mutableStateOf(NetworkRepository.DEFAULT_PORT) private val port = mutableStateOf(NetworkRepository.DEFAULT_PORT)
@ -41,13 +40,11 @@ class NetworkViewModel(
get() { get() {
val player = repository.player.collectAsState() val player = repository.player.collectAsState()
val status = repository.status.collectAsState() val status = repository.status.collectAsState()
val type = repository.type.collectAsState()
return remember { return remember {
derivedStateOf { derivedStateOf {
factory.convertToUio( factory.convertToUio(
player = player.value, player = player.value,
status = status.value, status = status.value,
type = type.value,
host = host.value, host = host.value,
port = port.value, port = port.value,
) )
@ -67,10 +64,6 @@ class NetworkViewModel(
this.host.value = host this.host.value = host
} }
fun host() {
repository.host(port = port.value)
}
fun connect() { fun connect() {
controller.show() controller.show()
_isLoading.value = true _isLoading.value = true

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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 com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View file

@ -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<Message>(json)
}
fun Json.encodeToFrame(message: Message): Frame {
val json = encodeToJsonElement(message)
return Frame.Text(text = json.toString())
}

View file

@ -3,4 +3,7 @@ kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx2048M kotlin.daemon.jvmargs=-Xmx2048M
#Gradle #Gradle
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
#Ktor
io.ktor.development=true

View file

@ -5,14 +5,19 @@ kotlinx-json = "1.7.3"
compose-multiplatform = "1.7.0" compose-multiplatform = "1.7.0"
androidx-lifecycle = "2.8.3" androidx-lifecycle = "2.8.3"
androidx-navigation = "2.8.0-alpha10" androidx-navigation = "2.8.0-alpha10"
ktor_version = "3.0.0" ktor = "3.0.1"
koin = "4.0.0" koin = "4.0.0"
turtle = "0.5.0"
logback = "1.5.11"
[plugins] [plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 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" } kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
[libraries] [libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 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 = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", 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-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { group = 'io.ktor', name = "ktor-client-cio", version.ref = "ktor_version" } 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_version" } 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_version" } 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_version" } 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_version" } 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"} turtle = { group = "com.lordcodes.turtle", name = "turtle", version.ref = "turtle" }
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }

1
server/.gitignore vendored
View file

@ -1 +0,0 @@
/build

View file

@ -1,44 +1,23 @@
plugins { plugins {
alias(libs.plugins.androidApplication) alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinAndroid) alias(libs.plugins.ktor)
alias(libs.plugins.kotlinSerialization)
application
} }
android { group = "com.pixelized.server.lwa"
namespace = "com.pixelized.server" version = "1.0.0"
compileSdk = 34
defaultConfig { application {
applicationId = "com.pixelized.server" mainClass.set("com.pixelized.server.lwa.ApplicationKt")
minSdk = 24 applicationDefaultJvmArgs = listOf("-Dio.ktor.development=${extra["io.ktor.development"] ?: "false"}")
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"
}
} }
dependencies { dependencies {
implementation(projects.shared)
implementation(libs.appcompat.v7) implementation(libs.kotlinx.serialization.json)
testImplementation(libs.junit) implementation(libs.logback)
androidTestImplementation(libs.runner) implementation(libs.ktor.server.core)
androidTestImplementation(libs.espresso.core) implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.websockets)
} }

View file

@ -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

View file

@ -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)
}
}

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.LwaCharacterSheet" />
</manifest>

View file

@ -0,0 +1,7 @@
package com.pixelized.server.lwa
import com.pixelized.server.lwa.server.LocalServer
fun main() {
LocalServer().create().start()
}

View file

@ -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<Message>(json)
}
fun Json.encodeToFrame(message: Message): Frame {
val json = encodeToJsonElement(message)
return Frame.Text(text = json.toString())
}

View file

@ -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<NettyApplicationEngine, NettyApplicationEngine.Configuration>
class LocalServer {
private var server: Server? = null
private val scope = CoroutineScope(Dispatchers.IO)
private var networkJob: Job? = null
private val outgoingMessageBuffer = MutableSharedFlow<Frame>()
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<NettyApplicationEngine, NettyApplicationEngine.Configuration> {
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,
)
}
},
)
}
}

View file

@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.LwaCharacterSheet" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryDark">@color/purple_700</item>
<item name="colorAccent">@color/teal_200</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View file

@ -1,3 +0,0 @@
<resources>
<string name="app_name">server</string>
</resources>

View file

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.LwaCharacterSheet" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryDark">@color/purple_700</item>
<item name="colorAccent">@color/teal_200</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -0,0 +1,12 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="trace">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>
</configuration>

View file

@ -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)
}
}

View file

@ -29,3 +29,5 @@ dependencyResolutionManagement {
} }
include(":composeApp") include(":composeApp")
include(":server")
include(":shared")

15
shared/build.gradle.kts Normal file
View file

@ -0,0 +1,15 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
jvm()
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.serialization.json)
}
}
}

View file

@ -0,0 +1,3 @@
package com.pixelized.server.lwa
const val SERVER_PORT = 16030

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.repository.network.protocol package com.pixelized.server.lwa.protocol
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.repository.network.protocol package com.pixelized.server.lwa.protocol
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.repository.network.protocol package com.pixelized.server.lwa.protocol
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable