Incomplete websocket implementation

This commit is contained in:
Christoph Hagen 2021-11-29 11:54:50 +01:00
parent 153f50294d
commit a24576f4f2
5 changed files with 98 additions and 70 deletions

View File

@ -50,8 +50,7 @@ async function performGetPublicTablesRequest(token) {
return fetch("/tables/public", { method: 'POST', body: token }) return fetch("/tables/public", { method: 'POST', body: token })
.then(convertServerResponse) .then(convertServerResponse)
.then(function(text) { .then(function(text) {
const decoded = atob(text) return JSON.parse(text);
return JSON.parse(decoded);
}) })
} }

View File

@ -1,23 +1,15 @@
import Foundation import Foundation
import Vapor import Vapor
let playerPerTable = 4
typealias TableId = String
typealias TableName = String
final class Database { final class Database {
private let players: PlayerManagement private let players: PlayerManagement
private let tables: TableManagement private let tables: TableManagement
private var sessions: [SessionToken : WebSocket]
init() { init() {
self.players = PlayerManagement() self.players = PlayerManagement()
self.tables = TableManagement() self.tables = TableManagement()
self.sessions = [:]
// TODO: Load server data from disk // TODO: Load server data from disk
// TODO: Save data to disk // TODO: Save data to disk
} }
@ -33,22 +25,19 @@ final class Database {
} }
func deletePlayer(named name: PlayerName) { func deletePlayer(named name: PlayerName) {
if let sessionToken = players.deletePlayer(named: name) { _ = players.deletePlayer(named: name)
closeAndRemoveSession(for: sessionToken) tables.remove(player: name)
}
// TODO: Delete player from tables
} }
func isValid(sessionToken token: SessionToken) -> Bool { func isValid(sessionToken token: SessionToken) -> Bool {
players.isValid(sessionToken: token) players.isValid(sessionToken: token)
} }
func startSession(socket: WebSocket, sessionToken: SessionToken) { func startSession(socket: WebSocket, sessionToken: SessionToken) -> Bool {
closeAndRemoveSession(for: sessionToken) guard let player = players.registeredPlayerExists(withSessionToken: sessionToken) else {
sessions[sessionToken] = socket return false
socket.onText { [weak self] socket, text in
self?.didReceive(message: text, forSessionToken: sessionToken)
} }
return tables.connect(player: player, using: socket)
} }
private func didReceive(message: String, forSessionToken token: SessionToken) { private func didReceive(message: String, forSessionToken token: SessionToken) {
@ -56,13 +45,15 @@ final class Database {
print("Session \(token.prefix(6)): \(message)") print("Session \(token.prefix(6)): \(message)")
} }
func endSession(forSessionToken token: SessionToken) { func endSession(forSessionToken sessionToken: SessionToken) {
players.endSession(forSessionToken: token) guard let player = players.endSession(forSessionToken: sessionToken) else {
closeAndRemoveSession(for: token) return
}
closeSession(for: player)
} }
private func closeAndRemoveSession(for token: SessionToken) { private func closeSession(for player: PlayerName) {
_ = sessions.removeValue(forKey: token)?.close() tables.disconnect(player: player)
} }
/** /**
@ -78,16 +69,12 @@ final class Database {
players.registeredPlayerExists(withSessionToken: token) players.registeredPlayerExists(withSessionToken: token)
} }
func currentTableOfPlayer(named player: PlayerName) -> TableId {
tables.currentTableOfPlayer(named: player) ?? ""
}
// MARK: Tables // MARK: Tables
func tableExists(withId id: TableId) -> Bool {
tables.tableExists(withId: id)
}
func tableIsFull(withId id: TableId) -> Bool {
tables.tableIsFull(withId: id)
}
/** /**
Create a new table with optional players. Create a new table with optional players.
- Parameter name: The name of the table - Parameter name: The name of the table
@ -103,14 +90,7 @@ final class Database {
tables.getPublicTableInfos() tables.getPublicTableInfos()
} }
func join(tableId: TableId, player: PlayerName) { func join(tableId: TableId, player: PlayerName) -> TableManagement.JoinTableResult {
let playersAtTable = tables.join(tableId: tableId, player: player) tables.join(tableId: tableId, player: player)
playersAtTable
.compactMap { players.sessionToken(forPlayer: $0) } // Session Tokens
.compactMap { sessions[$0] } // Sockets
.forEach { socket in
// TODO: Notify sessions about changed players
// socket.send("")
}
} }
} }

View File

@ -86,11 +86,12 @@ final class PlayerManagement {
return token return token
} }
func endSession(forSessionToken token: SessionToken) { func endSession(forSessionToken token: SessionToken) -> PlayerName? {
guard let player = playerNameForToken.removeValue(forKey: token) else { guard let player = playerNameForToken.removeValue(forKey: token) else {
return return nil
} }
sessionTokenForPlayer.removeValue(forKey: player) sessionTokenForPlayer.removeValue(forKey: player)
return player
} }
/** /**

View File

@ -1,4 +1,10 @@
import Foundation import Foundation
import WebSocketKit
let maximumPlayersPerTable = 4
typealias TableId = String
typealias TableName = String
final class TableManagement { final class TableManagement {
@ -14,18 +20,12 @@ final class TableManagement {
/// A reverse list of players and their table id /// A reverse list of players and their table id
private var playerTables = [PlayerName: TableId]() private var playerTables = [PlayerName: TableId]()
private var playerConnections = [PlayerName : WebSocket]()
init() { init() {
} }
func tableExists(withId id: TableId) -> Bool {
tableNames[id] != nil
}
func tableIsFull(withId id: TableId) -> Bool {
(tablePlayers[id]?.count ?? playerPerTable) < playerPerTable
}
/** /**
Create a new table with optional players. Create a new table with optional players.
- Parameter name: The name of the table - Parameter name: The name of the table
@ -52,24 +52,54 @@ final class TableManagement {
}.sorted() }.sorted()
} }
func currentTableOfPlayer(named player: PlayerName) -> TableId? {
playerTables[player]
}
/** /**
Join a table. Join a table.
- Returns: The player names present at the table - Returns: The result of the join operation
*/ */
func join(tableId: TableId, player: PlayerName) -> [PlayerName] { func join(tableId: TableId, player: PlayerName) -> JoinTableResult {
guard var players = tablePlayers[tableId] else { guard var players = tablePlayers[tableId] else {
return [] return .tableNotFound
}
guard !players.contains(player) else {
return .success
}
guard players.count < maximumPlayersPerTable else {
return .tableIsFull
} }
players.append(player) players.append(player)
if let oldTable = playerTables[tableId] { if let oldTable = playerTables[tableId] {
remove(player: player, fromTable: oldTable) remove(player: player, fromTable: oldTable)
// TODO: End game if needed
//
} }
tablePlayers[tableId] = players tablePlayers[tableId] = players
playerTables[tableId] = tableId playerTables[tableId] = tableId
return players return .success
} }
func remove(player: PlayerName, fromTable tableId: TableId) { func remove(player: PlayerName, fromTable tableId: TableId) {
tablePlayers[tableId] = tablePlayers[tableId]?.filter { $0 != player } tablePlayers[tableId] = tablePlayers[tableId]?.filter { $0 != player }
} }
func remove(player: PlayerName) {
fatalError()
}
func connect(player: PlayerName, using socket: WebSocket) -> Bool {
fatalError()
}
func disconnect(player: PlayerName) {
fatalError()
}
enum JoinTableResult {
case tableNotFound
case tableIsFull
case success
}
} }

