Skip to content

Commit e465881

Browse files
committed
[+] MA2_103Generator
1 parent ac4c7b4 commit e465881

7 files changed

Lines changed: 297 additions & 110 deletions

File tree

generator/MA2Generator.cs

Lines changed: 149 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@
66

77
namespace MuConvert.generator;
88

9-
record MA2Line(string Name, int Bar, int Tick, int Key, string Extra = "");
10-
119
public class MA2Generator : IGenerator
1210
{
11+
protected record MA2Line(string Name, int Bar, int Tick, int Key, string Extra = "")
12+
{
13+
public override string ToString()
14+
{
15+
var extra = !string.IsNullOrEmpty(Extra) ? "\t" + Extra : "";
16+
return $"{Name}\t{Bar}\t{Tick}\t{Key}{extra}";
17+
}
18+
};
19+
1320
public MA2Generator(bool isUtage = false)
1421
{
1522
IsUtage = isUtage;
@@ -20,8 +27,9 @@ public MA2Generator(bool isUtage = false)
2027
public int MA2Version = 105;
2128
public int RSL = 384;
2229

23-
private List<MA2Line> lines = [];
24-
private readonly List<Alert> alerts = [];
30+
protected Chart chart;
31+
protected List<MA2Line> lines = [];
32+
protected readonly List<Alert> alerts = [];
2533

2634
private string headTemplate = @"VERSION 0.00.00 {0}
2735
FES_MODE {1}
@@ -36,7 +44,7 @@ GENERATED_BY MuConvert v{8}
3644

3745
private Rational __1_384 = new(1, 384);
3846

39-
private (decimal, decimal, decimal, decimal) bpmStats(Chart chart)
47+
private (decimal, decimal, decimal, decimal) bpmStats()
4048
{
4149
var bpms = chart.BpmList.Select(x => x.Bpm).ToList();
4250
var max = bpms.Max();
@@ -55,14 +63,19 @@ GENERATED_BY MuConvert v{8}
5563
}
5664

5765
// 持续时间/等待时间,使用"总tick数"(可超过1小节),不是小节内tick
58-
private int T(Rational r, int offset = 0)
66+
protected int T(Rational r, int offset = 0)
5967
{
6068
var result = (int)Math.Round((double)(r* RSL));
6169
if (offset != 0) result = Math.Max(result + offset, result > 0 ? 1 : 0);
6270
return result;
6371
}
6472

65-
private void AddTap(Tap tap, int bar, int tick)
73+
protected void Warn(string description, Note note, MA2Line? ma2Line = null)
74+
{
75+
alerts.Add(new Alert(Warning, description, (chart, note.Time), lines.Count + 1, ma2Line?.ToString()));
76+
}
77+
78+
protected virtual MA2Line? AddTap(Tap tap, int bar, int tick)
6679
{
6780
var prefix = "NM";
6881
if (tap.IsBreak && tap.IsEx) prefix = "BX";
@@ -76,138 +89,156 @@ private void AddTap(Tap tap, int bar, int tick)
7689
name = "HLD";
7790
extra = T(hold.Duration.Bar, -hold.FalseEachIdx).ToString();
7891
}
79-
lines.Add(new MA2Line(prefix + name, bar, tick, tap.Key - 1, extra));
92+
return new MA2Line(prefix + name, bar, tick, tap.Key - 1, extra);
8093
}
81-
82-
public (string, List<Alert>) Generate(Chart chart)
94+
95+
protected virtual List<MA2Line> AddSlide(Slide slide, int bar, int tick)
8396
{
84-
if (lines.Count != 0) throw new Exception(Locale.InstanceMultipleUsage);
85-
chart.Sort();
86-
StringBuilder result = new StringBuilder();
97+
List<MA2Line> result = [];
98+
if (slide.OwnHead != null)
99+
{
100+
var headTap = AddTap(slide.OwnHead, bar, tick);
101+
if (headTap != null) result.Add(headTap);
102+
}
103+
104+
// 首先很重要的一点是,详见 https://github.com/Neskol/MaiLib/issues/46#issuecomment-3301893924 ,
105+
// 官机现在对于多段星星,是会无视掉每一段分别指定的时长,把总时长加和然后全程匀速处理的。
106+
// 至少在我上述测试的版本是这样;但为了防止万一我测试错了、或者将来相关的行为改变,这里还是尊重chart原始记法、分两类处理。
107+
var totalLen = T(slide.Duration.Bar);
108+
109+
# region 把时长平均分配给所有没有显式写出时长的段
110+
List<int?> segmentValue = [];
111+
var unassignedValue = totalLen;
112+
var unassignedCount = 0;
113+
for (int i = 0; i < slide.segments.Count - 1; i++)
114+
{
115+
var seg = slide.segments[i];
116+
if (seg.Duration != null)
117+
{
118+
var t = T(seg.Duration.Bar);
119+
segmentValue.Add(t);
120+
unassignedValue -= t;
121+
}
122+
else
123+
{
124+
segmentValue.Add(null);
125+
unassignedCount++;
126+
}
127+
}
128+
unassignedCount++; // 对应于最后一段
129+
var toAssignValue = unassignedValue / unassignedCount; // 未分配的时间分配给所有未分配段,每段分配到的量
130+
# endregion
87131

88-
// 文件头
89-
var bpmStatistics = bpmStats(chart);
132+
int segIdx;
133+
for (segIdx = 0; segIdx < slide.segments.Count; segIdx++)
134+
{
135+
var seg = slide.segments[segIdx];
136+
var len = segIdx == slide.segments.Count - 1 ?
137+
totalLen : // 对于最后一段,剩的时间全给它。以保证总长是正确的。
138+
segmentValue[segIdx] ?? toAssignValue; // 除此之外,则是优先使用显式分配的时间、没有则使用平均时间
139+
totalLen -= len;
140+
int waitTime = 0;
141+
142+
var prefix = "NM";
143+
if (segIdx == 0)
144+
{
145+
if (slide.IsBreak) prefix = "BR";
146+
waitTime = T(slide.WaitTime.Bar, -slide.FalseEachIdx);
147+
}
148+
else prefix = "CN";
149+
150+
var name = seg.Type.ToString();
151+
152+
result.Add(new MA2Line(prefix + name, bar, tick, seg.StartKey - 1,
153+
string.Join("\t", [waitTime, len, seg.EndKey - 1])));
154+
tick += waitTime + len;
155+
while (tick >= RSL) { tick -= RSL; bar++; }
156+
}
157+
158+
return result;
159+
}
160+
161+
protected virtual MA2Line? AddTouch(Touch touch, int bar, int tick)
162+
{
163+
const string prefix = "NM"; // touch目前只有normal的
164+
var name = "TTP";
165+
List<string> extras = [];
166+
if (touch is TouchHold th)
167+
{
168+
name = "THO";
169+
extras.Add(T(th.Duration.Bar, -th.FalseEachIdx).ToString());
170+
}
171+
172+
var area = touch.TouchArea[0];
173+
var key = area != 'C' ? touch.Key - 1 : 0; // 目前,官机还不支持C1和C2分别写touch
174+
extras.Add(area.ToString());
175+
extras.Add(touch.IsFirework ? "1" : "0");
176+
extras.Add(touch.TouchSize);
177+
178+
return new MA2Line(prefix + name, bar, tick, key, string.Join("\t", extras));
179+
}
180+
181+
// 生成文件头
182+
protected void GenerateFileHead(StringBuilder result)
183+
{
184+
var bpmStatistics = bpmStats();
90185
string head = string.Format(headTemplate,
91186
$"{MA2Version / 100}.{MA2Version % 100:D2}.00", IsUtage?1:0,
92187
bpmStatistics.Item1, bpmStatistics.Item2, bpmStatistics.Item3, bpmStatistics.Item4,
93188
RSL, RSL/4 * chart.ClockCount, Utils.AppVersion);
94189
result.Append(head);
95-
96-
// bpm段
190+
}
191+
192+
// 生成BPM段
193+
protected void GenerateBPM(StringBuilder result)
194+
{
97195
foreach (var bpm in chart.BpmList)
98196
{
99197
var (bar, tick) = BT(bpm.Time);
100198
result.AppendLine($"BPM\t{bar}\t{tick}\t{bpm.Bpm:F3}");
101199
}
102200
result.AppendLine($"MET\t0\t0\t4\t{chart.ClockCount}");
103201
result.AppendLine();
104-
105-
// 主体:音符段
106-
// 由于fes星星涉及一个重排序的问题,同时也为了后面统计方便,先把note放进lines数组中最后一块写入,而不是直接写入文件
202+
}
203+
204+
// 生成主体音符段
205+
protected void GenerateNotes(StringBuilder result)
206+
{
207+
// 由于fes星星涉及一个重排序的问题,同时也为了后面统计方便,我们先调用GenerateMA2Lines、把音符转为适合直接写入的表示并放进lines数组中,最后再一块写入StringBuilder。
107208
for (int noteIdx = 0; noteIdx < chart.Notes.Count; noteIdx++)
108209
{
109210
var note = chart.Notes[noteIdx];
110211
if (noteIdx > 0)
111212
{
112213
var distToPrev = note.Time - chart.Notes[noteIdx - 1].Time;
113-
if (distToPrev > 0 && distToPrev < __1_384)
114-
{
115-
alerts.Add(new Alert(Warning, Locale.NoteTooNear, (chart, note.Time)));
116-
}
214+
if (distToPrev > 0 && distToPrev < __1_384) Warn(Locale.NoteTooNear, note);
117215
}
118216

119217
var (bar, tick) = BT(note.Time, note.FalseEachIdx);
120218
if (note is Tap tap)
121219
{
122-
AddTap(tap, bar, tick);
220+
var l = AddTap(tap, bar, tick);
221+
if (l != null) lines.Add(l);
123222
}
124223
else if (note is Touch touch)
125224
{
126-
const string prefix = "NM"; // touch目前只有normal的
127-
var name = "TTP";
128-
List<string> extras = [];
129-
if (note is TouchHold th)
130-
{
131-
name = "THO";
132-
extras.Add(T(th.Duration.Bar, -th.FalseEachIdx).ToString());
133-
}
134-
135-
var area = touch.TouchArea[0];
136-
var key = area != 'C' ? touch.Key - 1 : 0; // 目前,官机还不支持C1和C2分别写touch
137-
extras.Add(area.ToString());
138-
extras.Add(touch.IsFirework ? "1" : "0");
139-
extras.Add(touch.TouchSize);
140-
141-
lines.Add(new MA2Line(prefix + name, bar, tick, key, string.Join("\t", extras)));
225+
var l = AddTouch(touch, bar, tick);
226+
if (l != null) lines.Add(l);
142227
}
143228
else if (note is Slide slide)
144229
{
145-
if (slide.OwnHead != null) AddTap(slide.OwnHead, bar, tick);
146-
147-
// 首先很重要的一点是,详见 https://github.com/Neskol/MaiLib/issues/46#issuecomment-3301893924 ,
148-
// 官机现在对于多段星星,是会无视掉每一段分别指定的时长,把总时长加和然后全程匀速处理的。
149-
// 至少在我上述测试的版本是这样;但为了防止万一我测试错了、或者将来相关的行为改变,这里还是尊重chart原始记法、分两类处理。
150-
var totalLen = T(slide.Duration.Bar);
151-
152-
# region 把时长平均分配给所有没有显式写出时长的段
153-
List<int?> segmentValue = [];
154-
var unassignedValue = totalLen;
155-
var unassignedCount = 0;
156-
for (int i = 0; i < slide.segments.Count - 1; i++)
157-
{
158-
var seg = slide.segments[i];
159-
if (seg.Duration != null)
160-
{
161-
var t = T(seg.Duration.Bar);
162-
segmentValue.Add(t);
163-
unassignedValue -= t;
164-
}
165-
else
166-
{
167-
segmentValue.Add(null);
168-
unassignedCount++;
169-
}
170-
}
171-
unassignedCount++; // 对应于最后一段
172-
var toAssignValue = unassignedValue / unassignedCount; // 未分配的时间分配给所有未分配段,每段分配到的量
173-
# endregion
174-
175-
int segIdx;
176-
for (segIdx = 0; segIdx < slide.segments.Count; segIdx++)
177-
{
178-
var seg = slide.segments[segIdx];
179-
var len = segIdx == slide.segments.Count - 1 ?
180-
totalLen : // 对于最后一段,剩的时间全给它。以保证总长是正确的。
181-
segmentValue[segIdx] ?? toAssignValue; // 除此之外,则是优先使用显式分配的时间、没有则使用平均时间
182-
totalLen -= len;
183-
int waitTime = 0;
184-
185-
var prefix = "NM";
186-
if (segIdx == 0)
187-
{
188-
if (slide.IsBreak) prefix = "BR";
189-
waitTime = T(slide.WaitTime.Bar, -slide.FalseEachIdx);
190-
}
191-
else prefix = "CN";
192-
193-
var name = seg.Type.ToString();
194-
195-
lines.Add(new MA2Line(prefix + name, bar, tick, seg.StartKey - 1,
196-
string.Join("\t", [waitTime, len, seg.EndKey - 1])));
197-
tick += waitTime + len;
198-
while (tick >= RSL) { tick -= RSL; bar++; }
199-
}
230+
var ls = AddSlide(slide, bar, tick);
231+
foreach (var l in ls) lines.Add(l);
200232
}
201233
}
202-
234+
203235
lines = lines.OrderBy(x => x.Bar * RSL + x.Tick).ToList();
204-
foreach (var l in lines)
205-
{
206-
var extra = !string.IsNullOrEmpty(l.Extra) ? "\t" + l.Extra : "";
207-
result.AppendLine($"{l.Name}\t{l.Bar}\t{l.Tick}\t{l.Key}{extra}");
208-
}
236+
foreach (var l in lines) result.AppendLine(l.ToString());
209237
result.AppendLine();
210-
238+
}
239+
240+
protected void GenerateStatistics(StringBuilder result)
241+
{
211242
// 统计段
212243
var stats = chart.Statistics;
213244
foreach (var (k, v) in statsNameConversion())
@@ -250,11 +281,25 @@ private void AddTap(Tap tap, int bar, int tick)
250281
result.AppendLine($"TTM_SCR_S\t{Math.Ceiling(score_sss * 0.97 / 50) * 50}");
251282
result.AppendLine($"TTM_SCR_SS\t{score_sss}");
252283
result.AppendLine($"TTM_RAT_ACV\t{(long)theoryScore * 10000 / score_sss }"); // 用long避免溢出
284+
result.AppendLine();
285+
}
286+
287+
public (string, List<Alert>) Generate(Chart _chart)
288+
{
289+
if (chart != null) throw new Exception(Locale.InstanceMultipleUsage);
290+
chart = _chart;
291+
chart.Sort();
292+
StringBuilder result = new StringBuilder();
293+
294+
GenerateFileHead(result);
295+
GenerateBPM(result);
296+
GenerateNotes(result);
297+
GenerateStatistics(result);
253298

254299
return (result.ToString(), alerts);
255300
}
256301

257-
private Dictionary<string, string> statsNameConversion() => new()
302+
protected virtual Dictionary<string, string> statsNameConversion() => new()
258303
{
259304
["TAP"] = "NMTAP", ["BRK"] = "BRTAP", ["XTP"] = "EXTAP", ["BXX"] = "BXTAP",
260305
["HLD"] = "NMHLD", ["XHO"] = "EXHLD", ["BHO"] = "BRHLD", ["BXH"] = "BXHLD",

0 commit comments

Comments
 (0)