Lots of bug fixes

- Toggling autoplay checkbox in debug now disallows saving the score
- Fix crown of the song that was previously selected rendering above the genre
- Outline of empty crowns is slightly darker
- Fix rendering when there is a crown on oni and no crown on ura
- Fix crowns overlapping the netplay 2P icon
- Fix gauge in latency calibration
- Fix debug to work on mobile, can be toggled with ctrl+alt+`;` on hacker's keyboard with permanent notification
- Fix being unable to scroll the settings list without toggling something
- Handle KeyboardInterrupt in server.py
- Fix category jumping not working in session mode
- Fix mouse cursor being hidden at results screen in session mode
- Make "Issues" on the about screen and "An error occurred, please refresh" on loading screens translateable
- CanvasCache uses integer increments for comparison instead of Date.now()
- For imported songs, exclude song titles from genre comparisons if they appear in the name of the folder
- Fix tja files with spaces in the notation
- Fix second player's score on the results screen to have upside down appearance
- Add fixed chinese strings
This commit is contained in:
LoveEevee 2020-03-09 15:36:57 +03:00
parent 5a1be53e21
commit 9248a52194
21 changed files with 459 additions and 211 deletions

View File

@ -31,6 +31,8 @@
this.items = []
this.getLink(this.linkIssues).innerText = strings.about.issues
this.linkIssues.setAttribute("alt", strings.about.issues)
var versionUrl = gameConfig._version.url
this.getLink(this.linkIssues).href = versionUrl + "issues"
this.items.push(this.linkIssues)

View File

