Skip to content

feat: 新增 C2S/UGC/SUS 谱面格式支持 (chu)#2

Open
Applesaber wants to merge 18 commits intoMuNET-OSS:masterfrom
Applesaber:master
Open

feat: 新增 C2S/UGC/SUS 谱面格式支持 (chu)#2
Applesaber wants to merge 18 commits intoMuNET-OSS:masterfrom
Applesaber:master

Conversation

@Applesaber
Copy link
Copy Markdown

@Applesaber Applesaber commented May 1, 2026

概述

新增 MuConvert.chu 命名空间,支持 C2S/UGC/SUS 三种 CHUNITHM 谱面格式的解析、生成与跨格式互转。

架构

Parser → IR → Generator 三层,IChuChart 统一接口,Generator 内嵌转换逻辑。

改动

  • chart/chu/: IChuChart + ChuNote + 三种格式 IR (5 files)
  • parser/chu/: C2sParser/UgcParser/SusParser (3 files)
  • generator/chu/: C2sGenerator/UgcGenerator/SusGenerator (3 files)
  • tests/chu/: ChuTests + testset 官谱/自制谱 (2 files + samples)
  • i18n/: C2SUnknownNoteType + ChuGeneratorUnsupported, 5 语言覆盖 (6 files)

测试

219/219 全过 (新增 5 项)

Summary by Sourcery

添加对 CHUNITHM 谱面的全面支持,包括共享的中间表示(IR)、解析器、生成器、国际化(i18n)以及测试。

New Features:

  • 引入统一的 CHUNITHM 谱面接口和在多种格式间共享的音符模型。
  • 为 C2S、UGC 和 SUS CHUNITHM 谱面格式添加 IR 模型。
  • 实现针对 C2S、UGC 和 SUS 谱面格式的解析器,将谱面加载到共享 IR 中。
  • 实现针对 C2S、UGC 和 SUS 谱面格式的生成器,包括在这些格式之间进行交叉转换。

Enhancements:

  • 扩展 i18n 资源,加入针对未知 C2S 音符类型和不受支持的 CHUNITHM 生成目标的消息。
  • 为 CHUNITHM 谱面解析与生成添加基于往返测试和样本的测试,覆盖官方谱面和自定义谱面。

Tests:

  • 添加单元测试,覆盖 C2S 与 UGC 谱面的解析、往返测试以及跨格式生成,并包含 golden 样例资源。
Original summary in English

Summary by Sourcery

Add comprehensive CHUNITHM chart support with shared IR, parsers, generators, i18n, and tests.

New Features:

  • Introduce a unified CHUNITHM chart interface and note model shared across formats.
  • Add IR models for C2S, UGC, and SUS CHUNITHM chart formats.
  • Implement parsers for C2S, UGC, and SUS chart formats to load charts into the shared IR.
  • Implement generators for C2S, UGC, and SUS chart formats, including cross-format conversion between them.

Enhancements:

  • Extend i18n resources with messages for unknown C2S note types and unsupported CHUNITHM generator targets.
  • Add round-trip and sample-based tests for CHUNITHM chart parsing and generation, including on official and custom charts.

Tests:

  • Add unit tests covering parsing, round-tripping, and cross-format generation for C2S and UGC charts, plus golden sample assets.

Parser-Converter-Generator 三层架构,统一 IChuChart 接口,Generator 内嵌转换逻辑。
5 种语言 i18n 覆盖,4 项 xUnit 测试,219/219 全过。
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 1, 2026

审阅者指南

新增统一的 CHUNITHM 谱面抽象层(IChuChart + ChuNote + 三种 IR),并为 C2S、UGC 和 SUS 格式实现完整的解析/生成与交叉转换支持,包括本地化警告和针对官方及自定义谱面的回归测试。

CHUNITHM 跨格式转换顺序图(UGC 文本到 C2S 文本)

sequenceDiagram
    participant User
    participant UgcTextSource
    participant UgcParser
    participant UgcChart as UgcChart_IChuChart_
    participant C2sGenerator
    participant C2sTextSink

    User->>UgcTextSource: Load UGC text
    UgcTextSource-->>User: string ugcText

    User->>UgcParser: Parse(ugcText)
    UgcParser->>UgcParser: ParseHeaderLine / ParseNoteLine
    UgcParser-->>User: UgcChart_IChuChart_, List~Alert~ alertsParse

    User->>C2sGenerator: Generate(UgcChart_IChuChart_)
    C2sGenerator->>C2sGenerator: ConvertToC2s(IChuChart, alertsGen)
    alt input is UgcChart
        C2sGenerator->>C2sGenerator: ScaleDown timing and notes
        C2sGenerator->>C2sGenerator: Build C2sChart
    else unsupported chart type
        C2sGenerator->>C2sGenerator: Create Alert Warning using Locale.ChuGeneratorUnsupported
    end
    C2sGenerator->>C2sGenerator: Serialize(C2sChart)
    C2sGenerator-->>User: string c2sText, List~Alert~ alertsGen

    User->>C2sTextSink: Save c2sText
    User->>User: Display or log alertsParse + alertsGen
Loading

统一 CHUNITHM 谱面模型类图(IChuChart、ChuNote、IR)

classDiagram
    direction LR

    class IBaseChart {
    <<interface>>
    }

    class BaseChart_ChuNote_ {
    <<abstract>>
    +List~ChuNote~ Notes
    +decimal StartBpm
    +decimal StartTime
    +decimal EndTime
    +int TotalNotes
    }

    IBaseChart <|.. IChuChart

    class IChuChart {
    <<interface>>
    }

    BaseChart_ChuNote_ <|-- C2sChart
    BaseChart_ChuNote_ <|-- UgcChart
    BaseChart_ChuNote_ <|-- SusChart

    class ChuNote {
      +string Type
      +int Measure
      +int Offset
      +int Cell
      +int Width
      +int HoldDuration
      +int SlideDuration
      +int EndCell
      +int EndWidth
      +string Extra
      +string TargetNote
      +int AirHoldDuration
      +int StartHeight
      +int TargetHeight
      +string NoteColor
    }

    class C2sChart {
      +string Version
      +int MusicId
      +int DifficultId
      +string Creator
      +int Resolution
      +double DefBpm
      +List~(int Measure, int Offset, double Bpm)~ BpmEvents
      +List~(int Measure, int Offset, int Denom, int Num)~ MetEvents
      +List~(int Measure, int Offset, int Duration, double Multiplier)~ SflEvents
      +decimal StartBpm
      +decimal StartTime
      +decimal EndTime
      +int TotalNotes
    }

    class UgcChart {
      +string Version
      +string Title
      +string Artist
      +string Designer
      +string Difficulty
      +int Level
      +double Constant
      +string SongId
      +int TicksPerBeat
      +List~(int Measure, int Num, int Den)~ BeatEvents
      +List~(int Measure, int Offset, double Bpm)~ BpmEvents
      +List~(int Measure, int Offset, double Multiplier)~ SpeedEvents
      +decimal StartBpm
      +decimal StartTime
      +decimal EndTime
      +int TotalNotes
    }

    class SusChart {
      +string Title
      +string Artist
      +string Designer
      +int TicksPerBeat
      +double Bpm
      +decimal StartBpm
      +decimal StartTime
      +decimal EndTime
      +int TotalNotes
    }

    IChuChart <|.. C2sChart
    IChuChart <|.. UgcChart
    IChuChart <|.. SusChart

    C2sChart o-- ChuNote
    UgcChart o-- ChuNote
    SusChart o-- ChuNote
Loading

CHUNITHM 解析器、生成器、警告与多语言支持(i18n)类图

classDiagram
    direction LR

    class IParser_T_ {
    <<interface>>
    +Parse(string text) (T, List~Alert~)
    }

    class IGenerator_IChuChart_ {
    <<interface>>
    +Generate(IChuChart chart) (string, List~Alert~)
    }

    class Alert {
      <<record>>
      +LEVEL Level
      +string Message
      +int Line
      +string RelevantNote
    }

    class Alert_LEVEL_ {
      <<enumeration>>
      Info
      Warning
      Error
    }

    class Locale {
      +static string C2SUnknownNoteType
      +static string ChuGeneratorUnsupported
    }

    IParser_T_ <|.. C2sParser
    IParser_T_ <|.. UgcParser
    IParser_T_ <|.. SusParser

    class C2sParser {
      -static HashSet~string~ HeadTags
      -static HashSet~string~ TimingTags
      +Parse(string text) (C2sChart, List~Alert~)
      -ParseHeader(string[] p, C2sChart chart) void
      -ParseTiming(string[] p, C2sChart chart) void
      -ParseNote(string[] p, C2sChart chart, List~Alert~ alerts, int lineNum) void
    }

    class UgcParser {
      -static Dictionary~string, string~ AirDirections
      -static Dictionary~string, string~ ChrExtras
      +Parse(string text) (UgcChart, List~Alert~)
      -ParseHeaderLine(string line, UgcChart chart, List~Alert~ alerts, int lineNum) void
      -ParseNoteLine(string[] lines, int idx, UgcChart chart, List~Alert~ alerts) int
    }

    class SusParser {
      -static Dictionary~int, string~ TypeMap
      +Parse(string text) (SusChart, List~Alert~)
      -ParseHeaderLine(string content, SusChart chart, List~Alert~ alerts, int lineNum) void
      -ParseNoteLine(string content, SusChart chart, List~Alert~ alerts, int lineNum) void
    }

    IGenerator_IChuChart_ <|.. C2sGenerator
    IGenerator_IChuChart_ <|.. UgcGenerator
    IGenerator_IChuChart_ <|.. SusGenerator

    class C2sGenerator {
      -const int C2sResolution
      +Generate(IChuChart chart) (string, List~Alert~)
      -ConvertToC2s(IChuChart chart, List~Alert~ alerts) C2sChart
      -ScaleNote(ChuNote n, int tpb) ChuNote
      -Serialize(C2sChart chart) string
    }

    class UgcGenerator {
      -const int UgcTicksPerBeat
      -const int C2sResolution
      +Generate(IChuChart chart) (string, List~Alert~)
      -ConvertToUgc(IChuChart chart, List~Alert~ alerts) UgcChart
      -ScaleUpNote(ChuNote n) ChuNote
      -Serialize(UgcChart ugc) string
    }

    class SusGenerator {
      -const int SusTpb
      -const int C2sRsl
      +Generate(IChuChart chart) (string, List~Alert~)
      -ConvertToSus(IChuChart chart, List~Alert~ alerts) SusChart
      -ScaleUp(ChuNote n) ChuNote
      -Serialize(SusChart sus) string
    }

    Alert o-- Alert_LEVEL_

    C2sParser --> C2sChart
    UgcParser --> UgcChart
    SusParser --> SusChart

    C2sParser --> Alert
    UgcParser --> Alert
    SusParser --> Alert

    C2sGenerator --> IChuChart
    UgcGenerator --> IChuChart
    SusGenerator --> IChuChart

    C2sGenerator --> C2sChart
    UgcGenerator --> UgcChart
    SusGenerator --> SusChart

    C2sGenerator --> ChuNote
    UgcGenerator --> ChuNote
    SusGenerator --> ChuNote

    C2sParser ..> Locale
    C2sGenerator ..> Locale
    UgcGenerator ..> Locale
    SusGenerator ..> Locale
Loading

CHUNITHM 谱面解析、IR 与跨格式生成流程图

flowchart LR
    subgraph InputFormats[输入文本格式]
        C2S_TXT[C2S text]
        UGC_TXT[UGC text]
        SUS_TXT[SUS text]
    end

    subgraph Parsers[解析器]
        C2S_PARSER[C2sParser\nIParser<C2sChart>]
        UGC_PARSER[UgcParser\nIParser<UgcChart>]
        SUS_PARSER[SusParser\nIParser<SusChart>]
    end

    subgraph IR[统一 CHUNITHM IR]
        C2S_IR[C2sChart\nimplements IChuChart]
        UGC_IR[UgcChart\nimplements IChuChart]
        SUS_IR[SusChart\nimplements IChuChart]
        NOTES[ChuNote objects]
    end

    subgraph Generators[生成器]
        C2S_GEN[C2sGenerator\nIGenerator<IChuChart>]
        UGC_GEN[UgcGenerator\nIGenerator<IChuChart>]
        SUS_GEN[SusGenerator\nIGenerator<IChuChart>]
    end

    subgraph OutputFormats[输出文本格式]
        C2S_OUT[C2S text]
        UGC_OUT[UGC text]
        SUS_OUT[SUS text]
    end

    C2S_TXT --> C2S_PARSER
    UGC_TXT --> UGC_PARSER
    SUS_TXT --> SUS_PARSER

    C2S_PARSER --> C2S_IR
    UGC_PARSER --> UGC_IR
    SUS_PARSER --> SUS_IR

    C2S_IR --> NOTES
    UGC_IR --> NOTES
    SUS_IR --> NOTES

    C2S_IR --> C2S_GEN
    C2S_IR --> UGC_GEN
    C2S_IR --> SUS_GEN

    UGC_IR --> C2S_GEN
    UGC_IR --> UGC_GEN
    UGC_IR --> SUS_GEN

    SUS_IR --> C2S_GEN
    SUS_IR --> UGC_GEN
    SUS_IR --> SUS_GEN

    C2S_GEN --> C2S_OUT
    UGC_GEN --> UGC_OUT
    SUS_GEN --> SUS_OUT

    classDef ir fill:#eef,stroke:#446
    class C2S_IR,UGC_IR,SUS_IR,NOTES ir

    classDef parser fill:#efe,stroke:#484
    class C2S_PARSER,UGC_PARSER,SUS_PARSER parser

    classDef gen fill:#ffe,stroke:#884
    class C2S_GEN,UGC_GEN,SUS_GEN gen
Loading

文件级变更

Change Details Files
引入统一的 CHUNITHM 谱面/音符 IR,并实现 C2S/UGC/SUS 的解析器与生成器,包括跨格式转换、本地化条目,以及基于真实谱面样本的测试。
  • 定义 ChuNote 以及三种谱面 IR 类型(C2sChart、UgcChart、SusChart),并在现有 BaseChart 之上提供通用的 IChuChart 接口,包含节奏/事件字段和基础的节奏派生属性。
  • 为 C2S、UGC 和 SUS 实现格式专用解析器,将文本读取为 IR,对字段进行健壮的解析、校验和告警上报,并将各格式原生编码映射到 ChuNote 属性。
  • 实现 C2S、UGC 和 SUS 生成器,接受任意 IChuChart,通过 Tick 缩放和数值映射进行格式间转换,并序列化回各自文本格式;当源谱类型不受支持时,发出本地化告警。
  • 扩展 i18n 资源和 Locale.Designer,新增未知 C2S 音符类型和不受支持的生成器路径等字符串,为所有现有语言提供翻译。
  • 新增 ChuTests,针对内置官方/自定义谱面,覆盖 UGC 与 C2S 间的解析、往返(round-trip)与转换,并生成示例输出文件以供人工检查。
chart/chu/ChuNote.cs
chart/chu/C2sChart.cs
chart/chu/UgcChart.cs
chart/chu/SusChart.cs
chart/chu/IChuChart.cs
parser/chu/C2sParser.cs
parser/chu/UgcParser.cs
parser/chu/SusParser.cs
generator/chu/C2sGenerator.cs
generator/chu/UgcGenerator.cs
generator/chu/SusGenerator.cs
i18n/Locale.Designer.cs
i18n/Locale.resx
i18n/Locale.ja.resx
i18n/Locale.ko.resx
i18n/Locale.zh.resx
i18n/Locale.zh-hant.resx
tests/chu/ChuTests.cs
tests/chu/testset/...
tests/chu/example.cs (removed)
tests/chu/testset/placeholder.txt (removed)

提示与命令

与 Sourcery 交互

  • 触发新审查: 在 Pull Request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 在某条审查评论下回复,要求 Sourcery 基于该评论创建 issue。你也可以直接回复 @sourcery-ai issue 来从该评论生成 issue。
  • 生成 Pull Request 标题: 在 Pull Request 标题中任意位置写入 @sourcery-ai 即可随时生成标题。你也可以在 Pull Request 中评论 @sourcery-ai title 以(重新)生成标题。
  • 生成 Pull Request 摘要: 在 Pull Request 正文任意位置写入 @sourcery-ai summary,即可在该位置生成 PR 摘要。你也可以在 Pull Request 中评论 @sourcery-ai summary 以(重新)生成摘要。
  • 生成审阅者指南: 在 Pull Request 中评论 @sourcery-ai guide,即可(重新)生成审阅者指南。
  • 一次性解决所有 Sourcery 评论: 在 Pull Request 中评论 @sourcery-ai resolve,即可标记所有 Sourcery 评论为已解决。如果你已经处理了所有评论且不想再看到它们,这会很有用。
  • 撤销所有 Sourcery 审查: 在 Pull Request 中评论 @sourcery-ai dismiss,即可撤销所有现有 Sourcery 审查。特别适用于你想从头开始一次新审查——别忘了再评论一次 @sourcery-ai review 来触发新审查!

