// ==UserScript== // @name 《闪韵灵境谱面编辑器》功能扩展 // @namespace cipher-editor-extension // @version 1.0.1 // @description 为《闪韵灵境谱面编辑器》扩展各种实用的功能 // @author 如梦Nya // @license MIT // @run-at document-body // @grant unsafeWindow // @match https://cipher-editor-cn.picovr.com/* // @icon https://cipher-editor-cn.picovr.com/assets/logo-eabc5412.png // @require https://code.jquery.com/jquery-3.6.0.min.js // @downloadURL none // ==/UserScript== const $ = window.jQuery let JSZip = undefined // ================================================================================ 工具类 ================================================================================ /** * 数据库操作类 */ class WebDB { constructor() { this.db = undefined } /** * 打开数据库 * @param {string} dbName 数据库名 * @param {number | undefined} dbVersion 数据库版本 * @returns */ open(dbName, dbVersion) { let self = this return new Promise(function (resolve, reject) { const indexDB = unsafeWindow.indexedDB || unsafeWindow.webkitIndexedDB || unsafeWindow.mozIndexedDB let req = indexDB.open(dbName, dbVersion) req.onerror = reject req.onsuccess = function (e) { self.db = e.target.result resolve(self) } }); } /** * 查出一条数据 * @param {string} tableName 表名 * @param {string} key 键名 * @returns */ get(tableName, key) { let self = this return new Promise(function (resolve, reject) { let req = self.db.transaction([tableName]).objectStore(tableName).get(key) req.onerror = reject req.onsuccess = function (e) { resolve(e.target.result) } }); } /** * 插入、更新一条数据 * @param {string} tableName 表名 * @param {string} key 键名 * @param {any} value 数据 * @returns */ put(tableName, key, value) { let self = this return new Promise(function (resolve, reject) { let req = self.db.transaction([tableName], 'readwrite').objectStore(tableName).put(value, key) req.onerror = reject req.onsuccess = function (e) { resolve(e.target.result) } }); } /** * 关闭数据库 */ close() { this.db.close() delete this.db } } /** * 闪韵灵境工具类 */ class CipherUtils { /** * 获取当前谱面的信息 */ static getNowBeatmapInfo() { let url = location.href // ID let matchId = url.match(/id=(\w*)/) let id = matchId ? matchId[1] : "" // BeatSaverID let beatsaverId = "" let nameBoxList = $(".css-tpsa02") if (nameBoxList.length > 0) { let name = nameBoxList[0].innerHTML let matchBeatsaverId = name.match(/\[(\w*)\]/) if (matchBeatsaverId) beatsaverId = matchBeatsaverId[1] } // 难度 let matchDifficulty = url.match(/difficulty=(\w*)/) let difficulty = matchDifficulty ? matchDifficulty[1] : "" return { id, difficulty, beatsaverId } } /** * 处理歌曲文件 * @param {ArrayBuffer} rawBuffer * @returns */ static applySongFile(rawBuffer) { // 前面追加数据,以通过校验 let rawData = new Uint8Array(rawBuffer) let BYTE_VERIFY_ARRAY = [235, 186, 174, 235, 186, 174, 235, 186, 174, 85, 85] let buffer = new ArrayBuffer(rawData.length + BYTE_VERIFY_ARRAY.length) let dataView = new DataView(buffer) for (let i = 0; i < BYTE_VERIFY_ARRAY.length; i++) { dataView.setUint8(i, BYTE_VERIFY_ARRAY[i]) } for (let i = 0; i < rawData.length; i++) { dataView.setUint8(BYTE_VERIFY_ARRAY.length + i, rawData[i]) } return new Blob([buffer], { type: "application/octet-stream" }) } /** * 关闭编辑器顶部菜单 */ static closeEditorTopMenu() { $(".css-1k12r02").click() } /** * 显示Loading */ static showLoading() { $("main").append('
') } /** * 隐藏Loading */ static hideLoading() { $(".css-c81162").remove() } } /** * 沙盒工具类 */ class SandBox { /** @type {HTMLIFrameElement | undefined} */ static _sandBoxIframe = undefined /** * 创建一个Iframe沙盒 * @returns {HTMLIFrameElement} */ static getDocument() { if (!SandBox._sandBoxIframe) { let id = GM_info.script.namespace + "_iframe" // 找ID let iframes = $('#' + id) if (iframes.length > 0) SandBox._sandBoxIframe = iframes[0] // 不存在,创建一个 if (!SandBox._sandBoxIframe) { let ifr = document.createElement("iframe"); ifr.id = id ifr.style.display = "none" document.body.appendChild(ifr); SandBox._sandBoxIframe = ifr; } } return SandBox._sandBoxIframe } /** * 动态添加Script * @param {string} url 脚本链接 * @returns {Promise} */ static dynamicLoadJs(url) { return new Promise(function (resolve, reject) { let ifrdoc = SandBox.getDocument().contentDocument; let script = ifrdoc.createElement('script') script.type = 'text/javascript' script.src = url script.onload = script.onreadystatechange = function () { if (!this.readyState || this.readyState === "loaded" || this.readyState === "complete") { resolve(script) script.onload = script.onreadystatechange = null } } ifrdoc.body.appendChild(script) }); } } /** * BeatSaver工具类 */ class BeatSaverUtils { /** * 搜索歌曲列表 * @param {string} searchKey 搜索关键字 * @param {number} pageCount 搜索页数 * @returns */ static searchSongList(searchKey, pageCount = 1) { return new Promise(function (resolve, reject) { let songList = [] let songInfoMap = {} let count = 0 let cbFlag = false let func = (data, status) => { if (status !== "success") { if (!cbFlag) { cbFlag = true reject("访问BeatSaver时发生错误!") } return } // 填充数据 data.docs.forEach(rawInfo => { let artist = rawInfo.metadata.songAuthorName let bpm = rawInfo.metadata.bpm let cover = rawInfo.versions[0].coverURL let song_name = "[" + rawInfo.id + "]" + rawInfo.metadata.songName let id = 80000000000 + parseInt(rawInfo.id, 36) songList.push({ artist, bpm, cover, song_name, id }) let downloadURL = rawInfo.versions[0].downloadURL let previewURL = rawInfo.versions[0].previewURL songInfoMap[id] = { downloadURL, previewURL } }) if (++count == pageCount) { cbFlag = true resolve({ songList, songInfoMap }) } } for (let i = 0; i < pageCount; i++) { $.get("https://api.beatsaver.com/search/text/" + i + "?sortOrder=Relevance&q=" + searchKey, func) } }) } /** * 从BeatSaver下载ogg文件 * @param {number} zipUrl 歌曲压缩包链接 * @param {function} onprogress 进度回调 * @returns {Promise} */ static downloadSongFile(zipUrl, onprogress) { return new Promise(function (resolve, reject) { let xhr = new XMLHttpRequest() xhr.open('GET', zipUrl, true) xhr.responseType = "blob" xhr.onprogress = onprogress xhr.onload = function () { if (this.status !== 200) { reject("http code:" + this.status) return } let blob = new Blob([this.response], { type: "application/zip" }) // 解压出ogg文件 BeatSaverUtils.getOggFromZip(blob).then(oggBlob => { resolve(oggBlob) }).catch(reject) } xhr.onerror = reject xhr.send() }) } /** * 从压缩包中提取出ogg文件 * @param {blob} zipBlob * @returns */ static async getOggFromZip(zipBlob) { let zip = await JSZip.loadAsync(zipBlob) let eggFile = undefined for (let fileName in zip.files) { if (!fileName.endsWith(".egg")) continue eggFile = zip.file(fileName) break } let rawBuffer = await eggFile.async("arraybuffer") return CipherUtils.applySongFile(rawBuffer) } /** * 获取压缩包下载链接 * @param {string} id 歌曲ID * @return {Promise} */ static getDownloadUrl(id) { return new Promise(function (resolve, reject) { $.ajax({ url: "https://api.beatsaver.com/maps/id/" + id, type: "get", success: (data) => { resolve(data.versions[0].downloadURL) }, error: (req, status, err) => { reject(req.status + " " + req.responseJSON.error) } }) }) } /** * 从压缩包中提取曲谱难度文件 * @param {Blob} zipBlob * @returns */ static async getBeatmapInfo(zipBlob) { let zip = await JSZip.loadAsync(zipBlob) // 谱面信息 let infoFile for (let fileName in zip.files) { if (fileName.toLowerCase() !== "info.dat") continue infoFile = zip.files[fileName] break } if (!infoFile) throw "请检查压缩包中是否包含info.dat文件" let rawBeatmapInfo = JSON.parse(await infoFile.async("string")) // 难度列表 let difficultyBeatmaps let diffBeatmapSets = rawBeatmapInfo._difficultyBeatmapSets for (let a in diffBeatmapSets) { let info = diffBeatmapSets[a] if (info["_beatmapCharacteristicName"] !== "Standard") continue difficultyBeatmaps = info._difficultyBeatmaps break } // 难度对应文件名 let beatmapInfo = { version: rawBeatmapInfo._version, levelAuthorName: rawBeatmapInfo._levelAuthorName, difficulties: [], files: {} } for (let index in difficultyBeatmaps) { let difficultyInfo = difficultyBeatmaps[index] let diffName = difficultyInfo._difficulty if (difficultyInfo._customData && difficultyInfo._customData._difficultyLabel) diffName = difficultyInfo._customData._difficultyLabel beatmapInfo.difficulties.push(diffName) beatmapInfo.files[diffName] = zip.files[difficultyInfo._beatmapFilename] } return beatmapInfo } } /** * XMLHttpRequest请求拦截器 */ class XHRIntercept { /** @type {XHRIntercept} */ static _self /** * 初始化 * @returns {XHRIntercept} */ constructor() { if (XHRIntercept._self) return XHRIntercept._self XHRIntercept._self = this // 修改EventListener方法 let rawXhrAddEventListener = XMLHttpRequest.prototype.addEventListener XMLHttpRequest.prototype.addEventListener = function (key, func) { if (key === "progress") { this.onprogress = func } else { rawXhrAddEventListener.apply(this, arguments) } } let rawXhrRemoveEventListener = XMLHttpRequest.prototype.removeEventListener XMLHttpRequest.prototype.removeEventListener = function (key, func) { if (key === "progress") { this.onprogress = undefined } else { rawXhrRemoveEventListener.apply(this, arguments) } } // 修改send方法 /** @type {function[]} */ this.sendIntercepts = [] this.rawXhrSend = XMLHttpRequest.prototype.send XMLHttpRequest.prototype.send = function () { XHRIntercept._self._xhrSend(this, arguments) } } /** * 添加Send拦截器 * @param {function} func */ onXhrSend(func) { if (this.sendIntercepts.indexOf(func) >= 0) return this.sendIntercepts.push(func) } /** * 删除Send拦截器 * @param {function | undefined} func */ offXhrSend(func) { if (typeof func === "function") { let index = this.sendIntercepts.indexOf(func) if (index < 0) return this.sendIntercepts.splice(index, 1) } else { this.sendIntercepts = [] } } /** * 发送拦截器 * @param {XMLHttpRequest} self * @param {IArguments} args */ _xhrSend(self, args) { let complete = () => { this.rawXhrSend.apply(self, args) } for (let i = 0; i < this.sendIntercepts.length; i++) { let flag = this.sendIntercepts[i](self, args, complete) if (flag) return } complete() } } /** * 通用工具类 */ class Utils { /** * 下载压缩包文件 * @param {number} zipUrl 歌曲压缩包链接 * @param {function | undefined} onprogress 进度回调 * @returns {Promise} */ static downloadZipFile(zipUrl, onprogress) { return new Promise(function (resolve, reject) { let xhr = new XMLHttpRequest() xhr.open('GET', zipUrl, true) xhr.responseType = "blob" xhr.onprogress = onprogress xhr.onload = function () { if (this.status !== 200) { reject("http code:" + this.status) return } resolve(new Blob([this.response], { type: "application/zip" })) } xhr.onerror = reject xhr.send() }) } } // ================================================================================ 拓展 ================================================================================ class SearchSongExtension { constructor() { this.searchFromBeatSaver = false this.songInfoMap = {} this.lastPageType = "other" } // 加载XHR拦截器 initXHRIntercept() { let _this = this let xhrIntercept = new XHRIntercept() /** * @param {XMLHttpRequest} self * @param {IArguments} args * @param {function} complete * @returns {boolean} 是否匹配 */ let onSend = function (self, args, complete) { let url = self._url if (!url || !_this.searchFromBeatSaver) return if (url.startsWith("/song/staticList")) { // 获取歌曲列表 let result = decodeURI(url).match(/songName=(\S*)&/) let key = "" if (result) key = result[1].replace("+", " ") BeatSaverUtils.searchSongList(key, 2).then(res => { self.extraSongList = res.songList _this.songInfoMap = res.songInfoMap complete() }).catch(err => { alert("搜索歌曲失败!") console.error(err) self.extraSongList = [] complete() }) self.addEventListener("readystatechange", function () { if (this.readyState !== this.DONE) return const res = JSON.parse(this.responseText) if (this.extraSongList) { res.data.data = this.extraSongList res.data.total = res.data.data.length this.extraSongList = [] } Object.defineProperty(this, 'responseText', { writable: true }); this.responseText = JSON.stringify(res) setTimeout(() => { _this.fixSongListStyle() _this.addPreviewFunc() }, 200) }); return true } else if (url.startsWith("/beatsaver/")) { let _onprogress = self.onprogress self.onprogress = undefined // 从BeatSaver下载歌曲 let result = decodeURI(url).match(/\d{1,}/) let id = parseInt(result[0]) BeatSaverUtils.downloadSongFile(_this.songInfoMap[id].downloadURL, _onprogress).then(oggBlob => { _this.songInfoMap[id].ogg = oggBlob complete() }).catch(err => { console.error(err) self.onerror(err) }) self.addEventListener("readystatechange", function () { if (this.readyState !== this.DONE) return let result = decodeURI(url).match(/\d{1,}/) let id = parseInt(result[0]) Object.defineProperty(this, 'response', { writable: true }); this.response = _this.songInfoMap[id].ogg }); return true } else if (url.startsWith("/song/ogg")) { // 获取ogg文件下载链接 let result = decodeURI(url).match(/id=(\d*)/) let id = parseInt(result[1]) if (id < 80000000000) return self.addEventListener("readystatechange", function () { if (this.readyState !== this.DONE) return const res = JSON.parse(this.responseText) res.code = 0 res.data = { link: "/beatsaver/" + id } res.msg = "success" Object.defineProperty(this, 'responseText', { writable: true }); this.responseText = JSON.stringify(res) }); complete() return true } } xhrIntercept.onXhrSend(onSend) } /** * 更新数据库 * @param {Boolean} isForce 强制转换 * @returns */ async updateDatabase(isForce) { let BLITZ_RHYTHM = await new WebDB().open("BLITZ_RHYTHM") let BLITZ_RHYTHM_files = await new WebDB().open("BLITZ_RHYTHM-files") let BLITZ_RHYTHM_official = await new WebDB().open("BLITZ_RHYTHM-official") // 获取用户名称 let userName { let rawUser = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:user") userName = JSON.parse(JSON.parse(rawUser).userInfo).name } let songInfos = [] let hasChanged = false let songsInfo // 更新歌曲信息 { let rawSongs = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:songs") songsInfo = JSON.parse(rawSongs) let songsById = JSON.parse(songsInfo.byId) for (let key in songsById) { let officialId = songsById[key].officialId if (typeof officialId != "number" || (!isForce && officialId < 80000000000)) continue let songInfo = songsById[key] songInfos.push(JSON.parse(JSON.stringify(songInfo))) songInfo.coverArtFilename = songInfo.coverArtFilename.replace("" + songInfo.officialId, songInfo.id) songInfo.songFilename = songInfo.songFilename.replace("" + songInfo.officialId, songInfo.id) songInfo.officialId = "" songInfo.mapAuthorName = userName songsById[key] = songInfo hasChanged = true } songsInfo.byId = JSON.stringify(songsById) } // 处理文件 for (let index in songInfos) { let songInfo = songInfos[index] // 复制封面和音乐文件 let cover = await BLITZ_RHYTHM_official.get("keyvaluepairs", songInfo.coverArtFilename) let song = await BLITZ_RHYTHM_official.get("keyvaluepairs", songInfo.songFilename) await BLITZ_RHYTHM_files.put("keyvaluepairs", songInfo.coverArtFilename.replace("" + songInfo.officialId, songInfo.id), cover) await BLITZ_RHYTHM_files.put("keyvaluepairs", songInfo.songFilename.replace("" + songInfo.officialId, songInfo.id), song) // 添加info记录 await BLITZ_RHYTHM_files.put("keyvaluepairs", songInfo.id + "_Info.dat", JSON.stringify({ _songFilename: "song.ogg" })) } // 保存数据 if (hasChanged) await BLITZ_RHYTHM.put("keyvaluepairs", "persist:songs", JSON.stringify(songsInfo)) BLITZ_RHYTHM.close() BLITZ_RHYTHM_files.close() BLITZ_RHYTHM_official.close() return hasChanged } /** * 修复歌单布局 */ fixSongListStyle() { let songListBox = $(".css-10szcx0")[0] songListBox.style["grid-template-columns"] = "repeat(3, minmax(0px, 1fr))" let songBox = songListBox.parentNode if ($(".css-1wfsuwr").length > 0) { songBox.style["overflow-y"] = "hidden" songBox.parentNode.style["margin-bottom"] = "" } else { songBox.style["overflow-y"] = "auto" songBox.parentNode.style["margin-bottom"] = "44px" } let itemBox = $(".css-bil4eh") for (let index = 0; index < itemBox.length; index++) itemBox[index].style.width = "230px" } /** * 在歌曲Card中添加双击预览功能 */ addPreviewFunc() { let searchBox = $(".css-1d92frk") $("#preview_tip").remove() searchBox.after("
双击歌曲可预览曲谱
") let infoViewList = $(".css-bil4eh") for (let index = 0; index < infoViewList.length; index++) { infoViewList[index].ondblclick = () => { let name = $(infoViewList[index]).find(".css-1y1rcqj")[0].innerHTML let result = name.match(/^\[(\w*)\]/) if (!result) return let previewUrl = "https://skystudioapps.com/bs-viewer/?id=" + result[1] window.open(previewUrl) } } } /** * 添加通过BeatSaver搜索歌曲的按钮 */ applySearchButton() { let boxList = $(".css-1u8wof2") // 弹窗 try { if (boxList.length == 0) throw "Box not found" let searchBoxList = boxList.find(".css-70qvj9") if (searchBoxList.length == 0) throw "item too few" // 搜索栏元素数量 if (searchBoxList[0].childNodes.length >= 3) return // 搜索栏元素数量 } catch { if (this.searchFromBeatSaver) this.searchFromBeatSaver = false return } let rawSearchBtn = $(boxList[0]).find("button")[0] // 搜索按钮 // 添加一个按钮 let searchBtn = document.createElement("button") searchBtn.className = rawSearchBtn.className searchBtn.innerHTML = "BeatSaver" $(rawSearchBtn.parentNode).append(searchBtn); // 绑定事件 rawSearchBtn.onmousedown = () => { this.searchFromBeatSaver = false $("#preview_tip").remove() } searchBtn.onmousedown = () => { this.searchFromBeatSaver = true $(rawSearchBtn).click() } } /** * 添加转换官方谱面的按钮 * @returns */ async applyConvertCiphermapButton() { let BLITZ_RHYTHM = await new WebDB().open("BLITZ_RHYTHM") try { let rawSongs = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:songs") let songsInfo = JSON.parse(rawSongs) let songsById = JSON.parse(songsInfo.byId) let songId = CipherUtils.getNowBeatmapInfo().id let officialId = songsById[songId].officialId if (!officialId) return } catch (error) { console.error(error) return } finally { BLITZ_RHYTHM.close() } let divList = $(".css-1tiz3p0") if (divList.length > 0) { if ($("#div-custom").length > 0) return let divBox = $(divList[0]).clone() divBox[0].id = "div-custom" divBox.find(".css-ujbghi")[0].innerHTML = "转换为自定义谱面" divBox.find(".css-1exyu3y")[0].innerHTML = "将官方谱面转换为自定义谱面, 以导出带有音乐文件的完整谱面压缩包。" divBox.find(".css-1y7rp4x")[0].innerText = "开始转换谱面" divBox[0].onclick = e => { // 更新歌曲信息 this.updateDatabase(true).then((hasChanged) => { if (hasChanged) setTimeout(() => { window.location.reload() }, 1000) }).catch(err => { console.log("转换谱面失败:", err) alert("转换谱面失败,请刷新再试!") }) } $(divList[0].parentNode).append(divBox) } } /** * 定时任务 1s */ handleTimer() { let url = window.location.href let pageType = url.indexOf("/edit/") >= 0 ? "edit" : "other" if (pageType === "edit") { if (pageType != this.lastPageType) { // 更新歌曲信息 this.updateDatabase().then((hasChanged) => { if (hasChanged) setTimeout(() => { window.location.reload() }, 1000) }).catch(err => { console.log("更新数据失败:", err) alert("更新歌曲信息失败,请刷新再试!") }) } this.applyConvertCiphermapButton() } else { this.applySearchButton() } this.lastPageType = pageType } async init() { // 初始化XHR拦截器 this.initXHRIntercept() // 启动定时任务 setInterval(() => { this.handleTimer() }, 1000) } } class ImportBeatmapExtension { constructor() { } /** * 在顶部菜单添加导入按钮 */ addImportButton() { if ($("#importBeatmap").length > 0) return let btnsBoxList = $(".css-4e93fo") if (btnsBoxList.length == 0) return // 按键组 let div = document.createElement("div") div.style["display"] = "flex" // 按钮模板 let btnTemp = $(btnsBoxList[0].childNodes[0]) // 按钮1 let btnImportBs = btnTemp.clone()[0] btnImportBs.id = "importBeatmap" btnImportBs.innerHTML = "导入谱面 BeatSaver链接" btnImportBs.onclick = () => { this.importFromBeatSaver() } btnImportBs.style["font-size"] = "13px" div.append(btnImportBs) // 按钮2 let btnImportZip = btnTemp.clone()[0] btnImportZip.id = "importBeatmap" btnImportZip.innerHTML = "导入谱面 BeatSaber压缩包" btnImportZip.onclick = () => { this.importFromBeatmap() } btnImportZip.style["margin-left"] = "5px" btnImportZip.style["font-size"] = "13px" div.append(btnImportZip) // 添加 btnsBoxList[0].prepend(div) } async importFromBeatSaver() { try { // 获取当前谱面信息 let nowBeatmapInfo = CipherUtils.getNowBeatmapInfo() // 获取谱面信息 let url = prompt('请输入BeatSaver铺面链接', "https://beatsaver.com/maps/" + nowBeatmapInfo.beatsaverId) if (!url) return let result = url.match(/^https:\/\/beatsaver.com\/maps\/(\S*)$/) if (!result) { alert("链接格式错误!") return } CipherUtils.showLoading() let downloadUrl = await BeatSaverUtils.getDownloadUrl(result[1]) let zipBlob = await Utils.downloadZipFile(downloadUrl) await this.importBeatmap(zipBlob, nowBeatmapInfo) } catch (err) { console.error(err) alert("出错啦:" + err) CipherUtils.hideLoading() } } /** * 通过压缩文件导入 */ importFromBeatmap() { try { // 创建上传按钮 let fileSelect = document.createElement('input') fileSelect.type = 'file' fileSelect.style.display = "none" fileSelect.accept = ".zip,.rar" fileSelect.addEventListener("change", (e) => { let files = e.target.files if (files == 0) return CipherUtils.showLoading() let file = files[0] // 获取当前谱面信息 let nowBeatmapInfo = CipherUtils.getNowBeatmapInfo() this.importBeatmap(new Blob([file]), nowBeatmapInfo).catch(err => { CipherUtils.hideLoading() console.error(err) alert("出错啦:" + err) }) }) // 点击按钮 document.body.append(fileSelect) fileSelect.click() fileSelect.remove() } catch (err) { alert("出错啦:" + err) } } /** * 从BeatSaber谱面压缩包导入信息 * @param {Blob} zipBlob * @param {{id:string, difficulty:string, beatsaverId:string}} nowBeatmapInfo */ async importBeatmap(zipBlob, nowBeatmapInfo) { let BLITZ_RHYTHM = await new WebDB().open("BLITZ_RHYTHM") let BLITZ_RHYTHM_files = await new WebDB().open("BLITZ_RHYTHM-files") try { // 获取当前谱面基本信息 let rawSongs = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:songs") let songsInfo = JSON.parse(rawSongs) let songsById = JSON.parse(songsInfo.byId) let songInfo = songsById[nowBeatmapInfo.id] let userName = "" let songDuration = -1 { let rawUser = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:user") userName = JSON.parse(JSON.parse(rawUser).userInfo).name songDuration = Math.floor(songInfo.songDuration * (songInfo.bpm / 60)) } // 获取当前谱面难度信息 let datKey = nowBeatmapInfo.id + "_" + nowBeatmapInfo.difficulty + "_Ring.dat" let datInfo = JSON.parse(await BLITZ_RHYTHM_files.get("keyvaluepairs", datKey)) if (datInfo._version !== "2.3.0") throw "插件不支持该谱面版本!可尝试重新创建谱面" let beatmapInfo = await BeatSaverUtils.getBeatmapInfo(zipBlob) if (beatmapInfo.difficulties.length == 0) throw "该谱面找不到可用的难度" // 选择导入难度 let tarDifficulty = 1 { let defaultDifficulty = "1" let promptTip = "" for (let index in beatmapInfo.difficulties) { if (index > 0) promptTip += "、" promptTip += beatmapInfo.difficulties[index] } let difficulty = "" while (true) { difficulty = prompt("请问要导入第几个难度(数字):" + promptTip, defaultDifficulty) if (!difficulty) { // Cancel CipherUtils.hideLoading() return } if (/^\d$/.test(difficulty)) { tarDifficulty = parseInt(difficulty) if (tarDifficulty > 0 && tarDifficulty <= beatmapInfo.difficulties.length) break alert("请输入准确的序号!") } else { alert("请输入准确的序号!") } } } // 开始导入 let beatmapInfoStr = await beatmapInfo.files[beatmapInfo.difficulties[tarDifficulty - 1]].async("string") let changeInfo = this.convertBeatMapInfo(beatmapInfo.version, JSON.parse(beatmapInfoStr), songDuration) datInfo._notes = changeInfo._notes datInfo._obstacles = changeInfo._obstacles await BLITZ_RHYTHM_files.put("keyvaluepairs", datKey, JSON.stringify(datInfo)) // 设置谱师署名 songInfo.mapAuthorName = userName + " & " + beatmapInfo.levelAuthorName songsInfo.byId = JSON.stringify(songsById) await BLITZ_RHYTHM.put("keyvaluepairs", "persist:songs", JSON.stringify(songsInfo)) // 导入完成 setTimeout(() => { CipherUtils.closeEditorTopMenu() window.location.reload() }, 1000) } catch (error) { throw error } finally { BLITZ_RHYTHM.close() BLITZ_RHYTHM_files.close() } } /** * 转换BeatSaber谱面信息 * @param {string} version * @param {JSON} info * @param {number} songDuration */ convertBeatMapInfo(version, rawInfo, songDuration) { let info = { _notes: [], // 音符 _obstacles: [], // 墙 } if (version.startsWith("3.")) { // 音符 for (let index in rawInfo.colorNotes) { let rawNote = rawInfo.colorNotes[index] if (songDuration > 0 && rawNote.b > songDuration) continue info._notes.push({ _time: rawNote.b, _lineIndex: rawNote.x, _lineLayer: rawNote.y, _type: rawNote.c, _cutDirection: 8, }) } } else if (version.startsWith("2.")) { // 音符 for (let index in rawInfo._notes) { let rawNote = rawInfo._notes[index] if (songDuration > 0 && rawNote._time > songDuration) continue info._notes.push({ _time: rawNote._time, _lineIndex: rawNote._lineIndex, _lineLayer: rawNote._lineLayer, _type: rawNote._type, _cutDirection: 8, }) } // 墙 for (let index in rawInfo._obstacles) { let rawNote = rawInfo._obstacles[index] if (songDuration > 0 && rawNote._time > songDuration) continue info._obstacles.push({ _time: rawNote._time, _duration: rawNote._duration, _type: rawNote._type, _lineIndex: rawNote._lineIndex, _width: rawNote._width, }) } } else { throw ("暂不支持该谱面的版本(" + version + "),请换个链接再试!") } // 因Cipher不支持长墙,所以转为多面墙 let newObstacles = [] for (let index in info._obstacles) { let baseInfo = info._obstacles[index] let startTime = baseInfo._time let endTime = baseInfo._time + baseInfo._duration let duration = baseInfo._duration baseInfo._duration = 0.04 // 头 baseInfo._time = startTime if (songDuration < 0 || (baseInfo._time + baseInfo._duration) < songDuration) newObstacles.push(JSON.parse(JSON.stringify(baseInfo))) // 中间 let count = Math.floor(duration / 1) - 2 // 至少间隔1秒 let dtime = ((endTime - 0.04) - (startTime + 0.04)) / count for (let i = 0; i < count; i++) { baseInfo._time += dtime if (songDuration < 0 || (baseInfo._time + baseInfo._duration) < songDuration) newObstacles.push(JSON.parse(JSON.stringify(baseInfo))) } // 尾 baseInfo._time = endTime - 0.04 if (songDuration < 0 || (baseInfo._time + baseInfo._duration) < songDuration) newObstacles.push(JSON.parse(JSON.stringify(baseInfo))) } info._obstacles = newObstacles return info } /** * 初始化 */ async init() { setInterval(() => { this.addImportButton() }, 1000) } } // ================================================================================ 入口 ================================================================================ (async function () { 'use strict'; // 依赖库 const sandBox = SandBox.getDocument() await SandBox.dynamicLoadJs("https://cmoyuer.gitee.io/my-resources/js/jszip.min.js") JSZip = sandBox.contentWindow.JSZip // 加载拓展 new SearchSongExtension().init() new ImportBeatmapExtension().init() })()