using System; using System.Collections.Generic; using System.Threading.Tasks; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Serialization; 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. /// 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; } } public Plane(string inName, int inChunkSize) { Name = inName; ChunkSize = inChunkSize; // 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 = $"./Planes/{Name}"; } /// /// 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) { 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; } 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); } } /// /// 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. protected 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} {2} updated", Name, updatingChunk.Location, eventArgs.UpdateType); // Make the chunk update bubble up to plane-level OnChunkUpdate(sender, eventArgs); } } }