From 8476cd84c68f55405a0f915807782e30133f86f6 Mon Sep 17 00:00:00 2001 From: dudehacker Date: Tue, 6 Oct 2026 22:38:33 -0400 Subject: [PATCH 1/3] Handle zero C2S meter in UGC export --- generator/chu/UgcGenerator.cs | 31 ++++++++++++++++++++----------- tests/chu/UgcTimeTests.cs | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/generator/chu/UgcGenerator.cs b/generator/chu/UgcGenerator.cs index 0a7bcc7..2274f3d 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,19 @@ void Dfs(ChuNote n) } // 为了实现从上述 T函数 中的换算,所必要的信息。可通过CalcUgcBeats函数算出。 private List<(int, int, int)> _ugcBeats = []; - + private void FillUgcBeats(List metList) { _ugcBeats = []; foreach (var met in metList) { + if (met.Numerator <= 0 || met.Denominator <= 0) + { + alerts.Add(new Alert(Alert.LEVEL.Warning, + $"UGC Generator忽略无效拍号: {met.Numerator}/{met.Denominator}", met.Time)); + continue; + } + if (_ugcBeats.Count == 0) { if (met.Time > 0) _ugcBeats.Add((0, 4, 4)); // 鲁棒性,补 @BEAT 0 4 4。不能continue,因为马上还要添加显式的那一条。 @@ -112,13 +119,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); - + var sb = new StringBuilder(); sb.AppendLine($"' Created with MuConvert v{Utils.AppVersion}"); sb.AppendLine("@VER\t8"); @@ -161,15 +170,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 +241,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 +250,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..6289992 100644 --- a/tests/chu/UgcTimeTests.cs +++ b/tests/chu/UgcTimeTests.cs @@ -104,6 +104,26 @@ public void FillUgcBeats_MatchesTerminalUgcBeats() 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], From 457d827ede9588b4894705dafe1a2635ef980f21 Mon Sep 17 00:00:00 2001 From: Starrah Date: Thu, 8 Oct 2026 17:37:04 +0800 Subject: [PATCH 2/3] fix: force met.Numerator to be equals with met.Denominator when met.Numerator <= 0 --- generator/chu/UgcGenerator.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/generator/chu/UgcGenerator.cs b/generator/chu/UgcGenerator.cs index 2274f3d..c39b6e6 100644 --- a/generator/chu/UgcGenerator.cs +++ b/generator/chu/UgcGenerator.cs @@ -87,16 +87,21 @@ 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) { - if (met.Numerator <= 0 || met.Denominator <= 0) + var met = origMet; + if (met.Numerator <= 0) { - alerts.Add(new Alert(Alert.LEVEL.Warning, - $"UGC Generator忽略无效拍号: {met.Numerator}/{met.Denominator}", met.Time)); - continue; + // 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) @@ -126,7 +131,7 @@ private void FillUgcBeats(List metList) private string Serialize(ChuChart ugc) { ugc.Sort(); - FillUgcBeats(ugc.MetList); + FillUgcBeats(ugc); var sb = new StringBuilder(); sb.AppendLine($"' Created with MuConvert v{Utils.AppVersion}"); From b12f65959446dabdb688432563d6bd7bdf8617ac Mon Sep 17 00:00:00 2001 From: Starrah Date: Thu, 8 Oct 2026 17:59:19 +0800 Subject: [PATCH 3/3] test: fix test --- tests/chu/UgcTimeTests.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/chu/UgcTimeTests.cs b/tests/chu/UgcTimeTests.cs index 6289992..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,7 +100,9 @@ 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)); }