Transitioned to shared Javascript library

There was a lot of repeated JS across all my tools (e.g., image conversion, BIOS hash checking, etc.), and so I decided to move all shared code out to its own separate library. This will improve scalability and maintainability at the cost of portability - a fair trade in my opinion.

This work also involved large-scale refactoring of all of the tools. There's no actual functional difference (all the tools should behave exactly the same), bar some UI improvements for the oldest tools as a result of moving to the same codebase used by the more recent tools.
This commit is contained in:
vonmillhausen 2023-06-26 14:11:01 +01:00
parent 617b8e5365
commit 25ce2e3221
6 changed files with 1184 additions and 1229 deletions

View File

@ -9,26 +9,31 @@
<body> <body>
<h1>Data Frog SF2000 BIOS CRC32 Patcher</h1> <h1>Data Frog SF2000 BIOS CRC32 Patcher</h1>
<p>This tool will recalculate and patch the CRC32 check bits for a modified <code>bisrv.asd</code> BIOS file for the SF2000; only really useful if you've manually modified data outside of the "safe" areas covered by my other tools. If you haven't done your own hex editing of the BIOS, then this tool probably isn't for you! 🙂</p> <p>This tool will recalculate and patch the CRC32 check bits for a modified <code>bisrv.asd</code> BIOS file for the SF2000; only really useful if you've manually modified data outside of the "safe" areas covered by my other tools. If you haven't done your own hex editing of the BIOS, then this tool probably isn't for you! 🙂</p>
<p><strong><em>IMPORTANT NOTE:</em></strong> This tool will just blindly modify bytes <code>0x18c</code> to <code>0x18f</code> of whatever input file you provide; correct usage of this tool is therefore up to you, not me! 🤣</p> <p id="initialWarning"></p>
<hr> <hr>
<div id="steps"> <div id="steps">
<section id="bisrvSection"> <section id="bisrvSection">
<h2>Step 1: Select Your <code>bisrv.asd</code></h2> <h2>Step 1: Select Your <code>bisrv.asd</code></h2>
<p>Select your <code>bisrv.asd</code> file. You should probably make a backup of the file first, just in case!</p> <p>Select your <code>bisrv.asd</code> file. You should probably make a backup of the file first, just in case!</p>
<div id="bisrvOutput"></div> <div id="bisrvMessages"></div>
<div class="controlContainer"> <div class="controlContainer">
<label class="control">Open <code>bisrv.asd</code>: <input id="bisrvSelector" type="file" accept=".asd,application/octet-stream" onchange="bisrvLoad(event.target.files[0])"></label> <label class="control">Open <code>bisrv.asd</code>: <input id="bisrvSelector" type="file" accept=".asd,application/octet-stream" onchange="bisrvLoad(event.target.files[0])"></label>
</div> </div>
</section> </section>
</div> </div>
<script src="tools.js"></script>
<script type="text/javascript"> <script type="text/javascript">
// Global variables... // Global variables...
var bisrvData; // Used to store the binary data from the bisrv.asd file var bisrvData; // Used to store the binary data from the bisrv.asd file
// Display our initial warning message
setMessage("warning", "initialWarning", "<strong><em>WARNING:</em></strong> This tool will just blindly modify bytes <code>0x18c</code> to <code>0x18f</code> of whatever input file you provide; correct usage of this tool is therefore up to you, not me! 🤣");
function bisrvLoad(file) { function bisrvLoad(file) {
// We've got a new file - clear any old error messages and any HTML after // We've got a new file - clear any old error messages and any HTML after
// Step 1... // Step 1...
document.getElementById("bisrvOutput").innerHTML = ""; document.getElementById("bisrvMessages").innerHTML = "";
while(document.getElementById("bisrvSection").nextSibling) { while(document.getElementById("bisrvSection").nextSibling) {
document.getElementById("bisrvSection").nextSibling.remove(); document.getElementById("bisrvSection").nextSibling.remove();
} }
@ -36,12 +41,12 @@
// Check to make sure the file is at least ~512-bytes-ish long (CRC32 for // Check to make sure the file is at least ~512-bytes-ish long (CRC32 for
// bisrv.asd is calculated for bytes 512 onwards)... // bisrv.asd is calculated for bytes 512 onwards)...
if (file.size < 520) { if (file.size < 520) {
document.getElementById("bisrvOutput").innerHTML = "<p class=\"errorMessage\">ERROR: Provided file is too small!</p>"; setMessage("error", "bisrvMessages", "ERROR: Provided file is too small!");
return; return;
} }
// If we're here then we should be good - read in our file and store its // If we're here then we should be good - read in our file and store its
// contents in bisrvData... // contents in bisrvData, then patch the CRC32 bytes...
var frBisrv = new FileReader(); var frBisrv = new FileReader();
frBisrv.readAsArrayBuffer(file); frBisrv.readAsArrayBuffer(file);
frBisrv.onload = function(event) { frBisrv.onload = function(event) {
@ -49,6 +54,9 @@
// Read the provided file's data into an array... // Read the provided file's data into an array...
bisrvData = new Uint8Array(event.target.result); bisrvData = new Uint8Array(event.target.result);
// Patch the CRC32 bytes...
patchCRC32(bisrvData);
// And display Step 2... // And display Step 2...
stepTwo(); stepTwo();
}; };
@ -68,47 +76,11 @@
// Attach our event handler to our download button... // Attach our event handler to our download button...
var dButton = document.getElementById("downloadButton"); var dButton = document.getElementById("downloadButton");
dButton.addEventListener("click", function() { dButton.addEventListener("click", function() {
download(); downloadToBrowser(bisrvData, "application/octet-stream", "bisrv.asd");
}); });
} }
function download() {
// Easy peasy - we're going to recalculate the CRC32 check bytes for our binary
// data, plonk them into the data in the right place, and then send it to the
// user's browser...
// Calculate a new CRC32 for the updated bisrv.asd and apply it; credit to
// osaka#9664 for this code!
var c;
var tabCRC32 = new Int32Array(256);
for (var i = 0; i < 256; i++) {
c = i << 24;
for (var j = 0; j < 8; j++) {
c = c & (1 << 31) ? c << 1 ^ 0x4c11db7 : c << 1;
}
tabCRC32[i] = c;
}
c = ~0;
for (var i = 512; i < bisrvData.length; i++) {
c = c << 8 ^ tabCRC32[c >>> 24 ^ bisrvData[i]];
}
bisrvData[0x18c] = c & 255;
bisrvData[0x18d] = c >>> 8 & 255;
bisrvData[0x18e] = c >>> 16 & 255;
bisrvData[0x18f] = c >>> 24;
// Send the data to the user's browser as a file download...
var link = document.createElement("a");
link.href = window.URL.createObjectURL(new Blob([bisrvData], {type: "application/octet-stream"}));
link.download = "bisrv.asd";
link.style.display = "none";
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(link.href);
document.body.removeChild(link);
}
</script> </script>
<hr> <hr>
<p><a rel="license" href="https://creativecommons.org/publicdomain/zero/1.0/">CC0</a>: public domain. Version 1.0, 20230526.1</p> <p><a rel="license" href="https://creativecommons.org/publicdomain/zero/1.0/">CC0</a>: public domain. Version 1.1, 20230626.1</p>
</body> </body>
</html> </html>

View File

