Exemplo n.º 1
0
        public GameLobby Read(string gameType, string lobbyId)
        {
            new ValidationCheck()
            .Assert(ValidationCheck.BasicStringCheck(gameType, nameof(gameType)))
            .Assert(ValidationCheck.BasicStringCheck(lobbyId, nameof(lobbyId)))
            .Throw();

            string lobbyKey = CreateKey(gameType, lobbyId);

            using (var r = new RedisClient(this.opt))
            {
                var p = new RedisPipeline()
                        .Send(RedisCommand.GET, lobbyKey)
                        .Send(RedisCommand.EXPIRE, lobbyKey);

                Stopwatch sw = new Stopwatch();
                sw.Start();
                var foundGame = r.Send(p);
                this.publishTimingStats(redisCategory, sw.ElapsedMilliseconds);

                if (foundGame[0] == null)
                {
                    throw new LobbyException(404, "lobby does not exist");
                }

                string    lobbyStr = Encoding.UTF8.GetString(foundGame[0]);
                GameLobby gl       = JsonConvert.DeserializeObject <GameLobby>(lobbyStr);

                return(gl);
            }
        }
Exemplo n.º 2
0
        public GameLobby Join(string gameType, string lobbyId, string ip, int port, string name)
        {
            new ValidationCheck()
            .Assert(ValidationCheck.BasicStringCheck(gameType, nameof(gameType)))
            .Assert(ValidationCheck.BasicStringCheck(lobbyId, nameof(lobbyId)))
            .Assert(ValidationCheck.BasicStringCheck(name, nameof(name)))
            .Assert(ValidationCheck.BasicStringCheck(ip, nameof(ip)))
            .Assert(port > 1024, nameof(port) + " is privileged")
            .Throw();

            gameType = gameType.ToUpperInvariant();
            lobbyId  = lobbyId.ToUpperInvariant();

            Client client = new Client();

            client.port = port;
            client.name = name;
            client.ip   = ip;

            string lobbyKey = CreateKey(gameType, lobbyId);

            using (var r = new RedisClient(this.opt))
            {
                Stopwatch sw = new Stopwatch();
                sw.Start();
                var foundGame = r.Send(RedisCommand.GET, lobbyKey);
                sw.Stop();

                if (foundGame == null)
                {
                    throw new LobbyException(404, "lobby does not exist");
                }
                string    lobbyStr = Encoding.UTF8.GetString(foundGame);
                GameLobby gl       = JsonConvert.DeserializeObject <GameLobby>(lobbyStr);
                new ValidationCheck()
                .Assert(gl.host.uid != client.uid, "host can't join her own game")
                .Assert(gl.clients.All(c => c.uid != client.uid), "already joined")
                .Assert(gl.clients.All(c => c.name != client.name), "must provide a unique name")
                .Assert(gl.gameType == gameType, "lobby type changed")
                .Assert(gl.lobbyId == lobbyId, "lobby id changed")
                .Assert(gl.clients.Count <= maxLobbySize, "lobby is full (" + maxLobbySize + ")")
                .Throw();

                gl.clients.Add(client);

                sw.Start();
                var resp = r.Send(RedisCommand.SET, lobbyKey, JsonConvert.SerializeObject(gl), "XX", "EX", ExpirationTimeSec);
                this.publishTimingStats(redisCategory, sw.ElapsedMilliseconds);

                if (resp == null)
                {
                    throw new LobbyException(404, "lobby no longer exists");
                }

                return(gl);
            }
        }
