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
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -15,12 +13,10 @@ import java.io.File
class AlterationStore(
private val pathProvider: PathProvider,
private val campaignJsonFactory: CampaignJsonFactory,
private val json: Json,
) {
private val directory = File(pathProvider.alterationsPath()).also { it.mkdirs() }
private val alterationsFlow = MutableStateFlow<List<AlterationJson>>(emptyList())
private val activeFlow = MutableStateFlow<Map<String, List<String>>>(emptyMap())
init {
// build a coroutine scope for async calls
@ -28,113 +24,44 @@ class AlterationStore(
// load the initial data
scope.launch {
updateAlterations()
updateActiveAlterations()
}
}
fun alterationsFlow(): StateFlow<List<AlterationJson>> = alterationsFlow
fun activeFlow(): StateFlow<Map<String, List<String>>> = activeFlow
private fun updateAlterations() {
alterationsFlow.value = 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)
alterationsFlow.value = try {
loadAlterations()
} catch (exception: Exception) {
// TODO log exception
println(exception) // TODO proper exception handling
emptyList()
}
}
private fun loadActiveAlterations(): Map<String, List<String>> {
val mainFile = file()
val jsonExt = ".json"
@Throws(
FileReadException::class,
JsonConversionException::class,
)
private fun loadAlterations(): List<AlterationJson> {
return directory
.listFiles()
?.filter { file ->
// guard ignore the main alteration file and non json files.
file.name != mainFile.name && file.name.contains(jsonExt)
}
?.mapNotNull { file ->
// read the alteration file.
val json = try {
file.readText(charset = Charsets.UTF_8)
} catch (exception: 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 {
val alterationIds = this.json.decodeFromString<List<String>>(json)
val characterInstanceId = file.name.dropLast(n = jsonExt.length)
characterInstanceId to alterationIds
this.json.decodeFromString<AlterationJson>(json)
} catch (exception: Exception) {
// TODO log exception
throw JsonConversionException(root = exception)
}
}
?.toMap()
?: 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
?: emptyList()
}
sealed class AlterationStoreException(root: Exception) : Exception(root)

View file

@ -18,7 +18,7 @@ class CampaignStore(
private val factory: CampaignJsonFactory,
private val json: Json,
) {
private val flow = MutableStateFlow(value = Campaign.EMPTY)
private val campaignFlow = MutableStateFlow(value = Campaign.EMPTY)
init {
// create the directory if needed.
@ -27,28 +27,51 @@ class CampaignStore(
val scope = CoroutineScope(Dispatchers.IO + Job())
// load the initial data
scope.launch {
update()
updateCampaignFromDisk()
}
}
fun campaignFlow(): StateFlow<Campaign> = flow
fun campaignFlow(): StateFlow<Campaign> = campaignFlow
suspend fun update() {
flow.value = load()
}
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)
private fun updateCampaignFromDisk() {
campaignFlow.value = try {
loadCampaign()
} catch (exception: Exception) {
println(exception) // TODO proper exception handling
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
val json = try {
factory.convertToJson(data = campaign).let(json::encodeToString)
@ -66,12 +89,13 @@ class CampaignStore(
throw FileWriteException(root = exception)
}
// Update the dataflow.
flow.value = campaign
campaignFlow.value = campaign
}
sealed class CampaignStoreException(root: Exception) : Exception(root)
class JsonConversionException(root: Exception) : CampaignStoreException(root)
class FileWriteException(root: Exception) : CampaignStoreException(root)
class FileReadException(root: Exception) : CampaignStoreException(root)
private fun file(): File {
return File("${pathProvider.campaignPath()}campaign.json")

View file

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

View file

@ -20,74 +20,33 @@ class CharacterSheetStore(
private val json: Json,
) {
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 {
// build a coroutine scope for async calls
val scope = CoroutineScope(Dispatchers.IO + Job())
// load the initial data
scope.launch {
flow.value = load()
updateCharacterSheets()
}
}
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> = flow
fun characterSheetsFlow(): StateFlow<List<CharacterSheet>> = characterSheetsFlow
@Throws(
CharacterSheetStoreException::class,
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)
private suspend fun updateCharacterSheets() {
characterSheetsFlow.value = try {
loadCharacterSheets()
} 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(
CharacterSheetStoreException::class,
FileReadException::class,
JsonConversionException::class,
)
suspend fun load(): List<CharacterSheet> {
suspend fun loadCharacterSheets(): List<CharacterSheet> {
return directory
.listFiles()
?.mapNotNull { file ->
@ -111,6 +70,56 @@ class CharacterSheetStore(
?: 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 {
return File("${pathProvider.characterStorePath()}${id}.json")
}