// ==UserScript==
// @name BlitzRhythm Editor Extra Beatmap Import
// @name:en Extra Beatmap Import
// @name:zh-CN 闪韵灵境谱面导入扩展
// @namespace cipher-editor-mod-extra-beatmap-import
// @version 1.1.0
// @description Import BeatSaber beatmap into the BlitzRhythm editor
// @description:en Import BeatSaber beatmap into the BlitzRhythm editor
// @description:zh-CN 将BeatSaber谱面导入到闪韵灵境编辑器内
// @author Moyuer
// @author:zh-CN 如梦Nya
// @source https://github.com/CMoyuer/BlitzRhythm-Editor-Mod-Loader
// @license MIT
// @run-at document-body
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @connect beatsaver.com
// @match https://cipher-editor-cn.picovr.com/*
// @match https://cipher-editor-va.picovr.com/*
// @icon https://cipher-editor-va.picovr.com/favicon.ico
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @require https://greasyfork.org/scripts/473358-jszip/code/main.js?version=1237031
// @require https://greasyfork.org/scripts/473361-xml-http-request-interceptor/code/main.js
// @require https://greasyfork.org/scripts/473362-web-indexeddb-helper/code/main.js
// @require https://greasyfork.org/scripts/474680-blitzrhythm-editor-mod-base-lib/code/main.js
// ==/UserScript==
const I18N = {
en: { // English
parameter: {
download_timeout: {
name: "Download Timeout",
description: "Timeout for download for beatmap",
}
},
methods: {},
code: {
tip: {
info_file_not_found: "Please check whether the zip file contains the info.dat file!",
input_bs_url: "Please enter the BeatSaver beatmap URL:",
url_format_error: "URL format error!",
not_support_map_ver: "Not support this beatmap version! You can try to recreate the beatmap.",
not_found_diff: "No available difficulty found for this map!",
input_diff: "Enter the difficulty level (index) you want to import:\r\n",
input_index_err: "Please enter the correct index!",
not_support_bs_ver: "This map version ({0}) is not supported yet, please change the URL and try again!",
import_map_err: "An error occurred while importing map! You can refresh and try again..."
},
button: {
import_from_url: "Import from BeatSaver URL",
import_from_file: "Import from BeatSaber zip",
}
}
},
zh: { // Chinese
parameter: {
download_timeout: {
name: "下载超时",
description: "下载谱面的超时时间",
}
},
methods: {},
code: {
tip: {
info_file_not_found: "请检查压缩包中是否包含info.dat文件",
input_bs_url: "请输入BeatSaver铺面链接",
url_format_error: "链接格式错误!",
not_support_map_ver: "插件不支持该谱面版本!可尝试重新创建谱面",
not_found_diff: "该谱面找不到可用的难度",
input_diff: "请问要导入第几个难度(数字):\r\n",
input_index_err: "请输入准确的序号!",
not_support_bs_ver: "暂不支持该谱面的版本({0}),请换个链接再试!",
import_map_err: "导入谱面时发生错误!可刷新页面重试..."
},
button: {
import_from_url: "导入谱面 BeatSaver链接",
import_from_file: "导入谱面 BeatSaber压缩包",
}
}
}
}
const PARAMETER = [
{
id: "download_timeout",
name: $t("parameter.download_timeout.name"),
description: $t("parameter.download_timeout.description"),
type: "number",
default: 60 * 1000,
min: 1000,
max: 2 * 60 * 1000
}
]
const METHODS = [
// {
// name: $t("methods.test.name"),
// description: $t("methods.test.description"),
// func: () => {
// log($t("methods.test.name"))
// }
// },
]
let pluginEnabled = false
let timerHandle = 0
function onEnabled() {
pluginEnabled = true
let timerFunc = () => {
if (!pluginEnabled) return
CipherUtils.waitLoading().then(() => {
tick()
}).catch(err => {
console.error(err)
}).finally(() => {
timerHandle = setTimeout(timerFunc, 250)
})
}
timerFunc()
}
function onDisabled() {
if (timerHandle > 0) {
clearTimeout(timerHandle)
timerHandle = 0
}
pluginEnabled = false
}
function onParameterValueChanged(id, val) {
log("onParameterValueChanged", id, val)
// log("debug", $p(id))
}
// =====================================================================================
/**
* 闪韵灵境工具类
*/
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 {Blob}
*/
static addSongVerificationCode(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" })
}
/**
* 获取页面参数
* @returns
*/
static getPageParmater() {
let url = window.location.href
let matchs = url.match(/\?import=(\w{1,})@(\w{1,})@(\w{1,})/)
if (!matchs) return
return {
event: "import",
source: matchs[1],
id: matchs[2],
mode: matchs[3],
}
}
/**
* 关闭编辑器顶部菜单
*/
static closeEditorTopMenu() {
$(".css-1k12r02").click()
}
/**
* 显示Loading
*/
static showLoading() {
let maskBox = $('<div style="position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,0.5);z-index:9999;" id="loading"></div>')
maskBox.append('<span style="display: block;position: absolute;width:40px;height:40px;left: calc(50vw - 20px);top: calc(50vh - 20px);"><svg viewBox="22 22 44 44"><circle cx="44" cy="44" r="20.2" fill="none" stroke-width="3.6" class="css-14891ef"></circle></svg></span>')
$("#root").append(maskBox)
}
/**
* 隐藏Loading
*/
static hideLoading() {
$("#loading").remove()
}
/**
* 网页弹窗
*/
static showIframe(src) {
this.hideIframe()
let maskBox = $('<div style="position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,0.5);z-index:9999;" id="iframe_box"></div>')
maskBox.click(this.hideIframe)
maskBox.append('<iframe src="' + src + '" style="width:calc(100vw - 400px);height:calc(100vh - 200px);position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);border-radius:12px;"></iframe>')
$("#root").append(maskBox)
}
/**
* 隐藏Loading
*/
static hideIframe() {
$("#iframe_box").remove()
}
/**
* 等待Loading结束
* @returns
*/
static waitLoading() {
return new Promise((resolve, reject) => {
let handle = setInterval((() => {
let loadingList = $(".css-c81162")
if (loadingList && loadingList.length > 0) return
clearInterval(handle)
resolve()
}), 500)
})
}
}
/**
* 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 => {
// 填充数据
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++) {
Utils.ajax({
url: "https://api.beatsaver.com/search/text/" + i + "?sortOrder=Relevance&q=" + searchKey,
method: "GET",
responseType: "json"
}).then(func)
}
})
}
/**
* 从BeatSaver下载ogg文件
* @param {number} zipUrl 歌曲压缩包链接
* @param {function} onprogress 进度回调
* @returns {Promise<blob, any>}
*/
static async downloadSongFile(zipUrl, onprogress) {
let blob = await Utils.downloadZipFile(zipUrl, onprogress)
// 解压出ogg文件
return await BeatSaverUtils.getOggFromZip(blob)
}
/**
* 从压缩包中提取出ogg文件
* @param {blob} zipBlob
* @param {boolean | undefined} verification
* @returns
*/
static async getOggFromZip(zipBlob, verification = true) {
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
}
if (verification) {
let rawBuffer = await eggFile.async("arraybuffer")
return CipherUtils.addSongVerificationCode(rawBuffer)
} else {
return await eggFile.async("blob")
}
}
/**
* 获取压缩包下载链接
* @param {string} id 歌曲ID
* @return {Promise}
*/
static getDownloadUrl(id) {
return new Promise(function (resolve, reject) {
Utils.ajax({
url: "https://api.beatsaver.com/maps/id/" + id,
method: "GET",
responseType: "json",
}).then(data => {
resolve(data.versions[0].downloadURL)
}).catch(err => {
reject(err)
})
})
}
/**
* 从压缩包中提取曲谱难度文件
* @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 $t("code.tip.info_file_not_found")
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 = {
raw: rawBeatmapInfo,
version: rawBeatmapInfo._version,
levelAuthorName: rawBeatmapInfo._levelAuthorName,
difficulties: []
}
for (let index in difficultyBeatmaps) {
let difficultyInfo = difficultyBeatmaps[index]
let difficulty = difficultyInfo._difficulty
let difficultyLabel = ""
if (difficultyInfo._customData && difficultyInfo._customData._difficultyLabel)
difficultyLabel = difficultyInfo._customData._difficultyLabel
beatmapInfo.difficulties.push({
difficulty,
difficultyLabel,
file: zip.files[difficultyInfo._beatmapFilename]
})
}
return beatmapInfo
}
}
/**
* 通用工具类
*/
class Utils {
/**
* 下载压缩包文件
* @param {number} zipUrl 歌曲压缩包链接
* @param {function | undefined} onprogress 进度回调
* @returns {Promise}
*/
static downloadZipFile(zipUrl, onprogress) {
return new Promise(function (resolve, reject) {
Utils.ajax({
url: zipUrl,
method: "GET",
responseType: "blob",
onprogress,
}).then(data => {
resolve(new Blob([data], { type: "application/zip" }))
}).catch(reject)
})
}
/**
* 获取音乐文件时长
* @param {Blob} blob
*/
static getOggDuration(blob) {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.onerror = () => {
reject(reader.error)
}
reader.onload = (e) => {
let audio = document.createElement('audio')
audio.addEventListener("loadedmetadata", () => {
resolve(audio.duration)
$(audio).remove()
})
audio.addEventListener('error', () => {
reject(audio.error)
$(audio).remove()
})
audio.src = e.target.result
}
reader.readAsDataURL(new File([blob], "song.ogg", { type: "audio/ogg" }))
})
}
/**
* 异步发起网络请求
* @param {object} config
* @returns
*/
static ajax(config) {
return new Promise((resolve, reject) => {
config.onload = res => {
if (res.status >= 200 && res.status < 300) {
try {
resolve(JSON.parse(res.response))
} catch {
resolve(res.response)
}
}
else {
reject("HTTP Code: " + res.status)
}
}
config.onerror = err => {
reject(err)
}
GM_xmlhttpRequest(config)
})
}
}
// =====================================================================================
/**
* 在顶部菜单添加导入按钮
*/
function 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 = $t("code.button.import_from_url")
btnImportBs.onclick = importFromBeatSaver
btnImportBs.style["font-size"] = "13px"
div.append(btnImportBs)
// 按钮2
let btnImportZip = btnTemp.clone()[0]
btnImportZip.id = "importBeatmap"
btnImportZip.innerHTML = $t("code.button.import_from_file")
btnImportZip.onclick = importFromBeatmapZip
btnImportZip.style["margin-left"] = "5px"
btnImportZip.style["font-size"] = "13px"
div.append(btnImportZip)
// 添加
btnsBoxList[0].prepend(div)
}
async function importFromBeatSaver() {
try {
// 获取当前谱面信息
let nowBeatmapInfo = CipherUtils.getNowBeatmapInfo()
// 获取谱面信息
let url = prompt($t("code.tip.input_bs_url"), "https://beatsaver.com/maps/" + nowBeatmapInfo.beatsaverId)
if (!url) return
let result = url.match(/^https:\/\/beatsaver.com\/maps\/(\S*)$/)
if (!result) {
alert($t("code.tip.url_format_error"))
return
}
CipherUtils.showLoading()
let downloadUrl = await BeatSaverUtils.getDownloadUrl(result[1])
let zipBlob = await Utils.downloadZipFile(downloadUrl)
await importBeatmap(zipBlob, nowBeatmapInfo)
} catch (err) {
console.error(err)
alert("Import Failed: " + err)
CipherUtils.hideLoading()
}
}
/**
* 通过压缩文件导入
*/
function importFromBeatmapZip() {
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()
importBeatmap(new Blob([file]), nowBeatmapInfo).catch(err => {
CipherUtils.hideLoading()
console.error(err)
alert("Import Failed: " + err)
})
})
// 点击按钮
document.body.append(fileSelect)
fileSelect.click()
fileSelect.remove()
} catch (err) {
alert("Import Failed: " + err)
}
}
/**
* 从BeatSaber谱面压缩包导入信息
* @param {Blob} zipBlob
* @param {{id:string, difficulty:string, beatsaverId:string}} nowBeatmapInfo
* @param {number} targetDifficulty
*/
async function importBeatmap(zipBlob, nowBeatmapInfo, targetDifficulty) {
let BLITZ_RHYTHM = await WebDB.open("BLITZ_RHYTHM")
let BLITZ_RHYTHM_files = await 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 $t("code.tip.not_support_map_ver")
let beatmapInfo = await BeatSaverUtils.getBeatmapInfo(zipBlob)
if (beatmapInfo.difficulties.length == 0)
throw $t("code.tip.not_found_diff")
// 选择导入难度
let tarDifficulty = 1
if (targetDifficulty >= 1 && targetDifficulty <= beatmapInfo.difficulties.length) {
tarDifficulty = targetDifficulty
} else {
let defaultDifficulty = "1"
let promptTip = ""
console.log(beatmapInfo.difficulties)
for (let index in beatmapInfo.difficulties) {
if (index > 0) promptTip += "\r\n"
promptTip += (parseInt(index) + 1) + "." + beatmapInfo.difficulties[index].difficulty
}
let difficulty = ""
while (true) {
difficulty = prompt($t("code.tip.input_diff") + promptTip, defaultDifficulty)
if (!difficulty) {
// Cancel
CipherUtils.hideLoading()
return
}
if (/^\d$/.test(difficulty)) {
tarDifficulty = parseInt(difficulty)
if (tarDifficulty > 0 && tarDifficulty <= beatmapInfo.difficulties.length) break
}
alert($t("code.tip.input_index_err"))
}
}
// 开始导入
let difficultyInfo = JSON.parse(await beatmapInfo.difficulties[tarDifficulty - 1].file.async("string"))
let changeInfo = convertBeatMapInfo(difficultyInfo.version || difficultyInfo._version, difficultyInfo, 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
*/
function 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 // 去除歌曲结束后的音符
if (rawNote._customData && rawNote._customData._track === "choarrowspazz") continue // 去除某个mod的前级音符
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 $t("code.tip.not_support_bs_ver", 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 function ApplyPageParmater() {
let BLITZ_RHYTHM = await WebDB.open("BLITZ_RHYTHM")
let BLITZ_RHYTHM_files = await WebDB.open("BLITZ_RHYTHM-files")
try {
let pagePar = CipherUtils.getPageParmater()
if (!pagePar) return
if (pagePar.event === "import") {
if (pagePar.source === "beatsaver") {
CipherUtils.showLoading()
if (pagePar.mode !== "song" && pagePar.mode !== "all") return
let zipUrl = await BeatSaverUtils.getDownloadUrl(pagePar.id)
let zipBlob = await Utils.downloadZipFile(zipUrl)
let beatsaverInfo = await BeatSaverUtils.getBeatmapInfo(zipBlob)
// console.log(beatsaverInfo)
let oggBlob = await BeatSaverUtils.getOggFromZip(zipBlob, false)
let zip = await JSZip.loadAsync(zipBlob)
let coverBlob = await zip.file(beatsaverInfo.raw._coverImageFilename).async("blob")
let coverType = beatsaverInfo.raw._coverImageFilename.match(/.(\w{1,})$/)[1]
let rawUserStr = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:user")
let userName = JSON.parse(JSON.parse(rawUserStr).userInfo).name
// Date to ID
let date = new Date()
let dateArray = [date.getFullYear().toString().padStart(4, "0"), (date.getMonth() + 1).toString().padStart(2, "0"), date.getDate().toString().padStart(2, "0"),
date.getHours().toString().padStart(2, "0"), date.getMinutes().toString().padStart(2, "0"),
date.getSeconds().toString().padStart(2, "0") + date.getMilliseconds().toString().padStart(3, "0") + (Math.floor(Math.random() * Math.pow(10, 11))).toString().padStart(11, "0")]
let id = dateArray.join("_")
let selectedDifficulty = "Easy"
// Apply Info
let cipherMapInfo = {
id,
officialId: "",
name: "[" + pagePar.id + "]" + beatsaverInfo.raw._songName,
// subName: beatsaverInfo.raw._songSubName,
artistName: beatsaverInfo.raw._songAuthorName,
mapAuthorName: userName + ((pagePar.mode === "all") ? (" & " + beatsaverInfo.raw._levelAuthorName) : ""),
bpm: beatsaverInfo.raw._beatsPerMinute,
offset: beatsaverInfo.raw._songTimeOffset,
// swingAmount: 0,
// swingPeriod: 0.5,
previewStartTime: beatsaverInfo.raw._previewStartTime,
previewDuration: beatsaverInfo.raw._previewDuration,
songFilename: id + "_song.ogg",
songDuration: await Utils.getOggDuration(oggBlob),
coverArtFilename: id + "_cover." + coverType,
environment: "DefaultEnvironment",
selectedDifficulty,
difficultiesRingById: {
Easy: {
id: "Easy",
noteJumpSpeed: 10,
calories: 3000,
startBeatOffset: 0,
customLabel: "",
ringNoteJumpSpeed: 10,
ringNoteStartBeatOffset: 0
},
Normal: {
id: "Normal",
noteJumpSpeed: 10,
calories: 4000,
startBeatOffset: 0,
customLabel: "",
ringNoteJumpSpeed: 10,
ringNoteStartBeatOffset: 0
},
Hard: {
id: "Hard",
noteJumpSpeed: 12,
calories: 4500,
startBeatOffset: 0,
customLabel: "",
ringNoteJumpSpeed: 12,
ringNoteStartBeatOffset: 0
},
Expert: {
id: "Expert",
noteJumpSpeed: 15,
calories: 5000,
startBeatOffset: 0,
customLabel: "",
ringNoteJumpSpeed: 15,
ringNoteStartBeatOffset: 0
}
},
createdAt: Date.now(),
lastOpenedAt: Date.now(),
// demo: false,
modSettings: {
customColors: {
isEnabled: false,
colorLeft: "#f21212",
colorLeftOverdrive: 0,
colorRight: "#006cff",
colorRightOverdrive: 0,
envColorLeft: "#FFDD55",
envColorLeftOverdrive: 0,
envColorRight: "#00FFCC",
envColorRightOverdrive: 0,
obstacleColor: "#f21212",
obstacleColorOverdrive: 0,
obstacle2Color: "#d500f9",
obstacleColorOverdrive2: 0
},
mappingExtensions: {
isEnabled: false,
numRows: 3,
numCols: 4,
colWidth: 1,
rowHeight: 1
}
},
// enabledFastWalls: false,
// enabledLightshow: false,
}
// Apply Difficulty Info
if (pagePar.mode === "song") {
delete cipherMapInfo.difficultiesRingById.Normal
delete cipherMapInfo.difficultiesRingById.Hard
delete cipherMapInfo.difficultiesRingById.Expert
} else if (pagePar.mode === "all") {
let tarDiffList = ["Easy", "Normal", "Hard", "Expert", "ExpertPlus"]
let diffMap = {}
for (let i = beatsaverInfo.difficulties.length - 1; i >= 0; i--) {
let difficultyInfo = beatsaverInfo.difficulties[i]
let difficulty = difficultyInfo.difficulty
if (difficulty === "ExpertPlus") difficulty = "Expert"
cipherMapInfo.selectedDifficulty = selectedDifficulty = difficulty
if (!diffMap.hasOwnProperty(difficulty)) {
diffMap[difficulty] = beatsaverInfo.difficulties[i].file
} else {
let index = tarDiffList.indexOf(difficulty) - 1
if (index < 0) continue
diffMap[tarDiffList[index]] = beatsaverInfo.difficulties[i].file
}
}
let rawDiffList = ["Easy", "Normal", "Hard", "Expert"]
for (let i = 0; i < rawDiffList.length; i++) {
let difficulty = rawDiffList[i]
if (!diffMap.hasOwnProperty(difficulty))
delete cipherMapInfo.difficultiesRingById[difficulty]
}
for (let difficulty in diffMap) {
let datKey = id + "_" + difficulty + "_Ring.dat"
let diffDatInfo = JSON.parse("{\"_version\":\"2.3.0\",\"_events\":[],\"_notes\":[],\"_ringNotes\":[],\"_obstacles\":[],\"_customData\":{\"_bookmarks\":[]}}")
let difficultyInfo = JSON.parse(await diffMap[difficulty].async("string"))
let changeInfo = convertBeatMapInfo(difficultyInfo.version || difficultyInfo._version, difficultyInfo, Math.floor(cipherMapInfo.songDuration * (cipherMapInfo.bpm / 60)))
diffDatInfo._notes = changeInfo._notes
diffDatInfo._obstacles = changeInfo._obstacles
await BLITZ_RHYTHM_files.put("keyvaluepairs", datKey, JSON.stringify(diffDatInfo))
}
}
// Create Asset File
await BLITZ_RHYTHM_files.put("keyvaluepairs", id + "_song.ogg", oggBlob)
await BLITZ_RHYTHM_files.put("keyvaluepairs", id + "_cover." + coverType, coverBlob)
// Create Cipher Map
let songsStr = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:songs")
let songsJson = JSON.parse(songsStr)
let songPairs = JSON.parse(songsJson.byId)
songPairs[id] = cipherMapInfo
songsJson.byId = JSON.stringify(songPairs)
await BLITZ_RHYTHM.put("keyvaluepairs", "persist:songs", JSON.stringify(songsJson))
// console.log(cipherMapInfo)
setTimeout(() => {
location.href = "https://cipher-editor-cn.picovr.com/edit/notes?id=" + id + "&difficulty=" + selectedDifficulty + "&mode=Ring"
}, 200)
return // Dont hide loading
}
}
CipherUtils.hideLoading()
} catch (e) {
CipherUtils.hideLoading()
throw e
} finally {
BLITZ_RHYTHM.close()
BLITZ_RHYTHM_files.close()
}
}
/**
* 定时任务 1s
*/
function tick() {
addImportButton()
}
(function () {
'use strict'
// Import beatmap via url parameter
ApplyPageParmater().catch(res => {
console.error(res)
alert($t("code.tip.import_map_err"))
})
})()