using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; namespace RBG_Server { public class GameServer { private readonly byte startRow, startColumn, rowSize, columnSize, zoomRow, zoomCol, zoomRowSpan, zoomColSpan; private readonly Dictionary boardImages; /// /// Initialise a new server, with the provided starting row & column /// /// /// The starting row of the board /// The starting column of the board /// The full-scale board image public GameServer(byte startRow, byte startColumn, byte rowSize, byte columnSize, byte zoomRow, byte zoomCol, byte zoomRowSpan, byte zoomColSpan, Stream boardImage) { this.startRow = startRow; this.startColumn = startColumn; this.rowSize = rowSize; this.columnSize = columnSize; this.zoomRow = zoomRow; this.zoomCol = zoomCol; this.zoomRowSpan = zoomRowSpan; this.zoomColSpan = zoomColSpan; BitmapImage gameBoardImage = new(); gameBoardImage.StreamSource = boardImage; // Hold images Dictionary sourceImages = new(); // Create a JPEG encoder JpegBitmapEncoder encoder = new() { QualityLevel = 100, }; // Add the base frame to the encoder and retrieve its byte array encoder.Frames.Add(BitmapFrame.Create(gameBoardImage)); using (MemoryStream stream = new()) { encoder.Save(stream); byte[] frameData = stream.ToArray(); sourceImages.Add(-1, frameData); encoder.Frames.Clear(); } // Add a resized version for each "common" scale if (gameBoardImage.PixelHeight > 2160) { double rate = 2160 / gameBoardImage.PixelHeight; TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate)); encoder.Frames.Add(BitmapFrame.Create(modified)); using MemoryStream stream = new(); encoder.Save(stream); byte[] frameData = stream.ToArray(); sourceImages.Add(2160, frameData); encoder.Frames.Clear(); } if (gameBoardImage.PixelHeight > 1440) { double rate = 1440 / gameBoardImage.PixelHeight; TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate)); encoder.Frames.Add(BitmapFrame.Create(modified)); using MemoryStream stream = new(); encoder.Save(stream); byte[] frameData = stream.ToArray(); sourceImages.Add(1440, frameData); encoder.Frames.Clear(); } if (gameBoardImage.PixelHeight > 1080) { double rate = 1080 / gameBoardImage.PixelHeight; TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate)); encoder.Frames.Add(BitmapFrame.Create(modified)); using MemoryStream stream = new(); encoder.Save(stream); byte[] frameData = stream.ToArray(); sourceImages.Add(1080, frameData); encoder.Frames.Clear(); } if (gameBoardImage.PixelHeight > 720) { double rate = 720 / gameBoardImage.PixelHeight; TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate)); encoder.Frames.Add(BitmapFrame.Create(modified)); using MemoryStream stream = new(); encoder.Save(stream); byte[] frameData = stream.ToArray(); sourceImages.Add(720, frameData); encoder.Frames.Clear(); } // Send a 240p thumbnail { double rate = 240 / gameBoardImage.PixelHeight; TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate)); encoder.Frames.Add(BitmapFrame.Create(modified)); using MemoryStream stream = new(); encoder.Save(stream); byte[] frameData = stream.ToArray(); sourceImages.Add(240, frameData); encoder.Frames.Clear(); } // add the board images to the boardImages = sourceImages; } public List Players { get; } = new(); // Map players to connections/clients Dictionary clients = new(); // Clients that have connected, but are not yet added to the list ConcurrentQueue awaitingAdds = new(); void Init(IPAddress localAddr, int port) { TcpListener listener = new(localAddr, port); listener.Start(); while (true) { TcpClient client = listener.AcceptTcpClient(); // Delegate to another thread _ = Task.Run(() => { AcceptConnections(client); }); } } /// /// Accept the client, and do the preprocessing /// /// void AcceptConnections(TcpClient client) { // Ensure we send data immediately client.NoDelay = true; // In the case the client NetworkStream stream = client.GetStream(); // Await the mode switch data from the client int command; do { command = stream.ReadByte(); break; } while (stream.DataAvailable); if (command == 0xFD) { // Connect to game; send request } else if (command == 0xFC) { // Next byte contains the screen size int screenSize = stream.ReadByte() << 8 | stream.ReadByte(); // Send three images, the low res, and the screen res, and the full res (in that order, so the client may display something SendImage(240, stream); // Send the customized screen res image if (screenSize <= 720) { SendImage(720, stream); } else if (screenSize <= 1080) { SendImage(1080, stream); } else if (screenSize <= 1440) { SendImage(1440, stream); } else if (screenSize <= 2160) { SendImage(2160, stream); } // Finally, send the full-screen image SendImage(-1, stream); } } void SendImage(int index, NetworkStream stream) { byte[] dataSize = new byte[] { (byte)(boardImages[index].Length >> 24), (byte)(boardImages[index].Length >> 16), (byte)(boardImages[index].Length >> 8), (byte)(boardImages[index].Length & 0xFF) }; stream.Write(dataSize); stream.Write(boardImages[index]); } static int currentTurn; void ServerUpdate() { // *************************** // ** Accept new connections** // *************************** while (!awaitingAdds.IsEmpty) { // try to dequeue a client if (awaitingAdds.TryDequeue(out TcpClient poppedClient)) { // TODO: // Check that the player isn't reconnecting Player newPlayer = new("", startRow, startColumn); Players.Add(newPlayer); clients.Add(poppedClient, newPlayer); Console.WriteLine("New player added."); } } // Create a list of clients that are no longer connected, so they may be closed at the end of this // loop ConcurrentQueue closedClients = new(); bool turnCompleted = false; // *************************** // ***** Recieve Data ***** // *************************** // Data are recieved in parallel, so that it may be processed quickly _ = Parallel.ForEach(clients, (item) => { Player player = item.Value; if (!item.Key.Connected) { // Client no longer connected, add to the removed list closedClients.Enqueue(item.Key); return; } else { // Game logic // If there are available bytes to be read, we can process them if (item.Key.Available > 0) { // Get number of bytes int availableBytes = item.Key.Available; // Store read bytes in an array byte[] receivedBytes = new byte[player.UnhandledBuffer.Length + availableBytes]; // Grab the stream to read from/write to NetworkStream clientStream = item.Key.GetStream(); // read the buffer to the end _ = clientStream.Read(receivedBytes, player.UnhandledBuffer.Length, availableBytes); // Copy the unhandled buffer to the recieved bytes player.UnhandledBuffer.CopyTo(receivedBytes, 0); // Reset the unhandled array player.UnhandledBuffer = Array.Empty(); int pos = 0; // While we still haven't reached the end of the buffer, read bytes while (pos < receivedBytes.Length) { if (pos == receivedBytes.Length - 1) { player.UnhandledBuffer = new byte[] { receivedBytes[^1] }; break; } int command = receivedBytes[pos] >> 4; // Get the four command bits if ((receivedBytes[pos++] & 0xF) != Players.IndexOf(item.Value)) { Console.WriteLine("Unexpected client ID: {0}", (receivedBytes[pos++] & 0xF)); } switch (command) { case 0: // Heartbeat from client {0x0, 0x1, 0xFF} // In case of buffer overflow, take the last command and paste it into // the unhandled buffer if (pos + 2 > receivedBytes.Length) { player.UnhandledBuffer = new byte[receivedBytes.Length - pos + 1]; for (int i = 0; i < receivedBytes.Length - pos + 1; i++) { player.UnhandledBuffer[i] = receivedBytes[(pos - 1) + i]; } // Finally, set the pos to the end of the buffer pos = receivedBytes.Length; } else { // Buffer won't overflow, process normally player.LastTime = DateTime.Now.Ticks; pos += 2; // Move two bytes } break; case 1: // Player sets name List strBytes = new(); bool endReached = false; while (pos < receivedBytes.Length && receivedBytes[pos] != 0xFF) { byte nextByte = receivedBytes[pos++]; if (pos != receivedBytes.Length - 1 && receivedBytes[pos + 1] == 0xFF) { endReached = true; } else { strBytes.Add(nextByte); } } if (endReached) { // Buffer contained full string, move to the next command player.PlayerName = Encoding.UTF8.GetString(strBytes.ToArray()); } else { player.UnhandledBuffer = new byte[receivedBytes.Length - strBytes.Count + 1]; for (int i = 0; i < receivedBytes.Length - strBytes.Count + 1; i++) { player.UnhandledBuffer[i] = receivedBytes[(strBytes.Count - 1) + i]; } // Pos is implicitely } break; case 2: // update player position if (Players[currentTurn].Equals(player)) { // It is our turn, move byte nextByte = receivedBytes[pos++]; int row = nextByte >> 4; int column = nextByte & 0xF; player.Column = column; player.Row = row; // Can't update turn until we are synchronised, else we would // inadvertently accept turns from the next player turnCompleted = true; } break; default: Console.WriteLine("Unknown client command"); break; } } } // If we haven't recieved data from this client for some time, send a packet that requests // a response else if (TimeSpan.FromTicks(DateTime.Now.Ticks - item.Value.LastTime) > TimeSpan.FromSeconds(5)) { item.Key.GetStream().Write(new byte[] { (byte)Players.IndexOf(item.Value), 0xFF }); // Heartbeat is 0x0, 0x1 Player curr = item.Value; curr.LastTime = DateTime.Now.Ticks; } } }); // Synchronised, update player turn if (turnCompleted) { currentTurn++; currentTurn %= Players.Count; } // *************************** // ** Remove old connections** // *************************** foreach (TcpClient item in closedClients) { _ = Players.Remove(clients[item]); _ = clients.Remove(item); } // *************************** // ******* Send Data ******* // *************************** IReadOnlyCollection dataPacket; { List allData = new(); allData.Add(1); // command bits; gets modulated when sent byte playersTurn = (byte)(Players.Count << 4 | currentTurn); // Player count & turn allData.Add(playersTurn); // Data about each player for (int i = 0; i < Players.Count; i++) { // Data begins with the UTF-8 formatted player-name string (and termination char) allData.AddRange(Encoding.UTF8.GetBytes(Players[i].PlayerName)); allData.Add(0xFE); // EoString // Next data byte is the row & column the player is in byte pos = (byte)(Players[i].Row << 4 | Players[i].Column); allData.Add(pos); // End of player is implicit } // Finally, terminate the command allData.Add(0xFF); dataPacket = allData.AsReadOnly(); } // Create a personalised data packet for each client (tells the client its order in the game) // Sent in parallel, to prevent a misbehaving connection from delaying everyone _ = Parallel.ForEach(clients, (x) => { Player item = x.Value; // Alert any further ops that there is a pending data send if (item.ObtainLock() == 0) { List clientPacket = new(dataPacket); clientPacket[0] = (byte)(clientPacket[0] << 4 | Players.IndexOf(item)); long attemptStart = DateTime.Now.Ticks; int attemptCount = 0; while (attemptCount < 10) { try { x.Key.GetStream().Write(clientPacket.ToArray(), 0, clientPacket.Count); break; } catch (IOException e) { Console.WriteLine(e); _ = Task.Delay(100); attemptCount++; } } // Reset the lock; item.ReleaseLock(); } else { // client hasn't recieved old data; avoid sending new state updates Console.WriteLine("Client is still recieving old data"); } }); } } }