@ -10,31 +10,17 @@
<h1>Data Frog SF2000 Boot Logo Changer</h1> <h1>Data Frog SF2000 Boot Logo Changer</h1>
<p>This tool can be used to alter the boot logo on an SF2000 handheld gaming console. Please note this tool is provided as-is, and no support will be given if this corrupts your device's bios; make sure you have backups of anything you care about before messing with your device's critical files! 🙂</p> <p>This tool can be used to alter the boot logo on an SF2000 handheld gaming console. Please note this tool is provided as-is, and no support will be given if this corrupts your device's bios; make sure you have backups of anything you care about before messing with your device's critical files! 🙂</p>
<hr> <hr>
<div id="steps">
<section id="bisrvSection"> <section id="bisrvSection">
<h2>Step 1: Select Original <code>bisrv.asd</code></h2> <h2>Step 1: Select Original <code>bisrv.asd</code></h2>
<p>Select the <code>bisrv.asd</code> file whose boot logo you want to modify. You should probably make a backup of the file first, just in case! You can find the <code>bisrv.asd</code> file in the <code>bios</code> folder on your microSD card.</p> <p>Select the <code>bisrv.asd</code> file whose boot logo you want to modify. You should probably make a backup of the file first, just in case! You can find the <code>bisrv.asd</code> file in the <code>bios</code> folder on your microSD card.</p>
<form id="bisrvForm" action="#"> <div id="bisrvMessages"></div>
<label>Open <code>bisrv.asd</code>: <input id="bisrvSelector" type="file" onchange="bisrvLoad(event.target.files[0])"></label> <div class="controlContainer">
</form> <label class="control">Open <code>bisrv.asd</code>: <input id="bisrvSelector" type="file" accept=".asd" onchange="bisrvLoad(event.target.files[0])"></label>
<div id="bisrvOutput"></div> </div>
</section>
<hr>
<section id="imageSection">
<h2>Step 2: Select New Logo Image File</h2>
<p>Select the image file you want to use as the new boot logo. It must be 256 pixels wide and 100 pixels tall. Only PNG and JPEG image types are supported. For PNG files, transparency is ignored; also, you might want to make sure your PNG image has any embedded gamma or colour profile information removed first using a metadata scrubbing tool, otherwise the colour output from <i>this</i> tool might be incorrect. The image is displayed centred on a black background, so you may want to factor that into your design as well.</p>
<form id="imageForm" action="#">
<label>Open image: <input id="imageSelector" type="file" onchange="imageLoad(event.target.files[0])" disabled></label>
</form>
<div id="imageOutput"></div>
</section>
<hr>
<section id="downloadSection">
<h2>Step 3: Download Updated <code>bisrv.asd</code></h2>
<p>Click the download button for the updated <code>bisrv.asd</code> file; use it to replace the one in the <code>bios</code> folder on your SF2000's microSD card.</p>
<form id="downloadForm" action="#">
<input id="downloadButton" type="button" value="Download" onclick="download()" disabled>
</form>
</section> </section>
</div>
<script src="tools.js"></script>
<script type="text/javascript"> <script type="text/javascript">
// Global variables... // Global variables...
@ -42,262 +28,177 @@
var logoOffset; // Will contain the offset of the boot logo within the bisrv.asd file var logoOffset; // Will contain the offset of the boot logo within the bisrv.asd file
var newLogoData; // Used to store the little-endian RGB565 binary data of the new boot logo var newLogoData; // Used to store the little-endian RGB565 binary data of the new boot logo
// Define a function that takes a Uint8Array and an optional offset and returns the index // This function is triggered when the person selects a file in Step 1...
// of the first match or -1 if not found...
function findSequence(needle, haystack, offset) {
// If offset is not provided, default to 0
offset = offset || 0;
// Loop through the data array starting from the offset
for (var i = offset; i < haystack.length - needle.length + 1; i++) {
// Assume a match until proven otherwise
var match = true;
// Loop through the target sequence and compare each byte
for (var j = 0; j < needle.length; j++) {
if (haystack[i + j] !== needle[j]) {
// Mismatch found, break the inner loop and continue the outer loop
match = false;
break;
}
}
// If match is still true after the inner loop, we have found a match
if (match) {
// Return the index of the first byte of the match
return i;
}
}
// If we reach this point, no match was found
return -1;
}
// Returns an SHA-256 hash of a given firmware (ignoring common user changes), or returns
// false on failure...
function getFirmwareHash(data) {
// Data should be a Uint8Array, which as an object is passed by reference... we're going
// to be manipulating that data before generating our hash, but we don't want to modify
// the original object at all... so we'll create a copy, and work only on the copy...
var dataCopy = data.slice();
// Only really worthwhile doing this for big bisrv.asd files...
if (dataCopy.length > 12640000) {
// First, replace CRC32 bits with 00...
dataCopy[396] = 0x00;
dataCopy[397] = 0x00;
dataCopy[398] = 0x00;
dataCopy[399] = 0x00;
// Next identify the boot logo position, and blank it out too...
var badExceptionOffset = findSequence([0x62, 0x61, 0x64, 0x5F, 0x65, 0x78, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x00, 0x00, 0x00], dataCopy);
if (badExceptionOffset > -1) {
var bootLogoStart = badExceptionOffset + 16;
for (var i = bootLogoStart; i < (bootLogoStart + 204800); i++) {
dataCopy[i] = 0x00;
}
}
else {
return false;
}
// Next identify the emulator button mappings (if they exist), and blank them out too...
var preButtonMapOffset = findSequence([0x00, 0x00, 0x00, 0x71, 0xDB, 0x8E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], dataCopy);
if (preButtonMapOffset > -1) {
var postButtonMapOffset = findSequence([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00], dataCopy, preButtonMapOffset);
if (postButtonMapOffset > -1) {
for (var i = preButtonMapOffset + 16; i < postButtonMapOffset; i++) {
dataCopy[i] = 0x00;
}
}
else {
return false;
}
}
else {
return false;
}
// If we're here, we've zeroed-out all of the bits of the firmware that are
// semi-user modifiable (boot logo, button mappings and the CRC32 bits); now
// we can generate a hash of what's left and compare it against some known
// values...
return crypto.subtle.digest("SHA-256", dataCopy.buffer)
.then(function(digest) {
var array = Array.from(new Uint8Array(digest));
var hash = array.map(byte => ("00" + byte.toString(16)).slice(-2)).join("");
return hash;
})
.catch(function(error) {
return false;
});
}
else {
return false;
}
}
function bisrvLoad(file) { function bisrvLoad(file) {
// Read in the contents of the selected file as array buffer...
var frBisrv = new FileReader(); var frBisrv = new FileReader();
frBisrv.readAsArrayBuffer(file); frBisrv.readAsArrayBuffer(file);
frBisrv.onload = function(event) { frBisrv.onload = function(event) {
// Clear out any HTML that might already exist after Step 1...
while(document.getElementById("bisrvSection").nextSibling) {
document.getElementById("bisrvSection").nextSibling.remove();
}
// Clear any old messages...
document.getElementById("bisrvMessages").innerHTML = "";
// Read the provided file's data into an array... // Read the provided file's data into an array...
var data = new Uint8Array(event.target.result); bisrvData = new Uint8Array(event.target.result);
// We'll do a hash-check against it... // We'll do a hash-check against it...
hashResult = getFirmwareHash(data); hashResult = getFirmwareHash(bisrvData);
// The result could be either a Promise if it had a bisrv.asd-like structure and we got // The result could be either a Promise if it had a bisrv.asd-like structure and we got
// a hash, or false otherwise... let's check! // a hash, or false otherwise... let's check!
if (hashResult instanceof Promise) { if (hashResult instanceof Promise) {
// We got a Promise! Wait for it to finish so we get our bisrv.asd hash... // We got a Promise! Wait for it to finish so we get our bisrv.asd hash...
hashResult.then(function(dataHash) { hashResult.then(function(dataHash) {
// Check the hash against all the known good ones... // Check the hash against all the known good ones...
switch (dataHash) { switch (knownHash(dataHash)) {
// Mid-March BIOS... // Mid-March BIOS...
case "4411143d3030adc442e99f7ac4e7772f300c844bbe10d639702fb3ba049a4ee1": case "03.15":
logoOffset = 0x9B9030; logoOffset = 0x9B9030;
bisrvData = data; setMessage("info", "bisrvMessages", "INFO: Mid-March <code>bisrv.asd</code> detected.");
document.getElementById("bisrvOutput").innerHTML = "<p class=\"infoMessage\">INFO: Mid-March <code>bisrv.asd</code> detected</p>";
break; break;
// April 20th BIOS... // April 20th BIOS...
case "b50e50aa4b1b1d41489586e989f09d47c4e2bc27c072cb0112f83e6bc04e2cca": case "04.20":
logoOffset = 0x9B91D8; logoOffset = 0x9B91D8;
bisrvData = data; setMessage("info", "bisrvMessages", "INFO: April 20th <code>bisrv.asd</code> detected.");
document.getElementById("bisrvOutput").innerHTML = "<p class=\"infoMessage\">INFO: April 20th <code>bisrv.asd</code> detected</p>";
break; break;
// May 15th BIOS... // May 15th BIOS...
case "d878a99d26242836178b452814e916bef532d05acfcc24d71baa31b8b6f38ffd": case "05.15":
logoOffset = 0x9BB0B8; logoOffset = 0x9BB0B8;
bisrvData = data; setMessage("info", "bisrvMessages", "INFO: May 15th <code>bisrv.asd</code> detected.");
document.getElementById("bisrvOutput").innerHTML = "<p class=\"infoMessage\">INFO: May 15th bisrv.asd detected</p>";
break; break;
// May 22nd BIOS... // May 22nd BIOS...
case "6aebab0e4da39e0a997df255ad6a1bd12fdd356cdf51a85c614d47109a0d7d07": case "05.22":
logoOffset = 0x9BB098; logoOffset = 0x9BB098;
bisrvData = data; setMessage("info", "bisrvMessages", "INFO: May 22nd <code>bisrv.asd</code> detected.");
document.getElementById("bisrvOutput").innerHTML = "<p class=\"infoMessage\">INFO: May 22nd bisrv.asd detected</p>";
break; break;
default: default:
// Huh... wasn't false so had bisrv.asd structure, but didn't return // Huh... wasn't false so had bisrv.asd structure, but didn't return
// a known hash... a new BIOS version? Unknown anyway! // a known hash... a new BIOS version? Unknown anyway!
console.log(dataHash); console.log(dataHash);
document.getElementById("bisrvOutput").innerHTML = "<p class=\"errorMessage\">ERROR: While the file you've selected does appear to be generally structured like the SF2000's <code>bisrv.asd</code> BIOS file, the specifics of your file don't match any known SF2000 BIOS version. As such, this tool cannot modify the selected file.</p>"; setMessage("error", "bisrvMessages", "ERROR: While the file you've selected does appear to be generally structured like the SF2000's <code>bisrv.asd</code> BIOS file, the specifics of your file don't match any known SF2000 BIOS version. As such, this tool cannot safely modify the selected file.");
return; return;
break;
} }
// If we're here we've got a good file, so enable the input for step 2 (image selection)... // If we're here we've got a good file, so onwards to Step 2 (image selection)...
document.getElementById("imageSelector").removeAttribute("disabled"); stepTwo();
}); });
} }
else { else {
// We got false, so whatever it was, it wasn't a bisrv.asd... // We got false, so whatever it was, it wasn't a bisrv.asd...
document.getElementById("bisrvOutput").innerHTML = "<p class=\"errorMessage\">ERROR: The file you've selected doesn't appear to have the same data structure as expected for a <code>bisrv.asd</code> file.</p>"; setMessage("error", "bisrvMessages", "ERROR: The file you've selected doesn't appear to have the same data structure as expected for a <code>bisrv.asd</code> file.");
return; return;
} }
}; };
} }
function imageLoad(file) { // This function triggers when the user has selected a known `bisrv.asd`
var frImage = new FileReader(); // variant...
function stepTwo() {
// We're going to build up the HTML for Step 2, which has the user
// browse for a PNG or JPEG image file. We need to validate the chosen
// file is an image, and that it has the correct dimensions of 256x100
// pixels; if it is, we convert the image data to an SF2000 format and
// move on to Step 3. First, let's start building our HTML...
var html = "<section id=\"imageSection\"><h2>Step 2: Select New Logo Image File</h2><p>Select the image file you want to use as the new boot logo. It must be 256 pixels wide and 100 pixels tall. Only PNG and JPEG image types are supported. For PNG files, transparency is ignored; also, you might want to make sure your PNG image has any embedded gamma or colour profile information removed first using a metadata scrubbing tool, otherwise the colour output from <i>this</i> tool might be incorrect. The image is displayed centred on a black background, so you may want to factor that into your design as well.</p><div id=\"imageMessages\"></div>";
// Add our file chooser...
html += "<div class=\"controlContainer\"><label class=\"control\"><input id=\"inputImage\" type=\"file\"></label></div>";
// Close our section...
html += "</section>";
// Finally, add a <hr> separator after the last step, and append the new step...
document.getElementById("steps").insertAdjacentHTML("beforeend", "<hr>");
document.getElementById("steps").insertAdjacentHTML("beforeend", html);
// Attach our event handler to our new file input control...
var userImageInput = document.getElementById("inputImage");
userImageInput.addEventListener("change", function() {
// The user has chosen a new file; it should be either a PNG or JPEG
// image for converting to an SF2000 binary image format...
// First let's clear any old messages...
document.getElementById("imageMessages").innerHTML = "";
// And clear up any HTML already added after the current step...
while(document.getElementById("imageSection").nextSibling) {
document.getElementById("imageSection").nextSibling.remove();
}
// Now let's read in the file...
var frImage = new FileReader();
frImage.readAsDataURL(event.target.files[0]);
frImage.onload = function(event) { frImage.onload = function(event) {
// First check to make sure the selected file's data URL includes a PNG or JPEG data type... // The file is loaded; let's check to make sure we got a PNG or JPEG...
if (!event.target.result.includes("data:image/png;") && !event.target.result.includes("data:image/jpeg;")) { if (!event.target.result.includes("data:image/png;") && !event.target.result.includes("data:image/jpeg;")) {
document.getElementById("imageOutput").innerHTML = "<p class=\"errorMessage\">ERROR: The selected file does not appear to be a PNG or JPEG image file!</p>";
document.getElementById("downloadButton").setAttribute("disabled", ""); // Whoops! Doesn't look like the user provided a PNG or a JPEG!
setMessage("error", "imageMessages", "ERROR: The selected file does not appear to be either a PNG or JPEG image; please make sure you're selecting an appropriate image file.");
return; return;
} }
// Create an image and set its src to our data URL; this triggers the onload event if // Now we're going to load the file's data into an Image object, so we can
// it's a valid image file... // access the raw image data...
var img = new Image; var img = new Image;
img.src = event.target.result; img.src = event.target.result;
img.onload = function() { img.onload = function(event) {
// Check to make sure the image has the right dimensions for the boot logo...
// Check to make sure the image has the correct dimensions for a
// boot logo...
if (img.width != 256 || img.height != 100) { if (img.width != 256 || img.height != 100) {
document.getElementById("imageOutput").innerHTML = "<p class=\"errorMessage\">ERROR: The selected image does not have dimensions of 256x100px!</p>"; setMessage("error", "imageMessages", "ERROR: The selected image does not have dimensions of 256x100px!");
document.getElementById("downloadButton").setAttribute("disabled", "");
return; return;
} }
// Create a virtual canvas, and load it up with our image file... // We need to scale the image to the internal resolution used by
var canv = document.createElement("canvas"); // the SF2000 (512x200)...
var cont = canv.getContext("2d"); var imageData = scaleImage(imageToImageData(img), 512, 200, "Nearest Neighbour");
cont.canvas.width = 512;
cont.canvas.height = 200;
// Draw our image to the canvas, which will allow us to get data about the image... // Next we'll convert the image data to little-endian RGB565
cont.drawImage(img, 0, 0, 256, 100); // format for the SF2000...
var data = cont.getImageData(0, 0, 256, 100).data; newLogoData = imageDataToRgb565(imageData);
// Now we're going to scale up the image to 512x200 (the internal image size used // On to Step 3!
// within the bios); we do this manually to get "nearest neighbour" scaling... stepThree();
for (var x = 0; x < img.width; ++x) {
for (var y = 0; y < img.height; ++y) {
var i = (y*img.width + x)*4;
var r = data[i];
var g = data[i+1];
var b = data[i+2];
cont.fillStyle = "rgb("+ r +", " + g + ", " + b + ")";
cont.fillRect(x*2, y*2, 2, 2);
} }
} }
data = cont.getImageData(0, 0, 512, 200).data; });
// Loop through the image data, and convert it to little-endian RGB565. First,
// we'll store the raw RGB565-converted integers in an array, one entry per pixel...
var intArray = [];
var pixelCount = 0;
for (var i = 0; i < data.length; i += 4){
// Read in the raw source RGB colours from the image data stream...
var red = data[i];
var green = data[i+1];
var blue = data[i+2];
// Use some shifting and masking to get a big-endian version of the RGB565 colour
// and store it in our array before moving on...
intArray[pixelCount] = ((red & 248)<<8) + ((green & 252)<<3) + (blue>>3);
pixelCount++;
} }
// Create a data buffer and a data view; we'll use the view to convert our int function stepThree() {
// array data to little-endian format (the "true" below) to be stored in the buffer... var html = "<section id=\"downloadSection\"><h2>Step 3: Download Updated <code>bisrv.asd</code></h2><p>Click the download button for the updated <code>bisrv.asd</code> file; use it to replace the one in the <code>bios</code> folder on your SF2000's microSD card.</p><div id=\"downloadMessages\"></div>";
var buffer = new ArrayBuffer(intArray.length * 2);
var dataView = new DataView(buffer);
for (var i = 0; i < intArray.length; i++) {
dataView.setInt16(i * 2, intArray[i], true);
}
// Use the buffer to fill a Uint8Array, which we'll assign to our global... // Add our download button...
newLogoData = new Uint8Array(buffer); html += "<div class=\"controlContainer\"><div class=\"control\"><input id=\"userDownload\" type=\"button\" value=\"Download\"></div></div>";
// We should be all done with this step; enable the download button and give the // Close our section...
// user some UI feedback... html += "</section>";
document.getElementById("imageOutput").innerHTML = "<p class=\"infoMessage\">INFO: Image successfully converted to RGB565 little-endian data stream.</p>";
document.getElementById("downloadButton").removeAttribute("disabled");
}
};
frImage.readAsDataURL(file); // Finally, add a <hr> separator after the last step, and append the new step...
} document.getElementById("steps").insertAdjacentHTML("beforeend", "<hr>");
document.getElementById("steps").insertAdjacentHTML("beforeend", html);
// Let's add the event handler for our Download button...
var dButton = document.getElementById("userDownload");
dButton.addEventListener("click", function() {
function download() {
// So, we should have the original bisrv.asd data in bisrvData; and we should have // So, we should have the original bisrv.asd data in bisrvData; and we should have
// the offset to the original logo data in logoOffset; and finally, we should have // the offset to the original logo data in logoOffset; and finally, we should have
// our new logo's binary data in newLogoData. All we need to do is replace the old // our new logo's binary data in newLogoData. All we need to do is replace the old
@ -309,38 +210,16 @@
bisrvData[logoOffset + i] = newLogoData[i]; bisrvData[logoOffset + i] = newLogoData[i];
} }
// Next, we calculate a new CRC32 for the updated bisrv.asd and apply it; credit // Next, we calculate a new CRC32 for the updated bisrv.asd and apply it...
// to osaka#9664 for this code! patchCRC32(bisrvData);
var c;
var tabCRC32 = new Int32Array(256);
for (var i = 0; i < 256; i++) {
c = i << 24;
for (var j = 0; j < 8; j++) {
c = c & (1 << 31) ? c << 1 ^ 0x4c11db7 : c << 1;
}
tabCRC32[i] = c;
}
c = ~0;
for (var i = 512; i < bisrvData.length; i++) {
c = c << 8 ^ tabCRC32[c >>> 24 ^ bisrvData[i]];
}
bisrvData[0x18c] = c & 255;
bisrvData[0x18d] = c >>> 8 & 255;
bisrvData[0x18e] = c >>> 16 & 255;
bisrvData[0x18f] = c >>> 24;
// And finally, send the data to the user's browser as a file download... // And finally, send the data to the user's browser as a file download...
var link = document.createElement("a"); downloadToBrowser(bisrvData, "application/octet-stream", "bisrv.asd");
link.href = window.URL.createObjectURL(new Blob([bisrvData], {type: "application/octet-stream"}));
link.download = "bisrv.asd"; });
link.style.display = "none";
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(link.href);
document.body.removeChild(link);
} }
</script> </script>
<hr> <hr>
<p><a rel="license" href="https://creativecommons.org/publicdomain/zero/1.0/">CC0</a>: public domain. Version 1.2, 20230522.1</p> <p><a rel="license" href="https://creativecommons.org/publicdomain/zero/1.0/">CC0</a>: public domain. Version 1.3, 20230626.1</p>
</body> </body>
</html> </html>

View File

