Click here to Skip to main content
15,891,316 members
Articles / Programming Languages / C#

The SpaceShoot server

Rate me:
Please Sign up or sign in to vote.
4.97/5 (32 votes)
27 Mar 2012CPOL28 min read 52K   1.9K   56  
Providing a stable and powerful server for the JavaScript / HTML5 browser game SpaceShoot with some gimmicks.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Fleck;
using System.Json;
using SpaceShooterServer.Structures;
using System.Net;
using System.Threading;

namespace SpaceShooterServer.Game
{
    /// <summary>
    /// The basic class that contains all matches and players
    /// </summary>
    public class MatchCollection : MatchHelpers
    {
        #region ctor

        public MatchCollection()
        {
            bannedConnections = new List<string>();
            Lobby = new List<IWebSocketConnection>();
            Matches = new List<Match>();
            DebugLog = new Queue<string>(99);
        }

        #endregion

        #region Members

        List<string> bannedConnections;

        #endregion

        #region Properties

        /// <summary>
        /// Gets the array with all players that are connected to the server
        /// </summary>
        public List<IWebSocketConnection> Lobby { get; private set; }

        /// <summary>
        /// Gets a list of currently available matches
        /// </summary>
        public List<Match> Matches { get; private set; }

        /// <summary>
        /// Gets the list of debug log entries
        /// </summary>
        public Queue<string> DebugLog { get; private set; }

        /// <summary>
        /// Gets the number of all matches that have been created since the server is running
        /// </summary>
        public int MatchCount { get; private set; }

        /// <summary>
        /// Gets the currently banned IP addresses
        /// </summary>
        public string[] BannedConnections { get { return bannedConnections.ToArray(); } }

        #endregion

        #region Methods

        private bool IsInList(IWebSocketConnection socket)
        {
            return Lobby.Contains(socket);
        }

        private bool IsInLobby(IWebSocketConnection socket)
        {
            return IsInList(socket) && !IsInMatch(socket);
        }

        private bool IsInMatch(IWebSocketConnection socket)
        {
            return Matches.Where(match => match.Ships.Any(ship => ship.Connection == socket)).Any();
        }

        private Match FindMatch(IWebSocketConnection socket)
        {
            return Matches.Where(match => match.Ships.Any(ship => ship.Connection == socket)).FirstOrDefault() ?? new Match();
        }

        /// <summary>
        /// Bans a specific connection from the server
        /// </summary>
        /// <param name="ip">The IP address of the user to ban</param>
        public void Ban(string ip)
        {
            if (!bannedConnections.Contains(ip))
            {
                bannedConnections.Add(ip);

                for (int i = 0; i < Lobby.Count; i++)
                {
                    var con = Lobby[i];
                    con.Close();
                    break;
                }
            }
        }

        /// <summary>
        /// Removes a previously committed ban to a user
        /// </summary>
        /// <param name="ip">The IP address of the user to release</param>
        public void Unban(string ip)
        {
            if(bannedConnections.Contains(ip))
                bannedConnections.Remove(ip);
        }

        /// <summary>
        /// Lists all existing (and running) matches
        /// </summary>
        /// <returns>A JSON Object that contains the list of matches</returns>
        public JsonObject ListMatches()
        {
            var j = new JsonObject();
            var ja = new JsonArray();

            for (int i = Matches.Count - 1; i >= 0; i--)
            {
                if (Matches[i].Sockets.Count == 0)
                    Matches.RemoveAt(i);
                else
                    ja.Add(Matches[i].ToJson());
            }

            j["list"] = ja;
            j["cmd"] = "list";
            return j;
        }

        /// <summary>
        /// Appends a message to the debug log (max. 99 entries)
        /// </summary>
        /// <param name="msg">The message to append</param>
        /// <param name="args">The arguments to display in the message using {0}, ...</param>
        void AddToDebugLog(string msg, params object[] args)
        {
            if (DebugLog.Count == 99)
                DebugLog.Dequeue();

            DebugLog.Enqueue(string.Format(msg, args));
        }

        /// <summary>
        /// Adds a player to the server
        /// </summary>
        /// <param name="socket">The player's connection</param>
        public void AddPlayer(IWebSocketConnection socket)
        {
            if (bannedConnections.Contains(socket.ConnectionInfo.ClientIpAddress))
            {
                socket.Send(ReportError("You are banned from the server."));
                new Timer(o => { socket.Close(); }, null, 100, Timeout.Infinite);
                return;
            }

            if (IsInList(socket))
                RemovePlayer(socket);

            Lobby.Add(socket);
            AddToDebugLog("Connection added [ Users : {0} ]", Lobby.Count);
        }

        /// <summary>
        /// Removes a player from the server
        /// </summary>
        /// <param name="socket">The player's connection</param>
        public void RemovePlayer(IWebSocketConnection socket)
        {
            if(IsInMatch(socket))
                FindMatch(socket).RemovePlayer(socket);

            Lobby.Remove(socket);
            AddToDebugLog("Connection removed [ Users : {0} ]", Lobby.Count);
        }

