diff --git a/PDGServer_WPF/App.xaml.cs b/PDGServer_WPF/App.xaml.cs
index c866abb..1c8b5ca 100644
--- a/PDGServer_WPF/App.xaml.cs
+++ b/PDGServer_WPF/App.xaml.cs
@@ -1,10 +1,15 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
+using System.IO;
using System.Linq;
+using System.Text;
using System.Threading.Tasks;
using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
namespace RBG_Server
{
@@ -13,5 +18,194 @@ namespace RBG_Server
///
public partial class App : Application
{
+ public GameServer BoardGameServer {get;}
+ public CommunicationHandler communicationHandler { get; private set;}
+ protected static readonly SolidColorBrush RED_BRUSH = new(Color.FromRgb(255, 0, 0));
+ protected static readonly SolidColorBrush BLUE_BRUSH = new(Color.FromRgb(0, 0, 255));
+ protected static readonly SolidColorBrush GREEN_BRUSH = new(Color.FromRgb(0, 255, 0));
+
+ ///
+ /// Loads the specified files from disk, creating mip maps as appropriate
+ ///
+ ///
+ ///
+ ///
+ void LoadFiles(string[] selectedFiles, bool isSprite=false, int spriteLimit=128)
+ {
+ // Continue in parallel
+ _ = Parallel.ForEach(selectedFiles, (file) =>
+ {
+ LoadFile(file);
+ });
+ }
+ ///
+ /// Loads a singular file from disk, generating Mip maps as required
+ ///
+ ///
+ void LoadFile(string path, bool isSprite=true)
+ {
+ // Check if the file is a .gif, first.
+ using FileStream fs = File.OpenRead(path);
+ LoadStream(fs, path, isSprite);
+
+ }
+
+ void LoadStream(Stream source, string path, bool isSprite)
+ {
+ long srcStart = source.Position;
+ byte[] vs = new byte[6]; // First 6 bytes is the gif specifier
+ source.Read(vs, 0, 6);
+ source.Position = srcStart;
+ string magicNumber = Encoding.ASCII.GetString(vs);
+ if (magicNumber == "GIF87a" || magicNumber == "GIF89a")
+ {
+ // Gif file, replace with a layered .png
+ GifBitmapDecoder decoder = new(source, BitmapCreateOptions.None, BitmapCacheOption.Default);
+ // Obtain each .gif frame
+ BitmapFrame[] frames = new BitmapFrame[decoder.Frames.Count];
+ decoder.Frames.CopyTo(frames, 0);
+ // Get frame sizes
+ int height = frames[0].PixelHeight;
+ int width = frames[0].PixelWidth;
+ // The lowest mip is static, using either the thumb or first frame
+ if (decoder.Thumbnail != null)
+ {
+ double scaleRate = 32 / (double)Math.Max(height, width);
+ MemoryStream ms = ScaleImage(decoder.Thumbnail, scaleRate);
+ _ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_low" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
+ }
+ else
+ {
+ double scaleRate = 32 / (double)Math.Max(height, width);
+ MemoryStream ms = ScaleImage(frames[0], scaleRate);
+ _ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_low" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
+ }
+ // The remaining mips are animated
+ if (width >= 128 || height >= 128)
+ {
+ double scaleRate = 128 / (double)Math.Max(height, width);
+ MemoryStream ms = ScaleAnimatedImage(frames, scaleRate);
+ _ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_med" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
+ }
+ if (width >= 512 || height >= 512)
+ {
+ double scaleRate = 512 / (double)Math.Max(height, width);
+ MemoryStream ms = ScaleAnimatedImage(frames, scaleRate);
+ _ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_high" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
+ }
+ if (width >= 512 || height >= 512)
+ {
+ // Just re-encode the gif to .png
+ PngBitmapEncoder encoder = new();
+ foreach (BitmapFrame frame in frames)
+ {
+ encoder.Frames.Add(frame);
+ }
+ MemoryStream ms = new();
+ encoder.Save(ms);
+ _ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_raw" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
+ }
+
+ }
+ else
+ {
+ // Regular image file
+ // Create the bitmap to get the current intrinsics
+ BitmapImage bitmapImage = new();
+ bitmapImage.BeginInit();
+ bitmapImage.StreamSource = source;
+ bitmapImage.CacheOption = BitmapCacheOption.None;
+ bitmapImage.EndInit();
+ int height = bitmapImage.PixelHeight;
+ int width = bitmapImage.PixelWidth;
+
+ // Behaviour depends on the required image
+ if (isSprite)
+ {
+ // Sprites should have a low mip-map, and the medium
+ // Low = 32*32
+ // Medium = 128*128
+ // High = 512*512 or image size if less than 1024
+ if (width >= 32 || height >= 32)
+ {
+ double scaleRate = 32 / (double)Math.Max(height, width);
+ MemoryStream ms = ScaleImage(bitmapImage, scaleRate);
+ _ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_low" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
+ }
+ if (width >= 128 || height >= 128)
+ {
+ double scaleRate = 128 / (double)Math.Max(height, width);
+ MemoryStream ms = ScaleImage(bitmapImage, scaleRate);
+ _ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_med" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
+ }
+ if (width >= 512 || height >= 512)
+ {
+ double scaleRate = 512 / (double)Math.Max(height, width);
+ MemoryStream ms = ScaleImage(bitmapImage, scaleRate);
+ _ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_high" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
+ }
+ if (width >= 512 || height >= 512)
+ {
+ MemoryStream ms = new();
+ source.CopyTo(ms);
+ _ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_raw" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
+ }
+ }
+ else
+ {
+ // Non-sprites should have a low mip-map, and the medium, but at a larger size than the sprites
+ // Low = 128*128
+ // Medium = 512*512
+ // High = 2048*2048
+ if (width >= 128 || height >= 128)
+ {
+ double scaleRate = 128 / (double)Math.Max(height, width);
+ MemoryStream ms = ScaleImage(bitmapImage, scaleRate);
+ _ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_low" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
+ }
+ if (width >= 512 || height >= 512)
+ {
+ double scaleRate = 512 / (double)Math.Max(height, width);
+ MemoryStream ms = ScaleImage(bitmapImage, scaleRate);
+ _ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_med" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
+ }
+ if (width >= 2048 || height >= 2048)
+ {
+ double scaleRate = 2048 / (double)Math.Max(height, width);
+ MemoryStream ms = ScaleImage(bitmapImage, scaleRate);
+ _ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_high" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
+ }
+ if (width >= 2048 || height >= 2048)
+ {
+ MemoryStream ms = new();
+ source.CopyTo(ms);
+ _ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_raw" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
+ }
+ }
+ }
+ }
+
+ MemoryStream ScaleImage(BitmapSource source, double scaleRate)
+ {
+ TransformedBitmap modified = new(source, new ScaleTransform(source.PixelWidth * scaleRate, source.PixelHeight * scaleRate));
+ PngBitmapEncoder encoder = new();
+ encoder.Frames.Add(BitmapFrame.Create(modified));
+ MemoryStream ms = new();
+ encoder.Save(ms);
+ return ms;
+ }
+
+ MemoryStream ScaleAnimatedImage(BitmapFrame[] frames, double scaleRate)
+ {
+ PngBitmapEncoder encoder = new();
+ foreach (BitmapFrame frame in frames)
+ {
+ TransformedBitmap modified = new(frame, new ScaleTransform(frame.PixelWidth * scaleRate, frame.PixelHeight * scaleRate));
+ encoder.Frames.Add(BitmapFrame.Create(modified));
+ }
+ MemoryStream ms = new();
+ encoder.Save(ms);
+ return ms;
+ }
}
}
diff --git a/RBG_Server.Core/CachedByteArray.cs b/RBG_Server.Core/CachedByteArray.cs
new file mode 100644
index 0000000..ca9090d
--- /dev/null
+++ b/RBG_Server.Core/CachedByteArray.cs
@@ -0,0 +1,240 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace RBG_Server
+{
+ ///
+ /// Stores data in either memory or disk, depending on the required behaviour of the system, namely, if the
+ /// byte array exceeds the size of the Memory Limit (objects can be required to stay in RAM)
+ ///
+ public struct CachedByteArray : IDisposable
+ {
+ // Static event, so that every instance of CachedByteArray is affected by a change in memory limit
+ private static event EventHandler MemoryLimitModifiedEvent;
+ // Properties to keep track of the actual allocated cache amounts
+ private static long memoryAllocationTotal = 0;
+ private static long memoryAllocationRAM = 0;
+ // static fields
+ private static int memoryLimit = 1024 * 1024 * 1; // Start with a memory limit of 1 MB
+
+ private static readonly string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Remote Board Game\\Cache Objects");
+ // static properties
+ public static long MemoryAllocationRAM { get { return memoryAllocationRAM; } }
+ public static long MemoryAllocationTotal { get { return memoryAllocationTotal; } }
+ public static long MemoryAllocationDisk { get { return memoryAllocationTotal - memoryAllocationRAM; } }
+
+ private SemaphoreSlim accessSem;
+
+ public bool IsInRam { get; private set; }
+
+ // struct data
+ private bool useOnlyRAM;
+
+ private Stream dataStream;
+
+ private string dataHashCode;
+ public bool UseOnlyRAM { get => useOnlyRAM; set => useOnlyRAM = value; }
+ // Properties from the array itself
+ public long Length => dataStream.Length;
+ ///
+ /// Set the memory limit for keeping data in RAM, as opposed to storing on disk.
+ /// Will raise events if modified, so all instances will be refactored to disk if appropriate
+ ///
+ public static int MemoryLimit
+ {
+ get
+ {
+ return memoryLimit;
+ }
+ set
+ {
+ memoryLimit = value;
+ MemoryLimitModifiedEvent?.Invoke(null, EventArgs.Empty);
+ }
+ }
+
+ public CachedByteArray(byte[] b) : this(b, false)
+ {
+ }
+
+ public CachedByteArray(byte[] b, bool useOnlyRAM)
+ {
+ accessSem = new SemaphoreSlim(0,1);
+ this.useOnlyRAM = useOnlyRAM;
+ dataHashCode = GenerateDataHash(b);
+ if (!useOnlyRAM && b.Length > MemoryLimit)
+ {
+ // Save to a file; load the data
+ string path = Path.Combine(folderPath, dataHashCode);
+ dataStream = CopyIntoFile(b, path);
+ IsInRam = false;
+ }
+ else
+ {
+ dataStream = new MemoryStream(b);
+ memoryAllocationRAM += dataStream.Length;
+ IsInRam = true;
+ }
+ dataStream.Position = 0;
+ memoryAllocationTotal += dataStream.Length;
+ // Signal the semaphore is ready
+ accessSem.Release();
+ // Listen to changes in the memory limit
+ MemoryLimitModifiedEvent += CachedByteArray_memoryLimitModifiedEvent;
+ }
+ ///
+ /// Generates a SHA256 hash of the data in the array, and returns it in a safe string format
+ ///
+ ///
+ ///
+ private static string GenerateDataHash(byte[] byteData)
+ {
+ using SHA256 dataHash = SHA256.Create();
+ byte[] hashCode = dataHash.ComputeHash(byteData);
+ string generatedHashCode = "";
+ foreach (byte hashByte in hashCode)
+ {
+ generatedHashCode += $"{hashByte:X2}";
+ }
+ return generatedHashCode;
+ }
+ ///
+ /// Copies the given into the file at , opening the stream, and returning it
+ ///
+ ///
+ ///
+ ///
+ private static Stream CopyIntoFile(byte[] byteData, string filePath)
+ {
+ FileStream s = File.OpenWrite(filePath);
+ foreach (byte b in byteData)
+ {
+ s.WriteByte(b);
+ }
+ return s;
+ }
+
+ private void CachedByteArray_memoryLimitModifiedEvent(object sender, EventArgs e)
+ {
+ if (!useOnlyRAM)
+ {
+ if (dataStream.Length > MemoryLimit && IsInRam)
+ {
+ // Memory stream exceeds the limit; create an FS and copy
+ accessSem.Wait();
+ // Get the underlying byte[]
+ byte[] dataBytes = ((MemoryStream)dataStream).ToArray();
+ // Generate the hashcode from the data
+ dataHashCode = GenerateDataHash(dataBytes);
+ // Create the file, and copy the data
+ dataStream = CopyIntoFile(dataBytes, Path.Combine(folderPath, dataHashCode));
+ // Disregard the allocation from RAM
+ memoryAllocationRAM -= dataStream.Length;
+ accessSem.Release();
+ }
+ else if (dataStream.Length <= MemoryLimit && !IsInRam)
+ {
+ // Data stream is no larger than the limit, but the file is saved on disk; load to RAM instead
+ // Obtain a lock on the stream
+ accessSem.Wait();
+ // Copy data to memory
+ dataStream.Position = 0;
+ MemoryStream ms = new();
+ dataStream.CopyTo(ms);
+ // Increase the amount noted in RAM
+ memoryAllocationRAM += ms.Length;
+ // Close the FileStream
+ dataStream.Close();
+ // Delete the file
+ DeleteFile(Path.Combine(folderPath, dataHashCode));
+ // Set the datastream to the memory location
+ dataStream = ms;
+ accessSem.Release();
+ // Check the loaded data matches the hashcode we already have
+ string loadedHashCode = GenerateDataHash(ms.ToArray());
+ if (loadedHashCode != dataHashCode)
+ {
+ throw new UnauthorizedAccessException("Data was modified");
+ }
+ }
+ // Otherwise, the data is already in the correct spot, so we don't need to do anything else
+ }
+ }
+ private static void DeleteFile(string filePath)
+ {
+ try
+ {
+ File.Delete(filePath);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex);
+ }
+ }
+ public static implicit operator byte[](CachedByteArray c)
+ {
+ byte[] dataBytes = new byte[c.dataStream.Length];
+ c.accessSem.Wait();
+ if (c.IsInRam)
+ {
+ // We can use the underlying array from the MemoryStream
+ dataBytes = (c.dataStream as MemoryStream).ToArray();
+ }
+ else
+ {
+ // Ensure we read from the beginning of the stream
+ c.dataStream.Position = 0;
+ c.dataStream.Read(dataBytes, 0, dataBytes.Length);
+ }
+ c.accessSem.Release();
+ return dataBytes;
+ }
+
+ public static explicit operator CachedByteArray(byte[] b) => new CachedByteArray(b);
+ ///
+ /// Array index operator, allows the CachedByteArray to act more completely as a byte[]
+ ///
+ ///
+ ///
+ public byte this[int index]
+ {
+ get
+ {
+ int b;
+ // Aquire the semaphore, so that the read is effectively atomic (ensures the read is performed on the expected data)
+ accessSem.Wait();
+ dataStream.Position = index;
+ b = dataStream.ReadByte();
+ accessSem.Release();
+ if (b == -1)
+ {
+ throw new IndexOutOfRangeException(index.ToString());
+ }
+ return (byte)b;
+ }
+ }
+
+ public void Dispose()
+ {
+ accessSem.Wait();
+ // Remove the event listener
+ MemoryLimitModifiedEvent -= CachedByteArray_memoryLimitModifiedEvent;
+ // Remove the allocation from the counter
+ memoryAllocationTotal -= dataStream.Length;
+ memoryAllocationRAM -= dataStream.Length;
+ // If the FileStream is opened, close and dispose it
+ dataStream?.Dispose();
+ // If the file is on disk, remove it
+ DeleteFile(Path.Combine(folderPath, dataHashCode));
+ // Release all resources obtained by the semaphore
+ accessSem.Dispose();
+ }
+ }
+}
diff --git a/RBG_Server.Core/CommunicationHandler.cs b/RBG_Server.Core/CommunicationHandler.cs
new file mode 100644
index 0000000..92fd9b8
--- /dev/null
+++ b/RBG_Server.Core/CommunicationHandler.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+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
+ {
+
+ ///
+ /// 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
+ ///
+ public ConcurrentDictionary ImageCollection { get; } = new();
+ }
+}