@ -16,17 +16,19 @@
<section id="fileSection"> <section id="fileSection">
<h2>Step 1: Select <code>bisrv.asd</code> or a game ROM</h2> <h2>Step 1: Select <code>bisrv.asd</code> or a game ROM</h2>
<p>Select the <code>bisrv.asd</code> (for global device mappings) or game ROM file (for per-game mappings) whose button mappings you want to modify. You can find the <code>bisrv.asd</code> file in the <code>bios</code> folder on your device's microSD card.</p> <p>Select the <code>bisrv.asd</code> (for global device mappings) or game ROM file (for per-game mappings) whose button mappings you want to modify. You can find the <code>bisrv.asd</code> file in the <code>bios</code> folder on your device's microSD card.</p>
<form id="fileForm" action="#"> <div id="fileMessages"></div>
<label>Open file: <input id="fileSelector" type="file" onchange="fileLoad(event.target.files[0])"></label> <div class="controlContainer">
</form> <label class="control">Open file: <input id="fileSelector" type="file" onchange="fileLoad(event.target.files[0])"></label>
<div id="fileOutput"></div> </div>
</section> </section>
</div> </div>
<script src="tools.js"></script>
<script> <script>
// Global variables... // Global variables...
var mappingTableOffset; // Will contain the offset of the button mappings within the data file var mappingTableOffset; // Will contain the offset of the button mappings within the data file
var mappingConsoles; // Will contain a list of the specific game consoles we'll be setting up mappings for var mappingConsoles; // Will contain a list of the specific game consoles we'll be setting up mappings for
var firmwareVersion; // Will contain the firmware version if the user selects a bisrv.asd file
var mappingData; // Used to store the binary data that will eventually be written to the downloadable file var mappingData; // Used to store the binary data that will eventually be written to the downloadable file
var fileName; // Will hold the name of the selected file, used for naming ROM .kmp files var fileName; // Will hold the name of the selected file, used for naming ROM .kmp files
@ -47,108 +49,6 @@
return { 'A': 8, 'B': 0, 'L': 10, 'R': 11, 'X': 9, 'Y': 1 }; return { 'A': 8, 'B': 0, 'L': 10, 'R': 11, 'X': 9, 'Y': 1 };
} }
// Define a function that takes a Uint8Array and an optional offset and returns the index
// of the first match or -1 if not found...
function findSequence(needle, haystack, offset) {
// If offset is not provided, default to 0
offset = offset || 0;
// Loop through the data array starting from the offset
for (var i = offset; i < haystack.length - needle.length + 1; i++) {
// Assume a match until proven otherwise
var match = true;
// Loop through the target sequence and compare each byte
for (var j = 0; j < needle.length; j++) {
if (haystack[i + j] !== needle[j]) {
// Mismatch found, break the inner loop and continue the outer loop
match = false;
break;
}
}
// If match is still true after the inner loop, we have found a match
if (match) {
// Return the index of the first byte of the match
return i;
}
}
// If we reach this point, no match was found
return -1;
}
// Returns an SHA-256 hash of a given firmware (ignoring common user changes), or returns
// false on failure...
function getFirmwareHash(data) {
// Data should be a Uint8Array, which as an object is passed by reference... we're going
// to be manipulating that data before generating our hash, but we don't want to modify
// the original object at all... so we'll create a copy, and work only on the copy...
var dataCopy = data.slice();
// Only really worthwhile doing this for big bisrv.asd files...
if (dataCopy.length > 12640000) {
// First, replace CRC32 bits with 00...
dataCopy[396] = 0x00;
dataCopy[397] = 0x00;
dataCopy[398] = 0x00;
dataCopy[399] = 0x00;
// Next identify the boot logo position, and blank it out too...
var badExceptionOffset = findSequence([0x62, 0x61, 0x64, 0x5F, 0x65, 0x78, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x00, 0x00, 0x00], dataCopy);
if (badExceptionOffset > -1) {
var bootLogoStart = badExceptionOffset + 16;
for (var i = bootLogoStart; i < (bootLogoStart + 204800); i++) {
dataCopy[i] = 0x00;
}
}
else {
return false;
}
// Next identify the emulator button mappings (if they exist), and blank them out too...
var preButtonMapOffset = findSequence([0x00, 0x00, 0x00, 0x71, 0xDB, 0x8E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], dataCopy);
if (preButtonMapOffset > -1) {
var postButtonMapOffset = findSequence([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00], dataCopy, preButtonMapOffset);
if (postButtonMapOffset > -1) {
for (var i = preButtonMapOffset + 16; i < postButtonMapOffset; i++) {
dataCopy[i] = 0x00;
}
}
else {
return false;
}
}
else {
return false;
}
// If we're here, we've zeroed-out all of the bits of the firmware that are
// semi-user modifiable (boot logo, button mappings and the CRC32 bits); now
// we can generate a hash of what's left and compare it against some known
// values...
return crypto.subtle.digest("SHA-256", dataCopy.buffer)
.then(function(digest) {
var array = Array.from(new Uint8Array(digest));
var hash = array.map(byte => ("00" + byte.toString(16)).slice(-2)).join("");
return hash;
})
.catch(function(error) {
return false;
});
}
else {
return false;
}
}
// This function is called whenever a file is selected in Step 1... // This function is called whenever a file is selected in Step 1...
function fileLoad(file) { function fileLoad(file) {
@ -156,21 +56,19 @@
// an array buffer... // an array buffer...
var fr = new FileReader(); var fr = new FileReader();
fr.readAsArrayBuffer(file); fr.readAsArrayBuffer(file);
// Triggered when the FileReader reads the file's contents...
fr.onload = function(event) { fr.onload = function(event) {
// First, reset our global variables...
mappingTableOffset = undefined;
mappingConsoles = undefined;
mappingData = undefined;
fileName = undefined;
// Clear out any HTML that might already exist after Step 1... // Clear out any HTML that might already exist after Step 1...
while(document.getElementById("fileSection").nextSibling) { while(document.getElementById("fileSection").nextSibling) {
document.getElementById("fileSection").nextSibling.remove(); document.getElementById("fileSection").nextSibling.remove();
} }
// Clear any old messages...
document.getElementById("fileMessages").innerHTML = "";
// We'll also reset the firmwareVersion global variable...
firmwareVersion = null;
// Read the provided file's data from the buffer array into an unsigned 8-bit int array... // Read the provided file's data from the buffer array into an unsigned 8-bit int array...
var data = new Uint8Array(event.target.result); var data = new Uint8Array(event.target.result);
@ -180,49 +78,52 @@
// The result could be either a Promise if it had a bisrv.asd-like structure and we got // The result could be either a Promise if it had a bisrv.asd-like structure and we got
// a hash, or false otherwise... let's check! // a hash, or false otherwise... let's check!
if (hashResult instanceof Promise) { if (hashResult instanceof Promise) {
// We got a Promise! Wait for it to finish so we get our bisrv.asd hash... // We got a Promise! Wait for it to finish so we get our bisrv.asd hash...
hashResult.then(function(dataHash) { hashResult.then(function(dataHash) {
// If we have a newer bisrv.asd that stores button mappings in an external KeyMapInfo.kmp, // If we have a newer bisrv.asd that stores button mappings in an external KeyMapInfo.kmp,
// we'll need a Step 1b to load that file as well... // we'll need a Step 1b to load that file as well...
var step1BRequired = false; var step1BRequired = false;
// Check the hash against all the known good ones... // Check the hash against all the known good ones...
switch (dataHash) { firmwareVersion = knownHash(dataHash);
switch (firmwareVersion) {
// Mid-March BIOS... // Mid-March BIOS...
case "4411143d3030adc442e99f7ac4e7772f300c844bbe10d639702fb3ba049a4ee1": case "03.15":
mappingTableOffset = 0x8DBC0C; mappingTableOffset = 0x8DBC0C;
mappingConsoles = ["Arcade", "Game Boy Advance", "SNES", "Genesis/Mega Drive, Master System", "NES, Game Boy, Game Boy Color"]; mappingConsoles = ["Arcade", "Game Boy Advance", "SNES", "Genesis/Mega Drive, Master System", "NES, Game Boy, Game Boy Color"];
document.getElementById("fileOutput").innerHTML = "<p class=\"infoMessage\">INFO: Mid-March <code>bisrv.asd</code> detected</p>"; setMessage("info", "fileMessages", "INFO: Mid-March <code>bisrv.asd</code> detected.");
break; break;
// April 20th BIOS... // April 20th BIOS...
case "b50e50aa4b1b1d41489586e989f09d47c4e2bc27c072cb0112f83e6bc04e2cca": case "04.20":
mappingTableOffset = 0x8DBC9C; mappingTableOffset = 0x8DBC9C;
mappingConsoles = ["Arcade", "Game Boy Advance", "Game Boy, Game Boy Color", "SNES", "Genesis/Mega Drive, Master System", "NES"]; mappingConsoles = ["Arcade", "Game Boy Advance", "Game Boy, Game Boy Color", "SNES", "Genesis/Mega Drive, Master System", "NES"];
document.getElementById("fileOutput").innerHTML = "<p class=\"infoMessage\">INFO: April 20th <code>bisrv.asd</code> detected</p>"; setMessage("info", "fileMessages", "INFO: April 20th <code>bisrv.asd</code> detected.");
break; break;
// May 15th BIOS... // May 15th BIOS...
case "d878a99d26242836178b452814e916bef532d05acfcc24d71baa31b8b6f38ffd": case "05.15":
mappingTableOffset = 0; mappingTableOffset = 0;
mappingConsoles = ["NES", "Genesis/Mega Drive, Master System", "SNES", "Game Boy, Game Boy Color", "Game Boy Advance", "Arcade"]; mappingConsoles = ["NES", "Genesis/Mega Drive, Master System", "SNES", "Game Boy, Game Boy Color", "Game Boy Advance", "Arcade"];
step1BRequired = true; step1BRequired = true;
document.getElementById("fileOutput").innerHTML = "<p class=\"infoMessage\">INFO: May 15th <code>bisrv.asd</code> detected</p>"; setMessage("info", "fileMessages", "INFO: May 15th <code>bisrv.asd</code> detected.");
break; break;
// May 22nd BIOS... // May 22nd BIOS...
case "6aebab0e4da39e0a997df255ad6a1bd12fdd356cdf51a85c614d47109a0d7d07": case "05.22":
mappingTableOffset = 0; mappingTableOffset = 0;
mappingConsoles = ["NES", "SNES", "Genesis/Mega Drive, Master System", "Game Boy, Game Boy Color", "Game Boy Advance", "Arcade"]; mappingConsoles = ["NES", "SNES", "Genesis/Mega Drive, Master System", "Game Boy, Game Boy Color", "Game Boy Advance", "Arcade"];
step1BRequired = true; step1BRequired = true;
document.getElementById("fileOutput").innerHTML = "<p class=\"infoMessage\">INFO: May 22nd <code>bisrv.asd</code> detected</p>"; setMessage("info", "fileMessages", "INFO: May 22nd <code>bisrv.asd</code> detected.");
break; break;
default: default:
// Huh... wasn't false so had bisrv.asd structure, but didn't return // Huh... wasn't false so had bisrv.asd structure, but didn't return
// a known hash... a new BIOS version? Unknown anyway! // a known hash... a new BIOS version? Unknown anyway!
console.log(dataHash); console.log(dataHash);
document.getElementById("fileOutput").innerHTML = "<p class=\"errorMessage\">ERROR: While the file you've selected does appear to be generally structured like the SF2000's <code>bisrv.asd</code> BIOS file, the specifics of your file don't match any known SF2000 BIOS version. As such, this tool cannot modify the selected file.</p>"; setMessage("error", "fileMessages", "ERROR: While the file you've selected does appear to be generally structured like the SF2000's <code>bisrv.asd</code> BIOS file, the specifics of your file don't match any known SF2000 BIOS version. As such, this tool cannot safely modify the selected file.");
return; return;
break; break;
} }
@ -252,7 +153,7 @@
if (data.length == 288) { if (data.length == 288) {
// That's the correct length for a KeyMapInfo.kmp file, however we must know the host // That's the correct length for a KeyMapInfo.kmp file, however we must know the host
// BIOS version before we can correctly process those files. Let the user know... // BIOS version before we can correctly process those files. Let the user know...
document.getElementById("fileOutput").innerHTML = "<p class=\"errorMessage\">ERROR: The file you've provided may be a <code>KeyMapInfo.kmp</code> button map file; however as the internal data structure of these files varies depending on the version of the host BIOS, you must select your device's <code>bisrv.asd</code> file first. You can find this file in the <code>bios</code> folder on your device's microSD card."; setMessage("error", "fileMessages", "ERROR: The file you've provided may be a <code>KeyMapInfo.kmp</code> button map file; however as the internal data structure of these files varies depending on the version of the host BIOS, you must select your device's <code>bisrv.asd</code> file first. You can find this file in the <code>bios</code> folder on your device's microSD card.");
return; return;
} }
// If we're still checking, next test the file extensions for the individual console's ROMs... // If we're still checking, next test the file extensions for the individual console's ROMs...
@ -283,7 +184,7 @@
else { else {
// Oh dear, the provided file didn't match any of the above rules! Display an error // Oh dear, the provided file didn't match any of the above rules! Display an error
// to the user... // to the user...
document.getElementById("fileOutput").innerHTML = "<p class=\"errorMessage\">ERROR: The selected file does not appear to be a known <code>bisrv.asd</code> file, a <code>KeyMapInfo.kmp</code> file, or a game ROM with a known extension!</p>"; setMessage("error", "fileMessages", "ERROR: The selected file does not appear to be a known <code>bisrv.asd</code> file, a <code>KeyMapInfo.kmp</code> file, or a game ROM with a known extension!");
return; return;
} }
@ -293,12 +194,14 @@
// and we'll set mappingData to it's full contents instead... // and we'll set mappingData to it's full contents instead...
if (mappingConsoles.length == 1) { if (mappingConsoles.length == 1) {
mappingData = new Uint8Array(48); mappingData = new Uint8Array(48);
mappingTableOffset = 0;
} }
else { else {
mappingData = data; mappingData = data;
} }
// In both cases, the mapping data begins at the very start of our data stream...
mappingTableOffset = 0;
// Keep a record of the input file's name as well... // Keep a record of the input file's name as well...
fileName = file.name; fileName = file.name;
@ -308,91 +211,123 @@
} }
} }
// This function is called if the file selected in Step 1 is a bisrv.asd
// version that relies on an external KeyMapInfo.kmp file - so we also
// need the user to supply that...
function stepOneB() { function stepOneB() {
var html = "<section id=\"stepOneB\"><h2>Step 1b: Select <code>KeyMapInfo.kmp</code></h2><p>This version of the SF2000 BIOS reads its button mappings from an external file called <code>KeyMapInfo.kmp</code>, stored in the <code>Resources</code> folder on the microSD card. Please select your device's <code>KeyMapInfo.kmp</code> file now.</p><label>Open <code>KeyMapInfo.kmp</code>: <input id=\"keyMapInfoSelector\" type=\"file\"></label><div id=\"stepOneBOutput\"></div></section>";
// Finally, add a <hr> separator after the last step, and append the new step... // Build our HTML...
var html = "<section id=\"stepOneB\"><h2>Step 1b: Select <code>KeyMapInfo.kmp</code></h2><p>This version of the SF2000 BIOS reads its button mappings from an external file called <code>KeyMapInfo.kmp</code>, stored in the <code>Resources</code> folder on the microSD card. Please select your device's <code>KeyMapInfo.kmp</code> file now.</p><div id=\"stepOneBMessages\"></div><div class=\"controlContainer\"><label class=\"control\">Open <code>KeyMapInfo.kmp</code>: <input id=\"keyMapInfoSelector\" type=\"file\" accept=\".kmp\"></label></div></section>";
// Add a <hr> separator after the last step, and append the new step...
document.getElementById("steps").insertAdjacentHTML("beforeend", "<hr>"); document.getElementById("steps").insertAdjacentHTML("beforeend", "<hr>");
document.getElementById("steps").insertAdjacentHTML("beforeend", html); document.getElementById("steps").insertAdjacentHTML("beforeend", html);
// Attach our event handler to our new file input control... // Attach our event handler to our new file input control...
var keyMapInfoInput = document.getElementById("keyMapInfoSelector"); var keyMapInfoInput = document.getElementById("keyMapInfoSelector");
keyMapInfoInput.addEventListener("change", function() { keyMapInfoInput.addEventListener("change", function() {
// The user has chosen a new file; it should be a KeyMapInfo.kmp file, // The user has chosen a new file; it should be a KeyMapInfo.kmp file,
// so let's do our best to check! // so let's do our best to check!
// First, clear out any HTML that might already exist after Step 1b...
while(document.getElementById("stepOneB").nextSibling) {
document.getElementById("stepOneB").nextSibling.remove();
}
// Clear any old messages...
document.getElementById("stepOneBMessages").innerHTML = "";
// Next, read in the contents of the user-provided file...
var frKMI = new FileReader(); var frKMI = new FileReader();
fileName = event.target.files[0].name; fileName = event.target.files[0].name;
frKMI.readAsDataURL(event.target.files[0]); frKMI.readAsDataURL(event.target.files[0]);
frKMI.onload = function(event) { frKMI.onload = function(event) {
// Get the file's data and data type...
var fileData = event.target.result; var fileData = event.target.result;
var dataType = fileData.substring(5, fileData.indexOf(";")); var dataType = fileData.substring(5, fileData.indexOf(";"));
// Check to make sure the data type is binary...
if (dataType === "application/octet-stream") { if (dataType === "application/octet-stream") {
// The user selected a file that appears to contain binary data; it's // The user selected a file that appears to contain binary data; it's
// a good candidate! Let's check its length next... // a good candidate! Let's check its length next...
var base64Data = fileData.substring(fileData.indexOf(",") + 1); var base64Data = fileData.substring(fileData.indexOf(",") + 1);
var binaryData = atob(base64Data); var binaryData = atob(base64Data);
if (binaryData.length == 288) { if (binaryData.length == 288) {
// It's the right length - let's assume it's a KeyMapInfo.kmp! // It's the right length - let's assume it's a KeyMapInfo.kmp!
// We'll assign it to our global mappingData variable, and
// we'll proceed to Step 2...
mappingData = new Uint8Array(binaryData.length); mappingData = new Uint8Array(binaryData.length);
for (var i = 0; i < binaryData.length; i++) { for (var i = 0; i < binaryData.length; i++) {
mappingData[i] = binaryData.charCodeAt(i); mappingData[i] = binaryData.charCodeAt(i);
} }
while(document.getElementById("stepOneB").nextSibling) {
document.getElementById("stepOneB").nextSibling.remove();
}
stepTwo(); stepTwo();
} }
else { else {
// Wrong length for a KeyMapInfo.kmp file! // Wrong length for a KeyMapInfo.kmp file!
document.getElementById("stepOneBOutput").innerHTML = "<p class=\"errorMessage\">ERROR: The selected file does not appear to be a valid <code>KeyMapInfo.kmp</code> file.</p>"; setMessage("error", "stepOneBMessages", "ERROR: The selected file does not appear to be a valid <code>KeyMapInfo.kmp</code> file.");
return; return;
} }
} }
else { else {
// The file the user selected doesn't appear to be binary data, so // The file the user selected doesn't appear to be binary data, so
// highly unlikely to be a KeyMapInfo.kmp file... // highly unlikely to be a KeyMapInfo.kmp file...
document.getElementById("stepOneBOutput").innerHTML = "<p class=\"errorMessage\">ERROR: The selected file does not appear to be a valid <code>KeyMapInfo.kmp</code> file.</p>"; setMessage("error", "stepOneBMessages", "ERROR: The selected file does not appear to be a valid <code>KeyMapInfo.kmp</code> file.");
return; return;
} }
} }
}); });
} }
// This function triggers automatically when the user selects a valid
// file (or files) in Step 1...
function stepTwo() { function stepTwo() {
// We're going to be creating a bunch of HTML here; we want to display banks of mapping
// controls to the user, one bank per console. Each bank will have a heading specifying // We're going to be creating a bunch of HTML here; we want to display
// which console it's for, and then a section each for Player 1 and Player 2. Each // banks of mapping controls to the user, one bank per console. Each
// player section will have a list of the six SF2000 buttons that are available to be // bank will have a heading specifying which console it's for, and then
// mapped, and for each a selection box of the target console's buttons for the mapping. // a section each for Player 1 and Player 2. Each player section will
// There'll also be a checkbox per button, which can be checked to enable "autofire" on // have a list of the six SF2000 buttons that are available to be
// that button. // mapped, and for each a selection box of the target console's buttons
// for the mapping. There'll also be a checkbox per button, which can
// be checked to enable "autofire" on that button.
var html = "<section id=\"mappingSection\"><h2>Step 2: Choose your button mappings</h2>"; var html = "<section id=\"mappingSection\"><h2>Step 2: Choose your button mappings</h2>";
// First, we need to update Step 2's instructions, depending on whether or not the user // First, we need to update Step 2's instructions, depending on whether
// supplied a bisrv.asd file or KeyMapInfo.kmp (multiple consoles) or a ROM (one console)... // or not the user supplied a bisrv.asd file or KeyMapInfo.kmp
// (multiple consoles) or a ROM (one console)...
if (mappingConsoles.length > 1) { if (mappingConsoles.length > 1) {
// They provided a bisrv.asd or a KeyMapInfo.kmp file! // They provided a bisrv.asd or a KeyMapInfo.kmp file!
html += "<p>Below you will see the current global button mappings for the file you provided. Each tile covers the button mappings for a different game console - the physical SF2000 buttons are on the left, and the virtual console buttons are in the middle. On the right are some \"autofire\" checkboxes - if the box for a button is checked, it means holding that button down will trigger multiple repeated button presses in the virtual console automatically. As the SF2000 supports local multiplayer via the use of a second wireless controller, there are <i>two</i> sets of button mappings per console - one for Player 1 and one for Player 2. When you have finished tweaking your button mappings, proceed to Step 3.</p>"; html += "<p>Below you will see the current global button mappings for the file you provided. Each tile covers the button mappings for a different game console - the physical SF2000 buttons are on the left, and the virtual console buttons are in the middle. On the right are some \"autofire\" checkboxes - if the box for a button is checked, it means holding that button down will trigger multiple repeated button presses in the virtual console automatically. As the SF2000 supports local multiplayer via the use of a second wireless controller, there are <i>two</i> sets of button mappings per console - one for Player 1 and one for Player 2. When you have finished tweaking your button mappings, proceed to Step 3.</p>";
} }
else { else {
// They provided a ROM file! // They provided a ROM file!
html += "<p>Below you will see an empty \"" + mappingConsoles[0] + "\" button mapping table, which will be used to create a unique button mapping profile for \"" + fileName + "\". In the table, the physical SF2000 buttons are on the left, and the virtual console buttons are in the middle. On the right are some \"autofire\" checkboxes - if the box for a button is checked, it means holding that button down will trigger multiple repeated button presses in the virtual console automatically. As the SF2000 supports local multiplayer via the use of a second wireless controller, there are <i>two</i> sets of button mappings per console - one for Player 1 and one for Player 2. When you have finished tweaking your button mappings, proceed to Step 3.</p>"; html += "<p>Below you will see an empty \"" + mappingConsoles[0] + "\" button mapping table, which will be used to create a unique button mapping profile for \"" + fileName + "\". In the table, the physical SF2000 buttons are on the left, and the virtual console buttons are in the middle. On the right are some \"autofire\" checkboxes - if the box for a button is checked, it means holding that button down will trigger multiple repeated button presses in the virtual console automatically. As the SF2000 supports local multiplayer via the use of a second wireless controller, there are <i>two</i> sets of button mappings per console - one for Player 1 and one for Player 2. When you have finished tweaking your button mappings, proceed to Step 3.</p>";
} }
html += "<div id=\"mappingControls\">"; html += "<div class=\"controlContainer\">";
// Next we'll be looping through all of the consoles we'll be setting up mappings for... // Next we'll be looping through all of the consoles we'll be setting
// up mappings for...
var presentationButtonOrder = ['A', 'B' ,'X', 'Y', 'L', 'R']; var presentationButtonOrder = ['A', 'B' ,'X', 'Y', 'L', 'R'];
for (var currentConsole = 0; currentConsole < mappingConsoles.length; currentConsole++) { for (var currentConsole = 0; currentConsole < mappingConsoles.length; currentConsole++) {
// This console's bank of mapping controls will be stored in a <div>, and we'll add // This console's bank of mapping controls will be stored in a <div>,
// a <h3> header for good measure as well... // and we'll add a <h3> header for good measure as well...
html += "<div class=\"mappingConsole\"><h3>" + mappingConsoles[currentConsole] + "</h3>"; html += "<div class=\"control\"><h3>" + mappingConsoles[currentConsole] + "</h3>";
// Get the button mapping for this console... // Get the button mapping for this console...
var buttonMap = getButtonMap(currentConsole); var buttonMap = getButtonMap(currentConsole);
// We'll add two tables of control mappings to the <div>, one each for Player 1 and // We'll add two tables of control mappings to the <div>, one each
// Player 2... // for Player 1 and Player 2...
for (var player = 0; player < 2; player++) { for (var player = 0; player < 2; player++) {
// Start creating our table HTML... // Start creating our table HTML...
@ -400,19 +335,22 @@
html += "<thead><tr><th class=\"alignL\">SF2000</th><th>Console</th><th>Autofire</th></tr></thead>"; html += "<thead><tr><th class=\"alignL\">SF2000</th><th>Console</th><th>Autofire</th></tr></thead>";
html += "<tbody>"; html += "<tbody>";
// Loop through all the SF2000's buttons (well, the ones that can be mapped, anyway)... // Loop through all the SF2000's buttons (well, the ones that can
// be mapped, anyway)...
for (var button = 0; button < 6; button++) { for (var button = 0; button < 6; button++) {
// By default, the SF2000 stores its button maps in XYLABR order... except for // By default, the SF2000 stores its button maps in XYLABR
// GBA under the May 15th's firmware where the order is LRXABY for some reason. // order... except for GBA under newer firmware versions where
// We specify the order the bytes are specified in here. If they do other weird // the order is LRXABY for some reason. We specify the order the
// stuff in the future, it'll probably be here that needs to change! // bytes are in here. If they do other weird stuff in the future,
// it'll probably be here that needs to change!
var buttonByteOrder = ['X', 'Y' ,'L', 'A', 'B', 'R']; var buttonByteOrder = ['X', 'Y' ,'L', 'A', 'B', 'R'];
if (mappingConsoles[currentConsole] == "Game Boy Advance" && fileName == "KeyMapInfo.kmp") { if (mappingConsoles[currentConsole] == "Game Boy Advance" && ["05.15", "05.22"].includes(firmwareVersion)) {
buttonByteOrder = ['L', 'R', 'X', 'A', 'B', 'Y']; buttonByteOrder = ['L', 'R', 'X', 'A', 'B', 'Y'];
} }
// Calculate our offset within our mapping data for the current button... // Calculate our offset within our mapping data for the current
// button...
var offset = mappingTableOffset + (currentConsole * 48) + (player * 24) + (buttonByteOrder.indexOf(presentationButtonOrder[button]) * 4); var offset = mappingTableOffset + (currentConsole * 48) + (player * 24) + (buttonByteOrder.indexOf(presentationButtonOrder[button]) * 4);
// Start creating the HTML data for this row in the table... // Start creating the HTML data for this row in the table...
@ -458,63 +396,77 @@
// OK, we're all done displaying our mapping table HTML; trigger Step 3's setup... // OK, we're all done displaying our mapping table HTML; trigger Step 3's setup...
document.getElementById("steps").insertAdjacentHTML("beforeend", "<hr>"); document.getElementById("steps").insertAdjacentHTML("beforeend", "<hr>");
document.getElementById("steps").insertAdjacentHTML("beforeend", html); document.getElementById("steps").insertAdjacentHTML("beforeend", html);
while(document.getElementById("mappingSection").nextSibling) {
document.getElementById("mappingSection").nextSibling.remove();
}
stepThreeSetup(); stepThreeSetup();
} }
function stepThreeSetup() { function stepThreeSetup() {
// More HTML in this function! We'll display the appropriate instructions to the
// user (either how to replace the bisrv.asd/KeyMapInfo.kmp file, or where to put // More HTML in this function! We'll display the appropriate
// the per-rom .kmp file), as well as generate a button that (when clicked) will // instructions to the user (either how to replace the
// bisrv.asd/KeyMapInfo.kmp file, or where to put the per-rom .kmp
// file), as well as generate a button that (when clicked) will
// download the appropriate file to their device... // download the appropriate file to their device...
// First, let's clear any HTML that might already exist after Step 2...
while(document.getElementById("mappingSection").nextSibling) {
document.getElementById("mappingSection").nextSibling.remove();
}
// Now let's start building our HTML...
var html = "<section id=\"saveSection\"><h2>Step 3: Save your mapping changes</h2>"; var html = "<section id=\"saveSection\"><h2>Step 3: Save your mapping changes</h2>";
// First up, instructions! These will depend on whether they provided a bisrv.asd, // First up, instructions! These will depend on whether they provided a
// a KeyMapInfo.kmp, or a game ROM... // bisrv.asd, a KeyMapInfo.kmp, or a game ROM...
if (mappingConsoles.length > 1) { if (mappingConsoles.length > 1) {
if (fileName == "bisrv.asd") { if (fileName == "bisrv.asd") {
// They provided a bisrv.asd file! // They provided a bisrv.asd file!
html += "<p>Click the Download button below to download a new <code>bisrv.asd</code> BIOS file for the SF2000, with your updated global button mappings baked into it. Use it to replace the existing <code>bisrv.asd</code> file in the <code>bios</code> folder on your device's microSD card.</p>"; html += "<p>Click the Download button below to download a new <code>bisrv.asd</code> BIOS file for the SF2000, with your updated global button mappings baked into it. Use it to replace the existing <code>bisrv.asd</code> file in the <code>bios</code> folder on your device's microSD card.</p>";
} }
else if (fileName == "KeyMapInfo.kmp") { else if (fileName == "KeyMapInfo.kmp") {
// They provided a KeyMapInfo.kmp file! // They provided a KeyMapInfo.kmp file!
html += "<p>Click the Download button below to download a new <code>KeyMapInfo.kmp</code> keymap file for the SF2000, with your updated global button mappings baked into it. Use it to replace the existing <code>KeyMapInfo.kmp</code> file in the <code>Resources</code> folder on your device's microSD card.</p>"; html += "<p>Click the Download button below to download a new <code>KeyMapInfo.kmp</code> keymap file for the SF2000, with your updated global button mappings baked into it. Use it to replace the existing <code>KeyMapInfo.kmp</code> file in the <code>Resources</code> folder on your device's microSD card.</p>";
} }
else { else {
// They provided a... something! // They provided a... something!
html += "<p>Click the Download button below to download an updated version of your file, with your updated global button mappings baked into it. Use it to replace the existing file on your device's microSD card.</p>"; html += "<p>Click the Download button below to download an updated version of your file, with your updated global button mappings baked into it. Use it to replace the existing file on your device's microSD card.</p>";
} }
} }
else { else {
// They provided a ROM file! To make the instructions clearer, let's calculate
// the name of the keymap file we're generating... // They provided a ROM file! To make the instructions clearer, let's
// calculate the name of the keymap file we're generating...
var kmpFileName = fileName.replace(/($|\.[^.]*$)/, function(m, p1) {return p1.toUpperCase() + '.kmp';}); var kmpFileName = fileName.replace(/($|\.[^.]*$)/, function(m, p1) {return p1.toUpperCase() + '.kmp';});
// Now the instructions themselves... // Now the instructions themselves...
html += "<p>Click the Download button below to download \"" + kmpFileName + "\", a game-specific keymap file for \"" + fileName + "\". Once downloaded, place it in the <code>save</code> subfolder of the folder where the ROM itself is stored. So for example, if \"" + fileName + "\" is in the <code>ROMS</code> folder on your SF2000's microSD card, place the \"" + kmpFileName + "\" file in <code>ROMS/save/</code>. If the <code>save</code> subfolder does not already exist, create it yourself first.</p>"; html += "<p>Click the Download button below to download \"" + kmpFileName + "\", a game-specific keymap file for \"" + fileName + "\". Once downloaded, place it in the <code>save</code> subfolder of the folder where the ROM itself is stored. So for example, if \"" + fileName + "\" is in the <code>ROMS</code> folder on your SF2000's microSD card, place the \"" + kmpFileName + "\" file in <code>ROMS/save/</code>. If the <code>save</code> subfolder does not already exist, create it yourself first.</p>";
} }
// Now let's add the Download button with it's event... // Add our download button...
html += "<form id=\"downloadForm\" action=\"#\"><input id=\"downloadButton\" type=\"button\" value=\"Download\" onclick=\"download()\"></form>"; html += "<div class=\"controlContainer\"><div class=\"control\"><input id=\"downloadButton\" type=\"button\" value=\"Download\"></div></div>";
// OK, we're all done; add our HTML to the page... // Close our section...
html += "</section>";
// Finally, add a <hr> separator after the last step, and append the
// new step...
document.getElementById("steps").insertAdjacentHTML("beforeend", "<hr>"); document.getElementById("steps").insertAdjacentHTML("beforeend", "<hr>");
document.getElementById("steps").insertAdjacentHTML("beforeend", html); document.getElementById("steps").insertAdjacentHTML("beforeend", html);
while(document.getElementById("saveSection").nextSibling) {
document.getElementById("saveSection").nextSibling.remove();
}
}
function download() { // Let's add the event handler for our Download button...
// Here, we'll construct the file for the user to download (a modified var dButton = document.getElementById("downloadButton");
// bisrv.asd, a modified KeyMapInfo.kmp, or a .kmp keymap file), and send dButton.addEventListener("click", function() {
// it to the user's browser...
// We need to loop through all of the mapping form data, read its settings, and // Here, we'll construct the file for the user to download (a
// use those settings to build the binary data of our button mapping. Loop // modified bisrv.asd, a modified KeyMapInfo.kmp, or a .kmp keymap
// through all of the consoles we're mapping for... // file), and send it to the user's browser...
// We need to loop through all of the mapping form data, read its
// settings, and use those settings to build the binary data of our
// button mapping. Loop through all of the consoles we're mapping
// for...
var presentationButtonOrder = ['A', 'B' ,'X', 'Y', 'L', 'R']; var presentationButtonOrder = ['A', 'B' ,'X', 'Y', 'L', 'R'];
for (var currentConsole = 0; currentConsole < mappingConsoles.length; currentConsole ++) { for (var currentConsole = 0; currentConsole < mappingConsoles.length; currentConsole ++) {
@ -523,20 +475,23 @@
// For each player... // For each player...
for (var player = 0; player < 2; player++) { for (var player = 0; player < 2; player++) {
// ... and for each button... // ... and for each button...
for (var button = 0; button < 6; button++) { for (var button = 0; button < 6; button++) {
// By default, the SF2000 stores its button maps in XYLABR order... except for
// GBA under the May 15th's firmware where the order is LRXABY for some reason. // By default, the SF2000 stores its button maps in XYLABR
// We specify the order the bytes are specified in here. If they do other weird // order... except for GBA under newer firmware versions where
// stuff in the future, it'll probably be here that needs to change! // the order is LRXABY for some reason. We specify the order
// the bytes are in here. If they do other weird stuff in the
// future, it'll probably be here that needs to change!
var buttonByteOrder = ['X', 'Y' ,'L', 'A', 'B', 'R']; var buttonByteOrder = ['X', 'Y' ,'L', 'A', 'B', 'R'];
if (mappingConsoles[currentConsole] == "Game Boy Advance" && fileName == "KeyMapInfo.kmp") { if (mappingConsoles[currentConsole] == "Game Boy Advance" && ["05.15", "05.22"].includes(firmwareVersion)) {
buttonByteOrder = ['L', 'R', 'X', 'A', 'B', 'Y']; buttonByteOrder = ['L', 'R', 'X', 'A', 'B', 'Y'];
} }
// Calculate the offset in our mapping data for the current button, read // Calculate the offset in our mapping data for the current
// the button settings from the HTML controls, and assign the appropriate // button, read the button settings from the HTML controls, and
// values to our binary mappingData... // assign the appropriate values to our binary mappingData...
var offset = mappingTableOffset + (currentConsole * 48) + (player * 24) + (buttonByteOrder.indexOf(presentationButtonOrder[button]) * 4); var offset = mappingTableOffset + (currentConsole * 48) + (player * 24) + (buttonByteOrder.indexOf(presentationButtonOrder[button]) * 4);
mappingData[offset] = buttonMap[document.getElementById("sel" + offset.toString(16)).value]; mappingData[offset] = buttonMap[document.getElementById("sel" + offset.toString(16)).value];
mappingData[offset + 2] = document.getElementById("cb" + offset.toString(16)).checked ? 1 : 0; mappingData[offset + 2] = document.getElementById("cb" + offset.toString(16)).checked ? 1 : 0;
@ -544,49 +499,29 @@
} }
} }
// Now that we've got our updated data, we'll need to check if it's an updated // Now that we've got our updated data, we'll need to check if it's
// bisrv.asd or not - if it is, we'll need to update some CRC32 check-bits // an updated bisrv.asd or not - if it is, we'll need to update some
// in the bisrv.asd data as well... // CRC32 check-bits in the bisrv.asd data as well...
if (mappingConsoles.length > 1 && fileName == "bisrv.asd") { if (mappingConsoles.length > 1 && fileName == "bisrv.asd") {
// It's a bisrv.asd alright! Let's do the CRC32 update dance... // It's a bisrv.asd alright! Let's do the CRC32 update dance...
var c; patchCRC32(mappingData);
var tabCRC32 = new Int32Array(256);
for (var i = 0; i < 256; i++) {
c = i << 24;
for (var j = 0; j < 8; j++) {
c = c & (1 << 31) ? c << 1 ^ 0x4c11db7 : c << 1;
}
tabCRC32[i] = c;
}
c = ~0;
for (var i = 512; i < mappingData.length; i++) {
c = c << 8 ^ tabCRC32[c >>> 24 ^ mappingData[i]];
}
mappingData[0x18c] = c & 255;
mappingData[0x18d] = c >>> 8 & 255;
mappingData[0x18e] = c >>> 16 & 255;
mappingData[0x18f] = c >>> 24;
} }
// Download time! First, let's determine the name of the file we're sending // Next, let's determine the name of the file we're going to send to
// to the user's browser... // the user's browser...
var downloadFileName = fileName; var downloadFileName = fileName;
if (mappingConsoles.length == 1) { if (mappingConsoles.length == 1) {
downloadFileName = downloadFileName.replace(/($|\.[^.]*$)/, function(m, p1) {return p1.toUpperCase() + '.kmp';}); downloadFileName = downloadFileName.replace(/($|\.[^.]*$)/, function(m, p1) {return p1.toUpperCase() + '.kmp';});
} }
// Finally, send the file! // And finally, send the data to the user's browser as a download...
var link = document.createElement("a"); downloadToBrowser(mappingData, "application/octet-stream", downloadFileName);
link.href = window.URL.createObjectURL(new Blob([mappingData], {type: "application/octet-stream"}));
link.download = downloadFileName; });
link.style.display = "none";
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(link.href);
document.body.removeChild(link);
} }
</script> </script>
<hr> <hr>
<p><a rel="license" href="https://creativecommons.org/publicdomain/zero/1.0/">CC0</a>: public domain. Version 1.2, 20230522.1</p> <p><a rel="license" href="https://creativecommons.org/publicdomain/zero/1.0/">CC0</a>: public domain. Version 1.3, 20230626.1</p>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@
<h1>Data Frog SF2000 Save State Tool</h1> <h1>Data Frog SF2000 Save State Tool</h1>
<p>The SF2000 supports save states for games, but it bundles the save state data into a custom format including a thumbnail and metadata; this bundle can't be directly used as save state data for other emulator platforms (e.g., Retroarch). This tool allows you to extract raw save-state data and thumbnails from SF2000 save-state bundles, in case you want to use them with the same emulator on another device; it also allows you to take save states from another device, and convert them to a format the SF2000 can parse. Please note this tool is provided as-is; make sure you have backups of anything you care about before potentially replacing any files on your SF2000's microSD card! 🙂</p> <p>The SF2000 supports save states for games, but it bundles the save state data into a custom format including a thumbnail and metadata; this bundle can't be directly used as save state data for other emulator platforms (e.g., Retroarch). This tool allows you to extract raw save-state data and thumbnails from SF2000 save-state bundles, in case you want to use them with the same emulator on another device; it also allows you to take save states from another device, and convert them to a format the SF2000 can parse. Please note this tool is provided as-is; make sure you have backups of anything you care about before potentially replacing any files on your SF2000's microSD card! 🙂</p>
<p>This tool makes use of the <a href="https://github.com/nodeca/pako">pako</a> JavaScript library.</p> <p>This tool makes use of the <a href="https://github.com/nodeca/pako">pako</a> JavaScript library.</p>
<p class="warning"><img class="icon" alt="An icon of a yellow triangle with a dark orange exclamation point, indicating a warning or notice" aria-hidden="true" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJyB2aWV3Qm94PScwIDAgNjQgNjQnPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0nYSc+PHN0b3Agb2Zmc2V0PScwJyBzdG9wLWNvbG9yPScjZTliYjNhJy8+PHN0b3Agb2Zmc2V0PScxJyBzdG9wLWNvbG9yPScjYzE2ODAzJy8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgeGxpbms6aHJlZj0nI2EnIGlkPSdiJyB4MT0nMjAnIHgyPSc1NicgeTE9JzEyJyB5Mj0nNjAnIGdyYWRpZW50VW5pdHM9J3VzZXJTcGFjZU9uVXNlJy8+PC9kZWZzPjxwYXRoIGZpbGw9JyNmZmYnIHN0cm9rZT0ndXJsKCNiKScgc3Ryb2tlLWxpbmVqb2luPSdyb3VuZCcgc3Ryb2tlLXdpZHRoPSc4JyBkPSdNNCA1Nmg1NkwzMiA4WicgcGFpbnQtb3JkZXI9J3N0cm9rZSBtYXJrZXJzIGZpbGwnLz48cGF0aCBmaWxsPScjZjVkODUyJyBkPSdNMTIgNTJoNDBMMzIgMThaJyBwYWludC1vcmRlcj0nbWFya2VycyBmaWxsIHN0cm9rZScvPjxwYXRoIGZpbGw9JyNjNTg3MTEnIGQ9J00yOSA0M3Y2aDZ2LTZ6TTI5IDQwaDZWMjZoLTZ6JyBwYWludC1vcmRlcj0nc3Ryb2tlIG1hcmtlcnMgZmlsbCcvPjwvc3ZnPg=="><strong><em>NOTE:</em></strong> This tool will <em>not</em> convert save states from one emulator's format to another; it <em>only</em> gives you either the raw save state data the SF2000 generated (when converting <em>from</em> the SF2000), or allows you to take any random file and shove it into the container format the SF2000 uses for save states (when converting <em>to</em> the SF2000)… whether or not the SF2000 is able to understand the file you give it is up to the SF2000, not to me! If you're converting save states to the SF2000, and the SF2000 doesn't work with whatever kind of save state you provided, there's nothing I can do about that.<br><br>You can <a href="https://vonmillhausen.github.io/sf2000/#emulators" target="_blank" rel="noreferrer noopener">find a reference to all the specific emulators and versions used by the SF2000 here</a>; you'll probably only have success if you're using at least the same emulator, if not also the same version. Also, to have any chance of success, you must be using the exact same ROM on both devices!</p> <div id="initialWarning"></div>
<hr> <hr>
<div id="steps"> <div id="steps">
<section id="modeSection"> <section id="modeSection">
@ -23,6 +23,7 @@
</section> </section>
</div> </div>
<script src="pako.js"></script> <script src="pako.js"></script>
<script src="tools.js"></script>
<script> <script>
// Global Variables // Global Variables
@ -36,10 +37,8 @@
var thumbHeight; // Integer height of a save state thumbnail var thumbHeight; // Integer height of a save state thumbnail
var romFileGood; // Boolean indicating if a chosen save state file appears to be binary or not var romFileGood; // Boolean indicating if a chosen save state file appears to be binary or not
// The following variable contains a definition for an SVG image to be used // Display our initial warning note...
// in any error messages shown to the user; defining it once so it can be setMessage("warning", "initialWarning", "<strong><em>NOTE:</em></strong> This tool will <em>not</em> convert save states from one emulator's format to another; it <em>only</em> gives you either the raw save state data the SF2000 generated (when converting <em>from</em> the SF2000), or allows you to take any random file and shove it into the container format the SF2000 uses for save states (when converting <em>to</em> the SF2000)… whether or not the SF2000 is able to understand the file you give it is up to the SF2000, not to me! If you're converting save states to the SF2000, and the SF2000 doesn't work with whatever kind of save state you provided, there's nothing I can do about that.<br><br>You can <a href=\"https://vonmillhausen.github.io/sf2000/#emulators\" target=\"_blank\" rel=\"noreferrer noopener\">find a reference to all the specific emulators and versions used by the SF2000 here</a>; you'll probably only have success if you're using at least the same emulator, if not also the same version. Also, to have any chance of success, you must be using the exact same ROM on both devices, and make sure save state compression is turned off if your save state is coming from Retroarch!");
// reused more easily...
var errorIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJyB2aWV3Qm94PScwIDAgNjQgNjQnPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0nYic+PHN0b3Agb2Zmc2V0PScwJyBzdG9wLWNvbG9yPScjZjU4MTYyJy8+PHN0b3Agb2Zmc2V0PScxJyBzdG9wLWNvbG9yPScjZTI1ZjUzJy8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9J2EnPjxzdG9wIG9mZnNldD0nMCcgc3RvcC1jb2xvcj0nI2VjODE2YycvPjxzdG9wIG9mZnNldD0nMScgc3RvcC1jb2xvcj0nI2JmNDMyOScvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IHhsaW5rOmhyZWY9JyNhJyBpZD0nYycgeDE9JzEyJyB4Mj0nNTInIHkxPScxMicgeTI9JzUyJyBncmFkaWVudFVuaXRzPSd1c2VyU3BhY2VPblVzZScvPjxsaW5lYXJHcmFkaWVudCB4bGluazpocmVmPScjYicgaWQ9J2QnIHgxPScyMCcgeDI9JzQ4JyB5MT0nMTYnIHkyPSc0NCcgZ3JhZGllbnRVbml0cz0ndXNlclNwYWNlT25Vc2UnLz48L2RlZnM+PGNpcmNsZSBjeD0nMzInIGN5PSczMicgcj0nMjgnIGZpbGw9J3VybCgjYyknIHBhaW50LW9yZGVyPSdtYXJrZXJzIHN0cm9rZSBmaWxsJy8+PGNpcmNsZSBjeD0nMzInIGN5PSczMicgcj0nMjQnIGZpbGw9JyNmOWQzY2MnIHBhaW50LW9yZGVyPSdtYXJrZXJzIHN0cm9rZSBmaWxsJy8+PGNpcmNsZSBjeD0nMzInIGN5PSczMicgcj0nMjAnIGZpbGw9J3VybCgjZCknIHBhaW50LW9yZGVyPSdtYXJrZXJzIHN0cm9rZSBmaWxsJy8+PHBhdGggZmlsbD0nI2ZmZicgZD0nTTI5IDE2djIwaDZWMTZaTTI5IDQwdjZoNnYtNnonIHBhaW50LW9yZGVyPSdmaWxsIG1hcmtlcnMgc3Ryb2tlJy8+PC9zdmc+";
// When the tool loads, add some event watchers for when the Step 1 radio // When the tool loads, add some event watchers for when the Step 1 radio
// buttons change; and depending on which mode the user selects, begin // buttons change; and depending on which mode the user selects, begin
@ -108,14 +107,16 @@
// save and thumbnail data... otherwise, it returns false and we let // save and thumbnail data... otherwise, it returns false and we let
// the user know the file didn't appear to be valid... // the user know the file didn't appear to be valid...
if (validateSave(new Uint8Array(event.target.result))) { if (validateSave(new Uint8Array(event.target.result))) {
// Great, the user selected a valid SF2000 format save state file! // Great, the user selected a valid SF2000 format save state file!
// On to Step 3! // On to Step 3!
setupStepThree_From(); setupStepThree_From();
} }
else { else {
// Whoops, the file doesn't looke like it was an SF2000 save state file. // Whoops, the file doesn't looke like it was an SF2000 save state file.
// Let the user know and return... // Let the user know and return...
document.getElementById("step2Messages").innerHTML = "<p class=\"error\"><img class=\"icon\" alt=\"An icon of a red circle with a white exclamation point, indicating an error has occurred\" aria-hidden=\"true\" src=\"" + errorIcon + "\"> ERROR: The selected file does not appear to be a valid SF2000 save state file.</p>"; setMessage("error", "step2Messages", "ERROR: The selected file does not appear to be a valid SF2000 save state file.");
return; return;
} }
} }
@ -160,9 +161,10 @@
// The file is loaded; let's check to make sure we got a binary file (can't // The file is loaded; let's check to make sure we got a binary file (can't
// really do much more validation than that, alas)... // really do much more validation than that, alas)...
if (!event.target.result.includes("application/octet-stream")) { if (!event.target.result.includes("application/octet-stream")) {
// Whoops! Doesn't look like the user provided a binary file, so can't be // Whoops! Doesn't look like the user provided a binary file, so can't be
// a save state! // a save state!
document.getElementById("step2Messages").innerHTML = "<p class=\"error\"><img class=\"icon\" alt=\"An icon of a red circle with a white exclamation point, indicating an error has occurred\" aria-hidden=\"true\" src=\"" + errorIcon + "\"> ERROR: The selected file does not appear to be a binary file, and therefore can't be a save state file; please make sure you're selecting a valid emulator save state file.</p>"; setMessage("error", "step2Messages", "ERROR: The selected file does not appear to be a binary file, and therefore can't be a save state file; please make sure you're selecting a valid emulator save state file.");
return; return;
} }
@ -182,7 +184,7 @@
// On to Step 3! // On to Step 3!
setupStepThree_To(); setupStepThree_To();
} }
}) });
} }
// This function sets up the HTML for "Convert from SF2000 > Step 2 > Step 3"... // This function sets up the HTML for "Convert from SF2000 > Step 2 > Step 3"...
@ -222,19 +224,13 @@
if (match) { if (match) {
downloadName = match[1] + ".state"; downloadName = match[1] + ".state";
} }
downloadSaveState(); downloadToBrowser(saveData, "application/octet-stream", downloadName);
}); });
// We're nearly ready to wrap up Step 3; all that's left is to set up // The last thing to do here in Step 3 is to perform our initial image
// our initial preview canvas state, and perform our initial conversion // data conversion and preview rendering... so let's do it!
// and rendering... convertFromSF2000AndRender();
var canvas = document.getElementById("thumbnailPreview");
var context = canvas.getContext("2d");
canvas.width = thumbWidth;
canvas.height = thumbHeight;
convertImageFromSF2000();
renderImageToCanvas();
} }
// This function sets up the HTML for "Convert to SF2000 > Step 2 > Step 3"... // This function sets up the HTML for "Convert to SF2000 > Step 2 > Step 3"...
@ -305,7 +301,7 @@
userStateDownloadCheck(); userStateDownloadCheck();
// Display a message to the user and return... // Display a message to the user and return...
document.getElementById("step3Messages").innerHTML = "<p class=\"error\"><img class=\"icon\" alt=\"An icon of a red circle with a white exclamation point, indicating an error has occurred\" aria-hidden=\"true\" src=\"" + errorIcon + "\"> ERROR: The selected file does not appear to be a binary file, and therefore can't be a game ROM file; please make sure you're selecting a valid game ROM file.</p>"; setMessage("error", "step3Messages", "ERROR: The selected file does not appear to be a binary file, and therefore can't be a game ROM file; please make sure you're selecting a valid game ROM file.");
return; return;
} }
else { else {
@ -319,7 +315,7 @@
} }
} }
}) });
// Next up, our radio buttons... // Next up, our radio buttons...
var slots = document.getElementsByName("saveSlot"); var slots = document.getElementsByName("saveSlot");
@ -329,7 +325,7 @@
// state-checking function; we'll pull the actual data from the // state-checking function; we'll pull the actual data from the
// selected radio button at run time during the download process... // selected radio button at run time during the download process...
userStateDownloadCheck(); userStateDownloadCheck();
}) });
} }
// And last but not least, our Download button... // And last but not least, our Download button...
@ -345,13 +341,13 @@
// Oh! We failed to compress the save state data using the pako // Oh! We failed to compress the save state data using the pako
// library for some reason... display a message to the user and // library for some reason... display a message to the user and
// return... // return...
document.getElementById("step3Messages").innerHTML = "<p class=\"error\"><img class=\"icon\" alt=\"An icon of a red circle with a white exclamation point, indicating an error has occurred\" aria-hidden=\"true\" src=\"" + errorIcon + "\"> ERROR: Failed to compress the chosen save state file's data!</p>"; setMessage("error", "step3Messages", "ERROR: Failed to compress the chosen save state file's data!");
return; return;
} }
// We also need to check our compressed data is of an appropriate size... // We also need to check our compressed data is of an appropriate size...
if (compressedSaveState.length < 0 || compressedSaveState.length > 4294967295) { if (compressedSaveState.length < 0 || compressedSaveState.length > 4294967295) {
document.getElementById("step3Messages").innerHTML = "<p class=\"error\"><img class=\"icon\" alt=\"An icon of a red circle with a white exclamation point, indicating an error has occurred\" aria-hidden=\"true\" src=\"" + errorIcon + "\"> ERROR: Compressed save state data is not valid (either it was 0 bytes in length, or was larger than 4 GB)!</p>"; setMessage("error", "step3Messages", "ERROR: Compressed save state data is not valid (either it was 0 bytes in length, or was larger than 4 GB)!");
return false; return false;
} }
@ -424,7 +420,7 @@
// Just as a final sanity check, offset should now be equal to // Just as a final sanity check, offset should now be equal to
// saveData.length... let's confirm! // saveData.length... let's confirm!
if (offset !== saveData.length) { if (offset !== saveData.length) {
document.getElementById("step3Messages").innerHTML = "<p class=\"error\"><img class=\"icon\" alt=\"An icon of a red circle with a white exclamation point, indicating an error has occurred\" aria-hidden=\"true\" src=\"" + errorIcon + "\"> ERROR: Inconsistent data length when building save state bundle!</p>"; setMessage("error", "step3Messages", "ERROR: Inconsistent data length when building save state bundle!");
return false; return false;
} }
@ -444,7 +440,7 @@
downloadName = saveFileName + ".sa" + document.querySelector('input[name="saveSlot"]:checked').value; downloadName = saveFileName + ".sa" + document.querySelector('input[name="saveSlot"]:checked').value;
// Right, that should be everything we need... to the Downloadmobile! // Right, that should be everything we need... to the Downloadmobile!
downloadSaveState(); downloadToBrowser(saveData, "application/octet-stream", downloadName);
}); });
} }
@ -567,7 +563,6 @@
return true; return true;
} }
// This function runs through our binary save state data, and checks it for any // This function runs through our binary save state data, and checks it for any
// of Retroarch's save state headers. If it finds them, it strips them out... // of Retroarch's save state headers. If it finds them, it strips them out...
// See https://github.com/libretro/RetroArch/blob/master/tasks/task_save.c // See https://github.com/libretro/RetroArch/blob/master/tasks/task_save.c
@ -653,8 +648,7 @@
// both are true, it enables the download button, otherwise it makes sure // both are true, it enables the download button, otherwise it makes sure
// the button is disabled... // the button is disabled...
function userStateDownloadCheck() { function userStateDownloadCheck() {
if (romFileGood === true && document.querySelector('input[name="saveSlot"]:checked')) if (romFileGood === true && document.querySelector('input[name="saveSlot"]:checked')) {
{
// Looks good - enable Download button! // Looks good - enable Download button!
var dButton = document.getElementById("saveStateDownload"); var dButton = document.getElementById("saveStateDownload");
dButton.removeAttribute("disabled"); dButton.removeAttribute("disabled");
@ -666,96 +660,20 @@
} }
} }
// This function takes the SF2000-format binary image data stored in our // This function takes our global thumbData object (Uint8Array), converts
// thumbData global variable, and converts it to 8-bit RGB(A) format, // it to an ImageData object and assigns it to our previewCanvasData
// storing the result in our previewCanvasData global. This is then used // global; it then renders that ImageData to our preview canvas...
// for drawing the image data to our preview canvas... function convertFromSF2000AndRender() {
function convertImageFromSF2000() { previewCanvasData = rgb565ToImageData(thumbData, thumbWidth, thumbHeight);
// We're converting RGB565-little-endian data (no alpha); size our
// output buffer accordingly...
previewCanvasData = new Uint8Array((thumbData.length / 2) * 3);
offset = 0;
for (var i = 0; i < thumbData.length; i += 2) {
// Read in two bytes, representing one RGB565 pixel in little-endian format
var byte1 = thumbData[i];
var byte2 = thumbData[i + 1];
// Extract the red, green and blue components. The first five bits of byte2
// are red...
var red = (byte2 & 0b11111000) >> 3;
// ... the first three bits of byte1 and the last three bits of byte2 are
// green...
var green = ((byte1 & 0b11100000) >> 5) | ((byte2 & 0b00000111) << 3);
// ... and the last five bits of byte1 are blue...
var blue = byte1 & 0b00011111;
// These values are in 5-bit/6-bit ranges - we need to scale them to 8-bit
// ranges for the colours to look right...
red = Math.round(red * 255 / 31);
green = Math.round(green * 255 / 63);
blue = Math.round(blue * 255 / 31);
// Finally store the RGB components in-order in our rgb888 buffer...
previewCanvasData[offset] = red;
previewCanvasData[offset + 1] = green;
previewCanvasData[offset + 2] = blue;
offset += 3;
}
}
// This function takes the current stream of SF2000 image data and renders
// it to our preview canvas...
function renderImageToCanvas() {
// Get our preview canvas...
var canvas = document.getElementById("thumbnailPreview"); var canvas = document.getElementById("thumbnailPreview");
var context = canvas.getContext("2d"); var context = canvas.getContext("2d");
canvas.width = previewCanvasData.width;
// Now, let's start drawing stuff to our canvas; we'll loop through canvas.height = previewCanvasData.height;
// every row and column... context.clearRect(0, 0, canvas.width, canvas.height);
for (var y = 0; y < canvas.height; y++) { context.putImageData(previewCanvasData, 0, 0);
for (var x = 0; x < canvas.width; x++) {
// Calculate the offset of our current pixel within our input data
// stream...
var pixel = ((canvas.width * y) * 3) + (x * 3);
// Before we try and draw anything, let's make sure we actually have
// enough data in our stream to match this pixel location...
if (previewCanvasData.length >= (pixel + 2)) {
// OK, we're good - we've stream data for this pixel. Get its
// colour (and possibly alpha) values, and set them up for
// our next canvas draw call...
context.fillStyle = "rgb("+ previewCanvasData[pixel] +", " + previewCanvasData[pixel + 1] + ", " + previewCanvasData[pixel + 2] + ")";
}
else {
// Oops! We've reached the end of our data stream, but not the
// end of our canvas... so just set up a solid white pixel
// instead...
context.fillStyle = "rgb(255, 255, 255)";
}
// Draw the actual pixel to the canvas...
context.fillRect(x, y, 1, 1);
}
}
}
// This function takes our save state data (be it in raw, non-SF2000
// format, or in SF2000 save state bundle format) and sends it to the
// user's browser as a download...
function downloadSaveState() {
var link = document.createElement("a");
link.href = window.URL.createObjectURL(new Blob([saveData], {type: "application/octet-stream"}));
link.download = downloadName;
link.style.display = "none";
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(link.href);
document.body.removeChild(link);
} }
</script> </script>
<hr> <hr>
<p><a rel="license" href="https://creativecommons.org/publicdomain/zero/1.0/">CC0</a>: public domain. Version 1.1, 20230621.1</p> <p><a rel="license" href="https://creativecommons.org/publicdomain/zero/1.0/">CC0</a>: public domain. Version 1.2, 20230626.1</p>
</body> </body>
</html> </html>

