import Foundation typealias PlayerName = String typealias PasswordHash = String typealias SessionToken = String /// Manages player registration, session tokens and password hashes final class PlayerManagement: DiskWriter { /// A mapping between player name and their password hashes private var playerPasswordHashes = [PlayerName: PasswordHash]() /// A mapping between player name and generated access tokens for a session private var sessionTokenForPlayer = [PlayerName: SessionToken]() /// A reverse mapping between generated access tokens and player name private var playerNameForToken = [SessionToken: PlayerName]() var storageFile: FileHandle let storageFileUrl: URL init(storageFolder: URL) throws { let url = storageFolder.appendingPathComponent("passwords.txt") storageFileUrl = url storageFile = try Self.prepareFile(at: url) var redundantEntries = 0 try readLinesFromDisk().forEach { line in let parts = line.components(separatedBy: ":") // Token may contain the separator guard parts.count >= 2 else { print("Invalid line in password file") return } let name = parts[0] let token = parts.dropFirst().joined(separator: ":") if token == "" { playerPasswordHashes[name] = nil redundantEntries += 2 // One for creation, one for deletion return } if playerPasswordHashes[name] != nil { redundantEntries += 1 } playerPasswordHashes[name] = token } let playerCount = playerPasswordHashes.count let totalEntries = playerCount + redundantEntries let percentage = playerCount * 100 / totalEntries print("Loaded \(playerCount) players from \(totalEntries) entries (\(percentage) % useful)") if percentage < 80 && redundantEntries > 10 { try optimizePlayerFile() } } private func optimizePlayerFile() throws { print("Optimizing player file...") let lines = playerPasswordHashes.map { $0.key + ":" + $0.value + "\n" }.joined() try replaceFile(data: lines) print("Done.") } private func save(password: PasswordHash, forPlayer player: PlayerName) -> Bool { writeToDisk(line: player + ":" + password) } private func deletePassword(forPlayer player: PlayerName) -> Bool { writeToDisk(line: player + ":") } /** Check if a player exists. - Parameter name: The name of the player - Returns: true, if the player exists */ func hasRegisteredPlayer(named user: PlayerName) -> Bool { playerPasswordHashes[user] != nil } /** Get the password hash for a player, if the player exists. - Parameter name: The name of the player - Returns: The stored password hash, if the player exists */ func passwordHash(ofRegisteredPlayer name: PlayerName) -> PasswordHash? { playerPasswordHashes[name] } /** Create a new player and assign an access token. - Parameter name: The name of the new player - Parameter hash: The password hash of the player - Returns: The generated access token for the session */ func registerPlayer(named name: PlayerName, hash: PasswordHash) -> SessionToken? { guard !hasRegisteredPlayer(named: name) else { return nil } guard save(password: hash, forPlayer: name) else { return nil } self.playerPasswordHashes[name] = hash return startNewSessionForRegisteredPlayer(named: name) } /** Delete a player - Parameter name: The name of the player to delete. - Returns: The session token of the current player, if one exists */ func deletePlayer(named name: PlayerName) -> SessionToken? { guard deletePassword(forPlayer: name) else { return nil } playerPasswordHashes.removeValue(forKey: name) guard let sessionToken = sessionTokenForPlayer.removeValue(forKey: name) else { return nil } playerNameForToken.removeValue(forKey: sessionToken) return sessionToken } func isValid(sessionToken token: SessionToken) -> Bool { playerNameForToken[token] != nil } func sessionToken(forPlayer player: PlayerName) -> SessionToken? { sessionTokenForPlayer[player] } /** Start a new session for an existing player. - Parameter name: The player name - Returns: The generated access token for the session */ func startNewSessionForRegisteredPlayer(named name: PlayerName) -> SessionToken { let token = SessionToken.newToken() self.sessionTokenForPlayer[name] = token self.playerNameForToken[token] = name return token } func endSession(forSessionToken token: SessionToken) -> PlayerName? { guard let player = playerNameForToken.removeValue(forKey: token) else { return nil } sessionTokenForPlayer.removeValue(forKey: player) return player } /** Get the player for a session token. - Parameter token: The access token for the player - Returns: The name of the player, if it exists */ func registeredPlayerExists(withSessionToken token: SessionToken) -> PlayerName? { playerNameForToken[token] } }