From 34d74281e34702bbfd662710399c9960e347bb6f Mon Sep 17 00:00:00 2001
From: Starbeamrainbowlabs <sbrl@starbeamrainbowlabs.com>
Date: Tue, 18 Jul 2017 20:43:34 +0100
Subject: [PATCH] Extensively refactor core to allow for loading. Also work on
 a bit of saving too.

Now I can see why they say that you should build this in from the beginning....
---
 Nibriboard/Client/NibriClient.cs              |  3 +-
 Nibriboard/Nibriboard.csproj                  |  4 +-
 Nibriboard/RippleSpace/Chunk.cs               |  4 +-
 Nibriboard/RippleSpace/Plane.cs               | 51 +++++++++++++------
 Nibriboard/RippleSpace/PlaneInfo.cs           | 23 +++++++++
 Nibriboard/RippleSpace/RippleSpaceManager.cs  | 37 ++++++++++----
 .../{Utilities.cs => Utilities/BinaryIO.cs}   |  5 +-
 Nibriboard/Utilities/CalcPaths.cs             | 40 +++++++++++++++
 8 files changed, 137 insertions(+), 30 deletions(-)
 create mode 100644 Nibriboard/RippleSpace/PlaneInfo.cs
 rename Nibriboard/{Utilities.cs => Utilities/BinaryIO.cs} (95%)
 create mode 100644 Nibriboard/Utilities/CalcPaths.cs

diff --git a/Nibriboard/Client/NibriClient.cs b/Nibriboard/Client/NibriClient.cs
index 5eb971a..1cc5bc9 100644
--- a/Nibriboard/Client/NibriClient.cs
+++ b/Nibriboard/Client/NibriClient.cs
@@ -306,8 +306,9 @@ namespace Nibriboard.Client
 			Log.WriteLine("[NibriClient#{0}] Changing to plane {1}.", Id, message.NewPlaneName);
 
 			// Create a new plane with the specified name if it doesn't exist already
+			// future we might want to allow the user to specify the chunk size
 			if(manager.SpaceManager[message.NewPlaneName] == default(Plane))
-				manager.SpaceManager.CreatePlane(message.NewPlaneName);
+				manager.SpaceManager.CreatePlane(new PlaneInfo(message.NewPlaneName));
 
 			// Remove the event listener from the old plane if there is indeed an old plane to remove it from
 			if(CurrentPlane != null)
diff --git a/Nibriboard/Nibriboard.csproj b/Nibriboard/Nibriboard.csproj
index d0f1615..66807bd 100644
--- a/Nibriboard/Nibriboard.csproj
+++ b/Nibriboard/Nibriboard.csproj
@@ -66,7 +66,6 @@
     <Compile Include="RippleSpace\ChunkReference.cs" />
     <Compile Include="RippleSpace\DrawnLine.cs" />
     <Compile Include="RippleSpace\Reference.cs" />
-    <Compile Include="Utilities.cs" />
     <Compile Include="NibriboardServer.cs" />
     <Compile Include="Log.cs" />
     <Compile Include="Utilities\EmbeddedFiles.cs" />
@@ -108,6 +107,9 @@
     <Compile Include="Client\Messages\LineCompleteReflectionMessage.cs" />
     <Compile Include="Client\Messages\LineStartMessage.cs" />
     <Compile Include="Client\Messages\LineStartReflectionMessage.cs" />
+    <Compile Include="Utilities\CalcPaths.cs" />
+    <Compile Include="RippleSpace\PlaneInfo.cs" />
+    <Compile Include="Utilities\BinaryIO.cs" />
   </ItemGroup>
   <ItemGroup>
     <EmbeddedResource Include="ClientFiles\index.html" />
diff --git a/Nibriboard/RippleSpace/Chunk.cs b/Nibriboard/RippleSpace/Chunk.cs
index 166b600..1d5aa20 100644
--- a/Nibriboard/RippleSpace/Chunk.cs
+++ b/Nibriboard/RippleSpace/Chunk.cs
@@ -6,6 +6,8 @@ using System.Collections;
 using System.Runtime.Serialization;
 using Newtonsoft.Json;
 
+using Nibriboard.Utilities;
+
 namespace Nibriboard.RippleSpace
 {
 	public enum ChunkUpdateType
@@ -219,7 +221,7 @@ namespace Nibriboard.RippleSpace
 		}
 		public static async Task<Chunk> FromStream(Plane plane, Stream chunkSource)
 		{
-			Chunk loadedChunk = await Utilities.DeserialiseBinaryObject<Chunk>(chunkSource);
+			Chunk loadedChunk = await BinaryIO.DeserialiseBinaryObject<Chunk>(chunkSource);
 			loadedChunk.plane = plane;
 
 			return loadedChunk;
diff --git a/Nibriboard/RippleSpace/Plane.cs b/Nibriboard/RippleSpace/Plane.cs
index bd7f0d5..891ec7f 100644
--- a/Nibriboard/RippleSpace/Plane.cs
+++ b/Nibriboard/RippleSpace/Plane.cs
@@ -7,6 +7,8 @@ using System.Runtime.Serialization;
 using SharpCompress.Writers;
 using SharpCompress.Common;
 using SharpCompress.Readers;
+using Nibriboard.Utilities;
+using Newtonsoft.Json;
 
 namespace Nibriboard.RippleSpace
 {
@@ -79,7 +81,8 @@ namespace Nibriboard.RippleSpace
 		public int UnloadableChunks {
 			get {
 				int result = 0;
-				foreach(KeyValuePair<ChunkReference, Chunk> chunkEntry in loadedChunkspace) {
+				foreach(KeyValuePair<ChunkReference, Chunk> chunkEntry in loadedChunkspace)
+				{
 					if(chunkEntry.Value.CouldUnload)
 						result++;
 				}
@@ -87,20 +90,21 @@ namespace Nibriboard.RippleSpace
 			}
 		}
 
-		public Plane(string inName, int inChunkSize, string inStorageDirectoryRoot)
+		/// <summary>
+		/// Initialises a new plane.
+		/// </summary>
+		/// <param name="inInfo">The settings to use to initialise the new plane.</param>
+		/// <param name="inStorageDirectory">The storage directory in which we should store the plane's chunks (may be prepopulated).</param>
+		public Plane(PlaneInfo inInfo, string inStorageDirectory)
 		{
-			Name = inName;
-			ChunkSize = inChunkSize;
+			Name = inInfo.Name;
+			ChunkSize = inInfo.ChunkSize;
+			StorageDirectory = inStorageDirectory;
 
 			// Set the soft loaded chunk limit to double the number of chunks in the
 			// primary chunks area
 			// Note that the primary chunk area is a radius around (0, 0) - not the diameter
 			SoftLoadedChunkLimit = PrimaryChunkAreaSize * PrimaryChunkAreaSize * 4;
-
-			StorageDirectory = inStorageDirectoryRoot + Name;
-			if(File.Exists(StorageDirectory))
-				throw new InvalidOperationException($"Error: The unpacked storage directory {StorageDirectory} already exists!");
-			Directory.CreateDirectory(StorageDirectory);
 		}
 
 		private async Task LoadPrimaryChunks()
@@ -189,7 +193,7 @@ namespace Nibriboard.RippleSpace
 			if(LoadedChunks < SoftLoadedChunkLimit ||
 			   UnloadableChunks < MinUnloadeableChunks)
 				return;
-			
+
 			foreach(KeyValuePair<ChunkReference, Chunk> chunkEntry in loadedChunkspace)
 			{
 				if(!chunkEntry.Value.CouldUnload)
@@ -245,22 +249,39 @@ namespace Nibriboard.RippleSpace
 			OnChunkUpdate(sender, eventArgs);
 		}
 
-		public static async Task<Plane> FromFile(string inName, int inChunkSize, string inStorageDirectoryRoot, string sourceFilename)
+		/// <summary>
+		/// Loads a plane form a given nplane file.
+		/// </summary>
+		/// <param name="planeName">The name of the plane to load.</param>
+		/// <param name="storageDirectoryRoot">The directory to which the plane should be unpacked.</param>
+		/// <param name="sourceFilename">The path to the nplane file to load.</param>
+		/// <param name="deleteSource">Whether the source file should be deleted once the plane has been loaded.</param>
+		/// <returns>The loaded plane.</returns>
+		public static async Task<Plane> FromFile(string planeName, string storageDirectoryRoot, string sourceFilename, bool deleteSource)
 		{
-			Plane loadedPlane = new Plane(inName, inChunkSize, inStorageDirectoryRoot);
+			string targetUnpackingPath = CalcPaths.UnpackedPlaneDir(storageDirectoryRoot, planeName);
 
 			// Unpack the plane to the temporary directory
 			using(Stream sourceStream = File.OpenRead(sourceFilename))
 			using(IReader unpacker = ReaderFactory.Open(sourceStream))
 			{
-				unpacker.WriteAllToDirectory(loadedPlane.StorageDirectory);
+				unpacker.WriteAllToDirectory(targetUnpackingPath);
 			}
 
+			PlaneInfo planeInfo = JsonConvert.DeserializeObject<PlaneInfo>(
+				File.ReadAllText(CalcPaths.UnpackedPlaneIndex(targetUnpackingPath))
+			);
+			planeInfo.Name = planeName;
+
+			Plane loadedPlane = new Plane(planeInfo, targetUnpackingPath);
+
 			// Load the primary chunks from disk inot the plane
 			await loadedPlane.LoadPrimaryChunks();
 
+			if(deleteSource)
+				File.Delete(sourceFilename);
+
 			return loadedPlane;
 		}
-
-}
+	}
 }
diff --git a/Nibriboard/RippleSpace/PlaneInfo.cs b/Nibriboard/RippleSpace/PlaneInfo.cs
new file mode 100644
index 0000000..ee17d60
--- /dev/null
+++ b/Nibriboard/RippleSpace/PlaneInfo.cs
@@ -0,0 +1,23 @@
+﻿using System;
+
+namespace Nibriboard.RippleSpace
+{
+	public class PlaneInfo
+	{
+		public string Name { get; set; }
+		public int ChunkSize { get; set; }
+
+		public PlaneInfo()
+		{
+		}
+		public PlaneInfo(string inName) : this(inName, 1024)
+		{
+		}
+		public PlaneInfo(string inName, int inChunkSize)
+		{
+			Name = inName;
+			ChunkSize = inChunkSize;
+		}
+	}
+
+}
diff --git a/Nibriboard/RippleSpace/RippleSpaceManager.cs b/Nibriboard/RippleSpace/RippleSpaceManager.cs
index 7f775c4..f3163f5 100644
--- a/Nibriboard/RippleSpace/RippleSpaceManager.cs
+++ b/Nibriboard/RippleSpace/RippleSpaceManager.cs
@@ -3,7 +3,9 @@ using System.IO;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using System.Diagnostics;
+using System.Linq;
 using SharpCompress.Readers;
