using System; using System.Collections.Generic; using System.Threading.Tasks; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Serialization; using SharpCompress.Writers; using SharpCompress.Common; using SharpCompress.Readers; using Nibriboard.Utilities; using Newtonsoft.Json; namespace Nibriboard.RippleSpace { /// /// Represents an infinite plane. /// public class Plane { /// /// The name of this plane. /// public readonly string Name; /// /// The size of the chunks on this plane. /// public readonly int ChunkSize; /// /// The path to the directory that the plane's information will be stored in. /// public readonly string StorageDirectory; /// /// The number of milliseconds that should pass since a chunk's last /// access in order for it to be considered inactive. /// public int InactiveMillisecs = 60 * 1000; /// /// The number of chunks in a square around (0, 0) that should always be /// loaded. /// Works like a radius. /// public int PrimaryChunkAreaSize = 10; /// /// The minimum number of potentially unloadable chunks that we should have /// before considering unloading some chunks /// public int MinUnloadeableChunks = 50; /// /// The soft limit on the number of chunks we can have loaded before we start /// bothering to try and unload any chunks /// public int SoftLoadedChunkLimit; /// /// Fired when one of the chunks on this plane updates. /// public event ChunkUpdateEvent OnChunkUpdate; /// /// The chunkspace that holds the currently loaded and active chunks. /// protected Dictionary loadedChunkspace = new Dictionary(); /// /// The number of chunks that this plane currently has laoded into active memory. /// public int LoadedChunks { get { return loadedChunkspace.Count; } } /// /// The number of potentially unloadable chunks this plane currently has. /// public int UnloadableChunks { get { int result = 0; foreach(KeyValuePair chunkEntry in loadedChunkspace) { if(chunkEntry.Value.CouldUnload) result++; } return result; } } /// /// Initialises a new plane. /// /// The settings to use to initialise the new plane. /// The storage directory in which we should store the plane's chunks (may be prepopulated). public Plane(PlaneInfo inInfo, string inStorageDirectory) { 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; } private async Task LoadPrimaryChunks() { List primaryChunkRefs = new List(); ChunkReference currentRef = new ChunkReference(this, -PrimaryChunkAreaSize, -PrimaryChunkAreaSize); while(currentRef.Y < PrimaryChunkAreaSize) { primaryChunkRefs.Add(currentRef.Clone() as ChunkReference); currentRef.X++; if(currentRef.X > PrimaryChunkAreaSize) { currentRef.X = -PrimaryChunkAreaSize; currentRef.Y++; } } await FetchChunks(primaryChunkRefs); } /// /// Fetches a list of chunks by a list of chunk refererences. /// /// The chunk references to fetch the attached chunks for. /// The chunks attached to the specified chunk references. public async Task> FetchChunks(List chunkRefs) { // todo Paralellise loading with https://www.nuget.org/packages/AsyncEnumerator List chunks = new List(); foreach(ChunkReference chunkRef in chunkRefs) chunks.Add(await FetchChunk(chunkRef)); return chunks; } public async Task FetchChunk(ChunkReference chunkLocation) { // If the chunk is in the loaded chunk-space, then return it immediately if(loadedChunkspace.ContainsKey(chunkLocation)) { return loadedChunkspace[chunkLocation]; } // Uh-oh! The chunk isn't loaded at moment. Load it quick & then // return it fast. string chunkFilePath = Path.Combine(StorageDirectory, chunkLocation.AsFilename()); Chunk loadedChunk; if(File.Exists(chunkFilePath)) // If the chunk exists on disk, load it loadedChunk = await Chunk.FromFile(this, chunkFilePath); else // Ooooh! It's a _new_, never-before-seen one! Create a brand new chunk :D loadedChunk = new Chunk(this, ChunkSize, chunkLocation); loadedChunk.OnChunkUpdate += HandleChunkUpdate; loadedChunkspace.Add(chunkLocation, loadedChunk); return loadedChunk; } /// /// Works out whether a chunk currently exists. /// /// The chunk location to check. /// Whether the chunk at specified location exists or not. public bool HasChunk(ChunkReference chunkLocation) { if(loadedChunkspace.ContainsKey(chunkLocation)) return true; string chunkFilePath = Path.Combine(StorageDirectory, chunkLocation.AsFilename()); if(File.Exists(chunkFilePath)) return true; return false; } public async Task SaveChunk(ChunkReference chunkLocation) { // It doesn't exist, so we can't save it :P if(!loadedChunkspace.ContainsKey(chunkLocation)) return; Chunk chunk = loadedChunkspace[chunkLocation]; string chunkFilePath = Path.Combine(StorageDirectory, chunkLocation.AsFilename()); using(StreamWriter chunkDestination = new StreamWriter(chunkFilePath)) { await chunk.SaveTo(chunkDestination); } } public async Task AddLine(DrawnLine newLine) { List chunkedLineParts; // Split the line up into chunked pieces if neccessary if(newLine.SpansMultipleChunks) chunkedLineParts = newLine.SplitOnChunks(); else chunkedLineParts = new List() { newLine }; foreach(DrawnLine linePart in chunkedLineParts) { if(linePart.Points.Count == 0) Log.WriteLine("[Plane/{0}] Warning: A line part has no points in it O.o", Name); } // Add each segment to the appropriate chunk foreach(DrawnLine newLineSegment in chunkedLineParts) { Chunk containingChunk = await FetchChunk(newLineSegment.ContainingChunk); containingChunk.Add(newLineSegment); } } public void PerformMaintenance() { // Be lazy and don't bother to perform maintenance if it's not needed if(LoadedChunks < SoftLoadedChunkLimit || UnloadableChunks < MinUnloadeableChunks) return; foreach(KeyValuePair chunkEntry in loadedChunkspace) { if(!chunkEntry.Value.CouldUnload) continue; // This chunk has been inactive for a while - let's serialise it and save it to disk Stream chunkSerializationSink = new FileStream( Path.Combine(StorageDirectory, chunkEntry.Key.AsFilename()), FileMode.Create, FileAccess.Write, FileShare.None ); IFormatter binaryFormatter = new BinaryFormatter(); binaryFormatter.Serialize(chunkSerializationSink, chunkEntry.Value); // Remove the chunk from the loaded chunkspace loadedChunkspace.Remove(chunkEntry.Key); } } public async Task Save(Stream destination) { // Save all the chunks to disk List chunkSavers = new List(); foreach(KeyValuePair loadedChunkItem in loadedChunkspace) { // Figure out where to put the chunk and create the relevant directories string chunkDestinationFilename = CalcPaths.ChunkFilepath(StorageDirectory, loadedChunkItem.Key); Directory.CreateDirectory(Path.GetDirectoryName(chunkDestinationFilename)); // Ask the chunk to save itself using(StreamWriter chunkDestination = new StreamWriter(chunkDestinationFilename)) { chunkSavers.Add(loadedChunkItem.Value.SaveTo(chunkDestination)); } } await Task.WhenAll(chunkSavers); // Pack the chunks into an nplane file WriterOptions packingOptions = new WriterOptions(CompressionType.GZip); IEnumerable chunkFiles = Directory.GetFiles(StorageDirectory); using(IWriter packer = WriterFactory.Open(destination, ArchiveType.Tar, packingOptions)) { foreach(string nextChunkFile in chunkFiles) { packer.Write($"{Name}/{Path.GetFileName(nextChunkFile)}", nextChunkFile); } } } /// /// Handles chunk updates from the individual loaded chunks on this plane. /// Re-emits chunk updates it catches wind of at plane-level. /// /// The chunk responsible for the update. /// The event arguments associated with the chunk update. public void HandleChunkUpdate(object sender, ChunkUpdateEventArgs eventArgs) { Chunk updatingChunk = sender as Chunk; if(updatingChunk == null) { Log.WriteLine("[Plane {0}] Invalid chunk update event captured - ignoring."); return; } Log.WriteLine("[Plane {0}] Chunk at {1} updated because {2}", Name, updatingChunk.Location, eventArgs.UpdateType); // Make the chunk update bubble up to plane-level OnChunkUpdate(sender, eventArgs); } /// /// Loads a plane form a given nplane file. /// /// The name of the plane to load. /// The directory to which the plane should be unpacked. /// The path to the nplane file to load. /// Whether the source file should be deleted once the plane has been loaded. /// The loaded plane. public static async Task FromFile(string planeName, string storageDirectoryRoot, string sourceFilename, bool deleteSource) { 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(targetUnpackingPath); } PlaneInfo planeInfo = JsonConvert.DeserializeObject( 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; } } }