Remote_Board_Game/RBG_Server.Core/CommunicationHandler.cs

322 lines
13 KiB
C#

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
{
/// <summary>
/// Contains all communication data, from both the client's and server's perspective (should be able to switch between each mode as necessary)
/// </summary>
///
public class CommunicationHandler
{
UPnPNAT upnpnat = new();
IStaticPortMapping portMapping;
/// <summary>
/// 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
/// </summary>
public ConcurrentDictionary<string, CachedByteArray> ImageCollection { get; } = new();
public Dictionary<int,Player> Players { get; } = new Dictionary<int, Player>();
public Player Self { get;set; }
public List<string> 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);
});
}
}
/// <summary>
/// Clears all sprites, but not the board image, from the collection; disposing data as required
/// </summary>
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<ProgressData> gameProgress = new Progress<ProgressData>();
Progress<ProgressData> dataProgress = new Progress<ProgressData>();
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
{
/// <summary>
/// Current activity being processed
/// </summary>
public enum Activity
{
Idle,
MessageSent,
MessageReceived,
ProcessingMessage,
MessageProcessed,
CollectionRecieved,
// ----- Image stuffs
ImageDownloaded,
// ----- Gameplay stuffs
Finished,
}
public Activity CurrentActivity;
public string Message;
public int Progress;
}
/// <summary>
/// Initialises data loader
/// </summary>
/// <param name="dataStream"></param>
/// <param name="progressUpdates"></param>
/// <returns></returns>
public async Task InitDataLoader(NetworkStream dataStream, IProgress<ProgressData> 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<byte> data = new List<byte>();
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<ProgressData> 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
}
}
/// <summary>
/// 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
/// </summary>
/// <param name="stream"></param>
/// <param name="targetLength"></param>
/// <returns></returns>
private async Task<byte[]> 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<byte>();
}
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);
}
}
}