Skip to content

在选曲分类界面添加了定数分类的效果#133

Merged
clansty merged 10 commits into
MuNET-OSS:mainfrom
MasterLiushi:main
Jun 1, 2026
Merged

在选曲分类界面添加了定数分类的效果#133
clansty merged 10 commits into
MuNET-OSS:mainfrom
MasterLiushi:main

Conversation

@MasterLiushi
Copy link
Copy Markdown
Contributor

@MasterLiushi MasterLiushi commented May 29, 2026

在原先的选曲分类界面添加了定数分类这一档,位置在全曲分类后,用来排序各个详细定数包含的歌曲。可能会方便选对应定数的歌曲。

Summary by Sourcery

在乐曲选择界面中新增基于常数难度的排序标签页,并将其集成进现有的排序和 UI 流程中。

新功能:

  • 在乐曲选择界面中引入可配置的常数难度排序标签页,放置在现有排序标签页旁。
  • 允许根据详细的常数值将乐曲分组到不同类别中,并将这些类别以可选择的标签页形式暴露给用户。
  • 通过外部资源包文件为常数类别提供自定义标签页贴图。

增强内容:

  • 将新的常数难度标签页集成到标签导航、类别生成以及 UI 显示逻辑中,包括为每个常数区间设置对应的标签文本和颜色。
Original summary in English

Summary by Sourcery

Add a new constant-based difficulty sort tab to the music selection screen and integrate it into existing sorting and UI flows.

New Features:

  • Introduce a configurable constant-difficulty sort tab in the music selection interface, positioned alongside existing sort tabs.
  • Allow grouping of songs into categories based on detailed constant values and expose those categories as selectable tabs.
  • Support custom tab sprites for constant categories via external asset bundle files.

Enhancements:

  • Integrate the new constant-difficulty tab into tab navigation, category generation, and UI display logic, including labels and colors for each constant band.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 29, 2026

审阅者指南

在乐曲选择界面中新增一个“常数难度”排序标签页,并将其接入配置、排序/标签导航、分类分组和 UI,从而可以按谱面常数浏览乐曲,并可选使用自定义标签精灵。

乐曲选择中常数难度排序标签的时序图

sequenceDiagram
    actor Player
    participant MusicSelectMonitor
    participant MusicSelectProcess
    participant SongConstantSort as SongConstantSort_CategoryTabPatches
    participant ConstNameStore
    participant NotesListManager as NotesListManager_Singleton
    participant DataManager as DataManager_Singleton
    participant ConstSpriteCache

    Player->>MusicSelectProcess: AddSort(root=Tab)
    MusicSelectProcess-->>MusicSelectProcess: _beforeCategorySortSetting++ (wrap to 0-6)

    Player->>MusicSelectProcess: CategoryTabSort(player)
    MusicSelectProcess->>SongConstantSort: CategoryTabSort_Prefix(player)
    SongConstantSort-->>MusicSelectProcess: return DoCategoryTabConstant(player)

    Note over SongConstantSort,NotesListManager: Build constant-based categories
    SongConstantSort->>NotesListManager_Singleton: GetNotesList()
    NotesListManager_Singleton-->>SongConstantSort: notes dict
    SongConstantSort-->>ConstNameStore: NameMap[constKey] = "Lv.x.y"

    Player->>MusicSelectMonitor: getTabString(data)
    MusicSelectMonitor->>MusicSelectMonitor: GetCategorySortSetting()
    MusicSelectMonitor->>ConstNameStore: NameMap.TryGetValue(data.categoryID)
    ConstNameStore-->>MusicSelectMonitor: name
    MusicSelectMonitor-->>Player: tab label (e.g. Lv.13.7)

    Player->>MusicSelectMonitor: getTabColor(data)
    MusicSelectMonitor->>MusicSelectMonitor: GetCategorySortSetting()
    MusicSelectMonitor->>DataManager_Singleton: GetMusicLevel(levelEnum)
    DataManager_Singleton-->>MusicSelectMonitor: levelData
    MusicSelectMonitor-->>Player: tab Color

    Player->>MusicSelectMonitor: GetTabSprite(data)
    MusicSelectMonitor->>MusicSelectMonitor: GetCategorySortSetting()
    MusicSelectMonitor->>ConstSpriteCache: GetSprite(data.categoryID)
    ConstSpriteCache-->>MusicSelectMonitor: custom Sprite or null
    MusicSelectMonitor-->>Player: tab Sprite (custom or default)
Loading

文件级变更

变更 详情 文件
引入 SongConstantSort 模组,新增一个常数难度排序标签 ID,并将其集成到排序标签的枚举与导航逻辑中。
  • 在乐曲选择界面配置段下注册新的 GameSystem.SongConstantSort 配置项,以便在 AquaMai 中开关此功能。
  • 补丁修改 DB.SortTabIDEnum 方法(GetEnd、IsValid、GetName、GetDetail、GetFilePath、GetEnumName、GetEnumValue),使新的标签 ID 6 表示常数排序标签,并使用合适的标签文案与资源。
  • 补丁修改 MusicSelectProcess.AddSort/SubSort,在导航排序标签时循环遍历 0–6 号标签 ID,将新的常数排序标签包含在内。
AquaMai/configSort.yaml
AquaMai.Mods/GameSystem/SongConstantSort.cs
在常数排序标签激活时,为乐曲选择流程实现基于常数的分类分组与命名。
  • 拦截 MusicSelectProcess.CategoryTabSort,当当前排序标签为常数标签时,构建一个新的 SortedList,使用由 notes.level 和 notes.levelDecimal 推导出的常数值作为键。
  • 遍历组合后的乐曲数据、谱面列表以及难度等级,为每个 (musicId, difficulty) 生成唯一键,并按计算出的常数值分组,同时保留 msDetailData 中的字段,如 musicId 和 difficultyId。
  • 向 ConstNameStore.NameMap 填充每个常数的展示名称(例如 “Lv.X.Y”),并更新内部的 CategoryNameList,使这些名称作为分类标签对外暴露。
  • 通过反射调用 SetSortList 和 AddRandomData,把已有的私有排序和随机化逻辑复用到每个常数桶上,然后返回分组结果。
AquaMai.Mods/GameSystem/SongConstantSort.cs
更新乐曲选择 UI,在新的常数排序分类下显示基于常数的标签、颜色以及可选的自定义精灵。
  • 添加一个精灵目录配置(Mods\SongConstantSort\Sprites)和惰性加载缓存,在启动时扫描 .ab 资源包,并根据资源名解析常数 ID,将精灵映射到对应常数。
  • 为 ConstNameStore 提供共享的名称和精灵查找功能,以及一个 IsActive 标志,允许在常数排序前后进行切换。
  • 补丁修改 MusicSelectMonitor.getTabString/getTabColor/GetTabSprite,当选择的是常数标签且条目不是 extra 时,从 ConstNameStore.NameMap 读取标签文案,将常数键映射到 level 枚举以从 DataManager 派生颜色,并可选从精灵缓存中加载自定义精灵。
AquaMai.Mods/GameSystem/SongConstantSort.cs

提示与命令

与 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 new "constant difficulty" sort tab to the music selection screen, wiring it into config, sort/tab navigation, category grouping, and UI so songs can be browsed by chart constant with optional custom tab sprites.

Sequence diagram for constant difficulty sort tab on music selection

sequenceDiagram
    actor Player
    participant MusicSelectMonitor
    participant MusicSelectProcess
    participant SongConstantSort as SongConstantSort_CategoryTabPatches
    participant ConstNameStore
    participant NotesListManager as NotesListManager_Singleton
    participant DataManager as DataManager_Singleton
    participant ConstSpriteCache

    Player->>MusicSelectProcess: AddSort(root=Tab)
    MusicSelectProcess-->>MusicSelectProcess: _beforeCategorySortSetting++ (wrap to 0-6)

    Player->>MusicSelectProcess: CategoryTabSort(player)
    MusicSelectProcess->>SongConstantSort: CategoryTabSort_Prefix(player)
    SongConstantSort-->>MusicSelectProcess: return DoCategoryTabConstant(player)

    Note over SongConstantSort,NotesListManager: Build constant-based categories
    SongConstantSort->>NotesListManager_Singleton: GetNotesList()
    NotesListManager_Singleton-->>SongConstantSort: notes dict
    SongConstantSort-->>ConstNameStore: NameMap[constKey] = "Lv.x.y"

    Player->>MusicSelectMonitor: getTabString(data)
    MusicSelectMonitor->>MusicSelectMonitor: GetCategorySortSetting()
    MusicSelectMonitor->>ConstNameStore: NameMap.TryGetValue(data.categoryID)
    ConstNameStore-->>MusicSelectMonitor: name
    MusicSelectMonitor-->>Player: tab label (e.g. Lv.13.7)

    Player->>MusicSelectMonitor: getTabColor(data)
    MusicSelectMonitor->>MusicSelectMonitor: GetCategorySortSetting()
    MusicSelectMonitor->>DataManager_Singleton: GetMusicLevel(levelEnum)
    DataManager_Singleton-->>MusicSelectMonitor: levelData
    MusicSelectMonitor-->>Player: tab Color

    Player->>MusicSelectMonitor: GetTabSprite(data)
    MusicSelectMonitor->>MusicSelectMonitor: GetCategorySortSetting()
    MusicSelectMonitor->>ConstSpriteCache: GetSprite(data.categoryID)
    ConstSpriteCache-->>MusicSelectMonitor: custom Sprite or null
    MusicSelectMonitor-->>Player: tab Sprite (custom or default)
Loading

File-Level Changes

Change Details Files
Introduce SongConstantSort mod that adds a new constant-difficulty sort tab ID and integrates it into sort tab enumeration and navigation logic.
  • Register new GameSystem.SongConstantSort config option under the song selection screen section so the feature can be toggled in AquaMai.
  • Patch DB.SortTabIDEnum methods (GetEnd, IsValid, GetName, GetDetail, GetFilePath, GetEnumName, GetEnumValue) so a new tab with ID 6 represents the constant sort tab with appropriate label and resources.
  • Patch MusicSelectProcess.AddSort/SubSort to cycle through tab IDs 0–6, including the new constant sort tab, when navigating sort tabs.
AquaMai/configSort.yaml
AquaMai.Mods/GameSystem/SongConstantSort.cs
Implement constant-based category grouping and naming for the music selection process when the constant sort tab is active.
  • Intercept MusicSelectProcess.CategoryTabSort and, when the active sort tab is the constant tab, build a new SortedList keyed by constant values derived from notes.level and notes.levelDecimal.
  • Iterate over combined music data, notes list, and difficulty levels, generating unique keys per (musicId, difficulty), and group entries by computed constant value while preserving msDetailData fields such as musicId and difficultyId.
  • Populate ConstNameStore.NameMap with per-constant display names (e.g., "Lv.X.Y") and update the internal CategoryNameList to expose those names as the category labels.
  • Reuse existing private sorting and randomization behavior by reflecting and invoking SetSortList and AddRandomData for each constant bucket before returning the grouped result.
AquaMai.Mods/GameSystem/SongConstantSort.cs
Update music selection UI to show constant-based labels, colors, and optional custom sprites for the new constant sort categories.
  • Add a sprite directory config (Mods\SongConstantSort\Sprites) and lazy-loading cache that scans .ab bundles at startup and maps sprites to constant IDs parsed from asset names.
  • Provide ConstNameStore with shared name and sprite lookup plus an IsActive flag that can be toggled around constant sorting.
  • Patch MusicSelectMonitor.getTabString/getTabColor/GetTabSprite to, when the constant tab is selected and the entry is not extra, use ConstNameStore.NameMap for labels, map constant keys to level enums to derive colors from DataManager, and optionally use custom sprites loaded from the sprite cache.
AquaMai.Mods/GameSystem/SongConstantSort.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.

Hey - 我发现了两个问题,并留下了一些整体性的反馈:

  • 新增标签页 ID 和边界相关的地方有不少硬编码的魔法数字(例如在 SortTabPatchesNavigationPatches 以及检查 CategorySortSetting 时用到的 67)。这会导致行为在枚举或标签数量变化时变得非常脆弱;建议基于 DB.SortTabID 把这些值集中到一个常量或辅助方法中,以减少未来改动时出错的风险。
  • DoCategoryTabConstant 中,你对 cd.msDetailData 做了原地修改(设置 musicIddifficultyId),然后把它传入新的 CombineMusicSelectData 实例。如果 msDetailData 在不同分类或不同调用之间是共享的,这种就地修改可能会引入很隐蔽的 bug。更安全的做法是在每个条目上克隆或新建一个 detail 对象,而不是复用并修改现有的对象。
  • ConstSpriteCache.GetSprite 中加载 AssetBundle 的逻辑,只在 foreach 期间保持每个 bundle 打开,但在第一次加载之后没有对 _sprites 做空值检查,而且吞掉了所有类型的异常。如果能收紧异常处理范围,并为 bundle 使用 using / try-finally 模式,会让它在面对部分失败或资源损坏时更加健壮。
给 AI Agents 的提示
请根据这次代码审查中的评论进行修改:

## 整体性评论
- 新增标签页 ID 和边界相关的地方有不少硬编码的魔法数字(例如在 `SortTabPatches``NavigationPatches` 以及检查 `CategorySortSetting` 时用到的 `6``7`)。这会导致行为在枚举或标签数量变化时变得非常脆弱;建议基于 `DB.SortTabID` 把这些值集中到一个常量或辅助方法中,以减少未来改动时出错的风险。
-`DoCategoryTabConstant` 中,你对 `cd.msDetailData` 做了原地修改(设置 `musicId``difficultyId`),然后把它传入新的 `CombineMusicSelectData` 实例。如果 `msDetailData` 在不同分类或不同调用之间是共享的,这种就地修改可能会引入很隐蔽的 bug。更安全的做法是在每个条目上克隆或新建一个 detail 对象,而不是复用并修改现有的对象。
-`ConstSpriteCache.GetSprite` 中加载 AssetBundle 的逻辑,只在 `foreach` 期间保持每个 bundle 打开,但在第一次加载之后没有对 `_sprites` 做空值检查,而且吞掉了所有类型的异常。如果能收紧异常处理范围,并为 bundle 使用 `using` / `try-finally` 模式,会让它在面对部分失败或资源损坏时更加健壮。

## 逐条评论

### 评论 1
<location path="AquaMai.Mods/GameSystem/SongConstantSort.cs" line_range="90" />
<code_context>
+    [HarmonyPatch]
+    public static class SortTabPatches
+    {
+        [HarmonyPatch(typeof(DB.SortTabIDEnum), "GetEnd", new System.Type[0])]
+        [HarmonyPrefix]
+        public static bool GetEnd_Static(ref int __result) { __result = 7; return false; }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 对标签索引 `6`/`7` 的硬编码让补丁与当前枚举布局高度耦合。

依赖 `6`/`7` 等于假设枚举的顺序和数量永远不会变化;如果未来 `DB.SortTabID` 被重新排序或扩展,标签文本和导航可能会以不明显的方式出现问题。更建议从枚举中推导这些值(例如使用 `Enum.GetValues`,或引入专门的 `SortTabID.Constant`),或者把它们集中到一个常量里,这样未来枚举变更时只需要改一个地方。

建议实现:

```csharp
    [HarmonyPatch]
    public static class SortTabPatches
    {
        /// <summary>
        /// Maximum underlying value of DB.SortTabID. Keeps patches resilient to enum reordering/extensions.
        /// </summary>
        private static readonly int MaxSortTabIndex = GetMaxSortTabIndex();

        private static int GetMaxSortTabIndex()
        {
            var values = (DB.SortTabID[])Enum.GetValues(typeof(DB.SortTabID));
            var max = int.MinValue;
            foreach (var value in values)
            {
                var intValue = (int)value;
                if (intValue > max)
                    max = intValue;
            }

            return max;
        }

```

```csharp
        [HarmonyPatch(typeof(DB.SortTabIDEnum), "GetEnd", new System.Type[0])]
        [HarmonyPrefix]
        public static bool GetEnd_Static(ref int __result)
        {
            __result = MaxSortTabIndex + 1;
            return false;
        }

```

```csharp
        [HarmonyPatch(typeof(DB.SortTabIDEnum), "GetEnd", new System.Type[] { typeof(DB.SortTabID) })]
        [HarmonyPrefix]
        public static bool GetEnd_Instance(ref int __result)
        {
            __result = MaxSortTabIndex + 1;
            return false;
        }

```

```csharp
        [HarmonyPatch(typeof(DB.SortTabIDEnum), "IsValid")]
        [HarmonyPrefix]
        public static bool IsValid(DB.SortTabID self, ref bool __result)
        {
            __result = self >= DB.SortTabID.Genre && (int)self <= MaxSortTabIndex;

```

如果 `DB.SortTabID` 中包含不打算作为用户可见标签的值(例如内部用或哨兵值),你可能需要调整 `GetMaxSortTabIndex`,只在特定区间(例如从 `Genre` 到某个特定最后标签)内筛选出真正需要的枚举值,而不是直接采用枚举的最大值。
</issue_to_address>

### 评论 2
<location path="AquaMai.Mods/GameSystem/SongConstantSort.cs" line_range="281" />
<code_context>
+            return result;
+        }
+
+        private static void InitReflection()
+        {
+            if (_combineMusicDataListField == null)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 反射初始化没有处理成员缺失的情况,在游戏更新后可能会静默损坏。

这些反射调用假设 `_combineMusicDataList`、`CategoryNameList`、`SetSortList` 和 `AddRandomData` 一定存在。如果未来某次游戏更新重命名或删除了其中任意一个,字段会保持为 `null`,并在之后某处抛出 `NullReferenceException`,距离根因很远。建议在某个统一入口(例如 `OnBeforePatch` 或 `InitReflection`)一次性校验所有反射查找,并在失败时打印清晰的错误日志,使版本不匹配问题更明显,同时避免难以排查的崩溃。

建议实现:

```csharp
using System.Reflection;
using UnityEngine;

```

```csharp
        private static void InitReflection()
        {
            // 预先解析所有反射目标,以便统一校验
            var type = typeof(Process.MusicSelectProcess);

            _combineMusicDataListField = type.GetField(
                "_combineMusicDataList", BindingFlags.NonPublic | BindingFlags.Instance);

            _categoryNameListProp = type.GetProperty(
                "CategoryNameList", BindingFlags.Public | BindingFlags.Instance);

            _setSortListMethod = type.GetMethod(
                "SetSortList", BindingFlags.NonPublic | BindingFlags.Instance);

            _addRandomDataMethod = type.GetMethod(
                "AddRandomData", BindingFlags.NonPublic | BindingFlags.Instance);

            // 校验所有成员是否成功获取;如果没有,记录日志并用清晰错误快速失败
            var missingMembers = new List<string>();
            if (_combineMusicDataListField == null)
                missingMembers.Add("_combineMusicDataList (Field, NonPublic Instance)");
            if (_categoryNameListProp == null)
                missingMembers.Add("CategoryNameList (Property, Public Instance)");
            if (_setSortListMethod == null)
                missingMembers.Add("SetSortList (Method, NonPublic Instance)");
            if (_addRandomDataMethod == null)
                missingMembers.Add("AddRandomData (Method, NonPublic Instance)");

            if (missingMembers.Count > 0)
            {
                var message =
                    "[SongConstantSort] Failed to initialize reflection bindings for Process.MusicSelectProcess. " +
                    "Missing members: " + string.Join(", ", missingMembers) +
                    ". The game may have been updated; SongConstantSort needs to be updated for this game version.";

                Debug.LogError(message);
                throw new MissingMemberException(message);
            }

```
</issue_to_address>

Sourcery 对开源项目免费——如果你觉得这次审查有帮助,可以考虑分享一下 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进今后的审查。
Original comment in English

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

  • There are a lot of hardcoded magic numbers around the new tab ID and bounds (e.g., 6, 7 in SortTabPatches, NavigationPatches, and checks for CategorySortSetting), which makes the behavior brittle if the enum or tab count changes; consider centralizing these values in a single constant or helper based on DB.SortTabID to avoid future breakage.
  • In DoCategoryTabConstant, you mutate cd.msDetailData (setting musicId and difficultyId) and then pass it into new CombineMusicSelectData instances; if msDetailData is shared across categories or calls, this in-place mutation could cause subtle bugs, so it would be safer to clone or create a fresh detail object per entry instead of reusing and modifying the existing one.
  • The asset bundle loading in ConstSpriteCache.GetSprite keeps each bundle open only for the duration of the foreach but performs no null checks on _sprites after the first load and swallows all exception types; tightening the exception handling and using a using/try-finally pattern for bundles would make this more robust against partial failures or corrupted assets.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- There are a lot of hardcoded magic numbers around the new tab ID and bounds (e.g., `6`, `7` in `SortTabPatches`, `NavigationPatches`, and checks for `CategorySortSetting`), which makes the behavior brittle if the enum or tab count changes; consider centralizing these values in a single constant or helper based on `DB.SortTabID` to avoid future breakage.
- In `DoCategoryTabConstant`, you mutate `cd.msDetailData` (setting `musicId` and `difficultyId`) and then pass it into new `CombineMusicSelectData` instances; if `msDetailData` is shared across categories or calls, this in-place mutation could cause subtle bugs, so it would be safer to clone or create a fresh detail object per entry instead of reusing and modifying the existing one.
- The asset bundle loading in `ConstSpriteCache.GetSprite` keeps each bundle open only for the duration of the foreach but performs no null checks on `_sprites` after the first load and swallows all exception types; tightening the exception handling and using a `using`/`try-finally` pattern for bundles would make this more robust against partial failures or corrupted assets.

## Individual Comments

### Comment 1
<location path="AquaMai.Mods/GameSystem/SongConstantSort.cs" line_range="90" />
<code_context>
+    [HarmonyPatch]
+    public static class SortTabPatches
+    {
+        [HarmonyPatch(typeof(DB.SortTabIDEnum), "GetEnd", new System.Type[0])]
+        [HarmonyPrefix]
+        public static bool GetEnd_Static(ref int __result) { __result = 7; return false; }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Hard-coded tab indices `6`/`7` tightly couple the patch to current enum layout.

Relying on `6`/`7` assumes the enum order and count never change; if `DB.SortTabID` is reordered or extended, tab labels and navigation could break in non-obvious ways. Prefer deriving these values from the enum (e.g., `Enum.GetValues` or a dedicated `SortTabID.Constant`) or centralizing them in a single constant so future enum changes only require updating one place.

Suggested implementation:

```csharp
    [HarmonyPatch]
    public static class SortTabPatches
    {
        /// <summary>
        /// Maximum underlying value of DB.SortTabID. Keeps patches resilient to enum reordering/extensions.
        /// </summary>
        private static readonly int MaxSortTabIndex = GetMaxSortTabIndex();

        private static int GetMaxSortTabIndex()
        {
            var values = (DB.SortTabID[])Enum.GetValues(typeof(DB.SortTabID));
            var max = int.MinValue;
            foreach (var value in values)
            {
                var intValue = (int)value;
                if (intValue > max)
                    max = intValue;
            }

            return max;
        }

```

```csharp
        [HarmonyPatch(typeof(DB.SortTabIDEnum), "GetEnd", new System.Type[0])]
        [HarmonyPrefix]
        public static bool GetEnd_Static(ref int __result)
        {
            __result = MaxSortTabIndex + 1;
            return false;
        }

```

```csharp
        [HarmonyPatch(typeof(DB.SortTabIDEnum), "GetEnd", new System.Type[] { typeof(DB.SortTabID) })]
        [HarmonyPrefix]
        public static bool GetEnd_Instance(ref int __result)
        {
            __result = MaxSortTabIndex + 1;
            return false;
        }

```

```csharp
        [HarmonyPatch(typeof(DB.SortTabIDEnum), "IsValid")]
        [HarmonyPrefix]
        public static bool IsValid(DB.SortTabID self, ref bool __result)
        {
            __result = self >= DB.SortTabID.Genre && (int)self <= MaxSortTabIndex;

```

If `DB.SortTabID` contains values that are not intended to be user-visible tabs (e.g., internal or sentinel values), you may want to adjust `GetMaxSortTabIndex` to filter the enum values to only the relevant subset (for example, between `Genre` and a specific final tab), instead of blindly using the maximum enum value.
</issue_to_address>

### Comment 2
<location path="AquaMai.Mods/GameSystem/SongConstantSort.cs" line_range="281" />
<code_context>
+            return result;
+        }
+
+        private static void InitReflection()
+        {
+            if (_combineMusicDataListField == null)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Reflection setup doesnt handle missing members, which may break silently on game updates.

These reflection calls assume `_combineMusicDataList`, `CategoryNameList`, `SetSortList`, and `AddRandomData` always exist. If a future game update renames or removes any of them, the fields will stay `null` and trigger `NullReferenceException`s later, far from the root cause. Consider validating all reflection lookups once (e.g., in `OnBeforePatch` or `InitReflection`) and logging a clear error on failure to make version mismatches obvious and avoid hard-to-debug crashes.

Suggested implementation:

```csharp
using System.Reflection;
using UnityEngine;

```

```csharp
        private static void InitReflection()
        {
            // Resolve all reflection targets eagerly so we can validate them together
            var type = typeof(Process.MusicSelectProcess);

            _combineMusicDataListField = type.GetField(
                "_combineMusicDataList", BindingFlags.NonPublic | BindingFlags.Instance);

            _categoryNameListProp = type.GetProperty(
                "CategoryNameList", BindingFlags.Public | BindingFlags.Instance);

            _setSortListMethod = type.GetMethod(
                "SetSortList", BindingFlags.NonPublic | BindingFlags.Instance);

            _addRandomDataMethod = type.GetMethod(
                "AddRandomData", BindingFlags.NonPublic | BindingFlags.Instance);

            // Validate that all members were found; if not, log and fail fast with a clear error
            var missingMembers = new List<string>();
            if (_combineMusicDataListField == null)
                missingMembers.Add("_combineMusicDataList (Field, NonPublic Instance)");
            if (_categoryNameListProp == null)
                missingMembers.Add("CategoryNameList (Property, Public Instance)");
            if (_setSortListMethod == null)
                missingMembers.Add("SetSortList (Method, NonPublic Instance)");
            if (_addRandomDataMethod == null)
                missingMembers.Add("AddRandomData (Method, NonPublic Instance)");

            if (missingMembers.Count > 0)
            {
                var message =
                    "[SongConstantSort] Failed to initialize reflection bindings for Process.MusicSelectProcess. " +
                    "Missing members: " + string.Join(", ", missingMembers) +
                    ". The game may have been updated; SongConstantSort needs to be updated for this game version.";

                Debug.LogError(message);
                throw new MissingMemberException(message);
            }

```
</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 AquaMai.Mods/GameSystem/SongConstantSort.cs Outdated
Comment thread AquaMai.Mods/GameSystem/SongConstantSort.cs Outdated
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 a new mod, SongConstantSort, which adds a constant difficulty sorting tab to the music selection screen, and registers it in the configuration file. The review feedback highlights a critical bug where modifying cd.msDetailData directly mutates the shared reference, causing data corruption across other categories; it suggests cloning the object first. Additionally, a performance bottleneck was identified due to retrieving the NotesListManager singleton inside the innermost loop, which should be hoisted outside the loops.

Comment thread AquaMai.Mods/GameSystem/SongConstantSort.cs
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 2 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="AquaMai.Mods/GameSystem/SongConstantSort.cs">

<violation number="1" location="AquaMai.Mods/GameSystem/SongConstantSort.cs:92">
P2: The magic numbers `6` and `7` are scattered across multiple patch classes (`SortTabPatches`, `NavigationPatches`, `UIPatches`) without a single source of truth. If `DB.SortTabID` is extended or reordered, every occurrence must be updated manually. Consider deriving these from the enum (e.g., `Enum.GetValues`) or centralizing them into named constants.</violation>

<violation number="2" location="AquaMai.Mods/GameSystem/SongConstantSort.cs:135">
P1: Unchecked reflection results can cause hard runtime crashes when private member names or signatures change. Add null checks after `GetField`/`GetMethod`/`GetProperty` calls and return `true` (fallback to original behavior) when reflection fails.</violation>

<violation number="3" location="AquaMai.Mods/GameSystem/SongConstantSort.cs:222">
P2: Repeated dictionary retrieval inside the innermost nested loop causes unnecessary overhead during category sorting</violation>

<violation number="4" location="AquaMai.Mods/GameSystem/SongConstantSort.cs:281">
P2: `InitReflection` does not validate that any of the reflection lookups succeeded. If a game update renames or removes any target member, these fields stay `null` and later cause a `NullReferenceException` far from the root cause. Add null-checks after the lookups and log/throw a descriptive error to make version mismatches immediately obvious.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread AquaMai.Mods/GameSystem/SongConstantSort.cs Outdated
Comment thread AquaMai.Mods/GameSystem/SongConstantSort.cs Outdated
int musicId = cd.GetID(sk);
if (musicId >= 100000) continue;

var notesDict = Singleton<NotesListManager>.Instance.GetNotesList();
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: Repeated dictionary retrieval inside the innermost nested loop causes unnecessary overhead during category sorting

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At AquaMai.Mods/GameSystem/SongConstantSort.cs, line 222:

<comment>Repeated dictionary retrieval inside the innermost nested loop causes unnecessary overhead during category sorting</comment>

<file context>
@@ -0,0 +1,381 @@
+                            int musicId = cd.GetID(sk);
+                            if (musicId >= 100000) continue;
+
+                            var notesDict = Singleton<NotesListManager>.Instance.GetNotesList();
+                            if (!notesDict.TryGetValue(musicId, out var nw)) continue;
+
</file context>

Comment thread AquaMai.Mods/GameSystem/SongConstantSort.cs Outdated
Comment thread AquaMai.Mods/GameSystem/SongConstantSort.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.

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="AquaMai.Mods/GameSystem/SongConstantSort.cs">

<violation number="1" location="AquaMai.Mods/GameSystem/SongConstantSort.cs:66">
P1: AssetBundle loaded from stream is never unloaded, causing a persistent native memory leak.</violation>

<violation number="2" location="AquaMai.Mods/GameSystem/SongConstantSort.cs:222">
P2: Repeated dictionary retrieval inside the innermost nested loop causes unnecessary overhead during category sorting</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread AquaMai.Mods/GameSystem/SongConstantSort.cs
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="AquaMai.Mods/GameSystem/SongConstantSort.cs">

<violation number="1" location="AquaMai.Mods/GameSystem/SongConstantSort.cs:222">
P2: Repeated dictionary retrieval inside the innermost nested loop causes unnecessary overhead during category sorting</violation>

<violation number="2" location="AquaMai.Mods/GameSystem/SongConstantSort.cs:289">
P1: Unsafe nested list indexing outside try/catch in IsForceMusicBack prefix creates a crash path during back-navigation state transitions.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment on lines +289 to +300
var musicData = __instance.CombineMusicDataList[__instance.CurrentCategorySelect]
[__instance.CurrentMusicSelect].musicSelectData[(int)__instance.ScoreType];
int musicId = musicData.MusicData.name.id;

// 用 _levelCategoryPositionList 跳转到新难度下的位置
try
{
var pos = __instance.GetLevelToListPositoin(musicId, newDiff);
__instance.CurrentCategorySelect = pos.Category;
__instance.CurrentMusicSelect = pos.Index;
__instance.CurrentDifficulty[__instance.SortDecidePlayer] = (Manager.MusicDifficultyID)newDiff;
}
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: Unsafe nested list indexing outside try/catch in IsForceMusicBack prefix creates a crash path during back-navigation state transitions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At AquaMai.Mods/GameSystem/SongConstantSort.cs, line 289:

<comment>Unsafe nested list indexing outside try/catch in IsForceMusicBack prefix creates a crash path during back-navigation state transitions.</comment>

<file context>
@@ -154,46 +183,127 @@ public static bool SubSort_Prefix(Process.MusicSelectProcess __instance, DB.Sort
+            if (newDiff < 0 || newDiff > 4) return;
+
+            // 获取当前歌曲 ID
+            var musicData = __instance.CombineMusicDataList[__instance.CurrentCategorySelect]
+                [__instance.CurrentMusicSelect].musicSelectData[(int)__instance.ScoreType];
+            int musicId = musicData.MusicData.name.id;
</file context>
Suggested change
var musicData = __instance.CombineMusicDataList[__instance.CurrentCategorySelect]
[__instance.CurrentMusicSelect].musicSelectData[(int)__instance.ScoreType];
int musicId = musicData.MusicData.name.id;
// 用 _levelCategoryPositionList 跳转到新难度下的位置
try
{
var pos = __instance.GetLevelToListPositoin(musicId, newDiff);
__instance.CurrentCategorySelect = pos.Category;
__instance.CurrentMusicSelect = pos.Index;
__instance.CurrentDifficulty[__instance.SortDecidePlayer] = (Manager.MusicDifficultyID)newDiff;
}
try
{
var musicData = __instance.CombineMusicDataList[__instance.CurrentCategorySelect]
[__instance.CurrentMusicSelect].musicSelectData[(int)__instance.ScoreType];
int musicId = musicData.MusicData.name.id;
var pos = __instance.GetLevelToListPositoin(musicId, newDiff);
__instance.CurrentCategorySelect = pos.Category;
__instance.CurrentMusicSelect = pos.Index;
__instance.CurrentDifficulty[__instance.SortDecidePlayer] = (Manager.MusicDifficultyID)newDiff;
}
catch { }

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.

毕竟也一起看了半天,留个名(?)

MasterLiushi and others added 5 commits May 31, 2026 21:26
具体而言,当AddSort尝试切换超出范围时,官方游戏是会Clamp固定在最边上、不会循环回开头;但这个patch下,会使得行为变成循环。
然后,修复上述问题、改为和原版游戏行为一致后,会发现:`SubSort_Prefix`函数实际上没有作用、patch后的版本代码和原版游戏完全相同。因此直接删除即可。
AddSort中所引用的变量,在小于1.65的版本中为`_categorySortSetting`,没有`_before`前缀。
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="AquaMai.Mods/GameSystem/SongConstantSort.cs">

<violation number="1" location="AquaMai.Mods/GameSystem/SongConstantSort.cs:222">
P2: Repeated dictionary retrieval inside the innermost nested loop causes unnecessary overhead during category sorting</violation>

<violation number="2" location="AquaMai.Mods/GameSystem/SongConstantSort.cs:289">
P1: Unsafe nested list indexing outside try/catch in IsForceMusicBack prefix creates a crash path during back-navigation state transitions.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread AquaMai.Mods/GameSystem/SongConstantSort.cs
@clansty clansty merged commit 2d6056f into MuNET-OSS:main Jun 1, 2026
4 checks passed
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.

3 participants