Skip to content

chunithm转谱:IR大重构,修正当前C2S和UGC的Parser与Generator中的许多错误。#3

Open
Starrah wants to merge 13 commits intodevfrom
refactor-chunithm
Open

chunithm转谱:IR大重构,修正当前C2S和UGC的Parser与Generator中的许多错误。#3
Starrah wants to merge 13 commits intodevfrom
refactor-chunithm

Conversation

@Starrah
Copy link
Copy Markdown
Collaborator

@Starrah Starrah commented May 7, 2026

本PR是 #2 ( #2 (comment) )的后续。
@Applesaber

重构中二谱面IR类,统一为ChuChart

首先,对Chunithm转谱模块的,IR中间表示(Chart)进行了整体的大重构。具体而言:

  1. 统一C2sChartUgcChartSusChart为统一的ChuChart
  2. 时间格式改为Rational分数(与maimai部分保持一致)
  3. ChuNote,也根据拿到的测试用紫谱C2S和这篇文章的内容进行了改写,使ChuNote可以全面、准确、无冗余的表示音符结构。

注意:所设计的ChuNoteChuChart主要以C2S的格式为基准

  • C2S中的每一行与单个ChuNote是一一对应的。
    • 也就是说,对于那种连续多段弯折的Slide,还是会产生许多个ChuNote,并不会合并到一起。
    • 为此,引入了Previous属性,用于标记某一段Slide的前驱段;实现了通用工具函数BaseChuParser.FillAllPrevious,自动为每一段Slide寻找前驱段、填充Previous属性。 见 9c13621
  • ChuNote中的属性,如Tag,优先以C2S中的记法为基准。
    • 例如Air的Tag会被记作DEF(与C2S一致),而不是与UGC一致的N
    • CHR的Tag也是UP/CE/DW(与C2S一致),而不是与UGC一致的U/D/C

基于新的IR,对C2S和UGC的Parser和Generator进行修改,(尽己所能)使其变得正确

  • ba1bbcc: UGC对@SPDMOD变速指令的正确支持和实现
  • UgcParser和UgcGenerator,此前对Slide、Air Hold、Air Slide的实现都是完全不对的。
    • Slide不会被正确按照”一个基础音符行+一个或多个跟随行“的方式生成。Parser原来会把多个跟随行,吃掉中间的只留下一段;Generator会把多段Slide逐个生成独立的基础音符行,而不是多个跟随行。修复于 fa9dbcbb54b7e9
    • Air Hold,解析和生成均存在明显错误,被和一般的Air音符混淆了。修复于 52af0a3b54b7e9
    • Air Slide,原来的UgcParser和UgcGenerator均干脆没有支持。添加于 52af0a3b54b7e9
  • 此外还修复了若干C2sParser和C2sGenerator的问题。

当前版本的主要缺陷(也是TODO):

  • UGC,对Air Crush还未能实现支持。 目前会给出警告并吞音。
    • 主要原因是,UGC不同版本对Air Crush的语法实现存在非常大的区别(我一会会把具体的情况发在楼下);
    • 而且,无论是C2S的ALD指令的复杂参数的含义,还是UGC的Air Crush语法中的复杂概念,目前也都没研究明白,可能还有待进一步研究。
  • Air Hold/Air Slide,对高度的支持尚未实现。
    • 目前UGCParser不会读取UGC中声明的高度信息,从而C2SGenerator会生成缺省值5(C2S侧值);UGCGenerator则会无视ChuChart中提供的高度信息,固定输出8(UGC侧值)
    • 之所以这里还没做,是因为,C2S和UGC的高度值并不是对应的,例如C2S的常用高度5对应的UGC高度为8,因此不同格式在高度读取写入的时候需要换算。所以就懒了还没做。
  • SUS的Parser和Generator,都完全没管,没修没做。(因为也没啥测例)
    • 我建议现阶段先修改宣传语,只说明我们支持UGC和C2S互转,先不提SUS的事。因为我猜测,现在的SUS转谱应该是基本用不了的状态。

Summary by Sourcery

围绕新的 ChuChart/ChuNote 中间表示(IR)统一 CHUNITHM 谱面处理,更新所有相关解析器/生成器以使用有理数计时并修正音符语义,并相应强化往返(round-trip)测试与文档。

New Features:

  • 为所有 CHUNITHM 格式引入统一的 ChuChart 和 ChuNote IR,采用有理数计时并共享元数据。
  • 新增 BaseChuParser,可在各格式中自动推断并关联滑键与空中音符的 Previous 链接。
  • 在 UGC 解析器/生成器中支持 UMIGURI @SPDMOD 变速、空中滑键以及点击音符(click notes)。

Bug Fixes:

  • 修正 UGC 对滑键、空中长押、空中滑键的解析/生成,以保持其结构和时长。
  • 修复 C2S 和 SUS 解析器/生成器,使其使用基于有理数的计时、正确的 BPM/SFL 处理以及更丰富的空中音符数据。
  • 解决 UGC/C2S/SUS 与内部 IR 之间在难度、等级和 BPM 元数据映射上的不一致问题。

Enhancements:

  • 重构 C2S、UGC 和 SUS 生成器,使其直接在 ChuChart 上运行,移除旧的按格式划分的谱面类型。
  • 添加 BPMList.BPM_DEF 辅助工具,并在生成器中复用,以保持 BPM_DEF 统计的一致性。
  • 改进 ChuTests 的往返比较,通过快照有理数时间、按格式处理量化,以及忽略冗余字段。

Documentation:

  • 更新 README,以反映统一的 ChuChart IR 以及新的 CHUNITHM 解析器/生成器函数签名。

Tests:

  • 扩展并细化 CHUNITHM 往返测试,包括具备分辨率缩放感知的 UGC↔C2S 等价性测试以及新的官方测试谱面。
Original summary in English

Summary by Sourcery

Unify CHUNITHM chart handling around a new ChuChart/ChuNote IR, update all related parsers/generators to use rational timing and correct note semantics, and bolster round-trip tests and docs accordingly.

New Features:

  • Introduce unified ChuChart and ChuNote IR for all CHUNITHM formats with rational timing and shared metadata.
  • Add BaseChuParser with automatic Previous linkage inference for slides and air notes across formats.
  • Support UMIGURI @SPDMOD speed changes, air slide notes, and click notes in the UGC parser/generator.

Bug Fixes:

  • Correct UGC parsing/generation of slide, air hold, and air slide notes to preserve structure and durations.
  • Fix C2S and SUS parsers/generators to use rational-based timing, proper BPM/SFL handling, and richer air note data.
  • Resolve inconsistencies in difficulty, level, and BPM metadata mapping between UGC/C2S/SUS and the internal IR.

Enhancements:

  • Refactor C2S, UGC, and SUS generators to operate directly on ChuChart, removing legacy per-format chart types.
  • Add BPMList.BPM_DEF helper and reuse it in generators for consistent BPM_DEF statistics.
  • Improve ChuTests round-trip comparison by snapshotting rational times, handling quantization per format, and ignoring redundant fields.

Documentation:

  • Update README to reflect the unified ChuChart IR and new CHUNITHM parser/generator signatures.

Tests:

  • Expand and refine CHUNITHM round-trip tests, including UGC↔C2S equivalence with resolution-aware scaling and new official test charts.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 7, 2026

Reviewer's Guide

将 CHUNITHM 的中间表示重构为统一的 ChuChart/ChuNote 模型(使用有理数时间),更新所有 C2S/UGC/SUS 解析器和生成器以使用该模型,修复许多此前不正确的 UGC 和 C2S 转换(尤其是滑条和空气类音符),并相应加强往返测试和文档。

使用 ChuChart 进行 UGC 滑条与空气滑条往返转换的时序图

