Server : Campaign error management

This commit is contained in:
Thomas Andres Gomez 2025-03-30 18:25:27 +02:00
parent acb445c480
commit 0e7bc87cde
12 changed files with 222 additions and 309 deletions

View file

@ -37,223 +37,95 @@ class CampaignService(
return campaignJsonFlow.value
}
@Throws
fun addCharacter(
characterSheetId: String,
): Boolean {
// check if the character is already in the campaign.
if (campaign.characters.contains(characterSheetId)) return false
// update the corresponding instance
val characters = campaign.characters.toMutableSet().also { it.add(characterSheetId) }
// save the campaign to the disk (update the flow).
return try {
) {
// Check if the character is already in the campaign.
if (campaign.instances.contains(characterSheetId)) {
val root = Exception("Character with id:$characterSheetId is already in the campaign.")
throw CampaignStore.BusinessException(root = root)
}
// Update the corresponding instance
val characters = campaign.characters.toMutableSet().also {
it.add(characterSheetId)
}
// Save the campaign to the disk (update the flow).
store.save(
campaign = campaign.copy(characters = characters)
)
true
} catch (exception: Exception) {
false
}
}
suspend fun addNpc(
@Throws
fun addNpc(
characterSheetId: String,
): Boolean {
// check if the character is already in the campaign.
if (campaign.npcs.contains(characterSheetId)) return false
// update the corresponding instance
val characters = campaign.npcs.toMutableSet().also { it.add(characterSheetId) }
// save the campaign to the disk (update the flow).
return try {
) {
// Check if the character is already in the campaign.
if (campaign.instances.contains(characterSheetId)) {
val root = Exception("Character with id:$characterSheetId is already in the campaign.")
throw CampaignStore.BusinessException(root = root)
}
// Update the corresponding instance
val characters = campaign.npcs.toMutableSet().also {
it.add(characterSheetId)
}
// Save the campaign to the disk (update the flow).
store.save(
campaign = campaign.copy(npcs = characters)
)
true
} catch (exception: Exception) {
false
}
}
suspend fun removeCharacter(
@Throws
fun removeCharacter(
characterSheetId: String,
): Boolean {
// check if the character is in the campaign.
if (campaign.characters.contains(characterSheetId).not()) return false
// update the corresponding instance
val characters = campaign.characters.toMutableSet().also { it.remove(characterSheetId) }
// save the campaign to the disk + update the flow.
return try {
) {
// Check if the character is in the campaign.
if (campaign.characters.contains(characterSheetId).not()) {
val root = Exception("Character with id:$characterSheetId is not in the party.")
throw CampaignStore.BusinessException(root = root)
}
// Update the corresponding instance
val characters = campaign.characters.toMutableSet().also {
it.remove(characterSheetId)
}
// Save the campaign to the disk + update the flow.
store.save(
campaign = campaign.copy(characters = characters)
)
true
} catch (exception: Exception) {
false
}
}
suspend fun removeNpc(
@Throws
fun removeNpc(
characterSheetId: String,
): Boolean {
// check if the character is in the campaign.
if (campaign.npcs.contains(characterSheetId).not()) return false
// update the corresponding instance
val characters = campaign.npcs.toMutableSet().also { it.remove(characterSheetId) }
// save the campaign to the disk + update the flow.
return try {
) {
// Check if the character is in the campaign.
if (campaign.npcs.contains(characterSheetId).not()) {
val root = Exception("Character with id:$characterSheetId is not in the npcs.")
throw CampaignStore.BusinessException(root = root)
}
// Update the corresponding instance
val characters = campaign.npcs.toMutableSet().also {
it.remove(characterSheetId)
}
// Save the campaign to the disk + update the flow.
store.save(
campaign = campaign.copy(npcs = characters)
)
true
} catch (exception: Exception) {
false
}
}
suspend fun removeInstance(
characterSheetId: String,
): Boolean {
return removeCharacter(characterSheetId) || removeNpc(characterSheetId)
}
suspend fun setScene(
@Throws
fun setScene(
scene: Campaign.Scene,
): Boolean {
) {
// save the campaign to the disk + update the flow.
return try {
store.save(
campaign.copy(scene = scene)
campaign = campaign.copy(scene = scene)
)
true
} catch (exception: Exception) {
false
}
}
// Data manipulation through WebSocket.
// suspend fun updateCharacteristic(
// characterInstanceId: Campaign.CharacterInstance.Id,
// characteristic: Campaign.CharacterInstance.Characteristic,
// value: Int,
// ) {
// when (characterInstanceId.prefix) {
// Campaign.CharacterInstance.Id.PLAYER -> {
// // fetch all the current campaign character
// val characters = campaign.characters.toMutableMap()
// // update the corresponding character using the use case.
// characters[characterInstanceId] = useCase.updateCharacteristic(
// instance = campaign.character(id = characterInstanceId),
// characteristic = characteristic,
// value = value,
// )
// // save the campaign to the disk + update the flow.
// store.save(
// campaign = campaign.copy(characters = characters)
// )
// }
//
// Campaign.CharacterInstance.Id.NPC -> {
// // fetch all the current campaign character
// val npcs = campaign.npcs.toMutableMap()
// // update the corresponding character using the use case.
// npcs[characterInstanceId] = useCase.updateCharacteristic(
// instance = campaign.npc(id = characterInstanceId),
// characteristic = characteristic,
// value = value,
// )
// // save the campaign to the disk + update the flow.
// store.save(
// campaign = campaign.copy(npcs = npcs)
// )
// }
// }
// }
//
// suspend fun updateDiminished(
// characterInstanceId: Campaign.CharacterInstance.Id,
// diminished: Int,
// ) {
// when (characterInstanceId.prefix) {
// Campaign.CharacterInstance.Id.PLAYER -> {
// // fetch all the current campaign character
// val characters = campaign.characters.toMutableMap()
// // update the corresponding character using the use case.
// characters[characterInstanceId] = useCase.updateDiminished(
// instance = campaign.character(id = characterInstanceId),
// diminished = diminished,
// )
// // save the campaign to the disk + update the flow.
// store.save(
// campaign = campaign.copy(characters = characters)
// )
// }
//
// Campaign.CharacterInstance.Id.NPC -> {
// // fetch all the current campaign character
// val npcs = campaign.npcs.toMutableMap()
// // update the corresponding character using the use case.
// npcs[characterInstanceId] = useCase.updateDiminished(
// instance = campaign.npc(id = characterInstanceId),
// diminished = diminished,
// )
// // save the campaign to the disk + update the flow.
// store.save(
// campaign = campaign.copy(npcs = npcs)
// )
// }
// }
// }
//
// suspend fun toggleAlteration(
// characterInstanceId: Campaign.CharacterInstance.Id,
// alterationId: String,
// ) {
// when (characterInstanceId.prefix) {
// Campaign.CharacterInstance.Id.PLAYER -> {
// // fetch all the current campaign character
// val characters = campaign.characters.toMutableMap()
// // update the corresponding character alterations
// characters[characterInstanceId]?.let { character ->
// characters[characterInstanceId] = character.copy(
// alterations = character.alterations.toMutableList().also { alterations ->
// if (alterations.contains(alterationId)) {
// alterations.remove(alterationId)
// } else {
// alterations.add(alterationId)
// }
// },
// )
// }
// // save the campaign to the disk + update the flow.
// store.save(
// campaign = campaign.copy(characters = characters)
// )
// }
//
// Campaign.CharacterInstance.Id.NPC -> {
// // fetch all the current campaign character
// val characters = campaign.npcs.toMutableMap()
// // update the corresponding character alterations
// characters[characterInstanceId]?.let { character ->
// characters[characterInstanceId] = character.copy(
// alterations = character.alterations.toMutableList().also { alterations ->
// if (alterations.contains(alterationId)) {
// alterations.remove(alterationId)
// } else {
// alterations.add(alterationId)
// }
// },
// )
// }
// // save the campaign to the disk + update the flow.
// store.save(
// campaign = campaign.copy(npcs = characters)
// )
// }
// }
// }
suspend fun updateToggleParty() {
fun updateToggleParty() {
store.save(
campaign = campaign.copy(
options = campaign.options.copy(
@ -263,7 +135,7 @@ class CampaignService(
)
}
suspend fun updateToggleNpc() {
fun updateToggleNpc() {
store.save(
campaign = campaign.copy(
options = campaign.options.copy(

View file

@ -1,5 +1,6 @@
package com.pixelized.server.lwa.model.campaign
import com.pixelized.server.lwa.model.alteration.AlterationStore.AlterationStoreException
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
@ -9,6 +10,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import java.io.File
@ -27,15 +29,15 @@ class CampaignStore(
val scope = CoroutineScope(Dispatchers.IO + Job())
// load the initial data
scope.launch {
updateCampaignFromDisk()
updateCampaignFlow()
}
}
fun campaignFlow(): StateFlow<Campaign> = campaignFlow
private fun updateCampaignFromDisk() {
private fun updateCampaignFlow() {
campaignFlow.value = try {
loadCampaign()
load()
} catch (exception: Exception) {
println(exception) // TODO proper exception handling
Campaign.empty()
@ -44,60 +46,74 @@ class CampaignStore(
@Throws(
FileReadException::class,
JsonCodingException::class,
JsonConversionException::class,
)
fun loadCampaign(): Campaign {
val file = file()
val json = try {
fun load(): Campaign {
val file = campaignFile()
// Read the campaign file.
val data = 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()
if (data.isBlank()) return Campaign.empty()
// Decode the json into a string.
val json = try {
this.json.decodeFromString<CampaignJson>(data)
} catch (exception: Exception) {
throw JsonCodingException(root = exception)
}
// Convert from the Json format
val campaign = try {
val data = this.json.decodeFromString<CampaignJson>(json)
factory.convertFromJson(data)
factory.convertFromJson(json)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
return campaign
}
@Throws(
JsonConversionException::class,
JsonCodingException::class,
FileWriteException::class,
)
fun save(campaign: Campaign) {
// convert the data to json format
// Transform the json into the model.
val json = try {
factory.convertToJson(campaign = campaign).let(json::encodeToString)
factory.convertToJson(campaign = campaign)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
// write the file
// Encode the json into a string.
val data = try {
this.json.encodeToString(json)
} catch (exception: Exception) {
throw JsonCodingException(root = exception)
}
// Write the file
try {
val file = file()
val file = campaignFile()
file.writeText(
text = json,
text = data,
charset = Charsets.UTF_8,
)
} catch (exception: Exception) {
throw FileWriteException(root = exception)
}
// Update the dataflow.
campaignFlow.value = campaign
campaignFlow.update { campaign }
}
private fun campaignFile(): File {
return File("${pathProvider.campaignPath()}campaign.json")
}
sealed class CampaignStoreException(root: Exception) : Exception(root)
class JsonConversionException(root: Exception) : CampaignStoreException(root)
class JsonCodingException(root: Exception) : CampaignStoreException(root)
class BusinessException(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

@ -11,11 +11,13 @@ import io.ktor.server.response.respond
fun Engine.deleteAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val alterationId = call.parameters.alterationId
// delete the alteration.
alterationService.delete(
alterationId = alterationId
)
// API & WebSocket responses.
call.respond(
message = ResultJson.Success(),
)
@ -32,13 +34,6 @@ fun Engine.deleteAlteration(): suspend io.ktor.server.routing.RoutingContext.()
message = exception.message ?: "?",
)
)
} catch (exception: AlterationStore.BusinessException) {
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.FILE_DOES_NOT_EXIST,
message = "Alteration doesn't exist."
)
)
} catch (exception: Exception) {
call.respond(
message = ResultJson.Error(

View file

@ -36,13 +36,6 @@ fun Engine.putAlteration(): suspend io.ktor.server.routing.RoutingContext.() ->
message = exception.message ?: "?",
)
)
} catch (exception: AlterationStore.BusinessException) {
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.FILE_ALREADY_EXIST,
message = "Alteration file already exist."
)
)
} catch (exception: Exception) {
call.respond(
message = ResultJson.Error(

View file

@ -1,10 +1,11 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText
import io.ktor.server.response.respond
fun Engine.removeCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
@ -12,17 +13,12 @@ fun Engine.removeCampaignCharacter(): suspend io.ktor.server.routing.RoutingCont
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
// remove the character form the party
val updated = campaignService.removeCharacter(
campaignService.removeCharacter(
characterSheetId = characterSheetId,
)
// error case
if (updated.not()) {
error("Unexpected error when removing character (characterSheetId:$characterSheetId) from party.")
}
// API & WebSocket responses
call.respondText(
text = "${HttpStatusCode.OK}",
status = HttpStatusCode.OK,
call.respond(
message = ResultJson.Success(),
)
webSocket.emit(
value = CampaignEvent.CharacterRemoved(
@ -30,10 +26,19 @@ fun Engine.removeCampaignCharacter(): suspend io.ktor.server.routing.RoutingCont
characterSheetId = characterSheetId,
)
)
} catch (exception: MissingParameterException) {
call.respond(
message = ResultJson.Error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respondText(
text = exception.localizedMessage,
status = HttpStatusCode.UnprocessableEntity,
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.GENERIC,
message = exception.message ?: "?",
)
)
}
}

View file

@ -1,9 +1,12 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
fun Engine.removeCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
@ -12,15 +15,12 @@ fun Engine.removeCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.()
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
// remove the character form the party
val updated = campaignService.removeNpc(characterSheetId = characterSheetId)
// error case
if (updated.not()) {
error("Unexpected error when removing character (characterSheetId:$characterSheetId) from npcs.")
}
campaignService.removeNpc(
characterSheetId = characterSheetId,
)
// API & WebSocket responses
call.respondText(
text = "${HttpStatusCode.OK}",
status = HttpStatusCode.OK,
call.respond(
message = ResultJson.Success(),
)
webSocket.emit(
value = CampaignEvent.NpcRemoved(
@ -28,10 +28,19 @@ fun Engine.removeCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.()
characterSheetId = characterSheetId,
)
)
} catch (exception: MissingParameterException) {
call.respond(
message = ResultJson.Error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respondText(
text = exception.localizedMessage,
status = HttpStatusCode.UnprocessableEntity,
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.GENERIC,
message = exception.message ?: "?",
)
)
}
}

View file

@ -1,12 +1,22 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import io.ktor.server.response.respond
fun Engine.getCampaign(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
try {
call.respond(
message = campaignService.campaignJson(),
)
} catch (exception: Exception) {
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.GENERIC,
message = exception.message ?: "?",
)
)
}
}
}

View file

@ -1,10 +1,11 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText
import io.ktor.server.response.respond
fun Engine.putCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
@ -12,15 +13,12 @@ fun Engine.putCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
// add the character to the party.
val update = campaignService.addCharacter(characterSheetId = characterSheetId)
// error case
if (update.not()) {
error("Unexpected error occurred when the character instance was added to the party")
}
campaignService.addCharacter(
characterSheetId = characterSheetId,
)
// API & WebSocket responses.
call.respondText(
text = "Character $characterSheetId successfully added to the party",
status = HttpStatusCode.OK,
call.respond(
message = ResultJson.Success(),
)
webSocket.emit(
value = CampaignEvent.CharacterAdded(
@ -28,10 +26,19 @@ fun Engine.putCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext
characterSheetId = characterSheetId,
)
)
} catch (exception: MissingParameterException) {
call.respond(
message = ResultJson.Error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respondText(
text = "${exception.message}",
status = HttpStatusCode.UnprocessableEntity,
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.GENERIC,
message = exception.message ?: "?",
)
)
}
}

View file

@ -1,10 +1,11 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText
import io.ktor.server.response.respond
fun Engine.putCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
@ -12,15 +13,12 @@ fun Engine.putCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() ->
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
// add the character to the npcs.
val update = campaignService.addNpc(characterSheetId = characterSheetId)
// error case
if (update.not()) {
error("Unexpected error occurred when the character instance was added to the npcs")
}
campaignService.addNpc(
characterSheetId = characterSheetId,
)
// API & WebSocket responses.
call.respondText(
text = "Character $characterSheetId successfully added to the npcs",
status = HttpStatusCode.OK,
call.respond(
message = ResultJson.Success(),
)
webSocket.emit(
value = CampaignEvent.NpcAdded(
@ -28,10 +26,19 @@ fun Engine.putCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() ->
characterSheetId = characterSheetId,
)
)
} catch (exception: MissingParameterException) {
call.respond(
message = ResultJson.Error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respondText(
text = "${exception.message}",
status = HttpStatusCode.UnprocessableEntity,
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.GENERIC,
message = exception.message ?: "?",
)
)
}
}

View file

@ -1,10 +1,13 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.shared.lwa.model.campaign.CampaignJsonV2
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
import io.ktor.http.HttpStatusCode
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
fun Engine.putCampaignScene(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
@ -15,15 +18,10 @@ fun Engine.putCampaignScene(): suspend io.ktor.server.routing.RoutingContext.()
// convert the scene into the a usable data model.
val scene = campaignJsonFactory.convertFromJson(json = form)
// update the campaign.
val updated = campaignService.setScene(scene = scene)
// error case
if (updated.not()) {
error("Unexpected error when updating the scene.")
}
campaignService.setScene(scene = scene)
// API & WebSocket responses
call.respondText(
text = "${HttpStatusCode.OK}",
status = HttpStatusCode.OK,
call.respond(
message = ResultJson.Success(),
)
webSocket.emit(
value = CampaignEvent.UpdateScene(
@ -31,10 +29,19 @@ fun Engine.putCampaignScene(): suspend io.ktor.server.routing.RoutingContext.()
name = scene.name,
)
)
} catch (exception: MissingParameterException) {
call.respond(
message = ResultJson.Error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respondText(
text = exception.localizedMessage,
status = HttpStatusCode.UnprocessableEntity,
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.GENERIC,
message = exception.message ?: "?",
)
)
}
}

View file

@ -13,11 +13,6 @@ fun Engine.deleteCharacter(): suspend io.ktor.server.routing.RoutingContext.() -
val deleted = characterService.deleteCharacterSheet(
characterSheetId = characterSheetId
)
// Remove the character fom the campaign if needed.
// TODO probably useless because all data will not be cleaned up (all campaign / screnes)
campaignService.removeInstance(
characterSheetId = characterSheetId,
)
if (deleted) {
call.respondText(

View file

@ -13,12 +13,9 @@ sealed interface ResultJson {
val message: String,
) : ResultJson {
companion object {
const val GENERIC = 500
const val GENERIC = 600
const val FILE_ALREADY_EXIST = GENERIC + 1
const val FILE_DOES_NOT_EXIST = GENERIC + 2
const val MISSING_PARAMETER = 1000
const val MISSING_PARAMETER = 700
const val MISSING_CHARACTER_SHEET_ID = MISSING_PARAMETER + 1
const val MISSING_ALTERATION_ID = MISSING_PARAMETER + 2
const val MISSING_CREATE = MISSING_PARAMETER + 3