Created
September 10, 2015 04:39
-
-
Save 123jimin/9d0a7ab762662f21a9cf to your computer and use it in GitHub Desktop.
Simple Jubeat Fumen Parser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(window){ | |
var JMap = function JubeatMap(o, print_min){ | |
var music_title = o.m.title ? o.m.title : | |
o.s.m ? o.s.m.split('/').pop().split('.')[0] : | |
o.s.bga ? o.s.bga : ""; | |
this.metadata = { | |
'music': o.s.m || null, 'bga': o.s.bga || null, | |
'title': music_title, 'level': o.m.level || 10, | |
'difficulty': o.m.dif || 3, 'artist': o.m.artist || null, | |
'jacket': o.m.jacket || null | |
}; | |
this.playdata = o.d; | |
this.bake(print_min || 0); | |
}; | |
JMap.parse = function JMap_parse(s, print_min){ | |
var unquote = function(x){if(x[0]=='"'&&x.slice(-1)=='"') return x.slice(1,-1); return x;}; | |
var r_setting = /^([a-z_:.]+)\s*\=\s*(\S.*)$/, | |
r_offset = /^\*\s*(\S)\s*\:\s*(\S+)$/, | |
r_line = /^(\S{4})\s*(?:\|\s*([^\|]+))?/, | |
r_timing = /\([^)]+\)|\[[^\]]+\]|./g; | |
var recent_comment = ""; | |
var metadata = {'type':'memo'}, | |
metadata_song = {}, | |
options = {'b':4,'t':120}; | |
var data = [], | |
current_time = 0, | |
prev_time = 0; | |
var delayed_options = {}, | |
char_map = {}; | |
"①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯".split('').forEach(function(c,i){char_map[c] = [i/4, null];}); | |
var current_map = [], | |
current_maps = [], | |
current_timing = []; | |
var last_hold = []; | |
for(i=0; i<16; i++) last_hold[i] = null; | |
var start_new_bar = function(){ | |
current_maps = []; | |
current_timing = []; | |
for(var key in delayed_options){ | |
apply_options(key, delayed_options[key]); | |
} | |
delayed_options = {}; | |
}; | |
// TODO: remove need of b in #memo2 | |
// c_v: symbol -> button locations | |
var process_bar = function process_bar(ver, holdby){ | |
var i, c, c_v = {}; | |
var hold_arrows = []; | |
// fill c_v | |
current_maps.forEach(function(map, i){ | |
for(var ai,c,ac,j=0; j<16; j++){ | |
c = map[j]; | |
if(holdby == 'arrow'){ | |
ai = ">V<∧>v<^>∨<∧야유여요".indexOf(c)%4; | |
if(ai >= 0){ | |
var ij = j, ta = []; | |
search_arr: for(;;){ | |
switch(ai){ | |
case 0: | |
ij++; if(ij%4 == 0) break search_arr; break; | |
case 1: | |
ij+=4; if(ij>=16) break search_arr; break; | |
case 2: | |
ij--; if(ij%4 == 3 || ij==-1) break search_arr; break; | |
case 3: | |
ij-=4; if(ij<0) break search_arr; break; | |
} | |
ta.push([ij, map[ij]]); | |
} | |
hold_arrows.push([j, ai, ta]); | |
} | |
} | |
if(!c_v[c]) c_v[c] = 1<<j; | |
else c_v[c] |= 1<<j; | |
} | |
}); | |
// remove unused symbols | |
for(c in c_v){ | |
i = -1; | |
current_timing.forEach(function(line, j){ | |
if(i == -2) return; | |
line.forEach(function(x, k){ | |
if(x[0] == c) i = i==-1 ? 0 : -2; | |
}); | |
}); | |
if(i < 0 && !(c in char_map)){ | |
delete c_v[c]; | |
continue; | |
} | |
} | |
hold_arrows.forEach(function(data){ | |
data[2] = data[2].filter(function(ind){ | |
return (ind[1] in c_v) && (c_v[ind[1]] & (1<<ind[0])); | |
}); | |
if(data[2].length == 0) throw new Error("Invalid hold arrow"); | |
}); | |
var tmp_arr = [], | |
tmp_hold = [], | |
tick_offset = 0; | |
// process timing info | |
if(ver == 0) current_timing.forEach(function(line, i){ | |
line.forEach(function(x, j){ | |
char_map[x[0]] = [tick_offset/4, x[1]]; | |
tick_offset++; | |
}); | |
}); | |
else current_timing.forEach(function(line, i){ | |
var line_size = ver == 2 ? line.filter(function(x){return x[0][0]!='('&&x[0][0]!='[';}).length : line.length, | |
i, j = tick_offset; | |
line.forEach(function(x){ | |
if(ver == 2 && (x[0][0]=='('||x[0][0]=='[')){ | |
// TODO: process these | |
return; | |
} | |
char_map[x[0]] = [j, x[1]]; | |
j += 1/line_size; | |
}); | |
tick_offset++; | |
}); | |
Object.keys(char_map).sort(function(a,b){return char_map[a][0]-char_map[b][0];}).forEach(function(c){ | |
if(!(c in c_v) || !c_v[c]) return; | |
var t = current_time + 6e4/options.t*(+char_map[c][0]); | |
if(holdby){ | |
var i, j, o, data; | |
for(i=0; i<16; i++) if(c_v[c] & (1<<i)){ | |
if(last_hold[i]){ | |
last_hold[i].h = t; | |
last_hold[i] = null; | |
c_v[c] &= ~(1<<i); | |
continue; | |
} | |
for(j=0; j<hold_arrows.length; j++){ | |
data = hold_arrows[j]; | |
if(data[2].length == 0) continue; | |
if(data[2][0][0] == i && data[2][0][1] == c){ | |
// console.log("Hold:", c, t, i, char_map[c][1]); | |
tmp_arr.push(o={ | |
't': t, 'v': i, 'x': recent_comment, 'l': char_map[c][1], | |
'h': null, 'hv': data[0] | |
}); | |
hold_arrows.splice(j, 1); | |
last_hold[i] = o; | |
c_v[c] &= ~(1<<i); | |
break; | |
} | |
}; | |
} | |
} | |
if(!c_v[c]) return; | |
// console.log("Norm:", c, t, c_v[c].toString(2), char_map[c][1]); | |
if(c_v[c]) tmp_arr.push({ | |
't': t, 'v': c_v[c], 'x': recent_comment, 'l': char_map[c][1] | |
}); | |
}); | |
tmp_arr.sort(function(a, b){return a.t-b.t;}).forEach(function(o){ | |
data.push(o); | |
}); | |
if(!ver) current_time += 6e4*tick_offset/4/options.t; | |
// TODO: really? | |
else current_time += 6e4*options.b/options.t; | |
}; | |
var apply_options = function(key, val){ | |
switch(key){ | |
case 'o': // abs. offset | |
val = +val; | |
current_time = val; | |
break; | |
case 'r': // rel. offset | |
val = +val; | |
current_time += val; | |
break; | |
case 't': // tempo | |
val = +val; | |
if(val <= 0) throw new Error("Invalid BPM"); | |
data.push({'t': current_time, 'c': val}); | |
break; | |
default: | |
metadata_song[key] = val; | |
} | |
options[key] = val; | |
}; | |
var line_count = 0, | |
timing_count = 0; | |
s.split('\n').forEach(function(line, li){ | |
var match, key, val; | |
// blank lines, comments | |
line = line.trim(); | |
if(!line) return; | |
if(line[0] == line[1] && line[1] == '/'){ | |
recent_comment = line.slice(2).trim(); | |
return; | |
} | |
// hashes | |
if(line[0] == '#'){ | |
if(match = line.slice(1).match(r_setting)){ | |
key = match[1]; val = unquote(match[2].trim()); | |
if((key == 'pw' || key == 'ph') && val != '4') | |
throw new Error("Unsupported map size"); | |
metadata[key] = val; | |
}else if(line.length > 1){ | |
val = line.slice(1).trim().toLowerCase(); | |
if(val == 'halt') throw new Error("HALT (line "+(li+1)+")"); | |
metadata.type = val; | |
switch(val){ | |
case 'boogie': | |
case 'iboogie': | |
// no break | |
case 'memo2': | |
if(!('o' in options)) options.o = 0; | |
break; | |
} | |
} | |
return; | |
} | |
// a=b | |
if(match = line.match(r_setting)){ | |
key = match[1]; val = unquote(match[2].trim()); | |
if(line_count) delayed_options[key] = val; | |
else apply_options(key, val); | |
return; | |
} | |
// char offest | |
if(match = line.match(r_offset)){ | |
char_map[match[1]] = [match[2], [li, -1]]; | |
return; | |
} | |
// main map | |
if(match = line.match(r_line)){ | |
var holdby = (+metadata.holdbyarrow) ? 'arrow' : null; | |
var timing_line = null; | |
current_map.push(match[1]); | |
if(match[2]){ | |
// for #memo | |
timing_count += match[2].length; | |
if(metadata.type == 'memo2') timing_line = Array.apply(null, match[2].match(r_timing)); | |
else timing_line = match[2].split(''); | |
current_timing.push(timing_line.map(function(c, i){ | |
return [c, [li, i]]; | |
})); | |
} | |
if(++line_count == 4){ | |
line_count = 0; | |
current_maps.push(current_map.join('')); | |
current_map = []; | |
switch(metadata.type){ | |
case 'memo2': | |
case 'memo1': | |
case 'iboogie': | |
case 'boogie': | |
if(current_timing.length >= options.b){ | |
prev_time = current_time; | |
process_bar(metadata.type == 'memo1' ? 1 : 2, holdby); | |
start_new_bar(); | |
}else if(current_timing.length == 0){ | |
var x = current_time; | |
current_time = prev_time; | |
process_bar(metadata.type == 'memo1' ? 1 : 2, holdby); | |
start_new_bar(); | |
current_time = x; | |
} | |
break; | |
case 'memo': | |
default: | |
if(timing_count == 4*options.b){ | |
process_bar(0, holdby); | |
start_new_bar(); | |
timing_count = 0; | |
}else if(timing_count > 4*options.b){ | |
console.error("Invalid timing section length (line "+(li+1)+")"); | |
throw new Error("Invalid timing section length (line "+(li+1)+")"); | |
} | |
} | |
} | |
} | |
}); | |
return new JMap({ | |
'm': metadata, | |
's': metadata_song, | |
'd': data | |
}, print_min || 0); | |
}; | |
var D2L = function(a){ | |
return a?"line "+(a[0]+1)+", col "+(a[1]+1):"line ?, col ?"; | |
}; | |
JMap.prototype.bake = function(print_min){ | |
this.bake_playdata = []; | |
this.bake_bpmdata = []; | |
this.last_timestamp = 0; | |
this.max_combo = 0; | |
this.hold_notes = 0; | |
var total_notes = 0; | |
var last_t = [], min_t = [], min_tl = [], ti; | |
for(ti=0; ti<16; ti++) last_t[ti] = min_t[ti] = min_tl[ti] = null; | |
this.playdata.forEach(function procPlayData(o){ | |
var ov, et; | |
if('c' in o){ | |
// TODO: regarding offset | |
}else{ | |
if(o.h){ | |
ov = 1<<o.v; | |
et = o.h; | |
this.hold_notes++; | |
}else{ | |
ov = o.v; | |
et = o.t; | |
} | |
if(this.last_timestamp<et) this.last_timestamp = et; | |
this.bake_playdata.push(o.h ? ['h', o.t, o.v, o.h, o.hv, o.x, o.l] : ['n', o.t, o.v, o.x, o.l]); | |
for(ti=0; ti<16; ti++) if(ov&(1<<ti)){ | |
total_notes++; | |
if(!print_min) continue; | |
if(last_t[ti] != null && (min_t[ti] == null || o.t - last_t[ti][0] < min_t[ti])){ | |
min_t[ti] = o.t - last_t[ti][0]; | |
min_tl[ti] = last_t[ti][1] + " ~ " + D2L(o.l); | |
} | |
last_t[ti] = [o.t, D2L(o.l)]; | |
} | |
} | |
}, this); | |
this.max_combo = total_notes+this.hold_notes; | |
console.log("Total notes: %d / Max combo: %d", total_notes, this.max_combo); | |
if(print_min) min_t.forEach(function(o, i){ | |
if(o && o < print_min){ | |
// alert("Note distant is too short in cell "+(i+1)); | |
console.info("Min. dist. between notes in cell %d: %s (%s)", i+1, o.toFixed(2), min_tl[i]); | |
} | |
}); | |
}; | |
JMap.prototype.first = function(a, t){ | |
if(a.length == 0) return null; | |
var left = 0, right = a.length; | |
while(right - left > 2){ | |
var mid = Math.floor((left+right)/2); | |
if(a[mid][1] == t) return a[mid]; | |
if(a[mid][1] < t) left = mid+1; | |
else right = mid+1; | |
if(t <= a[left][1]) return a[left]; | |
if(a[right-1][1] < t) return null; | |
} | |
return a[right-1]; | |
}; | |
JMap.prototype.firsts = function(a, t){ | |
if(a.length == 0) return []; | |
var r=[], f=this.firstInd(a, t), x; | |
if(f === null) return []; | |
for(x=a[f][1]; f < a.length && a[f][1] == x; f++) r.push(a[f]); | |
return r; | |
}; | |
/** | |
* Returns the index of first note. | |
*/ | |
JMap.prototype.firstInd = function(a, t){ | |
if(a.length == 0) return null; | |
var left = 0, right = a.length; | |
while(right - left > 2){ | |
var mid = Math.floor((left+right)/2); | |
if(a[mid][1] == t){ | |
for(; mid && a[mid-1][1] == t; mid--); | |
return mid; | |
} | |
if(a[mid][1] < t) left = mid+1; | |
else right = mid+1; | |
// a[left-1][1] should have been checked. | |
if(t <= a[left][1]) return left; | |
if(a[right-1][1] < t) return null; | |
} | |
return right-1; | |
}; | |
/** | |
* Returns all notes appears before t, starts from index i. | |
*/ | |
JMap.prototype.next = function(a, i, t){ | |
var m = []; | |
for(;i<a.length;i++){ | |
if(a[i][1] >= t) break; | |
m.push(a[i]); | |
} | |
return [i, m]; | |
}; | |
/** | |
* Returns all notes appears after t and length r. | |
*/ | |
JMap.prototype.getRange = function(a, t, r){ | |
var res = [], ind = this.firstInd(a, t); | |
if(ind === null) return res; | |
do{ | |
if(a[ind][1] >= t+r) break; | |
res.push(a[ind]); | |
ind++; | |
}while(ind < a.length); | |
return res; | |
}; | |
window.JMap = JMap; | |
}(this)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment