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 = 5; /// /// 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(); public PlaneInfo Info { get { return PlaneInfo.FromPlane(this); } } /// /// 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; } } /// /// Calculates the total number of chunks created that are on this plane - including those /// that are currently unloaded. /// /// /// This is, understandably, a rather expensive operation - so use with caution! /// Also, this count is only accurate to the last save. /// public int TotalChunks { get { return Directory.GetFileSystemEntries(StorageDirectory, "*.chunk").Length; } } /// /// 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 * 16; } 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, false); } /// /// 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(IEnumerable chunkRefs, bool autoCreate) { // Todo Paralellise loading with https://www.nuget.org/packages/AsyncEnumerator List chunks = new List(); foreach(ChunkReference chunkRef in chunkRefs) { Chunk nextChunk = await FetchChunk(chunkRef, true); if(nextChunk != null) // Might be null if we're not allowed to create new chunks chunks.Add(nextChunk); } return chunks; } public async Task> FetchChunks(IEnumerable chunkRefs) { return await FetchChunks(chunkRefs, true); } public async Task FetchChunk(ChunkReference chunkLocation, bool autoCreate) { // 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 // ....but only if we've been told it's ok to create new chunks. if(!autoCreate) return null; loadedChunk = new Chunk(this, ChunkSize, chunkLocation); } loadedChunk.OnChunkUpdate += HandleChunkUpdate; loadedChunkspace.Add(chunkLocation, loadedChunk); return loadedChunk; } public async Task FetchChunk(ChunkReference chunkLocation) { return await FetchChunk(chunkLocation, true); } /// /// 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) { if(newLine.Points.Count == 0) { Log.WriteLine("[Plane/{0}] Lines that don't contain any points can't be added to a chunk!", Name); return; } 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 async Task RemoveLineSegment(ChunkReference containingChunk, string targetLineUniqueId) { Chunk chunk = await FetchChunk(containingChunk); return chunk.Remove(targetLineUniqueId); } 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 StreamWriter chunkDestination = new StreamWriter(chunkDestinationFilename); chunkSavers.Add(loadedChunkItem.Value.SaveTo(chunkDestination)); } await Task.WhenAll(chunkSavers); // Save the plane information StreamWriter planeInfoWriter = new StreamWriter(CalcPaths.UnpackedPlaneIndex(StorageDirectory)); await planeInfoWriter.WriteLineAsync(JsonConvert.SerializeObject(Info)); planeInfoWriter.Close(); // Pack the chunks & plane information into an nplane file WriterOptions packingOptions = new WriterOptions(CompressionType.Deflate); IEnumerable chunkFiles = Directory.GetFiles(StorageDirectory.TrimEnd("/".ToCharArray())); using(IWriter packer = WriterFactory.Open(destination, ArchiveType.Zip, packingOptions)) { packer.Write("plane-index.json", CalcPaths.UnpackedPlaneIndex(StorageDirectory)); foreach(string nextChunkFile in chunkFiles) { packer.Write($"{Name}/{Path.GetFileName(nextChunkFile)}", nextChunkFile); } } destination.Flush(); destination.Close(); } /// /// 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)) { Directory.CreateDirectory(targetUnpackingPath); 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; } } }