Respack loading now uses Promises

master
William Toohey 10 years ago
parent b48e741eff
commit c4a013d007
  1. 30
      src/js/ResourceManager.js
  2. 423
      src/js/ResourcePack.js

@ -36,9 +36,7 @@ function Resources(core) {
this.enabledSongs = []; this.enabledSongs = [];
this.enabledImages = []; this.enabledImages = [];
this.toLoad = 0;
this.progressState = []; this.progressState = [];
this.rToLoad = [];
this.progressCallback = null; this.progressCallback = null;
this.root = null; this.root = null;
@ -82,31 +80,27 @@ function Resources(core) {
// Array of URLs to load, and a callback for when we're done // Array of URLs to load, and a callback for when we're done
// Preserves order of URLs being loaded // Preserves order of URLs being loaded
Resources.prototype.addAll = function(urls, progressCallback) { Resources.prototype.addAll = function(urls, progressCallback) {
return new Promise((resolve, reject) => {
this.toLoad += urls.length;
if(progressCallback) { if(progressCallback) {
this.progressCallback = progressCallback; this.progressCallback = progressCallback;
this.progressState = Array.apply(null, Array(urls.length)).map(Number.prototype.valueOf,0); this.progressState = Array.apply(null, Array(urls.length)).map(Number.prototype.valueOf,0);
} }
var respackPromises = []
for(var i = 0; i < urls.length; i++) { for(var i = 0; i < urls.length; i++) {
var r = new Respack(); var r = new Respack();
this.rToLoad.push(r); respackPromises.push(r.loadFromURL(urls[i], function(index, progress, pack) {
r.loadFromURL(urls[i], () => {
this.toLoad--;
if(this.toLoad <= 0) {
for(var i = 0; i < this.rToLoad.length; i++) {
this.addPack(this.rToLoad[i]);
}
this.rToLoad = [];
this.progressCallback = null;
resolve();
}
}, function(index, progress, pack) {
this.progressState[index] = progress; this.progressState[index] = progress;
this.updateProgress(pack); this.updateProgress(pack);
}.bind(this, i)); }.bind(this, i)));
} }
// Start all the promises at once, but add in sequence
return respackPromises.reduce((sequence, packPromise) => {
return sequence.then(() => {
return packPromise;
}).then(pack => {
this.addPack(pack);
}); });
}, Promise.resolve());
}; };
Resources.prototype.updateProgress = function(pack) { Resources.prototype.updateProgress = function(pack) {

@ -44,10 +44,9 @@ function Respack(url) {
this.downloaded = -1; this.downloaded = -1;
this.enabled = true; this.enabled = true;
this._songFile = null; this._songXMLPromise = null;
this._songFileParsed = false; this._imageXMLPromise = null;
this._imageFile = null; this._infoXMLPromise = null;
this._infoFile = null;
this.totalFiles = -1; this.totalFiles = -1;
@ -77,61 +76,77 @@ Respack.prototype.updateProgress = function() {
} }
}; };
Respack.prototype.loadFromURL = function(url, callback, progress) { Respack.prototype.loadFromURL = function(url, progress) {
this.loadedFromURL = true; this.loadedFromURL = true;
if(progress) {
this.progressCallback = progress;
}
return this.getBlob(url)
.then(response => {
return this.loadBlob(response);
}).then(zip => {
return this.parseZip(zip);
}).then(() => {
return this;
});
};
Respack.prototype.getBlob = function(url, progress) {
if(progress) {
this.progressCallback = progress;
}
return new Promise ((resolve, reject) => {
var req = new XMLHttpRequest(); var req = new XMLHttpRequest();
req.open('GET', url, true); req.open('GET', url, true);
req.responseType = 'blob'; req.responseType = 'blob';
req.onload = () => { req.onload = () => {
this.loadBlob(req.response, callback, progress); resolve(req.response);
}; };
req.onerror = function() { req.onerror = function() {
console.log("Could not load respack at URL", url); reject(Error("Could not load respack at URL" + url));
}; };
req.onprogress = event => { req.onprogress = event => {
if (event.lengthComputable) { if (event.lengthComputable) {
this.size = event.total; this.size = event.total;
this.downloaded = event.loaded; this.downloaded = event.loaded;
var percent = event.loaded / event.total; var percent = event.loaded / event.total;
if(progress) { if(this.progressCallback) {
progress(percent / 2, this); // because of processing too this.progressCallback(percent / 2, this); // because of processing too
} }
} else {
// Unable to compute progress information since the total size is unknown
} }
}; };
req.send(); req.send();
}; });
}
Respack.prototype.loadBlob = function(blob, callback, progress, errorCallback) { Respack.prototype.loadBlob = function(blob, progress) {
this._completionCallback = callback; if(progress) {
this.progressCallback = progress; this.progressCallback = progress;
}
return new Promise((resolve, reject) => {
this.size = blob.size; this.size = blob.size;
this.file = new zip.fs.FS(); var file = new zip.fs.FS();
this.file.importBlob(blob, file.importBlob(blob,
this.parseWholeZip.bind(this), () => {
resolve(file);
},
error => { // failure error => { // failure
console.log("Error loading respack :", error.toString()); reject(Error("Respack error:", error.toString()));
this.file = null;
if(errorCallback) {
errorCallback(error.toString());
}
} }
); );
});
}; };
Respack.prototype.parseWholeZip = function() { Respack.prototype.parseZip = function(zip) {
// TODO might break on bad file var entries = zip.entries;
console.log("Loading new respack: " + this.file.root.children[0].name);
var entries = this.file.entries;
this.totalFiles = 0; this.totalFiles = 0;
// Progress events // Progress events
this.filesToLoad = 0; this.filesToLoad = 0;
this.filesLoaded = 0; this.filesLoaded = 0;
// Get everything started
for(var i = 0; i < entries.length; i++) { for(var i = 0; i < entries.length; i++) {
if(!entries[i].directory && entries[i].name) { if(!entries[i].directory && entries[i].name) {
this.totalFiles++; this.totalFiles++;
@ -139,29 +154,36 @@ Respack.prototype.parseWholeZip = function() {
} }
} }
debug("ZIP loader: trying to finish"); return this.parseSongQueue()
this.tryFinish(); .then(() => {
return this.parseImageQueue();
}).then(() => {
return this.parseXML();
}).then(() => {
console.log("Loaded", this.name, "successfully with", this.songs.length,
"songs and", this.images.length, "images.");
});
}; };
Respack.prototype.parseFile = function(file) { Respack.prototype.parseFile = function(file) {
var name = file.name; var name = file.name;
if (name.match(this.audioExtensions)) { if (name.match(this.audioExtensions)) {
this.parseSong(file); this.songQueue.push(this.parseSong(file));
this.filesToLoad++; this.filesToLoad++;
} else if (name.match(this.imageExtensions)) { } else if (name.match(this.imageExtensions)) {
this.parseImage(file); this.imageQueue.push(this.parseImage(file));
this.filesToLoad++; this.filesToLoad++;
} }
else { else {
switch(name.toLowerCase()) { switch(name.toLowerCase()) {
case "songs.xml": case "songs.xml":
this._songFile = file; this._songXMLPromise = this.loadXML(file);
break; break;
case "images.xml": case "images.xml":
this._imageFile = file; this._imageXMLPromise = this.loadXML(file);
break; break;
case "info.xml": case "info.xml":
this._infoFile = file; this._infoXMLPromise = this.loadXML(file);
break; break;
default: default:
} }
@ -169,60 +191,191 @@ Respack.prototype.parseFile = function(file) {
}; };
Respack.prototype.parseSong = function(file) { Respack.prototype.parseSong = function(file) {
this.songQueue.push(file); var name = file.name.replace(this.audioExtensions, "");
debug("parsing song: " + name);
if (this.containsSong(name)) {
var oldSong = this.getSong(name);
debug("WARNING: Song", name, "already exists! Conflict with", name, "and", oldSong.name);
} else {
var newSong = {"name":name,
"title":null,
"rhythm":null,
"source":null,
//"crc":this.quickCRC(file), TODO
"sound":null,
"enabled":true,
"filename":file.name,
"charsPerBeat": null};
var extension = file.name.split('.').pop().toLowerCase();
var mime = "";
switch(extension) {
case "mp3":
mime = "audio/mpeg3";
break;
case "ogg":
mime = "audio/ogg";
break;
default:
mime = "application/octet-stream";
}
this.songs.push(newSong);
return new Promise((resolve, reject) => {
file.getBlob(mime, sound => {
resolve(sound);
});
}).then(blob => {
return new Promise((resolve, reject) => {
// Because blobs are crap
var fr = new FileReader();
fr.onload = () => {
resolve(fr.result);
};
fr.readAsArrayBuffer(blob);
});
}).then(sound => {
newSong.sound = sound;
this.filesLoaded++;
this.updateProgress();
});
}
}; };
Respack.prototype.parseSongQueue = function() {
return this.songQueue.reduce((sequence, songPromise) => {
return sequence.then(() => {
// Maintain order
return songPromise;
});
}, Promise.resolve());
}
Respack.prototype.parseImage = function(file) { Respack.prototype.parseImage = function(file) {
this.imageQueue.push(file); var match;
var name = file.name.replace(this.imageExtensions, "");
var img;
// Animation
if((match = name.match(new RegExp("^(.*)_(\\d+)$")))) {
var img = this.getImage(match[1]);
if(!img) { // make a fresh one
img = {"name":match[1],
"fullname":match[1],
"align":"center",
//"crc":this.quickCRC(file),
"bitmaps":[],
"frameDurations":[33],
"source":null,
"enabled":true,
"animated":true,
"beatsPerAnim": null};
this.images.push(img);
}
// Normal image
} else if (!this.containsImage(name)) {
var img = {"name":name,
"fullname":name,
"bitmap":null,
"align":"center",
//"crc":this.quickCRC(file),
"source":null,
"enabled":true,
"filename":file.name,
"animated":false};
this.images.push(img);
} else {
var existing = this.getImage(name);
debug("WARNING: Image", name, "already exists! Conflict with", file.name, "and", existing.name);
return;
}
return this.loadImage(file, img);
}; };
Respack.prototype.parseXML = function() { Respack.prototype.loadImage = function(imgFile, imageObj) {
if (this._infoFile) { var extension = imgFile.name.split('.').pop().toLowerCase();
this._infoFile.getText(text => { var mime = "";
switch(extension) {
case "png":
mime = "image/png";
break;
case "gif":
mime = "image/gif";
break;
case "jpg":
case "jpeg":
mime = "image/jpeg";
break;
default:
mime = "application/octet-stream";
}
return new Promise((resolve, reject) => {
imgFile.getData64URI(mime, resolve);
}).then(bitmap => {
return {bitmap: bitmap, img: imageObj};
});
};
Respack.prototype.parseImageQueue = function() {
return this.imageQueue.reduce((sequence, imagePromise) => {
return sequence.then(() => {
// Maintain order
return imagePromise;
}).then(response => {
var newImg = new Image();
newImg.src = response.bitmap;
if (response.img.animated) {
response.img.bitmaps.push(newImg);
} else {
response.img.bitmap = newImg;
}
this.filesLoaded++;
this.updateProgress();
});
}, Promise.resolve());
}
Respack.prototype.loadXML = function(file) {
return new Promise((resolve, reject) => {
file.getText(text => {
//XML parser will complain about a bare '&', but some respacks use &amp
text = text.replace(/&amp;/g, '&'); text = text.replace(/&amp;/g, '&');
text = text.replace(/&/g, '&amp;'); text = text.replace(/&/g, '&amp;');
resolve(text);
});
});
}
Respack.prototype.parseXML = function() {
var p = Promise.resolve();
// info xml?
if(this._infoXMLPromise) {
p = p.then(() => {
return this._infoXMLPromise;
}).then(text => {
this.parseInfoFile(text); this.parseInfoFile(text);
this._infoFile = null;
this.parseXML();
}); });
return;
} }
// song xml and songs exist?
if (this.songs.length > 0) { if (this.songs.length > 0) {
if (this._songFile) { if(this._songXMLPromise) {
this._songFile.getText(text => { p = p.then(() => {
//XML parser will complain about a bare '&', but some respacks use &amp return this._songXMLPromise;
text = text.replace(/&amp;/g, '&'); }).then(text => {
text = text.replace(/&/g, '&amp;');
this.parseSongFile(text); this.parseSongFile(text);
// Go to next in series
this._songFile = null;
this._songFileParsed = true;
this.parseXML();
}); });
return; } else {
} else if(!this._songFileParsed) {
console.log("!!!", "Got songs but no songs.xml!"); console.log("!!!", "Got songs but no songs.xml!");
this._songFileParsed = true;
} }
} }
if (this.images.length > 0 && this._imageFile) { // images xml and images exist?
this._imageFile.getText(text => { if (this.images.length > 0 && this._imageXMLPromise) {
text = text.replace(/&amp;/g, '&'); p = p.then(() => {
text = text.replace(/&/g, '&amp;'); return this._imageXMLPromise;
}).then(text => {
this.parseImageFile(text); this.parseImageFile(text);
this._imageFile = null;
this.parseXML();
}); });
return;
}
// Finally done!
this.file = null;
console.log("Loaded", this.name, "successfully with", this.songs.length,
"songs and", this.images.length, "images.");
if(this._completionCallback) {
this._completionCallback();
} }
return p;
}; };
// Save some chars // Save some chars
@ -410,138 +563,6 @@ Respack.prototype.getImage = function(name) {
return null; return null;
}; };
Respack.prototype.parseSongQueue = function() {
var songFile = this.songQueue.shift();
var name = songFile.name.replace(this.audioExtensions, "");
debug("parsing song: " + name);
if (this.containsSong(name)) {
var oldSong = this.getSong(name);
debug("WARNING: Song", name, "already exists! Conflict with", name, "and", oldSong.name);
} else {
var newSong = {"name":name,
"title":null,
"rhythm":null,
"source":null,
//"crc":this.quickCRC(file), TODO
"sound":null,
"enabled":true,
"filename":songFile.name,
"charsPerBeat": null};
var extension = songFile.name.split('.').pop().toLowerCase();
var mime = "";
switch(extension) {
case "mp3":
mime = "audio/mpeg3";
break;
case "ogg":
mime = "audio/ogg";
newSong.noTrim = true;
break;
default:
mime = "application/octet-stream";
}
songFile.getBlob(mime, sound => {
// Because blobs are crap
var fr = new FileReader();
fr.onload = () => {
newSong.sound = fr.result;
this.filesLoaded++;
this.updateProgress();
this.tryFinish();
};
fr.readAsArrayBuffer(sound);
});
this.songs.push(newSong);
}
};
Respack.prototype.parseImageQueue = function() {
var match;
var imgFile = this.imageQueue.shift();
var name = imgFile.name.replace(this.imageExtensions, "");
if((match = name.match(new RegExp("^(.*)_(\\d+)$")))) {
var anim = this.getImage(match[1]);
if(!anim) { // make a fresh one
anim = {"name":match[1],
"fullname":match[1],
"align":"center",
//"crc":this.quickCRC(imgFile),
"bitmaps":[],
"frameDurations":[33],
"source":null,
"enabled":true,
"animated":true,
"beatsPerAnim": null};
this.images.push(anim);
}
this.imageLoadStart(imgFile, anim);
} else if (!this.containsImage(name)) {
var img = {"name":name,
"fullname":name,
"bitmap":null,
"align":"center",
//"crc":this.quickCRC(imgFile),
"source":null,
"enabled":true,
"filename":imgFile.name,
"animated":false};
this.images.push(img);
this.imageLoadStart(imgFile, img);
} else {
var existing = this.getImage(name);
debug("WARNING: Image", name, "already exists! Conflict with", imgFile.name, "and", existing.name);
}
};
Respack.prototype.imageLoadStart = function(imgFile, imageObj) {
var extension = imgFile.name.split('.').pop().toLowerCase();
var mime = "";
switch(extension) {
case "png":
mime = "image/png";
break;
case "gif":
mime = "image/gif";
break;
case "jpg":
case "jpeg":
mime = "image/jpeg";
break;
default:
mime = "application/octet-stream";
}
imgFile.getData64URI(mime, image => {
this.imageLoadComplete(image, imageObj);
});
};
Respack.prototype.imageLoadComplete = function(imageBmp, imageObj) {
var newImg = new Image();
newImg.src = imageBmp;
if (imageObj.animated) {
imageObj.bitmaps.push(newImg);
} else {
imageObj.bitmap = newImg;
debug("parsing image:", imageObj.name);
}
this.filesLoaded++;
this.updateProgress();
this.tryFinish();
};
Respack.prototype.tryFinish = function() {
if (this.imageQueue.length > 0) {
this.parseImageQueue();
} else if(this.songQueue.length > 0) {
this.parseSongQueue();
} else {
debug("Finished parsing images/songs, parsing xml files...");
this.parseXML();
}
};
window.Respack = Respack; window.Respack = Respack;
})(window, document); })(window, document);
Loading…
Cancel
Save