Update client to add throw consume equip to InventoryItem.

This commit is contained in:
Thomas Andres Gomez 2025-04-19 21:46:03 +02:00
parent 9fce3f1cb8
commit ae820f5979
22 changed files with 791 additions and 121 deletions

View file

@ -1,5 +1,7 @@
package com.pixelized.server.lwa.model.inventory
import com.pixelized.server.lwa.model.item.ItemStore
import com.pixelized.server.lwa.server.exception.BusinessException
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
@ -9,9 +11,11 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import java.util.UUID
class InventoryService(
private val inventoryStore: InventoryStore,
private val itemStore: ItemStore,
private val factory: InventoryJsonFactory,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
@ -51,13 +55,185 @@ class InventoryService(
}
@Throws
fun createItem(
fun createInventoryItem(
characterSheetId: String,
itemId: String,
) : String {
return inventoryStore.createItem(
characterSheetId = characterSheetId,
): String {
// get the inventory of the character, if none create one.
val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
?: Inventory.empty(characterSheetId = characterSheetId)
// create an inventoryId.
val inventoryId = inventory.items.createInventoryId()
// create an inventory Item.
val item = Inventory.Item(
inventoryId = inventoryId,
itemId = itemId,
count = 1f,
equipped = false,
)
// update the inventory with the updated item.
val updatedInventory = inventory.copy(
items = inventory.items.toMutableList().also {
it.add(item)
}
)
// save the inventory
inventoryStore.save(
inventory = updatedInventory,
create = false,
)
return inventoryId
}
@Throws
fun changeInventoryItemCount(
characterSheetId: String,
inventoryId: String,
count: Float,
) {
if (count < 0) {
throw BusinessException(
message = "InventoryItem (id:$inventoryId) quantity cannot go below 0.",
)
}
// get the inventory of the character, if none create one.
val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
?: Inventory.empty(characterSheetId = characterSheetId)
// Guard case.
val inventoryItemIndex = inventory.items.indexOfFirst { it.inventoryId == inventoryId }
if (inventoryItemIndex < 0) {
throw BusinessException(
message = "InventoryItem (id:$inventoryId) not found in Inventory(characterSheetId:$characterSheetId).",
)
}
// update the inventory with the updated item.
val updatedInventory = inventory.copy(
items = inventory.items.toMutableList().also { items ->
if (count == 0f) {
items.removeAt(inventoryItemIndex)
} else {
items[inventoryItemIndex] = items[inventoryItemIndex].copy(count = count)
}
}
)
// save the inventory
inventoryStore.save(
inventory = updatedInventory,
create = false,
)
}
@Throws
fun consumeInventoryItem(
characterSheetId: String,
inventoryId: String,
) {
// get the inventory of the character, if none create one.
val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
?: Inventory.empty(characterSheetId = characterSheetId)
// Guard case.
val inventoryItemIndex = inventory.items.indexOfFirst { it.inventoryId == inventoryId }
if (inventoryItemIndex < 0) {
throw BusinessException(
message = "InventoryItem (id:$inventoryId) not found in Inventory(characterSheetId:$characterSheetId).",
)
}
// other Guard case.
val itemId = inventory.items[inventoryItemIndex].itemId
val item = itemStore.item(itemId = itemId)
?: throw BusinessException(message = "Item (id:$itemId) not found.")
if (item.options.consumable.not()) {
throw BusinessException(message = "Item (id:$itemId) is not consumable.")
}
// update the inventory with the updated item.
val updatedInventory = inventory.copy(
items = inventory.items.toMutableList().also { items ->
val inventoryItem = inventory.items[inventoryItemIndex]
if (inventoryItem.count - 1f <= 0f) {
items.removeAt(inventoryItemIndex)
} else {
items[inventoryItemIndex] = inventoryItem.copy(count = inventoryItem.count - 1f)
}
}
)
// save the inventory
inventoryStore.save(
inventory = updatedInventory,
create = false,
)
}
@Throws
fun equipInventoryItem(
characterSheetId: String,
inventoryId: String,
equip: Boolean,
) {
// get the inventory of the character, if none create one.
val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
?: Inventory.empty(characterSheetId = characterSheetId)
// Guard case.
val inventoryItemIndex = inventory.items.indexOfFirst { it.inventoryId == inventoryId }
if (inventoryItemIndex < 0) {
throw BusinessException(
message = "InventoryItem (id:$inventoryId) not found in Inventory(characterSheetId:$characterSheetId).",
)
}
// other Guard case.
val itemId = inventory.items[inventoryItemIndex].itemId
val item = itemStore.item(itemId = itemId)
?: throw BusinessException(message = "Item (id:$itemId) not found.")
if (item.options.equipable.not()) {
throw BusinessException(message = "Item (id:$itemId) is not equipable.")
}
// update the inventory with the updated item.
val updatedInventory = inventory.copy(
items = inventory.items.toMutableList().also { items ->
items[inventoryItemIndex] = inventory.items[inventoryItemIndex].copy(
equipped = equip
)
}
)
// save the inventory
inventoryStore.save(
inventory = updatedInventory,
create = false,
)
}
@Throws
fun deleteInventoryItem(
characterSheetId: String,
inventoryId: String,
) {
// get the inventory of the character, if none create one.
val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
?: Inventory.empty(characterSheetId = characterSheetId)
// Guard case.
val inventoryItemIndex = inventory.items.indexOfFirst { it.inventoryId == inventoryId }
if (inventoryItemIndex < 0) {
throw BusinessException(
message = "InventoryItem (id:$inventoryId) not found in Inventory(characterSheetId:$characterSheetId)",
)
}
// update the inventory with the updated item.
val updatedInventory = inventory.copy(
items = inventory.items.toMutableList().also { items ->
items.removeAt(inventoryItemIndex)
}
)
// save the inventory
inventoryStore.save(
inventory = updatedInventory,
create = false,
)
}
private fun List<Inventory.Item>.createInventoryId(): String {
var inventoryId: String
do {
inventoryId = UUID.randomUUID().toString()
} while (any { inventoryId == it.inventoryId })
return inventoryId
}
}

View file

@ -153,49 +153,7 @@ class InventoryStore(
}
}
@Throws
fun createItem(
characterSheetId: String,
itemId: String,
): String {
val (updatedInventory, inventoryId) = inventoryFlow.value.toMutableMap().let { characters ->
// get the inventory of the character, if none create one.
val inventory = characters[characterSheetId]
?: Inventory.empty(characterSheetId = characterSheetId)
// create an inventoryId.
val inventoryId = inventory.items.createInventoryId()
// create an inventory Item.
val item = Inventory.Item(
inventoryId = inventoryId,
itemId = itemId,
count = 1f,
equipped = false,
)
// update the inventory with the updated item.
inventory.copy(
items = inventory.items.toMutableList().also {
it.add(item)
}
) to inventoryId
}
// save the inventory
save(
inventory = updatedInventory,
create = false,
)
// return the inventory ID.
return inventoryId
}
private fun inventoryFile(id: String): File {
return File("${pathProvider.inventoryPath()}${id}.json")
}
private fun List<Inventory.Item>.createInventoryId(): String {
var inventoryId: String
do {
inventoryId = UUID.randomUUID().toString()
} while (any { inventoryId == it.inventoryId })
return inventoryId
}
}

View file

@ -37,6 +37,10 @@ class ItemStore(
}
}
fun item(itemId: String?): Item? {
return itemFlow.value.firstOrNull { it.id == itemId }
}
fun itemsFlow(): StateFlow<List<Item>> = itemFlow
private fun updateItemsFlow() {

View file

@ -19,8 +19,12 @@ 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.createInventory
import com.pixelized.server.lwa.server.rest.inventory.changeInventoryItemCount
import com.pixelized.server.lwa.server.rest.inventory.consumeInventoryItem
import com.pixelized.server.lwa.server.rest.inventory.createInventoryItem
import com.pixelized.server.lwa.server.rest.inventory.equipInventoryItem
import com.pixelized.server.lwa.server.rest.inventory.deleteInventory
import com.pixelized.server.lwa.server.rest.inventory.deleteInventoryItem
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
@ -218,7 +222,7 @@ class LocalServer {
body = engine.deleteAlteration()
)
}
route(path = "item") {
route(path = "/item") {
get(
path = "/all",
body = engine.getItems(),
@ -236,7 +240,7 @@ class LocalServer {
body = engine.deleteItem(),
)
}
route(path = "tag") {
route(path = "/tag") {
get(
path = "/character",
body = engine.getCharacterTags(),
@ -250,7 +254,7 @@ class LocalServer {
body = engine.getItemTags(),
)
}
route(path = "inventory") {
route(path = "/inventory") {
get(
path = "/detail",
body = engine.getInventory(),
@ -259,14 +263,34 @@ class LocalServer {
path = "/update",
body = engine.putInventory()
)
put(
path = "/create",
body = engine.createInventory()
)
delete(
path = "/delete",
body = engine.deleteInventory()
)
route(
path = "/item"
) {
put(
path = "/create",
body = engine.createInventoryItem()
)
put(
path = "/count",
body = engine.changeInventoryItemCount()
)
put(
path = "/consume",
body = engine.consumeInventoryItem()
)
put(
path = "/equip",
body = engine.equipInventoryItem()
)
delete(
path = "/delete",
body = engine.deleteInventoryItem()
)
}
}
}
}

View file

@ -0,0 +1,40 @@
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.server.lwa.utils.extentions.inventoryId
import com.pixelized.server.lwa.utils.extentions.itemId
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.deleteInventoryItem(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val inventoryId = call.queryParameters.inventoryId
// add the item to the inventory.
inventoryService.deleteInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
)
// API & WebSocket responses.
call.respond(
message = APIResponse.success(),
)
webSocket.emit(
value = ApiSynchronisation.InventoryUpdate(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId,
),
)
} 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.count
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.server.lwa.utils.extentions.inventoryId
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.changeInventoryItemCount(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val inventoryId = call.queryParameters.inventoryId
val count = call.queryParameters.count
// add the item to the inventory.
inventoryService.changeInventoryItemCount(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
count = count,
)
// API & WebSocket responses.
call.respond(
message = APIResponse.success(),
)
webSocket.emit(
value = ApiSynchronisation.InventoryUpdate(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId,
),
)
} catch (exception: Exception) {
call.exception(
exception = exception,
)
}
}
}

View file

@ -0,0 +1,39 @@
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.server.lwa.utils.extentions.inventoryId
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.consumeInventoryItem(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val inventoryId = call.queryParameters.inventoryId
// add the item to the inventory.
inventoryService.consumeInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
)
// API & WebSocket responses.
call.respond(
message = APIResponse.success(),
)
webSocket.emit(
value = ApiSynchronisation.InventoryUpdate(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId,
),
)
} catch (exception: Exception) {
call.exception(
exception = exception,
)
}
}
}

View file

@ -0,0 +1,43 @@
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.equip
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.server.lwa.utils.extentions.inventoryId
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.equipInventoryItem(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val inventoryId = call.queryParameters.inventoryId
val equip = call.queryParameters.equip
// add the item to the inventory.
inventoryService.equipInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
equip = equip,
)
// API & WebSocket responses.
call.respond(
message = APIResponse.success(),
)
webSocket.emit(
value = ApiSynchronisation.InventoryUpdate(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId,
),
)
} catch (exception: Exception) {
call.exception(
exception = exception,
)
}
}
}

View file

@ -9,14 +9,14 @@ import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
fun Engine.createInventory(): suspend RoutingContext.() -> Unit {
fun Engine.createInventoryItem(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val itemId = call.queryParameters.itemId
// add the item to the inventory.
val inventoryId = inventoryService.createItem(
val inventoryId = inventoryService.createInventoryItem(
characterSheetId = characterSheetId,
itemId = itemId,
)

View file

@ -10,6 +10,7 @@ inline fun <reified T> Parameters.param(
): T {
return when (T::class) {
String::class -> this[name] as? T
Float::class -> this[name]?.toFloatOrNull() as? T
Boolean::class -> this[name]?.toBooleanStrictOrNull() as? T
else -> null
} ?: throw MissingParameterException(
@ -30,12 +31,24 @@ val Parameters.alterationId: String
code = APIResponse.ErrorCode.AlterationId,
)
val Parameters.inventoryId: String
get() = param(
name = "inventoryId",
code = APIResponse.ErrorCode.InventoryId,
)
val Parameters.itemId: String
get() = param(
name = "itemId",
code = APIResponse.ErrorCode.ItemId,
)
val Parameters.count: Float
get() = param(
name = "count",
code = APIResponse.ErrorCode.Count,
)
val Parameters.create: Boolean
get() = param(
name = "create",
@ -46,4 +59,10 @@ val Parameters.active: Boolean
get() = param(
name = "active",
code = APIResponse.ErrorCode.Active,
)
val Parameters.equip: Boolean
get() = param(
name = "equip",
code = APIResponse.ErrorCode.Equip,
)