diff --git a/generator/chu/UgcGenerator.cs b/generator/chu/UgcGenerator.cs index 0a7bcc7..c39b6e6 100644 --- a/generator/chu/UgcGenerator.cs +++ b/generator/chu/UgcGenerator.cs @@ -12,7 +12,7 @@ public class UgcGenerator : IGenerator private int RSL = 480 * 4; private List alerts = []; public List<(string, string)> ExtraHeaders = []; - + private int useTil = 0; // 当前的 @USETIL 值 /** @@ -45,7 +45,7 @@ private List SortedNotesForConnectingPrevious(ChuChart chart) // 2. 遍历 chart.Notes,对每个 ChuNote 以 DFS 方式把它本身以及它所有 Next 子孙依次加入结果。 var result = new List(chart.Notes.Count); var visited = new HashSet(); - foreach (var root in chart.Notes.Where(x=>x.TargetNote == null)) Dfs(root); + foreach (var root in chart.Notes.Where(x => x.TargetNote == null)) Dfs(root); return result; void Dfs(ChuNote n) @@ -86,12 +86,24 @@ void Dfs(ChuNote n) } // 为了实现从上述 T函数 中的换算,所必要的信息。可通过CalcUgcBeats函数算出。 private List<(int, int, int)> _ugcBeats = []; - - private void FillUgcBeats(List metList) + + private void FillUgcBeats(ChuChart chart) { _ugcBeats = []; - foreach (var met in metList) + foreach (var origMet in chart.MetList) { + var met = origMet; + if (met.Numerator <= 0) + { + // See https://github.com/MuNET-OSS/MuConvert/pull/7#discussion_r4217218971 , + // in official game, met.Numerator == 0 means there should be no "beat lines", which cannot be easily implemented in Umiguri. + // But this will cause DivideByZeroException in the `T` function, so we just force met.Numerator to be equals with met.Denominator. + alerts.Add(new Alert(Alert.LEVEL.Info, $"UgcGenerator 不支持使用MET拍号的分子小于等于0,来取消节拍提示线的语法。" + + $"该处MET已被等效为\"MET {met.Time.WholePart} {(met.Time.FractionPart * 384).Round()} {met.Denominator} {met.Denominator}\",即节拍提示线还是会正常显示。", + (chart, met.Time), relevantNote: $"MET {met.Time.WholePart} {(met.Time.FractionPart * 384).Round()} {met.Denominator} {met.Numerator}")); + met = met with { Numerator = met.Denominator }; + } + if (_ugcBeats.Count == 0) { if (met.Time > 0) _ugcBeats.Add((0, 4, 4)); // 鲁棒性,补 @BEAT 0 4 4。不能continue,因为马上还要添加显式的那一条。 @@ -112,13 +124,15 @@ private void FillUgcBeats(List metList) } _ugcBeats.Add((ugcBar, met.Numerator, met.Denominator)); } + + if (_ugcBeats.Count == 0) _ugcBeats.Add((0, 4, 4)); } private string Serialize(ChuChart ugc) { ugc.Sort(); - FillUgcBeats(ugc.MetList); - + FillUgcBeats(ugc); + var sb = new StringBuilder(); sb.AppendLine($"' Created with MuConvert v{Utils.AppVersion}"); sb.AppendLine("@VER\t8"); @@ -161,15 +175,15 @@ private string Serialize(ChuChart ugc) tilList[((t.Time + t.Duration).CanonicalForm, groupId)] = 1; } } - + foreach (var s in tilList.ToList() - .OrderBy(x=>(x.Key.time, x.Key.groupId))) - { - var (m, o) = T(s.Key.time); + .OrderBy(x => (x.Key.time, x.Key.groupId))) + { + var (m, o) = T(s.Key.time); sb.AppendLine(FormattableString.Invariant($"@TIL\t{s.Key.groupId}\t{m}'{o}\t{s.Value:0.00000}")); } #endregion - + sb.AppendLine("@MAINTIL\t0"); // 用户没有通过ExtraHeaders指定,则提供一个默认值 sb.AppendLine("@ENDHEAD"); sb.AppendLine(); @@ -232,7 +246,7 @@ private void AppendFollowerLines(StringBuilder sb, ChuNote n) } private static string EncodeAirHeight(decimal value) => IToH36(Math.Clamp((int)Math.Round(Height_ToUgc(value) * 10), 0, 1295)).PadLeft(2, '0'); - + private string AirColor(ChuNote n) { var color = AirColor_ToUgc(n); @@ -241,7 +255,7 @@ private string AirColor(ChuNote n) return color; } private string CrushColor(ChuNote n) => AirCrush_Color_ToUgc[n.Color]; - private string CrushInterval(Rational? crushInterval) => + private string CrushInterval(Rational? crushInterval) => crushInterval != null ? Utils.Tick(crushInterval.Value, RSL).ToString() : "$"; private string UCode(ChuNote n) diff --git a/tests/chu/UgcTimeTests.cs b/tests/chu/UgcTimeTests.cs index edc56c2..c706bab 100644 --- a/tests/chu/UgcTimeTests.cs +++ b/tests/chu/UgcTimeTests.cs @@ -84,8 +84,8 @@ private static void AssertBeatEntriesEqual( } } - private static void FillUgcBeats(UgcGenerator gen, List metList) - => InvokeInstance(gen, "FillUgcBeats", metList); + private static void FillUgcBeats(UgcGenerator gen, ChuChart chart) + => InvokeInstance(gen, "FillUgcBeats", chart); private static List<(int Bar, int Num, int Den)> GetGeneratorUgcBeats(UgcGenerator gen) => GetInstanceField>(gen, "_ugcBeats"); @@ -100,10 +100,32 @@ private static (int Bar, int Tick) GeneratorT(UgcGenerator gen, Rational time) public void FillUgcBeats_MatchesTerminalUgcBeats() { var gen = new UgcGenerator(); - FillUgcBeats(gen, LoadTerminalMetList()); + var chart = new ChuChart(); + chart.MetList.AddRange(LoadTerminalMetList()); + FillUgcBeats(gen, chart); AssertBeatEntriesEqual(LoadTerminalUgcBeats(), GetGeneratorUgcBeats(gen)); } + [Fact] + public void UgcGenerator_IgnoresZeroNumeratorMeterAndKeepsMappingLaterNotes() + { + var chart = new ChuChart(); + chart.MetList.AddRange([ + new MET(Rational.Zero, 4, 4), + new MET(1, 1, 4), + new MET(3, 0, 4), + ]); + chart.BpmList.Add(new BPM(Rational.Zero, 120)); + chart.Notes.Add(new ChuNote { Type = ChuNoteType.Tap, Time = 3, Cell = 0, Width = 1 }); + + var (ugc, alerts) = new UgcGenerator().Generate(chart); + + Assert.Single(alerts); + Assert.Contains("@BEAT\t1\t1\t4", ugc); + Assert.DoesNotContain("@BEAT\t9\t0\t4", ugc); + Assert.Contains("#9'0:t01", ugc); + } + public static IEnumerable ParserTCases => [ [0, 0, Rational.Zero],