Add characterSheet and Campaing to the server.

This commit is contained in:
Thomas Andres Gomez 2025-02-22 21:25:08 +01:00
parent 1e5f0d88ae
commit 495768e5fe
53 changed files with 879 additions and 513 deletions

View file

@ -22,4 +22,5 @@ dependencies {
implementation(libs.ktor.server.websockets)
implementation(libs.ktor.server.negotiation)
implementation(libs.ktor.serialization.json)
implementation(libs.filament.android)
}

View file

@ -1,5 +1,8 @@
import com.pixelized.server.lwa.model.character.CharacterSheetRepository
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.server.Engine
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
@ -9,17 +12,25 @@ val serverModuleDependencies
factoryDependencies,
useCaseDependencies,
storeDependencies,
repositoryDependencies,
serviceDependencies,
engineDependencies,
)
val engineDependencies
get() = module {
singleOf(::Engine)
}
val storeDependencies
get() = module {
singleOf(::CharacterSheetStore)
singleOf(::CampaignStore)
}
val repositoryDependencies
val serviceDependencies
get() = module {
singleOf(::CharacterSheetRepository)
singleOf(::CharacterSheetService)
singleOf(::CampaignService)
}
val factoryDependencies

View file

@ -0,0 +1,49 @@
package com.pixelized.server.lwa.model.campaign
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory
import com.pixelized.shared.lwa.protocol.payload.UpdatePlayerCharacteristicMessage
import com.pixelized.shared.lwa.usecase.CampaignUseCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
class CampaignService(
private val store: CampaignStore,
private val factory: CampaignJsonFactory,
private val useCase: CampaignUseCase,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
private val campaign = store.campaignFlow().stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = Campaign.EMPTY,
)
fun campaign(): CampaignJson {
return campaign.value.let(factory::convertToJson)
}
suspend fun update(
message: UpdatePlayerCharacteristicMessage,
) {
// fetch all the current campaign character
val characters = campaign.value.characters.toMutableMap()
// update the corresponding character using the usecase
characters[message.characterId] = useCase.updateCharacteristic(
character = characters[message.characterId] ?: Campaign.CharacterInstance(
characteristic = emptyMap(),
usedSkill = emptyList(),
),
characteristic = message.characteristic,
value = message.value,
)
// save the campaign to the disk + update the flow.
store.save(
campaign = campaign.value.copy(characters = characters)
)
}
}

View file

@ -0,0 +1,72 @@
package com.pixelized.server.lwa.model.campaign
import com.pixelized.shared.lwa.campaignPath
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory
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
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File
class CampaignStore(
private val factory: CampaignJsonFactory,
private val json: Json,
) {
private val directory = File(campaignPath()).also { it.mkdirs() }
private val flow = MutableStateFlow(value = Campaign.EMPTY)
init {
val scope = CoroutineScope(Dispatchers.IO + Job())
scope.launch {
flow.value = load()
}
}
fun campaignFlow(): StateFlow<Campaign> = flow
suspend fun load(): Campaign {
return try {
val json = file().readText(charset = Charsets.UTF_8)
if (json.isBlank()) Campaign.EMPTY
val campaign = this.json.decodeFromString<CampaignJson>(json)
factory.convertFromJson(campaign)
} catch (exception: Exception) {
Campaign.EMPTY
}
}
suspend fun save(campaign: Campaign) {
// convert the data to json format
val json = try {
factory.convertToJson(data = campaign).let(json::encodeToString)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
// write the file
try {
val file = file()
file.writeText(
text = json,
charset = Charsets.UTF_8,
)
} catch (exception: Exception) {
throw FileWriteException(root = exception)
}
// Update the dataflow.
flow.value = campaign
}
sealed class CampaignStoreException(root: Exception) : Exception(root)
class JsonConversionException(root: Exception) : CampaignStoreException(root)
class FileWriteException(root: Exception) : CampaignStoreException(root)
private fun file(): File {
return File("${campaignPath()}campaign.json")
}
}

View file

@ -1,27 +0,0 @@
package com.pixelized.server.lwa.model.character
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
class CharacterSheetRepository(
store: CharacterSheetStore,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
private val sheets = store.characterSheetFlow()
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> {
return sheets
}
}

View file

@ -0,0 +1,41 @@
package com.pixelized.server.lwa.model.character
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
class CharacterSheetService(
private val store: CharacterSheetStore,
private val factory: CharacterSheetJsonFactory,
private val useCase: CharacterSheetUseCase,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
private val sheets = store.characterSheetFlow().stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
fun character(): List<CharacterSheetJson> {
return sheets.value.map(factory::convertToJson)
}
fun characterSkillChange(
characterId: String,
skillId: String,
) {
val character = sheets.value.firstOrNull { it.id == characterId }
if (character != null) {
val update = useCase.updateSkillUsage(
character = character,
skillId = skillId,
)
store.save(sheet = update)
}
}
}

View file

@ -1,9 +1,9 @@
package com.pixelized.server.lwa.model.character
import com.pixelized.shared.lwa.characterStorePath
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -17,9 +17,9 @@ import java.text.Collator
class CharacterSheetStore(
private val factory: CharacterSheetJsonFactory,
private val jsonFormatter: Json,
private val json: Json,
) {
private val characterDirectory = File(characterStorePath()).also { it.mkdirs() }
private val directory = File(characterStorePath()).also { it.mkdirs() }
private val flow = MutableStateFlow<List<CharacterSheet>>(value = emptyList())
init {
@ -39,7 +39,7 @@ class CharacterSheetStore(
fun save(sheet: CharacterSheet) {
// convert the character sheet into json format.
val json = try {
factory.convertToJson(sheet = sheet).let(jsonFormatter::encodeToString)
factory.convertToJson(sheet = sheet).let(json::encodeToString)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
@ -85,7 +85,7 @@ class CharacterSheetStore(
JsonConversionException::class,
)
suspend fun load(): List<CharacterSheet> {
return characterDirectory
return directory
.listFiles()
?.mapNotNull { file ->
val json = try {
@ -98,7 +98,7 @@ class CharacterSheetStore(
return@mapNotNull null
}
try {
val sheet = jsonFormatter.decodeFromString<CharacterSheetJson>(json)
val sheet = this.json.decodeFromString<CharacterSheetJson>(json)
factory.convertFromJson(sheet)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)

View file

@ -0,0 +1,38 @@
package com.pixelized.server.lwa.server
import com.pixelized.server.lwa.model.campaign.CampaignService
import com.pixelized.server.lwa.model.character.CharacterSheetService
import com.pixelized.shared.lwa.protocol.Message
import com.pixelized.shared.lwa.protocol.MessageType
import com.pixelized.shared.lwa.protocol.payload.UpdatePlayerCharacteristicMessage
import com.pixelized.shared.lwa.protocol.payload.UpdateSkillUsageMessage
import kotlinx.serialization.json.Json
class Engine(
private val characterService: CharacterSheetService,
private val campaignService: CampaignService,
private val json: Json,
) {
suspend fun handle(message: Message) {
println(message)
when (message.type) {
MessageType.Roll -> {
Unit // Nothing to do here.
}
MessageType.UpdateSkillUsage -> {
val data: UpdateSkillUsageMessage = json.decodeFromString(message.value)
characterService.characterSkillChange(
characterId = data.characterId,
skillId = data.skillId
)
}
MessageType.UpdatePlayerCharacteristic -> {
val data: UpdatePlayerCharacteristicMessage = json.decodeFromString(message.value)
campaignService.update(data)
}
}
}
}

View file

@ -3,9 +3,9 @@ package com.pixelized.server.lwa.server
import com.pixelized.server.lwa.extention.decodeFromFrame
import com.pixelized.server.lwa.extention.encodeToFrame
import com.pixelized.server.lwa.model.character.CharacterSheetRepository
import com.pixelized.server.lwa.model.campaign.CampaignService
import com.pixelized.server.lwa.model.character.CharacterSheetService
import com.pixelized.shared.lwa.SERVER_PORT
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.protocol.Message
import com.pixelized.shared.lwa.sharedModuleDependencies
import io.ktor.http.ContentType
@ -65,24 +65,34 @@ class LocalServer {
masking = false
}
val repository by inject<CharacterSheetRepository>()
val factory by inject<CharacterSheetJsonFactory>()
val engine by inject<Engine>()
val characterService by inject<CharacterSheetService>()
val campaignService by inject<CampaignService>()
routing {
get(
path = "/",
body = {
call.respondText(contentType = ContentType.Text.Html) {
"<a href=\"http://127.0.0.1:16030/characters\">characters</a>"
"""<html><body><ul>
<li><a href="http://127.0.0.1:16030/characters">characters</a></li>
<li><a href="http://127.0.0.1:16030/campaign">campaign</a></li>
</ul></body></html>"""
}
}
)
get(
path = "/characters",
body = {
val body = repository.characterSheetFlow().value.map(factory::convertToJson)
call.respond(body)
call.respond(characterService.character())
},
)
get(
path = "/campaign",
body = {
call.respond(campaignService.campaign())
}
)
webSocket(
path = "/ws",
handler = {
@ -97,7 +107,8 @@ class LocalServer {
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val message = Json.decodeFromFrame(frame = frame)
println(message)
// log the message
engine.handle(message)
// broadcast to clients the message
outgoingMessageBuffer.emit(message)
}