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 /// public ConcurrentDictionary ImageCollection { get; } = new(); 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() { // 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); }); } // At this point, the ImageCollection will have already been initialised. // The name of the board will also be set to {0}.. // Server logic, such as accepting players and maintaining communication goes in here } private void AcceptConnections(TcpClient client) { throw new NotImplementedException(); } public void InitialiseClient(TcpClient client) { // At this point, no details about the game are loaded; we must load them from the server (to which we have already connected [no data should have been sent]). NetworkStream stateStream = stateRetriever.GetStream(); Task stateLoader = new Task(async () => { // Get game board details 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 ColumnCount = GetInt32(stateResponse[..4]); RowCount = GetInt32(stateResponse[4..8]); ColumnZoomStart = GetInt32(stateResponse[8..12]); RowZoomStart = GetInt32(stateResponse[12..16]); ColumnZoomSpan = GetInt32(stateResponse[16..20]); RowZoomSpan = GetInt32(stateResponse[20..24]); StartingColumn = GetInt32(stateResponse[24..28]); StartingRow = GetInt32(stateResponse[28..32]); // 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 }); NetworkStream dataStream = dataRetriever.GetStream(); Task dataLoader = new Task(async () => { byte[] buffer = new byte[] { 2, 0, 0, 0, 128, 0 }; // Get image collection names dataStream.Write(buffer, 0, buffer.Length); var dataResponse = await GetResponse(stateStream, 4); dataResponse = await GetResponse(dataStream, GetInt32(dataResponse)); List data = new List(); for (int i = 0; i < dataResponse.Length; i++) { if (dataResponse[i] != 0) { data.Add(dataResponse[i]); } else { ImageList.Add(Encoding.UTF8.GetString(data.ToArray())); data.Clear(); } } // Load all low-resolution images foreach (string item in ImageList) { 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); } // At this point, the minimal amount of work required by the data thread has been done (load all thumbs) // When an asset is needed from here, queue a load }); dataLoader.Start(); stateLoader.Start(); byte[] buffer = new byte[] {0,0,0,1,1}; // Writing to the stream is to be considered near constant-time, but reading is non-constant. // This application model must be synchronous, but we execute other commands before expecting our response to have arrived (it can complete at any time in that period) stateStream.Write(buffer, 0, buffer.Length); buffer[0] = 1; buffer[3] = 0; dataStream.Write(buffer, 0, buffer.Length); // A details request is [, ] // The response is about the same format: var stateResponse = GetResponse(stateStream, 4); var dataResponse = GetResponse(dataStream, 4); // First, load the board state (low mip-map, row definitions, column definitions, zoom position etc.) // Retrieval command for the board // Then load the player list, and use the low mip for their sprites // Then check each loaded image and load the med, then large, then full sprite // Then load the low of each unused image (for quick retrieval) } /// /// 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]; StringBuilder myCompleteMessage = new StringBuilder(); int numberOfBytesRead = 0; // Incoming message may be larger than the buffer size. do { numberOfBytesRead += await stream.ReadAsync(buffer, 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); } } }