diff --git a/MaiChartManager/Controllers/Charts/ChartController.cs b/MaiChartManager/Controllers/Charts/ChartController.cs index 91a726a..a802a63 100644 --- a/MaiChartManager/Controllers/Charts/ChartController.cs +++ b/MaiChartManager/Controllers/Charts/ChartController.cs @@ -95,15 +95,29 @@ 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); 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 20bf25a..5776969 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(); @@ -559,6 +566,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; @@ -568,7 +576,7 @@ public ImportChartResult ImportChart( } - private static int ParseTNumAllFromMa2(string ma2Content) + public static int ParseTNumAllFromMa2(string ma2Content) { var lines = ma2Content.Split('\n'); // 从后往前读取,因为 T_NUM_ALL 在文件最后 diff --git a/MaiChartManager/Front/src/client/apiGen.ts b/MaiChartManager/Front/src/client/apiGen.ts index c0aa133..7fe258b 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 a0cbf59..06fb0ae 100644 --- a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx +++ b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx @@ -1,24 +1,68 @@ 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, 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 const replaceChartFileHandle = shallowRef(null); +export let prepareReplaceChart = async (fileHandle?: FileSystemFileHandle) => { +} export default defineComponent({ // props: { // }, setup(props, { emit }) { const message = useMessage(); + const dialog = useDialog(); - const replaceChart = async () => { - if (!replaceChartFileHandle.value) return; + const checking = ref(false); + const ma2Handle = shallowRef(null); + + 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; + // 对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')}) + } + } + + 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(); @@ -28,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/DragDropDispatcher/index.tsx b/MaiChartManager/Front/src/components/DragDropDispatcher/index.tsx index 86e0d7e..3cb9fb5 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/components/MusicEdit/ChartPanel.tsx b/MaiChartManager/Front/src/components/MusicEdit/ChartPanel.tsx index 1fd51cf..c780e60 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/Front/src/locales/en.yaml b/MaiChartManager/Front/src/locales/en.yaml index 8dd7b27..75f5943 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 da16da8..49fdaf7 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 f9c02ec..05b10a8 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: 保存成功 diff --git a/MaiChartManager/Locale.Designer.cs b/MaiChartManager/Locale.Designer.cs index 98c5e49..4961999 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 20f9e43..b89915e 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 f6ddf3c..810a0f5 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 91b66db..c1282c2 100644 --- a/MaiChartManager/Locale.zh-hant.resx +++ b/MaiChartManager/Locale.zh-hant.resx @@ -285,4 +285,7 @@ 開機自動以伺服器模式運作並最小化到托盤 + + >請注意!本「替換譜面」功能僅限用於:譜面音符內容在原來的基礎上發生修改,且音訊內容未變、&first偏移量未變、譜面中第一個音符的時刻未變的情況。否則,您需要刪除整個譜面後重新匯入。 + \ No newline at end of file diff --git a/MaiChartManager/Models/MusicXml.cs b/MaiChartManager/Models/MusicXml.cs index 86f6da8..7136519 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