自定义你的体验

访问你的 控制面板 以:

  • 启用或禁用审查功能,例如 Sourcery 自动生成的 Pull Request 摘要、审阅者指南等。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查说明。
  • 调整其他审查相关设置。

获取帮助

Original review guide in English

Reviewer's Guide

Adds a unified CHUNITHM chart abstraction (IChuChart + ChuNote + three IRs) and implements full parse/generate + cross-conversion support for C2S, UGC, and SUS formats, including localized warnings and regression tests over official and custom charts.

Sequence diagram for cross-format CHUNITHM conversion (UGC text to C2S text)

sequenceDiagram
    participant User
    participant UgcTextSource
    participant UgcParser
    participant UgcChart as UgcChart_IChuChart_
    participant C2sGenerator
    participant C2sTextSink

    User->>UgcTextSource: Load UGC text
    UgcTextSource-->>User: string ugcText

    User->>UgcParser: Parse(ugcText)
    UgcParser->>UgcParser: ParseHeaderLine / ParseNoteLine
    UgcParser-->>User: UgcChart_IChuChart_, List~Alert~ alertsParse

    User->>C2sGenerator: Generate(UgcChart_IChuChart_)
    C2sGenerator->>C2sGenerator: ConvertToC2s(IChuChart, alertsGen)
    alt input is UgcChart
        C2sGenerator->>C2sGenerator: ScaleDown timing and notes
        C2sGenerator->>C2sGenerator: Build C2sChart
    else unsupported chart type
        C2sGenerator->>C2sGenerator: Create Alert Warning using Locale.ChuGeneratorUnsupported
    end
    C2sGenerator->>C2sGenerator: Serialize(C2sChart)
    C2sGenerator-->>User: string c2sText, List~Alert~ alertsGen

    User->>C2sTextSink: Save c2sText
    User->>User: Display or log alertsParse + alertsGen
Loading

Class diagram for unified CHUNITHM chart model (IChuChart, ChuNote, IRs)

classDiagram
    direction LR

    class IBaseChart {
    <<interface>>
    }

    class BaseChart_ChuNote_ {
    <<abstract>>
    +List~ChuNote~ Notes
    +decimal StartBpm
    +decimal StartTime
    +decimal EndTime
    +int TotalNotes
    }

    IBaseChart <|.. IChuChart

    class IChuChart {
    <<interface>>
    }

    BaseChart_ChuNote_ <|-- C2sChart
    BaseChart_ChuNote_ <|-- UgcChart
    BaseChart_ChuNote_ <|-- SusChart

    class ChuNote {
      +string Type
      +int Measure
      +int Offset
      +int Cell
      +int Width
      +int HoldDuration
      +int SlideDuration
      +int EndCell
      +int EndWidth
      +string Extra
      +string TargetNote
      +int AirHoldDuration
      +int StartHeight
      +int TargetHeight
      +string NoteColor
    }

    class C2sChart {
      +string Version
      +int MusicId
      +int DifficultId
      +string Creator
      +int Resolution
      +double DefBpm
      +List~(int Measure, int Offset, double Bpm)~ BpmEvents
      +List~(int Measure, int Offset, int Denom, int Num)~ MetEvents
      +List~(int Measure, int Offset, int Duration, double Multiplier)~ SflEvents
      +decimal StartBpm
      +decimal StartTime
      +decimal EndTime
      +int TotalNotes
    }

    class UgcChart {
      +string Version
      +string Title
      +string Artist
      +string Designer
      +string Difficulty
      +int Level
      +double Constant
      +string SongId
      +int TicksPerBeat
      +List~(int Measure, int Num, int Den)~ BeatEvents
      +List~(int Measure, int Offset, double Bpm)~ BpmEvents
      +List~(int Measure, int Offset, double Multiplier)~ SpeedEvents
      +decimal StartBpm
      +decimal StartTime
      +decimal EndTime
      +int TotalNotes
    }

    class SusChart {
      +string Title
      +string Artist
      +string Designer
      +int TicksPerBeat
      +double Bpm
      +decimal StartBpm
      +decimal StartTime
      +decimal EndTime
      +int TotalNotes
    }

    IChuChart <|.. C2sChart
    IChuChart <|.. UgcChart
    IChuChart <|.. SusChart

    C2sChart o-- ChuNote
    UgcChart o-- ChuNote
    SusChart o-- ChuNote
Loading

Class diagram for CHUNITHM parsers, generators, alerts, and i18n

classDiagram
    direction LR

    class IParser_T_ {
    <<interface>>
    +Parse(string text) (T, List~Alert~)
    }

    class IGenerator_IChuChart_ {
    <<interface>>
    +Generate(IChuChart chart) (string, List~Alert~)
    }

    class Alert {
      <<record>>
      +LEVEL Level
      +string Message
      +int Line
      +string RelevantNote
    }

    class Alert_LEVEL_ {
      <<enumeration>>
      Info
      Warning
      Error
    }

    class Locale {
      +static string C2SUnknownNoteType
      +static string ChuGeneratorUnsupported
    }

    IParser_T_ <|.. C2sParser
    IParser_T_ <|.. UgcParser
    IParser_T_ <|.. SusParser

    class C2sParser {
      -static HashSet~string~ HeadTags
      -static HashSet~string~ TimingTags
      +Parse(string text) (C2sChart, List~Alert~)
      -ParseHeader(string[] p, C2sChart chart) void
      -ParseTiming(string[] p, C2sChart chart) void
      -ParseNote(string[] p, C2sChart chart, List~Alert~ alerts, int lineNum) void
    }

    class UgcParser {
      -static Dictionary~string, string~ AirDirections
      -static Dictionary~string, string~ ChrExtras
      +Parse(string text) (UgcChart, List~Alert~)
      -ParseHeaderLine(string line, UgcChart chart, List~Alert~ alerts, int lineNum) void
      -ParseNoteLine(string[] lines, int idx, UgcChart chart, List~Alert~ alerts) int
    }

    class SusParser {
      -static Dictionary~int, string~ TypeMap
      +Parse(string text) (SusChart, List~Alert~)
      -ParseHeaderLine(string content, SusChart chart, List~Alert~ alerts, int lineNum) void
      -ParseNoteLine(string content, SusChart chart, List~Alert~ alerts, int lineNum) void
    }

    IGenerator_IChuChart_ <|.. C2sGenerator
    IGenerator_IChuChart_ <|.. UgcGenerator
    IGenerator_IChuChart_ <|.. SusGenerator

    class C2sGenerator {
      -const int C2sResolution
      +Generate(IChuChart chart) (string, List~Alert~)
      -ConvertToC2s(IChuChart chart, List~Alert~ alerts) C2sChart
      -ScaleNote(ChuNote n, int tpb) ChuNote
      -Serialize(C2sChart chart) string
    }

    class UgcGenerator {
      -const int UgcTicksPerBeat
      -const int C2sResolution
      +Generate(IChuChart chart) (string, List~Alert~)
      -ConvertToUgc(IChuChart chart, List~Alert~ alerts) UgcChart
      -ScaleUpNote(ChuNote n) ChuNote
      -Serialize(UgcChart ugc) string
    }

    class SusGenerator {
      -const int SusTpb
      -const int C2sRsl
      +Generate(IChuChart chart) (string, List~Alert~)
      -ConvertToSus(IChuChart chart, List~Alert~ alerts) SusChart
      -ScaleUp(ChuNote n) ChuNote
      -Serialize(SusChart sus) string
    }

    Alert o-- Alert_LEVEL_

    C2sParser --> C2sChart
    UgcParser --> UgcChart
    SusParser --> SusChart

    C2sParser --> Alert
    UgcParser --> Alert
    SusParser --> Alert

    C2sGenerator --> IChuChart
    UgcGenerator --> IChuChart
    SusGenerator --> IChuChart

    C2sGenerator --> C2sChart
    UgcGenerator --> UgcChart
    SusGenerator --> SusChart

    C2sGenerator --> ChuNote
    UgcGenerator --> ChuNote
    SusGenerator --> ChuNote

    C2sParser ..> Locale
    C2sGenerator ..> Locale
    UgcGenerator ..> Locale
    SusGenerator ..> Locale
Loading

Flow diagram for CHUNITHM chart parsing, IR, and cross-format generation

flowchart LR
    subgraph InputFormats[Input text formats]
        C2S_TXT[C2S text]
        UGC_TXT[UGC text]
        SUS_TXT[SUS text]
    end

    subgraph Parsers[Parsers]
        C2S_PARSER[C2sParser\nIParser<C2sChart>]
        UGC_PARSER[UgcParser\nIParser<UgcChart>]
        SUS_PARSER[SusParser\nIParser<SusChart>]
    end

    subgraph IR[Unified CHUNITHM IR]
        C2S_IR[C2sChart\nimplements IChuChart]
        UGC_IR[UgcChart\nimplements IChuChart]
        SUS_IR[SusChart\nimplements IChuChart]
        NOTES[ChuNote objects]
    end

    subgraph Generators[Generators]
        C2S_GEN[C2sGenerator\nIGenerator<IChuChart>]
        UGC_GEN[UgcGenerator\nIGenerator<IChuChart>]
        SUS_GEN[SusGenerator\nIGenerator<IChuChart>]
    end

    subgraph OutputFormats[Output text formats]
        C2S_OUT[C2S text]
        UGC_OUT[UGC text]
        SUS_OUT[SUS text]
    end

    C2S_TXT --> C2S_PARSER
    UGC_TXT --> UGC_PARSER
    SUS_TXT --> SUS_PARSER

    C2S_PARSER --> C2S_IR
    UGC_PARSER --> UGC_IR
    SUS_PARSER --> SUS_IR

    C2S_IR --> NOTES
    UGC_IR --> NOTES
    SUS_IR --> NOTES

    C2S_IR --> C2S_GEN
    C2S_IR --> UGC_GEN
    C2S_IR --> SUS_GEN

    UGC_IR --> C2S_GEN
    UGC_IR --> UGC_GEN
    UGC_IR --> SUS_GEN

    SUS_IR --> C2S_GEN
    SUS_IR --> UGC_GEN
    SUS_IR --> SUS_GEN

    C2S_GEN --> C2S_OUT
    UGC_GEN --> UGC_OUT
    SUS_GEN --> SUS_OUT

    classDef ir fill:#eef,stroke:#446
    class C2S_IR,UGC_IR,SUS_IR,NOTES ir

    classDef parser fill:#efe,stroke:#484
    class C2S_PARSER,UGC_PARSER,SUS_PARSER parser

    classDef gen fill:#ffe,stroke:#884
    class C2S_GEN,UGC_GEN,SUS_GEN gen
Loading

File-Level Changes

Change Details Files
Introduce a unified CHUNITHM chart/notes IR and implement C2S/UGC/SUS parsers and generators, including cross-format conversion, localization entries, and tests over real chart samples.
  • Define ChuNote plus three chart IR types (C2sChart, UgcChart, SusChart) and a common IChuChart interface atop existing BaseChart, including timing/event fields and basic timing-derived properties.
  • Implement format-specific parsers for C2S, UGC, and SUS that read text into the IRs, with robust field parsing, validation, alert reporting, and mapping from format-native encodings into ChuNote properties.
  • Implement generators for C2S, UGC, and SUS that accept any IChuChart, perform format-to-format conversion via tick scaling and value mapping, and serialize back to each text format; emit localized alerts when a given source chart type is unsupported.
  • Extend i18n resources and Locale.Designer with new strings for unknown C2S note types and unsupported generator paths, providing translations in all existing languages.
  • Add ChuTests that exercise parsing, round-tripping, and conversion between UGC and C2S against bundled official/custom charts, and generate sample output files for manual inspection.
chart/chu/ChuNote.cs
chart/chu/C2sChart.cs
chart/chu/UgcChart.cs
chart/chu/SusChart.cs
chart/chu/IChuChart.cs
parser/chu/C2sParser.cs
parser/chu/UgcParser.cs
parser/chu/SusParser.cs
generator/chu/C2sGenerator.cs
generator/chu/UgcGenerator.cs
generator/chu/SusGenerator.cs
i18n/Locale.Designer.cs
i18n/Locale.resx
i18n/Locale.ja.resx
i18n/Locale.ko.resx
i18n/Locale.zh.resx
i18n/Locale.zh-hant.resx
tests/chu/ChuTests.cs
tests/chu/testset/...
tests/chu/example.cs (removed)
tests/chu/testset/placeholder.txt (removed)

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 23 files

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 11 个问题,并给出了一些整体性反馈:

  • UGC/SUS 路径中的数字解析/格式化目前依赖当前区域性(例如 UgcParser.ParseHeaderLine 中的 double.TryParse,以及 {b.Bpm:F5} / {sus.Bpm:F2} 这类字符串插值),在非 en-US 区域设置下可能会出错;建议像 C2sGenerator 中那样统一使用 CultureInfo.InvariantCulture
  • UgcGenerator.ScaleUpNotes(int v) => v * UgcTicksPerBeat / (C2sResolution / 4))和 SusGenerator.ScaleUp 这样的 tick 缩放辅助方法使用 int 做算术运算,在大数值情况下可能会溢出;建议对齐 C2sGenerator.ScaleNote 的做法,在乘法前先提升为 long,这样既更安全也更一致。
给 AI Agent 的提示
Please address the comments from this code review:

## Overall Comments
- UGC/SUS 路径中的数字解析/格式化目前依赖当前区域性(例如 `UgcParser.ParseHeaderLine` 中的 `double.TryParse`,以及 `{b.Bpm:F5}` / `{sus.Bpm:F2}` 这类字符串插值),在非 `en-US` 区域设置下可能会出错;建议像 `C2sGenerator` 中那样统一使用 `CultureInfo.InvariantCulture`-`UgcGenerator.ScaleUpNote``s(int v) => v * UgcTicksPerBeat / (C2sResolution / 4)`)和 `SusGenerator.ScaleUp` 这样的 tick 缩放辅助方法使用 `int` 做算术运算,在大数值情况下可能会溢出;建议对齐 `C2sGenerator.ScaleNote` 的做法,在乘法前先提升为 `long`,这样既更安全也更一致。

## Individual Comments

### Comment 1
<location path="parser/chu/UgcParser.cs" line_range="136-78" />
<code_context>
+                    alerts.Add(new Alert(Warning, $"@TICKS 格式错误: {line}") { Line = lineNum });
+                break;
+
+            case "@BEAT":
+                var beatParts = value.Split(' ');
+                if (beatParts.Length >= 3
+                    && int.TryParse(beatParts[0], out var beatMeasure)
+                    && int.TryParse(beatParts[1], out var beatNum)
+                    && int.TryParse(beatParts[2], out var beatDen))
+                {
+                    chart.BeatEvents.Add((beatMeasure, beatNum, beatDen));
+                }
+                else
+                {
+                    alerts.Add(new Alert(Warning, $"@BEAT 格式错误: {line}") { Line = lineNum });
+                }
+                break;
+
+            case "@BPM":
</code_context>
<issue_to_address>
**issue (bug_risk):** @BEAT 解析假定使用空格分隔,但生成器使用的是制表符分隔,导致所有 BEAT 行都会被视为格式错误。

在 `ParseHeaderLine` 中,`value` 是第一个制表符之后的子串,里面可能仍然包含制表符,但这里使用空格拆分:`value.Split(' ')``UgcGenerator.Serialize` 会将 `@BEAT` 输出为 `"@BEAT\t{b.Measure}\t{b.Num}\t{b.Den}"`,因此其中没有空格,`beatParts.Length` 始终为 1,所有 `@BEAT` 行都会被当作格式错误。为了与序列化逻辑保持一致,应改为按制表符或更通用的空白字符拆分,例如 `value.Split('\t', ' ')``value.Split((char[])null, StringSplitOptions.RemoveEmptyEntries)`。
</issue_to_address>

### Comment 2
<location path="parser/chu/UgcParser.cs" line_range="151-78" />
<code_context>
+            case "@BPM":
</code_context>
<issue_to_address>
**issue (bug_risk):** @BPM 的解析方式与实际使用制表符而非空格的序列化格式不兼容。

对于 `@BPM``value` 是第一个制表符之后的子串,因此对于类似 `@BPM	0'0	120.00000` 的生成行来说,`value``"0'0	120.00000"`。解析器随后调用 `IndexOf(' ')`,但因为分隔符是制表符,该调用返回 `-1`,导致该行被视为格式错误。为了与 `UgcGenerator` 保持兼容,这里的逻辑应改为按制表符(或更通用的空白字符)拆分,而不是假定在 `measure'offset` 与 BPM 之间存在空格。
</issue_to_address>

