From fa49d8ed22ee78f4467d96efdbb3cf7cf1c7b570 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Sat, 3 May 2025 19:31:22 +0200 Subject: [PATCH] Add server synchronization feature. --- .../drawable/ic_sync_24dp.xml | 9 +++ .../desktop/lwa/DataSyncViewModel.kt | 79 ++++++++++++------- .../campaign/text/TextMessageFactory.kt | 3 + .../screen/gamemaster/action/GMActionPage.kt | 13 +++ .../gamemaster/action/GMActionViewModel.kt | 14 ++++ .../lwa/model/alteration/AlterationStore.kt | 2 +- .../lwa/model/campaign/CampaignStore.kt | 2 +- .../model/character/CharacterSheetStore.kt | 4 +- .../lwa/model/inventory/InventoryStore.kt | 2 +- .../server/lwa/model/item/ItemStore.kt | 2 +- .../server/lwa/model/tag/TagStore.kt | 28 ++++--- .../com/pixelized/server/lwa/server/Engine.kt | 30 +++++++ .../factory/CharacterSheetJsonFactory.kt | 2 +- .../factory/CharacterSheetJsonV1Factory.kt | 2 +- .../lwa/protocol/websocket/GameAdminEvent.kt | 10 +++ 15 files changed, 154 insertions(+), 48 deletions(-) create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_sync_24dp.xml create mode 100644 shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/GameAdminEvent.kt diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_sync_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_sync_24dp.xml new file mode 100644 index 0000000..7e3b3e8 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_sync_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt index ba16d94..14c3bf4 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt @@ -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,24 +47,27 @@ 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 { - try { - tagRepository.updateAlterationTags() - alterationRepository.updateAlterationFlow() - tagRepository.updateCharacterTags() - characterRepository.updateCharacterPreviews() - campaignRepository.updateCampaign() - tagRepository.updateItemTags() - itemRepository.updateItemFlow() - } catch (exception: Exception) { - println(exception.message) // TODO proper exception handling - } - } + .onEach { updateRepository() } .launchIn(this) networkRepository.status @@ -70,20 +75,38 @@ class DataSyncViewModel( .flowOn(context = Dispatchers.IO) .flatMapLatest { campaignRepository.campaignFlow().map { it.instances } } .distinctUntilChanged() - .onEach { instances -> - instances.forEach { characterSheetId -> - try { - characterRepository.updateCharacterSheet( - characterSheetId = characterSheetId, - ) - inventoryRepository.updateInventoryFlow( - characterSheetId = characterSheetId, - ) - } catch (exception: Exception) { - println(exception.message) // TODO proper exception handling - } - } - } + .onEach { updateCharacterInstance(instances = it) } .launchIn(this) } + + private suspend fun updateRepository() { + try { + tagRepository.updateAlterationTags() + alterationRepository.updateAlterationFlow() + tagRepository.updateCharacterTags() + characterRepository.updateCharacterPreviews() + campaignRepository.updateCampaign() + tagRepository.updateItemTags() + itemRepository.updateItemFlow() + } catch (exception: Exception) { + println(exception.message) // TODO proper exception handling + } + } + + private suspend fun updateCharacterInstance( + instances: Set, + ) { + instances.forEach { characterSheetId -> + try { + characterRepository.updateCharacterSheet( + characterSheetId = characterSheetId, + ) + inventoryRepository.updateInventoryFlow( + characterSheetId = characterSheetId, + ) + } catch (exception: Exception) { + println(exception.message) // TODO proper exception handling + } + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt index ee9b771..caf7665 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt @@ -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 } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionPage.kt index 6ab0a45..fc30ffd 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionPage.kt @@ -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, + 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, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionViewModel.kt index 659c3bc..3a980c3 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionViewModel.kt @@ -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( diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt index ce74f7e..58a1113 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt @@ -41,7 +41,7 @@ class AlterationStore( fun alterationsFlow(): StateFlow> = alterationFlow - private fun updateAlterationFlow() { + fun updateAlterationFlow() { alterationFlow.value = try { load() } catch (exception: Exception) { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt index 5a499b4..f612cea 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt @@ -38,7 +38,7 @@ class CampaignStore( fun campaignFlow(): StateFlow = campaignFlow - private fun updateCampaignFlow() { + fun updateCampaignFlow() { campaignFlow.value = try { load() } catch (exception: Exception) { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt index 06ff442..f2b3545 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt @@ -40,7 +40,7 @@ class CharacterSheetStore( fun characterSheetsFlow(): StateFlow> = 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 { return directory diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt index 1bb13a5..b9f3fed 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt @@ -40,7 +40,7 @@ class InventoryStore( fun inventoryFlow(): StateFlow> = inventoryFlow - private suspend fun updateInventoryFlow() { + fun updateInventoryFlow() { inventoryFlow.value = try { load() } catch (exception: Exception) { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt index 0915684..cfea1ef 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt @@ -43,7 +43,7 @@ class ItemStore( fun itemsFlow(): StateFlow> = itemFlow - private fun updateItemsFlow() { + fun updateItemsFlow() { itemFlow.value = try { load() } catch (exception: Exception) { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt index 1486cde..b87d48d 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt @@ -33,18 +33,7 @@ class TagStore( val scope = CoroutineScope(Dispatchers.IO + Job()) // load the initial data scope.launch { - update( - flow = alterationTagsFlow, - file = alterationFile(), - ) - update( - flow = characterTagsFlow, - file = characterFile(), - ) - update( - flow = itemTagsFlow, - file = itemFile(), - ) + updateTagFlow() } } @@ -52,6 +41,21 @@ class TagStore( fun characterTags(): StateFlow> = characterTagsFlow fun itemTags(): StateFlow> = itemTagsFlow + fun updateTagFlow() { + update( + flow = alterationTagsFlow, + file = alterationFile(), + ) + update( + flow = characterTagsFlow, + file = characterFile(), + ) + update( + flow = itemTagsFlow, + file = itemFile(), + ) + } + private fun update( flow: MutableStateFlow>, file: File, diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt index e9b2849..55810bc 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt @@ -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() @@ -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. } } diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/factory/CharacterSheetJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/factory/CharacterSheetJsonFactory.kt index ae2bb8e..40bb456 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/factory/CharacterSheetJsonFactory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/factory/CharacterSheetJsonFactory.kt @@ -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) { diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/factory/CharacterSheetJsonV1Factory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/factory/CharacterSheetJsonV1Factory.kt index aa02201..deeac86 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/factory/CharacterSheetJsonV1Factory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/factory/CharacterSheetJsonV1Factory.kt @@ -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( diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/GameAdminEvent.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/GameAdminEvent.kt new file mode 100644 index 0000000..0c102af --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/GameAdminEvent.kt @@ -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 +} \ No newline at end of file