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(), "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.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.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; };