From 77ce5b4d7144448c27b23739e71da3fca522de65 Mon Sep 17 00:00:00 2001
From: Starbeamrainbowlabs <sbrl@starbeamrainbowlabs.com>
Date: Sat, 25 Mar 2017 19:49:44 +0000
Subject: [PATCH] [server] Continue wiring the ripplespace to nibri client,
 mainly by implementing the ChunkUpdateRequest message handler and all it's
 reuqired backend methods & classes.

---
 Nibriboard/Client/ChunkCache.cs               | 93 +++++++++++++++++++
 .../Messages/ChunkUpdateRequestMessage.cs     | 19 ++++
 Nibriboard/Client/NibriClient.cs              | 32 ++++++-
 Nibriboard/Nibriboard.csproj                  |  3 +
 Nibriboard/RippleSpace/LocationReference.cs   |  5 +-
 Nibriboard/RippleSpace/Plane.cs               | 12 +++
 Nibriboard/Utilities/ChunkTools.cs            | 45 +++++++++
 Nibriboard/Utilities/Rectangle.cs             | 48 ++++++++--
 8 files changed, 244 insertions(+), 13 deletions(-)
 create mode 100644 Nibriboard/Client/ChunkCache.cs
 create mode 100644 Nibriboard/Client/Messages/ChunkUpdateRequestMessage.cs
 create mode 100644 Nibriboard/Utilities/ChunkTools.cs

diff --git a/Nibriboard/Client/ChunkCache.cs b/Nibriboard/Client/ChunkCache.cs
new file mode 100644
index 0000000..b9a57b2
--- /dev/null
+++ b/Nibriboard/Client/ChunkCache.cs
@@ -0,0 +1,93 @@
+﻿using System;
+using System.Collections.Generic;
+using Nibriboard.RippleSpace;
+
+namespace Nibriboard.Client
+{
+	/// <summary>
+	/// Represents a cache of chunk references. Useful for keeping track which chunks
+	/// a remote party is currently keeping in memory.
+	/// </summary>
+	public class ChunkCache
+	{
+		List<ChunkReference> cache;
+
+		/// <summary>
+		/// Creates a new empty chunk cache.
+		/// </summary>
+		public ChunkCache()
+		{
+		}
+
+		/// <summary>
+		/// Adds a chunk reference to the cache.
+		/// If the chunk is already in the cache, then it won't be added again.
+		/// </summary>
+		/// <param name="chunkRef">The chunk reference to add.</param>
+		public void Add(ChunkReference chunkRef)
+		{
+			// If this cache already contains the specified chunk reference, then we
+			// probably shouldn't add it to the cache twice
+			if(cache.Contains(chunkRef))
+				return;
+
+			cache.Add(chunkRef);
+		}
+
+		/// <summary>
+		/// Adds the given chunk references to the cache.
+		/// Quietly skips over duplicate chunk references.
+		/// </summary>
+		/// <param name="chunkRefs">The chunk references to add.</param>
+		public void Add(IEnumerable<ChunkReference> chunkRefs)
+		{
+			foreach(ChunkReference chunkRef in chunkRefs)
+				Add(chunkRef);
+		}
+
+		/// <summary>
+		/// Remvoes a chunk reference from the cache.
+		/// </summary>
+		/// <param name="chunkRef">The chunk reference to remove.</param>
+		public void Remove(ChunkReference chunkRef)
+		{
+			cache.Remove(chunkRef);
+		}
+		/// <summary>
+		/// Removes a list of chunk references from the cache.
+		/// </summary>
+		/// <param name="chunkRefs">The chunk references to remove.</param>
+		public void Remove(IEnumerable<ChunkReference> chunkRefs)
+		{
+			foreach(ChunkReference chunkRef in chunkRefs)
+				Remove(chunkRef);
+		}
+
+		/// <summary>
+		/// Returns whether this cache contains the specified chunk reference.
+		/// </summary>
+		/// <param name="chunkRef">The chunk reference to check for.</param>
+		/// <returns>Whether this cache contaisn  the specified chunk reference..</returns>
+		public bool Contains(ChunkReference chunkRef)
+		{
+			return cache.Contains(chunkRef);
+		}
+
+		/// <summary>
+		/// Given a list of chunk references, return another list of chunk references
+		/// that aren't in this chunk cache.
+		/// </summary>
+		/// <param name="sourceChunkRefs">The list of chunk references to check against this chunk cache.</param>
+		/// <returns>The chunk references missing from this chunk cache.</returns>
+		public List<ChunkReference> FindMissing(IEnumerable<ChunkReference> sourceChunkRefs)
+		{
+			List<ChunkReference> result = new List<ChunkReference>();
+			foreach(ChunkReference sourceChunkRef in sourceChunkRefs)
+			{
+				if(!Contains(sourceChunkRef))
+					result.Add(sourceChunkRef);
+			}
+			return result;
+		}
+	}
+}
diff --git a/Nibriboard/Client/Messages/ChunkUpdateRequestMessage.cs b/Nibriboard/Client/Messages/ChunkUpdateRequestMessage.cs
new file mode 100644
index 0000000..cb91812
--- /dev/null
+++ b/Nibriboard/Client/Messages/ChunkUpdateRequestMessage.cs
@@ -0,0 +1,19 @@
+﻿using System;
+using System.Collections.Generic;
+using Nibriboard.RippleSpace;
+
+namespace Nibriboard.Client.Messages
+{
+	public class ChunkUpdateRequestMessage : Message
+	{
+		/// <summary>
+		/// A list of chunks that the client has intentionally forgotten about, and will need
+		/// to be resent to the client.
+		/// </summary>
+		public List<ChunkReference> ForgottenChunks = new List<ChunkReference>();
+
+		public ChunkUpdateRequestMessage()
+		{
+		}
+	}
+}
diff --git a/Nibriboard/Client/NibriClient.cs b/Nibriboard/Client/NibriClient.cs
index 10affda..c7337f8 100644
--- a/Nibriboard/Client/NibriClient.cs
+++ b/Nibriboard/Client/NibriClient.cs
@@ -50,11 +50,11 @@ namespace Nibriboard.Client
 		/// </summary>
 		private readonly WebSocket client;
 
