Add map management to the server (REST+WS)

This commit is contained in:
Thomas Andres Gomez 2025-12-07 10:18:57 +01:00
parent 3485b8a9fd
commit 03dbd7aad6
62 changed files with 1226 additions and 144 deletions

View file

@ -1,17 +1,19 @@
import com.pixelized.server.lwa.logics.ItemUsageLogic
import com.pixelized.server.lwa.model.alteration.AlterationService
import com.pixelized.server.lwa.model.alteration.AlterationStore
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
import com.pixelized.server.lwa.model.tag.TagStore
import com.pixelized.server.lwa.services.alteration.AlterationService
import com.pixelized.server.lwa.services.alteration.AlterationStore
import com.pixelized.server.lwa.services.campaign.CampaignService
import com.pixelized.server.lwa.services.campaign.CampaignStore
import com.pixelized.server.lwa.services.character.CharacterSheetService
import com.pixelized.server.lwa.services.character.CharacterSheetStore
import com.pixelized.server.lwa.services.inventory.InventoryService
import com.pixelized.server.lwa.services.inventory.InventoryStore
import com.pixelized.server.lwa.services.item.ItemService
import com.pixelized.server.lwa.services.item.ItemStore
import com.pixelized.server.lwa.services.tag.TagService
import com.pixelized.server.lwa.services.tag.TagStore
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.services.map.MapService
import com.pixelized.server.lwa.services.map.MapStore
import com.pixelized.shared.lwa.utils.PathProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@ -51,6 +53,7 @@ val storeDependencies
singleOf(::InventoryStore)
singleOf(::ItemStore)
singleOf(::TagStore)
singleOf(::MapStore)
}
val serviceDependencies
@ -61,6 +64,7 @@ val serviceDependencies
singleOf(::InventoryService)
singleOf(::ItemService)
singleOf(::TagService)
singleOf(::MapService)
}
val logicsDependencies

View file

@ -1,7 +1,7 @@
package com.pixelized.server.lwa.logics
import com.pixelized.server.lwa.model.character.CharacterSheetService
import com.pixelized.server.lwa.model.inventory.InventoryService
import com.pixelized.server.lwa.services.character.CharacterSheetService
import com.pixelized.server.lwa.services.inventory.InventoryService
class ItemUsageLogic(
private val characterSheetService: CharacterSheetService,

View file

@ -1,18 +1,20 @@
package com.pixelized.server.lwa.server
import com.pixelized.server.lwa.logics.ItemUsageLogic
import com.pixelized.server.lwa.model.alteration.AlterationService
import com.pixelized.server.lwa.model.alteration.AlterationStore
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
import com.pixelized.server.lwa.model.tag.TagStore
import com.pixelized.server.lwa.services.alteration.AlterationService
import com.pixelized.server.lwa.services.alteration.AlterationStore
import com.pixelized.server.lwa.services.campaign.CampaignService
import com.pixelized.server.lwa.services.campaign.CampaignStore
import com.pixelized.server.lwa.services.character.CharacterSheetService
import com.pixelized.server.lwa.services.character.CharacterSheetStore
import com.pixelized.server.lwa.services.inventory.InventoryService
import com.pixelized.server.lwa.services.inventory.InventoryStore
import com.pixelized.server.lwa.services.item.ItemService
import com.pixelized.server.lwa.services.item.ItemStore
import com.pixelized.server.lwa.services.map.MapService
import com.pixelized.server.lwa.services.map.MapStore
import com.pixelized.server.lwa.services.tag.TagService
import com.pixelized.server.lwa.services.tag.TagStore
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
@ -30,6 +32,7 @@ class Engine(
val itemService: ItemService,
val inventoryService: InventoryService,
val tagService: TagService,
val mapService: MapService,
val campaignJsonFactory: CampaignJsonFactory,
val itemUsageLogic: ItemUsageLogic,
private val campaignStore: CampaignStore,
@ -38,6 +41,7 @@ class Engine(
private val itemStore: ItemStore,
private val inventoryStore: InventoryStore,
private val tagStore: TagStore,
private val mapStore: MapStore,
) {
val webSocket = MutableSharedFlow<SocketMessage>()
@ -106,7 +110,19 @@ class Engine(
characterSheetId = message.characterSheetId,
)
is CampaignEvent.UpdateScene -> Unit
is CampaignEvent.MapUpdated -> {
// convert the map into the a usable data model.
val map = campaignJsonFactory.convertFromJson(json = message.map)
// update the map
campaignService.setMap(map = map)
}
is CampaignEvent.MapDeleted -> {
// update the map
campaignService.setMap(map = null)
}
is CampaignEvent.SceneUpdated -> Unit
}
is GameAdminEvent -> when (message) {
@ -117,6 +133,7 @@ class Engine(
itemStore.updateItemsFlow()
inventoryStore.updateInventoryFlow()
tagStore.updateTagFlow()
mapStore.updateMapFlow()
webSocket.emit(
value = GameAdminEvent.ServerSynchronization(

View file

@ -5,7 +5,9 @@ import com.pixelized.server.lwa.server.rest.alteration.deleteAlteration
import com.pixelized.server.lwa.server.rest.alteration.getAlteration
import com.pixelized.server.lwa.server.rest.alteration.getAlterations
import com.pixelized.server.lwa.server.rest.alteration.putAlteration
import com.pixelized.server.lwa.server.rest.campaign.deleteCampaignMap
import com.pixelized.server.lwa.server.rest.campaign.getCampaign
import com.pixelized.server.lwa.server.rest.campaign.putCampaignMap
import com.pixelized.server.lwa.server.rest.campaign.putCampaignCharacter
import com.pixelized.server.lwa.server.rest.campaign.putCampaignNpc
import com.pixelized.server.lwa.server.rest.campaign.putCampaignScene
@ -23,9 +25,9 @@ import com.pixelized.server.lwa.server.rest.character.putCharacterFatigue
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.equipInventoryItem
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.inventory.putPurse
@ -33,6 +35,10 @@ 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
import com.pixelized.server.lwa.server.rest.item.putItem
import com.pixelized.server.lwa.server.rest.map.deleteMap
import com.pixelized.server.lwa.server.rest.map.getMap
import com.pixelized.server.lwa.server.rest.map.getMaps
import com.pixelized.server.lwa.server.rest.map.putMap
import com.pixelized.server.lwa.server.rest.tag.getAlterationTags
import com.pixelized.server.lwa.server.rest.tag.getCharacterTags
import com.pixelized.server.lwa.server.rest.tag.getItemTags
@ -163,6 +169,18 @@ class LocalServer {
path = "/scene",
body = engine.putCampaignScene(),
)
route(
path = "/map",
) {
put(
path = "/update",
body = engine.putCampaignMap(),
)
put(
path = "/delete",
body = engine.deleteCampaignMap(),
)
}
}
route(
path = "/character",
@ -306,6 +324,24 @@ class LocalServer {
)
}
}
route(path = "/map") {
get(
path = "/all",
body = engine.getMaps(),
)
get(
path = "/detail",
body = engine.getMap(),
)
put(
path = "/update",
body = engine.putMap(),
)
delete(
path = "/delete",
body = engine.deleteMap(),
)
}
}
}
)

View file

@ -0,0 +1,25 @@
{
"id": "DAHOME_432_PU",
"name": "Dahomé",
"camera": {
"zoom": 1.52,
"offsetX": 1594,
"offsetY": 1149
},
"size": {
"width": 3840,
"height": 2160
},
"resources": [
{
"id": "ROOT",
"name": "Root",
"uri": "https://i.ibb.co/7dvDJ50L/image-dahome-maps.webp"
},
{
"id": "REGION",
"name": "Frontières",
"uri": "https://i.ibb.co/mC4jw8Yj/image-dahome-regions.webp"
}
]
}

View file

@ -0,0 +1,32 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
fun Engine.deleteCampaignMap(): suspend RoutingContext.() -> Unit {
return {
try {
// update the campaign.
campaignService.setMap(
map = null,
)
// API & WebSocket responses
call.respond(
message = APIResponse.success(),
)
webSocket.emit(
value = CampaignEvent.MapDeleted(
timestamp = System.currentTimeMillis(),
)
)
} catch (exception: Exception) {
call.exception(
exception = exception
)
}
}
}

View file

@ -0,0 +1,39 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.model.campaign.CampaignJsonV2
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
fun Engine.putCampaignMap(): suspend RoutingContext.() -> Unit {
return {
try {
// Get the map json from the body of the request
val form = call.receive<CampaignJsonV2.MiniMapJson>()
// convert the map into the a usable data model.
val map = campaignJsonFactory.convertFromJson(json = form)
// update the campaign.
campaignService.setMap(
map = map,
)
// API & WebSocket responses
call.respond(
message = APIResponse.success(),
)
webSocket.emit(
value = CampaignEvent.MapUpdated(
timestamp = System.currentTimeMillis(),
map = form,
)
)
} catch (exception: Exception) {
call.exception(
exception = exception
)
}
}
}

View file

@ -25,7 +25,7 @@ fun Engine.putCampaignScene(): suspend RoutingContext.() -> Unit {
message = APIResponse.success(),
)
webSocket.emit(
value = CampaignEvent.UpdateScene(
value = CampaignEvent.SceneUpdated(
timestamp = System.currentTimeMillis(),
name = scene.name,
)

View file

@ -0,0 +1,36 @@
package com.pixelized.server.lwa.server.rest.map
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.server.lwa.utils.extentions.mapId
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.deleteMap(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val mapId = call.queryParameters.mapId
// delete the map.
mapService.delete(
mapId = mapId,
)
// API & WebSocket responses.
call.respond(
message = APIResponse.success(),
)
webSocket.emit(
value = ApiSynchronisation.MapDelete(
timestamp = System.currentTimeMillis(),
id = mapId,
),
)
} catch (exception: Exception) {
call.exception(
exception = exception,
)
}
}
}

View file

@ -0,0 +1,29 @@
package com.pixelized.server.lwa.server.rest.map
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.server.lwa.utils.extentions.mapId
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
fun Engine.getMap(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val mapId = call.queryParameters.mapId
// fetch the map
val map = mapService.map(id = mapId)
// respond to the request
call.respond(
message = APIResponse.success(
data = map,
),
)
} catch (exception: Exception) {
call.exception(
exception = exception
)
}
}
}