sequenceDiagram
    participant User
    participant Program as Program_RunConvertChuSingleFile
    participant UgcParser
    participant ChuChart
    participant BaseChuParser
    participant UgcGenerator

    User->>Program: request convert UGC chart
    Program->>UgcParser: Parse(text)
    UgcParser->>ChuChart: new ChuChart()
    UgcParser->>ChuChart: parse header (BPM, BEAT, SPDMOD -> BpmList,MetList,SflList)
    loop note lines
        UgcParser->>ChuChart: ParseNoteLine
        alt slide parent (s/S)
            UgcParser->>ChuChart: create first ChuNote (SLD/ASD)
            UgcParser->>ChuChart: add segment notes for followers
        else other notes
            UgcParser->>ChuChart: add ChuNote
        end
    end
    UgcParser->>ChuChart: FinalizeUgcSflDurations
    UgcParser->>BaseChuParser: FillAllPrevious(chart,alerts)
    BaseChuParser->>ChuChart: scan notes, build endDict
    BaseChuParser->>ChuChart: set Previous for SLD/ASD/AIR/AHD
    BaseChuParser-->>UgcParser: chart with linked slide chains
    UgcParser-->>Program: ChuChart, alerts

    Program->>UgcGenerator: Generate(ChuChart)
    UgcGenerator->>ChuChart: Sort()
    UgcGenerator->>UgcGenerator: BuildSlideChains(Notes)
    loop notes
        alt slide head (SLD/ASD)
            UgcGenerator->>UgcGenerator: UCode(head)
            UgcGenerator-->>Program: write parent line s/S
            UgcGenerator->>UgcGenerator: emit follower lines using segments
        else hold / air hold
            UgcGenerator->>UgcGenerator: UCode(note)
            UgcGenerator-->>Program: write base line
            UgcGenerator-->>Program: write follower duration line
        else other
            UgcGenerator->>UgcGenerator: UCode(note)
            UgcGenerator-->>Program: write single line
        end
    end
    UgcGenerator-->>Program: ugcText, alerts
    Program-->>User: output UGC chart text
Loading

统一 ChuChart 与 ChuNote IR 的类图

classDiagram
    direction TB

    class BaseNote {
        <<abstract>>
        +Rational Time
        +Rational EndTime
    }

    class ChuNote {
        +string Type
        +int Cell
        +int Width
        +Rational Duration
        +int EndCell
        +int EndWidth
        +ChuNote Previous
        +string Tag
        +List~int~ ExtraData
        +string TargetNote
        +Rational EndTime
        -int _endCell
        -int _endWidth
    }

    BaseNote <|-- ChuNote

    class BaseChart_ChuNote_ {
        <<abstract>>
        +List~ChuNote~ Notes
        +BPMList BpmList
        +List~MET~ MetList
        +Rational ToSecond(Rational time)
        +void Sort()
    }

    class ChuChart {
        +string Title
        +string Artist
        +string Designer
        +int Difficulty
        +string DisplayLevel
        +decimal Level
        +string MusicId
        +List~(Rational,Rational,decimal)~ SflList
    }

    BaseChart_ChuNote_ <|-- ChuChart

    class BPMList {
        +Rational ConvertTime(Rational startTime,Rational value,decimal srcBpm,decimal destBpm)
        +ValueTuple~decimal,decimal,decimal,decimal~ BPM_DEF()
    }

    class BPM {
        +Rational Time
        +decimal Bpm
    }

    BPMList --> BPM : contains

    class MET {
        +Rational Time
        +int Numerator
        +int Denominator
    }

    ChuChart --> BPMList : has
    ChuChart --> MET : has MetList

    class BaseChuParser {
        <<abstract>>
        +(ChuChart,List~Alert~) Parse(string text)
        +void FillAllPrevious(ChuChart chart,List~Alert~ alerts,Dictionary~ChuNote,string~ rawTargetNote)
        +static bool IsSlide(string t)
        +static bool IsAirSlide(string t)
        +static bool IsAir(string t)
        +static bool IsAirHold(string t)
        +static bool IsAirCrush(string t)
        +static bool IsGeneralizedAir(string t)
    }

    class C2sParser {
        +(ChuChart,List~Alert~) Parse(string text)
        -Dictionary~ChuNote,string~ _rawTargetNote
    }

    class UgcParser {
        +(ChuChart,List~Alert~) Parse(string text)
        -int RSL
    }

    class SusParser {
        +(ChuChart,List~Alert~) Parse(string text)
        -int RSL
    }

    BaseChuParser <|-- C2sParser
    BaseChuParser <|-- UgcParser
    BaseChuParser <|-- SusParser

    class C2sGenerator {
        +(string,List~Alert~) Generate(ChuChart chart)
        -string Serialize(ChuChart chart,List~Alert~ alerts)
    }

    class UgcGenerator {
        +(string,List~Alert~) Generate(ChuChart chart)
        -string Serialize(ChuChart ugc,List~Alert~ alerts)
        -Dictionary~ChuNote,List~ChuNote~~ BuildSlideChains(List~ChuNote~ notes)
    }

    class SusGenerator {
        +(string,List~Alert~) Generate(ChuChart chart)
        -string Serialize(ChuChart sus)
    }

    ChuChart --> ChuNote : notes
    C2sParser --> ChuChart : builds
    UgcParser --> ChuChart : builds
    SusParser --> ChuChart : builds

    C2sGenerator --> ChuChart : consumes
    UgcGenerator --> ChuChart : consumes
    SusGenerator --> ChuChart : consumes

    class Utils {
        +int Tick(Rational time,int resolution,int extraTicks,int? maxTick)
        +ValueTuple~int,int~ BarAndTick(Rational time,int resolution)
        +Rational Max(Rational a,Rational b)
    }

    class ExtensionUtils {
        +void Add(Dictionary~K,List~V~ dict,K key,V value)
    }

    BPMList ..> Utils : uses
    C2sGenerator ..> Utils : uses
    UgcGenerator ..> Utils : uses
    SusGenerator ..> Utils : uses

    BaseChuParser ..> Utils : uses
Loading

文件级变更

Change Details Files
将 CHUNITHM 谱面 IR 统一为带有有理数时间和共享基础类型的 ChuChart/ChuNote。
  • 引入 ChuChart 作为 CHUNITHM 谱面的唯一 IR 类型,替代 C2sChart/UgcChart/SusChart,并继承 BaseChart<ChuNote>。
  • 重构 ChuNote 使其继承自 BaseNote,使用有理数 Time/Duration,推导 EndTime/EndCell/EndWidth,并添加 Previous 链接和用于复杂音符的 ExtraData。
  • 移除旧的 IChuChart/C2sChart/UgcChart/SusChart 类型,更新程序入口、生成器和测试以直接消费 ChuChart。
chart/chu/ChuChart.cs
chart/chu/ChuNote.cs
chart/chu/C2sChart.cs
chart/chu/UgcChart.cs
chart/chu/SusChart.cs
Program.cs
添加共享的 BaseChuParser,带自动 Previous 解析,并将 C2S/UGC/SUS 解析器迁移到 ChuChart 和有理数时间。
  • 创建 BaseChuParser,实现 IParser<ChuChart>,并提供通用的 FillAllPrevious 算法,用于根据可选的 rawTargetNote 提示推断滑条、空气、空气长押和空气滑条音符的 Previous。
  • 重构 C2sParser,使其输出带有基于 Rational 的 BPM/MET/SFL 列表的 ChuChart,跟踪 AIR 系列音符的原始 targetNote 字符串,并将其传入 FillAllPrevious。
  • 重构 SusParser,使其解析为具有有理数时间的 ChuChart,将 SUS 头部的 BPM/REQUEST 转为 BPMList/RSL,并复用 FillAllPrevious 进行滑条/空气音符的关联。
parser/chu/BaseChuParser.cs
parser/chu/C2sParser.cs
parser/chu/SusParser.cs
重写 UGC 解析器以针对 ChuChart,支持 UMIGURI v8 语义,并正确解析滑条、空气长押、空气滑条、速度变更和 follower 行。
  • 将 UgcParser 改为继承 BaseChuParser,并输出使用共享 RSL(每小节 tick 数)和有理数时间的 ChuChart。
  • 实现完整的头部解析,将 Difficulty/DisplayLevel/Level/MusicId、BPM/节拍事件(BpmList/MetList)以及 @SPDMOD 解析为 SflList,并在 FinalizeUgcSflDurations 中进行持续时间后处理。
  • 重写音符解析以使用通用的 TryParseUgcMeasureTick/TryParseFollowerLine 工具,正确处理 TAP/CHR、HLD/AHD/AHX、SLD/SLC 与 ASD/ASC 滑条链、带方向/颜色的 Air 音符、CLICK 和 MNE,并对 Air Crush 进行 stub 并发出明确警告。
  • 引入用于 base-36 高度解析和十六进制/base-36 转换的辅助方法,并更新告警格式,使其通过 Utils.BarAndTick 引用格式化的音符位置。
