Add server & shared module and remove the serveur from the client app.
This commit is contained in:
		
							parent
							
								
									fa87f05be6
								
							
						
					
					
						commit
						3419afbe59
					
				
					 47 changed files with 233 additions and 530 deletions
				
			
		| 
						 | 
				
			
			@ -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<NettyApplicationEngine, NettyApplicationEngine.Configuration>
 | 
			
		||||
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<Message>()
 | 
			
		||||
| 
						 | 
				
			
			@ -47,65 +46,14 @@ class NetworkRepository {
 | 
			
		|||
    private val _status = MutableStateFlow(Status.DISCONNECTED)
 | 
			
		||||
    val status: StateFlow<Status> get() = _status
 | 
			
		||||
 | 
			
		||||
    private val _type = MutableStateFlow(Type.NONE)
 | 
			
		||||
    val type: StateFlow<Type> 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<Message>(json)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun Json.encodeToFrame(message: Message): Frame {
 | 
			
		||||
    val json = encodeToJsonElement(message)
 | 
			
		||||
    return Frame.Text(text = json.toString())
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.repository.network.protocol
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
data class Message(
 | 
			
		||||
    val from: String,
 | 
			
		||||
    val value: MessageContent,
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.repository.network.protocol
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
sealed interface MessageContent
 | 
			
		||||
| 
						 | 
				
			
			@ -1,12 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.repository.network.protocol
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
data class RollMessage(
 | 
			
		||||
    val skillLabel: String,
 | 
			
		||||
    val resultLabel: String?,
 | 
			
		||||
    val rollDifficulty: String?,
 | 
			
		||||
    val rollValue: Int,
 | 
			
		||||
    val rollSuccessLimit: Int?,
 | 
			
		||||
) : MessageContent
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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())
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue