Add inventory managment to server.

This commit is contained in:
Thomas Andres Gomez 2025-04-12 22:36:25 +02:00
parent 04b203239d
commit 4f33492b23
18 changed files with 499 additions and 7 deletions

View file

@ -4,6 +4,8 @@ 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
@ -36,18 +38,20 @@ val engineDependencies
val storeDependencies
get() = module {
singleOf(::CharacterSheetStore)
singleOf(::CampaignStore)
singleOf(::AlterationStore)
singleOf(::TagStore)
singleOf(::CampaignStore)
singleOf(::CharacterSheetStore)
singleOf(::InventoryStore)
singleOf(::ItemStore)
singleOf(::TagStore)
}
val serviceDependencies
get() = module {
singleOf(::CharacterSheetService)
singleOf(::CampaignService)
singleOf(::AlterationService)
singleOf(::CampaignService)
singleOf(::CharacterSheetService)
singleOf(::InventoryService)
singleOf(::ItemService)
singleOf(::TagService)
}

View file

@ -0,0 +1,52 @@
package com.pixelized.server.lwa.model.inventory
import com.pixelized.shared.lwa.model.inventory.Inventory
import com.pixelized.shared.lwa.model.inventory.InventoryJson
import com.pixelized.shared.lwa.model.inventory.factory.InventoryJsonFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class InventoryService(
private val inventoryStore: InventoryStore,
private val factory: InventoryJsonFactory,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
private val inventoryJson = inventoryStore.inventoryFlow()
.map { it.mapValues { entry -> factory.convertToJson(inventory = entry.value) } }
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyMap()
)
fun inventory(characterSheetId: String): Inventory {
return inventoryStore.inventoryFlow().value[characterSheetId]
?: Inventory.empty(characterSheetId = characterSheetId)
}
fun inventoryJson(characterSheetId: String): InventoryJson {
return inventoryJson.value[characterSheetId]
?: factory.convertToJson(Inventory.empty(characterSheetId = characterSheetId))
}
@Throws
fun save(
inventoryJson: InventoryJson,
create: Boolean,
) {
inventoryStore.save(
inventory = factory.convertFromJson(json = inventoryJson),
create = create,
)
}
@Throws
fun delete(characterSheetId: String) {
inventoryStore.delete(characterSheetId = characterSheetId)
}
}

View file

@ -0,0 +1,158 @@
package com.pixelized.server.lwa.model.inventory
import com.pixelized.server.lwa.server.exception.BusinessException
import com.pixelized.server.lwa.server.exception.FileReadException
import com.pixelized.server.lwa.server.exception.FileWriteException
import com.pixelized.server.lwa.server.exception.JsonCodingException
import com.pixelized.server.lwa.server.exception.JsonConversionException
import com.pixelized.shared.lwa.model.inventory.Inventory
import com.pixelized.shared.lwa.model.inventory.InventoryJson
import com.pixelized.shared.lwa.model.inventory.factory.InventoryJsonFactory
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.utils.PathProvider
import kotlinx.coroutines.CoroutineScope
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
class InventoryStore(
private val pathProvider: PathProvider,
private val factory: InventoryJsonFactory,
private val json: Json,
) {
private val directory = File(pathProvider.inventoryPath()).also { it.mkdirs() }
private val inventoryFlow = MutableStateFlow<Map<String, Inventory>>(value = emptyMap())
init {
// build a coroutine scope for async calls
val scope = CoroutineScope(Dispatchers.IO + Job())
// load the initial data
scope.launch {
updateInventoryFlow()
}
}
fun inventoryFlow(): StateFlow<Map<String, Inventory>> = inventoryFlow
private suspend fun updateInventoryFlow() {
inventoryFlow.value = try {
load()
} catch (exception: Exception) {
println(exception.message) // TODO proper exception handling
emptyMap()
}
}
@Throws(
FileReadException::class,
JsonCodingException::class,
JsonConversionException::class,
)
private fun load(
directory: File = this.directory,
): Map<String, Inventory> {
return directory
.listFiles()
?.mapNotNull { file ->
val json = try {
file.readText(charset = Charsets.UTF_8)
} catch (exception: Exception) {
throw FileReadException(root = exception)
}
// Guard, if the json is blank no character have been save, ignore this file.
if (json.isBlank()) {
return@mapNotNull null
}
// decode the file
val data = try {
this.json.decodeFromString<InventoryJson>(json)
} catch (exception: Exception) {
throw JsonCodingException(root = exception)
}
// parse the json string.
val inventory = try {
factory.convertFromJson(data)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
file.name to inventory
}
?.toMap()
?: emptyMap()
}
@Throws(
BusinessException::class,
JsonConversionException::class,
JsonCodingException::class,
FileWriteException::class,
)
fun save(
inventory: Inventory,
create: Boolean,
) {
val file = inventoryFile(id = inventory.characterSheetId)
if (create && file.exists()) {
throw BusinessException(message = "Inventory already exist, creation is impossible.")
}
val json = try {
factory.convertToJson(inventory = inventory)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
val data = try {
this.json.encodeToString(json)
} catch (exception: Exception) {
throw JsonCodingException(root = exception)
}
try {
file.writeText(
text = data,
charset = Charsets.UTF_8,
)
} catch (exception: Exception) {
throw FileWriteException(root = exception)
}
inventoryFlow.update { flow ->
flow.toMutableMap().also { data ->
data[inventory.characterSheetId] = inventory
}
}
}
@Throws(BusinessException::class)
fun delete(characterSheetId: String) {
val file = inventoryFile(id = characterSheetId)
if (file.exists().not()) {
throw BusinessException(
message = "Inventory file with id:$characterSheetId doesn't not exist.",
code = APIResponse.ErrorCode.CharacterSheetId
)
}
if (file.delete().not()) {
throw BusinessException(
message = "Inventory file have not been deleted for unknown reason.",
)
}
inventoryFlow.update { characters ->
characters.toMutableMap().also { data -> data.remove(characterSheetId) }
}
}
private fun inventoryFile(id: String): File {
return File("${pathProvider.inventoryPath()}${id}.json")
}
}

View file

@ -27,7 +27,6 @@ class ItemStore(
private val json: Json,
) {
private val directory = File(pathProvider.itemsPath()).also { it.mkdirs() }
private val itemFlow = MutableStateFlow<List<Item>>(emptyList())
init {

View file

@ -3,6 +3,7 @@ package com.pixelized.server.lwa.server
import com.pixelized.server.lwa.model.alteration.AlterationService
import com.pixelized.server.lwa.model.campaign.CampaignService
import com.pixelized.server.lwa.model.character.CharacterSheetService
import com.pixelized.server.lwa.model.inventory.InventoryService
import com.pixelized.server.lwa.model.item.ItemService
import com.pixelized.server.lwa.model.tag.TagService
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
@ -19,6 +20,7 @@ class Engine(
val campaignService: CampaignService,
val alterationService: AlterationService,
val itemService: ItemService,
val inventoryService: InventoryService,
val tagService: TagService,
val campaignJsonFactory: CampaignJsonFactory,
) {

View file

@ -19,6 +19,9 @@ import com.pixelized.server.lwa.server.rest.character.putCharacterAlteration
import com.pixelized.server.lwa.server.rest.character.putCharacterDamage
import com.pixelized.server.lwa.server.rest.character.putCharacterDiminished
import com.pixelized.server.lwa.server.rest.character.putCharacterFatigue
import com.pixelized.server.lwa.server.rest.inventory.deleteInventory
import com.pixelized.server.lwa.server.rest.inventory.getInventory
import com.pixelized.server.lwa.server.rest.inventory.putInventory
import com.pixelized.server.lwa.server.rest.item.deleteItem
import com.pixelized.server.lwa.server.rest.item.getItem
import com.pixelized.server.lwa.server.rest.item.getItems
@ -246,6 +249,20 @@ class LocalServer {
body = engine.getItemTags(),
)
}
route(path = "inventory") {
get(
path = "/detail",
body = engine.getInventory(),
)
put(
path = "/update",
body = engine.putInventory()
)
delete(
path = "/delete",
body = engine.deleteInventory()
)
}
}
}
)

View file

@ -0,0 +1,36 @@
package com.pixelized.server.lwa.server.rest.inventory
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
fun Engine.deleteInventory(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
// delete the alteration.
inventoryService.delete(
characterSheetId = characterSheetId,
)
// API & WebSocket responses.
call.respond(
message = APIResponse.success(),
)
webSocket.emit(
value = ApiSynchronisation.InventoryDelete(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId,
),
)
} catch (exception: Exception) {
call.exception(
exception = exception,
)
}
}
}

View file

@ -0,0 +1,31 @@
package com.pixelized.server.lwa.server.rest.inventory
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
fun Engine.getInventory(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
// get the character inventory
val inventory = inventoryService.inventoryJson(
characterSheetId = characterSheetId,
)
// send it back to the user.
call.respond(
message = APIResponse.success(
data = inventory
)
)
} catch (exception: Exception) {
call.exception(
exception = exception
)
}
}
}

View file

@ -0,0 +1,42 @@
package com.pixelized.server.lwa.server.rest.inventory
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.create
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.model.inventory.InventoryJson
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
fun Engine.putInventory(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val form = call.receive<InventoryJson>()
val create = call.queryParameters.create
// get the character inventory
inventoryService.save(
inventoryJson = form,
create = create,
)
// send it back to the user.
call.respond(
message = APIResponse.success()
)
webSocket.emit(
value = ApiSynchronisation.InventoryUpdate(
timestamp = System.currentTimeMillis(),
characterSheetId = form.characterSheetId,
),
)
} catch (exception: Exception) {
call.exception(
exception = exception
)
}
}
}

View file

@ -12,7 +12,7 @@ fun Engine.deleteItem(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val itemId = call.parameters.itemId
val itemId = call.queryParameters.itemId
// delete the alteration.
itemService.delete(
itemId = itemId