commit 6493f7081652d403ff5d98e90d7c084e4ac06369 Author: Avril Date: Thu Jan 30 13:08:01 2020 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/config.js b/config.js new file mode 100644 index 0000000..7b64bbb --- /dev/null +++ b/config.js @@ -0,0 +1,11 @@ +const CONFIG = { + API_PORT: 23333, + + API_LOCATION: "127.0.0.1", + + STATIC_LOCATION: "./www", + STATIC_PORT: 8081, /* Warning: do not serve statics here. */ +}; + + +module.exports = CONFIG; diff --git a/debug.js b/debug.js new file mode 100644 index 0000000..2a8ea59 --- /dev/null +++ b/debug.js @@ -0,0 +1,25 @@ +const fs = require('fs'), + config = require('./config'); + http = require('http'); + + +console.log("Starting static server on port "+config.STATIC_PORT); + +(async () => { + await http.createServer((req, res) => { + let url = req.url; + if(!req.url || req.url == "" || req.url == "/") url = "/index.html"; + fs.readFile(config.STATIC_LOCATION + url, (err, data) => { + if(err) { + console.log("Failed to retreive requested url '"+url+"': "+err); + res.writeHead(404); + res.end(JSON.stringify(err)); + return; + } else { + console.log("Writing "+url); + res.writeHead(200); + res.end(data); + } + }); + }).listen(config.STATIC_PORT); +})(); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2eeb3b8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,11 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "line-navigator": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/line-navigator/-/line-navigator-2.1.6.tgz", + "integrity": "sha1-s46Iq7vmEuqMBhEnugiu+dMPBg0=" + } + } +} diff --git a/www/css/main.css b/www/css/main.css new file mode 100644 index 0000000..50f7243 --- /dev/null +++ b/www/css/main.css @@ -0,0 +1,119 @@ +body, pre { + font-family: Arial, Helvetica, sans-serif; + font-size: 10pt; +} + +body { + background-color: #1d1f21; + color: #c5c8c6; +} + +h1 { + font: bolder 28px Tahoma; + letter-spacing: -2px; +} + +h1, h2 { + text-align: center; +} + +article, aside, .popout, .page { + background-color: #282a2e; + border: 1px solid #393b3f; +} + +.page { + padding: 2px; + padding-left: 4px; + display: inline-block; + vertical-align: top; +} + +.popout { + display: inline; +} + +article, aside { + display: table; + border-color: #282a2e; + margin: 2px; + padding: 4px; +} + +.indent { + border-left-color: red; + border-left-style: solid; + border-radius: 2px; + padding-left: 5px; +} + +#selector { + padding: 10px; + display: block; +} + +article header { + font-weight: bolder; +} + +blockquote { + margin-top: 2px; +} + +.osufile { + width: 100%; + line-height: 150%; +} + +.collapse { + cursor: pointer; + display: inline-block; + height: 12px; + left: -1px; + top: 2px; + margin-top: -3px; + width: 12px; + position:relative; + + text-align: center; + + font-weight: normal; + + border: 1px solid white; + border-radius: 50%; + + transition: background-color 0.5s; + transition: color 1s; + -webkit-transition: color 1s; + -webkit-transition: background-color 0.5s; +} + +.collapse:hover { + color: orange; + background-color: red; +} + +.collapsed:hover { + background-color: green !important; +} + +.osufile:after { + cursor: pointer; + content: "X"; + color: red; + transition: color 1s; + -webkit-transition: color 1s; + margin: -1px; + float: right; + text-align: right; +} + +.osufile:hover:after { + color: white; +} + +#metadata { + right: 5px; + white-space: nowrap; + position: fixed; +} diff --git a/www/i/close.svg b/www/i/close.svg new file mode 100644 index 0000000..25ce681 --- /dev/null +++ b/www/i/close.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..74ceb8a --- /dev/null +++ b/www/index.html @@ -0,0 +1,79 @@ + + + + + + + + + + + + + +

osu!merge

+ osu!merge +
+ Merge .osu files together. +
+
+
+
+ Select .osu files +
+
test
+
test2
+
+ Upload file: +
+
+
+ Metadata + +
+
+
+
+ + General +
+
+
Audio Filename:
+
Lead-in:
+
Preview Time:
+
Countdown:
+
Default sample set:
+
Stack leniency:
+
Mode:
+
Letterbox:
+
Widescreen:
+
+
+
+
+ + Song +
+
+
Title:
+
Title (Raomanised):
+
Artist:
+
Artist (Romanised):
+
Mapper:
+
Difficulty:
+
Source:
+
Tags:
+
ID:
+
Set ID:
+
+
+ Viewing: + +
+
+
+
+ + diff --git a/www/js/client.js b/www/js/client.js new file mode 100644 index 0000000..3531a99 --- /dev/null +++ b/www/js/client.js @@ -0,0 +1,72 @@ +var OSU_FILES = []; + +const global = new Dispatcher(); + +function _removeFile(id) { + //TODO: Remove from bars too + let elem = document.getElementById(id); + const file_id = elem.dataset.index; + elem.remove(); + + if(file_id!==undefined && OSU_FILES[file_id]) + { + global.signal("FILE_REMOVE", {id: file_id, osu: OSU_FILES[file_id]}); + OSU_FILES[file_id] = null; + } +} + +const READ_WHOLE_BUFFER = true; + +function _readfile(event) { + var input = event.target; + + if(READ_WHOLE_BUFFER) { + var reader = new FileReader(); + + reader.onload = () => { + var osu = new OsuFile(); + osu.LoadString(reader.result).then(result => { + console.log("Read version "+osu.version); + if(osu.okay()) { + //TODO: Push to read osu files + + OSU_FILES.push(osu); + global.signal("FILE_ADD", {id: OSU_FILES.length-1, osu: osu}); + } else { + var er = osu.errors; + console.log("Parsing .osu file failed: "+JSON.stringify(er)); + //TODO: Signal error + } + }); + }; + reader.readAsText(input.files[0]); + } else { + var nav = new LineNavigator(input.files[0]); + } +} + + +function _onload() { + + // --- Set up collapsers --- // + const collapsers = document.getElementsByClassName('collapse'); + + for(const elem of collapsers) { + elem.addEventListener('click', function() { + let block; + if(this.dataset.collapse) { + block = document.getElementById(this.dataset.collapse); + } + else block = this.parentElement.nextSibling.nextSibling; + if(block.style.display === "none") { + removeClass(this, "collapsed"); + block.style.display = ""; + } + else { + addClass(this, "collapsed"); + block.style.display = "none"; + } + }, false); + } + // --- End collapsers --- // +} diff --git a/www/js/osu.js b/www/js/osu.js new file mode 100644 index 0000000..14455b5 --- /dev/null +++ b/www/js/osu.js @@ -0,0 +1,334 @@ +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.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(); + } + + console.log(this.data); + 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."); + + } + } +}; + +/// --- End parsers --- /// + +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.version = json.version; + osu.data = json.data; +}; + +OSU.serialise = function() { + return {version: this.version, data: this.data}; +}; + +OSU.toString = function() { + return JSON.stringify(this.serialise()); +}; diff --git a/www/js/stage.js b/www/js/stage.js new file mode 100644 index 0000000..e414695 --- /dev/null +++ b/www/js/stage.js @@ -0,0 +1,106 @@ +function Stage(from) { + var base = this; + this.array = from || []; + this.over = false; + this.acceptInvalid = false; + var next = { + waiters: [], + wait: function() { + return new Promise(resolve => next.waiters.push(function() { resolve(); })); + }, + signal: function() { + if(next.waiters.length>0) { + next.waiters.shift()(); + } + }, + flush: function() { + while(next.waiters.length>0) { + next.waiters.shift()(); + } + } + }; + + this.take = async function() { + if(base.over && base.array.length<=0) + return undefined; + + if(base.array.length<=0) + await next.wait(); + + + if(base.over && base.array.length<=0) + return undefined; + + return base.array.shift(); + + }; + + this.poll = function() { + return base.array; + }; + + this.takeNB = function() { + if(base.array.length<=0) + return null; + return base.array.shift(); + }; + + this.swallow0 = async function() { + var ar = []; + var token; + while(token = await base.take()) { + ar.push(token); + } + return ar; + }; + + this.swallow = function(timeout) { + if(!timeout) return base.swallow0(); + else { + return new Promise(function(_resolve,_reject) { + var running=true; + var resolve = function(v) { + if(running) _resolve(v); + running=false; + }; + var reject= function(v) { + if(running) _reject(v); + running=false; + }; + base.swallow0().then(resolve); + if(timeout>0) + setTimeout(()=> reject(new Error("swallow timeout reached ("+timeout+")")), timeout); + }); + } + }; + + this.giveMany= function(values) { + for(let value of values) + base.give(value); + }; + + this.give = function(value) { + if(!value && !base.acceptInvalid) + return; + base.array.push(value); + next.signal(); + }; + + this.commit = function() { + base.over = true; + next.flush(); + + return base; + }; + + this.forceClose = function() { + if(base.over) return base; + else return base.commit(); + + }; +}; + +Stage.commitAll = (stages) => { + for(const st of stages) + st.commit(); +}; diff --git a/www/js/util.js b/www/js/util.js new file mode 100644 index 0000000..c1abefd --- /dev/null +++ b/www/js/util.js @@ -0,0 +1,93 @@ +function addGlobalStyle(text) +{ + var style = document.createElement('style'); + style.type='text/css'; + if(style.styleSheet) { + style.styleSheet.cssText = text; + } else { + style.appendChild(document.createTextNode(text)); + } + document.getElementsByTagName('head')[0].appendChild(style); + return style; +} + +function hasClass(ele, cls) +{ + return !!ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)')); +} + +function addClass(ele, cls) +{ + if(!hasClass(ele,cls)) ele.className += " "+cls; +} + +Array.prototype.where = function(predicate) { + output = []; + for(const elem of this) { + if(predicate(elem)) output.push(elem); + } + return output; +}; + +Array.prototype.associate = function(keys) { + var output = {}; + for(let i=0;i