Exemplo n.º 3
0
        public GameLobby Create(string gameType, string ip, int port, string name, bool hidden, Dictionary <string, string> meta)
        {
            new ValidationCheck()
            .Assert(ValidationCheck.BasicStringCheck(gameType, nameof(gameType)))
            .Assert(ValidationCheck.BasicStringCheck(ip, nameof(ip)))
            .Assert(ValidationCheck.BasicStringCheck(name, nameof(name)))
            .Assert(port > 1024, nameof(port) + " is privileged")
            .Throw();

            gameType = gameType.ToUpperInvariant().Trim();
            ip       = ip.ToUpperInvariant().Trim();

            using (var r = new RedisClient(this.opt))
            {
                GameLobby gl = new GameLobby();
                gl.clients      = new List <Client>();
                gl.creationTime = DateTime.UtcNow;
                gl.host         = new Client();
                gl.host.port    = port;
                gl.host.name    = name;
                gl.host.ip      = ip;
                gl.gameType     = gameType;
                gl.hidden       = hidden;
                // The lobbyID is very much a magic string, and care
                // should be taken if you mess with it.
                // Redis search methods depend on it being a certain
                // format: "gameType-[s]shortId"
                gl.lobbyId = IdGenerator.GetId(hidden);

                gl.metaData = meta ?? new Dictionary <string, string>();

                string lobbyKey = CreateKey(gl.gameType, gl.lobbyId);

                var pipe = new RedisPipeline()
                           // only allow one game to be hosted per IP address
                           .Send(RedisCommand.EVAL, EnsureSingleLua, "2", hostPrefix + gl.host.ip, lobbyKey)
                           // actually create the lobby
                           .Send(RedisCommand.SET, lobbyKey, JsonConvert.SerializeObject(gl), "NX", "EX", ExpirationTimeSec);

                Stopwatch sw = new Stopwatch();
                sw.Start();
                var resp = r.Send(pipe);
                this.publishTimingStats(redisCategory, sw.ElapsedMilliseconds);

                if (resp[1] == null)
                {
                    throw new LobbyException(500, "lobby already exists");
                }

                return(gl);
            }
        }
Exemplo n.º 4
0
        /// <summary>
        /// get a list of lobbies for a gametype.
        /// hidden games will not be returned
        /// </summary>
        /// <param name="gameType">gametype to search</param>
        /// <param name="metaKey">optional metadata key filter. the key must exist</param>
        /// <param name="metaValue">optional metadata value filter. the value must match for the given key.</param>
        /// <returns></returns>
        public List <GameLobby> Search(string gameType, string metaKey = null, string metaValue = null)
        {
            new ValidationCheck()
            .Assert(ValidationCheck.BasicStringCheck(gameType, nameof(gameType)))
            .Throw();

            gameType = gameType.ToUpperInvariant();

            // As it stands now, this search method will not scale well when there are many instances
            // of lobbies for the same game. maxSearchReturnSize*keyMult games are pulled first,
            // and then those are filtered by metaKey and metaValue. If none of those games in that
            // first scan match, then no games will be returned.
            // If this ever becomes a problem, I could create additional keys for metadata
            // inside Redis and use pattern matching SCAN on those keys. This just opens me up
            // to requiring a much larger redis instance, though, unless I want to further restrict
            // metaData.
            int keyMult = 1;
            Func <Dictionary <string, string>, bool> matches = (d) => true;

            if (!string.IsNullOrWhiteSpace(metaKey))
            {
                keyMult = 10;
                if (!string.IsNullOrWhiteSpace(metaValue))
                {
                    matches = (d) => d != null && d.ContainsKey(metaKey) && string.Equals(d[metaKey], metaValue);
                }
                else
                {
                    matches = (d) => d != null && d.ContainsKey(metaKey);
                }
            }

            var           foundGames = new List <GameLobby>();
            RedisPipeline pipe       = new RedisPipeline();

            using (var r = new RedisClient(this.opt))
            {
                // get initial set of keys
                Stopwatch sw = new Stopwatch();
                sw.Start();
                foreach (var searchRes in r.Scan(RedisCommand.SCAN, lobbyPrefix + gameType + "-[^" + SecretPrefix.ToString() + "]*"))
                {
                    foreach (var key in searchRes)
                    {
                        // build up messages to get more data
                        pipe.Send(RedisCommand.GET, key);

                        if (pipe.Length >= maxSearchReturnSize * keyMult)
                        {
                            break;
                        }
                    }
                    if (pipe.Length >= maxSearchReturnSize * keyMult)
                    {
                        break;
                    }
                }

                // get lobby data
                var readDetails = r.Send(pipe);
                this.publishTimingStats(redisCategory, sw.ElapsedMilliseconds);

                foreach (var lobby in readDetails)
                {
                    string    lobbyStr = Encoding.UTF8.GetString(lobby);
                    GameLobby gl       = JsonConvert.DeserializeObject <GameLobby>(lobbyStr);

                    if (matches(gl.metaData) && (gl.gameType == gameType))
                    {
                        foundGames.Add(gl);
                    }

                    if (foundGames.Count >= maxSearchReturnSize)
                    {
                        break;
                    }
                }
            }

            if (foundGames.Count == 0)
            {
                throw new LobbyException(404, "no games match search parameters");
            }

            return(foundGames);
        }
