|
| 1 | +using System.Globalization; |
| 2 | +using System.Text; |
| 3 | +using MuConvert.chart; |
| 4 | +using MuConvert.generator; |
| 5 | +using MuConvert.utils; |
| 6 | +using static MuConvert.utils.Alert.LEVEL; |
| 7 | + |
| 8 | +namespace MuConvert.chu; |
| 9 | + |
| 10 | +/** |
| 11 | + * C2S 格式生成器。 |
| 12 | + * 输入 IChuChart,内部自动转换后输出 C2S 文本。 |
| 13 | + */ |
| 14 | +public class C2sGenerator : IGenerator<IChuChart> |
| 15 | +{ |
| 16 | + private const int C2sResolution = 384; |
| 17 | + |
| 18 | + public (string, List<Alert>) Generate(IChuChart chart) |
| 19 | + { |
| 20 | + var alerts = new List<Alert>(); |
| 21 | + var c2s = ConvertToC2s(chart, alerts); |
| 22 | + var text = Serialize(c2s); |
| 23 | + return (text, alerts); |
| 24 | + } |
| 25 | + |
| 26 | + private static C2sChart ConvertToC2s(IChuChart chart, List<Alert> alerts) |
| 27 | + { |
| 28 | + if (chart is C2sChart c2s) return c2s; |
| 29 | + |
| 30 | + if (chart is UgcChart ugc) |
| 31 | + { |
| 32 | + var result = new C2sChart |
| 33 | + { |
| 34 | + Version = "1.08.00\t1.08.00", |
| 35 | + Creator = ugc.Designer, |
| 36 | + DefBpm = ugc.BpmEvents.Count > 0 ? ugc.BpmEvents[0].Bpm : 120.0, |
| 37 | + }; |
| 38 | + foreach (var b in ugc.BpmEvents) |
| 39 | + result.BpmEvents.Add((b.Measure, ScaleDown(b.Offset, ugc.TicksPerBeat), b.Bpm)); |
| 40 | + foreach (var b in ugc.BeatEvents) |
| 41 | + result.MetEvents.Add((b.Measure, 0, b.Den, b.Num)); |
| 42 | + foreach (var n in ugc.Notes) |
| 43 | + result.Notes.Add(ScaleNote(n, ugc.TicksPerBeat)); |
| 44 | + return result; |
| 45 | + } |
| 46 | + |
| 47 | + if (chart is SusChart sus) |
| 48 | + { |
| 49 | + var result = new C2sChart { DefBpm = sus.Bpm }; |
| 50 | + result.BpmEvents.Add((0, 0, sus.Bpm)); |
| 51 | + foreach (var n in sus.Notes) |
| 52 | + result.Notes.Add(ScaleNote(n, sus.TicksPerBeat)); |
| 53 | + return result; |
| 54 | + } |
| 55 | + |
| 56 | + alerts.Add(new Alert(Warning, string.Format(Locale.ChuGeneratorUnsupported, "→ C2S"))); |
| 57 | + return new C2sChart(); |
| 58 | + } |
| 59 | + |
| 60 | + private static ChuNote ScaleNote(ChuNote n, int tpb) |
| 61 | + { |
| 62 | + int scaleDown(int v) => (int)((long)v * (C2sResolution / 4) / tpb); |
| 63 | + return new ChuNote |
| 64 | + { |
| 65 | + Type = n.Type, Measure = n.Measure, Offset = scaleDown(n.Offset), |
| 66 | + Cell = n.Cell, Width = n.Width, |
| 67 | + HoldDuration = scaleDown(n.HoldDuration), SlideDuration = scaleDown(n.SlideDuration), |
| 68 | + EndCell = n.EndCell, EndWidth = n.EndWidth, |
| 69 | + Extra = n.Extra, TargetNote = n.TargetNote, AirHoldDuration = scaleDown(n.AirHoldDuration), |
| 70 | + StartHeight = n.StartHeight, TargetHeight = n.TargetHeight, NoteColor = n.NoteColor, |
| 71 | + }; |
| 72 | + } |
| 73 | + |
| 74 | + private static int ScaleDown(int ticks, int tpb) => (int)((long)ticks * (C2sResolution / 4) / tpb); |
| 75 | + |
| 76 | + private static string Serialize(C2sChart chart) |
| 77 | + { |
| 78 | + var sb = new StringBuilder(); |
| 79 | + sb.AppendLine($"VERSION\t{chart.Version}"); |
| 80 | + sb.AppendLine($"MUSIC\t{chart.MusicId}"); |
| 81 | + sb.AppendLine("SEQUENCEID\t0"); |
| 82 | + sb.AppendLine($"DIFFICULT\t{chart.DifficultId:D2}"); |
| 83 | + sb.AppendLine("LEVEL\t0.0"); |
| 84 | + sb.AppendLine($"CREATOR\t{chart.Creator}"); |
| 85 | + sb.AppendLine($"BPM_DEF\t{Fmt(chart.DefBpm)}\t{Fmt(chart.DefBpm)}\t{Fmt(chart.DefBpm)}\t{Fmt(chart.DefBpm)}"); |
| 86 | + sb.AppendLine("MET_DEF\t4\t4"); |
| 87 | + sb.AppendLine($"RESOLUTION\t{chart.Resolution}"); |
| 88 | + sb.AppendLine($"CLK_DEF\t{chart.Resolution}"); |
| 89 | + sb.AppendLine("PROGJUDGE_BPM\t240.000"); |
| 90 | + sb.AppendLine("PROGJUDGE_AER\t0.999"); |
| 91 | + sb.AppendLine("TUTORIAL\t0"); |
| 92 | + sb.AppendLine(); |
| 93 | + |
| 94 | + foreach (var b in chart.BpmEvents) |
| 95 | + sb.AppendLine($"BPM\t{b.Measure}\t{b.Offset}\t{Fmt(b.Bpm)}"); |
| 96 | + foreach (var m in chart.MetEvents) |
| 97 | + sb.AppendLine($"MET\t{m.Measure}\t{m.Offset}\t{m.Denom}\t{m.Num}"); |
| 98 | + foreach (var s in chart.SflEvents) |
| 99 | + sb.AppendLine($"SFL\t{s.Measure}\t{s.Offset}\t{s.Duration}\t{Mlt(s.Multiplier)}"); |
| 100 | + sb.AppendLine(); |
| 101 | + |
| 102 | + foreach (var n in chart.Notes.OrderBy(n => n.Measure * C2sResolution + n.Offset)) |
| 103 | + sb.AppendLine(FormatNote(n)); |
| 104 | + |
| 105 | + sb.AppendLine(); |
| 106 | + return sb.ToString(); |
| 107 | + } |
| 108 | + |
| 109 | + private static string FormatNote(ChuNote n) => n.Type switch |
| 110 | + { |
| 111 | + "TAP" => $"TAP\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}", |
| 112 | + "CHR" => $"CHR\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.Extra}", |
| 113 | + "HLD" or "HXD" => $"{n.Type}\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.HoldDuration}", |
| 114 | + "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}", |
| 115 | + "FLK" => $"FLK\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.Extra}", |
| 116 | + "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}", |
| 117 | + "AHD" => $"AHD\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{n.AirHoldDuration}", |
| 118 | + "MNE" => $"MNE\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}", |
| 119 | + _ => $"TAP\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}" |
| 120 | + }; |
| 121 | + |
| 122 | + private static string Fmt(double v) => v.ToString("0.000", CultureInfo.InvariantCulture); |
| 123 | + private static string Mlt(double v) => v.ToString("0.000000", CultureInfo.InvariantCulture); |
| 124 | +} |
0 commit comments