429 lines
20 KiB
C#
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(byte startRow, byte startColumn, byte rowSize, byte columnSize, byte zoomRow, byte zoomCol, byte zoomRowSpan, byte zoomColSpan, byte[] boardImage)
|
|
{
|
|
this.startRow = startRow;
|
|
this.startColumn = startColumn;
|
|
this.rowSize = rowSize;
|
|
this.columnSize = columnSize;
|
|
this.zoomRow = zoomRow;
|
|
this.zoomCol = zoomCol;
|
|
this.zoomRowSpan = zoomRowSpan;
|
|
this.zoomColSpan = zoomColSpan;
|
|
BitmapImage gameBoardImage = new();
|
|
gameBoardImage.StreamSource = new MemoryStream(boardImage, false);
|
|
// 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;
|
|
|
|
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 (TcpClient 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");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|