using Newtonsoft.Json; using Newtonsoft.Json.Linq; using RhythmBase.Converters; using RhythmBase.Events; using RhythmBase.Exceptions; using RhythmBase.Extensions; using RhythmBase.Settings; using RhythmBase.Utils; using System.Diagnostics.CodeAnalysis; using System.IO.Compression; namespace RhythmBase.Components { /// /// Rhythm Doctor level. /// public class RDLevel : OrderedEventCollection, IDisposable { /// /// The calculator that comes with the level. /// [JsonIgnore] public BeatCalculator Calculator { get; } /// /// Level Settings. /// public Settings Settings { get; set; } internal List ModifiableRows { get; } = new List(16); internal List ModifiableDecorations { get; } = []; /// /// Level tile collection. /// public IReadOnlyList Rows => ModifiableRows.AsReadOnly(); /// /// Level decoration collection. /// public IReadOnlyList Decorations => ModifiableDecorations.AsReadOnly(); /// /// Level condition collection. /// public List Conditionals { get; } /// /// Level bookmark collection. /// public List Bookmarks { get; } /// /// Level colorPalette collection. /// public RDColor[] ColorPalette { get => colorPalette; set => colorPalette = value.Length == 21 ? value : throw new RhythmBaseException(); } /// /// Level file path. /// [JsonIgnore] public string Path => _path; /// /// Level directory path. /// [JsonIgnore] public string Directory => System.IO.Path.GetDirectoryName(_path)!; /// /// Default beats with levels. /// The beat is 1. /// [JsonIgnore] public RDBeat DefaultBeat => Calculator.BeatOf(1f); /// /// Initializes a new instance of the class. /// public RDLevel() { _path = ""; Variables = new RDVariables(); Calculator = new BeatCalculator(this); Settings = new Settings(); Conditionals = []; Bookmarks = []; ColorPalette = new RDColor[21]; } /// /// Initializes a new instance of the class with the specified items. /// /// The items to add to the level. public RDLevel(IEnumerable items) : this() { foreach (IBaseEvent item in items) Add(item); } /// /// The default level within the game. /// public static RDLevel Default { get { RDLevel rdlevel = []; rdlevel.ColorPalette = [ RDColor.Black, RDColor.White, new(0xFF7F7F7Fu), new(0xFFC3C3C3u), new(0xFF880015u), new(0xFFB97A57u), RDColor.Red, new(0xFFFFAEC9u), new(0xFFFF7F27u), new(0xFFFFC90Eu), new(0xFFFFF200u), new(0xFFEFE4B0u), new(0xFF22B14Cu), new(0xFFB5E61Du), new(0xFF00A2E8u), new(0xFF99D9EAu), new(0xFF3F48CCu), new(0xFF7092BEu), new(0xFFA349A4u), new(0xFFC8BFE7u), new(0x00000000u) ]; rdlevel.Settings.RankMaxMistakes = [ 20, 15, 10, 5 ]; rdlevel.Settings.RankDescription = [ "Better call 911, now!", "Ugh, you can do better", "Not bad I guess...", "We make a good team!", "You are really good!", "Wow! That's awesome!!" ]; PlaySong playsong = new(); SetTheme settheme = new(); playsong.Song = new RDAudio() { Filename = "sndOrientalTechno" }; settheme.Preset = SetTheme.Theme.OrientalTechno; rdlevel.AddRange([playsong, settheme]); RowEventCollection samurai = rdlevel.CreateRow(new RDSingleRoom(RDRoomIndex.Room1), RDCharacters.Samurai); samurai.Sound.Filename = "Shaker"; samurai.Add(new AddClassicBeat()); return rdlevel; } } /// /// Create a decoration and add it to the level. /// /// The room where this decoration is in. /// The sprite referenced by this decoration. /// Decoration that created and added to the level. public DecorationEventCollection CreateDecoration(RDSingleRoom room, [NotNull] string sprite) { DecorationEventCollection temp = new(room) { Parent = this, Filename = sprite }; ModifiableDecorations.Add(temp); return temp; } /// /// Clone the decoration and add it to the level. /// /// Decoration that was copied. /// public DecorationEventCollection CloneDecoration(DecorationEventCollection decoration) { DecorationEventCollection temp = decoration.Clone(); ModifiableDecorations.Add(temp); return temp; } /// /// Remove the decoration from the level. /// /// The decoration to be removed. /// public bool RemoveDecoration(DecorationEventCollection decoration) { if (Decorations.Contains(decoration)) { this.RemoveRange(decoration); return ModifiableDecorations.Remove(decoration); } else return false; } /// /// Create a row and add it to the level. /// /// The room where this row is in. /// The character used by this row. /// Row that created and added to the level. public RowEventCollection CreateRow(RDSingleRoom room, RDCharacter character) { RowEventCollection temp = new() { Character = character, Rooms = room, Parent = this }; temp.Parent = this; ModifiableRows.Add(temp); return temp; } /// /// Remove the row from the level. /// /// The row to be removed. /// public bool RemoveRow(RowEventCollection row) { if (Rows.Contains(row)) { this.RemoveRange(row); return ModifiableRows.Remove(row); } else return false; } /// /// Read from file as level. /// Use default input settings. /// Supports .rdlevel, .rdzip, .zip file extension. /// /// File path. /// The minimum level version number supported by this library is 54. /// /// File not supported. /// An instance of a level that reads from a file. public static RDLevel Read(string filepath) => Read(filepath, new LevelReadOrWriteSettings()); /// /// Read from file as level. /// Supports .rdlevel, .rdzip, .zip file extension. /// /// File path. /// Input settings. /// The minimum level version number supported by this library is 54. /// /// File not supported. /// An instance of a level that reads from a file. public static RDLevel Read(string filepath, LevelReadOrWriteSettings settings) { JsonSerializer LevelSerializer = new(); LevelSerializer.Converters.Add(new RDLevelConverter(filepath, settings)); string extension = System.IO.Path.GetExtension(filepath); RDLevel? Read; if (extension != ".rdzip" && extension != ".zip") { if (extension != ".rdlevel") throw new RhythmBaseException("File not supported."); settings.OnBeforeReading(); Read = LevelSerializer.Deserialize(new JsonTextReader(File.OpenText(filepath))); settings.OnAfterReading(); } else Read = ReadFromZip(filepath, settings); return Read ?? []; } /// /// Reads an RDLevel from a TextReader with the specified filepath and settings. /// /// The TextReader to read from. /// The filepath of the RDLevel. /// The settings to use for reading the RDLevel. /// The deserialized RDLevel object. /// Thrown when the file cannot be read. public static RDLevel Read(TextReader reader, string filepath, LevelReadOrWriteSettings settings) { JsonSerializer LevelSerializer = new(); LevelSerializer.Converters.Add(new RDLevelConverter(filepath, settings)); RDLevel Read; Read = LevelSerializer.Deserialize(new JsonTextReader(reader)) ?? throw new RhythmBaseException("Cannot read the file."); return Read; } /// /// Reads an RDLevel from a TextReader with the specified settings. /// /// The TextReader to read from. /// The settings to use for reading the RDLevel. /// The deserialized RDLevel object. /// Thrown when the file cannot be read. public static RDLevel Read(TextReader reader, LevelReadOrWriteSettings settings) => Read(reader, "", settings); /// /// Read from a zip file as a level. /// /// The path to the zip file. /// An instance of RDLevel that reads from a zip file. public static RDLevel ReadFromZip(string filepath) => ReadFromZip(filepath, new LevelReadOrWriteSettings()); /// /// Read from a zip file as a level with specific settings. /// /// The path to the zip file. /// The settings for reading the level. /// An instance of RDLevel that reads from a zip file with specific settings. public static RDLevel ReadFromZip(string filepath, LevelReadOrWriteSettings settings) => ReadFromZip(File.OpenRead(filepath), settings); /// /// Read from a zip file as a level. /// /// The stream of the zip file. /// An instance of RDLevel that reads from a zip file. public static RDLevel ReadFromZip(Stream stream) => ReadFromZip(stream, new LevelReadOrWriteSettings()); /// /// Read from a zip file as a level with specific settings. /// /// The stream of the zip file. /// The settings for reading the level. /// An instance of RDLevel that reads from a zip file with specific settings. public static RDLevel ReadFromZip(Stream stream, LevelReadOrWriteSettings settings) { DirectoryInfo tempDirectory = new(System.IO.Path.Combine(System.IO.Path.GetTempPath(), "RhythmBaseTemp_" + System.IO.Path.GetRandomFileName())); tempDirectory.Create(); RDLevel ReadFromZip; try { ZipFile.ExtractToDirectory(stream, tempDirectory.FullName); ReadFromZip = Read(tempDirectory.GetFiles().Single(i => i.Extension == ".rdlevel").FullName, settings); ReadFromZip.isZip = true; } catch (InvalidOperationException ex) { tempDirectory.Delete(true); throw new RhythmBaseException("More than one RDLevel file has been found.", ex); } catch (Exception ex2) { tempDirectory.Delete(true); throw new RhythmBaseException("Cannot extract the file.", ex2); } return ReadFromZip; } private JsonSerializer Serializer(LevelReadOrWriteSettings settings) => new() { Converters = { new RDLevelConverter(_path, settings) } }; /// /// Save the level. /// Use default output settings. /// /// File path. /// Overwriting is disabled by the settings and a file with the same name already exists. public void Write(string filepath) => Write(filepath, new LevelReadOrWriteSettings()); /// /// Save the level. /// /// File path. /// Output settings. /// Overwriting is disabled by the settings and a file with the same name already exists. public void Write(string filepath, LevelReadOrWriteSettings settings) { using StreamWriter file = File.CreateText(filepath); Write(file, settings); } /// /// Save the level to a text writer. /// Use default output settings. /// /// The text writer to write the level to. public void Write(TextWriter stream) => Write(stream, new LevelReadOrWriteSettings()); /// /// Save the level to a text writer. /// /// The text writer to write the level to. /// The settings for writing the level. public void Write(TextWriter stream, LevelReadOrWriteSettings settings) { using JsonTextWriter writer = new(stream); settings.OnBeforeWriting(); Serializer(settings).Serialize(writer, this); settings.OnAfterWriting(); } /// /// Save the level to a stream. /// Use default output settings. /// /// The stream to write the level to. public void Write(Stream stream) => Write(new StreamWriter(stream, leaveOpen: true), new LevelReadOrWriteSettings()); /// /// Save the level to a stream. /// /// The stream to write the level to. /// The settings for writing the level. public void Write(Stream stream, LevelReadOrWriteSettings settings) => Write(new StreamWriter(stream), settings); /// /// Convert to JObject type. /// /// A JObject type that stores all the data for the level. public JObject ToJObject() => ToJObject(new LevelReadOrWriteSettings()); /// /// Convert to JObject type. /// /// A JObject type that stores all the data for the level. public JObject ToJObject(LevelReadOrWriteSettings settings) => JObject.FromObject(this, new JsonSerializer { Converters = { new RDLevelConverter(Path, settings) } }); /// /// Convert to a string that can be read by the game. /// Use default output settings. /// /// Level string. public string ToRDLevelJson() => ToRDLevelJson(new LevelReadOrWriteSettings()); /// /// Convert to a string that can be read by the game. /// /// Output settings. /// Level string. public string ToRDLevelJson(LevelReadOrWriteSettings settings) { StringWriter file = new(); Write(file, settings); file.Close(); return file.ToString(); } /// /// Add event to the level. /// /// Event to be added. /// public override void Add(IBaseEvent item) { //添加默认节拍 if (((BaseEvent)item)._beat.IsEmpty) ((BaseEvent)item)._beat._calculator = Calculator; //部分事件只能在小节的开头 if (item is IBarBeginningEvent @event && ((BaseEvent)item)._beat.BarBeat.beat != 1f) throw new IllegalBeatException(@event); //更改节拍的关联关卡 ((BaseEvent)item)._beat._calculator = Calculator; ((BaseEvent)item)._beat.ResetCache(); if (item.Type == EventType.Comment && ((Comment)item).Parent == null) //注释事件可能在精灵板块,也可能不在 base.Add(item); else if (item.Type == EventType.TintRows && ((TintRows)item).Parent == null) base.Add(item); else if (EventTypeUtils.RowTypes.Contains(item.Type)) { BaseRowAction rowAction = (BaseRowAction)item; if (rowAction.Parent == null) throw new UnreadableEventException("The Parent property of this event should not be null. Call RowEventCollection.Add() instead.", item); //添加至对应轨道 rowAction.Parent.AddSafely((BaseRowAction)item); base.Add(item); } else if (EventTypeUtils.DecorationTypes.Contains(item.Type)) { BaseDecorationAction decoAction = (BaseDecorationAction)item; if (decoAction.Parent == null) throw new UnreadableEventException("The Parent property of this event should not be null. Call DecorationEventCollection.Add() instead.", item); //添加至对应精灵 decoAction.Parent.AddSafely((BaseDecorationAction)item); base.Add(item); } //BPM 和 CPB else if (item.Type == EventType.SetCrotchetsPerBar) AddSetCrotchetsPerBar((SetCrotchetsPerBar)item); else if (EventTypeUtils.ToEnums().Contains(item.Type)) AddBaseBeatsPerMinute((BaseBeatsPerMinute)item); // 其他 else base.Add(item); } /// /// Determine if the level contains this event. /// /// Event. /// public override bool Contains(IBaseEvent item) => (Utils.EventTypeUtils.RowTypes.Contains(item.Type) && Rows.Any((RowEventCollection i) => i.Contains(item))) || (Utils.EventTypeUtils.DecorationTypes.Contains(item.Type) && Decorations.Any((DecorationEventCollection i) => i.Contains(item))) || base.Contains(item); /// /// Remove event from the level. /// /// Event to be removed. /// public override bool Remove(IBaseEvent item) { bool Remove; if (Utils.EventTypeUtils.RowTypes.Contains(item.Type) && Rows.Any((RowEventCollection i) => i.RemoveSafely((BaseRowAction)item))) { base.Remove(item); ((BaseEvent)item)._beat._calculator = null; Remove = true; } else if (Utils.EventTypeUtils.DecorationTypes.Contains(item.Type) && Decorations.Any((DecorationEventCollection i) => i.RemoveSafely((BaseDecorationAction)item))) { base.Remove(item); ((BaseEvent)item)._beat._calculator = null; Remove = true; } else if (Contains(item)) { if (item.Type == EventType.SetCrotchetsPerBar) Remove = RemoveSetCrotchetsPerBar((SetCrotchetsPerBar)item); else if (Utils.EventTypeUtils.ToEnums().Contains(item.Type)) Remove = RemoveBaseBeatsPerMinute((BaseBeatsPerMinute)item); else { bool result = base.Remove(item); ((BaseEvent)item)._beat._calculator = null; Remove = result; } } else Remove = false; return Remove; } /// /// Gets the status of the level at the specified beat. /// /// The beat at which to get the status. /// A new instance of representing the status at the specified beat. internal RDStatus GetStatus(RDBeat beat) { return new() { Beat = beat, RoomStatus = [ new(){ Beat = beat, RunningVFXs = this.Where(i=>i.Rooms.Contains(RDRoomIndex.Room1)&& i.VFXDuration().Contains(beat), new RDRange(null,beat)) }, new(){ Beat = beat, RunningVFXs = this.Where(i=>i.Rooms.Contains(RDRoomIndex.Room2)&& i.VFXDuration().Contains(beat), new RDRange(null,beat)) }, new(){ Beat = beat, RunningVFXs = this.Where(i=>i.Rooms.Contains(RDRoomIndex.Room3)&& i.VFXDuration().Contains(beat), new RDRange(null,beat)) }, new(){ Beat = beat, RunningVFXs = this.Where(i=>i.Rooms.Contains(RDRoomIndex.Room4)&& i.VFXDuration().Contains(beat), new RDRange(null,beat)) }, new(){ Beat = beat, RunningVFXs = this.Where(i=>i.Rooms.Contains(RDRoomIndex.RoomTop)&& i.VFXDuration().Contains(beat), new RDRange(null,beat)) } ], }; } private void AddSetCrotchetsPerBar(SetCrotchetsPerBar item) { SetCrotchetsPerBar? frt = item.FrontOrDefault(); SetCrotchetsPerBar? nxt = item.NextOrDefault(); //更新拍号 RefreshCPBs(item._beat); //添加事件 base.Add(item); //更新计算器 Calculator.Refresh(); if (nxt != null) { BaseEvent? nxtE = item.After().FirstOrDefault((BaseEvent i) => i is IBarBeginningEvent && i.Type != EventType.SetCrotchetsPerBar && i._beat < nxt._beat); float interval = ((nxtE != null) ? nxtE._beat.BeatOnly : nxt._beat.BeatOnly) - item._beat.BeatOnly; float c = interval % item.CrotchetsPerBar; if (c > 0f) { c = (c < 2f) ? (c + item.CrotchetsPerBar) : c; base.Add(new SetCrotchetsPerBar { _beat = item._beat + interval - c, _crotchetsPerBar = checked((uint)Math.Round((double)unchecked(c - 1f))) }); } else if (nxt.CrotchetsPerBar == item.CrotchetsPerBar) base.Remove(nxt); if (nxtE != null) base.Add(new SetCrotchetsPerBar { _beat = nxtE._beat, _crotchetsPerBar = frt?.CrotchetsPerBar ?? 8 - 1 }); } // 更新计算器 Calculator.Refresh(); } private bool RemoveSetCrotchetsPerBar(SetCrotchetsPerBar item) { SetCrotchetsPerBar? frt = item.FrontOrDefault(); SetCrotchetsPerBar? nxt = item.NextOrDefault(); if (nxt != null) { BaseEvent? nxtE = item.After().FirstOrDefault((BaseEvent i) => i is IBarBeginningEvent && i.Type != EventType.SetCrotchetsPerBar && i._beat < nxt._beat); uint cpb = item.CrotchetsPerBar; int interval = (int)((nxtE ?? nxt)._beat.BeatOnly - item._beat.BeatOnly); long c = interval % frt?.CrotchetsPerBar ?? 8; if (c > 0) { c = c < 2 ? c + item.CrotchetsPerBar : c; if (c == nxt.CrotchetsPerBar) base.Remove(nxt); base.Add(new SetCrotchetsPerBar() { _beat = item._beat + interval - c, _crotchetsPerBar = (uint)(c - 1) }); } else { if (nxtE != null && nxt.CrotchetsPerBar == (frt?.CrotchetsPerBar ?? 8)) { base.Remove(nxt); } } if (nxtE != null) base.Add(new SetCrotchetsPerBar { _beat = nxtE._beat, _crotchetsPerBar = ((frt != null) ? frt.CrotchetsPerBar : 8u - 1u) }); Calculator.Refresh(); } //更新计算器 Calculator.Refresh(); bool result = base.Remove(item); RefreshCPBs(item.Beat); item._beat._calculator = null; Calculator.Refresh(); return result; } private void AddBaseBeatsPerMinute(BaseBeatsPerMinute item) { RefreshBPMs(item.Beat); base.Add(item); Calculator.Refresh(); } private bool RemoveBaseBeatsPerMinute(BaseBeatsPerMinute item) { bool result = base.Remove(item); Calculator.Refresh(); RefreshBPMs(item.Beat); item._beat._calculator = null; return result; } private void RefreshBPMs(RDBeat start) { foreach (var item in eventsBeatOrder.Keys) item.ResetBPM(); foreach (var item in this.Where(i => i.Beat > start)) item.Beat.ResetBPM(); foreach (var item in Bookmarks) item.Beat.ResetBPM(); } private void RefreshCPBs(RDBeat start) { foreach (var item in eventsBeatOrder.Keys) item.ResetCPB(); foreach (var item in this.Where(i => i.Beat > start)) item.Beat.ResetCPB(); foreach (var item in Bookmarks) item.Beat.ResetCPB(); } /// public void Dispose() { if (isZip) { System.IO.Directory.Delete(Directory, true); } GC.SuppressFinalize(this); } /// public override string ToString() => $"\"{Settings.Song}\" Count = {Count}"; internal string _path; private bool isZip = false; private RDColor[] colorPalette = new RDColor[21]; /// /// Variables. /// [JsonIgnore] public readonly RDVariables Variables; } }