From 2cbdcda64ffc70b1744303672ec7e53ec078879d Mon Sep 17 00:00:00 2001 From: Starrah Date: Wed, 28 Jan 2026 12:37:51 +0800 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=E5=9C=A8Music.xml=E4=B8=AD?= =?UTF-8?q?=E6=96=B0=E5=A2=9E"X-MCM/ShiftMethod"=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=EF=BC=8C=E8=AE=B0=E5=BD=95=E5=AF=BC=E5=85=A5=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E5=BB=B6=E8=BF=9F=E8=B0=83=E6=95=B4=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Charts/ImportChartController.cs | 1 + MaiChartManager/Models/MusicXml.cs | 52 ++++++++++++------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/MaiChartManager/Controllers/Charts/ImportChartController.cs b/MaiChartManager/Controllers/Charts/ImportChartController.cs index 20bf25a0..1466ab5a 100644 --- a/MaiChartManager/Controllers/Charts/ImportChartController.cs +++ b/MaiChartManager/Controllers/Charts/ImportChartController.cs @@ -559,6 +559,7 @@ public ImportChartResult ImportChart( music.AddVersionId = addVersionId; music.GenreId = genreId; music.Version = version; + music.ShiftMethod = shift.ToString(); float wholebpm; if (float.TryParse(maiData.GetValueOrDefault("wholebpm"), out wholebpm)) music.Bpm = wholebpm; diff --git a/MaiChartManager/Models/MusicXml.cs b/MaiChartManager/Models/MusicXml.cs index 86f6da87..7136519f 100644 --- a/MaiChartManager/Models/MusicXml.cs +++ b/MaiChartManager/Models/MusicXml.cs @@ -377,16 +377,7 @@ public string UtageKanji set { Modified = true; - var node = RootNode.SelectSingleNode(utageKanjiNode); - if (node is null) - { - node = xmlDoc.CreateNode(XmlNodeType.Element, utageKanjiNode, null); - node.InnerText = value; - RootNode.AppendChild(node); - return; - } - - RootNode.SelectSingleNode(utageKanjiNode).InnerText = value; + SelectSingleNodeOrCreate(RootNode, utageKanjiNode).InnerText = value; } } @@ -398,16 +389,7 @@ public string Comment set { Modified = true; - var node = RootNode.SelectSingleNode(commentNode); - if (node is null) - { - node = xmlDoc.CreateNode(XmlNodeType.Element, commentNode, null); - node.InnerText = value; - RootNode.AppendChild(node); - return; - } - - RootNode.SelectSingleNode(commentNode).InnerText = value; + SelectSingleNodeOrCreate(RootNode, commentNode).InnerText = value; } } @@ -466,6 +448,18 @@ public bool LongMusic node.InnerText = value ? "1" : "0"; } } + + // 以下是游戏本体不会使用、而是由MCM使用的扩展字段,统一放到"X-MCM"下。 + + public string ShiftMethod + { + get => RootNode.SelectSingleNode("X-MCM/shiftMethod")?.InnerText; + set + { + Modified = true; + SelectSingleNodeOrCreate(RootNode, "X-MCM/shiftMethod").InnerText = value; + } + } public class Chart { @@ -571,4 +565,22 @@ public void Save() Modified = false; xmlDoc.Save(FilePath); } + + private XmlNode SelectSingleNodeOrCreate(XmlNode parent, string xpath) + { + var node = parent.SelectSingleNode(xpath); + if (node is not null) return node; + // 由于xmlDoc.CreateNode不支持xpath,因此当xpath有多层时,必须对每一层依次分别创建 + foreach (var s in xpath.Split('/')) + { + node = parent.SelectSingleNode(s); + if (node is null) + { + node = xmlDoc.CreateNode(XmlNodeType.Element, s, null); + parent.AppendChild(node); + } + parent = node; // 继续下一层的处理 + } + return node; + } } \ No newline at end of file From 104429d481ee0617c36bd41e3dab8a65d25c724a Mon Sep 17 00:00:00 2001 From: Starrah Date: Wed, 28 Jan 2026 12:46:21 +0800 Subject: [PATCH 02/11] =?UTF-8?q?refactor:=20=E5=88=9D=E6=AD=A5=E9=87=8D?= =?UTF-8?q?=E6=9E=84=20ReplaceChartModal.tsx=20=E4=B8=BA=E5=92=8C=E5=85=B6?= =?UTF-8?q?=E4=BB=96=E6=9B=BF=E6=8D=A2=E5=8A=9F=E8=83=BD=E4=B8=80=E6=A0=B7?= =?UTF-8?q?=E7=9A=84=E2=80=9C=E5=B8=A6=E5=8F=AF=E9=80=89fileHandle?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E7=9A=84=E5=87=BD=E6=95=B0=E2=80=9D=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E4=BB=A5=E4=BE=BF=E5=90=8E=E7=BB=AD=E5=BC=80?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DragDropDispatcher/ReplaceChartModal.tsx | 34 +++++++++++++++++-- .../components/DragDropDispatcher/index.tsx | 6 ++-- MaiChartManager/Front/src/locales/en.yaml | 2 ++ MaiChartManager/Front/src/locales/zh-TW.yaml | 2 ++ MaiChartManager/Front/src/locales/zh.yaml | 2 ++ 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx index a0cbf59f..dd3fa835 100644 --- a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx +++ b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx @@ -1,11 +1,14 @@ import { t } from '@/locales'; import { globalCapture, selectedADir, selectedLevel, selectedMusic, selectMusicId, updateMusicList } from '@/store/refs'; -import { NButton, NFlex, NModal, useMessage } from 'naive-ui'; -import { defineComponent, PropType, ref, computed, watch, shallowRef } from 'vue'; +import { NButton, NFlex, NModal, useDialog, useMessage } from 'naive-ui'; +import { defineComponent, shallowRef } from 'vue'; import JacketBox from '../JacketBox'; import { DIFFICULTY, LEVEL_COLOR } from '@/consts'; import api from '@/client/api'; +export let prepareReplaceChart = async (fileHandle?: FileSystemFileHandle) => { +} + export const replaceChartFileHandle = shallowRef(null); export default defineComponent({ @@ -13,6 +16,33 @@ export default defineComponent({ // }, setup(props, { emit }) { const message = useMessage(); + const dialog = useDialog(); + + prepareReplaceChart = async (fileHandle?: FileSystemFileHandle) => { + if (!fileHandle) { + [fileHandle] = await window.showOpenFilePicker({ + id: 'chart', + startIn: 'downloads', + types: [ + { + description: t('music.edit.supportedFileTypes'), + accept: { + "application/x-supported": [".ma2", ".txt"], // 没办法限定只匹配maidata.txt,就只好先把一切txt都作为匹配 + }, + }, + ], + }); + } + if (!fileHandle) return // 用户未选择文件 + const name = fileHandle.name + if (!(name.endsWith(".ma2") || name == "maidata.txt")) { + dialog.error({title: t('error.unsupportedFileType'), content: t('music.edit.notValidChartFile')}) + return + } + // TODO:对ma2和maidata.txt分类讨论,后者执行ImportCheck + console.log(fileHandle.name) + replaceChartFileHandle.value = fileHandle + } const replaceChart = async () => { if (!replaceChartFileHandle.value) return; diff --git a/MaiChartManager/Front/src/components/DragDropDispatcher/index.tsx b/MaiChartManager/Front/src/components/DragDropDispatcher/index.tsx index 86e0d7e6..3cb9fb5a 100644 --- a/MaiChartManager/Front/src/components/DragDropDispatcher/index.tsx +++ b/MaiChartManager/Front/src/components/DragDropDispatcher/index.tsx @@ -5,7 +5,7 @@ import { uploadFlow as uploadFlowMovie } from '@/components/MusicEdit/SetMovieBu import { uploadFlow as uploadFlowAcbAwb } from '@/components/MusicEdit/AcbAwb'; import { selectedADir, selectedMusic } from '@/store/refs'; import { upload as uploadJacket } from '@/components/JacketBox'; -import ReplaceChartModal, { replaceChartFileHandle } from './ReplaceChartModal'; +import ReplaceChartModal, { prepareReplaceChart } from './ReplaceChartModal'; import AquaMaiManualInstaller, { setManualInstallAquaMai } from './AquaMaiManualInstaller'; export const mainDivRef = shallowRef(); @@ -48,8 +48,8 @@ export default defineComponent({ else if (file.kind === 'file' && (firstType.startsWith('image/') || file.name.endsWith('.jpeg') || file.name.endsWith('.jpg') || file.name.endsWith('.png'))) { uploadJacket(file); } - else if (file.kind === 'file' && file.name.endsWith('.ma2')) { - replaceChartFileHandle.value = file; + else if (file.kind === 'file' && (file.name.endsWith('.ma2') || file.name == "maidata.txt")) { + prepareReplaceChart(file); } } } diff --git a/MaiChartManager/Front/src/locales/en.yaml b/MaiChartManager/Front/src/locales/en.yaml index 8dd7b277..75f5943e 100644 --- a/MaiChartManager/Front/src/locales/en.yaml +++ b/MaiChartManager/Front/src/locales/en.yaml @@ -133,6 +133,7 @@ music: replaceChartConfirm: Confirm to replace {level}? replaceChartFailed: Failed to replace chart replaceChartSuccess: Chart replaced successfully + notValidChartFile: Chart file must be .ma2 or maidata.txt. batch: title: Batch Actions batchAndSearch: Batch Actions & Search @@ -456,6 +457,7 @@ error: confirm: Got it file: notSelected: No file selected + unsupportedFileType: 'Unsupported file type' message: notice: Notice saveSuccess: Saved successfully diff --git a/MaiChartManager/Front/src/locales/zh-TW.yaml b/MaiChartManager/Front/src/locales/zh-TW.yaml index da16da80..49fdaf71 100644 --- a/MaiChartManager/Front/src/locales/zh-TW.yaml +++ b/MaiChartManager/Front/src/locales/zh-TW.yaml @@ -126,6 +126,7 @@ music: replaceChartConfirm: 確認要替換 {level} 譜面嗎? replaceChartFailed: 替換譜面失敗 replaceChartSuccess: 替換譜面成功 + notValidChartFile: 譜面檔案必須是 .ma2 或 maidata.txt。 batch: title: 批次操作 batchAndSearch: 批次操作與搜尋 @@ -417,6 +418,7 @@ error: feedbackError: 回饋錯誤 file: notSelected: 未選擇檔案 + unsupportedFileType: '不支援的檔案類型' message: notice: 提示 saveSuccess: 儲存成功 diff --git a/MaiChartManager/Front/src/locales/zh.yaml b/MaiChartManager/Front/src/locales/zh.yaml index f9c02ec5..05b10a85 100644 --- a/MaiChartManager/Front/src/locales/zh.yaml +++ b/MaiChartManager/Front/src/locales/zh.yaml @@ -126,6 +126,7 @@ music: replaceChartConfirm: 确认要替换 {level} 谱面吗? replaceChartFailed: 替换谱面失败 replaceChartSuccess: 替换谱面成功 + notValidChartFile: 谱面文件必须是.ma2或maidata.txt batch: title: 批量操作 batchAndSearch: 批量操作与搜索 @@ -418,6 +419,7 @@ error: feedbackError: 反馈错误 file: notSelected: 未选择文件 + unsupportedFileType: 不支持的文件类型 message: notice: 提示 saveSuccess: 保存成功 From 6f99149fa7f9a0c92d07b770e1371385025d7d84 Mon Sep 17 00:00:00 2001 From: Starrah Date: Wed, 28 Jan 2026 14:16:51 +0800 Subject: [PATCH 03/11] =?UTF-8?q?fix:=20ReplaceChart=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=88=E7=9B=B4=E6=8E=A5=E6=9B=BF=E6=8D=A2MA2=EF=BC=89?= =?UTF-8?q?=E4=B8=8D=E4=BC=9A=E8=87=AA=E5=8A=A8=E4=BF=AE=E6=AD=A3=E9=9F=B3?= =?UTF-8?q?=E7=AC=A6=E6=95=B0=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Charts/ChartController.cs | 12 ++++++++++++ .../Controllers/Charts/ImportChartController.cs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/MaiChartManager/Controllers/Charts/ChartController.cs b/MaiChartManager/Controllers/Charts/ChartController.cs index 91a726a9..bf867b60 100644 --- a/MaiChartManager/Controllers/Charts/ChartController.cs +++ b/MaiChartManager/Controllers/Charts/ChartController.cs @@ -105,5 +105,17 @@ public void ReplaceChart(int id, int level, IFormFile file, string assetDir) file.CopyTo(stream); targetChart.Problems.Clear(); stream.Close(); + + // 检查新谱面ma2的音符数量是否有变化,如果有修正之 + string fileContent; + using (var reader = new StreamReader(file.OpenReadStream())) + { + fileContent = reader.ReadToEnd(); + } + var newMaxNotes = ImportChartController.ParseTNumAllFromMa2(fileContent); + if (newMaxNotes != 0 && targetChart.MaxNotes != newMaxNotes) + { + targetChart.MaxNotes = newMaxNotes; + } } } \ No newline at end of file diff --git a/MaiChartManager/Controllers/Charts/ImportChartController.cs b/MaiChartManager/Controllers/Charts/ImportChartController.cs index 1466ab5a..e5536be0 100644 --- a/MaiChartManager/Controllers/Charts/ImportChartController.cs +++ b/MaiChartManager/Controllers/Charts/ImportChartController.cs @@ -569,7 +569,7 @@ public ImportChartResult ImportChart( } - private static int ParseTNumAllFromMa2(string ma2Content) + public static int ParseTNumAllFromMa2(string ma2Content) { var lines = ma2Content.Split('\n'); // 从后往前读取,因为 T_NUM_ALL 在文件最后 From 3c01ae0e0cee5652ccaa545f6a1f28256d69e33c Mon Sep 17 00:00:00 2001 From: Starrah Date: Wed, 28 Jan 2026 14:21:36 +0800 Subject: [PATCH 04/11] =?UTF-8?q?[WIP]=201.=20ImportChartCheckApi=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=8F=82=E6=95=B0=EF=BC=9AisReplacement=EF=BC=8C?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E8=AF=A5=E6=A3=80=E6=9F=A5=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E6=98=AF=E7=94=B1=E2=80=9C=E6=9B=BF=E6=8D=A2=E8=B0=B1=E9=9D=A2?= =?UTF-8?q?=E2=80=9D=E5=8A=9F=E8=83=BD=E5=8F=91=E8=B5=B7=E7=9A=84=EF=BC=8C?= =?UTF-8?q?=E8=BF=98=E6=98=AF=E6=99=AE=E9=80=9A=E7=9A=84=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E8=B0=B1=E9=9D=A2=E5=8A=9F=E8=83=BD=E5=8F=91=E8=B5=B7=E7=9A=84?= =?UTF-8?q?=E3=80=82=202.=20ReplaceChart=E6=96=B0=E5=A2=9E=E5=8F=82?= =?UTF-8?q?=E6=95=B0shift=EF=BC=8C=E4=BB=85=E5=9C=A8=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E7=9A=84=E6=98=AFMA2=E6=97=B6=E9=9C=80=E8=A6=81=E4=BC=A0?= =?UTF-8?q?=E5=85=A5=EF=BC=8C=E8=A1=A8=E7=A4=BA=E5=BB=B6=E8=BF=9F=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E6=A8=A1=E5=BC=8F=E3=80=82=203.=20=E5=B9=B6=E8=BF=9B?= =?UTF-8?q?=E4=B8=80=E6=AD=A5=E9=87=8D=E6=9E=84=20ReplaceChartModal.tsx?= =?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=8E=9F=E6=9D=A5=E7=9A=84=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E6=9B=BF=E6=8D=A2MA2=E5=8A=9F=E8=83=BD=E5=8F=AF?= =?UTF-8?q?=E7=94=A8=E7=9A=84=E5=90=8C=E6=97=B6=EF=BC=8C=E9=A2=84=E7=95=99?= =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD=E7=9A=84=E7=A9=BA=E9=97=B4=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Charts/ChartController.cs | 4 +- .../Charts/ImportChartController.cs | 9 +- MaiChartManager/Front/src/client/apiGen.ts | 4 + .../DragDropDispatcher/ReplaceChartModal.tsx | 97 +++++++++++-------- .../src/components/MusicEdit/ChartPanel.tsx | 6 +- MaiChartManager/Locale.Designer.cs | 9 ++ MaiChartManager/Locale.resx | 3 + MaiChartManager/Locale.zh-hans.resx | 3 + MaiChartManager/Locale.zh-hant.resx | 3 + 9 files changed, 95 insertions(+), 43 deletions(-) diff --git a/MaiChartManager/Controllers/Charts/ChartController.cs b/MaiChartManager/Controllers/Charts/ChartController.cs index bf867b60..a802a630 100644 --- a/MaiChartManager/Controllers/Charts/ChartController.cs +++ b/MaiChartManager/Controllers/Charts/ChartController.cs @@ -95,10 +95,12 @@ public void EditChartEnable(int id, int level, [FromBody] bool value, string ass } [HttpPost] - public void ReplaceChart(int id, int level, IFormFile file, string assetDir) + public void ReplaceChart(int id, int level, IFormFile file, string assetDir, + [FromForm] ImportChartController.ShiftMethod? shift) { var music = settings.GetMusic(id, assetDir); if (music == null || file == null) return; + // TODO 判断是MA2还是maidata.txt,走不同的逻辑 var targetChart = music.Charts[level]; targetChart.Path = $"{id:000000}_0{level}.ma2"; using var stream = System.IO.File.Open(Path.Combine(StaticSettings.StreamingAssets, assetDir, "music", $"music{id:000000}", targetChart.Path), FileMode.Create); diff --git a/MaiChartManager/Controllers/Charts/ImportChartController.cs b/MaiChartManager/Controllers/Charts/ImportChartController.cs index e5536be0..5776969f 100644 --- a/MaiChartManager/Controllers/Charts/ImportChartController.cs +++ b/MaiChartManager/Controllers/Charts/ImportChartController.cs @@ -191,11 +191,18 @@ public record ImportChartMessage(string Message, MessageLevel Level); public record ImportChartCheckResult(bool Accept, IEnumerable Errors, float MusicPadding, bool IsDx, string? Title, float first, float bar); [HttpPost] - public ImportChartCheckResult ImportChartCheck(IFormFile file) + public ImportChartCheckResult ImportChartCheck(IFormFile file, [FromForm] bool isReplacement = false) { var errors = new List(); var fatal = false; + if (isReplacement) + { + // 替换谱面的操作也需要检查的过程,但检查的逻辑和导入谱面时可以说是一模一样的,故直接共用逻辑 + // 唯一的区别是给用户一个警告,明确说明直接替换谱面功能的适用范围 + errors.Add(new ImportChartMessage(Locale.NotesReplacementWarning, MessageLevel.Warning)); + } + try { var kvps = new SimaiFile(file.OpenReadStream()).ToKeyValuePairs(); diff --git a/MaiChartManager/Front/src/client/apiGen.ts b/MaiChartManager/Front/src/client/apiGen.ts index c0aa133d..7fe258b7 100644 --- a/MaiChartManager/Front/src/client/apiGen.ts +++ b/MaiChartManager/Front/src/client/apiGen.ts @@ -266,6 +266,7 @@ export interface MusicXmlWithABJacket { subLockType?: number; disable?: boolean; longMusic?: boolean; + shiftMethod?: string | null; charts?: Chart[] | null; assetBundleJacket?: string | null; pseudoAssetBundleJacket?: string | null; @@ -965,6 +966,7 @@ export class Api extends HttpClient @@ -1236,6 +1238,8 @@ export class Api extends HttpClient diff --git a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx index dd3fa835..06fb0ae0 100644 --- a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx +++ b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx @@ -1,16 +1,15 @@ import { t } from '@/locales'; import { globalCapture, selectedADir, selectedLevel, selectedMusic, selectMusicId, updateMusicList } from '@/store/refs'; import { NButton, NFlex, NModal, useDialog, useMessage } from 'naive-ui'; -import { defineComponent, shallowRef } from 'vue'; +import { defineComponent, ref, shallowRef } from 'vue'; import JacketBox from '../JacketBox'; import { DIFFICULTY, LEVEL_COLOR } from '@/consts'; import api from '@/client/api'; +import CheckingModal from "@/components/ImportCreateChartButton/ImportChartButton/CheckingModal"; export let prepareReplaceChart = async (fileHandle?: FileSystemFileHandle) => { } -export const replaceChartFileHandle = shallowRef(null); - export default defineComponent({ // props: { // }, @@ -18,6 +17,9 @@ export default defineComponent({ const message = useMessage(); const dialog = useDialog(); + const checking = ref(false); + const ma2Handle = shallowRef(null); + prepareReplaceChart = async (fileHandle?: FileSystemFileHandle) => { if (!fileHandle) { [fileHandle] = await window.showOpenFilePicker({ @@ -33,22 +35,34 @@ export default defineComponent({ ], }); } - if (!fileHandle) return // 用户未选择文件 - const name = fileHandle.name - if (!(name.endsWith(".ma2") || name == "maidata.txt")) { + if (!fileHandle) return; // 用户未选择文件 + + const name = fileHandle.name; + // 对maidata.txt和ma2分类讨论,前者执行ImportCheck + if (name == "maidata.txt") { + try { + checking.value = true; + const file = await fileHandle.getFile(); + const checkRet = (await api.ImportChartCheck({file, isReplacement: true})).data; + if (!checking.value) return; // 说明检查期间用户点击了关闭按钮、取消了操作。则不再执行后续流程。 + // TODO 显示导入界面(类似ErrorDisplayIdInput)、完成导入流程 + console.log(checkRet) + dialog.error({title: "NotImplemented"}) + } finally { + checking.value = false; + } + } else if (name.endsWith(".ma2")) { + ma2Handle.value = fileHandle + } else { dialog.error({title: t('error.unsupportedFileType'), content: t('music.edit.notValidChartFile')}) - return } - // TODO:对ma2和maidata.txt分类讨论,后者执行ImportCheck - console.log(fileHandle.name) - replaceChartFileHandle.value = fileHandle } - const replaceChart = async () => { - if (!replaceChartFileHandle.value) return; + const replaceMa2 = async () => { + if (!ma2Handle.value) return; try { - const file = await replaceChartFileHandle.value.getFile(); - replaceChartFileHandle.value = null; + const file = await ma2Handle.value.getFile(); + ma2Handle.value = null; await api.ReplaceChart(selectMusicId.value, selectedLevel.value, selectedADir.value, { file }); message.success(t('music.edit.replaceChartSuccess')); await updateMusicList(); @@ -58,34 +72,37 @@ export default defineComponent({ } } - return () => replaceChartFileHandle.value = null} - >{{ - default: () =>
- {t('music.edit.replaceChartConfirm', { level: DIFFICULTY[selectedLevel.value!] })} -
{replaceChartFileHandle.value?.name}
-
-
- -
-
#{selectMusicId.value}
-
{selectedMusic.value!.name}
-
-
- {selectedMusic.value!.charts![selectedLevel.value!]?.level}.{selectedMusic.value!.charts![selectedLevel.value!]?.levelDecimal} + return () =>
+ ma2Handle.value = null} + >{{ + default: () =>
+ {t('music.edit.replaceChartConfirm', { level: DIFFICULTY[selectedLevel.value!] })} +
{ma2Handle.value?.name}
+
+
+ +
+
#{selectMusicId.value}
+
{selectedMusic.value!.name}
+
+
+ {selectedMusic.value!.charts![selectedLevel.value!]?.level}.{selectedMusic.value!.charts![selectedLevel.value!]?.levelDecimal} +
-
-
, - footer: () => - replaceChartFileHandle.value = null}>{t('common.cancel')} - {t('common.confirm')} - - }}; +
, + footer: () => + ma2Handle.value = null}>{t('common.cancel')} + {t('common.confirm')} + + }} + checking.value=false} /> +
; }, }); diff --git a/MaiChartManager/Front/src/components/MusicEdit/ChartPanel.tsx b/MaiChartManager/Front/src/components/MusicEdit/ChartPanel.tsx index 1fd51cfd..c780e603 100644 --- a/MaiChartManager/Front/src/components/MusicEdit/ChartPanel.tsx +++ b/MaiChartManager/Front/src/components/MusicEdit/ChartPanel.tsx @@ -1,12 +1,13 @@ import { computed, defineComponent, PropType, watch } from "vue"; import { Chart } from "@/client/apiGen"; -import { NFlex, NForm, NFormItem, NInput, NInputNumber, NSelect, NSwitch } from "naive-ui"; +import { NButton, NFlex, NForm, NFormItem, NInput, NInputNumber, NSelect, NSwitch } from "naive-ui"; import api from "@/client/api"; import { selectedADir, selectedMusic } from "@/store/refs"; import { LEVELS } from "@/consts"; import ProblemsDisplay from "@/components/ProblemsDisplay"; import PreviewChartButton from "@/components/MusicEdit/PreviewChartButton"; import { useI18n } from 'vue-i18n'; +import { prepareReplaceChart } from "@/components/DragDropDispatcher/ReplaceChartModal"; const LEVELS_OPTIONS = LEVELS.map((level, index) => ({label: level, value: index})); @@ -43,6 +44,9 @@ export default defineComponent({ + prepareReplaceChart()}> + {t('music.edit.replaceChart')} + diff --git a/MaiChartManager/Locale.Designer.cs b/MaiChartManager/Locale.Designer.cs index 98c5e497..4961999e 100644 --- a/MaiChartManager/Locale.Designer.cs +++ b/MaiChartManager/Locale.Designer.cs @@ -630,6 +630,15 @@ internal static string MusicNoTitle { } } + /// + /// Looks up a localized string similar to Caution! This "Replace Chart" function should only be used when modifying the note content of the existing chart, while the audio remains unchanged, the &first offset remains unchanged, and the timing of the first note remains unchanged. Otherwise, you will need to delete the entire chart and re-import it.. + /// + internal static string NotesReplacementWarning { + get { + return ResourceManager.GetString("NotesReplacementWarning", resourceCulture); + } + } + /// /// Looks up a localized string similar to Please enter activation code. /// diff --git a/MaiChartManager/Locale.resx b/MaiChartManager/Locale.resx index 20f9e434..b89915e8 100644 --- a/MaiChartManager/Locale.resx +++ b/MaiChartManager/Locale.resx @@ -293,4 +293,7 @@ If you notice any issues with the conversion result, you can try testing it in A Automatically run in server mode and minimize to the system tray upon startup. + + Caution! This "Replace Chart" function should only be used when modifying the note content of the existing chart, while the audio remains unchanged, the &first offset remains unchanged, and the timing of the first note remains unchanged. Otherwise, you will need to delete the entire chart and re-import it. + \ No newline at end of file diff --git a/MaiChartManager/Locale.zh-hans.resx b/MaiChartManager/Locale.zh-hans.resx index f6ddf3c9..810a0f5d 100644 --- a/MaiChartManager/Locale.zh-hans.resx +++ b/MaiChartManager/Locale.zh-hans.resx @@ -285,4 +285,7 @@ 开机自动以服务器模式运行并最小化到托盘 + + 注意!本“替换谱面”功能仅限用于:谱面音符内容在原来的基础上发生修改,且音频内容未变、&first偏移量未变、谱面中第一个音符的时刻未变的情况。否则,您需要删除整个谱面后重新导入。 + \ No newline at end of file diff --git a/MaiChartManager/Locale.zh-hant.resx b/MaiChartManager/Locale.zh-hant.resx index 91b66db7..c1282c2b 100644 --- a/MaiChartManager/Locale.zh-hant.resx +++ b/MaiChartManager/Locale.zh-hant.resx @@ -285,4 +285,7 @@ 開機自動以伺服器模式運作並最小化到托盤 + + >請注意!本「替換譜面」功能僅限用於:譜面音符內容在原來的基礎上發生修改,且音訊內容未變、&first偏移量未變、譜面中第一個音符的時刻未變的情況。否則,您需要刪除整個譜面後重新匯入。 + \ No newline at end of file From 58d910f7294f69f7410070087e0f69ded7ff67d8 Mon Sep 17 00:00:00 2001 From: Starrah Date: Sat, 31 Jan 2026 12:30:25 +0800 Subject: [PATCH 05/11] =?UTF-8?q?refactor:=20=E6=8A=8A=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E8=B0=B1=E9=9D=A2=E6=97=B6=E2=80=9C=E6=98=BE=E7=A4=BA=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E8=AD=A6=E5=91=8A=E7=AD=89=E4=BF=A1=E6=81=AF=E2=80=9D?= =?UTF-8?q?=EF=BC=8C=E5=92=8C=E5=BB=B6=E8=BF=9F=E6=A8=A1=E5=BC=8F=E8=B0=83?= =?UTF-8?q?=E6=95=B4=EF=BC=8C=E7=9A=84UI=E6=8A=BD=E6=8F=90=E5=87=BA?= =?UTF-8?q?=E6=9D=A5=E4=BD=9C=E4=B8=BA=E5=8D=95=E7=8B=AC=E7=9A=84=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E6=96=B9=E4=BE=BF=E5=A4=8D=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ImportChartButton/ErrorDisplayIdInput.tsx | 79 +------------------ .../ImportChartButton/ImportAlert.tsx | 64 +++++++++++++++ .../ImportChartButton/ShiftModeSelector.tsx | 47 +++++++++++ 3 files changed, 115 insertions(+), 75 deletions(-) create mode 100644 MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ImportAlert.tsx create mode 100644 MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ShiftModeSelector.tsx diff --git a/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ErrorDisplayIdInput.tsx b/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ErrorDisplayIdInput.tsx index ab304095..c2b3c22e 100644 --- a/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ErrorDisplayIdInput.tsx +++ b/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ErrorDisplayIdInput.tsx @@ -9,6 +9,8 @@ import VersionInput from "@/components/VersionInput"; import { UTAGE_GENRE } from "@/consts"; import MusicIdConflictNotifier from "@/components/MusicIdConflictNotifier"; import { useI18n } from 'vue-i18n'; +import ImportAlert from "@/components/ImportCreateChartButton/ImportChartButton/ImportAlert"; +import ShiftModeSelector from "@/components/ImportCreateChartButton/ImportChartButton/ShiftModeSelector"; export default defineComponent({ props: { @@ -41,49 +43,7 @@ export default defineComponent({ v-model:show={show.value} >{{ default: () => - - - { - props.errors.map((error, i) => { - if ('first' in error) { - if (error.padding > 0 && props.tempOptions.shift === ShiftMethod.Legacy) { - return {t('chart.import.addPadding', {padding: error.padding.toFixed(3)})} - } - if (error.padding < 0 && props.tempOptions.shift === ShiftMethod.Legacy) { - return {t('chart.import.trimPadding', {padding: (-error.padding).toFixed(3)})} - } - if (error.first > 0 && props.tempOptions.shift === ShiftMethod.NoShift) { - return {t('chart.import.trimFirst', {first: error.first.toFixed(3)})} - } - if (error.first < 0 && props.tempOptions.shift === ShiftMethod.NoShift) { - return {t('chart.import.addFirst', {first: (-error.first).toFixed(3)})} - } - return <> - } - let type: "default" | "info" | "success" | "warning" | "error" = "default"; - switch (error.level) { - case MessageLevel.Info: - type = 'info'; - break; - case MessageLevel.Warning: - type = 'warning'; - break; - case MessageLevel.Fatal: - type = 'error'; - break; - } - return error.isPaid && (showNeedPurchaseDialog.value = true)} - > -
- {error.message} -
-
- }) - } -
-
+ {!!props.meta.length && <> {t('chart.import.assignId')} @@ -109,38 +69,7 @@ export default defineComponent({ - - - - - - {{ - trigger: () => , - default: () =>
- {t('chart.import.option.shiftByBarDesc')} -
- }} -
- - {{ - trigger: () => , - default: () =>
- {t('chart.import.option.shiftLegacyDesc')} -
- }} -
- - {{ - trigger: () => , - default: () =>
- {t('chart.import.option.shiftNoMoveDesc')} -
- }} -
-
-
-
-
+ {t('chart.import.option.noScale')} diff --git a/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ImportAlert.tsx b/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ImportAlert.tsx new file mode 100644 index 00000000..ab959e56 --- /dev/null +++ b/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ImportAlert.tsx @@ -0,0 +1,64 @@ +import { defineComponent, PropType } from "vue"; +import { NAlert, NFlex, NScrollbar } from "naive-ui"; +import { MessageLevel, ShiftMethod } from "@/client/apiGen"; +import { ImportChartMessageEx, TempOptions } from "./types"; +import { showNeedPurchaseDialog } from "@/store/refs"; +import { useI18n } from 'vue-i18n'; + +export default defineComponent({ + props: { + tempOptions: {type: Object as PropType, required: true}, + errors: {type: Array as PropType, required: true} + }, + setup(props, {emit}) { + const {t} = useI18n(); + + return () => + + { + props.errors.map((error, i) => { + if ('first' in error) { + if (error.padding > 0 && props.tempOptions.shift === ShiftMethod.Legacy) { + return {t('chart.import.addPadding', {padding: error.padding.toFixed(3)})} + } + if (error.padding < 0 && props.tempOptions.shift === ShiftMethod.Legacy) { + return {t('chart.import.trimPadding', {padding: (-error.padding).toFixed(3)})} + } + if (error.first > 0 && props.tempOptions.shift === ShiftMethod.NoShift) { + return {t('chart.import.trimFirst', {first: error.first.toFixed(3)})} + } + if (error.first < 0 && props.tempOptions.shift === ShiftMethod.NoShift) { + return {t('chart.import.addFirst', {first: (-error.first).toFixed(3)})} + } + return <> + } + let type: "default" | "info" | "success" | "warning" | "error" = "default"; + switch (error.level) { + case MessageLevel.Info: + type = 'info'; + break; + case MessageLevel.Warning: + type = 'warning'; + break; + case MessageLevel.Fatal: + type = 'error'; + break; + } + return error.isPaid && (showNeedPurchaseDialog.value = true)} + > +
+ {error.message} +
+
+ }) + } +
+
+ } +}) diff --git a/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ShiftModeSelector.tsx b/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ShiftModeSelector.tsx new file mode 100644 index 00000000..0b75d94d --- /dev/null +++ b/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ShiftModeSelector.tsx @@ -0,0 +1,47 @@ +import { defineComponent, PropType } from "vue"; +import { NFlex, NFormItem, NRadioGroup, NPopover, NRadio } from "naive-ui"; +import { ShiftMethod } from "@/client/apiGen"; +import { TempOptions } from "./types"; +import { useI18n } from 'vue-i18n'; + +export default defineComponent({ + props: { + tempOptions: {type: Object as PropType, required: true}, + }, + setup(props, {emit}) { + const {t} = useI18n(); + + return () => + + + + + {{ + trigger: () => , + default: () =>
+ {t('chart.import.option.shiftByBarDesc')} +
+ }} +
+ + {{ + trigger: () => , + default: () =>
+ {t('chart.import.option.shiftLegacyDesc')} +
+ }} +
+ + {{ + trigger: () => , + default: () =>
+ {t('chart.import.option.shiftNoMoveDesc')} +
+ }} +
+
+
+
+
+ } +}) From 38af84f7b6009a587202865647424e521f62a28e Mon Sep 17 00:00:00 2001 From: Starrah Date: Sat, 31 Jan 2026 22:23:22 +0800 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20ReplaceChartModal.tsx=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=AF=BC=E5=85=A5maidata=E6=89=80=E5=BF=85=E9=A1=BB?= =?UTF-8?q?=E7=9A=84=E6=98=BE=E7=A4=BA=EF=BC=8C=E5=8C=85=E6=8B=AC=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E8=B0=B1=E9=9D=A2=E6=A3=80=E6=9F=A5=E8=AD=A6=E5=91=8A?= =?UTF-8?q?=E5=92=8C=E9=80=89=E6=8B=A9=E5=BB=B6=E8=BF=9F=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DragDropDispatcher/ReplaceChartModal.tsx | 91 ++++++++++++------- .../ImportChartButton/ShiftModeSelector.tsx | 10 +- .../ImportChartButton/types.ts | 7 +- .../Front/src/components/LevelTagsDisplay.tsx | 21 +++++ .../src/components/MusicList/MusicEntry.tsx | 10 +- MaiChartManager/Front/src/locales/en.yaml | 2 + MaiChartManager/Front/src/locales/zh-TW.yaml | 2 + MaiChartManager/Front/src/locales/zh.yaml | 2 + 8 files changed, 100 insertions(+), 45 deletions(-) create mode 100644 MaiChartManager/Front/src/components/LevelTagsDisplay.tsx diff --git a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx index 06fb0ae0..af40140e 100644 --- a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx +++ b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx @@ -1,28 +1,40 @@ import { t } from '@/locales'; import { globalCapture, selectedADir, selectedLevel, selectedMusic, selectMusicId, updateMusicList } from '@/store/refs'; import { NButton, NFlex, NModal, useDialog, useMessage } from 'naive-ui'; -import { defineComponent, ref, shallowRef } from 'vue'; +import { computed, defineComponent, ref, shallowRef } from 'vue'; import JacketBox from '../JacketBox'; -import { DIFFICULTY, LEVEL_COLOR } from '@/consts'; +import { DIFFICULTY } from '@/consts'; import api from '@/client/api'; import CheckingModal from "@/components/ImportCreateChartButton/ImportChartButton/CheckingModal"; +import LevelTagsDisplay from "@/components/LevelTagsDisplay"; +import { Chart, ImportChartCheckResult, ShiftMethod } from "@/client/apiGen"; +import ImportAlert from "@/components/ImportCreateChartButton/ImportChartButton/ImportAlert"; +import { defaultTempOptions, ImportChartMessageEx, TempOptions } from "@/components/ImportCreateChartButton/ImportChartButton/types"; +import ShiftModeSelector from "@/components/ImportCreateChartButton/ImportChartButton/ShiftModeSelector"; +// noinspection JSUnusedLocalSymbols export let prepareReplaceChart = async (fileHandle?: FileSystemFileHandle) => { } export default defineComponent({ - // props: { - // }, - setup(props, { emit }) { + setup() { const message = useMessage(); const dialog = useDialog(); const checking = ref(false); - const ma2Handle = shallowRef(null); + const fileHandle = shallowRef(null); + const show = ref("") // 取值范围:"“(不显示),"ma2","maidata" - prepareReplaceChart = async (fileHandle?: FileSystemFileHandle) => { - if (!fileHandle) { - [fileHandle] = await window.showOpenFilePicker({ + const checkRet = ref(null) + const checkErrors = computed(()=>{ + return checkRet.value?.errors?.map(it=>({...it, name: checkRet.value?.title!})) ?? [] + }) + const tempOption = ref({...defaultTempOptions}) + + // 注:本功能的逻辑是,如果选择的是ma2文件,则只替换指定难度的谱面;如果选择的是maidata,则替换整首歌的所有难度。 + prepareReplaceChart = async (fHandle?: FileSystemFileHandle) => { + if (!fHandle) { + [fHandle] = await window.showOpenFilePicker({ id: 'chart', startIn: 'downloads', types: [ @@ -35,34 +47,39 @@ export default defineComponent({ ], }); } - if (!fileHandle) return; // 用户未选择文件 + if (!fHandle) return; // 用户未选择文件 + fileHandle.value = fHandle - const name = fileHandle.name; + const name = fHandle.name; // 对maidata.txt和ma2分类讨论,前者执行ImportCheck if (name == "maidata.txt") { try { checking.value = true; - const file = await fileHandle.getFile(); - const checkRet = (await api.ImportChartCheck({file, isReplacement: true})).data; + const file = await fHandle.getFile(); + const r = (await api.ImportChartCheck({file, isReplacement: true})).data; if (!checking.value) return; // 说明检查期间用户点击了关闭按钮、取消了操作。则不再执行后续流程。 - // TODO 显示导入界面(类似ErrorDisplayIdInput)、完成导入流程 - console.log(checkRet) - dialog.error({title: "NotImplemented"}) + + checkRet.value = r; + if (selectedMusic.value?.shiftMethod) { // 说明是新版导入的谱面、ShiftMethod已经被写入XML了。此时锁定ShiftMethod选项,不准用户自己选择 + tempOption.value = {shift: selectedMusic.value.shiftMethod as ShiftMethod, shiftLocked: true}; + } + show.value = "maidata"; } finally { checking.value = false; } } else if (name.endsWith(".ma2")) { - ma2Handle.value = fileHandle + show.value = "ma2" } else { dialog.error({title: t('error.unsupportedFileType'), content: t('music.edit.notValidChartFile')}) } } const replaceMa2 = async () => { - if (!ma2Handle.value) return; + if (!fileHandle.value) return; try { - const file = await ma2Handle.value.getFile(); - ma2Handle.value = null; + const file = await fileHandle.value.getFile(); + fileHandle.value = null; + show.value = ""; await api.ReplaceChart(selectMusicId.value, selectedLevel.value, selectedADir.value, { file }); message.success(t('music.edit.replaceChartSuccess')); await updateMusicList(); @@ -72,33 +89,45 @@ export default defineComponent({ } } + const chartsForDisplayLevel = computed(()=>{ + const result: (Chart | undefined)[] = [...selectedMusic.value!.charts!] + if (show.value == "ma2") { // 此时只显示所选择的难度,其他难度不要显示 + for (let i=0; i
ma2Handle.value = null} + show={!!show.value} >{{ default: () =>
- {t('music.edit.replaceChartConfirm', { level: DIFFICULTY[selectedLevel.value!] })} -
{ma2Handle.value?.name}
-
+ {show.value == "ma2" && <> + {t('music.edit.replaceChartConfirm', { level: DIFFICULTY[selectedLevel.value!] })} +
{fileHandle.value?.name}
+
+ } + {show.value == "maidata" && }
#{selectMusicId.value}
{selectedMusic.value!.name}
-
-
- {selectedMusic.value!.charts![selectedLevel.value!]?.level}.{selectedMusic.value!.charts![selectedLevel.value!]?.levelDecimal} -
-
+
+ {show.value == "maidata" &&
+ +
{t('music.edit.replaceChartShiftModeHint')}
+
}
, footer: () => - ma2Handle.value = null}>{t('common.cancel')} + show.value = ""}>{t('common.cancel')} {t('common.confirm')} }}
diff --git a/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ShiftModeSelector.tsx b/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ShiftModeSelector.tsx index 0b75d94d..f1600d61 100644 --- a/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ShiftModeSelector.tsx +++ b/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/ShiftModeSelector.tsx @@ -8,18 +8,18 @@ export default defineComponent({ props: { tempOptions: {type: Object as PropType, required: true}, }, - setup(props, {emit}) { + setup(props) { const {t} = useI18n(); return () => - + {{ trigger: () => , default: () =>
- {t('chart.import.option.shiftByBarDesc')} + {props.tempOptions.shiftLocked ? t('chart.import.option.shiftModeLocked') : t('chart.import.option.shiftByBarDesc')}
}}
@@ -27,7 +27,7 @@ export default defineComponent({ {{ trigger: () => , default: () =>
- {t('chart.import.option.shiftLegacyDesc')} + {props.tempOptions.shiftLocked ? t('chart.import.option.shiftModeLocked') : t('chart.import.option.shiftLegacyDesc')}
}} @@ -35,7 +35,7 @@ export default defineComponent({ {{ trigger: () => , default: () =>
- {t('chart.import.option.shiftNoMoveDesc')} + {props.tempOptions.shiftLocked ? t('chart.import.option.shiftModeLocked') : t('chart.import.option.shiftNoMoveDesc')}
}} diff --git a/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/types.ts b/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/types.ts index f6e749dc..79ef579d 100644 --- a/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/types.ts +++ b/MaiChartManager/Front/src/components/ImportCreateChartButton/ImportChartButton/types.ts @@ -38,7 +38,7 @@ export type ImportChartMessageEx = (ImportChartMessage | FirstPaddingMessage) & export const dummyMeta = {name: '', importStep: IMPORT_STEP.start} as ImportMeta -export const defaultTempOptions = { +export const defaultTempOptions: TempOptions = { shift: ShiftMethod.Bar, } @@ -60,5 +60,8 @@ export const defaultSavedOptions = { yuv420p: true, } -export type TempOptions = typeof defaultTempOptions; +export type TempOptions = { + shift: ShiftMethod, + shiftLocked?: boolean +}; export type SavedOptions = typeof defaultSavedOptions; diff --git a/MaiChartManager/Front/src/components/LevelTagsDisplay.tsx b/MaiChartManager/Front/src/components/LevelTagsDisplay.tsx new file mode 100644 index 00000000..e803b809 --- /dev/null +++ b/MaiChartManager/Front/src/components/LevelTagsDisplay.tsx @@ -0,0 +1,21 @@ +import { defineComponent, PropType } from "vue"; +import { NFlex } from "naive-ui"; +import { Chart } from "@/client/apiGen"; +import { LEVEL_COLOR, LEVELS } from "@/consts"; + +export default defineComponent({ + props: { + charts: {type: Array as PropType<(Chart | undefined)[]>, required: true}, + showLevelDecimal: {type: Boolean, default: false} // 如果为true,显示的是定数12.8,否则是12+。 + }, + setup(props) { + return () => + { + (props.charts || []).map((chart, index) => + chart && chart.enable &&
+ {props.showLevelDecimal ? chart.level + "." + chart.levelDecimal : LEVELS[chart.levelId!]} +
) + } +
+ } +}) diff --git a/MaiChartManager/Front/src/components/MusicList/MusicEntry.tsx b/MaiChartManager/Front/src/components/MusicList/MusicEntry.tsx index 2b900362..31b2734e 100644 --- a/MaiChartManager/Front/src/components/MusicList/MusicEntry.tsx +++ b/MaiChartManager/Front/src/components/MusicList/MusicEntry.tsx @@ -2,12 +2,13 @@ import { computed, defineComponent, PropType } from "vue"; import { MusicXmlWithABJacket } from "@/client/apiGen"; import noJacket from '@/assets/noJacket.webp'; import { NBadge, NFlex } from "naive-ui"; -import { DIFFICULTY, LEVEL_COLOR, LEVELS } from "@/consts"; +import { DIFFICULTY } from "@/consts"; import ProblemsDisplay from "@/components/ProblemsDisplay"; import { musicListAll, selectedADir } from "@/store/refs"; import ConflictDisplay from "@/components/MusicList/ConflictDisplay"; import { getUrl } from "@/client/api"; import LongMusicIcon from "./LongMusicIcon"; +import LevelTagsDisplay from "@/components/LevelTagsDisplay"; export default defineComponent({ props: { @@ -42,12 +43,7 @@ export default defineComponent({ {props.music.id?.toString().padStart(6, '0')}
{props.music.name}
- - { - (props.music.charts || []).map((chart, index) => - chart.enable &&
{LEVELS[chart.levelId!]}
) - } -
+
{props.music.longMusic && } diff --git a/MaiChartManager/Front/src/locales/en.yaml b/MaiChartManager/Front/src/locales/en.yaml index 75f5943e..f6983885 100644 --- a/MaiChartManager/Front/src/locales/en.yaml +++ b/MaiChartManager/Front/src/locales/en.yaml @@ -134,6 +134,7 @@ music: replaceChartFailed: Failed to replace chart replaceChartSuccess: Chart replaced successfully notValidChartFile: Chart file must be .ma2 or maidata.txt. + replaceChartShiftModeHint: Make sure the delay adjustment mode selected here matches the one used when importing the chart, otherwise notes may be positioned incorrectly! batch: title: Batch Actions batchAndSearch: Batch Actions & Search @@ -219,6 +220,7 @@ chart: option: advancedOptions: Advanced Options shiftMode: Delay Adjustment Mode + shiftModeLocked: The Delay Adjustment Mode is locked to the mode used when the chart was originally imported and cannot be changed. shiftByBar: By Bar shiftByBarDesc: >- If the rest at the beginning of the chart is less than one bar, add one diff --git a/MaiChartManager/Front/src/locales/zh-TW.yaml b/MaiChartManager/Front/src/locales/zh-TW.yaml index 49fdaf71..3c07380e 100644 --- a/MaiChartManager/Front/src/locales/zh-TW.yaml +++ b/MaiChartManager/Front/src/locales/zh-TW.yaml @@ -127,6 +127,7 @@ music: replaceChartFailed: 替換譜面失敗 replaceChartSuccess: 替換譜面成功 notValidChartFile: 譜面檔案必須是 .ma2 或 maidata.txt。 + replaceChartShiftModeHint: 請確保這裡選擇的延遲調整模式與匯入譜面時使用的一致,否則會導致音符位置不正確! batch: title: 批次操作 batchAndSearch: 批次操作與搜尋 @@ -210,6 +211,7 @@ chart: option: advancedOptions: 進階選項 shiftMode: 延遲調整模式 + shiftModeLocked: 延遲調整模式已被鎖定為最初匯入譜面時所使用的模式,無法變更 shiftByBar: 按小節 shiftByBarDesc: |- 如果譜面前面的休止符長度小於一小節,那就在前面加上一小節的空白 diff --git a/MaiChartManager/Front/src/locales/zh.yaml b/MaiChartManager/Front/src/locales/zh.yaml index 05b10a85..e22479d2 100644 --- a/MaiChartManager/Front/src/locales/zh.yaml +++ b/MaiChartManager/Front/src/locales/zh.yaml @@ -127,6 +127,7 @@ music: replaceChartFailed: 替换谱面失败 replaceChartSuccess: 替换谱面成功 notValidChartFile: 谱面文件必须是.ma2或maidata.txt + replaceChartShiftModeHint: 请确保这里选择的延迟调整模式和导入谱面时使用的一致,否则会导致音符位置不正确! batch: title: 批量操作 batchAndSearch: 批量操作与搜索 @@ -210,6 +211,7 @@ chart: option: advancedOptions: 高级选项 shiftMode: 延迟调整模式 + shiftModeLocked: 延迟调整模式已被锁定为最初导入谱面时所使用的模式,无法修改 shiftByBar: 按小节 shiftByBarDesc: |- 如果谱面前面的休止符长度小于一小节,那就在前面加上一小节的空白 From ef7b54cb10def97d8af0aef0c117f1117b99dba1 Mon Sep 17 00:00:00 2001 From: Starrah Date: Sun, 1 Feb 2026 00:02:21 +0800 Subject: [PATCH 07/11] =?UTF-8?q?refactor[1/2]:=20=E5=B0=86=E8=A7=A3?= =?UTF-8?q?=E6=9E=90Maidata=E3=80=81=E8=BD=AC=E8=B0=B1=E7=9A=84=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E9=80=BB=E8=BE=91=E6=8A=BD=E6=8F=90=E5=88=B0=E5=8D=95?= =?UTF-8?q?=E7=8B=AC=E7=9A=84ImportMaidata=E5=87=BD=E6=95=B0=E4=B8=AD?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 方便之后可以直接复用ImportMaidata函数实现替换。 但是这里的问题是ImportMaidata无法是静态的(因为里面有大量调用到的函数不是静态的,甚至还有一个logger是WebServer框架提供的)。因此后续还需要二阶段重构。 --- .../Charts/ImportChartController.cs | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/MaiChartManager/Controllers/Charts/ImportChartController.cs b/MaiChartManager/Controllers/Charts/ImportChartController.cs index 5776969f..99f1634e 100644 --- a/MaiChartManager/Controllers/Charts/ImportChartController.cs +++ b/MaiChartManager/Controllers/Charts/ImportChartController.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using MaiChartManager.Models; using MaiChartManager.Utils; using MaiLib; using Microsoft.AspNetCore.Mvc; @@ -362,22 +363,12 @@ private record AllChartsEntry(string chartText, MaiChart simaiSharpChart); [GeneratedRegex(@"Original Stack.*", RegexOptions.Singleline)] private static partial Regex MaiLibErrMsgRegex(); - [HttpPost] - // 创建完 Music 后调用 - public ImportChartResult ImportChart( - [FromForm] int id, - IFormFile file, - [FromForm] bool ignoreLevelNum, - [FromForm] int addVersionId, - [FromForm] int genreId, - [FromForm] int version, - [FromForm] string assetDir, - [FromForm] ShiftMethod shift, - [FromForm] bool debug = false) + public ImportChartResult ImportMaidata(MusicXml music, IFormFile file, ShiftMethod shift, + bool ignoreLevelNum, bool debug) { + var id = music.Id; var isUtage = id > 100000; var errors = new List(); - var music = settings.GetMusic(id, assetDir); var kvps = new SimaiFile(file.OpenReadStream()).ToKeyValuePairs(); var maiData = new Dictionary(); foreach (var (key, value) in kvps) @@ -563,17 +554,40 @@ public ImportChartResult ImportChart( music.Name = maiData["title"]; music.Artist = maiData.GetValueOrDefault("artist") ?? ""; - music.AddVersionId = addVersionId; - music.GenreId = genreId; - music.Version = version; music.ShiftMethod = shift.ToString(); float wholebpm; if (float.TryParse(maiData.GetValueOrDefault("wholebpm"), out wholebpm)) music.Bpm = wholebpm; - music.Save(); - music.Refresh(); + return new ImportChartResult(errors, false); } + + [HttpPost] + // 创建完 Music 后调用 + public ImportChartResult ImportChart( + [FromForm] int id, + IFormFile file, + [FromForm] bool ignoreLevelNum, + [FromForm] int addVersionId, + [FromForm] int genreId, + [FromForm] int version, + [FromForm] string assetDir, + [FromForm] ShiftMethod shift, + [FromForm] bool debug = false) + { + var music = settings.GetMusic(id, assetDir); + var importMaidataResult = ImportMaidata(music, file, shift, ignoreLevelNum, debug); + if (!importMaidataResult.Fatal) + { + music.AddVersionId = addVersionId; + music.GenreId = genreId; + music.Version = version; + music.Save(); + music.Refresh(); + } + + return importMaidataResult; + } public static int ParseTNumAllFromMa2(string ma2Content) From 6bdf568c7b819ea7688c7a62f5b201d177dcdbfb Mon Sep 17 00:00:00 2001 From: Starrah Date: Sun, 1 Feb 2026 01:42:37 +0800 Subject: [PATCH 08/11] =?UTF-8?q?refactor[2/2]:=20=E5=B0=86Maidata?= =?UTF-8?q?=E8=BD=AC=E8=B0=B1=E7=9A=84=E5=90=84=E7=A7=8D=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=92=8C=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0=EF=BC=8C=E6=8F=90?= =?UTF-8?q?=E5=8F=96=E5=88=B0=E4=B8=80=E4=B8=AAMaidataImportService?= =?UTF-8?q?=E4=B8=AD=E3=80=82=E7=84=B6=E5=90=8E=E8=AE=A9ImportChartControl?= =?UTF-8?q?ler=E8=B0=83=E7=94=A8=E8=BF=99=E4=B8=AAService=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这样,MaidataImportService便可以被其他地方如ReplaceChart复用。 --- .../Controllers/Charts/ChartController.cs | 7 +- .../Charts/ImportChartController.cs | 434 +----------------- MaiChartManager/ServerManager.cs | 2 + .../Services/MaidataImportService.cs | 431 +++++++++++++++++ 4 files changed, 446 insertions(+), 428 deletions(-) create mode 100644 MaiChartManager/Services/MaidataImportService.cs diff --git a/MaiChartManager/Controllers/Charts/ChartController.cs b/MaiChartManager/Controllers/Charts/ChartController.cs index a802a630..ed07830e 100644 --- a/MaiChartManager/Controllers/Charts/ChartController.cs +++ b/MaiChartManager/Controllers/Charts/ChartController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using MaiChartManager.Services; +using Microsoft.AspNetCore.Mvc; namespace MaiChartManager.Controllers.Charts; @@ -96,7 +97,7 @@ public void EditChartEnable(int id, int level, [FromBody] bool value, string ass [HttpPost] public void ReplaceChart(int id, int level, IFormFile file, string assetDir, - [FromForm] ImportChartController.ShiftMethod? shift) + [FromForm] ShiftMethod? shift) { var music = settings.GetMusic(id, assetDir); if (music == null || file == null) return; @@ -114,7 +115,7 @@ public void ReplaceChart(int id, int level, IFormFile file, string assetDir, { fileContent = reader.ReadToEnd(); } - var newMaxNotes = ImportChartController.ParseTNumAllFromMa2(fileContent); + var newMaxNotes = MaidataImportService.ParseTNumAllFromMa2(fileContent); if (newMaxNotes != 0 && targetChart.MaxNotes != newMaxNotes) { targetChart.MaxNotes = newMaxNotes; diff --git a/MaiChartManager/Controllers/Charts/ImportChartController.cs b/MaiChartManager/Controllers/Charts/ImportChartController.cs index 99f1634e..b3221004 100644 --- a/MaiChartManager/Controllers/Charts/ImportChartController.cs +++ b/MaiChartManager/Controllers/Charts/ImportChartController.cs @@ -1,7 +1,5 @@ using System.Text.RegularExpressions; -using MaiChartManager.Models; -using MaiChartManager.Utils; -using MaiLib; +using MaiChartManager.Services; using Microsoft.AspNetCore.Mvc; using SimaiSharp; using SimaiSharp.Structures; @@ -10,152 +8,9 @@ namespace MaiChartManager.Controllers.Charts; [ApiController] [Route("MaiChartManagerServlet/[action]Api")] -public partial class ImportChartController(StaticSettings settings, ILogger logger) : ControllerBase +public class ImportChartController(StaticSettings settings, ILogger logger, + MaidataImportService importService) : ControllerBase { - public enum MessageLevel - { - Info, - Warning, - Fatal - } - - [GeneratedRegex(@"^\([\d\.]+\)")] - private static partial Regex BpmTagRegex(); - - [GeneratedRegex(@"\{([\d\.]+)\}")] - private static partial Regex MeasureTagRegex(); - - private static string Add1Bar(string maidata) - { - var regex = BpmTagRegex(); - var bpm = regex.Match(maidata).Value; - // 这里使用 {4},,,, 而不是 {1}, 因为要是谱面一开始根本没有写 {x} 的话,默认是 {4}。要是用了 {1}, 会覆盖默认的 {4} - return string.Concat(bpm, "{4},,,,", maidata.AsSpan(bpm.Length)); - } - - [GeneratedRegex(@"(\d){")] - private static partial Regex SimaiError1(); - - [GeneratedRegex(@"\[(\d+)-(\d+)]")] - private static partial Regex SimaiError2(); - - [GeneratedRegex(@"(\d)\(")] - private static partial Regex SimaiError3(); - - [GeneratedRegex(@",[csbx\.\{\}],")] - private static partial Regex SimaiError4(); - - [GeneratedRegex(@"(\d)qx(\d)")] - private static partial Regex SimaiError5(); - - private static string FixChartSimaiSharp(string chart) - { - chart = chart.Replace("\n", "").Replace("\r", "").Replace("{{", "{").Replace("}}", "}"); - chart = SimaiError1().Replace(chart, "$1,{"); - chart = SimaiError3().Replace(chart, "$1,("); - chart = SimaiError2().Replace(chart, "[$1:$2]"); - chart = SimaiError4().Replace(chart, ",,"); - chart = SimaiError5().Replace(chart, "$1xq$2"); - return chart; - } - - private MaiChart TryParseChartSimaiSharp(string chartText, int level, List errors) - { - chartText = chartText.ReplaceLineEndings(); - try - { - return SimaiConvert.Deserialize(chartText); - } - catch (Exception e) - { - logger.LogWarning(e, "SimaiSharp 无法直接解析谱面"); - } - - try - { - var chart = SimaiConvert.Deserialize(FixChartSimaiSharp(chartText)); - errors.Add(new ImportChartMessage(string.Format(Locale.ChartFixedMinorErrors, level), MessageLevel.Info)); - return chart; - } - catch (Exception e) - { - logger.LogWarning(e, "SimaiSharp 无法解析修复后谱面"); - throw; - } - } - - [NonAction] - private Chart? TryParseChart(string chartText, MaiChart? simaiSharpChart, int level, List errors) - { - chartText = chartText.ReplaceLineEndings(); - try - { - return new SimaiParser().ChartOfToken(new SimaiTokenizer().TokensFromText(chartText)); - } - catch (Exception e) - { - logger.LogWarning(e, "无法直接解析谱面"); - } - - try - { - var normalizedText = SimaiCommentRegex().Replace(chartText, ""); - normalizedText = SimaiCommentRegex2().Replace(normalizedText, ""); - return new SimaiParser().ChartOfToken(new SimaiTokenizer().TokensFromText(normalizedText)); - } - catch (Exception) - { - // ignored - } - - try - { - var normalizedText = FixChartSimaiSharp(chartText) - // 不飞的星星 - .Replace("-?", "?-"); - // 移除注释 - normalizedText = SimaiCommentRegex().Replace(normalizedText, ""); - normalizedText = SimaiCommentRegex2().Replace(normalizedText, ""); - var tokens = new SimaiTokenizer().TokensFromText(normalizedText); - for (var i = 0; i < tokens.Length; i++) - { - if (tokens[i].Contains("]b")) - { - tokens[i] = tokens[i].Replace("]b", "]").Replace("[", "b["); - } - } - - var maiLibChart = new SimaiParser().ChartOfToken(tokens); - errors.Add(new ImportChartMessage(string.Format(Locale.ChartFixedMinorErrors, level), MessageLevel.Info)); - return maiLibChart; - } - catch (Exception e) - { - errors.Add(new ImportChartMessage(string.Format(Locale.ChartMaiLibParseError, level, MaiLibErrMsgRegex().Replace(e.Message, "")), MessageLevel.Warning)); - logger.LogWarning(e, "无法在手动修正错误后解析谱面"); - } - - if (simaiSharpChart is null) - { - return null; - } - - try - { - var reSerialized = SimaiConvert.Serialize(simaiSharpChart); - reSerialized = reSerialized.Replace("{0}", "{4}"); - var maiLibChart = new SimaiParser().ChartOfToken(new SimaiTokenizer().TokensFromText(reSerialized)); - errors.Add(new ImportChartMessage(string.Format(Locale.ChartSimaiSharpFallback, level), MessageLevel.Warning)); - return maiLibChart; - } - catch (Exception e) - { - SentrySdk.CaptureException(e); - errors.Add(new ImportChartMessage(string.Format(Locale.ChartParseFailed, level), MessageLevel.Fatal)); - return null; - } - } - private static float getFirstBarFromChart(MaiChart chart) { var bpm = chart.TimingChanges[0].tempo; @@ -167,28 +22,6 @@ private static float getFirstBarFromChart(MaiChart chart) return 60 / bpm * 4; } -// v1.1.2 新增 - public enum ShiftMethod - { - // 之前的办法,把第一押准确的对在第二小节的开头 - // noShiftChart = false, padding = MusicPadding - Legacy, - - // 简单粗暴的办法,不需要让库来平移谱面,解决各种平移不兼容问题 - // 之前修库都白修了其实 - // bar - 休止符的长度 如果是正数,那就直接在前面加一个小节的空白 - // 判断一下 > 0.1 好了,因为 < 0.1 秒可以忽略不计 - // noShiftChart = true, padding = (bar - 休止符的长度 > 0.1 ? bar - first : 0) - // bar - 休止符的长度 = MusicPadding + first - Bar, - - // 把音频裁掉 &first 秒,完全不用动谱面 - // noShiftChart = true, padding = -first - NoShift - } - - public record ImportChartMessage(string Message, MessageLevel Level); - public record ImportChartCheckResult(bool Accept, IEnumerable Errors, float MusicPadding, bool IsDx, string? Title, float first, float bar); [HttpPost] @@ -287,7 +120,7 @@ public ImportChartCheckResult ImportChartCheck(IFormFile file, [FromForm] bool i foreach (var kvp in allChartText) { var chartText = kvp.Value; - var measures = MeasureTagRegex().Matches(chartText); + var measures = MaidataImportService.MeasureTagRegex().Matches(chartText); foreach (Match measure in measures) { if (!float.TryParse(measure.Groups[1].Value, out var measureValue)) continue; @@ -301,10 +134,10 @@ public ImportChartCheckResult ImportChartCheck(IFormFile file, [FromForm] bool i try { - var chart = TryParseChartSimaiSharp(chartText, kvp.Key, errors); - paddings.Add(CalcMusicPadding(chart, first)); + var chart = importService.TryParseChartSimaiSharp(chartText, kvp.Key, errors); + paddings.Add(MaidataImportService.CalcMusicPadding(chart, first)); - var candidate = TryParseChart(chartText, chart, kvp.Key, errors); + var candidate = importService.TryParseChart(chartText, chart, kvp.Key, errors); if (candidate is null) throw new Exception(Locale.ChartParseGenericError); isDx = isDx || candidate.IsDxChart; } @@ -321,7 +154,7 @@ public ImportChartCheckResult ImportChartCheck(IFormFile file, [FromForm] bool i var padding = paddings.Max(); // 计算 bar - var bar = getFirstBarFromChart(TryParseChartSimaiSharp(allChartText.First().Value, allChartText.First().Key, errors)); + var bar = getFirstBarFromChart(importService.TryParseChartSimaiSharp(allChartText.First().Value, allChartText.First().Key, errors)); return new ImportChartCheckResult(!fatal, errors, padding, isDx, title, first, bar); } @@ -333,234 +166,6 @@ public ImportChartCheckResult ImportChartCheck(IFormFile file, [FromForm] bool i return new ImportChartCheckResult(!fatal, errors, 0, false, "", 0, 0); } } - - public static float CalcMusicPadding(MaiChart chart, float first) - { - // TimingChanges 对应的是所有的 {int} - var bpm = chart.TimingChanges[0].tempo; - // 一小节多长 - var bar = 60 / bpm * 4; - - // 第一押什么时候出来 - var firstTiming = chart.NoteCollections[0].time + first; - return bar - firstTiming; - } - - public record ImportChartResult(IEnumerable Errors, bool Fatal); - - private record AllChartsEntry(string chartText, MaiChart simaiSharpChart); - - [GeneratedRegex(@"\|\|.*$", RegexOptions.Multiline)] - private static partial Regex SimaiCommentRegex(); - - /* - * 根据[simai文档](https://w.atwiki.jp/simai/pages/1002.html),井号如果出现在[]或{}内是合法语法,并非注释。 - * 因此在尝试匹配注释时,应该排除掉#前面有未闭合的[或{的情况。 - */ - [GeneratedRegex(@"(? 100000; - var errors = new List(); - var kvps = new SimaiFile(file.OpenReadStream()).ToKeyValuePairs(); - var maiData = new Dictionary(); - foreach (var (key, value) in kvps) - { - maiData[key] = value; - } - - var allCharts = new Dictionary(); - for (var i = 2; i < 9; i++) - { - if (!string.IsNullOrWhiteSpace(maiData.GetValueOrDefault($"inote_{i}"))) - { - allCharts.Add(i, new AllChartsEntry(maiData[$"inote_{i}"], TryParseChartSimaiSharp(maiData[$"inote_{i}"], i, errors))); - } - } - - if (!string.IsNullOrWhiteSpace(maiData.GetValueOrDefault("inote_0"))) - { - allCharts.Add(0, new AllChartsEntry(maiData["inote_0"], TryParseChartSimaiSharp(maiData["inote_0"], 0, errors))); - } - - float.TryParse(maiData.GetValueOrDefault("first"), out var first); - // Mai 的歌曲是从两帧后开始播放的 - first -= 1 / 30f; - - var paddings = allCharts.Values.Select(chart => CalcMusicPadding(chart.simaiSharpChart, first)).ToList(); - // 音频前面被增加了多少 - var audioPadding = paddings.Max(); // bar - firstTiming = bar - 谱面前面休止符的时间 - &first - var shouldAddBar = false; - float chartPadding; - switch (shift) - { - case ShiftMethod.Legacy: - chartPadding = audioPadding + first; - break; - case ShiftMethod.Bar when audioPadding + first > 0.1: - shouldAddBar = true; - chartPadding = 0f; - break; - default: - chartPadding = 0f; - break; - } - - if (shouldAddBar) - { - foreach (var (level, chart) in allCharts) - { - var newText = Add1Bar(chart.chartText); - allCharts[level] = new AllChartsEntry(newText, TryParseChartSimaiSharp(newText, level, errors)); - } - } - - foreach (var (level, chart) in allCharts) - { - // 宴会场只导入第一个谱面 - if (isUtage && music.Charts[0].Enable) break; - - // var levelPadding = CalcMusicPadding(chart, first); - var bpm = chart.simaiSharpChart.TimingChanges[0].tempo; - music.Bpm = bpm; - // 一个小节多少秒 - var bar = 60 / bpm * 4; - - // 我们要让这个谱面真正的内容(忽略 first)延后多少 - // levelPadding 似乎不需要算,因为每个谱面真正的内容都是从同一个地方开始 - // 所以只要在前面加上 audioPadding + first 时间的休止符 - // 最早出音符的那个谱面的第一押之前一定是 1bar(小节)的休止符 - // |_| levelPadding - // |_______| audioPadding - // |________________| bar - // |________________| bar - // |-------|---|----|-----|----- - // | | | | | 这个谱面的第一押 - // | | | | 可能是另一个谱面难度的第一押,firstTiming,它可能导致 audioPadding > levelPadding - // | | |____| 这一段是休止符 - // | | | 每个谱面真正的内容都是从这里开始 - // | |___| first skip 掉的部分 - // | | 原先音频的开头 - // | 加了 padding 的音频开头 - - # region 设定 targetLevel - - var targetLevel = level - 2; - - // 处理非标准难度 - if (level is > 6 or < 1) - { - // 分给 3 4 0 - if (!music.Charts[3].Enable) - { - targetLevel = 3; - } - else if (!music.Charts[4].Enable) - { - targetLevel = 4; - } - else if (!music.Charts[0].Enable) - { - targetLevel = 0; - } - else - { - continue; - } - } - - if (isUtage) targetLevel = 0; - - # endregion - - var targetChart = music.Charts[targetLevel]; - targetChart.Path = $"{id:000000}_0{targetLevel}.ma2"; - var levelNumStr = maiData.GetValueOrDefault($"lv_{level}"); - if (!string.IsNullOrWhiteSpace(levelNumStr)) - { - levelNumStr = levelNumStr.Replace("+", ".7"); - } - - float.TryParse(levelNumStr, out var levelNum); - targetChart.LevelId = MaiUtils.GetLevelId((int)(levelNum * 10)); - // 忽略定数 - if (!ignoreLevelNum) - { - targetChart.Level = (int)Math.Floor(levelNum); - targetChart.LevelDecimal = (int)Math.Floor(levelNum * 10 % 10); - } - - targetChart.Designer = maiData.GetValueOrDefault($"des_{level}") ?? maiData.GetValueOrDefault("des") ?? ""; - var maiLibChart = TryParseChart(chart.chartText, chart.simaiSharpChart, level, errors); - if (maiLibChart is null) - { - return new ImportChartResult(errors, true); - } - - var originalConverted = maiLibChart.Compose(ChartEnum.ChartVersion.Ma2_104); - - if (debug) - { - System.IO.File.WriteAllText(Path.Combine(Path.GetDirectoryName(music.FilePath), targetChart.Path + ".afterSimaiSharp.txt"), SimaiConvert.Serialize(chart.simaiSharpChart)); - System.IO.File.WriteAllText(Path.Combine(Path.GetDirectoryName(music.FilePath), targetChart.Path + ".preShift.ma2"), originalConverted); - System.IO.File.WriteAllText(Path.Combine(Path.GetDirectoryName(music.FilePath), targetChart.Path + ".preShift.txt"), maiLibChart.Compose(ChartEnum.ChartVersion.SimaiFes)); - } - - if (chartPadding != 0) - { - try - { - maiLibChart.ShiftByOffset((int)Math.Round(chartPadding / bar * maiLibChart.Definition)); - } - catch (Exception e) - { - SentrySdk.CaptureEvent(new SentryEvent(e) - { - Message = Locale.ChartShiftByOffsetError - }); - errors.Add(new ImportChartMessage(Locale.ChartShiftError, MessageLevel.Fatal)); - return new ImportChartResult(errors, true); - } - } - - var shiftedConverted = maiLibChart.Compose(ChartEnum.ChartVersion.Ma2_104); - - if (shiftedConverted.Split('\n').Length != originalConverted.Split('\n').Length) - { - errors.Add(new ImportChartMessage(Locale.ChartNotesMissing, MessageLevel.Warning)); - logger.LogWarning("BUG! shiftedConverted: {shiftedLen}, originalConverted: {originalLen}", shiftedConverted.Split('\n').Length, originalConverted.Split('\n').Length); - } - - // Just use T_NUM_ALL value in ma2 file - targetChart.MaxNotes = ParseTNumAllFromMa2(shiftedConverted); - // Fallback to maiLibChart if T_NUM_ALL not found - if (targetChart.MaxNotes == 0) targetChart.MaxNotes = maiLibChart.AllNoteNum; - - System.IO.File.WriteAllText(Path.Combine(Path.GetDirectoryName(music.FilePath), targetChart.Path), shiftedConverted); - if (debug) - { - System.IO.File.WriteAllText(Path.Combine(Path.GetDirectoryName(music.FilePath), targetChart.Path + ".afterShift.txt"), maiLibChart.Compose(ChartEnum.ChartVersion.SimaiFes)); - } - - targetChart.Enable = true; - } - - music.Name = maiData["title"]; - music.Artist = maiData.GetValueOrDefault("artist") ?? ""; - music.ShiftMethod = shift.ToString(); - float wholebpm; - if (float.TryParse(maiData.GetValueOrDefault("wholebpm"), out wholebpm)) - music.Bpm = wholebpm; - - return new ImportChartResult(errors, false); - } [HttpPost] // 创建完 Music 后调用 @@ -576,7 +181,7 @@ public ImportChartResult ImportChart( [FromForm] bool debug = false) { var music = settings.GetMusic(id, assetDir); - var importMaidataResult = ImportMaidata(music, file, shift, ignoreLevelNum, debug); + var importMaidataResult = importService.ImportMaidata(music, file, shift, ignoreLevelNum, debug); if (!importMaidataResult.Fatal) { music.AddVersionId = addVersionId; @@ -588,25 +193,4 @@ public ImportChartResult ImportChart( return importMaidataResult; } - - - public static int ParseTNumAllFromMa2(string ma2Content) - { - var lines = ma2Content.Split('\n'); - // 从后往前读取,因为 T_NUM_ALL 在文件最后 - for (int i = lines.Length - 1; i >= 0; i--) - { - var trimmedLine = lines[i].Trim(); - if (trimmedLine.StartsWith("T_NUM_ALL", StringComparison.OrdinalIgnoreCase)) - { - var parts = trimmedLine.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length >= 2 && int.TryParse(parts[1], out int tNumAll)) - { - return tNumAll; - } - } - } - // Fallback to 0 in case - return 0; - } } \ No newline at end of file diff --git a/MaiChartManager/ServerManager.cs b/MaiChartManager/ServerManager.cs index 22d98a70..2169de1c 100644 --- a/MaiChartManager/ServerManager.cs +++ b/MaiChartManager/ServerManager.cs @@ -6,6 +6,7 @@ using System.Text.Json.Serialization; using System.Windows.Forms; using idunno.Authentication.Basic; +using MaiChartManager.Services; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.Extensions.FileProviders; @@ -111,6 +112,7 @@ public static void StartApp(bool export, Action? onStart = null) builder.Services .AddSingleton() + .AddSingleton() .AddEndpointsApiExplorer() .AddSwaggerGen(options => { options.CustomSchemaIds(type => type.Name == "Config" ? type.FullName : type.Name); }) .Configure(x => diff --git a/MaiChartManager/Services/MaidataImportService.cs b/MaiChartManager/Services/MaidataImportService.cs new file mode 100644 index 00000000..08d2b8c4 --- /dev/null +++ b/MaiChartManager/Services/MaidataImportService.cs @@ -0,0 +1,431 @@ +using System.Text.RegularExpressions; +using MaiChartManager.Models; +using MaiChartManager.Utils; +using MaiLib; +using SimaiSharp; +using SimaiSharp.Structures; + +namespace MaiChartManager.Services; + +public enum MessageLevel +{ + Info, + Warning, + Fatal +} + +public record ImportChartMessage(string Message, MessageLevel Level); + +public record ImportChartResult(IEnumerable Errors, bool Fatal); + +// v1.1.2 新增 +public enum ShiftMethod +{ + // 之前的办法,把第一押准确的对在第二小节的开头 + // noShiftChart = false, padding = MusicPadding + Legacy, + + // 简单粗暴的办法,不需要让库来平移谱面,解决各种平移不兼容问题 + // 之前修库都白修了其实 + // bar - 休止符的长度 如果是正数,那就直接在前面加一个小节的空白 + // 判断一下 > 0.1 好了,因为 < 0.1 秒可以忽略不计 + // noShiftChart = true, padding = (bar - 休止符的长度 > 0.1 ? bar - first : 0) + // bar - 休止符的长度 = MusicPadding + first + Bar, + + // 把音频裁掉 &first 秒,完全不用动谱面 + // noShiftChart = true, padding = -first + NoShift +} + +public partial class MaidataImportService +{ + private readonly ILogger logger; + + public MaidataImportService(ILogger logger) + { + this.logger = logger; + } + + [GeneratedRegex(@"^\([\d\.]+\)")] + private static partial Regex BpmTagRegex(); + + [GeneratedRegex(@"\{([\d\.]+)\}")] + public static partial Regex MeasureTagRegex(); + + private static string Add1Bar(string maidata) + { + var regex = BpmTagRegex(); + var bpm = regex.Match(maidata).Value; + // 这里使用 {4},,,, 而不是 {1}, 因为要是谱面一开始根本没有写 {x} 的话,默认是 {4}。要是用了 {1}, 会覆盖默认的 {4} + return string.Concat(bpm, "{4},,,,", maidata.AsSpan(bpm.Length)); + } + + [GeneratedRegex(@"(\d){")] + private static partial Regex SimaiError1(); + + [GeneratedRegex(@"\[(\d+)-(\d+)]")] + private static partial Regex SimaiError2(); + + [GeneratedRegex(@"(\d)\(")] + private static partial Regex SimaiError3(); + + [GeneratedRegex(@",[csbx\.\{\}],")] + private static partial Regex SimaiError4(); + + [GeneratedRegex(@"(\d)qx(\d)")] + private static partial Regex SimaiError5(); + + private static string FixChartSimaiSharp(string chart) + { + chart = chart.Replace("\n", "").Replace("\r", "").Replace("{{", "{").Replace("}}", "}"); + chart = SimaiError1().Replace(chart, "$1,{"); + chart = SimaiError3().Replace(chart, "$1,("); + chart = SimaiError2().Replace(chart, "[$1:$2]"); + chart = SimaiError4().Replace(chart, ",,"); + chart = SimaiError5().Replace(chart, "$1xq$2"); + return chart; + } + + public MaiChart TryParseChartSimaiSharp(string chartText, int level, List errors) + { + chartText = chartText.ReplaceLineEndings(); + try + { + return SimaiConvert.Deserialize(chartText); + } + catch (Exception e) + { + logger.LogWarning(e, "SimaiSharp 无法直接解析谱面"); + } + + try + { + var chart = SimaiConvert.Deserialize(FixChartSimaiSharp(chartText)); + errors.Add(new ImportChartMessage(string.Format(Locale.ChartFixedMinorErrors, level), MessageLevel.Info)); + return chart; + } + catch (Exception e) + { + logger.LogWarning(e, "SimaiSharp 无法解析修复后谱面"); + throw; + } + } + + [GeneratedRegex(@"\|\|.*$", RegexOptions.Multiline)] + private static partial Regex SimaiCommentRegex(); + + /* + * 根据[simai文档](https://w.atwiki.jp/simai/pages/1002.html),井号如果出现在[]或{}内是合法语法,并非注释。 + * 因此在尝试匹配注释时,应该排除掉#前面有未闭合的[或{的情况。 + */ + [GeneratedRegex(@"(? errors) + { + chartText = chartText.ReplaceLineEndings(); + try + { + return new SimaiParser().ChartOfToken(new SimaiTokenizer().TokensFromText(chartText)); + } + catch (Exception e) + { + logger.LogWarning(e, "无法直接解析谱面"); + } + + try + { + var normalizedText = SimaiCommentRegex().Replace(chartText, ""); + normalizedText = SimaiCommentRegex2().Replace(normalizedText, ""); + return new SimaiParser().ChartOfToken(new SimaiTokenizer().TokensFromText(normalizedText)); + } + catch (Exception) + { + // ignored + } + + try + { + var normalizedText = FixChartSimaiSharp(chartText) + // 不飞的星星 + .Replace("-?", "?-"); + // 移除注释 + normalizedText = SimaiCommentRegex().Replace(normalizedText, ""); + normalizedText = SimaiCommentRegex2().Replace(normalizedText, ""); + var tokens = new SimaiTokenizer().TokensFromText(normalizedText); + for (var i = 0; i < tokens.Length; i++) + { + if (tokens[i].Contains("]b")) + { + tokens[i] = tokens[i].Replace("]b", "]").Replace("[", "b["); + } + } + + var maiLibChart = new SimaiParser().ChartOfToken(tokens); + errors.Add(new ImportChartMessage(string.Format(Locale.ChartFixedMinorErrors, level), MessageLevel.Info)); + return maiLibChart; + } + catch (Exception e) + { + errors.Add(new ImportChartMessage(string.Format(Locale.ChartMaiLibParseError, level, MaiLibErrMsgRegex().Replace(e.Message, "")), MessageLevel.Warning)); + logger.LogWarning(e, "无法在手动修正错误后解析谱面"); + } + + if (simaiSharpChart is null) + { + return null; + } + + try + { + var reSerialized = SimaiConvert.Serialize(simaiSharpChart); + reSerialized = reSerialized.Replace("{0}", "{4}"); + var maiLibChart = new SimaiParser().ChartOfToken(new SimaiTokenizer().TokensFromText(reSerialized)); + errors.Add(new ImportChartMessage(string.Format(Locale.ChartSimaiSharpFallback, level), MessageLevel.Warning)); + return maiLibChart; + } + catch (Exception e) + { + SentrySdk.CaptureException(e); + errors.Add(new ImportChartMessage(string.Format(Locale.ChartParseFailed, level), MessageLevel.Fatal)); + return null; + } + } + + public static float CalcMusicPadding(MaiChart chart, float first) + { + // TimingChanges 对应的是所有的 {int} + var bpm = chart.TimingChanges[0].tempo; + // 一小节多长 + var bar = 60 / bpm * 4; + + // 第一押什么时候出来 + var firstTiming = chart.NoteCollections[0].time + first; + return bar - firstTiming; + } + + private record AllChartsEntry(string chartText, MaiChart simaiSharpChart); + + public ImportChartResult ImportMaidata(MusicXml music, IFormFile file, ShiftMethod shift, + bool ignoreLevelNum, bool debug) + { + var id = music.Id; + var isUtage = id > 100000; + var errors = new List(); + var kvps = new SimaiFile(file.OpenReadStream()).ToKeyValuePairs(); + var maiData = new Dictionary(); + foreach (var (key, value) in kvps) + { + maiData[key] = value; + } + + var allCharts = new Dictionary(); + for (var i = 2; i < 9; i++) + { + if (!string.IsNullOrWhiteSpace(maiData.GetValueOrDefault($"inote_{i}"))) + { + allCharts.Add(i, new AllChartsEntry(maiData[$"inote_{i}"], TryParseChartSimaiSharp(maiData[$"inote_{i}"], i, errors))); + } + } + + if (!string.IsNullOrWhiteSpace(maiData.GetValueOrDefault("inote_0"))) + { + allCharts.Add(0, new AllChartsEntry(maiData["inote_0"], TryParseChartSimaiSharp(maiData["inote_0"], 0, errors))); + } + + float.TryParse(maiData.GetValueOrDefault("first"), out var first); + // Mai 的歌曲是从两帧后开始播放的 + first -= 1 / 30f; + + var paddings = allCharts.Values.Select(chart => CalcMusicPadding(chart.simaiSharpChart, first)).ToList(); + // 音频前面被增加了多少 + var audioPadding = paddings.Max(); // bar - firstTiming = bar - 谱面前面休止符的时间 - &first + var shouldAddBar = false; + float chartPadding; + switch (shift) + { + case ShiftMethod.Legacy: + chartPadding = audioPadding + first; + break; + case ShiftMethod.Bar when audioPadding + first > 0.1: + shouldAddBar = true; + chartPadding = 0f; + break; + default: + chartPadding = 0f; + break; + } + + if (shouldAddBar) + { + foreach (var (level, chart) in allCharts) + { + var newText = Add1Bar(chart.chartText); + allCharts[level] = new AllChartsEntry(newText, TryParseChartSimaiSharp(newText, level, errors)); + } + } + + foreach (var (level, chart) in allCharts) + { + // 宴会场只导入第一个谱面 + if (isUtage && music.Charts[0].Enable) break; + + // var levelPadding = CalcMusicPadding(chart, first); + var bpm = chart.simaiSharpChart.TimingChanges[0].tempo; + music.Bpm = bpm; + // 一个小节多少秒 + var bar = 60 / bpm * 4; + + // 我们要让这个谱面真正的内容(忽略 first)延后多少 + // levelPadding 似乎不需要算,因为每个谱面真正的内容都是从同一个地方开始 + // 所以只要在前面加上 audioPadding + first 时间的休止符 + // 最早出音符的那个谱面的第一押之前一定是 1bar(小节)的休止符 + // |_| levelPadding + // |_______| audioPadding + // |________________| bar + // |________________| bar + // |-------|---|----|-----|----- + // | | | | | 这个谱面的第一押 + // | | | | 可能是另一个谱面难度的第一押,firstTiming,它可能导致 audioPadding > levelPadding + // | | |____| 这一段是休止符 + // | | | 每个谱面真正的内容都是从这里开始 + // | |___| first skip 掉的部分 + // | | 原先音频的开头 + // | 加了 padding 的音频开头 + + # region 设定 targetLevel + + var targetLevel = level - 2; + + // 处理非标准难度 + if (level is > 6 or < 1) + { + // 分给 3 4 0 + if (!music.Charts[3].Enable) + { + targetLevel = 3; + } + else if (!music.Charts[4].Enable) + { + targetLevel = 4; + } + else if (!music.Charts[0].Enable) + { + targetLevel = 0; + } + else + { + continue; + } + } + + if (isUtage) targetLevel = 0; + + # endregion + + var targetChart = music.Charts[targetLevel]; + targetChart.Path = $"{id:000000}_0{targetLevel}.ma2"; + var levelNumStr = maiData.GetValueOrDefault($"lv_{level}"); + if (!string.IsNullOrWhiteSpace(levelNumStr)) + { + levelNumStr = levelNumStr.Replace("+", ".7"); + } + + float.TryParse(levelNumStr, out var levelNum); + targetChart.LevelId = MaiUtils.GetLevelId((int)(levelNum * 10)); + // 忽略定数 + if (!ignoreLevelNum) + { + targetChart.Level = (int)Math.Floor(levelNum); + targetChart.LevelDecimal = (int)Math.Floor(levelNum * 10 % 10); + } + + targetChart.Designer = maiData.GetValueOrDefault($"des_{level}") ?? maiData.GetValueOrDefault("des") ?? ""; + var maiLibChart = TryParseChart(chart.chartText, chart.simaiSharpChart, level, errors); + if (maiLibChart is null) + { + return new ImportChartResult(errors, true); + } + + var originalConverted = maiLibChart.Compose(ChartEnum.ChartVersion.Ma2_104); + + if (debug) + { + File.WriteAllText(Path.Combine(Path.GetDirectoryName(music.FilePath)!, targetChart.Path + ".afterSimaiSharp.txt"), SimaiConvert.Serialize(chart.simaiSharpChart)); + File.WriteAllText(Path.Combine(Path.GetDirectoryName(music.FilePath)!, targetChart.Path + ".preShift.ma2"), originalConverted); + File.WriteAllText(Path.Combine(Path.GetDirectoryName(music.FilePath)!, targetChart.Path + ".preShift.txt"), maiLibChart.Compose(ChartEnum.ChartVersion.SimaiFes)); + } + + if (chartPadding != 0) + { + try + { + maiLibChart.ShiftByOffset((int)Math.Round(chartPadding / bar * maiLibChart.Definition)); + } + catch (Exception e) + { + SentrySdk.CaptureEvent(new SentryEvent(e) + { + Message = Locale.ChartShiftByOffsetError + }); + errors.Add(new ImportChartMessage(Locale.ChartShiftError, MessageLevel.Fatal)); + return new ImportChartResult(errors, true); + } + } + + var shiftedConverted = maiLibChart.Compose(ChartEnum.ChartVersion.Ma2_104); + + if (shiftedConverted.Split('\n').Length != originalConverted.Split('\n').Length) + { + errors.Add(new ImportChartMessage(Locale.ChartNotesMissing, MessageLevel.Warning)); + logger.LogWarning("BUG! shiftedConverted: {shiftedLen}, originalConverted: {originalLen}", shiftedConverted.Split('\n').Length, originalConverted.Split('\n').Length); + } + + // Just use T_NUM_ALL value in ma2 file + targetChart.MaxNotes = ParseTNumAllFromMa2(shiftedConverted); + // Fallback to maiLibChart if T_NUM_ALL not found + if (targetChart.MaxNotes == 0) targetChart.MaxNotes = maiLibChart.AllNoteNum; + + File.WriteAllText(Path.Combine(Path.GetDirectoryName(music.FilePath)!, targetChart.Path), shiftedConverted); + if (debug) + { + File.WriteAllText(Path.Combine(Path.GetDirectoryName(music.FilePath)!, targetChart.Path + ".afterShift.txt"), maiLibChart.Compose(ChartEnum.ChartVersion.SimaiFes)); + } + + targetChart.Enable = true; + } + + music.Name = maiData["title"]; + music.Artist = maiData.GetValueOrDefault("artist") ?? ""; + music.ShiftMethod = shift.ToString(); + float wholebpm; + if (float.TryParse(maiData.GetValueOrDefault("wholebpm"), out wholebpm)) + music.Bpm = wholebpm; + + return new ImportChartResult(errors, false); + } + + public static int ParseTNumAllFromMa2(string ma2Content) + { + var lines = ma2Content.Split('\n'); + // 从后往前读取,因为 T_NUM_ALL 在文件最后 + for (int i = lines.Length - 1; i >= 0; i--) + { + var trimmedLine = lines[i].Trim(); + if (trimmedLine.StartsWith("T_NUM_ALL", StringComparison.OrdinalIgnoreCase)) + { + var parts = trimmedLine.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries); + if (parts.Length >= 2 && int.TryParse(parts[1], out int tNumAll)) + { + return tNumAll; + } + } + } + // Fallback to 0 in case + return 0; + } +} From d403221c8e23993a977c218dad6a7306ec194fde Mon Sep 17 00:00:00 2001 From: Starrah Date: Sun, 1 Feb 2026 11:20:18 +0800 Subject: [PATCH 09/11] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0ReplaceChart?= =?UTF-8?q?=E7=9A=84=E6=9B=BF=E6=8D=A2maidata=E5=8A=9F=E8=83=BD=E7=9A=84?= =?UTF-8?q?=E5=89=8D=E5=90=8E=E7=AB=AF=E5=AE=8C=E6=95=B4=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=92=8C=E6=8E=A5=E5=8F=A3=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Charts/ChartController.cs | 62 +++++++++++++------ MaiChartManager/Front/src/client/apiGen.ts | 3 +- .../DragDropDispatcher/ReplaceChartModal.tsx | 43 +++++++------ .../Services/MaidataImportService.cs | 30 ++++++--- 4 files changed, 89 insertions(+), 49 deletions(-) diff --git a/MaiChartManager/Controllers/Charts/ChartController.cs b/MaiChartManager/Controllers/Charts/ChartController.cs index ed07830e..c89ca41d 100644 --- a/MaiChartManager/Controllers/Charts/ChartController.cs +++ b/MaiChartManager/Controllers/Charts/ChartController.cs @@ -5,7 +5,7 @@ namespace MaiChartManager.Controllers.Charts; [ApiController] [Route("MaiChartManagerServlet/[action]Api/{assetDir}/{id:int}/{level:int}")] -public class ChartController(StaticSettings settings, ILogger logger) : ControllerBase +public class ChartController(StaticSettings settings, ILogger logger, MaidataImportService importService) : ControllerBase { [HttpPost] public void EditChartLevel(int id, int level, [FromBody] int value, string assetDir) @@ -96,29 +96,53 @@ public void EditChartEnable(int id, int level, [FromBody] bool value, string ass } [HttpPost] - public void ReplaceChart(int id, int level, IFormFile file, string assetDir, + public ImportChartResult ReplaceChart(int id, int level, IFormFile file, string assetDir, [FromForm] ShiftMethod? shift) { var music = settings.GetMusic(id, assetDir); - if (music == null || file == null) return; - // TODO 判断是MA2还是maidata.txt,走不同的逻辑 - var targetChart = music.Charts[level]; - targetChart.Path = $"{id:000000}_0{level}.ma2"; - using var stream = System.IO.File.Open(Path.Combine(StaticSettings.StreamingAssets, assetDir, "music", $"music{id:000000}", targetChart.Path), FileMode.Create); - file.CopyTo(stream); - targetChart.Problems.Clear(); - stream.Close(); - - // 检查新谱面ma2的音符数量是否有变化,如果有修正之 - string fileContent; - using (var reader = new StreamReader(file.OpenReadStream())) + if (music == null || file == null) return new ImportChartResult([new ImportChartMessage("文件上传失败", MessageLevel.Fatal)], true); + if (file.FileName.EndsWith(".ma2")) { - fileContent = reader.ReadToEnd(); + var targetChart = music.Charts[level]; + targetChart.Path = $"{id:000000}_0{level}.ma2"; + using var stream = System.IO.File.Open(Path.Combine(StaticSettings.StreamingAssets, assetDir, "music", $"music{id:000000}", targetChart.Path), FileMode.Create); + file.CopyTo(stream); + targetChart.Problems.Clear(); + stream.Close(); + + // 检查新谱面ma2的音符数量是否有变化,如果有修正之 + string fileContent; + using (var reader = new StreamReader(file.OpenReadStream())) + { + fileContent = reader.ReadToEnd(); + } + var newMaxNotes = MaidataImportService.ParseTNumAllFromMa2(fileContent); + if (newMaxNotes != 0 && targetChart.MaxNotes != newMaxNotes) + { + targetChart.MaxNotes = newMaxNotes; + } + + return new ImportChartResult([], false); } - var newMaxNotes = MaidataImportService.ParseTNumAllFromMa2(fileContent); - if (newMaxNotes != 0 && targetChart.MaxNotes != newMaxNotes) + else if (file.FileName.EndsWith("maidata.txt")) { - targetChart.MaxNotes = newMaxNotes; - } + if (level != -1) throw new NotImplementedException("使用maidata时暂不支持只替换单个难度谱面,只能同时替换全部的"); + // 通过此前的谱面的定数是否为0,判断是否需要ignoreLevelNum + bool ignoreLevelNum = true; + foreach (var chart in music.Charts) + { + if (music.Id < 100000 && chart.Enable && chart.Level > 0) ignoreLevelNum = false; + } + var importResult = importService.ImportMaidata(music, file, (ShiftMethod)shift, ignoreLevelNum, false, true); + if (!importResult.Fatal) + { + music.Save(); + music.Refresh(); + } + + return importResult; + } + // 正常来说是不会进到这里的,因为前端已经对文件名做了校验了,所以这个报错用户正常来说是看不到的,就不做i18n了。 + else return new ImportChartResult([new ImportChartMessage("不支持的文件格式!", MessageLevel.Fatal)], true); } } \ No newline at end of file diff --git a/MaiChartManager/Front/src/client/apiGen.ts b/MaiChartManager/Front/src/client/apiGen.ts index 7fe258b7..442fbf58 100644 --- a/MaiChartManager/Front/src/client/apiGen.ts +++ b/MaiChartManager/Front/src/client/apiGen.ts @@ -970,11 +970,12 @@ export class Api extends HttpClient - this.request({ + this.request({ path: `/MaiChartManagerServlet/ReplaceChartApi/${assetDir}/${id}/${level}`, method: "POST", body: data, type: ContentType.FormData, + format: "json", ...params, }), diff --git a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx index af40140e..6275e0dd 100644 --- a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx +++ b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx @@ -7,7 +7,7 @@ import { DIFFICULTY } from '@/consts'; import api from '@/client/api'; import CheckingModal from "@/components/ImportCreateChartButton/ImportChartButton/CheckingModal"; import LevelTagsDisplay from "@/components/LevelTagsDisplay"; -import { Chart, ImportChartCheckResult, ShiftMethod } from "@/client/apiGen"; +import { Chart, ImportChartCheckResult, ImportChartResult, ShiftMethod } from "@/client/apiGen"; import ImportAlert from "@/components/ImportCreateChartButton/ImportChartButton/ImportAlert"; import { defaultTempOptions, ImportChartMessageEx, TempOptions } from "@/components/ImportCreateChartButton/ImportChartButton/types"; import ShiftModeSelector from "@/components/ImportCreateChartButton/ImportChartButton/ShiftModeSelector"; @@ -18,16 +18,15 @@ export let prepareReplaceChart = async (fileHandle?: FileSystemFileHandle) => { export default defineComponent({ setup() { - const message = useMessage(); const dialog = useDialog(); const checking = ref(false); const fileHandle = shallowRef(null); - const show = ref("") // 取值范围:"“(不显示),"ma2","maidata" + const show = ref<"" | "ma2" | "maidata" | "failed">(""); - const checkRet = ref(null) + const apiResp = ref(null) const checkErrors = computed(()=>{ - return checkRet.value?.errors?.map(it=>({...it, name: checkRet.value?.title!})) ?? [] + return apiResp.value?.errors?.map(it=>({...it, name: selectedMusic.value!.name!})) ?? [] }) const tempOption = ref({...defaultTempOptions}) @@ -52,14 +51,14 @@ export default defineComponent({ const name = fHandle.name; // 对maidata.txt和ma2分类讨论,前者执行ImportCheck - if (name == "maidata.txt") { + if (name === "maidata.txt") { try { checking.value = true; const file = await fHandle.getFile(); const r = (await api.ImportChartCheck({file, isReplacement: true})).data; if (!checking.value) return; // 说明检查期间用户点击了关闭按钮、取消了操作。则不再执行后续流程。 - checkRet.value = r; + apiResp.value = r; if (selectedMusic.value?.shiftMethod) { // 说明是新版导入的谱面、ShiftMethod已经被写入XML了。此时锁定ShiftMethod选项,不准用户自己选择 tempOption.value = {shift: selectedMusic.value.shiftMethod as ShiftMethod, shiftLocked: true}; } @@ -74,14 +73,20 @@ export default defineComponent({ } } - const replaceMa2 = async () => { + const replaceChart = async () => { if (!fileHandle.value) return; try { const file = await fileHandle.value.getFile(); fileHandle.value = null; + const level = show.value === "maidata" ? -1 : selectedLevel.value; show.value = ""; - await api.ReplaceChart(selectMusicId.value, selectedLevel.value, selectedADir.value, { file }); - message.success(t('music.edit.replaceChartSuccess')); + const result = (await api.ReplaceChart(selectMusicId.value, level, selectedADir.value, { file, shift: tempOption.value.shift })).data; + if (!result.fatal) { + dialog.success({title: t('music.edit.replaceChartSuccess')}); + } else { + apiResp.value = result; // 用于在失败时显示错误信息 + show.value = "failed"; + } await updateMusicList(); } catch (error) { globalCapture(error, t('music.edit.replaceChartFailed')); @@ -91,7 +96,7 @@ export default defineComponent({ const chartsForDisplayLevel = computed(()=>{ const result: (Chart | undefined)[] = [...selectedMusic.value!.charts!] - if (show.value == "ma2") { // 此时只显示所选择的难度,其他难度不要显示 + if (show.value === "ma2") { // 此时只显示所选择的难度,其他难度不要显示 for (let i=0; i{{ default: () =>
- {show.value == "ma2" && <> + {show.value === "ma2" && <> {t('music.edit.replaceChartConfirm', { level: DIFFICULTY[selectedLevel.value!] })}
{fileHandle.value?.name}
} - {show.value == "maidata" && } -
+ {(show.value === "maidata" || show.value === "failed") && } + {show.value !== "failed" &&
#{selectMusicId.value}
{selectedMusic.value!.name}
-
- {show.value == "maidata" &&
+
} + {show.value === "maidata" &&
{t('music.edit.replaceChartShiftModeHint')}
}
, footer: () => - show.value = ""}>{t('common.cancel')} - {t('common.confirm')} + show.value = ""}>{show.value !== "failed" ? t('common.cancel') : t('common.close')} + {show.value !== "failed" && {t('common.confirm')}} }} checking.value=false} /> diff --git a/MaiChartManager/Services/MaidataImportService.cs b/MaiChartManager/Services/MaidataImportService.cs index 08d2b8c4..9306b3a6 100644 --- a/MaiChartManager/Services/MaidataImportService.cs +++ b/MaiChartManager/Services/MaidataImportService.cs @@ -211,7 +211,7 @@ public static float CalcMusicPadding(MaiChart chart, float first) private record AllChartsEntry(string chartText, MaiChart simaiSharpChart); public ImportChartResult ImportMaidata(MusicXml music, IFormFile file, ShiftMethod shift, - bool ignoreLevelNum, bool debug) + bool ignoreLevelNum, bool debug, bool isReplacement = false) { var id = music.Id; var isUtage = id > 100000; @@ -269,14 +269,19 @@ public ImportChartResult ImportMaidata(MusicXml music, IFormFile file, ShiftMeth } } + foreach (var targetChart in music.Charts) + { + targetChart.Enable = false; + } + + float bpm = 0f; foreach (var (level, chart) in allCharts) { // 宴会场只导入第一个谱面 if (isUtage && music.Charts[0].Enable) break; // var levelPadding = CalcMusicPadding(chart, first); - var bpm = chart.simaiSharpChart.TimingChanges[0].tempo; - music.Bpm = bpm; + bpm = chart.simaiSharpChart.TimingChanges[0].tempo; // 一个小节多少秒 var bar = 60 / bpm * 4; @@ -399,13 +404,18 @@ public ImportChartResult ImportMaidata(MusicXml music, IFormFile file, ShiftMeth targetChart.Enable = true; } - music.Name = maiData["title"]; - music.Artist = maiData.GetValueOrDefault("artist") ?? ""; - music.ShiftMethod = shift.ToString(); - float wholebpm; - if (float.TryParse(maiData.GetValueOrDefault("wholebpm"), out wholebpm)) - music.Bpm = wholebpm; - + if (!isReplacement) + { + // 只在新建时设定曲目信息,替换时不设定 + music.Name = maiData["title"]; + music.Artist = maiData.GetValueOrDefault("artist") ?? ""; + music.ShiftMethod = shift.ToString(); + float wholebpm; + if (float.TryParse(maiData.GetValueOrDefault("wholebpm"), out wholebpm)) + music.Bpm = wholebpm; // 优先使用&wholebpm + else music.Bpm = bpm; // 如果不存在,则使用谱面中开头声明的bpm + } + return new ImportChartResult(errors, false); } From f355cb191fcea9d696a44ff2e4b935b4f716f8f8 Mon Sep 17 00:00:00 2001 From: Starrah Date: Sun, 1 Feb 2026 11:58:45 +0800 Subject: [PATCH 10/11] =?UTF-8?q?fix:=20=E5=9C=A8ReplaceChart=E5=90=8E?= =?UTF-8?q?=E3=80=81updateMusicList=E6=97=B6=EF=BC=8Cdisable=E6=8E=89Music?= =?UTF-8?q?Edit/ChartPanel=E7=9A=84watch=E7=9A=84=E8=87=AA=E5=8A=A8sync?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 如ReplaceChart等后端接口,可能会涉及对MusicXml中的信息进行修改后保存。此时前端updateMusicList时会发现相关数据出现变更,触发了MusicEdit/ChartPanel中的watch,造成发送了多个多余的edit请求、同时modified也被错误设置为true。 因此,对于涉及内部对xml进行修改后会自动保存的后端接口,可以在updateMusicList期间打开本选项,以阻止MusicEdit/ChartPanel中的sync动作。 --- MaiChartManager/Controllers/Charts/ChartController.cs | 1 + .../DragDropDispatcher/ReplaceChartModal.tsx | 2 +- .../Front/src/components/MusicEdit/ChartPanel.tsx | 4 ++-- .../Front/src/components/MusicEdit/index.tsx | 4 ++-- MaiChartManager/Front/src/store/refs.ts | 11 +++++++++-- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/MaiChartManager/Controllers/Charts/ChartController.cs b/MaiChartManager/Controllers/Charts/ChartController.cs index c89ca41d..11dbad2a 100644 --- a/MaiChartManager/Controllers/Charts/ChartController.cs +++ b/MaiChartManager/Controllers/Charts/ChartController.cs @@ -121,6 +121,7 @@ public ImportChartResult ReplaceChart(int id, int level, IFormFile file, string { targetChart.MaxNotes = newMaxNotes; } + music.Save(); return new ImportChartResult([], false); } diff --git a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx index 6275e0dd..cf1adcf0 100644 --- a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx +++ b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx @@ -87,7 +87,7 @@ export default defineComponent({ apiResp.value = result; // 用于在失败时显示错误信息 show.value = "failed"; } - await updateMusicList(); + await updateMusicList(true); } catch (error) { globalCapture(error, t('music.edit.replaceChartFailed')); console.error(error); diff --git a/MaiChartManager/Front/src/components/MusicEdit/ChartPanel.tsx b/MaiChartManager/Front/src/components/MusicEdit/ChartPanel.tsx index c780e603..64a0c20b 100644 --- a/MaiChartManager/Front/src/components/MusicEdit/ChartPanel.tsx +++ b/MaiChartManager/Front/src/components/MusicEdit/ChartPanel.tsx @@ -2,7 +2,7 @@ import { computed, defineComponent, PropType, watch } from "vue"; import { Chart } from "@/client/apiGen"; import { NButton, NFlex, NForm, NFormItem, NInput, NInputNumber, NSelect, NSwitch } from "naive-ui"; import api from "@/client/api"; -import { selectedADir, selectedMusic } from "@/store/refs"; +import { disableSync, selectedADir, selectedMusic } from "@/store/refs"; import { LEVELS } from "@/consts"; import ProblemsDisplay from "@/components/ProblemsDisplay"; import PreviewChartButton from "@/components/MusicEdit/PreviewChartButton"; @@ -28,7 +28,7 @@ export default defineComponent({ }) const sync = (key: keyof Chart, method: Function) => async () => { - if (!props.chart) return; + if (disableSync.value || !props.chart) return; selectedMusic.value!.modified = true; await method(props.songId, props.chartIndex, selectedADir.value, props.chart[key]!); } diff --git a/MaiChartManager/Front/src/components/MusicEdit/index.tsx b/MaiChartManager/Front/src/components/MusicEdit/index.tsx index 9914f09b..e94ebbef 100644 --- a/MaiChartManager/Front/src/components/MusicEdit/index.tsx +++ b/MaiChartManager/Front/src/components/MusicEdit/index.tsx @@ -1,6 +1,6 @@ import { computed, defineComponent, onMounted, PropType, ref, watch } from "vue"; import { Chart, GenreXml, MusicXmlWithABJacket } from "@/client/apiGen"; -import { addVersionList, genreList, globalCapture, selectedADir, selectedMusic as info, selectMusicId, updateAddVersionList, updateGenreList, updateMusicList, selectedLevel } from "@/store/refs"; +import { addVersionList, genreList, globalCapture, selectedADir, selectedMusic as info, selectMusicId, updateAddVersionList, updateGenreList, updateMusicList, selectedLevel, disableSync } from "@/store/refs"; import api from "@/client/api"; import { NButton, NFlex, NForm, NFormItem, NInput, NInputNumber, NSelect, NSwitch, NTabPane, NTabs, SelectOption, useDialog, useMessage } from "naive-ui"; import JacketBox from "../JacketBox"; @@ -27,7 +27,7 @@ const Component = defineComponent({ } const sync = (key: keyof MusicXmlWithABJacket, method: Function) => async () => { - if (!info.value) return; + if (disableSync.value || !info.value) return; info.value!.modified = true; await method(info.value.id!, info.value.assetDir, (info.value as any)[key]!); } diff --git a/MaiChartManager/Front/src/store/refs.ts b/MaiChartManager/Front/src/store/refs.ts index fe0d6f5f..345197ee 100644 --- a/MaiChartManager/Front/src/store/refs.ts +++ b/MaiChartManager/Front/src/store/refs.ts @@ -55,6 +55,10 @@ export const musicList = computed(() => musicListAll.value.filter(m => m.assetDi export const selectedMusic = computed(() => musicList.value.find(m => m.id === selectMusicId.value)); export const selectedLevel = ref(0); +// 如ReplaceChart等后端接口可能会涉及对MusicXml中的信息进行修改后保存。此时前端updateMusicList时会发现相关数据出现变更,触发了MusicEdit/ChartPanel中的watch,造成发送多余的edit请求、同时modified也被错误设置为true。 +// 因此,对于涉及内部对xml进行修改后会自动保存的后端接口,可以在updateMusicList期间打开本选项,以阻止MusicEdit/ChartPanel中的sync动作。 +export const disableSync = ref(false); + export const aquaMaiConfig = ref() export const modUpdateInfo = ref>['data']>([{ type: 'builtin', @@ -90,8 +94,11 @@ export const updateAddVersionList = async () => { addVersionList.value = response.data; } -export const updateMusicList = async () => { - musicListAll.value = (await api.GetMusicList()).data; +export const updateMusicList = async (disableAutoSync=false) => { + const data = (await api.GetMusicList()).data; + if (disableAutoSync) disableSync.value = true; + musicListAll.value = data; + setTimeout(()=>disableSync.value = false); // timeout=0表示在下一帧执行 } export const updateAssetDirs = async () => { From bd79db64b29409db38b5ca6dac2bd8ce70030e47 Mon Sep 17 00:00:00 2001 From: Starrah Date: Sun, 1 Feb 2026 12:58:27 +0800 Subject: [PATCH 11/11] =?UTF-8?q?gemini=E8=AF=B4=E5=BE=97=E5=AF=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MaiChartManager/Controllers/Charts/ChartController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MaiChartManager/Controllers/Charts/ChartController.cs b/MaiChartManager/Controllers/Charts/ChartController.cs index 11dbad2a..add7dbf0 100644 --- a/MaiChartManager/Controllers/Charts/ChartController.cs +++ b/MaiChartManager/Controllers/Charts/ChartController.cs @@ -108,7 +108,6 @@ public ImportChartResult ReplaceChart(int id, int level, IFormFile file, string using var stream = System.IO.File.Open(Path.Combine(StaticSettings.StreamingAssets, assetDir, "music", $"music{id:000000}", targetChart.Path), FileMode.Create); file.CopyTo(stream); targetChart.Problems.Clear(); - stream.Close(); // 检查新谱面ma2的音符数量是否有变化,如果有修正之 string fileContent;