-		private static readonly Dictionary<string, Type> messageEventTypes = new Dictionary<string, Type>()
-		{
+		private static readonly Dictionary<string, Type> messageEventTypes = new Dictionary<string, Type>() {
 			["HandshakeRequest"] = typeof(HandshakeRequestMessage),
 			["CursorPosition"] = typeof(CursorPositionMessage),
-			["PlaneChange"] = typeof(PlaneChangeMessage)
+			["PlaneChange"] = typeof(PlaneChangeMessage),
+			["ChunkUpdateRequest"] = typeof(ChunkUpdateRequestMessage)
 		};
 
 		/// <summary>
@@ -105,6 +105,11 @@ namespace Nibriboard.Client
 		/// This client's colour. Used to tell multiple clients apart visually.
 		/// </summary>
 		public readonly ColourHSL Colour = ColourHSL.RandomSaturated();
+		/// <summary>
+		/// The chunk cache. Keeps track of which chunks this client currently has.
+		/// </summary>
+		protected ChunkCache chunkCache = new ChunkCache();
+
 
 		#region Core Setup & Message Routing Logic
 
@@ -167,6 +172,7 @@ namespace Nibriboard.Client
 
 		#endregion
 
+
 		#region Message Sending
 
 		/// <summary>
@@ -239,12 +245,13 @@ namespace Nibriboard.Client
 		{
 			if(chunkRef.Plane != CurrentPlane)
 				return false;
-			
+
 			Rectangle chunkArea = chunkRef.InPlanespaceRectangle();
 
 			return chunkArea.Overlap(CurrentViewPort);
 		}
 
+
 		#region Message Handlers
 		/// <summary>
 		/// Handles an incoming handshake request. We should only receive one of these!
@@ -289,6 +296,21 @@ namespace Nibriboard.Client
 			return Task.CompletedTask;
 		}
 
+		protected async Task handleChunkUpdateRequestMessage(ChunkUpdateRequestMessage message)
+		{
+			chunkCache.Remove(message.ForgottenChunks);
+
+			ChunkUpdateMessage response = new ChunkUpdateMessage();
+			List<ChunkReference> missingChunks = ChunkTools.GetContainingChunkReferences(CurrentPlane, CurrentViewPort);
+			missingChunks = chunkCache.FindMissing(missingChunks);
+
+			response.Chunks = await CurrentPlane.FetchChunks(missingChunks);
+
+			Send(response);
+
+			chunkCache.Add(missingChunks);
+		}
+
 		/// <summary>
 		/// Handles an incoming cursor position message from the client..
 		/// </summary>
