Merge pull request #52 from LoveEevee/add-tja-support

Add .tja file support
This commit is contained in:
Bui 2018-10-11 08:02:39 +01:00 committed by GitHub
commit 1fdde6d4a8
11 changed files with 453 additions and 86 deletions

3
.gitignore vendored
View File

@ -44,4 +44,5 @@ Temporary Items
public/songs
public/api
taiko.db
version.json
version.json
public/index.html

View File

@ -10,9 +10,10 @@ Still in developement. Works best with Chrome.
Create a SQLite databse named `taiko.db` with the following schema:
CREATE TABLE "songs" ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `title_en` TEXT, `easy` INTEGER, `normal` INTEGER, `hard` INTEGER, `oni` INTEGER, `enabled` INTEGER NOT NULL, `category` INTEGER )
CREATE TABLE "songs" ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `title_en` TEXT, `easy` INTEGER, `normal` INTEGER, `hard` INTEGER, `oni` INTEGER, `enabled` INTEGER NOT NULL, `category` INTEGER, `type` TEXT , `offset` REAL NOT NULL )
CREATE TABLE "categories" ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `title_en` TEXT NOT NULL )
When inserting rows, leave any difficulty columns as NULL if you don't intend to add notecharts for them.
When inserting song rows, leave any difficulty columns as NULL if you don't intend to add notecharts for them.
Each song's data is contained within a directory under `public/songs/`. For example:

41
app.py
View File

@ -60,6 +60,26 @@ def get_osu_key(osu, section, key, default=None):
return default
def get_tja_preview(tja):
tja_lines = open(tja, 'r').read().replace('\x00', '').split('\n')
for line in tja_lines:
line = line.strip()
if ':' in line:
name, value = line.split(':', 1)
if name.lower() == 'demostart':
value = value.strip()
try:
value = float(value)
except ValueError:
pass
else:
return int(value * 1000)
elif line.lower() == '#start':
break
return 0
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
@ -85,12 +105,19 @@ def route_api_songs():
categories[cat[0]] = {'title': cat[1], 'title_en': cat[2]}
songs_out = []
for song in songs:
osus = [osu for osu in os.listdir('public/songs/%s' % song[0]) if osu in ['easy.osu', 'normal.osu', 'hard.osu', 'oni.osu']]
if osus:
osud = parse_osu('public/songs/%s/%s' % (song[0], osus[0]))
preview = int(get_osu_key(osud, 'General', 'PreviewTime', 0))
type = song[9]
if type == "tja":
if os.path.isfile('public/songs/%s/main.tja' % song[0]):
preview = get_tja_preview('public/songs/%s/main.tja' % song[0])
else:
preview = 0
else:
preview = 0
osus = [osu for osu in os.listdir('public/songs/%s' % song[0]) if osu in ['easy.osu', 'normal.osu', 'hard.osu', 'oni.osu']]
if osus:
osud = parse_osu('public/songs/%s/%s' % (song[0], osus[0]))
preview = int(get_osu_key(osud, 'General', 'PreviewTime', 0))
else:
preview = 0
category_out = categories[song[8]] if song[8] in categories else def_category
songs_out.append({
@ -102,7 +129,9 @@ def route_api_songs():
],
'preview': preview,
'category': category_out['title'],
'category_en': category_out['title_en']
'category_en': category_out['title_en'],
'type': type,
'offset': song[10]
})
return jsonify(songs_out)

View File

