Skip to content

Commit 41f6803

Browse files
committed
[+] OgkrGenerator
1 parent 14a985c commit 41f6803

3 files changed

Lines changed: 333 additions & 7 deletions

File tree

generator/ogk/OgkrGenerator.cs

Lines changed: 329 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,338 @@
1-
using MuConvert.generator;
1+
using System.Text;
2+
using MuConvert.generator;
23
using MuConvert.utils;
4+
using static MuConvert.utils.Alert.LEVEL;
35

46
namespace MuConvert.ogk;
57

68
public class OgkrGenerator: IGenerator<OgkChart>
79
{
10+
// 除非你知道你在做什么,不然以下两个变量请勿修改!
11+
public int RSL = 1920;
12+
public int RSL_X = 4096;
13+
14+
private List<Alert> alerts = [];
815
public (string, List<Alert>) Generate(OgkChart chart)
916
{
10-
throw new NotImplementedException();
17+
chart.Sort();
18+
19+
// 各类“需要分配ID”的对象,这里集中排序、ID分配好。
20+
var orderedLanes = SortLanesByTypeGroup(chart.Lanes);
21+
var generalLanes = AssignGroupIds(orderedLanes, chart.EnemyMovements); // EnemyMovements已经在chart.Sort()里排好序了,无需再排
22+
var paletteIds = AssignPaletteIds(chart);
23+
24+
var sb = new StringBuilder();
25+
sb.AppendLine();
26+
27+
EmitHeader(sb, chart);
28+
sb.AppendLine();
29+
sb.AppendLine();
30+
31+
EmitBPalette(sb, paletteIds);
32+
sb.AppendLine();
33+
sb.AppendLine();
34+
35+
EmitComposition(sb, chart);
36+
sb.AppendLine();
37+
sb.AppendLine();
38+
39+
EmitLane(sb, generalLanes);
40+
sb.AppendLine();
41+
42+
EmitLaneBlock(sb, generalLanes);
43+
sb.AppendLine();
44+
sb.AppendLine();
45+
46+
EmitBullet(sb, chart, paletteIds);
47+
sb.AppendLine();
48+
sb.AppendLine();
49+
50+
EmitBeam(sb, chart);
51+
sb.AppendLine();
52+
sb.AppendLine();
53+
54+
EmitBell(sb, chart, paletteIds);
55+
sb.AppendLine();
56+
sb.AppendLine();
57+
58+
EmitFlick(sb, chart);
59+
sb.AppendLine();
60+
sb.AppendLine();
61+
62+
EmitNotes(sb, chart, generalLanes);
63+
sb.AppendLine();
64+
sb.AppendLine();
65+
66+
sb.AppendLine("[TOTAL]");
67+
sb.AppendLine();
68+
69+
return (sb.ToString(), alerts);
70+
}
71+
72+
// 遵循官谱的顺序:首先按照Lane的类型(左右墙、红绿蓝线、彩色线),再按时间顺序
73+
private static List<Lane> SortLanesByTypeGroup(List<Lane> lanes)
74+
{
75+
int Order(Lane l) => l switch
76+
{
77+
Wall w when w.Direction == Direction.L => 0,
78+
Wall w when w.Direction == Direction.R => 1,
79+
_ => l.Type switch
80+
{
81+
LaneType.Red => 2,
82+
LaneType.Green => 3,
83+
LaneType.Blue => 4,
84+
LaneType.Colorful => 5,
85+
_ => 99,
86+
}
87+
};
88+
// 输入的lanes已经被chart.Sort()按Time升序排好序了,这里只需再做一次按类型的稳定排序即可。
89+
return lanes.OrderBy(Order).ToList();
90+
}
91+
92+
private static Dictionary<OgkBaseLane<OgkLanePoint>, int> AssignGroupIds(List<Lane> lanes, List<EnemyMovement> enemies)
93+
{
94+
var ids = new Dictionary<OgkBaseLane<OgkLanePoint>, int>();
95+
var c = 0;
96+
foreach (var l in lanes) ids[l] = c++;
97+
foreach (var e in enemies) ids[e] = c++;
98+
return ids;
99+
}
100+
101+
// 给所有BulletPallete分配字符串ID。按其在Bullets/Bells中首次出现的顺序分配,同时去重(按record的结构相等性)。
102+
private static Dictionary<BulletPallete, string> AssignPaletteIds(OgkChart chart)
103+
{
104+
var ids = new Dictionary<BulletPallete, string>();
105+
var counter = 360; // 起始值为 A0 = 10 * 36
106+
void Add(BulletPallete? p)
107+
{
108+
if (p == null) return;
109+
if (!ids.ContainsKey(p)) ids[p] = Utils.IToH(counter++, 36);
110+
}
111+
112+
foreach (var bul in chart.Bullets) Add(bul.Detail);
113+
foreach (var note in chart.Notes)
114+
if (note is IBullet bul) Add(bul.Detail);
115+
return ids;
116+
}
117+
118+
private void EmitHeader(StringBuilder sb, OgkChart chart)
119+
{
120+
sb.AppendLine("[HEADER]");
121+
sb.AppendLine("VERSION\t1\t7\t0");
122+
sb.AppendLine($"CREATOR\t{chart.Designer}");
123+
var (first, common, max, min) = chart.BpmList.BPM_DEF();
124+
sb.AppendLine(FormattableString.Invariant($"BPM_DEF\t{first:F3}\t{common:F3}\t{max:F3}\t{min:F3}"));
125+
var firstMet = chart.MetList[0];
126+
sb.AppendLine($"MET_DEF\t{firstMet.Numerator}\t{firstMet.Denominator}");
127+
sb.AppendLine($"TRESOLUTION\t{RSL}");
128+
sb.AppendLine($"XRESOLUTION\t{RSL_X}");
129+
sb.AppendLine($"CLK_DEF\t{chart.ClockCount * (RSL / 4)}");
130+
sb.AppendLine(FormattableString.Invariant($"PROGJUDGE_BPM\t{chart.ProgJudgeBpm:F3}"));
131+
sb.AppendLine("TUTORIAL\t0");
132+
sb.AppendLine(FormattableString.Invariant($"BULLET_DAMAGE\t{chart.BulletDamages[BulletDamage.NML]:F3}"));
133+
sb.AppendLine(FormattableString.Invariant($"HARDBULLET_DAMAGE\t{chart.BulletDamages[BulletDamage.STR]:F3}"));
134+
sb.AppendLine(FormattableString.Invariant($"DANGERBULLET_DAMAGE\t{chart.BulletDamages[BulletDamage.DNG]:F3}"));
135+
sb.AppendLine(FormattableString.Invariant($"BEAM_DAMAGE\t{chart.BeamDamage:F3}"));
136+
137+
var counts = chart.CountNotes();
138+
sb.AppendLine($"T_TOTAL\t{counts["T_TOTAL"]}");
139+
sb.AppendLine($"T_TAP\t{counts["T_TAP"]}");
140+
sb.AppendLine($"T_HOLD\t{counts["T_HOLD"]}");
141+
sb.AppendLine($"T_SIDE\t{counts["T_SIDE"]}");
142+
sb.AppendLine($"T_SHOLD\t{counts["T_SHOLD"]}");
143+
sb.AppendLine($"T_FLICK\t{counts["T_FLICK"]}");
144+
sb.AppendLine($"T_BELL\t{counts["T_BELL"]}");
145+
}
146+
147+
private void EmitBPalette(StringBuilder sb, Dictionary<BulletPallete, string> palettes)
148+
{
149+
sb.AppendLine("[B_PALETTE]");
150+
foreach (var (palette, id) in palettes)
151+
{
152+
sb.AppendLine(FormattableString.Invariant(
153+
$"BPL\t{id}\t{palette.Shooter}\t{palette.TargetOffset}\t{(palette.TargetToPlayer ? "PLR" : "FIX")}\t{palette.Speed:F6}\t{palette.Size}\t{palette.Type}\t{palette.RandomOffsetDist}"));
154+
}
155+
}
156+
157+
private void EmitComposition(StringBuilder sb, OgkChart chart)
158+
{
159+
sb.AppendLine("[COMPOSITION]");
160+
161+
// BPM:跳过首项(隐含于HEADER的BPM_DEF)
162+
foreach (var b in chart.BpmList.Skip(1))
163+
{
164+
var (t, g) = Utils.BarAndTick(b.Time, RSL);
165+
sb.AppendLine(FormattableString.Invariant($"BPM\t{t}\t{g}\t{b.Bpm:F3}"));
166+
}
167+
168+
// MET:跳过首项(隐含于HEADER的MET_DEF)。
169+
// ogkr中无论MET_DEF还是MET,字段顺序都是Numerator在前、Denominator在后,与maimai刚好相反
170+
foreach (var m in chart.MetList.Skip(1))
171+
{
172+
var (t, g) = Utils.BarAndTick(m.Time, RSL);
173+
sb.AppendLine($"MET\t{t}\t{g}\t{m.Numerator}\t{m.Denominator}");
174+
}
175+
176+
// SFL
177+
foreach (var (time, duration, mult) in chart.SflList)
178+
{
179+
var (t, g) = Utils.BarAndTick(time, RSL);
180+
var durTicks = Utils.Tick(duration, RSL);
181+
sb.AppendLine(FormattableString.Invariant($"SFL\t{t}\t{g}\t{durTicks}\t{mult:F6}"));
182+
}
183+
184+
// CLK(仅当显式声明时输出)
185+
if (chart.ExplicitClocks != null)
186+
{
187+
foreach (var time in chart.ExplicitClocks)
188+
{
189+
var (t, g) = Utils.BarAndTick(time, RSL);
190+
sb.AppendLine($"CLK\t{t}\t{g}");
191+
}
192+
}
193+
194+
// EST
195+
foreach (var (time, tag) in chart.EnemyList)
196+
{
197+
var (t, g) = Utils.BarAndTick(time, RSL);
198+
sb.AppendLine($"EST\t{t}\t{g}\t{tag}");
199+
}
200+
}
201+
202+
private void EmitOneLane<T>(StringBuilder sb, OgkBaseLane<T> lane, int id) where T: OgkLanePoint
203+
{
204+
var cmdRoot = lane switch
205+
{
206+
Wall { Direction: Direction.L } => "WL",
207+
Wall { Direction: Direction.R } => "WR",
208+
ColorfulLane => "CL",
209+
EnemyMovement => "EN",
210+
Lane { Type: LaneType.Red } => "LL",
211+
Lane { Type: LaneType.Green } => "LC",
212+
Lane { Type: LaneType.Blue } => "LR",
213+
Beam { ObliqueOffset: 0 } => "BM",
214+
Beam b when b.ObliqueOffset != 0 => "OB",
215+
_ => throw new InvalidOperationException($"Unsupported lane {lane} for emission"),
216+
};
217+
218+
var n = lane.Points.Count;
219+
for (var i = 0; i < n; i++)
220+
{
221+
var cmd = cmdRoot + (i == 0 ? "S" : i == n - 1 ? "E" : "N");
222+
var p = lane.Points[i];
223+
var (t, g) = Utils.BarAndTick(p.Time, RSL);
224+
var result = $"{cmd}\t{id}\t{t}\t{g}\t{p.Pos}";
225+
226+
if (lane is ColorfulLane cl)
227+
{ // 对ColorfulLane,加上colorId和brightnessId;
228+
result += $"\t{(int)cl.Color}\t{cl.Brightness}";
229+
}
230+
if (i == 0 && lane is Lane { IsTransparent: true })
231+
{ // 最后再加上表示transparent的1
232+
result += "\t1";
233+
}
234+
if (lane is Beam beam && p is BeamPoint pp)
235+
{ // 对Beam类型,添加上特有的属性
236+
result += $"\t{pp.Width}";
237+
if (beam.ObliqueOffset != 0) result += $"\t{beam.ObliqueOffset}"; // 对OBS/OBN/OBE,所有行都要带上shootPosXUnitOffset字段(虽然仅OBS的值有意义)。
238+
}
239+
sb.AppendLine(result);
240+
}
241+
sb.AppendLine();
242+
}
243+
244+
private void EmitLane(StringBuilder sb, Dictionary<OgkBaseLane<OgkLanePoint>, int> lanes)
245+
{
246+
sb.AppendLine("[LANE]");
247+
foreach (var (lane, id) in lanes)
248+
EmitOneLane(sb, lane, id);
249+
}
250+
251+
private void EmitLaneBlock(StringBuilder sb, Dictionary<OgkBaseLane<OgkLanePoint>, int> ids)
252+
{
253+
sb.AppendLine("[LANE_BLOCK]");
254+
255+
// 收集所有Wall上的Blocks,按“绝对的Fore时刻”排序后输出。
256+
// 同一Fore时刻有多个block时,右墙的block在前(与官谱观察到的惯例一致)。
257+
var entries = ids.Keys.OfType<Wall>()
258+
.SelectMany(w => w.Blocks.Select(b => (Wall: w, Block: b, ForeTime: w.Time + b.Start)))
259+
.OrderBy(x => x.ForeTime)
260+
.ThenBy(x => x.Wall.Direction == Direction.R ? 0 : 1)
261+
.ToList();
262+
263+
foreach (var (wall, block, foreTime) in entries)
264+
{
265+
var rearTime = foreTime + block.Duration;
266+
var (fT, fG) = Utils.BarAndTick(foreTime, RSL);
267+
var (rT, rG) = Utils.BarAndTick(rearTime, RSL);
268+
var (fXU, fXG) = Utils.BarAndTick(block.Pos, RSL_X);
269+
var (rXU, rXG) = Utils.BarAndTick(block.EndPos, RSL_X);
270+
sb.AppendLine($"LBK\t{ids[wall]}\t{fT}\t{fG}\t{fXU}\t{fXG}\t{rT}\t{rG}\t{rXU}\t{rXG}");
271+
}
272+
}
273+
274+
private void EmitBullet(StringBuilder sb, OgkChart chart, Dictionary<BulletPallete, string> palettes)
275+
{
276+
sb.AppendLine("[BULLET]");
277+
foreach (var bullet in chart.Bullets.OfType<Bullet>())
278+
{
279+
var (t, g) = Utils.BarAndTick(bullet.Time, RSL);
280+
sb.AppendLine($"BLT\t{palettes[bullet.Detail]}\t{t}\t{g}\t{bullet.Pos}\t{bullet.Damage}");
281+
}
282+
}
283+
284+
private void EmitBeam(StringBuilder sb, OgkChart chart)
285+
{
286+
sb.AppendLine("[BEAM]");
287+
var recordId = 0;
288+
foreach (var beam in chart.Bullets.OfType<Beam>())
289+
EmitOneLane(sb, beam, recordId++);
290+
}
291+
292+
private void EmitBell(StringBuilder sb, OgkChart chart, Dictionary<BulletPallete, string> palettes)
293+
{
294+
sb.AppendLine("[BELL]");
295+
foreach (var bell in chart.Notes.OfType<Bell>())
296+
{
297+
var (t, g) = Utils.BarAndTick(bell.Time, RSL);
298+
var palId = bell.Detail != null ? palettes[bell.Detail] : "--";
299+
sb.AppendLine($"BEL\t{t}\t{g}\t{bell.Pos.Round()}\t{palId}");
300+
}
301+
}
302+
303+
private void EmitFlick(StringBuilder sb, OgkChart chart)
304+
{
305+
sb.AppendLine("[FLICK]");
306+
foreach (var flick in chart.Notes.OfType<Flick>())
307+
{
308+
var (t, g) = Utils.BarAndTick(flick.Time, RSL);
309+
var cmd = flick.IsEx ? "CFK" : "FLK";
310+
sb.AppendLine($"{cmd}\t{t}\t{g}\t{flick.Pos.Round()}\t{flick.Direction}");
311+
}
312+
}
313+
314+
private void EmitNotes(StringBuilder sb, OgkChart chart, Dictionary<OgkBaseLane<OgkLanePoint>, int> laneIds)
315+
{
316+
sb.AppendLine("[NOTES]");
317+
foreach (var tap in chart.Notes.OfType<Tap>())
318+
{
319+
if (!laneIds.TryGetValue(tap.Lane, out var laneId))
320+
{
321+
alerts.Add(new Alert(Warning, "音符所引用的Lane未在LANE段中登记", (chart, tap.Time)));
322+
continue;
323+
}
324+
var cmd = tap.IsEx ? "CTP" : "TAP";
325+
var (t, g) = Utils.BarAndTick(tap.Time, RSL);
326+
var (xU, xG) = Utils.BarAndTick(tap.Pos, RSL_X);
327+
var paramStr = $"{laneId}\t{t}\t{g}\t{xU}\t{xG}";
328+
if (tap is Hold hold)
329+
{
330+
cmd = hold.IsEx ? "CHD" : "HLD";
331+
var (rT, rG) = Utils.BarAndTick(hold.EndTime, RSL);
332+
var (rXU, rXG) = Utils.BarAndTick(hold.EndPos, RSL_X);
333+
paramStr += $"\t{rT}\t{rG}\t{rXU}\t{rXG}";
334+
}
335+
sb.AppendLine($"{cmd}\t{paramStr}");
336+
}
11337
}
12-
}
338+
}

parser/ogk/OgkrParser.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,8 @@ private void ParseComposition(string[] p, int lineNo)
201201
chart.BpmList.Add(new BPM(ToBar(ParseInt(p[1]), ParseInt(p[2])), ParseDec(p[3])));
202202
break;
203203
case "MET":
204-
chart.MetList.Add(new MET(ToBar(ParseInt(p[1]), ParseInt(p[2])), ParseInt(p[4]), ParseInt(p[3])));
204+
// MET tUnit tGrid numerator denominator
205+
chart.MetList.Add(new MET(ToBar(ParseInt(p[1]), ParseInt(p[2])), ParseInt(p[3]), ParseInt(p[4])));
205206
break;
206207
case "SFL":
207208
// SFL tUnit tGrid tGridLength speed

utils/Utils.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,12 @@ public static BigInteger Ceil(this Rational r)
162162
}
163163

164164
private static readonly Rational _half = new(1, 2);
165-
// 工作范围仅限非负数;舍入策略方面,使用与系统库Math.Round相同的“四舍六入五成双”算法。
165+
// 舍入策略方面,使用与系统库Math.Round相同的“四舍六入五成双”算法。
166166
public static BigInteger Round(this Rational r)
167167
{
168-
if (r < 0) throw new ArgumentOutOfRangeException(nameof(r));
169168
var whole = r.WholePart;
170169
var frac = r.FractionPart;
171-
var shouldAdd = frac > _half || (frac == _half && whole % 2 == 1);
170+
var shouldAdd = frac > _half || (frac == _half && !whole.IsEven);
172171
return whole + (shouldAdd ? 1 : 0);
173172
}
174173

0 commit comments

Comments
 (0)