Repository navigation
feat: 乐曲切换流派至宴会场时自动更改 ID 并转换谱面 - #77
Conversation
- 乐曲流派切换至宴会场时二次确认,自动把乐曲 ID 改为原 ID + 100000(仅原 ID 小于 100000 时) - 把唯一启用的难度谱面转换为 Basic 谱面:交换两个槽位的谱面文件,原难度的等级/定数/谱师/音符数搬到 Basic 槽位并启用 Basic,原难度禁用(新增 ConvertSingleChartToBasicApi) - 本来就只有 Basic 启用时不再提示转换谱面 - 启用多个难度 / 没有启用谱面 / 目标 ID 已被占用时给出提示并回滚流派 - 流派由宴会场切回其他流派时提示并把 ID 减 100000
审查者指南本 PR 在音乐编辑页拦截流派与宴会场之间的切换,通过确认和冲突校验协调 ID 偏移、唯一启用谱面向 Basic 槽位转换及流派保存;服务端负责实际谱面文件和元数据迁移,前端 API、状态回滚和多语言提示同步完成。 将乐曲切换为宴会场的时序图sequenceDiagram
actor Editor
participant MusicEdit
participant MusicApi
participant MusicController
participant MusicStore
Editor->>MusicEdit: Change genre to Utage
MusicEdit->>MusicStore: Check enabled charts and target ID
alt Invalid chart state or occupied target ID
MusicEdit-->>Editor: Show error dialog
MusicEdit->>MusicEdit: revertGenre(oldGenreId)
else Conversion confirmed
opt Non-Basic chart enabled
MusicEdit->>MusicController: ConvertSingleChartToBasic(oldId, assetDir)
MusicController->>MusicController: Move chart files and metadata to Basic
MusicController-->>MusicEdit: Success or error
end
opt oldId < 100000
MusicEdit->>MusicApi: ModifyId(oldId, assetDir, newId)
end
MusicEdit->>MusicApi: EditMusicGenre(newId, assetDir, UTAGE_GENRE)
MusicEdit->>MusicApi: SaveMusic(newId, assetDir)
MusicApi->>MusicStore: updateMusicList()
MusicEdit-->>Editor: Select updated music
end
将宴会场乐曲切回其他流派的时序图sequenceDiagram
actor Editor
participant MusicEdit
participant MusicApi
participant MusicStore
Editor->>MusicEdit: Change genre from Utage
MusicEdit->>MusicEdit: Calculate newId = oldId - 100000
alt Target ID is occupied
MusicEdit-->>Editor: Show conflict dialog
MusicEdit->>MusicEdit: revertGenre(oldGenreId)
else Confirmation accepted
MusicEdit->>MusicApi: ModifyId(oldId, assetDir, newId)
MusicEdit->>MusicApi: EditMusicGenre(newId, assetDir, newGenreId)
MusicEdit->>MusicApi: SaveMusic(newId, assetDir)
MusicApi->>MusicStore: updateMusicList()
MusicEdit-->>Editor: Select updated music
end
文件级变更
提示与命令与 Sourcery 互动
自定义使用体验访问你的控制面板,以便:
获取帮助Original review guide in EnglishReviewer's Guide本 PR 在音乐编辑页拦截流派与宴会场之间的切换,通过确认和冲突校验协调 ID 偏移、唯一启用谱面向 Basic 槽位转换及流派保存;服务端负责实际谱面文件和元数据迁移,前端 API、状态回滚和多语言提示同步完成。 Sequence diagram for switching a music track to UtagesequenceDiagram
actor Editor
participant MusicEdit
participant MusicApi
participant MusicController
participant MusicStore
Editor->>MusicEdit: Change genre to Utage
MusicEdit->>MusicStore: Check enabled charts and target ID
alt Invalid chart state or occupied target ID
MusicEdit-->>Editor: Show error dialog
MusicEdit->>MusicEdit: revertGenre(oldGenreId)
else Conversion confirmed
opt Non-Basic chart enabled
MusicEdit->>MusicController: ConvertSingleChartToBasic(oldId, assetDir)
MusicController->>MusicController: Move chart files and metadata to Basic
MusicController-->>MusicEdit: Success or error
end
opt oldId < 100000
MusicEdit->>MusicApi: ModifyId(oldId, assetDir, newId)
end
MusicEdit->>MusicApi: EditMusicGenre(newId, assetDir, UTAGE_GENRE)
MusicEdit->>MusicApi: SaveMusic(newId, assetDir)
MusicApi->>MusicStore: updateMusicList()
MusicEdit-->>Editor: Select updated music
end
Sequence diagram for reverting Utage music genresequenceDiagram
actor Editor
participant MusicEdit
participant MusicApi
participant MusicStore
Editor->>MusicEdit: Change genre from Utage
MusicEdit->>MusicEdit: Calculate newId = oldId - 100000
alt Target ID is occupied
MusicEdit-->>Editor: Show conflict dialog
MusicEdit->>MusicEdit: revertGenre(oldGenreId)
else Confirmation accepted
MusicEdit->>MusicApi: ModifyId(oldId, assetDir, newId)
MusicEdit->>MusicApi: EditMusicGenre(newId, assetDir, newGenreId)
MusicEdit->>MusicApi: SaveMusic(newId, assetDir)
MusicApi->>MusicStore: updateMusicList()
MusicEdit-->>Editor: Select updated music
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
All reported issues were addressed across 6 files
Reply to a comment to ask cubic a question or push back. It learns from your replies.
View guided diff | Re-trigger cubic
There was a problem hiding this comment.
嗨——我发现了 11 个问题
给 AI 代理的提示
请处理这次代码审查中的评论:
## 单条评论
### 评论 1
<location path="MaiChartManager/Controllers/Music/MusicController.cs" line_range="153-160" />
<code_context>
+ // 注意:path 本身不动,槽位还是指向自己原来的文件名。
+ foreach (var suffix in new[] { "", "_L", "_R" })
+ {
+ var sourcePath = Path.Combine(musicDir, sourceChart.Path.Replace(".ma2", suffix + ".ma2"));
+ var targetPath = Path.Combine(musicDir, targetChart.Path.Replace(".ma2", suffix + ".ma2"));
+ if (!System.IO.File.Exists(sourcePath)) continue;
+ var tempPath = sourcePath + ".mcmswap";
+ PlatformFile.MoveFile(sourcePath, tempPath);
+ if (System.IO.File.Exists(targetPath)) PlatformFile.MoveFile(targetPath, sourcePath);
+ PlatformFile.MoveFile(tempPath, targetPath);
+ }
+
+ // 把原难度的谱面信息搬到 Basic 槽位并启用 Basic,原难度禁用
</code_context>
<issue_to_address>
**谱面路径可能逃逸出乐曲文件夹**
当已配置资源中的歌曲 XML 包含一个解析后指向乐曲目录之外的谱面路径,且调用方调用转换接口时,`Path.Combine` 会直接接受 `sourceChart.Path` 和 `targetChart.Path`,而不检查解析后的路径是否仍位于 `musicDir` 下;随后 `PlatformFile.MoveFile` 就会移动或覆盖歌曲文件夹之外的文件。
解析并规范化这两个路径,拒绝任何位于 `musicDir` 之外的路径,然后再移动文件。
</issue_to_address>
### 评论 2
<location path="MaiChartManager/Controllers/Music/MusicController.cs" line_range="129-130" />
<code_context>
+ /// 宴会场只识别 Basic 槽位的谱面,所以把普通乐曲改成宴会场之前需要先把选中的难度整体挪到 Basic 槽位。
+ /// 返回空字符串表示成功,否则是错误信息。
+ /// </summary>
+ [HttpPost]
+ public string ConvertSingleChartToBasic(int id, string assetDir)
+ {
+ var music = settings.GetMusic(id, assetDir);
</code_context>
<issue_to_address>
**未认证客户端可以交换谱面**
当应用以导出模式运行、使用默认的 `UseAuth` 设置,且未认证客户端能够访问 5001 端口时,新增的 POST 操作没有针对该操作的授权要求,因此远程调用方可以调用它来移动谱面文件并改写歌曲的谱面元数据。
只要该修改操作可通过网络访问,就应要求用户通过身份验证;或者将暴露的监听器限制为可信接口。
</issue_to_address>
### 评论 3
<location path="MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx" line_range="84" />
<code_context>
+ const needChangeId = newId !== oldId;
+
+ // 改 ID 时目标 ID 上的乐曲会被删掉,先确认没有同 ID 的乐曲,避免误删
+ if (needChangeId && musicList.value.some(music => music.id === newId)) {
+ await showTransactionalDialog(
+ t('music.edit.utageConvertTitle'),
</code_context>
<issue_to_address>
**目标位置中已有的文件会被删除**
如果目标 ID 对应的目录或歌曲是在客户端冲突检查后创建的,或者存在没有对应 `musicList` 条目的文件,`ModifyId` 会在移动源文件前删除目标目录。因此,前端对 `musicList` 的检查无法保护并发创建或列表中未包含的文件,目标位置已有的文件会丢失。
在 `ModifyId` 中以原子方式拒绝已存在的目标目录,且必须在删除或移动任何文件之前完成检查。
另请查看 `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:116`、`MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:142-164`。
</issue_to_address>
### 评论 4
<location path="MaiChartManager/Controllers/Music/MusicController.cs" line_range="157-160" />
<code_context>
+ var targetPath = Path.Combine(musicDir, targetChart.Path.Replace(".ma2", suffix + ".ma2"));
+ if (!System.IO.File.Exists(sourcePath)) continue;
+ var tempPath = sourcePath + ".mcmswap";
+ PlatformFile.MoveFile(sourcePath, tempPath);
+ if (System.IO.File.Exists(targetPath)) PlatformFile.MoveFile(targetPath, sourcePath);
+ PlatformFile.MoveFile(tempPath, targetPath);
+ }
+
+ // 把原难度的谱面信息搬到 Basic 槽位并启用 Basic,原难度禁用
</code_context>
<issue_to_address>
**交换失败会导致谱面文件受损**
如果后缀循环执行到一半时文件移动失败,`PlatformFile.MoveFile` 可能会在源文件已移动到 `.mcmswap` 或仅部分变体完成交换后抛出异常。由于接口没有回滚逻辑,谱面文件会不完整,或与其元数据不一致,导致谱面无法使用。
跟踪已完成的移动操作;如果后缀循环中的任何移动失败,则将这些操作回滚。
另请查看 `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:113-120`。
</issue_to_address>
### 评论 5
<location path="MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx" line_range="117-124" />
<code_context>
+ if (error) throw new Error(error);
+ }
+ if (needChangeId) {
+ await api.ModifyId(oldId, selectedADir.value, newId);
+ }
+ await api.EditMusicGenre(newId, selectedADir.value, UTAGE_GENRE);
+ await api.SaveMusic(newId, selectedADir.value);
+ await updateMusicList();
+ if (needChangeId) selectMusicId.value = newId;
+ } catch (e) {
+ revertGenre(oldGenreId);
+ await globalCapture(e, t('music.edit.utageConvertFailed'));
+ }
</code_context>
<issue_to_address>
**转换失败会导致歌曲只完成部分更改**
如果谱面转换或 ID 移动成功后,后续请求失败,`ConvertSingleChartToBasic`、`ModifyId`、`EditMusicGenre` 和 `SaveMusic` 可能会依次持久化更改,但 `catch` 只会为 UI 调用 `revertGenre`;已存储的谱面、ID 或曲风仍可能处于部分转换状态,而用户看到的却是失败提示。
将整个转换流程做成事务,确保后续请求失败时能还原所有已持久化的谱面、ID 和曲风更改。
另请查看 `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:170-172`。
</issue_to_address>
### 评论 6
<location path="MaiChartManager/Controllers/Music/MusicController.cs" line_range="155" />
<code_context>
+ {
+ var sourcePath = Path.Combine(musicDir, sourceChart.Path.Replace(".ma2", suffix + ".ma2"));
+ var targetPath = Path.Combine(musicDir, targetChart.Path.Replace(".ma2", suffix + ".ma2"));
+ if (!System.IO.File.Exists(sourcePath)) continue;
+ var tempPath = sourcePath + ".mcmswap";
+ PlatformFile.MoveFile(sourcePath, tempPath);
</code_context>
<issue_to_address>
**缺失的谱面仍会被启用为 Basic**
如果已启用的非 Basic 谱面缺少 `.ma2` 文件,`ConvertSingleChartToBasic` 会跳过缺失的源文件,却仍将其元数据复制到 Basic、启用该槽位、保存并报告成功;这样会把旧的 Basic 文件当作转换后的谱面显示,或者导致已启用的槽位没有对应文件。
如果已启用谱面所需的源文件缺失,应在更改元数据之前拒绝转换。
另请查看 `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:97`。
</issue_to_address>
### 评论 7
<location path="MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx" line_range="116-119" />
<code_context>
+ const error = (await api.ConvertSingleChartToBasic(oldId, selectedADir.value)).data;
+ if (error) throw new Error(error);
+ }
+ if (needChangeId) {
+ await api.ModifyId(oldId, selectedADir.value, newId);
+ }
+ await api.EditMusicGenre(newId, selectedADir.value, UTAGE_GENRE);
+ await api.SaveMusic(newId, selectedADir.value);
+ await updateMusicList();
</code_context>
<issue_to_address>
**无实际效果的 ID 更改会导致选择丢失**
当应用在未激活许可证的 Windows 上运行,且曲风更改需要变更 ID 时,`ModifyId` 会在未更改 ID 的情况下成功返回;但前端仍会使用新 ID 执行曲风修改、保存、刷新和选择操作。这些请求会指向不存在的歌曲,原歌曲也会从编辑器的选中状态中消失。
让 `ModifyId` 报告是否实际更改了 ID;如果没有更改,则以错误终止转换。
另请查看 `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:164-165`。
</issue_to_address>
### 评论 8
<location path="MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx" line_range="117" />
<code_context>
+ if (error) throw new Error(error);
+ }
+ if (needChangeId) {
+ await api.ModifyId(oldId, selectedADir.value, newId);
+ }
+ await api.EditMusicGenre(newId, selectedADir.value, UTAGE_GENRE);
</code_context>
<issue_to_address>
**分段谱面的配套文件会丢失**
如果已启用谱面有 `_L.ma2` 或 `_R.ma2` 配套文件,并且其 ID 发生变化,`ModifyId` 会重命名主 `.ma2` 文件并更新 XML 路径,却会将配套文件留在旧名称下;预览和导出会根据更新后的路径推导配套文件路径,因此找不到这些文件。
当 `ModifyId` 更改谱面路径时,也应重命名 `_L.ma2` 和 `_R.ma2` 配套文件。
</issue_to_address>
### 评论 9
<location path="MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx" line_range="113" />
<code_context>
+
+ try {
+ if (needConvertChart) {
+ const error = (await api.ConvertSingleChartToBasic(oldId, selectedADir.value)).data;
+ if (error) throw new Error(error);
+ }
</code_context>
<issue_to_address>
**跨目录时可能转换错误的歌曲**
用户在确认转换后、等待中的请求完成前切换所选资源目录时,每个请求都会重新读取 `selectedADir.value`。因此,若在等待步骤之间切换目录,后续的 ID 和曲风更改就会发送到与谱面转换不同的目录。原歌曲会处于部分转换状态,而另一首 ID 相同的歌曲可能被重命名或改为 Utage 曲风。
在第一个 `await` 之前捕获资源目录和歌曲身份,并在整个流程中使用这些已捕获的值;在流程完成前阻止更改选择。
另请查看 `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:117`、`MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:119`、`MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:164`。
</issue_to_address>
### 评论 10
<location path="MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx" line_range="101-105" />
<code_context>
+ const messageKey = needChangeId
+ ? (needConvertChart ? 'utageConvertConfirmBoth' : 'utageConvertConfirmIdOnly')
+ : (needConvertChart ? 'utageConvertConfirmChartOnly' : 'utageConvertConfirmPlain');
+ const confirmed = await showTransactionalDialog(
+ t('music.edit.utageConvertTitle'),
+ t(`music.edit.${messageKey}`, { oldId, newId, difficulty }),
+ [{ text: t('common.cancel'), action: false }, { text: t('common.confirm'), action: true }]
+ );
+ if (!confirmed) {
+ revertGenre(oldGenreId);
</code_context>
<issue_to_address>
**切换选择会导致转换错误的歌曲**
当转换或还原对话框打开时,如果用户更改了所选歌曲或资源目录,`changeGenreToUtage` 在确认对话框返回后会使用当前的 `info.value` 和 `selectedADir.value` 继续执行,但启用谱面的索引是在对话框打开前取得的。如果用户在对话框打开期间切换歌曲或资源目录,转换可能会应用于另一首歌曲,或回滚那首歌曲的曲风。
在第一个 `await` 之前捕获歌曲身份和资源目录;如果选择发生变化,则中止操作或重新验证。
</issue_to_address>
### 评论 11
<location path="MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx" line_range="80" />
<code_context>
+ }
+
+ const oldId = info.value.id!;
+ const newId = oldId < 1e5 ? oldId + 1e5 : oldId;
+ const needChangeId = newId !== oldId;
+
</code_context>
<issue_to_address>
**ID 未先增加就被减小**
当一首非 Utage 歌曲的 ID 原本就大于 100000,将其改为 Utage 后再改回来时,恢复流程会根据当前 ID 推断是否要减去 100000;但转换流程会保持已经大于或等于 100000 的 ID 不变。因此,这类歌曲切换为 Utage 后再切回来,会被分配一个比原始 ID 小 100000 的 ID。
记录此次转换是否更改了 ID;只有在同一次转换确实增加了 100000 的情况下,恢复时才减去 100000。
另请查看 `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:134`。
</issue_to_address>Sourcery 评估
需要人工审查。 还有 11 项问题需要先处理。此外,转换谱面会交换源谱面与 Basic 谱面文件、覆盖 Basic 槽位的元数据,并禁用源槽位。将曲风改回去只会还原曲风和可能更改的 ID,并不会还原原始谱面文件或元数据,因此误操作可能导致已持久化的谱面数据需要人工修复。
阻塞性问题:MaiChartManager/Controllers/Music/MusicController.cs:160、MaiChartManager/Controllers/Music/MusicController.cs:130、MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:84、MaiChartManager/Controllers/Music/MusicController.cs:160、MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:124,以及另外 6 项
Original comment in English
Hey - I've found 11 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="MaiChartManager/Controllers/Music/MusicController.cs" line_range="153-160" />
<code_context>
+ // 注意:path 本身不动,槽位还是指向自己原来的文件名。
+ foreach (var suffix in new[] { "", "_L", "_R" })
+ {
+ var sourcePath = Path.Combine(musicDir, sourceChart.Path.Replace(".ma2", suffix + ".ma2"));
+ var targetPath = Path.Combine(musicDir, targetChart.Path.Replace(".ma2", suffix + ".ma2"));
+ if (!System.IO.File.Exists(sourcePath)) continue;
+ var tempPath = sourcePath + ".mcmswap";
+ PlatformFile.MoveFile(sourcePath, tempPath);
+ if (System.IO.File.Exists(targetPath)) PlatformFile.MoveFile(targetPath, sourcePath);
+ PlatformFile.MoveFile(tempPath, targetPath);
+ }
+
+ // 把原难度的谱面信息搬到 Basic 槽位并启用 Basic,原难度禁用
</code_context>
<issue_to_address>
**Chart paths escape the music folder**
When a song XML in the configured assets contains a chart path that resolves outside its music directory and a caller invokes the conversion endpoint, `Path.Combine` accepts `sourceChart.Path` and `targetChart.Path` without checking that the resolved paths remain under `musicDir`; `PlatformFile.MoveFile` then moves or overwrites files outside the song folder.
Resolve and canonicalize both paths, reject any path outside `musicDir`, and only then move files.
</issue_to_address>
### Comment 2
<location path="MaiChartManager/Controllers/Music/MusicController.cs" line_range="129-130" />
<code_context>
+ /// 宴会场只识别 Basic 槽位的谱面,所以把普通乐曲改成宴会场之前需要先把选中的难度整体挪到 Basic 槽位。
+ /// 返回空字符串表示成功,否则是错误信息。
+ /// </summary>
+ [HttpPost]
+ public string ConvertSingleChartToBasic(int id, string assetDir)
+ {
+ var music = settings.GetMusic(id, assetDir);
</code_context>
<issue_to_address>
**Unauthenticated clients can swap charts**
When the application runs in export mode with the default `UseAuth` setting and an unauthenticated client can reach port 5001, the new POST action has no action-level authorization, so a remote caller can invoke it to move chart files and rewrite a song's chart metadata.
Require authentication for this mutating endpoint whenever it is reachable over the network, or restrict the exposed listener to trusted interfaces.
</issue_to_address>
### Comment 3
<location path="MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx" line_range="84" />
<code_context>
+ const needChangeId = newId !== oldId;
+
+ // 改 ID 时目标 ID 上的乐曲会被删掉,先确认没有同 ID 的乐曲,避免误删
+ if (needChangeId && musicList.value.some(music => music.id === newId)) {
+ await showTransactionalDialog(
+ t('music.edit.utageConvertTitle'),
</code_context>
<issue_to_address>
**Existing destination files are deleted**
When the target ID has a directory or music created after the client-side collision check, or files without a matching entry in `musicList`, `ModifyId` deletes the destination directory before moving the source, so the frontend check at `musicList` cannot protect files created concurrently or omitted from the list; the target's existing files are lost.
Make `ModifyId` reject an existing destination directory atomically before deleting or moving any files.
Also at `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:116`, `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:142-164`.
</issue_to_address>
### Comment 4
<location path="MaiChartManager/Controllers/Music/MusicController.cs" line_range="157-160" />
<code_context>
+ var targetPath = Path.Combine(musicDir, targetChart.Path.Replace(".ma2", suffix + ".ma2"));
+ if (!System.IO.File.Exists(sourcePath)) continue;
+ var tempPath = sourcePath + ".mcmswap";
+ PlatformFile.MoveFile(sourcePath, tempPath);
+ if (System.IO.File.Exists(targetPath)) PlatformFile.MoveFile(targetPath, sourcePath);
+ PlatformFile.MoveFile(tempPath, targetPath);
+ }
+
+ // 把原难度的谱面信息搬到 Basic 槽位并启用 Basic,原难度禁用
</code_context>
<issue_to_address>
**Failed swaps leave chart files damaged**
When a file move fails partway through the suffix loop, `PlatformFile.MoveFile` can throw after moving a source to `.mcmswap` or swapping only some variants, leaving chart files incomplete while the endpoint has no rollback; the chart becomes unusable or disagrees with its metadata.
Track completed moves and roll them back if any move in the suffix loop fails.
Also at `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:113-120`.
</issue_to_address>
### Comment 5
<location path="MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx" line_range="117-124" />
<code_context>
+ if (error) throw new Error(error);
+ }
+ if (needChangeId) {
+ await api.ModifyId(oldId, selectedADir.value, newId);
+ }
+ await api.EditMusicGenre(newId, selectedADir.value, UTAGE_GENRE);
+ await api.SaveMusic(newId, selectedADir.value);
+ await updateMusicList();
+ if (needChangeId) selectMusicId.value = newId;
+ } catch (e) {
+ revertGenre(oldGenreId);
+ await globalCapture(e, t('music.edit.utageConvertFailed'));
+ }
</code_context>
<issue_to_address>
**Failed conversion leaves song partially changed**
When a later request fails after chart conversion or an ID move succeeds, `ConvertSingleChartToBasic`, `ModifyId`, `EditMusicGenre` and `SaveMusic` can persist changes in sequence, but the catch only calls `revertGenre` for the UI; the stored chart, ID or genre can remain partially converted while the user sees a failure.
Make the conversion sequence transactional so a failed later request restores every persisted chart, ID and genre change.
Also at `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:170-172`.
</issue_to_address>
### Comment 6
<location path="MaiChartManager/Controllers/Music/MusicController.cs" line_range="155" />
<code_context>
+ {
+ var sourcePath = Path.Combine(musicDir, sourceChart.Path.Replace(".ma2", suffix + ".ma2"));
+ var targetPath = Path.Combine(musicDir, targetChart.Path.Replace(".ma2", suffix + ".ma2"));
+ if (!System.IO.File.Exists(sourcePath)) continue;
+ var tempPath = sourcePath + ".mcmswap";
+ PlatformFile.MoveFile(sourcePath, tempPath);
</code_context>
<issue_to_address>
**Missing chart is enabled as Basic**
When the enabled non-Basic chart's `.ma2` file is missing, `ConvertSingleChartToBasic` skips the missing source file but still copies its metadata to Basic, enables the slot, saves and reports success; the old Basic file is presented as the converted chart or the enabled slot has no file.
Reject the conversion before changing metadata when the enabled chart's required source file is missing.
Also at `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:97`.
</issue_to_address>
### Comment 7
<location path="MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx" line_range="116-119" />
<code_context>
+ const error = (await api.ConvertSingleChartToBasic(oldId, selectedADir.value)).data;
+ if (error) throw new Error(error);
+ }
+ if (needChangeId) {
+ await api.ModifyId(oldId, selectedADir.value, newId);
+ }
+ await api.EditMusicGenre(newId, selectedADir.value, UTAGE_GENRE);
+ await api.SaveMusic(newId, selectedADir.value);
+ await updateMusicList();
</code_context>
<issue_to_address>
**No-op ID change loses selection**
When the app runs on Windows without an active license and the genre change requires an ID change, `ModifyId` returns successfully without changing the ID, but the frontend continues with the new ID for genre, save, refresh and selection; the requests target nonexistent music and the unchanged song disappears from the editor's selection.
Have `ModifyId` report whether it changed the ID and stop the conversion with an error when it did not.
Also at `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:164-165`.
</issue_to_address>
### Comment 8
<location path="MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx" line_range="117" />
<code_context>
+ if (error) throw new Error(error);
+ }
+ if (needChangeId) {
+ await api.ModifyId(oldId, selectedADir.value, newId);
+ }
+ await api.EditMusicGenre(newId, selectedADir.value, UTAGE_GENRE);
</code_context>
<issue_to_address>
**Split chart companions go missing**
When the enabled chart has `_L.ma2` or `_R.ma2` companion files and its ID changes, `ModifyId` renames the main `.ma2` file and updates its XML path but leaves the companion files at their old names; preview and export derive companion paths from the updated path and cannot find them.
Rename the `_L.ma2` and `_R.ma2` companion files when `ModifyId` changes the chart path.
</issue_to_address>
### Comment 9
<location path="MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx" line_range="113" />
<code_context>
+
+ try {
+ if (needConvertChart) {
+ const error = (await api.ConvertSingleChartToBasic(oldId, selectedADir.value)).data;
+ if (error) throw new Error(error);
+ }
</code_context>
<issue_to_address>
**Wrong song converted across directories**
When the user switches the selected asset directory after confirming a conversion but before its awaited requests finish, each request reads `selectedADir.value` anew, so a directory switch between awaited steps sends later ID and genre changes to a different directory than the chart conversion. The original song is left partially converted and another song with the same ID can be renamed or changed to Utage.
Capture the asset directory and song identity before the first await, use those captured values throughout the sequence, and prevent selection changes until it completes.
Also at `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:117`, `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:119`, `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:164`.
</issue_to_address>
### Comment 10
<location path="MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx" line_range="101-105" />
<code_context>
+ const messageKey = needChangeId
+ ? (needConvertChart ? 'utageConvertConfirmBoth' : 'utageConvertConfirmIdOnly')
+ : (needConvertChart ? 'utageConvertConfirmChartOnly' : 'utageConvertConfirmPlain');
+ const confirmed = await showTransactionalDialog(
+ t('music.edit.utageConvertTitle'),
+ t(`music.edit.${messageKey}`, { oldId, newId, difficulty }),
+ [{ text: t('common.cancel'), action: false }, { text: t('common.confirm'), action: true }]
+ );
+ if (!confirmed) {
+ revertGenre(oldGenreId);
</code_context>
<issue_to_address>
**Switching selection converts wrong song**
When the user changes the selected song or asset directory while a conversion or revert dialog is open, `changeGenreToUtage` resumes after the confirmation dialog using the current `info.value` and `selectedADir.value`, but its enabled-chart indexes came from before the dialog. If the user switches songs or asset directories while the dialog is open, the conversion can apply to a different song or roll back that song’s genre.
Capture the music identity and asset directory before the first await, and abort or revalidate the operation if the selection changes.
</issue_to_address>
### Comment 11
<location path="MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx" line_range="80" />
<code_context>
+ }
+
+ const oldId = info.value.id!;
+ const newId = oldId < 1e5 ? oldId + 1e5 : oldId;
+ const needChangeId = newId !== oldId;
+
</code_context>
<issue_to_address>
**ID is reduced without prior increase**
When a non-Utage song already has an ID greater than 100000 and is changed to Utage, then changed back, the return path infers whether to subtract 100000 from the current ID, but the conversion path leaves IDs already at or above 100000 unchanged. Switching such a song to Utage and back therefore assigns it an ID 100000 lower than its original ID.
Record whether this conversion changed the ID and only subtract 100000 on reversion when that same conversion added it.
Also at `MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:134`.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 11 findings to address first, and converting a chart swaps the source and Basic chart files, overwrites the Basic slot’s metadata, and disables the source slot. Changing the genre back only restores the genre and possibly ID; it does not restore the original chart files or metadata, so a mistaken conversion can leave persisted chart data needing manual repair.
Blocking findings: MaiChartManager/Controllers/Music/MusicController.cs:160, MaiChartManager/Controllers/Music/MusicController.cs:130, MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:84, MaiChartManager/Controllers/Music/MusicController.cs:160, MaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:124, and 6 more
- 谱面转换、ID 变更(±100000)、流派变更合并到 ChangeMusicGenreApi,失败时回滚谱面转换,避免半改状态 - 抽出 ModifyIdCore,供 ModifyId 与流派切换复用 - 转换前校验:统计全部 6 个谱面槽位、源难度必须有谱面文件、双人谱(L/R)直接拒绝并提示 - 谱面文件交换改为临时名三段式 + 移动日志 + 逆序回滚,失败不再留下改了一半的 XML - ID 冲突只在当前资源目录内判断,冲突时提示是否覆盖该 ID 上的乐曲 - 文案:多难度改为「只保留一个启用的难度」,ID 占用提示不再让用户「改用其他 ID」
- 谱面转换前校验 Basic / 源难度的谱面路径都在乐曲目录内,防止 XML 里的 ../ 影响其他文件 - ModifyIdCore 重命名谱面时同时处理双人谱的 _L / _R 文件,改 ID 后不会留下旧 ID 命名的分段谱面
- 流派由宴会场切回其他流派时,先检查乐曲是否带宴会场特有的 L / R 谱面(_L / _R 文件或 utagePlayStyle=1),命中直接返回 double-player-chart 拒绝,不再改 ID 和流派 - 前端同步提前拦截,提示文案改为通用表述
There was a problem hiding this comment.
All reported issues were addressed across 8 files (changes from recent commits).
Tip: Get a Slack DM when cubic finishes reviewing your PRs. Connect Slack
View guided diff | Re-trigger cubic
- MoveToBasic / RestoreFromBasic 区分转换与回滚方向:回滚不再要求源难度谱面文件存在,文件交换支持单边缺失,回滚失败会记录日志
- 谱面路径校验按平台区分大小写(Windows 不敏感 / Linux 敏感)
- 前端补齐所有后端错误码的文案并映射,不再把原始错误码显示给用户
- 失败弹窗与覆盖确认弹窗的标题按操作方向传入,不再固定显示「更改为宴会场」
- 错误文案统一传入目标 ID,修复 {newId} 占位符未替换的问题
There was a problem hiding this comment.
All reported issues were addressed across 6 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
- 移除 info.value.genreId 的 watch,改用 GenreInput 的 update:value 回调,只有用户主动改流派才会进入宴会场转换流程 - 换歌/刷新列表导致 genreId 变化时不再触发转换流程,也不会把上一首曲子的流派回滚到当前曲子上 - GenreInput 声明 emits: ['update:value'],让 TSX 里能精确拦截用户选择 - 回滚流派时不再写入 undefined,也不再需要用 disableSync 绕开 watch
- 后端按「宴会场 ID = 序号 * 10000 + 原曲后四位」顺延找一个空闲 ID(序号 10~99),全部占用时返回 no-available-id - 移除 ChangeMusicGenreApi 的 overwrite 参数与前端覆盖确认流程,避免覆盖时把别的乐曲整个删掉 - 前端不再自己推算目标 ID,确认文案改为说明后四位保持不变,最终 ID 以后端返回的 newId 为准 - 补充 no-available-id 的简体/繁体/英文文案,删掉不再使用的覆盖文案
- 流派是宴会场但 ID 不是宴会场 ID(历史数据、手动改过)时,切回其它流派不再弹窗也不再改 ID,因为没有可恢复的 ID - 宴会场序号 ≥ 20 时减 100000 推不出普通曲 ID,前后端都改成明确提示用户手动改 ID,不再产生 2xxxx 之类的无效 ID - 宴会场 ID 的判定收敛到 consts.ts 的 isUtageId/UTAGE_ID_START 与 UtageConverter.IsUtageId,避免散落的 1e5 字面量
- 失败分支不再把错误码直接拼进用户文案:有对应文案的错误码走提示弹窗 - 没有文案的错误码(unexpected 等)改为走 globalCapture 上报 Sentry 并显示带 errorId 的全局错误弹窗
- 之前不论 ModifyIdCore 因为什么失败都映射成 invalid-music-directory,提示词会误导用户去查目录 - 新增 id-change-failed 错误码与三语文案
- requestGenreChange 之前用 selectedADir,和文件里其它修改都用的 info.value.assetDir 不一致,跨目录场景下语义不同 - 改为统一取乐曲自身的 assetDir
- 后端 FindSingleEnabledChart 遍历了 Music.xml 全部 6 个槽位,前端只数 Basic~Re:Master 这 5 个,两边口径不一致 - 抽出 DifficultyCount 常量,后端只统计前 5 个槽位
- 该文件相对 main 只有文件末尾换行的差异,属于噪音,还原成 main 的版本
- 控制器目录下不该放非控制器类,按 Charts/Services 的先例下沉到 Controllers/Music/Services - 更新 Controllers/AGENTS.md 的目录说明
- 原来用 id % 1e5 + 1e5,DX 曲会算成 115000(序号 11),而宴会场 ID 规则是「序号 * 10000 + 原曲后四位」,应以序号 10 起步得到 105000 - 宴会场 ID 本身不区分 STD/DX,切回普通流派时改为按 isDx 把 10000 补回来,不再依赖取模猜 - 输入框下限同步修正:宴会场模式下不再叠加 DX 的 10000 - consts.ts 抽出 UTAGE_SEQ_START,UTAGE_ID_START 由它推导
There was a problem hiding this comment.
All reported issues were addressed across 12 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
- 之前只判断「减 100000 后不在宴会场号段」,序号 12 以后的官方宴谱(如 120005)会算出 20005 这种不存在的普通曲 ID - 普通曲 ID 只有 0~9999(STD)和 10000~19999(DX)两段,落在范围外一律认为推不出来
- 宴会场 ID 是「序号 * 10000 + 原曲后四位」,不含 STD/DX 信息,DX 曲 15003 转成 105003 后只能减回 5003,切回来会变成 STD 曲或误报 target-id-exists - Music.xml 的 X-MCM 节点里记录 utageBaseId(切到宴会场前的原 ID),切回普通流派时优先用它还原,离开宴会场后清掉 - 没有记录的老数据(官方宴谱)仍然按序号硬算
- 之前切流派时无条件按 id % 1e4 重算,普通流派下手输的 11234 会被改成 1234 - 改成只有当前 ID 处于宴会场区间时才还原成普通 ID,本来就已经是普通 ID 时保持用户输入不动
- 导入时可以直接把谱面导入成宴会场曲,这条路径不写 utageBaseId,导致这类曲目以后切回普通流派只能按序号硬算、DX 曲会退回成 STD 曲 - 导入/替换宴会场谱面时按「原曲后四位 + 谱面本身的 STD/DX」记录 utageBaseId,和切流派时记录的值语义一致
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
- 宴会场 ID 中途被改过(例如通过改 ID 对话框)后,UtageBaseId 里的旧原 ID 会被直接采用,切回普通流派时得到和当前 ID 不匹配的普通曲 ID - 现在只有记录值确实是普通曲 ID、且后四位与当前宴会场 ID 一致时才采用,否则退化到按序号推算
- 手动改 ID(改 ID 对话框)走的是 ModifyIdCore,之前不会动 utageBaseId,导致记录陈旧 - 新增 SyncUtageBaseId:新 ID 仍是宴会场 ID 时把记录的后四位一起换掉并保留 STD/DX,改回普通 ID 时清空记录
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
- utageBaseId 原本写在可能 Fatal 返回的谱面写入循环之前,而 GetMusic 返回的是缓存实例,导入失败后脏值会在之后任意一次 Save 时被写进 XML - 挪到谱面写入和 L/R 清理之后再赋值,半路失败就不会留下错误的原曲 ID 记录
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Requires human review: Adds an automatic Utage genre-switch flow that changes music IDs and converts charts; this is a behavior and API expansion that requires human sign-off on the conversion rules and API policy.
View guided diff | Re-trigger cubic
改动内容
在乐曲编辑页把「流派」改成「宴会场」时:
另外,流派由宴会场切回其他流派时会提示,并把 ID 减 100000(ID 大于 100000 时)。
涉及文件
MaiChartManager/Controllers/Music/MusicController.cs:新增ConvertSingleChartToBasicApiMaiChartManager/Front/src/views/Charts/MusicEdit/index.tsx:流派切换拦截、确认弹窗、转换调用链MaiChartManager/Front/src/client/apiGen.ts:genClient 重新生成MaiChartManager/Front/src/locales/{zh,zh-TW,en}.yaml:新增文案测试
本地 Crack 构建(Packaging/Build.ps1 同流程)下验证:普通曲切宴会场(ID +100000、非 Basic 难度转 Basic)、仅 Basic 启用时切宴会场、多难度时拦截、宴会场切回其他流派(ID -100000)。
Sourcery 总结
通过协调曲种变更、ID 更新和谱面可用状态,自动安全地转换普通乐曲与宴谱乐曲。
新功能:
错误修复:
增强功能:
杂项:
Original summary in English
Sourcery 摘要
通过协调曲目类型变更、ID 和已启用谱面数据,实现普通曲目与宴曲之间安全、原子的转换。
新功能:
错误修复:
增强功能:
杂项:
Original summary in English
Sourcery 摘要
在普通类型与宴会(Utage)类型之间切换时,原子性地更新乐曲 ID 和谱面可用性,同时确保数据完整性。
新功能:
错误修复:
增强功能:
维护工作:
Original summary in English
Sourcery 总结
支持在普通曲风与宴曲曲风之间安全、事务性地切换,并协调处理 ID 和谱面转换。
新功能:
错误修复:
改进:
文档:
杂项:
Original summary in English
Sourcery 摘要
支持在普通音乐类型与 Utage 音乐类型之间安全、事务性地进行转换,同时确保 ID、谱面数据和启用状态保持一致。
新功能:
改进:
Original summary in English
Sourcery 总结
支持在宴会曲(Utage)与其他音乐类型之间安全、事务性地切换,同时确保音乐 ID、谱面文件、谱面元数据和启用状态保持一致。
新功能:
错误修复:
优化:
杂项:
Original summary in English
Summary by Sourcery
Support safe, transactional switching between Utage and other music genres while keeping music IDs, chart files, chart metadata, and enabled states consistent.
New Features:
Bug Fixes:
Enhancements:
Chores: