diff --git a/Program.cs b/Program.cs index 7cb11ee..b91b39e 100644 --- a/Program.cs +++ b/Program.cs @@ -474,7 +474,7 @@ private static void RunConvertChuSingleFile(string filePath, string inputKind) var destNote = _outputSpec.Kind == OutputSinkKind.Stdout ? "(标准输出)" : outPath; Console.Error.WriteLine($"{inputKind.ToUpperInvariant()} → {targetFormat.ToUpperInvariant()}: {full} → {destNote}"); - IChuChart chart; + ChuChart chart; List parseAlerts; switch (inputKind) { diff --git a/README.md b/README.md index 76dfb53..19788f0 100644 --- a/README.md +++ b/README.md @@ -160,13 +160,13 @@ return maidataText; // maidataText即为转谱结果 var (c2sChart, alerts) = new C2sParser().Parse(c2sText); // 解析 C2S 谱面字符串 var (ugcChart, alerts) = new UgcParser().Parse(ugcText); // 解析 UGC 谱面字符串 var (susChart, alerts) = new SusParser().Parse(susText); // 解析 SUS 谱面字符串 -// 以上得到的c2sChart、ugcChart、susChart,都是IChuChart类型的谱面表示对象; +// 以上得到的c2sChart、ugcChart、susChart,都是ChuChart类型的谱面表示对象; // alerts是解析过程中可能产生的警告信息等,建议打印出来。 var (c2sText, alerts) = new C2sGenerator().Generate(ugcChart); // UGC -> C2S var (ugcText, alerts) = new UgcGenerator().Generate(c2sChart); // C2S -> UGC var (susText, alerts) = new SusGenerator().Generate(c2sChart); // C2S -> SUS -// 各种Generator的Generate方法,均接受任意的IChuChart对象。 +// 各种Generator的Generate方法,均接受 ChuChart(可将任一 Parser 产出的 ChuChart 互相传入)。 // 同上,alerts是生成过程中可能产生的警告信息等,建议打印出来。 ``` @@ -215,13 +215,7 @@ finally - **parser(解析器)**:把“源格式文本”解析成中间表示 - `SimaiParser.Parse(string)` → `MaiChart` - `MA2Parser.Parse(string)` → `MaiChart` - - CHUNITHM的三种Parser(`C2sParser`、`UgcParser`、`SusParser`):`Parse(string)` → `IChuChart` - >
- > 关于IChuChart - > 当前实现IChuChart是一个通用的接口而非具体的类型,这是因为目前不同Parser解析出的谱面的IR尚未能够完全统一,所以只能都各自继承自IChuChart。 - > 不过不用担心,任意的Generator都接受任意IChuChart对象,因此你可以不在意它们之间的差异,直接拿来用就行了。 - > 未来如果有机会的话,我们会把它们进一步统一成同一个具体类型的IR,以进一步提升代码的可维护性和可读性。 - >
+ - CHUNITHM的三种Parser(`C2sParser`、`UgcParser`、`SusParser`):`Parse(string)` → `ChuChart` - 解析成功时,**返回值会同时带有 `List`**,这是转谱过程中可能遇到的警告等信息,建议打印出来(直接对`Alert`对象`ToString()`即可)。 - 如果解析失败,会抛出 `ConversionException`;该异常对象中同样含有一个 `List`,是导致转谱失败的错误信息,可以同上打印出来。 @@ -232,7 +226,7 @@ finally - **generator(生成器)**:把中间表示转回“目标格式文本” - `SimaiGenerator.Generate(MaiChart)` → Simai 单谱文本(可写入 `maidata.txt` 的 `&inote_*`) - `MA2Generator.Generate(MaiChart)` → MA2 文本 - - CHUNITHM的三种Generator(`C2sGenerator`、`UgcGenerator`、`SusGenerator`):`Generate(IChuChart)` → 目标格式的谱面文本 + - CHUNITHM的三种Generator(`C2sGenerator`、`UgcGenerator`、`SusGenerator`):`Generate(ChuChart)` → 目标格式的谱面文本 - 与parser类似,成功生成时,**返回值会同时带有 `List`**,这是转谱过程中可能遇到的警告等信息,建议打印出来(直接对`Alert`对象`ToString()`即可)。 - 如果生成失败,会抛出 `ConversionException`;该异常对象中同样含有一个 `List`,是导致转谱失败的错误信息,可以同上打印出来。 diff --git a/chart/BPMList.cs b/chart/BPMList.cs index ade5669..9009278 100644 --- a/chart/BPMList.cs +++ b/chart/BPMList.cs @@ -99,6 +99,15 @@ internal Rational ConvertTime(Rational startTime, Rational value, decimal? srcBp return result.CanonicalForm; } } + + internal (decimal, decimal, decimal, decimal) BPM_DEF() + { + var bpms = this.Select(x => x.Bpm).ToList(); + var max = bpms.Max(); + var min = bpms.Min(); + var modes = bpms.GroupBy(x => x).OrderByDescending(g => g.Count()).First().Key; // 众数 + return (this.First().Bpm, modes, max, min); + } } public record BPM(Rational Time, decimal Bpm); diff --git a/chart/chu/C2sChart.cs b/chart/chu/C2sChart.cs deleted file mode 100644 index 7418c5c..0000000 --- a/chart/chu/C2sChart.cs +++ /dev/null @@ -1,24 +0,0 @@ -using MuConvert.chart; - -namespace MuConvert.chu; - -/** - * C2S 格式谱面 IR(官方格式,RESOLUTION=384 tick/小节)。 - */ -public class C2sChart : BaseChart, IChuChart -{ - public string Version { get; set; } = "1.08.00\t1.08.00"; - public int MusicId { get; set; } - public int DifficultId { get; set; } - public string Creator { get; set; } = ""; - public int Resolution { get; set; } = 384; - public double DefBpm { get; set; } = 120.0; - public List<(int Measure, int Offset, double Bpm)> BpmEvents = []; - public List<(int Measure, int Offset, int Denom, int Num)> MetEvents = []; - public List<(int Measure, int Offset, int Duration, double Multiplier)> SflEvents = []; - - public override decimal StartBpm => (decimal)(BpmEvents.Count > 0 ? BpmEvents[0].Bpm : DefBpm); - public override decimal StartTime => Notes.Count > 0 ? Notes.Min(n => n.Measure * Resolution + n.Offset) / (decimal)Resolution * 240m / StartBpm : 0; - public override decimal EndTime => Notes.Count > 0 ? Notes.Max(n => n.Measure * Resolution + n.Offset + Math.Max(n.HoldDuration, Math.Max(n.SlideDuration, n.AirHoldDuration))) / (decimal)Resolution * 240m / StartBpm : 0; - public override int TotalNotes => Notes.Count; -} diff --git a/chart/chu/ChuChart.cs b/chart/chu/ChuChart.cs new file mode 100644 index 0000000..26624db --- /dev/null +++ b/chart/chu/ChuChart.cs @@ -0,0 +1,16 @@ +using MuConvert.chart; +using Rationals; + +namespace MuConvert.chu; + +public class ChuChart : BaseChart +{ + public string Title { get; set; } = ""; + public string Artist { get; set; } = ""; + public string Designer { get; set; } = ""; // 谱师 + public int Difficulty { get; set; } = 3; // 难度,0-basic, 1-advanced, ...。大多数情况下都是数字字符串。不直接存成数字是为了,万一自制谱这里写的不是数字、保留鲁棒性 + public string DisplayLevel { get; set; } = ""; // 显示等级,字符串 + public decimal Level { get; set; } // 定数,小数 + public string MusicId { get; set; } = "0"; + public List<(Rational Time, Rational Duration, decimal Multiplier)> SflList = []; // 所有变速声明构成的列表。 +} diff --git a/chart/chu/ChuNote.cs b/chart/chu/ChuNote.cs index f40e182..1205520 100644 --- a/chart/chu/ChuNote.cs +++ b/chart/chu/ChuNote.cs @@ -1,38 +1,55 @@ +using MuConvert.chart; +using Rationals; + namespace MuConvert.chu; /** * CHUNITHM 通用音符,C2S / UGC / SUS 共用此结构。 */ -public class ChuNote +public class ChuNote: BaseNote { /** 音符类型 (TAP, CHR, HLD, SLD, AIR, AHD 等) */ public string Type { get; set; } = "TAP"; - /** 小节号 */ - public int Measure { get; set; } - /** 小节内偏移 (C2S: 0–383, UGC/SUS: 0–1919) */ - public int Offset { get; set; } /** 起始列 (0–15) */ public int Cell { get; set; } /** 宽度 (1–16) */ public int Width { get; set; } = 1; - /** HLD 持续时长 */ - public int HoldDuration { get; set; } - /** SLD 持续时长 */ - public int SlideDuration { get; set; } + /** HLD/SLD/AHD/ASD等的 持续时长 */ + public Rational Duration { get; set => field = value.CanonicalForm; } = 0; + /** SLD 终点列 */ - public int EndCell { get; set; } + public int EndCell + { + get => _endCell ?? Cell; + set => _endCell = value; + } /** SLD 终点宽度 */ - public int EndWidth { get; set; } = 1; - /** CHR/FLK 附加数据(方向等) */ + public int EndWidth + { + get => _endWidth ?? Width; + set => _endWidth = value; + } + + /** + * 当前音符的”前驱“。对不同类型的音符,其定义不同: + * - 对 Slide(SLD/SLC),是该slide对应的前一段slide。(对首段slide,该值为null) + * - 对 Air(AIR/AUR/AUL/ADW/ADR/ADL),是它所依附的音符(可以是tap\hold等任何类型,应该只要不是air系列和aircrush(ALD)都行) + * - 对 Air Slide(ASD/ASC):对首段slide,同Air的情况、是它所依附的音符;对第二段及之后的slide,同Slide的情况,是该slide对应的前一段slide。 + * + * 不难分析出,在完成整个chart之后,这个属性其实可以根据完整chart的列表动态推断的。 + * 因此,在BaseChuParser类中提供了FillAllPrevious方法,该方法应该在所有Note被正常解析完成后调用,填充所有上述类型的音符的targetNote信息。这样就不用每个Parser都写一段相似的逻辑。 + */ + public ChuNote? Previous; + + /** CHR/FLK/Air系列音符可能会具有的标记(如UP、L、DEF等) */ public string Tag { get; set; } = ""; - /** AIR/AHD 关联的目标音符类型 */ - public string TargetNote { get; set; } = ""; - /** AHD 持续时长 */ - public int AirHoldDuration { get; set; } - /** Air Crush 起始高度 */ - public int StartHeight { get; set; } - /** Air Crush 目标高度 */ - public int TargetHeight { get; set; } - /** Air Crush 颜色 */ - public string NoteColor { get; set; } = ""; + /** ASD/ASC/ALD上具有的、目前含义还不明确的字段,统一收集到这个里面。 */ + public List ExtraData = []; + + public override Rational EndTime => (Time + Duration).CanonicalForm; + /** Air系列音符/Slide系列音符的 关联的目标音符类型。仅供向前兼容使用。 */ + public string TargetNote => Previous?.Type ?? "N"; + + private int? _endCell; + private int? _endWidth; } diff --git a/chart/chu/IChuChart.cs b/chart/chu/IChuChart.cs deleted file mode 100644 index d632bfd..0000000 --- a/chart/chu/IChuChart.cs +++ /dev/null @@ -1,8 +0,0 @@ -using MuConvert.chart; - -namespace MuConvert.chu; - -/** - * CHUNITHM 所有谱面格式的统一接口,作为 Generator 的输入类型。 - */ -public interface IChuChart : IBaseChart; diff --git a/chart/chu/SusChart.cs b/chart/chu/SusChart.cs deleted file mode 100644 index 5e2a442..0000000 --- a/chart/chu/SusChart.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MuConvert.chart; - -namespace MuConvert.chu; - -/** - * SUS 格式谱面 IR(REQUEST=480 tick/拍,lane 0–31)。 - */ -public class SusChart : BaseChart, IChuChart -{ - public string Title { get; set; } = ""; - public string Artist { get; set; } = ""; - public string Designer { get; set; } = ""; - public int TicksPerBeat { get; set; } = 480; - public double Bpm { get; set; } = 120.0; - - public override decimal StartBpm => (decimal)Bpm; - public override decimal StartTime => Notes.Count > 0 ? Notes.Min(n => n.Measure * TicksPerBeat * 4 + n.Offset) / (decimal)(TicksPerBeat * 4) * 240m / StartBpm : 0; - public override decimal EndTime => Notes.Count > 0 && StartBpm > 0 ? Notes.Max(n => n.Measure * TicksPerBeat * 4 + n.Offset + Math.Max(n.HoldDuration, Math.Max(n.SlideDuration, n.AirHoldDuration))) / (decimal)(TicksPerBeat * 4) * 240m / StartBpm : 0; - public override int TotalNotes => Notes.Count; -} diff --git a/chart/chu/UgcChart.cs b/chart/chu/UgcChart.cs deleted file mode 100644 index b0059ca..0000000 --- a/chart/chu/UgcChart.cs +++ /dev/null @@ -1,27 +0,0 @@ -using MuConvert.chart; - -namespace MuConvert.chu; - -/** - * UGC 格式谱面 IR(UMIGURI 格式,@TICKS=480 tick/拍)。 - */ -public class UgcChart : BaseChart, IChuChart -{ - public string Version { get; set; } = "6"; - public string Title { get; set; } = ""; - public string Artist { get; set; } = ""; - public string Designer { get; set; } = ""; - public string Difficulty { get; set; } = ""; - public int Level { get; set; } - public double Constant { get; set; } - public string SongId { get; set; } = ""; - public int TicksPerBeat { get; set; } = 480; - public List<(int Measure, int Num, int Den)> BeatEvents = []; - public List<(int Measure, int Offset, double Bpm)> BpmEvents = []; - public List<(int Measure, int Offset, double Multiplier)> SpeedEvents = []; - - public override decimal StartBpm => (decimal)(BpmEvents.Count > 0 ? BpmEvents[0].Bpm : 120.0); - public override decimal StartTime => Notes.Count > 0 ? Notes.Min(n => n.Measure * TicksPerBeat * 4 + n.Offset) / (decimal)(TicksPerBeat * 4) * 240m / StartBpm : 0; - public override decimal EndTime => Notes.Count > 0 && StartBpm > 0 ? Notes.Max(n => n.Measure * TicksPerBeat * 4 + n.Offset + Math.Max(n.HoldDuration, Math.Max(n.SlideDuration, n.AirHoldDuration))) / (decimal)(TicksPerBeat * 4) * 240m / StartBpm : 0; - public override int TotalNotes => Notes.Count; -} diff --git a/generator/chu/C2sGenerator.cs b/generator/chu/C2sGenerator.cs index 7e12a07..a9f5f25 100644 --- a/generator/chu/C2sGenerator.cs +++ b/generator/chu/C2sGenerator.cs @@ -1,125 +1,119 @@ -using System.Globalization; using System.Text; -using MuConvert.chart; using MuConvert.generator; using MuConvert.utils; -using static MuConvert.utils.Alert.LEVEL; namespace MuConvert.chu; -/** - * C2S 格式生成器。 - * 输入 IChuChart,内部自动转换后输出 C2S 文本。 - */ -public class C2sGenerator : IGenerator +public class C2sGenerator : IGenerator { - private const int C2sResolution = 384; - - public (string, List) Generate(IChuChart chart) + private const int RSL = 384; + + public (string, List) Generate(ChuChart chart) { var alerts = new List(); - var c2s = ConvertToC2s(chart, alerts); - var text = Serialize(c2s); + var text = Serialize(chart, alerts); return (text, alerts); } - private static C2sChart ConvertToC2s(IChuChart chart, List alerts) - { - if (chart is C2sChart c2s) return c2s; - - if (chart is UgcChart ugc) - { - var result = new C2sChart - { - Version = "1.08.00\t1.08.00", - Creator = ugc.Designer, - DefBpm = ugc.BpmEvents.Count > 0 ? ugc.BpmEvents[0].Bpm : 120.0, - }; - foreach (var b in ugc.BpmEvents) - result.BpmEvents.Add((b.Measure, ScaleDown(b.Offset, ugc.TicksPerBeat), b.Bpm)); - foreach (var b in ugc.BeatEvents) - result.MetEvents.Add((b.Measure, 0, b.Den, b.Num)); - foreach (var n in ugc.Notes) - result.Notes.Add(ScaleNote(n, ugc.TicksPerBeat)); - return result; - } - - if (chart is SusChart sus) - { - var result = new C2sChart { DefBpm = sus.Bpm }; - result.BpmEvents.Add((0, 0, sus.Bpm)); - foreach (var n in sus.Notes) - result.Notes.Add(ScaleNote(n, sus.TicksPerBeat)); - return result; - } - - alerts.Add(new Alert(Error, string.Format(Locale.ChuGeneratorUnsupported, "→ C2S"))); - throw new ConversionException(alerts); - } - - private static ChuNote ScaleNote(ChuNote n, int tpb) - { - int scaleDown(int v) => (int)((long)v * (C2sResolution / 4) / tpb); - return new ChuNote - { - Type = n.Type, Measure = n.Measure, Offset = scaleDown(n.Offset), - Cell = n.Cell, Width = n.Width, - HoldDuration = scaleDown(n.HoldDuration), SlideDuration = scaleDown(n.SlideDuration), - EndCell = n.EndCell, EndWidth = n.EndWidth, - Tag = n.Tag, TargetNote = n.TargetNote, AirHoldDuration = scaleDown(n.AirHoldDuration), - StartHeight = n.StartHeight, TargetHeight = n.TargetHeight, NoteColor = n.NoteColor, - }; - } - - private static int ScaleDown(int ticks, int tpb) => (int)((long)ticks * (C2sResolution / 4) / tpb); - - private static string Serialize(C2sChart chart) + private static string Serialize(ChuChart chart, List alerts) { + chart.Sort(); + + int.TryParse(chart.MusicId, out var musicId); var sb = new StringBuilder(); - sb.AppendLine($"VERSION\t{chart.Version}"); - sb.AppendLine($"MUSIC\t{chart.MusicId}"); + sb.AppendLine($"VERSION\t1.08.00\t1.08.00"); + sb.AppendLine($"MUSIC\t{musicId}"); sb.AppendLine("SEQUENCEID\t0"); - sb.AppendLine($"DIFFICULT\t{chart.DifficultId:D2}"); + sb.AppendLine($"DIFFICULT\t{chart.Difficulty:D2}"); sb.AppendLine("LEVEL\t0.0"); - sb.AppendLine($"CREATOR\t{chart.Creator}"); - sb.AppendLine($"BPM_DEF\t{Fmt(chart.DefBpm)}\t{Fmt(chart.DefBpm)}\t{Fmt(chart.DefBpm)}\t{Fmt(chart.DefBpm)}"); + sb.AppendLine($"CREATOR\t{chart.Designer}"); + var bpm_def = chart.BpmList.BPM_DEF(); + sb.AppendLine($"BPM_DEF\t{bpm_def.Item1}\t{bpm_def.Item2}\t{bpm_def.Item3}\t{bpm_def.Item4}"); sb.AppendLine("MET_DEF\t4\t4"); - sb.AppendLine($"RESOLUTION\t{chart.Resolution}"); - sb.AppendLine($"CLK_DEF\t{chart.Resolution}"); + sb.AppendLine($"RESOLUTION\t{RSL}"); + sb.AppendLine($"CLK_DEF\t{RSL}"); sb.AppendLine("PROGJUDGE_BPM\t240.000"); sb.AppendLine("PROGJUDGE_AER\t0.999"); sb.AppendLine("TUTORIAL\t0"); sb.AppendLine(); - foreach (var b in chart.BpmEvents) - sb.AppendLine($"BPM\t{b.Measure}\t{b.Offset}\t{Fmt(b.Bpm)}"); - foreach (var m in chart.MetEvents) - sb.AppendLine($"MET\t{m.Measure}\t{m.Offset}\t{m.Denom}\t{m.Num}"); - foreach (var s in chart.SflEvents) - sb.AppendLine($"SFL\t{s.Measure}\t{s.Offset}\t{s.Duration}\t{Mlt(s.Multiplier)}"); + foreach (var b in chart.BpmList) + { + var (m, o) = Utils.BarAndTick(b.Time, RSL); + sb.AppendLine($"BPM\t{m}\t{o}\t{b.Bpm:0.000}"); + } + + foreach (var met in chart.MetList) + { + var (m, o) = Utils.BarAndTick(met.Time, RSL); + sb.AppendLine($"MET\t{m}\t{o}\t{met.Denominator}\t{met.Numerator}"); + } + + foreach (var s in chart.SflList.OrderBy(s => s.Time)) + { + var (m, o) = Utils.BarAndTick(s.Time, RSL); + var durTicks = Utils.Tick(s.Duration, RSL); + sb.AppendLine($"SFL\t{m}\t{o}\t{durTicks}\t{s.Multiplier:0.000000}"); + } sb.AppendLine(); - foreach (var n in chart.Notes.OrderBy(n => n.Measure * C2sResolution + n.Offset)) - sb.AppendLine(FormatNote(n)); + foreach (var n in chart.Notes) + { + var line = FormatNote(n, RSL, alerts); + if (line != null) sb.AppendLine(line); + } sb.AppendLine(); return sb.ToString(); } - private static string FormatNote(ChuNote n) => n.Type switch + private static List allowedAirColors = ["DEF"]; // TODO 搞清楚UGC里的'I'颜色,在C2S里,对应的字符串是什么 + private static string AirColorTag(ChuNote n) + { + if (allowedAirColors.Contains(n.Tag)) return n.Tag; + else return "DEF"; + } + + private static string? FormatNote(ChuNote n, int tpm, List alerts) + { + var (m, o) = Utils.BarAndTick(n.Time, tpm); + var durTicks = Utils.Tick(n.Duration, tpm); + return n.Type switch + { + "TAP" => $"TAP\t{m}\t{o}\t{n.Cell}\t{n.Width}", + "CHR" => $"CHR\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.Tag}", + "HLD" or "HXD" => $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{durTicks}", + "SLD" or "SLC" or "SXD" or "SXC" => $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{durTicks}\t{n.EndCell}\t{n.EndWidth}", + "FLK" => $"FLK\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.Tag}", + "AIR" or "AUR" or "AUL" or "ADW" or "ADR" or "ADL" => + $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{AirColorTag(n)}", + "AHD" or "AHX" => $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{durTicks}\t{AirColorTag(n)}", + "ASD" or "ASC" => FormatAsdAsc(n, m, o, durTicks), + "ALD" => FormatAld(n, m, o), + "MNE" => $"MNE\t{m}\t{o}\t{n.Cell}\t{n.Width}", + _ => alert(), + }; + + string? alert() + { + alerts.Add(new Alert(Alert.LEVEL.Warning, Locale.C2SUnknownNoteType, n.Time)); + return null; + } + } + + private static string FormatAsdAsc(ChuNote n, int m, int o, int durTicks) { - "TAP" => $"TAP\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}", - "CHR" => $"CHR\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.Tag}", - "HLD" or "HXD" => $"{n.Type}\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.HoldDuration}", - "SLD" or "SLC" or "SXD" or "SXC" => $"{n.Type}\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.SlideDuration}\t{n.EndCell}\t{n.EndWidth}", - "FLK" => $"FLK\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.Tag}", - "AIR" or "AUR" or "AUL" or "ADW" or "ADR" or "ADL" => $"{n.Type}\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.TargetNote}", - "AHD" => $"AHD\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{n.AirHoldDuration}", - "ALD" or "ASD" => $"{n.Type}\t{n.Measure}\t{n.Offset}\t{n.StartHeight}\t{n.SlideDuration}\t{n.EndCell}\t{n.EndWidth}\t{n.TargetHeight}\t{n.NoteColor}", - "MNE" => $"MNE\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}", - _ => $"TAP\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}" - }; + var e0 = n.ExtraData.Count > 0 ? n.ExtraData[0] : 5; + var e1 = n.ExtraData.Count > 1 ? n.ExtraData[1] : 5; + return $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{e0}\t{durTicks}\t{n.EndCell}\t{n.EndWidth}\t{e1}\t{AirColorTag(n)}"; + } - private static string Fmt(double v) => v.ToString("0.000", CultureInfo.InvariantCulture); - private static string Mlt(double v) => v.ToString("0.000000", CultureInfo.InvariantCulture); + private static string FormatAld(ChuNote n, int m, int o) + { + var a = n.ExtraData.Count > 0 ? n.ExtraData[0] : 0; + var b = n.ExtraData.Count > 1 ? n.ExtraData[1] : 0; + var c = n.ExtraData.Count > 2 ? n.ExtraData[2] : 0; + var tail = n.ExtraData.Count > 3 ? n.ExtraData[3] : 0; + return $"ALD\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{a}\t{b}\t{c}\t{n.EndCell}\t{n.EndWidth}\t{tail}"; + } } diff --git a/generator/chu/SusGenerator.cs b/generator/chu/SusGenerator.cs index 25265e1..e4871ca 100644 --- a/generator/chu/SusGenerator.cs +++ b/generator/chu/SusGenerator.cs @@ -1,94 +1,52 @@ using System.Text; -using MuConvert.chart; using MuConvert.generator; using MuConvert.utils; -using static MuConvert.utils.Alert.LEVEL; namespace MuConvert.chu; -/** - * SUS 格式生成器。 - * 输入 IChuChart,内部自动转换后输出 SUS 文本。 - */ -public class SusGenerator : IGenerator +public class SusGenerator : IGenerator { - private const int SusTpb = 480; + private static int RSL = 480 * 4; - public (string, List) Generate(IChuChart chart) + public (string, List) Generate(ChuChart chart) { var alerts = new List(); - var sus = ConvertToSus(chart, alerts); - var text = Serialize(sus); + var text = Serialize(chart); return (text, alerts); } - private static SusChart ConvertToSus(IChuChart chart, List alerts) - { - if (chart is SusChart sus) return sus; - - double bpm = 120.0; - string title = "", artist = ""; - - if (chart is C2sChart c2s) - { - bpm = c2s.BpmEvents.Count > 0 ? c2s.BpmEvents[0].Bpm : c2s.DefBpm; - int c2sTpb = c2s.Resolution / 4; - var result = new SusChart { Bpm = bpm, TicksPerBeat = SusTpb, Title = title, Artist = artist }; - foreach (var n in c2s.Notes) result.Notes.Add(ScaleUp(n, c2sTpb)); - return result; - } - - if (chart is UgcChart ugc) - { - bpm = ugc.BpmEvents.Count > 0 ? ugc.BpmEvents[0].Bpm : 120.0; - var result = new SusChart { Bpm = bpm, TicksPerBeat = SusTpb, Title = ugc.Title, Artist = ugc.Artist }; - foreach (var n in ugc.Notes) result.Notes.Add(ScaleUp(n, ugc.TicksPerBeat)); - return result; - } - - alerts.Add(new Alert(Error, string.Format(Locale.ChuGeneratorUnsupported, "→ SUS"))); - throw new ConversionException(alerts); - } - - private static ChuNote ScaleUp(ChuNote n, int sourceTicksPerBeat) - { - int s(int v) => (int)((long)v * SusTpb / sourceTicksPerBeat); - return new ChuNote - { - Type = n.Type, Measure = n.Measure, Offset = s(n.Offset), - Cell = n.Cell * 2, Width = n.Width * 2, - HoldDuration = s(n.HoldDuration), SlideDuration = s(n.SlideDuration), - EndCell = n.EndCell * 2, EndWidth = n.EndWidth * 2, - Tag = n.Tag, TargetNote = n.TargetNote, AirHoldDuration = s(n.AirHoldDuration), - }; - } - - private static string Serialize(SusChart sus) + private static string Serialize(ChuChart sus) { + sus.Sort(); + var sb = new StringBuilder(); if (!string.IsNullOrEmpty(sus.Title)) sb.AppendLine($"#TITLE \"{sus.Title}\""); if (!string.IsNullOrEmpty(sus.Artist)) sb.AppendLine($"#ARTIST \"{sus.Artist}\""); if (!string.IsNullOrEmpty(sus.Designer)) sb.AppendLine($"#DESIGNER \"{sus.Designer}\""); - sb.AppendLine($"#BPM_DEF {sus.Bpm:F2}"); - sb.AppendLine($"#REQUEST \"{sus.TicksPerBeat}\""); + sb.AppendLine($"#BPM_DEF {sus.StartBpm:F2}"); + sb.AppendLine($"#REQUEST \"{RSL / 4}\""); sb.AppendLine(); - foreach (var n in sus.Notes.OrderBy(n => n.Measure).ThenBy(n => n.Offset)) - sb.AppendLine($"#{n.Measure:X2}{n.Offset:X3}:{FormatData(n)}"); + foreach (var n in sus.Notes) + { + var (m, o) = Utils.BarAndTick(n.Time, RSL); + sb.AppendLine($"#{m:X2}{o:X3}:{FormatData(n, RSL)}"); + } return sb.ToString(); } - private static string FormatData(ChuNote n) + private static string FormatData(ChuNote n, int tpm) { - string lw = $"{n.Cell:X2}{n.Width:X2}"; + string lw = $"{n.Cell*2:X2}{n.Width*2:X2}"; string tc = TypeCode(n.Type); - string dur = $"{(n.HoldDuration > 0 ? n.HoldDuration : n.SlideDuration > 0 ? n.SlideDuration : n.AirHoldDuration):X4}"; + var durTicks = Utils.Tick(n.Duration, tpm); + string dur = $"{durTicks:X4}"; return tc switch { "01" or "02" or "03" or "10" => $"{tc}{lw}", "05" or "08" => $"{tc}{lw}{dur}", - "06" => $"{tc}{lw}{dur}{n.EndCell:X2}{n.EndWidth:X2}", + "06" => $"{tc}{lw}{dur}{n.EndCell*2:X2}{n.EndWidth*2:X2}", "07" or "09" => $"{tc}{lw}{n.TargetNote}", _ => $"01{lw}" }; @@ -99,7 +57,7 @@ private static string FormatData(ChuNote n) "TAP" => "01", "CHR" => "02", "FLK" => "03", "HLD" => "05", "SLD" => "06", "SLC" => "06", "AIR" => "07", "AUR" => "07", "AUL" => "07", - "AHD" => "08", "ADW" => "09", "ADR" => "09", "ADL" => "09", + "AHD" => "08", "AHX" => "08", "ADW" => "09", "ADR" => "09", "ADL" => "09", "MNE" => "10", _ => "01" }; } diff --git a/generator/chu/UgcGenerator.cs b/generator/chu/UgcGenerator.cs index 36515c8..713ad43 100644 --- a/generator/chu/UgcGenerator.cs +++ b/generator/chu/UgcGenerator.cs @@ -1,113 +1,174 @@ using System.Text; -using MuConvert.chart; using MuConvert.generator; using MuConvert.utils; -using static MuConvert.utils.Alert.LEVEL; namespace MuConvert.chu; -/** - * UGC 格式生成器。 - * 输入 IChuChart,内部自动转换后输出 UGC 文本。 - */ -public class UgcGenerator : IGenerator +public class UgcGenerator : IGenerator { - private const int UgcTicksPerBeat = 480; - private const int C2sResolution = 384; + private static int RSL = 480 * 4; - public (string, List) Generate(IChuChart chart) + public (string, List) Generate(ChuChart chart) { var alerts = new List(); - var ugc = ConvertToUgc(chart, alerts); - var text = Serialize(ugc); + var text = Serialize(chart, alerts); return (text, alerts); } - private static UgcChart ConvertToUgc(IChuChart chart, List alerts) + private static string Serialize(ChuChart ugc, List alerts) { - if (chart is UgcChart ugc) return ugc; + ugc.Sort(); + + var sb = new StringBuilder(); + sb.AppendLine("@VER\t8"); + if (!string.IsNullOrEmpty(ugc.Title)) sb.AppendLine($"@TITLE\t{ugc.Title}"); + if (!string.IsNullOrEmpty(ugc.Artist)) sb.AppendLine($"@ARTIST\t{ugc.Artist}"); + if (!string.IsNullOrEmpty(ugc.Designer)) sb.AppendLine($"@DESIGN\t{ugc.Designer}"); + sb.AppendLine($"@DIFF\t{ugc.Difficulty}"); + sb.AppendLine($"@LEVEL\t{ugc.DisplayLevel}"); + sb.AppendLine($"@CONST\t{ugc.Level:F5}"); + sb.AppendLine($"@SONGID\t{ugc.MusicId}"); + sb.AppendLine($"@TICKS\t{RSL / 4}"); + foreach (var met in ugc.MetList) + { + var (m, _) = Utils.BarAndTick(met.Time, RSL); + sb.AppendLine($"@BEAT\t{m}\t{met.Numerator}\t{met.Denominator}"); + } + foreach (var b in ugc.BpmList) + { + var (m, o) = Utils.BarAndTick(b.Time, RSL); + sb.AppendLine($"@BPM\t{m}'{o}\t{b.Bpm:F5}"); + } + sb.AppendLine("@TIL\t0\t0'0\t1.00000"); + + foreach (var s in ugc.SflList.OrderBy(x => x.Time)) + { + var (m, o) = Utils.BarAndTick(s.Time, RSL); + sb.AppendLine($"@SPDMOD\t{m}'{o}\t{s.Multiplier:0.00000}"); + } + + sb.AppendLine("@MAINTIL\t0"); + sb.AppendLine("@ENDHEAD"); + sb.AppendLine(); + + // UGC Slide / AIR-SLIDE (v8): + // - Chains (ChuNote.Previous) serialize as ONE parent line + follower lines (#OffsetTick from parent time). + // - Ground slide: parent `s`, followers `>s` / `>c` + end cell/width. + // - Air slide: parent `S` + cell/width + hh (base-36 ×2, C2S/UGC height units) + N/I; followers `>s`/`>c` + xw + hh. + // - First segment may attach to TAP/HLD via Previous; only skip emit when Previous is another segment of the same chain. + var slideChains = BuildSlideChains(ugc.Notes); - if (chart is C2sChart c2s) + foreach (var n in ugc.Notes) { - var result = new UgcChart + if (IsSlideChainNote(n.Type) && n.Previous != null && IsSlideChainNote(n.Previous.Type)) + continue; // 是Slide且不是第一段Slide,则应当已经被处理过了,直接跳过 + + var (m, o) = Utils.BarAndTick(n.Time, RSL); + var ucode = UCode(n); + if (ucode == "") { - TicksPerBeat = UgcTicksPerBeat, - Designer = c2s.Creator, - Difficulty = MapDiffId(c2s.DifficultId), - SongId = c2s.MusicId.ToString(), - }; - foreach (var b in c2s.BpmEvents) - result.BpmEvents.Add((b.Measure, ScaleUp(b.Offset), b.Bpm)); - foreach (var m in c2s.MetEvents) - result.BeatEvents.Add((m.Measure, m.Num, m.Denom)); - foreach (var n in c2s.Notes) - result.Notes.Add(ScaleUpNote(n)); - return result; - } + alerts.Add(new Alert(Alert.LEVEL.Warning, $"UGC Generator遇到了不支持的音符类型: {n.Type}", n.Time, (double)ugc.ToSecond(n.Time))); + continue; + } + sb.Append($"#{m}'{o}:{ucode}"); + sb.AppendLine(); + + if (IsSlideChainNote(n.Type)) + { + if (slideChains.TryGetValue(n, out var segments)) + { + var isAir = IsAirSlideType(n.Type); + foreach (var seg in segments) + { + var endTicks = Utils.Tick(seg.EndTime - n.Time, RSL); + if (endTicks <= 0) continue; + if (isAir) + sb.AppendLine($"#{endTicks}>{SlideFollowerMarker(seg.Type)}{IntToHex(seg.EndCell)}{IntToHex(seg.EndWidth)}{EncodeUgcAirHeight2(AirSlideFollowerHeight(seg))}"); + else + sb.AppendLine($"#{endTicks}>{SlideFollowerMarker(seg.Type)}{IntToHex(seg.EndCell)}{IntToHex(seg.EndWidth)}"); + } + } + continue; + } - alerts.Add(new Alert(Error, string.Format(Locale.ChuGeneratorUnsupported, "→ UGC"))); - throw new ConversionException(alerts); + var durTicks = Utils.Tick(n.Duration, RSL); + if (n.Type is "HLD" or "HXD" && durTicks > 0) + sb.AppendLine($"#{durTicks}>s"); + else if (n.Type is "AHD" or "AHX" && durTicks > 0) + { + var marker = (n.Type == "AHX") ? 'c' : 's'; + sb.AppendLine($"#{durTicks}>{marker}"); + } + } + return sb.ToString(); } - private static ChuNote ScaleUpNote(ChuNote n) + private static Dictionary> BuildSlideChains(List notes) { - int s(int v) => (int)((long)v * UgcTicksPerBeat / (C2sResolution / 4)); - return new ChuNote + var chains = new Dictionary>(); + foreach (var n in notes) { - Type = n.Type, Measure = n.Measure, Offset = s(n.Offset), - Cell = n.Cell, Width = n.Width, - HoldDuration = s(n.HoldDuration), SlideDuration = s(n.SlideDuration), - EndCell = n.EndCell, EndWidth = n.EndWidth, - Tag = n.Tag, TargetNote = IsAir(n.Type) && string.IsNullOrEmpty(n.TargetNote) ? "N" : n.TargetNote, - AirHoldDuration = s(n.AirHoldDuration), - StartHeight = n.StartHeight, TargetHeight = n.TargetHeight, NoteColor = n.NoteColor, - }; - } + if (!IsSlideChainNote(n.Type)) continue; + var head = GetSlideHead(n); + if (!chains.TryGetValue(head, out var list)) + chains[head] = list = []; + list.Add(n); + } - private static int ScaleUp(int v) => (int)((long)v * UgcTicksPerBeat / (C2sResolution / 4)); + // Order segments by their end time so follower ticks are increasing. + foreach (var (_, segs) in chains) + { + segs.Sort((a, b) => + { + var t = a.EndTime.CompareTo(b.EndTime); + if (t != 0) return t; + // stable-ish tie-breakers + t = a.Time.CompareTo(b.Time); + if (t != 0) return t; + t = string.CompareOrdinal(a.Type, b.Type); + if (t != 0) return t; + return 0; + }); + } - private static bool IsAir(string t) => t is "AIR" or "AUR" or "AUL" or "AHD" or "ADW" or "ADR" or "ADL"; + // For a valid chain, follower ticks should be strictly increasing; if the chart has + // degenerate segments, later code simply skips non-positive offsets. + return chains; + } - private static string MapDiffId(int id) => id switch + private static ChuNote GetSlideHead(ChuNote n) { - 0 => "BASIC", 1 => "ADVANCED", 2 => "EXPERT", 3 => "MASTER", 4 => "ULTIMA", _ => "0" - }; + var cur = n; + while (cur.Previous != null && IsSlideChainNote(cur.Previous.Type)) + cur = cur.Previous; + return cur; + } - private static string Serialize(UgcChart ugc) - { - var sb = new StringBuilder(); - sb.AppendLine("@VER\t6"); - if (!string.IsNullOrEmpty(ugc.Title)) sb.AppendLine($"@TITLE\t{ugc.Title}"); - if (!string.IsNullOrEmpty(ugc.Artist)) sb.AppendLine($"@ARTIST\t{ugc.Artist}"); - if (!string.IsNullOrEmpty(ugc.Designer)) sb.AppendLine($"@DESIGN\t{ugc.Designer}"); - sb.AppendLine($"@DIFF\t{DiffId(ugc.Difficulty)}"); - sb.AppendLine($"@LEVEL\t{ugc.Level}"); - sb.AppendLine($"@CONST\t{ugc.Constant:F5}"); - sb.AppendLine($"@SONGID\t{ugc.SongId}"); - sb.AppendLine($"@TICKS\t{ugc.TicksPerBeat}"); - foreach (var b in ugc.BeatEvents) sb.AppendLine($"@BEAT\t{b.Measure}\t{b.Num}\t{b.Den}"); - foreach (var b in ugc.BpmEvents) sb.AppendLine($"@BPM\t{b.Measure}'{b.Offset}\t{b.Bpm:F5}"); - sb.AppendLine("@TIL\t0\t0'0\t1.00000"); - sb.AppendLine("@MAINTIL\t0"); - sb.AppendLine("@ENDHEAD"); - sb.AppendLine(); + private static bool IsSlideType(string t) => t is "SLD" or "SLC" or "SXD" or "SXC"; + private static bool IsAirSlideType(string t) => t is "ASD" or "ASC"; + private static bool IsSlideChainNote(string t) => IsSlideType(t) || IsAirSlideType(t); + private static char SlideFollowerMarker(string t) => t is "SLC" or "SXC" or "ASC" ? 'c' : 's'; - var notes = ugc.Notes.OrderBy(n => n.Measure).ThenBy(n => n.Offset).ToList(); - foreach (var n in notes) - { - sb.Append($"#{n.Measure}'{n.Offset}:{UCode(n)}"); - sb.AppendLine(); - if (n.Type == "HLD" && n.HoldDuration > 0) - sb.AppendLine($"#{n.HoldDuration}>s"); - else if (n.Type == "SLD" && n.SlideDuration > 0) - sb.AppendLine($"#{n.SlideDuration}>s{Hx(n.EndCell)}{Hw(n.EndWidth)}"); - } - return sb.ToString(); + /// C2S col.6 / follower height: integer stored as two base-36 digits (Umiguri v8 AIR-SLIDE). + private static string EncodeUgcAirHeight2(int value) + { + var v = Math.Clamp(value * 10, 0, 35 * 36 + 35); + var hi = v / 36; + var lo = v % 36; + return $"{IntToHex(hi)}{IntToHex(lo)}"; } + private static int AirSlideParentStartHeight(ChuNote head) => 8; // TODO 现在暂时写死,之后应该改成从ExtraData等地方读取 + private static int AirSlideFollowerHeight(ChuNote seg) => 8; // TODO 现在暂时写死,之后应该改成从ExtraData等地方读取 + + private static Dictionary AirDirections = new() + { + ["AIR"] = "UC", ["AUR"] = "UR", ["AUL"] = "UL", ["ADW"] = "DC", ["ADR"] = "DR", ["ADL"] = "DL", + }; private static string UCode(ChuNote n) { - string c = Hx(n.Cell), w = Hw(n.Width); + string c = IntToHex(n.Cell), w = IntToHex(n.Width); + var targetNote = string.IsNullOrEmpty(n.TargetNote) ? "N" : n.TargetNote; return n.Type switch { "TAP" => $"t{c}{w}", @@ -117,18 +178,21 @@ private static string UCode(ChuNote n) "SLC" or "SXC" => $"s{c}{w}", "FLK" => $"f{c}{w}A", "MNE" => $"d{c}{w}", - "AIR" => $"a{c}{w}UC{n.TargetNote}", - "AUR" => $"a{c}{w}UR{n.TargetNote}", - "AUL" => $"a{c}{w}UL{n.TargetNote}", - "AHD" => $"a{c}{w}HD{n.TargetNote}_{n.AirHoldDuration}", - "ADW" => $"a{c}{w}DC{n.TargetNote}", - "ADR" => $"a{c}{w}DR{n.TargetNote}", - "ADL" => $"a{c}{w}DL{n.TargetNote}", - _ => $"t{c}{w}" + // AIR-SLIDE (v8): #BarTick:S x w hh c + "ASD" or "ASC" => $"S{c}{w}{EncodeUgcAirHeight2(AirSlideParentStartHeight(n))}{AirHoldColorSuffix(n)}", + "AIR" or "AUR" or "AUL" or "ADW" or "ADR" or "ADL" => $"a{c}{w}{AirDirections[n.Type]}{targetNote}{AirHoldColorSuffix(n)}", + // AIR-HOLD (v8): #BarTick:H x w c + 子行 #OffsetTick:s / :c(见 Umiguri Chart v8 doc) + "AHD" or "AHX" => $"H{c}{w}{AirHoldColorSuffix(n)}", + _ => "" }; } - private static string Hx(int v) => "0123456789ABCDEF"[Math.Clamp(v, 0, 15)].ToString(); - private static string Hw(int v) => "123456789ABCDEFG"[Math.Clamp(v - 1, 0, 15)].ToString(); - private static int DiffId(string d) => d switch { "BASIC" => 0, "ADVANCED" => 1, "EXPERT" => 2, "MASTER" => 3, "ULTIMA" => 4, _ => 0 }; + private static string IntToHex(int v) => "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Math.Clamp(v, 0, 35)].ToString(); + + private static readonly Dictionary AirColor = new() + { + ["DEF"] = "N", + ["I"] = "I", // TODO 搞清楚UGC里的'I'颜色,在C2S里,对应的字符串是什么 + }; + private static string AirHoldColorSuffix(ChuNote n) => AirColor.GetValueOrDefault(n.Tag, "N"); } diff --git a/generator/mai/MA2Generator.cs b/generator/mai/MA2Generator.cs index 7aed680..2d6ec74 100644 --- a/generator/mai/MA2Generator.cs +++ b/generator/mai/MA2Generator.cs @@ -45,15 +45,6 @@ GENERATED_BY MuConvert v{8} "; private Rational __1_384 = new(1, 384); - - private (decimal, decimal, decimal, decimal) bpmStats() - { - var bpms = chart.BpmList.Select(x => x.Bpm).ToList(); - var max = bpms.Max(); - var min = bpms.Min(); - var modes = bpms.GroupBy(x => x).OrderByDescending(g => g.Count()).First().Key; // 众数 - return (chart.BpmList.First().Bpm, modes, max, min); - } /** * 把Rational的时间近似到RESOLUTION允许的最接近tick上 @@ -196,7 +187,7 @@ protected virtual List AddSlide(Slide slide, int bar, int tick) // 生成文件头 protected void GenerateFileHead(StringBuilder result) { - var bpmStatistics = bpmStats(); + var bpmStatistics = chart.BpmList.BPM_DEF(); string head = string.Format(headTemplate, $"{MA2Version / 100}.{MA2Version % 100:D2}.00", IsUtage?1:0, bpmStatistics.Item1, bpmStatistics.Item2, bpmStatistics.Item3, bpmStatistics.Item4, diff --git a/parser/chu/BaseChuParser.cs b/parser/chu/BaseChuParser.cs new file mode 100644 index 0000000..ae1b43b --- /dev/null +++ b/parser/chu/BaseChuParser.cs @@ -0,0 +1,90 @@ +using MuConvert.chu; +using MuConvert.utils; + +namespace MuConvert.parser; + +public abstract class BaseChuParser : IParser +{ + public abstract (ChuChart, List) Parse(string text); + + /** + * 填充所有需要 Previous 的音符(见 注释)。 + * 只会填充当前Previous没有被设置过的音符:如果某个音符的Previous不为null(在Parse过程中已经被设置过了),则会尊重Parse的决定,不会再次设置。 + * + * 推断规则: + * - 前驱音符必须满足“首尾相接”:prev.EndTime == cur.Time 且 prev.EndCell == cur.Cell 且 prev.EndWidth == cur.Width + * - 再按音符类型施加额外约束(slide / air / air-slide) + * + * 该方法应在所有音符解析完成后调用。 + * + * 谱面对象 + * 过程中产生的警告会被放进这个数组里。 + * 可选。对C2S这种,谱面中原始记录了targetNote的类型的格式,可以将相关记录通过这个字典传过来,供本函数作为选择previous时的优先和参考。 + */ + protected virtual void FillAllPrevious(ChuChart chart, List alerts, Dictionary? rawTargetNote = null) + { + if (chart.Notes.Count == 0) return; + + var endDict = new Dictionary<(Rationals.Rational EndTime, int EndCell, int EndWidth), List>(); + foreach (var n in chart.Notes) + { + endDict.Add((n.EndTime, n.EndCell, n.EndWidth), n); + } + + foreach (var cur in chart.Notes) + { + if (!NeedsPrevious(cur)) continue; + if (cur.Previous != null) continue; // 若某些 parser 已提前填了 Previous,则保留 + + var key = (cur.Time, cur.Cell, cur.Width); + var filtered = FilterPreviousCandidates(cur, endDict.GetValueOrDefault(key, [])); + + if (rawTargetNote != null && rawTargetNote.TryGetValue(cur, out var target) && !string.IsNullOrEmpty(target)) + { + var filteredByRaw = filtered.Where(x=>x.Type == target).ToList(); + if (filteredByRaw.Count == 0) + { + alerts.Add(new Alert(Alert.LEVEL.Warning, "未找到声明的前驱/依附音符", cur.Time, (double)chart.ToSecond(cur.Time))); + } + else filtered = filteredByRaw; // 缩小目标范围 + } + + if (filtered.Count > 0) cur.Previous = filtered[0]; // 取第一个 + } + } + + private static bool NeedsPrevious(ChuNote n) + { + return IsSlide(n.Type) || IsAir(n.Type) || IsAirSlide(n.Type) || IsAirHold(n.Type) || IsAirCrush(n.Type); + } + + public static bool IsSlide(string t) => t is "SLD" or "SLC" or "SXD" or "SXC"; + public static bool IsAirSlide(string t) => t is "ASD" or "ASC"; + public static bool IsAir(string t) => t is "AIR" or "AUR" or "AUL" or "ADW" or "ADR" or "ADL"; + public static bool IsAirHold(string t) => t is "AHD" or "AHX"; + public static bool IsAirCrush(string t) => t is "ALD"; + // 是否是广义的air音符(Air/Air Hold/Air Slide/Air Crush) + public static bool IsGeneralizedAir(string t) => IsAir(t) || IsAirHold(t) || IsAirSlide(t) || IsAirCrush(t); + + private static List FilterPreviousCandidates(ChuNote cur, List candidates) + { // 注意:候选列表已满足“首尾相接”,这里仅做类型约束 + List result = []; + + if (IsSlide(cur.Type)) + { // Slide 的 previous:上一段 slide(找不到则说明是首段,则为 null) + result.AddRange(candidates.Where(n => IsSlide(n.Type))); + } + else if (IsAirSlide(cur.Type)) + { // Air Slide:优先匹配“上一段airslide”,其次匹配“上一段其他 + result.AddRange(candidates.Where(n => IsAirSlide(n.Type))); + result.AddRange(candidates.Where(n => IsLegalPreviousForAir(n.Type))); + } + else if (IsAir(cur.Type) || IsAirHold(cur.Type)) + { // Air 系列:依附在一个“非广义Air”的音符上 + return candidates.Where(n => IsLegalPreviousForAir(n.Type)).ToList(); + } + return result; + + bool IsLegalPreviousForAir(string t) => !(IsGeneralizedAir(t) || t == "MNE" || t == "CLICK"); + } +} \ No newline at end of file diff --git a/parser/chu/C2sParser.cs b/parser/chu/C2sParser.cs index 518fbe5..e10cdbb 100644 --- a/parser/chu/C2sParser.cs +++ b/parser/chu/C2sParser.cs @@ -2,6 +2,7 @@ using MuConvert.chart; using MuConvert.parser; using MuConvert.utils; +using Rationals; using static MuConvert.utils.Alert.LEVEL; namespace MuConvert.chu; @@ -10,16 +11,20 @@ namespace MuConvert.chu; * C2S 格式解析器(官方格式,RESOLUTION=384 tick/小节)。 * Tab 分隔文本,识别 HEADER / TIMING / NOTES 区段。 */ -public class C2sParser : IParser +public class C2sParser: BaseChuParser { + private static int RSL = 384; private static readonly HashSet HeadTags = new(StringComparer.OrdinalIgnoreCase) { "VERSION", "MUSIC", "SEQUENCEID", "DIFFICULT", "LEVEL", "CREATOR", "BPM_DEF", "MET_DEF", "RESOLUTION", "CLK_DEF", "PROGJUDGE_BPM", "PROGJUDGE_AER", "TUTORIAL" }; private static readonly HashSet TimingTags = new(StringComparer.OrdinalIgnoreCase) { "BPM", "MET", "SFL" }; - public (C2sChart, List) Parse(string text) + // C2S 会原始记录 targetNote 字符串;用于在 Previous 推断有多个候选时优先匹配。 + private readonly Dictionary _rawTargetNote = new(); + + public override (ChuChart, List) Parse(string text) { - var chart = new C2sChart(); + var chart = new ChuChart(); var alerts = new List(); var lines = text.Replace("\r\n", "\n").Split('\n'); bool inNotes = false; @@ -49,44 +54,48 @@ public class C2sParser : IParser } } + FillAllPrevious(chart, alerts, _rawTargetNote); return (chart, alerts); } - private static void ParseHeader(string[] p, C2sChart chart) + private static void ParseHeader(string[] p, ChuChart chart) { var tag = p[0].ToUpperInvariant(); switch (tag) { - case "VERSION": chart.Version = Str(p, 1); break; - case "MUSIC": chart.MusicId = Int(p, 1); break; - case "DIFFICULT": chart.DifficultId = Int(p, 1); break; - case "CREATOR": chart.Creator = Str(p, 1); break; - case "BPM_DEF": chart.DefBpm = Dbl(p, 1, 120.0); break; - case "RESOLUTION": chart.Resolution = Math.Max(1, Int(p, 1, 384)); break; + case "MUSIC": chart.MusicId = Int(p, 1).ToString(); break; + case "DIFFICULT": chart.Difficulty = Int(p, 1); break; + case "CREATOR": chart.Designer = Str(p, 1); break; + case "RESOLUTION": RSL = Math.Max(1, Int(p, 1, 384)); break; } } - private static void ParseTiming(string[] p, C2sChart chart) + private static void ParseTiming(string[] p, ChuChart chart) { var tag = p[0].ToUpperInvariant(); switch (tag) { case "BPM": - chart.BpmEvents.Add((Int(p, 1), Int(p, 2), Dbl(p, 3, 120.0))); + chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), + decimal.Parse(p[3], CultureInfo.InvariantCulture))); break; case "MET": - chart.MetEvents.Add((Int(p, 1), Int(p, 2), Int(p, 3, 4), Int(p, 4, 4))); + chart.MetList.Add(new MET(Int(p, 1) + new Rational(Int(p, 2), RSL), Int(p, 4, 4), Int(p, 3, 4))); break; case "SFL": - chart.SflEvents.Add((Int(p, 1), Int(p, 2), Int(p, 3), Dbl(p, 4, 1.0))); + chart.SflList.Add(( + Int(p, 1) + new Rational(Int(p, 2), RSL), + new Rational(Int(p, 3), RSL), + decimal.Parse(p[4], CultureInfo.InvariantCulture))); break; } } - private static void ParseNote(string[] p, C2sChart chart, List alerts, int lineNum) + private void ParseNote(string[] p, ChuChart chart, List alerts, int lineNum) { var tag = p[0].ToUpperInvariant(); - var note = new ChuNote { Type = tag, Measure = Int(p, 1), Offset = Int(p, 2) }; + var note = new ChuNote { Type = tag, Time = Int(p, 1) + new Rational(Int(p, 2), RSL) }; + string? targetNote = null; switch (tag) { @@ -95,29 +104,56 @@ private static void ParseNote(string[] p, C2sChart chart, List alerts, in case "CHR": note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.Tag = Str(p, 5); break; case "HLD": case "HXD": - note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.HoldDuration = Int(p, 5); break; + note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.Duration = new Rational(Int(p, 5), RSL); break; case "SLD": case "SLC": case "SXD": case "SXC": note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); - note.SlideDuration = Int(p, 5); note.EndCell = Int(p, 6); note.EndWidth = Math.Max(1, Int(p, 7, 1)); break; + note.Duration = new Rational(Int(p, 5), RSL); + note.EndCell = Int(p, 6); note.EndWidth = Math.Max(1, Int(p, 7, 1)); + break; case "FLK": note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.Tag = Str(p, 5); break; case "AIR": case "AUR": case "AUL": case "ADW": case "ADR": case "ADL": - note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.TargetNote = Str(p, 5); break; - case "AHD": + note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); targetNote = Str(p, 5); + if (p.Length >= 7) note.Tag = Str(p, 6); + break; + case "AHD": case "AHX": + note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); + targetNote = Str(p, 5); note.Duration = new Rational(Int(p, 6), RSL); + if (p.Length >= 8) note.Tag = Str(p, 7); + break; + case "ASD": case "ASC": + // 文档:M O Cell Width | TargetNote | 未知 | Duration | EndCell | EndWidth | 未知 | Tag + if (p.Length < 12) + { + alerts.Add(new Alert(Warning, $"{tag} 列数不足(期望至少 12 列)") { Line = lineNum }); + return; + } + note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); + targetNote = Str(p, 5); + note.ExtraData = [Int(p, 6), Int(p, 10)]; + note.Duration = new Rational(Int(p, 7), RSL); + note.EndCell = Int(p, 8); note.EndWidth = Math.Max(1, Int(p, 9, 1)); + note.Tag = Str(p, 11); + break; + case "ALD": + // 文档:M O Cell Width | 未知×3 | EndCell | EndWidth | 未知(1 或 3) + if (p.Length < 11) + { + alerts.Add(new Alert(Warning, "ALD 列数不足(期望至少 11 列)") { Line = lineNum }); + return; + } note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); - note.TargetNote = Str(p, 5); note.AirHoldDuration = Int(p, 6); break; - case "ALD": case "ASD": - note.StartHeight = Int(p, 3); note.SlideDuration = Int(p, 4); - note.EndCell = Int(p, 5); note.EndWidth = Math.Max(1, Int(p, 6, 1)); - note.TargetHeight = Int(p, 7); note.NoteColor = Str(p, 8); break; + note.ExtraData = [Int(p, 5), Int(p, 6), Int(p, 7), Int(p, 10)]; + note.EndCell = Int(p, 8); note.EndWidth = Math.Max(1, Int(p, 9, 1)); + break; default: alerts.Add(new Alert(Warning, string.Format(Locale.C2SUnknownNoteType, tag)) { Line = lineNum }); return; } + if (targetNote != null) _rawTargetNote[note] = targetNote; chart.Notes.Add(note); } private static int Int(string[] p, int i, int def = 0) => i < p.Length && int.TryParse(p[i], NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? v : def; - private static double Dbl(string[] p, int i, double def = 0) => i < p.Length && double.TryParse(p[i], NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : def; private static string Str(string[] p, int i) => i < p.Length ? p[i] : ""; } diff --git a/parser/chu/SusParser.cs b/parser/chu/SusParser.cs index 89d0033..0b6c63f 100644 --- a/parser/chu/SusParser.cs +++ b/parser/chu/SusParser.cs @@ -2,6 +2,7 @@ using MuConvert.chart; using MuConvert.parser; using MuConvert.utils; +using Rationals; using static MuConvert.utils.Alert.LEVEL; namespace MuConvert.chu; @@ -10,8 +11,10 @@ namespace MuConvert.chu; * SUS 格式解析器(社区工具格式,REQUEST=480 tick/拍,lane 0–31)。 * #MMTT:data 十六进制编码音符。 */ -public class SusParser : IParser +public class SusParser: BaseChuParser { + private static int RSL = 480 * 4; + private static readonly Dictionary TypeMap = new() { [0x01] = "TAP", @@ -25,9 +28,9 @@ public class SusParser : IParser [0x10] = "MNE", }; - public (SusChart, List) Parse(string text) + public override (ChuChart, List) Parse(string text) { - var chart = new SusChart(); + var chart = new ChuChart(); var alerts = new List(); var lines = text.Replace("\r\n", "\n").Split('\n'); @@ -54,6 +57,7 @@ public class SusParser : IParser } } + FillAllPrevious(chart, alerts); return (chart, alerts); } @@ -66,7 +70,7 @@ private static bool IsHeaderLine(string content) || content.StartsWith("REQUEST "); } - private static void ParseHeaderLine(string content, SusChart chart, List alerts, int lineNum) + private static void ParseHeaderLine(string content, ChuChart chart, List alerts, int lineNum) { if (content.StartsWith("TITLE ")) { @@ -84,7 +88,7 @@ private static void ParseHeaderLine(string content, SusChart chart, List { var bpmStr = content[8..].Trim().Trim('"'); if (double.TryParse(bpmStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var bpm)) - chart.Bpm = bpm; + chart.BpmList.Add(new BPM(0, (decimal)bpm)); else alerts.Add(new Alert(Warning, $"BPM_DEF 格式错误: {content}") { Line = lineNum }); } @@ -92,13 +96,13 @@ private static void ParseHeaderLine(string content, SusChart chart, List { var reqStr = content[8..].Trim().Trim('"'); if (int.TryParse(reqStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ticks)) - chart.TicksPerBeat = ticks; + RSL = ticks * 4; else alerts.Add(new Alert(Warning, $"REQUEST 格式错误: {content}") { Line = lineNum }); } } - private static void ParseNoteLine(string content, SusChart chart, List alerts, int lineNum) + private static void ParseNoteLine(string content, ChuChart chart, List alerts, int lineNum) { var colonIdx = content.IndexOf(':'); if (colonIdx < 0) @@ -138,8 +142,7 @@ private static void ParseNoteLine(string content, SusChart chart, List al var note = new ChuNote { Type = typeName, - Measure = measure, - Offset = tick, + Time = measure + new Rational(tick, RSL), Cell = lane / 2, Width = Math.Max(1, width / 2), }; @@ -153,47 +156,47 @@ private static void ParseNoteLine(string content, SusChart chart, List al break; case "HLD": - ParseHoldData(dataStr, note, alerts, lineNum); + ParseHoldData(dataStr, note, RSL, alerts, lineNum); break; case "SLD": - ParseSlideData(dataStr, note, alerts, lineNum); + ParseSlideData(dataStr, note, RSL, alerts, lineNum); break; case "AIR": case "ADW": - ParseAirTarget(dataStr, note, alerts, lineNum); + ParseAirTarget(dataStr, note, RSL, alerts, lineNum); break; case "AHD": - ParseAhdData(dataStr, note, alerts, lineNum); + ParseAhdData(dataStr, note, RSL, alerts, lineNum); break; } chart.Notes.Add(note); } - private static void ParseHoldData(string dataStr, ChuNote note, List alerts, int lineNum) + private static void ParseHoldData(string dataStr, ChuNote note, int tpm, List alerts, int lineNum) { if (dataStr.Length >= 10) { - note.HoldDuration = HexToInt(dataStr[6..10]); + note.Duration = new Rational(HexToInt(dataStr[6..10]), tpm); } else { - alerts.Add(new Alert(Warning, $"HLD 音符缺少时长: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"HLD 音符缺少时长: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note, tpm) }); } } - private static void ParseSlideData(string dataStr, ChuNote note, List alerts, int lineNum) + private static void ParseSlideData(string dataStr, ChuNote note, int tpm, List alerts, int lineNum) { if (dataStr.Length >= 10) { - note.SlideDuration = HexToInt(dataStr[6..10]); + note.Duration = new Rational(HexToInt(dataStr[6..10]), tpm); } else { - alerts.Add(new Alert(Warning, $"SLD 音符缺少时长: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"SLD 音符缺少时长: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note, tpm) }); return; } @@ -204,27 +207,23 @@ private static void ParseSlideData(string dataStr, ChuNote note, List ale } } - private static void ParseAirTarget(string dataStr, ChuNote note, List alerts, int lineNum) + private static void ParseAirTarget(string dataStr, ChuNote note, int tpm, List alerts, int lineNum) { - if (dataStr.Length >= 8) + if (dataStr.Length < 8) { - note.TargetNote = HexToInt(dataStr[6..8]).ToString(); - } - else - { - alerts.Add(new Alert(Warning, $"AIR/ADW 音符缺少目标: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"AIR/ADW 音符缺少目标: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note, tpm) }); } } - private static void ParseAhdData(string dataStr, ChuNote note, List alerts, int lineNum) + private static void ParseAhdData(string dataStr, ChuNote note, int tpm, List alerts, int lineNum) { if (dataStr.Length >= 10) { - note.AirHoldDuration = HexToInt(dataStr[6..10]); + note.Duration = new Rational(HexToInt(dataStr[6..10]), tpm); } else { - alerts.Add(new Alert(Warning, $"AHD 音符缺少时长: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"AHD 音符缺少时长: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note, tpm) }); } } @@ -239,8 +238,9 @@ private static string Unquote(string s) return trimmed; } - private static string FormatNoteRef(ChuNote note) + private static string FormatNoteRef(ChuNote note, int tpm) { - return $"#{note.Measure:X2}{note.Offset:X2}:{note.Type}"; + var (m, o) = Utils.BarAndTick(note.Time, tpm); + return $"#{m:X2}{o:X3}:{note.Type}"; } } diff --git a/parser/chu/UgcParser.cs b/parser/chu/UgcParser.cs index 90dd03c..0b78c4d 100644 --- a/parser/chu/UgcParser.cs +++ b/parser/chu/UgcParser.cs @@ -2,16 +2,18 @@ using MuConvert.chart; using MuConvert.parser; using MuConvert.utils; +using Rationals; using static MuConvert.utils.Alert.LEVEL; namespace MuConvert.chu; /** - * UGC 格式解析器(UMIGURI 格式,@TICKS=480 tick/拍)。 - * @HEADER 标签 + #measure'tick:code 音符格式。 + * UMIGURI语法文档: https://gist.github.com/inonote/5c01e73781cab17765a1d93641d52298 */ -public class UgcParser : IParser +public class UgcParser: BaseChuParser { + private static int RSL = 480 * 4; + private static readonly Dictionary AirDirections = new() { ["UC"] = "AIR", @@ -20,7 +22,6 @@ public class UgcParser : IParser ["DC"] = "ADW", ["DR"] = "ADR", ["DL"] = "ADL", - ["HD"] = "AHD", }; private static readonly Dictionary ChrExtras = new() @@ -29,10 +30,16 @@ public class UgcParser : IParser ["D"] = "DW", ["C"] = "CE", }; + + private static readonly Dictionary AirColor = new() + { + ["N"] = "DEF", + ["I"] = "I", // TODO 搞清楚UGC里的'I'颜色,在C2S里,对应的字符串是什么 + }; - public (UgcChart, List) Parse(string text) + public override (ChuChart, List) Parse(string text) { - var chart = new UgcChart(); + var chart = new ChuChart(); var alerts = new List(); var lines = text.Replace("\r\n", "\n").Split('\n'); var inHeader = true; @@ -60,10 +67,28 @@ public class UgcParser : IParser } } + FinalizeUgcSflDurations(chart); + FillAllPrevious(chart, alerts); return (chart, alerts); } - private static void ParseHeaderLine(string line, UgcChart chart, List alerts, int lineNum) + private static void FinalizeUgcSflDurations(ChuChart chart) + { + if (chart.SflList.Count == 0) return; + chart.SflList = chart.SflList.OrderBy(s => s.Time).ToList(); + var endTime = Utils.Max(chart.SflList[^1].Time, chart.Notes.Max(x=>x.EndTime)); + + for (var i = 0; i < chart.SflList.Count; i++) + { + var t = chart.SflList[i].Time; + var dur = (i < chart.SflList.Count - 1 ? chart.SflList[i+1].Time : endTime) - t; + chart.SflList[i] = chart.SflList[i] with { Duration = dur.CanonicalForm }; + } + + chart.SflList = chart.SflList.Where(x => x.Multiplier != 1).ToList(); // 倍率为1的,没必要放进来的 + } + + private static void ParseHeaderLine(string line, ChuChart chart, List alerts, int lineNum) { if (!line.StartsWith('@')) { @@ -77,10 +102,6 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale switch (tag) { - case "@VER": - chart.Version = value; - break; - case "@TITLE": chart.Title = value; break; @@ -96,43 +117,41 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale case "@DIFF": if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var diff)) { - chart.Difficulty = diff switch - { - 0 => "BASIC", - 1 => "ADVANCED", - 2 => "EXPERT", - 3 => "MASTER", - 4 => "ULTIMA", - _ => value, - }; + chart.Difficulty = diff; } else { - chart.Difficulty = value; + chart.Difficulty = new string(value.Where(char.IsLetter).ToArray()).ToUpperInvariant() switch + { + "BASIC" => 0, + "ADVANCED" => 1, + "EXPERT" => 2, + "MASTER" => 3, + "WORLDSEND" => 4, + "ULTIMA" => 5, + _ => 3, + }; } break; case "@LEVEL": - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var level)) - chart.Level = level; - else - alerts.Add(new Alert(Warning, $"@LEVEL 格式错误: {line}") { Line = lineNum }); + chart.DisplayLevel = value; break; case "@CONST": - if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var constant)) - chart.Constant = constant; + if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var constant)) + chart.Level = constant; else alerts.Add(new Alert(Warning, $"@CONST 格式错误: {line}") { Line = lineNum }); break; case "@SONGID": - chart.SongId = value; + chart.MusicId = value; break; case "@TICKS": if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ticks)) - chart.TicksPerBeat = ticks; + RSL = ticks * 4; else alerts.Add(new Alert(Warning, $"@TICKS 格式错误: {line}") { Line = lineNum }); break; @@ -144,7 +163,7 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale && int.TryParse(beatParts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var beatNum) && int.TryParse(beatParts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var beatDen)) { - chart.BeatEvents.Add((beatMeasure, beatNum, beatDen)); + chart.MetList.Add(new MET(beatMeasure, beatNum, beatDen)); } else { @@ -159,13 +178,10 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale { var measureOffset = bpmPart[..bpmSpaceIdx]; var bpmValueStr = bpmPart[(bpmSpaceIdx + 1)..]; - var apostropheIdx = measureOffset.IndexOf('\''); - if (apostropheIdx > 0 - && int.TryParse(measureOffset[..apostropheIdx], NumberStyles.Integer, CultureInfo.InvariantCulture, out var bpmMeasure) - && int.TryParse(measureOffset[(apostropheIdx + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out var bpmOffset) - && double.TryParse(bpmValueStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var bpmValue)) + if (TryParseUgcMeasureTick(measureOffset, out var bpmMeasure, out var bpmOffset) + && decimal.TryParse(bpmValueStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var bpmValue)) { - chart.BpmEvents.Add((bpmMeasure, bpmOffset, bpmValue)); + chart.BpmList.Add(new BPM(bpmMeasure + new Rational(bpmOffset, RSL), bpmValue)); } else { @@ -179,20 +195,25 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale break; // silently ignored metadata tags - case "@EXVER": case "@SORT": case "@BGM": case "@BGMOFS": case "@BGMPRV": + case "@VER": case "@EXVER": case "@SORT": case "@BGM": case "@BGMOFS": case "@BGMPRV": case "@JACKET": case "@BGIMG": case "@BGMODE": case "@FLDCOL": case "@FLDIMG": case "@FLAG": case "@ATINFO": case "@DLURL": case "@COPYRIGHT": case "@LICENSE": - case "@MAINTIL": + case "@MAINTIL": case "@TIL": break; - case "@TIL": case "@SPDMOD": + case "@SPDMOD": + { + var parts = value.Split('\t', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (parts.Length >= 2 + && TryParseUgcMeasureTick(parts[0], out var meas, out var tick) + && decimal.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var mult)) { - var parts = value.Split(['\t', ' '], StringSplitOptions.RemoveEmptyEntries); - if (parts.Length >= 2 && int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var tilMeasure) - && double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var tilMult)) - chart.SpeedEvents.Add((tilMeasure, 0, tilMult)); + chart.SflList.Add((meas + new Rational(tick, RSL), Rational.Zero, mult)); } + else + alerts.Add(new Alert(Warning, $"@SPDMOD 格式错误: {line}") { Line = lineNum }); break; + } default: alerts.Add(new Alert(Info, $"未知头部标签: {tag}") { Line = lineNum }); @@ -200,7 +221,21 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale } } - private static int ParseNoteLine(string[] lines, int idx, UgcChart chart, List alerts) + /** UGC 时刻字符串 measure'tick(@BPM、@SPDMOD、音符行 #m't 共用)。 */ + private static bool TryParseUgcMeasureTick(string measureTick, out int measure, out int tick) + { + measure = 0; + tick = 0; + measureTick = measureTick.Trim(); + var ap = measureTick.IndexOf('\''); + if (ap <= 0) + return false; + + return int.TryParse(measureTick[..ap], NumberStyles.Integer, CultureInfo.InvariantCulture, out measure) + && int.TryParse(measureTick[(ap + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out tick); + } + + private static int ParseNoteLine(string[] lines, int idx, ChuChart chart, List alerts) { var line = lines[idx]; var lineNum = idx + 1; @@ -209,10 +244,6 @@ private static int ParseNoteLine(string[] lines, int idx, UgcChart chart, Lists") || line.Contains(">c"))) - return idx; - var colonIdx = line.IndexOf(':'); if (colonIdx < 0) { @@ -223,21 +254,15 @@ private static int ParseNoteLine(string[] lines, int idx, UgcChart chart, List 3) note.Tag = code[3..]; break; - case 'c': - return idx; // Margrete Air Crush, silently skip + case 'c': // Umiguri的CLICK音符,疑似在C2s中是没有对应的。这个音符没有Cell和Width,除了Type什么都没有,所以直接存下来就可以了。 + note.Type = "CLICK"; + break; case 'd': note.Type = "MNE"; - ParseCellWidth(code, 1, note, alerts, lineNum); + ParseCellWidth(code, 1, note, alerts, lineNum, chart); break; default: @@ -297,138 +334,172 @@ private static int ParseNoteLine(string[] lines, int idx, UgcChart chart, List alerts, int lineNum) + private static void ParseTapNote(string code, ChuNote note, List alerts, int lineNum, ChuChart chart, bool isCHR) { note.Type = "TAP"; - ParseCellWidth(code, 1, note, alerts, lineNum); + ParseCellWidth(code, 1, note, alerts, lineNum, chart); + if (isCHR) + { + note.Type = "CHR"; + var extraRaw = code.Length > 3 ? code[3..] : ""; + note.Tag = ChrExtras.GetValueOrDefault(extraRaw, extraRaw); + } } - private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote note, List alerts) + private static int ParseHoldNote(bool isAirHold, string[] lines, int idx, string code, ChuNote note, List alerts, ChuChart chart) { - note.Type = "HLD"; - ParseCellWidth(code, 1, note, alerts, idx + 1); + note.Type = isAirHold ? "AHD" : "HLD"; + ParseCellWidth(code, 1, note, alerts, idx + 1, chart); + + if (isAirHold) + { + var colorChar = code.Last(); // 颜色标记 N/I + note.Tag = AirColor.GetValueOrDefault(colorChar.ToString(), ""); + } bool foundFirst = false; while (idx + 1 < lines.Length) { var nextLine = lines[idx + 1].Trim(); - if (!TryParseFollowerLine(nextLine, out var duration, out _, out _)) + if (!TryParseFollowerLine(nextLine, out var marker, out var duration, out _, out _, out _, false)) { if (nextLine.StartsWith('\'') || nextLine.StartsWith('@')) { idx++; continue; } break; } - note.HoldDuration += duration; + note.Duration += new Rational(duration, RSL); + if (isAirHold && marker == "c") note.Type = "AHX"; // 可能是对应于UMIGURI文档中的 AirHold的 AIR-ACTION 无し终点 idx++; foundFirst = true; } if (!foundFirst) - alerts.Add(new Alert(Warning, $"HLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"HLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = lines[idx] }); return idx; } - private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote note, List alerts) + private static int ParseSlideNote(bool isAirSlide, string[] lines, int idx, string code, ChuNote previousNote, List alerts, ChuChart chart) { - note.Type = "SLD"; - ParseCellWidth(code, 1, note, alerts, idx + 1); + // 注:一开始从外面传进来的previousNote,最后并不会被添加进chart里,只是作为第一段的起点参照而已。 + var startTime = previousNote.Time; + ParseCellWidth(code, 1, previousNote, alerts, idx + 1, chart); + previousNote.EndCell = previousNote.Cell; + previousNote.EndWidth = previousNote.Width; + + string colorTag = ""; + if (isAirSlide) + { + var colorChar = code.Last(); // 颜色标记 N/I + colorTag = AirColor.GetValueOrDefault(colorChar.ToString(), ""); + // 解析高度数据。目前只解析、不使用。 + TryParseUgcBase36Int2(code.AsSpan(3, code.Length - 4), out _); // out var startHeight 起始的高度值 + } bool foundFirst = false; while (idx + 1 < lines.Length) - { + { // 循环处理所有的跟随行。idx始终指向上一条已经处理完的行。 var nextLine = lines[idx + 1].Trim(); - if (!TryParseFollowerLine(nextLine, out var duration, out var endCell, out var endWidth)) + if (!TryParseFollowerLine(nextLine, out var marker, out var duration, out var endCell, out var endWidth, out _, true)) { if (nextLine.StartsWith('\'') || nextLine.StartsWith('@')) { idx++; continue; } break; } - note.SlideDuration += duration; - note.EndCell = endCell; - note.EndWidth = endWidth; + var type = isAirSlide ? (marker == "s" ? "ASD" : "ASC") : (marker == "s" ? "SLD" : "SLC"); + + var segmentEnd = startTime + new Rational(duration, RSL); + var note = new ChuNote + { + Type = type, Time = previousNote.EndTime, + Cell = previousNote.EndCell, Width = previousNote.EndWidth, + Duration = segmentEnd - previousNote.EndTime, + EndCell = endCell, EndWidth = endWidth, + Previous = foundFirst ? previousNote : null, + }; + if (isAirSlide) note.Tag = colorTag; + + chart.Notes.Add(note); + previousNote = note; idx++; foundFirst = true; } if (!foundFirst) - alerts.Add(new Alert(Warning, $"SLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"SLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = lines[idx] }); return idx; } - - private static bool TryParseStandaloneFollower(string[] lines, int idx, UgcChart chart, List alerts) + + private static bool TryParseUgcBase36Int2(ReadOnlySpan twoChars, out int value) { - var line = lines[idx]; - if (!line.StartsWith('#') || !line.Contains(">s") && !line.Contains(">c")) return false; - - if (!TryParseFollowerLine(line, out var duration, out var endCell, out var endWidth)) return false; - - // find the last SLD or HLD note and attach duration - for (int i = chart.Notes.Count - 1; i >= 0; i--) + value = 0; + if (twoChars.Length == 0) return false; + else if (twoChars.Length == 1) { - var n = chart.Notes[i]; - if (n.Type is "SLD" or "HLD") - { - if (n.Type == "SLD") { n.SlideDuration = duration; n.EndCell = endCell; n.EndWidth = endWidth; } - else { n.HoldDuration = duration; } - return true; - } + if (!TryHexCharToInt(twoChars[0], out value)) return false; } - return false; + else + { + if (!TryHexCharToInt(twoChars[0], out var hi) || !TryHexCharToInt(twoChars[1], out var lo)) return false; + value = hi * 36 + lo; + } + return true; } - - private static bool TryParseFollowerLine(string line, out int duration, out int endCell, out int endWidth, bool requireEndCellWidth = false) + + private static bool TryParseFollowerLine(string line, out string marker, out int endTick, out int endCell, out int endWidth, out int? height, bool requireEndCellWidth) { - duration = 0; + endTick = 0; endCell = 0; endWidth = 1; + marker = ""; + height = null; if (!line.StartsWith('#')) return false; // support both >s (SLD) and >c (SLC) follower lines - int gtIdx = -1; - int markerLen = 0; - if (line.Contains(">s")) { gtIdx = line.IndexOf(">s"); markerLen = 2; } - else if (line.Contains(">c")) { gtIdx = line.IndexOf(">c"); markerLen = 2; } - if (gtIdx < 1) return false; + int sepIdx = line.IndexOfAny(['>', ':']); + if (sepIdx < 1) return false; + marker = line[sepIdx+1].ToString(); + int markerLen = 2; - var durationStr = line[1..gtIdx]; - if (!int.TryParse(durationStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out duration)) return false; + var endTickStr = line[1..sepIdx]; + if (!int.TryParse(endTickStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out endTick)) return false; - var afterMarker = line[(gtIdx + markerLen)..]; + var afterMarker = line[(sepIdx + markerLen)..]; if (afterMarker.Length >= 2) { endCell = HexCharToInt(afterMarker[0]); - endWidth = WidthHexCharToInt(afterMarker[1]); + endWidth = HexCharToInt(afterMarker[1]); } else if (requireEndCellWidth) return false; + if (afterMarker.Length > 2 && TryParseUgcBase36Int2(afterMarker.AsSpan()[2..], out var heightV)) height = heightV; + return true; } - private static void ParseCellWidth(string code, int startIdx, ChuNote note, List alerts, int lineNum) + private static void ParseCellWidth(string code, int startIdx, ChuNote note, List alerts, int lineNum, ChuChart chart) { if (code.Length > startIdx) { note.Cell = HexCharToInt(code[startIdx]); if (code.Length > startIdx + 1) - note.Width = WidthHexCharToInt(code[startIdx + 1]); + note.Width = HexCharToInt(code[startIdx + 1]); else - alerts.Add(new Alert(Warning, $"音符缺少 width: {code}") { Line = lineNum, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"音符缺少 width: {code}") { Line = lineNum, RelevantNote = FormatNoteRef(note, code) }); } else { - alerts.Add(new Alert(Warning, $"音符缺少 cell 和 width: {code}") { Line = lineNum, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"音符缺少 cell 和 width: {code}") { Line = lineNum, RelevantNote = FormatNoteRef(note, code) }); } } - private static void ParseAirNote(string code, ChuNote note, List alerts, int lineNum) + private static void ParseAirNote(string code, ChuNote note, List alerts, int lineNum, ChuChart chart) { - // Matches UgcGenerator: "a" + cell + width + two-letter direction + targetNote [ + "_" + airHoldDuration for AHD ] if (code.Length < 5) { alerts.Add(new Alert(Warning, $"AIR 音符代码过短: {code}") { Line = lineNum }); @@ -436,10 +507,8 @@ private static void ParseAirNote(string code, ChuNote note, List alerts, return; } - ParseCellWidth(code, 1, note, alerts, lineNum); - var afterCellWidth = code[3..]; - var underscoreIdx = afterCellWidth.IndexOf('_'); - var mainPart = underscoreIdx >= 0 ? afterCellWidth[..underscoreIdx] : afterCellWidth; + ParseCellWidth(code, 1, note, alerts, lineNum, chart); + var mainPart = code[3..5]; if (mainPart.Length < 2) { @@ -456,61 +525,58 @@ private static void ParseAirNote(string code, ChuNote note, List alerts, else { note.Type = "AIR"; - alerts.Add(new Alert(Warning, $"未知的 AIR 方向: {dir}") { Line = lineNum, RelevantNote = FormatNoteRef(note) }); - } - - note.TargetNote = mainPart.Length > 2 ? mainPart[2..] : "N"; - - if (underscoreIdx >= 0 && note.Type == "AHD") - { - var durStr = afterCellWidth[(underscoreIdx + 1)..]; - if (int.TryParse(durStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ahdDuration)) - note.AirHoldDuration = ahdDuration; + alerts.Add(new Alert(Warning, $"未知的 AIR 方向: {dir}") { Line = lineNum, RelevantNote = FormatNoteRef(note, code) }); } } - private static void ParseChrNote(string code, ChuNote note, List alerts, int lineNum) + private static int ParseAirCrushNote(string[] lines, int idx, string code, ChuNote previousNote, List alerts, ChuChart chart) { - note.Type = "CHR"; - if (code.Length < 3) - { - alerts.Add(new Alert(Warning, $"CHR 音符代码过短: {code}") { Line = lineNum }); - return; + // TODO 尚未实现,所以先给个警告 + alerts.Add(new Alert(Warning, "当前版本尚未实现对Air-Crush(UMIGURI的':C'或':T'音符)的解析。") { Line = idx, RelevantNote = lines[idx] }); + + bool foundFirst = false; + while (idx + 1 < lines.Length) + { // 循环处理所有的跟随行。idx始终指向上一条已经处理完的行。 + var nextLine = lines[idx + 1].Trim(); + if (!TryParseFollowerLine(nextLine, out var marker, out var duration, out _, out _, out _, false)) + { + if (nextLine.StartsWith('\'') || nextLine.StartsWith('@')) { idx++; continue; } + break; + } + + // TODO 尚未实现 + idx++; + foundFirst = true; } - ParseCellWidth(code, 1, note, alerts, lineNum); - var extraRaw = code.Length > 3 ? code[3..] : ""; - if (ChrExtras.TryGetValue(extraRaw, out var chrDir)) - note.Tag = chrDir; - else - note.Tag = extraRaw; + if (!foundFirst) + alerts.Add(new Alert(Warning, $"air-crush 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = lines[idx] }); + return idx; } private static int HexCharToInt(char c) { - return c switch - { - >= '0' and <= '9' => c - '0', - >= 'A' and <= 'F' => c - 'A' + 10, - >= 'a' and <= 'f' => c - 'a' + 10, - _ => 0, - }; + if (!TryHexCharToInt(c, out var result)) result = 0; + return result; } - private static int WidthHexCharToInt(char c) + private static bool TryHexCharToInt(char c, out int result) { - return c switch + result = c switch { - >= '1' and <= '9' => c - '1' + 1, - >= 'A' and <= 'G' => c - 'A' + 10, - >= 'a' and <= 'g' => c - 'a' + 10, - _ => 1, + >= '0' and <= '9' => c - '0', + >= 'A' and <= 'Z' => c - 'A' + 10, + >= 'a' and <= 'z' => c - 'a' + 10, + _ => -1, }; + return result >= 0; } - private static string FormatNoteRef(ChuNote note) + // ReSharper disable once UnusedParameter.Local + private static string FormatNoteRef(ChuNote note, string code) { - return $"#{note.Measure}'{note.Offset}:{note.Type}"; + var (m, o) = Utils.BarAndTick(note.Time, RSL); + return $"#{m}'{o}:{code}"; } } diff --git a/tests/chu/ChuTests.cs b/tests/chu/ChuTests.cs index 46c12f3..705f9be 100644 --- a/tests/chu/ChuTests.cs +++ b/tests/chu/ChuTests.cs @@ -1,5 +1,7 @@ using System.Reflection; using MuConvert.chu; +using MuConvert.utils; +using Rationals; namespace MuConvert.Tests.chu; @@ -17,7 +19,6 @@ public void CanParseOfficialC2S() if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}"); var (chart, _) = new C2sParser().Parse(File.ReadAllText(C2sPath)); Assert.NotEmpty(chart.Notes); - Assert.Equal(384, chart.Resolution); } [Fact] @@ -46,48 +47,99 @@ public void C2sRoundTrip() /// /// Builds a stable, comparable string from a note's public instance properties (name-sorted) /// so round-trip tests verify no field loss without hard-coding each property in the test. + /// Omits and (redundant with Time/Duration or not stable across formats). /// private static string SnapshotNote(ChuNote note) { - var props = typeof(ChuNote).GetProperties(BindingFlags.Instance | BindingFlags.Public); - var parts = props + static string F(object? v) => v switch + { + Rational r => r.CanonicalForm.ToString(), + List list => string.Join(",", list), + string s => s == "DEF" ? "" : s, + null => "", + _ => v.ToString() ?? "", + }; + + var propParts = typeof(ChuNote).GetProperties(BindingFlags.Instance | BindingFlags.Public) .OrderBy(p => p.Name) - .Select(p => $"{p.Name}={p.GetValue(note)}"); - return string.Join("|", parts); + .Where(p => p.Name != nameof(ChuNote.EndTime)) + .Select(p => $"{p.Name}={F(p.GetValue(note))}"); + var fieldParts = typeof(ChuNote).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) + .OrderBy(f => f.Name) + .Where(f => f.Name != nameof(ChuNote.ExtraData)) + .Select(f => $"{f.Name}={F(f.GetValue(note))}"); + return string.Join("|", propParts.Concat(fieldParts)); } /// - /// Same tick scaling as when converting UGC → C2S (384 ticks per measure). + /// 将 UGC 网格上的 的 Time / Duration 投影为「经 C2S 生成器写出再解析」后等价的分数(C2S 小节 tick = )。 /// - private static ChuNote UgcNoteScaledToC2sTicks(ChuNote n, int ticksPerBeat) + private static ChuNote UgcNoteScaledToC2sTicks(ChuNote n, int ugcTicksPerBeat, int c2sResolution) { - const int c2sResolution = 384; - int scaleDown(int v) => (int)((long)v * (c2sResolution / 4) / ticksPerBeat); - return new ChuNote - { - Type = n.Type, Measure = n.Measure, Offset = scaleDown(n.Offset), - Cell = n.Cell, Width = n.Width, - HoldDuration = scaleDown(n.HoldDuration), SlideDuration = scaleDown(n.SlideDuration), - EndCell = n.EndCell, EndWidth = n.EndWidth, - Tag = n.Tag, TargetNote = n.TargetNote, AirHoldDuration = scaleDown(n.AirHoldDuration), - StartHeight = n.StartHeight, TargetHeight = n.TargetHeight, NoteColor = n.NoteColor, - }; + var tpmUgc = ugcTicksPerBeat * 4; + var (m, oU) = Utils.BarAndTick(n.Time, tpmUgc); + var oC = (int)((long)oU * c2sResolution / tpmUgc); + var time = m + new Rational(oC, c2sResolution); + var dur = new Rational(Utils.Tick(n.Duration, c2sResolution), c2sResolution); + return CloneChuNoteWithTiming(n, time, dur); } /// - /// Compares UGC-side notes to C2S-side notes in C2S tick space (384): snapshots of - /// for each note vs snapshots of notes. - /// Use with UgcToC2sViaGenerator (source UGC, C2S from generate+parse) or C2sToUgcViaGenerator (UGC from generate+parse, source C2S). + /// 将 C2S 网格上的音符投影为「经 UGC 生成器写出再解析」后等价的分数(UGC 小节 tick = × 4)。 /// - private static void AssertUgcNotesEquivalentToReparsedC2s(UgcChart ugc, C2sChart c2s, bool isUgcReference) + private static ChuNote C2sNoteScaledToUgcTicks(ChuNote n, int ugcTicksPerBeat, int c2sResolution) { - var ugcSnaps = ugc.Notes - .Select(n => SnapshotNote(UgcNoteScaledToC2sTicks(n, ugc.TicksPerBeat))) - .OrderBy(s => s) - .ToArray(); - var c2sSnaps = c2s.Notes.Select(SnapshotNote).OrderBy(s => s).ToArray(); - if (isUgcReference) Assert.Equal(ugcSnaps, c2sSnaps); - else Assert.Equal(c2sSnaps, ugcSnaps); + var tpmUgc = ugcTicksPerBeat * 4; + var (m, oC) = Utils.BarAndTick(n.Time, c2sResolution); + var oU = (int)((long)oC * tpmUgc / c2sResolution); + var time = m + new Rational(oU, tpmUgc); + var dur = new Rational(Utils.Tick(n.Duration, tpmUgc), tpmUgc); + return CloneChuNoteWithTiming(n, time, dur); + } + + private static ChuNote CloneChuNoteWithTiming(ChuNote n, Rational time, Rational duration) => new() + { + Type = n.Type, + Time = time, + Cell = n.Cell, + Width = n.Width, + Duration = duration, + EndCell = n.EndCell, + EndWidth = n.EndWidth, + Previous = n.Previous, + Tag = n.Tag, + ExtraData = [..n.ExtraData], + }; + + /// + /// 比较 UGC 与 C2S 的音符 IR:因 tick 网格不同,在各自「经对方格式写回再解析」的量化意义下比较快照。 + /// + private static void AssertUgcNotesEquivalentToReparsedC2s(ChuChart ugc, ChuChart c2s, bool isUgcReference) + { + if (isUgcReference) + { + var ugcSnaps = ugc.Notes.Where(n=>n.Type != "CLICK") + .OrderBy(n=>n.Time).ThenBy(n=>n.Cell).ThenBy(n=>n.Width).ThenBy(n=>n.Duration).ThenBy(n=>n.Type) + .Select(n => SnapshotNote(UgcNoteScaledToC2sTicks(n, 480, 384))) + .ToArray(); + var c2sSnaps = c2s.Notes + .OrderBy(n=>n.Time).ThenBy(n=>n.Cell).ThenBy(n=>n.Width).ThenBy(n=>n.Duration).ThenBy(n=>n.Type) + .Select(SnapshotNote) + .ToArray(); + Assert.Equal(ugcSnaps, c2sSnaps); + } + else + { + var ugcSnaps = ugc.Notes.Where(n=>n.Type != "CLICK") + .OrderBy(n=>n.Time).ThenBy(n=>n.Cell).ThenBy(n=>n.Width).ThenBy(n=>n.Duration).ThenBy(n=>n.Type) + .Select(SnapshotNote) + .ToArray(); + var c2sSnaps = c2s.Notes + .OrderBy(n=>n.Time).ThenBy(n=>n.Cell).ThenBy(n=>n.Width).ThenBy(n=>n.Duration).ThenBy(n=>n.Type) + .Select(n => SnapshotNote(C2sNoteScaledToUgcTicks(n, 480, 384))) + .ToArray(); + Assert.Equal(c2sSnaps, ugcSnaps); + } } [Fact] @@ -96,7 +148,7 @@ public void CanParseUgc() if (!File.Exists(UgcPath)) throw new SkipException($"Missing: {UgcPath}"); var (chart, _) = new UgcParser().Parse(File.ReadAllText(UgcPath)); Assert.NotEmpty(chart.Notes); - Assert.Equal("MASTER", chart.Difficulty); + Assert.Equal(3, chart.Difficulty); } [Fact] diff --git "a/tests/chu/testset/\345\256\230\350\260\261/Virtual To Live/2098_03.c2s" "b/tests/chu/testset/\345\256\230\350\260\261/Virtual To Live/2098_03.c2s" new file mode 100644 index 0000000..bcaddd7 --- /dev/null +++ "b/tests/chu/testset/\345\256\230\350\260\261/Virtual To Live/2098_03.c2s" @@ -0,0 +1,1599 @@ +VERSION 1.10.01 1.10.01 +MUSIC 0 +SEQUENCEID 0 +DIFFICULT 00 +LEVEL 0.0 +CREATOR うさぎランドリー +BPM_DEF 156.000 156.000 156.000 156.000 +MET_DEF 4 4 +RESOLUTION 384 +CLK_DEF 384 +PROGJUDGE_BPM 240.000 +PROGJUDGE_AER 0.999 +TUTORIAL 0 + +BPM 0 0 156.000 +MET 0 0 4 4 + +CHR 4 288 0 4 UP +CHR 4 288 12 4 UP +HLD 5 0 0 4 2208 +TAP 5 0 6 8 +ASD 5 0 6 8 TAP 5 288 9 2 5 DEF +TAP 6 0 6 8 +ASD 6 0 6 8 TAP 5 288 9 2 5 DEF +TAP 7 0 6 8 +ASD 7 0 6 8 TAP 5 288 9 2 5 DEF +TAP 8 0 6 8 +ASD 8 0 6 8 TAP 5 288 9 2 5 DEF +TAP 9 0 6 8 +ASD 9 0 6 8 TAP 5 288 9 2 5 DEF +TAP 10 0 6 8 +ASD 10 0 6 8 TAP 5 288 9 2 5 DEF +CHR 11 0 0 8 UP +ASC 11 0 0 8 CHR 5 384 7 2 5 DEF +CHR 11 0 8 8 UP +ASC 11 0 8 8 CHR 5 384 7 2 5 DEF +ALD 12 0 0 8 1 5 4 0 8 1 +ALD 12 0 8 8 1 5 4 8 8 1 +CHR 13 0 0 6 UP +TAP 13 0 6 4 +TAP 13 96 6 4 +CHR 13 144 10 6 UP +TAP 13 192 6 4 +CHR 13 288 0 6 UP +TAP 13 288 6 4 +TAP 14 0 6 4 +CHR 14 0 13 3 UP +TAP 14 48 6 4 +CHR 14 48 10 3 UP +CHR 14 96 3 3 UP +TAP 14 96 6 4 +CHR 14 144 0 3 UP +TAP 14 144 6 4 +FLK 14 192 0 3 L +HLD 14 192 12 4 96 +FLK 14 208 3 3 L +FLK 14 224 6 3 L +FLK 14 240 9 3 L +FLK 14 256 6 3 L +FLK 14 272 3 3 L +FLK 14 288 0 3 L +AIR 14 288 12 4 HLD DEF +CHR 15 0 0 4 UP +AHD 15 0 0 4 CHR 96 DEF +CHR 15 0 12 4 UP +TAP 15 72 5 6 +TAP 15 96 0 4 +TAP 15 144 5 6 +CHR 15 192 0 4 UP +AHD 15 192 0 4 CHR 96 DEF +CHR 15 192 12 4 UP +TAP 15 264 5 6 +TAP 15 288 0 4 +TAP 15 336 5 6 +CHR 16 0 0 4 UP +AHD 16 0 0 4 CHR 96 DEF +CHR 16 0 12 4 UP +TAP 16 72 5 6 +TAP 16 96 0 4 +TAP 16 144 5 6 +TAP 16 192 0 4 +TAP 16 240 3 4 +TAP 16 288 6 4 +TAP 16 336 9 4 +CHR 17 0 0 4 UP +CHR 17 0 12 4 UP +AHD 17 0 12 4 CHR 96 DEF +TAP 17 72 5 6 +TAP 17 96 12 4 +TAP 17 144 5 6 +CHR 17 192 0 4 UP +CHR 17 192 12 4 UP +AHD 17 192 12 4 CHR 96 DEF +TAP 17 264 5 6 +TAP 17 288 12 4 +TAP 17 336 5 6 +CHR 18 0 0 4 UP +CHR 18 0 12 4 UP +AHD 18 0 12 4 CHR 96 DEF +TAP 18 72 5 6 +TAP 18 96 12 4 +TAP 18 144 5 6 +TAP 18 192 0 4 +TAP 18 192 12 4 +TAP 18 240 4 4 +TAP 18 240 8 4 +TAP 18 288 0 4 +AIR 18 288 0 4 TAP DEF +TAP 18 288 12 4 +AIR 18 288 12 4 TAP DEF +TAP 19 0 0 4 +AHD 19 0 0 4 TAP 96 DEF +SLC 19 0 12 4 4 11 4 SLD +SLC 19 4 11 4 5 10 4 SLD +SLC 19 9 10 4 5 9 4 SLD +SLC 19 14 9 4 7 8 4 SLD +SLC 19 21 8 4 9 7 4 SLD +SLC 19 30 7 4 14 6 4 SLD +SLC 19 44 6 4 4 6 4 SLD +SLC 19 48 6 4 4 6 4 SLD +SLC 19 52 6 4 14 7 4 SLD +SLC 19 66 7 4 9 8 4 SLD +SLC 19 75 8 4 7 9 4 SLD +SLC 19 82 9 4 5 10 4 SLD +SLC 19 87 10 4 5 11 4 SLD +SLD 19 92 11 4 4 12 4 SLD +SLC 19 96 0 4 4 1 4 SLD +AHD 19 96 12 4 SLD 96 DEF +SLC 19 100 1 4 5 2 4 SLD +SLC 19 105 2 4 5 3 4 SLD +SLC 19 110 3 4 7 4 4 SLD +SLC 19 117 4 4 9 5 4 SLD +SLC 19 126 5 4 14 6 4 SLD +SLC 19 140 6 4 4 6 4 SLD +SLC 19 144 6 4 4 6 4 SLD +SLC 19 148 6 4 14 5 4 SLD +SLC 19 162 5 4 9 4 4 SLD +SLC 19 171 4 4 7 3 4 SLD +SLC 19 178 3 4 5 2 4 SLD +SLC 19 183 2 4 5 1 4 SLD +SLC 19 188 1 4 4 0 4 SLD +SLC 19 192 0 4 96 0 4 SLD +TAP 19 192 12 4 +SLD 19 288 0 4 96 4 4 SLD +TAP 19 288 12 4 +TAP 19 336 10 4 +SLD 20 0 8 4 96 12 4 SLD +TAP 20 48 0 4 +TAP 20 96 2 4 +SLD 20 96 12 4 192 12 4 SLD +SLC 20 144 4 4 15 5 4 SLD +SLC 20 159 5 4 18 6 4 SLD +SLC 20 177 6 4 23 7 4 SLD +SLC 20 200 7 4 30 8 4 SLD +SLD 20 230 8 4 10 8 4 SLD +FLK 20 240 0 8 L +FLK 20 288 4 8 L +HLD 21 0 2 3 96 +HLD 21 0 11 3 96 +TAP 21 96 8 3 +TAP 21 120 5 3 +TAP 21 144 8 3 +TAP 21 192 0 4 +TAP 21 192 12 4 +TAP 21 264 2 4 +TAP 21 264 10 4 +TAP 21 336 5 3 +TAP 21 336 8 3 +TAP 22 0 7 2 +AHD 22 0 7 2 TAP 192 DEF +TAP 23 0 2 3 +TAP 23 0 9 2 +TAP 23 48 9 2 +TAP 23 96 2 3 +TAP 23 96 7 2 +TAP 23 144 5 2 +TAP 23 192 2 3 +HLD 23 192 7 2 96 +TAP 23 288 2 3 +TAP 23 336 7 2 +TAP 24 0 2 3 +TAP 24 0 5 2 +TAP 24 48 7 2 +TAP 24 96 2 3 +TAP 24 96 9 2 +HLD 24 192 2 3 96 +HLD 24 192 7 2 96 +FLK 24 288 9 3 L +TAP 24 336 5 2 +TAP 25 0 5 2 +TAP 25 0 11 3 +TAP 25 48 7 2 +TAP 25 96 9 2 +TAP 25 96 11 3 +TAP 25 144 7 2 +HLD 25 192 7 2 96 +TAP 25 192 11 3 +TAP 25 288 11 3 +TAP 25 336 7 2 +TAP 26 0 5 2 +TAP 26 0 11 3 +TAP 26 48 5 2 +TAP 26 96 7 2 +TAP 26 96 11 3 +TAP 26 144 9 2 +HLD 26 192 7 2 96 +HLD 26 192 11 3 96 +FLK 26 288 4 3 L +TAP 26 336 9 3 +TAP 27 0 3 3 +TAP 27 0 7 2 +TAP 27 48 5 3 +TAP 27 96 7 2 +TAP 27 96 10 3 +TAP 27 144 8 3 +TAP 27 192 3 3 +TAP 27 192 7 2 +TAP 27 288 7 2 +TAP 27 288 10 3 +TAP 27 336 8 3 +TAP 28 0 3 3 +TAP 28 0 7 2 +TAP 28 48 10 3 +TAP 28 96 3 3 +TAP 28 96 7 2 +TAP 28 144 5 3 +TAP 28 192 7 2 +TAP 28 192 10 3 +TAP 28 288 3 3 +TAP 28 288 7 2 +TAP 29 0 7 2 +SLC 29 0 13 3 23 12 3 SLD +SLC 29 23 12 3 27 11 3 SLD +SLC 29 50 11 3 35 10 3 SLD +SLD 29 85 10 3 11 10 3 SLD +TAP 29 96 7 2 +TAP 29 144 10 3 +SLC 29 192 0 3 23 1 3 SLD +TAP 29 192 7 2 +SLC 29 215 1 3 27 2 3 SLD +SLC 29 242 2 3 35 3 3 SLD +SLD 29 277 3 3 11 3 3 SLD +TAP 29 288 7 2 +TAP 29 336 3 3 +SLC 30 0 6 4 15 4 4 SLD +SLC 30 0 12 4 8 11 4 SLD +SLC 30 8 11 4 8 10 4 SLD +SLC 30 15 4 4 10 3 4 SLD +SLC 30 16 10 4 10 9 4 SLD +SLC 30 25 3 4 12 2 4 SLD +SLC 30 26 9 4 13 8 4 SLD +SLC 30 37 2 4 17 1 4 SLD +SLC 30 39 8 4 18 7 4 SLD +SLC 30 54 1 4 30 0 4 SLD +SLC 30 57 7 4 28 6 4 SLD +SLC 30 84 0 4 12 0 4 SLD +SLC 30 85 6 4 11 6 4 SLD +SLC 30 96 0 4 11 0 4 SLD +SLC 30 96 6 4 11 6 4 SLD +SLC 30 107 0 4 23 1 4 SLD +SLC 30 107 6 4 28 7 4 SLD +SLC 30 130 1 4 13 2 4 SLD +SLC 30 135 7 4 18 8 4 SLD +SLC 30 143 2 4 10 3 4 SLD +SLC 30 153 3 4 9 4 4 SLD +SLC 30 153 8 4 13 9 4 SLD +SLC 30 162 4 4 16 6 4 SLD +SLC 30 166 9 4 10 10 4 SLD +SLC 30 176 10 4 8 11 4 SLD +SLD 30 178 6 4 14 8 4 SLD +SLC 30 184 11 4 8 12 4 SLD +AHD 30 192 8 4 SLD 96 DEF +SLD 30 192 12 4 96 12 4 SLD +FLK 30 288 4 8 L +ADL 30 288 4 8 FLK DEF +TAP 31 0 9 6 +TAP 31 48 4 4 +TAP 31 96 2 4 +AUL 31 96 2 4 TAP DEF +TAP 31 144 8 4 +TAP 31 192 10 4 +AUR 31 192 10 4 TAP DEF +TAP 31 288 0 4 +TAP 31 288 6 4 +TAP 31 336 0 4 +TAP 31 336 6 4 +TAP 32 0 3 4 +TAP 32 0 9 4 +TAP 32 48 3 4 +TAP 32 48 9 4 +TAP 32 96 6 4 +AUL 32 96 6 4 TAP DEF +TAP 32 96 12 4 +AIR 32 96 12 4 TAP DEF +TAP 32 192 0 4 +SLC 32 192 0 4 6 0 5 SLD +ADW 32 192 0 4 TAP DEF +TAP 32 192 12 4 +SLC 32 192 12 4 6 11 5 SLD +ADW 32 192 12 4 TAP DEF +SLC 32 198 0 5 6 0 4 SLD +SLC 32 198 11 5 6 12 4 SLD +SLC 32 204 0 4 6 0 5 SLD +SLC 32 204 12 4 6 11 5 SLD +SLC 32 210 0 5 6 0 4 SLD +SLC 32 210 11 5 6 12 4 SLD +SLC 32 216 0 4 6 0 5 SLD +SLC 32 216 12 4 6 11 5 SLD +SLC 32 222 0 5 6 0 4 SLD +SLC 32 222 11 5 6 12 4 SLD +SLC 32 228 0 4 6 0 5 SLD +SLC 32 228 12 4 6 11 5 SLD +SLC 32 234 0 5 6 0 4 SLD +SLC 32 234 11 5 6 12 4 SLD +SLC 32 240 0 4 6 0 5 SLD +SLC 32 240 12 4 6 11 5 SLD +SLC 32 246 0 5 6 0 4 SLD +SLC 32 246 11 5 6 12 4 SLD +SLC 32 252 0 4 6 0 5 SLD +SLC 32 252 12 4 6 11 5 SLD +SLC 32 258 0 5 6 0 4 SLD +SLC 32 258 11 5 6 12 4 SLD +SLC 32 264 0 4 6 0 5 SLD +SLC 32 264 12 4 6 11 5 SLD +SLC 32 270 0 5 6 0 4 SLD +SLC 32 270 11 5 6 12 4 SLD +SLC 32 276 0 4 6 0 5 SLD +SLC 32 276 12 4 6 11 5 SLD +SLD 32 282 0 5 6 0 4 SLD +SLD 32 282 11 5 6 12 4 SLD +TAP 32 336 2 4 +SLC 33 0 2 4 192 2 4 SLD +TAP 33 48 6 4 +SLC 33 96 6 4 96 6 4 SLD +TAP 33 144 10 4 +SLC 33 192 2 4 24 10 4 SLD +SLC 33 192 6 4 24 10 4 SLD +HLD 33 192 10 4 384 +SLD 33 216 10 4 360 10 4 SLD +SLD 33 216 10 4 360 10 4 SLD +SLC 33 288 5 4 24 10 4 SLD +SLC 33 312 10 4 263 10 4 SLD +SLC 34 0 3 4 24 9 4 SLD +SLD 34 24 9 4 168 9 4 SLD +SLC 34 96 1 4 24 8 6 SLD +SLD 34 120 8 6 72 8 6 SLD +SLD 34 191 10 4 1 8 4 SLD +HLD 34 192 0 4 96 +AHD 34 192 8 4 SLD 96 DEF +AIR 34 192 8 6 SLD DEF +AIR 34 192 9 4 SLD DEF +AIR 34 192 10 4 SLD DEF +AIR 34 192 10 4 HLD DEF +AIR 34 192 10 4 SLD DEF +FLK 34 288 8 8 L +ADR 34 288 8 8 FLK DEF +TAP 35 0 1 6 +TAP 35 48 8 4 +TAP 35 96 10 4 +AUR 35 96 10 4 TAP DEF +TAP 35 144 4 4 +TAP 35 192 2 4 +AUL 35 192 2 4 TAP DEF +TAP 35 288 6 4 +TAP 35 288 12 4 +TAP 35 336 6 4 +TAP 35 336 12 4 +TAP 36 0 3 4 +TAP 36 0 9 4 +TAP 36 48 3 4 +TAP 36 48 9 4 +TAP 36 96 0 4 +AUR 36 96 0 4 TAP DEF +TAP 36 96 6 4 +AUR 36 96 6 4 TAP DEF +TAP 36 192 6 4 +TAP 36 192 12 4 +TAP 36 288 0 4 +TAP 36 288 6 4 +SLC 37 0 6 4 96 0 4 SLD +SLD 37 0 12 4 96 6 4 SLD +SLD 37 96 0 4 192 0 4 SLD +AHD 37 96 6 4 SLD 96 DEF +SLD 37 192 6 4 96 12 4 SLD +ASD 37 288 0 4 SLD 5 96 0 4 5 DEF +ASD 37 288 12 4 SLD 5 96 12 4 5 DEF +ASC 38 0 0 4 ASD 5 20 1 4 5 DEF +ASC 38 0 12 4 ASD 5 20 11 4 5 DEF +ASC 38 20 1 4 ASC 5 24 2 4 5 DEF +ASC 38 20 11 4 ASC 5 24 10 4 5 DEF +ASC 38 44 2 4 ASC 5 31 3 4 5 DEF +ASC 38 44 10 4 ASC 5 31 9 4 5 DEF +ASC 38 75 3 4 ASC 5 42 4 4 5 DEF +ASC 38 75 9 4 ASC 5 42 8 4 5 DEF +ASC 38 117 4 4 ASC 5 57 5 4 5 DEF +ASC 38 117 8 4 ASC 5 57 7 4 5 DEF +ASD 38 174 5 4 ASC 5 114 5 4 5 DEF +ASD 38 174 7 4 ASC 5 114 7 4 5 DEF +ALD 38 288 4 4 1 5 2 4 4 1 +ALD 38 288 8 4 1 5 2 8 4 1 +SLC 39 0 6 4 7 5 4 SLD +SLC 39 0 12 4 7 11 4 SLD +SLC 39 7 5 4 8 4 4 SLD +SLC 39 7 11 4 8 10 4 SLD +SLC 39 15 4 4 9 3 4 SLD +SLC 39 15 10 4 9 9 4 SLD +SLC 39 24 3 4 13 2 4 SLD +SLC 39 24 9 4 13 8 4 SLD +SLC 39 37 2 4 15 1 4 SLD +SLC 39 37 8 4 15 7 4 SLD +SLC 39 52 1 4 28 0 4 SLD +SLC 39 52 7 4 28 6 4 SLD +SLD 39 80 0 4 208 0 4 SLD +SLD 39 80 6 4 16 6 4 SLD +TAP 39 144 6 4 +SLD 39 192 6 4 96 7 2 SLD +AHD 39 288 0 4 SLD 96 DEF +FLK 39 288 4 3 L +AUR 39 288 4 3 FLK DEF +AHD 39 288 7 2 SLD 96 DEF +FLK 39 288 9 3 L +AUR 39 288 9 3 FLK DEF +SLC 40 0 0 4 7 1 4 SLD +SLC 40 0 6 4 7 7 4 SLD +SLC 40 7 1 4 8 2 4 SLD +SLC 40 7 7 4 8 8 4 SLD +SLC 40 15 2 4 9 3 4 SLD +SLC 40 15 8 4 9 9 4 SLD +SLC 40 24 3 4 13 4 4 SLD +SLC 40 24 9 4 13 10 4 SLD +SLC 40 37 4 4 15 5 4 SLD +SLC 40 37 10 4 15 11 4 SLD +SLC 40 52 5 4 28 6 4 SLD +SLC 40 52 11 4 28 12 4 SLD +SLD 40 80 6 4 16 6 4 SLD +SLD 40 80 12 4 208 12 4 SLD +TAP 40 144 6 4 +SLD 40 192 6 4 96 7 2 SLD +FLK 40 288 4 3 L +AUL 40 288 4 3 FLK DEF +AHD 40 288 7 2 SLD 96 DEF +FLK 40 288 9 3 L +AUL 40 288 9 3 FLK DEF +AHD 40 288 12 4 SLD 96 DEF +SLC 41 0 6 4 7 5 4 SLD +SLC 41 0 12 4 7 11 4 SLD +SLC 41 7 5 4 8 4 4 SLD +SLC 41 7 11 4 8 10 4 SLD +SLC 41 15 4 4 9 3 4 SLD +SLC 41 15 10 4 9 9 4 SLD +SLC 41 24 3 4 13 2 4 SLD +SLC 41 24 9 4 13 8 4 SLD +SLC 41 37 2 4 15 1 4 SLD +SLC 41 37 8 4 15 7 4 SLD +SLC 41 52 1 4 28 0 4 SLD +SLC 41 52 7 4 28 6 4 SLD +SLD 41 80 0 4 208 0 4 SLD +SLD 41 80 6 4 16 6 4 SLD +TAP 41 144 6 4 +SLD 41 192 6 4 96 7 2 SLD +FLK 41 288 4 3 L +FLK 41 288 9 3 L +TAP 42 0 2 4 +AHD 42 0 2 4 TAP 96 DEF +TAP 42 0 10 4 +TAP 42 48 6 4 +TAP 42 96 2 4 +TAP 42 96 10 4 +AHD 42 96 10 4 TAP 96 DEF +TAP 42 144 6 4 +CHR 42 192 0 8 UP +CHR 42 192 10 4 UP +TAP 42 288 0 4 +TAP 42 288 6 4 +TAP 42 336 4 4 +TAP 42 336 10 4 +SLC 43 0 6 4 7 5 4 SLD +SLC 43 0 12 4 7 11 4 SLD +SLC 43 7 5 4 8 4 4 SLD +SLC 43 7 11 4 8 10 4 SLD +SLC 43 15 4 4 9 3 4 SLD +SLC 43 15 10 4 9 9 4 SLD +SLC 43 24 3 4 13 2 4 SLD +SLC 43 24 9 4 13 8 4 SLD +SLC 43 37 2 4 15 1 4 SLD +SLC 43 37 8 4 15 7 4 SLD +SLC 43 52 1 4 28 0 4 SLD +SLC 43 52 7 4 28 6 4 SLD +SLD 43 80 0 4 208 0 4 SLD +SLD 43 80 6 4 16 6 4 SLD +TAP 43 144 6 4 +SLD 43 192 6 4 96 7 2 SLD +AHD 43 288 0 4 SLD 96 DEF +FLK 43 288 4 3 L +AUR 43 288 4 3 FLK DEF +AHD 43 288 7 2 SLD 96 DEF +FLK 43 288 9 3 L +AUR 43 288 9 3 FLK DEF +SLC 44 0 0 4 7 1 4 SLD +SLC 44 0 6 4 7 7 4 SLD +SLC 44 7 1 4 8 2 4 SLD +SLC 44 7 7 4 8 8 4 SLD +SLC 44 15 2 4 9 3 4 SLD +SLC 44 15 8 4 9 9 4 SLD +SLC 44 24 3 4 13 4 4 SLD +SLC 44 24 9 4 13 10 4 SLD +SLC 44 37 4 4 15 5 4 SLD +SLC 44 37 10 4 15 11 4 SLD +SLC 44 52 5 4 28 6 4 SLD +SLC 44 52 11 4 28 12 4 SLD +SLD 44 80 6 4 16 6 4 SLD +SLD 44 80 12 4 208 12 4 SLD +TAP 44 144 6 4 +SLD 44 192 6 4 96 7 2 SLD +FLK 44 288 4 3 L +FLK 44 288 9 3 L +TAP 45 0 0 4 +SLD 45 0 7 8 96 1 6 SLD +SLD 45 96 1 6 96 9 5 SLD +TAP 45 96 11 4 +TAP 45 192 2 4 +SLD 45 192 9 5 96 2 4 SLD +SLD 45 288 2 4 96 11 2 SLD +TAP 45 288 9 4 +SLC 46 0 3 2 96 3 2 SLD +SLC 46 0 11 2 96 11 2 SLD +SLD 46 96 3 2 96 0 16 SLD +SLD 46 96 11 2 96 0 16 SLD +AHD 46 192 0 16 SLD 96 DEF +AHD 46 192 0 16 SLD 96 DEF +CHR 46 288 0 16 UP +ADW 46 288 0 16 CHR DEF +CHR 47 0 0 4 UP +AHD 47 0 0 4 CHR 96 DEF +CHR 47 0 12 4 UP +TAP 47 72 5 6 +TAP 47 96 0 4 +TAP 47 144 5 6 +SLC 47 192 0 4 6 2 4 SLD +TAP 47 192 12 4 +AUL 47 192 12 4 TAP DEF +SLC 47 198 2 4 12 5 4 SLD +SLC 47 210 5 4 15 8 4 SLD +SLC 47 225 8 4 7 9 4 SLD +SLC 47 232 9 4 8 10 4 SLD +SLC 47 240 10 4 10 11 4 SLD +SLC 47 250 11 4 18 12 4 SLD +SLD 47 268 12 4 20 12 4 SLD +TAP 47 288 4 4 +SLC 47 288 4 4 2 5 4 SLD +ADR 47 288 4 4 TAP DEF +SLC 47 290 5 4 9 8 4 SLD +SLC 47 299 8 4 4 9 4 SLD +SLC 47 303 9 4 5 10 4 SLD +SLC 47 308 10 4 6 11 4 SLD +SLC 47 314 11 4 12 12 4 SLD +SLC 47 326 12 4 10 12 4 SLD +TAP 47 336 0 8 +SLC 47 336 12 4 10 12 4 SLD +SLC 47 346 12 4 24 11 4 SLD +SLC 47 370 11 4 13 10 4 SLD +SLC 47 383 10 4 1 2 12 SLD +CHR 48 0 2 4 UP +SLC 48 0 2 12 1 2 4 SLD +SLC 48 1 2 4 12 1 4 SLD +SLC 48 13 1 4 16 0 4 SLD +SLC 48 29 0 4 8 0 4 SLD +SLC 48 37 0 4 11 0 4 SLD +SLC 48 48 0 4 10 0 4 SLD +TAP 48 48 8 8 +SLC 48 58 0 4 23 1 4 SLD +SLC 48 81 1 4 14 2 4 SLD +SLC 48 95 2 4 1 2 12 SLD +SLC 48 96 2 12 1 10 4 SLD +CHR 48 96 10 4 UP +SLC 48 97 10 4 11 11 4 SLD +SLC 48 108 11 4 15 12 4 SLD +SLC 48 123 12 4 10 12 4 SLD +SLC 48 133 12 4 11 12 4 SLD +TAP 48 144 0 8 +SLC 48 144 12 4 10 12 4 SLD +SLC 48 154 12 4 23 11 4 SLD +SLD 48 177 11 4 15 10 4 SLD +CHR 48 192 0 6 UP +AUL 48 192 0 6 CHR DEF +FLK 48 192 6 6 L +AUL 48 192 6 6 FLK DEF +CHR 48 288 0 4 UP +FLK 48 288 4 4 L +ADR 48 288 4 4 FLK DEF +CHR 48 288 8 4 UP +FLK 48 288 12 4 L +ADR 48 288 12 4 FLK DEF +TAP 49 0 0 4 +TAP 49 0 12 4 +AHD 49 0 12 4 TAP 96 DEF +TAP 49 72 5 6 +TAP 49 96 12 4 +TAP 49 144 5 6 +TAP 49 192 0 4 +AIR 49 192 0 4 TAP DEF +SLC 49 192 12 4 4 10 4 SLD +SLC 49 196 10 4 6 8 4 SLD +SLC 49 202 8 4 9 6 4 SLD +SLC 49 211 6 4 6 5 4 SLD +SLC 49 217 5 4 9 4 4 SLD +SLC 49 226 4 4 13 3 4 SLD +SLC 49 239 3 4 8 3 4 SLD +SLC 49 247 3 4 16 4 5 SLD +SLC 49 263 4 5 13 5 6 SLD +SLD 49 276 5 6 12 5 6 SLD +SLC 49 288 0 4 24 5 6 SLD +SLD 49 288 5 6 288 5 6 SLD +SLC 49 312 5 6 264 5 6 SLD +SLC 49 336 12 4 24 5 6 SLD +SLC 49 360 5 6 216 5 6 SLD +SLC 50 0 0 4 24 5 6 SLD +SLC 50 24 5 6 168 5 6 SLD +SLC 50 48 12 4 24 5 6 SLD +SLC 50 72 5 6 120 5 6 SLD +SLC 50 96 0 4 24 5 6 SLD +SLC 50 120 5 6 72 5 6 SLD +SLC 50 144 12 4 24 5 6 SLD +SLC 50 168 5 6 24 5 6 SLD +SLD 50 192 5 6 48 0 4 SLD +SLD 50 192 5 6 56 2 4 SLD +SLD 50 192 5 6 64 4 4 SLD +SLD 50 192 5 6 72 6 4 SLD +SLD 50 192 5 6 80 8 4 SLD +SLD 50 192 5 6 88 10 4 SLD +SLD 50 192 5 6 96 12 4 SLD +TAP 51 0 4 4 +TAP 51 0 12 4 +TAP 51 48 0 4 +TAP 51 48 8 8 +TAP 51 96 0 8 +TAP 51 96 12 4 +TAP 51 144 2 6 +TAP 51 144 8 6 +TAP 51 192 4 4 +ASC 51 192 4 4 TAP 5 12 3 4 5 DEF +SLC 51 192 8 4 12 9 4 SLD +ASC 51 204 3 4 ASC 5 14 2 4 5 DEF +SLC 51 204 9 4 14 10 4 SLD +ASC 51 218 2 4 ASC 5 19 1 4 5 DEF +SLC 51 218 10 4 19 11 4 SLD +ASC 51 237 1 4 ASC 5 37 0 4 5 DEF +SLC 51 237 11 4 37 12 4 SLD +ASD 51 274 0 4 ASC 5 14 0 4 5 DEF +SLD 51 274 12 4 14 12 4 SLD +CHR 51 288 0 4 UP +ADW 51 288 0 4 CHR DEF +TAP 51 336 8 8 +TAP 52 0 0 4 +TAP 52 0 8 4 +TAP 52 48 0 8 +TAP 52 48 12 4 +TAP 52 96 0 4 +TAP 52 96 8 8 +TAP 52 144 2 6 +TAP 52 144 8 6 +SLC 52 192 4 4 12 3 4 SLD +TAP 52 192 8 4 +ASC 52 192 8 4 TAP 5 12 9 4 5 DEF +SLC 52 204 3 4 14 2 4 SLD +ASC 52 204 9 4 ASC 5 14 10 4 5 DEF +SLC 52 218 2 4 19 1 4 SLD +ASC 52 218 10 4 ASC 5 19 11 4 5 DEF +SLC 52 237 1 4 37 0 4 SLD +ASC 52 237 11 4 ASC 5 37 12 4 5 DEF +SLD 52 274 0 4 14 0 4 SLD +ASD 52 274 12 4 ASC 5 14 12 4 5 DEF +CHR 52 288 12 4 UP +ADW 52 288 12 4 CHR DEF +TAP 52 336 0 3 +SLC 53 0 0 3 48 5 3 SLD +SLC 53 0 8 3 48 13 3 SLD +SLC 53 48 5 3 144 5 3 SLD +SLC 53 48 13 3 144 13 3 SLD +TAP 53 96 9 3 +TAP 53 144 9 3 +SLC 53 192 5 3 48 2 3 SLD +TAP 53 192 9 3 +SLC 53 192 13 3 48 11 3 SLD +SLC 53 240 2 3 144 2 3 SLD +SLC 53 240 11 3 144 11 3 SLD +TAP 53 288 6 4 +TAP 53 336 6 4 +SLD 54 0 2 3 96 6 3 SLD +HLD 54 0 6 4 96 +SLD 54 0 11 3 96 7 3 SLD +FLK 54 96 2 4 L +FLK 54 96 10 4 L +CHR 54 192 3 3 UP +AHD 54 192 3 3 CHR 96 DEF +CHR 54 192 10 3 UP +AHD 54 192 10 3 CHR 96 DEF +CHR 54 288 0 3 UP +ADW 54 288 0 3 CHR DEF +CHR 54 288 3 3 UP +ADW 54 288 3 3 CHR DEF +CHR 54 288 6 4 UP +ADW 54 288 6 4 CHR DEF +CHR 54 288 10 3 UP +ADW 54 288 10 3 CHR DEF +CHR 54 288 13 3 UP +ADW 54 288 13 3 CHR DEF +CHR 55 0 0 4 UP +AHD 55 0 0 4 CHR 96 DEF +CHR 55 0 12 4 UP +TAP 55 72 5 6 +TAP 55 96 0 4 +TAP 55 144 5 6 +SLC 55 192 0 4 6 2 4 SLD +TAP 55 192 12 4 +AUL 55 192 12 4 TAP DEF +SLC 55 198 2 4 12 5 4 SLD +SLC 55 210 5 4 15 8 4 SLD +SLC 55 225 8 4 7 9 4 SLD +SLC 55 232 9 4 8 10 4 SLD +SLC 55 240 10 4 10 11 4 SLD +SLC 55 250 11 4 18 12 4 SLD +SLD 55 268 12 4 20 12 4 SLD +TAP 55 288 4 4 +SLC 55 288 4 4 2 5 4 SLD +ADR 55 288 4 4 TAP DEF +SLC 55 290 5 4 9 8 4 SLD +SLC 55 299 8 4 4 9 4 SLD +SLC 55 303 9 4 5 10 4 SLD +SLC 55 308 10 4 6 11 4 SLD +SLC 55 314 11 4 12 12 4 SLD +SLC 55 326 12 4 10 12 4 SLD +TAP 55 336 0 8 +SLC 55 336 12 4 11 12 4 SLD +SLC 55 347 12 4 15 11 4 SLD +SLC 55 362 11 4 9 10 4 SLD +SLC 55 371 10 4 7 9 4 SLD +SLD 55 378 9 4 6 8 4 SLD +SLC 56 0 0 4 2 1 4 SLD +SLC 56 2 1 4 9 4 4 SLD +SLC 56 11 4 4 4 5 4 SLD +SLC 56 15 5 4 5 6 4 SLD +SLC 56 20 6 4 6 7 4 SLD +SLC 56 26 7 4 8 8 4 SLD +SLC 56 34 8 4 11 9 4 SLD +SLC 56 45 9 4 3 9 4 SLD +TAP 56 48 0 6 +SLC 56 48 9 4 3 9 4 SLD +SLC 56 51 9 4 11 8 4 SLD +SLC 56 62 8 4 8 7 4 SLD +SLC 56 70 7 4 6 6 4 SLD +SLC 56 76 6 4 5 5 4 SLD +SLC 56 81 5 4 4 4 4 SLD +SLC 56 85 4 4 9 1 4 SLD +SLD 56 94 1 4 2 0 4 SLD +SLC 56 96 10 4 15 11 4 SLD +SLC 56 111 11 4 21 12 4 SLD +SLC 56 132 12 4 14 12 4 SLD +TAP 56 144 0 8 +SLC 56 146 12 4 17 11 4 SLD +SLC 56 163 11 4 9 10 4 SLD +SLC 56 172 10 4 7 9 4 SLD +SLC 56 179 9 4 5 8 4 SLD +SLD 56 184 8 4 8 6 4 SLD +SLC 56 192 0 4 2 1 4 SLD +AUL 56 192 6 4 SLD DEF +SLC 56 194 1 4 3 2 4 SLD +SLC 56 197 2 4 8 4 4 SLD +SLC 56 205 4 4 5 5 4 SLD +SLC 56 210 5 4 6 6 4 SLD +SLC 56 216 6 4 9 7 4 SLD +SLC 56 225 7 4 12 8 4 SLD +SLC 56 237 8 4 3 8 4 SLD +SLC 56 240 8 4 4 8 4 SLD +SLC 56 244 8 4 13 7 4 SLD +SLC 56 257 7 4 9 6 4 SLD +SLC 56 266 6 4 7 5 4 SLD +SLC 56 273 5 4 6 4 4 SLD +SLC 56 279 4 4 5 3 4 SLD +SLD 56 284 3 4 4 2 4 SLD +CHR 56 288 0 4 UP +AUL 56 288 0 4 CHR DEF +AUL 56 288 2 4 SLD DEF +FLK 56 288 8 8 L +ADR 56 288 8 8 FLK DEF +CHR 57 0 0 4 UP +CHR 57 0 12 4 UP +AHD 57 0 12 4 CHR 96 DEF +TAP 57 72 5 6 +TAP 57 96 12 4 +TAP 57 144 5 6 +TAP 57 192 0 4 +AUR 57 192 0 4 TAP DEF +SLC 57 192 12 4 6 10 4 SLD +SLC 57 198 10 4 12 7 4 SLD +SLC 57 210 7 4 15 4 4 SLD +SLC 57 225 4 4 7 3 4 SLD +SLC 57 232 3 4 8 2 4 SLD +SLC 57 240 2 4 10 1 4 SLD +SLC 57 250 1 4 18 0 4 SLD +SLD 57 268 0 4 20 0 4 SLD +TAP 57 288 8 4 +SLC 57 288 8 4 2 7 4 SLD +ADL 57 288 8 4 TAP DEF +SLC 57 290 7 4 9 4 4 SLD +SLC 57 299 4 4 4 3 4 SLD +SLC 57 303 3 4 5 2 4 SLD +SLC 57 308 2 4 6 1 4 SLD +SLC 57 314 1 4 12 0 4 SLD +SLC 57 326 0 4 10 0 4 SLD +SLC 57 336 0 4 11 0 4 SLD +TAP 57 336 8 8 +SLC 57 347 0 4 15 1 4 SLD +SLC 57 362 1 4 9 2 4 SLD +SLC 57 371 2 4 7 3 4 SLD +SLD 57 378 3 4 6 4 4 SLD +SLC 58 0 12 4 2 11 4 SLD +SLC 58 2 11 4 9 8 4 SLD +SLC 58 11 8 4 4 7 4 SLD +SLC 58 15 7 4 5 6 4 SLD +SLC 58 20 6 4 6 5 4 SLD +SLC 58 26 5 4 8 4 4 SLD +SLC 58 34 4 4 11 3 4 SLD +SLC 58 45 3 4 3 3 4 SLD +SLC 58 48 3 4 3 3 4 SLD +TAP 58 48 10 6 +SLC 58 51 3 4 11 4 4 SLD +SLC 58 62 4 4 8 5 4 SLD +SLC 58 70 5 4 6 6 4 SLD +SLC 58 76 6 4 5 7 4 SLD +SLC 58 81 7 4 4 8 4 SLD +SLC 58 85 8 4 9 11 4 SLD +SLD 58 94 11 4 2 12 4 SLD +SLC 58 96 2 4 15 1 4 SLD +SLC 58 111 1 4 21 0 4 SLD +SLC 58 132 0 4 14 0 4 SLD +TAP 58 144 8 8 +SLC 58 146 0 4 17 1 4 SLD +SLC 58 163 1 4 9 2 4 SLD +SLC 58 172 2 4 7 3 4 SLD +SLC 58 179 3 4 5 4 4 SLD +SLD 58 184 4 4 8 6 4 SLD +AUR 58 192 6 4 SLD DEF +SLC 58 192 12 4 2 11 4 SLD +SLC 58 194 11 4 3 10 4 SLD +SLC 58 197 10 4 8 8 4 SLD +SLC 58 205 8 4 5 7 4 SLD +SLC 58 210 7 4 6 6 4 SLD +SLC 58 216 6 4 9 5 4 SLD +SLC 58 225 5 4 12 4 4 SLD +SLC 58 237 4 4 3 4 4 SLD +SLC 58 240 4 4 4 4 4 SLD +SLC 58 244 4 4 13 5 4 SLD +SLC 58 257 5 4 9 6 4 SLD +SLC 58 266 6 4 7 7 4 SLD +SLC 58 273 7 4 6 8 4 SLD +SLC 58 279 8 4 5 9 4 SLD +SLD 58 284 9 4 4 10 4 SLD +FLK 58 288 0 8 L +ADL 58 288 0 8 FLK DEF +AUR 58 288 10 4 SLD DEF +CHR 58 288 12 4 UP +AUR 58 288 12 4 CHR DEF +TAP 59 0 4 4 +TAP 59 0 10 6 +TAP 59 48 0 6 +TAP 59 48 8 4 +TAP 59 96 4 6 +TAP 59 96 12 4 +TAP 59 144 0 4 +TAP 59 144 6 6 +TAP 59 192 6 4 +ASC 59 192 6 4 TAP 5 7 5 4 5 DEF +SLC 59 192 12 4 7 11 4 SLD +ASC 59 199 5 4 ASC 5 8 4 4 5 DEF +SLC 59 199 11 4 8 10 4 SLD +ASC 59 207 4 4 ASC 5 9 3 4 5 DEF +SLC 59 207 10 4 9 9 4 SLD +ASC 59 216 3 4 ASC 5 11 2 4 5 DEF +SLC 59 216 9 4 11 8 4 SLD +ASC 59 227 2 4 ASC 5 15 1 4 5 DEF +SLC 59 227 8 4 15 7 4 SLD +ASC 59 242 1 4 ASC 5 27 0 4 5 DEF +SLC 59 242 7 4 27 6 4 SLD +ASD 59 269 0 4 ASC 5 19 0 4 5 DEF +SLD 59 269 6 4 19 6 4 SLD +CHR 59 288 0 4 UP +ADW 59 288 0 4 CHR DEF +TAP 59 336 6 4 +TAP 60 0 4 4 +TAP 60 0 10 6 +TAP 60 48 0 6 +TAP 60 48 8 4 +TAP 60 96 4 6 +TAP 60 96 12 4 +TAP 60 144 0 4 +TAP 60 144 6 6 +SLC 60 192 6 4 7 5 4 SLD +TAP 60 192 12 4 +ASC 60 192 12 4 TAP 5 7 11 4 5 DEF +SLC 60 199 5 4 8 4 4 SLD +ASC 60 199 11 4 ASC 5 8 10 4 5 DEF +SLC 60 207 4 4 9 3 4 SLD +ASC 60 207 10 4 ASC 5 9 9 4 5 DEF +SLC 60 216 3 4 11 2 4 SLD +ASC 60 216 9 4 ASC 5 11 8 4 5 DEF +SLC 60 227 2 4 15 1 4 SLD +ASC 60 227 8 4 ASC 5 15 7 4 5 DEF +SLC 60 242 1 4 27 0 4 SLD +ASC 60 242 7 4 ASC 5 27 6 4 5 DEF +SLD 60 269 0 4 19 0 4 SLD +ASD 60 269 6 4 ASC 5 19 6 4 5 DEF +CHR 60 288 6 4 UP +ADW 60 288 6 4 CHR DEF +TAP 60 336 3 4 +TAP 60 336 9 4 +SLC 61 0 0 4 17 1 4 SLD +SLC 61 0 12 4 17 11 4 SLD +SLC 61 17 1 4 19 2 4 SLD +SLC 61 17 11 4 19 10 4 SLD +SLC 61 36 2 4 20 3 4 SLD +SLC 61 36 10 4 20 9 4 SLD +SLC 61 56 3 4 27 4 4 SLD +SLC 61 56 9 4 27 8 4 SLD +SLD 61 83 4 4 13 4 4 SLD +SLD 61 83 8 4 13 8 4 SLD +TAP 61 144 4 3 +TAP 61 144 9 3 +HLD 61 192 0 3 96 +HLD 61 192 3 3 96 +HLD 61 192 6 4 96 +HLD 61 192 10 3 96 +HLD 61 192 13 3 96 +AIR 61 288 0 3 HLD DEF +AIR 61 288 3 3 HLD DEF +AIR 61 288 6 4 HLD DEF +AIR 61 288 10 3 HLD DEF +AIR 61 288 13 3 HLD DEF +SLD 62 0 3 3 96 1 3 SLD +SLD 62 0 3 3 96 5 3 SLD +SLC 62 96 13 2 3 10 2 SLD +SLC 62 99 10 2 2 10 2 SLD +SLC 62 101 10 2 29 10 2 SLD +SLC 62 130 10 2 1 8 6 SLD +SLC 62 131 8 6 4 8 6 SLD +SLC 62 135 8 6 1 10 2 SLD +SLD 62 136 10 2 8 10 2 SLD +SLC 62 192 0 4 1 0 12 SLD +TAP 62 192 4 4 +AHX 62 192 4 4 TAP 96 DEF +TAP 62 192 8 4 +AHX 62 192 8 4 TAP 96 DEF +SLC 62 193 0 12 23 0 12 SLD +SLC 62 216 0 12 1 0 4 SLD +SLD 62 217 0 4 71 0 4 SLD +ALD 62 288 4 8 1 5 5 4 8 1 +CHR 63 0 0 4 UP +CHR 63 0 6 4 UP +ASC 63 0 6 4 CHR 5 6 7 4 5 DEF +ASC 63 6 7 4 ASC 5 7 8 4 5 DEF +ASC 63 13 8 4 ASC 5 8 9 4 5 DEF +ASC 63 21 9 4 ASC 5 11 10 4 5 DEF +ASC 63 32 10 4 ASC 5 13 11 4 5 DEF +ASC 63 45 11 4 ASC 5 25 12 4 5 DEF +ASD 63 70 12 4 ASC 5 26 12 4 5 DEF +TAP 63 72 5 6 +TAP 63 96 12 4 +TAP 63 144 5 6 +CHR 63 192 6 4 UP +ASC 63 192 6 4 CHR 5 6 5 4 5 DEF +CHR 63 192 12 4 UP +ASC 63 198 5 4 ASC 5 7 4 4 5 DEF +ASC 63 205 4 4 ASC 5 8 3 4 5 DEF +ASC 63 213 3 4 ASC 5 11 2 4 5 DEF +ASC 63 224 2 4 ASC 5 13 1 4 5 DEF +ASC 63 237 1 4 ASC 5 25 0 4 5 DEF +ASD 63 262 0 4 ASC 5 26 0 4 5 DEF +TAP 63 264 5 6 +TAP 63 288 0 4 +TAP 63 336 5 6 +CHR 64 0 0 4 UP +CHR 64 0 6 4 UP +ASC 64 0 6 4 CHR 5 6 7 4 5 DEF +ASC 64 6 7 4 ASC 5 7 8 4 5 DEF +ASC 64 13 8 4 ASC 5 8 9 4 5 DEF +ASC 64 21 9 4 ASC 5 11 10 4 5 DEF +ASC 64 32 10 4 ASC 5 13 11 4 5 DEF +ASC 64 45 11 4 ASC 5 25 12 4 5 DEF +ASD 64 70 12 4 ASC 5 26 12 4 5 DEF +TAP 64 72 5 6 +TAP 64 96 12 4 +TAP 64 144 5 6 +TAP 64 192 0 4 +TAP 64 240 4 4 +TAP 64 288 8 4 +TAP 64 336 12 4 +CHR 65 0 6 4 UP +ASC 65 0 6 4 CHR 5 6 5 4 5 DEF +CHR 65 0 12 4 UP +ASC 65 6 5 4 ASC 5 7 4 4 5 DEF +ASC 65 13 4 4 ASC 5 8 3 4 5 DEF +ASC 65 21 3 4 ASC 5 11 2 4 5 DEF +ASC 65 32 2 4 ASC 5 13 1 4 5 DEF +ASC 65 45 1 4 ASC 5 25 0 4 5 DEF +ASD 65 70 0 4 ASC 5 26 0 4 5 DEF +TAP 65 72 5 6 +TAP 65 96 0 4 +TAP 65 144 5 6 +CHR 65 192 0 4 UP +CHR 65 192 6 4 UP +ASC 65 192 6 4 CHR 5 6 7 4 5 DEF +ASC 65 198 7 4 ASC 5 7 8 4 5 DEF +ASC 65 205 8 4 ASC 5 8 9 4 5 DEF +ASC 65 213 9 4 ASC 5 11 10 4 5 DEF +ASC 65 224 10 4 ASC 5 13 11 4 5 DEF +ASC 65 237 11 4 ASC 5 25 12 4 5 DEF +ASD 65 262 12 4 ASC 5 26 12 4 5 DEF +TAP 65 264 5 6 +TAP 65 288 12 4 +TAP 65 336 5 6 +CHR 66 0 6 4 UP +ASC 66 0 6 4 CHR 5 6 5 4 5 DEF +CHR 66 0 12 4 UP +ASC 66 6 5 4 ASC 5 7 4 4 5 DEF +ASC 66 13 4 4 ASC 5 8 3 4 5 DEF +ASC 66 21 3 4 ASC 5 11 2 4 5 DEF +ASC 66 32 2 4 ASC 5 13 1 4 5 DEF +ASC 66 45 1 4 ASC 5 25 0 4 5 DEF +ASD 66 70 0 4 ASC 5 26 0 4 5 DEF +TAP 66 72 5 6 +TAP 66 96 0 4 +TAP 66 144 5 6 +TAP 66 192 0 4 +TAP 66 192 12 4 +TAP 66 240 4 4 +TAP 66 240 8 4 +TAP 66 288 0 4 +AIR 66 288 0 4 TAP DEF +TAP 66 288 12 4 +AIR 66 288 12 4 TAP DEF +SLC 67 0 0 4 4 1 4 SLD +TAP 67 0 12 4 +AHD 67 0 12 4 TAP 96 DEF +SLC 67 4 1 4 5 2 4 SLD +SLC 67 9 2 4 5 3 4 SLD +SLC 67 14 3 4 7 4 4 SLD +SLC 67 21 4 4 9 5 4 SLD +SLC 67 30 5 4 14 6 4 SLD +SLC 67 44 6 4 4 6 4 SLD +SLC 67 48 6 4 4 6 4 SLD +SLC 67 52 6 4 14 5 4 SLD +SLC 67 66 5 4 9 4 4 SLD +SLC 67 75 4 4 7 3 4 SLD +SLC 67 82 3 4 5 2 4 SLD +SLC 67 87 2 4 5 1 4 SLD +SLD 67 92 1 4 4 0 4 SLD +AHD 67 96 0 4 SLD 96 DEF +SLC 67 96 12 4 4 11 4 SLD +SLC 67 100 11 4 5 10 4 SLD +SLC 67 105 10 4 5 9 4 SLD +SLC 67 110 9 4 7 8 4 SLD +SLC 67 117 8 4 9 7 4 SLD +SLC 67 126 7 4 14 6 4 SLD +SLC 67 140 6 4 4 6 4 SLD +SLC 67 144 6 4 4 6 4 SLD +SLC 67 148 6 4 14 7 4 SLD +SLC 67 162 7 4 9 8 4 SLD +SLC 67 171 8 4 7 9 4 SLD +SLC 67 178 9 4 5 10 4 SLD +SLC 67 183 10 4 5 11 4 SLD +SLC 67 188 11 4 4 12 4 SLD +TAP 67 192 0 4 +SLC 67 192 12 4 96 12 4 SLD +TAP 67 288 0 4 +SLD 67 288 12 4 96 8 4 SLD +TAP 67 336 2 4 +SLD 68 0 4 4 96 0 4 SLD +TAP 68 48 12 4 +SLD 68 96 0 4 192 0 4 SLD +TAP 68 96 10 4 +SLC 68 144 8 4 15 7 4 SLD +SLC 68 159 7 4 18 6 4 SLD +SLC 68 177 6 4 23 5 4 SLD +SLC 68 200 5 4 30 4 4 SLD +SLD 68 230 4 4 10 4 4 SLD +FLK 68 240 8 8 L +FLK 68 288 4 8 L +HLD 69 0 2 3 96 +HLD 69 0 11 3 96 +TAP 69 96 5 3 +TAP 69 120 8 3 +TAP 69 144 5 3 +TAP 69 192 0 4 +TAP 69 192 12 4 +TAP 69 264 2 4 +TAP 69 264 10 4 +TAP 69 336 5 3 +TAP 69 336 8 3 +SLC 70 0 6 4 15 5 4 SLD +SLC 70 0 6 4 15 7 4 SLD +SLC 70 15 5 4 17 4 4 SLD +SLC 70 15 7 4 17 8 4 SLD +SLC 70 32 4 4 19 3 4 SLD +SLC 70 32 8 4 19 9 4 SLD +SLC 70 51 3 4 24 2 4 SLD +SLC 70 51 9 4 24 10 4 SLD +SLC 70 75 2 4 33 1 4 SLD +SLC 70 75 10 4 33 11 4 SLD +SLC 70 108 1 4 52 0 4 SLD +SLC 70 108 11 4 52 12 4 SLD +SLD 70 160 0 4 32 0 4 SLD +SLD 70 160 12 4 32 12 4 SLD +ASD 70 192 0 4 SLD 5 192 0 4 5 DEF +AHD 70 192 12 4 SLD 192 DEF +ASC 71 0 0 4 ASD 5 3 1 4 5 DEF +ALD 71 0 0 4 1 5 4 0 4 3 +SLC 71 0 12 4 3 11 4 SLD +ASC 71 3 1 4 ASC 5 10 4 4 5 DEF +SLC 71 3 11 4 10 8 4 SLD +ASC 71 13 4 4 ASC 5 11 7 4 5 DEF +SLC 71 13 8 4 11 5 4 SLD +SLC 71 24 5 4 5 4 4 SLD +ASC 71 24 7 4 ASC 5 5 8 4 5 DEF +SLC 71 29 4 4 5 3 4 SLD +ASC 71 29 8 4 ASC 5 5 9 4 5 DEF +SLC 71 34 3 4 8 2 4 SLD +ASC 71 34 9 4 ASC 5 8 10 4 5 DEF +SLC 71 42 2 4 10 1 4 SLD +ASC 71 42 10 4 ASC 5 10 11 4 5 DEF +SLC 71 52 1 4 19 0 4 SLD +ASC 71 52 11 4 ASC 5 19 12 4 5 DEF +SLD 71 71 0 4 25 0 4 SLD +ASD 71 71 12 4 ASC 5 25 12 4 5 DEF +SLC 71 96 0 4 3 1 4 SLD +ASC 71 96 12 4 ASD 5 3 11 4 5 DEF +ALD 71 96 12 4 1 5 4 12 4 3 +SLC 71 99 1 4 10 4 4 SLD +ASC 71 99 11 4 ASC 5 10 8 4 5 DEF +SLC 71 109 4 4 11 7 4 SLD +ASC 71 109 8 4 ASC 5 11 5 4 5 DEF +ASC 71 120 5 4 ASC 5 5 4 4 5 DEF +SLC 71 120 7 4 5 8 4 SLD +ASC 71 125 4 4 ASC 5 5 3 4 5 DEF +SLC 71 125 8 4 5 9 4 SLD +ASC 71 130 3 4 ASC 5 8 2 4 5 DEF +SLC 71 130 9 4 8 10 4 SLD +ASC 71 138 2 4 ASC 5 10 1 4 5 DEF +SLC 71 138 10 4 10 11 4 SLD +ASC 71 148 1 4 ASC 5 19 0 4 5 DEF +SLC 71 148 11 4 19 12 4 SLD +ASD 71 167 0 4 ASC 5 25 0 4 5 DEF +SLD 71 167 12 4 25 12 4 SLD +ASC 71 192 0 4 ASD 5 3 1 4 5 DEF +ALD 71 192 0 4 1 5 4 0 4 3 +SLC 71 192 12 4 3 11 4 SLD +ASC 71 195 1 4 ASC 5 10 4 4 5 DEF +SLC 71 195 11 4 10 8 4 SLD +ASC 71 205 4 4 ASC 5 11 7 4 5 DEF +SLC 71 205 8 4 11 5 4 SLD +SLC 71 216 5 4 5 4 4 SLD +ASC 71 216 7 4 ASC 5 5 8 4 5 DEF +SLC 71 221 4 4 5 3 4 SLD +ASC 71 221 8 4 ASC 5 5 9 4 5 DEF +SLC 71 226 3 4 8 2 4 SLD +ASC 71 226 9 4 ASC 5 8 10 4 5 DEF +SLC 71 234 2 4 10 1 4 SLD +ASC 71 234 10 4 ASC 5 10 11 4 5 DEF +SLC 71 244 1 4 19 0 4 SLD +ASC 71 244 11 4 ASC 5 19 12 4 5 DEF +SLD 71 263 0 4 25 0 4 SLD +ASD 71 263 12 4 ASC 5 25 12 4 5 DEF +SLC 71 288 0 4 3 1 4 SLD +ASC 71 288 12 4 ASD 5 3 11 4 5 DEF +ALD 71 288 12 4 1 5 4 12 4 3 +SLC 71 291 1 4 10 4 4 SLD +ASC 71 291 11 4 ASC 5 10 8 4 5 DEF +SLC 71 301 4 4 11 7 4 SLD +ASC 71 301 8 4 ASC 5 11 5 4 5 DEF +ASC 71 312 5 4 ASC 5 5 4 4 5 DEF +SLC 71 312 7 4 5 8 4 SLD +ASC 71 317 4 4 ASC 5 5 3 4 5 DEF +SLC 71 317 8 4 5 9 4 SLD +ASC 71 322 3 4 ASC 5 8 2 4 5 DEF +SLC 71 322 9 4 8 10 4 SLD +ASC 71 330 2 4 ASC 5 10 1 4 5 DEF +SLC 71 330 10 4 10 11 4 SLD +ASC 71 340 1 4 ASC 5 19 0 4 5 DEF +SLC 71 340 11 4 19 12 4 SLD +ASD 71 359 0 4 ASC 5 25 0 4 5 DEF +SLD 71 359 12 4 25 12 4 SLD +ASC 72 0 0 4 ASD 5 3 1 4 5 DEF +ALD 72 0 0 4 1 5 4 0 4 3 +SLC 72 0 12 4 3 11 4 SLD +ASC 72 3 1 4 ASC 5 10 4 4 5 DEF +SLC 72 3 11 4 10 8 4 SLD +ASC 72 13 4 4 ASC 5 11 7 4 5 DEF +SLC 72 13 8 4 11 5 4 SLD +SLC 72 24 5 4 5 4 4 SLD +ASC 72 24 7 4 ASC 5 5 8 4 5 DEF +SLC 72 29 4 4 5 3 4 SLD +ASC 72 29 8 4 ASC 5 5 9 4 5 DEF +SLC 72 34 3 4 8 2 4 SLD +ASC 72 34 9 4 ASC 5 8 10 4 5 DEF +SLC 72 42 2 4 10 1 4 SLD +ASC 72 42 10 4 ASC 5 10 11 4 5 DEF +SLC 72 52 1 4 19 0 4 SLD +ASC 72 52 11 4 ASC 5 19 12 4 5 DEF +SLD 72 71 0 4 25 0 4 SLD +ASD 72 71 12 4 ASC 5 25 12 4 5 DEF +SLC 72 96 0 4 3 1 4 SLD +ASC 72 96 12 4 ASD 5 3 11 4 5 DEF +ALD 72 96 12 4 1 5 4 12 4 3 +SLC 72 99 1 4 10 4 4 SLD +ASC 72 99 11 4 ASC 5 10 8 4 5 DEF +SLC 72 109 4 4 11 7 4 SLD +ASC 72 109 8 4 ASC 5 11 5 4 5 DEF +ASC 72 120 5 4 ASC 5 5 4 4 5 DEF +SLC 72 120 7 4 5 8 4 SLD +ASC 72 125 4 4 ASC 5 5 3 4 5 DEF +SLC 72 125 8 4 5 9 4 SLD +ASC 72 130 3 4 ASC 5 8 2 4 5 DEF +SLC 72 130 9 4 8 10 4 SLD +ASC 72 138 2 4 ASC 5 10 1 4 5 DEF +SLC 72 138 10 4 10 11 4 SLD +ASC 72 148 1 4 ASC 5 19 0 4 5 DEF +SLC 72 148 11 4 19 12 4 SLD +ASD 72 167 0 4 ASC 5 25 0 4 5 DEF +SLD 72 167 12 4 25 12 4 SLD +ASC 72 192 0 4 ASD 5 3 1 4 5 DEF +ALD 72 192 0 4 1 5 4 0 4 3 +SLC 72 192 12 4 3 11 4 SLD +ASC 72 195 1 4 ASC 5 10 4 4 5 DEF +SLC 72 195 11 4 10 8 4 SLD +ASC 72 205 4 4 ASC 5 11 7 4 5 DEF +SLC 72 205 8 4 11 5 4 SLD +SLC 72 216 5 4 5 4 4 SLD +ASC 72 216 7 4 ASC 5 5 8 4 5 DEF +SLC 72 221 4 4 5 3 4 SLD +ASC 72 221 8 4 ASC 5 5 9 4 5 DEF +SLC 72 226 3 4 8 2 4 SLD +ASC 72 226 9 4 ASC 5 8 10 4 5 DEF +SLC 72 234 2 4 10 1 4 SLD +ASC 72 234 10 4 ASC 5 10 11 4 5 DEF +SLC 72 244 1 4 19 0 4 SLD +ASC 72 244 11 4 ASC 5 19 12 4 5 DEF +SLD 72 263 0 4 25 0 4 SLD +ASD 72 263 12 4 ASC 5 25 12 4 5 DEF +SLC 72 288 0 4 3 1 4 SLD +ASC 72 288 12 4 ASD 5 3 11 4 5 DEF +ALD 72 288 12 4 1 5 4 12 4 3 +SLC 72 291 1 4 10 4 4 SLD +ASC 72 291 11 4 ASC 5 10 8 4 5 DEF +SLC 72 301 4 4 11 7 4 SLD +ASC 72 301 8 4 ASC 5 11 5 4 5 DEF +ASC 72 312 5 4 ASC 5 5 4 4 5 DEF +SLC 72 312 7 4 5 8 4 SLD +ASC 72 317 4 4 ASC 5 5 3 4 5 DEF +SLC 72 317 8 4 5 9 4 SLD +ASC 72 322 3 4 ASC 5 8 2 4 5 DEF +SLC 72 322 9 4 8 10 4 SLD +ASC 72 330 2 4 ASC 5 10 1 4 5 DEF +SLC 72 330 10 4 10 11 4 SLD +ASC 72 340 1 4 ASC 5 19 0 4 5 DEF +SLC 72 340 11 4 19 12 4 SLD +ASD 72 359 0 4 ASC 5 25 0 4 5 DEF +SLD 72 359 12 4 25 12 4 SLD +ASC 73 0 0 4 ASD 5 3 1 4 5 DEF +ALD 73 0 0 4 1 5 4 0 4 3 +SLC 73 0 12 4 3 11 4 SLD +ASC 73 3 1 4 ASC 5 10 4 4 5 DEF +SLC 73 3 11 4 10 8 4 SLD +ASC 73 13 4 4 ASC 5 11 7 4 5 DEF +SLC 73 13 8 4 11 5 4 SLD +SLC 73 24 5 4 5 4 4 SLD +ASC 73 24 7 4 ASC 5 5 8 4 5 DEF +SLC 73 29 4 4 5 3 4 SLD +ASC 73 29 8 4 ASC 5 5 9 4 5 DEF +SLC 73 34 3 4 8 2 4 SLD +ASC 73 34 9 4 ASC 5 8 10 4 5 DEF +SLC 73 42 2 4 10 1 4 SLD +ASC 73 42 10 4 ASC 5 10 11 4 5 DEF +SLC 73 52 1 4 19 0 4 SLD +ASC 73 52 11 4 ASC 5 19 12 4 5 DEF +SLD 73 71 0 4 25 0 4 SLD +ASD 73 71 12 4 ASC 5 25 12 4 5 DEF +SLC 73 96 0 4 3 1 4 SLD +ASC 73 96 12 4 ASD 5 3 11 4 5 DEF +ALD 73 96 12 4 1 5 4 12 4 3 +SLC 73 99 1 4 10 4 4 SLD +ASC 73 99 11 4 ASC 5 10 8 4 5 DEF +SLC 73 109 4 4 11 7 4 SLD +ASC 73 109 8 4 ASC 5 11 5 4 5 DEF +ASC 73 120 5 4 ASC 5 5 4 4 5 DEF +SLC 73 120 7 4 5 8 4 SLD +ASC 73 125 4 4 ASC 5 5 3 4 5 DEF +SLC 73 125 8 4 5 9 4 SLD +ASC 73 130 3 4 ASC 5 8 2 4 5 DEF +SLC 73 130 9 4 8 10 4 SLD +ASC 73 138 2 4 ASC 5 10 1 4 5 DEF +SLC 73 138 10 4 10 11 4 SLD +ASC 73 148 1 4 ASC 5 19 0 4 5 DEF +SLC 73 148 11 4 19 12 4 SLD +ASD 73 167 0 4 ASC 5 25 0 4 5 DEF +SLD 73 167 12 4 25 12 4 SLD +ASC 73 192 0 4 ASD 5 3 1 4 5 DEF +ALD 73 192 0 4 1 5 4 0 4 3 +SLC 73 192 12 4 3 11 4 SLD +ASC 73 195 1 4 ASC 5 10 4 4 5 DEF +SLC 73 195 11 4 10 8 4 SLD +ASC 73 205 4 4 ASC 5 11 7 4 5 DEF +SLC 73 205 8 4 11 5 4 SLD +SLC 73 216 5 4 5 4 4 SLD +ASC 73 216 7 4 ASC 5 5 8 4 5 DEF +SLC 73 221 4 4 5 3 4 SLD +ASC 73 221 8 4 ASC 5 5 9 4 5 DEF +SLC 73 226 3 4 8 2 4 SLD +ASC 73 226 9 4 ASC 5 8 10 4 5 DEF +SLC 73 234 2 4 10 1 4 SLD +ASC 73 234 10 4 ASC 5 10 11 4 5 DEF +SLC 73 244 1 4 19 0 4 SLD +ASC 73 244 11 4 ASC 5 19 12 4 5 DEF +SLD 73 263 0 4 25 0 4 SLD +ASD 73 263 12 4 ASC 5 25 12 4 5 DEF +SLC 73 288 0 4 3 1 4 SLD +ASC 73 288 12 4 ASD 5 3 11 4 5 DEF +ALD 73 288 12 4 1 5 4 12 4 3 +SLC 73 291 1 4 10 4 4 SLD +ASC 73 291 11 4 ASC 5 10 8 4 5 DEF +SLC 73 301 4 4 11 7 4 SLD +ASC 73 301 8 4 ASC 5 11 5 4 5 DEF +ASC 73 312 5 4 ASC 5 5 4 4 5 DEF +SLC 73 312 7 4 5 8 4 SLD +ASC 73 317 4 4 ASC 5 5 3 4 5 DEF +SLC 73 317 8 4 5 9 4 SLD +ASC 73 322 3 4 ASC 5 8 2 4 5 DEF +SLC 73 322 9 4 8 10 4 SLD +ASC 73 330 2 4 ASC 5 10 1 4 5 DEF +SLC 73 330 10 4 10 11 4 SLD +ASC 73 340 1 4 ASC 5 19 0 4 5 DEF +SLC 73 340 11 4 19 12 4 SLD +ASD 73 359 0 4 ASC 5 25 0 4 5 DEF +SLD 73 359 12 4 25 12 4 SLD +ASC 74 0 0 4 ASD 5 3 1 4 5 DEF +ALD 74 0 0 4 1 5 4 0 4 3 +SLC 74 0 12 4 3 11 4 SLD +ASC 74 3 1 4 ASC 5 10 4 4 5 DEF +SLC 74 3 11 4 10 8 4 SLD +ASC 74 13 4 4 ASC 5 11 7 4 5 DEF +SLC 74 13 8 4 11 5 4 SLD +SLC 74 24 5 4 5 4 4 SLD +ASC 74 24 7 4 ASC 5 5 8 4 5 DEF +SLC 74 29 4 4 5 3 4 SLD +ASC 74 29 8 4 ASC 5 5 9 4 5 DEF +SLC 74 34 3 4 8 2 4 SLD +ASC 74 34 9 4 ASC 5 8 10 4 5 DEF +SLC 74 42 2 4 10 1 4 SLD +ASC 74 42 10 4 ASC 5 10 11 4 5 DEF +SLC 74 52 1 4 19 0 4 SLD +ASC 74 52 11 4 ASC 5 19 12 4 5 DEF +SLD 74 71 0 4 25 0 4 SLD +ASD 74 71 12 4 ASC 5 25 12 4 5 DEF +SLC 74 96 0 4 3 1 4 SLD +ASC 74 96 12 4 ASD 5 3 11 4 5 DEF +ALD 74 96 12 4 1 5 4 12 4 3 +SLC 74 99 1 4 10 4 4 SLD +ASC 74 99 11 4 ASC 5 10 8 4 5 DEF +SLC 74 109 4 4 11 7 4 SLD +ASC 74 109 8 4 ASC 5 11 5 4 5 DEF +ASC 74 120 5 4 ASC 5 5 4 4 5 DEF +SLC 74 120 7 4 5 8 4 SLD +ASC 74 125 4 4 ASC 5 5 3 4 5 DEF +SLC 74 125 8 4 5 9 4 SLD +ASC 74 130 3 4 ASC 5 8 2 4 5 DEF +SLC 74 130 9 4 8 10 4 SLD +ASC 74 138 2 4 ASC 5 10 1 4 5 DEF +SLC 74 138 10 4 10 11 4 SLD +ASC 74 148 1 4 ASC 5 19 0 4 5 DEF +SLC 74 148 11 4 19 12 4 SLD +ASD 74 167 0 4 ASC 5 25 0 4 5 DEF +SLD 74 167 12 4 25 12 4 SLD +ASD 74 192 0 4 ASD 5 96 0 4 5 DEF +ALD 74 192 0 4 1 5 4 0 4 3 +ASD 74 192 12 4 SLD 5 96 12 4 5 DEF +ASD 74 288 0 4 ASD 5 96 0 4 5 DEF +ALD 74 288 0 4 1 5 4 0 4 3 +ASD 74 288 12 4 ASD 5 96 12 4 5 DEF +ALD 74 288 12 4 1 5 4 12 4 3 +SLC 75 0 0 4 3 1 4 SLD +ASC 75 0 12 4 ASD 5 3 11 4 5 DEF +ALD 75 0 12 4 1 5 4 12 4 3 +SLC 75 3 1 4 10 4 4 SLD +ASC 75 3 11 4 ASC 5 10 8 4 5 DEF +SLC 75 13 4 4 11 7 4 SLD +ASC 75 13 8 4 ASC 5 11 5 4 5 DEF +ASC 75 24 5 4 ASC 5 5 4 4 5 DEF +SLC 75 24 7 4 5 8 4 SLD +ASC 75 29 4 4 ASC 5 5 3 4 5 DEF +SLC 75 29 8 4 5 9 4 SLD +ASC 75 34 3 4 ASC 5 8 2 4 5 DEF +SLC 75 34 9 4 8 10 4 SLD +ASC 75 42 2 4 ASC 5 10 1 4 5 DEF +SLC 75 42 10 4 10 11 4 SLD +ASC 75 52 1 4 ASC 5 19 0 4 5 DEF +SLC 75 52 11 4 19 12 4 SLD +ASD 75 71 0 4 ASC 5 25 0 4 5 DEF +SLD 75 71 12 4 25 12 4 SLD +ASC 75 96 0 4 ASD 5 3 1 4 5 DEF +ALD 75 96 0 4 1 5 4 0 4 3 +SLC 75 96 12 4 3 11 4 SLD +ASC 75 99 1 4 ASC 5 10 4 4 5 DEF +SLC 75 99 11 4 10 8 4 SLD +ASC 75 109 4 4 ASC 5 11 7 4 5 DEF +SLC 75 109 8 4 11 5 4 SLD +SLC 75 120 5 4 5 4 4 SLD +ASC 75 120 7 4 ASC 5 5 8 4 5 DEF +SLC 75 125 4 4 5 3 4 SLD +ASC 75 125 8 4 ASC 5 5 9 4 5 DEF +SLC 75 130 3 4 8 2 4 SLD +ASC 75 130 9 4 ASC 5 8 10 4 5 DEF +SLC 75 138 2 4 10 1 4 SLD +ASC 75 138 10 4 ASC 5 10 11 4 5 DEF +SLC 75 148 1 4 19 0 4 SLD +ASC 75 148 11 4 ASC 5 19 12 4 5 DEF +SLD 75 167 0 4 25 0 4 SLD +ASD 75 167 12 4 ASC 5 25 12 4 5 DEF +SLC 75 192 0 4 3 1 4 SLD +ASC 75 192 12 4 ASD 5 3 11 4 5 DEF +ALD 75 192 12 4 1 5 4 12 4 3 +SLC 75 195 1 4 10 4 4 SLD +ASC 75 195 11 4 ASC 5 10 8 4 5 DEF +SLC 75 205 4 4 11 7 4 SLD +ASC 75 205 8 4 ASC 5 11 5 4 5 DEF +ASC 75 216 5 4 ASC 5 5 4 4 5 DEF +SLC 75 216 7 4 5 8 4 SLD +ASC 75 221 4 4 ASC 5 5 3 4 5 DEF +SLC 75 221 8 4 5 9 4 SLD +ASC 75 226 3 4 ASC 5 8 2 4 5 DEF +SLC 75 226 9 4 8 10 4 SLD +ASC 75 234 2 4 ASC 5 10 1 4 5 DEF +SLC 75 234 10 4 10 11 4 SLD +ASC 75 244 1 4 ASC 5 19 0 4 5 DEF +SLC 75 244 11 4 19 12 4 SLD +ASD 75 263 0 4 ASC 5 25 0 4 5 DEF +SLD 75 263 12 4 25 12 4 SLD +ASC 75 288 0 4 ASD 5 3 1 4 5 DEF +ALD 75 288 0 4 1 5 4 0 4 3 +SLC 75 288 12 4 3 11 4 SLD +ASC 75 291 1 4 ASC 5 10 4 4 5 DEF +SLC 75 291 11 4 10 8 4 SLD +ASC 75 301 4 4 ASC 5 11 7 4 5 DEF +SLC 75 301 8 4 11 5 4 SLD +SLC 75 312 5 4 5 4 4 SLD +ASC 75 312 7 4 ASC 5 5 8 4 5 DEF +SLC 75 317 4 4 5 3 4 SLD +ASC 75 317 8 4 ASC 5 5 9 4 5 DEF +SLC 75 322 3 4 8 2 4 SLD +ASC 75 322 9 4 ASC 5 8 10 4 5 DEF +SLC 75 330 2 4 10 1 4 SLD +ASC 75 330 10 4 ASC 5 10 11 4 5 DEF +SLC 75 340 1 4 19 0 4 SLD +ASC 75 340 11 4 ASC 5 19 12 4 5 DEF +SLD 75 359 0 4 25 0 4 SLD +ASD 75 359 12 4 ASC 5 25 12 4 5 DEF +SLC 76 0 0 4 3 1 4 SLD +ASC 76 0 12 4 ASD 5 3 11 4 5 DEF +ALD 76 0 12 4 1 5 4 12 4 3 +SLC 76 3 1 4 10 4 4 SLD +ASC 76 3 11 4 ASC 5 10 8 4 5 DEF +SLC 76 13 4 4 11 7 4 SLD +ASC 76 13 8 4 ASC 5 11 5 4 5 DEF +ASC 76 24 5 4 ASC 5 5 4 4 5 DEF +SLC 76 24 7 4 5 8 4 SLD +ASC 76 29 4 4 ASC 5 5 3 4 5 DEF +SLC 76 29 8 4 5 9 4 SLD +ASC 76 34 3 4 ASC 5 8 2 4 5 DEF +SLC 76 34 9 4 8 10 4 SLD +ASC 76 42 2 4 ASC 5 10 1 4 5 DEF +SLC 76 42 10 4 10 11 4 SLD +ASC 76 52 1 4 ASC 5 19 0 4 5 DEF +SLC 76 52 11 4 19 12 4 SLD +ASD 76 71 0 4 ASC 5 25 0 4 5 DEF +SLD 76 71 12 4 25 12 4 SLD +ASC 76 96 0 4 ASD 5 3 1 4 5 DEF +ALD 76 96 0 4 1 5 4 0 4 3 +SLC 76 96 12 4 3 11 4 SLD +ASC 76 99 1 4 ASC 5 10 4 4 5 DEF +SLC 76 99 11 4 10 8 4 SLD +ASC 76 109 4 4 ASC 5 11 7 4 5 DEF +SLC 76 109 8 4 11 5 4 SLD +SLC 76 120 5 4 5 4 4 SLD +ASC 76 120 7 4 ASC 5 5 8 4 5 DEF +SLC 76 125 4 4 5 3 4 SLD +ASC 76 125 8 4 ASC 5 5 9 4 5 DEF +SLC 76 130 3 4 8 2 4 SLD +ASC 76 130 9 4 ASC 5 8 10 4 5 DEF +SLC 76 138 2 4 10 1 4 SLD +ASC 76 138 10 4 ASC 5 10 11 4 5 DEF +SLC 76 148 1 4 19 0 4 SLD +ASC 76 148 11 4 ASC 5 19 12 4 5 DEF +SLD 76 167 0 4 25 0 4 SLD +ASD 76 167 12 4 ASC 5 25 12 4 5 DEF +AIR 76 192 0 4 SLD DEF +ALD 76 192 12 4 1 5 4 12 4 3 +CHR 76 288 0 3 CE +CHR 76 288 3 3 CE +CHR 76 288 6 4 CE +CHR 76 288 10 3 CE +CHR 76 288 13 3 CE +SLD 77 0 0 3 384 0 3 SLD +SLD 77 96 13 3 288 13 3 SLD +SLD 77 192 3 3 192 3 3 SLD +SLD 77 288 10 3 96 10 3 SLD +SLC 78 0 0 3 16 1 3 SLD +SLC 78 0 3 3 38 4 3 SLD +HLD 78 0 6 4 96 +SLC 78 0 10 3 38 9 3 SLD +SLC 78 0 13 3 16 12 3 SLD +SLC 78 16 1 3 18 2 3 SLD +SLC 78 16 12 3 18 11 3 SLD +SLC 78 34 2 3 21 3 3 SLD +SLC 78 34 11 3 21 10 3 SLD +SLC 78 38 4 3 44 5 3 SLD +SLC 78 38 9 3 44 8 3 SLD +SLC 78 55 3 3 31 4 3 SLD +SLC 78 55 10 3 31 9 3 SLD +SLD 78 82 5 3 14 5 3 SLD +SLD 78 82 8 3 14 8 3 SLD +SLD 78 86 4 3 10 4 3 SLD +SLD 78 86 9 3 10 9 3 SLD +HLD 78 192 0 8 96 +HLD 78 192 8 8 96 +CHR 79 0 0 16 UP +ASD 79 0 0 16 CHR 5 768 7 2 5 DEF + +T_REC_TAP 297 +T_REC_CHR 68 +T_REC_FLK 32 +T_REC_MNE 0 +T_REC_HLD 23 +T_REC_SLD 108 +T_REC_AIR 63 +T_REC_AHD 77 +T_REC_ALL 668 +T_NOTE_TAP 297 +T_NOTE_CHR 68 +T_NOTE_FLK 32 +T_NOTE_MNE 0 +T_NOTE_HLD 23 +T_NOTE_SLD 78 +T_NOTE_AIR 63 +T_NOTE_AHD 56 +T_NOTE_ALL 617 +T_NUM_TAP 398 +T_NUM_CHR 68 +T_NUM_FLK 32 +T_NUM_MNE 0 +T_NUM_HLD 23 +T_NUM_SLD 108 +T_NUM_AIR 119 +T_NUM_AHD 77 +T_NUM_AAC 219 +T_CHRTYPE_UP 63 +T_CHRTYPE_DW 0 +T_CHRTYPE_CE 5 +T_LEN_HLD 18448 +T_LEN_SLD 65819 +T_LEN_AHD 43427 +T_LEN_ALL 127694 +T_JUDGE_TAP 466 +T_JUDGE_HLD 89 +T_JUDGE_SLD 318 +T_JUDGE_AIR 402 +T_JUDGE_FLK 32 +T_JUDGE_ALL 1307 +T_FIRST_MSEC 7307 +T_FIRST_RES 1824 +T_FINAL_MSEC 124615 +T_FINAL_RES 31104 +T_PROG_00 57 +T_PROG_05 62 +T_PROG_10 42 +T_PROG_15 53 +T_PROG_20 46 +T_PROG_25 48 +T_PROG_30 45 +T_PROG_35 83 +T_PROG_40 66 +T_PROG_45 67 +T_PROG_50 60 +T_PROG_55 78 +T_PROG_60 94 +T_PROG_65 68 +T_PROG_70 84 +T_PROG_75 87 +T_PROG_80 66 +T_PROG_85 177 +T_PROG_90 196 +T_PROG_95 56 + diff --git a/utils/Utils.cs b/utils/Utils.cs index 5beebdd..dff50f7 100644 --- a/utils/Utils.cs +++ b/utils/Utils.cs @@ -28,6 +28,8 @@ internal static Exception Fail(string msg = "") public static BigInteger Max(BigInteger a, BigInteger b) => a > b ? a : b; + public static Rational Max(Rational a, Rational b) => a > b ? a : b; + public static Rational Min(Rational a, Rational b) => a < b ? a : b; private static readonly Dictionary _simaiLexerMap = Enumerable.Range(1, L.ruleNames.Length) @@ -66,7 +68,7 @@ public static int Tick(Rational time, int resolution, int extraTicks = 0, int? m } } -internal static class ExtensionUtils +public static class ExtensionUtils { internal static void Add(this Dictionary> dict, K key, V value) where K : notnull {