Skip to content

feat: JudgeDisplayPro - #140

Open
clansty wants to merge 20 commits into
mainfrom
feat/JudgeDisplayPro
Open

feat: JudgeDisplayPro#140
clansty wants to merge 20 commits into
mainfrom
feat/JudgeDisplayPro

Conversation

@clansty

@clansty clansty commented Aug 3, 2026

Copy link
Copy Markdown
Member

Review in cubic


Note

Medium Risk
Wide Harmony surface on judge rendering, scoring, and result IL; incorrect patches could skew FAST/LATE stats or break on game updates, though behavior is gated by per-player enable flags.

Overview
Adds JudgeDisplayPro, a per-player mod that overrides tap, touch, slide, and break judge sprites and visibility from new in-game option rows, with settings persisted across music select.

Gameplay integration: Harmony hooks on JudgeGrade, SlideJudge, and related paths apply display modes (judge-only, FAST/LATE, colored, hidden) and critical-perfect rules; touch judges get monitor index via thread-local state from parent notes, slides via SlideRoot bindings. Score handling sets DispJudge from critical mode, adjusts FAST/LATE counts when timing is shown, and transpiles ResultProcess.OnStart to read judge display from GameScoreList instead of raw user data.

Infrastructure: GameSettingsManager patches under a lock; new GameSettingsManagerSprites patches MusicSelectProcess.GetOptionValueSprite for custom option preview sprites from a registered asset bundle (judgedisplaypro embedded resource). SongConstantSort drops unused compressed level.ab fallback; level.ab path moves under Resources/.

Reviewed by Cursor Bugbot for commit 9d0a300. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by Sourcery

引入可配置的 JudgeDisplayPro 用户体验(UX)Mod,用于自定义游戏内判定视觉效果,并让分数/统计信息显示与画面中显示的内容保持一致。

新功能:

  • 新增 JudgeDisplayPro Mod,支持为每位玩家单独设置,控制 PERFECT(含 break)、GREAT、GOOD 以及 critical(“大P”)判定的可见性和显示样式。
  • 将 JudgeDisplayPro 集成到游戏选项 UI 中,包括通过 GameSettingsManagerSprites 从内嵌的 asset bundle 加载自定义选项贴图(sprites)。
  • 使用基于存储的 UserSettings 模型,在不同游戏会话之间为每位玩家持久化 JudgeDisplayPro 设置。

改进:

  • 通过在首次应用补丁时使用锁进行保护,使 GameSettingsManager 的补丁注册线程安全。
  • 调整点击(tap)、触摸(touch)、长按(hold)和滑条(slide)音符的游戏内判定渲染,使 JudgeDisplayPro 设置决定显示哪些判定/判定时机贴图。
  • 通过对 GameScoreList 打补丁以及对 ResultProcess.OnStart 进行 transpile,使 FAST/LATE 计数器和结算画面的判定显示与 JudgeDisplayPro 行为保持一致。

构建:

  • 将新的 judgedisplaypro asset bundle 注册为内嵌资源,并通过移除未使用的 compressed level.ab 回退选项,简化 SongConstantSort asset bundle 的加载逻辑。
Original summary in English

Summary by Sourcery

Introduce a configurable JudgeDisplayPro UX mod that customizes in-game judge visuals and keeps score/stat displays consistent with what is shown.

New Features:

  • Add JudgeDisplayPro mod with per-player settings to control visibility and style of PERFECT (including break), GREAT, GOOD, and critical (“大P”) judgments.
  • Integrate JudgeDisplayPro with the game options UI, including custom option sprites loaded from an embedded asset bundle via GameSettingsManagerSprites.
  • Persist JudgeDisplayPro settings per player across sessions using a storage-backed UserSettings model.

Enhancements:

  • Make GameSettingsManager patch registration thread-safe by guarding first-time patch application with a lock.
  • Adjust gameplay judge rendering for tap, touch, hold, and slide notes so JudgeDisplayPro settings drive which judge/timing sprites appear.
  • Align FAST/LATE counters and result screen judge display with JudgeDisplayPro behavior by patching GameScoreList and transpiling ResultProcess.OnStart.

Build:

  • Register the new judgedisplaypro asset bundle as an embedded resource and simplify SongConstantSort asset bundle loading by removing the unused compressed level.ab fallback.
Original summary in English

Summary by Sourcery

引入可配置的 JudgeDisplayPro 用户体验(UX)Mod,用于自定义游戏内判定视觉效果,并让分数/统计信息显示与画面中显示的内容保持一致。

新功能:

  • 新增 JudgeDisplayPro Mod,支持为每位玩家单独设置,控制 PERFECT(含 break)、GREAT、GOOD 以及 critical(“大P”)判定的可见性和显示样式。
  • 将 JudgeDisplayPro 集成到游戏选项 UI 中,包括通过 GameSettingsManagerSprites 从内嵌的 asset bundle 加载自定义选项贴图(sprites)。
  • 使用基于存储的 UserSettings 模型,在不同游戏会话之间为每位玩家持久化 JudgeDisplayPro 设置。

改进:

  • 通过在首次应用补丁时使用锁进行保护,使 GameSettingsManager 的补丁注册线程安全。
  • 调整点击(tap)、触摸(touch)、长按(hold)和滑条(slide)音符的游戏内判定渲染,使 JudgeDisplayPro 设置决定显示哪些判定/判定时机贴图。
  • 通过对 GameScoreList 打补丁以及对 ResultProcess.OnStart 进行 transpile,使 FAST/LATE 计数器和结算画面的判定显示与 JudgeDisplayPro 行为保持一致。

构建:

  • 将新的 judgedisplaypro asset bundle 注册为内嵌资源,并通过移除未使用的 compressed level.ab 回退选项,简化 SongConstantSort asset bundle 的加载逻辑。
Original summary in English

Summary by Sourcery

Introduce a configurable JudgeDisplayPro UX mod that customizes in-game judge visuals and keeps score/stat displays consistent with what is shown.

New Features:

  • Add JudgeDisplayPro mod with per-player settings to control visibility and style of PERFECT (including break), GREAT, GOOD, and critical (“大P”) judgments.
  • Integrate JudgeDisplayPro with the game options UI, including custom option sprites loaded from an embedded asset bundle via GameSettingsManagerSprites.
  • Persist JudgeDisplayPro settings per player across sessions using a storage-backed UserSettings model.

Enhancements:

  • Make GameSettingsManager patch registration thread-safe by guarding first-time patch application with a lock.
  • Adjust gameplay judge rendering for tap, touch, hold, and slide notes so JudgeDisplayPro settings drive which judge/timing sprites appear.
  • Align FAST/LATE counters and result screen judge display with JudgeDisplayPro behavior by patching GameScoreList and transpiling ResultProcess.OnStart.

Build:

  • Register the new judgedisplaypro asset bundle as an embedded resource and simplify SongConstantSort asset bundle loading by removing the unused compressed level.ab fallback.

clansty added 8 commits July 12, 2026 10:42
- 通过 SlideRoot.SetJudgeObject 登记 monitor,覆盖普通 Slide 与 SlideFan 的对象池复用
- 修正 Break/非 Break 的 CRITICAL 矩阵及 Slide PERFECT 的 timing 素材映射
- 每次判定复位 FAST/LATE 和附加图层,并保持判定位置 OFF 时隐藏
- 按 PERFECT/GREAT/GOOD 各自显示模式统计 FAST/LATE,防止重复判定和 TrackSkip 误计
- 保留 GameScoreList 单曲快照的大 P 分桶,并让主结算读取同一份快照
- 限制设置项增减边界,并按枚举名称反序列化玩家设置
@gemini-code-assist

This comment was marked as low quality.

@sourcery-ai

sourcery-ai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Reviewer's Guide

