Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MuConvert.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
</ItemGroup>

<ItemGroup>
<Antlr4 Include="parser\\simai\\Simai.g4">
<Antlr4 Include="parser\\mai\\Simai.g4">
<Generator>MSBuild:Compile</Generator>
<Listener>false</Listener>
<Visitor>true</Visitor>
Expand Down
4 changes: 1 addition & 3 deletions Program.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
using System.CommandLine;
using System.Text;
using System.Text.RegularExpressions;
using MuConvert.generator;
using MuConvert.maidata;
using MuConvert.parser;
using MuConvert.mai;
using MuConvert.utils;

namespace MuConvert;
Expand Down
36 changes: 36 additions & 0 deletions chart/BaseChart.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace MuConvert.chart;

public interface IBaseChart;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

In C#, interfaces must have a body enclosed in braces { }, even if they are empty. The semicolon-only syntax ; is not valid for interface declarations and will cause a compilation error.

public interface IBaseChart { }


/**
* 所有的谱面均应该继承自此类。
* 此类中提供了Notes列表,作为存储音符的核心列表;存储的音符类型是特定于谱面的(继承时传入泛型)
* 此外,应当重写以下四个抽象的getter。
*/
public abstract class BaseChart<TNote> : IBaseChart
{
/**
* 所有音符构成的列表
*/
public List<TNote> Notes = [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Exposing a List<T> as a public field is generally discouraged in C# as it breaks encapsulation. It is better to use a property with a private setter or an auto-property.

    public List<TNote> Notes { get; set; } = [];


/**
* 谱面开头的BPM
*/
public abstract decimal StartBpm { get; }

/**
* 获得谱面开始的时刻(即谱面中第一个音符的开始时刻)。单位为秒
*/
public abstract decimal StartTime { get; }

/**
* 获得谱面结束的时刻(即谱面中最后一个音符的完成时刻)。单位为秒
*/
public abstract decimal EndTime { get; }

/**
* 总音符数量(物量)
*/
public abstract int TotalNotes { get; }
}
3 changes: 2 additions & 1 deletion chart/Duration.cs → chart/mai/Duration.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using System.Diagnostics;
using MuConvert.chart;
using Rationals;

namespace MuConvert.chart;
namespace MuConvert.mai;

/**
* 用于表示持续时间的类,使用于hold和slide的持续时长以及slide的等待时长中。
Expand Down
16 changes: 8 additions & 8 deletions chart/Chart.cs → chart/mai/MaiChart.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using MuConvert.chart;
using MuConvert.utils;
using Rationals;

namespace MuConvert.chart;
namespace MuConvert.mai;

public class Chart
public class MaiChart: BaseChart<Note>
{
public BPMList BpmList = [];
public List<Note> Notes = [];

public string DefaultTouchSize = "M1";

Expand All @@ -33,13 +33,16 @@ public void Sort()
}

// 谱面开头的BPM
public decimal StartBpm {
public override decimal StartBpm {
get
{
Utils.Assert(BpmList[0].Time == 0, "BPM列表的开头必须为0时刻");
return BpmList[0].Bpm;
}
}
public override decimal StartTime => (decimal)FirstNoteTime.Seconds;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: StartTime assumes at least one note; empty charts will throw when accessing Notes[0] through FirstNoteTime. Add an empty-check fallback.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At chart/mai/MaiChart.cs, line 43:

<comment>`StartTime` assumes at least one note; empty charts will throw when accessing `Notes[0]` through `FirstNoteTime`. Add an empty-check fallback.</comment>

<file context>
@@ -33,13 +33,16 @@ public void Sort()
             return BpmList[0].Bpm;
         }
     }
+    public override decimal StartTime => (decimal)FirstNoteTime.Seconds;
+    public override decimal EndTime => (decimal)ToSecond(Notes.Select(x=>x.EndTime).Max());
+    public override int TotalNotes => Statistics.Total;
</file context>
Suggested change
public override decimal StartTime => (decimal)FirstNoteTime.Seconds;
public override decimal StartTime => Notes.Count == 0 ? 0m : (decimal)FirstNoteTime.Seconds;

public override decimal EndTime => (decimal)ToSecond(Notes.Select(x=>x.EndTime).Max());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This property has two issues:

  1. Correctness: If the Notes list is empty, Max() will throw an InvalidOperationException. You should handle the empty case (e.g., using DefaultIfEmpty).
  2. Efficiency: This is an $O(N)$ operation. If EndTime is accessed frequently, it could become a performance bottleneck. Consider caching this value or calculating it only when the chart is modified.
    public override decimal EndTime => Notes.Count == 0 ? 0 : (decimal)ToSecond(Notes.Max(x => x.EndTime));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: EndTime calls Max() on Notes without guarding empty charts, which will throw at runtime.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At chart/mai/MaiChart.cs, line 44:

<comment>`EndTime` calls `Max()` on `Notes` without guarding empty charts, which will throw at runtime.</comment>

<file context>
@@ -33,13 +33,16 @@ public void Sort()
         }
     }
+    public override decimal StartTime => (decimal)FirstNoteTime.Seconds;
+    public override decimal EndTime => (decimal)ToSecond(Notes.Select(x=>x.EndTime).Max());
+    public override int TotalNotes => Statistics.Total;
 
</file context>
Suggested change
public override decimal EndTime => (decimal)ToSecond(Notes.Select(x=>x.EndTime).Max());
public override decimal EndTime => Notes.Count == 0 ? 0m : (decimal)ToSecond(Notes.Select(x => x.EndTime).Max());

public override int TotalNotes => Statistics.Total;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The TotalNotes property is extremely inefficient. It calls the Statistics property, which creates a new Statistics instance and iterates through all notes in the chart every time it is accessed. This makes TotalNotes an $O(N)$ operation. It should be cached or calculated once.

    public override int TotalNotes => Notes.Count;


/**
* 获得“谱面中第一个音符的时刻”,或者返回的Duration也可以理解成“从谱面开头到出现第一个音符所经过的时长”。
Expand Down Expand Up @@ -81,14 +84,11 @@ public void Shift(Rational offset, decimal? bpm = null)

public Statistics Statistics => new(this);

// 总音符数量(物量)
public int TotalNotes => Statistics.Total;

/**
* 这是MA2语句中,通过CLK指令所显式指定的哒哒哒哒的时刻。
* 一般来说极少会用到,这里只是忠实地记录一下;一方面符合我们“0信息损失”的原则、忠实地记录铺面中的信息;
* 另一方面,可以用作ClockCount自动推导的来源之一。
* 普通用户理论上极少会用到这个东西。
*/
public List<Rational>? ExplicitClocks = null;
public List<Rational>? ExplicitClocks;
}
18 changes: 10 additions & 8 deletions chart/Note.cs → chart/mai/Note.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
using MuConvert.utils;
using Rationals;

namespace MuConvert.chart;
namespace MuConvert.mai;

public abstract class Note
{
public readonly Chart Chart;
public readonly MaiChart Chart;
public Rational Time { get; set => field = value.CanonicalForm; }
protected int _key;

Expand All @@ -33,7 +33,7 @@ public virtual int Key
}
}

protected Note(Chart chart, Rational time)
protected Note(MaiChart chart, Rational time)
{
Chart = chart;
Time = time;
Expand Down Expand Up @@ -64,10 +64,12 @@ protected Note(Chart chart, Rational time)
}

internal virtual string DebuggerDisplay() => "";

public virtual Rational EndTime => Time + Duration.Bar;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This will throw a NullReferenceException for note types that do not initialize the Duration property (such as Tap or Touch). Since Duration is a class and is only initialized in the Hold and TouchHold constructors, you should add a null check.

    public virtual Rational EndTime => Time + (Duration?.Bar ?? 0);

}

[DebuggerDisplay("{DebuggerDisplay(),nq}")]
public class Tap(Chart chart, Rational time) : Note(chart, time)
public class Tap(MaiChart chart, Rational time) : Note(chart, time)
{
public Tap(Tap inTake): this(inTake.Chart, inTake.Time) // 拷贝构造函数
{
Expand All @@ -85,13 +87,13 @@ public class Hold : Tap
{
public override Duration Duration { get; set; }

public Hold(Chart chart, Rational time) : base(chart, time) { Duration = new Duration(this); }
public Hold(MaiChart chart, Rational time) : base(chart, time) { Duration = new Duration(this); }

internal override string DebuggerDisplay() => $"{Key}h{Modifiers}{Duration.DebuggerDisplay()}";
}

[DebuggerDisplay("{DebuggerDisplay(),nq}")]
public class Touch(Chart chart, Rational time) : Note(chart, time)
public class Touch(MaiChart chart, Rational time) : Note(chart, time)
{
private TouchSeries _touchSeries;

Expand Down Expand Up @@ -130,10 +132,10 @@ public class TouchHold : Touch
{
public override Duration Duration { get; set; }

public TouchHold(Chart chart, Rational time) : base(chart, time) { Duration = new Duration(this); }
public TouchHold(MaiChart chart, Rational time) : base(chart, time) { Duration = new Duration(this); }

internal override string DebuggerDisplay() => $"{TouchArea}h{Modifiers}{Duration.DebuggerDisplay()}";
}

// 仅用于内部实现某些trick时使用的“伪音符”。用户在正常的谱面中是不会看到这个的。
internal class PseudoNote(Chart chart) : Note(chart, 0);
internal class PseudoNote(MaiChart chart) : Note(chart, 0);
8 changes: 5 additions & 3 deletions chart/Slide.cs → chart/mai/Slide.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
using MuConvert.utils;
using Rationals;

namespace MuConvert.chart;
namespace MuConvert.mai;

public class Star : Tap
{
public Star(Chart chart, Rational time): base(chart, time) {}
public Star(MaiChart chart, Rational time): base(chart, time) {}

public Star(Tap inTake): base(inTake) {} // 拷贝构造函数
}
Expand All @@ -24,7 +24,7 @@ public class Slide : Note
public List<SlideSegment> segments = new();
public Duration WaitTime;

public Slide(Chart chart, Rational time) : base(chart, time)
public Slide(MaiChart chart, Rational time) : base(chart, time)
{
WaitTime = new Duration(this) { InvariantBar = new Rational(1, 4) };
}
Expand Down Expand Up @@ -93,6 +93,8 @@ internal override string DebuggerDisplay()
result += Modifiers;
return result;
}

public override Rational EndTime => base.EndTime + WaitTime.Bar;
}

public class SlideSegment(Slide slide)
Expand Down
4 changes: 2 additions & 2 deletions chart/Statistics.cs → chart/mai/Statistics.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using MuConvert.utils;
using Rationals;

namespace MuConvert.chart;
namespace MuConvert.mai;

public class Statistics
{
Expand Down Expand Up @@ -57,7 +57,7 @@ private void AddNote(Note note)
}
}