parser/chu/UgcParser.cs
utils/Utils.cs
重写 UGC 生成器以从 ChuChart 序列化,输出正确的 UMIGURI v8 头部/节奏与滑条/空气语义。
  • 将 UgcGenerator 改为实现 IGenerator<ChuChart>,移除从其他 IR 转换的路径,并直接从 ChuChart 序列化,其中 BPM/BEAT/SPDMOD 由 BpmList/MetList/SflList 以及有理数时间到小节/tick 的转换得到。
  • 通过 Previous 指针构建滑条链,以 head 为单位分组各 segment,并输出一行父级 s/S 行以及带有正确 >s/>c 标记、结束 cell/width 和空气滑条 base-36 高度的 follower 行。
  • 更新 UCode 映射以覆盖 TAP/CHR/HLD/SLD/SLC/FLK/MNE、带方向的 AIR、AHD/AHX、ASD/ASC(v8 空气滑条),并将内部空气颜色映射到 UGC 的 N/I 后缀。
  • 添加辅助工具:链构建、滑条类型谓词、高度编码、IntToHex、AirDirections/AirColor 字典,并对不支持的音符类型发出警告,而不是静默错误编码。
generator/chu/UgcGenerator.cs
重构 C2S 与 SUS 生成器,使其消费带有有理数时间和新 IR 的 ChuChart,同时改进 BPM/SFL/AIR/ASD/ALD 的处理。
  • 将 C2sGenerator 和 SusGenerator 改为实现 IGenerator<ChuChart>,移除旧的转换路径,并通过 BpmList/MetList/SflList 以及 Utils.BarAndTick/Tick(C2S 使用 384 分辨率,SUS 使用 480*4)进行时间序列化。
  • 重写 C2sGenerator.FormatNote 以覆盖扩展的 CHUNITHM 规范:TAP/CHR/HLD/SLD/SLC/FLK/MNE,带颜色标签的 AIR,AHD/AHX,利用 ExtraData 和 Tag 的 ASD/ASC,以及利用 ExtraData 的 ALD;对未知类型发出告警。
  • 调整 SusGenerator 的格式化逻辑,从 Cell/Width 导出轨道/宽度,使用 Duration 替代 HoldDuration/SlideDuration/AirHoldDuration,并支持 AHX 和正确缩放 SLD 的 end-cell/width。
  • 将 MA2Generator 的 BPM_DEF 计算切换为使用新的 BPMList.BPM_DEF() 工具,以便在多个系统之间对齐 BPM 统计。
generator/chu/C2sGenerator.cs
generator/chu/SusGenerator.cs
generator/mai/MA2Generator.cs
chart/BPMList.cs
加强 CHUNITHM UGC/C2S 往返测试,使其基于 ChuChart、有理数时间与新的缩放规则。
  • 更新 ChuTests 以使用 ChuChart 而非特定格式的谱面,移除依赖旧 IR 的分辨率断言,并将 Difficulty 视为 int。
  • 重新定义 SnapshotNote,以统一有理数和列表字段,对合适场景忽略多余的 EndTime/ExtraData,并将默认空气颜色视为空值以提升稳定性。
  • 引入缩放助手 UgcNoteScaledToC2sTicks 与 C2sNoteScaledToUgcTicks,通过 Utils.BarAndTick/Tick 和 CloneChuNoteWithTiming 将音符映射到对端格式的 tick 网格(384 vs 480*4)。
  • 重写 AssertUgcNotesEquivalentToReparsedC2s,以稳定顺序对音符排序,忽略 CLICK 音符,根据参考方应用正确缩放,并据此比较快照。
tests/chu/ChuTests.cs
utils/Utils.cs
更新公共 API 和文档,以反映基于 ChuChart 的 CHUNITHM 转换流水线以及当前 SUS 的限制。
  • 更新 README,说明三个 CHUNITHM 解析器现在都返回 ChuChart,生成器也接收 ChuChart,移除关于 IChuChart 多态性的讨论。
  • 调整 Program.RunConvertChuSingleFile,以 ChuChart 作为单一 IR 类型在 C2S/UGC/SUS 解析器和生成器之间进行分发。
  • 在注释和告警中标注 SUS 与 Air Crush 支持仍为部分实现或 TODO 的位置,以指导未来工作并管理用户预期。
README.md
Program.cs
parser/chu/UgcParser.cs
parser/chu/SusParser.cs

Tips and commands

Interacting with 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 以在任意时间(重新)生成摘要。
  • 生成 Reviewer's Guide: 在 Pull Request 中评论 @sourcery-ai guide 以在任意时间(重新)生成 reviewer's guide。
  • 解决所有 Sourcery 评论: 在 Pull Request 中评论 @sourcery-ai resolve 以解决所有 Sourcery 评论。如果你已经处理完所有评论且不想再看到它们,这会非常有用。
  • 撤销所有 Sourcery 评审: 在 Pull Request 中评论 @sourcery-ai dismiss 以撤销所有已有的 Sourcery 评审。尤其适合在你希望从一次全新的评审开始时使用——别忘了再评论 @sourcery-ai review 以触发新的评审!

Customizing Your Experience

访问你的 dashboard 以:

  • 启用或禁用评审功能,例如 Sourcery 自动生成的 Pull Request 摘要、reviewer's guide 等。
  • 更改评审语言。
  • 添加、移除或编辑自定义评审指令。
  • 调整其他评审设置。

Getting Help

Original review guide in English

Reviewer's Guide

Refactors CHUNITHM’s intermediate representation into a unified ChuChart/ChuNote model with rational timing, updates all C2S/UGC/SUS parsers and generators to use it, fixes many previously incorrect UGC and C2S conversions (especially slides and air types), and strengthens round‑trip tests and docs accordingly.

Sequence diagram for UGC slide and air slide round trip using ChuChart

sequenceDiagram
    participant User
    participant Program as Program_RunConvertChuSingleFile
    participant UgcParser
    participant ChuChart
    participant BaseChuParser
    participant UgcGenerator

    User->>Program: request convert UGC chart
    Program->>UgcParser: Parse(text)
    UgcParser->>ChuChart: new ChuChart()
    UgcParser->>ChuChart: parse header (BPM, BEAT, SPDMOD -> BpmList,MetList,SflList)
    loop note lines
        UgcParser->>ChuChart: ParseNoteLine
        alt slide parent (s/S)
            UgcParser->>ChuChart: create first ChuNote (SLD/ASD)
            UgcParser->>ChuChart: add segment notes for followers
        else other notes
            UgcParser->>ChuChart: add ChuNote
        end
    end
    UgcParser->>ChuChart: FinalizeUgcSflDurations
    UgcParser->>BaseChuParser: FillAllPrevious(chart,alerts)
    BaseChuParser->>ChuChart: scan notes, build endDict
    BaseChuParser->>ChuChart: set Previous for SLD/ASD/AIR/AHD
    BaseChuParser-->>UgcParser: chart with linked slide chains
    UgcParser-->>Program: ChuChart, alerts

    Program->>UgcGenerator: Generate(ChuChart)
    UgcGenerator->>ChuChart: Sort()
    UgcGenerator->>UgcGenerator: BuildSlideChains(Notes)
    loop notes
        alt slide head (SLD/ASD)
            UgcGenerator->>UgcGenerator: UCode(head)
            UgcGenerator-->>Program: write parent line s/S
            UgcGenerator->>UgcGenerator: emit follower lines using segments
        else hold / air hold
            UgcGenerator->>UgcGenerator: UCode(note)
            UgcGenerator-->>Program: write base line
            UgcGenerator-->>Program: write follower duration line
        else other
            UgcGenerator->>UgcGenerator: UCode(note)
            UgcGenerator-->>Program: write single line
        end
    end
    UgcGenerator-->>Program: ugcText, alerts
    Program-->>User: output UGC chart text
Loading

Class diagram for unified ChuChart and ChuNote IR