646
tools/tools.js Normal file
View File

@ -0,0 +1,646 @@
/*
Von Millhausen's SF2000 Tools Shared JS Library
===============================================
There's a bit of overlap between what my SF2000 tools do (e.g., CRC32
recalculation is fairly common, etc.), and there's some general functions
that might be useful for future tools as well (e.g., `bisrv.asd` hash
checking), so rather than repeating code in each separate tool, any "shared"
functions will go here.
Just like the tools themselves, this file should be considered CC0 Public
Domain (https://creativecommons.org/publicdomain/zero/1.0/)
Version 1.0: Initial version
*/
// This function takes in a Uint8Array object, and patches bytes 0x18c to
// 0x18f with an updated CRC32 calculated on bytes 512 to the end of the
// array. Credit to `bnister` for this code!
function patchCRC32(data) {
var c;
var tabCRC32 = new Int32Array(256);
for (var i = 0; i < 256; i++) {
c = i << 24;
for (var j = 0; j < 8; j++) {
c = c & (1 << 31) ? c << 1 ^ 0x4c11db7 : c << 1;
}
tabCRC32[i] = c;
}
c = ~0;
for (var i = 512; i < data.length; i++) {
c = c << 8 ^ tabCRC32[c >>> 24 ^ data[i]];
}
data[0x18c] = c & 255;
data[0x18d] = c >>> 8 & 255;
data[0x18e] = c >>> 16 & 255;
data[0x18f] = c >>> 24;
}
// Returns an SHA-256 hash of a given firmware (ignoring common user changes),
// or returns false on failure...
function getFirmwareHash(data) {
// Data should be a Uint8Array, which as an object is passed by reference...
// we're going to be manipulating that data before generating our hash, but we
// don't want to modify the original object at all... so we'll create a copy,
// and work only on the copy...
var dataCopy = data.slice();
// Only really worthwhile doing this for big bisrv.asd files...
if (dataCopy.length > 12640000) {
// First, replace CRC32 bits with 00...
dataCopy[396] = 0x00;
dataCopy[397] = 0x00;
dataCopy[398] = 0x00;
dataCopy[399] = 0x00;
// Next identify the boot logo position, and blank it out too...
var badExceptionOffset = findSequence([0x62, 0x61, 0x64, 0x5F, 0x65, 0x78, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x00, 0x00, 0x00], dataCopy);
if (badExceptionOffset > -1) {
var bootLogoStart = badExceptionOffset + 16;
for (var i = bootLogoStart; i < (bootLogoStart + 204800); i++) {
dataCopy[i] = 0x00;
}
}
else {
return false;
}
// Next identify the emulator button mappings (if they exist), and blank
// them out too...
var preButtonMapOffset = findSequence([0x00, 0x00, 0x00, 0x71, 0xDB, 0x8E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], dataCopy);
if (preButtonMapOffset > -1) {
var postButtonMapOffset = findSequence([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00], dataCopy, preButtonMapOffset);
if (postButtonMapOffset > -1) {
for (var i = preButtonMapOffset + 16; i < postButtonMapOffset; i++) {
dataCopy[i] = 0x00;
}
}
else {
return false;
}
}
else {
return false;
}
// If we're here, we've zeroed-out all of the bits of the firmware that are
// semi-user modifiable (boot logo, button mappings and the CRC32 bits); now
// we can generate a hash of what's left and compare it against some known
// values...
return crypto.subtle.digest("SHA-256", dataCopy.buffer)
.then(function(digest) {
var array = Array.from(new Uint8Array(digest));
var hash = array.map(byte => ("00" + byte.toString(16)).slice(-2)).join("");
return hash;
})
.catch(function(error) {
return false;
});
}
else {
return false;
}
}
// This function searches for array needle in array haystack starting at offset
// and returns the zero-based index of the first match found, or -1 if not
// found...
function findSequence(needle, haystack, offset) {
// If offset is not provided, default to 0...
offset = offset || 0;
// Loop through the haystack array starting from the offset...
for (var i = offset; i < haystack.length - needle.length + 1; i++) {
// Assume a match until proven otherwise...
var match = true;
// Loop through the needle array and compare each byte...
for (var j = 0; j < needle.length; j++) {
if (haystack[i + j] !== needle[j]) {
// Mismatch found, break the inner loop and continue the outer loop...
match = false;
break;
}
}
// If match is still true after the inner loop, we have found a match;
// return the index of the start of the match...
if (match) {
return i;
}
}
// If we reach this point, no match was found...
return -1;
}
// Generic download function, for sending data to the user's browser as a
// download...
function downloadToBrowser(data, type, name) {
// Send the data to the user's browser as a file download...
var link = document.createElement("a");
link.href = window.URL.createObjectURL(new Blob([data], {type: type}));
link.download = name;
link.style.display = "none";
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(link.href);
document.body.removeChild(link);
}
// This simple function takes in a string representing an SHA1 hash, and
// compares it against the known hashes my tools use for the various stock
// firmware versions; returns a string with my internal "MM.DD" firmware naming
// convention, or false if the provided hash doesn't match...
function knownHash(hash) {
switch (hash) {
case "4411143d3030adc442e99f7ac4e7772f300c844bbe10d639702fb3ba049a4ee1":
return "03.15";
case "b50e50aa4b1b1d41489586e989f09d47c4e2bc27c072cb0112f83e6bc04e2cca":
return "04.20";
case "d878a99d26242836178b452814e916bef532d05acfcc24d71baa31b8b6f38ffd":
return "05.15";
case "6aebab0e4da39e0a997df255ad6a1bd12fdd356cdf51a85c614d47109a0d7d07":
return "05.22";
default:
return false;
}
}
// Takes in an ImageData object, and returns a Uint8Array object containing the
// data in little-endian RGB565 format...
function imageDataToRgb565(input) {
// Loop through the image data, and convert it to little-endian RGB565. First,
// we'll store the raw RGB565-converted integers in an array, one entry per
// pixel...
var intArray = [];
var pixelCount = 0;
for (var i = 0; i < input.data.length; i += 4){
// Read in the raw source RGB colours from the image data stream...
var red = input.data[i];
var green = input.data[i+1];
var blue = input.data[i+2];
// Use some shifting and masking to get a big-endian version of the RGB565
// colour and store it in our array before moving on...
intArray[pixelCount] = ((red & 248)<<8) + ((green & 252)<<3) + (blue>>3);
pixelCount++;
}
// Create a data buffer and a data view; we'll use the view to convert our int
// array data to little-endian format (the "true" below) to be stored in the
// buffer...
var buffer = new ArrayBuffer(intArray.length * 2);
var dataView = new DataView(buffer);
for (var i = 0; i < intArray.length; i++) {
dataView.setInt16(i * 2, intArray[i], true);
}
// Use the buffer to fill a Uint8Array, which we'll return...
return new Uint8Array(buffer);
}
// Takes in an ImageData object, and returns a Uint8Array object containing the
// data in BGRA format...
function imageDataToBgra(input) {
// This is pretty simple - we just loop through the input data (which is in
// RGBA format), and swap the Red and Blue channels to output BGRA instead...
output = new Uint8Array(input.data.length);
for (var i = 0; i < input.data.length; i += 4) {
output[i] = input.data[i + 2];
output[i + 1] = input.data[i + 1];
output[i + 2] = input.data[i];
output[i + 3] = input.data[i + 3];
}
return output;
}
// Takes in a Uint8Array object containing little-endian RGB565 image data, a
// width and a height, and outputs an ImageData object...
function rgb565ToImageData(input, width, height) {
// Create an output ImageData object of the specified dimensions; it'll
// default to transparent black, but we'll fill it with our input data...
output = new ImageData(width, height);
outputIndex = 0;
for (var i = 0; i < input.length; i += 2) {
// Check to make sure we haven't run out of space in our output buffer...
if (outputIndex < output.data.length) {
// Read in two bytes, representing one RGB565 pixel in little-endian
// format...
var byte1 = input[i];
var byte2 = input[i + 1];
// Extract the red, green and blue components from them. The first five
// bits of byte2 are red, the first three bits of byte1 and the last
// three bits of byte 2 are green, and the last five bits of byte1 are
// blue...
var red = (byte2 & 0b11111000) >> 3;
var green = ((byte1 & 0b11100000) >> 5) | ((byte2 & 0b00000111) << 3);
var blue = byte1 & 0b00011111;
// These values are in 5-bit/6-bit ranges; we need to scale them to 8-bit
// ranges for the colours to look right...
red = Math.round(red * 255 / 31);
green = Math.round(green * 255 / 63);
blue = Math.round(blue * 255 / 31);
// Finally, store the RGB values in our ImageData's data array, being
// sure to set the A component to 255...
output.data[outputIndex] = red;
output.data[outputIndex + 1] = green;
output.data[outputIndex + 2] = blue;
output.data[outputIndex + 3] = 255;
outputIndex += 4;
}
else {
// Oops, we've run out of room in our output data buffer; no point in
// trying to process any more input data!
break;
}
}
// If we've run out of input data, but haven't reached the end of our
// ImageData object's buffer, fill the rest of that buffer with white...
while (outputIndex / 4 < width * height) {
output.data[outputIndex] = 255;
output.data[outputIndex + 1] = 255;
output.data[outputIndex + 2] = 255;
output.data[outputIndex + 3] = 255;
outputIndex += 4;
}
// Finally, return our ImageData object...
return output;
}
// Takes in a Uint8Array object containing raw BGRA image data, a width and a
// height, and outputs an ImageData object...
function bgraToImageData(input, width, height) {
// Create an output ImageData object of the specified dimensions; it'll
// default to transparent black, but we'll fill it with our input data...
output = new ImageData(width, height);
outputIndex = 0;
for (var i = 0; i < input.length; i += 4) {
// Check to make sure we haven't run out of space in our output buffer...
if (outputIndex < output.data.length) {
// The input data is *nearly* RGBA as it is - it's just that the R and B
// colour channels are swapped... so, just swap them back!
output.data[i] = input[i + 2];
output.data[i + 1] = input[i + 1];
output.data[i + 2] = input[i];
output.data[i + 3] = input[i + 3];
outputIndex += 4;
}
else {
// Oops, we've run out of room in our output data buffer; no point in
// trying to process any more input data!
break;
}
}
// If we've run out of input data, but haven't reached the end of our
// ImageData object's buffer, fill the rest of that buffer with white...
while (outputIndex / 4 < width * height) {
output.data[outputIndex] = 255;
output.data[outputIndex + 1] = 255;
output.data[outputIndex + 2] = 255;
output.data[outputIndex + 3] = 255;
outputIndex += 4;
}
// Finally, return our ImageData object...
return output;
}
// This function takes in a Javascript Image object, and outputs it as an
// ImageData object instead...
function imageToImageData(image) {
// Create a virtual canvas, and load it up with our image file...
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
canvas.width = image.width;
canvas.height = image.height;
// Draw our image to the canvas, which will allow us to get data about the
// image...
context.drawImage(image, 0, 0, image.width, image.height);
var imageData = context.getImageData(0, 0, image.width, image.height);
// Return the ImageData object...
return imageData;
}
// This function takes in an ImageData object, and returns a new ImageData
// object containing a scaled version of the original image. The new image's
// width, height and scaling method are also specified...
function scaleImage(input, newWidth, newHeight, method) {
// Utility function which takes in an ImageData object, and returns
// a (partially) upscaled version using bilinear filtering...
function _bilinear(imageData, newWidth, newHeight) {
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
canvas.width = imageData.width;
canvas.height = imageData.height;
context.putImageData(imageData, 0, 0);
var outCanvas = document.createElement("canvas");
var outContext = outCanvas.getContext("2d");
outContext.imageSmoothingEnabled = true;
outContext.imageSmoothingQuality = "high";
outCanvas.width = newWidth;
outCanvas.height = newHeight;
outContext.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, newWidth, newHeight);
return outContext.getImageData(0, 0, newWidth, newHeight);
}
// Utility function which takes in an ImageData object, and returns
// a (partially) downscaled version using gaussian resampling...
// [Side note: I asked Bing AI for this function; ain't technology grand!]
function _gaussian(imageData, newWidth, newHeight) {
// Get the original width and height of the image data
let oldWidth = imageData.width;
let oldHeight = imageData.height;
// Get the pixel data array of the image data
let oldData = imageData.data;
// Create a new pixel data array for the scaled image data
let newData = new Uint8ClampedArray(newWidth * newHeight * 4);
// Calculate the scaling factor along each axis
let scaleX = newWidth / oldWidth;
let scaleY = newHeight / oldHeight;
// Calculate the radius of the Gaussian kernel based on the scaling factor
let radiusX = Math.ceil(1 / scaleX);
let radiusY = Math.ceil(1 / scaleY);
// Calculate the size of the Gaussian kernel along each axis
let sizeX = radiusX * 2 + 1;
let sizeY = radiusY * 2 + 1;
// Create a Gaussian kernel array
let kernel = new Float32Array(sizeX * sizeY);
// Calculate the standard deviation of the Gaussian distribution based on
// the radius
let sigmaX = radiusX / 3;
let sigmaY = radiusY / 3;
// Calculate the inverse of the variance of the Gaussian distribution along
// each axis
let invVarX = 1 / (2 * sigmaX * sigmaX);
let invVarY = 1 / (2 * sigmaY * sigmaY);
// Calculate the normalization factor for the Gaussian kernel
let norm = Math.sqrt(2 * Math.PI * sigmaX * sigmaY);
// Loop through each element in the Gaussian kernel array
for (let ky = -radiusY; ky <= radiusY; ky++) {
for (let kx = -radiusX; kx <= radiusX; kx++) {
// Calculate the index of the element in the Gaussian kernel array
let k = (ky + radiusY) * sizeX + (kx + radiusX);
// Calculate the value of the element using the Gaussian formula
kernel[k] = Math.exp(-(kx * kx) * invVarX - (ky * ky) * invVarY) / norm;
}
}
// Loop through each pixel in the new image data
for (let y = 0; y < newHeight; y++) {
for (let x = 0; x < newWidth; x++) {
// Calculate the corresponding coordinates in the old image data
let oldX = x / scaleX;
let oldY = y / scaleY;
// Initialize the RGBA values of the pixel to zero
let r7 = 0;
let g7 = 0;
let b7 = 0;
let a7 = 0;
// Initialize the sum of the kernel values to zero
let sum = 0;
// Loop through each element in the Gaussian kernel array
for (let ky = -radiusY; ky <= radiusY; ky++) {
for (let kx = -radiusX; kx <= radiusX; kx++) {
// Calculate the index of the element in the Gaussian kernel array
let k = (ky + radiusY) * sizeX + (kx + radiusX);
// Get the value of the element in the Gaussian kernel array
let w = kernel[k];
// Calculate the coordinates of the pixel in the old image data that
// corresponds to this element
let x1 = Math.round(oldX + kx);
let y1 = Math.round(oldY + ky);
// Clamp the coordinates to the valid range
x1 = Math.max(0, Math.min(x1, oldWidth - 1));
y1 = Math.max(0, Math.min(y1, oldHeight - 1));
// Get the index of the pixel in the old pixel data array
let i1 = (y1 * oldWidth + x1) * 4;
// Get the RGBA values of the pixel in the old pixel data array
let r1 = oldData[i1];
let g1 = oldData[i1 + 1];
let b1 = oldData[i1 + 2];
let a1 = oldData[i1 + 3];
// Multiply the RGBA values by the kernel value and add them to the
// pixel values
r7 += r1 * w;
g7 += g1 * w;
b7 += b1 * w;
a7 += a1 * w;
// Add the kernel value to the sum
sum += w;
}
}
// Divide the RGBA values by the sum to get an average value
r7 /= sum;
g7 /= sum;
b7 /= sum;
a7 /= sum;
// Round the RGBA values to integers
r7 = Math.round(r7);
g7 = Math.round(g7);
b7 = Math.round(b7);
a7 = Math.round(a7);
// Get the index of the pixel in the new pixel data array
let j = (y * newWidth + x) * 4;
// Set the RGBA values of the pixel in the new pixel data array
newData[j] = r7;
newData[j + 1] = g7;
newData[j + 2] = b7;
newData[j + 3] = a7;
}
}
// Create and return a new ImageData object with the new pixel data array
// and dimensions
return new ImageData(newData, newWidth, newHeight);
}
// Get the original width and height...
var width = input.width;
var height = input.height;
// Before we consider doing *any* scaling, let's check to make sure the new
// dimensions are different from the old ones; if they're not, there's no
// point in doing any scaling!
if (width == newWidth && height == newHeight) {
return input;
}
// If we're here, then we're really scaling; the process to follow is
// *heavily* dependent upon the provided method, so let's switch based on
// that...
switch (method) {
// If the method is "Nearest Neighbour"...
case "Nearest Neighbour":
// Create a new canvas element to draw the scaled image (we'll use the
// canvas to get our output ImageData object)...
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
// Set the canvas size to the new dimensions...
canvas.width = newWidth;
canvas.height = newHeight;
// Create a new image data object to store the scaled pixel data...
var scaledData = context.createImageData(newWidth, newHeight);
// Loop through each pixel of the new image...
for (var y = 0; y < newHeight; y++) {
for (var x = 0; x < newWidth; x++) {
// Calculate the index of the new pixel in the scaled data array...
var index = (y * newWidth + x) * 4;
// Calculate the x and y coordinates of the corresponding pixel in
// the original image...
var x2 = Math.floor(x * width / newWidth);
var y2 = Math.floor(y * height / newHeight);
// Calculate the index of the original pixel in the data array...
var index2 = (y2 * width + x2) * 4;
// Copy the color values from the original pixel to the new pixel...
scaledData.data[index] = input.data[index2]; // Red
scaledData.data[index + 1] = input.data[index2 + 1]; // Green
scaledData.data[index + 2] = input.data[index2 + 2]; // Blue
scaledData.data[index + 3] = input.data[index2 + 3]; // Alpha
}
}
// Finally, return the scaled ImageData object...
return scaledData;
// If the method is "Bilinear"...
case "Bilinear":
// OK, "Bilinear" is a bit of a lie... bilinear filtering is fine when
// you're *upscaling* an image, but if you're *downscaling* an image
// by more than half the original's width/height, then true bilinear
// filtering creates just as much of an aliased mess as nearest
// neighbour filtering. Most image editing apps therefore cheat and
// use a resampling algorithm when downscaling, and bilinear filtering
// when upscaling... so that's what we're going to do here too! We'll
// use gaussian resampling for any image axis that's being downscaled,
// and bilinear for any axis that's being upscaled; this should give
// the user a result that's much closer to what they'd expect to see...
// Let's see which kind of scaling scenario we're in...
if (newWidth > width && newHeight > height) {
// All dimensions being upscaled, so we'll use bilinear filtering for
// everything...
return _bilinear(input, newWidth, newHeight);
}
else if (newWidth < width && newHeight < height) {
// All dimensions being downscaled, so we'll use gaussian resampling
// for everything...
return _gaussian(input, newWidth, newHeight);
}
else {
// It's a mix!
if (newWidth < width) {
// Gaussian for width, bilinear for height...
let partial = _gaussian(input, newWidth, height);
return _bilinear(partial, newWidth, newHeight);
}
else if (newHeight < height) {
// Gaussian for height, bilinear for width...
let partial = _gaussian(input, width, newHeight);
return _bilinear(partial, newWidth, newHeight);
}
}
break;
}
}
// This utility function is used for adding info, warning and error messages to
// the appropriate spots in my tools. It uses custom-modified versions of the
// SVG Famfamfam Silk icon set via https://github.com/frhun/silk-icon-scalable
function setMessage(type, divID, text) {
var icon;
switch(type) {
case "info":
icon = '<img class="icon" alt="An icon of a blue circle with a white lowercase letter I, indicating an informative message" aria-hidden="true" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJyB2aWV3Qm94PScwIDAgNjQgNjQnPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0nYSc+PHN0b3Agb2Zmc2V0PScwJyBzdG9wLWNvbG9yPScjNDk5MWQyJyBzdG9wLW9wYWNpdHk9Jy45Jy8+PHN0b3Agb2Zmc2V0PScxJyBzdG9wLWNvbG9yPScjMmU0NjgxJyBzdG9wLW9wYWNpdHk9Jy45NicvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IHhsaW5rOmhyZWY9JyNhJyBpZD0nYycgeDE9JzEyJyB4Mj0nNTInIHkxPScxMicgeTI9JzUyJyBncmFkaWVudFVuaXRzPSd1c2VyU3BhY2VPblVzZScvPjxsaW5lYXJHcmFkaWVudCBpZD0nYic+PHN0b3Agb2Zmc2V0PScwJyBzdG9wLWNvbG9yPScjN2RhOWQ2Jy8+PHN0b3Agb2Zmc2V0PScxJyBzdG9wLWNvbG9yPScjNWU4NWIzJy8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgeGxpbms6aHJlZj0nI2InIGlkPSdkJyB4MT0nMTYnIHgyPSc0OCcgeTE9JzE2JyB5Mj0nNDgnIGdyYWRpZW50VW5pdHM9J3VzZXJTcGFjZU9uVXNlJy8+PC9kZWZzPjxnIHBhaW50LW9yZGVyPSdtYXJrZXJzIHN0cm9rZSBmaWxsJz48Y2lyY2xlIGN4PSczMicgY3k9JzMyJyByPScyOCcgZmlsbD0ndXJsKCNjKScvPjxjaXJjbGUgY3g9JzMyJyBjeT0nMzInIHI9JzI0JyBmaWxsPScjYWVjNWUzJy8+PGNpcmNsZSBjeD0nMzInIGN5PSczMicgcj0nMjAnIGZpbGw9J3VybCgjZCknLz48cGF0aCBmaWxsPScjZmZmJyBkPSdNMjYgNDB2NGgxMnYtNGgtM1YyOGgtOHY0aDJ2OHpNMzUgMTloLTZ2Nmg2eicvPjwvZz48L3N2Zz4=">';
break;
case "warning":
icon = '<img class="icon" alt="An icon of a yellow triangle with a dark orange exclamation point, indicating a warning or notice" aria-hidden="true" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJyB2aWV3Qm94PScwIDAgNjQgNjQnPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0nYSc+PHN0b3Agb2Zmc2V0PScwJyBzdG9wLWNvbG9yPScjZTliYjNhJy8+PHN0b3Agb2Zmc2V0PScxJyBzdG9wLWNvbG9yPScjYzE2ODAzJy8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgeGxpbms6aHJlZj0nI2EnIGlkPSdiJyB4MT0nMjAnIHgyPSc1NicgeTE9JzEyJyB5Mj0nNjAnIGdyYWRpZW50VW5pdHM9J3VzZXJTcGFjZU9uVXNlJy8+PC9kZWZzPjxwYXRoIGZpbGw9JyNmZmYnIHN0cm9rZT0ndXJsKCNiKScgc3Ryb2tlLWxpbmVqb2luPSdyb3VuZCcgc3Ryb2tlLXdpZHRoPSc4JyBkPSdNNCA1Nmg1NkwzMiA4WicgcGFpbnQtb3JkZXI9J3N0cm9rZSBtYXJrZXJzIGZpbGwnLz48cGF0aCBmaWxsPScjZjVkODUyJyBkPSdNMTIgNTJoNDBMMzIgMThaJyBwYWludC1vcmRlcj0nbWFya2VycyBmaWxsIHN0cm9rZScvPjxwYXRoIGZpbGw9JyNjNTg3MTEnIGQ9J00yOSA0M3Y2aDZ2LTZ6TTI5IDQwaDZWMjZoLTZ6JyBwYWludC1vcmRlcj0nc3Ryb2tlIG1hcmtlcnMgZmlsbCcvPjwvc3ZnPg==">';
break;
case "error":
icon = '<img class="icon" alt="An icon of a red circle with a white exclamation point, indicating an error has occurred" aria-hidden="true" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJyB2aWV3Qm94PScwIDAgNjQgNjQnPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0nYic+PHN0b3Agb2Zmc2V0PScwJyBzdG9wLWNvbG9yPScjZjU4MTYyJy8+PHN0b3Agb2Zmc2V0PScxJyBzdG9wLWNvbG9yPScjZTI1ZjUzJy8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9J2EnPjxzdG9wIG9mZnNldD0nMCcgc3RvcC1jb2xvcj0nI2VjODE2YycvPjxzdG9wIG9mZnNldD0nMScgc3RvcC1jb2xvcj0nI2JmNDMyOScvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IHhsaW5rOmhyZWY9JyNhJyBpZD0nYycgeDE9JzEyJyB4Mj0nNTInIHkxPScxMicgeTI9JzUyJyBncmFkaWVudFVuaXRzPSd1c2VyU3BhY2VPblVzZScvPjxsaW5lYXJHcmFkaWVudCB4bGluazpocmVmPScjYicgaWQ9J2QnIHgxPScyMCcgeDI9JzQ4JyB5MT0nMTYnIHkyPSc0NCcgZ3JhZGllbnRVbml0cz0ndXNlclNwYWNlT25Vc2UnLz48L2RlZnM+PGNpcmNsZSBjeD0nMzInIGN5PSczMicgcj0nMjgnIGZpbGw9J3VybCgjYyknIHBhaW50LW9yZGVyPSdtYXJrZXJzIHN0cm9rZSBmaWxsJy8+PGNpcmNsZSBjeD0nMzInIGN5PSczMicgcj0nMjQnIGZpbGw9JyNmOWQzY2MnIHBhaW50LW9yZGVyPSdtYXJrZXJzIHN0cm9rZSBmaWxsJy8+PGNpcmNsZSBjeD0nMzInIGN5PSczMicgcj0nMjAnIGZpbGw9J3VybCgjZCknIHBhaW50LW9yZGVyPSdtYXJrZXJzIHN0cm9rZSBmaWxsJy8+PHBhdGggZmlsbD0nI2ZmZicgZD0nTTI5IDE2djIwaDZWMTZaTTI5IDQwdjZoNnYtNnonIHBhaW50LW9yZGVyPSdmaWxsIG1hcmtlcnMgc3Ryb2tlJy8+PC9zdmc+">';
break;
}
document.getElementById(divID).innerHTML = "<p class=\"" + type + "\">" + icon + " " + text + "</p>";
return;
}