public Statistics(Chart chart)
public Statistics(MaiChart chart)
{
foreach (var note in chart.Notes) AddNote(note);
}
Expand Down
2 changes: 1 addition & 1 deletion maidata/Maidata.cs → collection/Maidata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using System.Text;
using MuConvert.utils;

namespace MuConvert.maidata;
namespace MuConvert.mai;

public record MaidataChart(string Inote, string? Level = null, string? NoteDesigner = null);

Expand Down
4 changes: 2 additions & 2 deletions generator/IGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace MuConvert.generator;

public interface IGenerator
public interface IGenerator<TChart> where TChart : IBaseChart
{
public (string, List<Alert>) Generate(Chart chart);
public (string, List<Alert>) Generate(TChart chart);
}
10 changes: 5 additions & 5 deletions generator/MA2Generator.cs → generator/mai/MA2Generator.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using System.Text;
using MuConvert.chart;
using MuConvert.generator;
using MuConvert.utils;
using Rationals;
using static MuConvert.utils.Alert.LEVEL;

namespace MuConvert.generator;
namespace MuConvert.mai;

public class MA2Generator : IGenerator
public class MA2Generator : IGenerator<MaiChart>
{
protected record MA2Line(string Name, int Bar, int Tick, int Key, string Extra = "")
{
Expand All @@ -29,7 +29,7 @@ public MA2Generator(bool isUtage = false)
public int MA2Version = 105;
public int RSL = 384;

protected Chart chart;
protected MaiChart chart;
protected List<MA2Line> lines = [];
protected readonly List<Alert> alerts = [];

Expand Down Expand Up @@ -315,7 +315,7 @@ protected void GenerateStatistics(StringBuilder result, Statistics stats)
result.AppendLine();
}

public (string, List<Alert>) Generate(Chart _chart)
public (string, List<Alert>) Generate(MaiChart _chart)
{
if (chart != null) throw new Exception(Locale.InstanceMultipleUsage);
chart = _chart;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using MuConvert.chart;
using MuConvert.utils;
using MuConvert.utils;

namespace MuConvert.generator;
namespace MuConvert.mai;

public class MA2_103Generator : MA2Generator
{
Expand Down
10 changes: 5 additions & 5 deletions generator/SimaiGenerator.cs → generator/mai/SimaiGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using System.Numerics;
using MuConvert.chart;
using MuConvert.generator;
using MuConvert.utils;
using Rationals;
using static MuConvert.utils.Alert.LEVEL;

namespace MuConvert.generator;
namespace MuConvert.mai;

class SimaiNote
{
Expand All @@ -22,7 +22,7 @@ public SimaiNote(Rational time, string note, int falseEachIndex, bool isBpm = fa
}
};

public class SimaiGenerator : IGenerator
public class SimaiGenerator : IGenerator<MaiChart>
{
/**
* 这是一个Workaround的选项。
Expand All @@ -36,7 +36,7 @@ public class SimaiGenerator : IGenerator
private readonly List<Alert> alerts = [];
private string result = ""; // 不用StringBuilder是因为生成过程不可避免地需要对字符串做一些回溯的操作,需要倒着从字符串中查找字符。这样的场景下,StringBuilder并无性能优势,用string就够了。
#pragma warning disable CS8618
private Chart chart;
private MaiChart chart;
#pragma warning restore CS8618

private int bpmIdx = 0; // 当前遍历到了哪个bpm
Expand Down Expand Up @@ -129,7 +129,7 @@ private string DurationStr(Rational start, Duration duration, bool forceAbsTime

private string DurationStr(Note note) => DurationStr(note.Time, note.Duration);

public (string, List<Alert>) Generate(Chart _chart)
public (string, List<Alert>) Generate(MaiChart _chart)
{
if (chart != null) throw new Exception(Locale.InstanceMultipleUsage);
chart = _chart;
Expand Down
20 changes: 19 additions & 1 deletion i18n/Locale.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion i18n/Locale.ja.resx
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,15 @@
<data name="MessageLine" xml:space="preserve">
<value>{0} 行目</value>
</data>
<data name="MessageTime" xml:space="preserve">
<data name="MessageTimeAndBar" xml:space="preserve">
<value>{0:W} 小節({1:F2} 秒)</value>
</data>
<data name="MessageTime" xml:space="preserve">
<value>{0:F2} 秒</value>
</data>
<data name="MessageBar" xml:space="preserve">
<value>{0:W} 小節</value>
</data>
<data name="MessageParsing" xml:space="preserve">
<value>「{0}」の解析中</value>
</data>
Expand Down
Loading