View file

@ -0,0 +1,24 @@
package com.pixelized.server.lwa.server.rest.map
import com.pixelized.server.lwa.server.Engine
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.getMaps(): suspend RoutingContext.() -> Unit {
return {
try {
call.respond(
message = APIResponse.success(
data = mapService.maps(),
),
)
} catch (exception: Exception) {
call.exception(
exception = exception
)
}
}
}

View file

@ -0,0 +1,37 @@
package com.pixelized.server.lwa.server.rest.map
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.create
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.model.alteration.AlterationJson
import com.pixelized.shared.lwa.model.map.MapJson
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.putMap(): suspend RoutingContext.() -> Unit {
return {
try {
val form = call.receive<MapJson>()
mapService.save(
json = form,
)
call.respond(
message = APIResponse.success(),
)
webSocket.emit(
value = ApiSynchronisation.MapUpdate(
timestamp = System.currentTimeMillis(),
id = form.id,
),
)
} catch (exception: Exception) {
call.exception(
exception = exception,
)
}
}
}

View file

@ -1,4 +1,4 @@
package com.pixelized.server.lwa.model.alteration
package com.pixelized.server.lwa.services.alteration
import com.pixelized.shared.lwa.model.alteration.AlterationJson
import com.pixelized.shared.lwa.model.alteration.AlterationJsonFactory

View file

@ -1,4 +1,4 @@
package com.pixelized.server.lwa.model.alteration
package com.pixelized.server.lwa.services.alteration
import com.pixelized.server.lwa.server.exception.BusinessException
import com.pixelized.server.lwa.server.exception.FileReadException

View file

@ -1,4 +1,4 @@
package com.pixelized.server.lwa.model.campaign
package com.pixelized.server.lwa.services.campaign
import com.pixelized.server.lwa.server.exception.BusinessException
import com.pixelized.shared.lwa.model.campaign.Campaign
@ -120,6 +120,16 @@ class CampaignService(
)
}
@Throws
suspend fun setMap(
map: Campaign.MiniMap?,
) {
// save the campaign to the disk + update the flow.
store.save(
campaign = campaign.copy(map = map)
)
}
// Data manipulation through WebSocket.
suspend fun updateToggleParty() {

View file

@ -1,4 +1,4 @@
package com.pixelized.server.lwa.model.campaign
package com.pixelized.server.lwa.services.campaign
import com.pixelized.server.lwa.server.exception.FileReadException
import com.pixelized.server.lwa.server.exception.FileWriteException
@ -10,7 +10,6 @@ import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
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

View file

@ -1,4 +1,4 @@
package com.pixelized.server.lwa.model.character
package com.pixelized.server.lwa.services.character
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson

View file

@ -1,4 +1,4 @@
package com.pixelized.server.lwa.model.character
package com.pixelized.server.lwa.services.character
import com.pixelized.server.lwa.server.exception.BusinessException
import com.pixelized.server.lwa.server.exception.FileReadException

View file

@ -1,6 +1,6 @@
package com.pixelized.server.lwa.model.inventory
package com.pixelized.server.lwa.services.inventory
import com.pixelized.server.lwa.model.item.ItemStore
import com.pixelized.server.lwa.services.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

View file

@ -1,4 +1,4 @@
package com.pixelized.server.lwa.model.inventory
package com.pixelized.server.lwa.services.inventory
import com.pixelized.server.lwa.server.exception.BusinessException
import com.pixelized.server.lwa.server.exception.FileReadException

View file

@ -1,4 +1,4 @@
package com.pixelized.server.lwa.model.item
package com.pixelized.server.lwa.services.item
import com.pixelized.shared.lwa.model.item.ItemJson
import com.pixelized.shared.lwa.model.item.factory.ItemJsonFactory

View file

@ -1,4 +1,4 @@
package com.pixelized.server.lwa.model.item
package com.pixelized.server.lwa.services.item
import com.pixelized.server.lwa.server.exception.BusinessException
import com.pixelized.server.lwa.server.exception.FileReadException

View file

@ -0,0 +1,32 @@
package com.pixelized.server.lwa.services.map
import com.pixelized.server.lwa.server.exception.BusinessException
import com.pixelized.shared.lwa.model.map.MapJson
class MapService(
private val mapStore: MapStore,
) {
fun maps(): List<MapJson> {
return mapStore.maps().value.values.toList()
}
@Throws
fun map(id: String): MapJson {
return mapStore.maps().value[id]
?: throw BusinessException("Map with id:$id is not found")
}
@Throws
suspend fun save(
json: MapJson,
) {
mapStore.save(
map = json,
)
}
@Throws
fun delete(mapId: String) {
mapStore.delete(id = mapId)
}
}

View file

@ -0,0 +1,126 @@
package com.pixelized.server.lwa.services.map
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.map.MapJson
import com.pixelized.shared.lwa.utils.PathProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.io.File
import java.text.Collator
class MapStore(
private val pathProvider: PathProvider,
private val jsonSerializer: Json,
scope: CoroutineScope,
) {
private val directory = File(pathProvider.mapPath()).also { it.mkdirs() }
private val mapsFlow = MutableStateFlow<Map<String, MapJson>>(emptyMap())
init {
// make the file path.
File(pathProvider.mapPath()).mkdirs()
// load the initial data
scope.launch {
updateMapFlow()
}
}
fun maps(): StateFlow<Map<String, MapJson>> = mapsFlow
suspend fun updateMapFlow() {
mapsFlow.value = try {
load().associateBy { it.id }
} catch (exception: Exception) {
println(exception.message) // TODO proper exception handling
emptyMap()
}
}
private suspend fun load(
directory: File = this.directory,
): List<MapJson> {
return withContext(Dispatchers.IO) {
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
try {
jsonSerializer.decodeFromString<MapJson>(json)
} catch (exception: Exception) {
throw JsonCodingException(root = exception)
}
}
?: emptyList()
}
}
@Throws(JsonConversionException::class, FileWriteException::class)
suspend fun save(map: MapJson) {
// convert the data to json format
val json = try {
this.jsonSerializer.encodeToString(map)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
// write the file
try {
val file = mapFile(id = map.id)
file.writeText(
text = json,
charset = Charsets.UTF_8,
)
} catch (exception: Exception) {
throw FileWriteException(root = exception)
}
// Update the dataflow.
mapsFlow.update { maps ->
maps.toMutableMap().also {
it[map.id] = map
}
}
}
@Throws(BusinessException::class)
fun delete(id: String) {
val file = mapFile(id = id)
// Guard case on the file existence.
if (file.exists().not()) {
throw BusinessException(
message = "Alteration doesn't not exist, deletion is impossible.",
)
}
// Guard case on the file deletion
if (file.delete().not()) {
throw BusinessException(
message = "Alteration file have not been deleted for unknown reason.",
)
}
// Update the data model with the deleted alteration.
mapsFlow.update { maps ->
maps.toMutableMap().also {
it.remove(key = id)
}
}
}
private fun mapFile(id: String) = File("${pathProvider.mapPath()}$id.json")
}

View file

@ -1,4 +1,4 @@
package com.pixelized.server.lwa.model.tag
package com.pixelized.server.lwa.services.tag
import com.pixelized.shared.lwa.model.tag.TagJson

View file

@ -1,4 +1,4 @@
package com.pixelized.server.lwa.model.tag
package com.pixelized.server.lwa.services.tag
import com.pixelized.server.lwa.server.exception.FileReadException
import com.pixelized.server.lwa.server.exception.FileWriteException
@ -6,8 +6,6 @@ import com.pixelized.server.lwa.server.exception.JsonConversionException
import com.pixelized.shared.lwa.model.tag.TagJson
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.launch

View file

@ -43,6 +43,12 @@ val Parameters.itemId: String
code = APIResponse.ErrorCode.ItemId,
)
val Parameters.mapId: String
get() = param(
name = "mapId",
code = APIResponse.ErrorCode.ItemId,
)
val Parameters.count: Float
get() = param(
name = "count",