classDiagram
    direction TB

    class BaseNote {
        <<abstract>>
        +Rational Time
        +Rational EndTime
    }

    class ChuNote {
        +string Type
        +int Cell
        +int Width
        +Rational Duration
        +int EndCell
        +int EndWidth
        +ChuNote Previous
        +string Tag
        +List~int~ ExtraData
        +string TargetNote
        +Rational EndTime
        -int _endCell
        -int _endWidth
    }

    BaseNote <|-- ChuNote

    class BaseChart_ChuNote_ {
        <<abstract>>
        +List~ChuNote~ Notes
        +BPMList BpmList
        +List~MET~ MetList
        +Rational ToSecond(Rational time)
        +void Sort()
    }

    class ChuChart {
        +string Title
        +string Artist
        +string Designer
        +int Difficulty
        +string DisplayLevel
        +decimal Level
        +string MusicId
        +List~(Rational,Rational,decimal)~ SflList
    }

    BaseChart_ChuNote_ <|-- ChuChart

    class BPMList {
        +Rational ConvertTime(Rational startTime,Rational value,decimal srcBpm,decimal destBpm)
        +ValueTuple~decimal,decimal,decimal,decimal~ BPM_DEF()
    }

    class BPM {
        +Rational Time
        +decimal Bpm
    }

    BPMList --> BPM : contains

    class MET {
        +Rational Time
        +int Numerator
        +int Denominator
    }

    ChuChart --> BPMList : has
    ChuChart --> MET : has MetList

    class BaseChuParser {
        <<abstract>>
        +(ChuChart,List~Alert~) Parse(string text)
        +void FillAllPrevious(ChuChart chart,List~Alert~ alerts,Dictionary~ChuNote,string~ rawTargetNote)
        +static bool IsSlide(string t)
        +static bool IsAirSlide(string t)
        +static bool IsAir(string t)
        +static bool IsAirHold(string t)
        +static bool IsAirCrush(string t)
        +static bool IsGeneralizedAir(string t)
    }

    class C2sParser {
        +(ChuChart,List~Alert~) Parse(string text)
        -Dictionary~ChuNote,string~ _rawTargetNote
    }

    class UgcParser {
        +(ChuChart,List~Alert~) Parse(string text)
        -int RSL
    }

    class SusParser {
        +(ChuChart,List~Alert~) Parse(string text)
        -int RSL
    }

    BaseChuParser <|-- C2sParser
    BaseChuParser <|-- UgcParser
    BaseChuParser <|-- SusParser

    class C2sGenerator {
        +(string,List~Alert~) Generate(ChuChart chart)
        -string Serialize(ChuChart chart,List~Alert~ alerts)
    }

    class UgcGenerator {
        +(string,List~Alert~) Generate(ChuChart chart)
        -string Serialize(ChuChart ugc,List~Alert~ alerts)
        -Dictionary~ChuNote,List~ChuNote~~ BuildSlideChains(List~ChuNote~ notes)
    }

    class SusGenerator {
        +(string,List~Alert~) Generate(ChuChart chart)
        -string Serialize(ChuChart sus)
    }

    ChuChart --> ChuNote : notes
    C2sParser --> ChuChart : builds
    UgcParser --> ChuChart : builds
    SusParser --> ChuChart : builds

    C2sGenerator --> ChuChart : consumes
    UgcGenerator --> ChuChart : consumes
    SusGenerator --> ChuChart : consumes

    class Utils {
        +int Tick(Rational time,int resolution,int extraTicks,int? maxTick)
        +ValueTuple~int,int~ BarAndTick(Rational time,int resolution)
        +Rational Max(Rational a,Rational b)
    }

    class ExtensionUtils {
        +void Add(Dictionary~K,List~V~ dict,K key,V value)
    }

    BPMList ..> Utils : uses
    C2sGenerator ..> Utils : uses
    UgcGenerator ..> Utils : uses
    SusGenerator ..> Utils : uses

    BaseChuParser ..> Utils : uses
Loading

File-Level Changes

Change Details Files
Unify CHUNITHM chart IR into ChuChart/ChuNote with rational timing and shared base types.
  • Introduce ChuChart as the single IR type for CHUNITHM charts, replacing C2sChart/UgcChart/SusChart and extending BaseChart.
  • Refactor ChuNote to inherit from BaseNote, use Rational Time/Duration, derive EndTime/EndCell/EndWidth, and add Previous linkage and ExtraData for complex notes.
  • Remove legacy IChuChart/C2sChart/UgcChart/SusChart types and update program entry points, generators, and tests to consume ChuChart directly.
chart/chu/ChuChart.cs
chart/chu/ChuNote.cs
chart/chu/C2sChart.cs
chart/chu/UgcChart.cs
chart/chu/SusChart.cs
Program.cs
Add a shared BaseChuParser with automatic Previous resolution and migrate C2S/UGC/SUS parsers onto ChuChart and Rational timing.
  • Create BaseChuParser implementing IParser and a generic FillAllPrevious algorithm that infers Previous for slides, air, air-hold, and air-slide notes, with optional rawTargetNote hints.
  • Refactor C2sParser to output ChuChart with Rational-based BPM/MET/SFL lists, track raw targetNote strings for AIR-family notes, and feed them into FillAllPrevious.
  • Refactor SusParser to parse into ChuChart with Rational timing, convert SUS header BPM/REQUEST into BPMList/RSL, and reuse FillAllPrevious for slide/air linkage.
parser/chu/BaseChuParser.cs
parser/chu/C2sParser.cs
parser/chu/SusParser.cs
Rewrite UGC parser to target ChuChart, support UMIGURI v8 semantics, and correctly parse slides, air-hold, air-slide, speed mods, and follower lines.
  • Change UgcParser to inherit BaseChuParser and output ChuChart using a shared RSL (ticks-per-measure) and Rational times.
  • Implement full header parsing to Difficulty/DisplayLevel/Level/MusicId, BPM/beat events (BpmList/MetList), and @SPDMOD into SflList with duration post-processing in FinalizeUgcSflDurations.
  • Rewrite note parsing to use generalized TryParseUgcMeasureTick/TryParseFollowerLine utilities, correctly handle TAP/CHR, HLD/AHD/AHX, SLD/SLC and ASD/ASC slide chains, Air notes with direction/color, CLICK and MNE, and stub Air Crush with explicit warnings.
  • Introduce helpers for base-36 height parsing and hex/base-36 conversions, and update alert formatting to reference formatted note locations via Utils.BarAndTick.
parser/chu/UgcParser.cs
utils/Utils.cs
Rewrite UGC generator to serialize from ChuChart, emitting correct UMIGURI v8 header/timing and slide/air semantics.
  • Change UgcGenerator to implement IGenerator, remove conversion from other IRs, and serialize directly from ChuChart with BPM/BEAT/SPDMOD derived from BpmList/MetList/SflList using Rational time to bar/tick conversion.
  • Build slide chains via Previous pointers, group segments per head, and emit one parent s/S line plus follower lines with proper >s/>c markers, end cell/width, and base-36 heights for air-slides.
  • Update UCode mapping to cover TAP/CHR/HLD/SLD/SLC/FLK/MNE, AIR directions, AHD/AHX, ASD/ASC (v8 air-slide), and map internal air colors to UGC N/I suffixes.
  • Add helper utilities: chain construction, slide type predicates, height encoding, IntToHex, AirDirections/AirColor dictionaries, and emit warnings for unsupported note types instead of silently mis-encoding them.
generator/chu/UgcGenerator.cs
Refactor C2S and SUS generators to consume ChuChart with Rational timing and the new IR, while improving handling of BPM/SFL/AIR/ASD/ALD.
  • Change C2sGenerator and SusGenerator to implement IGenerator, drop legacy conversion paths, and serialize timing via BpmList/MetList/SflList and Utils.BarAndTick/Tick using fixed resolutions (384 for C2S, 480*4 for SUS).
  • Rewrite C2sGenerator.FormatNote to cover the extended CHUNITHM spec: TAP/CHR/HLD/SLD/SLC/FLK/MNE, AIR with color tags, AHD/AHX, ASD/ASC using ExtraData and Tag, and ALD using ExtraData; emit alerts for unknown types.
  • Adjust SusGenerator formatting to derive lane/width from Cell/Width, use Duration instead of HoldDuration/SlideDuration/AirHoldDuration, and support AHX and correct SLD end-cell/width scaling.
  • Switch MA2Generator BPM_DEF computation to use the new BPMList.BPM_DEF() helper, aligning BPM statistics across systems.
