429 lines
20 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace RBG_Server
{
public class GameServer
{
private readonly byte startRow, startColumn, rowSize, columnSize, zoomRow, zoomCol, zoomRowSpan, zoomColSpan;
private readonly Dictionary<int, byte[]> boardImages;
/// <summary>
/// Initialise a new server, with the provided starting row & column
///
/// </summary>
/// <param name="startRow">The starting row of the board</param>
/// <param name="startColumn">The starting column of the board</param>
/// <param name="boardImage">The full-scale board image</param>
public GameServer(int startRow, int startColumn, int rowSize, int columnSize, int zoomRow, int zoomCol, int zoomRowSpan, int zoomColSpan, Stream boardImage)
{
this.startRow = (byte)startRow;
this.startColumn = (byte)startColumn;
this.rowSize = (byte)rowSize;
this.columnSize = (byte)columnSize;
this.zoomRow = (byte)zoomRow;
this.zoomCol = (byte)zoomCol;
this.zoomRowSpan = (byte)zoomRowSpan;
this.zoomColSpan = (byte)zoomColSpan;
BitmapImage gameBoardImage = new();
gameBoardImage.StreamSource = boardImage;
// Hold images
Dictionary<int, byte[]> sourceImages = new();
// Create a JPEG encoder
JpegBitmapEncoder encoder = new()
{
QualityLevel = 100,
};
// Add the base frame to the encoder and retrieve its byte array
encoder.Frames.Add(BitmapFrame.Create(gameBoardImage));
using (MemoryStream stream = new())
{
encoder.Save(stream);
byte[] frameData = stream.ToArray();
sourceImages.Add(-1, frameData);
encoder.Frames.Clear();
}
// Add a resized version for each "common" scale
if (gameBoardImage.PixelHeight > 2160)
{
double rate = 2160 / gameBoardImage.PixelHeight;
TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate));
encoder.Frames.Add(BitmapFrame.Create(modified));
using MemoryStream stream = new();
encoder.Save(stream);
byte[] frameData = stream.ToArray();
sourceImages.Add(2160, frameData);
encoder.Frames.Clear();
}
if (gameBoardImage.PixelHeight > 1440)
{
double rate = 1440 / gameBoardImage.PixelHeight;
TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate));
encoder.Frames.Add(BitmapFrame.Create(modified));
using MemoryStream stream = new();
encoder.Save(stream);
byte[] frameData = stream.ToArray();
sourceImages.Add(1440, frameData);
encoder.Frames.Clear();
}
if (gameBoardImage.PixelHeight > 1080)
{
double rate = 1080 / gameBoardImage.PixelHeight;
TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate));
encoder.Frames.Add(BitmapFrame.Create(modified));
using MemoryStream stream = new();
encoder.Save(stream);
byte[] frameData = stream.ToArray();
sourceImages.Add(1080, frameData);
encoder.Frames.Clear();
}
if (gameBoardImage.PixelHeight > 720)
{
double rate = 720 / gameBoardImage.PixelHeight;
TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate));
encoder.Frames.Add(BitmapFrame.Create(modified));
using MemoryStream stream = new();
encoder.Save(stream);
byte[] frameData = stream.ToArray();
sourceImages.Add(720, frameData);
encoder.Frames.Clear();
}
// Send a 240p thumbnail
{
double rate = 240 / gameBoardImage.PixelHeight;
TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate));
encoder.Frames.Add(BitmapFrame.Create(modified));
using MemoryStream stream = new();
encoder.Save(stream);
byte[] frameData = stream.ToArray();
sourceImages.Add(240, frameData);
encoder.Frames.Clear();
}
// add the board images to the
boardImages = sourceImages;
}
public List<Player> Players { get; } = new();
// Map players to connections/clients
Dictionary<TcpClient, Player> clients = new();
// Clients that have connected, but are not yet added to the list
ConcurrentQueue<TcpClient> awaitingAdds = new();
void Init(IPAddress localAddr, int port)
{
TcpListener listener = new(localAddr, port);
listener.Start();
while (true)
{
TcpClient client = listener.AcceptTcpClient();
// Delegate to another thread
_ = Task.Run(() =>
{
AcceptConnections(client);
});
}
}
/// <summary>
/// Accept the client, and do the preprocessing
/// </summary>
/// <param name="client"></param>
void AcceptConnections(TcpClient client)
{
// Ensure we send data immediately
client.NoDelay = true;
// In the case the client
NetworkStream stream = client.GetStream();
// Await the mode switch data from the client
int command;
do
{
command = stream.ReadByte();
break;
}
while (stream.DataAvailable);
if (command == 0xFD)
{
// Connect to game; send request
}
else if (command == 0xFC)
{
// Next byte contains the screen size
int screenSize = stream.ReadByte() << 8 | stream.ReadByte();
// Send three images, the low res, and the screen res, and the full res (in that order, so the client may display something
SendImage(240, stream);
// Send the customized screen res image
if (screenSize <= 720)
{
SendImage(720, stream);
}
else if (screenSize <= 1080)
{
SendImage(1080, stream);
}
else if (screenSize <= 1440)
{
SendImage(1440, stream);
}
else if (screenSize <= 2160)
{
SendImage(2160, stream);
}
// Finally, send the full-screen image
SendImage(-1, stream);
}
}
void SendImage(int index, NetworkStream stream)
{
byte[] dataSize = new byte[] { (byte)(boardImages[index].Length >> 24), (byte)(boardImages[index].Length >> 16), (byte)(boardImages[index].Length >> 8), (byte)(boardImages[index].Length & 0xFF) };
stream.Write(dataSize);
stream.Write(boardImages[index]);
}
static int currentTurn = 0;
void ServerUpdate()
{
// ***************************
// ** Accept new connections**
// ***************************
while (!awaitingAdds.IsEmpty)
{
// try to dequeue a client
if (awaitingAdds.TryDequeue(out TcpClient poppedClient))
{
// TODO:
// Check that the player isn't reconnecting
Player newPlayer = new("<loading>", startRow, startColumn);
Players.Add(newPlayer);
clients.Add(poppedClient, newPlayer);
Console.WriteLine("New player added.");
}
}
// Create a list of clients that are no longer connected, so they may be closed at the end of this
// loop
ConcurrentQueue<TcpClient> closedClients = new();
bool turnCompleted = false;
// ***************************
// ***** Recieve Data *****
// ***************************
// Data are recieved in parallel, so that it may be processed quickly
Parallel.ForEach(clients, (item) =>
{
Player player = item.Value;
if (!item.Key.Connected)
{
// Client no longer connected, add to the removed list
closedClients.Enqueue(item.Key);
return;
}
else
{
// Game logic
// If there are available bytes to be read, we can process them
if (item.Key.Available > 0)
{
// Get number of bytes
int availableBytes = item.Key.Available;
// Store read bytes in an array
byte[] receivedBytes = new byte[player.UnhandledBuffer.Length + availableBytes];
// Grab the stream to read from/write to
NetworkStream clientStream = item.Key.GetStream();
// read the buffer to the end
clientStream.Read(receivedBytes, player.UnhandledBuffer.Length, availableBytes);
// Copy the unhandled buffer to the recieved bytes
player.UnhandledBuffer.CopyTo(receivedBytes, 0);
// Reset the unhandled array
player.UnhandledBuffer = Array.Empty<byte>();
int pos = 0;
// While we still haven't reached the end of the buffer, read bytes
while (pos < receivedBytes.Length)
{
if (pos == receivedBytes.Length - 1)
{
player.UnhandledBuffer = new byte[] { receivedBytes[^1] };
break;
}
int command = receivedBytes[pos] >> 4; // Get the four command bits
if ((receivedBytes[pos++] & 0xF) != Players.IndexOf(item.Value))
{
Console.WriteLine("Unexpected client ID: {0}", (receivedBytes[pos++] & 0xF));
}
switch (command)
{
case 0: // Heartbeat from client {0x0, 0x1, 0xFF}
// In case of buffer overflow, take the last command and paste it into
// the unhandled buffer
if (pos + 2 > receivedBytes.Length)
{
player.UnhandledBuffer = new byte[receivedBytes.Length - pos + 1];
for (int i = 0; i < receivedBytes.Length - pos + 1; i++)
{
player.UnhandledBuffer[i] = receivedBytes[(pos - 1) + i];
}
// Finally, set the pos to the end of the buffer
pos = receivedBytes.Length;
}
else
{
// Buffer won't overflow, process normally
player.LastTime = DateTime.Now.Ticks;
pos += 2; // Move two bytes
}
break;
case 1: // Player sets name
List<byte> strBytes = new();
bool endReached = false;
while (pos < receivedBytes.Length && receivedBytes[pos] != 0xFF)
{
byte nextByte = receivedBytes[pos++];
if (pos != receivedBytes.Length - 1 && receivedBytes[pos + 1] == 0xFF)
{
endReached = true;
}
else
{
strBytes.Add(nextByte);
}
}
if (endReached)
{
// Buffer contained full string, move to the next command
player.PlayerName = Encoding.UTF8.GetString(strBytes.ToArray());
}
else
{
player.UnhandledBuffer = new byte[receivedBytes.Length - strBytes.Count + 1];
for (int i = 0; i < receivedBytes.Length - strBytes.Count + 1; i++)
{
player.UnhandledBuffer[i] = receivedBytes[(strBytes.Count - 1) + i];
}
// Pos is implicitely
}
break;
case 2: // update player position
if (Players[currentTurn].Equals(player))
{
// It is our turn, move
byte nextByte = receivedBytes[pos++];
int row = nextByte >> 4;
int column = nextByte & 0xF;
player.Column = column;
player.Row = row;
// Can't update turn until we are synchronised, else we would
// inadvertently accept turns from the next player
turnCompleted = true;
}
break;
default:
Console.WriteLine("Unknown client command");
break;
}
}
}
// If we haven't recieved data from this client for some time, send a packet that requests
// a response
else if (TimeSpan.FromTicks(DateTime.Now.Ticks - item.Value.LastTime) > TimeSpan.FromSeconds(5))
{
item.Key.GetStream().Write(new byte[] { (byte)(Players.IndexOf(item.Value)), 0xFF }); // Heartbeat is 0x0, 0x1
Player curr = item.Value;
curr.LastTime = DateTime.Now.Ticks;
}
}
});
// Synchronised, update player turn
if (turnCompleted)
{
currentTurn++;
currentTurn %= Players.Count;
}
// ***************************
// ** Remove old connections**
// ***************************
foreach (var item in closedClients)
{
_ = Players.Remove(clients[item]);
_ = clients.Remove(item);
}
// ***************************
// ******* Send Data *******
// ***************************
IReadOnlyCollection<byte> dataPacket;
{
List<byte> allData = new();
allData.Add(1); // command bits; gets modulated when sent
byte playersTurn = (byte)(Players.Count << 4 | currentTurn); // Player count & turn
allData.Add(playersTurn);
// Data about each player
for (int i = 0; i < Players.Count; i++)
{
// Data begins with the UTF-8 formatted player-name string (and termination char)
allData.AddRange(Encoding.UTF8.GetBytes(Players[i].PlayerName));
allData.Add(0xFE); // EoString
// Next data byte is the row & column the player is in
byte pos = (byte)(Players[i].Row << 4 | Players[i].Column);
allData.Add(pos);
// End of player is implicit
}
// Finally, terminate the command
allData.Add(0xFF);
dataPacket = allData.AsReadOnly();
}
// Create a personalised data packet for each client (tells the client its order in the game)
// Sent in parallel, to prevent a misbehaving connection from delaying everyone
_ = Parallel.ForEach(clients, (x) =>
{
Player item = x.Value;
// Alert any further ops that there is a pending data send
if (item.ObtainLock() == 0)
{
List<byte> clientPacket = new(dataPacket);
clientPacket[0] = (byte)(clientPacket[0] << 4 | Players.IndexOf(item));
long attemptStart = DateTime.Now.Ticks;
int attemptCount = 0;
while (attemptCount < 10)
{
try
{
x.Key.GetStream().Write(clientPacket.ToArray(), 0, clientPacket.Count);
break;
}
catch (IOException e)
{
Console.WriteLine(e);
_ = Task.Delay(100);
attemptCount++;
}
}
// Reset the lock;
item.ReleaseLock();
}
else
{
// client hasn't recieved old data; avoid sending new state updates
Console.WriteLine("Client is still recieving old data");
}
});
}
}
}