Skip to content

Instantly share code, notes, and snippets.

@123jimin
Created September 10, 2015 04:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 123jimin/9d0a7ab762662f21a9cf to your computer and use it in GitHub Desktop.
Save 123jimin/9d0a7ab762662f21a9cf to your computer and use it in GitHub Desktop.
Simple Jubeat Fumen Parser
(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