generator/chu/C2sGenerator.cs
generator/chu/SusGenerator.cs
generator/mai/MA2Generator.cs
chart/BPMList.cs
Strengthen CHUNITHM UGC/C2S round-trip tests to work on ChuChart with Rational timing and new scaling rules.
  • Update ChuTests to work with ChuChart instead of format-specific charts, remove resolution assertions tied to old IR, and interpret Difficulty as an int.
  • Redefine SnapshotNote to normalize Rational and list fields, ignore redundant EndTime/ExtraData where appropriate, and treat default air color as empty for stability.
  • Introduce scaling helpers UgcNoteScaledToC2sTicks and C2sNoteScaledToUgcTicks that project notes into the opposite format’s tick grid (384 vs 480*4) using Utils.BarAndTick/Tick and CloneChuNoteWithTiming.
  • Rewrite AssertUgcNotesEquivalentToReparsedC2s to sort notes in a stable order, ignore CLICK notes, apply proper scaling depending on which side is reference, and compare snapshots accordingly.
tests/chu/ChuTests.cs
utils/Utils.cs
Update public API and documentation to reflect ChuChart-based CHUNITHM conversion pipelines and current SUS limitations.
  • Change README to state that all three CHUNITHM parsers now return ChuChart and generators take ChuChart, removing the IChuChart polymorphism discussion.
  • Adjust Program.RunConvertChuSingleFile to use ChuChart as the single IR type when dispatching between C2S/UGC/SUS parsers and generators.
  • Note in comments and alerts where SUS and Air Crush support remain partial or TODO, guiding future work and managing user expectations.
README.md
Program.cs
parser/chu/UgcParser.cs
parser/chu/SusParser.cs

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

@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.

你好——我发现了 3 个问题,并给出了一些总体反馈:

  • 现在各个 CHU 解析器/生成器使用了可变的静态字段来表示 resolution/ticks(例如 UgcParserUgcGeneratorC2sParserSusParserSusGenerator 中的 RSL),这意味着在并行解析不同谱面,或者顺序解析时使用不同的 @TICKS/RESOLUTION 值时,它们会相互干扰;建议将这些改为实例字段,或者显式传入 resolution,以便将状态限定在每个谱面内。
  • C2sParser.ParseTiming 中,你改成了使用 decimal.Parse(p[3]) / decimal.Parse(p[4]),但没有指定区域设置或错误处理,现在对于格式错误或带本地化格式的 BPM/SFL 值,会直接抛异常,而旧代码会安全地回退;使用 decimal.TryParse(..., NumberStyles.Float, CultureInfo.InvariantCulture, out var bpm)(并在失败时给出默认值或警告)会让解析器更健壮。
给 AI Agent 的提示
请根据这次代码审查中的评论进行修改:

## 总体评论
- 现在各个 CHU 解析器/生成器使用了可变的静态字段来表示 resolution/ticks(例如 `UgcParser``UgcGenerator``C2sParser``SusParser``SusGenerator` 中的 `RSL`),这意味着在并行解析不同谱面,或者顺序解析时使用不同的 `@TICKS`/`RESOLUTION` 值时,它们会相互干扰;建议将这些改为实例字段,或者显式传入 resolution,以便将状态限定在每个谱面内。
-`C2sParser.ParseTiming` 中,你改成了使用 `decimal.Parse(p[3])` / `decimal.Parse(p[4])`,但没有指定区域设置或错误处理,现在对于格式错误或带本地化格式的 BPM/SFL 值,会直接抛异常,而旧代码会安全地回退;使用 `decimal.TryParse(..., NumberStyles.Float, CultureInfo.InvariantCulture, out var bpm)`(并在失败时给出默认值或警告)会让解析器更健壮。

## 逐条评论

### 评论 1
<location path="chart/chu/ChuNote.cs" line_range="17-18" />
<code_context>
-    public int HoldDuration { get; set; }
-    /** SLD 持续时长 */
-    public int SlideDuration { get; set; }
+    /** HLD/SLD/AHD/ASD等的 持续时长 */
+    public Rational Duration { get; set => field = value.CanonicalForm; } = 0;
+
     /** SLD 终点列 */
</code_context>
<issue_to_address>
**issue (bug_risk):** Duration 自动属性的实现无效,代码无法通过编译。

在 C# 中,自动属性的 setter 里不能使用 `field`,因此这段代码无法编译。请显式引入一个后备字段,并在 setter 中赋值 `CanonicalForm`,例如:

```csharp
private Rational _duration;
public Rational Duration
{
    get => _duration;
    set => _duration = value.CanonicalForm;
}
```

如果不需要在赋值时进行规范化,可以保留普通的自动属性,只在使用时做规范化处理。
</issue_to_address>

### 评论 2
<location path="parser/chu/C2sParser.cs" line_range="79" />
<code_context>
         {
             case "BPM":
-                chart.BpmEvents.Add((Int(p, 1), Int(p, 2), Dbl(p, 3, 120.0)));
+                chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), decimal.Parse(p[3])));
                 break;
             case "MET":
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 在不指定区域设置或校验的情况下使用 decimal.Parse,可能会错误解析 BPM 值。

在其他地方,我们使用 `TryParse` + `NumberStyles.Float``CultureInfo.InvariantCulture` 来解析数值元数据。这里的 `decimal.Parse(p[3])` 会依赖当前区域设置,并在输入不合法时抛出异常。

为了保持行为一致且健壮,建议使用:

```csharp
if (decimal.TryParse(p[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var bpm))
{
    chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), bpm));
}
else
{
    alerts.Add(new Alert(Warning, $"BPM 格式错误: {string.Join('\t', p)}") { Line = lineNum });
}
```

你可能还需要将 `lineNum` 作为参数传入 `ParseTiming`,以便正确地填充 `Alert.Line`。

建议的实现方式:

```csharp
    }


```

```csharp
    private static void ParseTiming(string[] p, ChuChart chart, int lineNum, List<Alert> alerts)

```

```csharp
            case "BPM":
                if (decimal.TryParse(p[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var bpm))
                {
                    chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), bpm));
                }
                else
                {
                    alerts.Add(new Alert(Warning, $"BPM 格式错误: {string.Join('\t', p)}") { Line = lineNum });
                }
                break;

```

1. 确保在 `parser/chu/C2sParser.cs` 顶部已经包含如下 `using`(如果尚未添加):
   - `using System.Globalization;`
2. 将方法签名改为 `ParseTiming(string[] p, ChuChart chart, int lineNum, List<Alert> alerts)` 之后,需要更新所有 `ParseTiming` 的调用点,传入当前行号以及共享的 `List<Alert>`(或你用于保存告警的集合类型)。
3. 如果 `Alert.Line` 不是可设置属性,或者 `Alert`/`Warning` 在不同命名空间中,请添加相应的 `using` 或使用完全限定名。
4. 为了与解析器其它部分的行为完全一致,你也可以将本文件中其它 `decimal.Parse` 调用(例如 `SFL` 分支)替换为 `TryParse` + `NumberStyles.Float` + `CultureInfo.InvariantCulture`,并在解析失败时以类似方式上报告警。
</issue_to_address>

### 评论 3
<location path="parser/chu/SusParser.cs" line_range="210-214" />
<code_context>
     }

