Skip to content
Open
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
35 changes: 28 additions & 7 deletions generator/mai/MA2Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public MA2Generator(bool isUtage = false)
protected MaiChart chart;
protected List<MA2Line> lines = [];
protected readonly List<Alert> alerts = [];
private HashSet<(int, int)> _slideEnds = []; // 登记一下每个Slide的最终结束时间。用于AddSlide函数中“防止多段星星的CN连接的歧义”的逻辑。

private string headTemplate = @"VERSION 0.00.00 {0}
FES_MODE {1}
Expand All @@ -51,6 +52,7 @@ GENERATED_BY MuConvert v{8}
* 把Rational的时间近似到RESOLUTION允许的最接近tick上
*/
private (int, int) BT(Rational r, int offset = 0) => Utils.BarAndTick(r, RSL, offset);
private (int, int) BT(int totalTick) => (totalTick / RSL, totalTick % RSL);
Comment thread
Starrah marked this conversation as resolved.

// 持续时间/等待时间,使用"总tick数"(可超过1小节),不是小节内tick
protected int T(Rational r, int offset = 0) => Utils.Tick(r, RSL, offset, r > 0 ? 1 : 0);
Expand Down Expand Up @@ -96,6 +98,7 @@ protected bool hasSameTimeTap(MA2Line ma2Line)
protected virtual List<MA2Line> AddSlide(Slide slide, int bar, int tick)
{
List<MA2Line> result = [];
List<(string name, int totalTick, SlideSegment seg, int waitTime, int len)> resSegs = [];
if (slide.OwnHead != null)
{
var headTap = AddTap(slide.OwnHead, bar, tick);
Expand Down Expand Up @@ -134,6 +137,7 @@ protected virtual List<MA2Line> AddSlide(Slide slide, int bar, int tick)
var toAssignValue = unassignedValue / unassignedCount; // 未分配的时间分配给所有未分配段,每段分配到的量
# endregion

int totalTick = bar * RSL + tick;
int segIdx;
for (segIdx = 0; segIdx < slide.segments.Count; segIdx++)
{
Expand All @@ -150,14 +154,31 @@ protected virtual List<MA2Line> AddSlide(Slide slide, int bar, int tick)
if (slide.IsBreak) prefix = "BR";
waitTime = T(slide.WaitTime.Bar, -slide.FalseEachIdx);
}
else prefix = "CN";
else
{
prefix = "CN";
// 对于CN段,需要主动检查并规避“它恰好和某个在先的Slide尾部完全重合”的情况,
// 否则游戏逻辑会把这个CN段接在这个“在先Slide”的后面,造成星星连接错误。
// 测试样例详见 tests/mai/testset/片段/多段星星CN连接的歧义.yaml,以及PR #8中的分析。
while (_slideEnds.Contains((totalTick, seg.StartKey)))

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 · 后续 Slide 的尾部未被检查

当一个较晚开始的 Slide 在时间和按键上与较早 Slide 的 CN 段重合时,AddSlide 会在该较晚开始的 Slide 注册其尾部之前检查 _slideEnds,因此 CN 段仍然处于有歧义的时间点,HDD 可能会将其连接到错误的星星上。

请在检查 CN 冲突之前注册所有 Slide 的尾部,以便每次 AddSlide 检查时都包含后续处理的 Slide 的尾部。

另见 generator/mai/MA2Generator.cs:164-176。

面向 AI Agent 的提示
在 `generator/mai/MA2Generator.cs` 第 163 行:

**后续 Slide 的尾部未被检查**

当一个较晚开始的 Slide 在时间和按键上与较早 Slide 的 CN 段重合时,`AddSlide` 会在该较晚开始的 Slide 注册其尾部之前检查 `_slideEnds`,因此 CN 段仍然处于有歧义的时间点,HDD 可能会将其连接到错误的星星上。

请在检查 CN 冲突之前注册所有 Slide 的尾部,以便每次 `AddSlide` 检查时都包含后续处理的 Slide 的尾部。

另见 `generator/mai/MA2Generator.cs:164-176`。
Original comment in English

🟠 High · Later slide tails are missed

When a later-starting slide ends at the time and key of an earlier slide’s CN segment, AddSlide checks _slideEnds before the later-starting slide has registered its tail, so the CN segment remains at the ambiguous time and HDD can attach it to the wrong star.

Register all slide tails before checking CN collisions, so each AddSlide check includes tails from slides processed later.

Also at generator/mai/MA2Generator.cs:164-176.

Prompt for AI agents
In `generator/mai/MA2Generator.cs` at line 163:

**Later slide tails are missed**

When a later-starting slide ends at the time and key of an earlier slide’s CN segment, `AddSlide` checks `_slideEnds` before the later-starting slide has registered its tail, so the CN segment remains at the ambiguous time and HDD can attach it to the wrong star.

Register all slide tails before checking CN collisions, so each `AddSlide` check includes tails from slides processed later.

Also at `generator/mai/MA2Generator.cs:164-176`.

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: 这里只检查已处理 Slide 的终点;同起点星星按稳定输入顺序遍历,若尾部重合的 Slide 排在当前 Slide 后面,其终点尚未登记,CN 仍会与它重合。请让冲突检测覆盖尚未处理的 Slide 终点,避免依赖遍历顺序。

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At generator/mai/MA2Generator.cs, line 163:

<comment>这里只检查已处理 Slide 的终点;同起点星星按稳定输入顺序遍历,若尾部重合的 Slide 排在当前 Slide 后面,其终点尚未登记,CN 仍会与它重合。请让冲突检测覆盖尚未处理的 Slide 终点,避免依赖遍历顺序。</comment>

<file context>
@@ -150,14 +154,31 @@ protected virtual List<MA2Line> AddSlide(Slide slide, int bar, int tick)
+                // 对于CN段,需要主动检查并规避“它恰好和某个在先的Slide尾部完全重合”的情况,
+                // 否则游戏逻辑会把这个CN段接在这个“在先Slide”的后面,造成星星连接错误。
+                // 测试样例详见 tests/mai/testset/片段/多段星星CN连接的歧义.yaml,以及PR #8中的分析。
+                while (_slideEnds.Contains((totalTick, seg.StartKey)))
+                { // 说明命中了上述情况。此时,应该调整当前段和上一段的时间分配来规避掉问题。
+                    resSegs[^1] = resSegs[^1] with { len = resSegs[^1].len + 1 };
</file context>

{ // 说明命中了上述情况。此时,应该调整当前段和上一段的时间分配来规避掉问题。
resSegs[^1] = resSegs[^1] with { len = resSegs[^1].len + 1 };
totalTick++; // 以上两行实现的是,把当前段的时刻往后挪1tick。
if (len > 1) len--; // 那么长度自然就要补偿这1tick。要么在当前段的len里补偿、从而后面段的开始时刻不受影响;
else if (totalLen > 1) totalLen--; // 要么就在“剩余长度”中补偿,从而下一段也还是晚开始1tick。
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
}
Comment thread
Starrah marked this conversation as resolved.
}

var name = seg.Type.ToString();

result.Add(new MA2Line(prefix + name, bar, tick, seg.StartKey - 1,
string.Join("\t", [waitTime, len, seg.EndKey - 1])));
tick += waitTime + len;
while (tick >= RSL) { tick -= RSL; bar++; }
resSegs.Add((prefix + seg.Type, totalTick, seg, waitTime, len));
totalTick += waitTime + len;
}

_slideEnds.Add((totalTick, slide.EndKey));
foreach (var r in resSegs)
{
var (b, t) = BT(r.totalTick);
result.Add(new MA2Line(r.name, b, t, r.seg.StartKey - 1,
string.Join("\t", [r.waitTime, r.len, r.seg.EndKey - 1])));
}

if (slide.IsEx) Warn(Locale.ExSlideIn105, slide);
Expand Down
67 changes: 1 addition & 66 deletions tests/mai/MA2_103测试.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,71 +47,6 @@ public void Simai转MA2_103(TestInput input)
var (ma2, genAlerts) = new MA2_103Generator(isUtage: false).Generate(chart);
Assert.DoesNotContain(genAlerts, a => a.Level >= Alert.LEVEL.Error);

ma2 = KeepNotesOnly(ma2);
expectedMa2 = KeepNotesOnly(expectedMa2);
AssertMa2NotesEqual(expectedMa2, ma2, input.ToString());
}

private static (int TimeTick, int Len, string Extra) GetSlideTime(string slide)
{
var values = slide.Split('\t');
return (int.Parse(values[1]) * 384 + int.Parse(values[2]),
int.Parse(values[5]),
string.Join("\t", values[0], values[3], values[4], values[6]));
}

private static bool CompareLine(string exp, string act)
{
var result = string.Equals(exp, act, StringComparison.Ordinal);
if (!result && exp.Length >= 5 && act.Length >= 5 && exp[..5] == act[..5] && SlideTypeTool.IsSlide(exp[2..5]))
{
var (expTime, expLen, expExtra) = GetSlideTime(exp);
var (actTime, actLen, actExtra) = GetSlideTime(act);
if (expExtra != actExtra) return result;
if (exp[..2] == "CN")
{
if (expTime + expLen == actTime + actLen || Math.Abs(expLen - actLen) <= 1) result = true;
}
else
{
if (expTime == actTime && Math.Abs(expLen - actLen) <= 1) result = true;
}
}

return result;
}

private static void AssertMa2NotesEqual(string expected, string actual, string context)
{
var expectedLines = expected.Split('\n');
var actualLines = actual.Split('\n');
var max = Math.Max(expectedLines.Length, actualLines.Length);

for (var i = 0; i < max; i++)
{
var exp = i < expectedLines.Length ? expectedLines[i] : "<EOF>";
var act = i < actualLines.Length ? actualLines[i] : "<EOF>";
var result = CompareLine(exp, act);
if (!result)
{
for (var j = 1; j < Math.Min(expectedLines.Length, i + 5); j++)
{
if (CompareLine(expectedLines[j], act))
{
(expectedLines[j], expectedLines[i]) = (expectedLines[i], expectedLines[j]);
result = true;
break;
}
}
}

if (!result)
{
Assert.Fail(
$"{context}: first difference at line {i + 1}:{Environment.NewLine}" +
$"EXPECTED: {exp}{Environment.NewLine}" +
$"ACTUAL : {act}");
}
}
AssertMa2NotesEqual(KeepNotesOnly(expectedMa2), KeepNotesOnly(ma2), input.ToString());
}
}
Loading
Loading