### Comment 3
<location path="generator/chu/SusGenerator.cs" line_range="76-77" />
<code_context>
+        sb.AppendLine($"#REQUEST \"{sus.TicksPerBeat}\"");
+        sb.AppendLine();
+
+        foreach (var n in sus.Notes.OrderBy(n => n.Measure).ThenBy(n => n.Offset))
+            sb.AppendLine($"#{n.Measure:X2}{n.Offset:X3}:{FormatData(n)}");
+
+        return sb.ToString();
</code_context>
<issue_to_address>
**issue (bug_risk):** SUS 生成器使用 3 位十六进制 tick 字段,而解析器只读取 2 位,导致往返转换失败。

生成器会输出一个 5 位的时间字段 `"{measure:X2}{offset:X3}"`,但 `SusParser.ParseNoteLine` 期待的是 `#MMTT:data`,且仅读取 2 位 tick(`[..2]` 为小节,`[2..4]` 为 tick)。多出的那一位 tick 会被静默丢弃,从而截断大于 0xFF 的 offset,并在往返转换时丢失精度。请考虑要么将生成器改为只使用 2 位 tick(并相应调整时间分辨率),要么更新解析器以接受 3 位 tick 格式(例如 `[..2]` 为小节,其余部分为 tick),以保证格式一致并保留时间精度。
</issue_to_address>

### Comment 4
<location path="parser/chu/SusParser.cs" line_range="42" />
<code_context>
+    private static void ParseAirTarget(string dataStr, SusChart chart, List<Alert> alerts, int lineNum)
</code_context>
<issue_to_address>
**issue (bug_risk):** SUS AIR/ADW 目标解析期望使用十六进制编码的整数,但生成器写入的是原始的 `TargetNote` 字符串。

`ParseAirTarget` 要求 `dataStr.Length >= 8`,并解码 `HexToInt(dataStr[6..8])`,但 `SusGenerator.FormatData` 对 AIR/ADW 的输出是 `"{tc}{lw}{n.TargetNote}"`,其中 `TargetNote` 可以是一个非十六进制字符(例如 "N")。这会生成短于 8 个字符或包含非十六进制内容的字符串,从而导致警告或错误的目标解析。请考虑要么在生成器中将 `TargetNote` 编码为十六进制值以匹配当前解析逻辑,要么更新解析器,将结尾的子串视为原始的目标标记而不是十六进制数。
</issue_to_address>

### Comment 5
<location path="generator/chu/UgcGenerator.cs" line_range="88" />
<code_context>
+        sb.AppendLine($"@CONST\t{ugc.Constant:F5}");
+        sb.AppendLine($"@SONGID\t{ugc.SongId}");
+        sb.AppendLine($"@TICKS\t{ugc.TicksPerBeat}");
+        foreach (var b in ugc.BeatEvents) sb.AppendLine($"@BEAT\t{b.Measure}\t{b.Num}\t{b.Den}");
+        foreach (var b in ugc.BpmEvents) sb.AppendLine($"@BPM\t{b.Measure}'{b.Offset}\t{b.Bpm:F5}");
+        sb.AppendLine("@TIL\t0\t0'0\t1.00000");
</code_context>
<issue_to_address>
**suggestion (bug_risk):** UgcGenerator 的 @BEAT 序列化使用制表符,与当前 UgcParser 期望的空格分隔值不一致。

结合 `UgcParser.ParseHeaderLine`,意味着 `@BEAT` 被写成制表符分隔,而解析时却按空格分隔(`value.Split(' ')`)。即便你在解析器中做了修正,仍然建议选择一种统一的分隔符(制表符或空格),并在序列化和解析两侧进行文档说明,以避免未来再出现不一致,尤其是对手工编辑谱面的情况。

建议实现如下:

```csharp
        sb.AppendLine($"@TICKS\t{ugc.TicksPerBeat}");
        // NOTE: @BEAT fields are space-separated; keep this delimiter in sync with UgcParser.ParseHeaderLine
        foreach (var b in ugc.BeatEvents) sb.AppendLine($"@BEAT {b.Measure} {b.Num} {b.Den}");
        foreach (var b in ugc.BpmEvents) sb.AppendLine($"@BPM\t{b.Measure}'{b.Offset}\t{b.Bpm:F5}");

```

要完全落实该建议,UgcParser.ParseHeaderLine(或专门的 `@BEAT` 解析分支)也应更新为:
1. 使用与序列化端相同的分隔符(空格),例如对 `@BEAT` 载荷使用 `line.Split(' ', StringSplitOptions.RemoveEmptyEntries)`2. 在 UgcParser 中添加相应注释,说明 `@BEAT` 字段使用空格分隔,且必须与 UgcGenerator 的序列化逻辑保持同步。
</issue_to_address>

### Comment 6
<location path="generator/chu/C2sGenerator.cs" line_range="102-103" />
<code_context>
+            sb.AppendLine($"SFL\t{s.Measure}\t{s.Offset}\t{s.Duration}\t{Mlt(s.Multiplier)}");
+        sb.AppendLine();
+
+        foreach (var n in chart.Notes.OrderBy(n => n.Measure * C2sResolution + n.Offset))
+            sb.AppendLine(FormatNote(n));
+
+        sb.AppendLine();
</code_context>
<issue_to_address>
**issue (bug_risk):** 从 C2S 解析出的 ALD/ASD 音符类型在生成时没有区分处理,而是回退成 TAP。

`C2sParser.ParseNote``ALD``ASD` 视为具有额外属性的独立音符类型,但 `C2sGenerator.FormatNote` 中缺少对应的分支处理,最终会回退到默认的 TAP 序列化。这会在导出时静默降级这些音符。请考虑在 `FormatNote` 中为 `ALD`/`ASD` 添加显式处理,或者在碰到它们时记录/输出警告,让用户意识到细节被丢失了。
</issue_to_address>

### Comment 7
<location path="tests/chu/ChuTests.cs" line_range="7" />
<code_context>
+
+namespace MuConvert.Tests.chu;
+
+public class ChuTests
+{
+    private static string TestsetDir => Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "chu", "testset");
</code_context>
<issue_to_address>
**suggestion (testing):** 增加涵盖 SUS 解析器/生成器以及跨格式转换的测试

目前仅覆盖了 C2S 和 UGC 路径。请另外添加测试:(1) 解析一个 SUS 测试谱并验证关键字段/音符;(2) 通过 SusGenerator/SusParser 对 SUS 做往返测试(类似 C2sRoundTrip);以及 (3) 覆盖 C2S → SUS 和 UGC → SUS 的转换,以验证缩放和类型映射。这将更好地验证完整的 C2S/UGC/SUS 转换矩阵。

建议实现如下:

```csharp
    private static string TestsetDir => Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "chu", "testset");
    private static string OfficialDir => Path.Combine(TestsetDir, "官谱", "B.B.K.K.B.K.K");
    private static string CustomDir => Path.Combine(TestsetDir, "自制谱", "Example");
    private static string C2sPath => Path.Combine(OfficialDir, "0003_00.c2s");
    private static string UgcPath => Path.Combine(CustomDir, "basic.ugc");
    private static string SusPath => Path.Combine(OfficialDir, "0003_00.sus");

    [Fact]
    public void CanParseOfficialSus()
    {
        if (!File.Exists(SusPath)) throw new SkipException($"Missing: {SusPath}");

        var susText = File.ReadAllText(SusPath);
        var (chart, _) = new SusParser().Parse(susText);

        Assert.NotNull(chart);
    }

    [Fact]
    public void SusRoundTrip()
    {
        if (!File.Exists(SusPath)) throw new SkipException($"Missing: {SusPath}");

        var originalSus = File.ReadAllText(SusPath);
        var (chart, _) = new SusParser().Parse(originalSus);

        var generatedSus = new SusGenerator().Generate(chart);
        Assert.False(string.IsNullOrWhiteSpace(generatedSus));

        var (roundTrippedChart, _) = new SusParser().Parse(generatedSus);
        Assert.NotNull(roundTrippedChart);
    }

    [Fact]
    public void CanConvertC2sToSus()
    {
        if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}");

        var (chart, _) = new C2sParser().Parse(File.ReadAllText(C2sPath));
        var susText = new SusGenerator().Generate(chart);

        Assert.False(string.IsNullOrWhiteSpace(susText));

        var (susChart, _) = new SusParser().Parse(susText);
        Assert.NotNull(susChart);
    }

    [Fact]
    public void CanConvertUgcToSus()
    {
        if (!File.Exists(UgcPath)) throw new SkipException($"Missing: {UgcPath}");

        var (chart, _) = new UgcParser().Parse(File.ReadAllText(UgcPath));
        var susText = new SusGenerator().Generate(chart);

        Assert.False(string.IsNullOrWhiteSpace(susText));

        var (susChart, _) = new SusParser().Parse(susText);
        Assert.NotNull(susChart);
    }

    [Fact]
    public void CanParseOfficialC2S()

```

这些修改假定:
1. `SusParser``SusGenerator` 已存在于 `MuConvert.parser` / `MuConvert.chu` 命名空间中,并提供与 `C2sParser`/`UgcParser` 相同元组返回形态的 `Parse(string)``Generate(chart)` 方法;
2.`官谱/B.B.K.K.B.K.K/0003_00.sus` 位置存在一份官方 SUS 文件。如果你的测试数据使用了不同的文件名或目录结构,请相应调整 `SusPath`3. 如果谱面类型暴露了更丰富的属性(例如音符数量、时间、轨道数据等),并且已经有用于谱面深度比较的辅助方法,可以扩展这些测试中的断言,用来比较这些字段,从而更加直接地验证跨格式的缩放和类型映射。
</issue_to_address>

### Comment 8
<location path="tests/chu/ChuTests.cs" line_range="25-31" />
<code_context>
+    }
+
+    [Fact]
+    public void C2sRoundTrip()
+    {
+        if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}");
+        var (chart, _) = new C2sParser().Parse(File.ReadAllText(C2sPath));
+        var (rt, _) = new C2sGenerator().Generate(chart);
+        var (reparsed, _) = new C2sParser().Parse(rt);
+        Assert.Equal(chart.Notes.Count, reparsed.Notes.Count);
+    }
+
</code_context>
<issue_to_address>
**suggestion (testing):** 将 C2S 往返测试的断言从仅比较音符数量加强为更严格的验证

目前仅比较解析 → 生成 → 再解析后音符总数是否相同。如果音符被重新排序、时间发生变化或者部分字段丢失,但数量不变,这个测试仍然可能通过。请加强断言,例如对关键字段(小节、偏移、类型、格子、宽度、时长/结束位置等)的有序投影进行比较,或者至少确保具有代表性的音符类型(如滑条、长押、AIR)在往返后保持不变。这样能更好地验证解析器/生成器对数据的无损性。

建议实现如下:

```csharp
    [Fact]
    public void C2sRoundTrip()
    {
        if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}");

        var (chart, _) = new C2sParser().Parse(File.ReadAllText(C2sPath));
        var (rt, _) = new C2sGenerator().Generate(chart);
        var (reparsed, _) = new C2sParser().Parse(rt);

        // Basic sanity: note count must be preserved
        Assert.Equal(chart.Notes.Count, reparsed.Notes.Count);

        // Stronger guarantee: the set of notes (by their public properties) must be preserved
        var originalSnapshots = chart.Notes
            .Select(SnapshotNote)
            .OrderBy(s => s)
            .ToArray();

        var reparsedSnapshots = reparsed.Notes
            .Select(SnapshotNote)
            .OrderBy(s => s)
            .ToArray();

        Assert.Equal(originalSnapshots, reparsedSnapshots);
    }


```

```csharp
    private static string C2sPath => Path.Combine(OfficialDir, "0003_00.c2s");
    private static string UgcPath => Path.Combine(CustomDir, "basic.ugc");

    /// <summary>
    /// Creates a stable, comparable string snapshot of a note by concatenating all
    /// of its public instance properties in name-sorted order. This lets the
    /// round-trip test verify that no note data was lost or reordered without
    /// needing to know the concrete note type shape.
    /// </summary>
    private static string SnapshotNote(object note)
    {
        if (note is null) return string.Empty;

        var type = note.GetType();
        var props = type.GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);

        var parts = props
            .OrderBy(p => p.Name)
            .Select(p => $"{p.Name}={p.GetValue(note)}");

        return string.Join("|", parts);
    }


```

该修改假定 `ChuTests.cs` 中已经通过 `using System.Linq;` 引入了 `Select``OrderBy``ToArray`。如果没有,请在文件顶部的其他 `using` 之后添加 `using System.Linq;`。
</issue_to_address>

### Comment 9
<location path="tests/chu/ChuTests.cs" line_range="16-21" />
<code_context>
+    private static string UgcPath => Path.Combine(CustomDir, "basic.ugc");
+
+    [Fact]
+    public void CanParseOfficialC2S()
+    {
+        if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}");
+        var (chart, _) = new C2sParser().Parse(File.ReadAllText(C2sPath));
+        Assert.NotEmpty(chart.Notes);
+        Assert.Equal(384, chart.Resolution);
+    }
+
</code_context>
<issue_to_address>
**suggestion (testing):** 考虑为 C2S/UGC 添加负例/边界情况解析测试,以验证告警行为

这些测试目前只覆盖了正常路径的谱面。由于解析器会针对错误输入(例如未知音符类型、错误头部、缺少跟随行等)生成 `Alert` 对象,请增加一些使用小型合成输入的单元测试,命中这些错误路径,并断言解析可以成功完成,同时产生预期的告警(级别、消息和行号信息)。这有助于捕捉错误处理和本地化(例如 `Locale.C2SUnknownNoteType`)方面的回归问题。

建议实现如下:

```csharp
    private static string UgcPath => Path.Combine(CustomDir, "basic.ugc");

    [Fact]
    public void CanParseOfficialC2S()
    {
        if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}");
        var (chart, _) = new C2sParser().Parse(File.ReadAllText(C2sPath));
        Assert.NotEmpty(chart.Notes);
        Assert.Equal(384, chart.Resolution);
    }

    [Fact]
    public void C2S_UnknownNoteType_ProducesAlert()
    {
        // Minimal C2S chart with an unknown note type to drive the "unknown note type" alert.
        const string input =
            "VERSION:1\n" +
            "TITLE:Test\n" +
            "ARTIST:Test\n" +
            "BEAT:4/4\n" +
            "BPM:120\n" +
            "START:\n" +
            "001,1,UNKNOWN,1,0\n";

        var (chart, alerts) = new C2sParser().Parse(input);

        Assert.NotNull(chart);
        Assert.NotNull(alerts);

        var alert = Assert.Single(alerts);
        Assert.Equal(AlertLevel.Error, alert.Level);
        Assert.Equal(Locale.C2SUnknownNoteType, alert.Message);
        Assert.True(alert.Line > 0);
    }

    [Fact]
    public void C2S_MissingFollowerLine_ProducesAlert()
    {
        // Start of a flick/slide without the expected follower line.
        const string input =
            "VERSION:1\n" +
            "TITLE:Test\n" +
            "ARTIST:Test\n" +
            "BEAT:4/4\n" +
            "BPM:120\n" +
            "START:\n" +
            "001,1,START_FLICK,1,0\n";

        var (chart, alerts) = new C2sParser().Parse(input);

        Assert.NotNull(chart);
        Assert.NotNull(alerts);
        Assert.NotEmpty(chart.Notes);

        var alert = Assert.Single(alerts);
        Assert.Equal(AlertLevel.Warning, alert.Level);
        Assert.Equal(Locale.C2SMissingFollowerNote, alert.Message);
        Assert.True(alert.Line > 0);
    }

    [Fact]
    public void Ugc_UnknownNoteType_ProducesAlert()
    {
        // UGC chart with an unknown note type token.
        const string input =
            "#TITLE:Test\n" +
            "#ARTIST:Test\n" +
            "#BPM:120\n" +
            "#DIFFICULTY:1\n" +
            "001,1,UNKNOWN,1,0\n";

        var (chart, alerts) = new UgcParser().Parse(input);

        Assert.NotNull(chart);
        Assert.NotNull(alerts);

        var alert = Assert.Single(alerts);
        Assert.Equal(AlertLevel.Error, alert.Level);
        Assert.Equal(Locale.UGCUnknownNoteType, alert.Message);
        Assert.True(alert.Line > 0);
    }

    [Fact]
    public void Ugc_InvalidHeader_ProducesAlert()
    {
        // Header with missing title and invalid BPM to hit header-validation alerts.
        const string input =
            "#TITLE:\n" +      // missing title value
            "#ARTIST:Test\n" +
            "#BPM:abc\n" +     // invalid BPM
            "#DIFFICULTY:1\n" +
            "001,1,TAP,1,0\n";

        var (chart, alerts) = new UgcParser().Parse(input);

        Assert.NotNull(chart);
        Assert.NotNull(alerts);
        Assert.NotEmpty(chart.Notes);

        Assert.NotEmpty(alerts);
        Assert.Contains(alerts, a => a.Level == AlertLevel.Error && a.Message == Locale.UGCInvalidBpm);
        Assert.All(alerts, a => Assert.True(a.Line > 0));
    }

```