-    private static void ParseAirTarget(string dataStr, ChuNote note, List<Alert> alerts, int lineNum)
+    private static void ParseAirTarget(string dataStr, ChuNote note, int tpm, List<Alert> alerts, int lineNum)
     {
-        if (dataStr.Length >= 8)
+        if (dataStr.Length < 8)
         {
-            note.TargetNote = HexToInt(dataStr[6..8]).ToString();
-        }
-        else
-        {
-            alerts.Add(new Alert(Warning, $"AIR/ADW 音符缺少目标: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note) });
+            alerts.Add(new Alert(Warning, $"AIR/ADW 音符缺少目标: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note, tpm) });
         }
     }
</code_context>
<issue_to_address>
**issue (bug_risk):** SUS AIR/ADW 的解析不再为音符设置任何目标 ID。

在之前的实现中,这个方法会解析目标 ID(十六进制 → 整数 → 字符串),并将其存到音符上(`note.TargetNote`)。新版本只检查长度并在缺失时发出告警,但从未把目标信息保存到任何地方。

由于 SUS AIR/ADW 的行为以及后续生成器依赖这个 ID,我们现在丢失了重建原始连接所需的信息。一旦你重新引入用于保存原始目标的稳定字段(例如 `ChuNote.TargetNote` 或新的 `RawTargetId`),该方法应该为其赋值,例如:

```csharp
note.RawTargetId = HexToInt(dataStr[6..8]).ToString();
```

这样生成器在输出 SUS/C2S 时就可以保留原始的目标 ID。
</issue_to_address>

Sourcery 对开源项目免费 —— 如果你觉得这些评论有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,你的反馈会用来改进后续的代码审查。
Original comment in English

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

  • The various CHU parsers/generators now use mutable static fields for resolution/ticks (e.g. RSL in UgcParser, UgcGenerator, C2sParser, SusParser, SusGenerator), which means parsing different charts in parallel or sequentially with different @TICKS/RESOLUTION values can interfere with each other; consider making these instance fields or passing resolution explicitly to keep state per chart.
  • In C2sParser.ParseTiming you switched to decimal.Parse(p[3]) / decimal.Parse(p[4]) without culture or error handling, which can now throw on malformed or locale-formatted BPM/SFL values where the old code would fallback safely; using decimal.TryParse(..., NumberStyles.Float, CultureInfo.InvariantCulture, out var bpm) (and defaulting or warning on failure) would make the parser more robust.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The various CHU parsers/generators now use mutable static fields for resolution/ticks (e.g. `RSL` in `UgcParser`, `UgcGenerator`, `C2sParser`, `SusParser`, `SusGenerator`), which means parsing different charts in parallel or sequentially with different `@TICKS`/`RESOLUTION` values can interfere with each other; consider making these instance fields or passing resolution explicitly to keep state per chart.
- In `C2sParser.ParseTiming` you switched to `decimal.Parse(p[3])` / `decimal.Parse(p[4])` without culture or error handling, which can now throw on malformed or locale-formatted BPM/SFL values where the old code would fallback safely; using `decimal.TryParse(..., NumberStyles.Float, CultureInfo.InvariantCulture, out var bpm)` (and defaulting or warning on failure) would make the parser more robust.

## Individual Comments

### Comment 1
<location path="chart/chu/ChuNote.cs" line_range="17-18" />
<code_context>
-    public int HoldDuration { get; set; }
-    /** SLD 持续时长 */
-    public int SlideDuration { get; set; }
+    /** HLD/SLD/AHD/ASD等的 持续时长 */
+    public Rational Duration { get; set => field = value.CanonicalForm; } = 0;
+
     /** SLD 终点列 */
</code_context>
<issue_to_address>
**issue (bug_risk):** Duration auto-property implementation is invalid and won’t compile.

`field` can’t be used in an auto-property setter in C#, so this code won’t compile. Introduce an explicit backing field and assign `CanonicalForm` in the setter, e.g.

```csharp
private Rational _duration;
public Rational Duration
{
    get => _duration;
    set => _duration = value.CanonicalForm;
}
```

If normalization on assignment isn’t required, keep a plain auto-property and normalize only at use sites.
</issue_to_address>

### Comment 2
<location path="parser/chu/C2sParser.cs" line_range="79" />
<code_context>
         {
             case "BPM":
-                chart.BpmEvents.Add((Int(p, 1), Int(p, 2), Dbl(p, 3, 120.0)));
+                chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), decimal.Parse(p[3])));
                 break;
             case "MET":
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using decimal.Parse without specifying culture or validation may mis-parse BPM values.

Elsewhere we parse numeric metadata with `TryParse` + `NumberStyles.Float` and `CultureInfo.InvariantCulture`. Here, `decimal.Parse(p[3])` is culture-dependent and will throw on bad input.

To keep behavior consistent and robust, consider using:

```csharp
if (decimal.TryParse(p[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var bpm))
{
    chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), bpm));
}
else
{
    alerts.Add(new Alert(Warning, $"BPM 格式错误: {string.Join('\t', p)}") { Line = lineNum });
}
```

You may also want to pass `lineNum` into `ParseTiming` so `Alert.Line` is populated correctly.

Suggested implementation:

```csharp
    }


```

```csharp
    private static void ParseTiming(string[] p, ChuChart chart, int lineNum, List<Alert> alerts)

```

```csharp
            case "BPM":
                if (decimal.TryParse(p[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var bpm))
                {
                    chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), bpm));
                }
                else
                {
                    alerts.Add(new Alert(Warning, $"BPM 格式错误: {string.Join('\t', p)}") { Line = lineNum });
                }
                break;

```

1. Ensure the following `using` is present at the top of `parser/chu/C2sParser.cs` (if it is not already there):
   - `using System.Globalization;`
2. The method signature change to `ParseTiming(string[] p, ChuChart chart, int lineNum, List<Alert> alerts)` requires updating all call sites of `ParseTiming` to pass the current line number and the shared `List<Alert>` (or whatever collection type you use for alerts).
3. If `Alert.Line` is not a settable property or if `Alert`/`Warning` are in a different namespace, add the appropriate `using` or fully qualify those symbols.
4. For full consistency with the rest of the parser, you may also want to replace other `decimal.Parse` calls in this file (e.g. the `SFL` case) with `TryParse` + `NumberStyles.Float` + `CultureInfo.InvariantCulture` and report alerts similarly on failure.
</issue_to_address>

### Comment 3
<location path="parser/chu/SusParser.cs" line_range="210-214" />
<code_context>
     }

-    private static void ParseAirTarget(string dataStr, ChuNote note, List<Alert> alerts, int lineNum)
+    private static void ParseAirTarget(string dataStr, ChuNote note, int tpm, List<Alert> alerts, int lineNum)
     {
-        if (dataStr.Length >= 8)
+        if (dataStr.Length < 8)
         {
-            note.TargetNote = HexToInt(dataStr[6..8]).ToString();
-        }
-        else
-        {
-            alerts.Add(new Alert(Warning, $"AIR/ADW 音符缺少目标: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note) });
+            alerts.Add(new Alert(Warning, $"AIR/ADW 音符缺少目标: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note, tpm) });
         }
     }
</code_context>
<issue_to_address>
**issue (bug_risk):** SUS AIR/ADW parsing no longer assigns any target ID to the note.

In the previous implementation, this method decoded the target ID (hex → int → string) and stored it on the note (`note.TargetNote`). The new version only checks length and warns when missing, but never persists the target anywhere.

Since SUS AIR/ADW behavior and downstream generators depend on this ID, we’re now losing information needed to reproduce the original linkage. Once you reintroduce a stable field for the raw target (e.g. `ChuNote.TargetNote` or a new `RawTargetId`), this method should set it, for example:

```csharp
note.RawTargetId = HexToInt(dataStr[6..8]).ToString();
```

so generators can retain the original target ID when emitting SUS/C2S.
</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 chart/chu/ChuNote.cs
Comment thread parser/chu/C2sParser.cs Outdated
Comment thread parser/chu/SusParser.cs
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 unifies the CHUNITHM chart Intermediate Representation (IR) by replacing format-specific chart classes with a single ChuChart and updating ChuNote to use Rational types for precise timing. It refactors the C2S, SUS, and UGC parsers and generators to support this unified structure and introduces a base parser for shared note-linking logic. Key feedback includes addressing potential null reference exceptions in BPM statistics, ensuring culture-invariant formatting and parsing for decimal values to prevent locale-specific bugs, and investigating a potential regression in SUS target note parsing.

Comment thread chart/BPMList.cs
sb.AppendLine($"BPM_DEF\t{Fmt(chart.DefBpm)}\t{Fmt(chart.DefBpm)}\t{Fmt(chart.DefBpm)}\t{Fmt(chart.DefBpm)}");
sb.AppendLine($"CREATOR\t{chart.Designer}");
var bpm_def = chart.BpmList.BPM_DEF();
sb.AppendLine($"BPM_DEF\t{bpm_def.Item1}\t{bpm_def.Item2}\t{bpm_def.Item3}\t{bpm_def.Item4}");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

使用字符串插值格式化 decimal 值时,默认会使用当前线程的区域性设置。这可能导致在某些系统上小数点被格式化为逗号(,),从而生成无效的谱面文件。为了确保输出格式的统一性,建议使用 CultureInfo.InvariantCulture。此问题同样存在于第43行和第56行。

        sb.AppendLine(FormattableString.Invariant($"BPM_DEF\t{bpm_def.Item1:0.000}\t{bpm_def.Item2:0.000}\t{bpm_def.Item3:0.000}\t{bpm_def.Item4:0.000}"));

if (!string.IsNullOrEmpty(sus.Designer)) sb.AppendLine($"#DESIGNER \"{sus.Designer}\"");
sb.AppendLine($"#BPM_DEF {sus.Bpm:F2}");
sb.AppendLine($"#REQUEST \"{sus.TicksPerBeat}\"");
sb.AppendLine($"#BPM_DEF {sus.StartBpm:F2}");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

使用字符串插值格式化 decimal 值时,默认会使用当前线程的区域性设置。这可能导致在某些系统上小数点被格式化为逗号(,),从而生成无效的谱面文件。为了确保输出格式的统一性,建议使用 CultureInfo.InvariantCulture

        sb.AppendLine(FormattableString.Invariant($"#BPM_DEF {sus.StartBpm:F2}"));

if (!string.IsNullOrEmpty(ugc.Designer)) sb.AppendLine($"@DESIGN\t{ugc.Designer}");
sb.AppendLine($"@DIFF\t{ugc.Difficulty}");
sb.AppendLine($"@LEVEL\t{ugc.DisplayLevel}");
sb.AppendLine($"@CONST\t{ugc.Level:F5}");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

使用字符串插值格式化 decimal 值时,默认会使用当前线程的区域性设置。这可能导致在某些系统上小数点被格式化为逗号(,),从而生成无效的谱面文件。为了确保输出格式的统一性,建议使用 CultureInfo.InvariantCulture。此问题同样存在于第40行和第47行。

        sb.AppendLine(FormattableString.Invariant($"@CONST\t{ugc.Level:F5}"));

Comment thread parser/chu/C2sParser.cs Outdated
{
case "BPM":
chart.BpmEvents.Add((Int(p, 1), Int(p, 2), Dbl(p, 3, 120.0)));
chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), decimal.Parse(p[3])));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

decimal.Parse(p[3]) 默认使用当前线程的区域性设置,如果系统的小数点分隔符不是 .,可能会导致解析失败或结果不正确。考虑到C2S文件格式的普遍性,建议显式指定 CultureInfo.InvariantCulture 以确保解析的稳定性。此问题同样存在于第88行。

                chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), decimal.Parse(p[3], CultureInfo.InvariantCulture)));

