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;
}
}