2021-10-16 20:01:49 +13:00
using NATUPNPLib ;
using System ;
2021-10-16 16:21:46 +13:00
using System.Collections.Concurrent ;
using System.Collections.Generic ;
using System.Linq ;
2021-10-16 20:01:49 +13:00
using System.Net ;
using System.Net.Sockets ;
2021-10-16 16:21:46 +13:00
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>
2021-10-16 20:01:49 +13:00
///
2021-10-16 16:21:46 +13:00
public class CommunicationHandler
{
2021-10-16 20:01:49 +13:00
UPnPNAT upnpnat = new ( ) ;
IStaticPortMapping portMapping ;
2021-10-16 16:21:46 +13:00
2021-10-16 20:01:49 +13:00
2021-10-16 16:21:46 +13:00
/// <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.
2021-10-17 12:46:12 +13:00
/// This limit can be changed at any time, if memory is required to be freed
2021-10-16 16:21:46 +13:00
/// </summary>
public ConcurrentDictionary < string , CachedByteArray > ImageCollection { get ; } = new ( ) ;
2021-10-16 20:44:20 +13:00
public List < Player > Players { get ; } = new List < Player > ( ) ;
2021-10-17 12:46:12 +13:00
2021-10-16 20:01:49 +13:00
public List < string > ImageList { get ; } = new ( ) ;
2021-10-17 12:46:12 +13:00
2021-10-16 20:01:49 +13:00
public string BoardName { get ; set ; }
public IPAddress IpAddress { get ; set ; }
2021-10-17 12:46:12 +13:00
2021-10-16 20:01:49 +13:00
public short Port { get ; set ; }
private TcpClient stateRetriever { get ; set ; }
2021-10-17 12:46:12 +13:00
2021-10-16 20:01:49 +13:00
private TcpClient dataRetriever { get ; set ; }
public int ColumnCount { get ; private set ; }
2021-10-17 12:46:12 +13:00
2021-10-16 20:01:49 +13:00
public int RowCount { get ; private set ; }
2021-10-17 12:46:12 +13:00
2021-10-16 20:01:49 +13:00
public int ColumnZoomStart { get ; private set ; }
2021-10-17 12:46:12 +13:00
2021-10-16 20:01:49 +13:00
public int RowZoomStart { get ; private set ; }
2021-10-17 12:46:12 +13:00
2021-10-16 20:01:49 +13:00
public int ColumnZoomSpan { get ; private set ; }
2021-10-17 12:46:12 +13:00
2021-10-16 20:01:49 +13:00
public int RowZoomSpan { get ; private set ; }
2021-10-17 12:46:12 +13:00
2021-10-16 20:01:49 +13:00
public int StartingColumn { get ; private set ; }
2021-10-17 12:46:12 +13:00
2021-10-16 20:01:49 +13:00
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 )
{
2021-10-17 12:46:12 +13:00
// Handle actual connections here
2021-10-16 20:01:49 +13:00
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 ( ) = >
{
2021-10-17 12:46:12 +13:00
2021-10-16 20:01:49 +13:00
} ) ;
NetworkStream dataStream = dataRetriever . GetStream ( ) ;
Task dataLoader = new Task ( async ( ) = >
{
2021-10-17 12:46:12 +13:00
2021-10-16 20:01:49 +13:00
// 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
} ) ;
2021-10-17 12:46:12 +13:00
// start the tasks; they should have
2021-10-16 20:01:49 +13:00
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 [<uint32, length>, <byte command>]
// 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)
}
2021-10-17 12:46:12 +13:00
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 int Progress ;
}
/// <summary>
/// Initialises data loader
/// </summary>
/// <param name="dataStream"></param>
/// <param name="progressUpdates"></param>
/// <returns></returns>
private 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 ,
Progress = i ,
} ) ;
}
}
private async Task InitGameLoader ( NetworkStream stateStream , IProgress < ProgressData > 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 ) ;
}
}
2021-10-16 20:01:49 +13:00
/// <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
{
2021-10-16 20:44:20 +13:00
numberOfBytesRead + = await stream . ReadAsync ( buffer . AsMemory ( numberOfBytesRead , targetLength - numberOfBytesRead ) ) ;
2021-10-16 20:01:49 +13:00
}
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 ) ;
}
2021-10-16 16:21:46 +13:00
}
}