Comment thread parser/chu/SusParser.cs
@Starrah
Copy link
Copy Markdown
Collaborator Author

Starrah commented May 7, 2026

关于不同版本UGC,在 Air-Crush (:C:T) 语句上的不同
分析样本包括:UGC @ver 版本4、6、7、8的谱面各一个

v8文档

https://gist.github.com/inonote/5c01e73781cab17765a1d93641d52298

结论摘要

v4 / v6 / v7 ,也就是v8之前,都是相同的;然而遗憾的是,未能找到相关文档。我尽力找了,但网上只有v8的文档。

  • 包括Air Crush在内的各种Air音符,高度属性 height 的值均为 1 位。
    • 例如,CD380,拆解为C(AirCrush) + D3(cell&width) + 8(高度) + 0(颜色)
  • 跟随行机制:在 :C 音符的后面,可观察到 >c>s 两种子音符写法都是有的。
    • 其中,对>s的情况,有可能会出现只有单个字母 >s、而没有cell/width/height三位参数的情况。但>c时则必定后面又有cell/width/height参数数
  • v8版本之前的谱面中,还存在:T类型的音符。这种类型的音符在v8中已经完全不存在了,因此其含义并不清楚,只有一些观察:
    • 一些情况下,:T会与:C伴生出现、时刻相同。但并不绝对。
    • 在这种情况下,往往是:C先出现、跟随行为>c,然后出现:T、跟随行为>s。尚不清楚他们之间有什么关联和关系。
    • :T也是可以单独、不依附于:C出现的。单独出现时的含义不明确,也没发现什么规律。

v8相比于前代,在Air Crush方面则引入了重大变更:•
Air-Crush 升级为长格式并引入间隔:C x w hh c , {interval}:

  • 高度属性 height 的值变为 2 位。根据文档说明,2位数整体理解成36进制数后,除以10,即为前代意义下的高度。相当于把精度扩大了10倍。
  • 新增 ,{interval}
    • 这是最大的变更,也是制约我把AirCrush的解析写出来的最根本原因。我不知道这个interval的含义是什么、与原来的语法和C2S ALD中的参数的对应是什么。
    • 根据观察,样本中常见的interval的值是 0$这两个特殊值。日文文档中有提到它们是特殊值,但我不能理解其实际意义。
  • 在上述新增interval的基础上,跟随行不再有>s了,而是有且仅有一行>c
  • :T音符类型被移除。文档里,和谱面中,都找不到了。怀疑是被interval吸收了。

@Starrah
Copy link
Copy Markdown
Collaborator Author

Starrah commented May 7, 2026

Request for @Applesaber review

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.

14 issues found across 20 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="README.md">

<violation number="1" location="README.md:218">
P3: README now documents `ChuChart`, but nearby CHUNITHM usage comments still describe `IChuChart`, creating contradictory API guidance.</violation>
</file>

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

<violation number="1" location="parser/chu/UgcParser.cs:15">
P1: `RSL` is a mutable static field that retains its value between `Parse()` calls. Parsing a file with a non-default `@TICKS` will corrupt timing for all subsequently parsed files that don't explicitly declare `@TICKS`. Reset it at the start of `Parse()` or make it an instance field.</violation>

<violation number="2" location="parser/chu/UgcParser.cs:361">
P1: `AirColor[colorChar.ToString()]` will throw `KeyNotFoundException` if the last character of the code is not 'N' or 'I'. Use `GetValueOrDefault` or `TryGetValue` with a fallback (as done with `ChrExtras` in `ParseTapNote`).</violation>

<violation number="3" location="parser/chu/UgcParser.cs:466">
P1: `line[sepIdx+1]` will throw `IndexOutOfRangeException` if the separator is the last character in the line. Add a bounds check before accessing.</violation>
</file>

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

<violation number="1" location="chart/chu/ChuChart.cs:11">
P2: Using `int` for `Difficulty` loses non-numeric difficulty labels, which contradicts the intended robust IR behavior and breaks metadata round-tripping.</violation>
</file>

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

<violation number="1" location="parser/chu/C2sParser.cs:16">
P2: Avoid using a static mutable resolution field; keep resolution per parse instance/call to prevent cross-chart state leakage.</violation>

<violation number="2" location="parser/chu/C2sParser.cs:79">
P2: Use culture-invariant, validated decimal parsing for BPM/SFL values to avoid locale-dependent parse failures and unhandled exceptions.</violation>
</file>

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

<violation number="1" location="generator/chu/C2sGenerator.cs:22">
P2: `MUSIC` is silently rewritten to `0` when `chart.MusicId` is not parseable as `int`, causing data loss.</violation>

<violation number="2" location="generator/chu/C2sGenerator.cs:31">
P1: Use invariant-culture formatting for BPM_DEF numeric output to avoid locale-dependent decimal separators in generated C2S files.</violation>

<violation number="3" location="generator/chu/C2sGenerator.cs:43">
P1: Decimal output is locale-dependent; C2S serialization should use invariant formatting for BPM/SFL/BPM_DEF values.</violation>
</file>

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

<violation number="1" location="parser/chu/SusParser.cs:16">
P1: Using a mutable `static` `RSL` leaks timing resolution across parse calls, so one chart’s `REQUEST` can corrupt timing in later charts.</violation>

<violation number="2" location="parser/chu/SusParser.cs:99">
P1: Validate `REQUEST` ticks as `> 0` before assigning `RSL`; zero/negative values can break rational time calculations.