介绍 JudgeDisplayPro 用户体验模组,将按玩家的判定显示配置接入现有设置系统、游戏中判定渲染(tap/touch/slide)、FAST/LATE 分数统计、结算流程以及选项预览贴图,同时收紧补丁应用与 AssetBundle 加载逻辑。

通过 GameScoreList.SetResult 更新后的 FAST/LATE 统计时序图

sequenceDiagram
    participant GameLogic
    participant GameScoreList
    participant JudgeDisplayPro
    participant Logic
    participant NotesManager

    GameLogic->>GameScoreList: SetResult(index, timing)
    activate GameScoreList

    GameScoreList->>JudgeDisplayPro: PreGameScoreListSetResult(__instance, index, timing, monitorIndex, out FastLateState)
    activate JudgeDisplayPro
    JudgeDisplayPro->>NotesManager: Instance(monitorIndex)
    NotesManager-->>JudgeDisplayPro: getReader().GetNoteList()[index]
    JudgeDisplayPro->>Logic: ShouldCountFastLate(settings, timing, isBreak)
    Logic-->>JudgeDisplayPro: bool
    JudgeDisplayPro-->>GameScoreList: FastLateState (saved as __state)
    deactivate JudgeDisplayPro

    GameScoreList-->>GameScoreList: original SetResult logic

    GameScoreList->>JudgeDisplayPro: PostGameScoreListSetResult(__instance, __state)
    activate JudgeDisplayPro
    JudgeDisplayPro->>GameScoreList: setFast(__instance, newFast)
    JudgeDisplayPro->>GameScoreList: setLate(__instance, newLate)
    deactivate JudgeDisplayPro

    deactivate GameScoreList
Loading

File-Level Changes

Change Details Files
使用锁保护 GameSettingsManager 和新的 GameSettingsManagerSprites 的 Harmony 补丁应用,避免并发打补丁以及每次注册都重新打补丁。
  • 将 GameSettingsManager.RegisterSetting 的补丁路径包裹在 lock 中,并通过 isPatched 标志只在首次时应用补丁。
  • 引入 GameSettingsManagerSprites,包含一个 bundle 注册表,并在 lock 下以懒加载方式应用 Harmony 补丁。
  • 通过 Harmony postfix 钩住 MusicSelectProcess.GetOptionValueSprite,使选项贴图可以按键前缀从已注册的 AssetBundle 中加载。
AquaMai.Core/Helpers/GameSettingsManager.cs
AquaMai.Core/Helpers/GameSettingsManagerSprites.cs
添加 JudgeDisplayPro 核心模块,用于注册新的设置项、管理按玩家持久化配置,并加载专用的预览贴图 AssetBundle。
  • 定义带有 UserSettings 数组、持久化存储集成以及 MusicSelectProcess 生命周期钩子(用于按玩家加载/保存设置)的 JudgeDisplayPro 部分类。
  • 从资源中加载内嵌的 judgedisplaypro AssetBundle,并使用键前缀将其注册到 GameSettingsManagerSprites。
  • 在 OnBeforePatch 阶段向 GameSettingsManager 注册开关(OnOff)、大 P 判定以及普通判定显示设置项。
AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.cs
AquaMai.Mods/UX/JudgeDisplayPro/Models.cs
AquaMai.Mods/UX/JudgeDisplayPro/SettingsEntryBase.cs
通过给 JudgeGrade 初始化打补丁,并基于按玩家设置将 timing 结果映射到贴图,实现对 tap/touch 与 break 音符的判定显示自定义。
  • 为捕获触摸判定的显示器索引,在 TouchNoteB.EndNote 和 TouchHoldC.EndNote 中通过 thread-static 字段记录 monitor index。
  • 在 JudgeGrade.Initialize 的 postfix 中,根据 NormalDisplayMode 和 CriticalDisplayMode 设置覆盖普通和大 P 判定的可见性与贴图。
  • 在 JudgeGrade.InitializeBreak 的 postfix 中正确区分 break-perfect 与 normal-perfect 的显示差异以及大 P break 行为。
  • 添加辅助方法,用于选择基础与彩色判定贴图、应用显示模式,并渲染大 P vs perfect 的行为,包括可选的 break 附加贴图。
AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.JudgeGrade.cs
AquaMai.Mods/UX/JudgeDisplayPro/Logic.cs
通过将 SlideJudge 实例绑定到显示器索引并根据 JudgeDisplayPro 设置重新驱动贴图,实现滑条音符判定显示自定义。
  • 在 SlideRoot.SetJudgeObject 中通过 ConditionalWeakTable 附加绑定对象,来跟踪 SlideJudge 的显示器索引。
  • 在 SlideJudge.Initialize 的 postfix 中,基于按玩家的 NormalDisplayMode 和 CriticalDisplayMode 重新选择滑条判定贴图(good/great/perfect/critical,普通 vs 彩色)。
  • 提供 ApplySlideDisplayMode 辅助方法,将显示模式映射为使用判定贴图或 timing 上色贴图,并可选地对所有 perfect 显示 timing 上色贴图。
AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.Slide.cs
AquaMai.Mods/UX/JudgeDisplayPro/Logic.cs
引入可配置的设置项,用于开启/关闭 JudgeDisplayPro、大 P(critical)行为,以及普通判定类型(perfect / break perfect / great / good),包含选项文本与预览贴图后缀。
  • 添加 OnOffSettingsEntry,用于按玩家切换 JudgeDisplayPro,并在启用时选择自定义选项贴图。
  • 添加 CriticalSettingsEntry,包含有界的 CriticalDisplayMode 范围、选项循环、已本地化描述,以及按模式映射预览贴图后缀。
  • 为每种普通判定类型添加 NormalSettingsEntry,用于驱动 NormalDisplayMode 循环、选项文本、值索引以及基于类型和模式构造贴图后缀。
AquaMai.Mods/UX/JudgeDisplayPro/OnOffSettingsEntry.cs
AquaMai.Mods/UX/JudgeDisplayPro/CriticalSettingsEntry.cs
AquaMai.Mods/UX/JudgeDisplayPro/NormalSettingsEntry.cs
AquaMai.Mods/UX/JudgeDisplayPro/SettingsEntryBase.cs
AquaMai.Mods/UX/JudgeDisplayPro/Models.cs
通过给 GameScoreList 打补丁并对 ResultProcess.OnStart 进行 transpile,使 FAST/LATE 计数器以及结算画面的判定显示与 JudgeDisplayPro 设置保持一致。
  • 在 GameScoreList.Initialize 的 postfix 中,根据配置的 CriticalDisplayMode 重写 UserOption.DispJudge。
  • 在 GameScoreList.SetResult 的 prefix 中对 fast/late 计数器进行快照,并基于每个 note 的 break 状态与 NormalDisplayMode 计算当前 timing 是否参与统计。
  • 在 GameScoreList.SetResult 的 postfix 中,通过由 GameScoreList.Fast/Late setter 构建的委托重新应用计数,避免对已判定音符重复计数。
  • 对 ResultProcess.OnStart 进行 transpile,使其从游戏内的 GameScoreList.UserOption 读取判定显示(DispJudge),而不是直接使用原始的 UserData.Option,确保结算画面显示的内容与实际游玩时一致。
  • 提供从 CriticalDisplayMode 到游戏内选项使用的 OptionDispjudgeID 的映射。
AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.Score.cs
AquaMai.Mods/UX/JudgeDisplayPro/Logic.cs
通过移除未使用的压缩 level.ab 支持,简化曲目常数 AssetBundle 处理逻辑。
  • 从 SongConstantSort.GetAssetBundleStream 中移除 level.ab.compressed 回退逻辑与 DeflateStream 解压,仅在缺少 level.ab 时返回 null。
  • 文档说明 level.ab 本身没有被压缩,不应再次压缩。
AquaMai.Mods/GameSystem/SongConstantSort.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 来(重新)生成概要。
  • 生成审查者指南: 在 pull request 中评论 @sourcery-ai guide,即可随时(重新)生成审查者指南。
  • 一次性解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,将所有 Sourcery 评论标记为已解决。如果你已经处理完所有评论且不想再看到它们,这会很有用。
  • 一次性撤销所有 Sourcery 审查: 在 pull request 中评论 @sourcery-ai dismiss,撤销所有现有的 Sourcery 审查。特别适用于你想从一个全新的审查开始——别忘了再评论 @sourcery-ai review 触发新的审查!

Customizing Your Experience

打开你的 dashboard 以:

  • 启用或禁用 Sourcery 生成的 PR 概要、审查者指南等审查功能。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查说明。
  • 调整其他审查设置。

Getting Help

Original review guide in English

Reviewer's Guide

Introduces the JudgeDisplayPro UX mod, wiring per-player judge display configuration into the existing settings system, gameplay judge rendering (tap/touch/slide), FAST/LATE score counting, result flow, and option preview sprites, while tightening patch application and asset bundle loading.

Sequence diagram for updated FAST/LATE counting via GameScoreList.SetResult

sequenceDiagram
    participant GameLogic
    participant GameScoreList
    participant JudgeDisplayPro
    participant Logic
    participant NotesManager

    GameLogic->>GameScoreList: SetResult(index, timing)
    activate GameScoreList

    GameScoreList->>JudgeDisplayPro: PreGameScoreListSetResult(__instance, index, timing, monitorIndex, out FastLateState)
    activate JudgeDisplayPro
    JudgeDisplayPro->>NotesManager: Instance(monitorIndex)
    NotesManager-->>JudgeDisplayPro: getReader().GetNoteList()[index]
    JudgeDisplayPro->>Logic: ShouldCountFastLate(settings, timing, isBreak)
    Logic-->>JudgeDisplayPro: bool
    JudgeDisplayPro-->>GameScoreList: FastLateState (saved as __state)
    deactivate JudgeDisplayPro

    GameScoreList-->>GameScoreList: original SetResult logic

    GameScoreList->>JudgeDisplayPro: PostGameScoreListSetResult(__instance, __state)
    activate JudgeDisplayPro
    JudgeDisplayPro->>GameScoreList: setFast(__instance, newFast)
    JudgeDisplayPro->>GameScoreList: setLate(__instance, newLate)
    deactivate JudgeDisplayPro

    deactivate GameScoreList
Loading

File-Level Changes

Change Details Files
Guard GameSettingsManager and new GameSettingsManagerSprites Harmony patch application with a lock to avoid concurrent patching and patch-on-every-register.
  • Wrap GameSettingsManager.RegisterSetting patching path in a lock and only apply patches once using an isPatched flag.
  • Introduce GameSettingsManagerSprites with a bundle registry and lazy Harmony patch application under a lock.
  • Hook MusicSelectProcess.GetOptionValueSprite via Harmony postfix so option sprites can be loaded from registered AssetBundles by key prefix.
AquaMai.Core/Helpers/GameSettingsManager.cs
AquaMai.Core/Helpers/GameSettingsManagerSprites.cs
Add JudgeDisplayPro core module that registers new settings entries, manages per-player persistent configuration, and loads a dedicated AssetBundle for preview sprites.
  • Define JudgeDisplayPro partial class with UserSettings array, persistent storage integration, and MusicSelectProcess lifecycle hooks to load/save per-player settings.
  • Load embedded judgedisplaypro AssetBundle from resources and register it with GameSettingsManagerSprites using a key prefix.
  • Register OnOff, critical, and normal judge display settings entries with GameSettingsManager during OnBeforePatch.
AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.cs
AquaMai.Mods/UX/JudgeDisplayPro/Models.cs
AquaMai.Mods/UX/JudgeDisplayPro/SettingsEntryBase.cs
Implement judge display customization logic for tap/touch and break notes by patching JudgeGrade initialization and mapping timing results to sprites based on per-player settings.
  • Patch TouchNoteB.EndNote and TouchHoldC.EndNote to capture monitor index for touch judges via a thread-static field.
  • Postfix JudgeGrade.Initialize to override judge visibility and sprites for normal and critical timings according to NormalDisplayMode and CriticalDisplayMode settings.
  • Postfix JudgeGrade.InitializeBreak to correctly handle break-perfect vs normal-perfect display differences and critical break behavior.
  • Add helper methods to choose base and colored judge sprites, apply display modes, and render critical vs perfect behavior, including optional break add-on sprites.
AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.JudgeGrade.cs
AquaMai.Mods/UX/JudgeDisplayPro/Logic.cs
Implement slide note judge display customization by binding SlideJudge instances to monitor indices and re-driving sprites based on JudgeDisplayPro settings.
  • Track SlideJudge monitor indices by attaching a binding object in SlideRoot.SetJudgeObject via ConditionalWeakTable.
  • Postfix SlideJudge.Initialize to re-select slide judge sprites (good/great/perfect/critical, normal vs colored) using per-player NormalDisplayMode and CriticalDisplayMode.
  • Provide ApplySlideDisplayMode helper to map display modes to either judge or timing-colored sprites and optionally show timing-colored sprites for all perfects.
AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.Slide.cs
AquaMai.Mods/UX/JudgeDisplayPro/Logic.cs
Introduce configurable settings entries for enabling JudgeDisplayPro, critical (大P) behavior, and normal judge types (perfect/break perfect/great/good), including option text and preview sprite suffixes.
  • Add OnOffSettingsEntry to toggle JudgeDisplayPro per player, including custom option sprite selection when enabled.
  • Add CriticalSettingsEntry with a bounded CriticalDisplayMode range, option cycling, localized descriptions, and preview sprite suffix mapping per mode.
  • Add NormalSettingsEntry for each normal judge type, driving NormalDisplayMode cycling, option text, value indices, and sprite suffix construction based on type and mode.
AquaMai.Mods/UX/JudgeDisplayPro/OnOffSettingsEntry.cs
AquaMai.Mods/UX/JudgeDisplayPro/CriticalSettingsEntry.cs
AquaMai.Mods/UX/JudgeDisplayPro/NormalSettingsEntry.cs
AquaMai.Mods/UX/JudgeDisplayPro/SettingsEntryBase.cs
AquaMai.Mods/UX/JudgeDisplayPro/Models.cs
Align FAST/LATE score counters and result-screen judge display with JudgeDisplayPro settings by patching GameScoreList and transpiling ResultProcess.OnStart.
  • Postfix GameScoreList.Initialize to overwrite UserOption.DispJudge based on the configured CriticalDisplayMode.
  • Prefix GameScoreList.SetResult to snapshot fast/late counters and compute whether the current timing should contribute, based on per-note break state and NormalDisplayMode.
  • Postfix GameScoreList.SetResult to reapply counters using a delegate built from GameScoreList.Fast/Late setters, avoiding double-counting on already judged notes.
  • Transpile ResultProcess.OnStart to read judge display (DispJudge) from the in-play GameScoreList.UserOption instead of raw UserData.Option, ensuring result screen shows what was actually used.
  • Provide mapping from CriticalDisplayMode to OptionDispjudgeID selection for in-game options.
AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.Score.cs
AquaMai.Mods/UX/JudgeDisplayPro/Logic.cs
Simplify song constant asset bundle handling by dropping unused compressed level.ab support.
  • Remove the level.ab.compressed fallback and DeflateStream decompression from SongConstantSort.GetAssetBundleStream, returning null when level.ab is missing.
  • Document that level.ab is not compressed and should not be re-compressed.
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

@clansty
clansty marked this pull request as draft August 3, 2026 14:00
@cursor

This comment was marked as low quality.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - 我发现了两个问题,并留下了一些高层次的反馈:

  • UserSettings.Deserialize 方法假定输入格式是固定且有效的,并且使用 bool.Parse / Enum.Parse 时没有做边界或错误检查。建议你先验证 values.Length,并使用 TryParse / 默认回退值,这样可以避免在已损坏或旧版本存储数据的情况下发生硬崩溃。
  • JudgeDisplayPro.JudgeGrade 中的大型嵌套 switch(以及类似的 slide 处理逻辑)在 ETiming、显示模式和精灵之间重复了大量映射逻辑;可以考虑抽取共享的辅助方法或查找表,使这部分逻辑更易维护,并在添加新模式或时序时减少出错风险。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `UserSettings.Deserialize` method assumes a fixed, valid format and uses `bool.Parse`/`Enum.Parse` without bounds or error checking, so consider validating `values.Length` and using `TryParse`/default fallbacks to avoid hard crashes on corrupted or older stored data.
- The large nested `switch` blocks in `JudgeDisplayPro.JudgeGrade` (and similarly in the slide handling) duplicate a lot of mapping logic between `ETiming`, display modes and sprites; extracting shared helpers or lookup tables would make this logic easier to maintain and less error-prone when adding new modes or timings.

## Individual Comments

### Comment 1
<location path="AquaMai.Mods/UX/JudgeDisplayPro/Models.cs" line_range="54-61" />
<code_context>
+        return $"{IsEnable},{CriticalDisplayMode},{PerfectDisplayMode},{GreatDisplayMode},{GoodDisplayMode}";
+    }
+
+    public void Deserialize(string data)
+    {
+        var values = data.Split(',');
+        IsEnable = bool.Parse(values[0]);
+        CriticalDisplayMode = (CriticalDisplayMode)Enum.Parse(typeof(CriticalDisplayMode), values[1]);
+        PerfectDisplayMode = (NormalDisplayMode)Enum.Parse(typeof(NormalDisplayMode), values[2]);
+        GreatDisplayMode = (NormalDisplayMode)Enum.Parse(typeof(NormalDisplayMode), values[3]);
+        GoodDisplayMode = (NormalDisplayMode)Enum.Parse(typeof(NormalDisplayMode), values[4]);
+    }
+}
</code_context>
<issue_to_address>
**issue (bug_risk):** Make `Deserialize` more robust against malformed or older serialized data.

This relies on a perfectly formed string (5 comma‑separated values, valid enum names). Any deviation (empty or shorter string, renamed enum values, corrupted settings) will throw and can break startup/settings loading.

Please add basic guards: verify `values.Length >= 5`, use `bool.TryParse` / `Enum.TryParse(..., ignoreCase: true, out ...)`, and on failure either keep existing values or use safe defaults. That will improve compatibility with older formats and make the app more resilient to bad data.
</issue_to_address>

### Comment 2
<location path="AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.Score.cs" line_range="77-86" />
<code_context>
+    [HarmonyTranspiler]
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Failing hard in the transpiler on pattern mismatch can make the whole mod brittle.

Since IL layouts can change between game versions, throwing `InvalidOperationException` on a pattern mismatch may stop the process from starting for a non-critical UX feature. Consider a softer failure: log a warning (including `matches.Length`) and return the original `instructions` when the pattern isn’t exactly 1, so behavior degrades gracefully instead of crashing.

Suggested implementation:

```csharp
        if (matches.Length != 1)
        {
            UnityEngine.Debug.LogWarning($"[JudgeDisplayPro] ResultProcess.OnStart transpiler expected exactly one match for pattern but found {matches.Length}; leaving method unpatched.");
            return instructions;
        }

```

Because I only see part of the file, you may need to:

1. Adjust the `SEARCH` block to match the exact text of your existing `InvalidOperationException` line(s) if the message differs.
2. Ensure the `matches` symbol is indeed the array/collection whose count you’re checking; if it has a different name or type, update the condition and the interpolated string accordingly.
3. If you prefer a different logging mechanism (e.g., your mod’s own logger instead of `UnityEngine.Debug.LogWarning`), replace the logging call with your project’s standard logging API while keeping the early `return instructions;`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的代码审查。
Original comment in English

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

  • The UserSettings.Deserialize method assumes a fixed, valid format and uses bool.Parse/Enum.Parse without bounds or error checking, so consider validating values.Length and using TryParse/default fallbacks to avoid hard crashes on corrupted or older stored data.
  • The large nested switch blocks in JudgeDisplayPro.JudgeGrade (and similarly in the slide handling) duplicate a lot of mapping logic between ETiming, display modes and sprites; extracting shared helpers or lookup tables would make this logic easier to maintain and less error-prone when adding new modes or timings.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `UserSettings.Deserialize` method assumes a fixed, valid format and uses `bool.Parse`/`Enum.Parse` without bounds or error checking, so consider validating `values.Length` and using `TryParse`/default fallbacks to avoid hard crashes on corrupted or older stored data.
- The large nested `switch` blocks in `JudgeDisplayPro.JudgeGrade` (and similarly in the slide handling) duplicate a lot of mapping logic between `ETiming`, display modes and sprites; extracting shared helpers or lookup tables would make this logic easier to maintain and less error-prone when adding new modes or timings.

## Individual Comments

### Comment 1
<location path="AquaMai.Mods/UX/JudgeDisplayPro/Models.cs" line_range="54-61" />
<code_context>
+        return $"{IsEnable},{CriticalDisplayMode},{PerfectDisplayMode},{GreatDisplayMode},{GoodDisplayMode}";
+    }
+
+    public void Deserialize(string data)
+    {
+        var values = data.Split(',');
+        IsEnable = bool.Parse(values[0]);
+        CriticalDisplayMode = (CriticalDisplayMode)Enum.Parse(typeof(CriticalDisplayMode), values[1]);
+        PerfectDisplayMode = (NormalDisplayMode)Enum.Parse(typeof(NormalDisplayMode), values[2]);
+        GreatDisplayMode = (NormalDisplayMode)Enum.Parse(typeof(NormalDisplayMode), values[3]);
+        GoodDisplayMode = (NormalDisplayMode)Enum.Parse(typeof(NormalDisplayMode), values[4]);
+    }
+}
</code_context>
<issue_to_address>
**issue (bug_risk):** Make `Deserialize` more robust against malformed or older serialized data.

This relies on a perfectly formed string (5 comma‑separated values, valid enum names). Any deviation (empty or shorter string, renamed enum values, corrupted settings) will throw and can break startup/settings loading.

Please add basic guards: verify `values.Length >= 5`, use `bool.TryParse` / `Enum.TryParse(..., ignoreCase: true, out ...)`, and on failure either keep existing values or use safe defaults. That will improve compatibility with older formats and make the app more resilient to bad data.
</issue_to_address>

### Comment 2
<location path="AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.Score.cs" line_range="77-86" />
<code_context>
+    [HarmonyTranspiler]
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Failing hard in the transpiler on pattern mismatch can make the whole mod brittle.

Since IL layouts can change between game versions, throwing `InvalidOperationException` on a pattern mismatch may stop the process from starting for a non-critical UX feature. Consider a softer failure: log a warning (including `matches.Length`) and return the original `instructions` when the pattern isn’t exactly 1, so behavior degrades gracefully instead of crashing.

Suggested implementation:

```csharp
        if (matches.Length != 1)
        {
            UnityEngine.Debug.LogWarning($"[JudgeDisplayPro] ResultProcess.OnStart transpiler expected exactly one match for pattern but found {matches.Length}; leaving method unpatched.");
            return instructions;
        }

```

Because I only see part of the file, you may need to:

1. Adjust the `SEARCH` block to match the exact text of your existing `InvalidOperationException` line(s) if the message differs.
2. Ensure the `matches` symbol is indeed the array/collection whose count you’re checking; if it has a different name or type, update the condition and the interpolated string accordingly.
3. If you prefer a different logging mechanism (e.g., your mod’s own logger instead of `UnityEngine.Debug.LogWarning`), replace the logging call with your project’s standard logging API while keeping the early `return instructions;`.
</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/UX/JudgeDisplayPro/Models.cs Outdated
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.Score.cs

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 8 files

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

Re-trigger cubic

Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.JudgeGrade.cs
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.cs
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.JudgeGrade.cs
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.JudgeGrade.cs
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.JudgeGrade.cs
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.cs
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.JudgeGrade.cs Outdated
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.JudgeGrade.cs
@clansty
clansty marked this pull request as ready for review August 6, 2026 18:19

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

There are 9 total unresolved issues (including 5 from previous reviews).

Fix All in Cursor

Bugbot Autofix is ON, but it could not run because Privacy Mode (Legacy) is turned on. To enable Bugbot Autofix, switch your privacy mode in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 53d4124. Configure here.

Comment thread AquaMai.Mods/UX/JudgeDisplayPro/CriticalSettingsEntry.cs
Comment thread AquaMai.Core/Helpers/GameSettingsManager.cs
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.Score.cs
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.Slide.cs Outdated

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

  • GameSettingsManagerSprites.bundleMapRegisterBundle 中会在锁内被修改,但在 GetOptionValueSprite 中却在没有任何同步措施的情况下(并且通过 LINQ)被读取;建议使用并发/不可变字典或共享锁来保护读写,并在这一性能敏感路径中避免使用 LINQ,以防止竞态条件并减少每帧的分配。
  • UserSettings.Serialize/Deserialize 假定固定的逗号分隔布局,并在没有校验的情况下使用 bool.Parse/Enum.Parse;将解析逻辑改得更“防御性”一些(例如长度检查、带默认值的 TryParse、版本控制)会让已保存的设置在数据损坏或未来格式变更的情况下更加健壮。
  • CreateScoreCounterSetter 假定 GameScoreListFast/Late 属性 setter 永远存在;在游戏更新重命名或移除这些属性时,这有可能导致空引用异常。更安全的做法是对解析得到的 MethodInfo 做空检查,并在失败时优雅地处理(或记录日志)。
给 AI Agent 的提示
请根据本次代码评审中的评论进行修改:

## 总体评论
- `GameSettingsManagerSprites.bundleMap``RegisterBundle` 中会在锁内被修改,但在 `GetOptionValueSprite` 中却在没有任何同步措施的情况下(并且通过 LINQ)被读取;建议使用并发/不可变字典或共享锁来保护读写,并在这一性能敏感路径中避免使用 LINQ,以防止竞态条件并减少每帧的分配。
- `UserSettings.Serialize/Deserialize` 假定固定的逗号分隔布局,并在没有校验的情况下使用 `bool.Parse/Enum.Parse`;将解析逻辑改得更“防御性”一些(例如长度检查、带默认值的 `TryParse`、版本控制)会让已保存的设置在数据损坏或未来格式变更的情况下更加健壮。
- `CreateScoreCounterSetter` 假定 `GameScoreList``Fast/Late` 属性 setter 永远存在;在游戏更新重命名或移除这些属性时,这有可能导致空引用异常。更安全的做法是对解析得到的 `MethodInfo` 做空检查,并在失败时优雅地处理(或记录日志)。

## 具体评论

### 评论 1
<location path="AquaMai.Mods/UX/JudgeDisplayPro/Models.cs" line_range="60-69" />
<code_context>
+    public void Deserialize(string data)
</code_context>
<issue_to_address>
**suggestion:** `Deserialize` 假定数据格式始终正确,这会在遇到格式错误或旧版本数据时抛出异常;建议使用更防御性的解析方式。

`Deserialize` 假定 `data` 总是合法:

```csharp
var values = data.Split(',');
IsEnable = bool.Parse(values[0]);
CriticalDisplayMode = (CriticalDisplayMode)Enum.Parse(..., values[1]);
PerfectDisplayMode = (NormalDisplayMode)Enum.Parse(..., values[2]);
if (values.Length >= 6)
{
    BreakPerfectDisplayMode = (NormalDisplayMode)Enum.Parse(..., values[3]);
    GreatDisplayMode = (NormalDisplayMode)Enum.Parse(..., values[4]);
    GoodDisplayMode = (NormalDisplayMode)Enum.Parse(..., values[5]);
}
else
{
    BreakPerfectDisplayMode = PerfectDisplayMode;
    GreatDisplayMode = (NormalDisplayMode)Enum.Parse(..., values[3]);
    GoodDisplayMode = (NormalDisplayMode)Enum.Parse(..., values[4]);
}
```

损坏、截断或版本不匹配的输入可能导致 `IndexOutOfRangeException`,或者在 `Enum.Parse` 时抛出 `ArgumentException`。建议在访问每个索引前检查 `values.Length`,并使用 `TryParse` 配合安全默认值,这样即便设置字符串有问题,也不会让整个反序列化过程失败。
</issue_to_address>

### 评论 2
<location path="AquaMai.Core/Helpers/GameSettingsManagerSprites.cs" line_range="11-18" />
<code_context>
+
+public class GameSettingsManagerSprites
+{
+    private static Dictionary<string, AssetBundle> bundleMap = new();
+    private static bool isPatched = false;
+    private static readonly object patchLock = new();
+
+    public static void RegisterBundle(string prefix, AssetBundle bundle)
+    {
+        if(bundleMap.ContainsKey(prefix)) return;
+        bundleMap.Add(prefix, bundle);
+        lock(patchLock)
+        {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 在存在并发读取的情况下,Bundle 注册过程不是线程安全的;建议对写操作加锁。

`RegisterBundle` 只在处理 `isPatched`/`ApplyPatch` 时使用了 `patchLock`,但对 `bundleMap` 的修改没有同步:

```csharp
if (bundleMap.ContainsKey(prefix)) return;
bundleMap.Add(prefix, bundle);
lock (patchLock)
{
    if (!isPatched)
    {
        isPatched = true;
        Startup.ApplyPatch(typeof(GameSettingsManagerSprites));
    }
}
```

`GetOptionValueSprite` 通过 `FirstOrDefault` 枚举 `bundleMap`,因此并发注册可能导致竞态条件,或者在枚举期间修改字典从而触发 `InvalidOperationException`。建议将 `ContainsKey`/`Add` 包裹在同一个锁中,或改用 `ConcurrentDictionary` 来保证线程安全访问。

建议实现如下:

```csharp
    public static void RegisterBundle(string prefix, AssetBundle bundle)
    {
        lock (patchLock)
        {
            if (bundleMap.ContainsKey(prefix)) return;

            bundleMap.Add(prefix, bundle);

            if (!isPatched)
            {
                isPatched = true;
                Startup.ApplyPatch(typeof(GameSettingsManagerSprites));
            }
        }
    }

````GetOptionValueSprite`(以及其他读者)中,`bundleMap` 仍然是在没有同步的情况下被读取和枚举。为了完全避免竞态条件和在枚举期间的 `InvalidOperationException`,可以:
1. 将所有对 `bundleMap` 的读取/枚举都放到 `lock (patchLock) { ... }` 中,或者
2.`Dictionary<string, AssetBundle>` 替换为 `ConcurrentDictionary<string, AssetBundle>`,并相应调整用法(例如使用 `TryAdd``TryGetValue`,并直接枚举 `ConcurrentDictionary` 实例)。
</issue_to_address>

### 评论 3
<location path="AquaMai.Mods/UX/JudgeDisplayPro/SettingsEntryBase.cs" line_range="9-11" />
<code_context>
+{
+    public string GetSpriteFile(int player)
+    {
+        var suffix = GetSpriteSuffix(player);
+        if(suffix == null) return "UI_OPT_00_00";
+        return "AQM_JudgeDisplayPro_" + GetSpriteSuffix(player);
+    }
+
</code_context>
<issue_to_address>
**nitpick:** 避免重复调用 `GetSpriteSuffix`,并确保复用已计算的值。

在 `GetSpriteFile` 中虽然计算了 `suffix`,却没有复用它:

```csharp
var suffix = GetSpriteSuffix(player);
if (suffix == null) return "UI_OPT_00_00";
return "AQM_JudgeDisplayPro_" + GetSpriteSuffix(player);
```

建议复用缓存值,既避免多余调用,又能保证空检查与返回值的一致性:

```csharp
var suffix = GetSpriteSuffix(player);
return suffix == null
    ? "UI_OPT_00_00"
    : "AQM_JudgeDisplayPro_" + suffix;
```
</issue_to_address>

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

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

  • GameSettingsManagerSprites.bundleMap is mutated under a lock in RegisterBundle but read without any synchronization (and via LINQ) in GetOptionValueSprite; consider using a concurrent/immutable dictionary or shared lock and avoiding LINQ in this hot path to prevent race conditions and reduce per-frame allocations.
  • UserSettings.Serialize/Deserialize assumes a fixed comma-separated layout and uses bool.Parse/Enum.Parse without validation; making this parsing more defensive (e.g., length checks, TryParse with defaults, versioning) would make saved settings more robust to corruption or future format changes.
  • CreateScoreCounterSetter assumes the GameScoreList Fast/Late property setters always exist; it may be safer to null-check the resolved MethodInfo and fail gracefully (or log) instead of risking a null reference if game updates rename or remove these properties.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- GameSettingsManagerSprites.bundleMap is mutated under a lock in RegisterBundle but read without any synchronization (and via LINQ) in GetOptionValueSprite; consider using a concurrent/immutable dictionary or shared lock and avoiding LINQ in this hot path to prevent race conditions and reduce per-frame allocations.
- UserSettings.Serialize/Deserialize assumes a fixed comma-separated layout and uses bool.Parse/Enum.Parse without validation; making this parsing more defensive (e.g., length checks, TryParse with defaults, versioning) would make saved settings more robust to corruption or future format changes.
- CreateScoreCounterSetter assumes the GameScoreList Fast/Late property setters always exist; it may be safer to null-check the resolved MethodInfo and fail gracefully (or log) instead of risking a null reference if game updates rename or remove these properties.

## Individual Comments

### Comment 1
<location path="AquaMai.Mods/UX/JudgeDisplayPro/Models.cs" line_range="60-69" />
<code_context>
+    public void Deserialize(string data)
</code_context>
<issue_to_address>
**suggestion:** `Deserialize` assumes well-formed data and can throw on malformed or older values; consider defensive parsing.

`Deserialize` assumes `data` is always valid:

```csharp
var values = data.Split(',');
IsEnable = bool.Parse(values[0]);
CriticalDisplayMode = (CriticalDisplayMode)Enum.Parse(..., values[1]);
PerfectDisplayMode = (NormalDisplayMode)Enum.Parse(..., values[2]);
if (values.Length >= 6)
{
    BreakPerfectDisplayMode = (NormalDisplayMode)Enum.Parse(..., values[3]);
    GreatDisplayMode = (NormalDisplayMode)Enum.Parse(..., values[4]);
    GoodDisplayMode = (NormalDisplayMode)Enum.Parse(..., values[5]);
}
else
{
    BreakPerfectDisplayMode = PerfectDisplayMode;
    GreatDisplayMode = (NormalDisplayMode)Enum.Parse(..., values[3]);
    GoodDisplayMode = (NormalDisplayMode)Enum.Parse(..., values[4]);
}
```

Corrupted, truncated, or version-mismatched input can cause `IndexOutOfRangeException` or `ArgumentException` from `Enum.Parse`. Consider checking `values.Length` before each index and using `TryParse` with safe defaults so a bad settings string doesn’t break deserialization entirely.
</issue_to_address>

### Comment 2
<location path="AquaMai.Core/Helpers/GameSettingsManagerSprites.cs" line_range="11-18" />
<code_context>
+
+public class GameSettingsManagerSprites
+{
+    private static Dictionary<string, AssetBundle> bundleMap = new();
+    private static bool isPatched = false;
+    private static readonly object patchLock = new();
+
+    public static void RegisterBundle(string prefix, AssetBundle bundle)
+    {
+        if(bundleMap.ContainsKey(prefix)) return;
+        bundleMap.Add(prefix, bundle);
+        lock(patchLock)
+        {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Bundle registration is not thread-safe while the map is read concurrently; consider locking around writes.

`RegisterBundle` uses `patchLock` only for `isPatched`/`ApplyPatch`, but `bundleMap` is modified without synchronization:

```csharp
if (bundleMap.ContainsKey(prefix)) return;
bundleMap.Add(prefix, bundle);
lock (patchLock)
{
    if (!isPatched)
    {
        isPatched = true;
        Startup.ApplyPatch(typeof(GameSettingsManagerSprites));
    }
}
```

`GetOptionValueSprite` enumerates `bundleMap` via `FirstOrDefault`, so concurrent registration could cause race conditions or `InvalidOperationException` from modifying the dictionary during enumeration. Consider guarding the `ContainsKey`/`Add` with the same lock or switching to a `ConcurrentDictionary` to ensure thread-safe access.

Suggested implementation:

```csharp
    public static void RegisterBundle(string prefix, AssetBundle bundle)
    {
        lock (patchLock)
        {
            if (bundleMap.ContainsKey(prefix)) return;

            bundleMap.Add(prefix, bundle);

            if (!isPatched)
            {
                isPatched = true;
                Startup.ApplyPatch(typeof(GameSettingsManagerSprites));
            }
        }
    }

```

`bundleMap` is still read without synchronization in `GetOptionValueSprite` (and any other readers). To fully avoid race conditions and `InvalidOperationException` during enumeration, either:
1. Wrap all reads/enumerations of `bundleMap` in `lock (patchLock) { ... }`, or
2. Replace `Dictionary<string, AssetBundle>` with `ConcurrentDictionary<string, AssetBundle>` and update uses accordingly (e.g., use `TryAdd`, `TryGetValue`, and enumerate the `ConcurrentDictionary` instance directly).
</issue_to_address>

### Comment 3
<location path="AquaMai.Mods/UX/JudgeDisplayPro/SettingsEntryBase.cs" line_range="9-11" />
<code_context>
+{
+    public string GetSpriteFile(int player)
+    {
+        var suffix = GetSpriteSuffix(player);
+        if(suffix == null) return "UI_OPT_00_00";
+        return "AQM_JudgeDisplayPro_" + GetSpriteSuffix(player);
+    }
+
</code_context>
<issue_to_address>
**nitpick:** Avoid calling `GetSpriteSuffix` twice and ensure consistent use of the computed value.

`suffix` is computed but not reused in `GetSpriteFile`:

```csharp
var suffix = GetSpriteSuffix(player);
if (suffix == null) return "UI_OPT_00_00";
return "AQM_JudgeDisplayPro_" + GetSpriteSuffix(player);
```

Reuse the cached value to avoid the redundant call and keep the null check and returned value consistent:

```csharp
var suffix = GetSpriteSuffix(player);
return suffix == null
    ? "UI_OPT_00_00"
    : "AQM_JudgeDisplayPro_" + suffix;
```
</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/UX/JudgeDisplayPro/Models.cs
Comment thread AquaMai.Core/Helpers/GameSettingsManagerSprites.cs
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/SettingsEntryBase.cs Outdated

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 1 file (changes from recent commits).

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

Re-trigger cubic

Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.cs

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review completed against the latest diff

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

Re-trigger cubic

Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.Slide.cs Outdated
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/CriticalSettingsEntry.cs
Comment thread AquaMai.Core/Helpers/GameSettingsManagerSprites.cs
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.JudgeGrade.cs
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.cs
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.cs Outdated
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.Score.cs
Comment thread AquaMai.Core/Helpers/GameSettingsManagerSprites.cs

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files (changes from recent commits).

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

Re-trigger cubic

Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.cs
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/JudgeDisplayPro.cs
Comment thread AquaMai/Resources/judgedisplaypro
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/NormalSettingsEntry.cs Outdated
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/NormalSettingsEntry.cs Outdated
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/NormalSettingsEntry.cs Outdated
Comment thread AquaMai.Mods/UX/JudgeDisplayPro/NormalSettingsEntry.cs Outdated

@Starrah Starrah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

大概整体review了下,感觉牛蛙!这个工作量好大,而且功能感觉很实用。官方的判定调整选项确实太少了
这下majdataplay化了(x)

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Starrah <starrah@foxmail.com>

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files (changes from recent commits).

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

Re-trigger cubic

Comment thread AquaMai.Mods/UX/JudgeDisplayPro/NormalSettingsEntry.cs Outdated

@Starrah Starrah Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

我本来想进一步重构一下这个文件,在PostJudgeGradeInitialize里判断是不是绝赞、直接用正确的isBreak参数调用ApplyNormalJudgeGradeDisplayApplyCriticalJudgeGradeDisplay,从而避免PostJudgeGradeInitializeBreak里的相似代码的。
然后看了半天发现,JudgeGrade里面的实现混乱无比,没有一个稳定的变量用于判定一个音符是不是绝赞。(NoteBase.JudgeType对保护套绝赞会直接输出EJudgeType.ExTap)。很明显就是fes当年新增保护套绝赞的时候乱搞导致的。SBGA程序员这一块。
综合想了一下,现在这种写法已经比较好了,很难找到一个完全比现在好的写法

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

…音符时才打开spriteRenderAdd。

然后给一些比较复杂和边界的逻辑增加了注释(和小重构),也许稍微增强了代码的可读性

@Starrah Starrah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

我稍微推了一点小修改( 9d0a300 ),主要是增加注释(JudgeDisplayPro那几个长文件全都是绕来绕去的相似逻辑,我把不容易缕明白的点注释了一下)。
其他的全都LGTM

@Starrah

Starrah commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

但是CI怎么一直在炸,这个可能得您看看 @clansty
{D8766248-521A-44AB-BF7F-F20B03EC0974}

@clansty

clansty commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

CI 是上次加固导致的,因为 PR 相关的没怎么测试
感觉 CI 是一个很难做测试的东西,因为它没有一个测试环境(

@Starrah

Starrah commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

CI 是上次加固导致的,因为 PR 相关的没怎么测试 感觉 CI 是一个很难做测试的东西,因为它没有一个测试环境(

反正是build CI又不是release,直接推到一个新分支上测,测完了squash回主分支就好吧()
在辅助分支上CI怎么炸也没啥影响?

@clansty

clansty commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

我也 LGTM

@clansty clansty changed the title WIP: JudgeDisplayPro feat: JudgeDisplayPro Aug 11, 2026
@clansty clansty closed this Aug 11, 2026
@clansty clansty reopened this Aug 11, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - 我发现了 1 个问题,并给出了一些整体层面的反馈:

  • GameSettingsManagerSprites 中,RegisterBundle 会在加锁的情况下修改 bundleMap,但 GetOptionValueSprite 在读取时没有做任何同步;如果在游戏开始后仍然可能注册新的 bundle,建议把 bundleMap 改成 ConcurrentDictionary,或者至少在读取时也加锁,以避免潜在的字典损坏。
  • 当 IL 模式数量不等于 1 时,ResultProcessOnStartTranspiler 会抛出 InvalidOperationException,这会在游戏更新时导致硬崩溃;你可能希望在这种情况下只记录一个警告并跳过打补丁,这样游戏仍然可以运行(只是没有增强的判定显示集成)。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
-`GameSettingsManagerSprites` 中,`RegisterBundle` 会在加锁的情况下修改 `bundleMap`,但 `GetOptionValueSprite` 在读取时没有做任何同步;如果在游戏开始后仍然可能注册新的 bundle,建议把 `bundleMap` 改成 `ConcurrentDictionary`,或者至少在读取时也加锁,以避免潜在的字典损坏。
- 当 IL 模式数量不等于 1 时,`ResultProcessOnStartTranspiler` 会抛出 `InvalidOperationException`,这会在游戏更新时导致硬崩溃;你可能希望在这种情况下只记录一个警告并跳过打补丁,这样游戏仍然可以运行(只是没有增强的判定显示集成)。

## Individual Comments

### Comment 1
<location path="AquaMai.Core/Helpers/GameSettingsManager.cs" line_range="37-46" />
<code_context>
     private static readonly List<IPlayerSettingsItem> settings = [];
     public static void RegisterSetting(IPlayerSettingsItem setting)
     {
         settings.Add(setting);
         settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));
-        if (!isPatched)
+        lock (patchLock)
         {
-            isPatched = true;
-            Startup.ApplyPatch(typeof(GameSettingsManager));
-            Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
+            if (!isPatched)
+            {
+                isPatched = true;
+                Startup.ApplyPatch(typeof(GameSettingsManager));
+                Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
+            }
         }
     }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 考虑使用同一个锁来保护 settings 列表,以避免在多线程注册时发生竞争。

`settings.Add``settings.Sort` 仍然在没有同步的情况下执行,因此来自多个线程的并发注册可能会产生竞争或抛出异常(例如在排序时抛出 `InvalidOperationException`)。请将 `lock (patchLock)` 扩展到这些列表操作,或者为 `settings` 添加一个专门的锁,并在注册和枚举时都一致地使用它来保证线程安全。

建议实现如下:

```csharp
    private static readonly List<IPlayerSettingsItem> settings = [];
    public static void RegisterSetting(IPlayerSettingsItem setting)
    {
        lock (patchLock)
        {
            settings.Add(setting);
            settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));

            if (!isPatched)
            {
                isPatched = true;
                Startup.ApplyPatch(typeof(GameSettingsManager));
                Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
            }
        }
    }

```

` 因为它们没有在这里展示)。

以下是具体的编辑:

<file_operations>
<file_operation operation="edit" file_path="AquaMai.Core/Helpers/GameSettingsManager.cs">
<<<<<<< SEARCH
    private static readonly List<IPlayerSettingsItem> settings = [];
    public static void RegisterSetting(IPlayerSettingsItem setting)
    {
        settings.Add(setting);
        settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));
        lock (patchLock)
        {
            if (!isPatched)
            {
                isPatched = true;
                Startup.ApplyPatch(typeof(GameSettingsManager));
                Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
            }
        }
    }
=======
    private static readonly List<IPlayerSettingsItem> settings = [];
    public static void RegisterSetting(IPlayerSettingsItem setting)
    {
        lock (patchLock)
        {
            settings.Add(setting);
            settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));

            if (!isPatched)
            {
                isPatched = true;
                Startup.ApplyPatch(typeof(GameSettingsManager));
                Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
            }
        }
    }
>>>>>>> REPLACE
</file_operation>
</file_operations>

<additional_changes>
为了实现完全线程安全,任何读取、枚举或修改 `settings` 的其他方法也应在 `patchLock` 上进行同步。例如,如果有类似 `GetSettings()``foreach (var s in settings)` 的方法,或者其他修改操作(`Clear``Remove` 等),都应该在访问时使用同一个锁对象 `lock (patchLock)` 包裹,以避免数据竞争以及在枚举期间发生 `InvalidOperationException`。
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据反馈改进后续的代码审查。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • In GameSettingsManagerSprites, bundleMap is mutated under a lock in RegisterBundle but read without synchronization in GetOptionValueSprite; if bundles can be registered after gameplay has started, consider making bundleMap a ConcurrentDictionary or at least locking on reads to avoid potential dictionary corruption.
  • ResultProcessOnStartTranspiler throws an InvalidOperationException when the IL pattern count is not exactly 1, which will hard-crash on game updates; you might want to log a warning and skip patching in that case so the game still runs (just without the enhanced judge display integration).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `GameSettingsManagerSprites`, `bundleMap` is mutated under a lock in `RegisterBundle` but read without synchronization in `GetOptionValueSprite`; if bundles can be registered after gameplay has started, consider making `bundleMap` a `ConcurrentDictionary` or at least locking on reads to avoid potential dictionary corruption.
- `ResultProcessOnStartTranspiler` throws an `InvalidOperationException` when the IL pattern count is not exactly 1, which will hard-crash on game updates; you might want to log a warning and skip patching in that case so the game still runs (just without the enhanced judge display integration).

## Individual Comments

### Comment 1
<location path="AquaMai.Core/Helpers/GameSettingsManager.cs" line_range="37-46" />
<code_context>
     private static readonly List<IPlayerSettingsItem> settings = [];
     public static void RegisterSetting(IPlayerSettingsItem setting)
     {
         settings.Add(setting);
         settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));
-        if (!isPatched)
+        lock (patchLock)
         {
-            isPatched = true;
-            Startup.ApplyPatch(typeof(GameSettingsManager));
-            Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
+            if (!isPatched)
+            {
+                isPatched = true;
+                Startup.ApplyPatch(typeof(GameSettingsManager));
+                Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
+            }
         }
     }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider protecting the settings list with the same lock to avoid races when registering from multiple threads.

`settings.Add` and `settings.Sort` still run without synchronization, so concurrent registration from multiple threads can race or throw (e.g., `InvalidOperationException` during sort). Please either extend `lock (patchLock)` to cover these list operations, or add a dedicated lock for `settings` and use it consistently for registration and enumeration to ensure thread safety.

Suggested implementation:

```csharp
    private static readonly List<IPlayerSettingsItem> settings = [];
    public static void RegisterSetting(IPlayerSettingsItem setting)
    {
        lock (patchLock)
        {
            settings.Add(setting);
            settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));

            if (!isPatched)
            {
                isPatched = true;
                Startup.ApplyPatch(typeof(GameSettingsManager));
                Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
            }
        }
    }

```

` since they’re not shown here).

Here are the concrete edits:

<file_operations>
<file_operation operation="edit" file_path="AquaMai.Core/Helpers/GameSettingsManager.cs">
<<<<<<< SEARCH
    private static readonly List<IPlayerSettingsItem> settings = [];
    public static void RegisterSetting(IPlayerSettingsItem setting)
    {
        settings.Add(setting);
        settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));
        lock (patchLock)
        {
            if (!isPatched)
            {
                isPatched = true;
                Startup.ApplyPatch(typeof(GameSettingsManager));
                Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
            }
        }
    }
=======
    private static readonly List<IPlayerSettingsItem> settings = [];
    public static void RegisterSetting(IPlayerSettingsItem setting)
    {
        lock (patchLock)
        {
            settings.Add(setting);
            settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));

            if (!isPatched)
            {
                isPatched = true;
                Startup.ApplyPatch(typeof(GameSettingsManager));
                Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
            }
        }
    }
>>>>>>> REPLACE
</file_operation>
</file_operations>

<additional_changes>
To be fully thread-safe, any other methods that read, enumerate, or modify `settings` should also synchronize on `patchLock`. For example, if there are methods like `GetSettings()`, `foreach (var s in settings)`, or other mutations (`Clear`, `Remove`, etc.), those should wrap access in `lock (patchLock)` using the same lock object to avoid data races and `InvalidOperationException` during enumeration.
</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 on lines 37 to +46
settings.Add(setting);
settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));
if (!isPatched)
lock (patchLock)
{
isPatched = true;
Startup.ApplyPatch(typeof(GameSettingsManager));
Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
if (!isPatched)
{
isPatched = true;
Startup.ApplyPatch(typeof(GameSettingsManager));
Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): 考虑使用同一个锁来保护 settings 列表,以避免在多线程注册时发生竞争。

settings.Addsettings.Sort 仍然在没有同步的情况下执行,因此来自多个线程的并发注册可能会产生竞争或抛出异常(例如在排序时抛出 InvalidOperationException)。请将 lock (patchLock) 扩展到这些列表操作,或者为 settings 添加一个专门的锁,并在注册和枚举时都一致地使用它来保证线程安全。

建议实现如下:

    private static readonly List<IPlayerSettingsItem> settings = [];
    public static void RegisterSetting(IPlayerSettingsItem setting)
    {
        lock (patchLock)
        {
            settings.Add(setting);
            settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));

            if (!isPatched)
            {
                isPatched = true;
                Startup.ApplyPatch(typeof(GameSettingsManager));
                Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
            }
        }
    }

` 因为它们没有在这里展示)。

以下是具体的编辑:

<file_operations>
<file_operation operation="edit" file_path="AquaMai.Core/Helpers/GameSettingsManager.cs">
<<<<<<< SEARCH
private static readonly List settings = [];
public static void RegisterSetting(IPlayerSettingsItem setting)
{
settings.Add(setting);
settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));
lock (patchLock)
{
if (!isPatched)
{
isPatched = true;
Startup.ApplyPatch(typeof(GameSettingsManager));
Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
}
}
}

private static readonly List<IPlayerSettingsItem> settings = [];
public static void RegisterSetting(IPlayerSettingsItem setting)
{
    lock (patchLock)
    {
        settings.Add(setting);
        settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));

        if (!isPatched)
        {
            isPatched = true;
            Startup.ApplyPatch(typeof(GameSettingsManager));
            Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
        }
    }
}

REPLACE
</file_operation>
</file_operations>

<additional_changes>
为了实现完全线程安全,任何读取、枚举或修改 settings 的其他方法也应在 patchLock 上进行同步。例如,如果有类似 GetSettings()foreach (var s in settings) 的方法,或者其他修改操作(ClearRemove 等),都应该在访问时使用同一个锁对象 lock (patchLock) 包裹,以避免数据竞争以及在枚举期间发生 InvalidOperationException
</additional_changes>

Original comment in English

suggestion (bug_risk): Consider protecting the settings list with the same lock to avoid races when registering from multiple threads.

settings.Add and settings.Sort still run without synchronization, so concurrent registration from multiple threads can race or throw (e.g., InvalidOperationException during sort). Please either extend lock (patchLock) to cover these list operations, or add a dedicated lock for settings and use it consistently for registration and enumeration to ensure thread safety.

Suggested implementation:

    private static readonly List<IPlayerSettingsItem> settings = [];
    public static void RegisterSetting(IPlayerSettingsItem setting)
    {
        lock (patchLock)
        {
            settings.Add(setting);
            settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));

            if (!isPatched)
            {
                isPatched = true;
                Startup.ApplyPatch(typeof(GameSettingsManager));
                Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
            }
        }
    }

` since they’re not shown here).

