diff --git a/public/src/css/search.css b/public/src/css/search.css index d10c389..d93649f 100644 --- a/public/src/css/search.css +++ b/public/src/css/search.css @@ -48,7 +48,7 @@ padding: 0.5em 0.7em; border-radius: 0.2em; border: 0.13em black solid; - font-family: TnT; + font-family: inherit; box-sizing: border-box; -webkit-box-sizing:border-box; -moz-box-sizing: border-box; diff --git a/public/src/js/about.js b/public/src/js/about.js index 7611145..7734e9b 100644 --- a/public/src/js/about.js +++ b/public/src/js/about.js @@ -160,6 +160,10 @@ diag.push("Language: " + strings.id + userLangStr) var latency = settings.getItem("latency") diag.push("Audio Latency: " + (latency.audio > 0 ? "+" : "") + latency.audio.toString() + "ms, Video Latency: " + (latency.video > 0 ? "+" : "") + latency.video.toString() + "ms") + var pluginList = plugins.allPlugins.map(pluginLoader => { + return (pluginLoader.plugin.module && pluginLoader.plugin.module.name || pluginLoader.name) + (pluginLoader.plugin.started ? " (started)" : "") + }) + diag.push("Plugins: " + pluginList.join(", ")) var errorObj = {} if(localStorage["lastError"]){ try{ diff --git a/public/src/js/canvasdraw.js b/public/src/js/canvasdraw.js index e80529e..ee021f0 100644 --- a/public/src/js/canvasdraw.js +++ b/public/src/js/canvasdraw.js @@ -299,7 +299,7 @@ verticalText(config){ var ctx = config.ctx - var inputText = config.text.toString() + var inputText = "" + config.text var mul = config.fontSize / 40 var ura = false var r = this.regex @@ -623,7 +623,7 @@ layeredText(config, layers){ var ctx = config.ctx - var inputText = config.text.toString() + var inputText = "" + config.text var mul = config.fontSize / 40 var ura = false var r = this.regex diff --git a/public/src/js/loader.js b/public/src/js/loader.js index 1ca40d7..bf52532 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -327,7 +327,9 @@ class Loader{ p2.hash("") } - promises.push(this.canvasTest.drawAllImages()) + promises.push(this.canvasTest.drawAllImages().then(result => { + perf.allImg = result + })) if(gameConfig.plugins){ gameConfig.plugins.forEach(obj => { @@ -349,8 +351,7 @@ class Loader{ }) } - Promise.all(promises).then(result => { - perf.allImg = result + Promise.all(promises).then(() => { perf.load = Date.now() - this.startTime this.canvasTest.clean() this.clean() diff --git a/public/src/js/lyrics.js b/public/src/js/lyrics.js index 8459fc3..6cd6bd6 100644 --- a/public/src/js/lyrics.js +++ b/public/src/js/lyrics.js @@ -77,7 +77,7 @@ class Lyrics{ break } var lang = text.slice(index1 + 6, index2).toLowerCase() - if(strings.id === lang){ + if(strings.preferEn && lang === "en" || strings.id === lang){ var index3 = text.indexOf(" obj.name === name) if(index !== -1){ this.allPlugins.splice(index, 1) @@ -159,6 +165,34 @@ class Plugins{ } 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++){ @@ -207,10 +241,12 @@ class Plugins{ default: true, getItem: () => plugin.started, setItem: value => { - if(plugin.started && !value){ - this.stop(plugin.name) - }else if(!plugin.started && value){ - this.start(plugin.name) + if(plugin.name in this.pluginMap){ + if(plugin.started && !value){ + this.stop(plugin.name) + }else if(!plugin.started && value){ + this.start(plugin.name) + } } } }) @@ -293,9 +329,15 @@ class PluginLoader{ try{ this.module = new module.default() }catch(e){ - console.error(e) this.error() - return + 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{ @@ -306,22 +348,41 @@ class PluginLoader{ output = this.module.load(this) } }catch(e){ - console.error(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 => { - console.error(e) this.error() - return Promise.resolve() + 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 => { - console.error(e) this.error() - if(loadErrors){ - return Promise.reject(e) + 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() } }) @@ -342,13 +403,15 @@ class PluginLoader{ this.module.start() } }catch(e){ - console.error(e) + var error = new Error() + error.stack = "Error in plugin start: " + this.name + "\n" + e.stack + console.error(error) this.error() } } }) } - stop(orderChange, error){ + stop(orderChange, noError){ if(this.loaded && this.started){ if(!orderChange){ var stopIndex = plugins.startOrder.indexOf(this.name) @@ -369,8 +432,10 @@ class PluginLoader{ this.module.stop() } }catch(e){ - console.error(e) - if(!error){ + var error = new Error() + error.stack = "Error in plugin stop: " + this.name + "\n" + e.stack + console.error(error) + if(!noError){ this.error() } } @@ -398,7 +463,9 @@ class PluginLoader{ this.module.unload() } }catch(e){ - console.error(e) + var error = new Error() + error.stack = "Error in plugin unload: " + this.name + "\n" + e.stack + console.error(error) } delete this.module } @@ -409,7 +476,9 @@ class PluginLoader{ try{ this.module.error() }catch(e){ - console.error(e) + var error = new Error() + error.stack = "Error in plugin error: " + this.name + "\n" + e.stack + console.error(error) } } this.unload(true) @@ -453,9 +522,17 @@ class EditValue{ if(this.name){ this.original = this.name[0][this.name[1]] } - var output = this.loadCallback(this.original) + 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"){ - throw new Error("A value is expected to be returned") + 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 @@ -472,6 +549,27 @@ class EditValue{ } 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 @@ -485,11 +583,33 @@ class EditFunction extends EditValue{ this.original = this.name[0][this.name[1]] } var args = plugins.argsFromFunc(this.original) - var output = this.loadCallback(plugins.strFromFunc(this.original), args) - if(typeof output === "undefined"){ - throw new Error("A value is expected to be returned") + 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 } - var output = Function(...args, output) if(this.name){ this.name[0][this.name[1]] = output } @@ -499,14 +619,32 @@ class EditFunction extends EditValue{ class Patch{ edits = [] + 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(){ - this.edits.forEach(edit => edit.stop()) + 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()) diff --git a/public/src/js/scorestorage.js b/public/src/js/scorestorage.js index 2d2ce2f..6fac5c0 100644 --- a/public/src/js/scorestorage.js +++ b/public/src/js/scorestorage.js @@ -48,7 +48,7 @@ class ScoreStorage{ var scoreArray = diffArray[i].slice(1).split(",") for(var j in this.scoreKeys){ var name = this.scoreKeys[j] - var value = parseInt(scoreArray[j], 36) || 0 + var value = parseInt(scoreArray[j] || 0, 36) || 0 if(value < 0){ value = 0 } diff --git a/public/src/js/settings.js b/public/src/js/settings.js index dc6e5b9..e9c9f2d 100644 --- a/public/src/js/settings.js +++ b/public/src/js/settings.js @@ -159,6 +159,58 @@ class Settings{ pageEvents.send("language-change", lang.id) } } + 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")]) + } + } } class SettingsView{ @@ -204,6 +256,10 @@ class SettingsView{ } }, this.windowSymbol) + if(this.customSettings){ + pageEvents.add(window, "language-change", event => this.setLang(), this.windowSymbol) + } + var gamepadEnabled = false if("getGamepads" in navigator){ var gamepads = navigator.getGamepads() @@ -441,7 +497,11 @@ class SettingsView{ this.mode = "latency" this.latencySet() } - pageEvents.send("settings") + if(this.customSettings){ + pageEvents.send("plugins") + }else{ + pageEvents.send("settings") + } } getElement(name){ return loader.screen.getElementsByClassName(name)[0] @@ -1014,7 +1074,9 @@ class SettingsView{ return title } setLang(lang){ - settings.setLang(lang) + if(lang){ + settings.setLang(lang) + } if(failedTests.length !== 0){ showUnsupported(strings) } @@ -1098,6 +1160,9 @@ class SettingsView{ this.gamepad.clean() assets.sounds["bgm_settings"].stop() pageEvents.remove(window, ["mouseup", "touchstart", "touchmove", "touchend", "blur"], this.windowSymbol) + if(this.customSettings){ + pageEvents.remove(window, "language-change", this.windowSymbol) + } for(var i in this.items){ this.removeTouchEnd(this.items[i].settingBox) } diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index c2ec8d0..934dd9d 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -86,6 +86,7 @@ class SongSelect{ if(category.songSkin.sort === null){ category.songSkin.sort = songSkinLength + 1 } + category.songSkin.id = category.id this.songSkin[category.title] = category.songSkin } } @@ -95,12 +96,14 @@ class SongSelect{ var searchCss = [] Object.keys(this.songSkin).forEach(key => { var skin = this.songSkin[key] - var stripped = key.replace(/\W/g, '') - - searchCss.push('.song-search-' + stripped + ' { background-color: ' + skin.background + ' }') - searchCss.push('.song-search-' + stripped + '::before { border: 0.4em solid ' + skin.border[0] + ' ; border-bottom-color: ' + skin.border[1] + ' ; border-right-color: ' + skin.border[1] + ' }') - searchCss.push('.song-search-' + stripped + ' .song-search-result-title::before { -webkit-text-stroke: 0.4em ' + skin.outline + ' }') - searchCss.push('.song-search-' + stripped + ' .song-search-result-subtitle::before { -webkit-text-stroke: 0.4em ' + skin.outline + ' }') + if("id" in skin || key === "default"){ + var id = "id" in skin ? ("cat" + skin.id) : key + + searchCss.push('.song-search-' + id + ' { background-color: ' + skin.background + ' }') + searchCss.push('.song-search-' + id + '::before { border: 0.4em solid ' + skin.border[0] + ' ; border-bottom-color: ' + skin.border[1] + ' ; border-right-color: ' + skin.border[1] + ' }') + searchCss.push('.song-search-' + id + ' .song-search-result-title::before { -webkit-text-stroke: 0.4em ' + skin.outline + ' }') + searchCss.push('.song-search-' + id + ' .song-search-result-subtitle::before { -webkit-text-stroke: 0.4em ' + skin.outline + ' }') + } }) this.searchStyle.appendChild(document.createTextNode(searchCss.join("\n"))) loader.screen.appendChild(this.searchStyle) @@ -447,7 +450,7 @@ class SongSelect{ } }else if(name === "confirm"){ if(Number.isInteger(this.search.active)){ - this.searchProceed(parseInt(this.search.results[this.search.active].dataset.song_id)) + this.searchProceed(parseInt(this.search.results[this.search.active].dataset.songId)) } } }else if(this.state.screen === "song"){ @@ -487,7 +490,7 @@ class SongSelect{ }else if(this.state.screen === "difficulty"){ if(event && event.keyCode && event.keyCode === 70 && ctrl){ this.displaySearch() - event.preventDefault() + if(event){ event.preventDefault() } }else if(name === "confirm"){ if(this.selectedDiff === 0){ this.toSongSelect() @@ -508,13 +511,18 @@ class SongSelect{ this.endPreview(true) this.playBgm(false) } + }else if(this.state.screen === "title" || this.state.screen === "titleFadeIn"){ + if(event && event.keyCode && event.keyCode === 70 && ctrl){ + this.displaySearch() + if(event){ event.preventDefault() } + } } } mouseDown(event){ if(event.target === this.selectable || event.target.parentNode === this.selectable){ this.selectable.focus() - }else{ + }else if(event.target.tagName !== "INPUT"){ getSelection().removeAllRanges() this.selectable.blur() } @@ -896,7 +904,7 @@ class SongSelect{ this.selectedDiff = 1 do{ this.state.options = this.mod(this.optionsList.length, this.state.options + moveBy) - }while((p2.socket && p2.socket.readyState !== 1 || assets.customSongs) && this.state.options === 2) + }while((!p2.socket || p2.socket.readyState !== 1 || assets.customSongs) && this.state.options === 2) } } toTitleScreen(){ @@ -2711,15 +2719,17 @@ class SongSelect{ var title = this.getLocalTitle(song.title, song.title_lang) var subtitle = this.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang) - var strippedCat = "default" + var id = "default" if(song.category_id){ var cat = assets.categories.find(cat => cat.id === song.category_id) - strippedCat = cat.title.replace(/\W/g, '') + if(cat && "id" in cat){ + id = "cat" + cat.id + } } var resultDiv = document.createElement("div") - resultDiv.classList.add("song-search-result", "song-search-" + strippedCat) - resultDiv.dataset.song_id = song.id + resultDiv.classList.add("song-search-result", "song-search-" + id) + resultDiv.dataset.songId = song.id var resultInfoDiv = document.createElement("div") resultInfoDiv.classList.add("song-search-result-info") @@ -2868,9 +2878,15 @@ class SongSelect{ this.setSearchTip() cancelTouch = false noResizeRoot = true + if(this.songs[this.selectedSong].courses){ + snd.previewGain.setVolumeMul(0.5) + }else if(this.bgmEnabled){ + snd.musicGain.setVolumeMul(0.5) + } setTimeout(() => { this.search.input.focus() + this.search.input.setSelectionRange(0, this.search.input.value.length) }, 10) var lastQuery = localStorage.getItem("lastSearchQuery") @@ -2894,6 +2910,11 @@ class SongSelect{ delete this.search cancelTouch = true noResizeRoot = false + if(this.songs[this.selectedSong].courses){ + snd.previewGain.setVolumeMul(1) + }else if(this.bgmEnabled){ + snd.musicGain.setVolumeMul(1) + } } } @@ -2964,6 +2985,7 @@ class SongSelect{ case "creative": case "played": case "maker": + case "diverge": filters[parts[0]] = parts[1] break } @@ -3037,7 +3059,12 @@ class SongSelect{ if(aliases.find(alias => alias.toLowerCase() === value.toLowerCase())){ passedFilters++ } - + break + case "diverge": + var branch = Object.values(song.courses).find(course => course && course.branch) + if((value === "yes" && branch) || (value === "no" && !branch)){ + passedFilters++ + } break } }) @@ -3068,7 +3095,7 @@ class SongSelect{ } lastIdx = range[1] }) - var index = a[0].target.indexOf(query) + var index = a[0].target.toLowerCase().indexOf(query) if(index !== -1){ a[0].ranges = [[index, index + query.length - 1]] }else if(rangeAmount > a[0].indexes.length / 2){ @@ -3134,8 +3161,9 @@ class SongSelect{ } searchInput(){ - var text = this.search.input.value.toLowerCase() + var text = this.search.input.value localStorage.setItem("lastSearchQuery", text) + text = text.toLowerCase() if(text.length === 0){ this.setSearchTip() @@ -3181,7 +3209,7 @@ class SongSelect{ }else if(e.which === 1){ var songEl = e.target.closest(".song-search-result") if(songEl){ - var songId = parseInt(songEl.dataset.song_id) + var songId = parseInt(songEl.dataset.songId) this.searchProceed(songId) } } @@ -3309,7 +3337,7 @@ class SongSelect{ getLocalTitle(title, titleLang){ if(titleLang){ for(var id in titleLang){ - if(id === strings.id && titleLang[id]){ + if(id === "en" && strings.preferEn && !(strings.id in titleLang) && titleLang.en || id === strings.id && titleLang[id]){ return titleLang[id] } }