1. 确保文件中已经为解析器、告警类型和本地化引入了必要的 `using` 指令:
   - `using MuConvert.Chu;`(或实际包含 `C2sParser`/`UgcParser` 的命名空间);
   - `using MuConvert.Chu.Localization;`(或 `Locale` 所在的命名空间);
   - `using MuConvert.Chu.Alerts;`(或 `Alert`/`AlertLevel` 所在的命名空间),如果它们尚未在作用域内。
2. 如果实际类型或属性名称与此不同,请相应调整:
   - 若告警暴露的是 `Severity``MessageKey``LineNumber` 等属性,请将断言中的 `alert.Level``alert.Message``alert.Line` 改为对应的真实 API,并与正确的本地化键或值进行比较(例如 `Locale.C2SUnknownNoteType` 可能是一个键而非字符串);
   - 若解析器返回的不是 `(chart, alerts)` 元组,而是自定义结果类型,请调整解构和访问方式。
3. 必要时根据真实语法对上述 C2S/UGC 合成文本做适当修改(字段数、`START_FLICK` 等 token),以确保它们既能被正确解析,又能触发目标告警路径。
4. 如果 `ChuTests.cs` 中已经存在名为 `CanParseOfficialC2S` 的方法,请删除新增的同名方法,避免重复定义。
</issue_to_address>

### Comment 10
<location path="tests/chu/ChuTests.cs" line_range="43-52" />
<code_context>
+    }
+
+    [Fact]
+    public void UgcToC2sViaGenerator()
+    {
+        if (!File.Exists(UgcPath)) throw new SkipException($"Missing: {UgcPath}");
+        var (ugc, _) = new UgcParser().Parse(File.ReadAllText(UgcPath));
+        var (c2sText, _) = new C2sGenerator().Generate(ugc);
+        Assert.Contains("VERSION", c2sText);
+        Assert.Contains("TAP\t", c2sText);
+    }
+
</code_context>
<issue_to_address>
**suggestion (testing):** 加强 UgcToC2sViaGenerator 的断言,使其验证内容不仅局限于关键字是否存在

当前断言只检查 `"VERSION"``"TAP\t"` 是否存在,因此即便大部分音符被丢弃或时间完全错误,该测试仍可能通过。请考虑增加往返流程:使用 `C2sParser` 解析生成的 C2S,并对结果谱面进行断言(例如:音符非空、存在预期的音符类型、大致的音符数量或某些已知音符属性),从而让这一跨格式转换测试更加健壮。

```suggestion
    [Fact]
    public void UgcToC2sViaGenerator()
    {
        if (!File.Exists(UgcPath)) throw new SkipException($"Missing: {UgcPath}");

        // Parse UGC chart
        var (ugcChart, _) = new UgcParser().Parse(File.ReadAllText(UgcPath));
        Assert.NotEmpty(ugcChart.Notes);

        // Generate C2S text from UGC chart
        var (c2sText, _) = new C2sGenerator().Generate(ugcChart);
        Assert.Contains("VERSION", c2sText);
        Assert.Contains("TAP\t", c2sText);

        // Parse back the generated C2S into a chart
        var (c2sChart, _) = new C2sParser().Parse(c2sText);

        // Ensure the generated chart is non-empty
        Assert.NotEmpty(c2sChart.Notes);

        // Ensure note count is approximately preserved across formats
        var originalNoteCount = ugcChart.Notes.Count;
        var generatedNoteCount = c2sChart.Notes.Count;
        Assert.InRange(generatedNoteCount, (int)(originalNoteCount * 0.8), (int)(originalNoteCount * 1.2));
    }

```
</issue_to_address>

### Comment 11
<location path="tests/chu/ChuTests.cs" line_range="54-63" />
<code_context>
+    }
+
+    [Fact]
+    public void DumpOutputFiles()
+    {
+        if (!File.Exists(UgcPath)) throw new SkipException($"Missing: {UgcPath}");
+        var (ugc, _) = new UgcParser().Parse(File.ReadAllText(UgcPath));
+        var (c2sText, _) = new C2sGenerator().Generate(ugc);
+        File.WriteAllText(Path.Combine(OfficialDir, "basic_output.c2s"), c2sText);
+
+        if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}");
+        var (c2s, _) = new C2sParser().Parse(File.ReadAllText(C2sPath));
+        var (ugcText, _) = new UgcGenerator().Generate(c2s);
+        File.WriteAllText(Path.Combine(OfficialDir, "0003_output.ugc"), ugcText);
+
+        Assert.True(File.Exists(Path.Combine(OfficialDir, "basic_output.c2s")));
+        Assert.True(File.Exists(Path.Combine(OfficialDir, "0003_output.ugc")));
+    }
+}
</code_context>
<issue_to_address>
**suggestion (testing):** 避免在 DumpOutputFiles 中产生文件系统副作用,或明确将其标记为非单元/集成测试

该测试会向仓库中的 `OfficialDir` 写入文件并据此断言,这在并行执行、只读 CI 环境或导致工作区脏状态时都可能产生不稳定因素。请考虑将其改为使用临时目录的集成/基线文件测试(可按需运行),或者从标准单元测试集中移除这些文件写入和断言,以保持单元测试的无副作用性。

建议实现如下:

```csharp
    [Fact]
    public void DumpOutputFiles()
    {
        if (!File.Exists(UgcPath)) throw new SkipException($"Missing: {UgcPath}");

        // Use an isolated temp directory to avoid polluting the repo and to be CI‑safe.
        var tempDir = Path.Combine(
            Path.GetTempPath(),
            "ChuTests",
            "DumpOutputFiles",
            Guid.NewGuid().ToString("N"));

        Directory.CreateDirectory(tempDir);

        var c2sOutputPath = Path.Combine(tempDir, "basic_output.c2s");
        var ugcOutputPath = Path.Combine(tempDir, "0003_output.ugc");

        try
        {
            var (ugc, _) = new UgcParser().Parse(File.ReadAllText(UgcPath));
            var (c2sText, _) = new C2sGenerator().Generate(ugc);
            File.WriteAllText(c2sOutputPath, c2sText);

            if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}");
            var (c2s, _) = new C2sParser().Parse(File.ReadAllText(C2sPath));
            var (ugcText, _) = new UgcGenerator().Generate(c2s);
            File.WriteAllText(ugcOutputPath, ugcText);

            Assert.True(File.Exists(c2sOutputPath));
            Assert.True(File.Exists(ugcOutputPath));
        }
        finally
        {
            // Best‑effort cleanup; ignore failures so test results are driven by assertions above.
            try
            {
                if (Directory.Exists(tempDir))
                {
                    Directory.Delete(tempDir, recursive: true);
                }
            }
            catch
            {
                // intentionally ignored
            }
        }
    }

```

1. 确保在 `tests/chu/ChuTests.cs` 顶部已经引入 `using System;``using System.IO;`,以便 `Guid``Path``File``Directory` 能正确解析;
2. 如果你已在测试集中使用特性/分类系统来标记集成测试,并希望明确指出这是非单元测试,可以按照现有约定为该方法加上额外特性,例如 `[Trait("Category", "Integration")]`。
</issue_to_address>

Sourcery 对开源项目免费使用——如果你觉得这些 Review 有帮助,可以考虑分享给更多人 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进 Review 质量。
Original comment in English

Hey - I've found 11 issues, and left some high level feedback:

  • Numeric parsing/formatting in the UGC/SUS paths currently rely on the current culture (e.g., double.TryParse in UgcParser.ParseHeaderLine and string interpolation like {b.Bpm:F5} / {sus.Bpm:F2}), which can break on non-en-US locales; consider using CultureInfo.InvariantCulture consistently as done in C2sGenerator.
  • Tick-scaling helpers such as UgcGenerator.ScaleUpNote (s(int v) => v * UgcTicksPerBeat / (C2sResolution / 4)) and SusGenerator.ScaleUp perform arithmetic in int and may overflow on large values; aligning these with C2sGenerator.ScaleNote by promoting to long before multiplication would make them safer and more consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Numeric parsing/formatting in the UGC/SUS paths currently rely on the current culture (e.g., `double.TryParse` in `UgcParser.ParseHeaderLine` and string interpolation like `{b.Bpm:F5}` / `{sus.Bpm:F2}`), which can break on non-`en-US` locales; consider using `CultureInfo.InvariantCulture` consistently as done in `C2sGenerator`.
- Tick-scaling helpers such as `UgcGenerator.ScaleUpNote` (`s(int v) => v * UgcTicksPerBeat / (C2sResolution / 4)`) and `SusGenerator.ScaleUp` perform arithmetic in `int` and may overflow on large values; aligning these with `C2sGenerator.ScaleNote` by promoting to `long` before multiplication would make them safer and more consistent.

## Individual Comments

### Comment 1
<location path="parser/chu/UgcParser.cs" line_range="136-78" />
<code_context>
+                    alerts.Add(new Alert(Warning, $"@TICKS 格式错误: {line}") { Line = lineNum });
+                break;
+
+            case "@BEAT":
+                var beatParts = value.Split(' ');
+                if (beatParts.Length >= 3
+                    && int.TryParse(beatParts[0], out var beatMeasure)
+                    && int.TryParse(beatParts[1], out var beatNum)
+                    && int.TryParse(beatParts[2], out var beatDen))
+                {
+                    chart.BeatEvents.Add((beatMeasure, beatNum, beatDen));
+                }
+                else
+                {
+                    alerts.Add(new Alert(Warning, $"@BEAT 格式错误: {line}") { Line = lineNum });
+                }
+                break;
+
+            case "@BPM":
</code_context>
<issue_to_address>
**issue (bug_risk):** @BEAT parsing assumes space separators but the generator emits tab separators, causing all BEAT lines to be treated as malformed.

In `ParseHeaderLine`, `value` is the substring after the first tab and may still contain tabs, but here it’s split on spaces: `value.Split(' ')`. `UgcGenerator.Serialize` emits `@BEAT` as `"@BEAT\t{b.Measure}\t{b.Num}\t{b.Den}"`, so there are no spaces, `beatParts.Length` is always 1, and all `@BEAT` lines are treated as malformed. To match the serializer, split on tabs or general whitespace instead, e.g. `value.Split('\t', ' ')` or `value.Split((char[])null, StringSplitOptions.RemoveEmptyEntries)`.
</issue_to_address>

### Comment 2
<location path="parser/chu/UgcParser.cs" line_range="151-78" />
<code_context>
+            case "@BPM":
</code_context>
<issue_to_address>
**issue (bug_risk):** @BPM parsing is incompatible with the serialized format that uses tabs instead of spaces.

For `@BPM`, `value` is taken as the substring after the first tab, so for a generated line like `@BPM	0'0	120.00000`, `value` is `"0'0	120.00000"`. The parser then calls `IndexOf(' ')`, which returns `-1` because the separator is a tab, causing the line to be treated as malformed. To stay compatible with `UgcGenerator`, this logic should split on tabs (or more generally whitespace) instead of assuming a space between `measure'offset` and BPM.
</issue_to_address>

### Comment 3
<location path="generator/chu/SusGenerator.cs" line_range="76-77" />
<code_context>
+        sb.AppendLine($"#REQUEST \"{sus.TicksPerBeat}\"");
+        sb.AppendLine();
+
+        foreach (var n in sus.Notes.OrderBy(n => n.Measure).ThenBy(n => n.Offset))
+            sb.AppendLine($"#{n.Measure:X2}{n.Offset:X3}:{FormatData(n)}");
+
+        return sb.ToString();
</code_context>
<issue_to_address>
**issue (bug_risk):** SUS generator uses a 3-digit hex tick field while the parser only reads 2 digits, breaking round-trips.

The generator emits a 5‑digit timing field `"{measure:X2}{offset:X3}"`, but `SusParser.ParseNoteLine` expects `#MMTT:data` and only reads 2 digits for the tick (`[..2]` for measure, `[2..4]` for tick). The extra tick digit is silently dropped, truncating offsets > 0xFF and losing precision on round‑trip. Please either change the generator to use a 2‑digit tick (and align timing resolution accordingly) or update the parser to accept the 3‑digit tick format (e.g., `[..2]` for measure and the remainder for tick) so the formats are consistent and timing is preserved.
</issue_to_address>

### Comment 4
<location path="parser/chu/SusParser.cs" line_range="42" />
<code_context>
+    private static void ParseAirTarget(string dataStr, SusChart chart, List<Alert> alerts, int lineNum)
</code_context>
<issue_to_address>
**issue (bug_risk):** SUS AIR/ADW target parsing expects hex-encoded integers, but the generator writes raw `TargetNote` strings.

`ParseAirTarget` enforces `dataStr.Length >= 8` and decodes `HexToInt(dataStr[6..8])`, but `SusGenerator.FormatData` emits AIR/ADW as `"{tc}{lw}{n.TargetNote}"`, where `TargetNote` can be a single non-hex character (e.g., "N"). This can produce strings shorter than 8 chars or with non-hex content, causing warnings or incorrect targets. Please either encode `TargetNote` as a hex value in the generator to match this parser, or update the parser to treat the trailing substring as a raw target token instead of hex.
</issue_to_address>

### Comment 5
<location path="generator/chu/UgcGenerator.cs" line_range="88" />
<code_context>
+        sb.AppendLine($"@CONST\t{ugc.Constant:F5}");
+        sb.AppendLine($"@SONGID\t{ugc.SongId}");
+        sb.AppendLine($"@TICKS\t{ugc.TicksPerBeat}");
+        foreach (var b in ugc.BeatEvents) sb.AppendLine($"@BEAT\t{b.Measure}\t{b.Num}\t{b.Den}");
+        foreach (var b in ugc.BpmEvents) sb.AppendLine($"@BPM\t{b.Measure}'{b.Offset}\t{b.Bpm:F5}");
+        sb.AppendLine("@TIL\t0\t0'0\t1.00000");
</code_context>
<issue_to_address>
**suggestion (bug_risk):** UgcGenerator’s @BEAT serialization uses tabs, which currently conflicts with UgcParser’s expectation of space-separated values.

Together with `UgcParser.ParseHeaderLine`, this means `@BEAT` is written as tab‑separated but parsed as space‑separated (`value.Split(' ')`). Even after aligning the parser, please choose a single delimiter (tabs vs spaces) and document it in both serializer and parser to avoid future inconsistencies, especially for hand-edited charts.

Suggested implementation:

```csharp
        sb.AppendLine($"@TICKS\t{ugc.TicksPerBeat}");
        // NOTE: @BEAT fields are space-separated; keep this delimiter in sync with UgcParser.ParseHeaderLine
        foreach (var b in ugc.BeatEvents) sb.AppendLine($"@BEAT {b.Measure} {b.Num} {b.Den}");
        foreach (var b in ugc.BpmEvents) sb.AppendLine($"@BPM\t{b.Measure}'{b.Offset}\t{b.Bpm:F5}");

```

To fully implement the suggestion, UgcParser.ParseHeaderLine (or the specific `@BEAT` parsing path) should be updated to:
1. Use the same delimiter (spaces) as the serializer, e.g. `line.Split(' ', StringSplitOptions.RemoveEmptyEntries)` for the `@BEAT` payload.
2. Add a corresponding comment in UgcParser documenting that `@BEAT` fields are space-separated and must remain in sync with UgcGenerator’s serialization.
</issue_to_address>

### Comment 6
<location path="generator/chu/C2sGenerator.cs" line_range="102-103" />
<code_context>
+            sb.AppendLine($"SFL\t{s.Measure}\t{s.Offset}\t{s.Duration}\t{Mlt(s.Multiplier)}");
+        sb.AppendLine();
+
+        foreach (var n in chart.Notes.OrderBy(n => n.Measure * C2sResolution + n.Offset))
+            sb.AppendLine(FormatNote(n));
+
+        sb.AppendLine();
</code_context>
<issue_to_address>
**issue (bug_risk):** ALD/ASD note types parsed from C2S are not emitted distinctly and fall back to TAP in the generator.

`C2sParser.ParseNote` treats `ALD` and `ASD` as distinct note types with extra properties, but `C2sGenerator.FormatNote` lacks corresponding cases and falls back to the default TAP serialization. This will silently degrade these notes on export. Please either add explicit `ALD`/`ASD` handling in `FormatNote` or log/emit a warning when they’re encountered so users are aware of the loss of detail.
</issue_to_address>

### Comment 7
<location path="tests/chu/ChuTests.cs" line_range="7" />
<code_context>
+
+namespace MuConvert.Tests.chu;
+
+public class ChuTests
+{
+    private static string TestsetDir => Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "chu", "testset");
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests that cover SUS parser/generator and cross-format conversions

Currently only C2S and UGC paths are covered. Please also add tests that (1) parse a SUS test chart and verify key fields/notes, (2) round-trip SUS via SusGenerator/SusParser (like C2sRoundTrip), and (3) exercise C2S → SUS and UGC → SUS conversions to verify scaling and type mapping. This will better validate the full C2S/UGC/SUS conversion matrix.

Suggested implementation:

```csharp
    private static string TestsetDir => Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "chu", "testset");
    private static string OfficialDir => Path.Combine(TestsetDir, "官谱", "B.B.K.K.B.K.K");
    private static string CustomDir => Path.Combine(TestsetDir, "自制谱", "Example");
    private static string C2sPath => Path.Combine(OfficialDir, "0003_00.c2s");
    private static string UgcPath => Path.Combine(CustomDir, "basic.ugc");
    private static string SusPath => Path.Combine(OfficialDir, "0003_00.sus");

    [Fact]
    public void CanParseOfficialSus()
    {
        if (!File.Exists(SusPath)) throw new SkipException($"Missing: {SusPath}");

        var susText = File.ReadAllText(SusPath);
        var (chart, _) = new SusParser().Parse(susText);

        Assert.NotNull(chart);
    }

    [Fact]
    public void SusRoundTrip()
    {
        if (!File.Exists(SusPath)) throw new SkipException($"Missing: {SusPath}");

        var originalSus = File.ReadAllText(SusPath);
        var (chart, _) = new SusParser().Parse(originalSus);

        var generatedSus = new SusGenerator().Generate(chart);
        Assert.False(string.IsNullOrWhiteSpace(generatedSus));

        var (roundTrippedChart, _) = new SusParser().Parse(generatedSus);
        Assert.NotNull(roundTrippedChart);
    }

    [Fact]
    public void CanConvertC2sToSus()
    {
        if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}");

        var (chart, _) = new C2sParser().Parse(File.ReadAllText(C2sPath));
        var susText = new SusGenerator().Generate(chart);

        Assert.False(string.IsNullOrWhiteSpace(susText));

        var (susChart, _) = new SusParser().Parse(susText);
        Assert.NotNull(susChart);
    }

    [Fact]
    public void CanConvertUgcToSus()
    {
        if (!File.Exists(UgcPath)) throw new SkipException($"Missing: {UgcPath}");

        var (chart, _) = new UgcParser().Parse(File.ReadAllText(UgcPath));
        var susText = new SusGenerator().Generate(chart);

        Assert.False(string.IsNullOrWhiteSpace(susText));

        var (susChart, _) = new SusParser().Parse(susText);
        Assert.NotNull(susChart);
    }

    [Fact]
    public void CanParseOfficialC2S()

```

These edits assume:
1. `SusParser` and `SusGenerator` exist in the `MuConvert.parser` / `MuConvert.chu` namespaces and expose `Parse(string)` and `Generate(chart)` methods with the same tuple shape as `C2sParser`/`UgcParser`.
2. There is an official SUS file at `官谱/B.B.K.K.B.K.K/0003_00.sus`. If your test data uses a different filename or directory structure, adjust `SusPath` accordingly.
3. If the chart type exposes richer properties (e.g., note counts, timing, or lane data) and there are existing helpers for deep equality between charts, you can extend the assertions in these tests to compare those fields to more directly validate scaling and type mapping across formats.
</issue_to_address>

### Comment 8
<location path="tests/chu/ChuTests.cs" line_range="25-31" />
<code_context>
+    }
+
+    [Fact]
+    public void C2sRoundTrip()
+    {
+        if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}");
+        var (chart, _) = new C2sParser().Parse(File.ReadAllText(C2sPath));
+        var (rt, _) = new C2sGenerator().Generate(chart);
+        var (reparsed, _) = new C2sParser().Parse(rt);
+        Assert.Equal(chart.Notes.Count, reparsed.Notes.Count);
+    }
+
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen C2S round-trip assertions beyond just comparing note counts

Right now this only compares the total note count after parse → generate → parse. That could still pass if notes are reordered, timings change, or fields are dropped while keeping the same count. Please tighten the assertion: for example, compare a sorted projection of key fields (measure, offset, type, cell, width, duration/end position, etc.), or at least assert that representative notes (e.g., slide, hold, air) survive the round trip unchanged. This will better validate that the parser/generator pair is lossless.

Suggested implementation:

```csharp
    [Fact]
    public void C2sRoundTrip()
    {
        if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}");

        var (chart, _) = new C2sParser().Parse(File.ReadAllText(C2sPath));
        var (rt, _) = new C2sGenerator().Generate(chart);
        var (reparsed, _) = new C2sParser().Parse(rt);

        // Basic sanity: note count must be preserved
        Assert.Equal(chart.Notes.Count, reparsed.Notes.Count);

        // Stronger guarantee: the set of notes (by their public properties) must be preserved
        var originalSnapshots = chart.Notes
            .Select(SnapshotNote)
            .OrderBy(s => s)
            .ToArray();

        var reparsedSnapshots = reparsed.Notes
            .Select(SnapshotNote)
            .OrderBy(s => s)
            .ToArray();

        Assert.Equal(originalSnapshots, reparsedSnapshots);
    }


```

```csharp
    private static string C2sPath => Path.Combine(OfficialDir, "0003_00.c2s");
    private static string UgcPath => Path.Combine(CustomDir, "basic.ugc");

    /// <summary>
    /// Creates a stable, comparable string snapshot of a note by concatenating all
    /// of its public instance properties in name-sorted order. This lets the
    /// round-trip test verify that no note data was lost or reordered without
    /// needing to know the concrete note type shape.
    /// </summary>
    private static string SnapshotNote(object note)
    {
        if (note is null) return string.Empty;

        var type = note.GetType();
        var props = type.GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);

        var parts = props
            .OrderBy(p => p.Name)
            .Select(p => $"{p.Name}={p.GetValue(note)}");

        return string.Join("|", parts);
    }


```

This change assumes `System.Linq` is already imported in `ChuTests.cs` for `Select`, `OrderBy`, and `ToArray`.  
If it is not, add `using System.Linq;` at the top of the file with the other `using` directives.
</issue_to_address>

### Comment 9
<location path="tests/chu/ChuTests.cs" line_range="16-21" />
<code_context>
+    private static string UgcPath => Path.Combine(CustomDir, "basic.ugc");
+
+    [Fact]
+    public void CanParseOfficialC2S()
+    {
+        if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}");
+        var (chart, _) = new C2sParser().Parse(File.ReadAllText(C2sPath));
+        Assert.NotEmpty(chart.Notes);
+        Assert.Equal(384, chart.Resolution);
+    }
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding negative/edge-case parsing tests for C2S/UGC to validate alert behavior

These tests currently only cover happy-path charts. Since the parsers emit `Alert` objects for malformed input (e.g., unknown note types, bad headers, missing follower lines), please add focused unit tests with small synthetic inputs that hit those error paths and assert that parsing succeeds while producing the expected alerts (LEVEL, message, and line info). This will help catch regressions in error handling and localization (e.g., `Locale.C2SUnknownNoteType`).

Suggested implementation:

```csharp
    private static string UgcPath => Path.Combine(CustomDir, "basic.ugc");

    [Fact]
    public void CanParseOfficialC2S()
    {
        if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}");
        var (chart, _) = new C2sParser().Parse(File.ReadAllText(C2sPath));
        Assert.NotEmpty(chart.Notes);
        Assert.Equal(384, chart.Resolution);
    }

    [Fact]
    public void C2S_UnknownNoteType_ProducesAlert()
    {
        // Minimal C2S chart with an unknown note type to drive the "unknown note type" alert.
        const string input =
            "VERSION:1\n" +
            "TITLE:Test\n" +
            "ARTIST:Test\n" +
            "BEAT:4/4\n" +
            "BPM:120\n" +
            "START:\n" +
            "001,1,UNKNOWN,1,0\n";

        var (chart, alerts) = new C2sParser().Parse(input);

        Assert.NotNull(chart);
        Assert.NotNull(alerts);

        var alert = Assert.Single(alerts);
        Assert.Equal(AlertLevel.Error, alert.Level);
        Assert.Equal(Locale.C2SUnknownNoteType, alert.Message);
        Assert.True(alert.Line > 0);
    }

    [Fact]
    public void C2S_MissingFollowerLine_ProducesAlert()
    {
        // Start of a flick/slide without the expected follower line.
        const string input =
            "VERSION:1\n" +
            "TITLE:Test\n" +
            "ARTIST:Test\n" +
            "BEAT:4/4\n" +
            "BPM:120\n" +
            "START:\n" +
            "001,1,START_FLICK,1,0\n";

        var (chart, alerts) = new C2sParser().Parse(input);

        Assert.NotNull(chart);
        Assert.NotNull(alerts);
        Assert.NotEmpty(chart.Notes);

        var alert = Assert.Single(alerts);
        Assert.Equal(AlertLevel.Warning, alert.Level);
        Assert.Equal(Locale.C2SMissingFollowerNote, alert.Message);
        Assert.True(alert.Line > 0);
    }

    [Fact]
    public void Ugc_UnknownNoteType_ProducesAlert()
    {
        // UGC chart with an unknown note type token.
        const string input =
            "#TITLE:Test\n" +
            "#ARTIST:Test\n" +
            "#BPM:120\n" +
            "#DIFFICULTY:1\n" +
            "001,1,UNKNOWN,1,0\n";

        var (chart, alerts) = new UgcParser().Parse(input);

        Assert.NotNull(chart);
        Assert.NotNull(alerts);

        var alert = Assert.Single(alerts);
        Assert.Equal(AlertLevel.Error, alert.Level);
        Assert.Equal(Locale.UGCUnknownNoteType, alert.Message);
        Assert.True(alert.Line > 0);
    }

    [Fact]
    public void Ugc_InvalidHeader_ProducesAlert()
    {
        // Header with missing title and invalid BPM to hit header-validation alerts.
        const string input =
            "#TITLE:\n" +      // missing title value
            "#ARTIST:Test\n" +
            "#BPM:abc\n" +     // invalid BPM
            "#DIFFICULTY:1\n" +
            "001,1,TAP,1,0\n";

        var (chart, alerts) = new UgcParser().Parse(input);

        Assert.NotNull(chart);
        Assert.NotNull(alerts);
        Assert.NotEmpty(chart.Notes);

        Assert.NotEmpty(alerts);
        Assert.Contains(alerts, a => a.Level == AlertLevel.Error && a.Message == Locale.UGCInvalidBpm);
        Assert.All(alerts, a => Assert.True(a.Line > 0));
    }

```

1. Ensure the file has the necessary `using` directives for the parser and alert types and localization:
   - `using MuConvert.Chu;` (or the actual namespace containing `C2sParser`/`UgcParser`).
   - `using MuConvert.Chu.Localization;` (or the actual namespace for `Locale`).
   - `using MuConvert.Chu.Alerts;` (or the actual namespace for `Alert`/`AlertLevel`), if they are not already in scope.
2. Adjust property and type names if they differ from the assumptions above:
   - If alerts expose properties like `Severity`, `MessageKey`, `LineNumber`, etc., update the assertions (`alert.Level`, `alert.Message`, `alert.Line`) to match the real API and to compare against the correct localized value/key (e.g., `Locale.C2SUnknownNoteType` might be a key instead of a string).
   - If the parsers return something other than a tuple `(chart, alerts)` (e.g., a custom result type), adapt the destructuring and access accordingly.
3. The synthetic C2S/UGC snippets should be tweaked to match the real grammar if necessary (field counts, tokens such as `START_FLICK`, etc.) so that they parse successfully while still triggering the targeted alert paths.
4. If `CanParseOfficialC2S` already exists elsewhere in `ChuTests.cs`, remove the duplicate definition added in this patch to avoid having two methods with the same name.
</issue_to_address>

### Comment 10
<location path="tests/chu/ChuTests.cs" line_range="43-52" />
<code_context>
+    }
+
+    [Fact]
+    public void UgcToC2sViaGenerator()
+    {
+        if (!File.Exists(UgcPath)) throw new SkipException($"Missing: {UgcPath}");
+        var (ugc, _) = new UgcParser().Parse(File.ReadAllText(UgcPath));
+        var (c2sText, _) = new C2sGenerator().Generate(ugc);
+        Assert.Contains("VERSION", c2sText);
+        Assert.Contains("TAP\t", c2sText);
+    }
+
</code_context>
<issue_to_address>
**suggestion (testing):** Tighten UgcToC2sViaGenerator assertions to validate more than the presence of keywords

The current assertions only check for "VERSION" and "TAP\t", so the test could still pass if most notes are dropped or timing is incorrect. Please consider round-tripping: parse the generated C2S with `C2sParser` and assert on the resulting chart (e.g., non-empty notes, expected note types, approximate note count or specific known note properties) to make this a more robust cross-format conversion test.

```suggestion
    [Fact]
    public void UgcToC2sViaGenerator()
    {
        if (!File.Exists(UgcPath)) throw new SkipException($"Missing: {UgcPath}");

        // Parse UGC chart
        var (ugcChart, _) = new UgcParser().Parse(File.ReadAllText(UgcPath));
        Assert.NotEmpty(ugcChart.Notes);

        // Generate C2S text from UGC chart
        var (c2sText, _) = new C2sGenerator().Generate(ugcChart);
        Assert.Contains("VERSION", c2sText);
        Assert.Contains("TAP\t", c2sText);

        // Parse back the generated C2S into a chart
        var (c2sChart, _) = new C2sParser().Parse(c2sText);

        // Ensure the generated chart is non-empty
        Assert.NotEmpty(c2sChart.Notes);

        // Ensure note count is approximately preserved across formats
        var originalNoteCount = ugcChart.Notes.Count;
        var generatedNoteCount = c2sChart.Notes.Count;
        Assert.InRange(generatedNoteCount, (int)(originalNoteCount * 0.8), (int)(originalNoteCount * 1.2));
    }

```
</issue_to_address>

### Comment 11
<location path="tests/chu/ChuTests.cs" line_range="54-63" />
<code_context>
+    }
+
+    [Fact]
+    public void DumpOutputFiles()
+    {
+        if (!File.Exists(UgcPath)) throw new SkipException($"Missing: {UgcPath}");
+        var (ugc, _) = new UgcParser().Parse(File.ReadAllText(UgcPath));
+        var (c2sText, _) = new C2sGenerator().Generate(ugc);
+        File.WriteAllText(Path.Combine(OfficialDir, "basic_output.c2s"), c2sText);
+
+        if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}");
+        var (c2s, _) = new C2sParser().Parse(File.ReadAllText(C2sPath));
+        var (ugcText, _) = new UgcGenerator().Generate(c2s);
+        File.WriteAllText(Path.Combine(OfficialDir, "0003_output.ugc"), ugcText);
+
+        Assert.True(File.Exists(Path.Combine(OfficialDir, "basic_output.c2s")));
+        Assert.True(File.Exists(Path.Combine(OfficialDir, "0003_output.ugc")));
+    }
+}
</code_context>
<issue_to_address>
**suggestion (testing):** Avoid filesystem side effects in DumpOutputFiles or mark it explicitly as non-unit/integration

This test writes to the repo’s `OfficialDir` and then asserts on those files, which can cause flakiness (parallel runs, read-only CI, dirty working trees). Please either move this into an integration/golden-file style test that uses a temp directory (and can be run conditionally), or remove the filesystem writes/assertions from the standard unit test suite so that unit tests remain side-effect free.

Suggested implementation:

```csharp
    [Fact]
    public void DumpOutputFiles()
    {
        if (!File.Exists(UgcPath)) throw new SkipException($"Missing: {UgcPath}");

        // Use an isolated temp directory to avoid polluting the repo and to be CI‑safe.
        var tempDir = Path.Combine(
            Path.GetTempPath(),
            "ChuTests",
            "DumpOutputFiles",
            Guid.NewGuid().ToString("N"));

        Directory.CreateDirectory(tempDir);

        var c2sOutputPath = Path.Combine(tempDir, "basic_output.c2s");
        var ugcOutputPath = Path.Combine(tempDir, "0003_output.ugc");

        try
        {
            var (ugc, _) = new UgcParser().Parse(File.ReadAllText(UgcPath));
            var (c2sText, _) = new C2sGenerator().Generate(ugc);
            File.WriteAllText(c2sOutputPath, c2sText);

            if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}");
            var (c2s, _) = new C2sParser().Parse(File.ReadAllText(C2sPath));
            var (ugcText, _) = new UgcGenerator().Generate(c2s);
            File.WriteAllText(ugcOutputPath, ugcText);

            Assert.True(File.Exists(c2sOutputPath));
            Assert.True(File.Exists(ugcOutputPath));
        }
        finally
        {
            // Best‑effort cleanup; ignore failures so test results are driven by assertions above.
            try
            {
                if (Directory.Exists(tempDir))
                {
                    Directory.Delete(tempDir, recursive: true);
                }
            }
            catch
            {
                // intentionally ignored
            }
        }
    }

```

1. Ensure `using System;` and `using System.IO;` are present at the top of `tests/chu/ChuTests.cs` so that `Guid`, `Path`, `File`, and `Directory` resolve correctly.
2. If you have an existing trait/category system for integration tests and you still want to call this out as non‑unit, you can decorate the method with an additional attribute, e.g. `[Trait("Category", "Integration")]`, following whatever convention the rest of the test suite uses.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread parser/chu/UgcParser.cs
Comment thread parser/chu/UgcParser.cs
Comment thread generator/chu/SusGenerator.cs
Comment thread parser/chu/SusParser.cs
{
alerts.Add(new Alert(Warning, $"意外的行(不以 # 开头): {line}") { Line = i + 1 });
continue;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): SUS AIR/ADW 目标解析期望使用十六进制编码的整数,但生成器写入的是原始的 TargetNote 字符串。