@@ -303,8 +325,10 @@ namespace Nibriboard.Client
 
 			return Task.CompletedTask;
 		}
+
 		#endregion
 
+
 		/// <summary>
 		/// Generates an update message that contains information about the locations and states of all connected clients.
 		/// Automatically omits information about the current client, and clients on other planes.
diff --git a/Nibriboard/Nibriboard.csproj b/Nibriboard/Nibriboard.csproj
index b2aae8a..df3ce55 100644
--- a/Nibriboard/Nibriboard.csproj
+++ b/Nibriboard/Nibriboard.csproj
@@ -92,6 +92,9 @@
     <Compile Include="Client\Messages\ChunkUpdateMessage.cs" />
     <Compile Include="Client\Messages\PlaneChangeMessage.cs" />
     <Compile Include="Client\Messages\ExceptionMessage.cs" />
+    <Compile Include="Utilities\ChunkTools.cs" />
+    <Compile Include="Client\Messages\ChunkUpdateRequestMessage.cs" />
+    <Compile Include="Client\ChunkCache.cs" />
   </ItemGroup>
   <ItemGroup>
     <EmbeddedResource Include="ClientFiles\index.html" />
diff --git a/Nibriboard/RippleSpace/LocationReference.cs b/Nibriboard/RippleSpace/LocationReference.cs
index 1c445d9..4b9e591 100644
--- a/Nibriboard/RippleSpace/LocationReference.cs
+++ b/Nibriboard/RippleSpace/LocationReference.cs
@@ -9,6 +9,9 @@ namespace Nibriboard.RippleSpace
 	/// </summary>
 	public class LocationReference : Reference
 	{
+		/// <summary>
+		/// The chunk that this location reference fall inside.
+		/// </summary>
 		public ChunkReference ContainingChunk {
 			get {
 				return new ChunkReference(
@@ -36,7 +39,7 @@ namespace Nibriboard.RippleSpace
 			}
 			return false;
 		}
-		public override int GetHashCode ()
+		public override int GetHashCode()
 		{
 			return $"({Plane.Name})+{X}+{Y}".GetHashCode();
 		}
diff --git a/Nibriboard/RippleSpace/Plane.cs b/Nibriboard/RippleSpace/Plane.cs
index be7c171..5bfc7a0 100644
--- a/Nibriboard/RippleSpace/Plane.cs
+++ b/Nibriboard/RippleSpace/Plane.cs
@@ -49,6 +49,18 @@ namespace Nibriboard.RippleSpace
 
 			StorageDirectory = $"./Planes/{Name}";
 		}
+		/// <summary>
+		/// Fetches a list of chunks by a list of chunk refererences.
+		/// </summary>
+		/// <param name="chunkRefs">The chunk references to fetch the attached chunks for.</param>
+		/// <returns>The chunks attached to the specified chunk references.</returns>
+		public async Task<List<Chunk>> FetchChunks(List<ChunkReference> chunkRefs)
+		{
+			List<Chunk> chunks = new List<Chunk>();
+			foreach(ChunkReference chunkRef in chunkRefs)
+				chunks.Add(await FetchChunk(chunkRef));
+			return chunks;
+		}
 
 		public async Task<Chunk> FetchChunk(ChunkReference chunkLocation)
 		{
diff --git a/Nibriboard/Utilities/ChunkTools.cs b/Nibriboard/Utilities/ChunkTools.cs
new file mode 100644
index 0000000..99321ac
--- /dev/null
+++ b/Nibriboard/Utilities/ChunkTools.cs
@@ -0,0 +1,45 @@
+﻿using System;
+using System.Collections.Generic;
+using Nibriboard.RippleSpace;
+using SBRL.Utilities;
+
+namespace SBRL.Utilities
+{
+	/// <summary>
+	/// A collection of tools aid in the manipulation of chunks.
+	/// </summary>
+	public static class ChunkTools
+	{
+		/// <summary>
+		/// Gets a list of chunk references that cross inside the specified rectangle.
+		/// </summary>
+		/// <param name="plane">The plane to operate on.</param>
+		/// <param name="area">The rectangle to find the containing chunks for.</param>
+		/// <returns>All the chunk references that fall inside the specified area.</returns>
+		public static List<ChunkReference> GetContainingChunkReferences(Plane plane, Rectangle area)
+		{
+			List<ChunkReference> result = new List<ChunkReference>();
+
+			Vector2 currentLocation = area.TopLeft;
+			while(currentLocation.X < area.BottomRight.X &&
+				  currentLocation.Y < area.BottomRight.Y)
+			{
+				result.Add(new ChunkReference(
+					plane,
+					currentLocation.X,
+					currentLocation.Y
+				));
+
+				currentLocation.X += plane.ChunkSize;
+
+				if(currentLocation.X > area.Right)
+				{
+					currentLocation.X = area.Left;
+					currentLocation.Y += plane.ChunkSize;
+				}
+			}
+
+			return result;
+		}
+	}
+}
diff --git a/Nibriboard/Utilities/Rectangle.cs b/Nibriboard/Utilities/Rectangle.cs
index 02fcc0b..417125a 100644
--- a/Nibriboard/Utilities/Rectangle.cs
+++ b/Nibriboard/Utilities/Rectangle.cs
@@ -39,36 +39,36 @@ namespace SBRL.Utilities
 		/// The top-left corner of the rectangle.
 		/// </summary>
 		[JsonIgnore]
-		public Point TopLeft {
+		public Vector2 TopLeft {
 			get {
-				return new Point(X, Y);
+				return new Vector2(X, Y);
 			}
 		}
 		/// <summary>
 		/// The top-right corner of the rectangle.
 		/// </summary>
 		[JsonIgnore]
-		public Point TopRight {
+		public Vector2 TopRight {
 			get {
-				return new Point(X + Width, Y);
+				return new Vector2(X + Width, Y);
 			}
 		}
 		/// <summary>
 		/// The bottom-left corner of the rectangle.
 		/// </summary>
 		[JsonIgnore]
-		public Point BottomLeft {
+		public Vector2 BottomLeft {
 			get {
-				return new Point(X, Y + Height);
+				return new Vector2(X, Y + Height);
 			}
 		}
 		/// <summary>
 		/// The bottom-right corner of the rectangle.
 		/// </summary>
 		[JsonIgnore]
-		public Point BottomRight {
+		public Vector2 BottomRight {
 			get {
-				return new Point(X + Width, Y + Height);
+				return new Vector2(X + Width, Y + Height);
 			}
 		}
 		#endregion
@@ -82,6 +82,9 @@ namespace SBRL.Utilities
 			get {
 				return Y;
 			}
+			set {
+				Y = value;
+			}
 		}
 		/// <summary>
 		/// The Y coordinate of the bottom of the rectangle.
@@ -91,6 +94,9 @@ namespace SBRL.Utilities
 			get {
 				return Y + Height;
 			}
+			set {
+				Height = value - Y;
+			}
 		}
 		/// <summary>
 		/// The X coordinate of the left side of the rectangle.
@@ -100,6 +106,9 @@ namespace SBRL.Utilities
 			get {
 				return X;
 			}
+			set {
+				X = value;
+			}
 		}
 		/// <summary>
 		/// The X coordinate of the right side of the rectangle.
@@ -109,6 +118,9 @@ namespace SBRL.Utilities
 			get {
 				return X + Width;
 			}
+			set {
+				Width = value - X;
+			}
 		}
 		#endregion
 
@@ -135,6 +147,26 @@ namespace SBRL.Utilities
 
 			return true;
 		}
+
+		/// <summary>
+		/// Returns a Rectangle representing the area that this rectangle overlaps with another.
+		/// Returns an empty rectangle if the two don't overlap at all.
+		/// </summary>
+		/// <param name="otherRectangle">The other rectangle that overlaps this one.</param>
+		/// <returns>The area that this rectanagle overlaps with another.</returns>
+		public Rectangle OverlappingArea(Rectangle otherRectangle)
+		{
+			if(!Overlap(otherRectangle))
+				return Rectangle.Zero;
+
+			Rectangle result = new Rectangle();
+			result.Top = Math.Max(Top, otherRectangle.Top);
+			result.Left = Math.Max(Left, otherRectangle.Left);
+			result.Bottom = Math.Max(Bottom, otherRectangle.Bottom);
+			result.Right = Math.Max(Right, otherRectangle.Right);
+
+			return result;
+		}
 	}
 }
 
