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 List Players { get; } = new List(); 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); }); } } 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); dataLoader.Start(); stateLoader.Start(); } 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 int Progress; } /// /// Initialises data loader /// /// /// /// private 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, Progress = i, }); } } private async Task InitGameLoader(NetworkStream stateStream, IProgress progressUpdates) { // 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 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 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; 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, playerRow, playerColumn); Players.Add(player); } } /// /// 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); } } }