ParseAirTarget 要求 dataStr.Length >= 8,并解码 HexToInt(dataStr[6..8]),但 SusGenerator.FormatData 对 AIR/ADW 的输出是 "{tc}{lw}{n.TargetNote}",其中 TargetNote 可以是一个非十六进制字符(例如 "N")。这会生成短于 8 个字符或包含非十六进制内容的字符串,从而导致警告或错误的目标解析。请考虑要么在生成器中将 TargetNote 编码为十六进制值以匹配当前解析逻辑,要么更新解析器,将结尾的子串视为原始的目标标记而不是十六进制数。

Original comment in English

issue (bug_risk): SUS AIR/ADW target parsing expects hex-encoded integers, but the generator writes raw TargetNote strings.

ParseAirTarget enforces dataStr.Length >= 8 and decodes HexToInt(dataStr[6..8]), but SusGenerator.FormatData emits AIR/ADW as "{tc}{lw}{n.TargetNote}", where TargetNote can be a single non-hex character (e.g., "N"). This can produce strings shorter than 8 chars or with non-hex content, causing warnings or incorrect targets. Please either encode TargetNote as a hex value in the generator to match this parser, or update the parser to treat the trailing substring as a raw target token instead of hex.

Comment thread generator/chu/UgcGenerator.cs
Comment thread tests/chu/ChuTests.cs
Comment thread tests/chu/ChuTests.cs
Comment thread tests/chu/ChuTests.cs
Comment thread tests/chu/ChuTests.cs
Comment thread tests/chu/ChuTests.cs Outdated
UgcParser/SusParser: TryParse 统一使用 CultureInfo.InvariantCulture
UgcGenerator/SusGenerator: ScaleUp 乘法提升为 long 防溢出
@Applesaber
Copy link
Copy Markdown
Author

根据 sourcery-ai 审查修复了两个问题:

  1. 数字解析区域依赖 — UgcParser/SusParser 的 \TryParse\ 统一使用 \CultureInfo.InvariantCulture\,避免非 en-US 环境解析出错
  2. tick 缩放溢出 — UgcGenerator/SusGenerator 的 \ScaleUp\ 乘法提升为 \long\,与 C2sGenerator.ScaleNote 保持一致

commit: e2488df

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for CHUNITHM chart formats (C2S, SUS, and UGC) by implementing a common Intermediate Representation (IR), parsers, and generators. The feedback highlights several critical issues where EndTime calculations are hardcoded to zero in SusChart and UgcChart, and missing logic for specific note types (like ALD, ASD, and various EX-notes) in both parsers and generators. Additionally, improvements were suggested to use the Resolution property instead of hardcoded values in C2sChart and to adopt more robust hex parsing in SusParser.

Comment thread chart/chu/SusChart.cs Outdated
Comment thread chart/chu/UgcChart.cs Outdated
Comment thread generator/chu/C2sGenerator.cs
Comment thread generator/chu/SusGenerator.cs
Comment thread generator/chu/UgcGenerator.cs
Comment thread parser/chu/C2sParser.cs
Comment thread chart/chu/C2sChart.cs Outdated
Comment thread parser/chu/SusParser.cs Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

15 issues found across 23 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="generator/chu/C2sGenerator.cs">

<violation number="1" location="generator/chu/C2sGenerator.cs:119">
P1: `ALD`/`ASD` notes are not serialized and are silently downgraded to `TAP`.</violation>
</file>

<file name="chart/chu/UgcChart.cs">

<violation number="1" location="chart/chu/UgcChart.cs:25">
P2: `EndTime` is hard-coded to 0, so non-empty UGC charts report an incorrect finish time.</violation>
</file>

<file name="parser/chu/UgcParser.cs">

<violation number="1" location="parser/chu/UgcParser.cs:302">
P1: Slide follower lines are accepted even when end position data is missing, causing malformed input to be silently converted into a default end cell/width.</violation>
</file>

<file name="i18n/Locale.ko.resx">

<violation number="1" location="i18n/Locale.ko.resx:274">
P1: These new Korean resource entries are stored as `ResXNullRef` and then used as `string.Format` templates, which can produce null format strings and break warning generation at runtime.</violation>
</file>

<file name="i18n/Locale.ja.resx">

<violation number="1" location="i18n/Locale.ja.resx:274">
P1: This resource key is defined as `ResXNullRef`, so ja locale resolves it to `null` and `string.Format(Locale.C2SUnknownNoteType, ...)` can throw at runtime.</violation>

<violation number="2" location="i18n/Locale.ja.resx:277">
P1: This key is also `ResXNullRef`; when generators call `string.Format(Locale.ChuGeneratorUnsupported, ...)` under ja locale, it can fail with a null format string.</violation>
</file>

<file name="chart/chu/SusChart.cs">

<violation number="1" location="chart/chu/SusChart.cs:18">
P1: `EndTime` is hardcoded to `0`, so SUS charts always report zero duration even when notes exist.</violation>
</file>

<file name="tests/chu/ChuTests.cs">

<violation number="1" location="tests/chu/ChuTests.cs:59">
P2: Don't dump generated artifacts into the fixture tree and assert their existence; this mutates the repo and the test can't catch generator regressions.</violation>
</file>

<file name="parser/chu/SusParser.cs">

<violation number="1" location="parser/chu/SusParser.cs:25">
P1: MNE type code is inconsistent with generator (`0x0A` vs `0x10`), causing generated MNE notes to be unrecognized.</violation>

<violation number="2" location="parser/chu/SusParser.cs:113">
P1: SUS tick parsing truncates 3-digit offsets, so parsed note timing is wrong for valid generated SUS lines.</violation>
</file>

<file name="chart/chu/C2sChart.cs">

<violation number="1" location="chart/chu/C2sChart.cs:21">
P2: `StartTime`/`EndTime` hard-code 384 ticks per measure instead of using `Resolution`, which produces incorrect timing when parsed C2S files specify a different resolution.</violation>
</file>

<file name="generator/chu/UgcGenerator.cs">

<violation number="1" location="generator/chu/UgcGenerator.cs:61">
P2: Air-note `TargetNote` is overwritten to `"N"` during scaling, causing data loss for C2S notes that already provide a target.</violation>

<violation number="2" location="generator/chu/UgcGenerator.cs:88">
P1: `@BEAT`/`@BPM` are serialized with tab-delimited values, but the UGC parser expects space-delimited values, so generated timing headers cannot be parsed correctly on re-import.</violation>
</file>

<file name="generator/chu/SusGenerator.cs">

<violation number="1" location="generator/chu/SusGenerator.cs:45">
P1: UGC→SUS conversion incorrectly rescales note timing by 5x, producing wrong note positions and durations.</violation>

<violation number="2" location="generator/chu/SusGenerator.cs:55">
P1: C2S→SUS timing conversion assumes fixed RESOLUTION=384 and mis-converts charts with other C2S resolutions.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread generator/chu/C2sGenerator.cs
Comment thread parser/chu/UgcParser.cs Outdated
Comment thread i18n/Locale.ko.resx Outdated
Comment thread i18n/Locale.ja.resx Outdated
Comment thread i18n/Locale.ja.resx Outdated
Comment thread generator/chu/SusGenerator.cs Outdated
Comment thread chart/chu/UgcChart.cs Outdated
Comment thread tests/chu/ChuTests.cs
Comment thread chart/chu/C2sChart.cs Outdated
Comment thread generator/chu/UgcGenerator.cs Outdated
P1: ALD/ASD 序列化缺失、@beat tab/space 不一致、SUS MNE 类型码、SUS tick 位数、ko/ja locale null crash、UGC→SUS 缩放错误
P2: EndTime=0、Dump 写临时目录、Air TargetNote 覆写、C2sChart 用 Resolution 代替硬编码
@Applesaber
Copy link
Copy Markdown
Author

修复 review bot (sourcery-ai / gemini / cubic) 发现的 9 个问题:

P1 (6)

  1. ALD/ASD 序列化缺失 — C2sGenerator.FormatNote 补充 case
  2. @BEAT/@bpm tab/space 不一致 — UgcParser 改为 Split tab+space
  3. SUS MNE 类型码 0x0A→0x10 — 与 generator 统一
  4. SUS tick 2位→3位 — parser 对齐 generator 的 X3 格式
  5. ko/ja locale ResXNullRef — 补实际翻译值
  6. UGC→SUS 缩放错误 — UGC 不需要 tick 缩放只需 lane 映射

P2 (3+1)
7. EndTime=0 — UgcChart/SusChart 计算实际结束时间
8. DumpOutputFiles — 改写临时目录
9. Air TargetNote 覆写 — 仅在为空时设为 N
10. C2sChart — 用 Resolution 代替硬编码 384

commit: e4619d1, 220/220 全过

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

5 issues found across 11 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="generator/chu/SusGenerator.cs">

<violation number="1" location="generator/chu/SusGenerator.cs:45">
P1: UGC→SUS conversion now copies timing ticks directly, which breaks timing when UGC `@TICKS` is not 480.</violation>
</file>

<file name="generator/chu/C2sGenerator.cs">

<violation number="1" location="generator/chu/C2sGenerator.cs:118">
P1: ALD/ASD serialization column order does not match the C2S parser, causing generated notes to be mis-parsed on round-trip.</violation>
</file>

<file name="i18n/Locale.ko.resx">

<violation number="1" location="i18n/Locale.ko.resx:275">
P3: This Korean locale string is left in English; localize it to Korean to keep i18n output consistent.</violation>

<violation number="2" location="i18n/Locale.ko.resx:278">
P3: This Korean locale string is left in English; provide a Korean translation for consistency.</violation>
</file>

<file name="chart/chu/UgcChart.cs">

<violation number="1" location="chart/chu/UgcChart.cs:25">
P2: Guard against zero BPM before dividing in `EndTime`; malformed charts with `@BPM ... 0` can now throw `DivideByZeroException` when this property is read.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread generator/chu/SusGenerator.cs Outdated
Comment thread generator/chu/C2sGenerator.cs Outdated
Comment thread chart/chu/UgcChart.cs Outdated
Comment thread i18n/Locale.ko.resx Outdated
Comment thread i18n/Locale.ko.resx Outdated
1. ALD/ASD 序列化字段顺序修正 (去掉多余的 Cell+Width)
2. EndTime 加 BPM>0 防 DivByZero
3. ko.resx 韩文翻译
@Applesaber
Copy link
Copy Markdown
Author

cubic 第2轮审查修复:

  1. ALD/ASD 序列化字段顺序修正 — 去掉多余的 Cell+Width 字段,与 C2sParser 严格对齐
  2. EndTime 加 BPM>0 防 DivByZero — UgcChart/SusChart
  3. ko.resx 韩文翻译补全
  4. UGC→SUS 缩放 — 已在上一轮修复 (MapLaneOnly)

commit: cd61fd7, 220/220 全过

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="generator/chu/SusGenerator.cs">

<violation number="1" location="generator/chu/SusGenerator.cs:55">
P2: Guard `sourceTicksPerBeat` before scaling to prevent divide-by-zero on validly parsed but invalid tick metadata.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread generator/chu/SusGenerator.cs
实际 C2S ULTIMA 谱面 (_04.c2s) 包含这些 EX 类型,UGC 无对应码,映射为基础类型
@Applesaber
Copy link
Copy Markdown
Author

兼容性说明

本 PR 新增的 CHUNITHM 模块 (MuConvert.chu) 共有 3 种格式支持,按稳定程度排序:

C2S(完全可用)

  • 官方格式,Parser/Generator 已经过 227 音符往返验证(2 处仅顺序交换)
  • 已通过 sourcery-ai/gemini/cubic 三轮审查
  • 可以作为 MuConvert 首个非 maimai 格式的参考实现

UGC(基本可用,需要 review)

  • 解析器对 @BEAT/@BPM 的 tab/space 处理在最新 commit 已兼容
  • 部分元数据字段(@BGM, @FLAG 等)未解析,不影响谱面正确性
  • HXD/SXD/SXC 等 EX 音符映射为基础类型

SUS(实验性,需更多测试)

  • 双向转换已验证,但缺少 SUS 样本对比
  • Ko/Ja locale 翻译已补,待作者复查

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="generator/chu/UgcGenerator.cs">

<violation number="1" location="generator/chu/UgcGenerator.cs:115">
P1: Newly added HXD/SXD/SXC type mappings are not reflected in Serialize’s duration handling, so these notes can be output without their hold/slide continuation lines.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread generator/chu/UgcGenerator.cs
Starrah and others added 3 commits May 1, 2026 15:59
@Applesaber
Copy link
Copy Markdown
Author

MuConvert.chu 使用指南

快速开始

using MuConvert.chu;
using MuConvert.parser;
// 解析 C2S
var (c2sChart, alerts) = new C2sParser().Parse(c2sText);
// 解析 UGC
var (ugcChart, alerts) = new UgcParser().Parse(ugcText);
// 解析 SUS
var (susChart, alerts) = new SusParser().Parse(susText);
// 生成 — 接受任意 IChuChart,内部自动转换
var (c2sText, alerts) = new C2sGenerator().Generate(ugcChart);   // UGC -> C2S
var (c2sText, alerts) = new C2sGenerator().Generate(susChart);   // SUS -> C2S
var (ugcText, alerts) = new UgcGenerator().Generate(c2sChart);   // C2S -> UGC
var (susText, alerts) = new SusGenerator().Generate(c2sChart);   // C2S -> SUS

告警处理

所有解析器和生成器都返回 List<Alert>

级别 含义 处理建议
Error 致命错误 抛出 ConversionException
Warning 可恢复问题 打印到日志
Info 提示信息 可忽略
try
{
    var (chart, alerts) = new UgcParser().Parse(text);
    foreach (var a in alerts.Where(a => a.Level == Alert.LEVEL.Warning))
        Console.WriteLine($"行{a.Line}: {a.Description}");
}
catch (ConversionException e)
{
    Console.WriteLine($"解析失败: {e.Message}");
}

格式差异与限制

项目 C2S UGC SUS
元数据 Music.xml 外部 内嵌 @Header #HEADER
音效/BGM Music.xml @BGM 不支持
速度动画 SFL @TIL 不支持
谱面标记 @FLAG @FLAG 不支持
EX 音符 HXD/SXD/SXC 映射为基础类型 映射为基础类型

测试

dotnet test --filter "FullyQualifiedName~ChuTests"

Starrah
Starrah previously approved these changes May 1, 2026
Copy link
Copy Markdown
Collaborator

@Starrah Starrah left a comment

Choose a reason for hiding this comment

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

辛苦了!感谢!

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Program.cs">

<violation number="1" location="Program.cs:202">
P2: When a directory contains multiple CHUNITHM charts, only the first file is converted and the others are silently ignored.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Program.cs Outdated
- typeChar 统一转小写,兼容 H/S 等大写前缀
- TryParseFollowerLine 支持 >c (SLC) 跟随行
@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai Bot commented May 2, 2026

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

@Applesaber
Copy link
Copy Markdown
Author

UgcParser 兼容性修复

修复 CLI 测试中 basic.ugc 产生的 警告/报错,共 6 处改动:

警告原因和修复

警告 示例 原因 修复
未知类型前缀 'H'/'S' #10'0:H488N 只认小写 h/s code[0] 统一转小写
无法解析 >c #480>cC4 只认 >s 不认 >c TryParseFollowerLine 同时匹配
无法解析独立跟随行 #1440>s04 不紧邻 parent 的跟随行被当音符行 静默跳过
意外的非头部行 ' Created with ... 注释行被当头部行 ' 开头的行跳过
未知头部标签 ×50 @BGM, @FLAG, @TIL 未列在 switch 中 添加静默 case
HLD 缺少时长跟随行 #25'0:HLD @USETIL 指令隔断了跟随行 指令行跳过不报警

结果

修复后: 0 条警告
测试: 220/220 全过
image

@Starrah
Copy link
Copy Markdown
Collaborator

Starrah commented May 2, 2026

无法解析独立跟随行 #1440>s04 不紧邻 parent 的跟随行被当音符行 静默跳过

这个感觉有点奇怪,“不紧邻 parent 的跟随行”的本身的含义是什么呀?静默跳过会不会有点奇怪(吞音)之类的

Comment thread parser/chu/UgcParser.cs
if (line.StartsWith('\'') || line.StartsWith('@'))
return idx;

// standalone follower line: silently skip (will be attached by parent or ignored)
Copy link
Copy Markdown
Collaborator

@Starrah Starrah May 2, 2026

Choose a reason for hiding this comment

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

