using System; using System.IO; using System.Threading.Tasks; using System.Collections.Generic; using System.Collections; using System.Runtime.Serialization; using Newtonsoft.Json; using Nibriboard.Utilities; namespace Nibriboard.RippleSpace { public enum ChunkUpdateType { /// /// Something was added to the chunk. /// Addition, /// /// Something was deleted form the chunk. /// Deletion, /// /// A combination of additions and deletions were made to the chunk's contents. /// Combination } public class ChunkUpdateEventArgs : EventArgs { /// /// The type of update made to the chunk /// public ChunkUpdateType UpdateType { get; set; } } public delegate void ChunkUpdateEvent(object sender, ChunkUpdateEventArgs eventArgs); /// /// Represents a single chunk of an infinite . /// [Serializable] [JsonObject(MemberSerialization.OptIn)] public class Chunk : IEnumerable, IDeserializationCallback { /// /// The plane upon which this chunk is located. /// private Plane plane; /// /// The name of the plane that this chunk is on. /// /// The name of the plane. [JsonProperty] public string PlaneName { get { return plane.Name; } } /// /// The lines that this chunk currently contains. /// [JsonProperty] private List lines = new List(); /// /// The size of this chunk. /// [JsonProperty] public readonly int Size; /// /// The location of this chunk, in chunk-space, on the plane. /// [JsonProperty] public ChunkReference Location { get; private set; } /// /// Fired when this chunk is updated. /// public event ChunkUpdateEvent OnChunkUpdate; /// /// The time at which this chunk was loaded. /// public readonly DateTime TimeLoaded = DateTime.Now; /// /// The time at which this chunk was last accessed. /// public DateTime TimeLastAccessed { get; private set; } = DateTime.Now; /// /// Whether this is a primary chunk. /// Primary chunks are always loaded. /// [JsonProperty] public bool IsPrimaryChunk { get { if(Location.X < Location.Plane.PrimaryChunkAreaSize && Location.X > -Location.Plane.PrimaryChunkAreaSize && Location.Y < Location.Plane.PrimaryChunkAreaSize && Location.Y > -Location.Plane.PrimaryChunkAreaSize) { return true; } return false; } } /// /// Whether this chunk is inactive or not. /// /// /// Note that even if a chunk is inactive, it's not guaranteed that /// it will be unloaded. It's possible that the server will keep it /// loaded anyway - it could be a primary chunk, or the server may not /// have many chunks loaded at a particular time. /// public bool Inactive { get { // If the time we were last accessed + the inactive timer is // still less than the current time, then we're inactive. if (TimeLastAccessed.AddMilliseconds(plane.InactiveMillisecs) < DateTime.Now) return false; return true; } } /// /// Whether this chunk could, theorectically, be unloaded. /// This method takes into account whether this is a primary chunk or not. /// public bool CouldUnload { get { // If we're a primary chunk or not inactive, then we shouldn't // unload it. if (IsPrimaryChunk || !Inactive) return false; return true; } } public Chunk(Plane inPlane, int inSize, ChunkReference inLocation) { plane = inPlane; Size = inSize; Location = inLocation; } /// /// Updates the time the chunk was last accessed, thereby preventing it /// from becoming inactive. /// public void UpdateAccessTime() { TimeLastAccessed = DateTime.Now; } #region Enumerator public DrawnLine this[int i] { get { UpdateAccessTime(); return lines[i]; } set { UpdateAccessTime(); lines[i] = value; } } /// /// Adds one or more new drawn lines to the chunk. /// Note that new lines added must not cross chunk borders. /// /// The new line(s) to add. public void Add(params DrawnLine[] newLines) { int i = 0; foreach (DrawnLine newLine in newLines) { if (newLine.SpansMultipleChunks == true) throw new ArgumentException("Error: A line you tried to add spans multiple chunks.", $"newLines[{i}]"); if (!newLine.ContainingChunk.Equals(Location)) throw new ArgumentException($"Error: A line you tried to add isn't in this chunk ({Location}).", $"newLine[{i}]"); lines.Add(newLine); i++; } OnChunkUpdate(this, new ChunkUpdateEventArgs() { UpdateType = ChunkUpdateType.Addition }); } public IEnumerator GetEnumerator() { UpdateAccessTime(); return lines.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { UpdateAccessTime(); return GetEnumerator(); } #endregion #region Serialisation public static async Task FromFile(Plane plane, string filename) { StreamReader chunkSource = new StreamReader(filename); return await FromStream(plane, chunkSource); } public static async Task FromStream(Plane plane, StreamReader chunkSource) { Chunk loadedChunk = JsonConvert.DeserializeObject( await chunkSource.ReadToEndAsync(), new JsonSerializerSettings() { MissingMemberHandling = MissingMemberHandling.Ignore, NullValueHandling = NullValueHandling.Ignore } ); loadedChunk.plane = plane; loadedChunk.Location.Plane = plane; foreach(DrawnLine line in loadedChunk.lines) { foreach(LocationReference point in line.Points) { point.Plane = plane; } } loadedChunk.OnChunkUpdate += plane.HandleChunkUpdate; return loadedChunk; } /// /// Saves this chunk to the specified stream. /// /// The destination stream to save the chunk to. public async Task SaveTo(StreamWriter destination) { await destination.WriteLineAsync(JsonConvert.SerializeObject(this)); destination.Close(); } public void OnDeserialization(object sender) { UpdateAccessTime(); } #endregion public override string ToString() { return string.Format( "Chunk{0} {1} - {2} lines", CouldUnload ? "!" : ":", Location, lines.Count ); } } }