(Based on your team's feedback about rejecting zero/negative SUS tick metadata instead of silently accepting it.) [FEEDBACK_USED]</violation>
</file>

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

<violation number="1" location="generator/chu/UgcGenerator.cs:29">
P1: Write `@CONST` using invariant-culture numeric formatting to prevent locale-specific decimal separators in UGC output.</violation>
</file>

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

<violation number="1" location="generator/chu/SusGenerator.cs:26">
P1: Format `#BPM_DEF` with invariant culture so decimal output is stable across locales.</violation>
</file>

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Comment thread parser/chu/UgcParser.cs Outdated
Comment thread parser/chu/UgcParser.cs
Comment thread parser/chu/UgcParser.cs
public class UgcParser : IParser<UgcChart>
public class UgcParser: BaseChuParser
{
private static int RSL = 480 * 4;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: RSL is a mutable static field that retains its value between Parse() calls. Parsing a file with a non-default @TICKS will corrupt timing for all subsequently parsed files that don't explicitly declare @TICKS. Reset it at the start of Parse() or make it an instance field.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At parser/chu/UgcParser.cs, line 15:

<comment>`RSL` is a mutable static field that retains its value between `Parse()` calls. Parsing a file with a non-default `@TICKS` will corrupt timing for all subsequently parsed files that don't explicitly declare `@TICKS`. Reset it at the start of `Parse()` or make it an instance field.</comment>

<file context>
@@ -2,16 +2,18 @@
-public class UgcParser : IParser<UgcChart>
+public class UgcParser: BaseChuParser
 {
+    private static int RSL = 480 * 4;
+    
     private static readonly Dictionary<string, string> AirDirections = new()
</file context>

foreach (var b in chart.BpmList)
{
var (m, o) = Utils.BarAndTick(b.Time, RSL);
sb.AppendLine($"BPM\t{m}\t{o}\t{b.Bpm:0.000}");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Decimal output is locale-dependent; C2S serialization should use invariant formatting for BPM/SFL/BPM_DEF values.

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

<comment>Decimal output is locale-dependent; C2S serialization should use invariant formatting for BPM/SFL/BPM_DEF values.</comment>

<file context>
@@ -1,125 +1,119 @@
+        foreach (var b in chart.BpmList)
+        {
+            var (m, o) = Utils.BarAndTick(b.Time, RSL);
+            sb.AppendLine($"BPM\t{m}\t{o}\t{b.Bpm:0.000}");
+        }
+
</file context>

Comment thread parser/chu/SusParser.cs
public class SusParser : IParser<SusChart>
public class SusParser: BaseChuParser
{
private static int RSL = 480 * 4;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Using a mutable static RSL leaks timing resolution across parse calls, so one chart’s REQUEST can corrupt timing in later charts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At parser/chu/SusParser.cs, line 16:

<comment>Using a mutable `static` `RSL` leaks timing resolution across parse calls, so one chart’s `REQUEST` can corrupt timing in later charts.</comment>

<file context>
@@ -10,8 +11,10 @@ namespace MuConvert.chu;
-public class SusParser : IParser<SusChart>
+public class SusParser: BaseChuParser
 {
+    private static int RSL = 480 * 4;
+    
     private static readonly Dictionary<int, string> TypeMap = new()
</file context>

Comment thread chart/chu/ChuChart.cs
Comment thread parser/chu/C2sParser.cs
Comment thread parser/chu/C2sParser.cs Outdated
{
case "BPM":
chart.BpmEvents.Add((Int(p, 1), Int(p, 2), Dbl(p, 3, 120.0)));
chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), decimal.Parse(p[3])));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Use culture-invariant, validated decimal parsing for BPM/SFL values to avoid locale-dependent parse failures and unhandled exceptions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At parser/chu/C2sParser.cs, line 79:

<comment>Use culture-invariant, validated decimal parsing for BPM/SFL values to avoid locale-dependent parse failures and unhandled exceptions.</comment>

<file context>
@@ -49,44 +54,47 @@ public class C2sParser : IParser<C2sChart>
         {
             case "BPM":
-                chart.BpmEvents.Add((Int(p, 1), Int(p, 2), Dbl(p, 3, 120.0)));
+                chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), decimal.Parse(p[3])));
                 break;
             case "MET":
</file context>
Suggested change
chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), decimal.Parse(p[3])));
chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), p.Length > 3 && decimal.TryParse(p[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var bpm) ? bpm : 120m));

Comment thread generator/chu/C2sGenerator.cs
Comment thread README.md
@Starrah Starrah force-pushed the refactor-chunithm branch from 28bfe8c to d580e85 Compare May 7, 2026 18:42
@Applesaber
Copy link
Copy Markdown

关于不同版本UGC,在 Air-Crush (:C:T) 语句上的不同 分析样本包括:UGC @ver 版本4、6、7、8的谱面各一个

v8文档

https://gist.github.com/inonote/5c01e73781cab17765a1d93641d52298

结论摘要

v4 / v6 / v7 ,也就是v8之前,都是相同的;然而遗憾的是,未能找到相关文档。我尽力找了,但网上只有v8的文档。

  • 包括Air Crush在内的各种Air音符,高度属性 height 的值均为 1 位。

    • 例如,CD380,拆解为C(AirCrush) + D3(cell&width) + 8(高度) + 0(颜色)
  • 跟随行机制:在 :C 音符的后面,可观察到 >c>s 两种子音符写法都是有的。

    • 其中,对>s的情况,有可能会出现只有单个字母 >s、而没有cell/width/height三位参数的情况。但>c时则必定后面又有cell/width/height参数数
  • v8版本之前的谱面中,还存在:T类型的音符。这种类型的音符在v8中已经完全不存在了,因此其含义并不清楚,只有一些观察:

    • 一些情况下,:T会与:C伴生出现、时刻相同。但并不绝对。
    • 在这种情况下,往往是:C先出现、跟随行为>c,然后出现:T、跟随行为>s。尚不清楚他们之间有什么关联和关系。
    • :T也是可以单独、不依附于:C出现的。单独出现时的含义不明确,也没发现什么规律。

v8相比于前代,在Air Crush方面则引入了重大变更:• Air-Crush 升级为长格式并引入间隔:C x w hh c , {interval}:

  • 高度属性 height 的值变为 2 位。根据文档说明,2位数整体理解成36进制数后,除以10,即为前代意义下的高度。相当于把精度扩大了10倍。

  • 新增 ,{interval}

    • 这是最大的变更,也是制约我把AirCrush的解析写出来的最根本原因。我不知道这个interval的含义是什么、与原来的语法和C2S ALD中的参数的对应是什么。
    • 根据观察,样本中常见的interval的值是 0$这两个特殊值。日文文档中有提到它们是特殊值,但我不能理解其实际意义。
  • 在上述新增interval的基础上,跟随行不再有>s了,而是有且仅有一行>c

  • :T音符类型被移除。文档里,和谱面中,都找不到了。怀疑是被interval吸收了。

UMIGURI 2.0 已经做了 v1.0 的迁移,所以只支持 v8 应该就行了

一、高度值

C2S 字段布局确认(12 字段)

ALD  [measure]  [offset]  [cell]  [width]  [unknown=0]  [start_h]  [duration]  [end_cell]  [end_width]  [target_h]  [color]
ASD  [measure]  [offset]  [cell]  [width]  [target_note] [start_h]  [duration]  [end_cell]  [end_width]  [target_h]  [color]

示例:

ALD	5	0	8	2	0	1.0	768	9	2	1.0	CYN
ALD	7	336	1	2	0	2.0	192	0	2	15.0	PPL
ASD	85	0	12	4	CHR	5.0	96	10	4	5.0	DEF

高度是浮点数,精度 0.1。ASD 绝大多数为 5.0,ALD 范围 1.0~22.5。

UGC v8 高度编码

参考 inonote 规范(https://gist.github.com/inonote/5c01e73781cab17765a1d93641d52298):

  • Air Slide S: #BarTick:txwhhc,hh = base-36 两位,值 = 原始值 × 10
  • Air Crush C: #BarTick:txwhhc,{interval},同上
    示例:高度 8.0 → 8.0×10=80 → base-36 "28"(2×36+8=80)

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