// ==UserScript== // @name Plugins in Old Taiko Web // @namespace plugins-in-old-taiko-web // @icon https://gitlab.com/uploads/-/system/project/avatar/36006078/taiko-web-plugins.png?width=64 // @version 2023.1.20 // @author Katie Frogs // @description Implements the plugin interface in older versions of taiko-web // @homepage https://github.com/KatieFrogs/taiko-web-plugins // @supportURL https://github.com/KatieFrogs/taiko-web-plugins/issues // @match https://*.lv5.ac/ // @grant none // @run-at document-start // ==/UserScript== var pluginsTranslations = { title: { ja: "プラグイン", en: "Plugins", ko: "플러그인" }, unloadAll: { ja: "すべて無効にする", en: "Unload All", ko: "모두 해제" }, warning: { ja: "%sを読み込もうとしています。プラグインは信頼できる場合のみ読み込むようにしてください。続行しますか?", en: "You are about to load %s. Plugins should only be loaded if you trust them. Continue?", ko: "%s을 로드하려고 합니다. 신뢰할 수 있는 플러그인만 로드하시기 바랍니다. 계속할까요?" }, plugin: { ja: { one: "%sつのプラグイン", other: "%sつのプラグイン" }, en: { one: "%s plugin", other: "%s plugins" }, ko: { one: "%s 플러그인", other: "%s 플러그인들" } }, author: { ja: "作成者:%s", en: "By %s", ko: "제작자:%s" }, version: { ja: "Ver. %s", en: "Version %s", ko: "버전 %s" }, browse: { ja: "参照する…", en: "Browse...", cn: "浏览…", tw: "開啟檔案…", ko: "찾아보기…" }, noPlugins: { ja: null, en: "No .taikoweb.js plugin files have been found in the provided file list.", ko: "주어진 파일 리스트에서 .taikoweb.js 플러그인 파일들을 발견할 수 없습니다." } } var pluginsStrings = {} var languageList = ["ja", "en", "cn", "tw", "ko"] function separateStrings(){ for(var j in languageList){ var lang = languageList[j] pluginsStrings[lang] = { id: lang } var str = pluginsStrings[lang] var translateObj = function(obj, name, str){ if("en" in obj){ for(var i in obj){ str[name] = obj[lang] || obj.en } }else if(obj){ str[name] = {} for(var i in obj){ translateObj(obj[i], i, str[name]) } } } for(var i in pluginsTranslations){ translateObj(pluginsTranslations[i], i, str) } } } separateStrings() function pluginsStr(lang){ if(!lang){ lang = strings.id } return pluginsStrings[lang] || allStrings[lang].plugins || pluginsStrings.en } class Plugins{ constructor(...args){ this.init(...args) } init(){ this.allPlugins = [] this.pluginMap = {} this.hashes = [] this.startOrder = [] } add(script, options){ options = options || {} var hash = md5.base64(script.toString()) var isUrl = typeof script === "string" && !options.raw if(isUrl){ hash = "url " + hash }else if(typeof script !== "string"){ hash = "class " + hash } var name = options.name if(!name && isUrl){ name = script var index = name.lastIndexOf("?") if(index !== -1){ name = name.slice(0, index) } var index = name.lastIndexOf("/") if(index !== -1){ name = name.slice(index + 1) } if(name.endsWith(".taikoweb.js")){ name = name.slice(0, -".taikoweb.js".length) }else if(name.endsWith(".js")){ name = name.slice(0, -".js".length) } } name = name || "plugin" if(this.hashes.indexOf(hash) !== -1){ console.warn("Skip adding an already addded plugin: " + name) return } var baseName = name for(var i = 2; name in this.pluginMap; i++){ name = baseName + i.toString() } var plugin = new PluginLoader(script, name, hash, options.raw) plugin.hide = !!options.hide this.allPlugins.push({ name: name, plugin: plugin }) this.pluginMap[name] = plugin this.hashes.push(hash) return plugin } remove(name){ if(name in this.pluginMap){ var hash = this.pluginMap[name].hash if(hash){ var index = this.hashes.indexOf(hash) if(index !== -1){ this.hashes.splice(index, 1) } } this.unload(name) } var index = this.allPlugins.findIndex(obj => obj.name === name) if(index !== -1){ this.allPlugins.splice(index, 1) } var index = this.startOrder.indexOf(name) if(index !== -1){ this.startOrder.splice(index, 1) } delete this.pluginMap[name] } load(name){ return this.pluginMap[name].load() } loadAll(){ for(var i = 0; i < this.allPlugins.length; i++){ this.allPlugins[i].plugin.load() } } start(name){ return this.pluginMap[name].start() } startAll(){ for(var i = 0; i < this.allPlugins.length; i++){ this.allPlugins[i].plugin.start() } } stop(name){ return this.pluginMap[name].stop() } stopAll(){ for(var i = this.startOrder.length; i--;){ this.pluginMap[this.startOrder[i]].stop() } } unload(name){ return this.pluginMap[name].unload() } unloadAll(){ for(var i = this.startOrder.length; i--;){ this.pluginMap[this.startOrder[i]].unload() } for(var i = this.allPlugins.length; i--;){ this.allPlugins[i].plugin.unload() } } unloadImported(){ for(var i = this.startOrder.length; i--;){ var plugin = this.pluginMap[this.startOrder[i]] if(plugin.imported){ plugin.unload() } } for(var i = this.allPlugins.length; i--;){ var obj = this.allPlugins[i] if(obj.plugin.imported){ obj.plugin.unload() } } } strFromFunc(func){ var output = func.toString() return output.slice(output.indexOf("{") + 1, output.lastIndexOf("}")) } argsFromFunc(func){ var output = func.toString() output = output.slice(0, output.indexOf("{")) output = output.slice(output.indexOf("(") + 1, output.lastIndexOf(")")) return output.split(",").map(str => str.trim()).filter(Boolean) } insertBefore(input, insertedText, searchString){ var index = input.indexOf(searchString) if(index === -1){ throw new Error("searchString not found: " + searchString) } return input.slice(0, index) + insertedText + input.slice(index) } insertAfter(input, searchString, insertedText){ var index = input.indexOf(searchString) if(index === -1){ throw new Error("searchString not found: " + searchString) } var length = searchString.length return input.slice(0, index + length) + insertedText + input.slice(index + length) } strReplace(input, searchString, insertedText, repeat=1){ var position = 0 for(var i = 0; i < repeat; i++){ var index = input.indexOf(searchString, position) if(index === -1){ if(repeat === Infinity){ break }else{ throw new Error("searchString not found: " + searchString) } } input = input.slice(0, index) + insertedText + input.slice(index + searchString.length) position = index + insertedText.length } return input } isObject(input){ return input && typeof input === "object" && input.constructor === Object } deepMerge(target, ...sources){ sources.forEach(source => { if(this.isObject(target) && this.isObject(source)){ for(var i in source){ if(this.isObject(source[i])){ if(!target[i]){ target[i] = {} } this.deepMerge(target[i], source[i]) }else if(source[i]){ target[i] = source[i] } } } }) return target } arrayDel(array, item){ var index = array.indexOf(item) if(index !== -1){ array.splice(index, 1) return true } return false } hasSettings(){ for(var i = 0; i < this.allPlugins.length; i++){ var plugin = this.allPlugins[i].plugin if(plugin.loaded && (!plugin.hide || plugin.settings())){ return true } } return false } getSettings(){ var items = [] for(var i = 0; i < this.allPlugins.length; i++){ var obj = this.allPlugins[i] let plugin = obj.plugin if(!plugin.loaded){ continue } if(!plugin.hide){ let description let description_lang var module = plugin.module if(module){ description = [ module.description, module.author ? pluginsStr().author.replace("%s", module.author) : null, module.version ? pluginsStr().version.replace("%s", module.version) : null ].filter(Boolean).join("\n") description_lang = {} languageList.forEach(lang => { description_lang[lang] = [ this.getLocalTitle(module.description, module.description_lang, lang), module.author ? pluginsStr(lang).author.replace("%s", module.author) : null, module.version ? pluginsStr(lang).version.replace("%s", module.version) : null ].filter(Boolean).join("\n") }) } var name = module && module.name || obj.name var name_lang = module && module.name_lang items.push({ name: name, name_lang: name_lang, description: description, description_lang: description_lang, type: "toggle", default: true, getItem: () => plugin.started, setItem: value => { if(plugin.name in this.pluginMap){ if(plugin.started && !value){ this.stop(plugin.name) }else if(!plugin.started && value){ this.start(plugin.name) } } } }) } var settings = plugin.settings() if(settings){ settings.forEach(setting => { if(!setting.name){ setting.name = name if(!setting.name_lang){ setting.name_lang = name_lang } } if(typeof setting.getItem !== "function"){ setting.getItem = () => {} } if(typeof setting.setItem !== "function"){ setting.setItem = () => {} } if(!("indent" in setting) && !plugin.hide){ setting.indent = 1 } items.push(setting) }) } } return items } getLocalTitle(title, titleLang, lang){ if(titleLang){ for(var id in titleLang){ if(id === (lang || strings.id) && titleLang[id]){ return titleLang[id] } } } return title } } class PluginLoader{ constructor(...args){ this.init(...args) } init(script, name, hash, raw){ this.name = name this.hash = hash if(typeof script === "string"){ if(raw){ this.url = URL.createObjectURL(new Blob([script], { type: "application/javascript" })) }else{ this.url = script } }else{ this.class = script } } load(loadErrors){ if(this.loaded){ return Promise.resolve() }else if(!this.url && !this.class){ if(loadErrors){ return Promise.reject() }else{ return Promise.resolve() } }else{ return (this.url ? import(this.url) : Promise.resolve({ default: this.class })).then(module => { if(this.url){ URL.revokeObjectURL(this.url) delete this.url }else{ delete this.class } this.loaded = true try{ this.module = new module.default() }catch(e){ this.error() var error = new Error() error.stack = "Error initializing plugin: " + this.name + "\n" + e.stack if(loadErrors){ return Promise.reject(error) }else{ console.error(error) return Promise.resolve() } } var output try{ if(this.module.beforeLoad){ this.module.beforeLoad(this) } if(this.module.load){ output = this.module.load(this) } }catch(e){ this.error() var error = new Error() error.stack = "Error in plugin load: " + this.name + "\n" + e.stack if(loadErrors){ return Promise.reject(error) }else{ console.error(error) return Promise.resolve() } } if(typeof output === "object" && output.constructor === Promise){ return output.catch(e => { this.error() var error = new Error() error.stack = "Error in plugin load promise: " + this.name + (e ? "\n" + e.stack : "") if(loadErrors){ return Promise.reject(error) }else{ console.error(error) return Promise.resolve() } }) } }, e => { this.error() plugins.remove(this.name) if(e.name === "SyntaxError"){ var error = new SyntaxError() error.stack = "Error in plugin syntax: " + this.name + "\n" + e.stack }else{ var error = e } if(loadErrors){ return Promise.reject(error) }else{ console.error(error) return Promise.resolve() } }) } } start(orderChange, startErrors){ if(!orderChange){ plugins.startOrder.push(this.name) } return this.load().then(() => { if(!this.started && this.module){ this.started = true try{ if(this.module.beforeStart){ this.module.beforeStart() } if(this.module.start){ this.module.start() } }catch(e){ this.error() var error = new Error() error.stack = "Error in plugin start: " + this.name + "\n" + e.stack if(startErrors){ return Promise.reject(error) }else{ console.error(error) return Promise.resolve() } } } }) } stop(orderChange, noError){ if(this.loaded && this.started){ if(!orderChange){ var stopIndex = plugins.startOrder.indexOf(this.name) if(stopIndex !== -1){ plugins.startOrder.splice(stopIndex, 1) for(var i = plugins.startOrder.length; i-- > stopIndex;){ plugins.pluginMap[plugins.startOrder[i]].stop(true) } } } this.started = false try{ if(this.module.beforeStop){ this.module.beforeStop() } if(this.module.stop){ this.module.stop() } }catch(e){ var error = new Error() error.stack = "Error in plugin stop: " + this.name + "\n" + e.stack console.error(error) if(!noError){ this.error() } } if(!orderChange && stopIndex !== -1){ for(var i = stopIndex; i < plugins.startOrder.length; i++){ plugins.pluginMap[plugins.startOrder[i]].start(true) } } } } unload(error){ if(this.loaded){ if(this.started){ this.stop(false, error) } this.loaded = false plugins.remove(this.name) if(this.module){ try{ if(this.module.beforeUnload){ this.module.beforeUnload() } if(this.module.unload){ this.module.unload() } }catch(e){ var error = new Error() error.stack = "Error in plugin unload: " + this.name + "\n" + e.stack console.error(error) } delete this.module } } } error(){ if(this.module && this.module.error){ try{ this.module.error() }catch(e){ var error = new Error() error.stack = "Error in plugin error: " + this.name + "\n" + e.stack console.error(error) } } this.unload(true) } settings(){ if(this.module && this.module.settings){ try{ var settings = this.module.settings() }catch(e){ console.error(e) this.error() return } if(Array.isArray(settings)){ return settings } } } } class EditValue{ constructor(...args){ this.init(...args) } init(parent, name){ if(name){ if(!parent){ throw new Error("Parent is not defined") } this.name = [parent, name] this.delete = !(name in parent) }else{ this.original = parent } } load(callback){ this.loadCallback = callback return this } start(){ if(this.name){ this.original = this.name[0][this.name[1]] } try{ var output = this.loadCallback(this.original) }catch(e){ console.error(this.loadCallback) var error = new Error() error.stack = "Error editing the value of " + this.getName() + "\n" + e.stack throw error } if(typeof output === "undefined"){ console.error(this.loadCallback) throw new Error("Error editing the value of " + this.getName() + ": A value is expected to be returned") } if(this.name){ this.name[0][this.name[1]] = output } return output } stop(){ if(this.name){ if(this.delete){ delete this.name[0][this.name[1]] }else{ this.name[0][this.name[1]] = this.original } } return this.original } getName(){ var name = "unknown" try{ if(this.name){ var name = ( typeof this.name[0] === "function" && this.name[0].name || ( typeof this.name[0] === "object" && typeof this.name[0].constructor === "function" && ( this.name[0] instanceof this.name[0].constructor ? (() => { var consName = this.name[0].constructor.name || "" return consName.slice(0, 1).toLowerCase() + consName.slice(1) })() : this.name[0].constructor.name + ".prototype" ) ) || name ) + (this.name[1] ? "." + this.name[1] : "") } }catch(e){ name = "error" } return name } unload(){ delete this.name delete this.original delete this.loadCallback } } class EditFunction extends EditValue{ start(){ if(this.name){ this.original = this.name[0][this.name[1]] } if(typeof this.original !== "function"){ console.error(this.loadCallback) var error = new Error() error.stack = "Error editing the function value of " + this.getName() + ": Original value is not a function" throw error } var args = plugins.argsFromFunc(this.original) try{ var output = this.loadCallback(plugins.strFromFunc(this.original), args) }catch(e){ console.error(this.loadCallback) var error = new Error() error.stack = "Error editing the function value of " + this.getName() + "\n" + e.stack throw error } if(typeof output === "undefined"){ console.error(this.loadCallback) throw new Error("Error editing the function value of " + this.getName() + ": A value is expected to be returned") } try{ var output = Function(...args, output) }catch(e){ console.error(this.loadCallback) var error = new SyntaxError() var blob = new Blob([output], { type: "application/javascript" }) var url = URL.createObjectURL(blob) error.stack = "Error editing the function value of " + this.getName() + ": Could not evaluate string, check the full string for errors: " + url + "\n" + e.stack setTimeout(() => { URL.revokeObjectURL(url) }, 5 * 60 * 1000) throw error } if(this.name){ this.name[0][this.name[1]] = output } return output } } class Patch{ constructor(...args){ this.init(...args) } init(){ this.edits = [] this.addedLanguages = [] } addEdits(...args){ args.forEach(arg => this.edits.push(arg)) } addLanguage(lang, forceSet, fallback="en"){ if(fallback){ lang = plugins.deepMerge({}, allStrings[fallback], lang) } this.addedLanguages.push({ lang: lang, forceSet: forceSet }) } beforeStart(){ this.edits.forEach(edit => edit.start()) this.addedLanguages.forEach(obj => { settings.addLang(obj.lang, obj.forceSet) }) } beforeStop(){ for(var i = this.edits.length; i--;){ this.edits[i].stop() } for(var i = this.addedLanguages.length; i--;){ settings.removeLang(this.addedLanguages[i].lang) } } beforeUnload(){ this.edits.forEach(edit => edit.unload()) } log(...args){ var name = this.name || "Plugin" console.log( "%c[" + name + "]", "font-weight: bold;", ...args ) } } function readFile(file, arrayBuffer, encoding){ var reader = new FileReader() var promise = pageEvents.load(reader).then(event => event.target.result) reader[arrayBuffer ? "readAsArrayBuffer" : "readAsText"](file, encoding) return promise } class RemoteFile{ constructor(...args){ this.init(...args) } init(url){ this.url = url try{ this.path = new URL(url).pathname }catch(e){ this.path = url } if(this.path.startsWith("/")){ this.path = this.path.slice(1) } if(this.url.startsWith("data:")){ this.name = "datauri" if(this.url.startsWith("data:audio/ogg")){ this.name += ".ogg" } }else{ this.name = this.path var index = this.name.lastIndexOf("/") if(index !== -1){ this.name = this.name.slice(index + 1) } } } arrayBuffer(){ return loader.ajax(this.url, request => { request.responseType = "arraybuffer" }) } read(encoding){ if(encoding){ return this.blob().then(blob => readFile(blob, false, encoding)) }else{ return loader.ajax(this.url) } } blob(){ return loader.ajax(this.url, request => { request.responseType = "blob" }) } } class LocalFile{ constructor(...args){ this.init(...args) } init(file, path){ this.file = file this.path = path || file.webkitRelativePath || file.name this.webkitRelativePath = this.path this.url = this.path this.name = file.name } arrayBuffer(){ return readFile(this.file, true) } read(encoding){ return readFile(this.file, false, encoding) } blob(){ return Promise.resolve(this.file) } } class CustomSongs2{ constructor(...args){ this.init(...args) } init(touchEnabled, noPage){ this.loaderDiv = document.createElement("div") this.loaderDiv.innerHTML = assets.pages["loadsong"] var loadingText = this.loaderDiv.querySelector("#loading-text") this.setAltText(loadingText, strings.loading) this.locked = false this.mode = "main" } setAltText(element, text){ element.innerText = text element.setAttribute("alt", text) } importLocal(files){ if(!files.length){ return Promise.reject("cancel") } this.locked = true this.loading(true) var importSongs = new ImportSongs() return importSongs.load(files).then(this.songsLoaded.bind(this), e => { this.locked = false this.loading(false) if(e === "nosongs"){ this.showError(strings.customSongs.noSongs, "nosongs") }else if(e !== "cancel"){ return Promise.reject(e) } return false }) } loading(show){ if(show){ loader.screen.appendChild(this.loaderDiv) }else if(this.loaderDiv.parentNode){ this.loaderDiv.parentNode.removeChild(this.loaderDiv) } } songsLoaded(songs){ if(songs){ var length = songs.length assets.songs = songs assets.customSongs = true assets.customSelected = +localStorage.getItem("customSelected") } pageEvents.send("import-songs", length) this.clean() return songs && songs.length } showError(text, errorName){ this.locked = false this.loading(false) var error = new Error(text) error.name = errorName throw error } hideError(confirm){ if(this.mode !== "error"){ return } this.mode = "main" this.errorDiv.style.display = "" assets.sounds[confirm ? "se_don" : "se_cancel"].play() } clean(){ delete this.loaderDiv } } class Search{ onClick(){ var songId = parseInt(songEl.dataset.songId) } keyPress(){ this.proceed(parseInt(this.results[this.active].dataset.songId)) } } class PluginsInOld extends Patch{ name = "Plugins in Old Taiko Web" load(){ this.style = document.createElement("style") this.style.appendChild(document.createTextNode(` .plugin-browse-button{ position: relative; overflow: hidden; } #plugin-browse{ position: absolute; font-size: inherit; top: -0.1em; left: -0.1em; right: -0.1em; bottom: -0.1em; border-radius: 0.5em; opacity: 0; cursor: pointer; } #plugin-browse::-webkit-file-upload-button{ cursor: pointer; } .setting-value{ position: relative; }`)) this.addEdits( new EditValue(allStrings.ja, "intl").load(() => "ja"), new EditValue(allStrings.en, "intl").load(() => "en-GB"), new EditValue(allStrings.cn, "intl").load(() => "zh-Hans"), new EditValue(allStrings.tw, "intl").load(() => "zh-Hant"), new EditValue(allStrings.ko, "intl").load(() => "ko"), new EditValue(strings, "plural").load(() => new Intl.PluralRules(strings.intl)), new EditFunction(settings, "setLang").load(str => { return plugins.insertBefore(str, `strings.plural = new Intl.PluralRules(lang.intl) `, 'if(!noEvent){') }), new EditValue(settings, "addLang").load(() => this.addLang), new EditValue(settings, "removeLang").load(() => this.removeLang), new EditValue(SettingsView.prototype, "pluginsStr").load(() => pluginsStr), new EditFunction(SettingsView.prototype, "init").load((str, args) => { args.push("settingsItems", "noSoundStart") str = plugins.insertAfter(str, 'this.songId = songId', ` this.customSettings = !!settingsItems this.settingsItems = settingsItems || settings.items this.locked = false`) str = plugins.insertBefore(str, `if(!noSoundStart) `, 'assets.sounds["bgm_settings"].playLoop') str = plugins.insertBefore(str, `if(this.customSettings){ pageEvents.add(window, "language-change", event => this.setLang(), this.windowSymbol) } `, 'var gamepadEnabled = false') str = plugins.strReplace(str, 'for(let i in settings.items){\n\t\t\tvar current = settings.items[i]', `for(let i in this.settingsItems){ var current = this.settingsItems[i]`) str = plugins.insertAfter(str, 'settingBox.classList.add("setting-box")', ` if(current.indent){ settingBox.style.marginLeft = (2 * current.indent || 0).toString() + "em" }`) str = plugins.strReplace(str, 'var name = strings.settings[i].name\n\t\t\tthis.setAltText(nameDiv, name)', `if(current.name || current.name_lang){ var name = this.getLocalTitle(current.name, current.name_lang) }else{ var name = strings.settings[i].name } this.setAltText(nameDiv, name) if(current.description || current.description_lang){ settingBox.title = this.getLocalTitle(current.description, current.description_lang) || "" }`) str = plugins.strReplace(str, 'this.getValue(i, valueDiv)', `let outputObject = { id: i, settingBox: settingBox, nameDiv: nameDiv, valueDiv: valueDiv, name: current.name, name_lang: current.name_lang, description: current.description, description_lang: current.description_lang } if(current.type === "number"){ ["min", "max", "fixedPoint", "step", "sign", "format", "format_lang"].forEach(opt => { if(opt in current){ outputObject[opt] = current[opt] } }) outputObject.valueText = document.createTextNode("") valueDiv.appendChild(outputObject.valueText) var buttons = document.createElement("div") buttons.classList.add("latency-buttons") buttons.title = "" var buttonMinus = document.createElement("span") buttonMinus.innerText = "-" buttons.appendChild(buttonMinus) this.addTouchRepeat(buttonMinus, event => { this.numberAdjust(outputObject, -1) }) var buttonPlus = document.createElement("span") buttonPlus.innerText = "+" buttons.appendChild(buttonPlus) this.addTouchRepeat(buttonPlus, event => { this.numberAdjust(outputObject, 1) }) valueDiv.appendChild(buttons) this.addTouch(settingBox, event => { if(event.target.tagName !== "SPAN"){ this.setValue(i) } }, true) }else{ this.addTouchEnd(settingBox, event => this.setValue(i)) }`) str = plugins.strReplace(str, 'this.addTouchEnd(settingBox, event => this.setValue(i))\n\t\t\tthis.items.push({\n\t\t\t\tid: i,\n\t\t\t\tsettingBox: settingBox,\n\t\t\t\tnameDiv: nameDiv,\n\t\t\t\tvalueDiv: valueDiv\n\t\t\t})\n\t\t}\n\t\tthis.items.push({\n\t\t\tid: "default",\n\t\t\tsettingBox: this.defaultButton\n\t\t})\n\t\tthis.addTouch(this.defaultButton, this.defaultSettings.bind(this))', ` this.items.push(outputObject) this.getValue(i, valueDiv) } var selectBack = this.items.length === 0 if(this.customSettings){ var form = document.createElement("form") this.browse = document.createElement("input") this.browse.id = "plugin-browse" this.browse.type = "file" this.browse.multiple = true this.browse.accept = ".taikoweb.js" pageEvents.add(this.browse, "change", this.browseChange.bind(this)) form.appendChild(this.browse) this.browseButton = document.createElement("div") this.browseButton.classList.add("taibtn", "stroke-sub", "plugin-browse-button") this.browseText = document.createTextNode("") this.browseButton.appendChild(this.browseText) this.browseButton.appendChild(form) this.defaultButton.parentNode.insertBefore(this.browseButton, this.defaultButton) this.items.push({ id: "browse", settingBox: this.browseButton }) } this.showDefault = !this.customSettings || plugins.allPlugins.filter(obj => obj.plugin.imported).length if(this.showDefault){ this.items.push({ id: "default", settingBox: this.defaultButton }) this.addTouch(this.defaultButton, this.defaultSettings.bind(this)) }else{ this.defaultButton.parentNode.removeChild(this.defaultButton) }`) str = plugins.insertAfter(str, 'this.addTouch(this.endButton, this.onEnd.bind(this))', ` if(selectBack){ this.selected = this.items.length - 1 this.endButton.classList.add("selected") } if(!this.customSettings){`) str = plugins.strReplace(str, 'this.latencySetValue(current, event.type === "touchstart")', `this.latencySetValue(current, event.type === "touchend")`) str = plugins.insertBefore(str, `, true`, ')\n\t\t\tif(current !== "calibration"){') str = plugins.insertBefore(str, `} `, 'this.setStrings()') return plugins.strReplace(str, 'pageEvents.send("settings")', `if(this.customSettings){ pageEvents.send("plugins") }else{ pageEvents.send("settings") }`) }), new EditFunction(SettingsView.prototype, "addTouch").load(str => { return plugins.insertBefore(str, `if(event.cancelable) `, 'event.preventDefault()') }), new EditFunction(SettingsView.prototype, "getValue").load(str => { str = plugins.strReplace(str, 'var current = settings.items[name]\n\t\tvar value = settings.getItem(name)', `if(!this.items){ return } var current = this.settingsItems[name] if(current.getItem){ var value = current.getItem() }else{ var value = settings.getItem(name) }`) str = plugins.strReplace(str, 'value = strings.settings[name][value]', `if(current.options_lang && current.options_lang[value]){ value = this.getLocalTitle(value, current.options_lang[value]) }else if(!current.getItem){ value = strings.settings[name][value] }`) str = plugins.insertBefore(str, `else if(current.type === "number"){ var mul = Math.pow(10, current.fixedPoint || 0) this.items[name].value = value * mul value = Intl.NumberFormat(strings.intl, current.sign ? { signDisplay: "always" } : undefined).format(value) if(current.format || current.format_lang){ value = this.getLocalTitle(current.format, current.format_lang).replace("%s", value) } this.items[name].valueText.data = value return } `, 'valueDiv.innerText = value') return str }), new EditFunction(SettingsView.prototype, "setValue").load(str => { str = plugins.strReplace(str, 'var current = settings.items[name]\n\t\tvar value = settings.getItem(name)', `if(this.locked){ return } var promise var current = this.settingsItems[name] if(current.getItem){ var value = current.getItem() }else{ var value = settings.getItem(name) }`) str = plugins.insertBefore(str, `if(this.mode === "number"){ return this.numberBack(this.items[this.selected]) } `, 'if(this.selected === selectedIndex){') str = plugins.strReplace(str, 'settings.setItem(name, value)', `else if(current.type === "number"){ this.mode = "number" selected.settingBox.style.animation = "none" selected.valueDiv.classList.add("selected") this.playSound("se_don") return } if(current.setItem){ promise = current.setItem(value) }else{ settings.setItem(name, value) } (promise || Promise.resolve()).then(() => {`) return plugins.insertAfter(str, 'this.setLang(allStrings[value])\n\t\t}', ` })`) }), new EditFunction(SettingsView.prototype, "keyPressed").load(str => { str = plugins.insertBefore(str, `if(this.locked){ return } `, 'if(pressed){') str = plugins.insertAfter(str, 'this.defaultSettings()', ` }else if(selected.id === "browse"){ if(event){ this.playSound("se_don") this.browse.click() }`) str = plugins.strReplace(str, '}while(this.items[this.selected].id === "default" && name !== "left")', `}while((this.items[this.selected].id === "default" || this.items[this.selected].id === "browse") && name !== "left")`) return plugins.insertAfter(str, 'this.latencySetAdjust(latencySelected, (name === "up" || name === "right") ? 1 : -1)', ` if(event){ event.preventDefault() } } }else if(this.mode === "number"){ if(name === "confirm" || name === "back"){ this.numberBack(selected) this.playSound(name === "confirm" ? "se_don" : "se_cancel") }else if(name === "up" || name === "right" || name === "down" || name === "left"){ this.numberAdjust(selected, (name === "up" || name === "right") ? 1 : -1) if(event){ event.preventDefault() }`) }), new EditFunction(SettingsView.prototype, "keyboardSet").load(str => { return plugins.strReplace(str, 'settings.items', `this.settingsItems`) }), new EditFunction(SettingsView.prototype, "gamepadSet").load(str => { return plugins.strReplace(str, 'settings.items', `this.settingsItems`) }), new EditFunction(SettingsView.prototype, "gamepadBack").load(str => { return plugins.strReplace(str, 'settings.items', `this.settingsItems`) }), new EditFunction(SettingsView.prototype, "latencySet").load(str => { return plugins.strReplace(str, 'settings.items', `this.settingsItems`) }), new EditFunction(SettingsView.prototype, "latencyBack").load(str => { return plugins.strReplace(str, 'settings.items', `this.settingsItems`) }), new EditValue(SettingsView.prototype, "numberAdjust").load(() => this.numberAdjust), new EditValue(SettingsView.prototype, "numberBack").load(() => this.numberBack), new EditFunction(SettingsView.prototype, "defaultSettings").load(str => { str = plugins.insertBefore(str, `if(this.customSettings){ plugins.unloadImported() this.clean(true) this.playSound("se_don") return setTimeout(() => this.restart(), 500) } `, 'if(this.mode === "keyboard"){') return plugins.strReplace(str, 'settings.items', `this.settingsItems`) }), new EditValue(SettingsView.prototype, "browseChange").load(() => this.browseChange), new EditFunction(SettingsView.prototype, "onEnd").load(str => { str = plugins.insertBefore(str, `if(this.mode === "number"){ this.numberBack(this.items[this.selected]) } `, 'this.clean()') return plugins.strReplace(str, '"settings"', `this.customSettings ? "plugins" : "settings"`) }), new EditValue(SettingsView.prototype, "restart").load(() => this.restart), new EditValue(SettingsView.prototype, "getLocalTitle").load(() => this.getLocalTitle), new EditFunction(SettingsView.prototype, "setLang").load(str => { str = plugins.insertBefore(str, `if(lang) `, 'settings.setLang(lang)') return plugins.strReplace(str, 'var name = strings.settings[item.id].name\n\t\t\t\tthis.setAltText(item.nameDiv, name)', `if(item.name || item.name_lang){ var name = this.getLocalTitle(item.name, item.name_lang) }else{ var name = strings.settings[item.id].name } this.setAltText(item.nameDiv, name) if(item.description || item.description_lang){ item.settingBox.title = this.getLocalTitle(item.description, item.description_lang) || "" }`) }), new EditFunction(SettingsView.prototype, "setStrings").load(str => { str = plugins.insertBefore(str, `this.customSettings ? this.pluginsStr().title : `, 'strings.gameSettings') str = plugins.insertAfter(str, 'this.setAltText(this.endButton, strings.settings.ok)', ` if(this.customSettings){ this.browseText.data = this.pluginsStr().browse this.browseButton.setAttribute("alt", this.pluginsStr().browse) }else{`) return plugins.strReplace(str, 'this.setAltText(this.defaultButton, strings.settings.default)', `} if(this.showDefault){ this.setAltText(this.defaultButton, this.customSettings ? this.pluginsStr().unloadAll : strings.settings.default) }`) }), new EditFunction(SettingsView.prototype, "clean").load((str, args) => { args.push("noSoundStop") str = plugins.insertBefore(str, `if(this.customSettings){ pageEvents.remove(window, "language-change", this.windowSymbol) } if(!noSoundStop) `, 'assets.sounds["bgm_settings"].stop()') str = plugins.insertBefore(str, `if(this.customSettings){ pageEvents.remove(this.browse, "change") delete this.browse delete this.browseButton delete this.browseText }else{ `, 'this.removeTouch(this.gamepadSettings)') str = plugins.insertAfter(str, 'this.removeTouch(this.latencyEndButton)', ` }`) return plugins.strReplace(str, '){\n\t\t\t\t\tURL.revokeObjectURL(assets.image[i].src)', ` || i.startsWith("results_")){ var img = assets.image[i] URL.revokeObjectURL(img.src) if(img.parentNode){ img.parentNode.removeChild(img) }`) }), new EditValue(SongSelect.prototype, "pluginsStr").load(() => pluginsStr), new EditFunction(SongSelect.prototype, "init").load(str => { str = plugins.insertBefore(str, `"plugins": { sort: 0, background: "#f6bba1", border: ["#fde9df", "#ce7553"], outline: "#ce7553" }, `, '"default": {') str = plugins.insertAfter(str, 'music: song.music,', ` chart: song.chart, lyricsFile: song.lyricsFile,`) str = plugins.insertAfter(str, 'action: "browse",\n\t\t\t\tcategory: strings.random\n\t\t\t})\n\t\t}', `this.songs.push({ title: this.pluginsStr().title, skin: this.songSkin.plugins, action: "plugins", category: strings.random })`) return plugins.strReplace(str, 'speed: 800', `speed: 400 * 2`) }), new EditFunction(SongSelect.prototype, "toSelectDifficulty").load(str => { return plugins.insertAfter(str, 'this.toBrowse()', ` }else if(currentSong.action === "plugins"){ this.toPlugins()`) }), new EditValue(SongSelect.prototype, "toPlugins").load(() => this.toPlugins), new EditValue(ImportSongs.prototype, "pluginsStr").load(() => pluginsStr), new EditFunction(SongSelect.prototype, "diffSelMouse").load(str => { return plugins.strReplace(str, 'if(223 < x && x < 367 && 132 < y && y < 436){\n\t\t\t\treturn Math.floor((x - 223) / ((367 - 223) / 2))', `if(223 < x && x < 223 + 72 * this.diffOptions.length && 132 < y && y < 436){ return Math.floor((x - 223) / 72)`) }), new EditFunction(SongSelect.prototype, "redraw").load(str => { return plugins.strReplace(str, 'ms >= this.pressedKeys[key] + 50', `ms >= this.pressedKeys[key] + (this.state.screen === "song" && (key === "right" || key === "left") ? 20 : 50)`) }), new EditFunction(ImportSongs.prototype, "init").load((str, args) => { args.push("otherFiles") str = plugins.insertBefore(str, `if(!songSelect && !otherFiles){ return } if(songSelect){ `, 'this.songSelect = songSelect') str = plugins.insertBefore(str, `}else{ var files = otherFiles } this.pluginFiles = [] this.plugins = [] `, 'this.tjaFiles = []') str = plugins.insertBefore(str, `if(name.endsWith(".taikoweb.js")){ this.pluginFiles.push({ file: file, index: i }) }else `, 'if(name.endsWith(".tja")){') str = plugins.insertBefore(str, `this.pluginAmount = 0 if(this.pluginFiles.length){ var pluginPromises = [] this.pluginFiles.forEach(fileObj => { pluginPromises.push(this.addPlugin(fileObj).catch(e => console.warn(e))) }) return Promise.all(pluginPromises).then(() => { var startPromises = [] if(this.plugins.length && confirm(this.pluginsStr().warning.replace("%s", this.pluginsStr().plugin[strings.plural.select(this.plugins.length)].replace("%s", this.plugins.length.toString() ) ))){ this.plugins.forEach(obj => { var plugin = plugins.add(obj.data, { name: obj.name, raw: true }) if(plugin){ this.pluginAmount++ plugin.imported = true startPromises.push(plugin.start()) } }) } return Promise.all(startPromises).then(this.loaded.bind(this)) }) } `, 'var metaPromises = []') str = plugins.insertBefore(str, `return `, 'Promise.all(metaPromises)') return plugins.insertBefore(str, `return `, 'Promise.all(songPromises)') }), new EditValue(ImportSongs.prototype, "load").load(() => this.importSongsLoad), new EditValue(ImportSongs.prototype, "addPlugin").load(() => this.addPlugin), new EditFunction(ImportSongs.prototype, "loaded").load(str => { return plugins.insertBefore(str, `if(!this.songSelect){ if(this.songs.length){ return Promise.resolve(this.songs) }else{ if(this.pluginAmount || Object.keys(this.assetFiles).length){ return Promise.resolve() }else{ return Promise.reject("nosongs") } } return this.clean() } `, 'if(this.stylesheet.length){') }), new EditValue(loader, "cssRuleset").load(() => this.cssRuleset), new EditValue(p2, "disabled").load(() => 0), new EditFunction(p2, "open").load(str => { return plugins.insertBefore(str, `if(!this.closed || this.disabled){ return } `, 'this.closed = false') }), new EditValue(p2, "enable").load(() => this.p2enable), new EditValue(p2, "disable").load(() => this.p2disable), new EditFunction(snd.sfxGain.soundBuffer, "load").load(str => { return plugins.insertBefore(str, `if(typeof url.arrayBuffer === "function"){ var loadPromise = url.arrayBuffer() }else `, 'if(local){') }), new EditFunction(SongSelect.prototype, "getLocalTitle").load(str => { return plugins.strReplace(str, 'if(id === strings.id && titleLang[id]){', `if(id === "en" && strings.preferEn && !(strings.id in titleLang) && titleLang.en || id === strings.id && titleLang[id]){`) }), new EditFunction(Account.prototype, "accountForm").load(str => { return plugins.insertAfter(str, 'this.redrawRunning = true', ` this.redrawPaused = 'matchMedia("(prefers-reduced-motion: reduce)").matches' === 'true'`) }), new EditFunction(Account.prototype, "customdonRedraw").load(str => { return plugins.insertAfter(str, 'var frame = ', `this.redrawPaused ? 0 : `) }), new EditFunction(LoadSong.prototype, "run").load(str => { str = plugins.strReplace(str, 'if(songObj.chart){', `if(songObj.chart && !songObj.chart.separateDiff && !(songObj.chart instanceof RemoteFile)){`) return plugins.strReplace(str, 'if(songObj.music){', `if(songObj.music && !(songObj.chart instanceof RemoteFile)){`) }), new EditFunction(Controller.prototype, "restartSong").load(str => { str = plugins.strReplace(str, 'if(songObj.chart &&', `if(songObj.chart && !songObj.chart.separateDiff && !(songObj.chart instanceof RemoteFile) &&`) return plugins.strReplace(str, 'if(songObj.lyricsFile){', `if(songObj.lyricsFile && !(songObj.lyricsFile instanceof RemoteFile)){`) }), ) } start(){ document.head.appendChild(this.style) if(assets && assets.songsDefault){ assets.songsDefault.forEach(song => { var directory = gameConfig.songs_baseurl + song.id + "/" var songExt = song.music_type ? song.music_type : "mp3" song.music = new RemoteFile(directory + "main." + songExt) if(song.type === "tja"){ song.chart = new RemoteFile(directory + "main.tja") }else{ song.chart = {separateDiff: true} for(var diff in song.courses){ if(song.courses[diff]){ song.chart[diff] = new RemoteFile(directory + diff + ".osu") } } } if(song.lyrics){ song.lyricsFile = new RemoteFile(directory + "main.vtt") } }) } } stop(){ if(this.style.parentNode){ this.style.parentNode.removeChild(this.style) } if(assets && assets.songsDefault){ assets.songsDefault.forEach(song => { if(song.music instanceof RemoteFile){ delete song.music } delete song.chart delete song.lyricsFile }) } } addLang(lang, forceSet){ allStrings[lang.id] = lang if(lang.categories){ assets.categories.forEach(category => { if("title_lang" in category && lang.categories[category.title_lang.en]){ category.title_lang[lang.id] = lang.categories[category.title_lang.en] } }) } languageList.push(lang.id) this.allLanguages.push(lang.id) this.items.language.default = this.getLang() if(forceSet){ this.storage.language = lang.id }else{ try{ this.storage.language = localStorage.getItem("lang") }catch(e){} if(this.items.language.options.indexOf(this.storage.language) === -1){ this.storage.language = null } } if(settings.getItem("language") === lang.id){ settings.setLang(lang) } } removeLang(lang){ delete allStrings[lang.id] assets.categories.forEach(category => { if("title_lang" in category){ delete category.title_lang[lang.id] } }) var index = languageList.indexOf(lang.id) if(index !== -1){ languageList.splice(index, 1) } var index = this.allLanguages.indexOf(lang.id) if(index !== -1){ this.allLanguages.splice(index, 1) } this.items.language.default = this.getLang() try{ this.storage.language = localStorage.getItem("lang") }catch(e){} if(this.items.language.options.indexOf(this.storage.language) === -1){ this.storage.language = null } if(lang.id === strings.id){ settings.setLang(allStrings[this.getItem("language")]) } } numberAdjust(selected, add){ var selectedItem = this.items[this.selected] var mul = Math.pow(10, selected.fixedPoint || 0) selectedItem.value += add * ("step" in selected ? selected.step : 1) if("max" in selected && selectedItem.value > selected.max * mul){ selectedItem.value = selected.max * mul }else if("min" in selected && selectedItem.value < selected.min * mul){ selectedItem.value = selected.min * mul }else{ this.playSound("se_ka") } var valueText = Intl.NumberFormat(strings.intl, selected.sign ? { signDisplay: "always" } : undefined).format(selectedItem.value / mul) if(selected.format || selected.format_lang){ valueText = this.getLocalTitle(selected.format, selected.format_lang).replace("%s", valueText) } selectedItem.valueText.data = valueText } numberBack(selected){ this.mode = "settings" selected.settingBox.style.animation = "" selected.valueDiv.classList.remove("selected") var current = this.settingsItems[selected.id] var promise var mul = Math.pow(10, selected.fixedPoint || 0) var value = selected.value / mul if(current.setItem){ promise = current.setItem(value) }else{ settings.setItem(selected.id, value) } (promise || Promise.resolve()).then(() => { this.getValue(selected.id, selected.valueText) }) } browseChange(event){ this.locked = true var files = [] for(var i = 0; i < event.target.files.length; i++){ files.push(new LocalFile(event.target.files[i])) } var customSongs = new CustomSongs(this.touchEnabled, true) customSongs.importLocal(files).then(() => { this.clean(true) return this.restart() }).catch(e => { if(e){ var message = e.message if(e.name === "nosongs"){ message = this.pluginsStr().noPlugins } if(message){ alert(message) } } this.locked = false this.browse.form.reset() return Promise.resolve() }) } restart(){ if(this.mode === "number"){ this.numberBack(this.items[this.selected]) } return new SettingsView(this.touchEnabled, this.tutorial, this.songId, undefined, this.customSettings ? plugins.getSettings() : undefined, true) } getLocalTitle(title, titleLang){ if(titleLang){ for(var id in titleLang){ if(id === strings.id && titleLang[id]){ return titleLang[id] } } } return title } toPlugins(){ this.playSound("se_don") this.clean() setTimeout(() => { new SettingsView(this.touchEnabled, false, undefined, undefined, plugins.getSettings()) }, 500) } importSongsLoad(files){ return this.init(null, null, files) } addPlugin(fileObj){ var file = fileObj.file var filePromise = file.read() return filePromise.then(dataRaw => { var name = file.name.slice(0, file.name.lastIndexOf(".taikoweb.js")) this.plugins.push({ name: name, data: dataRaw }) }) } cssRuleset(rulesets){ var css = [] for(var selector in rulesets){ var declarationsObj = rulesets[selector] var declarations = [] for(var property in declarationsObj){ var value = declarationsObj[property] declarations.push("\t" + property + ": " + value + ";") } css.push(selector + "{\n" + declarations.join("\n") + "\n}") } return css.join("\n") } p2enable(){ this.disabled = Math.max(0, this.disabled - 1) setTimeout(this.open.bind(this), 100) } p2disable(){ this.disabled++ this.close() } } function ready(){ if(typeof CustomSongs !== "undefined"){ console.error("[Plugins in Old Taiko Web] Cannot run on new taiko-web") return } window.Plugins = Plugins window.PluginLoader = PluginLoader window.EditValue = EditValue window.EditFunction = EditFunction window.Patch = Patch window.readFile = readFile window.RemoteFile = RemoteFile window.LocalFile = LocalFile window.CustomSongs = CustomSongs2 window.Search = Search var classes = [ About, Account, Settings, SongSelect, AutoScore, CanvasAsset, CanvasCache, CanvasDraw, CanvasTest, Circle, Controller, Debug, InputSlider, Game, GameInput, Gamepad, GameRules, ImportSongs, Keyboard, LoadSong, Logo, Lyrics, Mekadon, P2Connection, ParseOsu, ParseTja, Scoresheet, ScoreStorage, Session, Settings, SettingsView, SongSelect, SoundBuffer, SoundGain, Sound, Titlescreen, Tutorial, View, ViewAssets ] classes.forEach(cls => { var str = cls.toString() var needle1 = "class " var index1 = str.indexOf(needle1) if(index1 !== -1){ var pos = index1 + needle1.length var needle2 = "{" var index2 = str.indexOf(needle2, pos) if(index2 !== -1){ var name = str.slice(pos, index2).trim() if(name){ pos = index2 + needle2.length var needle3 = "constructor" var index3 = str.indexOf(needle3, pos) if(index3 !== -1){ var str = name + " = " + str.slice(0, index3 + needle3.length) + "(...args){\n\t\tthis.init(...args)\n\t}\n\tinit" + str.slice(index3 + needle3.length) return window.eval(str) } } } } throw new Error("Error attempting to fix class:", cls) }) window.plugins = new Plugins() var plugin = plugins.add(PluginsInOld, { name: PluginsInOld.name, hide: true }) plugin?.start() } if(typeof perf !== "undefined" && perf.allImg){ ready() }else{ addEventListener("ready", ready, {once: true}) }