#region Copyright & License Information /* * Copyright 2007,2009,2010 Chris Forbes, Robert Pepperell, Matthew Bowra-Dean, Paul Chote, Alli Witheford. * This file is part of OpenRA. * * OpenRA is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * OpenRA is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenRA. If not, see . */ #endregion using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Threading; using OpenRA.FileFormats; namespace OpenRA.Server { static class Server { static List conns = new List(); static TcpListener listener; static Dictionary> inFlightFrames = new Dictionary>(); static Session lobbyInfo; static bool GameStarted = false; static string[] initialMods; static string Name; static WebClient wc = new WebClient(); static int ExternalPort; const int DownloadChunkInterval = 20000; const int DownloadChunkSize = 16384; const int MasterPingInterval = 60 * 3; // 3 minutes. server has a 5 minute TTL for games, so give ourselves a bit // of leeway. static int lastPing = 0; static bool isInternetServer; static string masterServerUrl; public static void ServerMain(bool internetServer, string masterServerUrl, string name, int port, int extport, string[] mods) { Server.masterServerUrl = masterServerUrl; isInternetServer = internetServer; listener = new TcpListener(IPAddress.Any, port); initialMods = mods; Name = name; ExternalPort = extport; lobbyInfo = new Session(); lobbyInfo.GlobalSettings.Mods = mods; Console.WriteLine("Initial mods: "); foreach( var m in lobbyInfo.GlobalSettings.Mods ) Console.WriteLine("- {0}", m); try { listener.Start(); } catch (Exception) { throw new InvalidOperationException( "Unable to start server: port is already in use" ); } new Thread( _ => { for( ; ; ) { var checkRead = new ArrayList(); checkRead.Add( listener.Server ); foreach( var c in conns ) checkRead.Add( c.socket ); var isSendingPackages = conns.Any( c => c.Stream != null ); /* msdn lies, -1 doesnt work. this is ~1h instead. */ Socket.Select( checkRead, null, null, isSendingPackages ? DownloadChunkInterval : MasterPingInterval * 1000000 ); foreach( Socket s in checkRead ) if( s == listener.Server ) AcceptConnection(); else conns.Single( c => c.socket == s ).ReadData(); foreach( var c in conns.Where( a => a.Stream != null ).ToArray() ) SendNextChunk( c ); if (Environment.TickCount - lastPing > MasterPingInterval * 1000) PingMasterServer(); } } ) { IsBackground = true }.Start(); } static int ChooseFreePlayerIndex() { for (var i = 0; i < 8; i++) if (conns.All(c => c.PlayerIndex != i)) return i; throw new InvalidOperationException("Already got 8 players"); } static int ChooseFreePalette() { // TODO: Query the list of palettes from somewhere, and pick one return 0; } static void AcceptConnection() { var newConn = new Connection { socket = listener.AcceptSocket() }; try { if (GameStarted) { Console.WriteLine("Rejected connection from {0}; game is already started.", newConn.socket.RemoteEndPoint); newConn.socket.Close(); return; } newConn.socket.Blocking = false; newConn.socket.NoDelay = true; // assign the player number. newConn.PlayerIndex = ChooseFreePlayerIndex(); newConn.socket.Send(BitConverter.GetBytes(ProtocolVersion.Version)); newConn.socket.Send(BitConverter.GetBytes(newConn.PlayerIndex)); conns.Add(newConn); lobbyInfo.Clients.Add( new Session.Client() { Index = newConn.PlayerIndex, PaletteIndex = ChooseFreePalette(), Name = "Player {0}".F(1 + newConn.PlayerIndex), Country = "Random", State = Session.ClientState.NotReady }); Console.WriteLine("Client {0}: Accepted connection from {1}", newConn.PlayerIndex, newConn.socket.RemoteEndPoint); SendChat(newConn, "has joined the game."); SyncLobbyInfo(); } catch (Exception e) { DropClient(newConn, e); } } public static void UpdateInFlightFrames(Connection conn) { if (conn.Frame != 0) { if (!inFlightFrames.ContainsKey(conn.Frame)) inFlightFrames[conn.Frame] = new List { conn }; else inFlightFrames[conn.Frame].Add(conn); if (conns.All(c => inFlightFrames[conn.Frame].Contains(c))) { inFlightFrames.Remove(conn.Frame); } } } class Chunk { public int Index = 0; public int Count = 0; public string Data = ""; } static void SendNextChunk(Connection c) { try { var data = c.Stream.Read(Math.Min(DownloadChunkSize, c.RemainingBytes)); if (data.Length != 0) { var chunk = new Chunk { Index = c.NextChunk++, Count = c.NumChunks, Data = Convert.ToBase64String(data) }; DispatchOrdersToClient(c, 0, 0, new ServerOrder("FileChunk", FieldSaver.Save(chunk).Nodes.WriteToString()).Serialize()); } c.RemainingBytes -= data.Length; if (c.RemainingBytes == 0) { GetClient(c).State = Session.ClientState.NotReady; c.Stream.Dispose(); c.Stream = null; SyncLobbyInfo(); } } catch (Exception e) { DropClient(c, e); } } static void DispatchOrdersToClient(Connection c, int client, int frame, byte[] data) { try { c.socket.Blocking = true; c.socket.Send(BitConverter.GetBytes(data.Length + 4)); c.socket.Send(BitConverter.GetBytes(client)); c.socket.Send(BitConverter.GetBytes(frame)); c.socket.Send(data); c.socket.Blocking = false; } catch (Exception e) { DropClient(c, e); } } public static void DispatchOrders(Connection conn, int frame, byte[] data) { if (frame == 0 && conn != null) InterpretServerOrders(conn, data); else { var from = conn != null ? conn.PlayerIndex : 0; foreach (var c in conns.Except(conn).ToArray()) DispatchOrdersToClient(c, from, frame, data); } } static void InterpretServerOrders(Connection conn, byte[] data) { var ms = new MemoryStream(data); var br = new BinaryReader(ms); try { for (; ; ) { var so = ServerOrder.Deserialize(br); if (so == null) return; InterpretServerOrder(conn, so); } } catch (EndOfStreamException) { } } static bool InterpretCommand(Connection conn, string cmd) { var dict = new Dictionary> { { "ready", s => { // if we're downloading, we can't ready up. var client = GetClient(conn); if (client.State == Session.ClientState.NotReady) client.State = Session.ClientState.Ready; else if (client.State == Session.ClientState.Ready) client.State = Session.ClientState.NotReady; Console.WriteLine("Player @{0} is {1}", conn.socket.RemoteEndPoint, client.State); SyncLobbyInfo(); // start the game if everyone is ready. if (conns.Count > 0 && conns.All(c => GetClient(c).State == Session.ClientState.Ready)) { Console.WriteLine("All players are ready. Starting the game!"); GameStarted = true; foreach( var c in conns ) foreach( var d in conns ) DispatchOrdersToClient( c, d.PlayerIndex, 0x7FFFFFFF, new byte[] { 0xBF } ); DispatchOrders(null, 0, new ServerOrder("StartGame", "").Serialize()); PingMasterServer(); } return true; }}, { "name", s => { Console.WriteLine("Player@{0} is now known as {1}", conn.socket.RemoteEndPoint, s); GetClient(conn).Name = s; SyncLobbyInfo(); return true; }}, { "lag", s => { int lag; if (!int.TryParse(s, out lag)) { Console.WriteLine("Invalid order lag: {0}", s); return false; } Console.WriteLine("Order lag is now {0} frames.", lag); lobbyInfo.GlobalSettings.OrderLatency = lag; SyncLobbyInfo(); return true; }}, { "race", s => { if (GameStarted) { SendChatTo( conn, "You can't change your race after the game has started" ); return true; } GetClient(conn).Country = s; SyncLobbyInfo(); return true; }}, { "spawn", s => { if (GameStarted) { SendChatTo( conn, "You can't change your spawn point after the game has started" ); return true; } int spawnPoint; if (!int.TryParse(s, out spawnPoint) || spawnPoint < 0 || spawnPoint > 8) //TODO: SET properly! { Console.WriteLine("Invalid spawn point: {0}", s); return false; } if (lobbyInfo.Clients.Where( c => c != GetClient(conn) ).Any( c => (c.SpawnPoint == spawnPoint) && (c.SpawnPoint != 0) )) { SendChatTo( conn, "You can't be at the same spawn point as another player" ); return true; } GetClient(conn).SpawnPoint = spawnPoint; SyncLobbyInfo(); return true; }}, { "pal", s => { if (GameStarted) { SendChatTo( conn, "You can't change your color after the game has started" ); return true; } int pali; if (!int.TryParse(s, out pali)) { Console.WriteLine("Invalid palette: {0}", s); return false; } if (lobbyInfo.Clients.Where( c => c != GetClient(conn) ).Any( c => c.PaletteIndex == pali )) { SendChatTo( conn, "You can't be the same color as another player" ); return true; } GetClient(conn).PaletteIndex = pali; SyncLobbyInfo(); return true; }}, { "map", s => { if (conn.PlayerIndex != 0) { SendChatTo( conn, "Only the host can change the map" ); return true; } if (GameStarted) { SendChatTo( conn, "You can't change the map after the game has started" ); return true; } lobbyInfo.GlobalSettings.Map = s; foreach(var client in lobbyInfo.Clients) client.SpawnPoint = 0; SyncLobbyInfo(); return true; }}, { "addpkg", s => { if (GameStarted) { SendChatTo( conn, "You can't change packages after the game has started" ); return true; } Console.WriteLine("** Added package: `{0}`", s); try { lobbyInfo.GlobalSettings.Packages = lobbyInfo.GlobalSettings.Packages.Concat( new string[] { MakePackageString(s)}).ToArray(); SyncLobbyInfo(); return true; } catch { Console.WriteLine("Adding the package failed."); SendChatTo( conn, "Adding the package failed." ); return true; } }}, { "mods", s => { if (GameStarted) { SendChatTo( conn, "You can't change mods after the game has started" ); return true; } lobbyInfo.GlobalSettings.Mods = s.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); SyncLobbyInfo(); return true; }}, }; var cmdName = cmd.Split(' ').First(); var cmdValue = string.Join(" ", cmd.Split(' ').Skip(1).ToArray()); Func a; if (!dict.TryGetValue(cmdName, out a)) return false; Console.WriteLine( "Client {0} sent server command: {1}", conn.PlayerIndex, cmd ); return a(cmdValue); } static void SendChatTo(Connection conn, string text) { DispatchOrdersToClient(conn, 0, 0, new ServerOrder("Chat", text).Serialize()); } static void SendChat(Connection asConn, string text) { DispatchOrders(null, 0, new ServerOrder("Chat", text).Serialize()); } static void InterpretServerOrder(Connection conn, ServerOrder so) { switch (so.Name) { case "Chat": if (so.Data.StartsWith("/")) { if (!InterpretCommand(conn, so.Data.Substring(1))) { Console.WriteLine("Bad server command: {0}", so.Data.Substring(1)); SendChatTo(conn, "Bad server command."); } } else foreach (var c in conns.Except(conn).ToArray()) DispatchOrdersToClient(c, GetClient(conn).Index, 0, so.Serialize()); break; case "RequestFile": { Console.WriteLine("** Requesting file: `{0}`", so.Data); var client = GetClient(conn); client.State = Session.ClientState.Downloading; var filename = so.Data.Split(':')[0]; if (conn.Stream != null) conn.Stream.Dispose(); conn.Stream = File.OpenRead(filename); // todo: validate that the SHA1 they asked for matches what we've got. var length = (int) new FileInfo(filename).Length; conn.NextChunk = 0; conn.NumChunks = (length + DownloadChunkSize - 1) / DownloadChunkSize; conn.RemainingBytes = length; SyncLobbyInfo(); } break; } } static Session.Client GetClient(Connection conn) { return lobbyInfo.Clients.First(c => c.Index == conn.PlayerIndex); } public static void DropClient(Connection toDrop, Exception e) { Console.WriteLine("Client dropped: {0}.", toDrop.socket.RemoteEndPoint); conns.Remove(toDrop); SendChat(toDrop, "Connection Dropped"); lobbyInfo.Clients.RemoveAll(c => c.Index == toDrop.PlayerIndex); foreach( var c in conns ) DispatchOrders( toDrop, toDrop.MostRecentFrame, new byte[] { 0xbf } ); if (conns.Count == 0) OnServerEmpty(); else SyncLobbyInfo(); } static void OnServerEmpty() { Console.WriteLine("Server emptied out; doing a bit of housekeeping to prepare for next game.."); inFlightFrames.Clear(); lobbyInfo = new Session(); lobbyInfo.GlobalSettings.Mods = initialMods; GameStarted = false; } static string MakePackageString(string a) { return "{0}:{1}".F(a, CalculateSHA1(a)); } static string CalculateSHA1(string filename) { using (var csp = SHA1.Create()) return new string(csp.ComputeHash(File.ReadAllBytes(filename)) .SelectMany(a => a.ToString("x2")).ToArray()); } static void SyncLobbyInfo() { var clientData = lobbyInfo.Clients.ToDictionary( a => a.Index.ToString(), a => FieldSaver.Save(a)); clientData["GlobalSettings"] = FieldSaver.Save(lobbyInfo.GlobalSettings); DispatchOrders(null, 0, new ServerOrder("SyncInfo", clientData.WriteToString()).Serialize()); PingMasterServer(); } static void PingMasterServer() { if (wc.IsBusy || !isInternetServer) return; wc.DownloadDataAsync(new Uri( masterServerUrl + "ping.php?port={0}&name={1}&state={2}&players={3}&mods={4}&map={5}".F( ExternalPort, Uri.EscapeUriString(Name), GameStarted ? 2 : 1, // todo: post-game states, etc. lobbyInfo.Clients.Count, string.Join(",", lobbyInfo.GlobalSettings.Mods), lobbyInfo.GlobalSettings.Map))); lastPing = Environment.TickCount; } } }