        /// <summary>
        /// Information update from a player
        /// </summary>
        /// <param name="socket">The player's connection</param>
        /// <param name="message">The JSON string with the update information</param>
        public void UpdatePlayer(IWebSocketConnection socket, string message)
        {
            var j = JsonObject.Parse(message);

            if (j.ContainsKey("cmd"))
            {
                switch (ParseString(j["cmd"]))
                {
                    //Sent update (new keyboard state) of a game
                    case "keys":
#if DEBUG
                        AddToDebugLog("Keyboard update received.");
#endif
                        FindMatch(socket).ReceiveKeyboard(socket, j);
                        break;

                    //Send a chat message
                    case "chat":
#if DEBUG
                        AddToDebugLog("Chat was sent.");
#endif
                        FindMatch(socket).Chat(socket, ParseString(j["msg"]));
                        break;

                    //Leave a game
                    case "leave":
#if DEBUG
                        AddToDebugLog("A player is leaving the match.");
#endif
                        FindMatch(socket).RemovePlayer(socket);
                        break;

                    //Sent update (new color / player name) of a game
                    case "update":
#if DEBUG
                        AddToDebugLog("Update received.");
#endif
                        FindMatch(socket).UpdatePlayer(socket, j);
                        break;

                    //Request the list of available games
                    case "list":
#if DEBUG
                        AddToDebugLog("The match list is requested.");
#endif
                        socket.Send(ListMatches());
                        break;

                    //Join an existing game
                    case "join":
#if DEBUG
                        AddToDebugLog("A player joins a match.");
#endif
                        socket.Send(JoinMatch(j, socket));
                        break;

                    //Host a game
                    case "host":
#if DEBUG
                        AddToDebugLog("A match is about to get hosted.");
#endif
                        socket.Send(AddMatch(j, socket));
                        break;

                    //Everything in order to control the console
                    case "console":
                        if (socket.ConnectionInfo.ClientIpAddress.Equals(IPAddress.Loopback.ToString()))
                        {
                            if (Commands.Instance.Invoke(ParseString(j["msg"])))
                                socket.Send(ReportConsole(Commands.Instance.Last.FlushOutput()));
                            else
                                socket.Send(ReportConsole("Command not found or wrong arguments."));
                        }
                        else
                            socket.Send(ReportConsole("Not enough privileges for sending commands."));
                        break;
                }
            }
        }

        private JsonObject AddPlayer(JsonValue j, Match match, IWebSocketConnection socket)
        {
            var name = "Player";
            var color = new Colors();

            if (j.ContainsKey("player"))
                name = ParseString(j["player"]);

            if (j.ContainsKey("primaryColor"))
                color.Primary = ParseString(j["primaryColor"]);

            if (j.ContainsKey("secondaryColor"))
                color.Secondary = ParseString(j["secondaryColor"]);

            return match.AddPlayer(socket, name, color);
        }

        /// <summary>
        /// A player joins a certain game
        /// </summary>
        /// <param name="j">The JSON data of the game to join</param>
        /// <param name="socket">The player's connection</param>
        /// <returns>A JSON Object with more information about the joining process</returns>
        public JsonObject JoinMatch(JsonValue j, IWebSocketConnection socket)
        {
            if (!j.ContainsKey("id") || !j.ContainsKey("password"))
                return ReportError();

            var id = ParseInt(j["id"], 0);
            var matches = Matches.Where(m => m.Id == id);

            if (!matches.Any())
                return ReportError("The game you specified was not found.");

            var match = matches.First();

            if (!match.Password.Equals(ParseString(j["password"])))
                return ReportError("Wrong password. The game is password protected.");

            return AddPlayer(j, match, socket);
        }

        /// <summary>
        /// Adds a match to the list of available matches
        /// </summary>
        /// <param name="j">The JSON data of the game to add</param>
        /// <param name="socket">The creator's connection</param>
        /// <returns>A JSON object with more information about the creation process</returns>
        public JsonObject AddMatch(JsonValue j, IWebSocketConnection socket)
        {
            if (!j.ContainsKey("height") || !j.ContainsKey("width") || !j.ContainsKey("name") ||
                !j.ContainsKey("password") || !j.ContainsKey("friendly") || !j.ContainsKey("maxplayers") ||
                !j.ContainsKey("maxbots") || !j.ContainsKey("negative") || !j.ContainsKey("upgrades"))
                return ReportError();

            var match = new Match(Matches.Count + 1);
            match.Height = ParseInt(j["height"], 768);
            match.Width = ParseInt(j["width"], 1024);
            match.Name = ParseString(j["name"]);
            match.Password = ParseString(j["password"]);
            match.FriendlyFire = ParseBool(j["friendly"]);
            match.Maxplayers = ParseInt(j["maxplayers"], 64);
            match.Maxbots = ParseInt(j["maxbots"], 0);
            match.NegativePoints = ParseBool(j["negative"]);
            match.AllowUpgrades = ParseBool(j["upgrades"]);

            if (string.IsNullOrEmpty(match.Name))
                return ReportError("You have to specify a proper name for the match.");

            MatchCount++;
            Matches.Add(match);
            return AddPlayer(j, match, socket);
        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer
Germany Germany
Florian lives in Munich, Germany. He started his programming career with Perl. After programming C/C++ for some years he discovered his favorite programming language C#. He did work at Siemens as a programmer until he decided to study Physics.

During his studies he worked as an IT consultant for various companies. After graduating with a PhD in theoretical particle Physics he is working as a senior technical consultant in the field of home automation and IoT.

Florian has been giving lectures in C#, HTML5 with CSS3 and JavaScript, software design, and other topics. He is regularly giving talks at user groups, conferences, and companies. He is actively contributing to open-source projects. Florian is the maintainer of AngleSharp, a completely managed browser engine.

Comments and Discussions