+using Nibriboard.Utilities;
 
 namespace Nibriboard.RippleSpace
 {
@@ -71,16 +73,19 @@ namespace Nibriboard.RippleSpace
 		/// <summary>
 		/// Creates a new plane, adds it to this RippleSpaceManager, and then returns it.
 		/// </summary>
-		/// <param name="newPlaneName">The name of the new plane to create.</param>
+		/// <param name="newPlaneInfo">The settings for the new plane to create.</param>
 		/// <returns>The newly created plane.</returns>
-		public Plane CreatePlane(string newPlaneName)
+		public Plane CreatePlane(PlaneInfo newPlaneInfo)
 		{
-			if(this[newPlaneName] != null)
-				throw new InvalidOperationException($"Error: A plane with the name '{newPlaneName}' already exists in this RippleSpaceManager.");
+			if(this[newPlaneInfo.Name] != null)
+				throw new InvalidOperationException($"Error: A plane with the name '{newPlaneInfo.Name}' already exists in this RippleSpaceManager.");
 
-			Log.WriteLine("[RippleSpace] Creating plane {0}", newPlaneName);
+			Log.WriteLine("[RippleSpace] Creating plane {0}", newPlaneInfo.Name);
 
-			Plane newPlane = new Plane(newPlaneName, DefaultChunkSize);
+			Plane newPlane = new Plane(
+				newPlaneInfo,
+				CalcPaths.UnpackedPlaneDir(UnpackedDirectory, newPlaneInfo.Name)
+			);
 			Planes.Add(newPlane);
 			return newPlane;
 		}
@@ -124,17 +129,29 @@ namespace Nibriboard.RippleSpace
 			Log.WriteLine("[Core] Importing planes");
 
 			StreamReader planes = new StreamReader(rippleSpace.UnpackedDirectory + "index.list");
-			List<Task> planeReaders = new List<Task>();
+			List<Task<Plane>> planeReaders = new List<Task<Plane>>();
 			string nextPlane;
+			int planeCount = 0;
 			while((nextPlane = await planes.ReadLineAsync()) != null)
 			{
-				planeReaders.Add(Plane.FromFile(rippleSpace.UnpackedDirectory + nextPlane));
+				planeReaders.Add(Plane.FromFile(
+					planeName: nextPlane,
+					storageDirectoryRoot: rippleSpace.UnpackedDirectory,
+					sourceFilename: CalcPaths.UnpackedPlaneFile(rippleSpace.UnpackedDirectory, nextPlane),
+					deleteSource: true
+				));
+				planeCount++;
 			}
 			await Task.WhenAll(planeReaders);
 
-			Log.WriteLine("[Core] done!");
+			rippleSpace.Planes.AddRange(
+				planeReaders.Select((Task<Plane> planeReader) => planeReader.Result)
+			);
+
+			Log.WriteLine("[Core] done! {0} planes loaded.", planeCount);
 
 			return rippleSpace;
 		}
-	}
+
+}
 }
diff --git a/Nibriboard/Utilities.cs b/Nibriboard/Utilities/BinaryIO.cs
similarity index 95%
rename from Nibriboard/Utilities.cs
rename to Nibriboard/Utilities/BinaryIO.cs
index 46d307d..e5ec685 100644
--- a/Nibriboard/Utilities.cs
+++ b/Nibriboard/Utilities/BinaryIO.cs
@@ -2,9 +2,10 @@
 using System.Threading.Tasks;
 using System.IO;
 using System.Runtime.Serialization.Formatters.Binary;
-namespace Nibriboard
+
+namespace Nibriboard.Utilities
 {
-	public static class Utilities
+	public static class BinaryIO
 	{
 		/// <summary>
 		/// Deserialises an object from it's binary representation.
diff --git a/Nibriboard/Utilities/CalcPaths.cs b/Nibriboard/Utilities/CalcPaths.cs
new file mode 100644
index 0000000..06ed16d
--- /dev/null
+++ b/Nibriboard/Utilities/CalcPaths.cs
@@ -0,0 +1,40 @@
+﻿using System;
+
+namespace Nibriboard.Utilities
+{
+	public static class CalcPaths
+	{
+		/// <summary>
+		/// Returns the directory in which a plane's data should be unpacked to.
+		/// </summary>
+		/// <param name="unpackingRoot">The root directory to which everything is going to be unpacked.</param>
+		/// <param name="planeName">The name of the plane that will be unpacked.</param>
+		/// <returns>The directory to which a plane should unpack it's data to.</returns>
+		public static string UnpackedPlaneDir(string unpackingRoot, string planeName)
+		{
+			string result = $"{unpackingRoot}/Planes/{planeName}/";
+			return result;
+		}
+
+		/// <summary>
+		/// Returns the path to the plane index file given a directory that a plane has been unpacked to.
+		/// </summary>
+		/// <param name="unpackingPlaneDir">The directory to which a plane's data has been unpacked.</param>
+		/// <returns>The path to the plane index file.</returns>
+		public static string UnpackedPlaneIndex(string unpackingPlaneDir)
+		{
+			return $"{unpackingPlaneDir}/plane-index.json";
+		}
+
+		/// <summary>
+		/// Calculates the path to a packed plane file.
+		/// </summary>
+		/// <param name="unpackingDir">The directory to which the nplane files were unpacked.</param>
+		/// <param name="planeName">The name of the plane to fetch the filepath for.</param>
+		/// <returns>The path to the packed plane file.</returns>
+		public static string UnpackedPlaneFile(string unpackingDir, string planeName)
+		{
+			return $"{unpackingDir}/{planeName}.nplane";
+		}
+	}
+}
