Add server synchronization feature.

This commit is contained in:
Thomas Andres Gomez 2025-05-03 19:31:22 +02:00
parent 53c6aede27
commit fa49d8ed22
15 changed files with 154 additions and 48 deletions

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M160,800L160,720L270,720L254,706Q202,660 181,601Q160,542 160,482Q160,371 226.5,284.5Q293,198 400,170L400,254Q328,280 284,342.5Q240,405 240,482Q240,527 257,569.5Q274,612 310,648L320,658L320,560L400,560L400,800L160,800ZM560,790L560,706Q632,680 676,617.5Q720,555 720,478Q720,433 703,390.5Q686,348 650,312L640,302L640,400L560,400L560,160L800,160L800,240L690,240L706,254Q755,303 777.5,360.5Q800,418 800,478Q800,589 733.5,675.5Q667,762 560,790Z" />
</vector>

View file

@ -9,11 +9,13 @@ import com.pixelized.desktop.lwa.repository.item.ItemRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.repository.tag.TagRepository
import com.pixelized.shared.lwa.protocol.websocket.GameAdminEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
@ -45,12 +47,39 @@ class DataSyncViewModel(
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun synchronise() = coroutineScope {
networkRepository.data.onEach { println(it) }.launchIn(this)
networkRepository.data
.onEach { println(it) }
.launchIn(this)
networkRepository.status
.filter { status -> status == NetworkRepository.Status.CONNECTED }
.flatMapLatest { networkRepository.data }
.filterIsInstance(GameAdminEvent.ServerSynchronization::class)
.flowOn(context = Dispatchers.IO)
.onEach {
updateRepository()
updateCharacterInstance(
instances = campaignRepository.campaignFlow().value.instances
)
}
.launchIn(this)
networkRepository.status
.filter { status -> status == NetworkRepository.Status.CONNECTED }
.flowOn(context = Dispatchers.IO)
.onEach {
.onEach { updateRepository() }
.launchIn(this)
networkRepository.status
.filter { status -> status == NetworkRepository.Status.CONNECTED }
.flowOn(context = Dispatchers.IO)
.flatMapLatest { campaignRepository.campaignFlow().map { it.instances } }
.distinctUntilChanged()
.onEach { updateCharacterInstance(instances = it) }
.launchIn(this)
}
private suspend fun updateRepository() {
try {
tagRepository.updateAlterationTags()
alterationRepository.updateAlterationFlow()
@ -63,14 +92,10 @@ class DataSyncViewModel(
println(exception.message) // TODO proper exception handling
}
}
.launchIn(this)
networkRepository.status
.filter { status -> status == NetworkRepository.Status.CONNECTED }
.flowOn(context = Dispatchers.IO)
.flatMapLatest { campaignRepository.campaignFlow().map { it.instances } }
.distinctUntilChanged()
.onEach { instances ->
private suspend fun updateCharacterInstance(
instances: Set<String>,
) {
instances.forEach { characterSheetId ->
try {
characterRepository.updateCharacterSheet(
@ -84,6 +109,4 @@ class DataSyncViewModel(
}
}
}
.launchIn(this)
}
}

View file

@ -10,6 +10,7 @@ import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent
import com.pixelized.shared.lwa.protocol.websocket.GameAdminEvent
import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
import com.pixelized.shared.lwa.protocol.websocket.RollEvent
import com.pixelized.shared.lwa.protocol.websocket.SocketMessage
@ -140,6 +141,8 @@ class TextMessageFactory(
is GameMasterEvent -> null
is GameAdminEvent -> null
is ApiSynchronisation -> null
}
}

View file

@ -19,6 +19,7 @@ import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_camping_24dp
import lwacharactersheet.composeapp.generated.resources.ic_sync_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_off_24dp
import org.koin.compose.viewmodel.koinViewModel
@ -40,6 +41,11 @@ fun GMActionPage(
GMActionContent(
actions = actions,
scroll = scroll,
onServerSync = {
scope.launch {
viewModel.onServerSync()
}
},
onPartyHeal = {
scope.launch {
viewModel.onPartyHeal()
@ -68,6 +74,7 @@ fun GMActionContent(
scroll: ScrollState,
spacing: Dp = 8.dp,
actions: State<ActionPageUio?>,
onServerSync: () -> Unit,
onPartyHeal: () -> Unit,
onPartyVisibility: () -> Unit,
onNpcVisibility: () -> Unit,
@ -78,6 +85,12 @@ fun GMActionContent(
.padding(vertical = spacing, horizontal = spacing),
verticalArrangement = Arrangement.spacedBy(space = spacing),
) {
GMAction(
modifier = Modifier.fillMaxWidth(),
icon = Res.drawable.ic_sync_24dp,
label = "Syncrhonization du serveur",
onAction = onServerSync,
)
GMAction(
modifier = Modifier.fillMaxWidth(),
icon = Res.drawable.ic_camping_24dp,

View file

@ -6,6 +6,7 @@ import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio
import com.pixelized.shared.lwa.protocol.websocket.GameAdminEvent
import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
@ -38,6 +39,19 @@ class GMActionViewModel(
initialValue = null,
)
suspend fun onServerSync() {
try {
networkRepository.share(
GameAdminEvent.ServerSynchronization(
timestamp = System.currentTimeMillis(),
)
)
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception = exception)
_error.emit(message)
}
}
suspend fun onPartyHeal() {
campaignRepository.campaignFlow().value.characters.forEach { characterSheetId ->
val sheet = characterRepository.characterDetail(

View file

@ -41,7 +41,7 @@ class AlterationStore(
fun alterationsFlow(): StateFlow<List<Alteration>> = alterationFlow
private fun updateAlterationFlow() {
fun updateAlterationFlow() {
alterationFlow.value = try {
load()
} catch (exception: Exception) {

View file

@ -38,7 +38,7 @@ class CampaignStore(
fun campaignFlow(): StateFlow<Campaign> = campaignFlow
private fun updateCampaignFlow() {
fun updateCampaignFlow() {
campaignFlow.value = try {
load()
} catch (exception: Exception) {

View file

@ -40,7 +40,7 @@ class CharacterSheetStore(
fun characterSheetsFlow(): StateFlow<List<CharacterSheet>> = characterSheetsFlow
private suspend fun updateCharacterFlow() {
fun updateCharacterFlow() {
characterSheetsFlow.value = try {
load()
} catch (exception: Exception) {
@ -54,7 +54,7 @@ class CharacterSheetStore(
JsonCodingException::class,
JsonConversionException::class,
)
suspend fun load(
fun load(
directory: File = this.directory,
): List<CharacterSheet> {
return directory

View file

@ -40,7 +40,7 @@ class InventoryStore(
fun inventoryFlow(): StateFlow<Map<String, Inventory>> = inventoryFlow
private suspend fun updateInventoryFlow() {
fun updateInventoryFlow() {
inventoryFlow.value = try {
load()
} catch (exception: Exception) {

View file

@ -43,7 +43,7 @@ class ItemStore(
fun itemsFlow(): StateFlow<List<Item>> = itemFlow
private fun updateItemsFlow() {
fun updateItemsFlow() {
itemFlow.value = try {
load()
} catch (exception: Exception) {

View file

@ -33,6 +33,15 @@ class TagStore(
val scope = CoroutineScope(Dispatchers.IO + Job())
// load the initial data
scope.launch {
updateTagFlow()
}
}
fun alterationTags(): StateFlow<Map<String, TagJson>> = alterationTagsFlow
fun characterTags(): StateFlow<Map<String, TagJson>> = characterTagsFlow
fun itemTags(): StateFlow<Map<String, TagJson>> = itemTagsFlow
fun updateTagFlow() {
update(
flow = alterationTagsFlow,
file = alterationFile(),
@ -46,11 +55,6 @@ class TagStore(
file = itemFile(),
)
}
}
fun alterationTags(): StateFlow<Map<String, TagJson>> = alterationTagsFlow
fun characterTags(): StateFlow<Map<String, TagJson>> = characterTagsFlow
fun itemTags(): StateFlow<Map<String, TagJson>> = itemTagsFlow
private fun update(
flow: MutableStateFlow<Map<String, TagJson>>,

View file

@ -1,15 +1,22 @@
package com.pixelized.server.lwa.server
import com.pixelized.server.lwa.model.alteration.AlterationService
import com.pixelized.server.lwa.model.alteration.AlterationStore
import com.pixelized.server.lwa.model.campaign.CampaignService
import com.pixelized.server.lwa.model.campaign.CampaignStore
import com.pixelized.server.lwa.model.character.CharacterSheetService
import com.pixelized.server.lwa.model.character.CharacterSheetStore
import com.pixelized.server.lwa.model.inventory.InventoryService
import com.pixelized.server.lwa.model.inventory.InventoryStore
import com.pixelized.server.lwa.model.item.ItemService
import com.pixelized.server.lwa.model.item.ItemStore
import com.pixelized.server.lwa.model.tag.TagService
import com.pixelized.server.lwa.model.tag.TagStore
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent
import com.pixelized.shared.lwa.protocol.websocket.GameAdminEvent
import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
import com.pixelized.shared.lwa.protocol.websocket.RollEvent
import com.pixelized.shared.lwa.protocol.websocket.SocketMessage
@ -23,6 +30,12 @@ class Engine(
val inventoryService: InventoryService,
val tagService: TagService,
val campaignJsonFactory: CampaignJsonFactory,
private val campaignStore: CampaignStore,
private val characterStore: CharacterSheetStore,
private val alterationStore: AlterationStore,
private val itemStore: ItemStore,
private val inventoryStore: InventoryStore,
private val tagStore: TagStore,
) {
val webSocket = MutableSharedFlow<SocketMessage>()
@ -94,6 +107,23 @@ class Engine(
is CampaignEvent.UpdateScene -> Unit
}
is GameAdminEvent -> when (message) {
is GameAdminEvent.ServerSynchronization -> {
characterStore.updateCharacterFlow()
campaignStore.updateCampaignFlow()
alterationStore.updateAlterationFlow()
itemStore.updateItemsFlow()
inventoryStore.updateInventoryFlow()
tagStore.updateTagFlow()
webSocket.emit(
value = GameAdminEvent.ServerSynchronization(
timestamp = System.currentTimeMillis(),
),
)
}
}
is ApiSynchronisation -> Unit // Nothing to do there.
}
}

View file

@ -10,7 +10,7 @@ import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson
class CharacterSheetJsonFactory(
private val v1: CharacterSheetJsonV1Factory,
) {
suspend fun convertFromJson(
fun convertFromJson(
json: CharacterSheetJson,
): CharacterSheet {
return when (json) {

View file

@ -7,7 +7,7 @@ import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
class CharacterSheetJsonV1Factory(
private val characterSheetUseCase: CharacterSheetUseCase,
) {
suspend fun convertFromJson(
fun convertFromJson(
json: CharacterSheetJsonV1,
): CharacterSheet = characterSheetUseCase.run {
CharacterSheet(

View file

@ -0,0 +1,10 @@
package com.pixelized.shared.lwa.protocol.websocket
import kotlinx.serialization.Serializable
@Serializable
sealed interface GameAdminEvent : SocketMessage {
@Serializable
data class ServerSynchronization(override val timestamp: Long) : GameAdminEvent
}