|
|
|
function OsuFile() {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
var OSU = OsuFile.prototype;
|
|
|
|
|
|
|
|
const get_bpm = len => (1.0 / len * 60000.00);
|
|
|
|
const get_beat = bpm => 60000.00 / bpm;
|
|
|
|
|
|
|
|
const get_sv = inverse => 100.00 / (-1.0 * inverse);
|
|
|
|
const get_inverse = sv => (sv / 100.00) * -1.0;
|
|
|
|
|
|
|
|
OSU.LoadString = async function(contents) {
|
|
|
|
const lines = contents.split(/(\n|\r|\f)+/).where(line => line && line.trim() != "");
|
|
|
|
|
|
|
|
var stage = new Stage();
|
|
|
|
stage.giveMany(lines);
|
|
|
|
stage.commit();
|
|
|
|
|
|
|
|
await this.LoadTokens(stage);
|
|
|
|
};
|
|
|
|
|
|
|
|
OSU.LoadTokens = async function(tokens) {
|
|
|
|
const stages = {
|
|
|
|
"General": new Stage(),
|
|
|
|
"Editor": new Stage(),
|
|
|
|
"Metadata": new Stage(),
|
|
|
|
"Difficulty": new Stage(),
|
|
|
|
"Events": new Stage(),
|
|
|
|
"Colours": new Stage(),
|
|
|
|
"TimingPoints": new Stage(),
|
|
|
|
"HitObjects": new Stage(),
|
|
|
|
};
|
|
|
|
|
|
|
|
var line;
|
|
|
|
|
|
|
|
var group = null;
|
|
|
|
|
|
|
|
this.errors = [];
|
|
|
|
this.data = {};
|
|
|
|
this.spec = {};
|
|
|
|
|
|
|
|
this.futsuu(stages["General"]);
|
|
|
|
this.han(stages["Editor"]);
|
|
|
|
this.meta(stages["Metadata"]);
|
|
|
|
this.muzukashisa(stages["Difficulty"]);
|
|
|
|
this.iro(stages["Colours"]);
|
|
|
|
this.jiken(stages["Events"]);
|
|
|
|
this.keiji(stages["TimingPoints"]);
|
|
|
|
this.maru(stages["HitObjects"]);
|
|
|
|
while(line = await tokens.take()) {
|
|
|
|
if(!this.version)
|
|
|
|
{
|
|
|
|
if(/^osu file format v\d+$/.test(line))
|
|
|
|
{
|
|
|
|
this.version = line.match(/\d+$/)[0];
|
|
|
|
}
|
|
|
|
else return ["no version present"];
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if(/^\/\//.test(line)) continue;
|
|
|
|
if(line.trim() === "") continue;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let new_Group = line.match(/^\[(\w+)\]$/i);
|
|
|
|
console.log(new_Group);
|
|
|
|
if(new_Group && new_Group[1]) {
|
|
|
|
console.log("Setting new group `"+new_Group[1]+"' from old group "+group);
|
|
|
|
if(group) {
|
|
|
|
stages[group].commit();
|
|
|
|
}
|
|
|
|
|
|
|
|
group = new_Group[1];
|
|
|
|
this.data[group] = {};
|
|
|
|
stages[group].give(group);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!group) continue;
|
|
|
|
if(!stages[group]) return ["unknown group "+group];
|
|
|
|
|
|
|
|
stages[group].give(line);
|
|
|
|
}
|
|
|
|
console.log("----END OF FILE");
|
|
|
|
if(group)
|
|
|
|
stages[group].commit();
|
|
|
|
|
|
|
|
for(const key of Object.keys(stages))
|
|
|
|
{
|
|
|
|
stages[key].forceClose();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.specialise();
|
|
|
|
return this.errors;
|
|
|
|
};
|
|
|
|
|
|
|
|
/// --- Parsers --- ///
|
|
|
|
|
|
|
|
OSU.futsuu = async function(tokens) {
|
|
|
|
const group = await tokens.take();
|
|
|
|
if(group) {
|
|
|
|
var data = this.data[group];
|
|
|
|
var tok;
|
|
|
|
while(tok = await tokens.take()) {
|
|
|
|
|
|
|
|
var match = tok.match(/^(\w+):\s(.*)$/i);
|
|
|
|
if(match && match[0])
|
|
|
|
{
|
|
|
|
data[match[1]] = match[2];
|
|
|
|
}
|
|
|
|
else this.errors.push("futsuu: parsing line `"+tok+"' failed.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
OSU.han = async function(tokens) {
|
|
|
|
const group = await tokens.take();
|
|
|
|
if(group) {
|
|
|
|
var data = this.data[group];
|
|
|
|
var tok;
|
|
|
|
|
|
|
|
while(tok = await tokens.take()) {
|
|
|
|
var match = tok.match(/^(\w+):\s(.*)$/i);
|
|
|
|
if(match && match[0])
|
|
|
|
{
|
|
|
|
if(match[1] === "Bookmarks")
|
|
|
|
{
|
|
|
|
data[match[1]] = match[2].split(/,/);
|
|
|
|
}
|
|
|
|
else data[match[1]] = match[2];
|
|
|
|
}
|
|
|
|
else this.errors.push("han: parsing line `"+tok+"' failed.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
OSU.meta = async function(tokens) {
|
|
|
|
const group = await tokens.take();
|
|
|
|
if(group) {
|
|
|
|
var data = this.data[group];
|
|
|
|
var tok;
|
|
|
|
|
|
|
|
while(tok = await tokens.take()) {
|
|
|
|
var match = tok.match(/^(\w+):\s?(.*)$/i);
|
|
|
|
if(match && match[0])
|
|
|
|
{
|
|
|
|
if(match[1] === "Tags")
|
|
|
|
{
|
|
|
|
data[match[1]] = match[2].split(/\s/);
|
|
|
|
}
|
|
|
|
else data[match[1]] = match[2];
|
|
|
|
}
|
|
|
|
else this.errors.push("meta: parsing line `"+tok+"' failed.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
OSU.muzukashisa = async function(tokens) {
|
|
|
|
const group = await tokens.take();
|
|
|
|
if(group) {
|
|
|
|
var data = this.data[group];
|
|
|
|
var tok;
|
|
|
|
|
|
|
|
while(tok = await tokens.take()) {
|
|
|
|
var match = tok.match(/^(\w+):\s?(.*)$/i);
|
|
|
|
if(match && match[0])
|
|
|
|
{
|
|
|
|
data[match[1]] = match[2];
|
|
|
|
}
|
|
|
|
else this.errors.push("muzukashisa: parsing line `"+tok+"' failed.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
OSU.iro = async function(tokens) {
|
|
|
|
const group = await tokens.take();
|
|
|
|
if(group) {
|
|
|
|
var data = this.data[group];
|
|
|
|
var tok;
|
|
|
|
|
|
|
|
while(tok = await tokens.take()) {
|
|
|
|
var match = tok.match(/^(Combo\d+)\s:\s(\d+),(\d+),(\d+)$/);
|
|
|
|
if(match && match[0])
|
|
|
|
{
|
|
|
|
data[match[1]] = {r: parseInt(match[2]), g: parseInt(match[3]), b: parseInt(match[4]), toString: function() {
|
|
|
|
return "#"+ this.r.toString(16)+this.g.toString(16)+this.b.toString(16);
|
|
|
|
}};
|
|
|
|
}
|
|
|
|
else this.errors.push("iso: parsing line `"+tok+"' failed.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
OSU.jiken = async function(tokens) {
|
|
|
|
const group = await tokens.take();
|
|
|
|
if(group) {
|
|
|
|
var data = this.data[group];
|
|
|
|
var tok;
|
|
|
|
|
|
|
|
while(tok = await tokens.take()) {
|
|
|
|
var match = tok.match(/^(\w+|\d+),.+$/i);
|
|
|
|
|
|
|
|
if(match && match[0])
|
|
|
|
{
|
|
|
|
//convenience?
|
|
|
|
const ar = JSON.parse("[" + tok + "]");
|
|
|
|
data[match[1]] = {time: ar[1], params: ar.slice(2)};
|
|
|
|
}
|
|
|
|
else this.errors.push("jiken: parsing line `"+tok+"' failed");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
OSU.keiji = async function(tokens) {
|
|
|
|
const group = await tokens.take();
|
|
|
|
if(group) {
|
|
|
|
var _data = this.data[group] = [];
|
|
|
|
var tok;
|
|
|
|
|
|
|
|
while(tok = await tokens.take()) {
|
|
|
|
var match = tok.match(/^(\d+),(-?\d+(?:\.\d+)?),(\d+),(\d+),(\d+),(\d+),([01]),(\d+)$/i);
|
|
|
|
let data = {};
|
|
|
|
if(match && match[0])
|
|
|
|
{
|
|
|
|
data.time = match[1];
|
|
|
|
data.beatLength = match[2];
|
|
|
|
data.meter = match[3];
|
|
|
|
data.sampleSet = match[4];
|
|
|
|
data.sampleIndex = match[5];
|
|
|
|
data.volume = match[6];
|
|
|
|
data.uninherited = match[7] == "1";
|
|
|
|
data.effects = match[8];
|
|
|
|
}
|
|
|
|
else this.errors.push("keiji: parsing line `"+tok+"' failed.");
|
|
|
|
_data.push(data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const BEZIER = 0;
|
|
|
|
const CENTRIPETAL_CATMULL_ROM = 1;
|
|
|
|
const LINEAR = 2;
|
|
|
|
const PERFECT_CIRCLE = 3;
|
|
|
|
const SLIDER_UNKNOWN = 4;
|
|
|
|
|
|
|
|
const parse_params = (params) => {
|
|
|
|
const re_slider_1 = /^(B|C|L|P)\|(.+)/i; //slider type
|
|
|
|
|
|
|
|
if(!params[0]) {
|
|
|
|
//Is circle
|
|
|
|
return {type: "circle"};
|
|
|
|
}
|
|
|
|
else if(re_slider_1 .test(params[0]))
|
|
|
|
{
|
|
|
|
//Is slider
|
|
|
|
const type = params[0].match(re_slider_1)[1].il_switch({'B': BEZIER,
|
|
|
|
'C': CENTRIPETAL_CATMULL_ROM,
|
|
|
|
'L': LINEAR,
|
|
|
|
'P': PERFECT_CIRCLE
|
|
|
|
}, SLIDER_UNKNOWN);
|
|
|
|
const points = params[0].split(/\|/).slice(1).map(x=> x.split(/:/));
|
|
|
|
const slides = params[1];
|
|
|
|
const length = params[2];
|
|
|
|
const edgeSounds = params[3].split(/\|/);
|
|
|
|
const edgeSets = params[4].split(/\|/).map(x=> x.split(/:/));
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: "slider",
|
|
|
|
sliderType: type,
|
|
|
|
points: points,
|
|
|
|
slides: slides,
|
|
|
|
length: length,
|
|
|
|
edgeSounds: edgeSounds,
|
|
|
|
edgeSets: edgeSets,
|
|
|
|
};
|
|
|
|
|
|
|
|
} else {
|
|
|
|
//Spinner (or hold? TODO)
|
|
|
|
return {type: "spinner", endTime: params[0]};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
OSU.maru = async function(tokens) {
|
|
|
|
const group = await tokens.take();
|
|
|
|
if(group) {
|
|
|
|
var _data = this.data[group] = [];
|
|
|
|
var tok;
|
|
|
|
|
|
|
|
while(tok = await tokens.take()) {
|
|
|
|
let data = {};
|
|
|
|
var match = tok.match(/^(\d+),(\d+),(\d+),(\d+),(.+)$/i); //i honestly have no fucking idea what regex to get all these so we'll cheat
|
|
|
|
if(match && match[0])
|
|
|
|
{
|
|
|
|
data.x = match[1];
|
|
|
|
data.y = match[2];
|
|
|
|
data.time = match[2];
|
|
|
|
data.hitSound = match[3];
|
|
|
|
|
|
|
|
//Parse params and sample
|
|
|
|
if(/:i(?:.*\.wav)?$/.test(match[4]))
|
|
|
|
{
|
|
|
|
//Sample is there
|
|
|
|
let param = match[4].split(/,/);
|
|
|
|
data.hitSample = param.pop().split(/:/).where(/\d+/.test).associate([
|
|
|
|
"normalSet",
|
|
|
|
"additionalSet",
|
|
|
|
"index",
|
|
|
|
"volume",
|
|
|
|
"filename",
|
|
|
|
]);
|
|
|
|
data.params = parse_params(param);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
//No sample, rest is params
|
|
|
|
data.params = parse_params(match[4].split(/,/));
|
|
|
|
data.hitSample = {
|
|
|
|
normalSet: "0",
|
|
|
|
additionalSet: "0",
|
|
|
|
index: "0",
|
|
|
|
volume: "0",
|
|
|
|
};
|
|
|
|
}
|
|
|
|
_data.push(data);
|
|
|
|
}
|
|
|
|
else this.errors.push("maru: parsing line `"+tok+"' failed.");
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
OSU.specialise = function() {
|
|
|
|
this.spec.Background = this.data.Events[0].params[0];
|
|
|
|
};
|
|
|
|
|
|
|
|
/// --- End parsers --- ///
|
|
|
|
|
|
|
|
|
|
|
|
/// --- Begin Helpers --- ///
|
|
|
|
|
|
|
|
OSU.Fullname = function() {
|
|
|
|
return this.data.Metadata.ArtistUnicode + " - "+this.data.Metadata.TitleUnicode+" ["+this.data.Metadata.Version+"]";
|
|
|
|
};
|
|
|
|
|
|
|
|
OSU.Shortname = function() {
|
|
|
|
return this.data.Metadata.Version;
|
|
|
|
};
|
|
|
|
|
|
|
|
/// --- End helpers --- ///
|
|
|
|
|
|
|
|
OSU.okay = function() {
|
|
|
|
return !!this.version && this.errors.length<1;
|
|
|
|
};
|
|
|
|
|
|
|
|
OsuFile.Unserialise = (json) => {
|
|
|
|
|
|
|
|
if(typeof(json) === 'string') return OsuFile.Unserialise(JSON.parse(json));
|
|
|
|
|
|
|
|
var osu = new OsuFile();
|
|
|
|
osu.errors = [];
|
|
|
|
osu.spec = {};
|
|
|
|
osu.version = json.version;
|
|
|
|
osu.data = json.data;
|
|
|
|
|
|
|
|
osu.specialise();
|
|
|
|
|
|
|
|
return osu;
|
|
|
|
};
|
|
|
|
|
|
|
|
OSU.serialise = function() {
|
|
|
|
return {version: this.version, data: this.data};
|
|
|
|
};
|
|
|
|
|
|
|
|
OSU.toString = function() {
|
|
|
|
return JSON.stringify(this.serialise());
|
|
|
|
};
|
|
|
|
|
|
|
|
OSU.hash = function() {
|
|
|
|
if(this._hash) return this._hash;
|
|
|
|
else
|
|
|
|
return this._hash = forge_sha256(this.toString());
|
|
|
|
};
|
|
|
|
|
|
|
|
OSU.equals = function(...osu) {
|
|
|
|
const hash = this.hash();
|
|
|
|
console.log(hash);
|
|
|
|
return osu.where(x=> x && x.hash() === hash).length == osu.length;
|
|
|
|
};
|