using NATUPNPLib; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace RBG_Server { /// /// Contains all communication data, from both the client's and server's perspective (should be able to switch between each mode as necessary) /// /// public class CommunicationHandler { UPnPNAT upnpnat = new(); IStaticPortMapping portMapping; /// /// Image data is stored in memory in a dictionary collection. Each byte[] represents an image file, compressed according to its file type; /// which helps save space in memory. Uses a CachedByteArray; essentially a normal byte array but automatically stored on disk if it is larger /// than a set size. /// This limit can be changed at any time, if memory is required to be freed /// public ConcurrentDictionary ImageCollection { get; } = new(); public Dictionary Players { get; } = new Dictionary(); public Player Self { get;set; } public List ImageList { get; } = new(); public string BoardName { get; set; } public IPAddress IpAddress { get; set; } public short Port { get; set; } private TcpClient stateRetriever { get; set; } private TcpClient dataRetriever { get; set; } public int ColumnCount { get;private set; } public int RowCount { get;private set; } public int ColumnZoomStart { get;private set; } public int RowZoomStart { get;private set; } public int ColumnZoomSpan { get;private set; } public int RowZoomSpan { get;private set; } public int StartingColumn { get;private set; } public int StartingRow { get;private set; } public void InitialiseServer(int rowCount, int colCount, int zoomRowStart, int zoomColStart, int zoomRowSpan, int zoomColSpan, int startingRow, int startingColumn) { // Find the server's active (reliable) network adapter, by creating a remote connection and retrieving our IP from it: using (Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) { s.Bind(new IPEndPoint(IPAddress.Any, 0)); s.Connect("microsoft.com", 0); // The IP is implicitly the one assigned to the interface the OS would use to connect to the remote address IpAddress = (s.LocalEndPoint as IPEndPoint).Address; } // Create the upnp mapping upnpnat.StaticPortMappingCollection.Add(Port, "TCP", Port, IpAddress.ToString(), true, "RBGServer"); TcpListener listener = new(IPAddress.Any, Port); // Allow local comms listener.Start(); while (true) { TcpClient client = listener.AcceptTcpClient(); // Delegate to another thread _ = Task.Run(() => { AcceptConnections(client); }); } } /// /// Clears all sprites, but not the board image, from the collection; disposing data as required /// public void ClearSprites() { // Remove all existing except the board image foreach (string image in ImageList) { if (image != BoardName) { ImageCollection.Remove(image, out CachedByteArray removed); removed.Dispose(); } } // Reset the image list ImageList.Clear(); ImageList.Add(BoardName); } private void AcceptConnections(TcpClient client) { // Handle actual connections here throw new NotImplementedException(); } public void InitialiseClient(TcpClient client) { Progress gameProgress = new Progress(); Progress dataProgress = new Progress(); Task dataLoader = InitDataLoader(dataRetriever.GetStream(), dataProgress); Task stateLoader = InitDataLoader(stateRetriever.GetStream(), gameProgress); // Data loader starts first - we're waiting for the thumbs to load so the player can choose thier dataLoader.Start(); // State stateLoader.Start(); } public struct ProgressData { /// /// Current activity being processed /// public enum Activity { Idle, MessageSent, MessageReceived, ProcessingMessage, MessageProcessed, CollectionRecieved, // ----- Image stuffs ImageDownloaded, // ----- Gameplay stuffs Finished, } public Activity CurrentActivity; public string Message; public int Progress; } /// /// Initialises data loader /// /// /// /// public async Task InitDataLoader(NetworkStream dataStream, IProgress progressUpdates) { byte[] buffer = new byte[] { 2, 0, 0, 0, 128, 0 }; // Get image collection names dataStream.Write(buffer, 0, buffer.Length); // Notify Data was sent progressUpdates.Report(new ProgressData() { CurrentActivity = ProgressData.Activity.MessageSent, Progress = 0, }); var dataResponse = await GetResponse(dataStream, 4); // Notify Data was recieved progressUpdates.Report(new ProgressData() { CurrentActivity = ProgressData.Activity.MessageReceived, Progress = 1, }); dataResponse = await GetResponse(dataStream, GetInt32(dataResponse)); List data = new List(); int start = 0; while (start < dataResponse.Length) { int pos = start; while (dataResponse[pos++] != 0) ; ImageList.Add(Encoding.UTF8.GetString(dataResponse[start..pos])); start = pos; } // Load all low-resolution images for (int i = 0; i < ImageList.Count; i++) { string item = ImageList[i]; byte[] strBytes = Encoding.ASCII.GetBytes(item); buffer = new byte[strBytes.Length + 6]; byte[] lenBytes = GetBytes(strBytes.Length + 6); lenBytes.CopyTo(buffer, 0); buffer[4] = 129; // Download image buffer[5] = 0; // mip_low strBytes.CopyTo(buffer, 6); dataStream.Write(buffer, 0, buffer.Length); // Read the length, then the data dataResponse = await GetResponse(dataStream); dataResponse = await GetResponse(dataStream, GetInt32(dataResponse)); ImageCollection.TryAdd(item + "_mip_low", (CachedByteArray)dataResponse); progressUpdates.Report(new ProgressData() { CurrentActivity = ProgressData.Activity.MessageReceived, Message = item, Progress = i, }); } } private async Task InitGameLoader(NetworkStream stateStream, IProgress progressUpdates) { // Coomunication has already been opened with the server; must ID byte[] buffer = new byte[] { 1, 0, 0, 0, 1 }; stateStream.Write(buffer, 0, buffer.Length); // Response size var stateResponse = await GetResponse(stateStream, 4); // Response data stateResponse = await GetResponse(stateStream, GetInt32(stateResponse)); // Get the full response data // Board state data int playerID = GetInt32(stateResponse[..4]); ColumnCount = GetInt32(stateResponse[..8]); RowCount = GetInt32(stateResponse[8..12]); ColumnZoomStart = GetInt32(stateResponse[12..16]); RowZoomStart = GetInt32(stateResponse[16..20]); ColumnZoomSpan = GetInt32(stateResponse[20..24]); RowZoomSpan = GetInt32(stateResponse[24..28]); StartingColumn = GetInt32(stateResponse[28..32]); StartingRow = GetInt32(stateResponse[32..36]); // Basic board data loaded; fetch players buffer = new byte[] { 1, 0, 0, 0, 2 }; stateStream.Write(buffer, 0, buffer.Length); stateResponse = await GetResponse(stateStream, 4); stateResponse = await GetResponse(stateStream, GetInt32(stateResponse)); // state response contains a player list; // Player ID (Int32) // Player hash length: // Player 'Unique' Hash // Player Name (null-terminated string) // Player Sprite (null-terminated string) // Player Column (Int32) // Player Row (Int32) int start = 0; while (start < stateResponse.Length) { int pos = start + 4; int responsePlayerID = GetInt32(stateResponse[start..pos]); start = pos; pos += 4; int length = GetInt32(stateResponse[start..pos]); start = pos; pos += length; byte[] playerHash = stateResponse[start..pos]; start = pos; while (stateResponse[pos++] != 0) ; // skip the bytes that aren't null string responsePlayerName = Encoding.UTF8.GetString(stateResponse[start..pos]); start = pos; while (stateResponse[pos++] != 0) ; // skip the bytes that aren't null string responsePlayerSprite = Encoding.UTF8.GetString(stateResponse[start..pos]); start = pos; pos += 4; int playerColumn = GetInt32(stateResponse[start..pos]); start = pos; pos += 4; int playerRow = GetInt32(stateResponse[start..pos]); start = pos; Player player = new Player(responsePlayerName, responsePlayerSprite, playerHash, playerRow, playerColumn); Players.Add(responsePlayerID, player); // We build a reference of response ID --> Player, and the players are ID'd by the hash. We can use this to modify } } /// /// Idea is that each response is prefaced by a 4 byte stream length specifier. /// This requires a busy wait to achieve, if not all recieved at once. /// We retrieve the /// /// /// /// private async Task GetResponse(NetworkStream stream, int targetLength=4) { if (stream.CanRead) { byte[] buffer = new byte[targetLength]; int numberOfBytesRead = 0; // Incoming message may be larger than the buffer size. do { numberOfBytesRead += await stream.ReadAsync(buffer.AsMemory(numberOfBytesRead, targetLength - numberOfBytesRead)); } while (numberOfBytesRead < targetLength); return buffer; } return Array.Empty(); } static internal byte[] GetBytes(int i) { byte[] bytes = BitConverter.GetBytes(i); if (BitConverter.IsLittleEndian) { // We need data in big-endian format; so reverse the array Array.Reverse(bytes); } return bytes; } static internal int GetInt32(byte[] bytes) { if (BitConverter.IsLittleEndian) { // We need data in little-endian format; so reverse the array Array.Reverse(bytes); } return BitConverter.ToInt32(bytes, 0); } } }