View File

@ -135,17 +135,34 @@ func routes(_ app: Application) throws {
} }
/** /**
Start a new bidirectional session connection. Get the current table of the player, if one exists.
- Parameter token: The session token of the player, as a string in the request body
- Throws:
- 400: Missing token
- 401: Invalid token
- Returns: The table id, or an empty string
*/
app.post("player", "table") { req -> String in
guard let token = req.body.string else {
throw Abort(.badRequest) // 400
}
guard let player = database.registeredPlayerExists(withSessionToken: token) else {
throw Abort(.unauthorized) // 401
}
return database.currentTableOfPlayer(named: player)
}
/**
Start a new websocket connection for the client to receive table updates from the server
- Returns: Nothing - Returns: Nothing
- Note: The first message over the connection must be a valid session token. - Note: The first (and only) message from the client over the connection must be a valid session token.
*/ */
app.webSocket("session", "start") { req, socket in app.webSocket("session", "start") { req, socket in
socket.onText { socket, text in socket.onText { socket, text in
guard database.isValid(sessionToken: text) else { guard database.startSession(socket: socket, sessionToken: text) else {
_ = socket.close() _ = socket.close()
return return
} }
database.startSession(socket: socket, sessionToken: text)
} }
} }
@ -198,7 +215,8 @@ func routes(_ app: Application) throws {
throw Abort(.forbidden) // 403 throw Abort(.forbidden) // 403
} }
let list = database.getPublicTableInfos() let list = database.getPublicTableInfos()
return try encoder.encode(list).base64EncodedString() let data = try encoder.encode(list)
return String(data: data, encoding: .utf8)!
} }
/** /**
@ -208,8 +226,8 @@ func routes(_ app: Application) throws {
- Throws: - Throws:
- 400: Missing token - 400: Missing token
- 401: The session token is invalid - 401: The session token is invalid
- 404: The table id doesn't exist - 410: The table id doesn't exist
- 406: The table is already full and can't be joined - 417: The table is already full and can't be joined
- Returns: Nothing - Returns: Nothing
*/ */
app.post("table", "join", ":table") { req -> String in app.post("table", "join", ":table") { req -> String in
@ -220,13 +238,13 @@ func routes(_ app: Application) throws {
guard let player = database.registeredPlayerExists(withSessionToken: token) else { guard let player = database.registeredPlayerExists(withSessionToken: token) else {
throw Abort(.unauthorized) // 401 throw Abort(.unauthorized) // 401
} }
guard database.tableExists(withId: table) else { switch database.join(tableId: table, player: player) {
throw Abort(.notFound) // 404 case .tableNotFound:
throw Abort(.gone) // 410
case .tableIsFull:
throw Abort(.expectationFailed) // 417
case .success:
return ""
} }
guard !database.tableIsFull(withId: table) else {
throw Abort(.notAcceptable) // 406
}
database.join(tableId: table, player: player)
return ""
} }
} }