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 {
// 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
}

View file

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

View file

@ -136,7 +136,6 @@
<string name="network__host__label">host</string>
<string name="network__port__label">port</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__status_state">État de connexion : %1$s</string>
<string name="network__socket__status_connected">Connecté</string>

View file

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

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

View file

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

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__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,

View file

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

View file

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

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
#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"
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"}
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 {
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)
}

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

@ -28,4 +28,6 @@ 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

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.repository.network.protocol
package com.pixelized.server.lwa.protocol
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