ReModel the Alteration system into subfiles.

This commit is contained in:
Thomas Andres Gomez 2025-03-21 18:49:22 +01:00
parent b314a28f82
commit 0d94362ca2
4 changed files with 115 additions and 155 deletions

View file

@ -1,8 +1,6 @@
package com.pixelized.server.lwa.model.alteration package com.pixelized.server.lwa.model.alteration
import com.pixelized.shared.lwa.model.alteration.AlterationJson import com.pixelized.shared.lwa.model.alteration.AlterationJson
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
import com.pixelized.shared.lwa.utils.PathProvider import com.pixelized.shared.lwa.utils.PathProvider
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -15,12 +13,10 @@ import java.io.File
class AlterationStore( class AlterationStore(
private val pathProvider: PathProvider, private val pathProvider: PathProvider,
private val campaignJsonFactory: CampaignJsonFactory,
private val json: Json, private val json: Json,
) { ) {
private val directory = File(pathProvider.alterationsPath()).also { it.mkdirs() } private val directory = File(pathProvider.alterationsPath()).also { it.mkdirs() }
private val alterationsFlow = MutableStateFlow<List<AlterationJson>>(emptyList()) private val alterationsFlow = MutableStateFlow<List<AlterationJson>>(emptyList())
private val activeFlow = MutableStateFlow<Map<String, List<String>>>(emptyMap())
init { init {
// build a coroutine scope for async calls // build a coroutine scope for async calls
@ -28,113 +24,44 @@ class AlterationStore(
// load the initial data // load the initial data
scope.launch { scope.launch {
updateAlterations() updateAlterations()
updateActiveAlterations()
} }
} }
fun alterationsFlow(): StateFlow<List<AlterationJson>> = alterationsFlow fun alterationsFlow(): StateFlow<List<AlterationJson>> = alterationsFlow
fun activeFlow(): StateFlow<Map<String, List<String>>> = activeFlow
private fun updateAlterations() { private fun updateAlterations() {
alterationsFlow.value = loadAlterations() alterationsFlow.value = try {
} loadAlterations()
private fun updateActiveAlterations() {
activeFlow.value = loadActiveAlterations()
}
private fun loadAlterations(): List<AlterationJson> {
return try {
val alterationFile = file()
val json = alterationFile.readText(charset = Charsets.UTF_8)
if (json.isBlank()) error("alterations file is empty")
this.json.decodeFromString<List<AlterationJson>>(json)
} catch (exception: Exception) { } catch (exception: Exception) {
// TODO log exception println(exception) // TODO proper exception handling
emptyList() emptyList()
} }
} }
private fun loadActiveAlterations(): Map<String, List<String>> { @Throws(
val mainFile = file() FileReadException::class,
val jsonExt = ".json" JsonConversionException::class,
)
private fun loadAlterations(): List<AlterationJson> {
return directory return directory
.listFiles() .listFiles()
?.filter { file ->
// guard ignore the main alteration file and non json files.
file.name != mainFile.name && file.name.contains(jsonExt)
}
?.mapNotNull { file -> ?.mapNotNull { file ->
// read the alteration file.
val json = try { val json = try {
file.readText(charset = Charsets.UTF_8) file.readText(charset = Charsets.UTF_8)
} catch (exception: Exception) { } catch (exception: Exception) {
throw FileReadException(root = exception) throw FileReadException(root = exception)
} }
// Guard, if the json is blank no alteration have been save, ignore this file.
if (json.isBlank()) {
return@mapNotNull null
}
try { try {
val alterationIds = this.json.decodeFromString<List<String>>(json) this.json.decodeFromString<AlterationJson>(json)
val characterInstanceId = file.name.dropLast(n = jsonExt.length)
characterInstanceId to alterationIds
} catch (exception: Exception) { } catch (exception: Exception) {
// TODO log exception
throw JsonConversionException(root = exception) throw JsonConversionException(root = exception)
} }
} }
?.toMap() ?: emptyList()
?: emptyMap()
}
fun toggleActiveAlteration(
characterInstanceId: Campaign.CharacterInstance.Id,
alterationId: String,
): Boolean {
val id = campaignJsonFactory.convertToJson(id = characterInstanceId)
// toggle the activation state
val characterActiveAlterationIds = activeFlow.value[id]
?.toMutableList()
?.toggle(alterationId = alterationId)
?: listOf(alterationId)
// build the json string to save
val json = try {
this.json.encodeToString(characterActiveAlterationIds)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
// write the file
try {
val file = file(id = id)
file.writeText(
text = json,
charset = Charsets.UTF_8,
)
} catch (exception: Exception) {
throw FileWriteException(root = exception)
}
// Update the dataflow.
activeFlow.value = activeFlow.value.toMutableMap().also {
it[id] = characterActiveAlterationIds
}
return true
}
private fun file(): File {
return File("${pathProvider.alterationsPath()}alterations.json")
}
private fun file(
id: String,
): File {
return File("${pathProvider.alterationsPath()}$id.json")
}
private fun MutableList<String>.toggle(alterationId: String): MutableList<String> {
if (contains(alterationId)) {
remove(alterationId)
} else {
add(alterationId)
}
return this
} }
sealed class AlterationStoreException(root: Exception) : Exception(root) sealed class AlterationStoreException(root: Exception) : Exception(root)

View file

@ -18,7 +18,7 @@ class CampaignStore(
private val factory: CampaignJsonFactory, private val factory: CampaignJsonFactory,
private val json: Json, private val json: Json,
) { ) {
private val flow = MutableStateFlow(value = Campaign.EMPTY) private val campaignFlow = MutableStateFlow(value = Campaign.EMPTY)
init { init {
// create the directory if needed. // create the directory if needed.
@ -27,28 +27,51 @@ class CampaignStore(
val scope = CoroutineScope(Dispatchers.IO + Job()) val scope = CoroutineScope(Dispatchers.IO + Job())
// load the initial data // load the initial data
scope.launch { scope.launch {
update() updateCampaignFromDisk()
} }
} }
fun campaignFlow(): StateFlow<Campaign> = flow fun campaignFlow(): StateFlow<Campaign> = campaignFlow
suspend fun update() { private fun updateCampaignFromDisk() {
flow.value = load() campaignFlow.value = try {
} loadCampaign()
suspend fun load(): Campaign {
return try {
val json = file().readText(charset = Charsets.UTF_8)
if (json.isBlank()) error("Campaign file is empty")
val campaign = this.json.decodeFromString<CampaignJson>(json)
factory.convertFromJson(campaign)
} catch (exception: Exception) { } catch (exception: Exception) {
println(exception) // TODO proper exception handling
Campaign.EMPTY Campaign.EMPTY
} }
} }
suspend fun save(campaign: Campaign) { @Throws(
FileReadException::class,
JsonConversionException::class,
)
fun loadCampaign(): Campaign {
val file = file()
val json = try {
file.readText(charset = Charsets.UTF_8)
} catch (exception: Exception) {
throw FileReadException(root = exception)
}
// Guard, if the file is empty we load a default campaign.
if (json.isBlank()) return Campaign.EMPTY
val campaign = try {
val data = this.json.decodeFromString<CampaignJson>(json)
factory.convertFromJson(data)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
return campaign
}
@Throws(
JsonConversionException::class,
FileWriteException::class,
)
fun save(campaign: Campaign) {
// convert the data to json format // convert the data to json format
val json = try { val json = try {
factory.convertToJson(data = campaign).let(json::encodeToString) factory.convertToJson(data = campaign).let(json::encodeToString)
@ -66,12 +89,13 @@ class CampaignStore(
throw FileWriteException(root = exception) throw FileWriteException(root = exception)
} }
// Update the dataflow. // Update the dataflow.
flow.value = campaign campaignFlow.value = campaign
} }
sealed class CampaignStoreException(root: Exception) : Exception(root) sealed class CampaignStoreException(root: Exception) : Exception(root)
class JsonConversionException(root: Exception) : CampaignStoreException(root) class JsonConversionException(root: Exception) : CampaignStoreException(root)
class FileWriteException(root: Exception) : CampaignStoreException(root) class FileWriteException(root: Exception) : CampaignStoreException(root)
class FileReadException(root: Exception) : CampaignStoreException(root)
private fun file(): File { private fun file(): File {
return File("${pathProvider.campaignPath()}campaign.json") return File("${pathProvider.campaignPath()}campaign.json")

View file

@ -18,7 +18,7 @@ class CharacterSheetService(
) { ) {
private val scope = CoroutineScope(Dispatchers.IO + Job()) private val scope = CoroutineScope(Dispatchers.IO + Job())
private val sheets get() = sheetsFlow.value private val sheets get() = sheetsFlow.value
private val sheetsFlow = store.characterSheetFlow() private val sheetsFlow = store.characterSheetsFlow()
.map { entry -> entry.associateBy { character -> character.id } } .map { entry -> entry.associateBy { character -> character.id } }
.stateIn( .stateIn(
scope = scope, scope = scope,

View file

@ -20,74 +20,33 @@ class CharacterSheetStore(
private val json: Json, private val json: Json,
) { ) {
private val directory = File(pathProvider.characterStorePath()).also { it.mkdirs() } private val directory = File(pathProvider.characterStorePath()).also { it.mkdirs() }
private val flow = MutableStateFlow<List<CharacterSheet>>(value = emptyList()) private val characterSheetsFlow = MutableStateFlow<List<CharacterSheet>>(value = emptyList())
init { init {
// build a coroutine scope for async calls
val scope = CoroutineScope(Dispatchers.IO + Job()) val scope = CoroutineScope(Dispatchers.IO + Job())
// load the initial data
scope.launch { scope.launch {
flow.value = load() updateCharacterSheets()
} }
} }
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> = flow fun characterSheetsFlow(): StateFlow<List<CharacterSheet>> = characterSheetsFlow
@Throws( private suspend fun updateCharacterSheets() {
CharacterSheetStoreException::class, characterSheetsFlow.value = try {
FileWriteException::class, loadCharacterSheets()
JsonConversionException::class,
)
fun save(sheet: CharacterSheet) {
// convert the character sheet into json format.
val json = try {
factory.convertToJson(sheet = sheet).let(json::encodeToString)
} catch (exception: Exception) { } catch (exception: Exception) {
throw JsonConversionException(root = exception) println(exception) // TODO proper exception handling
emptyList()
} }
// write the character file.
try {
val file = characterSheetFile(id = sheet.id)
file.writeText(
text = json,
charset = Charsets.UTF_8,
)
} catch (exception: Exception) {
throw FileWriteException(root = exception)
}
// Update the dataflow.
flow.value = flow.value
.toMutableList()
.also { data ->
val index = data.indexOfFirst { it.id == sheet.id }
if (index >= 0) {
data[index] = sheet
} else {
data.add(sheet)
}
}
.sortedWith(compareBy(Collator.getInstance()) { it.name })
}
fun delete(id: String): Boolean {
val file = characterSheetFile(id = id)
val deleted = file.delete()
if (deleted) {
flow.value = flow.value.toMutableList()
.also { data ->
data.removeIf { it.id == id }
}
.sortedBy {
it.name
}
}
return deleted
} }
@Throws( @Throws(
CharacterSheetStoreException::class,
FileReadException::class, FileReadException::class,
JsonConversionException::class, JsonConversionException::class,
) )
suspend fun load(): List<CharacterSheet> { suspend fun loadCharacterSheets(): List<CharacterSheet> {
return directory return directory
.listFiles() .listFiles()
?.mapNotNull { file -> ?.mapNotNull { file ->
@ -111,6 +70,56 @@ class CharacterSheetStore(
?: emptyList() ?: emptyList()
} }
@Throws(
FileWriteException::class,
JsonConversionException::class,
)
fun save(sheet: CharacterSheet) {
// convert the character sheet into json format.
val json = try {
factory.convertToJson(sheet = sheet).let(json::encodeToString)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
// write the character file.
try {
val file = characterSheetFile(id = sheet.id)
file.writeText(
text = json,
charset = Charsets.UTF_8,
)
} catch (exception: Exception) {
throw FileWriteException(root = exception)
}
// Update the dataflow.
characterSheetsFlow.value = characterSheetsFlow.value
.toMutableList()
.also { data ->
val index = data.indexOfFirst { it.id == sheet.id }
if (index >= 0) {
data[index] = sheet
} else {
data.add(sheet)
}
}
.sortedWith(compareBy(Collator.getInstance()) { it.name })
}
fun delete(id: String): Boolean {
val file = characterSheetFile(id = id)
val deleted = file.delete()
if (deleted) {
characterSheetsFlow.value = characterSheetsFlow.value.toMutableList()
.also { data ->
data.removeIf { it.id == id }
}
.sortedBy {
it.name
}
}
return deleted
}
private fun characterSheetFile(id: String): File { private fun characterSheetFile(id: String): File {
return File("${pathProvider.characterStorePath()}${id}.json") return File("${pathProvider.characterStorePath()}${id}.json")
} }