Exemplo n.º 5
0
        public GameLobby Leave(string gameType, string lobbyId, string callingIp, string kickName = null)
        {
            new ValidationCheck()
            .Assert(ValidationCheck.BasicStringCheck(gameType, nameof(gameType)))
            .Assert(ValidationCheck.BasicStringCheck(lobbyId, nameof(lobbyId)))
            .Assert(ValidationCheck.BasicStringCheck(kickName, nameof(kickName)))
            .Assert(ValidationCheck.BasicStringCheck(callingIp, nameof(callingIp)))
            .Throw();

            gameType = gameType.ToUpperInvariant();
            lobbyId  = lobbyId.ToUpperInvariant();

            // unlike other methods, the Host can force a different user to Leave
            string lobbyKey = CreateKey(gameType, lobbyId);

            using (var r = new RedisClient(this.opt))
            {
                Stopwatch sw = new Stopwatch();
                sw.Start();
                var foundGame = r.Send(RedisCommand.GET, lobbyKey);
                sw.Stop();

                if (foundGame == null)
                {
                    throw new LobbyException(404, "lobby does not exist");
                }
                string    lobbyStr = Encoding.UTF8.GetString(foundGame);
                GameLobby gl       = JsonConvert.DeserializeObject <GameLobby>(lobbyStr);
                new ValidationCheck()
                .Assert(gl.lobbyId == lobbyId, "lobby id changed")
                .Assert(gl.gameType == gameType, "gametype changed")
                .Throw();

                // Determine the client of the caller
                Client caller = gl.clients
                                .Concat(new Client[] { gl.host })
                                .First(c => string.Equals(c.ip, callingIp, StringComparison.InvariantCultureIgnoreCase));

                // Determine who is being kicked. If kickName is null, assume the caller is leaving.
                Client kickee = caller;
                if (!string.IsNullOrWhiteSpace(kickName))
                {
                    kickee = gl.clients
                             .Concat(new Client[] { gl.host })
                             .FirstOrDefault(c => string.Equals(c.name, kickName, StringComparison.InvariantCultureIgnoreCase));
                }

                new ValidationCheck()
                .Assert(caller != null, "caller must be in the client list")
                .Assert(kickee != null, "client to kick must be in the client list")
                .Throw();

                // Caller is the host and is leaving their own game
                if ((gl.host == caller) && (gl.host == kickee))
                {
                    sw.Start();
                    r.Send(RedisCommand.DEL, lobbyKey);
                    sw.Stop();
                }
                // Host is kicking someone or someone volunteered to leave
                else if ((gl.host == caller) || (kickee == caller))
                {
                    gl.clients.RemoveAll(c => string.Equals(c.name, kickName, StringComparison.InvariantCultureIgnoreCase));

                    sw.Start();
                    var resp = r.Send(RedisCommand.SET, lobbyKey, JsonConvert.SerializeObject(gl), "XX", "EX", ExpirationTimeSec);
                    sw.Stop();

                    if (resp == null)
                    {
                        throw new LobbyException(404, "lobby no longer exists");
                    }
                }
                else
                {
                    throw new LobbyException(403, "only the host can kick other users");
                }

                this.publishTimingStats(redisCategory, sw.ElapsedMilliseconds);

                return(gl);
            }
        }