Here are the concrete edits:

<file_operations>
<file_operation operation="edit" file_path="AquaMai.Core/Helpers/GameSettingsManager.cs">
<<<<<<< SEARCH
private static readonly List settings = [];
public static void RegisterSetting(IPlayerSettingsItem setting)
{
settings.Add(setting);
settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));
lock (patchLock)
{
if (!isPatched)
{
isPatched = true;
Startup.ApplyPatch(typeof(GameSettingsManager));
Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
}
}
}

private static readonly List<IPlayerSettingsItem> settings = [];
public static void RegisterSetting(IPlayerSettingsItem setting)
{
    lock (patchLock)
    {
        settings.Add(setting);
        settings.Sort((a, b) => a.Sort.CompareTo(b.Sort));

        if (!isPatched)
        {
            isPatched = true;
            Startup.ApplyPatch(typeof(GameSettingsManager));
            Startup.ApplyPatch(typeof(PatchOptionCategoryIDExtension));
        }
    }
}

REPLACE
</file_operation>
</file_operations>

<additional_changes>
To be fully thread-safe, any other methods that read, enumerate, or modify settings should also synchronize on patchLock. For example, if there are methods like GetSettings(), foreach (var s in settings), or other mutations (Clear, Remove, etc.), those should wrap access in lock (patchLock) using the same lock object to avoid data races and InvalidOperationException during enumeration.

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