@ -4,6 +4,7 @@ class CanvasCache{
if(w){
this.resize(w, h, scale)
}
this.index = Number.MIN_SAFE_INTEGER
}
resize(w, h, scale){
if(this.canvas){
@ -33,7 +34,7 @@ class CanvasCache{
return
}
var saved = false
var time = Date.now()
var index = this.index++
if(!img){
var w = config.w
var h = config.h
@ -44,11 +45,11 @@ class CanvasCache{
}
if(this.y + h > this.h){
var clear = true
var oldest = {time: time}
var oldest = {index: index}
this.map.forEach((oldImg, id) => {
if(oldImg.time < oldest.time){
if(oldImg.index < oldest.index){
oldest.id = id
oldest.time = oldImg.time
oldest.index = oldImg.index
}
})
var oldImg = this.map.get(oldest.id)
@ -84,7 +85,7 @@ class CanvasCache{
this.map.set(config.id, img)
callback(this.ctx)
}
img.time = time
img.index = index
if(setOnly){
this.ctx.restore()
return

View File

@ -1304,7 +1304,7 @@
ctx.globalAlpha = 1 - config.shine
}
ctx.strokeStyle = config.type ? "#000" : "rgba(255, 193, 0, 0.5)"
ctx.strokeStyle = config.type ? "#000" : "#ffc616"
ctx.lineWidth = 18
ctx.stroke(this.crownPath)
@ -1362,12 +1362,21 @@
ctx.fillStyle = "#000"
ctx.beginPath()
if(config.scoresheet){
ctx.moveTo(-4, 26)
ctx.lineTo(gaugeClear - 4, 26)
this.roundedCorner(ctx, gaugeClear - 4, 4, 13, 0)
this.roundedCorner(ctx, 760, 4, 13, 1)
ctx.lineTo(760, 56)
ctx.lineTo(-4, 56)
if(config.multiplayer){
ctx.moveTo(-4, -4)
ctx.lineTo(760, -4)
this.roundedCorner(ctx, 760, 48, 13, 2)
this.roundedCorner(ctx, gaugeClear - 4, 48, 13, 3)
ctx.lineTo(gaugeClear - 4, 26)
ctx.lineTo(-4, 26)
}else{
ctx.moveTo(-4, 26)
ctx.lineTo(gaugeClear - 4, 26)
this.roundedCorner(ctx, gaugeClear - 4, 4, 13, 0)
this.roundedCorner(ctx, 760, 4, 13, 1)
ctx.lineTo(760, 56)
ctx.lineTo(-4, 56)
}
}else if(config.multiplayer){
ctx.moveTo(gaugeClear - 7, 27)
ctx.lineTo(788, 27)
@ -1381,42 +1390,44 @@
}
ctx.fill()
if(gaugeFilled <= gaugeClear){
if(gaugeFilled < gaugeClear){
ctx.fillStyle = config.blue ? "#184d55" : "#680000"
var x = Math.max(0, gaugeFilled - 5)
ctx.fillRect(x, firstTop, gaugeClear - x + 2, 22)
ctx.fillRect(x, firstTop, gaugeClear - x + 2 + (gaugeClear < gaugeW ? 0 : -7), 22)
}
if(gaugeFilled > 0){
var w = Math.min(gaugeClear + 1, gaugeFilled - 4)
var w = Math.min(gaugeW - 5, gaugeClear + 1, gaugeFilled - 4)
ctx.fillStyle = config.blue ? "#00edff" : "#ff3408"
ctx.fillRect(0, firstTop + 2, w, 20)
ctx.fillStyle = config.blue ? "#9cffff" : "#ffa191"
ctx.fillRect(0, firstTop, w, 3)
}
if(gaugeFilled < gaugeW - 4){
ctx.fillStyle = "#684900"
var x = Math.max(gaugeClear + 9, gaugeFilled - gaugeClear + 9)
ctx.fillRect(x, secondTop, gaugeW - 4 - x, 44)
if(gaugeClear < gaugeW){
if(gaugeFilled < gaugeW - 4){
ctx.fillStyle = "#684900"
var x = Math.max(gaugeClear + 9, gaugeFilled - gaugeClear + 9)
ctx.fillRect(x, secondTop, gaugeW - 4 - x, 44)
}
if(gaugeFilled > gaugeClear + 14){
var w = Math.min(gaugeW - 4, gaugeFilled - gaugeClear - 14)
ctx.fillStyle = "#ff0"
ctx.fillRect(gaugeClear + 9, secondTop + 2, w, 42)
ctx.fillStyle = "#fff"
ctx.fillRect(gaugeClear + 9, secondTop, w, 3)
}
ctx.fillStyle = cleared ? "#ff0" : "#684900"
ctx.beginPath()
if(config.multiplayer){
this.roundedCorner(ctx, gaugeClear, secondTop + 44, 10, 3)
ctx.lineTo(gaugeClear, secondTop)
ctx.lineTo(gaugeClear + 10, secondTop)
}else{
ctx.moveTo(gaugeClear, secondTop + 44)
this.roundedCorner(ctx, gaugeClear, secondTop, 10, 0)
ctx.lineTo(gaugeClear + 10, secondTop + 44)
}
ctx.fill()
}
if(gaugeFilled > gaugeClear + 14){
var w = Math.min(gaugeW - 4, gaugeFilled - gaugeClear - 14)
ctx.fillStyle = "#ff0"
ctx.fillRect(gaugeClear + 9, secondTop + 2, w, 42)
ctx.fillStyle = "#fff"
ctx.fillRect(gaugeClear + 9, secondTop, w, 3)
}
ctx.fillStyle = cleared ? "#ff0" : "#684900"
ctx.beginPath()
if(config.multiplayer){
this.roundedCorner(ctx, gaugeClear, secondTop + 44, 10, 3)
ctx.lineTo(gaugeClear, secondTop)
ctx.lineTo(gaugeClear + 10, secondTop)
}else{
ctx.moveTo(gaugeClear, secondTop + 44)
this.roundedCorner(ctx, gaugeClear, secondTop, 10, 0)
ctx.lineTo(gaugeClear + 10, secondTop + 44)
}
ctx.fill()
if(cleared){
ctx.save()
ctx.clip()
@ -1430,7 +1441,7 @@
ctx.lineWidth = 5
for(var i = 0; i < 49; i++){
var x = 14 + i * 14 - ctx.lineWidth / 2
if(i === 26){
if(i === config.clear * 50 - 1){
ctx.stroke()
ctx.beginPath()
ctx.lineWidth = 4
@ -1439,18 +1450,20 @@
ctx.lineTo(x, x < gaugeClear ? firstTop + 22 : secondTop + 44)
}
ctx.stroke()
this.layeredText({
ctx: ctx,
text: strings.clear,
fontSize: 18,
fontFamily: config.font,
x: gaugeClear + 3,
y: config.multiplayer ? 22 : 11,
letterSpacing: -2
}, [
{scale: [1.1, 1.01], outline: "#000", letterBorder: 6},
{scale: [1.11, 1], fill: cleared ? "#fff" : "#737373"}
])
if(config.clear < 47 / 50){
this.layeredText({
ctx: ctx,
text: strings.clear,
fontSize: 18,
fontFamily: config.font,
x: gaugeClear + 3,
y: config.multiplayer ? 22 : 11,
letterSpacing: -2
}, [
{scale: [1.1, 1.01], outline: "#000", letterBorder: 6},
{scale: [1.11, 1], fill: cleared ? "#fff" : "#737373"}
])
}
ctx.restore()
}

View File

@ -3,6 +3,7 @@ class Controller{
this.selectedSong = selectedSong
this.songData = songData
this.autoPlayEnabled = autoPlayEnabled
this.saveScore = !autoPlayEnabled
this.multiplayer = multiplayer
this.touchEnabled = touchEnabled
this.snd = this.multiplayer ? "_p" + this.multiplayer : ""

View File

@ -17,6 +17,7 @@ class Debug{
this.branchSelect = this.branchSelectDiv.getElementsByTagName("select")[0]
this.branchResetBtn = this.branchSelectDiv.getElementsByClassName("reset")[0]
this.volumeDiv = this.byClass("music-volume")
this.restartLabel = this.byClass("change-restart-label")
this.restartCheckbox = this.byClass("change-restart")
this.autoplayLabel = this.byClass("autoplay-label")
this.autoplayCheckbox = this.byClass("autoplay")
@ -24,15 +25,19 @@ class Debug{
this.exitBtn = this.byClass("exit-btn")
this.moving = false
pageEvents.add(window, ["mousedown", "mouseup", "blur"], this.stopMove.bind(this))
this.windowSymbol = Symbol()
pageEvents.add(window, ["mousedown", "mouseup", "touchstart", "touchend", "blur", "resize"], this.stopMove.bind(this), this.windowSymbol)
pageEvents.mouseAdd(this, this.onMove.bind(this))
pageEvents.add(this.titleDiv, "mousedown", this.startMove.bind(this))
pageEvents.add(this.minimiseDiv, "click", this.minimise.bind(this))
pageEvents.add(this.restartBtn, "click", this.restartSong.bind(this))
pageEvents.add(this.exitBtn, "click", this.clean.bind(this))
pageEvents.add(window, "touchmove", this.onMove.bind(this))
pageEvents.add(this.titleDiv, ["mousedown", "touchstart"], this.startMove.bind(this))
pageEvents.add(this.minimiseDiv, ["click", "touchstart"], this.minimise.bind(this))
pageEvents.add(this.restartBtn, ["click", "touchstart"], this.restartSong.bind(this))
pageEvents.add(this.exitBtn, ["click", "touchstart"], this.clean.bind(this))
pageEvents.add(this.restartLabel, "touchstart", this.touchBox.bind(this))
pageEvents.add(this.autoplayLabel, "touchstart", this.touchBox.bind(this))
pageEvents.add(this.autoplayCheckbox, "change", this.toggleAutoplay.bind(this))
pageEvents.add(this.branchSelect, "change", this.branchChange.bind(this))
pageEvents.add(this.branchResetBtn, "click", this.branchReset.bind(this))
pageEvents.add(this.branchResetBtn, ["click", "touchstart"], this.branchReset.bind(this))
this.offsetSlider = new InputSlider(this.offsetDiv, -60, 60, 3)
this.offsetSlider.onchange(this.offsetChange.bind(this))
@ -54,24 +59,38 @@ class Debug{
return this.debugDiv.getElementsByClassName(name)[0]
}
startMove(event){
if(event.which === 1){
if(event.which === 1 || event.type === "touchstart"){
event.stopPropagation()
this.moving = {
x: event.offsetX,
y: event.offsetY
}
var divPos = this.debugDiv.getBoundingClientRect()
var click = event.type === "touchstart" ? event.changedTouches[0] : event
var x = click.pageX - divPos.left
var y = click.pageY - divPos.top
this.moving = {x: x, y: y}
}
}
onMove(event){
if(this.moving){
var x = event.clientX - this.moving.x
var y = event.clientY - this.moving.y
var click = event.type === "touchmove" ? event.changedTouches[0] : event
var x = click.clientX - this.moving.x
var y = click.clientY - this.moving.y
this.moveTo(x, y)
}
}
stopMove(event){
var x = event.clientX - this.moving.x
var y = event.clientY - this.moving.y
if(!event || event.type === "resize"){
var divPos = this.debugDiv.getBoundingClientRect()
var x = divPos.left
var y = divPos.top
}else{
var click = event.type === "touchstart" || event.type === "touchend" ? event.changedTouches[0] : event
if(event.type == "blur"){
var x = this.moving.x
var y = this.moving.y
}else{
var x = click.clientX - this.moving.x
var y = click.clientY - this.moving.y
}
}
var w = this.debugDiv.offsetWidth
var h = this.debugDiv.offsetHeight
if(x + w > innerWidth){
@ -95,6 +114,7 @@ class Debug{
restore(){
debugObj.state = "open"
this.debugDiv.style.display = ""
this.stopMove()
}
minimise(){
debugObj.state = "minimised"
@ -156,6 +176,7 @@ class Debug{
this.branchHideDiv.style.display = ""
this.controller = null
}
this.stopMove()
}
offsetChange(value, noRestart){
if(this.controller){
@ -197,10 +218,12 @@ class Debug{
this.controller.restartSong()
}
}
toggleAutoplay(){
toggleAutoplay(event){
if(this.controller){
this.controller.autoPlayEnabled = this.autoplayCheckbox.checked
if(!this.controller.autoPlayEnabled){
if(this.controller.autoPlayEnabled){
this.controller.saveScore = false
}else{
var keyboard = debugObj.controller.keyboard
keyboard.setKey(false, "don_l")
keyboard.setKey(false, "don_r")
@ -229,21 +252,30 @@ class Debug{
this.branchSelect.value = "auto"
this.branchChange(null, noRestart)
}
touchBox(event){
event.currentTarget.click()
}
clean(){
this.offsetSlider.clean()
this.measureNumSlider.clean()
this.volumeSlider.clean()
pageEvents.remove(window, ["mousedown", "mouseup", "blur"])
pageEvents.remove(window, ["mousedown", "mouseup", "touchstart", "touchend", "blur", "resize"], this.windowSymbol)
pageEvents.mouseRemove(this)
pageEvents.remove(this.titleDiv, "mousedown")
pageEvents.remove(this.title, "mousedown")
pageEvents.remove(this.minimiseDiv, "click")
pageEvents.remove(this.restartBtn, "click")
pageEvents.remove(this.exitBtn, "click")
pageEvents.remove(window, "touchmove")
pageEvents.remove(this.titleDiv, ["mousedown", "touchstart"])
pageEvents.remove(this.minimiseDiv, ["click", "touchstart"])
pageEvents.remove(this.restartBtn, ["click", "touchstart"])
pageEvents.remove(this.exitBtn, ["click", "touchstart"])
pageEvents.remove(this.restartLabel, "touchstart")
pageEvents.remove(this.autoplayLabel, "touchstart")
pageEvents.remove(this.autoplayCheckbox, "change")
pageEvents.remove(this.branchSelect, "change")
pageEvents.remove(this.branchResetBtn, "click")
pageEvents.remove(this.branchResetBtn, ["click", "touchstart"])
delete this.offsetSlider
delete this.measureNumSlider
delete this.volumeSlider
delete this.titleDiv
delete this.minimiseDiv
delete this.offsetDiv
@ -259,6 +291,7 @@ class Debug{
delete this.restartBtn
delete this.exitBtn
delete this.controller
delete this.windowSymbol
debugObj.state = "closed"
debugObj.debug = null
@ -281,10 +314,22 @@ class InputSlider{
this.value = null
this.defaultValue = null
this.callbacks = []
this.touchEnd = []
this.windowSymbol = Symbol()
pageEvents.add(this.input, ["touchstart", "touchend"], event => {
event.stopPropagation()
})
pageEvents.add(window, ["mouseup", "touchstart", "touchend", "blur"], event => {
if(event.type !== "touchstart"){
this.touchEnd.forEach(func => func(event))
}else if(event.target !== this.input){
this.input.blur()
}
}, this.windowSymbol)
pageEvents.add(this.plus, "click", this.add.bind(this))
pageEvents.add(this.minus, "click", this.subtract.bind(this))
pageEvents.add(this.reset, "click", this.resetValue.bind(this))
this.addTouchRepeat(this.plus, this.add.bind(this))
this.addTouchRepeat(this.minus, this.subtract.bind(this))
this.addTouch(this.reset, this.resetValue.bind(this))
pageEvents.add(this.input, "change", this.manualSet.bind(this))
pageEvents.add(this.input, "keydown", this.captureKeys.bind(this))
}
@ -364,15 +409,49 @@ class InputSlider{
captureKeys(event){
event.stopPropagation()
}
addTouch(element, callback){
pageEvents.add(element, ["mousedown", "touchstart"], event => {
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
callback(event)
})
}
addTouchRepeat(element, callback){
this.addTouch(element, event => {
var active = true
var func = () => {
active = false
this.touchEnd.splice(this.touchEnd.indexOf(func), 1)
}
this.touchEnd.push(func)
var repeat = delay => {
if(active && this.touchEnd){
callback(event)
setTimeout(() => repeat(50), delay)
}
}
repeat(400)
})
}
removeTouch(element){
pageEvents.remove(element, ["mousedown", "touchstart"])
}
clean(){
pageEvents.remove(this.plus, "click")
pageEvents.remove(this.minus, "click")
pageEvents.remove(this.reset, "click")
pageEvents.remove(this.input, ["change", "keydown"])
this.removeTouch(this.plus)
this.removeTouch(this.minus)
this.removeTouch(this.reset)
pageEvents.remove(this.input, ["touchstart", "touchend"])
pageEvents.remove(window, ["mouseup", "touchstart", "touchend", "blur"], this.windowSymbol)
pageEvents.remove(this.input, ["touchstart", "change", "keydown"])
delete this.input
delete this.reset
delete this.plus
delete this.minus
delete this.windowSymbol
delete this.touchEnd
}
}

View File

@ -811,7 +811,7 @@ class Game{
offsets.push(offset)
progress.hit++
progress.last = current
this.globalScore.gauge = 100 / (progress.requirement / progress.hit)
this.globalScore.gauge = 10000 / (progress.requirement / progress.hit)
}
}
calibrationReset(to, togglePause){

View File

@ -13,6 +13,7 @@ class GameRules{
case "hard":
case "oni":
case "ura":
default:
this.good = 3 / 2 * frame
this.ok = 9 / 2 * frame
this.bad = 13 / 2 * frame
@ -30,6 +31,9 @@ class GameRules{
case "ura":
this.gaugeClear = 40 / 50
break
default:
this.gaugeClear = 51 / 50
break
}
this.daiLeniency = 2 * frame
@ -61,8 +65,10 @@ class GameRules{
}
return {good: good, ok: ok, bad: bad}
}
gaugePercent(gauge){
return Math.floor(gauge / 200) / 50
}
clearReached(gauge){
var gaugePercent = Math.round(gauge / 200) / 50
return gaugePercent >= this.gaugeClear
return this.gaugePercent(gauge) >= this.gaugeClear
}
}

View File

@ -269,7 +269,7 @@
songObj.subtitle_lang = subtitleLangArray.join("\n")
}
if(!songObj.category){
songObj.category = category || this.getCategory(file)
songObj.category = category || this.getCategory(file, [songTitle || songObj.title, file.name.slice(0, file.name.lastIndexOf("."))])
}
if(songObj.stars.length !== 0){
this.songs[index] = songObj
@ -277,7 +277,7 @@
var hash = md5.base64(event.target.result).slice(0, -2)
songObj.hash = hash
scoreStorage.songTitles[songObj.title] = hash
var score = scoreStorage.get(hash)
var score = scoreStorage.get(hash, false, true)
if(score){
score.title = songObj.title
}
@ -307,7 +307,7 @@
music: this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] || "muted"
}
var filename = file.name.slice(0, file.name.lastIndexOf("."))
var title = osu.metadata.TitleUnicode || osu.metadata.Title
var title = osu.metadata.TitleUnicode || osu.metadata.Title || file.name.slice(0, file.name.lastIndexOf("."))
if(title){
var suffix = ""
var matches = filename.match(/\[.+?\]$/)
@ -320,11 +320,11 @@
songObj.title = filename
}
this.songs[index] = songObj
songObj.category = category || this.getCategory(file)
songObj.category = category || this.getCategory(file, [osu.metadata.TitleUnicode, osu.metadata.Title, file.name.slice(0, file.name.lastIndexOf("."))])
var hash = md5.base64(event.target.result).slice(0, -2)
songObj.hash = hash
scoreStorage.songTitles[songObj.title] = hash
var score = scoreStorage.get(hash)
var score = scoreStorage.get(hash, false, true)
if(score){
score.title = songObj.title
}
@ -394,12 +394,21 @@
return name.slice(0, name.lastIndexOf("."))
}
getCategory(file){
getCategory(file, exclude){
var path = file.webkitRelativePath.toLowerCase().split("/")
for(var i = path.length - 2; i >= 0; i--){
for(var cat in this.categories){
if(path[i].indexOf(cat) !== -1){
return this.categories[cat]
var hasTitle = false
for(var j in exclude){
if(path[i].indexOf(exclude[j].toLowerCase()) !== -1){
hasTitle = true
break
}
}
if(!hasTitle){
for(var cat in this.categories){
if(path[i].indexOf(cat) !== -1){
return this.categories[cat]
}
}
}
}

View File

@ -20,6 +20,7 @@ class Loader{
}
run(){
this.promises = []
this.loaderDiv = document.querySelector("#loader")
this.loaderPercentage = document.querySelector("#loader .percentage")
this.loaderProgress = document.querySelector("#loader .progress")
@ -213,7 +214,7 @@ class Loader{
song.hash = song.title
}
scoreStorage.songTitles[song.title] = song.hash
var score = scoreStorage.get(song.hash)
var score = scoreStorage.get(song.hash, false, true)
if(score){
score.title = song.title
}
@ -248,12 +249,38 @@ class Loader{
return name.slice(0, name.lastIndexOf("."))
}
errorMsg(error){
if(Array.isArray(error) && error[1] instanceof HTMLElement){
error = error[0] + ": " + error[1].outerHTML
}
console.error(error)
pageEvents.send("loader-error", error)
this.error = true
this.loaderPercentage.appendChild(document.createElement("br"))
this.loaderPercentage.appendChild(document.createTextNode("An error occurred, please refresh"))
this.clean()
if(!this.error){
this.error = true
this.loaderDiv.classList.add("loaderError")
if(typeof allStrings === "object"){
var lang = localStorage.lang
if(!lang){
var userLang = navigator.languages.slice()
userLang.unshift(navigator.language)
for(var i in userLang){
for(var j in allStrings){
if(allStrings[j].regex.test(userLang[i])){
lang = j
}
}
}
}
if(!lang){
lang = "en"
}
var errorOccured = allStrings[lang].errorOccured
}else{
var errorOccured = "An error occurred, please refresh"
}
this.loaderPercentage.appendChild(document.createElement("br"))
this.loaderPercentage.appendChild(document.createTextNode(errorOccured))
this.clean()
}
}
assetLoaded(){
if(!this.error){

View File

@ -160,7 +160,7 @@ class LoadSong{
console.error(error)
pageEvents.send("load-song-error", error)
errorMessage(new Error(error).stack)
alert("An error occurred, please refresh")
alert(strings.errorOccured)
})
}
loadSongBg(){

View File

@ -102,7 +102,7 @@ pageEvents.add(versionDiv, ["click", "touchend"], event => {
resizeRoot()
setInterval(resizeRoot, 100)
pageEvents.keyAdd(debugObj, "all", "down", event => {
if((event.keyCode === 186 || event.keyCode === 59) && event.ctrlKey && event.shiftKey && !event.altKey){
if((event.keyCode === 186 || event.keyCode === 59) && event.ctrlKey && (event.shiftKey || event.altKey)){
// Semicolon
if(debugObj.state === "open"){
debugObj.debug.minimise()

View File

@ -11,60 +11,62 @@ class PageEvents{
this.add(window, "blur", this.blurEvent.bind(this))
this.kbd = []
}
add(target, type, callback){
add(target, type, callback, symbol){
if(Array.isArray(type)){
type.forEach(type => this.add(target, type, callback))
type.forEach(type => this.add(target, type, callback, symbol))
return
}
this.remove(target, type)
var addedEvent = this.allEvents.get(target)
var addedEvent = this.allEvents.get(symbol || target)
if(!addedEvent){
addedEvent = new Map()
this.allEvents.set(target, addedEvent)
this.allEvents.set(symbol || target, addedEvent)
}
addedEvent.set(type, callback)
return target.addEventListener(type, callback)
}
remove(target, type){
remove(target, type, symbol){
if(Array.isArray(type)){
type.forEach(type => this.remove(target, type))
type.forEach(type => this.remove(target, type, symbol))
return
}
var addedEvent = this.allEvents.get(target)
var addedEvent = this.allEvents.get(symbol || target)
if(addedEvent){
var callback = addedEvent.get(type)
if(callback){
target.removeEventListener(type, callback)
addedEvent.delete(type)
if(addedEvent.size == 0){
return this.allEvents.delete(target)
return this.allEvents.delete(symbol || target)
}
}
}
}
once(target, type){
once(target, type, symbol){
return new Promise(resolve => {
this.add(target, type, event => {
this.remove(target, type)
return resolve(event)
})
}, symbol)
})
}
race(){
var symbols = []
var target = arguments[0]
return new Promise(resolve => {
for(var i = 1;i < arguments.length; i++){
symbols[i] = Symbol()
let type = arguments[i]
this.add(target, type, event => {
resolve({
type: type,
event: event
})
})
}, symbols[i])
}
}).then(response => {
for(var i = 1;i < arguments.length; i++){
this.remove(target, arguments[i])
this.remove(target, arguments[i], symbols[i])
}
return response
})

View File

@ -149,6 +149,7 @@
var circles = []
var circleID = 0
var regexAZ = /[A-Z]/
var regexSpace = /\s/
var isAllDon = (note_chain, start_pos) => {
for (var i = start_pos; i < note_chain.length; ++i) {
var note = note_chain[i];
@ -470,7 +471,7 @@
bpm: bpm,
scroll: scroll
})
}else{
}else if(!regexSpace.test(symbol)){
error = true
}
break

View File

@ -63,7 +63,8 @@ class Scoresheet{
assets.sounds["v_results"].play()
assets.sounds["bgm_result"].playLoop(3, false, 0, 0.847, 17.689)
if(p2.session){
this.session = p2.session
if(this.session){
if(p2.getMessage("songsel")){
this.toSongsel(true)
}
@ -324,14 +325,15 @@ class Scoresheet{
var elapsed = 0
}
var gaugePercent = Math.round(this.results.gauge / 200) / 50
var gaugeClear = [this.controller.game.rules.gaugeClear]
var rules = this.controller.game.rules
var gaugePercent = rules.gaugePercent(this.results.gauge)
var gaugeClear = [rules.gaugeClear]
if(players === 2){
gaugeClear.push(this.controller.syncWith.game.rules.gaugeClear)
}
var failedOffset = gaugePercent >= gaugeClear[0] ? 0 : -2000
if(players === 2){
var gauge2 = Math.round(p2.results.gauge / 200) / 50
var gauge2 = this.controller.syncWith.game.rules.gaugePercent(p2.results.gauge)
if(gauge2 > gaugePercent && failedOffset !== 0 && gauge2 >= gaugeClear[1]){
failedOffset = 0
}
@ -343,7 +345,8 @@ class Scoresheet{
if(p === 1){
results = p2.results
}
var resultGauge = Math.round(results.gauge / 200) / 50
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules
var resultGauge = playerRules.gaugePercent(results.gauge)
var clear = resultGauge >= gaugeClear[p]
if(p === 1 || !this.multiplayer && clear){
ctx.translate(0, 290)
@ -368,8 +371,8 @@ class Scoresheet{
if(elapsed >= 0){
if(this.state.hasPointer === 0){
this.state.hasPointer = 1
if(!this.state.pointerLocked && !p2.session){
this.canvas.style.cursor = "pointer"
if(!this.state.pointerLocked){
this.canvas.style.cursor = this.session ? "" : "pointer"
}
}
ctx.save()
@ -578,7 +581,7 @@ class Scoresheet{
if(this.tetsuoHanaClass){
this.tetsuoHana.classList.remove(this.tetsuoHanaClass)
}
this.tetsuoHanaClass = this.controller.game.rules.clearReached(this.results.gauge) ? "dance" : "failed"
this.tetsuoHanaClass = rules.clearReached(this.results.gauge) ? "dance" : "failed"
this.tetsuoHana.classList.add(this.tetsuoHanaClass)
}
}
@ -597,26 +600,27 @@ class Scoresheet{
results = p2.results
ctx.translate(0, p2Offset)
}
var gaugePercent = Math.round(results.gauge / 200) / 50
var gaugePercent = rules.gaugePercent(results.gauge)
var w = 712
this.draw.gauge({
ctx: ctx,
x: 558 + w,
y: 116,
y: p === 1 ? 124 : 116,
clear: gaugeClear[p],
percentage: gaugePercent,
font: this.font,
scale: w / 788,
scoresheet: true,
blue: p === 1
blue: p === 1,
multiplayer: p === 1
})
var rules = p === 0 ? this.controller.game.rules : this.controller.syncWith.game.rules
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules
this.draw.soul({
ctx: ctx,
x: 1215,
y: 144,
scale: 36 / 42,
cleared: rules.clearReached(results.gauge)
cleared: playerRules.clearReached(results.gauge)
})
}
})
@ -634,8 +638,8 @@ class Scoresheet{
results = p2.results
}
var crownType = null
var rules = p === 0 ? this.controller.game.rules : this.controller.syncWith.game.rules
if(rules.clearReached(results.gauge)){
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules
if(playerRules.clearReached(results.gauge)){
crownType = results.bad === "0" ? "gold" : "silver"
}
if(crownType !== null){
@ -796,9 +800,13 @@ class Scoresheet{
ctx.restore()
}
if(p2.session && !this.state.scoreNext && this.state.screen === "scoresShown" && ms - this.state.screenMS >= 10000){
if(this.session && !this.state.scoreNext && this.state.screen === "scoresShown" && ms - this.state.screenMS >= 10000){
this.state.scoreNext = true
p2.send("songsel")
if(p2.session){
p2.send("songsel")
}else{
this.toSongsel(true)
}
}
if(this.state.screen === "fadeOut"){
@ -861,7 +869,7 @@ class Scoresheet{
}
saveScore(){
if(!this.controller.autoPlayEnabled){
if(this.controller.saveScore){
if(this.resultsObj.points < 0){
this.resultsObj.points = 0
}
@ -903,7 +911,7 @@ class Scoresheet{
if(this.multiplayer !== 2 && this.touchEnabled){
pageEvents.remove(document.getElementById("touch-full-btn"), "touchend")
}
if(p2.session){
if(this.session){
pageEvents.remove(p2, "message")
}
if(!this.multiplayer){

View File

@ -167,9 +167,29 @@ class SettingsView{
this.viewOuter.classList.add("touch-enabled")
}
this.touchEnd = []
pageEvents.add(this.viewOuter, ["mouseup", "touchend"], event => {
this.touchEnd.forEach(func => func(event))
})
this.windowSymbol = Symbol()
this.touchMove = {
active: false,
x: 0,
y: 0
}
pageEvents.add(window, ["mouseup", "touchstart", "touchmove", "touchend", "blur"], event => {
var move = this.touchMove
if(event.type === "touchstart"){
var cursor = event.changedTouches[0]
move.active = false
move.x = cursor.pageX
move.y = cursor.pageY
}else if(event.type === "touchmove"){
var cursor = event.changedTouches[0]
if (Math.abs(move.x - cursor.pageX) > 10 || Math.abs(move.y - cursor.pageY) > 10){
move.active = true
}
}else{
this.touchEnd.forEach(func => func(event))
move.active = false
}
}, this.windowSymbol)
var gamepadEnabled = false
if("getGamepads" in navigator){
@ -234,7 +254,7 @@ class SettingsView{
this.selected = this.items.length
settingBox.classList.add("selected")
}
this.addTouch(settingBox, event => this.setValue(i))
this.addTouchEnd(settingBox, event => this.setValue(i))
this.items.push({
id: i,
settingBox: settingBox,
@ -365,9 +385,10 @@ class SettingsView{
getElement(name){
return loader.screen.getElementsByClassName(name)[0]
}
addTouch(element, callback){
pageEvents.add(element, ["mousedown", "touchstart"], event => {
if(event.type === "touchstart"){
addTouch(element, callback, end){
var touchEvent = end ? "touchend" : "touchstart"
pageEvents.add(element, ["mousedown", touchEvent], event => {
if(event.type === touchEvent){
event.preventDefault()
this.touched = true
}else if(event.which !== 1){
@ -375,9 +396,14 @@ class SettingsView{
}else{
this.touched = false
}
callback(event)
if(event.type !== "touchend" || !this.touchMove.active){
callback(event)
}
})
}
addTouchEnd(element, callback){
this.addTouch(element, callback, true)
}
addTouchRepeat(element, callback){
this.addTouch(element, event => {
var active = true
@ -398,6 +424,9 @@ class SettingsView{
removeTouch(element){
pageEvents.remove(element, ["mousedown", "touchstart"])
}
removeTouchEnd(element){
pageEvents.remove(element, ["mousedown", "touchend"])
}
getValue(name, valueDiv){
var current = settings.items[name]
var value = settings.getItem(name)
@ -879,9 +908,9 @@ class SettingsView{
this.keyboard.clean()
this.gamepad.clean()
assets.sounds["bgm_settings"].stop()
pageEvents.remove(this.viewOuter, ["mouseup", "touchend"])
pageEvents.remove(window, ["mouseup", "touchstart", "touchmove", "touchend", "blur"], this.windowSymbol)
for(var i in this.items){
this.removeTouch(this.items[i].settingBox)
this.removeTouchEnd(this.items[i].settingBox)
}
for(var i in this.latencyItems){
this.removeTouch(this.latencyItems[i].settingBox)
@ -899,6 +928,8 @@ class SettingsView{
this.removeTouch(this.latencySettings)
this.removeTouch(this.latencyDefaultButton)
this.removeTouch(this.latencyEndButton)
delete this.windowSymbol
delete this.touchMove
delete this.viewOuter
delete this.touchEnd
delete this.tutorialTitle

View File

@ -608,18 +608,25 @@ class SongSelect{
this.pointer(false)
}
}
categoryJump(moveBy){
if(this.state.locked === 1){
return
categoryJump(moveBy, fromP2){
if(p2.session && !fromP2){
var ms = this.getMS()
if(!this.state.selLock && ms > this.state.moveMS + 800){
this.state.selLock = true
p2.send("catjump", {
song: this.selectedSong,
move: moveBy
})
}
}else if(this.state.locked !== 1 || fromP2){
this.state.catJump = true
this.state.move = moveBy;
this.state.locked = 1
this.endPreview()
this.playSound("se_jump")
}
this.state.catJump = true
this.state.move = moveBy;
this.state.locked = 1
this.endPreview()
this.playSound("se_jump")
}
moveToDiff(moveBy){
@ -925,7 +932,7 @@ class SongSelect{
}
this.selectableText = ""
}else if(!document.hasFocus()){
}else if(!document.hasFocus() && !p2.session){
this.pointer(false)
return
}else{
@ -1128,7 +1135,15 @@ class SongSelect{
disabled: p2.session && this.songs[index].action && this.songs[index].action !== "random"
})
}
var startFrom
for(var i = this.selectedSong + 1; ; i++){
var _x = winW / 2 + (i - this.selectedSong - 1) * (this.songAsset.width + this.songAsset.marginLeft) + this.songAsset.marginLeft + selectedWidth / 2 + xOffset
if(_x > winW){
startFrom = i - 1
break
}
}
for(var i = startFrom; i > this.selectedSong ; i--){
var highlight = 0
if(i - this.selectedSong === this.state.moveHover){
highlight = 1
@ -1136,9 +1151,6 @@ class SongSelect{
var index = this.mod(this.songs.length, i)
var currentSong = this.songs[index]
var _x = winW / 2 + (i - this.selectedSong - 1) * (this.songAsset.width + this.songAsset.marginLeft) + this.songAsset.marginLeft + selectedWidth / 2 + xOffset
if(_x > winW){
break
}
this.drawClosedSong({
ctx: ctx,
x: _x,
@ -1150,6 +1162,43 @@ class SongSelect{
}
}
var currentSong = this.songs[this.selectedSong]
var highlight = 0
if(!currentSong.stars){
highlight = 2
}
if(this.state.moveHover === 0){
highlight = 1
}
var selectedSkin = this.songSkin.selected
if(screen === "title" || screen === "titleFadeIn" || this.state.locked === 3){
selectedSkin = currentSong.skin
highlight = 2
}else if(songSelMoving){
selectedSkin = currentSong.skin
highlight = 0
}
var selectedHeight = this.songAsset.height
if(screen === "difficulty"){
selectedWidth = this.songAsset.fullWidth
selectedHeight = this.songAsset.fullHeight
highlight = 0
}
if(this.currentSongTitle !== currentSong.title){
this.currentSongTitle = currentSong.title
this.currentSongCache.clear()
}
if(ms > this.state.screenMS + 2000 && selectedWidth === this.songAsset.width){
this.drawSongCrown({
ctx: ctx,
song: currentSong,
x: winW / 2 - selectedWidth / 2 + xOffset,
y: songTop + this.songAsset.height - selectedHeight
})
}
if(screen === "title" || screen === "titleFadeIn" || screen === "song"){
var textW = strings.id === "en" ? 350 : 280
this.selectTextCache.get({
@ -1230,35 +1279,7 @@ class SongSelect{
})
}
var currentSong = this.songs[this.selectedSong]
var highlight = 0
if(!currentSong.stars){
highlight = 2
}
if(this.state.moveHover === 0){
highlight = 1
}
var selectedSkin = this.songSkin.selected
if(screen === "title" || screen === "titleFadeIn" || this.state.locked === 3){
selectedSkin = currentSong.skin
highlight = 2
}else if(songSelMoving){
selectedSkin = currentSong.skin
highlight = 0
}
var selectedHeight = this.songAsset.height
if(screen === "difficulty"){
selectedWidth = this.songAsset.fullWidth
selectedHeight = this.songAsset.fullHeight
highlight = 0
}
if(this.currentSongTitle !== currentSong.title){
this.currentSongTitle = currentSong.title
this.currentSongCache.clear()
}
if(selectedWidth === this.songAsset.width){
if(ms <= this.state.screenMS + 2000 && selectedWidth === this.songAsset.width){
this.drawSongCrown({
ctx: ctx,
song: currentSong,
@ -1398,7 +1419,7 @@ class SongSelect{
}
var drawDifficulty = (ctx, i, currentUra) => {
if(currentSong.stars[i] || currentUra){
var score = scoreStorage.get(currentSong.hash)
var score = scoreStorage.get(currentSong.hash, false, true)
var crownDiff = currentUra ? "ura" : this.difficultyId[i]
var crownType = ""
if(score && score[crownDiff]){
@ -1585,6 +1606,8 @@ class SongSelect{
alphaFade = this.draw.easeIn((fade - 0.45) * 20)
}
this.draw.alpha(alphaFade, ctx, ctx => {
ctx.fillStyle = this.songSkin.selected.background
ctx.fillRect(x + 7 + i * 60, y + 60, 52, 352)
drawDifficulty(ctx, i, true)
}, winW, winH)
}else{
@ -1990,7 +2013,7 @@ class SongSelect{
drawSongCrown(config){
if(!config.song.action && config.song.hash){
var ctx = config.ctx
var score = scoreStorage.get(config.song.hash)
var score = scoreStorage.get(config.song.hash, false, true)
for(var i = this.difficultyId.length; i--;){
var diff = this.difficultyId[i]
if(!score){
@ -2140,13 +2163,19 @@ class SongSelect{
onsongsel(response){
if(response && response.value){
var selected = false
if("selected" in response.value){
if(response.type === "songsel" && "selected" in response.value){
selected = response.value.selected
}
if("song" in response.value){
var song = +response.value.song
if(song >= 0 && song < this.songs.length){
if(!selected){
if(response.type === "catjump"){
var moveBy = response.value.move
if(moveBy === -1 || moveBy === 1){
this.selectedSong = song
this.categoryJump(moveBy, true)
}
}else if(!selected){
this.state.locked = true
if(this.state.screen === "difficulty"){
this.toSongSelect(true)
@ -2174,6 +2203,16 @@ class SongSelect{
}
}
}
oncatjump(response){
if(response && response.value){
if("song" in response.value){
var song = +response.value.song
if(song >= 0 && song < this.songs.length){
this.state.locked = true
}
}
}
}
startP2(){
this.onusers(p2.getMessage("users"))
if(p2.session){
@ -2183,7 +2222,7 @@ class SongSelect{
if(response.type == "users"){
this.onusers(response)
}
if(p2.session && response.type == "songsel"){
if(p2.session && (response.type == "songsel" || response.type == "catjump")){
this.onsongsel(response)
this.state.selLock = false
}

View File

@ -86,6 +86,7 @@
this.maxCombo = "最大コンボ数"
this.drumroll = "連打数"
this.errorOccured = "エラーが発生しました。再読み込みしてください。"
this.tutorial = {
basics: [
"流れてくる音符がワクに重なったらバチで太鼓をたたこう!",
@ -109,7 +110,8 @@
"Gitリポジトリかメールでバグを報告してください。"
],
diagnosticWarning: "以下の端末診断情報も併せて報告してください!",
issueTemplate: "###### 下記の問題を説明してください。 スクリーンショットと診断情報を含めてください。"
issueTemplate: "###### 下記の問題を説明してください。 スクリーンショットと診断情報を含めてください。",
issues: "課題"
}
this.session = {
multiplayerSession: "オンラインセッション",
@ -281,6 +283,7 @@ function StringsEn(){
this.maxCombo = "MAX Combo"
this.drumroll = "Drumroll"
this.errorOccured = "An error occurred, please refresh"
this.tutorial = {
basics: [
"When a note overlaps the frame, that is your cue to hit the drum!",
@ -304,7 +307,8 @@ function StringsEn(){
"You can report bugs either via our Git repository or email."
],
diagnosticWarning: "Be sure to include the following diagnostic data!",
issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information."
issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information.",
issues: "Issues"
}
this.session = {
multiplayerSession: "Multiplayer Session",
@ -426,10 +430,10 @@ function StringsCn(){
this.hard = "困难"
this.oni = "魔王"
this.songBranch = "有谱面分歧"
this.sessionStart = "开始在线会话!"
this.sessionStart = "开始在线会话"
this.sessionEnd = "结束在线会话"
this.loading = "加载中..."
this.waitingForP2 = "Waiting for Another Player..."
this.waitingForP2 = "正在等待对方玩家..."
this.cancel = "取消"
this.note = {
don: "咚",
@ -476,6 +480,7 @@ function StringsCn(){
this.maxCombo = "最多连段数"
this.drumroll = "连打数"
this.errorOccured = "An error occurred, please refresh"
this.tutorial = {
basics: [
"当流动的音符将与框框重叠时就用鼓棒敲打太鼓吧",
@ -499,11 +504,12 @@ function StringsCn(){
"You can report bugs either via our Git repository or email."
],
diagnosticWarning: "Be sure to include the following diagnostic data!",
issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information."
issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information.",
issues: "工单"
}
this.session = {
multiplayerSession: "Multiplayer Session",
linkTutorial: "Share this link with your friend to start playing together! Do not leave this screen while they join.",
multiplayerSession: "在线会话",
linkTutorial: "复制下方地址,给你的朋友即可开始一起游戏!当他们与您联系之前,请不要离开此页面。",
cancel: "取消"
}
this.settings = {
@ -624,7 +630,7 @@ function StringsTw(){
this.sessionStart = "開始多人模式!"
this.sessionEnd = "結束多人模式"
this.loading = "讀取中..."
this.waitingForP2 = "Waiting for Another Player..."
this.waitingForP2 = "正在等待對方玩家..."
this.cancel = "取消"
this.note = {
don: "咚",
@ -671,6 +677,7 @@ function StringsTw(){
this.maxCombo = "最多連段數"
this.drumroll = "連打數"
this.errorOccured = "An error occurred, please refresh"
this.tutorial = {
basics: [
"當流動的音符將與框框重疊時就用鼓棒敲打太鼓吧",
@ -694,11 +701,12 @@ function StringsTw(){
"You can report bugs either via our Git repository or email."
],
diagnosticWarning: "Be sure to include the following diagnostic data!",
issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information."
issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information.",
issues: "問題"
}
this.session = {
multiplayerSession: "Multiplayer Session",
linkTutorial: "Share this link with your friend to start playing together! Do not leave this screen while they join.",
multiplayerSession: "多人模式",
linkTutorial: "複製下方地址,給你的朋友即可開始一起遊戲!當他們與您聯繫之前,請不要離開此頁面。",
cancel: "取消"
}
this.settings = {
@ -866,6 +874,7 @@ function StringsKo(){
this.maxCombo = "최대 콤보 수"
this.drumroll = "연타 횟수"
this.errorOccured = "An error occurred, please refresh"
this.tutorial = {
basics: [
"이동하는 음표가 테두리와 겹쳐졌을 때 북채로 태고를 두드리자!",
@ -889,7 +898,8 @@ function StringsKo(){
"You can report bugs either via our Git repository or email."
],
diagnosticWarning: "Be sure to include the following diagnostic data!",
issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information."
issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information.",
issues: "이슈"
}
this.session = {
multiplayerSession: "Multiplayer Session",

View File

@ -348,7 +348,7 @@
}
var score = this.controller.getGlobalScore()
var gaugePercent = Math.round(score.gauge / 200) / 50
var gaugePercent = this.rules.gaugePercent(score.gauge)
if(this.multiplayer === 2){
var scoreImg = "bg_score_p2"

View File

@ -4,8 +4,8 @@
<div class="view-content"></div>
<div id="diag-txt"></div>
<div class="left-buttons">
<div id="link-issues" class="taibtn stroke-sub link-btn" alt="Issues">
<a target="_blank">Issues</a>
<div id="link-issues" class="taibtn stroke-sub link-btn">
<a target="_blank"></a>
</div>
<div id="link-email" class="taibtn stroke-sub link-btn">
<a></a>

View File

@ -24,7 +24,7 @@
<div class="music-volume input-slider">
<span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span>
</div>
<label><input class="change-restart" type="checkbox">Restart on change</label>
<label class="change-restart-label"><input class="change-restart" type="checkbox">Restart on change</label>
<label class="autoplay-label"><input class="autoplay" type="checkbox">Auto play</label>
<div class="bottom-btns">
<div class="restart-btn">Restart song</div>

View File

@ -262,7 +262,7 @@ async def connection(ws, path):
elif action == "songsel":
# Session song selection
if "other_user" in user and "ws" in user["other_user"]:
if type == "songsel":
if type == "songsel" or type == "catjump":
# Change song select position
if user["other_user"]["action"] == "songsel":
sent_msg = msgobj(type, value)
@ -336,7 +336,26 @@ async def connection(ws, path):
port = int(sys.argv[1]) if len(sys.argv) > 1 else 34802
print('Starting server on port %d' % port)
asyncio.get_event_loop().run_until_complete(
loop = asyncio.get_event_loop()
tasks = asyncio.gather(
websockets.serve(connection, "localhost", port)
)
asyncio.get_event_loop().run_forever()
try:
loop.run_until_complete(tasks)
loop.run_forever()
except KeyboardInterrupt:
print("Stopping server")
def shutdown_exception_handler(loop, context):
if "exception" not in context or not isinstance(context["exception"], asyncio.CancelledError):
loop.default_exception_handler(context)
loop.set_exception_handler(shutdown_exception_handler)
tasks = asyncio.gather(*asyncio.all_tasks(loop=loop), loop=loop, return_exceptions=True)
tasks.add_done_callback(lambda t: loop.stop())
tasks.cancel()
while not tasks.done() and not loop.is_closed():
loop.run_forever()
finally:
if hasattr(loop, "shutdown_asyncgens"):
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()