@ -8,8 +8,13 @@ class Controller{
this.snd = this.multiplayer ? "_p" + this.multiplayer : ""
var backgroundURL = "/songs/" + this.selectedSong.folder + "/bg.png"
var songParser = new ParseSong(songData)
this.parsedSongData = songParser.getData()
if(selectedSong.type === "tja"){
this.parsedSongData = new ParseTja(songData, selectedSong.difficulty, selectedSong.offset)
}else{
this.parsedSongData = new ParseOsu(songData, selectedSong.offset)
}
this.offset = this.parsedSongData.soundOffset
assets.songs.forEach(song => {
if(song.id == this.selectedSong.folder){
@ -168,9 +173,6 @@ class Controller{
getBindings(){
return this.keyboard.getBindings()
}
getSongData(){
return this.game.getSongData()
}
getElapsedTime(){
return this.game.elapsedTime
}

View File

@ -49,7 +49,6 @@ class Game{
update(){
// Main operations
this.updateTime()
this.checkTiming()
this.updateCirclesStatus()
this.checkPlays()
// Event operations
@ -277,6 +276,7 @@ class Game{
var started = this.fadeOutStarted
if(started){
var ms = this.elapsedTime
var musicDuration = this.controller.mainAsset.duration * 1000 - this.controller.offset
if(this.musicFadeOut === 0){
if(this.controller.multiplayer === 1){
p2.send("gameresults", this.getGlobalScore())
@ -286,10 +286,10 @@ class Game{
this.controller.gameEnded()
p2.send("gameend")
this.musicFadeOut++
}else if(this.musicFadeOut === 2 && (ms >= started + 8600 && ms >= this.controller.mainAsset.duration * 1000 + 250)){
}else if(this.musicFadeOut === 2 && (ms >= started + 8600 && ms >= musicDuration + 250)){
this.controller.displayResults()
this.musicFadeOut++
}else if(this.musicFadeOut === 3 && (ms >= started + 9600 && ms >= this.controller.mainAsset.duration * 1000 + 1250)){
}else if(this.musicFadeOut === 3 && (ms >= started + 9600 && ms >= musicDuration + 1250)){
this.controller.clean()
if(this.controller.scoresheet){
this.controller.scoresheet.startRedraw()
@ -297,16 +297,9 @@ class Game{
}
}
}
checkTiming(){
if(this.songData.timingPoints[this.currentTimingPoint + 1]){
if(this.elapsedTime >= this.songData.timingPoints[this.currentTimingPoint + 1].start){
this.currentTimingPoint++
}
}
}
playMainMusic(){
var ms = this.elapsedTime
if(!this.mainMusicPlaying && (!this.fadeOutStarted || ms<this.fadeOutStarted + 1600)){
var ms = this.elapsedTime + this.controller.offset
if(!this.mainMusicPlaying && (!this.fadeOutStarted || ms < this.fadeOutStarted + 1600)){
if(this.controller.multiplayer !== 2){
this.mainAsset.play((ms < 0 ? -ms : 0) / 1000, false, Math.max(0, ms / 1000))
}
@ -362,9 +355,6 @@ class Game{
getCircles(){
return this.songData.circles
}
getSongData(){
return this.songData
}
updateCurrentCircle(){
this.currentCircle++
}

View File

@ -40,7 +40,7 @@ class loadSong{
}, reject)
}
}))
promises.push(loader.ajax(this.getOsuPath(this.selectedSong)).then(data => {
promises.push(loader.ajax(this.getSongPath(this.selectedSong)).then(data => {
this.songData = data.replace(/\0/g, "").split("\n")
}))
Promise.all(promises).then(() => {
@ -50,8 +50,13 @@ class loadSong{
alert("An error occurred, please refresh")
})
}
getOsuPath(selectedSong){
return "/songs/" + selectedSong.folder + "/" + selectedSong.difficulty + ".osu"
getSongPath(selectedSong){
var directory = "/songs/" + selectedSong.folder + "/"
if(selectedSong.type === "tja"){
return directory + "main.tja"
}else{
return directory + selectedSong.difficulty + ".osu"
}
}
setupMultiplayer(){
if(this.multiplayer){
@ -70,14 +75,20 @@ class loadSong{
this.selectedSong2 = {
title: this.selectedSong.title,
folder: this.selectedSong.folder,
difficulty: event.value
difficulty: event.value,
type: this.selectedSong.type,
offset: this.selectedSong.offset
}
loader.ajax(this.getOsuPath(this.selectedSong2)).then(data => {
this.song2Data = data.replace(/\0/g, "").split("\n")
if(this.selectedSong.type === "tja"){
p2.send("gamestart")
}, () => {
p2.send("gamestart")
})
}else{
loader.ajax(this.getSongPath(this.selectedSong2)).then(data => {
this.song2Data = data.replace(/\0/g, "").split("\n")
p2.send("gamestart")
}, () => {
p2.send("gamestart")
})
}
}
}else if(event.type === "gamestart"){
this.clean()

View File

@ -1,5 +1,5 @@
class ParseSong{
constructor(fileContent){
class ParseOsu{
constructor(fileContent, offset){
this.osu = {
OFFSET: 0,
MSPERBEAT: 1,
@ -36,11 +36,13 @@ class ParseSong{
}
this.data = []
for(let line of fileContent){
line = line.trim().replace(/\/\/.*/, "")
line = line.replace(/\/\/.*/, "").trim()
if(line !== ""){
this.data.push(line)
}
}
this.offset = (offset || 0) * -1000
this.soundOffset = 0
this.beatInfo = {
beatInterval: 0,
lastBeatInterval: 0,
@ -126,7 +128,7 @@ class ParseSong{
this.difficulty.lastMultiplier = sliderMultiplier
}
timingPoints.push({
start: start,
start: start + this.offset,
sliderMultiplier: sliderMultiplier,
measure: parseInt(values[this.osu.METER]),
gogoTime: parseInt(values[this.osu.KIAIMODE])
@ -139,20 +141,18 @@ class ParseSong{
var measureNumber = 0
for(var i = 0; i<this.timingPoints.length; i++){
if(this.timingPoints[i + 1]){
var limit = this.timingPoints[i + 1].start
var limit = this.timingPoints[i + 1].start - this.offset
}else{
var limit = this.circles[this.circles.length - 1].getMS()
var limit = this.circles[this.circles.length - 1].getMS() - this.offset
}
for(var j = this.timingPoints[i].start; j <= limit; j += this.beatInfo.beatInterval){
measures.push({
ms: j,
nb: measureNumber,
speed: this.timingPoints[i].sliderMultiplier
})
measureNumber++
if(measureNumber === this.timingPoints[i].measure + 1){
measureNumber = 0
for(var start = this.timingPoints[i].start; start <= limit; start += this.beatInfo.beatInterval){
if(measureNumber === 0){
measures.push({
ms: start + this.offset,
speed: this.timingPoints[i].sliderMultiplier
})
}
measureNumber = (measureNumber + 1) % (this.timingPoints[i].measure + 1)
}
}
return measures
@ -242,9 +242,14 @@ class ParseSong{
var hitSound = parseInt(values[this.osu.HITSOUND])
var beatLength = speed
var lastMultiplier = this.difficulty.lastMultiplier
if(circleID === 1 && start + this.offset < 0){
var offset = start + this.offset
this.soundOffset = offset
this.offset -= offset
}
for(var j = 0; j < this.timingPoints.length; j++){
if(this.timingPoints[j].start > start){
if(this.timingPoints[j].start - this.offset > start){
break
}
speed = this.timingPoints[j].sliderMultiplier
@ -258,11 +263,11 @@ class ParseSong{
var requiredHits = Math.floor(Math.max(1, (endTime - start) / 1000 * hitMultiplier))
circles.push(new Circle({
id: circleID,
start: start,
start: start + this.offset,
type: "balloon",
txt: "ふうせん",
speed: speed,
endTime: endTime,
endTime: endTime + this.offset,
requiredHits: requiredHits,
gogoTime: gogoTime
}))
@ -284,11 +289,11 @@ class ParseSong{
}
circles.push(new Circle({
id: circleID,
start: start,
start: start + this.offset,
type: type,
txt: txt,
speed: speed,
endTime: endTime,
endTime: endTime + this.offset,
gogoTime: gogoTime
}))
@ -318,7 +323,7 @@ class ParseSong{
if(!emptyValue){
circles.push(new Circle({
id: circleID,
start: start,
start: start + this.offset,
type: type,
txt: txt,
speed: speed,
@ -334,16 +339,4 @@ class ParseSong{
}
return circles
}
getData(){
return {
generalInfo: this.generalInfo,
metaData: this.metadata,
editor: this.editor,
beatInfo: this.beatInfo,
difficulty: this.difficulty,
timingPoints: this.timingPoints,
circles: this.circles,
measures: this.measures
}
}
}

338
public/src/js/parsetja.js Normal file
View File

@ -0,0 +1,338 @@
class ParseTja{
constructor(file, difficulty, offset){
this.data = []
for(let line of file){
line = line.replace(/\/\/.*/, "").trim()
if(line !== ""){
this.data.push(line)
}
}
this.difficulty = difficulty
this.offset = (offset || 0) * -1000
this.soundOffset = 0
this.noteTypes = [
{name: false, txt: false},
{name: "don", txt: "ドン"},
{name: "ka", txt: "カッ"},
{name: "daiDon", txt: "ドン(大)"},
{name: "daiKa", txt: "カッ(大)"},
{name: "drumroll", txt: "連打ーっ!!"},
{name: "daiDrumroll", txt: "連打(大)ーっ!!"},
{name: "balloon", txt: "ふうせん"},
{name: false, txt: false},
{name: "balloon", txt: "ふうせん"}
]
this.courseTypes = ["easy", "normal", "hard", "oni"]
this.metadata = this.parseMetadata()
this.measures = []
this.beatInfo = {}
this.circles = this.parseCircles()
}
parseMetadata(){
var metaNumbers = ["bpm", "offset"]
var inSong = false
var courses = {}
var currentCourse = {}
var courseName = this.difficulty
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" && !inSong){
inSong = true
for(var name in currentCourse){
if(!(courseName in courses)){
courses[courseName] = {}
}
courses[courseName][name] = currentCourse[name]
}
courses[courseName].start = lineNum + 1
courses[courseName].end = this.data.length
}else if(name === "end" && inSong){
inSong = false
courses[courseName].end = lineNum
}
}else if(!inSong){
if(line.indexOf(":") > 0){
var [name, value] = this.split(line, ":")
name = name.toLowerCase().trim()
value = value.trim()
if(name === "course"){
if(value in this.courseTypes){
courseName = this.courseTypes[value]
}else{
courseName = value.toLowerCase()
}
}else if(name === "balloon"){
value = value ? value.split(",").map(digit => parseInt(digit)) : []
}else if(this.inArray(name, metaNumbers)){
value = parseFloat(value)
}
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(){
var meta = this.metadata[this.difficulty]
var ms = (meta.offset || 0) * -1000 + this.offset
var bpm = meta.bpm || 0
if(bpm <= 0){
bpm = 1
}
var scroll = 1
var measure = 4
this.beatInfo.beatInterval = 60000 / bpm
var gogo = false
var barLine = true
var balloonID = 0
var balloons = meta.balloon || []
var lastDrumroll = false
var branch = false
var branchType
var branchPreference = "m"
var currentMeasure = []
var firstMeasure = true
var firstNote = true
var circles = []
var circleID = 0
var pushMeasure = () => {
if(barLine){
var note = currentMeasure[0]
if(note){
var speed = note.bpm * note.scroll / 60
}else{
var speed = bpm * scroll / 60
}
this.measures.push({
ms: ms,
speed: speed
})
if(firstMeasure){
firstMeasure = false
var msPerMeasure = 60000 * measure / bpm
for(var measureMs = ms - msPerMeasure; measureMs > 0; measureMs -= msPerMeasure){
this.measures.push({
ms: measureMs,
speed: speed
})
}
}
}
if(currentMeasure.length){
for(var i = 0; i < currentMeasure.length; i++){
var note = currentMeasure[i]
if(firstNote && note.type){
firstNote = false
if(ms < 0){
this.soundOffset = ms
ms = 0
}
}
note.start = ms
if(note.endDrumroll){
note.endDrumroll.endTime = ms
}
var msPerMeasure = 60000 * measure / bpm
ms += msPerMeasure / currentMeasure.length
}
for(var i = 0; i < currentMeasure.length; i++){
var note = currentMeasure[i]
if(note.type){
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
})
if(lastDrumroll === note){
lastDrumroll = circleObj
}
circles.push(circleObj)
}
}
}else{
var msPerMeasure = 60000 * measure / bpm
ms += msPerMeasure
}
}
for(var lineNum = meta.start; lineNum < meta.end; lineNum++){
var line = this.data[lineNum]
if(line.slice(0, 1) === "#"){
var line = line.slice(1).toLowerCase()
var [name, value] = this.split(line, " ")
switch(name){
case "gogostart":
gogo = true
break
case "gogoend":
gogo = false
break
case "bpmchange":
bpm = parseFloat(value)
break
case "scroll":
scroll = parseFloat(value)
break
case "branchstart":
branch = true
branchType = ""
value = value.split(",")
var forkType = value[0].toLowerCase()
if(forkType === "r" || parseFloat(value[2]) <= 100){
branchPreference = "m"
}else if(parseFloat(value[1]) <= 100){
branchPreference = "e"
}else{
branchPreference = "n"
}
break
case "branchend":
case "section":
branch = false
break
case "n": case "e": case "m":
branchType = name
break
case "measure":
var [numerator, denominator] = value.split("/")
measure = numerator / denominator * 4
break
case "delay":
ms += (parseFloat(value) || 0) * 1000
break
case "barlineon":
barLine = true
break
case "barlineoff":
barLine = false
break
}
}else if(!branch || branch && branchType === branchPreference){
var string = line.split("")
for(let symbol of string){
var error = false
switch(symbol){
case "0":
currentMeasure.push({
bpm: bpm,
scroll: scroll
})
break
case "1": case "2": case "3": case "4":
var type = this.noteTypes[symbol]
var circleObj = {
type: type.name,
txt: type.txt,
gogo: gogo,
bpm: bpm,
scroll: scroll
}
if(lastDrumroll){
circleObj.endDrumroll = lastDrumroll
lastDrumroll = false
}
currentMeasure.push(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
}
if(lastDrumroll){
circleObj.endDrumroll = lastDrumroll
}
if(symbol === "7" || symbol === "9"){
var hits = balloons[balloonID]
if(!hits || hits < 1){
hits = 1
}
circleObj.requiredHits = hits
balloonID++
}
lastDrumroll = circleObj
currentMeasure.push(circleObj)
break
case "8":
if(lastDrumroll){
currentMeasure.push({
endDrumroll: lastDrumroll,
bpm: bpm,
scroll: scroll
})
lastDrumroll = false
}else{
currentMeasure.push({
bpm: bpm,
scroll: scroll
})
}
break
case ",":
pushMeasure()
currentMeasure = []
break
default:
error = true
break
}
if(error){
break
}
}
}
}
pushMeasure()
if(lastDrumroll){
lastDrumroll.endTime = ms
}
return circles
}
}

View File

@ -88,7 +88,9 @@ class SongSelect{
skin: song.category in this.songSkin ? this.songSkin[song.category] : this.songSkin.default,
stars: song.stars,
category: song.category,
preview: song.preview || 0
preview: song.preview || 0,
type: song.type,
offset: song.offset
})
}
this.songs.sort((a, b) => {
@ -470,7 +472,9 @@ class SongSelect{
"title": selectedSong.title,
"folder": selectedSong.id,
"difficulty": this.difficultyId[difficulty],
"category": selectedSong.category
"category": selectedSong.category,
"type": selectedSong.type,
"offset": selectedSong.offset
}, shift, ctrl, touch)
}
toTitleScreen(){

View File

@ -48,7 +48,7 @@ class View{
this.drumroll = []
this.beatInterval = this.controller.getSongData().beatInfo.beatInterval
this.beatInterval = this.controller.parsedSongData.beatInfo.beatInterval
this.assets = new ViewAssets(this)
this.touch = -Infinity
@ -216,13 +216,14 @@ class View{
//this.drawTime()
}
updateDonFaces(){
if(this.controller.getElapsedTime() >= this.nextBeat){
var ms = this.controller.getElapsedTime()
while(ms >= this.nextBeat){
this.nextBeat += this.beatInterval
if(this.controller.getCombo() >= 50){
this.currentBigDonFace = (this.currentBigDonFace + 1) % 2
this.currentDonFace = (this.currentDonFace + 1) % 2
}
else{
var face = Math.floor(ms / this.beatInterval) % 2
this.currentBigDonFace = face
this.currentDonFace = face
}else{
this.currentBigDonFace = 1
this.currentDonFace = 0
}
@ -289,16 +290,12 @@ class View{
}
}
drawMeasures(){
var measures = this.controller.getSongData().measures
var measures = this.controller.parsedSongData.measures
var currentTime = this.controller.getElapsedTime()
measures.forEach((measure, index)=>{
var timeForDistance = this.posToMs(this.distanceForCircle, measure.speed)
if(
currentTime >= measure.ms - timeForDistance
&& currentTime <= measure.ms + 350
&& measure.nb == 0
){
if(currentTime >= measure.ms - timeForDistance && currentTime <= measure.ms + 350){
this.drawMeasure(measure)
}
})

View File

@ -27,7 +27,7 @@
<script src="/src/js/assets.js?{{version.commit_short}}"></script>
<script src="/src/js/loadsong.js?{{version.commit_short}}"></script>
<script src="/src/js/parsesong.js?{{version.commit_short}}"></script>
<script src="/src/js/parseosu.js?{{version.commit_short}}"></script>
<script src="/src/js/titlescreen.js?{{version.commit_short}}"></script>
<script src="/src/js/scoresheet.js?{{version.commit_short}}"></script>
<script src="/src/js/songselect.js?{{version.commit_short}}"></script>
@ -51,6 +51,7 @@
<script src="/src/js/loader.js?{{version.commit_short}}"></script>
<script src="/src/js/canvastest.js?{{version.commit_short}}"></script>
<script src="/src/js/canvascache.js?{{version.commit_short}}"></script>
<script src="/src/js/parsetja.js?{{version.commit_short}}"></script>
</head>
<body>