You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

392 lines
8.4 KiB

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;
};