taiko-web/public/src/js/parsetja.js
KatieFrogs e43c4afceb Lyrics, search, and other fixes
- #LYRIC
  - Parse #LYRIC commands and apply them to all difficulties that do not have them
  - #LYRIC command now supports branches
  - Fix last #LYRIC at the end of the chart getting ignored
- Fix the glitchy dragging and dropping of files on the custom song importing page
- Fix Ctrl and Shift keys getting stuck on song select when switching tabs with Ctrl(+Shift)+Tab
- Search
  - Fix the search box "random:yes" query to randomize the entire results and not just the first 50
  - Add "all:yes" query to the search box to remove the result limit and display all of the results
  - Fix searching for an invalid query (like "cleared:yes" or ":") unexpectedly returning all the songs
  - Fix pressing Q then jumping to a song through search not unmuting the sound
  - Pressing the search key on mobile will hide the keyboard
  - Fix search tips changing rapidly when the window is resized
- Use comments instead of `######` in the issue template so that the warning does not appear in the issue
- Fix TJA MAKER: url between angle brackets not working
- Add a check for Class field declarations in the browser support warning
- Fix gpicker getting stuck if a network error occurs
- Fix not being able to replace some assets using a "taiko-web assets" folder
- Fix selectable song title not being aligned with the game if the game window is too wide
- Allow plugin developers to use the "select" type for the settings options
  - It uses "options" array and "options_lang" object
- Fix plugins not getting removed from the plugin list on syntax error
- Fix error messages not working if a default plugin is broken
- Fix the start of default plugins not stopping the page from loading on error
- Fix not being able to scroll the plugins screen on mobile
2022-07-15 16:00:43 +02:00

668 lines
17 KiB
JavaScript

class ParseTja{
constructor(...args){
this.init(...args)
}
init(file, difficulty, stars, offset, metaOnly){
this.data = []
for(let line of file){
var indexComment = line.indexOf("//")
if(indexComment !== -1 && !line.trim().toLowerCase().startsWith("maker:")){
line = line.slice(0, indexComment).trim()
}else{
line = line.trim()
}
if(line !== ""){
this.data.push(line)
}
}
this.difficulty = difficulty
this.stars = stars
this.offset = (offset || 0) * -1000
this.soundOffset = 0
this.noteTypes = {
"0": {name: false, txt: false},
"1": {name: "don", txt: strings.note.don},
"2": {name: "ka", txt: strings.note.ka},
"3": {name: "daiDon", txt: strings.note.daiDon},
"4": {name: "daiKa", txt: strings.note.daiKa},
"5": {name: "drumroll", txt: strings.note.drumroll},
"6": {name: "daiDrumroll", txt: strings.note.daiDrumroll},
"7": {name: "balloon", txt: strings.note.balloon},
"8": {name: false, txt: false},
"9": {name: "balloon", txt: strings.note.balloon},
"A": {name: "daiDon", txt: strings.note.daiDon},
"B": {name: "daiKa", txt: strings.note.daiKa}
}
this.noteTypes_ex = strings.ex_note;
this.courseTypes = {
"0": "easy",
"1": "normal",
"2": "hard",
"3": "oni",
"4": "ura",
"edit": "ura"
}
this.metadata = this.parseMetadata()
this.measures = []
this.beatInfo = {}
this.events = []
if(!metaOnly){
this.circles = this.parseCircles(difficulty)
}
}
parseMetadata(){
var metaNumbers = ["bpm", "offset", "demostart", "level", "scoremode", "scorediff"]
var inSong = false
var hasSong = false
var courses = {}
var currentCourse = {}
var courseName = "oni"
for(var lineNum = 0; lineNum < this.data.length; lineNum++){
var line = this.data[lineNum]
if(line.slice(0, 1) === "#"){
var name = line.slice(1).toLowerCase()
if((name === "start" || name === "start p1") && !inSong){
inSong = true
if(!hasSong || name === "start" && courses[courseName] && courses[courseName].startName !== "start"){
hasSong = false
if(!(courseName in courses)){
courses[courseName] = {}
}
courses[courseName].startName = name
for(var opt in currentCourse){
if(opt !== "branch"){
courses[courseName][opt] = currentCourse[opt]
}
}
courses[courseName].start = lineNum + 1
courses[courseName].end = this.data.length
}
}else if(name === "end" && inSong){
inSong = false
if(!hasSong){
hasSong = true
courses[courseName].end = lineNum
}
}else if(name.startsWith("branchstart") && inSong){
courses[courseName].branch = true
}else if(name.startsWith("lyric") && inSong){
courses[courseName].inlineLyrics = true
}
}else if(!inSong){
if(line.indexOf(":") > 0){
var [name, value] = this.split(line, ":")
name = name.toLowerCase().trim()
value = value.trim()
if(name === "course"){
value = value.toLowerCase()
if(value in this.courseTypes){
courseName = this.courseTypes[value]
}else{
courseName = value
}
hasSong = false
}else if(name === "balloon"){
value = value ? value.split(",").map(digit => parseInt(digit)) : []
}else if(this.inArray(name, metaNumbers)){
value = parseFloat(value)
}
else if (name === "scoreinit") {
value = value ? parseFloat(value.split(",")[0]) : 0;
}
currentCourse[name] = value
}
}
}
return courses
}
inArray(string, array){
return array.indexOf(string) >= 0
}
split(string, delimiter){
var index = string.indexOf(delimiter)
if(index < 0){
return [string, ""]
}
return [string.slice(0, index), string.slice(index + delimiter.length)]
}
parseCircles(difficulty, lyricsOnly){
var meta = this.metadata[difficulty] || {}
var ms = (meta.offset || 0) * -1000 + this.offset
var bpm = Math.abs(meta.bpm) || 120
var scroll = 1
var measure = 4
if(!lyricsOnly){
this.beatInfo.beatInterval = 60000 / bpm
}
var gogo = false
var barLine = true
var balloonID = 0
var balloons = meta.balloon || []
var lastDrumroll = false
var branches
var branch = false
var branchObj = {}
var currentBranch = false
var branchSettings = {}
var branchFirstMeasure = false
var sectionBegin = true
var lastBpm = bpm
var lastGogo = gogo
var lyrics
var lyricsIndex = null
var lyricsLine = null
var lyricsCopy = false
var measures = []
var currentMeasure = []
var firstNote = true
var circles = []
var circleID = 0
var events = []
var regexAZ = /[A-Z]/
var regexSpace = /\s/
var regexLinebreak = /\\n/g
var isAllDon = (note_chain, start_pos) => {
for (var i = start_pos; i < note_chain.length; ++i) {
var note = note_chain[i];
if (note && note.type !== "don" && note.type !== "daiDon") {
return false;
}
}
return true;
}
var checkChain = (note_chain, measure_length, is_last) => {
//console.log(note_chain, measure_length, is_last);
/*if (measure_length >= 24) {
for (var note of note_chain) {
note.text = this.noteTypes_ex[note.type][0];
}
} else { */
var alldon_pos = null;
for (var i = 0; i < note_chain.length - (is_last ? 1 : 0); ++i) {
var note = note_chain[i];
if (alldon_pos === null && is_last && isAllDon(note_chain, i)) {
alldon_pos = i;
}
note.text = this.noteTypes_ex[note.type][alldon_pos != null ? (i - alldon_pos) % 2 : 0];
}
//}
}
var pushMeasure = () => {
var note = currentMeasure[0]
if(note){
var speed = note.bpm * note.scroll / 60
}else{
var speed = bpm * scroll / 60
}
if(!lyricsOnly){
measures.push({
ms: ms,
originalMS: ms,
speed: speed,
visible: barLine,
branch: currentBranch,
branchFirst: branchFirstMeasure
})
}
branchFirstMeasure = false
if(currentMeasure.length){
for(var i = 0; i < currentMeasure.length; i++){
var note = currentMeasure[i]
if(firstNote && note.type && note.type !== "event"){
firstNote = false
if(ms < 0){
this.soundOffset = ms
ms = 0
}
}
note.start = ms
if(note.endDrumroll){
note.endDrumroll.endTime = ms
note.endDrumroll.originalEndTime = ms
}
var msPerMeasure = 60000 * measure / note.bpm
ms += msPerMeasure / currentMeasure.length
}
var note_chain = [];
for (var i = 0; i < currentMeasure.length; i++){
var note = currentMeasure[i]
if(!lyricsOnly){
circleID++
var circleObj = new Circle({
id: circleID,
start: note.start,
type: note.type,
txt: note.txt,
speed: note.bpm * note.scroll / 60,
gogoTime: note.gogo,
endTime: note.endTime,
requiredHits: note.requiredHits,
beatMS: 60000 / note.bpm,
branch: currentBranch,
section: note.section
})
if(note.type){
if(note.type === "don" || note.type === "ka" || note.type === "daiDon" || note.type === "daiKa"){
note_chain.push(circleObj)
}else{
if(note_chain.length > 1 && currentMeasure.length >= 8){
checkChain(note_chain, currentMeasure.length, false)
}
note_chain = []
}
if (lastDrumroll === note) {
lastDrumroll = circleObj
}
if(note.type !== "event"){
circles.push(circleObj)
}
}else if(
(currentMeasure.length < 24 ||
currentMeasure[i + 1]
&& !currentMeasure[i + 1].type
) && (currentMeasure.length < 48 ||
currentMeasure[i + 2]
&& !currentMeasure[i + 2].type
&& currentMeasure[i + 3]
&& !currentMeasure[i + 3].type
)
){
if(note_chain.length > 1 && currentMeasure.length >= 8){
checkChain(note_chain, currentMeasure.length, true)
}
note_chain = []
}
if(note.event){
events.push(circleObj)
}
}
var lyricsObj = null
if("lyricsLine" in note){
lyricsObj = {
start: note.start,
text: note.lyricsLine
}
}else if(note.lyricsCopy){
lyricsObj = {
start: note.start,
copy: true
}
}
if(lyricsObj){
if(currentBranch){
lyricsObj.branch = currentBranch.name
}
insertLyrics(lyricsObj)
}
}
if(!lyricsOnly && note_chain.length > 1 && currentMeasure.length >= 8){
checkChain(note_chain, currentMeasure.length, false)
}
}else{
var msPerMeasure = 60000 * measure / bpm
ms += msPerMeasure
}
}
var insertNote = circleObj => {
if(circleObj){
if(bpm !== lastBpm || gogo !== lastGogo){
circleObj.event = true
lastBpm = bpm
lastGogo = gogo
}
if(lyricsLine !== null){
circleObj.lyricsLine = lyricsLine
lyricsLine = null
}else if(lyricsCopy){
circleObj.lyricsCopy = true
}
lyricsCopy = false
currentMeasure.push(circleObj)
}
}
var insertBlankNote = circleObj => {
if(bpm !== lastBpm || gogo !== lastGogo){
insertNote({
type: "event",
bpm: bpm,
scroll: scroll,
gogo: gogo
})
}else if(!circleObj){
var circleObj2 = {
bpm: bpm,
scroll: scroll
}
if(lyricsLine !== null){
circleObj2.lyricsLine = lyricsLine
lyricsLine = null
}else if(lyricsCopy){
circleObj2.lyricsCopy = true
}
lyricsCopy = false
currentMeasure.push(circleObj2)
}
if(circleObj){
if(lyricsLine !== null){
circleObj.lyricsLine = lyricsLine
lyricsLine = null
}else if(lyricsCopy){
circleObj.lyricsCopy = true
}
lyricsCopy = false
currentMeasure.push(circleObj)
}
}
var insertLyrics = obj => {
if(!lyrics){
lyrics = []
}else if(lyricsIndex !== null){
lyrics[lyricsIndex].end = obj.start
}
lyricsIndex = lyrics.length
lyrics.push(obj)
}
for(var lineNum = meta.start; lineNum < meta.end; lineNum++){
var line = this.data[lineNum]
if(line.slice(0, 1) === "#"){
var line = line.slice(1)
var [name, value] = this.split(line, " ")
name = name.toLowerCase()
switch(name){
case "gogostart":
gogo = true
break
case "gogoend":
gogo = false
break
case "bpmchange":
bpm = parseFloat(value) || bpm
break
case "scroll":
scroll = Math.abs(parseFloat(value)) || scroll
break
case "measure":
var [numerator, denominator] = value.split("/")
measure = numerator / denominator * 4 || measure
break
case "delay":
ms += (parseFloat(value) || 0) * 1000
break
case "barlineon":
barLine = true
break
case "barlineoff":
barLine = false
break
case "branchstart":
branch = true
currentBranch = false
branchFirstMeasure = true
branchSettings = {
ms: ms,
gogo: gogo,
bpm: bpm,
scroll: scroll,
sectionBegin: sectionBegin,
lyricsCopy: !!lyrics
}
if(lyrics && lyricsIndex !== null){
var line = lyrics[lyricsIndex]
line.end = ms
}
lyricsIndex = null
value = value.split(",")
if(!branches){
branches = []
}
var req = {
advanced: parseFloat(value[1]) || 0,
master: parseFloat(value[2]) || 0
}
if(req.advanced > 0){
var active = req.master > 0 ? "normal" : "master"
}else{
var active = req.master > 0 ? "advanced" : "master"
}
branchObj = {
ms: ms,
originalMS: ms,
active: active,
type: value[0].trim().toLowerCase() === "r" ? "drumroll" : "accuracy",
requirement: req
}
branches.push(branchObj)
if(measures.length === 1 && branchObj.type === "drumroll"){
for(var i = circles.length; i--;){
var circle = circles[i]
if(circle.endTime && (circle.type === "drumroll" || circle.type === "daiDrumroll" || circle.type === "balloon")){
measures.push({
ms: circle.endTime,
originalMS: circle.endTime,
speed: circle.bpm * circle.scroll / 60,
visible: false,
branch: circle.branch
})
break
}
}
}
if(measures.length !== 0){
measures[measures.length - 1].nextBranch = branchObj
}
break
case "branchend":
branch = false
currentBranch = false
lyricsCopy = lyricsCopy || !!lyrics
break
case "section":
sectionBegin = true
if(branch && !currentBranch){
branchSettings.sectionBegin = true
}
break
case "n": case "e": case "m":
if(!branch){
break
}
if(lyrics){
if(lyricsIndex !== null){
var line = lyrics[lyricsIndex]
line.end = ms
}
lyricsIndex = null
}
ms = branchSettings.ms
gogo = branchSettings.gogo
bpm = branchSettings.bpm
scroll = branchSettings.scroll
sectionBegin = branchSettings.sectionBegin
lyricsCopy = branchSettings.lyricsCopy
branchFirstMeasure = true
var branchName = name === "m" ? "master" : (name === "e" ? "advanced" : "normal")
currentBranch = {
name: branchName,
active: branchName === branchObj.active
}
branchObj[branchName] = currentBranch
break
case "lyric":
lyricsLine = value.replace(regexLinebreak, "\n").trim()
break
}
}else{
var string = line.toUpperCase().split("")
for(let symbol of string){
var error = false
switch(symbol){
case "0":
insertBlankNote()
break
case "1": case "2": case "3": case "4": case "A": case "B":
var type = this.noteTypes[symbol]
var circleObj = {
type: type.name,
txt: type.txt,
gogo: gogo,
bpm: bpm,
scroll: scroll,
section: sectionBegin
}
sectionBegin = false
if(lastDrumroll){
circleObj.endDrumroll = lastDrumroll
lastDrumroll = false
}
insertNote(circleObj)
break
case "5": case "6": case "7": case "9":
var type = this.noteTypes[symbol]
var circleObj = {
type: type.name,
txt: type.txt,
gogo: gogo,
bpm: bpm,
scroll: scroll,
section: sectionBegin
}
sectionBegin = false
if(lastDrumroll){
if(symbol === "9"){
insertNote({
endDrumroll: lastDrumroll,
gogo: gogo,
bpm: bpm,
scroll: scroll,
section: sectionBegin
})
sectionBegin = false
lastDrumroll = false
}else{
insertBlankNote()
}
break
}
if(symbol === "7" || symbol === "9"){
var hits = balloons[balloonID]
if(!hits || hits < 1){
hits = 1
}
circleObj.requiredHits = hits
balloonID++
}
lastDrumroll = circleObj
insertNote(circleObj)
break
case "8":
if(lastDrumroll){
insertNote({
endDrumroll: lastDrumroll,
gogo: gogo,
bpm: bpm,
scroll: scroll,
section: sectionBegin
})
sectionBegin = false
lastDrumroll = false
}else{
insertBlankNote()
}
break
case ",":
if(currentMeasure.length === 0 && (bpm !== lastBpm || gogo !== lastGogo || lyricsLine !== null)){
insertBlankNote()
}
pushMeasure()
currentMeasure = []
break
default:
if(regexAZ.test(symbol)){
insertBlankNote()
}else if(!regexSpace.test(symbol)){
error = true
}
break
}
if(error){
break
}
}
}
}
if(lastDrumroll){
lastDrumroll.endTime = ms
lastDrumroll.originalEndTime = ms
}
if(lyricsLine !== null){
insertLyrics({
start: ms,
text: lyricsLine
})
}
pushMeasure()
if(!lyricsOnly){
if(branches){
circles.sort((a, b) => a.ms > b.ms ? 1 : -1)
measures.sort((a, b) => a.ms > b.ms ? 1 : -1)
circles.forEach((circle, i) => circle.id = i + 1)
}
this.measures = measures
this.events = events
this.branches = branches
this.scoreinit = meta.scoreinit
this.scorediff = meta.scorediff
if(this.scoreinit && this.scorediff){
this.scoremode = meta.scoremode || 1
}else{
this.scoremode = meta.scoremode || 2
var autoscore = new AutoScore(difficulty, this.stars, this.scoremode, circles)
this.scoreinit = autoscore.ScoreInit
this.scorediff = autoscore.ScoreDiff
}
}
if(lyrics && lyricsIndex !== null){
var line = lyrics[lyricsIndex]
line.end = Math.max(ms, line.start) + 5000
}
if(lyrics){
this.lyrics = lyrics
}else if(!lyricsOnly){
for(var courseName in this.metadata){
if(this.metadata[courseName].inlineLyrics){
this.parseCircles(courseName, true)
break
}
}
}
return circles
}
}