Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.github.feelbeatapp.androidclient.game.datastreaming

import com.github.feelbeatapp.androidclient.game.model.GameState
import com.github.feelbeatapp.androidclient.game.model.Player
import com.github.feelbeatapp.androidclient.game.model.RoomStage

class Game(private var gameState: GameState) {
fun gameState(): GameState {
Expand All @@ -19,4 +20,16 @@ class Game(private var gameState: GameState) {
fun setAdmin(playerId: String) {
gameState = gameState.copy(adminId = playerId)
}

fun setMyReadyStatus(ready: Boolean) {
gameState = gameState.copy(readyMap = gameState.readyMap.plus(Pair(gameState.me, ready)))
}

fun updateReadyStatus(playerId: String, ready: Boolean) {
gameState = gameState.copy(readyMap = gameState.readyMap.plus(Pair(playerId, ready)))
}

fun setStage(stage: RoomStage) {
gameState = gameState.copy(stage = stage)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.feelbeatapp.androidclient.game.datastreaming

import com.github.feelbeatapp.androidclient.game.model.GameState
import com.github.feelbeatapp.androidclient.game.model.RoomSettings
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.flow.StateFlow

Expand All @@ -10,4 +11,8 @@ interface GameDataStreamer {
fun leaveRoom()

fun gameStateFlow(): StateFlow<GameState?>

suspend fun updateSettings(settings: RoomSettings)

suspend fun sendReadyStatus(ready: Boolean)
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
package com.github.feelbeatapp.androidclient.game.datastreaming

import android.util.Log
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.client.ReadyStatusMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.client.SettingsUpdateMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.client.SettingsUpdatePayload
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.InitialGameState
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.InitialMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.NewPlayerMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.PlayerLeftMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.PlayerReadyMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.RoomStageMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.ServerErrorMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.ServerMessageType
import com.github.feelbeatapp.androidclient.game.model.GameState
import com.github.feelbeatapp.androidclient.game.model.RoomSettings
import com.github.feelbeatapp.androidclient.game.model.RoomStage
import com.github.feelbeatapp.androidclient.infra.auth.AuthManager
import com.github.feelbeatapp.androidclient.infra.error.ErrorCode
import com.github.feelbeatapp.androidclient.infra.error.ErrorReceiver
import com.github.feelbeatapp.androidclient.infra.error.FeelBeatException
import com.github.feelbeatapp.androidclient.infra.error.FeelBeatServerException
import com.github.feelbeatapp.androidclient.infra.network.NetworkClient
import javax.inject.Inject
import kotlinx.coroutines.CancellationException
Expand All @@ -23,14 +33,18 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive

class RemoteGameDataStreamer
@Inject
constructor(private val networkClient: NetworkClient, private val errorReceiver: ErrorReceiver) :
GameDataStreamer {
constructor(
private val networkClient: NetworkClient,
private val errorReceiver: ErrorReceiver,
private val authManager: AuthManager,
) : GameDataStreamer {
private var game: Game? = null
private var gameStateFlow = MutableStateFlow<GameState?>(null)
private var scope: CoroutineScope? = null
Expand Down Expand Up @@ -67,37 +81,70 @@ constructor(private val networkClient: NetworkClient, private val errorReceiver:
override fun leaveRoom() {
scope?.cancel()
scope = null
gameStateFlow.value = null
synchronized(this) {
game = null
gameStateFlow.value = null
}
}

override fun gameStateFlow(): StateFlow<GameState?> {
return gameStateFlow.asStateFlow()
}

private fun processMessage(content: String) {
override suspend fun updateSettings(settings: RoomSettings) {
withContext(Dispatchers.IO) {
val token = authManager.getAccessToken()
val message =
SettingsUpdateMessage(
payload = SettingsUpdatePayload(token = token, settings = settings)
)
networkClient.sendMessage(Json.encodeToString(message))
}
}

override suspend fun sendReadyStatus(ready: Boolean) {
synchronized(this) {
game.let {
if (it == null) {
return
}

it.setMyReadyStatus(ready)
gameStateFlow.value = it.gameState()
}
}

withContext(Dispatchers.IO) {
networkClient.sendMessage(Json.encodeToString(ReadyStatusMessage(payload = ready)))
}
}

private suspend fun processMessage(content: String) {
try {
val type = Json.decodeFromString<JsonObject>(content)["type"]?.jsonPrimitive?.content

when (type) {
ServerMessageType.SERVER_ERROR.name -> processServerError(content)
ServerMessageType.INITIAL.name ->
loadInitialState(Json.decodeFromString<InitialMessage>(content).payload)
ServerMessageType.NEW_PLAYER.name -> {
game?.addPlayer(Json.decodeFromString<NewPlayerMessage>(content).payload)
gameStateFlow.value = game?.gameState()
}
ServerMessageType.PLAYER_LEFT.name -> {
val payload = Json.decodeFromString<PlayerLeftMessage>(content).payload
game?.removePlayer(payload.left)
game?.setAdmin(payload.admin)
gameStateFlow.value = game?.gameState()
}
ServerMessageType.NEW_PLAYER.name -> processNewPlayer(content)
ServerMessageType.PLAYER_LEFT.name -> processPlayerLeft(content)
ServerMessageType.PLAYER_READY.name -> processPlayerReady(content)
ServerMessageType.ROOM_STAGE.name -> processRoomStage(content)
else -> Log.w("RemoteGameDataStreamer", "Received unexpected message: $content")
}
} catch (e: Exception) {
throw FeelBeatException(ErrorCode.FEELBEAT_SERVER_INCORRECT_RESPONSE_FORMAT, e)
}
}

private suspend fun processServerError(content: String) {
errorReceiver.submitError(
FeelBeatServerException(Json.decodeFromString<ServerErrorMessage>(content).payload)
)
}

@Synchronized
private fun loadInitialState(initialState: InitialGameState) {
game =
Game(
Expand All @@ -109,8 +156,44 @@ constructor(private val networkClient: NetworkClient, private val errorReceiver:
me = initialState.me,
players = initialState.players,
songs = initialState.playlist.songs.map { it.toSongModel() },
settings = initialState.settings,
readyMap = initialState.readyMap,
stage = RoomStage.LOBBY,
)
)
gameStateFlow.value = game?.gameState()
}

@Synchronized
private fun processNewPlayer(content: String) {
game?.addPlayer(Json.decodeFromString<NewPlayerMessage>(content).payload)
gameStateFlow.value = game?.gameState()
}

private fun processPlayerLeft(content: String) {
val payload = Json.decodeFromString<PlayerLeftMessage>(content).payload
synchronized(this) {
game?.removePlayer(payload.left)
game?.setAdmin(payload.admin)
gameStateFlow.value = game?.gameState()
}
}

private fun processPlayerReady(content: String) {
val payload = Json.decodeFromString<PlayerReadyMessage>(content).payload

synchronized(this) {
game?.updateReadyStatus(payload.player, payload.ready)
gameStateFlow.value = game?.gameState()
}
}

private fun processRoomStage(content: String) {
val stage = Json.decodeFromString<RoomStageMessage>(content).payload

synchronized(this) {
game?.setStage(RoomStage.valueOf(stage))
gameStateFlow.value = game?.gameState()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.github.feelbeatapp.androidclient.game.datastreaming.messages.client

enum class ClientMessageType {
SETTINGS_UPDATE,
READY_STATUS,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.github.feelbeatapp.androidclient.game.datastreaming.messages.client

import kotlinx.serialization.Required
import kotlinx.serialization.Serializable

@Serializable
data class ReadyStatusMessage(
@Required val type: String = ClientMessageType.READY_STATUS.name,
val payload: Boolean,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.github.feelbeatapp.androidclient.game.datastreaming.messages.client

import com.github.feelbeatapp.androidclient.game.model.RoomSettings
import kotlinx.serialization.Required
import kotlinx.serialization.Serializable

@Serializable
data class SettingsUpdateMessage(
@Required val type: String = ClientMessageType.SETTINGS_UPDATE.name,
val payload: SettingsUpdatePayload,
)

@Serializable data class SettingsUpdatePayload(val settings: RoomSettings, val token: String)
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package com.github.feelbeatapp.androidclient.game.datastreaming.messages.server

import com.github.feelbeatapp.androidclient.game.model.Player
import com.github.feelbeatapp.androidclient.game.model.RoomSettings
import kotlin.time.Duration.Companion.seconds
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.time.Duration.Companion.seconds

@Serializable
@SerialName("INITIAL")
Expand All @@ -20,6 +20,7 @@ data class InitialGameState(
val admin: String,
val playlist: Playlist,
val players: List<Player>,
val readyMap: Map<String, Boolean>,
val settings: RoomSettings,
)

Expand All @@ -43,4 +44,3 @@ data class Song(
)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.github.feelbeatapp.androidclient.game.datastreaming.messages.server

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
@SerialName("PLAYER_READY")
data class PlayerReadyMessage(
override val type: String = ServerMessageType.PLAYER_READY.name,
val payload: PlayerReadyPayload,
) : ServerMessage()

@Serializable data class PlayerReadyPayload(val player: String, val ready: Boolean)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.github.feelbeatapp.androidclient.game.datastreaming.messages.server

import kotlinx.serialization.Required
import kotlinx.serialization.Serializable

@Serializable
data class RoomStageMessage(
@Required override val type: String = ServerMessageType.ROOM_STAGE.name,
val payload: String,
) : ServerMessage()
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.github.feelbeatapp.androidclient.game.datastreaming.messages.server

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable


@Serializable
@SerialName("SERVER_ERROR")
data class ServerErrorMessage(
override val type: String = ServerMessageType.SERVER_ERROR.name,
val payload: String
) : ServerMessage()


Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ enum class ServerMessageType {
INITIAL,
NEW_PLAYER,
PLAYER_LEFT,
SERVER_ERROR,
PLAYER_READY,
ROOM_STAGE,
}

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ data class GameState(
val me: String,
val players: List<Player>,
val songs: List<Song>,
val settings: RoomSettings,
val readyMap: Map<String, Boolean>,
val stage: RoomStage,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.github.feelbeatapp.androidclient.game.model

enum class RoomStage {
LOBBY,
GAME,
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ enum class ErrorCode {
FEELBEAT_SERVER_ERROR,
FEELBEAT_SERVER_INCORRECT_RESPONSE_FORMAT,
INCORRECT_PLAYLIST_LINK,
FEELBEAT_SERVER_FAILED_TO_JOIN_ROOM;
FEELBEAT_SERVER_FAILED_TO_JOIN_ROOM,
MAX_PLAYERS_TOO_SMALL;

fun toStringId(): Int {
return when (this) {
Expand All @@ -20,6 +21,7 @@ enum class ErrorCode {
INCORRECT_PLAYLIST_LINK -> R.string.incorrect_playlist_link
FEELBEAT_SERVER_ERROR -> R.string.feelbeat_server_error
FEELBEAT_SERVER_FAILED_TO_JOIN_ROOM -> R.string.failed_to_join_room
MAX_PLAYERS_TOO_SMALL -> R.string.max_players_too_small
else -> R.string.unexpected_error
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.withContext

/** Websocket implementation of communication with FeelBeat server */
class WebsocketClient
Expand All @@ -47,6 +49,7 @@ constructor(
path = "${serverUrl.fullPath}${path}",
request = { header("Authorization", "Bearer $token") },
) {
session = this
for (msg in offlineQueue) {
this.send(msg)
}
Expand All @@ -65,17 +68,20 @@ constructor(
.catch { e ->
throw FeelBeatException(ErrorCode.FEELBEAT_SERVER_FAILED_TO_JOIN_ROOM, e)
}
.onCompletion { session = null }
}

override suspend fun disconnect() {
session?.close()
}

override suspend fun sendMessage(text: String) {
if (session == null) {
offlineQueue.offer(text)
} else {
session?.send(text)
withContext(Dispatchers.IO) {
if (session == null) {
offlineQueue.offer(text)
} else {
session?.send(text)
}
}
}
}
Loading
Loading