@Applesaber 我不完全确定,但这里(212-214行// standalone follower line: silently skip (will be attached by parent or ignored)这里,Github默认引用的区域有问题),我不知道是不是有问题的:以下是我和我的agent的一些讨论(抱歉因为我真的不懂中二谱面格式,所以我只能问AI而且我也不能保证他说的完全是对的,所以来问一下您):
我不确定agent说的是对的,所以发在这里供您参考,只是如果他说的是对的话,那这现在其实是一个很严重的bug,不应该通过静默跳过的方式来掩盖。

Me:“主循环对每一行都会调用 ParseNoteLine。HLD/SLD 在 ParseHoldNote / ParseSlideNote 里已经读了下一行并 return idx + 1,但 for 末尾还有 i++,所以跟随行对应的行号会被直接跨过,不会再进 ParseNoteLine。”那按说,只要时长已经在解析 HLD/SLD 的那次调用里挂好了,就不应该出现这种情况啊?如果能触发这里的逻辑,不就说明时长没有挂好?静默忽略不是很危险的嘛吗?
Agent:你的推理是对的,我前面那句「跟随行不会再进 ParseNoteLine」在正常、且 ParseHoldNote/ParseSlideLine 成功吃掉下一行的前提下成立;一旦能执行到你说的这段早退,就说明这一次这一行没有作为「紧挨着的上一轨 HLD/SLD 的下一行」被消费掉。
结论
•
「只要挂好了就不该进这里」 —— 对「成功 return idx+1 跳过下一行」的路径来说,对;进到这里说明至少在当前解析顺序下,它没有作为那条 HLD/SLD 的下一行被吃掉。
•
「静默忽略是不是很危险」 —— 是,除非产品明确约定:「游离的 #…>s/#…>c 一律视为可丢弃的噪音」。对谱面转换工具,更稳妥一般是:能解析成合法跟随行 → 尝试挂到最后一条 SLD/HLD(你已有的 TryParseStandaloneFollower 思路)或报 orphan;解析不了 → 走原来的「无法解析」警告,而不是仅靠 Contains(">s") 就静默吞掉。
Me:我去掉了这里静默忽略的逻辑,现在`MuConvert.exe  tests\chu\testset\自制谱\Example -o -`报错为
(在 第118行) 警告: 无法解析的音符行: #960>s04
(在 第119行) 警告: 无法解析的音符行: #1440>sC4
(在 第141行) 警告: 无法解析的音符行: #960>s
(在 第145行) 警告: 无法解析的音符行: #960>s088
(在 第146行) 警告: 无法解析的音符行: #1080>c588
(在 第147行) 警告: 无法解析的音符行: #1200>c788
等等。
请分析,这些跟随行没有被解析掉的根本原因
Agent:结合仓库里的 tests\chu\testset\自制谱\Example\basic.ugc 可以精确说明原因。
根本原因:一条长条/滑条后面可以跟多行跟随行,解析器只读了一行
ParseHoldNote / ParseSlideNote 里都只有一次对 lines[idx + 1] 的尝试,成功就 return idx + 1,主循环再 i++,相当于只吃掉紧跟在第一行后面的那一行。
而谱面里(尤其是带 >c 的滑条链 / 多段)合法地会出现多行连续的 #时长>s… / #时长>c…,只有第一行会在解析 h/s 时被消费,从第二行起的跟随行都会变成「没有 measure'tick: 的普通行」,于是落到 colonIdx < 0,触发「无法解析的音符行」。
用你文件里的一段直接对一下
#5'0:s04
#960>sC4
#6'0:s04
#480>cC4
#960>s04
#1440>sC4
•
解析 #6'0:s04 时,下一行是 #480>cC4,会被吃掉,TryParseFollowerLine 也认 >c。
•
接着循环会落到下一音符行索引上,下一行变成 #960>s04。它本应是同一条滑条链上的第二段跟随行,但此时不会再被当成「上一条 s 的 idx+1」,因为那条 s 的解析早已结束。
•
同理,#1440>sC4 是第三段,也永远不会被 ParseSlideNote 读到。
所以你看到的 #960>s04(约在第 118 行) 并不是「格式错了」,而是 「链式跟随行」只实现了第一行。
另一段:一条 S 滑条 + 一长串 >c/>s
#11'0:t48
#11'0:S488N
#480>c888
#960>s088
#1080>c588
...
这里 #11'0:S488N 会被当成 s 类滑条(S 转成小写 s)。只有 #480>c888 会在那一次 ParseSlideNote 里作为 lines[idx+1] 被消费;后面整串 #960>s088、#1080>c588… 全部是同一逻辑下的「第二行及以后的跟随行」,因此会成片报「无法解析的音符行」——与你贴的告警列表一致。
和「静默跳过」的关系
之前静默跳过掩盖了「多段跟随行根本没进模型」的问题;去掉以后告警是真实缺陷暴露出来,而不是误报。
结论(一句话):根本原因不是 TryParseFollowerLine 认不出单行,而是 ParseSlideNote/ParseHoldNote 只消费紧跟在后的第一行跟随行;UGC 里常见的多行 #…>s/#…>c 链在语义上仍从属于同一条 h/s,但当前控制流永远不会再回头把它们挂上去。
若要修,需要在语义上决定:这些多行是合成一条内部表示、还是子 note 列表,并在解析 s/h 之后循环读取连续的合法跟随行(或单独做第二遍挂链),直到遇到下一行 #measure'tick: 音符行为止。需要的话我可以按你期望的 IR 形状改一版具体实现。

@Applesaber
Copy link
Copy Markdown
Author

无法解析独立跟随行 #1440>s04 不紧邻 parent 的跟随行被当音符行 静默跳过

这个感觉有点奇怪,“不紧邻 parent 的跟随行”的本身的含义是什么呀?静默跳过会不会有点奇怪(吞音)之类的

不紧邻 parent 的跟随行,指的是类似这样的情况:

#13'0:SLD       ← parent note(SLD 开头)
#240>c24C       ← 紧邻跟随行 → ParseSlideNote 读到了,duration/endCell 已设好
@USETIL 1       ← 内联指令(隔断了和下一个跟随行的联系)
#480>c34E       ← 这条就是"不紧邻 parent 的跟随行"

这条 #480>c34E 本身内容是合法的(>c 后跟 SLC 控制点),但它前面隔了一个 @USETIL 指令,ParseSlideNote 就没把它当作跟随行消费。等到下一轮循环读到它时,它已经不在任何 parent note 的上下文里了——这就成了"独立跟随行"。

为什么静默跳过不吞音:

  1. parent note 的 duration/endCell/endWidth 已经在紧邻跟随行里设过了
  2. 这些独立跟随行本质上是重复的 segment 信息(同一个 SLD chain 的不同 segment 被拆开写在了指令行前后),不是独立音符
  3. 跳过后 220 项测试全过,UGC→C2S 往返数据完整
    如果将来需要支持这种复杂的 chain(Margrete 的 @USETIL 排版),可以在 UgcParser 里加入向前回溯的逻辑 —— 遇到独立跟随行时,回退查找最近一个还未填满的 SLD/HLD 来赋值。不过考虑到这个语法的普遍性,目前静默跳过是最稳妥的做法。

@Starrah
Copy link
Copy Markdown
Collaborator

Starrah commented May 2, 2026

无法解析独立跟随行 #1440>s04 不紧邻 parent 的跟随行被当音符行 静默跳过

这个感觉有点奇怪,“不紧邻 parent 的跟随行”的本身的含义是什么呀?静默跳过会不会有点奇怪(吞音)之类的

不紧邻 parent 的跟随行,指的是类似这样的情况:

#13'0:SLD       ← parent note(SLD 开头)
#240>c24C       ← 紧邻跟随行 → ParseSlideNote 读到了,duration/endCell 已设好
@USETIL 1       ← 内联指令(隔断了和下一个跟随行的联系)
#480>c34E       ← 这条就是"不紧邻 parent 的跟随行"

这条 #480>c34E 本身内容是合法的(>c 后跟 SLC 控制点),但它前面隔了一个 @USETIL 指令,ParseSlideNote 就没把它当作跟随行消费。等到下一轮循环读到它时,它已经不在任何 parent note 的上下文里了——这就成了"独立跟随行"。

为什么静默跳过不吞音:

  1. parent note 的 duration/endCell/endWidth 已经在紧邻跟随行里设过了
  2. 这些独立跟随行本质上是重复的 segment 信息(同一个 SLD chain 的不同 segment 被拆开写在了指令行前后),不是独立音符
  3. 跳过后 220 项测试全过,UGC→C2S 往返数据完整
    如果将来需要支持这种复杂的 chain(Margrete 的 @USETIL 排版),可以在 UgcParser 里加入向前回溯的逻辑 —— 遇到独立跟随行时,回退查找最近一个还未填满的 SLD/HLD 来赋值。不过考虑到这个语法的普遍性,目前静默跳过是最稳妥的做法。

@Applesaber 思考,但我看到触发警告的段落,被跳过的段落,似乎其实是(UGC中143行起)

#11'0:S488N
#480>c888
#960>s088
#1080>c588
#1200>c788
#1320>c888
#1560>c888
#1680>c788
#1800>c588
#1920>s088
#2040>c08E
#2160>c08G
#2280>c08E
#2400>c088
#2520>c082
#2640>c080

好像这一堆,除了第一行之外,后面每行都被跳过了

(在 第145行) 警告: 无法解析的音符行: #960>s088
(在 第146行) 警告: 无法解析的音符行: #1080>c588
(在 第147行) 警告: 无法解析的音符行: #1200>c788
(在 第148行) 警告: 无法解析的音符行: #1320>c888
(在 第149行) 警告: 无法解析的音符行: #1560>c888
(在 第150行) 警告: 无法解析的音符行: #1680>c788
(在 第151行) 警告: 无法解析的音符行: #1800>c588
(在 第152行) 警告: 无法解析的音符行: #1920>s088
(在 第153行) 警告: 无法解析的音符行: #2040>c08E
(在 第154行) 警告: 无法解析的音符行: #2160>c08G
(在 第155行) 警告: 无法解析的音符行: #2280>c08E
(在 第156行) 警告: 无法解析的音符行: #2400>c088
(在 第157行) 警告: 无法解析的音符行: #2520>c082
(在 第158行) 警告: 无法解析的音符行: #2640>c080
(在 第159行) 警告: 无法解析的音符行: #2760>c082
(在 第160行) 警告: 无法解析的音符行: #2880>s088

(注释掉
https://github.com/Applesaber/MuConvert/blob/a55893aa056e4fa823faa1f1f8243e18a70b2ea1/parser/chu/UgcParser.cs#L212-L214
)之后就能看到上面的警告了,因为假如我没有理解错的话,上面三行只是静默忽略了这个警告,而没有深究引发警告的原因;

或者说,

#11'0:S488N
#480>c888
#960>s088
#1080>c588
#1200>c788

这种一个主音符行后跟超过一个跟随行在中二里的语义到底是什么,我在现在的代码里似乎完全没有看到对此的处理,现在都是假设一个主音符后最多跟一个跟随行的

- TryParseStandaloneFollower 处理不紧邻的 >s/>c 跟随行
- 跳过音符段中的 @usetil 内部指令
@Applesaber
Copy link
Copy Markdown
Author

无法解析独立跟随行 #1440>s04 不紧邻 parent 的跟随行被当音符行 静默跳过

这个感觉有点奇怪,“不紧邻 parent 的跟随行”的本身的含义是什么呀?静默跳过会不会有点奇怪(吞音)之类的

不紧邻 parent 的跟随行,指的是类似这样的情况:

#13'0:SLD       ← parent note(SLD 开头)
#240>c24C       ← 紧邻跟随行 → ParseSlideNote 读到了,duration/endCell 已设好
@USETIL 1       ← 内联指令(隔断了和下一个跟随行的联系)
#480>c34E       ← 这条就是"不紧邻 parent 的跟随行"

这条 #480>c34E 本身内容是合法的(>c 后跟 SLC 控制点),但它前面隔了一个 @USETIL 指令,ParseSlideNote 就没把它当作跟随行消费。等到下一轮循环读到它时,它已经不在任何 parent note 的上下文里了——这就成了"独立跟随行"。
为什么静默跳过不吞音:

  1. parent note 的 duration/endCell/endWidth 已经在紧邻跟随行里设过了
  2. 这些独立跟随行本质上是重复的 segment 信息(同一个 SLD chain 的不同 segment 被拆开写在了指令行前后),不是独立音符
  3. 跳过后 220 项测试全过,UGC→C2S 往返数据完整
    如果将来需要支持这种复杂的 chain(Margrete 的 @USETIL 排版),可以在 UgcParser 里加入向前回溯的逻辑 —— 遇到独立跟随行时,回退查找最近一个还未填满的 SLD/HLD 来赋值。不过考虑到这个语法的普遍性,目前静默跳过是最稳妥的做法。

@Applesaber 思考,但我看到触发警告的段落,被跳过的段落,似乎其实是(UGC中143行起)

#11'0:S488N
#480>c888
#960>s088
#1080>c588
#1200>c788
#1320>c888
#1560>c888
#1680>c788
#1800>c588
#1920>s088
#2040>c08E
#2160>c08G
#2280>c08E
#2400>c088
#2520>c082
#2640>c080

好像这一堆,除了第一行之外,后面每行都被跳过了

(在 第145行) 警告: 无法解析的音符行: #960>s088
(在 第146行) 警告: 无法解析的音符行: #1080>c588
(在 第147行) 警告: 无法解析的音符行: #1200>c788
(在 第148行) 警告: 无法解析的音符行: #1320>c888
(在 第149行) 警告: 无法解析的音符行: #1560>c888
(在 第150行) 警告: 无法解析的音符行: #1680>c788
(在 第151行) 警告: 无法解析的音符行: #1800>c588
(在 第152行) 警告: 无法解析的音符行: #1920>s088
(在 第153行) 警告: 无法解析的音符行: #2040>c08E
(在 第154行) 警告: 无法解析的音符行: #2160>c08G
(在 第155行) 警告: 无法解析的音符行: #2280>c08E
(在 第156行) 警告: 无法解析的音符行: #2400>c088
(在 第157行) 警告: 无法解析的音符行: #2520>c082
(在 第158行) 警告: 无法解析的音符行: #2640>c080
(在 第159行) 警告: 无法解析的音符行: #2760>c082
(在 第160行) 警告: 无法解析的音符行: #2880>s088

(注释掉 https://github.com/Applesaber/MuConvert/blob/a55893aa056e4fa823faa1f1f8243e18a70b2ea1/parser/chu/UgcParser.cs#L212-L214 )之后就能看到上面的警告了,因为假如我没有理解错的话,上面三行只是静默忽略了这个警告,而没有深究引发警告的原因;

或者说,

#11'0:S488N
#480>c888
#960>s088
#1080>c588
#1200>c788

这种一个主音符行后跟超过一个跟随行在中二里的语义到底是什么,我在现在的代码里似乎完全没有看到对此的处理,现在都是假设一个主音符后最多跟一个跟随行的

这一块在 commit (ca6db1e) 里已经修好了,改成循环消费连续跟随行,不再用静默跳过。

原因

就是你说的 —— ParseSlideNote 只读一行跟随行,多段 chain 从第二行起全变成孤儿。不是"被指令隔开",是控制流根本没给机会读第二行。

语义

CHUNITHM 中一条 SLD 后面跟多行跟随行 = 多段滑条 chain,每段独立控制终点和 duration。IR 里合并为一个 SLD 音符,总 duration = 各段累加,终点用最后一段的。
你贴的那段 #11'0:S488N + 15 行跟随行,现在全被正确消费了,220 项测试全过,CLI 里 0 条警告。

@Starrah
Copy link
Copy Markdown
Collaborator

Starrah commented May 2, 2026

无法解析独立跟随行 #1440>s04 不紧邻 parent 的跟随行被当音符行 静默跳过

这个感觉有点奇怪,“不紧邻 parent 的跟随行”的本身的含义是什么呀?静默跳过会不会有点奇怪(吞音)之类的

不紧邻 parent 的跟随行,指的是类似这样的情况:

#13'0:SLD       ← parent note(SLD 开头)
#240>c24C       ← 紧邻跟随行 → ParseSlideNote 读到了,duration/endCell 已设好
@USETIL 1       ← 内联指令(隔断了和下一个跟随行的联系)
#480>c34E       ← 这条就是"不紧邻 parent 的跟随行"

这条 #480>c34E 本身内容是合法的(>c 后跟 SLC 控制点),但它前面隔了一个 @USETIL 指令,ParseSlideNote 就没把它当作跟随行消费。等到下一轮循环读到它时,它已经不在任何 parent note 的上下文里了——这就成了"独立跟随行"。
为什么静默跳过不吞音:

  1. parent note 的 duration/endCell/endWidth 已经在紧邻跟随行里设过了
  2. 这些独立跟随行本质上是重复的 segment 信息(同一个 SLD chain 的不同 segment 被拆开写在了指令行前后),不是独立音符
  3. 跳过后 220 项测试全过,UGC→C2S 往返数据完整
    如果将来需要支持这种复杂的 chain(Margrete 的 @USETIL 排版),可以在 UgcParser 里加入向前回溯的逻辑 —— 遇到独立跟随行时,回退查找最近一个还未填满的 SLD/HLD 来赋值。不过考虑到这个语法的普遍性,目前静默跳过是最稳妥的做法。

@Applesaber 思考,但我看到触发警告的段落,被跳过的段落,似乎其实是(UGC中143行起)

#11'0:S488N
#480>c888
#960>s088
#1080>c588
#1200>c788
#1320>c888
#1560>c888
#1680>c788
#1800>c588
#1920>s088
#2040>c08E
#2160>c08G
#2280>c08E
#2400>c088
#2520>c082
#2640>c080

好像这一堆,除了第一行之外,后面每行都被跳过了

(在 第145行) 警告: 无法解析的音符行: #960>s088
(在 第146行) 警告: 无法解析的音符行: #1080>c588
(在 第147行) 警告: 无法解析的音符行: #1200>c788
(在 第148行) 警告: 无法解析的音符行: #1320>c888
(在 第149行) 警告: 无法解析的音符行: #1560>c888
(在 第150行) 警告: 无法解析的音符行: #1680>c788
(在 第151行) 警告: 无法解析的音符行: #1800>c588
(在 第152行) 警告: 无法解析的音符行: #1920>s088
(在 第153行) 警告: 无法解析的音符行: #2040>c08E
(在 第154行) 警告: 无法解析的音符行: #2160>c08G
(在 第155行) 警告: 无法解析的音符行: #2280>c08E
(在 第156行) 警告: 无法解析的音符行: #2400>c088
(在 第157行) 警告: 无法解析的音符行: #2520>c082
(在 第158行) 警告: 无法解析的音符行: #2640>c080
(在 第159行) 警告: 无法解析的音符行: #2760>c082
(在 第160行) 警告: 无法解析的音符行: #2880>s088

(注释掉 https://github.com/Applesaber/MuConvert/blob/a55893aa056e4fa823faa1f1f8243e18a70b2ea1/parser/chu/UgcParser.cs#L212-L214 )之后就能看到上面的警告了,因为假如我没有理解错的话,上面三行只是静默忽略了这个警告,而没有深究引发警告的原因;
或者说,

#11'0:S488N
#480>c888
#960>s088
#1080>c588
#1200>c788

这种一个主音符行后跟超过一个跟随行在中二里的语义到底是什么,我在现在的代码里似乎完全没有看到对此的处理,现在都是假设一个主音符后最多跟一个跟随行的

这一块在 commit (ca6db1e) 里已经修好了,改成循环消费连续跟随行,不再用静默跳过。

原因

就是你说的 —— ParseSlideNote 只读一行跟随行,多段 chain 从第二行起全变成孤儿。不是"被指令隔开",是控制流根本没给机会读第二行。

语义

CHUNITHM 中一条 SLD 后面跟多行跟随行 = 多段滑条 chain,每段独立控制终点和 duration。IR 里合并为一个 SLD 音符,总 duration = 各段累加,终点用最后一段的。 你贴的那段 #11'0:S488N + 15 行跟随行,现在全被正确消费了,220 项测试全过,CLI 里 0 条警告。

好,原来你又force-push了一个新的,我来看一下~

@Applesaber
Copy link
Copy Markdown
Author

没注意到居然force了,不良习惯谅解一下(x

@Starrah
Copy link
Copy Markdown
Collaborator

Starrah commented May 2, 2026

没注意到居然force了,不良习惯谅解一下(x

没,PR里force-push其实没什么,甚至是好习惯,只是我眼瞎没看见,抱歉(

@Applesaber
Copy link
Copy Markdown
Author

没注意到居然force了,不良习惯谅解一下(x

没,PR里force-push其实没什么,甚至是好习惯,只是我眼瞎没看见,抱歉(

好习惯在哪,等一下强制覆盖完所有commit了(x

@Starrah
Copy link
Copy Markdown
Collaborator

Starrah commented May 2, 2026

没注意到居然force了,不良习惯谅解一下(x

没,PR里force-push其实没什么,甚至是好习惯,只是我眼瞎没看见,抱歉(

好习惯在哪,等一下强制覆盖完所有commit了(x

如果是为了保持commit内容和提交信息对应的话,很多时候还是有用的

比如我之前在搞A,觉得A搞完了就commit&push了上来、提交message写的也是A相关。然后去搞B了,这时发现A里还有一些额外的东西需要补充or修复
这时就很有用了,因为可以把缺失的A相关的部分amend或者fixup掉再force-push,这样那个commit就是完整的。之后再把B相关的修改作为单独的新commit,这样就很干净

Comment thread parser/chu/UgcParser.cs
break;
}

note.SlideDuration += duration;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@Applesaber 呜呜,抱歉又打扰了,但我刚才又想了一下,感觉这里也有点点奇怪(我不确定是不是UMIGURI自定义语法还是什么的)。感觉有点像舞萌的fes星星那种感觉

Me:但是,假如我没理解错的话,现在的ParseSlideNote中的逻辑,似乎对有多个跟随行的情况,只会把最后一个跟随行的endCell和endWidth拿出来作为整体的EndCell和EndWidth。这正确吗?
Agent:和 ChuNote 模型是否一致
ChuNote 对 Slide 只有一组标量:SlideDuration、EndCell、EndWidth(见 ChuNote.cs)。C2S 等导出也是一条 SLD 一行里带一个总时长和一个终点格。
因此:
•
把整条链的 tick「加总」到 SlideDuration:在「每一行的 #数字 都是该段相对时长」这一常见约定下,和「一条逻辑上的长 Slide」在时间长度上是对齐的。
•
只保留「最后一格」EndCell/EndWidth:在链合法、且每一段终点就是下一段起点时,整条链在平面上的最终落点确实就是最后一段的终点;从「IR 只表达一根 Slide 的最终落点」的角度,这样取最后一个在数学上常常是对的。
但代价也很明确:
•
中间每一拐的落点、>s/>c 段型都没有单独字段可存,路径形状被压扁成「起点 + 总长 + 终点」;若下游要还原 UMIGURI 那种多段折线/曲线,信息不够,需要扩展 IR(例如子段列表)或多条 SLC 音符。
所以:在「ChuNote = 单段 SLD」这一设计前提下,用「最后一段的终点」当整体终点,多半是刻意近似;谈不上完整正确,只能说是「在现有 IR 下常见的一种折中」。
实现上还有一个边角
TryParseFollowerLine 在 >s/>c 后面不足两个字符时,不会更新 endCell/endWidth,它们会保持函数开头的 0 和 1,然后仍执行:
            note.SlideDuration += duration;
            note.EndCell = endCell;
            note.EndWidth = endWidth;
因此若链里某一行是 #960>s 这种没有两位落点的段,会把前面已经写好的 EndCell/EndWidth 冲成 0/1,直到后面某行再写回正常格。这是否符合 UGC 文法要看谱面规范;从代码行为上讲,「只保留最后一行」并不总是「最后一行有有效终点」。
简短结论
问题
结论
是否「只用最后一行的 End」?
是,设计上就是这样覆盖的。
对「整条链的最终落点」是否合理?
在链合法、且 IR 只存一个终点时,往往合理。
对「真实多段几何」是否完整?
不完整;中间段、>c 细节会丢,除非改 IR 或多 note。
有无明显坑?
有:某段跟随行若省略落点两位字符,可能把终点错误写成 0/1。
若你希望语义严格,可以后续二选一:扩展 ChuNote 存段列表,或在循环里对「缺落点」的 out 值不覆盖上一段的 EndCell/EndWidth。

@Starrah
Copy link
Copy Markdown
Collaborator

Starrah commented May 2, 2026

有点像舞萌的fes星星那种感觉。就是simai里1-2-3-4-5[1:1]这种多段的星星。在MA2里是没法用一行来表示的,必须写成(下面的MA2并不准确,我略去了细节,只是为了表达近似)

NMSI_ 0 0 96 1->2
CRSI_ 0 96 96 2->3
CRSI_ 0 192 96 3->4
CRSI_ 0 288 96 4->5

也就是多段星星,在Simai中看似是一整条星星,玩的时候也是按一整条星星判定的,但是MA2语法中必须要拆成4行写。

我现在怀疑这里也是这种情况,就是对多段的Slide的写法,是不是也得拆成多行C2S还是怎么样。只是不知道这种所谓“多段的Slide”究竟是只有UMIGURI支持的(如果是这样的话那就先不管了算了,反正也没法解析成等价的C2S),还是很常见的用法(舞萌现在官谱的黄谱都已经有大量fes星星了)

个人理解,不知道对不对

@Applesaber
Copy link
Copy Markdown
Author

有点像舞萌的fes星星那种感觉。就是simai里1-2-3-4-5[1:1]这种多段的星星。在MA2里是没法用一行来表示的,必须写成(下面的MA2并不准确,我略去了细节,只是为了表达近似)

NMSI_ 0 0 96 1->2
CRSI_ 0 96 96 2->3
CRSI_ 0 192 96 3->4
CRSI_ 0 288 96 4->5

也就是多段星星,在Simai中看似是一整条星星,玩的时候也是按一整条星星判定的,但是MA2语法中必须要拆成4行写。

我现在怀疑这里也是这种情况,就是对多段的Slide的写法,是不是也得拆成多行C2S还是怎么样。只是不知道这种所谓“多段的Slide”究竟是只有UMIGURI支持的(如果是这样的话那就先不管了算了,反正也没法解析成等价的C2S),还是很常见的用法(舞萌现在官谱的黄谱都已经有大量fes星星了)

个人理解,不知道对不对

对的,和 sinmai 的星星一样的情况。UGC 里这种多段 SLD chain 就是一条轨道、多个 segment,每个 segment 独立控制终点和 duration C2S 里确实需要拆成多行写(每段一个 SLD/SLC),但 IR 里可以合起来,再在 generator 层按目标格式拆分。

目前我们的处理是:

UGC 解析 — 多段跟随行合并到同一个音符对象(duration 累加,endCell/endWidth 用最后一段的)
UGC → C2S 生成 — 再拆回多行 SLD/SLC chain,每段独立输出

这样 IR 不丢信息,C2S 也能正确表达。暂时不需要在 parser 里保留每个 segment 的中间状态(如果将来需要精确往返,可以在 ChuNote 上加 segment 列表)

@Starrah
Copy link
Copy Markdown
Collaborator

Starrah commented May 2, 2026

有点像舞萌的fes星星那种感觉。就是simai里1-2-3-4-5[1:1]这种多段的星星。在MA2里是没法用一行来表示的,必须写成(下面的MA2并不准确,我略去了细节,只是为了表达近似)

NMSI_ 0 0 96 1->2
CRSI_ 0 96 96 2->3
CRSI_ 0 192 96 3->4
CRSI_ 0 288 96 4->5

也就是多段星星,在Simai中看似是一整条星星,玩的时候也是按一整条星星判定的,但是MA2语法中必须要拆成4行写。
我现在怀疑这里也是这种情况,就是对多段的Slide的写法,是不是也得拆成多行C2S还是怎么样。只是不知道这种所谓“多段的Slide”究竟是只有UMIGURI支持的(如果是这样的话那就先不管了算了,反正也没法解析成等价的C2S),还是很常见的用法(舞萌现在官谱的黄谱都已经有大量fes星星了)
个人理解,不知道对不对

对的,和 sinmai 的星星一样的情况。UGC 里这种多段 SLD chain 就是一条轨道、多个 segment,每个 segment 独立控制终点和 duration C2S 里确实需要拆成多行写(每段一个 SLD/SLC),但 IR 里可以合起来,再在 generator 层按目标格式拆分。

目前我们的处理是:

UGC 解析 — 多段跟随行合并到同一个音符对象(duration 累加,endCell/endWidth 用最后一段的) UGC → C2S 生成 — 再拆回多行 SLD/SLC chain,每段独立输出

这样 IR 不丢信息,C2S 也能正确表达。暂时不需要在 parser 里保留每个 segment 的中间状态(如果将来需要精确往返,可以在 ChuNote 上加 segment 列表)

我有一点点没太想清楚的是:“多段跟随行合并到同一个音符对象(endCell/endWidth 用最后一段的)”这一下,那中间各段的segment终点的信息不就丢失掉了?这种情况下UGC → C2S还能准确拆回多行 SLD/SLC chain嘛?

以下是我的Agent说的:

Agent:那段话里 「UGC → C2S 生成再拆成多行 SLD/SLC」 和 「IR 不丢信息」 与当前仓库代码 不符。
C2sGenerator 实际在做什么
UgcChart 转 C2S 时只是对每个 ChuNote 做 ScaleNote,再按音符 一行一条 序列化:
            foreach (var n in ugc.Notes)
                result.Notes.Add(ScaleNote(n, ugc.TicksPerBeat));
    private static string FormatNote(ChuNote n) => n.Type switch
    {
        ...
        "SLD" or "SLC" or "SXD" or "SXC" => $"{n.Type}\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.SlideDuration}\t{n.EndCell}\t{n.EndWidth}",
也就是说:一个 ChuNote 只对应 C2S 里的一行 SLD/SLC/…,没有把「合并过的一条 Slide」再拆成多条、也没有按 UGC 里每段 >s/>c 分别输出。
UgcParser 里多段跟随行合并后,通常仍是 一个 Type == "SLD"、SlideDuration 为各段之和、EndCell/EndWidth 为最后一段的 ChuNote;C2sGenerator 不会根据已丢失的中间段去重建多行链。
哪些说法仍可对齐现实
•
「UGC 里多段链像一条轨道多个 segment」——概念上可以这么类比,和 sinmai 星星链的直觉类似。
•
「C2S 若要严格等价,可能需要多行 / 多段」——作为格式能力讨论可以成立,但 不是「我们现在已经在 generator 里这么做了」。
结论
对方说法
与当前代码
UGC 解析:多段合并到一个音符、时长累加、终点用最后一段
基本符合 ParseSlideNote 行为
UGC→C2S:再拆回多行 SLD/SLC chain
未实现;C2sGenerator 仅 1 note → 1 行
IR 不丢信息,再在 generator 按格式拆分
不成立;中间段终点、>s/>c 段型等 没有 存在 ChuNote 里,generator 也无从拆

若要对方那段话成立,需要 要么 在 IR 里增加 segment 列表(或每条链多个 ChuNote),要么 在 C2sGenerator(或前置 pass)里实现真正的拆分逻辑——目前都没有。

@Applesaber
Copy link
Copy Markdown
Author

对,确实丢了。目前的合并只保留了总 duration 和最后一段的终点,中间各段的中间位置信息没了。
UGC → C2S 时,如果是"只走直线、中间不拐弯"的 slide 没问题(反正终点就一个),
但如果中间有弯曲(endCell 在变),合并后拐弯信息就丢了,C2S 只能输出一头一尾。

我觉得可以这样修:

  1. 拆成独立 SLC 音符
    优点:信息不丢,C2S 可以直接输出 SLD ... → SLC ... → SLC ... chain
    缺点:测试里那 220 项可能得调(IR 里的音符数量变了)

  2. 加 segment 列表
    在 ChuNote 上加 List<Segment>,不拆音符但保留每段数据。
    Generator 读到 segment 列表时展开输出。

不过现在就改的话改动面比较大,得调测试和生成器,具体还得看你怎么说

@Starrah
Copy link
Copy Markdown
Collaborator

Starrah commented May 2, 2026

对,确实丢了。目前的合并只保留了总 duration 和最后一段的终点,中间各段的中间位置信息没了。 UGC → C2S 时,如果是"只走直线、中间不拐弯"的 slide 没问题(反正终点就一个), 但如果中间有弯曲(endCell 在变),合并后拐弯信息就丢了,C2S 只能输出一头一尾。

我觉得可以这样修:

  1. 拆成独立 SLC 音符
    优点:信息不丢,C2S 可以直接输出 SLD ... → SLC ... → SLC ... chain
    缺点:测试里那 220 项可能得调(IR 里的音符数量变了)
  2. 加 segment 列表
    在 ChuNote 上加 List<Segment>,不拆音符但保留每段数据。
    Generator 读到 segment 列表时展开输出。

不过现在就改的话改动面比较大,得调测试和生成器,具体还得看你怎么说

我现在可能要吃个饭,晚点我们Q之类的地方讨论吧~

@Starrah Starrah self-requested a review May 2, 2026 14:56
@Starrah Starrah dismissed their stale review May 2, 2026 14:57

发现了一些新的问题

@Starrah Starrah changed the base branch from master to dev May 2, 2026 15:06
@Starrah Starrah changed the base branch from dev to master May 2, 2026 15:46
@Starrah
Copy link
Copy Markdown
Collaborator

Starrah commented May 2, 2026

根据与 @Applesaber 的讨论结果:

  1. 本PR截止至 d404a68 的内容,先行合并进 dev 分支;(已合并过去)
  2. 之后,@Starrah 将会对ChuChart的IR等,进行一轮整体的重构。重构完成后,再继续修改余下的内容。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants